Files
akvorado/common/schema/clickhouse_test.go
Vincent Bernat e2f1df9add tests: replace godebug by go-cmp for structure diffs
go-cmp is stricter and allow to catch more problems. Moreover, the
output is a bit nicer.
2025-08-23 16:03:09 +02:00

621 lines
18 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package schema
import (
"net/netip"
"slices"
"strings"
"testing"
"akvorado/common/helpers"
"github.com/ClickHouse/ch-go/proto"
"github.com/google/go-cmp/cmp"
)
func TestAppendDefault(t *testing.T) {
c := NewMock(t).EnableAllColumns()
bf := c.NewFlowMessage()
bf.Finalize()
if bf.batch.rowCount != 1 {
t.Errorf("rowCount should be 1, not %d", bf.batch.rowCount)
}
if bf.batch.columnSet.Any() {
t.Error("columnSet should be empty after finalize")
}
for idx, col := range bf.batch.columns {
if col == nil {
continue
}
if col.Rows() != 1 {
t.Errorf("column %q should be length 1", ColumnKey(idx))
}
}
}
func TestAppendBasics(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Test basic append
bf.AppendDateTime(ColumnTimeReceived, 1000)
bf.AppendUint(ColumnSamplingRate, 20000)
bf.AppendUint(ColumnDstAS, 65000)
// Test zero value (should not append)
bf.AppendUint(ColumnSrcAS, 0)
// Test duplicate append
bf.AppendUint(ColumnPackets, 100)
bf.AppendUint(ColumnPackets, 200)
expected := map[ColumnKey]any{
ColumnTimeReceived: uint32(1000),
ColumnSamplingRate: uint64(20000),
ColumnDstAS: uint32(65000),
ColumnPackets: uint64(100),
}
got := bf.OtherColumns
if diff := helpers.Diff(got, expected); diff != "" {
t.Errorf("Append() (-got, +want):\n%s", diff)
}
bf.Finalize()
for idx, col := range bf.batch.columns {
if col == nil {
continue
}
if col.Rows() != 1 {
t.Errorf("column %q should be length 1", ColumnKey(idx))
}
}
}
func TestAppendWithDisabledColumns(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Try to append to a disabled column (L2 group is disabled by default in mock)
bf.AppendUint(ColumnSrcVlan, 100)
bf.Finalize()
}
func TestAppendArrayUInt32Columns(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
bf.AppendArrayUInt32(ColumnDstASPath, []uint32{65400, 65500, 65001})
bf.Finalize()
bf.AppendArrayUInt32(ColumnDstASPath, []uint32{65403, 65503, 65003})
bf.Finalize()
// Verify column has data
got := bf.batch.columns[ColumnDstASPath].(*proto.ColArr[uint32])
expected := proto.ColArr[uint32]{
Offsets: proto.ColUInt64{3, 6},
Data: &proto.ColUInt32{65400, 65500, 65001, 65403, 65503, 65003},
}
if diff := helpers.Diff(got, &expected); diff != "" {
t.Errorf("AppendArrayUInt32 (-got, +want):\n%s", diff)
}
}
func TestAppendArrayUInt128Columns(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
bf.AppendArrayUInt128(ColumnDstLargeCommunities, []UInt128{
{
High: (65401 << 32) + 100,
Low: 200,
},
{
High: (65401 << 32) + 100,
Low: 201,
},
})
bf.Finalize()
got := bf.batch.columns[ColumnDstLargeCommunities].(*proto.ColArr[proto.UInt128])
expected := proto.ColArr[proto.UInt128]{
Offsets: proto.ColUInt64{2},
Data: &proto.ColUInt128{
{High: (65401 << 32) + 100, Low: 200},
{High: (65401 << 32) + 100, Low: 201},
},
}
if diff := helpers.Diff(got, &expected); diff != "" {
t.Errorf("AppendArrayUInt128 (-got, +want):\n%s", diff)
}
}
func TestUndoUInt64(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add two values
bf.AppendUint(ColumnBytes, 100)
bf.AppendUint(ColumnPackets, 200)
// Check we have the expected initial state
bytesCol := bf.batch.columns[ColumnBytes].(*proto.ColUInt64)
packetsCol := bf.batch.columns[ColumnPackets].(*proto.ColUInt64)
expectedBytes := proto.ColUInt64{100}
expectedPackets := proto.ColUInt64{200}
if diff := helpers.Diff(bytesCol, &expectedBytes); diff != "" {
t.Errorf("Initial bytes column state (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(packetsCol, &expectedPackets); diff != "" {
t.Errorf("Initial packets column state (-got, +want):\n%s", diff)
}
// Undo should remove the last appended values
bf.Undo()
expectedBytesAfter := proto.ColUInt64{}
expectedPacketsAfter := proto.ColUInt64{}
if diff := helpers.Diff(bytesCol, &expectedBytesAfter); diff != "" {
t.Errorf("Bytes column after undo (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(packetsCol, &expectedPacketsAfter); diff != "" {
t.Errorf("Packets column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoUInt32(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add two values
bf.AppendUint(ColumnSrcAS, 65001)
bf.AppendUint(ColumnDstAS, 65002)
// Check we have the expected initial state
srcCol := bf.batch.columns[ColumnSrcAS].(*proto.ColUInt32)
dstCol := bf.batch.columns[ColumnDstAS].(*proto.ColUInt32)
expectedSrc := proto.ColUInt32{65001}
expectedDst := proto.ColUInt32{65002}
if diff := helpers.Diff(srcCol, &expectedSrc); diff != "" {
t.Errorf("Initial SrcAS column state (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(dstCol, &expectedDst); diff != "" {
t.Errorf("Initial DstAS column state (-got, +want):\n%s", diff)
}
// Undo should remove the last appended values
bf.Undo()
expectedSrcAfter := proto.ColUInt32{}
expectedDstAfter := proto.ColUInt32{}
if diff := helpers.Diff(srcCol, &expectedSrcAfter); diff != "" {
t.Errorf("SrcAS column after undo (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(dstCol, &expectedDstAfter); diff != "" {
t.Errorf("DstAS column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoUInt16(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add two values
bf.AppendUint(ColumnSrcPort, 80)
bf.AppendUint(ColumnDstPort, 443)
// Check we have the expected initial state
srcCol := bf.batch.columns[ColumnSrcPort].(*proto.ColUInt16)
dstCol := bf.batch.columns[ColumnDstPort].(*proto.ColUInt16)
expectedSrc := proto.ColUInt16{80}
expectedDst := proto.ColUInt16{443}
if diff := helpers.Diff(srcCol, &expectedSrc); diff != "" {
t.Errorf("Initial SrcPort column state (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(dstCol, &expectedDst); diff != "" {
t.Errorf("Initial DstPort column state (-got, +want):\n%s", diff)
}
// Undo should remove the last appended values
bf.Undo()
expectedSrcAfter := proto.ColUInt16{}
expectedDstAfter := proto.ColUInt16{}
if diff := helpers.Diff(srcCol, &expectedSrcAfter); diff != "" {
t.Errorf("SrcPort column after undo (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(dstCol, &expectedDstAfter); diff != "" {
t.Errorf("DstPort column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoUInt8(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add value
bf.AppendUint(ColumnSrcNetMask, 6)
// Check we have the expected initial state
col := bf.batch.columns[ColumnSrcNetMask].(*proto.ColUInt8)
expected := proto.ColUInt8{6}
if diff := helpers.Diff(col, &expected); diff != "" {
t.Errorf("Initial Proto column state (-got, +want):\n%s", diff)
}
// Undo should remove the last appended value
bf.Undo()
expectedAfter := proto.ColUInt8{}
if diff := helpers.Diff(col, &expectedAfter); diff != "" {
t.Errorf("Proto column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoIPv6(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add IPv6 values
srcAddr := netip.MustParseAddr("2001:db8::1")
dstAddr := netip.MustParseAddr("2001:db8::2")
bf.AppendIPv6(ColumnSrcAddr, srcAddr)
bf.AppendIPv6(ColumnDstAddr, dstAddr)
// Check we have the expected initial state
srcCol := bf.batch.columns[ColumnSrcAddr].(*proto.ColIPv6)
dstCol := bf.batch.columns[ColumnDstAddr].(*proto.ColIPv6)
expectedSrc := proto.ColIPv6{srcAddr.As16()}
expectedDst := proto.ColIPv6{dstAddr.As16()}
if diff := helpers.Diff(srcCol, &expectedSrc); diff != "" {
t.Errorf("Initial SrcAddr column state (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(dstCol, &expectedDst); diff != "" {
t.Errorf("Initial DstAddr column state (-got, +want):\n%s", diff)
}
// Undo should remove the values
bf.Undo()
expectedSrcAfter := proto.ColIPv6{}
expectedDstAfter := proto.ColIPv6{}
if diff := helpers.Diff(srcCol, &expectedSrcAfter); diff != "" {
t.Errorf("SrcAddr column after undo (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(dstCol, &expectedDstAfter); diff != "" {
t.Errorf("DstAddr column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoDateTime(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add DateTime value
bf.AppendDateTime(ColumnTimeReceived, 1000)
// Check we have the expected initial state
col := bf.batch.columns[ColumnTimeReceived].(*proto.ColDateTime)
expected := proto.ColDateTime{Data: []proto.DateTime{1000}}
if diff := helpers.Diff(col, &expected); diff != "" {
t.Errorf("Initial TimeReceived column state (-got, +want):\n%s", diff)
}
// Undo should remove the value
bf.Undo()
expectedAfter := proto.ColDateTime{Data: []proto.DateTime{}}
if diff := helpers.Diff(col, &expectedAfter); diff != "" {
t.Errorf("TimeReceived column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoEnum8(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add Enum8 value (using interface boundary enum)
bf.AppendUint(ColumnInIfBoundary, uint64(InterfaceBoundaryExternal))
// Check we have the expected initial state
col := bf.batch.columns[ColumnInIfBoundary].(*proto.ColEnum8)
expected := proto.ColEnum8{proto.Enum8(InterfaceBoundaryExternal)}
if diff := helpers.Diff(col, &expected); diff != "" {
t.Errorf("Initial InIfBoundary column state (-got, +want):\n%s", diff)
}
// Undo should remove the value
bf.Undo()
expectedAfter := proto.ColEnum8{}
if diff := helpers.Diff(col, &expectedAfter); diff != "" {
t.Errorf("InIfBoundary column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoLowCardinalityString(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add LowCardinality string values
bf.AppendString(ColumnExporterName, "router1")
bf.AppendString(ColumnExporterRole, "edge")
// Check we have the expected initial state
nameCol := bf.batch.columns[ColumnExporterName].(*proto.ColLowCardinality[string])
roleCol := bf.batch.columns[ColumnExporterRole].(*proto.ColLowCardinality[string])
expectedName := proto.ColLowCardinality[string]{Values: []string{"router1"}}
expectedRole := proto.ColLowCardinality[string]{Values: []string{"edge"}}
diffOpt := cmp.Comparer(func(x, y proto.ColLowCardinality[string]) bool {
return slices.Compare(x.Values, y.Values) == 0
})
if diff := helpers.Diff(nameCol, &expectedName, diffOpt); diff != "" {
t.Errorf("Initial ExporterName column state (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(roleCol, &expectedRole, diffOpt); diff != "" {
t.Errorf("Initial ExporterRole column state (-got, +want):\n%s", diff)
}
// Undo should remove the values
bf.Undo()
expectedNameAfter := proto.ColLowCardinality[string]{Values: []string{}}
expectedRoleAfter := proto.ColLowCardinality[string]{Values: []string{}}
if diff := helpers.Diff(nameCol, &expectedNameAfter, diffOpt); diff != "" {
t.Errorf("ExporterName column after undo (-got, +want):\n%s", diff)
}
if diff := helpers.Diff(roleCol, &expectedRoleAfter, diffOpt); diff != "" {
t.Errorf("ExporterRole column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoLowCardinalityIPv6(t *testing.T) {
c := NewMock(t)
bf := c.NewFlowMessage()
// Add LowCardinality IPv6 value
addr := netip.MustParseAddr("2001:db8::1")
bf.AppendIPv6(ColumnExporterAddress, addr)
diffOpt := cmp.Comparer(func(x, y proto.ColLowCardinality[proto.IPv6]) bool {
return slices.CompareFunc(x.Values, y.Values, func(a, b proto.IPv6) int {
return strings.Compare(a.String(), b.String())
}) == 0
})
// Check we have the expected initial state
col := bf.batch.columns[ColumnExporterAddress].(*proto.ColLowCardinality[proto.IPv6])
expected := proto.ColLowCardinality[proto.IPv6]{Values: []proto.IPv6{addr.As16()}}
if diff := helpers.Diff(col, &expected, diffOpt); diff != "" {
t.Errorf("Initial ExporterAddress column state (-got, +want):\n%s", diff)
}
// Undo should remove the value
bf.Undo()
expectedAfter := proto.ColLowCardinality[proto.IPv6]{Values: []proto.IPv6{}}
if diff := helpers.Diff(col, &expectedAfter, diffOpt); diff != "" {
t.Errorf("ExporterAddress column after undo (-got, +want):\n%s", diff)
}
}
func TestUndoArrayUInt32(t *testing.T) {
c := NewMock(t)
t.Run("one value", func(t *testing.T) {
bf := c.NewFlowMessage()
bf.AppendArrayUInt32(ColumnDstASPath, []uint32{65001, 65002, 65003})
// Check we have the expected initial state
col := bf.batch.columns[ColumnDstASPath].(*proto.ColArr[uint32])
expected := proto.ColArr[uint32]{
Offsets: proto.ColUInt64{3},
Data: &proto.ColUInt32{65001, 65002, 65003},
}
if diff := helpers.Diff(*col, expected); diff != "" {
t.Errorf("Initial DstASPath column state (-got, +want):\n%s", diff)
}
// Undo should remove the array
bf.Undo()
expectedAfter := proto.ColArr[uint32]{
Offsets: proto.ColUInt64{},
Data: &proto.ColUInt32{},
}
if diff := helpers.Diff(*col, expectedAfter); diff != "" {
t.Errorf("DstASPath column after undo (-got, +want):\n%s", diff)
}
})
t.Run("two values", func(t *testing.T) {
bf := c.NewFlowMessage()
bf.AppendArrayUInt32(ColumnDstASPath, []uint32{65001, 65002, 65003})
bf.Finalize()
bf.AppendArrayUInt32(ColumnDstASPath, []uint32{65007, 65008})
// Check we have the expected initial state
col := bf.batch.columns[ColumnDstASPath].(*proto.ColArr[uint32])
expected := proto.ColArr[uint32]{
Offsets: proto.ColUInt64{3, 5},
Data: &proto.ColUInt32{65001, 65002, 65003, 65007, 65008},
}
if diff := helpers.Diff(*col, expected); diff != "" {
t.Errorf("Initial DstASPath column state (-got, +want):\n%s", diff)
}
// Undo should remove the last array
bf.Undo()
expectedAfter := proto.ColArr[uint32]{
Offsets: proto.ColUInt64{3},
Data: &proto.ColUInt32{65001, 65002, 65003},
}
if diff := helpers.Diff(*col, expectedAfter); diff != "" {
t.Errorf("DstASPath column after undo (-got, +want):\n%s", diff)
}
})
}
func TestUndoArrayUInt128(t *testing.T) {
c := NewMock(t)
t.Run("one value", func(t *testing.T) {
bf := c.NewFlowMessage()
// Add Array(UInt128) value
bf.AppendArrayUInt128(ColumnDstLargeCommunities, []UInt128{
{High: (65401 << 32) + 100, Low: 200},
{High: (65401 << 32) + 100, Low: 201},
})
// Check we have the expected initial state
col := bf.batch.columns[ColumnDstLargeCommunities].(*proto.ColArr[proto.UInt128])
expected := proto.ColArr[proto.UInt128]{
Offsets: proto.ColUInt64{2},
Data: &proto.ColUInt128{
{High: (65401 << 32) + 100, Low: 200},
{High: (65401 << 32) + 100, Low: 201},
},
}
if diff := helpers.Diff(*col, expected); diff != "" {
t.Errorf("Initial DstLargeCommunities column state (-got, +want):\n%s", diff)
}
// Undo should remove the array
bf.Undo()
expectedAfter := proto.ColArr[proto.UInt128]{
Offsets: proto.ColUInt64{},
Data: &proto.ColUInt128{},
}
if diff := helpers.Diff(*col, expectedAfter); diff != "" {
t.Errorf("DstLargeCommunities column after undo (-got, +want):\n%s", diff)
}
})
t.Run("two values", func(t *testing.T) {
bf := c.NewFlowMessage()
// Add first Array(UInt128) value
bf.AppendArrayUInt128(ColumnDstLargeCommunities, []UInt128{
{High: (65401 << 32) + 100, Low: 200},
{High: (65401 << 32) + 100, Low: 201},
})
bf.Finalize()
// Add second Array(UInt128) value
bf.AppendArrayUInt128(ColumnDstLargeCommunities, []UInt128{
{High: (65402 << 32) + 100, Low: 300},
})
// Check we have the expected initial state
col := bf.batch.columns[ColumnDstLargeCommunities].(*proto.ColArr[proto.UInt128])
expected := proto.ColArr[proto.UInt128]{
Offsets: proto.ColUInt64{2, 3},
Data: &proto.ColUInt128{
{High: (65401 << 32) + 100, Low: 200},
{High: (65401 << 32) + 100, Low: 201},
{High: (65402 << 32) + 100, Low: 300},
},
}
if diff := helpers.Diff(*col, expected); diff != "" {
t.Errorf("Initial DstLargeCommunities column state (-got, +want):\n%s", diff)
}
// Undo should remove the last array
bf.Undo()
expectedAfter := proto.ColArr[proto.UInt128]{
Offsets: proto.ColUInt64{2},
Data: &proto.ColUInt128{
{High: (65401 << 32) + 100, Low: 200},
{High: (65401 << 32) + 100, Low: 201},
},
}
if diff := helpers.Diff(*col, expectedAfter); diff != "" {
t.Errorf("DstLargeCommunities column after undo (-got, +want):\n%s", diff)
}
})
}
func TestBuildProtoInput(t *testing.T) {
// Use a smaller version
exporterAddress := netip.MustParseAddr("::ffff:203.0.113.14")
c := NewMock(t)
bf := c.NewFlowMessage()
got := bf.ClickHouseProtoInput()
bf.TimeReceived = 1000
bf.SamplingRate = 20000
bf.ExporterAddress = exporterAddress
bf.AppendUint(ColumnDstAS, 65000)
bf.AppendUint(ColumnBytes, 200)
bf.AppendUint(ColumnPackets, 300)
bf.Finalize()
bf.Clear()
bf.TimeReceived = 1002
bf.ExporterAddress = exporterAddress
bf.AppendUint(ColumnSrcAS, 65000)
bf.AppendUint(ColumnBytes, 2000)
bf.AppendUint(ColumnPackets, 30)
bf.AppendUint(ColumnBytes, 300) // Duplicate!
bf.Finalize()
bf.TimeReceived = 1003
bf.ExporterAddress = exporterAddress
bf.AppendUint(ColumnSrcAS, 65001)
bf.AppendUint(ColumnBytes, 202)
bf.AppendUint(ColumnPackets, 3)
bf.Finalize()
// Let's compare a subset
expected := proto.Input{
{Name: "TimeReceived", Data: &proto.ColDateTime{Data: []proto.DateTime{1002, 1003}}},
{Name: "SrcAS", Data: &proto.ColUInt32{65000, 65001}},
{Name: "DstAS", Data: &proto.ColUInt32{0, 0}},
{Name: "Bytes", Data: &proto.ColUInt64{2000, 202}},
{Name: "Packets", Data: &proto.ColUInt64{30, 3}},
}
got = slices.DeleteFunc(got, func(col proto.InputColumn) bool {
return !slices.Contains([]string{"TimeReceived", "SrcAS", "DstAS", "Packets", "Bytes"}, col.Name)
})
if diff := helpers.Diff(got, expected); diff != "" {
t.Fatalf("ClickHouseProtoInput() (-got, +want):\n%s", diff)
}
}