| // 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. |
| |
| // check-spec-examples tests that WGSL specification examples compile as |
| // expected. |
| // |
| // The tool parses the WGSL HTML specification from the web or from a local file |
| // and then runs the WGSL compiler for all examples annotated with the 'wgsl' |
| // and 'global-scope' or 'function-scope' HTML class types. |
| // |
| // To run: |
| // go get golang.org/x/net/html # Only required once |
| // go run tools/check-spec-examples/main.go --compiler=<path-to-tint> |
| package main |
| |
| import ( |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "net/url" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| |
| "golang.org/x/net/html" |
| ) |
| |
| const ( |
| toolName = "check-spec-examples" |
| defaultSpecPath = "https://gpuweb.github.io/gpuweb/wgsl/" |
| ) |
| |
| var ( |
| errInvalidArg = errors.New("Invalid arguments") |
| ) |
| |
| func main() { |
| flag.Usage = func() { |
| out := flag.CommandLine.Output() |
| fmt.Fprintf(out, "%v tests that WGSL specification examples compile as expected.\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 path or URL to the WGSL specification.\n") |
| fmt.Fprintf(out, "If spec is omitted then the specification is fetched from %v\n", defaultSpecPath) |
| fmt.Fprintf(out, "\n") |
| 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 |
| compilerPath := flag.String("compiler", "tint", "path to compiler executable") |
| verbose := flag.Bool("verbose", false, "print examples that pass") |
| flag.Parse() |
| |
| // Try to find the WGSL compiler |
| compiler, err := exec.LookPath(*compilerPath) |
| if err != nil { |
| return fmt.Errorf("Failed to find WGSL compiler: %w", err) |
| } |
| if compiler, err = filepath.Abs(compiler); err != nil { |
| return fmt.Errorf("Failed to find WGSL compiler: %w", err) |
| } |
| |
| // Check for explicit WGSL spec path |
| args := flag.Args() |
| specURL, _ := url.Parse(defaultSpecPath) |
| switch len(args) { |
| case 0: |
| case 1: |
| var err error |
| specURL, err = url.Parse(args[0]) |
| if err != nil { |
| return err |
| } |
| default: |
| if len(args) > 1 { |
| return 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 fmt.Errorf("Failed to load the WGSL spec from '%v': %w", specURL, err) |
| } |
| specContent = response.Body |
| case "file": |
| specURL.Path, err = filepath.Abs(specURL.Path) |
| if err != nil { |
| return fmt.Errorf("Failed to load the WGSL spec from '%v': %w", specURL, err) |
| } |
| |
| file, err := os.Open(specURL.Path) |
| if err != nil { |
| return fmt.Errorf("Failed to load the WGSL spec from '%v': %w", specURL, err) |
| } |
| specContent = file |
| default: |
| return fmt.Errorf("Unsupported URL scheme: %v", specURL.Scheme) |
| } |
| defer specContent.Close() |
| |
| // Create the HTML parser |
| doc, err := html.Parse(specContent) |
| if err != nil { |
| return err |
| } |
| |
| // Parse all the WGSL examples |
| examples := []example{} |
| if err := gatherExamples(doc, &examples); err != nil { |
| return err |
| } |
| |
| if len(examples) == 0 { |
| return fmt.Errorf("no examples found") |
| } |
| |
| // Create a temporary directory to hold the examples as separate files |
| tmpDir, err := ioutil.TempDir("", "wgsl-spec-examples") |
| if err != nil { |
| return err |
| } |
| if err := os.MkdirAll(tmpDir, 0666); err != nil { |
| return fmt.Errorf("Failed to create temporary directory: %w", err) |
| } |
| defer os.RemoveAll(tmpDir) |
| |
| // For each compilable WGSL example... |
| for _, e := range examples { |
| exampleURL := specURL.String() + "#" + e.name |
| |
| if err := tryCompile(compiler, tmpDir, e); err != nil { |
| if !e.expectError { |
| fmt.Printf("✘ %v ✘\n%v\n", exampleURL, err) |
| continue |
| } |
| } else if e.expectError { |
| fmt.Printf("✘ %v ✘\nCompiled even though it was marked with 'expect-error'\n", exampleURL) |
| } |
| if *verbose { |
| fmt.Printf("✔ %v ✔\n", exampleURL) |
| } |
| } |
| return nil |
| } |
| |
| // Holds all the information about a single, compilable WGSL example in the spec |
| type example struct { |
| name string // The name (typically hash generated by bikeshed) |
| code string // The example source |
| globalScope bool // Annotated with 'global-scope' ? |
| functionScope bool // Annotated with 'function-scope' ? |
| expectError bool // Annotated with 'expect-error' ? |
| } |
| |
| // tryCompile attempts to compile the example e in the directory wd, using the |
| // compiler at the given path. If the example is annotated with 'function-scope' |
| // then the code is wrapped with a basic vertex-stage-entry function. |
| // If the first compile fails then a dummy vertex-state-entry function is |
| // appended to the source, and another attempt to compile the shader is made. |
| func tryCompile(compiler, wd string, e example) error { |
| code := e.code |
| if e.functionScope { |
| code = "\n@stage(vertex) fn main() -> @builtin(position) vec4<f32> {\n" + code + " return vec4<f32>();}\n" |
| } |
| |
| addedStubFunction := false |
| for { |
| err := compile(compiler, wd, e.name, code) |
| if err == nil { |
| return nil |
| } |
| |
| if !addedStubFunction { |
| code += "\n@stage(vertex) fn main() {}\n" |
| addedStubFunction = true |
| continue |
| } |
| |
| return err |
| } |
| } |
| |
| // compile creates a file in wd and uses the compiler to attempt to compile it. |
| func compile(compiler, wd, name, code string) error { |
| filename := name + ".wgsl" |
| path := filepath.Join(wd, filename) |
| if err := ioutil.WriteFile(path, []byte(code), 0666); err != nil { |
| return fmt.Errorf("Failed to write example file '%v'", path) |
| } |
| cmd := exec.Command(compiler, filename) |
| cmd.Dir = wd |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| return fmt.Errorf("%v\n%v", err, string(out)) |
| } |
| return nil |
| } |
| |
| // gatherExamples scans the HTML node and its children for blocks that contain |
| // WGSL example code, populating the examples slice. |
| func gatherExamples(node *html.Node, examples *[]example) error { |
| if hasClass(node, "example") && hasClass(node, "wgsl") { |
| e := example{ |
| name: nodeID(node), |
| code: exampleCode(node), |
| globalScope: hasClass(node, "global-scope"), |
| functionScope: hasClass(node, "function-scope"), |
| expectError: hasClass(node, "expect-error"), |
| } |
| // If the example is annotated with a scope, then it can be compiled. |
| if e.globalScope || e.functionScope { |
| *examples = append(*examples, e) |
| } |
| } |
| for child := node.FirstChild; child != nil; child = child.NextSibling { |
| if err := gatherExamples(child, examples); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // exampleCode returns a string formed from all the TextNodes found in <pre> |
| // blocks that are children of node. |
| func exampleCode(node *html.Node) string { |
| sb := strings.Builder{} |
| for child := node.FirstChild; child != nil; child = child.NextSibling { |
| if child.Data == "pre" { |
| printNodeText(child, &sb) |
| } |
| } |
| return sb.String() |
| } |
| |
| // printNodeText traverses node and its children, writing the Data of all |
| // TextNodes to sb. |
| func printNodeText(node *html.Node, sb *strings.Builder) { |
| if node.Type == html.TextNode { |
| sb.WriteString(node.Data) |
| } |
| |
| for child := node.FirstChild; child != nil; child = child.NextSibling { |
| printNodeText(child, sb) |
| } |
| } |
| |
| // hasClass returns true iff 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 |
| } |
| |
| // nodeID returns the given "id" attribute of node, or an empty string if there |
| // is no "id" attribute. |
| func nodeID(node *html.Node) string { |
| for _, attr := range node.Attr { |
| if attr.Key == "id" { |
| return attr.Val |
| } |
| } |
| return "" |
| } |