From c8910ce30158de7e6adfdd25761c4b97895387cb Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 2 Oct 2025 20:09:55 +0000 Subject: [PATCH] TestKit: time warp backend --- README.md | 2 +- hooks/pre-commit | 9 +- neo4j/driver_testkit.go | 2 +- neo4j/internal/pool/pool_test.go | 2 +- neo4j/internal/retry/state_test.go | 2 +- neo4j/internal/router/router_test.go | 2 +- neo4j/internal/router/router_testkit.go | 2 +- neo4j/internal/time/time.go | 2 +- neo4j/internal/time/time_mockable.go | 2 +- testkit-backend/backend.go | 170 +++++++----------------- testkit-backend/build_tags.sh | 24 ++++ testkit-backend/extras.go | 111 ++++++++++++++++ testkit-backend/util.go | 27 ++++ testkit-backend/z_final_init.go | 157 ++++++++++++++++++++++ testkit/Dockerfile | 21 ++- testkit/backend.py | 3 +- testkit/build.py | 13 +- testkit/common.py | 6 + testkit/unittests.py | 2 +- 19 files changed, 418 insertions(+), 141 deletions(-) create mode 100755 testkit-backend/build_tags.sh create mode 100644 testkit-backend/extras.go create mode 100644 testkit-backend/util.go create mode 100644 testkit-backend/z_final_init.go diff --git a/README.md b/README.md index 93d26795..a4de0055 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ modules](https://go.dev/ref/mod) for dependency resolution. You can run unit tests as follows: ```shell -go test -tags internal_testkit,internal_time_mock -short ./... +go test -tags internal_neo4j_go_driver_testkit,internal_neo4j_go_driver_time_mock -short ./... ``` ### Integration and Benchmark Testing diff --git a/hooks/pre-commit b/hooks/pre-commit index d14992dc..2d19211c 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -19,14 +19,19 @@ if verlt $go_version "1.24.0"; then exit 1 fi +tags="\ +internal_neo4j_go_driver_testkit,\ +internal_neo4j_go_driver_time_mock\ +" + echo "# pre-commit hook" printf '%-15s' "## staticcheck " cd "$(mktemp -d)" && go install honnef.co/go/tools/cmd/staticcheck@"${staticcheck_version}" && cd - > /dev/null -"${GOBIN:-$(go env GOPATH)/bin}"/staticcheck -tags internal_testkit,internal_time_mock ./... +"${GOBIN:-$(go env GOPATH)/bin}"/staticcheck -tags "$tags" ./... echo "✅" printf '%-15s' "## go vet " -go vet -tags internal_testkit,internal_time_mock ./... +go vet -tags "$tags" ./... echo "✅" printf '%-15s' "## go test " diff --git a/neo4j/driver_testkit.go b/neo4j/driver_testkit.go index 8d14d16b..65cf4ab2 100644 --- a/neo4j/driver_testkit.go +++ b/neo4j/driver_testkit.go @@ -1,4 +1,4 @@ -//go:build internal_testkit && internal_time_mock +//go:build internal_neo4j_go_driver_testkit && internal_neo4j_go_driver_time_mock /* * Copyright (c) "Neo4j" diff --git a/neo4j/internal/pool/pool_test.go b/neo4j/internal/pool/pool_test.go index 3891729f..0c1d7b80 100644 --- a/neo4j/internal/pool/pool_test.go +++ b/neo4j/internal/pool/pool_test.go @@ -1,4 +1,4 @@ -//go:build internal_time_mock +//go:build internal_neo4j_go_driver_time_mock /* * Copyright (c) "Neo4j" diff --git a/neo4j/internal/retry/state_test.go b/neo4j/internal/retry/state_test.go index 5d27e0ff..8e403655 100644 --- a/neo4j/internal/retry/state_test.go +++ b/neo4j/internal/retry/state_test.go @@ -1,4 +1,4 @@ -//go:build internal_time_mock +//go:build internal_neo4j_go_driver_time_mock /* * Copyright (c) "Neo4j" diff --git a/neo4j/internal/router/router_test.go b/neo4j/internal/router/router_test.go index f383030e..d08d9b0d 100644 --- a/neo4j/internal/router/router_test.go +++ b/neo4j/internal/router/router_test.go @@ -1,4 +1,4 @@ -//go:build internal_time_mock +//go:build internal_neo4j_go_driver_time_mock /* * Copyright (c) "Neo4j" diff --git a/neo4j/internal/router/router_testkit.go b/neo4j/internal/router/router_testkit.go index 52ad50d1..7db128a2 100644 --- a/neo4j/internal/router/router_testkit.go +++ b/neo4j/internal/router/router_testkit.go @@ -1,4 +1,4 @@ -//go:build internal_testkit +//go:build internal_neo4j_go_driver_testkit /* * Copyright (c) "Neo4j" diff --git a/neo4j/internal/time/time.go b/neo4j/internal/time/time.go index 12222763..aaae33e0 100644 --- a/neo4j/internal/time/time.go +++ b/neo4j/internal/time/time.go @@ -1,4 +1,4 @@ -//go:build !internal_time_mock +//go:build !internal_neo4j_go_driver_time_mock /* * Copyright (c) "Neo4j" diff --git a/neo4j/internal/time/time_mockable.go b/neo4j/internal/time/time_mockable.go index cec855ad..38675437 100644 --- a/neo4j/internal/time/time_mockable.go +++ b/neo4j/internal/time/time_mockable.go @@ -1,4 +1,4 @@ -//go:build internal_time_mock +//go:build internal_neo4j_go_driver_time_mock /* * Copyright (c) "Neo4j" diff --git a/testkit-backend/backend.go b/testkit-backend/backend.go index 753b7dfc..44a09b46 100644 --- a/testkit-backend/backend.go +++ b/testkit-backend/backend.go @@ -67,6 +67,7 @@ type backend struct { clientCertificateProviders map[string]auth.ClientCertificateProvider resolvedClientCertificates map[string]auth.ClientCertificate closed bool + extrasData map[string]any } // To implement transactional functions a bit of extra state is needed on the @@ -164,6 +165,7 @@ func newBackend(rd *bufio.Reader, wr io.Writer) *backend { clientCertificateProviders: make(map[string]auth.ClientCertificateProvider), resolvedClientCertificates: make(map[string]auth.ClientCertificate), closed: false, + extrasData: newBackendExtraData(), } } @@ -661,6 +663,13 @@ func (b *backend) handleRequest(req map[string]any) { if data["connectionTimeoutMs"] != nil { c.SocketConnectTimeout = time.Millisecond * time.Duration(asInt64(data["connectionTimeoutMs"].(json.Number))) } + for _, configurer := range extrasDriverConfigurers { + err = configurer(b, data, c) + if err != nil { + b.writeError(err) + return + } + } if data["notificationsMinSeverity"] != nil { minSeverity, err := mapNotificationMinSeverityLevel(data["notificationsMinSeverity"].(string)) if err != nil { @@ -706,6 +715,13 @@ func (b *backend) handleRequest(req map[string]any) { if data["domainNameResolverRegistered"] != nil && data["domainNameResolverRegistered"].(bool) { neo4j.RegisterDnsResolver(driver, b.dnsResolverFunction()) } + for _, handler := range extrasNewDriverHandlers { + err = handler(b, data, driver) + if err != nil { + b.writeError(err) + return + } + } idKey := b.nextId() b.drivers[idKey] = driver @@ -812,6 +828,13 @@ func (b *backend) handleRequest(req map[string]any) { } config.Auth = &token } + for _, configurer := range extrasExecuteQueryConfigurers { + err := configurer(b, data, config) + if err != nil { + b.writeError(err) + return + } + } }) } @@ -900,6 +923,13 @@ func (b *backend) handleRequest(req map[string]any) { } sessionConfig.Auth = &authToken } + for _, configurer := range extrasSessionConfigurers { + err = configurer(b, data, &sessionConfig) + if err != nil { + b.writeError(err) + return + } + } session := driver.NewSession(ctx, sessionConfig) idKey := b.nextId() b.sessionStates[idKey] = &sessionState{session: session} @@ -1329,88 +1359,7 @@ func (b *backend) handleRequest(req map[string]any) { case "GetFeatures": b.writeResponse("FeatureList", map[string]any{ - "features": []string{ - // === FUNCTIONAL FEATURES === - "Feature:API:BookmarkManager", - "Feature:API:ConnectionAcquisitionTimeout", - "Feature:API:Driver.ExecuteQuery", - "Feature:API:Driver.ExecuteQuery:WithAuth", - "Feature:API:Driver:GetServerInfo", - "Feature:API:Driver.IsEncrypted", - "Feature:API:Driver:MaxConnectionLifetime", - "Feature:API:Driver:NotificationsConfig", - "Feature:API:Driver.VerifyAuthentication", - "Feature:API:Driver.VerifyConnectivity", - //"Feature:API:Driver.SupportsSessionAuth", - "Feature:API:Liveness.Check", - "Feature:API:Result.List", - "Feature:API:Result.Peek", - //"Feature:API:Result.Single", - //"Feature:API:Result.SingleOptional", - "Feature:API:RetryableExceptions", - "Feature:API:Session:AuthConfig", - "Feature:API:Session:NotificationsConfig", - "Feature:API:SSLClientCertificate", - //"Feature:API:SSLConfig", - //"Feature:API:SSLSchemes", - "Feature:API:Summary:GqlStatusObjects", - "Feature:API:Type.Spatial", - "Feature:API:Type.Temporal", - "Feature:API:Type.Vector", - "Feature:API:Type.UnsupportedType", - "Feature:Auth:Bearer", - "Feature:Auth:Custom", - "Feature:Auth:Kerberos", - "Feature:Auth:Managed", - "Feature:Bolt:3.0", - "Feature:Bolt:4.2", - "Feature:Bolt:4.3", - "Feature:Bolt:4.4", - "Feature:Bolt:5.0", - "Feature:Bolt:5.1", - "Feature:Bolt:5.2", - "Feature:Bolt:5.3", - "Feature:Bolt:5.4", - "Feature:Bolt:5.5", - "Feature:Bolt:5.6", - "Feature:Bolt:5.7", - "Feature:Bolt:5.8", - "Feature:Bolt:6.0", - "Feature:Bolt:Patch:UTC", - "Feature:Bolt:HandshakeManifestV1", - "Feature:Impersonation", - //"Feature:TLS:1.1", - "Feature:TLS:1.2", - "Feature:TLS:1.3", - - // === OPTIMIZATIONS === - "AuthorizationExpiredTreatment", - "Optimization:AuthPipelining", - "Optimization:ConnectionReuse", - "Optimization:EagerTransactionBegin", - "Optimization:ExecuteQueryPipelining", - "Optimization:HomeDatabaseCache", - "Optimization:HomeDbCacheBasicPrincipalIsImpersonatedUser", - "Optimization:ImplicitDefaultArguments", - "Optimization:MinimalBookmarksSet", - "Optimization:MinimalResets", - //"Optimization:MinimalVerifyAuthentication", - "Optimization:PullPipelining", - //"Optimization:ResultListFetchAll", - - // === IMPLEMENTATION DETAILS === - "Detail:ClosedDriverIsEncrypted", - "Detail:DefaultSecurityConfigValueEquality", - //"Detail:NumberIsNumber", - - // === CONFIGURATION HINTS (BOLT 4.3+) === - "ConfHint:connection.recv_timeout_seconds", - - // === BACKEND FEATURES FOR TESTING === - "Backend:MockTime", - "Backend:RTFetch", - "Backend:RTForceUpdate", - }, + "features": features, }) case "StartTest": @@ -1436,7 +1385,11 @@ func (b *backend) handleRequest(req map[string]any) { b.writeResponse("RunTest", nil) default: - b.writeError(errors.New("Unknown request: " + name)) + if extraHandler, ok := extrasRequestHandlers[name]; ok { + extraHandler(b, data) + } else { + b.writeError(errors.New("Unknown request: " + name)) + } } } @@ -1518,7 +1471,7 @@ func (b *backend) writeRecord(result neo4j.Result, record *neo4j.Record, expectR } func mustSkip(testName string) (string, bool) { - skippedTests := testSkips() + skippedTests := testSkips for testPattern, exclusionReason := range skippedTests { if matches(testPattern, testName) { return exclusionReason, true @@ -1529,7 +1482,10 @@ func mustSkip(testName string) (string, bool) { func mustSkipSubTest(testName string, arguments map[string]any) (string, bool) { if strings.Contains(testName, "test_should_echo_all_timezone_ids") { - return mustSkipTimeZoneSubTest(arguments) + return mustSkipTimeZoneEchoSubTest(arguments) + } + if strings.Contains(testName, "test_date_time_cypher_created_tz_id") { + return mustSkipTimeZoneCypherSubTest(arguments) } return "", false } @@ -1760,40 +1716,7 @@ func firstRecordInvalidValue(record *db.Record) *neo4j.InvalidValue { return nil } -// you can use '*' as wildcards anywhere in the qualified test name (useful to exclude a whole class e.g.) -func testSkips() map[string]string { - return map[string]string{ - // Won't fix - accepted/idiomatic behavioral differences - "stub.iteration.test_result_scope.TestResultScope.*": "Won't fix - Results are always valid but don't return records when out of scope", - "stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_routing_fail_when_no_reader_are_available": "Won't fix - Go driver retries routing table when no readers are available", - "stub.connectivity_check.test_verify_connectivity.TestVerifyConnectivity.test_routing_fail_when_no_reader_are_available": "Won't fix - Go driver retries routing table when no readers are available", - "stub.driver_parameters.test_connection_acquisition_timeout_ms.TestConnectionAcquisitionTimeoutMs.test_does_not_encompass_router_*": "Won't fix - ConnectionAcquisitionTimeout spans the whole process including db resolution, RT updates, connection acquisition from the pool, and creation of new connections.", - "stub.driver_parameters.test_connection_acquisition_timeout_ms.TestConnectionAcquisitionTimeoutMs.test_router_handshake_has_own_timeout_*": "Won't fix - ConnectionAcquisitionTimeout spans the whole process including db resolution, RT updates, connection acquisition from the pool, and creation of new connections.", - "stub.routing.test_routing_v*.RoutingV*.test_should_successfully_check_if_support_for_multi_db_is_available": "Won't fix - driver.SupportsMultiDb() is not implemented", - "stub.routing.test_no_routing_v*.NoRoutingV*.test_should_check_multi_db_support": "Won't fix - driver.SupportsMultiDb() is not implemented", - "stub.routing.test_routing_v3.RoutingV3.test_should_fail_discovery_when_router_fails_with_procedure_not_found_code": "Won't fix - only Bolt 3 affected (not officially supported by this driver) + this is only a difference in how errors are surfaced", - "stub.routing.test_routing_v3.RoutingV3.test_should_fail_when_writing_on_unexpectedly_interrupting_writer_on_pull_using_tx_run": "Won't fix - only Bolt 3 affected (not officially supported by this driver): broken servers are not removed from routing table", - "stub.routing.test_routing_v3.RoutingV3.test_should_fail_when_writing_on_unexpectedly_interrupting_writer_on_run_using_tx_run": "Won't fix - only Bolt 3 affected (not officially supported by this driver): broken servers are not removed from routing table", - "stub.routing.test_routing_v3.RoutingV3.test_should_fail_when_writing_on_unexpectedly_interrupting_writer_using_tx_run": "Won't fix - only Bolt 3 affected (not officially supported by this driver): broken servers are not removed from routing table", - - // To fix/to decide whether to fix - "stub.routing.*.*.test_should_successfully_acquire_rt_when_router_ip_changes": "Backend lacks custom DNS resolution and Go driver RT discovery differs.", - "stub.routing.test_routing_v*.RoutingV*.test_should_revert_to_initial_router_if_known_router_throws_protocol_errors": "Driver always uses configured URL first and custom resolver only if that fails", - "stub.routing.test_routing_v*.RoutingV*.test_should_request_rt_from_all_initial_routers_until_successful_on_authorization_expired": "Driver always uses configured URL first and custom resolver only if that fails", - "stub.routing.test_routing_v*.RoutingV*test_should_request_rt_from_all_initial_routers_until_successful_on_unknown_failure": "Driver always uses configured URL first and custom resolver only if that fails", - "stub.routing.test_routing_v*.RoutingV*.test_should_read_successfully_from_reachable_db_after_trying_unreachable_db": "Driver retries to fetch a routing table up to 100 times if it's empty", - "stub.routing.test_routing_v*.RoutingV*.test_should_write_successfully_after_leader_switch_using_tx_run": "Driver retries to fetch a routing table up to 100 times if it's empty", - "stub.routing.test_routing_v*.RoutingV*.test_should_fail_when_writing_without_writers_using_session_run": "Driver retries to fetch a routing table up to 100 times if it's empty", - "stub.routing.test_routing_v*.RoutingV*.test_should_accept_routing_table_without_writers_and_then_rediscover": "Driver retries to fetch a routing table up to 100 times if it's empty", - "stub.routing.test_routing_v*.RoutingV*.test_should_fail_on_routing_table_with_no_reader": "Driver retries to fetch a routing table up to 100 times if it's empty", - "stub.routing.test_routing_v*.RoutingV*.test_should_fail_discovery_when_router_fails_with_unknown_code": "Unify: other drivers have a list of fast failing errors during discover: on anything else, the driver will try the next router", - "stub.routing.test_routing_v*.RoutingV*.test_should_drop_connections_failing_liveness_check": "Liveness check error handling is not (yet) unified: https://github.com/neo-technology/drivers-adr/pull/83", - "stub.*.test_0_timeout": "Fixme: driver omits 0 as tx timeout value", - "stub.summary.test_summary.TestSummaryBasicInfo*.test_server_info": "pending unification: should the server address be pre or post DNS resolution?", - } -} - -func mustSkipTimeZoneSubTest(arguments map[string]any) (string, bool) { +func mustSkipTimeZoneEchoSubTest(arguments map[string]any) (string, bool) { rawDateTime := arguments["dt"].(map[string]any) dateTimeData := rawDateTime["data"].(map[string]any) timeZoneName := dateTimeData["timezone_id"].(string) @@ -1820,6 +1743,15 @@ func mustSkipTimeZoneSubTest(arguments map[string]any) (string, bool) { return "", false } +func mustSkipTimeZoneCypherSubTest(arguments map[string]any) (string, bool) { + timeZoneName := arguments["tz_id"].(string) + _, err := time.LoadLocation(timeZoneName) + if err != nil { + return fmt.Sprintf("time zone not supported: %s", err), true + } + return "", false +} + // some TestKit tests send large integer values which require to configure // the JSON deserializer to use json.Number instead of float64 (lossy conversions // would happen otherwise) diff --git a/testkit-backend/build_tags.sh b/testkit-backend/build_tags.sh new file mode 100755 index 00000000..49ccae4f --- /dev/null +++ b/testkit-backend/build_tags.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +verlte() { + [ "$1" = "$(echo -e "$1\n$2" | sort -V | head -n1)" ] +} + +verlt() { + [ "$1" = "$2" ] && return 1 || verlte "$1" "$2" +} + +version="${1:-''}" + +if [ -z "$version" ]; then + # choose a version bigger than any version checked below + # => next major version, at which point we can clean-up this script as it only needs to support the current major + version="7.0.0" +elif [ "$(echo "$version" | cut -d "." -f 1)" != "6" ]; then + echo "Script only works for 6.x" >&2 + exit 1 +fi + +echo $tags diff --git a/testkit-backend/extras.go b/testkit-backend/extras.go new file mode 100644 index 00000000..52b33c48 --- /dev/null +++ b/testkit-backend/extras.go @@ -0,0 +1,111 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +import ( + "github.com/neo4j/neo4j-go-driver/v6/neo4j" + "github.com/neo4j/neo4j-go-driver/v6/neo4j/config" +) + +type extrasRequestHandlerFunc = func(backend *backend, data map[string]any) +type extrasDriverConfigFunc = func(backend *backend, data map[string]any, config *config.Config) error +type extrasNewDriverHandlerFunc = func(backend *backend, data map[string]any, driver neo4j.Driver) error +type extrasExecuteQueryConfigFunc = func(backend *backend, data map[string]any, config *neo4j.ExecuteQueryConfiguration) error +type extrasSessionConfigFunc = func(backend *backend, data map[string]any, config *neo4j.SessionConfig) error + +var extrasBlockedTestKitFeatures = make(map[string]any) +var extrasTestSkips = make(map[string]string) +var extrasRequestHandlers = make(map[string]extrasRequestHandlerFunc) +var extrasDriverConfigurers = make([]extrasDriverConfigFunc, 0) +var extrasNewDriverHandlers = make([]extrasNewDriverHandlerFunc, 0) +var extrasExecuteQueryConfigurers = make([]extrasExecuteQueryConfigFunc, 0) +var extrasSessionConfigurers = make([]extrasSessionConfigFunc, 0) + +type ExtrasRegisterEntry struct { + newBackendExtraData func() any + extraBlockedTestKitFeatures []string + extraTestSkips map[string]string + extraRequestHandlers map[string]extrasRequestHandlerFunc + extraDriverConfigurer extrasDriverConfigFunc + extraNewDriverHandler extrasNewDriverHandlerFunc + extraExecuteQueryConfigurer extrasExecuteQueryConfigFunc + extraSessionConfigurer extrasSessionConfigFunc +} + +var extrasRegister = make(map[string]ExtrasRegisterEntry) + +//lint:ignore U1000 Infrastructure for future expansion +func registerExtra(name string, entry ExtrasRegisterEntry) { + if _, ok := extrasRegister[name]; ok { + panic("Extra name '" + name + "' already registered") + } + extrasRegister[name] = entry + + for _, feature := range entry.extraBlockedTestKitFeatures { + if _, ok := extrasBlockedTestKitFeatures[feature]; ok { + panic("Extra TestKit feature '" + feature + "' already blocked") + } + extrasBlockedTestKitFeatures[feature] = struct{}{} + } + + for testPattern, reason := range entry.extraTestSkips { + if _, ok := extrasTestSkips[testPattern]; ok { + panic("Extra test reason '" + testPattern + "' already registered") + } + extrasTestSkips[testPattern] = reason + } + + for msgName, handler := range entry.extraRequestHandlers { + if _, ok := extrasRequestHandlers[msgName]; ok { + panic("Extra request handler '" + msgName + "' already registered") + } + extrasRequestHandlers[msgName] = handler + } + + if entry.extraDriverConfigurer != nil { + extrasDriverConfigurers = append(extrasDriverConfigurers, entry.extraDriverConfigurer) + } + + if entry.extraNewDriverHandler != nil { + extrasNewDriverHandlers = append(extrasNewDriverHandlers, entry.extraNewDriverHandler) + } + + if entry.extraExecuteQueryConfigurer != nil { + extrasExecuteQueryConfigurers = append(extrasExecuteQueryConfigurers, entry.extraExecuteQueryConfigurer) + } + + if entry.extraSessionConfigurer != nil { + extrasSessionConfigurers = append(extrasSessionConfigurers, entry.extraSessionConfigurer) + } +} + +func newBackendExtraData() map[string]any { + extraData := make(map[string]any, len(extrasRegister)) + for key, entry := range extrasRegister { + if entry.newBackendExtraData == nil { + continue + } + extraData[key] = entry.newBackendExtraData() + } + return extraData +} + +//lint:ignore U1000 Infrastructure for future expansion +func getBackendExtraData(backend *backend, name string) any { + return backend.extrasData[name] +} diff --git a/testkit-backend/util.go b/testkit-backend/util.go new file mode 100644 index 00000000..e57babf5 --- /dev/null +++ b/testkit-backend/util.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +func contains[T comparable](slice []T, item T) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} diff --git a/testkit-backend/z_final_init.go b/testkit-backend/z_final_init.go new file mode 100644 index 00000000..474a3ced --- /dev/null +++ b/testkit-backend/z_final_init.go @@ -0,0 +1,157 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +var testSkips map[string]string + +func init() { + // you can use '*' as wildcards anywhere in the qualified test name (useful to exclude a whole class e.g.) + testSkips = map[string]string{ + // Won't fix - accepted/idiomatic behavioral differences + "stub.iteration.test_result_scope.TestResultScope.*": "Won't fix - Results are always valid but don't return records when out of scope", + "stub.connectivity_check.test_get_server_info.TestGetServerInfo.test_routing_fail_when_no_reader_are_available": "Won't fix - Go driver retries routing table when no readers are available", + "stub.connectivity_check.test_verify_connectivity.TestVerifyConnectivity.test_routing_fail_when_no_reader_are_available": "Won't fix - Go driver retries routing table when no readers are available", + "stub.driver_parameters.test_connection_acquisition_timeout_ms.TestConnectionAcquisitionTimeoutMs.test_does_not_encompass_router_*": "Won't fix - ConnectionAcquisitionTimeout spans the whole process including db resolution, RT updates, connection acquisition from the pool, and creation of new connections.", + "stub.driver_parameters.test_connection_acquisition_timeout_ms.TestConnectionAcquisitionTimeoutMs.test_router_handshake_has_own_timeout_*": "Won't fix - ConnectionAcquisitionTimeout spans the whole process including db resolution, RT updates, connection acquisition from the pool, and creation of new connections.", + "stub.routing.test_routing_v*.RoutingV*.test_should_successfully_check_if_support_for_multi_db_is_available": "Won't fix - driver.SupportsMultiDb() is not implemented", + "stub.routing.test_no_routing_v*.NoRoutingV*.test_should_check_multi_db_support": "Won't fix - driver.SupportsMultiDb() is not implemented", + "stub.routing.test_routing_v3.RoutingV3.test_should_fail_discovery_when_router_fails_with_procedure_not_found_code": "Won't fix - only Bolt 3 affected (not officially supported by this driver) + this is only a difference in how errors are surfaced", + "stub.routing.test_routing_v3.RoutingV3.test_should_fail_when_writing_on_unexpectedly_interrupting_writer_on_pull_using_tx_run": "Won't fix - only Bolt 3 affected (not officially supported by this driver): broken servers are not removed from routing table", + "stub.routing.test_routing_v3.RoutingV3.test_should_fail_when_writing_on_unexpectedly_interrupting_writer_on_run_using_tx_run": "Won't fix - only Bolt 3 affected (not officially supported by this driver): broken servers are not removed from routing table", + "stub.routing.test_routing_v3.RoutingV3.test_should_fail_when_writing_on_unexpectedly_interrupting_writer_using_tx_run": "Won't fix - only Bolt 3 affected (not officially supported by this driver): broken servers are not removed from routing table", + + // To fix/to decide whether to fix + "stub.routing.*.*.test_should_successfully_acquire_rt_when_router_ip_changes": "Backend lacks custom DNS resolution and Go driver RT discovery differs.", + "stub.routing.test_routing_v*.RoutingV*.test_should_revert_to_initial_router_if_known_router_throws_protocol_errors": "Driver always uses configured URL first and custom resolver only if that fails", + "stub.routing.test_routing_v*.RoutingV*.test_should_request_rt_from_all_initial_routers_until_successful_on_authorization_expired": "Driver always uses configured URL first and custom resolver only if that fails", + "stub.routing.test_routing_v*.RoutingV*test_should_request_rt_from_all_initial_routers_until_successful_on_unknown_failure": "Driver always uses configured URL first and custom resolver only if that fails", + "stub.routing.test_routing_v*.RoutingV*.test_should_read_successfully_from_reachable_db_after_trying_unreachable_db": "Driver retries to fetch a routing table up to 100 times if it's empty", + "stub.routing.test_routing_v*.RoutingV*.test_should_write_successfully_after_leader_switch_using_tx_run": "Driver retries to fetch a routing table up to 100 times if it's empty", + "stub.routing.test_routing_v*.RoutingV*.test_should_fail_when_writing_without_writers_using_session_run": "Driver retries to fetch a routing table up to 100 times if it's empty", + "stub.routing.test_routing_v*.RoutingV*.test_should_accept_routing_table_without_writers_and_then_rediscover": "Driver retries to fetch a routing table up to 100 times if it's empty", + "stub.routing.test_routing_v*.RoutingV*.test_should_fail_on_routing_table_with_no_reader": "Driver retries to fetch a routing table up to 100 times if it's empty", + "stub.routing.test_routing_v*.RoutingV*.test_should_fail_discovery_when_router_fails_with_unknown_code": "Unify: other drivers have a list of fast failing errors during discover: on anything else, the driver will try the next router", + "stub.routing.test_routing_v*.RoutingV*.test_should_drop_connections_failing_liveness_check": "Liveness check error handling is not (yet) unified: https://github.com/neo-technology/drivers-adr/pull/83", + "stub.*.test_0_timeout": "Fixme: driver omits 0 as tx timeout value", + "stub.summary.test_summary.TestSummaryBasicInfo*.test_server_info": "pending unification: should the server address be pre or post DNS resolution?", + } + for testPattern, reason := range extrasTestSkips { + if _, ok := extrasTestSkips[testPattern]; ok { + panic("Fixed test skip colliding with extra skip: '" + testPattern + "'") + } + testSkips[testPattern] = reason + } +} + +var features []string + +func init() { + allFeatures := []string{ + // === FUNCTIONAL FEATURES === + "Feature:API:BookmarkManager", + "Feature:API:ConnectionAcquisitionTimeout", + "Feature:API:Driver.ExecuteQuery", + "Feature:API:Driver.ExecuteQuery:WithAuth", + "Feature:API:Driver:GetServerInfo", + "Feature:API:Driver.IsEncrypted", + "Feature:API:Driver:MaxConnectionLifetime", + "Feature:API:Driver:NotificationsConfig", + "Feature:API:Driver.VerifyAuthentication", + "Feature:API:Driver.VerifyConnectivity", + //"Feature:API:Driver.SupportsSessionAuth", + "Feature:API:Liveness.Check", + "Feature:API:Result.List", + "Feature:API:Result.Peek", + //"Feature:API:Result.Single", + //"Feature:API:Result.SingleOptional", + "Feature:API:RetryableExceptions", + "Feature:API:Session:AuthConfig", + "Feature:API:Session:NotificationsConfig", + "Feature:API:SSLClientCertificate", + //"Feature:API:SSLConfig", + //"Feature:API:SSLSchemes", + "Feature:API:Summary:GqlStatusObjects", + "Feature:API:Type.Spatial", + "Feature:API:Type.Temporal", + "Feature:API:Type.Vector", + "Feature:API:Type.UnsupportedType", + "Feature:Auth:Bearer", + "Feature:Auth:Custom", + "Feature:Auth:Kerberos", + "Feature:Auth:Managed", + "Feature:Bolt:3.0", + "Feature:Bolt:4.2", + "Feature:Bolt:4.3", + "Feature:Bolt:4.4", + "Feature:Bolt:5.0", + "Feature:Bolt:5.1", + "Feature:Bolt:5.2", + "Feature:Bolt:5.3", + "Feature:Bolt:5.4", + "Feature:Bolt:5.5", + "Feature:Bolt:5.6", + "Feature:Bolt:5.7", + "Feature:Bolt:5.8", + "Feature:Bolt:6.0", + "Feature:Bolt:Patch:UTC", + "Feature:Bolt:HandshakeManifestV1", + "Feature:Impersonation", + //"Feature:TLS:1.1", + "Feature:TLS:1.2", + "Feature:TLS:1.3", + + // === OPTIMIZATIONS === + "AuthorizationExpiredTreatment", + "Optimization:AuthPipelining", + "Optimization:ConnectionReuse", + "Optimization:EagerTransactionBegin", + "Optimization:ExecuteQueryPipelining", + "Optimization:HomeDatabaseCache", + "Optimization:HomeDbCacheBasicPrincipalIsImpersonatedUser", + "Optimization:ImplicitDefaultArguments", + "Optimization:MinimalBookmarksSet", + "Optimization:MinimalResets", + //"Optimization:MinimalVerifyAuthentication", + "Optimization:PullPipelining", + //"Optimization:ResultListFetchAll", + + // === IMPLEMENTATION DETAILS === + "Detail:ClosedDriverIsEncrypted", + "Detail:DefaultSecurityConfigValueEquality", + //"Detail:NumberIsNumber", + + // === CONFIGURATION HINTS (BOLT 4.3+) === + "ConfHint:connection.recv_timeout_seconds", + + // === BACKEND FEATURES FOR TESTING === + "Backend:MockTime", + "Backend:RTFetch", + "Backend:RTForceUpdate", + } + features = make([]string, 0, len(allFeatures)) + for _, feature := range allFeatures { + if _, ok := extrasBlockedTestKitFeatures[feature]; !ok { + features = append(features, feature) + } + } + for blockedFeature := range extrasBlockedTestKitFeatures { + if !contains(allFeatures, blockedFeature) { + panic("Extra is trying to block an unsupported feature: '" + blockedFeature + "'") + } + } +} diff --git a/testkit/Dockerfile b/testkit/Dockerfile index f77cac35..346ad010 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -1,4 +1,5 @@ -FROM ubuntu:20.04 +ARG TIME_WARP="" +FROM ubuntu:20.04 AS base ARG go_version_min=1.24.6 ARG go_version_default=1.24.6 @@ -27,6 +28,18 @@ ENV GOMINBIN=go${go_version_min} # Install minimum version of Go alongside latest RUN go install golang.org/dl/go${go_version_min}@latest && go${go_version_min} download + +FROM base AS backend-timewarp +ARG TIME_WARP +WORKDIR /home/root/testkit +COPY testkit-backend . +RUN go mod init github.com/neo4j/neo4j-go-driver/testkit-backend/v2 \ + && go get github.com/neo4j/neo4j-go-driver/v6@v${TIME_WARP} \ + && echo tags: $(./build_tags.sh ${TIME_WARP}) \ + && go build -tags "$(./build_tags.sh ${TIME_WARP})" -o backend +ENTRYPOINT ["./backend"] + +FROM base AS backend # Install python stuff ENV PYTHON=python3 @@ -34,3 +47,9 @@ ENV PYTHON=python3 # Assumes Linux Debian based image. COPY CAs/* /usr/local/share/ca-certificates/ RUN update-ca-certificates + +FROM backend${TIME_WARP:+"-timewarp"} AS final +ARG TIME_WARP +ENV DRIVER_TIME_WARP=$TIME_WARP +WORKDIR /home/root/testkit +EXPOSE 9876/tcp diff --git a/testkit/backend.py b/testkit/backend.py index fc4b2476..b84186df 100644 --- a/testkit/backend.py +++ b/testkit/backend.py @@ -19,7 +19,8 @@ run_go( [ "run", "-tags", - "internal_testkit,internal_time_mock", "-buildvcs=false", + "internal_neo4j_go_driver_testkit,internal_neo4j_go_driver_time_mock", + "-buildvcs=false", backend_path ], go_bin=go_bin, diff --git a/testkit/build.py b/testkit/build.py index d15ebfc5..374d1710 100644 --- a/testkit/build.py +++ b/testkit/build.py @@ -6,6 +6,7 @@ import os from common import ( + ALL_BUILD_TAGS, get_go_min_bin, run_go, run_go_bin, @@ -20,10 +21,7 @@ print("Building for current target", flush=True) run_go( - [ - "build", "-tags", "internal_testkit,internal_time_mock", - "-v", "./..." - ], + ["build", "-tags", ALL_BUILD_TAGS, "-v", "./..."], go_bin=go_bin, env=defaultEnv ) @@ -38,10 +36,7 @@ print("Vet sources", flush=True) run_go( - [ - "vet", "-tags", "internal_testkit,internal_time_mock", - "./..." - ], + ["vet", "-tags", ALL_BUILD_TAGS, "./..."], go_bin=go_bin, env=defaultEnv ) @@ -56,7 +51,7 @@ print("Run staticcheck", flush=True) run_go_bin( "staticcheck", - ["-tags", "internal_testkit,internal_time_mock", "./..."], + ["-tags", ALL_BUILD_TAGS, "./..."], go_bin=go_bin, env=defaultEnv ) diff --git a/testkit/common.py b/testkit/common.py index cacbf0ca..ee73b979 100644 --- a/testkit/common.py +++ b/testkit/common.py @@ -5,6 +5,12 @@ from pathlib import Path +ALL_BUILD_TAGS = ( + "internal_neo4j_go_driver_testkit," + "internal_neo4j_go_driver_time_mock" +) + + def get_go_min_bin(): return os.environ.get('GOMINBIN', 'go') diff --git a/testkit/unittests.py b/testkit/unittests.py index 066219d0..bdac3f78 100644 --- a/testkit/unittests.py +++ b/testkit/unittests.py @@ -22,7 +22,7 @@ for go_bin in go_bins: for extra_args in ( - (), ("-tags", "internal_time_mock") + (), ("-tags", "internal_neo4j_go_driver_time_mock") ): cmd = ["test", "-race", *extra_args] if os.environ.get("TEST_IN_TEAMCITY", False):