Skip to content

Commit

Permalink
feat: add fuzz and bench tests (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
thevilledev authored Oct 30, 2024
1 parent 5b7f4f5 commit 0b50f6a
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 13 deletions.
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ lit: lint
vet:
go vet $$(go list ./...)

test:
test: test-unit test-fuzz test-bench

test-unit:
go test -v -race -run ^Test -parallel=8 ./...

test-bench:
go test -v -benchmem -bench ^Benchmark -parallel=8 ./...

test-fuzz:
go test -v -race -run ^Fuzz -parallel=8 ./...

.PHONY: fmt lint test
42 changes: 30 additions & 12 deletions thespine.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,46 +78,64 @@ func Encode(s string) (string, error) {
// EncodeText takes a UTF-8 string as an input, splits it by whitespace and runs an anagram for each word.
// Error returned in case of an invalid UTF-8 string.
func EncodeText(s string) (string, error) {
o := ""
if s == "" {
return "", nil
}

var builder strings.Builder
ws := strings.Split(s, " ")
for i, w := range ws {
if w == "" {
continue // Skip empty strings or preserve them, depending on requirements
}

ew, err := Encode(w)
if err != nil {
return "", err
}
o += ew

builder.WriteString(ew)
if i != len(ws)-1 {
o += " "
builder.WriteString(" ")
}
}

return o, nil
return builder.String(), nil
}

// DecodeText takes a UTF-8 string as an input, splits it by whitespace and decodes each anagram word-by-word.
// Error returned in case of an invalid UTF-8 string.
func DecodeText(s string) (string, error) {
o := ""
if s == "" {
return "", nil
}

var builder strings.Builder
ws := strings.Split(s, " ")
for i, w := range ws {
if w == "" {
continue // Skip empty strings or preserve them, depending on requirements
}

ew, err := Decode(w)
if err != nil {
return "", err
}
o += ew

builder.WriteString(ew)
if i < len(ws)-1 {
o += " "
builder.WriteString(" ")
}
}

return o, nil
return builder.String(), nil
}

func runestring(r [][]rune) string {
var s string
for _, r := range r {
s += string(r)
var builder strings.Builder
for _, runes := range r {
builder.WriteString(string(runes))
}

return s
return builder.String()
}
154 changes: 154 additions & 0 deletions thespine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package thespine
import (
"fmt"
"log"
"strings"
"testing"
"unicode/utf8"
)

func ExampleDecode() {
Expand Down Expand Up @@ -252,3 +254,155 @@ func Test_DecodeText(t *testing.T) {
})
}
}

func FuzzEncodeDecodeComprehensive(f *testing.F) {
// Add seed corpus
seeds := []string{
"", // Empty string
"a", // Single char
"ab", // Two chars
"abc", // Three chars (group size)
"abcd", // More than group size
"Hello, 世界!", // Mixed ASCII and Unicode
"🌍🌎🌏", // Only emojis
" ", // Multiple spaces
"a\nb\tc", // Special whitespace
"a b c", // Multiple consecutive spaces
strings.Repeat("a", 1000), // Long string
"ᚠᛇᚻ᛫ᛒᛦᚦ", // Runes
"\u200B\u200C\u200D", // Zero-width characters
"a\u0300\u0301b\u0302c", // Combining diacritical marks
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, input string) {
// Skip invalid UTF-8
if !utf8.ValidString(input) {
return
}

// Test 1: Encode->Decode roundtrip
encoded, err := Encode(input)
if err != nil {
// Some inputs might legitimately fail to encode
return
}
decoded, err := Decode(encoded)
if err != nil {
t.Errorf("Failed to decode encoded string: %v", err)

return
}
if decoded != input {
t.Errorf("Roundtrip failed: input=%q, got=%q", input, decoded)
}

// Test 2: Check encoded string properties
inputRunes := []rune(input)
encodedRunes := []rune(encoded)
if len(inputRunes) != len(encodedRunes) {
t.Errorf("Length mismatch: input=%d, encoded=%d", len(inputRunes), len(encodedRunes))
}

// Test 3: Multiple encode/decode cycles
current := input
for i := range 3 {
encoded, err := Encode(current)
if err != nil {
t.Errorf("Failed at cycle %d: %v", i, err)

return
}
decoded, err := Decode(encoded)
if err != nil {
t.Errorf("Failed at cycle %d: %v", i, err)

return
}
if decoded != current {
t.Errorf("Cycle %d failed: expected=%q, got=%q", i, current, decoded)
}
current = decoded
}
})
}

func FuzzEncodeDecodeText(f *testing.F) {
seeds := []string{
"",
"hello world",
" spaced words ",
"one two three four",
"Hello,\nWorld!",
"Tab\there",
"Mixed 世界 Unicode",
"🌍 Earth 🌎 Globe 🌏",
strings.Repeat("word ", 100),
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, input string) {
// Skip invalid UTF-8
if !utf8.ValidString(input) {
return
}

// Test 1: EncodeText->DecodeText roundtrip
encoded, err := EncodeText(input)
if err != nil {
return
}
decoded, err := DecodeText(encoded)
if err != nil {
t.Errorf("Failed to decode encoded text: %v", err)

return
}

// Normalize spaces for comparison since that's part of the spec
normalizeSpaces := func(s string) string {
return strings.Join(strings.Fields(s), " ")
}

normalizedInput := normalizeSpaces(input)
normalizedDecoded := normalizeSpaces(decoded)

if normalizedDecoded != normalizedInput {
t.Errorf("Roundtrip failed:\ninput=%q\ngot=%q", normalizedInput, normalizedDecoded)
}

// Test 2: Check word boundaries are preserved
inputWords := strings.Fields(input)
encodedWords := strings.Fields(encoded)
if len(inputWords) != len(encodedWords) {
t.Errorf("Word count mismatch: input=%d, encoded=%d", len(inputWords), len(encodedWords))
}
})
}

// Add benchmark tests.
func BenchmarkEncode(b *testing.B) {
inputs := []struct {
name string
str string
}{
{"small", "hello"},
{"medium", strings.Repeat("hello", 100)},
{"large", strings.Repeat("hello", 1000)},
{"unicode", "Hello, 世界! 🌍"},
}

for _, input := range inputs {
b.Run(input.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = Encode(input.str)
}
})
}
}

0 comments on commit 0b50f6a

Please sign in to comment.