From 71b262fd686b8988d58ccd007909eb8ee5c6c7dc Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Sun, 13 Oct 2024 13:47:36 +0100 Subject: [PATCH] feat: match before checking cache This changes the flow of processing to make unmatched behaviour more consistent. Before, we had been: - traversing the filesystem - comparing with the cache and only emitting files which had changed - applying the matching rules to determine which formatters should be applied to a given file - applying the formatters Now, we do the following: - traverse the filesystem - apply the matching rules to determine which formatters should be applied to a given file - compare with the cache and only emit files which have changed for formatting - apply the formatters It does mean we are applying the matching rules against files which we may not have to format, but in testing against Nixpkgs the performance impact appears negligible. This makes sense since most of the processing time will be spent in the formatters, not applying some globs to file paths. Signed-off-by: Brian McGee --- cmd/format/format.go | 99 ++++++---- cmd/root_test.go | 395 ++++++++++++++++++++++++++++++++++------ stats/stats.go | 7 +- stats/stats_type.go | 98 ++++++++++ walk/filesystem_test.go | 2 +- walk/git_test.go | 2 +- 6 files changed, 503 insertions(+), 100 deletions(-) create mode 100644 stats/stats_type.go diff --git a/cmd/format/format.go b/cmd/format/format.go index 2c3c77fd..8a69b98e 100644 --- a/cmd/format/format.go +++ b/cmd/format/format.go @@ -205,33 +205,34 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string) return fmt.Errorf("failed to create walker: %w", err) } + // start traversing files := make([]*walk.File, BatchSize) + for { + // read the next batch ctx, cancel := context.WithTimeout(ctx, 1*time.Second) - n, err := reader.Read(ctx, files) - for idx := 0; idx < n; idx++ { - file := files[idx] + // ensure context is cancelled to release resources + cancel() - // check if this file is new or has changed when compared to the cache entry - if file.Cache == nil || file.Cache.HasChanged(file.Info) { - filesCh <- file - statz.Add(stats.Emitted, 1) - } + // pass each file into the file channel for processing + for idx := 0; idx < n; idx++ { + filesCh <- files[idx] } - cancel() - if errors.Is(err, io.EOF) { + // we have finished traversing break } else if err != nil { + // something went wrong log.Errorf("failed to read files: %v", err) cancel() break } } + // indicate no further files for processing close(filesCh) // wait for everything to complete @@ -263,6 +264,8 @@ func applyFormatters( // formatters which should be applied to their respective files batches := make(map[string][]*format.Task) + // apply check if the given batch key has enough tasks to trigger processing + // flush is used to force processing regardless of the number of tasks apply := func(key string, flush bool) { // lookup the batch and exit early if it's empty batch := batches[key] @@ -304,6 +307,7 @@ func applyFormatters( } } + // tryApply batches tasks by their batch key and processes the batch if there is enough ready tryApply := func(task *format.Task) { // append to batch key := task.BatchKey @@ -314,53 +318,68 @@ func applyFormatters( return func() error { defer func() { - // close processed channel + // indicate processing has finished close(formattedCh) }() + // parse unmatched log level unmatchedLevel, err := log.ParseLevel(cfg.OnUnmatched) if err != nil { return fmt.Errorf("invalid on-unmatched value: %w", err) } - // iterate the files channel + // iterate the file channel for file := range filesCh { + // a list of formatters that match this file + var matches []*format.Formatter + // first check if this file has been globally excluded if format.PathMatches(file.RelPath, globalExcludes) { log.Debugf("path matched global excludes: %s", file.RelPath) - // mark it as processed and continue to the next - formattedCh <- &format.Task{ - File: file, + } else { + // otherwise, check if any formatters are interested in it + for _, formatter := range formatters { + if formatter.Wants(file) { + matches = append(matches, formatter) + } } - continue } - // check if any formatters are interested in this file - var matches []*format.Formatter - for _, formatter := range formatters { - if formatter.Wants(file) { - matches = append(matches, formatter) - } - } + // indicates no further processing + var release bool - // see if any formatters matched + // check if there were no matches if len(matches) == 0 { - + // log that there was no match, exiting with an error if the unmatched level was set to fatal if unmatchedLevel == log.FatalLevel { return fmt.Errorf("no formatter for path: %s", file.RelPath) } + log.Logf(unmatchedLevel, "no formatter for path: %s", file.RelPath) - // mark it as processed and continue to the next - formattedCh <- &format.Task{ - File: file, - } + + // no further processing + release = true } else { - // record the match + // record there was a match statz.Add(stats.Matched, 1) - // create a new format task, add it to a batch based on its batch key and try to apply if the batch is full - task := format.NewTask(file, matches) - tryApply(&task) + + // check if the file is new or has changed when compared to the cache entry + if file.Cache == nil || file.Cache.HasChanged(file.Info) { + // if so, generate a format task, add it to the relevant batch (by batch key) and try to process + task := format.NewTask(file, matches) + tryApply(&task) + } else { + // indicate no further processing + release = true + } + } + + if release { + // release the file as there is no more processing to be done on it + if err := file.Release(); err != nil { + return fmt.Errorf("failed to release file: %w", err) + } } } @@ -398,16 +417,20 @@ func postProcessing( break LOOP } - // check if the file has changed + // grab the underlying file reference file := task.File + + // check if the file has changed changed, newInfo, err := file.Stat() if err != nil { return err } + statz.Add(stats.Formatted, 1) + if changed { - // record the change - statz.Add(stats.Formatted, 1) + // record that a change in the underlying file occurred + statz.Add(stats.Changed, 1) logMethod := log.Debug if cfg.FailOnChange { @@ -434,8 +457,8 @@ func postProcessing( } } - // if fail on change has been enabled, check that no files were actually formatted, throwing an error if so - if cfg.FailOnChange && statz.Value(stats.Formatted) != 0 { + // if fail on change has been enabled, check that no files were actually changed, throwing an error if so + if cfg.FailOnChange && statz.Value(stats.Changed) != 0 { return ErrFailOnChange } diff --git a/cmd/root_test.go b/cmd/root_test.go index 6365f807..b037f4d7 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -72,24 +72,24 @@ func TestOnUnmatched(t *testing.T) { var out []byte // default is warn - out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c") + out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter") as.NoError(err) checkOutput("WARN", out) - out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "--on-unmatched", "warn") + out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "warn") as.NoError(err) checkOutput("WARN", out) - out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-u", "error") + out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-u", "error") as.NoError(err) checkOutput("ERRO", out) - out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-v", "--on-unmatched", "info") + out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-v", "--on-unmatched", "info") as.NoError(err) checkOutput("INFO", out) t.Setenv("TREEFMT_ON_UNMATCHED", "debug") - out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv") + out, _, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "-vv") as.NoError(err) checkOutput("DEBU", out) } @@ -182,25 +182,53 @@ func TestSpecifyingFormatters(t *testing.T) { setup() _, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 3, 3) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 3, + stats.Formatted: 3, + stats.Changed: 3, + }) setup() + _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix") as.NoError(err) - assertStats(t, as, statz, 32, 32, 2, 2) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 2, + stats.Formatted: 2, + stats.Changed: 2, + }) setup() + _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "-f", "ruby,nix") as.NoError(err) - assertStats(t, as, statz, 32, 32, 2, 2) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 2, + stats.Formatted: 2, + stats.Changed: 2, + }) setup() + _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix") as.NoError(err) - assertStats(t, as, statz, 32, 32, 1, 1) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 1, + stats.Formatted: 1, + stats.Changed: 1, + }) // test bad names setup() + _, _, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo") as.Errorf(err, "formatter not found in config: foo") @@ -228,7 +256,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) // globally exclude nix files cfg.Excludes = []string{"*.nix"} @@ -236,7 +270,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 31, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 31, + stats.Formatted: 31, + stats.Changed: 0, + }) // add haskell files to the global exclude cfg.Excludes = []string{"*.nix", "*.hs"} @@ -244,7 +284,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 25, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 25, + stats.Formatted: 25, + stats.Changed: 0, + }) echo := cfg.FormatterConfigs["echo"] @@ -254,7 +300,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 23, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 23, + stats.Formatted: 23, + stats.Changed: 0, + }) // remove go files from the echo formatter via env t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "*.py,*.go") @@ -262,7 +314,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 22, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 22, + stats.Formatted: 22, + stats.Changed: 0, + }) t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "") // reset @@ -272,7 +330,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 1, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 1, + stats.Formatted: 1, + stats.Changed: 0, + }) // add js files to echo formatter via env t.Setenv("TREEFMT_FORMATTER_ECHO_INCLUDES", "*.elm,*.js") @@ -280,7 +344,13 @@ func TestIncludesAndExcludes(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err = treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 2, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 2, + stats.Formatted: 2, + stats.Changed: 0, + }) } func TestPrjRootEnvVariable(t *testing.T) { @@ -303,7 +373,13 @@ func TestPrjRootEnvVariable(t *testing.T) { t.Setenv("PRJ_ROOT", tempDir) _, statz, err := treefmt(t, "--config-file", configPath) as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) } func TestCache(t *testing.T) { @@ -327,34 +403,76 @@ func TestCache(t *testing.T) { test.WriteConfig(t, configPath, cfg) _, statz, err := treefmt(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) _, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 0, + stats.Changed: 0, + }) // clear cache _, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "-c") as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) _, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 0, + stats.Changed: 0, + }) // clear cache _, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "-c") as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) _, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 0, + stats.Changed: 0, + }) // no cache _, statz, err = treefmt(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) } func TestChangeWorkingDirectory(t *testing.T) { @@ -388,13 +506,25 @@ func TestChangeWorkingDirectory(t *testing.T) { // this should fail if the working directory hasn't been changed first _, statz, err := treefmt(t, "-C", tempDir) as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) // use env t.Setenv("TREEFMT_WORKING_DIR", tempDir) _, statz, err = treefmt(t, "-c") as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) } func TestFailOnChange(t *testing.T) { @@ -467,31 +597,61 @@ func TestBustCacheOnFormatterChange(t *testing.T) { args := []string{"--config-file", configPath, "--tree-root", tempDir} _, statz, err := treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 32, 3, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 3, + stats.Formatted: 3, + stats.Changed: 0, + }) // tweak mod time of elm formatter as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format")) _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 32, 3, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 3, + stats.Formatted: 3, + stats.Changed: 0, + }) // check cache is working _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 3, + stats.Formatted: 0, + stats.Changed: 0, + }) // tweak mod time of python formatter as.NoError(test.RecreateSymlink(t, binPath+"/"+"black")) _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 32, 3, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 3, + stats.Formatted: 3, + stats.Changed: 0, + }) // check cache is working _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 3, + stats.Formatted: 0, + stats.Changed: 0, + }) // add go formatter cfg.FormatterConfigs["go"] = &config.Formatter{ @@ -503,12 +663,24 @@ func TestBustCacheOnFormatterChange(t *testing.T) { _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 32, 4, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 4, + stats.Formatted: 4, + stats.Changed: 0, + }) // check cache is working _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 4, + stats.Formatted: 0, + stats.Changed: 0, + }) // remove python formatter delete(cfg.FormatterConfigs, "python") @@ -516,12 +688,24 @@ func TestBustCacheOnFormatterChange(t *testing.T) { _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 32, 2, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 2, + stats.Formatted: 2, + stats.Changed: 0, + }) // check cache is working _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 2, + stats.Formatted: 0, + stats.Changed: 0, + }) // remove elm formatter delete(cfg.FormatterConfigs, "elm") @@ -529,12 +713,24 @@ func TestBustCacheOnFormatterChange(t *testing.T) { _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 32, 1, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 1, + stats.Formatted: 1, + stats.Changed: 0, + }) // check cache is working _, statz, err = treefmt(t, args...) as.NoError(err) - assertStats(t, as, statz, 32, 0, 0, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 1, + stats.Formatted: 0, + stats.Changed: 0, + }) } func TestGitWorktree(t *testing.T) { @@ -569,10 +765,16 @@ func TestGitWorktree(t *testing.T) { wt, err := repo.Worktree() as.NoError(err, "failed to get git worktree") - run := func(traversed int32, emitted int32, matched int32, formatted int32) { + run := func(traversed int32, matched int32, formatted int32, changed int32) { _, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, statz, traversed, emitted, matched, formatted) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: traversed, + stats.Matched: matched, + stats.Formatted: formatted, + stats.Changed: changed, + }) } // run before adding anything to the worktree @@ -592,9 +794,15 @@ func TestGitWorktree(t *testing.T) { run(28, 28, 28, 0) // walk with filesystem instead of git + // we should traverse more files since we will look in the .git folder _, statz, err := treefmt(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem") as.NoError(err) - assertStats(t, as, statz, 60, 60, 60, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 60, + stats.Matched: 60, + stats.Changed: 0, + }) // capture current cwd, so we can replace it after the test is finished cwd, err := os.Getwd() @@ -608,15 +816,30 @@ func TestGitWorktree(t *testing.T) { // format specific sub paths _, statz, err = treefmt(t, "-C", tempDir, "-c", "go", "-vv") as.NoError(err) - assertStats(t, as, statz, 2, 2, 2, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 2, + stats.Matched: 2, + stats.Changed: 0, + }) _, statz, err = treefmt(t, "-C", tempDir, "-c", "go", "haskell") as.NoError(err) - assertStats(t, as, statz, 9, 9, 9, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 9, + stats.Matched: 9, + stats.Changed: 0, + }) _, statz, err = treefmt(t, "-C", tempDir, "-c", "go", "haskell", "ruby") as.NoError(err) - assertStats(t, as, statz, 10, 10, 10, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 10, + stats.Matched: 10, + stats.Changed: 0, + }) // try with a bad path _, _, err = treefmt(t, "-C", tempDir, "-c", "haskell", "foo") @@ -628,11 +851,21 @@ func TestGitWorktree(t *testing.T) { _, statz, err = treefmt(t, "-C", tempDir, "-c", "haskell", "foo.txt") as.NoError(err) - assertStats(t, as, statz, 8, 8, 8, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 8, + stats.Matched: 8, + stats.Changed: 0, + }) _, statz, err = treefmt(t, "-C", tempDir, "-c", "foo.txt") as.NoError(err) - assertStats(t, as, statz, 1, 1, 1, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 1, + stats.Matched: 1, + stats.Changed: 0, + }) } func TestPathsArg(t *testing.T) { @@ -677,19 +910,38 @@ func TestPathsArg(t *testing.T) { // without any path args _, statz, err := treefmt(t) as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) // specify some explicit paths _, statz, err = treefmt(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") as.NoError(err) - assertStats(t, as, statz, 2, 2, 2, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 2, + stats.Matched: 2, + stats.Formatted: 2, + stats.Changed: 0, + }) // specify an absolute path absoluteInternalPath, err := filepath.Abs("elm/elm.json") as.NoError(err) + _, statz, err = treefmt(t, "-c", absoluteInternalPath) as.NoError(err) - assertStats(t, as, statz, 1, 1, 1, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 1, + stats.Matched: 1, + stats.Formatted: 1, + stats.Changed: 0, + }) // specify a bad path _, _, err = treefmt(t, "-c", "elm/elm.json", "haskell/Nested/Bar.hs") @@ -742,7 +994,13 @@ func TestStdin(t *testing.T) { out, statz, err := treefmt(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.nix") as.NoError(err) - assertStats(t, as, statz, 1, 1, 1, 1) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 1, + stats.Matched: 1, + stats.Formatted: 1, + stats.Changed: 1, + }) // the nix formatters should have reduced the example to the following as.Equal(`{ ...}: "hello" @@ -767,7 +1025,13 @@ func TestStdin(t *testing.T) { out, statz, err = treefmt(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.md") as.NoError(err) - assertStats(t, as, statz, 1, 1, 1, 1) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 1, + stats.Matched: 1, + stats.Formatted: 1, + stats.Changed: 1, + }) as.Equal(`| col1 | col2 | | ------ | --------- | @@ -881,7 +1145,13 @@ func TestRunInSubdir(t *testing.T) { // without any path args, should reformat the whole tree _, statz, err := treefmt(t) as.NoError(err) - assertStats(t, as, statz, 32, 32, 32, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 32, + stats.Matched: 32, + stats.Formatted: 32, + stats.Changed: 0, + }) // specify some explicit paths, relative to the tree root // this should not work, as we're in a subdirectory @@ -891,7 +1161,13 @@ func TestRunInSubdir(t *testing.T) { // specify some explicit paths, relative to the current directory _, statz, err = treefmt(t, "-c", "elm.json", "../haskell/Nested/Foo.hs") as.NoError(err) - assertStats(t, as, statz, 2, 2, 2, 0) + + assertStats(t, as, statz, map[stats.Type]int32{ + stats.Traversed: 2, + stats.Matched: 2, + stats.Formatted: 2, + stats.Changed: 0, + }) } func treefmt(t *testing.T, args ...string) ([]byte, *stats.Stats, error) { @@ -945,10 +1221,15 @@ func treefmt(t *testing.T, args ...string) ([]byte, *stats.Stats, error) { return out, statz, nil } -func assertStats(t *testing.T, as *require.Assertions, statz *stats.Stats, traversed int32, emitted int32, matched int32, formatted int32) { +func assertStats( + t *testing.T, + as *require.Assertions, + statz *stats.Stats, + expected map[stats.Type]int32, +) { t.Helper() - as.Equal(traversed, statz.Value(stats.Traversed), "stats.traversed") - as.Equal(emitted, statz.Value(stats.Emitted), "stats.emitted") - as.Equal(matched, statz.Value(stats.Matched), "stats.matched") - as.Equal(formatted, statz.Value(stats.Formatted), "stats.formatted") + + for k, v := range expected { + as.Equal(v, statz.Value(k), k.String()) + } } diff --git a/stats/stats.go b/stats/stats.go index 8c5e960b..7c38d0bd 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -7,13 +7,14 @@ import ( "time" ) +//go:generate enumer -type=Type -text -transform=snake -output=./stats_type.go type Type int const ( Traversed Type = iota - Emitted Matched Formatted + Changed ) type Stats struct { @@ -44,9 +45,9 @@ func (s *Stats) Print() { fmt.Printf( strings.Join(components, "\n"), s.Value(Traversed), - s.Value(Emitted), s.Value(Matched), s.Value(Formatted), + s.Value(Changed), s.Elapsed().Round(time.Millisecond), ) } @@ -55,9 +56,9 @@ func New() Stats { // init counters counters := make(map[Type]*atomic.Int32) counters[Traversed] = &atomic.Int32{} - counters[Emitted] = &atomic.Int32{} counters[Matched] = &atomic.Int32{} counters[Formatted] = &atomic.Int32{} + counters[Changed] = &atomic.Int32{} return Stats{ start: time.Now(), diff --git a/stats/stats_type.go b/stats/stats_type.go new file mode 100644 index 00000000..e865fdf1 --- /dev/null +++ b/stats/stats_type.go @@ -0,0 +1,98 @@ +// Code generated by "enumer -type=Type -text -transform=snake -output=./stats_type.go"; DO NOT EDIT. + +package stats + +import ( + "fmt" + "strings" +) + +const _TypeName = "traversedmatchedformattedchanged" + +var _TypeIndex = [...]uint8{0, 9, 16, 25, 32} + +const _TypeLowerName = "traversedmatchedformattedchanged" + +func (i Type) String() string { + if i < 0 || i >= Type(len(_TypeIndex)-1) { + return fmt.Sprintf("Type(%d)", i) + } + return _TypeName[_TypeIndex[i]:_TypeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _TypeNoOp() { + var x [1]struct{} + _ = x[Traversed-(0)] + _ = x[Matched-(1)] + _ = x[Formatted-(2)] + _ = x[Changed-(3)] +} + +var _TypeValues = []Type{Traversed, Matched, Formatted, Changed} + +var _TypeNameToValueMap = map[string]Type{ + _TypeName[0:9]: Traversed, + _TypeLowerName[0:9]: Traversed, + _TypeName[9:16]: Matched, + _TypeLowerName[9:16]: Matched, + _TypeName[16:25]: Formatted, + _TypeLowerName[16:25]: Formatted, + _TypeName[25:32]: Changed, + _TypeLowerName[25:32]: Changed, +} + +var _TypeNames = []string{ + _TypeName[0:9], + _TypeName[9:16], + _TypeName[16:25], + _TypeName[25:32], +} + +// TypeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func TypeString(s string) (Type, error) { + if val, ok := _TypeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _TypeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + return 0, fmt.Errorf("%s does not belong to Type values", s) +} + +// TypeValues returns all values of the enum +func TypeValues() []Type { + return _TypeValues +} + +// TypeStrings returns a slice of all String values of the enum +func TypeStrings() []string { + strs := make([]string, len(_TypeNames)) + copy(strs, _TypeNames) + return strs +} + +// IsAType returns "true" if the value is listed in the enum definition. "false" otherwise +func (i Type) IsAType() bool { + for _, v := range _TypeValues { + if i == v { + return true + } + } + return false +} + +// MarshalText implements the encoding.TextMarshaler interface for Type +func (i Type) MarshalText() ([]byte, error) { + return []byte(i.String()), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface for Type +func (i *Type) UnmarshalText(text []byte) error { + var err error + *i, err = TypeString(string(text)) + return err +} diff --git a/walk/filesystem_test.go b/walk/filesystem_test.go index 8b1a2e86..60870fb4 100644 --- a/walk/filesystem_test.go +++ b/walk/filesystem_test.go @@ -81,7 +81,7 @@ func TestFilesystemReader(t *testing.T) { as.Equal(32, count) as.Equal(int32(32), statz.Value(stats.Traversed)) - as.Equal(int32(0), statz.Value(stats.Emitted)) as.Equal(int32(0), statz.Value(stats.Matched)) as.Equal(int32(0), statz.Value(stats.Formatted)) + as.Equal(int32(0), statz.Value(stats.Changed)) } diff --git a/walk/git_test.go b/walk/git_test.go index d7ed418f..2b2abbc0 100644 --- a/walk/git_test.go +++ b/walk/git_test.go @@ -62,7 +62,7 @@ func TestGitReader(t *testing.T) { as.Equal(32, count) as.Equal(int32(32), statz.Value(stats.Traversed)) - as.Equal(int32(0), statz.Value(stats.Emitted)) as.Equal(int32(0), statz.Value(stats.Matched)) as.Equal(int32(0), statz.Value(stats.Formatted)) + as.Equal(int32(0), statz.Value(stats.Changed)) }