Kinetica   C#   API  Version 7.2.3.1
AvroWireFormatTests.cs
Go to the documentation of this file.
1 using System;
2 using System.Linq;
3 using Xunit;
4 
6 {
16  [Trait("Category", "WireFormat")]
17  public class AvroWireFormatTests
18  {
19  #region Varint Encoding Tests
20 
26  [Theory]
27  [InlineData(0, new byte[] { 0x00 })] // 0 -> zig-zag: 0 -> 0x00
28  [InlineData(1, new byte[] { 0x02 })] // 1 -> zig-zag: 2 -> 0x02
29  [InlineData(-1, new byte[] { 0x01 })] // -1 -> zig-zag: 1 -> 0x01
30  [InlineData(2, new byte[] { 0x04 })] // 2 -> zig-zag: 4 -> 0x04
31  [InlineData(-2, new byte[] { 0x03 })] // -2 -> zig-zag: 3 -> 0x03
32  [InlineData(63, new byte[] { 0x7E })] // 63 -> zig-zag: 126 -> 0x7E
33  [InlineData(64, new byte[] { 0x80, 0x01 })] // 64 -> zig-zag: 128 -> needs 2 bytes
34  [InlineData(-64, new byte[] { 0x7F })] // -64 -> zig-zag: 127 -> 0x7F
35  [InlineData(127, new byte[] { 0xFE, 0x01 })] // 127 -> zig-zag: 254 -> 0xFE,0x01
36  [InlineData(-128, new byte[] { 0xFF, 0x01 })] // -128 -> zig-zag: 255 -> 0xFF,0x01
37  [InlineData(8191, new byte[] { 0xFE, 0x7F })] // 8191 -> needs 2 bytes
38  [InlineData(8192, new byte[] { 0x80, 0x80, 0x01 })] // 8192 -> needs 3 bytes
39  [InlineData(int.MaxValue, new byte[] { 0xFE, 0xFF, 0xFF, 0xFF, 0x0F })] // Max int
40  [InlineData(int.MinValue, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F })] // Min int
41  public void AvroVarint_EncodesCorrectly(int value, byte[] expectedBytes)
42  {
43  // Arrange
44  byte[] buffer = new byte[10];
45  int position = 0;
46 
47  // Act
48  position = kinetica.AvroEncoding.WriteVarInt(buffer, position, value);
49 
50  // Assert
51  byte[] actualBytes = buffer.Take(position).ToArray();
52  Assert.Equal(expectedBytes, actualBytes);
53  }
54 
58  [Theory]
59  [InlineData(0L, new byte[] { 0x00 })]
60  [InlineData(1L, new byte[] { 0x02 })]
61  [InlineData(-1L, new byte[] { 0x01 })]
62  [InlineData(long.MaxValue, new byte[] { 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01 })]
63  [InlineData(long.MinValue, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01 })]
64  public void AvroVarintLong_EncodesCorrectly(long value, byte[] expectedBytes)
65  {
66  // Arrange
67  byte[] buffer = new byte[10];
68  int position = 0;
69 
70  // Act
71  position = kinetica.AvroEncoding.WriteVarLong(buffer, position, value);
72 
73  // Assert
74  byte[] actualBytes = buffer.Take(position).ToArray();
75  Assert.Equal(expectedBytes, actualBytes);
76  }
77 
78  #endregion
79 
80  #region Simple Type Encoding Tests
81 
85  public class SimpleRecord
86  {
87  public int IntField { get; set; }
88  public long LongField { get; set; }
89  public float FloatField { get; set; }
90  public double DoubleField { get; set; }
91  public bool BoolField { get; set; }
92  public string StringField { get; set; } = "";
93  }
94 
99  [Fact]
101  {
102  // Arrange: Create a record with known values
103  var record = new SimpleRecord
104  {
105  IntField = 42, // zig-zag: 84 -> 0x54
106  LongField = 1000, // zig-zag: 2000 -> 0xD0, 0x0F
107  FloatField = 3.14f, // IEEE 754 single precision LE
108  DoubleField = 2.718, // IEEE 754 double precision LE
109  BoolField = true, // 0x01
110  StringField = "test" // len=4: 0x08 + "test"
111  };
112 
113  // Expected bytes:
114  // Int: 42 -> 0x54
115  // Long: 1000 -> 0xD0, 0x0F
116  // Float: 3.14f -> 0xC3, 0xF5, 0x48, 0x40 (little-endian)
117  // Double: 2.718 -> 0x58, 0x39, 0xB4, 0xC8, 0x76, 0xBE, 0x05, 0x40 (little-endian)
118  // Bool: true -> 0x01
119  // String: "test" -> length 4 -> 0x08, then UTF-8 bytes 0x74, 0x65, 0x73, 0x74
120  byte[] expectedBytes =
121  {
122  0x54, // int: 42
123  0xD0, 0x0F, // long: 1000
124  0xC3, 0xF5, 0x48, 0x40, // float: 3.14
125  0x58, 0x39, 0xB4, 0xC8, 0x76, 0xBE, 0x05, 0x40, // double: 2.718
126  0x01, // bool: true
127  0x08, 0x74, 0x65, 0x73, 0x74 // string: "test"
128  };
129 
130  // Create KineticaType for the record
131  var ktype = CreateSimpleRecordType();
132 
133  // Act
134  var encoder = kinetica.DirectAvroEncoder<SimpleRecord>.GetOrCreate(ktype);
135  byte[] actualBytes = encoder.Encode(record);
136 
137  // Assert
138  Assert.Equal(expectedBytes, actualBytes);
139  }
140 
144  [Fact]
146  {
147  // Arrange
148  var record = new SimpleRecord
149  {
150  IntField = 0,
151  LongField = 0,
152  FloatField = 0.0f,
153  DoubleField = 0.0,
154  BoolField = false,
155  StringField = ""
156  };
157 
158  // Expected bytes:
159  // Int: 0 -> 0x00
160  // Long: 0 -> 0x00
161  // Float: 0.0f -> 0x00, 0x00, 0x00, 0x00
162  // Double: 0.0 -> 0x00 * 8
163  // Bool: false -> 0x00
164  // String: "" -> length 0 -> 0x00
165  byte[] expectedBytes =
166  {
167  0x00, // int: 0
168  0x00, // long: 0
169  0x00, 0x00, 0x00, 0x00, // float: 0.0
170  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // double: 0.0
171  0x00, // bool: false
172  0x00 // string: ""
173  };
174 
175  var ktype = CreateSimpleRecordType();
176 
177  // Act
178  var encoder = kinetica.DirectAvroEncoder<SimpleRecord>.GetOrCreate(ktype);
179  byte[] actualBytes = encoder.Encode(record);
180 
181  // Assert
182  Assert.Equal(expectedBytes, actualBytes);
183  }
184 
188  [Fact]
190  {
191  // Arrange
192  var record = new SimpleRecord
193  {
194  IntField = -42, // zig-zag: 83 -> 0x53
195  LongField = -1000, // zig-zag: 1999 -> 0xCF, 0x0F
196  FloatField = -3.14f,
197  DoubleField = -2.718,
198  BoolField = false,
199  StringField = ""
200  };
201 
202  // Expected bytes for negative values
203  byte[] expectedBytes =
204  {
205  0x53, // int: -42
206  0xCF, 0x0F, // long: -1000
207  0xC3, 0xF5, 0x48, 0xC0, // float: -3.14
208  0x58, 0x39, 0xB4, 0xC8, 0x76, 0xBE, 0x05, 0xC0, // double: -2.718
209  0x00, // bool: false
210  0x00 // string: ""
211  };
212 
213  var ktype = CreateSimpleRecordType();
214 
215  // Act
216  var encoder = kinetica.DirectAvroEncoder<SimpleRecord>.GetOrCreate(ktype);
217  byte[] actualBytes = encoder.Encode(record);
218 
219  // Assert
220  Assert.Equal(expectedBytes, actualBytes);
221  }
222 
226  [Fact]
228  {
229  // Arrange
230  var record = new SimpleRecord
231  {
232  StringField = "Hello™" // ™ is UTF-8: 0xE2 0x84 0xA2 (3 bytes)
233  };
234 
235  // "Hello" = 5 bytes + "™" = 3 bytes = 8 bytes total
236  // Length prefix: 8 -> zig-zag: 16 -> 0x10
237  byte[] expectedStringBytes =
238  {
239  0x10, // length: 8
240  0x48, 0x65, 0x6C, 0x6C, 0x6F, // "Hello"
241  0xE2, 0x84, 0xA2 // "™"
242  };
243 
244  // Full record with defaults for other fields
245  byte[] expectedBytes =
246  {
247  0x00, // int: 0
248  0x00, // long: 0
249  0x00, 0x00, 0x00, 0x00, // float: 0.0
250  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // double: 0.0
251  0x00, // bool: false
252  0x10, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0xE2, 0x84, 0xA2 // "Hello™"
253  };
254 
255  var ktype = CreateSimpleRecordType();
256 
257  // Act
258  var encoder = kinetica.DirectAvroEncoder<SimpleRecord>.GetOrCreate(ktype);
259  byte[] actualBytes = encoder.Encode(record);
260 
261  // Assert
262  Assert.Equal(expectedBytes, actualBytes);
263  }
264 
265  #endregion
266 
267  #region Nullable Type Encoding Tests
268 
272  public class NullableRecord
273  {
274  public int? NullableInt { get; set; }
275  public long? NullableLong { get; set; }
276  public string? NullableString { get; set; }
277  }
278 
283  [Fact]
285  {
286  // Arrange
287  var record = new NullableRecord
288  {
289  NullableInt = null,
290  NullableLong = null,
291  NullableString = null
292  };
293 
294  // Expected: Each null field is encoded as union index 0 (null branch)
295  // Union index is encoded as varint: 0 -> 0x00
296  byte[] expectedBytes =
297  {
298  0x00, // NullableInt: union index 0 (null)
299  0x00, // NullableLong: union index 0 (null)
300  0x00 // NullableString: union index 0 (null)
301  };
302 
303  var ktype = CreateNullableRecordType();
304 
305  // Act
306  var encoder = kinetica.DirectAvroEncoder<NullableRecord>.GetOrCreate(ktype);
307  byte[] actualBytes = encoder.Encode(record);
308 
309  // Assert
310  Assert.Equal(expectedBytes, actualBytes);
311  }
312 
316  [Fact]
318  {
319  // Arrange
320  var record = new NullableRecord
321  {
322  NullableInt = 42,
323  NullableLong = 1000,
324  NullableString = "test"
325  };
326 
327  // Expected: Each non-null field is union index 1 (non-null branch) followed by the value
328  // Union index 1 -> 0x02 (varint encoded)
329  byte[] expectedBytes =
330  {
331  0x02, 0x54, // NullableInt: union index 1, value 42
332  0x02, 0xD0, 0x0F, // NullableLong: union index 1, value 1000
333  0x02, 0x08, 0x74, 0x65, 0x73, 0x74 // NullableString: union index 1, "test"
334  };
335 
336  var ktype = CreateNullableRecordType();
337 
338  // Act
339  var encoder = kinetica.DirectAvroEncoder<NullableRecord>.GetOrCreate(ktype);
340  byte[] actualBytes = encoder.Encode(record);
341 
342  // Assert
343  Assert.Equal(expectedBytes, actualBytes);
344  }
345 
346  #endregion
347 
348  #region Helper Methods
349 
353  private kinetica.KineticaType CreateSimpleRecordType()
354  {
355  string schemaJson = @"{
356  ""type"": ""record"",
357  ""name"": ""SimpleRecord"",
358  ""fields"": [
359  {""name"": ""IntField"", ""type"": ""int""},
360  {""name"": ""LongField"", ""type"": ""long""},
361  {""name"": ""FloatField"", ""type"": ""float""},
362  {""name"": ""DoubleField"", ""type"": ""double""},
363  {""name"": ""BoolField"", ""type"": ""boolean""},
364  {""name"": ""StringField"", ""type"": ""string""}
365  ]
366  }";
367 
368  return new kinetica.KineticaType(schemaJson);
369  }
370 
374  private kinetica.KineticaType CreateNullableRecordType()
375  {
376  string schemaJson = @"{
377  ""type"": ""record"",
378  ""name"": ""NullableRecord"",
379  ""fields"": [
380  {""name"": ""NullableInt"", ""type"": [""null"", ""int""]},
381  {""name"": ""NullableLong"", ""type"": [""null"", ""long""]},
382  {""name"": ""NullableString"", ""type"": [""null"", ""string""]}
383  ]
384  }";
385 
386  return new kinetica.KineticaType(schemaJson);
387  }
388 
389  #endregion
390 
391  #region Regression Tests
392 
396  [Theory]
397  [InlineData(16383, new byte[] { 0xFE, 0xFF, 0x01 })] // 3 bytes
398  [InlineData(2097151, new byte[] { 0xFE, 0xFF, 0xFF, 0x01 })] // 4 bytes (0x1FFFFF)
399  [InlineData(2097152, new byte[] { 0x80, 0x80, 0x80, 0x02 })] // 4 bytes (0x200000)
400  public void LargeVarint_EncodesWithMultipleBytes(int value, byte[] expectedBytes)
401  {
402  // Arrange
403  byte[] buffer = new byte[10];
404 
405  // Act
406  int position = kinetica.AvroEncoding.WriteVarInt(buffer, 0, value);
407 
408  // Assert
409  byte[] actualBytes = buffer.Take(position).ToArray();
410  Assert.Equal(expectedBytes, actualBytes);
411  }
412 
416  [Fact]
418  {
419  // Arrange
420  var record = new SimpleRecord { StringField = "" };
421  var ktype = CreateSimpleRecordType();
422 
423  // Act
424  var encoder = kinetica.DirectAvroEncoder<SimpleRecord>.GetOrCreate(ktype);
425  byte[] encoded = encoder.Encode(record);
426 
427  // Assert: Last byte should be 0x00 (empty string length)
428  Assert.Equal(0x00, encoded[^1]);
429  }
430 
434  [Fact]
436  {
437  // Arrange: String of 1000 'A' characters
438  var longString = new string('A', 1000);
439  var record = new SimpleRecord { StringField = longString };
440  var ktype = CreateSimpleRecordType();
441 
442  // Act
443  var encoder = kinetica.DirectAvroEncoder<SimpleRecord>.GetOrCreate(ktype);
444  byte[] encoded = encoder.Encode(record);
445 
446  // Assert: Check that length is encoded correctly
447  // 1000 -> zig-zag: 2000 -> 0xD0, 0x0F
448  // Find the string part (after all fixed fields)
449  // int(0)=1 byte + long(0)=1 byte + float(0.0)=4 bytes + double(0.0)=8 bytes + bool(false)=1 byte = 15 bytes
450  int stringStart = 15;
451  Assert.Equal(0xD0, encoded[stringStart]);
452  Assert.Equal(0x0F, encoded[stringStart + 1]);
453 
454  // Verify total length: stringStart + 2 (length varint) + 1000 (content)
455  Assert.Equal(stringStart + 2 + 1000, encoded.Length);
456  }
457 
458  #endregion
459  }
460 }
void NullableRecord_WithValues_EncodesCorrectly()
Tests encoding of nullable fields with non-null values.
void String_WithUTF8_EncodesCorrectly()
Tests string encoding with UTF-8 multi-byte characters.
void AvroVarint_EncodesCorrectly(int value, byte[] expectedBytes)
Tests Avro varint zig-zag encoding for positive integers.
void LargeVarint_EncodesWithMultipleBytes(int value, byte[] expectedBytes)
Regression test: Large varint values (ensures multi-byte encoding).
void SimpleRecord_WithZeroValues_EncodesCorrectly()
Tests encoding of a record with zero/empty values.
Wire-format pinning tests for Avro encoding.
void SimpleRecord_WithNegativeValues_EncodesCorrectly()
Tests encoding of negative integers.
void AvroVarintLong_EncodesCorrectly(long value, byte[] expectedBytes)
Tests Avro varint encoding for long values.
void EmptyString_EncodesAsZeroLength()
Regression test: Empty string encoding.
void LongString_EncodesCorrectly()
Regression test: Very long string (tests buffer resizing).
void SimpleRecord_WithBasicValues_EncodesCorrectly()
Tests encoding of a simple record with all basic types.
void NullableRecord_WithNulls_EncodesCorrectly()
Tests encoding of nullable fields with null values.