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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -0
  3. data/.travis.yml +5 -5
  4. data/CHANGELOG.md +29 -18
  5. data/README.md +145 -115
  6. data/exe/rspec_starter +14 -1
  7. data/lib/rspec_starter.rb +69 -24
  8. data/lib/rspec_starter/command.rb +59 -0
  9. data/lib/rspec_starter/command_context.rb +38 -0
  10. data/lib/rspec_starter/core_ext/string.rb +6 -3
  11. data/lib/rspec_starter/environment.rb +57 -0
  12. data/lib/rspec_starter/errors/step_error.rb +9 -0
  13. data/lib/rspec_starter/errors/step_stopper.rb +9 -0
  14. data/lib/rspec_starter/help.rb +79 -32
  15. data/lib/rspec_starter/helpers.rb +74 -0
  16. data/lib/rspec_starter/{which.rb → helpers/which.rb} +0 -0
  17. data/lib/rspec_starter/legacy.rb +16 -0
  18. data/lib/rspec_starter/legacy/help.rb +40 -0
  19. data/lib/rspec_starter/legacy/legacy_runner.rb +90 -0
  20. data/lib/rspec_starter/{steps → legacy/steps}/invoke_rspec_step.rb +2 -0
  21. data/lib/rspec_starter/{steps → legacy/steps}/prepare_database_step.rb +3 -1
  22. data/lib/rspec_starter/{steps → legacy/steps}/remove_tmp_folder_step.rb +3 -0
  23. data/lib/rspec_starter/{steps → legacy/steps}/step.rb +0 -0
  24. data/lib/rspec_starter/{steps → legacy/steps}/verify_xvfb_step.rb +2 -0
  25. data/lib/rspec_starter/option.rb +84 -0
  26. data/lib/rspec_starter/options.rb +96 -0
  27. data/lib/rspec_starter/rspec_starter_task.rb +35 -0
  28. data/lib/rspec_starter/runner.rb +15 -74
  29. data/lib/rspec_starter/step.rb +181 -0
  30. data/lib/rspec_starter/step_context.rb +28 -0
  31. data/lib/rspec_starter/step_options.rb +20 -0
  32. data/lib/rspec_starter/task_context.rb +63 -0
  33. data/lib/rspec_starter/tasks/rebuild_rails_app_database.rb +50 -0
  34. data/lib/rspec_starter/tasks/remove_tmp_folder.rb +28 -0
  35. data/lib/rspec_starter/tasks/start_rspec.rb +68 -0
  36. data/lib/rspec_starter/tasks/verify_display_server.rb +43 -0
  37. data/lib/rspec_starter/version.rb +1 -1
  38. data/lib/templates/rails_engine_start_rspec +38 -0
  39. data/lib/templates/rails_start_rspec +38 -0
  40. data/lib/templates/start_rspec +20 -5
  41. data/rspec_starter.gemspec +4 -2
  42. 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
@@ -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 is a simple class that encapulates the process of running RSpec. When a Runner is created, it creates a set of
14
- # steps that will be executed, in order, when the 'run' method is invoked. Each step encapsulates an action that can be
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
- include Help
20
- attr_reader :xvfb_installed, :step_num, :steps
5
+ attr_reader :environment
21
6
 
22
- def initialize(defaults)
23
- @steps = []
24
- @step_num = 1
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 unless step.should_execute?
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
- def project_is_rails_app?
48
- File.file?(File.join(Dir.pwd, 'config', 'application.rb'))
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 operating_system_name
64
- result = `uname`
65
- return 'Linux' if result.include?('Linux')
66
- return 'MacOS' if result.include?('Darwin')
67
- 'Unknown'
68
- end
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