forked from mirrors/gecko-dev
Backed out 2 changesets (bug 1720941) for web-platform tests failures. CLOSED TREE
Backed out changeset f7fed5fde8b7 (bug 1720941) Backed out changeset a211d76a5e01 (bug 1720941)
This commit is contained in:
parent
3cfa044456
commit
703b1846fd
1238 changed files with 7 additions and 348780 deletions
|
|
@ -171,7 +171,6 @@ _OPT\.OBJ/
|
||||||
^servo/ports/geckolib/target/
|
^servo/ports/geckolib/target/
|
||||||
^dom/base/rust/target/
|
^dom/base/rust/target/
|
||||||
^servo/components/style/target/
|
^servo/components/style/target/
|
||||||
^dom/webgpu/tests/cts/vendor/target/
|
|
||||||
|
|
||||||
# Ignore mozharness execution files
|
# Ignore mozharness execution files
|
||||||
^testing/mozharness/.tox/
|
^testing/mozharness/.tox/
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# WebGPU CTS vendor checkout
|
|
||||||
|
|
||||||
This directory contains the following:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
.
|
|
||||||
├── README.md # You are here!
|
|
||||||
├── arguments.txt # Used by `vendor/`
|
|
||||||
├── checkout/ # Our vendored copy of WebGPU CTS
|
|
||||||
├── myexpectations.txt # Used by `vendor/`
|
|
||||||
└── vendor/ # Rust binary crate for updating `checkout/` and generating WPT tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## Re-vendoring
|
|
||||||
|
|
||||||
You can re-vendor by running the Rust binary crate from its Cargo project root. Change your working
|
|
||||||
directory to `vendor/` and invoke `cargo run -- --help` for more details.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
?q=
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const resolve = require('resolve')
|
|
||||||
|
|
||||||
// Implements the following resolver spec:
|
|
||||||
// https://github.com/benmosher/eslint-plugin-import/blob/master/resolvers/README.md
|
|
||||||
exports.interfaceVersion = 2
|
|
||||||
|
|
||||||
exports.resolve = function (source, file, config) {
|
|
||||||
if (resolve.isCore(source)) return { found: true, path: null }
|
|
||||||
|
|
||||||
source = source.replace(/\.js$/, '.ts');
|
|
||||||
try {
|
|
||||||
return {
|
|
||||||
found: true, path: resolve.sync(source, {
|
|
||||||
extensions: [],
|
|
||||||
basedir: path.dirname(path.resolve(file)),
|
|
||||||
...config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return { found: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
/src/external/*
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": { "project": "./tsconfig.json" },
|
|
||||||
"extends": [
|
|
||||||
"./node_modules/gts",
|
|
||||||
"plugin:import/errors",
|
|
||||||
"plugin:import/warnings",
|
|
||||||
"plugin:import/typescript"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"plugins": ["node", "ban", "import", "deprecation"],
|
|
||||||
"rules": {
|
|
||||||
// Core rules
|
|
||||||
"linebreak-style": ["warn", "unix"],
|
|
||||||
"no-console": "warn",
|
|
||||||
"no-undef": "off",
|
|
||||||
"no-useless-rename": "warn",
|
|
||||||
"object-shorthand": "warn",
|
|
||||||
"quotes": ["warn", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
|
|
||||||
|
|
||||||
// All test TODOs must be tracked inside file/test descriptions or READMEs.
|
|
||||||
// Comments relating to TODOs in descriptions can be marked with references like "[1]".
|
|
||||||
// TODOs not relating to test coverage can be marked MAINTENANCE_TODO or similar.
|
|
||||||
"no-warning-comments": ["warn", { "terms": ["todo", "fixme", "xxx"], "location": "anywhere" }],
|
|
||||||
|
|
||||||
// Plugin: @typescript-eslint
|
|
||||||
"@typescript-eslint/no-inferrable-types": "off",
|
|
||||||
"@typescript-eslint/consistent-type-assertions": "warn",
|
|
||||||
// Recommended lints
|
|
||||||
// https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/README.md
|
|
||||||
"@typescript-eslint/adjacent-overload-signatures": "warn",
|
|
||||||
"@typescript-eslint/await-thenable": "warn",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "warn",
|
|
||||||
"@typescript-eslint/no-empty-interface": "warn",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"@typescript-eslint/no-extra-non-null-assertion": "warn",
|
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
|
||||||
"@typescript-eslint/no-for-in-array": "warn",
|
|
||||||
"@typescript-eslint/no-misused-new": "warn",
|
|
||||||
"@typescript-eslint/no-namespace": "warn",
|
|
||||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "warn",
|
|
||||||
"@typescript-eslint/no-this-alias": "warn",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
|
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { "vars": "all", "args": "none" }],
|
|
||||||
"@typescript-eslint/prefer-as-const": "warn",
|
|
||||||
"@typescript-eslint/prefer-for-of": "warn",
|
|
||||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
|
||||||
"@typescript-eslint/restrict-plus-operands": "warn",
|
|
||||||
"@typescript-eslint/triple-slash-reference": "warn",
|
|
||||||
"@typescript-eslint/unbound-method": "warn",
|
|
||||||
// MAINTENANCE_TODO: Try to clean up and enable these recommended lints?
|
|
||||||
//"@typescript-eslint/no-unsafe-argument": "warn",
|
|
||||||
//"@typescript-eslint/no-unsafe-assignment": "warn",
|
|
||||||
//"@typescript-eslint/no-unsafe-call": "warn",
|
|
||||||
//"@typescript-eslint/no-unsafe-member-access": "warn",
|
|
||||||
//"@typescript-eslint/no-unsafe-return": "warn",
|
|
||||||
// Note: These recommended lints are probably not practical to enable.
|
|
||||||
//"@typescript-eslint/no-misused-promises": "warn",
|
|
||||||
//"@typescript-eslint/no-non-null-assertion": "warn",
|
|
||||||
//"@typescript-eslint/no-var-requires": "warn",
|
|
||||||
//"@typescript-eslint/restrict-template-expressions": "warn",
|
|
||||||
|
|
||||||
// Plugin: ban
|
|
||||||
"ban/ban": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"name": "setTimeout",
|
|
||||||
"message": "WPT disallows setTimeout; use `common/util/timeout.js`."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
// Plugin: deprecation
|
|
||||||
//"deprecation/deprecation": "warn",
|
|
||||||
|
|
||||||
// Plugin: import
|
|
||||||
"import/order": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
|
|
||||||
"newlines-between": "always",
|
|
||||||
"alphabetize": { "order": "asc", "caseInsensitive": false }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"import/newline-after-import": ["warn", { "count": 1 }],
|
|
||||||
"import/no-duplicates": "warn",
|
|
||||||
"import/no-restricted-paths": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"zones": [
|
|
||||||
{
|
|
||||||
"target": "./src/webgpu",
|
|
||||||
"from": "./src/common",
|
|
||||||
"except": ["./framework", "./util"],
|
|
||||||
"message": "Non-framework common/ code imported from webgpu/ suite"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "./src/unittests",
|
|
||||||
"from": "./src/common",
|
|
||||||
"except": ["./framework", "./util", "./internal"],
|
|
||||||
"message": "Non-framework common/ code imported from unittests/ suite"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "./src/webgpu",
|
|
||||||
"from": "./src/unittests",
|
|
||||||
"message": "unittests/ suite imported from webgpu/ suite"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"target": "./src/common",
|
|
||||||
"from": "./src",
|
|
||||||
"except": ["./common", "./external"],
|
|
||||||
"message": "Non common/ code imported from common/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"settings": {
|
|
||||||
"import/resolver": {
|
|
||||||
"./.eslint-resolver": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1
dom/webgpu/tests/cts/checkout/.gitattributes
vendored
1
dom/webgpu/tests/cts/checkout/.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Issue: #<!-- Fill in the issue number here. See docs/intro/life_of.md -->
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
**Requirements for PR author:**
|
|
||||||
|
|
||||||
- [ ] All missing test coverage is tracked with "TODO" or `.unimplemented()`.
|
|
||||||
- [ ] New helpers are `/** documented */` and new helper files are found in `helper_index.txt`.
|
|
||||||
- [ ] Test behaves as expected in a WebGPU implementation. (If not passing, explain above.)
|
|
||||||
|
|
||||||
**Requirements for [reviewer sign-off](https://github.com/gpuweb/cts/blob/main/docs/reviews.md):**
|
|
||||||
|
|
||||||
- [ ] Tests are properly located in the test tree.
|
|
||||||
- [ ] [Test descriptions](https://github.com/gpuweb/cts/blob/main/docs/intro/plans.md) allow a reader to "read only the test plans and evaluate coverage completeness", and accurately reflect the test code.
|
|
||||||
- [ ] Tests provide complete coverage (including validation control cases). **Missing coverage MUST be covered by TODOs.**
|
|
||||||
- [ ] Helpers and types promote readability and maintainability.
|
|
||||||
|
|
||||||
When landing this PR, be sure to make any necessary issue status updates.
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
name: Pull Request CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2.3.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- run: |
|
|
||||||
git fetch origin ${{ github.event.pull_request.head.sha }}
|
|
||||||
git checkout ${{ github.event.pull_request.head.sha }}
|
|
||||||
- uses: actions/setup-node@v2-beta
|
|
||||||
with:
|
|
||||||
node-version: "15.x"
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm test
|
|
||||||
- run: |
|
|
||||||
mkdir deploy-build/
|
|
||||||
cp -r README.md src standalone out docs deploy-build/
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: pr-artifact
|
|
||||||
path: deploy-build/
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
name: Push CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2.3.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: actions/setup-node@v2-beta
|
|
||||||
with:
|
|
||||||
node-version: "15.x"
|
|
||||||
- run: npm ci
|
|
||||||
- run: |
|
|
||||||
npm test
|
|
||||||
mkdir deploy-build/
|
|
||||||
cp -r README.md src standalone out out-wpt docs tools deploy-build/
|
|
||||||
- uses: JamesIves/github-pages-deploy-action@4.1.4
|
|
||||||
with:
|
|
||||||
BRANCH: gh-pages
|
|
||||||
FOLDER: deploy-build
|
|
||||||
CLEAN: true
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
name: Workflow CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows:
|
|
||||||
- "Pull Request CI"
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2.3.1
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
- run: |
|
|
||||||
PR=$(curl https://api.github.com/search/issues?q=${{ github.event.workflow_run.head_sha }} |
|
|
||||||
grep -Po "(?<=${{ github.event.workflow_run.repository.full_name }}\/pulls\/)\d*" | head -1)
|
|
||||||
echo "PR=$PR" >> $GITHUB_ENV
|
|
||||||
- uses: actions/github-script@v3
|
|
||||||
id: pr-artifact
|
|
||||||
with:
|
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
result-encoding: string
|
|
||||||
script: |
|
|
||||||
const artifacts_url = context.payload.workflow_run.artifacts_url
|
|
||||||
const artifacts_req = await github.request(artifacts_url)
|
|
||||||
const artifact = artifacts_req.data.artifacts[0]
|
|
||||||
const download = await github.actions.downloadArtifact({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
artifact_id: artifact.id,
|
|
||||||
archive_format: "zip"
|
|
||||||
})
|
|
||||||
return download.url
|
|
||||||
- run: |
|
|
||||||
rm -rf *
|
|
||||||
curl -L -o "pr-artifact.zip" "${{ steps.pr-artifact.outputs.result }}"
|
|
||||||
unzip -o pr-artifact.zip
|
|
||||||
rm pr-artifact.zip
|
|
||||||
- run: |
|
|
||||||
cat << EOF >> firebase.json
|
|
||||||
{
|
|
||||||
"hosting": {
|
|
||||||
"public": ".",
|
|
||||||
"ignore": [
|
|
||||||
"firebase.json",
|
|
||||||
"**/.*",
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
cat << EOF >> .firebaserc
|
|
||||||
{
|
|
||||||
"projects": {
|
|
||||||
"default": "gpuweb-cts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
- id: deployment
|
|
||||||
continue-on-error: true
|
|
||||||
uses: FirebaseExtended/action-hosting-deploy@v0
|
|
||||||
with:
|
|
||||||
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_CTS }}
|
|
||||||
expires: 10d
|
|
||||||
channelId: cts-prs-${{ env.PR }}-${{ github.event.workflow_run.head_sha }}
|
|
||||||
- uses: peter-evans/create-or-update-comment@v1
|
|
||||||
continue-on-error: true
|
|
||||||
if: ${{ steps.deployment.outcome == 'success' }}
|
|
||||||
with:
|
|
||||||
issue-number: ${{ env.PR }}
|
|
||||||
body: |
|
|
||||||
Previews, as seen when this [build job](https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}) started (${{ github.event.workflow_run.head_sha }}):
|
|
||||||
[**Run tests**](${{ steps.deployment.outputs.details_url }}/standalone/) | [**View tsdoc**](${{ steps.deployment.outputs.details_url }}/docs/tsdoc/)
|
|
||||||
<!--
|
|
||||||
pr;head;sha
|
|
||||||
${{ env.PR }};${{ github.event.workflow_run.head_repository.full_name }};${{ github.event.workflow_run.head_sha }}
|
|
||||||
-->
|
|
||||||
196
dom/webgpu/tests/cts/checkout/.gitignore
vendored
196
dom/webgpu/tests/cts/checkout/.gitignore
vendored
|
|
@ -1,196 +0,0 @@
|
||||||
# VSCode - see .vscode/README.md
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# Build files
|
|
||||||
/out/
|
|
||||||
/out-wpt/
|
|
||||||
/out-node/
|
|
||||||
/out-wpt-reftest-screenshots/
|
|
||||||
.tscache/
|
|
||||||
*.tmp.txt
|
|
||||||
/docs/tsdoc/
|
|
||||||
|
|
||||||
# Cache files
|
|
||||||
/standalone/data
|
|
||||||
|
|
||||||
# Created by https://www.gitignore.io/api/linux,macos,windows,node
|
|
||||||
# Edit at https://www.gitignore.io/?templates=linux,macos,windows,node
|
|
||||||
|
|
||||||
### Linux ###
|
|
||||||
*~
|
|
||||||
|
|
||||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
|
||||||
.fuse_hidden*
|
|
||||||
|
|
||||||
# KDE directory preferences
|
|
||||||
.directory
|
|
||||||
|
|
||||||
# Linux trash folder which might appear on any partition or disk
|
|
||||||
.Trash-*
|
|
||||||
|
|
||||||
# .nfs files are created when an open file is removed but is still being accessed
|
|
||||||
.nfs*
|
|
||||||
|
|
||||||
### macOS ###
|
|
||||||
# General
|
|
||||||
.DS_Store
|
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Icon must end with two \r
|
|
||||||
Icon
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear in the root of a volume
|
|
||||||
.DocumentRevisions-V100
|
|
||||||
.fseventsd
|
|
||||||
.Spotlight-V100
|
|
||||||
.TemporaryItems
|
|
||||||
.Trashes
|
|
||||||
.VolumeIcon.icns
|
|
||||||
.com.apple.timemachine.donotpresent
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
### Node ###
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# rollup.js default build output
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Uncomment the public line if your project uses Gatsby
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
|
|
||||||
# public
|
|
||||||
|
|
||||||
# Storybook build outputs
|
|
||||||
.out
|
|
||||||
.storybook-out
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Temporary folders
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
|
|
||||||
### Windows ###
|
|
||||||
# Windows thumbnail cache files
|
|
||||||
Thumbs.db
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
ehthumbs_vista.db
|
|
||||||
|
|
||||||
# Dump file
|
|
||||||
*.stackdump
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
[Dd]esktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msix
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|
||||||
trace/
|
|
||||||
|
|
||||||
# End of https://www.gitignore.io/api/linux,macos,windows,node
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
# GPU for the Web
|
|
||||||
|
|
||||||
This repository is being used for work in the [W3C GPU for the Web Community
|
|
||||||
Group](https://www.w3.org/community/gpu/), governed by the [W3C Community
|
|
||||||
License Agreement (CLA)](http://www.w3.org/community/about/agreements/cla/). To
|
|
||||||
make substantive contributions, you must join the CG.
|
|
||||||
|
|
||||||
Contributions to the source code repository are subject to the terms of the
|
|
||||||
[3-Clause BSD License](./LICENSE.txt).
|
|
||||||
**Contributions will also be exported to
|
|
||||||
[web-platform-tests](https://github.com/web-platform-tests/wpt)
|
|
||||||
under the same license, and under the terms of its
|
|
||||||
[CONTRIBUTING.md](https://github.com/web-platform-tests/wpt/blob/master/CONTRIBUTING.md).**
|
|
||||||
|
|
||||||
If you are not the sole contributor to a contribution (pull request), please identify all
|
|
||||||
contributors in the pull request comment.
|
|
||||||
|
|
||||||
To add a contributor (other than yourself, that's automatic), mark them one per line as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
+@github_username
|
|
||||||
```
|
|
||||||
|
|
||||||
If you added a contributor by mistake, you can remove them in a comment with:
|
|
||||||
|
|
||||||
```
|
|
||||||
-@github_username
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are making a pull request on behalf of someone else but you had no part in designing the
|
|
||||||
feature, you can remove yourself with the above syntax.
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
/* eslint-disable node/no-unpublished-require */
|
|
||||||
/* eslint-disable prettier/prettier */
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
|
|
||||||
module.exports = function (grunt) {
|
|
||||||
// Project configuration.
|
|
||||||
grunt.initConfig({
|
|
||||||
pkg: grunt.file.readJSON('package.json'),
|
|
||||||
|
|
||||||
clean: {
|
|
||||||
out: ['out/', 'out-wpt/', 'out-node/'],
|
|
||||||
},
|
|
||||||
|
|
||||||
run: {
|
|
||||||
'generate-version': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['tools/gen_version'],
|
|
||||||
},
|
|
||||||
'generate-listings': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['tools/gen_listings', 'out/', 'src/webgpu', 'src/stress', 'src/manual', 'src/unittests', 'src/demo'],
|
|
||||||
},
|
|
||||||
'generate-wpt-cts-html': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['tools/gen_wpt_cts_html', 'out-wpt/cts.https.html', 'src/common/templates/cts.https.html'],
|
|
||||||
},
|
|
||||||
'generate-cache': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['tools/gen_cache', 'out/data', 'src/webgpu'],
|
|
||||||
},
|
|
||||||
unittest: {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['tools/run_node', 'unittests:*'],
|
|
||||||
},
|
|
||||||
'build-out': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: [
|
|
||||||
'node_modules/@babel/cli/bin/babel',
|
|
||||||
'--extensions=.ts,.js',
|
|
||||||
'--source-maps=true',
|
|
||||||
'--out-dir=out/',
|
|
||||||
'src/',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'build-out-wpt': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: [
|
|
||||||
'node_modules/@babel/cli/bin/babel',
|
|
||||||
'--extensions=.ts,.js',
|
|
||||||
'--source-maps=false',
|
|
||||||
'--delete-dir-on-start',
|
|
||||||
'--out-dir=out-wpt/',
|
|
||||||
'src/',
|
|
||||||
'--only=src/common/framework/',
|
|
||||||
'--only=src/common/runtime/helper/',
|
|
||||||
'--only=src/common/runtime/wpt.ts',
|
|
||||||
'--only=src/webgpu/',
|
|
||||||
// These files will be generated, instead of compiled from TypeScript.
|
|
||||||
'--ignore=src/common/internal/version.ts',
|
|
||||||
'--ignore=src/webgpu/listing.ts',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'build-out-node': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: [
|
|
||||||
'node_modules/typescript/lib/tsc.js',
|
|
||||||
'--project', 'node.tsconfig.json',
|
|
||||||
'--outDir', 'out-node/',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'copy-assets': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: [
|
|
||||||
'node_modules/@babel/cli/bin/babel',
|
|
||||||
'src/resources/',
|
|
||||||
'--out-dir=out/resources/',
|
|
||||||
'--copy-files'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'copy-assets-wpt': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: [
|
|
||||||
'node_modules/@babel/cli/bin/babel',
|
|
||||||
'src/resources/',
|
|
||||||
'--out-dir=out-wpt/resources/',
|
|
||||||
'--copy-files'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
lint: {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['node_modules/eslint/bin/eslint', 'src/**/*.ts', '--max-warnings=0'],
|
|
||||||
},
|
|
||||||
presubmit: {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['tools/presubmit'],
|
|
||||||
},
|
|
||||||
fix: {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['node_modules/eslint/bin/eslint', 'src/**/*.ts', '--fix'],
|
|
||||||
},
|
|
||||||
'autoformat-out-wpt': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['node_modules/prettier/bin-prettier', '--loglevel=warn', '--write', 'out-wpt/**/*.js'],
|
|
||||||
},
|
|
||||||
tsdoc: {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['node_modules/typedoc/bin/typedoc'],
|
|
||||||
},
|
|
||||||
'tsdoc-treatWarningsAsErrors': {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['node_modules/typedoc/bin/typedoc', '--treatWarningsAsErrors'],
|
|
||||||
},
|
|
||||||
|
|
||||||
serve: {
|
|
||||||
cmd: 'node',
|
|
||||||
args: ['node_modules/http-server/bin/http-server', '-p8080', '-a127.0.0.1', '-c-1']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
copy: {
|
|
||||||
'out-wpt-generated': {
|
|
||||||
files: [
|
|
||||||
{ expand: true, cwd: 'out', src: 'common/internal/version.js', dest: 'out-wpt/' },
|
|
||||||
{ expand: true, cwd: 'out', src: 'webgpu/listing.js', dest: 'out-wpt/' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'out-wpt-htmlfiles': {
|
|
||||||
files: [
|
|
||||||
{ expand: true, cwd: 'src', src: 'webgpu/**/*.html', dest: 'out-wpt/' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
ts: {
|
|
||||||
check: {
|
|
||||||
tsconfig: {
|
|
||||||
tsconfig: 'tsconfig.json',
|
|
||||||
passThrough: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-clean');
|
|
||||||
grunt.loadNpmTasks('grunt-contrib-copy');
|
|
||||||
grunt.loadNpmTasks('grunt-run');
|
|
||||||
grunt.loadNpmTasks('grunt-ts');
|
|
||||||
|
|
||||||
const helpMessageTasks = [];
|
|
||||||
function registerTaskAndAddToHelp(name, desc, deps) {
|
|
||||||
grunt.registerTask(name, deps);
|
|
||||||
addExistingTaskToHelp(name, desc);
|
|
||||||
}
|
|
||||||
function addExistingTaskToHelp(name, desc) {
|
|
||||||
helpMessageTasks.push({ name, desc });
|
|
||||||
}
|
|
||||||
|
|
||||||
grunt.registerTask('set-quiet-mode', () => {
|
|
||||||
grunt.log.write('Running tasks');
|
|
||||||
require('quiet-grunt');
|
|
||||||
});
|
|
||||||
|
|
||||||
grunt.registerTask('build-standalone', 'Build out/ (no checks, no WPT)', [
|
|
||||||
'run:build-out',
|
|
||||||
'run:copy-assets',
|
|
||||||
'run:generate-version',
|
|
||||||
'run:generate-listings',
|
|
||||||
]);
|
|
||||||
grunt.registerTask('build-wpt', 'Build out/ (no checks)', [
|
|
||||||
'run:build-out-wpt',
|
|
||||||
'run:copy-assets-wpt',
|
|
||||||
'run:autoformat-out-wpt',
|
|
||||||
'run:generate-version',
|
|
||||||
'run:generate-listings',
|
|
||||||
'copy:out-wpt-generated',
|
|
||||||
'copy:out-wpt-htmlfiles',
|
|
||||||
'run:generate-wpt-cts-html',
|
|
||||||
]);
|
|
||||||
grunt.registerTask('build-done-message', () => {
|
|
||||||
process.stderr.write('\nBuild completed! Running checks/tests');
|
|
||||||
});
|
|
||||||
|
|
||||||
registerTaskAndAddToHelp('pre', 'Run all presubmit checks: standalone+wpt+typecheck+unittest+lint', [
|
|
||||||
'set-quiet-mode',
|
|
||||||
'clean',
|
|
||||||
'build-standalone',
|
|
||||||
'build-wpt',
|
|
||||||
'run:build-out-node',
|
|
||||||
'build-done-message',
|
|
||||||
'ts:check',
|
|
||||||
'run:presubmit',
|
|
||||||
'run:unittest',
|
|
||||||
'run:lint',
|
|
||||||
'run:tsdoc-treatWarningsAsErrors',
|
|
||||||
]);
|
|
||||||
registerTaskAndAddToHelp('standalone', 'Build standalone and typecheck', [
|
|
||||||
'set-quiet-mode',
|
|
||||||
'build-standalone',
|
|
||||||
'build-done-message',
|
|
||||||
'ts:check',
|
|
||||||
]);
|
|
||||||
registerTaskAndAddToHelp('wpt', 'Build for WPT and typecheck', [
|
|
||||||
'set-quiet-mode',
|
|
||||||
'build-wpt',
|
|
||||||
'build-done-message',
|
|
||||||
'ts:check',
|
|
||||||
]);
|
|
||||||
registerTaskAndAddToHelp('unittest', 'Build standalone, typecheck, and unittest', [
|
|
||||||
'standalone',
|
|
||||||
'run:unittest',
|
|
||||||
]);
|
|
||||||
registerTaskAndAddToHelp('check', 'Just typecheck', [
|
|
||||||
'set-quiet-mode',
|
|
||||||
'ts:check',
|
|
||||||
]);
|
|
||||||
|
|
||||||
registerTaskAndAddToHelp('serve', 'Serve out/ on 127.0.0.1:8080 (does NOT compile source)', ['run:serve']);
|
|
||||||
registerTaskAndAddToHelp('fix', 'Fix lint and formatting', ['run:fix']);
|
|
||||||
|
|
||||||
addExistingTaskToHelp('clean', 'Clean out/ and out-wpt/');
|
|
||||||
|
|
||||||
grunt.registerTask('default', '', () => {
|
|
||||||
console.error('\nAvailable tasks (see grunt --help for info):');
|
|
||||||
for (const { name, desc } of helpMessageTasks) {
|
|
||||||
console.error(`$ grunt ${name}`);
|
|
||||||
console.error(` ${desc}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
Copyright 2019 WebGPU CTS Contributors
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from this
|
|
||||||
software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
# WebGPU Conformance Test Suite
|
|
||||||
|
|
||||||
This is the conformance test suite for WebGPU.
|
|
||||||
It tests the behaviors defined by the [WebGPU specification](https://gpuweb.github.io/gpuweb/).
|
|
||||||
|
|
||||||
The contents of this test suite are considered **normative**; implementations must pass
|
|
||||||
them to be WebGPU-conformant. Mismatches between the specification and tests are bugs.
|
|
||||||
|
|
||||||
This test suite can be embedded inside [WPT](https://github.com/web-platform-tests/wpt) or run in standalone.
|
|
||||||
|
|
||||||
## [Launch the standalone CTS runner / test plan viewer](https://gpuweb.github.io/cts/standalone/)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Please read the [introductory guidelines](docs/intro/README.md) before contributing.
|
|
||||||
Other documentation may be found in [`docs/`](docs/) and in the [helper index](https://gpuweb.github.io/cts/docs/tsdoc/) ([source](docs/helper_index.txt)).
|
|
||||||
|
|
||||||
Read [CONTRIBUTING.md](CONTRIBUTING.md) on licensing.
|
|
||||||
|
|
||||||
For realtime communication about WebGPU spec and test, join the
|
|
||||||
[#WebGPU:matrix.org room](https://app.element.io/#/room/#WebGPU:matrix.org)
|
|
||||||
on Matrix.
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
module.exports = function (api) {
|
|
||||||
api.cache(true);
|
|
||||||
return {
|
|
||||||
presets: ['@babel/preset-typescript'],
|
|
||||||
plugins: [
|
|
||||||
'const-enum',
|
|
||||||
[
|
|
||||||
'add-header-comment',
|
|
||||||
{
|
|
||||||
header: ['AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
compact: false,
|
|
||||||
// Keeps comments from getting hoisted to the end of the previous line of code.
|
|
||||||
// (Also keeps lines close to their original line numbers - but for WPT we
|
|
||||||
// reformat with prettier anyway.)
|
|
||||||
retainLines: true,
|
|
||||||
shouldPrintComment: val => !/eslint|prettier-ignore/.test(val),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
// Note: VS Code's setting precedence is `.vscode/` > `cts.code-workspace` > global user settings.
|
|
||||||
{
|
|
||||||
"folders": [
|
|
||||||
{
|
|
||||||
"name": "cts",
|
|
||||||
"path": "."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "webgpu",
|
|
||||||
"path": "src/webgpu"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"settings": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [100],
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"files.insertFinalNewline": true,
|
|
||||||
"files.trimFinalNewlines": true,
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"files.exclude": {
|
|
||||||
"*.tmp.txt": true,
|
|
||||||
".gitignore": true,
|
|
||||||
".travis.yml": true,
|
|
||||||
".tscache": true,
|
|
||||||
"deploy_key.enc": true,
|
|
||||||
"node_modules": true,
|
|
||||||
"out": true,
|
|
||||||
"out-node": true,
|
|
||||||
"out-wpt": true,
|
|
||||||
"docs/tsdoc": true,
|
|
||||||
"package-lock.json": true
|
|
||||||
},
|
|
||||||
// Configure VSCode to use the right style when automatically adding imports on autocomplete.
|
|
||||||
"typescript.preferences.importModuleSpecifier": "relative",
|
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
|
||||||
"typescript.preferences.quoteStyle": "single"
|
|
||||||
},
|
|
||||||
"tasks": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
// Only supports "shell" and "process" tasks.
|
|
||||||
// https://code.visualstudio.com/docs/editor/multi-root-workspaces#_workspace-task-configuration
|
|
||||||
{
|
|
||||||
// Use "group": "build" instead of "test" so it's easy to access from cmd-shift-B.
|
|
||||||
"group": "build",
|
|
||||||
"label": "npm: test",
|
|
||||||
"detail": "Run all presubmit checks",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm run test",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "build",
|
|
||||||
"label": "npm: check",
|
|
||||||
"detail": "Just typecheck",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm run check",
|
|
||||||
"problemMatcher": ["$tsc"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "build",
|
|
||||||
"label": "npm: standalone",
|
|
||||||
"detail": "Build standalone and typecheck",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm run standalone",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "build",
|
|
||||||
"label": "npm: wpt",
|
|
||||||
"detail": "Build for WPT and typecheck",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm run wpt",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "build",
|
|
||||||
"label": "npm: unittest",
|
|
||||||
"detail": "Build standalone, typecheck, and unittest",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm run unittest",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "build",
|
|
||||||
"label": "npm: tsdoc",
|
|
||||||
"detail": "Build docs/tsdoc/",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npm run tsdoc",
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "build",
|
|
||||||
"label": "grunt: run:lint",
|
|
||||||
"detail": "Run eslint",
|
|
||||||
|
|
||||||
"type": "shell",
|
|
||||||
"command": "npx grunt run:lint",
|
|
||||||
"problemMatcher": ["$eslint-stylish"]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# Building
|
|
||||||
|
|
||||||
Building the project is not usually needed for local development.
|
|
||||||
However, for exports to WPT, or deployment (https://gpuweb.github.io/cts/),
|
|
||||||
files can be pre-generated.
|
|
||||||
|
|
||||||
The project builds into two directories:
|
|
||||||
|
|
||||||
- `out/`: Built framework and test files, needed to run standalone or command line.
|
|
||||||
- `out-wpt/`: Build directory for export into WPT. Contains:
|
|
||||||
- An adapter for running WebGPU CTS tests under WPT
|
|
||||||
- A copy of the needed files from `out/`
|
|
||||||
- A copy of any `.html` test cases from `src/`
|
|
||||||
|
|
||||||
To build and run all pre-submit checks (including type and lint checks and
|
|
||||||
unittests), use:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
For checks only:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run check
|
|
||||||
```
|
|
||||||
|
|
||||||
For a quicker iterative build:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run standalone
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
To serve the built files (rather than using the dev server), run `npx grunt serve`.
|
|
||||||
|
|
||||||
## Export to WPT
|
|
||||||
|
|
||||||
Run `npm run wpt`.
|
|
||||||
|
|
||||||
Copy (or symlink) the `out-wpt/` directory as the `webgpu/` directory in your
|
|
||||||
WPT checkout or your browser's "internal" WPT test directory.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Running the CTS on Deno
|
|
||||||
|
|
||||||
Since version 1.8, Deno experimentally implements the WebGPU API out of the box.
|
|
||||||
You can use the `./tools/deno` script to run the CTS in Deno. To do this you
|
|
||||||
will first need to install Deno: [stable](https://deno.land#installation), or
|
|
||||||
build the main branch from source
|
|
||||||
(`cargo install --git https://github.com/denoland/deno --bin deno`).
|
|
||||||
|
|
||||||
On macOS and recent Linux, you can just run `./tools/run_deno` as is. On Windows and
|
|
||||||
older Linux releases you will need to run
|
|
||||||
`deno run --unstable --allow-read --allow-write --allow-env ./tools/deno`.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
Usage:
|
|
||||||
tools/run_deno [OPTIONS...] QUERIES...
|
|
||||||
tools/run_deno 'unittests:*' 'webgpu:buffers,*'
|
|
||||||
Options:
|
|
||||||
--verbose Print result/log of every test as it runs.
|
|
||||||
--debug Include debug messages in logging.
|
|
||||||
--print-json Print the complete result JSON in the output.
|
|
||||||
--expectations Path to expectations file.
|
|
||||||
```
|
|
||||||
|
|
@ -1,516 +0,0 @@
|
||||||
# Floating Point Primer
|
|
||||||
|
|
||||||
This document is meant to be a primer of the concepts related to floating point
|
|
||||||
numbers that are needed to be understood when working on tests in WebGPU's CTS.
|
|
||||||
|
|
||||||
WebGPU's CTS is responsible for testing if implementations of WebGPU are
|
|
||||||
conformant to the spec, and thus interoperable with each other.
|
|
||||||
|
|
||||||
Floating point math makes up a significant portion of the WGSL spec, and has
|
|
||||||
many subtle corner cases to get correct.
|
|
||||||
|
|
||||||
Additionally, floating point math, unlike integer math, is broadly not exact, so
|
|
||||||
how inaccurate a calculation is allowed to be is required to be stated in the
|
|
||||||
spec and tested in the CTS, as opposed to testing for a singular correct
|
|
||||||
response.
|
|
||||||
|
|
||||||
Thus, the WebGPU CTS has a significant amount of machinery around how to
|
|
||||||
correctly test floating point expectations in a fluent manner.
|
|
||||||
|
|
||||||
## Floating Point Numbers
|
|
||||||
|
|
||||||
For the context of this discussion floating point numbers, fp for short, are
|
|
||||||
single precision IEEE floating point numbers, f32 for short.
|
|
||||||
|
|
||||||
Details of how this format works are discussed as needed below, but for a more
|
|
||||||
involved discussion, please see the references in the Resources sections.
|
|
||||||
|
|
||||||
Additionally, in the Appendix there is a table of interesting/common values that
|
|
||||||
are often referenced in tests or this document.
|
|
||||||
|
|
||||||
*In the future support for f16 and abstract floats will be added to the CTS, and
|
|
||||||
this document will need to be updated.*
|
|
||||||
|
|
||||||
Floating point numbers are effectively lossy compression of the infinite number
|
|
||||||
of possible values over their range down to 32-bits of distinct points.
|
|
||||||
|
|
||||||
This means that not all numbers in the range can be exactly represented as a f32.
|
|
||||||
|
|
||||||
For example, the integer `1` is exactly represented as `0x3f800000`, but the next
|
|
||||||
nearest number `0x3f800001` is `1.00000011920928955`.
|
|
||||||
|
|
||||||
So any number between `1` and `1.00000011920928955` is not exactly represented
|
|
||||||
as a f32 and instead is approximated as either `1` or `1.00000011920928955`.
|
|
||||||
|
|
||||||
When a number X is not exactly represented by a f32 value, there are normally
|
|
||||||
two neighbouring numbers that could reasonably represent X: the nearest f32
|
|
||||||
value above X, and the nearest f32 value below X. Which of these values gets
|
|
||||||
used is dictated by the rounding mode being used, which may be something like
|
|
||||||
always round towards 0 or go to the nearest neighbour, or something else
|
|
||||||
entirely.
|
|
||||||
|
|
||||||
The process of converting numbers between precisions, like non-f32 to f32, is
|
|
||||||
called quantization. WGSL does not prescribe a specific rounding mode when
|
|
||||||
quantizing, so either of the neighbouring values is considered valid
|
|
||||||
when converting a non-exactly representable value to f32. This has significant
|
|
||||||
implications on the CTS that are discussed later.
|
|
||||||
|
|
||||||
From here on, we assume you are familiar with the internal structure of a f32
|
|
||||||
value: a sign bit, a biased exponent, and a mantissa. For reference, see
|
|
||||||
[float32 on Wikipedia](https://en.wikipedia.org/wiki/Single-precision_floating-point_format)
|
|
||||||
|
|
||||||
In the f32 format as described above, there are two possible zero values, one
|
|
||||||
with all bits being 0, called positive zero, and one all the same except with
|
|
||||||
the sign bit being 1, called negative zero.
|
|
||||||
|
|
||||||
For WGSL, and thus the CTS's purposes, these values are considered equivalent.
|
|
||||||
Typescript, which the CTS is written in, treats all zeros as positive zeros,
|
|
||||||
unless you explicitly escape hatch to differentiate between them, so most of the
|
|
||||||
time there being two zeros doesn't materially affect code.
|
|
||||||
|
|
||||||
### Normals
|
|
||||||
|
|
||||||
Normal numbers are floating point numbers whose biased exponent is not all 0s or
|
|
||||||
all 1s. For WGSL these numbers behave as you expect for floating point values
|
|
||||||
with no interesting caveats.
|
|
||||||
|
|
||||||
### Subnormals
|
|
||||||
|
|
||||||
Subnormal numbers are numbers whose biased exponent is all 0s, also called
|
|
||||||
denorms.
|
|
||||||
|
|
||||||
These are the closest numbers to zero, both positive and negative, and fill in
|
|
||||||
the gap between the normal numbers with smallest magnitude, and 0.
|
|
||||||
|
|
||||||
Some devices, for performance reasons, do not handle operations on the
|
|
||||||
subnormal numbers, and instead treat them as being zero, this is called *flush
|
|
||||||
to zero* or FTZ behaviour.
|
|
||||||
|
|
||||||
This means in the CTS that when a subnormal number is consumed or produced by an
|
|
||||||
operation, an implementation may choose to replace it with zero.
|
|
||||||
|
|
||||||
Like the rounding mode for quantization, this adds significant complexity to the
|
|
||||||
CTS, which will be discussed later.
|
|
||||||
|
|
||||||
### Inf & NaNs
|
|
||||||
|
|
||||||
Floating point numbers include positive and negative infinity to represent
|
|
||||||
values that are out of the bounds supported by the current precision.
|
|
||||||
|
|
||||||
Implementations may assume that infinities are not present. When an evaluation
|
|
||||||
would produce an infinity, an undefined value is produced instead.
|
|
||||||
|
|
||||||
Additionally, when a calculation would produce a finite value outside the
|
|
||||||
bounds of the current precision, the implementation may convert that value to
|
|
||||||
either an infinity with same sign, or the min/max representable value as
|
|
||||||
appropriate.
|
|
||||||
|
|
||||||
The CTS encodes the least restrictive interpretation of the rules in the spec,
|
|
||||||
i.e. assuming someone has made a slightly adversarial implementation that always
|
|
||||||
chooses the thing with the least accuracy.
|
|
||||||
|
|
||||||
This means that the above rules about infinities combine to say that any time an
|
|
||||||
out of bounds value is seen, any finite value is acceptable afterwards.
|
|
||||||
|
|
||||||
This is because the out of bounds value may be converted to an infinity and then
|
|
||||||
an undefined value can be used instead of the infinity.
|
|
||||||
|
|
||||||
This is actually a significant boon for the CTS implementation, because it short
|
|
||||||
circuits a bunch of complexity about clamping to edge values and handling
|
|
||||||
infinities.
|
|
||||||
|
|
||||||
Signaling NaNs are treated as quiet NaNs in the WGSL spec. And quiet NaNs have
|
|
||||||
the same "may-convert-to-undefined-value" behaviour that infinities have, so for
|
|
||||||
the purpose of the CTS they are handled by the infinite/out of bounds logic
|
|
||||||
normally.
|
|
||||||
|
|
||||||
## Notation/Terminology
|
|
||||||
|
|
||||||
When discussing floating point values in the CTS, there are a few terms used
|
|
||||||
with precise meanings, which will be elaborated here.
|
|
||||||
|
|
||||||
Additionally, any specific notation used will be specified here to avoid
|
|
||||||
confusion.
|
|
||||||
|
|
||||||
### Operations
|
|
||||||
|
|
||||||
The CTS tests for the proper execution of f32 builtins, i.e. sin, sqrt, abs,
|
|
||||||
etc, and expressions, i.e. *, /, <, etc. These collectively can be referred to
|
|
||||||
as f32 operations.
|
|
||||||
|
|
||||||
Operations, which can be thought of as mathematical functions, are mappings from
|
|
||||||
a set of inputs to a set of outputs.
|
|
||||||
|
|
||||||
Denoted `f(x, y) = X`, where f is a placeholder or the name of the operation,
|
|
||||||
lower case variables are the inputs to the function, and uppercase variables are
|
|
||||||
the outputs of the function.
|
|
||||||
|
|
||||||
Operations have one or more inputs and an output. Being a f32 operation means
|
|
||||||
that the primary space for input and output values is f32, but there is some
|
|
||||||
flexibility in this definition. For example operations with values being
|
|
||||||
restricted to a subset of integers that are representable as f32 are often
|
|
||||||
referred to as being f32 based.
|
|
||||||
|
|
||||||
Values are generally floats, integers, booleans, vector, and matrices. Consult
|
|
||||||
the WGSL spec for the exact list of types and their definitions.
|
|
||||||
|
|
||||||
For composite outputs where there are multiple values being returned, there is a
|
|
||||||
single result value made of structured data. Whereas inputs handle this by
|
|
||||||
having multiple input parameters.
|
|
||||||
|
|
||||||
Some examples of different types of operations:
|
|
||||||
|
|
||||||
`multiplication(x, y) = X`, which represents the WGSL expression `x * y`, takes
|
|
||||||
in f32 values, `x` and `y`, and produces a f32 value `X`.
|
|
||||||
|
|
||||||
`lessThen(x, y) = X`, which represents the WGSL expression `x < y`, again takes
|
|
||||||
in f32 values, but in this case returns a boolean value.
|
|
||||||
|
|
||||||
`ldexp(x, y) = X`, which builds a f32 takes, takes in a f32 values `x` and a
|
|
||||||
restricted integer `y`.
|
|
||||||
|
|
||||||
### Domain, Range, and Intervals
|
|
||||||
|
|
||||||
For an operation `f(x) = X`, the interval of valid values for the input, `x`, is
|
|
||||||
called the *domain*, and the interval for valid results, `X`, is called the
|
|
||||||
*range*.
|
|
||||||
|
|
||||||
An interval, `[a, b]`, is a set of real numbers that contains `a`, `b`, and all
|
|
||||||
the real numbers between them.
|
|
||||||
|
|
||||||
Open-ended intervals, i.e. ones that don't include `a` and/or `b`, are avoided,
|
|
||||||
and are called out explicitly when they occur.
|
|
||||||
|
|
||||||
The convention in this doc and the CTS code is that `a <= b`, so `a` can be
|
|
||||||
referred to as the beginning of the interval and `b` as the end of the interval.
|
|
||||||
|
|
||||||
When talking about intervals, this doc and the code endeavours to avoid using
|
|
||||||
the term **range** to refer to the span of values that an interval covers,
|
|
||||||
instead using the term bounds to avoid confusion of terminology around output of
|
|
||||||
operations.
|
|
||||||
|
|
||||||
## Accuracy
|
|
||||||
|
|
||||||
As mentioned above floating point numbers are not able to represent all the
|
|
||||||
possible values over their bounds, but instead represent discrete values in that
|
|
||||||
interval, and approximate the remainder.
|
|
||||||
|
|
||||||
Additionally, floating point numbers are not evenly distributed over the real
|
|
||||||
number line, but instead are clustered closer together near zero, and further
|
|
||||||
apart as their magnitudes grow.
|
|
||||||
|
|
||||||
When discussing operations on floating point numbers, there is often reference
|
|
||||||
to a true value. This is the value that given no performance constraints and
|
|
||||||
infinite precision you would get, i.e `acos(1) = π`, where π has infinite
|
|
||||||
digits of precision.
|
|
||||||
|
|
||||||
For the CTS it is often sufficient to calculate the true value using TypeScript,
|
|
||||||
since its native number format is higher precision (double-precision/f64), and
|
|
||||||
all f32 values can be represented in it.
|
|
||||||
|
|
||||||
The true value is sometimes representable exactly as a f32 value, but often is
|
|
||||||
not.
|
|
||||||
|
|
||||||
Additionally, many operations are implemented using approximations from
|
|
||||||
numerical analysis, where there is a tradeoff between the precision of the
|
|
||||||
result and the cost.
|
|
||||||
|
|
||||||
Thus, the spec specifies what the accuracy constraints for specific operations
|
|
||||||
is, how close to truth an implementation is required to be, to be
|
|
||||||
considered conformant.
|
|
||||||
|
|
||||||
There are 5 different ways that accuracy requirements are defined in the spec:
|
|
||||||
|
|
||||||
1. *Exact*
|
|
||||||
|
|
||||||
This is the situation where it is expected that true value for an operation
|
|
||||||
is always expected to be exactly representable. This doesn't happen for any
|
|
||||||
of the operations that return floating point values, but does occur for
|
|
||||||
logical operations that return boolean values.
|
|
||||||
|
|
||||||
|
|
||||||
2. *Correctly Rounded*
|
|
||||||
|
|
||||||
For the case that the true value is exactly representable as a f32, this is
|
|
||||||
the equivalent of exactly from above. In the event that the true value is not
|
|
||||||
exact, then the acceptable answer for most numbers is either the nearest f32
|
|
||||||
above or the nearest f32 below the true value.
|
|
||||||
|
|
||||||
For values near the subnormal range, e.g. close to zero, this becomes more
|
|
||||||
complex, since an implementation may FTZ at any point. So if the exact
|
|
||||||
solution is subnormal or either of the neighbours of the true value are
|
|
||||||
subnormal, zero becomes a possible result, thus the acceptance interval is
|
|
||||||
wider than naively expected.
|
|
||||||
|
|
||||||
|
|
||||||
3. *Absolute Error*
|
|
||||||
|
|
||||||
This type of accuracy specifies an error value, ε, and the calculated result
|
|
||||||
is expected to be within that distance from the true value, i.e.
|
|
||||||
`[ X - ε, X + ε ]`.
|
|
||||||
|
|
||||||
The main drawback with this manner of specifying accuracy is that it doesn't
|
|
||||||
scale with the level of precision in floating point numbers themselves at a
|
|
||||||
specific value. Thus, it tends to be only used for specifying accuracy over
|
|
||||||
specific limited intervals, i.e. [-π, π].
|
|
||||||
|
|
||||||
|
|
||||||
4. *Units of Least Precision (ULP)*
|
|
||||||
|
|
||||||
The solution to the issue of not scaling with precision of floating point is
|
|
||||||
to use units of least precision.
|
|
||||||
|
|
||||||
ULP(X) is min (b-a) over all pairs (a,b) of representable floating point
|
|
||||||
numbers such that (a <= X <= b and a =/= b). For a more formal discussion of
|
|
||||||
ULP see
|
|
||||||
[On the definition of ulp(x)](https://hal.inria.fr/inria-00070503/document).
|
|
||||||
|
|
||||||
n * ULP or nULP means `[X - n * ULP @ X, X + n * ULP @ X]`.
|
|
||||||
|
|
||||||
|
|
||||||
5. *Inherited*
|
|
||||||
|
|
||||||
When an operation's accuracy is defined in terms of other operations, then
|
|
||||||
its accuracy is said to be inherited. Handling of inherited accuracies is
|
|
||||||
one of the main driving factors in the design of testing framework, so will
|
|
||||||
need to be discussed in detail.
|
|
||||||
|
|
||||||
## Acceptance Intervals
|
|
||||||
|
|
||||||
The first four accuracy types; Exact, Correctly Rounded, Absolute Error, and
|
|
||||||
ULP, sometimes called simple accuracies, can be defined in isolation from each
|
|
||||||
other, and by association can be implemented using relatively independent
|
|
||||||
implementations.
|
|
||||||
|
|
||||||
The original implementation of the floating point framework did this as it was
|
|
||||||
being built out, but ran into difficulties when defining the inherited
|
|
||||||
accuracies.
|
|
||||||
|
|
||||||
For examples, `tan(x) inherits from sin(x)/cos(x)`, one can take the defined
|
|
||||||
rules and manually build up a bespoke solution for checking the results, but
|
|
||||||
this is tedious, error-prone, and doesn't allow for code re-use.
|
|
||||||
|
|
||||||
Instead, it would be better if there was a single conceptual framework that one
|
|
||||||
can express all the 'simple' accuracy requirements in, and then have a mechanism
|
|
||||||
for composing them to define inherited accuracies.
|
|
||||||
|
|
||||||
In the WebGPU CTS this is done via the concept of acceptance intervals, which is
|
|
||||||
derived from a similar concept in the Vulkan CTS, though implemented
|
|
||||||
significantly differently.
|
|
||||||
|
|
||||||
The core of this idea is that each of different accuracy types can be integrated
|
|
||||||
into the definition of the operation, so that instead of transforming an input
|
|
||||||
from the domain to a point in the range, the operation is producing an interval
|
|
||||||
in the range, that is the acceptable values an implementation may emit.
|
|
||||||
|
|
||||||
|
|
||||||
The simple accuracies can be defined as follows:
|
|
||||||
|
|
||||||
1. *Exact*
|
|
||||||
|
|
||||||
`f(x) => [X, X]`
|
|
||||||
|
|
||||||
|
|
||||||
2. *Correctly Rounded*
|
|
||||||
|
|
||||||
If `X` is precisely defined as a f32
|
|
||||||
|
|
||||||
`f(x) => [X, X]`
|
|
||||||
|
|
||||||
otherwise,
|
|
||||||
|
|
||||||
`[a, b]` where `a` is the largest representable number with `a <= X`, and `b`
|
|
||||||
is the smallest representable number with `X <= b`
|
|
||||||
|
|
||||||
|
|
||||||
3. *Absolute Error*
|
|
||||||
|
|
||||||
`f(x) => [ X - ε, X + ε ]`, where ε is the absolute error value
|
|
||||||
|
|
||||||
|
|
||||||
4. **ULP Error**
|
|
||||||
|
|
||||||
`f(x) = X => [X - n*ULP(X), X + n*ULP(X)]`
|
|
||||||
|
|
||||||
As defined, these definitions handle mapping from a point in the domain into an
|
|
||||||
interval in the range.
|
|
||||||
|
|
||||||
This is insufficient for implementing inherited accuracies, since inheritance
|
|
||||||
sometimes involve mapping domain intervals to range intervals.
|
|
||||||
|
|
||||||
Here we use the convention for naturally extending a function on real numbers
|
|
||||||
into a function on intervals of real numbers, i.e. `f([a, b]) = [A, B]`.
|
|
||||||
|
|
||||||
Given that floating point numbers have a finite number of precise values for any
|
|
||||||
given interval, one could implement just running the accuracy computation for
|
|
||||||
every point in the interval and then spanning together the resultant intervals.
|
|
||||||
That would be very inefficient though and make your reviewer sad to read.
|
|
||||||
|
|
||||||
For mapping intervals to intervals the key insight is that we only need to be
|
|
||||||
concerned with the extrema of the operation in the interval, since the
|
|
||||||
acceptance interval is the bounds of the possible outputs.
|
|
||||||
|
|
||||||
In more precise terms:
|
|
||||||
```
|
|
||||||
f(x) => X, x = [a, b] and X = [A, B]
|
|
||||||
|
|
||||||
X = [min(f(x)), max(f(x))]
|
|
||||||
X = [min(f([a, b])), max(f([a, b]))]
|
|
||||||
X = [f(m), f(M)]
|
|
||||||
```
|
|
||||||
where m and M are in `[a, b]`, `m <= M`, and produce the min and max results
|
|
||||||
for `f` on the interval, respectively.
|
|
||||||
|
|
||||||
So how do we find the minima and maxima for our operation in the domain?
|
|
||||||
|
|
||||||
The common general solution for this requires using calculus to calculate the
|
|
||||||
derivative of `f`, `f'`, and then find the zeroes `f'` to find inflection
|
|
||||||
points of `f`.
|
|
||||||
|
|
||||||
This solution wouldn't be sufficient for all builtins, i.e. `step` which is not
|
|
||||||
differentiable at 'edge' values.
|
|
||||||
|
|
||||||
Thankfully we do not need a general solution for the CTS, since all the builtin
|
|
||||||
operations are defined in the spec, so `f` is from a known set of options.
|
|
||||||
|
|
||||||
These operations can be divided into two broad categories: monotonic, and
|
|
||||||
non-monotonic, with respect to an interval.
|
|
||||||
|
|
||||||
The monotonic operations are ones that preserve the order of inputs in their
|
|
||||||
outputs (or reverse it). Their graph only ever decreases or increases,
|
|
||||||
never changing from one or the other, though it can have flat sections.
|
|
||||||
|
|
||||||
The non-monotonic operations are ones whose graph would have both regions of
|
|
||||||
increase and decrease.
|
|
||||||
|
|
||||||
The monotonic operations, when mapping an interval to an interval, are simple to
|
|
||||||
handle, since the extrema are guaranteed to be the ends of the domain, `a` and `b`.
|
|
||||||
|
|
||||||
So `f([a, b])` = `[f(a), f(b)]` or `[f(b), f(a)]`. We could figure out if `f` is
|
|
||||||
increasing or decreasing beforehand to determine if it should be `[f(a), f(b)]`
|
|
||||||
or `[f(b), f(a)]`.
|
|
||||||
|
|
||||||
It is simpler to just use min & max to have an implementation that is agnostic
|
|
||||||
to the details of `f`.
|
|
||||||
```
|
|
||||||
A = f(a), B = f(b)
|
|
||||||
X = [min(A, B), max(A, B)]
|
|
||||||
```
|
|
||||||
|
|
||||||
The non-monotonic functions that we need to handle for interval-to-interval
|
|
||||||
mappings are more complex. Thankfully are a small number of the overall
|
|
||||||
operations that need to be handled, since they are only the operations that are
|
|
||||||
used in an inherited accuracy and take in the output of another operation as
|
|
||||||
part of that inherited accuracy.
|
|
||||||
|
|
||||||
So in the CTS we just have bespoke implementations for each of them.
|
|
||||||
|
|
||||||
Part of the operation definition in the CTS is a function that takes in the
|
|
||||||
domain interval, and returns a sub-interval such that the subject function is
|
|
||||||
monotonic over that sub-interval, and hence the function's minima and maxima are
|
|
||||||
at the ends.
|
|
||||||
|
|
||||||
This adjusted domain interval can then be fed through the same machinery as the
|
|
||||||
monotonic functions.
|
|
||||||
|
|
||||||
### Inherited Accuracy
|
|
||||||
|
|
||||||
So with all of that background out of the way, we can now define an inherited
|
|
||||||
accuracy in terms of acceptance intervals.
|
|
||||||
|
|
||||||
The crux of this is the insight that the range of one operation can become the
|
|
||||||
domain of another operation to compose them together.
|
|
||||||
|
|
||||||
And since we have defined how to do this interval to interval mapping above,
|
|
||||||
transforming things becomes mechanical and thus implementable in reusable code.
|
|
||||||
|
|
||||||
When talking about inherited accuracies `f(x) => g(x)` is used to denote that
|
|
||||||
`f`'s accuracy is a defined as `g`.
|
|
||||||
|
|
||||||
An example to illustrate inherited accuracies:
|
|
||||||
|
|
||||||
```
|
|
||||||
tan(x) => sin(x)/cos(x)
|
|
||||||
|
|
||||||
sin(x) => [sin(x) - 2^-11, sin(x) + 2^-11]`
|
|
||||||
cos(x) => [cos(x) - 2^-11, cos(x) + 2-11]
|
|
||||||
|
|
||||||
x/y => [x/y - 2.5 * ULP(x/y), x/y + 2.5 * ULP(x/y)]
|
|
||||||
```
|
|
||||||
|
|
||||||
`sin(x)` and `cos(x)` are non-monotonic, so calculating out a closed generic
|
|
||||||
form over an interval is a pain, since the min and max vary depending on the
|
|
||||||
value of x. Let's isolate this to a single point, so you don't have to read
|
|
||||||
literally pages of expanded intervals.
|
|
||||||
|
|
||||||
```
|
|
||||||
x = π/2
|
|
||||||
|
|
||||||
sin(π/2) => [sin(π/2) - 2-11, sin(π/2) + 2-11]
|
|
||||||
=> [0 - 2-11, 0 + 2-11]
|
|
||||||
=> [-0.000488.., 0.000488...]
|
|
||||||
cos(π/2) => [cos(π/2) - 2-11, cos(π/2) + 2-11]
|
|
||||||
=> [-0.500488, -0.499511...]
|
|
||||||
|
|
||||||
tan(π/2) => sin(π/2)/cos(π/2)
|
|
||||||
=> [-0.000488.., 0.000488...]/[-0.500488..., -0.499511...]
|
|
||||||
=> [min({-0.000488.../-0.500488..., -0.000488.../-0.499511..., ...}),
|
|
||||||
max(min({-0.000488.../-0.500488..., -0.000488.../-0.499511..., ...}) ]
|
|
||||||
=> [0.000488.../-0.499511..., 0.000488.../0.499511...]
|
|
||||||
=> [-0.0009775171, 0.0009775171]
|
|
||||||
```
|
|
||||||
|
|
||||||
For clarity this has omitted a bunch of complexity around FTZ behaviours, and
|
|
||||||
that these operations are only defined for specific domains, but the high-level
|
|
||||||
concepts hold.
|
|
||||||
|
|
||||||
For each of the inherited operations we could implement a manually written out
|
|
||||||
closed form solution, but that would be quite error-prone and not be
|
|
||||||
re-using code between builtins.
|
|
||||||
|
|
||||||
Instead, the CTS takes advantage of the fact in addition to testing
|
|
||||||
implementations of `tan(x)` we are going to be testing implementations of
|
|
||||||
`sin(x)`, `cos(x)` and `x/y`, so there should be functions to generate
|
|
||||||
acceptance intervals for those operations.
|
|
||||||
|
|
||||||
The `tan(x)` acceptance interval can be constructed by generating the acceptance
|
|
||||||
intervals for `sin(x)`, `cos(x)` and `x/y` via function calls and composing the
|
|
||||||
results.
|
|
||||||
|
|
||||||
This algorithmically looks something like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
tan(x):
|
|
||||||
Calculate sin(x) interval
|
|
||||||
Calculate cos(x) interval
|
|
||||||
Calculate sin(x) result divided by cos(x) result
|
|
||||||
Return division result
|
|
||||||
```
|
|
||||||
|
|
||||||
# Appendix
|
|
||||||
|
|
||||||
### Significant f32 Values
|
|
||||||
|
|
||||||
| Name | Decimal (~) | Hex | Sign Bit | Exponent Bits | Significand Bits |
|
|
||||||
| ---------------------- | --------------: | ----------: | -------: | ------------: | ---------------------------: |
|
|
||||||
| Negative Infinity | -∞ | 0xff80 0000 | 1 | 1111 1111 | 0000 0000 0000 0000 0000 000 |
|
|
||||||
| Min Negative Normal | -3.40282346E38 | 0xff7f ffff | 1 | 1111 1110 | 1111 1111 1111 1111 1111 111 |
|
|
||||||
| Max Negative Normal | -1.1754943E−38 | 0x8080 0000 | 1 | 0000 0001 | 0000 0000 0000 0000 0000 000 |
|
|
||||||
| Min Negative Subnormal | -1.1754942E-38 | 0x807f ffff | 1 | 0000 0000 | 1111 1111 1111 1111 1111 111 |
|
|
||||||
| Max Negative Subnormal | -1.4012984E−45 | 0x8000 0001 | 1 | 0000 0000 | 0000 0000 0000 0000 0000 001 |
|
|
||||||
| Negative Zero | -0 | 0x8000 0000 | 1 | 0000 0000 | 0000 0000 0000 0000 0000 000 |
|
|
||||||
| Positive Zero | 0 | 0x0000 0000 | 0 | 0000 0000 | 0000 0000 0000 0000 0000 000 |
|
|
||||||
| Min Positive Subnormal | 1.4012984E−45 | 0x0000 0001 | 0 | 0000 0000 | 0000 0000 0000 0000 0000 001 |
|
|
||||||
| Max Positive Subnormal | 1.1754942E-38 | 0x007f ffff | 0 | 0000 0000 | 1111 1111 1111 1111 1111 111 |
|
|
||||||
| Min Positive Normal | 1.1754943E−38 | 0x0080 0000 | 0 | 0000 0001 | 0000 0000 0000 0000 0000 000 |
|
|
||||||
| Max Positive Normal | 3.40282346E38 | 0x7f7f ffff | 0 | 1111 1110 | 1111 1111 1111 1111 1111 111 |
|
|
||||||
| Negative Infinity | ∞ | 0x7f80 0000 | 0 | 1111 1111 | 0000 0000 0000 0000 0000 000 |
|
|
||||||
|
|
||||||
# Resources
|
|
||||||
- [WebGPU Spec](https://www.w3.org/TR/webgpu/)
|
|
||||||
- [WGSL Spec](https://www.w3.org/TR/WGSL/)
|
|
||||||
- [float32 on Wikipedia](https://en.wikipedia.org/wiki/Single-precision_floating-point_format)
|
|
||||||
- [IEEE-754 Floating Point Converter](https://www.h-schmidt.net/FloatConverter/IEEE754.html)
|
|
||||||
- [IEEE 754 Calculator](http://weitz.de/ieee/)
|
|
||||||
- [Keisan High Precision Calculator](https://keisan.casio.com/calculator)
|
|
||||||
- [On the definition of ulp(x)](https://hal.inria.fr/inria-00070503/document)
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
<!--
|
|
||||||
View this file in Typedoc!
|
|
||||||
|
|
||||||
- At https://gpuweb.github.io/cts/docs/tsdoc/
|
|
||||||
- Or locally:
|
|
||||||
- npm run tsdoc
|
|
||||||
- npm start
|
|
||||||
- http://localhost:8080/docs/tsdoc/
|
|
||||||
|
|
||||||
This file is parsed as a tsdoc.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Index of Test Helpers
|
|
||||||
|
|
||||||
This index is a quick-reference of helper functions in the test suite.
|
|
||||||
Use it to determine whether you can reuse a helper, instead of writing new code,
|
|
||||||
to improve readability and reviewability.
|
|
||||||
|
|
||||||
Whenever a new generally-useful helper is added, it should be indexed here.
|
|
||||||
|
|
||||||
**See linked documentation for full helper listings.**
|
|
||||||
|
|
||||||
- {@link common/framework/params_builder!CaseParamsBuilder} and {@link common/framework/params_builder!SubcaseParamsBuilder}:
|
|
||||||
Combinatorial generation of test parameters. They are iterated by the test framework at runtime.
|
|
||||||
See `examples.spec.ts` for basic examples of how this behaves.
|
|
||||||
- {@link common/framework/params_builder!CaseParamsBuilder}:
|
|
||||||
`ParamsBuilder` for adding "cases" to a test.
|
|
||||||
- {@link common/framework/params_builder!CaseParamsBuilder#beginSubcases}:
|
|
||||||
"Finalizes" the `CaseParamsBuilder`, returning a `SubcaseParamsBuilder`.
|
|
||||||
- {@link common/framework/params_builder!SubcaseParamsBuilder}:
|
|
||||||
`ParamsBuilder` for adding "subcases" to a test.
|
|
||||||
|
|
||||||
### Fixtures
|
|
||||||
|
|
||||||
(Uncheck the "Inherited" box to hide inherited methods from documentation pages.)
|
|
||||||
|
|
||||||
- {@link common/framework/fixture!Fixture}: Base fixture for all tests.
|
|
||||||
- {@link webgpu/gpu_test!GPUTest}: Base fixture for WebGPU tests.
|
|
||||||
- {@link webgpu/api/validation/validation_test!ValidationTest}: Base fixture for WebGPU validation tests.
|
|
||||||
- {@link webgpu/shader/validation/shader_validation_test!ShaderValidationTest}: Base fixture for WGSL shader validation tests.
|
|
||||||
- {@link webgpu/idl/idl_test!IDLTest}:
|
|
||||||
Base fixture for testing the exposed interface is correct (without actually using WebGPU).
|
|
||||||
|
|
||||||
### WebGPU Helpers
|
|
||||||
|
|
||||||
- {@link webgpu/capability_info}: Structured information about texture formats, binding types, etc.
|
|
||||||
- {@link webgpu/constants}:
|
|
||||||
Constant values (needed anytime a WebGPU constant is needed outside of a test function).
|
|
||||||
- {@link webgpu/util/buffer}: Helpers for GPUBuffers.
|
|
||||||
- {@link webgpu/util/texture}: Helpers for GPUTextures.
|
|
||||||
- {@link webgpu/util/unions}: Helpers for various union typedefs in the WebGPU spec.
|
|
||||||
- {@link webgpu/util/math}: Helpers for common math operations.
|
|
||||||
- {@link webgpu/util/check_contents}: Check the contents of TypedArrays, with nice messages.
|
|
||||||
Also can be composed with {@link webgpu/gpu_test!GPUTest#expectGPUBufferValuesPassCheck}, used to implement
|
|
||||||
GPUBuffer checking helpers in GPUTest.
|
|
||||||
- {@link webgpu/util/conversion}: Numeric encoding/decoding for float/unorm/snorm values, etc.
|
|
||||||
- {@link webgpu/util/copy_to_texture}:
|
|
||||||
Helper class for copyToTexture test suites for execution copy and check results.
|
|
||||||
- {@link webgpu/util/color_space_conversion}:
|
|
||||||
Helper functions to do color space conversion. The algorithm is the same as defined in
|
|
||||||
CSS Color Module Level 4.
|
|
||||||
- {@link webgpu/util/create_elements}:
|
|
||||||
Helpers for creating web elements like HTMLCanvasElement, OffscreenCanvas, etc.
|
|
||||||
- {@link webgpu/util/shader}: Helpers for creating fragment shader based on intended output values, plainType, and componentCount.
|
|
||||||
- {@link webgpu/util/texture/base}: General texture-related helpers.
|
|
||||||
- {@link webgpu/util/texture/data_generation}: Helper for generating dummy texture data.
|
|
||||||
- {@link webgpu/util/texture/layout}: Helpers for working with linear image data
|
|
||||||
(like in copyBufferToTexture, copyTextureToBuffer, writeTexture).
|
|
||||||
- {@link webgpu/util/texture/subresource}: Helpers for working with texture subresource ranges.
|
|
||||||
- {@link webgpu/util/texture/texel_data}: Helpers encoding/decoding texel formats.
|
|
||||||
- {@link webgpu/util/texture/texel_view}: Helper class to create and view texture data through various representations.
|
|
||||||
- {@link webgpu/util/texture/texture_ok}: Helpers for checking texture contents.
|
|
||||||
- {@link webgpu/shader/types}: Helpers for WGSL data types.
|
|
||||||
- {@link webgpu/shader/execution/expression/expression}: Helpers for WGSL expression execution tests.
|
|
||||||
- {@link webgpu/web_platform/util}: Helpers for web platform features (e.g. video elements).
|
|
||||||
|
|
||||||
### General Helpers
|
|
||||||
|
|
||||||
- {@link common/framework/resources}: Provides the path to the `resources/` directory.
|
|
||||||
- {@link common/util/navigator_gpu}: Finds and returns the `navigator.gpu` object or equivalent.
|
|
||||||
- {@link common/util/util}: Miscellaneous utilities.
|
|
||||||
- {@link common/util/util!assert}: Assert a condition, otherwise throw an exception.
|
|
||||||
- {@link common/util/util!unreachable}: Assert unreachable code.
|
|
||||||
- {@link common/util/util!assertReject}, {@link common/util/util!resolveOnTimeout},
|
|
||||||
{@link common/util/util!rejectOnTimeout},
|
|
||||||
{@link common/util/util!raceWithRejectOnTimeout}, and more.
|
|
||||||
- {@link common/util/collect_garbage}:
|
|
||||||
Attempt to trigger garbage collection, for testing that garbage collection is not observable.
|
|
||||||
- {@link common/util/preprocessor}: A simple template-based, non-line-based preprocessor,
|
|
||||||
implementing if/elif/else/endif. Possibly useful for WGSL shader generation.
|
|
||||||
- {@link common/util/timeout}: Use this instead of `setTimeout`.
|
|
||||||
- {@link common/util/types}: Type metaprogramming helpers.
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
# Test Implementation
|
|
||||||
|
|
||||||
Concepts important to understand when writing tests. See existing tests for examples to copy from.
|
|
||||||
|
|
||||||
## Test fixtures
|
|
||||||
|
|
||||||
Most tests can use one of the several common test fixtures:
|
|
||||||
|
|
||||||
- `Fixture`: Base fixture, provides core functions like `expect()`, `skip()`.
|
|
||||||
- `GPUTest`: Wraps every test in error scopes. Provides helpers like `expectContents()`.
|
|
||||||
- `ValidationTest`: Extends `GPUTest`, provides helpers like `expectValidationError()`, `getErrorTextureView()`.
|
|
||||||
- Or create your own. (Often not necessary - helper functions can be used instead.)
|
|
||||||
|
|
||||||
Test fixtures or helper functions may be defined in `.spec.ts` files, but if used by multiple
|
|
||||||
test files, should be defined in separate `.ts` files (without `.spec`) alongside the files that
|
|
||||||
use them.
|
|
||||||
|
|
||||||
### GPUDevices in tests
|
|
||||||
|
|
||||||
`GPUDevice`s are largely stateless (except for `lost`-ness, error scope stack, and `label`).
|
|
||||||
This allows the CTS to reuse one device across multiple test cases using the `DevicePool`,
|
|
||||||
which provides `GPUDevice` objects to tests.
|
|
||||||
|
|
||||||
Currently, there is one `GPUDevice` with the default descriptor, and
|
|
||||||
a cache of several more, for devices with additional capabilities.
|
|
||||||
Devices in the `DevicePool` are automatically removed when certain things go wrong.
|
|
||||||
|
|
||||||
Later, there may be multiple `GPUDevice`s to allow multiple test cases to run concurrently.
|
|
||||||
|
|
||||||
## Test parameterization
|
|
||||||
|
|
||||||
The CTS provides helpers (`.params()` and friends) for creating large cartesian products of test parameters.
|
|
||||||
These generate "test cases" further subdivided into "test subcases".
|
|
||||||
See `basic,*` in `examples.spec.ts` for examples, and the [helper index](./helper_index.txt)
|
|
||||||
for a list of capabilities.
|
|
||||||
|
|
||||||
Test parameterization should be applied liberally to ensure the maximum coverage
|
|
||||||
possible within reasonable time. You can skip some with `.filter()`. And remember: computers are
|
|
||||||
pretty fast - thousands of test cases can be reasonable.
|
|
||||||
|
|
||||||
Use existing lists of parameters values (such as
|
|
||||||
[`kTextureFormats`](https://github.com/gpuweb/cts/blob/0f38b85/src/suites/cts/capability_info.ts#L61),
|
|
||||||
to parameterize tests), instead of making your own list. Use the info tables (such as
|
|
||||||
`kTextureFormatInfo`) to define and retrieve information about the parameters.
|
|
||||||
|
|
||||||
## Asynchrony in tests
|
|
||||||
|
|
||||||
Since there are no synchronous operations in WebGPU, almost every test is asynchronous in some
|
|
||||||
way. For example:
|
|
||||||
|
|
||||||
- Checking the result of a readback.
|
|
||||||
- Capturing the result of a `popErrorScope()`.
|
|
||||||
|
|
||||||
That said, test functions don't always need to be `async`; see below.
|
|
||||||
|
|
||||||
### Checking asynchronous errors/results
|
|
||||||
|
|
||||||
Validation is inherently asynchronous (`popErrorScope()` returns a promise). However, the error
|
|
||||||
scope stack itself is synchronous - operations immediately after a `popErrorScope()` are outside
|
|
||||||
that error scope.
|
|
||||||
|
|
||||||
As a result, tests can assert things like validation errors/successes without having an `async`
|
|
||||||
test body.
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
t.expectValidationError(() => {
|
|
||||||
device.createThing();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
does:
|
|
||||||
|
|
||||||
- `pushErrorScope('validation')`
|
|
||||||
- `popErrorScope()` and "eventually" check whether it returned an error.
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
t.expectGPUBufferValuesEqual(srcBuffer, expectedData);
|
|
||||||
```
|
|
||||||
|
|
||||||
does:
|
|
||||||
|
|
||||||
- copy `srcBuffer` into a new mappable buffer `dst`
|
|
||||||
- `dst.mapReadAsync()`, and "eventually" check what data it returned.
|
|
||||||
|
|
||||||
Internally, this is accomplished via an "eventual expectation": `eventualAsyncExpectation()`
|
|
||||||
takes an async function, calls it immediately, and stores off the resulting `Promise` to
|
|
||||||
automatically await at the end before determining the pass/fail state.
|
|
||||||
|
|
||||||
### Asynchronous parallelism
|
|
||||||
|
|
||||||
A side effect of test asynchrony is that it's possible for multiple tests to be in flight at
|
|
||||||
once. We do not currently do this, but it will eventually be an option to run `N` tests in
|
|
||||||
"parallel", for faster local test runs.
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
# Introduction
|
|
||||||
|
|
||||||
These documents contains guidelines for contributors to the WebGPU CTS (Conformance Test Suite)
|
|
||||||
on how to write effective tests, and on the testing philosophy to adopt.
|
|
||||||
|
|
||||||
The WebGPU CTS is arguably more important than the WebGPU specification itself, because
|
|
||||||
it is what forces implementation to be interoperable by checking they conform to the specification.
|
|
||||||
However writing a CTS is hard and requires a lot of effort to reach good coverage.
|
|
||||||
|
|
||||||
More than a collection of tests like regular end2end and unit tests for software artifacts, a CTS
|
|
||||||
needs to be exhaustive. Contrast for example the WebGL2 CTS with the ANGLE end2end tests: they
|
|
||||||
cover the same functionality (WebGL 2 / OpenGL ES 3) but are structured very differently:
|
|
||||||
|
|
||||||
- ANGLE's test suite has one or two tests per functionality to check it works correctly, plus
|
|
||||||
regression tests and special tests to cover implementation details.
|
|
||||||
- WebGL2's CTS can have thousands of tests per API aspect to cover every combination of
|
|
||||||
parameters (and global state) used by an operation.
|
|
||||||
|
|
||||||
Below are guidelines based on our collective experience with graphics API CTSes like WebGL's.
|
|
||||||
They are expected to evolve over time and have exceptions, but should give a general idea of what
|
|
||||||
to do.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Testing tasks are tracked in the [CTS project tracker](https://github.com/orgs/gpuweb/projects/3).
|
|
||||||
Go here if you're looking for tasks, or if you have a test idea that isn't already covered.
|
|
||||||
|
|
||||||
If contributing conformance tests, the directory you'll work in is [`src/webgpu/`](../src/webgpu/).
|
|
||||||
This directory is organized according to the goal of the test (API validation behavior vs
|
|
||||||
actual results) and its target (API entry points and spec areas, e.g. texture sampling).
|
|
||||||
|
|
||||||
The contents of a test file (`src/webgpu/**/*.spec.ts`) are twofold:
|
|
||||||
|
|
||||||
- Documentation ("test plans") on what tests do, how they do it, and what cases they cover.
|
|
||||||
Some test plans are fully or partially unimplemented:
|
|
||||||
they either contain "TODO" in a description or are `.unimplemented()`.
|
|
||||||
- Actual tests.
|
|
||||||
|
|
||||||
**Please read the following short documents before contributing.**
|
|
||||||
|
|
||||||
### 0. [Developing](developing.md)
|
|
||||||
|
|
||||||
- Reviewers should also read [Review Requirements](../reviews.md).
|
|
||||||
|
|
||||||
### 1. [Life of a Test Change](life_of.md)
|
|
||||||
|
|
||||||
### 2. [Adding or Editing Test Plans](plans.md)
|
|
||||||
|
|
||||||
### 3. [Implementing Tests](tests.md)
|
|
||||||
|
|
||||||
## [Additional Documentation](../)
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Operation testing of vertex input id generation
|
|
||||||
|
|
||||||
This section provides an example of the planning process for a test.
|
|
||||||
It has not been refined into a set of final test plan descriptions.
|
|
||||||
(Note: this predates the actual implementation of these tests, so doesn't match the actual tests.)
|
|
||||||
|
|
||||||
Somewhere under the `api/operation` node are tests checking that running `GPURenderPipelines` on
|
|
||||||
the device using the `GPURenderEncoderBase.draw` family of functions works correctly. Render
|
|
||||||
pipelines are composed of several stages that are mostly independent so they can be split in
|
|
||||||
several parts such as `vertex_input`, `rasterization`, `blending`.
|
|
||||||
|
|
||||||
Vertex input itself has several parts that are mostly separate in hardware:
|
|
||||||
|
|
||||||
- generation of the vertex and instance indices to run for this draw
|
|
||||||
- fetching of vertex data from vertex buffers based on these indices
|
|
||||||
- conversion from the vertex attribute `GPUVertexFormat` to the datatype for the input variable
|
|
||||||
in the shader
|
|
||||||
|
|
||||||
Each of these are tested separately and have cases for each combination of the variables that may
|
|
||||||
affect them. This means that `api/operation/render/vertex_input/id_generation` checks that the
|
|
||||||
correct operation is performed for the cartesian product of all the following dimensions:
|
|
||||||
|
|
||||||
- for encoding in a `GPURenderPassEncoder` or a `GPURenderBundleEncoder`
|
|
||||||
- whether the draw is direct or indirect
|
|
||||||
- whether the draw is indexed or not
|
|
||||||
- for various values of the `firstInstance` argument
|
|
||||||
- for various values of the `instanceCount` argument
|
|
||||||
- if the draw is not indexed:
|
|
||||||
- for various values of the `firstVertex` argument
|
|
||||||
- for various values of the `vertexCount` argument
|
|
||||||
- if the draw is indexed:
|
|
||||||
- for each `GPUIndexFormat`
|
|
||||||
- for various values of the indices in the index buffer including the primitive restart values
|
|
||||||
- for various values for the `offset` argument to `setIndexBuffer`
|
|
||||||
- for various values of the `firstIndex` argument
|
|
||||||
- for various values of the `indexCount` argument
|
|
||||||
- for various values of the `baseVertex` argument
|
|
||||||
|
|
||||||
"Various values" above mean several small values, including `0` and the second smallest valid
|
|
||||||
value to check for corner cases, as well as some large value.
|
|
||||||
|
|
||||||
An instance of the test sets up a `draw*` call based on the parameters, using point rendering and
|
|
||||||
a fragment shader that outputs to a storage buffer. After the draw the test checks the content of
|
|
||||||
the storage buffer to make sure all expected vertex shader invocation, and only these ones have
|
|
||||||
been generated.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -1,134 +0,0 @@
|
||||||
# Developing
|
|
||||||
|
|
||||||
The WebGPU CTS is written in TypeScript.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
After checking out the repository and installing node/npm, run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm ci
|
|
||||||
```
|
|
||||||
|
|
||||||
Before uploading, you can run pre-submit checks (`npm test`) to make sure it will pass CI.
|
|
||||||
Use `npm run fix` to fix linting issues.
|
|
||||||
|
|
||||||
`npm run` will show available npm scripts.
|
|
||||||
Some more scripts can be listed using `npx grunt`.
|
|
||||||
|
|
||||||
## Dev Server
|
|
||||||
|
|
||||||
To start the development server, use:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, browse to the standalone test runner at the printed URL.
|
|
||||||
|
|
||||||
The server will generate and compile code on the fly, so no build step is necessary.
|
|
||||||
Only a reload is needed to see saved changes.
|
|
||||||
(TODO: except, currently, `README.txt` and file `description` changes won't be reflected in
|
|
||||||
the standalone runner.)
|
|
||||||
|
|
||||||
Note: The first load of a test suite may take some time as generating the test suite listing can
|
|
||||||
take a few seconds.
|
|
||||||
|
|
||||||
## Standalone Test Runner / Test Plan Viewer
|
|
||||||
|
|
||||||
**The standalone test runner also serves as a test plan viewer.**
|
|
||||||
(This can be done in a browser without WebGPU support.)
|
|
||||||
You can use this to preview how your test plan will appear.
|
|
||||||
|
|
||||||
You can view different suites (webgpu, unittests, stress, etc.) or different subtrees of
|
|
||||||
the test suite.
|
|
||||||
|
|
||||||
- `http://localhost:8080/standalone/` (defaults to `?runnow=0&worker=0&debug=0&q=webgpu:*`)
|
|
||||||
- `http://localhost:8080/standalone/?q=unittests:*`
|
|
||||||
- `http://localhost:8080/standalone/?q=unittests:basic:*`
|
|
||||||
|
|
||||||
The following url parameters change how the harness runs:
|
|
||||||
|
|
||||||
- `runnow=1` runs all matching tests on page load.
|
|
||||||
- `debug=1` enables verbose debug logging from tests.
|
|
||||||
- `worker=1` runs the tests on a Web Worker instead of the main thread.
|
|
||||||
- `power_preference=low-power` runs most tests passing `powerPreference: low-power` to `requestAdapter`
|
|
||||||
- `power_preference=high-performance` runs most tests passing `powerPreference: high-performance` to `requestAdapter`
|
|
||||||
|
|
||||||
### Web Platform Tests (wpt) - Ref Tests
|
|
||||||
|
|
||||||
You can inspect the actual and reference pages for web platform reftests in the standalone
|
|
||||||
runner by navigating to them. For example, by loading:
|
|
||||||
|
|
||||||
- `http://localhost:8080/out/webgpu/web_platform/reftests/canvas_clear.https.html`
|
|
||||||
- `http://localhost:8080/out/webgpu/web_platform/reftests/ref/canvas_clear-ref.html`
|
|
||||||
|
|
||||||
You can also run a minimal ref test runner.
|
|
||||||
|
|
||||||
- open 2 terminals / command lines.
|
|
||||||
- in one, `npm start`
|
|
||||||
- in the other, `node tools/run_wpt_ref_tests <path-to-browser-executable> [name-of-test]`
|
|
||||||
|
|
||||||
Without `[name-of-test]` all ref tests will be run. `[name-of-test]` is just a simple check for
|
|
||||||
substring so passing in `rgba` will run every test with `rgba` in its filename.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
MacOS
|
|
||||||
|
|
||||||
```
|
|
||||||
# Chrome
|
|
||||||
node tools/run_wpt_ref_tests /Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary
|
|
||||||
```
|
|
||||||
|
|
||||||
Windows
|
|
||||||
|
|
||||||
```
|
|
||||||
# Chrome
|
|
||||||
node .\tools\run_wpt_ref_tests "C:\Users\your-user-name\AppData\Local\Google\Chrome SxS\Application\chrome.exe"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Editor
|
|
||||||
|
|
||||||
Since this project is written in TypeScript, it integrates best with
|
|
||||||
[Visual Studio Code](https://code.visualstudio.com/).
|
|
||||||
This is optional, but highly recommended: it automatically adds `import` lines and
|
|
||||||
provides robust completions, cross-references, renames, error highlighting,
|
|
||||||
deprecation highlighting, and type/JSDoc popups.
|
|
||||||
|
|
||||||
Open the `cts.code-workspace` workspace file to load settings convenient for this project.
|
|
||||||
You can make local configuration changes in `.vscode/`, which is untracked by Git.
|
|
||||||
|
|
||||||
## Pull Requests
|
|
||||||
|
|
||||||
When opening a pull request, fill out the PR checklist and attach the issue number.
|
|
||||||
If an issue hasn't been opened, find the draft issue on the
|
|
||||||
[project tracker](https://github.com/orgs/gpuweb/projects/3) and choose "Convert to issue":
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Opening a pull request will automatically notify reviewers.
|
|
||||||
|
|
||||||
To make the review process smoother, once a reviewer has started looking at your change:
|
|
||||||
|
|
||||||
- Avoid major additions or changes that would be best done in a follow-up PR.
|
|
||||||
- Avoid rebases (`git rebase`) and force pushes (`git push -f`). These can make
|
|
||||||
it difficult for reviewers to review incremental changes as GitHub often cannot
|
|
||||||
view a useful diff across a rebase. If it's necessary to resolve conflicts
|
|
||||||
with upstream changes, use a merge commit (`git merge`) and don't include any
|
|
||||||
consequential changes in the merge, so a reviewer can skip over merge commits
|
|
||||||
when working through the individual commits in the PR.
|
|
||||||
- When you address a review comment, mark the thread as "Resolved".
|
|
||||||
|
|
||||||
Pull requests will (usually) be landed with the "Squash and merge" option.
|
|
||||||
|
|
||||||
### TODOs
|
|
||||||
|
|
||||||
The word "TODO" refers to missing test coverage. It may only appear inside file/test descriptions
|
|
||||||
and README files (enforced by linting).
|
|
||||||
|
|
||||||
To use comments to refer to TODOs inside the description, use a backreference, e.g., in the
|
|
||||||
description, `TODO: Also test the FROBNICATE usage flag [1]`, and somewhere in the code, `[1]:
|
|
||||||
Need to add FROBNICATE to this list.`.
|
|
||||||
|
|
||||||
Use `MAINTENANCE_TODO` for TODOs which don't impact test coverage.
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
# Life of a Test Change
|
|
||||||
|
|
||||||
A "test change" could be a new test, an expansion of an existing test, a test bug fix, or a
|
|
||||||
modification to existing tests to make them match new spec changes.
|
|
||||||
|
|
||||||
**CTS contributors should contribute to the tracker and strive to keep it up to date, especially
|
|
||||||
relating to their own changes.**
|
|
||||||
|
|
||||||
Filing new draft issues in the CTS project tracker is very lightweight.
|
|
||||||
Anyone with access should do this eagerly, to ensure no testing ideas are forgotten.
|
|
||||||
(And if you don't have access, just file a regular issue.)
|
|
||||||
|
|
||||||
1. Enter a [draft issue](https://github.com/orgs/gpuweb/projects/3), with the Status
|
|
||||||
set to "New (not in repo)", and any available info included in the issue description
|
|
||||||
(notes/plans to ensure full test coverage of the change). The source of this may be:
|
|
||||||
|
|
||||||
- Anything in the spec/API that is found not to be covered by the CTS yet.
|
|
||||||
- Any test is found to be outdated or otherwise buggy.
|
|
||||||
- A spec change from the "Needs CTS Issue" column in the
|
|
||||||
[spec project tracker](https://github.com/orgs/gpuweb/projects/1).
|
|
||||||
Once information on the required test changes is entered into the CTS project tracker,
|
|
||||||
the spec issue moves to "Specification Done".
|
|
||||||
|
|
||||||
Note: at some point, someone may make a PR to flush "New (not in repo)" issues into `TODO`s in
|
|
||||||
CTS file/test description text, changing their "Status" to "Open".
|
|
||||||
These may be done in bulk without linking back to the issue.
|
|
||||||
|
|
||||||
1. As necessary:
|
|
||||||
|
|
||||||
- Convert the draft issue to a full, numbered issue for linking from later PRs.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Update the "Assignees" of the issue when an issue is assigned or unassigned
|
|
||||||
(you can assign yourself).
|
|
||||||
- Change the "Status" of the issue to "Started" once you start the task.
|
|
||||||
|
|
||||||
1. Open one or more PRs, **each linking to the associated issue**.
|
|
||||||
Each PR may is reviewed and landed, and may leave further TODOs for parts it doesn't complete.
|
|
||||||
|
|
||||||
1. Test are "planned" in test descriptions. (For complex tests, open a separate PR with the
|
|
||||||
tests `.unimplemented()` so a reviewer can evaluate the plan before you implement tests.)
|
|
||||||
1. Tests are implemented.
|
|
||||||
|
|
||||||
1. When **no TODOs remain** for an issue, close it and change its status to "Complete".
|
|
||||||
(Enter a new more, specific draft issue into the tracker if you need to track related TODOs.)
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
# Adding or Editing Test Plans
|
|
||||||
|
|
||||||
## 1. Write a test plan
|
|
||||||
|
|
||||||
For new tests, if some notes exist already, incorporate them into your plan.
|
|
||||||
|
|
||||||
A detailed test plan should be written and reviewed before substantial test code is written.
|
|
||||||
This allows reviewers a chance to identify additional tests and cases, opportunities for
|
|
||||||
generalizations that would improve the strength of tests, similar existing tests or test plans,
|
|
||||||
and potentially useful [helpers](../helper_index.txt).
|
|
||||||
|
|
||||||
**A test plan must serve two functions:**
|
|
||||||
|
|
||||||
- Describes the test, succinctly, but in enough detail that a reader can read *only* the test
|
|
||||||
plans and evaluate coverage completeness of a file/directory.
|
|
||||||
- Describes the test precisely enough that, when code is added, the reviewer can ensure that the
|
|
||||||
test really covers what the test plan says.
|
|
||||||
|
|
||||||
There should be one test plan for each test. It should describe what it tests, how, and describe
|
|
||||||
important cases that need to be covered. Here's an example:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
g.test('x,some_detail')
|
|
||||||
.desc(
|
|
||||||
`
|
|
||||||
Tests [some detail] about x. Tests calling x in various 'mode's { mode1, mode2 },
|
|
||||||
with various values of 'arg', and checks correctness of the result.
|
|
||||||
Tries to trigger [some conditional path].
|
|
||||||
|
|
||||||
- Valid values (control case) // <- (to make sure the test function works well)
|
|
||||||
- Unaligned values (should fail) // <- (only validation tests need to intentionally hit invalid cases)
|
|
||||||
- Extreme values`
|
|
||||||
)
|
|
||||||
.params(u =>
|
|
||||||
u //
|
|
||||||
.combine('mode', ['mode1', 'mode2'])
|
|
||||||
.beginSubcases()
|
|
||||||
.combine('arg', [
|
|
||||||
// Valid // <- Comment params as you see fit.
|
|
||||||
4,
|
|
||||||
8,
|
|
||||||
100,
|
|
||||||
// Invalid
|
|
||||||
2,
|
|
||||||
6,
|
|
||||||
1e30,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
.unimplemented();
|
|
||||||
```
|
|
||||||
|
|
||||||
"Cases" each appear as individual items in the `/standalone/` runner.
|
|
||||||
"Subcases" run inside each case, like a for-loop wrapping the `.fn(`test function`)`.
|
|
||||||
Documentation on the parameter builder can be found in the [helper index](../helper_index.txt).
|
|
||||||
|
|
||||||
It's often impossible to predict the exact case/subcase structure before implementing tests, so they
|
|
||||||
can be added during implementation, instead of planning.
|
|
||||||
|
|
||||||
For any notes which are not specific to a single test, or for preliminary notes for tests that
|
|
||||||
haven't been planned in full detail, put them in the test file's `description` variable at
|
|
||||||
the top. Or, if they aren't associated with a test file, put them in a `README.txt` file.
|
|
||||||
|
|
||||||
**Any notes about missing test coverage must be marked with the word `TODO` inside a
|
|
||||||
description or README.** This makes them appear on the `/standalone/` page.
|
|
||||||
|
|
||||||
## 2. Open a pull request
|
|
||||||
|
|
||||||
Open a PR, and work with the reviewer(s) to revise the test plan.
|
|
||||||
|
|
||||||
Usually (probably), plans will be landed in separate PRs before test implementations.
|
|
||||||
|
|
||||||
## Conventions used in test plans
|
|
||||||
|
|
||||||
- `Iff`: If and only if
|
|
||||||
- `x=`: "cartesian-cross equals", like `+=` for cartesian product.
|
|
||||||
Used for combinatorial test coverage.
|
|
||||||
- Sometimes this will result in too many test cases; simplify/reduce as needed
|
|
||||||
during planning *or* implementation.
|
|
||||||
- `{x,y,z}`: list of cases to test
|
|
||||||
- e.g. `x= texture format {r8unorm, r8snorm}`
|
|
||||||
- *Control case*: a case included to make sure that the rest of the cases aren't
|
|
||||||
missing their target by testing some other error case.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
# Implementing Tests
|
|
||||||
|
|
||||||
Once a test plan is done, you can start writing tests.
|
|
||||||
To add new tests, imitate the pattern in neigboring tests or neighboring files.
|
|
||||||
New test files must be named ending in `.spec.ts`.
|
|
||||||
|
|
||||||
For an example test file, see [`src/webgpu/examples.spec.ts`](../../src/webgpu/examples.spec.ts).
|
|
||||||
For a more complex, well-structured reference test file, see
|
|
||||||
[`src/webgpu/api/validation/vertex_state.spec.ts`](../../src/webgpu/api/validation/vertex_state.spec.ts).
|
|
||||||
|
|
||||||
Implement some tests and open a pull request. You can open a PR any time you're ready for a review.
|
|
||||||
(If two tests are non-trivial but independent, consider separate pull requests.)
|
|
||||||
|
|
||||||
Before uploading, you can run pre-submit checks (`npm test`) to make sure it will pass CI.
|
|
||||||
Use `npm run fix` to fix linting issues.
|
|
||||||
|
|
||||||
## Test Helpers
|
|
||||||
|
|
||||||
It's best to be familiar with helpers available in the test suite for simplifying
|
|
||||||
test implementations.
|
|
||||||
|
|
||||||
New test helpers can be added at any time to either of those files, or to new `.ts` files anywhere
|
|
||||||
near the `.spec.ts` file where they're used.
|
|
||||||
|
|
||||||
Documentation on existing helpers can be found in the [helper index](../helper_index.txt).
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
||||||
# Test Organization
|
|
||||||
|
|
||||||
## `src/webgpu/`
|
|
||||||
|
|
||||||
Because of the glorious amount of test needed, the WebGPU CTS is organized as a tree of arbitrary
|
|
||||||
depth (a filesystem with multiple tests per file).
|
|
||||||
|
|
||||||
Each directory may have a `README.txt` describing its contents.
|
|
||||||
Tests are grouped in large families (each of which has a `README.txt`);
|
|
||||||
the root and first few levels looks like the following (some nodes omitted for simplicity):
|
|
||||||
|
|
||||||
- **`api`** with tests for full coverage of the Javascript API surface of WebGPU.
|
|
||||||
- **`validation`** with positive and negative tests for all the validation rules of the API.
|
|
||||||
- **`operation`** with tests that checks the result of performing valid WebGPU operations,
|
|
||||||
taking advantage of parametrization to exercise interactions between parts of the API.
|
|
||||||
- **`regression`** for one-off tests that reproduce bugs found in implementations to prevent
|
|
||||||
the bugs from appearing again.
|
|
||||||
- **`shader`** with tests for full coverage of the shaders that can be passed to WebGPU.
|
|
||||||
- **`validation`**.
|
|
||||||
- **`execution`** similar to `api/operation`.
|
|
||||||
- **`regression`**.
|
|
||||||
- **`idl`** with tests to check that the WebGPU IDL is correctly implemented, for examples that
|
|
||||||
objects exposed exactly the correct members, and that methods throw when passed incomplete
|
|
||||||
dictionaries.
|
|
||||||
- **`web-platform`** with tests for Web platform-specific interactions like `GPUSwapChain` and
|
|
||||||
`<canvas>`, WebXR and `GPUQueue.copyExternalImageToTexture`.
|
|
||||||
|
|
||||||
At the same time test hierarchies can be used to split the testing of a single sub-object into
|
|
||||||
several file for maintainability. For example `GPURenderPipeline` has a large descriptor and some
|
|
||||||
parts could be tested independently like `vertex_input` vs. `primitive_topology` vs. `blending`
|
|
||||||
but all live under the `render_pipeline` directory.
|
|
||||||
|
|
||||||
In addition to the test tree, each test can be parameterized. For coverage it is important to
|
|
||||||
test all enums values, for example for `GPUTextureFormat`. Instead of having a loop to iterate
|
|
||||||
over all the `GPUTextureFormat`, it is better to parameterize the test over them. Each format
|
|
||||||
will have a different entry in the test list which will help WebGPU implementers debug the test,
|
|
||||||
or suppress the failure without losing test coverage while they fix the bug.
|
|
||||||
|
|
||||||
Extra capabilities (limits and features) are often tested in the same files as the rest of the API.
|
|
||||||
For example, a compressed texture format capability would simply add a `GPUTextureFormat` to the
|
|
||||||
parametrization lists of many tests, while a capability adding significant new functionality
|
|
||||||
like ray-tracing could have a separate subtree.
|
|
||||||
|
|
||||||
Operation tests for optional features should be skipped using `t.selectDeviceOrSkipTestCase()` or
|
|
||||||
`t.skip()`. Validation tests should be written that test the behavior with and without the
|
|
||||||
capability enabled via `t.selectDeviceOrSkipTestCase()`, to ensure the functionality is valid
|
|
||||||
only with the capability enabled.
|
|
||||||
|
|
||||||
### Validation tests
|
|
||||||
|
|
||||||
Validation tests check the validation rules that are (or will be) set by the
|
|
||||||
WebGPU spec. Validation tests try to carefully trigger the individual validation
|
|
||||||
rules in the spec, without simultaneously triggering other rules.
|
|
||||||
|
|
||||||
Validation errors *generally* generate WebGPU errors, not exceptions.
|
|
||||||
But check the spec on a case-by-case basis.
|
|
||||||
|
|
||||||
Like all `GPUTest`s, `ValidationTest`s are wrapped in both types of error scope. These
|
|
||||||
"catch-all" error scopes look for any errors during the test, and report them as test failures.
|
|
||||||
Since error scopes can be nested, validation tests can nest an error scope to expect that there
|
|
||||||
*are* errors from specific operations.
|
|
||||||
|
|
||||||
#### Parameterization
|
|
||||||
|
|
||||||
Test parameterization can help write many validation tests more succinctly,
|
|
||||||
while making it easier for both authors and reviewers to be confident that
|
|
||||||
an aspect of the API is tested fully. Examples:
|
|
||||||
|
|
||||||
- [`webgpu:api,validation,render_pass,resolve:resolve_attachment:*`](https://github.com/gpuweb/cts/blob/ded3b7c8a4680a1a01621a8ac859facefadf32d0/src/webgpu/api/validation/render_pass/resolve.spec.ts#L35)
|
|
||||||
- [`webgpu:api,validation,createBindGroupLayout:bindingTypeSpecific_optional_members:*`](https://github.com/gpuweb/cts/blob/ded3b7c8a4680a1a01621a8ac859facefadf32d0/src/webgpu/api/validation/createBindGroupLayout.spec.ts#L68)
|
|
||||||
|
|
||||||
Use your own discretion when deciding the balance between heavily parameterizing
|
|
||||||
a test and writing multiple separate tests.
|
|
||||||
|
|
||||||
#### Guidelines
|
|
||||||
|
|
||||||
There are many aspects that should be tested in all validation tests:
|
|
||||||
|
|
||||||
- each individual argument to a method call (including `this`) or member of a descriptor
|
|
||||||
dictionary should be tested including:
|
|
||||||
- what happens when an error object is passed.
|
|
||||||
- what happens when an optional feature enum or method is used.
|
|
||||||
- what happens for numeric values when they are at 0, too large, too small, etc.
|
|
||||||
- each validation rule in the specification should be checked both with a control success case,
|
|
||||||
and error cases.
|
|
||||||
- each set of arguments or state that interact for validation.
|
|
||||||
|
|
||||||
When testing numeric values, it is important to check on both sides of the boundary: if the error
|
|
||||||
happens for value N and not N - 1, both should be tested. Alignment of integer values should also
|
|
||||||
be tested but boundary testing of alignment should be between a value aligned to 2^N and a value
|
|
||||||
aligned to 2^(N-1).
|
|
||||||
|
|
||||||
Finally, this is probably also where we would test that extensions follow the rule that: if the
|
|
||||||
browser supports a feature but it is not enabled on the device, then calling methods from that
|
|
||||||
feature throws `TypeError`.
|
|
||||||
|
|
||||||
- Test providing unknown properties *that are definitely not part of any feature* are
|
|
||||||
valid/ignored. (Unfortunately, due to the rules of IDL, adding a member to a dictionary is
|
|
||||||
always a breaking change. So this is how we have to test this unless we can get a "strict"
|
|
||||||
dictionary type in IDL. We can't test adding members from non-enabled extensions.)
|
|
||||||
|
|
||||||
### Operation tests
|
|
||||||
|
|
||||||
Operation tests test the actual results of using the API. They execute
|
|
||||||
(sometimes significant) code and check that the result is within the expected
|
|
||||||
set of behaviors (which can be quite complex to compute).
|
|
||||||
|
|
||||||
Note that operation tests need to test a lot of interactions between different
|
|
||||||
parts of the API, and so can become quite complex. Try to reduce the complexity by
|
|
||||||
utilizing combinatorics and [helpers](./helper_index.txt), and splitting/merging test files as needed.
|
|
||||||
|
|
||||||
#### Errors
|
|
||||||
|
|
||||||
Operation tests are usually `GPUTest`s. As a result, they automatically fail on any validation
|
|
||||||
errors that occur during the test.
|
|
||||||
|
|
||||||
When it's easier to write an operation test with invalid cases, use
|
|
||||||
`ParamsBuilder.filter`/`.unless` to avoid invalid cases, or detect and
|
|
||||||
`expect` validation errors in some cases.
|
|
||||||
|
|
||||||
#### Implementation
|
|
||||||
|
|
||||||
Use helpers like `expectContents` (and more to come) to check the values of data on the GPU.
|
|
||||||
(These are "eventual expectations" - the harness will wait for them to finish at the end).
|
|
||||||
|
|
||||||
When testing something inside a shader, it's not always necessary to output the result to a
|
|
||||||
render output. In fragment shaders, you can output to a storage buffer. In vertex shaders, you
|
|
||||||
can't - but you can render with points (simplest), send the result to the fragment shader, and
|
|
||||||
output it from there. (Someday, we may end up wanting a helper for this.)
|
|
||||||
|
|
||||||
#### Testing Default Values
|
|
||||||
|
|
||||||
Default value tests (for arguments and dictionary members) should usually be operation tests -
|
|
||||||
all you have to do is include `undefined` in parameterizations of other tests to make sure the
|
|
||||||
behavior with `undefined` has the same expected result that you have when the default value is
|
|
||||||
specified explicitly.
|
|
||||||
|
|
||||||
### IDL tests
|
|
||||||
|
|
||||||
TODO: figure out how to implement these. https://github.com/gpuweb/cts/issues/332
|
|
||||||
|
|
||||||
These tests test only rules that come directly from WebIDL. For example:
|
|
||||||
|
|
||||||
- Values out of range for `[EnforceRange]` cause exceptions.
|
|
||||||
- Required function arguments and dictionary members cause exceptions if omitted.
|
|
||||||
- Arguments and dictionary members cause exceptions if passed the wrong type.
|
|
||||||
|
|
||||||
They may also test positive cases like the following, but the behavior of these should be tested in
|
|
||||||
operation tests.
|
|
||||||
|
|
||||||
- OK to omit optional arguments/members.
|
|
||||||
- OK to pass the correct argument/member type (or of any type in a union type).
|
|
||||||
|
|
||||||
Every overload of every method should be tested.
|
|
||||||
|
|
||||||
## `src/stress/`, `src/manual/`
|
|
||||||
|
|
||||||
Stress tests and manual tests for WebGPU that are not intended to be run in an automated way.
|
|
||||||
|
|
||||||
## `src/unittests/`
|
|
||||||
|
|
||||||
Unit tests for the test framework (`src/common/framework/`).
|
|
||||||
|
|
||||||
## `src/demo/`
|
|
||||||
|
|
||||||
A demo of test hierarchies for the purpose of testing the `standalone` test runner page.
|
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
# Review Requirements
|
|
||||||
|
|
||||||
A review should have several items checked off before it is landed.
|
|
||||||
Checkboxes are pre-filled into the pull request summary when it's created.
|
|
||||||
|
|
||||||
The uploader may pre-check-off boxes if they are not applicable
|
|
||||||
(e.g. TypeScript readability on a plan PR).
|
|
||||||
|
|
||||||
## Readability
|
|
||||||
|
|
||||||
A reviewer has "readability" for a topic if they have enough expertise in that topic to ensure
|
|
||||||
good practices are followed in pull requests, or know when to loop in other reviewers.
|
|
||||||
Perfection is not required!
|
|
||||||
|
|
||||||
**It is up to reviewers' own discretion** whether they are qualified to check off a
|
|
||||||
"readability" checkbox on any given pull request.
|
|
||||||
|
|
||||||
- WebGPU Readability: Familiarity with the API to ensure:
|
|
||||||
|
|
||||||
- WebGPU is being used correctly; expected results seem reasonable.
|
|
||||||
- WebGPU is being tested completely; tests have control cases.
|
|
||||||
- Test code has a clear correspondence with the test description.
|
|
||||||
- [Test helpers](./helper_index.txt) are used or created appropriately
|
|
||||||
(where the reviewer is familiar with the helpers).
|
|
||||||
|
|
||||||
- TypeScript Readability: Make sure TypeScript is utilized in a way that:
|
|
||||||
|
|
||||||
- Ensures test code is reasonably type-safe.
|
|
||||||
Reviewers may recommend changes to make type-safety either weaker (`as`, etc.) or stronger.
|
|
||||||
- Is understandable and has appropriate verbosity and dynamicity
|
|
||||||
(e.g. type inference and `as const` are used to reduce unnecessary boilerplate).
|
|
||||||
|
|
||||||
## Plan Reviews
|
|
||||||
|
|
||||||
**Changes *must* have an author or reviewer with the following readability:** WebGPU
|
|
||||||
|
|
||||||
Reviewers must carefully ensure the following:
|
|
||||||
|
|
||||||
- The test plan name accurately describes the area being tested.
|
|
||||||
- The test plan covers the area described by the file/test name and file/test description
|
|
||||||
as fully as possible (or adds TODOs for incomplete areas).
|
|
||||||
- Validation tests have control cases (where no validation error should occur).
|
|
||||||
- Each validation rule is tested in isolation, in at least one case which does not validate any
|
|
||||||
other validation rules.
|
|
||||||
|
|
||||||
See also: [Adding or Editing Test Plans](intro/plans.md).
|
|
||||||
|
|
||||||
## Implementation Reviews
|
|
||||||
|
|
||||||
**Changes *must* have an author or reviewer with the following readability:** WebGPU, TypeScript
|
|
||||||
|
|
||||||
Reviewers must carefully ensure the following:
|
|
||||||
|
|
||||||
- The coverage of the test implementation precisely matches the test description.
|
|
||||||
- Everything required for test plan reviews above.
|
|
||||||
|
|
||||||
Reviewers should ensure the following:
|
|
||||||
|
|
||||||
- New test helpers are documented in [helper index](./helper_index.txt).
|
|
||||||
- Framework and test helpers are used where they would make test code clearer.
|
|
||||||
|
|
||||||
See also: [Implementing Tests](intro/tests.md).
|
|
||||||
|
|
||||||
## Framework
|
|
||||||
|
|
||||||
**Changes *must* have an author or reviewer with the following readability:** TypeScript
|
|
||||||
|
|
||||||
Reviewers should ensure the following:
|
|
||||||
|
|
||||||
- Changes are reasonably type-safe, and covered by unit tests where appropriate.
|
|
||||||
|
|
@ -1,270 +0,0 @@
|
||||||
# Terminology
|
|
||||||
|
|
||||||
Each test suite is organized as a tree, both in the filesystem and further within each file.
|
|
||||||
|
|
||||||
- _Suites_, e.g. `src/webgpu/`.
|
|
||||||
- _READMEs_, e.g. `src/webgpu/README.txt`.
|
|
||||||
- _Test Spec Files_, e.g. `src/webgpu/examples.spec.ts`.
|
|
||||||
Identified by their file path.
|
|
||||||
Each test spec file provides a description and a _Test Group_.
|
|
||||||
A _Test Group_ defines a test fixture, and contains multiple:
|
|
||||||
- _Tests_.
|
|
||||||
Identified by a comma-separated list of parts (e.g. `basic,async`)
|
|
||||||
which define a path through a filesystem-like tree (analogy: `basic/async.txt`).
|
|
||||||
Defines a _test function_ and contains multiple:
|
|
||||||
- _Test Cases_.
|
|
||||||
Identified by a list of _Public Parameters_ (e.g. `x` = `1`, `y` = `2`).
|
|
||||||
Each Test Case has the same test function but different Public Parameters.
|
|
||||||
|
|
||||||
## Test Tree
|
|
||||||
|
|
||||||
A _Test Tree_ is a tree whose leaves are individual Test Cases.
|
|
||||||
|
|
||||||
A Test Tree can be thought of as follows:
|
|
||||||
|
|
||||||
- Suite, which is the root of a tree with "leaves" which are:
|
|
||||||
- Test Spec Files, each of which is a tree with "leaves" which are:
|
|
||||||
- Tests, each of which is a tree with leaves which are:
|
|
||||||
- Test Cases.
|
|
||||||
|
|
||||||
(In the implementation, this conceptual tree of trees is decomposed into one big tree
|
|
||||||
whose leaves are Test Cases.)
|
|
||||||
|
|
||||||
**Type:** `TestTree`
|
|
||||||
|
|
||||||
## Suite
|
|
||||||
|
|
||||||
A suite of tests.
|
|
||||||
A single suite has a directory structure, and many _test spec files_
|
|
||||||
(`.spec.ts` files containing tests) and _READMEs_.
|
|
||||||
Each member of a suite is identified by its path within the suite.
|
|
||||||
|
|
||||||
**Example:** `src/webgpu/`
|
|
||||||
|
|
||||||
### README
|
|
||||||
|
|
||||||
**Example:** `src/webgpu/README.txt`
|
|
||||||
|
|
||||||
Describes (in prose) the contents of a subdirectory in a suite.
|
|
||||||
|
|
||||||
READMEs are only processed at build time, when generating the _Listing_ for a suite.
|
|
||||||
|
|
||||||
**Type:** `TestSuiteListingEntryReadme`
|
|
||||||
|
|
||||||
## Queries
|
|
||||||
|
|
||||||
A _Query_ is a structured object which specifies a subset of cases in exactly one Suite.
|
|
||||||
A Query can be represented uniquely as a string.
|
|
||||||
Queries are used to:
|
|
||||||
|
|
||||||
- Identify a subtree of a suite (by identifying the root node of that subtree).
|
|
||||||
- Identify individual cases.
|
|
||||||
- Represent the list of tests that a test runner (standalone, wpt, or cmdline) should run.
|
|
||||||
- Identify subtrees which should not be "collapsed" during WPT `cts.https.html` generation,
|
|
||||||
so that that cts.https.html "variants" can have individual test expectations
|
|
||||||
(i.e. marked as "expected to fail", "skip", etc.).
|
|
||||||
|
|
||||||
There are four types of `TestQuery`:
|
|
||||||
|
|
||||||
- `TestQueryMultiFile` represents any subtree of the file hierarchy:
|
|
||||||
- `suite:*`
|
|
||||||
- `suite:path,to,*`
|
|
||||||
- `suite:path,to,file,*`
|
|
||||||
- `TestQueryMultiTest` represents any subtree of the test hierarchy:
|
|
||||||
- `suite:path,to,file:*`
|
|
||||||
- `suite:path,to,file:path,to,*`
|
|
||||||
- `suite:path,to,file:path,to,test,*`
|
|
||||||
- `TestQueryMultiCase` represents any subtree of the case hierarchy:
|
|
||||||
- `suite:path,to,file:path,to,test:*`
|
|
||||||
- `suite:path,to,file:path,to,test:my=0;*`
|
|
||||||
- `suite:path,to,file:path,to,test:my=0;params="here";*`
|
|
||||||
- `TestQuerySingleCase` represents as single case:
|
|
||||||
- `suite:path,to,file:path,to,test:my=0;params="here"`
|
|
||||||
|
|
||||||
Test Queries are a **weakly ordered set**: any query is
|
|
||||||
_Unordered_, _Equal_, _StrictSuperset_, or _StrictSubset_ relative to any other.
|
|
||||||
This property is used to construct the complete tree of test cases.
|
|
||||||
In the examples above, every example query is a StrictSubset of the previous one
|
|
||||||
(note: even `:*` is a subset of `,*`).
|
|
||||||
|
|
||||||
In the WPT and standalone harnesses, the query is stored in the URL, e.g.
|
|
||||||
`index.html?q=q:u,e:r,y:*`.
|
|
||||||
|
|
||||||
Queries are selectively URL-encoded for readability and compatibility with browsers
|
|
||||||
(see `encodeURIComponentSelectively`).
|
|
||||||
|
|
||||||
**Type:** `TestQuery`
|
|
||||||
|
|
||||||
## Listing
|
|
||||||
|
|
||||||
A listing of the **test spec files** in a suite.
|
|
||||||
|
|
||||||
This can be generated only in Node, which has filesystem access (see `src/tools/crawl.ts`).
|
|
||||||
As part of the build step, a _listing file_ is generated (see `src/tools/gen.ts`) so that the
|
|
||||||
Test Spec Files can be discovered by the web runner (since it does not have filesystem access).
|
|
||||||
|
|
||||||
**Type:** `TestSuiteListing`
|
|
||||||
|
|
||||||
### Listing File
|
|
||||||
|
|
||||||
Each Suite has one Listing File (`suite/listing.[tj]s`), containing a list of the files
|
|
||||||
in the suite.
|
|
||||||
|
|
||||||
In `src/suite/listing.ts`, this is computed dynamically.
|
|
||||||
In `out/suite/listing.js`, the listing has been pre-baked (by `tools/gen_listings`).
|
|
||||||
|
|
||||||
**Type:** Once `import`ed, `ListingFile`
|
|
||||||
|
|
||||||
**Example:** `out/webgpu/listing.js`
|
|
||||||
|
|
||||||
## Test Spec File
|
|
||||||
|
|
||||||
A Test Spec File has a `description` and a Test Group (under which tests and cases are defined).
|
|
||||||
|
|
||||||
**Type:** Once `import`ed, `SpecFile`
|
|
||||||
|
|
||||||
**Example:** `src/webgpu/**/*.spec.ts`
|
|
||||||
|
|
||||||
## Test Group
|
|
||||||
|
|
||||||
A subtree of tests. There is one Test Group per Test Spec File.
|
|
||||||
|
|
||||||
The Test Fixture used for tests is defined at TestGroup creation.
|
|
||||||
|
|
||||||
**Type:** `TestGroup`
|
|
||||||
|
|
||||||
## Test
|
|
||||||
|
|
||||||
One test. It has a single _test function_.
|
|
||||||
|
|
||||||
It may represent multiple _test cases_, each of which runs the same Test Function with different
|
|
||||||
Parameters.
|
|
||||||
|
|
||||||
A test is named using `TestGroup.test()`, which returns a `TestBuilder`.
|
|
||||||
`TestBuilder.params()`/`.paramsSimple()`/`.paramsSubcasesOnly()`
|
|
||||||
can optionally be used to parametrically generate instances (cases and subcases) of the test.
|
|
||||||
Finally, `TestBuilder.fn()` provides the Test Function
|
|
||||||
(or, a test can be marked unimplemented with `TestBuilder.unimplemented()`).
|
|
||||||
|
|
||||||
### Test Function
|
|
||||||
|
|
||||||
When a test subcase is run, the Test Function receives an instance of the
|
|
||||||
Test Fixture provided to the Test Group, producing test results.
|
|
||||||
|
|
||||||
**Type:** `TestFn`
|
|
||||||
|
|
||||||
## Test Case / Case
|
|
||||||
|
|
||||||
A single case of a test. It is identified by a `TestCaseID`: a test name, and its parameters.
|
|
||||||
|
|
||||||
Each case appears as an individual item (tree leaf) in `/standalone/`,
|
|
||||||
and as an individual "step" in WPT.
|
|
||||||
|
|
||||||
If `TestBuilder.params()`/`.paramsSimple()`/`.paramsSubcasesOnly()` are not used,
|
|
||||||
there is exactly one case with one subcase, with parameters `{}`.
|
|
||||||
|
|
||||||
**Type:** During test run time, a case is encapsulated as a `RunCase`.
|
|
||||||
|
|
||||||
## Test Subcase / Subcase
|
|
||||||
|
|
||||||
A single "subcase" of a test. It can also be identified by a `TestCaseID`, though
|
|
||||||
not all contexts allow subdividing cases into subcases.
|
|
||||||
|
|
||||||
All of the subcases of a case will run _inside_ the case, essentially as a for-loop wrapping the
|
|
||||||
test function. They do _not_ appear individually in `/standalone/` or WPT.
|
|
||||||
|
|
||||||
If `CaseParamsBuilder.beginSubcases()` is not used, there is exactly one subcase per case.
|
|
||||||
|
|
||||||
## Test Parameters / Params
|
|
||||||
|
|
||||||
Each Test Subcase has a (possibly empty) set of Test Parameters,
|
|
||||||
The parameters are passed to the Test Function `f(t)` via `t.params`.
|
|
||||||
|
|
||||||
A set of Public Parameters identifies a Test Case or Test Subcase within a Test.
|
|
||||||
|
|
||||||
There are also Private Parameters: any parameter name beginning with an underscore (`_`).
|
|
||||||
These parameters are not part of the Test Case identification, but are still passed into
|
|
||||||
the Test Function. They can be used, e.g., to manually specify expected results.
|
|
||||||
|
|
||||||
**Type:** `TestParams`
|
|
||||||
|
|
||||||
## Test Fixture / Fixture
|
|
||||||
|
|
||||||
_Test Fixtures_ provide helpers for tests to use.
|
|
||||||
A new instance of the fixture is created for every run of every test case.
|
|
||||||
|
|
||||||
There is always one fixture class for a whole test group (though this may change).
|
|
||||||
|
|
||||||
The fixture is also how a test gets access to the _case recorder_,
|
|
||||||
which allows it to produce test results.
|
|
||||||
|
|
||||||
They are also how tests produce results: `.skip()`, `.fail()`, etc.
|
|
||||||
|
|
||||||
**Type:** `Fixture`
|
|
||||||
|
|
||||||
### `UnitTest` Fixture
|
|
||||||
|
|
||||||
Provides basic fixture utilities most useful in the `unittests` suite.
|
|
||||||
|
|
||||||
### `GPUTest` Fixture
|
|
||||||
|
|
||||||
Provides utilities useful in WebGPU CTS tests.
|
|
||||||
|
|
||||||
# Test Results
|
|
||||||
|
|
||||||
## Logger
|
|
||||||
|
|
||||||
A logger logs the results of a whole test run.
|
|
||||||
|
|
||||||
It saves an empty `LiveTestSpecResult` into its results map, then creates a
|
|
||||||
_test spec recorder_, which records the results for a group into the `LiveTestSpecResult`.
|
|
||||||
|
|
||||||
**Type:** `Logger`
|
|
||||||
|
|
||||||
### Test Case Recorder
|
|
||||||
|
|
||||||
Refers to a `LiveTestCaseResult` created by the logger.
|
|
||||||
Records the results of running a test case (its pass-status, run time, and logs) into it.
|
|
||||||
|
|
||||||
**Types:** `TestCaseRecorder`, `LiveTestCaseResult`
|
|
||||||
|
|
||||||
#### Test Case Status
|
|
||||||
|
|
||||||
The `status` of a `LiveTestCaseResult` can be one of:
|
|
||||||
|
|
||||||
- `'running'` (only while still running)
|
|
||||||
- `'pass'`
|
|
||||||
- `'skip'`
|
|
||||||
- `'warn'`
|
|
||||||
- `'fail'`
|
|
||||||
|
|
||||||
The "worst" result from running a case is always reported (fail > warn > skip > pass).
|
|
||||||
Note this means a test can still fail if it's "skipped", if it failed before
|
|
||||||
`.skip()` was called.
|
|
||||||
|
|
||||||
**Type:** `Status`
|
|
||||||
|
|
||||||
## Results Format
|
|
||||||
|
|
||||||
The results are returned in JSON format.
|
|
||||||
|
|
||||||
They are designed to be easily merged in JavaScript:
|
|
||||||
the `"results"` can be passed into the constructor of `Map` and merged from there.
|
|
||||||
|
|
||||||
(TODO: Write a merge tool, if needed.)
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
"version": "bf472c5698138cdf801006cd400f587e9b1910a5-dirty",
|
|
||||||
"results": [
|
|
||||||
[
|
|
||||||
"unittests:async_mutex:basic:",
|
|
||||||
{ "status": "pass", "timems": 0.286, "logs": [] }
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"unittests:async_mutex:serial:",
|
|
||||||
{ "status": "pass", "timems": 0.415, "logs": [] }
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
// Typescript configuration for compile sources and
|
|
||||||
// dependent files for usage directly with Node.js. This
|
|
||||||
// is useful for running scripts in tools/ directly with Node
|
|
||||||
// without including extra dependencies.
|
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"incremental": false,
|
|
||||||
"noEmit": false,
|
|
||||||
"declaration": false,
|
|
||||||
},
|
|
||||||
|
|
||||||
"exclude": [
|
|
||||||
"src/common/runtime/wpt.ts",
|
|
||||||
"src/common/runtime/standalone.ts",
|
|
||||||
"src/common/runtime/helper/test_worker.ts",
|
|
||||||
"src/webgpu/web_platform/worker/worker_launcher.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
15798
dom/webgpu/tests/cts/checkout/package-lock.json
generated
15798
dom/webgpu/tests/cts/checkout/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,77 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@webgpu/cts",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "WebGPU Conformance Test Suite",
|
|
||||||
"scripts": {
|
|
||||||
"test": "grunt pre",
|
|
||||||
"check": "grunt check",
|
|
||||||
"standalone": "grunt standalone",
|
|
||||||
"wpt": "grunt wpt",
|
|
||||||
"fix": "grunt fix",
|
|
||||||
"unittest": "grunt unittest",
|
|
||||||
"gen_wpt_cts_html": "node tools/gen_wpt_cts_html",
|
|
||||||
"gen_cache": "node tools/gen_cache",
|
|
||||||
"tsdoc": "grunt run:tsdoc",
|
|
||||||
"start": "node tools/dev_server",
|
|
||||||
"dev": "node tools/dev_server"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/gpuweb/cts.git"
|
|
||||||
},
|
|
||||||
"author": "WebGPU CTS Contributors",
|
|
||||||
"private": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/gpuweb/cts/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/gpuweb/cts#readme",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/cli": "^7.19.3",
|
|
||||||
"@babel/core": "^7.20.5",
|
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
|
||||||
"@types/babel__core": "^7.1.20",
|
|
||||||
"@types/dom-mediacapture-transform": "^0.1.4",
|
|
||||||
"@types/dom-webcodecs": "^0.1.5",
|
|
||||||
"@types/express": "^4.17.14",
|
|
||||||
"@types/jquery": "^3.5.14",
|
|
||||||
"@types/morgan": "^1.9.3",
|
|
||||||
"@types/node": "^14.18.12",
|
|
||||||
"@types/offscreencanvas": "^2019.7.0",
|
|
||||||
"@types/pngjs": "^6.0.1",
|
|
||||||
"@types/serve-index": "^1.9.1",
|
|
||||||
"@typescript-eslint/parser": "^4.33.0",
|
|
||||||
"@webgpu/types": "0.1.25",
|
|
||||||
"ansi-colors": "4.1.1",
|
|
||||||
"babel-plugin-add-header-comment": "^1.0.3",
|
|
||||||
"babel-plugin-const-enum": "^1.2.0",
|
|
||||||
"chokidar": "^3.5.3",
|
|
||||||
"eslint": "^7.11.0",
|
|
||||||
"eslint-plugin-ban": "^1.6.0",
|
|
||||||
"eslint-plugin-deprecation": "^1.3.3",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"grunt": "^1.5.3",
|
|
||||||
"grunt-cli": "^1.4.3",
|
|
||||||
"grunt-contrib-clean": "^2.0.1",
|
|
||||||
"grunt-contrib-copy": "^1.0.0",
|
|
||||||
"grunt-run": "^0.8.1",
|
|
||||||
"grunt-ts": "^6.0.0-beta.22",
|
|
||||||
"gts": "^3.1.1",
|
|
||||||
"http-server": "^14.1.1",
|
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"playwright-core": "^1.29.2",
|
|
||||||
"pngjs": "^6.0.0",
|
|
||||||
"portfinder": "^1.0.32",
|
|
||||||
"prettier": "~2.1.2",
|
|
||||||
"quiet-grunt": "^0.2.3",
|
|
||||||
"screenshot-ftw": "^1.0.5",
|
|
||||||
"serve-index": "^1.9.1",
|
|
||||||
"ts-node": "^9.0.0",
|
|
||||||
"typedoc": "^0.23.21",
|
|
||||||
"typescript": "~4.7.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
printWidth: 100,
|
|
||||||
|
|
||||||
arrowParens: 'avoid',
|
|
||||||
bracketSpacing: true,
|
|
||||||
singleQuote: true,
|
|
||||||
trailingComma: 'es5',
|
|
||||||
};
|
|
||||||
|
|
@ -1,120 +0,0 @@
|
||||||
/**
|
|
||||||
* Utilities to improve the performance of the CTS, by caching data that is
|
|
||||||
* expensive to build using a two-level cache (in-memory, pre-computed file).
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface DataStore {
|
|
||||||
load(path: string): Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Logger is a basic debug logger function */
|
|
||||||
export type Logger = (s: string) => void;
|
|
||||||
|
|
||||||
/** DataCache is an interface to a data store used to hold cached data */
|
|
||||||
export class DataCache {
|
|
||||||
/** setDataStore() sets the backing data store used by the data cache */
|
|
||||||
public setStore(dataStore: DataStore) {
|
|
||||||
this.dataStore = dataStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** setDebugLogger() sets the verbose logger */
|
|
||||||
public setDebugLogger(logger: Logger) {
|
|
||||||
this.debugLogger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* fetch() retrieves cacheable data from the data cache, first checking the
|
|
||||||
* in-memory cache, then the data store (if specified), then resorting to
|
|
||||||
* building the data and storing it in the cache.
|
|
||||||
*/
|
|
||||||
public async fetch<Data>(cacheable: Cacheable<Data>): Promise<Data> {
|
|
||||||
// First check the in-memory cache
|
|
||||||
let data = this.cache.get(cacheable.path);
|
|
||||||
if (data !== undefined) {
|
|
||||||
this.log('in-memory cache hit');
|
|
||||||
return Promise.resolve(data as Data);
|
|
||||||
}
|
|
||||||
this.log('in-memory cache miss');
|
|
||||||
// In in-memory cache miss.
|
|
||||||
// Next, try the data store.
|
|
||||||
if (this.dataStore !== null && !this.unavailableFiles.has(cacheable.path)) {
|
|
||||||
let serialized: string | undefined;
|
|
||||||
try {
|
|
||||||
serialized = await this.dataStore.load(cacheable.path);
|
|
||||||
this.log('loaded serialized');
|
|
||||||
} catch (err) {
|
|
||||||
// not found in data store
|
|
||||||
this.log(`failed to load (${cacheable.path}): ${err}`);
|
|
||||||
this.unavailableFiles.add(cacheable.path);
|
|
||||||
}
|
|
||||||
if (serialized !== undefined) {
|
|
||||||
this.log(`deserializing`);
|
|
||||||
data = cacheable.deserialize(serialized);
|
|
||||||
this.cache.set(cacheable.path, data);
|
|
||||||
return data as Data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Not found anywhere. Build the data, and cache for future lookup.
|
|
||||||
this.log(`cache: building (${cacheable.path})`);
|
|
||||||
data = await cacheable.build();
|
|
||||||
this.cache.set(cacheable.path, data);
|
|
||||||
return data as Data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private log(msg: string) {
|
|
||||||
if (this.debugLogger !== null) {
|
|
||||||
this.debugLogger(`DataCache: ${msg}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private cache = new Map<string, unknown>();
|
|
||||||
private unavailableFiles = new Set<string>();
|
|
||||||
private dataStore: DataStore | null = null;
|
|
||||||
private debugLogger: Logger | null = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The data cache */
|
|
||||||
export const dataCache = new DataCache();
|
|
||||||
|
|
||||||
/** true if the current process is building the cache */
|
|
||||||
let isBuildingDataCache = false;
|
|
||||||
|
|
||||||
/** @returns true if the data cache is currently being built */
|
|
||||||
export function getIsBuildingDataCache() {
|
|
||||||
return isBuildingDataCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets whether the data cache is currently being built */
|
|
||||||
export function setIsBuildingDataCache(value = true) {
|
|
||||||
isBuildingDataCache = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cacheable is the interface to something that can be stored into the
|
|
||||||
* DataCache.
|
|
||||||
* The 'npm run gen_cache' tool will look for module-scope variables of this
|
|
||||||
* interface, with the name `d`.
|
|
||||||
*/
|
|
||||||
export interface Cacheable<Data> {
|
|
||||||
/** the globally unique path for the cacheable data */
|
|
||||||
readonly path: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* build() builds the cacheable data.
|
|
||||||
* This is assumed to be an expensive operation and will only happen if the
|
|
||||||
* cache does not already contain the built data.
|
|
||||||
*/
|
|
||||||
build(): Promise<Data>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* serialize() transforms `data` to a string (usually JSON encoded) so that it
|
|
||||||
* can be stored in a text cache file.
|
|
||||||
*/
|
|
||||||
serialize(data: Data): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* deserialize() is the inverse of serialize(), transforming the string back
|
|
||||||
* to the Data object.
|
|
||||||
*/
|
|
||||||
deserialize(serialized: string): Data;
|
|
||||||
}
|
|
||||||
|
|
@ -1,328 +0,0 @@
|
||||||
import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
|
|
||||||
import { JSONWithUndefined } from '../internal/params_utils.js';
|
|
||||||
import { assert, unreachable } from '../util/util.js';
|
|
||||||
|
|
||||||
export class SkipTestCase extends Error {}
|
|
||||||
export class UnexpectedPassError extends Error {}
|
|
||||||
|
|
||||||
export { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
|
|
||||||
|
|
||||||
/** The fully-general type for params passed to a test function invocation. */
|
|
||||||
export type TestParams = {
|
|
||||||
readonly [k: string]: JSONWithUndefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DestroyableObject =
|
|
||||||
| { destroy(): void }
|
|
||||||
| { close(): void }
|
|
||||||
| { getExtension(extensionName: 'WEBGL_lose_context'): WEBGL_lose_context };
|
|
||||||
|
|
||||||
export class SubcaseBatchState {
|
|
||||||
private _params: TestParams;
|
|
||||||
|
|
||||||
constructor(params: TestParams) {
|
|
||||||
this._params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the case parameters for this test fixture shared state. Subcase params
|
|
||||||
* are not included.
|
|
||||||
*/
|
|
||||||
get params(): TestParams {
|
|
||||||
return this._params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs before the `.before()` function.
|
|
||||||
* @internal MAINTENANCE_TODO: Make this not visible to test code?
|
|
||||||
*/
|
|
||||||
async init() {}
|
|
||||||
/**
|
|
||||||
* Runs between the `.before()` function and the subcases.
|
|
||||||
* @internal MAINTENANCE_TODO: Make this not visible to test code?
|
|
||||||
*/
|
|
||||||
async postInit() {}
|
|
||||||
/**
|
|
||||||
* Runs after all subcases finish.
|
|
||||||
* @internal MAINTENANCE_TODO: Make this not visible to test code?
|
|
||||||
*/
|
|
||||||
async finalize() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Fixture is a class used to instantiate each test sub/case at run time.
|
|
||||||
* A new instance of the Fixture is created for every single test subcase
|
|
||||||
* (i.e. every time the test function is run).
|
|
||||||
*/
|
|
||||||
export class Fixture<S extends SubcaseBatchState = SubcaseBatchState> {
|
|
||||||
private _params: unknown;
|
|
||||||
private _sharedState: S;
|
|
||||||
/**
|
|
||||||
* Interface for recording logs and test status.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
protected rec: TestCaseRecorder;
|
|
||||||
private eventualExpectations: Array<Promise<unknown>> = [];
|
|
||||||
private numOutstandingAsyncExpectations = 0;
|
|
||||||
private objectsToCleanUp: DestroyableObject[] = [];
|
|
||||||
|
|
||||||
public static MakeSharedState(params: TestParams): SubcaseBatchState {
|
|
||||||
return new SubcaseBatchState(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
constructor(sharedState: S, rec: TestCaseRecorder, params: TestParams) {
|
|
||||||
this._sharedState = sharedState;
|
|
||||||
this.rec = rec;
|
|
||||||
this._params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the (case+subcase) parameters for this test function invocation.
|
|
||||||
*/
|
|
||||||
get params(): unknown {
|
|
||||||
return this._params;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the test fixture's shared state. This object is shared between subcases
|
|
||||||
* within the same testcase.
|
|
||||||
*/
|
|
||||||
get sharedState(): S {
|
|
||||||
return this._sharedState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override this to do additional pre-test-function work in a derived fixture.
|
|
||||||
* This has to be a member function instead of an async `createFixture` function, because
|
|
||||||
* we need to be able to ergonomically override it in subclasses.
|
|
||||||
*
|
|
||||||
* @internal MAINTENANCE_TODO: Make this not visible to test code?
|
|
||||||
*/
|
|
||||||
async init(): Promise<void> {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override this to do additional post-test-function work in a derived fixture.
|
|
||||||
*
|
|
||||||
* Called even if init was unsuccessful.
|
|
||||||
*
|
|
||||||
* @internal MAINTENANCE_TODO: Make this not visible to test code?
|
|
||||||
*/
|
|
||||||
async finalize(): Promise<void> {
|
|
||||||
assert(
|
|
||||||
this.numOutstandingAsyncExpectations === 0,
|
|
||||||
'there were outstanding immediateAsyncExpectations (e.g. expectUncapturedError) at the end of the test'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Loop to exhaust the eventualExpectations in case they chain off each other.
|
|
||||||
while (this.eventualExpectations.length) {
|
|
||||||
const p = this.eventualExpectations.shift()!;
|
|
||||||
try {
|
|
||||||
await p;
|
|
||||||
} catch (ex) {
|
|
||||||
this.rec.threw(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And clean up any objects now that they're done being used.
|
|
||||||
for (const o of this.objectsToCleanUp) {
|
|
||||||
if ('getExtension' in o) {
|
|
||||||
const WEBGL_lose_context = o.getExtension('WEBGL_lose_context');
|
|
||||||
if (WEBGL_lose_context) WEBGL_lose_context.loseContext();
|
|
||||||
} else if ('destroy' in o) {
|
|
||||||
o.destroy();
|
|
||||||
} else {
|
|
||||||
o.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks an object to be cleaned up after the test finishes.
|
|
||||||
*
|
|
||||||
* MAINTENANCE_TODO: Use this in more places. (Will be easier once .destroy() is allowed on
|
|
||||||
* invalid objects.)
|
|
||||||
*/
|
|
||||||
trackForCleanup<T extends DestroyableObject>(o: T): T {
|
|
||||||
this.objectsToCleanUp.push(o);
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tracks an object, if it's destroyable, to be cleaned up after the test finishes. */
|
|
||||||
tryTrackForCleanup<T>(o: T): T {
|
|
||||||
if (typeof o === 'object' && o !== null) {
|
|
||||||
if (
|
|
||||||
'destroy' in o ||
|
|
||||||
'close' in o ||
|
|
||||||
o instanceof WebGLRenderingContext ||
|
|
||||||
o instanceof WebGL2RenderingContext
|
|
||||||
) {
|
|
||||||
this.objectsToCleanUp.push((o as unknown) as DestroyableObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Log a debug message. */
|
|
||||||
debug(msg: string): void {
|
|
||||||
this.rec.debug(new Error(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Throws an exception marking the subcase as skipped. */
|
|
||||||
skip(msg: string): never {
|
|
||||||
throw new SkipTestCase(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Log a warning and increase the result status to "Warn". */
|
|
||||||
warn(msg?: string): void {
|
|
||||||
this.rec.warn(new Error(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Log an error and increase the result status to "ExpectFailed". */
|
|
||||||
fail(msg?: string): void {
|
|
||||||
this.rec.expectationFailed(new Error(msg));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps an async function. Tracks its status to fail if the test tries to report a test status
|
|
||||||
* before the async work has finished.
|
|
||||||
*/
|
|
||||||
protected async immediateAsyncExpectation<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
this.numOutstandingAsyncExpectations++;
|
|
||||||
const ret = await fn();
|
|
||||||
this.numOutstandingAsyncExpectations--;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wraps an async function, passing it an `Error` object recording the original stack trace.
|
|
||||||
* The async work will be implicitly waited upon before reporting a test status.
|
|
||||||
*/
|
|
||||||
protected eventualAsyncExpectation<T>(fn: (niceStack: Error) => Promise<T>): void {
|
|
||||||
const promise = fn(new Error());
|
|
||||||
this.eventualExpectations.push(promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
private expectErrorValue(expectedError: string | true, ex: unknown, niceStack: Error): void {
|
|
||||||
if (!(ex instanceof Error)) {
|
|
||||||
niceStack.message = `THREW non-error value, of type ${typeof ex}: ${ex}`;
|
|
||||||
this.rec.expectationFailed(niceStack);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const actualName = ex.name;
|
|
||||||
if (expectedError !== true && actualName !== expectedError) {
|
|
||||||
niceStack.message = `THREW ${actualName}, instead of ${expectedError}: ${ex}`;
|
|
||||||
this.rec.expectationFailed(niceStack);
|
|
||||||
} else {
|
|
||||||
niceStack.message = `OK: threw ${actualName}: ${ex.message}`;
|
|
||||||
this.rec.debug(niceStack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Expect that the provided promise resolves (fulfills). */
|
|
||||||
shouldResolve(p: Promise<unknown>, msg?: string): void {
|
|
||||||
this.eventualAsyncExpectation(async niceStack => {
|
|
||||||
const m = msg ? ': ' + msg : '';
|
|
||||||
try {
|
|
||||||
await p;
|
|
||||||
niceStack.message = 'resolved as expected' + m;
|
|
||||||
} catch (ex) {
|
|
||||||
niceStack.message = `REJECTED${m}`;
|
|
||||||
if (ex instanceof Error) {
|
|
||||||
niceStack.message += '\n' + ex.message;
|
|
||||||
}
|
|
||||||
this.rec.expectationFailed(niceStack);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Expect that the provided promise rejects, with the provided exception name. */
|
|
||||||
shouldReject(expectedName: string, p: Promise<unknown>, msg?: string): void {
|
|
||||||
this.eventualAsyncExpectation(async niceStack => {
|
|
||||||
const m = msg ? ': ' + msg : '';
|
|
||||||
try {
|
|
||||||
await p;
|
|
||||||
niceStack.message = 'DID NOT REJECT' + m;
|
|
||||||
this.rec.expectationFailed(niceStack);
|
|
||||||
} catch (ex) {
|
|
||||||
niceStack.message = 'rejected as expected' + m;
|
|
||||||
this.expectErrorValue(expectedName, ex, niceStack);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expect that the provided function throws.
|
|
||||||
* If an `expectedName` is provided, expect that the throw exception has that name.
|
|
||||||
*/
|
|
||||||
shouldThrow(expectedError: string | boolean, fn: () => void, msg?: string): void {
|
|
||||||
const m = msg ? ': ' + msg : '';
|
|
||||||
try {
|
|
||||||
fn();
|
|
||||||
if (expectedError === false) {
|
|
||||||
this.rec.debug(new Error('did not throw, as expected' + m));
|
|
||||||
} else {
|
|
||||||
this.rec.expectationFailed(new Error('unexpectedly did not throw' + m));
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
if (expectedError === false) {
|
|
||||||
this.rec.expectationFailed(new Error('threw unexpectedly' + m));
|
|
||||||
} else {
|
|
||||||
this.expectErrorValue(expectedError, ex, new Error(m));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Expect that a condition is true. */
|
|
||||||
expect(cond: boolean, msg?: string): boolean {
|
|
||||||
if (cond) {
|
|
||||||
const m = msg ? ': ' + msg : '';
|
|
||||||
this.rec.debug(new Error('expect OK' + m));
|
|
||||||
} else {
|
|
||||||
this.rec.expectationFailed(new Error(msg));
|
|
||||||
}
|
|
||||||
return cond;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the argument is an `Error`, fail (or warn). If it's `undefined`, no-op.
|
|
||||||
* If the argument is an array, apply the above behavior on each of elements.
|
|
||||||
*/
|
|
||||||
expectOK(
|
|
||||||
error: Error | undefined | (Error | undefined)[],
|
|
||||||
{ mode = 'fail', niceStack }: { mode?: 'fail' | 'warn'; niceStack?: Error } = {}
|
|
||||||
): void {
|
|
||||||
const handleError = (error: Error | undefined) => {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (niceStack) {
|
|
||||||
error.stack = niceStack.stack;
|
|
||||||
}
|
|
||||||
if (mode === 'fail') {
|
|
||||||
this.rec.expectationFailed(error);
|
|
||||||
} else if (mode === 'warn') {
|
|
||||||
this.rec.warn(error);
|
|
||||||
} else {
|
|
||||||
unreachable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Array.isArray(error)) {
|
|
||||||
for (const e of error) {
|
|
||||||
handleError(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
eventualExpectOK(
|
|
||||||
error: Promise<Error | undefined | (Error | undefined)[]>,
|
|
||||||
{ mode = 'fail' }: { mode?: 'fail' | 'warn' } = {}
|
|
||||||
) {
|
|
||||||
this.eventualAsyncExpectation(async niceStack => {
|
|
||||||
this.expectOK(await error, { mode, niceStack });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,337 +0,0 @@
|
||||||
import { Merged, mergeParams } from '../internal/params_utils.js';
|
|
||||||
import { stringifyPublicParams } from '../internal/query/stringify_params.js';
|
|
||||||
import { assert, mapLazy } from '../util/util.js';
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// "Public" ParamsBuilder API / Documentation
|
|
||||||
// ================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides doc comments for the methods of CaseParamsBuilder and SubcaseParamsBuilder.
|
|
||||||
* (Also enforces rough interface match between them.)
|
|
||||||
*/
|
|
||||||
export interface ParamsBuilder {
|
|
||||||
/**
|
|
||||||
* Expands each item in `this` into zero or more items.
|
|
||||||
* Each item has its parameters expanded with those returned by the `expander`.
|
|
||||||
*
|
|
||||||
* **Note:** When only a single key is being added, use the simpler `expand` for readability.
|
|
||||||
*
|
|
||||||
* ```text
|
|
||||||
* this = [ a , b , c ]
|
|
||||||
* this.map(expander) = [ f(a) f(b) f(c) ]
|
|
||||||
* = [[a1, a2, a3] , [ b1 ] , [] ]
|
|
||||||
* merge and flatten = [ merge(a, a1), merge(a, a2), merge(a, a3), merge(b, b1) ]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
expandWithParams(expander: (_: any) => any): any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expands each item in `this` into zero or more items. Each item has its parameters expanded
|
|
||||||
* with one new key, `key`, and the values returned by `expander`.
|
|
||||||
*/
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
expand(key: string, expander: (_: any) => any): any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expands each item in `this` to multiple items, one for each item in `newParams`.
|
|
||||||
*
|
|
||||||
* In other words, takes the cartesian product of [ the items in `this` ] and `newParams`.
|
|
||||||
*
|
|
||||||
* **Note:** When only a single key is being added, use the simpler `combine` for readability.
|
|
||||||
*
|
|
||||||
* ```text
|
|
||||||
* this = [ {a:1}, {b:2} ]
|
|
||||||
* newParams = [ {x:1}, {y:2} ]
|
|
||||||
* this.combineP(newParams) = [ {a:1,x:1}, {a:1,y:2}, {b:2,x:1}, {b:2,y:2} ]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
combineWithParams(newParams: Iterable<any>): any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expands each item in `this` to multiple items with `{ [name]: value }` for each value.
|
|
||||||
*
|
|
||||||
* In other words, takes the cartesian product of [ the items in `this` ]
|
|
||||||
* and `[ {[name]: value} for each value in values ]`
|
|
||||||
*/
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
combine(key: string, newParams: Iterable<any>): any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters `this` to only items for which `pred` returns true.
|
|
||||||
*/
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
filter(pred: (_: any) => boolean): any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters `this` to only items for which `pred` returns false.
|
|
||||||
*/
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
unless(pred: (_: any) => boolean): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the resulting parameter object type which would be generated by an object of
|
|
||||||
* the given ParamsBuilder type.
|
|
||||||
*/
|
|
||||||
export type ParamTypeOf<
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
T extends ParamsBuilder
|
|
||||||
> = T extends SubcaseParamsBuilder<infer CaseP, infer SubcaseP>
|
|
||||||
? Merged<CaseP, SubcaseP>
|
|
||||||
: T extends CaseParamsBuilder<infer CaseP>
|
|
||||||
? CaseP
|
|
||||||
: never;
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Implementation
|
|
||||||
// ================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterable over pairs of either:
|
|
||||||
* - `[case params, Iterable<subcase params>]` if there are subcases.
|
|
||||||
* - `[case params, undefined]` if not.
|
|
||||||
*/
|
|
||||||
export type CaseSubcaseIterable<CaseP, SubcaseP> = Iterable<
|
|
||||||
readonly [CaseP, Iterable<SubcaseP> | undefined]
|
|
||||||
>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for `CaseParamsBuilder` and `SubcaseParamsBuilder`.
|
|
||||||
*/
|
|
||||||
export abstract class ParamsBuilderBase<CaseP extends {}, SubcaseP extends {}> {
|
|
||||||
protected readonly cases: () => Generator<CaseP>;
|
|
||||||
|
|
||||||
constructor(cases: () => Generator<CaseP>) {
|
|
||||||
this.cases = cases;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hidden from test files. Use `builderIterateCasesWithSubcases` to access this.
|
|
||||||
*/
|
|
||||||
protected abstract iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the (normally hidden) `iterateCasesWithSubcases()` method.
|
|
||||||
*/
|
|
||||||
export function builderIterateCasesWithSubcases(builder: ParamsBuilderBase<{}, {}>) {
|
|
||||||
interface IterableParamsBuilder {
|
|
||||||
iterateCasesWithSubcases(): CaseSubcaseIterable<{}, {}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((builder as unknown) as IterableParamsBuilder).iterateCasesWithSubcases();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builder for combinatorial test **case** parameters.
|
|
||||||
*
|
|
||||||
* CaseParamsBuilder is immutable. Each method call returns a new, immutable object,
|
|
||||||
* modifying the list of cases according to the method called.
|
|
||||||
*
|
|
||||||
* This means, for example, that the `unit` passed into `TestBuilder.params()` can be reused.
|
|
||||||
*/
|
|
||||||
export class CaseParamsBuilder<CaseP extends {}>
|
|
||||||
extends ParamsBuilderBase<CaseP, {}>
|
|
||||||
implements Iterable<CaseP>, ParamsBuilder {
|
|
||||||
*iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, {}> {
|
|
||||||
for (const a of this.cases()) {
|
|
||||||
yield [a, undefined];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.iterator](): Iterator<CaseP> {
|
|
||||||
return this.cases();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
expandWithParams<NewP extends {}>(
|
|
||||||
expander: (_: Merged<{}, CaseP>) => Iterable<NewP>
|
|
||||||
): CaseParamsBuilder<Merged<CaseP, NewP>> {
|
|
||||||
const newGenerator = expanderGenerator(this.cases, expander);
|
|
||||||
return new CaseParamsBuilder(() => newGenerator({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
expand<NewPKey extends string, NewPValue>(
|
|
||||||
key: NewPKey,
|
|
||||||
expander: (_: Merged<{}, CaseP>) => Iterable<NewPValue>
|
|
||||||
): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
|
|
||||||
return this.expandWithParams(function* (p) {
|
|
||||||
for (const value of expander(p)) {
|
|
||||||
yield { [key]: value } as { readonly [name in NewPKey]: NewPValue };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
combineWithParams<NewP extends {}>(
|
|
||||||
newParams: Iterable<NewP>
|
|
||||||
): CaseParamsBuilder<Merged<CaseP, NewP>> {
|
|
||||||
assertNotGenerator(newParams);
|
|
||||||
const seenValues = new Set<string>();
|
|
||||||
for (const params of newParams) {
|
|
||||||
const paramsStr = stringifyPublicParams(params);
|
|
||||||
assert(!seenValues.has(paramsStr), `Duplicate entry in combine[WithParams]: ${paramsStr}`);
|
|
||||||
seenValues.add(paramsStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.expandWithParams(() => newParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
combine<NewPKey extends string, NewPValue>(
|
|
||||||
key: NewPKey,
|
|
||||||
values: Iterable<NewPValue>
|
|
||||||
): CaseParamsBuilder<Merged<CaseP, { [name in NewPKey]: NewPValue }>> {
|
|
||||||
assertNotGenerator(values);
|
|
||||||
const mapped = mapLazy(values, v => ({ [key]: v } as { [name in NewPKey]: NewPValue }));
|
|
||||||
return this.combineWithParams(mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
filter(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
|
|
||||||
const newGenerator = filterGenerator(this.cases, pred);
|
|
||||||
return new CaseParamsBuilder(() => newGenerator({}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
unless(pred: (_: Merged<{}, CaseP>) => boolean): CaseParamsBuilder<CaseP> {
|
|
||||||
return this.filter(x => !pred(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Finalize" the list of cases and begin defining subcases.
|
|
||||||
* Returns a new SubcaseParamsBuilder. Methods called on SubcaseParamsBuilder
|
|
||||||
* generate new subcases instead of new cases.
|
|
||||||
*/
|
|
||||||
beginSubcases(): SubcaseParamsBuilder<CaseP, {}> {
|
|
||||||
return new SubcaseParamsBuilder(
|
|
||||||
() => this.cases(),
|
|
||||||
function* () {
|
|
||||||
yield {};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unit CaseParamsBuilder, representing a single case with no params: `[ {} ]`.
|
|
||||||
*
|
|
||||||
* `punit` is passed to every `.params()`/`.paramsSubcasesOnly()` call, so `kUnitCaseParamsBuilder`
|
|
||||||
* is only explicitly needed if constructing a ParamsBuilder outside of a test builder.
|
|
||||||
*/
|
|
||||||
export const kUnitCaseParamsBuilder = new CaseParamsBuilder(function* () {
|
|
||||||
yield {};
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builder for combinatorial test _subcase_ parameters.
|
|
||||||
*
|
|
||||||
* SubcaseParamsBuilder is immutable. Each method call returns a new, immutable object,
|
|
||||||
* modifying the list of subcases according to the method called.
|
|
||||||
*/
|
|
||||||
export class SubcaseParamsBuilder<CaseP extends {}, SubcaseP extends {}>
|
|
||||||
extends ParamsBuilderBase<CaseP, SubcaseP>
|
|
||||||
implements ParamsBuilder {
|
|
||||||
protected readonly subcases: (_: CaseP) => Generator<SubcaseP>;
|
|
||||||
|
|
||||||
constructor(cases: () => Generator<CaseP>, generator: (_: CaseP) => Generator<SubcaseP>) {
|
|
||||||
super(cases);
|
|
||||||
this.subcases = generator;
|
|
||||||
}
|
|
||||||
|
|
||||||
*iterateCasesWithSubcases(): CaseSubcaseIterable<CaseP, SubcaseP> {
|
|
||||||
for (const caseP of this.cases()) {
|
|
||||||
const subcases = Array.from(this.subcases(caseP));
|
|
||||||
if (subcases.length) {
|
|
||||||
yield [caseP, subcases];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
expandWithParams<NewP extends {}>(
|
|
||||||
expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewP>
|
|
||||||
): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
|
|
||||||
return new SubcaseParamsBuilder(this.cases, expanderGenerator(this.subcases, expander));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
expand<NewPKey extends string, NewPValue>(
|
|
||||||
key: NewPKey,
|
|
||||||
expander: (_: Merged<CaseP, SubcaseP>) => Iterable<NewPValue>
|
|
||||||
): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
|
|
||||||
return this.expandWithParams(function* (p) {
|
|
||||||
for (const value of expander(p)) {
|
|
||||||
// TypeScript doesn't know here that NewPKey is always a single literal string type.
|
|
||||||
yield { [key]: value } as { [name in NewPKey]: NewPValue };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
combineWithParams<NewP extends {}>(
|
|
||||||
newParams: Iterable<NewP>
|
|
||||||
): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, NewP>> {
|
|
||||||
assertNotGenerator(newParams);
|
|
||||||
return this.expandWithParams(() => newParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
combine<NewPKey extends string, NewPValue>(
|
|
||||||
key: NewPKey,
|
|
||||||
values: Iterable<NewPValue>
|
|
||||||
): SubcaseParamsBuilder<CaseP, Merged<SubcaseP, { [name in NewPKey]: NewPValue }>> {
|
|
||||||
assertNotGenerator(values);
|
|
||||||
return this.expand(key, () => values);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
filter(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
|
|
||||||
return new SubcaseParamsBuilder(this.cases, filterGenerator(this.subcases, pred));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritDoc */
|
|
||||||
unless(pred: (_: Merged<CaseP, SubcaseP>) => boolean): SubcaseParamsBuilder<CaseP, SubcaseP> {
|
|
||||||
return this.filter(x => !pred(x));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function expanderGenerator<Base, A, B>(
|
|
||||||
baseGenerator: (_: Base) => Generator<A>,
|
|
||||||
expander: (_: Merged<Base, A>) => Iterable<B>
|
|
||||||
): (_: Base) => Generator<Merged<A, B>> {
|
|
||||||
return function* (base: Base) {
|
|
||||||
for (const a of baseGenerator(base)) {
|
|
||||||
for (const b of expander(mergeParams(base, a))) {
|
|
||||||
yield mergeParams(a, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterGenerator<Base, A>(
|
|
||||||
baseGenerator: (_: Base) => Generator<A>,
|
|
||||||
pred: (_: Merged<Base, A>) => boolean
|
|
||||||
): (_: Base) => Generator<A> {
|
|
||||||
return function* (base: Base) {
|
|
||||||
for (const a of baseGenerator(base)) {
|
|
||||||
if (pred(mergeParams(base, a))) {
|
|
||||||
yield a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Assert an object is not a Generator (a thing returned from a generator function). */
|
|
||||||
function assertNotGenerator(x: object) {
|
|
||||||
if ('constructor' in x) {
|
|
||||||
assert(
|
|
||||||
x.constructor !== (function* () {})().constructor,
|
|
||||||
'Argument must not be a generator, as generators are not reusable'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
/**
|
|
||||||
* Base path for resources. The default value is correct for non-worker WPT, but standalone and
|
|
||||||
* workers must access resources using a different base path, so this is overridden in
|
|
||||||
* `test_worker-worker.ts` and `standalone.ts`.
|
|
||||||
*/
|
|
||||||
let baseResourcePath = './resources';
|
|
||||||
let crossOriginHost = '';
|
|
||||||
|
|
||||||
function getAbsoluteBaseResourcePath(path: string) {
|
|
||||||
// Path is already an absolute one.
|
|
||||||
if (path[0] === '/') {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path is relative
|
|
||||||
const relparts = window.location.pathname.split('/');
|
|
||||||
relparts.pop();
|
|
||||||
const pathparts = path.split('/');
|
|
||||||
|
|
||||||
let i;
|
|
||||||
for (i = 0; i < pathparts.length; ++i) {
|
|
||||||
switch (pathparts[i]) {
|
|
||||||
case '':
|
|
||||||
break;
|
|
||||||
case '.':
|
|
||||||
break;
|
|
||||||
case '..':
|
|
||||||
relparts.pop();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
relparts.push(pathparts[i]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return relparts.join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
function runningOnLocalHost(): boolean {
|
|
||||||
const hostname = window.location.hostname;
|
|
||||||
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a path to a resource in the `resources` directory relative to the current execution context
|
|
||||||
* (html file or worker .js file), for `fetch()`, `<img>`, `<video>`, etc but from cross origin host.
|
|
||||||
* Provide onlineUrl if the case running online.
|
|
||||||
* @internal MAINTENANCE_TODO: Cases may run in the LAN environment (not localhost but no internet
|
|
||||||
* access). We temporarily use `crossOriginHost` to configure the cross origin host name in that situation.
|
|
||||||
* But opening to auto-detect mechanism or other solutions.
|
|
||||||
*/
|
|
||||||
export function getCrossOriginResourcePath(pathRelativeToResourcesDir: string, onlineUrl = '') {
|
|
||||||
// A cross origin host has been configured. Use this to load resource.
|
|
||||||
if (crossOriginHost !== '') {
|
|
||||||
return (
|
|
||||||
crossOriginHost +
|
|
||||||
getAbsoluteBaseResourcePath(baseResourcePath) +
|
|
||||||
'/' +
|
|
||||||
pathRelativeToResourcesDir
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using 'localhost' and '127.0.0.1' trick to load cross origin resource. Set cross origin host name
|
|
||||||
// to 'localhost' if case is not running in 'localhost' domain. Otherwise, use '127.0.0.1'.
|
|
||||||
// host name to locahost unless the server running in
|
|
||||||
if (runningOnLocalHost()) {
|
|
||||||
let crossOriginHostName = '';
|
|
||||||
if (location.hostname === 'localhost') {
|
|
||||||
crossOriginHostName = 'http://127.0.0.1';
|
|
||||||
} else {
|
|
||||||
crossOriginHostName = 'http://localhost';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
crossOriginHostName +
|
|
||||||
':' +
|
|
||||||
location.port +
|
|
||||||
getAbsoluteBaseResourcePath(baseResourcePath) +
|
|
||||||
'/' +
|
|
||||||
pathRelativeToResourcesDir
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return onlineUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a path to a resource in the `resources` directory, relative to the current execution context
|
|
||||||
* (html file or worker .js file), for `fetch()`, `<img>`, `<video>`, etc. Pass the cross origin host
|
|
||||||
* name if wants to load resoruce from cross origin host.
|
|
||||||
*/
|
|
||||||
export function getResourcePath(pathRelativeToResourcesDir: string) {
|
|
||||||
return baseResourcePath + '/' + pathRelativeToResourcesDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the base resource path (path to the `resources` directory relative to the current
|
|
||||||
* execution context).
|
|
||||||
*/
|
|
||||||
export function setBaseResourcePath(path: string) {
|
|
||||||
baseResourcePath = path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the cross origin host and cases related to cross origin
|
|
||||||
* will load resource from the given host.
|
|
||||||
*/
|
|
||||||
export function setCrossOriginHost(host: string) {
|
|
||||||
crossOriginHost = host;
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
export type TestConfig = {
|
|
||||||
maxSubcasesInFlight: number;
|
|
||||||
testHeartbeatCallback: () => void;
|
|
||||||
noRaceWithRejectOnTimeout: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls the emission of loops in constant-evaluation shaders under
|
|
||||||
* 'webgpu:shader,execution,expression,*'
|
|
||||||
* FXC is extremely slow to compile shaders with loops unrolled, where as the
|
|
||||||
* MSL compiler is extremely slow to compile with loops rolled.
|
|
||||||
*/
|
|
||||||
unrollConstEvalLoops: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const globalTestConfig: TestConfig = {
|
|
||||||
maxSubcasesInFlight: 500,
|
|
||||||
testHeartbeatCallback: () => {},
|
|
||||||
noRaceWithRejectOnTimeout: false,
|
|
||||||
unrollConstEvalLoops: false,
|
|
||||||
};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export { makeTestGroup } from '../internal/test_group.js';
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
import { IterableTestGroup } from '../internal/test_group.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
import { parseQuery } from './query/parseQuery.js';
|
|
||||||
import { TestQuery } from './query/query.js';
|
|
||||||
import { TestSuiteListing } from './test_suite_listing.js';
|
|
||||||
import { loadTreeForQuery, TestTree, TestTreeLeaf } from './tree.js';
|
|
||||||
|
|
||||||
// A listing file, e.g. either of:
|
|
||||||
// - `src/webgpu/listing.ts` (which is dynamically computed, has a Promise<TestSuiteListing>)
|
|
||||||
// - `out/webgpu/listing.js` (which is pre-baked, has a TestSuiteListing)
|
|
||||||
interface ListingFile {
|
|
||||||
listing: Promise<TestSuiteListing> | TestSuiteListing;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A .spec.ts file, as imported.
|
|
||||||
export interface SpecFile {
|
|
||||||
readonly description: string;
|
|
||||||
readonly g: IterableTestGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ImportInfo {
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestFileLoaderEventMap {
|
|
||||||
import: MessageEvent<ImportInfo>;
|
|
||||||
finish: MessageEvent<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestFileLoader extends EventTarget {
|
|
||||||
addEventListener<K extends keyof TestFileLoaderEventMap>(
|
|
||||||
type: K,
|
|
||||||
listener: (this: TestFileLoader, ev: TestFileLoaderEventMap[K]) => void,
|
|
||||||
options?: boolean | AddEventListenerOptions
|
|
||||||
): void;
|
|
||||||
addEventListener(
|
|
||||||
type: string,
|
|
||||||
listener: EventListenerOrEventListenerObject,
|
|
||||||
options?: boolean | AddEventListenerOptions
|
|
||||||
): void;
|
|
||||||
removeEventListener<K extends keyof TestFileLoaderEventMap>(
|
|
||||||
type: K,
|
|
||||||
listener: (this: TestFileLoader, ev: TestFileLoaderEventMap[K]) => void,
|
|
||||||
options?: boolean | EventListenerOptions
|
|
||||||
): void;
|
|
||||||
removeEventListener(
|
|
||||||
type: string,
|
|
||||||
listener: EventListenerOrEventListenerObject,
|
|
||||||
options?: boolean | EventListenerOptions
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base class for DefaultTestFileLoader and FakeTestFileLoader.
|
|
||||||
export abstract class TestFileLoader extends EventTarget {
|
|
||||||
abstract listing(suite: string): Promise<TestSuiteListing>;
|
|
||||||
protected abstract import(path: string): Promise<SpecFile>;
|
|
||||||
|
|
||||||
importSpecFile(suite: string, path: string[]): Promise<SpecFile> {
|
|
||||||
const url = `${suite}/${path.join('/')}.spec.js`;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new MessageEvent<ImportInfo>('import', { data: { url } })
|
|
||||||
);
|
|
||||||
return this.import(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadTree(query: TestQuery, subqueriesToExpand: string[] = []): Promise<TestTree> {
|
|
||||||
const tree = await loadTreeForQuery(
|
|
||||||
this,
|
|
||||||
query,
|
|
||||||
subqueriesToExpand.map(s => {
|
|
||||||
const q = parseQuery(s);
|
|
||||||
assert(q.level >= 2, () => `subqueriesToExpand entries should not be multi-file:\n ${q}`);
|
|
||||||
return q;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.dispatchEvent(new MessageEvent<void>('finish'));
|
|
||||||
return tree;
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadCases(query: TestQuery): Promise<IterableIterator<TestTreeLeaf>> {
|
|
||||||
const tree = await this.loadTree(query);
|
|
||||||
return tree.iterateLeaves();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DefaultTestFileLoader extends TestFileLoader {
|
|
||||||
async listing(suite: string): Promise<TestSuiteListing> {
|
|
||||||
return ((await import(`../../${suite}/listing.js`)) as ListingFile).listing;
|
|
||||||
}
|
|
||||||
|
|
||||||
import(path: string): Promise<SpecFile> {
|
|
||||||
return import(`../../${path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { ErrorWithExtra } from '../../util/util.js';
|
|
||||||
import { extractImportantStackTrace } from '../stack.js';
|
|
||||||
|
|
||||||
export class LogMessageWithStack extends Error {
|
|
||||||
readonly extra: unknown;
|
|
||||||
|
|
||||||
private stackHiddenMessage: string | undefined = undefined;
|
|
||||||
|
|
||||||
constructor(name: string, ex: Error | ErrorWithExtra) {
|
|
||||||
super(ex.message);
|
|
||||||
|
|
||||||
this.name = name;
|
|
||||||
this.stack = ex.stack;
|
|
||||||
if ('extra' in ex) {
|
|
||||||
this.extra = ex.extra;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Set a flag so the stack is not printed in toJSON(). */
|
|
||||||
setStackHidden(stackHiddenMessage: string) {
|
|
||||||
this.stackHiddenMessage ??= stackHiddenMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): string {
|
|
||||||
let m = this.name;
|
|
||||||
if (this.message) m += ': ' + this.message;
|
|
||||||
if (this.stack) {
|
|
||||||
if (this.stackHiddenMessage === undefined) {
|
|
||||||
m += '\n' + extractImportantStackTrace(this);
|
|
||||||
} else if (this.stackHiddenMessage) {
|
|
||||||
m += `\n at (elided: ${this.stackHiddenMessage})`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string, nicely indented, for debug logs.
|
|
||||||
* This is used in the cmdline and wpt runtimes. In WPT, it shows up in the `*-actual.txt` file.
|
|
||||||
*/
|
|
||||||
export function prettyPrintLog(log: LogMessageWithStack): string {
|
|
||||||
return ' - ' + log.toJSON().replace(/\n/g, '\n ');
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { version } from '../version.js';
|
|
||||||
|
|
||||||
import { LiveTestCaseResult } from './result.js';
|
|
||||||
import { TestCaseRecorder } from './test_case_recorder.js';
|
|
||||||
|
|
||||||
export type LogResults = Map<string, LiveTestCaseResult>;
|
|
||||||
|
|
||||||
export class Logger {
|
|
||||||
static globalDebugMode: boolean = false;
|
|
||||||
|
|
||||||
readonly overriddenDebugMode: boolean | undefined;
|
|
||||||
readonly results: LogResults = new Map();
|
|
||||||
|
|
||||||
constructor({ overrideDebugMode }: { overrideDebugMode?: boolean } = {}) {
|
|
||||||
this.overriddenDebugMode = overrideDebugMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
record(name: string): [TestCaseRecorder, LiveTestCaseResult] {
|
|
||||||
const result: LiveTestCaseResult = { status: 'running', timems: -1 };
|
|
||||||
this.results.set(name, result);
|
|
||||||
return [
|
|
||||||
new TestCaseRecorder(result, this.overriddenDebugMode ?? Logger.globalDebugMode),
|
|
||||||
result,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
asJSON(space?: number): string {
|
|
||||||
return JSON.stringify({ version, results: Array.from(this.results) }, undefined, space);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { LogMessageWithStack } from './log_message.js';
|
|
||||||
|
|
||||||
// MAINTENANCE_TODO: Add warn expectations
|
|
||||||
export type Expectation = 'pass' | 'skip' | 'fail';
|
|
||||||
|
|
||||||
export type Status = 'running' | 'warn' | Expectation;
|
|
||||||
|
|
||||||
export interface TestCaseResult {
|
|
||||||
status: Status;
|
|
||||||
timems: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LiveTestCaseResult extends TestCaseResult {
|
|
||||||
logs?: LogMessageWithStack[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TransferredTestCaseResult extends TestCaseResult {
|
|
||||||
// When transferred from a worker, a LogMessageWithStack turns into a generic Error
|
|
||||||
// (its prototype gets lost and replaced with Error).
|
|
||||||
logs?: Error[];
|
|
||||||
}
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
import { SkipTestCase, UnexpectedPassError } from '../../framework/fixture.js';
|
|
||||||
import { globalTestConfig } from '../../framework/test_config.js';
|
|
||||||
import { now, assert } from '../../util/util.js';
|
|
||||||
|
|
||||||
import { LogMessageWithStack } from './log_message.js';
|
|
||||||
import { Expectation, LiveTestCaseResult } from './result.js';
|
|
||||||
|
|
||||||
enum LogSeverity {
|
|
||||||
Pass = 0,
|
|
||||||
Skip = 1,
|
|
||||||
Warn = 2,
|
|
||||||
ExpectFailed = 3,
|
|
||||||
ValidationFailed = 4,
|
|
||||||
ThrewException = 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
const kMaxLogStacks = 2;
|
|
||||||
const kMinSeverityForStack = LogSeverity.Warn;
|
|
||||||
|
|
||||||
/** Holds onto a LiveTestCaseResult owned by the Logger, and writes the results into it. */
|
|
||||||
export class TestCaseRecorder {
|
|
||||||
private result: LiveTestCaseResult;
|
|
||||||
private inSubCase: boolean = false;
|
|
||||||
private subCaseStatus = LogSeverity.Pass;
|
|
||||||
private finalCaseStatus = LogSeverity.Pass;
|
|
||||||
private hideStacksBelowSeverity = kMinSeverityForStack;
|
|
||||||
private startTime = -1;
|
|
||||||
private logs: LogMessageWithStack[] = [];
|
|
||||||
private logLinesAtCurrentSeverity = 0;
|
|
||||||
private debugging = false;
|
|
||||||
/** Used to dedup log messages which have identical stacks. */
|
|
||||||
private messagesForPreviouslySeenStacks = new Map<string, LogMessageWithStack>();
|
|
||||||
|
|
||||||
constructor(result: LiveTestCaseResult, debugging: boolean) {
|
|
||||||
this.result = result;
|
|
||||||
this.debugging = debugging;
|
|
||||||
}
|
|
||||||
|
|
||||||
start(): void {
|
|
||||||
assert(this.startTime < 0, 'TestCaseRecorder cannot be reused');
|
|
||||||
this.startTime = now();
|
|
||||||
}
|
|
||||||
|
|
||||||
finish(): void {
|
|
||||||
assert(this.startTime >= 0, 'finish() before start()');
|
|
||||||
|
|
||||||
const timeMilliseconds = now() - this.startTime;
|
|
||||||
// Round to next microsecond to avoid storing useless .xxxx00000000000002 in results.
|
|
||||||
this.result.timems = Math.ceil(timeMilliseconds * 1000) / 1000;
|
|
||||||
|
|
||||||
// Convert numeric enum back to string (but expose 'exception' as 'fail')
|
|
||||||
this.result.status =
|
|
||||||
this.finalCaseStatus === LogSeverity.Pass
|
|
||||||
? 'pass'
|
|
||||||
: this.finalCaseStatus === LogSeverity.Skip
|
|
||||||
? 'skip'
|
|
||||||
: this.finalCaseStatus === LogSeverity.Warn
|
|
||||||
? 'warn'
|
|
||||||
: 'fail'; // Everything else is an error
|
|
||||||
|
|
||||||
this.result.logs = this.logs;
|
|
||||||
}
|
|
||||||
|
|
||||||
beginSubCase() {
|
|
||||||
this.subCaseStatus = LogSeverity.Pass;
|
|
||||||
this.inSubCase = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
endSubCase(expectedStatus: Expectation) {
|
|
||||||
try {
|
|
||||||
if (expectedStatus === 'fail') {
|
|
||||||
if (this.subCaseStatus <= LogSeverity.Warn) {
|
|
||||||
throw new UnexpectedPassError();
|
|
||||||
} else {
|
|
||||||
this.subCaseStatus = LogSeverity.Pass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.inSubCase = false;
|
|
||||||
if (this.subCaseStatus > this.finalCaseStatus) {
|
|
||||||
this.finalCaseStatus = this.subCaseStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
injectResult(injectedResult: LiveTestCaseResult): void {
|
|
||||||
Object.assign(this.result, injectedResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(ex: Error): void {
|
|
||||||
if (!this.debugging) return;
|
|
||||||
this.logImpl(LogSeverity.Pass, 'DEBUG', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
info(ex: Error): void {
|
|
||||||
this.logImpl(LogSeverity.Pass, 'INFO', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
skipped(ex: SkipTestCase): void {
|
|
||||||
this.logImpl(LogSeverity.Skip, 'SKIP', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
warn(ex: Error): void {
|
|
||||||
this.logImpl(LogSeverity.Warn, 'WARN', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
expectationFailed(ex: Error): void {
|
|
||||||
this.logImpl(LogSeverity.ExpectFailed, 'EXPECTATION FAILED', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
validationFailed(ex: Error): void {
|
|
||||||
this.logImpl(LogSeverity.ValidationFailed, 'VALIDATION FAILED', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
threw(ex: unknown): void {
|
|
||||||
if (ex instanceof SkipTestCase) {
|
|
||||||
this.skipped(ex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.logImpl(LogSeverity.ThrewException, 'EXCEPTION', ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private logImpl(level: LogSeverity, name: string, baseException: unknown): void {
|
|
||||||
assert(baseException instanceof Error, 'test threw a non-Error object');
|
|
||||||
globalTestConfig.testHeartbeatCallback();
|
|
||||||
const logMessage = new LogMessageWithStack(name, baseException);
|
|
||||||
|
|
||||||
// Final case status should be the "worst" of all log entries.
|
|
||||||
if (this.inSubCase) {
|
|
||||||
if (level > this.subCaseStatus) this.subCaseStatus = level;
|
|
||||||
} else {
|
|
||||||
if (level > this.finalCaseStatus) this.finalCaseStatus = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setFirstLineOnly for all logs except `kMaxLogStacks` stacks at the highest severity
|
|
||||||
if (level > this.hideStacksBelowSeverity) {
|
|
||||||
this.logLinesAtCurrentSeverity = 0;
|
|
||||||
this.hideStacksBelowSeverity = level;
|
|
||||||
|
|
||||||
// Go back and setFirstLineOnly for everything of a lower log level
|
|
||||||
for (const log of this.logs) {
|
|
||||||
log.setStackHidden('below max severity');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (level === this.hideStacksBelowSeverity) {
|
|
||||||
this.logLinesAtCurrentSeverity++;
|
|
||||||
} else if (level < kMinSeverityForStack) {
|
|
||||||
logMessage.setStackHidden('');
|
|
||||||
} else if (level < this.hideStacksBelowSeverity) {
|
|
||||||
logMessage.setStackHidden('below max severity');
|
|
||||||
}
|
|
||||||
if (this.logLinesAtCurrentSeverity > kMaxLogStacks) {
|
|
||||||
logMessage.setStackHidden(`only ${kMaxLogStacks} shown`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logs.push(logMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
import { TestParams } from '../framework/fixture.js';
|
|
||||||
import { ResolveType, UnionToIntersection } from '../util/types.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
import { comparePublicParamsPaths, Ordering } from './query/compare.js';
|
|
||||||
import { kWildcard, kParamSeparator, kParamKVSeparator } from './query/separators.js';
|
|
||||||
|
|
||||||
export type JSONWithUndefined =
|
|
||||||
| undefined
|
|
||||||
| null
|
|
||||||
| number
|
|
||||||
| string
|
|
||||||
| boolean
|
|
||||||
| readonly JSONWithUndefined[]
|
|
||||||
// Ideally this would recurse into JSONWithUndefined, but it breaks code.
|
|
||||||
| { readonly [k: string]: unknown };
|
|
||||||
export interface TestParamsRW {
|
|
||||||
[k: string]: JSONWithUndefined;
|
|
||||||
}
|
|
||||||
export type TestParamsIterable = Iterable<TestParams>;
|
|
||||||
|
|
||||||
export function paramKeyIsPublic(key: string): boolean {
|
|
||||||
return !key.startsWith('_');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function extractPublicParams(params: TestParams): TestParams {
|
|
||||||
const publicParams: TestParamsRW = {};
|
|
||||||
for (const k of Object.keys(params)) {
|
|
||||||
if (paramKeyIsPublic(k)) {
|
|
||||||
publicParams[k] = params[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return publicParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const badParamValueChars = new RegExp(
|
|
||||||
'[' + kParamKVSeparator + kParamSeparator + kWildcard + ']'
|
|
||||||
);
|
|
||||||
|
|
||||||
export function publicParamsEquals(x: TestParams, y: TestParams): boolean {
|
|
||||||
return comparePublicParamsPaths(x, y) === Ordering.Equal;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KeyOfNeverable<T> = T extends never ? never : keyof T;
|
|
||||||
export type AllKeysFromUnion<T> = keyof T | KeyOfNeverable<UnionToIntersection<T>>;
|
|
||||||
export type KeyOfOr<T, K, Default> = K extends keyof T ? T[K] : Default;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flatten a union of interfaces into a single interface encoding the same type.
|
|
||||||
*
|
|
||||||
* Flattens a union in such a way that:
|
|
||||||
* `{ a: number, b?: undefined } | { b: string, a?: undefined }`
|
|
||||||
* (which is the value type of `[{ a: 1 }, { b: 1 }]`)
|
|
||||||
* becomes `{ a: number | undefined, b: string | undefined }`.
|
|
||||||
*
|
|
||||||
* And also works for `{ a: number } | { b: string }` which maps to the same.
|
|
||||||
*/
|
|
||||||
export type FlattenUnionOfInterfaces<T> = {
|
|
||||||
[K in AllKeysFromUnion<T>]: KeyOfOr<
|
|
||||||
T,
|
|
||||||
// If T always has K, just take T[K] (union of C[K] for each component C of T):
|
|
||||||
K,
|
|
||||||
// Otherwise, take the union of C[K] for each component C of T, PLUS undefined:
|
|
||||||
undefined | KeyOfOr<UnionToIntersection<T>, K, void>
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
||||||
function typeAssert<T extends 'pass'>() {}
|
|
||||||
{
|
|
||||||
type Test<T, U> = [T] extends [U]
|
|
||||||
? [U] extends [T]
|
|
||||||
? 'pass'
|
|
||||||
: { actual: ResolveType<T>; expected: U }
|
|
||||||
: { actual: ResolveType<T>; expected: U };
|
|
||||||
|
|
||||||
type T01 = { a: number } | { b: string };
|
|
||||||
type T02 = { a: number } | { b?: string };
|
|
||||||
type T03 = { a: number } | { a?: number };
|
|
||||||
type T04 = { a: number } | { a: string };
|
|
||||||
type T05 = { a: number } | { a?: string };
|
|
||||||
|
|
||||||
type T11 = { a: number; b?: undefined } | { a?: undefined; b: string };
|
|
||||||
|
|
||||||
type T21 = { a: number; b?: undefined } | { b: string };
|
|
||||||
type T22 = { a: number; b?: undefined } | { b?: string };
|
|
||||||
type T23 = { a: number; b?: undefined } | { a?: number };
|
|
||||||
type T24 = { a: number; b?: undefined } | { a: string };
|
|
||||||
type T25 = { a: number; b?: undefined } | { a?: string };
|
|
||||||
type T26 = { a: number; b?: undefined } | { a: undefined };
|
|
||||||
type T27 = { a: number; b?: undefined } | { a: undefined; b: undefined };
|
|
||||||
|
|
||||||
/* prettier-ignore */ {
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T01>, { a: number | undefined; b: string | undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T02>, { a: number | undefined; b: string | undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T03>, { a: number | undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T04>, { a: number | string }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T05>, { a: number | string | undefined }>>();
|
|
||||||
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T11>, { a: number | undefined; b: string | undefined }>>();
|
|
||||||
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T22>, { a: number | undefined; b: string | undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T23>, { a: number | undefined; b: undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T24>, { a: number | string; b: undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T25>, { a: number | string | undefined; b: undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T27>, { a: number | undefined; b: undefined }>>();
|
|
||||||
|
|
||||||
// Unexpected test results - hopefully okay to ignore these
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T21>, { b: string | undefined }>>();
|
|
||||||
typeAssert<Test<FlattenUnionOfInterfaces<T26>, { a: number | undefined }>>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Merged<A, B> = MergedFromFlat<A, FlattenUnionOfInterfaces<B>>;
|
|
||||||
export type MergedFromFlat<A, B> = {
|
|
||||||
[K in keyof A | keyof B]: K extends keyof B ? B[K] : K extends keyof A ? A[K] : never;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function mergeParams<A extends {}, B extends {}>(a: A, b: B): Merged<A, B> {
|
|
||||||
for (const key of Object.keys(a)) {
|
|
||||||
assert(!(key in b), 'Duplicate key: ' + key);
|
|
||||||
}
|
|
||||||
return { ...a, ...b } as Merged<A, B>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,94 +0,0 @@
|
||||||
import { TestParams } from '../../framework/fixture.js';
|
|
||||||
import { assert, objectEquals } from '../../util/util.js';
|
|
||||||
import { paramKeyIsPublic } from '../params_utils.js';
|
|
||||||
|
|
||||||
import { TestQuery } from './query.js';
|
|
||||||
|
|
||||||
export const enum Ordering {
|
|
||||||
Unordered,
|
|
||||||
StrictSuperset,
|
|
||||||
Equal,
|
|
||||||
StrictSubset,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two queries for their ordering (which is used to build the tree).
|
|
||||||
*
|
|
||||||
* See src/unittests/query_compare.spec.ts for examples.
|
|
||||||
*/
|
|
||||||
export function compareQueries(a: TestQuery, b: TestQuery): Ordering {
|
|
||||||
if (a.suite !== b.suite) {
|
|
||||||
return Ordering.Unordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePathOrdering = comparePaths(a.filePathParts, b.filePathParts);
|
|
||||||
if (filePathOrdering !== Ordering.Equal || a.isMultiFile || b.isMultiFile) {
|
|
||||||
return compareOneLevel(filePathOrdering, a.isMultiFile, b.isMultiFile);
|
|
||||||
}
|
|
||||||
assert('testPathParts' in a && 'testPathParts' in b);
|
|
||||||
|
|
||||||
const testPathOrdering = comparePaths(a.testPathParts, b.testPathParts);
|
|
||||||
if (testPathOrdering !== Ordering.Equal || a.isMultiTest || b.isMultiTest) {
|
|
||||||
return compareOneLevel(testPathOrdering, a.isMultiTest, b.isMultiTest);
|
|
||||||
}
|
|
||||||
assert('params' in a && 'params' in b);
|
|
||||||
|
|
||||||
const paramsPathOrdering = comparePublicParamsPaths(a.params, b.params);
|
|
||||||
if (paramsPathOrdering !== Ordering.Equal || a.isMultiCase || b.isMultiCase) {
|
|
||||||
return compareOneLevel(paramsPathOrdering, a.isMultiCase, b.isMultiCase);
|
|
||||||
}
|
|
||||||
return Ordering.Equal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares a single level of a query.
|
|
||||||
*
|
|
||||||
* "IsBig" means the query is big relative to the level, e.g. for test-level:
|
|
||||||
* - Anything >= `suite:a,*` is big
|
|
||||||
* - Anything <= `suite:a:*` is small
|
|
||||||
*/
|
|
||||||
function compareOneLevel(ordering: Ordering, aIsBig: boolean, bIsBig: boolean): Ordering {
|
|
||||||
assert(ordering !== Ordering.Equal || aIsBig || bIsBig);
|
|
||||||
if (ordering === Ordering.Unordered) return Ordering.Unordered;
|
|
||||||
if (aIsBig && bIsBig) return ordering;
|
|
||||||
if (!aIsBig && !bIsBig) return Ordering.Unordered; // Equal case is already handled
|
|
||||||
// Exactly one of (a, b) is big.
|
|
||||||
if (aIsBig && ordering !== Ordering.StrictSubset) return Ordering.StrictSuperset;
|
|
||||||
if (bIsBig && ordering !== Ordering.StrictSuperset) return Ordering.StrictSubset;
|
|
||||||
return Ordering.Unordered;
|
|
||||||
}
|
|
||||||
|
|
||||||
function comparePaths(a: readonly string[], b: readonly string[]): Ordering {
|
|
||||||
const shorter = Math.min(a.length, b.length);
|
|
||||||
|
|
||||||
for (let i = 0; i < shorter; ++i) {
|
|
||||||
if (a[i] !== b[i]) {
|
|
||||||
return Ordering.Unordered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (a.length === b.length) {
|
|
||||||
return Ordering.Equal;
|
|
||||||
} else if (a.length < b.length) {
|
|
||||||
return Ordering.StrictSuperset;
|
|
||||||
} else {
|
|
||||||
return Ordering.StrictSubset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function comparePublicParamsPaths(a: TestParams, b: TestParams): Ordering {
|
|
||||||
const aKeys = Object.keys(a).filter(k => paramKeyIsPublic(k));
|
|
||||||
const commonKeys = new Set(aKeys.filter(k => k in b));
|
|
||||||
|
|
||||||
for (const k of commonKeys) {
|
|
||||||
if (!objectEquals(a[k], b[k])) {
|
|
||||||
return Ordering.Unordered;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const bKeys = Object.keys(b).filter(k => paramKeyIsPublic(k));
|
|
||||||
const aRemainingKeys = aKeys.length - commonKeys.size;
|
|
||||||
const bRemainingKeys = bKeys.length - commonKeys.size;
|
|
||||||
if (aRemainingKeys === 0 && bRemainingKeys === 0) return Ordering.Equal;
|
|
||||||
if (aRemainingKeys === 0) return Ordering.StrictSuperset;
|
|
||||||
if (bRemainingKeys === 0) return Ordering.StrictSubset;
|
|
||||||
return Ordering.Unordered;
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/**
|
|
||||||
* Encodes a stringified TestQuery so that it can be placed in a `?q=` parameter in a URL.
|
|
||||||
*
|
|
||||||
* `encodeURIComponent` encodes in accordance with `application/x-www-form-urlencoded`,
|
|
||||||
* but URLs don't actually have to be as strict as HTML form encoding
|
|
||||||
* (we interpret this purely from JavaScript).
|
|
||||||
* So we encode the component, then selectively convert some %-encoded escape codes
|
|
||||||
* back to their original form for readability/copyability.
|
|
||||||
*/
|
|
||||||
export function encodeURIComponentSelectively(s: string): string {
|
|
||||||
let ret = encodeURIComponent(s);
|
|
||||||
ret = ret.replace(/%22/g, '"'); // for JSON strings
|
|
||||||
ret = ret.replace(/%2C/g, ','); // for path separator, and JSON arrays
|
|
||||||
ret = ret.replace(/%3A/g, ':'); // for big separator
|
|
||||||
ret = ret.replace(/%3B/g, ';'); // for param separator
|
|
||||||
ret = ret.replace(/%3D/g, '='); // for params (k=v)
|
|
||||||
ret = ret.replace(/%5B/g, '['); // for JSON arrays
|
|
||||||
ret = ret.replace(/%5D/g, ']'); // for JSON arrays
|
|
||||||
ret = ret.replace(/%7B/g, '{'); // for JSON objects
|
|
||||||
ret = ret.replace(/%7D/g, '}'); // for JSON objects
|
|
||||||
ret = ret.replace(/%E2%9C%97/g, '✗'); // for jsUndefinedMagicValue
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
import { assert, sortObjectByKey } from '../../util/util.js';
|
|
||||||
import { JSONWithUndefined } from '../params_utils.js';
|
|
||||||
|
|
||||||
// JSON can't represent various values and by default stores them as `null`.
|
|
||||||
// Instead, storing them as a magic string values in JSON.
|
|
||||||
const jsUndefinedMagicValue = '_undef_';
|
|
||||||
const jsNaNMagicValue = '_nan_';
|
|
||||||
const jsPositiveInfinityMagicValue = '_posinfinity_';
|
|
||||||
const jsNegativeInfinityMagicValue = '_neginfinity_';
|
|
||||||
|
|
||||||
// -0 needs to be handled separately, because -0 === +0 returns true. Not
|
|
||||||
// special casing +0/0, since it behaves intuitively. Assuming that if -0 is
|
|
||||||
// being used, the differentiation from +0 is desired.
|
|
||||||
const jsNegativeZeroMagicValue = '_negzero_';
|
|
||||||
|
|
||||||
const toStringMagicValue = new Map<unknown, string>([
|
|
||||||
[undefined, jsUndefinedMagicValue],
|
|
||||||
[NaN, jsNaNMagicValue],
|
|
||||||
[Number.POSITIVE_INFINITY, jsPositiveInfinityMagicValue],
|
|
||||||
[Number.NEGATIVE_INFINITY, jsNegativeInfinityMagicValue],
|
|
||||||
// No -0 handling because it is special cased.
|
|
||||||
]);
|
|
||||||
|
|
||||||
const fromStringMagicValue = new Map<string, unknown>([
|
|
||||||
[jsUndefinedMagicValue, undefined],
|
|
||||||
[jsNaNMagicValue, NaN],
|
|
||||||
[jsPositiveInfinityMagicValue, Number.POSITIVE_INFINITY],
|
|
||||||
[jsNegativeInfinityMagicValue, Number.NEGATIVE_INFINITY],
|
|
||||||
// -0 is handled in this direction because there is no comparison issue.
|
|
||||||
[jsNegativeZeroMagicValue, -0],
|
|
||||||
]);
|
|
||||||
|
|
||||||
function stringifyFilter(k: string, v: unknown): unknown {
|
|
||||||
// Make sure no one actually uses a magic value as a parameter.
|
|
||||||
if (typeof v === 'string') {
|
|
||||||
assert(
|
|
||||||
!fromStringMagicValue.has(v),
|
|
||||||
`${v} is a magic value for stringification, so cannot be used`
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
|
||||||
v !== jsNegativeZeroMagicValue,
|
|
||||||
`${v} is a magic value for stringification, so cannot be used`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.is(v, -0)) {
|
|
||||||
return jsNegativeZeroMagicValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return toStringMagicValue.has(v) ? toStringMagicValue.get(v) : v;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringifyParamValue(value: JSONWithUndefined): string {
|
|
||||||
return JSON.stringify(value, stringifyFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Like stringifyParamValue but sorts dictionaries by key, for hashing.
|
|
||||||
*/
|
|
||||||
export function stringifyParamValueUniquely(value: JSONWithUndefined): string {
|
|
||||||
return JSON.stringify(value, (k, v) => {
|
|
||||||
if (typeof v === 'object' && v !== null) {
|
|
||||||
return sortObjectByKey(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return stringifyFilter(k, v);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'any' is part of the JSON.parse reviver interface, so cannot be avoided.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function parseParamValueReviver(k: string, v: any): any {
|
|
||||||
if (fromStringMagicValue.has(v)) {
|
|
||||||
return fromStringMagicValue.get(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseParamValue(s: string): JSONWithUndefined {
|
|
||||||
return JSON.parse(s, parseParamValueReviver);
|
|
||||||
}
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
import { assert } from '../../util/util.js';
|
|
||||||
import {
|
|
||||||
TestParamsRW,
|
|
||||||
JSONWithUndefined,
|
|
||||||
badParamValueChars,
|
|
||||||
paramKeyIsPublic,
|
|
||||||
} from '../params_utils.js';
|
|
||||||
|
|
||||||
import { parseParamValue } from './json_param_value.js';
|
|
||||||
import {
|
|
||||||
TestQuery,
|
|
||||||
TestQueryMultiFile,
|
|
||||||
TestQueryMultiTest,
|
|
||||||
TestQueryMultiCase,
|
|
||||||
TestQuerySingleCase,
|
|
||||||
} from './query.js';
|
|
||||||
import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './separators.js';
|
|
||||||
import { validQueryPart } from './validQueryPart.js';
|
|
||||||
|
|
||||||
export function parseQuery(s: string): TestQuery {
|
|
||||||
try {
|
|
||||||
return parseQueryImpl(s);
|
|
||||||
} catch (ex) {
|
|
||||||
if (ex instanceof Error) {
|
|
||||||
ex.message += '\n on: ' + s;
|
|
||||||
}
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseQueryImpl(s: string): TestQuery {
|
|
||||||
// Undo encodeURIComponentSelectively
|
|
||||||
s = decodeURIComponent(s);
|
|
||||||
|
|
||||||
// bigParts are: suite, file, test, params (note kBigSeparator could appear in params)
|
|
||||||
let suite: string;
|
|
||||||
let fileString: string | undefined;
|
|
||||||
let testString: string | undefined;
|
|
||||||
let paramsString: string | undefined;
|
|
||||||
{
|
|
||||||
const i1 = s.indexOf(kBigSeparator);
|
|
||||||
assert(i1 !== -1, `query string must have at least one ${kBigSeparator}`);
|
|
||||||
suite = s.substring(0, i1);
|
|
||||||
const i2 = s.indexOf(kBigSeparator, i1 + 1);
|
|
||||||
if (i2 === -1) {
|
|
||||||
fileString = s.substring(i1 + 1);
|
|
||||||
} else {
|
|
||||||
fileString = s.substring(i1 + 1, i2);
|
|
||||||
const i3 = s.indexOf(kBigSeparator, i2 + 1);
|
|
||||||
if (i3 === -1) {
|
|
||||||
testString = s.substring(i2 + 1);
|
|
||||||
} else {
|
|
||||||
testString = s.substring(i2 + 1, i3);
|
|
||||||
paramsString = s.substring(i3 + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { parts: file, wildcard: filePathHasWildcard } = parseBigPart(fileString, kPathSeparator);
|
|
||||||
|
|
||||||
if (testString === undefined) {
|
|
||||||
// Query is file-level
|
|
||||||
assert(
|
|
||||||
filePathHasWildcard,
|
|
||||||
`File-level query without wildcard ${kWildcard}. Did you want a file-level query \
|
|
||||||
(append ${kPathSeparator}${kWildcard}) or test-level query (append ${kBigSeparator}${kWildcard})?`
|
|
||||||
);
|
|
||||||
return new TestQueryMultiFile(suite, file);
|
|
||||||
}
|
|
||||||
assert(!filePathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);
|
|
||||||
|
|
||||||
const { parts: test, wildcard: testPathHasWildcard } = parseBigPart(testString, kPathSeparator);
|
|
||||||
|
|
||||||
if (paramsString === undefined) {
|
|
||||||
// Query is test-level
|
|
||||||
assert(
|
|
||||||
testPathHasWildcard,
|
|
||||||
`Test-level query without wildcard ${kWildcard}; did you want a test-level query \
|
|
||||||
(append ${kPathSeparator}${kWildcard}) or case-level query (append ${kBigSeparator}${kWildcard})?`
|
|
||||||
);
|
|
||||||
assert(file.length > 0, 'File part of test-level query was empty (::)');
|
|
||||||
return new TestQueryMultiTest(suite, file, test);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query is case-level
|
|
||||||
assert(!testPathHasWildcard, `Wildcard ${kWildcard} must be at the end of the query string`);
|
|
||||||
|
|
||||||
const { parts: paramsParts, wildcard: paramsHasWildcard } = parseBigPart(
|
|
||||||
paramsString,
|
|
||||||
kParamSeparator
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(test.length > 0, 'Test part of case-level query was empty (::)');
|
|
||||||
|
|
||||||
const params: TestParamsRW = {};
|
|
||||||
for (const paramPart of paramsParts) {
|
|
||||||
const [k, v] = parseSingleParam(paramPart);
|
|
||||||
assert(validQueryPart.test(k), `param key names must match ${validQueryPart}`);
|
|
||||||
params[k] = v;
|
|
||||||
}
|
|
||||||
if (paramsHasWildcard) {
|
|
||||||
return new TestQueryMultiCase(suite, file, test, params);
|
|
||||||
} else {
|
|
||||||
return new TestQuerySingleCase(suite, file, test, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// webgpu:a,b,* or webgpu:a,b,c:*
|
|
||||||
const kExampleQueries = `\
|
|
||||||
webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}${kWildcard} or \
|
|
||||||
webgpu${kBigSeparator}a${kPathSeparator}b${kPathSeparator}c${kBigSeparator}${kWildcard}`;
|
|
||||||
|
|
||||||
function parseBigPart(
|
|
||||||
s: string,
|
|
||||||
separator: typeof kParamSeparator | typeof kPathSeparator
|
|
||||||
): { parts: string[]; wildcard: boolean } {
|
|
||||||
if (s === '') {
|
|
||||||
return { parts: [], wildcard: false };
|
|
||||||
}
|
|
||||||
const parts = s.split(separator);
|
|
||||||
|
|
||||||
let endsWithWildcard = false;
|
|
||||||
for (const [i, part] of parts.entries()) {
|
|
||||||
if (i === parts.length - 1) {
|
|
||||||
endsWithWildcard = part === kWildcard;
|
|
||||||
}
|
|
||||||
assert(
|
|
||||||
part.indexOf(kWildcard) === -1 || endsWithWildcard,
|
|
||||||
`Wildcard ${kWildcard} must be complete last part of a path (e.g. ${kExampleQueries})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (endsWithWildcard) {
|
|
||||||
// Remove the last element of the array (which is just the wildcard).
|
|
||||||
parts.length = parts.length - 1;
|
|
||||||
}
|
|
||||||
return { parts, wildcard: endsWithWildcard };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSingleParam(paramSubstring: string): [string, JSONWithUndefined] {
|
|
||||||
assert(paramSubstring !== '', 'Param in a query must not be blank (is there a trailing comma?)');
|
|
||||||
const i = paramSubstring.indexOf('=');
|
|
||||||
assert(i !== -1, 'Param in a query must be of form key=value');
|
|
||||||
const k = paramSubstring.substring(0, i);
|
|
||||||
assert(paramKeyIsPublic(k), 'Param in a query must not be private (start with _)');
|
|
||||||
const v = paramSubstring.substring(i + 1);
|
|
||||||
return [k, parseSingleParamValue(v)];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSingleParamValue(s: string): JSONWithUndefined {
|
|
||||||
assert(
|
|
||||||
!badParamValueChars.test(s),
|
|
||||||
`param value must not match ${badParamValueChars} - was ${s}`
|
|
||||||
);
|
|
||||||
return parseParamValue(s);
|
|
||||||
}
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
import { TestParams } from '../../framework/fixture.js';
|
|
||||||
import { optionEnabled } from '../../runtime/helper/options.js';
|
|
||||||
import { assert, unreachable } from '../../util/util.js';
|
|
||||||
import { Expectation } from '../logging/result.js';
|
|
||||||
|
|
||||||
import { compareQueries, Ordering } from './compare.js';
|
|
||||||
import { encodeURIComponentSelectively } from './encode_selectively.js';
|
|
||||||
import { parseQuery } from './parseQuery.js';
|
|
||||||
import { kBigSeparator, kPathSeparator, kWildcard } from './separators.js';
|
|
||||||
import { stringifyPublicParams } from './stringify_params.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a test query of some level.
|
|
||||||
*
|
|
||||||
* TestQuery types are immutable.
|
|
||||||
*/
|
|
||||||
export type TestQuery =
|
|
||||||
| TestQuerySingleCase
|
|
||||||
| TestQueryMultiCase
|
|
||||||
| TestQueryMultiTest
|
|
||||||
| TestQueryMultiFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* - 1 = MultiFile.
|
|
||||||
* - 2 = MultiTest.
|
|
||||||
* - 3 = MultiCase.
|
|
||||||
* - 4 = SingleCase.
|
|
||||||
*/
|
|
||||||
export type TestQueryLevel = 1 | 2 | 3 | 4;
|
|
||||||
|
|
||||||
export interface TestQueryWithExpectation {
|
|
||||||
query: TestQuery;
|
|
||||||
expectation: Expectation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A multi-file test query, like `s:*` or `s:a,b,*`.
|
|
||||||
*
|
|
||||||
* Immutable (makes copies of constructor args).
|
|
||||||
*/
|
|
||||||
export class TestQueryMultiFile {
|
|
||||||
readonly level: TestQueryLevel = 1;
|
|
||||||
readonly isMultiFile: boolean = true;
|
|
||||||
readonly suite: string;
|
|
||||||
readonly filePathParts: readonly string[];
|
|
||||||
|
|
||||||
constructor(suite: string, file: readonly string[]) {
|
|
||||||
this.suite = suite;
|
|
||||||
this.filePathParts = [...file];
|
|
||||||
}
|
|
||||||
|
|
||||||
get depthInLevel() {
|
|
||||||
return this.filePathParts.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return encodeURIComponentSelectively(this.toStringHelper().join(kBigSeparator));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toStringHelper(): string[] {
|
|
||||||
return [this.suite, [...this.filePathParts, kWildcard].join(kPathSeparator)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A multi-test test query, like `s:f:*` or `s:f:a,b,*`.
|
|
||||||
*
|
|
||||||
* Immutable (makes copies of constructor args).
|
|
||||||
*/
|
|
||||||
export class TestQueryMultiTest extends TestQueryMultiFile {
|
|
||||||
readonly level: TestQueryLevel = 2;
|
|
||||||
readonly isMultiFile: false = false;
|
|
||||||
readonly isMultiTest: boolean = true;
|
|
||||||
readonly testPathParts: readonly string[];
|
|
||||||
|
|
||||||
constructor(suite: string, file: readonly string[], test: readonly string[]) {
|
|
||||||
super(suite, file);
|
|
||||||
assert(file.length > 0, 'multi-test (or finer) query must have file-path');
|
|
||||||
this.testPathParts = [...test];
|
|
||||||
}
|
|
||||||
|
|
||||||
get depthInLevel() {
|
|
||||||
return this.testPathParts.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toStringHelper(): string[] {
|
|
||||||
return [
|
|
||||||
this.suite,
|
|
||||||
this.filePathParts.join(kPathSeparator),
|
|
||||||
[...this.testPathParts, kWildcard].join(kPathSeparator),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A multi-case test query, like `s:f:t:*` or `s:f:t:a,b,*`.
|
|
||||||
*
|
|
||||||
* Immutable (makes copies of constructor args), except for param values
|
|
||||||
* (which aren't normally supposed to change; they're marked readonly in TestParams).
|
|
||||||
*/
|
|
||||||
export class TestQueryMultiCase extends TestQueryMultiTest {
|
|
||||||
readonly level: TestQueryLevel = 3;
|
|
||||||
readonly isMultiTest: false = false;
|
|
||||||
readonly isMultiCase: boolean = true;
|
|
||||||
readonly params: TestParams;
|
|
||||||
|
|
||||||
constructor(suite: string, file: readonly string[], test: readonly string[], params: TestParams) {
|
|
||||||
super(suite, file, test);
|
|
||||||
assert(test.length > 0, 'multi-case (or finer) query must have test-path');
|
|
||||||
this.params = { ...params };
|
|
||||||
}
|
|
||||||
|
|
||||||
get depthInLevel() {
|
|
||||||
return Object.keys(this.params).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toStringHelper(): string[] {
|
|
||||||
return [
|
|
||||||
this.suite,
|
|
||||||
this.filePathParts.join(kPathSeparator),
|
|
||||||
this.testPathParts.join(kPathSeparator),
|
|
||||||
stringifyPublicParams(this.params, true),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A multi-case test query, like `s:f:t:` or `s:f:t:a=1,b=1`.
|
|
||||||
*
|
|
||||||
* Immutable (makes copies of constructor args).
|
|
||||||
*/
|
|
||||||
export class TestQuerySingleCase extends TestQueryMultiCase {
|
|
||||||
readonly level: TestQueryLevel = 4;
|
|
||||||
readonly isMultiCase: false = false;
|
|
||||||
|
|
||||||
get depthInLevel() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toStringHelper(): string[] {
|
|
||||||
return [
|
|
||||||
this.suite,
|
|
||||||
this.filePathParts.join(kPathSeparator),
|
|
||||||
this.testPathParts.join(kPathSeparator),
|
|
||||||
stringifyPublicParams(this.params),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse raw expectations input into TestQueryWithExpectation[], filtering so that only
|
|
||||||
* expectations that are relevant for the provided query and wptURL.
|
|
||||||
*
|
|
||||||
* `rawExpectations` should be @type {{ query: string, expectation: Expectation }[]}
|
|
||||||
*
|
|
||||||
* The `rawExpectations` are parsed and validated that they are in the correct format.
|
|
||||||
* If `wptURL` is passed, the query string should be of the full path format such
|
|
||||||
* as `path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;*`.
|
|
||||||
* If `wptURL` is `undefined`, the query string should be only the query
|
|
||||||
* `suite:test_path:test_name:foo=1;bar=2;*`.
|
|
||||||
*/
|
|
||||||
export function parseExpectationsForTestQuery(
|
|
||||||
rawExpectations:
|
|
||||||
| unknown
|
|
||||||
| {
|
|
||||||
query: string;
|
|
||||||
expectation: Expectation;
|
|
||||||
}[],
|
|
||||||
query: TestQuery,
|
|
||||||
wptURL?: URL
|
|
||||||
) {
|
|
||||||
if (!Array.isArray(rawExpectations)) {
|
|
||||||
unreachable('Expectations should be an array');
|
|
||||||
}
|
|
||||||
const expectations: TestQueryWithExpectation[] = [];
|
|
||||||
for (const entry of rawExpectations) {
|
|
||||||
assert(typeof entry === 'object');
|
|
||||||
const rawExpectation = entry as { query?: string; expectation?: string };
|
|
||||||
assert(rawExpectation.query !== undefined, 'Expectation missing query string');
|
|
||||||
assert(rawExpectation.expectation !== undefined, 'Expectation missing expectation string');
|
|
||||||
|
|
||||||
let expectationQuery: TestQuery;
|
|
||||||
if (wptURL !== undefined) {
|
|
||||||
const expectationURL = new URL(`${wptURL.origin}/${entry.query}`);
|
|
||||||
if (expectationURL.pathname !== wptURL.pathname) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
assert(
|
|
||||||
expectationURL.pathname === wptURL.pathname,
|
|
||||||
`Invalid expectation path ${expectationURL.pathname}
|
|
||||||
Expectation should be of the form path/to/cts.https.html?worker=0&q=suite:test_path:test_name:foo=1;bar=2;...
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const params = expectationURL.searchParams;
|
|
||||||
if (optionEnabled('worker', params) !== optionEnabled('worker', wptURL.searchParams)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const qs = params.getAll('q');
|
|
||||||
assert(qs.length === 1, 'currently, there must be exactly one ?q= in the expectation string');
|
|
||||||
expectationQuery = parseQuery(qs[0]);
|
|
||||||
} else {
|
|
||||||
expectationQuery = parseQuery(entry.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip params from multicase expectations so that an expectation of foo=2;*
|
|
||||||
// is stored if the test query is bar=3;*
|
|
||||||
const queryForFilter =
|
|
||||||
expectationQuery instanceof TestQueryMultiCase
|
|
||||||
? new TestQueryMultiCase(
|
|
||||||
expectationQuery.suite,
|
|
||||||
expectationQuery.filePathParts,
|
|
||||||
expectationQuery.testPathParts,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
: expectationQuery;
|
|
||||||
|
|
||||||
if (compareQueries(query, queryForFilter) === Ordering.Unordered) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (entry.expectation) {
|
|
||||||
case 'pass':
|
|
||||||
case 'skip':
|
|
||||||
case 'fail':
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
unreachable(`Invalid expectation ${entry.expectation}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
expectations.push({
|
|
||||||
query: expectationQuery,
|
|
||||||
expectation: entry.expectation,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return expectations;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For display purposes only, produces a "relative" query string from parent to child.
|
|
||||||
* Used in the wpt runtime to reduce the verbosity of logs.
|
|
||||||
*/
|
|
||||||
export function relativeQueryString(parent: TestQuery, child: TestQuery): string {
|
|
||||||
const ordering = compareQueries(parent, child);
|
|
||||||
if (ordering === Ordering.Equal) {
|
|
||||||
return '';
|
|
||||||
} else if (ordering === Ordering.StrictSuperset) {
|
|
||||||
const parentString = parent.toString();
|
|
||||||
assert(parentString.endsWith(kWildcard));
|
|
||||||
const childString = child.toString();
|
|
||||||
assert(
|
|
||||||
childString.startsWith(parentString.substring(0, parentString.length - 2)),
|
|
||||||
'impossible?: childString does not start with parentString[:-2]'
|
|
||||||
);
|
|
||||||
return childString.substring(parentString.length - 2);
|
|
||||||
} else {
|
|
||||||
unreachable(
|
|
||||||
`relativeQueryString arguments have invalid ordering ${ordering}:\n${parent}\n${child}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
/** Separator between big parts: suite:file:test:case */
|
|
||||||
export const kBigSeparator = ':';
|
|
||||||
|
|
||||||
/** Separator between path,to,file or path,to,test */
|
|
||||||
export const kPathSeparator = ',';
|
|
||||||
|
|
||||||
/** Separator between k=v;k=v */
|
|
||||||
export const kParamSeparator = ';';
|
|
||||||
|
|
||||||
/** Separator between key and value in k=v */
|
|
||||||
export const kParamKVSeparator = '=';
|
|
||||||
|
|
||||||
/** Final wildcard, if query is not single-case */
|
|
||||||
export const kWildcard = '*';
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { TestParams } from '../../framework/fixture.js';
|
|
||||||
import { assert } from '../../util/util.js';
|
|
||||||
import { JSONWithUndefined, badParamValueChars, paramKeyIsPublic } from '../params_utils.js';
|
|
||||||
|
|
||||||
import { stringifyParamValue, stringifyParamValueUniquely } from './json_param_value.js';
|
|
||||||
import { kParamKVSeparator, kParamSeparator, kWildcard } from './separators.js';
|
|
||||||
|
|
||||||
export function stringifyPublicParams(p: TestParams, addWildcard = false): string {
|
|
||||||
const parts = Object.keys(p)
|
|
||||||
.filter(k => paramKeyIsPublic(k))
|
|
||||||
.map(k => stringifySingleParam(k, p[k]));
|
|
||||||
|
|
||||||
if (addWildcard) parts.push(kWildcard);
|
|
||||||
|
|
||||||
return parts.join(kParamSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An _approximately_ unique string representing a CaseParams value.
|
|
||||||
*/
|
|
||||||
export function stringifyPublicParamsUniquely(p: TestParams): string {
|
|
||||||
const keys = Object.keys(p).sort();
|
|
||||||
return keys
|
|
||||||
.filter(k => paramKeyIsPublic(k))
|
|
||||||
.map(k => stringifySingleParamUniquely(k, p[k]))
|
|
||||||
.join(kParamSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringifySingleParam(k: string, v: JSONWithUndefined) {
|
|
||||||
return `${k}${kParamKVSeparator}${stringifySingleParamValue(v)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifySingleParamUniquely(k: string, v: JSONWithUndefined) {
|
|
||||||
return `${k}${kParamKVSeparator}${stringifyParamValueUniquely(v)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringifySingleParamValue(v: JSONWithUndefined): string {
|
|
||||||
const s = stringifyParamValue(v);
|
|
||||||
assert(
|
|
||||||
!badParamValueChars.test(s),
|
|
||||||
`JSON.stringified param value must not match ${badParamValueChars} - was ${s}`
|
|
||||||
);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
/** Applies to group parts, test parts, params keys. */
|
|
||||||
export const validQueryPart = /^[a-zA-Z0-9_]+$/;
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
// Returns the stack trace of an Error, but without the extra boilerplate at the bottom
|
|
||||||
// (e.g. RunCaseSpecific, processTicksAndRejections, etc.), for logging.
|
|
||||||
export function extractImportantStackTrace(e: Error): string {
|
|
||||||
let stack = e.stack;
|
|
||||||
if (!stack) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const redundantMessage = 'Error: ' + e.message + '\n';
|
|
||||||
if (stack.startsWith(redundantMessage)) {
|
|
||||||
stack = stack.substring(redundantMessage.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = stack.split('\n');
|
|
||||||
for (let i = lines.length - 1; i >= 0; --i) {
|
|
||||||
const line = lines[i];
|
|
||||||
if (line.indexOf('.spec.') !== -1) {
|
|
||||||
return lines.slice(0, i + 1).join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
// *** Examples ***
|
|
||||||
//
|
|
||||||
// Node fail()
|
|
||||||
// > Error:
|
|
||||||
// > at CaseRecorder.fail (/Users/kainino/src/cts/src/common/framework/logger.ts:99:30)
|
|
||||||
// > at RunCaseSpecific.exports.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/logger.spec.ts:80:7)
|
|
||||||
// x at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
|
|
||||||
// x at processTicksAndRejections (internal/process/task_queues.js:86:5)
|
|
||||||
//
|
|
||||||
// Node throw
|
|
||||||
// > Error: hello
|
|
||||||
// > at RunCaseSpecific.g.test.t [as fn] (/Users/kainino/src/cts/src/unittests/test_group.spec.ts:51:11)
|
|
||||||
// x at RunCaseSpecific.run (/Users/kainino/src/cts/src/common/framework/test_group.ts:121:18)
|
|
||||||
// x at processTicksAndRejections (internal/process/task_queues.js:86:5)
|
|
||||||
//
|
|
||||||
// Firefox fail()
|
|
||||||
// > fail@http://localhost:8080/out/framework/logger.js:104:30
|
|
||||||
// > expect@http://localhost:8080/out/framework/default_fixture.js:59:16
|
|
||||||
// > @http://localhost:8080/out/unittests/util.spec.js:35:5
|
|
||||||
// x run@http://localhost:8080/out/framework/test_group.js:119:18
|
|
||||||
//
|
|
||||||
// Firefox throw
|
|
||||||
// > @http://localhost:8080/out/unittests/test_group.spec.js:48:11
|
|
||||||
// x run@http://localhost:8080/out/framework/test_group.js:119:18
|
|
||||||
//
|
|
||||||
// Safari fail()
|
|
||||||
// > fail@http://localhost:8080/out/framework/logger.js:104:39
|
|
||||||
// > expect@http://localhost:8080/out/framework/default_fixture.js:59:20
|
|
||||||
// > http://localhost:8080/out/unittests/util.spec.js:35:11
|
|
||||||
// x http://localhost:8080/out/framework/test_group.js:119:20
|
|
||||||
// x asyncFunctionResume@[native code]
|
|
||||||
// x [native code]
|
|
||||||
// x promiseReactionJob@[native code]
|
|
||||||
//
|
|
||||||
// Safari throw
|
|
||||||
// > http://localhost:8080/out/unittests/test_group.spec.js:48:20
|
|
||||||
// x http://localhost:8080/out/framework/test_group.js:119:20
|
|
||||||
// x asyncFunctionResume@[native code]
|
|
||||||
// x [native code]
|
|
||||||
// x promiseReactionJob@[native code]
|
|
||||||
//
|
|
||||||
// Chrome fail()
|
|
||||||
// x Error
|
|
||||||
// x at CaseRecorder.fail (http://localhost:8080/out/framework/logger.js:104:30)
|
|
||||||
// x at DefaultFixture.expect (http://localhost:8080/out/framework/default_fixture.js:59:16)
|
|
||||||
// > at RunCaseSpecific.fn (http://localhost:8080/out/unittests/util.spec.js:35:5)
|
|
||||||
// x at RunCaseSpecific.run (http://localhost:8080/out/framework/test_group.js:119:18)
|
|
||||||
// x at async runCase (http://localhost:8080/out/runtime/standalone.js:37:17)
|
|
||||||
// x at async http://localhost:8080/out/runtime/standalone.js:102:7
|
|
||||||
//
|
|
||||||
// Chrome throw
|
|
||||||
// x Error: hello
|
|
||||||
// > at RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:48:11)
|
|
||||||
// x at RunCaseSpecific.run (http://localhost:8080/out/framework/test_group.js:119:18)"
|
|
||||||
// x at async Promise.all (index 0)
|
|
||||||
// x at async TestGroupTest.run (http://localhost:8080/out/unittests/test_group_test.js:6:5)
|
|
||||||
// x at async RunCaseSpecific.fn (http://localhost:8080/out/unittests/test_group.spec.js:53:15)
|
|
||||||
// x at async RunCaseSpecific.run (http://localhost:8080/out/framework/test_group.js:119:7)
|
|
||||||
// x at async runCase (http://localhost:8080/out/runtime/standalone.js:37:17)
|
|
||||||
// x at async http://localhost:8080/out/runtime/standalone.js:102:7
|
|
||||||
|
|
@ -1,646 +0,0 @@
|
||||||
import {
|
|
||||||
Fixture,
|
|
||||||
SubcaseBatchState,
|
|
||||||
SkipTestCase,
|
|
||||||
TestParams,
|
|
||||||
UnexpectedPassError,
|
|
||||||
} from '../framework/fixture.js';
|
|
||||||
import {
|
|
||||||
CaseParamsBuilder,
|
|
||||||
builderIterateCasesWithSubcases,
|
|
||||||
kUnitCaseParamsBuilder,
|
|
||||||
ParamsBuilderBase,
|
|
||||||
SubcaseParamsBuilder,
|
|
||||||
} from '../framework/params_builder.js';
|
|
||||||
import { globalTestConfig } from '../framework/test_config.js';
|
|
||||||
import { Expectation } from '../internal/logging/result.js';
|
|
||||||
import { TestCaseRecorder } from '../internal/logging/test_case_recorder.js';
|
|
||||||
import { extractPublicParams, Merged, mergeParams } from '../internal/params_utils.js';
|
|
||||||
import { compareQueries, Ordering } from '../internal/query/compare.js';
|
|
||||||
import { TestQuerySingleCase, TestQueryWithExpectation } from '../internal/query/query.js';
|
|
||||||
import { kPathSeparator } from '../internal/query/separators.js';
|
|
||||||
import {
|
|
||||||
stringifyPublicParams,
|
|
||||||
stringifyPublicParamsUniquely,
|
|
||||||
} from '../internal/query/stringify_params.js';
|
|
||||||
import { validQueryPart } from '../internal/query/validQueryPart.js';
|
|
||||||
import { assert, unreachable } from '../util/util.js';
|
|
||||||
|
|
||||||
export type RunFn = (
|
|
||||||
rec: TestCaseRecorder,
|
|
||||||
expectations?: TestQueryWithExpectation[]
|
|
||||||
) => Promise<void>;
|
|
||||||
|
|
||||||
export interface TestCaseID {
|
|
||||||
readonly test: readonly string[];
|
|
||||||
readonly params: TestParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RunCase {
|
|
||||||
readonly id: TestCaseID;
|
|
||||||
readonly isUnimplemented: boolean;
|
|
||||||
run(
|
|
||||||
rec: TestCaseRecorder,
|
|
||||||
selfQuery: TestQuerySingleCase,
|
|
||||||
expectations: TestQueryWithExpectation[]
|
|
||||||
): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface for defining tests
|
|
||||||
export interface TestGroupBuilder<S extends SubcaseBatchState, F extends Fixture<S>> {
|
|
||||||
test(name: string): TestBuilderWithName<S, F>;
|
|
||||||
}
|
|
||||||
export function makeTestGroup<S extends SubcaseBatchState, F extends Fixture<S>>(
|
|
||||||
fixture: FixtureClass<S, F>
|
|
||||||
): TestGroupBuilder<S, F> {
|
|
||||||
return new TestGroup((fixture as unknown) as FixtureClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interfaces for running tests
|
|
||||||
export interface IterableTestGroup {
|
|
||||||
iterate(): Iterable<IterableTest>;
|
|
||||||
validate(): void;
|
|
||||||
}
|
|
||||||
export interface IterableTest {
|
|
||||||
testPath: string[];
|
|
||||||
description: string | undefined;
|
|
||||||
readonly testCreationStack: Error;
|
|
||||||
iterate(): Iterable<RunCase>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeTestGroupForUnitTesting<F extends Fixture>(
|
|
||||||
fixture: FixtureClass<SubcaseBatchState, F>
|
|
||||||
): TestGroup<SubcaseBatchState, F> {
|
|
||||||
return new TestGroup(fixture);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FixtureClass<
|
|
||||||
S extends SubcaseBatchState = SubcaseBatchState,
|
|
||||||
F extends Fixture<S> = Fixture<S>
|
|
||||||
> = {
|
|
||||||
new (sharedState: S, log: TestCaseRecorder, params: TestParams): F;
|
|
||||||
MakeSharedState(params: TestParams): S;
|
|
||||||
};
|
|
||||||
type TestFn<F extends Fixture, P extends {}> = (t: F & { params: P }) => Promise<void> | void;
|
|
||||||
type BeforeAllSubcasesFn<S extends SubcaseBatchState, P extends {}> = (
|
|
||||||
s: S & { params: P }
|
|
||||||
) => Promise<void> | void;
|
|
||||||
|
|
||||||
export class TestGroup<S extends SubcaseBatchState, F extends Fixture<S>>
|
|
||||||
implements TestGroupBuilder<S, F> {
|
|
||||||
private fixture: FixtureClass;
|
|
||||||
private seen: Set<string> = new Set();
|
|
||||||
private tests: Array<TestBuilder<S, F>> = [];
|
|
||||||
|
|
||||||
constructor(fixture: FixtureClass) {
|
|
||||||
this.fixture = fixture;
|
|
||||||
}
|
|
||||||
|
|
||||||
iterate(): Iterable<IterableTest> {
|
|
||||||
return this.tests;
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkName(name: string): void {
|
|
||||||
assert(
|
|
||||||
// Shouldn't happen due to the rule above. Just makes sure that treating
|
|
||||||
// unencoded strings as encoded strings is OK.
|
|
||||||
name === decodeURIComponent(name),
|
|
||||||
`Not decodeURIComponent-idempotent: ${name} !== ${decodeURIComponent(name)}`
|
|
||||||
);
|
|
||||||
assert(!this.seen.has(name), `Duplicate test name: ${name}`);
|
|
||||||
|
|
||||||
this.seen.add(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
test(name: string): TestBuilderWithName<S, F> {
|
|
||||||
const testCreationStack = new Error(`Test created: ${name}`);
|
|
||||||
|
|
||||||
this.checkName(name);
|
|
||||||
|
|
||||||
const parts = name.split(kPathSeparator);
|
|
||||||
for (const p of parts) {
|
|
||||||
assert(validQueryPart.test(p), `Invalid test name part ${p}; must match ${validQueryPart}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const test = new TestBuilder(parts, this.fixture, testCreationStack);
|
|
||||||
this.tests.push(test);
|
|
||||||
return (test as unknown) as TestBuilderWithName<S, F>;
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
for (const test of this.tests) {
|
|
||||||
test.validate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestBuilderWithName<S extends SubcaseBatchState, F extends Fixture<S>>
|
|
||||||
extends TestBuilderWithParams<S, F, {}, {}> {
|
|
||||||
desc(description: string): this;
|
|
||||||
/**
|
|
||||||
* A noop function to associate a test with the relevant part of the specification.
|
|
||||||
*
|
|
||||||
* @param url a link to the spec where test is extracted from.
|
|
||||||
*/
|
|
||||||
specURL(url: string): this;
|
|
||||||
/**
|
|
||||||
* Parameterize the test, generating multiple cases, each possibly having subcases.
|
|
||||||
*
|
|
||||||
* The `unit` value passed to the `cases` callback is an immutable constant
|
|
||||||
* `CaseParamsBuilder<{}>` representing the "unit" builder `[ {} ]`,
|
|
||||||
* provided for convenience. The non-callback overload can be used if `unit` is not needed.
|
|
||||||
*/
|
|
||||||
params<CaseP extends {}, SubcaseP extends {}>(
|
|
||||||
cases: (unit: CaseParamsBuilder<{}>) => ParamsBuilderBase<CaseP, SubcaseP>
|
|
||||||
): TestBuilderWithParams<S, F, CaseP, SubcaseP>;
|
|
||||||
/**
|
|
||||||
* Parameterize the test, generating multiple cases, each possibly having subcases.
|
|
||||||
*
|
|
||||||
* Use the callback overload of this method if a "unit" builder is needed.
|
|
||||||
*/
|
|
||||||
params<CaseP extends {}, SubcaseP extends {}>(
|
|
||||||
cases: ParamsBuilderBase<CaseP, SubcaseP>
|
|
||||||
): TestBuilderWithParams<S, F, CaseP, SubcaseP>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameterize the test, generating multiple cases, without subcases.
|
|
||||||
*/
|
|
||||||
paramsSimple<P extends {}>(cases: Iterable<P>): TestBuilderWithParams<S, F, P, {}>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parameterize the test, generating one case with multiple subcases.
|
|
||||||
*/
|
|
||||||
paramsSubcasesOnly<P extends {}>(subcases: Iterable<P>): TestBuilderWithParams<S, F, {}, P>;
|
|
||||||
/**
|
|
||||||
* Parameterize the test, generating one case with multiple subcases.
|
|
||||||
*
|
|
||||||
* The `unit` value passed to the `subcases` callback is an immutable constant
|
|
||||||
* `SubcaseParamsBuilder<{}>`, with one empty case `{}` and one empty subcase `{}`.
|
|
||||||
*/
|
|
||||||
paramsSubcasesOnly<P extends {}>(
|
|
||||||
subcases: (unit: SubcaseParamsBuilder<{}, {}>) => SubcaseParamsBuilder<{}, P>
|
|
||||||
): TestBuilderWithParams<S, F, {}, P>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestBuilderWithParams<
|
|
||||||
S extends SubcaseBatchState,
|
|
||||||
F extends Fixture<S>,
|
|
||||||
CaseP extends {},
|
|
||||||
SubcaseP extends {}
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Limit subcases to a maximum number of per testcase.
|
|
||||||
* @param b the maximum number of subcases per testcase.
|
|
||||||
*
|
|
||||||
* If the number of subcases exceeds `b`, add an internal
|
|
||||||
* numeric, incrementing `batch__` param to split subcases
|
|
||||||
* into groups of at most `b` subcases.
|
|
||||||
*/
|
|
||||||
batch(b: number): this;
|
|
||||||
/**
|
|
||||||
* Run a function on shared subcase batch state before each
|
|
||||||
* batch of subcases.
|
|
||||||
* @param fn the function to run. It is called with the test
|
|
||||||
* fixture's shared subcase batch state.
|
|
||||||
*
|
|
||||||
* Generally, this function should be careful to avoid mutating
|
|
||||||
* any state on the shared subcase batch state which could result
|
|
||||||
* in unexpected order-dependent test behavior.
|
|
||||||
*/
|
|
||||||
beforeAllSubcases(fn: BeforeAllSubcasesFn<S, CaseP>): this;
|
|
||||||
/**
|
|
||||||
* Set the test function.
|
|
||||||
* @param fn the test function.
|
|
||||||
*/
|
|
||||||
fn(fn: TestFn<F, Merged<CaseP, SubcaseP>>): void;
|
|
||||||
/**
|
|
||||||
* Mark the test as unimplemented.
|
|
||||||
*/
|
|
||||||
unimplemented(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestBuilder<S extends SubcaseBatchState, F extends Fixture> {
|
|
||||||
readonly testPath: string[];
|
|
||||||
isUnimplemented: boolean;
|
|
||||||
description: string | undefined;
|
|
||||||
readonly testCreationStack: Error;
|
|
||||||
|
|
||||||
private readonly fixture: FixtureClass;
|
|
||||||
private testFn: TestFn<Fixture, {}> | undefined;
|
|
||||||
private beforeFn: BeforeAllSubcasesFn<SubcaseBatchState, {}> | undefined;
|
|
||||||
private testCases?: ParamsBuilderBase<{}, {}> = undefined;
|
|
||||||
private batchSize: number = 0;
|
|
||||||
|
|
||||||
constructor(testPath: string[], fixture: FixtureClass, testCreationStack: Error) {
|
|
||||||
this.testPath = testPath;
|
|
||||||
this.isUnimplemented = false;
|
|
||||||
this.fixture = fixture;
|
|
||||||
this.testCreationStack = testCreationStack;
|
|
||||||
}
|
|
||||||
|
|
||||||
desc(description: string): this {
|
|
||||||
this.description = description.trim();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
specURL(url: string): this {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAllSubcases(fn: BeforeAllSubcasesFn<SubcaseBatchState, {}>): this {
|
|
||||||
assert(this.beforeFn === undefined);
|
|
||||||
this.beforeFn = fn;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn(fn: TestFn<Fixture, {}>): void {
|
|
||||||
// eslint-disable-next-line no-warning-comments
|
|
||||||
// MAINTENANCE_TODO: add "TODO" if there's no description? (and make sure it only ends up on
|
|
||||||
// actual tests, not on test parents in the tree, which is what happens if you do it here, not
|
|
||||||
// sure why)
|
|
||||||
assert(this.testFn === undefined);
|
|
||||||
this.testFn = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
batch(b: number): this {
|
|
||||||
this.batchSize = b;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
unimplemented(): void {
|
|
||||||
assert(this.testFn === undefined);
|
|
||||||
|
|
||||||
this.description =
|
|
||||||
(this.description ? this.description + '\n\n' : '') + 'TODO: .unimplemented()';
|
|
||||||
this.isUnimplemented = true;
|
|
||||||
|
|
||||||
this.testFn = () => {
|
|
||||||
throw new SkipTestCase('test unimplemented');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
validate(): void {
|
|
||||||
const testPathString = this.testPath.join(kPathSeparator);
|
|
||||||
assert(this.testFn !== undefined, () => {
|
|
||||||
let s = `Test is missing .fn(): ${testPathString}`;
|
|
||||||
if (this.testCreationStack.stack) {
|
|
||||||
s += `\n-> test created at:\n${this.testCreationStack.stack}`;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.testCases === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const seen = new Set<string>();
|
|
||||||
for (const [caseParams, subcases] of builderIterateCasesWithSubcases(this.testCases)) {
|
|
||||||
for (const subcaseParams of subcases ?? [{}]) {
|
|
||||||
const params = mergeParams(caseParams, subcaseParams);
|
|
||||||
assert(this.batchSize === 0 || !('batch__' in params));
|
|
||||||
|
|
||||||
// stringifyPublicParams also checks for invalid params values
|
|
||||||
const testcaseString = stringifyPublicParams(params);
|
|
||||||
|
|
||||||
// A (hopefully) unique representation of a params value.
|
|
||||||
const testcaseStringUnique = stringifyPublicParamsUniquely(params);
|
|
||||||
assert(
|
|
||||||
!seen.has(testcaseStringUnique),
|
|
||||||
`Duplicate public test case params for test ${testPathString}: ${testcaseString}`
|
|
||||||
);
|
|
||||||
seen.add(testcaseStringUnique);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params(
|
|
||||||
cases: ((unit: CaseParamsBuilder<{}>) => ParamsBuilderBase<{}, {}>) | ParamsBuilderBase<{}, {}>
|
|
||||||
): TestBuilder<S, F> {
|
|
||||||
assert(this.testCases === undefined, 'test case is already parameterized');
|
|
||||||
if (cases instanceof Function) {
|
|
||||||
this.testCases = cases(kUnitCaseParamsBuilder);
|
|
||||||
} else {
|
|
||||||
this.testCases = cases;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
paramsSimple(cases: Iterable<{}>): TestBuilder<S, F> {
|
|
||||||
assert(this.testCases === undefined, 'test case is already parameterized');
|
|
||||||
this.testCases = kUnitCaseParamsBuilder.combineWithParams(cases);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
paramsSubcasesOnly(
|
|
||||||
subcases: Iterable<{}> | ((unit: SubcaseParamsBuilder<{}, {}>) => SubcaseParamsBuilder<{}, {}>)
|
|
||||||
): TestBuilder<S, F> {
|
|
||||||
if (subcases instanceof Function) {
|
|
||||||
return this.params(subcases(kUnitCaseParamsBuilder.beginSubcases()));
|
|
||||||
} else {
|
|
||||||
return this.params(kUnitCaseParamsBuilder.beginSubcases().combineWithParams(subcases));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*iterate(): IterableIterator<RunCase> {
|
|
||||||
assert(this.testFn !== undefined, 'No test function (.fn()) for test');
|
|
||||||
this.testCases ??= kUnitCaseParamsBuilder;
|
|
||||||
for (const [caseParams, subcases] of builderIterateCasesWithSubcases(this.testCases)) {
|
|
||||||
if (this.batchSize === 0 || subcases === undefined) {
|
|
||||||
yield new RunCaseSpecific(
|
|
||||||
this.testPath,
|
|
||||||
caseParams,
|
|
||||||
this.isUnimplemented,
|
|
||||||
subcases,
|
|
||||||
this.fixture,
|
|
||||||
this.testFn,
|
|
||||||
this.beforeFn,
|
|
||||||
this.testCreationStack
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const subcaseArray = Array.from(subcases);
|
|
||||||
if (subcaseArray.length <= this.batchSize) {
|
|
||||||
yield new RunCaseSpecific(
|
|
||||||
this.testPath,
|
|
||||||
caseParams,
|
|
||||||
this.isUnimplemented,
|
|
||||||
subcaseArray,
|
|
||||||
this.fixture,
|
|
||||||
this.testFn,
|
|
||||||
this.beforeFn,
|
|
||||||
this.testCreationStack
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
for (let i = 0; i < subcaseArray.length; i = i + this.batchSize) {
|
|
||||||
yield new RunCaseSpecific(
|
|
||||||
this.testPath,
|
|
||||||
{ ...caseParams, batch__: i / this.batchSize },
|
|
||||||
this.isUnimplemented,
|
|
||||||
subcaseArray.slice(i, Math.min(subcaseArray.length, i + this.batchSize)),
|
|
||||||
this.fixture,
|
|
||||||
this.testFn,
|
|
||||||
this.beforeFn,
|
|
||||||
this.testCreationStack
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RunCaseSpecific implements RunCase {
|
|
||||||
readonly id: TestCaseID;
|
|
||||||
readonly isUnimplemented: boolean;
|
|
||||||
|
|
||||||
private readonly params: {};
|
|
||||||
private readonly subcases: Iterable<{}> | undefined;
|
|
||||||
private readonly fixture: FixtureClass;
|
|
||||||
private readonly fn: TestFn<Fixture, {}>;
|
|
||||||
private readonly beforeFn?: BeforeAllSubcasesFn<SubcaseBatchState, {}>;
|
|
||||||
private readonly testCreationStack: Error;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
testPath: string[],
|
|
||||||
params: {},
|
|
||||||
isUnimplemented: boolean,
|
|
||||||
subcases: Iterable<{}> | undefined,
|
|
||||||
fixture: FixtureClass,
|
|
||||||
fn: TestFn<Fixture, {}>,
|
|
||||||
beforeFn: BeforeAllSubcasesFn<SubcaseBatchState, {}> | undefined,
|
|
||||||
testCreationStack: Error
|
|
||||||
) {
|
|
||||||
this.id = { test: testPath, params: extractPublicParams(params) };
|
|
||||||
this.isUnimplemented = isUnimplemented;
|
|
||||||
this.params = params;
|
|
||||||
this.subcases = subcases;
|
|
||||||
this.fixture = fixture;
|
|
||||||
this.fn = fn;
|
|
||||||
this.beforeFn = beforeFn;
|
|
||||||
this.testCreationStack = testCreationStack;
|
|
||||||
}
|
|
||||||
|
|
||||||
async runTest(
|
|
||||||
rec: TestCaseRecorder,
|
|
||||||
sharedState: SubcaseBatchState,
|
|
||||||
params: TestParams,
|
|
||||||
throwSkip: boolean,
|
|
||||||
expectedStatus: Expectation
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
rec.beginSubCase();
|
|
||||||
if (expectedStatus === 'skip') {
|
|
||||||
throw new SkipTestCase('Skipped by expectations');
|
|
||||||
}
|
|
||||||
|
|
||||||
const inst = new this.fixture(sharedState, rec, params);
|
|
||||||
try {
|
|
||||||
await inst.init();
|
|
||||||
await this.fn(inst as Fixture & { params: {} });
|
|
||||||
} finally {
|
|
||||||
// Runs as long as constructor succeeded, even if initialization or the test failed.
|
|
||||||
await inst.finalize();
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
// There was an exception from constructor, init, test, or finalize.
|
|
||||||
// An error from init or test may have been a SkipTestCase.
|
|
||||||
// An error from finalize may have been an eventualAsyncExpectation failure
|
|
||||||
// or unexpected validation/OOM error from the GPUDevice.
|
|
||||||
if (throwSkip && ex instanceof SkipTestCase) {
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
rec.threw(ex);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
rec.endSubCase(expectedStatus);
|
|
||||||
} catch (ex) {
|
|
||||||
assert(ex instanceof UnexpectedPassError);
|
|
||||||
ex.message = `Testcase passed unexpectedly.`;
|
|
||||||
ex.stack = this.testCreationStack.stack;
|
|
||||||
rec.warn(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(
|
|
||||||
rec: TestCaseRecorder,
|
|
||||||
selfQuery: TestQuerySingleCase,
|
|
||||||
expectations: TestQueryWithExpectation[]
|
|
||||||
): Promise<void> {
|
|
||||||
const getExpectedStatus = (selfQueryWithSubParams: TestQuerySingleCase) => {
|
|
||||||
let didSeeFail = false;
|
|
||||||
for (const exp of expectations) {
|
|
||||||
const ordering = compareQueries(exp.query, selfQueryWithSubParams);
|
|
||||||
if (ordering === Ordering.Unordered || ordering === Ordering.StrictSubset) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (exp.expectation) {
|
|
||||||
// Skip takes precedence. If there is any expectation indicating a skip,
|
|
||||||
// signal it immediately.
|
|
||||||
case 'skip':
|
|
||||||
return 'skip';
|
|
||||||
case 'fail':
|
|
||||||
// Otherwise, indicate that we might expect a failure.
|
|
||||||
didSeeFail = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
unreachable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return didSeeFail ? 'fail' : 'pass';
|
|
||||||
};
|
|
||||||
|
|
||||||
const { testHeartbeatCallback, maxSubcasesInFlight } = globalTestConfig;
|
|
||||||
try {
|
|
||||||
rec.start();
|
|
||||||
const sharedState = this.fixture.MakeSharedState(this.params);
|
|
||||||
try {
|
|
||||||
await sharedState.init();
|
|
||||||
if (this.beforeFn) {
|
|
||||||
await this.beforeFn(sharedState);
|
|
||||||
}
|
|
||||||
await sharedState.postInit();
|
|
||||||
testHeartbeatCallback();
|
|
||||||
|
|
||||||
let allPreviousSubcasesFinalizedPromise: Promise<void> = Promise.resolve();
|
|
||||||
if (this.subcases) {
|
|
||||||
let totalCount = 0;
|
|
||||||
let skipCount = 0;
|
|
||||||
|
|
||||||
// If there are too many subcases in flight, starting the next subcase will register
|
|
||||||
// `resolvePromiseBlockingSubcase` and wait until `subcaseFinishedCallback` is called.
|
|
||||||
let subcasesInFlight = 0;
|
|
||||||
let resolvePromiseBlockingSubcase: (() => void) | undefined = undefined;
|
|
||||||
const subcaseFinishedCallback = () => {
|
|
||||||
subcasesInFlight -= 1;
|
|
||||||
// If there is any subcase waiting on a previous subcase to finish,
|
|
||||||
// unblock it now, and clear the resolve callback.
|
|
||||||
if (resolvePromiseBlockingSubcase) {
|
|
||||||
resolvePromiseBlockingSubcase();
|
|
||||||
resolvePromiseBlockingSubcase = undefined;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const subParams of this.subcases) {
|
|
||||||
// Make a recorder that will defer all calls until `allPreviousSubcasesFinalizedPromise`
|
|
||||||
// resolves. Waiting on `allPreviousSubcasesFinalizedPromise` ensures that
|
|
||||||
// logs from all the previous subcases have been flushed before flushing new logs.
|
|
||||||
const subcasePrefix = 'subcase: ' + stringifyPublicParams(subParams);
|
|
||||||
const subRec = new Proxy(rec, {
|
|
||||||
get: (target, k: keyof TestCaseRecorder) => {
|
|
||||||
const prop = TestCaseRecorder.prototype[k];
|
|
||||||
if (typeof prop === 'function') {
|
|
||||||
testHeartbeatCallback();
|
|
||||||
return function (...args: Parameters<typeof prop>) {
|
|
||||||
void allPreviousSubcasesFinalizedPromise.then(() => {
|
|
||||||
// Prepend the subcase name to all error messages.
|
|
||||||
for (const arg of args) {
|
|
||||||
if (arg instanceof Error) {
|
|
||||||
try {
|
|
||||||
arg.message = subcasePrefix + '\n' + arg.message;
|
|
||||||
} catch {
|
|
||||||
// If that fails (e.g. on DOMException), try to put it in the stack:
|
|
||||||
let stack = subcasePrefix;
|
|
||||||
if (arg.stack) stack += '\n' + arg.stack;
|
|
||||||
try {
|
|
||||||
arg.stack = stack;
|
|
||||||
} catch {
|
|
||||||
// If that fails too, just silence it.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const rv = (prop as any).apply(target, args);
|
|
||||||
// Because this proxy executes functions in a deferred manner,
|
|
||||||
// it should never be used for functions that need to return a value.
|
|
||||||
assert(rv === undefined);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return prop;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const params = mergeParams(this.params, subParams);
|
|
||||||
const subcaseQuery = new TestQuerySingleCase(
|
|
||||||
selfQuery.suite,
|
|
||||||
selfQuery.filePathParts,
|
|
||||||
selfQuery.testPathParts,
|
|
||||||
params
|
|
||||||
);
|
|
||||||
|
|
||||||
// Limit the maximum number of subcases in flight.
|
|
||||||
if (subcasesInFlight >= maxSubcasesInFlight) {
|
|
||||||
await new Promise<void>(resolve => {
|
|
||||||
// There should only be one subcase waiting at a time.
|
|
||||||
assert(resolvePromiseBlockingSubcase === undefined);
|
|
||||||
resolvePromiseBlockingSubcase = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
subcasesInFlight += 1;
|
|
||||||
// Runs async without waiting so that subsequent subcases can start.
|
|
||||||
// All finalization steps will be waited on at the end of the testcase.
|
|
||||||
const finalizePromise = this.runTest(
|
|
||||||
subRec,
|
|
||||||
sharedState,
|
|
||||||
params,
|
|
||||||
/* throwSkip */ true,
|
|
||||||
getExpectedStatus(subcaseQuery)
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
subRec.info(new Error('OK'));
|
|
||||||
})
|
|
||||||
.catch(ex => {
|
|
||||||
if (ex instanceof SkipTestCase) {
|
|
||||||
// Convert SkipTestCase to info messages
|
|
||||||
ex.message = 'subcase skipped: ' + ex.message;
|
|
||||||
subRec.info(ex);
|
|
||||||
++skipCount;
|
|
||||||
} else {
|
|
||||||
// Since we are catching all error inside runTest(), this should never happen
|
|
||||||
subRec.threw(ex);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(subcaseFinishedCallback);
|
|
||||||
|
|
||||||
allPreviousSubcasesFinalizedPromise = allPreviousSubcasesFinalizedPromise.then(
|
|
||||||
() => finalizePromise
|
|
||||||
);
|
|
||||||
++totalCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all subcases to finalize and report their results.
|
|
||||||
await allPreviousSubcasesFinalizedPromise;
|
|
||||||
|
|
||||||
if (skipCount === totalCount) {
|
|
||||||
rec.skipped(new SkipTestCase('all subcases were skipped'));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.runTest(
|
|
||||||
rec,
|
|
||||||
sharedState,
|
|
||||||
this.params,
|
|
||||||
/* throwSkip */ false,
|
|
||||||
getExpectedStatus(selfQuery)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
testHeartbeatCallback();
|
|
||||||
// Runs as long as the shared state constructor succeeded, even if initialization or a test failed.
|
|
||||||
await sharedState.finalize();
|
|
||||||
testHeartbeatCallback();
|
|
||||||
}
|
|
||||||
} catch (ex) {
|
|
||||||
// There was an exception from sharedState/fixture constructor, init, beforeFn, or test.
|
|
||||||
// An error from beforeFn may have been SkipTestCase.
|
|
||||||
// An error from finalize may have been an eventualAsyncExpectation failure
|
|
||||||
// or unexpected validation/OOM error from the GPUDevice.
|
|
||||||
rec.threw(ex);
|
|
||||||
} finally {
|
|
||||||
rec.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// A listing of all specs within a single suite. This is the (awaited) type of
|
|
||||||
// `groups` in '{cts,unittests}/listing.ts' and `listing` in the auto-generated
|
|
||||||
// 'out/{cts,unittests}/listing.js' files (see tools/gen_listings).
|
|
||||||
export type TestSuiteListing = TestSuiteListingEntry[];
|
|
||||||
|
|
||||||
export type TestSuiteListingEntry = TestSuiteListingEntrySpec | TestSuiteListingEntryReadme;
|
|
||||||
|
|
||||||
interface TestSuiteListingEntrySpec {
|
|
||||||
readonly file: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestSuiteListingEntryReadme {
|
|
||||||
readonly file: string[];
|
|
||||||
readonly readme: string;
|
|
||||||
}
|
|
||||||
|
|
@ -1,575 +0,0 @@
|
||||||
import { RunCase, RunFn } from '../internal/test_group.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
import { TestFileLoader } from './file_loader.js';
|
|
||||||
import { TestParamsRW } from './params_utils.js';
|
|
||||||
import { compareQueries, Ordering } from './query/compare.js';
|
|
||||||
import {
|
|
||||||
TestQuery,
|
|
||||||
TestQueryMultiCase,
|
|
||||||
TestQuerySingleCase,
|
|
||||||
TestQueryMultiFile,
|
|
||||||
TestQueryMultiTest,
|
|
||||||
} from './query/query.js';
|
|
||||||
import { kBigSeparator, kWildcard, kPathSeparator, kParamSeparator } from './query/separators.js';
|
|
||||||
import { stringifySingleParam } from './query/stringify_params.js';
|
|
||||||
import { StacklessError } from './util.js';
|
|
||||||
|
|
||||||
// `loadTreeForQuery()` loads a TestTree for a given queryToLoad.
|
|
||||||
// The resulting tree is a linked-list all the way from `suite:*` to queryToLoad,
|
|
||||||
// and under queryToLoad is a tree containing every case matched by queryToLoad.
|
|
||||||
//
|
|
||||||
// `subqueriesToExpand` influences the `collapsible` flag on nodes in the resulting tree.
|
|
||||||
// A node is considered "collapsible" if none of the subqueriesToExpand is a StrictSubset
|
|
||||||
// of that node.
|
|
||||||
//
|
|
||||||
// In WebKit/Blink-style web_tests, an expectation file marks individual cts.https.html "variants
|
|
||||||
// as "Failure", "Crash", etc. By passing in the list of expectations as the subqueriesToExpand,
|
|
||||||
// we can programmatically subdivide the cts.https.html "variants" list to be able to implement
|
|
||||||
// arbitrarily-fine suppressions (instead of having to suppress entire test files, which would
|
|
||||||
// lose a lot of coverage).
|
|
||||||
//
|
|
||||||
// `iterateCollapsedNodes()` produces the list of queries for the variants list.
|
|
||||||
//
|
|
||||||
// Though somewhat complicated, this system has important benefits:
|
|
||||||
// - Avoids having to suppress entire test files, which would cause large test coverage loss.
|
|
||||||
// - Minimizes the number of page loads needed for fine-grained suppressions.
|
|
||||||
// (In the naive case, we could do one page load per test case - but the test suite would
|
|
||||||
// take impossibly long to run.)
|
|
||||||
// - Enables developers to put any number of tests in one file as appropriate, without worrying
|
|
||||||
// about expectation granularity.
|
|
||||||
|
|
||||||
interface TestTreeNodeBase<T extends TestQuery> {
|
|
||||||
readonly query: T;
|
|
||||||
/**
|
|
||||||
* Readable "relative" name for display in standalone runner.
|
|
||||||
* Not always the exact relative name, because sometimes there isn't
|
|
||||||
* one (e.g. s:f:* relative to s:f,*), but something that is readable.
|
|
||||||
*/
|
|
||||||
readonly readableRelativeName: string;
|
|
||||||
subtreeCounts?: { tests: number; nodesWithTODO: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestSubtree<T extends TestQuery = TestQuery> extends TestTreeNodeBase<T> {
|
|
||||||
readonly children: Map<string, TestTreeNode>;
|
|
||||||
readonly collapsible: boolean;
|
|
||||||
description?: string;
|
|
||||||
readonly testCreationStack?: Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TestTreeLeaf extends TestTreeNodeBase<TestQuerySingleCase> {
|
|
||||||
readonly run: RunFn;
|
|
||||||
readonly isUnimplemented?: boolean;
|
|
||||||
subtreeCounts?: undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TestTreeNode = TestSubtree | TestTreeLeaf;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When iterating through "collapsed" tree nodes, indicates how many "query levels" to traverse
|
|
||||||
* through before starting to collapse nodes.
|
|
||||||
*
|
|
||||||
* Corresponds with TestQueryLevel, but excludes 4 (SingleCase):
|
|
||||||
* - 1 = MultiFile. Expands so every file is in the collapsed tree.
|
|
||||||
* - 2 = MultiTest. Expands so every test is in the collapsed tree.
|
|
||||||
* - 3 = MultiCase. Expands so every case is in the collapsed tree (i.e. collapsing disabled).
|
|
||||||
*/
|
|
||||||
export type ExpandThroughLevel = 1 | 2 | 3;
|
|
||||||
|
|
||||||
export class TestTree {
|
|
||||||
/**
|
|
||||||
* The `queryToLoad` that this test tree was created for.
|
|
||||||
* Test trees are always rooted at `suite:*`, but they only contain nodes that fit
|
|
||||||
* within `forQuery`.
|
|
||||||
*
|
|
||||||
* This is used for `iterateCollapsedNodes` which only starts collapsing at the next
|
|
||||||
* `TestQueryLevel` after `forQuery`.
|
|
||||||
*/
|
|
||||||
readonly forQuery: TestQuery;
|
|
||||||
readonly root: TestSubtree;
|
|
||||||
|
|
||||||
constructor(forQuery: TestQuery, root: TestSubtree) {
|
|
||||||
this.forQuery = forQuery;
|
|
||||||
TestTree.propagateCounts(root);
|
|
||||||
this.root = root;
|
|
||||||
assert(
|
|
||||||
root.query.level === 1 && root.query.depthInLevel === 0,
|
|
||||||
'TestTree root must be the root (suite:*)'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate through the leaves of a version of the tree which has been pruned to exclude
|
|
||||||
* subtrees which:
|
|
||||||
* - are at a deeper `TestQueryLevel` than `this.forQuery`, and
|
|
||||||
* - were not a `Ordering.StrictSubset` of any of the `subqueriesToExpand` during tree creation.
|
|
||||||
*/
|
|
||||||
iterateCollapsedNodes({
|
|
||||||
includeIntermediateNodes = false,
|
|
||||||
includeEmptySubtrees = false,
|
|
||||||
alwaysExpandThroughLevel,
|
|
||||||
}: {
|
|
||||||
/** Whether to include intermediate tree nodes or only collapsed-leaves. */
|
|
||||||
includeIntermediateNodes?: boolean;
|
|
||||||
/** Whether to include collapsed-leaves with no children. */
|
|
||||||
includeEmptySubtrees?: boolean;
|
|
||||||
/** Never collapse nodes up through this level. */
|
|
||||||
alwaysExpandThroughLevel: ExpandThroughLevel;
|
|
||||||
}): IterableIterator<Readonly<TestTreeNode>> {
|
|
||||||
const expandThroughLevel = Math.max(this.forQuery.level, alwaysExpandThroughLevel);
|
|
||||||
return TestTree.iterateSubtreeNodes(this.root, {
|
|
||||||
includeIntermediateNodes,
|
|
||||||
includeEmptySubtrees,
|
|
||||||
expandThroughLevel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
iterateLeaves(): IterableIterator<Readonly<TestTreeLeaf>> {
|
|
||||||
return TestTree.iterateSubtreeLeaves(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dissolve nodes which have only one child, e.g.:
|
|
||||||
* a,* { a,b,* { a,b:* { ... } } }
|
|
||||||
* collapses down into:
|
|
||||||
* a,* { a,b:* { ... } }
|
|
||||||
* which is less needlessly verbose when displaying the tree in the standalone runner.
|
|
||||||
*/
|
|
||||||
dissolveSingleChildTrees(): void {
|
|
||||||
const newRoot = dissolveSingleChildTrees(this.root);
|
|
||||||
assert(newRoot === this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string {
|
|
||||||
return TestTree.subtreeToString('(root)', this.root, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
static *iterateSubtreeNodes(
|
|
||||||
subtree: TestSubtree,
|
|
||||||
opts: {
|
|
||||||
includeIntermediateNodes: boolean;
|
|
||||||
includeEmptySubtrees: boolean;
|
|
||||||
expandThroughLevel: number;
|
|
||||||
}
|
|
||||||
): IterableIterator<TestTreeNode> {
|
|
||||||
if (opts.includeIntermediateNodes) {
|
|
||||||
yield subtree;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [, child] of subtree.children) {
|
|
||||||
if ('children' in child) {
|
|
||||||
// Is a subtree
|
|
||||||
const collapsible = child.collapsible && child.query.level > opts.expandThroughLevel;
|
|
||||||
if (child.children.size > 0 && !collapsible) {
|
|
||||||
yield* TestTree.iterateSubtreeNodes(child, opts);
|
|
||||||
} else if (child.children.size > 0 || opts.includeEmptySubtrees) {
|
|
||||||
// Don't yield empty subtrees (e.g. files with no tests) unless includeEmptySubtrees
|
|
||||||
yield child;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Is a leaf
|
|
||||||
yield child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static *iterateSubtreeLeaves(subtree: TestSubtree): IterableIterator<TestTreeLeaf> {
|
|
||||||
for (const [, child] of subtree.children) {
|
|
||||||
if ('children' in child) {
|
|
||||||
yield* TestTree.iterateSubtreeLeaves(child);
|
|
||||||
} else {
|
|
||||||
yield child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Propagate the subtreeTODOs/subtreeTests state upward from leaves to parent nodes. */
|
|
||||||
static propagateCounts(subtree: TestSubtree): { tests: number; nodesWithTODO: number } {
|
|
||||||
subtree.subtreeCounts ??= { tests: 0, nodesWithTODO: 0 };
|
|
||||||
for (const [, child] of subtree.children) {
|
|
||||||
if ('children' in child) {
|
|
||||||
const counts = TestTree.propagateCounts(child);
|
|
||||||
subtree.subtreeCounts.tests += counts.tests;
|
|
||||||
subtree.subtreeCounts.nodesWithTODO += counts.nodesWithTODO;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subtree.subtreeCounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Displays counts in the format `(Nodes with TODOs) / (Total test count)`. */
|
|
||||||
static countsToString(tree: TestTreeNode): string {
|
|
||||||
if (tree.subtreeCounts) {
|
|
||||||
return `${tree.subtreeCounts.nodesWithTODO} / ${tree.subtreeCounts.tests}`;
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static subtreeToString(name: string, tree: TestTreeNode, indent: string): string {
|
|
||||||
const collapsible = 'run' in tree ? '>' : tree.collapsible ? '+' : '-';
|
|
||||||
let s =
|
|
||||||
indent +
|
|
||||||
`${collapsible} ${TestTree.countsToString(tree)} ${JSON.stringify(name)} => ${tree.query}`;
|
|
||||||
if ('children' in tree) {
|
|
||||||
if (tree.description !== undefined) {
|
|
||||||
s += `\n${indent} | ${JSON.stringify(tree.description)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, child] of tree.children) {
|
|
||||||
s += '\n' + TestTree.subtreeToString(name, child, indent + ' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAINTENANCE_TODO: Consider having subqueriesToExpand actually impact the depth-order of params
|
|
||||||
// in the tree.
|
|
||||||
export async function loadTreeForQuery(
|
|
||||||
loader: TestFileLoader,
|
|
||||||
queryToLoad: TestQuery,
|
|
||||||
subqueriesToExpand: TestQuery[]
|
|
||||||
): Promise<TestTree> {
|
|
||||||
const suite = queryToLoad.suite;
|
|
||||||
const specs = await loader.listing(suite);
|
|
||||||
|
|
||||||
const subqueriesToExpandEntries = Array.from(subqueriesToExpand.entries());
|
|
||||||
const seenSubqueriesToExpand: boolean[] = new Array(subqueriesToExpand.length);
|
|
||||||
seenSubqueriesToExpand.fill(false);
|
|
||||||
|
|
||||||
const isCollapsible = (subquery: TestQuery) =>
|
|
||||||
subqueriesToExpandEntries.every(([i, toExpand]) => {
|
|
||||||
const ordering = compareQueries(toExpand, subquery);
|
|
||||||
|
|
||||||
// If toExpand == subquery, no expansion is needed (but it's still "seen").
|
|
||||||
if (ordering === Ordering.Equal) seenSubqueriesToExpand[i] = true;
|
|
||||||
return ordering !== Ordering.StrictSubset;
|
|
||||||
});
|
|
||||||
|
|
||||||
// L0 = suite-level, e.g. suite:*
|
|
||||||
// L1 = file-level, e.g. suite:a,b:*
|
|
||||||
// L2 = test-level, e.g. suite:a,b:c,d:*
|
|
||||||
// L3 = case-level, e.g. suite:a,b:c,d:
|
|
||||||
let foundCase = false;
|
|
||||||
// L0 is suite:*
|
|
||||||
const subtreeL0 = makeTreeForSuite(suite, isCollapsible);
|
|
||||||
for (const entry of specs) {
|
|
||||||
if (entry.file.length === 0 && 'readme' in entry) {
|
|
||||||
// Suite-level readme.
|
|
||||||
setSubtreeDescriptionAndCountTODOs(subtreeL0, entry.readme);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const queryL1 = new TestQueryMultiFile(suite, entry.file);
|
|
||||||
const orderingL1 = compareQueries(queryL1, queryToLoad);
|
|
||||||
if (orderingL1 === Ordering.Unordered) {
|
|
||||||
// File path is not matched by this query.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('readme' in entry) {
|
|
||||||
// Entry is a README that is an ancestor or descendant of the query.
|
|
||||||
// (It's included for display in the standalone runner.)
|
|
||||||
|
|
||||||
// readmeSubtree is suite:a,b,*
|
|
||||||
// (This is always going to dedup with a file path, if there are any test spec files under
|
|
||||||
// the directory that has the README).
|
|
||||||
const readmeSubtree: TestSubtree<TestQueryMultiFile> = addSubtreeForDirPath(
|
|
||||||
subtreeL0,
|
|
||||||
entry.file,
|
|
||||||
isCollapsible
|
|
||||||
);
|
|
||||||
setSubtreeDescriptionAndCountTODOs(readmeSubtree, entry.readme);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Entry is a spec file.
|
|
||||||
|
|
||||||
const spec = await loader.importSpecFile(queryToLoad.suite, entry.file);
|
|
||||||
// subtreeL1 is suite:a,b:*
|
|
||||||
const subtreeL1: TestSubtree<TestQueryMultiTest> = addSubtreeForFilePath(
|
|
||||||
subtreeL0,
|
|
||||||
entry.file,
|
|
||||||
isCollapsible
|
|
||||||
);
|
|
||||||
setSubtreeDescriptionAndCountTODOs(subtreeL1, spec.description);
|
|
||||||
|
|
||||||
let groupHasTests = false;
|
|
||||||
for (const t of spec.g.iterate()) {
|
|
||||||
groupHasTests = true;
|
|
||||||
{
|
|
||||||
const queryL2 = new TestQueryMultiCase(suite, entry.file, t.testPath, {});
|
|
||||||
const orderingL2 = compareQueries(queryL2, queryToLoad);
|
|
||||||
if (orderingL2 === Ordering.Unordered) {
|
|
||||||
// Test path is not matched by this query.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// subtreeL2 is suite:a,b:c,d:*
|
|
||||||
const subtreeL2: TestSubtree<TestQueryMultiCase> = addSubtreeForTestPath(
|
|
||||||
subtreeL1,
|
|
||||||
t.testPath,
|
|
||||||
t.testCreationStack,
|
|
||||||
isCollapsible
|
|
||||||
);
|
|
||||||
// This is 1 test. Set tests=1 then count TODOs.
|
|
||||||
subtreeL2.subtreeCounts ??= { tests: 1, nodesWithTODO: 0 };
|
|
||||||
if (t.description) setSubtreeDescriptionAndCountTODOs(subtreeL2, t.description);
|
|
||||||
|
|
||||||
// MAINTENANCE_TODO: If tree generation gets too slow, avoid actually iterating the cases in a
|
|
||||||
// file if there's no need to (based on the subqueriesToExpand).
|
|
||||||
for (const c of t.iterate()) {
|
|
||||||
{
|
|
||||||
const queryL3 = new TestQuerySingleCase(suite, entry.file, c.id.test, c.id.params);
|
|
||||||
const orderingL3 = compareQueries(queryL3, queryToLoad);
|
|
||||||
if (orderingL3 === Ordering.Unordered || orderingL3 === Ordering.StrictSuperset) {
|
|
||||||
// Case is not matched by this query.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leaf for case is suite:a,b:c,d:x=1;y=2
|
|
||||||
addLeafForCase(subtreeL2, c, isCollapsible);
|
|
||||||
|
|
||||||
foundCase = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!groupHasTests && !subtreeL1.subtreeCounts) {
|
|
||||||
throw new StacklessError(
|
|
||||||
`${subtreeL1.query} has no tests - it must have "TODO" in its description`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [i, sq] of subqueriesToExpandEntries) {
|
|
||||||
const subquerySeen = seenSubqueriesToExpand[i];
|
|
||||||
if (!subquerySeen) {
|
|
||||||
throw new StacklessError(
|
|
||||||
`subqueriesToExpand entry did not match anything \
|
|
||||||
(could be wrong, or could be redundant with a previous subquery):\n ${sq.toString()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(foundCase, `Query \`${queryToLoad.toString()}\` does not match any cases`);
|
|
||||||
|
|
||||||
return new TestTree(queryToLoad, subtreeL0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSubtreeDescriptionAndCountTODOs(
|
|
||||||
subtree: TestSubtree<TestQueryMultiFile>,
|
|
||||||
description: string
|
|
||||||
) {
|
|
||||||
assert(subtree.description === undefined);
|
|
||||||
subtree.description = description.trim();
|
|
||||||
subtree.subtreeCounts ??= { tests: 0, nodesWithTODO: 0 };
|
|
||||||
if (subtree.description.indexOf('TODO') !== -1) {
|
|
||||||
subtree.subtreeCounts.nodesWithTODO++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeTreeForSuite(
|
|
||||||
suite: string,
|
|
||||||
isCollapsible: (sq: TestQuery) => boolean
|
|
||||||
): TestSubtree<TestQueryMultiFile> {
|
|
||||||
const query = new TestQueryMultiFile(suite, []);
|
|
||||||
return {
|
|
||||||
readableRelativeName: suite + kBigSeparator,
|
|
||||||
query,
|
|
||||||
children: new Map(),
|
|
||||||
collapsible: isCollapsible(query),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSubtreeForDirPath(
|
|
||||||
tree: TestSubtree<TestQueryMultiFile>,
|
|
||||||
file: string[],
|
|
||||||
isCollapsible: (sq: TestQuery) => boolean
|
|
||||||
): TestSubtree<TestQueryMultiFile> {
|
|
||||||
const subqueryFile: string[] = [];
|
|
||||||
// To start, tree is suite:*
|
|
||||||
// This loop goes from that -> suite:a,* -> suite:a,b,*
|
|
||||||
for (const part of file) {
|
|
||||||
subqueryFile.push(part);
|
|
||||||
tree = getOrInsertSubtree(part, tree, () => {
|
|
||||||
const query = new TestQueryMultiFile(tree.query.suite, subqueryFile);
|
|
||||||
return {
|
|
||||||
readableRelativeName: part + kPathSeparator + kWildcard,
|
|
||||||
query,
|
|
||||||
collapsible: isCollapsible(query),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return tree;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSubtreeForFilePath(
|
|
||||||
tree: TestSubtree<TestQueryMultiFile>,
|
|
||||||
file: string[],
|
|
||||||
isCollapsible: (sq: TestQuery) => boolean
|
|
||||||
): TestSubtree<TestQueryMultiTest> {
|
|
||||||
// To start, tree is suite:*
|
|
||||||
// This goes from that -> suite:a,* -> suite:a,b,*
|
|
||||||
tree = addSubtreeForDirPath(tree, file, isCollapsible);
|
|
||||||
// This goes from that -> suite:a,b:*
|
|
||||||
const subtree = getOrInsertSubtree('', tree, () => {
|
|
||||||
const query = new TestQueryMultiTest(tree.query.suite, tree.query.filePathParts, []);
|
|
||||||
assert(file.length > 0, 'file path is empty');
|
|
||||||
return {
|
|
||||||
readableRelativeName: file[file.length - 1] + kBigSeparator + kWildcard,
|
|
||||||
query,
|
|
||||||
collapsible: isCollapsible(query),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return subtree;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSubtreeForTestPath(
|
|
||||||
tree: TestSubtree<TestQueryMultiTest>,
|
|
||||||
test: readonly string[],
|
|
||||||
testCreationStack: Error,
|
|
||||||
isCollapsible: (sq: TestQuery) => boolean
|
|
||||||
): TestSubtree<TestQueryMultiCase> {
|
|
||||||
const subqueryTest: string[] = [];
|
|
||||||
// To start, tree is suite:a,b:*
|
|
||||||
// This loop goes from that -> suite:a,b:c,* -> suite:a,b:c,d,*
|
|
||||||
for (const part of test) {
|
|
||||||
subqueryTest.push(part);
|
|
||||||
tree = getOrInsertSubtree(part, tree, () => {
|
|
||||||
const query = new TestQueryMultiTest(
|
|
||||||
tree.query.suite,
|
|
||||||
tree.query.filePathParts,
|
|
||||||
subqueryTest
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
readableRelativeName: part + kPathSeparator + kWildcard,
|
|
||||||
query,
|
|
||||||
collapsible: isCollapsible(query),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// This goes from that -> suite:a,b:c,d:*
|
|
||||||
return getOrInsertSubtree('', tree, () => {
|
|
||||||
const query = new TestQueryMultiCase(
|
|
||||||
tree.query.suite,
|
|
||||||
tree.query.filePathParts,
|
|
||||||
subqueryTest,
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
assert(subqueryTest.length > 0, 'subqueryTest is empty');
|
|
||||||
return {
|
|
||||||
readableRelativeName: subqueryTest[subqueryTest.length - 1] + kBigSeparator + kWildcard,
|
|
||||||
kWildcard,
|
|
||||||
query,
|
|
||||||
testCreationStack,
|
|
||||||
collapsible: isCollapsible(query),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addLeafForCase(
|
|
||||||
tree: TestSubtree<TestQueryMultiTest>,
|
|
||||||
t: RunCase,
|
|
||||||
checkCollapsible: (sq: TestQuery) => boolean
|
|
||||||
): void {
|
|
||||||
const query = tree.query;
|
|
||||||
let name: string = '';
|
|
||||||
const subqueryParams: TestParamsRW = {};
|
|
||||||
|
|
||||||
// To start, tree is suite:a,b:c,d:*
|
|
||||||
// This loop goes from that -> suite:a,b:c,d:x=1;* -> suite:a,b:c,d:x=1;y=2;*
|
|
||||||
for (const [k, v] of Object.entries(t.id.params)) {
|
|
||||||
name = stringifySingleParam(k, v);
|
|
||||||
subqueryParams[k] = v;
|
|
||||||
|
|
||||||
tree = getOrInsertSubtree(name, tree, () => {
|
|
||||||
const subquery = new TestQueryMultiCase(
|
|
||||||
query.suite,
|
|
||||||
query.filePathParts,
|
|
||||||
query.testPathParts,
|
|
||||||
subqueryParams
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
readableRelativeName: name + kParamSeparator + kWildcard,
|
|
||||||
query: subquery,
|
|
||||||
collapsible: checkCollapsible(subquery),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This goes from that -> suite:a,b:c,d:x=1;y=2
|
|
||||||
const subquery = new TestQuerySingleCase(
|
|
||||||
query.suite,
|
|
||||||
query.filePathParts,
|
|
||||||
query.testPathParts,
|
|
||||||
subqueryParams
|
|
||||||
);
|
|
||||||
checkCollapsible(subquery); // mark seenSubqueriesToExpand
|
|
||||||
insertLeaf(tree, subquery, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOrInsertSubtree<T extends TestQuery>(
|
|
||||||
key: string,
|
|
||||||
parent: TestSubtree,
|
|
||||||
createSubtree: () => Omit<TestSubtree<T>, 'children'>
|
|
||||||
): TestSubtree<T> {
|
|
||||||
let v: TestSubtree<T>;
|
|
||||||
const child = parent.children.get(key);
|
|
||||||
if (child !== undefined) {
|
|
||||||
assert('children' in child); // Make sure cached subtree is not actually a leaf
|
|
||||||
v = child as TestSubtree<T>;
|
|
||||||
} else {
|
|
||||||
v = { ...createSubtree(), children: new Map() };
|
|
||||||
parent.children.set(key, v);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
function insertLeaf(parent: TestSubtree, query: TestQuerySingleCase, t: RunCase) {
|
|
||||||
const leaf: TestTreeLeaf = {
|
|
||||||
readableRelativeName: readableNameForCase(query),
|
|
||||||
query,
|
|
||||||
run: (rec, expectations) => t.run(rec, query, expectations || []),
|
|
||||||
isUnimplemented: t.isUnimplemented,
|
|
||||||
};
|
|
||||||
|
|
||||||
// This is a leaf (e.g. s:f:t:x=1;* -> s:f:t:x=1). The key is always ''.
|
|
||||||
const key = '';
|
|
||||||
assert(!parent.children.has(key), `Duplicate testcase: ${query}`);
|
|
||||||
parent.children.set(key, leaf);
|
|
||||||
}
|
|
||||||
|
|
||||||
function dissolveSingleChildTrees(tree: TestTreeNode): TestTreeNode {
|
|
||||||
if ('children' in tree) {
|
|
||||||
const shouldDissolveThisTree =
|
|
||||||
tree.children.size === 1 && tree.query.depthInLevel !== 0 && tree.description === undefined;
|
|
||||||
if (shouldDissolveThisTree) {
|
|
||||||
// Loops exactly once
|
|
||||||
for (const [, child] of tree.children) {
|
|
||||||
// Recurse on child
|
|
||||||
return dissolveSingleChildTrees(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [k, child] of tree.children) {
|
|
||||||
// Recurse on each child
|
|
||||||
const newChild = dissolveSingleChildTrees(child);
|
|
||||||
if (newChild !== child) {
|
|
||||||
tree.children.set(k, newChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tree;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Generate a readable relative name for a case (used in standalone). */
|
|
||||||
function readableNameForCase(query: TestQuerySingleCase): string {
|
|
||||||
const paramsKeys = Object.keys(query.params);
|
|
||||||
if (paramsKeys.length === 0) {
|
|
||||||
return query.testPathParts[query.testPathParts.length - 1] + kBigSeparator;
|
|
||||||
} else {
|
|
||||||
const lastKey = paramsKeys[paramsKeys.length - 1];
|
|
||||||
return stringifySingleParam(lastKey, query.params[lastKey]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/**
|
|
||||||
* Error without a stack, which can be used to fatally exit from `tool/` scripts with a
|
|
||||||
* user-friendly message (and no confusing stack).
|
|
||||||
*/
|
|
||||||
export class StacklessError extends Error {
|
|
||||||
constructor(message: string) {
|
|
||||||
super(message);
|
|
||||||
this.stack = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export const version = 'unknown';
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
/* eslint no-console: "off" */
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
import { dataCache } from '../framework/data_cache.js';
|
|
||||||
import { globalTestConfig } from '../framework/test_config.js';
|
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { prettyPrintLog } from '../internal/logging/log_message.js';
|
|
||||||
import { Logger } from '../internal/logging/logger.js';
|
|
||||||
import { LiveTestCaseResult } from '../internal/logging/result.js';
|
|
||||||
import { parseQuery } from '../internal/query/parseQuery.js';
|
|
||||||
import { parseExpectationsForTestQuery } from '../internal/query/query.js';
|
|
||||||
import { Colors } from '../util/colors.js';
|
|
||||||
import { setGPUProvider } from '../util/navigator_gpu.js';
|
|
||||||
import { assert, unreachable } from '../util/util.js';
|
|
||||||
|
|
||||||
import sys from './helper/sys.js';
|
|
||||||
|
|
||||||
function usage(rc: number): never {
|
|
||||||
console.log(`Usage:
|
|
||||||
tools/run_${sys.type} [OPTIONS...] QUERIES...
|
|
||||||
tools/run_${sys.type} 'unittests:*' 'webgpu:buffers,*'
|
|
||||||
Options:
|
|
||||||
--colors Enable ANSI colors in output.
|
|
||||||
--coverage Emit coverage data.
|
|
||||||
--verbose Print result/log of every test as it runs.
|
|
||||||
--list Print all testcase names that match the given query and exit.
|
|
||||||
--debug Include debug messages in logging.
|
|
||||||
--print-json Print the complete result JSON in the output.
|
|
||||||
--expectations Path to expectations file.
|
|
||||||
--gpu-provider Path to node module that provides the GPU implementation.
|
|
||||||
--gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value>
|
|
||||||
--unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests
|
|
||||||
--quiet Suppress summary information in output
|
|
||||||
`);
|
|
||||||
return sys.exit(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The interface that exposes creation of the GPU, and optional interface to code coverage.
|
|
||||||
interface GPUProviderModule {
|
|
||||||
// @returns a GPU with the given flags
|
|
||||||
create(flags: string[]): GPU;
|
|
||||||
// An optional interface to a CodeCoverageProvider
|
|
||||||
coverage?: CodeCoverageProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CodeCoverageProvider {
|
|
||||||
// Starts collecting code coverage
|
|
||||||
begin(): void;
|
|
||||||
// Ends collecting of code coverage, returning the coverage data.
|
|
||||||
// This data is opaque (implementation defined).
|
|
||||||
end(): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type listModes = 'none' | 'cases' | 'unimplemented';
|
|
||||||
|
|
||||||
Colors.enabled = false;
|
|
||||||
|
|
||||||
let verbose = false;
|
|
||||||
let emitCoverage = false;
|
|
||||||
let listMode: listModes = 'none';
|
|
||||||
let debug = false;
|
|
||||||
let printJSON = false;
|
|
||||||
let quiet = false;
|
|
||||||
let loadWebGPUExpectations: Promise<unknown> | undefined = undefined;
|
|
||||||
let gpuProviderModule: GPUProviderModule | undefined = undefined;
|
|
||||||
let dataPath: string | undefined = undefined;
|
|
||||||
|
|
||||||
const queries: string[] = [];
|
|
||||||
const gpuProviderFlags: string[] = [];
|
|
||||||
for (let i = 0; i < sys.args.length; ++i) {
|
|
||||||
const a = sys.args[i];
|
|
||||||
if (a.startsWith('-')) {
|
|
||||||
if (a === '--colors') {
|
|
||||||
Colors.enabled = true;
|
|
||||||
} else if (a === '--coverage') {
|
|
||||||
emitCoverage = true;
|
|
||||||
} else if (a === '--verbose') {
|
|
||||||
verbose = true;
|
|
||||||
} else if (a === '--list') {
|
|
||||||
listMode = 'cases';
|
|
||||||
} else if (a === '--list-unimplemented') {
|
|
||||||
listMode = 'unimplemented';
|
|
||||||
} else if (a === '--debug') {
|
|
||||||
debug = true;
|
|
||||||
} else if (a === '--data') {
|
|
||||||
dataPath = sys.args[++i];
|
|
||||||
} else if (a === '--print-json') {
|
|
||||||
printJSON = true;
|
|
||||||
} else if (a === '--expectations') {
|
|
||||||
const expectationsFile = new URL(sys.args[++i], `file://${sys.cwd()}`).pathname;
|
|
||||||
loadWebGPUExpectations = import(expectationsFile).then(m => m.expectations);
|
|
||||||
} else if (a === '--gpu-provider') {
|
|
||||||
const modulePath = sys.args[++i];
|
|
||||||
gpuProviderModule = require(modulePath);
|
|
||||||
} else if (a === '--gpu-provider-flag') {
|
|
||||||
gpuProviderFlags.push(sys.args[++i]);
|
|
||||||
} else if (a === '--quiet') {
|
|
||||||
quiet = true;
|
|
||||||
} else if (a === '--unroll-const-eval-loops') {
|
|
||||||
globalTestConfig.unrollConstEvalLoops = true;
|
|
||||||
} else {
|
|
||||||
console.log('unrecognized flag: ', a);
|
|
||||||
usage(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
queries.push(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let codeCoverage: CodeCoverageProvider | undefined = undefined;
|
|
||||||
|
|
||||||
if (gpuProviderModule) {
|
|
||||||
setGPUProvider(() => gpuProviderModule!.create(gpuProviderFlags));
|
|
||||||
if (emitCoverage) {
|
|
||||||
codeCoverage = gpuProviderModule.coverage;
|
|
||||||
if (codeCoverage === undefined) {
|
|
||||||
console.error(
|
|
||||||
`--coverage specified, but the GPUProviderModule does not support code coverage.
|
|
||||||
Did you remember to build with code coverage instrumentation enabled?`
|
|
||||||
);
|
|
||||||
sys.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataPath !== undefined) {
|
|
||||||
dataCache.setStore({
|
|
||||||
load: (path: string) => {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
fs.readFile(`${dataPath}/${path}`, 'utf8', (err, data) => {
|
|
||||||
if (err !== null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (verbose) {
|
|
||||||
dataCache.setDebugLogger(console.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queries.length === 0) {
|
|
||||||
console.log('no queries specified');
|
|
||||||
usage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
assert(queries.length === 1, 'currently, there must be exactly one query on the cmd line');
|
|
||||||
const filterQuery = parseQuery(queries[0]);
|
|
||||||
const testcases = await loader.loadCases(filterQuery);
|
|
||||||
const expectations = parseExpectationsForTestQuery(
|
|
||||||
await (loadWebGPUExpectations ?? []),
|
|
||||||
filterQuery
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.globalDebugMode = debug;
|
|
||||||
const log = new Logger();
|
|
||||||
|
|
||||||
const failed: Array<[string, LiveTestCaseResult]> = [];
|
|
||||||
const warned: Array<[string, LiveTestCaseResult]> = [];
|
|
||||||
const skipped: Array<[string, LiveTestCaseResult]> = [];
|
|
||||||
|
|
||||||
let total = 0;
|
|
||||||
|
|
||||||
if (codeCoverage !== undefined) {
|
|
||||||
codeCoverage.begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const testcase of testcases) {
|
|
||||||
const name = testcase.query.toString();
|
|
||||||
switch (listMode) {
|
|
||||||
case 'cases':
|
|
||||||
console.log(name);
|
|
||||||
continue;
|
|
||||||
case 'unimplemented':
|
|
||||||
if (testcase.isUnimplemented) {
|
|
||||||
console.log(name);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [rec, res] = log.record(name);
|
|
||||||
await testcase.run(rec, expectations);
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
printResults([[name, res]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
total++;
|
|
||||||
switch (res.status) {
|
|
||||||
case 'pass':
|
|
||||||
break;
|
|
||||||
case 'fail':
|
|
||||||
failed.push([name, res]);
|
|
||||||
break;
|
|
||||||
case 'warn':
|
|
||||||
warned.push([name, res]);
|
|
||||||
break;
|
|
||||||
case 'skip':
|
|
||||||
skipped.push([name, res]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
unreachable('unrecognized status');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (codeCoverage !== undefined) {
|
|
||||||
const coverage = codeCoverage.end();
|
|
||||||
console.log(`Code-coverage: [[${coverage}]]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listMode !== 'none') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(total > 0, 'found no tests!');
|
|
||||||
|
|
||||||
// MAINTENANCE_TODO: write results out somewhere (a file?)
|
|
||||||
if (printJSON) {
|
|
||||||
console.log(log.asJSON(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quiet) {
|
|
||||||
if (skipped.length) {
|
|
||||||
console.log('');
|
|
||||||
console.log('** Skipped **');
|
|
||||||
printResults(skipped);
|
|
||||||
}
|
|
||||||
if (warned.length) {
|
|
||||||
console.log('');
|
|
||||||
console.log('** Warnings **');
|
|
||||||
printResults(warned);
|
|
||||||
}
|
|
||||||
if (failed.length) {
|
|
||||||
console.log('');
|
|
||||||
console.log('** Failures **');
|
|
||||||
printResults(failed);
|
|
||||||
}
|
|
||||||
|
|
||||||
const passed = total - warned.length - failed.length - skipped.length;
|
|
||||||
const pct = (x: number) => ((100 * x) / total).toFixed(2);
|
|
||||||
const rpt = (x: number) => {
|
|
||||||
const xs = x.toString().padStart(1 + Math.log10(total), ' ');
|
|
||||||
return `${xs} / ${total} = ${pct(x).padStart(6, ' ')}%`;
|
|
||||||
};
|
|
||||||
console.log('');
|
|
||||||
console.log(`** Summary **
|
|
||||||
Passed w/o warnings = ${rpt(passed)}
|
|
||||||
Passed with warnings = ${rpt(warned.length)}
|
|
||||||
Skipped = ${rpt(skipped.length)}
|
|
||||||
Failed = ${rpt(failed.length)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (failed.length || warned.length) {
|
|
||||||
sys.exit(1);
|
|
||||||
}
|
|
||||||
})().catch(ex => {
|
|
||||||
console.log(ex.stack ?? ex.toString());
|
|
||||||
sys.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
function printResults(results: Array<[string, LiveTestCaseResult]>): void {
|
|
||||||
for (const [name, r] of results) {
|
|
||||||
console.log(`[${r.status}] ${name} (${r.timems}ms). Log:`);
|
|
||||||
if (r.logs) {
|
|
||||||
for (const l of r.logs) {
|
|
||||||
console.log(prettyPrintLog(l));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
let windowURL: URL | undefined = undefined;
|
|
||||||
function getWindowURL() {
|
|
||||||
if (windowURL === undefined) {
|
|
||||||
windowURL = new URL(window.location.toString());
|
|
||||||
}
|
|
||||||
return windowURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function optionEnabled(
|
|
||||||
opt: string,
|
|
||||||
searchParams: URLSearchParams = getWindowURL().searchParams
|
|
||||||
): boolean {
|
|
||||||
const val = searchParams.get(opt);
|
|
||||||
return val !== null && val !== '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function optionString(
|
|
||||||
opt: string,
|
|
||||||
searchParams: URLSearchParams = getWindowURL().searchParams
|
|
||||||
): string {
|
|
||||||
return searchParams.get(opt) || '';
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
/* eslint no-process-exit: "off" */
|
|
||||||
/* eslint @typescript-eslint/no-namespace: "off" */
|
|
||||||
|
|
||||||
function node() {
|
|
||||||
const { existsSync } = require('fs');
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'node',
|
|
||||||
existsSync,
|
|
||||||
args: process.argv.slice(2),
|
|
||||||
cwd: () => process.cwd(),
|
|
||||||
exit: (code?: number | undefined) => process.exit(code),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace Deno {
|
|
||||||
function readFileSync(path: string): Uint8Array;
|
|
||||||
const args: string[];
|
|
||||||
const cwd: () => string;
|
|
||||||
function exit(code?: number): never;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deno() {
|
|
||||||
function existsSync(path: string) {
|
|
||||||
try {
|
|
||||||
Deno.readFileSync(path);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'deno',
|
|
||||||
existsSync,
|
|
||||||
args: Deno.args,
|
|
||||||
cwd: Deno.cwd,
|
|
||||||
exit: Deno.exit,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const sys = typeof globalThis.process !== 'undefined' ? node() : deno();
|
|
||||||
|
|
||||||
export default sys;
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { setBaseResourcePath } from '../../framework/resources.js';
|
|
||||||
import { DefaultTestFileLoader } from '../../internal/file_loader.js';
|
|
||||||
import { Logger } from '../../internal/logging/logger.js';
|
|
||||||
import { parseQuery } from '../../internal/query/parseQuery.js';
|
|
||||||
import { TestQueryWithExpectation } from '../../internal/query/query.js';
|
|
||||||
import { assert } from '../../util/util.js';
|
|
||||||
|
|
||||||
// Should be DedicatedWorkerGlobalScope, but importing lib "webworker" conflicts with lib "dom".
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
declare const self: any;
|
|
||||||
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
|
|
||||||
setBaseResourcePath('../../../resources');
|
|
||||||
|
|
||||||
self.onmessage = async (ev: MessageEvent) => {
|
|
||||||
const query: string = ev.data.query;
|
|
||||||
const expectations: TestQueryWithExpectation[] = ev.data.expectations;
|
|
||||||
const debug: boolean = ev.data.debug;
|
|
||||||
|
|
||||||
Logger.globalDebugMode = debug;
|
|
||||||
const log = new Logger();
|
|
||||||
|
|
||||||
const testcases = Array.from(await loader.loadCases(parseQuery(query)));
|
|
||||||
assert(testcases.length === 1, 'worker query resulted in != 1 cases');
|
|
||||||
|
|
||||||
const testcase = testcases[0];
|
|
||||||
const [rec, result] = log.record(testcase.query.toString());
|
|
||||||
await testcase.run(rec, expectations);
|
|
||||||
|
|
||||||
self.postMessage({ query, result });
|
|
||||||
};
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
import { LogMessageWithStack } from '../../internal/logging/log_message.js';
|
|
||||||
import { TransferredTestCaseResult, LiveTestCaseResult } from '../../internal/logging/result.js';
|
|
||||||
import { TestCaseRecorder } from '../../internal/logging/test_case_recorder.js';
|
|
||||||
import { TestQueryWithExpectation } from '../../internal/query/query.js';
|
|
||||||
|
|
||||||
export class TestWorker {
|
|
||||||
private readonly debug: boolean;
|
|
||||||
private readonly worker: Worker;
|
|
||||||
private readonly resolvers = new Map<string, (result: LiveTestCaseResult) => void>();
|
|
||||||
|
|
||||||
constructor(debug: boolean) {
|
|
||||||
this.debug = debug;
|
|
||||||
|
|
||||||
const selfPath = import.meta.url;
|
|
||||||
const selfPathDir = selfPath.substring(0, selfPath.lastIndexOf('/'));
|
|
||||||
const workerPath = selfPathDir + '/test_worker-worker.js';
|
|
||||||
this.worker = new Worker(workerPath, { type: 'module' });
|
|
||||||
this.worker.onmessage = ev => {
|
|
||||||
const query: string = ev.data.query;
|
|
||||||
const result: TransferredTestCaseResult = ev.data.result;
|
|
||||||
if (result.logs) {
|
|
||||||
for (const l of result.logs) {
|
|
||||||
Object.setPrototypeOf(l, LogMessageWithStack.prototype);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.resolvers.get(query)!(result as LiveTestCaseResult);
|
|
||||||
|
|
||||||
// MAINTENANCE_TODO(kainino0x): update the Logger with this result (or don't have a logger and
|
|
||||||
// update the entire results JSON somehow at some point).
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(
|
|
||||||
rec: TestCaseRecorder,
|
|
||||||
query: string,
|
|
||||||
expectations: TestQueryWithExpectation[] = []
|
|
||||||
): Promise<void> {
|
|
||||||
this.worker.postMessage({ query, expectations, debug: this.debug });
|
|
||||||
const workerResult = await new Promise<LiveTestCaseResult>(resolve => {
|
|
||||||
this.resolvers.set(query, resolve);
|
|
||||||
});
|
|
||||||
rec.injectResult(workerResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
/* eslint no-console: "off" */
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as http from 'http';
|
|
||||||
import { AddressInfo } from 'net';
|
|
||||||
|
|
||||||
import { dataCache } from '../framework/data_cache.js';
|
|
||||||
import { globalTestConfig } from '../framework/test_config.js';
|
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { prettyPrintLog } from '../internal/logging/log_message.js';
|
|
||||||
import { Logger } from '../internal/logging/logger.js';
|
|
||||||
import { LiveTestCaseResult, Status } from '../internal/logging/result.js';
|
|
||||||
import { parseQuery } from '../internal/query/parseQuery.js';
|
|
||||||
import { TestQueryWithExpectation } from '../internal/query/query.js';
|
|
||||||
import { TestTreeLeaf } from '../internal/tree.js';
|
|
||||||
import { Colors } from '../util/colors.js';
|
|
||||||
import { setGPUProvider } from '../util/navigator_gpu.js';
|
|
||||||
|
|
||||||
import sys from './helper/sys.js';
|
|
||||||
|
|
||||||
function usage(rc: number): never {
|
|
||||||
console.log(`Usage:
|
|
||||||
tools/run_${sys.type} [OPTIONS...]
|
|
||||||
Options:
|
|
||||||
--colors Enable ANSI colors in output.
|
|
||||||
--coverage Add coverage data to each result.
|
|
||||||
--data Path to the data cache directory.
|
|
||||||
--verbose Print result/log of every test as it runs.
|
|
||||||
--gpu-provider Path to node module that provides the GPU implementation.
|
|
||||||
--gpu-provider-flag Flag to set on the gpu-provider as <flag>=<value>
|
|
||||||
--unroll-const-eval-loops Unrolls loops in constant-evaluation shader execution tests
|
|
||||||
--u Flag to set on the gpu-provider as <flag>=<value>
|
|
||||||
|
|
||||||
Provides an HTTP server used for running tests via an HTTP RPC interface
|
|
||||||
To run a test, perform an HTTP GET or POST at the URL:
|
|
||||||
http://localhost:port/run?<test-name>
|
|
||||||
To shutdown the server perform an HTTP GET or POST at the URL:
|
|
||||||
http://localhost:port/terminate
|
|
||||||
`);
|
|
||||||
return sys.exit(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RunResult {
|
|
||||||
// The result of the test
|
|
||||||
status: Status;
|
|
||||||
// Any additional messages printed
|
|
||||||
message: string;
|
|
||||||
// Code coverage data, if the server was started with `--coverage`
|
|
||||||
// This data is opaque (implementation defined).
|
|
||||||
coverageData?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The interface that exposes creation of the GPU, and optional interface to code coverage.
|
|
||||||
interface GPUProviderModule {
|
|
||||||
// @returns a GPU with the given flags
|
|
||||||
create(flags: string[]): GPU;
|
|
||||||
// An optional interface to a CodeCoverageProvider
|
|
||||||
coverage?: CodeCoverageProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CodeCoverageProvider {
|
|
||||||
// Starts collecting code coverage
|
|
||||||
begin(): void;
|
|
||||||
// Ends collecting of code coverage, returning the coverage data.
|
|
||||||
// This data is opaque (implementation defined).
|
|
||||||
end(): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sys.existsSync('src/common/runtime/cmdline.ts')) {
|
|
||||||
console.log('Must be run from repository root');
|
|
||||||
usage(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Colors.enabled = false;
|
|
||||||
|
|
||||||
let emitCoverage = false;
|
|
||||||
let verbose = false;
|
|
||||||
let gpuProviderModule: GPUProviderModule | undefined = undefined;
|
|
||||||
let dataPath: string | undefined = undefined;
|
|
||||||
|
|
||||||
const gpuProviderFlags: string[] = [];
|
|
||||||
for (let i = 0; i < sys.args.length; ++i) {
|
|
||||||
const a = sys.args[i];
|
|
||||||
if (a.startsWith('-')) {
|
|
||||||
if (a === '--colors') {
|
|
||||||
Colors.enabled = true;
|
|
||||||
} else if (a === '--coverage') {
|
|
||||||
emitCoverage = true;
|
|
||||||
} else if (a === '--data') {
|
|
||||||
dataPath = sys.args[++i];
|
|
||||||
} else if (a === '--gpu-provider') {
|
|
||||||
const modulePath = sys.args[++i];
|
|
||||||
gpuProviderModule = require(modulePath);
|
|
||||||
} else if (a === '--gpu-provider-flag') {
|
|
||||||
gpuProviderFlags.push(sys.args[++i]);
|
|
||||||
} else if (a === '--unroll-const-eval-loops') {
|
|
||||||
globalTestConfig.unrollConstEvalLoops = true;
|
|
||||||
} else if (a === '--help') {
|
|
||||||
usage(1);
|
|
||||||
} else if (a === '--verbose') {
|
|
||||||
verbose = true;
|
|
||||||
} else {
|
|
||||||
console.log(`unrecognised flag: ${a}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let codeCoverage: CodeCoverageProvider | undefined = undefined;
|
|
||||||
|
|
||||||
if (gpuProviderModule) {
|
|
||||||
setGPUProvider(() => gpuProviderModule!.create(gpuProviderFlags));
|
|
||||||
|
|
||||||
if (emitCoverage) {
|
|
||||||
codeCoverage = gpuProviderModule.coverage;
|
|
||||||
if (codeCoverage === undefined) {
|
|
||||||
console.error(
|
|
||||||
`--coverage specified, but the GPUProviderModule does not support code coverage.
|
|
||||||
Did you remember to build with code coverage instrumentation enabled?`
|
|
||||||
);
|
|
||||||
sys.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataPath !== undefined) {
|
|
||||||
dataCache.setStore({
|
|
||||||
load: (path: string) => {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
fs.readFile(`${dataPath}/${path}`, 'utf8', (err, data) => {
|
|
||||||
if (err !== null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (verbose) {
|
|
||||||
dataCache.setDebugLogger(console.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
Logger.globalDebugMode = verbose;
|
|
||||||
const log = new Logger();
|
|
||||||
const testcases = new Map<string, TestTreeLeaf>();
|
|
||||||
|
|
||||||
async function runTestcase(
|
|
||||||
testcase: TestTreeLeaf,
|
|
||||||
expectations: TestQueryWithExpectation[] = []
|
|
||||||
): Promise<LiveTestCaseResult> {
|
|
||||||
const name = testcase.query.toString();
|
|
||||||
const [rec, res] = log.record(name);
|
|
||||||
await testcase.run(rec, expectations);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = http.createServer(
|
|
||||||
async (request: http.IncomingMessage, response: http.ServerResponse) => {
|
|
||||||
if (request.url === undefined) {
|
|
||||||
response.end('invalid url');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadCasesPrefix = '/load?';
|
|
||||||
const runPrefix = '/run?';
|
|
||||||
const terminatePrefix = '/terminate';
|
|
||||||
|
|
||||||
if (request.url.startsWith(loadCasesPrefix)) {
|
|
||||||
const query = request.url.substr(loadCasesPrefix.length);
|
|
||||||
try {
|
|
||||||
const webgpuQuery = parseQuery(query);
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
for (const testcase of await loader.loadCases(webgpuQuery)) {
|
|
||||||
testcases.set(testcase.query.toString(), testcase);
|
|
||||||
}
|
|
||||||
response.statusCode = 200;
|
|
||||||
response.end();
|
|
||||||
} catch (err) {
|
|
||||||
response.statusCode = 500;
|
|
||||||
response.end(`load failed with error: ${err}\n${(err as Error).stack}`);
|
|
||||||
}
|
|
||||||
} else if (request.url.startsWith(runPrefix)) {
|
|
||||||
const name = request.url.substr(runPrefix.length);
|
|
||||||
try {
|
|
||||||
const testcase = testcases.get(name);
|
|
||||||
if (testcase) {
|
|
||||||
if (codeCoverage !== undefined) {
|
|
||||||
codeCoverage.begin();
|
|
||||||
}
|
|
||||||
const result = await runTestcase(testcase);
|
|
||||||
const coverageData = codeCoverage !== undefined ? codeCoverage.end() : undefined;
|
|
||||||
let message = '';
|
|
||||||
if (result.logs !== undefined) {
|
|
||||||
message = result.logs.map(log => prettyPrintLog(log)).join('\n');
|
|
||||||
}
|
|
||||||
const status = result.status;
|
|
||||||
const res: RunResult = { status, message, coverageData };
|
|
||||||
response.statusCode = 200;
|
|
||||||
response.end(JSON.stringify(res));
|
|
||||||
} else {
|
|
||||||
response.statusCode = 404;
|
|
||||||
response.end(`test case '${name}' not found`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
response.statusCode = 500;
|
|
||||||
response.end(`run failed with error: ${err}`);
|
|
||||||
}
|
|
||||||
} else if (request.url.startsWith(terminatePrefix)) {
|
|
||||||
server.close();
|
|
||||||
sys.exit(1);
|
|
||||||
} else {
|
|
||||||
response.statusCode = 404;
|
|
||||||
response.end('unhandled url request');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
server.listen(0, () => {
|
|
||||||
const address = server.address() as AddressInfo;
|
|
||||||
console.log(`Server listening at [[${address.port}]]`);
|
|
||||||
});
|
|
||||||
})().catch(ex => {
|
|
||||||
console.error(ex.stack ?? ex.toString());
|
|
||||||
sys.exit(1);
|
|
||||||
});
|
|
||||||
|
|
@ -1,625 +0,0 @@
|
||||||
// Implements the standalone test runner (see also: /standalone/index.html).
|
|
||||||
|
|
||||||
import { dataCache } from '../framework/data_cache.js';
|
|
||||||
import { setBaseResourcePath } from '../framework/resources.js';
|
|
||||||
import { globalTestConfig } from '../framework/test_config.js';
|
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { Logger } from '../internal/logging/logger.js';
|
|
||||||
import { LiveTestCaseResult } from '../internal/logging/result.js';
|
|
||||||
import { parseQuery } from '../internal/query/parseQuery.js';
|
|
||||||
import { TestQueryLevel } from '../internal/query/query.js';
|
|
||||||
import { TestTreeNode, TestSubtree, TestTreeLeaf, TestTree } from '../internal/tree.js';
|
|
||||||
import { setDefaultRequestAdapterOptions } from '../util/navigator_gpu.js';
|
|
||||||
import { assert, ErrorWithExtra, unreachable } from '../util/util.js';
|
|
||||||
|
|
||||||
import { optionEnabled, optionString } from './helper/options.js';
|
|
||||||
import { TestWorker } from './helper/test_worker.js';
|
|
||||||
|
|
||||||
window.onbeforeunload = () => {
|
|
||||||
// Prompt user before reloading if there are any results
|
|
||||||
return haveSomeResults ? false : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
let haveSomeResults = false;
|
|
||||||
|
|
||||||
// The possible options for the tests.
|
|
||||||
interface StandaloneOptions {
|
|
||||||
runnow: boolean;
|
|
||||||
worker: boolean;
|
|
||||||
debug: boolean;
|
|
||||||
unrollConstEvalLoops: boolean;
|
|
||||||
powerPreference: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extra per option info.
|
|
||||||
interface StandaloneOptionInfo {
|
|
||||||
description: string;
|
|
||||||
parser?: (key: string) => boolean | string;
|
|
||||||
selectValueDescriptions?: { value: string; description: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type for info for every option. This definition means adding an option
|
|
||||||
// will generate a compile time error if not extra info is provided.
|
|
||||||
type StandaloneOptionsInfos = Record<keyof StandaloneOptions, StandaloneOptionInfo>;
|
|
||||||
|
|
||||||
const optionsInfo: StandaloneOptionsInfos = {
|
|
||||||
runnow: { description: 'run immediately on load' },
|
|
||||||
worker: { description: 'run in a worker' },
|
|
||||||
debug: { description: 'show more info' },
|
|
||||||
unrollConstEvalLoops: { description: 'unroll const eval loops in WGSL' },
|
|
||||||
powerPreference: {
|
|
||||||
description: 'set default powerPreference for some tests',
|
|
||||||
parser: optionString,
|
|
||||||
selectValueDescriptions: [
|
|
||||||
{ value: '', description: 'default' },
|
|
||||||
{ value: 'low-power', description: 'low-power' },
|
|
||||||
{ value: 'high-performance', description: 'high-performance' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts camel case to snake case.
|
|
||||||
* Examples:
|
|
||||||
* fooBar -> foo_bar
|
|
||||||
* parseHTMLFile -> parse_html_file
|
|
||||||
*/
|
|
||||||
function camelCaseToSnakeCase(id: string) {
|
|
||||||
return id
|
|
||||||
.replace(/(.)([A-Z][a-z]+)/g, '$1_$2')
|
|
||||||
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
||||||
.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a StandaloneOptions from the current URL search parameters.
|
|
||||||
*/
|
|
||||||
function getOptionsInfoFromSearchParameters(
|
|
||||||
optionsInfos: StandaloneOptionsInfos
|
|
||||||
): StandaloneOptions {
|
|
||||||
const optionValues: Record<string, boolean | string> = {};
|
|
||||||
for (const [optionName, info] of Object.entries(optionsInfos)) {
|
|
||||||
const parser = info.parser || optionEnabled;
|
|
||||||
optionValues[optionName] = parser(camelCaseToSnakeCase(optionName));
|
|
||||||
}
|
|
||||||
return (optionValues as unknown) as StandaloneOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is just a cast in one place.
|
|
||||||
function optionsToRecord(options: StandaloneOptions) {
|
|
||||||
return (options as unknown) as Record<string, boolean | string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = getOptionsInfoFromSearchParameters(optionsInfo);
|
|
||||||
const { runnow, debug, unrollConstEvalLoops, powerPreference } = options;
|
|
||||||
globalTestConfig.unrollConstEvalLoops = unrollConstEvalLoops;
|
|
||||||
|
|
||||||
Logger.globalDebugMode = debug;
|
|
||||||
const logger = new Logger();
|
|
||||||
|
|
||||||
setBaseResourcePath('../out/resources');
|
|
||||||
|
|
||||||
const worker = options.worker ? new TestWorker(debug) : undefined;
|
|
||||||
|
|
||||||
const autoCloseOnPass = document.getElementById('autoCloseOnPass') as HTMLInputElement;
|
|
||||||
const resultsVis = document.getElementById('resultsVis')!;
|
|
||||||
const progressElem = document.getElementById('progress')!;
|
|
||||||
const progressTestNameElem = progressElem.querySelector('.progress-test-name')!;
|
|
||||||
const stopButtonElem = progressElem.querySelector('button')!;
|
|
||||||
let runDepth = 0;
|
|
||||||
let stopRequested = false;
|
|
||||||
|
|
||||||
stopButtonElem.addEventListener('click', () => {
|
|
||||||
stopRequested = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (powerPreference) {
|
|
||||||
setDefaultRequestAdapterOptions({ powerPreference: powerPreference as GPUPowerPreference });
|
|
||||||
}
|
|
||||||
|
|
||||||
dataCache.setStore({
|
|
||||||
load: async (path: string) => {
|
|
||||||
const response = await fetch(`data/${path}`);
|
|
||||||
if (!response.ok) {
|
|
||||||
return Promise.reject(response.statusText);
|
|
||||||
}
|
|
||||||
return await response.text();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
interface SubtreeResult {
|
|
||||||
pass: number;
|
|
||||||
fail: number;
|
|
||||||
warn: number;
|
|
||||||
skip: number;
|
|
||||||
total: number;
|
|
||||||
timems: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function emptySubtreeResult() {
|
|
||||||
return { pass: 0, fail: 0, warn: 0, skip: 0, total: 0, timems: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeSubtreeResults(...results: SubtreeResult[]) {
|
|
||||||
const target = emptySubtreeResult();
|
|
||||||
for (const result of results) {
|
|
||||||
target.pass += result.pass;
|
|
||||||
target.fail += result.fail;
|
|
||||||
target.warn += result.warn;
|
|
||||||
target.skip += result.skip;
|
|
||||||
target.total += result.total;
|
|
||||||
target.timems += result.timems;
|
|
||||||
}
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetCheckedRecursively = () => void;
|
|
||||||
type GenerateSubtreeHTML = (parent: HTMLElement) => SetCheckedRecursively;
|
|
||||||
type RunSubtree = () => Promise<SubtreeResult>;
|
|
||||||
|
|
||||||
interface VisualizedSubtree {
|
|
||||||
generateSubtreeHTML: GenerateSubtreeHTML;
|
|
||||||
runSubtree: RunSubtree;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DOM generation
|
|
||||||
|
|
||||||
function memoize<T>(fn: () => T): () => T {
|
|
||||||
let value: T | undefined;
|
|
||||||
return () => {
|
|
||||||
if (value === undefined) {
|
|
||||||
value = fn();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeTreeNodeHTML(tree: TestTreeNode, parentLevel: TestQueryLevel): VisualizedSubtree {
|
|
||||||
let subtree: VisualizedSubtree;
|
|
||||||
|
|
||||||
if ('children' in tree) {
|
|
||||||
subtree = makeSubtreeHTML(tree, parentLevel);
|
|
||||||
} else {
|
|
||||||
subtree = makeCaseHTML(tree);
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateMyHTML = (parentElement: HTMLElement) => {
|
|
||||||
const div = $('<div>').appendTo(parentElement)[0];
|
|
||||||
return subtree.generateSubtreeHTML(div);
|
|
||||||
};
|
|
||||||
return { runSubtree: subtree.runSubtree, generateSubtreeHTML: generateMyHTML };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeCaseHTML(t: TestTreeLeaf): VisualizedSubtree {
|
|
||||||
// Becomes set once the case has been run once.
|
|
||||||
let caseResult: LiveTestCaseResult | undefined;
|
|
||||||
|
|
||||||
// Becomes set once the DOM for this case exists.
|
|
||||||
let clearRenderedResult: (() => void) | undefined;
|
|
||||||
let updateRenderedResult: (() => void) | undefined;
|
|
||||||
|
|
||||||
const name = t.query.toString();
|
|
||||||
const runSubtree = async () => {
|
|
||||||
if (clearRenderedResult) clearRenderedResult();
|
|
||||||
|
|
||||||
const result: SubtreeResult = emptySubtreeResult();
|
|
||||||
progressTestNameElem.textContent = name;
|
|
||||||
|
|
||||||
haveSomeResults = true;
|
|
||||||
const [rec, res] = logger.record(name);
|
|
||||||
caseResult = res;
|
|
||||||
if (worker) {
|
|
||||||
await worker.run(rec, name);
|
|
||||||
} else {
|
|
||||||
await t.run(rec);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.total++;
|
|
||||||
result.timems += caseResult.timems;
|
|
||||||
switch (caseResult.status) {
|
|
||||||
case 'pass':
|
|
||||||
result.pass++;
|
|
||||||
break;
|
|
||||||
case 'fail':
|
|
||||||
result.fail++;
|
|
||||||
break;
|
|
||||||
case 'skip':
|
|
||||||
result.skip++;
|
|
||||||
break;
|
|
||||||
case 'warn':
|
|
||||||
result.warn++;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
unreachable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateRenderedResult) updateRenderedResult();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateSubtreeHTML = (div: HTMLElement) => {
|
|
||||||
div.classList.add('testcase');
|
|
||||||
|
|
||||||
const caselogs = $('<div>').addClass('testcaselogs').hide();
|
|
||||||
const [casehead, setChecked] = makeTreeNodeHeaderHTML(t, runSubtree, 2, checked => {
|
|
||||||
checked ? caselogs.show() : caselogs.hide();
|
|
||||||
});
|
|
||||||
const casetime = $('<div>').addClass('testcasetime').html('ms').appendTo(casehead);
|
|
||||||
div.appendChild(casehead);
|
|
||||||
div.appendChild(caselogs[0]);
|
|
||||||
|
|
||||||
clearRenderedResult = () => {
|
|
||||||
div.removeAttribute('data-status');
|
|
||||||
casetime.text('ms');
|
|
||||||
caselogs.empty();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateRenderedResult = () => {
|
|
||||||
if (caseResult) {
|
|
||||||
div.setAttribute('data-status', caseResult.status);
|
|
||||||
|
|
||||||
casetime.text(caseResult.timems.toFixed(4) + ' ms');
|
|
||||||
|
|
||||||
if (caseResult.logs) {
|
|
||||||
caselogs.empty();
|
|
||||||
for (const l of caseResult.logs) {
|
|
||||||
const caselog = $('<div>').addClass('testcaselog').appendTo(caselogs);
|
|
||||||
$('<button>')
|
|
||||||
.addClass('testcaselogbtn')
|
|
||||||
.attr('alt', 'Log stack to console')
|
|
||||||
.attr('title', 'Log stack to console')
|
|
||||||
.appendTo(caselog)
|
|
||||||
.on('click', () => {
|
|
||||||
consoleLogError(l);
|
|
||||||
});
|
|
||||||
$('<pre>').addClass('testcaselogtext').appendTo(caselog).text(l.toJSON());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateRenderedResult();
|
|
||||||
|
|
||||||
return setChecked;
|
|
||||||
};
|
|
||||||
|
|
||||||
return { runSubtree, generateSubtreeHTML };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSubtreeHTML(n: TestSubtree, parentLevel: TestQueryLevel): VisualizedSubtree {
|
|
||||||
let subtreeResult: SubtreeResult = emptySubtreeResult();
|
|
||||||
// Becomes set once the DOM for this case exists.
|
|
||||||
let clearRenderedResult: (() => void) | undefined;
|
|
||||||
let updateRenderedResult: (() => void) | undefined;
|
|
||||||
|
|
||||||
const { runSubtree, generateSubtreeHTML } = makeSubtreeChildrenHTML(
|
|
||||||
n.children.values(),
|
|
||||||
n.query.level
|
|
||||||
);
|
|
||||||
|
|
||||||
const runMySubtree = async () => {
|
|
||||||
if (runDepth === 0) {
|
|
||||||
stopRequested = false;
|
|
||||||
progressElem.style.display = '';
|
|
||||||
}
|
|
||||||
if (stopRequested) {
|
|
||||||
const result = emptySubtreeResult();
|
|
||||||
result.skip = 1;
|
|
||||||
result.total = 1;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
++runDepth;
|
|
||||||
|
|
||||||
if (clearRenderedResult) clearRenderedResult();
|
|
||||||
subtreeResult = await runSubtree();
|
|
||||||
if (updateRenderedResult) updateRenderedResult();
|
|
||||||
|
|
||||||
--runDepth;
|
|
||||||
if (runDepth === 0) {
|
|
||||||
progressElem.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
return subtreeResult;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generateMyHTML = (div: HTMLElement) => {
|
|
||||||
const subtreeHTML = $('<div>').addClass('subtreechildren');
|
|
||||||
const generateSubtree = memoize(() => generateSubtreeHTML(subtreeHTML[0]));
|
|
||||||
|
|
||||||
// Hide subtree - it's not generated yet.
|
|
||||||
subtreeHTML.hide();
|
|
||||||
const [header, setChecked] = makeTreeNodeHeaderHTML(n, runMySubtree, parentLevel, checked => {
|
|
||||||
if (checked) {
|
|
||||||
// Make sure the subtree is generated and then show it.
|
|
||||||
generateSubtree();
|
|
||||||
subtreeHTML.show();
|
|
||||||
} else {
|
|
||||||
subtreeHTML.hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
div.classList.add('subtree');
|
|
||||||
div.classList.add(['', 'multifile', 'multitest', 'multicase'][n.query.level]);
|
|
||||||
div.appendChild(header);
|
|
||||||
div.appendChild(subtreeHTML[0]);
|
|
||||||
|
|
||||||
clearRenderedResult = () => {
|
|
||||||
div.removeAttribute('data-status');
|
|
||||||
};
|
|
||||||
|
|
||||||
updateRenderedResult = () => {
|
|
||||||
let status = '';
|
|
||||||
if (subtreeResult.pass > 0) {
|
|
||||||
status += 'pass';
|
|
||||||
}
|
|
||||||
if (subtreeResult.fail > 0) {
|
|
||||||
status += 'fail';
|
|
||||||
}
|
|
||||||
div.setAttribute('data-status', status);
|
|
||||||
if (autoCloseOnPass.checked && status === 'pass') {
|
|
||||||
div.firstElementChild!.removeAttribute('open');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateRenderedResult();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
setChecked();
|
|
||||||
const setChildrenChecked = generateSubtree();
|
|
||||||
setChildrenChecked();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return { runSubtree: runMySubtree, generateSubtreeHTML: generateMyHTML };
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeSubtreeChildrenHTML(
|
|
||||||
children: Iterable<TestTreeNode>,
|
|
||||||
parentLevel: TestQueryLevel
|
|
||||||
): VisualizedSubtree {
|
|
||||||
const childFns = Array.from(children, subtree => makeTreeNodeHTML(subtree, parentLevel));
|
|
||||||
|
|
||||||
const runMySubtree = async () => {
|
|
||||||
const results: SubtreeResult[] = [];
|
|
||||||
for (const { runSubtree } of childFns) {
|
|
||||||
results.push(await runSubtree());
|
|
||||||
}
|
|
||||||
return mergeSubtreeResults(...results);
|
|
||||||
};
|
|
||||||
const generateMyHTML = (div: HTMLElement) => {
|
|
||||||
const setChildrenChecked = Array.from(childFns, ({ generateSubtreeHTML }) =>
|
|
||||||
generateSubtreeHTML(div)
|
|
||||||
);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const setChildChecked of setChildrenChecked) {
|
|
||||||
setChildChecked();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return { runSubtree: runMySubtree, generateSubtreeHTML: generateMyHTML };
|
|
||||||
}
|
|
||||||
|
|
||||||
function consoleLogError(e: Error | ErrorWithExtra | undefined) {
|
|
||||||
if (e === undefined) return;
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
(globalThis as any)._stack = e;
|
|
||||||
/* eslint-disable-next-line no-console */
|
|
||||||
console.log('_stack =', e);
|
|
||||||
if ('extra' in e && e.extra !== undefined) {
|
|
||||||
/* eslint-disable-next-line no-console */
|
|
||||||
console.log('_stack.extra =', e.extra);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeTreeNodeHeaderHTML(
|
|
||||||
n: TestTreeNode,
|
|
||||||
runSubtree: RunSubtree,
|
|
||||||
parentLevel: TestQueryLevel,
|
|
||||||
onChange: (checked: boolean) => void
|
|
||||||
): [HTMLElement, SetCheckedRecursively] {
|
|
||||||
const isLeaf = 'run' in n;
|
|
||||||
const div = $('<details>').addClass('nodeheader');
|
|
||||||
const header = $('<summary>').appendTo(div);
|
|
||||||
|
|
||||||
const setChecked = () => {
|
|
||||||
div.prop('open', true); // (does not fire onChange)
|
|
||||||
onChange(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const href = `?${worker ? 'worker&' : ''}${debug ? 'debug&' : ''}q=${n.query.toString()}`;
|
|
||||||
if (onChange) {
|
|
||||||
div.on('toggle', function (this) {
|
|
||||||
onChange((this as HTMLDetailsElement).open);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expand the shallower parts of the tree at load.
|
|
||||||
// Also expand completely within subtrees that are at the same query level
|
|
||||||
// (e.g. s:f:t,* and s:f:t,t,*).
|
|
||||||
if (n.query.level <= lastQueryLevelToExpand || n.query.level === parentLevel) {
|
|
||||||
setChecked();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const runtext = isLeaf ? 'Run case' : 'Run subtree';
|
|
||||||
$('<button>')
|
|
||||||
.addClass(isLeaf ? 'leafrun' : 'subtreerun')
|
|
||||||
.attr('alt', runtext)
|
|
||||||
.attr('title', runtext)
|
|
||||||
.on('click', () => void runSubtree())
|
|
||||||
.appendTo(header);
|
|
||||||
$('<a>')
|
|
||||||
.addClass('nodelink')
|
|
||||||
.attr('href', href)
|
|
||||||
.attr('alt', 'Open')
|
|
||||||
.attr('title', 'Open')
|
|
||||||
.appendTo(header);
|
|
||||||
if ('testCreationStack' in n && n.testCreationStack) {
|
|
||||||
$('<button>')
|
|
||||||
.addClass('testcaselogbtn')
|
|
||||||
.attr('alt', 'Log test creation stack to console')
|
|
||||||
.attr('title', 'Log test creation stack to console')
|
|
||||||
.appendTo(header)
|
|
||||||
.on('click', () => {
|
|
||||||
consoleLogError(n.testCreationStack);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const nodetitle = $('<div>').addClass('nodetitle').appendTo(header);
|
|
||||||
const nodecolumns = $('<span>').addClass('nodecolumns').appendTo(nodetitle);
|
|
||||||
{
|
|
||||||
$('<input>')
|
|
||||||
.attr('type', 'text')
|
|
||||||
.prop('readonly', true)
|
|
||||||
.addClass('nodequery')
|
|
||||||
.val(n.query.toString())
|
|
||||||
.appendTo(nodecolumns);
|
|
||||||
if (n.subtreeCounts) {
|
|
||||||
$('<span>')
|
|
||||||
.attr('title', '(Nodes with TODOs) / (Total test count)')
|
|
||||||
.text(TestTree.countsToString(n))
|
|
||||||
.appendTo(nodecolumns);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ('description' in n && n.description) {
|
|
||||||
nodetitle.append(' ');
|
|
||||||
$('<pre>') //
|
|
||||||
.addClass('nodedescription')
|
|
||||||
.text(n.description)
|
|
||||||
.appendTo(header);
|
|
||||||
}
|
|
||||||
return [div[0], setChecked];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapse s:f:t:* or s:f:t:c by default.
|
|
||||||
let lastQueryLevelToExpand: TestQueryLevel = 2;
|
|
||||||
|
|
||||||
type ParamValue = string | undefined | null | boolean | string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes an array of string, ParamValue and returns an array of pairs
|
|
||||||
* of [key, value] where value is a string. Converts boolean to '0' or '1'.
|
|
||||||
*/
|
|
||||||
function keyValueToPairs([k, v]: [string, ParamValue]): [string, string][] {
|
|
||||||
const key = camelCaseToSnakeCase(k);
|
|
||||||
if (typeof v === 'boolean') {
|
|
||||||
return [[key, v ? '1' : '0']];
|
|
||||||
} else if (Array.isArray(v)) {
|
|
||||||
return v.map(v => [key, v]);
|
|
||||||
} else {
|
|
||||||
return [[key, v!.toString()]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts key value pairs to a search string.
|
|
||||||
* Keys will appear in order in the search string.
|
|
||||||
* Values can be undefined, null, boolean, string, or string[]
|
|
||||||
* If the value is falsy the key will not appear in the search string.
|
|
||||||
* If the value is an array the key will appear multiple times.
|
|
||||||
*
|
|
||||||
* @param params Some object with key value pairs.
|
|
||||||
* @returns a search string.
|
|
||||||
*/
|
|
||||||
function prepareParams(params: Record<string, ParamValue>): string {
|
|
||||||
const pairsArrays = Object.entries(params)
|
|
||||||
.filter(([, v]) => !!v)
|
|
||||||
.map(keyValueToPairs);
|
|
||||||
const pairs = pairsArrays.flat();
|
|
||||||
return new URLSearchParams(pairs).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
|
|
||||||
// MAINTENANCE_TODO: start populating page before waiting for everything to load?
|
|
||||||
const qs = new URLSearchParams(window.location.search).getAll('q');
|
|
||||||
if (qs.length === 0) {
|
|
||||||
qs.push('webgpu:*');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the URL bar to match the exact current options.
|
|
||||||
const updateURLWithCurrentOptions = () => {
|
|
||||||
const search = prepareParams(optionsToRecord(options));
|
|
||||||
let url = `${window.location.origin}${window.location.pathname}`;
|
|
||||||
// Add in q separately to avoid escaping punctuation marks.
|
|
||||||
url += `?${search}${search ? '&' : ''}${qs.map(q => 'q=' + q).join('&')}`;
|
|
||||||
window.history.replaceState(null, '', url.toString());
|
|
||||||
};
|
|
||||||
updateURLWithCurrentOptions();
|
|
||||||
|
|
||||||
const addOptionsToPage = (options: StandaloneOptions, optionsInfos: StandaloneOptionsInfos) => {
|
|
||||||
const optionsElem = $('table#options>tbody')[0];
|
|
||||||
const optionValues = optionsToRecord(options);
|
|
||||||
|
|
||||||
const createCheckbox = (optionName: string) => {
|
|
||||||
return $(`<input>`)
|
|
||||||
.attr('type', 'checkbox')
|
|
||||||
.prop('checked', optionValues[optionName] as boolean)
|
|
||||||
.on('change', function () {
|
|
||||||
optionValues[optionName] = (this as HTMLInputElement).checked;
|
|
||||||
updateURLWithCurrentOptions();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSelect = (optionName: string, info: StandaloneOptionInfo) => {
|
|
||||||
const select = $('<select>').on('change', function () {
|
|
||||||
optionValues[optionName] = (this as HTMLInputElement).value;
|
|
||||||
updateURLWithCurrentOptions();
|
|
||||||
});
|
|
||||||
const currentValue = optionValues[optionName];
|
|
||||||
for (const { value, description } of info.selectValueDescriptions!) {
|
|
||||||
$('<option>')
|
|
||||||
.text(description)
|
|
||||||
.val(value)
|
|
||||||
.prop('selected', value === currentValue)
|
|
||||||
.appendTo(select);
|
|
||||||
}
|
|
||||||
return select;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const [optionName, info] of Object.entries(optionsInfos)) {
|
|
||||||
const input =
|
|
||||||
typeof optionValues[optionName] === 'boolean'
|
|
||||||
? createCheckbox(optionName)
|
|
||||||
: createSelect(optionName, info);
|
|
||||||
$('<tr>')
|
|
||||||
.append($('<td>').append(input))
|
|
||||||
.append($('<td>').text(camelCaseToSnakeCase(optionName)))
|
|
||||||
.append($('<td>').text(info.description))
|
|
||||||
.appendTo(optionsElem);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addOptionsToPage(options, optionsInfo);
|
|
||||||
|
|
||||||
assert(qs.length === 1, 'currently, there must be exactly one ?q=');
|
|
||||||
const rootQuery = parseQuery(qs[0]);
|
|
||||||
if (rootQuery.level > lastQueryLevelToExpand) {
|
|
||||||
lastQueryLevelToExpand = rootQuery.level;
|
|
||||||
}
|
|
||||||
loader.addEventListener('import', ev => {
|
|
||||||
$('#info')[0].textContent = `loading: ${ev.data.url}`;
|
|
||||||
});
|
|
||||||
loader.addEventListener('finish', () => {
|
|
||||||
$('#info')[0].textContent = '';
|
|
||||||
});
|
|
||||||
const tree = await loader.loadTree(rootQuery);
|
|
||||||
|
|
||||||
tree.dissolveSingleChildTrees();
|
|
||||||
|
|
||||||
const { runSubtree, generateSubtreeHTML } = makeSubtreeHTML(tree.root, 1);
|
|
||||||
const setTreeCheckedRecursively = generateSubtreeHTML(resultsVis);
|
|
||||||
|
|
||||||
document.getElementById('expandall')!.addEventListener('click', () => {
|
|
||||||
setTreeCheckedRecursively();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('copyResultsJSON')!.addEventListener('click', () => {
|
|
||||||
void navigator.clipboard.writeText(logger.asJSON(2));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (runnow) {
|
|
||||||
void runSubtree();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
// Implements the wpt-embedded test runner (see also: wpt/cts.https.html).
|
|
||||||
|
|
||||||
import { globalTestConfig } from '../framework/test_config.js';
|
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { prettyPrintLog } from '../internal/logging/log_message.js';
|
|
||||||
import { Logger } from '../internal/logging/logger.js';
|
|
||||||
import { parseQuery } from '../internal/query/parseQuery.js';
|
|
||||||
import { parseExpectationsForTestQuery, relativeQueryString } from '../internal/query/query.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
import { optionEnabled } from './helper/options.js';
|
|
||||||
import { TestWorker } from './helper/test_worker.js';
|
|
||||||
|
|
||||||
// testharness.js API (https://web-platform-tests.org/writing-tests/testharness-api.html)
|
|
||||||
declare interface WptTestObject {
|
|
||||||
step(f: () => void): void;
|
|
||||||
done(): void;
|
|
||||||
}
|
|
||||||
declare function setup(properties: { explicit_done?: boolean }): void;
|
|
||||||
declare function promise_test(f: (t: WptTestObject) => Promise<void>, name: string): void;
|
|
||||||
declare function done(): void;
|
|
||||||
declare function assert_unreached(description: string): void;
|
|
||||||
|
|
||||||
declare const loadWebGPUExpectations: Promise<unknown> | undefined;
|
|
||||||
declare const shouldWebGPUCTSFailOnWarnings: Promise<boolean> | undefined;
|
|
||||||
|
|
||||||
setup({
|
|
||||||
// It's convenient for us to asynchronously add tests to the page. Prevent done() from being
|
|
||||||
// called implicitly when the page is finished loading.
|
|
||||||
explicit_done: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
const workerEnabled = optionEnabled('worker');
|
|
||||||
const worker = workerEnabled ? new TestWorker(false) : undefined;
|
|
||||||
|
|
||||||
globalTestConfig.unrollConstEvalLoops = optionEnabled('unroll_const_eval_loops');
|
|
||||||
|
|
||||||
const failOnWarnings =
|
|
||||||
typeof shouldWebGPUCTSFailOnWarnings !== 'undefined' && (await shouldWebGPUCTSFailOnWarnings);
|
|
||||||
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
const qs = new URLSearchParams(window.location.search).getAll('q');
|
|
||||||
assert(qs.length === 1, 'currently, there must be exactly one ?q=');
|
|
||||||
const filterQuery = parseQuery(qs[0]);
|
|
||||||
const testcases = await loader.loadCases(filterQuery);
|
|
||||||
|
|
||||||
const expectations =
|
|
||||||
typeof loadWebGPUExpectations !== 'undefined'
|
|
||||||
? parseExpectationsForTestQuery(
|
|
||||||
await loadWebGPUExpectations,
|
|
||||||
filterQuery,
|
|
||||||
new URL(window.location.href)
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const log = new Logger();
|
|
||||||
|
|
||||||
for (const testcase of testcases) {
|
|
||||||
const name = testcase.query.toString();
|
|
||||||
// For brevity, display the case name "relative" to the ?q= path.
|
|
||||||
const shortName = relativeQueryString(filterQuery, testcase.query) || '(case)';
|
|
||||||
|
|
||||||
const wpt_fn = async () => {
|
|
||||||
const [rec, res] = log.record(name);
|
|
||||||
if (worker) {
|
|
||||||
await worker.run(rec, name, expectations);
|
|
||||||
} else {
|
|
||||||
await testcase.run(rec, expectations);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unfortunately, it seems not possible to surface any logs for warn/skip.
|
|
||||||
if (res.status === 'fail' || (res.status === 'warn' && failOnWarnings)) {
|
|
||||||
const logs = (res.logs ?? []).map(prettyPrintLog);
|
|
||||||
assert_unreached('\n' + logs.join('\n') + '\n');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
promise_test(wpt_fn, shortName);
|
|
||||||
}
|
|
||||||
|
|
||||||
done();
|
|
||||||
})();
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
<!--
|
|
||||||
This test suite is built from the TypeScript sources at:
|
|
||||||
https://github.com/gpuweb/cts
|
|
||||||
|
|
||||||
If you are debugging WebGPU conformance tests, it's highly recommended that
|
|
||||||
you use the standalone interactive runner in that repository, which
|
|
||||||
provides tools for easier debugging and editing (source maps, debug
|
|
||||||
logging, warn/skip functionality, etc.)
|
|
||||||
|
|
||||||
NOTE:
|
|
||||||
The WPT version of this file is generated with *one variant per test spec
|
|
||||||
file*. If your harness needs more fine-grained suppressions, you'll need to
|
|
||||||
generate your own variants list from your suppression list.
|
|
||||||
See `tools/gen_wpt_cts_html` to do this.
|
|
||||||
|
|
||||||
When run under browser CI, the original cts.https.html should be skipped, and
|
|
||||||
this alternate version should be run instead, under a non-exported WPT test
|
|
||||||
directory (e.g. Chromium's wpt_internal).
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<title>WebGPU CTS</title>
|
|
||||||
<meta charset=utf-8>
|
|
||||||
<link rel=help href='https://gpuweb.github.io/gpuweb/'>
|
|
||||||
|
|
||||||
<script src=/resources/testharness.js></script>
|
|
||||||
<script src=/resources/testharnessreport.js></script>
|
|
||||||
<script>
|
|
||||||
const loadWebGPUExpectations = undefined;
|
|
||||||
const shouldWebGPUCTSFailOnWarnings = undefined;
|
|
||||||
</script>
|
|
||||||
<script type=module src=/webgpu/common/runtime/wpt.js></script>
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"no-process-exit": "off",
|
|
||||||
"node/no-unpublished-import": "off",
|
|
||||||
"node/no-unpublished-require": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as process from 'process';
|
|
||||||
|
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { Ordering, compareQueries } from '../internal/query/compare.js';
|
|
||||||
import { parseQuery } from '../internal/query/parseQuery.js';
|
|
||||||
import { TestQuery, TestQueryMultiFile } from '../internal/query/query.js';
|
|
||||||
import { loadTreeForQuery, TestTree } from '../internal/tree.js';
|
|
||||||
import { StacklessError } from '../internal/util.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
function usage(rc: number): void {
|
|
||||||
console.error('Usage:');
|
|
||||||
console.error(' tools/checklist FILE');
|
|
||||||
console.error(' tools/checklist my/list.txt');
|
|
||||||
process.exit(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.argv.length === 2) usage(0);
|
|
||||||
if (process.argv.length !== 3) usage(1);
|
|
||||||
|
|
||||||
type QueryInSuite = { readonly query: TestQuery; readonly done: boolean };
|
|
||||||
type QueriesInSuite = QueryInSuite[];
|
|
||||||
type QueriesBySuite = Map<string, QueriesInSuite>;
|
|
||||||
async function loadQueryListFromTextFile(filename: string): Promise<QueriesBySuite> {
|
|
||||||
const lines = (await fs.promises.readFile(filename, 'utf8')).split(/\r?\n/);
|
|
||||||
const allQueries = lines
|
|
||||||
.filter(l => l)
|
|
||||||
.map(l => {
|
|
||||||
const [doneStr, q] = l.split(/\s+/);
|
|
||||||
assert(doneStr === 'DONE' || doneStr === 'TODO', 'first column must be DONE or TODO');
|
|
||||||
return { query: parseQuery(q), done: doneStr === 'DONE' } as const;
|
|
||||||
});
|
|
||||||
|
|
||||||
const queriesBySuite: QueriesBySuite = new Map();
|
|
||||||
for (const q of allQueries) {
|
|
||||||
let suiteQueries = queriesBySuite.get(q.query.suite);
|
|
||||||
if (suiteQueries === undefined) {
|
|
||||||
suiteQueries = [];
|
|
||||||
queriesBySuite.set(q.query.suite, suiteQueries);
|
|
||||||
}
|
|
||||||
|
|
||||||
suiteQueries.push(q);
|
|
||||||
}
|
|
||||||
|
|
||||||
return queriesBySuite;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForOverlappingQueries(queries: QueriesInSuite): void {
|
|
||||||
for (let i1 = 0; i1 < queries.length; ++i1) {
|
|
||||||
for (let i2 = i1 + 1; i2 < queries.length; ++i2) {
|
|
||||||
const q1 = queries[i1].query;
|
|
||||||
const q2 = queries[i2].query;
|
|
||||||
if (compareQueries(q1, q2) !== Ordering.Unordered) {
|
|
||||||
console.log(` FYI, the following checklist items overlap:\n ${q1}\n ${q2}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkForUnmatchedSubtreesAndDoneness(
|
|
||||||
tree: TestTree,
|
|
||||||
matchQueries: QueriesInSuite
|
|
||||||
): number {
|
|
||||||
let subtreeCount = 0;
|
|
||||||
const unmatchedSubtrees: TestQuery[] = [];
|
|
||||||
const overbroadMatches: [TestQuery, TestQuery][] = [];
|
|
||||||
const donenessMismatches: QueryInSuite[] = [];
|
|
||||||
const alwaysExpandThroughLevel = 1; // expand to, at minimum, every file.
|
|
||||||
for (const subtree of tree.iterateCollapsedNodes({
|
|
||||||
includeIntermediateNodes: true,
|
|
||||||
includeEmptySubtrees: true,
|
|
||||||
alwaysExpandThroughLevel,
|
|
||||||
})) {
|
|
||||||
subtreeCount++;
|
|
||||||
const subtreeDone = !subtree.subtreeCounts?.nodesWithTODO;
|
|
||||||
|
|
||||||
let subtreeMatched = false;
|
|
||||||
for (const q of matchQueries) {
|
|
||||||
const comparison = compareQueries(q.query, subtree.query);
|
|
||||||
if (comparison !== Ordering.Unordered) subtreeMatched = true;
|
|
||||||
if (comparison === Ordering.StrictSubset) continue;
|
|
||||||
if (comparison === Ordering.StrictSuperset) overbroadMatches.push([q.query, subtree.query]);
|
|
||||||
if (comparison === Ordering.Equal && q.done !== subtreeDone) donenessMismatches.push(q);
|
|
||||||
}
|
|
||||||
if (!subtreeMatched) unmatchedSubtrees.push(subtree.query);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overbroadMatches.length) {
|
|
||||||
// (note, this doesn't show ALL multi-test queries - just ones that actually match any .spec.ts)
|
|
||||||
console.log(` FYI, the following checklist items were broader than one file:`);
|
|
||||||
for (const [q, collapsedSubtree] of overbroadMatches) {
|
|
||||||
console.log(` ${q} > ${collapsedSubtree}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unmatchedSubtrees.length) {
|
|
||||||
throw new StacklessError(`Found unmatched tests:\n ${unmatchedSubtrees.join('\n ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (donenessMismatches.length) {
|
|
||||||
throw new StacklessError(
|
|
||||||
'Found done/todo mismatches:\n ' +
|
|
||||||
donenessMismatches
|
|
||||||
.map(q => `marked ${q.done ? 'DONE, but is TODO' : 'TODO, but is DONE'}: ${q.query}`)
|
|
||||||
.join('\n ')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return subtreeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
console.log('Loading queries...');
|
|
||||||
const queriesBySuite = await loadQueryListFromTextFile(process.argv[2]);
|
|
||||||
console.log(' Found suites: ' + Array.from(queriesBySuite.keys()).join(' '));
|
|
||||||
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
for (const [suite, queriesInSuite] of queriesBySuite.entries()) {
|
|
||||||
console.log(`Suite "${suite}":`);
|
|
||||||
console.log(` Checking overlaps between ${queriesInSuite.length} checklist items...`);
|
|
||||||
checkForOverlappingQueries(queriesInSuite);
|
|
||||||
const suiteQuery = new TestQueryMultiFile(suite, []);
|
|
||||||
console.log(` Loading tree ${suiteQuery}...`);
|
|
||||||
const tree = await loadTreeForQuery(
|
|
||||||
loader,
|
|
||||||
suiteQuery,
|
|
||||||
queriesInSuite.map(q => q.query)
|
|
||||||
);
|
|
||||||
console.log(' Found no invalid queries in the checklist. Checking for unmatched tests...');
|
|
||||||
const subtreeCount = checkForUnmatchedSubtreesAndDoneness(tree, queriesInSuite);
|
|
||||||
console.log(` No unmatched tests or done/todo mismatches among ${subtreeCount} subtrees!`);
|
|
||||||
}
|
|
||||||
console.log(`Checklist looks good!`);
|
|
||||||
})().catch(ex => {
|
|
||||||
console.log(ex.stack ?? ex.toString());
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
// Node can look at the filesystem, but JS in the browser can't.
|
|
||||||
// This crawls the file tree under src/suites/${suite} to generate a (non-hierarchical) static
|
|
||||||
// listing file that can then be used in the browser to load the modules containing the tests.
|
|
||||||
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { SpecFile } from '../internal/file_loader.js';
|
|
||||||
import { validQueryPart } from '../internal/query/validQueryPart.js';
|
|
||||||
import { TestSuiteListingEntry, TestSuiteListing } from '../internal/test_suite_listing.js';
|
|
||||||
import { assert, unreachable } from '../util/util.js';
|
|
||||||
|
|
||||||
const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js';
|
|
||||||
|
|
||||||
async function crawlFilesRecursively(dir: string): Promise<string[]> {
|
|
||||||
const subpathInfo = await Promise.all(
|
|
||||||
(await fs.promises.readdir(dir)).map(async d => {
|
|
||||||
const p = path.join(dir, d);
|
|
||||||
const stats = await fs.promises.stat(p);
|
|
||||||
return {
|
|
||||||
path: p,
|
|
||||||
isDirectory: stats.isDirectory(),
|
|
||||||
isFile: stats.isFile(),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const files = subpathInfo
|
|
||||||
.filter(
|
|
||||||
i =>
|
|
||||||
i.isFile &&
|
|
||||||
(i.path.endsWith(specFileSuffix) ||
|
|
||||||
i.path.endsWith(`${path.sep}README.txt`) ||
|
|
||||||
i.path === 'README.txt')
|
|
||||||
)
|
|
||||||
.map(i => i.path);
|
|
||||||
|
|
||||||
return files.concat(
|
|
||||||
await subpathInfo
|
|
||||||
.filter(i => i.isDirectory)
|
|
||||||
.map(i => crawlFilesRecursively(i.path))
|
|
||||||
.reduce(async (a, b) => (await a).concat(await b), Promise.resolve([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function crawl(
|
|
||||||
suiteDir: string,
|
|
||||||
validate: boolean = true
|
|
||||||
): Promise<TestSuiteListingEntry[]> {
|
|
||||||
if (!fs.existsSync(suiteDir)) {
|
|
||||||
console.error(`Could not find ${suiteDir}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crawl files and convert paths to be POSIX-style, relative to suiteDir.
|
|
||||||
const filesToEnumerate = (await crawlFilesRecursively(suiteDir))
|
|
||||||
.map(f => path.relative(suiteDir, f).replace(/\\/g, '/'))
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
const entries: TestSuiteListingEntry[] = [];
|
|
||||||
for (const file of filesToEnumerate) {
|
|
||||||
// |file| is the suite-relative file path.
|
|
||||||
if (file.endsWith(specFileSuffix)) {
|
|
||||||
const filepathWithoutExtension = file.substring(0, file.length - specFileSuffix.length);
|
|
||||||
|
|
||||||
const suite = path.basename(suiteDir);
|
|
||||||
|
|
||||||
if (validate) {
|
|
||||||
const filename = `../../${suite}/${filepathWithoutExtension}.spec.js`;
|
|
||||||
|
|
||||||
assert(!process.env.STANDALONE_DEV_SERVER);
|
|
||||||
const mod = (await import(filename)) as SpecFile;
|
|
||||||
assert(mod.description !== undefined, 'Test spec file missing description: ' + filename);
|
|
||||||
assert(mod.g !== undefined, 'Test spec file missing TestGroup definition: ' + filename);
|
|
||||||
|
|
||||||
mod.g.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathSegments = filepathWithoutExtension.split('/');
|
|
||||||
for (const p of pathSegments) {
|
|
||||||
assert(validQueryPart.test(p), `Invalid directory name ${p}; must match ${validQueryPart}`);
|
|
||||||
}
|
|
||||||
entries.push({ file: pathSegments });
|
|
||||||
} else if (path.basename(file) === 'README.txt') {
|
|
||||||
const dirname = path.dirname(file);
|
|
||||||
const readme = fs.readFileSync(path.join(suiteDir, file), 'utf8').trim();
|
|
||||||
|
|
||||||
const pathSegments = dirname !== '.' ? dirname.split('/') : [];
|
|
||||||
entries.push({ file: pathSegments, readme });
|
|
||||||
} else {
|
|
||||||
unreachable(`Matched an unrecognized filename ${file}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeListing(filename: string): Promise<TestSuiteListing> {
|
|
||||||
// Don't validate. This path is only used for the dev server and running tests with Node.
|
|
||||||
// Validation is done for listing generation and presubmit.
|
|
||||||
return crawl(path.dirname(filename), false);
|
|
||||||
}
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as os from 'os';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import * as babel from '@babel/core';
|
|
||||||
import * as chokidar from 'chokidar';
|
|
||||||
import * as express from 'express';
|
|
||||||
import * as morgan from 'morgan';
|
|
||||||
import * as portfinder from 'portfinder';
|
|
||||||
import * as serveIndex from 'serve-index';
|
|
||||||
|
|
||||||
import { makeListing } from './crawl.js';
|
|
||||||
|
|
||||||
// Make sure that makeListing doesn't cache imported spec files. See crawl().
|
|
||||||
process.env.STANDALONE_DEV_SERVER = '1';
|
|
||||||
|
|
||||||
const srcDir = path.resolve(__dirname, '../../');
|
|
||||||
|
|
||||||
// Import the project's babel.config.js. We'll use the same config for the runtime compiler.
|
|
||||||
const babelConfig = {
|
|
||||||
...require(path.resolve(srcDir, '../babel.config.js'))({
|
|
||||||
cache: () => {
|
|
||||||
/* not used */
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
sourceMaps: 'inline',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Caches for the generated listing file and compiled TS sources to speed up reloads.
|
|
||||||
// Keyed by suite name
|
|
||||||
const listingCache = new Map<string, string>();
|
|
||||||
// Keyed by the path to the .ts file, without src/
|
|
||||||
const compileCache = new Map<string, string>();
|
|
||||||
|
|
||||||
console.log('Watching changes in', srcDir);
|
|
||||||
const watcher = chokidar.watch(srcDir, {
|
|
||||||
persistent: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to dirty the compile cache for changed .ts files.
|
|
||||||
*/
|
|
||||||
function dirtyCompileCache(absPath: string, stats?: fs.Stats) {
|
|
||||||
const relPath = path.relative(srcDir, absPath);
|
|
||||||
if ((stats === undefined || stats.isFile()) && relPath.endsWith('.ts')) {
|
|
||||||
const tsUrl = relPath;
|
|
||||||
if (compileCache.has(tsUrl)) {
|
|
||||||
console.debug('Dirtying compile cache', tsUrl);
|
|
||||||
}
|
|
||||||
compileCache.delete(tsUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler to dirty the listing cache for:
|
|
||||||
* - Directory changes
|
|
||||||
* - .spec.ts changes
|
|
||||||
* - README.txt changes
|
|
||||||
* Also dirties the compile cache for changed files.
|
|
||||||
*/
|
|
||||||
function dirtyListingAndCompileCache(absPath: string, stats?: fs.Stats) {
|
|
||||||
const relPath = path.relative(srcDir, absPath);
|
|
||||||
|
|
||||||
const segments = relPath.split(path.sep);
|
|
||||||
// The listing changes if the directories change, or if a .spec.ts file is added/removed.
|
|
||||||
const listingChange =
|
|
||||||
// A directory or a file with no extension that we can't stat.
|
|
||||||
// (stat doesn't work for deletions)
|
|
||||||
((path.extname(relPath) === '' && (stats === undefined || !stats.isFile())) ||
|
|
||||||
// A spec file
|
|
||||||
relPath.endsWith('.spec.ts') ||
|
|
||||||
// A README.txt
|
|
||||||
path.basename(relPath, 'txt') === 'README') &&
|
|
||||||
segments.length > 0;
|
|
||||||
if (listingChange) {
|
|
||||||
const suite = segments[0];
|
|
||||||
if (listingCache.has(suite)) {
|
|
||||||
console.debug('Dirtying listing cache', suite);
|
|
||||||
}
|
|
||||||
listingCache.delete(suite);
|
|
||||||
}
|
|
||||||
|
|
||||||
dirtyCompileCache(absPath, stats);
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher.on('add', dirtyListingAndCompileCache);
|
|
||||||
watcher.on('unlink', dirtyListingAndCompileCache);
|
|
||||||
watcher.on('addDir', dirtyListingAndCompileCache);
|
|
||||||
watcher.on('unlinkDir', dirtyListingAndCompileCache);
|
|
||||||
watcher.on('change', dirtyCompileCache);
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Send Chrome Origin Trial tokens
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
res.header('Origin-Trial', [
|
|
||||||
// Token for http://localhost:8080
|
|
||||||
'AvyDIV+RJoYs8fn3W6kIrBhWw0te0klraoz04mw/nPb8VTus3w5HCdy+vXqsSzomIH745CT6B5j1naHgWqt/tw8AAABJeyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjgwODAiLCJmZWF0dXJlIjoiV2ViR1BVIiwiZXhwaXJ5IjoxNjYzNzE4Mzk5fQ==',
|
|
||||||
]);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up logging
|
|
||||||
app.use(morgan('dev'));
|
|
||||||
|
|
||||||
// Serve the standalone runner directory
|
|
||||||
app.use('/standalone', express.static(path.resolve(srcDir, '../standalone')));
|
|
||||||
// Add out-wpt/ build dir for convenience
|
|
||||||
app.use('/out-wpt', express.static(path.resolve(srcDir, '../out-wpt')));
|
|
||||||
app.use('/docs/tsdoc', express.static(path.resolve(srcDir, '../docs/tsdoc')));
|
|
||||||
|
|
||||||
// Serve a suite's listing.js file by crawling the filesystem for all tests.
|
|
||||||
app.get('/out/:suite/listing.js', async (req, res, next) => {
|
|
||||||
const suite = req.params['suite'];
|
|
||||||
|
|
||||||
if (listingCache.has(suite)) {
|
|
||||||
res.setHeader('Content-Type', 'application/javascript');
|
|
||||||
res.send(listingCache.get(suite));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const listing = await makeListing(path.resolve(srcDir, suite, 'listing.ts'));
|
|
||||||
const result = `export const listing = ${JSON.stringify(listing, undefined, 2)}`;
|
|
||||||
|
|
||||||
listingCache.set(suite, result);
|
|
||||||
res.setHeader('Content-Type', 'application/javascript');
|
|
||||||
res.send(result);
|
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serve all other .js files by fetching the source .ts file and compiling it.
|
|
||||||
app.get('/out/**/*.js', async (req, res, next) => {
|
|
||||||
const jsUrl = path.relative('/out', req.url);
|
|
||||||
const tsUrl = jsUrl.replace(/\.js$/, '.ts');
|
|
||||||
if (compileCache.has(tsUrl)) {
|
|
||||||
res.setHeader('Content-Type', 'application/javascript');
|
|
||||||
res.send(compileCache.get(tsUrl));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let absPath = path.join(srcDir, tsUrl);
|
|
||||||
if (!fs.existsSync(absPath)) {
|
|
||||||
// The .ts file doesn't exist. Try .js file in case this is a .js/.d.ts pair.
|
|
||||||
absPath = path.join(srcDir, jsUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await babel.transformFileAsync(absPath, babelConfig);
|
|
||||||
if (result && result.code) {
|
|
||||||
compileCache.set(tsUrl, result.code);
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/javascript');
|
|
||||||
res.send(result.code);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Failed compile ${tsUrl}.`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const host = '0.0.0.0';
|
|
||||||
const port = 8080;
|
|
||||||
// Find an available port, starting at 8080.
|
|
||||||
portfinder.getPort({ host, port }, (err, port) => {
|
|
||||||
if (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
watcher.on('ready', () => {
|
|
||||||
// Listen on the available port.
|
|
||||||
app.listen(port, host, () => {
|
|
||||||
console.log('Standalone test runner running at:');
|
|
||||||
for (const iface of Object.values(os.networkInterfaces())) {
|
|
||||||
for (const details of iface || []) {
|
|
||||||
if (details.family === 'IPv4') {
|
|
||||||
console.log(` http://${details.address}:${port}/standalone/`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serve everything else (not .js) as static, and directories as directory listings.
|
|
||||||
app.use('/out', serveIndex(path.resolve(srcDir, '../src')));
|
|
||||||
app.use('/out', express.static(path.resolve(srcDir, '../src')));
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as process from 'process';
|
|
||||||
|
|
||||||
import { Cacheable, dataCache, setIsBuildingDataCache } from '../framework/data_cache.js';
|
|
||||||
|
|
||||||
function usage(rc: number): void {
|
|
||||||
console.error(`Usage: tools/gen_cache [options] [OUT_DIR] [SUITE_DIRS...]
|
|
||||||
|
|
||||||
For each suite in SUITE_DIRS, pre-compute data that is expensive to generate
|
|
||||||
at runtime and store it under OUT_DIR. If the data file is found then the
|
|
||||||
DataCache will load this instead of building the expensive data at CTS runtime.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help Print this message and exit.
|
|
||||||
--list Print the list of output files without writing them.
|
|
||||||
`);
|
|
||||||
process.exit(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mode: 'emit' | 'list' = 'emit';
|
|
||||||
|
|
||||||
const nonFlagsArgs: string[] = [];
|
|
||||||
for (const a of process.argv) {
|
|
||||||
if (a.startsWith('-')) {
|
|
||||||
if (a === '--list') {
|
|
||||||
mode = 'list';
|
|
||||||
} else if (a === '--help') {
|
|
||||||
usage(0);
|
|
||||||
} else {
|
|
||||||
console.log('unrecognized flag: ', a);
|
|
||||||
usage(1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
nonFlagsArgs.push(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nonFlagsArgs.length < 4) {
|
|
||||||
usage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outRootDir = nonFlagsArgs[2];
|
|
||||||
|
|
||||||
dataCache.setStore({
|
|
||||||
load: (path: string) => {
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
fs.readFile(`data/${path}`, 'utf8', (err, data) => {
|
|
||||||
if (err !== null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setIsBuildingDataCache();
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
for (const suiteDir of nonFlagsArgs.slice(3)) {
|
|
||||||
await build(suiteDir);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
const specFileSuffix = __filename.endsWith('.ts') ? '.spec.ts' : '.spec.js';
|
|
||||||
|
|
||||||
async function crawlFilesRecursively(dir: string): Promise<string[]> {
|
|
||||||
const subpathInfo = await Promise.all(
|
|
||||||
(await fs.promises.readdir(dir)).map(async d => {
|
|
||||||
const p = path.join(dir, d);
|
|
||||||
const stats = await fs.promises.stat(p);
|
|
||||||
return {
|
|
||||||
path: p,
|
|
||||||
isDirectory: stats.isDirectory(),
|
|
||||||
isFile: stats.isFile(),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const files = subpathInfo
|
|
||||||
.filter(i => i.isFile && i.path.endsWith(specFileSuffix))
|
|
||||||
.map(i => i.path);
|
|
||||||
|
|
||||||
return files.concat(
|
|
||||||
await subpathInfo
|
|
||||||
.filter(i => i.isDirectory)
|
|
||||||
.map(i => crawlFilesRecursively(i.path))
|
|
||||||
.reduce(async (a, b) => (await a).concat(await b), Promise.resolve([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function build(suiteDir: string) {
|
|
||||||
if (!fs.existsSync(suiteDir)) {
|
|
||||||
console.error(`Could not find ${suiteDir}`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crawl files and convert paths to be POSIX-style, relative to suiteDir.
|
|
||||||
const filesToEnumerate = (await crawlFilesRecursively(suiteDir)).sort();
|
|
||||||
|
|
||||||
const cacheablePathToTS = new Map<string, string>();
|
|
||||||
|
|
||||||
for (const file of filesToEnumerate) {
|
|
||||||
if (file.endsWith(specFileSuffix)) {
|
|
||||||
const pathWithoutExtension = file.substring(0, file.length - specFileSuffix.length);
|
|
||||||
const mod = await import(`../../../${pathWithoutExtension}.spec.js`);
|
|
||||||
if (mod.d?.serialize !== undefined) {
|
|
||||||
const cacheable = mod.d as Cacheable<unknown>;
|
|
||||||
|
|
||||||
{
|
|
||||||
// Check for collisions
|
|
||||||
const existing = cacheablePathToTS.get(cacheable.path);
|
|
||||||
if (existing !== undefined) {
|
|
||||||
console.error(
|
|
||||||
`error: Cacheable '${cacheable.path}' is emitted by both:
|
|
||||||
'${existing}'
|
|
||||||
and
|
|
||||||
'${file}'`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
cacheablePathToTS.set(cacheable.path, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const outPath = `${outRootDir}/data/${cacheable.path}`;
|
|
||||||
|
|
||||||
switch (mode) {
|
|
||||||
case 'emit': {
|
|
||||||
const data = await cacheable.build();
|
|
||||||
const serialized = cacheable.serialize(data);
|
|
||||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
||||||
fs.writeFileSync(outPath, serialized);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'list': {
|
|
||||||
console.log(outPath);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import * as process from 'process';
|
|
||||||
|
|
||||||
import { crawl } from './crawl.js';
|
|
||||||
|
|
||||||
function usage(rc: number): void {
|
|
||||||
console.error(`Usage: tools/gen_listings [options] [OUT_DIR] [SUITE_DIRS...]
|
|
||||||
|
|
||||||
For each suite in SUITE_DIRS, generate listings and write each listing.js
|
|
||||||
into OUT_DIR/{suite}/listing.js. Example:
|
|
||||||
tools/gen_listings out/ src/unittests/ src/webgpu/
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--help Print this message and exit.
|
|
||||||
--no-validate Whether to validate test modules while crawling.
|
|
||||||
`);
|
|
||||||
process.exit(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
const argv = process.argv;
|
|
||||||
if (argv.indexOf('--help') !== -1) {
|
|
||||||
usage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let validate = true;
|
|
||||||
{
|
|
||||||
const i = argv.indexOf('--no-validate');
|
|
||||||
if (i !== -1) {
|
|
||||||
validate = false;
|
|
||||||
argv.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (argv.length < 4) {
|
|
||||||
usage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const myself = 'src/common/tools/gen_listings.ts';
|
|
||||||
|
|
||||||
const outDir = argv[2];
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
for (const suiteDir of argv.slice(3)) {
|
|
||||||
const listing = await crawl(suiteDir, validate);
|
|
||||||
|
|
||||||
const suite = path.basename(suiteDir);
|
|
||||||
const outFile = path.normalize(path.join(outDir, `${suite}/listing.js`));
|
|
||||||
fs.mkdirSync(path.join(outDir, suite), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
outFile,
|
|
||||||
`\
|
|
||||||
// AUTO-GENERATED - DO NOT EDIT. See ${myself}.
|
|
||||||
|
|
||||||
export const listing = ${JSON.stringify(listing, undefined, 2)};
|
|
||||||
`
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(outFile + '.map');
|
|
||||||
} catch (ex) {
|
|
||||||
// ignore if file didn't exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
|
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { TestQueryMultiFile } from '../internal/query/query.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
function printUsageAndExit(rc: number): void {
|
|
||||||
console.error(`\
|
|
||||||
Usage:
|
|
||||||
tools/gen_wpt_cts_html OUTPUT_FILE TEMPLATE_FILE [ARGUMENTS_PREFIXES_FILE EXPECTATIONS_FILE EXPECTATIONS_PREFIX [SUITE]]
|
|
||||||
tools/gen_wpt_cts_html out-wpt/cts.https.html templates/cts.https.html
|
|
||||||
tools/gen_wpt_cts_html my/path/to/cts.https.html templates/cts.https.html arguments.txt myexpectations.txt 'path/to/cts.https.html' cts
|
|
||||||
|
|
||||||
where arguments.txt is a file containing a list of arguments prefixes to both generate and expect
|
|
||||||
in the expectations. The entire variant list generation runs *once per prefix*, so this
|
|
||||||
multiplies the size of the variant list.
|
|
||||||
|
|
||||||
?worker=0&q=
|
|
||||||
?worker=1&q=
|
|
||||||
|
|
||||||
and myexpectations.txt is a file containing a list of WPT paths to suppress, e.g.:
|
|
||||||
|
|
||||||
path/to/cts.https.html?worker=0&q=webgpu:a/foo:bar={"x":1}
|
|
||||||
path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":1}
|
|
||||||
|
|
||||||
path/to/cts.https.html?worker=1&q=webgpu:a/foo:bar={"x":3}
|
|
||||||
`);
|
|
||||||
process.exit(rc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.argv.length !== 4 && process.argv.length !== 7 && process.argv.length !== 8) {
|
|
||||||
printUsageAndExit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [
|
|
||||||
,
|
|
||||||
,
|
|
||||||
outFile,
|
|
||||||
templateFile,
|
|
||||||
argsPrefixesFile,
|
|
||||||
expectationsFile,
|
|
||||||
expectationsPrefix,
|
|
||||||
suite = 'webgpu',
|
|
||||||
] = process.argv;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
let argsPrefixes = [''];
|
|
||||||
let expectationLines = new Set<string>();
|
|
||||||
|
|
||||||
if (process.argv.length >= 7) {
|
|
||||||
// Prefixes sorted from longest to shortest
|
|
||||||
const argsPrefixesFromFile = (await fs.readFile(argsPrefixesFile, 'utf8'))
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.filter(a => a.length)
|
|
||||||
.sort((a, b) => b.length - a.length);
|
|
||||||
if (argsPrefixesFromFile.length) argsPrefixes = argsPrefixesFromFile;
|
|
||||||
expectationLines = new Set(
|
|
||||||
(await fs.readFile(expectationsFile, 'utf8')).split(/\r?\n/).filter(l => l.length)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectations: Map<string, string[]> = new Map();
|
|
||||||
for (const prefix of argsPrefixes) {
|
|
||||||
expectations.set(prefix, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
expLoop: for (const exp of expectationLines) {
|
|
||||||
// Take each expectation for the longest prefix it matches.
|
|
||||||
for (const argsPrefix of argsPrefixes) {
|
|
||||||
const prefix = expectationsPrefix + argsPrefix;
|
|
||||||
if (exp.startsWith(prefix)) {
|
|
||||||
expectations.get(argsPrefix)!.push(exp.substring(prefix.length));
|
|
||||||
continue expLoop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('note: ignored expectation: ' + exp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
const lines: Array<string | undefined> = [];
|
|
||||||
for (const prefix of argsPrefixes) {
|
|
||||||
const rootQuery = new TestQueryMultiFile(suite, []);
|
|
||||||
const tree = await loader.loadTree(rootQuery, expectations.get(prefix));
|
|
||||||
|
|
||||||
lines.push(undefined); // output blank line between prefixes
|
|
||||||
const alwaysExpandThroughLevel = 2; // expand to, at minimum, every test.
|
|
||||||
for (const { query } of tree.iterateCollapsedNodes({ alwaysExpandThroughLevel })) {
|
|
||||||
const urlQueryString = prefix + query.toString(); // "?worker=0&q=..."
|
|
||||||
// Check for a safe-ish path length limit. Filename must be <= 255, and on Windows the whole
|
|
||||||
// path must be <= 259. Leave room for e.g.:
|
|
||||||
// 'c:\b\s\w\xxxxxxxx\layout-test-results\external\wpt\webgpu\cts_worker=0_q=...-actual.txt'
|
|
||||||
assert(
|
|
||||||
urlQueryString.length < 185,
|
|
||||||
'Generated test variant would produce too-long -actual.txt filename. \
|
|
||||||
Try broadening suppressions to avoid long test variant names. ' +
|
|
||||||
urlQueryString
|
|
||||||
);
|
|
||||||
lines.push(urlQueryString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await generateFile(lines);
|
|
||||||
})().catch(ex => {
|
|
||||||
console.log(ex.stack ?? ex.toString());
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function generateFile(lines: Array<string | undefined>): Promise<void> {
|
|
||||||
let result = '';
|
|
||||||
result += '<!-- AUTO-GENERATED - DO NOT EDIT. See WebGPU CTS: tools/gen_wpt_cts_html. -->\n';
|
|
||||||
|
|
||||||
result += await fs.readFile(templateFile, 'utf8');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line === undefined) {
|
|
||||||
result += '\n';
|
|
||||||
} else {
|
|
||||||
result += `<meta name=variant content='${line}'>\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.writeFile(outFile, result);
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
|
|
||||||
import { Page } from 'playwright-core';
|
|
||||||
import { PNG } from 'pngjs';
|
|
||||||
import { screenshot, WindowInfo } from 'screenshot-ftw';
|
|
||||||
|
|
||||||
// eslint-disable-next-line ban/ban
|
|
||||||
const waitMS = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
export function readPng(filename: string) {
|
|
||||||
const data = fs.readFileSync(filename);
|
|
||||||
return PNG.sync.read(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writePng(filename: string, width: number, height: number, data: Buffer) {
|
|
||||||
const png = new PNG({ colorType: 6, width, height });
|
|
||||||
for (let i = 0; i < data.byteLength; ++i) {
|
|
||||||
png.data[i] = data[i];
|
|
||||||
}
|
|
||||||
const buffer = PNG.sync.write(png);
|
|
||||||
fs.writeFileSync(filename, buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ScreenshotManager {
|
|
||||||
window?: WindowInfo;
|
|
||||||
|
|
||||||
async init(page: Page) {
|
|
||||||
// set the title to some random number so we can find the window by title
|
|
||||||
const title: string = await page.evaluate(() => {
|
|
||||||
const title = `t-${Math.random()}`;
|
|
||||||
document.title = title;
|
|
||||||
return title;
|
|
||||||
});
|
|
||||||
|
|
||||||
// wait for the window to show up
|
|
||||||
let window;
|
|
||||||
for (let i = 0; !window && i < 100; ++i) {
|
|
||||||
await waitMS(50);
|
|
||||||
const windows = await screenshot.getWindows();
|
|
||||||
window = windows.find(window => window.title.includes(title));
|
|
||||||
}
|
|
||||||
if (!window) {
|
|
||||||
throw Error(`could not find window: ${title}`);
|
|
||||||
}
|
|
||||||
this.window = window;
|
|
||||||
}
|
|
||||||
|
|
||||||
async takeScreenshot(page: Page, screenshotName: string) {
|
|
||||||
// await page.screenshot({ path: screenshotName });
|
|
||||||
|
|
||||||
// we need to set the url and title since the screenshot will include the chrome
|
|
||||||
await page.evaluate(async () => {
|
|
||||||
document.title = 'screenshot';
|
|
||||||
window.history.replaceState({}, '', '/screenshot');
|
|
||||||
});
|
|
||||||
await screenshot.captureWindowById(screenshotName, this.window!.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { DefaultTestFileLoader } from '../internal/file_loader.js';
|
|
||||||
import { parseQuery } from '../internal/query/parseQuery.js';
|
|
||||||
import { assert } from '../util/util.js';
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
for (const suite of ['unittests', 'webgpu']) {
|
|
||||||
const loader = new DefaultTestFileLoader();
|
|
||||||
const filterQuery = parseQuery(`${suite}:*`);
|
|
||||||
const testcases = await loader.loadCases(filterQuery);
|
|
||||||
for (const testcase of testcases) {
|
|
||||||
const name = testcase.query.toString();
|
|
||||||
const maxLength = 375;
|
|
||||||
assert(
|
|
||||||
name.length <= maxLength,
|
|
||||||
`Testcase ${name} is too long. Max length is ${maxLength} characters. Please shorten names or reduce parameters.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,446 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { chromium, firefox, webkit, Page, Browser } from 'playwright-core';
|
|
||||||
|
|
||||||
import { ScreenshotManager, readPng, writePng } from './image_utils.js';
|
|
||||||
|
|
||||||
declare function wptRefTestPageReady(): boolean;
|
|
||||||
declare function wptRefTestGetTimeout(): boolean;
|
|
||||||
|
|
||||||
const verbose = !!process.env.VERBOSE;
|
|
||||||
const kRefTestsBaseURL = 'http://localhost:8080/out/webgpu/web_platform/reftests';
|
|
||||||
const kRefTestsPath = 'src/webgpu/web_platform/reftests';
|
|
||||||
const kScreenshotPath = 'out-wpt-reftest-screenshots';
|
|
||||||
|
|
||||||
// note: technically we should use an HTML parser to find this to deal with whitespace
|
|
||||||
// attribute order, quotes, entities, etc but since we control the test source we can just
|
|
||||||
// make sure they match
|
|
||||||
const kRefLinkRE = /<link\s+rel="match"\s+href="(.*?)"/;
|
|
||||||
const kRefWaitClassRE = /class="reftest-wait"/;
|
|
||||||
const kFuzzy = /<meta\s+name="?fuzzy"?\s+content="(.*?)">/;
|
|
||||||
|
|
||||||
function printUsage() {
|
|
||||||
console.log(`
|
|
||||||
run_wpt_ref_tests path-to-browser-executable [ref-test-name]
|
|
||||||
|
|
||||||
where ref-test-name is just a simple check for the test including the given string.
|
|
||||||
If not passed all ref tests are run
|
|
||||||
|
|
||||||
MacOS Chrome Example:
|
|
||||||
node tools/run_wpt_ref_tests /Applications/Google\\ Chrome\\ Canary.app/Contents/MacOS/Google\\ Chrome\\ Canary
|
|
||||||
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all of filenames that end with '.html'
|
|
||||||
function getRefTestNames(refTestPath: string) {
|
|
||||||
return fs.readdirSync(refTestPath).filter(name => name.endsWith('.html'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a regex with one capture, return it or the empty string if no match.
|
|
||||||
function getRegexMatchCapture(re: RegExp, content: string) {
|
|
||||||
const m = re.exec(content);
|
|
||||||
return m ? m[1] : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileInfo = {
|
|
||||||
content: string;
|
|
||||||
refLink: string;
|
|
||||||
refWait: boolean;
|
|
||||||
fuzzy: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function readHTMLFile(filename: string): FileInfo {
|
|
||||||
const content = fs.readFileSync(filename, { encoding: 'utf8' });
|
|
||||||
return {
|
|
||||||
content,
|
|
||||||
refLink: getRegexMatchCapture(kRefLinkRE, content),
|
|
||||||
refWait: kRefWaitClassRE.test(content),
|
|
||||||
fuzzy: getRegexMatchCapture(kFuzzy, content),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is workaround for a bug in Chrome. The bug is when in emulation mode
|
|
||||||
* Chrome lets you set a devicePixelRatio but Chrome still renders in the
|
|
||||||
* actual devicePixelRatio, at least on MacOS.
|
|
||||||
* So, we compute the ratio and then use that.
|
|
||||||
*/
|
|
||||||
async function getComputedDevicePixelRatio(browser: Browser): Promise<number> {
|
|
||||||
const context = await browser.newContext();
|
|
||||||
const page = await context.newPage();
|
|
||||||
await page.goto('data:text/html,<html></html>');
|
|
||||||
await page.waitForLoadState('networkidle');
|
|
||||||
const devicePixelRatio = await page.evaluate(() => {
|
|
||||||
let resolve: (v: number) => void;
|
|
||||||
const promise = new Promise(_resolve => (resolve = _resolve));
|
|
||||||
const observer = new ResizeObserver(entries => {
|
|
||||||
const devicePixelWidth = entries[0].devicePixelContentBoxSize[0].inlineSize;
|
|
||||||
const clientWidth = entries[0].target.clientWidth;
|
|
||||||
const devicePixelRatio = devicePixelWidth / clientWidth;
|
|
||||||
resolve(devicePixelRatio);
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement);
|
|
||||||
return promise;
|
|
||||||
});
|
|
||||||
await page.close();
|
|
||||||
await context.close();
|
|
||||||
return devicePixelRatio as number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: If possible, rather then start adding command line options to this tool,
|
|
||||||
// see if you can just make it work based off the path.
|
|
||||||
async function getBrowserInterface(executablePath: string) {
|
|
||||||
const lc = executablePath.toLowerCase();
|
|
||||||
if (lc.includes('chrom')) {
|
|
||||||
const browser = await chromium.launch({
|
|
||||||
executablePath,
|
|
||||||
headless: false,
|
|
||||||
args: ['--enable-unsafe-webgpu'],
|
|
||||||
});
|
|
||||||
const devicePixelRatio = await getComputedDevicePixelRatio(browser);
|
|
||||||
const context = await browser.newContext({
|
|
||||||
deviceScaleFactor: devicePixelRatio,
|
|
||||||
});
|
|
||||||
return { browser, context };
|
|
||||||
} else if (lc.includes('firefox')) {
|
|
||||||
const browser = await firefox.launch({
|
|
||||||
executablePath,
|
|
||||||
headless: false,
|
|
||||||
});
|
|
||||||
const context = await browser.newContext();
|
|
||||||
return { browser, context };
|
|
||||||
} else if (lc.includes('safari') || lc.includes('webkit')) {
|
|
||||||
const browser = await webkit.launch({
|
|
||||||
executablePath,
|
|
||||||
headless: false,
|
|
||||||
});
|
|
||||||
const context = await browser.newContext();
|
|
||||||
return { browser, context };
|
|
||||||
} else {
|
|
||||||
throw new Error(`could not guess browser from executable path: ${executablePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parses a fuzzy spec as defined here
|
|
||||||
// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
|
|
||||||
// Note: This is not robust but the tests will eventually be run in the real wpt.
|
|
||||||
function parseFuzzy(fuzzy: string) {
|
|
||||||
if (!fuzzy) {
|
|
||||||
return { maxDifference: [0, 0], totalPixels: [0, 0] };
|
|
||||||
} else {
|
|
||||||
const parts = fuzzy.split(';');
|
|
||||||
if (parts.length !== 2) {
|
|
||||||
throw Error(`unhandled fuzzy format: ${fuzzy}`);
|
|
||||||
}
|
|
||||||
const ranges = parts.map(part => {
|
|
||||||
const range = part
|
|
||||||
.replace(/[a-zA-Z=]/g, '')
|
|
||||||
.split('-')
|
|
||||||
.map(v => parseInt(v));
|
|
||||||
return range.length === 1 ? [0, range[0]] : range;
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
maxDifference: ranges[0],
|
|
||||||
totalPixels: ranges[1],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compares two images using the algorithm described in the web platform tests
|
|
||||||
// https://web-platform-tests.org/writing-tests/reftests.html#fuzzy-matching
|
|
||||||
// If they are different will write out a diff mask.
|
|
||||||
async function compareImages(
|
|
||||||
filename1: string,
|
|
||||||
filename2: string,
|
|
||||||
fuzzy: string,
|
|
||||||
diffName: string,
|
|
||||||
startingRow: number = 0
|
|
||||||
) {
|
|
||||||
const img1 = readPng(filename1);
|
|
||||||
const img2 = readPng(filename2);
|
|
||||||
const { width, height } = img1;
|
|
||||||
if (img2.width !== width || img2.height !== height) {
|
|
||||||
console.error('images are not the same size:', filename1, filename2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { maxDifference, totalPixels } = parseFuzzy(fuzzy);
|
|
||||||
|
|
||||||
const diffData = Buffer.alloc(width * height * 4);
|
|
||||||
const diffPixels = new Uint32Array(diffData.buffer);
|
|
||||||
const kRed = 0xff0000ff;
|
|
||||||
const kWhite = 0xffffffff;
|
|
||||||
const kYellow = 0xff00ffff;
|
|
||||||
|
|
||||||
let numPixelsDifferent = 0;
|
|
||||||
let anyPixelsOutOfRange = false;
|
|
||||||
for (let y = startingRow; y < height; ++y) {
|
|
||||||
for (let x = 0; x < width; ++x) {
|
|
||||||
const offset = y * width + x;
|
|
||||||
let isDifferent = false;
|
|
||||||
let outOfRange = false;
|
|
||||||
for (let c = 0; c < 4 && !outOfRange; ++c) {
|
|
||||||
const off = offset * 4 + c;
|
|
||||||
const v0 = img1.data[off];
|
|
||||||
const v1 = img2.data[off];
|
|
||||||
const channelDiff = Math.abs(v0 - v1);
|
|
||||||
outOfRange ||= channelDiff < maxDifference[0] || channelDiff > maxDifference[1];
|
|
||||||
isDifferent ||= channelDiff > 0;
|
|
||||||
}
|
|
||||||
numPixelsDifferent += isDifferent ? 1 : 0;
|
|
||||||
anyPixelsOutOfRange ||= outOfRange;
|
|
||||||
diffPixels[offset] = outOfRange ? kRed : isDifferent ? kYellow : kWhite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pass =
|
|
||||||
!anyPixelsOutOfRange &&
|
|
||||||
numPixelsDifferent >= totalPixels[0] &&
|
|
||||||
numPixelsDifferent <= totalPixels[1];
|
|
||||||
if (!pass) {
|
|
||||||
writePng(diffName, width, height, diffData);
|
|
||||||
console.error(
|
|
||||||
`FAIL: too many differences in: ${filename1} vs ${filename2}
|
|
||||||
${numPixelsDifferent} differences, expected: ${totalPixels[0]}-${totalPixels[1]} with range: ${maxDifference[0]}-${maxDifference[1]}
|
|
||||||
wrote difference to: ${diffName};
|
|
||||||
`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(`PASS`);
|
|
||||||
}
|
|
||||||
return pass;
|
|
||||||
}
|
|
||||||
|
|
||||||
function exists(filename: string) {
|
|
||||||
try {
|
|
||||||
fs.accessSync(filename);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForPageRender(page: Page) {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
return new Promise(resolve => requestAnimationFrame(resolve));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if the page timed out.
|
|
||||||
async function runPage(page: Page, url: string, refWait: boolean) {
|
|
||||||
console.log(' loading:', url);
|
|
||||||
// we need to load about:blank to force the browser to re-render
|
|
||||||
// else the previous page may still be visible if the page we are loading fails
|
|
||||||
await page.goto('about:blank');
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
await waitForPageRender(page);
|
|
||||||
|
|
||||||
await page.goto(url);
|
|
||||||
await page.waitForLoadState('domcontentloaded');
|
|
||||||
await waitForPageRender(page);
|
|
||||||
|
|
||||||
if (refWait) {
|
|
||||||
await page.waitForFunction(() => wptRefTestPageReady());
|
|
||||||
const timeout = await page.evaluate(() => wptRefTestGetTimeout());
|
|
||||||
if (timeout) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
if (args.length < 1 || args.length > 2) {
|
|
||||||
printUsage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [executablePath, refTestName] = args;
|
|
||||||
|
|
||||||
if (!exists(executablePath)) {
|
|
||||||
console.error(executablePath, 'does not exist');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const testNames = getRefTestNames(kRefTestsPath).filter(name =>
|
|
||||||
refTestName ? name.includes(refTestName) : true
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exists(kScreenshotPath)) {
|
|
||||||
fs.mkdirSync(kScreenshotPath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (testNames.length === 0) {
|
|
||||||
console.error(`no tests include "${refTestName}"`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { browser, context } = await getBrowserInterface(executablePath);
|
|
||||||
const page = await context.newPage();
|
|
||||||
|
|
||||||
const screenshotManager = new ScreenshotManager();
|
|
||||||
await screenshotManager.init(page);
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
page.on('console', async msg => {
|
|
||||||
const { url, lineNumber, columnNumber } = msg.location();
|
|
||||||
const values = await Promise.all(msg.args().map(a => a.jsonValue()));
|
|
||||||
console.log(`${url}:${lineNumber}:${columnNumber}:`, ...values);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.addInitScript({
|
|
||||||
content: `
|
|
||||||
(() => {
|
|
||||||
let timeout = false;
|
|
||||||
setTimeout(() => timeout = true, 5000);
|
|
||||||
|
|
||||||
window.wptRefTestPageReady = function() {
|
|
||||||
return timeout || !document.documentElement.classList.contains('reftest-wait');
|
|
||||||
};
|
|
||||||
|
|
||||||
window.wptRefTestGetTimeout = function() {
|
|
||||||
return timeout;
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
|
|
||||||
type Result = {
|
|
||||||
status: string;
|
|
||||||
testName: string;
|
|
||||||
refName: string;
|
|
||||||
testScreenshotName: string;
|
|
||||||
refScreenshotName: string;
|
|
||||||
diffName: string;
|
|
||||||
};
|
|
||||||
const results: Result[] = [];
|
|
||||||
const addResult = (
|
|
||||||
status: string,
|
|
||||||
testName: string,
|
|
||||||
refName: string,
|
|
||||||
testScreenshotName: string = '',
|
|
||||||
refScreenshotName: string = '',
|
|
||||||
diffName: string = ''
|
|
||||||
) => {
|
|
||||||
results.push({ status, testName, refName, testScreenshotName, refScreenshotName, diffName });
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const testName of testNames) {
|
|
||||||
console.log('processing:', testName);
|
|
||||||
const { refLink, refWait, fuzzy } = readHTMLFile(path.join(kRefTestsPath, testName));
|
|
||||||
if (!refLink) {
|
|
||||||
throw new Error(`could not find ref link in: ${testName}`);
|
|
||||||
}
|
|
||||||
const testURL = `${kRefTestsBaseURL}/${testName}`;
|
|
||||||
const refURL = `${kRefTestsBaseURL}/${refLink}`;
|
|
||||||
|
|
||||||
// Technically this is not correct but it fits the existing tests.
|
|
||||||
// It assumes refLink is relative to the refTestsPath but it's actually
|
|
||||||
// supposed to be relative to the test. It might also be an absolute
|
|
||||||
// path. Neither of those cases exist at the time of writing this.
|
|
||||||
const refFileInfo = readHTMLFile(path.join(kRefTestsPath, refLink));
|
|
||||||
const testScreenshotName = path.join(kScreenshotPath, `${testName}-actual.png`);
|
|
||||||
const refScreenshotName = path.join(kScreenshotPath, `${testName}-expected.png`);
|
|
||||||
const diffName = path.join(kScreenshotPath, `${testName}-diff.png`);
|
|
||||||
|
|
||||||
const timeoutTest = await runPage(page, testURL, refWait);
|
|
||||||
if (timeoutTest) {
|
|
||||||
addResult('TIMEOUT', testName, refLink);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await screenshotManager.takeScreenshot(page, testScreenshotName);
|
|
||||||
|
|
||||||
const timeoutRef = await runPage(page, refURL, refFileInfo.refWait);
|
|
||||||
if (timeoutRef) {
|
|
||||||
addResult('TIMEOUT', testName, refLink);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await screenshotManager.takeScreenshot(page, refScreenshotName);
|
|
||||||
|
|
||||||
const pass = await compareImages(testScreenshotName, refScreenshotName, fuzzy, diffName);
|
|
||||||
addResult(
|
|
||||||
pass ? 'PASS' : 'FAILURE',
|
|
||||||
testName,
|
|
||||||
refLink,
|
|
||||||
testScreenshotName,
|
|
||||||
refScreenshotName,
|
|
||||||
diffName
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`----results----\n${results
|
|
||||||
.map(({ status, testName }) => `[ ${status.padEnd(7)} ] ${testName}`)
|
|
||||||
.join('\n')}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const imgLink = (filename: string, title: string) => {
|
|
||||||
const name = path.basename(filename);
|
|
||||||
return `
|
|
||||||
<div class="screenshot">
|
|
||||||
${title}
|
|
||||||
<a href="${name}" title="${name}">
|
|
||||||
<img src="${name}" width="256"/>
|
|
||||||
</a>
|
|
||||||
</div>`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const indexName = path.join(kScreenshotPath, 'index.html');
|
|
||||||
fs.writeFileSync(
|
|
||||||
indexName,
|
|
||||||
`<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
.screenshot {
|
|
||||||
display: inline-block;
|
|
||||||
background: #CCC;
|
|
||||||
margin-right: 5px;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
.screenshot a {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.screenshot
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${results
|
|
||||||
.map(({ status, testName, refName, testScreenshotName, refScreenshotName, diffName }) => {
|
|
||||||
return `
|
|
||||||
<div>
|
|
||||||
<div>[ ${status} ]: ${testName} ref: ${refName}</div>
|
|
||||||
${
|
|
||||||
status === 'FAILURE'
|
|
||||||
? `${imgLink(testScreenshotName, 'actual')}
|
|
||||||
${imgLink(refScreenshotName, 'ref')}
|
|
||||||
${imgLink(diffName, 'diff')}`
|
|
||||||
: ``
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<hr>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join('\n')}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
// the file:// with an absolute path makes it clickable in some terminals
|
|
||||||
console.log(`\nsee: file://${path.resolve(indexName)}\n`);
|
|
||||||
|
|
||||||
await page.close();
|
|
||||||
await context.close();
|
|
||||||
// I have no idea why it's taking ~30 seconds for playwright to close.
|
|
||||||
console.log('-- [ done: waiting for browser to close ] --');
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(e => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Automatically transpile .ts imports
|
|
||||||
require('ts-node').register({
|
|
||||||
// Specify the project file so ts-node doesn't try to find it itself based on the CWD.
|
|
||||||
project: path.resolve(__dirname, '../../../tsconfig.json'),
|
|
||||||
compilerOptions: {
|
|
||||||
module: 'commonjs',
|
|
||||||
},
|
|
||||||
transpileOnly: true,
|
|
||||||
});
|
|
||||||
const Module = require('module');
|
|
||||||
|
|
||||||
// Redirect imports of .js files to .ts files
|
|
||||||
const resolveFilename = Module._resolveFilename;
|
|
||||||
Module._resolveFilename = (request, parentModule, isMain) => {
|
|
||||||
do {
|
|
||||||
if (request.startsWith('.') && parentModule.filename.endsWith('.ts')) {
|
|
||||||
// Required for browser (because it needs the actual correct file path and
|
|
||||||
// can't do any kind of file resolution).
|
|
||||||
if (request.endsWith('/index.js')) {
|
|
||||||
throw new Error(
|
|
||||||
"Avoid the name `index.js`; we don't have Node-style path resolution: " + request
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import of Node addon modules are valid and should pass through.
|
|
||||||
if (request.endsWith('.node')) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!request.endsWith('.js')) {
|
|
||||||
throw new Error('All relative imports must end in .js: ' + request);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const tsRequest = request.substring(0, request.length - '.js'.length) + '.ts';
|
|
||||||
return resolveFilename.call(this, tsRequest, parentModule, isMain);
|
|
||||||
} catch (ex) {
|
|
||||||
// If the .ts file doesn't exist, try .js instead.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (0);
|
|
||||||
|
|
||||||
return resolveFilename.call(this, request, parentModule, isMain);
|
|
||||||
};
|
|
||||||
|
|
||||||
process.on('unhandledRejection', ex => {
|
|
||||||
throw ex;
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export const version = require('child_process')
|
|
||||||
.execSync('git describe --always --abbrev=0 --dirty')
|
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import { resolveOnTimeout } from './util.js';
|
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
declare const Components: any;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to trigger JavaScript garbage collection, either using explicit methods if exposed
|
|
||||||
* (may be available in testing environments with special browser runtime flags set), or using
|
|
||||||
* some weird tricks to incur GC pressure. Adopted from the WebGL CTS.
|
|
||||||
*/
|
|
||||||
export async function attemptGarbageCollection(): Promise<void> {
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
const w: any = globalThis;
|
|
||||||
if (w.GCController) {
|
|
||||||
w.GCController.collect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (w.opera && w.opera.collect) {
|
|
||||||
w.opera.collect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
w.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
|
|
||||||
.getInterface(Components.interfaces.nsIDOMWindowUtils)
|
|
||||||
.garbageCollect();
|
|
||||||
return;
|
|
||||||
} catch (e) {
|
|
||||||
// ignore any failure
|
|
||||||
}
|
|
||||||
|
|
||||||
if (w.gc) {
|
|
||||||
w.gc();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (w.CollectGarbage) {
|
|
||||||
w.CollectGarbage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let i: number;
|
|
||||||
function gcRec(n: number): void {
|
|
||||||
if (n < 1) return;
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
|
||||||
let temp: object | string = { i: 'ab' + i + i / 100000 };
|
|
||||||
/* eslint-disable @typescript-eslint/restrict-plus-operands */
|
|
||||||
temp = temp + 'foo';
|
|
||||||
temp; // dummy use of unused variable
|
|
||||||
gcRec(n - 1);
|
|
||||||
}
|
|
||||||
for (i = 0; i < 1000; i++) {
|
|
||||||
gcRec(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolveOnTimeout(35); // Let the event loop run a few frames in case it helps.
|
|
||||||
}
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
/**
|
|
||||||
* The interface used for formatting strings to contain color metadata.
|
|
||||||
*
|
|
||||||
* Use the interface properties to construct a style, then use the
|
|
||||||
* `(s: string): string` function to format the provided string with the given
|
|
||||||
* style.
|
|
||||||
*/
|
|
||||||
export interface Colors {
|
|
||||||
// Are colors enabled?
|
|
||||||
enabled: boolean;
|
|
||||||
|
|
||||||
// Returns the string formatted to contain the specified color or style.
|
|
||||||
(s: string): string;
|
|
||||||
|
|
||||||
// modifiers
|
|
||||||
reset: Colors;
|
|
||||||
bold: Colors;
|
|
||||||
dim: Colors;
|
|
||||||
italic: Colors;
|
|
||||||
underline: Colors;
|
|
||||||
inverse: Colors;
|
|
||||||
hidden: Colors;
|
|
||||||
strikethrough: Colors;
|
|
||||||
|
|
||||||
// colors
|
|
||||||
black: Colors;
|
|
||||||
red: Colors;
|
|
||||||
green: Colors;
|
|
||||||
yellow: Colors;
|
|
||||||
blue: Colors;
|
|
||||||
magenta: Colors;
|
|
||||||
cyan: Colors;
|
|
||||||
white: Colors;
|
|
||||||
gray: Colors;
|
|
||||||
grey: Colors;
|
|
||||||
|
|
||||||
// bright colors
|
|
||||||
blackBright: Colors;
|
|
||||||
redBright: Colors;
|
|
||||||
greenBright: Colors;
|
|
||||||
yellowBright: Colors;
|
|
||||||
blueBright: Colors;
|
|
||||||
magentaBright: Colors;
|
|
||||||
cyanBright: Colors;
|
|
||||||
whiteBright: Colors;
|
|
||||||
|
|
||||||
// background colors
|
|
||||||
bgBlack: Colors;
|
|
||||||
bgRed: Colors;
|
|
||||||
bgGreen: Colors;
|
|
||||||
bgYellow: Colors;
|
|
||||||
bgBlue: Colors;
|
|
||||||
bgMagenta: Colors;
|
|
||||||
bgCyan: Colors;
|
|
||||||
bgWhite: Colors;
|
|
||||||
|
|
||||||
// bright background colors
|
|
||||||
bgBlackBright: Colors;
|
|
||||||
bgRedBright: Colors;
|
|
||||||
bgGreenBright: Colors;
|
|
||||||
bgYellowBright: Colors;
|
|
||||||
bgBlueBright: Colors;
|
|
||||||
bgMagentaBright: Colors;
|
|
||||||
bgCyanBright: Colors;
|
|
||||||
bgWhiteBright: Colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interface used for formatting strings with color metadata.
|
|
||||||
*
|
|
||||||
* Currently Colors will use the 'ansi-colors' module if it can be loaded.
|
|
||||||
* If it cannot be loaded, then the Colors implementation is a straight pass-through.
|
|
||||||
*
|
|
||||||
* Colors may also be a no-op if the current environment does not support colors.
|
|
||||||
*/
|
|
||||||
export let Colors: Colors;
|
|
||||||
|
|
||||||
try {
|
|
||||||
/* eslint-disable-next-line node/no-unpublished-require */
|
|
||||||
Colors = require('ansi-colors') as Colors;
|
|
||||||
} catch {
|
|
||||||
const passthrough = ((s: string) => s) as Colors;
|
|
||||||
passthrough.enabled = false;
|
|
||||||
passthrough.reset = passthrough;
|
|
||||||
passthrough.bold = passthrough;
|
|
||||||
passthrough.dim = passthrough;
|
|
||||||
passthrough.italic = passthrough;
|
|
||||||
passthrough.underline = passthrough;
|
|
||||||
passthrough.inverse = passthrough;
|
|
||||||
passthrough.hidden = passthrough;
|
|
||||||
passthrough.strikethrough = passthrough;
|
|
||||||
passthrough.black = passthrough;
|
|
||||||
passthrough.red = passthrough;
|
|
||||||
passthrough.green = passthrough;
|
|
||||||
passthrough.yellow = passthrough;
|
|
||||||
passthrough.blue = passthrough;
|
|
||||||
passthrough.magenta = passthrough;
|
|
||||||
passthrough.cyan = passthrough;
|
|
||||||
passthrough.white = passthrough;
|
|
||||||
passthrough.gray = passthrough;
|
|
||||||
passthrough.grey = passthrough;
|
|
||||||
passthrough.blackBright = passthrough;
|
|
||||||
passthrough.redBright = passthrough;
|
|
||||||
passthrough.greenBright = passthrough;
|
|
||||||
passthrough.yellowBright = passthrough;
|
|
||||||
passthrough.blueBright = passthrough;
|
|
||||||
passthrough.magentaBright = passthrough;
|
|
||||||
passthrough.cyanBright = passthrough;
|
|
||||||
passthrough.whiteBright = passthrough;
|
|
||||||
passthrough.bgBlack = passthrough;
|
|
||||||
passthrough.bgRed = passthrough;
|
|
||||||
passthrough.bgGreen = passthrough;
|
|
||||||
passthrough.bgYellow = passthrough;
|
|
||||||
passthrough.bgBlue = passthrough;
|
|
||||||
passthrough.bgMagenta = passthrough;
|
|
||||||
passthrough.bgCyan = passthrough;
|
|
||||||
passthrough.bgWhite = passthrough;
|
|
||||||
passthrough.bgBlackBright = passthrough;
|
|
||||||
passthrough.bgRedBright = passthrough;
|
|
||||||
passthrough.bgGreenBright = passthrough;
|
|
||||||
passthrough.bgYellowBright = passthrough;
|
|
||||||
passthrough.bgBlueBright = passthrough;
|
|
||||||
passthrough.bgMagentaBright = passthrough;
|
|
||||||
passthrough.bgCyanBright = passthrough;
|
|
||||||
passthrough.bgWhiteBright = passthrough;
|
|
||||||
Colors = passthrough;
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
import { ResolveType, ZipKeysWithValues } from './types.js';
|
|
||||||
|
|
||||||
export type valueof<K> = K[keyof K];
|
|
||||||
|
|
||||||
export function keysOf<T extends string>(obj: { [k in T]: unknown }): readonly T[] {
|
|
||||||
return (Object.keys(obj) as unknown[]) as T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function numericKeysOf<T>(obj: object): readonly T[] {
|
|
||||||
return (Object.keys(obj).map(n => Number(n)) as unknown[]) as T[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an info lookup object from a more nicely-formatted table. See below for examples.
|
|
||||||
*
|
|
||||||
* Note: Using `as const` on the arguments to this function is necessary to infer the correct type.
|
|
||||||
*/
|
|
||||||
export function makeTable<
|
|
||||||
Members extends readonly string[],
|
|
||||||
Defaults extends readonly unknown[],
|
|
||||||
Table extends { readonly [k: string]: readonly unknown[] }
|
|
||||||
>(
|
|
||||||
members: Members,
|
|
||||||
defaults: Defaults,
|
|
||||||
table: Table
|
|
||||||
): {
|
|
||||||
readonly [k in keyof Table]: ResolveType<ZipKeysWithValues<Members, Table[k], Defaults>>;
|
|
||||||
} {
|
|
||||||
const result: { [k: string]: { [m: string]: unknown } } = {};
|
|
||||||
for (const [k, v] of Object.entries<readonly unknown[]>(table)) {
|
|
||||||
const item: { [m: string]: unknown } = {};
|
|
||||||
for (let i = 0; i < members.length; ++i) {
|
|
||||||
item[members[i]] = v[i] ?? defaults[i];
|
|
||||||
}
|
|
||||||
result[k] = item;
|
|
||||||
}
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
return result as any;
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
/// <reference types="@webgpu/types" />
|
|
||||||
|
|
||||||
import { assert } from './util.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
|
|
||||||
* Throws an exception if not found.
|
|
||||||
*/
|
|
||||||
function defaultGPUProvider(): GPU {
|
|
||||||
assert(
|
|
||||||
typeof navigator !== 'undefined' && navigator.gpu !== undefined,
|
|
||||||
'No WebGPU implementation found'
|
|
||||||
);
|
|
||||||
return navigator.gpu;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GPUProvider is a function that creates and returns a new GPU instance.
|
|
||||||
* May throw an exception if a GPU cannot be created.
|
|
||||||
*/
|
|
||||||
export type GPUProvider = () => GPU;
|
|
||||||
|
|
||||||
let gpuProvider: GPUProvider = defaultGPUProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the function to create and return a new GPU instance.
|
|
||||||
*/
|
|
||||||
export function setGPUProvider(provider: GPUProvider) {
|
|
||||||
assert(impl === undefined, 'setGPUProvider() should not be after getGPU()');
|
|
||||||
gpuProvider = provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
let impl: GPU | undefined = undefined;
|
|
||||||
|
|
||||||
let defaultRequestAdapterOptions: GPURequestAdapterOptions | undefined;
|
|
||||||
|
|
||||||
export function setDefaultRequestAdapterOptions(options: GPURequestAdapterOptions) {
|
|
||||||
if (impl) {
|
|
||||||
throw new Error('must call setDefaultRequestAdapterOptions before getGPU');
|
|
||||||
}
|
|
||||||
defaultRequestAdapterOptions = { ...options };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds and returns the `navigator.gpu` object (or equivalent, for non-browser implementations).
|
|
||||||
* Throws an exception if not found.
|
|
||||||
*/
|
|
||||||
export function getGPU(): GPU {
|
|
||||||
if (impl) {
|
|
||||||
return impl;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl = gpuProvider();
|
|
||||||
|
|
||||||
if (defaultRequestAdapterOptions) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
||||||
const oldFn = impl.requestAdapter;
|
|
||||||
impl.requestAdapter = function (
|
|
||||||
options?: GPURequestAdapterOptions
|
|
||||||
): Promise<GPUAdapter | null> {
|
|
||||||
const promise = oldFn.call(this, { ...defaultRequestAdapterOptions, ...(options || {}) });
|
|
||||||
void promise.then(async adapter => {
|
|
||||||
if (adapter) {
|
|
||||||
const info = await adapter.requestAdapterInfo();
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(info);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return promise;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return impl;
|
|
||||||
}
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
import { assert } from './util.js';
|
|
||||||
|
|
||||||
// The state of the preprocessor is a stack of States.
|
|
||||||
type StateStack = { allowsFollowingElse: boolean; state: State }[];
|
|
||||||
const enum State {
|
|
||||||
Seeking, // Still looking for a passing condition
|
|
||||||
Passing, // Currently inside a passing condition (the root is always in this state)
|
|
||||||
Skipping, // Have already seen a passing condition; now skipping the rest
|
|
||||||
}
|
|
||||||
|
|
||||||
// The transitions in the state space are the following preprocessor directives:
|
|
||||||
// - Sibling elif
|
|
||||||
// - Sibling else
|
|
||||||
// - Sibling endif
|
|
||||||
// - Child if
|
|
||||||
abstract class Directive {
|
|
||||||
private readonly depth: number;
|
|
||||||
|
|
||||||
constructor(depth: number) {
|
|
||||||
this.depth = depth;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected checkDepth(stack: StateStack): void {
|
|
||||||
assert(
|
|
||||||
stack.length === this.depth,
|
|
||||||
`Number of "$"s must match nesting depth, currently ${stack.length} (e.g. $if $$if $$endif $endif)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract applyTo(stack: StateStack): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
class If extends Directive {
|
|
||||||
private readonly predicate: boolean;
|
|
||||||
|
|
||||||
constructor(depth: number, predicate: boolean) {
|
|
||||||
super(depth);
|
|
||||||
this.predicate = predicate;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTo(stack: StateStack) {
|
|
||||||
this.checkDepth(stack);
|
|
||||||
const parentState = stack[stack.length - 1].state;
|
|
||||||
stack.push({
|
|
||||||
allowsFollowingElse: true,
|
|
||||||
state:
|
|
||||||
parentState !== State.Passing
|
|
||||||
? State.Skipping
|
|
||||||
: this.predicate
|
|
||||||
? State.Passing
|
|
||||||
: State.Seeking,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ElseIf extends If {
|
|
||||||
applyTo(stack: StateStack) {
|
|
||||||
assert(stack.length >= 1);
|
|
||||||
const { allowsFollowingElse, state: siblingState } = stack.pop()!;
|
|
||||||
this.checkDepth(stack);
|
|
||||||
assert(allowsFollowingElse, 'pp.elif after pp.else');
|
|
||||||
if (siblingState !== State.Seeking) {
|
|
||||||
stack.push({ allowsFollowingElse: true, state: State.Skipping });
|
|
||||||
} else {
|
|
||||||
super.applyTo(stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Else extends Directive {
|
|
||||||
applyTo(stack: StateStack) {
|
|
||||||
assert(stack.length >= 1);
|
|
||||||
const { allowsFollowingElse, state: siblingState } = stack.pop()!;
|
|
||||||
this.checkDepth(stack);
|
|
||||||
assert(allowsFollowingElse, 'pp.else after pp.else');
|
|
||||||
stack.push({
|
|
||||||
allowsFollowingElse: false,
|
|
||||||
state: siblingState === State.Seeking ? State.Passing : State.Skipping,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class EndIf extends Directive {
|
|
||||||
applyTo(stack: StateStack) {
|
|
||||||
stack.pop();
|
|
||||||
this.checkDepth(stack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple template-based, non-line-based preprocessor implementing if/elif/else/endif.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```
|
|
||||||
* const shader = pp`
|
|
||||||
* ${pp._if(expr)}
|
|
||||||
* const x: ${type} = ${value};
|
|
||||||
* ${pp._elif(expr)}
|
|
||||||
* ${pp.__if(expr)}
|
|
||||||
* ...
|
|
||||||
* ${pp.__else}
|
|
||||||
* ...
|
|
||||||
* ${pp.__endif}
|
|
||||||
* ${pp._endif}`;
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param strings - The array of constant string chunks of the template string.
|
|
||||||
* @param ...values - The array of interpolated `${}` values within the template string.
|
|
||||||
*/
|
|
||||||
export function pp(
|
|
||||||
strings: TemplateStringsArray,
|
|
||||||
...values: ReadonlyArray<Directive | string | number>
|
|
||||||
): string {
|
|
||||||
let result = '';
|
|
||||||
const stateStack: StateStack = [{ allowsFollowingElse: false, state: State.Passing }];
|
|
||||||
|
|
||||||
for (let i = 0; i < values.length; ++i) {
|
|
||||||
const passing = stateStack[stateStack.length - 1].state === State.Passing;
|
|
||||||
if (passing) {
|
|
||||||
result += strings[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = values[i];
|
|
||||||
if (value instanceof Directive) {
|
|
||||||
value.applyTo(stateStack);
|
|
||||||
} else {
|
|
||||||
if (passing) {
|
|
||||||
result += value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert(stateStack.length === 1, 'Unterminated preprocessor condition at end of file');
|
|
||||||
result += strings[values.length];
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
pp._if = (predicate: boolean) => new If(1, predicate);
|
|
||||||
pp._elif = (predicate: boolean) => new ElseIf(1, predicate);
|
|
||||||
pp._else = new Else(1);
|
|
||||||
pp._endif = new EndIf(1);
|
|
||||||
pp.__if = (predicate: boolean) => new If(2, predicate);
|
|
||||||
pp.__elif = (predicate: boolean) => new ElseIf(2, predicate);
|
|
||||||
pp.__else = new Else(2);
|
|
||||||
pp.__endif = new EndIf(2);
|
|
||||||
pp.___if = (predicate: boolean) => new If(3, predicate);
|
|
||||||
pp.___elif = (predicate: boolean) => new ElseIf(3, predicate);
|
|
||||||
pp.___else = new Else(3);
|
|
||||||
pp.___endif = new EndIf(3);
|
|
||||||
// Add more if needed.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
/** Defined by WPT. Like `setTimeout`, but applies a timeout multiplier for slow test systems. */
|
|
||||||
declare const step_timeout: undefined | typeof setTimeout;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Equivalent of `setTimeout`, but redirects to WPT's `step_timeout` when it is defined.
|
|
||||||
*/
|
|
||||||
export const timeout = typeof step_timeout !== 'undefined' ? step_timeout : setTimeout;
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
/** Forces a type to resolve its type definitions, to make it readable/debuggable. */
|
|
||||||
export type ResolveType<T> = T extends object
|
|
||||||
? T extends infer O
|
|
||||||
? { [K in keyof O]: ResolveType<O[K]> }
|
|
||||||
: never
|
|
||||||
: T;
|
|
||||||
|
|
||||||
/** Returns the type `true` iff X and Y are exactly equal */
|
|
||||||
export type TypeEqual<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
|
|
||||||
? true
|
|
||||||
: false;
|
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
|
|
||||||
export function assertTypeTrue<T extends true>() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computes the intersection of a set of types, given the union of those types.
|
|
||||||
*
|
|
||||||
* From: https://stackoverflow.com/a/56375136
|
|
||||||
*/
|
|
||||||
export type UnionToIntersection<U> =
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
|
||||||
|
|
||||||
/** "Type asserts" that `X` is a subtype of `Y`. */
|
|
||||||
type EnsureSubtype<X, Y> = X extends Y ? X : never;
|
|
||||||
|
|
||||||
type TupleHeadOr<T, Default> = T extends readonly [infer H, ...(readonly unknown[])] ? H : Default;
|
|
||||||
type TupleTailOr<T, Default> = T extends readonly [unknown, ...infer Tail] ? Tail : Default;
|
|
||||||
type TypeOr<T, Default> = T extends undefined ? Default : T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Zips a key tuple type and a value tuple type together into an object.
|
|
||||||
*
|
|
||||||
* @template Keys Keys of the resulting object.
|
|
||||||
* @template Values Values of the resulting object. If a key corresponds to a `Values` member that
|
|
||||||
* is undefined or past the end, it defaults to the corresponding `Defaults` member.
|
|
||||||
* @template Defaults Default values. If a key corresponds to a `Defaults` member that is past the
|
|
||||||
* end, the default falls back to `undefined`.
|
|
||||||
*/
|
|
||||||
export type ZipKeysWithValues<
|
|
||||||
Keys extends readonly string[],
|
|
||||||
Values extends readonly unknown[],
|
|
||||||
Defaults extends readonly unknown[]
|
|
||||||
> =
|
|
||||||
//
|
|
||||||
Keys extends readonly [infer KHead, ...infer KTail]
|
|
||||||
? {
|
|
||||||
readonly [k in EnsureSubtype<KHead, string>]: TypeOr<
|
|
||||||
TupleHeadOr<Values, undefined>,
|
|
||||||
TupleHeadOr<Defaults, undefined>
|
|
||||||
>;
|
|
||||||
} &
|
|
||||||
ZipKeysWithValues<
|
|
||||||
EnsureSubtype<KTail, readonly string[]>,
|
|
||||||
TupleTailOr<Values, []>,
|
|
||||||
TupleTailOr<Defaults, []>
|
|
||||||
>
|
|
||||||
: {}; // K exhausted
|
|
||||||
|
|
@ -1,303 +0,0 @@
|
||||||
import { Float16Array } from '../../external/petamoriken/float16/float16.js';
|
|
||||||
import { globalTestConfig } from '../framework/test_config.js';
|
|
||||||
import { Logger } from '../internal/logging/logger.js';
|
|
||||||
|
|
||||||
import { keysOf } from './data_tables.js';
|
|
||||||
import { timeout } from './timeout.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Error with arbitrary `extra` data attached, for debugging.
|
|
||||||
* The extra data is omitted if not running the test in debug mode (`?debug=1`).
|
|
||||||
*/
|
|
||||||
export class ErrorWithExtra extends Error {
|
|
||||||
readonly extra: { [k: string]: unknown };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `extra` function is only called if in debug mode.
|
|
||||||
* If an `ErrorWithExtra` is passed, its message is used and its extras are passed through.
|
|
||||||
*/
|
|
||||||
constructor(message: string, extra: () => {});
|
|
||||||
constructor(base: ErrorWithExtra, newExtra: () => {});
|
|
||||||
constructor(baseOrMessage: string | ErrorWithExtra, newExtra: () => {}) {
|
|
||||||
const message = typeof baseOrMessage === 'string' ? baseOrMessage : baseOrMessage.message;
|
|
||||||
super(message);
|
|
||||||
|
|
||||||
const oldExtras = baseOrMessage instanceof ErrorWithExtra ? baseOrMessage.extra : {};
|
|
||||||
this.extra = Logger.globalDebugMode
|
|
||||||
? { ...oldExtras, ...newExtra() }
|
|
||||||
: { omitted: 'pass ?debug=1' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asserts `condition` is true. Otherwise, throws an `Error` with the provided message.
|
|
||||||
*/
|
|
||||||
export function assert(condition: boolean, msg?: string | (() => string)): asserts condition {
|
|
||||||
if (!condition) {
|
|
||||||
throw new Error(msg && (typeof msg === 'string' ? msg : msg()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** If the argument is an Error, throw it. Otherwise, pass it back. */
|
|
||||||
export function assertOK<T>(value: Error | T): T {
|
|
||||||
if (value instanceof Error) {
|
|
||||||
throw value;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves if the provided promise rejects; rejects if it does not.
|
|
||||||
*/
|
|
||||||
export async function assertReject(p: Promise<unknown>, msg?: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await p;
|
|
||||||
unreachable(msg);
|
|
||||||
} catch (ex) {
|
|
||||||
// Assertion OK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert this code is unreachable. Unconditionally throws an `Error`.
|
|
||||||
*/
|
|
||||||
export function unreachable(msg?: string): never {
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `performance` interface.
|
|
||||||
* It is available in all browsers, but it is not in scope by default in Node.
|
|
||||||
*/
|
|
||||||
const perf = typeof performance !== 'undefined' ? performance : require('perf_hooks').performance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the appropriate `performance.now()` depending on whether running in a browser or Node.
|
|
||||||
*/
|
|
||||||
export function now(): number {
|
|
||||||
return perf.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a promise which resolves after the specified time.
|
|
||||||
*/
|
|
||||||
export function resolveOnTimeout(ms: number): Promise<void> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
timeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PromiseTimeoutError extends Error {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a promise which rejects after the specified time.
|
|
||||||
*/
|
|
||||||
export function rejectOnTimeout(ms: number, msg: string): Promise<never> {
|
|
||||||
return new Promise((_resolve, reject) => {
|
|
||||||
timeout(() => {
|
|
||||||
reject(new PromiseTimeoutError(msg));
|
|
||||||
}, ms);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a promise `p`, and returns a new one which rejects if `p` takes too long,
|
|
||||||
* and otherwise passes the result through.
|
|
||||||
*/
|
|
||||||
export function raceWithRejectOnTimeout<T>(p: Promise<T>, ms: number, msg: string): Promise<T> {
|
|
||||||
if (globalTestConfig.noRaceWithRejectOnTimeout) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
// Setup a promise that will reject after `ms` milliseconds. We cancel this timeout when
|
|
||||||
// `p` is finalized, so the JavaScript VM doesn't hang around waiting for the timer to
|
|
||||||
// complete, once the test runner has finished executing the tests.
|
|
||||||
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
||||||
const handle = timeout(() => {
|
|
||||||
reject(new PromiseTimeoutError(msg));
|
|
||||||
}, ms);
|
|
||||||
p = p.finally(() => clearTimeout(handle));
|
|
||||||
});
|
|
||||||
return Promise.race([p, timeoutPromise]) as Promise<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes a promise `p` and returns a new one which rejects if `p` resolves or rejects,
|
|
||||||
* and otherwise resolves after the specified time.
|
|
||||||
*/
|
|
||||||
export function assertNotSettledWithinTime(
|
|
||||||
p: Promise<unknown>,
|
|
||||||
ms: number,
|
|
||||||
msg: string
|
|
||||||
): Promise<undefined> {
|
|
||||||
// Rejects regardless of whether p resolves or rejects.
|
|
||||||
const rejectWhenSettled = p.then(() => Promise.reject(new Error(msg)));
|
|
||||||
// Resolves after `ms` milliseconds.
|
|
||||||
const timeoutPromise = new Promise<undefined>(resolve => {
|
|
||||||
const handle = timeout(() => {
|
|
||||||
resolve(undefined);
|
|
||||||
}, ms);
|
|
||||||
p.finally(() => clearTimeout(handle));
|
|
||||||
});
|
|
||||||
return Promise.race([rejectWhenSettled, timeoutPromise]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a `Promise.reject()`, but also registers a dummy `.catch()` handler so it doesn't count
|
|
||||||
* as an uncaught promise rejection in the runtime.
|
|
||||||
*/
|
|
||||||
export function rejectWithoutUncaught<T>(err: unknown): Promise<T> {
|
|
||||||
const p = Promise.reject(err);
|
|
||||||
// Suppress uncaught promise rejection.
|
|
||||||
p.catch(() => {});
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes a copy of a JS `object`, with the keys reordered into sorted order.
|
|
||||||
*/
|
|
||||||
export function sortObjectByKey(v: { [k: string]: unknown }): { [k: string]: unknown } {
|
|
||||||
const sortedObject: { [k: string]: unknown } = {};
|
|
||||||
for (const k of Object.keys(v).sort()) {
|
|
||||||
sortedObject[k] = v[k];
|
|
||||||
}
|
|
||||||
return sortedObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether two JS values are equal, recursing into objects and arrays.
|
|
||||||
* NaN is treated specially, such that `objectEquals(NaN, NaN)`.
|
|
||||||
*/
|
|
||||||
export function objectEquals(x: unknown, y: unknown): boolean {
|
|
||||||
if (typeof x !== 'object' || typeof y !== 'object') {
|
|
||||||
if (typeof x === 'number' && typeof y === 'number' && Number.isNaN(x) && Number.isNaN(y)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return x === y;
|
|
||||||
}
|
|
||||||
if (x === null || y === null) return x === y;
|
|
||||||
if (x.constructor !== y.constructor) return false;
|
|
||||||
if (x instanceof Function) return x === y;
|
|
||||||
if (x instanceof RegExp) return x === y;
|
|
||||||
if (x === y || x.valueOf() === y.valueOf()) return true;
|
|
||||||
if (Array.isArray(x) && Array.isArray(y) && x.length !== y.length) return false;
|
|
||||||
if (x instanceof Date) return false;
|
|
||||||
if (!(x instanceof Object)) return false;
|
|
||||||
if (!(y instanceof Object)) return false;
|
|
||||||
|
|
||||||
const x1 = x as { [k: string]: unknown };
|
|
||||||
const y1 = y as { [k: string]: unknown };
|
|
||||||
const p = Object.keys(x);
|
|
||||||
return Object.keys(y).every(i => p.indexOf(i) !== -1) && p.every(i => objectEquals(x1[i], y1[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a range of values `fn(0)..fn(n-1)`.
|
|
||||||
*/
|
|
||||||
export function range<T>(n: number, fn: (i: number) => T): T[] {
|
|
||||||
return [...new Array(n)].map((_, i) => fn(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a range of values `fn(0)..fn(n-1)`.
|
|
||||||
*/
|
|
||||||
export function* iterRange<T>(n: number, fn: (i: number) => T): Iterable<T> {
|
|
||||||
for (let i = 0; i < n; ++i) {
|
|
||||||
yield fn(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Creates a (reusable) iterable object that maps `f` over `xs`, lazily. */
|
|
||||||
export function mapLazy<T, R>(xs: Iterable<T>, f: (x: T) => R): Iterable<R> {
|
|
||||||
return {
|
|
||||||
*[Symbol.iterator]() {
|
|
||||||
for (const x of xs) {
|
|
||||||
yield f(x);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const TypedArrayBufferViewInstances = [
|
|
||||||
new Uint8Array(),
|
|
||||||
new Uint8ClampedArray(),
|
|
||||||
new Uint16Array(),
|
|
||||||
new Uint32Array(),
|
|
||||||
new Int8Array(),
|
|
||||||
new Int16Array(),
|
|
||||||
new Int32Array(),
|
|
||||||
new Float16Array(),
|
|
||||||
new Float32Array(),
|
|
||||||
new Float64Array(),
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type TypedArrayBufferView = typeof TypedArrayBufferViewInstances[number];
|
|
||||||
|
|
||||||
export type TypedArrayBufferViewConstructor<
|
|
||||||
A extends TypedArrayBufferView = TypedArrayBufferView
|
|
||||||
> = {
|
|
||||||
// Interface copied from Uint8Array, and made generic.
|
|
||||||
readonly prototype: A;
|
|
||||||
readonly BYTES_PER_ELEMENT: number;
|
|
||||||
|
|
||||||
new (): A;
|
|
||||||
new (elements: Iterable<number>): A;
|
|
||||||
new (array: ArrayLike<number> | ArrayBufferLike): A;
|
|
||||||
new (buffer: ArrayBufferLike, byteOffset?: number, length?: number): A;
|
|
||||||
new (length: number): A;
|
|
||||||
|
|
||||||
from(arrayLike: ArrayLike<number>): A;
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
from(arrayLike: Iterable<number>, mapfn?: (v: number, k: number) => number, thisArg?: any): A;
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
from<T>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => number, thisArg?: any): A;
|
|
||||||
of(...items: number[]): A;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kTypedArrayBufferViews: {
|
|
||||||
readonly [k: string]: TypedArrayBufferViewConstructor;
|
|
||||||
} = {
|
|
||||||
...(() => {
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
||||||
const result: { [k: string]: any } = {};
|
|
||||||
for (const v of TypedArrayBufferViewInstances) {
|
|
||||||
result[v.constructor.name] = v.constructor;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
export const kTypedArrayBufferViewKeys = keysOf(kTypedArrayBufferViews);
|
|
||||||
export const kTypedArrayBufferViewConstructors = Object.values(kTypedArrayBufferViews);
|
|
||||||
|
|
||||||
function subarrayAsU8(
|
|
||||||
buf: ArrayBuffer | TypedArrayBufferView,
|
|
||||||
{ start = 0, length }: { start?: number; length?: number }
|
|
||||||
): Uint8Array | Uint8ClampedArray {
|
|
||||||
if (buf instanceof ArrayBuffer) {
|
|
||||||
return new Uint8Array(buf, start, length);
|
|
||||||
} else if (buf instanceof Uint8Array || buf instanceof Uint8ClampedArray) {
|
|
||||||
// Don't wrap in new views if we don't need to.
|
|
||||||
if (start === 0 && (length === undefined || length === buf.byteLength)) {
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const byteOffset = buf.byteOffset + start * buf.BYTES_PER_ELEMENT;
|
|
||||||
const byteLength =
|
|
||||||
length !== undefined
|
|
||||||
? length * buf.BYTES_PER_ELEMENT
|
|
||||||
: buf.byteLength - (byteOffset - buf.byteOffset);
|
|
||||||
return new Uint8Array(buf.buffer, byteOffset, byteLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy a range of bytes from one ArrayBuffer or TypedArray to another.
|
|
||||||
*
|
|
||||||
* `start`/`length` are in elements (or in bytes, if ArrayBuffer).
|
|
||||||
*/
|
|
||||||
export function memcpy(
|
|
||||||
src: { src: ArrayBuffer | TypedArrayBufferView; start?: number; length?: number },
|
|
||||||
dst: { dst: ArrayBuffer | TypedArrayBufferView; start?: number }
|
|
||||||
): void {
|
|
||||||
subarrayAsU8(dst.dst, dst).set(subarrayAsU8(src.src, src));
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { timeout } from './timeout.js';
|
|
||||||
|
|
||||||
// Copied from https://github.com/web-platform-tests/wpt/blob/master/common/reftest-wait.js
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the `reftest-wait` class on the document element.
|
|
||||||
* The reftest runner will wait with taking a screenshot while
|
|
||||||
* this class is present.
|
|
||||||
*
|
|
||||||
* See https://web-platform-tests.org/writing-tests/reftests.html#controlling-when-comparison-occurs
|
|
||||||
*/
|
|
||||||
export function takeScreenshot() {
|
|
||||||
document.documentElement.classList.remove('reftest-wait');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call `takeScreenshot()` after a delay of at least `ms` milliseconds.
|
|
||||||
* @param {number} ms - milliseconds
|
|
||||||
*/
|
|
||||||
export function takeScreenshotDelayed(ms: number) {
|
|
||||||
timeout(() => {
|
|
||||||
takeScreenshot();
|
|
||||||
}, ms);
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Demo test suite for manually testing test runners.
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export const description = 'Description for a.spec.ts';
|
|
||||||
|
|
||||||
import { makeTestGroup } from '../common/framework/test_group.js';
|
|
||||||
import { UnitTest } from '../unittests/unit_test.js';
|
|
||||||
|
|
||||||
export const g = makeTestGroup(UnitTest);
|
|
||||||
|
|
||||||
g.test('not_implemented_yet').unimplemented();
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
README for a/
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export const description = 'Description for b.spec.ts';
|
|
||||||
|
|
||||||
import { makeTestGroup } from '../../common/framework/test_group.js';
|
|
||||||
import { UnitTest } from '../../unittests/unit_test.js';
|
|
||||||
|
|
||||||
export const g = makeTestGroup(UnitTest);
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
README for a/b/
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
export const description = 'Description for c.spec.ts';
|
|
||||||
|
|
||||||
import { makeTestGroup } from '../../../common/framework/test_group.js';
|
|
||||||
import { unreachable } from '../../../common/util/util.js';
|
|
||||||
import { UnitTest } from '../../../unittests/unit_test.js';
|
|
||||||
|
|
||||||
export const g = makeTestGroup(UnitTest);
|
|
||||||
|
|
||||||
g.test('f')
|
|
||||||
.desc(
|
|
||||||
`Test plan for f
|
|
||||||
- Test stuff
|
|
||||||
- Test some more stuff`
|
|
||||||
)
|
|
||||||
.fn(() => {});
|
|
||||||
|
|
||||||
g.test('f,g').fn(() => {});
|
|
||||||
|
|
||||||
g.test('f,g,h')
|
|
||||||
.paramsSimple([{}, { x: 0 }, { x: 0, y: 0 }])
|
|
||||||
.fn(() => {});
|
|
||||||
|
|
||||||
g.test('case_depth_2_in_single_child_test')
|
|
||||||
.paramsSimple([{ x: 0, y: 0 }])
|
|
||||||
.fn(() => {});
|
|
||||||
|
|
||||||
g.test('deep_case_tree')
|
|
||||||
.params(u =>
|
|
||||||
u //
|
|
||||||
.combine('x', [1, 2])
|
|
||||||
.combine('y', [1, 2])
|
|
||||||
.combine('z', [1, 2])
|
|
||||||
)
|
|
||||||
.fn(() => {});
|
|
||||||
|
|
||||||
g.test('statuses,debug').fn(t => {
|
|
||||||
t.debug('debug');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('statuses,skip').fn(t => {
|
|
||||||
t.skip('skip');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('statuses,warn').fn(t => {
|
|
||||||
t.warn('warn');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('statuses,fail').fn(t => {
|
|
||||||
t.fail('fail');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('statuses,throw').fn(() => {
|
|
||||||
unreachable('unreachable');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('multiple_same_stack').fn(t => {
|
|
||||||
for (let i = 0; i < 3; ++i) {
|
|
||||||
t.fail(
|
|
||||||
i === 2
|
|
||||||
? 'this should appear after deduplicated line'
|
|
||||||
: 'this should be "seen 2 times with identical stack"'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('multiple_same_level').fn(t => {
|
|
||||||
t.fail('this should print a stack');
|
|
||||||
t.fail('this should print a stack');
|
|
||||||
t.fail('this should not print a stack');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('lower_levels_hidden,before').fn(t => {
|
|
||||||
t.warn('warn - this should not print a stack');
|
|
||||||
t.fail('fail');
|
|
||||||
});
|
|
||||||
|
|
||||||
g.test('lower_levels_hidden,after').fn(t => {
|
|
||||||
t.fail('fail');
|
|
||||||
t.warn('warn - this should not print a stack');
|
|
||||||
});
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export const description = 'Description for d.spec.ts';
|
|
||||||
|
|
||||||
import { makeTestGroup } from '../../../common/framework/test_group.js';
|
|
||||||
import { UnitTest } from '../../../unittests/unit_test.js';
|
|
||||||
|
|
||||||
export const g = makeTestGroup(UnitTest);
|
|
||||||
|
|
||||||
g.test('test_depth_2,in_single_child_file').fn(() => {});
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export const description = 'Description for r.spec.ts';
|
|
||||||
|
|
||||||
import { makeTestGroup } from '../../../common/framework/test_group.js';
|
|
||||||
import { UnitTest } from '../../../unittests/unit_test.js';
|
|
||||||
|
|
||||||
export const g = makeTestGroup(UnitTest);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue