diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-03-19 14:46:58 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-03-20 11:07:24 +0100 |
commit | 88219ea163c159be7d16203be32d8f0b3d7e7d0a (patch) | |
tree | ef96d6c0860ebac6b53ff6f39d521a733c14c25b /sonar-testing-harness/src | |
parent | 1af9a73913f04992e8a9033b29980dd74c6f0a22 (diff) | |
download | sonarqube-88219ea163c159be7d16203be32d8f0b3d7e7d0a.tar.gz sonarqube-88219ea163c159be7d16203be32d8f0b3d7e7d0a.zip |
Replace lib org.skyscreamer.jsonassert by org.sonar.test.JsonAssert.
This class supports lenient comparison of datetime values.
Diffstat (limited to 'sonar-testing-harness/src')
6 files changed, 575 insertions, 0 deletions
diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/JsonAssert.java b/sonar-testing-harness/src/main/java/org/sonar/test/JsonAssert.java new file mode 100644 index 00000000000..ac9f2470da6 --- /dev/null +++ b/sonar-testing-harness/src/main/java/org/sonar/test/JsonAssert.java @@ -0,0 +1,100 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.test; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.apache.commons.io.Charsets; +import org.apache.commons.io.IOUtils; +import org.junit.ComparisonFailure; + +import java.io.IOException; +import java.net.URL; + +/** + * Assertion to compare JSON documents. Comparison is not strict: + * <ul> + * <li>formatting differences are ignored</li> + * <li>order of elements in objects <code>{}</code> is not verified</li> + * <li>objects can contain more elements than expected, for example <code>{"one":1, "two":2}</code> + * matches <code>{"one":1}</code></li> + * <li>order of elements in arrays <code>[]</code> is not verified by default, for example <code>[1, 2]</code> + * matches <code>[2, 1]</code>. This mode can be disabled with {@link #setStrictArrayOrder(boolean)}</li> + * <li>timezones in datetime values are not strictly verified, for example <code>{"foo": "2015-01-01T13:00:00+2000"}</code> + * matches <code>{"foo": "2015-01-01T10:00:00-1000"}</code>. This feature can be disabled with + * {@link #setStrictTimezone(boolean)} + * </li> + * </ul> + * + * @since 5.2 + */ +public class JsonAssert { + + private final String actualJson; + private final JsonComparison comparison = new JsonComparison(); + + private JsonAssert(String actualJson) { + this.actualJson = actualJson; + } + + public JsonAssert setStrictTimezone(boolean b) { + comparison.setStrictTimezone(b); + return this; + } + + public JsonAssert setStrictArrayOrder(boolean b) { + comparison.setStrictArrayOrder(b); + return this; + } + + public JsonAssert isSimilarTo(String expected) { + boolean similar = comparison.areSimilar(expected, actualJson); + if (!similar) { + throw new ComparisonFailure("Not a super-set of expected JSON -", pretty(expected), pretty(actualJson)); + } + return this; + } + + public JsonAssert isSimilarTo(URL expected) { + return isSimilarTo(urlToString(expected)); + } + + public static JsonAssert assertJson(String actualJson) { + return new JsonAssert(actualJson); + } + + public static JsonAssert assertJson(URL actualJson) { + return new JsonAssert(urlToString(actualJson)); + } + + private static String urlToString(URL url) { + try { + return IOUtils.toString(url, Charsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Fail to load JSON from " + url, e); + } + } + + private static String pretty(String json) { + JsonElement gson = new JsonParser().parse(json); + return new GsonBuilder().setPrettyPrinting().serializeNulls().create().toJson(gson); + } +} diff --git a/sonar-testing-harness/src/main/java/org/sonar/test/JsonComparison.java b/sonar-testing-harness/src/main/java/org/sonar/test/JsonComparison.java new file mode 100644 index 00000000000..1a437f5daad --- /dev/null +++ b/sonar-testing-harness/src/main/java/org/sonar/test/JsonComparison.java @@ -0,0 +1,198 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.test; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Not thread-safe because of field datetimeFormat which is SimpleDateFormat. + */ +class JsonComparison { + + private static final SimpleDateFormat datetimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); + + private boolean strictTimezone = false; + private boolean strictArrayOrder = false; + + boolean isStrictTimezone() { + return strictTimezone; + } + + JsonComparison setStrictTimezone(boolean b) { + this.strictTimezone = b; + return this; + } + + boolean isStrictArrayOrder() { + return strictArrayOrder; + } + + JsonComparison setStrictArrayOrder(boolean b) { + this.strictArrayOrder = b; + return this; + } + + boolean areSimilar(String expected, String actual) { + Object expectedJson = parse(expected); + Object actualJson = parse(actual); + return compare(expectedJson, actualJson); + } + + private Object parse(String s) { + try { + JSONParser parser = new JSONParser(); + return parser.parse(s); + } catch (Exception e) { + throw new IllegalStateException("Invalid JSON: " + s, e); + } + } + + private boolean compare(@Nullable Object expectedObject, @Nullable Object actualObject) { + if (expectedObject == null) { + return actualObject == null; + } + if (actualObject == null) { + return false; + } + if (expectedObject.getClass() != actualObject.getClass()) { + return false; + } + if (expectedObject instanceof JSONArray) { + return compareArrays((JSONArray) expectedObject, (JSONArray) actualObject); + } + if (expectedObject instanceof JSONObject) { + return compareMaps((JSONObject) expectedObject, (JSONObject) actualObject); + } + if (expectedObject instanceof String) { + return compareStrings((String) expectedObject, (String) actualObject); + } + if (expectedObject instanceof Number) { + return compareNumbers((Number) expectedObject, (Number) actualObject); + } + return compareBooleans((Boolean) expectedObject, (Boolean) actualObject); + } + + private boolean compareBooleans(Boolean expected, Boolean actual) { + return expected.equals(actual); + } + + private boolean compareNumbers(Number expected, Number actual) { + double d1 = expected.doubleValue(); + double d2 = actual.doubleValue(); + if (Double.compare(d1, d2) == 0) { + return true; + } + return (Math.abs(d1 - d2) <= 0.0000001); + } + + private boolean compareStrings(String expected, String actual) { + if (!strictTimezone) { + // two instants with different timezones are considered as identical (2015-01-01T13:00:00+0100 and 2015-01-01T12:00:00+0000) + Date expectedDate = tryParseDate(expected); + Date actualDate = tryParseDate(actual); + if (expectedDate != null && actualDate != null) { + return expectedDate.getTime() == actualDate.getTime(); + } + } + return expected.equals(actual); + } + + private boolean compareArrays(JSONArray expected, JSONArray actual) { + if (strictArrayOrder) { + return compareArraysByStrictOrder(expected, actual); + } + return compareArraysByLenientOrder(expected, actual); + } + + private boolean compareArraysByStrictOrder(JSONArray expected, JSONArray actual) { + if (expected.size() != actual.size()) { + return false; + } + + for (int index = 0; index < expected.size(); index++) { + Object expectedElt = expected.get(index); + Object actualElt = actual.get(index); + if (!compare(expectedElt, actualElt)) { + return false; + } + } + return true; + } + + private boolean compareArraysByLenientOrder(JSONArray expected, JSONArray actual) { + if (expected.size() > actual.size()) { + return false; + } + + List remainingActual = new ArrayList(actual); + for (Object expectedElement : expected) { + // element can be null + boolean found = false; + for (Object actualElement : remainingActual) { + if (compare(expectedElement, actualElement)) { + found = true; + remainingActual.remove(actualElement); + break; + } + } + if (!found) { + return false; + } + } + if (!remainingActual.isEmpty()) { + return false; + } + return true; + } + + private boolean compareMaps(JSONObject expectedMap, JSONObject actualMap) { + // each key-value of expected map must exist in actual map + for (Object expectedKey : expectedMap.keySet()) { + if (!actualMap.containsKey(expectedKey)) { + return false; + } + if (!compare(expectedMap.get(expectedKey), actualMap.get(expectedKey))) { + return false; + } + } + return true; + } + + @CheckForNull + Date tryParseDate(String s) { + try { + return datetimeFormat.parse(s); + } catch (ParseException ignored) { + // not a datetime + return null; + } + } +} diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/JsonAssertTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/JsonAssertTest.java new file mode 100644 index 00000000000..59a05d6506f --- /dev/null +++ b/sonar-testing-harness/src/test/java/org/sonar/test/JsonAssertTest.java @@ -0,0 +1,97 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.test; + +import org.junit.ComparisonFailure; +import org.junit.Test; + +import java.io.File; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.sonar.test.JsonAssert.assertJson; + +public class JsonAssertTest { + + @Test + public void isSimilarAs_strings() throws Exception { + assertJson("{}").isSimilarTo("{}"); + + try { + assertJson("{}").isSimilarTo("[]"); + fail(); + } catch (ComparisonFailure error) { + assertThat(error.getMessage()).isEqualTo("Not a super-set of expected JSON - expected:<[[]]> but was:<[{}]>"); + assertThat(error.getActual()).isEqualTo("{}"); + assertThat(error.getExpected()).isEqualTo("[]"); + } + } + + @Test + public void isSimilarAs_urls() throws Exception { + URL url1 = getClass().getResource("JsonAssertTest/sample1.json"); + URL url2 = getClass().getResource("JsonAssertTest/sample2.json"); + assertJson(url1).isSimilarTo(url1); + + try { + assertJson(url1).isSimilarTo(url2); + fail(); + } catch (AssertionError error) { + // ok + } + } + + @Test + public void actual_can_be_superset_of_expected() throws Exception { + assertJson("{\"foo\": \"bar\"}").isSimilarTo("{}"); + try { + assertJson("{}").isSimilarTo("{\"foo\": \"bar\"}"); + fail(); + } catch (AssertionError error) { + // ok + } + } + + @Test(expected = IllegalStateException.class) + public void fail_to_load_url() throws Exception { + assertJson(new File("target/missing").toURL()); + } + + @Test + public void enable_strict_order_of_arrays() throws Exception { + try { + assertJson("[1,2]").setStrictArrayOrder(true).isSimilarTo("[2, 1]"); + fail(); + } catch (AssertionError error) { + // ok + } + } + + @Test + public void enable_strict_timezone() throws Exception { + try { + assertJson("[\"2010-05-18T15:50:45+0100\"]").setStrictTimezone(true).isSimilarTo("[\"2010-05-18T16:50:45+0200\"]"); + fail(); + } catch (AssertionError error) { + // ok + } + } +} diff --git a/sonar-testing-harness/src/test/java/org/sonar/test/JsonComparisonTest.java b/sonar-testing-harness/src/test/java/org/sonar/test/JsonComparisonTest.java new file mode 100644 index 00000000000..6dc57199a7c --- /dev/null +++ b/sonar-testing-harness/src/test/java/org/sonar/test/JsonComparisonTest.java @@ -0,0 +1,178 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.test; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonComparisonTest { + + @Test(expected = IllegalStateException.class) + public void fail_if_invalid_json() { + areSimilar("{]", ""); + } + + @Test + public void syntax_agnostic() throws Exception { + assertThat(areSimilar("{}", " { } ")).isTrue(); + assertThat(areSimilar("{\"foo\":\"bar\"}", "{\"foo\": \"bar\" \n }")).isTrue(); + } + + @Test + public void object() throws Exception { + assertThat(areSimilar("{}", "{}")).isTrue(); + + // exactly the same + assertThat(areSimilar("{\"foo\":\"bar\"}", "{\"foo\":\"bar\"}")).isTrue(); + + // same key but different value + assertThat(areSimilar("{\"foo\":\"bar\"}", "{\"foo\":\"baz\"}")).isFalse(); + + // missing key + assertThat(areSimilar("{\"foo\":\"bar\"}", "{\"xxx\":\"bar\"}")).isFalse(); + + // expected json can be a subset of actual json + assertThat(areSimilar("{\"foo\":\"bar\"}", "{\"xxx\":\"bar\", \"foo\": \"bar\"}")).isTrue(); + } + + @Test + public void strict_order_of_array() throws Exception { + assertThat(isSimilar_strict_array_order("[]", "[]")).isTrue(); + assertThat(isSimilar_strict_array_order("[1, 2]", "[1, 2]")).isTrue(); + + assertThat(isSimilar_strict_array_order("[1, 2]", "[1]")).isFalse(); + assertThat(isSimilar_strict_array_order("[1, 2]", "[2, 1]")).isFalse(); + assertThat(isSimilar_strict_array_order("[1, 2]", "[1 , 2, 3]")).isFalse(); + assertThat(isSimilar_strict_array_order("[1, 2]", "[1 , false]")).isFalse(); + assertThat(isSimilar_strict_array_order("[1, 2]", "[1 , 3.14]")).isFalse(); + } + + @Test + public void lenient_order_of_array() throws Exception { + assertThat(areSimilar("[]", "[]")).isTrue(); + assertThat(areSimilar("[1, 2]", "[1, 2]")).isTrue(); + assertThat(areSimilar("[1, 2]", "[1]")).isFalse(); + assertThat(areSimilar("[1, 2]", "[2, 1]")).isTrue(); + assertThat(areSimilar("[1, 2]", "[1 , 2, 3]")).isFalse(); + assertThat(areSimilar("[1, 2]", "[1 , false]")).isFalse(); + assertThat(areSimilar("[1, 2]", "[1 , 3.14]")).isFalse(); + } + + @Test + public void lenient_order_of_arrays_by_default() throws Exception { + assertThat(new JsonComparison().isStrictArrayOrder()).isFalse(); + } + + @Test + public void null_value() throws Exception { + assertThat(areSimilar("[null]", "[null]")).isTrue(); + assertThat(areSimilar("[null]", "[]")).isFalse(); + + assertThat(areSimilar("{\"foo\": null}", "{\"foo\": null}")).isTrue(); + assertThat(areSimilar("{\"foo\": null}", "{\"foo\": \"bar\"}")).isFalse(); + assertThat(areSimilar("{\"foo\": 3}", "{\"foo\": null}")).isFalse(); + assertThat(areSimilar("{\"foo\": 3.14}", "{\"foo\": null}")).isFalse(); + assertThat(areSimilar("{\"foo\": false}", "{\"foo\": null}")).isFalse(); + assertThat(areSimilar("{\"foo\": true}", "{\"foo\": null}")).isFalse(); + assertThat(areSimilar("{\"foo\": null}", "{\"foo\": 3}")).isFalse(); + assertThat(areSimilar("{\"foo\": null}", "{\"foo\": 3.14}")).isFalse(); + assertThat(areSimilar("{\"foo\": null}", "{\"foo\": false}")).isFalse(); + assertThat(areSimilar("{\"foo\": null}", "{\"foo\": true}")).isFalse(); + } + + @Test + public void maps_and_arrays() throws Exception { + assertThat(areSimilar("[]", "{}")).isFalse(); + assertThat(areSimilar("{}", "[]")).isFalse(); + + // map of array + assertThat(areSimilar("{\"foo\": []}", "{\"foo\": []}")).isTrue(); + assertThat(areSimilar("{\"foo\": [1, 3]}", "{\"foo\": [1, 3], \"bar\": [1, 3]}")).isTrue(); + assertThat(areSimilar("{\"foo\": []}", "{\"foo\": []}")).isTrue(); + assertThat(areSimilar("{\"foo\": [1, 2]}", "{\"foo\": [1, 3]}")).isFalse(); + + // array of maps + assertThat(areSimilar("[{}]", "[{}]")).isTrue(); + assertThat(areSimilar("[{}]", "[{\"foo\": 1}]")).isTrue(); + // exactly the sames + assertThat(areSimilar("[{\"1\": \"3\"}, {\"2\":\"4\"}]", "[{\"1\": \"3\"}, {\"2\":\"4\"}]")).isTrue(); + // different value + assertThat(areSimilar("[{\"1\": \"3\"}, {\"2\":\"4\"}]", "[{\"1\": \"3\"}, {\"2\":\"3\"}]")).isFalse(); + // missing key + assertThat(areSimilar("[{\"1\": \"3\"}, {\"2\":\"4\"}]", "[{\"1\": \"3\"}, {\"5\":\"10\"}]")).isFalse(); + } + + @Test + public void lenient_timezone() throws Exception { + // lenient mode by default + assertThat(new JsonComparison().isStrictTimezone()).isFalse(); + + // same instant, same timezone + assertThat(areSimilar("{\"foo\": \"2010-05-18T15:50:45+0100\"}", "{\"foo\": \"2010-05-18T15:50:45+0100\"}")).isTrue(); + + // same instant, but different timezone + assertThat(areSimilar("{\"foo\": \"2010-05-18T15:50:45+0100\"}", "{\"foo\": \"2010-05-18T18:50:45+0400\"}")).isTrue(); + + // different time + assertThat(areSimilar("{\"foo\": \"2010-05-18T15:50:45+0100\"}", "{\"foo\": \"2010-05-18T15:51:45+0100\"}")).isFalse(); + } + + @Test + public void strict_timezone() throws Exception { + assertThat(new JsonComparison().setStrictTimezone(true).isStrictTimezone()).isTrue(); + + // same instant, same timezone + assertThat(isSimilar_strict_timezone("{\"foo\": \"2010-05-18T15:50:45+0100\"}", "{\"foo\": \"2010-05-18T15:50:45+0100\"}")).isTrue(); + assertThat(isSimilar_strict_timezone("[\"2010-05-18T15:50:45+0100\"]", "[\"2010-05-18T15:50:45+0100\"]")).isTrue(); + + // same instant, but different timezone + assertThat(isSimilar_strict_timezone("{\"foo\": \"2010-05-18T15:50:45+0100\"}", "{\"foo\": \"2010-05-18T18:50:45+0400\"}")).isFalse(); + + // different time + assertThat(isSimilar_strict_timezone("{\"foo\": \"2010-05-18T15:50:45+0100\"}", "{\"foo\": \"2010-05-18T15:51:45+0100\"}")).isFalse(); + } + + @Test + public void compare_doubles() throws Exception { + assertThat(areSimilar("{\"foo\": true}", "{\"foo\": false}")).isFalse(); + assertThat(areSimilar("{\"foo\": true}", "{\"foo\": true}")).isTrue(); + assertThat(areSimilar("{\"foo\": true}", "{\"foo\": \"true\"}")).isFalse(); + assertThat(areSimilar("{\"foo\": true}", "{\"foo\": 1}")).isFalse(); + } + + @Test + public void compare_booleans() throws Exception { + assertThat(areSimilar("{\"foo\": 3.14}", "{\"foo\": 3.14000000}")).isTrue(); + assertThat(areSimilar("{\"foo\": 3.14}", "{\"foo\": 3.1400001}")).isTrue(); + } + + private boolean areSimilar(String expected, String actual) { + return new JsonComparison().areSimilar(expected, actual); + } + + private boolean isSimilar_strict_timezone(String expected, String actual) { + return new JsonComparison().setStrictTimezone(true).areSimilar(expected, actual); + } + + private boolean isSimilar_strict_array_order(String expected, String actual) { + return new JsonComparison().setStrictArrayOrder(true).areSimilar(expected, actual); + } +} diff --git a/sonar-testing-harness/src/test/resources/org/sonar/test/JsonAssertTest/sample1.json b/sonar-testing-harness/src/test/resources/org/sonar/test/JsonAssertTest/sample1.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/test/JsonAssertTest/sample1.json @@ -0,0 +1 @@ +{} diff --git a/sonar-testing-harness/src/test/resources/org/sonar/test/JsonAssertTest/sample2.json b/sonar-testing-harness/src/test/resources/org/sonar/test/JsonAssertTest/sample2.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/sonar-testing-harness/src/test/resources/org/sonar/test/JsonAssertTest/sample2.json @@ -0,0 +1 @@ +[] |