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
@@ -1,170 +0,0 @@
1
- module Specjour
2
- class Dispatcher
3
- require 'dnssd'
4
- Thread.abort_on_exception = true
5
- include SocketHelper
6
-
7
- attr_reader :project_alias, :managers, :manager_threads, :hosts, :options, :drb_connection_errors, :test_paths, :rsync_port
8
- attr_accessor :worker_size, :project_path
9
-
10
- def initialize(options = {})
11
- Specjour.load_custom_hooks
12
- @options = options
13
- @project_path = options[:project_path]
14
- @test_paths = options[:test_paths]
15
- @worker_size = 0
16
- @managers = []
17
- @drb_connection_errors = Hash.new(0)
18
- @rsync_port = options[:rsync_port]
19
- @manager_threads = []
20
- end
21
-
22
- def start
23
- abort("#{project_path} doesn't exist") unless File.directory?(project_path)
24
- gather_managers
25
- rsync_daemon.start
26
- dispatch_work
27
- if dispatching_tests?
28
- printer.start
29
- else
30
- wait_on_managers
31
- end
32
- exit printer.exit_status
33
- end
34
-
35
- protected
36
-
37
- def add_manager(manager)
38
- set_up_manager(manager)
39
- managers << manager
40
- self.worker_size += manager.worker_size
41
- end
42
-
43
- def command_managers(&block)
44
- managers.each do |manager|
45
- manager_threads << Thread.new(manager, &block)
46
- end
47
- end
48
-
49
- def dispatcher_uri
50
- @dispatcher_uri ||= URI::Generic.build :scheme => "specjour", :host => local_ip, :port => printer.port
51
- end
52
-
53
- def dispatch_work
54
- puts "Workers found: #{worker_size}"
55
- managers.each do |manager|
56
- puts "#{manager.hostname} (#{manager.worker_size})"
57
- end
58
- command_managers { |m| m.dispatch rescue DRb::DRbConnError }
59
- end
60
-
61
- def dispatching_tests?
62
- worker_task == 'run_tests'
63
- end
64
-
65
- def fetch_manager(uri)
66
- manager = DRbObject.new_with_uri(uri.to_s)
67
- if !managers.include?(manager) && manager.available_for?(project_alias)
68
- add_manager(manager)
69
- end
70
- rescue DRb::DRbConnError => e
71
- drb_connection_errors[uri] += 1
72
- Specjour.logger.debug "#{e.message}: couldn't connect to manager at #{uri}"
73
- sleep(0.1) && retry if drb_connection_errors[uri] < 5
74
- end
75
-
76
- def fork_local_manager
77
- puts "No listeners found on this machine, starting one..."
78
- manager_options = {:worker_size => options[:worker_size], :registered_projects => [project_alias], :rsync_port => rsync_port}
79
- manager = Manager.start_quietly manager_options
80
- Process.detach manager.pid
81
- fetch_manager(manager.drb_uri)
82
- at_exit do
83
- unless Specjour.interrupted?
84
- Process.kill('TERM', manager.pid) rescue Errno::ESRCH
85
- end
86
- end
87
- end
88
-
89
- def gather_managers
90
- puts "Looking for listeners..."
91
- gather_remote_managers
92
- fork_local_manager if local_manager_needed?
93
- abort "No listeners found" if managers.size.zero?
94
- end
95
-
96
- def gather_remote_managers
97
- replies = []
98
- Timeout.timeout(1) do
99
- DNSSD.browse!('_druby._tcp') do |reply|
100
- replies << reply if reply.flags.add?
101
- end
102
- raise Timeout::Error
103
- end
104
- rescue Timeout::Error
105
- replies.each {|r| resolve_reply(r)}
106
- end
107
-
108
- def local_manager_needed?
109
- options[:worker_size] > 0 && no_local_managers?
110
- end
111
-
112
- def no_local_managers?
113
- managers.none? {|m| m.local_ip == local_ip}
114
- end
115
-
116
- def printer
117
- @printer ||= Printer.new
118
- end
119
-
120
- def project_alias
121
- @project_alias ||= options[:project_alias] || project_name
122
- end
123
-
124
- def project_name
125
- @project_name ||= File.basename(project_path)
126
- end
127
-
128
- def resolve_reply(reply)
129
- Timeout.timeout(1) do
130
- DNSSD.resolve!(reply.name, reply.type, reply.domain, flags=0, reply.interface) do |resolved|
131
- Specjour.logger.debug "Bonjour discovered #{resolved.target}"
132
- if resolved.text_record && resolved.text_record['version'] == Specjour::VERSION
133
- resolved_ip = ip_from_hostname(resolved.target)
134
- uri = URI::Generic.build :scheme => reply.service_name, :host => resolved_ip, :port => resolved.port
135
- fetch_manager(uri)
136
- else
137
- puts "Found #{resolved.target} but its version doesn't match v#{Specjour::VERSION}. Skipping..."
138
- end
139
- break unless resolved.flags.more_coming?
140
- end
141
- end
142
- rescue Timeout::Error
143
- end
144
-
145
- def rsync_daemon
146
- @rsync_daemon ||= RsyncDaemon.new(project_path, project_name, rsync_port)
147
- end
148
-
149
- def set_up_manager(manager)
150
- manager.project_name = project_name
151
- manager.dispatcher_uri = dispatcher_uri
152
- manager.test_paths = test_paths
153
- manager.worker_task = worker_task
154
- at_exit do
155
- begin
156
- manager.interrupted = Specjour.interrupted?
157
- rescue DRb::DRbConnError
158
- end
159
- end
160
- end
161
-
162
- def wait_on_managers
163
- manager_threads.each {|t| t.join; t.exit}
164
- end
165
-
166
- def worker_task
167
- options[:worker_task] || 'run_tests'
168
- end
169
- end
170
- end
@@ -1,174 +0,0 @@
1
- module Specjour
2
- class Manager
3
- require 'dnssd'
4
-
5
- include DRbUndumped
6
- include SocketHelper
7
- include Fork
8
-
9
- attr_accessor :test_paths, :project_name, :worker_task, :pid
10
- attr_reader :worker_size, :dispatcher_uri, :registered_projects, :loader_pid, :options, :rsync_port
11
-
12
- def self.start_quietly(options)
13
- manager = new options.merge(:quiet => true)
14
- manager.drb_uri
15
- manager.pid = Fork.fork_quietly { manager.start }
16
- manager
17
- end
18
-
19
- def initialize(options = {})
20
- @options = options
21
- @worker_size = options[:worker_size]
22
- @worker_task = options[:worker_task]
23
- @registered_projects = options[:registered_projects]
24
- @rsync_port = options[:rsync_port]
25
- Specjour.load_custom_hooks
26
- end
27
-
28
- def available_for?(project_name)
29
- registered_projects ? registered_projects.include?(project_name) : false
30
- end
31
-
32
- def dispatcher_uri=(uri)
33
- uri.host = ip_from_hostname(uri.host)
34
- @dispatcher_uri = uri
35
- end
36
-
37
- def dispatch
38
- suspend_bonjour do
39
- sync
40
- with_clean_env do
41
- execute_before_fork
42
- dispatch_loader
43
- end
44
- end
45
- end
46
-
47
- def drb_start
48
- $PROGRAM_NAME = "specjour listen" if quiet?
49
- DRb.start_service drb_uri.to_s, self
50
- at_exit { DRb.stop_service }
51
- end
52
-
53
- def drb_uri
54
- @drb_uri ||= begin
55
- current_uri.scheme = "druby"
56
- current_uri
57
- end
58
- end
59
-
60
- def dispatch_loader
61
- @loader_pid = fork do
62
- exec_cmd = "load --printer-uri #{dispatcher_uri} --workers #{worker_size} --task #{worker_task} --project-path #{project_path}"
63
- exec_cmd << " --test-paths #{test_paths.join(" ")}" if test_paths.any?
64
- exec_cmd << " --log" if Specjour.log?
65
- exec_cmd << " --quiet" if quiet?
66
- specjour_path = $LOAD_PATH.detect {|l| l =~ %r(specjour[^/]*/lib$)}
67
- bin_path = File.expand_path(File.join(specjour_path, "../bin"))
68
- Kernel.exec({"RUBYLIB" => $LOAD_PATH.join(":")}, "#{bin_path}/specjour #{exec_cmd}")
69
- end
70
- Process.waitall
71
- ensure
72
- kill_loader_process if loader_pid
73
- end
74
-
75
- def in_project(&block)
76
- Dir.chdir(project_path, &block)
77
- end
78
-
79
- def interrupted=(bool)
80
- Specjour.interrupted = bool
81
- kill_loader_process if loader_pid
82
- end
83
-
84
- def kill_loader_process
85
- if Specjour.interrupted?
86
- Process.kill('INT', loader_pid) rescue Errno::ESRCH
87
- else
88
- Process.kill('TERM', loader_pid) rescue Errno::ESRCH
89
- end
90
- @loader_pid = nil
91
- end
92
-
93
- def pid
94
- @pid || Process.pid
95
- end
96
-
97
- def project_path
98
- File.expand_path(project_name, File.realpath('/tmp'))
99
- end
100
-
101
- def start
102
- drb_start
103
- bonjour_announce
104
- at_exit { stop_bonjour }
105
- DRb.thread.join
106
- end
107
-
108
- def quiet?
109
- options.has_key? :quiet
110
- end
111
-
112
- def sync
113
- cmd "rsync #{Specjour::Configuration.rsync_options} --port=#{rsync_port} #{dispatcher_uri.host}::#{project_name} #{project_path}"
114
- end
115
-
116
- protected
117
-
118
- def bonjour_announce
119
- projects = registered_projects.join(", ")
120
- puts "Workers ready: #{worker_size}"
121
- puts "Listening for #{projects}"
122
- unless quiet?
123
- text = DNSSD::TextRecord.new
124
- text['version'] = Specjour::VERSION
125
- bonjour_service.register "specjour_manager_#{projects}_#{Process.pid}", "_#{drb_uri.scheme}._tcp", domain=nil, drb_uri.port, host=nil, text
126
- end
127
- end
128
-
129
- def bonjour_service
130
- @bonjour_service ||= DNSSD::Service.new
131
- end
132
-
133
- def cmd(command)
134
- Specjour.benchmark(command) do
135
- system *command.split
136
- end
137
- end
138
-
139
- def execute_before_fork
140
- Specjour.benchmark("before_fork") do
141
- in_project do
142
- Specjour.load_custom_hooks
143
- Configuration.before_fork.call
144
- end
145
- end
146
- end
147
-
148
- def stop_bonjour
149
- bonjour_service.stop if bonjour_service && !bonjour_service.stopped?
150
- @bonjour_service = nil
151
- end
152
-
153
- def suspend_bonjour(&block)
154
- stop_bonjour
155
- block.call
156
- bonjour_announce
157
- end
158
-
159
- def with_clean_env
160
- if defined?(Bundler)
161
- Bundler.with_clean_env do
162
- if ENV['RUBYOPT']
163
- opts = ENV['RUBYOPT'].split(" ").delete_if {|opt| opt =~ /bundler/}
164
- ENV['RUBYOPT'] = opts.join(" ")
165
- end
166
- yield
167
- end
168
- else
169
- yield
170
- end
171
- end
172
-
173
- end
174
- end
@@ -1,50 +0,0 @@
1
- module Specjour::RSpec
2
- class DistributedFormatter < Specjour::Configuration.rspec_formatter.call
3
-
4
- def metadata_for_examples
5
- examples.map do |example|
6
- metadata = example.metadata
7
- {
8
- :execution_result => marshalable_execution_result(example.execution_result),
9
- :description => metadata[:description],
10
- :file_path => metadata[:file_path],
11
- :full_description => metadata[:full_description],
12
- :line_number => metadata[:line_number],
13
- :location => metadata[:location]
14
- }
15
- end
16
- end
17
-
18
- def noop(*args)
19
- end
20
- alias dump_pending noop
21
- alias dump_failures noop
22
- alias start_dump noop
23
- alias message noop
24
-
25
- def color_enabled?
26
- true
27
- end
28
-
29
- def dump_summary(*args)
30
- output.send_message :rspec_summary=, metadata_for_examples
31
- end
32
-
33
- def close
34
- examples.clear
35
- super
36
- end
37
-
38
- protected
39
-
40
- def marshalable_execution_result(execution_result)
41
- if exception = execution_result[:exception]
42
- execution_result[:exception] = MarshalableException.new(exception)
43
- end
44
- execution_result[:started_at] = Time.at(execution_result[:started_at])
45
- execution_result[:finished_at] = Time.at(execution_result[:finished_at])
46
- execution_result
47
- end
48
-
49
- end
50
- end
@@ -1,73 +0,0 @@
1
- module Specjour::RSpec
2
- class FinalReport
3
- attr_reader :examples
4
- attr_reader :duration
5
-
6
- def initialize
7
- @examples = []
8
- @duration = 0.0
9
- ::RSpec.configuration.color_enabled = true
10
- ::RSpec.configuration.output_stream = $stdout
11
- end
12
-
13
- def add(data)
14
- if data.respond_to?(:has_key?) && data.has_key?(:duration)
15
- self.duration = data[:duration]
16
- else
17
- metadata_for_examples(data)
18
- end
19
- end
20
-
21
- def duration=(value)
22
- @duration = value.to_f if duration < value.to_f
23
- end
24
-
25
- def exit_status
26
- formatter.failed_examples.empty?
27
- end
28
-
29
- def metadata_for_examples(metadata_collection)
30
- examples.concat(
31
- metadata_collection.map do |partial_metadata|
32
- example = ::RSpec::Core::Example.allocate
33
- example.instance_variable_set(:@example_group_class,
34
- OpenStruct.new(:metadata => {}, :ancestors => [], :parent_groups => [])
35
- )
36
- metadata = ::RSpec::Core::Metadata.new
37
- metadata.merge! partial_metadata
38
- example.instance_variable_set(:@metadata, metadata)
39
- example
40
- end
41
- )
42
- end
43
-
44
- def pending_examples
45
- examples.select {|e| e.execution_result[:status] == 'pending'}
46
- end
47
-
48
- def failed_examples
49
- examples.select {|e| e.execution_result[:status] == 'failed'}
50
- end
51
-
52
- def formatter
53
- @formatter ||= new_progress_formatter
54
- end
55
-
56
- def summarize
57
- if examples.size > 0
58
- formatter.start_dump
59
- formatter.dump_pending
60
- formatter.dump_failures
61
- formatter.dump_summary(duration, examples.size, failed_examples.size, pending_examples.size)
62
- end
63
- end
64
-
65
- protected
66
- def new_progress_formatter
67
- new_formatter = ::RSpec::Core::Formatters::ProgressFormatter.new($stdout)
68
- new_formatter.instance_variable_set(:@failed_examples, failed_examples)
69
- new_formatter.instance_variable_set(:@pending_examples, pending_examples)
70
- new_formatter
71
- end
72
- end
73
- end
@@ -1,19 +0,0 @@
1
- module Specjour::RSpec
2
- class MarshalableException
3
- attr_accessor :message, :backtrace, :class_name
4
-
5
- def initialize(exception)
6
- self.class_name = exception.class.name
7
- self.message = exception.message
8
- self.backtrace = exception.backtrace
9
- end
10
-
11
- def class
12
- @class ||= OpenStruct.new :name => class_name
13
- end
14
-
15
- def pending_fixed?
16
- false
17
- end
18
- end
19
- end
@@ -1,15 +0,0 @@
1
- class Specjour::RSpec::Preloader
2
- def self.load(paths=[])
3
- Specjour.benchmark("Loading RSpec environment") do
4
- require File.expand_path('spec/spec_helper', Dir.pwd)
5
- load_spec_files paths
6
- end
7
- end
8
-
9
- def self.load_spec_files(paths)
10
- options = ::RSpec::Core::ConfigurationOptions.new(paths)
11
- options.parse_options
12
- options.configure ::RSpec.configuration
13
- ::RSpec.configuration.load_spec_files
14
- end
15
- end
@@ -1,14 +0,0 @@
1
- module Specjour::RSpec::Runner
2
- ::RSpec.configuration.backtrace_clean_patterns << %r(lib/specjour/)
3
-
4
- def self.run(spec, output)
5
- args = ['--format=Specjour::RSpec::DistributedFormatter', spec]
6
- ::RSpec::Core::Runner.run args, $stderr, output
7
- ensure
8
- ::RSpec.configuration.filter_manager = ::RSpec::Core::FilterManager.new
9
- ::RSpec.world.filtered_examples.clear
10
- ::RSpec.world.inclusion_filter.clear
11
- ::RSpec.world.exclusion_filter.clear
12
- ::RSpec.world.send(:instance_variable_set, :@line_numbers, nil)
13
- end
14
- end
@@ -1,9 +0,0 @@
1
- RSpec::Core::SharedExampleGroup.class_eval do
2
-
3
- def ensure_shared_example_group_name_not_taken(name)
4
- if RSpec.world.shared_example_groups.has_key?(name)
5
- Specjour.logger.debug "Shared example group '#{name}' already exists"
6
- end
7
- end
8
-
9
- end
@@ -1,17 +0,0 @@
1
- module Specjour
2
- module RSpec
3
- require 'rspec/core'
4
- require 'rspec/core/formatters/progress_formatter'
5
-
6
- require 'specjour/rspec/marshalable_exception'
7
- require 'specjour/rspec/preloader'
8
- require 'specjour/rspec/distributed_formatter'
9
- require 'specjour/rspec/final_report'
10
- require 'specjour/rspec/runner'
11
- require 'specjour/rspec/shared_example_group_ext'
12
-
13
- ::RSpec::Core::Runner.disable_autorun!
14
- ::RSpec::Core::Runner.class_eval "def self.trap_interrupt;end"
15
- ::RSpec.class_eval "def self.reset; world.reset; configuration.reset; end"
16
- end
17
- end