Skip to content

Commit

Permalink
refactor: Fully test ResultSetImpl contract.
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-simons committed Mar 12, 2024
1 parent cce2a30 commit a2711a1
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -885,10 +885,27 @@ else if (named) {
Mockito.verify(in).close();
}

try (var ps = connection.prepareStatement("MATCH (m:CSTest {type: $1}) RETURN m.content")) {
try (var ps = connection.prepareStatement(
"MATCH (m:CSTest {type: $1}) RETURN m.content AS content, 23.42 AS invalid, null AS n, 'a lengthy lengthy lengthy lengthy lengthy string' AS s")) {
if (lengthUsed != null && lengthUsed != 0) {
ps.setMaxFieldSize(lengthUsed);
}
ps.setString(1, type);
try (var result = ps.executeQuery()) {

assertThat(result.next()).isTrue();

try (var r = new BufferedReader(new InputStreamReader(result.getBinaryStream("s")))) {
var actual = r.readLine();
assertThat(actual).isEqualTo("a lengthy lengthy lengthy lengthy lengthy string".substring(0,
(lengthUsed != null && lengthUsed != 0) ? Math.min(lengthUsed, actual.length())
: actual.length()));
}
assertThat(result.getBinaryStream("n")).isNull();
assertThatExceptionOfType(SQLException.class)
.isThrownBy(() -> result.getBinaryStream("invalid"))
.withMessage("FLOAT value can not be mapped to java.io.InputStream");

var actual = result.getObject(1, Value.class).asByteArray();
if (lengthUsed != null) {
if (lengthUsed == 0) {
Expand All @@ -907,6 +924,12 @@ else if (named) {
else {
assertThat(md5.digest(actual)).asHexString().isEqualTo("DA4AF72F62E96D5A00CF20FEA8766D1C");
}
try (var compare = result.getBinaryStream(1)) {
assertThat(actual).isEqualTo(compare.readAllBytes());
}
try (var compare = result.getBinaryStream("content")) {
assertThat(actual).isEqualTo(compare.readAllBytes());
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions neo4j-jdbc/src/main/java/org/neo4j/jdbc/ResultSetImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,9 @@ private static InputStream mapToBinaryStream(Value value, int maxFieldSize) thro
if (Type.STRING.isTypeOf(value)) {
return new ByteArrayInputStream(truncate(value.asString(), maxFieldSize).getBytes());
}
if (Type.BYTES.isTypeOf(value)) {
return new ByteArrayInputStream(truncate(value.asByteArray(), maxFieldSize));
}
if (Type.NULL.isTypeOf(value)) {
return null;
}
Expand Down
171 changes: 171 additions & 0 deletions neo4j-jdbc/src/test/java/org/neo4j/jdbc/ResultSetImplTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.sql.Time;
import java.sql.Timestamp;
Expand All @@ -44,11 +48,21 @@
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.Executable;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
Expand All @@ -61,6 +75,8 @@
import org.neo4j.jdbc.values.Values;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -1375,6 +1391,161 @@ private static Stream<Arguments> shouldTruncateOnGetCharacterStreamArgs() {
.flatMap(ResultSetImplTests::mapArgumentToBothIndexAndLabelAccess);
}

@Test
void statementShouldBeAvailable() throws SQLException {
try (var resultSet = emptyResultSet()) {
assertThat(resultSet.getStatement()).isNotNull();
}
}

@Test
void indexShouldBeChecked() throws SQLException {
try (var resultSet = setupWithValue(Values.value("test"), 0)) {
resultSet.next();
assertThatExceptionOfType(SQLException.class).isThrownBy(() -> resultSet.getInt(-23))
.withMessage("Invalid column index value");
assertThatExceptionOfType(SQLException.class).isThrownBy(() -> resultSet.getInt(42))
.withMessage("Invalid column index value");
}
}

@Test
void nameShouldBeChecked() throws SQLException {
try (var resultSet = setupWithValue(Values.value("test"), 0)) {
resultSet.next();
assertThatExceptionOfType(SQLException.class).isThrownBy(() -> resultSet.getInt("42"))
.withMessage("Invalid column label value");
}
}

@Test
void uncoercibleObject() throws SQLException {
try (var resultSet = setupWithValue(Values.value("test"), 0)) {
resultSet.next();
assertThatExceptionOfType(SQLException.class).isThrownBy(() -> resultSet.getObject(1, Float.class))
.withMessage(
"org.neo4j.jdbc.values.UncoercibleException: Cannot coerce java.lang.String to java.lang.Float");
}
}

@Test
void shouldThrowWhenClosed() throws SQLException {
var resultSet = emptyResultSet();
resultSet.close();
assertThatExceptionOfType(SQLException.class).isThrownBy(resultSet::next)
.withMessage("This result set is closed");
}

@SuppressWarnings("deprecation")
@Test
void bigDecimalRounding() throws SQLException {
try (var resultSet = setupWithValue(Values.value(1.25), 0)) {
resultSet.next();

assertThat(resultSet.getBigDecimal(1, 2)).isEqualTo(new BigDecimal("1.25"));
assertThatExceptionOfType(SQLException.class).isThrownBy(() -> resultSet.getBigDecimal(1, 1))
.withMessage("java.lang.ArithmeticException: Rounding necessary");
}
}

@SuppressWarnings("resource")
@TestFactory
Stream<DynamicNode> characteristicsShouldWork() {
var resultSet = emptyResultSet();
return Stream.of(
DynamicContainer
.dynamicContainer("fetchDirection",
Stream.of(
DynamicTest.dynamicTest("get",
() -> assertThat(resultSet.getFetchDirection())
.isEqualTo(ResultSet.FETCH_FORWARD)),
DynamicTest.dynamicTest("set", () -> {
assertThatNoException()
.isThrownBy(() -> resultSet.setFetchDirection(ResultSet.FETCH_FORWARD));
assertThatExceptionOfType(SQLException.class)
.isThrownBy(() -> resultSet.setFetchDirection(ResultSet.FETCH_REVERSE))
.withMessage("Only forward fetching is supported");
}))),
DynamicTest.dynamicTest("fetchSize", () -> {
var changedValue = StatementImpl.DEFAULT_FETCH_SIZE - 1;
resultSet.setFetchSize(changedValue);
assertThat(resultSet.getFetchSize()).isEqualTo(changedValue);
resultSet.setFetchSize(-1);
assertThat(resultSet.getFetchSize()).isEqualTo(StatementImpl.DEFAULT_FETCH_SIZE);
}),
DynamicTest.dynamicTest("type",
() -> assertThat(resultSet.getType()).isEqualTo(ResultSet.TYPE_FORWARD_ONLY)),
DynamicTest.dynamicTest("concurrency",
() -> assertThat(resultSet.getConcurrency()).isEqualTo(ResultSet.CONCUR_READ_ONLY)),
DynamicTest.dynamicTest("holdability",
() -> assertThat(resultSet.getHoldability()).isEqualTo(ResultSet.CLOSE_CURSORS_AT_COMMIT))

);
}

@TestFactory
Stream<DynamicContainer> unsupportedShouldThrowCorrectException() {
var testSupplier = generateTestsForUnsupportedMethods(emptyResultSet());

var updates = testSupplier.apply(method -> {
var name = method.getName();
return (name.startsWith("update") && !"updateRow".equals(name)) || name.matches("row.*ed");
});
var rowUpdates = testSupplier
.apply(method -> Set
.of("beforeFirst", "first", "last", "getRow", "absolute", "relative", "previous", "moveToCurrentRow",
"afterLast")
.contains(method.getName()));
var positional = testSupplier.apply(method -> Set
.of("insertRow", "updateRow", "deleteRow", "refreshRow", "cancelRowUpdates", "moveToInsertRow")
.contains(method.getName()));
var getters = testSupplier.apply(method -> Set
.of("getRef", "getBlob", "getClob", "getNClob", "getSQLXML", "getNString", "getNCharacterStream",
"getArray", "getURL", "getRowId", "getUnicodeStream", "getCursorName")
.contains(method.getName())
|| "getObject".equals(method.getName()) && method.getParameterTypes().length == 2
&& method.getParameterTypes()[1].isAssignableFrom(Map.class));

return Stream.of(DynamicContainer.dynamicContainer("updates", updates),
DynamicContainer.dynamicContainer("rowUpdates", rowUpdates),
DynamicContainer.dynamicContainer("positional", positional),
DynamicContainer.dynamicContainer("some getters", getters));
}

private static Function<Predicate<Method>, Stream<DynamicTest>> generateTestsForUnsupportedMethods(
ResultSet resultSet) {
var methods = ResultSet.class.getMethods();

BiFunction<Method, Object[], Executable> assertionSupplier = (method,
args) -> (Executable) () -> assertThatExceptionOfType(InvocationTargetException.class)
.isThrownBy(() -> method.invoke(resultSet, args))
.withCauseInstanceOf(SQLFeatureNotSupportedException.class);

return p -> Arrays.stream(methods).map(method -> {
if (p.test(method)) {
var name = method.getName();
var args = Arrays.stream(method.getParameterTypes()).map(ResultSetImplTests::getDefaultValue).toArray();
return DynamicTest.dynamicTest(name, assertionSupplier.apply(method, args));
}
return null;
}).filter(Objects::nonNull);
}

@SuppressWarnings("unchecked")
private static <T> T getDefaultValue(Class<T> clazz) {
return (T) Array.get(Array.newInstance(clazz, 1), 0);
}

private ResultSet emptyResultSet() {
var statement = mock(StatementImpl.class);
var runResponse = mock(RunResponse.class);

var pullResponse = mock(PullResponse.class);
given(pullResponse.records()).willReturn(List.of());

return new ResultSetImpl(statement, mock(Neo4jTransaction.class), runResponse, pullResponse, 1000, 0, 0);
}

private ResultSet setupWithValue(Value expectedValue, int maxFieldSize) throws SQLException {
var statement = mock(StatementImpl.class);
var runResponse = mock(RunResponse.class);
Expand Down

0 comments on commit a2711a1

Please sign in to comment.