blob: 8614cf6136538ca5b8acdb28df6c763c54ee364e [file]
<?php
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace Apache\Avro\Tests;
use Apache\Avro\AvroDebug;
use Apache\Avro\AvroException;
use Apache\Avro\Datum\AvroIOBinaryDecoder;
use Apache\Avro\Datum\AvroIOBinaryEncoder;
use Apache\Avro\Datum\AvroIODatumReader;
use Apache\Avro\Datum\AvroIODatumWriter;
use Apache\Avro\Datum\Type\AvroDuration;
use Apache\Avro\IO\AvroStringIO;
use Apache\Avro\Schema\AvroSchema;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class DatumIOTest extends TestCase
{
#[DataProvider('data_provider')]
public function test_datum_round_trip(string $schema_json, mixed $datum, string $binary): void
{
$this->assertIsValidDatumForSchema($schema_json, $datum, $binary);
}
public static function data_provider(): array
{
return [
['"null"', null, ''],
['"boolean"', true, "\001"],
['"boolean"', false, "\000"],
['"int"', (int) -2147483648, "\xFF\xFF\xFF\xFF\x0F"],
['"int"', -1, "\001"],
['"int"', 0, "\000"],
['"int"', 1, "\002"],
['"int"', 2147483647, "\xFE\xFF\xFF\xFF\x0F"],
['"long"', (int) -9223372036854775808, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"],
['"long"', -(1 << 62), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F"],
['"long"', -(1 << 61), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F"],
['"long"', -4294967295, "\xFD\xFF\xFF\xFF\x1F"],
['"long"', -1 << 24, "\xFF\xFF\xFF\x0F"],
['"long"', -1 << 16, "\xFF\xFF\x07"],
['"long"', -255, "\xFD\x03"],
['"long"', -128, "\xFF\x01"],
['"long"', -127, "\xFD\x01"],
['"long"', -10, "\x13"],
['"long"', -3, "\005"],
['"long"', -2, "\003"],
['"long"', -1, "\001"],
['"long"', 0, "\000"],
['"long"', 1, "\002"],
['"long"', 2, "\004"],
['"long"', 3, "\006"],
['"long"', 10, "\x14"],
['"long"', 127, "\xFE\x01"],
['"long"', 128, "\x80\x02"],
['"long"', 255, "\xFE\x03"],
['"long"', 1 << 16, "\x80\x80\x08"],
['"long"', 1 << 24, "\x80\x80\x80\x10"],
['"long"', 4294967295, "\xFE\xFF\xFF\xFF\x1F"],
['"long"', 1 << 61, "\x80\x80\x80\x80\x80\x80\x80\x80\x40"],
['"long"', 1 << 62, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01"],
['"long"', 9223372036854775807, "\xFE\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01"],
['"float"', (float) -10.0, "\000\000 \301"],
['"float"', (float) -1.0, "\000\000\200\277"],
['"float"', (float) 0.0, "\000\000\000\000"],
['"float"', (float) 2.0, "\000\000\000@"],
['"float"', (float) 9.0, "\000\000\020A"],
['"double"', (float) -10.0, "\000\000\000\000\000\000$\300"],
['"double"', (float) -1.0, "\000\000\000\000\000\000\360\277"],
['"double"', (float) 0.0, "\000\000\000\000\000\000\000\000"],
['"double"', (float) 2.0, "\000\000\000\000\000\000\000@"],
['"double"', (float) 9.0, "\000\000\000\000\000\000\"@"],
['"string"', 'foo', "\x06foo"],
['"bytes"', "\x01\x02\x03", "\x06\x01\x02\x03"],
[
'{"type":"array","items":"int"}',
[1, 2, 3],
"\x06\x02\x04\x06\x00",
],
[
'{"type":"map","values":"int"}',
['foo' => 1, 'bar' => 2, 'baz' => 3],
"\x06\x06foo\x02\x06bar\x04\x06baz\x06\x00",
],
['["null", "int"]', 1, "\x02\x02"],
[
'{"name":"fix","type":"fixed","size":3}',
"\xAA\xBB\xCC",
"\xAA\xBB\xCC",
],
[
'{"name":"enm","type":"enum","symbols":["A","B","C"]}',
'B',
"\x02",
],
[
'{"name":"rec","type":"record","fields":[{"name":"a","type":"int"},{"name":"b","type":"boolean"}]}',
['a' => 1, 'b' => false],
"\x02\x00",
],
];
}
public static function validBytesDecimalLogicalType(): array
{
return [
'positive' => [
'10.95',
4,
2,
"\x04\x04\x47",
],
'negative' => [
'-10.95',
4,
2,
"\x04\xFB\xB9",
],
'small positive' => [
'0.05',
3,
2,
"\x02\x05",
],
'zero value' => [
'0',
4,
2,
"\x02\x00",
],
'unscaled positive' => [
'12345',
6,
0,
"\x04\x30\x39",
],
'trimming edge case 127' => [
'127',
3,
0,
"\x02\x7F",
],
'trimming edge case 128' => [
'128',
3,
0,
"\x04\x00\x80",
],
'negative trimming -1' => [
'-1',
3,
0,
"\x02\xFF",
],
'negative trimming -129' => [
'-129',
3,
0,
"\x04\xFF\x7F",
],
'high precision positive number' => [
'549755813887',
12,
0,
"\x0A\x7F\xFF\xFF\xFF\xFF",
],
'high precision positive number with scale' => [
'54975581.3887',
12,
4,
"\x0A\x7F\xFF\xFF\xFF\xFF",
],
'high precision negative number' => [
'-549755813888',
12,
0,
"\x0A\x80\x00\x00\x00\x00",
],
'high precision negative number with scale' => [
'-54975581.3888',
12,
4,
"\x0A\x80\x00\x00\x00\x00",
],
];
}
#[DataProvider('validBytesDecimalLogicalType')]
public function test_valid_bytes_decimal_logical_type(string $datum, int $precision, int $scale, string $expected): void
{
$bytesSchemaJson = <<<JSON
{
"name": "number",
"type": "bytes",
"logicalType": "decimal",
"precision": {$precision},
"scale": {$scale}
}
JSON;
$this->assertIsValidDatumForSchema($bytesSchemaJson, $datum, $expected);
$fixedSchemaJson = <<<JSON
{
"name": "number",
"type": "fixed",
"size": 8,
"logicalType": "decimal",
"precision": {$precision},
"scale": {$scale}
}
JSON;
$this->assertIsValidDatumForSchema($fixedSchemaJson, $datum, $expected);
}
public function test_invalid_bytes_logical_type_out_of_range(): void
{
$schemaJson = <<<JSON
{
"name": "number",
"type": "bytes",
"logicalType": "decimal",
"precision": 4,
"scale": 0
}
JSON;
$schema = AvroSchema::parse($schemaJson);
$written = new AvroStringIO();
$encoder = new AvroIOBinaryEncoder($written);
$writer = new AvroIODatumWriter($schema);
$this->expectException(AvroException::class);
$this->expectExceptionMessage("Decimal value '10000' is out of range for precision=4, scale=0");
$writer->write("10000", $encoder);
}
public static function validDurationLogicalTypes(): array
{
return [
[new AvroDuration(1, 2, 3)],
[new AvroDuration(-1, -2, -3)],
[new AvroDuration(AvroSchema::INT_MAX_VALUE, AvroSchema::INT_MAX_VALUE, AvroSchema::INT_MIN_VALUE)],
];
}
#[DataProvider('validDurationLogicalTypes')]
public function test_duration_logical_type(AvroDuration $avroDuration): void
{
$bytesSchemaJson = <<<JSON
{
"name": "number",
"type": "fixed",
"size": 12,
"logicalType": "duration"
}
JSON;
$this->assertIsValidDatumForSchema(
$bytesSchemaJson,
$avroDuration,
$avroDuration
);
}
public static function durationLogicalTypeOutOfBounds(): array
{
return [
[AvroSchema::INT_MIN_VALUE - 1, 0, 0],
[0, AvroSchema::INT_MIN_VALUE - 1, 0],
[0, 0, AvroSchema::INT_MIN_VALUE - 1],
[AvroSchema::INT_MAX_VALUE + 1, 0, 0],
[0, AvroSchema::INT_MAX_VALUE + 1, 0],
[0, 0, AvroSchema::INT_MAX_VALUE + 1],
];
}
#[DataProvider('durationLogicalTypeOutOfBounds')]
public function test_duration_logical_type_out_of_bounds(int $months, int $days, int $milliseconds): void
{
$this->expectException(AvroException::class);
new AvroDuration($months, $days, $milliseconds);
}
public static function default_provider(): array
{
return [
[
'"null"',
'null',
null,
],
[
'"boolean"',
'true',
true,
],
[
'"int"',
'1',
1,
],
[
'"long"',
'2000',
2000,
],
[
'"float"',
'1.1',
1.1,
],
[
'"double"',
'200.2',
200.2,
],
[
'"string"',
'"quux"',
'quux',
],
[
'"bytes"',
'"\u00FF"',
"\xC3\xBF"],
[
'{"type":"array","items":"int"}',
'[5,4,3,2]',
[5, 4, 3, 2],
],
[
'{"type":"map","values":"int"}',
'{"a":9}',
['a' => 9],
],
[
'["int","string"]',
'8',
8,
],
[
'{"name":"x","type":"enum","symbols":["A","V"]}',
'"A"',
'A',
],
[
'{"name":"x","type":"fixed","size":4}',
'"\u00ff"',
"\xC3\xBF",
],
[
'{"name":"x","type":"record","fields":[{"name":"label","type":"int"}]}',
'{"label":7}',
['label' => 7],
],
'logical type' => [
'{"type":"string","logicalType":"uuid"}',
'"550e8400-e29b-41d4-a716-446655"',
'550e8400-e29b-41d4-a716-446655',
],
'logical type in a record' => [
'{"name":"x","type":"record","fields":[{"name":"price","type":"bytes","logicalType":"decimal","precision":4,"scale":2}]}',
'{"price": "\u0000\u0000"}',
['price' => "0"],
],
];
}
#[DataProvider('default_provider')]
public function test_field_default_value(
string $field_schema_json,
string $default_json,
mixed $default_value
): void {
$writers_schema_json = '{"name":"foo","type":"record","fields":[]}';
$writers_schema = AvroSchema::parse($writers_schema_json);
$readers_schema_json = sprintf(
'{"name":"foo","type":"record","fields":[{"name":"f","type":%s,"default":%s}]}',
$field_schema_json,
$default_json
);
$readers_schema = AvroSchema::parse($readers_schema_json);
$reader = new AvroIODatumReader($writers_schema, $readers_schema);
$record = $reader->read(new AvroIOBinaryDecoder(new AvroStringIO()));
if (array_key_exists('f', $record)) {
$this->assertEquals($default_value, $record['f']);
} else {
$this->fail(sprintf('expected field record[f]: %s', print_r($record, true)));
}
}
/**
* @param string $datum
* @param string $expected
* @throws \Apache\Avro\IO\AvroIOException
*/
private function assertIsValidDatumForSchema(string $schemaJson, $datum, $expected): void
{
$schema = AvroSchema::parse($schemaJson);
$written = new AvroStringIO();
$encoder = new AvroIOBinaryEncoder($written);
$writer = new AvroIODatumWriter($schema);
$writer->write($datum, $encoder);
$output = (string) $written;
$this->assertEquals(
$expected,
$output,
sprintf(
"expected: %s\n actual: %s",
AvroDebug::asciiString($expected, 'hex'),
AvroDebug::asciiString($output, 'hex')
)
);
$read = new AvroStringIO((string) $expected);
$decoder = new AvroIOBinaryDecoder($read);
$reader = new AvroIODatumReader($schema);
$read_datum = $reader->read($decoder);
$this->assertEquals($datum, $read_datum);
}
}