grntest 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,18 @@
1
1
  # News
2
2
 
3
+ ## 1.0.3: 2013-08-12
4
+
5
+ This is a minor improvment release.
6
+
7
+ ### Improvements
8
+
9
+ * Supported XML output.
10
+ * Supported to show the actual result on leaked and not checked status.
11
+ * Supported warning message test.
12
+
3
13
  ## 1.0.2: 2012-12-11
4
14
 
5
- This is the release that add some directive.
15
+ This is the release that adds some directive.
6
16
 
7
17
  ### Improvements
8
18
 
@@ -0,0 +1,32 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module Grntest
19
+ class BaseResult
20
+ attr_accessor :elapsed_time
21
+ def initialize
22
+ @elapsed_time = 0
23
+ end
24
+
25
+ def measure
26
+ start_time = Time.now
27
+ yield
28
+ ensure
29
+ @elapsed_time = Time.now - start_time
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module Grntest
19
+ class Error < StandardError
20
+ end
21
+
22
+ class NotExist < Error
23
+ attr_reader :path
24
+ def initialize(path)
25
+ @path = path
26
+ super("<#{path}> doesn't exist.")
27
+ end
28
+ end
29
+
30
+ class ParseError < Error
31
+ attr_reader :type, :content, :reason
32
+ def initialize(type, content, reason)
33
+ @type = type
34
+ @content = content
35
+ @reason = reason
36
+ super("failed to parse <#{@type}> content: #{reason}: <#{content}>")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,89 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module Grntest
19
+ class ExecutionContext
20
+ attr_writer :logging
21
+ attr_accessor :base_directory, :temporary_directory_path, :db_path
22
+ attr_accessor :groonga_suggest_create_dataset
23
+ attr_accessor :result
24
+ attr_accessor :output_type
25
+ attr_accessor :on_error
26
+ attr_accessor :abort_tag
27
+ def initialize
28
+ @logging = true
29
+ @base_directory = Pathname(".")
30
+ @temporary_directory_path = Pathname("tmp")
31
+ @db_path = Pathname("db")
32
+ @groonga_suggest_create_dataset = "groonga-suggest-create-dataset"
33
+ @n_nested = 0
34
+ @result = []
35
+ @output_type = "json"
36
+ @log = nil
37
+ @on_error = :default
38
+ @abort_tag = nil
39
+ @omitted = false
40
+ end
41
+
42
+ def logging?
43
+ @logging
44
+ end
45
+
46
+ def execute
47
+ @n_nested += 1
48
+ yield
49
+ ensure
50
+ @n_nested -= 1
51
+ end
52
+
53
+ def top_level?
54
+ @n_nested == 1
55
+ end
56
+
57
+ def log_path
58
+ @temporary_directory_path + "groonga.log"
59
+ end
60
+
61
+ def log
62
+ @log ||= File.open(log_path.to_s, "a+")
63
+ end
64
+
65
+ def relative_db_path
66
+ @db_path.relative_path_from(@temporary_directory_path)
67
+ end
68
+
69
+ def omitted?
70
+ @omitted
71
+ end
72
+
73
+ def error
74
+ case @on_error
75
+ when :omit
76
+ omit
77
+ end
78
+ end
79
+
80
+ def omit
81
+ @omitted = true
82
+ abort
83
+ end
84
+
85
+ def abort
86
+ throw @abort_tag
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require "grntest/executors/standard-io-executor"
19
+ require "grntest/executors/http-executor"
@@ -0,0 +1,332 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require "pathname"
19
+ require "fileutils"
20
+ require "shellwords"
21
+
22
+ require "groonga/command"
23
+
24
+ require "grntest/error"
25
+ require "grntest/execution-context"
26
+ require "grntest/response-parser"
27
+
28
+ module Grntest
29
+ module Executors
30
+ class BaseExecutor
31
+ module ReturnCode
32
+ SUCCESS = 0
33
+ end
34
+
35
+ attr_reader :context
36
+ def initialize(context=nil)
37
+ @loading = false
38
+ @pending_command = ""
39
+ @pending_load_command = nil
40
+ @current_command_name = nil
41
+ @output_type = nil
42
+ @long_timeout = default_long_timeout
43
+ @context = context || ExecutionContext.new
44
+ end
45
+
46
+ def execute(script_path)
47
+ unless script_path.exist?
48
+ raise NotExist.new(script_path)
49
+ end
50
+
51
+ @context.execute do
52
+ script_path.open("r:ascii-8bit") do |script_file|
53
+ parser = create_parser
54
+ script_file.each_line do |line|
55
+ begin
56
+ parser << line
57
+ rescue Error, Groonga::Command::ParseError
58
+ line_info = "#{script_path}:#{script_file.lineno}:#{line.chomp}"
59
+ log_error("#{line_info}: #{$!.message}")
60
+ if $!.is_a?(Groonga::Command::ParseError)
61
+ @context.abort
62
+ else
63
+ log_error("#{line_info}: #{$!.message}")
64
+ raise unless @context.top_level?
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ @context.result
72
+ end
73
+
74
+ private
75
+ def create_parser
76
+ parser = Groonga::Command::Parser.new
77
+ parser.on_command do |command|
78
+ execute_command(command)
79
+ end
80
+ parser.on_load_complete do |command|
81
+ execute_command(command)
82
+ end
83
+ parser.on_comment do |comment|
84
+ if /\A@/ =~ comment
85
+ directive_content = $POSTMATCH
86
+ execute_directive("\##{comment}", directive_content)
87
+ end
88
+ end
89
+ parser
90
+ end
91
+
92
+ def resolve_path(path)
93
+ if path.relative?
94
+ @context.base_directory + path
95
+ else
96
+ path
97
+ end
98
+ end
99
+
100
+ def execute_directive_suggest_create_dataset(line, content, options)
101
+ dataset_name = options.first
102
+ if dataset_name.nil?
103
+ log_input(line)
104
+ log_error("#|e| [suggest-create-dataset] dataset name is missing")
105
+ return
106
+ end
107
+ execute_suggest_create_dataset(dataset_name)
108
+ end
109
+
110
+ def execute_directive_include(line, content, options)
111
+ path = options.first
112
+ if path.nil?
113
+ log_input(line)
114
+ log_error("#|e| [include] path is missing")
115
+ return
116
+ end
117
+ execute_script(Pathname(path))
118
+ end
119
+
120
+ def execute_directive_copy_path(line, content, options)
121
+ source, destination, = options
122
+ if source.nil? or destination.nil?
123
+ log_input(line)
124
+ if source.nil?
125
+ log_error("#|e| [copy-path] source is missing")
126
+ end
127
+ if destiantion.nil?
128
+ log_error("#|e| [copy-path] destination is missing")
129
+ end
130
+ return
131
+ end
132
+ source = resolve_path(Pathname(source))
133
+ destination = resolve_path(Pathname(destination))
134
+ FileUtils.cp_r(source.to_s, destination.to_s)
135
+ end
136
+
137
+ def execute_directive_long_timeout(line, content, options)
138
+ long_timeout, = options
139
+ invalid_value_p = false
140
+ case long_timeout
141
+ when "default"
142
+ @long_timeout = default_long_timeout
143
+ when nil
144
+ invalid_value_p = true
145
+ else
146
+ begin
147
+ @long_timeout = Float(long_timeout)
148
+ rescue ArgumentError
149
+ invalid_value_p = true
150
+ end
151
+ end
152
+
153
+ if invalid_value_p
154
+ log_input(line)
155
+ message = "long-timeout must be number or 'default': <#{long_timeout}>"
156
+ log_error("#|e| [long-timeout] #{message}")
157
+ end
158
+ end
159
+
160
+ def execute_directive_on_error(line, content, options)
161
+ action, = options
162
+ invalid_value_p = false
163
+ valid_actions = ["default", "omit"]
164
+ if valid_actions.include?(action)
165
+ @context.on_error = action.to_sym
166
+ else
167
+ invalid_value_p = true
168
+ end
169
+
170
+ if invalid_value_p
171
+ log_input(line)
172
+ valid_actions_label = "[#{valid_actions.join(', ')}]"
173
+ message = "on-error must be one of #{valid_actions_label}"
174
+ log_error("#|e| [on-error] #{message}: <#{action}>")
175
+ end
176
+ end
177
+
178
+ def execute_directive_omit(line, content, options)
179
+ reason, = options
180
+ @output_type = "raw"
181
+ log_output("omit: #{reason}")
182
+ @context.omit
183
+ end
184
+
185
+ def execute_directive(line, content)
186
+ command, *options = Shellwords.split(content)
187
+ case command
188
+ when "disable-logging"
189
+ @context.logging = false
190
+ when "enable-logging"
191
+ @context.logging = true
192
+ when "suggest-create-dataset"
193
+ execute_directive_suggest_create_dataset(line, content, options)
194
+ when "include"
195
+ execute_directive_include(line, content, options)
196
+ when "copy-path"
197
+ execute_directive_copy_path(line, content, options)
198
+ when "long-timeout"
199
+ execute_directive_long_timeout(line, content, options)
200
+ when "on-error"
201
+ execute_directive_on_error(line, content, options)
202
+ when "omit"
203
+ execute_directive_omit(line, content, options)
204
+ else
205
+ log_input(line)
206
+ log_error("#|e| unknown directive: <#{command}>")
207
+ end
208
+ end
209
+
210
+ def execute_suggest_create_dataset(dataset_name)
211
+ command_line = [@context.groonga_suggest_create_dataset,
212
+ @context.db_path.to_s,
213
+ dataset_name]
214
+ packed_command_line = command_line.join(" ")
215
+ log_input("#{packed_command_line}\n")
216
+ begin
217
+ IO.popen(command_line, "r:ascii-8bit") do |io|
218
+ log_output(io.read)
219
+ end
220
+ rescue SystemCallError
221
+ raise Error.new("failed to run groonga-suggest-create-dataset: " +
222
+ "<#{packed_command_line}>: #{$!}")
223
+ end
224
+ end
225
+
226
+ def execute_script(script_path)
227
+ executor = create_sub_executor(@context)
228
+ executor.execute(resolve_path(script_path))
229
+ end
230
+
231
+ def extract_command_info(command)
232
+ @current_command = command
233
+ if @current_command.name == "dump"
234
+ @output_type = "groonga-command"
235
+ else
236
+ @output_type = @current_command[:output_type] || @context.output_type
237
+ end
238
+ end
239
+
240
+ def execute_command(command)
241
+ extract_command_info(command)
242
+ log_input("#{command.original_source}\n")
243
+ response = send_command(command)
244
+ type = @output_type
245
+ log_output(response)
246
+ log_error(extract_important_messages(read_all_log))
247
+
248
+ @context.error if error_response?(response, type)
249
+ end
250
+
251
+ def read_all_log
252
+ read_all_readable_content(context.log, :first_timeout => 0)
253
+ end
254
+
255
+ def extract_important_messages(log)
256
+ important_messages = ""
257
+ log.each_line do |line|
258
+ timestamp, log_level, message = line.split(/\|\s*/, 3)
259
+ _ = timestamp # suppress warning
260
+ next unless important_log_level?(log_level)
261
+ next if backtrace_log_message?(message)
262
+ important_messages << "\#|#{log_level}| #{message}"
263
+ end
264
+ important_messages.chomp
265
+ end
266
+
267
+ def read_all_readable_content(output, options={})
268
+ content = ""
269
+ first_timeout = options[:first_timeout] || 1
270
+ timeout = first_timeout
271
+ while IO.select([output], [], [], timeout)
272
+ break if output.eof?
273
+ request_bytes = 1024
274
+ read_content = output.readpartial(request_bytes)
275
+ content << read_content
276
+ timeout = 0 if read_content.bytesize < request_bytes
277
+ end
278
+ content
279
+ end
280
+
281
+ def important_log_level?(log_level)
282
+ ["E", "A", "C", "e", "w"].include?(log_level)
283
+ end
284
+
285
+ def backtrace_log_message?(message)
286
+ message.start_with?("/")
287
+ end
288
+
289
+ def error_response?(response, type)
290
+ status = nil
291
+ begin
292
+ status, = ResponseParser.parse(response, type)
293
+ rescue ParseError
294
+ return false
295
+ end
296
+
297
+ return_code, = status
298
+ return_code != ReturnCode::SUCCESS
299
+ end
300
+
301
+ def log(tag, content, options={})
302
+ return unless @context.logging?
303
+ log_force(tag, content, options)
304
+ end
305
+
306
+ def log_force(tag, content, options)
307
+ return if content.empty?
308
+ @context.result << [tag, content, options]
309
+ end
310
+
311
+ def log_input(content)
312
+ log(:input, content)
313
+ end
314
+
315
+ def log_output(content)
316
+ log(:output, content,
317
+ :command => @current_command,
318
+ :type => @output_type)
319
+ @current_command = nil
320
+ @output_type = nil
321
+ end
322
+
323
+ def log_error(content)
324
+ log_force(:error, content, {})
325
+ end
326
+
327
+ def default_long_timeout
328
+ 180
329
+ end
330
+ end
331
+ end
332
+ end