blob: ca5ca8786e494d8bd4e266bdd9b58df012f9c615 [file] [log] [blame]
// Copyright 2021 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This tool parses WGSL specification and outputs WGSL rules.
//
// To run from root of tint repo:
// go get golang.org/x/net/html # Only required once
// Then run
// ./tools/get-test-plan --spec=<path-to-spec-file-or-url> --output=<path-to-output-file>
// Or run
// cd tools/src && go run cmd/get-spec-rules/main.go --output=<path-to-output-file>
//
// To see help
// ./tools/get-test-plan --help
package main
import (
"crypto/sha1"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"golang.org/x/net/html"
)
const (
toolName = "get-test-plan"
specPath = "https://www.w3.org/TR/WGSL/"
specVersionUsed = "https://www.w3.org/TR/2021/WD-WGSL-20210929/"
)
var (
errInvalidArg = errors.New("invalid arguments")
headURL = specVersionUsed
markedNodesSet = make(map[*html.Node]bool)
testNamesSet = make(map[string]bool)
sha1sSet = make(map[string]bool)
keywords = []string{
"MUST ", "MUST NOT ", "REQUIRED ", "SHALL ",
"SHALL NOT ", "SHOULD ", "SHOULD NOT ",
"RECOMMENDED ", "MAY ", "OPTIONAL ",
}
globalSection = ""
globalPrevSectionX = -1
globalRuleCounter = 0
)
// Holds all the information about a WGSL rule
type rule struct {
Number int // The index of this obj in an array of 'rules'
Section int // The section this rule belongs to
SubSection string // The section this rule belongs to
URL string // The section's URL of this rule
Description string // The rule's description
TestName string // The suggested test name to use when writing CTS
Keyword string // The keyword e.g. MUST, ALGORITHM, ..., i.e. Indicating why the rule is added
Desc []string
Sha string
}
func main() {
flag.Usage = func() {
out := flag.CommandLine.Output()
fmt.Fprintf(out, "%v parses WGSL spec and outputs a test plan\n", toolName)
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "Usage:\n")
fmt.Fprintf(out, " %s [spec] [flags]\n", toolName)
fmt.Fprintf(out, "\n")
fmt.Fprintf(out, "spec is an optional local file or a URL to the WGSL specification.\n")
fmt.Fprintf(out, "If spec is omitted then the specification is fetched from %v\n\n", specPath)
fmt.Fprintf(out, "this tools is developed based on: %v\n", specVersionUsed)
fmt.Fprintf(out, "flags may be any combination of:\n")
flag.PrintDefaults()
}
err := run()
switch err {
case nil:
return
case errInvalidArg:
fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
flag.Usage()
default:
fmt.Fprintf(os.Stderr, "%v\n", err)
}
os.Exit(1)
}
func run() error {
// Parse flags
keyword := flag.String("keyword", "",
`if provided, it will be used as the keyword to search WGSL spec for rules
if omitted, the keywords indicated in RFC 2119 requirement are used,
in addition to nodes containing a nowrap or an algorithm tag eg. <tr algorithm=...>`)
ctsDir := flag.String("cts-directory", "",
`if provided:
validation cts test plan will be written to: '<cts-directory>/validation/'
builtin functions cts test plan will be written to: '<cts-directory>/execution/builtin'`)
output := flag.String("output", "",
`if file extension is 'txt' the output format will be a human readable text
if file extension is 'tsv' the output format will be a tab separated file
if file extension is 'json' the output format will be json
if omitted, a human readable version of the rules is written to stdout`)
testNameFilter := flag.String("test-name-filter", "",
`if provided will be used to filter reported rules based on if their name
contains the provided string`)
flag.Parse()
args := flag.Args()
// Parse spec
spec, err := parseSpec(args)
if err != nil {
return err
}
// Set keywords
if *keyword != "" {
keywords = []string{*keyword}
}
parser, err := Parse(spec)
if err != nil {
return err
}
rules := parser.rules
if *ctsDir != "" {
err := getUnimplementedTestPlan(*parser, *ctsDir)
if err != nil {
return err
}
}
txt, tsv := concatRules(rules, *testNameFilter)
// if no output then write rules to stdout
if *output == "" {
fmt.Println(txt)
// write concatenated rules to file
} else if strings.HasSuffix(*output, ".json") {
j, err := json.Marshal(rules)
if err != nil {
return err
}
return writeFile(*output, string(j))
} else if strings.HasSuffix(*output, ".txt") {
return writeFile(*output, txt)
} else if strings.HasSuffix(*output, ".tsv") {
return writeFile(*output, tsv)
} else {
return fmt.Errorf("unsupported output file extension: %v", *output)
}
return nil
}
// getSectionRange scans all the rules and returns the rule index interval of a given section.
// The sections range is the interval: rules[start:end].
// example: section = [x, y, z] i.e. x.y.z(.w)* it returns (start = min(w),end = max(w))
// if there are no rules extracted from x.y.z it returns (-1, -1)
func getSectionRange(rules []rule, s []int) (start, end int, err error) {
start = -1
end = -1
for _, r := range rules {
sectionDims, err := parseSection(r.SubSection)
if err != nil {
return -1, -1, err
}
ruleIsInSection := true
for i := range s {
if sectionDims[i] != s[i] {
ruleIsInSection = false
break
}
}
if !ruleIsInSection {
continue
}
dim := -1
if len(sectionDims) == len(s) {
//x.y is the same as x.y.0
dim = 0
} else if len(sectionDims) > len(s) {
dim = sectionDims[len(s)]
} else {
continue
}
if start == -1 {
start = dim
}
if dim > end {
end = dim
}
}
if start == -1 || end == -1 {
return -1, -1, fmt.Errorf("cannot determine section range")
}
return start, end, nil
}
// parseSection return the numbers for any dot-separated string of numbers
// example: x.y.z.w returns [x, y, z, w]
// returns an error if the string does not match "^\d(.\d)*$"
func parseSection(in string) ([]int, error) {
parts := strings.Split(in, ".")
out := make([]int, len(parts))
for i, part := range parts {
var err error
out[i], err = strconv.Atoi(part)
if err != nil {
return nil, fmt.Errorf(`cannot parse sections string "%v": %w`, in, err)
}
}
return out, nil
}
// concatRules concatenate rules slice to make two string outputs;
//
// txt, a human-readable string
// tsv, a tab separated string
//
// If testNameFilter is a non-empty string, then only rules whose TestName
// contains the string are included
func concatRules(rules []rule, testNameFilter string) (string, string) {
txtLines := []string{}
tsvLines := []string{"Number\tUniqueId\tSection\tURL\tDescription\tProposed Test Name\tkeyword"}
for _, r := range rules {
if testNameFilter != "" && !strings.Contains(r.TestName, testNameFilter) {
continue
}
txtLines = append(txtLines, strings.Join([]string{
"Rule Number " + strconv.Itoa(r.Number) + ":",
"Unique Id: " + r.Sha,
"Section: " + r.SubSection,
"Keyword: " + r.Keyword,
"testName: " + r.TestName,
"URL: " + r.URL,
r.Description,
"---------------------------------------------------"}, "\n"))
tsvLines = append(tsvLines, strings.Join([]string{
strconv.Itoa(r.Number),
r.Sha,
r.SubSection,
r.URL,
strings.Trim(r.Description, "\n\t "),
r.Keyword,
r.TestName}, "\t"))
}
txt := strings.Join(txtLines, "\n")
tsv := strings.Join(tsvLines, "\n")
return txt, tsv
}
// writeFile writes content to path
// the existing content will be overwritten
func writeFile(path, content string) error {
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
return fmt.Errorf("failed to create directory for '%v': %w", path, err)
}
if err := ioutil.WriteFile(path, []byte(content), 0666); err != nil {
return fmt.Errorf("failed to write file '%v': %w", path, err)
}
return nil
}
// parseSpec reads the spec from a local file, or the URL to WGSL spec
func parseSpec(args []string) (*html.Node, error) {
// Check for explicit WGSL spec path
specURL, _ := url.Parse(specPath)
switch len(args) {
case 0:
case 1:
var err error
specURL, err = url.Parse(args[0])
if err != nil {
return nil, err
}
default:
if len(args) > 1 {
return nil, errInvalidArg
}
}
// The specURL might just be a local file path, in which case automatically
// add the 'file' URL scheme
if specURL.Scheme == "" {
specURL.Scheme = "file"
}
// Open the spec from HTTP(S) or from a local file
var specContent io.ReadCloser
switch specURL.Scheme {
case "http", "https":
response, err := http.Get(specURL.String())
if err != nil {
return nil, fmt.Errorf("failed to load the WGSL spec from '%v': %w", specURL, err)
}
specContent = response.Body
case "file":
path, err := filepath.Abs(specURL.Path)
if err != nil {
return nil, fmt.Errorf("failed to load the WGSL spec from '%v': %w", specURL, err)
}
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to load the WGSL spec from '%v': %w", specURL, err)
}
specContent = file
default:
return nil, fmt.Errorf("unsupported URL scheme: %v", specURL.Scheme)
}
defer specContent.Close()
// Open the spec from HTTP(S) or from a local file
switch specURL.Scheme {
case "http", "https":
response, err := http.Get(specURL.String())
if err != nil {
return nil, fmt.Errorf("failed to load the WGSL spec from '%v': %w", specURL, err)
}
specContent = response.Body
case "file":
path, err := filepath.Abs(specURL.Path)
if err != nil {
return nil, fmt.Errorf("failed to load the WGSL spec from '%v': %w", specURL, err)
}
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to load the WGSL spec from '%v': %w", specURL, err)
}
specContent = file
default:
return nil, fmt.Errorf("unsupported URL scheme: %v", specURL.Scheme)
}
defer specContent.Close()
// Parse spec
spec, err := html.Parse(specContent)
if err != nil {
return spec, err
}
return spec, nil
}
// containsKeyword returns (true, 'kw'), if input string 'data' contains an
// element of the string list, otherwise it returns (false, "")
// search is not case-sensitive
func containsKeyword(data string, list []string) (bool, string) {
for _, kw := range list {
if strings.Contains(
strings.ToLower(data),
strings.ToLower(kw),
) {
return true, kw
}
}
return false, ""
}
// Parser holds the information extracted from the spec
// TODO(sarahM0): https://bugs.c/tint/1149/ clean up the vars holding section information
type Parser struct {
rules []rule // a slice to store the rules extracted from the spec
firstSectionContainingRule int // the first section a rules is extracted from
lastSectionContainingRule int // the last section a rules is extracted form
}
func Parse(node *html.Node) (*Parser, error) {
var p *Parser = new(Parser)
p.firstSectionContainingRule = -1
p.lastSectionContainingRule = -1
return p, p.getRules(node)
}
// getRules populates the rule slice by scanning HTML node and its children
func (p *Parser) getRules(node *html.Node) error {
section, subSection, err := getSectionInfo(node)
if err != nil {
// skip this node and move on to its children
} else {
// Do not generate rules for introductory sections
if section > 2 {
// Check if this node is visited before. This is necessary since
// sometimes to create rule description we visit siblings or children
if marked := markedNodesSet[node]; marked {
return nil
}
// update parser's section info
if p.firstSectionContainingRule == -1 {
p.firstSectionContainingRule = section
}
p.lastSectionContainingRule = section
// extract rules from the node
if err := p.getAlgorithmRule(node, section, subSection); err != nil {
return err
}
if err := p.getNowrapRule(node, section, subSection); err != nil {
return err
}
if err := p.getKeywordRule(node, section, subSection); err != nil {
return err
}
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
if err := p.getRules(child); err != nil {
return err
}
}
return nil
}
// gatherKeywordRules scans the HTML node data, adds a new rules if it contains one
// of the keywords
func (p *Parser) getKeywordRule(node *html.Node, section int, subSection string) error {
if node.Type != html.TextNode {
return nil
}
hasKeyword, keyword := containsKeyword(node.Data, keywords)
if !hasKeyword {
return nil
}
// TODO(sarah): create a list of rule.sha1 for unwanted rules
if strings.HasPrefix(node.Data, "/*") ||
strings.Contains(node.Data, "reference must load and store from the same") ||
strings.Contains(node.Data, " to an invalid reference may either: ") ||
// Do not add Issues
strings.Contains(node.Data, "Issue: ") ||
strings.Contains(node.Data, "WebGPU issue") ||
strings.Contains(node.Data, "/issues/") {
return nil
}
id := getID(node)
desc := cleanUpString(getNodeData(node))
t, _, err := testName(id, desc, subSection)
if err != nil {
return err
}
sha, err := getSha1(desc, id)
if err != nil {
return err
}
r := rule{
Sha: sha,
Number: len(p.rules) + 1,
Section: section,
SubSection: subSection,
URL: headURL + "#" + id,
Description: desc,
TestName: t,
Keyword: keyword,
}
p.rules = append(p.rules, r)
return nil
}
// getNodeData builds the rule's description from the HTML node's data and all of its siblings.
// the node data is a usually a partial sentence, build the description from the node's data and
// all it's siblings to get a full context of the rule.
func getNodeData(node *html.Node) string {
sb := strings.Builder{}
if node.Parent != nil {
for n := node.Parent.FirstChild; n != nil; n = n.NextSibling {
printNodeText(n, &sb)
}
} else {
printNodeText(node, &sb)
}
return sb.String()
}
// getAlgorithmRules scans the HTML node for blocks that
// contain an 'algorithm' class, populating the rule slice.
// i.e. <tr algorithm=...> and <p algorithm=...>
func (p *Parser) getAlgorithmRule(node *html.Node, section int, subSection string) error {
if !hasClass(node, "algorithm") {
return nil
}
// mark this node as seen
markedNodesSet[node] = true
sb := strings.Builder{}
printNodeText(node, &sb)
title := cleanUpStartEnd(getNodeAttrValue(node, "data-algorithm"))
desc := title + ":\n" + cleanUpString(sb.String())
id := getID(node)
testName, _, err := testName(id, desc, subSection)
if err != nil {
return err
}
sha, err := getSha1(desc, id)
if err != nil {
return err
}
r := rule{
Sha: sha,
Number: len(p.rules) + 1,
Section: section,
SubSection: subSection,
URL: headURL + "#" + id,
Description: desc,
TestName: testName,
Keyword: "ALGORITHM",
}
p.rules = append(p.rules, r)
return nil
}
// getNowrapRules scans the HTML node for blocks that contain a
// 'nowrap' class , populating the rule slice.
// ie. <td class="nowrap">
// TODO(https://crbug.com/tint/1157)
// remove this when https://github.com/gpuweb/gpuweb/pull/2084 is closed
// and make sure Derivative built-in functions are added to the rules
func (p *Parser) getNowrapRule(node *html.Node, section int, subSection string) error {
if !hasClass(node, "nowrap") {
return nil
}
// mark this node as seen
markedNodesSet[node] = true
desc := cleanUpStartEnd(getNodeData(node))
id := getID(node)
t, _, err := testName(id, desc, subSection)
if err != nil {
return err
}
sha, err := getSha1(desc, id)
if err != nil {
return err
}
r := rule{
Sha: sha,
Number: len(p.rules) + 1,
SubSection: subSection,
Section: section,
URL: headURL + "#" + id,
Description: desc,
TestName: t,
Keyword: "Nowrap",
}
p.rules = append(p.rules, r)
return nil
}
// hasClass returns true if node is has the given "class" attribute.
func hasClass(node *html.Node, class string) bool {
for _, attr := range node.Attr {
if attr.Key == "class" {
classes := strings.Split(attr.Val, " ")
for _, c := range classes {
if c == class {
return true
}
}
}
}
return false
}
// getSectionInfo returns the section this node belongs to
func getSectionInfo(node *html.Node) (int, string, error) {
sub := getNodeAttrValue(node, "data-level")
for p := node; sub == "" && p != nil; p = p.Parent {
sub = getSiblingSectionInfo(p)
}
// when there is and ISSUE in HTML section cannot be set
// use the previously set section
if sub == "" && globalSection == "" {
// for the section Abstract no section can be found
// return -1 to skip this node
return -1, "", fmt.Errorf("cannot get section info")
}
if sub == "" {
sub = globalSection
}
globalSection = sub
sectionDims, err := parseSection(sub)
if len(sectionDims) > -1 {
return sectionDims[0], sub, err
}
return -1, sub, err
}
// getSection return the section of this node's sibling
// iterates over all siblings and return the first one it can determine
func getSiblingSectionInfo(node *html.Node) string {
for sp := node.PrevSibling; sp != nil; sp = sp.PrevSibling {
section := getNodeAttrValue(sp, "data-level")
if section != "" {
return section
}
}
return ""
}
// GetSiblingSectionInfo determines if the node's id refers to an example
func isExampleNode(node *html.Node) string {
for sp := node.PrevSibling; sp != nil; sp = sp.PrevSibling {
id := getNodeAttrValue(sp, "id")
if id != "" && !strings.Contains(id, "example-") {
return id
}
}
return ""
}
// getID returns the id of the section this node belongs to
func getID(node *html.Node) string {
id := getNodeAttrValue(node, "id")
for p := node; id == "" && p != nil; p = p.Parent {
id = isExampleNode(p)
}
return id
}
var (
reCleanUpString = regexp.MustCompile(`\n(\n|\s|\t)+|(\s|\t)+\n`)
reSpacePlusTwo = regexp.MustCompile(`\t|\s{2,}`)
reBeginOrEndWithSpace = regexp.MustCompile(`^\s|\s$`)
reIrregularWhiteSpace = regexp.MustCompile(`§.`)
)
// cleanUpString creates a string by removing all extra spaces, newlines and tabs
// form input string 'in' and returns it
// This is done so that the uniqueID does not change because of a change in white spaces
//
// example in:
// ` float abs:
// T is f32 or vecN<f32>
//
// abs(e: T ) -> T
// Returns the absolute value of e (e.g. e with a positive sign bit). Component-wise when T is a vector.
// (GLSLstd450Fabs)`
//
// example out:
// `float abs:
// T is f32 or vecN<f32> abs(e: T ) -> T Returns the absolute value of e (e.g. e with a positive sign bit). Component-wise when T is a vector. (GLSLstd450Fabs)`
func cleanUpString(in string) string {
out := reCleanUpString.ReplaceAllString(in, " ")
out = reSpacePlusTwo.ReplaceAllString(out, " ")
//`§.` is not a valid character for a cts description
// ie. this is invalid: g.test().desc(`§.`)
out = reIrregularWhiteSpace.ReplaceAllString(out, "section ")
out = reBeginOrEndWithSpace.ReplaceAllString(out, "")
return out
}
var (
reCleanUpStartEnd = regexp.MustCompile(`^\s+|\s+$|^\t+|\t+$|^\n+|\n+$`)
)
// cleanUpStartEnd creates a string by removing all extra spaces,
// newlines and tabs form the start and end of the input string.
// Example:
//
// input: "\s\t\nHello\s\n\t\Bye\s\s\s\t\n\n\n"
// output: "Hello\s\n\tBye"
// input2: "\nbye\n\n"
// output2: "\nbye"
func cleanUpStartEnd(in string) string {
out := reCleanUpStartEnd.ReplaceAllString(in, "")
return out
}
var (
name = "^[a-zA-Z0-9_]+$"
reName = regexp.MustCompile(`[^a-zA-Z0-9_]`)
reUnderScore = regexp.MustCompile(`[_]+`)
reDoNotBegin = regexp.MustCompile(`^[0-9_]+|[_]$`)
)
// testName creates a test name given a rule id (ie. section name), description and section
// returns for a builtin rule:
//
// testName:${section name} + "," + ${builtin name}
// builtinName: ${builtin name}
// err: nil
//
// returns for a other rules:
//
// testName: ${section name} + "_rule_ + " + ${string(counter)}
// builtinName: ""
// err: nil
//
// if it cannot create a unique name it returns "", "", err.
func testName(id string, desc string, section string) (testName, builtinName string, err error) {
// regex for every thing other than letters and numbers
if desc == "" || section == "" || id == "" {
return "", "", fmt.Errorf("cannot generate test name")
}
// avoid any characters other than letters, numbers and underscore
id = reName.ReplaceAllString(id, "_")
// avoid underscore repeats
id = reUnderScore.ReplaceAllString(id, "_")
// test name must not start with underscore or a number
// nor end with and underscore
id = reDoNotBegin.ReplaceAllString(id, "")
sectionX, err := parseSection(section)
if err != nil {
return "", "", err
}
builtinName = ""
index := strings.Index(desc, ":")
if strings.Contains(id, "builtin_functions") && index > -1 {
builtinName = reName.ReplaceAllString(desc[:index], "_")
builtinName = reDoNotBegin.ReplaceAllString(builtinName, "")
builtinName = reUnderScore.ReplaceAllString(builtinName, "_")
match, _ := regexp.MatchString(name, builtinName)
if match {
testName = id + "," + builtinName
// in case there is more than one builtin functions
// with the same name in one section:
// "id,builtin", "id,builtin2", "id,builtin3", ...
for i := 2; testNamesSet[testName]; i++ {
testName = id + "," + builtinName + strconv.Itoa(i)
}
testNamesSet[testName] = true
return testName, builtinName, nil
}
}
if sectionX[0] == globalPrevSectionX {
globalRuleCounter++
} else {
globalRuleCounter = 0
globalPrevSectionX = sectionX[0]
}
testName = id + ",rule" + strconv.Itoa(globalRuleCounter)
if testNamesSet[testName] {
testName = "error-unable-to-generate-unique-file-name"
return testName, "", fmt.Errorf("unable to generate unique test name\n" + desc)
}
testNamesSet[testName] = true
return testName, "", nil
}
// printNodeText traverses node and its children, writing the Data of all TextNodes to sb.
func printNodeText(node *html.Node, sb *strings.Builder) {
// mark this node as seen
markedNodesSet[node] = true
if node.Type == html.TextNode {
sb.WriteString(node.Data)
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
printNodeText(child, sb)
}
}
// getNodeAttrValue scans attributes of 'node' and returns the value of attribute 'key'
// or an empty string if 'node' doesn't have an attribute 'key'
func getNodeAttrValue(node *html.Node, key string) string {
for _, attr := range node.Attr {
if attr.Key == key {
return attr.Val
}
}
return ""
}
// getSha1 returns the first 8 byte of sha1(a+b)
func getSha1(a string, b string) (string, error) {
sum := sha1.Sum([]byte(a + b))
sha := fmt.Sprintf("%x", sum[0:8])
if sha1sSet[sha] {
return "", fmt.Errorf("sha1 is not unique")
}
sha1sSet[sha] = true
return sha, nil
}
// getUnimplementedPlan generate the typescript code of a test plan for rules in sections[start, end]
// then it writes the generated test plans in the given 'path'
func getUnimplementedTestPlan(p Parser, path string) error {
rules := p.rules
start := p.firstSectionContainingRule
end := p.lastSectionContainingRule
validationPath := filepath.Join(path, "validation")
if err := validationTestPlan(rules, validationPath, start, end); err != nil {
return err
}
executionPath := filepath.Join(path, "execution", "builtin")
if err := executionTestPlan(rules, executionPath); err != nil {
return err
}
return nil
}
// getTestPlanFilePath returns a sort friendly path
// example: if we have 10 sections, and generate filenames naively, this will be the sorted result:
//
// section1.spec.ts -> section10.spec.ts -> section2.spec.ts -> ...
// if we make all the section numbers have the same number of digits, we will get:
// section01.spec.ts -> section02.spec.ts -> ... -> section10.spec.ts
func getTestPlanFilePath(path string, x, y, digits int) (string, error) {
fileName := ""
if y != -1 {
// section16.01.spec.ts, ...
sectionFmt := fmt.Sprintf("section%%d_%%.%dd.spec.ts", digits)
fileName = fmt.Sprintf(sectionFmt, x, y)
} else {
// section01.spec.ts, ...
sectionFmt := fmt.Sprintf("section%%.%dd.spec.ts", digits)
fileName = fmt.Sprintf(sectionFmt, x)
}
return filepath.Join(path, fileName), nil
}
// validationTestPlan generates the typescript code of a test plan for rules in sections[start, end]
func validationTestPlan(rules []rule, path string, start int, end int) error {
content := [][]string{}
filePath := []string{}
for section := 0; section <= end; section++ {
sb := strings.Builder{}
sectionStr := strconv.Itoa(section)
testDescription := "`WGSL Section " + sectionStr + " Test Plan`"
sb.WriteString(fmt.Sprintf(validationTestHeader, testDescription))
content = append(content, []string{sb.String()})
f, err := getTestPlanFilePath(path, section, -1, len(strconv.Itoa(end)))
if err != nil {
return nil
}
filePath = append(filePath, f)
}
for _, r := range rules {
sectionDims, err := parseSection(r.SubSection)
if err != nil || len(sectionDims) == 0 {
return err
}
section := sectionDims[0]
if section < start || section >= end {
continue
}
content[section] = append(content[section], testPlan(r))
}
for i := start; i <= end; i++ {
if len(content[i]) > 1 {
if err := writeFile(filePath[i], strings.Join(content[i], "\n")); err != nil {
return err
}
}
}
return nil
}
// executionTestPlan generates the typescript code of a test plan for rules in the given section
// the rules in section X.Y.* will be written to path/sectionX_Y.spec.ts
func executionTestPlan(rules []rule, path string) error {
// TODO(SarahM) This generates execution tests for builtin function tests. Add other executions tests.
section, err := getBuiltinSectionNum(rules)
if err != nil {
return err
}
content := [][]string{}
filePath := []string{}
start, end, err := getSectionRange(rules, []int{section})
if err != nil || start == -1 || end == -1 {
return err
}
for y := 0; y <= end; y++ {
fileName, err := getTestPlanFilePath(path, section, y, len(strconv.Itoa(end)))
if err != nil {
return err
}
filePath = append(filePath, fileName)
sb := strings.Builder{}
testDescription := fmt.Sprintf("`WGSL section %v.%v execution test`", section, y)
sb.WriteString(fmt.Sprintf(executionTestHeader, testDescription))
content = append(content, []string{sb.String()})
}
for _, r := range rules {
if r.Section != section || !isBuiltinFunctionRule(r) {
continue
}
index := -1
sectionDims, err := parseSection(r.SubSection)
if err != nil || len(sectionDims) == 0 {
return err
}
if len(sectionDims) == 1 {
// section = x
index = 0
} else {
// section = x.y(.z)*
index = sectionDims[1]
}
if index < 0 && index >= len(content) {
return fmt.Errorf("cannot append to content, index %v out of range 0..%v",
index, len(content)-1)
}
content[index] = append(content[index], testPlan(r))
}
for i := start; i <= end; i++ {
// Write the file if there is a test in there
// compared with >1 because content has at least the test description
if len(content[i]) > 1 {
if err := writeFile(filePath[i], strings.Join(content[i], "\n")); err != nil {
return err
}
}
}
return nil
}
func getBuiltinSectionNum(rules []rule) (int, error) {
for _, r := range rules {
if strings.Contains(r.URL, "builtin-functions") {
return r.Section, nil
}
}
return -1, fmt.Errorf("unable to find the built-in function section")
}
func isBuiltinFunctionRule(r rule) bool {
_, builtinName, _ := testName(r.URL, r.Description, r.SubSection)
return builtinName != "" || strings.Contains(r.URL, "builtin-functions")
}
func testPlan(r rule) string {
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf(unImplementedTestTemplate, r.TestName, r.Sha, r.URL,
"`\n"+r.Description+"\n"+howToContribute+"\n`"))
return sb.String()
}
const (
validationTestHeader = `export const description = %v;
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { ShaderValidationTest } from './shader_validation_test.js';
export const g = makeTestGroup(ShaderValidationTest);
`
executionTestHeader = `export const description = %v;
import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { GPUTest } from '../../../gpu_test.js';
export const g = makeTestGroup(GPUTest);
`
unImplementedTestTemplate = `g.test('%v')
.uniqueId('%v')
.specURL('%v')
.desc(
%v
)
.params(u => u.combine('placeHolder1', ['placeHolder2', 'placeHolder3']))
.unimplemented();
`
howToContribute = `
Please read the following guidelines before contributing:
https://github.com/gpuweb/cts/blob/main/docs/plan_autogen.md`
)