rspec_starter 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +9 -0
- data/.travis.yml +5 -5
- data/CHANGELOG.md +29 -18
- data/README.md +145 -115
- data/exe/rspec_starter +14 -1
- data/lib/rspec_starter.rb +69 -24
- data/lib/rspec_starter/command.rb +59 -0
- data/lib/rspec_starter/command_context.rb +38 -0
- data/lib/rspec_starter/core_ext/string.rb +6 -3
- data/lib/rspec_starter/environment.rb +57 -0
- data/lib/rspec_starter/errors/step_error.rb +9 -0
- data/lib/rspec_starter/errors/step_stopper.rb +9 -0
- data/lib/rspec_starter/help.rb +79 -32
- data/lib/rspec_starter/helpers.rb +74 -0
- data/lib/rspec_starter/{which.rb → helpers/which.rb} +0 -0
- data/lib/rspec_starter/legacy.rb +16 -0
- data/lib/rspec_starter/legacy/help.rb +40 -0
- data/lib/rspec_starter/legacy/legacy_runner.rb +90 -0
- data/lib/rspec_starter/{steps → legacy/steps}/invoke_rspec_step.rb +2 -0
- data/lib/rspec_starter/{steps → legacy/steps}/prepare_database_step.rb +3 -1
- data/lib/rspec_starter/{steps → legacy/steps}/remove_tmp_folder_step.rb +3 -0
- data/lib/rspec_starter/{steps → legacy/steps}/step.rb +0 -0
- data/lib/rspec_starter/{steps → legacy/steps}/verify_xvfb_step.rb +2 -0
- data/lib/rspec_starter/option.rb +84 -0
- data/lib/rspec_starter/options.rb +96 -0
- data/lib/rspec_starter/rspec_starter_task.rb +35 -0
- data/lib/rspec_starter/runner.rb +15 -74
- data/lib/rspec_starter/step.rb +181 -0
- data/lib/rspec_starter/step_context.rb +28 -0
- data/lib/rspec_starter/step_options.rb +20 -0
- data/lib/rspec_starter/task_context.rb +63 -0
- data/lib/rspec_starter/tasks/rebuild_rails_app_database.rb +50 -0
- data/lib/rspec_starter/tasks/remove_tmp_folder.rb +28 -0
- data/lib/rspec_starter/tasks/start_rspec.rb +68 -0
- data/lib/rspec_starter/tasks/verify_display_server.rb +43 -0
- data/lib/rspec_starter/version.rb +1 -1
- data/lib/templates/rails_engine_start_rspec +38 -0
- data/lib/templates/rails_start_rspec +38 -0
- data/lib/templates/start_rspec +20 -5
- data/rspec_starter.gemspec +4 -2
- metadata +63 -14
@@ -0,0 +1,35 @@
|
|
1
|
+
# Tasks are classes that implement an 'execute' method. Tasks can execute any ruby code they want inside the 'execute' method.
|
2
|
+
# Tasks are defined by listing their name inside the RspecStarter.start block:
|
3
|
+
#
|
4
|
+
# RspecStarter.start do
|
5
|
+
# task :verify_display_server
|
6
|
+
# task :rebuild_rails_app_database, stop_on_problem: true
|
7
|
+
# task :start_rspec, quiet: true
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# Tasks accept a `quiet` option which tells the task to be more or less verbose. Tasks accept a `stop_on_problem` method
|
11
|
+
# that determines whether a problem should cause the entire start-up process to stop when the task encounters a problem.
|
12
|
+
class RspecStarterTask < RspecStarterStep
|
13
|
+
def self.register_option(hash)
|
14
|
+
@options_registrar.register_task_option(self, hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.description
|
18
|
+
""
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Convert something like VerifyDisplayServer to :verify_display_server
|
24
|
+
def self.name_for_class(klass)
|
25
|
+
klass.name.underscore.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_starting_message
|
29
|
+
print "#{@starting_message} ..."
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize_name
|
33
|
+
@name = self.class.name_for_class(self.class)
|
34
|
+
end
|
35
|
+
end
|
data/lib/rspec_starter/runner.rb
CHANGED
@@ -1,88 +1,29 @@
|
|
1
|
-
require 'pathname'
|
2
|
-
require 'open3'
|
3
|
-
require_relative 'core_ext/string'
|
4
|
-
require_relative 'which'
|
5
|
-
require_relative 'help'
|
6
|
-
require_relative 'steps/step'
|
7
|
-
require_relative 'steps/verify_xvfb_step'
|
8
|
-
require_relative 'steps/prepare_database_step'
|
9
|
-
require_relative 'steps/remove_tmp_folder_step'
|
10
|
-
require_relative 'steps/invoke_rspec_step'
|
11
|
-
|
12
1
|
module RspecStarter
|
13
|
-
# This
|
14
|
-
#
|
15
|
-
# taken to help invoke Rspec. Steps are typically independent do not depend on information from other steps. However
|
16
|
-
# this is not a hard rule. If more complex steps are needed, feel free to create them. Each steps knows about the main
|
17
|
-
# runner object, so the runner object is a good place to store shared info.
|
2
|
+
# This class implements the main control loop that processes steps. It maintains a list of steps and executes
|
3
|
+
# each one. Steps can be skipped if command line options, or other options dictate turn off the step.
|
18
4
|
class Runner
|
19
|
-
|
20
|
-
attr_reader :xvfb_installed, :step_num, :steps
|
5
|
+
attr_reader :environment
|
21
6
|
|
22
|
-
def initialize(
|
23
|
-
@
|
24
|
-
@
|
25
|
-
@xvfb_installed = RspecStarter.which("xvfb-run")
|
26
|
-
@prep_db_step = PrepareDatabaseStep.new(defaults, self)
|
27
|
-
@run_rspec_step = InvokeRspecStep.new(defaults, self)
|
28
|
-
@steps << VerifyXvfbStep.new(defaults, self)
|
29
|
-
@steps << @prep_db_step
|
30
|
-
@steps << RemoveTmpFolderStep.new(defaults, self)
|
31
|
-
@steps << @run_rspec_step
|
7
|
+
def initialize(environment)
|
8
|
+
@environment = environment
|
9
|
+
@steps = @environment.step_contexts.collect { |step_context| step_context.instantiate(self) }
|
32
10
|
end
|
33
11
|
|
34
12
|
def run
|
35
|
-
return show_help if should_show_help? # If we show help, exit and don't do anything else.
|
36
|
-
|
37
13
|
@steps.each do |step|
|
38
|
-
next
|
39
|
-
step.execute
|
40
|
-
@step_num += 1
|
41
|
-
break if step.failed?
|
42
|
-
end
|
43
|
-
|
44
|
-
finalize_exit
|
45
|
-
end
|
14
|
+
next if step.should_skip?
|
46
15
|
|
47
|
-
|
48
|
-
|
49
|
-
end
|
50
|
-
|
51
|
-
def project_is_rails_engine?
|
52
|
-
return false unless project_has_lib_dir?
|
53
|
-
Dir["#{Dir.pwd}/lib/**/*.rb"].each do |file|
|
54
|
-
return true if File.readlines(file).detect { |line| line.match(/\s*class\s+.*<\s+::Rails::Engine/) }
|
16
|
+
print "[#{step.id}] "
|
17
|
+
step.run
|
55
18
|
end
|
56
|
-
false
|
57
|
-
end
|
58
|
-
|
59
|
-
def project_has_lib_dir?
|
60
|
-
Dir.exist?("#{Dir.pwd}/lib")
|
61
19
|
end
|
62
20
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
def is_linux?
|
71
|
-
operating_system_name == 'Linux'
|
72
|
-
end
|
73
|
-
|
74
|
-
def is_mac?
|
75
|
-
operating_system_name == 'MacOS'
|
76
|
-
end
|
77
|
-
|
78
|
-
def xvfb_installed?
|
79
|
-
@xvfb_installed
|
80
|
-
end
|
81
|
-
|
82
|
-
def finalize_exit
|
83
|
-
exit(1) if @run_rspec_step.rspec_exit_status.nonzero?
|
84
|
-
exit(1) if @prep_db_step.exit_status.nonzero?
|
85
|
-
exit(0)
|
21
|
+
def largest_exit_status
|
22
|
+
@steps.inject(0) do |max, step|
|
23
|
+
# If a step doesn't execute, it's exit_status will be nil.
|
24
|
+
current = step.exit_status || 0
|
25
|
+
max > current ? max : current
|
26
|
+
end
|
86
27
|
end
|
87
28
|
end
|
88
29
|
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# RspecStarterStep is essentially an abstract super class. It should not be instantiated directly. It primarily holds the
|
2
|
+
# logic for executing a Task or Command. Steps (and their subclasses) maintain three different types of options.
|
3
|
+
# 1. Command Options - These are specified on the command line when the bin/start_rspec command is run.
|
4
|
+
# 2. Step Options - These are specified inside the bin/start_rspec file when a task or command is added to the step list.
|
5
|
+
# 3. Step Defaults - Steps define default values when Command Options or Step Options are not given.
|
6
|
+
class RspecStarterStep
|
7
|
+
attr_accessor :id, :exit_status, :name, :quiet, :options, :run_time, :runner, :successful
|
8
|
+
|
9
|
+
alias_method :successful?, :successful
|
10
|
+
|
11
|
+
def initialize(id, runner, options)
|
12
|
+
@id = id
|
13
|
+
initialize_name
|
14
|
+
@runner = runner
|
15
|
+
@options = options
|
16
|
+
|
17
|
+
@start_time = nil
|
18
|
+
@finish_time = nil
|
19
|
+
@run_time = nil
|
20
|
+
|
21
|
+
@exit_status = nil
|
22
|
+
@successful = nil
|
23
|
+
# This is set when the step runs
|
24
|
+
@starting_message = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.provide_options_to(registrar)
|
28
|
+
@options_registrar = registrar
|
29
|
+
register_options
|
30
|
+
end
|
31
|
+
|
32
|
+
# Tasks can implement this method and register options that they support.
|
33
|
+
def self.register_options
|
34
|
+
end
|
35
|
+
|
36
|
+
def should_skip?
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def quiet?
|
41
|
+
options.quiet
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_on_problem?
|
45
|
+
options.stop_on_problem
|
46
|
+
end
|
47
|
+
|
48
|
+
def failed?
|
49
|
+
!@successful
|
50
|
+
end
|
51
|
+
|
52
|
+
def verbose?
|
53
|
+
!quiet?
|
54
|
+
end
|
55
|
+
|
56
|
+
def helpers
|
57
|
+
RspecStarter.helpers
|
58
|
+
end
|
59
|
+
|
60
|
+
def run
|
61
|
+
set_starting_message
|
62
|
+
print_starting_message
|
63
|
+
set_start_time
|
64
|
+
execute_step
|
65
|
+
set_finish_time
|
66
|
+
set_run_time
|
67
|
+
write_run_time
|
68
|
+
handle_step_failure
|
69
|
+
end
|
70
|
+
|
71
|
+
# Most subclasses will suppress output when they run.
|
72
|
+
def self.default_quiet
|
73
|
+
true
|
74
|
+
end
|
75
|
+
|
76
|
+
# Most subclasses will prefer to stop rspec_starter if they hit a problem.
|
77
|
+
def self.default_stop_on_problem
|
78
|
+
true
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# rubocop:disable Style/RescueStandardError
|
84
|
+
# This method executes the step and ensures the output is displayed to the screen. If errors occur, it will not raise
|
85
|
+
# an error that terminates the entire process. This method is focused on running a step, getting results and displaying output.
|
86
|
+
# The `run` method does a final check at the end to determine if we should proceed to the next step, or raise an error
|
87
|
+
# that terminates everything.
|
88
|
+
def execute_step
|
89
|
+
# Tell the step to execute. Steps should raise a 'RspecStarter::StepStopper' error by calling the 'problem' method when they
|
90
|
+
# want to terminate the step and report a problem. Steps should raise a 'RspecStarter::StepStopper' error when they
|
91
|
+
# want to terminate the step and report success. The code the step runs may also trigger any Ruby error. Those errors
|
92
|
+
# are captured and are treated like a problem occured.
|
93
|
+
execute
|
94
|
+
# If we get here, the step didn't explicitly trigger a problem, trigger a success or raise an error.
|
95
|
+
# Assume success and trigger it now.
|
96
|
+
success
|
97
|
+
rescue RspecStarter::StepStopper => e
|
98
|
+
# If we get here, it's because the step called `problem` or `success`. Those two methods gracefully record final results
|
99
|
+
# for the step and format a message to show the user. Since the results are already recorded, we just need to display
|
100
|
+
# the message.
|
101
|
+
print e.message
|
102
|
+
rescue => e # Catch any other error
|
103
|
+
mark_result(success: false, exit_status: 1)
|
104
|
+
print formatted_problem_message(e.message)
|
105
|
+
end
|
106
|
+
# rubocop:enable Style/RescueStandardError
|
107
|
+
|
108
|
+
def handle_step_failure
|
109
|
+
return unless failed?
|
110
|
+
|
111
|
+
# If the task ran quietly, give it the opportunity to write any error output it might want to show.
|
112
|
+
write_error_info if quiet?
|
113
|
+
raise RspecStarter::StepError if stop_on_problem?
|
114
|
+
end
|
115
|
+
|
116
|
+
def write_run_time
|
117
|
+
puts " (#{run_time}s)"
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_run_time
|
121
|
+
@run_time = (@finish_time - @start_time).round(3)
|
122
|
+
end
|
123
|
+
|
124
|
+
def set_start_time
|
125
|
+
@start_time = time_now
|
126
|
+
end
|
127
|
+
|
128
|
+
def set_starting_message
|
129
|
+
@starting_message = starting_message
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_finish_time
|
133
|
+
@finish_time = time_now
|
134
|
+
end
|
135
|
+
|
136
|
+
def time_now
|
137
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
138
|
+
end
|
139
|
+
|
140
|
+
def starting_message
|
141
|
+
"Add the starting_message method to your task and say what it's doing".warning
|
142
|
+
end
|
143
|
+
|
144
|
+
def mark_result(success:, exit_status:)
|
145
|
+
@successful = success
|
146
|
+
@exit_status = exit_status
|
147
|
+
end
|
148
|
+
|
149
|
+
def success(msg="")
|
150
|
+
mark_result(success: true, exit_status: 0)
|
151
|
+
raise RspecStarter::StepStopper, formatted_success_message(msg)
|
152
|
+
end
|
153
|
+
|
154
|
+
def problem(details="", exact: false, exit_status: 1)
|
155
|
+
mark_result(success: false, exit_status: exit_status)
|
156
|
+
raise RspecStarter::StepStopper, formatted_problem_message(details, exact: exact)
|
157
|
+
end
|
158
|
+
|
159
|
+
def formatted_success_message(msg)
|
160
|
+
(msg.empty? ? " Success!!" : " Success!! (#{msg})").colorize(:green)
|
161
|
+
end
|
162
|
+
|
163
|
+
def formatted_problem_message(details, exact: false)
|
164
|
+
details_part = details.empty? ? "" : " (#{details})"
|
165
|
+
msg = exact ? details : " #{problem_msg_label}#{details_part}"
|
166
|
+
msg.colorize(problem_msg_color)
|
167
|
+
end
|
168
|
+
|
169
|
+
def problem_msg_label
|
170
|
+
stop_on_problem? ? "Failed" : "Warning"
|
171
|
+
end
|
172
|
+
|
173
|
+
def problem_msg_color
|
174
|
+
stop_on_problem? ? :red : :yellow
|
175
|
+
end
|
176
|
+
|
177
|
+
# Do nothing by default. Subclasses will implement this method if they want to display error info.
|
178
|
+
# This method is only called in certain situations.
|
179
|
+
def write_error_info
|
180
|
+
end
|
181
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module RspecStarter
|
2
|
+
# StepContext is an abstract class. Subclasses hold the information from the RspecStarter.start block when the block is parsed,
|
3
|
+
# but they don't actually execute Steps. Their job is to instantiate Step objects, and bind the appropriate options to the
|
4
|
+
# objects so they can execute correctly.
|
5
|
+
class StepContext
|
6
|
+
attr_reader :environment, :id, :requested_args
|
7
|
+
|
8
|
+
def initialize(environment:, id:, requested_args:)
|
9
|
+
@environment = environment
|
10
|
+
@id = id
|
11
|
+
@requested_args = requested_args
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_options
|
17
|
+
options = StepOptions.new
|
18
|
+
apply_global_options_to_options(options)
|
19
|
+
options
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply_global_options_to_options(options)
|
23
|
+
options.add("quiet", step_class.default_quiet)
|
24
|
+
options.add("stop_on_problem", step_class.default_stop_on_problem)
|
25
|
+
options.add("rspec_args_string", environment.options.rspec_args_string)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module RspecStarter
|
2
|
+
# StepOptions is like an OpenStruct. It lets us add getters and setters to an object dynamically. It's used to hold the
|
3
|
+
# options and values that uses specify on the commandline, and inside the RspecStarter.start bock. We used this custom class
|
4
|
+
# instead of an OpenStruct to avoid the performance issues associated with OpenStruct and to give the ability to add additional
|
5
|
+
# methods when needed.
|
6
|
+
class StepOptions
|
7
|
+
def add(key, value)
|
8
|
+
instance_variable_set("@#{key}", value)
|
9
|
+
self.class.define_method(key.to_s) do
|
10
|
+
instance_variable_get("@#{key}")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def update(key, value, add_missing: true)
|
15
|
+
return instance_variable_set("@#{key}", value) if respond_to?(key)
|
16
|
+
|
17
|
+
add(key, value) if add_missing
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module RspecStarter
|
2
|
+
# TaskContext's are created to when parsing the RspecStater.start block. They hold the args and Task class name. They are
|
3
|
+
# asked to create instances of Task subclasses from this information when it is time to execute. The also resolve the options
|
4
|
+
# that each Task is allowed to access.
|
5
|
+
class TaskContext < StepContext
|
6
|
+
attr_reader :step_class
|
7
|
+
|
8
|
+
def initialize(environment:, id:, step_class:, requested_args:)
|
9
|
+
super(environment: environment, id: id, requested_args: requested_args)
|
10
|
+
|
11
|
+
@step_class = step_class
|
12
|
+
end
|
13
|
+
|
14
|
+
def instantiate(runner)
|
15
|
+
@step_class.new(@id, runner, build_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_task?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_command?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build_options
|
29
|
+
options = super
|
30
|
+
add_defaults_to_options(options)
|
31
|
+
apply_args_to_options(options)
|
32
|
+
apply_command_line_switches_to_options(options)
|
33
|
+
options
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_defaults_to_options(options)
|
37
|
+
registered_options = environment.options.registered_task_option(@step_class)
|
38
|
+
registered_options.each { |option| options.add(option.key, option.default) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def apply_args_to_options(options)
|
42
|
+
registered_options = environment.options.registered_task_option(@step_class)
|
43
|
+
dsl_option_names = registered_options.select(&:is_dsl_option?).collect { |option| option.name.to_sym }
|
44
|
+
@requested_args.each do |key, value|
|
45
|
+
next unless dsl_option_names.include?(key)
|
46
|
+
|
47
|
+
options.update(key, value, add_missing: false)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply_command_line_switches_to_options(options)
|
52
|
+
registered_options = environment.options.registered_task_option(@step_class)
|
53
|
+
present_switches = environment.options.present_switches
|
54
|
+
registered_options.each do |option|
|
55
|
+
# The switch could be nil for the option, which means the step isn't registering a switch for this option. The step
|
56
|
+
# developer just wants to register an option for the dsl (which is applied earlier).
|
57
|
+
if option.switch
|
58
|
+
options.update(option.key, !option.default, add_missing: false) if present_switches.include?(option.switch)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# Rebuild the database for a rails application or a rails engine. This task honors the following command line options
|
2
|
+
# --skip-db-prep Causes the task to be skipped
|
3
|
+
class RebuildRailsAppDatabase < RspecStarterTask
|
4
|
+
def self.description
|
5
|
+
"Rebuild a Ruby on Rails application or engine database."
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.register_options
|
9
|
+
register_option default: false, switch: '--skip-db-prep',
|
10
|
+
switch_description: "DO NOT prepare the Rails application database"
|
11
|
+
register_option name: "command",
|
12
|
+
default: "DISABLE_DATABASE_ENVIRONMENT_CHECK=1 RAILS_ENV=test rake db:drop db:create db:migrate",
|
13
|
+
description: "A command string that is used to rebuild the database."
|
14
|
+
end
|
15
|
+
|
16
|
+
def should_skip?
|
17
|
+
options.skip_db_prep || options.command.nil? || options.command.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def starting_message
|
21
|
+
"Running #{options.command.highlight}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
if quiet?
|
26
|
+
@stdout, @stderr, @status = Open3.capture3(options.command)
|
27
|
+
else
|
28
|
+
puts "\n\n"
|
29
|
+
@verbose_command_passed = system options.command
|
30
|
+
@status = $CHILD_STATUS
|
31
|
+
print @starting_message
|
32
|
+
end
|
33
|
+
problem if command_failed?
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_error_info
|
37
|
+
puts @stdout
|
38
|
+
puts @stderr
|
39
|
+
puts "\n\nThere was an error rebuilding the test database. See the output above for details " \
|
40
|
+
"or manually run '#{options.command}' for more information.".colorize(:red)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Simply checking the exitstatus isn't good enough. When rake aborts due to a bug, it will still
|
46
|
+
# return a zero exit status. We need to see if 'rake aborted!' has been written to the output.
|
47
|
+
def command_failed?
|
48
|
+
@status.exitstatus.nonzero? || (!@stderr.nil? && @stderr.include?("rake aborted!")) || @verbose_command_passed == false
|
49
|
+
end
|
50
|
+
end
|