blob: 733b34054be7383c26491c78329116208ac9d9c0 [file] [log] [blame] [edit]
// 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 ""
}