Repository: Evertras/bubble-table
Pull request #107 (2022-06-11 14:48:25)

Add fuzzing tests for go 1.18 to fix scrolling edge cases
Development only requires 1.17, but if we use 1.18 we can take advantage of fuzzing to find more edge cases for scrolling.  This fixes the edge cases found and seems to now be stable/correct for horizontal scrolling edge guard.

Relates to #83 

Evertras: The coverage decrease is fine, because we removed code.  No new code has been added that is untested/uncovered.

Patch:
diff --git a/.gitignore b/.gitignore
index 429a559..fedc270 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,6 +13,9 @@
 *.out
 *.coverage
 
+# Fuzzing data
+testdata
+
 # Go vendor folder
 vendor/
 
diff --git a/table/scrolling.go b/table/scrolling.go
index c2a0689..3ee3256 100644
--- a/table/scrolling.go
+++ b/table/scrolling.go
@@ -47,10 +47,4 @@ func (m *Model) recalculateLastHorizontalColumn() {
 			m.maxHorizontalColumnIndex = i - m.horizontalScrollFreezeColumnsCount
 		}
 	}
-
-	if m.maxHorizontalColumnIndex <= m.horizontalScrollFreezeColumnsCount {
-		m.maxHorizontalColumnIndex = 0
-
-		return
-	}
 }


Test patch:
diff --git a/table/scrolling_fuzz_test.go b/table/scrolling_fuzz_test.go
new file mode 100644
index 0000000..93909aa
--- /dev/null
+++ b/table/scrolling_fuzz_test.go
@@ -0,0 +1,96 @@
+//go:build go1.18
+// +build go1.18
+
+package table
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/stretchr/testify/assert"
+)
+
+// This is long because of test cases
+// nolint: funlen
+func FuzzHorizontalScrollingStopEdgeCases(f *testing.F) {
+	const (
+		minNameWidth = 2
+		maxNameWidth = 50
+
+		minColWidth = 4
+		maxColWidth = 50
+
+		minNumCols = 1
+		maxNumCols = 500
+
+		minMaxWidth = 5
+		maxMaxWidth = 200
+
+		borderBuffer = 4
+	)
+
+	f.Add(5, 3, 5, 30)
+	f.Fuzz(func(t *testing.T, nameWidth, colWidth, numCols, maxWidth int) {
+		if nameWidth < minNameWidth ||
+			nameWidth > maxNameWidth ||
+			nameWidth > maxWidth-colWidth ||
+			nameWidth+colWidth+borderBuffer >= maxWidth {
+			return
+		}
+
+		if colWidth < minColWidth ||
+			colWidth > maxColWidth ||
+			colWidth >= maxWidth {
+			return
+		}
+
+		if numCols < minNumCols || numCols > maxNumCols {
+			return
+		}
+
+		if maxWidth < minMaxWidth || maxWidth > maxMaxWidth {
+			return
+		}
+
+		cols := []Column{NewColumn("Name", "Name", nameWidth)}
+		for i := 0; i < numCols; i++ {
+			s := fmt.Sprintf("%d", i+1)
+			cols = append(cols, NewColumn(s, s, colWidth))
+		}
+
+		rowData := RowData{"Name": "A"}
+
+		for i := 0; i < numCols; i++ {
+			s := fmt.Sprintf("%d", i+1)
+			rowData[s] = s
+		}
+
+		rows := []Row{NewRow(rowData)}
+
+		model := New(cols).
+			WithRows(rows).
+			WithStaticFooter("Footer").
+			WithMaxTotalWidth(maxWidth).
+			WithHorizontalFreezeColumnCount(1).
+			Focused(true)
+
+		hitScrollRight := func() {
+			model, _ = model.Update(tea.KeyMsg{Type: tea.KeyShiftRight})
+		}
+
+		// Excessive scrolling attempts to be sure
+		for i := 0; i < numCols*2; i++ {
+			hitScrollRight()
+		}
+
+		rendered := model.View()
+
+		assert.NotContains(t, rendered, ">")
+
+		if !strings.Contains(rendered, "…") {
+			assert.Contains(t, rendered, fmt.Sprintf("%d", numCols))
+		}
+	})
+}
diff --git a/table/scrolling_test.go b/table/scrolling_test.go
index 4f2c7c9..52f49a8 100644
--- a/table/scrolling_test.go
+++ b/table/scrolling_test.go
@@ -307,6 +307,18 @@ func TestHorizontalScrollingStopEdgeCases(t *testing.T) {
 			maxWidth:     20,
 			expectedCols: []int{7, 8},
 		},
+		{
+			numCols:   6,
+			nameWidth: 5,
+			colWidth:  3,
+			maxWidth:  30,
+		},
+		{
+			numCols:   50,
+			nameWidth: 20,
+			colWidth:  6,
+			maxWidth:  31,
+		},
 	}
 
 	for i, test := range tests {