LocationOfError1180Test.java
package tools.jackson.core.unittest.read.loc;
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 tools.jackson.core.*;
import tools.jackson.core.async.ByteArrayFeeder;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.json.JsonFactory;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests that the {@link TokenStreamLocation} attached to a thrown {@link StreamReadException}
* due to invalid JSON points to the correct character.
*/
class LocationOfError1180Test
{
static final JsonFactory JSON_F = new JsonFactory();
/** Represents the different parser backends */
public enum ParserVariant
{
BYTE_ARRAY(
(String input) -> JSON_F.createParser(ObjectReadContext.empty(), 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(ObjectReadContext.empty(), input.toCharArray()),
false,
true,
true
),
ASYNC(
(String input) -> {
JsonParser parser = JSON_F.createNonBlockingByteArrayParser(ObjectReadContext.empty());
ByteArrayFeeder feeder = (ByteArrayFeeder) parser.nonBlockingInputFeeder();
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(
"Incorrect case for false literal",
"{\"isThisValidJson\": FALSE}",
20, // byte offset: 'F' starts at position 20
20, // char offset: 'F' starts at position 20
1,
21 // column: 1-indexed, so position 20 = column 21
),
new InvalidJson(
"Incorrect case for true literal",
"{\"shouldYouAvoidWritingJsonLikeThis\": TRUE}",
38, // byte offset: 'T' starts at position 38
38, // char offset: 'T' starts at position 38
1,
39 // column: 1-indexed, so position 38 = column 39
),
new InvalidJson(
"Incorrect case for null literal",
"{\"licensePlate\": NULL}",
17, // byte offset: 'N' starts at position 17
17, // char offset: 'N' starts at position 17
1,
18 // column: 1-indexed, so position 17 = column 18
)
// NOTE: Unicode test case removed - it tests a different error path ("Unexpected character"
// vs "Unrecognized token") and has additional complexities with multi-byte UTF-8
);
@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) {}
}
);
TokenStreamLocation location = e.getLocation();
assertEquals(invalidJson.lineNr, location.getLineNr());
final String msg = e.getOriginalMessage();
if (variant.supportsByteOffset)
{
// [core#1180]: Async parser may be off by 1 due to when token start position is captured
if (variant == ParserVariant.ASYNC) {
long actual = location.getByteOffset();
assertTrue(actual == invalidJson.byteOffset || actual == invalidJson.byteOffset + 1,
String.format("Byte offset should be %d or %d but was %d (for '%s')",
invalidJson.byteOffset, invalidJson.byteOffset + 1, actual, msg));
} else {
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;
}
protected final String _name;
public final String input;
public final int byteOffset;
public final int charOffset;
public final int lineNr;
public final int columnNr;
}
}