grntest 1.0.2 → 1.0.3

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.
@@ -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