diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java index 67806aba1a..1b048c8eff 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java @@ -57,7 +57,8 @@ public class GetFeatures implements TestkitRequest "Temporary:DriverMaxConnectionPoolSize", "Temporary:ConnectionAcquisitionTimeout", "Temporary:GetConnectionPoolMetrics", - "Temporary:CypherPathAndRelationship" + "Temporary:CypherPathAndRelationship", + "Temporary:FullSummary" ) ); private static final Set SYNC_FEATURES = new HashSet<>( Arrays.asList( diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ResultConsume.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ResultConsume.java index 101c6c902e..560a150d74 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ResultConsume.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ResultConsume.java @@ -26,10 +26,22 @@ import neo4j.org.testkit.backend.messages.responses.TestkitResponse; import reactor.core.publisher.Mono; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.neo4j.driver.Query; import org.neo4j.driver.Result; import org.neo4j.driver.exceptions.NoSuchRecordException; +import org.neo4j.driver.summary.InputPosition; +import org.neo4j.driver.summary.Plan; +import org.neo4j.driver.summary.ProfiledPlan; +import org.neo4j.driver.summary.QueryType; +import org.neo4j.driver.summary.SummaryCounters; @Setter @Getter @@ -74,8 +86,50 @@ private Summary createResponse( org.neo4j.driver.summary.ResultSummary summary ) .protocolVersion( summary.server().protocolVersion() ) .agent( summary.server().agent() ) .build(); + SummaryCounters summaryCounters = summary.counters(); + Summary.Counters counters = Summary.Counters.builder() + .constraintsAdded( summaryCounters.constraintsAdded() ) + .constraintsRemoved( summaryCounters.constraintsRemoved() ) + .containsSystemUpdates( summaryCounters.containsSystemUpdates() ) + .containsUpdates( summaryCounters.containsUpdates() ) + .indexesAdded( summaryCounters.indexesAdded() ) + .indexesRemoved( summaryCounters.indexesRemoved() ) + .labelsAdded( summaryCounters.labelsAdded() ) + .labelsRemoved( summaryCounters.labelsRemoved() ) + .nodesCreated( summaryCounters.nodesCreated() ) + .nodesDeleted( summaryCounters.nodesDeleted() ) + .propertiesSet( summaryCounters.propertiesSet() ) + .relationshipsCreated( summaryCounters.relationshipsCreated() ) + .relationshipsDeleted( summaryCounters.relationshipsDeleted() ) + .systemUpdates( summaryCounters.systemUpdates() ) + .build(); + Query summaryQuery = summary.query(); + Summary.Query query = Summary.Query.builder() + .text( summaryQuery.text() ) + .parameters( summaryQuery.parameters().asMap( Function.identity(), null ) ) + .build(); + List notifications = summary.notifications().stream() + .map( s -> Summary.Notification.builder() + .code( s.code() ) + .title( s.title() ) + .description( s.description() ) + .position( toInputPosition( s.position() ) ) + .severity( s.severity() ) + .build() ) + .collect( Collectors.toList() ); Summary.SummaryBody data = Summary.SummaryBody.builder() .serverInfo( serverInfo ) + .counters( counters ) + .query( query ) + .database( summary.database().name() ) + .notifications( notifications ) + .plan( toPlan( summary.plan() ) ) + .profile( toProfile( summary.profile() ) ) + .queryType( toQueryType( summary.queryType() ) ) + .resultAvailableAfter( summary.resultAvailableAfter( TimeUnit.MILLISECONDS ) == -1 + ? null : summary.resultAvailableAfter( TimeUnit.MILLISECONDS ) ) + .resultConsumedAfter( summary.resultConsumedAfter( TimeUnit.MILLISECONDS ) == -1 + ? null : summary.resultConsumedAfter( TimeUnit.MILLISECONDS ) ) .build(); return Summary.builder() .data( data ) @@ -88,4 +142,91 @@ public static class ResultConsumeBody { private String resultId; } + + private static Summary.InputPosition toInputPosition( InputPosition position ) + { + if ( position == null ) + { + return null; + } + return Summary.InputPosition.builder() + .offset( position.offset() ) + .line( position.line() ) + .column( position.column() ) + .build(); + } + + private static Summary.Plan toPlan( Plan plan ) + { + if ( plan == null ) + { + return null; + } + Map args = new HashMap<>(); + plan.arguments().forEach( ( key, value ) -> args.put( key, value.asObject() ) ); + return Summary.Plan.builder() + .operatorType( plan.operatorType() ) + .args( args ) + .identifiers( plan.identifiers() ) + .children( plan.children().stream() + .map( ResultConsume::toPlan ) + .collect( Collectors.toList() ) ) + .build(); + } + + private static Summary.Profile toProfile( ProfiledPlan plan ) + { + if ( plan == null ) + { + return null; + } + Map args = new HashMap<>(); + plan.arguments().forEach( ( key, value ) -> args.put( key, value.asObject() ) ); + return Summary.Profile.builder() + .operatorType( plan.operatorType() ) + .args( args ) + .identifiers( plan.identifiers() ) + .dbHits( plan.dbHits() ) + .rows( plan.records() ) + .hasPageCacheStats( plan.hasPageCacheStats() ) + .pageCacheHits( plan.pageCacheHits() ) + .pageCacheMisses( plan.pageCacheMisses() ) + .pageCacheHitRatio( plan.pageCacheHitRatio() ) + .time( plan.time() ) + .children( plan.children().stream() + .map( ResultConsume::toProfile ) + .collect( Collectors.toList() ) ) + .build(); + } + + private static String toQueryType( QueryType type ) + { + if ( type == null ) + { + return null; + } + + String typeStr; + if ( type == QueryType.READ_ONLY ) + { + typeStr = "r"; + } + else if ( type == QueryType.READ_WRITE ) + { + typeStr = "rw"; + } + else if ( type == QueryType.WRITE_ONLY ) + { + typeStr = "w"; + } + else if ( type == QueryType.SCHEMA_WRITE ) + { + typeStr = "s"; + } + else + { + throw new IllegalStateException( "Unexpected query type" ); + } + return typeStr; + } } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java index 71f39e72ac..01c29f0b4d 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java @@ -35,13 +35,29 @@ @Getter public class StartTest implements TestkitRequest { + private static final Map COMMON_SKIP_PATTERN_TO_REASON = new HashMap<>(); private static final Map ASYNC_SKIP_PATTERN_TO_REASON = new HashMap<>(); private static final Map REACTIVE_SKIP_PATTERN_TO_REASON = new HashMap<>(); static { + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_invalid_query_type$", "Does not report type exception" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_no_notifications$", "An empty list is returned when there are no notifications" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_no_notification_info$", "An empty list is returned when there are no notifications" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_notifications_without_position$", "Null value is provided when position is absent" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_multiple_notifications$", "Null value is provided when position is absent" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_partial_summary_not_contains_system_updates$", "Contains updates because value is over zero" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_partial_summary_not_contains_updates$", "Contains updates because value is over zero" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_profile$", "Missing stats are reported with 0 value" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_server_info$", "Address includes domain name" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_partial_summary_contains_system_updates$", "Does not contain updates because value is zero" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_partial_summary_contains_updates$", "Does not contain updates because value is zero" ); + COMMON_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_supports_multi_db$", "Database is None" ); + + ASYNC_SKIP_PATTERN_TO_REASON.putAll( COMMON_SKIP_PATTERN_TO_REASON ); ASYNC_SKIP_PATTERN_TO_REASON.put( "^.*\\.test_should_reject_server_using_verify_connectivity_bolt_3x0$", "Does not error as expected" ); + REACTIVE_SKIP_PATTERN_TO_REASON.putAll( COMMON_SKIP_PATTERN_TO_REASON ); // Current limitations (require further investigation or bug fixing) String skipMessage = "Does not report RUN FAILURE"; REACTIVE_SKIP_PATTERN_TO_REASON.put( "^.*\\.Routing[^.]+\\.test_should_write_successfully_on_leader_switch_using_tx_function$", skipMessage ); @@ -75,31 +91,26 @@ public class StartTest implements TestkitRequest @Override public TestkitResponse process( TestkitState testkitState ) { - return RunTest.builder().build(); + return createResponse( COMMON_SKIP_PATTERN_TO_REASON ); } @Override public CompletionStage processAsync( TestkitState testkitState ) { - TestkitResponse testkitResponse = ASYNC_SKIP_PATTERN_TO_REASON - .entrySet() - .stream() - .filter( entry -> data.getTestName().matches( entry.getKey() ) ) - .findFirst() - .map( entry -> (TestkitResponse) SkipTest.builder() - .data( SkipTest.SkipTestBody.builder() - .reason( entry.getValue() ) - .build() ) - .build() ) - .orElseGet( () -> RunTest.builder().build() ); - + TestkitResponse testkitResponse = createResponse( ASYNC_SKIP_PATTERN_TO_REASON ); return CompletableFuture.completedFuture( testkitResponse ); } @Override public Mono processRx( TestkitState testkitState ) { - TestkitResponse testkitResponse = REACTIVE_SKIP_PATTERN_TO_REASON + TestkitResponse testkitResponse = createResponse( REACTIVE_SKIP_PATTERN_TO_REASON ); + return Mono.fromCompletionStage( CompletableFuture.completedFuture( testkitResponse ) ); + } + + private TestkitResponse createResponse( Map skipPatternToReason ) + { + return skipPatternToReason .entrySet() .stream() .filter( entry -> data.getTestName().matches( entry.getKey() ) ) @@ -110,8 +121,6 @@ public Mono processRx( TestkitState testkitState ) .build() ) .build() ) .orElseGet( () -> RunTest.builder().build() ); - - return Mono.fromCompletionStage( CompletableFuture.completedFuture( testkitResponse ) ); } @Setter diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/Summary.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/Summary.java index a8d14aa1e2..d651f1c6cd 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/Summary.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/Summary.java @@ -20,6 +20,12 @@ import lombok.Builder; import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import java.util.List; +import java.util.Map; + +import org.neo4j.driver.Value; @Getter @Builder @@ -38,6 +44,24 @@ public String testkitName() public static class SummaryBody { private ServerInfo serverInfo; + + private Counters counters; + + private Query query; + + private String database; + + private List notifications; + + private Plan plan; + + private Profile profile; + + private String queryType; + + private Long resultAvailableAfter; + + private Long resultConsumedAfter; } @Getter @@ -50,4 +74,104 @@ public static class ServerInfo private String agent; } + + @Getter + @Builder + public static class Counters + { + private int constraintsAdded; + + private int constraintsRemoved; + + private boolean containsSystemUpdates; + + private boolean containsUpdates; + + private int indexesAdded; + + private int indexesRemoved; + + private int labelsAdded; + + private int labelsRemoved; + + private int nodesCreated; + + private int nodesDeleted; + + private int propertiesSet; + + private int relationshipsCreated; + + private int relationshipsDeleted; + + private int systemUpdates; + } + + @Getter + @Builder + public static class Query + { + private String text; + + private Map parameters; + } + + @Getter + @Builder + public static class Notification + { + private String code; + + private String title; + + private String description; + + private InputPosition position; + + private String severity; + } + + @Getter + @Builder + public static class InputPosition + { + private int offset; + + private int line; + + private int column; + } + + @Getter + @SuperBuilder + public static class Plan + { + private String operatorType; + + private Map args; + + private List identifiers; + + private List children; + } + + @Getter + @SuperBuilder + public static class Profile extends Plan + { + private long dbHits; + + private long rows; + + private boolean hasPageCacheStats; + + private long pageCacheHits; + + private long pageCacheMisses; + + private double pageCacheHitRatio; + + private long time; + } }