File: error_context_test.go

package info (click to toggle)
golang-github-oschwald-maxminddb-golang-v2 2.1.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,132 kB
  • sloc: perl: 557; makefile: 3
file content (286 lines) | stat: -rw-r--r-- 7,946 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package decoder

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/oschwald/maxminddb-golang/v2/internal/mmdberrors"
)

func TestWrapError_ZeroAllocationHappyPath(t *testing.T) {
	buffer := []byte{0x44, 't', 'e', 's', 't'} // String "test"
	dd := NewDataDecoder(buffer)
	decoder := NewDecoder(dd, 0)

	// Test that no error wrapping has zero allocation
	err := decoder.wrapError(nil)
	require.NoError(t, err)

	// DataDecoder should always have path tracking enabled
	require.NotNil(t, decoder.d)
}

func TestWrapError_ContextWhenError(t *testing.T) {
	buffer := []byte{0x44, 't', 'e', 's', 't'} // String "test"
	dd := NewDataDecoder(buffer)
	decoder := NewDecoder(dd, 0)

	// Simulate an error with context
	originalErr := mmdberrors.NewInvalidDatabaseError("test error")
	wrappedErr := decoder.wrapError(originalErr)

	require.Error(t, wrappedErr)

	// Should be a ContextualError
	var contextErr mmdberrors.ContextualError
	require.ErrorAs(t, wrappedErr, &contextErr)

	// Should have offset information
	require.Equal(t, uint(0), contextErr.Offset)
	require.Equal(t, originalErr, contextErr.Err)
}

func TestPathBuilder(t *testing.T) {
	builder := mmdberrors.NewPathBuilder()

	// Test basic path building
	require.Equal(t, "/", builder.Build())

	builder.PushMap("city")
	require.Equal(t, "/city", builder.Build())

	builder.PushMap("names")
	require.Equal(t, "/city/names", builder.Build())

	builder.PushSlice(0)
	require.Equal(t, "/city/names/0", builder.Build())

	// Test pop
	builder.Pop()
	require.Equal(t, "/city/names", builder.Build())

	// Test reset
	builder.Reset()
	require.Equal(t, "/", builder.Build())
}

// Benchmark to verify zero allocation on happy path.
func BenchmarkWrapError_HappyPath(b *testing.B) {
	buffer := []byte{0x44, 't', 'e', 's', 't'} // String "test"
	dd := NewDataDecoder(buffer)
	decoder := NewDecoder(dd, 0)

	b.ReportAllocs()

	for b.Loop() {
		err := decoder.wrapError(nil)
		if err != nil {
			b.Fatal("unexpected error")
		}
	}
}

// Benchmark to show allocation only occurs on error path.
func BenchmarkWrapError_ErrorPath(b *testing.B) {
	buffer := []byte{0x44, 't', 'e', 's', 't'} // String "test"
	dd := NewDataDecoder(buffer)
	decoder := NewDecoder(dd, 0)

	originalErr := mmdberrors.NewInvalidDatabaseError("test error")

	b.ReportAllocs()

	for b.Loop() {
		err := decoder.wrapError(originalErr)
		if err == nil {
			b.Fatal("expected error")
		}
	}
}

// Example showing the API in action.
func ExampleContextualError() {
	// This would be internal to the decoder, shown for illustration
	builder := mmdberrors.NewPathBuilder()
	builder.PushMap("city")
	builder.PushMap("names")
	builder.PushMap("en")

	// Simulate an error with context
	originalErr := mmdberrors.NewInvalidDatabaseError("string too long")

	contextTracker := &errorContext{path: builder}
	wrappedErr := mmdberrors.WrapWithContext(originalErr, 1234, contextTracker)

	fmt.Println(wrappedErr.Error())
	// Output: at offset 1234, path /city/names/en: string too long
}

func TestContextualErrorIntegration(t *testing.T) {
	t.Run("InvalidStringLength", func(t *testing.T) {
		// String claims size 4 but buffer only has 3 bytes total
		buffer := []byte{0x44, 't', 'e', 's'}

		// Test ReflectionDecoder
		rd := New(buffer)
		var result string
		err := rd.Decode(0, &result)
		require.Error(t, err)

		var contextErr mmdberrors.ContextualError
		require.ErrorAs(t, err, &contextErr)
		require.Equal(t, uint(0), contextErr.Offset)
		require.Contains(t, contextErr.Error(), "offset 0")

		// Test new Decoder API
		dd := NewDataDecoder(buffer)
		decoder := NewDecoder(dd, 0)
		_, err = decoder.ReadString()
		require.Error(t, err)

		require.ErrorAs(t, err, &contextErr)
		require.Equal(t, uint(0), contextErr.Offset)
		require.Contains(t, contextErr.Error(), "offset 0")
	})

	t.Run("NestedMapWithPath", func(t *testing.T) {
		// Map with nested structure that has an error deep inside
		// Map { "key": invalid_string }
		buffer := []byte{
			0xe1,                // Map with 1 item
			0x43, 'k', 'e', 'y', // Key "key" (3 bytes)
			0x44, 't', 'e', // Invalid string (claims size 4, only has 2 bytes)
		}

		// Test ReflectionDecoder with map decoding
		rd := New(buffer)
		var result map[string]string
		err := rd.Decode(0, &result)
		require.Error(t, err)

		// Should get a wrapped error with path information
		var contextErr mmdberrors.ContextualError
		require.ErrorAs(t, err, &contextErr)
		require.Equal(t, "/key", contextErr.Path)
		require.Contains(t, contextErr.Error(), "path /key")

		// Test new Decoder API - no automatic path tracking
		dd := NewDataDecoder(buffer)
		decoder := NewDecoder(dd, 0)
		mapIter, _, err := decoder.ReadMap()
		require.NoError(t, err, "ReadMap failed")

		var mapErr error
		for _, iterErr := range mapIter {
			if iterErr != nil {
				mapErr = iterErr
				break
			}

			// Try to read the value (this should fail)
			_, mapErr = decoder.ReadString()
			if mapErr != nil {
				break
			}
		}

		require.Error(t, mapErr)
		require.ErrorAs(t, mapErr, &contextErr)
		// New API should have offset but no path
		require.Contains(t, contextErr.Error(), "offset")
		require.Empty(t, contextErr.Path)
	})

	t.Run("SliceIndexInPath", func(t *testing.T) {
		// Create nested map-slice-map structure: { "list": [{"name": invalid_string}] }
		// This will test path like /list/0/name
		buffer := []byte{
			0xe1,                     // Map with 1 item
			0x44, 'l', 'i', 's', 't', // Key "list" (4 bytes)
			0x01, 0x04, // Array with 1 item (extended type: type=4 (slice), count=1)
			0xe1,                     // Map with 1 item (array element)
			0x44, 'n', 'a', 'm', 'e', // Key "name" (4 bytes)
			0x44, 't', 'e', // Invalid string (claims size 4, only has 2 bytes)
		}

		// Test ReflectionDecoder with slice index in path
		rd := New(buffer)
		var result map[string][]map[string]string
		err := rd.Decode(0, &result)
		require.Error(t, err)

		// Debug: print the actual error and path
		t.Logf("Error: %v", err)

		// Should get a wrapped error with slice index in path
		var contextErr mmdberrors.ContextualError
		require.ErrorAs(t, err, &contextErr)
		t.Logf("Path: %s", contextErr.Path)

		// Verify we get the exact path with correct order
		require.Equal(t, "/list/0/name", contextErr.Path)
		require.Contains(t, contextErr.Error(), "path /list/0/name")
		require.Contains(t, contextErr.Error(), "offset")

		// Test new Decoder API - manual iteration, no automatic path tracking
		dd := NewDataDecoder(buffer)
		decoder := NewDecoder(dd, 0)

		// Navigate through the nested structure manually
		mapIter, _, err := decoder.ReadMap()
		require.NoError(t, err, "ReadMap failed")
		var mapErr error

		for key, iterErr := range mapIter {
			if iterErr != nil {
				mapErr = iterErr
				break
			}
			require.Equal(t, "list", string(key))

			// Read the array
			sliceIter, _, err := decoder.ReadSlice()
			require.NoError(t, err, "ReadSlice failed")
			sliceIndex := 0
			for sliceIterErr := range sliceIter {
				if sliceIterErr != nil {
					mapErr = sliceIterErr
					break
				}
				require.Equal(t, 0, sliceIndex) // Should be first element

				// Read the nested map (array element)
				innerMapIter, _, err := decoder.ReadMap()
				require.NoError(t, err, "ReadMap failed")
				for innerKey, innerIterErr := range innerMapIter {
					if innerIterErr != nil {
						mapErr = innerIterErr
						break
					}
					require.Equal(t, "name", string(innerKey))

					// Try to read the invalid string (this should fail)
					_, mapErr = decoder.ReadString()
					if mapErr != nil {
						break
					}
				}
				if mapErr != nil {
					break
				}
				sliceIndex++
			}
			if mapErr != nil {
				break
			}
		}

		require.Error(t, mapErr)
		require.ErrorAs(t, mapErr, &contextErr)
		// New API should have offset but no path (since it's manual iteration)
		require.Contains(t, contextErr.Error(), "offset")
		require.Empty(t, contextErr.Path)
	})
}