diff --git a/pkg/geo/geogfn/BUILD.bazel b/pkg/geo/geogfn/BUILD.bazel index f9998a923f4e..3bd49da88546 100644 --- a/pkg/geo/geogfn/BUILD.bazel +++ b/pkg/geo/geogfn/BUILD.bazel @@ -53,6 +53,7 @@ go_test( deps = [ "//pkg/geo", "//pkg/geo/geoprojbase", + "//pkg/geo/geotest", "@com_github_golang_geo//s1", "@com_github_golang_geo//s2", "@com_github_stretchr_testify//require", diff --git a/pkg/geo/geogfn/azimuth_test.go b/pkg/geo/geogfn/azimuth_test.go index 93a63b6dd01f..cd0181f5a134 100644 --- a/pkg/geo/geogfn/azimuth_test.go +++ b/pkg/geo/geogfn/azimuth_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" ) @@ -67,7 +68,11 @@ func TestAzimuth(t *testing.T) { r, err := Azimuth(a, b) require.NoError(t, err) require.NotNil(t, r) - require.Equal(t, tc.expected, *r) + if tc.expected == 0 { + require.Equal(t, tc.expected, *r) + } else { + require.InEpsilon(t, tc.expected, *r, geotest.Epsilon) + } }) } diff --git a/pkg/geo/geogfn/segmentize_test.go b/pkg/geo/geogfn/segmentize_test.go index c9629209c1b6..0418c5f31273 100644 --- a/pkg/geo/geogfn/segmentize_test.go +++ b/pkg/geo/geogfn/segmentize_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" ) @@ -130,7 +131,7 @@ func TestSegmentize(t *testing.T) { require.NoError(t, err) expectedGeog, err := geo.ParseGeography(test.expectedWKT) require.NoError(t, err) - require.Equal(t, expectedGeog, modifiedGeog) + geotest.RequireGeographyInEpsilon(t, expectedGeog, modifiedGeog, geotest.Epsilon) }) } // Test for segment maximum length as negative. @@ -216,7 +217,7 @@ func TestSegmentizeCoords(t *testing.T) { t.Run(test.desc, func(t *testing.T) { convertedPoints, err := segmentizeCoords(test.a, test.b, test.segmentMaxAngle) require.NoError(t, err) - require.Equal(t, test.resultantCoordinates, convertedPoints) + geotest.FlatCoordsInEpsilon(t, test.resultantCoordinates, convertedPoints, geotest.Epsilon) }) } diff --git a/pkg/geo/geogfn/unary_operators_test.go b/pkg/geo/geogfn/unary_operators_test.go index 9b1c4d6f1835..05f6d46d6cac 100644 --- a/pkg/geo/geogfn/unary_operators_test.go +++ b/pkg/geo/geogfn/unary_operators_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/golang/geo/s1" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" @@ -270,13 +271,11 @@ func TestProject(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { projected, err := Project(tc.point, tc.distance, s1.Angle(tc.azimuth)) require.NoError(t, err) - require.Equalf( + geotest.RequireGeographyInEpsilon( t, tc.projected, projected, - "expected %f, found %f", - &tc.projected, - projected, + geotest.Epsilon, ) }) } diff --git a/pkg/geo/geomfn/BUILD.bazel b/pkg/geo/geomfn/BUILD.bazel index 1ec5c3a5e6ad..3a0b17491255 100644 --- a/pkg/geo/geomfn/BUILD.bazel +++ b/pkg/geo/geomfn/BUILD.bazel @@ -106,6 +106,7 @@ go_test( "//pkg/geo", "//pkg/geo/geopb", "//pkg/geo/geos", + "//pkg/geo/geotest", "@com_github_cockroachdb_errors//:errors", "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", diff --git a/pkg/geo/geomfn/affine_transforms_test.go b/pkg/geo/geomfn/affine_transforms_test.go index aefe0b9578a3..973e220520eb 100644 --- a/pkg/geo/geomfn/affine_transforms_test.go +++ b/pkg/geo/geomfn/affine_transforms_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" ) @@ -497,7 +498,7 @@ func TestRotate(t *testing.T) { actual, err := Rotate(geometry, tc.rotRadians) require.NoError(t, err) - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) }) } } @@ -597,7 +598,7 @@ func TestRotateWithPointOrigin(t *testing.T) { require.EqualError(t, err, tt.wantErrStr) } else { require.NoError(t, err) - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tt.wantGeom), got, 1e-5) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tt.wantGeom), got, 1e-5) } }) } @@ -743,7 +744,7 @@ func TestRotateX(t *testing.T) { actual, err := RotateX(geometry, tc.rotRadians) require.NoError(t, err) - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) }) } } @@ -818,7 +819,7 @@ func TestRotateY(t *testing.T) { actual, err := RotateY(geometry, tc.rotRadians) require.NoError(t, err) - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) }) } } @@ -893,7 +894,7 @@ func TestRotateZ(t *testing.T) { actual, err := RotateZ(geometry, tc.rotRadians) require.NoError(t, err) - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) }) } } diff --git a/pkg/geo/geomfn/distance_test.go b/pkg/geo/geomfn/distance_test.go index a028ee50e73c..37bda7ab8a6f 100644 --- a/pkg/geo/geomfn/distance_test.go +++ b/pkg/geo/geomfn/distance_test.go @@ -17,6 +17,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/geo" "github.com/cockroachdb/cockroach/pkg/geo/geos" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" ) @@ -1001,7 +1002,7 @@ func TestClosestPoint(t *testing.T) { ret, err := ClosestPoint(gA, gB) require.NoError(t, err) - requireGeometryWithinEpsilon(t, expected, ret, 2e-10) + geotest.RequireGeometryInEpsilon(t, expected, ret, 2e-10) }) } diff --git a/pkg/geo/geomfn/generate_points_test.go b/pkg/geo/geomfn/generate_points_test.go index e945396c97a1..7ebe890cc2bf 100644 --- a/pkg/geo/geomfn/generate_points_test.go +++ b/pkg/geo/geomfn/generate_points_test.go @@ -15,6 +15,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" ) @@ -70,7 +71,7 @@ func TestGenerateRandomPoints(t *testing.T) { rng := rand.New(rand.NewSource(tt.args.seed)) got, err := GenerateRandomPoints(tt.args.g, tt.args.nPoints, rng) require.NoError(t, err) - require.Equal(t, tt.want, got) + geotest.RequireGeometryInEpsilon(t, tt.want, got, 1e-5) }) } diff --git a/pkg/geo/geomfn/geomfn_test.go b/pkg/geo/geomfn/geomfn_test.go index d436b9a7b208..f72602de57c4 100644 --- a/pkg/geo/geomfn/geomfn_test.go +++ b/pkg/geo/geomfn/geomfn_test.go @@ -11,11 +11,9 @@ package geomfn import ( - "math" "testing" "github.com/cockroachdb/cockroach/pkg/geo" - "github.com/cockroachdb/errors" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" ) @@ -136,52 +134,3 @@ func requireGeometryFromGeomT(t *testing.T, g geom.T) geo.Geometry { require.NoError(t, err) return ret } - -// flatCoordsInEpsilon ensures the flat coords are within the expected epsilon. -func flatCoordsInEpsilon(t *testing.T, expected []float64, actual []float64, epsilon float64) { - require.Equal(t, len(expected), len(actual), "expected %#v, got %#v", expected, actual) - for i := range expected { - require.True(t, math.Abs(expected[i]-actual[i]) < epsilon, "expected %#v, got %#v (mismatching at position %d)", expected, actual, i) - } -} - -// requireGeometryWithinEpsilon and ensures the geometry shape and SRID are equal, -// and that each coordinate is within the provided epsilon. -func requireGeometryWithinEpsilon(t *testing.T, expected, got geo.Geometry, epsilon float64) { - expectedT, err := expected.AsGeomT() - require.NoError(t, err) - gotT, err := got.AsGeomT() - require.NoError(t, err) - requireGeomTWithinEpsilon(t, expectedT, gotT, epsilon) -} - -func requireGeomTWithinEpsilon(t *testing.T, expectedT, gotT geom.T, epsilon float64) { - require.Equal(t, expectedT.SRID(), gotT.SRID()) - require.Equal(t, expectedT.Layout(), gotT.Layout()) - require.IsType(t, expectedT, gotT) - switch lhs := expectedT.(type) { - case *geom.Point, *geom.LineString: - flatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) - case *geom.MultiPoint, *geom.Polygon, *geom.MultiLineString: - require.Equal(t, expectedT.Ends(), gotT.Ends()) - flatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) - case *geom.MultiPolygon: - require.Equal(t, expectedT.Ends(), gotT.Ends()) - require.Equal(t, expectedT.Endss(), gotT.Endss()) - flatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) - case *geom.GeometryCollection: - rhs, ok := gotT.(*geom.GeometryCollection) - require.True(t, ok) - require.Len(t, rhs.Geoms(), len(lhs.Geoms())) - for i := range lhs.Geoms() { - requireGeomTWithinEpsilon( - t, - lhs.Geom(i), - rhs.Geom(i), - epsilon, - ) - } - default: - panic(errors.AssertionFailedf("unknown geometry type: %T", expectedT)) - } -} diff --git a/pkg/geo/geomfn/linestring_test.go b/pkg/geo/geomfn/linestring_test.go index a108ef91c36b..6e74afb69a94 100644 --- a/pkg/geo/geomfn/linestring_test.go +++ b/pkg/geo/geomfn/linestring_test.go @@ -17,6 +17,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/geo" "github.com/cockroachdb/cockroach/pkg/geo/geopb" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" ) @@ -604,7 +605,7 @@ func TestLineSubstring(t *testing.T) { require.Equal(t, tt.wantErrString, err.Error()) return } - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tt.wantGeomT), got, 1e-4) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tt.wantGeomT), got, 1e-4) }) } } diff --git a/pkg/geo/geomfn/segmentize_test.go b/pkg/geo/geomfn/segmentize_test.go index 00d2908e216e..936325d6db73 100644 --- a/pkg/geo/geomfn/segmentize_test.go +++ b/pkg/geo/geomfn/segmentize_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" ) @@ -155,7 +156,7 @@ func TestSegmentize(t *testing.T) { require.NoError(t, err) expectedGeom, err := geo.ParseGeometry(test.expectedWKT) require.NoError(t, err) - requireGeometryWithinEpsilon(t, expectedGeom, modifiedGeom, 1e-6) + geotest.RequireGeometryInEpsilon(t, expectedGeom, modifiedGeom, geotest.Epsilon) }) } // Test for segment maximum length as negative. diff --git a/pkg/geo/geomfn/snap_test.go b/pkg/geo/geomfn/snap_test.go index 703acadefe87..5d41f4dadf51 100644 --- a/pkg/geo/geomfn/snap_test.go +++ b/pkg/geo/geomfn/snap_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" ) @@ -79,7 +80,7 @@ func TestSnap(t *testing.T) { actual, err := Snap(input, target, tc.tolerance) require.NoError(t, err) - requireGeometryWithinEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) + geotest.RequireGeometryInEpsilon(t, requireGeometryFromGeomT(t, tc.expected), actual, 1e-5) }) } } diff --git a/pkg/geo/geomfn/topology_operations_test.go b/pkg/geo/geomfn/topology_operations_test.go index 594fe713cdf4..8c38e73d5370 100644 --- a/pkg/geo/geomfn/topology_operations_test.go +++ b/pkg/geo/geomfn/topology_operations_test.go @@ -16,6 +16,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geotest" "github.com/cockroachdb/errors" "github.com/stretchr/testify/require" "github.com/twpayne/go-geom" @@ -75,7 +76,7 @@ func TestCentroid(t *testing.T) { require.NoError(t, err) expected, err := geo.ParseGeometry(tc.expected) require.NoError(t, err) - requireGeometryWithinEpsilon(t, expected, ret, 2e-10) + geotest.RequireGeometryInEpsilon(t, expected, ret, 2e-10) }) } } @@ -300,7 +301,7 @@ func TestPointOnSurface(t *testing.T) { require.NoError(t, err) expected, err := geo.ParseGeometry(tc.expected) require.NoError(t, err) - requireGeometryWithinEpsilon(t, expected, ret, 2e-10) + geotest.RequireGeometryInEpsilon(t, expected, ret, 2e-10) }) } } diff --git a/pkg/geo/geotest/BUILD.bazel b/pkg/geo/geotest/BUILD.bazel new file mode 100644 index 000000000000..be5f8e2f3dd4 --- /dev/null +++ b/pkg/geo/geotest/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "geotest", + srcs = ["geotest.go"], + importpath = "github.com/cockroachdb/cockroach/pkg/geo/geotest", + visibility = ["//visibility:public"], + deps = [ + "//pkg/geo", + "@com_github_cockroachdb_errors//:errors", + "@com_github_stretchr_testify//require", + "@com_github_twpayne_go_geom//:go-geom", + ], +) diff --git a/pkg/geo/geotest/geotest.go b/pkg/geo/geotest/geotest.go new file mode 100644 index 000000000000..fbdc075eb784 --- /dev/null +++ b/pkg/geo/geotest/geotest.go @@ -0,0 +1,86 @@ +// Copyright 2022 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package geotest + +import ( + "math" + "testing" + + "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/errors" + "github.com/stretchr/testify/require" + "github.com/twpayne/go-geom" +) + +// Epsilon is the default epsilon to use in spatial tests. +// This corresponds to ~1cm in lat/lng. +const Epsilon = 0.000001 + +// RequireGeographyInEpsilon and ensures the geometry shape and SRID are equal, +// and that each coordinate is within the provided epsilon. +func RequireGeographyInEpsilon(t *testing.T, expected, got geo.Geography, epsilon float64) { + expectedT, err := expected.AsGeomT() + require.NoError(t, err) + gotT, err := got.AsGeomT() + require.NoError(t, err) + RequireGeomTInEpsilon(t, expectedT, gotT, epsilon) +} + +// RequireGeometryInEpsilon and ensures the geometry shape and SRID are equal, +// and that each coordinate is within the provided epsilon. +func RequireGeometryInEpsilon(t *testing.T, expected, got geo.Geometry, epsilon float64) { + expectedT, err := expected.AsGeomT() + require.NoError(t, err) + gotT, err := got.AsGeomT() + require.NoError(t, err) + RequireGeomTInEpsilon(t, expectedT, gotT, epsilon) +} + +// FlatCoordsInEpsilon ensures the flat coords are within the expected epsilon. +func FlatCoordsInEpsilon(t *testing.T, expected []float64, actual []float64, epsilon float64) { + require.Equal(t, len(expected), len(actual), "expected %#v, got %#v", expected, actual) + for i := range expected { + require.True(t, math.Abs(expected[i]-actual[i]) < epsilon, "expected %#v, got %#v (mismatching at position %d)", expected, actual, i) + } +} + +// RequireGeomTInEpsilon ensures that the geom.T are equal, except for +// coords which should be within the given epsilon. +func RequireGeomTInEpsilon(t *testing.T, expectedT, gotT geom.T, epsilon float64) { + require.Equal(t, expectedT.SRID(), gotT.SRID()) + require.Equal(t, expectedT.Layout(), gotT.Layout()) + require.IsType(t, expectedT, gotT) + switch lhs := expectedT.(type) { + case *geom.Point, *geom.LineString: + FlatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) + case *geom.MultiPoint, *geom.Polygon, *geom.MultiLineString: + require.Equal(t, expectedT.Ends(), gotT.Ends()) + FlatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) + case *geom.MultiPolygon: + require.Equal(t, expectedT.Ends(), gotT.Ends()) + require.Equal(t, expectedT.Endss(), gotT.Endss()) + FlatCoordsInEpsilon(t, expectedT.FlatCoords(), gotT.FlatCoords(), epsilon) + case *geom.GeometryCollection: + rhs, ok := gotT.(*geom.GeometryCollection) + require.True(t, ok) + require.Len(t, rhs.Geoms(), len(lhs.Geoms())) + for i := range lhs.Geoms() { + RequireGeomTInEpsilon( + t, + lhs.Geom(i), + rhs.Geom(i), + epsilon, + ) + } + default: + panic(errors.AssertionFailedf("unknown geometry type: %T", expectedT)) + } +}