rspec_starter 1.5.0 → 1.6.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 +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
|