fluentd 0.12.0.pre.1 → 0.12.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +1 -0
  4. data/ChangeLog +21 -0
  5. data/README.md +10 -2
  6. data/Rakefile +4 -13
  7. data/example/v1_literal_example.conf +36 -0
  8. data/fluentd.gemspec +4 -1
  9. data/lib/fluent/buffer.rb +73 -46
  10. data/lib/fluent/command/fluentd.rb +7 -2
  11. data/lib/fluent/config/basic_parser.rb +5 -0
  12. data/lib/fluent/config/element.rb +2 -5
  13. data/lib/fluent/config/literal_parser.rb +26 -7
  14. data/lib/fluent/config/section.rb +2 -0
  15. data/lib/fluent/config/v1_parser.rb +9 -2
  16. data/lib/fluent/formatter.rb +2 -1
  17. data/lib/fluent/mixin.rb +22 -7
  18. data/lib/fluent/output.rb +17 -8
  19. data/lib/fluent/parser.rb +14 -3
  20. data/lib/fluent/plugin/buf_file.rb +30 -15
  21. data/lib/fluent/plugin/filter_grep.rb +69 -0
  22. data/lib/fluent/plugin/filter_record_transformer.rb +183 -0
  23. data/lib/fluent/plugin/in_exec.rb +6 -0
  24. data/lib/fluent/plugin/in_forward.rb +34 -4
  25. data/lib/fluent/plugin/in_http.rb +1 -1
  26. data/lib/fluent/plugin/out_exec.rb +1 -1
  27. data/lib/fluent/plugin/out_exec_filter.rb +8 -1
  28. data/lib/fluent/plugin/out_forward.rb +82 -4
  29. data/lib/fluent/supervisor.rb +1 -1
  30. data/lib/fluent/timezone.rb +131 -0
  31. data/lib/fluent/version.rb +1 -1
  32. data/test/config/assertions.rb +42 -0
  33. data/test/config/test_config_parser.rb +385 -0
  34. data/test/config/test_configurable.rb +530 -0
  35. data/test/config/test_configure_proxy.rb +99 -0
  36. data/test/config/test_dsl.rb +237 -0
  37. data/test/config/test_literal_parser.rb +293 -0
  38. data/test/config/test_section.rb +112 -0
  39. data/test/config/test_system_config.rb +49 -0
  40. data/test/helper.rb +25 -0
  41. data/test/plugin/test_buf_file.rb +604 -0
  42. data/test/plugin/test_buf_memory.rb +204 -0
  43. data/test/plugin/test_filter_grep.rb +124 -0
  44. data/test/plugin/test_filter_record_transformer.rb +251 -0
  45. data/test/plugin/test_in_exec.rb +1 -0
  46. data/test/plugin/test_in_forward.rb +205 -2
  47. data/test/plugin/test_in_gc_stat.rb +1 -0
  48. data/test/plugin/test_in_http.rb +58 -2
  49. data/test/plugin/test_in_object_space.rb +1 -0
  50. data/test/plugin/test_in_status.rb +1 -0
  51. data/test/plugin/test_in_stream.rb +1 -1
  52. data/test/plugin/test_in_syslog.rb +1 -1
  53. data/test/plugin/test_in_tail.rb +1 -0
  54. data/test/plugin/test_in_tcp.rb +1 -1
  55. data/test/plugin/test_in_udp.rb +1 -1
  56. data/test/plugin/test_out_copy.rb +1 -0
  57. data/test/plugin/test_out_exec.rb +1 -0
  58. data/test/plugin/test_out_exec_filter.rb +1 -0
  59. data/test/plugin/test_out_file.rb +36 -0
  60. data/test/plugin/test_out_forward.rb +279 -8
  61. data/test/plugin/test_out_roundrobin.rb +1 -0
  62. data/test/plugin/test_out_stdout.rb +1 -0
  63. data/test/plugin/test_out_stream.rb +1 -1
  64. data/test/test_buffer.rb +530 -0
  65. data/test/test_config.rb +1 -1
  66. data/test/test_configdsl.rb +1 -1
  67. data/test/test_formatter.rb +223 -0
  68. data/test/test_match.rb +1 -2
  69. data/test/test_mixin.rb +74 -2
  70. data/test/test_parser.rb +7 -1
  71. metadata +88 -35
  72. data/lib/fluent/plugin/buf_zfile.rb +0 -75
  73. data/spec/config/config_parser_spec.rb +0 -314
  74. data/spec/config/configurable_spec.rb +0 -524
  75. data/spec/config/configure_proxy_spec.rb +0 -96
  76. data/spec/config/dsl_spec.rb +0 -239
  77. data/spec/config/helper.rb +0 -49
  78. data/spec/config/literal_parser_spec.rb +0 -222
  79. data/spec/config/section_spec.rb +0 -97
  80. data/spec/config/system_config_spec.rb +0 -49
  81. data/spec/spec_helper.rb +0 -60
@@ -85,7 +85,7 @@ module Fluent
85
85
  :chuser => nil,
86
86
  :chgroup => nil,
87
87
  :suppress_interval => 0,
88
- :suppress_repeated_stacktrace => false,
88
+ :suppress_repeated_stacktrace => true,
89
89
  :without_source => false,
90
90
  :use_v1_config => true,
91
91
  }
@@ -0,0 +1,131 @@
1
+ #
2
+ # Fluentd
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'tzinfo'
18
+
19
+ module Fluent
20
+ class Timezone
21
+ # [+-]HH:MM, [+-]HHMM, [+-]HH
22
+ NUMERIC_PATTERN = %r{\A[+-]\d\d(:?\d\d)?\z}
23
+
24
+ # Region/Zone, Region/Zone/Zone
25
+ NAME_PATTERN = %r{\A[^/]+/[^/]+(/[^/]+)?\z}
26
+
27
+ # Validate the format of the specified timezone.
28
+ #
29
+ # Valid formats are as follows. Note that timezone abbreviations
30
+ # such as PST and JST are not supported intentionally.
31
+ #
32
+ # 1. [+-]HH:MM (e.g. "+09:00")
33
+ # 2. [+-]HHMM (e.g. "+0900")
34
+ # 3. [+-]HH (e.g. "+09")
35
+ # 4. Region/Zone (e.g. "Asia/Tokyo")
36
+ # 5. Region/Zone/Zone (e.g. "America/Argentina/Buenos_Aires")
37
+ #
38
+ # In the 4th and 5th cases, it is checked whether the specified
39
+ # timezone exists in the timezone database.
40
+ #
41
+ # When the given timezone is valid, true is returned. Otherwise,
42
+ # false is returned. When nil is given, false is returned.
43
+ def self.validate(timezone)
44
+ # If the specified timezone is nil.
45
+ if timezone.nil?
46
+ # Invalid.
47
+ return false
48
+ end
49
+
50
+ # [+-]HH:MM, [+-]HHMM, [+-]HH
51
+ if NUMERIC_PATTERN === timezone
52
+ # Valid. It can be parsed by Time.zone_offset method.
53
+ return true
54
+ end
55
+
56
+ # Region/Zone, Region/Zone/Zone
57
+ if NAME_PATTERN === timezone
58
+ begin
59
+ # Get a Timezone instance for the specified timezone.
60
+ TZInfo::Timezone.get(timezone)
61
+ rescue
62
+ # Invalid. The string does not exist in the timezone database.
63
+ return false
64
+ else
65
+ # Valid. The string was found in the timezone database.
66
+ return true
67
+ end
68
+ else
69
+ # Invalid. Timezone abbreviations are not supported.
70
+ return false
71
+ end
72
+ end
73
+
74
+ # Validate the format of the specified timezone.
75
+ #
76
+ # The implementation of this method calls validate(timezone) method
77
+ # to check whether the given timezone is valid. When invalid, this
78
+ # method raises a ConfigError.
79
+ def self.validate!(timezone)
80
+ unless validate(timezone)
81
+ raise ConfigError, "Unsupported timezone '#{timezone}'"
82
+ end
83
+ end
84
+
85
+ # Create a formatter for a timezone and optionally a format.
86
+ #
87
+ # An Proc object is returned. If the given timezone is invalid,
88
+ # nil is returned.
89
+ def self.formatter(timezone, format = nil)
90
+ if timezone.nil?
91
+ return nil
92
+ end
93
+
94
+ # [+-]HH:MM, [+-]HHMM, [+-]HH
95
+ if NUMERIC_PATTERN === timezone
96
+ offset = Time.zone_offset(timezone)
97
+
98
+ if format
99
+ return Proc.new {|time|
100
+ Time.at(time).localtime(offset).strftime(format)
101
+ }
102
+ else
103
+ return Proc.new {|time|
104
+ Time.at(time).localtime(offset).iso8601
105
+ }
106
+ end
107
+ end
108
+
109
+ # Region/Zone, Region/Zone/Zone
110
+ if NAME_PATTERN === timezone
111
+ begin
112
+ tz = TZInfo::Timezone.get(timezone)
113
+ rescue
114
+ return nil
115
+ end
116
+
117
+ if format
118
+ return Proc.new {|time|
119
+ Time.at(time).localtime(tz.period_for_utc(time).utc_total_offset).strftime(format)
120
+ }
121
+ else
122
+ return Proc.new {|time|
123
+ Time.at(time).localtime(tz.period_for_utc(time).utc_total_offset).iso8601
124
+ }
125
+ end
126
+ end
127
+
128
+ return nil
129
+ end
130
+ end
131
+ end
@@ -16,6 +16,6 @@
16
16
 
17
17
  module Fluent
18
18
 
19
- VERSION = '0.12.0.pre.1'
19
+ VERSION = '0.12.0.pre.2'
20
20
 
21
21
  end
@@ -0,0 +1,42 @@
1
+ require 'test/unit/assertions'
2
+
3
+ module Test::Unit::Assertions
4
+ def assert_text_parsed_as(expected, actual)
5
+ msg = parse_text(actual).inspect rescue 'failed'
6
+ msg = "expected that #{actual.inspect} would be a parsed as #{expected.inspect} but got #{msg}"
7
+ assert_block(msg) {
8
+ v = parse_text(actual)
9
+ if expected.is_a?(Float)
10
+ v.is_a?(Float) && (v == obj || (v.nan? && obj.nan?) || (v - obj).abs < 0.000001)
11
+ else
12
+ v == expected
13
+ end
14
+ }
15
+ end
16
+
17
+ def assert_text_parsed_as_json(expected, actual)
18
+ msg = parse_text(actual).inspect rescue 'failed'
19
+ msg = "expected that #{actual.inspect} would be a parsed as #{expected.inspect} but got #{msg}"
20
+ assert_block(msg) {
21
+ v = JSON.parse(parse_text(actual))
22
+ v == expected
23
+ }
24
+ end
25
+
26
+ def assert_parse_error(actual)
27
+ msg = begin
28
+ parse_text(actual).inspect
29
+ rescue => e
30
+ e.inspect
31
+ end
32
+ msg = "expected that #{actual.inspect} would cause a parse error but got #{msg}"
33
+ assert_block(msg) {
34
+ begin
35
+ parse_text(actual)
36
+ false
37
+ rescue Fluent::ConfigParseError
38
+ true
39
+ end
40
+ }
41
+ end
42
+ end
@@ -0,0 +1,385 @@
1
+ require 'helper'
2
+ require "json"
3
+ require "config/assertions"
4
+ require "fluent/config/error"
5
+ require "fluent/config/basic_parser"
6
+ require "fluent/config/literal_parser"
7
+ require "fluent/config/v1_parser"
8
+
9
+ module Fluent::Config
10
+ module V1TestHelper
11
+ def root(*elements)
12
+ if elements.first.is_a?(Fluent::Config::Element)
13
+ attrs = {}
14
+ else
15
+ attrs = elements.shift || {}
16
+ end
17
+ Fluent::Config::Element.new('ROOT', '', attrs, elements)
18
+ end
19
+
20
+ def e(name, arg='', attrs={}, elements=[])
21
+ Fluent::Config::Element.new(name, arg, attrs, elements)
22
+ end
23
+ end
24
+
25
+ class TestV1Parser < ::Test::Unit::TestCase
26
+ def read_config(path)
27
+ path = File.expand_path(path)
28
+ data = File.read(path)
29
+ Fluent::Config::V1Parser.parse(data, File.basename(path), File.dirname(path))
30
+ end
31
+
32
+ def parse_text(text)
33
+ basepath = File.expand_path(File.dirname(__FILE__) + '/../../')
34
+ Fluent::Config::V1Parser.parse(text, '(test)', basepath, nil)
35
+ end
36
+
37
+ include V1TestHelper
38
+ extend V1TestHelper
39
+
40
+ sub_test_case 'attribute parsing' do
41
+ test "parses attributes" do
42
+ assert_text_parsed_as(e('ROOT', '', {"k1"=>"v1", "k2"=>"v2"}), %[
43
+ k1 v1
44
+ k2 v2
45
+ ])
46
+ end
47
+
48
+ test "allows attribute without value" do
49
+ assert_text_parsed_as(e('ROOT', '', {"k1"=>"", "k2"=>"v2"}), %[
50
+ k1
51
+ k2 v2
52
+ ])
53
+ end
54
+
55
+ test "parses attribute key always string" do
56
+ assert_text_parsed_as(e('ROOT', '', {"1" => "1"}), "1 1")
57
+ end
58
+
59
+ data("_.%$!," => "_.%$!,",
60
+ "/=~-~@\`:?" => "/=~-~@\`:?",
61
+ "()*{}.[]" => "()*{}.[]")
62
+ test "parses a value with symbols" do |v|
63
+ assert_text_parsed_as(e('ROOT', '', {"k" => v}), "k #{v}")
64
+ end
65
+
66
+ test "ignores spacing around value" do
67
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "a"}), " k1 a ")
68
+ end
69
+
70
+ test "allows spaces in value" do
71
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "a b c"}), "k1 a b c")
72
+ end
73
+
74
+ sub_test_case 'non-quoted string' do
75
+ test "remains text starting with '#'" do
76
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "#not_comment"}), " k1 #not_comment")
77
+ end
78
+
79
+ test "remains text just after '#'" do
80
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "a#not_comment"}), " k1 a#not_comment")
81
+ end
82
+
83
+ test "remove text after ` #` (comment)" do
84
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "a"}), " k1 a #comment")
85
+ end
86
+
87
+ test "does not require escaping backslash" do
88
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "\\\\"}), " k1 \\\\")
89
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "\\"}), " k1 \\")
90
+ end
91
+
92
+ test "remains backslash in front of a normal character" do
93
+ assert_text_parsed_as(e('ROOT', '', {"k1" => '\['}), " k1 \\[")
94
+ end
95
+
96
+ test "does not accept escape characters" do
97
+ assert_text_parsed_as(e('ROOT', '', {"k1" => '\n'}), " k1 \\n")
98
+ end
99
+ end
100
+
101
+ sub_test_case 'double quoted string' do
102
+ test "allows # in value" do
103
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "a#comment"}), ' k1 "a#comment"')
104
+ end
105
+
106
+ test "rejects characters after double quoted string" do
107
+ assert_parse_error(' k1 "a" 1')
108
+ end
109
+
110
+ test "requires escaping backslash" do
111
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "\\"}), ' k1 "\\\\"')
112
+ assert_parse_error(' k1 "\\"')
113
+ end
114
+
115
+ test "requires escaping double quote" do
116
+ assert_text_parsed_as(e('ROOT', '', {"k1" => '"'}), ' k1 "\\""')
117
+ assert_parse_error(' k1 """')
118
+ end
119
+
120
+ test "removes backslash in front of a normal character" do
121
+ assert_text_parsed_as(e('ROOT', '', {"k1" => '['}), ' k1 "\\["')
122
+ end
123
+
124
+ test "accepts escape characters" do
125
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "\n"}), ' k1 "\\n"')
126
+ end
127
+ end
128
+
129
+ sub_test_case 'single quoted string' do
130
+ test "allows # in value" do
131
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "a#comment"}), " k1 'a#comment'")
132
+ end
133
+
134
+ test "rejects characters after single quoted string" do
135
+ assert_parse_error(" k1 'a' 1")
136
+ end
137
+
138
+ test "requires escaping backslash" do
139
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "\\"}), " k1 '\\\\'")
140
+ assert_parse_error(" k1 '\\'")
141
+ end
142
+
143
+ test "requires escaping single quote" do
144
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "'"}), " k1 '\\''")
145
+ assert_parse_error(" k1 '''")
146
+ end
147
+
148
+ test "remains backslash in front of a normal character" do
149
+ assert_text_parsed_as(e('ROOT', '', {"k1" => '\\['}), " k1 '\\['")
150
+ end
151
+
152
+ test "does not accept escape characters" do
153
+ assert_text_parsed_as(e('ROOT', '', {"k1" => "\\n"}), " k1 '\\n'")
154
+ end
155
+ end
156
+
157
+ data(
158
+ "in match" => %[
159
+ <match>
160
+ @k v
161
+ </match>
162
+ ],
163
+ "in source" => %[
164
+ <source>
165
+ @k v
166
+ </source>
167
+ ],
168
+ "in filter" => %[
169
+ <filter>
170
+ @k v
171
+ </filter>
172
+ ],
173
+ "in top-level" => ' @k v '
174
+ )
175
+ def test_rejects_at_prefix_in_the_parameter_name(data)
176
+ assert_parse_error(data)
177
+ end
178
+
179
+ data(
180
+ "in nested" => %[
181
+ <match>
182
+ <record>
183
+ @k v
184
+ </record>
185
+ </match>
186
+ ]
187
+ )
188
+ def test_not_reject_at_prefix_in_the_parameter_name(data)
189
+ assert_nothing_raised { parse_text(data) }
190
+ end
191
+ end
192
+
193
+ sub_test_case 'element parsing' do
194
+ data(
195
+ 'root' => [root, ""],
196
+ "accepts empty element" => [root(e("test")), %[
197
+ <test>
198
+ </test>
199
+ ]],
200
+ "accepts argument and attributes" => [root(e("test", 'var', {'key'=>"val"})), %[
201
+ <test var>
202
+ key val
203
+ </test>
204
+ ]],
205
+ "accepts nested elements" => [root(
206
+ e("test", 'var', {'key'=>'1'}, [
207
+ e('nested1'),
208
+ e('nested2')
209
+ ])), %[
210
+ <test var>
211
+ key 1
212
+ <nested1>
213
+ </nested1>
214
+ <nested2>
215
+ </nested2>
216
+ </test>
217
+ ]],
218
+ "accepts multiline json values" => [root(e("test", 'var', {'key'=>"[\"a\",\"b\",\"c\",\"d\"]"})), %[
219
+ <test var>
220
+ key ["a",
221
+ "b", "c",
222
+ "d"]
223
+ </test>
224
+ ]],
225
+ "parses empty element argument to nil" => [root(e("test", '')), %[
226
+ <test >
227
+ </test>
228
+ ]],
229
+ "ignores spacing around element argument" => [root(e("test", "a")), %[
230
+ <test a >
231
+ </test>
232
+ ]])
233
+ def test_parse_element(data)
234
+ expected, target = data
235
+ assert_text_parsed_as(expected, target)
236
+ end
237
+
238
+ [
239
+ "**",
240
+ "*.*",
241
+ "1",
242
+ "_.%$!",
243
+ "/",
244
+ "()*{}.[]",
245
+ ].each do |arg|
246
+ test "parses symbol element argument:#{arg}" do
247
+ assert_text_parsed_as(root(e("test", arg)), %[
248
+ <test #{arg}>
249
+ </test>
250
+ ])
251
+ end
252
+ end
253
+
254
+ data(
255
+ "considers comments in element argument" => %[
256
+ <test #a>
257
+ </test>
258
+ ],
259
+ "requires line_end after begin tag" => %[
260
+ <test></test>
261
+ ],
262
+ "requires line_end after end tag" => %[
263
+ <test>
264
+ </test><test>
265
+ </test>
266
+ ])
267
+ def test_parse_error(data)
268
+ assert_parse_error(data)
269
+ end
270
+ end
271
+
272
+ # port from test_config.rb
273
+ sub_test_case '@include parsing' do
274
+ TMP_DIR = File.dirname(__FILE__) + "/tmp/v1_config#{ENV['TEST_ENV_NUMBER']}"
275
+
276
+ def write_config(path, data)
277
+ FileUtils.mkdir_p(File.dirname(path))
278
+ File.open(path, "w") { |f| f.write data }
279
+ end
280
+
281
+ def prepare_config
282
+ write_config "#{TMP_DIR}/config_test_1.conf", %[
283
+ k1 root_config
284
+ include dir/config_test_2.conf #
285
+ @include #{TMP_DIR}/config_test_4.conf
286
+ include file://#{TMP_DIR}/config_test_5.conf
287
+ @include config.d/*.conf
288
+ ]
289
+ write_config "#{TMP_DIR}/dir/config_test_2.conf", %[
290
+ k2 relative_path_include
291
+ @include ../config_test_3.conf
292
+ ]
293
+ write_config "#{TMP_DIR}/config_test_3.conf", %[
294
+ k3 relative_include_in_included_file
295
+ ]
296
+ write_config "#{TMP_DIR}/config_test_4.conf", %[
297
+ k4 absolute_path_include
298
+ ]
299
+ write_config "#{TMP_DIR}/config_test_5.conf", %[
300
+ k5 uri_include
301
+ ]
302
+ write_config "#{TMP_DIR}/config.d/config_test_6.conf", %[
303
+ k6 wildcard_include_1
304
+ <elem1 name>
305
+ include normal_parameter
306
+ </elem1>
307
+ ]
308
+ write_config "#{TMP_DIR}/config.d/config_test_7.conf", %[
309
+ k7 wildcard_include_2
310
+ ]
311
+ write_config "#{TMP_DIR}/config.d/config_test_8.conf", %[
312
+ <elem2 name>
313
+ @include ../dir/config_test_9.conf
314
+ </elem2>
315
+ ]
316
+ write_config "#{TMP_DIR}/dir/config_test_9.conf", %[
317
+ k9 embeded
318
+ <elem3 name>
319
+ nested nested_value
320
+ include hoge
321
+ </elem3>
322
+ ]
323
+ write_config "#{TMP_DIR}/config.d/00_config_test_8.conf", %[
324
+ k8 wildcard_include_3
325
+ <elem4 name>
326
+ include normal_parameter
327
+ </elem4>
328
+ ]
329
+ end
330
+
331
+ test 'parses @include / include correctly' do
332
+ prepare_config
333
+ c = read_config("#{TMP_DIR}/config_test_1.conf")
334
+ assert_equal('root_config', c['k1'])
335
+ assert_equal('relative_path_include', c['k2'])
336
+ assert_equal('relative_include_in_included_file', c['k3'])
337
+ assert_equal('absolute_path_include', c['k4'])
338
+ assert_equal('uri_include', c['k5'])
339
+ assert_equal('wildcard_include_1', c['k6'])
340
+ assert_equal('wildcard_include_2', c['k7'])
341
+ assert_equal('wildcard_include_3', c['k8'])
342
+ assert_equal([
343
+ 'k1',
344
+ 'k2',
345
+ 'k3',
346
+ 'k4',
347
+ 'k5',
348
+ 'k8', # Because of the file name this comes first.
349
+ 'k6',
350
+ 'k7',
351
+ ], c.keys)
352
+
353
+ elem1 = c.elements.find { |e| e.name == 'elem1' }
354
+ assert(elem1)
355
+ assert_equal('name', elem1.arg)
356
+ assert_equal('normal_parameter', elem1['include'])
357
+
358
+ elem2 = c.elements.find { |e| e.name == 'elem2' }
359
+ assert(elem2)
360
+ assert_equal('name', elem2.arg)
361
+ assert_equal('embeded', elem2['k9'])
362
+ assert_not_include(elem2, 'include')
363
+
364
+ elem3 = elem2.elements.find { |e| e.name == 'elem3' }
365
+ assert(elem3)
366
+ assert_equal('nested_value', elem3['nested'])
367
+ assert_equal('hoge', elem3['include'])
368
+ end
369
+
370
+ # TODO: Add uri based include spec
371
+ end
372
+
373
+ sub_test_case '#to_s' do
374
+ test 'parses dumpped configuration' do
375
+ original = %q!a\\\n\r\f\b'"z!
376
+ expected = %q!a\\\n\r\f\b'"z!
377
+
378
+ conf = parse_text(%[k1 #{original}])
379
+ assert_equal(expected, conf['k1']) # escape check
380
+ conf2 = parse_text(conf.to_s) # use dumpped configuration to check unescape
381
+ assert_equal(expected, conf2.elements.first['k1'])
382
+ end
383
+ end
384
+ end
385
+ end