rrrspec-client 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 187ea5b96b3437a7c839c23b3e5f1aa3df3b5716
4
+ data.tar.gz: d83a9df93ffa5ae672b5909e887a52173f6941ff
5
+ SHA512:
6
+ metadata.gz: c6583325da9e6865baca29415e524f662ecfcdf1a7d2be524b3769748855a20f6629cfe9dae6102bf035534fd4a314a4271e5ace84e421780e56ed5121d435af
7
+ data.tar.gz: b1f67aba5e30a24d1ee5dad72ae4dc15587d5b25470546c29ebc290be512c3b63743ae43a1b3e630619abb1b9d1308563ad2d0347d8ed7c9cc004431e611a93e
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0-p247
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/rrrspec ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ case ARGV[0]
4
+ when "server", "worker"
5
+ require 'rrrspec/server/cli'
6
+ RRRSpec::Server::CLI.start
7
+ when "scaler", "scalerspy"
8
+ require 'rrrspec/scaler/cli'
9
+ RRRSpec::Scaler::CLI.start
10
+ else
11
+ require 'rrrspec/client/cli'
12
+ RRRSpec::Client::CLI.start
13
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rrrspec/client/cli'
4
+ RRRSpec::Client::CLI.start
@@ -0,0 +1,131 @@
1
+ require 'rrrspec/client'
2
+ require 'launchy'
3
+ require 'thor'
4
+
5
+ module RRRSpec
6
+ module Client
7
+ class CLI < Thor
8
+ WAIT_POLLING_SEC = 10
9
+
10
+ package_name 'RRRSpec'
11
+ default_command 'help'
12
+ class_option :config, aliases: '-c', type: :string, default: ''
13
+
14
+ no_commands do
15
+ def setup(conf)
16
+ RRRSpec.setup(conf, options[:config].split(':'))
17
+ end
18
+
19
+ def log_exception
20
+ yield
21
+ rescue
22
+ RRRSpec.logger.error($!)
23
+ raise
24
+ end
25
+ end
26
+
27
+ option :'key-only', type: :boolean
28
+ option :'rsync-name', type: :string, default: ENV['USER']
29
+ option :'worker-type', type: :string
30
+ desc 'start', 'start RRRSpec'
31
+ def start
32
+ setup(ClientConfiguration.new)
33
+ if options[:'worker-type']
34
+ RRRSpec.configuration.worker_type = options[:'worker-type']
35
+ end
36
+ taskset = Support.start_taskset(RRRSpec.configuration, options[:'rsync-name'])
37
+ puts taskset.key
38
+
39
+ if RRRSpec.configuration.rrrspec_web_base && !options[:'key-only']
40
+ url = "#{RRRSpec.configuration.rrrspec_web_base}/tasksets/#{taskset.key}"
41
+ Launchy.open(url)
42
+ end
43
+ end
44
+
45
+ desc 'cancel', 'cancel the taskset'
46
+ def cancel(taskset_id)
47
+ setup(Configuration.new)
48
+ taskset = Taskset.new(taskset_id)
49
+ exit(1) unless taskset.exist?
50
+ taskset.cancel
51
+ end
52
+
53
+ desc 'cancelall', 'cancel all tasksets whose rsync name is specified name'
54
+ def cancelall(rsync_name)
55
+ setup(Configuration.new)
56
+ ActiveTaskset.all_tasksets_of(rsync_name).each do |taskset|
57
+ taskset.cancel
58
+ end
59
+ end
60
+
61
+ desc 'actives', 'list up the active tasksets'
62
+ def actives
63
+ setup(Configuration.new)
64
+ ActiveTaskset.list.each { |taskset| puts taskset.key }
65
+ end
66
+
67
+ desc 'nodes', 'list up the active nodes'
68
+ def nodes
69
+ setup(Configuration.new)
70
+ puts "Server:"
71
+ puts "\t#{RSyncInfo.exist? ? "Yes" : "No"}"
72
+ puts "Workers:"
73
+ Worker.list.each { |worker| puts "\t#{worker.key}" }
74
+ end
75
+
76
+ option :pollsec, type: :numeric, default: WAIT_POLLING_SEC
77
+ desc 'waitfor', 'wait for the taskset'
78
+ def waitfor(taskset_id)
79
+ setup(Configuration.new)
80
+ taskset = Taskset.new(taskset_id)
81
+ exit(1) unless taskset.exist?
82
+
83
+ rd, wt = IO.pipe
84
+ Signal.trap(:TERM) { wt.write("1") }
85
+ Signal.trap(:INT) { wt.write("1") }
86
+
87
+ loop do
88
+ rs, ws, = IO.select([rd], [], [], options[:pollsec])
89
+ if rs == nil
90
+ break if taskset.persisted?
91
+ elsif rs.size != 0
92
+ rs[0].getc
93
+ taskset.cancel
94
+ end
95
+ end
96
+ end
97
+
98
+ option :'failure-exit-code', type: :numeric, default: 1
99
+ option :verbose, type: :boolean, default: false
100
+ desc 'show', 'show the result of the taskset'
101
+ def show(taskset_id)
102
+ setup(Configuration.new)
103
+ taskset = Taskset.new(taskset_id)
104
+ exit 1 unless taskset.exist?
105
+ Support.show_result(taskset, options[:verbose])
106
+
107
+ if taskset.status != 'succeeded'
108
+ exit options[:'failure-exit-code']
109
+ end
110
+ end
111
+
112
+ desc 'slave', 'run RRRSpec as a slave'
113
+ def slave(working_dir=nil, taskset_key=nil)
114
+ $0 = 'rrrspec slave'
115
+ working_dir ||= ENV['RRRSPEC_WORKING_DIR']
116
+ taskset_key ||= ENV['RRRSPEC_TASKSET_KEY']
117
+ exit 1 unless taskset_key && working_dir
118
+
119
+ setup(Configuration.new)
120
+ log_exception do
121
+ slave = Slave.create
122
+ slave_runner = SlaveRunner.new(slave, working_dir, taskset_key)
123
+ Thread.abort_on_exception = true
124
+ Thread.fork { RRRSpec.pacemaker(slave, 60, 5) }
125
+ Thread.fork { slave_runner.work_loop }
126
+ Kernel.sleep
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,82 @@
1
+ module RRRSpec
2
+ module Client
3
+ class ClientConfiguration < Configuration
4
+ attr_accessor :packaging_dir
5
+ attr_accessor :packaging_rsync_options
6
+ attr_writer :spec_files
7
+ attr_accessor :setup_command, :slave_command
8
+ attr_accessor :taskset_class, :worker_type
9
+ attr_accessor :max_workers, :max_trials
10
+ attr_accessor :rrrspec_web_base
11
+ attr_accessor :unknown_spec_timeout_sec, :least_timeout_sec
12
+
13
+ def spec_files
14
+ case @spec_files
15
+ when Proc then @spec_files.call
16
+ when String then [@spec_files]
17
+ else @spec_files
18
+ end
19
+ end
20
+
21
+ def initialize
22
+ super()
23
+ @type = :client
24
+ @unknown_spec_timeout_sec = 5 * 60
25
+ @least_timeout_sec = 30
26
+ end
27
+
28
+ def check_validity
29
+ validity = super
30
+
31
+ unless Dir.exists?(packaging_dir)
32
+ $stderr.puts("The packaging_dir does not exists: '#{packaging_dir}'")
33
+ validity = false
34
+ end
35
+
36
+ unless spec_files.is_a?(Array)
37
+ $stderr.puts("The spec_files should be an Array: '#{spec_files}'")
38
+ validity = false
39
+ else
40
+ spec_files.each do |filepath|
41
+ unless File.exists?(File.join(packaging_dir, filepath))
42
+ $stderr.puts("One of the spec_files does not exists '#{filepath}'")
43
+ validity = false
44
+ end
45
+ end
46
+ end
47
+
48
+ unless max_workers.is_a?(Integer)
49
+ $stderr.puts("The max_workers should be an Integer: '#{max_workers}'")
50
+ validity = false
51
+ else
52
+ unless max_workers >= 1
53
+ $stderr.puts("The max_workers should not be less than 1: #{max_workers}")
54
+ validity = false
55
+ end
56
+ end
57
+
58
+ unless max_trials.is_a?(Integer)
59
+ $stderr.puts("The max_trials should be an Integer: '#{max_trials}'")
60
+ validity = false
61
+ end
62
+
63
+ unless taskset_class.is_a?(String)
64
+ $stderr.puts("The taskset_class should be a String: '#{taskset_class}'")
65
+ validity = false
66
+ end
67
+
68
+ unless unknown_spec_timeout_sec.is_a?(Integer)
69
+ $stderr.puts("The unknown_spec_timeout_sec should be an Integer: '#{unknown_spec_timeout_sec}'")
70
+ validity = false
71
+ end
72
+
73
+ unless least_timeout_sec.is_a?(Integer)
74
+ $stderr.puts("The least_timeout_sec should be an Integer: '#{least_timeout_sec}'")
75
+ validity = false
76
+ end
77
+
78
+ validity
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,83 @@
1
+ require 'rspec'
2
+ require 'rspec/core/formatters/base_text_formatter'
3
+
4
+ module RRRSpec
5
+ module Client
6
+ class RSpecRunner
7
+ def initialize
8
+ @options = RSpec::Core::ConfigurationOptions.new([])
9
+ @options.parse_options
10
+ @configuration = RSpec.configuration
11
+ @configuration.setup_load_path_and_require([])
12
+ @world = RSpec.world
13
+ @before_suite_run = false
14
+ end
15
+
16
+ def exc_safe_replace_stdouts
17
+ outbuf = ''
18
+ errbuf = ''
19
+ $stdout = StringIO.new(outbuf)
20
+ $stderr = StringIO.new(errbuf)
21
+ begin
22
+ yield
23
+ rescue Exception
24
+ $stdout.puts $!
25
+ $stdout.puts $!.backtrace.join("\n")
26
+ end
27
+ [outbuf, errbuf]
28
+ ensure
29
+ $stdout = STDOUT
30
+ $stderr = STDERR
31
+ end
32
+
33
+ def setup(filepath)
34
+ status = false
35
+ outbuf, errbuf = exc_safe_replace_stdouts do
36
+ begin
37
+ @options.configure(@configuration)
38
+ @configuration.files_to_run = [filepath]
39
+ @configuration.load_spec_files
40
+ @world.announce_filters
41
+ unless @before_suite_run
42
+ @configuration.run_hook(:before, :suite)
43
+ @before_suite_run = true
44
+ end
45
+ status = true
46
+ rescue Exception
47
+ $stdout.puts $!
48
+ $stdout.puts $!.backtrace.join("\n")
49
+ status = false
50
+ end
51
+ end
52
+
53
+ [status, outbuf, errbuf]
54
+ end
55
+
56
+ def run(*formatters)
57
+ status = false
58
+ outbuf, errbuf = exc_safe_replace_stdouts do
59
+ @configuration.formatters << RSpec::Core::Formatters::BaseTextFormatter.new($stdout)
60
+ formatters.each do |formatter|
61
+ @configuration.formatters << formatter
62
+ end
63
+ @configuration.reporter.report(
64
+ @world.example_count,
65
+ @configuration.randomize? ? @configuration.seed : nil
66
+ ) do |reporter|
67
+ @world.example_groups.ordered.each do |example_group|
68
+ example_group.run(reporter)
69
+ end
70
+ end
71
+ status = true
72
+ end
73
+
74
+ [status, outbuf, errbuf]
75
+ end
76
+
77
+ def reset
78
+ @world.example_groups.clear
79
+ @configuration.reset
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,128 @@
1
+ require 'set'
2
+ require 'extreme_timeout'
3
+ require 'timeout'
4
+
5
+ module RRRSpec
6
+ module Client
7
+ class SlaveRunner
8
+ attr_reader :key
9
+
10
+ TASKQUEUE_TASK_TIMEOUT = -1
11
+ TASKQUEUE_ARBITER_TIMEOUT = 20
12
+ TIMEOUT_EXITCODE = 42
13
+ class SoftTimeoutException < Exception; end
14
+
15
+ def initialize(slave, working_dir, taskset_key)
16
+ @slave = slave
17
+ @taskset = Taskset.new(taskset_key)
18
+ @timeout = TASKQUEUE_TASK_TIMEOUT
19
+ @rspec_runner = RSpecRunner.new
20
+ @working_path = File.join(working_dir, @taskset.rsync_name)
21
+ @unknown_spec_timeout_sec = @taskset.unknown_spec_timeout_sec
22
+ @least_timeout_sec = @taskset.least_timeout_sec
23
+ @worked_task_keys = Set.new
24
+ end
25
+
26
+ def work_loop
27
+ loop { work }
28
+ end
29
+
30
+ def spec_timeout_sec(task)
31
+ if task.estimate_sec == nil
32
+ soft_timeout_sec = @unknown_spec_timeout_sec
33
+ hard_timeout_sec = @unknown_spec_timeout_sec + 30
34
+ else
35
+ estimate_sec = task.estimate_sec
36
+ soft_timeout_sec = estimate_sec * 2
37
+ hard_timeout_sec = estimate_sec * 3
38
+ end
39
+ return [soft_timeout_sec, @least_timeout_sec].max, [hard_timeout_sec, @least_timeout_sec].max
40
+ end
41
+
42
+ def work
43
+ task = @taskset.dequeue_task(@timeout)
44
+ unless task
45
+ @timeout = TASKQUEUE_ARBITER_TIMEOUT
46
+ ArbiterQueue.check(@taskset)
47
+ else
48
+ @timeout = TASKQUEUE_TASK_TIMEOUT
49
+ if @worked_task_keys.include?(task.key)
50
+ @taskset.reversed_enqueue_task(task)
51
+ return
52
+ else
53
+ @worked_task_keys << task.key
54
+ end
55
+
56
+ return if task.status.present?
57
+ trial = Trial.create(task, @slave)
58
+
59
+ @rspec_runner.reset
60
+ status, outbuf, errbuf = @rspec_runner.setup(File.join(@working_path, task.spec_file))
61
+ unless status
62
+ trial.finish('error', outbuf, errbuf, nil, nil, nil)
63
+ ArbiterQueue.trial(trial)
64
+ return
65
+ end
66
+
67
+ soft_timeout_sec, hard_timeout_sec = spec_timeout_sec(task)
68
+
69
+ formatter = RedisReportingFormatter.new
70
+ trial.start
71
+ status, outbuf, errbuf = ExtremeTimeout::timeout(
72
+ hard_timeout_sec, TIMEOUT_EXITCODE
73
+ ) do
74
+ Timeout::timeout(soft_timeout_sec, SoftTimeoutException) do
75
+ @rspec_runner.run(formatter)
76
+ end
77
+ end
78
+ if status
79
+ trial.finish(formatter.status, outbuf, errbuf,
80
+ formatter.passed, formatter.pending, formatter.failed)
81
+ else
82
+ trial.finish('error', outbuf, errbuf, nil, nil, nil)
83
+ end
84
+
85
+ ArbiterQueue.trial(trial)
86
+ end
87
+ end
88
+
89
+ class RedisReportingFormatter
90
+ attr_reader :passed, :pending, :failed
91
+
92
+ def initialize
93
+ @passed = 0
94
+ @pending = 0
95
+ @failed = 0
96
+ @timeout = false
97
+ end
98
+
99
+ def example_passed(example)
100
+ @passed += 1
101
+ end
102
+
103
+ def example_pending(example)
104
+ @pending += 1
105
+ end
106
+
107
+ def example_failed(example)
108
+ @failed += 1
109
+ if example.exception.is_a?(SoftTimeoutException)
110
+ @timeout = true
111
+ end
112
+ end
113
+
114
+ def status
115
+ if @timeout
116
+ 'timeout'
117
+ elsif @failed != 0
118
+ 'failed'
119
+ elsif @pending != 0
120
+ 'pending'
121
+ else
122
+ 'passed'
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,147 @@
1
+ module RRRSpec
2
+ module Client
3
+ module Support
4
+ module_function
5
+
6
+ def start_taskset(conf, rsync_name)
7
+ $stderr.puts '1/3) Making a package...'
8
+ if is_using_rsync?(rsync_name)
9
+ $stderr.puts 'It seems you are running rrrspec already'
10
+ $stderr.puts 'Please wait until the previous run finishes'
11
+ exit 1
12
+ end
13
+ unless run_rsync_package(rsync_name)
14
+ $stderr.puts 'rsync failed.'
15
+ exit 1
16
+ end
17
+
18
+ $stderr.puts '2/3) Uploading the package...'
19
+ taskset = Taskset.create(
20
+ rsync_name,
21
+ conf.setup_command,
22
+ conf.slave_command,
23
+ conf.worker_type,
24
+ conf.taskset_class,
25
+ conf.max_workers,
26
+ conf.max_trials,
27
+ conf.unknown_spec_timeout_sec,
28
+ conf.least_timeout_sec
29
+ )
30
+ estimate_sec_sorted(conf.taskset_class, conf.spec_files.uniq).reverse_each do |spec_file, estimate_sec|
31
+ task = Task.create(taskset, estimate_sec, spec_file)
32
+ taskset.add_task(task)
33
+ taskset.enqueue_task(task)
34
+ end
35
+
36
+ $stderr.puts '3/3) Enqueue the taskset...'
37
+ ActiveTaskset.add(taskset)
38
+ DispatcherQueue.notify
39
+ $stderr.puts 'Your request is successfully enqueued!'
40
+ return taskset
41
+ end
42
+
43
+ def show_result(taskset, verbose=false)
44
+ puts "Status: #{taskset.status}"
45
+ puts "Created: #{taskset.created_at}"
46
+ puts "Finished: #{taskset.finished_at}"
47
+ puts "Tasks: #{taskset.task_size}"
48
+ puts "Succeeded: #{taskset.succeeded_count}"
49
+ puts "Failed: #{taskset.failed_count}"
50
+
51
+ if verbose
52
+ puts
53
+
54
+ puts "Log:"
55
+ taskset.log.split("\n").each { |line| puts "\t#{line}" }
56
+ puts
57
+
58
+ puts "Workers:"
59
+ taskset.worker_logs.each do |worker_log|
60
+ puts "\tKey: #{worker_log.key}"
61
+ puts "\tStarted: #{worker_log.started_at}"
62
+ puts "\tRSync Finished: #{worker_log.rsync_finished_at}"
63
+ puts "\tSetup Finished: #{worker_log.setup_finished_at}"
64
+ puts "\tFinished: #{worker_log.finished_at}"
65
+ puts "\tLog:"
66
+ worker_log.log.split("\n").each { |line| puts "\t\t#{line}" }
67
+ end
68
+ puts
69
+
70
+ puts "Slaves:"
71
+ taskset.slaves.each do |slave|
72
+ puts "\tKey: #{slave.key}"
73
+ puts "\tStatus: #{slave.status}"
74
+ puts "\tTrials:"
75
+ slave.trials.each do |trial|
76
+ puts "\t\t#{trial.key}"
77
+ end
78
+ puts "\tLog:"
79
+ slave.log.split("\n").each { |line| puts "\t\t#{line}" }
80
+ end
81
+ puts
82
+
83
+ puts "Tasks:"
84
+ taskset.tasks.each do |task|
85
+ puts "\tKey: #{task.key}"
86
+ puts "\tSpec: #{task.spec_file}"
87
+ puts "\tStatus: #{task.status}"
88
+ puts "\tTrials:"
89
+ task.trials.each do |trial|
90
+ puts "\t\tKey: #{trial.key}"
91
+ puts "\t\tSlave: #{trial.slave.key}"
92
+ puts "\t\tStatus: #{trial.status}"
93
+ puts "\t\tStarted: #{trial.started_at}"
94
+ puts "\t\tFinished: #{trial.finished_at}"
95
+ puts "\t\tPassed: #{trial.passed}"
96
+ puts "\t\tPending: #{trial.pending}"
97
+ puts "\t\tFailed: #{trial.failed}"
98
+ stdout = trial.stdout
99
+ if stdout
100
+ puts "\t\tSTDOUT:"
101
+ stdout.split("\n").each { |line| puts "\t\t\t#{line}" }
102
+ end
103
+ stderr = trial.stderr
104
+ if stderr
105
+ puts "\t\tSTDERR:"
106
+ stderr.split("\n").each { |line| puts "\t\t\t#{line}" }
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ def run_rsync_package(rsync_name)
114
+ conf = RRRSpec.configuration
115
+ remote_dir = File.join(RSyncInfo.rsync_dir, rsync_name)
116
+ remote_path = "#{RSyncInfo.rsync_server}:#{remote_dir}"
117
+ command = "rsync #{conf.packaging_rsync_options} #{conf.packaging_dir}/ #{remote_path}"
118
+ $stderr.puts command
119
+ system(command)
120
+ $?.success?
121
+ end
122
+
123
+ def is_using_rsync?(rsync_name)
124
+ ActiveTaskset.list.any? do |taskset|
125
+ taskset.rsync_name == rsync_name
126
+ end
127
+ end
128
+
129
+ # Public: Sort the spec filepaths by their estiamted spec execution times.
130
+ #
131
+ # Returns an array of [spec_file, estiamte_sec]
132
+ def estimate_sec_sorted(taskset_class, spec_files)
133
+ estimate_secs = TasksetEstimation.estimate_secs(taskset_class)
134
+ spec_files.map do |spec_file|
135
+ [spec_file, estimate_secs[spec_file]]
136
+ end.sort do |a, b|
137
+ case
138
+ when a[1] == nil && b[1] == nil then 0
139
+ when a[1] == nil && b[1] != nil then 1
140
+ when a[1] != nil && b[1] == nil then -1
141
+ else a[1] - b[1]
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,5 @@
1
+ module RRRSpec
2
+ module Client
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'rrrspec'
2
+ require 'rrrspec/client/configuration'
3
+ require 'rrrspec/client/rspec_runner'
4
+ require 'rrrspec/client/slave_runner'
5
+ require 'rrrspec/client/support'
@@ -0,0 +1,42 @@
1
+ module RRRSpec
2
+ class Configuration
3
+ attr_accessor :loaded
4
+ attr_reader :type
5
+
6
+ def redis=(arg)
7
+ @redis_value = arg
8
+ end
9
+
10
+ def redis
11
+ case @redis_value
12
+ when Hash then Redis.new(@redis_value)
13
+ when Proc then @redis_value.call
14
+ else @redis_value
15
+ end
16
+ end
17
+
18
+ def check_validity
19
+ validity = true
20
+
21
+ unless @redis_value
22
+ $stderr.puts("Redis configuration is empty")
23
+ validity = false
24
+ end
25
+
26
+ validity
27
+ end
28
+
29
+ def load_files(files)
30
+ loaded = []
31
+ files.each do |filepath|
32
+ filepath = File.absolute_path(filepath)
33
+ next unless File.exists?(filepath)
34
+ $stderr.puts("Loading: #{filepath}")
35
+ load filepath
36
+ loaded << filepath
37
+ end
38
+
39
+ RRRSpec.configuration.loaded = loaded
40
+ end
41
+ end
42
+ end