blob: 95cdc67cdab0aeef2ce18aa0c99bc2952c2b5dc5 [file] [log] [blame]
// Licensed 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
//
// http://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
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <algorithm>
#include <string>
#include <stout/gtest.hpp>
#include <stout/json.hpp>
#include <stout/jsonify.hpp>
#include <stout/protobuf.hpp>
#include <stout/stringify.hpp>
#include <stout/strings.hpp>
#include <stout/uuid.hpp>
#include "protobuf_tests.pb.h"
using std::string;
using google::protobuf::RepeatedPtrField;
namespace tests {
// Trivial equality operators to enable gtest macros.
bool operator==(const SimpleMessage& left, const SimpleMessage& right)
{
if (left.id() != right.id() ||
left.numbers().size() != right.numbers().size()) {
return false;
}
return std::equal(
left.numbers().begin(), left.numbers().end(), right.numbers().begin());
}
bool operator!=(const SimpleMessage& left, const SimpleMessage& right)
{
return !(left == right);
}
} // namespace tests {
TEST(ProtobufTest, JSON)
{
tests::Message message;
message.set_b(true);
message.set_str("string");
message.set_bytes("bytes");
message.set_int32(-1);
message.set_int64(-1);
message.set_uint32(1);
message.set_uint64(1);
message.set_sint32(-1);
message.set_sint64(-1);
message.set_f(1.0);
message.set_d(1.0);
message.set_e(tests::ONE);
message.mutable_nested()->set_str("nested");
message.add_repeated_bool(true);
message.add_repeated_string("repeated_string");
message.add_repeated_bytes("repeated_bytes");
message.add_repeated_int32(-2);
message.add_repeated_int64(-2);
message.add_repeated_uint32(2);
message.add_repeated_uint64(2);
message.add_repeated_sint32(-2);
message.add_repeated_sint64(-2);
message.add_repeated_float(1.0);
message.add_repeated_double(1.0);
message.add_repeated_double(2.0);
message.add_repeated_enum(tests::TWO);
message.add_repeated_nested()->set_str("repeated_nested");
// TODO(bmahler): To dynamically generate a protobuf message,
// see the commented-out code below.
//
// DescriptorProto proto;
//
// proto.set_name("Message");
//
// FieldDescriptorProto* field = proto.add_field();
// field->set_name("str");
// field->set_type(FieldDescriptorProto::TYPE_STRING);
//
// const Descriptor* descriptor = proto.descriptor();
//
// DynamicMessageFactory factory;
// Message* message = factory.GetPrototype(descriptor);
//
// Reflection* message.getReflection();
// The keys are in alphabetical order.
string expected =
R"~(
{
"b": true,
"bytes": "Ynl0ZXM=",
"d": 1.0,
"e": "ONE",
"f": 1.0,
"int32": -1,
"int64": -1,
"nested": { "str": "nested"},
"optional_default": 42.0,
"repeated_bool": [true],
"repeated_bytes": ["cmVwZWF0ZWRfYnl0ZXM="],
"repeated_double": [1.0, 2.0],
"repeated_enum": ["TWO"],
"repeated_float": [1.0],
"repeated_int32": [-2],
"repeated_int64": [-2],
"repeated_nested": [ { "str": "repeated_nested" } ],
"repeated_sint32": [-2],
"repeated_sint64": [-2],
"repeated_string": ["repeated_string"],
"repeated_uint32": [2],
"repeated_uint64": [2],
"sint32": -1,
"sint64": -1,
"str": "string",
"uint32": 1,
"uint64": 1
})~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
// The only difference between this JSON string and the above one is, all
// the bools and numbers are in the format of strings.
string accepted =
R"~(
{
"b": "true",
"bytes": "Ynl0ZXM=",
"d": "1.0",
"e": "ONE",
"f": "1.0",
"int32": "-1",
"int64": "-1",
"nested": { "str": "nested"},
"optional_default": "42.0",
"repeated_bool": ["true"],
"repeated_bytes": ["cmVwZWF0ZWRfYnl0ZXM="],
"repeated_double": ["1.0", "2.0"],
"repeated_enum": ["TWO"],
"repeated_float": ["1.0"],
"repeated_int32": ["-2"],
"repeated_int64": ["-2"],
"repeated_nested": [ { "str": "repeated_nested" } ],
"repeated_sint32": ["-2"],
"repeated_sint64": ["-2"],
"repeated_string": ["repeated_string"],
"repeated_uint32": ["2"],
"repeated_uint64": ["2"],
"sint32": "-1",
"sint64": "-1",
"str": "string",
"uint32": "1",
"uint64": "1"
})~";
JSON::Object object = JSON::protobuf(message);
EXPECT_EQ(expected, stringify(object));
// Test parsing too.
Try<tests::Message> parse = protobuf::parse<tests::Message>(object);
ASSERT_SOME(parse);
EXPECT_EQ(object, JSON::protobuf(parse.get()));
// Test all the bools and numbers in the JSON string `accepted` can be
// successfully parsed.
Try<JSON::Object> json = JSON::parse<JSON::Object>(accepted);
ASSERT_SOME(json);
parse = protobuf::parse<tests::Message>(json.get());
ASSERT_SOME(parse);
EXPECT_EQ(object, JSON::protobuf(parse.get()));
// Modify the message to test (de-)serialization of random bytes generated
// by UUID.
message.set_bytes(id::UUID::random().toBytes());
object = JSON::protobuf(message);
// Test parsing too.
parse = protobuf::parse<tests::Message>(object);
ASSERT_SOME(parse);
EXPECT_EQ(object, JSON::protobuf(parse.get()));
// Now convert JSON to string and parse it back as JSON.
ASSERT_SOME_EQ(object, JSON::parse(stringify(object)));
}
TEST(ProtobufTest, JSONArray)
{
tests::SimpleMessage message1;
message1.set_id("message1");
message1.add_numbers(1);
message1.add_numbers(2);
// Messages with different IDs are not equal.
tests::SimpleMessage message2;
message2.set_id("message2");
message2.add_numbers(1);
message2.add_numbers(2);
// The keys are in alphabetical order.
string expected =
R"~(
[
{
"id": "message1",
"numbers": [1, 2]
},
{
"id": "message2",
"numbers": [1, 2]
}
])~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
tests::ArrayMessage arrayMessage;
arrayMessage.add_values()->CopyFrom(message1);
arrayMessage.add_values()->CopyFrom(message2);
JSON::Array array = JSON::protobuf(arrayMessage.values());
EXPECT_EQ(expected, stringify(array));
}
// Tests that integer precision is maintained between
// JSON <-> Protobuf conversions.
TEST(ProtobufTest, JsonLargeIntegers)
{
// These numbers are equal or close to the integer limits.
tests::Message message;
message.set_int32(-2147483647);
message.set_int64(-9223372036854775807);
message.set_uint32(4294967295U);
message.set_uint64(9223372036854775807);
message.set_sint32(-1234567890);
message.set_sint64(-1234567890123456789);
message.add_repeated_int32(-2000000000);
message.add_repeated_int64(-9000000000000000000);
message.add_repeated_uint32(3000000000U);
message.add_repeated_uint64(7000000000000000000);
message.add_repeated_sint32(-1000000000);
message.add_repeated_sint64(-8000000000000000000);
// Parts of the protobuf that are required. Copied from the above test.
message.set_b(true);
message.set_str("string");
message.set_bytes("bytes");
message.set_f(1.0);
message.set_d(1.0);
message.set_e(tests::ONE);
message.mutable_nested()->set_str("nested");
// The keys are in alphabetical order.
string expected =
R"~(
{
"b": true,
"bytes": "Ynl0ZXM=",
"d": 1.0,
"e": "ONE",
"f": 1.0,
"int32": -2147483647,
"int64": -9223372036854775807,
"nested": {"str": "nested"},
"optional_default": 42.0,
"repeated_int32": [-2000000000],
"repeated_int64": [-9000000000000000000],
"repeated_sint32": [-1000000000],
"repeated_sint64": [-8000000000000000000],
"repeated_uint32": [3000000000],
"repeated_uint64": [7000000000000000000],
"sint32": -1234567890,
"sint64": -1234567890123456789,
"str": "string",
"uint32": 4294967295,
"uint64": 9223372036854775807
})~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
// Check JSON -> String.
JSON::Object object = JSON::protobuf(message);
EXPECT_EQ(expected, stringify(object));
// Check JSON -> Protobuf.
Try<tests::Message> parse = protobuf::parse<tests::Message>(object);
ASSERT_SOME(parse);
// Check Protobuf -> JSON.
EXPECT_EQ(object, JSON::protobuf(parse.get()));
// Check String -> JSON.
Try<JSON::Object> json = JSON::parse<JSON::Object>(expected);
EXPECT_SOME_EQ(object, json);
}
TEST(ProtobufTest, SimpleMessageEquals)
{
tests::SimpleMessage message1;
message1.set_id("message1");
message1.add_numbers(1);
message1.add_numbers(2);
// Obviously, a message should equal to itself.
EXPECT_EQ(message1, message1);
// Messages with different IDs are not equal.
tests::SimpleMessage message2;
message2.set_id("message2");
message2.add_numbers(1);
message2.add_numbers(2);
EXPECT_NE(message1, message2);
// Messages with not identical collection of numbers are not equal.
tests::SimpleMessage message3;
message3.set_id("message1");
message3.add_numbers(1);
EXPECT_NE(message1, message3);
tests::SimpleMessage message4;
message4.set_id("message1");
message4.add_numbers(2);
message4.add_numbers(1);
EXPECT_NE(message1, message4);
// Different messages with the same ID and collection of numbers should
// be equal. Their JSON counterparts should be equal as well.
tests::SimpleMessage message5;
message5.set_id("message1");
message5.add_numbers(1);
message5.add_numbers(2);
EXPECT_EQ(message1, message5);
EXPECT_EQ(JSON::protobuf(message1), JSON::protobuf(message5));
}
TEST(ProtobufTest, ParseJSONArray)
{
tests::SimpleMessage message;
message.set_id("message1");
message.add_numbers(1);
message.add_numbers(2);
// Convert protobuf message to a JSON object.
JSON::Object object = JSON::protobuf(message);
// Populate JSON array with JSON objects, conversion JSON::Object ->
// JSON::Value is implicit.
JSON::Array array;
array.values.push_back(object);
array.values.push_back(object);
// Parse JSON array into a collection of protobuf messages.
auto parse =
protobuf::parse<RepeatedPtrField<tests::SimpleMessage>>(array);
ASSERT_SOME(parse);
auto repeated = parse.get();
// Make sure the parsed message equals to the original one.
EXPECT_EQ(message, repeated.Get(0));
EXPECT_EQ(message, repeated.Get(1));
}
TEST(ProtobufTest, ParseJSONNull)
{
tests::Nested nested;
nested.set_str("value");
// Test message with optional field set to 'null'.
string message =
R"~(
{
"str": "value",
"optional_str": null
})~";
Try<JSON::Object> json = JSON::parse<JSON::Object>(message);
ASSERT_SOME(json);
Try<tests::Nested> parse = protobuf::parse<tests::Nested>(json.get());
ASSERT_SOME(parse);
EXPECT_EQ(parse->SerializeAsString(), nested.SerializeAsString());
// Test message with repeated field set to 'null'.
message =
R"~(
{
"str": "value",
"repeated_str": null
})~";
json = JSON::parse<JSON::Object>(message);
ASSERT_SOME(json);
parse = protobuf::parse<tests::Nested>(json.get());
ASSERT_SOME(parse);
EXPECT_EQ(parse->SerializeAsString(), nested.SerializeAsString());
// Test message with required field set to 'null'.
message =
R"~(
{
"str": null
})~";
json = JSON::parse<JSON::Object>(message);
ASSERT_SOME(json);
EXPECT_ERROR(protobuf::parse<tests::Nested>(json.get()));
}
TEST(ProtobufTest, ParseJSONNestedError)
{
// Here we trigger an error parsing the 'nested' message, i.e., set
// the string type field `nested.str` to a number.
string message =
R"~(
{
"b": true,
"str": "string",
"bytes": "Ynl0ZXM=",
"f": 1.0,
"d": 1.0,
"e": "ONE",
"nested": {
"str": 1.0
}
})~";
Try<JSON::Object> json = JSON::parse<JSON::Object>(message);
ASSERT_SOME(json);
Try<tests::Message> parse = protobuf::parse<tests::Message>(json.get());
ASSERT_ERROR(parse);
EXPECT_TRUE(strings::contains(
parse.error(), "Not expecting a JSON number for field"));
}
// Tests when parsing protobuf from JSON, for the optional enum field which
// has an unrecognized enum value, after the parsing the field will be unset
// and its getter will return the default enum value. For the repeated enum
// field which contains an unrecognized enum value, after the parsing the
// field will not contain that unrecognized value anymore.
TEST(ProtobufTest, ParseJSONUnrecognizedEnum)
{
string message =
R"~(
{
"e1": "XXX",
"e2": "",
"repeated_enum": ["ONE", "XXX", "", "TWO"]
})~";
Try<JSON::Object> json = JSON::parse<JSON::Object>(message);
ASSERT_SOME(json);
Try<tests::EnumMessage> parse =
protobuf::parse<tests::EnumMessage>(json.get());
ASSERT_SOME(parse);
EXPECT_FALSE(parse->has_e1());
EXPECT_EQ(tests::UNKNOWN, parse->e1());
EXPECT_FALSE(parse->has_e2());
EXPECT_EQ(tests::UNKNOWN, parse->e2());
EXPECT_EQ(2, parse->repeated_enum_size());
EXPECT_EQ(tests::ONE, parse->repeated_enum(0));
EXPECT_EQ(tests::TWO, parse->repeated_enum(1));
}
TEST(ProtobufTest, Jsonify)
{
tests::Message message;
message.set_b(true);
message.set_str("string");
message.set_bytes("bytes");
message.set_int32(-1);
message.set_int64(-1);
message.set_uint32(1);
message.set_uint64(1);
message.set_sint32(-1);
message.set_sint64(-1);
message.set_f(1.0);
message.set_d(1.0);
message.set_e(tests::ONE);
message.mutable_nested()->set_str("nested");
message.add_repeated_bool(true);
message.add_repeated_string("repeated_string");
message.add_repeated_bytes("repeated_bytes");
message.add_repeated_int32(-2);
message.add_repeated_int64(-2);
message.add_repeated_uint32(2);
message.add_repeated_uint64(2);
message.add_repeated_sint32(-2);
message.add_repeated_sint64(-2);
message.add_repeated_float(1.0);
message.add_repeated_double(1.0);
message.add_repeated_double(2.0);
message.add_repeated_enum(tests::TWO);
message.add_repeated_nested()->set_str("repeated_nested");
// TODO(bmahler): To dynamically generate a protobuf message,
// see the commented-out code below.
// DescriptorProto proto;
//
// proto.set_name("Message");
//
// FieldDescriptorProto* field = proto.add_field();
// field->set_name("str");
// field->set_type(FieldDescriptorProto::TYPE_STRING);
//
// const Descriptor* descriptor = proto.descriptor();
//
// DynamicMessageFactory factory;
// Message* message = factory.GetPrototype(descriptor);
//
// Reflection* message.getReflection();
// The keys are in alphabetical order.
string expected =
R"~(
{
"b": true,
"str": "string",
"bytes": "Ynl0ZXM=",
"int32": -1,
"int64": -1,
"uint32": 1,
"uint64": 1,
"sint32": -1,
"sint64": -1,
"f": 1.0,
"d": 1.0,
"e": "ONE",
"nested": { "str": "nested"},
"repeated_bool": [true],
"repeated_string": ["repeated_string"],
"repeated_bytes": ["cmVwZWF0ZWRfYnl0ZXM="],
"repeated_int32": [-2],
"repeated_int64": [-2],
"repeated_uint32": [2],
"repeated_uint64": [2],
"repeated_sint32": [-2],
"repeated_sint64": [-2],
"repeated_float": [1.0],
"repeated_double": [1.0, 2.0],
"repeated_enum": ["TWO"],
"repeated_nested": [ { "str": "repeated_nested" } ],
"optional_default": 42.0
})~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
EXPECT_EQ(expected, string(jsonify(JSON::Protobuf(message))));
}
TEST(ProtobufTest, JsonifyArray)
{
tests::SimpleMessage message1;
message1.set_id("message1");
message1.add_numbers(1);
message1.add_numbers(2);
// Messages with different IDs are not equal.
tests::SimpleMessage message2;
message2.set_id("message2");
message2.add_numbers(1);
message2.add_numbers(2);
// The keys are in alphabetical order.
string expected =
R"~(
[
{
"id": "message1",
"numbers": [1, 2]
},
{
"id": "message2",
"numbers": [1, 2]
}
])~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
tests::ArrayMessage arrayMessage;
arrayMessage.add_values()->CopyFrom(message1);
arrayMessage.add_values()->CopyFrom(message2);
string actual = jsonify([&arrayMessage](JSON::ArrayWriter* writer) {
foreach (const tests::SimpleMessage& message, arrayMessage.values()) {
writer->element(JSON::Protobuf(message));
}
});
EXPECT_EQ(expected, actual);
}
// Tests that integer precision is maintained between
// JSON <-> Protobuf conversions.
TEST(ProtobufTest, JsonifyLargeIntegers)
{
// These numbers are equal or close to the integer limits.
tests::Message message;
message.set_int32(-2147483647);
message.set_int64(-9223372036854775807);
message.set_uint32(4294967295U);
message.set_uint64(9223372036854775807);
message.set_sint32(-1234567890);
message.set_sint64(-1234567890123456789);
message.add_repeated_int32(-2000000000);
message.add_repeated_int64(-9000000000000000000);
message.add_repeated_uint32(3000000000U);
message.add_repeated_uint64(7000000000000000000);
message.add_repeated_sint32(-1000000000);
message.add_repeated_sint64(-8000000000000000000);
// Parts of the protobuf that are required. Copied from the above test.
message.set_b(true);
message.set_str("string");
message.set_bytes("bytes");
message.set_f(1.0);
message.set_d(1.0);
message.set_e(tests::ONE);
message.mutable_nested()->set_str("nested");
// The keys are in alphabetical order.
string expected =
R"~(
{
"b": true,
"str": "string",
"bytes": "Ynl0ZXM=",
"int32": -2147483647,
"int64": -9223372036854775807,
"uint32": 4294967295,
"uint64": 9223372036854775807,
"sint32": -1234567890,
"sint64": -1234567890123456789,
"f": 1.0,
"d": 1.0,
"e": "ONE",
"nested": {"str": "nested"},
"repeated_int32": [-2000000000],
"repeated_int64": [-9000000000000000000],
"repeated_uint32": [3000000000],
"repeated_uint64": [7000000000000000000],
"repeated_sint32": [-1000000000],
"repeated_sint64": [-8000000000000000000],
"optional_default": 42.0
})~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
// Check JSON -> String.
EXPECT_EQ(expected, string(jsonify(JSON::Protobuf(message))));
}
TEST(ProtobufTest, JsonifyMap)
{
tests::MapMessage message;
(*message.mutable_string_to_bool())["key1"] = true;
(*message.mutable_string_to_bool())["key2"] = false;
(*message.mutable_string_to_bytes())["key"] = "bytes";
(*message.mutable_string_to_double())["key"] = 1.0;
(*message.mutable_string_to_enum())["key"] = tests::ONE;
(*message.mutable_string_to_float())["key"] = 1.0;
(*message.mutable_string_to_string())["key1"] = "value1";
(*message.mutable_string_to_string())["key2"] = "value2";
tests::Nested nested;
nested.set_str("nested");
(*message.mutable_string_to_nested())["key"] = nested;
// These numbers are equal or close to the integer limits.
(*message.mutable_string_to_int32())["key"] = -2147483647;
(*message.mutable_string_to_int64())["key"] = -9223372036854775807;
(*message.mutable_string_to_sint32())["key"] = -1234567890;
(*message.mutable_string_to_sint64())["key"] = -1234567890123456789;
(*message.mutable_string_to_uint32())["key"] = 4294967295;
(*message.mutable_string_to_uint64())["key"] = 9223372036854775807;
(*message.mutable_bool_to_string())[true] = "value1";
(*message.mutable_bool_to_string())[false] = "value2";
// These numbers are equal or close to the integer limits.
(*message.mutable_int32_to_string())[-2147483647] = "value";
(*message.mutable_int64_to_string())[-9223372036854775807] = "value";
(*message.mutable_sint32_to_string())[-1234567890] = "value";
(*message.mutable_sint64_to_string())[-1234567890123456789] = "value";
(*message.mutable_uint32_to_string())[4294967295] = "value";
(*message.mutable_uint64_to_string())[9223372036854775807] = "value";
// The keys are in alphabetical order.
// The value of `string_to_bytes` is base64 encoded.
string expected =
R"~(
{
"bool_to_string": {
"false": "value2",
"true": "value1"
},
"int32_to_string": {
"-2147483647": "value"
},
"int64_to_string": {
"-9223372036854775807": "value"
},
"sint32_to_string": {
"-1234567890": "value"
},
"sint64_to_string": {
"-1234567890123456789": "value"
},
"string_to_bool": {
"key1": true,
"key2": false
},
"string_to_bytes": {
"key": "Ynl0ZXM="
},
"string_to_double": {
"key": 1.0
},
"string_to_enum": {
"key": "ONE"
},
"string_to_float": {
"key": 1.0
},
"string_to_int32": {
"key": -2147483647
},
"string_to_int64": {
"key": -9223372036854775807
},
"string_to_nested": {
"key": {
"str": "nested"
}
},
"string_to_sint32": {
"key": -1234567890
},
"string_to_sint64": {
"key": -1234567890123456789
},
"string_to_string": {
"key1": "value1",
"key2": "value2"
},
"string_to_uint32": {
"key": 4294967295
},
"string_to_uint64": {
"key": 9223372036854775807
},
"uint32_to_string": {
"4294967295": "value"
},
"uint64_to_string": {
"9223372036854775807": "value"
}
})~";
// Remove ' ' and '\n' from `expected` so that we can compare
// it with the JSON string parsed from protobuf message.
expected.erase(
std::remove_if(expected.begin(), expected.end(), ::isspace),
expected.end());
JSON::Object object = JSON::protobuf(message);
EXPECT_EQ(expected, stringify(object));
// Test parsing too.
Try<tests::MapMessage> parse = protobuf::parse<tests::MapMessage>(object);
ASSERT_SOME(parse);
EXPECT_EQ(object, JSON::protobuf(parse.get()));
}