diff --git a/.go-covercheck.history.json b/.go-covercheck.history.json index c2e9878..0e9bd93 100644 --- a/.go-covercheck.history.json +++ b/.go-covercheck.history.json @@ -1,5 +1,50 @@ { "entries": [ + { + "commit": "a33d6daaa0cea52472df889a1e6a04490fa41151", + "branch": "copilot/fix-42", + "timestamp": "2025-08-02T06:01:01.917674791Z", + "results": { + "byFile": [ + { + "statementCoverage": "1/2", + "blockCoverage": "1/2", + "statementPercentage": 50, + "blockPercentage": 50, + "statementThreshold": 65, + "blockThreshold": 50, + "failed": true, + "file": "github.com/mach6/go-covercheck/pkg/math/math.go" + } + ], + "byPackage": [ + { + "statementCoverage": "1/2", + "blockCoverage": "1/2", + "statementPercentage": 50, + "blockPercentage": 50, + "statementThreshold": 65, + "blockThreshold": 50, + "failed": true, + "package": "github.com/mach6/go-covercheck/pkg/math" + } + ], + "byTotal": { + "statements": { + "coverage": "1/2", + "threshold": 80, + "percentage": 50, + "failed": true + }, + "blocks": { + "coverage": "1/2", + "threshold": 75, + "percentage": 50, + "failed": true + } + } + } + }, { "commit": "ef613d597be47cdeaa401aebee56ef0605eb4f8b", "branch": "detached", @@ -542,4 +587,4 @@ } } ] -} +} \ No newline at end of file diff --git a/README.md b/README.md index 573ae5d..938274c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A fast, flexible CLI tool for enforcing test coverage thresholds in Go projects. - Enforce minimum coverage thresholds for files, packages, and the entire project. - Supports statement and block coverage separately. - Native `table`|`json`|`yaml`|`md`|`html`|`csv`|`tsv` output. +- Configurable table styles (`default`|`light`|`bold`|`rounded`|`double`). - Configurable via a `.go-covercheck.yml` or CLI flags. - Sorting and colored table output. - Colored `json` and `yaml` output. @@ -27,7 +28,6 @@ The following items are noteworthy and not (currently) supported. - Does not support configurable profile block count (how many times a section of code was hit) thresholds. The assumption is any value `>=1` is enough. -- Table style is not configurable. - Color codes (see [Color Legend](#🎨-color-legend)) are not configurable. - Severity weights (see [Color Legend](#🎨-color-legend)) are not configurable. @@ -111,6 +111,9 @@ Here is a sample `.go-covercheck.yml` configuration file: statementThreshold: 65.0 blockThreshold: 60.0 +# Optional, table style for table output format (default: light) +tableStyle: bold + # Optional, by total thresholds overriding the global values above total: statements: 75.0 @@ -202,6 +205,26 @@ Flags: -v, --version version for go-covercheck ``` +### 🎨 Table Styles + +You can customize the appearance of table output using the `--table-style` flag or by configuring `tableStyle` in your `.go-covercheck.yml` file. + +Available table styles: +- `default` - ASCII characters (compatible with all terminals) +- `light` - Unicode light box drawing characters (default) +- `bold` - Unicode bold box drawing characters +- `rounded` - Unicode rounded corner characters +- `double` - Unicode double line characters + +**Example:** +```shell +# Use bold table style via CLI +go-covercheck --table-style=bold + +# Use rounded table style via config file +echo "tableStyle: rounded" >> .go-covercheck.yml +``` + ## 🕰️ History History is a feature that allows you to save and compare coverage results against previous runs. diff --git a/cmd/go-covercheck/root.go b/cmd/go-covercheck/root.go index b0413d8..6c9ef64 100644 --- a/cmd/go-covercheck/root.go +++ b/cmd/go-covercheck/root.go @@ -37,6 +37,9 @@ const ( FormatFlag = "format" FormatFlagShort = "f" + TableStyleFlag = "table-style" + TableStyleFlagUsage = "table style [default|light|bold|rounded|double]" + StatementThresholdFlag = "statement-threshold" StatementThresholdFlagShort = "s" StatementThresholdFlagUsage = "global statement threshold to enforce [0=disabled]" @@ -453,6 +456,10 @@ func applyConfigOverrides(cfg *config.Config, cmd *cobra.Command, noConfigFile b noConfigFile { cfg.Format = v } + if v, _ := cmd.Flags().GetString(TableStyleFlag); cmd.Flags().Changed(TableStyleFlag) || + noConfigFile { + cfg.TableStyle = v + } if v, _ := cmd.Flags().GetBool(NoTableFlag); cmd.Flags().Changed(NoTableFlag) || noConfigFile { cfg.NoTable = v @@ -538,6 +545,12 @@ func initFlags(cmd *cobra.Command) { FormatFlagUsage, ) + cmd.Flags().String( + TableStyleFlag, + config.TableStyleDefValue, + TableStyleFlagUsage, + ) + cmd.Flags().Float64P( StatementThresholdFlag, StatementThresholdFlagShort, diff --git a/pkg/config/config.go b/pkg/config/config.go index b5e5c39..9b13fb0 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -40,6 +40,13 @@ const ( FormatMD = "md" FormatDefault = FormatTable + TableStyleDefault = "default" + TableStyleLight = "light" + TableStyleBold = "bold" + TableStyleRounded = "rounded" + TableStyleDouble = "double" + TableStyleDefValue = TableStyleLight + thresholdOff = 0 thresholdMax = 100 @@ -78,6 +85,7 @@ type Config struct { NoSummary bool `yaml:"noSummary,omitempty"` NoColor bool `yaml:"noColor,omitempty"` Format string `yaml:"format,omitempty"` + TableStyle string `yaml:"tableStyle,omitempty"` TerminalWidth int `yaml:"terminalWidth,omitempty"` ModuleName string `yaml:"moduleName,omitempty"` } @@ -110,6 +118,7 @@ func (c *Config) ApplyDefaults() { c.SortOrder = SortOrderDefault c.Skip = []string{} c.Format = FormatDefault + c.TableStyle = TableStyleDefValue c.initPerFileWhenNil() c.initPerPackageWhenNil() @@ -148,6 +157,14 @@ func (c *Config) Validate() error { //nolint:cyclop FormatJSON, FormatYAML, FormatTable, FormatCSV, FormatHTML, FormatTSV, FormatMD) } + switch c.TableStyle { + case TableStyleDefault, TableStyleLight, TableStyleBold, TableStyleRounded, TableStyleDouble: + break + default: + return fmt.Errorf("table-style must be one of %s|%s|%s|%s|%s", + TableStyleDefault, TableStyleLight, TableStyleBold, TableStyleRounded, TableStyleDouble) + } + if c.NoSummary && c.NoTable && c.Format != FormatJSON && c.Format != FormatYAML { return fmt.Errorf("cannot specify both no-summary and no-table with format %s", c.Format) } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 4f54009..cac13fd 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -44,3 +44,64 @@ func TestLoad_InvalidYAML(t *testing.T) { require.Error(t, err) require.Nil(t, cfg) } + +func TestLoad_ValidYAML_WithTableStyle(t *testing.T) { + yaml := ` +statementThreshold: 75.0 +tableStyle: bold +` + tmpFile := path.Join(t.TempDir(), "test_config_table_style.yaml") + err := os.WriteFile(tmpFile, []byte(yaml), 0600) + require.NoError(t, err) + defer os.Remove(tmpFile) //nolint:errcheck + + cfg, err := config.Load(tmpFile) + require.NoError(t, err) + require.Equal(t, "bold", cfg.TableStyle) +} + +func TestApplyDefaults_TableStyle(t *testing.T) { + cfg := &config.Config{} + cfg.ApplyDefaults() + require.Equal(t, config.TableStyleDefValue, cfg.TableStyle) + require.Equal(t, "light", cfg.TableStyle) // Should be light by default +} + +func TestValidate_InvalidTableStyle(t *testing.T) { + cfg := &config.Config{ + StatementThreshold: 70, + BlockThreshold: 50, + SortBy: "file", + SortOrder: "asc", + Format: "table", + TableStyle: "invalid", + } + err := cfg.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "table-style must be one of") +} + +func TestValidate_ValidTableStyles(t *testing.T) { + validStyles := []string{ + config.TableStyleDefault, + config.TableStyleLight, + config.TableStyleBold, + config.TableStyleRounded, + config.TableStyleDouble, + } + + for _, style := range validStyles { + t.Run(style, func(t *testing.T) { + cfg := &config.Config{ + StatementThreshold: 70, + BlockThreshold: 50, + SortBy: "file", + SortOrder: "asc", + Format: "table", + TableStyle: style, + } + err := cfg.Validate() + require.NoError(t, err) + }) + } +} diff --git a/pkg/output/history.go b/pkg/output/history.go index 34d4455..623256a 100644 --- a/pkg/output/history.go +++ b/pkg/output/history.go @@ -13,6 +13,55 @@ import ( "github.com/mach6/go-covercheck/pkg/history" ) +// getHistoryTableStyle returns the appropriate table.Style for history tables. +func getHistoryTableStyle(cfg *config.Config) table.Style { + var boxStyle table.BoxStyle + + switch cfg.TableStyle { + case config.TableStyleDefault: + boxStyle = table.StyleBoxDefault + case config.TableStyleBold: + boxStyle = table.StyleBoxBold + case config.TableStyleRounded: + boxStyle = table.StyleBoxRounded + case config.TableStyleDouble: + boxStyle = table.StyleBoxDouble + default: // config.TableStyleLight or any other value + boxStyle = table.StyleBoxLight + } + + return table.Style{ + Name: "Custom", + Box: boxStyle, + Color: table.ColorOptionsDefault, + Format: table.FormatOptions{ + Footer: text.FormatDefault, + FooterAlign: text.AlignRight, + FooterVAlign: text.VAlignDefault, + Header: text.FormatUpper, + HeaderAlign: text.AlignCenter, + HeaderVAlign: text.VAlignDefault, + Row: text.FormatDefault, + RowAlign: text.AlignRight, + RowVAlign: text.VAlignDefault, + }, + HTML: table.DefaultHTMLOptions, + Options: table.Options{ + DoNotColorBordersAndSeparators: false, + DrawBorder: true, + SeparateColumns: true, + SeparateFooter: true, + SeparateHeader: true, + SeparateRows: true, + }, + Size: table.SizeOptions{ + WidthMax: cfg.TerminalWidth, + WidthMin: 0, + }, + Title: table.TitleOptionsDefault, + } +} + // CompareHistory shows the comparison output for a ref: and the results. func CompareHistory(ref string, refEntry *history.Entry, results compute.Results) { fmt.Printf("\n≡ Comparing against ref: %s [commit %s]\n", @@ -122,38 +171,7 @@ func ShowHistory(h *history.History, limit int, cfg *config.Config) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.SetStyle( - table.Style{ - Name: "Custom", - Box: table.StyleBoxLight, - Color: table.ColorOptionsDefault, - Format: table.FormatOptions{ - Footer: text.FormatDefault, - FooterAlign: text.AlignRight, - FooterVAlign: text.VAlignDefault, - Header: text.FormatUpper, - HeaderAlign: text.AlignCenter, - HeaderVAlign: text.VAlignDefault, - Row: text.FormatDefault, - RowAlign: text.AlignRight, - RowVAlign: text.VAlignDefault, - }, - HTML: table.DefaultHTMLOptions, - Options: table.Options{ - DoNotColorBordersAndSeparators: false, - DrawBorder: true, - SeparateColumns: true, - SeparateFooter: true, - SeparateHeader: true, - SeparateRows: true, - }, - Size: table.SizeOptions{ - WidthMax: cfg.TerminalWidth, - WidthMin: 0, - }, - Title: table.TitleOptionsDefault, - }, - ) + t.SetStyle(getHistoryTableStyle(cfg)) t.AppendHeader(table.Row{"Timestamp", "Commit", "Branch", "Tags", "Label", "Coverage"}) diff --git a/pkg/output/table.go b/pkg/output/table.go index 60b9fdf..c6f62a0 100644 --- a/pkg/output/table.go +++ b/pkg/output/table.go @@ -11,6 +11,55 @@ import ( "github.com/jedib0t/go-pretty/v6/text" ) +// getTableStyle returns the appropriate table.Style based on the config. +func getTableStyle(cfg *config.Config) table.Style { + var boxStyle table.BoxStyle + + switch cfg.TableStyle { + case config.TableStyleDefault: + boxStyle = table.StyleBoxDefault + case config.TableStyleBold: + boxStyle = table.StyleBoxBold + case config.TableStyleRounded: + boxStyle = table.StyleBoxRounded + case config.TableStyleDouble: + boxStyle = table.StyleBoxDouble + default: // config.TableStyleLight or any other value + boxStyle = table.StyleBoxLight + } + + return table.Style{ + Name: "Custom", + Box: boxStyle, + Color: table.ColorOptionsDefault, + Format: table.FormatOptions{ + Footer: text.FormatDefault, + FooterAlign: text.AlignRight, + FooterVAlign: text.VAlignDefault, + Header: text.FormatUpper, + HeaderAlign: text.AlignCenter, + HeaderVAlign: text.VAlignDefault, + Row: text.FormatDefault, + RowAlign: text.AlignRight, + RowVAlign: text.VAlignDefault, + }, + HTML: table.DefaultHTMLOptions, + Options: table.Options{ + DoNotColorBordersAndSeparators: false, + DrawBorder: true, + SeparateColumns: true, + SeparateFooter: true, + SeparateHeader: true, + SeparateRows: false, + }, + Size: table.SizeOptions{ + WidthMax: cfg.TerminalWidth, + WidthMin: 0, + }, + Title: table.TitleOptionsDefault, + } +} + func renderTable(results compute.Results, cfg *config.Config) { if cfg.NoTable { return @@ -19,38 +68,7 @@ func renderTable(results compute.Results, cfg *config.Config) { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.SetAllowedRowLength(cfg.TerminalWidth) - t.SetStyle( - table.Style{ - Name: "Custom", - Box: table.StyleBoxLight, - Color: table.ColorOptionsDefault, - Format: table.FormatOptions{ - Footer: text.FormatDefault, - FooterAlign: text.AlignRight, - FooterVAlign: text.VAlignDefault, - Header: text.FormatUpper, - HeaderAlign: text.AlignCenter, - HeaderVAlign: text.VAlignDefault, - Row: text.FormatDefault, - RowAlign: text.AlignRight, - RowVAlign: text.VAlignDefault, - }, - HTML: table.DefaultHTMLOptions, - Options: table.Options{ - DoNotColorBordersAndSeparators: false, - DrawBorder: true, - SeparateColumns: true, - SeparateFooter: true, - SeparateHeader: true, - SeparateRows: false, - }, - Size: table.SizeOptions{ - WidthMax: cfg.TerminalWidth, - WidthMin: 0, - }, - Title: table.TitleOptionsDefault, - }, - ) + t.SetStyle(getTableStyle(cfg)) t.AppendHeader(table.Row{"", "Statements", "Blocks", "Statement %", "Block %"}) diff --git a/samples/.go-covercheck.yml b/samples/.go-covercheck.yml index 8c3d8c7..9130bea 100644 --- a/samples/.go-covercheck.yml +++ b/samples/.go-covercheck.yml @@ -42,6 +42,11 @@ noColor: false # default table format: table +# the table style for table output format +# default|light|bold|rounded|double +# default light +tableStyle: light + # force output to the specified column width # autoselected when <= 0 # default 0