From 5b2fc67eeef09b76a93dab3d93b15b725aaf1259 Mon Sep 17 00:00:00 2001 From: kevaundray Date: Mon, 18 Aug 2025 13:42:22 +0100 Subject: [PATCH] core/rawdb: add non-unix alternative for tablewriter (#32455) Continuation of https://github.com/ethereum/go-ethereum/issues/32022 tablewriter assumes unix or windows, which may not be the case for embedded targets. For v0.0.5 of tablewriter, it is noted in table.go: "The protocols were written in pure Go and works on windows and unix systems" --------- Co-authored-by: rjl493456442 --- core/rawdb/database.go | 3 +- core/rawdb/database_tablewriter_tinygo.go | 208 ++++++++++++++++++ .../rawdb/database_tablewriter_tinygo_test.go | 124 +++++++++++ core/rawdb/database_tablewriter_unix.go | 33 +++ 4 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 core/rawdb/database_tablewriter_tinygo.go create mode 100644 core/rawdb/database_tablewriter_tinygo_test.go create mode 100644 core/rawdb/database_tablewriter_unix.go diff --git a/core/rawdb/database.go b/core/rawdb/database.go index 25cd20d164..6a1b717066 100644 --- a/core/rawdb/database.go +++ b/core/rawdb/database.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/log" - "github.com/olekukonko/tablewriter" ) var ErrDeleteRangeInterrupted = errors.New("safe delete range operation interrupted") @@ -582,7 +581,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error { } total += ancient.size() } - table := tablewriter.NewWriter(os.Stdout) + table := newTableWriter(os.Stdout) table.SetHeader([]string{"Database", "Category", "Size", "Items"}) table.SetFooter([]string{"", "Total", total.String(), " "}) table.AppendBulk(stats) diff --git a/core/rawdb/database_tablewriter_tinygo.go b/core/rawdb/database_tablewriter_tinygo.go new file mode 100644 index 0000000000..2f8e456fd5 --- /dev/null +++ b/core/rawdb/database_tablewriter_tinygo.go @@ -0,0 +1,208 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// TODO: naive stub implementation for tablewriter + +//go:build tinygo +// +build tinygo + +package rawdb + +import ( + "errors" + "fmt" + "io" + "strings" +) + +const ( + cellPadding = 1 // Number of spaces on each side of cell content + totalPadding = 2 * cellPadding // Total padding per cell. Its two because we pad equally on both sides +) + +type Table struct { + out io.Writer + headers []string + footer []string + rows [][]string +} + +func newTableWriter(w io.Writer) *Table { + return &Table{out: w} +} + +// SetHeader sets the header row for the table. Headers define the column names +// and determine the number of columns for the entire table. +// +// All data rows and footer must have the same number of columns as the headers. +// +// Note: Headers are required - tables without headers will fail validation. +func (t *Table) SetHeader(headers []string) { + t.headers = headers +} + +// SetFooter sets an optional footer row for the table. +// +// The footer must have the same number of columns as the headers, or validation will fail. +func (t *Table) SetFooter(footer []string) { + t.footer = footer +} + +// AppendBulk sets all data rows for the table at once, replacing any existing rows. +// +// Each row must have the same number of columns as the headers, or validation +// will fail during Render(). +func (t *Table) AppendBulk(rows [][]string) { + t.rows = rows +} + +// Render outputs the complete table to the configured writer. The table is rendered +// with headers, data rows, and optional footer. +// +// If validation fails, an error message is written to the output and rendering stops. +func (t *Table) Render() { + if err := t.render(); err != nil { + fmt.Fprintf(t.out, "Error: %v\n", err) + return + } +} + +func (t *Table) render() error { + if err := t.validateColumnCount(); err != nil { + return err + } + + widths := t.calculateColumnWidths() + rowSeparator := t.buildRowSeparator(widths) + + if len(t.headers) > 0 { + t.printRow(t.headers, widths) + fmt.Fprintln(t.out, rowSeparator) + } + + for _, row := range t.rows { + t.printRow(row, widths) + } + + if len(t.footer) > 0 { + fmt.Fprintln(t.out, rowSeparator) + t.printRow(t.footer, widths) + } + + return nil +} + +// validateColumnCount checks that all rows and footer match the header column count +func (t *Table) validateColumnCount() error { + if len(t.headers) == 0 { + return errors.New("table must have headers") + } + + expectedCols := len(t.headers) + + // Check all rows have same column count as headers + for i, row := range t.rows { + if len(row) != expectedCols { + return fmt.Errorf("row %d has %d columns, expected %d", i, len(row), expectedCols) + } + } + + // Check footer has same column count as headers (if present) + footerPresent := len(t.footer) > 0 + if footerPresent && len(t.footer) != expectedCols { + return fmt.Errorf("footer has %d columns, expected %d", len(t.footer), expectedCols) + } + + return nil +} + +// calculateColumnWidths determines the minimum width needed for each column. +// +// This is done by finding the longest content in each column across headers, rows, and footer. +// +// Returns an int slice where widths[i] is the display width for column i (including padding). +func (t *Table) calculateColumnWidths() []int { + // Headers define the number of columns + cols := len(t.headers) + if cols == 0 { + return nil + } + + // Track maximum content width for each column (before padding) + widths := make([]int, cols) + + // Start with header widths + for i, h := range t.headers { + widths[i] = len(h) + } + + // Find max width needed for data cells in each column + for _, row := range t.rows { + for i, cell := range row { + widths[i] = max(widths[i], len(cell)) + } + } + + // Find max width needed for footer in each column + for i, f := range t.footer { + widths[i] = max(widths[i], len(f)) + } + + for i := range widths { + widths[i] += totalPadding + } + + return widths +} + +// buildRowSeparator creates a horizontal line to separate table rows. +// +// It generates a string with dashes (-) for each column width, joined by plus signs (+). +// +// Example output: "----------+--------+-----------" +func (t *Table) buildRowSeparator(widths []int) string { + parts := make([]string, len(widths)) + for i, w := range widths { + parts[i] = strings.Repeat("-", w) + } + return strings.Join(parts, "+") +} + +// printRow outputs a single row to the table writer. +// +// Each cell is padded with spaces and separated by pipe characters (|). +// +// Example output: " Database | Size | Items " +func (t *Table) printRow(row []string, widths []int) { + for i, cell := range row { + if i > 0 { + fmt.Fprint(t.out, "|") + } + + // Calculate centering pad without padding + contentWidth := widths[i] - totalPadding + cellLen := len(cell) + leftPadCentering := (contentWidth - cellLen) / 2 + rightPadCentering := contentWidth - cellLen - leftPadCentering + + // Build padded cell with centering + leftPadding := strings.Repeat(" ", cellPadding+leftPadCentering) + rightPadding := strings.Repeat(" ", cellPadding+rightPadCentering) + + fmt.Fprintf(t.out, "%s%s%s", leftPadding, cell, rightPadding) + } + fmt.Fprintln(t.out) +} diff --git a/core/rawdb/database_tablewriter_tinygo_test.go b/core/rawdb/database_tablewriter_tinygo_test.go new file mode 100644 index 0000000000..3bcf93832b --- /dev/null +++ b/core/rawdb/database_tablewriter_tinygo_test.go @@ -0,0 +1,124 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build tinygo +// +build tinygo + +package rawdb + +import ( + "bytes" + "strings" + "testing" +) + +func TestTableWriterTinyGo(t *testing.T) { + var buf bytes.Buffer + table := newTableWriter(&buf) + + headers := []string{"Database", "Size", "Items", "Status"} + rows := [][]string{ + {"chaindata", "2.5 GB", "1,234,567", "Active"}, + {"state", "890 MB", "456,789", "Active"}, + {"ancient", "15.2 GB", "2,345,678", "Readonly"}, + {"logs", "120 MB", "89,012", "Active"}, + } + footer := []string{"Total", "18.71 GB", "4,125,046", "-"} + + table.SetHeader(headers) + table.AppendBulk(rows) + table.SetFooter(footer) + table.Render() + + output := buf.String() + t.Logf("Table output using custom stub implementation:\n%s", output) +} + +func TestTableWriterValidationErrors(t *testing.T) { + // Test missing headers + t.Run("MissingHeaders", func(t *testing.T) { + var buf bytes.Buffer + table := newTableWriter(&buf) + + rows := [][]string{{"x", "y", "z"}} + + table.AppendBulk(rows) + table.Render() + + output := buf.String() + if !strings.Contains(output, "table must have headers") { + t.Errorf("Expected error for missing headers, got: %s", output) + } + }) + + t.Run("NotEnoughRowColumns", func(t *testing.T) { + var buf bytes.Buffer + table := newTableWriter(&buf) + + headers := []string{"A", "B", "C"} + badRows := [][]string{ + {"x", "y"}, // Missing column + } + + table.SetHeader(headers) + table.AppendBulk(badRows) + table.Render() + + output := buf.String() + if !strings.Contains(output, "row 0 has 2 columns, expected 3") { + t.Errorf("Expected validation error for row 0, got: %s", output) + } + }) + + t.Run("TooManyRowColumns", func(t *testing.T) { + var buf bytes.Buffer + table := newTableWriter(&buf) + + headers := []string{"A", "B", "C"} + badRows := [][]string{ + {"p", "q", "r", "s"}, // Extra column + } + + table.SetHeader(headers) + table.AppendBulk(badRows) + table.Render() + + output := buf.String() + if !strings.Contains(output, "row 0 has 4 columns, expected 3") { + t.Errorf("Expected validation error for row 0, got: %s", output) + } + }) + + // Test mismatched footer columns + t.Run("MismatchedFooterColumns", func(t *testing.T) { + var buf bytes.Buffer + table := newTableWriter(&buf) + + headers := []string{"A", "B", "C"} + rows := [][]string{{"x", "y", "z"}} + footer := []string{"total", "sum"} // Missing column + + table.SetHeader(headers) + table.AppendBulk(rows) + table.SetFooter(footer) + table.Render() + + output := buf.String() + if !strings.Contains(output, "footer has 2 columns, expected 3") { + t.Errorf("Expected validation error for footer, got: %s", output) + } + }) +} diff --git a/core/rawdb/database_tablewriter_unix.go b/core/rawdb/database_tablewriter_unix.go new file mode 100644 index 0000000000..8bec5396e8 --- /dev/null +++ b/core/rawdb/database_tablewriter_unix.go @@ -0,0 +1,33 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +//go:build !tinygo +// +build !tinygo + +package rawdb + +import ( + "io" + + "github.com/olekukonko/tablewriter" +) + +// Re-export the real tablewriter types and functions +type Table = tablewriter.Table + +func newTableWriter(w io.Writer) *Table { + return tablewriter.NewWriter(w) +}