Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit cb08d42

Browse files
committed
Merge pull request #179 from rspec/allowed-stdlibs
WIP: Update shared context to minimize loaded stdlibs.
2 parents 732245d + 4e1f0a6 commit cb08d42

File tree

5 files changed

+180
-7
lines changed

5 files changed

+180
-7
lines changed

lib/rspec/support/ruby_features.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'rbconfig'
2+
13
module RSpec
24
module Support
35
# @api private
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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

lib/rspec/support/spec/prevent_load_time_warnings.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# This file is deprecated, and is being replaced by library_wide_checks.rb.
2+
# For now we have to keep it until core/expectations/mocks have been updated.
3+
14
require 'rspec/support/spec/shell_out'
25

36
module RSpec

lib/rspec/support/spec/shell_out.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ def shell_out(*command)
2222
return stdout, filter(stderr), status
2323
end
2424
else # 1.8.7
25+
# popen3 doesn't provide the exit status so we fake it out.
26+
FakeProcessStatus = Struct.new(:exitstatus)
27+
2528
def shell_out(*command)
2629
stdout = stderr = nil
2730

@@ -30,8 +33,7 @@ def shell_out(*command)
3033
stderr = err.read
3134
end
3235

33-
# popen3 doesn't provide the exit status so we fake it out.
34-
status = instance_double(Process::Status, :exitstatus => 0)
36+
status = FakeProcessStatus.new(0)
3537
return stdout, filter(stderr), status
3638
end
3739
end

spec/rspec/support_spec.rb

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
require 'rspec/support'
2-
require 'rspec/support/spec/prevent_load_time_warnings'
2+
require 'rspec/support/spec/library_wide_checks'
33

44
module RSpec
55
describe Support do
66
extend Support::RubyFeatures
77

8-
it_behaves_like "a library that issues no warnings when loaded", "rspec-support",
9-
# Define methods that some of our rspec/support/spec files use at load time.
10-
"module RSpec; def self.configure; end; def self.shared_context(*); end; def self.shared_examples_for(*); end; end",
11-
'require "rspec/support"'
8+
it_behaves_like "library wide checks", "rspec-support",
9+
:consider_a_test_env_file => %r{rspec/support/spec},
10+
:allowed_loaded_feature_regexps => [
11+
/rbconfig/, # Used by RubyFeatures
12+
/prettyprint.rb/, /pp.rb/, /diff\/lcs/ # These are all loaded by the differ.
13+
]
1214

1315
describe '.method_handle_for(object, method_name)' do
1416
untampered_class = Class.new do

0 commit comments

Comments
 (0)