diff options
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/syz-testbed/checkout.go | 53 | ||||
| -rw-r--r-- | tools/syz-testbed/html.go | 8 | ||||
| -rw-r--r-- | tools/syz-testbed/instance.go | 134 | ||||
| -rw-r--r-- | tools/syz-testbed/stats.go | 29 | ||||
| -rw-r--r-- | tools/syz-testbed/targets.go | 83 | ||||
| -rw-r--r-- | tools/syz-testbed/testbed.go | 178 |
6 files changed, 312 insertions, 173 deletions
diff --git a/tools/syz-testbed/checkout.go b/tools/syz-testbed/checkout.go index 3a4265013..75e79d7a3 100644 --- a/tools/syz-testbed/checkout.go +++ b/tools/syz-testbed/checkout.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "path/filepath" + "sync" "time" syz_instance "github.com/google/syzkaller/pkg/instance" @@ -16,23 +17,52 @@ import ( ) type Checkout struct { - Path string - Name string + Path string + Name string + ManagerConfig json.RawMessage - Running []*Instance - Completed []*RunResult + Running map[Instance]bool + Completed []RunResult + LastRunning time.Time + mu sync.Mutex +} + +func (checkout *Checkout) AddRunning(instance Instance) { + checkout.mu.Lock() + defer checkout.mu.Unlock() + checkout.Running[instance] = true + checkout.LastRunning = time.Now() } -func (checkout *Checkout) ArchiveRunning() error { - for _, instance := range checkout.Running { +func (checkout *Checkout) ArchiveInstance(instance Instance) error { + checkout.mu.Lock() + defer checkout.mu.Unlock() + result, err := instance.FetchResult() + if err != nil { + return err + } + checkout.Completed = append(checkout.Completed, result) + delete(checkout.Running, instance) + return nil +} + +func (checkout *Checkout) GetRunningResults() []RunResult { + checkout.mu.Lock() + defer checkout.mu.Unlock() + running := []RunResult{} + for instance := range checkout.Running { result, err := instance.FetchResult() - if err != nil { - return err + if err == nil { + running = append(running, result) } - checkout.Completed = append(checkout.Completed, result) } - checkout.Running = []*Instance{} - return nil + return running +} + +func (checkout *Checkout) GetCompletedResults() []RunResult { + checkout.mu.Lock() + defer checkout.mu.Unlock() + return append([]RunResult{}, checkout.Completed...) } func (ctx *TestbedContext) NewCheckout(config *CheckoutConfig, mgrConfig json.RawMessage) (*Checkout, error) { @@ -40,6 +70,7 @@ func (ctx *TestbedContext) NewCheckout(config *CheckoutConfig, mgrConfig json.Ra Name: config.Name, Path: filepath.Join(ctx.Config.Workdir, "checkouts", config.Name), ManagerConfig: mgrConfig, + Running: make(map[Instance]bool), } log.Printf("[%s] Checking out", checkout.Name) if osutil.IsExist(checkout.Path) { diff --git a/tools/syz-testbed/html.go b/tools/syz-testbed/html.go index 5a9b2f0b9..f76601a41 100644 --- a/tools/syz-testbed/html.go +++ b/tools/syz-testbed/html.go @@ -166,11 +166,17 @@ type uiMainPage struct { } func (ctx *TestbedContext) getTableTypes() []uiTableType { - typeList := []uiTableType{ + allTypeList := []uiTableType{ {HTMLStatsTable, "Statistics", ctx.httpMainStatsTable}, {HTMLBugsTable, "Bugs", ctx.genSimpleTableController((StatView).GenerateBugTable, true)}, {HTMLBugCountsTable, "Bug Counts", ctx.genSimpleTableController((StatView).GenerateBugCountsTable, false)}, } + typeList := []uiTableType{} + for _, t := range allTypeList { + if ctx.Target.SupportsHTMLView(t.Key) { + typeList = append(typeList, t) + } + } return typeList } diff --git a/tools/syz-testbed/instance.go b/tools/syz-testbed/instance.go index 888ad9ff7..f34df8a98 100644 --- a/tools/syz-testbed/instance.go +++ b/tools/syz-testbed/instance.go @@ -14,28 +14,35 @@ import ( "github.com/google/syzkaller/pkg/osutil" ) +type Instance interface { + Run() error + Stop() + FetchResult() (RunResult, error) +} + // The essential information about an active instance. -type Instance struct { +type InstanceCommon struct { Name string - Workdir string - BenchFile string LogFile string ExecCommand string ExecCommandArgs []string stopChannel chan bool } -func (inst *Instance) Run() error { +func (inst *InstanceCommon) Run() error { const stopDelay = time.Minute - logfile, err := os.Create(inst.LogFile) - if err != nil { - return fmt.Errorf("[%s] failed to create logfile: %s", inst.Name, err) - } log.Printf("[%s] starting", inst.Name) cmd := osutil.GraciousCommand(inst.ExecCommand, inst.ExecCommandArgs...) - cmd.Stdout = logfile - cmd.Stderr = logfile + + if inst.LogFile != "" { + logfile, err := os.Create(inst.LogFile) + if err != nil { + return fmt.Errorf("[%s] failed to create logfile: %s", inst.Name, err) + } + cmd.Stdout = logfile + cmd.Stderr = logfile + } complete := make(chan error) cmd.Start() @@ -44,8 +51,8 @@ func (inst *Instance) Run() error { }() select { - case err := <-complete: - return fmt.Errorf("[%s] stopped: %s", inst.Name, err) + case <-complete: + return nil case <-inst.stopChannel: // TODO: handle other OSes? cmd.Process.Signal(os.Interrupt) @@ -62,14 +69,20 @@ func (inst *Instance) Run() error { } } -func (inst *Instance) Stop() { +func (inst *InstanceCommon) Stop() { select { case inst.stopChannel <- true: default: } } -func (inst *Instance) FetchResult() (*RunResult, error) { +type SyzManagerInstance struct { + InstanceCommon + SyzkallerInfo + RunTime time.Duration +} + +func (inst *SyzManagerInstance) FetchResult() (RunResult, error) { bugs, err := collectBugs(inst.Workdir) if err != nil { return nil, err @@ -78,39 +91,45 @@ func (inst *Instance) FetchResult() (*RunResult, error) { if err != nil { return nil, err } - return &RunResult{ - Workdir: inst.Workdir, + return &SyzManagerResult{ Bugs: bugs, StatRecords: records, }, nil } -func (ctx *TestbedContext) NewInstance(checkout *Checkout, mgrName string) (*Instance, error) { - defer func() { - ctx.NextInstanceID++ +func (inst *SyzManagerInstance) Run() error { + ret := make(chan error, 1) + go func() { + ret <- inst.InstanceCommon.Run() }() - name := fmt.Sprintf("%s-%d", checkout.Name, ctx.NextInstanceID) - managerCfgPath := filepath.Join(checkout.Path, fmt.Sprintf("syz-%d.cnf", ctx.NextInstanceID)) - workdir := filepath.Join(checkout.Path, fmt.Sprintf("workdir_%d", ctx.NextInstanceID)) - bench := filepath.Join(checkout.Path, fmt.Sprintf("bench-%d.txt", ctx.NextInstanceID)) - logFile := filepath.Join(checkout.Path, fmt.Sprintf("log-%d.txt", ctx.NextInstanceID)) - log.Printf("[%s] Generating workdir", name) + select { + case err := <-ret: + // Syz-managers are not supposed to stop themselves under normal circumstances. + // If one of them did stop, there must have been a very good reason to do so. + return fmt.Errorf("[%s] stopped: %v", inst.Name, err) + case <-time.After(inst.RunTime): + inst.Stop() + <-ret + return nil + } +} + +type SyzkallerInfo struct { + Workdir string + CfgFile string + BenchFile string +} + +func SetupSyzkallerInstance(mgrName, folder string, checkout *Checkout) (*SyzkallerInfo, error) { + workdir := filepath.Join(folder, "workdir") + log.Printf("[%s] Generating workdir", mgrName) err := osutil.MkdirAll(workdir) if err != nil { return nil, fmt.Errorf("failed to create workdir %s", workdir) } - - if ctx.Config.Corpus != "" { - log.Printf("[%s] Copying corpus", name) - corpusPath := filepath.Join(workdir, "corpus.db") - err = osutil.CopyFile(ctx.Config.Corpus, corpusPath) - if err != nil { - return nil, fmt.Errorf("failed to copy corpus from %s: %s", ctx.Config.Corpus, err) - } - } - - log.Printf("[%s] Generating syz-manager config", name) + log.Printf("[%s] Generating syz-manager config", mgrName) + cfgFile := filepath.Join(folder, "manager.cfg") managerCfg, err := config.PatchJSON(checkout.ManagerConfig, map[string]interface{}{ "name": mgrName, "workdir": workdir, @@ -119,19 +138,40 @@ func (ctx *TestbedContext) NewInstance(checkout *Checkout, mgrName string) (*Ins if err != nil { return nil, fmt.Errorf("failed to patch mgr config") } - - err = osutil.WriteFile(managerCfgPath, managerCfg) + err = osutil.WriteFile(cfgFile, managerCfg) if err != nil { - return nil, fmt.Errorf("failed to save manager config to %s: %s", managerCfgPath, err) + return nil, fmt.Errorf("failed to save manager config to %s: %s", cfgFile, err) } + return &SyzkallerInfo{ + Workdir: workdir, + CfgFile: cfgFile, + BenchFile: filepath.Join(folder, "bench.txt"), + }, nil +} - return &Instance{ - Name: name, - Workdir: workdir, - BenchFile: bench, - LogFile: logFile, - ExecCommand: filepath.Join(checkout.Path, "bin", "syz-manager"), - ExecCommandArgs: []string{"-config", managerCfgPath, "-bench", bench}, - stopChannel: make(chan bool, 1), +func (t *SyzManagerTarget) newSyzManagerInstance(slotName, uniqName string, checkout *Checkout) (Instance, error) { + folder := filepath.Join(checkout.Path, fmt.Sprintf("run-%s", uniqName)) + common, err := SetupSyzkallerInstance(slotName, folder, checkout) + if err != nil { + return nil, err + } + if t.config.Corpus != "" { + log.Printf("[%s] Copying corpus", uniqName) + corpusPath := filepath.Join(common.Workdir, "corpus.db") + err = osutil.CopyFile(t.config.Corpus, corpusPath) + if err != nil { + return nil, fmt.Errorf("failed to copy corpus from %s: %s", t.config.Corpus, err) + } + } + return &SyzManagerInstance{ + InstanceCommon: InstanceCommon{ + Name: uniqName, + LogFile: filepath.Join(folder, "log.txt"), + ExecCommand: filepath.Join(checkout.Path, "bin", "syz-manager"), + ExecCommandArgs: []string{"-config", common.CfgFile, "-bench", common.BenchFile}, + stopChannel: make(chan bool, 1), + }, + SyzkallerInfo: *common, + RunTime: t.config.RunTime.Duration, }, nil } diff --git a/tools/syz-testbed/stats.go b/tools/syz-testbed/stats.go index 0db75c45c..77fed55b7 100644 --- a/tools/syz-testbed/stats.go +++ b/tools/syz-testbed/stats.go @@ -19,9 +19,10 @@ type BugInfo struct { Title string } +type RunResult interface{} + // The information collected from a syz-manager instance. -type RunResult struct { - Workdir string +type SyzManagerResult struct { Bugs []BugInfo StatRecords []StatRecord } @@ -32,7 +33,7 @@ type StatRecord map[string]uint64 // The grouping of single instance results. Taken by all stat generating routines. type RunResultGroup struct { Name string - Results []*RunResult + Results []RunResult } // Different "views" of the statistics, e.g. only completed instances or completed + running. @@ -105,7 +106,7 @@ type BugSummary struct { func summarizeBugs(groups []RunResultGroup) ([]*BugSummary, error) { bugsMap := make(map[string]*BugSummary) for _, group := range groups { - for _, result := range group.Results { + for _, result := range group.SyzManagerResults() { for _, bug := range result.Bugs { summary := bugsMap[bug.Title] if summary == nil { @@ -171,6 +172,17 @@ func (view StatView) GenerateBugCountsTable() (*Table, error) { return table, nil } +func (group RunResultGroup) SyzManagerResults() []*SyzManagerResult { + ret := []*SyzManagerResult{} + for _, rawRes := range group.Results { + res, ok := rawRes.(*SyzManagerResult) + if ok { + ret = append(ret, res) + } + } + return ret +} + func (group RunResultGroup) AvgStatRecords() []map[string]uint64 { ret := []map[string]uint64{} commonLen := group.minResultLength() @@ -188,8 +200,9 @@ func (group RunResultGroup) minResultLength() int { if len(group.Results) == 0 { return 0 } - ret := len(group.Results[0].StatRecords) - for _, result := range group.Results { + results := group.SyzManagerResults() + ret := len(results[0].StatRecords) + for _, result := range results { currLen := len(result.StatRecords) if currLen < ret { ret = currLen @@ -200,7 +213,7 @@ func (group RunResultGroup) minResultLength() int { func (group RunResultGroup) groupNthRecord(i int) map[string]*stats.Sample { records := []StatRecord{} - for _, result := range group.Results { + for _, result := range group.SyzManagerResults() { records = append(records, result.StatRecords[i]) } return groupSamples(records) @@ -264,7 +277,7 @@ func (view StatView) InstanceStatsTable() (*Table, error) { for i, result := range group.Results { newView.Groups = append(newView.Groups, RunResultGroup{ Name: fmt.Sprintf("%s-%d", group.Name, i), - Results: []*RunResult{result}, + Results: []RunResult{result}, }) } } diff --git a/tools/syz-testbed/targets.go b/tools/syz-testbed/targets.go new file mode 100644 index 000000000..c19dec2ca --- /dev/null +++ b/tools/syz-testbed/targets.go @@ -0,0 +1,83 @@ +// Copyright 2022 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "fmt" + "log" + "path/filepath" + "sync" + + "github.com/google/syzkaller/pkg/osutil" +) + +// TestbedTarget represents all behavioral differences between specific testbed targets. +type TestbedTarget interface { + NewJob(slotName string, checkouts []*Checkout) (*Checkout, Instance, error) + SaveStatView(view StatView, dir string) error + SupportsHTMLView(key string) bool +} + +type SyzManagerTarget struct { + config *TestbedConfig + nextCheckoutID int + nextInstanceID int + mu sync.Mutex +} + +var targetConstructors = map[string]func(cfg *TestbedConfig) TestbedTarget{ + "syz-manager": func(cfg *TestbedConfig) TestbedTarget { + return &SyzManagerTarget{ + config: cfg, + } + }, +} + +func (t *SyzManagerTarget) NewJob(slotName string, checkouts []*Checkout) (*Checkout, Instance, error) { + // Round-robin strategy should suffice. + t.mu.Lock() + checkout := checkouts[t.nextCheckoutID%len(checkouts)] + instanceID := t.nextInstanceID + t.nextCheckoutID++ + t.nextInstanceID++ + t.mu.Unlock() + uniqName := fmt.Sprintf("%s-%d", checkout.Name, instanceID) + instance, err := t.newSyzManagerInstance(slotName, uniqName, checkout) + if err != nil { + return nil, nil, err + } + return checkout, instance, nil +} + +func (t *SyzManagerTarget) SupportsHTMLView(key string) bool { + supported := map[string]bool{ + HTMLBugsTable: true, + HTMLBugCountsTable: true, + HTMLStatsTable: true, + } + return supported[key] +} + +func (t *SyzManagerTarget) SaveStatView(view StatView, dir string) error { + benchDir := filepath.Join(dir, "benches") + err := osutil.MkdirAll(benchDir) + if err != nil { + return fmt.Errorf("failed to create %s: %s", benchDir, err) + } + tableStats := map[string]func(view StatView) (*Table, error){ + "bugs.csv": (StatView).GenerateBugTable, + "checkout_stats.csv": (StatView).StatsTable, + "instance_stats.csv": (StatView).InstanceStatsTable, + } + for fileName, genFunc := range tableStats { + table, err := genFunc(view) + if err == nil { + table.SaveAsCsv(filepath.Join(dir, fileName)) + } else { + log.Printf("stat generation error: %s", err) + } + } + _, err = view.SaveAvgBenches(benchDir) + return err +} diff --git a/tools/syz-testbed/testbed.go b/tools/syz-testbed/testbed.go index f83d1dfa9..6f7a1868d 100644 --- a/tools/syz-testbed/testbed.go +++ b/tools/syz-testbed/testbed.go @@ -30,6 +30,7 @@ var ( type TestbedConfig struct { Name string `json:"name"` // name of the testbed + Target string `json:"target"` // what application to test MaxInstances int `json:"max_instances"` // max # of simultaneously running instances RunTime DurationConfig `json:"run_time"` // lifetime of an instance (default "24h") HTTP string `json:"http"` // on which port to set up a simple web dashboard @@ -54,16 +55,17 @@ type CheckoutConfig struct { type TestbedContext struct { Config *TestbedConfig Checkouts []*Checkout - NextRestart time.Time NextCheckoutID int NextInstanceID int - statMutex sync.Mutex + Target TestbedTarget + mu sync.Mutex } func main() { flag.Parse() cfg := &TestbedConfig{ Name: "testbed", + Target: "syz-manager", RunTime: DurationConfig{24 * time.Hour}, } err := config.LoadFile(*flagConfig, &cfg) @@ -77,6 +79,7 @@ func main() { } ctx := TestbedContext{ Config: cfg, + Target: targetConstructors[cfg.Target](cfg), } go ctx.setupHTTPServer() @@ -123,20 +126,15 @@ func (ctx *TestbedContext) GetStatViews() ([]StatView, error) { groupsCompleted := []RunResultGroup{} groupsAll := []RunResultGroup{} for _, checkout := range ctx.Checkouts { - running := []*RunResult{} - for _, instance := range checkout.Running { - result, err := instance.FetchResult() - if err == nil { - running = append(running, result) - } - } + running := checkout.GetRunningResults() + completed := checkout.GetCompletedResults() groupsCompleted = append(groupsCompleted, RunResultGroup{ Name: checkout.Name, - Results: checkout.Completed, + Results: completed, }) groupsAll = append(groupsAll, RunResultGroup{ Name: checkout.Name, - Results: append(checkout.Completed, running...), + Results: append(completed, running...), }) } return []StatView{ @@ -151,56 +149,35 @@ func (ctx *TestbedContext) GetStatViews() ([]StatView, error) { }, nil } -func (ctx *TestbedContext) saveStatView(view StatView) error { - dir := filepath.Join(ctx.Config.Workdir, "stats_"+view.Name) - benchDir := filepath.Join(dir, "benches") - err := osutil.MkdirAll(benchDir) - if err != nil { - return fmt.Errorf("failed to create %s: %s", benchDir, err) - } - tableStats := map[string]func(view StatView) (*Table, error){ - "bugs.csv": (StatView).GenerateBugTable, - "checkout_stats.csv": (StatView).StatsTable, - "instance_stats.csv": (StatView).InstanceStatsTable, - } - for fileName, genFunc := range tableStats { - table, err := genFunc(view) - if err == nil { - table.SaveAsCsv(filepath.Join(dir, fileName)) - } else { - log.Printf("stat generation error: %s", err) - } - } - _, err = view.SaveAvgBenches(benchDir) - return err -} - func (ctx *TestbedContext) TestbedStatsTable() *Table { - table := NewTable("Checkout", "Running", "Completed", "Until reset") + table := NewTable("Checkout", "Running", "Completed", "Last started") for _, checkout := range ctx.Checkouts { - until := "-" - if ctx.NextRestart.After(time.Now()) { - until = time.Until(ctx.NextRestart).Round(time.Second).String() + checkout.mu.Lock() + last := "" + if !checkout.LastRunning.IsZero() { + last = time.Since(checkout.LastRunning).Round(time.Second).String() } table.AddRow(checkout.Name, fmt.Sprintf("%d", len(checkout.Running)), fmt.Sprintf("%d", len(checkout.Completed)), - until, + last, ) + checkout.mu.Unlock() } return table } func (ctx *TestbedContext) SaveStats() error { // Preventing concurrent saving of the stats. - ctx.statMutex.Lock() - defer ctx.statMutex.Unlock() + ctx.mu.Lock() + defer ctx.mu.Unlock() views, err := ctx.GetStatViews() if err != nil { return err } for _, view := range views { - err := ctx.saveStatView(view) + dir := filepath.Join(ctx.Config.Workdir, "stats_"+view.Name) + err := ctx.Target.SaveStatView(view, dir) if err != nil { return err } @@ -209,84 +186,69 @@ func (ctx *TestbedContext) SaveStats() error { return table.SaveAsCsv(filepath.Join(ctx.Config.Workdir, "testbed.csv")) } -func (ctx *TestbedContext) generateInstances(count int) ([]*Instance, error) { +func (ctx *TestbedContext) Slot(slotID int, stop chan struct{}, ret chan error) { // It seems that even gracefully finished syz-managers can leak GCE instances. - // To allow for that strange behavior, let's reuse syz-manager names, so that - // they will in turn reuse the names of the leaked GCE instances. - instances := []*Instance{} - for idx := 1; idx <= count; idx++ { - checkout := ctx.Checkouts[ctx.NextCheckoutID] - ctx.NextCheckoutID = (ctx.NextCheckoutID + 1) % len(ctx.Checkouts) - instance, err := ctx.NewInstance(checkout, fmt.Sprintf("%s-%d", ctx.Config.Name, idx)) - if err != nil { - return nil, err - } - checkout.Running = append(checkout.Running, instance) - instances = append(instances, instance) - } - return instances, nil -} - -// Create instances, run them, stop them, archive them, and so on... -func (ctx *TestbedContext) Loop(stop chan struct{}) { - duration := ctx.Config.RunTime.Duration - mustStop := false - for !mustStop { - log.Printf("setting up instances") - instances, err := ctx.generateInstances(ctx.Config.MaxInstances) + // To allow for that strange behavior, let's reuse syz-manager names in each slot, + // so that its VMs will in turn reuse the names of the leaked ones. + slotName := fmt.Sprintf("%s-%d", ctx.Config.Name, slotID) + for { + checkout, instance, err := ctx.Target.NewJob(slotName, ctx.Checkouts) if err != nil { - tool.Failf("failed to set up intances: %s", err) - } - log.Printf("starting instances") - instanceStatuses := make(chan error, len(instances)) - var wg sync.WaitGroup - for _, inst := range instances { - wg.Add(1) - go func(instance *Instance) { - instanceStatuses <- instance.Run() - wg.Done() - }(inst) + ret <- fmt.Errorf("failed to create instance: %s", err) + return } + checkout.AddRunning(instance) + retChannel := make(chan error) + go func() { + retChannel <- instance.Run() + }() - ctx.NextRestart = time.Now().Add(duration) + var retErr error select { - case err := <-instanceStatuses: - // Syz-managers are not supposed to stop under normal circumstances. - // If one of them did stop, there must have been a very good reason to. - // For now, we just shut down the whole experiment in such a case. - log.Printf("an instance has failed (%s), stopping everything", err) - mustStop = true case <-stop: - log.Printf("stopping the experiment") - mustStop = true - case <-time.After(duration): - log.Printf("run period has finished") - } - - // Wait for all instances to finish. - for _, instance := range instances { instance.Stop() + <-retChannel + retErr = fmt.Errorf("instance was killed") + case retErr = <-retChannel: } - wg.Wait() - // Only mark instances completed if they've indeed been running the whole iteration. - if !mustStop { - for _, checkout := range ctx.Checkouts { - err = checkout.ArchiveRunning() - if err != nil { - tool.Failf("ArchiveRunning error: %s", err) - } - } + // For now, we only archive instances that finished normally (ret == nil). + // syz-testbed will anyway stop after such an error, so it's not a problem + // that they remain in Running. + if retErr != nil { + ret <- retErr + return } - - log.Printf("collecting statistics") - err = ctx.SaveStats() + err = checkout.ArchiveInstance(instance) if err != nil { - log.Printf("stats saving error: %s", err) + ret <- fmt.Errorf("a call to ArchiveInstance failed: %s", err) + return } } } +// Create instances, run them, stop them, archive them, and so on... +func (ctx *TestbedContext) Loop(stop chan struct{}) { + stopAll := make(chan struct{}) + errors := make(chan error) + for i := 0; i < ctx.Config.MaxInstances; i++ { + go ctx.Slot(i, stopAll, errors) + } + + exited := 0 + select { + case <-stop: + log.Printf("stopping the experiment") + case err := <-errors: + exited = 1 + log.Printf("an instance has failed (%s), stopping everything", err) + } + close(stopAll) + for ; exited < ctx.Config.MaxInstances; exited++ { + <-errors + } +} + func (d *DurationConfig) UnmarshalJSON(data []byte) error { var v interface{} if err := json.Unmarshal(data, &v); err != nil { @@ -302,6 +264,7 @@ func (d *DurationConfig) UnmarshalJSON(data []byte) error { } return err } + func (d *DurationConfig) MarshalJSON() ([]byte, error) { return json.Marshal(d.String()) } @@ -328,6 +291,9 @@ func checkConfig(cfg *TestbedConfig) error { if cfg.BenchCmp != "" && !osutil.IsExist(cfg.BenchCmp) { return fmt.Errorf("benchmp path is specified, but %s does not exist", cfg.BenchCmp) } + if _, ok := targetConstructors[cfg.Target]; !ok { + return fmt.Errorf("unknown target %s", cfg.Target) + } cfg.Corpus = osutil.Abs(cfg.Corpus) names := make(map[string]bool) for idx := range cfg.Checkouts { |
