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
@@ -13,6 +13,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
13
13
 
14
14
  require "cri"
15
15
  require 'fileutils'
16
+ require 'rspec_starter/helpers'
16
17
 
17
18
  module Cri
18
19
  # A small monkey patch to force the cri gem to stop coloring output.
@@ -23,6 +24,7 @@ module Cri
23
24
  end
24
25
  end
25
26
 
27
+ # rubocop:disable Metrics/BlockLength
26
28
  command = Cri::Command.define do
27
29
  usage 'rspec_starter [options]'
28
30
  # aliases :ds, :stuff
@@ -35,12 +37,22 @@ command = Cri::Command.define do
35
37
  end
36
38
 
37
39
  flag :i, :init, "(re)initialize your project bin/start_rspec file" do |_value, _cmd|
40
+ helpers = RspecStarter::Helpers.new
41
+
38
42
  if File.file?("bin/start_rspec")
39
43
  FileUtils.mv("bin/start_rspec", "bin/start_rspec.bak")
40
44
  puts "bin/start_rspec renamed to bin/start_rspec.bak"
41
45
  end
42
46
 
43
- template_path = File.expand_path('../lib/templates/start_rspec', __dir__)
47
+ template_path =
48
+ if helpers.project_is_rails_app?
49
+ File.expand_path('../lib/templates/rails_start_rspec', __dir__)
50
+ elsif helpers.project_is_rails_app?
51
+ File.expand_path('../lib/templates/rails_engine_start_rspec', __dir__)
52
+ else
53
+ File.expand_path('../lib/templates/start_rspec', __dir__)
54
+ end
55
+
44
56
  FileUtils.cp(template_path, "bin/start_rspec")
45
57
  File.chmod(0o775, "bin/start_rspec")
46
58
  puts "bin/start_rspec created"
@@ -56,6 +68,7 @@ command = Cri::Command.define do
56
68
  run do |options, args, cmd|
57
69
  end
58
70
  end
71
+ # rubocop:enable Metrics/BlockLength
59
72
 
60
73
  begin
61
74
  command.run(ARGV)
@@ -1,7 +1,37 @@
1
+ require 'active_support/core_ext/string/inflections'
1
2
  require 'English'
2
3
  require 'colorize'
4
+ require 'open3'
5
+ require 'rbconfig'
6
+
7
+ require 'rspec_starter/core_ext/string'
8
+
9
+ require 'rspec_starter/help'
10
+ require 'rspec_starter/helpers'
11
+ require 'rspec_starter/helpers/which'
12
+ require 'rspec_starter/option'
13
+ require 'rspec_starter/options'
14
+ require 'rspec_starter/step_options'
15
+ require 'rspec_starter/environment'
16
+
3
17
  require "rspec_starter/version"
4
- require_relative 'rspec_starter/runner'
18
+ require "rspec_starter/errors/step_error"
19
+ require "rspec_starter/errors/step_stopper"
20
+
21
+ require 'rspec_starter/runner'
22
+ require 'rspec_starter/step_context'
23
+ require 'rspec_starter/step'
24
+ require 'rspec_starter/task_context'
25
+ require 'rspec_starter/rspec_starter_task'
26
+ require 'rspec_starter/command_context'
27
+ require 'rspec_starter/command'
28
+
29
+ require 'rspec_starter/tasks/rebuild_rails_app_database'
30
+ require 'rspec_starter/tasks/remove_tmp_folder'
31
+ require 'rspec_starter/tasks/start_rspec'
32
+ require 'rspec_starter/tasks/verify_display_server'
33
+
34
+ require 'rspec_starter/legacy'
5
35
 
6
36
  # Setup pry for development when running "rake console". Guard against load
7
37
  # errors in production (since pry is only loaded as a DEVELOPMENT dependency
@@ -13,29 +43,44 @@ rescue LoadError
13
43
  end
14
44
  # rubocop:enable Lint/HandleExceptions
15
45
 
16
- # Entry point for the RspecStarter gem.
46
+ # The main entry point for the RspecStarter gem. The 'bin/start_rspec' file contains a block of code like this:
47
+ #
48
+ # RspecStarter.start do
49
+ # command "echo 'something'"
50
+ # task :some_task_name
51
+ # ... more tasks and commands ...
52
+ # end
53
+ #
54
+ # The start method that is executed is show below. The call to 'instance_eval(&block)' below is the special sauce that
55
+ # makes 'self' equal the RspecStarter module inside the block.
17
56
  module RspecStarter
18
- # The 'start' method takes arguments that can be used to control the steps that are executed when running Rspec. These
19
- # arguments are specified by the developer when configuring how the app prefers to run RSpec. In addition to the arguments,
20
- # the end user can pass command line options to the script/executable that is executing 'start'. The command line options
21
- # allow the end user to customize execution on a per-run basis. In general, the arguments have the ability to turn features
22
- # on and off while the command line options allow users to turn features off. For example, a developer could probably
23
- # configure his aplication to always prepare the database before running RSpec. When the command is run, the developer might
24
- # want to bypass preparing the database for a specific run because he already knows the database is clean (this will save some
25
- # startup time). On the other hand, if the developer has configured 'start' to never prepare the database, he cannot
26
- # enable it via the command line on a specific run.
27
- #
28
- # Arguments List
29
- # defaults(Hash)
30
- # :prepare_db => (true/false) Should the database be rebuilt?
31
- # :remove_tmp => (true/false) Should the tmp folder inside the application be removed before starting RSpec?
32
- # :allow_xvfb => (true/false) Should XVFB be allowed on systems that need it (like Linux)
33
- #
34
- # Command Line Options
35
- # --no-xvfb Do not attempt to start XVFB on Linux. On Macs, this doesn't do anything since XVFB doesn't exist.
36
- # --no-prep-db Do not try to rebuild the database. This is useful when the db is already clean and want to save time.
37
- # --no-remove-tmp Do not attempt to remove the tmp folder.
38
- def self.start(defaults={})
39
- Runner.new(defaults).run
57
+ def self.start(defaults={}, &block)
58
+ # If a block is missing, then the user is using the old method based starter.
59
+ return invoke_legacy_starter(defaults) unless block_given?
60
+
61
+ # Loads the information from the bin/start_rspec file and loads/parses the options. Provides info to the @runner.
62
+ @environment = Environment.new(ARGV, &block)
63
+
64
+ # Holds the step objects and executes them.
65
+ @runner = Runner.new(@environment)
66
+
67
+ # The bin/start_rspec file in the host application sets the APP_ROOT to the root folder of the host application.
68
+ Dir.chdir APP_ROOT do
69
+ # If we show help, exit and don't do anything else. We need to run the help in the context of the app root
70
+ # so the help output can show file paths relative to where the user is running (and not relative to this gem).
71
+ return show_help if should_show_help?
72
+
73
+ begin
74
+ @runner.run
75
+ rescue StepError
76
+ # The runner executes steps (which are tasks and commands). If a step reports a problem and the 'stop_on_problem' option
77
+ # is set to 'true' (for a given step), the step will raise a StepError error. That is the signal that execution should
78
+ # terminate immediately.
79
+ exit(@runner.largest_exit_status)
80
+ end
81
+
82
+ # If we get here RSpec has been started and the rspec_starter's job is done.
83
+ exit(0)
84
+ end
40
85
  end
41
86
  end
@@ -0,0 +1,59 @@
1
+ module RspecStarter
2
+ # Commands are steps that execute shell scripts or shell commands. They can be created using the command method
3
+ # inside the RspecStarter.start block. Commands execute quietly unless you specifically set 'quiet: false'.
4
+ #
5
+ # RspecStarter.start do
6
+ # command "echo 'Done'"
7
+ # command "echo 'This command shows the echo'", quiet: false
8
+ # end
9
+ #
10
+ class Command < RspecStarterStep
11
+ attr_accessor :status, :stderr, :stdout
12
+
13
+ def initialize(id, runner, command_string, options)
14
+ super(id, runner, options)
15
+ @command_string = command_string
16
+ end
17
+
18
+ def self.default_quiet
19
+ true
20
+ end
21
+
22
+ def print_starting_message
23
+ print "Executing #{colored_command_string} ..." + (quiet? ? "" : "\n")
24
+ end
25
+
26
+ def execute
27
+ if quiet?
28
+ @stdout, @stderr, @status = Open3.capture3(@command_string)
29
+ else
30
+ @verbose_command_passed = system(@command_string)
31
+ @status = $CHILD_STATUS
32
+ print "Executed #{colored_command_string} -"
33
+ end
34
+
35
+ problem(exit_status: @status.exitstatus) if command_failed?
36
+ end
37
+
38
+ private
39
+
40
+ def initialize_name
41
+ @name = :command
42
+ end
43
+
44
+ def write_error_info
45
+ puts @stdout
46
+ puts @stderr
47
+ puts "There was an error running '#{@command_string}'. It returned with exit status #{@status.exitstatus}.\n" \
48
+ "See the output above for details or manually run the command for more information.".colorize(:red)
49
+ end
50
+
51
+ def colored_command_string
52
+ @command_string.colorize(:light_blue)
53
+ end
54
+
55
+ def command_failed?
56
+ @status.exitstatus > 0 || @verbose_command_passed == false
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ module RspecStarter
2
+ # CommandContext's are created to when parsing the RspecStater.start block. They hold the args for Commands. They are
3
+ # asked to create instances of Command from this information when it is time to execute. The also resolve the options
4
+ # that each Command is allowed to access.
5
+ class CommandContext < StepContext
6
+ attr_reader :command_string
7
+
8
+ def initialize(environment:, id:, command_string:, requested_args:)
9
+ super(environment: environment, id: id, requested_args: requested_args)
10
+
11
+ @command_string = command_string
12
+ end
13
+
14
+ def instantiate(runner)
15
+ Command.new(@id, runner, @command_string, build_options)
16
+ end
17
+
18
+ def step_class
19
+ Command
20
+ end
21
+
22
+ def is_task?
23
+ false
24
+ end
25
+
26
+ def is_command?
27
+ true
28
+ end
29
+
30
+ private
31
+
32
+ def build_options
33
+ options = super
34
+ apply_args_to_options(options)
35
+ options
36
+ end
37
+ end
38
+ end
@@ -1,7 +1,10 @@
1
1
  # Add some ome simple colorization methods for formatting console output.
2
- # Prefix methods with rs_ so they don't collide with other gems.
3
2
  class String
4
- def rs_colorize(color_code)
5
- "\e[#{color_code}m#{self}\e[0m"
3
+ def highlight
4
+ colorize(:light_blue)
5
+ end
6
+
7
+ def warning
8
+ colorize(:yellow)
6
9
  end
7
10
  end
@@ -0,0 +1,57 @@
1
+ module RspecStarter
2
+ # Load the environment for RspecStarter.
3
+ class Environment
4
+ attr_reader :options
5
+
6
+ # &start_block is the start block from the host application's bin/start_rspec file. It looks like this:
7
+ #
8
+ # RspecStarter.start do
9
+ # command "something"
10
+ # task :task_one
11
+ # command "something"
12
+ # task :task two
13
+ # ... more commands and tasks...
14
+ # end
15
+ def initialize(args, &start_block)
16
+ @next_id = 0
17
+ @step_info = {}
18
+
19
+ load_step_info(&start_block)
20
+
21
+ @options = Options.new(self, args, &start_block)
22
+ # This will lazily load once execution starts.
23
+ end
24
+
25
+ def step_contexts
26
+ @step_info.values
27
+ end
28
+
29
+ def unique_task_classes
30
+ @step_info.values.select(&:is_task?).collect(&:step_class).uniq
31
+ end
32
+
33
+ def command(string, **args)
34
+ step_id = next_id
35
+ @step_info[step_id] = CommandContext.new(environment: self, id: step_id, command_string: string, requested_args: args)
36
+ end
37
+
38
+ def task(task_name, **args)
39
+ step_id = next_id
40
+ klass = RspecStarter.helpers.class_for_task_name(task_name)
41
+ @step_info[step_id] = TaskContext.new(environment: self, id: step_id, step_class: klass, requested_args: args)
42
+ end
43
+
44
+ private
45
+
46
+ def next_id
47
+ @next_id += 1
48
+ end
49
+
50
+ def load_step_info(&start_block)
51
+ # This evaluates the start block and sets 'self' to an instance of Environment. The 'command' and 'task' methods
52
+ # in this class capture the 'command' and 'task' calls in the block. From there, we can determine the Steps classes that the
53
+ # user is trying to load.
54
+ instance_eval(&start_block)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ module RspecStarter
2
+ # This error is raised when execution should completely stop. The current step should be finalized before this
3
+ # error is raised. Once this error is raised, RspecStarter will halt and report a non-zero exit status to the user.
4
+ class StepError < StandardError
5
+ def initialize(msg="")
6
+ super
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RspecStarter
2
+ # This error is raised when a step wants to gracefully stop execution. It does not prevent later steps from running.
3
+ # It only ends on the current step execution.
4
+ class StepStopper < StandardError
5
+ def initialize(msg="")
6
+ super
7
+ end
8
+ end
9
+ end
@@ -1,40 +1,87 @@
1
+ # Code that shows output when 'bin/start_rspec --help' or 'bin/start_rspec -h' is executed.
1
2
  module RspecStarter
2
- # Method that support the help option on the bin/start_rspec script.
3
- module Help
4
- def should_show_help?
5
- ARGV.any? { |option| option.include? "--help" }
3
+ def self.should_show_help?
4
+ ARGV.any? { |option| option.include?("--help") || option.include?("-h") }
5
+ end
6
+
7
+ def self.show_help
8
+ # Figure out the file name that invoked the rspec_starter helper and treat it as the name of the
9
+ # script. We install a bin/rspec_starter file, but the user could call it anything.
10
+ script_name = helpers.starter_script_file_name
11
+ colored_script_name = script_name.colorize(:light_blue)
12
+
13
+ write_help_usage_section(script_name, colored_script_name)
14
+ write_help_command_line_options_section(colored_script_name)
15
+ write_help_examples_section(colored_script_name)
16
+ write_available_tasks
17
+ write_task_info(colored_script_name)
18
+
19
+ puts ""
20
+
21
+ exit(0)
22
+ end
23
+
24
+ def self.write_help_usage_section(script_name, colored_script_name)
25
+ puts "Usage: #{colored_script_name} #{'[options] [options for RSpec]'.colorize(:light_blue)}\n"
26
+ puts " #{script_name} will look for its own options first then pass any remaining options to RSpec."
27
+ end
28
+
29
+ def self.write_help_command_line_options_section(colored_script_name)
30
+ puts "\nCommand Line Options: (run #{'rspec --help'.colorize(:light_blue)} to see RSpec's options)"
31
+ puts " This list is computed dynamically based on the tasks that are enabled in the #{colored_script_name} file."
32
+
33
+ max_length = @environment.options.all_task_switches.max_by(&:length).size
34
+
35
+ @environment.options.all_task_options.each do |option|
36
+ next unless option.switch
37
+
38
+ length = option.switch.length
39
+ padding = max_length - length + 5 # 5 is the number of spaces past the longest swtich to start the description column
40
+ puts " #{option.switch.colorize(:light_blue)}#{' ' * padding}#{option.switch_description}"
6
41
  end
42
+ end
43
+
44
+ def self.write_help_examples_section(colored_script_name)
45
+ # rubocop:disable Metrics/LineLength
46
+ puts "\nExamples:"
47
+ puts " #{colored_script_name} #{'spec/features'.colorize(:light_blue)} (only run specs in the specs/features folder)"
48
+ puts " #{colored_script_name} #{'spec/features/some_spec:53'.colorize(:light_blue)} (run the spec on line 53 of the spec/features_some_spec.rb file)"
49
+ puts " #{colored_script_name} #{'--no-xvfb'.colorize(:light_blue)} #{'spec/requests/some_spec'.colorize(:light_blue)} (don't start XVFB since it's not needed for request specs)"
50
+ puts " #{'SIMPLECOV_FORMATTER=rcov'.colorize(:light_blue)} #{colored_script_name} (use with environment variables)\n"
51
+ # rubocop:enable Metrics/LineLength
52
+ end
7
53
 
8
- def show_help
9
- # Figure out the file name that invoked the rspec_starter helper. This is the name of the script; it be called anything.
10
- script_name = calling_file_name
11
- puts "Usage: #{script_name.colorize(:light_blue)} #{'[options] [options for RSpec]'.colorize(:light_blue)}\n"
12
- puts " #{script_name} will look for its own options first then pass any remaining options to rspec"
13
-
14
- # rubocop:disable Metrics/LineLength
15
- puts "\nOptions: (run 'rspec --help' to see RSpec's options)"
16
- puts " #{'--no-xvfb'.colorize(:light_blue)} DO NOT run XVFB (this can speed up RSpec when running tests that don't need XVFB)"
17
- puts " #{'--no-prep'.colorize(:light_blue)} DO NOT prepare the test database (can speed up testing if you know the DB is clean)"
18
-
19
- puts "\nExamples:"
20
- puts " #{script_name.colorize(:light_blue)} #{'spec/features'.colorize(:light_blue)} (only run specs in the specs/features folder)"
21
- puts " #{script_name.colorize(:light_blue)} #{'spec/features/some_spec:53'.colorize(:light_blue)} (run the spec on line 53 of the spec/features_some_spec.rb file)"
22
- puts " #{script_name.colorize(:light_blue)} #{'--no-xvfb'.colorize(:light_blue)} #{'spec/requests/some_spec'.colorize(:light_blue)} (don't start XVFB since it's not needed for request specs)"
23
- puts " #{'SIMPLECOV_FORMATTER=rcov'.colorize(:light_blue)} #{script_name.colorize(:light_blue)} (use with environment variables)\n"
24
- # rubocop:enable Metrics/LineLength
54
+ def self.write_available_tasks
55
+ subclasses = ObjectSpace.each_object(Class).select { |klass| klass < RspecStarterTask }.sort { |a, b| a.name <=> b.name }
56
+
57
+ puts "\nAvailable Tasks:"
58
+ subclasses.each do |klass|
59
+ task_name = RspecStarterTask.name_for_class(klass).to_s
60
+ puts " #{task_name.colorize(:light_blue)} - #{klass.description}"
25
61
  end
62
+ end
63
+
64
+ def self.write_task_info(colored_script_name)
65
+ puts "\nTask Options:"
66
+ puts " These options can be used inside the #{colored_script_name} file on #{"task".colorize(:light_blue)} lines."
67
+ puts " This list is computed dynamically based on the tasks that are enabled in the #{colored_script_name} file."
68
+ puts " Every task accepts the following options:"
69
+ puts " #{"quiet".colorize(:light_blue)} (true/false - Tell the task to be verbose. Some tasks may disregard at times.)"
70
+ puts " #{"stop_on_problem".colorize(:light_blue)} (true/false - Tell the task to stop startup if it encounters a problem.)"
71
+ puts ""
72
+
73
+ sorted_task_options = Hash[@environment.options.registered_task_options.sort_by do |klass, options|
74
+ RspecStarterTask.name_for_class(klass)
75
+ end]
26
76
 
27
- # This is ugly, but it gives us some added flexibility. Users can invoke the rspec_starter method from any script or
28
- # executable. This method attempts to find out the name of the script/exectuable.
29
- # "caller" returns the method stack, and because of the location of this file in the gem, we happen to be the 4th item in the
30
- # the array (hence "caller[3]" below).
31
- #
32
- # This method may not return a pretty result in all cases, but it's decent if the user has defined a script in their project
33
- # (possibly in the bin folder, or root of the project).
34
- def calling_file_name
35
- # rubocop:disable Performance/Caller
36
- caller[3].split(":")[0]
37
- # rubocop:enable Performance/Caller
77
+ sorted_task_options.each do |klass, options|
78
+ dsl_options = options.select(&:is_dsl_option?).sort {|a,b| a.name <=> b.name }
79
+ unless dsl_options.empty?
80
+ puts " :#{RspecStarterTask.name_for_class(klass).to_s.colorize(:light_blue)}"
81
+ dsl_options.each do |option|
82
+ puts " #{option.name.colorize(:light_blue)} (#{option.description})"
83
+ end
84
+ end
38
85
  end
39
86
  end
40
87
  end