Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import neo4j.org.testkit.backend.holder.TransactionHolder;
import neo4j.org.testkit.backend.messages.requests.TestkitCallbackResult;
import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
import org.neo4j.driver.exceptions.Neo4jException;
import org.neo4j.driver.internal.cluster.RoutingTableRegistry;
import reactor.core.publisher.Mono;

Expand All @@ -63,7 +62,7 @@ public class TestkitState {
private final Map<String, RxTransactionHolder> transactionIdToRxTransactionHolder = new HashMap<>();

@Getter
private final Map<String, Neo4jException> errors = new HashMap<>();
private final Map<String, Exception> errors = new HashMap<>();

private final AtomicInteger idGenerator = new AtomicInteger(0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.time.zone.ZoneRulesException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
Expand Down Expand Up @@ -79,7 +80,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
}
});
} catch (Throwable throwable) {
ctx.writeAndFlush(createErrorResponse(throwable));
exceptionCaught(ctx, throwable);
}
});
}
Expand All @@ -95,6 +96,11 @@ private static CompletionStage<TestkitResponse> wrapSyncRequest(
return result;
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.writeAndFlush(createErrorResponse(cause));
}

private TestkitResponse createErrorResponse(Throwable throwable) {
if (throwable instanceof CompletionException) {
throwable = throwable.getCause();
Expand All @@ -111,8 +117,11 @@ private TestkitResponse createErrorResponse(Throwable throwable) {
.msg(e.getMessage())
.build())
.build();
} else if (isConnectionPoolClosedException(throwable) || throwable instanceof UntrustedServerException) {
} else if (isConnectionPoolClosedException(throwable)
|| throwable instanceof UntrustedServerException
|| throwable instanceof ZoneRulesException) {
String id = testkitState.newId();
testkitState.getErrors().put(id, (Exception) throwable);
return DriverError.builder()
.data(DriverError.DriverErrorBody.builder()
.id(id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
package neo4j.org.testkit.backend.channel.handler;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelDuplexHandler;
Expand All @@ -32,14 +31,10 @@ public class TestkitRequestResponseMapperHandler extends ChannelDuplexHandler {
private final ObjectMapper objectMapper = newObjectMapper();

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String testkitMessage = (String) msg;
TestkitRequest testkitRequest;
try {
testkitRequest = objectMapper.readValue(testkitMessage, TestkitRequest.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize Testkit message", e);
}
testkitRequest = objectMapper.readValue(testkitMessage, TestkitRequest.class);
ctx.fireChannelRead(testkitRequest);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,62 @@
package neo4j.org.testkit.backend.messages;

import com.fasterxml.jackson.databind.module.SimpleModule;
import java.time.ZonedDateTime;
import java.time.LocalDate;
import java.util.List;
import neo4j.org.testkit.backend.messages.requests.deserializer.TestkitCypherDateDeserializer;
import neo4j.org.testkit.backend.messages.requests.deserializer.TestkitCypherDateTimeDeserializer;
import neo4j.org.testkit.backend.messages.requests.deserializer.TestkitCypherDurationDeserializer;
import neo4j.org.testkit.backend.messages.requests.deserializer.TestkitCypherTimeDeserializer;
import neo4j.org.testkit.backend.messages.requests.deserializer.TestkitListDeserializer;
import neo4j.org.testkit.backend.messages.requests.deserializer.types.CypherDateTime;
import neo4j.org.testkit.backend.messages.requests.deserializer.types.CypherTime;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitBookmarkSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitDateTimeValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitDateValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitDurationValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitListValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitLocalDateTimeValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitLocalTimeValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitMapValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitNodeValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitPathValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitRecordSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitRelationshipValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitTimeValueSerializer;
import neo4j.org.testkit.backend.messages.responses.serializer.TestkitValueSerializer;
import org.neo4j.driver.Bookmark;
import org.neo4j.driver.Record;
import org.neo4j.driver.Value;
import org.neo4j.driver.internal.value.DateTimeValue;
import org.neo4j.driver.internal.value.DateValue;
import org.neo4j.driver.internal.value.DurationValue;
import org.neo4j.driver.internal.value.ListValue;
import org.neo4j.driver.internal.value.LocalDateTimeValue;
import org.neo4j.driver.internal.value.LocalTimeValue;
import org.neo4j.driver.internal.value.MapValue;
import org.neo4j.driver.internal.value.NodeValue;
import org.neo4j.driver.internal.value.PathValue;
import org.neo4j.driver.internal.value.RelationshipValue;
import org.neo4j.driver.internal.value.TimeValue;
import org.neo4j.driver.types.IsoDuration;

public class TestkitModule extends SimpleModule {
public TestkitModule() {
this.addDeserializer(List.class, new TestkitListDeserializer());
this.addDeserializer(ZonedDateTime.class, new TestkitCypherDateTimeDeserializer());
this.addDeserializer(CypherDateTime.class, new TestkitCypherDateTimeDeserializer());
this.addDeserializer(CypherTime.class, new TestkitCypherTimeDeserializer());
this.addDeserializer(IsoDuration.class, new TestkitCypherDurationDeserializer());
this.addDeserializer(LocalDate.class, new TestkitCypherDateDeserializer());

this.addSerializer(Value.class, new TestkitValueSerializer());
this.addSerializer(NodeValue.class, new TestkitNodeValueSerializer());
this.addSerializer(ListValue.class, new TestkitListValueSerializer());
this.addSerializer(DateTimeValue.class, new TestkitDateTimeValueSerializer());
this.addSerializer(DateValue.class, new TestkitDateValueSerializer());
this.addSerializer(DurationValue.class, new TestkitDurationValueSerializer());
this.addSerializer(LocalDateTimeValue.class, new TestkitLocalDateTimeValueSerializer());
this.addSerializer(LocalTimeValue.class, new TestkitLocalTimeValueSerializer());
this.addSerializer(TimeValue.class, new TestkitTimeValueSerializer());
this.addSerializer(Record.class, new TestkitRecordSerializer());
this.addSerializer(MapValue.class, new TestkitMapValueSerializer());
this.addSerializer(Bookmark.class, new TestkitBookmarkSerializer());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import org.neo4j.driver.TransactionWork;
import org.neo4j.driver.async.AsyncSession;
import org.neo4j.driver.async.AsyncTransactionWork;
import org.neo4j.driver.exceptions.Neo4jException;
import org.neo4j.driver.reactive.RxTransactionWork;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -106,12 +105,10 @@ private TransactionWork<Void> handle(TestkitState testkitState, SessionHolder se
if (workThrowable instanceof ExecutionException) {
workThrowable = workThrowable.getCause();
}
if (workThrowable instanceof Neo4jException) {
throw (Neo4jException) workThrowable;
} else {
throw new RuntimeException(
"Unexpected exception occurred in transaction work function", workThrowable);
if (workThrowable instanceof RuntimeException) {
throw (RuntimeException) workThrowable;
}
throw new RuntimeException("Unexpected exception occurred in transaction work function", workThrowable);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
import org.neo4j.driver.TransactionWork;
import org.neo4j.driver.async.AsyncSession;
import org.neo4j.driver.async.AsyncTransactionWork;
import org.neo4j.driver.exceptions.Neo4jException;
import org.neo4j.driver.reactive.RxTransactionWork;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -107,12 +106,10 @@ private TransactionWork<Void> handle(TestkitState testkitState, SessionHolder se
if (workThrowable instanceof ExecutionException) {
workThrowable = workThrowable.getCause();
}
if (workThrowable instanceof Neo4jException) {
throw (Neo4jException) workThrowable;
} else {
throw new RuntimeException(
"Unexpected exception occurred in transaction work function", workThrowable);
if (workThrowable instanceof RuntimeException) {
throw (RuntimeException) workThrowable;
}
throw new RuntimeException("Unexpected exception occurred in transaction work function", workThrowable);
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package neo4j.org.testkit.backend.messages.requests;

import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import lombok.Getter;
import lombok.Setter;
import neo4j.org.testkit.backend.TestkitState;
import neo4j.org.testkit.backend.messages.responses.RunTest;
import neo4j.org.testkit.backend.messages.responses.SkipTest;
import neo4j.org.testkit.backend.messages.responses.TestkitResponse;
import reactor.core.publisher.Mono;

@Setter
@Getter
public class StartSubTest implements TestkitRequest {
interface SkipDeciderInterface {
SkipDecision check(Map<String, Object> params);
}

public static class SkipDecision {
private final boolean skipped;
private final String reason;

public SkipDecision(boolean skipped, String reason) {
this.skipped = skipped;
this.reason = reason;
}

public boolean isSkipped() {
return skipped;
}

public String getReason() {
return reason;
}

static SkipDecision ofNonSkipped() {
return new SkipDecision(false, null);
}

static SkipDecision ofSkipped(String reason) {
return new SkipDecision(true, reason);
}
}

private static final Map<String, SkipDeciderInterface> COMMON_SKIP_PATTERN_TO_CHECK = new HashMap<>();
private static final Map<String, SkipDeciderInterface> ASYNC_SKIP_PATTERN_TO_CHECK = new HashMap<>();
private static final Map<String, SkipDeciderInterface> REACTIVE_LEGACY_SKIP_PATTERN_TO_CHECK = new HashMap<>();
private static final Map<String, SkipDeciderInterface> REACTIVE_SKIP_PATTERN_TO_CHECK = new HashMap<>();

private static SkipDecision checkTzIdSupported(Map<String, Object> params) {
String tzId = (String) params.get("tz_id");
try {
ZoneId.of(tzId);
return SkipDecision.ofNonSkipped();
} catch (DateTimeException e) {
return SkipDecision.ofSkipped("Timezone not supported: " + tzId);
}
}

private static SkipDecision checkDateTimeSupported(Map<String, Object> params) {
@SuppressWarnings("unchecked")
HashMap<String, Object> dt_param = (HashMap<String, Object>) params.get("dt");
if (dt_param == null) {
throw new RuntimeException("params expected to contain 'dt'");
}
@SuppressWarnings("unchecked")
HashMap<String, Object> data = (HashMap<String, Object>) dt_param.get("data");
if (data == null) {
throw new RuntimeException("param 'dt' expected to contain 'data'");
}
Integer year = (Integer) data.get("year");
Integer month = (Integer) data.get("month");
Integer day = (Integer) data.get("day");
Integer hour = (Integer) data.get("hour");
Integer minute = (Integer) data.get("minute");
Integer second = (Integer) data.get("second");
Integer nano = (Integer) data.get("nanosecond");
Integer utcOffset = (Integer) data.get("utc_offset_s");
String tzId = (String) data.get("timezone_id");
try {
ZonedDateTime dt = ZonedDateTime.of(year, month, day, hour, minute, second, nano, ZoneId.of(tzId));
if (dt.getOffset().getTotalSeconds() != utcOffset) {
throw new DateTimeException(String.format(
"Unmatched UTC offset. TestKit expected %d, local zone db yielded %d",
utcOffset, dt.getOffset().getTotalSeconds()));
}
return SkipDecision.ofNonSkipped();
} catch (DateTimeException e) {
return SkipDecision.ofSkipped("DateTime not supported: " + e.getMessage());
}
}

static {
COMMON_SKIP_PATTERN_TO_CHECK.put(
"neo4j\\.datatypes\\.test_temporal_types\\.TestDataTypes\\.test_should_echo_all_timezone_ids",
StartSubTest::checkDateTimeSupported);
COMMON_SKIP_PATTERN_TO_CHECK.put(
"neo4j\\.datatypes\\.test_temporal_types\\.TestDataTypes\\.test_date_time_cypher_created_tz_id",
StartSubTest::checkTzIdSupported);

ASYNC_SKIP_PATTERN_TO_CHECK.putAll(COMMON_SKIP_PATTERN_TO_CHECK);

REACTIVE_LEGACY_SKIP_PATTERN_TO_CHECK.putAll(COMMON_SKIP_PATTERN_TO_CHECK);

REACTIVE_SKIP_PATTERN_TO_CHECK.putAll(COMMON_SKIP_PATTERN_TO_CHECK);
}

private StartSubTestBody data;

public static boolean decidePerSubTest(String testName) {
return skipPatternMatches(testName, COMMON_SKIP_PATTERN_TO_CHECK);
}

public static boolean decidePerSubTestAsync(String testName) {
return skipPatternMatches(testName, ASYNC_SKIP_PATTERN_TO_CHECK);
}

public static boolean decidePerSubTestReactiveLegacy(String testName) {
return skipPatternMatches(testName, REACTIVE_LEGACY_SKIP_PATTERN_TO_CHECK);
}

public static boolean decidePerSubTestReactive(String testName) {
return skipPatternMatches(testName, REACTIVE_SKIP_PATTERN_TO_CHECK);
}

private static boolean skipPatternMatches(
String testName, Map<String, SkipDeciderInterface> skipPatternToFunction) {
return skipPatternToFunction.entrySet().stream().anyMatch(entry -> testName.matches(entry.getKey()));
}

@Override
public TestkitResponse process(TestkitState testkitState) {
return createResponse(COMMON_SKIP_PATTERN_TO_CHECK);
}

@Override
public CompletionStage<TestkitResponse> processAsync(TestkitState testkitState) {
TestkitResponse testkitResponse = createResponse(ASYNC_SKIP_PATTERN_TO_CHECK);
return CompletableFuture.completedFuture(testkitResponse);
}

@Override
public Mono<TestkitResponse> processRx(TestkitState testkitState) {
TestkitResponse testkitResponse = createResponse(REACTIVE_LEGACY_SKIP_PATTERN_TO_CHECK);
return Mono.just(testkitResponse);
}

private TestkitResponse createResponse(Map<String, SkipDeciderInterface> skipPatternToCheck) {
return skipPatternToCheck.entrySet().stream()
.filter(entry -> data.getTestName().matches(entry.getKey()))
.findFirst()
.map(entry -> {
SkipDecision decision = entry.getValue().check(data.getSubtestArguments());
if (decision.isSkipped()) {
return SkipTest.builder()
.data(SkipTest.SkipTestBody.builder()
.reason(decision.getReason())
.build())
.build();
}
return RunTest.builder().build();
})
.orElse(RunTest.builder().build());
}

@Setter
@Getter
public static class StartSubTestBody {
private String testName;
private Map<String, Object> subtestArguments;
}
}
Loading