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,74 @@
1
+ # Helper methods that can be used anywhere in RspecStarter.
2
+ module RspecStarter
3
+ def self.helpers
4
+ @helpers ||= Helpers.new
5
+ end
6
+
7
+ # A class that provides helper methods that can be used anywhere in RspecStarter.
8
+ class Helpers
9
+ def class_for_task_name(string_or_symbol)
10
+ string_or_symbol.to_s.camelize.constantize
11
+ end
12
+
13
+ def project_is_rails_app?
14
+ @project_is_rails_app ||= File.file?(File.join(Dir.pwd, 'config', 'application.rb'))
15
+ end
16
+
17
+ def project_is_rails_engine?
18
+ return false unless project_has_lib_dir?
19
+
20
+ Dir["#{Dir.pwd}/lib/**/*.rb"].each do |file|
21
+ return true if File.readlines(file).detect { |line| line.match(/\s*class\s+.*<\s+::Rails::Engine/) }
22
+ end
23
+ false
24
+ end
25
+
26
+ def project_has_lib_dir?
27
+ @project_has_lib_dir ||= Dir.exist?("#{Dir.pwd}/lib")
28
+ end
29
+
30
+ # Taken from https://stackoverflow.com/questions/11784109/detecting-operating-systems-in-ruby/13586108
31
+ def operating_system_name
32
+ @operating_system_name ||= begin
33
+ host_os = RbConfig::CONFIG['host_os']
34
+ case host_os
35
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
36
+ :windows
37
+ when /darwin|mac os/
38
+ :macosx
39
+ when /linux/
40
+ :linux
41
+ when /solaris|bsd/
42
+ :unix
43
+ else
44
+ :unknown
45
+ end
46
+ end
47
+ end
48
+
49
+ def is_linux?
50
+ operating_system_name == :linux
51
+ end
52
+
53
+ def is_mac?
54
+ operating_system_name == :maxosx
55
+ end
56
+
57
+ def xvfb_installed?
58
+ @xvfb_installed ||= RspecStarter.which("xvfb-run")
59
+ end
60
+
61
+ def xvfb_not_installed?
62
+ !xvfb_installed?
63
+ end
64
+
65
+ # This is ugly, but it gives us some added flexibility. Users can invoke the rspec_starter method from any script or
66
+ # executable. This method attempts to find out the file name of the script/exectuable.
67
+ #
68
+ # 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
69
+ # (possibly in the bin folder, or root of the project).
70
+ def starter_script_file_name
71
+ caller.last.split(":")[0]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,16 @@
1
+ # This module loads the legacy RspecStarter code that uses the non-block DSL. It will be removed in 2.0.0.
2
+ # The file is loaded when RspecStarter starts, but most of the legacy code is not loaded until the
3
+ # 'invoke_legacy_starter' method is called.
4
+ module RspecStarter
5
+ def self.invoke_legacy_starter(defaults)
6
+ require 'rspec_starter/legacy/legacy_runner'
7
+ # puts "[DEPRECATION NOTICE] Your #{helpers.starter_script_file_name} file uses an old method for starting RSpec.\n" \
8
+ # "RspecStarter 1.6.0 introduced a new interface that is faster and more flexible.\n" \
9
+ # "To upgrade:\n" \
10
+ # " 1. Run 'rspec_starter --init' to install a new bin/start_rspec file.\n" \
11
+ # " 2. Your old bin/start_rspec will be renamed to bin/start_rspec.bak. If you customized\n" \
12
+ # " this file, open it up and consider moving the changes to the new bin/start_rspec file.\n" \
13
+ # " See https://github.com/roberts1000/rspec_starter for instructions on the new interface.\n".colorize(:yellow)
14
+ LegacyRunner.new(defaults).run
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ 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" }
6
+ end
7
+
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
25
+ end
26
+
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
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,90 @@
1
+ require 'pathname'
2
+ require 'open3'
3
+
4
+ require_relative 'help'
5
+ require_relative 'steps/step'
6
+ require_relative 'steps/verify_xvfb_step'
7
+ require_relative 'steps/prepare_database_step'
8
+ require_relative 'steps/remove_tmp_folder_step'
9
+ require_relative 'steps/invoke_rspec_step'
10
+
11
+ module RspecStarter
12
+ # This is a simple class that encapulates the process of running RSpec. When a Runner is created, it creates a set of
13
+ # steps that will be executed, in order, when the 'run' method is invoked. Each step encapsulates an action that can be
14
+ # taken to help invoke Rspec. Steps are typically independent do not depend on information from other steps. However
15
+ # this is not a hard rule. If more complex steps are needed, feel free to create them. Each steps knows about the main
16
+ # runner object, so the runner object is a good place to store shared info.
17
+ class LegacyRunner
18
+ include Help
19
+ attr_reader :xvfb_installed, :step_num, :steps
20
+
21
+ def initialize(defaults)
22
+ @steps = []
23
+ @step_num = 1
24
+ @xvfb_installed = RspecStarter.which("xvfb-run")
25
+ @prep_db_step = PrepareDatabaseStep.new(defaults, self)
26
+ @run_rspec_step = InvokeRspecStep.new(defaults, self)
27
+ @steps << VerifyXvfbStep.new(defaults, self)
28
+ @steps << @prep_db_step
29
+ @steps << RemoveTmpFolderStep.new(defaults, self)
30
+ @steps << @run_rspec_step
31
+ end
32
+
33
+ def run
34
+ return show_help if should_show_help? # If we show help, exit and don't do anything else.
35
+
36
+ @steps.each do |step|
37
+ next unless step.should_execute?
38
+
39
+ step.execute
40
+ @step_num += 1
41
+ break if step.failed?
42
+ end
43
+
44
+ finalize_exit
45
+ end
46
+
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
+
54
+ Dir["#{Dir.pwd}/lib/**/*.rb"].each do |file|
55
+ return true if File.readlines(file).detect { |line| line.match(/\s*class\s+.*<\s+::Rails::Engine/) }
56
+ end
57
+ false
58
+ end
59
+
60
+ def project_has_lib_dir?
61
+ Dir.exist?("#{Dir.pwd}/lib")
62
+ end
63
+
64
+ def operating_system_name
65
+ result = `uname`
66
+ return 'Linux' if result.include?('Linux')
67
+ return 'MacOS' if result.include?('Darwin')
68
+
69
+ 'Unknown'
70
+ end
71
+
72
+ def is_linux?
73
+ operating_system_name == 'Linux'
74
+ end
75
+
76
+ def is_mac?
77
+ operating_system_name == 'MacOS'
78
+ end
79
+
80
+ def xvfb_installed?
81
+ @xvfb_installed
82
+ end
83
+
84
+ def finalize_exit
85
+ exit(1) if @run_rspec_step.rspec_exit_status.nonzero?
86
+ exit(1) if @prep_db_step.exit_status.nonzero?
87
+ exit(0)
88
+ end
89
+ end
90
+ end
@@ -5,6 +5,7 @@ module RspecStarter
5
5
 
6
6
  def initialize(defaults, runner)
7
7
  super(runner)
8
+
8
9
  @allow_xvfb = defaults.fetch(:allow_xvfb, true)
9
10
  @relevant_options = ["--no-xvfb"]
10
11
  @success_or_skipped = nil # Will be updated once step executes
@@ -42,6 +43,7 @@ module RspecStarter
42
43
  return base if @runner.is_mac?
43
44
  return base unless @allow_xvfb
44
45
  return base if @user_wants_to_skip_xvfb
46
+
45
47
  @runner.xvfb_installed? ? "xvfb-run #{base}" : base
46
48
  end
47
49
  end
@@ -5,6 +5,7 @@ module RspecStarter
5
5
 
6
6
  def initialize(defaults, runner)
7
7
  super(runner)
8
+
8
9
  @prepare_database = defaults.fetch(:prepare_db, true)
9
10
  @relevant_options << '--no-prep-db'
10
11
  @user_wants_to_skip = ARGV.any? { |option| option.include?("--no-prep-db") }
@@ -19,6 +20,7 @@ module RspecStarter
19
20
  def should_execute?
20
21
  return false if @user_wants_to_skip
21
22
  return false unless @prepare_database
23
+
22
24
  @runner.project_is_rails_app? || @runner.project_is_rails_engine?
23
25
  end
24
26
 
@@ -41,7 +43,6 @@ module RspecStarter
41
43
  end
42
44
  end
43
45
 
44
-
45
46
  private
46
47
 
47
48
  def rebuild_command
@@ -52,6 +53,7 @@ module RspecStarter
52
53
  # return a zero exit status. We need to see if 'rake aborted!' has been written to the output.
53
54
  def successful?(stderr)
54
55
  return false if @exit_status.nonzero?
56
+
55
57
  !stderr.include?("rake aborted!")
56
58
  end
57
59
  end
@@ -3,6 +3,7 @@ module RspecStarter
3
3
  class RemoveTmpFolderStep < RspecStarter::Step
4
4
  def initialize(defaults, runner)
5
5
  super(runner)
6
+
6
7
  @remove_tmp_folder = defaults.fetch(:remove_tmp, true)
7
8
  @runner = runner
8
9
  @relevant_options << "--no-remove-tmp"
@@ -16,11 +17,13 @@ module RspecStarter
16
17
 
17
18
  def should_execute?
18
19
  return false if @user_wants_to_skip_removal
20
+
19
21
  @remove_tmp_folder
20
22
  end
21
23
 
22
24
  def execute
23
25
  return @success_or_skipped = true unless should_execute?
26
+
24
27
  existed_before = tmp_folder_exists?
25
28
 
26
29
  print "[#{@runner.step_num}] Removing #{'tmp'.colorize(:light_blue)} folder ... "
@@ -5,6 +5,7 @@ module RspecStarter
5
5
  class VerifyXvfbStep < RspecStarter::Step
6
6
  def initialize(defaults, runner)
7
7
  super(runner)
8
+
8
9
  @relevant_options << '--no-xvfb'
9
10
  @use_xvfb = defaults.fetch(:use_xvfb, true)
10
11
  @user_wants_to_skip_xvfb = ARGV.any? { |option| option.include?("--no-xvfb") }
@@ -21,6 +22,7 @@ module RspecStarter
21
22
 
22
23
  def should_execute?
23
24
  return false if @user_wants_to_skip_xvfb
25
+
24
26
  @use_xvfb
25
27
  end
26
28
 
@@ -0,0 +1,84 @@
1
+ module RspecStarter
2
+ # Option objects hold the information that Step subclasses want to register as options.
3
+ class Option
4
+ attr_reader :name, :default, :description, :key, :owner, :switch, :switch_description
5
+
6
+ def initialize(name:, default:, description: "", owner:, switch:, switch_description: "")
7
+ @owner = owner
8
+ @switch = switch
9
+ @switch_description = switch_description
10
+ @simplified_switch = switch.nil? ? nil : self.class.simplified_switch_name(switch)
11
+ @description = description
12
+ @default = default
13
+ self.name = name
14
+ @key = name || @simplified_switch
15
+ validate
16
+ end
17
+
18
+ def name=(value)
19
+ @name = value.nil? ? nil : value.to_s
20
+ end
21
+
22
+ # Remove leading hyphens
23
+ # Convert remaining hyphens to underscores
24
+ # Undercase
25
+ def self.simplified_switch_name(switch)
26
+ switch.sub(/^-*/, "").sub("-", "_").underscore
27
+ end
28
+
29
+ # Does this option apply to the DSL inside the RspecStarter.start block. If so, users can use it like this:
30
+ # RspecStarter.start do
31
+ # task :foo, option_name_here: "some_value"
32
+ # end
33
+ def is_dsl_option?
34
+ !name.nil?
35
+ end
36
+
37
+ private
38
+
39
+ def validate
40
+ validate_name_has_no_hyphens
41
+ validate_switch_starts_with_hyphens
42
+ validate_name_or_switch_present
43
+ validate_default_is_true_false_for_switch
44
+ validate_switch_is_like_name
45
+ end
46
+
47
+ def validate_name_has_no_hyphens
48
+ return unless name
49
+ return unless name.include?("-")
50
+
51
+ raise "#{owner.name}#register_options is trying to register #{name}. Option names should not include hyphens"
52
+ end
53
+
54
+ def validate_switch_starts_with_hyphens
55
+ return unless switch
56
+ return unless switch[0] != "-" || switch[2] == "-"
57
+
58
+ raise "#{owner.name}#register_options is trying to register switch #{switch}. The switch name must start with '--' or '-'"
59
+ end
60
+
61
+ def validate_name_or_switch_present
62
+ return unless switch.nil? && name.nil?
63
+
64
+ raise "#{owner.name}#register_options is trying to create an option but 'name:' or 'switch:' are not specified. " \
65
+ "At least one must be specified."
66
+ end
67
+
68
+ def validate_default_is_true_false_for_switch
69
+ return unless switch && ![true, false].include?(default)
70
+
71
+ raise "#{owner.name}#register_options is trying to create an option for switch #{switch}. Options with switches " \
72
+ "can only return true/false. Set the 'default' argument to true or false for the option."
73
+ end
74
+
75
+ def validate_switch_is_like_name
76
+ return unless !switch.nil? && !name.nil?
77
+ return if name == @simplified_switch
78
+
79
+ raise "#{owner.name}#register_options is trying to create an option with name #{name} and switch #{switch}. " \
80
+ "The switch must be the same as the name when hyphens are converted to underscores and leading hyphens " \
81
+ "are removed."
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,96 @@
1
+ module RspecStarter
2
+ # Manages the option registration process for the Step subclasses.
3
+ # Holds raw option information so it can organized when steps run.
4
+ class Options
5
+ attr_reader :present_switches, :registered_task_options, :rspec_args_string
6
+
7
+ def initialize(environment, args)
8
+ @environment = environment
9
+ # These are the ARGV args that are passed to the command line.
10
+ @command_line_args = args
11
+ # Task classes that are used in the RspecStarter.start block: VerifyDisplayServer, StartRspec, etc...
12
+ @task_classes = environment.unique_task_classes
13
+ # The options that each Task wants to use.
14
+ @registered_task_options = {}
15
+
16
+ # Tasks register the command line switches they care about and RspecStarter has some global switches that it uses. This
17
+ # holds the list of switches that the user provided that match a task switch or a global switch. If the user doesn't
18
+ # supply a switch, or supplies a switch that nobody cares about, this will be an empty list.
19
+ @present_switches = nil
20
+
21
+ load_registered_task_options
22
+ initialize_present_switches
23
+
24
+ # The args that should be passed to rspec when it starts.
25
+ @rspec_args_string = nil
26
+ initialize_rspec_args_string
27
+ end
28
+
29
+ # If 'switch' is given, the option can only return 'true' or 'false'. 'default' must be set to 'true' or 'false'. When
30
+ # the switch isn't specified the user, the default (which must be true/false) is return. When the user specifies the default
31
+ # !default is returned.
32
+
33
+ # If switch is not given, default an be set to anything.
34
+
35
+ # If name isn't specified, this option is only a commandline switch.
36
+ # If name is specified, this option can be used inside the start block on a task.
37
+ def register_task_option(klass, name: nil, default: nil, description: "", switch: nil, switch_description: "")
38
+ new_option = RspecStarter::Option.new(name: name, default: default, description: description, owner: klass, switch: switch,
39
+ switch_description: switch_description)
40
+ @registered_task_options[klass] << new_option
41
+ end
42
+
43
+ def all_task_options
44
+ hash = {}
45
+ @registered_task_options.each do |_task_class, options|
46
+ options.each do |option|
47
+ hash[option.switch] = option unless hash.has_key?(option.switch)
48
+ end
49
+ end
50
+
51
+ list = []
52
+ hash.each { |_switch, option| list << option }
53
+ list
54
+ end
55
+
56
+ def all_switches
57
+ (global_switches + all_task_switches).sort
58
+ end
59
+
60
+ def global_switches
61
+ ['--help', '-h']
62
+ end
63
+
64
+ def all_task_switches
65
+ list = Set.new
66
+ @registered_task_options.each do |_task_class, options|
67
+ options.each { |option| list << option.switch unless option.switch.nil? }
68
+ end
69
+ list.to_a
70
+ end
71
+
72
+ def registered_task_option(task_class)
73
+ @registered_task_options[task_class]
74
+ end
75
+
76
+ private
77
+
78
+ def initialize_rspec_args_string
79
+ list = @command_line_args.dup
80
+ all_switches.each { |switch| list.delete(switch) }
81
+ @rspec_args_string = list.join(" ")
82
+ end
83
+
84
+ def load_registered_task_options
85
+ # Give each step class the ability to register options that we should know about.
86
+ @task_classes.each do |klass|
87
+ @registered_task_options[klass] = []
88
+ klass.provide_options_to(self)
89
+ end
90
+ end
91
+
92
+ def initialize_present_switches
93
+ @present_switches = all_switches.select { |switch| @command_line_args.include?(switch) }.to_a
94
+ end
95
+ end
96
+ end