zspec 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ require "rspec/core/formatters/base_formatter"
2
+
3
+ module ZSpec
4
+ class Formatter < ::RSpec::Core::Formatters::BaseFormatter
5
+ def initialize(queue:, tracker:, stdout:, message:)
6
+ super
7
+ @output_hash = { failures: [] }
8
+ @failed = false
9
+ @errors_outside_of_examples = false
10
+ @message = message
11
+ @queue = queue
12
+ @tracker = tracker
13
+ @stdout = stdout
14
+ end
15
+
16
+ def example_failed(failure)
17
+ @failed = true
18
+ @output_hash[:failures] << format_example(failure.example)
19
+ end
20
+
21
+ def dump_summary(summary)
22
+ @duration = summary.duration
23
+ # only set to true if there is a failure, otherwise it will override the failures from example_failed
24
+ if summary.errors_outside_of_examples_count.to_i > 0
25
+ @failed = true
26
+ @errors_outside_of_examples = true
27
+ end
28
+ @output_hash[:summary] = format_summary(summary)
29
+ end
30
+
31
+ def close(_notification)
32
+ @queue.resolve(
33
+ @failed,
34
+ @message,
35
+ @output_hash.to_json,
36
+ @stdout.string
37
+ )
38
+ @tracker.track_runtime(@message, @duration)
39
+ @tracker.track_failures(@output_hash[:failures]) if @failed
40
+ @tracker.track_sequence(@message)
41
+ raise if @errors_outside_of_examples
42
+ end
43
+
44
+ private
45
+
46
+ def format_summary(summary)
47
+ {
48
+ duration: summary.duration,
49
+ load_time: summary.load_time,
50
+ file_path: @message,
51
+ example_count: summary.example_count,
52
+ failure_count: summary.failure_count,
53
+ pending_count: summary.pending_count,
54
+ errors_outside_of_examples_count: summary.errors_outside_of_examples_count
55
+ }
56
+ end
57
+
58
+ def format_example(example)
59
+ hash = {
60
+ id: example.id,
61
+ description: example.description,
62
+ full_description: example.full_description,
63
+ status: example.execution_result.status.to_s,
64
+ run_time: example.execution_result.run_time
65
+ }
66
+ e = example.exception
67
+ if e
68
+ hash[:exception] = {
69
+ class: e.class.name,
70
+ message: e.message,
71
+ backtrace: e.backtrace
72
+ }
73
+ end
74
+ hash
75
+ end
76
+
77
+ ::RSpec::Core::Formatters.register self,
78
+ :close, :dump_summary, :example_failed
79
+ end
80
+ end
@@ -0,0 +1,172 @@
1
+ module ZSpec
2
+ class Presenter
3
+ def initialize(queue:, tracker:, display_count:, truncate_length:, out: $stdout)
4
+ ::RSpec.configuration.tty = true
5
+ ::RSpec.configuration.color = true
6
+
7
+ @queue = queue
8
+ @tracker = tracker
9
+ @display_count = display_count
10
+ @truncate_length = truncate_length
11
+ @out = out
12
+
13
+ @failures = []
14
+ @errors_outside_of_examples = []
15
+ @runtimes = []
16
+
17
+ @example_count = 0
18
+ @failure_count = 0
19
+ @pending_count = 0
20
+ @errors_outside_of_examples_count = 0
21
+ end
22
+
23
+ def poll_results
24
+ @queue.done_queue.each do |results, stdout|
25
+ next if results.nil?
26
+ present(::JSON.parse(results), stdout)
27
+ end
28
+ print_summary
29
+ end
30
+
31
+ private
32
+
33
+ def present(results, stdout)
34
+ track_counts(results)
35
+ track_errors_outside_of_examples(results, stdout)
36
+ track_runtimes(results)
37
+ track_failures(results)
38
+ end
39
+
40
+ def print_summary
41
+ @out.puts ""
42
+ @out.puts "example_count: #{@example_count}"
43
+ @out.puts "failure_count: #{@failure_count}"
44
+ @out.puts "pending_count: #{@pending_count}"
45
+ @out.puts "errors_outside_of_examples_count: #{@errors_outside_of_examples_count}"
46
+
47
+ print_slow_specs
48
+ print_current_flaky_specs
49
+ print_alltime_flaky_specs
50
+ print_failed_specs
51
+ print_outside_of_examples
52
+
53
+ @out.flush
54
+ @failures.any? || @errors_outside_of_examples.any?
55
+ end
56
+
57
+ def print_outside_of_examples
58
+ if @errors_outside_of_examples.any?
59
+ @out.puts wrap("\nFIRST #{@display_count} ERRORS OUTSIDE OF EXAMPLES:", :bold)
60
+ @errors_outside_of_examples.take(@display_count).each do |message|
61
+ @out.puts wrap(truncated(message), :failure)
62
+ end
63
+ end
64
+ end
65
+
66
+ def print_failed_specs
67
+ if @failures.any?
68
+ @out.puts wrap("\nFIRST #{@display_count} FAILURES:", :bold)
69
+ @failures.take(@display_count).each_with_index do |example, index|
70
+ @out.puts wrap("#{example['id']}\n" \
71
+ "#{example['description']} (FAILED - #{index + 1})\n" \
72
+ "Exception - #{truncated(message_or_default(example))}\n" \
73
+ "Backtrace - #{truncated(backtrace_or_default(example).join("\n"))}\n",
74
+ :failure)
75
+ end
76
+ end
77
+ end
78
+
79
+ def print_current_flaky_specs
80
+ if @tracker.current_failures.any?
81
+ @out.puts wrap("\nFIRST #{@display_count} FLAKY SPECS CURRENT RUN:", :bold)
82
+ @tracker.current_failures.take(@display_count).each do |failure|
83
+ @out.puts "#{failure['message']} failed #{failure['count']} times. "
84
+ if failure["sequence"].any?
85
+ @out.puts " PREVIOUS FILES:"
86
+ failure["sequence"].each { |spec| @out.puts " #{spec}" }
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def print_alltime_flaky_specs
93
+ if @tracker.alltime_failures.any?
94
+ @out.puts wrap("\nFIRST #{@display_count} FLAKY SPECS LAST #{humanize(@tracker.threshold).upcase}:", :bold)
95
+ @tracker.alltime_failures.take(@display_count).each do |failure|
96
+ @out.puts "#{failure['message']} failed #{failure['count']} times. " \
97
+ "last failure was #{humanize(Time.now.to_i - failure['last_failure'])} ago.\n"
98
+ end
99
+ end
100
+ end
101
+
102
+ def print_slow_specs
103
+ @out.puts wrap("\n#{@display_count} SLOWEST FILES CURRENT RUN:", :bold)
104
+ @runtimes.sort_by { |h| h[:duration] }.reverse.take(@display_count).each do |h|
105
+ @out.puts "#{h[:file_path]} finished in #{format_duration(h[:duration])} " \
106
+ "(file took #{format_duration(h[:load_time])} to load)\n"
107
+ end
108
+ end
109
+
110
+ def track_failures(results)
111
+ results["failures"].each do |example|
112
+ @failures << example
113
+ end
114
+ end
115
+
116
+ def track_counts(results)
117
+ @example_count += results["summary"]["example_count"].to_i
118
+ @failure_count += results["summary"]["failure_count"].to_i
119
+ @pending_count += results["summary"]["pending_count"].to_i
120
+ @errors_outside_of_examples_count += results["summary"]["errors_outside_of_examples_count"].to_i
121
+ end
122
+
123
+ def track_errors_outside_of_examples(results, stdout)
124
+ unless stdout.nil? || stdout.empty? || results["summary"]["errors_outside_of_examples_count"].to_i == 0
125
+ @errors_outside_of_examples << stdout
126
+ end
127
+ end
128
+
129
+ def track_runtimes(results)
130
+ @runtimes << {
131
+ file_path: results["summary"]["file_path"],
132
+ duration: results["summary"]["duration"],
133
+ load_time: results["summary"]["load_time"]
134
+ }
135
+ end
136
+
137
+ def humanize(secs)
138
+ [[60, :seconds], [60, :minutes], [24, :hours], [Float::INFINITY, :days]].map { |count, name|
139
+ if secs > 0
140
+ secs, n = secs.divmod(count)
141
+ "#{n.to_i} #{name}" unless n.to_i == 0
142
+ end
143
+ }.compact.reverse.join(" ")
144
+ end
145
+
146
+ def message_or_default(example)
147
+ example.dig("exception", "message") || ""
148
+ end
149
+
150
+ def backtrace_or_default(example)
151
+ example.dig("exception", "backtrace") || []
152
+ end
153
+
154
+ def truncated(message)
155
+ unless message.empty?
156
+ if message.length < @truncate_length
157
+ message
158
+ else
159
+ message.slice(0..@truncate_length) + "... (truncated)"
160
+ end
161
+ end
162
+ end
163
+
164
+ def format_duration(duration)
165
+ ::RSpec::Core::Formatters::Helpers.format_duration(duration)
166
+ end
167
+
168
+ def wrap(message, symbol)
169
+ ::RSpec::Core::Formatters::ConsoleCodes.wrap(message, symbol)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,138 @@
1
+ require "zspec/util"
2
+
3
+ module ZSpec
4
+ class Queue
5
+ attr_reader :counter_name, :pending_queue_name, :processing_queue_name,
6
+ :done_queue_name, :metadata_hash_name, :workers_ready_key_name
7
+
8
+ include ZSpec::Util
9
+
10
+ def initialize(sink:, build_prefix:, retries:, timeout:)
11
+ @sink = sink
12
+ @retries = retries.to_i
13
+ @timeout = timeout.to_i
14
+ @counter_name = build_prefix + ":count"
15
+ @pending_queue_name = build_prefix + ":pending"
16
+ @processing_queue_name = build_prefix + ":processing"
17
+ @done_queue_name = build_prefix + ":done"
18
+ @metadata_hash_name = build_prefix + ":metadata"
19
+ @workers_ready_key_name = build_prefix + ":ready"
20
+ end
21
+
22
+ def cleanup(expire_seconds = EXPIRE_SECONDS)
23
+ @sink.expire(@counter_name, expire_seconds)
24
+ @sink.expire(@pending_queue_name, expire_seconds)
25
+ @sink.expire(@processing_queue_name, expire_seconds)
26
+ @sink.expire(@done_queue_name, expire_seconds)
27
+ @sink.expire(@metadata_hash_name, expire_seconds)
28
+ @sink.expire(@workers_ready_key_name, expire_seconds)
29
+ end
30
+
31
+ def enqueue(messages)
32
+ messages.each do |message|
33
+ @sink.lpush(@pending_queue_name, message)
34
+ @sink.incr(@counter_name)
35
+ end
36
+ @sink.set(@workers_ready_key_name, true)
37
+ end
38
+
39
+ def done_queue
40
+ Enumerator.new do |yielder|
41
+ until workers_ready? && complete?
42
+ expire_processing
43
+
44
+ _list, message = @sink.brpop(@done_queue_name, timeout: 1)
45
+ if message.nil?
46
+ yielder << [nil, nil]
47
+ next
48
+ end
49
+
50
+ if @sink.hget(@metadata_hash_name, dedupe_key(message))
51
+ yielder << [nil, nil]
52
+ next
53
+ end
54
+
55
+ results = @sink.hget(@metadata_hash_name, results_key(message))
56
+ if results.nil?
57
+ yielder << [nil, nil]
58
+ next
59
+ end
60
+
61
+ stdout = @sink.hget(@metadata_hash_name, stdout_key(message))
62
+
63
+ @sink.hset(@metadata_hash_name, dedupe_key(message), true)
64
+ @sink.decr(@counter_name)
65
+
66
+ yielder << [results, stdout]
67
+ end
68
+ end
69
+ end
70
+
71
+ def pending_queue
72
+ Enumerator.new do |yielder|
73
+ until workers_ready? && complete?
74
+ message = @sink.brpoplpush(@pending_queue_name, @processing_queue_name, timeout: 1)
75
+ if message.nil?
76
+ yielder << nil
77
+ next
78
+ end
79
+ @sink.hset(@metadata_hash_name, timeout_key(message), @sink.time.first)
80
+ yielder << message
81
+ end
82
+ end
83
+ end
84
+
85
+ def resolve(failed, message, results, stdout)
86
+ if failed && (count = retry_count(message)) && (count < @retries)
87
+ retry_message(message, count)
88
+ else
89
+ resolve_message(message, results, stdout)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def expire_processing
96
+ processing.each do |message|
97
+ next unless expired?(message)
98
+
99
+ @sink.lrem(@processing_queue_name, 0, message)
100
+ @sink.rpush(@pending_queue_name, message)
101
+ @sink.hdel(@metadata_hash_name, timeout_key(message))
102
+ end
103
+ end
104
+
105
+ def workers_ready?
106
+ @sink.get(@workers_ready_key_name)
107
+ end
108
+
109
+ def processing
110
+ @sink.lrange(@processing_queue_name, 0, -1)
111
+ end
112
+
113
+ def complete?
114
+ @sink.get(@counter_name).to_i == 0
115
+ end
116
+
117
+ def retry_count(message)
118
+ @sink.hget(@metadata_hash_name, retry_key(message)).to_i
119
+ end
120
+
121
+ def expired?(message)
122
+ proccess_time = @sink.hget(@metadata_hash_name, timeout_key(message)).to_i
123
+ (@sink.time.first - proccess_time) > @timeout
124
+ end
125
+
126
+ def resolve_message(message, results, stdout)
127
+ @sink.hset(@metadata_hash_name, stdout_key(message), stdout)
128
+ @sink.hset(@metadata_hash_name, results_key(message), results)
129
+ @sink.lrem(@processing_queue_name, 0, message)
130
+ @sink.lpush(@done_queue_name, message)
131
+ end
132
+
133
+ def retry_message(message, count)
134
+ @sink.hdel(@metadata_hash_name, timeout_key(message))
135
+ @sink.hset(@metadata_hash_name, retry_key(message), count + 1)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,42 @@
1
+ module ZSpec
2
+ class Scheduler
3
+ def initialize(queue:, tracker:)
4
+ @queue = queue
5
+ @tracker = tracker
6
+ end
7
+
8
+ def schedule(args)
9
+ enqueue(
10
+ extract(args)
11
+ .uniq
12
+ .map(&method(:normalize))
13
+ .sort_by(&method(:runtime))
14
+ .reverse
15
+ )
16
+ end
17
+
18
+ private
19
+
20
+ def extract(args)
21
+ configuration = ::RSpec.configuration
22
+ ::RSpec::Core::ConfigurationOptions.new([args]).configure(configuration)
23
+ configuration.files_to_run
24
+ end
25
+
26
+ def runtimes
27
+ @runtimes ||= @tracker.all_runtimes
28
+ end
29
+
30
+ def runtime(example)
31
+ runtimes[example].to_i || 0
32
+ end
33
+
34
+ def enqueue(examples)
35
+ @queue.enqueue(examples)
36
+ end
37
+
38
+ def normalize(file)
39
+ file.sub("#{Dir.pwd}/", "./")
40
+ end
41
+ end
42
+ end
@@ -0,0 +1 @@
1
+ require_relative "sink/memory_sink"
@@ -0,0 +1,86 @@
1
+ module ZSpec
2
+ module Sink
3
+ class MemorySink
4
+ def initialize(state:, expirations:)
5
+ @state = state
6
+ @expirations = expirations
7
+ end
8
+
9
+ def time
10
+ [@state[:time]]
11
+ end
12
+
13
+ def expire(key, seconds)
14
+ @expirations[key] = seconds
15
+ end
16
+
17
+ def lpush(key, value)
18
+ (@state[key] ||= []).unshift(value)
19
+ end
20
+
21
+ def lrem(key, n, value)
22
+ (@state[key] ||= []).delete(value)
23
+ end
24
+
25
+ def lrange(key, start, stop)
26
+ (@state[key] ||= [])[start..stop]
27
+ end
28
+
29
+ def rpush(key, value)
30
+ (@state[key] ||= []).push(value)
31
+ end
32
+
33
+ def rpop(key)
34
+ (@state[key] ||= []).pop
35
+ end
36
+
37
+ def brpop(key, timeout: 0)
38
+ return [], rpop(key)
39
+ end
40
+
41
+ def brpoplpush(source, destination, timeout: 0)
42
+ _list, message = brpop(source)
43
+ lpush(destination, message)
44
+ message
45
+ end
46
+
47
+ def hget(key, field)
48
+ (@state[key] ||= {})[field]
49
+ end
50
+
51
+ def hgetall(key)
52
+ @state[key] ||= {}
53
+ end
54
+
55
+ def hset(key, field, value)
56
+ (@state[key] ||= {})[field] = value
57
+ end
58
+
59
+ def hdel(key, field)
60
+ (@state[key] ||= {}).delete(field)
61
+ end
62
+
63
+ def hincrby(key, field, value)
64
+ (@state[key] ||= {})[field] = (hget(key, field) || 0) + 1
65
+ end
66
+
67
+ def incr(key)
68
+ @state[key] ||= 0
69
+ @state[key] += 1
70
+ end
71
+
72
+ def decr(key)
73
+ @state[key] ||= 0
74
+ @state[key] -= 1
75
+ end
76
+
77
+ def set(key, value)
78
+ @state[key] = value
79
+ end
80
+
81
+ def get(key)
82
+ @state[key]
83
+ end
84
+ end
85
+ end
86
+ end