shapelib 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2233 @@
1
+ require 'shapelib'
2
+ require 'test/unit'
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+ require 'open3'
6
+ require 'rbconfig'
7
+
8
+ class TestZillowData < Test::Unit::TestCase
9
+ def test_open_rejects_missing_or_corrupt_geometry
10
+ Dir.mktmpdir('shapelib-test-') do |dir|
11
+ fixture = 'test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA'
12
+ FileUtils.cp("#{fixture}.shp", File.join(dir, 'no_index.shp'))
13
+ File.binwrite(File.join(dir, 'corrupt.shp'), 'not a shapefile')
14
+ FileUtils.cp("#{fixture}.shx", File.join(dir, 'corrupt.shx'))
15
+ %w[missing no_index corrupt].each do |name|
16
+ filename = File.join(dir, name)
17
+ assert_file_operation_failure('SHPOpen', filename) do |block|
18
+ Shapelib::ShapeFile.open(filename, &block)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def test_creation_rejects_nonexistent_parent
25
+ Dir.mktmpdir('shapelib-test-') do |dir|
26
+ filename = File.join(dir, 'absent', 'points')
27
+ assert_file_operation_failure('SHPCreate', filename) do |block|
28
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, &block)
29
+ end
30
+ assert_empty Dir.children(dir)
31
+ end
32
+ end
33
+
34
+ def test_creation_rejects_partial_failure_without_deleting_files
35
+ Dir.mktmpdir('shapelib-test-') do |dir|
36
+ filename = File.join(dir, 'points')
37
+ Dir.mkdir("#{filename}.dbf")
38
+ assert_file_operation_failure('DBFCreate', filename) do |block|
39
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, &block)
40
+ end
41
+ assert_path_exist "#{filename}.shp"
42
+ assert_path_exist "#{filename}.shx"
43
+ assert_true File.directory?("#{filename}.dbf")
44
+ end
45
+ end
46
+
47
+ def test_file_creation_and_open_round_trip
48
+ Dir.mktmpdir('shapelib-test-') do |dir|
49
+ filename = File.join(dir, 'points')
50
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['name', Shapelib::String, 10]]) do |file|
51
+ assert_equal 0, file.write(Shapelib::Point.new(1, 2, { 'name' => 'example' }))
52
+ end
53
+ Shapelib::ShapeFile.open(filename) do |file|
54
+ assert_equal 1, file.size
55
+ assert_equal 1, file.field_count
56
+ shape = file.read(0)
57
+ assert_equal [1.0], shape.xvals
58
+ assert_equal [2.0], shape.yvals
59
+ assert_equal 'example', shape['name']
60
+ end
61
+ end
62
+ end
63
+
64
+ def test_invalid_write_indexes_do_not_modify_files
65
+ [0, 2].each do |count|
66
+ Dir.mktmpdir('shapelib-test-') do |dir|
67
+ filename = File.join(dir, 'points')
68
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['name', Shapelib::String, 10]]) do |file|
69
+ count.times { |i| file.write(Shapelib::Point.new(i, i, 'name' => "before#{i}")) }
70
+ end
71
+ expected = nil
72
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
73
+ expected = count.times.map { |i| file.read(i).to_h }
74
+ point = Shapelib::Point.new(9, 9, 'name' => 'after')
75
+ [-2, count, count + 10].each do |index|
76
+ error = assert_raise(ArgumentError) { file.write(point, index) }
77
+ assert_match(/write index #{index} /, error.message)
78
+ assert_equal count, file.size
79
+ assert_equal expected, count.times.map { |i| file.read(i).to_h }
80
+ end
81
+ end
82
+ Shapelib::ShapeFile.open(filename) do |file|
83
+ assert_equal count, file.size
84
+ assert_equal expected, count.times.map { |i| file.read(i).to_h }
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def test_write_indexes_use_current_count_after_appends
91
+ Dir.mktmpdir('shapelib-test-') do |dir|
92
+ filename = File.join(dir, 'points')
93
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['name', Shapelib::String, 10]]) do |file|
94
+ assert_equal 0, file.size
95
+ assert_equal 0, file.write(Shapelib::Point.new(1, 2, 'name' => 'first'))
96
+ assert_equal 1, file.write(Shapelib::Point.new(3, 4, 'name' => 'second'), -1)
97
+ [0, 1].each do |index|
98
+ assert_equal index, file.write(Shapelib::Point.new(9, 8, 'name' => 'after'), index)
99
+ end
100
+ end
101
+ Shapelib::ShapeFile.open(filename) do |file|
102
+ assert_equal 2, file.size
103
+ 2.times do |index|
104
+ point = file.read(index)
105
+ assert_equal [9.0], point.xvals
106
+ assert_equal [8.0], point.yvals
107
+ assert_equal 'after', point['name']
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def test_write_append_index_forms_round_trip
114
+ Dir.mktmpdir('shapelib-test-') do |dir|
115
+ filename = File.join(dir, 'points')
116
+ points = 3.times.map { |index| Shapelib::Point.new(index, -index, 'name' => "point#{index}") }
117
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['name', Shapelib::String, 10]]) do |file|
118
+ assert_equal 0, file.write(points[0])
119
+ assert_equal 1, file.write(points[1], nil)
120
+ assert_equal 2, file.write(points[2], -1)
121
+ assert_equal points.length, file.size
122
+ end
123
+ Shapelib::ShapeFile.open(filename) do |file|
124
+ assert_equal points.length, file.size
125
+ points.each_with_index do |point, index|
126
+ assert_equal point.to_h.merge(shape_id: index), file.read(index).to_h
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ def test_indexed_read_preserves_following_sequential_position
133
+ Dir.mktmpdir('shapelib-test-') do |dir|
134
+ filename = File.join(dir, 'points')
135
+ Shapelib::ShapeFile.new(filename, Shapelib::Point) do |file|
136
+ 3.times { |i| file.write(Shapelib::Point.new(i, i)) }
137
+ end
138
+ [false, true].each do |size_first|
139
+ Shapelib::ShapeFile.open(filename) do |file|
140
+ file.size if size_first
141
+ assert_equal [1.0], file.read(1).xvals
142
+ assert_equal [2.0], file.read.xvals
143
+ file.rewind
144
+ assert_equal [0.0], file.read.xvals
145
+ end
146
+ end
147
+ Shapelib::ShapeFile.open(filename) { |file| assert_equal [0.0], file.read.xvals }
148
+ end
149
+ end
150
+
151
+ def test_date_field_types_and_string_values_round_trip
152
+ omit 'native shapelib has no FTDate' unless Shapelib.const_defined?(:Date, false)
153
+ [Shapelib::Date, :Date, 'Date'].each do |type|
154
+ Dir.mktmpdir('shapelib-test-') do |dir|
155
+ filename = File.join(dir, 'dates')
156
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['date', type, 8]]) do |file|
157
+ assert_equal :Date, file.field_type('date')
158
+ assert_equal ':Date', file.field_type(0).inspect
159
+ assert_equal 8, file.field_width(0)
160
+ assert_equal 0, file.field_decimals(0)
161
+ ['20261019', '00010102', nil].each do |value|
162
+ file.write(Shapelib::Point.new(1, 2, { 'date' => value }))
163
+ end
164
+ end
165
+ Shapelib::ShapeFile.open(filename) do |file|
166
+ assert_equal :Date, file.field_type(0)
167
+ ['20261019', '00010102', nil].each_with_index do |value, index|
168
+ shape = file.read(index)
169
+ assert_equal value, shape['date']
170
+ assert_equal value, shape.to_h['date']
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def test_existing_dbf_date_fields_preserve_string_access
178
+ Dir.mktmpdir('shapelib-test-') do |dir|
179
+ filename = File.join(dir, 'dates')
180
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['date', Shapelib::String, 8]]) do |file|
181
+ ['20261019', '00010102', '00000000'].each do |value|
182
+ file.write(Shapelib::Point.new(1, 2, { 'date' => value }))
183
+ end
184
+ end
185
+ # Use a native DBF date descriptor independently of the gem's Date support.
186
+ File.open("#{filename}.dbf", 'r+b') { |file| file.seek(32 + 11); file.write('D') }
187
+ Shapelib::ShapeFile.open(filename) do |file|
188
+ assert_equal Shapelib.const_defined?(:Date, false) ? :Date : :String, file.field_type('date')
189
+ ['20261019', '00010102', nil].each_with_index do |value, index|
190
+ shape = file.read(index)
191
+ assert_equal value, shape['date']
192
+ assert_equal value, shape.to_h['date']
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ def test_invalid_field_names_do_not_change_schema
199
+ Dir.mktmpdir('shapelib-test-') do |dir|
200
+ filename = File.join(dir, 'fields')
201
+ fields = [['name', :String, 10, 0], ['amount', :Float, 12, 2]]
202
+ metadata = lambda do |file|
203
+ file.fields.map { |name| [name.b, file.field_type(name), file.field_width(name), file.field_decimals(name)] }
204
+ end
205
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
206
+ ['abcdefghijk', 'é' * 6, 'name', 'NAME'].each do |name|
207
+ error = assert_raise(ArgumentError) { file.add_field(name, :Integer, 8) }
208
+ expected = name.bytesize > 10 ? 'field name exceeds 10 bytes' : "field name already exists: #{name}"
209
+ assert_equal expected, error.message
210
+ assert_equal fields.length, file.field_count
211
+ assert_equal fields, metadata.call(file)
212
+ end
213
+ file.add_field('abcdefghij', :String, 10)
214
+ file.add_field('é' * 5, :String, 10)
215
+ fields += [['abcdefghij', :String, 10, 0], [('é' * 5).b, :String, 10, 0]]
216
+ assert_equal fields, metadata.call(file)
217
+ point = Shapelib::Point.new(1, 2, 'name' => 'before', 'amount' => 1.25,
218
+ 'abcdefghij' => 'ten bytes', 'é' * 5 => 'multibyte')
219
+ assert_equal 0, file.write(point)
220
+ end
221
+ Shapelib::ShapeFile.open(filename) do |file|
222
+ assert_equal fields.length, file.field_count
223
+ assert_equal fields, metadata.call(file)
224
+ point = file.read(0)
225
+ assert_equal 'before', point['name']
226
+ assert_equal 1.25, point['amount']
227
+ assert_equal 'ten bytes', point['abcdefghij']
228
+ assert_equal 'multibyte', point['é' * 5]
229
+ end
230
+ end
231
+ end
232
+
233
+ def test_constructor_rejects_invalid_field_names
234
+ ['abcdefghijk', 'é' * 6, 'name', 'NAME'].each do |name|
235
+ Dir.mktmpdir('shapelib-test-') do |dir|
236
+ fields = [['name', :String, 10], [name, :Integer, 8]]
237
+ yielded = false
238
+ error = assert_raise(ArgumentError) do
239
+ Shapelib::ShapeFile.new(File.join(dir, 'fields'), Shapelib::Point, fields) { yielded = true }
240
+ end
241
+ expected = name.bytesize > 10 ? 'field name exceeds 10 bytes' : "field name already exists: #{name}"
242
+ assert_equal expected, error.message
243
+ assert_equal false, yielded
244
+ end
245
+ end
246
+ end
247
+
248
+ def test_numeric_fields_reject_invalid_precision_without_changing_schema
249
+ Dir.mktmpdir('shapelib-test-') do |dir|
250
+ filename = File.join(dir, 'numbers')
251
+ fields = [['name', :String, 10, 0], ['amount', :Float, 12, 2]]
252
+ metadata = lambda do |file|
253
+ file.fields.map { |name| [name, file.field_type(name), file.field_width(name), file.field_decimals(name)] }
254
+ end
255
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
256
+ [:Integer, :Float].each do |type|
257
+ [-1, 256].each do |precision|
258
+ error = assert_raise(ArgumentError) { file.add_field('bad', type, 12, precision) }
259
+ assert_equal 'numeric field precision must be between 0 and 255', error.message
260
+ assert_equal fields.length, file.field_count
261
+ assert_equal fields, metadata.call(file)
262
+ end
263
+ end
264
+ end
265
+ Shapelib::ShapeFile.open(filename) do |file|
266
+ assert_equal fields.length, file.field_count
267
+ assert_equal fields, metadata.call(file)
268
+ end
269
+ end
270
+ end
271
+
272
+ def test_constructor_rejects_invalid_numeric_precision
273
+ [:Integer, :Float].each do |type|
274
+ [-1, 256].each do |precision|
275
+ Dir.mktmpdir('shapelib-test-') do |dir|
276
+ fields = [['bad', type, 12, precision]]
277
+ error = assert_raise(ArgumentError) do
278
+ Shapelib::ShapeFile.new(File.join(dir, 'numbers'), Shapelib::Point, fields)
279
+ end
280
+ assert_equal 'numeric field precision must be between 0 and 255', error.message
281
+ end
282
+ end
283
+ end
284
+ end
285
+
286
+ def test_numeric_precision_boundaries_persist
287
+ [:Integer, :Float].each do |type|
288
+ Dir.mktmpdir('shapelib-test-') do |dir|
289
+ filename = File.join(dir, 'numbers')
290
+ precisions = [0, 2, 255]
291
+ Shapelib::ShapeFile.new(filename, Shapelib::Point) do |file|
292
+ precisions.each do |precision|
293
+ name = "field#{precision}"
294
+ file.add_field(name, type, 255, precision)
295
+ assert_equal precision, file.field_decimals(name)
296
+ end
297
+ end
298
+ Shapelib::ShapeFile.open(filename) do |file|
299
+ assert_equal precisions.length, file.field_count
300
+ precisions.each { |precision| assert_equal precision, file.field_decimals("field#{precision}") }
301
+ end
302
+ end
303
+ end
304
+ end
305
+
306
+ def test_date_fields_require_fixed_width_and_zero_decimals
307
+ omit 'native shapelib has no FTDate' unless Shapelib.const_defined?(:Date, false)
308
+ Dir.mktmpdir('shapelib-test-') do |dir|
309
+ Shapelib::ShapeFile.new(File.join(dir, 'dates'), Shapelib::Point) do |file|
310
+ [[0, 0], [7, 0], [9, 0], [8, 1]].each do |width, decimals|
311
+ error = assert_raise(ArgumentError) { file.add_field('date', :Date, width, decimals) }
312
+ assert_equal 'Date fields require width 8 and zero decimal places', error.message
313
+ assert_equal 0, file.field_count
314
+ end
315
+ end
316
+ end
317
+ end
318
+
319
+ def test_invalid_date_writes_do_not_modify_files
320
+ omit 'native shapelib has no FTDate' unless Shapelib.const_defined?(:Date, false)
321
+ [7, 1.5, '', '2026101', '202610199', '2026x019', '2026-10-19'].each do |value|
322
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
323
+ Dir.mktmpdir('shapelib-test-') do |dir|
324
+ filename = File.join(dir, 'dates')
325
+ fields = [['name', Shapelib::String, 10], ['date', :Date, 8]]
326
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
327
+ count.times { file.write(Shapelib::Point.new(1, 2, { 'name' => 'before', 'date' => '20261019' })) }
328
+ end
329
+ expected = nil
330
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
331
+ expected = count.times.map { |record| file.read(record).to_h }
332
+ shape = Shapelib::Point.new(9, 9, { 'name' => 'after', 'date' => value })
333
+ assert_raise(value.is_a?(String) ? ArgumentError : TypeError) { file.write(shape, index) }
334
+ assert_equal count, file.size
335
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
336
+ end
337
+ Shapelib::ShapeFile.open(filename) do |file|
338
+ assert_equal count, file.size
339
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
340
+ end
341
+ end
342
+ end
343
+ end
344
+ end
345
+
346
+ def test_date_attributes_can_be_copied_and_replaced
347
+ omit 'native shapelib has no FTDate' unless Shapelib.const_defined?(:Date, false)
348
+ Dir.mktmpdir('shapelib-test-') do |dir|
349
+ filename = File.join(dir, 'dates')
350
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['date', :Date, 8]]) do |file|
351
+ file.write(Shapelib::Point.new(1, 2, { 'date' => '20261019' }))
352
+ end
353
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
354
+ shape = file.read(0)
355
+ [:Date, :String].each do |type|
356
+ copy = File.join(dir, type.to_s)
357
+ Shapelib::ShapeFile.new(copy, Shapelib::Point, [['date', type, 8]]) { |target| target.write(shape) }
358
+ Shapelib::ShapeFile.open(copy) { |target| assert_equal '20261019', target.read(0)['date'] }
359
+ end
360
+ ['00010102', nil].each do |value|
361
+ shape['date'] = value
362
+ assert_equal value, shape['date']
363
+ assert_equal value, shape.to_h['date']
364
+ file.write(shape, 0)
365
+ assert_equal value, file.read(0)['date']
366
+ end
367
+ end
368
+ Shapelib::ShapeFile.open(filename) { |file| assert_nil file.read(0)['date'] }
369
+ end
370
+ end
371
+
372
+ def test_undefined_attributes_do_not_modify_files
373
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
374
+ Dir.mktmpdir('shapelib-test-') do |dir|
375
+ filename = File.join(dir, 'points')
376
+ fields = [['name', Shapelib::String, 10], ['other', Shapelib::Integer, 8]]
377
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
378
+ count.times { file.write(Shapelib::Point.new(1, 2, { 'name' => 'before', 'other' => 7 })) }
379
+ end
380
+ expected = nil
381
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
382
+ expected = count.times.map { |record| file.read(record).to_h }
383
+ shape = Shapelib::Point.new(9, 9, { 'name' => 'after', 'missing' => 'bad' })
384
+ error = assert_raise(ArgumentError) { file.write(shape, index) }
385
+ assert_equal 'attribute missing not defined in ShapeFile', error.message
386
+ assert_equal count, file.size
387
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
388
+ end
389
+ Shapelib::ShapeFile.open(filename) do |file|
390
+ assert_equal count, file.size
391
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
392
+ end
393
+ end
394
+ end
395
+ end
396
+
397
+ def test_over_width_string_writes_do_not_modify_files
398
+ ['toolong', 'éé'].each do |value|
399
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
400
+ Dir.mktmpdir('shapelib-test-') do |dir|
401
+ filename = File.join(dir, 'points')
402
+ fields = [['name', Shapelib::String, 10], ['short', Shapelib::String, 3]]
403
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
404
+ count.times { file.write(Shapelib::Point.new(1, 2, { 'name' => 'before', 'short' => 'old' })) }
405
+ end
406
+ expected = nil
407
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
408
+ expected = count.times.map { |record| file.read(record).to_h }
409
+ shape = Shapelib::Point.new(9, 9, { 'name' => 'after', 'short' => value })
410
+ error = assert_raise(RuntimeError) { file.write(shape, index) }
411
+ assert_equal count, file.size
412
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
413
+ assert_equal 'attribute short exceeds field width', error.message
414
+ end
415
+ Shapelib::ShapeFile.open(filename) do |file|
416
+ assert_equal count, file.size
417
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
418
+ end
419
+ end
420
+ end
421
+ end
422
+ end
423
+
424
+ def test_over_width_numeric_writes_do_not_modify_files
425
+ cases = [[:Integer, 3, 0, 12345], [:Integer, 3, 0, -100],
426
+ [:Integer, 3, 0, 999.6], [:Integer, 3, 0, -99.6],
427
+ [:Float, 5, 2, 99.999], [:Float, 5, 2, -9.999],
428
+ [:Float, 5, 2, 100], [:Float, 255, 0, Float::MAX]]
429
+ cases.each do |type, width, decimals, value|
430
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
431
+ Dir.mktmpdir('shapelib-test-') do |dir|
432
+ filename = File.join(dir, 'points')
433
+ fields = [['name', Shapelib::String, 10], ['number', type, width, decimals]]
434
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
435
+ count.times { file.write(Shapelib::Point.new(1, 2, 'name' => 'before', 'number' => 1)) }
436
+ end
437
+ expected = nil
438
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
439
+ expected = count.times.map { |record| file.read(record).to_h }
440
+ shape = Shapelib::Point.new(9, 9, 'name' => 'after', 'number' => value)
441
+ error = assert_raise(RuntimeError) { file.write(shape, index) }
442
+ assert_equal 'attribute number exceeds field width', error.message
443
+ assert_equal count, file.size
444
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
445
+ end
446
+ Shapelib::ShapeFile.open(filename) do |file|
447
+ assert_equal count, file.size
448
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
449
+ end
450
+ end
451
+ end
452
+ end
453
+ end
454
+
455
+ def test_numeric_width_boundaries_preserve_valid_writes
456
+ cases = [[:Integer, 3, 0, [999, -99, 999.4, -99.4, 12.6, 998.5, -98.5, nil],
457
+ [999, -99, 999, -99, 13, 998, -98, nil]],
458
+ [:Float, 5, 2, [99.99, -9.99, 99.994, -9.994, 12, 0, 2.125, nil],
459
+ [99.99, -9.99, 99.99, -9.99, 12.0, 0.0, 2.12, nil]],
460
+ [:Float, 255, 0, [1e200, -1e200, nil], [1e200, -1e200, nil]]]
461
+ cases.each do |type, width, decimals, values, expected|
462
+ Dir.mktmpdir('shapelib-test-') do |dir|
463
+ filename = File.join(dir, 'points')
464
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['number', type, width, decimals]]) do |file|
465
+ values.each_with_index do |value, index|
466
+ assert_equal index, file.write(Shapelib::Point.new(1, 2, 'number' => value))
467
+ end
468
+ end
469
+ Shapelib::ShapeFile.open(filename) do |file|
470
+ assert_equal values.length, file.size
471
+ assert_equal expected, values.each_index.map { |index| file.read(index)['number'] }
472
+ end
473
+ end
474
+ end
475
+ end
476
+
477
+ def test_string_width_boundaries_preserve_valid_writes
478
+ values = ['a', 'abc', '', 'é', 'éa', nil]
479
+ Dir.mktmpdir('shapelib-test-') do |dir|
480
+ filename = File.join(dir, 'points')
481
+ fields = [['short', Shapelib::String, 3], ['flag', Shapelib::Logical, 1]]
482
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
483
+ values.each_with_index do |value, index|
484
+ assert_equal index, file.write(Shapelib::Point.new(1, 2, { 'short' => value, 'flag' => 'True' }))
485
+ end
486
+ end
487
+ Shapelib::ShapeFile.open(filename) do |file|
488
+ assert_equal values.length, file.size
489
+ values.each_with_index do |value, index|
490
+ shape = file.read(index)
491
+ expected = value.nil? || value.empty? ? nil : value.b
492
+ assert_equal expected, shape['short']
493
+ assert_equal 'T', shape['flag']
494
+ end
495
+ end
496
+ end
497
+ end
498
+
499
+ def test_invalid_geometry_writes_do_not_modify_files
500
+ points = [Shapelib::Point.new(1, 2), Shapelib::PointM.new(1, 2, 3), Shapelib::PointZ.new(1, 2, 3, 4)]
501
+ arc = Shapelib::Arc.new([1, 2], [3, 4])
502
+ cases = points.permutation(2).to_a + [[points.first, arc], [arc, points.first]]
503
+ cases.map! do |before, invalid|
504
+ [before, invalid, /shape type \d+ does not match ShapeFile type \d+/]
505
+ end
506
+ points.each do |point|
507
+ cases << [point, Shapelib::Shape.new(empty_geometry(point.shape_type)), /point has no vertices/]
508
+ data = point.to_h.merge(n_vertices: 2, xvals: [9, 10], yvals: [11, 12],
509
+ zvals: [13, 14], mvals: [15, 16])
510
+ cases << [point, Shapelib::Shape.new(data), /point must have exactly one vertex \(got 2\)/]
511
+ end
512
+ [1, 2].each do |count|
513
+ data = empty_geometry(:Null).merge(n_vertices: count, xvals: Array.new(count, 9),
514
+ yvals: Array.new(count, 10), zvals: Array.new(count, 11),
515
+ mvals: Array.new(count, 12))
516
+ (points + [arc]).each do |before|
517
+ cases << [before, Shapelib::Shape.new(data), /null shape must have no vertices \(got #{count}\)/]
518
+ end
519
+ end
520
+ cases.each do |before, invalid, message|
521
+ before['name'] = 'before'
522
+ invalid['name'] = 'after'
523
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
524
+ Dir.mktmpdir('shapelib-test-') do |dir|
525
+ filename = File.join(dir, 'shapes')
526
+ Shapelib::ShapeFile.new(filename, before.shape_type, [['name', Shapelib::String, 10]]) do |file|
527
+ count.times { file.write(before) }
528
+ end
529
+ expected = nil
530
+ expected_bounds = nil
531
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
532
+ expected = count.times.map { |record| file.read(record).to_h }
533
+ expected_bounds = [file.minbound, file.maxbound]
534
+ error = assert_raise(ArgumentError) { file.write(invalid, index) }
535
+ assert_match message, error.message
536
+ assert_equal count, file.size
537
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
538
+ assert_equal expected_bounds, [file.minbound, file.maxbound]
539
+ end
540
+ Shapelib::ShapeFile.open(filename) do |file|
541
+ assert_equal count, file.size
542
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
543
+ assert_equal expected_bounds, [file.minbound, file.maxbound]
544
+ end
545
+ end
546
+ end
547
+ end
548
+ end
549
+
550
+ def test_unsupported_types_read_from_files_cannot_be_written
551
+ script = <<~'RUBY'
552
+ Process.setrlimit(Process::RLIMIT_CORE, 0, 0) if Process.const_defined?(:RLIMIT_CORE)
553
+ require 'shapelib'
554
+ filename, type, index = ARGV
555
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
556
+ shape = file.read(0)
557
+ abort 'unexpected fixture type' unless file.shape_type == type.to_i && shape.shape_type == type.to_i
558
+ begin
559
+ file.write(shape, index.to_i)
560
+ rescue ArgumentError => error
561
+ abort error.message unless error.message == "unsupported shape type #{type}"
562
+ else
563
+ abort 'unsupported write succeeded'
564
+ end
565
+ end
566
+ RUBY
567
+ [2, 99].product([-1, 0]).each do |type, index|
568
+ Dir.mktmpdir('shapelib-test-') do |dir|
569
+ filename = File.join(dir, 'shapes')
570
+ Shapelib::ShapeFile.new(filename, :Point, [['name', :String, 10]]) do |file|
571
+ file.write(Shapelib::Point.new(1, 2, 'name' => 'before'))
572
+ end
573
+ paths = %w[shp shx dbf].map { |extension| "#{filename}.#{extension}" }
574
+ paths.first(2).each do |path|
575
+ bytes = File.binread(path)
576
+ bytes[32, 4] = [type].pack('V')
577
+ bytes[108, 4] = [type].pack('V') if path.end_with?('.shp')
578
+ File.binwrite(path, bytes)
579
+ end
580
+ before = paths.map { |path| File.binread(path) }
581
+ load_paths = $LOAD_PATH.map { |path| "-I#{path}" }
582
+ _stdout, stderr, status = Open3.capture3(RbConfig.ruby, *load_paths, '-e', script,
583
+ filename, type.to_s, index.to_s)
584
+ assert_true status.success?, "subprocess failed: #{stderr}"
585
+ assert_equal before, paths.map { |path| File.binread(path) }
586
+ end
587
+ end
588
+ end
589
+
590
+ def test_invalid_part_layout_writes_do_not_modify_files
591
+ [:Arc, :ArcM, :ArcZ, :Polygon, :PolygonM, :PolygonZ, :MultiPatch].each do |type|
592
+ data = hash_geometry.merge(shape_type: type)
593
+ before = Shapelib::Shape.new(data.merge('name' => 'before'))
594
+ cases = [[data, [0, 3, 2]], [data, [0, 0]], [data, [0, 2, 2]],
595
+ [data, [0, 4]], [empty_geometry(type), [0, 0]]]
596
+ cases.each do |geometry, starts|
597
+ invalid = Shapelib::Shape.new(geometry.merge(n_parts: starts.length,
598
+ part_start: starts,
599
+ part_type: Array.new(starts.length, :Ring),
600
+ 'name' => 'after'))
601
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
602
+ Dir.mktmpdir('shapelib-test-') do |dir|
603
+ filename = File.join(dir, 'shapes')
604
+ Shapelib::ShapeFile.new(filename, type, [['name', Shapelib::String, 10]]) do |file|
605
+ count.times { file.write(before) }
606
+ end
607
+ expected = nil
608
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
609
+ expected = count.times.map { |record| file.read(record).to_h }
610
+ error = assert_raise(ArgumentError) { file.write(invalid, index) }
611
+ assert_match(/part_start\[\d+\]/, error.message)
612
+ assert_equal count, file.size
613
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
614
+ end
615
+ Shapelib::ShapeFile.open(filename) do |file|
616
+ assert_equal count, file.size
617
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
618
+ end
619
+ end
620
+ end
621
+ end
622
+ end
623
+ end
624
+
625
+ def test_valid_part_layouts_and_empty_shapes_round_trip
626
+ [:Arc, :ArcM, :ArcZ, :Polygon, :PolygonM, :PolygonZ, :MultiPatch].each do |type|
627
+ data = hash_geometry.merge(shape_type: type)
628
+ # Native builds may omit MultiPatch measures when writing.
629
+ data[:mvals] = Array.new(data[:n_vertices], 0) if type == :MultiPatch
630
+ shapes = [Shapelib::Shape.new(data),
631
+ Shapelib::Shape.new(empty_geometry(type))]
632
+ shapes.each { |shape| shape['name'] = 'example' }
633
+ Dir.mktmpdir('shapelib-test-') do |dir|
634
+ filename = File.join(dir, 'shapes')
635
+ Shapelib::ShapeFile.new(filename, type, [['name', Shapelib::String, 10]]) do |file|
636
+ shapes.each_with_index { |shape, index| assert_equal index, file.write(shape) }
637
+ end
638
+ Shapelib::ShapeFile.open(filename) do |file|
639
+ assert_equal shapes.length, file.size
640
+ shapes.each_with_index do |shape, index|
641
+ assert_equal shape.to_h.merge(shape_id: index), file.read(index).to_h
642
+ end
643
+ end
644
+ end
645
+ end
646
+ end
647
+
648
+ def test_matching_geometry_and_null_writes_round_trip
649
+ shapes = [Shapelib::Point.new(1, 2), Shapelib::PointM.new(1, 2, 3),
650
+ Shapelib::PointZ.new(1, 2, 3, 4), Shapelib::Arc.new([1, 2], [3, 4])]
651
+ shapes.each do |shape|
652
+ shape['name'] = 'before'
653
+ Dir.mktmpdir('shapelib-test-') do |dir|
654
+ filename = File.join(dir, 'shapes')
655
+ Shapelib::ShapeFile.new(filename, shape.shape_type, [['name', Shapelib::String, 10]]) do |file|
656
+ assert_equal 0, file.write(shape)
657
+ assert_equal 1, file.write(Shapelib::Shape.new(empty_geometry(0)))
658
+ end
659
+ Shapelib::ShapeFile.open(filename) do |file|
660
+ assert_equal 2, file.size
661
+ assert_equal shape.to_h.merge(shape_id: 0), file.read(0).to_h
662
+ assert_nil file.read(1)
663
+ end
664
+ end
665
+ end
666
+ end
667
+
668
+ def test_incompatible_attribute_types_do_not_modify_files
669
+ cases = [[Shapelib::String, 12, 0, 7], [Shapelib::String, 12, 0, 1.5],
670
+ [Shapelib::Logical, 1, 0, 7], [Shapelib::Logical, 1, 0, 1.5],
671
+ [Shapelib::Integer, 8, 0, '12345678'], [Shapelib::Float, 12, 2, '12345678']]
672
+ cases.each do |type, width, decimals, value|
673
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
674
+ Dir.mktmpdir('shapelib-test-') do |dir|
675
+ filename = File.join(dir, 'points')
676
+ fields = [['name', Shapelib::String, 10], ['value', type, width, decimals]]
677
+ before = type == Shapelib::String ? 'before' : type == Shapelib::Logical ? 'T' : 7
678
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
679
+ count.times { file.write(Shapelib::Point.new(1, 2, { 'name' => 'before', 'value' => before })) }
680
+ end
681
+ expected = nil
682
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
683
+ expected = count.times.map { |record| file.read(record).to_h }
684
+ shape = Shapelib::Point.new(9, 9, { 'name' => 'after', 'value' => value })
685
+ error = assert_raise(TypeError) { file.write(shape, index) }
686
+ assert_equal 'attribute value has incompatible type for ShapeFile field', error.message
687
+ assert_equal count, file.size
688
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
689
+ end
690
+ Shapelib::ShapeFile.open(filename) do |file|
691
+ assert_equal count, file.size
692
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
693
+ end
694
+ end
695
+ end
696
+ end
697
+ end
698
+
699
+ def test_invalid_logical_values_do_not_modify_files
700
+ ['true', 'false', 't', 'f', '', '?', 'Y', 'N'].each do |value|
701
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
702
+ Dir.mktmpdir('shapelib-test-') do |dir|
703
+ filename = File.join(dir, 'points')
704
+ fields = [['name', Shapelib::String, 10], ['flag', Shapelib::Logical, 1]]
705
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
706
+ count.times { file.write(Shapelib::Point.new(1, 2, { 'name' => 'before', 'flag' => 'T' })) }
707
+ end
708
+ expected = expected_bounds = nil
709
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
710
+ expected = count.times.map { |record| file.read(record).to_h }
711
+ expected_bounds = [file.minbound, file.maxbound]
712
+ shape = Shapelib::Point.new(9, 9, { 'name' => 'after', 'flag' => value })
713
+ error = assert_raise(ArgumentError) { file.write(shape, index) }
714
+ assert_equal 'attribute flag must start with T or F for a Logical field', error.message
715
+ assert_equal count, file.size
716
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
717
+ assert_equal expected_bounds, [file.minbound, file.maxbound]
718
+ end
719
+ Shapelib::ShapeFile.open(filename) do |file|
720
+ assert_equal count, file.size
721
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
722
+ assert_equal expected_bounds, [file.minbound, file.maxbound]
723
+ end
724
+ end
725
+ end
726
+ end
727
+ end
728
+
729
+ def test_logical_prefixes_and_null_replacements_round_trip
730
+ Dir.mktmpdir('shapelib-test-') do |dir|
731
+ filename = File.join(dir, 'points')
732
+ values = ['T', 'F', 'TRUE', 'False', 'Tanything', 'Fanything', nil]
733
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['flag', Shapelib::Logical, 1]]) do |file|
734
+ values.each { |value| file.write(Shapelib::Point.new(1, 2, { 'flag' => value })) }
735
+ end
736
+ values.each_with_index do |value, index|
737
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
738
+ assert_equal value && value[0], file.read(index)['flag']
739
+ assert_equal index, file.write(Shapelib::Point.new(3, 4, { 'flag' => 'T' }), index)
740
+ assert_equal index, file.write(Shapelib::Point.new(5, 6, { 'flag' => value }), index)
741
+ assert_equal value && value[0], file.read(index)['flag']
742
+ end
743
+ Shapelib::ShapeFile.open(filename) do |file|
744
+ assert_equal values.length, file.size
745
+ assert_equal value && value[0], file.read(index)['flag']
746
+ end
747
+ end
748
+ end
749
+ end
750
+
751
+ def test_compatible_attribute_types_round_trip
752
+ Dir.mktmpdir('shapelib-test-') do |dir|
753
+ source = File.join(dir, 'logical')
754
+ Shapelib::ShapeFile.new(source, Shapelib::Point, [['value', Shapelib::Logical, 1]]) do |file|
755
+ file.write(Shapelib::Point.new(1, 2, { 'value' => 'T' }))
756
+ end
757
+ logical = nil
758
+ Shapelib::ShapeFile.open(source) { |file| logical = file.read(0) }
759
+ cases = [[Shapelib::Integer, 8, 0, [7, 7.0, nil]],
760
+ [Shapelib::Float, 12, 2, [7, 7.5, nil]],
761
+ [Shapelib::String, 12, 0, ['text', logical, nil]],
762
+ [Shapelib::Logical, 1, 0, ['T', 'F', logical, nil]]]
763
+ cases.each_with_index do |(type, width, decimals, values), index|
764
+ filename = File.join(dir, "points#{index}")
765
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['value', type, width, decimals]]) do |file|
766
+ values.each_with_index do |value, record|
767
+ shape = value.is_a?(Shapelib::Shape) ? value : Shapelib::Point.new(1, 2, { 'value' => value })
768
+ assert_equal record, file.write(shape)
769
+ end
770
+ end
771
+ Shapelib::ShapeFile.open(filename) do |file|
772
+ assert_equal values.length, file.size
773
+ values.each_with_index do |value, record|
774
+ shape = file.read(record)
775
+ assert_equal [1.0], shape.xvals
776
+ assert_equal [2.0], shape.yvals
777
+ assert_equal value.is_a?(Shapelib::Shape) ? value['value'] : value, shape['value']
778
+ end
779
+ end
780
+ end
781
+ end
782
+ end
783
+
784
+ def test_writes_ignore_cleared_attributes_and_null_omitted_fields
785
+ Dir.mktmpdir('shapelib-test-') do |dir|
786
+ filename = File.join(dir, 'points')
787
+ fields = [['name', Shapelib::String, 10], ['other', Shapelib::Integer, 8]]
788
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
789
+ shape = Shapelib::Point.new(1, 2, { 'name' => 'valid', 'missing' => 'discard' })
790
+ shape['missing'] = nil
791
+ assert_equal 0, file.write(shape)
792
+ assert_equal 1, file.write(Shapelib::Point.new(3, 4))
793
+ end
794
+ Shapelib::ShapeFile.open(filename) do |file|
795
+ assert_equal 2, file.size
796
+ first = file.read(0)
797
+ assert_equal 'valid', first['name']
798
+ assert_nil first['other']
799
+ second = file.read(1)
800
+ assert_equal [3.0], second.xvals
801
+ assert_nil second['name']
802
+ assert_nil second['other']
803
+ end
804
+ end
805
+ end
806
+
807
+ def test_sequential_reads_find_appended_records_after_eof
808
+ cases = [[[], 1, false], [[1], 1, false], [[1], 3, false],
809
+ [[1, nil, nil], 3, false], [[nil, nil], 3, false],
810
+ [[1], 3, true], [[1, nil], :each, false]]
811
+ cases.each do |records, eof_reads, append_null|
812
+ Dir.mktmpdir('shapelib-test-') do |dir|
813
+ filename = File.join(dir, 'points')
814
+ Shapelib::ShapeFile.new(filename, Shapelib::Point) do |file|
815
+ records.each do |value|
816
+ shape = value.nil? ? Shapelib::Shape.new(empty_geometry(:Null)) : Shapelib::Point.new(value, -value)
817
+ file.write(shape)
818
+ end
819
+ expected_ids = records.each_index.reject { |index| records[index].nil? }
820
+ if eof_reads == :each
821
+ ids = []
822
+ file.each { |shape| ids << shape.shape_id }
823
+ assert_equal expected_ids, ids
824
+ else
825
+ expected_ids.each { |index| assert_equal index, file.read.shape_id }
826
+ eof_reads.times { assert_nil file.read }
827
+ end
828
+ file.write(Shapelib::Shape.new(empty_geometry(:Null))) if append_null
829
+ index = file.write(Shapelib::Point.new(9, -9))
830
+ shape = file.read
831
+ assert_not_nil shape
832
+ assert_equal index, shape.shape_id
833
+ assert_equal [9.0], shape.xvals
834
+ assert_equal [-9.0], shape.yvals
835
+ assert_nil file.read
836
+ end
837
+ end
838
+ end
839
+ end
840
+
841
+ def test_null_records_are_skipped_only_by_sequential_reads
842
+ [[nil, 1], [1, nil, 2], [1, nil], [nil, nil, 1],
843
+ [1, nil, nil, 2], [nil, nil]].each do |records|
844
+ Dir.mktmpdir('shapelib-test-') do |dir|
845
+ filename = File.join(dir, 'points')
846
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['id', Shapelib::Integer, 8]]) do |file|
847
+ records.each_with_index do |value, index|
848
+ shape = if value.nil?
849
+ Shapelib::Shape.new(empty_geometry(:Null).merge('id' => index))
850
+ else
851
+ Shapelib::Point.new(value, -value, { 'id' => index })
852
+ end
853
+ file.write(shape)
854
+ end
855
+ end
856
+ expected = records.each_index.reject { |index| records[index].nil? }
857
+ Shapelib::ShapeFile.open(filename) do |file|
858
+ assert_equal records.length, file.size
859
+ records.each_with_index do |value, index|
860
+ shape = file.read(index)
861
+ if value.nil?
862
+ assert_nil shape
863
+ following = expected.find { |record| record > index }
864
+ row = file.read
865
+ following.nil? ? assert_nil(row) : assert_equal(following, row['id'])
866
+ else
867
+ assert_equal [value.to_f], shape.xvals
868
+ assert_equal index, shape['id']
869
+ end
870
+ end
871
+ file.rewind
872
+ assert_equal expected, expected.map { file.read['id'] }
873
+ assert_nil file.read
874
+ assert_nil file.read
875
+ 2.times do
876
+ ids = []
877
+ file.each { |shape| ids << shape['id'] }
878
+ assert_equal expected, ids
879
+ end
880
+ assert_equal records.length, file.size
881
+ end
882
+ end
883
+ end
884
+ end
885
+
886
+ def test_unreadable_dbf_records_raise_and_leave_intact_records_readable
887
+ [0, 1, 3, 6, 10, :short_count].each do |damage|
888
+ Dir.mktmpdir('shapelib-test-') do |dir|
889
+ filename = File.join(dir, 'points')
890
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, [['name', :String, 10]]) do |file|
891
+ file.write(Shapelib::Point.new(1, 2, 'name' => 'first'))
892
+ file.write(Shapelib::Point.new(3, 4, 'name' => 'second'))
893
+ end
894
+ if damage != :short_count
895
+ header = File.binread("#{filename}.dbf", 12)
896
+ header_length, record_length = header[8, 4].unpack('v2')
897
+ File.truncate("#{filename}.dbf", header_length + record_length + damage)
898
+ else
899
+ File.open("#{filename}.dbf", 'rb+') { |file| file.seek(4); file.write([1].pack('V')) }
900
+ end
901
+ [:direct, :sequential, :each].each do |method|
902
+ Shapelib::ShapeFile.open(filename) do |file|
903
+ first = file.read(0).to_h
904
+ assert_equal 'first', first['name']
905
+ yielded = []
906
+ error = assert_raise(IOError) do
907
+ case method
908
+ when :direct then file.read(1)
909
+ when :sequential then file.read
910
+ when :each then file.each { |shape| yielded << shape.to_h }
911
+ end
912
+ end
913
+ assert_equal 'cannot read DBF record 1', error.message
914
+ assert_equal [first], yielded if method == :each
915
+ assert_nil file.read
916
+ assert_equal first, file.read(0).to_h
917
+ end
918
+ end
919
+ end
920
+ end
921
+ end
922
+
923
+ def test_null_attributes_and_files_without_attributes_remain_readable
924
+ [:nulls, :no_fields, :missing_dbf].each do |kind|
925
+ Dir.mktmpdir('shapelib-test-') do |dir|
926
+ filename = File.join(dir, 'points')
927
+ fields = kind == :no_fields ? [] : [['name', :String, 10], ['amount', :Float, 12, 2], ['flag', :Logical, 1]]
928
+ Shapelib::ShapeFile.new(filename, Shapelib::Point, fields) do |file|
929
+ 2.times { |index| file.write(Shapelib::Point.new(index, index + 1)) }
930
+ end
931
+ File.unlink("#{filename}.dbf") if kind == :missing_dbf
932
+ Shapelib::ShapeFile.open(filename) do |file|
933
+ shapes = []
934
+ file.each { |shape| shapes << shape }
935
+ assert_equal 2, shapes.length
936
+ shapes.each_with_index do |shape, index|
937
+ assert_equal [index.to_f], shape.xvals
938
+ assert_equal [(index + 1).to_f], shape.yvals
939
+ %w[name amount flag].each { |name| assert_nil shape[name] }
940
+ assert_equal shape.to_h, file.read(index).to_h
941
+ end
942
+ assert_nil file.read
943
+ end
944
+ end
945
+ end
946
+ end
947
+
948
+ def test_geometry_only_reads_and_rejects_writes
949
+ Dir.mktmpdir('shapelib-test-') do |dir|
950
+ filename = File.join(dir, 'points')
951
+ Shapelib::ShapeFile.new(filename, Shapelib::Point) do |file|
952
+ file.write(Shapelib::Point.new(1, 2))
953
+ end
954
+ File.unlink("#{filename}.dbf")
955
+ paths = %w[shp shx].map { |suffix| "#{filename}.#{suffix}" }
956
+ contents = paths.map { |path| File.binread(path) }
957
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
958
+ assert_equal 1, file.size
959
+ assert_equal(-1, file.field_count)
960
+ assert_nil file.fields
961
+ assert_equal [1.0], file.read(0).xvals
962
+ shapes = []
963
+ file.each { |shape| shapes << shape }
964
+ assert_equal 1, shapes.size
965
+ assert_equal [2.0], shapes.first.yvals
966
+ assert_raise(IOError) { file.write(Shapelib::Point.new(3, 4)) }
967
+ end
968
+ assert_equal contents, paths.map { |path| File.binread(path) }
969
+ end
970
+ end
971
+
972
+ def test_hash_constructor_rejects_negative_counts
973
+ [:n_vertices, :n_parts].each do |key|
974
+ data = hash_geometry
975
+ data[key] = -1
976
+ assert_raise(ArgumentError) { Shapelib::Shape.new(data) }
977
+ end
978
+ end
979
+
980
+ def test_hash_constructor_rejects_short_arrays
981
+ [:part_type, :part_start, :xvals, :yvals, :zvals, :mvals].each do |key|
982
+ data = hash_geometry
983
+ count = data[[:part_type, :part_start].include?(key) ? :n_parts : :n_vertices]
984
+ data[key] = data[key].take(count - 1)
985
+ error = assert_raise(ArgumentError) { Shapelib::Shape.new(data) }
986
+ assert_match(/#{key}.*#{count}/, error.message)
987
+ end
988
+ end
989
+
990
+ def test_hash_constructor_rejects_out_of_bounds_part_starts
991
+ [:ArcZ, :PolygonZ].each do |type|
992
+ [-1, 5].each do |start|
993
+ data = hash_geometry.merge(shape_type: type, part_start: [0, start])
994
+ error = assert_raise(ArgumentError) { Shapelib::Shape.new(data) }
995
+ assert_equal 'part_start[1] must be between 0 and 4', error.message
996
+ end
997
+ end
998
+ GC.start
999
+ end
1000
+
1001
+ def test_hash_constructor_preserves_part_wkt
1002
+ { ArcZ: 'MULTILINESTRING((0 1, 4 5), (8 9, 12 13))',
1003
+ PolygonZ: 'POLYGON((0 1, 4 5), (8 9, 12 13))' }.each do |type, wkt|
1004
+ shape = Shapelib::Shape.new(hash_geometry.merge(shape_type: type))
1005
+ assert_equal [0, 2], shape.part_start
1006
+ assert_equal wkt, shape.wkt
1007
+ end
1008
+ end
1009
+
1010
+ def test_coordinate_reads_and_iteration_with_frozen_shapes
1011
+ [false, true].each do |frozen|
1012
+ [false, true].each do |cache_first|
1013
+ cases = [[Shapelib::Point.new(1, 2), [[2.0, 1.0]]],
1014
+ [Shapelib::Arc.new([[1, 2], [3, 4]], [[5, 6], [7, 8]]),
1015
+ [[2.0, 1.0], [4.0, 3.0], [6.0, 5.0], [8.0, 7.0]]],
1016
+ [Shapelib::Shape.new(empty_geometry(:Null)), []]]
1017
+ cases.each do |shape, expected|
1018
+ cached = shape.latlngs if cache_first
1019
+ shape.freeze if frozen
1020
+ before = shape.instance_variables.map { |name| [name, shape.instance_variable_get(name)] }
1021
+ coordinates = shape.latlngs
1022
+ assert_equal expected, coordinates
1023
+ points = []
1024
+ assert_equal expected, shape.each { |point| points << point }
1025
+ assert_equal expected, points
1026
+ if frozen
1027
+ assert_equal before, shape.instance_variables.map { |name| [name, shape.instance_variable_get(name)] }
1028
+ end
1029
+ assert_same cached, coordinates if cache_first
1030
+ assert_same coordinates, shape.latlngs unless frozen && !cache_first
1031
+ end
1032
+ end
1033
+ end
1034
+ end
1035
+
1036
+ def test_rewind_polygon_refreshes_cached_latlngs_and_iteration
1037
+ [Shapelib::Polygon, Shapelib::PolygonM, Shapelib::PolygonZ].each do |klass|
1038
+ [false, true].each do |cache_first|
1039
+ shape = klass.new([0, 0], [2, 0], [2, 1], [0, 0])
1040
+ before = shape.yvals.zip(shape.xvals)
1041
+ shape.latlngs if cache_first
1042
+ assert_equal true, shape.rewind_polygon
1043
+ expected = shape.yvals.zip(shape.xvals)
1044
+ assert_not_equal before, expected
1045
+ assert_equal expected, shape.latlngs
1046
+ points = []
1047
+ shape.each { |point| points << point }
1048
+ assert_equal expected, points
1049
+ refreshed = shape.latlngs
1050
+ assert_equal false, shape.rewind_polygon
1051
+ assert_same refreshed, shape.latlngs
1052
+ end
1053
+ end
1054
+ end
1055
+
1056
+ def test_rewind_polygon_rejects_frozen_shapes_without_mutation
1057
+ [Shapelib::Polygon, Shapelib::PolygonM, Shapelib::PolygonZ].each do |klass|
1058
+ [false, true].each do |cache_first|
1059
+ shape = klass.new([0, 0], [2, 0], [2, 1], [0, 0])
1060
+ cached = shape.latlngs if cache_first
1061
+ before = shape.to_h
1062
+ shape.freeze
1063
+ assert_raise(FrozenError) { shape.rewind_polygon }
1064
+ assert_equal before, shape.to_h
1065
+ assert_same cached, shape.latlngs if cache_first
1066
+ end
1067
+ end
1068
+ end
1069
+
1070
+ def test_hash_constructor_preserves_empty_parts
1071
+ { ArcZ: 'MULTILINESTRING', PolygonZ: 'POLYGON' }.each do |type, prefix|
1072
+ { [0, 0] => '((), (0 1, 4 5, 8 9, 12 13))',
1073
+ [0, 4] => '((0 1, 4 5, 8 9, 12 13), ())' }.each do |starts, body|
1074
+ shape = Shapelib::Shape.new(hash_geometry.merge(shape_type: type, part_start: starts))
1075
+ assert_equal starts, shape.part_start
1076
+ assert_equal prefix + body, shape.wkt
1077
+ end
1078
+ end
1079
+ end
1080
+
1081
+ def test_wkt_preserves_many_empty_parts
1082
+ { Arc: 'MULTILINESTRING', ArcM: 'MULTILINESTRING', ArcZ: 'MULTILINESTRING',
1083
+ Polygon: 'POLYGON', PolygonM: 'POLYGON', PolygonZ: 'POLYGON' }.each do |type, prefix|
1084
+ [2, 4, 8].each do |count|
1085
+ data = empty_geometry(type).merge(n_parts: count,
1086
+ part_type: Array.new(count, 0),
1087
+ part_start: Array.new(count, 0))
1088
+ expected = "#{prefix}(#{Array.new(count, '()').join(', ')})"
1089
+ assert_equal expected, Shapelib::Shape.new(data).wkt
1090
+ end
1091
+ parts = Array.new(32, '()') + ['(1 2, 3 4)'] + Array.new(32, '()')
1092
+ data = empty_geometry(type).merge(n_vertices: 2, n_parts: parts.length,
1093
+ part_type: Array.new(parts.length, 0),
1094
+ part_start: Array.new(33, 0) + Array.new(32, 2),
1095
+ xvals: [1, 3], yvals: [2, 4],
1096
+ zvals: [0, 0], mvals: [nil, nil])
1097
+ assert_equal "#{prefix}(#{parts.join(', ')})", Shapelib::Shape.new(data).wkt
1098
+ end
1099
+ end
1100
+
1101
+ def test_multipoint_wkt_preserves_closing_parenthesis
1102
+ [Shapelib::MultiPoint, Shapelib::MultiPointM, Shapelib::MultiPointZ].each do |klass|
1103
+ assert_equal 'MULTIPOINT()', klass.new.wkt
1104
+ assert_equal 'MULTIPOINT(1 2, 3 4)', klass.new([1, 2], [3, 4]).wkt
1105
+ end
1106
+ end
1107
+
1108
+ def test_hash_constructor_preserves_part_start_normalization_and_prefix_counts
1109
+ [:ArcZ, :PolygonZ].each do |type|
1110
+ expected = Shapelib::Shape.new(hash_geometry.merge(shape_type: type)).to_h
1111
+ [-1, 1, 5].each do |first|
1112
+ data = hash_geometry.merge(shape_type: type, part_start: [first, 2, -1, 5])
1113
+ assert_equal expected, Shapelib::Shape.new(data).to_h
1114
+ end
1115
+ end
1116
+ end
1117
+
1118
+ def test_hash_constructor_preserves_matching_counts
1119
+ data = hash_geometry
1120
+ data['name'] = 'example'
1121
+ assert_equal data, Shapelib::Shape.new(data).to_h
1122
+ end
1123
+
1124
+ def test_hash_constructor_preserves_explicit_prefix_counts
1125
+ data = hash_geometry
1126
+ data[:n_parts] = 1
1127
+ data[:n_vertices] = 2
1128
+ shape = Shapelib::Shape.new(data)
1129
+ assert_equal 1, shape.n_parts
1130
+ assert_equal 2, shape.n_vertices
1131
+ [:xvals, :yvals, :zvals, :mvals, :part_start, :part_type].each do |key|
1132
+ count = [:part_start, :part_type].include?(key) ? 1 : 2
1133
+ assert_equal data[key].take(count), shape.to_h[key]
1134
+ end
1135
+ end
1136
+
1137
+ def test_hash_constructor_reads_each_field_once
1138
+ data = hash_geometry
1139
+ reads = Hash.new(0)
1140
+ input = Object.new
1141
+ input.define_singleton_method(:[]) do |key|
1142
+ reads[key] += 1
1143
+ raise 'field fetched twice' if reads[key] > 1
1144
+ data[key]
1145
+ end
1146
+ assert_equal data, Shapelib::Shape.new(input).to_h
1147
+ expected = [:shape_type, :shape_id, :n_parts, :n_vertices,
1148
+ :part_type, :part_start, :xvals, :yvals, :zvals, :mvals]
1149
+ assert_equal expected.sort, reads.keys.sort
1150
+ assert_equal [1], reads.values.uniq
1151
+ end
1152
+
1153
+ def test_hash_constructor_preserves_conversion_exceptions_and_recovers
1154
+ [2, 512].each do |size|
1155
+ [:part_type, :part_start, :xvals, :yvals, :zvals, :mvals].each do |key|
1156
+ [0, size - 1].each do |index|
1157
+ data = { shape_type: :ArcZ, n_parts: size, n_vertices: size,
1158
+ part_type: Array.new(size, :Ring), part_start: (0...size).to_a,
1159
+ xvals: Array.new(size, 1.0), yvals: Array.new(size, 2.0),
1160
+ zvals: Array.new(size, 4.0), mvals: Array.new(size, 3.0) }
1161
+ expected = Shapelib::Shape.new(data).to_h
1162
+ original = key == :part_type ? 5 : data[key][index]
1163
+ error = RuntimeError.new('conversion failed')
1164
+ fail_conversion = true
1165
+ value = Object.new
1166
+ conversion = [:part_type, :part_start].include?(key) ? :to_int : :to_f
1167
+ value.define_singleton_method(conversion) do
1168
+ GC.start
1169
+ raise error if fail_conversion
1170
+ original
1171
+ end
1172
+ data[key][index] = value
1173
+ assert_same error, assert_raise(RuntimeError) { Shapelib::Shape.new(data) }
1174
+ fail_conversion = false
1175
+ assert_equal expected, Shapelib::Shape.new(data).to_h
1176
+ end
1177
+ end
1178
+ end
1179
+ end
1180
+
1181
+ def test_convenience_constructors_preserve_conversion_exceptions_and_recover
1182
+ [2, 512].each do |size|
1183
+ [:single, :multipart, :multipoint, :multipoint_list].each do |form|
1184
+ [:to_f, :x, :y, :m, :z].each do |failure|
1185
+ points = Array.new(size) { [1, 2, 3, 4] }
1186
+ build = case form
1187
+ when :single then -> { Shapelib::ArcZ.new(*points) }
1188
+ when :multipart then -> { Shapelib::ArcZ.new(*points.map { |point| [point] }) }
1189
+ when :multipoint then -> { Shapelib::MultiPointZ.new(*points) }
1190
+ when :multipoint_list then -> { Shapelib::MultiPointZ.new(points) }
1191
+ end
1192
+ expected = build.call.to_h
1193
+ error = RuntimeError.new('conversion failed')
1194
+ fail_conversion = true
1195
+ value = Object.new
1196
+ if failure == :to_f
1197
+ value.define_singleton_method(:to_f) do
1198
+ GC.start
1199
+ raise error if fail_conversion
1200
+ 4.0
1201
+ end
1202
+ points[-1][-1] = value
1203
+ else
1204
+ [:x, :y, :m, :z].each_with_index do |method, index|
1205
+ value.define_singleton_method(method) do
1206
+ GC.start
1207
+ raise error if fail_conversion && method == failure
1208
+ index + 1.0
1209
+ end
1210
+ end
1211
+ points[-1] = value
1212
+ end
1213
+ assert_same error, assert_raise(RuntimeError) { build.call }
1214
+ fail_conversion = false
1215
+ assert_equal expected, build.call.to_h
1216
+ end
1217
+ end
1218
+ end
1219
+ end
1220
+
1221
+ def test_hash_constructor_snapshots_arrays_before_numeric_conversion
1222
+ [:part_type, :part_start, :xvals, :yvals, :zvals, :mvals].each do |key|
1223
+ [:grow, :clear].each do |mutation|
1224
+ data = hash_geometry
1225
+ expected = data.transform_values { |value| value.is_a?(Array) ? value.dup : value }
1226
+ array = data[key]
1227
+ original = array[0]
1228
+ number = key == :part_type ? 5 : original
1229
+ conversion = [:part_type, :part_start].include?(key) ? :to_int : :to_f
1230
+ value = Object.new
1231
+ value.define_singleton_method(conversion) do
1232
+ mutation == :grow ? array.concat([original] * 16) : array.clear
1233
+ number
1234
+ end
1235
+ array[0] = value
1236
+ assert_equal expected, Shapelib::Shape.new(data).to_h
1237
+ assert_equal mutation == :grow ? expected[key].length + 16 : 0, array.length
1238
+ end
1239
+ end
1240
+ end
1241
+
1242
+ def test_hash_constructor_snapshots_later_arrays_before_numeric_conversion
1243
+ [:part_type, :part_start, :xvals, :yvals, :zvals, :mvals].each_cons(2) do |key, later_key|
1244
+ data = hash_geometry
1245
+ expected = data.transform_values { |value| value.is_a?(Array) ? value.dup : value }
1246
+ later = data[later_key]
1247
+ number = key == :part_type ? 5 : data[key][0]
1248
+ conversion = [:part_type, :part_start].include?(key) ? :to_int : :to_f
1249
+ value = Object.new
1250
+ value.define_singleton_method(conversion) do
1251
+ later.clear
1252
+ number
1253
+ end
1254
+ data[key][0] = value
1255
+ assert_equal expected, Shapelib::Shape.new(data).to_h
1256
+ assert_empty later
1257
+ end
1258
+ end
1259
+
1260
+ def test_hash_constructor_preserves_optional_xy_arrays
1261
+ data = hash_geometry
1262
+ data[:xvals] = data[:yvals] = nil
1263
+ shape = Shapelib::Shape.new(data)
1264
+ assert_equal [0.0] * data[:n_vertices], shape.xvals
1265
+ assert_equal [0.0] * data[:n_vertices], shape.yvals
1266
+ assert_equal data[:zvals], shape.zvals
1267
+ assert_equal data[:mvals], shape.mvals
1268
+ end
1269
+
1270
+ def test_hash_constructor_still_requires_counts_and_arrays
1271
+ [:n_vertices, :n_parts, :part_type, :part_start].each do |key|
1272
+ data = hash_geometry
1273
+ data[key] = nil
1274
+ assert_raise(TypeError) { Shapelib::Shape.new(data) }
1275
+ end
1276
+ [:zvals, :mvals].each do |key|
1277
+ data = hash_geometry
1278
+ data[key] = nil
1279
+ error = assert_raise(RuntimeError) { Shapelib::Shape.new(data) }
1280
+ assert_equal "missing #{key}", error.message
1281
+ end
1282
+ end
1283
+
1284
+ def test_empty_points_reject_coordinate_reads_and_wkt
1285
+ { Point: [:m, :z], PointM: [:z], PointZ: [] }.each do |type, absent|
1286
+ shape = Shapelib::Shape.new(empty_geometry(type))
1287
+ assert_equal 0, shape.n_vertices
1288
+ assert_equal type, shape.to_h[:shape_type]
1289
+ ([:x, :y, :m, :z, :wkt, :to_s] - absent).each do |method|
1290
+ error = assert_raise(ArgumentError) { shape.public_send(method) }
1291
+ assert_equal 'point has no vertices', error.message
1292
+ end
1293
+ absent.each { |method| assert_nil shape.public_send(method) }
1294
+ end
1295
+ end
1296
+
1297
+ def test_empty_lines_reject_wkt
1298
+ [Shapelib::Arc, Shapelib::ArcM, Shapelib::ArcZ].each do |klass|
1299
+ [klass.new(:Ring), Shapelib::Shape.new(empty_geometry(klass))].each do |shape|
1300
+ assert_equal 0, shape.n_vertices
1301
+ [:wkt, :to_s].each do |method|
1302
+ error = assert_raise(ArgumentError) { shape.public_send(method) }
1303
+ assert_equal 'cannot format an empty line', error.message
1304
+ end
1305
+ end
1306
+ end
1307
+ end
1308
+
1309
+ def test_constructors_reject_unsupported_numeric_shape_types
1310
+ [-1, 2, 4, 32, 99].each do |type|
1311
+ error = assert_raise(ArgumentError) { Shapelib::Shape.new(empty_geometry(type)) }
1312
+ assert_equal "invalid shape_type #{type}", error.message
1313
+ Dir.mktmpdir('shapelib-test-') do |dir|
1314
+ error = assert_raise(ArgumentError) { Shapelib::ShapeFile.new(File.join(dir, 'shapes'), type) }
1315
+ assert_equal "invalid shape_type #{type}", error.message
1316
+ assert_empty Dir.children(dir)
1317
+ end
1318
+ end
1319
+ end
1320
+
1321
+ def test_constructors_accept_supported_numeric_shape_types
1322
+ { 0 => :Null, 1 => :Point, 3 => :Arc, 5 => :Polygon, 8 => :MultiPoint,
1323
+ 11 => :PointZ, 13 => :ArcZ, 15 => :PolygonZ, 18 => :MultiPointZ,
1324
+ 21 => :PointM, 23 => :ArcM, 25 => :PolygonM, 28 => :MultiPointM,
1325
+ 31 => :MultiPatch }.each do |type, name|
1326
+ assert_equal name, Shapelib::Shape.new(empty_geometry(type)).shape_type
1327
+ Dir.mktmpdir('shapelib-test-') do |dir|
1328
+ filename = File.join(dir, 'shapes')
1329
+ Shapelib::ShapeFile.new(filename, type) do |file|
1330
+ assert_equal name, file.shape_type
1331
+ assert_equal 0, file.size
1332
+ end
1333
+ Shapelib::ShapeFile.open(filename) { |file| assert_equal name, file.shape_type }
1334
+ end
1335
+ end
1336
+ end
1337
+
1338
+ def test_null_shape_types_reject_wkt
1339
+ { Null: 0, 0 => 0 }.each do |type, number|
1340
+ shape = Shapelib::Shape.new(empty_geometry(type))
1341
+ assert_equal :Null, shape.shape_type
1342
+ assert_equal shape.shape_type, shape.to_h[:shape_type]
1343
+ [:wkt, :to_s].each do |method|
1344
+ error = assert_raise(ArgumentError) { shape.public_send(method) }
1345
+ assert_equal "unsupported WKT shape type #{number}", error.message
1346
+ end
1347
+ end
1348
+ end
1349
+
1350
+ def test_new_point_selects_constructor_with_optional_attributes
1351
+ [[[1, 2], Shapelib::Point, [1.0, 2.0, nil, nil]],
1352
+ [[1, 2, 3], Shapelib::PointM, [1.0, 2.0, 3.0, nil]],
1353
+ [[1, 2, 3, 4], Shapelib::PointZ, [1.0, 2.0, 3.0, 4.0]]].each do |args, klass, coordinates|
1354
+ [false, true].each do |attributes|
1355
+ point = Shapelib.new_point(*(attributes ? args + [{ 'name' => 'example' }] : args))
1356
+ assert_instance_of klass, point
1357
+ assert_equal coordinates, [:x, :y, :m, :z].map { |method| point.public_send(method) }
1358
+ attributes ? assert_equal('example', point['name']) : assert_nil(point['name'])
1359
+ end
1360
+ end
1361
+ end
1362
+
1363
+ def test_new_point_preserves_explicit_nil_arguments
1364
+ [[[1, 2, nil], Shapelib::PointM, [1.0, 2.0, nil, nil]],
1365
+ [[1, 2, 3, nil], Shapelib::PointM, [1.0, 2.0, 3.0, nil]],
1366
+ [[1, 2, 3, 4, nil], Shapelib::PointZ, [1.0, 2.0, 3.0, 4.0]]].each do |args, klass, coordinates|
1367
+ point = Shapelib.new_point(*args)
1368
+ assert_instance_of klass, point
1369
+ assert_equal coordinates, [:x, :y, :m, :z].map { |method| point.public_send(method) }
1370
+ end
1371
+ end
1372
+
1373
+ def test_new_point_rejects_invalid_arguments
1374
+ [[], [1], [1, 2, 3, 4, {}, 6]].each do |args|
1375
+ assert_raise(ArgumentError) { Shapelib.new_point(*args) }
1376
+ end
1377
+ [42, 'attributes', Object.new].each do |attributes|
1378
+ assert_raise(TypeError) { Shapelib.new_point(1, 2, 3, 4, attributes) }
1379
+ end
1380
+ end
1381
+
1382
+ def test_point_measures_preserve_negatives_and_recognize_no_data
1383
+ measure_cases.each do |value, expected|
1384
+ [Shapelib::PointM.new(-1, -2, value), Shapelib::PointZ.new(-1, -2, value, -3)].each do |point|
1385
+ assert_equal expected, point.m
1386
+ assert_equal [expected], point.mvals
1387
+ assert_equal [expected], point.to_h[:mvals]
1388
+ assert_equal [-1.0, -2.0, expected, point.z || 0.0], point.minbound
1389
+ assert_equal point.minbound, point.maxbound
1390
+ end
1391
+ end
1392
+ end
1393
+
1394
+ def test_geometry_measure_arrays_use_the_same_no_data_rules
1395
+ [Shapelib::ArcM, Shapelib::ArcZ, Shapelib::MultiPointM, Shapelib::MultiPointZ].each do |klass|
1396
+ measure_cases.each do |value, expected|
1397
+ shape = klass.new([-1, -2, value, -3])
1398
+ hash = shape.to_h.merge(mvals: [value], part_type: shape.part_type || [], part_start: shape.part_start || [])
1399
+ [shape, Shapelib::Shape.new(hash)].each do |geometry|
1400
+ assert_equal [expected], geometry.mvals
1401
+ assert_equal [expected], geometry.to_h[:mvals]
1402
+ assert_equal expected, geometry.minbound[2]
1403
+ assert_equal expected, geometry.maxbound[2]
1404
+ end
1405
+ end
1406
+ end
1407
+ end
1408
+
1409
+ def test_file_bounds_use_the_same_measure_rules_as_shapes
1410
+ [Shapelib::PointM, Shapelib::PointZ].each do |klass|
1411
+ measure_cases.each do |value, expected|
1412
+ Dir.mktmpdir('shapelib-test-') do |dir|
1413
+ filename = File.join(dir, 'measures')
1414
+ point = klass == Shapelib::PointM ? klass.new(-1, -2, value) : klass.new(-1, -2, value, -3)
1415
+ bounds = [-1.0, -2.0, expected, point.z || 0.0]
1416
+ Shapelib::ShapeFile.new(filename, klass) do |file|
1417
+ file.write(point)
1418
+ assert_equal bounds, file.minbound
1419
+ assert_equal bounds, file.maxbound
1420
+ assert_equal point.minbound, file.minbound
1421
+ assert_equal point.maxbound, file.maxbound
1422
+ end
1423
+ Shapelib::ShapeFile.open(filename) do |file|
1424
+ shape = file.read(0)
1425
+ assert_equal bounds, file.minbound
1426
+ assert_equal bounds, file.maxbound
1427
+ assert_equal shape.minbound, file.minbound
1428
+ assert_equal shape.maxbound, file.maxbound
1429
+ end
1430
+ end
1431
+ end
1432
+ end
1433
+ end
1434
+
1435
+ def test_measure_files_preserve_negatives_and_legacy_nulls
1436
+ [[Shapelib::PointM, 128], [Shapelib::PointZ, 136]].each do |klass, offset|
1437
+ [-1.0, -1e38, nil, -2e-38].each do |value|
1438
+ Dir.mktmpdir('shapelib-test-') do |dir|
1439
+ filename = File.join(dir, 'measures')
1440
+ point = klass == Shapelib::PointM ? klass.new(1, 2, value) : klass.new(1, 2, value, 3)
1441
+ Shapelib::ShapeFile.new(filename, klass) { |file| file.write(point) }
1442
+ stored = File.binread("#{filename}.shp", 8, offset).unpack('E').first
1443
+ assert_equal value.nil? ? -2e38 : value, stored
1444
+ expected = value == -2e-38 ? nil : value
1445
+ Shapelib::ShapeFile.open(filename) do |file|
1446
+ shape = file.read(0)
1447
+ assert_equal [expected], shape.mvals
1448
+ assert_equal [expected], shape.to_h[:mvals]
1449
+ assert_equal expected, shape.minbound[2]
1450
+ assert_equal expected, shape.maxbound[2]
1451
+ end
1452
+ end
1453
+ end
1454
+ end
1455
+ end
1456
+
1457
+ def test_nonempty_point_coordinates_and_wkt
1458
+ [Shapelib::Point.new(1, 2), Shapelib::PointM.new(1, 2, 3),
1459
+ Shapelib::PointZ.new(1, 2, 3, 4)].each_with_index do |shape, index|
1460
+ assert_equal 1.0, shape.x
1461
+ assert_equal 2.0, shape.y
1462
+ index == 0 ? assert_nil(shape.m) : assert_equal(3.0, shape.m)
1463
+ index == 2 ? assert_equal(4.0, shape.z) : assert_nil(shape.z)
1464
+ assert_equal 'POINT(1 2)', shape.wkt
1465
+ assert_equal shape.wkt, shape.to_s
1466
+ end
1467
+ end
1468
+
1469
+ def test_nonempty_single_line_wkt
1470
+ [Shapelib::Arc, Shapelib::ArcM, Shapelib::ArcZ].each do |klass|
1471
+ shape = klass.new([1, 2], [3, 4])
1472
+ assert_equal 'LINESTRING(1 2, 3 4)', shape.wkt
1473
+ assert_equal shape.wkt, shape.to_s
1474
+ end
1475
+ end
1476
+
1477
+ def test_geometry_constructors_reject_empty_inputs
1478
+ part = [[0, 0], [1, 1]]
1479
+ classes = [Shapelib::Arc, Shapelib::ArcM, Shapelib::ArcZ,
1480
+ Shapelib::Polygon, Shapelib::PolygonM, Shapelib::PolygonZ, Shapelib::MultiPatch]
1481
+ classes.each do |klass|
1482
+ [[], [[]], [[], part], [part, []]].each do |args|
1483
+ assert_raise(ArgumentError) { klass.new(*args) }
1484
+ end
1485
+ end
1486
+ end
1487
+
1488
+ def test_multipart_constructor_rejects_non_array_part
1489
+ error = assert_raise(ArgumentError) { Shapelib::Arc.new([[0, 0], [1, 1]], nil) }
1490
+ assert_equal 'arg 1 must be Array', error.message
1491
+ end
1492
+
1493
+ def test_part_starts_initialize_under_gc_stress
1494
+ script = <<~'RUBY'
1495
+ Process.setrlimit(Process::RLIMIT_CORE, 0, 0) if Process.const_defined?(:RLIMIT_CORE)
1496
+ require 'shapelib'
1497
+ shape = Shapelib::Arc.new([[1, 2], [3, 4]], [[5, 6], [7, 8]])
1498
+ previous_stress = GC.stress
1499
+ begin
1500
+ GC.stress = true
1501
+ results = 5.times.map { shape.part_start }
1502
+ ensure
1503
+ GC.stress = previous_stress
1504
+ end
1505
+ abort "unexpected part starts: #{results.inspect}" unless results.all? { |parts| parts == [0, 2] }
1506
+ RUBY
1507
+ load_paths = $LOAD_PATH.map { |path| "-I#{path}" }
1508
+ _stdout, stderr, status = Open3.capture3(RbConfig.ruby, *load_paths, '-e', script)
1509
+ assert_true status.success?, "subprocess failed: #{stderr}"
1510
+ end
1511
+
1512
+ def test_geometry_constructors_accept_single_and_multiple_parts
1513
+ part = [[0, 0], [1, 0], [1, 1], [0, 0]]
1514
+ [Shapelib::Arc, Shapelib::Polygon].each do |klass|
1515
+ single = klass.new(*part)
1516
+ assert_equal 1, single.n_parts
1517
+ assert_equal 4, single.n_vertices
1518
+ assert_equal [0.0, 1.0, 1.0, 0.0], single.xvals
1519
+ multiple = klass.new(part, part)
1520
+ assert_equal 2, multiple.n_parts
1521
+ assert_equal 8, multiple.n_vertices
1522
+ assert_equal single.xvals * 2, multiple.xvals
1523
+ end
1524
+ end
1525
+
1526
+ def test_geometry_constructors_accept_large_integer_coordinates
1527
+ classes = [Shapelib::Arc, Shapelib::ArcM, Shapelib::ArcZ,
1528
+ Shapelib::Polygon, Shapelib::PolygonM, Shapelib::PolygonZ, Shapelib::MultiPatch]
1529
+ classes.each do |klass|
1530
+ [2**64, -(2**64)].each do |value|
1531
+ points = [[value, 2], [3, 4], [5, 6], [value, 2]]
1532
+ shape = klass.new(*points)
1533
+ assert_equal 1, shape.n_parts
1534
+ assert_equal points.length, shape.n_vertices
1535
+ assert_equal [value.to_f, 3.0, 5.0, value.to_f], shape.xvals
1536
+ assert_equal [2.0, 4.0, 6.0, 2.0], shape.yvals
1537
+ assert_equal klass.new(*points.map { |point| point.map(&:to_f) }).to_h, shape.to_h
1538
+ end
1539
+ end
1540
+ end
1541
+
1542
+ def test_multipart_constructor_accepts_explicit_part_type
1543
+ shape = Shapelib::MultiPatch.new([:TriangleStrip, [0, 0], [1, 0], [0, 1]])
1544
+ assert_equal 1, shape.n_parts
1545
+ assert_equal 3, shape.n_vertices
1546
+ assert_equal [:TriangleStrip], shape.part_type
1547
+ assert_equal [0.0, 1.0, 0.0], shape.xvals
1548
+ end
1549
+
1550
+ def test_geometry_constructors_reject_unknown_part_types
1551
+ part = [[0, 0], [1, 0], [0, 1]]
1552
+ data = Shapelib::MultiPatch.new([:Ring, *part]).to_h
1553
+ [:Unknown, 'Unknown'].each do |type|
1554
+ assert_raise(ArgumentError) { Shapelib::MultiPatch.new(type, *part) }
1555
+ assert_raise(ArgumentError) { Shapelib::MultiPatch.new([type, *part]) }
1556
+ assert_raise(ArgumentError) { Shapelib::MultiPatch.new([:Ring, *part], [type, *part]) }
1557
+ assert_raise(ArgumentError) { Shapelib::Shape.new(data.merge(part_type: [type])) }
1558
+ end
1559
+ [-1, 6, 99].each do |type|
1560
+ error = assert_raise(ArgumentError) { Shapelib::Shape.new(data.merge(part_type: [type])) }
1561
+ assert_equal "invalid part_type #{type}", error.message
1562
+ end
1563
+ number = Object.new
1564
+ number.define_singleton_method(:to_int) { 6 }
1565
+ assert_raise(ArgumentError) { Shapelib::Shape.new(data.merge(part_type: [number])) }
1566
+ end
1567
+
1568
+ def test_supported_part_types_round_trip
1569
+ types = [:TriangleStrip, :TriangleFan, :OuterRing, :InnerRing, :FirstRing, :Ring]
1570
+ part = [[0, 0, 0, 0], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 0]]
1571
+ shapes = []
1572
+ types.each_with_index do |type, index|
1573
+ single = Shapelib::MultiPatch.new(type, *part)
1574
+ assert_equal [type], single.part_type
1575
+ [type, type.to_s].each do |label|
1576
+ shape = Shapelib::MultiPatch.new([label, *part])
1577
+ assert_equal single.to_h, shape.to_h
1578
+ shapes << shape
1579
+ end
1580
+ number = Object.new
1581
+ number.define_singleton_method(:to_int) { index }
1582
+ [type, type.to_s, index, number].each do |value|
1583
+ shape = Shapelib::Shape.new(single.to_h.merge(part_type: [value]))
1584
+ assert_equal single.to_h, shape.to_h
1585
+ shapes << shape
1586
+ end
1587
+ end
1588
+ Dir.mktmpdir('shapelib-test-') do |dir|
1589
+ filename = File.join(dir, 'patches')
1590
+ Shapelib::ShapeFile.new(filename, Shapelib::MultiPatch) do |file|
1591
+ shapes.each_with_index { |shape, index| assert_equal index, file.write(shape) }
1592
+ end
1593
+ Shapelib::ShapeFile.open(filename) do |file|
1594
+ assert_equal shapes.length, file.size
1595
+ shapes.each_with_index do |shape, index|
1596
+ assert_equal shape.to_h.merge(shape_id: index), file.read(index).to_h
1597
+ end
1598
+ end
1599
+ end
1600
+ end
1601
+
1602
+ def test_invalid_native_part_types_do_not_modify_files
1603
+ Dir.mktmpdir('shapelib-test-') do |dir|
1604
+ source = File.join(dir, 'source')
1605
+ fields = [['name', Shapelib::String, 10]]
1606
+ shape = Shapelib::MultiPatch.new([:Ring, [9, 9, 0, 0], [10, 9, 0, 0], [9, 10, 0, 0]])
1607
+ shape['name'] = 'after'
1608
+ Shapelib::ShapeFile.new(source, Shapelib::MultiPatch, fields) { |file| file.write(shape) }
1609
+ [-1, 6, 99].each do |type|
1610
+ # First record: 100-byte header, 8-byte record header, then the
1611
+ # MultiPatch type, bounds, counts, part starts, and part types.
1612
+ File.open("#{source}.shp", 'rb+') do |file|
1613
+ file.seek(108 + 36)
1614
+ parts = file.read(4).unpack('V').first
1615
+ file.seek(108 + 44 + 4 * parts)
1616
+ file.write([type].pack('l<'))
1617
+ end
1618
+ invalid = nil
1619
+ Shapelib::ShapeFile.open(source) { |file| invalid = file.read(0) }
1620
+ assert_equal [type], invalid.part_type
1621
+ [[0, -1], [1, -1], [1, 0]].each do |count, index|
1622
+ filename = File.join(dir, 'destination')
1623
+ before = Shapelib::MultiPatch.new([:Ring, [0, 0, 0, 0], [1, 0, 0, 0], [0, 1, 0, 0]])
1624
+ before['name'] = 'before'
1625
+ Shapelib::ShapeFile.new(filename, Shapelib::MultiPatch, fields) do |file|
1626
+ count.times { file.write(before) }
1627
+ end
1628
+ expected = expected_bounds = nil
1629
+ Shapelib::ShapeFile.open(filename, 'rb+') do |file|
1630
+ expected = count.times.map { |record| file.read(record).to_h }
1631
+ expected_bounds = [file.minbound, file.maxbound]
1632
+ error = assert_raise(ArgumentError) { file.write(invalid, index) }
1633
+ assert_equal "part_type[0] must be between 0 and 5 (got #{type})", error.message
1634
+ assert_equal count, file.size
1635
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
1636
+ assert_equal expected_bounds, [file.minbound, file.maxbound]
1637
+ end
1638
+ Shapelib::ShapeFile.open(filename) do |file|
1639
+ assert_equal count, file.size
1640
+ assert_equal expected, count.times.map { |record| file.read(record).to_h }
1641
+ assert_equal expected_bounds, [file.minbound, file.maxbound]
1642
+ end
1643
+ end
1644
+ end
1645
+ end
1646
+ end
1647
+
1648
+ def test_multipart_constructors_snapshot_later_parts_before_conversion
1649
+ classes = [Shapelib::Arc, Shapelib::ArcM, Shapelib::ArcZ,
1650
+ Shapelib::Polygon, Shapelib::PolygonM, Shapelib::PolygonZ, Shapelib::MultiPatch]
1651
+ classes.each do |klass|
1652
+ [:grow, :clear].each do |mutation|
1653
+ first = [[1, 2, 3, 4], [5, 6, 7, 8]]
1654
+ later = [:Ring, [9, 10, 11, 12]]
1655
+ expected = klass.new(first, later).to_h
1656
+ number = Object.new
1657
+ number.define_singleton_method(:to_f) do
1658
+ mutation == :grow ? later.concat([[13, 14, 15, 16]] * 16) : later.clear
1659
+ 2.0
1660
+ end
1661
+ first[0][1] = number
1662
+ assert_equal expected, klass.new(first, later).to_h
1663
+ assert_equal mutation == :grow ? 18 : 0, later.length
1664
+ end
1665
+ end
1666
+ end
1667
+
1668
+ def test_convenience_constructors_snapshot_later_coordinates_before_conversion
1669
+ [:single, :multipart, :multipoint].each do |form|
1670
+ [:replace, :clear].each do |mutation|
1671
+ first = [1, 2, 3, 4]
1672
+ later = [5, 6, 7, 8]
1673
+ build = case form
1674
+ when :single then -> { Shapelib::ArcZ.new(first, later) }
1675
+ when :multipart then -> { Shapelib::ArcZ.new([first], [later]) }
1676
+ when :multipoint then -> { Shapelib::MultiPointZ.new(first, later) }
1677
+ end
1678
+ expected = build.call.to_h
1679
+ number = Object.new
1680
+ number.define_singleton_method(:to_f) do
1681
+ mutation == :replace ? later.replace([50, 60, 70, 80]) : later.clear
1682
+ 2.0
1683
+ end
1684
+ first[1] = number
1685
+ assert_equal expected, build.call.to_h
1686
+ assert_equal mutation == :replace ? [50, 60, 70, 80] : [], later
1687
+ end
1688
+ end
1689
+ end
1690
+
1691
+ def test_convenience_constructors_snapshot_current_coordinates_before_conversion
1692
+ [:single, :multipart, :multipoint].each do |form|
1693
+ [:grow, :clear].each do |mutation|
1694
+ point = [1, 2, 3, 4]
1695
+ build = case form
1696
+ when :single then -> { Shapelib::ArcZ.new(point) }
1697
+ when :multipart then -> { Shapelib::ArcZ.new([point]) }
1698
+ when :multipoint then -> { Shapelib::MultiPointZ.new(point) }
1699
+ end
1700
+ expected = build.call.to_h
1701
+ number = Object.new
1702
+ number.define_singleton_method(:to_f) do
1703
+ mutation == :grow ? point.concat([9] * 16) : point.clear
1704
+ GC.start
1705
+ 2.0
1706
+ end
1707
+ point[1] = number
1708
+ assert_equal expected, build.call.to_h
1709
+ assert_equal mutation == :grow ? 20 : 0, point.length
1710
+ end
1711
+ end
1712
+ end
1713
+
1714
+ def test_geometry_coordinate_defaults_are_preserved
1715
+ shape = Shapelib::Arc.new([1, 2], [])
1716
+ assert_equal [1.0, 0.0], shape.xvals
1717
+ assert_equal [2.0, 0.0], shape.yvals
1718
+ end
1719
+
1720
+ def test_point_and_multipoint_hash_round_trips
1721
+ [Shapelib::Point, Shapelib::PointM, Shapelib::PointZ,
1722
+ Shapelib::MultiPoint, Shapelib::MultiPointM, Shapelib::MultiPointZ].each do |klass|
1723
+ shapes = if klass.name.include?('MultiPoint')
1724
+ [klass.new([1, 2, nil, 4], [5, 6, -1, 8]), klass.new]
1725
+ elsif klass == Shapelib::Point
1726
+ [klass.new(1, 2), klass.new(5, 6)]
1727
+ elsif klass == Shapelib::PointM
1728
+ [klass.new(1, 2, nil), klass.new(5, 6, -1)]
1729
+ else
1730
+ [klass.new(1, 2, nil, 4), klass.new(5, 6, -1, 8)]
1731
+ end
1732
+ shapes.each do |shape|
1733
+ shape['name'] = 'example'
1734
+ data = shape.to_h
1735
+ copy = Shapelib::Shape.new(data)
1736
+ assert_instance_of klass, copy
1737
+ assert_equal data, copy.to_h
1738
+ assert_nil data[:part_type]
1739
+ assert_nil data[:part_start]
1740
+ end
1741
+ Dir.mktmpdir('shapelib-test-') do |dir|
1742
+ filename = File.join(dir, 'points')
1743
+ Shapelib::ShapeFile.new(filename, klass, [['name', Shapelib::String, 10]]) do |file|
1744
+ file.write(shapes.first)
1745
+ end
1746
+ Shapelib::ShapeFile.open(filename) do |file|
1747
+ data = file.read(0).to_h
1748
+ assert_equal data, Shapelib::Shape.new(data).to_h
1749
+ end
1750
+ end
1751
+ end
1752
+ end
1753
+
1754
+ def test_multipoint_constructors_reject_part_labels_before_conversion
1755
+ [Shapelib::MultiPoint, Shapelib::MultiPointM, Shapelib::MultiPointZ].each do |klass|
1756
+ converted = false
1757
+ number = Object.new
1758
+ number.define_singleton_method(:to_f) { converted = true; 1.0 }
1759
+ [:Ring, 'Ring'].each do |label|
1760
+ [[label], [label, [number, 2]]].each do |args|
1761
+ converted = false
1762
+ error = assert_raise(ArgumentError) { klass.new(*args) }
1763
+ assert_equal 'MultiPoint does not accept part type labels', error.message
1764
+ assert_equal false, converted
1765
+ end
1766
+ end
1767
+ end
1768
+ end
1769
+
1770
+ def test_multipoint_nested_point_lists_and_existing_coordinate_forms
1771
+ point_class = Struct.new(:x, :y, :m, :z)
1772
+ [Shapelib::MultiPoint, Shapelib::MultiPointM, Shapelib::MultiPointZ].each do |klass|
1773
+ [[[1, 2, nil, 4], [5, 6, -1, 8]],
1774
+ [Shapelib::PointZ.new(1, 2, nil, 4), Shapelib::PointZ.new(5, 6, -1, 8)],
1775
+ [point_class.new(1, 2, nil, 4), point_class.new(5, 6, -1, 8)]].each do |points|
1776
+ assert_equal klass.new(*points).to_h, klass.new(points).to_h
1777
+ assert_equal klass.new(points.first).to_h, klass.new([points.first]).to_h
1778
+ end
1779
+ assert_equal 0, klass.new.n_vertices
1780
+ assert_equal klass.new([0, 0]).to_h, klass.new([]).to_h
1781
+ assert_equal [1.0], klass.new([1, 2]).xvals
1782
+ number = Object.new
1783
+ number.define_singleton_method(:to_f) { 1.0 }
1784
+ assert_equal klass.new([1, 2]).to_h, klass.new([number, 2]).to_h
1785
+ end
1786
+ end
1787
+
1788
+ def test_multipoint_snapshots_nested_lists_before_callbacks
1789
+ [:numeric_conversion, :point_detection].each do |callback|
1790
+ later = [5, 6, 7, 8]
1791
+ points = [[1, 2, 3, 4], later]
1792
+ expected = Shapelib::MultiPointZ.new(*points).to_h
1793
+ mutate = -> { points.clear; later.clear; GC.start }
1794
+ value = Object.new
1795
+ if callback == :numeric_conversion
1796
+ value.define_singleton_method(:to_f) { mutate.call; 2.0 }
1797
+ points.first[1] = value
1798
+ else
1799
+ coordinates = { x: 1.0, y: 2.0, m: 3.0, z: 4.0 }
1800
+ value.define_singleton_method(:respond_to_missing?) do |name, include_private = false|
1801
+ mutate.call if name == :x
1802
+ coordinates.key?(name)
1803
+ end
1804
+ value.define_singleton_method(:method_missing) { |name, *args| coordinates.fetch(name) }
1805
+ points[0] = value
1806
+ end
1807
+ assert_equal expected, Shapelib::MultiPointZ.new(points).to_h
1808
+ assert_empty points
1809
+ assert_empty later
1810
+ end
1811
+ end
1812
+
1813
+ def test_open_coerces_both_arguments_before_using_string_storage
1814
+ fixture = 'test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA'
1815
+ [:filename, :mode].each do |mutation|
1816
+ filename = fixture.dup
1817
+ mode = mutation == :filename ? 'rb' + ' ' * 10000 : 'rb'
1818
+ calls = Hash.new(0)
1819
+ name_arg = Object.new
1820
+ name_arg.define_singleton_method(:to_str) do
1821
+ calls[:filename] += 1
1822
+ mode.replace('rb') if mutation == :filename
1823
+ filename
1824
+ end
1825
+ mode_arg = Object.new
1826
+ mode_arg.define_singleton_method(:to_str) do
1827
+ calls[:mode] += 1
1828
+ filename.replace("#{fixture}.shp") if mutation == :mode
1829
+ mode
1830
+ end
1831
+ Shapelib::ShapeFile.open(name_arg, mode_arg) do |file|
1832
+ assert_equal :Polygon, file.shape_type
1833
+ assert_equal 0, file.read(0).shape_id
1834
+ end
1835
+ assert_equal({ filename: 1, mode: 1 }, calls)
1836
+ end
1837
+ end
1838
+
1839
+ def test_open_rejects_nul_introduced_by_argument_conversion
1840
+ fixture = 'test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA'
1841
+ [:filename, :mode].each do |mutation|
1842
+ filename = fixture.dup
1843
+ mode = 'rb'.dup
1844
+ name_arg = Object.new
1845
+ name_arg.define_singleton_method(:to_str) do
1846
+ mode.replace("rb\0ignored") if mutation == :filename
1847
+ filename
1848
+ end
1849
+ mode_arg = Object.new
1850
+ mode_arg.define_singleton_method(:to_str) do
1851
+ filename.replace("#{fixture}\0ignored") if mutation == :mode
1852
+ mode
1853
+ end
1854
+ yielded = false
1855
+ assert_raise(ArgumentError) do
1856
+ Shapelib::ShapeFile.open(name_arg, mode_arg) { yielded = true }
1857
+ end
1858
+ assert_false yielded
1859
+ end
1860
+ end
1861
+
1862
+ def test_open_preserves_argument_conversion_exceptions
1863
+ fixture = 'test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA'
1864
+ [:filename, :mode].each do |argument|
1865
+ error = RuntimeError.new('string conversion failed')
1866
+ value = Object.new
1867
+ value.define_singleton_method(:to_str) { raise error }
1868
+ args = argument == :filename ? [value, 'rb'] : [fixture, value]
1869
+ yielded = false
1870
+ raised = assert_raise(RuntimeError) do
1871
+ Shapelib::ShapeFile.open(*args) { yielded = true }
1872
+ end
1873
+ assert_same error, raised
1874
+ assert_false yielded
1875
+ end
1876
+ end
1877
+
1878
+ def test_open_filename_rejects_nul
1879
+ filename = "test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA\0ignored"
1880
+ assert_raise(ArgumentError) { Shapelib::ShapeFile.open(filename) {} }
1881
+ end
1882
+
1883
+ def test_new_filename_rejects_nul
1884
+ Dir.mktmpdir('shapelib-test-') do |dir|
1885
+ filename = "#{File.join(dir, 'points')}\0ignored"
1886
+ assert_raise(ArgumentError) { Shapelib::ShapeFile.new(filename, Shapelib::Point) {} }
1887
+ assert_empty Dir.children(dir)
1888
+ end
1889
+ end
1890
+
1891
+ def test_open_mode_rejects_nul
1892
+ filename = 'test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA'
1893
+ assert_raise(ArgumentError) { Shapelib::ShapeFile.open(filename, "rb\0ignored") {} }
1894
+ end
1895
+
1896
+ def test_field_creation_rejects_nul
1897
+ Dir.mktmpdir('shapelib-test-') do |dir|
1898
+ Shapelib::ShapeFile.new(File.join(dir, 'points'), Shapelib::Point) do |file|
1899
+ assert_raise(ArgumentError) { file.add_field("name\0ignored", Shapelib::String, 10) }
1900
+ assert_equal 0, file.field_count
1901
+ end
1902
+ end
1903
+ end
1904
+
1905
+ def test_field_lookup_rejects_nul
1906
+ Dir.mktmpdir('shapelib-test-') do |dir|
1907
+ Shapelib::ShapeFile.new(File.join(dir, 'points'), Shapelib::Point) do |file|
1908
+ file.add_field('name', Shapelib::String, 10)
1909
+ assert_equal 0, file.field_index('name')
1910
+ assert_raise(ArgumentError) { file.field_index("name\0ignored") }
1911
+ end
1912
+ end
1913
+ end
1914
+
1915
+ def test_field_metadata_rejects_invalid_lookups_without_reusing_values
1916
+ Dir.mktmpdir('shapelib-test-') do |dir|
1917
+ fields = [['name', Shapelib::String, 10], ['amount', Shapelib::Float, 12, 3]]
1918
+ Shapelib::ShapeFile.new(File.join(dir, 'points'), Shapelib::Point, fields) do |file|
1919
+ fields.each_with_index do |(name, type, width, decimals), index|
1920
+ assert_equal name, file.field_name(index)
1921
+ assert_equal width, file.field_width(index)
1922
+ assert_equal decimals || 0, file.field_decimals(index)
1923
+ [-1, fields.length, 999].each do |invalid|
1924
+ assert_nil file.field_name(invalid)
1925
+ assert_equal(-1, file.field_width(invalid))
1926
+ assert_equal(-1, file.field_decimals(invalid))
1927
+ end
1928
+ assert_equal(-1, file.field_width('missing'))
1929
+ assert_equal(-1, file.field_decimals('missing'))
1930
+ assert_equal name, file.field_name(index)
1931
+ assert_equal width, file.field_width(name)
1932
+ assert_equal decimals || 0, file.field_decimals(name)
1933
+ end
1934
+ end
1935
+ end
1936
+ end
1937
+
1938
+ def test_unavailable_dbf_metadata_keeps_failure_results
1939
+ Dir.mktmpdir('shapelib-test-') do |dir|
1940
+ filename = File.join(dir, 'points')
1941
+ file = Shapelib::ShapeFile.new(filename, Shapelib::Point, [['name', Shapelib::String, 10]])
1942
+ assert_equal 'name', file.field_name(0)
1943
+ file.close
1944
+ assert_nil file.field_name(0)
1945
+ assert_equal(-1, file.field_width(0))
1946
+ assert_equal(-1, file.field_decimals(0))
1947
+ File.unlink("#{filename}.dbf")
1948
+ Shapelib::ShapeFile.open(filename) do |geometry|
1949
+ assert_nil geometry.field_name(0)
1950
+ assert_equal(-1, geometry.field_width(0))
1951
+ assert_equal(-1, geometry.field_decimals(0))
1952
+ end
1953
+ ensure
1954
+ file.close if file
1955
+ end
1956
+ end
1957
+
1958
+ def test_point_attributes_snapshot_pairs_before_name_conversion
1959
+ [[Shapelib::Point, [1, 2]], [Shapelib::PointM, [1, 2, 3]],
1960
+ [Shapelib::PointZ, [1, 2, 3, 4]]].each do |klass, args|
1961
+ [:clear, :grow].each do |mutation|
1962
+ later = ['second', 2]
1963
+ pairs = []
1964
+ name = Object.new
1965
+ name.define_singleton_method(:to_str) do
1966
+ later.replace(['changed', 99])
1967
+ mutation == :clear ? pairs.clear : pairs.concat(Array.new(1000) { ['extra', 5] })
1968
+ GC.start
1969
+ 'first'
1970
+ end
1971
+ pairs.replace([[name, 1], later, ['third', 3], ['fourth', 4]])
1972
+ attributes = Class.new(Hash) { define_method(:to_a) { pairs } }.new
1973
+ point = klass.new(*args, attributes)
1974
+ assert_equal({ 'first' => 1, 'second' => 2, 'third' => 3, 'fourth' => 4 },
1975
+ point.to_h.select { |key, _| key.is_a?(String) })
1976
+ assert_equal ['changed', 99], later
1977
+ assert_equal mutation == :clear ? 0 : 1004, pairs.length
1978
+ end
1979
+ end
1980
+ end
1981
+
1982
+ def test_point_attributes_reject_malformed_pairs
1983
+ [[Shapelib::Point, [1, 2]], [Shapelib::PointM, [1, 2, 3]],
1984
+ [Shapelib::PointZ, [1, 2, 3, 4]]].each do |klass, args|
1985
+ [[], ['name'], ['name', 1, 2], nil, 'not a pair'].each do |pair|
1986
+ attributes = Class.new(Hash) { define_method(:to_a) { [pair] } }.new
1987
+ error_class = pair.is_a?(Array) ? ArgumentError : TypeError
1988
+ assert_raise(error_class) { klass.new(*args, attributes) }
1989
+ end
1990
+ attributes = Class.new(Hash) { def to_a; nil; end }.new
1991
+ assert_raise(TypeError) { klass.new(*args, attributes) }
1992
+ end
1993
+ end
1994
+
1995
+ def test_point_attributes_preserve_name_conversion_exceptions
1996
+ [[Shapelib::Point, [1, 2]], [Shapelib::PointM, [1, 2, 3]],
1997
+ [Shapelib::PointZ, [1, 2, 3, 4]]].each do |klass, args|
1998
+ error = RuntimeError.new('name conversion failed')
1999
+ name = Object.new
2000
+ name.define_singleton_method(:to_str) { GC.start; raise error }
2001
+ attributes = { name => 1 }
2002
+ assert_same error, assert_raise(RuntimeError) { klass.new(*args, attributes) }
2003
+ end
2004
+ end
2005
+
2006
+ def test_attribute_lookup_rejects_nul
2007
+ point = Shapelib::Point.new(1, 2)
2008
+ point['name'] = 42
2009
+ assert_raise(ArgumentError) { point["name\0ignored"] }
2010
+ end
2011
+
2012
+ def test_attribute_assignment_rejects_nul
2013
+ point = Shapelib::Point.new(1, 2)
2014
+ point['name'] = 42
2015
+ assert_raise(ArgumentError) { point["name\0ignored"] = 43 }
2016
+ assert_equal 42, point['name']
2017
+ end
2018
+
2019
+ def test_attribute_value_rejects_nul_without_changing_previous_value
2020
+ [42, 'before'].each do |value|
2021
+ point = Shapelib::Point.new(1, 2)
2022
+ point['name'] = value
2023
+ assert_raise(ArgumentError) { point['name'] = "after\0ignored" }
2024
+ assert_equal value, point['name']
2025
+ end
2026
+ end
2027
+
2028
+ def test_frozen_shapes_reject_attribute_assignment_without_mutation
2029
+ shapes = [Shapelib::Point.new(1, 2)]
2030
+ Shapelib::ShapeFile.open('test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA') do |file|
2031
+ shapes << file.read(0)
2032
+ end
2033
+ shapes.each do |shape|
2034
+ shape['name'] = 'before'
2035
+ before = shape.to_h
2036
+ shape.freeze
2037
+ [['name', 'after'], ['name', nil], ['extra', 42]].each do |name, value|
2038
+ assert_raise(FrozenError) { shape[name] = value }
2039
+ assert_equal before, shape.to_h
2040
+ end
2041
+ converted = false
2042
+ name = Object.new
2043
+ name.define_singleton_method(:to_str) { converted = true; 'name' }
2044
+ assert_raise(FrozenError) { shape[name] = 'after' }
2045
+ assert_false converted
2046
+ assert_equal before, shape.to_h
2047
+ end
2048
+ end
2049
+
2050
+ def test_attribute_name_conversion_cannot_modify_a_shape_it_freezes
2051
+ shapes = [Shapelib::Point.new(1, 2)]
2052
+ Shapelib::ShapeFile.open('test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA') do |file|
2053
+ shapes << file.read(0)
2054
+ end
2055
+ shapes.each do |shape|
2056
+ shape['name'] = 'before'
2057
+ before = shape.to_h
2058
+ name = Object.new
2059
+ name.define_singleton_method(:to_str) { shape.freeze; 'name' }
2060
+ assert_raise(FrozenError) { shape[name] = 'after' }
2061
+ assert_true shape.frozen?
2062
+ assert_equal before, shape.to_h
2063
+ end
2064
+ end
2065
+
2066
+ def test_attribute_replacement_between_supported_types
2067
+ values = [42, 1.5, 'before', nil]
2068
+ values.product(values).each do |before, after|
2069
+ point = Shapelib::Point.new(1, 2)
2070
+ point['name'] = before
2071
+ assert_same point, point.send(:[]=, 'name', after)
2072
+ assert_equal after, point['name']
2073
+ assert_equal !after.nil?, point.to_h.key?('name')
2074
+ end
2075
+ point = nil
2076
+ GC.start
2077
+ end
2078
+
2079
+ def test_cleared_attribute_slot_can_be_reused
2080
+ [42, 1.5, 'before', nil].each do |value|
2081
+ point = Shapelib::Point.new(1, 2)
2082
+ point['old'] = value
2083
+ point['old'] = nil
2084
+ point['new'] = 'after'
2085
+ assert_nil point['old']
2086
+ assert_equal 'after', point['new']
2087
+ assert_equal ['new'], point.to_h.keys.grep(String)
2088
+ end
2089
+ point = nil
2090
+ GC.start
2091
+ end
2092
+
2093
+ def test_invalid_attribute_replacement_preserves_previous_value
2094
+ [42, 1.5, 'before', nil].each do |value|
2095
+ point = Shapelib::Point.new(1, 2)
2096
+ point['name'] = value
2097
+ assert_raise(TypeError) { point['name'] = Object.new }
2098
+ assert_equal value, point['name']
2099
+ assert_raise(ArgumentError) { point['name'] = "after\0ignored" }
2100
+ assert_equal value, point['name']
2101
+ if 0.size > 4
2102
+ assert_raise(RangeError) { point['name'] = 2**31 }
2103
+ assert_equal value, point['name']
2104
+ end
2105
+ end
2106
+ point = nil
2107
+ GC.start
2108
+ end
2109
+
2110
+ def test_replacing_attributes_read_from_file
2111
+ fixture = 'test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA'
2112
+ Shapelib::ShapeFile.open(fixture) do |file|
2113
+ shape = file.read(0)
2114
+ names = shape.to_h.select { |key, value| key.is_a?(String) && value.is_a?(String) }.keys
2115
+ assert_not_empty names
2116
+ names.each do |name|
2117
+ shape[name] = 'replacement'
2118
+ assert_equal 'replacement', shape[name]
2119
+ shape[name] = nil
2120
+ assert_nil shape[name]
2121
+ assert_false shape.to_h.key?(name)
2122
+ end
2123
+ shape = nil
2124
+ GC.start
2125
+ end
2126
+ end
2127
+
2128
+ def test_attribute_name_boundaries
2129
+ point = Shapelib::Point.new(1, 2)
2130
+ ['a' * 10, 'b' * 11].each do |name|
2131
+ point[name] = 42
2132
+ assert_equal 42, point[name]
2133
+ assert_equal 42, point.to_h[name]
2134
+ end
2135
+ end
2136
+
2137
+ def test_long_attribute_names_are_truncated
2138
+ name = 'long_attribute_name' * 8
2139
+ prefix = name[0, 11]
2140
+ [0, 16].each do |count|
2141
+ point = Shapelib::Point.new(1, 2)
2142
+ count.times { |i| point["field#{i}"] = i + 1 }
2143
+ point[name] = 42
2144
+ assert_equal 42, point[name]
2145
+ assert_equal 42, point[prefix]
2146
+ assert_equal 42, point[prefix.upcase]
2147
+ assert_equal 42, point.to_h[prefix]
2148
+ assert_equal count + 1, point.to_h.keys.grep(String).size
2149
+ point["#{prefix}_other"] = 43
2150
+ assert_equal 43, point[name]
2151
+ assert_equal count + 1, point.to_h.keys.grep(String).size
2152
+ count.times { |i| assert_equal i + 1, point["field#{i}"] }
2153
+ end
2154
+ end
2155
+
2156
+ def test_point_z_coordinate
2157
+ point = Shapelib::PointZ.new(1, 2, 3, 4)
2158
+ assert_equal 3.0, point.m
2159
+ assert_equal 4.0, point.z
2160
+ end
2161
+
2162
+ def test_open_block_closes_file
2163
+ assert_block_closes_file do |block|
2164
+ Shapelib::ShapeFile.open('test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA', &block)
2165
+ end
2166
+ end
2167
+
2168
+ def test_new_block_closes_file
2169
+ Dir.mktmpdir('shapelib-test-') do |dir|
2170
+ assert_block_closes_file do |block|
2171
+ Shapelib::ShapeFile.new(File.join(dir, 'points'), Shapelib::Point, &block)
2172
+ end
2173
+ end
2174
+ end
2175
+
2176
+ def test_file_loading
2177
+ filename = "test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA"
2178
+ file = Shapelib::ShapeFile.open(filename)
2179
+
2180
+ assert_equal 948, file.size
2181
+ file.each do |shape|
2182
+ p shape
2183
+ end
2184
+ ensure
2185
+ file.close if file
2186
+ end
2187
+
2188
+ private
2189
+
2190
+ def measure_cases
2191
+ [[-1.0, -1.0], [0.0, 0.0], [1.0, 1.0], [-1e38, -1e38],
2192
+ [(-1e38).prev_float, nil], [-2e38, nil], [nil, nil],
2193
+ [-2e-38, nil], [-1.5e-38, -1.5e-38], [-3e-38, -3e-38]]
2194
+ end
2195
+
2196
+ def assert_file_operation_failure(operation, filename)
2197
+ yielded = false
2198
+ error = assert_raise(IOError) { yield proc { yielded = true } }
2199
+ assert_false yielded
2200
+ assert_match(/#{Regexp.escape(operation)}/, error.message)
2201
+ assert_match(/#{Regexp.escape(filename)}/, error.message)
2202
+ end
2203
+
2204
+ def empty_geometry(type)
2205
+ { shape_type: type, n_vertices: 0, n_parts: 0, part_type: [], part_start: [],
2206
+ xvals: [], yvals: [], zvals: [], mvals: [] }
2207
+ end
2208
+
2209
+ def hash_geometry
2210
+ Shapelib::ArcZ.new([[0, 1, 2, 3], [4, 5, 6, 7]],
2211
+ [[8, 9, 10, 11], [12, 13, 14, 15]]).to_h
2212
+ end
2213
+
2214
+ def assert_block_closes_file
2215
+ [nil, RuntimeError.new('block failure')].each do |error|
2216
+ file = nil
2217
+ block = proc do |opened|
2218
+ file = opened
2219
+ assert_operator file.field_count, :>=, 0
2220
+ raise error if error
2221
+ :ignored
2222
+ end
2223
+ if error
2224
+ assert_same error, assert_raise(RuntimeError) { yield block }
2225
+ else
2226
+ assert_nil yield(block)
2227
+ end
2228
+ assert_equal(-1, file.field_count)
2229
+ ensure
2230
+ file.close if file
2231
+ end
2232
+ end
2233
+ end