| // Copyright 2023 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // fuzz is a helper for running the tint fuzzer executables |
| package main |
| |
| import ( |
| "context" |
| "errors" |
| "flag" |
| "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "sync/atomic" |
| |
| "dawn.googlesource.com/dawn/tools/src/execwrapper" |
| "dawn.googlesource.com/dawn/tools/src/fileutils" |
| "dawn.googlesource.com/dawn/tools/src/glob" |
| "dawn.googlesource.com/dawn/tools/src/oswrapper" |
| "dawn.googlesource.com/dawn/tools/src/progressbar" |
| "dawn.googlesource.com/dawn/tools/src/transform" |
| "dawn.googlesource.com/dawn/tools/src/utils" |
| |
| "golang.org/x/sync/errgroup" |
| ) |
| |
| // TODO(crbug.com/416755658): Add unittest coverage when exec calls are done |
| // via dependency injection. |
| // TODO(crbug.com/344014313): Add unittests once fileutils and term are updated |
| // to support dependency injection. |
| // TODO(crbug.com/344014313): Add unittests once term is converted to support |
| // dependency injection. |
| |
| type TaskMode int |
| |
| func (t TaskMode) String() string { |
| switch t { |
| case TaskModeRun: |
| return "run" |
| case TaskModeCheck: |
| return "check" |
| case TaskModeGenerate: |
| return "generate" |
| case TaskModeTriage: |
| return "triage" |
| case TaskModeBisect: |
| return "bisect" |
| case TaskModeBisectStep: |
| return "bisect-step" |
| case TaskModeExperiment: |
| return "experiment" |
| default: |
| return fmt.Sprintf("<unknown> (%d)", t) |
| } |
| } |
| |
| const ( |
| TaskModeRun TaskMode = iota |
| TaskModeCheck |
| TaskModeGenerate |
| TaskModeTriage |
| TaskModeBisect |
| TaskModeBisectStep |
| TaskModeExperiment |
| TaskModeAnalyze |
| ) |
| |
| type FuzzMode int |
| |
| func (f FuzzMode) String() string { |
| switch f { |
| case FuzzModeWgsl: |
| return "wgsl" |
| case FuzzModeIr: |
| return "ir" |
| default: |
| return fmt.Sprintf("<unknown> (%d)", f) |
| } |
| } |
| |
| const ( |
| FuzzModeWgsl FuzzMode = iota |
| FuzzModeIr |
| ) |
| |
| // mainConfig represents the top-level configuration for the fuzz utility application. |
| // It stores the command-line flags and parameters parsed during startup, serving as |
| // the central config for deciding which tasks and modes are executed. |
| type mainConfig struct { |
| verbose bool |
| dump bool |
| timeout int |
| fuzzMode FuzzMode |
| cmdMode TaskMode // meta-task being requested by the user, may require running multiple tasks internally |
| mesaMode bool |
| filter string |
| inputs string |
| triageFile string |
| bisectFile string |
| knownFailing string |
| knownPassing string |
| bisectStep bool |
| isFix bool |
| skipInputTypeCheck bool |
| build string |
| out string |
| temporaryOut bool |
| numProcesses int |
| experimentPath string |
| analyzePath string |
| machineName string |
| osWrapper oswrapper.OSWrapper |
| execWrapper execwrapper.ExecWrapper |
| progressBuilder progressbar.Build |
| exitFn func(int) |
| } |
| |
| func newDefaultMainConfig() mainConfig { |
| c := mainConfig{} |
| c.osWrapper = oswrapper.GetRealOSWrapper() |
| c.execWrapper = execwrapper.CreateRealExecWrapper() |
| c.progressBuilder = progressbar.New |
| c.exitFn = os.Exit |
| return c |
| } |
| |
| type subcommand struct { |
| name string |
| description string |
| usageArgs string |
| registerFlags func(fs *flag.FlagSet, c *mainConfig, irMode *bool) |
| validate func(fs *flag.FlagSet, c *mainConfig) error |
| } |
| |
| var ( |
| runCmd = &subcommand{ |
| name: "run", |
| description: "run a fuzzer locally against test cases", |
| usageArgs: "[flags]", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.BoolVar(irMode, "ir", false, "runs using IR fuzzer instead of WGSL fuzzer") |
| fs.BoolVar(&c.mesaMode, "mesa", false, "runs using Mesa fuzzer variants") |
| fs.BoolVar(&c.dump, "dump", false, "dumps shader input/output from fuzzer") |
| fs.StringVar(&c.filter, "filter", "", "filter the fuzzing passes run to those with this substring") |
| fs.StringVar(&c.inputs, "corpus", defaultWgslCorpusDir(c.osWrapper), "obsolete, use -inputs instead") |
| fs.StringVar(&c.inputs, "inputs", defaultWgslCorpusDir(c.osWrapper), "the directory that holds the files to use") |
| fs.StringVar(&c.out, "out", "<tmp>", "the directory to store outputs to") |
| fs.IntVar(&c.numProcesses, "j", 0, "number of concurrent fuzzers to run (defaults to NumCPU)") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) > 0 { |
| return fmt.Errorf("unexpected arguments for run: %v", fs.Args()) |
| } |
| if c.mesaMode && c.filter != "" { |
| return fmt.Errorf("cannot set -mesa and -filter flags at the same time, as Mesa fuzzers only run a single specific pass") |
| } |
| if c.numProcesses < 1 { |
| c.numProcesses = runtime.NumCPU() |
| } |
| c.cmdMode = TaskModeRun |
| return nil |
| }, |
| } |
| |
| checkCmd = &subcommand{ |
| name: "check", |
| description: "check that all the end-to-end tests in -inputs do not fail", |
| usageArgs: "[flags]", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.BoolVar(irMode, "ir", false, "runs using IR fuzzer instead of WGSL fuzzer") |
| fs.BoolVar(&c.mesaMode, "mesa", false, "runs using Mesa fuzzer variants") |
| fs.StringVar(&c.inputs, "corpus", defaultWgslCorpusDir(c.osWrapper), "obsolete, use -inputs instead") |
| fs.StringVar(&c.inputs, "inputs", defaultWgslCorpusDir(c.osWrapper), "the directory that holds the files to use") |
| fs.StringVar(&c.out, "out", "<tmp>", "the directory to store outputs to") |
| fs.IntVar(&c.numProcesses, "j", 0, "number of concurrent fuzzers to run (defaults to NumCPU)") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) > 0 { |
| return fmt.Errorf("unexpected arguments for check: %v", fs.Args()) |
| } |
| if c.numProcesses < 1 { |
| c.numProcesses = runtime.NumCPU() |
| } |
| c.cmdMode = TaskModeCheck |
| return nil |
| }, |
| } |
| |
| generateCmd = &subcommand{ |
| name: "generate", |
| description: "generate fuzzing corpus based on -inputs", |
| usageArgs: "[flags]", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.BoolVar(irMode, "ir", false, "runs using IR fuzzer instead of WGSL fuzzer") |
| fs.BoolVar(&c.mesaMode, "mesa", false, "runs using Mesa fuzzer variants") |
| fs.StringVar(&c.inputs, "corpus", defaultWgslCorpusDir(c.osWrapper), "obsolete, use -inputs instead") |
| fs.StringVar(&c.inputs, "inputs", defaultWgslCorpusDir(c.osWrapper), "the directory that holds the files to use") |
| fs.StringVar(&c.out, "out", "<tmp>", "the directory to store outputs to") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) > 0 { |
| return fmt.Errorf("unexpected arguments for generate: %v", fs.Args()) |
| } |
| if c.out == "" || c.out == "<tmp>" { |
| return fmt.Errorf("need to specify -out when using generate") |
| } |
| c.cmdMode = TaskModeGenerate |
| return nil |
| }, |
| } |
| |
| triageCmd = &subcommand{ |
| name: "triage", |
| description: "triage a specific fuzzer crash test case", |
| usageArgs: "[flags] <file>", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.BoolVar(irMode, "ir", false, "runs using IR fuzzer instead of WGSL fuzzer") |
| fs.BoolVar(&c.mesaMode, "mesa", false, "runs using Mesa fuzzer variants") |
| fs.BoolVar(&c.skipInputTypeCheck, "skip-input-type-check", false, "bypass the heuristic text/binary input file type check") |
| fs.IntVar(&c.timeout, "timeout", 60, "override the default timeout (in seconds)") |
| fs.StringVar(&c.out, "out", "<tmp>", "the directory to store outputs to") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) == 0 { |
| return fmt.Errorf("triage requires a test case file path\nusage: fuzz triage [flags] <file>") |
| } |
| if len(fs.Args()) > 1 { |
| return fmt.Errorf("triage accepts only one test case file path\nusage: fuzz triage [flags] <file>") |
| } |
| c.triageFile = fs.Arg(0) |
| c.cmdMode = TaskModeTriage |
| return nil |
| }, |
| } |
| |
| bisectCmd = &subcommand{ |
| name: "bisect", |
| description: "bisect a specific fuzzer crash test case", |
| usageArgs: "[flags] <file>", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.BoolVar(irMode, "ir", false, "runs using IR fuzzer instead of WGSL fuzzer") |
| fs.BoolVar(&c.mesaMode, "mesa", false, "runs using Mesa fuzzer variants") |
| fs.StringVar(&c.knownFailing, "known-failing", "", "known failing git hash or time") |
| fs.StringVar(&c.knownPassing, "known-passing", "", "known passing git hash or time") |
| fs.BoolVar(&c.skipInputTypeCheck, "skip-input-type-check", false, "bypass the heuristic text/binary input file type check") |
| fs.IntVar(&c.timeout, "timeout", 60, "override the default timeout (in seconds)") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) == 0 { |
| return fmt.Errorf("bisect requires a test case file path\nusage: fuzz bisect [flags] <file>") |
| } |
| if len(fs.Args()) > 1 { |
| return fmt.Errorf("bisect accepts only one test case file path\nusage: fuzz bisect [flags] <file>") |
| } |
| if c.knownFailing == "" { |
| return fmt.Errorf("-known-failing flag is required when bisecting") |
| } |
| c.bisectFile = fs.Arg(0) |
| c.cmdMode = TaskModeBisect |
| return nil |
| }, |
| } |
| |
| bisectStepCmd = &subcommand{ |
| name: "bisect-step", |
| description: "internal step command invoked during git bisect run", |
| usageArgs: "[flags] <file>", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.BoolVar(irMode, "ir", false, "runs using IR fuzzer instead of WGSL fuzzer") |
| fs.BoolVar(&c.mesaMode, "mesa", false, "runs using Mesa fuzzer variants") |
| fs.BoolVar(&c.isFix, "is-fix", false, "internal flag used by git bisect run to indicate if we are bisecting a fix") |
| fs.IntVar(&c.timeout, "timeout", 60, "override the default timeout (in seconds)") |
| fs.StringVar(&c.bisectFile, "bisect", "", "test case file path (alternative to positional argument)") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) == 1 { |
| c.bisectFile = fs.Arg(0) |
| } else if len(fs.Args()) > 1 { |
| return fmt.Errorf("bisect-step accepts only one test case file path\nusage: fuzz bisect-step [flags] <file>") |
| } else if c.bisectFile == "" { |
| return fmt.Errorf("bisect-step requires a test case file path\nusage: fuzz bisect-step [flags] <file>") |
| } |
| c.bisectStep = true |
| c.cmdMode = TaskModeBisectStep |
| return nil |
| }, |
| } |
| |
| experimentCmd = &subcommand{ |
| name: "experiment", |
| description: "run performance and benchmarking experiments", |
| usageArgs: "[flags] <dir>", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.StringVar(&c.machineName, "machine", "", "machine name to identify results") |
| fs.IntVar(&c.numProcesses, "j", 0, "number of concurrent fuzzers to run (defaults to 1)") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) == 0 { |
| return fmt.Errorf("experiment requires an experiment root directory\nusage: fuzz experiment [flags] <dir>") |
| } |
| if len(fs.Args()) > 1 { |
| return fmt.Errorf("experiment accepts only one experiment root directory\nusage: fuzz experiment [flags] <dir>") |
| } |
| c.experimentPath = fs.Arg(0) |
| if c.numProcesses < 1 { |
| c.numProcesses = 1 |
| } |
| c.cmdMode = TaskModeExperiment |
| return nil |
| }, |
| } |
| |
| analyzeCmd = &subcommand{ |
| name: "analyze", |
| description: "analyze data from experiment runs", |
| usageArgs: "[flags] <dir>", |
| registerFlags: func(fs *flag.FlagSet, c *mainConfig, irMode *bool) { |
| fs.StringVar(&c.machineName, "machine", "", "machine name to identify results") |
| fs.IntVar(&c.numProcesses, "j", 0, "number of concurrent fuzzers to run (defaults to NumCPU)") |
| }, |
| validate: func(fs *flag.FlagSet, c *mainConfig) error { |
| if len(fs.Args()) == 0 { |
| return fmt.Errorf("analyze requires an experiment root directory\nusage: fuzz analyze [flags] <dir>") |
| } |
| if len(fs.Args()) > 1 { |
| return fmt.Errorf("analyze accepts only one experiment root directory\nusage: fuzz analyze [flags] <dir>") |
| } |
| c.analyzePath = fs.Arg(0) |
| if c.numProcesses < 1 { |
| c.numProcesses = runtime.NumCPU() |
| } |
| c.cmdMode = TaskModeAnalyze |
| return nil |
| }, |
| } |
| |
| subcommands = map[string]*subcommand{ |
| "run": runCmd, |
| "check": checkCmd, |
| "generate": generateCmd, |
| "triage": triageCmd, |
| "bisect": bisectCmd, |
| "bisect-step": bisectStepCmd, |
| "experiment": experimentCmd, |
| "analyze": analyzeCmd, |
| "help": nil, |
| } |
| ) |
| |
| func printTopLevelHelp(out io.Writer) { |
| fmt.Fprintln(out, `fuzz is a helper for running the tint fuzzer executables and other related tasks |
| |
| Usage: |
| fuzz <subcommand> [flags...] [args...] |
| |
| Available Subcommands: |
| run run a fuzzer locally against test cases |
| check check that all the end-to-end tests in -inputs do not fail |
| generate generate fuzzing corpus based on -inputs |
| triage triage a specific fuzzer crash test case |
| bisect bisect a fuzzer crash test case |
| bisect-step internal step command invoked during git bisect run |
| experiment run performance and benchmarking experiments |
| analyze analyze data from experiment runs |
| help show help for fuzz or a specific subcommand |
| |
| Common Flags (available across subcommands): |
| -build string |
| the build directory (default "out/active") |
| -verbose |
| print additional output |
| |
| Use 'fuzz help <subcommand>' or 'fuzz <subcommand> -help' for more information about a subcommand.`) |
| } |
| |
| func printSubcommandHelp(cmd *subcommand, c *mainConfig, out io.Writer) { |
| fs := flag.NewFlagSet(cmd.name, flag.ContinueOnError) |
| fs.SetOutput(out) |
| fs.Usage = func() { |
| fmt.Fprintf(out, "Usage: fuzz %s %s\n\n", cmd.name, cmd.usageArgs) |
| fmt.Fprintf(out, "%s\n\n", cmd.description) |
| fmt.Fprintf(out, "Flags:\n") |
| fs.PrintDefaults() |
| } |
| config := *c |
| fs.StringVar(&config.build, "build", defaultBuildDir(config.osWrapper), "the build directory") |
| fs.BoolVar(&config.verbose, "verbose", false, "print additional output") |
| var irMode bool |
| cmd.registerFlags(fs, &config, &irMode) |
| fs.Usage() |
| } |
| |
| func parseSubcommandFlags(args []string, c *mainConfig, stdout, stderr io.Writer) error { |
| subcmdName := args[0] |
| if subcmdName == "help" { |
| if len(args) == 1 { |
| printTopLevelHelp(stdout) |
| return flag.ErrHelp |
| } |
| targetCmd := subcommands[args[1]] |
| if targetCmd == nil { |
| return fmt.Errorf("unknown subcommand %q to get help for. Run 'fuzz help' for a list of available subcommands", args[1]) |
| } |
| printSubcommandHelp(targetCmd, c, stdout) |
| return flag.ErrHelp |
| } |
| |
| cmd := subcommands[subcmdName] |
| if cmd == nil { |
| return fmt.Errorf("unknown subcommand %q. Run 'fuzz help' for a list of available subcommands", subcmdName) |
| } |
| |
| fs := flag.NewFlagSet(subcmdName, flag.ContinueOnError) |
| fs.SetOutput(stderr) |
| fs.Usage = func() { |
| fmt.Fprintf(stderr, "Usage: fuzz %s %s\n\n", cmd.name, cmd.usageArgs) |
| fmt.Fprintf(stderr, "%s\n\n", cmd.description) |
| fmt.Fprintf(stderr, "Flags:\n") |
| fs.PrintDefaults() |
| } |
| |
| fs.StringVar(&c.build, "build", defaultBuildDir(c.osWrapper), "the build directory") |
| fs.BoolVar(&c.verbose, "verbose", false, "print additional output") |
| |
| var irMode bool |
| cmd.registerFlags(fs, c, &irMode) |
| |
| if err := fs.Parse(args[1:]); err != nil { |
| return err |
| } |
| |
| if irMode { |
| c.fuzzMode = FuzzModeIr |
| } else { |
| c.fuzzMode = FuzzModeWgsl |
| } |
| |
| if err := cmd.validate(fs, c); err != nil { |
| return err |
| } |
| |
| return nil |
| } |
| |
| // parseFlags parses the command line arguments to determine the subcommand to run and configures the mainConfig |
| // appropriately. If no subcommand is specified, it prints the top-level help and returns flag.ErrHelp. |
| func parseFlags(args []string, c *mainConfig, stdout, stderr io.Writer) error { |
| if len(args) == 0 { |
| fmt.Fprintln(stderr, "error: subcommand required") |
| printTopLevelHelp(stderr) |
| return flag.ErrHelp |
| } |
| |
| return parseSubcommandFlags(args, c, stdout, stderr) |
| } |
| |
| func main() { |
| c := newDefaultMainConfig() |
| err := parseFlags(os.Args[1:], &c, os.Stdout, os.Stderr) |
| if err != nil { |
| if errors.Is(err, flag.ErrHelp) { |
| os.Exit(0) |
| } |
| fmt.Fprintln(os.Stderr, err) |
| os.Exit(1) |
| } |
| |
| if err := run(&c); err != nil { |
| fmt.Println(err) |
| os.Exit(1) |
| } |
| } |
| |
| type taskConfig struct { |
| mainConfig |
| taskMode TaskMode // specific task being run at this time, may be different from mainConfig.cmdMode |
| fuzzer string // path to the fuzzer binary, tint_wgsl_fuzzer or tint_ir_fuzzer |
| assembler string // path to the test case assembler, tint_fuzz_as |
| dictionary string // path to dictionary to use for tint_wgsl_fuzzer |
| } |
| |
| // runCmd executes a command with standardized output capturing and logging behavior. |
| func (t *taskConfig) runCmd(name string, args ...string) ([]byte, error) { |
| if t.verbose { |
| fmt.Printf("executing: %s %s\n", name, strings.Join(args, " ")) |
| } |
| cmd := t.execWrapper.Command(name, args...) |
| out, err := cmd.RunWithCombinedOutput() |
| if t.verbose { |
| fmt.Printf("output:\n%s\n", string(out)) |
| } |
| return out, err |
| } |
| |
| // runCmdUnbuffered executes a command with standardized logging, mapping output directly to the terminal. |
| // It does not capture the output. |
| func (t *taskConfig) runCmdUnbuffered(name string, args ...string) error { |
| if t.verbose { |
| fmt.Printf("executing: %s %s\n", name, strings.Join(args, " ")) |
| } |
| cmd := t.execWrapper.Command(name, args...).WithStdout(os.Stdout).WithStderr(os.Stderr) |
| return cmd.Run() |
| } |
| |
| // atGitHash saves the current git state, checks out a specific hash, executes a |
| // function, and then restores the original git state. |
| func (t *taskConfig) atGitHash(hash string, fn func() error) error { |
| origRefBytes, err := t.runCmd("git", "rev-parse", "--abbrev-ref", "HEAD") |
| if err != nil { |
| return fmt.Errorf("failed to get original HEAD reference: %w", err) |
| } |
| origRef := strings.TrimSpace(string(origRefBytes)) |
| if origRef == "HEAD" { |
| origHeadBytes, err := t.runCmd("git", "rev-parse", "HEAD") |
| if err != nil { |
| return fmt.Errorf("failed to get original HEAD commit: %w", err) |
| } |
| origRef = strings.TrimSpace(string(origHeadBytes)) |
| } |
| |
| defer func() { |
| fmt.Printf("Restoring repository to original state (%s)...\n", origRef) |
| _, _ = t.runCmd("git", "checkout", origRef) |
| _, _ = t.runCmd("gclient", "sync") |
| }() |
| |
| fmt.Printf("Syncing repository to hash %s...\n", hash) |
| if _, err := t.runCmd("git", "checkout", hash); err != nil { |
| return fmt.Errorf("failed to checkout %s: %w", hash, err) |
| } |
| |
| return fn() |
| } |
| |
| func run(c *mainConfig) error { |
| // Verify / create the directories needed for writing |
| switch c.cmdMode { |
| case TaskModeExperiment, TaskModeAnalyze: |
| // output/build directory checking is part of runExperiment/Analyze, since the expected locations/content are based on |
| // the values in the experiment config file. |
| case TaskModeRun, TaskModeCheck, TaskModeGenerate, TaskModeTriage: |
| // These modes allow for using a temporary directory |
| if c.out == "" || c.out == "<tmp>" { |
| if tmp, err := c.osWrapper.MkdirTemp("", "tint_fuzz"); err == nil { |
| defer c.osWrapper.RemoveAll(tmp) |
| c.out = tmp |
| c.temporaryOut = true |
| } else { |
| return err |
| } |
| break |
| } |
| fallthrough |
| default: |
| // If temporary directories are allowed, c.out should already have been set to the created value by this point |
| if c.out == "" || c.out == "<tmp>" { |
| return fmt.Errorf("temporary output is not allowed for '%v'", c.cmdMode) |
| } |
| |
| err := c.osWrapper.MkdirAll(c.out, os.ModePerm) |
| if err != nil { |
| return err |
| } |
| |
| // Check the build directory |
| if !fileutils.IsDir(c.build, c.osWrapper) { |
| return fmt.Errorf("build directory '%v' does not exist", c.build) |
| } |
| } |
| |
| queue := make([]*taskConfig, 0, 1) |
| |
| if c.cmdMode == TaskModeRun || c.cmdMode == TaskModeCheck { |
| // Preprocess inputs when pointing at the default test directory: |
| // - Strips comments and copyright headers from .wgsl files |
| // - Converts .wgsl files to .tirb files for IR mode |
| if c.inputs == defaultWgslCorpusDir(c.osWrapper) { |
| origOut := c.out |
| tmp, err := c.osWrapper.MkdirTemp("", "fuzz_corpus") |
| if err != nil { |
| return fmt.Errorf("failed to create temporary directory for fuzz corpus: %w", err) |
| } |
| defer c.osWrapper.RemoveAll(tmp) |
| |
| c.out = tmp |
| t, err := generateTaskConfig(TaskModeGenerate, c) |
| if err != nil { |
| return fmt.Errorf("failed to generate task config for fuzz corpus generation: %w", err) |
| } |
| queue = append(queue, t) |
| |
| c.out = origOut |
| c.inputs = tmp |
| } |
| } |
| |
| t, err := generateTaskConfig(c.cmdMode, c) |
| if err != nil { |
| return fmt.Errorf("failed to generate task config for command mode %v: %w", c.cmdMode, err) |
| } |
| queue = append(queue, t) |
| |
| for _, t := range queue { |
| var err error |
| switch t.taskMode { |
| case TaskModeRun: |
| err = runFuzzer(t) |
| case TaskModeCheck: |
| err = checkFuzzer(t) |
| case TaskModeGenerate: |
| err = runCorpusGenerator(t) |
| case TaskModeTriage: |
| err = runTriage(t) |
| case TaskModeBisect: |
| err = runBisect(t) |
| case TaskModeBisectStep: |
| err = runBisectStep(t) |
| case TaskModeExperiment: |
| err = runExperiment(t) |
| case TaskModeAnalyze: |
| err = runAnalyze(t) |
| default: |
| err = fmt.Errorf("unknown task mode %v", t.taskMode) |
| } |
| if err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // generateTaskConfig produces a taskConfig based off the supplied mainConfig and specified TaskMode. |
| func generateTaskConfig(tm TaskMode, c *mainConfig) (*taskConfig, error) { |
| t := taskConfig{ |
| mainConfig: *c, |
| taskMode: tm, |
| } |
| |
| type depConfig struct { |
| name string |
| path *string |
| } |
| dependencies := make([]depConfig, 0) |
| switch tm { |
| case TaskModeRun: |
| if c.fuzzMode == FuzzModeWgsl { |
| dependencies = append(dependencies, depConfig{"dictionary.txt", &t.dictionary}) |
| } |
| fallthrough |
| case TaskModeCheck, TaskModeTriage, TaskModeBisect, TaskModeBisectStep: |
| fuzzerName := "tint_wgsl_fuzzer" |
| if c.fuzzMode == FuzzModeIr { |
| fuzzerName = "tint_ir_fuzzer" |
| } |
| if c.mesaMode { |
| fuzzerName = strings.Replace(fuzzerName, "_fuzzer", "_mesa_fuzzer", 1) |
| } |
| dependencies = append(dependencies, depConfig{fuzzerName, &t.fuzzer}) |
| |
| if tm == TaskModeTriage { |
| if c.fuzzMode == FuzzModeIr { |
| dependencies = append(dependencies, depConfig{"ir_fuzz_dis", &t.assembler}) |
| } else { |
| dependencies = append(dependencies, depConfig{"ir_fuzz_as", &t.assembler}) |
| } |
| } |
| case TaskModeGenerate: |
| if c.fuzzMode == FuzzModeIr { |
| dependencies = append(dependencies, depConfig{"ir_fuzz_as", &t.assembler}) |
| } |
| case TaskModeExperiment, TaskModeAnalyze: |
| // No dependencies required to be pre-validated by this helper |
| } |
| |
| // Verify all the required dependencies are present |
| for _, config := range dependencies { |
| switch { |
| case filepath.Ext(config.name) == ".txt": |
| *config.path = filepath.Join(filepath.Join(fileutils.DawnRoot(c.osWrapper), "src", "tint", "cmd", "fuzz", "wgsl"), config.name) |
| if !fileutils.IsFile(*config.path, t.osWrapper) { |
| return nil, fmt.Errorf("resource '%v' not found at '%v'. Please ensure the Dawn repository is correctly cloned and up-to-date", config.name, *config.path) |
| } |
| default: |
| *config.path = filepath.Join(t.build, config.name+fileutils.ExeExt) |
| if tm != TaskModeBisect && tm != TaskModeBisectStep && !fileutils.IsExe(*config.path, t.osWrapper) { |
| return nil, fmt.Errorf("binary '%v' not found at '%v'. Please ensure the project has been built (e.g., with `ninja -C %s %s`)", config.name, *config.path, t.build, config.name) |
| } |
| } |
| } |
| |
| return &t, nil |
| } |
| |
| // checkFuzzer runs the fuzzer against all the test files the inputs directory, |
| // ensuring that the fuzzers do not error for the given file. |
| func checkFuzzer(t *taskConfig) error { |
| var files []string |
| var err error |
| switch t.fuzzMode { |
| case FuzzModeIr: |
| files, err = glob.Glob(filepath.Join(t.inputs, "**.tirb"), t.osWrapper) |
| case FuzzModeWgsl: |
| files, err = glob.Glob(filepath.Join(t.inputs, "**.wgsl"), t.osWrapper) |
| default: |
| err = fmt.Errorf("unknown fuzzer mode %v", t.fuzzMode) |
| } |
| if err != nil { |
| return err |
| } |
| |
| // Remove '*.expected.*' |
| files = transform.Filter(files, func(s string) bool { return !strings.Contains(s, ".expected.") }) |
| |
| fmt.Printf("checking %v files...\n", len(files)) |
| |
| remaining := transform.SliceToChan(files) |
| |
| pb := t.progressBuilder(os.Stdout, nil) |
| defer pb.Stop() |
| var numDone uint32 |
| |
| failureChan := make(chan error, len(files)) |
| |
| eg, ctx := errgroup.WithContext(utils.CancelOnInterruptContext(context.Background())) |
| for i := 0; i < t.numProcesses; i++ { |
| eg.Go(func() error { |
| for { |
| select { |
| case <-ctx.Done(): |
| return ctx.Err() |
| case file, ok := <-remaining: |
| if !ok { |
| return nil |
| } |
| atomic.AddUint32(&numDone, 1) |
| pb.Update(progressbar.Status{ |
| Total: len(files), |
| Segments: []progressbar.Segment{ |
| {Count: int(atomic.LoadUint32(&numDone))}, |
| }, |
| }) |
| |
| if out, err := t.runCmd(t.fuzzer, file); err != nil { |
| _, fuzzer := filepath.Split(t.fuzzer) |
| failureChan <- fmt.Errorf("fuzzer '%s' failed to process file '%s' with error: %w\nOutput:\n%s", fuzzer, file, err, string(out)) |
| } |
| } |
| } |
| }) |
| } |
| |
| if err := eg.Wait(); err != nil && !errors.Is(err, context.Canceled) { |
| return err |
| } |
| |
| close(failureChan) |
| |
| if len(failureChan) > 0 { |
| runFailures := make([]error, 0, len(failureChan)) |
| for err := range failureChan { |
| runFailures = append(runFailures, err) |
| } |
| return errors.Join(runFailures...) |
| } |
| |
| fmt.Println("done") |
| return nil |
| } |
| |
| func defaultWgslCorpusDir(fsReader oswrapper.FilesystemReader) string { |
| return filepath.Join(fileutils.DawnRoot(fsReader), "test", "tint") |
| } |
| |
| func defaultBuildDir(fsReader oswrapper.FilesystemReader) string { |
| return filepath.Join(fileutils.DawnRoot(fsReader), "out", "active") |
| } |
| |
| // checkInputFileType performs a heuristic check on a single input file to ensure it matches |
| // the expected file type for the given fuzzer mode. WGSL mode expects text files, while IR |
| // mode expects binary files. It returns an error if the file type appears incorrect. This may |
| // have false positives, so there is a CLI escape hatch |
| func checkInputFileType(filePath string, mode FuzzMode, fsReader oswrapper.FilesystemReader) error { |
| content, err := fsReader.ReadFile(filePath) |
| if err != nil { |
| return fmt.Errorf("failed to read input file for type check: %w", err) |
| } |
| |
| isText := true |
| for _, b := range content { |
| // If byte is null or a control character other than newline, carriage return, or tab, consider it binary. |
| if b < 0x20 && b != '\n' && b != '\r' && b != '\t' { |
| isText = false |
| break |
| } |
| } |
| |
| if mode == FuzzModeWgsl && !isText { |
| return fmt.Errorf("wrong file type detected: expected a text file for WGSL mode, but '%s' appears to be binary. Did you forget to add the -ir flag? (If you are sure this is correct, use -skip-input-type-check)", filePath) |
| } else if mode == FuzzModeIr && isText && len(content) > 0 { |
| return fmt.Errorf("wrong file type detected: expected a binary file for IR mode, but '%s' appears to be text. Did you mean to remove the -ir flag? (If you are sure this is correct, use -skip-input-type-check)", filePath) |
| } |
| |
| return nil |
| } |