Skip to content

extract byte diff comparison test utility into common testhelper #283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion filesystem/ext4/directory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package ext4

import (
"testing"

"github.com/diskfs/go-diskfs/testhelper"
)

func TestDirectoryToBytes(t *testing.T) {
Expand All @@ -17,7 +19,7 @@ func TestDirectoryToBytes(t *testing.T) {
b := dir.toBytes(bytesPerBlock, directoryChecksumAppender(sb.checksumSeed, 2, 0))

// read the bytes from the disk
diff, diffString := dumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
diff, diffString := testhelper.DumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
if diff {
t.Errorf("directory.toBytes() mismatched, actual then expected\n%s", diffString)
}
Expand Down
5 changes: 3 additions & 2 deletions filesystem/ext4/groupdescriptors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"testing"

"github.com/diskfs/go-diskfs/testhelper"
"github.com/go-test/deep"
)

Expand Down Expand Up @@ -60,7 +61,7 @@ func TestGroupDescriptorToBytes(t *testing.T) {
}
b := gd.toBytes(sb.gdtChecksumType(), sb.checksumSeed)
expected = expected[:64]
diff, diffString := dumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
diff, diffString := testhelper.DumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
if diff {
t.Errorf("groupdescriptor.toBytes() mismatched, actual then expected\n%s", diffString)
}
Expand Down Expand Up @@ -94,7 +95,7 @@ func TestGroupDescriptorsToBytes(t *testing.T) {
descriptors: groupdescriptors,
}
b := gds.toBytes(sb.gdtChecksumType(), sb.checksumSeed)
diff, diffString := dumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
diff, diffString := testhelper.DumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
if diff {
t.Errorf("groupDescriptors.toBytes() mismatched, actual then expected\n%s", diffString)
}
Expand Down
3 changes: 2 additions & 1 deletion filesystem/ext4/superblock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"reflect"
"testing"

"github.com/diskfs/go-diskfs/testhelper"
"github.com/go-test/deep"
)

Expand Down Expand Up @@ -32,7 +33,7 @@ func TestSuperblockToBytes(t *testing.T) {
if err != nil {
t.Fatalf("Failed to serialize superblock: %v", err)
}
diff, diffString := dumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
diff, diffString := testhelper.DumpByteSlicesWithDiffs(b, expected, 32, false, true, true)
if diff {
t.Errorf("superblock.toBytes() mismatched, actual then expected\n%s", diffString)
}
Expand Down
135 changes: 0 additions & 135 deletions filesystem/ext4/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package ext4

import (
"bytes"
"fmt"
"testing"
)

Expand Down Expand Up @@ -64,137 +63,3 @@ func TestMinString(t *testing.T) {
})
}
}

// dumpByteSlice dump a byte slice in hex and optionally ASCII format.
// Optionally but position at the beginning of each row, like xxd.
// Optionally convert to ASCII at end of each row, like xxd.
// Can show positions at beginning of each row in hex, decimal or both.
// Can filter out all rows except those containing given positions in showOnlyBytes. If showOnlyBytes is nil, all rows are shown.
// If showOnlyBytes is not nil, even an empty slice, will only show those rows that contain the given positions.
func dumpByteSlice(b []byte, bytesPerRow int, showASCII, showPosHex, showPosDec bool, showOnlyBytes []int) (out string) {
var ascii []byte
// go through each byte.
// At each position:
// - if we are at the end of a row, print the ASCII representation of the row.
// - if we are at the middle of a row, add an extra space
// - if we are still in the byte slice, print the byte in hex with a space before it.
// - if we are past the end of the row, print spaces.
showOnlyMap := make(map[int]bool)
for _, v := range showOnlyBytes {
showOnlyMap[v] = true
}
// run by rows
numRows := len(b) / bytesPerRow
if len(b)%bytesPerRow != 0 {
numRows++
}
for i := 0; i < numRows; i++ {
firstByte := i * bytesPerRow
lastByte := firstByte + bytesPerRow
var row string
// row header includes optional position numbers
if showPosHex {
row += fmt.Sprintf("%08x ", firstByte)
}
if showPosDec {
row += fmt.Sprintf("%4d ", firstByte)
}
row += ": "
for j := firstByte; j < lastByte; j++ {
// every 8 bytes add extra spacing to make it easier to read
if j%8 == 0 {
row += " "
}
// regular byte, print in hex
if j < len(b) {
hex := fmt.Sprintf(" %02x", b[j])
if showOnlyBytes != nil && showOnlyMap[j] {
hex = "\033[1m\033[31m" + hex + "\033[0m"
}
row += hex
} else {
row += " "
}
switch {
case j >= len(b):
// past end of byte slice, print spaces
ascii = append(ascii, ' ')
case b[j] < 32 || b[j] > 126:
// unprintable characters, print a dot
ascii = append(ascii, '.')
default:
// printable characters, print the character
ascii = append(ascii, b[j])
}
}
// end of row, print the ASCII representation and a newline
if showASCII {
row += fmt.Sprintf(" %s", string(ascii))
ascii = ascii[:0]
}
row += "\n"

// calculate if we should include this row
var includeRow = true
if showOnlyBytes != nil {
includeRow = false
for j := firstByte; j < lastByte; j++ {
if showOnlyMap[j] {
includeRow = true
break
}
}
}
if includeRow {
out += row
}
}
return out
}

// diff
type diff struct {
Offset int
ByteA byte
ByteB byte
}

// compareByteSlices compares two byte slices position by position. If the byte slices are identical, diffs is length 0,
// otherwise it contains the positions of the differences.
func compareByteSlices(a, b []byte) (diffs []diff) {
maxSize := len(a)
if len(b) > maxSize {
maxSize = len(b)
}
for i := 0; i < maxSize; i++ {
switch {
case i >= len(a):
diffs = append(diffs, diff{Offset: i, ByteA: 0, ByteB: b[i]})
case i >= len(b):
diffs = append(diffs, diff{Offset: i, ByteA: a[i], ByteB: 0})
case a[i] != b[i]:
diffs = append(diffs, diff{Offset: i, ByteA: a[i], ByteB: b[i]})
}
}
return diffs
}

// dumpByteSlicesWithDiffs show two byte slices in hex and ASCII format, with differences highlighted.
//
//nolint:unparam // sure, bytesPerRow always is 32, but it could be something else
func dumpByteSlicesWithDiffs(a, b []byte, bytesPerRow int, showASCII, showPosHex, showPosDec bool) (different bool, out string) {
diffs := compareByteSlices(a, b)
// if there are no differences, just return an empty string
if len(diffs) == 0 {
return false, ""
}

showOnlyBytes := make([]int, len(diffs))
for i, d := range diffs {
showOnlyBytes[i] = d.Offset
}
out = dumpByteSlice(a, bytesPerRow, showASCII, showPosHex, showPosDec, showOnlyBytes)
out += "\n"
out += dumpByteSlice(b, bytesPerRow, showASCII, showPosHex, showPosDec, showOnlyBytes)
return true, out
}
135 changes: 135 additions & 0 deletions testhelper/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package testhelper

import "fmt"

// dumpByteSlice dump a byte slice in hex and optionally ASCII format.
// Optionally but position at the beginning of each row, like xxd.
// Optionally convert to ASCII at end of each row, like xxd.
// Can show positions at beginning of each row in hex, decimal or both.
// Can filter out all rows except those containing given positions in showOnlyBytes. If showOnlyBytes is nil, all rows are shown.
// If showOnlyBytes is not nil, even an empty slice, will only show those rows that contain the given positions.
func dumpByteSlice(b []byte, bytesPerRow int, showASCII, showPosHex, showPosDec bool, showOnlyBytes []int) (out string) {
var ascii []byte
// go through each byte.
// At each position:
// - if we are at the end of a row, print the ASCII representation of the row.
// - if we are at the middle of a row, add an extra space
// - if we are still in the byte slice, print the byte in hex with a space before it.
// - if we are past the end of the row, print spaces.
showOnlyMap := make(map[int]bool)
for _, v := range showOnlyBytes {
showOnlyMap[v] = true
}
// run by rows
numRows := len(b) / bytesPerRow
if len(b)%bytesPerRow != 0 {
numRows++
}
for i := 0; i < numRows; i++ {
firstByte := i * bytesPerRow
lastByte := firstByte + bytesPerRow
var row string
// row header includes optional position numbers
if showPosHex {
row += fmt.Sprintf("%08x ", firstByte)
}
if showPosDec {
row += fmt.Sprintf("%4d ", firstByte)
}
row += ": "
for j := firstByte; j < lastByte; j++ {
// every 8 bytes add extra spacing to make it easier to read
if j%8 == 0 {
row += " "
}
// regular byte, print in hex
if j < len(b) {
hex := fmt.Sprintf(" %02x", b[j])
if showOnlyBytes != nil && showOnlyMap[j] {
hex = "\033[1m\033[31m" + hex + "\033[0m"
}
row += hex
} else {
row += " "
}
switch {
case j >= len(b):
// past end of byte slice, print spaces
ascii = append(ascii, ' ')
case b[j] < 32 || b[j] > 126:
// unprintable characters, print a dot
ascii = append(ascii, '.')
default:
// printable characters, print the character
ascii = append(ascii, b[j])
}
}
// end of row, print the ASCII representation and a newline
if showASCII {
row += fmt.Sprintf(" %s", string(ascii))
ascii = ascii[:0]
}
row += "\n"

// calculate if we should include this row
var includeRow = true
if showOnlyBytes != nil {
includeRow = false
for j := firstByte; j < lastByte; j++ {
if showOnlyMap[j] {
includeRow = true
break
}
}
}
if includeRow {
out += row
}
}
return out
}

// diff
type diff struct {
Offset int
ByteA byte
ByteB byte
}

// compareByteSlices compares two byte slices position by position. If the byte slices are identical, diffs is length 0,
// otherwise it contains the positions of the differences.
func compareByteSlices(a, b []byte) (diffs []diff) {
maxSize := len(a)
if len(b) > maxSize {
maxSize = len(b)
}
for i := 0; i < maxSize; i++ {
switch {
case i >= len(a):
diffs = append(diffs, diff{Offset: i, ByteA: 0, ByteB: b[i]})
case i >= len(b):
diffs = append(diffs, diff{Offset: i, ByteA: a[i], ByteB: 0})
case a[i] != b[i]:
diffs = append(diffs, diff{Offset: i, ByteA: a[i], ByteB: b[i]})
}
}
return diffs
}

// dumpByteSlicesWithDiffs show two byte slices in hex and ASCII format, with differences highlighted.
func DumpByteSlicesWithDiffs(a, b []byte, bytesPerRow int, showASCII, showPosHex, showPosDec bool) (different bool, out string) {
diffs := compareByteSlices(a, b)
// if there are no differences, just return an empty string
if len(diffs) == 0 {
return false, ""
}

showOnlyBytes := make([]int, len(diffs))
for i, d := range diffs {
showOnlyBytes[i] = d.Offset
}
out = dumpByteSlice(a, bytesPerRow, showASCII, showPosHex, showPosDec, showOnlyBytes)
out += "\n"
out += dumpByteSlice(b, bytesPerRow, showASCII, showPosHex, showPosDec, showOnlyBytes)
return true, out
}