• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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