groonga-query-log 1.2.8 → 1.2.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/bin/groonga-query-log-analyze +3 -4
  3. data/bin/groonga-query-log-analyze-load +22 -0
  4. data/bin/groonga-query-log-check-command-version-compatibility +2 -2
  5. data/bin/groonga-query-log-check-crash +22 -0
  6. data/bin/groonga-query-log-detect-memory-leak +2 -2
  7. data/bin/groonga-query-log-extract +2 -2
  8. data/bin/groonga-query-log-format-regression-test-logs +2 -2
  9. data/bin/groonga-query-log-replay +3 -5
  10. data/bin/groonga-query-log-run-regression-test +2 -2
  11. data/bin/groonga-query-log-show-running-queries +2 -2
  12. data/bin/groonga-query-log-verify-server +2 -2
  13. data/doc/text/news.md +31 -0
  14. data/groonga-query-log.gemspec +5 -4
  15. data/lib/groonga-query-log.rb +42 -0
  16. data/lib/{groonga/query-log/command-line-utils.rb → groonga-query-log/command-line.rb} +25 -13
  17. data/lib/{groonga/query-log → groonga-query-log}/command-version-compatibility-checker.rb +3 -5
  18. data/lib/groonga-query-log/command/analyze-load.rb +188 -0
  19. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer.rb +60 -44
  20. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/reporter.rb +15 -20
  21. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/reporter/console.rb +19 -18
  22. data/lib/groonga-query-log/command/analyzer/reporter/csv.rb +77 -0
  23. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/reporter/html.rb +32 -16
  24. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/reporter/json-stream.rb +4 -6
  25. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/reporter/json.rb +7 -7
  26. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/sized-grouped-operations.rb +3 -5
  27. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/sized-statistics.rb +4 -6
  28. data/lib/{groonga/query-log → groonga-query-log/command}/analyzer/streamer.rb +4 -6
  29. data/lib/groonga-query-log/command/check-command-version-compatibility.rb +69 -0
  30. data/lib/groonga-query-log/command/check-crash.rb +169 -0
  31. data/lib/groonga-query-log/command/detect-memory-leak.rb +89 -0
  32. data/lib/groonga-query-log/command/extract.rb +171 -0
  33. data/lib/groonga-query-log/command/format-regression-test-logs.rb +143 -0
  34. data/lib/groonga-query-log/command/replay.rb +117 -0
  35. data/lib/groonga-query-log/command/run-regression-test.rb +432 -0
  36. data/lib/groonga-query-log/command/show-running-queries.rb +78 -0
  37. data/lib/{groonga/query-log/command/replay.rb → groonga-query-log/command/verify-server.rb} +68 -37
  38. data/lib/{groonga/query-log → groonga-query-log}/incompatibility-detector.rb +3 -5
  39. data/lib/{groonga/query-log → groonga-query-log}/memory-leak-detector.rb +3 -7
  40. data/lib/groonga-query-log/parser.rb +173 -0
  41. data/lib/{groonga/query-log → groonga-query-log}/replayer.rb +7 -8
  42. data/lib/{groonga/query-log → groonga-query-log}/response-comparer.rb +3 -5
  43. data/lib/{groonga/query-log → groonga-query-log}/server-verifier.rb +3 -5
  44. data/lib/groonga-query-log/statistic.rb +192 -0
  45. data/lib/{groonga/query-log → groonga-query-log}/version.rb +2 -4
  46. data/lib/groonga/query-log.rb +21 -9
  47. data/lib/groonga/query-log/command/analyzer.rb +18 -0
  48. data/lib/groonga/query-log/command/check-command-version-compatibility.rb +2 -55
  49. data/lib/groonga/query-log/command/detect-memory-leak.rb +3 -78
  50. data/lib/groonga/query-log/command/extract.rb +5 -179
  51. data/lib/groonga/query-log/command/format-regression-test-logs.rb +3 -130
  52. data/lib/groonga/query-log/command/reply.rb +18 -0
  53. data/lib/groonga/query-log/command/run-regression-test.rb +2 -418
  54. data/lib/groonga/query-log/command/show-running-queries.rb +3 -65
  55. data/lib/groonga/query-log/command/verify-server.rb +2 -137
  56. data/test/{test-analyzer.rb → command/test-analyzer.rb} +17 -11
  57. data/test/command/test-extract.rb +9 -18
  58. data/test/command/test-format-regression-test-logs.rb +3 -3
  59. data/test/fixtures/reporter/html.expected +55 -20
  60. data/test/helper.rb +22 -7
  61. data/test/test-incompatibility-detector.rb +3 -3
  62. data/test/test-parser.rb +19 -4
  63. data/test/test-replayer.rb +4 -4
  64. data/test/test-response-comparer.rb +2 -2
  65. metadata +86 -97
  66. data/lib/groonga/query-log/analyzer/statistic.rb +0 -194
  67. data/lib/groonga/query-log/parser.rb +0 -125
  68. data/test/fixtures/run-regression-test/db.new/db +0 -0
  69. data/test/fixtures/run-regression-test/db.new/db.0000000 +0 -0
  70. data/test/fixtures/run-regression-test/db.new/db.0000100 +0 -0
  71. data/test/fixtures/run-regression-test/db.new/db.0000101 +0 -0
  72. data/test/fixtures/run-regression-test/db.new/db.0000102 +0 -0
  73. data/test/fixtures/run-regression-test/db.new/db.0000103 +0 -0
  74. data/test/fixtures/run-regression-test/db.new/db.0000103.c +0 -0
  75. data/test/fixtures/run-regression-test/db.new/db.001 +0 -0
  76. data/test/fixtures/run-regression-test/db.new/db.conf +0 -0
  77. data/test/fixtures/run-regression-test/db.new/groonga.log +0 -165
  78. data/test/fixtures/run-regression-test/db.old/db +0 -0
  79. data/test/fixtures/run-regression-test/db.old/db.0000000 +0 -0
  80. data/test/fixtures/run-regression-test/db.old/db.0000100 +0 -0
  81. data/test/fixtures/run-regression-test/db.old/db.0000101 +0 -0
  82. data/test/fixtures/run-regression-test/db.old/db.0000102 +0 -0
  83. data/test/fixtures/run-regression-test/db.old/db.0000103 +0 -0
  84. data/test/fixtures/run-regression-test/db.old/db.0000103.c +0 -0
  85. data/test/fixtures/run-regression-test/db.old/db.001 +0 -0
  86. data/test/fixtures/run-regression-test/db.old/db.conf +0 -0
  87. data/test/fixtures/run-regression-test/db.old/groonga.log +0 -79
  88. data/test/fixtures/run-regression-test/results/query.log +0 -0
@@ -0,0 +1,171 @@
1
+ # Copyright (C) 2011-2018 Kouhei Sutou <kou@clear-code.com>
2
+ # Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License version 2.1 as published by the Free Software Foundation.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
+
17
+ require "ostruct"
18
+ require "optparse"
19
+ require "pathname"
20
+
21
+ require "groonga-query-log"
22
+ require "groonga-query-log/command-line"
23
+
24
+ module GroongaQueryLog
25
+ module Command
26
+ class Extract < CommandLine
27
+ attr_accessor :options
28
+ attr_reader :option_parser
29
+
30
+ def initialize
31
+ @options = nil
32
+ @option_parser = nil
33
+ setup_options
34
+ end
35
+
36
+ # Executes extractor for Groonga's query logs.
37
+ # "groonga-query-log-extract" command runs this method.
38
+ #
39
+ # @example
40
+ # extractor = GroongaQueryLog::Command::Extract.new
41
+ # extractor.run("--output", "commands.output",
42
+ # "--command", "select",
43
+ # "query.log")
44
+ #
45
+ # If only paths of query log files are specified,
46
+ # this method prints command(s) of them to console.
47
+ #
48
+ # @param [Array<String>] arguments arguments for
49
+ # groonga-query-log-extract. Please execute
50
+ # "groonga-query-log-extract --help" or see #setup_options.
51
+ def run(arguments)
52
+ begin
53
+ log_paths = @option_parser.parse!(arguments)
54
+ rescue OptionParser::ParseError
55
+ $stderr.puts($!.message)
56
+ return false
57
+ end
58
+
59
+ begin
60
+ if @options.output_path
61
+ File.open(@options.output_path, "w") do |output|
62
+ extract(log_paths, output)
63
+ end
64
+ else
65
+ extract(log_paths, $stdout)
66
+ end
67
+ rescue Interrupt
68
+ rescue Error
69
+ $stderr.puts($!.message)
70
+ return false
71
+ end
72
+
73
+ true
74
+ end
75
+
76
+ private
77
+ def setup_options
78
+ @options = OpenStruct.new
79
+ @options.unify_format = nil
80
+ @options.commands = []
81
+ @options.exclude_commands = []
82
+ @options.output_path = nil
83
+ @option_parser = OptionParser.new do |parser|
84
+ parser.version = VERSION
85
+ parser.banner += " QUERY_LOG1 ..."
86
+
87
+ available_formats = ["uri", "command"]
88
+ parser.on("--unify-format=FORMAT",
89
+ available_formats,
90
+ "Unify command format to FORMAT.",
91
+ "(#{available_formats.join(', ')})",
92
+ "[not unify]") do |format|
93
+ @options.unify_format = format
94
+ end
95
+
96
+ parser.on("--command=COMMAND",
97
+ "Extract only COMMAND.",
98
+ "To extract one or more commands,",
99
+ "specify this command a number of times.",
100
+ "Use /.../ as COMMAND to match command with regular expression.",
101
+ "[all commands]") do |command|
102
+ case command
103
+ when /\A\/(.*)\/(i)?\z/
104
+ @options.commands << Regexp.new($1, $2 == "i")
105
+ when
106
+ @options.commands << command
107
+ end
108
+ end
109
+
110
+ parser.on("--exclude-command=COMMAND",
111
+ "Don't extract COMMAND.",
112
+ "To ignore one or more commands,",
113
+ "specify this command a number of times.",
114
+ "Use /.../ as COMMAND to match command with regular expression.",
115
+ "[no commands]") do |command|
116
+ case command
117
+ when /\A\/(.*)\/(i)?\z/
118
+ @options.exclude_commands << Regexp.new($1, $2 == "i")
119
+ when
120
+ @options.exclude_commands << command
121
+ end
122
+ end
123
+
124
+ parser.on("--output=PATH",
125
+ "Output to PATH.",
126
+ "[standard output]") do |path|
127
+ @options.output_path = path
128
+ end
129
+ end
130
+ end
131
+
132
+ def extract(log_paths, output)
133
+ parser = Parser.new
134
+ parse_log(parser, log_paths) do |statistic|
135
+ extract_command(statistic, output)
136
+ end
137
+ end
138
+
139
+ def extract_command(statistic, output)
140
+ command = statistic.command
141
+ return unless target?(command)
142
+ command_text = nil
143
+ case @options.unify_format
144
+ when "uri"
145
+ command_text = command.to_uri_format
146
+ when "command"
147
+ command_text = command.to_command_format
148
+ else
149
+ command_text = statistic.raw_command
150
+ end
151
+ output.puts(command_text)
152
+ end
153
+
154
+ def target?(command)
155
+ name = command.command_name
156
+ target_commands = @options.commands
157
+ exclude_commands = @options.exclude_commands
158
+
159
+ unless target_commands.empty?
160
+ return target_commands.any? {|target_command| target_command === name}
161
+ end
162
+
163
+ unless exclude_commands.empty?
164
+ return (not exclude_commands.any? {|exclude_command| exclude_command === name})
165
+ end
166
+
167
+ true
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,143 @@
1
+ # Copyright (C) 2014-2017 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "English"
18
+ require "find"
19
+ require "tempfile"
20
+ require "pp"
21
+ require "optparse"
22
+ require "json"
23
+
24
+ require "groonga/command/parser"
25
+
26
+ require "groonga-query-log/version"
27
+
28
+ module GroongaQueryLog
29
+ module Command
30
+ class FormatRegressionTestLogs
31
+ def initialize
32
+ end
33
+
34
+ def run(command_line)
35
+ parser = OptionParser.new
36
+ parser.banner += " PATH1 PATH2 ..."
37
+ parser.version = VERSION
38
+ paths = parser.parse!(command_line)
39
+
40
+ if paths.empty?
41
+ format_log($stdin, "-")
42
+ else
43
+ paths.each do |path|
44
+ if File.directory?(path)
45
+ Find.find(path) do |sub_path|
46
+ next unless File.file?(sub_path)
47
+ File.open(sub_path) do |file|
48
+ format_log(file, sub_path)
49
+ end
50
+ end
51
+ else
52
+ File.open(path) do |file|
53
+ format_log(file, path)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ true
59
+ end
60
+
61
+ private
62
+ def format_log(input, path)
63
+ command = nil
64
+ response_old = nil
65
+ response_new = nil
66
+
67
+ input.each_line do |line|
68
+ unless line.valid_encoding?
69
+ puts("invalid encoding line")
70
+ puts("#{path}:#{input.lineno}:#{line}")
71
+ next
72
+ end
73
+ case line
74
+ when /\Acommand: /
75
+ command = $POSTMATCH.chomp
76
+ when /\Aresponse1: /
77
+ response_old = $POSTMATCH.chomp
78
+ when /\Aresponse2: /
79
+ response_new = $POSTMATCH.chomp
80
+ next unless valid_entry?(command, response_old, response_new)
81
+ report_diff(command, response_old, response_new)
82
+ end
83
+ end
84
+ end
85
+
86
+ def valid_entry?(command, response_old, response_new)
87
+ valid = true
88
+
89
+ begin
90
+ JSON.parse(response_old)
91
+ rescue JSON::ParserError
92
+ puts(command)
93
+ puts("failed to parse old response: #{$!.message}")
94
+ puts(response_old)
95
+ valid = false
96
+ end
97
+
98
+ begin
99
+ JSON.parse(response_new)
100
+ rescue JSON::ParserError
101
+ puts(command)
102
+ puts("failed to parse new response: #{$!.message}")
103
+ puts(response_new)
104
+ valid = false
105
+ end
106
+
107
+ valid
108
+ end
109
+
110
+ def report_diff(command, response_old, response_new)
111
+ return if response_old == response_new
112
+
113
+ Tempfile.open("response-old") do |response_old_file|
114
+ PP.pp(JSON.parse(response_old), response_old_file)
115
+ response_old_file.flush
116
+ Tempfile.open("response-new") do |response_new_file|
117
+ PP.pp(JSON.parse(response_new), response_new_file)
118
+ response_new_file.flush
119
+ report_command(command)
120
+ system("diff",
121
+ "--label=old",
122
+ "--label=new",
123
+ "-u",
124
+ response_old_file.path, response_new_file.path)
125
+ end
126
+ end
127
+ end
128
+
129
+ def report_command(command)
130
+ puts(command)
131
+ parsed_command = Groonga::Command::Parser.parse(command)
132
+ puts("Name: #{parsed_command.name}")
133
+ puts("Arguments:")
134
+ sorted_arguments = parsed_command.arguments.sort_by do |key, value|
135
+ key
136
+ end
137
+ sorted_arguments.each do |key, value|
138
+ puts(" #{key}: #{value}")
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,117 @@
1
+ # Copyright (C) 2013-2017 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "optparse"
18
+
19
+ require "groonga-query-log/version"
20
+ require "groonga-query-log/replayer"
21
+
22
+ module GroongaQueryLog
23
+ module Command
24
+ class Replay
25
+ def initialize
26
+ @options = Replayer::Options.new
27
+ end
28
+
29
+ def run(command_line)
30
+ input_paths = create_parser.parse(command_line)
31
+ replayer = Replayer.new(@options)
32
+ input_paths.each do |input_path|
33
+ File.open(input_path) do |input|
34
+ replayer.replay(input)
35
+ end
36
+ end
37
+ true
38
+ end
39
+
40
+ private
41
+ def create_parser
42
+ parser = OptionParser.new
43
+ parser.version = VERSION
44
+ parser.banner += " QUERY_LOG"
45
+
46
+ parser.separator("")
47
+ parser.separator("Options:")
48
+
49
+ parser.on("--host=HOST",
50
+ "Host name or IP address of Groonga server",
51
+ "[#{@options.host}]") do |host|
52
+ @options.host = host
53
+ end
54
+
55
+ parser.on("--port=PORT", Integer,
56
+ "Port number of Groonga server",
57
+ "[#{@options.port}]") do |port|
58
+ @options.port = port
59
+ end
60
+
61
+ available_protocols = [:gqtp, :http]
62
+ available_protocols_label = "(#{available_protocols.join(', ')})"
63
+ parser.on("--protocol=PROTOCOL", available_protocols,
64
+ "Protocol of Groonga server",
65
+ "[#{@options.protocol}]",
66
+ available_protocols_label) do |protocol|
67
+ @options.protocol = protocol
68
+ end
69
+
70
+ parser.on("--n-clients=N", Integer,
71
+ "The max number of concurrency",
72
+ "[#{@options.n_clients}]") do |n_clients|
73
+ @options.n_clients = n_clients
74
+ end
75
+
76
+ parser.on("--request-queue-size=SIZE", Integer,
77
+ "The size of request queue",
78
+ "[auto]") do |size|
79
+ @options.request_queue_size = size
80
+ end
81
+
82
+ parser.on("--disable-cache",
83
+ "Add 'cache=no' parameter to request",
84
+ "[#{@options.disable_cache?}]") do
85
+ @options.disable_cache = true
86
+ end
87
+
88
+ parser.on("--target-command-name=NAME",
89
+ "Add NAME to target command names",
90
+ "You can specify this option zero or more times",
91
+ "See also --target-command-names") do |name|
92
+ @options.target_command_names << name
93
+ end
94
+
95
+ target_command_names_label = @options.target_command_names.join(", ")
96
+ parser.on("--target-command-names=NAME1,NAME2,...", Array,
97
+ "Replay only NAME1,NAME2,... commands",
98
+ "You can use glob to choose command name",
99
+ "[#{target_command_names_label}]") do |names|
100
+ @options.target_command_names = names
101
+ end
102
+
103
+ parser.on("--output-requests=PATH",
104
+ "Output requests to PATH",
105
+ "[not output]") do |path|
106
+ @options.requests_path = path
107
+ end
108
+
109
+ parser.on("--output-responses=PATH",
110
+ "Output responses to PATH",
111
+ "[not output]") do |path|
112
+ @options.responses_path = path
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,432 @@
1
+ # Copyright (C) 2014-2017 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "rbconfig"
18
+ require "optparse"
19
+ require "socket"
20
+ require "fileutils"
21
+ require "pathname"
22
+ require "net/http"
23
+
24
+ require "groonga-query-log"
25
+ require "groonga-query-log/command/verify-server"
26
+
27
+ module GroongaQueryLog
28
+ module Command
29
+ class RunRegressionTest
30
+ def initialize
31
+ @input_directory = Pathname.new(".")
32
+ @working_directory = Pathname.new(".")
33
+
34
+ @old_groonga = "groonga"
35
+ @old_database = "db.old/db"
36
+ @old_groonga_options = []
37
+
38
+ @new_groonga = "groonga"
39
+ @new_database = "db.new/db"
40
+ @new_groonga_options = []
41
+
42
+ @recreate_database = false
43
+ @load_data = true
44
+ @run_queries = true
45
+ @skip_finished_queries = false
46
+ @output_query_log = false
47
+ @care_order = true
48
+ @verify_cachehit_mode = false
49
+ end
50
+
51
+ def run(command_line)
52
+ option_parser = create_option_parser
53
+ begin
54
+ option_parser.parse!(command_line)
55
+ rescue OptionParser::ParseError => error
56
+ $stderr.puts(error.message)
57
+ return false
58
+ end
59
+
60
+ tester = Tester.new(old_groonga_server,
61
+ new_groonga_server,
62
+ tester_options)
63
+ tester.run
64
+ end
65
+
66
+ private
67
+ def create_option_parser
68
+ parser = OptionParser.new
69
+ parser.version = VERSION
70
+
71
+ parser.separator("")
72
+ parser.separator("Path:")
73
+ parser.on("--input-directory=DIRECTORY",
74
+ "Load schema and data from DIRECTORY.",
75
+ "(#{@input_directory})") do |directory|
76
+ @input_directory = Pathname.new(directory)
77
+ end
78
+ parser.on("--working-directory=DIRECTORY",
79
+ "Use DIRECTORY as working directory.",
80
+ "(#{@working_directory})") do |directory|
81
+ @working_directory = Pathname.new(directory)
82
+ end
83
+
84
+ parser.separator("")
85
+ parser.separator("Throughput:")
86
+ parser.on("--n-clients=N", Integer,
87
+ "Use N clients concurrently.",
88
+ "(#{@n_clients})") do |n|
89
+ @n_clients = n
90
+ end
91
+
92
+ parser.separator("")
93
+ parser.separator("Old Groonga:")
94
+ parser.on("--old-groonga=GROONGA",
95
+ "Old groonga command",
96
+ "(#{@old_groonga})") do |groonga|
97
+ @old_groonga = groonga
98
+ end
99
+
100
+ parser.on("--old-groonga-option=OPTION",
101
+ "Add an additional old groonga option",
102
+ "You can specify this option multiple times to specify multiple groonga options",
103
+ "(no options)") do |groonga_option|
104
+ @old_groonga_options << groonga_option
105
+ end
106
+
107
+ parser.separator("")
108
+ parser.separator("New Groonga:")
109
+ parser.on("--new-groonga=GROONGA",
110
+ "New groonga command",
111
+ "(#{@new_groonga})") do |groonga|
112
+ @new_groonga = groonga
113
+ end
114
+
115
+ parser.on("--new-groonga-option=OPTION",
116
+ "Add an additional new groonga option",
117
+ "You can specify this option multiple times to specify multiple groonga options",
118
+ "(no options)") do |groonga_option|
119
+ @new_groonga_options << groonga_option
120
+ end
121
+
122
+ parser.separator("")
123
+ parser.separator("Operations:")
124
+ parser.on("--recreate-database",
125
+ "Always recreate Groonga database") do
126
+ @recreate_database = true
127
+ end
128
+ parser.on("--no-load-data",
129
+ "Don't load data. Just loads schema to Groonga database") do
130
+ @load_data = false
131
+ end
132
+ parser.on("--no-run-queries",
133
+ "Don't run queries. Just creates Groonga database") do
134
+ @run_queries = false
135
+ end
136
+ parser.on("--skip-finished-queries",
137
+ "Don't run finished query logs.") do
138
+ @skip_finished_queries = true
139
+ end
140
+ parser.on("--output-query-log",
141
+ "Output query log in verified target Groonga servers") do
142
+ @output_query_log = true
143
+ end
144
+ parser.on("--no-care-order",
145
+ "Don't care order of select response records") do
146
+ @care_order = false
147
+ end
148
+
149
+ parser
150
+ end
151
+
152
+ def directory_options
153
+ {
154
+ :input_directory => @input_directory,
155
+ :working_directory => @working_directory,
156
+ }
157
+ end
158
+
159
+ def server_options
160
+ options = {
161
+ :load_data => @load_data,
162
+ :run_queries => @run_queries,
163
+ :recreate_database => @recreate_database,
164
+ :output_query_log => @output_query_log,
165
+ }
166
+ directory_options.merge(options)
167
+ end
168
+
169
+ def tester_options
170
+ options = {
171
+ :n_clients => @n_clients,
172
+ :care_order => @care_order,
173
+ :skip_finished_queries => @skip_finished_queries,
174
+ }
175
+ directory_options.merge(options)
176
+ end
177
+
178
+ def old_groonga_server
179
+ GroongaServer.new(@old_groonga,
180
+ @old_groonga_options,
181
+ @old_database,
182
+ server_options)
183
+ end
184
+
185
+ def new_groonga_server
186
+ GroongaServer.new(@new_groonga,
187
+ @new_groonga_options,
188
+ @new_database,
189
+ server_options)
190
+ end
191
+
192
+ class GroongaServer
193
+ attr_reader :host, :port
194
+ def initialize(groonga, groonga_options, database_path, options)
195
+ @input_directory = options[:input_directory] || Pathname.new(".")
196
+ @working_directory = options[:working_directory] || Pathname.new(".")
197
+ @groonga = groonga
198
+ @groonga_options = groonga_options
199
+ @database_path = @working_directory + database_path
200
+ @host = "127.0.0.1"
201
+ @port = find_unused_port
202
+ @options = options
203
+ end
204
+
205
+ def run
206
+ return unless @options[:run_queries]
207
+
208
+ arguments = @groonga_options.dup
209
+ arguments.concat(["--bind-address", @host])
210
+ arguments.concat(["--port", @port.to_s])
211
+ arguments.concat(["--protocol", "http"])
212
+ arguments.concat(["--log-path", log_path.to_s])
213
+ if @options[:output_query_log]
214
+ arguments.concat(["--query-log-path", query_log_path.to_s])
215
+ end
216
+ arguments << "-s"
217
+ arguments << @database_path.to_s
218
+ @pid = spawn(@groonga, *arguments)
219
+
220
+ n_retries = 10
221
+ begin
222
+ send_command("status")
223
+ rescue SystemCallError
224
+ sleep(1)
225
+ n_retries -= 1
226
+ raise if n_retries.zero?
227
+ retry
228
+ end
229
+
230
+ yield if block_given?
231
+ end
232
+
233
+ def ensure_database
234
+ if @options[:recreate_database]
235
+ FileUtils.rm_rf(@database_path.dirname.to_s)
236
+ end
237
+
238
+ return if @database_path.exist?
239
+ FileUtils.mkdir_p(@database_path.dirname.to_s)
240
+ system(@groonga, "-n", @database_path.to_s, "quit")
241
+ grn_files.each do |grn_file|
242
+ command = [
243
+ @groonga,
244
+ "--log-path", log_path.to_s,
245
+ "--file", grn_file.to_s,
246
+ @database_path.to_s,
247
+ ]
248
+ command_line = command.join(" ")
249
+ puts("Running...: #{command_line}")
250
+ pid = spawn(*command)
251
+ begin
252
+ pid, status = Process.waitpid2(pid)
253
+ rescue Interrupt
254
+ Process.kill(:TERM, pid)
255
+ pid, status = Process.waitpid2(pid)
256
+ end
257
+ unless status.success?
258
+ raise "Failed to run: #{command_line}"
259
+ end
260
+ end
261
+ end
262
+
263
+ def use_persistent_cache?
264
+ @groonga_options.include?("--cache-base-path")
265
+ end
266
+
267
+ def shutdown
268
+ begin
269
+ send_command("shutdown")
270
+ rescue SystemCallError
271
+ end
272
+ Process.waitpid(@pid)
273
+ end
274
+
275
+ private
276
+ def find_unused_port
277
+ server = TCPServer.new(@host, 0)
278
+ begin
279
+ server.addr[1]
280
+ ensure
281
+ server.close
282
+ end
283
+ end
284
+
285
+ def log_path
286
+ @database_path.dirname + "groonga.log"
287
+ end
288
+
289
+ def query_log_path
290
+ @database_path.dirname + "query.log"
291
+ end
292
+
293
+ def send_command(name)
294
+ Net::HTTP.start(@host, @port) do |http|
295
+ response = http.get("/d/#{name}")
296
+ response.body
297
+ end
298
+ end
299
+
300
+ def grn_files
301
+ files = schema_files
302
+ files += data_files if @options[:load_data]
303
+ files += index_files
304
+ files
305
+ end
306
+
307
+ def schema_files
308
+ Pathname.glob("#{@input_directory}/schema/**/*.grn").sort
309
+ end
310
+
311
+ def index_files
312
+ Pathname.glob("#{@input_directory}/indexes/**/*.grn").sort
313
+ end
314
+
315
+ def data_files
316
+ Pathname.glob("#{@input_directory}/data/**/*.grn").sort
317
+ end
318
+ end
319
+
320
+ class Tester
321
+ def initialize(old, new, options)
322
+ @old = old
323
+ @new = new
324
+ @input_directory = options[:input_directory] || Pathname.new(".")
325
+ @working_directory = options[:working_directory] || Pathname.new(".")
326
+ @n_clients = options[:n_clients] || 1
327
+ @options = options
328
+ @n_ready_waits = 2
329
+ end
330
+
331
+ def run
332
+ @old.ensure_database
333
+ @new.ensure_database
334
+
335
+ old_thread = Thread.new do
336
+ @old.run do
337
+ run_test
338
+ end
339
+ end
340
+ new_thread = Thread.new do
341
+ @new.run do
342
+ run_test
343
+ end
344
+ end
345
+
346
+ old_thread_success = old_thread.value
347
+ new_thread_success = new_thread.value
348
+
349
+ old_thread_success and new_thread_success
350
+ end
351
+
352
+ private
353
+ def run_test
354
+ @n_ready_waits -= 1
355
+ return true unless @n_ready_waits.zero?
356
+
357
+ query_log_paths.each do |query_log_path|
358
+ log_path = test_log_path(query_log_path)
359
+ if @options[:skip_finished_queries] and log_path.exist?
360
+ puts("Skip query log: #{query_log_path}")
361
+ next
362
+ else
363
+ puts("Running test against query log...: #{query_log_path}")
364
+ end
365
+ begin
366
+ if use_persistent_cache?
367
+ callback = lambda do
368
+ if @old.use_persistent_cache?
369
+ @old.shutdown
370
+ @old.run
371
+ end
372
+ if @new.use_persistent_cache?
373
+ @new.shutdown
374
+ @new.run
375
+ end
376
+ end
377
+ else
378
+ callback = nil
379
+ end
380
+ verify_server(log_path, query_log_path, &callback)
381
+ rescue Interrupt
382
+ puts("Interrupt: #{query_log_path}")
383
+ end
384
+ end
385
+
386
+ old_thread = Thread.new do
387
+ @old.shutdown
388
+ end
389
+ new_thread = Thread.new do
390
+ @new.shutdown
391
+ end
392
+ old_thread.join
393
+ new_thread.join
394
+
395
+ true
396
+ end
397
+
398
+ def verify_server(test_log_path, query_log_path, &callback)
399
+ command_line = [
400
+ "--n-clients=#{@n_clients}",
401
+ "--groonga1-host=#{@old.host}",
402
+ "--groonga1-port=#{@old.port}",
403
+ "--groonga1-protocol=http",
404
+ "--groonga2-host=#{@new.host}",
405
+ "--groonga2-port=#{@new.port}",
406
+ "--groonga2-protocol=http",
407
+ "--output", test_log_path.to_s,
408
+ ]
409
+ command_line << "--no-care-order" if @options[:care_order] == false
410
+ command_line << query_log_path.to_s
411
+ if use_persistent_cache?
412
+ command_line << "--verify-cache"
413
+ end
414
+ verify_server = VerifyServer.new
415
+ verify_server.run(command_line, &callback)
416
+ end
417
+
418
+ def query_log_paths
419
+ Pathname.glob("#{@input_directory}/query-logs/**/*.log").sort
420
+ end
421
+
422
+ def test_log_path(query_log_path)
423
+ @working_directory + "results" + query_log_path.basename
424
+ end
425
+
426
+ def use_persistent_cache?
427
+ @old.use_persistent_cache? or @new.use_persistent_cache?
428
+ end
429
+ end
430
+ end
431
+ end
432
+ end