ConformanceTest.java

package org.json.simple;

import java.math.BigDecimal;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.json.simple.parser.ParseException;

import junit.framework.TestCase;

/**
 * Differential test against JSON-java (org.json), the reference implementation.
 *
 * Every other test in this project checks json-simple against itself, so it can
 * only catch what the author thought to check. This one uses an independently
 * written library as the judge.
 *
 * Comparison is semantic, not textual. The two libraries produce different but
 * equally valid JSON text for the same data, and none of those differences is a
 * defect:
 *
 *   - json-simple escapes "/" as "\/" and escapes U+007F; the reference does
 *     not. Both are legal, and both are on this project's list of behaviours
 *     that must not change.
 *   - json-simple writes u-escapes with upper-case hex digits, the reference
 *     lower-case.
 *   - Numbers are formatted differently: 1.0E10 against 1E+10, -0.0 against -0.
 *
 * Comparing the text would therefore report failures for correct output. What
 * matters is that both libraries agree on what a document *means*, so the
 * assertions below compare decoded structures, using the reference parser as an
 * independent reader of json-simple's output.
 *
 * Note that org.json.JSONObject and org.json.simple.JSONObject are different
 * classes with the same name; the reference types are spelled out in full here.
 */
public class ConformanceTest extends TestCase {

	/**
	 * Strictly valid RFC 8259 documents. This is deliberately not the place for
	 * json-simple's documented leniency (trailing commas, optional separators,
	 * duplicate keys): the reference implementation rejects those, and the
	 * disagreement would say nothing about correctness. RegressionTest pins that
	 * behaviour instead.
	 */
	private static final String[] DOCUMENTS = {
		"{}",
		"[]",
		"null",
		"true",
		"false",
		"0",
		"-1",
		"42",
		"1.5",
		"1.50",
		"-0.0",
		"1e10",
		"1E-10",
		"1.7976931348623157e308",
		"9223372036854775807",
		"-9223372036854775808",
		"\"\"",
		"\"plain\"",
		"\"a/b\"",
		"\"</script>\"",
		"\"tab\\there\"",
		"\"newline\\nhere\"",
		"\"quote\\\"here\"",
		"\"backslash\\\\here\"",
		"\"\\b\\f\\r\"",
		"\"\\u0001\"",
		"\"\\u007f\"",
		"\"\\u0085\"",
		"\"\\u2028\\u2029\"",
		"\"\\u20ac\"",
		"\"\\ud83d\\ude00\"",
		"\"caf\u00e9\"",
		"[1,2,3]",
		"[\"a\",\"b\"]",
		"[null,true,false]",
		"[[],[[]],[[[]]]]",
		"[{},{\"a\":1}]",
		"{\"a\":1}",
		"{\"\":1}",
		"{\"a\":null}",
		"{\"a\":\"/\"}",
		"{\"a\":{\"b\":{\"c\":[1,2,{\"d\":null}]}}}",
		"{\"one\":1,\"two\":2,\"three\":3,\"four\":4,\"five\":5}",
		"  {  \"a\"  :  [ 1 , 2 ]  }  ",
		"{\"unicode\":\"\\u4e2d\\u6587\",\"emoji\":\"\\ud83c\\udf89\"}",
	};

	/** Both libraries must read the same document as the same data. */
	public void testDecodingAgreesWithTheReference() throws Exception {
		for (int i = 0; i < DOCUMENTS.length; i++) {
			String document = DOCUMENTS[i];
			assertEquivalent(document,
					JSONValue.parseWithException(document),
					reference(document));
		}
	}

	/**
	 * What json-simple writes must mean the same thing when an independent
	 * parser reads it back. This is what catches an encoder that escapes
	 * incorrectly or emits malformed text: json-simple's own parser might well
	 * accept its own mistake, but the reference will not.
	 */
	public void testEncodingIsReadableByTheReference() throws Exception {
		for (int i = 0; i < DOCUMENTS.length; i++) {
			String document = DOCUMENTS[i];
			String encoded = JSONValue.toJSONString(JSONValue.parseWithException(document));
			assertEquivalent(document + " -> " + encoded,
					JSONValue.parseWithException(document),
					reference(encoded));
		}
	}

	/** And json-simple must read back what the reference writes. */
	public void testReferenceOutputIsReadableByJsonSimple() throws Exception {
		for (int i = 0; i < DOCUMENTS.length; i++) {
			// A bare scalar comes back from the reference as a plain Java value
			// whose toString() is not JSON text, so wrap those in an array to
			// get something the reference will actually serialise. The scalar
			// still gets exercised, one level down.
			String document = isContainer(DOCUMENTS[i])
					? DOCUMENTS[i]
					: "[" + DOCUMENTS[i] + "]";
			String encoded = String.valueOf(reference(document));
			assertEquivalent(document + " <- " + encoded,
					JSONValue.parseWithException(encoded),
					reference(document));
		}
	}

	/**
	 * A differential test only catches what the other implementation rejects or
	 * reads differently, and the reference is laxer than RFC 8259 here: of the
	 * thirty-two control characters it refuses only NUL, LF and CR inside a
	 * string, so an encoder that stopped escaping U+0001 would produce invalid
	 * JSON that this comparison still called equivalent. Check the text itself.
	 *
	 * json-simple emits no insignificant whitespace, so no character below
	 * U+0020 may appear anywhere in its output.
	 */
	public void testEncodedOutputHasNoRawControlCharacters() throws Exception {
		for (int i = 0; i < DOCUMENTS.length; i++) {
			String encoded = JSONValue.toJSONString(
					JSONValue.parseWithException(DOCUMENTS[i]));
			for (int at = 0; at < encoded.length(); at++) {
				char c = encoded.charAt(at);
				assertTrue("unescaped control character U+"
								+ Integer.toHexString(c) + " at " + at
								+ " of " + DOCUMENTS[i] + " -> " + encoded,
						c >= 0x20);
			}
		}
	}

	/**
	 * A known and accepted divergence, asserted so that it is visible and so
	 * that this test starts failing the day it is addressed.
	 *
	 * The reference implementation returns a BigInteger for an integer too large
	 * for a long. json-simple reports a ParseException instead. Reading such
	 * numbers is a 1.2.0 item; until then the failure is at least well formed,
	 * which it was not before 1.1.2 - a raw NumberFormatException used to escape.
	 */
	public void testIntegersBeyondLongAreNotYetSupported() {
		String document = "123456789012345678901234567890";

		assertEquals(java.math.BigInteger.class, reference(document).getClass());

		try {
			JSONValue.parseWithException(document);
			fail("json-simple now reads integers beyond long; update this test "
					+ "and the note in README.md");
		} catch (ParseException expected) {
			assertEquals(ParseException.ERROR_UNEXPECTED_EXCEPTION, expected.getErrorType());
		}
	}

	// ----------------------------------------------------------------------

	private static Object reference(String document) {
		return new org.json.JSONTokener(document).nextValue();
	}

	private static boolean isContainer(String document) {
		String trimmed = document.trim();
		return trimmed.startsWith("{") || trimmed.startsWith("[");
	}

	/**
	 * Compares a json-simple value against a reference value by meaning:
	 * containers by contents, numbers by numeric value rather than by the way
	 * they happen to be formatted, everything else by equality.
	 */
	private static void assertEquivalent(String context, Object mine, Object theirs) {
		if (mine == null || org.json.JSONObject.NULL.equals(theirs)) {
			assertTrue(context + ": one side is null, the other is " + mine + " / " + theirs,
					mine == null && org.json.JSONObject.NULL.equals(theirs));
			return;
		}

		if (mine instanceof Map) {
			assertTrue(context + ": expected an object, got " + theirs.getClass().getName(),
					theirs instanceof org.json.JSONObject);
			Map mineMap = (Map) mine;
			org.json.JSONObject theirsObject = (org.json.JSONObject) theirs;
			assertEquals(context + ": different number of members",
					mineMap.size(), theirsObject.length());
			for (Iterator it = mineMap.keySet().iterator(); it.hasNext();) {
				String key = String.valueOf(it.next());
				assertTrue(context + ": the reference has no member " + key,
						theirsObject.has(key));
				assertEquivalent(context + "." + key, mineMap.get(key), theirsObject.get(key));
			}
			return;
		}

		if (mine instanceof List) {
			assertTrue(context + ": expected an array, got " + theirs.getClass().getName(),
					theirs instanceof org.json.JSONArray);
			List mineList = (List) mine;
			org.json.JSONArray theirsArray = (org.json.JSONArray) theirs;
			assertEquals(context + ": different number of elements",
					mineList.size(), theirsArray.length());
			for (int i = 0; i < mineList.size(); i++) {
				assertEquivalent(context + "[" + i + "]", mineList.get(i), theirsArray.get(i));
			}
			return;
		}

		if (mine instanceof Number) {
			assertTrue(context + ": expected a number, got " + theirs.getClass().getName(),
					theirs instanceof Number);
			// 1.0E10 and 1E+10 are the same number written two ways, and so are
			// -0.0 and -0; compareTo ignores scale, equals would not.
			assertEquals(context + ": " + mine + " != " + theirs, 0,
					new BigDecimal(mine.toString()).compareTo(new BigDecimal(theirs.toString())));
			return;
		}

		assertEquals(context, mine, theirs);
	}
}