specjour 0.7.0 → 2.0.0.rc1

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/History.markdown +12 -0
  3. data/README.markdown +24 -1
  4. data/Rakefile +12 -12
  5. data/bin/specjour +3 -1
  6. data/lib/specjour/cli.rb +86 -110
  7. data/lib/specjour/colors.rb +23 -0
  8. data/lib/specjour/configuration.rb +47 -91
  9. data/lib/specjour/connection.rb +69 -20
  10. data/lib/specjour/cpu.rb +4 -0
  11. data/lib/specjour/fork.rb +1 -1
  12. data/lib/specjour/formatter.rb +153 -0
  13. data/lib/specjour/listener.rb +181 -0
  14. data/lib/specjour/loader.rb +55 -119
  15. data/lib/specjour/logger.rb +34 -0
  16. data/lib/specjour/plugin/base.rb +61 -0
  17. data/lib/specjour/plugin/manager.rb +28 -0
  18. data/lib/specjour/plugin/rails.rb +47 -0
  19. data/lib/specjour/plugin/rails_v3.rb +23 -0
  20. data/lib/specjour/plugin/rails_v4.rb +25 -0
  21. data/lib/specjour/plugin/rspec.rb +160 -0
  22. data/lib/specjour/plugin/rspec_v2.rb +53 -0
  23. data/lib/specjour/plugin/rspec_v3.rb +59 -0
  24. data/lib/specjour/plugin/ssh.rb +24 -0
  25. data/lib/specjour/plugin.rb +4 -0
  26. data/lib/specjour/printer.rb +235 -67
  27. data/lib/specjour/protocol.rb +13 -6
  28. data/lib/specjour/rspec_formatter.rb +17 -0
  29. data/lib/specjour/rsync_daemon.rb +6 -3
  30. data/lib/specjour/socket_helper.rb +26 -10
  31. data/lib/specjour/worker.rb +36 -62
  32. data/lib/specjour.rb +50 -24
  33. data/lib/specjour_plugin.rb +5 -0
  34. metadata +52 -84
  35. data/lib/specjour/cucumber/distributed_formatter.rb +0 -82
  36. data/lib/specjour/cucumber/final_report.rb +0 -83
  37. data/lib/specjour/cucumber/preloader.rb +0 -22
  38. data/lib/specjour/cucumber/runner.rb +0 -15
  39. data/lib/specjour/cucumber.rb +0 -16
  40. data/lib/specjour/db_scrub.rb +0 -56
  41. data/lib/specjour/dispatcher.rb +0 -170
  42. data/lib/specjour/manager.rb +0 -174
  43. data/lib/specjour/rspec/distributed_formatter.rb +0 -50
  44. data/lib/specjour/rspec/final_report.rb +0 -73
  45. data/lib/specjour/rspec/marshalable_exception.rb +0 -19
  46. data/lib/specjour/rspec/preloader.rb +0 -15
  47. data/lib/specjour/rspec/runner.rb +0 -14
  48. data/lib/specjour/rspec/shared_example_group_ext.rb +0 -9
  49. data/lib/specjour/rspec.rb +0 -17
@@ -0,0 +1,153 @@
1
+ module Specjour
2
+ class Formatter
3
+ require 'json'
4
+ include Colors
5
+ # description, status [pending,failed,passed] file_path, line_number, exception => [class, message, backtrace]
6
+
7
+ STATUSES = Hash.new({char: "?", color: :white}).merge!(
8
+ "passed" => {char: ".", color: :green},
9
+ "failed" => {char: "F", color: :red},
10
+ "error" => {char: "E", color: :magenta},
11
+ "pending" => {char: "P", color: :yellow},
12
+ "other" => {char: "O", color: :white}
13
+ )
14
+
15
+ attr_accessor \
16
+ :error_count,
17
+ :fail_count,
18
+ :failures,
19
+ :output,
20
+ :pass_count,
21
+ :pending_count,
22
+ :start_time,
23
+ :end_time,
24
+ :tests
25
+
26
+ def initialize(output=$stdout)
27
+ @output = output
28
+ @tests = []
29
+ @failures = []
30
+ @pass_count, @pending_count, @fail_count, @error_count = 0, 0, 0, 0
31
+ @start_time = Time.now
32
+ end
33
+
34
+ def print(test)
35
+ if test[:status] == "failed"
36
+ @output.puts
37
+ print_failure(test, fail_count)
38
+ else
39
+ status_format = STATUSES[test[:status]]
40
+ @output.print colorize(status_format[:char], status_format[:color])
41
+ end
42
+ end
43
+
44
+ def print_failures
45
+ @output.puts "Failures:\n\n"
46
+ failures.each_with_index do |test, index|
47
+ print_failure(test, index)
48
+ end
49
+ end
50
+
51
+ def print_failure(test, index)
52
+ exception = test[:exception]
53
+ num = index + 1
54
+ indent = num.to_s.size + 2
55
+ worker = "#{test[:hostname]}[#{test[:worker_number]}]"
56
+ @output.puts colorize("#{num}. #{worker}", :red)
57
+ description = colorize("#{test[:description]}", :red)
58
+ @output.print " " * indent
59
+ @output.puts description
60
+ message = colorize("#{exception[:class]}: #{exception[:message]}", :red)
61
+ @output.print " " * indent
62
+ @output.puts message
63
+ @output.puts format_backtrace(exception[:backtrace])
64
+ @output.puts
65
+ end
66
+
67
+ def format_backtrace(backtrace)
68
+ backtrace = Array(backtrace)
69
+ if Specjour.configuration.full_backtrace
70
+ backtrace
71
+ else
72
+ backtrace.reject {|l| Specjour.configuration.backtrace_exclusion_pattern =~ l}
73
+ end
74
+ end
75
+
76
+ def failing_test_paths
77
+ failures.map do |f|
78
+ "#{f[:file_path]}:#{f[:line_number]}"
79
+ end.uniq
80
+ end
81
+
82
+
83
+ def print_counts
84
+ @output.puts colorize("Pending: #{pending_count}", :yellow)
85
+ @output.puts colorize("Errors: #{error_count}", :magenta)
86
+ @output.puts colorize("Passed: #{pass_count}", :green)
87
+ @output.puts colorize("Failed: #{fail_count}", :red)
88
+ end
89
+
90
+ def execution_time
91
+ Time.new(2000,1,1,0,0,0,0) + (end_time - start_time)
92
+ end
93
+
94
+ def print_overview
95
+ overall_color = fail_count == 0 ? :green : :red
96
+ @output.puts colorize("\nRan: #{tests.size} tests in #{execution_time.strftime("%Mm:%Ss:%Lms")}", overall_color)
97
+ end
98
+
99
+ def print_summary
100
+ @output.puts "\n\n"
101
+ print_failures if failures.any?
102
+ print_counts
103
+ print_overview
104
+ @output.puts "\n"
105
+ end
106
+
107
+ def report_test(test)
108
+ print(test)
109
+ tests << test
110
+ case test[:status]
111
+ when "passed"
112
+ @pass_count += 1
113
+ when "failed"
114
+ @fail_count += 1
115
+ failures << test
116
+ when "error"
117
+ @error_count += 1
118
+ when "pending"
119
+ @pending_count += 1
120
+ end
121
+ end
122
+
123
+ def set_end_time!
124
+ @end_time = Time.now
125
+ end
126
+
127
+ def exit_status
128
+ failures.any? ? 1 : 0
129
+ end
130
+
131
+ end
132
+ end
133
+ __END__
134
+ Pending:
135
+ Specjour pends as an example
136
+ # No reason given
137
+ # ./spec/specjour_spec.rb:25
138
+
139
+ Failures:
140
+
141
+ 1) Specjour fails as an example
142
+ Failure/Error: raise CustomException, 'fails'
143
+ CustomException:
144
+ fails
145
+ # ./spec/specjour_spec.rb:7:in `boo'
146
+ # ./spec/specjour_spec.rb:30:in `block (2 levels) in <top (required)>'
147
+
148
+ Finished in 0.00297 seconds
149
+ 14 examples, 1 failure, 1 pending
150
+
151
+ Failed examples:
152
+
153
+ rspec ./spec/specjour_spec.rb:29 # Specjour fails as an example
@@ -0,0 +1,181 @@
1
+ module Specjour
2
+ class Listener
3
+ require 'dnssd'
4
+ Thread.abort_on_exception = true
5
+
6
+ LOCK_FILE = "listener.lock"
7
+
8
+ include Logger
9
+ include SocketHelper
10
+
11
+ attr_accessor :options, :printer
12
+
13
+ def self.ensure_started
14
+ listener = new
15
+ unless listener.started?
16
+ listener_pid = fork do
17
+ Specjour.plugin_manager.send_task(:remove_connection)
18
+ listener.daemonize
19
+ listener.start
20
+ end
21
+ Process.detach(listener_pid)
22
+ end
23
+ listener
24
+ end
25
+
26
+ def initialize(options={})
27
+ self.options = options
28
+ end
29
+
30
+ def available_for?(project_name)
31
+ if Specjour.configuration.project_aliases.any? || !project_name.empty?
32
+ Specjour.configuration.project_aliases.include? project_name
33
+ else
34
+ true
35
+ end
36
+ end
37
+
38
+ def config_directory
39
+ return @config_directory if @config_directory
40
+ @config_directory = File.join(Dir.tmpdir, "specjour")
41
+ FileUtils.mkdir_p @config_directory
42
+ @config_directory
43
+ end
44
+
45
+ def daemonize
46
+ Process.daemon
47
+ end
48
+
49
+ def add_printer(params)
50
+ log "Listener adding printer #{params}"
51
+ self.printer = params
52
+ Specjour.configuration.printer_uri = params[:uri]
53
+ Specjour.configuration.remote_job = remote_ip?(params[:ip])
54
+ end
55
+
56
+ def remove_printer
57
+ self.printer = nil
58
+ Specjour.configuration.printer_uri = nil
59
+ Specjour.configuration.remote_job = nil
60
+ end
61
+
62
+ def fork_loader
63
+ Specjour.plugin_manager.send_task(:before_loader_fork)
64
+ fork do
65
+ Specjour.plugin_manager.send_task(:remove_connection)
66
+ loader = Loader.new({task: "run_tests"})
67
+ Specjour.plugin_manager.send_task(:after_loader_fork)
68
+ loader.start
69
+ end
70
+ end
71
+
72
+ def gather
73
+ @dnssd_service = DNSSD.browse!('_specjour._tcp') do |reply|
74
+ log ['reply', reply.name, reply.service_name, reply.domain,reply.flags, reply.interface]
75
+ if reply.flags.add?
76
+ DNSSD.resolve!(reply.name, reply.type, reply.domain, flags=0, reply.interface) do |resolved|
77
+ log "Bonjour discovered #{resolved.target} #{resolved.text_record.inspect}"
78
+ if resolved.text_record && resolved.text_record['version'] == Specjour::VERSION
79
+ if available_for?(resolved.text_record['project_alias'].to_s)
80
+ resolved_ip = ip_from_hostname(resolved.target)
81
+ uri = URI::Generic.build :host => resolved_ip, :port => resolved.port
82
+ add_printer(name: resolved.name, uri: uri, ip: resolved_ip)
83
+ else
84
+ $stderr.puts "Found #{resolved.target} but not listening to project alias: #{resolved.text_record['project_alias']}. Skipping..."
85
+ end
86
+ else
87
+ $stderr.puts "Found #{resolved.target} but its version doesn't match v#{Specjour::VERSION}. Skipping..."
88
+ end
89
+ break
90
+ end
91
+ break if printer
92
+ else
93
+ log "REMOVING #{reply.name} #{reply}"
94
+ remove_printer
95
+ end
96
+ end
97
+ end
98
+
99
+ def pid
100
+ if File.exist?(pid_file) && !File.directory?(pid_file)
101
+ File.read(pid_file).strip.to_i
102
+ end
103
+ end
104
+
105
+ def pid_file
106
+ @pid_file ||= File.join(config_directory, "#{Specjour.configuration.project_aliases.join("-")}.pid")
107
+ end
108
+
109
+ def lock_file
110
+ @lock_file ||= File.join(config_directory, LOCK_FILE)
111
+ end
112
+
113
+ # only allow one listener to execute at a time
114
+ def listener_lock(&block)
115
+ exception = nil
116
+ File.open(lock_file, "w") do |file|
117
+ begin
118
+ debug "Acquiring listener lock"
119
+ file.flock File::LOCK_EX
120
+ debug "Listener lock acquired"
121
+ block.call
122
+ rescue => exception
123
+ file.flock File::LOCK_UN
124
+ end
125
+ end
126
+ raise exception if exception
127
+ end
128
+
129
+ def program_name
130
+ name = "specjour listen"
131
+ if Specjour.configuration.project_aliases.any?
132
+ name += " -a #{Specjour.configuration.project_aliases.join(",")}"
133
+ end
134
+ name
135
+ end
136
+
137
+ def start
138
+ $PROGRAM_NAME = program_name
139
+ log "Listener starting"
140
+ write_pid
141
+ loop do
142
+ log "listening..."
143
+ gather
144
+ listener_lock do
145
+ @loader_pid = fork_loader
146
+ Process.waitall
147
+ remove_printer
148
+ sleep 3 # let bonjour services stop
149
+ end
150
+ end
151
+ rescue StandardError, ScriptError => e
152
+ $stderr.puts "RESCUED #{e.message}"
153
+ $stderr.puts e.backtrace
154
+ connection.error(e)
155
+ ensure
156
+ remove_pid
157
+ remove_connection
158
+ log "Shutting down listener"
159
+ end
160
+
161
+ def started?
162
+ !pid.nil?
163
+ end
164
+
165
+ def stop
166
+ Process.kill("TERM", pid) rescue TypeError
167
+ ensure
168
+ remove_pid
169
+ end
170
+
171
+ def write_pid
172
+ File.open(pid_file, 'w') do |f|
173
+ f.write Process.pid
174
+ end
175
+ end
176
+
177
+ def remove_pid
178
+ File.unlink(pid_file) if File.exist?(pid_file) && pid == Process.pid
179
+ end
180
+ end
181
+ end
@@ -1,150 +1,86 @@
1
1
  module Specjour
2
+ require 'specjour/worker'
2
3
  class Loader
3
- include Protocol
4
- include Fork
4
+ include Logger
5
+ include SocketHelper
5
6
 
6
- attr_reader :test_paths, :printer_uri, :project_path, :task, :worker_size, :worker_pids, :quiet
7
+ attr_reader \
8
+ :options,
9
+ :quiet,
10
+ :task,
11
+ :worker_pids
7
12
 
8
13
  def initialize(options = {})
9
14
  @options = options
10
- @printer_uri = options[:printer_uri]
11
- @test_paths = options[:test_paths]
12
- @worker_size = options[:worker_size]
13
15
  @task = options[:task]
14
16
  @quiet = options[:quiet]
15
- @project_path = options[:project_path]
16
17
  @worker_pids = []
17
- Dir.chdir project_path
18
- Specjour.load_custom_hooks
19
18
  end
20
19
 
21
20
  def start
22
- load_app
23
- Configuration.after_load.call
24
- (1..worker_size).each do |index|
25
- worker_pids << fork do
26
- Worker.new(
27
- :number => index,
28
- :printer_uri => printer_uri,
29
- :quiet => quiet
30
- ).send(task)
31
- end
32
- end
33
- Process.waitall
21
+ Process.setsid
22
+ $PROGRAM_NAME = "specjour loader"
23
+ set_up
24
+ sync
25
+ Specjour.plugin_manager.send_task(:load_application)
26
+ Specjour.plugin_manager.send_task(:register_tests_with_printer)
27
+ fork_workers
28
+ wait_srv
29
+ rescue StandardError, ScriptError => e
30
+ $stderr.puts "RESCUED #{e.class} '#{e.message}'"
31
+ $stderr.puts e.backtrace
32
+ $stderr.puts "\n\n"
33
+ connection.error(e)
34
34
  ensure
35
- kill_worker_processes
36
- end
37
-
38
- def spec_files
39
- @spec_files ||= file_collector(spec_paths) do |path|
40
- if path == project_path
41
- Dir["spec/**/*_spec.rb"]
42
- else
43
- Dir["**/*_spec.rb"]
44
- end
45
- end
35
+ remove_connection
36
+ log "Loader killing group #{Process.getsid}"
46
37
  end
47
38
 
48
- def feature_files
49
- @feature_files ||= file_collector(feature_paths) do |path|
50
- if path == project_path
51
- Dir["features/**/*.feature"]
52
- else
53
- Dir["**/*.feature"]
39
+ def fork_workers
40
+ Specjour.plugin_manager.send_task(:before_worker_fork)
41
+ (1..Specjour.configuration.worker_size).each do |index|
42
+ worker_pids << fork do
43
+ remove_connection
44
+ Specjour.plugin_manager.send_task(:remove_connection)
45
+ $PROGRAM_NAME = "specjour worker"
46
+ worker = Worker.new(
47
+ :number => index,
48
+ :quiet => quiet
49
+ )
50
+ Specjour.plugin_manager.send_task(:after_worker_fork)
51
+ worker.send(task)
54
52
  end
55
53
  end
56
54
  end
57
55
 
58
- protected
59
-
60
- def spec_paths
61
- @spec_paths ||= test_paths.select {|p| p =~ /spec.*$/}
62
- end
63
-
64
- def feature_paths
65
- @feature_paths ||= test_paths.select {|p| p =~ /features.*$/}
66
- end
67
-
68
- def file_collector(paths, &globber)
69
- if spec_paths.empty? && feature_paths.empty?
70
- globber[project_path]
71
- else
72
- paths.map do |path|
73
- path = File.expand_path(path, project_path)
74
- if File.directory?(path)
75
- globber[path]
76
- else
77
- path
78
- end
79
- end.flatten.uniq
80
- end
81
- end
82
-
83
- def load_app
84
- RSpec::Preloader.load spec_files if spec_files.any?
85
- Cucumber::Preloader.load(feature_files, connection) if feature_files.any?
86
- register_tests_with_printer
87
- end
88
-
89
- def register_tests_with_printer
90
- tests = rspec_examples | cucumber_scenarios
91
- connection.send_message :tests=, tests
92
- end
93
-
94
- def rspec_examples
95
- if spec_files.any?
96
- filtered_examples
97
- else
98
- []
99
- end
100
- end
101
-
102
- def filtered_examples
103
- examples = ::RSpec.world.example_groups.map do |g|
104
- g.descendant_filtered_examples
105
- end.flatten
106
- locations = examples.map do |e|
107
- meta = e.metadata
108
- groups = e.example_group.parent_groups + [e.example_group]
109
- shared_group = groups.detect do |group|
110
- group.metadata[:shared_group_name]
111
- end
112
- if shared_group
113
- meta = shared_group.metadata[:example_group]
56
+ def wait_srv
57
+ select [connection.socket]
58
+ if !connection.socket.eof?
59
+ signal = connection.get_server_done
60
+ case signal
61
+ when "INT"
62
+ debug "Sending INT to -#{Process.getsid}"
63
+ Process.kill("INT", -Process.getsid)
114
64
  end
115
- meta[:location]
116
- end
117
- ensure
118
- ::RSpec.reset
119
- end
120
-
121
- def cucumber_scenarios
122
- if feature_files.any?
123
- scenarios
124
- else
125
- []
126
65
  end
127
66
  end
128
67
 
129
- def scenarios
130
- Cucumber.runtime.send(:features).map do |feature|
131
- feature.feature_elements.map do |scenario|
132
- "#{feature.file}:#{scenario.instance_variable_get(:@line)}"
133
- end
134
- end.flatten
68
+ def set_up
69
+ data = connection.ready({hostname: hostname, worker_size: Specjour.configuration.worker_size})
70
+ Specjour.configuration.project_name = data[:project_name]
71
+ Specjour.configuration.test_paths = data[:test_paths]
72
+ Specjour.configuration.project_path = File.expand_path(Specjour.configuration.project_name, Specjour.configuration.tmp_path)
135
73
  end
136
74
 
137
- def kill_worker_processes
138
- signal = Specjour.interrupted? ? 'INT' : 'TERM'
139
- Process.kill(signal, *worker_pids) rescue Errno::ESRCH
75
+ def sync
76
+ cmd "rsync #{Specjour.configuration.rsync_options} --port=#{Specjour.configuration.rsync_port} #{connection.host}::#{Specjour.configuration.project_name} #{Specjour.configuration.project_path}"
77
+ Dir.chdir Specjour.configuration.project_path
140
78
  end
141
79
 
142
- def connection
143
- @connection ||= begin
144
- at_exit { connection.disconnect }
145
- Connection.new URI.parse(printer_uri)
80
+ def cmd(command)
81
+ Specjour.benchmark(command) do
82
+ system *command.split
146
83
  end
147
84
  end
148
-
149
85
  end
150
86
  end
@@ -0,0 +1,34 @@
1
+ module Specjour
2
+ module Logger
3
+
4
+ def log(msg)
5
+ Specjour.logger.info(self.class.name) { format(msg) }
6
+ end
7
+
8
+ def debug(msg)
9
+ Specjour.logger.debug(self.class.name) { format("#{msg}\n\t#{called_from}") }
10
+ end
11
+
12
+ private
13
+
14
+ def format(msg)
15
+ prefix = Specjour.configuration.worker_number > 0 ? "[#{Specjour.configuration.worker_number}] " : ""
16
+ "#{prefix}#{msg}"
17
+ end
18
+
19
+ def called_from
20
+ caller.detect {|s| s =~ /specjour\/lib\/specjour\/(?!logger\.rb)/}
21
+ end
22
+
23
+ # def self.extended(base)
24
+ # base.instance_methods.each do |instance_method|
25
+ # define_method instance_method do |*args|
26
+ # log __method__
27
+ # val = super *args
28
+ # log val
29
+ # val
30
+ # end
31
+ # end
32
+ # end
33
+ end
34
+ end
@@ -0,0 +1,61 @@
1
+ module Specjour
2
+ module Plugin
3
+ class Base
4
+ include SocketHelper
5
+ include Specjour::Logger
6
+
7
+ attr_reader :listener, :loader, :worker
8
+
9
+ def after_register
10
+ end
11
+
12
+ def before_suite
13
+ end
14
+
15
+ def after_suite
16
+ end
17
+
18
+ def before_loader_fork
19
+ end
20
+
21
+ def load_application
22
+ end
23
+
24
+ def load_test_suite
25
+ end
26
+
27
+ def after_loader_fork
28
+ end
29
+
30
+ def before_worker_fork
31
+ end
32
+
33
+ def after_worker_fork
34
+ end
35
+
36
+ def interrupted!
37
+ end
38
+
39
+ def register_tests_with_printer
40
+ end
41
+
42
+ def run_test(test)
43
+ false
44
+ end
45
+
46
+ def tests_to_register
47
+ []
48
+ end
49
+
50
+ def before_print_summary(formatter)
51
+ end
52
+
53
+ def after_print_summary(formatter)
54
+ end
55
+
56
+ def exit_status(formatter)
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,28 @@
1
+ module Specjour::Plugin
2
+ class Manager
3
+ include Specjour::Logger
4
+ attr_reader :plugins
5
+
6
+ def initialize
7
+ @plugins = []
8
+ end
9
+
10
+ def register_plugin(plugin, position=-1)
11
+ if !plugins.include?(plugin)
12
+ plugins.insert(position, plugin)
13
+ end
14
+ end
15
+
16
+ def clear_plugins
17
+ plugins.clear
18
+ end
19
+
20
+ def send_task(task, *args)
21
+ plugins.map do |plugin|
22
+ log "sending task to plugin: #{task}, #{plugin}"
23
+ plugin.__send__(task, *args)
24
+ # break if plugin.__send__(task, *args) == false
25
+ end
26
+ end
27
+ end
28
+ end