ucl 0.1.3.2 → 0.2.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 +4 -4
- data/LICENSE +21 -0
- data/LICENSE-DEPENDENCIES.md +161 -0
- data/README.md +270 -0
- data/ext/extconf.rb +67 -3
- data/ext/libucl/COPYING +23 -0
- data/ext/libucl/include/ucl.h +1689 -0
- data/ext/libucl/klib/khash.h +627 -0
- data/ext/libucl/klib/kvec.h +161 -0
- data/ext/libucl/src/mum.h +440 -0
- data/ext/libucl/src/tree.h +209 -0
- data/ext/libucl/src/ucl_chartable.h +267 -0
- data/ext/libucl/src/ucl_emitter.c +721 -0
- data/ext/libucl/src/ucl_emitter_streamline.c +178 -0
- data/ext/libucl/src/ucl_emitter_utils.c +521 -0
- data/ext/libucl/src/ucl_hash.c +642 -0
- data/ext/libucl/src/ucl_hash.h +115 -0
- data/ext/libucl/src/ucl_internal.h +668 -0
- data/ext/libucl/src/ucl_msgpack.c +1574 -0
- data/ext/libucl/src/ucl_parser.c +3247 -0
- data/ext/libucl/src/ucl_schema.c +1102 -0
- data/ext/libucl/src/ucl_sexp.c +229 -0
- data/ext/libucl/src/ucl_util.c +4004 -0
- data/ext/libucl/uthash/uthash.h +720 -0
- data/ext/libucl/uthash/utlist.h +1076 -0
- data/ext/libucl/uthash/utstring.h +412 -0
- data/ext/ucl.c +398 -90
- data/test/test_ucl.rb +452 -0
- data/ucl.gemspec +30 -7
- metadata +77 -14
data/test/test_ucl.rb
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
require 'minitest/autorun'
|
|
2
|
+
require 'tempfile'
|
|
3
|
+
|
|
4
|
+
# Load the compiled extension. By default it is looked up in ../ext (where
|
|
5
|
+
# `rake compile` builds it); UCL_EXT_DIR can point elsewhere.
|
|
6
|
+
libdir = ENV['UCL_EXT_DIR'] || File.expand_path('../ext', __dir__)
|
|
7
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
|
8
|
+
require 'ucl'
|
|
9
|
+
|
|
10
|
+
class TestUCL < Minitest::Test
|
|
11
|
+
def setup
|
|
12
|
+
# UCL.flags is global state; keep tests independent.
|
|
13
|
+
UCL.flags = 0
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# ---- scalar types -------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
def test_integer
|
|
19
|
+
assert_equal({ 'n' => 42 }, UCL.parse('n = 42'))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_negative_integer
|
|
23
|
+
assert_equal({ 'n' => -5 }, UCL.parse('n = -5'))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_large_integer
|
|
27
|
+
assert_equal({ 'n' => 9_999_999_999_999 }, UCL.parse('n = 9999999999999'))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_max_int64
|
|
31
|
+
assert_equal({ 'n' => 9_223_372_036_854_775_807 },
|
|
32
|
+
UCL.parse('n = 9223372036854775807'))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def test_hexadecimal_integer
|
|
36
|
+
assert_equal({ 'h' => 31 }, UCL.parse('h = 0x1f'))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_float
|
|
40
|
+
assert_equal({ 'f' => 1.5 }, UCL.parse('f = 1.5'))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_float_with_exponent
|
|
44
|
+
assert_equal({ 'f' => 1500.0 }, UCL.parse('f = 1.5e3'))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_string
|
|
48
|
+
assert_equal({ 's' => 'hello' }, UCL.parse('s = hello'))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_quoted_string
|
|
52
|
+
assert_equal({ 's' => 'hello world' }, UCL.parse('s = "hello world"'))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_string_keeps_utf8_bytes
|
|
56
|
+
# Strings are returned with ASCII-8BIT encoding, but the bytes are the
|
|
57
|
+
# verbatim (UTF-8) content of the configuration.
|
|
58
|
+
s = UCL.parse('s = "héllo"')['s']
|
|
59
|
+
assert_equal 'héllo'.b, s.b
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_booleans
|
|
63
|
+
assert_equal({ 't' => true, 'f' => false }, UCL.parse("t = true\nf = false"))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_boolean_word_variants
|
|
67
|
+
assert_equal({ 'a' => true, 'b' => true, 'c' => false, 'd' => false },
|
|
68
|
+
UCL.parse("a = yes\nb = on\nc = off\nd = no"))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def test_null
|
|
72
|
+
assert_equal({ 'z' => nil }, UCL.parse('z = null'))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# ---- size multipliers ---------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def test_si_multiplier
|
|
78
|
+
assert_equal({ 's' => 1000 }, UCL.parse('s = 1k'))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_binary_multipliers
|
|
82
|
+
assert_equal({ 'a' => 1024, 'b' => 1_048_576 },
|
|
83
|
+
UCL.parse("a = 1kb\nb = 1mb"))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# ---- containers ---------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def test_array
|
|
89
|
+
assert_equal({ 'a' => [1, 2, 3] }, UCL.parse('a = [1, 2, 3]'))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_mixed_type_array
|
|
93
|
+
assert_equal({ 'a' => [1, 'two', true, nil] },
|
|
94
|
+
UCL.parse('a = [1, "two", true, null]'))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_nested_arrays
|
|
98
|
+
assert_equal({ 'a' => [[1, 2], [3, 4]] }, UCL.parse('a = [[1,2],[3,4]]'))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_array_of_objects
|
|
102
|
+
assert_equal({ 'a' => [{ 'x' => 1 }, { 'y' => 2 }] },
|
|
103
|
+
UCL.parse('a = [ {x=1}, {y=2} ]'))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_nested_object
|
|
107
|
+
assert_equal({ 'o' => { 'x' => 1 } }, UCL.parse('o { x = 1 }'))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_deeply_nested
|
|
111
|
+
assert_equal({ 'a' => { 'b' => { 'c' => { 'd' => 1 } } } },
|
|
112
|
+
UCL.parse('a { b { c { d = 1 } } }'))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_dotted_key_is_kept_flat
|
|
116
|
+
assert_equal({ 'a.b.c' => 1 }, UCL.parse('a.b.c = 1'))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ---- JSON compatibility -------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def test_parses_json
|
|
122
|
+
assert_equal({ 'a' => 1, 'b' => [2, 3] }, UCL.parse('{"a": 1, "b": [2,3]}'))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ---- comments -----------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def test_comments_are_ignored
|
|
128
|
+
assert_equal({ 'x' => 1, 'y' => 2 },
|
|
129
|
+
UCL.parse("# header\nx = 1 # inline\ny = 2"))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Characterisation, not endorsement: in libucl a trailing comment suppresses
|
|
133
|
+
# suffix parsing, so a value carrying a unit or a multiplier comes back as a
|
|
134
|
+
# string. Plain numbers are unaffected (see above). The README documents this
|
|
135
|
+
# as a trap; if a future libucl fixes it, this test is the canary that says
|
|
136
|
+
# the documentation needs updating.
|
|
137
|
+
def test_trailing_comment_suppresses_suffix_parsing
|
|
138
|
+
assert_equal({ 't' => 30.0 }, UCL.parse('t = 30s'))
|
|
139
|
+
assert_equal({ 't' => '30s' }, UCL.parse('t = 30s # note'))
|
|
140
|
+
assert_equal({ 'n' => 10_485_760 }, UCL.parse('n = 10mb'))
|
|
141
|
+
assert_equal({ 'n' => '10mb' }, UCL.parse('n = 10mb # note'))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ---- duplicate keys become an explicit array ----------------------------
|
|
145
|
+
|
|
146
|
+
def test_duplicate_keys_make_array
|
|
147
|
+
assert_equal({ 'k' => [1, 2, 3] }, UCL.parse("k = 1\nk = 2\nk = 3"))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_single_key_stays_scalar
|
|
151
|
+
assert_equal({ 'k' => 1 }, UCL.parse('k = 1'))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# ---- empty objects (regression: used to raise) --------------------------
|
|
155
|
+
|
|
156
|
+
def test_empty_input_returns_empty_hash
|
|
157
|
+
assert_equal({}, UCL.parse(''))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_whitespace_only_returns_empty_hash
|
|
161
|
+
assert_equal({}, UCL.parse(" \n "))
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def test_comment_only_returns_empty_hash
|
|
165
|
+
assert_equal({}, UCL.parse("# nothing here\n"))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def test_empty_object
|
|
169
|
+
assert_equal({ 'e' => {} }, UCL.parse('e {}'))
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def test_nested_empty_object
|
|
173
|
+
assert_equal({ 'a' => { 'b' => {} } }, UCL.parse('a { b {} }'))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def test_empty_object_then_more_keys
|
|
177
|
+
assert_equal({ 'a' => {}, 'b' => 2 }, UCL.parse("a {}\nb = 2"))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def test_empty_array
|
|
181
|
+
assert_equal({ 'a' => [] }, UCL.parse('a = []'))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# ---- time ---------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
def test_time_is_parsed_to_float
|
|
187
|
+
assert_equal({ 't' => 10.0 }, UCL.parse('t = 10s'))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_no_time_keeps_string
|
|
191
|
+
assert_equal({ 't' => '10s' }, UCL.parse('t = 10s', UCL::NO_TIME))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# ---- key flags ----------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def test_key_symbol
|
|
197
|
+
assert_equal({ name: 'value' }, UCL.parse('name = value', UCL::KEY_SYMBOL))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_key_symbol_recurses_into_nested_objects
|
|
201
|
+
assert_equal({ o: { x: 1 } }, UCL.parse('o { x = 1 }', UCL::KEY_SYMBOL))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_key_lowercase
|
|
205
|
+
assert_equal({ 'foo' => 1 }, UCL.parse('FOO = 1', UCL::KEY_LOWERCASE))
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def test_combined_flags
|
|
209
|
+
assert_equal({ foo: 1 },
|
|
210
|
+
UCL.parse('FOO = 1', UCL::KEY_SYMBOL | UCL::KEY_LOWERCASE))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# ---- macros -------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def test_disable_macro_does_not_process_include
|
|
216
|
+
# DISABLE_MACRO stops macros from being executed, so the referenced file
|
|
217
|
+
# is never read. libucl versions differ in how they treat the macro line:
|
|
218
|
+
# older ones ignore it (parsing only the remaining keys), newer ones reject
|
|
219
|
+
# it as an invalid key. Both outcomes are acceptable here.
|
|
220
|
+
config = %(.include "/no/such/file"\nx = 1)
|
|
221
|
+
begin
|
|
222
|
+
assert_equal({ 'x' => 1 }, UCL.parse(config, UCL::DISABLE_MACRO))
|
|
223
|
+
rescue UCL::Error => e
|
|
224
|
+
refute_empty e.message
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_include_of_missing_file_raises_without_flag
|
|
229
|
+
assert_raises(UCL::Error) { UCL.parse(%(.include "/no/such"\nx = 1)) }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# ---- file variables -----------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def test_no_filevars_leaves_variable_unexpanded
|
|
235
|
+
assert_equal({ 'd' => '$CURDIR' },
|
|
236
|
+
UCL.parse('d = $CURDIR', UCL::NO_FILEVARS))
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_parse_expands_curdir_by_default
|
|
240
|
+
refute_equal '$CURDIR', UCL.parse('d = $CURDIR')['d']
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# ---- default flags ------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def test_flags_default_to_zero
|
|
246
|
+
assert_equal 0, UCL.flags
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def test_flags_are_used_as_default
|
|
250
|
+
UCL.flags = UCL::KEY_SYMBOL
|
|
251
|
+
assert_equal({ name: 'value' }, UCL.parse('name = value'))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def test_explicit_flags_override_default
|
|
255
|
+
UCL.flags = UCL::KEY_SYMBOL
|
|
256
|
+
assert_equal({ 'name' => 'value' }, UCL.parse('name = value', 0))
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# ---- result object ------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def test_result_is_mutable
|
|
262
|
+
result = UCL.parse('a = 1')
|
|
263
|
+
result['b'] = 2
|
|
264
|
+
assert_equal({ 'a' => 1, 'b' => 2 }, result)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# ---- load_file ----------------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def test_load_file
|
|
270
|
+
with_conf("list = [1, 2, 3]\nname = test\n") do |path|
|
|
271
|
+
assert_equal({ 'list' => [1, 2, 3], 'name' => 'test' },
|
|
272
|
+
UCL.load_file(path))
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def test_load_file_sets_filename_variable
|
|
277
|
+
with_conf('f = "$FILENAME"' + "\n") do |path|
|
|
278
|
+
assert_equal({ 'f' => File.realpath(path) }, UCL.load_file(path))
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def test_load_file_with_flags
|
|
283
|
+
with_conf("Name = value\n") do |path|
|
|
284
|
+
assert_equal({ name: 'value' },
|
|
285
|
+
UCL.load_file(path, UCL::KEY_SYMBOL | UCL::KEY_LOWERCASE))
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def test_load_missing_file_raises
|
|
290
|
+
assert_raises(UCL::Error) { UCL.load_file('/no/such/file.conf') }
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# ---- errors -------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
def test_malformed_raises_ucl_error
|
|
296
|
+
assert_raises(UCL::Error) { UCL.parse('a = [') }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def test_error_message_is_present
|
|
300
|
+
err = assert_raises(UCL::Error) { UCL.parse('a = [') }
|
|
301
|
+
refute_empty err.message
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def test_error_is_a_standard_error
|
|
305
|
+
assert_operator UCL::Error, :<, StandardError
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def test_repeated_malformed_parses_do_not_crash
|
|
309
|
+
100.times do
|
|
310
|
+
assert_raises(UCL::Error) { UCL.parse('} bad {') }
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def test_parse_rejects_non_string
|
|
315
|
+
assert_raises(TypeError) { UCL.parse(42) }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# ---- nesting limit ------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
# The conversion to Ruby objects and libucl's own tree handling both recurse
|
|
321
|
+
# once per nesting level, so the depth is capped instead of being left to
|
|
322
|
+
# exhaust the C stack. The threads below are what make these meaningful: a
|
|
323
|
+
# thread gets a 1 MiB machine stack, which is the small stack the limit has
|
|
324
|
+
# to protect.
|
|
325
|
+
|
|
326
|
+
def test_deeply_nested_array_raises_ucl_error
|
|
327
|
+
err = Thread.new { assert_raises(UCL::Error) { UCL.parse(deep_array) } }.value
|
|
328
|
+
assert_match(/nesting/, err.message)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def test_deeply_nested_object_raises_ucl_error
|
|
332
|
+
src = ('k {' * 20_000) + ('}' * 20_000)
|
|
333
|
+
Thread.new { assert_raises(UCL::Error) { UCL.parse(src) } }.join
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def test_deeply_nested_file_raises_ucl_error
|
|
337
|
+
with_conf(deep_array) do |path|
|
|
338
|
+
Thread.new { assert_raises(UCL::Error) { UCL.load_file(path) } }.join
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def test_nesting_just_below_the_limit_is_accepted
|
|
343
|
+
src = 'a = ' + ('[' * 999) + (']' * 999)
|
|
344
|
+
assert_kind_of Array, UCL.parse(src)['a']
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def test_parser_still_works_after_a_rejected_document
|
|
348
|
+
Thread.new { assert_raises(UCL::Error) { UCL.parse(deep_array) } }.join
|
|
349
|
+
assert_equal({ 'x' => 1 }, UCL.parse('x = 1'))
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def test_rejected_document_does_not_leak_the_parser
|
|
353
|
+
# Regression: the conversion used to escape with the parser and the whole
|
|
354
|
+
# parsed tree still allocated, losing ~2.7 MiB per rejected 40 KiB input.
|
|
355
|
+
before = process_rss_kib
|
|
356
|
+
skip 'cannot read RSS on this platform' if before.nil?
|
|
357
|
+
100.times do
|
|
358
|
+
UCL.parse(deep_array)
|
|
359
|
+
rescue UCL::Error
|
|
360
|
+
# expected; what is being measured is what the failure path leaves behind
|
|
361
|
+
end
|
|
362
|
+
GC.start
|
|
363
|
+
growth = process_rss_kib - before
|
|
364
|
+
# Pre-fix this grew by ~280 MiB; the margin is deliberately generous so
|
|
365
|
+
# that ordinary heap growth cannot make the test flaky.
|
|
366
|
+
assert_operator growth, :<, 20 * 1024,
|
|
367
|
+
"leaked #{growth} KiB over 100 rejected documents"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# ---- macro safety -------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
def test_parse_processes_the_include_macro
|
|
373
|
+
with_conf("secret = value\n") do |path|
|
|
374
|
+
assert_equal({ 'secret' => 'value' }, UCL.parse(%(.include "#{path}")))
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def test_safe_parse_does_not_read_the_included_file
|
|
379
|
+
with_conf("secret = value\n") do |path|
|
|
380
|
+
# libucl versions differ in how they treat a macro line when macros are
|
|
381
|
+
# disabled (ignored, or rejected as an invalid key); either is fine, as
|
|
382
|
+
# long as the file is not read.
|
|
383
|
+
begin
|
|
384
|
+
refute_includes UCL.safe_parse(%(.include "#{path}")), 'secret'
|
|
385
|
+
rescue UCL::Error => e
|
|
386
|
+
refute_empty e.message
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def test_safe_parse_parses_ordinary_input
|
|
392
|
+
assert_equal({ 'a' => 1, 'b' => [2, 3] }, UCL.safe_parse("a = 1\nb = [2,3]"))
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def test_safe_parse_honours_explicit_flags
|
|
396
|
+
assert_equal({ a: 1 },
|
|
397
|
+
UCL.safe_parse('A = 1', UCL::KEY_SYMBOL | UCL::KEY_LOWERCASE))
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def test_safe_parse_uses_the_default_flags
|
|
401
|
+
UCL.flags = UCL::KEY_SYMBOL
|
|
402
|
+
assert_equal({ a: 1 }, UCL.safe_parse('a = 1'))
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# ---- subclasses ---------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
def test_subclass_uses_its_own_default_flags
|
|
408
|
+
klass = Class.new(UCL)
|
|
409
|
+
klass.flags = UCL::KEY_SYMBOL
|
|
410
|
+
assert_equal({ 'name' => 'value' }, UCL.parse('name = value'))
|
|
411
|
+
assert_equal({ name: 'value' }, klass.parse('name = value'))
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_subclass_without_flags_falls_back_to_ucl
|
|
415
|
+
UCL.flags = UCL::KEY_SYMBOL
|
|
416
|
+
klass = Class.new(UCL)
|
|
417
|
+
assert_equal UCL::KEY_SYMBOL, klass.flags
|
|
418
|
+
assert_equal({ name: 'value' }, klass.parse('name = value'))
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# ---- constants ----------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
def test_constants_defined
|
|
424
|
+
%i[KEY_SYMBOL KEY_LOWERCASE NO_TIME DISABLE_MACRO NO_FILEVARS].each do |c|
|
|
425
|
+
assert UCL.const_defined?(c), "UCL::#{c} should be defined"
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
private
|
|
430
|
+
|
|
431
|
+
# 20 000 levels: deeper than the 1000-level limit, and deep enough that the
|
|
432
|
+
# C stack would be gone without it.
|
|
433
|
+
def deep_array
|
|
434
|
+
'a = ' + ('[' * 20_000) + (']' * 20_000)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Resident set size in KiB, or nil where it cannot be read.
|
|
438
|
+
def process_rss_kib
|
|
439
|
+
out = `ps -o rss= -p #{$$} 2>/dev/null`
|
|
440
|
+
$?.success? && !out.strip.empty? ? out.to_i : nil
|
|
441
|
+
rescue StandardError
|
|
442
|
+
nil
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def with_conf(content)
|
|
446
|
+
Tempfile.create(['ucl', '.conf']) do |f|
|
|
447
|
+
f.write(content)
|
|
448
|
+
f.flush
|
|
449
|
+
yield f.path
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
data/ucl.gemspec
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'ucl'
|
|
3
|
-
s.version = '0.
|
|
4
|
-
s.summary =
|
|
3
|
+
s.version = '0.2.0'
|
|
4
|
+
s.summary = 'Universal Configuration Language (UCL) parser'
|
|
5
5
|
s.description = <<~EOF
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
Parse configuration files written in the Universal Configuration
|
|
7
|
+
Language (UCL), a human-friendly JSON superset. Native bindings to
|
|
8
|
+
the libucl library; results are returned as plain Ruby objects.
|
|
9
|
+
EOF
|
|
10
10
|
|
|
11
11
|
s.homepage = 'https://github.com/sdalu/ruby-ucl'
|
|
12
12
|
s.license = 'MIT'
|
|
@@ -14,6 +14,29 @@ Gem::Specification.new do |s|
|
|
|
14
14
|
s.authors = [ "Stéphane D'Alu" ]
|
|
15
15
|
s.email = [ 'sdalu@sdalu.com' ]
|
|
16
16
|
|
|
17
|
+
# The floor that has actually been exercised: 3.1 (Debian bookworm) and
|
|
18
|
+
# 3.4. Nothing here is known to need a newer Ruby.
|
|
19
|
+
s.required_ruby_version = '>= 3.1'
|
|
20
|
+
|
|
17
21
|
s.extensions = [ 'ext/extconf.rb' ]
|
|
18
|
-
|
|
22
|
+
|
|
23
|
+
# ext/libucl is a git submodule holding the whole upstream tree, most of
|
|
24
|
+
# which (tests, utils, the Lua and Python bindings, docs) has no business
|
|
25
|
+
# in the gem -- hence an explicit list rather than an ext/**/* glob. It
|
|
26
|
+
# names what the embedded build compiles, plus the licence texts that
|
|
27
|
+
# redistributing those sources requires; see LICENSE-DEPENDENCIES.md.
|
|
28
|
+
# `rake build` refuses to run when the submodule is not populated, since
|
|
29
|
+
# these globs would silently come back empty.
|
|
30
|
+
s.files = %w[ ucl.gemspec README.md LICENSE LICENSE-DEPENDENCIES.md ] +
|
|
31
|
+
Dir['ext/*.{c,h,rb}'] +
|
|
32
|
+
Dir['test/**/*.rb'] +
|
|
33
|
+
%w[ ext/libucl/COPYING ] +
|
|
34
|
+
Dir['ext/libucl/include/ucl.h'] +
|
|
35
|
+
Dir['ext/libucl/src/*.{c,h}'] +
|
|
36
|
+
Dir['ext/libucl/uthash/*.h'] +
|
|
37
|
+
Dir['ext/libucl/klib/*.h']
|
|
38
|
+
|
|
39
|
+
s.add_development_dependency 'rake'
|
|
40
|
+
s.add_development_dependency 'minitest', '~> 5.0'
|
|
41
|
+
s.add_development_dependency 'yard'
|
|
19
42
|
end
|
metadata
CHANGED
|
@@ -1,19 +1,60 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ucl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stéphane D'Alu
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
12
|
-
dependencies:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: minitest
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: yard
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
description: |
|
|
55
|
+
Parse configuration files written in the Universal Configuration
|
|
56
|
+
Language (UCL), a human-friendly JSON superset. Native bindings to
|
|
57
|
+
the libucl library; results are returned as plain Ruby objects.
|
|
17
58
|
email:
|
|
18
59
|
- sdalu@sdalu.com
|
|
19
60
|
executables: []
|
|
@@ -21,14 +62,38 @@ extensions:
|
|
|
21
62
|
- ext/extconf.rb
|
|
22
63
|
extra_rdoc_files: []
|
|
23
64
|
files:
|
|
65
|
+
- LICENSE
|
|
66
|
+
- LICENSE-DEPENDENCIES.md
|
|
67
|
+
- README.md
|
|
24
68
|
- ext/extconf.rb
|
|
69
|
+
- ext/libucl/COPYING
|
|
70
|
+
- ext/libucl/include/ucl.h
|
|
71
|
+
- ext/libucl/klib/khash.h
|
|
72
|
+
- ext/libucl/klib/kvec.h
|
|
73
|
+
- ext/libucl/src/mum.h
|
|
74
|
+
- ext/libucl/src/tree.h
|
|
75
|
+
- ext/libucl/src/ucl_chartable.h
|
|
76
|
+
- ext/libucl/src/ucl_emitter.c
|
|
77
|
+
- ext/libucl/src/ucl_emitter_streamline.c
|
|
78
|
+
- ext/libucl/src/ucl_emitter_utils.c
|
|
79
|
+
- ext/libucl/src/ucl_hash.c
|
|
80
|
+
- ext/libucl/src/ucl_hash.h
|
|
81
|
+
- ext/libucl/src/ucl_internal.h
|
|
82
|
+
- ext/libucl/src/ucl_msgpack.c
|
|
83
|
+
- ext/libucl/src/ucl_parser.c
|
|
84
|
+
- ext/libucl/src/ucl_schema.c
|
|
85
|
+
- ext/libucl/src/ucl_sexp.c
|
|
86
|
+
- ext/libucl/src/ucl_util.c
|
|
87
|
+
- ext/libucl/uthash/uthash.h
|
|
88
|
+
- ext/libucl/uthash/utlist.h
|
|
89
|
+
- ext/libucl/uthash/utstring.h
|
|
25
90
|
- ext/ucl.c
|
|
91
|
+
- test/test_ucl.rb
|
|
26
92
|
- ucl.gemspec
|
|
27
93
|
homepage: https://github.com/sdalu/ruby-ucl
|
|
28
94
|
licenses:
|
|
29
95
|
- MIT
|
|
30
96
|
metadata: {}
|
|
31
|
-
post_install_message:
|
|
32
97
|
rdoc_options: []
|
|
33
98
|
require_paths:
|
|
34
99
|
- lib
|
|
@@ -36,16 +101,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
36
101
|
requirements:
|
|
37
102
|
- - ">="
|
|
38
103
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
104
|
+
version: '3.1'
|
|
40
105
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
106
|
requirements:
|
|
42
107
|
- - ">="
|
|
43
108
|
- !ruby/object:Gem::Version
|
|
44
109
|
version: '0'
|
|
45
110
|
requirements: []
|
|
46
|
-
rubygems_version:
|
|
47
|
-
signing_key:
|
|
111
|
+
rubygems_version: 4.0.16
|
|
48
112
|
specification_version: 4
|
|
49
|
-
summary: Universal
|
|
113
|
+
summary: Universal Configuration Language (UCL) parser
|
|
50
114
|
test_files: []
|
|
51
|
-
...
|