From 48d4b4b8139e825f4b92907c4e2ae33c3f3343fa Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Wed, 9 Oct 2024 15:34:21 +0100 Subject: [PATCH] feat: remove globals from stats package Signed-off-by: Brian McGee --- cache/cache.go | 6 +- cmd/format/format.go | 44 +++++--- cmd/root.go | 15 ++- cmd/root_test.go | 244 ++++++++++++++++++++++--------------------- main.go | 3 +- stats/stats.go | 54 +++++----- 6 files changed, 193 insertions(+), 173 deletions(-) diff --git a/cache/cache.go b/cache/cache.go index 7effc933..f0ed5871 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -188,7 +188,7 @@ func putEntry(bucket *bolt.Bucket, path string, entry *Entry) error { // ChangeSet is used to walk a filesystem, starting at root, and outputting any new or changed paths using pathsCh. // It determines if a path is new or has changed by comparing against cache entries. -func ChangeSet(ctx context.Context, walker walk.Walker, filesCh chan<- *walk.File) error { +func ChangeSet(ctx context.Context, statz *stats.Stats, walker walk.Walker, filesCh chan<- *walk.File) error { start := time.Now() defer func() { @@ -236,13 +236,13 @@ func ChangeSet(ctx context.Context, walker walk.Walker, filesCh chan<- *walk.Fil changedOrNew := cached == nil || !(cached.Modified == file.Info.ModTime() && cached.Size == file.Info.Size()) - stats.Add(stats.Traversed, 1) + statz.Add(stats.Traversed, 1) if !changedOrNew { // no change return nil } - stats.Add(stats.Emitted, 1) + statz.Add(stats.Emitted, 1) // pass on the path select { diff --git a/cmd/format/format.go b/cmd/format/format.go index 9046d297..3b7010a9 100644 --- a/cmd/format/format.go +++ b/cmd/format/format.go @@ -33,7 +33,7 @@ const ( var ErrFailOnChange = errors.New("unexpected changes detected, --fail-on-change is enabled") -func Run(v *viper.Viper, cmd *cobra.Command, paths []string) error { +func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string) error { cmd.SilenceUsage = true cfg, err := config.FromViper(v) @@ -41,9 +41,6 @@ func Run(v *viper.Viper, cmd *cobra.Command, paths []string) error { return fmt.Errorf("failed to load config: %w", err) } - // initialise stats collection - stats.Init() - if cfg.CI { log.Info("ci mode enabled") @@ -191,10 +188,10 @@ func Run(v *viper.Viper, cmd *cobra.Command, paths []string) error { processedCh := make(chan *format.Task, cap(filesCh)) // start concurrent processing tasks in reverse order - eg.Go(updateCache(ctx, cfg, processedCh)) - eg.Go(detectFormatted(ctx, cfg, formattedCh, processedCh)) - eg.Go(applyFormatters(ctx, cfg, globalExcludes, formatters, filesCh, formattedCh)) - eg.Go(walkFilesystem(ctx, cfg, paths, filesCh)) + eg.Go(updateCache(ctx, cfg, statz, processedCh)) + eg.Go(detectFormatted(ctx, cfg, statz, formattedCh, processedCh)) + eg.Go(applyFormatters(ctx, cfg, statz, globalExcludes, formatters, filesCh, formattedCh)) + eg.Go(walkFilesystem(ctx, cfg, statz, paths, filesCh)) // wait for everything to complete return eg.Wait() @@ -203,6 +200,7 @@ func Run(v *viper.Viper, cmd *cobra.Command, paths []string) error { func walkFilesystem( ctx context.Context, cfg *config.Config, + statz *stats.Stats, paths []string, filesCh chan *walk.File, ) func() error { @@ -258,8 +256,8 @@ func walkFilesystem( case <-ctx.Done(): return ctx.Err() default: - stats.Add(stats.Traversed, 1) - stats.Add(stats.Emitted, 1) + statz.Add(stats.Traversed, 1) + statz.Add(stats.Emitted, 1) filesCh <- file return nil } @@ -268,7 +266,7 @@ func walkFilesystem( // otherwise we pass the walker to the cache and have it generate files for processing based on whether or not // they have been added/changed since the last invocation - if err = cache.ChangeSet(ctx, walker, filesCh); err != nil { + if err = cache.ChangeSet(ctx, statz, walker, filesCh); err != nil { return fmt.Errorf("failed to generate change set: %w", err) } return nil @@ -279,6 +277,7 @@ func walkFilesystem( func applyFormatters( ctx context.Context, cfg *config.Config, + statz *stats.Stats, globalExcludes []glob.Glob, formatters map[string]*format.Formatter, filesCh chan *walk.File, @@ -389,7 +388,7 @@ func applyFormatters( } } else { // record the match - stats.Add(stats.Matched, 1) + 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) @@ -409,7 +408,13 @@ func applyFormatters( } } -func detectFormatted(ctx context.Context, cfg *config.Config, formattedCh chan *format.Task, processedCh chan *format.Task) func() error { +func detectFormatted( + ctx context.Context, + cfg *config.Config, + statz *stats.Stats, + formattedCh chan *format.Task, + processedCh chan *format.Task, +) func() error { return func() error { defer func() { // close formatted channel @@ -438,7 +443,7 @@ func detectFormatted(ctx context.Context, cfg *config.Config, formattedCh chan * if changed { // record the change - stats.Add(stats.Formatted, 1) + statz.Add(stats.Formatted, 1) logMethod := log.Debug if cfg.FailOnChange { @@ -466,7 +471,12 @@ func detectFormatted(ctx context.Context, cfg *config.Config, formattedCh chan * } } -func updateCache(ctx context.Context, cfg *config.Config, processedCh chan *format.Task) func() error { +func updateCache( + ctx context.Context, + cfg *config.Config, + statz *stats.Stats, + processedCh chan *format.Task, +) func() error { return func() error { // used to batch updates for more efficient txs batch := make([]*format.Task, 0, BatchSize) @@ -544,13 +554,13 @@ func updateCache(ctx context.Context, cfg *config.Config, processedCh chan *form } // if fail on change has been enabled, check that no files were actually formatted, throwing an error if so - if cfg.FailOnChange && stats.Value(stats.Formatted) != 0 { + if cfg.FailOnChange && statz.Value(stats.Formatted) != 0 { return ErrFailOnChange } // print stats to stdout unless we are processing stdin and printing the results to stdout if !cfg.Stdin { - stats.Print() + statz.Print() } return nil diff --git a/cmd/root.go b/cmd/root.go index 58064eb8..89ae12eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" + "github.com/numtide/treefmt/stats" + "github.com/charmbracelet/log" "github.com/numtide/treefmt/build" "github.com/numtide/treefmt/cmd/format" @@ -14,7 +16,7 @@ import ( "github.com/spf13/viper" ) -func NewRoot() *cobra.Command { +func NewRoot() (*cobra.Command, *stats.Stats) { var ( treefmtInit bool configFile string @@ -26,13 +28,16 @@ func NewRoot() *cobra.Command { cobra.CheckErr(fmt.Errorf("failed to create viper instance: %w", err)) } + // create a new stats instance + statz := stats.New() + // create out root command cmd := &cobra.Command{ Use: "treefmt ", Short: "One CLI to format your repo", Version: build.Version, RunE: func(cmd *cobra.Command, args []string) error { - return runE(v, cmd, args) + return runE(v, &statz, cmd, args) }, } @@ -62,10 +67,10 @@ func NewRoot() *cobra.Command { // conforms with https://github.com/numtide/prj-spec/blob/main/PRJ_SPEC.md cobra.CheckErr(v.BindPFlag("prj_root", fs.Lookup("tree-root"))) - return cmd + return cmd, &statz } -func runE(v *viper.Viper, cmd *cobra.Command, args []string) error { +func runE(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, args []string) error { flags := cmd.Flags() // change working directory if required @@ -123,5 +128,5 @@ func runE(v *viper.Viper, cmd *cobra.Command, args []string) error { } // format - return format.Run(v, cmd, args) + return format.Run(v, statz, cmd, args) } diff --git a/cmd/root_test.go b/cmd/root_test.go index 0da24793..050450d3 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -58,7 +58,7 @@ func TestOnUnmatched(t *testing.T) { // - "haskell/treefmt.toml" } - _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "fatal") + _, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--on-unmatched", "fatal") as.ErrorContains(err, fmt.Sprintf("no formatter for path: %s", paths[0])) checkOutput := func(level string, output []byte) { @@ -70,24 +70,24 @@ func TestOnUnmatched(t *testing.T) { var out []byte // default is warn - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c") + out, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c") as.NoError(err) checkOutput("WARN", out) - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "--on-unmatched", "warn") + out, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "--on-unmatched", "warn") as.NoError(err) checkOutput("WARN", out) - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-u", "error") + out, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-u", "error") as.NoError(err) checkOutput("ERRO", out) - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-v", "--on-unmatched", "info") + out, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-v", "--on-unmatched", "info") as.NoError(err) checkOutput("INFO", out) t.Setenv("TREEFMT_ON_UNMATCHED", "debug") - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv") + out, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "-c", "-vv") as.NoError(err) checkOutput("DEBU", out) } @@ -105,14 +105,14 @@ func TestCpuProfile(t *testing.T) { as.NoError(os.Chdir(cwd)) }) - _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--cpu-profile", "cpu.pprof") + _, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--cpu-profile", "cpu.pprof") as.NoError(err) as.FileExists(filepath.Join(tempDir, "cpu.pprof")) _, err = os.Stat(filepath.Join(tempDir, "cpu.pprof")) as.NoError(err) t.Setenv("TREEFMT_CPU_PROFILE", "env.pprof") - _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter") + _, _, err = cmd(t, "-C", tempDir, "--allow-missing-formatter") as.NoError(err) as.FileExists(filepath.Join(tempDir, "env.pprof")) _, err = os.Stat(filepath.Join(tempDir, "env.pprof")) @@ -133,14 +133,14 @@ func TestAllowMissingFormatter(t *testing.T) { }, }) - _, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-vv") + _, _, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-vv") as.ErrorIs(err, format.ErrCommandNotFound) - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter") + _, _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--allow-missing-formatter") as.NoError(err) t.Setenv("TREEFMT_ALLOW_MISSING_FORMATTER", "true") - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + _, _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) } @@ -178,32 +178,32 @@ func TestSpecifyingFormatters(t *testing.T) { } setup() - _, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 3, 3) + assertStats(t, as, statz, 32, 32, 3, 3) setup() - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix") + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "elm,nix") as.NoError(err) - assertStats(t, as, 32, 32, 2, 2) + assertStats(t, as, statz, 32, 32, 2, 2) setup() - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "-f", "ruby,nix") + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "-f", "ruby,nix") as.NoError(err) - assertStats(t, as, 32, 32, 2, 2) + assertStats(t, as, statz, 32, 32, 2, 2) setup() - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix") + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "nix") as.NoError(err) - assertStats(t, as, 32, 32, 1, 1) + assertStats(t, as, statz, 32, 32, 1, 1) // test bad names setup() - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo") + _, _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--formatters", "foo") as.Errorf(err, "formatter not found in config: foo") t.Setenv("TREEFMT_FORMATTERS", "bar,foo") - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.Errorf(err, "formatter not found in config: bar") } @@ -224,25 +224,25 @@ func TestIncludesAndExcludes(t *testing.T) { } test.WriteConfig(t, configPath, cfg) - _, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) // globally exclude nix files cfg.Excludes = []string{"*.nix"} test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 31, 0) + assertStats(t, as, statz, 32, 32, 31, 0) // add haskell files to the global exclude cfg.Excludes = []string{"*.nix", "*.hs"} test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 25, 0) + assertStats(t, as, statz, 32, 32, 25, 0) echo := cfg.FormatterConfigs["echo"] @@ -250,17 +250,17 @@ func TestIncludesAndExcludes(t *testing.T) { echo.Excludes = []string{"*.py"} test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 23, 0) + assertStats(t, as, statz, 32, 32, 23, 0) // remove go files from the echo formatter via env t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "*.py,*.go") test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 22, 0) + assertStats(t, as, statz, 32, 32, 22, 0) t.Setenv("TREEFMT_FORMATTER_ECHO_EXCLUDES", "") // reset @@ -268,17 +268,17 @@ func TestIncludesAndExcludes(t *testing.T) { echo.Includes = []string{"*.elm"} test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 1, 0) + assertStats(t, as, statz, 32, 32, 1, 0) // add js files to echo formatter via env t.Setenv("TREEFMT_FORMATTER_ECHO_INCLUDES", "*.elm,*.js") test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 2, 0) + assertStats(t, as, statz, 32, 32, 2, 0) } func TestPrjRootEnvVariable(t *testing.T) { @@ -299,9 +299,9 @@ func TestPrjRootEnvVariable(t *testing.T) { test.WriteConfig(t, configPath, cfg) t.Setenv("PRJ_ROOT", tempDir) - _, err := cmd(t, "--config-file", configPath) + _, statz, err := cmd(t, "--config-file", configPath) as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) } func TestCache(t *testing.T) { @@ -323,36 +323,36 @@ func TestCache(t *testing.T) { var err error test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + _, statz, err := cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // clear cache - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") + _, statz, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // clear cache - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") + _, statz, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "-c") as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) + _, statz, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // no cache - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") + _, statz, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) } func TestChangeWorkingDirectory(t *testing.T) { @@ -384,15 +384,15 @@ func TestChangeWorkingDirectory(t *testing.T) { // by default, we look for ./treefmt.toml and use the cwd for the tree root // this should fail if the working directory hasn't been changed first - _, err = cmd(t, "-C", tempDir) + _, statz, err := cmd(t, "-C", tempDir) as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) // use env t.Setenv("TREEFMT_WORKING_DIR", tempDir) - _, err = cmd(t, "-c") + _, statz, err = cmd(t, "-c") as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) } func TestFailOnChange(t *testing.T) { @@ -412,7 +412,7 @@ func TestFailOnChange(t *testing.T) { } test.WriteConfig(t, configPath, cfg) - _, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir) + _, _, err := cmd(t, "--fail-on-change", "--config-file", configPath, "--tree-root", tempDir) as.ErrorIs(err, format2.ErrFailOnChange) // we have second precision mod time tracking @@ -421,7 +421,7 @@ func TestFailOnChange(t *testing.T) { // test with no cache t.Setenv("TREEFMT_FAIL_ON_CHANGE", "true") test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") + _, _, err = cmd(t, "--config-file", configPath, "--tree-root", tempDir, "--no-cache") as.ErrorIs(err, format2.ErrFailOnChange) } @@ -463,33 +463,33 @@ func TestBustCacheOnFormatterChange(t *testing.T) { test.WriteConfig(t, configPath, cfg) args := []string{"--config-file", configPath, "--tree-root", tempDir} - _, err := cmd(t, args...) + _, statz, err := cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 32, 3, 0) + assertStats(t, as, statz, 32, 32, 3, 0) // tweak mod time of elm formatter as.NoError(test.RecreateSymlink(t, binPath+"/"+"elm-format")) - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 32, 3, 0) + assertStats(t, as, statz, 32, 32, 3, 0) // check cache is working - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // tweak mod time of python formatter as.NoError(test.RecreateSymlink(t, binPath+"/"+"black")) - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 32, 3, 0) + assertStats(t, as, statz, 32, 32, 3, 0) // check cache is working - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // add go formatter cfg.FormatterConfigs["go"] = &config.Formatter{ @@ -499,40 +499,40 @@ func TestBustCacheOnFormatterChange(t *testing.T) { } test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 32, 4, 0) + assertStats(t, as, statz, 32, 32, 4, 0) // check cache is working - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // remove python formatter delete(cfg.FormatterConfigs, "python") test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 32, 2, 0) + assertStats(t, as, statz, 32, 32, 2, 0) // check cache is working - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) // remove elm formatter delete(cfg.FormatterConfigs, "elm") test.WriteConfig(t, configPath, cfg) - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 32, 1, 0) + assertStats(t, as, statz, 32, 32, 1, 0) // check cache is working - _, err = cmd(t, args...) + _, statz, err = cmd(t, args...) as.NoError(err) - assertStats(t, as, 32, 0, 0, 0) + assertStats(t, as, statz, 32, 0, 0, 0) } func TestGitWorktree(t *testing.T) { @@ -568,9 +568,9 @@ func TestGitWorktree(t *testing.T) { as.NoError(err, "failed to get git worktree") run := func(traversed int32, emitted int32, matched int32, formatted int32) { - _, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) + _, statz, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir) as.NoError(err) - assertStats(t, as, traversed, emitted, matched, formatted) + assertStats(t, as, statz, traversed, emitted, matched, formatted) } // run before adding anything to the worktree @@ -590,9 +590,9 @@ func TestGitWorktree(t *testing.T) { run(28, 28, 28, 0) // walk with filesystem instead of git - _, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem") + _, statz, err := cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem") as.NoError(err) - assertStats(t, as, 60, 60, 60, 0) + assertStats(t, as, statz, 60, 60, 60, 0) // capture current cwd, so we can replace it after the test is finished cwd, err := os.Getwd() @@ -604,31 +604,33 @@ func TestGitWorktree(t *testing.T) { }) // format specific sub paths - _, err = cmd(t, "-C", tempDir, "-c", "go", "-vv") + _, statz, err = cmd(t, "-C", tempDir, "-c", "go", "-vv") as.NoError(err) - assertStats(t, as, 2, 2, 2, 0) + assertStats(t, as, statz, 2, 2, 2, 0) - _, err = cmd(t, "-C", tempDir, "-c", "go", "haskell") + _, statz, err = cmd(t, "-C", tempDir, "-c", "go", "haskell") as.NoError(err) - assertStats(t, as, 9, 9, 9, 0) + assertStats(t, as, statz, 9, 9, 9, 0) - _, err = cmd(t, "-C", tempDir, "-c", "go", "haskell", "ruby") + _, statz, err = cmd(t, "-C", tempDir, "-c", "go", "haskell", "ruby") as.NoError(err) - assertStats(t, as, 10, 10, 10, 0) + assertStats(t, as, statz, 10, 10, 10, 0) // try with a bad path - _, err = cmd(t, "-C", tempDir, "-c", "haskell", "foo") + _, _, err = cmd(t, "-C", tempDir, "-c", "haskell", "foo") as.ErrorContains(err, "path foo not found within the tree root") - assertStats(t, as, 0, 0, 0, 0) // try with a path not in the git index, e.g. it is skipped _, err = os.Create(filepath.Join(tempDir, "foo.txt")) as.NoError(err) - assertStats(t, as, 0, 0, 0, 0) - _, err = cmd(t, "-C", tempDir, "-c", "foo.txt") + _, statz, err = cmd(t, "-C", tempDir, "-c", "haskell", "foo.txt") as.NoError(err) - assertStats(t, as, 0, 0, 0, 0) + assertStats(t, as, statz, 7, 7, 7, 0) + + _, statz, err = cmd(t, "-C", tempDir, "-c", "foo.txt") + as.NoError(err) + assertStats(t, as, statz, 0, 0, 0, 0) } func TestPathsArg(t *testing.T) { @@ -662,22 +664,22 @@ func TestPathsArg(t *testing.T) { test.WriteConfig(t, configPath, cfg) // without any path args - _, err = cmd(t) + _, statz, err := cmd(t) as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) // specify some explicit paths - _, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") + _, statz, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") as.NoError(err) - assertStats(t, as, 2, 2, 2, 0) + assertStats(t, as, statz, 2, 2, 2, 0) // specify a bad path - _, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Bar.hs") + _, _, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Bar.hs") as.ErrorContains(err, "path haskell/Nested/Bar.hs not found within the tree root") // specify a path outside the tree root externalPath := filepath.Join(cwd, "go.mod") - _, err = cmd(t, "-c", externalPath) + _, _, err = cmd(t, "-c", externalPath) as.ErrorContains(err, fmt.Sprintf("path %s not found within the tree root", externalPath)) } @@ -704,7 +706,7 @@ func TestStdin(t *testing.T) { contents := `{ foo, ... }: "hello"` os.Stdin = test.TempFile(t, "", "stdin", &contents) // we get an error about the missing filename parameter. - out, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin") + out, _, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin") as.EqualError(err, "exactly one path should be specified when using the --stdin flag") as.Equal("", string(out)) @@ -712,9 +714,9 @@ func TestStdin(t *testing.T) { contents = `{ foo, ... }: "hello"` os.Stdin = test.TempFile(t, "", "stdin", &contents) - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.nix") + out, statz, err := cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.nix") as.NoError(err) - assertStats(t, as, 1, 1, 1, 1) + assertStats(t, as, statz, 1, 1, 1, 1) // the nix formatters should have reduced the example to the following as.Equal(`{ ...}: "hello" @@ -729,9 +731,9 @@ func TestStdin(t *testing.T) { ` os.Stdin = test.TempFile(t, "", "stdin", &contents) - out, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.md") + out, statz, err = cmd(t, "-C", tempDir, "--allow-missing-formatter", "--stdin", "test.md") as.NoError(err) - assertStats(t, as, 1, 1, 1, 1) + assertStats(t, as, statz, 1, 1, 1, 1) as.Equal(`| col1 | col2 | | ------ | --------- | @@ -778,7 +780,7 @@ func TestDeterministicOrderingInPipeline(t *testing.T) { }, }, }) - _, err = cmd(t, "-C", tempDir) + _, _, err = cmd(t, "-C", tempDir) as.NoError(err) matcher := regexp.MustCompile("^fmt-(.*)") @@ -843,17 +845,17 @@ func TestRunInSubdir(t *testing.T) { test.WriteConfig(t, configPath, cfg) // without any path args, should reformat the whole tree - _, err = cmd(t) + _, statz, err := cmd(t) as.NoError(err) - assertStats(t, as, 32, 32, 32, 0) + assertStats(t, as, statz, 32, 32, 32, 0) // specify some explicit paths, relative to the tree root - _, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") + _, statz, err = cmd(t, "-c", "elm/elm.json", "haskell/Nested/Foo.hs") as.NoError(err) - assertStats(t, as, 2, 2, 2, 0) + assertStats(t, as, statz, 2, 2, 2, 0) } -func cmd(t *testing.T, args ...string) ([]byte, error) { +func cmd(t *testing.T, args ...string) ([]byte, *stats.Stats, error) { t.Helper() tempDir := t.TempDir() @@ -877,7 +879,7 @@ func cmd(t *testing.T, args ...string) ([]byte, error) { }() // run the command - root := NewRoot() + root, statz := NewRoot() if args == nil { // we must pass an empty array otherwise cobra with use os.Args[1:] @@ -888,26 +890,26 @@ func cmd(t *testing.T, args ...string) ([]byte, error) { root.SetErr(tempOut) if err := root.Execute(); err != nil { - return nil, err + return nil, nil, err } // reset and read the temporary output if _, err := tempOut.Seek(0, 0); err != nil { - return nil, fmt.Errorf("failed to reset temp output for reading: %w", err) + return nil, nil, fmt.Errorf("failed to reset temp output for reading: %w", err) } out, err := io.ReadAll(tempOut) if err != nil { - return nil, fmt.Errorf("failed to read temp output: %w", err) + return nil, nil, fmt.Errorf("failed to read temp output: %w", err) } - return out, nil + return out, statz, nil } -func assertStats(t *testing.T, as *require.Assertions, traversed int32, emitted int32, matched int32, formatted int32) { +func assertStats(t *testing.T, as *require.Assertions, statz *stats.Stats, traversed int32, emitted int32, matched int32, formatted int32) { t.Helper() - as.Equal(traversed, stats.Value(stats.Traversed), "stats.traversed") - as.Equal(emitted, stats.Value(stats.Emitted), "stats.emitted") - as.Equal(matched, stats.Value(stats.Matched), "stats.matched") - as.Equal(formatted, stats.Value(stats.Formatted), "stats.formatted") + 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") } diff --git a/main.go b/main.go index 1e6f0732..2bb72428 100644 --- a/main.go +++ b/main.go @@ -8,7 +8,8 @@ import ( func main() { // todo how are exit codes thrown by commands? - if err := cmd.NewRoot().Execute(); err != nil { + root, _ := cmd.NewRoot() + if err := root.Execute(); err != nil { os.Exit(1) } } diff --git a/stats/stats.go b/stats/stats.go index 80f9da37..8c5e960b 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -16,36 +16,24 @@ const ( Formatted ) -var ( - counters map[Type]*atomic.Int32 +type Stats struct { start time.Time -) - -func Init() { - // record start time - start = time.Now() - - // 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 map[Type]*atomic.Int32 } -func Add(t Type, delta int32) int32 { - return counters[t].Add(delta) +func (s *Stats) Add(t Type, delta int32) int32 { + return s.counters[t].Add(delta) } -func Value(t Type) int32 { - return counters[t].Load() +func (s *Stats) Value(t Type) int32 { + return s.counters[t].Load() } -func Elapsed() time.Duration { - return time.Since(start) +func (s *Stats) Elapsed() time.Duration { + return time.Since(s.start) } -func Print() { +func (s *Stats) Print() { components := []string{ "traversed %d files", "emitted %d files for processing", @@ -55,10 +43,24 @@ func Print() { fmt.Printf( strings.Join(components, "\n"), - Value(Traversed), - Value(Emitted), - Value(Matched), - Value(Formatted), - Elapsed().Round(time.Millisecond), + s.Value(Traversed), + s.Value(Emitted), + s.Value(Matched), + s.Value(Formatted), + s.Elapsed().Round(time.Millisecond), ) } + +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{} + + return Stats{ + start: time.Now(), + counters: counters, + } +}