diff --git a/README.md b/README.md index 777a009..aeceda5 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ structalign [flags] [packages] -summary in diff mode, print a one-line summary after the diffs -sort present results largest-first (diff: by bytes saved; inspect: by struct size) + -threshold int in diff mode, only show structs that save at least N bytes + (default 0; negatives treated as 0) -type string only consider named structs matching these comma-separated glob patterns (e.g. "*Request,Config"); empty means all diff --git a/internal/app/app.go b/internal/app/app.go index 2873d74..e585b7a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -72,6 +72,7 @@ type options struct { skipCachePadded bool summary bool sort bool + threshold int } // savings is the absolute bytes a finding saves, or 0 when sizes are unknown or @@ -108,6 +109,7 @@ func (a *App) Run(args []string) int { fs.BoolVar(&opt.skipCachePadded, "skip-cache-padded", false, "skip structs containing a golang.org/x/sys/cpu.CacheLinePad field") fs.BoolVar(&opt.summary, "summary", false, "in diff mode, print a one-line summary after the diffs") fs.BoolVar(&opt.sort, "sort", false, "present results largest-first (diff: by bytes saved; inspect: by struct size)") + fs.IntVar(&opt.threshold, "threshold", 0, "in diff mode, only show structs that save at least this many bytes") fs.Usage = func() { fmt.Fprintf(a.Stderr, "structalign: print field-aligned struct reorderings (no file changes)\n\n") fmt.Fprintf(a.Stderr, "usage: structalign [flags] [packages]\n\n") @@ -197,6 +199,17 @@ func (a *App) Run(args []string) int { } } + if !opt.inspect && opt.threshold > 0 { + min := int64(opt.threshold) + kept := allFindings[:0] + for _, f := range allFindings { + if savings(f) >= min { + kept = append(kept, f) + } + } + allFindings = kept + } + if opt.sort { if opt.inspect { sort.SliceStable(allLayouts, func(i, j int) bool { diff --git a/internal/app/threshold_test.go b/internal/app/threshold_test.go new file mode 100644 index 0000000..d6eb1c1 --- /dev/null +++ b/internal/app/threshold_test.go @@ -0,0 +1,55 @@ +package app_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +// These reuse sortApp / sortSrc from sort_test.go, where Small saves 8 bytes +// (24->16) and Big saves 16 (40->24). + +func TestRunThresholdFiltersSmallSavings(t *testing.T) { + var out, errb bytes.Buffer + a := sortApp(t, &out, &errb) + // 16 is inclusive: keeps Big (saves 16), drops Small (saves 8). + code := a.Run([]string{"-threshold=16", "pkg"}) + assert.Equal(t, 1, code) + s := out.String() + assert.Contains(t, s, "Big") + assert.NotContains(t, s, "Small") +} + +func TestRunThresholdZeroShowsAll(t *testing.T) { + var out, errb bytes.Buffer + a := sortApp(t, &out, &errb) + _ = a.Run([]string{"-threshold=0", "pkg"}) + s := out.String() + assert.Contains(t, s, "Big") + assert.Contains(t, s, "Small") +} + +func TestRunThresholdNegativeIsZero(t *testing.T) { + var out, errb bytes.Buffer + a := sortApp(t, &out, &errb) + _ = a.Run([]string{"-threshold=-5", "pkg"}) + assert.Contains(t, out.String(), "Small", "a negative threshold behaves like 0 (no filtering)") +} + +func TestRunThresholdExitsZeroWhenAllFiltered(t *testing.T) { + var out, errb bytes.Buffer + a := sortApp(t, &out, &errb) + code := a.Run([]string{"-threshold=10000", "pkg"}) + assert.Equal(t, 0, code, "all filtered out => no findings => exit 0") + assert.Equal(t, "", strings.TrimSpace(out.String())) +} + +func TestRunThresholdSummaryReflectsFiltered(t *testing.T) { + var out, errb bytes.Buffer + a := sortApp(t, &out, &errb) + _ = a.Run([]string{"-threshold=16", "-summary", "pkg"}) + assert.Contains(t, out.String(), "Summary: 1 struct affected, 16 bytes saved", + "summary counts only the structs that pass the threshold") +}