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.
- checksums.yaml +7 -0
- data/LICENSE +36 -0
- data/LICENSE-CC-BY-SA-3.0 +375 -0
- data/LICENSE-LGPL +481 -0
- data/LICENSE-MIT +21 -0
- data/README.md +75 -24
- data/Rakefile +142 -32
- data/doc/Interface.html +53 -32
- data/doc/Interface.rd +51 -30
- data/ext/shapelib_ext/extconf.rb +5 -1
- data/ext/shapelib_ext/main.c +37 -0
- data/ext/shapelib_ext/sfcode.h +67 -39
- data/ext/shapelib_ext/sflist.h +18 -18
- data/ext/shapelib_ext/shpplus.c +245 -57
- data/ext/shapelib_ext/shpplus.h +1 -0
- data/ext/shapelib_ext/spcode.h +245 -126
- data/ext/shapelib_ext/splist.h +23 -41
- data/ext/shapelib_ext/spwkt.h +29 -24
- data/ext/shapelib_ext/valconv.h +33 -21
- data/lib/shapelib/shape.rb +8 -1
- data/lib/shapelib/version.rb +4 -2
- data/lib/shapelib.rb +2 -1
- data/shapelib.gemspec +58 -0
- data/test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA.dbf +0 -0
- data/test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA.prj +1 -0
- data/test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA.shp +0 -0
- data/test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA.shp.xml +3 -0
- data/test/data/ZillowNeighborhoods-CA/ZillowNeighborhoods-CA.shx +0 -0
- data/test/test_zillow_data.rb +2233 -0
- metadata +70 -65
- data/test/libtest.rb +0 -80
- data/test/zsample1.rb +0 -168
- data/test/ztest1.rb +0 -253
|
@@ -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
|