mirror of
https://github.com/ruby/ruby.git
synced 2026-01-26 20:19:19 +00:00
This change updates `write_binary` to use a new class, `AtomicFileWriter.open` to write the gem's files. This implementation is borrowed from Active Support's [`atomic_write`](https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb). Atomic write will write the files to a temporary file and then once created, sets permissions and renames the file. If the file is corrupted - ie on failed download, an error occurs, or for some other reason, the real file will not be created. The changes made here make `verify_gz` obsolete, we don't need to verify it if we have successfully created the file atomically. If it exists, it is not corrupt. If it is corrupt, the file won't exist on disk. While writing tests for this functionality I replaced the `RemoteFetcher` stub with `FakeFetcher` except for where we really do need to overwrite the `RemoteFetcher`. The new test implementation is much clearer on what it's trying to accomplish versus the prior test implementation. https://github.com/ruby/rubygems/commit/0cd4b54291
601 lines
16 KiB
Ruby
601 lines
16 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "helper"
|
|
|
|
require "rubygems/remote_fetcher"
|
|
require "rubygems/package"
|
|
|
|
class TestGemRemoteFetcher < Gem::TestCase
|
|
include Gem::DefaultUserInteraction
|
|
|
|
def setup
|
|
super
|
|
|
|
@cache_dir = File.join @gemhome, "cache"
|
|
|
|
# TODO: why does the remote fetcher need it written to disk?
|
|
@a1, @a1_gem = util_gem "a", "1" do |s|
|
|
s.executables << "a_bin"
|
|
end
|
|
|
|
@a1.loaded_from = File.join(@gemhome, "specifications", @a1.full_name)
|
|
|
|
Gem::RemoteFetcher.fetcher = nil
|
|
@stub_ui = Gem::MockGemUi.new
|
|
@fetcher = Gem::RemoteFetcher.fetcher
|
|
end
|
|
|
|
def test_self_fetcher
|
|
fetcher = Gem::RemoteFetcher.fetcher
|
|
refute_nil fetcher
|
|
assert_kind_of Gem::RemoteFetcher, fetcher
|
|
end
|
|
|
|
def test_self_fetcher_with_proxy
|
|
proxy_uri = "http://proxy.example.com"
|
|
Gem.configuration[:http_proxy] = proxy_uri
|
|
Gem::RemoteFetcher.fetcher = nil
|
|
|
|
fetcher = Gem::RemoteFetcher.fetcher
|
|
|
|
refute_nil fetcher
|
|
assert_kind_of Gem::RemoteFetcher, fetcher
|
|
assert_equal proxy_uri, fetcher.instance_variable_get(:@proxy).to_s
|
|
ensure
|
|
Gem.configuration[:http_proxy] = nil
|
|
end
|
|
|
|
def test_fetch_path_bad_uri
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
e = assert_raise ArgumentError do
|
|
@fetcher.fetch_path("gems.example.com/yaml", nil, true)
|
|
end
|
|
|
|
assert_equal "uri scheme is invalid: nil", e.message
|
|
end
|
|
|
|
def test_cache_update_path
|
|
uri = Gem::URI "http://example/file"
|
|
path = File.join @tempdir, "file"
|
|
|
|
fetcher = fake_fetcher(uri.to_s, "hello")
|
|
|
|
data = fetcher.cache_update_path uri, path
|
|
|
|
assert_equal "hello", data
|
|
|
|
assert_equal "hello", File.read(path)
|
|
end
|
|
|
|
def test_cache_update_path_with_utf8_internal_encoding
|
|
with_internal_encoding("UTF-8") do
|
|
uri = Gem::URI "http://example/file"
|
|
path = File.join @tempdir, "file"
|
|
data = String.new("\xC8").force_encoding(Encoding::BINARY)
|
|
|
|
fetcher = fake_fetcher(uri.to_s, data)
|
|
|
|
written_data = fetcher.cache_update_path uri, path
|
|
|
|
assert_equal data, written_data
|
|
assert_equal data, File.binread(path)
|
|
end
|
|
end
|
|
|
|
def test_cache_update_path_no_update
|
|
uri = Gem::URI "http://example/file"
|
|
path = File.join @tempdir, "file"
|
|
|
|
fetcher = fake_fetcher(uri.to_s, "hello")
|
|
|
|
data = fetcher.cache_update_path uri, path, false
|
|
|
|
assert_equal "hello", data
|
|
|
|
assert_path_not_exist path
|
|
end
|
|
|
|
def test_cache_update_path_overwrites_existing_file
|
|
uri = Gem::URI "http://example/file"
|
|
path = File.join @tempdir, "file"
|
|
|
|
# Create existing file with old content
|
|
File.write(path, "old content")
|
|
assert_equal "old content", File.read(path)
|
|
|
|
fetcher = fake_fetcher(uri.to_s, "new content")
|
|
|
|
data = fetcher.cache_update_path uri, path
|
|
|
|
assert_equal "new content", data
|
|
assert_equal "new content", File.read(path)
|
|
end
|
|
|
|
def test_download
|
|
a1_data = File.open @a1_gem, "rb", &:read
|
|
a1_url = "http://gems.example.com/gems/a-1.gem"
|
|
|
|
fetcher = fake_fetcher(a1_url, a1_data)
|
|
|
|
a1_cache_gem = @a1.cache_file
|
|
assert_equal a1_cache_gem, fetcher.download(@a1, "http://gems.example.com")
|
|
assert_equal a1_url, fetcher.paths.last
|
|
assert File.exist?(a1_cache_gem)
|
|
end
|
|
|
|
def test_download_with_auth
|
|
a1_data = File.open @a1_gem, "rb", &:read
|
|
a1_url = "http://user:password@gems.example.com/gems/a-1.gem"
|
|
|
|
fetcher = fake_fetcher(a1_url, a1_data)
|
|
|
|
a1_cache_gem = @a1.cache_file
|
|
assert_equal a1_cache_gem, fetcher.download(@a1, "http://user:password@gems.example.com")
|
|
assert_equal a1_url, fetcher.paths.last
|
|
assert File.exist?(a1_cache_gem)
|
|
end
|
|
|
|
def test_download_with_token
|
|
a1_data = File.open @a1_gem, "rb", &:read
|
|
a1_url = "http://token@gems.example.com/gems/a-1.gem"
|
|
|
|
fetcher = fake_fetcher(a1_url, a1_data)
|
|
|
|
a1_cache_gem = @a1.cache_file
|
|
assert_equal a1_cache_gem, fetcher.download(@a1, "http://token@gems.example.com")
|
|
assert_equal a1_url, fetcher.paths.last
|
|
assert File.exist?(a1_cache_gem)
|
|
end
|
|
|
|
def test_download_with_x_oauth_basic
|
|
a1_data = File.open @a1_gem, "rb", &:read
|
|
a1_url = "http://token:x-oauth-basic@gems.example.com/gems/a-1.gem"
|
|
|
|
fetcher = fake_fetcher(a1_url, a1_data)
|
|
|
|
a1_cache_gem = @a1.cache_file
|
|
assert_equal a1_cache_gem, fetcher.download(@a1, "http://token:x-oauth-basic@gems.example.com")
|
|
assert_equal a1_url, fetcher.paths.last
|
|
assert File.exist?(a1_cache_gem)
|
|
end
|
|
|
|
def test_download_with_encoded_auth
|
|
a1_data = File.open @a1_gem, "rb", &:read
|
|
a1_url = "http://user:%25pas%25sword@gems.example.com/gems/a-1.gem"
|
|
|
|
fetcher = fake_fetcher(a1_url, a1_data)
|
|
|
|
a1_cache_gem = @a1.cache_file
|
|
assert_equal a1_cache_gem, fetcher.download(@a1, "http://user:%25pas%25sword@gems.example.com")
|
|
assert_equal a1_url, fetcher.paths.last
|
|
assert File.exist?(a1_cache_gem)
|
|
end
|
|
|
|
def test_download_cached
|
|
FileUtils.mv @a1_gem, @cache_dir
|
|
|
|
inst = Gem::RemoteFetcher.fetcher
|
|
|
|
assert_equal @a1.cache_file, inst.download(@a1, "http://gems.example.com")
|
|
end
|
|
|
|
def test_download_local
|
|
omit "doesn't work if tempdir has +" if @tempdir.include?("+")
|
|
FileUtils.mv @a1_gem, @tempdir
|
|
local_path = File.join @tempdir, @a1.file_name
|
|
inst = nil
|
|
|
|
Dir.chdir @tempdir do
|
|
inst = Gem::RemoteFetcher.fetcher
|
|
end
|
|
|
|
assert_equal @a1.cache_file, inst.download(@a1, local_path)
|
|
end
|
|
|
|
def test_download_local_space
|
|
omit "doesn't work if tempdir has +" if @tempdir.include?("+")
|
|
space_path = File.join @tempdir, "space path"
|
|
FileUtils.mkdir space_path
|
|
FileUtils.mv @a1_gem, space_path
|
|
local_path = File.join space_path, @a1.file_name
|
|
inst = nil
|
|
|
|
Dir.chdir @tempdir do
|
|
inst = Gem::RemoteFetcher.fetcher
|
|
end
|
|
|
|
assert_equal @a1.cache_file, inst.download(@a1, local_path)
|
|
end
|
|
|
|
def test_download_install_dir
|
|
a1_data = File.open @a1_gem, "rb", &:read
|
|
a1_url = "http://gems.example.com/gems/a-1.gem"
|
|
|
|
fetcher = fake_fetcher(a1_url, a1_data)
|
|
|
|
install_dir = File.join @tempdir, "more_gems"
|
|
|
|
a1_cache_gem = File.join install_dir, "cache", @a1.file_name
|
|
FileUtils.mkdir_p(File.dirname(a1_cache_gem))
|
|
actual = fetcher.download(@a1, "http://gems.example.com", install_dir)
|
|
|
|
assert_equal a1_cache_gem, actual
|
|
assert_equal a1_url, fetcher.paths.last
|
|
|
|
assert File.exist?(a1_cache_gem)
|
|
end
|
|
|
|
unless Gem.win_platform? || Process.uid.zero? # File.chmod doesn't work
|
|
def test_download_local_read_only
|
|
omit "doesn't work if tempdir has +" if @tempdir.include?("+")
|
|
FileUtils.mv @a1_gem, @tempdir
|
|
local_path = File.join @tempdir, @a1.file_name
|
|
inst = nil
|
|
FileUtils.chmod 0o555, @a1.cache_dir
|
|
begin
|
|
FileUtils.mkdir_p File.join(Gem.user_dir, "cache")
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
FileUtils.chmod 0o555, File.join(Gem.user_dir, "cache")
|
|
|
|
Dir.chdir @tempdir do
|
|
inst = Gem::RemoteFetcher.fetcher
|
|
end
|
|
|
|
assert_equal(File.join(@tempdir, @a1.file_name),
|
|
inst.download(@a1, local_path))
|
|
ensure
|
|
if local_path
|
|
FileUtils.chmod 0o755, File.join(Gem.user_dir, "cache")
|
|
FileUtils.chmod 0o755, @a1.cache_dir
|
|
end
|
|
end
|
|
|
|
def test_download_read_only
|
|
FileUtils.chmod 0o555, @a1.cache_dir
|
|
FileUtils.chmod 0o555, @gemhome
|
|
|
|
fetcher = Gem::RemoteFetcher.fetcher
|
|
def fetcher.fetch_path(uri, *rest)
|
|
File.read File.join(@test_gem_dir, "a-1.gem")
|
|
end
|
|
fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem))
|
|
|
|
fetcher.download(@a1, "http://gems.example.com")
|
|
a1_cache_gem = File.join Gem.user_dir, "cache", @a1.file_name
|
|
assert File.exist? a1_cache_gem
|
|
ensure
|
|
FileUtils.chmod 0o755, @gemhome
|
|
FileUtils.chmod 0o755, @a1.cache_dir
|
|
end
|
|
end
|
|
|
|
def test_download_platform_legacy
|
|
original_platform = "old-platform"
|
|
|
|
e1, e1_gem = util_gem "e", "1" do |s|
|
|
s.platform = Gem::Platform::CURRENT
|
|
s.instance_variable_set :@original_platform, original_platform
|
|
end
|
|
e1.loaded_from = File.join(@gemhome, "specifications", e1.full_name)
|
|
|
|
e1_data = File.open e1_gem, "rb", &:read
|
|
|
|
fetcher = Gem::RemoteFetcher.fetcher
|
|
def fetcher.fetch_path(uri, *rest)
|
|
@call_count ||= 0
|
|
@call_count += 1
|
|
raise Gem::RemoteFetcher::FetchError.new("error", uri) if @call_count == 1
|
|
@test_data
|
|
end
|
|
fetcher.instance_variable_set(:@test_data, e1_data)
|
|
|
|
e1_cache_gem = e1.cache_file
|
|
|
|
assert_equal e1_cache_gem, fetcher.download(e1, "http://gems.example.com")
|
|
|
|
assert File.exist?(e1_cache_gem)
|
|
end
|
|
|
|
def test_download_same_file
|
|
omit "doesn't work if tempdir has +" if @tempdir.include?("+")
|
|
FileUtils.mv @a1_gem, @tempdir
|
|
local_path = File.join @tempdir, @a1.file_name
|
|
inst = nil
|
|
|
|
Dir.chdir @tempdir do
|
|
inst = Gem::RemoteFetcher.fetcher
|
|
end
|
|
|
|
cache_path = @a1.cache_file
|
|
FileUtils.mv local_path, cache_path
|
|
|
|
gem = Gem::Package.new cache_path
|
|
|
|
assert_equal cache_path, inst.download(gem.spec, cache_path)
|
|
end
|
|
|
|
def test_download_unsupported
|
|
inst = Gem::RemoteFetcher.fetcher
|
|
|
|
e = assert_raise ArgumentError do
|
|
inst.download @a1, "ftp://gems.rubyforge.org"
|
|
end
|
|
|
|
assert_equal "unsupported URI scheme ftp", e.message
|
|
end
|
|
|
|
def test_download_to_cache
|
|
@a2, @a2_gem = util_gem "a", "2"
|
|
|
|
util_setup_spec_fetcher @a1, @a2
|
|
@fetcher.instance_variable_set :@a1, @a1
|
|
@fetcher.instance_variable_set :@a2, @a2
|
|
def @fetcher.fetch_path(uri, mtime = nil, head = false)
|
|
case uri.request_uri
|
|
when /#{@a1.spec_name}/ then
|
|
Gem.deflate Marshal.dump @a1
|
|
when /#{@a2.spec_name}/ then
|
|
Gem.deflate Marshal.dump @a2
|
|
else
|
|
uri.to_s
|
|
end
|
|
end
|
|
|
|
gem = Gem::RemoteFetcher.fetcher.download_to_cache dep "a"
|
|
|
|
assert_equal @a2.file_name, File.basename(gem)
|
|
end
|
|
|
|
def test_fetch_path_gzip
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime, head = nil)
|
|
Gem::Util.gzip "foo"
|
|
end
|
|
|
|
assert_equal "foo", fetcher.fetch_path(@uri + "foo.gz")
|
|
end
|
|
|
|
def test_fetch_path_gzip_unmodified
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime, head = nil)
|
|
nil
|
|
end
|
|
|
|
assert_nil fetcher.fetch_path(@uri + "foo.gz", Time.at(0))
|
|
end
|
|
|
|
def test_fetch_path_io_error
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(*)
|
|
raise EOFError
|
|
end
|
|
|
|
url = "http://example.com/uri"
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_path url
|
|
end
|
|
|
|
assert_equal "EOFError: EOFError (#{url})", e.message
|
|
assert_equal url, e.uri
|
|
end
|
|
|
|
def test_fetch_path_socket_error
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime, head = nil)
|
|
raise SocketError
|
|
end
|
|
|
|
url = "http://example.com/uri"
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_path url
|
|
end
|
|
|
|
assert_equal "SocketError: SocketError (#{url})", e.message
|
|
assert_equal url, e.uri
|
|
end
|
|
|
|
def test_fetch_path_system_call_error
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime = nil, head = nil)
|
|
raise Errno::ECONNREFUSED, "connect(2)"
|
|
end
|
|
|
|
url = "http://example.com/uri"
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_path url
|
|
end
|
|
|
|
assert_match(/ECONNREFUSED:.*connect\(2\) \(#{Regexp.escape url}\)\z/,
|
|
e.message)
|
|
assert_equal url, e.uri
|
|
end
|
|
|
|
def test_fetch_path_timeout_error
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime = nil, head = nil)
|
|
raise Gem::Timeout::Error, "timed out"
|
|
end
|
|
|
|
url = "http://example.com/uri"
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_path url
|
|
end
|
|
|
|
assert_match(/Gem::Timeout::Error: timed out \(#{Regexp.escape url}\)\z/,
|
|
e.message)
|
|
assert_equal url, e.uri
|
|
end
|
|
|
|
def test_fetch_path_getaddrinfo_error
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime = nil, head = nil)
|
|
raise SocketError, "getaddrinfo: nodename nor servname provided"
|
|
end
|
|
|
|
url = "http://example.com/uri"
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_path url
|
|
end
|
|
|
|
assert_match(/SocketError: getaddrinfo: nodename nor servname provided \(#{Regexp.escape url}\)\z/,
|
|
e.message)
|
|
assert_equal url, e.uri
|
|
end
|
|
|
|
def test_fetch_path_openssl_ssl_sslerror
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime = nil, head = nil)
|
|
raise OpenSSL::SSL::SSLError
|
|
end
|
|
|
|
url = "http://example.com/uri"
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_path url
|
|
end
|
|
|
|
assert_equal "OpenSSL::SSL::SSLError: OpenSSL::SSL::SSLError (#{url})", e.message
|
|
assert_equal url, e.uri
|
|
end
|
|
|
|
def test_fetch_path_unmodified
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
def fetcher.fetch_http(uri, mtime, head = nil)
|
|
nil
|
|
end
|
|
|
|
assert_nil fetcher.fetch_path(Gem::URI.parse(@gem_repo), Time.at(0))
|
|
end
|
|
|
|
def test_fetch_http
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
url = "http://gems.example.com/redirect"
|
|
|
|
def fetcher.request(uri, request_class, last_modified = nil)
|
|
url = "http://gems.example.com/redirect"
|
|
if defined? @requested
|
|
res = Gem::Net::HTTPOK.new nil, 200, nil
|
|
def res.body
|
|
"real_path"
|
|
end
|
|
else
|
|
@requested = true
|
|
res = Gem::Net::HTTPMovedPermanently.new nil, 301, nil
|
|
res.add_field "Location", url
|
|
end
|
|
res
|
|
end
|
|
|
|
data = fetcher.fetch_http Gem::URI.parse(url)
|
|
|
|
assert_equal "real_path", data
|
|
end
|
|
|
|
def test_fetch_http_redirects
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
url = "http://gems.example.com/redirect"
|
|
|
|
def fetcher.request(uri, request_class, last_modified = nil)
|
|
url = "http://gems.example.com/redirect"
|
|
res = Gem::Net::HTTPMovedPermanently.new nil, 301, nil
|
|
res.add_field "Location", url
|
|
res
|
|
end
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_http Gem::URI.parse(url)
|
|
end
|
|
|
|
assert_equal "too many redirects (#{url})", e.message
|
|
end
|
|
|
|
def test_fetch_http_redirects_without_location
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
url = "http://gems.example.com/redirect"
|
|
|
|
def fetcher.request(uri, request_class, last_modified = nil)
|
|
res = Gem::Net::HTTPMovedPermanently.new nil, 301, nil
|
|
res
|
|
end
|
|
|
|
e = assert_raise Gem::RemoteFetcher::FetchError do
|
|
fetcher.fetch_http Gem::URI.parse(url)
|
|
end
|
|
|
|
assert_equal "redirecting but no redirect location was given (#{url})", e.message
|
|
end
|
|
|
|
def test_request_block
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
|
|
assert_throws :block_called do
|
|
fetcher.request Gem::URI("http://example"), Gem::Net::HTTP::Get do |req|
|
|
assert_kind_of Gem::Net::HTTPGenericRequest, req
|
|
throw :block_called
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_yaml_error_on_size
|
|
use_ui @stub_ui do
|
|
fetcher = Gem::RemoteFetcher.new nil
|
|
@fetcher = fetcher
|
|
assert_error { fetcher.size }
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def assert_error(exception_class = Exception)
|
|
got_exception = false
|
|
|
|
begin
|
|
yield
|
|
rescue exception_class
|
|
got_exception = true
|
|
end
|
|
|
|
assert got_exception, "Expected exception conforming to #{exception_class}"
|
|
end
|
|
|
|
def fake_fetcher(url, data)
|
|
original_fetcher = Gem::RemoteFetcher.fetcher
|
|
fetcher = Gem::FakeFetcher.new
|
|
fetcher.data[url] = data
|
|
Gem::RemoteFetcher.fetcher = fetcher
|
|
ensure
|
|
Gem::RemoteFetcher.fetcher = original_fetcher
|
|
end
|
|
end
|