1// Copyright 2021 The Tint Authors. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// check-spec-examples tests that WGSL specification examples compile as 16// expected. 17// 18// The tool parses the WGSL HTML specification from the web or from a local file 19// and then runs the WGSL compiler for all examples annotated with the 'wgsl' 20// and 'global-scope' or 'function-scope' HTML class types. 21// 22// To run: 23// go get golang.org/x/net/html # Only required once 24// go run tools/check-spec-examples/main.go --compiler=<path-to-tint> 25package main 26 27import ( 28 "errors" 29 "flag" 30 "fmt" 31 "io" 32 "io/ioutil" 33 "net/http" 34 "net/url" 35 "os" 36 "os/exec" 37 "path/filepath" 38 "strings" 39 40 "golang.org/x/net/html" 41) 42 43const ( 44 toolName = "check-spec-examples" 45 defaultSpecPath = "https://gpuweb.github.io/gpuweb/wgsl/" 46) 47 48var ( 49 errInvalidArg = errors.New("Invalid arguments") 50) 51 52func main() { 53 flag.Usage = func() { 54 out := flag.CommandLine.Output() 55 fmt.Fprintf(out, "%v tests that WGSL specification examples compile as expected.\n", toolName) 56 fmt.Fprintf(out, "\n") 57 fmt.Fprintf(out, "Usage:\n") 58 fmt.Fprintf(out, " %s [spec] [flags]\n", toolName) 59 fmt.Fprintf(out, "\n") 60 fmt.Fprintf(out, "spec is an optional local file path or URL to the WGSL specification.\n") 61 fmt.Fprintf(out, "If spec is omitted then the specification is fetched from %v\n", defaultSpecPath) 62 fmt.Fprintf(out, "\n") 63 fmt.Fprintf(out, "flags may be any combination of:\n") 64 flag.PrintDefaults() 65 } 66 67 err := run() 68 switch err { 69 case nil: 70 return 71 case errInvalidArg: 72 fmt.Fprintf(os.Stderr, "Error: %v\n\n", err) 73 flag.Usage() 74 default: 75 fmt.Fprintf(os.Stderr, "%v\n", err) 76 } 77 os.Exit(1) 78} 79 80func run() error { 81 // Parse flags 82 compilerPath := flag.String("compiler", "tint", "path to compiler executable") 83 verbose := flag.Bool("verbose", false, "print examples that pass") 84 flag.Parse() 85 86 // Try to find the WGSL compiler 87 compiler, err := exec.LookPath(*compilerPath) 88 if err != nil { 89 return fmt.Errorf("Failed to find WGSL compiler: %w", err) 90 } 91 if compiler, err = filepath.Abs(compiler); err != nil { 92 return fmt.Errorf("Failed to find WGSL compiler: %w", err) 93 } 94 95 // Check for explicit WGSL spec path 96 args := flag.Args() 97 specURL, _ := url.Parse(defaultSpecPath) 98 switch len(args) { 99 case 0: 100 case 1: 101 var err error 102 specURL, err = url.Parse(args[0]) 103 if err != nil { 104 return err 105 } 106 default: 107 if len(args) > 1 { 108 return errInvalidArg 109 } 110 } 111 112 // The specURL might just be a local file path, in which case automatically 113 // add the 'file' URL scheme 114 if specURL.Scheme == "" { 115 specURL.Scheme = "file" 116 } 117 118 // Open the spec from HTTP(S) or from a local file 119 var specContent io.ReadCloser 120 switch specURL.Scheme { 121 case "http", "https": 122 response, err := http.Get(specURL.String()) 123 if err != nil { 124 return fmt.Errorf("Failed to load the WGSL spec from '%v': %w", specURL, err) 125 } 126 specContent = response.Body 127 case "file": 128 specURL.Path, err = filepath.Abs(specURL.Path) 129 if err != nil { 130 return fmt.Errorf("Failed to load the WGSL spec from '%v': %w", specURL, err) 131 } 132 133 file, err := os.Open(specURL.Path) 134 if err != nil { 135 return fmt.Errorf("Failed to load the WGSL spec from '%v': %w", specURL, err) 136 } 137 specContent = file 138 default: 139 return fmt.Errorf("Unsupported URL scheme: %v", specURL.Scheme) 140 } 141 defer specContent.Close() 142 143 // Create the HTML parser 144 doc, err := html.Parse(specContent) 145 if err != nil { 146 return err 147 } 148 149 // Parse all the WGSL examples 150 examples := []example{} 151 if err := gatherExamples(doc, &examples); err != nil { 152 return err 153 } 154 155 if len(examples) == 0 { 156 return fmt.Errorf("no examples found") 157 } 158 159 // Create a temporary directory to hold the examples as separate files 160 tmpDir, err := ioutil.TempDir("", "wgsl-spec-examples") 161 if err != nil { 162 return err 163 } 164 if err := os.MkdirAll(tmpDir, 0666); err != nil { 165 return fmt.Errorf("Failed to create temporary directory: %w", err) 166 } 167 defer os.RemoveAll(tmpDir) 168 169 // For each compilable WGSL example... 170 for _, e := range examples { 171 exampleURL := specURL.String() + "#" + e.name 172 173 if err := tryCompile(compiler, tmpDir, e); err != nil { 174 if !e.expectError { 175 fmt.Printf("✘ %v ✘\n%v\n", exampleURL, err) 176 continue 177 } 178 } else if e.expectError { 179 fmt.Printf("✘ %v ✘\nCompiled even though it was marked with 'expect-error'\n", exampleURL) 180 } 181 if *verbose { 182 fmt.Printf("✔ %v ✔\n", exampleURL) 183 } 184 } 185 return nil 186} 187 188// Holds all the information about a single, compilable WGSL example in the spec 189type example struct { 190 name string // The name (typically hash generated by bikeshed) 191 code string // The example source 192 globalScope bool // Annotated with 'global-scope' ? 193 functionScope bool // Annotated with 'function-scope' ? 194 expectError bool // Annotated with 'expect-error' ? 195} 196 197// tryCompile attempts to compile the example e in the directory wd, using the 198// compiler at the given path. If the example is annotated with 'function-scope' 199// then the code is wrapped with a basic vertex-stage-entry function. 200// If the first compile fails then a dummy vertex-state-entry function is 201// appended to the source, and another attempt to compile the shader is made. 202func tryCompile(compiler, wd string, e example) error { 203 code := e.code 204 if e.functionScope { 205 code = "\n[[stage(vertex)]] fn main() -> [[builtin(position)]] vec4<f32> {\n" + code + " return vec4<f32>();}\n" 206 } 207 208 addedStubFunction := false 209 for { 210 err := compile(compiler, wd, e.name, code) 211 if err == nil { 212 return nil 213 } 214 215 if !addedStubFunction { 216 code += "\n[[stage(vertex)]] fn main() {}\n" 217 addedStubFunction = true 218 continue 219 } 220 221 return err 222 } 223} 224 225// compile creates a file in wd and uses the compiler to attempt to compile it. 226func compile(compiler, wd, name, code string) error { 227 filename := name + ".wgsl" 228 path := filepath.Join(wd, filename) 229 if err := ioutil.WriteFile(path, []byte(code), 0666); err != nil { 230 return fmt.Errorf("Failed to write example file '%v'", path) 231 } 232 cmd := exec.Command(compiler, filename) 233 cmd.Dir = wd 234 out, err := cmd.CombinedOutput() 235 if err != nil { 236 return fmt.Errorf("%v\n%v", err, string(out)) 237 } 238 return nil 239} 240 241// gatherExamples scans the HTML node and its children for blocks that contain 242// WGSL example code, populating the examples slice. 243func gatherExamples(node *html.Node, examples *[]example) error { 244 if hasClass(node, "example") && hasClass(node, "wgsl") { 245 e := example{ 246 name: nodeID(node), 247 code: exampleCode(node), 248 globalScope: hasClass(node, "global-scope"), 249 functionScope: hasClass(node, "function-scope"), 250 expectError: hasClass(node, "expect-error"), 251 } 252 // If the example is annotated with a scope, then it can be compiled. 253 if e.globalScope || e.functionScope { 254 *examples = append(*examples, e) 255 } 256 } 257 for child := node.FirstChild; child != nil; child = child.NextSibling { 258 if err := gatherExamples(child, examples); err != nil { 259 return err 260 } 261 } 262 return nil 263} 264 265// exampleCode returns a string formed from all the TextNodes found in <pre> 266// blocks that are children of node. 267func exampleCode(node *html.Node) string { 268 sb := strings.Builder{} 269 for child := node.FirstChild; child != nil; child = child.NextSibling { 270 if child.Data == "pre" { 271 printNodeText(child, &sb) 272 } 273 } 274 return sb.String() 275} 276 277// printNodeText traverses node and its children, writing the Data of all 278// TextNodes to sb. 279func printNodeText(node *html.Node, sb *strings.Builder) { 280 if node.Type == html.TextNode { 281 sb.WriteString(node.Data) 282 } 283 284 for child := node.FirstChild; child != nil; child = child.NextSibling { 285 printNodeText(child, sb) 286 } 287} 288 289// hasClass returns true iff node is has the given "class" attribute. 290func hasClass(node *html.Node, class string) bool { 291 for _, attr := range node.Attr { 292 if attr.Key == "class" { 293 classes := strings.Split(attr.Val, " ") 294 for _, c := range classes { 295 if c == class { 296 return true 297 } 298 } 299 } 300 } 301 return false 302} 303 304// nodeID returns the given "id" attribute of node, or an empty string if there 305// is no "id" attribute. 306func nodeID(node *html.Node) string { 307 for _, attr := range node.Attr { 308 if attr.Key == "id" { 309 return attr.Val 310 } 311 } 312 return "" 313} 314