Commit 2e427ec7 authored by Giuseppe Ottaviano's avatar Giuseppe Ottaviano Committed by Facebook Github Bot

Add test utilities to compare JSON documents

Summary:
Sometimes unit tests compare the JSON output of a
component. We want a simple way to compare them semantically and also
to ignore numerical errors for double values.

Reviewed By: yfeldblum

Differential Revision: D8505209

fbshipit-source-id: 04da89889f08a595e8296716f71b9a22156e3506
parent 36602b54
/*
* Copyright 2018-present Facebook, Inc.
*
* 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 <folly/test/JsonTestUtil.h>
#include <algorithm>
#include <cmath>
#include <folly/Conv.h>
#include <folly/json.h>
#include <folly/lang/Assume.h>
namespace folly {
bool compareJson(StringPiece json1, StringPiece json2) {
auto obj1 = parseJson(json1);
auto obj2 = parseJson(json2);
return obj1 == obj2;
}
namespace {
bool isClose(double x, double y, double tolerance) {
return std::abs(x - y) <= tolerance;
}
} // namespace
bool compareDynamicWithTolerance(
const dynamic& obj1,
const dynamic& obj2,
double tolerance) {
if (obj1.type() != obj2.type()) {
if (obj1.isNumber() && obj2.isNumber()) {
const auto& integ = obj1.isInt() ? obj1 : obj2;
const auto& doubl = obj1.isInt() ? obj2 : obj1;
// Use to<double> to fail on precision loss for very large
// integers (in which case the comparison does not make sense).
return isClose(to<double>(integ.asInt()), doubl.asDouble(), tolerance);
}
return false;
}
switch (obj1.type()) {
case dynamic::Type::NULLT:
return true;
case dynamic::Type::ARRAY:
if (obj1.size() != obj2.size()) {
return false;
}
for (auto i1 = obj1.begin(), i2 = obj2.begin(); i1 != obj1.end();
++i1, ++i2) {
if (!compareDynamicWithTolerance(*i1, *i2, tolerance)) {
return false;
}
}
return true;
case dynamic::Type::BOOL:
return obj1.asBool() == obj2.asBool();
case dynamic::Type::DOUBLE:
return isClose(obj1.asDouble(), obj2.asDouble(), tolerance);
case dynamic::Type::INT64:
return obj1.asInt() == obj2.asInt();
case dynamic::Type::OBJECT:
if (obj1.size() != obj2.size()) {
return false;
}
return std::all_of(
obj1.items().begin(), obj1.items().end(), [&](const auto& item) {
const auto& value1 = item.second;
const auto value2 = obj2.get_ptr(item.first);
return value2 &&
compareDynamicWithTolerance(value1, *value2, tolerance);
});
case dynamic::Type::STRING:
return obj1.asString() == obj2.asString();
}
assume_unreachable();
}
bool compareJsonWithTolerance(
StringPiece json1,
StringPiece json2,
double tolerance) {
auto obj1 = parseJson(json1);
auto obj2 = parseJson(json2);
return compareDynamicWithTolerance(obj1, obj2, tolerance);
}
} // namespace folly
/*
* Copyright 2018-present Facebook, Inc.
*
* 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.
*/
#pragma once
#include <folly/Range.h>
#include <folly/dynamic.h>
namespace folly {
/**
* Compares two JSON strings and returns whether they represent the
* same document (thus ignoring things like object ordering or
* multiple representations of the same number).
*
* This is implemented by deserializing both strings into dynamic, so
* it is not efficient and it is meant to only be used in tests.
*
* It will throw an exception if any of the inputs is invalid.
*/
bool compareJson(StringPiece json1, StringPiece json2);
/**
* Like compareJson, but allows for the given tolerance when comparing
* numbers.
*
* Note that in the dynamic flavor of JSON 64-bit integers are a
* supported type. If the values to be compared are both integers,
* tolerance is not applied (it may not be possible to represent them
* as double without loss of precision).
*
* When comparing objects exact key match is required, including if
* keys are doubles (again a dynamic extension).
*/
bool compareJsonWithTolerance(
StringPiece json1,
StringPiece json2,
double tolerance);
/**
* Like compareJsonWithTolerance, but operates directly on the
* dynamics.
*/
bool compareDynamicWithTolerance(
const dynamic& obj1,
const dynamic& obj2,
double tolerance);
} // namespace folly
/*
* Copyright 2018-present Facebook, Inc.
*
* 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 <stdexcept>
#include <folly/portability/GTest.h>
#include <folly/test/JsonTestUtil.h>
using namespace folly;
TEST(CompareJson, Simple) {
constexpr StringPiece json1 = R"({"a": 1, "b": 2})";
constexpr StringPiece json2 = R"({"b": 2, "a": 1})";
EXPECT_TRUE(compareJson(json1, json2));
constexpr StringPiece json3 = R"({"b": 3, "a": 1})";
EXPECT_FALSE(compareJson(json1, json3));
}
TEST(CompareJson, Malformed) {
constexpr StringPiece json1 = R"({"a": 1, "b": 2})";
constexpr StringPiece json2 = R"({"b": 2, "a": 1)";
EXPECT_THROW(compareJson(json1, json2), std::runtime_error);
}
TEST(CompareJsonWithTolerance, Simple) {
// Use the same tolerance for all tests.
auto compare = [](StringPiece obj1, StringPiece obj2) {
return compareJsonWithTolerance(obj1, obj2, 0.1);
};
EXPECT_TRUE(compare("1", "1.05"));
EXPECT_FALSE(compare("1", "1.2"));
EXPECT_TRUE(compare("true", "true"));
EXPECT_FALSE(compare("true", "false"));
EXPECT_FALSE(compare("true", "1"));
EXPECT_TRUE(compare(R"([1, 2, 3])", R"([1.05, 2, 3.01])"));
EXPECT_FALSE(compare(R"([1, 2, 3])", R"([1.2, 2, 3.01])"));
EXPECT_FALSE(compare(R"([1, 2, 3])", R"([1, 2])"));
EXPECT_TRUE(compare("1.0", "1.05"));
EXPECT_FALSE(compare("1.0", "1.2"));
EXPECT_TRUE(compare("1", "1"));
EXPECT_FALSE(compare("1", "2"));
EXPECT_TRUE(compare(R"({"a": 1, "b": 2})", R"({"b": 2.01, "a": 1.05})"));
EXPECT_FALSE(compare(R"({"a": 1, "b": 2})", R"({"b": 2.01, "a": 1.2})"));
EXPECT_FALSE(compare(R"({"a": 1, "b": 2})", R"({"b": 2})"));
EXPECT_FALSE(compare(R"({"a": 1, "b": 2})", R"({"c": 2.01, "a": 1.05})"));
EXPECT_TRUE(compare(R"("hello")", R"("hello")"));
EXPECT_FALSE(compare(R"("hello")", R"("world")"));
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment