LocationOfError1173Test.java

package com.fasterxml.jackson.core.read.loc;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.async.ByteArrayFeeder;
import com.fasterxml.jackson.core.exc.StreamReadException;

import static org.junit.jupiter.api.Assertions.*;

/**
 * Tests that the {@link JsonLocation} attached to a thrown {@link StreamReadException}
 * due to invalid JSON points to the correct character.
 */
class LocationOfError1173Test extends JUnit5TestBase
{
    static final JsonFactory JSON_F = new JsonFactory();

    /** Represents the different parser backends */
    public enum ParserVariant
    {
        BYTE_ARRAY(
            (String input) -> JSON_F.createParser(input.getBytes(StandardCharsets.UTF_8)),
            true,   // supports byte offsets in reported location
            false,  // supports character offsets in reported location
            true    // supports column numbers in reported location
        ),
        CHAR_ARRAY(
            (String input) -> JSON_F.createParser(input.toCharArray()),
            false,
            true,
            true
        ),
        DATA_INPUT(
            (String input) -> JSON_F.createParser((DataInput) new DataInputStream(new ByteArrayInputStream(
                input.getBytes(StandardCharsets.UTF_8)
            ))),
            false,
            false,
            false
        ),
        ASYNC(
            (String input) -> {
                JsonParser parser = JSON_F.createNonBlockingByteArrayParser();
                ByteArrayFeeder feeder = (ByteArrayFeeder) parser.getNonBlockingInputFeeder();
                assertTrue(feeder.needMoreInput());

                byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
                feeder.feedInput(inputBytes, 0, inputBytes.length);
                feeder.endOfInput();

                return parser;
            },
            true,
            false,
            true
        )
        ;

        ParserVariant(
            ParserGenerator parserGenerator,
            boolean supportsByteOffset,
            boolean supportsCharOffset,
            boolean supportsColumnNr
        )
        {
            _parserGenerator = parserGenerator;

            this.supportsByteOffset = supportsByteOffset;
            this.supportsCharOffset = supportsCharOffset;
            this.supportsColumnNr = supportsColumnNr;
        }

        public JsonParser createParser(String input) throws Exception
        {
            return _parserGenerator.createParser(input);
        }

        private final ParserGenerator _parserGenerator;
        public final boolean supportsByteOffset;
        public final boolean supportsCharOffset;
        public final boolean supportsColumnNr;
    }

    /** Collection of differing invalid JSON input cases to test */
    private static final List<InvalidJson> INVALID_JSON_CASES = Arrays.asList(
        new InvalidJson(
            "Object property missing colon",
            a2q("{'invalid' 'json'}"),
            11, // byte offset
            11, // char offset
            1,  // line number
            12  // column number
        ),
        new InvalidJson(
            "Comma after key in object property",
            a2q("{'invalid', 'json'}"),
            10,
            10,
            1,
            11
        ),
        new InvalidJson(
            "Missing comma between object properties",
            a2q("{'key1':'value1' 'key2':'value2'}"),
            17,
            17,
            1,
            18
        ),
        new InvalidJson(
            "Number as a property key",
            "{1234: 5678}",
            1,
            1,
            1,
            2
        ),
        new InvalidJson(
            "false literal as property key",
            "{false: true}",
            1,
            1,
            1,
            2
        ),
        new InvalidJson(
            "true literal as property key",
            "{true: false}",
            1,
            1,
            1,
            2
        ),
        new InvalidJson(
            "null literal as property key",
            "{null: \"content\"}",
            1,
            1,
            1,
            2
        ),
        new InvalidJson(
            "Missing comma between list elements",
            "[\"no\" \"commas\"]",
            6,
            6,
            1,
            7
        ),
        new InvalidJson(
            "Property key/value delimiter in list",
            "[\"still\":\"invalid\"]",
            8,
            8,
            1,
            9
        ),
        new InvalidJson(
            "Unexpected EOF",
            "{",
            1,
            1,
            1,
            2
        ),
        new InvalidJson(
            "Close marker without matching open marker",
            "}",
            0,
            0,
            1,
            1
        ),
        new InvalidJson(
            "Mismatched open/close tokens",
            "{\"open\":\"close\"]",
            15,
            15,
            1,
            16
        ),
        new InvalidJson(
            "Bare strings in JSON",
            "{missing: quotes}",
            1,
            1,
            1,
            2
        ),
        new InvalidJson(
            "Error in middle of line for multiline input",
            // missing comma delimiter between properties two and three
            "{\n  \"one\": 1,\n  \"two\": 2\n  \"three\": 3\n}",
            27,
            27,
            4,
            3
        ),
        new InvalidJson(
            "Error at end of line for multiline input",
            // double commas between keys
            "{\n\"key1\":\"value1\",,\n\"key2\":\"value2\"\n}",
            18,
            18,
            2,
            17
        )
    );

    @ParameterizedTest
    @MethodSource("_generateTestData")
    void parserBackendWithInvalidJson(ParserVariant variant, InvalidJson invalidJson)
            throws Exception
    {
        try (JsonParser parser = variant.createParser(invalidJson.input))
        {
            StreamReadException e = assertThrows(
                    StreamReadException.class,
                () -> {
                    // Blindly advance the parser through the end of input
                    while (parser.nextToken() != null) {}
                }
            );

            JsonLocation location = e.getLocation();
            assertEquals(invalidJson.lineNr, location.getLineNr());
            final String msg = e.getOriginalMessage();

            if (variant.supportsByteOffset)
            {
                assertEquals(invalidJson.byteOffset, location.getByteOffset(), "Incorrect byte offset (for '"+msg+"')");
            }
            if (variant.supportsCharOffset)
            {
                assertEquals(invalidJson.charOffset, location.getCharOffset(), "Incorrect char offset (for '"+msg+"')");
            }
            if (variant.supportsColumnNr)
            {
                assertEquals(invalidJson.columnNr, location.getColumnNr(), "Incorrect column (for '"+msg+"')");
            }
        }
    }

    private static Stream<Arguments> _generateTestData()
    {
        return Arrays.stream(ParserVariant.values())
            .flatMap(parserVariant -> INVALID_JSON_CASES.stream().map(
                invalidJson -> Arguments.of(parserVariant, invalidJson)
            ));
    }

    @FunctionalInterface
    public interface ParserGenerator
    {
        JsonParser createParser(String input) throws Exception;
    }

    static class InvalidJson
    {
        InvalidJson(String name, String input, int byteOffset, int charOffset,
                int lineNr, int columnNr)
        {
            _name = name;

            this.input = input;
            this.byteOffset = byteOffset;
            this.charOffset = charOffset;
            this.lineNr = lineNr;
            this.columnNr = columnNr;
        }

        @Override
        public String toString()
        {
            return _name;
        }

        private final String _name;
        public final String input;
        public final int byteOffset;
        public final int charOffset;
        public final int lineNr;
        public final int columnNr;
    }
}