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.
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