// 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= --output= // Or run // cd tools/src && go run cmd/get-spec-rules/main.go --output= // // 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. `) ctsDir := flag.String("cts-directory", "", `if provided: validation cts test plan will be written to: '/validation/' builtin functions cts test plan will be written to: '/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. and

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. // 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 // // 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 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` )