|
| 1 | +require 'rspec/support/spec/shell_out' |
| 2 | + |
| 3 | +RSpec.shared_examples_for "library wide checks" do |lib, options| |
| 4 | + consider_a_test_env_file = options.fetch(:consider_a_test_env_file, /MATCHES NOTHING/) |
| 5 | + allowed_loaded_feature_regexps = options.fetch(:allowed_loaded_feature_regexps, []) |
| 6 | + preamble_for_lib = options[:preamble_for_lib] |
| 7 | + preamble_for_spec = "require 'rspec/core'; require 'spec_helper'" |
| 8 | + skip_spec_files = options.fetch(:skip_spec_files, /MATCHES NOTHING/) |
| 9 | + |
| 10 | + include RSpec::Support::ShellOut |
| 11 | + |
| 12 | + define_method :files_to_require_for do |sub_dir| |
| 13 | + slash = File::SEPARATOR |
| 14 | + lib_path_re = /#{slash + lib}[^#{slash}]*#{slash}lib/ |
| 15 | + load_path = $LOAD_PATH.grep(lib_path_re).first |
| 16 | + directory = load_path.sub(/lib$/, sub_dir) |
| 17 | + files = Dir["#{directory}/**/*.rb"] |
| 18 | + extract_regex = /#{Regexp.escape(directory) + File::SEPARATOR}(.+)\.rb$/ |
| 19 | + |
| 20 | + # We sort to ensure the files are loaded in a consistent order, regardless |
| 21 | + # of OS. Otherwise, it could load in a different order on Travis than |
| 22 | + # locally, and potentially trigger a "circular require considered harmful" |
| 23 | + # warning or similar. |
| 24 | + files.sort.map { |file| file[extract_regex, 1] } |
| 25 | + end |
| 26 | + |
| 27 | + def command_from(code_lines) |
| 28 | + code_lines.join("\n") |
| 29 | + end |
| 30 | + |
| 31 | + def load_all_files(files, preamble, postamble=nil) |
| 32 | + requires = files.map { |f| "require '#{f}'" } |
| 33 | + command = command_from(Array(preamble) + requires + Array(postamble)) |
| 34 | + |
| 35 | + stdout, stderr, status = with_env 'NO_COVERAGE' => '1' do |
| 36 | + options = %w[ -w ] |
| 37 | + options << "--disable=gem" if RUBY_VERSION.to_f >= 1.9 && RSpec::Support::Ruby.mri? |
| 38 | + run_ruby_with_current_load_path(command, *options) |
| 39 | + end |
| 40 | + |
| 41 | + # Ignore bundler warning. |
| 42 | + stderr = stderr.split("\n").reject { |l| l =~ %r{bundler/source/rubygems} }.join("\n") |
| 43 | + [stdout, stderr, status.exitstatus] |
| 44 | + end |
| 45 | + |
| 46 | + define_method :load_all_lib_files do |
| 47 | + files = all_lib_files - lib_test_env_files |
| 48 | + preamble = ['orig_loaded_features = $".dup', preamble_for_lib] |
| 49 | + postamble = [ |
| 50 | + 'loaded_features = ($" - orig_loaded_features).join("\n")', |
| 51 | + "File.open('#{loaded_features_outfile}', 'w') { |f| f.write(loaded_features) }" |
| 52 | + ] |
| 53 | + |
| 54 | + load_all_files(files, preamble, postamble) |
| 55 | + end |
| 56 | + |
| 57 | + define_method :load_all_spec_files do |
| 58 | + files = files_to_require_for("spec") + lib_test_env_files |
| 59 | + files = files.reject { |f| f =~ skip_spec_files } |
| 60 | + load_all_files(files, preamble_for_spec) |
| 61 | + end |
| 62 | + |
| 63 | + attr_reader :loaded_features_outfile, :all_lib_files, :lib_test_env_files, |
| 64 | + :lib_file_results, :spec_file_results |
| 65 | + |
| 66 | + before(:context) do |
| 67 | + @loaded_features_outfile = if ENV['CI'] |
| 68 | + # On AppVeyor we get exit status 5 ("Access is Denied", |
| 69 | + # from what I've read) when trying to write to a tempfile. |
| 70 | + # |
| 71 | + # On Travis, we occasionally get Errno::ENOENT (No such file |
| 72 | + # or directory) when reading from a tempfile. |
| 73 | + # |
| 74 | + # In both cases if we leave a file behind in the current dir, |
| 75 | + # it's not a big deal so we put it in the current dir. |
| 76 | + File.join(".", "loaded_features.txt") |
| 77 | + else |
| 78 | + # Locally it's nice not to pollute the current working directory |
| 79 | + # so we use a tempfile instead. |
| 80 | + require 'tempfile' |
| 81 | + Tempfile.new("loaded_features.txt").path |
| 82 | + end |
| 83 | + |
| 84 | + @all_lib_files = files_to_require_for("lib") |
| 85 | + @lib_test_env_files = all_lib_files.grep(consider_a_test_env_file) |
| 86 | + |
| 87 | + @lib_file_results, @spec_file_results = [ |
| 88 | + # Load them in parallel so it's faster... |
| 89 | + Thread.new { load_all_lib_files }, |
| 90 | + Thread.new { load_all_spec_files } |
| 91 | + ].map(&:join).map(&:value) |
| 92 | + end |
| 93 | + |
| 94 | + def have_successful_no_warnings_output |
| 95 | + eq ["", "", 0] |
| 96 | + end |
| 97 | + |
| 98 | + it "issues no warnings when loaded", :slow, :failing_on_appveyor do |
| 99 | + expect(lib_file_results).to have_successful_no_warnings_output |
| 100 | + end |
| 101 | + |
| 102 | + it "issues no warnings when the spec files are loaded", :slow do |
| 103 | + expect(spec_file_results).to have_successful_no_warnings_output |
| 104 | + end |
| 105 | + |
| 106 | + it 'only loads a known set of stdlibs so gem authors are forced ' \ |
| 107 | + 'to load libs they use to have passing specs', :slow, :failing_on_appveyor do |
| 108 | + loaded_features = File.read(loaded_features_outfile).split("\n") |
| 109 | + if RUBY_VERSION == '1.8.7' |
| 110 | + # On 1.8.7, $" returns the relative require path if that was used |
| 111 | + # to require the file. LIB_REGEX will not match the relative version |
| 112 | + # since it has a `/lib` prefix. Here we deal with this by expanding |
| 113 | + # relative files relative to the $LOAD_PATH dir (lib). |
| 114 | + Dir.chdir("lib") { loaded_features.map! { |f| File.expand_path(f) } } |
| 115 | + end |
| 116 | + |
| 117 | + loaded_features.reject! { |feature| RSpec::CallerFilter::LIB_REGEX =~ feature } |
| 118 | + loaded_features.reject! { |feature| allowed_loaded_feature_regexps.any? { |r| r =~ feature } } |
| 119 | + |
| 120 | + expect(loaded_features).to eq([]) |
| 121 | + end |
| 122 | + |
| 123 | + # This malformed whitespace detection logic has been borrowed from bundler: |
| 124 | + # https://github.com/bundler/bundler/blob/v1.8.0/spec/quality_spec.rb |
| 125 | + def check_for_tab_characters(filename) |
| 126 | + failing_lines = [] |
| 127 | + File.readlines(filename).each_with_index do |line, number| |
| 128 | + failing_lines << number + 1 if line =~ /\t/ |
| 129 | + end |
| 130 | + |
| 131 | + return if failing_lines.empty? |
| 132 | + "#{filename} has tab characters on lines #{failing_lines.join(', ')}" |
| 133 | + end |
| 134 | + |
| 135 | + def check_for_extra_spaces(filename) |
| 136 | + failing_lines = [] |
| 137 | + File.readlines(filename).each_with_index do |line, number| |
| 138 | + next if line =~ /^\s+#.*\s+\n$/ |
| 139 | + failing_lines << number + 1 if line =~ /\s+\n$/ |
| 140 | + end |
| 141 | + |
| 142 | + return if failing_lines.empty? |
| 143 | + "#{filename} has spaces on the EOL on lines #{failing_lines.join(', ')}" |
| 144 | + end |
| 145 | + |
| 146 | + RSpec::Matchers.define :be_well_formed do |
| 147 | + match do |actual| |
| 148 | + actual.empty? |
| 149 | + end |
| 150 | + |
| 151 | + failure_message do |actual| |
| 152 | + actual.join("\n") |
| 153 | + end |
| 154 | + end |
| 155 | + |
| 156 | + it "has no malformed whitespace" do |
| 157 | + error_messages = [] |
| 158 | + `git ls-files -z`.split("\x0").each do |filename| |
| 159 | + error_messages << check_for_tab_characters(filename) |
| 160 | + error_messages << check_for_extra_spaces(filename) |
| 161 | + end |
| 162 | + expect(error_messages.compact).to be_well_formed |
| 163 | + end |
| 164 | +end |
0 commit comments