diff --git a/codebeaver.yml b/codebeaver.yml new file mode 100644 index 0000000000..03fd7d60b4 --- /dev/null +++ b/codebeaver.yml @@ -0,0 +1,2 @@ +from: go-1.21 +# This file was generated automatically by CodeBeaver based on your repository. Learn how to customize it here: https://docs.codebeaver.ai/configuration/ \ No newline at end of file diff --git a/fq_test.go b/fq_test.go new file mode 100644 index 0000000000..996e8aa449 --- /dev/null +++ b/fq_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "testing" + "github.com/wader/fq/pkg/cli" + "github.com/wader/fq/pkg/interp" +) + +// TestMainCallsCliMain tests that main() calls cliMain with the correct parameters. +func TestMainCallsCliMain(t *testing.T) { +var cliMain = cli.Main + + var called bool + mainFunc := func() { + cliMain(interp.DefaultRegistry, version) + } + origCliMain := cliMain + defer func() { cliMain = origCliMain }() + + cliMain = func(reg *interp.Registry, ver string) { + called = true + if reg != interp.DefaultRegistry { + t.Errorf("Expected interp.DefaultRegistry, got %v", reg) + } + if ver != version { + t.Errorf("Expected version %s, got %s", version, ver) + } + } + + mainFunc() + + if !called { + t.Error("cliMain was not called by main()") + } +} + +// TestVersion tests that the version constant is set to the expected value. +func TestVersion(t *testing.T) { + expected := "0.12.0" + if version != expected { + t.Errorf("Expected version %s, got %s", expected, version) + } +} \ No newline at end of file diff --git a/internal/bitiox/zeroreadatseeker_test.go b/internal/bitiox/zeroreadatseeker_test.go new file mode 100644 index 0000000000..3b6923e20c --- /dev/null +++ b/internal/bitiox/zeroreadatseeker_test.go @@ -0,0 +1,295 @@ +package bitiox + +import ( + "io" + "testing" + + "github.com/wader/fq/pkg/bitio" +) + +// TestZeroReadAtSeeker_SeekBits tests the SeekBits method including valid operations and error conditions. +func TestZeroReadAtSeeker_SeekBits(t *testing.T) { + const totalBits = 64 + zr := NewZeroAtSeeker(totalBits) + + // Valid Seek using io.SeekStart. + pos, err := zr.SeekBits(10, io.SeekStart) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if pos != 10 { + t.Fatalf("expected pos 10, got %d", pos) + } + + // Valid Seek using io.SeekCurrent. + pos, err = zr.SeekBits(5, io.SeekCurrent) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if pos != 15 { + t.Fatalf("expected pos 15, got %d", pos) + } + + // Valid Seek using io.SeekEnd. + pos, err = zr.SeekBits(-10, io.SeekEnd) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if pos != totalBits-10 { + t.Fatalf("expected pos %d, got %d", totalBits-10, pos) + } + + // Test error: negative position with io.SeekStart. + _, err = zr.SeekBits(-1, io.SeekStart) + if err != bitio.ErrOffset { + t.Errorf("expected ErrOffset for negative seek, got %v", err) + } + + // Test error: position greater than total bits with io.SeekStart. + _, err = zr.SeekBits(totalBits+1, io.SeekStart) + if err != bitio.ErrOffset { + t.Errorf("expected ErrOffset for excessive seek, got %v", err) + } + + // Test panic for unknown whence. + func() { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic for unknown whence, but no panic occurred") + } + }() + // This should panic. + zr.SeekBits(10, 999) + }() +} + +// TestZeroReadAtSeeker_ReadBitsAt tests the ReadBitsAt method including edge cases and partial reads. +func TestZeroReadAtSeeker_ReadBitsAt(t *testing.T) { + const totalBits = 64 + zr := NewZeroAtSeeker(totalBits) + + // Test error: negative bitOff. + buf := make([]byte, 10) + _, err := zr.ReadBitsAt(buf, 8, -1) + if err != bitio.ErrOffset { + t.Errorf("expected ErrOffset for negative bitOff, got %v", err) + } + + // Test EOF when bitOff equals total bits. + _, err = zr.ReadBitsAt(buf, 8, totalBits) + if err != io.EOF { + t.Errorf("expected EOF when bitOff equals totalBits, got %v", err) + } + + // Test reading within available bits: reading 20 bits from bitOff=0. + n, err := zr.ReadBitsAt(buf, 20, 0) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if n != 20 { + t.Errorf("expected to read 20 bits, got %d", n) + } + // Verify that the corresponding bytes are zero. + rBytes := bitio.BitsByteCount(20) + for i := int64(0); i < rBytes; i++ { + if buf[i] != 0 { + t.Errorf("expected byte at index %d to be 0, got %d", i, buf[i]) + } + } + + // Test reading beyond available bits: request 20 bits starting from bitOff=50, available = 14 bits. + n, err = zr.ReadBitsAt(buf, 20, 50) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if n != 14 { + t.Errorf("expected to read 14 bits, got %d", n) + } +} + +// TestZeroReadAtSeeker_CloneReadAtSeeker tests cloning of the reader and confirms independent operation. +func TestZeroReadAtSeeker_CloneReadAtSeeker(t *testing.T) { + const totalBits = 64 + zr := NewZeroAtSeeker(totalBits) + + // Advance the original seeker. + _, err := zr.SeekBits(10, io.SeekStart) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Clone the seeker. + clone, err := zr.CloneReadAtSeeker() + if err != nil { + t.Fatalf("unexpected error in clone: %v", err) + } + + // The clone should be independent and start at position 0. + posClone, err := clone.SeekBits(0, io.SeekCurrent) + if err != nil { + t.Fatalf("unexpected error on clone seek: %v", err) + } + if posClone != 0 { + t.Errorf("expected cloned seeker pos to be 0, got %d", posClone) + } + + // Changing clone's position should not affect original. + _, err = clone.SeekBits(20, io.SeekStart) + if err != nil { + t.Fatalf("unexpected error on clone seek: %v", err) + } + posOrig, err := zr.SeekBits(0, io.SeekCurrent) + if err != nil { + t.Fatalf("unexpected error on original seek: %v", err) + } + if posOrig != 10 { + t.Errorf("expected original seeker pos to remain 10, got %d", posOrig) + } +} +// TestZeroReadAtSeeker_ReadBitsAt_ZeroBits tests that reading zero bits returns 0 and no error. +func TestZeroReadAtSeeker_ReadBitsAt_ZeroBits(t *testing.T) { + const totalBits = 32 + zr := NewZeroAtSeeker(totalBits) + + buf := make([]byte, 5) + n, err := zr.ReadBitsAt(buf, 0, 0) + if err != nil { + t.Fatalf("expected no error for zero bits read, got %v", err) + } + if n != 0 { + t.Errorf("expected to read 0 bits, got %d", n) + } +} + +// TestZeroReadAtSeeker_ReadBitsAt_PartialByte tests reading a number of bits that does not perfectly align to a full byte. +func TestZeroReadAtSeeker_ReadBitsAt_PartialByte(t *testing.T) { + const totalBits = 16 + zr := NewZeroAtSeeker(totalBits) + + buf := make([]byte, 2) + // Request 5 bits starting at offset 0. + n, err := zr.ReadBitsAt(buf, 5, 0) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if n != 5 { + t.Errorf("expected to read 5 bits, got %d", n) + } + // The first byte should have been set to 0. + if buf[0] != 0 { + t.Errorf("expected the first byte to be 0, got %d", buf[0]) + } +} + +// TestZeroReadAtSeeker_SeekBits_NegativeCurrent tests that seeking backwards past the beginning returns an error. +func TestZeroReadAtSeeker_SeekBits_NegativeCurrent(t *testing.T) { + const totalBits = 64 + zr := NewZeroAtSeeker(totalBits) + + // Move to 30 bits. + _, err := zr.SeekBits(30, io.SeekStart) + if err != nil { + t.Fatalf("expected no error seeking to 30, got %v", err) + } + + // Attempt to move backwards beyond the beginning. + _, err = zr.SeekBits(-40, io.SeekCurrent) + if err != bitio.ErrOffset { + t.Errorf("expected ErrOffset for seeking before zero, got %v", err) + } +} +// TestZeroReadAtSeeker_ReadBitsAt_BufferTooSmall tests that providing a buffer which is too small causes a panic. +func TestZeroReadAtSeeker_ReadBitsAt_BufferTooSmall(t *testing.T) { + zr := NewZeroAtSeeker(64) + buf := make([]byte, 1) // insufficient buffer: reading 16 bits requires at least 2 bytes. + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic due to insufficient buffer size, but no panic occurred") + } + }() + _, _ = zr.ReadBitsAt(buf, 16, 0) // this should trigger a panic due to index out of range. +} + +// TestZeroReadAtSeeker_FullRead tests that reading the full available bits works correctly. +func TestZeroReadAtSeeker_FullRead(t *testing.T) { + const totalBits = 50 + zr := NewZeroAtSeeker(totalBits) + bufSize := bitio.BitsByteCount(totalBits) + buf := make([]byte, bufSize) + n, err := zr.ReadBitsAt(buf, totalBits, 0) + if err != nil { + t.Fatalf("expected no error on full read, got %v", err) + } + if n != totalBits { + t.Errorf("expected to read %d bits, got %d", totalBits, n) + } + for i, b := range buf { + if b != 0 { + t.Errorf("expected buffer byte %d to be 0, got %d", i, b) + } + } +} + +// TestZeroReadAtSeeker_SeekBits_End tests seeking to the end position using io.SeekEnd with offset 0. +func TestZeroReadAtSeeker_SeekBits_End(t *testing.T) { + const totalBits = 64 + zr := NewZeroAtSeeker(totalBits) + pos, err := zr.SeekBits(0, io.SeekEnd) + if err != nil { + t.Fatalf("expected no error seeking to end, got %v", err) + } + if pos != totalBits { + t.Errorf("expected position to be %d, got %d", totalBits, pos) + } +} +// TestZeroReadAtSeeker_ReadBitsAt_BufferIntegrity tests that ReadBitsAt writes only the required bytes in the buffer +// while leaving additional bytes unchanged. +func TestZeroReadAtSeeker_ReadBitsAt_BufferIntegrity(t *testing.T) { + const totalBits = 64 + zr := NewZeroAtSeeker(totalBits) + // Prepare a buffer longer than needed and initialize extra bytes to a non-zero value. + buf := []byte{0xFF, 0xFF, 0xFF, 0xFF} + // Request to read 12 bits, which requires 2 bytes (since (12+7)/8 == 2). + n, err := zr.ReadBitsAt(buf, 12, 0) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if n != 12 { + t.Errorf("expected to read 12 bits, got %d", n) + } + // The first 2 bytes should be set to 0. + if buf[0] != 0 || buf[1] != 0 { + t.Errorf("expected first two bytes to be 0, got %v", buf[:2]) + } + // The remaining bytes should remain unchanged (0xFF). + if buf[2] != 0xFF || buf[3] != 0xFF { + t.Errorf("expected remaining bytes to be unchanged, got %v", buf[2:4]) + } +} + +// TestZeroReadAtSeeker_ZeroTotalBits tests that an instance with zero total bits behaves as expected. +func TestZeroReadAtSeeker_ZeroTotalBits(t *testing.T) { + zr := NewZeroAtSeeker(0) + + // Seeking to 0 should be allowed. + pos, err := zr.SeekBits(0, io.SeekStart) + if err != nil { + t.Fatalf("expected no error seeking to 0, got %v", err) + } + if pos != 0 { + t.Errorf("expected position to be 0, got %d", pos) + } + + // Attempting to seek to any positive value should return an ErrOffset. + _, err = zr.SeekBits(1, io.SeekStart) + if err != bitio.ErrOffset { + t.Errorf("expected ErrOffset when seeking beyond 0, got %v", err) + } + + // Reading at offset 0 should return EOF as there are no bits. + buf := make([]byte, 1) + _, err = zr.ReadBitsAt(buf, 1, 0) + if err != io.EOF { + t.Errorf("expected EOF when reading from an instance with zero total bits, got %v", err) + } +} \ No newline at end of file diff --git a/internal/hexpairwriter/hexpairwriter_test.go b/internal/hexpairwriter/hexpairwriter_test.go index 41ca3dfa3e..75afae0d14 100644 --- a/internal/hexpairwriter/hexpairwriter_test.go +++ b/internal/hexpairwriter/hexpairwriter_test.go @@ -1,20 +1,235 @@ package hexpairwriter_test import ( - "bytes" - "log" - "testing" + "bytes" + "log" + "testing" + "errors" +"strings" - "github.com/wader/fq/internal/hexpairwriter" + "github.com/wader/fq/internal/hexpairwriter" ) +// errorWriter is a fake writer that always returns a write error. +type errorWriter struct{} + +func (e *errorWriter) Write(p []byte) (int, error) { + return 0, errors.New("simulated write error") +} func TestWrite(t *testing.T) { - b := &bytes.Buffer{} - h := hexpairwriter.New(b, 4, 0, hexpairwriter.Pair) - _, _ = h.Write([]byte("")) - _, _ = h.Write([]byte("ab")) - _, _ = h.Write([]byte("c")) - _, _ = h.Write([]byte("d")) + b := &bytes.Buffer{} + h := hexpairwriter.New(b, 4, 0, hexpairwriter.Pair) + _, _ = h.Write([]byte("")) + _, _ = h.Write([]byte("ab")) + _, _ = h.Write([]byte("c")) + _, _ = h.Write([]byte("d")) + + log.Printf("b.Bytes(): '%s'\n", b.Bytes()) +} - log.Printf("b.Bytes(): '%s'\n", b.Bytes()) +func TestStartLineOffset(t *testing.T) { + // Test writer with startLineOffset > 0 to ensure that the initial offset lines are written. + b := &bytes.Buffer{} + // Use width=4 and a startLineOffset=5 so that the pre-written blank lines are output. + h := hexpairwriter.New(b, 4, 5, hexpairwriter.Pair) + // Writing a single byte triggers the branch that writes the data after pre-filling. + if _, err := h.Write([]byte("A")); err != nil { + t.Fatalf("Write error: %v", err) + } + out := b.String() + if out == "" { + t.Errorf("Expected non-empty output, got empty") + } + t.Logf("TestStartLineOffset output:\n%s", out) +} +func TestWriteMultipleCalls(t *testing.T) { + // Test writing data in multiple calls so the internal buffer properly flushes complete lines. + b := &bytes.Buffer{} + // width=3 so that every 3 bytes a newline is inserted. + h := hexpairwriter.New(b, 3, 0, hexpairwriter.Pair) + // Write first chunk (partial line) + chunk1 := []byte("ab") + if _, err := h.Write(chunk1); err != nil { + t.Fatalf("Write error on chunk1: %v", err) + } + // Write second chunk (causing one full line and one partial line) + chunk2 := []byte("cdef") + if _, err := h.Write(chunk2); err != nil { + t.Fatalf("Write error on chunk2: %v", err) + } + out := b.String() + if out == "" { + t.Errorf("Expected non-empty output, got empty") + } + t.Logf("TestWriteMultipleCalls output:\n%s", out) +} +func TestWithCustomFn(t *testing.T) { + // Test using a custom formatting function, here wrapping the byte in brackets. + customFn := func(b byte) string { + return "[" + string(b) + "]" + } + b := &bytes.Buffer{} + // Use width=5 so that we don’t always complete a full line. + h := hexpairwriter.New(b, 5, 0, customFn) + input := []byte("hello") + if _, err := h.Write(input); err != nil { + t.Fatalf("Write error: %v", err) + } + out := b.String() + if out == "" { + t.Errorf("Expected non-empty output, got empty") + } + t.Logf("TestWithCustomFn output:\n%s", out) } +// TestEmptyInput verifies that writing an empty slice produces no output. +func TestEmptyInput(t *testing.T) { + b := &bytes.Buffer{} + h := hexpairwriter.New(b, 4, 0, hexpairwriter.Pair) + n, err := h.Write([]byte("")) + if err != nil { + t.Fatalf("Write error: %v", err) + } + if n != 0 { + t.Errorf("Expected 0 written bytes, got %d", n) + } + if b.Len() != 0 { + t.Errorf("Expected empty output, got %q", b.String()) + } +} + +// TestWriteError verifies that Write properly propagates errors from the underlying writer. +func TestWriteError(t *testing.T) { + // errorWriter is a fake writer that always returns an error. + + w := &errorWriter{} + h := hexpairwriter.New(w, 4, 0, hexpairwriter.Pair) + _, err := h.Write([]byte("abc")) + if err == nil { + t.Error("Expected error, got nil") + } +} + +// TestCompleteLineFlush verifies that the writer flushes complete lines as expected. +func TestCompleteLineFlush(t *testing.T) { + b := &bytes.Buffer{} + h := hexpairwriter.New(b, 2, 0, hexpairwriter.Pair) + // Write "abcd" which should produce one complete line and one partial line. + _, err := h.Write([]byte("abcd")) + if err != nil { + t.Fatalf("Write error: %v", err) + } + expected := hexpairwriter.Pair('a') + " " + hexpairwriter.Pair('b') + "\n" + + hexpairwriter.Pair('c') + " " + hexpairwriter.Pair('d') + got := b.String() + if got != expected { + t.Errorf("Expected output:\n%q\ngot:\n%q", expected, got) + } +} + +// TestCustomFnEmpty verifies behavior when the custom formatting function returns an empty string. +func TestCustomFnEmpty(t *testing.T) { + emptyFn := func(b byte) string { return "" } + b := &bytes.Buffer{} + h := hexpairwriter.New(b, 3, 0, emptyFn) + _, err := h.Write([]byte("abc")) + if err != nil { + t.Fatalf("Write error: %v", err) + } + got := b.String() + if len(got) == 0 { + t.Errorf("Expected non-empty output when custom function returns empty string, got empty") + } +} +func TestWriteOneByteAtATime(t *testing.T) { + // Test writing one byte at a time to ensure flushes occur correctly for each call when + // the line width is reached. This helps validate the per-byte flushing logic. + var b bytes.Buffer + h := hexpairwriter.New(&b, 4, 0, hexpairwriter.Pair) + for i := 0; i < 16; i++ { + if _, err := h.Write([]byte{byte(i)}); err != nil { + t.Fatalf("Write error at iteration %d: %v", i, err) + } + } + out := b.String() + expected := hexpairwriter.Pair(0) + " " + hexpairwriter.Pair(1) + " " + hexpairwriter.Pair(2) + " " + hexpairwriter.Pair(3) + "\n" + + hexpairwriter.Pair(4) + " " + hexpairwriter.Pair(5) + " " + hexpairwriter.Pair(6) + " " + hexpairwriter.Pair(7) + "\n" + + hexpairwriter.Pair(8) + " " + hexpairwriter.Pair(9) + " " + hexpairwriter.Pair(10) + " " + hexpairwriter.Pair(11) + "\n" + + hexpairwriter.Pair(12) + " " + hexpairwriter.Pair(13) + " " + hexpairwriter.Pair(14) + " " + hexpairwriter.Pair(15) + if out != expected { + t.Errorf("Expected output:\n%q\ngot:\n%q", expected, out) + } +} + +func TestWidthOne(t *testing.T) { + // Test using width=1 to ensure that every written byte (when formatted) appears on its own line. + var b bytes.Buffer + h := hexpairwriter.New(&b, 1, 0, hexpairwriter.Pair) + _, err := h.Write([]byte("abcd")) + if err != nil { + t.Fatalf("Write error: %v", err) + } + // With a width of one, every written byte should be flushed as its own line. + expected := hexpairwriter.Pair('a') + "\n" + + hexpairwriter.Pair('b') + "\n" + + hexpairwriter.Pair('c') + "\n" + + hexpairwriter.Pair('d') + if b.String() != expected { + t.Errorf("Expected %q, got %q", expected, b.String()) + } +} +// TestLongInputFlushes writes a long input slice at once to verify that complete lines flush correctly +func TestLongInputFlushes(t *testing.T) { + const width = 4 + const numBytes = 20 + // Create an input slice with 20 sequential bytes (0 to 19) + input := make([]byte, numBytes) + for i := 0; i < numBytes; i++ { + input[i] = byte(i) + } + + b := &bytes.Buffer{} + h := hexpairwriter.New(b, width, 0, hexpairwriter.Pair) + if _, err := h.Write(input); err != nil { + t.Fatalf("Write error: %v", err) + } + + out := b.String() + // Every time we complete a full line (except possibly the last partial one) the writer flushes with a newline. + // For width=4 and numBytes=20 bytes, we expect flushes after 4,8,12,16 bytes => 4 newlines total. + newlineCount := strings.Count(out, "\n") + const expectedNewlines = 4 + if newlineCount != expectedNewlines { + t.Errorf("Expected %d newlines, got %d", expectedNewlines, newlineCount) + } + // Verify that the output is non-empty and the last character is not a trailing space. + if out == "" { + t.Errorf("Expected non-empty output, got empty") + } + if out[len(out)-1] == ' ' { + t.Errorf("Output should not end with a space, got %q", out) + } +} + +// TestConstantFn verifies that a custom formatting function returning a constant string works as expected. +func TestConstantFn(t *testing.T) { + // customFn always returns "const" irrespective of the input byte. + customFn := func(b byte) string { + return "const" + } + b := &bytes.Buffer{} + // For width=3 writing three characters should produce a single flushed line. + h := hexpairwriter.New(b, 3, 0, customFn) + input := []byte("ABC") + if _, err := h.Write(input); err != nil { + t.Fatalf("Write error: %v", err) + } + out := b.String() + // The expected output is the concatenation of "const" for each input, separated by a space. + // Since the final flush is issued on the last byte without the extra trailing space, + // the expected output is: + // "const const const" + expected := "const const const" + if out != expected { + t.Errorf("Expected output:\n%q\ngot:\n%q", expected, out) + } +} \ No newline at end of file