From e29d00d7f0e58a31b7211c5231286d48ce8e8291 Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Fri, 27 Jun 2025 21:33:36 +0100 Subject: [PATCH 1/6] Add tests on file_operations --- .gitignore | 3 +- go.mod | 3 + go.sum | 16 ++--- internal/file_operations_test.go | 100 +++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 internal/file_operations_test.go diff --git a/.gitignore b/.gitignore index 7fc9b6fe..c92f9510 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ go.work.sum # .vscode/ data/ -__debug_bin* \ No newline at end of file +__debug_bin* +test-reports/ \ No newline at end of file diff --git a/go.mod b/go.mod index 13929168..ab039fd8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/paulmach/orb v0.11.1 github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.10.0 github.com/tavsec/gin-healthcheck v1.7.8 go.eigsys.de/gin-cachecontrol/v2 v2.3.0 ) @@ -23,6 +24,7 @@ require ( github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -45,6 +47,7 @@ require ( github.com/oapi-codegen/runtime v1.1.1 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect diff --git a/go.sum b/go.sum index d1b7f53c..956d3dac 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= @@ -139,8 +137,6 @@ github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zt github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= @@ -164,12 +160,9 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -222,16 +215,12 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw= github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o= -github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= -github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -243,6 +232,10 @@ github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0Zqm github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/gunit v1.4.2 h1:tyWYZffdPhQPfK5VsMQXfauwnJkqg7Tv5DLuQVYxq3Q= +github.com/smartystreets/gunit v1.4.2/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -362,7 +355,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/internal/file_operations_test.go b/internal/file_operations_test.go new file mode 100644 index 00000000..119f3ac1 --- /dev/null +++ b/internal/file_operations_test.go @@ -0,0 +1,100 @@ +package internal + +import ( + "os" + "path/filepath" + "testing" + + "github.com/paulmach/orb" + "github.com/paulmach/orb/geojson" + "github.com/stretchr/testify/require" +) + +// Helper to create a compressed .bz2 file with a known FeatureCollection +func createTestBZ2File(t *testing.T, fc *geojson.FeatureCollection) string { + tmpfile, err := os.CreateTemp("", "testfc-*.geojson.bz2") + require.NoError(t, err) + _ = tmpfile.Close() + _, err = CompressFeatureCollection(tmpfile.Name(), fc) + require.NoError(t, err) + return tmpfile.Name() +} + +func TestDecompressFeatureCollection_Success(t *testing.T) { + fc := geojson.NewFeatureCollection() + fc.Append(geojson.NewFeature(orb.Point{1, 2})) + fc.Append(geojson.NewFeature(orb.Point{3, 4})) + + bz2file := createTestBZ2File(t, fc) + defer func() { _ = os.Remove(bz2file) }() + + result, err := DecompressFeatureCollection(bz2file) + require.NoError(t, err) + require.Equal(t, len(fc.Features), len(result.Features)) + for i := range fc.Features { + require.Equal(t, fc.Features[i].Geometry, result.Features[i].Geometry) + } +} + +func TestDecompressFeatureCollection_FileNotFound(t *testing.T) { + _, err := DecompressFeatureCollection("nonexistent_file.bz2") + require.Error(t, err) +} + +func TestDecompressFeatureCollection_InvalidBZ2(t *testing.T) { + tmpfile := filepath.Join(os.TempDir(), "invalid.bz2") + err := os.WriteFile(tmpfile, []byte("not a bzip2 file"), 0644) + require.NoError(t, err) + defer func() { _ = os.Remove(tmpfile) }() + + _, err = DecompressFeatureCollection(tmpfile) + require.Error(t, err) +} + +func TestCompressFeatureCollection_Success(t *testing.T) { + fc := geojson.NewFeatureCollection() + fc.Append(geojson.NewFeature(orb.Point{10, 20})) + fc.Append(geojson.NewFeature(orb.Point{30, 40})) + + tmpfile, err := os.CreateTemp("", "compressfc-*.geojson.bz2") + require.NoError(t, err) + _ = tmpfile.Close() + defer func() { _ = os.Remove(tmpfile.Name()) }() + + n, err := CompressFeatureCollection(tmpfile.Name(), fc) + require.NoError(t, err) + require.Greater(t, n, 0) + + // Check file exists and is not empty + info, err := os.Stat(tmpfile.Name()) + require.NoError(t, err) + require.Equal(t, info.Size(), int64(n)) +} + +func TestCompressFeatureCollection_ErrorOnCreate(t *testing.T) { + // Try to write to a directory (should fail) + _, err := CompressFeatureCollection("/this/does/not/exist/test.bz2", geojson.NewFeatureCollection()) + require.Error(t, err) +} + +func TestCompressFeatureCollection_And_DecompressFeatureCollection_RoundTrip(t *testing.T) { + // Create a FeatureCollection with properties + fc := geojson.NewFeatureCollection() + feat := geojson.NewFeature(orb.Point{5, 6}) + feat.Properties = map[string]any{"foo": "bar"} + fc.Append(feat) + + tmpfile, err := os.CreateTemp("", "roundtripfc-*.geojson.bz2") + require.NoError(t, err) + _ = tmpfile.Close() + defer func() { _ = os.Remove(tmpfile.Name()) }() + + _, err = CompressFeatureCollection(tmpfile.Name(), fc) + require.NoError(t, err) + + result, err := DecompressFeatureCollection(tmpfile.Name()) + require.NoError(t, err) + require.Equal(t, len(fc.Features), len(result.Features)) + require.Equal(t, fc.Features[0].Geometry, result.Features[0].Geometry) + require.Equal(t, fc.Features[0].Properties["foo"], result.Features[0].Properties["foo"]) +} From 8a46382ddee3626a3a90b8411c787891d2bb8566 Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Fri, 27 Jun 2025 21:55:36 +0100 Subject: [PATCH 2/6] Add tests on CSV parser --- spatial-index/csv_test.go | 124 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 spatial-index/csv_test.go diff --git a/spatial-index/csv_test.go b/spatial-index/csv_test.go new file mode 100644 index 00000000..ca71e0db --- /dev/null +++ b/spatial-index/csv_test.go @@ -0,0 +1,124 @@ +package spatialindex + +import ( + "errors" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +type testRow struct { + A string + B string +} + +func fromFuncSimple(data []string, headers []string) (testRow, error) { + return testRow{A: data[0], B: data[1]}, nil +} + +func fromFuncWithError(data []string, headers []string) (testRow, error) { + return testRow{}, errors.New("parse error") +} + +func TestParseCSV_WithHeader_Success(t *testing.T) { + csvData := "a,b\nfoo,bar\nbaz,qux\n" + reader := strings.NewReader(csvData) + + var results []Result[testRow] + iter := parseCSV(reader, true, fromFuncSimple) + for r := range iter { + results = append(results, r) + } + + require.Len(t, results, 2) + require.Equal(t, 1, results[0].LineNum) + require.Equal(t, "foo", results[0].Value.A) + require.Equal(t, "bar", results[0].Value.B) + require.NoError(t, results[0].Error) + require.Equal(t, 2, results[1].LineNum) + require.Equal(t, "baz", results[1].Value.A) + require.Equal(t, "qux", results[1].Value.B) + require.NoError(t, results[1].Error) +} + +func TestParseCSV_WithoutHeader_Success(t *testing.T) { + csvData := "foo,bar\nbaz,qux\n" + reader := strings.NewReader(csvData) + + var results []Result[testRow] + iter := parseCSV(reader, false, fromFuncSimple) + for r := range iter { + results = append(results, r) + } + + require.Len(t, results, 2) + require.Equal(t, 1, results[0].LineNum) + require.Equal(t, "foo", results[0].Value.A) + require.Equal(t, "bar", results[0].Value.B) + require.NoError(t, results[0].Error) + require.Equal(t, 2, results[1].LineNum) + require.Equal(t, "baz", results[1].Value.A) + require.Equal(t, "qux", results[1].Value.B) + require.NoError(t, results[1].Error) +} + +func TestParseCSV_ParseError(t *testing.T) { + csvData := "a,b\nfoo,bar\nbaz,qux\n" + reader := strings.NewReader(csvData) + + var results []Result[testRow] + iter := parseCSV(reader, true, fromFuncWithError) + for r := range iter { + results = append(results, r) + } + + require.Len(t, results, 1) + require.Equal(t, 1, results[0].LineNum) + require.Error(t, results[0].Error) + require.Contains(t, results[0].Error.Error(), "failed to parse CSV line 1") +} + +func TestParseCSV_MalformedLine(t *testing.T) { + csvData := "a,b\nfoo\n" + reader := strings.NewReader(csvData) + + var results []Result[testRow] + iter := parseCSV(reader, true, fromFuncSimple) + for r := range iter { + results = append(results, r) + } + + require.Len(t, results, 1) + require.Equal(t, 1, results[0].LineNum) + require.Error(t, results[0].Error) + // Error message is from underlying csv.Reader + require.Contains(t, results[0].Error.Error(), "wrong number of fields") +} + +func TestParseCSV_HeaderReadError(t *testing.T) { + // An empty reader will cause an io.EOF when reading headers. + reader := strings.NewReader("") + iter := parseCSV(reader, true, fromFuncSimple) + var results []Result[testRow] + for r := range iter { + results = append(results, r) + } + require.Len(t, results, 1) + require.Equal(t, 0, results[0].LineNum) + require.Error(t, results[0].Error) + require.Contains(t, results[0].Error.Error(), "failed to read CSV headers") +} + +func TestParseCSV_YieldStopsEarly(t *testing.T) { + csvData := "a,b\nfoo,bar\nbaz,qux\n" + reader := strings.NewReader(csvData) + + count := 0 + iter := parseCSV(reader, true, fromFuncSimple) + iter(func(r Result[testRow]) bool { + count++ + return false // stop after first + }) + require.Equal(t, 1, count) +} From 2b5581b1889f9780132765e576a73c997942e2e1 Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Fri, 27 Jun 2025 22:38:23 +0100 Subject: [PATCH 3/6] Add tests on codepoint spatial index --- spatial-index/codepoint.go | 26 ++++---- spatial-index/codepoint_test.go | 103 ++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 spatial-index/codepoint_test.go diff --git a/spatial-index/codepoint.go b/spatial-index/codepoint.go index a763868b..d66ee50f 100644 --- a/spatial-index/codepoint.go +++ b/spatial-index/codepoint.go @@ -20,6 +20,19 @@ type SpatialIndex struct { tree *rtree.RTreeGN[uint32, string] } +func NewCodePointSpatialIndex(zipFile string) (*SpatialIndex, error) { + idx := SpatialIndex{ + tree: &rtree.RTreeGN[uint32, string]{}, + } + + err := idx.importCodePoint(zipFile) + if err != nil { + return nil, fmt.Errorf("failed to load codepoint index from zip file: %w", err) + } + + return &idx, nil +} + func (idx *SpatialIndex) Search(bounds []uint32) (*[]CodePoint, error) { results := make([]CodePoint, 0, 100) @@ -51,19 +64,6 @@ func (idx *SpatialIndex) SearchIter(bounds []uint32, iter func(min, max [2]uint3 return nil } -func NewCodePointSpatialIndex(zipFile string) (*SpatialIndex, error) { - idx := SpatialIndex{ - tree: &rtree.RTreeGN[uint32, string]{}, - } - - err := idx.importCodePoint(zipFile) - if err != nil { - return nil, fmt.Errorf("failed to load codepoint index from zip file: %w", err) - } - - return &idx, nil -} - func (idx *SpatialIndex) Len() int { return idx.tree.Len() } diff --git a/spatial-index/codepoint_test.go b/spatial-index/codepoint_test.go new file mode 100644 index 00000000..d38e1d04 --- /dev/null +++ b/spatial-index/codepoint_test.go @@ -0,0 +1,103 @@ +package spatialindex + +import ( + "archive/zip" + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/rtree" +) + +func createTestZip(t *testing.T, files map[string]string) string { + t.Helper() + tmpfile, err := os.CreateTemp("", "test-*.zip") + require.NoError(t, err, "failed to create temp file") + defer func() { _ = tmpfile.Close() }() + + w := zip.NewWriter(tmpfile) + for name, content := range files { + f, err := w.Create(name) + require.NoError(t, err, "failed to create file in zip") + _, err = f.Write([]byte(content)) + require.NoError(t, err, "failed to write file in zip") + } + _ = w.Close() + return tmpfile.Name() +} + +func TestNewCodePointSpatialIndex_Success(t *testing.T) { + csv := "PC1,PC2,123,456\nPC3,PC4,789,1011\n" + zipPath := createTestZip(t, map[string]string{"Data/CSV/test.csv": csv}) + defer func() { _ = os.Remove(zipPath) }() + + idx, err := NewCodePointSpatialIndex(zipPath) + require.NoError(t, err) + require.Equal(t, 2, idx.Len()) +} + +func TestNewCodePointSpatialIndex_BadZip(t *testing.T) { + _, err := NewCodePointSpatialIndex("/no/such/file.zip") + require.Error(t, err, "expected error for missing zip file") +} + +func TestSearch(t *testing.T) { + csv := "PC1,PC2,100,200\nPC2,PC3,300,400\n" + zipPath := createTestZip(t, map[string]string{"Data/CSV/test.csv": csv}) + defer func() { _ = os.Remove(zipPath) }() + idx, err := NewCodePointSpatialIndex(zipPath) + require.NoError(t, err) + + // Search for both + res, err := idx.Search([]uint32{0, 0, 500, 500}) + require.NoError(t, err) + require.Equal(t, 2, len(*res)) + + // Search for one + res, err = idx.Search([]uint32{90, 190, 110, 210}) + require.NoError(t, err) + require.Equal(t, 1, len(*res)) + require.Equal(t, "PC1", (*res)[0].PostCode) + + // Search for none + res, err = idx.Search([]uint32{1000, 1000, 2000, 2000}) + require.NoError(t, err) + require.Equal(t, 0, len(*res)) +} + +func TestSearchIter_InvalidBounds(t *testing.T) { + idx := &SpatialIndex{tree: &rtree.RTreeGN[uint32, string]{}} + err := idx.SearchIter([]uint32{1, 2, 3}, func([2]uint32, [2]uint32, string) bool { return true }) + require.Error(t, err) + require.Contains(t, err.Error(), "4 values") +} + +func TestLen(t *testing.T) { + csv := "PC1,PC2,1,2\nPC2,PC3,3,4\nPC3,PC4,5,6\n" + zipPath := createTestZip(t, map[string]string{"Data/CSV/test.csv": csv}) + defer func() { _ = os.Remove(zipPath) }() + idx, err := NewCodePointSpatialIndex(zipPath) + require.NoError(t, err) + require.Equal(t, 3, idx.Len()) +} + +func Test_fromCodePointCSV(t *testing.T) { + rec := []string{"PC1", "PC2", "123", "456"} + cp, err := fromCodePointCSV(rec, nil) + require.NoError(t, err) + require.Equal(t, "PC1", cp.PostCode) + require.Equal(t, uint32(123), cp.Easting) + require.Equal(t, uint32(456), cp.Northing) + + // Bad easting + rec = []string{"PC1", "PC2", "bad", "456"} + _, err = fromCodePointCSV(rec, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "easting") + + // Bad northing + rec = []string{"PC1", "PC2", "123", "bad"} + _, err = fromCodePointCSV(rec, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "northing") +} From 60c5cada94055f269ef429cfc214e3e66fa1a3eb Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Fri, 27 Jun 2025 22:44:14 +0100 Subject: [PATCH 4/6] Update spatial-index/codepoint_test.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- spatial-index/codepoint_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatial-index/codepoint_test.go b/spatial-index/codepoint_test.go index d38e1d04..fc9e7a44 100644 --- a/spatial-index/codepoint_test.go +++ b/spatial-index/codepoint_test.go @@ -38,7 +38,7 @@ func TestNewCodePointSpatialIndex_Success(t *testing.T) { func TestNewCodePointSpatialIndex_BadZip(t *testing.T) { _, err := NewCodePointSpatialIndex("/no/such/file.zip") - require.Error(t, err, "expected error for missing zip file") + require.Error(t, err, "failed to open zip file: file does not exist") } func TestSearch(t *testing.T) { From 4a491e842cb1e11801324c8a964a3151eb5b3a2e Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Fri, 27 Jun 2025 22:44:21 +0100 Subject: [PATCH 5/6] Update spatial-index/codepoint_test.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- spatial-index/codepoint_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatial-index/codepoint_test.go b/spatial-index/codepoint_test.go index fc9e7a44..c60f9ea1 100644 --- a/spatial-index/codepoint_test.go +++ b/spatial-index/codepoint_test.go @@ -69,7 +69,7 @@ func TestSearchIter_InvalidBounds(t *testing.T) { idx := &SpatialIndex{tree: &rtree.RTreeGN[uint32, string]{}} err := idx.SearchIter([]uint32{1, 2, 3}, func([2]uint32, [2]uint32, string) bool { return true }) require.Error(t, err) - require.Contains(t, err.Error(), "4 values") + require.Contains(t, err.Error(), "bounds must contain 4 values") } func TestLen(t *testing.T) { From 08897df8fadc6b851da68bbf6c16603493aea8cf Mon Sep 17 00:00:00 2001 From: Richard Hull Date: Fri, 27 Jun 2025 22:56:04 +0100 Subject: [PATCH 6/6] Fix test error --- spatial-index/codepoint_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spatial-index/codepoint_test.go b/spatial-index/codepoint_test.go index c60f9ea1..38066511 100644 --- a/spatial-index/codepoint_test.go +++ b/spatial-index/codepoint_test.go @@ -69,7 +69,7 @@ func TestSearchIter_InvalidBounds(t *testing.T) { idx := &SpatialIndex{tree: &rtree.RTreeGN[uint32, string]{}} err := idx.SearchIter([]uint32{1, 2, 3}, func([2]uint32, [2]uint32, string) bool { return true }) require.Error(t, err) - require.Contains(t, err.Error(), "bounds must contain 4 values") + require.Contains(t, err.Error(), "bounds must contain exactly 4 values") } func TestLen(t *testing.T) {