diff options
Diffstat (limited to 'test/rubygems')
85 files changed, 5614 insertions, 3158 deletions
diff --git a/test/rubygems/coverage_setup.rb b/test/rubygems/coverage_setup.rb new file mode 100644 index 0000000000..7e978e59e0 --- /dev/null +++ b/test/rubygems/coverage_setup.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# This file is loaded via -r flag BEFORE rubygems to enable coverage tracking +# of rubygems boot files. It must be used with --disable-gems and -Ilib +# so that Coverage.start runs before rubygems is loaded. + +require "coverage" +Coverage.start(lines: true) +require "rubygems" diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index eaf3e7037e..2411dbc649 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -3,6 +3,32 @@ require "rubygems" begin + raise LoadError if ENV["GEM_COMMAND"] + + gem "simplecov_json_formatter" + require "simplecov" + + unless ENV["SIMPLECOV_SUBPROCESS"] + SimpleCov.start do + command_name "rubygems" + root File.expand_path("../..", __dir__) + coverage_dir File.expand_path("../../coverage", __dir__) + + add_filter "/test/" + add_filter "/bundler/" + add_filter "/tool/" + add_filter "/lib/rubygems/vendor/" + add_filter ".gemspec" + end + + # Prevent SimpleCov from running in subprocesses spawned by assert_separately + ENV["SIMPLECOV_SUBPROCESS"] = "1" + end +rescue LoadError + # SimpleCov is not installed +end + +begin gem "test-unit", "~> 3.0" rescue Gem::LoadError end @@ -12,6 +38,7 @@ require "test/unit" require "fileutils" require "pathname" require "pp" +require "rubygems/installer" require "rubygems/package" require "shellwords" require "tmpdir" @@ -19,6 +46,24 @@ require "rubygems/vendor/uri/lib/uri" require "zlib" require_relative "mock_gem_ui" +# JRuby on Windows raises TypeError inside File.symlink (the wincode helper +# trips on a nil path), so any test that exercises Gem::Installer's symlink +# branch fails to even install the gem. Real users hit the wrapper branch via +# `gem install` (DependencyInstaller passes wrappers: true), so mirror that +# default for direct Gem::Installer.at callers in the test suite. +if Gem.win_platform? && Gem.java_platform? + module Gem::InstallerDefaultWrappersOnJRubyWindows + def at(path, options = {}) + super(path, { wrappers: true }.merge(options)) + end + + def for_spec(spec, options = {}) + super(spec, { wrappers: true }.merge(options)) + end + end + Gem::Installer.singleton_class.prepend(Gem::InstallerDefaultWrappersOnJRubyWindows) +end + module Gem ## # Allows setting the gem path searcher. @@ -60,6 +105,44 @@ class Gem::Command end end +class Gem::Installer + # Copy from Gem::Installer#install with install_as_default option from old version + def install_default_gem + pre_install_checks + + run_pre_install_hooks + + spec.loaded_from = default_spec_file + + FileUtils.rm_rf gem_dir + FileUtils.rm_rf spec.extension_dir + + dir_mode = options[:dir_mode] + FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755 + + extract_bin + write_default_spec + + generate_bin + generate_plugins + + File.chmod(dir_mode, gem_dir) if dir_mode + + say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? + + Gem::Specification.add_spec(spec) + + load_plugin + + run_post_install_hooks + + spec + rescue Errno::EACCES => e + # Permission denied - /path/to/foo + raise Gem::FilePermissionError, e.message.split(" - ").last + end +end + ## # RubyGemTestCase provides a variety of methods for testing rubygems and # gem-related behavior in a sandbox. Through RubyGemTestCase you can install @@ -295,8 +378,12 @@ class Gem::TestCase < Test::Unit::TestCase ENV["XDG_CONFIG_HOME"] = nil ENV["XDG_DATA_HOME"] = nil ENV["XDG_STATE_HOME"] = nil + ENV["MAKEFLAGS"] = nil ENV["SOURCE_DATE_EPOCH"] = nil ENV["BUNDLER_VERSION"] = nil + ENV["BUNDLE_CONFIG"] = nil + ENV["BUNDLE_USER_CONFIG"] = nil + ENV["BUNDLE_USER_HOME"] = nil ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = "true" @current_dir = Dir.pwd @@ -400,8 +487,9 @@ class Gem::TestCase < Test::Unit::TestCase Gem::RemoteFetcher.fetcher = Gem::FakeFetcher.new @gem_repo = "http://gems.example.com/" + Gem.instance_variable_set :@default_sources, [@gem_repo] + Gem.instance_variable_set :@sources, nil @uri = Gem::URI.parse @gem_repo - Gem.sources.replace [@gem_repo] Gem.searcher = nil Gem::SpecFetcher.fetcher = nil @@ -418,6 +506,9 @@ class Gem::TestCase < Test::Unit::TestCase @orig_hooks[name] = Gem.send(name).dup end + Gem::Platform.const_get(:GENERIC_CACHE).clear + Gem::Platform.const_get(:GENERICS).each {|g| Gem::Platform.const_get(:GENERIC_CACHE)[g] = g } + @marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" @orig_loaded_features = $LOADED_FEATURES.dup end @@ -680,15 +771,19 @@ class Gem::TestCase < Test::Unit::TestCase path end + def write_dummy_extconf(gem_name) + write_file File.join(@tempdir, "extconf.rb") do |io| + io.puts "require 'mkmf'" + yield io if block_given? + io.puts "create_makefile '#{gem_name}'" + end + end + ## - # Load a YAML string, the psych 3 way + # Load a YAML string using the safe loader with gem-spec permitted classes. def load_yaml(yaml) - if Psych.respond_to?(:unsafe_load) - Psych.unsafe_load(yaml) - else - Psych.load(yaml) - end + Gem::SafeYAML.safe_load(yaml) end ## @@ -713,7 +808,7 @@ class Gem::TestCase < Test::Unit::TestCase # # Use this with #write_file to build an installed gem. - def quick_gem(name, version="2") + def quick_gem(name, version = "2") require "rubygems/specification" spec = Gem::Specification.new do |s| @@ -799,8 +894,8 @@ class Gem::TestCase < Test::Unit::TestCase def install_default_gems(*specs) specs.each do |spec| - installer = Gem::Installer.for_spec(spec, install_as_default: true) - installer.install + installer = Gem::Installer.for_spec(spec) + installer.install_default_gem Gem.register_default_spec(spec) end end @@ -1022,7 +1117,7 @@ Also, a list: # Add +spec+ to +@fetcher+ serving the data in the file +path+. # +repo+ indicates which repo to make +spec+ appear to be in. - def add_to_fetcher(spec, path=nil, repo=@gem_repo) + def add_to_fetcher(spec, path = nil, repo = @gem_repo) path ||= spec.cache_file @fetcher.data["#{@gem_repo}gems/#{spec.file_name}"] = read_binary(path) end @@ -1184,6 +1279,26 @@ Also, a list: system("nmake /? 1>NUL 2>&1") end + @@symlink_supported = nil + + # This is needed for Windows environment without symlink support enabled (the default + # for non admin) to be able to skip test for features using symlinks. + def symlink_supported? + if @@symlink_supported.nil? + begin + File.symlink(File.join(@tempdir, "a"), File.join(@tempdir, "b")) + File.readlink(File.join(@tempdir, "b")) + rescue NotImplementedError, SystemCallError + @@symlink_supported = false + else + @@symlink_supported = true + ensure + File.unlink(File.join(@tempdir, "b")) if File.symlink?(File.join(@tempdir, "b")) + end + end + @@symlink_supported + end + # In case we're building docs in a background process, this method waits for # that process to exit (or if it's already been reaped, or never happened, # swallows the Errno::ECHILD error). @@ -1195,7 +1310,7 @@ Also, a list: ## # Allows the proper version of +rake+ to be used for the test. - def build_rake_in(good=true) + def build_rake_in(good = true) gem_ruby = Gem.ruby Gem.ruby = self.class.rubybin env_rake = ENV["rake"] @@ -1567,3 +1682,9 @@ class Object end require_relative "utilities" + +# mise installed rubygems_plugin.rb to system wide `site_ruby` directory. +# This empty module avoid to call `mise` command. +module ReshimInstaller + def self.reshim; end +end diff --git a/test/rubygems/installer_test_case.rb b/test/rubygems/installer_test_case.rb index 8a34d28db8..9e0cbf9c69 100644 --- a/test/rubygems/installer_test_case.rb +++ b/test/rubygems/installer_test_case.rb @@ -215,26 +215,26 @@ class Gem::InstallerTestCase < Gem::TestCase ## # Creates an installer for +spec+ that will install into +gem_home+. - def util_installer(spec, gem_home, force=true) + def util_installer(spec, gem_home, force = true) Gem::Installer.at(spec.cache_file, install_dir: gem_home, force: force) end - @@symlink_supported = nil - - # This is needed for Windows environment without symlink support enabled (the default - # for non admin) to be able to skip test for features using symlinks. - def symlink_supported? - if @@symlink_supported.nil? - begin - File.symlink("", "") - rescue Errno::ENOENT, Errno::EEXIST - @@symlink_supported = true - rescue NotImplementedError, SystemCallError - @@symlink_supported = false - end + def test_ensure_writable_dir_creates_missing_parent_directories + installer = setup_base_installer(false) + + non_existent_parent = File.join(@tempdir, "non_existent_parent") + target_dir = File.join(non_existent_parent, "target_dir") + + refute_directory_exists non_existent_parent, "Parent directory should not exist yet" + refute_directory_exists target_dir, "Target directory should not exist yet" + + assert_nothing_raised do + installer.send(:ensure_writable_dir, target_dir) end - @@symlink_supported + + assert_directory_exists non_existent_parent, "Parent directory should exist now" + assert_directory_exists target_dir, "Target directory should exist now" end end diff --git a/test/rubygems/mock_gem_ui.rb b/test/rubygems/mock_gem_ui.rb index 218d4b6965..fb804c5555 100644 --- a/test/rubygems/mock_gem_ui.rb +++ b/test/rubygems/mock_gem_ui.rb @@ -77,7 +77,7 @@ class Gem::MockGemUi < Gem::StreamUI @terminated end - def terminate_interaction(status=0) + def terminate_interaction(status = 0) @terminated = true raise TermError, status if status != 0 diff --git a/test/rubygems/package/tar_test_case.rb b/test/rubygems/package/tar_test_case.rb index e3d812bf3f..26135cf296 100644 --- a/test/rubygems/package/tar_test_case.rb +++ b/test/rubygems/package/tar_test_case.rb @@ -6,23 +6,7 @@ require "rubygems/package" ## # A test case for Gem::Package::Tar* classes -class Gem::Package::TarTestCase < Gem::TestCase - def ASCIIZ(str, length) - str + "\0" * (length - str.length) - end - - def SP(s) - s + " " - end - - def SP_Z(s) - s + " \0" - end - - def Z(s) - s + "\0" - end - +module Gem::Package::TarTestMethods def assert_headers_equal(expected, actual) expected = expected.to_s unless String === expected actual = actual.to_s unless String === actual @@ -66,6 +50,26 @@ class Gem::Package::TarTestCase < Gem::TestCase assert_equal expected[chksum_off, 8], actual[chksum_off, 8] end +end + +class Gem::Package::TarTestCase < Gem::TestCase + include Gem::Package::TarTestMethods + + def ASCIIZ(str, length) + str + "\0" * (length - str.length) + end + + def SP(s) + s + " " + end + + def SP_Z(s) + s + " \0" + end + + def Z(s) + s + "\0" + end def calc_checksum(header) sum = header.sum(0) diff --git a/test/rubygems/test_bundled_ca.rb b/test/rubygems/test_bundled_ca.rb index a737185681..cc8fa884ca 100644 --- a/test/rubygems/test_bundled_ca.rb +++ b/test/rubygems/test_bundled_ca.rb @@ -12,7 +12,7 @@ require "rubygems/request" # = Testing Bundled CA # -# The tested hosts are explained in detail here: https://github.com/rubygems/rubygems/commit/5e16a5428f973667cabfa07e94ff939e7a83ebd9 +# The tested hosts are explained in detail here: https://github.com/ruby/rubygems/commit/5e16a5428f973667cabfa07e94ff939e7a83ebd9 # class TestGemBundledCA < Gem::TestCase diff --git a/test/rubygems/test_config.rb b/test/rubygems/test_config.rb index 657624d526..822b57b0dc 100644 --- a/test/rubygems/test_config.rb +++ b/test/rubygems/test_config.rb @@ -5,13 +5,6 @@ require "rubygems" require "shellwords" class TestGemConfig < Gem::TestCase - def test_datadir - util_make_gems - spec = Gem::Specification.find_by_name("a") - spec.activate - assert_equal "#{spec.full_gem_path}/data/a", spec.datadir - end - def test_good_rake_path_is_escaped path = Gem::TestCase.class_variable_get(:@@good_rake) ruby, rake = path.shellsplit diff --git a/test/rubygems/test_gem.rb b/test/rubygems/test_gem.rb index cdc3479e37..c81b0b0547 100644 --- a/test/rubygems/test_gem.rb +++ b/test/rubygems/test_gem.rb @@ -150,6 +150,8 @@ class TestGem < Gem::TestCase end def assert_self_install_permissions(format_executable: false, data_mode: 0o640) + omit "FileUtils.install signature differs on JRuby/Windows" if Gem.win_platform? && Gem.java_platform? + mask = Gem.win_platform? ? 0o700 : 0o777 options = { dir_mode: 0o500, @@ -199,7 +201,8 @@ class TestGem < Gem::TestCase end assert_equal(expected, result) ensure - File.chmod(0o755, *Dir.glob(@gemhome + "/gems/**/")) + files = Dir.glob(@gemhome + "/gems/**/") + File.chmod(0o755, *files) unless files.empty? end def test_require_missing @@ -310,7 +313,7 @@ class TestGem < Gem::TestCase assert_equal %w[a-1 b-2 c-2], loaded_spec_names end - def test_activate_bin_path_raises_a_meaningful_error_if_a_gem_thats_finally_activated_has_orphaned_dependencies + def test_activate_bin_path_backtracks_when_highest_version_has_orphaned_dependencies a1 = util_spec "a", "1" do |s| s.executables = ["exec"] s.add_dependency "b" @@ -328,13 +331,11 @@ class TestGem < Gem::TestCase install_specs c1, b1, b2, a1 - # c2 is missing, and b2 which has it as a dependency will be activated, so we should get an error about the orphaned dependency - - e = assert_raise Gem::UnsatisfiableDependencyError do - load Gem.activate_bin_path("a", "exec", ">= 0") - end + # c2 is missing, but the resolver backtracks from b2 to b1 which + # works with c1, finding a valid solution despite partial installation + load Gem.activate_bin_path("a", "exec", ">= 0") - assert_equal "Unable to resolve dependency: 'b (>= 0)' requires 'c (= 2)'", e.message + assert_equal %w[a-1 b-1 c-1], loaded_spec_names end def test_activate_bin_path_in_debug_mode @@ -527,35 +528,6 @@ class TestGem < Gem::TestCase assert_equal expected, Gem.configuration end - def test_self_datadir - foo = nil - - Dir.chdir @tempdir do - FileUtils.mkdir_p "data" - File.open File.join("data", "foo.txt"), "w" do |fp| - fp.puts "blah" - end - - foo = util_spec "foo" do |s| - s.files = %w[data/foo.txt] - end - - install_gem foo - end - - gem "foo" - - expected = File.join @gemhome, "gems", foo.full_name, "data", "foo" - - assert_equal expected, Gem::Specification.find_by_name("foo").datadir - end - - def test_self_datadir_nonexistent_package - assert_raise(Gem::MissingSpecError) do - Gem::Specification.find_by_name("xyzzy").datadir - end - end - def test_self_default_exec_format ruby_install_name "ruby" do assert_equal "%s", Gem.default_exec_format @@ -615,6 +587,7 @@ class TestGem < Gem::TestCase end def test_self_default_sources + Gem.remove_instance_variable :@default_sources assert_equal %w[https://rubygems.org/], Gem.default_sources end @@ -1227,6 +1200,8 @@ class TestGem < Gem::TestCase Gem.sources = nil Gem.configuration.sources = %w[http://test.example.com/] assert_equal %w[http://test.example.com/], Gem.sources + ensure + Gem.configuration.sources = nil end def test_try_activate_returns_true_for_activated_specs @@ -1239,6 +1214,28 @@ class TestGem < Gem::TestCase assert Gem.try_activate("b"), "try_activate should still return true" end + def test_try_activate_does_not_raise_no_method_error_on_activation_conflict + a1 = util_spec "a", "1.0" do |s| + s.files << "lib/a/old.rb" + end + + a2 = util_spec "a", "2.0" do |s| + s.files << "lib/a/old.rb" + s.files << "lib/a/new_file.rb" + end + + install_specs a1, a2 + + # Activate the older version + gem "a", "= 1.0" + + # try_activate a file only in the newer version should not raise + # NoMethodError on nil (https://bugs.ruby-lang.org/issues/21954) + assert_nothing_raised do + Gem.try_activate("a/new_file") + end + end + def test_spec_order_is_consistent b1 = util_spec "b", "1.0" b2 = util_spec "b", "2.0" @@ -1308,10 +1305,14 @@ class TestGem < Gem::TestCase refute Gem.try_activate "nonexistent" end - expected = "Ignoring ext-1 because its extensions are not built. " \ - "Try: gem pristine ext --version 1\n" + if RUBY_ENGINE == "jruby" + assert_equal "", err + else + expected = "Ignoring ext-1 because its extensions are not built. " \ + "Try: gem pristine ext --version 1\n" - assert_equal expected, err + assert_equal expected, err + end end def test_self_use_paths_with_nils @@ -1659,6 +1660,27 @@ class TestGem < Gem::TestCase assert_nil Gem.find_unresolved_default_spec("README") end + def test_register_default_spec_new_style_with_native_extension + Gem.clear_default_specs + + dlext = RbConfig::CONFIG["DLEXT"] + + new_style = Gem::Specification.new do |spec| + spec.name = "my_ext" + spec.version = "1.0" + spec.files = ["lib/my_ext.rb", "my_ext_core.#{dlext}", "ext/my_ext/my_ext_core.c", "README.md"] + spec.require_paths = ["lib"] + end + + Gem.register_default_spec new_style + + assert_equal new_style, Gem.find_unresolved_default_spec("my_ext.rb") + assert_equal new_style, Gem.find_unresolved_default_spec("my_ext_core") + assert_equal new_style, Gem.find_unresolved_default_spec("my_ext_core.#{dlext}") + assert_nil Gem.find_unresolved_default_spec("ext/my_ext/my_ext_core.c") + assert_nil Gem.find_unresolved_default_spec("README.md") + end + def test_register_default_spec_old_style_with_folder_starting_with_lib Gem.clear_default_specs diff --git a/test/rubygems/test_gem_bundler_version_finder.rb b/test/rubygems/test_gem_bundler_version_finder.rb index b72670b802..b5ef6293ab 100644 --- a/test/rubygems/test_gem_bundler_version_finder.rb +++ b/test/rubygems/test_gem_bundler_version_finder.rb @@ -2,6 +2,7 @@ require_relative "helper" require "rubygems/bundler_version_finder" +require "tempfile" class TestGemBundlerVersionFinder < Gem::TestCase def setup @@ -32,6 +33,11 @@ class TestGemBundlerVersionFinder < Gem::TestCase assert_equal v("1.1.1.1"), bvf.bundler_version end + def test_bundler_version_with_empty_env_var + ENV["BUNDLER_VERSION"] = "" + assert_nil bvf.bundler_version + end + def test_bundler_version_with_bundle_update_bundler ARGV.replace %w[update --bundler] assert_nil bvf.bundler_version @@ -51,6 +57,157 @@ class TestGemBundlerVersionFinder < Gem::TestCase assert_nil bvf.bundler_version end + def test_bundler_version_with_bundle_config + config_content = <<~CONFIG + BUNDLE_VERSION: "system" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + assert_nil bvf.bundler_version + end + end + end + + def test_bundler_version_with_bundle_config_single_quoted + config_with_single_quoted_version = <<~CONFIG + BUNDLE_VERSION: 'system' + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_with_single_quoted_version) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + assert_nil bvf.bundler_version + end + end + end + + def test_bundler_version_with_bundle_config_version + ENV["BUNDLER_VERSION"] = "1.1.1.1" + + config_content = <<~CONFIG + BUNDLE_VERSION: "1.2.3" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + assert_equal v("1.1.1.1"), bvf.bundler_version + end + end + end + + def test_bundler_version_with_bundle_version_env_system + ENV["BUNDLE_VERSION"] = "system" + + bvf.stub(:lockfile_contents, "\n\nBUNDLED WITH\n 1.1.1.1\n") do + assert_nil bvf.bundler_version + end + end + + def test_bundler_version_with_bundle_version_env_overrides_config + ENV["BUNDLE_VERSION"] = "2.3.4" + + config_content = <<~CONFIG + BUNDLE_VERSION: "1.2.3" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + assert_equal v("2.3.4"), bvf.bundler_version + end + end + end + + def test_bundler_version_with_empty_bundle_version_env + ENV["BUNDLE_VERSION"] = "" + + config_content = <<~CONFIG + BUNDLE_VERSION: "1.2.3" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + assert_equal v("1.2.3"), bvf.bundler_version + end + end + end + + def test_bundler_version_with_bundle_version_env_lockfile + ENV["BUNDLE_VERSION"] = "lockfile" + + bvf.stub(:lockfile_contents, "\n\nBUNDLED WITH\n 1.1.1.1\n") do + assert_equal v("1.1.1.1"), bvf.bundler_version + end + end + + def test_bundler_version_with_bundle_config_version_lockfile + config_content = <<~CONFIG + BUNDLE_VERSION: "lockfile" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + bvf.stub(:lockfile_contents, "\n\nBUNDLED WITH\n 1.1.1.1\n") do + assert_equal v("1.1.1.1"), bvf.bundler_version + end + end + end + end + + def test_bundler_version_with_bundle_config_non_existent_file + bvf.stub(:bundler_global_config_file, "/non/existent/path") do + assert_nil bvf.bundler_version + end + end + + def test_bundler_version_set_on_local_config + config_content = <<~CONFIG + BUNDLE_VERSION: "1.2.3" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_content) + f.flush + + bvf.stub(:bundler_local_config_file, f.path) do + assert_equal v("1.2.3"), bvf.bundler_version + end + end + end + + def test_bundler_version_with_bundle_config_without_version + config_without_version = <<~CONFIG + BUNDLE_JOBS: "8" + BUNDLE_GEM__TEST: "minitest" + CONFIG + + Tempfile.create("bundle_config") do |f| + f.write(config_without_version) + f.flush + + bvf.stub(:bundler_global_config_file, f.path) do + assert_nil bvf.bundler_version + end + end + end + def test_bundler_version_with_lockfile bvf.stub(:lockfile_contents, "") do assert_nil bvf.bundler_version @@ -82,7 +239,7 @@ class TestGemBundlerVersionFinder < Gem::TestCase def test_deleted_directory pend "Cannot perform this test on windows" if Gem.win_platform? - pend "Cannot perform this test on Solaris" if RUBY_PLATFORM.include?("solaris") + require "tmpdir" orig_dir = Dir.pwd diff --git a/test/rubygems/test_gem_command_manager.rb b/test/rubygems/test_gem_command_manager.rb index f3848e498d..889d5ce9e6 100644 --- a/test/rubygems/test_gem_command_manager.rb +++ b/test/rubygems/test_gem_command_manager.rb @@ -43,7 +43,7 @@ class TestGemCommandManager < Gem::TestCase assert_kind_of Gem::Commands::SigninCommand, command end - def test_find_logout_alias_comamnd + def test_find_logout_alias_command command = @command_manager.find_command "logout" assert_kind_of Gem::Commands::SignoutCommand, command @@ -78,7 +78,7 @@ class TestGemCommandManager < Gem::TestCase message = "Unknown command pish".dup - if defined?(DidYouMean::SPELL_CHECKERS) && defined?(DidYouMean::Correctable) + if e.respond_to?(:corrections) message << "\nDid you mean? \"push\"" end @@ -287,47 +287,6 @@ class TestGemCommandManager < Gem::TestCase assert_equal "foobar.rb", check_options[:args].first end - # HACK: move to query command test - def test_process_args_query - # capture all query options - check_options = nil - @command_manager["query"].when_invoked do |options| - check_options = options - true - end - - # check defaults - Gem::Deprecate.skip_during do - @command_manager.process_args %w[query] - end - assert_nil(check_options[:name]) - assert_equal :local, check_options[:domain] - assert_equal false, check_options[:details] - - # check settings - check_options = nil - Gem::Deprecate.skip_during do - @command_manager.process_args %w[query --name foobar --local --details] - end - assert_equal(/foobar/i, check_options[:name]) - assert_equal :local, check_options[:domain] - assert_equal true, check_options[:details] - - # remote domain - check_options = nil - Gem::Deprecate.skip_during do - @command_manager.process_args %w[query --remote] - end - assert_equal :remote, check_options[:domain] - - # both (local/remote) domains - check_options = nil - Gem::Deprecate.skip_during do - @command_manager.process_args %w[query --both] - end - assert_equal :both, check_options[:domain] - end - # HACK: move to update command test def test_process_args_update # capture all update options diff --git a/test/rubygems/test_gem_commands_build_command.rb b/test/rubygems/test_gem_commands_build_command.rb index d44126d204..9339f41f7c 100644 --- a/test/rubygems/test_gem_commands_build_command.rb +++ b/test/rubygems/test_gem_commands_build_command.rb @@ -43,16 +43,6 @@ class TestGemCommandsBuildCommand < Gem::TestCase assert_includes Gem.platforms, Gem::Platform.local end - def test_handle_deprecated_options - use_ui @ui do - @cmd.handle_options %w[-C ./test/dir] - end - - assert_equal "WARNING: The \"-C\" option has been deprecated and will be removed in Rubygems 4.0. " \ - "-C is a global flag now. Use `gem -C PATH build GEMSPEC_FILE [options]` instead\n", - @ui.error - end - def test_options_filename gemspec_file = File.join(@tempdir, @gem.spec_name) diff --git a/test/rubygems/test_gem_commands_cert_command.rb b/test/rubygems/test_gem_commands_cert_command.rb index c173467935..ed1a1c8627 100644 --- a/test/rubygems/test_gem_commands_cert_command.rb +++ b/test/rubygems/test_gem_commands_cert_command.rb @@ -31,14 +31,6 @@ class TestGemCommandsCertCommand < Gem::TestCase @cmd = Gem::Commands::CertCommand.new @trust_dir = Gem::Security.trust_dir - - @cleanup = [] - end - - def teardown - FileUtils.rm_f(@cleanup) - - super end def test_certificates_matching @@ -498,7 +490,7 @@ Removed '/CN=alternate/DC=example' assert_equal "/CN=nobody/DC=example", cert.issuer.to_s - mask = 0o100600 & (~File.umask) + mask = 0o100600 & ~File.umask assert_equal mask, File.stat(path).mode unless Gem.win_platform? end @@ -527,7 +519,7 @@ Removed '/CN=alternate/DC=example' assert_equal "/CN=nobody/DC=example", cert.issuer.to_s - mask = 0o100600 & (~File.umask) + mask = 0o100600 & ~File.umask assert_equal mask, File.stat(path).mode unless Gem.win_platform? end @@ -559,7 +551,7 @@ Removed '/CN=alternate/DC=example' assert_equal "/CN=nobody/DC=example", cert.issuer.to_s - mask = 0o100600 & (~File.umask) + mask = 0o100600 & ~File.umask assert_equal mask, File.stat(path).mode unless Gem.win_platform? end @@ -591,7 +583,7 @@ Removed '/CN=alternate/DC=example' assert_equal "/CN=nobody/DC=example", cert.issuer.to_s - mask = 0o100600 & (~File.umask) + mask = 0o100600 & ~File.umask assert_equal mask, File.stat(path).mode unless Gem.win_platform? end @@ -661,8 +653,7 @@ ERROR: --private-key not specified and ~/.gem/gem-private_key.pem does not exis assert_equal "/CN=nobody/DC=example", EXPIRED_PUBLIC_CERT.issuer.to_s - tmp_expired_cert_file = File.join(Dir.tmpdir, File.basename(EXPIRED_PUBLIC_CERT_FILE)) - @cleanup << tmp_expired_cert_file + tmp_expired_cert_file = File.join(@tempdir, File.basename(EXPIRED_PUBLIC_CERT_FILE)) File.write(tmp_expired_cert_file, File.read(EXPIRED_PUBLIC_CERT_FILE)) @cmd.handle_options %W[ @@ -694,8 +685,7 @@ ERROR: --private-key not specified and ~/.gem/gem-private_key.pem does not exis assert_equal "/CN=nobody/DC=example", EXPIRED_PUBLIC_CERT.issuer.to_s - tmp_expired_cert_file = File.join(Dir.tmpdir, File.basename(EXPIRED_PUBLIC_CERT_FILE)) - @cleanup << tmp_expired_cert_file + tmp_expired_cert_file = File.join(@tempdir, File.basename(EXPIRED_PUBLIC_CERT_FILE)) File.write(tmp_expired_cert_file, File.read(EXPIRED_PUBLIC_CERT_FILE)) @cmd.handle_options %W[ diff --git a/test/rubygems/test_gem_commands_environment_command.rb b/test/rubygems/test_gem_commands_environment_command.rb index 48252d84d4..e27de544c6 100644 --- a/test/rubygems/test_gem_commands_environment_command.rb +++ b/test/rubygems/test_gem_commands_environment_command.rb @@ -164,4 +164,8 @@ class TestGemCommandsEnvironmentCommand < Gem::TestCase assert_equal "#{Gem.platforms.join File::PATH_SEPARATOR}\n", @ui.output assert_equal "", @ui.error end + + def test_description_mentions_concurrent_downloads + assert_match(/:concurrent_downloads:/, @cmd.description) + end end diff --git a/test/rubygems/test_gem_commands_exec_command.rb b/test/rubygems/test_gem_commands_exec_command.rb index b9d5888068..b949cd34a6 100644 --- a/test/rubygems/test_gem_commands_exec_command.rb +++ b/test/rubygems/test_gem_commands_exec_command.rb @@ -370,8 +370,11 @@ class TestGemCommandsExecCommand < Gem::TestCase util_clear_gems use_ui @ui do - @cmd.invoke "a:2" - assert_equal "a-2 foo\n", @ui.output + e = assert_raise Gem::MockGemUi::TermError do + @cmd.invoke "a:2" + end + assert_equal 1, e.exit_code + assert_equal "ERROR: Ambiguous which executable from gem `a` should be run: the options are [\"bar\", \"foo\"], specify one via COMMAND, and use `-g` and `-v` to specify gem and version\n", @ui.error end end @@ -853,4 +856,33 @@ class TestGemCommandsExecCommand < Gem::TestCase assert_equal %w[a-1.1.a], @installed_specs.map(&:full_name) end end + + def test_install_dependency_resolution_error + spec_fetcher do |fetcher| + fetcher.gem "a", 2 do |s| + s.executables = %w[a] + s.add_dependency "b", "~> 1.0" + s.add_dependency "c", "~> 1.0" + end + fetcher.gem "b", 1 do |s| + s.add_dependency "d", "= 1.0" + end + fetcher.gem "c", 1 do |s| + s.add_dependency "d", "= 2.0" + end + fetcher.gem "d", 1 + fetcher.gem "d", 2 + end + + util_clear_gems + + use_ui @ui do + e = assert_raise Gem::MockGemUi::TermError do + @cmd.invoke "a:2" + end + assert_equal 2, e.exit_code + end + + assert_match(/ERROR:.*Error installing a:/, @ui.error) + end end diff --git a/test/rubygems/test_gem_commands_fetch_command.rb b/test/rubygems/test_gem_commands_fetch_command.rb index 84fad08fd6..e673e391fe 100644 --- a/test/rubygems/test_gem_commands_fetch_command.rb +++ b/test/rubygems/test_gem_commands_fetch_command.rb @@ -157,7 +157,7 @@ class TestGemCommandsFetchCommand < Gem::TestCase execute_with_term_error msg = "ERROR: Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:>=2'`" assert_empty @ui.output assert_equal msg, @ui.error.chomp diff --git a/test/rubygems/test_gem_commands_help_command.rb b/test/rubygems/test_gem_commands_help_command.rb index 01ab4aab2f..4ce7285d1f 100644 --- a/test/rubygems/test_gem_commands_help_command.rb +++ b/test/rubygems/test_gem_commands_help_command.rb @@ -36,7 +36,7 @@ class TestGemCommandsHelpCommand < Gem::TestCase def test_gem_help_build util_gem "build" do |out, err| - assert_match(/-C PATH *Run as if gem build was started in <PATH>/, out) + assert_match(/--platform PLATFORM\s+Specify the platform of gem to build/, out) assert_equal "", err end end diff --git a/test/rubygems/test_gem_commands_info_command.rb b/test/rubygems/test_gem_commands_info_command.rb index f020d380d2..dab7cfb836 100644 --- a/test/rubygems/test_gem_commands_info_command.rb +++ b/test/rubygems/test_gem_commands_info_command.rb @@ -13,7 +13,7 @@ class TestGemCommandsInfoCommand < Gem::TestCase def gem(name, version = "1.0") spec = quick_gem name do |gem| gem.summary = "test gem" - gem.homepage = "https://github.com/rubygems/rubygems" + gem.homepage = "https://github.com/ruby/rubygems" gem.files = %W[lib/#{name}.rb Rakefile] gem.authors = ["Colby", "Jack"] gem.license = "MIT" diff --git a/test/rubygems/test_gem_commands_install_command.rb b/test/rubygems/test_gem_commands_install_command.rb index 4e49f52b4c..d75ba349f9 100644 --- a/test/rubygems/test_gem_commands_install_command.rb +++ b/test/rubygems/test_gem_commands_install_command.rb @@ -119,11 +119,7 @@ class TestGemCommandsInstallCommand < Gem::TestCase end end - expected = <<-EXPECTED -ERROR: Could not find a valid gem 'bar' (= 0.5) (required by 'foo' (>= 0)) in any repository - EXPECTED - - assert_equal expected, @ui.error + assert_match(/ERROR:.*foo.*bar/m, @ui.error) end def test_execute_local_dependency_nonexistent_ignore_dependencies @@ -303,11 +299,7 @@ ERROR: Could not find a valid gem 'bar' (= 0.5) (required by 'foo' (>= 0)) in a assert_equal 2, e.exit_code end - expected = <<-EXPECTED -ERROR: Could not find a valid gem 'bar' (= 0.5) (required by 'foo' (>= 0)) in any repository - EXPECTED - - assert_equal expected, @ui.error + assert_match(/ERROR:.*foo.*bar/m, @ui.error) end def test_execute_http_proxy @@ -647,17 +639,10 @@ ERROR: Possible alternatives: non_existent_with_hint @cmd.options[:args] = %w[a] use_ui @ui do - # Don't use Dir.chdir with a block, it warnings a lot because - # of a downstream Dir.chdir with a block - old = Dir.getwd - - begin - Dir.chdir @tempdir + Dir.chdir @tempdir do assert_raise Gem::MockGemUi::SystemExitException, @ui.error do @cmd.execute end - ensure - Dir.chdir old end end @@ -684,17 +669,10 @@ ERROR: Possible alternatives: non_existent_with_hint @cmd.options[:args] = %w[a] use_ui @ui do - # Don't use Dir.chdir with a block, it warnings a lot because - # of a downstream Dir.chdir with a block - old = Dir.getwd - - begin - Dir.chdir @tempdir + Dir.chdir @tempdir do assert_raise Gem::MockGemUi::SystemExitException, @ui.error do @cmd.execute end - ensure - Dir.chdir old end end @@ -720,17 +698,10 @@ ERROR: Possible alternatives: non_existent_with_hint @cmd.options[:args] = %w[a] use_ui @ui do - # Don't use Dir.chdir with a block, it warnings a lot because - # of a downstream Dir.chdir with a block - old = Dir.getwd - - begin - Dir.chdir @tempdir + Dir.chdir @tempdir do assert_raise Gem::MockGemUi::SystemExitException, @ui.error do @cmd.execute end - ensure - Dir.chdir old end end @@ -901,7 +872,7 @@ ERROR: Possible alternatives: non_existent_with_hint assert_empty @cmd.installed_specs msg = "ERROR: Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:>=2'`" assert_empty @ui.output assert_equal msg, @ui.error.chomp @@ -1005,6 +976,38 @@ ERROR: Possible alternatives: non_existent_with_hint assert_equal %W[a-3-#{local}], @cmd.installed_specs.map(&:full_name) end + def test_install_gem_platform_specificity_match + util_set_arch "arm64-darwin-20" + + spec_fetcher do |fetcher| + %w[ruby universal-darwin universal-darwin-20 x64-darwin-20 arm64-darwin-20].each do |platform| + fetcher.download "a", 3 do |s| + s.platform = platform + end + end + end + + @cmd.install_gem "a", ">= 0" + + assert_equal %w[a-3-arm64-darwin-20], @cmd.installed_specs.map(&:full_name) + end + + def test_install_gem_platform_specificity_match_reverse_order + util_set_arch "arm64-darwin-20" + + spec_fetcher do |fetcher| + %w[ruby universal-darwin universal-darwin-20 x64-darwin-20 arm64-darwin-20].reverse_each do |platform| + fetcher.download "a", 3 do |s| + s.platform = platform + end + end + end + + @cmd.install_gem "a", ">= 0" + + assert_equal %w[a-3-arm64-darwin-20], @cmd.installed_specs.map(&:full_name) + end + def test_install_gem_ignore_dependencies_specific_file spec = util_spec "a", 2 @@ -1214,6 +1217,30 @@ ERROR: Possible alternatives: non_existent_with_hint assert_match "Installing a (2)", @ui.output end + def test_execute_installs_from_a_gemdeps_with_prerelease + spec_fetcher do |fetcher| + fetcher.download "a", 1 + fetcher.download "a", "2.a" + end + + File.open @gemdeps, "w" do |f| + f << "gem 'a'" + end + + @cmd.handle_options %w[--prerelease] + @cmd.options[:gemdeps] = @gemdeps + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_equal %w[a-2.a], @cmd.installed_specs.map(&:full_name) + + assert_match "Installing a (2.a)", @ui.output + end + def test_execute_installs_deps_a_gemdeps spec_fetcher do |fetcher| fetcher.download "q", "1.0" @@ -1548,4 +1575,63 @@ ERROR: Possible alternatives: non_existent_with_hint assert_includes @ui.output, "A new release of RubyGems is available: 1.2.3 → 2.0.0!" end end + + def test_pass_down_the_job_option_to_make + gemspec = nil + + spec_fetcher do |fetcher| + fetcher.gem "a", 2 do |spec| + gemspec = spec + + extconf_path = "#{spec.gem_dir}/extconf.rb" + + write_file(extconf_path) do |io| + io.puts "require 'mkmf'" + io.puts "create_makefile '#{spec.name}'" + end + + spec.extensions = "extconf.rb" + end + end + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.invoke "a", "-j4" + end + end + + gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out")) + if vc_windows? && nmake_found? + refute_includes(gem_make_out, " -j4") + else + assert_includes(gem_make_out, "make -j4") + end + end + + def test_execute_bindir_with_nonexistent_parent_dirs + spec_fetcher do |fetcher| + fetcher.gem "a", 2 do |s| + s.executables = %w[a_bin] + s.files = %w[bin/a_bin] + end + end + + @cmd.options[:args] = %w[a] + + nested_bin_dir = File.join(@tempdir, "not", "exists") + refute_directory_exists nested_bin_dir, "Nested bin directory should not exist yet" + + @cmd.options[:bin_dir] = nested_bin_dir + + use_ui @ui do + assert_raise Gem::MockGemUi::SystemExitException, @ui.error do + @cmd.execute + end + end + + assert_directory_exists nested_bin_dir, "Nested bin directory should exist now" + assert_path_exist File.join(nested_bin_dir, "a_bin") + + assert_equal %w[a-2], @cmd.installed_specs.map(&:full_name) + end end diff --git a/test/rubygems/test_gem_commands_open_command.rb b/test/rubygems/test_gem_commands_open_command.rb index d9e518048c..addc7427e2 100644 --- a/test/rubygems/test_gem_commands_open_command.rb +++ b/test/rubygems/test_gem_commands_open_command.rb @@ -21,6 +21,8 @@ class TestGemCommandsOpenCommand < Gem::TestCase end def test_execute + omit "JRuby on Windows spawns the editor with a different cwd" if Gem.win_platform? && Gem.java_platform? + @cmd.options[:args] = %w[foo] @cmd.options[:editor] = (ruby_with_rubygems_in_load_path + ["-e", "puts(ARGV,Dir.pwd)", "--"]).join(" ") diff --git a/test/rubygems/test_gem_commands_owner_command.rb b/test/rubygems/test_gem_commands_owner_command.rb index bc4f13ff2a..f6d4d03f84 100644 --- a/test/rubygems/test_gem_commands_owner_command.rb +++ b/test/rubygems/test_gem_commands_owner_command.rb @@ -32,9 +32,12 @@ class TestGemCommandsOwnerCommand < Gem::TestCase - email: user1@example.com id: 1 handle: user1 + role: owner - email: user2@example.com + role: maintainer - id: 3 handle: user3 + role: owner - id: 4 EOF @@ -48,14 +51,14 @@ EOF assert_equal Gem.configuration.rubygems_api_key, @stub_fetcher.last_request["Authorization"] assert_match(/Owners for gem: freewill/, @stub_ui.output) - assert_match(/- user1@example.com/, @stub_ui.output) - assert_match(/- user2@example.com/, @stub_ui.output) - assert_match(/- user3/, @stub_ui.output) + assert_match(/- user1@example.com \(owner\)/, @stub_ui.output) + assert_match(/- user2@example.com \(maintainer\)/, @stub_ui.output) + assert_match(/- user3 \(owner\)/, @stub_ui.output) assert_match(/- 4/, @stub_ui.output) end def test_show_owners_dont_load_objects - pend "testing a psych-only API" unless defined?(::Psych::DisallowedClass) + Gem.load_yaml response = <<EOF --- @@ -386,9 +389,10 @@ EOF end end - assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output + assert_match @stub_fetcher.webauthn_url_with_port(server.port), @stub_ui.output assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output assert_equal "Uvh6T57tkWuUnWYo", @stub_fetcher.last_request["OTP"] assert_match response_success, @stub_ui.output @@ -412,10 +416,12 @@ EOF end end - assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(server.port)} " \ + webauthn_verification_request = @stub_fetcher.requests.find {|req| req.path == "/api/v1/webauthn_verification" } + assert_match webauthn_verification_request["Authorization"], Gem.configuration.rubygems_api_key + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @stub_ui.output + assert_match @stub_fetcher.webauthn_url_with_port(server.port), @stub_ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @stub_ui.error refute_match "You are verified with a security device. You may close the browser window.", @stub_ui.output refute_match response_success, @stub_ui.output @@ -435,9 +441,10 @@ EOF end end - assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ "command with the `--otp [your_code]` option.", @stub_ui.output + assert_match @stub_fetcher.webauthn_url_with_port(server.port), @stub_ui.output assert_match "You are verified with a security device. You may close the browser window.", @stub_ui.output assert_equal "Uvh6T57tkWuUnWYo", @stub_fetcher.last_request["OTP"] assert_match response_success, @stub_ui.output @@ -463,16 +470,17 @@ EOF end assert_match @stub_fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - assert_match "You have enabled multi-factor authentication. Please visit #{@stub_fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ "command with the `--otp [your_code]` option.", @stub_ui.output + assert_match @stub_fetcher.webauthn_url_with_port(server.port), @stub_ui.output assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \ "or been used already.", @stub_ui.error refute_match "You are verified with a security device. You may close the browser window.", @stub_ui.output refute_match response_success, @stub_ui.output end - def test_remove_owners_unathorized_api_key + def test_remove_owners_unauthorized_api_key response_forbidden = "The API key doesn't have access" response_success = "Owner removed successfully." @@ -537,7 +545,7 @@ EOF assert_empty reused_otp_codes end - def test_add_owners_unathorized_api_key + def test_add_owners_unauthorized_api_key response_forbidden = "The API key doesn't have access" response_success = "Owner added successfully." diff --git a/test/rubygems/test_gem_commands_pristine_command.rb b/test/rubygems/test_gem_commands_pristine_command.rb index 46c06db014..0ea140897c 100644 --- a/test/rubygems/test_gem_commands_pristine_command.rb +++ b/test/rubygems/test_gem_commands_pristine_command.rb @@ -125,8 +125,8 @@ class TestGemCommandsPristineCommand < Gem::TestCase @cmd.execute end - assert File.exist?(gem_bin) - assert File.exist?(gem_stub) + assert_path_exist gem_bin + assert_path_exist gem_stub out = @ui.output.split "\n" @@ -248,7 +248,13 @@ class TestGemCommandsPristineCommand < Gem::TestCase end refute_includes @ui.output, "Restored #{a.full_name}" - assert_includes @ui.output, "Restored #{b.full_name}" + + if Gem.java_platform? + refute_includes @ui.output, "Restored #{b.full_name}" + assert_includes @ui.output, "No gems with missing extensions to restore" + else + assert_includes @ui.output, "Restored #{b.full_name}" + end end def test_execute_no_extension @@ -537,8 +543,8 @@ class TestGemCommandsPristineCommand < Gem::TestCase @cmd.execute end - assert File.exist? gem_exec - refute File.exist? gem_lib + assert_path_exist gem_exec + assert_path_not_exist gem_lib end def test_execute_only_plugins @@ -572,9 +578,9 @@ class TestGemCommandsPristineCommand < Gem::TestCase @cmd.execute end - refute File.exist? gem_exec - assert File.exist? gem_plugin - refute File.exist? gem_lib + assert_path_not_exist gem_exec + assert_path_exist gem_plugin + assert_path_not_exist gem_lib end def test_execute_bindir @@ -606,8 +612,8 @@ class TestGemCommandsPristineCommand < Gem::TestCase @cmd.execute end - refute File.exist? gem_exec - assert File.exist? gem_bindir + assert_path_not_exist gem_exec + assert_path_exist gem_bindir end def test_execute_unknown_gem_at_remote_source @@ -659,6 +665,42 @@ class TestGemCommandsPristineCommand < Gem::TestCase refute_includes "ruby_executable_hooks", File.read(exe) end + def test_execute_default_gem_and_regular_gem + a_default = new_default_spec("a", "1.2.0") + + a = util_spec "a" do |s| + s.extensions << "ext/a/extconf.rb" + end + + ext_path = File.join @tempdir, "ext", "a", "extconf.rb" + write_file ext_path do |io| + io.write <<-'RUBY' + File.open "Makefile", "w" do |f| + f.puts "clean:\n\techo cleaned\n" + f.puts "all:\n\techo built\n" + f.puts "install:\n\techo installed\n" + end + RUBY + end + + install_default_gems a_default + install_gem a + + # Remove the extension files for a + FileUtils.rm_rf a.gem_build_complete_path + + @cmd.options[:args] = %w[a] + + use_ui @ui do + @cmd.execute + end + + assert_includes @ui.output, "Restored #{a.full_name}" + + # Check extension files for a were restored + assert_path_exist a.gem_build_complete_path + end + def test_execute_multi_platform a = util_spec "a" do |s| s.extensions << "ext/a/extconf.rb" diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 2d0190b49f..ada95e89b4 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -115,40 +115,116 @@ class TestGemCommandsPushCommand < Gem::TestCase assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class content_length = @fetcher.last_request["Content-Length"].to_i assert_equal content_length, @fetcher.last_request.body.length - assert_equal "multipart", @fetcher.last_request.main_type, @fetcher.last_request.content_type - assert_equal "form-data", @fetcher.last_request.sub_type - assert_include @fetcher.last_request.type_params, "boundary" - boundary = @fetcher.last_request.type_params["boundary"] + assert_attestation_multipart Gem.read_binary("#{@path}.sigstore.json") + end - parts = @fetcher.last_request.body.split(/(?:\r\n|\A)--#{Regexp.quote(boundary)}(?:\r\n|--)/m) - refute_empty parts - assert_empty parts[0] - parts.shift # remove the first empty part + def test_execute_attestation_auto + omit if RUBY_ENGINE == "jruby" - p1 = parts.shift - p2 = parts.shift - assert_equal "\r\n", parts.shift - assert_empty parts + ENV["GITHUB_ACTIONS"] = "true" + begin + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - assert_equal [ - "Content-Disposition: form-data; name=\"gem\"; filename=\"#{@path}\"", - "Content-Type: application/octet-stream", - nil, - Gem.read_binary(@path), - ].join("\r\n").b, p1 - assert_equal [ - "Content-Disposition: form-data; name=\"attestations\"", - nil, - "[#{Gem.read_binary("#{@path}.sigstore.json")}]", - ].join("\r\n").b, p2 + attestation_path = "#{@path}.sigstore.json" + attestation_content = "auto-attestation" + File.write(attestation_path, attestation_content) + @cmd.options[:args] = [@path] + + @cmd.stub(:attest!, attestation_path) do + @cmd.execute + end + + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + content_length = @fetcher.last_request["Content-Length"].to_i + assert_equal content_length, @fetcher.last_request.body.length + assert_attestation_multipart attestation_content + ensure + ENV.delete("GITHUB_ACTIONS") + end end - def test_execute_allowed_push_host + def test_execute_attestation_fallback + omit if RUBY_ENGINE == "jruby" + + ENV["GITHUB_ACTIONS"] = "true" + begin + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [@path] + + @cmd.stub(:attest!, proc { raise Gem::Exception, "boom" }) do + use_ui @ui do + @cmd.execute + end + end + + assert_match "Failed to push with attestation, retrying without attestation.", @ui.error + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] + ensure + ENV.delete("GITHUB_ACTIONS") + end + end + + def test_execute_attestation_skipped_on_non_rubygems_host @spec, @path = util_gem "freebird", "1.0.1" do |spec| spec.metadata["allowed_push_host"] = "https://privategemserver.example" end + @response = "Successfully registered gem: freebird (1.0.1)" + @fetcher.data["#{@spec.metadata["allowed_push_host"]}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [@path] + + attest_called = false + @cmd.stub(:attest!, proc { attest_called = true }) do + @cmd.execute + end + + refute attest_called, "attest! should not be called for non-rubygems.org hosts" + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] + end + + def test_execute_attestation_skipped_on_jruby @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [@path] + + attest_called = false + engine = RUBY_ENGINE + Object.send :remove_const, :RUBY_ENGINE + Object.const_set :RUBY_ENGINE, "jruby" + + begin + @cmd.stub(:attest!, proc { attest_called = true }) do + @cmd.execute + end + + refute attest_called, "attest! should not be called on JRuby" + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] + ensure + Object.send :remove_const, :RUBY_ENGINE + Object.const_set :RUBY_ENGINE, engine + end + end + + def test_execute_allowed_push_host + @spec, @path = util_gem "freebird", "1.0.1" do |spec| + spec.metadata["allowed_push_host"] = "https://privategemserver.example" + end + + @response = "Successfully registered gem: freebird (1.0.1)" @fetcher.data["#{@spec.metadata["allowed_push_host"]}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") @fetcher.data["#{Gem.host}/api/v1/gems"] = ["fail", 500, "Internal Server Error"] @@ -477,15 +553,17 @@ class TestGemCommandsPushCommand < Gem::TestCase end end - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match response_success, @ui.output end def test_with_webauthn_enabled_failure + pend "Flaky on TruffleRuby" if RUBY_ENGINE == "truffleruby" response_success = "Successfully registered gem: freewill (1.0.0)" server = Gem::MockTCPServer.new error = Gem::WebauthnVerificationError.new("Something went wrong") @@ -505,9 +583,10 @@ class TestGemCommandsPushCommand < Gem::TestCase assert_equal 1, error.exit_code assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output refute_match response_success, @ui.output @@ -527,9 +606,10 @@ class TestGemCommandsPushCommand < Gem::TestCase end end - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match response_success, @ui.output @@ -553,16 +633,17 @@ class TestGemCommandsPushCommand < Gem::TestCase assert_equal 1, error.exit_code assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ - "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin " \ - "command with the `--otp [your_code]` option.", @ui.output + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ + "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ + "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \ "or been used already.", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output refute_match response_success, @ui.output end - def test_sending_gem_unathorized_api_key_with_mfa_enabled + def test_sending_gem_unauthorized_api_key_with_mfa_enabled response_mfa_enabled = "You have enabled multifactor authentication but your request doesn't have the correct OTP code. Please check it and retry." response_forbidden = "The API key doesn't have access" response_success = "Successfully registered gem: freewill (1.0.0)" @@ -638,6 +719,35 @@ class TestGemCommandsPushCommand < Gem::TestCase private + def assert_attestation_multipart(attestation_payload) + assert_equal "multipart", @fetcher.last_request.main_type, @fetcher.last_request.content_type + assert_equal "form-data", @fetcher.last_request.sub_type + assert_include @fetcher.last_request.type_params, "boundary" + boundary = @fetcher.last_request.type_params["boundary"] + + parts = @fetcher.last_request.body.split(/(?:\r\n|\A)--#{Regexp.quote(boundary)}(?:\r\n|--)/m) + refute_empty parts + assert_empty parts[0] + parts.shift # remove the first empty part + + p1 = parts.shift + p2 = parts.shift + assert_equal "\r\n", parts.shift + assert_empty parts + + assert_equal [ + "Content-Disposition: form-data; name=\"gem\"; filename=\"#{@path}\"", + "Content-Type: application/octet-stream", + nil, + Gem.read_binary(@path), + ].join("\r\n").b, p1 + assert_equal [ + "Content-Disposition: form-data; name=\"attestations\"", + nil, + "[#{attestation_payload}]", + ].join("\r\n").b, p2 + end + def singleton_gem_class class << Gem; self; end end diff --git a/test/rubygems/test_gem_commands_query_command.rb b/test/rubygems/test_gem_commands_query_command.rb deleted file mode 100644 index 8e590df124..0000000000 --- a/test/rubygems/test_gem_commands_query_command.rb +++ /dev/null @@ -1,830 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" -require "rubygems/commands/query_command" - -module TestGemCommandsQueryCommandSetup - def setup - super - - @cmd = Gem::Commands::QueryCommand.new - - @specs = add_gems_to_fetcher - @stub_ui = Gem::MockGemUi.new - @stub_fetcher = Gem::FakeFetcher.new - - @stub_fetcher.data["#{@gem_repo}Marshal.#{Gem.marshal_version}"] = proc do - raise Gem::RemoteFetcher::FetchError - end - end -end - -class TestGemCommandsQueryCommandWithInstalledGems < Gem::TestCase - include TestGemCommandsQueryCommandSetup - - def test_execute - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-r] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_all - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-r --all] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_all_prerelease - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-r --all --prerelease] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (3.a, 2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_details - spec_fetcher do |fetcher| - fetcher.spec "a", 2 do |s| - s.summary = "This is a lot of text. " * 4 - s.authors = ["Abraham Lincoln", "Hirohito"] - s.homepage = "http://a.example.com/" - end - - fetcher.legacy_platform - end - - @cmd.handle_options %w[-r -d] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2) - Authors: Abraham Lincoln, Hirohito - Homepage: http://a.example.com/ - - This is a lot of text. This is a lot of text. This is a lot of text. - This is a lot of text. - -pl (1) - Platform: i386-linux - Author: A User - Homepage: http://example.com - - this is a summary - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_details_cleans_text - spec_fetcher do |fetcher| - fetcher.spec "a", 2 do |s| - s.summary = "This is a lot of text. " * 4 - s.authors = ["Abraham Lincoln \x01", "\x02 Hirohito"] - s.homepage = "http://a.example.com/\x03" - end - - fetcher.legacy_platform - end - - @cmd.handle_options %w[-r -d] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2) - Authors: Abraham Lincoln ., . Hirohito - Homepage: http://a.example.com/. - - This is a lot of text. This is a lot of text. This is a lot of text. - This is a lot of text. - -pl (1) - Platform: i386-linux - Author: A User - Homepage: http://example.com - - this is a summary - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_details_truncates_summary - spec_fetcher do |fetcher| - fetcher.spec "a", 2 do |s| - s.summary = "This is a lot of text. " * 10_000 - s.authors = ["Abraham Lincoln \x01", "\x02 Hirohito"] - s.homepage = "http://a.example.com/\x03" - end - - fetcher.legacy_platform - end - - @cmd.handle_options %w[-r -d] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2) - Authors: Abraham Lincoln ., . Hirohito - Homepage: http://a.example.com/. - - Truncating the summary for a-2 to 100,000 characters: -#{" This is a lot of text. This is a lot of text. This is a lot of text.\n" * 1449} This is a lot of te - -pl (1) - Platform: i386-linux - Author: A User - Homepage: http://example.com - - this is a summary - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_installed - @cmd.handle_options %w[-n a --installed] - - assert_raise Gem::MockGemUi::SystemExitException do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "true\n", @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_installed_inverse - @cmd.handle_options %w[-n a --no-installed] - - e = assert_raise Gem::MockGemUi::TermError do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "false\n", @stub_ui.output - assert_equal "", @stub_ui.error - - assert_equal 1, e.exit_code - end - - def test_execute_installed_inverse_not_installed - @cmd.handle_options %w[-n not_installed --no-installed] - - assert_raise Gem::MockGemUi::SystemExitException do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "true\n", @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_installed_no_name - @cmd.handle_options %w[--installed] - - e = assert_raise Gem::MockGemUi::TermError do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "", @stub_ui.output - assert_equal "ERROR: You must specify a gem name\n", @stub_ui.error - - assert_equal 4, e.exit_code - end - - def test_execute_installed_not_installed - @cmd.handle_options %w[-n not_installed --installed] - - e = assert_raise Gem::MockGemUi::TermError do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "false\n", @stub_ui.output - assert_equal "", @stub_ui.error - - assert_equal 1, e.exit_code - end - - def test_execute_installed_version - @cmd.handle_options %w[-n a --installed --version 2] - - assert_raise Gem::MockGemUi::SystemExitException do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "true\n", @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_installed_version_not_installed - @cmd.handle_options %w[-n c --installed --version 2] - - e = assert_raise Gem::MockGemUi::TermError do - use_ui @stub_ui do - @cmd.execute - end - end - - assert_equal "false\n", @stub_ui.output - assert_equal "", @stub_ui.error - - assert_equal 1, e.exit_code - end - - def test_execute_local - spec_fetcher(&:legacy_platform) - - @cmd.options[:domain] = :local - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (3.a, 2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_local_notty - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[] - - @stub_ui.outs.tty = false - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF -a (3.a, 2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_local_quiet - spec_fetcher(&:legacy_platform) - - @cmd.options[:domain] = :local - Gem.configuration.verbose = false - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF -a (3.a, 2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_no_versions - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-r --no-versions] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a -pl - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_notty - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-r] - - @stub_ui.outs.tty = false - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF -a (2) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_prerelease - @cmd.handle_options %w[-r --prerelease] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (3.a) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_prerelease_local - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-l --prerelease] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (3.a, 2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_no_prerelease_local - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[-l --no-prerelease] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_remote - spec_fetcher(&:legacy_platform) - - @cmd.options[:domain] = :remote - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_remote_notty - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[] - - @stub_ui.outs.tty = false - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF -a (3.a, 2, 1) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_remote_quiet - spec_fetcher(&:legacy_platform) - - @cmd.options[:domain] = :remote - Gem.configuration.verbose = false - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF -a (2) -pl (1 i386-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_make_entry - a_2_name = @specs["a-2"].original_name - - @stub_fetcher.data.delete \ - "#{@gem_repo}quick/Marshal.#{Gem.marshal_version}/#{a_2_name}.gemspec.rz" - - a2 = @specs["a-2"] - entry_tuples = [ - [Gem::NameTuple.new(a2.name, a2.version, a2.platform), - Gem.sources.first], - ] - - platforms = { a2.version => [a2.platform] } - - entry = @cmd.send :make_entry, entry_tuples, platforms - - assert_equal "a (2)", entry - end - - # Test for multiple args handling! - def test_execute_multiple_args - spec_fetcher(&:legacy_platform) - - @cmd.handle_options %w[a pl] - - use_ui @stub_ui do - @cmd.execute - end - - assert_match(/^a /, @stub_ui.output) - assert_match(/^pl /, @stub_ui.output) - assert_equal "", @stub_ui.error - end - - def test_show_gems - @cmd.options[:name] = // - @cmd.options[:domain] = :remote - - use_ui @stub_ui do - @cmd.send :show_gems, /a/i - end - - assert_match(/^a /, @stub_ui.output) - refute_match(/^pl /, @stub_ui.output) - assert_empty @stub_ui.error - end - - private - - def add_gems_to_fetcher - spec_fetcher do |fetcher| - fetcher.spec "a", 1 - fetcher.spec "a", 2 - fetcher.spec "a", "3.a" - end - end -end - -class TestGemCommandsQueryCommandWithoutInstalledGems < Gem::TestCase - include TestGemCommandsQueryCommandSetup - - def test_execute_platform - spec_fetcher do |fetcher| - fetcher.spec "a", 1 - fetcher.spec "a", 1 do |s| - s.platform = "x86-linux" - end - - fetcher.spec "a", 2 do |s| - s.platform = "universal-darwin" - end - end - - @cmd.handle_options %w[-r -a] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -a (2 universal-darwin, 1 ruby x86-linux) - EOF - - assert_equal expected, @stub_ui.output - assert_equal "", @stub_ui.error - end - - def test_execute_show_default_gems - spec_fetcher {|fetcher| fetcher.spec "a", 2 } - - a1 = new_default_spec "a", 1 - install_default_gems a1 - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (2, default: 1) -EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_show_default_gems_with_platform - a1 = new_default_spec "a", 1 - a1.platform = "java" - install_default_gems a1 - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (default: 1 java) -EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_default_details - spec_fetcher do |fetcher| - fetcher.spec "a", 2 - end - - a1 = new_default_spec "a", 1 - install_default_gems a1 - - @cmd.handle_options %w[-l -d] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (2, 1) - Author: A User - Homepage: http://example.com - Installed at (2): #{@gemhome} - (1, default): #{a1.base_dir} - - this is a summary - EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_local_details - spec_fetcher do |fetcher| - fetcher.spec "a", 1 do |s| - s.platform = "x86-linux" - end - - fetcher.spec "a", 2 do |s| - s.summary = "This is a lot of text. " * 4 - s.authors = ["Abraham Lincoln", "Hirohito"] - s.homepage = "http://a.example.com/" - s.platform = "universal-darwin" - end - - fetcher.legacy_platform - end - - @cmd.handle_options %w[-l -d] - - use_ui @stub_ui do - @cmd.execute - end - - str = @stub_ui.output - - str.gsub!(/\(\d\): [^\n]*/, "-") - str.gsub!(/at: [^\n]*/, "at: -") - - expected = <<-EOF - -*** LOCAL GEMS *** - -a (2, 1) - Platforms: - 1: x86-linux - 2: universal-darwin - Authors: Abraham Lincoln, Hirohito - Homepage: http://a.example.com/ - Installed at - - - - - This is a lot of text. This is a lot of text. This is a lot of text. - This is a lot of text. - -pl (1) - Platform: i386-linux - Author: A User - Homepage: http://example.com - Installed at: - - - this is a summary - EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_exact_remote - spec_fetcher do |fetcher| - fetcher.spec "coolgem-omg", 3 - fetcher.spec "coolgem", "4.2.1" - fetcher.spec "wow_coolgem", 1 - end - - @cmd.handle_options %w[--remote --exact coolgem] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** REMOTE GEMS *** - -coolgem (4.2.1) - EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_exact_local - spec_fetcher do |fetcher| - fetcher.spec "coolgem-omg", 3 - fetcher.spec "coolgem", "4.2.1" - fetcher.spec "wow_coolgem", 1 - end - - @cmd.handle_options %w[--exact coolgem] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -coolgem (4.2.1) - EOF - - assert_equal expected, @stub_ui.output - end - - def test_execute_exact_multiple - spec_fetcher do |fetcher| - fetcher.spec "coolgem-omg", 3 - fetcher.spec "coolgem", "4.2.1" - fetcher.spec "wow_coolgem", 1 - - fetcher.spec "othergem-omg", 3 - fetcher.spec "othergem", "1.2.3" - fetcher.spec "wow_othergem", 1 - end - - @cmd.handle_options %w[--exact coolgem othergem] - - use_ui @stub_ui do - @cmd.execute - end - - expected = <<-EOF - -*** LOCAL GEMS *** - -coolgem (4.2.1) - -*** LOCAL GEMS *** - -othergem (1.2.3) - EOF - - assert_equal expected, @stub_ui.output - end - - def test_depprecated - assert @cmd.deprecated? - end - - private - - def add_gems_to_fetcher - spec_fetcher do |fetcher| - fetcher.download "a", 1 - fetcher.download "a", 2 - fetcher.download "a", "3.a" - end - end -end diff --git a/test/rubygems/test_gem_commands_setup_command.rb b/test/rubygems/test_gem_commands_setup_command.rb index c3622c02cd..b33e05ab28 100644 --- a/test/rubygems/test_gem_commands_setup_command.rb +++ b/test/rubygems/test_gem_commands_setup_command.rb @@ -4,13 +4,6 @@ require_relative "helper" require "rubygems/commands/setup_command" class TestGemCommandsSetupCommand < Gem::TestCase - bundler_gemspec = File.expand_path("../../bundler/lib/bundler/version.rb", __dir__) - if File.exist?(bundler_gemspec) - BUNDLER_VERS = File.read(bundler_gemspec).match(/VERSION = "(#{Gem::Version::VERSION_PATTERN})"/)[1] - else - BUNDLER_VERS = "2.0.1" - end - def setup super @@ -35,9 +28,10 @@ class TestGemCommandsSetupCommand < Gem::TestCase create_dummy_files(filelist) - gemspec = util_spec "bundler", BUNDLER_VERS do |s| + gemspec = util_spec "bundler", "9.9.9" do |s| s.bindir = "exe" s.executables = ["bundle", "bundler"] + s.files = ["lib/bundler.rb"] end File.open "bundler/bundler.gemspec", "w" do |io| @@ -229,6 +223,9 @@ class TestGemCommandsSetupCommand < Gem::TestCase assert_path_exist "#{Gem.dir}/gems/bundler-#{bundler_version}" assert_path_exist "#{Gem.dir}/gems/bundler-audit-1.0.0" + + assert_path_exist "#{Gem.dir}/gems/bundler-#{bundler_version}/exe/bundle" + assert_path_not_exist "#{Gem.dir}/gems/bundler-#{bundler_version}/lib/bundler.rb" end def test_install_default_bundler_gem_with_default_gems_not_installed_at_default_dir @@ -380,20 +377,22 @@ class TestGemCommandsSetupCommand < Gem::TestCase File.open "CHANGELOG.md", "w" do |io| io.puts <<-HISTORY_TXT -# #{Gem::VERSION} / 2013-03-26 +# Changelog + +## #{Gem::VERSION} / 2013-03-26 -## Bug fixes: +### Bug fixes: * Fixed release note display for LANG=C when installing rubygems * π is tasty -# 2.0.2 / 2013-03-06 +## 2.0.2 / 2013-03-06 -## Bug fixes: +### Bug fixes: * Other bugs fixed -# 2.0.1 / 2013-03-05 +## 2.0.1 / 2013-03-05 -## Bug fixes: +### Bug fixes: * Yet more bugs fixed HISTORY_TXT end @@ -403,9 +402,9 @@ class TestGemCommandsSetupCommand < Gem::TestCase end expected = <<-EXPECTED -# #{Gem::VERSION} / 2013-03-26 +## #{Gem::VERSION} / 2013-03-26 -## Bug fixes: +### Bug fixes: * Fixed release note display for LANG=C when installing rubygems * π is tasty diff --git a/test/rubygems/test_gem_commands_signin_command.rb b/test/rubygems/test_gem_commands_signin_command.rb index 29e5edceb7..e612288faf 100644 --- a/test/rubygems/test_gem_commands_signin_command.rb +++ b/test/rubygems/test_gem_commands_signin_command.rb @@ -121,7 +121,7 @@ class TestGemCommandsSigninCommand < Gem::TestCase assert_match "The default access scope is:", key_name_ui.output assert_match "index_rubygems: y", key_name_ui.output assert_match "Do you want to customise scopes? [yN]", key_name_ui.output - assert_equal "name=test-key&index_rubygems=true", fetcher.last_request.body + assert_equal "name=test-key&index_rubygems=true&push_rubygem=true", fetcher.last_request.body credentials = load_yaml_file Gem.configuration.credentials_path assert_equal api_key, credentials[:rubygems_api_key] diff --git a/test/rubygems/test_gem_commands_sources_command.rb b/test/rubygems/test_gem_commands_sources_command.rb index 5e675e5c84..71c6d5ce16 100644 --- a/test/rubygems/test_gem_commands_sources_command.rb +++ b/test/rubygems/test_gem_commands_sources_command.rb @@ -32,7 +32,7 @@ class TestGemCommandsSourcesCommand < Gem::TestCase end expected = <<-EOF -*** CURRENT SOURCES *** +*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW *** #{@gem_repo} EOF @@ -42,23 +42,104 @@ class TestGemCommandsSourcesCommand < Gem::TestCase end def test_execute_add - spec_fetcher do |fetcher| - fetcher.spec "a", 1 + setup_fake_source(@new_repo) + + @cmd.handle_options %W[--add #{@new_repo}] + + use_ui @ui do + @cmd.execute end - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] + assert_equal [@gem_repo, @new_repo], Gem.sources + + expected = <<-EOF +#{@new_repo} added to sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_add_without_trailing_slash + setup_fake_source("https://rubygems.pkg.github.com/my-org") + + @cmd.handle_options %W[--add https://rubygems.pkg.github.com/my-org] + + use_ui @ui do + @cmd.execute end - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap specs_dump_gz do |io| - Marshal.dump specs, io + assert_equal [@gem_repo, "https://rubygems.pkg.github.com/my-org/"], Gem.sources + + expected = <<-EOF +https://rubygems.pkg.github.com/my-org/ added to sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_add_multiple_trailing_slash + setup_fake_source("https://rubygems.pkg.github.com/my-org/") + + @cmd.handle_options %W[--add https://rubygems.pkg.github.com/my-org///] + + use_ui @ui do + @cmd.execute end - @fetcher.data["#{@new_repo}/specs.#{@marshal_version}.gz"] = - specs_dump_gz.string + assert_equal [@gem_repo, "https://rubygems.pkg.github.com/my-org/"], Gem.sources - @cmd.handle_options %W[--add #{@new_repo}] + expected = <<-EOF +https://rubygems.pkg.github.com/my-org/ added to sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_append_without_trailing_slash + setup_fake_source("https://rubygems.pkg.github.com/my-org") + + @cmd.handle_options %W[--append https://rubygems.pkg.github.com/my-org] + + use_ui @ui do + @cmd.execute + end + + assert_equal [@gem_repo, "https://rubygems.pkg.github.com/my-org/"], Gem.sources + + expected = <<-EOF +https://rubygems.pkg.github.com/my-org/ added to sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_prepend_without_trailing_slash + setup_fake_source("https://rubygems.pkg.github.com/my-org") + + @cmd.handle_options %W[--prepend https://rubygems.pkg.github.com/my-org] + + use_ui @ui do + @cmd.execute + end + + assert_equal ["https://rubygems.pkg.github.com/my-org/", @gem_repo], Gem.sources + + expected = <<-EOF +https://rubygems.pkg.github.com/my-org/ added to sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_append + setup_fake_source(@new_repo) + + @cmd.handle_options %W[--append #{@new_repo}] use_ui @ui do @cmd.execute @@ -77,21 +158,31 @@ class TestGemCommandsSourcesCommand < Gem::TestCase def test_execute_add_allow_typo_squatting_source rubygems_org = "https://rubyems.org" - spec_fetcher do |fetcher| - fetcher.spec("a", 1) - end + setup_fake_source(rubygems_org) - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] - end + @cmd.handle_options %W[--add #{rubygems_org}] + ui = Gem::MockGemUi.new("y") - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap(specs_dump_gz) do |io| - Marshal.dump(specs, io) + use_ui ui do + @cmd.execute end - @fetcher.data["#{rubygems_org}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string - @cmd.handle_options %W[--add #{rubygems_org}] + expected = "https://rubyems.org is too similar to https://rubygems.org\n\nDo you want to add this source? [yn] https://rubyems.org added to sources\n" + + assert_equal expected, ui.output + + source = Gem::Source.new(rubygems_org) + assert Gem.sources.include?(source) + + assert_empty ui.error + end + + def test_execute_append_allow_typo_squatting_source + rubygems_org = "https://rubyems.org" + + setup_fake_source(rubygems_org) + + @cmd.handle_options %W[--append #{rubygems_org}] ui = Gem::MockGemUi.new("y") use_ui ui do @@ -111,21 +202,27 @@ class TestGemCommandsSourcesCommand < Gem::TestCase def test_execute_add_allow_typo_squatting_source_forced rubygems_org = "https://rubyems.org" - spec_fetcher do |fetcher| - fetcher.spec("a", 1) - end + setup_fake_source(rubygems_org) - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] - end + @cmd.handle_options %W[--force --add #{rubygems_org}] - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap(specs_dump_gz) do |io| - Marshal.dump(specs, io) - end + @cmd.execute - @fetcher.data["#{rubygems_org}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string - @cmd.handle_options %W[--force --add #{rubygems_org}] + expected = "https://rubyems.org added to sources\n" + assert_equal expected, ui.output + + source = Gem::Source.new(rubygems_org) + assert Gem.sources.include?(source) + + assert_empty ui.error + end + + def test_execute_append_allow_typo_squatting_source_forced + rubygems_org = "https://rubyems.org" + + setup_fake_source(rubygems_org) + + @cmd.handle_options %W[--force --append #{rubygems_org}] @cmd.execute @@ -141,23 +238,34 @@ class TestGemCommandsSourcesCommand < Gem::TestCase def test_execute_add_deny_typo_squatting_source rubygems_org = "https://rubyems.org" - spec_fetcher do |fetcher| - fetcher.spec("a", 1) - end + setup_fake_source(rubygems_org) - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] - end + @cmd.handle_options %W[--add #{rubygems_org}] - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap(specs_dump_gz) do |io| - Marshal.dump(specs, io) + ui = Gem::MockGemUi.new("n") + + use_ui ui do + assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end end - @fetcher.data["#{rubygems_org}/specs.#{@marshal_version}.gz"] = - specs_dump_gz.string + expected = "https://rubyems.org is too similar to https://rubygems.org\n\nDo you want to add this source? [yn] " - @cmd.handle_options %W[--add #{rubygems_org}] + assert_equal expected, ui.output + + source = Gem::Source.new(rubygems_org) + refute Gem.sources.include?(source) + + assert_empty ui.error + end + + def test_execute_append_deny_typo_squatting_source + rubygems_org = "https://rubyems.org" + + setup_fake_source(rubygems_org) + + @cmd.handle_options %W[--append #{rubygems_org}] ui = Gem::MockGemUi.new("n") @@ -202,6 +310,31 @@ Error fetching http://beta-gems.example.com: assert_equal "", @ui.error end + def test_execute_append_nonexistent_source + spec_fetcher + + uri = "http://beta-gems.example.com/specs.#{@marshal_version}.gz" + @fetcher.data[uri] = proc do + raise Gem::RemoteFetcher::FetchError.new("it died", uri) + end + + @cmd.handle_options %w[--append http://beta-gems.example.com] + + use_ui @ui do + assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end + end + + expected = <<-EOF +Error fetching http://beta-gems.example.com: +\tit died (#{uri}) + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + def test_execute_add_existent_source_invalid_uri spec_fetcher @@ -227,6 +360,31 @@ Error fetching https://u:REDACTED@example.com: assert_equal "", @ui.error end + def test_execute_append_existent_source_invalid_uri + spec_fetcher + + uri = "https://u:p@example.com/specs.#{@marshal_version}.gz" + + @cmd.handle_options %w[--append https://u:p@example.com] + @fetcher.data[uri] = proc do + raise Gem::RemoteFetcher::FetchError.new("it died", uri) + end + + use_ui @ui do + assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end + end + + expected = <<-EOF +Error fetching https://u:REDACTED@example.com: +\tit died (https://u:REDACTED@example.com/specs.#{@marshal_version}.gz) + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + def test_execute_add_existent_source_invalid_uri_with_error_by_chance_including_the_uri_password spec_fetcher @@ -252,6 +410,31 @@ Error fetching https://u:REDACTED@example.com: assert_equal "", @ui.error end + def test_execute_append_existent_source_invalid_uri_with_error_by_chance_including_the_uri_password + spec_fetcher + + uri = "https://u:secret@example.com/specs.#{@marshal_version}.gz" + + @cmd.handle_options %w[--append https://u:secret@example.com] + @fetcher.data[uri] = proc do + raise Gem::RemoteFetcher::FetchError.new("it secretly died", uri) + end + + use_ui @ui do + assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end + end + + expected = <<-EOF +Error fetching https://u:REDACTED@example.com: +\tit secretly died (https://u:REDACTED@example.com/specs.#{@marshal_version}.gz) + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + def test_execute_add_redundant_source spec_fetcher @@ -271,27 +454,34 @@ source #{@gem_repo} already present in the cache assert_equal "", @ui.error end - def test_execute_add_redundant_source_trailing_slash + def test_execute_append_redundant_source spec_fetcher - # Remove pre-existing gem source (w/ slash) - repo_with_slash = "http://gems.example.com/" - @cmd.handle_options %W[--remove #{repo_with_slash}] + @cmd.handle_options %W[--append #{@gem_repo}] + use_ui @ui do @cmd.execute end - source = Gem::Source.new repo_with_slash - assert_equal false, Gem.sources.include?(source) + + assert_equal [@gem_repo], Gem.sources expected = <<-EOF -#{repo_with_slash} removed from sources +#{@gem_repo} moved to end of sources EOF assert_equal expected, @ui.output assert_equal "", @ui.error + end + + def test_execute_add_redundant_source_trailing_slash + repo_with_slash = "http://sample.repo/" + + Gem.configuration.sources = [repo_with_slash] + + setup_fake_source(repo_with_slash) # Re-add pre-existing gem source (w/o slash) - repo_without_slash = "http://gems.example.com" + repo_without_slash = repo_with_slash.delete_suffix("/") @cmd.handle_options %W[--add #{repo_without_slash}] use_ui @ui do @cmd.execute @@ -300,8 +490,7 @@ source #{@gem_repo} already present in the cache assert_equal true, Gem.sources.include?(source) expected = <<-EOF -http://gems.example.com/ removed from sources -http://gems.example.com added to sources +source #{repo_without_slash} already present in the cache EOF assert_equal expected, @ui.output @@ -316,35 +505,46 @@ http://gems.example.com added to sources assert_equal true, Gem.sources.include?(source) expected = <<-EOF -http://gems.example.com/ removed from sources -http://gems.example.com added to sources -source http://gems.example.com/ already present in the cache +source #{repo_without_slash} already present in the cache +source #{repo_with_slash} already present in the cache EOF assert_equal expected, @ui.output assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil end def test_execute_add_http_rubygems_org http_rubygems_org = "http://rubygems.org/" - spec_fetcher do |fetcher| - fetcher.spec "a", 1 - end + setup_fake_source(http_rubygems_org) - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] - end + @cmd.handle_options %W[--add #{http_rubygems_org}] - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap specs_dump_gz do |io| - Marshal.dump specs, io + ui = Gem::MockGemUi.new "n" + + use_ui ui do + assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end end - @fetcher.data["#{http_rubygems_org}/specs.#{@marshal_version}.gz"] = - specs_dump_gz.string + assert_equal [@gem_repo], Gem.sources - @cmd.handle_options %W[--add #{http_rubygems_org}] + expected = <<-EXPECTED + EXPECTED + + assert_equal expected, @ui.output + assert_empty @ui.error + end + + def test_execute_append_http_rubygems_org + http_rubygems_org = "http://rubygems.org/" + + setup_fake_source(http_rubygems_org) + + @cmd.handle_options %W[--append #{http_rubygems_org}] ui = Gem::MockGemUi.new "n" @@ -366,21 +566,27 @@ source http://gems.example.com/ already present in the cache def test_execute_add_http_rubygems_org_forced rubygems_org = "http://rubygems.org" - spec_fetcher do |fetcher| - fetcher.spec("a", 1) - end + setup_fake_source(rubygems_org) - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] - end + @cmd.handle_options %W[--force --add #{rubygems_org}] - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap(specs_dump_gz) do |io| - Marshal.dump(specs, io) - end + @cmd.execute - @fetcher.data["#{rubygems_org}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string - @cmd.handle_options %W[--force --add #{rubygems_org}] + expected = "http://rubygems.org added to sources\n" + assert_equal expected, ui.output + + source = Gem::Source.new(rubygems_org) + assert Gem.sources.include?(source) + + assert_empty ui.error + end + + def test_execute_append_http_rubygems_org_forced + rubygems_org = "http://rubygems.org" + + setup_fake_source(rubygems_org) + + @cmd.handle_options %W[--force --append #{rubygems_org}] @cmd.execute @@ -396,27 +602,68 @@ source http://gems.example.com/ already present in the cache def test_execute_add_https_rubygems_org https_rubygems_org = "https://rubygems.org/" - spec_fetcher do |fetcher| - fetcher.spec "a", 1 + setup_fake_source(https_rubygems_org) + + @cmd.handle_options %W[--add #{https_rubygems_org}] + + use_ui @ui do + @cmd.execute end - specs = Gem::Specification.map do |spec| - [spec.name, spec.version, spec.original_platform] + assert_equal [@gem_repo, https_rubygems_org], Gem.sources + + expected = <<-EXPECTED +#{https_rubygems_org} added to sources + EXPECTED + + assert_equal expected, @ui.output + assert_empty @ui.error + end + + def test_execute_append_https_rubygems_org + https_rubygems_org = "https://rubygems.org/" + + setup_fake_source(https_rubygems_org) + + @cmd.handle_options %W[--append #{https_rubygems_org}] + + use_ui @ui do + @cmd.execute end - specs_dump_gz = StringIO.new - Zlib::GzipWriter.wrap specs_dump_gz do |io| - Marshal.dump specs, io + assert_equal [@gem_repo, https_rubygems_org], Gem.sources + + expected = <<-EXPECTED +#{https_rubygems_org} added to sources + EXPECTED + + assert_equal expected, @ui.output + assert_empty @ui.error + end + + def test_execute_add_bad_uri + @cmd.handle_options %w[--add beta-gems.example.com] + + use_ui @ui do + assert_raise Gem::MockGemUi::TermError do + @cmd.execute + end end - @fetcher.data["#{https_rubygems_org}/specs.#{@marshal_version}.gz"] = - specs_dump_gz.string + assert_equal [@gem_repo], Gem.sources - @cmd.handle_options %W[--add #{https_rubygems_org}] + expected = <<-EOF +beta-gems.example.com/ is not a URI + EOF - ui = Gem::MockGemUi.new "n" + assert_equal expected, @ui.output + assert_equal "", @ui.error + end - use_ui ui do + def test_execute_append_bad_uri + @cmd.handle_options %w[--append beta-gems.example.com] + + use_ui @ui do assert_raise Gem::MockGemUi::TermError do @cmd.execute end @@ -424,15 +671,16 @@ source http://gems.example.com/ already present in the cache assert_equal [@gem_repo], Gem.sources - expected = <<-EXPECTED - EXPECTED + expected = <<-EOF +beta-gems.example.com/ is not a URI + EOF assert_equal expected, @ui.output - assert_empty @ui.error + assert_equal "", @ui.error end - def test_execute_add_bad_uri - @cmd.handle_options %w[--add beta-gems.example.com] + def test_execute_prepend_bad_uri + @cmd.handle_options %w[--prepend beta-gems.example.com] use_ui @ui do assert_raise Gem::MockGemUi::TermError do @@ -443,7 +691,7 @@ source http://gems.example.com/ already present in the cache assert_equal [@gem_repo], Gem.sources expected = <<-EOF -beta-gems.example.com is not a URI +beta-gems.example.com/ is not a URI EOF assert_equal expected, @ui.output @@ -476,7 +724,7 @@ beta-gems.example.com is not a URI end expected = <<-EOF -*** CURRENT SOURCES *** +*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW *** #{@gem_repo} EOF @@ -486,24 +734,32 @@ beta-gems.example.com is not a URI end def test_execute_remove - @cmd.handle_options %W[--remove #{@gem_repo}] + Gem.configuration.sources = [@new_repo] + + setup_fake_source(@new_repo) + + @cmd.handle_options %W[--remove #{@new_repo}] use_ui @ui do @cmd.execute end - expected = "#{@gem_repo} removed from sources\n" + expected = "#{@new_repo} removed from sources\n" assert_equal expected, @ui.output assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil end def test_execute_remove_no_network + Gem.configuration.sources = [@new_repo] + spec_fetcher - @cmd.handle_options %W[--remove #{@gem_repo}] + @cmd.handle_options %W[--remove #{@new_repo}] - @fetcher.data["#{@gem_repo}Marshal.#{Gem.marshal_version}"] = proc do + @fetcher.data["#{@new_repo}Marshal.#{Gem.marshal_version}"] = proc do raise Gem::RemoteFetcher::FetchError end @@ -511,10 +767,129 @@ beta-gems.example.com is not a URI @cmd.execute end + expected = "#{@new_repo} removed from sources\n" + + assert_equal expected, @ui.output + assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil + end + + def test_execute_remove_not_present + Gem.configuration.sources = ["https://other.repo"] + + @cmd.handle_options %W[--remove #{@new_repo}] + + use_ui @ui do + @cmd.execute + end + + expected = "source #{@new_repo} cannot be removed because it's not present in #{Gem.configuration.config_file_name}\n" + + assert_equal expected, @ui.output + assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil + end + + def test_execute_remove_nothing_configured + spec_fetcher + + @cmd.handle_options %W[--remove https://does.not.exist] + + use_ui @ui do + @cmd.execute + end + + expected = "source https://does.not.exist cannot be removed because there are no configured sources in #{Gem.configuration.config_file_name}\n" + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_remove_default_also_present_in_configuration + Gem.configuration.sources = [@gem_repo] + + @cmd.handle_options %W[--remove #{@gem_repo}] + + use_ui @ui do + @cmd.execute + end + + expected = "WARNING: Removing a default source when it is the only source has no effect. Add a different source to #{Gem.configuration.config_file_name} if you want to stop using it as a source.\n" + + assert_equal "", @ui.output + assert_equal expected, @ui.error + ensure + Gem.configuration.sources = nil + end + + def test_remove_default_also_present_in_configuration_when_there_are_more_configured_sources + Gem.configuration.sources = [@gem_repo, "https://other.repo"] + + @cmd.handle_options %W[--remove #{@gem_repo}] + + use_ui @ui do + @cmd.execute + end + expected = "#{@gem_repo} removed from sources\n" assert_equal expected, @ui.output assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil + end + + def test_execute_remove_redundant_source_trailing_slash + repo_with_slash = "http://sample.repo/" + + Gem.configuration.sources = [repo_with_slash] + + setup_fake_source(repo_with_slash) + + repo_without_slash = repo_with_slash.delete_suffix("/") + + @cmd.handle_options %W[--remove #{repo_without_slash}] + use_ui @ui do + @cmd.execute + end + source = Gem::Source.new repo_without_slash + assert_equal false, Gem.sources.include?(source) + + expected = <<-EOF +#{repo_without_slash} removed from sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil + end + + def test_execute_remove_without_trailing_slash + source_uri = "https://rubygems.pkg.github.com/my-org/" + + Gem.configuration.sources = [source_uri] + + setup_fake_source(source_uri) + + @cmd.handle_options %W[--remove https://rubygems.pkg.github.com/my-org] + + use_ui @ui do + @cmd.execute + end + + assert_equal [], Gem.sources + + expected = <<-EOF +#{source_uri} removed from sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + ensure + Gem.configuration.sources = nil end def test_execute_update @@ -531,4 +906,102 @@ beta-gems.example.com is not a URI assert_equal "source cache successfully updated\n", @ui.output assert_equal "", @ui.error end + + def test_execute_prepend_new_source + setup_fake_source(@new_repo) + + @cmd.handle_options %W[--prepend #{@new_repo}] + + use_ui @ui do + @cmd.execute + end + + assert_equal [@new_repo, @gem_repo], Gem.sources + + expected = <<-EOF +#{@new_repo} added to sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_prepend_existing_source + setup_fake_source(@new_repo) + + # Append the source normally first + @cmd.handle_options %W[--append #{@new_repo}] + use_ui @ui do + @cmd.execute + end + + # Initial state: [@gem_repo, @new_repo] + assert_equal [@gem_repo, @new_repo], Gem.sources + + # Now prepend the existing source + @cmd.handle_options %W[--prepend #{@new_repo}] + use_ui @ui do + @cmd.execute + end + + # Should be moved to front: [@new_repo, @gem_repo] + assert_equal [@new_repo, @gem_repo], Gem.sources + + expected = <<-EOF +#{@new_repo} added to sources +#{@new_repo} moved to top of sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + def test_execute_append_existing_source + setup_fake_source(@new_repo) + + # Prepend the source first so it's at the beginning + @cmd.handle_options %W[--prepend #{@new_repo}] + use_ui @ui do + @cmd.execute + end + + # Initial state: [@new_repo, @gem_repo] (new_repo is first) + assert_equal [@new_repo, @gem_repo], Gem.sources + + # Now append the existing source + @cmd.handle_options %W[--append #{@new_repo}] + use_ui @ui do + @cmd.execute + end + + # Should be moved to end: [@gem_repo, @new_repo] + assert_equal [@gem_repo, @new_repo], Gem.sources + + expected = <<-EOF +#{@new_repo} added to sources +#{@new_repo} moved to end of sources + EOF + + assert_equal expected, @ui.output + assert_equal "", @ui.error + end + + private + + def setup_fake_source(uri) + spec_fetcher do |fetcher| + fetcher.spec "a", 1 + end + + specs = Gem::Specification.map do |spec| + [spec.name, spec.version, spec.original_platform] + end + + specs_dump_gz = StringIO.new + Zlib::GzipWriter.wrap specs_dump_gz do |io| + Marshal.dump specs, io + end + + @fetcher.data["#{uri.chomp("/")}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string + end end diff --git a/test/rubygems/test_gem_commands_uninstall_command.rb b/test/rubygems/test_gem_commands_uninstall_command.rb index 32553d1730..71ceb22ce5 100644 --- a/test/rubygems/test_gem_commands_uninstall_command.rb +++ b/test/rubygems/test_gem_commands_uninstall_command.rb @@ -513,7 +513,7 @@ WARNING: Use your OS package manager to uninstall vendor gems end msg = "ERROR: Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`" assert_empty @ui.output assert_equal msg, @ui.error.lines.last.chomp diff --git a/test/rubygems/test_gem_commands_update_command.rb b/test/rubygems/test_gem_commands_update_command.rb index 3b106e4581..5ed12ad481 100644 --- a/test/rubygems/test_gem_commands_update_command.rb +++ b/test/rubygems/test_gem_commands_update_command.rb @@ -696,6 +696,38 @@ class TestGemCommandsUpdateCommand < Gem::TestCase assert_equal expected, @cmd.fetch_remote_gems(specs["a-1"]) end + def test_pass_down_the_job_option_to_make + gemspec = nil + + spec_fetcher do |fetcher| + fetcher.download "a", 3 do |spec| + gemspec = spec + + extconf_path = "#{spec.gem_dir}/extconf.rb" + + write_file(extconf_path) do |io| + io.puts "require 'mkmf'" + io.puts "create_makefile '#{spec.name}'" + end + + spec.extensions = "extconf.rb" + end + + fetcher.gem "a", 2 + end + + use_ui @ui do + @cmd.invoke("a", "-j2") + end + + gem_make_out = File.read(File.join(gemspec.extension_dir, "gem_make.out")) + if vc_windows? && nmake_found? + refute_includes(gem_make_out, " -j2") + else + assert_includes(gem_make_out, "make -j2") + end + end + def test_handle_options_system @cmd.handle_options %w[--system] diff --git a/test/rubygems/test_gem_commands_which_command.rb b/test/rubygems/test_gem_commands_which_command.rb index cbd5b5ef14..e114d6e689 100644 --- a/test/rubygems/test_gem_commands_which_command.rb +++ b/test/rubygems/test_gem_commands_which_command.rb @@ -38,8 +38,6 @@ class TestGemCommandsWhichCommand < Gem::TestCase end def test_execute_one_missing - # TODO: this test fails in isolation - util_foo_bar @cmd.handle_options %w[foo_bar missinglib] diff --git a/test/rubygems/test_gem_commands_yank_command.rb b/test/rubygems/test_gem_commands_yank_command.rb index eb78e3a542..457a0e65c8 100644 --- a/test/rubygems/test_gem_commands_yank_command.rb +++ b/test/rubygems/test_gem_commands_yank_command.rb @@ -131,15 +131,17 @@ class TestGemCommandsYankCommand < Gem::TestCase end assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match "Successfully yanked", @ui.output end def test_with_webauthn_enabled_failure + pend "Flaky on TruffleRuby" if RUBY_ENGINE == "truffleruby" server = Gem::MockTCPServer.new error = Gem::WebauthnVerificationError.new("Something went wrong") @@ -163,9 +165,10 @@ class TestGemCommandsYankCommand < Gem::TestCase assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output refute_match "Successfully yanked", @ui.output @@ -189,9 +192,10 @@ class TestGemCommandsYankCommand < Gem::TestCase end assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "You are verified with a security device. You may close the browser window.", @ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] assert_match "Successfully yanked", @ui.output @@ -219,9 +223,10 @@ class TestGemCommandsYankCommand < Gem::TestCase assert_match @fetcher.last_request["Authorization"], Gem.configuration.rubygems_api_key assert_match %r{Yanking gem from http://example}, @ui.output - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @ui.output assert_match "ERROR: Security device verification failed: The token in the link you used has either expired " \ "or been used already.", @ui.error refute_match "You are verified with a security device. You may close the browser window.", @ui.output @@ -267,7 +272,7 @@ class TestGemCommandsYankCommand < Gem::TestCase assert_equal [yank_uri], @fetcher.paths end - def test_yank_gem_unathorized_api_key + def test_yank_gem_unauthorized_api_key response_forbidden = "The API key doesn't have access" response_success = "Successfully yanked" host = "http://example" diff --git a/test/rubygems/test_gem_config_file.rb b/test/rubygems/test_gem_config_file.rb index 4230eda4d3..3c79cb0762 100644 --- a/test/rubygems/test_gem_config_file.rb +++ b/test/rubygems/test_gem_config_file.rb @@ -43,6 +43,7 @@ class TestGemConfigFile < Gem::TestCase assert_equal [@gem_repo], Gem.sources assert_equal 365, @cfg.cert_expiration_length_days assert_equal false, @cfg.ipv4_fallback_enabled + assert_equal true, @cfg.install_extension_in_lib File.open @temp_conf, "w" do |fp| fp.puts ":backtrace: true" @@ -52,14 +53,16 @@ class TestGemConfigFile < Gem::TestCase fp.puts ":sources:" fp.puts " - http://more-gems.example.com" fp.puts "install: --wrappers" + fp.puts ":gemhome: /tmp/gems" fp.puts ":gempath:" fp.puts "- /usr/ruby/1.8/lib/ruby/gems/1.8" fp.puts "- /var/ruby/1.8/gem_home" fp.puts ":ssl_verify_mode: 0" fp.puts ":ssl_ca_cert: /etc/ssl/certs" fp.puts ":cert_expiration_length_days: 28" - fp.puts ":install_extension_in_lib: true" + fp.puts ":install_extension_in_lib: false" fp.puts ":ipv4_fallback_enabled: true" + fp.puts ":use_psych: true" end util_config_file @@ -69,13 +72,15 @@ class TestGemConfigFile < Gem::TestCase assert_equal false, @cfg.update_sources assert_equal %w[http://more-gems.example.com], @cfg.sources assert_equal "--wrappers", @cfg[:install] + assert_equal "/tmp/gems", @cfg.home assert_equal(["/usr/ruby/1.8/lib/ruby/gems/1.8", "/var/ruby/1.8/gem_home"], @cfg.path) assert_equal 0, @cfg.ssl_verify_mode assert_equal "/etc/ssl/certs", @cfg.ssl_ca_cert assert_equal 28, @cfg.cert_expiration_length_days - assert_equal true, @cfg.install_extension_in_lib + assert_equal false, @cfg.install_extension_in_lib assert_equal true, @cfg.ipv4_fallback_enabled + assert_equal true, @cfg.use_psych end def test_initialize_ipv4_fallback_enabled_env @@ -83,6 +88,53 @@ class TestGemConfigFile < Gem::TestCase util_config_file %W[--config-file #{@temp_conf}] assert_equal true, @cfg.ipv4_fallback_enabled + ensure + ENV.delete("IPV4_FALLBACK_ENABLED") + end + + def test_initialize_global_gem_cache_default + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal false, @cfg.global_gem_cache + end + + def test_initialize_global_gem_cache_env + ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] = "true" + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal true, @cfg.global_gem_cache + ensure + ENV.delete("RUBYGEMS_GLOBAL_GEM_CACHE") + end + + def test_initialize_global_gem_cache_gemrc + File.open @temp_conf, "w" do |fp| + fp.puts ":global_gem_cache: true" + end + + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal true, @cfg.global_gem_cache + end + + def test_initialize_use_psych_env + orig_use_psych = ENV["RUBYGEMS_USE_PSYCH"] + ENV["RUBYGEMS_USE_PSYCH"] = "true" + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal true, @cfg.use_psych + ensure + ENV["RUBYGEMS_USE_PSYCH"] = orig_use_psych + end + + def test_initialize_concurrent_downloads + File.open @temp_conf, "w" do |fp| + fp.puts ":concurrent_downloads: 2" + end + + util_config_file %W[--config-file #{@temp_conf}] + + assert_equal 2, @cfg.concurrent_downloads end def test_initialize_handle_arguments_config_file diff --git a/test/rubygems/test_gem_dependency_installer.rb b/test/rubygems/test_gem_dependency_installer.rb index 56b84160c4..c2fb6f264b 100644 --- a/test/rubygems/test_gem_dependency_installer.rb +++ b/test/rubygems/test_gem_dependency_installer.rb @@ -382,13 +382,9 @@ class TestGemDependencyInstaller < Gem::TestCase FileUtils.mv f1_gem, @tempdir inst = nil - pwd = Dir.getwd - Dir.chdir @tempdir - begin + Dir.chdir @tempdir do inst = Gem::DependencyInstaller.new inst.install "f" - ensure - Dir.chdir pwd end assert_equal %w[f-1], inst.installed_gems.map(&:full_name) @@ -523,6 +519,58 @@ class TestGemDependencyInstaller < Gem::TestCase assert_equal %w[a-1], inst.installed_gems.map(&:full_name) end + def test_install_local_with_extensions_already_installed + pend "needs investigation" if Gem.java_platform? + pend "ruby.h is not provided by ruby repo" if ruby_repo? + + @spec = quick_gem "a" do |s| + s.extensions << "extconf.rb" + s.files += %w[extconf.rb a.c] + end + + write_dummy_extconf "a" + + c_source_path = File.join(@tempdir, "a.c") + + write_file c_source_path do |io| + io.write <<-C + #include <ruby.h> + void Init_a() { } + C + end + + package_path = Gem::Package.build @spec + installer = Gem::Installer.at(package_path) + + # Make sure the gem is installed and backup the correct package + + installer.install + + package_bkp_path = "#{package_path}.bkp" + FileUtils.cp package_path, package_bkp_path + + # Break the extension, rebuild it, and try to install it + + write_file c_source_path do |io| + io.write "typo" + end + + Gem::Package.build @spec + + assert_raise Gem::Ext::BuildError do + installer.install + end + + # Make sure installing the good package again still works + + FileUtils.cp "#{package_path}.bkp", package_path + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new domain: :local + inst.install package_path + end + end + def test_install_minimal_deps util_setup_gems @@ -629,8 +677,7 @@ class TestGemDependencyInstaller < Gem::TestCase util_setup_gems FileUtils.mv @b1_gem, @tempdir - si = util_setup_spec_fetcher @b1 - @fetcher.data["http://gems.example.com/gems/yaml"] = si.to_yaml + util_setup_spec_fetcher @b1 inst = nil Dir.chdir @tempdir do @@ -641,6 +688,25 @@ class TestGemDependencyInstaller < Gem::TestCase assert_equal %w[b-1], inst.installed_gems.map(&:full_name) end + def test_install_force_with_unsatisfiable_dep + # foo depends on bar >= 2.0, but only bar-1.0 exists. + # With --force, the unsatisfiable dep should be skipped. + _, foo_gem = util_gem "foo", "1" do |s| + s.add_dependency "bar", ">= 2.0" + end + + util_setup_spec_fetcher(util_spec("bar", "1.0")) + FileUtils.mv foo_gem, @tempdir + inst = nil + + Dir.chdir @tempdir do + inst = Gem::DependencyInstaller.new force: true + inst.install "foo" + end + + assert_equal %w[foo-1], inst.installed_gems.map(&:full_name) + end + def test_install_build_args util_setup_gems @@ -746,13 +812,12 @@ class TestGemDependencyInstaller < Gem::TestCase inst = nil Dir.chdir @tempdir do - e = assert_raise Gem::UnsatisfiableDependencyError do + e = assert_raise Gem::DependencyResolutionError do inst = Gem::DependencyInstaller.new domain: :local inst.install "b" end - expected = "Unable to resolve dependency: 'b (>= 0)' requires 'a (>= 0)'" - assert_equal expected, e.message + assert_match(/depends on a >= 0 which could not be found in any repository/, e.message) end assert_equal [], inst.installed_gems.map(&:full_name) @@ -907,9 +972,7 @@ class TestGemDependencyInstaller < Gem::TestCase s.platform = Gem::Platform.new %w[cpu other_platform 1] end - si = util_setup_spec_fetcher @a1, a2_o - - @fetcher.data["http://gems.example.com/gems/yaml"] = si.to_yaml + util_setup_spec_fetcher @a1, a2_o a1_data = nil a2_o_data = nil @@ -1066,117 +1129,6 @@ class TestGemDependencyInstaller < Gem::TestCase assert_equal %w[activesupport-1.0.0], Gem::Specification.map(&:full_name) end - def test_find_gems_gems_with_sources - util_setup_gems - - inst = Gem::DependencyInstaller.new - dep = Gem::Dependency.new "b", ">= 0" - - Gem::Specification.reset - - set = Gem::Deprecate.skip_during do - inst.find_gems_with_sources(dep) - end - - assert_kind_of Gem::AvailableSet, set - - s = set.set.first - - assert_equal @b1, s.spec - assert_equal Gem::Source.new(@gem_repo), s.source - end - - def test_find_gems_with_sources_local - util_setup_gems - - FileUtils.mv @a1_gem, @tempdir - inst = Gem::DependencyInstaller.new - dep = Gem::Dependency.new "a", ">= 0" - set = nil - - Dir.chdir @tempdir do - set = Gem::Deprecate.skip_during do - inst.find_gems_with_sources dep - end - end - - gems = set.sorted - - assert_equal 2, gems.length - - remote, local = gems - - assert_equal "a-1", local.spec.full_name, "local spec" - assert_equal File.join(@tempdir, @a1.file_name), - local.source.download(local.spec), "local path" - - assert_equal "a-1", remote.spec.full_name, "remote spec" - assert_equal Gem::Source.new(@gem_repo), remote.source, "remote path" - end - - def test_find_gems_with_sources_prerelease - util_setup_gems - - installer = Gem::DependencyInstaller.new - - dependency = Gem::Dependency.new("a", Gem::Requirement.default) - - set = Gem::Deprecate.skip_during do - installer.find_gems_with_sources(dependency) - end - - releases = set.all_specs - - assert releases.any? {|s| s.name == "a" && s.version.to_s == "1" } - refute releases.any? {|s| s.name == "a" && s.version.to_s == "1.a" } - - dependency.prerelease = true - - set = Gem::Deprecate.skip_during do - installer.find_gems_with_sources(dependency) - end - - prereleases = set.all_specs - - assert_equal [@a1_pre, @a1], prereleases - end - - def test_find_gems_with_sources_with_best_only_and_platform - util_setup_gems - a1_x86_mingw32, = util_gem "a", "1" do |s| - s.platform = "x86-mingw32" - end - util_setup_spec_fetcher @a1, a1_x86_mingw32 - Gem.platforms << Gem::Platform.new("x86-mingw32") - - installer = Gem::DependencyInstaller.new - - dependency = Gem::Dependency.new("a", Gem::Requirement.default) - - set = Gem::Deprecate.skip_during do - installer.find_gems_with_sources(dependency, true) - end - - releases = set.all_specs - - assert_equal [a1_x86_mingw32], releases - end - - def test_find_gems_with_sources_with_bad_source - Gem.sources.replace ["http://not-there.nothing"] - - installer = Gem::DependencyInstaller.new - - dep = Gem::Dependency.new("a") - - out = Gem::Deprecate.skip_during do - installer.find_gems_with_sources(dep) - end - - assert out.empty? - assert_kind_of Gem::SourceFetchProblem, installer.errors.first - end - def test_resolve_dependencies util_setup_gems diff --git a/test/rubygems/test_gem_dependency_resolution_error.rb b/test/rubygems/test_gem_dependency_resolution_error.rb index 98a6b6b8fd..d8fa96a260 100644 --- a/test/rubygems/test_gem_dependency_resolution_error.rb +++ b/test/rubygems/test_gem_dependency_resolution_error.rb @@ -6,20 +6,23 @@ class TestGemDependencyResolutionError < Gem::TestCase def setup super - @spec = util_spec "a", 2 - - @a1_req = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - @a2_req = Gem::Resolver::DependencyRequest.new dep("a", "= 2"), nil + failure = Struct.new(:explanation).new("a depends on b (= 1.0) but no versions match") + @error = Gem::DependencyResolutionError.new failure + end - @activated = Gem::Resolver::ActivationRequest.new @spec, @a2_req + def test_message + assert_equal "a depends on b (= 1.0) but no versions match", @error.message + end - @conflict = Gem::Resolver::Conflict.new @a1_req, @activated + def test_explanation + assert_equal "a depends on b (= 1.0) but no versions match", @error.explanation + end - @error = Gem::DependencyResolutionError.new @conflict + def test_conflict + assert_nil @error.conflict end - def test_message - assert_match(/^conflicting dependencies a \(= 1\) and a \(= 2\)$/, - @error.message) + def test_conflicting_dependencies + assert_equal [], @error.conflicting_dependencies end end diff --git a/test/rubygems/test_gem_ext_builder.rb b/test/rubygems/test_gem_ext_builder.rb index 34f85e6b75..37204f3c47 100644 --- a/test/rubygems/test_gem_ext_builder.rb +++ b/test/rubygems/test_gem_ext_builder.rb @@ -18,7 +18,7 @@ class TestGemExtBuilder < Gem::TestCase @spec = util_spec "a" - @builder = Gem::Ext::Builder.new @spec, "" + @builder = Gem::Ext::Builder.new @spec end def teardown @@ -106,6 +106,22 @@ install: assert_match(/install: OK/, results) end + def test_class_run_closes_stdin + results = [] + check_stdin_script = <<~'RUBY' + if IO.select([STDIN], nil, nil, 1) + puts "STDIN: #{STDIN.read.inspect}" + else + puts "NOT_READY" + end + RUBY + + Gem::Ext::Builder.run([Gem.ruby, "-e", check_stdin_script], results) + + command_output = results.last + assert_equal "STDIN: \"\"\n", command_output + end + def test_build_extensions pend "terminates on mswin" if vc_windows? && ruby_repo? @@ -201,6 +217,57 @@ install: Gem.configuration.install_extension_in_lib = @orig_install_extension_in_lib end + def test_build_multiple_extensions + pend if RUBY_ENGINE == "truffleruby" + pend "terminates on ruby/ruby" if ruby_repo? + + extension_in_lib do + @spec.extensions << "ext/Rakefile" + @spec.extensions << "ext/extconf.rb" + + ext_dir = File.join @spec.gem_dir, "ext" + + FileUtils.mkdir_p ext_dir + + extconf_rb = File.join ext_dir, "extconf.rb" + rakefile = File.join ext_dir, "Rakefile" + + File.open extconf_rb, "w" do |f| + f.write <<-'RUBY' + require 'mkmf' + + create_makefile 'a' + RUBY + end + + File.open rakefile, "w" do |f| + f.write <<-RUBY + task :default do + FileUtils.touch File.join "#{ext_dir}", 'foo' + end + RUBY + end + + ext_lib_dir = File.join ext_dir, "lib" + FileUtils.mkdir ext_lib_dir + FileUtils.touch File.join ext_lib_dir, "a.rb" + FileUtils.mkdir File.join ext_lib_dir, "a" + FileUtils.touch File.join ext_lib_dir, "a", "b.rb" + + use_ui @ui do + @builder.build_extensions + end + + assert_path_exist @spec.extension_dir + assert_path_exist @spec.gem_build_complete_path + assert_path_exist File.join @spec.gem_dir, "ext", "foo" + assert_path_exist File.join @spec.extension_dir, "gem_make.out" + assert_path_exist File.join @spec.extension_dir, "a.rb" + assert_path_exist File.join @spec.gem_dir, "lib", "a.rb" + assert_path_exist File.join @spec.gem_dir, "lib", "a", "b.rb" + end + end + def test_build_extensions_none use_ui @ui do @builder.build_extensions diff --git a/test/rubygems/test_gem_ext_cargo_builder.rb b/test/rubygems/test_gem_ext_cargo_builder.rb index 5035937544..b970e442c2 100644 --- a/test/rubygems/test_gem_ext_cargo_builder.rb +++ b/test/rubygems/test_gem_ext_cargo_builder.rb @@ -141,6 +141,58 @@ class TestGemExtCargoBuilder < Gem::TestCase end end + def test_linker_args + orig_cc = RbConfig::MAKEFILE_CONFIG["CC"] + RbConfig::MAKEFILE_CONFIG["CC"] = "clang" + + builder = Gem::Ext::CargoBuilder.new + args = builder.send(:linker_args) + + assert args[1], "linker=clang" + assert_nil args[2] + ensure + RbConfig::MAKEFILE_CONFIG["CC"] = orig_cc + end + + def test_linker_args_with_options + orig_cc = RbConfig::MAKEFILE_CONFIG["CC"] + RbConfig::MAKEFILE_CONFIG["CC"] = "gcc -Wl,--no-undefined" + + builder = Gem::Ext::CargoBuilder.new + args = builder.send(:linker_args) + + assert args[1], "linker=clang" + assert args[3], "link-args=-Wl,--no-undefined" + ensure + RbConfig::MAKEFILE_CONFIG["CC"] = orig_cc + end + + def test_linker_args_with_cachetools + orig_cc = RbConfig::MAKEFILE_CONFIG["CC"] + RbConfig::MAKEFILE_CONFIG["CC"] = "sccache clang" + + builder = Gem::Ext::CargoBuilder.new + args = builder.send(:linker_args) + + assert args[1], "linker=clang" + assert_nil args[2] + ensure + RbConfig::MAKEFILE_CONFIG["CC"] = orig_cc + end + + def test_linker_args_with_cachetools_and_options + orig_cc = RbConfig::MAKEFILE_CONFIG["CC"] + RbConfig::MAKEFILE_CONFIG["CC"] = "ccache gcc -Wl,--no-undefined" + + builder = Gem::Ext::CargoBuilder.new + args = builder.send(:linker_args) + + assert args[1], "linker=clang" + assert args[3], "link-args=-Wl,--no-undefined" + ensure + RbConfig::MAKEFILE_CONFIG["CC"] = orig_cc + end + private def skip_unsupported_platforms! diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock index f16c0eb140..d6c49c3de1 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.lock @@ -13,16 +13,14 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", + "itertools", "proc-macro2", "quote", "regex", @@ -71,22 +69,31 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] -name = "lazy_static" -version = "1.4.0" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] [[package]] -name = "lazycell" -version = "1.3.0" +name = "lazy_static" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" @@ -127,16 +134,10 @@ dependencies = [ ] [[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -152,18 +153,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.111" +version = "0.9.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af" +checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.111" +version = "0.9.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29" +checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" dependencies = [ "bindgen", "lazy_static", @@ -193,9 +194,9 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "shell-words" diff --git a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml index a66404aa41..056567c708 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/custom_name/ext/custom_name_lib/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = "0.9.111" +rb-sys = "0.9.128" diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock index 1230f8ae96..806d51d3a1 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.lock @@ -13,16 +13,14 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.1" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ffcebc3849946a7170a05992aac39da343a90676ab392c51a4280981d6379c2" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", + "itertools", "proc-macro2", "quote", "regex", @@ -64,22 +62,31 @@ dependencies = [ ] [[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] -name = "lazy_static" -version = "1.4.0" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] [[package]] -name = "lazycell" -version = "1.3.0" +name = "lazy_static" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" @@ -120,16 +127,10 @@ dependencies = [ ] [[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - -[[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -145,18 +146,18 @@ dependencies = [ [[package]] name = "rb-sys" -version = "0.9.111" +version = "0.9.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "becea799ce051c16fb140be80f5e7cf781070f99ca099332383c2b17861249af" +checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a" dependencies = [ "rb-sys-build", ] [[package]] name = "rb-sys-build" -version = "0.9.111" +version = "0.9.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64691175abc704862f60a9ca8ef06174080cc50615f2bf1d4759f46db18b4d29" +checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a" dependencies = [ "bindgen", "lazy_static", @@ -193,9 +194,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "shell-words" diff --git a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml index 03853fea08..f0ddeeb91c 100644 --- a/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml +++ b/test/rubygems/test_gem_ext_cargo_builder/rust_ruby_example/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -rb-sys = "0.9.111" +rb-sys = "0.9.128" diff --git a/test/rubygems/test_gem_ext_cargo_builder_link_flag_converter.rb b/test/rubygems/test_gem_ext_cargo_builder_link_flag_converter.rb index a3fef50d54..3693f63df6 100644 --- a/test/rubygems/test_gem_ext_cargo_builder_link_flag_converter.rb +++ b/test/rubygems/test_gem_ext_cargo_builder_link_flag_converter.rb @@ -25,7 +25,7 @@ class TestGemExtCargoBuilderLinkFlagConverter < Gem::TestCase }.freeze CASES.each do |test_name, (arg, expected)| - raise "duplicate test name" if instance_methods.include?(test_name) + raise "duplicate test name" if method_defined?(test_name) define_method(test_name) do assert_equal(expected, Gem::Ext::CargoBuilder::LinkFlagConverter.convert(arg)) diff --git a/test/rubygems/test_gem_ext_cmake_builder.rb b/test/rubygems/test_gem_ext_cmake_builder.rb index 5f886af05f..b9b57084d4 100644 --- a/test/rubygems/test_gem_ext_cmake_builder.rb +++ b/test/rubygems/test_gem_ext_cmake_builder.rb @@ -7,7 +7,7 @@ class TestGemExtCmakeBuilder < Gem::TestCase def setup super - # Details: https://github.com/rubygems/rubygems/issues/1270#issuecomment-177368340 + # Details: https://github.com/ruby/rubygems/issues/1270#issuecomment-177368340 pend "CmakeBuilder doesn't work on Windows." if Gem.win_platform? require "open3" @@ -29,7 +29,7 @@ class TestGemExtCmakeBuilder < Gem::TestCase def test_self_build File.open File.join(@ext, "CMakeLists.txt"), "w" do |cmakelists| cmakelists.write <<-EO_CMAKE -cmake_minimum_required(VERSION 2.6) +cmake_minimum_required(VERSION 3.26) project(self_build NONE) install (FILES test.txt DESTINATION bin) EO_CMAKE @@ -39,46 +39,107 @@ install (FILES test.txt DESTINATION bin) output = [] - Gem::Ext::CmakeBuilder.build nil, @dest_path, output, [], nil, @ext + builder = Gem::Ext::CmakeBuilder.new + builder.build nil, @dest_path, output, [], @dest_path, @ext output = output.join "\n" - assert_match(/^cmake \. -DCMAKE_INSTALL_PREFIX\\=#{Regexp.escape @dest_path}/, output) + assert_match(/^current directory: #{Regexp.escape @ext}/, output) + assert_match(/cmake.*-DCMAKE_RUNTIME_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) + assert_match(/cmake.*-DCMAKE_LIBRARY_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) + assert_match(/#{Regexp.escape @ext}/, output) + end + + def test_self_build_presets + File.open File.join(@ext, "CMakeLists.txt"), "w" do |cmakelists| + cmakelists.write <<-EO_CMAKE +cmake_minimum_required(VERSION 3.26) +project(self_build NONE) +install (FILES test.txt DESTINATION bin) + EO_CMAKE + end + + File.open File.join(@ext, "CMakePresets.json"), "w" do |presets| + presets.write <<-EO_CMAKE +{ + "version": 6, + "configurePresets": [ + { + "name": "debug", + "displayName": "Debug", + "generator": "Ninja", + "binaryDir": "build/debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "displayName": "Release", + "generator": "Ninja", + "binaryDir": "build/release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ] +} + EO_CMAKE + end + + FileUtils.touch File.join(@ext, "test.txt") + + output = [] + + builder = Gem::Ext::CmakeBuilder.new + builder.build nil, @dest_path, output, [], @dest_path, @ext + + output = output.join "\n" + + assert_match(/The gem author provided a list of presets that can be used to build the gem./, output) + assert_match(/Available configure presets/, output) + assert_match(/\"debug\" - Debug/, output) + assert_match(/\"release\" - Release/, output) + assert_match(/^current directory: #{Regexp.escape @ext}/, output) + assert_match(/cmake.*-DCMAKE_RUNTIME_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) + assert_match(/cmake.*-DCMAKE_LIBRARY_OUTPUT_DIRECTORY\\=#{Regexp.escape @dest_path}/, output) assert_match(/#{Regexp.escape @ext}/, output) - assert_contains_make_command "", output - assert_contains_make_command "install", output - assert_match(/test\.txt/, output) end def test_self_build_fail output = [] + builder = Gem::Ext::CmakeBuilder.new error = assert_raise Gem::InstallError do - Gem::Ext::CmakeBuilder.build nil, @dest_path, output, [], nil, @ext + builder.build nil, @dest_path, output, [], @dest_path, @ext end - output = output.join "\n" + assert_match "cmake_configure failed", error.message shell_error_msg = /(CMake Error: .*)/ - - assert_match "cmake failed", error.message - - assert_match(/^cmake . -DCMAKE_INSTALL_PREFIX\\=#{Regexp.escape @dest_path}/, output) + output = output.join "\n" assert_match(/#{shell_error_msg}/, output) + assert_match(/CMake Error: The source directory .* does not appear to contain CMakeLists.txt./, output) end def test_self_build_has_makefile - File.open File.join(@ext, "Makefile"), "w" do |makefile| - makefile.puts "all:\n\t@echo ok\ninstall:\n\t@echo ok" + File.open File.join(@ext, "CMakeLists.txt"), "w" do |cmakelists| + cmakelists.write <<-EO_CMAKE +cmake_minimum_required(VERSION 3.26) +project(self_build NONE) +install (FILES test.txt DESTINATION bin) + EO_CMAKE end output = [] - Gem::Ext::CmakeBuilder.build nil, @dest_path, output, [], nil, @ext + builder = Gem::Ext::CmakeBuilder.new + builder.build nil, @dest_path, output, [], @dest_path, @ext output = output.join "\n" - assert_contains_make_command "", output - assert_contains_make_command "install", output + # The default generator will create a Makefile in the build directory + makefile = File.join(@ext, "build", "Makefile") + assert(File.exist?(makefile)) end end diff --git a/test/rubygems/test_gem_ext_ext_conf_builder.rb b/test/rubygems/test_gem_ext_ext_conf_builder.rb index 218c6f3d5e..bc383e5540 100644 --- a/test/rubygems/test_gem_ext_ext_conf_builder.rb +++ b/test/rubygems/test_gem_ext_ext_conf_builder.rb @@ -15,15 +15,12 @@ class TestGemExtExtConfBuilder < Gem::TestCase end def test_class_build - if Gem.java_platform? - pend("failing on jruby") - end - if vc_windows? && !nmake_found? pend("test_class_build skipped - nmake not found") end File.open File.join(@ext, "extconf.rb"), "w" do |extconf| + extconf.puts "return if Gem.java_platform?" extconf.puts "require 'mkmf'\ncreate_makefile 'foo'" end @@ -35,20 +32,22 @@ class TestGemExtExtConfBuilder < Gem::TestCase assert_match(/^current directory:/, output[0]) assert_match(/^#{Regexp.quote(Gem.ruby)}.* extconf.rb/, output[1]) - assert_equal "creating Makefile\n", output[2] - assert_match(/^current directory:/, output[3]) - assert_contains_make_command "clean", output[4] - assert_contains_make_command "", output[7] - assert_contains_make_command "install", output[10] + + if Gem.java_platform? + assert_includes(output, "Skipping make for extconf.rb as no Makefile was found.") + else + assert_equal "creating Makefile\n", output[2] + assert_match(/^current directory:/, output[3]) + assert_contains_make_command "clean", output[4] + assert_contains_make_command "", output[7] + assert_contains_make_command "install", output[10] + end + assert_empty Dir.glob(File.join(@ext, "siteconf*.rb")) assert_empty Dir.glob(File.join(@ext, ".gem.*")) end def test_class_build_rbconfig_make_prog - if Gem.java_platform? - pend("failing on jruby") - end - configure_args do File.open File.join(@ext, "extconf.rb"), "w" do |extconf| extconf.puts "require 'mkmf'\ncreate_makefile 'foo'" @@ -72,10 +71,6 @@ class TestGemExtExtConfBuilder < Gem::TestCase env_large_make = ENV.delete "MAKE" ENV["MAKE"] = "anothermake" - if Gem.java_platform? - pend("failing on jruby") - end - configure_args "" do File.open File.join(@ext, "extconf.rb"), "w" do |extconf| extconf.puts "require 'mkmf'\ncreate_makefile 'foo'" @@ -206,11 +201,11 @@ end end def test_class_make_no_Makefile - error = assert_raise Gem::InstallError do + error = assert_raise Gem::Ext::Builder::NoMakefileError do Gem::Ext::ExtConfBuilder.make @ext, ["output"], @ext end - assert_equal "Makefile not found", error.message + assert_match(/No Makefile found/, error.message) end def configure_args(args = nil) diff --git a/test/rubygems/test_gem_ext_rake_builder.rb b/test/rubygems/test_gem_ext_rake_builder.rb index bd72c1aa08..68ad15b044 100644 --- a/test/rubygems/test_gem_ext_rake_builder.rb +++ b/test/rubygems/test_gem_ext_rake_builder.rb @@ -29,7 +29,7 @@ class TestGemExtRakeBuilder < Gem::TestCase end end - # https://github.com/rubygems/rubygems/pull/1819 + # https://github.com/ruby/rubygems/pull/1819 # # It should not fail with a non-empty args list either def test_class_build_with_args diff --git a/test/rubygems/test_gem_gem_runner.rb b/test/rubygems/test_gem_gem_runner.rb index 4fb205040c..9cc2fac619 100644 --- a/test/rubygems/test_gem_gem_runner.rb +++ b/test/rubygems/test_gem_gem_runner.rb @@ -82,17 +82,6 @@ class TestGemGemRunner < Gem::TestCase assert_equal %w[--foo], args end - def test_query_is_deprecated - args = %w[query] - - use_ui @ui do - @runner.run(args) - end - - assert_match(/WARNING: query command is deprecated. It will be removed in Rubygems [0-9]+/, @ui.error) - assert_match(/WARNING: It is recommended that you use `gem search` or `gem list` instead/, @ui.error) - end - def test_info_succeeds args = %w[info] diff --git a/test/rubygems/test_gem_gemcutter_utilities.rb b/test/rubygems/test_gem_gemcutter_utilities.rb index a3236e6276..ca34c8d03d 100644 --- a/test/rubygems/test_gem_gemcutter_utilities.rb +++ b/test/rubygems/test_gem_gemcutter_utilities.rb @@ -150,7 +150,7 @@ class TestGemGemcutterUtilities < Gem::TestCase util_sign_in - assert_equal "", @sign_in_ui.output + assert_match(/You are already signed in/, @sign_in_ui.output) end def test_sign_in_skips_with_key_override @@ -158,7 +158,7 @@ class TestGemGemcutterUtilities < Gem::TestCase @cmd.options[:key] = :KEY util_sign_in - assert_equal "", @sign_in_ui.output + assert_match(/You are already signed in/, @sign_in_ui.output) end def test_sign_in_with_other_credentials_doesnt_overwrite_other_keys @@ -233,9 +233,10 @@ class TestGemGemcutterUtilities < Gem::TestCase end end - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @sign_in_ui.output assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] end @@ -255,9 +256,10 @@ class TestGemGemcutterUtilities < Gem::TestCase end assert_equal 1, error.exit_code - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @sign_in_ui.output assert_match "ERROR: Security device verification failed: Something went wrong", @sign_in_ui.error refute_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output refute_match "Signed in with API key:", @sign_in_ui.output @@ -273,9 +275,10 @@ class TestGemGemcutterUtilities < Gem::TestCase util_sign_in end - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @sign_in_ui.output assert_match "You are verified with a security device. You may close the browser window.", @sign_in_ui.output assert_equal "Uvh6T57tkWuUnWYo", @fetcher.last_request["OTP"] end @@ -292,9 +295,10 @@ class TestGemGemcutterUtilities < Gem::TestCase end end - assert_match "You have enabled multi-factor authentication. Please visit #{@fetcher.webauthn_url_with_port(server.port)} " \ + assert_match "You have enabled multi-factor authentication. Please visit the following URL " \ "to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, " \ "you can re-run the gem signin command with the `--otp [your_code]` option.", @sign_in_ui.output + assert_match @fetcher.webauthn_url_with_port(server.port), @sign_in_ui.output assert_match "ERROR: Security device verification failed: " \ "The token in the link you used has either expired or been used already.", @sign_in_ui.error end diff --git a/test/rubygems/test_gem_impossible_dependencies_error.rb b/test/rubygems/test_gem_impossible_dependencies_error.rb deleted file mode 100644 index 94c0290ea1..0000000000 --- a/test/rubygems/test_gem_impossible_dependencies_error.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" - -class TestGemImpossibleDependenciesError < Gem::TestCase - def test_message_conflict - request = dependency_request dep("net-ssh", ">= 2.0.13"), "rye", "0.9.8" - - conflicts = [] - - # These conflicts are lies as their dependencies does not have the correct - # requested-by entries, but they are suitable for testing the message. - # See #485 to construct a correct conflict. - net_ssh_2_2_2 = - dependency_request dep("net-ssh", ">= 2.6.5"), "net-ssh", "2.2.2", request - net_ssh_2_6_5 = - dependency_request dep("net-ssh", "~> 2.2.2"), "net-ssh", "2.6.5", request - - conflict1 = Gem::Resolver::Conflict.new \ - net_ssh_2_6_5, net_ssh_2_6_5.requester - - conflict2 = Gem::Resolver::Conflict.new \ - net_ssh_2_2_2, net_ssh_2_2_2.requester - - conflicts << [net_ssh_2_6_5.requester.spec, conflict1] - conflicts << [net_ssh_2_2_2.requester.spec, conflict2] - - error = Gem::ImpossibleDependenciesError.new request, conflicts - - expected = <<-EXPECTED -rye-0.9.8 requires net-ssh (>= 2.0.13) but it conflicted: - Activated net-ssh-2.6.5 - which does not match conflicting dependency (~> 2.2.2) - - Conflicting dependency chains: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.6.5 activated - - versus: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.6.5 activated, depends on - net-ssh (~> 2.2.2) - - Activated net-ssh-2.2.2 - which does not match conflicting dependency (>= 2.6.5) - - Conflicting dependency chains: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.2.2 activated - - versus: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.2.2 activated, depends on - net-ssh (>= 2.6.5) - - EXPECTED - - assert_equal expected, error.message - end -end diff --git a/test/rubygems/test_gem_install_update_options.rb b/test/rubygems/test_gem_install_update_options.rb index 8fd5d9c543..1e451dcb05 100644 --- a/test/rubygems/test_gem_install_update_options.rb +++ b/test/rubygems/test_gem_install_update_options.rb @@ -202,4 +202,16 @@ class TestGemInstallUpdateOptions < Gem::InstallerTestCase assert_equal true, @cmd.options[:minimal_deps] end + + def test_build_jobs_short_version + @cmd.handle_options %w[-j 4] + + assert_equal 4, @cmd.options[:build_jobs] + end + + def test_build_jobs_long_version + @cmd.handle_options %w[--build-jobs 4] + + assert_equal 4, @cmd.options[:build_jobs] + end end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index 83e43c135f..8947694f53 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -24,36 +24,35 @@ class TestGemInstaller < Gem::InstallerTestCase util_make_exec @spec, "" - expected = <<-EOF -#!#{Gem.ruby} -# -# This file was generated by RubyGems. -# -# The application 'a' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require 'rubygems' - -Gem.use_gemdeps - -version = \">= 0.a\" - -str = ARGV.first -if str - str = str.b[/\\A_(.*)_\\z/, 1] - if str and Gem::Version.correct?(str) - version = str - ARGV.shift - end -end + expected = <<~EOF + #!#{Gem.ruby} + # + # This file was generated by RubyGems. + # + # The application 'a' is installed as part of a gem, and + # this file is here to facilitate running it. + # + + require 'rubygems' + + Gem.use_gemdeps + + version = \">= 0.a\" + + str = ARGV.first + if str + str = str.b[/\\A_(.*)_\\z/, 1] + if str and Gem::Version.correct?(str) + version = str + ARGV.shift + end + end -if Gem.respond_to?(:activate_bin_path) -load Gem.activate_bin_path('a', 'executable', version) -else -gem "a", version -load Gem.bin_path("a", "executable", version) -end + if Gem.respond_to?(:activate_and_load_bin_path) + Gem.activate_and_load_bin_path('a', 'executable', version) + else + load Gem.activate_bin_path('a', 'executable', version) + end EOF wrapper = installer.app_script_text "executable" @@ -61,6 +60,21 @@ end end end + def test_app_script_text_escapes_executable_name + installer = setup_base_installer + + malicious = "evil');system('id');#" + @spec.bindir = "bin" + write_file @spec.bin_file(malicious) do |io| + io.puts "#!/usr/bin/ruby" + end + + wrapper = installer.app_script_text malicious + + assert_includes wrapper, %q{Gem.activate_and_load_bin_path('a', 'evil\');system(\'id\');#', version)} + assert_includes wrapper, %q{load Gem.activate_bin_path('a', 'evil\');system(\'id\');#', version)} + end + def test_check_executable_overwrite installer = setup_base_installer @@ -121,12 +135,12 @@ end end File.open File.join(util_inst_bindir, "executable"), "w" do |io| - io.write <<-EXEC -#!/usr/local/bin/ruby -# -# This file was generated by RubyGems + io.write <<~EXEC + #!/usr/local/bin/ruby + # + # This file was generated by RubyGems -gem 'other', version + gem 'other', version EXEC end @@ -690,8 +704,11 @@ gem 'other', version def test_generate_bin_symlink_win32 old_win_platform = Gem.win_platform? - Gem.win_platform = true old_alt_separator = File::ALT_SEPARATOR + + omit "JRuby on Windows still creates the symlink so the wrapper branch is not exercised" if Gem.win_platform? && Gem.java_platform? + + Gem.win_platform = true File.__send__(:remove_const, :ALT_SEPARATOR) File.const_set(:ALT_SEPARATOR, "\\") @@ -744,6 +761,8 @@ gem 'other', version end def test_generate_bin_with_dangling_symlink + omit "JRuby on Windows still creates the symlink so the wrapper branch is not exercised" if Gem.win_platform? && Gem.java_platform? + gem_with_dangling_symlink = File.expand_path("packages/ascii_binder-0.1.10.1.gem", __dir__) installer = Gem::Installer.at( @@ -760,8 +779,12 @@ gem 'other', version errors = @ui.error.split("\n") assert_equal "WARNING: ascii_binder-0.1.10.1 ships with a dangling symlink named bin/ascii_binder pointing to missing bin/asciibinder file. Ignoring", errors.shift - assert_empty errors - + if symlink_supported? + assert_empty errors + else + assert_match(/Unable to use symlinks, installing wrapper/i, + errors.to_s) + end assert_empty @ui.output end @@ -869,11 +892,11 @@ gem 'other', version spec_version = spec.version plugin_path = File.join("lib", "rubygems_plugin.rb") write_file File.join(@tempdir, plugin_path) do |io| - io.write <<-PLUGIN -#{self.class}.plugin_loaded = true -Gem.post_install do - #{self.class}.post_install_is_called = true -end + io.write <<~PLUGIN + #{self.class}.plugin_loaded = true + Gem.post_install do + #{self.class}.post_install_is_called = true + end PLUGIN end spec.files += [plugin_path] @@ -1247,7 +1270,7 @@ end end assert_raise(Gem::Ext::BuildError) do - installer.install + build_rake_in { installer.install } end assert_path_not_exist(File.join(installer.bin_dir, "executable.lock")) @@ -1473,17 +1496,45 @@ end refute_match(/I am a shiny gem!/, @ui.output) end + def test_install_sanitizes_post_install_message + # Use for_spec so the in-memory message reaches the installer verbatim; + # building a gem would escape the control characters during serialization. + @spec = setup_base_spec + @spec.post_install_message = "shiny \e]2;pwn\a gem" + + installer = Gem::Installer.for_spec @spec, post_install_message: true + installer.gem_home = @gemhome + + use_ui @ui do + installer.install + end + + assert_match(/shiny \.\]2;pwn\. gem/, @ui.output) + refute_match(/\e\]2;pwn/, @ui.output) + end + + def test_install_handles_non_string_post_install_message + # post_install_message may be a non-String (the gemspec schema allows an + # array), so sanitizing must not assume it responds to gsub. + @spec = setup_base_spec + @spec.post_install_message = %w[one two] + + installer = Gem::Installer.for_spec @spec, post_install_message: true + installer.gem_home = @gemhome + + use_ui @ui do + installer.install + end + + assert_match(/one/, @ui.output) + end + def test_install_extension_dir gemhome2 = "#{@gemhome}2" @spec = setup_base_spec @spec.extensions << "extconf.rb" - write_file File.join(@tempdir, "extconf.rb") do |io| - io.write <<-RUBY - require "mkmf" - create_makefile("#{@spec.name}") - RUBY - end + write_dummy_extconf @spec.name @spec.files += %w[extconf.rb] @@ -1503,12 +1554,7 @@ end @spec = setup_base_spec @spec.extensions << "extconf.rb" - write_file File.join(@tempdir, "extconf.rb") do |io| - io.write <<-RUBY - require "mkmf" - create_makefile("#{@spec.name}") - RUBY - end + write_dummy_extconf @spec.name @spec.files += %w[extconf.rb] @@ -1539,12 +1585,7 @@ end def test_install_user_extension_dir @spec = setup_base_spec @spec.extensions << "extconf.rb" - write_file File.join(@tempdir, "extconf.rb") do |io| - io.write <<-RUBY - require "mkmf" - create_makefile("#{@spec.name}") - RUBY - end + write_dummy_extconf @spec.name @spec.files += %w[extconf.rb] @@ -1571,22 +1612,20 @@ end @spec = setup_base_spec @spec.extensions << "extconf.rb" - write_file File.join(@tempdir, "extconf.rb") do |io| - io.write <<-RUBY - require "mkmf" + write_dummy_extconf @spec.name do |io| + io.write <<~RUBY CONFIG['CC'] = '$(TOUCH) $@ ||' CONFIG['LDSHARED'] = '$(TOUCH) $@ ||' $ruby = '#{Gem.ruby}' - create_makefile("#{@spec.name}") RUBY end write_file File.join(@tempdir, "depend") write_file File.join(@tempdir, "a.c") do |io| - io.write <<-C + io.write <<~C #include <ruby.h> void Init_a() { } C @@ -1618,17 +1657,12 @@ end @spec = setup_base_spec @spec.extensions << "extconf.rb" - write_file File.join(@tempdir, "extconf.rb") do |io| - io.write <<-RUBY - require "mkmf" - create_makefile("#{@spec.name}") - RUBY - end + write_dummy_extconf @spec.name rb = File.join("lib", "#{@spec.name}.rb") @spec.files += [rb] write_file File.join(@tempdir, rb) do |io| - io.write <<-RUBY + io.write <<~RUBY # #{@spec.name}.rb RUBY end @@ -1637,7 +1671,7 @@ end rb2 = File.join("lib", @spec.name, "#{@spec.name}.rb") @spec.files << rb2 write_file File.join(@tempdir, rb2) do |io| - io.write <<-RUBY + io.write <<~RUBY # #{@spec.name}/#{@spec.name}.rb RUBY end @@ -1663,15 +1697,13 @@ end @spec.extensions << "extconf.rb" - write_file File.join(@tempdir, "extconf.rb") do |io| - io.write <<-RUBY - require "mkmf" + write_dummy_extconf @spec.name do |io| + io.write <<~RUBY CONFIG['CC'] = '$(TOUCH) $@ ||' CONFIG['LDSHARED'] = '$(TOUCH) $@ ||' $ruby = '#{Gem.ruby}' - create_makefile("#{@spec.name}") RUBY end @@ -1698,13 +1730,13 @@ end @spec.require_paths = ["."] @spec.extensions << "extconf.rb" - File.write File.join(@tempdir, "extconf.rb"), <<-RUBY - require "mkmf" - CONFIG['CC'] = '$(TOUCH) $@ ||' - CONFIG['LDSHARED'] = '$(TOUCH) $@ ||' - $ruby = '#{Gem.ruby}' - create_makefile("#{@spec.name}") - RUBY + write_dummy_extconf @spec.name do |io| + io.write <<~RUBY + CONFIG['CC'] = '$(TOUCH) $@ ||' + CONFIG['LDSHARED'] = '$(TOUCH) $@ ||' + $ruby = '#{Gem.ruby}' + RUBY + end # empty depend file for no auto dependencies @spec.files += %W[depend #{@spec.name}.c].each do |file| @@ -1937,11 +1969,87 @@ end end end + def test_pre_install_checks_malicious_executables_before_eval + spec = util_spec "malicious", "1" + def spec.full_name # so the spec is buildable + "malicious-1" + end + + def spec.validate(*args); end + spec.executables = ["../../../tmp/malicious"] + + util_build_gem spec + + gem = File.join(@gemhome, "cache", spec.file_name) + + use_ui @ui do + installer = Gem::Installer.at gem + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "#<Gem::Specification name=malicious version=1> has an invalid executable", e.message + end + end + + def test_pre_install_checks_malicious_bindir_before_eval + spec = util_spec "malicious", "1" + def spec.full_name # so the spec is buildable + "malicious-1" + end + + def spec.validate(*args); end + spec.bindir = "../../../tmp/malicious" + + util_build_gem spec + + gem = File.join(@gemhome, "cache", spec.file_name) + + use_ui @ui do + installer = Gem::Installer.at gem + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "#<Gem::Specification name=malicious version=1> has an invalid bindir", e.message + end + end + + def test_pre_install_checks_non_string_executable + spec = util_spec "malicious", "1" + def spec.validate(*args); end + spec.executables = [nil] + + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome + + use_ui @ui do + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "#<Gem::Specification name=malicious version=1> has an invalid executable", e.message + end + end + + def test_pre_install_checks_non_string_bindir + spec = util_spec "malicious", "1" + def spec.validate(*args); end + spec.bindir = true + + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome + + use_ui @ui do + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "#<Gem::Specification name=malicious version=1> has an invalid bindir", e.message + end + end + def test_pre_install_checks_malicious_platform_before_eval - gem_with_ill_formated_platform = File.expand_path("packages/ill-formatted-platform-1.0.0.10.gem", __dir__) + gem_with_ill_formatted_platform = File.expand_path("packages/ill-formatted-platform-1.0.0.10.gem", __dir__) installer = Gem::Installer.at( - gem_with_ill_formated_platform, + gem_with_ill_formatted_platform, install_dir: @gemhome, user_install: false, force: true @@ -2304,19 +2412,6 @@ end assert_equal "#!1 #{bin_env} 2 #{Gem.ruby} -ws 3 executable", shebang end - def test_unpack - installer = util_setup_installer - - dest = File.join @gemhome, "gems", @spec.full_name - - Gem::Deprecate.skip_during do - installer.unpack dest - end - - assert_path_exist File.join dest, "lib", "code.rb" - assert_path_exist File.join dest, "bin", "executable" - end - def test_write_build_info_file installer = setup_base_installer @@ -2423,25 +2518,31 @@ end installer = Gem::Installer.for_spec @spec installer.gem_home = @gemhome - File.singleton_class.class_eval do - alias_method :original_binwrite, :binwrite - - def binwrite(path, data) + assert_raise(Errno::ENOSPC) do + Gem::AtomicFileWriter.open(@spec.spec_file) do raise Errno::ENOSPC end end - assert_raise Errno::ENOSPC do - installer.write_spec - end - assert_path_not_exist @spec.spec_file - ensure - File.singleton_class.class_eval do - remove_method :binwrite - alias_method :binwrite, :original_binwrite - remove_method :original_binwrite - end + end + + def test_write_default_spec + @spec = setup_base_spec + @spec.files = %w[a.rb b.rb c.rb] + + installer = Gem::Installer.for_spec @spec + installer.gem_home = @gemhome + + installer.write_default_spec + + assert_path_exist installer.default_spec_file + + loaded = Gem::Specification.load installer.default_spec_file + + assert_equal @spec.files, loaded.files + assert_equal @spec.name, loaded.name + assert_equal @spec.version, loaded.version end def test_dir @@ -2450,137 +2551,154 @@ end assert_match %r{/gemhome/gems/a-2$}, installer.dir end - def test_default_gem_loaded_from - spec = util_spec "a" - installer = Gem::Installer.for_spec spec, install_as_default: true - installer.install - assert_predicate spec, :default_gem? + def test_package_attribute + gem = quick_gem "c" do |spec| + util_make_exec spec, "#!/usr/bin/ruby", "exe" + end + + installer = util_installer(gem, @gemhome) + assert_respond_to(installer, :package) + assert_kind_of(Gem::Package, installer.package) end - def test_default_gem_without_wrappers - installer = setup_base_installer + def test_gem_attribute + gem = quick_gem "c" do |spec| + util_make_exec spec, "#!/usr/bin/ruby", "exe" + end + + installer = util_installer(gem, @gemhome) + assert_respond_to(installer, :gem) + assert_kind_of(String, installer.gem) + end - FileUtils.rm_rf File.join(Gem.default_dir, "specifications") + def test_install_no_build_extension + installer = util_setup_installer - installer.wrappers = false - installer.options[:install_as_default] = true - installer.gem_dir = @spec.gem_dir + gemdir = File.join @gemhome, "gems", @spec.full_name + + installer.options[:build_extension] = false use_ui @ui do installer.install end - assert_directory_exists File.join(@spec.gem_dir, "bin") - installed_exec = File.join @spec.gem_dir, "bin", "executable" - assert_path_exist installed_exec - - assert_directory_exists File.join(Gem.default_dir, "specifications") - assert_directory_exists File.join(Gem.default_dir, "specifications", "default") - - default_spec = eval File.read File.join(Gem.default_dir, "specifications", "default", "a-2.gemspec") - assert_equal Gem::Version.new("2"), default_spec.version - assert_equal ["bin/executable"], default_spec.files + assert_path_exist gemdir + assert_path_not_exist File.join(@spec.extension_dir, "gem.build_complete") + assert_match "contains native extensions that were not built", @ui.error + assert_match "gem pristine #{@spec.name} --extensions", @ui.error + end - assert_directory_exists util_inst_bindir + def test_install_no_build_extension_without_extensions + spec = quick_gem "b", 2 - installed_exec = File.join util_inst_bindir, "executable" - assert_path_exist installed_exec + util_build_gem spec - wrapper = File.read installed_exec + installer = util_installer spec, @gemhome + installer.options[:build_extension] = false - if symlink_supported? - refute_match(/generated by RubyGems/, wrapper) - else # when symlink not supported, it warns and fallbacks back to installing wrapper - assert_match(/Unable to use symlinks, installing wrapper/, @ui.error) - assert_match(/generated by RubyGems/, wrapper) + use_ui @ui do + installer.install end - end - def test_default_gem_with_wrappers - installer = setup_base_installer + refute_match "contains native extensions", @ui.error + end - installer.wrappers = true - installer.options[:install_as_default] = true - installer.gem_dir = @spec.gem_dir + def test_install_no_install_plugin + installer = util_setup_installer do |spec| + write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io| + io.write "# do nothing" + end - use_ui @ui do - installer.install + spec.files += %w[lib/rubygems_plugin.rb] end - assert_directory_exists util_inst_bindir + installer.options[:install_plugin] = false - installed_exec = File.join util_inst_bindir, "executable" - assert_path_exist installed_exec + build_rake_in do + use_ui @ui do + installer.install + end + end - wrapper = File.read installed_exec - assert_match(/generated by RubyGems/, wrapper) + plugin_path = File.join Gem.plugindir, "a_plugin.rb" + refute File.exist?(plugin_path), "plugin must not be written when --no-install-plugin" + assert_match "contains plugins that were not installed", @ui.error + assert_match "gem pristine #{@spec.name} --only-plugins", @ui.error end - def test_default_gem_with_exe_as_bindir - @spec = quick_gem "c" do |spec| - util_make_exec spec, "#!/usr/bin/ruby", "exe" + def test_install_no_install_plugin_skips_load_plugin + installer = util_setup_installer do |spec| + write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io| + io.write "$no_install_plugin_test_loaded = true" + end + + spec.files += %w[lib/rubygems_plugin.rb] end - util_build_gem @spec + # Simulate a pre-existing plugin wrapper from a previous install + FileUtils.mkdir_p Gem.plugindir + plugin_path = File.join Gem.plugindir, "a_plugin.rb" + File.write(plugin_path, "require_relative '../../gems/#{@spec.full_name}/lib/rubygems_plugin'") - @spec.cache_file + installer.options[:install_plugin] = false - installer = util_installer @spec, @gemhome + build_rake_in do + use_ui @ui do + installer.install + end + end - installer.options[:install_as_default] = true - installer.gem_dir = @spec.gem_dir + refute defined?($no_install_plugin_test_loaded) && $no_install_plugin_test_loaded, + "plugin must not be loaded when --no-install-plugin" + ensure + $no_install_plugin_test_loaded = nil + end - use_ui @ui do - installer.install - end + def test_install_no_install_plugin_without_plugins + installer = util_setup_installer - assert_directory_exists File.join(@spec.gem_dir, "exe") - installed_exec = File.join @spec.gem_dir, "exe", "executable" - assert_path_exist installed_exec + installer.options[:install_plugin] = false - assert_directory_exists File.join(Gem.default_dir, "specifications") - assert_directory_exists File.join(Gem.default_dir, "specifications", "default") + build_rake_in do + use_ui @ui do + installer.install + end + end - default_spec = eval File.read File.join(Gem.default_dir, "specifications", "default", "c-2.gemspec") - assert_equal Gem::Version.new("2"), default_spec.version - assert_equal ["exe/executable"], default_spec.files + refute_match "contains plugins", @ui.error end - def test_default_gem_to_specific_install_dir - @gem = setup_base_gem - installer = util_installer @spec, "#{@gemhome}2" - installer.options[:install_as_default] = true + def test_install_no_install_plugin_removes_stale_wrappers + # First install a version with a plugin + installer = util_setup_installer do |spec| + write_file File.join(@tempdir, "lib", "rubygems_plugin.rb") do |io| + io.write "# plugin code" + end - use_ui @ui do - installer.install + spec.files += %w[lib/rubygems_plugin.rb] end - assert_directory_exists File.join("#{@gemhome}2", "specifications") - assert_directory_exists File.join("#{@gemhome}2", "specifications", "default") + build_rake_in do + use_ui @ui do + installer.install + end + end - default_spec = eval File.read File.join("#{@gemhome}2", "specifications", "default", "a-2.gemspec") - assert_equal Gem::Version.new("2"), default_spec.version - assert_equal ["bin/executable"], default_spec.files - end + plugin_path = File.join Gem.plugindir, "a_plugin.rb" + assert File.exist?(plugin_path), "plugin wrapper should exist after first install" - def test_package_attribute - gem = quick_gem "c" do |spec| - util_make_exec spec, "#!/usr/bin/ruby", "exe" - end + # Now install a new version without plugins, using --no-install-plugin + spec2 = quick_gem "a", 3 + util_build_gem spec2 - installer = util_installer(gem, @gemhome) - assert_respond_to(installer, :package) - assert_kind_of(Gem::Package, installer.package) - end + installer2 = util_installer spec2, @gemhome + installer2.options[:install_plugin] = false - def test_gem_attribute - gem = quick_gem "c" do |spec| - util_make_exec spec, "#!/usr/bin/ruby", "exe" + use_ui @ui do + installer2.install end - installer = util_installer(gem, @gemhome) - assert_respond_to(installer, :gem) - assert_kind_of(String, installer.gem) + refute File.exist?(plugin_path), "stale plugin wrapper must be removed" end private diff --git a/test/rubygems/test_gem_name_tuple.rb b/test/rubygems/test_gem_name_tuple.rb index bdb8181ce8..4876737c83 100644 --- a/test/rubygems/test_gem_name_tuple.rb +++ b/test/rubygems/test_gem_name_tuple.rb @@ -57,4 +57,41 @@ class TestGemNameTuple < Gem::TestCase assert_equal 1, a_p.<=>(a) end + + def test_deconstruct + name_tuple = Gem::NameTuple.new "rails", Gem::Version.new("7.0.0"), "ruby" + assert_equal ["rails", Gem::Version.new("7.0.0"), "ruby"], name_tuple.deconstruct + end + + def test_deconstruct_keys + name_tuple = Gem::NameTuple.new "rails", Gem::Version.new("7.0.0"), "x86_64-linux" + keys = name_tuple.deconstruct_keys(nil) + assert_equal "rails", keys[:name] + assert_equal Gem::Version.new("7.0.0"), keys[:version] + assert_equal "x86_64-linux", keys[:platform] + end + + def test_pattern_matching_array + name_tuple = Gem::NameTuple.new "rails", Gem::Version.new("7.0.0"), "ruby" + result = + case name_tuple + in [name, version, "ruby"] + "#{name}-#{version}" + else + "no match" + end + assert_equal "rails-7.0.0", result + end + + def test_pattern_matching_hash + name_tuple = Gem::NameTuple.new "rails", Gem::Version.new("7.0.0"), "ruby" + result = + case name_tuple + in name: "rails", version:, platform: "ruby" + version.to_s + else + "no match" + end + assert_equal "7.0.0", result + end end diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index 8a9cc85580..0014c20737 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -175,6 +175,9 @@ class TestGemPackage < Gem::Package::TarTestCase end def test_add_files_symlink + unless symlink_supported? + omit("symlink - developer mode must be enabled on Windows") + end spec = Gem::Specification.new spec.files = %w[lib/code.rb lib/code_sym.rb lib/code_sym2.rb] @@ -185,16 +188,8 @@ class TestGemPackage < Gem::Package::TarTestCase end # NOTE: 'code.rb' is correct, because it's relative to lib/code_sym.rb - begin - File.symlink("code.rb", "lib/code_sym.rb") - File.symlink("../lib/code.rb", "lib/code_sym2.rb") - rescue Errno::EACCES => e - if Gem.win_platform? - pend "symlink - must be admin with no UAC on Windows" - else - raise e - end - end + File.symlink("code.rb", "lib/code_sym.rb") + File.symlink("../lib/code.rb", "lib/code_sym2.rb") package = Gem::Package.new "bogus.gem" package.spec = spec @@ -506,7 +501,7 @@ class TestGemPackage < Gem::Package::TarTestCase extracted = File.join @destination, "lib/code.rb" assert_path_exist extracted - mask = 0o100666 & (~File.umask) + mask = 0o100666 & ~File.umask assert_equal mask.to_s(8), File.stat(extracted).mode.to_s(8) unless Gem.win_platform? @@ -583,25 +578,71 @@ class TestGemPackage < Gem::Package::TarTestCase tar.add_symlink "lib/foo.rb", "../relative.rb", 0o644 end - begin - package.extract_tar_gz tgz_io, @destination - rescue Errno::EACCES => e - if Gem.win_platform? - pend "symlink - must be admin with no UAC on Windows" - else - raise e - end - end + package.extract_tar_gz tgz_io, @destination extracted = File.join @destination, "lib/foo.rb" assert_path_exist extracted - assert_equal "../relative.rb", - File.readlink(extracted) + if symlink_supported? + assert_equal "../relative.rb", + File.readlink(extracted) + end assert_equal "hi", + File.read(extracted), + "should read file content either by following symlink or on Windows by reading copy" + end + + def test_extract_tar_gz_symlink_directory + package = Gem::Package.new @gem + package.verify + + tgz_io = util_tar_gz do |tar| + tar.add_symlink "link", "lib/orig", 0o644 + tar.mkdir "lib", 0o755 + tar.mkdir "lib/orig", 0o755 + tar.add_file "lib/orig/file.rb", 0o644 do |io| + io.write "ok" + end + end + + package.extract_tar_gz tgz_io, @destination + extracted = File.join @destination, "link/file.rb" + assert_path_exist extracted + if symlink_supported? + assert_equal "lib/orig", + File.readlink(File.dirname(extracted)) + end + assert_equal "ok", File.read(extracted) end + def test_extract_tar_gz_rejects_preexisting_symlink_escape + omit "Symlinks not supported or not enabled" unless symlink_supported? + + package = Gem::Package.new @gem + + tgz_io = util_tar_gz do |tar| + tar.add_file "lib/owned.txt", 0o644 do |io| + io.write "poc-content" + end + end + + escape_dir = File.join(@tempdir, "escape") + FileUtils.mkdir_p escape_dir + + FileUtils.rm_rf File.join(@destination, "lib") + File.symlink escape_dir, File.join(@destination, "lib") + + escaped = File.join(escape_dir, "owned.txt") + + assert_raise Gem::Package::PathError do + package.extract_tar_gz tgz_io, @destination + end + + refute File.exist?(escaped), "must not write outside extraction root via symlink" + end + def test_extract_symlink_into_symlink_dir + omit "Symlinks not supported or not enabled" unless symlink_supported? package = Gem::Package.new @gem tgz_io = util_tar_gz do |tar| tar.mkdir "lib", 0o755 @@ -665,14 +706,10 @@ class TestGemPackage < Gem::Package::TarTestCase destination_subdir = File.join @destination, "subdir" FileUtils.mkdir_p destination_subdir - expected_exceptions = Gem.win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError] - - e = assert_raise(*expected_exceptions) do + e = assert_raise(Gem::Package::SymlinkError) do package.extract_tar_gz tgz_io, destination_subdir end - pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e - assert_equal("installing symlink 'lib/link' pointing to parent path #{@destination} of " \ "#{destination_subdir} is not allowed", e.message) @@ -700,14 +737,10 @@ class TestGemPackage < Gem::Package::TarTestCase tar.add_symlink "link/dir", ".", 16_877 end - expected_exceptions = Gem.win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError] - - e = assert_raise(*expected_exceptions) do + e = assert_raise(Gem::Package::SymlinkError) do package.extract_tar_gz tgz_io, destination_subdir end - pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e - assert_equal("installing symlink 'link' pointing to parent path #{destination_user_dir} of " \ "#{destination_subdir} is not allowed", e.message) @@ -858,7 +891,7 @@ class TestGemPackage < Gem::Package::TarTestCase "#{@destination} is not allowed", e.message) end - def test_load_spec + def test_load_spec_from_metadata entry = StringIO.new Gem::Util.gzip @spec.to_yaml def entry.full_name "metadata.gz" @@ -866,7 +899,7 @@ class TestGemPackage < Gem::Package::TarTestCase package = Gem::Package.new "nonexistent.gem" - spec = package.load_spec entry + spec = package.load_spec_from_metadata entry assert_equal @spec, spec end @@ -909,7 +942,11 @@ class TestGemPackage < Gem::Package::TarTestCase } tar.add_file "checksums.yaml.gz", 0o444 do |io| Zlib::GzipWriter.wrap io do |gz_io| - gz_io.write Psych.dump bogus_checksums + if Gem.use_psych? + gz_io.write Psych.dump(bogus_checksums) + else + gz_io.write Gem::YAMLSerializer.dump(bogus_checksums) + end end end end @@ -955,7 +992,11 @@ class TestGemPackage < Gem::Package::TarTestCase tar.add_file "checksums.yaml.gz", 0o444 do |io| Zlib::GzipWriter.wrap io do |gz_io| - gz_io.write Psych.dump checksums + if Gem.use_psych? + gz_io.write Psych.dump(checksums) + else + gz_io.write Gem::YAMLSerializer.dump(checksums) + end end end @@ -1247,71 +1288,25 @@ class TestGemPackage < Gem::Package::TarTestCase # end #verify tests - def test_verify_entry - entry = Object.new - def entry.full_name - raise ArgumentError, "whatever" - end - - package = Gem::Package.new @gem - - _, err = use_ui @ui do - e = nil - - out_err = capture_output do - e = assert_raise ArgumentError do - package.verify_entry entry + def test_missing_metadata + invalid_metadata = ["metadataxgz", "foobar\nmetadata", "metadata\nfoobar"] + invalid_metadata.each do |fname| + tar = StringIO.new + + Gem::Package::TarWriter.new(tar) do |gem_tar| + gem_tar.add_file fname, 0o444 do |io| + gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION + gz_io.write "bad metadata" + gz_io.close end end - assert_equal "whatever", e.message - assert_equal "full_name", e.backtrace_locations.first.label - - out_err - end - - assert_equal "Exception while verifying #{@gem}\n", err - - valid_metadata = ["metadata", "metadata.gz"] - valid_metadata.each do |vm| - $spec_loaded = false - $good_name = vm - - entry = Object.new - def entry.full_name - $good_name - end - - package = Gem::Package.new(@gem) - package.instance_variable_set(:@files, []) - def package.load_spec(entry) - $spec_loaded = true - end - - package.verify_entry(entry) + tar.rewind - assert $spec_loaded - end - - invalid_metadata = ["metadataxgz", "foobar\nmetadata", "metadata\nfoobar"] - invalid_metadata.each do |vm| - $spec_loaded = false - $bad_name = vm - - entry = Object.new - def entry.full_name - $bad_name - end - - package = Gem::Package.new(@gem) - package.instance_variable_set(:@files, []) - def package.load_spec(entry) - $spec_loaded = true + package = Gem::Package.new(Gem::Package::IOSource.new(tar)) + assert_raise Gem::Package::FormatError do + package.verify end - - package.verify_entry(entry) - - refute $spec_loaded end end diff --git a/test/rubygems/test_gem_package_old.rb b/test/rubygems/test_gem_package_old.rb index 7582dbedd4..e532fa25e1 100644 --- a/test/rubygems/test_gem_package_old.rb +++ b/test/rubygems/test_gem_package_old.rb @@ -39,7 +39,7 @@ unless Gem.java_platform? # jruby can't require the simple_gem file extracted = File.join @destination, "lib/foo.rb" assert_path_exist extracted - mask = 0o100644 & (~File.umask) + mask = 0o100644 & ~File.umask assert_equal mask, File.stat(extracted).mode unless Gem.win_platform? end diff --git a/test/rubygems/test_gem_package_tar_header_ractor.rb b/test/rubygems/test_gem_package_tar_header_ractor.rb new file mode 100644 index 0000000000..5714064805 --- /dev/null +++ b/test/rubygems/test_gem_package_tar_header_ractor.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require_relative "package/tar_test_case" + +unless Gem::Package::TarTestCase.method_defined?(:assert_ractor) + require "core_assertions" + Gem::Package::TarTestCase.include Test::Unit::CoreAssertions +end + +class TestGemPackageTarHeaderRactor < Gem::Package::TarTestCase + SETUP = <<~RUBY + header = { + name: "x", + mode: 0o644, + uid: 1000, + gid: 10_000, + size: 100, + mtime: 12_345, + typeflag: "0", + linkname: "link", + uname: "user", + gname: "group", + devmajor: 1, + devminor: 2, + prefix: "y", + } + + tar_header = Gem::Package::TarHeader.new header + # Move this require to arguments of assert_ractor after Ruby 4.0 or updating core_assertions.rb at Ruby 3.4. + require "stringio" + # Remove this after Ruby 4.0 or updating core_assertions.rb at Ruby 3.4. + class Ractor; alias value take unless method_defined?(:value); end + RUBY + + def test_decode_in_ractor + assert_ractor(SETUP + <<~RUBY, require: "rubygems/package", require_relative: "package/tar_test_case") + include Gem::Package::TarTestMethods + + new_header = Ractor.new(tar_header.to_s) do |str| + Gem::Package::TarHeader.from StringIO.new str + end.value + + assert_headers_equal tar_header, new_header + RUBY + end + + def test_encode_in_ractor + assert_ractor(SETUP + <<~RUBY, require: "rubygems/package", require_relative: "package/tar_test_case") + include Gem::Package::TarTestMethods + + header_bytes = tar_header.to_s + + new_header_bytes = Ractor.new(header_bytes) do |str| + new_header = Gem::Package::TarHeader.from StringIO.new str + new_header.to_s + end.value + + assert_headers_equal header_bytes, new_header_bytes + RUBY + end +end unless RUBY_PLATFORM.match?(/mingw|mswin/) diff --git a/test/rubygems/test_gem_package_tar_writer.rb b/test/rubygems/test_gem_package_tar_writer.rb index 751ceaca81..cb9e0d26fa 100644 --- a/test/rubygems/test_gem_package_tar_writer.rb +++ b/test/rubygems/test_gem_package_tar_writer.rb @@ -33,7 +33,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase f.write "a" * 10 end - assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.now), + assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512]) end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] @@ -50,11 +50,24 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase end end + def test_add_file_with_mtime + Time.stub :now, Time.at(1_458_518_157) do + mtime = Time.now + + @tar_writer.add_file "x", 0o644, mtime do |f| + f.write "a" * 10 + end + + assert_headers_equal(tar_file_header("x", "", 0o644, 10, mtime), + @io.string[0, 512]) + end + end + def test_add_symlink Time.stub :now, Time.at(1_458_518_157) do @tar_writer.add_symlink "x", "y", 0o644 - assert_headers_equal(tar_symlink_header("x", "", 0o644, Time.now, "y"), + assert_headers_equal(tar_symlink_header("x", "", 0o644, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc, "y"), @io.string[0, 512]) end assert_equal 512, @io.pos @@ -86,7 +99,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase "e1cf14b0", digests["SHA512"].hexdigest - assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.now), + assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512]) end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] @@ -109,7 +122,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase "e1cf14b0", digests["SHA512"].hexdigest - assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.now), + assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512]) end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] @@ -126,7 +139,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase io.write "a" * 10 end - assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.now), + assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512]) assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] @@ -137,7 +150,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase signature = signer.sign digest.digest assert_headers_equal(tar_file_header("x.sig", "", 0o444, signature.length, - Time.now), + Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[1024, 512]) assert_equal "#{signature}#{"\0" * (512 - signature.length)}", @io.string[1536, 512] @@ -154,7 +167,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase io.write "a" * 10 end - assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.now), + assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512]) end assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] @@ -168,7 +181,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase io.write "a" * 10 end - assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.now), + assert_headers_equal(tar_file_header("x", "", 0o644, 10, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512]) assert_equal "aaaaaaaaaa#{"\0" * 502}", @io.string[512, 512] @@ -192,7 +205,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase Time.stub :now, Time.at(1_458_518_157) do @tar_writer.add_file_simple "x", 0, 100 - assert_headers_equal tar_file_header("x", "", 0, 100, Time.now), + assert_headers_equal tar_file_header("x", "", 0, 100, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512] end @@ -250,7 +263,7 @@ class TestGemPackageTarWriter < Gem::Package::TarTestCase Time.stub :now, Time.at(1_458_518_157) do @tar_writer.mkdir "foo", 0o644 - assert_headers_equal tar_dir_header("foo", "", 0o644, Time.now), + assert_headers_equal tar_dir_header("foo", "", 0o644, Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc), @io.string[0, 512] assert_equal 512, @io.pos diff --git a/test/rubygems/test_gem_path_support.rb b/test/rubygems/test_gem_path_support.rb index 8720bcf858..c5181496c0 100644 --- a/test/rubygems/test_gem_path_support.rb +++ b/test/rubygems/test_gem_path_support.rb @@ -121,14 +121,12 @@ class TestGemPathSupport < Gem::TestCase end def test_gem_paths_do_not_contain_symlinks + pend "symlinks not supported" unless symlink_supported? + dir = "#{@tempdir}/realgemdir" symlink = "#{@tempdir}/symdir" Dir.mkdir dir - begin - File.symlink(dir, symlink) - rescue NotImplementedError, SystemCallError - pend "symlinks not supported" - end + File.symlink(dir, symlink) not_existing = "#{@tempdir}/does_not_exist" path = "#{symlink}#{File::PATH_SEPARATOR}#{not_existing}" diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb index 070c8007bc..c1ff36772b 100644 --- a/test/rubygems/test_gem_platform.rb +++ b/test/rubygems/test_gem_platform.rb @@ -11,15 +11,6 @@ class TestGemPlatform < Gem::TestCase assert_equal Gem::Platform.new(%w[x86 darwin 8]), Gem::Platform.local end - def test_self_match - Gem::Deprecate.skip_during do - assert Gem::Platform.match(nil), "nil == ruby" - assert Gem::Platform.match(Gem::Platform.local), "exact match" - assert Gem::Platform.match(Gem::Platform.local.to_s), "=~ match" - assert Gem::Platform.match(Gem::Platform::RUBY), "ruby" - end - end - def test_self_match_gem? assert Gem::Platform.match_gem?(nil, "json"), "nil == ruby" assert Gem::Platform.match_gem?(Gem::Platform.local, "json"), "exact match" @@ -148,12 +139,29 @@ class TestGemPlatform < Gem::TestCase "wasm32-wasi" => ["wasm32", "wasi", nil], "wasm32-wasip1" => ["wasm32", "wasi", nil], "wasm32-wasip2" => ["wasm32", "wasi", nil], + + "darwin-java-java" => ["darwin", "java", nil], + "linux-linux-linux" => ["linux", "linux", "linux"], + "linux-linux-linux1.0" => ["linux", "linux", "linux1"], + "x86x86-1x86x86x86x861linuxx86x86" => ["x86x86", "linux", "x86x86"], + "freebsd0" => [nil, "freebsd", "0"], + "darwin0" => [nil, "darwin", "0"], + "darwin0---" => [nil, "darwin", "0"], + "x86-linux-x8611.0l" => ["x86", "linux", "x8611"], + "0-x86linuxx86---" => ["0", "linux", "x86"], + "x86_64-macruby-x86" => ["x86_64", "macruby", nil], + "x86_64-dotnetx86" => ["x86_64", "dotnet", nil], + "x86_64-dalvik0" => ["x86_64", "dalvik", "0"], + "x86_64-dotnet1." => ["x86_64", "dotnet", "1"], + + "--" => [nil, "unknown", nil], } test_cases.each do |arch, expected| platform = Gem::Platform.new arch assert_equal expected, platform.to_a, arch.inspect - assert_equal expected, Gem::Platform.new(platform.to_s).to_a, arch.inspect + platform2 = Gem::Platform.new platform.to_s + assert_equal expected, platform2.to_a, "#{arch.inspect} => #{platform2.inspect}" end end @@ -246,19 +254,19 @@ class TestGemPlatform < Gem::TestCase x86_darwin8 = Gem::Platform.new "i686-darwin8.0" util_set_arch "powerpc-darwin8" - assert((ppc_darwin8 === Gem::Platform.local), "powerpc =~ universal") - assert((uni_darwin8 === Gem::Platform.local), "powerpc =~ universal") - refute((x86_darwin8 === Gem::Platform.local), "powerpc =~ universal") + assert(ppc_darwin8 === Gem::Platform.local, "powerpc =~ universal") + assert(uni_darwin8 === Gem::Platform.local, "powerpc =~ universal") + refute(x86_darwin8 === Gem::Platform.local, "powerpc =~ universal") util_set_arch "i686-darwin8" - refute((ppc_darwin8 === Gem::Platform.local), "powerpc =~ universal") - assert((uni_darwin8 === Gem::Platform.local), "x86 =~ universal") - assert((x86_darwin8 === Gem::Platform.local), "powerpc =~ universal") + refute(ppc_darwin8 === Gem::Platform.local, "powerpc =~ universal") + assert(uni_darwin8 === Gem::Platform.local, "x86 =~ universal") + assert(x86_darwin8 === Gem::Platform.local, "powerpc =~ universal") util_set_arch "universal-darwin8" - assert((ppc_darwin8 === Gem::Platform.local), "universal =~ ppc") - assert((uni_darwin8 === Gem::Platform.local), "universal =~ universal") - assert((x86_darwin8 === Gem::Platform.local), "universal =~ x86") + assert(ppc_darwin8 === Gem::Platform.local, "universal =~ ppc") + assert(uni_darwin8 === Gem::Platform.local, "universal =~ universal") + assert(x86_darwin8 === Gem::Platform.local, "universal =~ x86") end def test_nil_cpu_arch_is_treated_as_universal @@ -266,18 +274,18 @@ class TestGemPlatform < Gem::TestCase with_uni_arch = Gem::Platform.new ["universal", "mingw32"] with_x86_arch = Gem::Platform.new ["x86", "mingw32"] - assert((with_nil_arch === with_uni_arch), "nil =~ universal") - assert((with_uni_arch === with_nil_arch), "universal =~ nil") - assert((with_nil_arch === with_x86_arch), "nil =~ x86") - assert((with_x86_arch === with_nil_arch), "x86 =~ nil") + assert(with_nil_arch === with_uni_arch, "nil =~ universal") + assert(with_uni_arch === with_nil_arch, "universal =~ nil") + assert(with_nil_arch === with_x86_arch, "nil =~ x86") + assert(with_x86_arch === with_nil_arch, "x86 =~ nil") end def test_nil_version_is_treated_as_any_version x86_darwin_8 = Gem::Platform.new "i686-darwin8.0" x86_darwin_nil = Gem::Platform.new "i686-darwin" - assert((x86_darwin_8 === x86_darwin_nil), "8.0 =~ nil") - assert((x86_darwin_nil === x86_darwin_8), "nil =~ 8.0") + assert(x86_darwin_8 === x86_darwin_nil, "8.0 =~ nil") + assert(x86_darwin_nil === x86_darwin_8, "nil =~ 8.0") end def test_nil_version_is_stricter_for_linux_os @@ -371,40 +379,33 @@ class TestGemPlatform < Gem::TestCase arm64 = Gem::Platform.new "arm64-linux" util_set_arch "armv5-linux" - assert((arm === Gem::Platform.local), "arm === armv5") - assert((armv5 === Gem::Platform.local), "armv5 === armv5") - refute((armv7 === Gem::Platform.local), "armv7 === armv5") - refute((arm64 === Gem::Platform.local), "arm64 === armv5") - refute((Gem::Platform.local === arm), "armv5 === arm") + assert(arm === Gem::Platform.local, "arm === armv5") + assert(armv5 === Gem::Platform.local, "armv5 === armv5") + refute(armv7 === Gem::Platform.local, "armv7 === armv5") + refute(arm64 === Gem::Platform.local, "arm64 === armv5") + refute(Gem::Platform.local === arm, "armv5 === arm") util_set_arch "armv7-linux" - assert((arm === Gem::Platform.local), "arm === armv7") - refute((armv5 === Gem::Platform.local), "armv5 === armv7") - assert((armv7 === Gem::Platform.local), "armv7 === armv7") - refute((arm64 === Gem::Platform.local), "arm64 === armv7") - refute((Gem::Platform.local === arm), "armv7 === arm") + assert(arm === Gem::Platform.local, "arm === armv7") + refute(armv5 === Gem::Platform.local, "armv5 === armv7") + assert(armv7 === Gem::Platform.local, "armv7 === armv7") + refute(arm64 === Gem::Platform.local, "arm64 === armv7") + refute(Gem::Platform.local === arm, "armv7 === arm") util_set_arch "arm64-linux" - refute((arm === Gem::Platform.local), "arm === arm64") - refute((armv5 === Gem::Platform.local), "armv5 === arm64") - refute((armv7 === Gem::Platform.local), "armv7 === arm64") - assert((arm64 === Gem::Platform.local), "arm64 === arm64") + refute(arm === Gem::Platform.local, "arm === arm64") + refute(armv5 === Gem::Platform.local, "armv5 === arm64") + refute(armv7 === Gem::Platform.local, "armv7 === arm64") + assert(arm64 === Gem::Platform.local, "arm64 === arm64") end def test_equals3_universal_mingw uni_mingw = Gem::Platform.new "universal-mingw" - mingw32 = Gem::Platform.new "x64-mingw32" mingw_ucrt = Gem::Platform.new "x64-mingw-ucrt" - util_set_arch "x64-mingw32" - assert((uni_mingw === Gem::Platform.local), "uni_mingw === mingw32") - assert((mingw32 === Gem::Platform.local), "mingw32 === mingw32") - refute((mingw_ucrt === Gem::Platform.local), "mingw32 === mingw_ucrt") - util_set_arch "x64-mingw-ucrt" - assert((uni_mingw === Gem::Platform.local), "uni_mingw === mingw32") - assert((mingw_ucrt === Gem::Platform.local), "mingw_ucrt === mingw_ucrt") - refute((mingw32 === Gem::Platform.local), "mingw32 === mingw_ucrt") + assert(uni_mingw === Gem::Platform.local, "uni_mingw === mingw_ucrt") + assert(mingw_ucrt === Gem::Platform.local, "mingw_ucrt === mingw_ucrt") end def test_equals3_version @@ -415,11 +416,11 @@ class TestGemPlatform < Gem::TestCase x86_darwin8 = Gem::Platform.new ["x86", "darwin", "8"] x86_darwin9 = Gem::Platform.new ["x86", "darwin", "9"] - assert((x86_darwin === Gem::Platform.local), "x86_darwin === x86_darwin8") - assert((x86_darwin8 === Gem::Platform.local), "x86_darwin8 === x86_darwin8") + assert(x86_darwin === Gem::Platform.local, "x86_darwin === x86_darwin8") + assert(x86_darwin8 === Gem::Platform.local, "x86_darwin8 === x86_darwin8") - refute((x86_darwin7 === Gem::Platform.local), "x86_darwin7 === x86_darwin8") - refute((x86_darwin9 === Gem::Platform.local), "x86_darwin9 === x86_darwin8") + refute(x86_darwin7 === Gem::Platform.local, "x86_darwin7 === x86_darwin8") + refute(x86_darwin9 === Gem::Platform.local, "x86_darwin9 === x86_darwin8") end def test_equals_tilde @@ -492,15 +493,171 @@ class TestGemPlatform < Gem::TestCase assert_equal 1, result.scan(/@version=/).size end - def test_gem_platform_match_with_string_argument - util_set_arch "x86_64-linux-musl" + def test_constants + assert_equal [nil, "java", nil], Gem::Platform::JAVA.to_a + assert_equal ["x86", "mswin32", nil], Gem::Platform::MSWIN.to_a + assert_equal [nil, "mswin64", nil], Gem::Platform::MSWIN64.to_a + assert_equal ["x86", "mingw32", nil], Gem::Platform::MINGW.to_a + assert_equal ["x64", "mingw", "ucrt"], Gem::Platform::X64_MINGW.to_a + assert_equal ["universal", "mingw", nil], Gem::Platform::UNIVERSAL_MINGW.to_a + assert_equal [["x86", "mswin32", nil], [nil, "mswin64", nil], ["universal", "mingw", nil]], Gem::Platform::WINDOWS.map(&:to_a) + assert_equal ["x86_64", "linux", nil], Gem::Platform::X64_LINUX.to_a + assert_equal ["x86_64", "linux", "musl"], Gem::Platform::X64_LINUX_MUSL.to_a + end + + def test_generic + # converts non-windows platforms into ruby + assert_equal Gem::Platform::RUBY, Gem::Platform.generic(Gem::Platform.new("x86-darwin-10")) + assert_equal Gem::Platform::RUBY, Gem::Platform.generic(Gem::Platform::RUBY) + + # converts java platform variants into java + assert_equal Gem::Platform::JAVA, Gem::Platform.generic(Gem::Platform.new("java")) + assert_equal Gem::Platform::JAVA, Gem::Platform.generic(Gem::Platform.new("universal-java-17")) + + # converts mswin platform variants into x86-mswin32 + assert_equal Gem::Platform::MSWIN, Gem::Platform.generic(Gem::Platform.new("mswin32")) + assert_equal Gem::Platform::MSWIN, Gem::Platform.generic(Gem::Platform.new("i386-mswin32")) + assert_equal Gem::Platform::MSWIN, Gem::Platform.generic(Gem::Platform.new("x86-mswin32")) + + # converts 32-bit mingw platform variants into universal-mingw + assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("i386-mingw32")) + assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("x86-mingw32")) + + # converts 64-bit mingw platform variants into universal-mingw + assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("x64-mingw32")) - Gem::Deprecate.skip_during do - assert(Gem::Platform.match(Gem::Platform.new("x86_64-linux")), "should match Gem::Platform") - assert(Gem::Platform.match("x86_64-linux"), "should match String platform") + # converts x64 mingw UCRT platform variants into universal-mingw + assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("x64-mingw-ucrt")) + + # converts aarch64 mingw UCRT platform variants into universal-mingw + assert_equal Gem::Platform::UNIVERSAL_MINGW, Gem::Platform.generic(Gem::Platform.new("aarch64-mingw-ucrt")) + + assert_equal Gem::Platform::RUBY, Gem::Platform.generic(Gem::Platform.new("unknown")) + assert_equal Gem::Platform::RUBY, Gem::Platform.generic(nil) + assert_equal Gem::Platform::MSWIN64, Gem::Platform.generic(Gem::Platform.new("mswin64")) + end + + def test_platform_specificity_match + [ + ["ruby", "ruby", -1, -1], + ["x86_64-linux-musl", "x86_64-linux-musl", -1, -1], + ["x86_64-linux", "x86_64-linux-musl", 100, 200], + ["universal-darwin", "x86-darwin", 10, 20], + ["universal-darwin-19", "x86-darwin", 210, 120], + ["universal-darwin-19", "universal-darwin-20", 200, 200], + ["arm-darwin-19", "arm64-darwin-19", 0, 20], + ].each do |spec_platform, user_platform, s1, s2| + spec_platform = Gem::Platform.new(spec_platform) + user_platform = Gem::Platform.new(user_platform) + assert_equal s1, Gem::Platform.platform_specificity_match(spec_platform, user_platform), + "Gem::Platform.platform_specificity_match(#{spec_platform.to_s.inspect}, #{user_platform.to_s.inspect})" + assert_equal s2, Gem::Platform.platform_specificity_match(user_platform, spec_platform), + "Gem::Platform.platform_specificity_match(#{user_platform.to_s.inspect}, #{spec_platform.to_s.inspect})" end end + def test_sort_and_filter_best_platform_match + a_1 = util_spec "a", "1" + a_1_java = util_spec "a", "1" do |s| + s.platform = Gem::Platform::JAVA + end + a_1_universal_darwin = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("universal-darwin") + end + a_1_universal_darwin_19 = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("universal-darwin-19") + end + a_1_universal_darwin_20 = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("universal-darwin-20") + end + a_1_arm_darwin_19 = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("arm64-darwin-19") + end + a_1_x86_darwin = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("x86-darwin") + end + specs = [a_1, a_1_java, a_1_universal_darwin, a_1_universal_darwin_19, a_1_universal_darwin_20, a_1_arm_darwin_19, a_1_x86_darwin] + assert_equal [a_1], Gem::Platform.sort_and_filter_best_platform_match(specs, "ruby") + assert_equal [a_1_java], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform::JAVA) + assert_equal [a_1_arm_darwin_19], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("arm64-darwin-19")) + assert_equal [a_1_universal_darwin_20], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("arm64-darwin-20")) + assert_equal [a_1_universal_darwin_19], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("x86-darwin-19")) + assert_equal [a_1_universal_darwin_20], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("x86-darwin-20")) + assert_equal [a_1_x86_darwin], Gem::Platform.sort_and_filter_best_platform_match(specs, Gem::Platform.new("x86-darwin-21")) + end + + def test_sort_best_platform_match + a_1 = util_spec "a", "1" + a_1_java = util_spec "a", "1" do |s| + s.platform = Gem::Platform::JAVA + end + a_1_universal_darwin = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("universal-darwin") + end + a_1_universal_darwin_19 = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("universal-darwin-19") + end + a_1_universal_darwin_20 = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("universal-darwin-20") + end + a_1_arm_darwin_19 = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("arm64-darwin-19") + end + a_1_x86_darwin = util_spec "a", "1" do |s| + s.platform = Gem::Platform.new("x86-darwin") + end + specs = [a_1, a_1_java, a_1_universal_darwin, a_1_universal_darwin_19, a_1_universal_darwin_20, a_1_arm_darwin_19, a_1_x86_darwin] + assert_equal ["ruby", + "java", + "universal-darwin", + "universal-darwin-19", + "universal-darwin-20", + "arm64-darwin-19", + "x86-darwin"], Gem::Platform.sort_best_platform_match(specs, "ruby").map {|s| s.platform.to_s } + assert_equal ["java", + "universal-darwin", + "x86-darwin", + "universal-darwin-19", + "universal-darwin-20", + "arm64-darwin-19", + "ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform::JAVA).map {|s| s.platform.to_s } + assert_equal ["arm64-darwin-19", + "universal-darwin-19", + "universal-darwin", + "java", + "x86-darwin", + "universal-darwin-20", + "ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("arm64-darwin-19")).map {|s| s.platform.to_s } + assert_equal ["universal-darwin-20", + "universal-darwin", + "java", + "x86-darwin", + "arm64-darwin-19", + "universal-darwin-19", + "ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("arm64-darwin-20")).map {|s| s.platform.to_s } + assert_equal ["universal-darwin-19", + "arm64-darwin-19", + "x86-darwin", + "universal-darwin", + "java", + "universal-darwin-20", + "ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("x86-darwin-19")).map {|s| s.platform.to_s } + assert_equal ["universal-darwin-20", + "x86-darwin", + "universal-darwin", + "java", + "universal-darwin-19", + "arm64-darwin-19", + "ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("x86-darwin-20")).map {|s| s.platform.to_s } + assert_equal ["x86-darwin", + "universal-darwin", + "java", + "universal-darwin-19", + "universal-darwin-20", + "arm64-darwin-19", + "ruby"], Gem::Platform.sort_best_platform_match(specs, Gem::Platform.new("x86-darwin-21")).map {|s| s.platform.to_s } + end + def assert_local_match(name) assert_match Gem::Platform.local, name end @@ -508,4 +665,38 @@ class TestGemPlatform < Gem::TestCase def refute_local_match(name) refute_match Gem::Platform.local, name end + + def test_deconstruct + platform = Gem::Platform.new("x86_64-linux") + assert_equal ["x86_64", "linux", nil], platform.deconstruct + end + + def test_deconstruct_keys + platform = Gem::Platform.new("x86_64-darwin-20") + assert_equal({ cpu: "x86_64", os: "darwin", version: "20" }, platform.deconstruct_keys(nil)) + end + + def test_pattern_matching_array + platform = Gem::Platform.new("arm64-darwin-21") + result = + case platform + in ["arm64", "darwin", version] + version + else + "no match" + end + assert_equal "21", result + end + + def test_pattern_matching_hash + platform = Gem::Platform.new("x86_64-linux") + result = + case platform + in cpu: "x86_64", os: "linux" + "matched" + else + "no match" + end + assert_equal "matched", result + end end diff --git a/test/rubygems/test_gem_remote_fetcher.rb b/test/rubygems/test_gem_remote_fetcher.rb index ca858cfda5..c35da2fc5a 100644 --- a/test/rubygems/test_gem_remote_fetcher.rb +++ b/test/rubygems/test_gem_remote_fetcher.rb @@ -60,7 +60,7 @@ class TestGemRemoteFetcher < Gem::TestCase uri = Gem::URI "http://example/file" path = File.join @tempdir, "file" - fetcher = util_fuck_with_fetcher "hello" + fetcher = fake_fetcher(uri.to_s, "hello") data = fetcher.cache_update_path uri, path @@ -75,7 +75,7 @@ class TestGemRemoteFetcher < Gem::TestCase path = File.join @tempdir, "file" data = String.new("\xC8").force_encoding(Encoding::BINARY) - fetcher = util_fuck_with_fetcher data + fetcher = fake_fetcher(uri.to_s, data) written_data = fetcher.cache_update_path uri, path @@ -88,7 +88,7 @@ class TestGemRemoteFetcher < Gem::TestCase uri = Gem::URI "http://example/file" path = File.join @tempdir, "file" - fetcher = util_fuck_with_fetcher "hello" + fetcher = fake_fetcher(uri.to_s, "hello") data = fetcher.cache_update_path uri, path, false @@ -97,103 +97,79 @@ class TestGemRemoteFetcher < Gem::TestCase assert_path_not_exist path end - def util_fuck_with_fetcher(data, blow = false) - fetcher = Gem::RemoteFetcher.fetcher - fetcher.instance_variable_set :@test_data, data - - if blow - def fetcher.fetch_path(arg, *rest) - # OMG I'm such an ass - class << self; remove_method :fetch_path; end - def self.fetch_path(arg, *rest) - @test_arg = arg - @test_data - end + def test_cache_update_path_overwrites_existing_file + uri = Gem::URI "http://example/file" + path = File.join @tempdir, "file" - raise Gem::RemoteFetcher::FetchError.new("haha!", "") - end - else - def fetcher.fetch_path(arg, *rest) - @test_arg = arg - @test_data - end - end + # 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 - fetcher + assert_equal "new content", data + assert_equal "new content", File.read(path) end def test_download - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + 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("http://gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_auth - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://user:password@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + 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("http://user:password@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_token - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://token@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + 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("http://token@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_x_oauth_basic - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://token:x-oauth-basic@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + 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("http://token:x-oauth-basic@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end def test_download_with_encoded_auth - a1_data = nil - File.open @a1_gem, "rb" do |fp| - a1_data = fp.read - end + a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://user:%25pas%25sword@gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + 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("http://user:%25pas%25sword@gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end @@ -235,8 +211,9 @@ class TestGemRemoteFetcher < Gem::TestCase def test_download_install_dir a1_data = File.open @a1_gem, "rb", &:read + a1_url = "http://gems.example.com/gems/a-1.gem" - fetcher = util_fuck_with_fetcher a1_data + fetcher = fake_fetcher(a1_url, a1_data) install_dir = File.join @tempdir, "more_gems" @@ -245,8 +222,7 @@ class TestGemRemoteFetcher < Gem::TestCase actual = fetcher.download(@a1, "http://gems.example.com", install_dir) assert_equal a1_cache_gem, actual - assert_equal("http://gems.example.com/gems/a-1.gem", - fetcher.instance_variable_get(:@test_arg).to_s) + assert_equal a1_url, fetcher.paths.last assert File.exist?(a1_cache_gem) end @@ -282,7 +258,12 @@ class TestGemRemoteFetcher < Gem::TestCase FileUtils.chmod 0o555, @a1.cache_dir FileUtils.chmod 0o555, @gemhome - fetcher = util_fuck_with_fetcher File.read(@a1_gem) + 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 @@ -301,19 +282,21 @@ class TestGemRemoteFetcher < Gem::TestCase end e1.loaded_from = File.join(@gemhome, "specifications", e1.full_name) - e1_data = nil - File.open e1_gem, "rb" do |fp| - e1_data = fp.read - end + e1_data = File.open e1_gem, "rb", &:read - fetcher = util_fuck_with_fetcher e1_data, :blow_chunks + 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_equal("http://gems.example.com/gems/#{e1.original_name}.gem", - fetcher.instance_variable_get(:@test_arg).to_s) assert File.exist?(e1_cache_gem) end @@ -592,7 +575,112 @@ class TestGemRemoteFetcher < Gem::TestCase end end - def assert_error(exception_class=Exception) + def test_download_with_global_gem_cache + # Use a temp directory to safely test global cache behavior + test_cache_dir = File.join(@tempdir, "global_gem_cache_test") + + Gem.stub :global_gem_cache_path, test_cache_dir do + Gem.configuration.global_gem_cache = true + + # Use the real RemoteFetcher with stubbed fetch_path + fetcher = Gem::RemoteFetcher.fetcher + def fetcher.fetch_path(uri, *rest) + File.binread File.join(@test_gem_dir, "a-1.gem") + end + fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem)) + + # With global cache enabled, gem goes directly to global cache + global_cache_gem = File.join(test_cache_dir, @a1.file_name) + assert_equal global_cache_gem, fetcher.download(@a1, "http://gems.example.com") + assert File.exist?(global_cache_gem), "Gem should be in global cache" + end + ensure + Gem.configuration.global_gem_cache = false + end + + def test_download_uses_global_gem_cache + # Use a temp directory to safely test global cache behavior + test_cache_dir = File.join(@tempdir, "global_gem_cache_test") + + Gem.stub :global_gem_cache_path, test_cache_dir do + Gem.configuration.global_gem_cache = true + + # Pre-populate global cache + FileUtils.mkdir_p test_cache_dir + global_cache_gem = File.join(test_cache_dir, @a1.file_name) + FileUtils.cp @a1_gem, global_cache_gem + + fetcher = Gem::RemoteFetcher.fetcher + + # Should return global cache path without downloading + result = fetcher.download(@a1, "http://gems.example.com") + assert_equal global_cache_gem, result + end + ensure + Gem.configuration.global_gem_cache = false + end + + def test_download_without_global_gem_cache + # Use a temp directory to safely test global cache behavior + test_cache_dir = File.join(@tempdir, "global_gem_cache_test") + + Gem.stub :global_gem_cache_path, test_cache_dir do + Gem.configuration.global_gem_cache = false + + # Use the real RemoteFetcher with stubbed fetch_path + fetcher = Gem::RemoteFetcher.fetcher + def fetcher.fetch_path(uri, *rest) + File.binread File.join(@test_gem_dir, "a-1.gem") + end + fetcher.instance_variable_set(:@test_gem_dir, File.dirname(@a1_gem)) + + a1_cache_gem = @a1.cache_file + assert_equal a1_cache_gem, fetcher.download(@a1, "http://gems.example.com") + + # Verify gem was NOT copied to global cache + global_cache_gem = File.join(test_cache_dir, @a1.file_name) + refute File.exist?(global_cache_gem), "Gem should not be copied to global cache when disabled" + end + end + + def test_fetch_http_with_custom_error_header + fetcher = Gem::RemoteFetcher.new nil + @fetcher = fetcher + url = "http://gems.example.com/error" + + def fetcher.request(uri, request_class, last_modified = nil) + res = Gem::Net::HTTPBadRequest.new nil, 403, "Forbidden" + res.add_field "X-Error-Message", "Component blocked by policy" + res + end + + e = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.fetch_http Gem::URI.parse(url) + end + + assert_equal "Bad response Component blocked by policy 403 (#{url})", e.message + end + + def test_fetch_http_without_custom_error_header + fetcher = Gem::RemoteFetcher.new nil + @fetcher = fetcher + url = "http://gems.example.com/error" + + def fetcher.request(uri, request_class, last_modified = nil) + res = Gem::Net::HTTPBadRequest.new nil, 403, "Forbidden" + res + end + + e = assert_raise Gem::RemoteFetcher::FetchError do + fetcher.fetch_http Gem::URI.parse(url) + end + + assert_equal "Bad response Forbidden 403 (#{url})", e.message + end + + private + + def assert_error(exception_class = Exception) got_exception = false begin @@ -603,4 +691,13 @@ class TestGemRemoteFetcher < Gem::TestCase 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 diff --git a/test/rubygems/test_gem_remote_fetcher_s3.rb b/test/rubygems/test_gem_remote_fetcher_s3.rb index fe7eb7ec01..4a5acc5a86 100644 --- a/test/rubygems/test_gem_remote_fetcher_s3.rb +++ b/test/rubygems/test_gem_remote_fetcher_s3.rb @@ -8,6 +8,100 @@ require "rubygems/package" class TestGemRemoteFetcherS3 < Gem::TestCase include Gem::DefaultUserInteraction + class FakeGemRequest < Gem::Request + attr_reader :last_request, :uri + + # Override perform_request to stub things + def perform_request(request) + @last_request = request + @response + end + + def set_response(response) + @response = response + end + end + + class FakeS3URISigner < Gem::S3URISigner + class << self + attr_accessor :return_token, :instance_profile + end + + # Convenience method to output the recent aws iam queries made in tests + # this outputs the verb, path, and any non-generic headers + def recent_aws_query_logs + sreqs = @aws_iam_calls.map do |c| + r = c.last_request + s = +"#{r.method} #{c.uri}\n" + r.each_header do |key, v| + # Only include headers that start with x- + next unless key.start_with?("x-") + s << " #{key}=#{v}\n" + end + s + end + + sreqs.join("") + end + + def initialize(uri, method) + @aws_iam_calls = [] + super + end + + def ec2_iam_request(uri, verb) + fake_s3_request = FakeGemRequest.new(uri, verb, nil, nil) + @aws_iam_calls << fake_s3_request + + case uri.to_s + when "http://169.254.169.254/latest/api/token" + if FakeS3URISigner.return_token.nil? + res = Gem::Net::HTTPUnauthorized.new nil, 401, nil + def res.body = "you got a 401! panic!" + else + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body = FakeS3URISigner.return_token + end + when "http://169.254.169.254/latest/meta-data/iam/info" + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body + <<~JSON + { + "Code": "Success", + "LastUpdated": "2023-05-27:05:05", + "InstanceProfileArn": "arn:aws:iam::somesecretid:instance-profile/TestRole", + "InstanceProfileId": "SOMEPROFILEID" + } + JSON + end + + when "http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole" + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body = FakeS3URISigner.instance_profile + else + raise "Unexpected request to #{uri}" + end + + fake_s3_request.set_response(res) + fake_s3_request + end + end + + class FakeGemFetcher < Gem::RemoteFetcher + attr_reader :fetched_uri, :last_s3_uri_signer + + def request(uri, request_class, last_modified = nil) + @fetched_uri = uri + res = Gem::Net::HTTPOK.new nil, 200, nil + def res.body = "success" + res + end + + def s3_uri_signer(uri, method) + @last_s3_uri_signer = FakeS3URISigner.new(uri, method) + end + end + def setup super @@ -18,39 +112,61 @@ class TestGemRemoteFetcherS3 < Gem::TestCase @a1.loaded_from = File.join(@gemhome, "specifications", @a1.full_name) end - def assert_fetch_s3(url, signature, token=nil, region="us-east-1", instance_profile_json=nil) - fetcher = Gem::RemoteFetcher.new nil - @fetcher = fetcher - $fetched_uri = nil - $instance_profile = instance_profile_json + def assert_fetched_s3_with_imds_v2(expected_token) + # Three API requests: + # 1. Get the token + # 2. Lookup profile details + # 3. Query the credentials + expected = <<~TEXT + PUT http://169.254.169.254/latest/api/token + x-aws-ec2-metadata-token-ttl-seconds=60 + GET http://169.254.169.254/latest/meta-data/iam/info + x-aws-ec2-metadata-token=#{expected_token} + GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole + x-aws-ec2-metadata-token=#{expected_token} + TEXT + recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs + assert_equal(expected.strip, recent_aws_query_logs.strip) + end - def fetcher.request(uri, request_class, last_modified = nil) - $fetched_uri = uri - res = Gem::Net::HTTPOK.new nil, 200, nil - def res.body - "success" - end - res - end + def assert_fetched_s3_with_imds_v1 + # Three API requests: + # 1. Get the token (which fails) + # 2. Lookup profile details without token + # 3. Query the credentials without token + expected = <<~TEXT + PUT http://169.254.169.254/latest/api/token + x-aws-ec2-metadata-token-ttl-seconds=60 + GET http://169.254.169.254/latest/meta-data/iam/info + GET http://169.254.169.254/latest/meta-data/iam/security-credentials/TestRole + TEXT + recent_aws_query_logs = @fetcher.last_s3_uri_signer.recent_aws_query_logs + assert_equal(expected.strip, recent_aws_query_logs.strip) + end - def fetcher.s3_uri_signer(uri) - require "json" - s3_uri_signer = Gem::S3URISigner.new(uri) - def s3_uri_signer.ec2_metadata_credentials_json - JSON.parse($instance_profile) - end - # Running sign operation to make sure uri.query is not mutated - s3_uri_signer.sign - raise "URI query is not empty: #{uri.query}" unless uri.query.nil? - s3_uri_signer - end + def with_imds_v2_failure + FakeS3URISigner.should_fail = true + yield(fetcher) + ensure + FakeS3URISigner.should_fail = false + end - data = fetcher.fetch_s3 Gem::URI.parse(url) + def assert_fetch_s3(url:, signature:, token: nil, region: "us-east-1", instance_profile_json: nil, fetcher: nil, method: "GET") + FakeS3URISigner.instance_profile = instance_profile_json + FakeS3URISigner.return_token = token - assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T050641Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", $fetched_uri.to_s - assert_equal "success", data + @fetcher = fetcher || FakeGemFetcher.new(nil) + res = @fetcher.fetch_s3 Gem::URI.parse(url), nil, (method == "HEAD") + + assert_equal "https://my-bucket.s3.#{region}.amazonaws.com/gems/specs.4.8.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=testuser%2F20190624%2F#{region}%2Fs3%2Faws4_request&X-Amz-Date=20190624T051941Z&X-Amz-Expires=86400#{token ? "&X-Amz-Security-Token=" + token : ""}&X-Amz-SignedHeaders=host&X-Amz-Signature=#{signature}", @fetcher.fetched_uri.to_s + if method == "HEAD" + assert_equal 200, res.code + else + assert_equal "success", res + end ensure - $fetched_uri = nil + FakeS3URISigner.instance_profile = nil + FakeS3URISigner.return_token = nil end def test_fetch_s3_config_creds @@ -59,7 +175,34 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "20f974027db2f3cd6193565327a7c73457a138efb1a63ea248d185ce6827d41b" + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + ) + end + ensure + Gem.configuration[:s3_source] = nil + end + + def test_fetch_s3_head_request + Gem.configuration[:s3_source] = { + "my-bucket" => { id: "testuser", secret: "testpass" }, + } + url = "s3://my-bucket/gems/specs.4.8.gz" + Time.stub :now, Time.at(1_561_353_581) do + token = nil + region = "us-east-1" + instance_profile_json = nil + method = "HEAD" + + assert_fetch_s3( + url: url, + signature: "a3c6cf9a2db62e85f4e57f8fc8ac8b5ff5c1fdd4aeef55935d05e05174d9c885", + token: token, + region: region, + instance_profile_json: instance_profile_json, + method: method + ) end ensure Gem.configuration[:s3_source] = nil @@ -71,7 +214,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "4afc3010757f1fd143e769f1d1dabd406476a4fc7c120e9884fd02acbb8f26c9", nil, "us-west-2" + assert_fetch_s3( + url: url, + signature: "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", + region: "us-west-2" + ) end ensure Gem.configuration[:s3_source] = nil @@ -83,7 +230,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "935160a427ef97e7630f799232b8f208c4a4e49aad07d0540572a2ad5fe9f93c", "testtoken" + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken" + ) end ensure Gem.configuration[:s3_source] = nil @@ -98,7 +249,10 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "20f974027db2f3cd6193565327a7c73457a138efb1a63ea248d185ce6827d41b" + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + ) end ensure ENV.each_key {|key| ENV.delete(key) if key.start_with?("AWS") } @@ -114,7 +268,12 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "4afc3010757f1fd143e769f1d1dabd406476a4fc7c120e9884fd02acbb8f26c9", nil, "us-west-2" + assert_fetch_s3( + url: url, + signature: "ef07487bfd8e3ca594f8fc29775b70c0a0636f51318f95d4f12b2e6e1fd8c716", + token: nil, + region: "us-west-2" + ) end ensure ENV.each_key {|key| ENV.delete(key) if key.start_with?("AWS") } @@ -130,7 +289,11 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "935160a427ef97e7630f799232b8f208c4a4e49aad07d0540572a2ad5fe9f93c", "testtoken" + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken" + ) end ensure ENV.each_key {|key| ENV.delete(key) if key.start_with?("AWS") } @@ -140,7 +303,10 @@ class TestGemRemoteFetcherS3 < Gem::TestCase def test_fetch_s3_url_creds url = "s3://testuser:testpass@my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "20f974027db2f3cd6193565327a7c73457a138efb1a63ea248d185ce6827d41b" + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c" + ) end end @@ -151,8 +317,14 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "20f974027db2f3cd6193565327a7c73457a138efb1a63ea248d185ce6827d41b", nil, "us-east-1", - '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + assert_fetch_s3( + url: url, + signature: "da82e098bdaed0d3087047670efc98eaadc20559a473b5eac8d70190d2a9e8fd", + region: "us-east-1", + token: "mysecrettoken", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "mysecrettoken"}' + ) + assert_fetched_s3_with_imds_v2("mysecrettoken") end ensure Gem.configuration[:s3_source] = nil @@ -165,8 +337,14 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "4afc3010757f1fd143e769f1d1dabd406476a4fc7c120e9884fd02acbb8f26c9", nil, "us-west-2", - '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + assert_fetch_s3( + url: url, + signature: "532960594dbfe31d1bbfc0e8e7a666c3cbdd8b00a143774da51b7f920704afd2", + region: "us-west-2", + token: "mysecrettoken", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "mysecrettoken"}' + ) + assert_fetched_s3_with_imds_v2("mysecrettoken") end ensure Gem.configuration[:s3_source] = nil @@ -179,14 +357,40 @@ class TestGemRemoteFetcherS3 < Gem::TestCase url = "s3://my-bucket/gems/specs.4.8.gz" Time.stub :now, Time.at(1_561_353_581) do - assert_fetch_s3 url, "935160a427ef97e7630f799232b8f208c4a4e49aad07d0540572a2ad5fe9f93c", "testtoken", "us-east-1", - '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' + assert_fetch_s3( + url: url, + signature: "e709338735f9077edf8f6b94b247171c266a9605975e08e4a519a123c3322625", + token: "testtoken", + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass", "Token": "testtoken"}' + ) + assert_fetched_s3_with_imds_v2("testtoken") + end + ensure + Gem.configuration[:s3_source] = nil + end + + def test_fetch_s3_instance_profile_creds_with_fallback + Gem.configuration[:s3_source] = { + "my-bucket" => { provider: "instance_profile" }, + } + + url = "s3://my-bucket/gems/specs.4.8.gz" + Time.stub :now, Time.at(1_561_353_581) do + assert_fetch_s3( + url: url, + signature: "b5cb80c1301f7b1c50c4af54f1f6c034f80b56d32f000a855f0a903dc5a8413c", + token: nil, + region: "us-east-1", + instance_profile_json: '{"AccessKeyId": "testuser", "SecretAccessKey": "testpass"}' + ) + assert_fetched_s3_with_imds_v1 end ensure Gem.configuration[:s3_source] = nil end - def refute_fetch_s3(url, expected_message) + def refute_fetch_s3(url:, expected_message:) fetcher = Gem::RemoteFetcher.new nil @fetcher = fetcher @@ -199,7 +403,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase def test_fetch_s3_no_source_key url = "s3://my-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "no s3_source key exists in .gemrc" + refute_fetch_s3(url: url, expected_message: "no s3_source key exists in .gemrc") end def test_fetch_s3_no_host @@ -208,7 +412,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase } url = "s3://other-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "no key for host other-bucket in s3_source in .gemrc" + refute_fetch_s3(url: url, expected_message: "no key for host other-bucket in s3_source in .gemrc") ensure Gem.configuration[:s3_source] = nil end @@ -217,7 +421,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Gem.configuration[:s3_source] = { "my-bucket" => { secret: "testpass" } } url = "s3://my-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "s3_source for my-bucket missing id or secret" + refute_fetch_s3(url: url, expected_message: "s3_source for my-bucket missing id or secret") ensure Gem.configuration[:s3_source] = nil end @@ -226,7 +430,7 @@ class TestGemRemoteFetcherS3 < Gem::TestCase Gem.configuration[:s3_source] = { "my-bucket" => { id: "testuser" } } url = "s3://my-bucket/gems/specs.4.8.gz" - refute_fetch_s3 url, "s3_source for my-bucket missing id or secret" + refute_fetch_s3(url: url, expected_message: "s3_source for my-bucket missing id or secret") ensure Gem.configuration[:s3_source] = nil end diff --git a/test/rubygems/test_gem_request.rb b/test/rubygems/test_gem_request.rb index eb15eed749..cd0a416e79 100644 --- a/test/rubygems/test_gem_request.rb +++ b/test/rubygems/test_gem_request.rb @@ -248,7 +248,7 @@ class TestGemRequest < Gem::TestCase auth_header = conn.payload["Authorization"] assert_equal "Basic #{base64_encode64("{DEScede}pass:x-oauth-basic")}".strip, auth_header - assert_includes @ui.output, "GET https://REDACTED:x-oauth-basic@example.rubygems/specs.#{Gem.marshal_version}" + assert_includes @ui.output, "GET https://REDACTED@example.rubygems/specs.#{Gem.marshal_version}" end def test_fetch_head @@ -363,19 +363,19 @@ class TestGemRequest < Gem::TestCase def test_verify_certificate_extra_message pend if Gem.java_platform? - error_number = OpenSSL::X509::V_ERR_INVALID_CA + error_number = OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY store = OpenSSL::X509::Store.new - context = OpenSSL::X509::StoreContext.new store - context.error = error_number + context = OpenSSL::X509::StoreContext.new store, CHILD_CERT + context.verify use_ui @ui do Gem::Request.verify_certificate context end expected = <<-ERROR -ERROR: SSL verification error at depth 0: invalid CA certificate (#{error_number}) -ERROR: Certificate is an invalid CA certificate +ERROR: SSL verification error at depth 0: unable to get local issuer certificate (#{error_number}) +ERROR: You must add #{CHILD_CERT.issuer} to your local trusted store ERROR assert_equal expected, @ui.error diff --git a/test/rubygems/test_gem_request_connection_pools.rb b/test/rubygems/test_gem_request_connection_pools.rb index 966447bff6..2860deabf7 100644 --- a/test/rubygems/test_gem_request_connection_pools.rb +++ b/test/rubygems/test_gem_request_connection_pools.rb @@ -148,4 +148,16 @@ class TestGemRequestConnectionPool < Gem::TestCase end end.join end + + def test_checkouts_multiple_connections_from_the_pool + uri = Gem::URI.parse("http://example/some_endpoint") + pools = Gem::Request::ConnectionPools.new nil, [], 2 + pool = pools.pool_for uri + + pool.checkout + + Thread.new do + assert_not_nil(pool.checkout) + end.join + end end diff --git a/test/rubygems/test_gem_request_set.rb b/test/rubygems/test_gem_request_set.rb index 9aa244892c..33054aa8e5 100644 --- a/test/rubygems/test_gem_request_set.rb +++ b/test/rubygems/test_gem_request_set.rb @@ -93,6 +93,34 @@ Gems to install: end end + def test_install_from_gemdeps_explain_verbose + spec_fetcher do |fetcher| + fetcher.gem "a", 2 + end + + rs = Gem::RequestSet.new + + verbose = Gem.configuration.verbose + Gem.configuration.verbose = :really + + File.open "gem.deps.rb", "w" do |io| + io.puts 'gem "a"' + io.flush + + expected = <<-EXPECTED +Gems to install: + a-2 + EXPECTED + + actual, _ = capture_output do + rs.install_from_gemdeps gemdeps: io.path, explain: true + end + assert_equal(expected, actual) + end + ensure + Gem.configuration.verbose = verbose + end + def test_install_from_gemdeps_install_dir spec_fetcher do |fetcher| fetcher.gem "a", 2 @@ -311,6 +339,110 @@ ruby "0" assert_empty rs.dependencies end + def test_load_gemdeps_with_lockfile_gem_section + rs = Gem::RequestSet.new + + File.open "gem.deps.rb", "w" do |io| + io.puts 'gem "b"' + end + + File.open "gem.deps.rb.lock", "w" do |io| + io.puts <<~LOCKFILE + GEM + remote: #{@gem_repo} + specs: + a (1) + b (1) + a (~> 1.0) + + PLATFORMS + #{Gem::Platform::RUBY} + + DEPENDENCIES + b + LOCKFILE + end + + rs.load_gemdeps "gem.deps.rb" + + lock_set = rs.sets.find {|set| Gem::Resolver::LockSet === set } + refute_nil lock_set, "LockSet should be created from GEM section" + assert_equal %w[a-1 b-1], lock_set.specs.map(&:full_name).sort + end + + def test_load_gemdeps_with_lockfile_git_section + rs = Gem::RequestSet.new + + File.open "gem.deps.rb", "w" do |io| + io.puts 'gem "a", :git => "git://example/a.git"' + end + + File.open "gem.deps.rb.lock", "w" do |io| + io.puts <<~LOCKFILE + GIT + remote: git://example/a.git + revision: deadbeef + specs: + a (1) + + PLATFORMS + #{Gem::Platform::RUBY} + + DEPENDENCIES + a! + LOCKFILE + end + + rs.load_gemdeps "gem.deps.rb" + + git_set = rs.sets.find {|set| Gem::Resolver::GitSet === set } + refute_nil git_set, "GitSet should be created from GIT section" + assert_includes git_set.specs.keys, "a" + end + + def test_load_gemdeps_with_lockfile_path_section + _, _, directory = vendor_gem + + rs = Gem::RequestSet.new + + File.open "gem.deps.rb", "w" do |io| + io.puts "gem \"a\", :path => #{directory.inspect}" + end + + File.open "gem.deps.rb.lock", "w" do |io| + io.puts <<~LOCKFILE + PATH + remote: #{directory} + specs: + a (1) + + PLATFORMS + #{Gem::Platform::RUBY} + + DEPENDENCIES + a! + LOCKFILE + end + + rs.load_gemdeps "gem.deps.rb" + + vendor_set = rs.sets.find {|set| Gem::Resolver::VendorSet === set } + refute_nil vendor_set, "VendorSet should be created from PATH section" + assert_equal %w[a-1], vendor_set.specs.values.map(&:full_name) + end + + def test_load_gemdeps_with_missing_lockfile + rs = Gem::RequestSet.new + + File.open "gem.deps.rb", "w" do |io| + io.puts 'gem "a"' + end + + rs.load_gemdeps "gem.deps.rb" + + assert_equal [dep("a")], rs.dependencies + end + def test_resolve a = util_spec "a", "2", "b" => ">= 2" b = util_spec "b", "2" diff --git a/test/rubygems/test_gem_request_set_lockfile_parser.rb b/test/rubygems/test_gem_request_set_lockfile_parser.rb deleted file mode 100644 index 253a59b243..0000000000 --- a/test/rubygems/test_gem_request_set_lockfile_parser.rb +++ /dev/null @@ -1,544 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" -require "rubygems/request_set" -require "rubygems/request_set/lockfile" -require "rubygems/request_set/lockfile/tokenizer" -require "rubygems/request_set/lockfile/parser" - -class TestGemRequestSetLockfileParser < Gem::TestCase - def setup - super - @gem_deps_file = "gem.deps.rb" - @lock_file = File.expand_path "#{@gem_deps_file}.lock" - @set = Gem::RequestSet.new - end - - def test_get - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "\n" - parser = tokenizer.make_parser nil, nil - - assert_equal :newline, parser.get.first - end - - def test_get_type_mismatch - filename = File.expand_path("#{@gem_deps_file}.lock") - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "foo", filename, 1, 0 - parser = tokenizer.make_parser nil, nil - - e = assert_raise Gem::RequestSet::Lockfile::ParseError do - parser.get :section - end - - expected = - 'unexpected token [:text, "foo"], expected :section (at line 1 column 0)' - - assert_equal expected, e.message - - assert_equal 1, e.line - assert_equal 0, e.column - assert_equal filename, e.path - end - - def test_get_type_multiple - filename = File.expand_path("#{@gem_deps_file}.lock") - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "x", filename, 1 - parser = tokenizer.make_parser nil, nil - - assert parser.get [:text, :section] - end - - def test_get_type_value_mismatch - filename = File.expand_path("#{@gem_deps_file}.lock") - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "x", filename, 1 - parser = tokenizer.make_parser nil, nil - - e = assert_raise Gem::RequestSet::Lockfile::ParseError do - parser.get :text, "y" - end - - expected = - 'unexpected token [:text, "x"], expected [:text, "y"] (at line 1 column 0)' - - assert_equal expected, e.message - - assert_equal 1, e.line - assert_equal 0, e.column - assert_equal File.expand_path("#{@gem_deps_file}.lock"), e.path - end - - def test_parse - write_lockfile <<-LOCKFILE.strip -GEM - remote: #{@gem_repo} - specs: - a (2) - -PLATFORMS - #{Gem::Platform::RUBY} - -DEPENDENCIES - a - LOCKFILE - - platforms = [] - parse_lockfile @set, platforms - - assert_equal [dep("a")], @set.dependencies - - assert_equal [Gem::Platform::RUBY], platforms - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - assert lockfile_set, "could not find a LockSet" - - assert_equal %w[a-2], lockfile_set.specs.map(&:full_name) - end - - def test_parse_dependencies - write_lockfile <<-LOCKFILE -GEM - remote: #{@gem_repo} - specs: - a (2) - -PLATFORMS - #{Gem::Platform::RUBY} - -DEPENDENCIES - a (>= 1, <= 2) - LOCKFILE - - platforms = [] - parse_lockfile @set, platforms - - assert_equal [dep("a", ">= 1", "<= 2")], @set.dependencies - - assert_equal [Gem::Platform::RUBY], platforms - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - assert lockfile_set, "could not find a LockSet" - - assert_equal %w[a-2], lockfile_set.specs.map(&:full_name) - end - - def test_parse_DEPENDENCIES_git - write_lockfile <<-LOCKFILE -GIT - remote: git://git.example/josevalim/rails-footnotes.git - revision: 3a6ac1971e91d822f057650cc5916ebfcbd6ee37 - specs: - rails-footnotes (3.7.9) - rails (>= 3.0.0) - -GIT - remote: git://git.example/svenfuchs/i18n-active_record.git - revision: 55507cf59f8f2173d38e07e18df0e90d25b1f0f6 - specs: - i18n-active_record (0.0.2) - i18n (>= 0.5.0) - -GEM - remote: http://gems.example/ - specs: - i18n (0.6.9) - rails (4.0.0) - -PLATFORMS - ruby - -DEPENDENCIES - i18n-active_record! - rails-footnotes! - LOCKFILE - - parse_lockfile @set, [] - - expected = [ - dep("i18n-active_record", "= 0.0.2"), - dep("rails-footnotes", "= 3.7.9"), - ] - - assert_equal expected, @set.dependencies - end - - def test_parse_DEPENDENCIES_git_version - write_lockfile <<-LOCKFILE -GIT - remote: git://github.com/progrium/ruby-jwt.git - revision: 8d74770c6cd92ea234b428b5d0c1f18306a4f41c - specs: - jwt (1.1) - -GEM - remote: http://gems.example/ - specs: - -PLATFORMS - ruby - -DEPENDENCIES - jwt (= 1.1)! - LOCKFILE - - parse_lockfile @set, [] - - expected = [ - dep("jwt", "= 1.1"), - ] - - assert_equal expected, @set.dependencies - end - - def test_parse_GEM - write_lockfile <<-LOCKFILE -GEM - specs: - a (2) - -PLATFORMS - ruby - -DEPENDENCIES - a - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", ">= 0")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - assert lockfile_set, "found a LockSet" - - assert_equal %w[a-2], lockfile_set.specs.map(&:full_name) - end - - def test_parse_GEM_remote_multiple - write_lockfile <<-LOCKFILE -GEM - remote: https://gems.example/ - remote: https://other.example/ - specs: - a (2) - -PLATFORMS - ruby - -DEPENDENCIES - a - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", ">= 0")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - assert lockfile_set, "found a LockSet" - - assert_equal %w[a-2], lockfile_set.specs.map(&:full_name) - - assert_equal %w[https://gems.example/ https://other.example/], - lockfile_set.specs.flat_map {|s| s.sources.map {|src| src.uri.to_s } } - end - - def test_parse_GIT - @set.instance_variable_set :@install_dir, "install_dir" - - write_lockfile <<-LOCKFILE -GIT - remote: git://example/a.git - revision: abranch - specs: - a (2) - b (>= 3) - c - -DEPENDENCIES - a! - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", "= 2")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - refute lockfile_set, "found a LockSet" - - git_set = @set.sets.find do |set| - Gem::Resolver::GitSet === set - end - - assert git_set, "could not find a GitSet" - - assert_equal %w[a-2], git_set.specs.values.map(&:full_name) - - assert_equal [dep("b", ">= 3"), dep("c")], - git_set.specs.values.first.dependencies - - expected = { - "a" => %w[git://example/a.git abranch], - } - - assert_equal expected, git_set.repositories - assert_equal "install_dir", git_set.root_dir - end - - def test_parse_GIT_branch - write_lockfile <<-LOCKFILE -GIT - remote: git://example/a.git - revision: 1234abc - branch: 0-9-12-stable - specs: - a (2) - b (>= 3) - -DEPENDENCIES - a! - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", "= 2")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - refute lockfile_set, "found a LockSet" - - git_set = @set.sets.find do |set| - Gem::Resolver::GitSet === set - end - - assert git_set, "could not find a GitSet" - - expected = { - "a" => %w[git://example/a.git 1234abc], - } - - assert_equal expected, git_set.repositories - end - - def test_parse_GIT_ref - write_lockfile <<-LOCKFILE -GIT - remote: git://example/a.git - revision: 1234abc - ref: 1234abc - specs: - a (2) - b (>= 3) - -DEPENDENCIES - a! - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", "= 2")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - refute lockfile_set, "found a LockSet" - - git_set = @set.sets.find do |set| - Gem::Resolver::GitSet === set - end - - assert git_set, "could not find a GitSet" - - expected = { - "a" => %w[git://example/a.git 1234abc], - } - - assert_equal expected, git_set.repositories - end - - def test_parse_GIT_tag - write_lockfile <<-LOCKFILE -GIT - remote: git://example/a.git - revision: 1234abc - tag: v0.9.12 - specs: - a (2) - b (>= 3) - -DEPENDENCIES - a! - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", "= 2")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - refute lockfile_set, "found a LockSet" - - git_set = @set.sets.find do |set| - Gem::Resolver::GitSet === set - end - - assert git_set, "could not find a GitSet" - - expected = { - "a" => %w[git://example/a.git 1234abc], - } - - assert_equal expected, git_set.repositories - end - - def test_parse_PATH - _, _, directory = vendor_gem - - write_lockfile <<-LOCKFILE -PATH - remote: #{directory} - specs: - a (1) - b (2) - -DEPENDENCIES - a! - LOCKFILE - - parse_lockfile @set, [] - - assert_equal [dep("a", "= 1")], @set.dependencies - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - refute lockfile_set, "found a LockSet" - - vendor_set = @set.sets.find do |set| - Gem::Resolver::VendorSet === set - end - - assert vendor_set, "could not find a VendorSet" - - assert_equal %w[a-1], vendor_set.specs.values.map(&:full_name) - - spec = vendor_set.load_spec "a", nil, nil, nil - - assert_equal [dep("b", "= 2")], spec.dependencies - end - - def test_parse_dependency - write_lockfile " 1)" - - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file @lock_file - parser = tokenizer.make_parser nil, nil - - parsed = parser.parse_dependency "a", "=" - - assert_equal dep("a", "= 1"), parsed - - write_lockfile ")" - - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file @lock_file - parser = tokenizer.make_parser nil, nil - - parsed = parser.parse_dependency "a", "2" - - assert_equal dep("a", "= 2"), parsed - end - - def test_parse_gem_specs_dependency - write_lockfile <<-LOCKFILE -GEM - remote: #{@gem_repo} - specs: - a (2) - b (= 3) - c (~> 4) - d - e (~> 5.0, >= 5.0.1) - b (3-x86_64-linux) - -PLATFORMS - #{Gem::Platform::RUBY} - -DEPENDENCIES - a - LOCKFILE - - platforms = [] - parse_lockfile @set, platforms - - assert_equal [dep("a")], @set.dependencies - - assert_equal [Gem::Platform::RUBY], platforms - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - assert lockfile_set, "could not find a LockSet" - - assert_equal %w[a-2 b-3], lockfile_set.specs.map(&:full_name) - - expected = [ - Gem::Platform::RUBY, - Gem::Platform.new("x86_64-linux"), - ] - - assert_equal expected, lockfile_set.specs.map(&:platform) - - spec = lockfile_set.specs.first - - expected = [ - dep("b", "= 3"), - dep("c", "~> 4"), - dep("d"), - dep("e", "~> 5.0", ">= 5.0.1"), - ] - - assert_equal expected, spec.dependencies - end - - def test_parse_missing - assert_raise(Errno::ENOENT) do - parse_lockfile @set, [] - end - - lockfile_set = @set.sets.find do |set| - Gem::Resolver::LockSet === set - end - - refute lockfile_set - end - - def write_lockfile(lockfile) - File.open @lock_file, "w" do |io| - io.write lockfile - end - end - - def parse_lockfile(set, platforms) - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file @lock_file - parser = tokenizer.make_parser set, platforms - parser.parse - end -end diff --git a/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb b/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb deleted file mode 100644 index dce8c9ada5..0000000000 --- a/test/rubygems/test_gem_request_set_lockfile_tokenizer.rb +++ /dev/null @@ -1,307 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" -require "rubygems/request_set" -require "rubygems/request_set/lockfile" -require "rubygems/request_set/lockfile/tokenizer" -require "rubygems/request_set/lockfile/parser" - -class TestGemRequestSetLockfileTokenizer < Gem::TestCase - def setup - super - - @gem_deps_file = "gem.deps.rb" - @lock_file = File.expand_path "#{@gem_deps_file}.lock" - end - - def test_peek - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "\n" - - assert_equal :newline, tokenizer.peek.first - - assert_equal :newline, tokenizer.next_token.first - - assert_equal :EOF, tokenizer.peek.first - end - - def test_skip - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "\n" - - refute_predicate tokenizer, :empty? - - tokenizer.skip :newline - - assert_empty tokenizer - end - - def test_token_pos - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "" - assert_equal [5, 0], tokenizer.token_pos(5) - - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "", nil, 1, 2 - assert_equal [3, 1], tokenizer.token_pos(5) - end - - def test_tokenize - write_lockfile <<-LOCKFILE -GEM - remote: #{@gem_repo} - specs: - a (2) - b (= 2) - c (!= 3) - d (> 4) - e (< 5) - f (>= 6) - g (<= 7) - h (~> 8) - -PLATFORMS - #{Gem::Platform::RUBY} - -DEPENDENCIES - a - LOCKFILE - - expected = [ - [:section, "GEM", 0, 0], - [:newline, nil, 3, 0], - - [:entry, "remote", 2, 1], - [:text, @gem_repo, 10, 1], - [:newline, nil, 34, 1], - - [:entry, "specs", 2, 2], - [:newline, nil, 8, 2], - - [:text, "a", 4, 3], - [:l_paren, nil, 6, 3], - [:text, "2", 7, 3], - [:r_paren, nil, 8, 3], - [:newline, nil, 9, 3], - - [:text, "b", 6, 4], - [:l_paren, nil, 8, 4], - [:requirement, "=", 9, 4], - [:text, "2", 11, 4], - [:r_paren, nil, 12, 4], - [:newline, nil, 13, 4], - - [:text, "c", 6, 5], - [:l_paren, nil, 8, 5], - [:requirement, "!=", 9, 5], - [:text, "3", 12, 5], - [:r_paren, nil, 13, 5], - [:newline, nil, 14, 5], - - [:text, "d", 6, 6], - [:l_paren, nil, 8, 6], - [:requirement, ">", 9, 6], - [:text, "4", 11, 6], - [:r_paren, nil, 12, 6], - [:newline, nil, 13, 6], - - [:text, "e", 6, 7], - [:l_paren, nil, 8, 7], - [:requirement, "<", 9, 7], - [:text, "5", 11, 7], - [:r_paren, nil, 12, 7], - [:newline, nil, 13, 7], - - [:text, "f", 6, 8], - [:l_paren, nil, 8, 8], - [:requirement, ">=", 9, 8], - [:text, "6", 12, 8], - [:r_paren, nil, 13, 8], - [:newline, nil, 14, 8], - - [:text, "g", 6, 9], - [:l_paren, nil, 8, 9], - [:requirement, "<=", 9, 9], - [:text, "7", 12, 9], - [:r_paren, nil, 13, 9], - [:newline, nil, 14, 9], - - [:text, "h", 6, 10], - [:l_paren, nil, 8, 10], - [:requirement, "~>", 9, 10], - [:text, "8", 12, 10], - [:r_paren, nil, 13, 10], - [:newline, nil, 14, 10], - - [:newline, nil, 0, 11], - - [:section, "PLATFORMS", 0, 12], - [:newline, nil, 9, 12], - - [:text, Gem::Platform::RUBY, 2, 13], - [:newline, nil, 6, 13], - - [:newline, nil, 0, 14], - - [:section, "DEPENDENCIES", 0, 15], - [:newline, nil, 12, 15], - - [:text, "a", 2, 16], - [:newline, nil, 3, 16], - ] - - assert_equal expected, tokenize_lockfile - end - - def test_tokenize_capitals - write_lockfile <<-LOCKFILE -GEM - remote: #{@gem_repo} - specs: - Ab (2) - -PLATFORMS - #{Gem::Platform::RUBY} - -DEPENDENCIES - Ab - LOCKFILE - - expected = [ - [:section, "GEM", 0, 0], - [:newline, nil, 3, 0], - [:entry, "remote", 2, 1], - [:text, @gem_repo, 10, 1], - [:newline, nil, 34, 1], - [:entry, "specs", 2, 2], - [:newline, nil, 8, 2], - [:text, "Ab", 4, 3], - [:l_paren, nil, 7, 3], - [:text, "2", 8, 3], - [:r_paren, nil, 9, 3], - [:newline, nil, 10, 3], - [:newline, nil, 0, 4], - [:section, "PLATFORMS", 0, 5], - [:newline, nil, 9, 5], - [:text, Gem::Platform::RUBY, 2, 6], - [:newline, nil, 6, 6], - [:newline, nil, 0, 7], - [:section, "DEPENDENCIES", 0, 8], - [:newline, nil, 12, 8], - [:text, "Ab", 2, 9], - [:newline, nil, 4, 9], - ] - - assert_equal expected, tokenize_lockfile - end - - def test_tokenize_conflict_markers - write_lockfile "<<<<<<<" - - e = assert_raise Gem::RequestSet::Lockfile::ParseError do - tokenize_lockfile - end - - assert_equal "your #{@lock_file} contains merge conflict markers (at line 0 column 0)", - e.message - - write_lockfile "|||||||" - - e = assert_raise Gem::RequestSet::Lockfile::ParseError do - tokenize_lockfile - end - - assert_equal "your #{@lock_file} contains merge conflict markers (at line 0 column 0)", - e.message - - write_lockfile "=======" - - e = assert_raise Gem::RequestSet::Lockfile::ParseError do - tokenize_lockfile - end - - assert_equal "your #{@lock_file} contains merge conflict markers (at line 0 column 0)", - e.message - - write_lockfile ">>>>>>>" - - e = assert_raise Gem::RequestSet::Lockfile::ParseError do - tokenize_lockfile - end - - assert_equal "your #{@lock_file} contains merge conflict markers (at line 0 column 0)", - e.message - end - - def test_tokenize_git - write_lockfile <<-LOCKFILE -DEPENDENCIES - a! - LOCKFILE - - expected = [ - [:section, "DEPENDENCIES", 0, 0], - [:newline, nil, 12, 0], - - [:text, "a", 2, 1], - [:bang, nil, 3, 1], - [:newline, nil, 4, 1], - ] - - assert_equal expected, tokenize_lockfile - end - - def test_tokenize_multiple - write_lockfile <<-LOCKFILE -GEM - remote: #{@gem_repo} - specs: - a (2) - b (~> 3.0, >= 3.0.1) - LOCKFILE - - expected = [ - [:section, "GEM", 0, 0], - [:newline, nil, 3, 0], - - [:entry, "remote", 2, 1], - [:text, @gem_repo, 10, 1], - [:newline, nil, 34, 1], - - [:entry, "specs", 2, 2], - [:newline, nil, 8, 2], - - [:text, "a", 4, 3], - [:l_paren, nil, 6, 3], - [:text, "2", 7, 3], - [:r_paren, nil, 8, 3], - [:newline, nil, 9, 3], - - [:text, "b", 6, 4], - [:l_paren, nil, 8, 4], - [:requirement, "~>", 9, 4], - [:text, "3.0", 12, 4], - [:comma, nil, 15, 4], - [:requirement, ">=", 17, 4], - [:text, "3.0.1", 20, 4], - [:r_paren, nil, 25, 4], - [:newline, nil, 26, 4], - ] - - assert_equal expected, tokenize_lockfile - end - - def test_unget - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.new "\n" - tokenizer.unshift :token - parser = tokenizer.make_parser nil, nil - - assert_equal :token, parser.get - end - - def write_lockfile(lockfile) - File.open @lock_file, "w" do |io| - io.write lockfile - end - end - - def tokenize_lockfile - Gem::RequestSet::Lockfile::Tokenizer.from_file(@lock_file).to_a - end -end diff --git a/test/rubygems/test_gem_requirement.rb b/test/rubygems/test_gem_requirement.rb index de0d11ec00..00634dc7f4 100644 --- a/test/rubygems/test_gem_requirement.rb +++ b/test/rubygems/test_gem_requirement.rb @@ -137,11 +137,7 @@ class TestGemRequirement < Gem::TestCase refute_satisfied_by "1.2", r assert_satisfied_by "1.3", r - assert_raise ArgumentError do - Gem::Deprecate.skip_during do - assert_satisfied_by nil, r - end - end + assert_satisfied_by nil, r end def test_satisfied_by_eh_blank @@ -151,11 +147,7 @@ class TestGemRequirement < Gem::TestCase assert_satisfied_by "1.2", r refute_satisfied_by "1.3", r - assert_raise ArgumentError do - Gem::Deprecate.skip_during do - assert_satisfied_by nil, r - end - end + refute_satisfied_by nil, r end def test_satisfied_by_eh_equal @@ -165,11 +157,7 @@ class TestGemRequirement < Gem::TestCase assert_satisfied_by "1.2", r refute_satisfied_by "1.3", r - assert_raise ArgumentError do - Gem::Deprecate.skip_during do - assert_satisfied_by nil, r - end - end + refute_satisfied_by nil, r end def test_satisfied_by_eh_gt @@ -179,9 +167,7 @@ class TestGemRequirement < Gem::TestCase refute_satisfied_by "1.2", r assert_satisfied_by "1.3", r - assert_raise ArgumentError do - r.satisfied_by? nil - end + refute_satisfied_by nil, r end def test_satisfied_by_eh_gte diff --git a/test/rubygems/test_gem_resolver.rb b/test/rubygems/test_gem_resolver.rb index 4990d5d2dd..84ede36b6c 100644 --- a/test/rubygems/test_gem_resolver.rb +++ b/test/rubygems/test_gem_resolver.rb @@ -86,63 +86,6 @@ class TestGemResolver < Gem::TestCase assert_same index_set, composed end - def test_requests - a1 = util_spec "a", 1, "b" => 2 - - r1 = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - - act = Gem::Resolver::ActivationRequest.new a1, r1 - - res = Gem::Resolver.new [a1] - - reqs = [] - - res.requests a1, act, reqs - - assert_equal ["b (= 2)"], reqs.map(&:to_s) - end - - def test_requests_development - a1 = util_spec "a", 1, "b" => 2 - - spec = Gem::Resolver::SpecSpecification.new nil, a1 - def spec.fetch_development_dependencies - @called = true - end - - r1 = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - - act = Gem::Resolver::ActivationRequest.new spec, r1 - - res = Gem::Resolver.new [act] - res.development = true - - reqs = [] - - res.requests spec, act, reqs - - assert_equal ["b (= 2)"], reqs.map(&:to_s) - - assert spec.instance_variable_defined? :@called - end - - def test_requests_ignore_dependencies - a1 = util_spec "a", 1, "b" => 2 - - r1 = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - - act = Gem::Resolver::ActivationRequest.new a1, r1 - - res = Gem::Resolver.new [a1] - res.ignore_dependencies = true - - reqs = [] - - res.requests a1, act, reqs - - assert_empty reqs - end - def test_resolve_conservative a1_spec = util_spec "a", 1 @@ -197,6 +140,34 @@ class TestGemResolver < Gem::TestCase assert_resolves_to [a2_spec, b2_spec, c1_spec, d2_spec, e1_spec], res end + def test_conservative_upgrades_when_installed_blocked + # Conservative mode floats the installed (skip) version to the front but + # keeps newer versions selectable. When the installed version cannot be + # used because its own dependency is unsatisfiable, the solver backtracks + # to a newer version instead of failing. This intentionally diverges from + # Molinillo (which hard-restricted to skip versions and raised) and reaches + # Bundler's upgrade-over-raise outcome. See the comment in + # Gem::Resolver#all_versions_for. + a1_spec = util_spec "a", 1 do |s| + s.add_dependency "b", ">= 2" + end + a2_spec = util_spec "a", 2 do |s| + s.add_dependency "b", ">= 1" + end + b1_spec = util_spec "b", 1 + + # b-2 is intentionally absent, so a-1's `b >= 2` cannot be satisfied. + deps = [make_dep("a", ">= 1")] + s = set a1_spec, a2_spec, b1_spec + + res = Gem::Resolver.new deps, s + # a-1 is already installed and satisfies `a >= 1`, so conservative mode + # prefers it - but it is blocked by the missing b-2, forcing an upgrade. + res.skip_gems = { "a" => [a1_spec] } + + assert_resolves_to [a2_spec, b1_spec], res + end + def test_resolve_development a_spec = util_spec "a", 1 do |s| s.add_development_dependency "b" @@ -511,19 +482,10 @@ class TestGemResolver < Gem::TestCase r.resolve end - deps = [make_dep("c", "= 2"), make_dep("c", "= 1")] - assert_equal deps, e.conflicting_dependencies - - con = e.conflict - - act = con.activated - assert_equal "c-1", act.spec.full_name - - parent = act.parent - assert_equal "a-1", parent.spec.full_name - - act = con.requester - assert_equal "b-1", act.spec.full_name + assert_nil e.conflict + assert_match(/your request/, e.message) + assert_match(/a depends on c/, e.message) + assert_match(/b depends on c/, e.message) end def test_raises_when_a_gem_is_missing @@ -578,12 +540,11 @@ class TestGemResolver < Gem::TestCase r = Gem::Resolver.new([ad], set(a1)) - e = assert_raise Gem::UnsatisfiableDependencyError do + e = assert_raise Gem::DependencyResolutionError do r.resolve end - assert_equal "Unable to resolve dependency: 'a (= 1)' requires 'b (= 2)'", - e.message + assert_match(/depends on b = 2 which could not be found in any repository/, e.message) end def test_raises_when_possibles_are_exhausted @@ -605,18 +566,9 @@ class TestGemResolver < Gem::TestCase r.resolve end - dependency = e.conflict.dependency - - assert_includes %w[a b], dependency.name - assert_equal req(">= 0"), dependency.requirement - - activated = e.conflict.activated - assert_equal "c-1", activated.full_name - - assert_equal dep("c", "= 1"), activated.request.dependency - - assert_equal [dep("c", ">= 2"), dep("c", "= 1")], - e.conflict.conflicting_dependencies + assert_nil e.conflict + assert_match(/a depends on c/, e.message) + assert_match(/b depends on c/, e.message) end def test_keeps_resolving_after_seeing_satisfied_dep @@ -772,7 +724,7 @@ class TestGemResolver < Gem::TestCase assert_resolves_to [b1, c1, d2], r end - def test_sorts_by_source_then_version + def test_picks_highest_version_across_sources source_a = Gem::Source.new "http://example.com/a" source_b = Gem::Source.new "http://example.com/b" source_c = Gem::Source.new "http://example.com/c" @@ -795,7 +747,43 @@ class TestGemResolver < Gem::TestCase resolver = Gem::Resolver.new [dependency], set - assert_resolves_to [spec_b_2], resolver + assert_resolves_to [spec_a_2], resolver + end + + def test_same_version_prefers_earlier_source + source_a = Gem::Source.new "http://example.com/a" + source_b = Gem::Source.new "http://example.com/b" + + spec_a = util_spec "some-dep", "1.0.0" + spec_b = util_spec "some-dep", "1.0.0" + + set = StaticSet.new [ + Gem::Resolver::SpecSpecification.new(nil, spec_a, source_a), + Gem::Resolver::SpecSpecification.new(nil, spec_b, source_b), + ] + + resolver = Gem::Resolver.new [make_dep("some-dep", "> 0")], set + result = resolver.resolve + + assert_equal source_a, result.first.spec.source + end + + def test_same_version_prefers_earlier_source_when_order_flipped + source_a = Gem::Source.new "http://example.com/a" + source_b = Gem::Source.new "http://example.com/b" + + spec_a = util_spec "some-dep", "1.0.0" + spec_b = util_spec "some-dep", "1.0.0" + + set = StaticSet.new [ + Gem::Resolver::SpecSpecification.new(nil, spec_b, source_b), + Gem::Resolver::SpecSpecification.new(nil, spec_a, source_a), + ] + + resolver = Gem::Resolver.new [make_dep("some-dep", "> 0")], set + result = resolver.resolve + + assert_equal source_b, result.first.spec.source end def test_select_local_platforms @@ -850,4 +838,338 @@ class TestGemResolver < Gem::TestCase assert_match "No match for 'a (= 1)' on this platform. Found: c-p-1", e.message end + + def test_resolve_prerelease_not_considered_when_stable_exists + # a-1.0 depends on b ~> 2.0 - only b-2.0.pre satisfies that, but + # b also has a stable version (1.0), so prereleases are filtered out. + # The resolver must fail, not silently use b-2.0.pre during propagation. + a_stable = util_spec "a", "1.0" do |s| + s.add_dependency "b", "~> 2.0" + end + + b_stable = util_spec "b", "1.0" + b_pre = util_spec "b", "2.0.pre" + + s = set(a_stable, b_stable, b_pre) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_raise Gem::DependencyResolutionError do + r.resolve + end + end + + def test_resolve_prerelease_considered_when_enabled + a_stable = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b_pre = util_spec "b", "2.0.pre" + + s = set(a_stable, b_pre) + s.prerelease = true + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a_stable, b_pre], r + end + + def test_resolve_prerelease_used_when_no_stable_versions_exist + a_stable = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b_pre = util_spec "b", "2.0.pre" + b_other_pre = util_spec "b", "1.0.pre" + + s = set(a_stable, b_pre, b_other_pre) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a_stable, b_pre], r + end + + def test_resolve_prerelease_required_by_exact_requirement + # A root dep with an exact prerelease version must resolve to that + # version even when stable versions of the same gem are in the set. + # Gem.finish_resolve hits this: it imports loaded_specs as exact-version + # deps, so the currently-activated prerelease bundler becomes a root dep. + a_stable = util_spec "a", "1.0" + a_pre = util_spec "a", "2.0.pre" + + s = set(a_stable, a_pre) + + ad = make_dep "a", "= 2.0.pre" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a_pre], r + end + + def test_resolve_transitive_prerelease_required_by_exact_requirement + # A transitive dep with an exact prerelease version must resolve to that + # version even when stable versions of the same gem are in the set. + # The gate on prereleases lives in versions_for and is per-constraint: + # `= 2.0.pre` carries a prerelease bound, so prereleases are admitted for + # this range even though the global prerelease flag is off. + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", "= 2.0.pre" + end + + b_stable = util_spec "b", "1.0" + b_pre = util_spec "b", "2.0.pre" + + s = set(a, b_stable, b_pre) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a, b_pre], r + end + + def test_error_includes_platform_hint_when_specs_exist_for_other_platforms + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b_foreign = util_spec "b", "1.0" do |s| + s.platform = "java" + end + + s = set(a, b_foreign) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/could not be found in any repository/, e.message) + assert_match(/b-1.0-java/, e.message) + end + + def test_error_includes_ruby_version_hint_when_filtered + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 1.0" + end + + b = util_spec "b", "1.0" do |s| + s.required_ruby_version = ">= 999.0" + end + + s = set(a, b) + + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/requires Ruby/, e.message) + assert_match(/you have/, e.message) + end + + def test_root_gem_incompatible_ruby_version_names_ruby_requirement + # A requested (root) gem available only for an incompatible Ruby version + # flows through the solver to a DependencyResolutionError whose message + # names the Ruby requirement. This matches Bundler (which models Ruby as a + # synthetic dependency and reports a solve failure) and is clearer than the + # platform-oriented UnsatisfiableDependencyError. Contrast the foreign- + # *platform* case (test_raises_and_explains_when_platform_prevents_install), + # which is genuinely "not found" and does raise UnsatisfiableDependencyError. + a = util_spec "a", "1.0" do |s| + s.required_ruby_version = ">= 999.0" + end + + ad = make_dep "a", "= 1.0" + r = Gem::Resolver.new([ad], set(a)) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/requires Ruby >= 999.0/, e.message) + end + + def test_self_dependency_does_not_crash + a = util_spec "a", "1.0" do |s| + s.add_dependency "a" + end + + s = set(a) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + assert_resolves_to [a], r + end + + def test_contradictory_root_requirements_give_clear_error + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" + + s = set(a1, a2) + r = Gem::Resolver.new([make_dep("a", "= 1"), make_dep("a", "= 2")], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/contradictory/, e.message) + refute_match(/unknown package/, e.message) + end + + def test_empty_range_transitive_dep_does_not_say_unknown + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", "> 2", "< 1" + end + + b = util_spec "b", "1.5" + + s = set(a, b) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/contradictory/, e.message) + refute_match(/unknown package/, e.message) + end + + def test_error_hints_about_prerelease_when_filtered + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", "~> 2.0" + end + + b_stable = util_spec "b", "1.0" + b_pre = util_spec "b", "2.0.pre" + + s = set(a, b_stable, b_pre) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/pre-release/, e.message) + assert_match(/--prerelease/, e.message) + end + + def test_soft_missing_skips_dep_with_wrong_version + a = util_spec "a", "1.0" do |s| + s.add_dependency "b", ">= 2.0" + end + + b = util_spec "b", "1.0" + + s = set(a, b) + ad = make_dep "a" + r = Gem::Resolver.new([ad], s) + r.soft_missing = true + + # b exists but only 1.0, which doesn't satisfy >= 2.0. + # With soft_missing (--force), the dep should be skipped. + assert_resolves_to [a], r + end + + def test_backtracks_to_clean_sibling_when_higher_version_has_missing_dep + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" do |s| + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2)) + + # 'zzz' has zero specs anywhere, so a-2 is unusable, but a-1 is clean + # and resolution must backtrack to it rather than declaring every + # version of 'a' invalid. + assert_resolves_to [a1], r + end + + def test_backtracks_over_band_of_bad_high_versions_to_clean_lower + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" do |s| + s.add_dependency "zzz", ">= 1" + end + a3 = util_spec "a", "3" do |s| + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2, a3)) + + # Only the a-2..a-3 band shares the missing 'zzz' dep and should be + # eliminated; band scoping is load-bearing here, not just sibling + # presence. + assert_resolves_to [a1], r + end + + def test_backtracks_when_one_of_several_deps_is_missing + good = util_spec "good", "1" + a1 = util_spec "a", "1" do |s| + s.add_dependency "good", ">= 1" + end + a2 = util_spec "a", "2" do |s| + s.add_dependency "good", ">= 1" + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2, good)) + + # Only a-2, which carries the missing 'zzz' dep, is eliminated; the + # per-dep check inside a multi-dep version must not poison a-1. + assert_resolves_to [a1, good], r + end + + def test_fails_when_every_version_depends_on_missing_package + a1 = util_spec "a", "1" do |s| + s.add_dependency "zzz", ">= 1" + end + a2 = util_spec "a", "2" do |s| + s.add_dependency "zzz", ">= 1" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2)) + + e = assert_raise Gem::DependencyResolutionError do + r.resolve + end + + assert_match(/every version of a depends on zzz >= 1 which could not be found in any repository/, e.message) + end + + def test_resolves_when_only_lowest_version_has_missing_dep + a1 = util_spec "a", "1" do |s| + s.add_dependency "zzz", ">= 1" + end + a2 = util_spec "a", "2" + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2)) + + # a-2 is preferred/tried first, so this is already green; it guards + # against the bug being re-introduced in an order-sensitive way. + assert_resolves_to [a2], r + end + + def test_filtered_platform_dep_lets_clean_sibling_backtrack + a1 = util_spec "a", "1" + a2 = util_spec "a", "2" do |s| + s.add_dependency "b", ">= 1.0" + end + b_java = util_spec "b", "1.0" do |s| + s.platform = "java" + end + + r = Gem::Resolver.new([make_dep("a")], set(a1, a2, b_java)) + + # 'b' EXISTS in the unfiltered specs but is platform-filtered, so a-2 + # is unusable via NoVersions (not InvalidDependency). Resolution must + # backtrack to the clean a-1 rather than eliminating it. + assert_resolves_to [a1], r + end end diff --git a/test/rubygems/test_gem_resolver_best_set.rb b/test/rubygems/test_gem_resolver_best_set.rb index 02f542efc0..ac186884d1 100644 --- a/test/rubygems/test_gem_resolver_best_set.rb +++ b/test/rubygems/test_gem_resolver_best_set.rb @@ -31,6 +31,20 @@ class TestGemResolverBestSet < Gem::TestCase assert_equal %w[a-1], found.map(&:full_name) end + def test_pick_sets_prerelease + set = Gem::Resolver::BestSet.new + set.prerelease = true + + set.pick_sets + + sets = set.sets + + assert_equal 1, sets.count + + source_set = sets.first + assert_equal true, source_set.prerelease + end + def test_find_all_local spec_fetcher do |fetcher| fetcher.spec "a", 1 diff --git a/test/rubygems/test_gem_resolver_conflict.rb b/test/rubygems/test_gem_resolver_conflict.rb deleted file mode 100644 index 5696ff266d..0000000000 --- a/test/rubygems/test_gem_resolver_conflict.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require_relative "helper" - -class TestGemResolverConflict < Gem::TestCase - def test_explanation - root = - dependency_request dep("net-ssh", ">= 2.0.13"), "rye", "0.9.8" - child = - dependency_request dep("net-ssh", ">= 2.6.5"), "net-ssh", "2.2.2", root - - dep = Gem::Resolver::DependencyRequest.new dep("net-ssh", ">= 2.0.13"), nil - - spec = util_spec "net-ssh", "2.2.2" - active = - Gem::Resolver::ActivationRequest.new spec, dep - - conflict = - Gem::Resolver::Conflict.new child, active - - expected = <<-EXPECTED - Activated net-ssh-2.2.2 - which does not match conflicting dependency (>= 2.6.5) - - Conflicting dependency chains: - net-ssh (>= 2.0.13), 2.2.2 activated - - versus: - rye (= 0.9.8), 0.9.8 activated, depends on - net-ssh (>= 2.0.13), 2.2.2 activated, depends on - net-ssh (>= 2.6.5) - - EXPECTED - - assert_equal expected, conflict.explanation - end - - def test_explanation_user_request - spec = util_spec "a", 2 - - a1_req = Gem::Resolver::DependencyRequest.new dep("a", "= 1"), nil - a2_req = Gem::Resolver::DependencyRequest.new dep("a", "= 2"), nil - - activated = Gem::Resolver::ActivationRequest.new spec, a2_req - - conflict = Gem::Resolver::Conflict.new a1_req, activated - - expected = <<-EXPECTED - Activated a-2 - which does not match conflicting dependency (= 1) - - Conflicting dependency chains: - a (= 2), 2 activated - - versus: - a (= 1) - - EXPECTED - - assert_equal expected, conflict.explanation - end - - def test_request_path - root = - dependency_request dep("net-ssh", ">= 2.0.13"), "rye", "0.9.8" - - child = - dependency_request dep("other", ">= 1.0"), "net-ssh", "2.2.2", root - - conflict = - Gem::Resolver::Conflict.new nil, nil - - expected = [ - "net-ssh (>= 2.0.13), 2.2.2 activated", - "rye (= 0.9.8), 0.9.8 activated", - ] - - assert_equal expected, conflict.request_path(child.requester) - end -end diff --git a/test/rubygems/test_gem_resolver_git_specification.rb b/test/rubygems/test_gem_resolver_git_specification.rb index 621333d3bf..e03c61e27d 100644 --- a/test/rubygems/test_gem_resolver_git_specification.rb +++ b/test/rubygems/test_gem_resolver_git_specification.rb @@ -97,6 +97,44 @@ class TestGemResolverGitSpecification < Gem::TestCase assert_path_exist File.join git_spec.spec.extension_dir, "b.rb" end + def test_install_no_build_extension + pend if Gem.java_platform? + pend "terminates on mswin" if vc_windows? && ruby_repo? + name, _, repository, = git_gem "a", 1 do |s| + s.extensions << "ext/extconf.rb" + end + + Dir.chdir "git/a" do + FileUtils.mkdir_p "ext/lib" + + File.open "ext/extconf.rb", "w" do |io| + io.puts 'require "mkmf"' + io.puts 'create_makefile "a"' + end + + FileUtils.touch "ext/lib/b.rb" + + system @git, "add", "ext/extconf.rb" + system @git, "add", "ext/lib/b.rb" + + system @git, "commit", "--quiet", "-m", "Add extension files" + end + + source = Gem::Source::Git.new name, repository, nil, true + + spec = source.specs.first + + git_spec = Gem::Resolver::GitSpecification.new @set, spec, source + + use_ui @ui do + git_spec.install(build_extension: false) + end + + assert_path_not_exist File.join(git_spec.spec.extension_dir, "b.rb") + assert_match "contains native extensions that were not built", @ui.error + assert_match "gem pristine #{git_spec.spec.name} --extensions", @ui.error + end + def test_install_installed git_gem "a", 1 diff --git a/test/rubygems/test_gem_resolver_strategy.rb b/test/rubygems/test_gem_resolver_strategy.rb new file mode 100644 index 0000000000..57c9aadde8 --- /dev/null +++ b/test/rubygems/test_gem_resolver_strategy.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +require_relative "helper" + +class TestGemResolverStrategy < Gem::TestCase + # Minimal source that implements the two methods Strategy calls: + # all_versions_for(package) - returns versions in preference order + # versions_for(package, range) - returns versions matching a range + # + # Tracks call counts so we can assert on caching behavior. + class StubSource + attr_reader :versions_for_calls + + def initialize(versions_by_package) + @versions_by_package = versions_by_package + @versions_for_calls = 0 + end + + def all_versions_for(package) + @versions_by_package.fetch(package.to_s, []) + end + + def versions_for(package, range) + @versions_for_calls += 1 + all = @versions_by_package.fetch(package.to_s, []) + all.select {|v| range.include?(v) } + end + end + + def v(version_string) + Gem::Version.new(version_string) + end + + def make_package(name) + Gem::PubGrub::Package.new(name) + end + + def make_range_any + Gem::PubGrub::VersionRange.any + end + + # A range >= min (unbounded above) + def make_range_gte(version) + Gem::PubGrub::VersionRange.new(min: version, include_min: true) + end + + # A range >= min AND < max + def make_range_between(min, max) + Gem::PubGrub::VersionRange.new( + min: min, max: max, + include_min: true, include_max: false + ) + end + + def test_most_preferred_version_respects_all_versions_for_ordering + # all_versions_for returns [2.0, 1.0, 3.0] - so 2.0 is most preferred + # even though 3.0 is numerically highest. + pkg = make_package("a") + source = StubSource.new("a" => [v("2.0"), v("1.0"), v("3.0")]) + + strategy = Gem::Resolver::Strategy.new(source) + unsatisfied = { pkg => make_range_any } + + _package, version = strategy.next_package_and_version(unsatisfied) + + assert_equal v("2.0"), version + end + + def test_picks_most_constrained_package + # "a" has 3 matching versions, "b" has 1 matching version. + # Strategy should pick "b" because it's more constrained. + pkg_a = make_package("a") + pkg_b = make_package("b") + + source = StubSource.new( + "a" => [v("3.0"), v("2.0"), v("1.0")], + "b" => [v("1.0")] + ) + + strategy = Gem::Resolver::Strategy.new(source) + + unsatisfied = { + pkg_a => make_range_any, + pkg_b => make_range_any, + } + + package, _version = strategy.next_package_and_version(unsatisfied) + + assert_equal pkg_b, package + end + + def test_picks_package_with_fewer_higher_versions_as_tiebreaker + # Both "a" and "b" have 2 matching versions (so both get priority [1, ...]). + # "a" has matching [2.0, 1.0] with higher (above range) = [] (0 higher) + # "b" has matching [2.0, 1.0] with higher [3.0] (1 higher) + # Tiebreaker: fewer higher versions wins, so "a" is picked. + pkg_a = make_package("a") + pkg_b = make_package("b") + + range = make_range_between(v("0.5"), v("2.5")) + + source = StubSource.new( + "a" => [v("2.0"), v("1.0")], + "b" => [v("3.0"), v("2.0"), v("1.0")] + ) + + strategy = Gem::Resolver::Strategy.new(source) + + unsatisfied = { + pkg_a => range, + pkg_b => range, + } + + package, _version = strategy.next_package_and_version(unsatisfied) + + assert_equal pkg_a, package + end + + def test_cache_prevents_redundant_versions_for_calls + pkg = make_package("a") + source = StubSource.new("a" => [v("2.0"), v("1.0")]) + + strategy = Gem::Resolver::Strategy.new(source) + + range = make_range_any + unsatisfied = { pkg => range } + + # First call: should call versions_for for matching + upper_invert + most_preferred + strategy.next_package_and_version(unsatisfied) + calls_after_first = source.versions_for_calls + + # Second call with same package+range: next_term_to_try_from should + # hit the cache, so only most_preferred_version_of adds a call. + strategy.next_package_and_version(unsatisfied) + calls_after_second = source.versions_for_calls + + # The cached path saves the 2 calls in next_term_to_try_from, + # so only the 1 call from most_preferred_version_of is added. + assert_equal 1, calls_after_second - calls_after_first + end + + def test_cache_is_keyed_by_package_and_range + pkg = make_package("a") + source = StubSource.new("a" => [v("3.0"), v("2.0"), v("1.0")]) + + strategy = Gem::Resolver::Strategy.new(source) + + range_any = make_range_any + range_gte = make_range_gte(v("2.0")) + + # First call with range_any + strategy.next_package_and_version({ pkg => range_any }) + calls_after_first = source.versions_for_calls + + # Second call with different range - cache miss, so versions_for is called again + strategy.next_package_and_version({ pkg => range_gte }) + calls_after_second = source.versions_for_calls + + # A cache miss means 2 new versions_for calls (matching + upper_invert) + # plus 1 from most_preferred_version_of = 3 total new calls + assert_equal 3, calls_after_second - calls_after_first + end +end diff --git a/test/rubygems/test_gem_safe_marshal.rb b/test/rubygems/test_gem_safe_marshal.rb index deeb8205bc..7e3a046c4e 100644 --- a/test/rubygems/test_gem_safe_marshal.rb +++ b/test/rubygems/test_gem_safe_marshal.rb @@ -234,8 +234,6 @@ class TestGemSafeMarshal < Gem::TestCase end def test_link_after_float - pend "Marshal.load of links and floats is broken on truffleruby, see https://github.com/oracle/truffleruby/issues/3747" if RUBY_ENGINE == "truffleruby" - a = [] a << a assert_safe_load_as [0.0, a, 1.0, a] @@ -254,6 +252,8 @@ class TestGemSafeMarshal < Gem::TestCase end def test_hash_with_compare_by_identity + pend "Marshal.dump of a compare_by_identity Hash emits an unexpected ivar on jruby" if RUBY_ENGINE == "jruby" + with_const(Gem::SafeMarshal, :PERMITTED_CLASSES, %w[Hash]) do assert_safe_load_as Hash.new.compare_by_identity.tap {|h| h[+"a"] = 1 diff --git a/test/rubygems/test_gem_safe_yaml.rb b/test/rubygems/test_gem_safe_yaml.rb index 02df9f97da..8d0ac63c41 100644 --- a/test/rubygems/test_gem_safe_yaml.rb +++ b/test/rubygems/test_gem_safe_yaml.rb @@ -5,6 +5,28 @@ require_relative "helper" Gem.load_yaml class TestGemSafeYAML < Gem::TestCase + def yaml_load(input, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES, + permitted_symbols: Gem::SafeYAML::PERMITTED_SYMBOLS, + aliases: true) + if Gem.use_psych? + Psych.safe_load(input, permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + aliases: aliases) + else + Gem::YAMLSerializer.load(input, permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + aliases: aliases) + end + end + + def yaml_dump(obj) + if Gem.use_psych? + obj.to_yaml + else + Gem::YAMLSerializer.dump(obj) + end + end + def test_aliases_enabled_by_default assert_predicate Gem::SafeYAML, :aliases_enabled? assert_equal({ "a" => "a", "b" => "a" }, Gem::SafeYAML.safe_load("a: &a a\nb: *a\n")) @@ -21,4 +43,1284 @@ class TestGemSafeYAML < Gem::TestCase ensure Gem::SafeYAML.aliases_enabled = aliases_enabled end + + def test_specification_version_is_integer + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test + version: !ruby/object:Gem::Version + version: 1.0.0 + specification_version: 4 + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Integer, spec.specification_version + assert_equal 4, spec.specification_version + end + + def test_disallowed_class_rejected + yaml = <<~YAML + --- !ruby/object:SomeDisallowedClass + foo: bar + YAML + + exception = assert_raise(Psych::DisallowedClass) do + Gem::SafeYAML.safe_load(yaml) + end + assert_match(/unspecified class/, exception.message) + end + + def test_plain_tag_key_does_not_construct_specification + yaml = <<~YAML + tag: "!ruby/object:Gem::Specification" + name: pwned + arbitrary_ivar: hello + YAML + + result = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Hash, result + assert_equal "!ruby/object:Gem::Specification", result["tag"] + assert_equal "pwned", result["name"] + end + + def test_disallowed_symbol_rejected + yaml = <<~YAML + --- !ruby/object:Gem::Dependency + name: test + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 0 + type: :invalid_type + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 0 + YAML + + exception = assert_raise(Psych::DisallowedClass) do + Gem::SafeYAML.safe_load(yaml) + end + assert_match(/unspecified class/, exception.message) + end + + def test_disallowed_symbol_not_interned + unique = "rejected_symbol_#{rand(1 << 30)}" + yaml = <<~YAML + --- !ruby/object:Gem::Dependency + name: test + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 0 + type: :#{unique} + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 0 + YAML + + assert_raise(Psych::DisallowedClass) do + Gem::YAMLSerializer.load(yaml, + permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES, + permitted_symbols: Gem::SafeYAML::PERMITTED_SYMBOLS) + end + refute_includes Symbol.all_symbols.map(&:to_s), unique + end + + def test_inline_array_nesting_capped + depth = Gem::YAMLSerializer::Parser::MAX_NESTING_DEPTH + 1 + yaml = "x: " + ("[" * depth) + "a" + ("]" * depth) + "\n" + + expected = [Psych::SyntaxError] + # JRuby's JVM stack overflows before the Ruby-level nesting cap fires. + expected << ::Java::JavaLang::StackOverflowError if RUBY_ENGINE == "jruby" + + assert_raise(*expected) do + Gem::YAMLSerializer.load(yaml, permitted_classes: []) + end + end + + def test_unknown_alias_raises + yaml = <<~YAML + foo: 1 + bar: *missing + YAML + + expected_error = defined?(Psych::AnchorNotDefined) ? Psych::AnchorNotDefined : Psych::BadAlias + assert_raise(expected_error) { Gem::SafeYAML.safe_load(yaml) } + end + + def test_unused_anchor_with_aliases_disabled_is_allowed + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = false + + result = Gem::SafeYAML.safe_load("foo: &unused 1\nbar: 2\n") + assert_equal({ "foo" => 1, "bar" => 2 }, result) + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_yaml_serializer_aliases_disabled + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = false + refute_predicate Gem::SafeYAML, :aliases_enabled? + + yaml = "a: &anchor value\nb: *anchor\n" + + assert_raise(Psych::AliasesNotEnabled) do + Gem::SafeYAML.safe_load(yaml) + end + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_real_gemspec_fileutils + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: fileutils + version: !ruby/object:Gem::Version + version: 1.8.0 + platform: ruby + authors: + - Minero Aoki + bindir: bin + cert_chain: [] + date: 1980-01-02 00:00:00.000000000 Z + dependencies: [] + description: Several file utility methods for copying, moving, removing, etc. + email: + - + executables: [] + extensions: [] + extra_rdoc_files: [] + files: + - BSDL + - COPYING + - README.md + - Rakefile + - fileutils.gemspec + - lib/fileutils.rb + homepage: https://github.com/ruby/fileutils + licenses: + - Ruby + - BSD-2-Clause + metadata: + source_code_uri: https://github.com/ruby/fileutils + rdoc_options: [] + require_paths: + - lib + required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 2.5.0 + required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + requirements: [] + rubygems_version: 3.6.9 + specification_version: 4 + summary: Several file utility methods for copying, moving, removing, etc. + test_files: [] + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "fileutils", spec.name + assert_equal Gem::Version.new("1.8.0"), spec.version + assert_kind_of Integer, spec.specification_version + assert_equal 4, spec.specification_version + end + + def test_yaml_anchor_and_alias_enabled + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = true + + yaml = <<~YAML + dependencies: + - &req !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + - *req + YAML + + result = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Hash, result + assert_kind_of Array, result["dependencies"] + assert_equal 2, result["dependencies"].size + assert_kind_of Gem::Requirement, result["dependencies"][0] + assert_kind_of Gem::Requirement, result["dependencies"][1] + assert_equal result["dependencies"][0].requirements, result["dependencies"][1].requirements + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_real_gemspec_rubygems_bundler + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: rubygems-bundler + version: !ruby/object:Gem::Version + version: 1.4.5 + platform: ruby + authors: + - Josh Hull + - Michal Papis + autorequire: + bindir: bin + cert_chain: [] + date: 2018-06-24 00:00:00.000000000 Z + dependencies: + - !ruby/object:Gem::Dependency + name: bundler-unload + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 1.0.2 + type: :runtime + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 1.0.2 + description: Stop using bundle exec. + email: + - joshbuddy@gmail.com + - mpapis@gmail.com + executables: [] + extensions: [] + extra_rdoc_files: [] + files: + - ".gem.config" + homepage: http://mpapis.github.com/rubygems-bundler + licenses: + - Apache-2.0 + metadata: {} + post_install_message: + rdoc_options: [] + require_paths: + - lib + required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + rubyforge_project: + rubygems_version: 2.7.6 + signing_key: + specification_version: 4 + summary: Stop using bundle exec + test_files: [] + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "rubygems-bundler", spec.name + assert_equal Gem::Version.new("1.4.5"), spec.version + assert_equal 1, spec.dependencies.size + + dep = spec.dependencies.first + assert_equal "bundler-unload", dep.name + assert_kind_of Gem::Requirement, dep.requirement + assert_kind_of Gem::Requirement, dep.instance_variable_get(:@version_requirements) + assert_equal dep.requirement.requirements, [[">=", Gem::Version.new("1.0.2")]] + + # Empty fields should be nil + assert_nil spec.autorequire + assert_nil spec.post_install_message + + # Metadata should be empty hash + assert_equal({}, spec.metadata) + + # specification_version should be Integer + assert_kind_of Integer, spec.specification_version + assert_equal 4, spec.specification_version + end + + def test_empty_requirements_array + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test + dependencies: + - !ruby/object:Gem::Dependency + name: foo + requirement: !ruby/object:Gem::Requirement + requirements: + type: :runtime + version_requirements: !ruby/object:Gem::Requirement + requirements: + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "test", spec.name + assert_equal 1, spec.dependencies.size + + dep = spec.dependencies.first + assert_equal "foo", dep.name + assert_kind_of Gem::Requirement, dep.requirement + + reqs = dep.requirement.instance_variable_get(:@requirements) + assert_nil reqs + end + + def test_requirements_hash_converted_to_array + # Malformed YAML where requirements is a Hash instead of Array + yaml = <<~YAML + !ruby/object:Gem::Requirement + requirements: + foo: bar + YAML + + req = yaml_load(yaml, permitted_classes: ["Gem::Requirement"]) + assert_kind_of Gem::Requirement, req + + reqs = req.instance_variable_get(:@requirements) + assert_kind_of Hash, reqs + end + + def test_requirement_quote + yaml = <<~YAML + requirements: + - "system: arrow-glib>=25.0.0: amazon_linux: arrow-glib-devel" + - 'system: arrow-glib>=25.0.0: fedora: libarrow-glib-devel' + YAML + + expected = [ + "system: arrow-glib>=25.0.0: amazon_linux: arrow-glib-devel", + "system: arrow-glib>=25.0.0: fedora: libarrow-glib-devel", + ] + assert_equal expected, yaml_load(yaml)["requirements"] + end + + def test_rdoc_options_hash_converted_to_array + # Some gemspecs incorrectly have rdoc_options: {} instead of rdoc_options: [] + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test-gem + version: !ruby/object:Gem::Version + version: 1.0.0 + rdoc_options: {} + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "test-gem", spec.name + + assert_equal [], spec.rdoc_options + end + + def test_load_returns_nil_for_comment_only_yaml + # Bundler config files may contain only comments after deleting all keys + result = yaml_load("---\n# BUNDLE_FOO: \"bar\"\n") + assert_nil result + end + + def test_load_returns_nil_for_empty_document + assert_nil yaml_load("---\n") + assert_nil yaml_load("") + assert_raise(TypeError) { yaml_load(nil) } + end + + def test_load_returns_hash_for_flow_empty_hash + # yaml_dump({}) produces "--- {}\n" + result = yaml_load("--- {}\n") + assert_kind_of Hash, result + assert_empty result + end + + def test_load_parses_flow_empty_hash_as_value + result = yaml_load("metadata: {}\n") + assert_kind_of Hash, result + assert_kind_of Hash, result["metadata"] + assert_empty result["metadata"] + end + + def test_yaml_non_specific_tag_stripped + # Legacy RubyGems (1.x) generated YAML with ! non-specific tags like: + # - ! '>=' + # The ! prefix should be ignored. + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: legacy-gem + version: !ruby/object:Gem::Version + version: 0.1.0 + required_ruby_version: !ruby/object:Gem::Requirement + none: false + requirements: + - - ! '>=' + - !ruby/object:Gem::Version + version: '0' + required_rubygems_version: !ruby/object:Gem::Requirement + none: false + requirements: + - - ! '>=' + - !ruby/object:Gem::Version + version: 1.3.5 + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "legacy-gem", spec.name + assert_equal Gem::Requirement.new(">= 0"), spec.required_ruby_version + assert_equal Gem::Requirement.new(">= 1.3.5"), spec.required_rubygems_version + end + + def test_legacy_gemspec_with_anchors_and_non_specific_tags + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = true + + # Real-world pattern from gems like vegas-0.1.11 that combine + # YAML anchors/aliases with ! non-specific tags + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: legacy-gem + version: !ruby/object:Gem::Version + version: 0.1.11 + dependencies: + - !ruby/object:Gem::Dependency + name: rack + requirement: &id001 !ruby/object:Gem::Requirement + none: false + requirements: + - - ! '>=' + - !ruby/object:Gem::Version + version: 1.0.0 + type: :runtime + prerelease: false + version_requirements: *id001 + - !ruby/object:Gem::Dependency + name: mocha + requirement: &id002 !ruby/object:Gem::Requirement + none: false + requirements: + - - ~> + - !ruby/object:Gem::Version + version: 0.9.8 + type: :development + prerelease: false + version_requirements: *id002 + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "legacy-gem", spec.name + + assert_equal 2, spec.dependencies.size + + rack_dep = spec.dependencies.find {|d| d.name == "rack" } + assert_kind_of Gem::Dependency, rack_dep + assert_equal :runtime, rack_dep.type + assert_equal Gem::Requirement.new(">= 1.0.0"), rack_dep.requirement + + mocha_dep = spec.dependencies.find {|d| d.name == "mocha" } + assert_kind_of Gem::Dependency, mocha_dep + assert_equal :development, mocha_dep.type + assert_equal Gem::Requirement.new("~> 0.9.8"), mocha_dep.requirement + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_non_specific_tag_on_plain_value + # ! tag on a bracketed value like rubyforge_project: ! '[none]' + result = yaml_load("key: ! '[none]'\n") + assert_equal({ "key" => "[none]" }, result) + end + + def test_dump_quotes_dollar_sign_values + # Values starting with $ should be quoted to preserve them as strings + yaml = yaml_dump({ "BUNDLE_FOO" => "$BUILD_DIR", "BUNDLE_BAR" => "baz" }) + assert_include yaml, 'BUNDLE_FOO: "$BUILD_DIR"' + assert_include yaml, "BUNDLE_BAR: baz" + + # Round-trip: ensure the quoted value is parsed back correctly + result = yaml_load(yaml) + assert_equal "$BUILD_DIR", result["BUNDLE_FOO"] + assert_equal "baz", result["BUNDLE_BAR"] + end + + def test_dump_quotes_special_characters + # Various special characters that should trigger quoting + special_values = { + "dollar" => "$HOME", + "exclamation" => "!important", + "ampersand" => "&anchor", + "asterisk" => "*ref", + "colon_prefix" => ":symbol", + "at_sign" => "@mention", + "percent" => "%encoded", + } + + yaml = yaml_dump(special_values) + special_values.each do |key, value| + assert_include yaml, "#{key}: #{value.inspect}", "Value #{value.inspect} for key #{key} should be quoted" + end + + # Round-trip + result = yaml_load(yaml) + special_values.each do |key, value| + assert_equal value, result[key], "Round-trip failed for key #{key}" + end + end + + def test_load_ambiguous_value_with_colon + # "invalid: yaml: hah" is ambiguous YAML - our parser treats it as + # {"invalid" => "yaml: hah"}, but the value looks like a nested mapping. + # config_file.rb's load_file should detect this and reject it. + if Gem.use_psych? + # Psych raises a syntax error for this ambiguous YAML + assert_raise(Psych::SyntaxError) do + yaml_load("invalid: yaml: hah") + end + else + result = yaml_load("invalid: yaml: hah") + assert_kind_of Hash, result + assert_equal "yaml: hah", result["invalid"] + end + end + + def test_nested_anchor_in_array_item + # Ensure aliases are enabled for this test + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = true + + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test-gem + version: !ruby/object:Gem::Version + version: 1.0.0 + dependencies: + - !ruby/object:Gem::Dependency + name: foo + requirement: !ruby/object:Gem::Requirement + requirements: + - &id002 + - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :runtime + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + assert_equal "test-gem", spec.name + + dep = spec.dependencies.first + assert_kind_of Gem::Dependency, dep + + # Requirements should be parsed as nested arrays, not strings + assert_kind_of Array, dep.requirement.requirements + assert_equal 1, dep.requirement.requirements.size + + req_item = dep.requirement.requirements.first + assert_kind_of Array, req_item + assert_equal ">=", req_item[0] + assert_kind_of Gem::Version, req_item[1] + assert_equal "0", req_item[1].version + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_roundtrip_specification + spec = Gem::Specification.new do |s| + s.name = "round-trip-test" + s.version = "2.3.4" + s.platform = "ruby" + s.authors = ["Test Author"] + s.summary = "A test gem for round-trip" + s.description = "Longer description of the test gem" + s.files = ["lib/foo.rb", "README.md"] + s.require_paths = ["lib"] + s.homepage = "https://example.com" + s.licenses = ["MIT"] + s.metadata = { "source_code_uri" => "https://example.com/src" } + s.add_dependency "rake", ">= 1.0" + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_equal "round-trip-test", loaded.name + assert_equal Gem::Version.new("2.3.4"), loaded.version + assert_equal ["Test Author"], loaded.authors + assert_equal "A test gem for round-trip", loaded.summary + assert_equal ["README.md", "lib/foo.rb"], loaded.files + assert_equal ["lib"], loaded.require_paths + assert_equal "https://example.com", loaded.homepage + assert_equal ["MIT"], loaded.licenses + assert_equal({ "source_code_uri" => "https://example.com/src" }, loaded.metadata) + assert_equal 1, loaded.dependencies.size + + dep = loaded.dependencies.first + assert_equal "rake", dep.name + assert_equal :runtime, dep.type + end + + def test_roundtrip_specification_with_extensions + spec = Gem::Specification.new do |s| + s.name = "native-ext-test" + s.version = "1.0.0" + s.authors = ["Test"] + s.summary = "A gem with native extensions" + s.files = ["lib/native.rb", "ext/native/extconf.rb", "ext/native/native.c"] + s.extensions = ["ext/native/extconf.rb"] + s.require_paths = ["lib"] + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_equal ["ext/native/extconf.rb"], loaded.extensions + assert_equal ["ext/native/extconf.rb", "ext/native/native.c", "lib/native.rb"], loaded.files + end + + def test_roundtrip_specification_with_windows_paths + spec = Gem::Specification.new do |s| + s.name = "win-path-test" + s.version = "1.0.0" + s.authors = ["Test"] + s.summary = "A gem with Windows-style paths" + s.files = ["lib/foo.rb", "lib/foo/bar.rb"] + s.require_paths = ["lib"] + s.description = 'Installed in D:\ruby\lib\ruby\gems' + s.post_install_message = "Installed to C:\\Program Files\\Ruby\\lib\\rdoc" + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_equal 'Installed in D:\ruby\lib\ruby\gems', loaded.description + assert_equal "Installed to C:\\Program Files\\Ruby\\lib\\rdoc", loaded.post_install_message + end + + def test_roundtrip_specification_with_metadata + spec = Gem::Specification.new do |s| + s.name = "metadata-test" + s.version = "1.0.0" + s.authors = ["Test"] + s.summary = "A gem with metadata" + s.files = ["lib/foo.rb"] + s.require_paths = ["lib"] + s.metadata = { + "changelog_uri" => "https://example.com/CHANGELOG.md", + "source_code_uri" => "https://github.com/example/metadata-test", + "bug_tracker_uri" => "https://github.com/example/metadata-test/issues", + "allowed_push_host" => "https://rubygems.org", + } + end + + yaml = yaml_dump(spec) + loaded = Gem::SafeYAML.safe_load(yaml) + + assert_kind_of Gem::Specification, loaded + assert_kind_of Hash, loaded.metadata + assert_equal 4, loaded.metadata.size + assert_equal "https://example.com/CHANGELOG.md", loaded.metadata["changelog_uri"] + assert_equal "https://github.com/example/metadata-test", loaded.metadata["source_code_uri"] + assert_equal "https://github.com/example/metadata-test/issues", loaded.metadata["bug_tracker_uri"] + assert_equal "https://rubygems.org", loaded.metadata["allowed_push_host"] + end + + def test_roundtrip_version + ver = Gem::Version.new("1.2.3") + yaml = yaml_dump(ver) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + + assert_kind_of Gem::Version, loaded + assert_equal ver, loaded + end + + def test_roundtrip_platform + plat = Gem::Platform.new("x86_64-linux") + yaml = yaml_dump(plat) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + + assert_kind_of Gem::Platform, loaded + assert_equal plat.cpu, loaded.cpu + assert_equal plat.os, loaded.os + assert_equal plat.version, loaded.version + end + + def test_roundtrip_requirement + req = Gem::Requirement.new(">= 1.0", "< 2.0") + yaml = yaml_dump(req) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + + assert_kind_of Gem::Requirement, loaded + assert_equal req.requirements.sort_by(&:to_s), loaded.requirements.sort_by(&:to_s) + end + + def test_roundtrip_dependency + dep = Gem::Dependency.new("foo", ">= 1.0", :development) + yaml = yaml_dump(dep) + loaded = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + + assert_kind_of Gem::Dependency, loaded + assert_equal "foo", loaded.name + assert_equal :development, loaded.type + assert_equal dep.requirement.requirements, loaded.requirement.requirements + end + + def test_roundtrip_nested_hash + obj = { "a" => { "b" => "c", "d" => [1, 2, 3] } } + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) + + assert_equal obj, loaded + end + + def test_roundtrip_block_scalar + obj = { "text" => "line1\nline2\n" } + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) + + assert_equal "line1\nline2\n", loaded["text"] + end + + def test_roundtrip_special_characters + obj = { + "dollar" => "$HOME", + "exclamation" => "!important", + "ampersand" => "&anchor", + "asterisk" => "*ref", + "colon_prefix" => ":symbol", + "hash_char" => "value#comment", + "brackets" => "[item]", + "braces" => "{key}", + "comma" => "a,b,c", + } + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) + + obj.each do |key, value| + assert_equal value, loaded[key], "Round-trip failed for key #{key}" + end + end + + def test_roundtrip_boolean_nil_integer + obj = { "flag" => true, "count" => 42, "empty" => nil, "off" => false } + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) + + assert_equal true, loaded["flag"] + assert_equal 42, loaded["count"] + assert_nil loaded["empty"] + assert_equal false, loaded["off"] + end + + def test_roundtrip_time + time = Time.utc(2024, 6, 15, 12, 30, 45) + obj = { "created" => time } + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) + + assert_kind_of Time, loaded["created"] + assert_equal time.year, loaded["created"].year + assert_equal time.month, loaded["created"].month + assert_equal time.day, loaded["created"].day + end + + def test_roundtrip_empty_collections + obj = { "arr" => [], "hash" => {} } + yaml = yaml_dump(obj) + loaded = yaml_load(yaml) + + assert_equal [], loaded["arr"] + assert_equal({}, loaded["hash"]) + end + + def test_load_double_quoted_escape_sequences + result = yaml_load("newline: \"hello\\nworld\"") + assert_equal "hello\nworld", result["newline"] + + result = yaml_load("tab: \"col1\\tcol2\"") + assert_equal "col1\tcol2", result["tab"] + + result = yaml_load("cr: \"line\\rend\"") + assert_equal "line\rend", result["cr"] + + result = yaml_load("quote: \"say\\\"hi\\\"\"") + assert_equal "say\"hi\"", result["quote"] + end + + def test_load_double_quoted_backslash_before_escape_chars + # \\r in YAML should become literal backslash + r, not carriage return + result = yaml_load('path: "D:\\\\ruby-mswin\\\\lib"') + assert_equal "D:\\ruby-mswin\\lib", result["path"] + + # \\n should become literal backslash + n, not newline + result = yaml_load('path: "C:\\\\new_folder"') + assert_equal "C:\\new_folder", result["path"] + + # \\t should become literal backslash + t, not tab + result = yaml_load('path: "C:\\\\tmp\\\\test"') + assert_equal "C:\\tmp\\test", result["path"] + + # \\\\ should become two literal backslashes + result = yaml_load('val: "a\\\\\\\\b"') + assert_equal "a\\\\b", result["val"] + end + + def test_load_single_quoted_escape + result = yaml_load("key: 'it''s'") + assert_equal "it's", result["key"] + + result = yaml_load("key: 'no escape \\n here'") + assert_equal "no escape \\n here", result["key"] + end + + def test_load_quoted_numeric_stays_string + result = yaml_load("key: \"42\"") + assert_equal "42", result["key"] + assert_kind_of String, result["key"] + + result = yaml_load("key: '99'") + assert_equal "99", result["key"] + assert_kind_of String, result["key"] + end + + def test_load_empty_string_value + result = yaml_load("key: \"\"") + assert_equal "", result["key"] + end + + def test_load_unquoted_integer + result = yaml_load("key: 42") + assert_equal 42, result["key"] + assert_kind_of Integer, result["key"] + + result = yaml_load("key: -7") + assert_equal(-7, result["key"]) + end + + def test_load_boolean_values + result = yaml_load("a: true\nb: false") + assert_equal true, result["a"] + assert_equal false, result["b"] + end + + def test_load_nil_value + # YAML 1.2: "nil" is not a null value, only ~ and null are + result = yaml_load("key: nil") + assert_equal "nil", result["key"] + + result = yaml_load("key: ~") + assert_nil result["key"] + + result = yaml_load("key: null") + assert_nil result["key"] + end + + def test_load_time_value + result = yaml_load("date: 2024-06-15 12:30:45.000000000 Z") + assert_kind_of Time, result["date"] + assert_equal 2024, result["date"].year + assert_equal 6, result["date"].month + assert_equal 15, result["date"].day + end + + def test_load_block_scalar_keep_trailing_newline + yaml = "text: |\n line1\n line2\n" + result = yaml_load(yaml) + assert_equal "line1\nline2\n", result["text"] + end + + def test_load_block_scalar_strip_trailing_newline + yaml = "text: |-\n no trailing newline\n" + result = yaml_load(yaml) + assert_equal "no trailing newline", result["text"] + refute result["text"].end_with?("\n") + end + + def test_load_flow_array + result = yaml_load("items: [a, b, c]") + assert_equal ["a", "b", "c"], result["items"] + end + + def test_load_flow_empty_array + result = yaml_load("items: []") + assert_equal [], result["items"] + end + + def test_load_mapping_key_with_no_value + result = yaml_load("key:") + assert_kind_of Hash, result + assert_nil result["key"] + end + + def test_load_sequence_item_as_mapping + yaml = "items:\n- name: foo\n ver: 1\n- name: bar\n ver: 2" + result = yaml_load(yaml) + assert_equal [{ "name" => "foo", "ver" => 1 }, { "name" => "bar", "ver" => 2 }], result["items"] + end + + def test_load_nested_sequence + yaml = "matrix:\n- - a\n - b\n- - c\n - d" + result = yaml_load(yaml) + assert_equal [["a", "b"], ["c", "d"]], result["matrix"] + end + + def test_load_comment_stripped_from_value + result = yaml_load("key: value # this is a comment") + assert_equal "value", result["key"] + end + + def test_load_comment_in_quoted_string_preserved + result = yaml_load("key: \"value # not a comment\"") + assert_equal "value # not a comment", result["key"] + + result = yaml_load("key: 'value # not a comment'") + assert_equal "value # not a comment", result["key"] + end + + def test_load_crlf_line_endings + result = yaml_load("key: value\r\nother: data\r\n") + assert_equal "value", result["key"] + assert_equal "data", result["other"] + end + + def test_load_version_requirement_old_tag + yaml = <<~YAML + !ruby/object:Gem::Version::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "1.0" + YAML + + req = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + assert_kind_of Gem::Requirement, req + assert_equal [[">=", Gem::Version.new("1.0")]], req.requirements + end + + def test_load_dependency_version_version_requirement_old_tag + yaml = <<~YAML + - !ruby/object:Gem::Dependency + name: test-unit + type: :development + version_requirement: + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 2.0.2 + version: + YAML + + deps = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + assert_not_nil(deps.first) + + assert_equal [[">=", Gem::Version.new("2.0.2")]], deps.first.requirement.requirements + end + + def test_load_platform_from_value_field + yaml = "!ruby/object:Gem::Platform\nvalue: x86-linux\n" + plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + assert_kind_of Gem::Platform, plat + assert_nil plat.cpu + end + + def test_load_platform_from_cpu_os_version_fields + yaml = "!ruby/object:Gem::Platform\ncpu: x86_64\nos: darwin\nversion: nil\n" + plat = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + assert_kind_of Gem::Platform, plat + assert_equal "x86_64", plat.cpu + assert_equal "darwin", plat.os + end + + def test_load_platform_malicious_sequence + yaml = "!ruby/object:Gem::Platform\n- \"x86-mswin32\\n system('id')#\"\n" + result = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + refute_kind_of Gem::Platform, result + assert_kind_of Array, result + end + + def test_load_dependency_missing_requirement_uses_default + yaml = <<~YAML + !ruby/object:Gem::Dependency + name: foo + type: :runtime + YAML + + dep = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + assert_kind_of Gem::Dependency, dep + assert_equal "foo", dep.name + assert_equal :runtime, dep.type + assert_nil dep.instance_variable_get(:@requirement) + end + + def test_load_dependency_missing_type_defaults_to_runtime + yaml = <<~YAML + !ruby/object:Gem::Dependency + name: bar + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + YAML + + dep = yaml_load(yaml, permitted_classes: Gem::SafeYAML::PERMITTED_CLASSES) + assert_equal :runtime, dep.type + end + + def test_specification_version_non_numeric_string_not_converted + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test + version: !ruby/object:Gem::Version + version: 1.0.0 + specification_version: abc + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Specification, spec + # Non-numeric string should not be converted to Integer + assert_equal "abc", spec.specification_version + end + + def test_unknown_permitted_tag_raises_argument_error + yaml = "!ruby/object:MyCustomClass\nfoo: bar\n" + assert_raise(ArgumentError) do + yaml_load(yaml, permitted_classes: ["MyCustomClass"]) + end + end + + def test_dump_block_scalar_with_trailing_newline + yaml = yaml_dump({ "text" => "line1\nline2\n" }) + assert_include yaml, " |\n" + refute_includes yaml, " |-\n" + end + + def test_dump_block_scalar_without_trailing_newline + yaml = yaml_dump({ "text" => "line1\nline2" }) + assert_include yaml, " |-\n" + end + + def test_dump_nil_value + yaml = yaml_dump({ "key" => nil }) + + loaded = yaml_load(yaml) + assert_nil loaded["key"] + end + + def test_dump_symbol_keys_quoted + yaml = yaml_dump({ foo: "bar" }) + # Symbol keys should use inspect format + assert_include yaml, ":foo:" + + # Symbol values in hash with symbol keys should be quoted + yaml = yaml_dump({ type: ":runtime" }) + assert_include yaml, "\":runtime\"" + end + + def test_regression_flow_empty_hash_as_root + # Previously returned Mapping struct instead of Hash + result = yaml_load("--- {}") + assert_kind_of Hash, result + assert_empty result + end + + def test_regression_alias_check_in_builder_not_parser + # Previously aliases were resolved in Parser, bypassing Builder's policy check. + # The Builder must enforce aliases: false. + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = false + + # Alias in mapping value + assert_raise(Psych::AliasesNotEnabled) do + yaml_load("a: &x val\nb: *x", aliases: false) + end + + # Alias in sequence item + assert_raise(Psych::AliasesNotEnabled) do + yaml_load("items:\n- &x val\n- *x", aliases: false) + end + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_regression_anchored_mapping_stored_for_alias_resolution + # Previously build_mapping didn't call store_anchor, so anchored + # Gem types (Requirement, etc.) couldn't be resolved via aliases. + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = true + + yaml = <<~YAML + a: &req !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + b: *req + YAML + + result = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Gem::Requirement, result["a"] + assert_kind_of Gem::Requirement, result["b"] + assert_equal result["a"].requirements, result["b"].requirements + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_regression_register_anchor_sets_node_anchor + # Previously register_anchor only stored node in @anchors hash but + # didn't set node.anchor, so Builder couldn't track anchored values. + aliases_enabled = Gem::SafeYAML.aliases_enabled? + Gem::SafeYAML.aliases_enabled = true + + yaml = <<~YAML + items: + - &item !ruby/object:Gem::Version + version: '1.0' + - *item + YAML + + result = Gem::SafeYAML.safe_load(yaml) + assert_kind_of Array, result["items"] + assert_equal 2, result["items"].size + assert_kind_of Gem::Version, result["items"][0] + assert_kind_of Gem::Version, result["items"][1] + assert_equal result["items"][0], result["items"][1] + ensure + Gem::SafeYAML.aliases_enabled = aliases_enabled + end + + def test_regression_coerce_empty_hash_not_wrapped_in_scalar + # Previously coerce("{}") returned Mapping but parse_plain_scalar + # wrapped it in Scalar.new(value: Mapping), causing type mismatch. + result = yaml_load("--- {}") + assert_kind_of Hash, result + + result = yaml_load("key: {}") + assert_kind_of Hash, result["key"] + end + + def test_regression_rdoc_options_normalized_to_array + # rdoc_options as Hash (malformed gemspec) + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test + version: !ruby/object:Gem::Version + version: 1.0.0 + rdoc_options: + --title: MyGem + --main: README + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_equal ["--title", "MyGem", "--main", "README"], spec.rdoc_options + end + + def test_regression_requirements_field_normalized_to_array + # The "requirements" field in a Specification (not Requirement) + # should be normalized from Hash to Array if malformed + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: test + version: !ruby/object:Gem::Version + version: 1.0.0 + requirements: + foo: bar + YAML + + spec = Gem::SafeYAML.safe_load(yaml) + assert_equal [["foo", "bar"]], spec.requirements + end + + def test_binary_tag_decoded_in_mapping_key + yaml = <<~YAML + --- + !binary "U0hBMQ==": + metadata.gz: abc123 + YAML + + result = yaml_load(yaml) + assert_equal "SHA1", result.keys.first + assert_equal "abc123", result["SHA1"]["metadata.gz"] + end + + def test_binary_tag_decoded_in_block_scalar_value + yaml = <<~YAML + --- + SHA256: + metadata.gz: !binary |- + OWY4YTM5Y2MxOTc3Mzc5MWYzNzk1NjRmZjVlYzljYjY1MDQwYWIwMg== + YAML + + result = yaml_load(yaml) + assert_equal "9f8a39cc19773791f379564ff5ec9cb65040ab02", result["SHA256"]["metadata.gz"] + end + + def test_binary_tag_decoded_in_inline_value + yaml = <<~YAML + --- + key: !binary "U0hBMQ==" + YAML + + result = yaml_load(yaml) + assert_equal "SHA1", result["key"] + end + + def test_binary_tag_checksums_yaml_roundtrip + # Simulates the checksums.yaml.gz format from older gems + yaml = <<~YAML + --- + !binary "U0hBMQ==": + metadata.gz: !binary |- + OWY4YTM5Y2MxOTc3Mzc5MWYzNzk1NjRmZjVlYzljYjY1MDQwYWIwMg== + data.tar.gz: !binary |- + ZTRmZGRhNjc1MWM5NmIwYzRhODFkYjI0OTlkMjY3ZjQ2MWNkMGM1ZA== + YAML + + result = yaml_load(yaml) + assert_equal ["SHA1"], result.keys + assert_equal "9f8a39cc19773791f379564ff5ec9cb65040ab02", result["SHA1"]["metadata.gz"] + assert_equal "e4fdda6751c96b0c4a81db2499d267f461cd0c5d", result["SHA1"]["data.tar.gz"] + end + + def test_binary_tag_decoded_in_sequence_item_inline + yaml = <<~YAML + --- + - !binary "U0hBMQ==" + YAML + + result = yaml_load(yaml) + assert_equal ["SHA1"], result + end + + def test_version_requirement_tag_always_permitted + yaml = <<~YAML + --- !ruby/object:Gem::Specification + name: escape + version: !ruby/object:Gem::Version + version: 0.0.4 + required_ruby_version: !ruby/object:Gem::Version::Requirement + requirements: + - - ">" + - !ruby/object:Gem::Version + version: 0.0.0 + version: + YAML + + result = yaml_load(yaml) + assert_kind_of Gem::Specification, result + assert_equal "escape", result.name + assert_kind_of Gem::Requirement, result.required_ruby_version + end end diff --git a/test/rubygems/test_gem_security_trust_dir.rb b/test/rubygems/test_gem_security_trust_dir.rb index cfde8e9d48..bd3dfb86c2 100644 --- a/test/rubygems/test_gem_security_trust_dir.rb +++ b/test/rubygems/test_gem_security_trust_dir.rb @@ -56,7 +56,7 @@ class TestGemSecurityTrustDir < Gem::TestCase assert_path_exist trusted - mask = 0o100600 & (~File.umask) + mask = 0o100600 & ~File.umask assert_equal mask, File.stat(trusted).mode unless Gem.win_platform? @@ -70,7 +70,7 @@ class TestGemSecurityTrustDir < Gem::TestCase assert_path_exist @dest_dir - mask = 0o040700 & (~File.umask) + mask = 0o040700 & ~File.umask mask |= 0o200000 if RUBY_PLATFORM.include?("aix") assert_equal mask, File.stat(@dest_dir).mode unless Gem.win_platform? @@ -91,7 +91,7 @@ class TestGemSecurityTrustDir < Gem::TestCase @trust_dir.verify - mask = 0o40700 & (~File.umask) + mask = 0o40700 & ~File.umask mask |= 0o200000 if RUBY_PLATFORM.include?("aix") assert_equal mask, File.stat(@dest_dir).mode unless Gem.win_platform? diff --git a/test/rubygems/test_gem_source_git.rb b/test/rubygems/test_gem_source_git.rb index fef79a0743..b7b2c52f9a 100644 --- a/test/rubygems/test_gem_source_git.rb +++ b/test/rubygems/test_gem_source_git.rb @@ -65,6 +65,8 @@ class TestGemSourceGit < Gem::TestCase end def test_checkout_submodules + omit "JRuby on Windows hits git submodule path differences" if Gem.win_platform? && Gem.java_platform? + # We need to allow to checkout submodules with file:// protocol # CVE-2022-39253 # https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/ diff --git a/test/rubygems/test_gem_source_list.rb b/test/rubygems/test_gem_source_list.rb index 64353f8f90..5327b14db8 100644 --- a/test/rubygems/test_gem_source_list.rb +++ b/test/rubygems/test_gem_source_list.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -require "rubygems" -require "rubygems/source_list" require_relative "helper" +require "rubygems/source_list" class TestGemSourceList < Gem::TestCase def setup @@ -116,4 +115,128 @@ class TestGemSourceList < Gem::TestCase @sl.delete Gem::Source.new(@uri) assert_equal @sl.sources, [] end + + def test_prepend_new_source + uri2 = "http://example2" + source2 = Gem::Source.new(uri2) + + result = @sl.prepend(uri2) + + assert_kind_of Gem::Source, result + assert_kind_of Gem::URI, result.uri + assert_equal uri2, result.uri.to_s + assert_equal [source2, @source], @sl.sources + end + + def test_prepend_existing_source + uri2 = "http://example2" + source2 = Gem::Source.new(uri2) + @sl << uri2 + + assert_equal [@source, source2], @sl.sources + + result = @sl.prepend(uri2) + + assert_kind_of Gem::Source, result + assert_kind_of Gem::URI, result.uri + assert_equal uri2, result.uri.to_s + assert_equal [source2, @source], @sl.sources + end + + def test_prepend_alias_behaves_like_unshift + sl = Gem::SourceList.new + + uri1 = "http://one" + uri2 = "http://two" + + source1 = sl << uri1 + source2 = sl << uri2 + + # move existing to front + result = sl.prepend(uri2) + + assert_kind_of Gem::Source, result + assert_equal [source2, source1], sl.sources + + # and again with the other + result = sl.prepend(uri1) + assert_equal [source1, source2], sl.sources + end + + def test_append_method_new_source + sl = Gem::SourceList.new + + uri1 = "http://example1" + + result = sl.append(uri1) + + assert_kind_of Gem::Source, result + assert_kind_of Gem::URI, result.uri + assert_equal uri1, result.uri.to_s + assert_equal [result], sl.sources + end + + def test_append_method_existing_moves_to_end + sl = Gem::SourceList.new + + uri1 = "http://example1" + uri2 = "http://example2" + + s1 = sl << uri1 + s2 = sl << uri2 + + # list is [s1, s2]; appending s1 should move it to end => [s2, s1] + result = sl.append(uri1) + + assert_equal s1, result + assert_equal [s2, s1], sl.sources + end + + def test_prepend_with_gem_source_object + sl = Gem::SourceList.new + + uri1 = "http://example1" + uri2 = "http://example2" + source1 = Gem::Source.new(uri1) + source2 = Gem::Source.new(uri2) + + # Add first source + sl << source1 + + # Prepend with Gem::Source object + result = sl.prepend(source2) + + assert_equal source2, result + assert_equal [source2, source1], sl.sources + + # Prepend existing source - should move to front + result = sl.prepend(source1) + + assert_equal source1, result + assert_equal [source1, source2], sl.sources + end + + def test_append_with_gem_source_object + sl = Gem::SourceList.new + + uri1 = "http://example1" + uri2 = "http://example2" + source1 = Gem::Source.new(uri1) + source2 = Gem::Source.new(uri2) + + # Add first source + sl << source1 + + # Append with Gem::Source object + result = sl.append(source2) + + assert_equal source2, result + assert_equal [source1, source2], sl.sources + + # Append existing source - should move to end + result = sl.append(source1) + + assert_equal source1, result + assert_equal [source2, source1], sl.sources + end end diff --git a/test/rubygems/test_gem_source_local.rb b/test/rubygems/test_gem_source_local.rb index e9d7f45482..6062173629 100644 --- a/test/rubygems/test_gem_source_local.rb +++ b/test/rubygems/test_gem_source_local.rb @@ -63,6 +63,30 @@ class TestGemSourceLocal < Gem::TestCase assert_equal "a-2.a", @sl.find_gem("a", req, true).full_name end + def test_find_all_gems + _, a2_gem = util_gem "a", "2" + FileUtils.mv a2_gem, @tempdir + + results = @sl.find_all_gems("a") + assert_equal ["a-1", "a-2"], results.map(&:full_name).sort + end + + def test_find_all_gems_excludes_prerelease_by_default + results = @sl.find_all_gems("a") + assert_equal ["a-1"], results.map(&:full_name) + end + + def test_find_all_gems_includes_prerelease_when_requested + results = @sl.find_all_gems("a", Gem::Requirement.create(">= 0"), true) + assert_equal ["a-1", "a-2.a"], results.map(&:full_name).sort + end + + def test_find_all_gems_includes_prerelease_when_requirement_is_prerelease + req = Gem::Requirement.create("= 2.a") + results = @sl.find_all_gems("a", req) + assert_equal ["a-2.a"], results.map(&:full_name) + end + def test_fetch_spec s = @sl.fetch_spec @a.name_tuple assert_equal s, @a diff --git a/test/rubygems/test_gem_specification.rb b/test/rubygems/test_gem_specification.rb index 697a26338c..79be0c996d 100644 --- a/test/rubygems/test_gem_specification.rb +++ b/test/rubygems/test_gem_specification.rb @@ -16,7 +16,7 @@ rubygems_version: "1.0" name: keyedlist version: !ruby/object:Gem::Version version: 0.4.0 -date: 2004-03-28 15:37:49.828000 +02:00 +date: 1980-01-02 00:00:00 UTC platform: summary: A Hash which automatically computes keys. require_paths: @@ -33,7 +33,6 @@ has_rdoc: true Gem::Specification.new do |s| s.name = %q{keyedlist} s.version = %q{0.4.0} - s.has_rdoc = true s.summary = %q{A Hash which automatically computes keys.} s.files = [%q{lib/keyedlist.rb}] s.require_paths = [%q{lib}] @@ -75,7 +74,7 @@ end def assert_date(date) assert_kind_of Time, date assert_equal [0, 0, 0], [date.hour, date.min, date.sec] - assert_operator (Gem::Specification::TODAY..Time.now), :cover?, date + assert_equal Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc, date end def setup @@ -818,7 +817,7 @@ dependencies: [] write_file full_path do |io| io.write @a2.to_ruby_for_cache end - rescue Errno::EINVAL + rescue Errno::EINVAL, Errno::EACCES pend "cannot create '#{full_path}' on this platform" end @@ -837,7 +836,7 @@ dependencies: [] write_file full_path do |io| io.write @a2.to_ruby_for_cache end - rescue Errno::EINVAL + rescue Errno::EINVAL, Errno::EACCES pend "cannot create '#{full_path}' on this platform" end @@ -856,7 +855,7 @@ dependencies: [] write_file full_path do |io| io.write @a2.to_ruby_for_cache end - rescue Errno::EINVAL + rescue Errno::EINVAL, Errno::EACCES pend "cannot create '#{full_path}' on this platform" end @@ -1029,7 +1028,7 @@ dependencies: [] gem = "mingw" v = "1.1.1" - platforms = ["x86-mingw32", "x64-mingw32"] + platforms = ["x86-mingw32", "x64-mingw-ucrt"] # create specs platforms.each do |plat| @@ -1248,12 +1247,37 @@ dependencies: [] end def test_initialize_nil_version - expected = "nil versions are discouraged and will be deprecated in Rubygems 4\n" - actual_stdout, actual_stderr = capture_output do - Gem::Specification.new.version = nil + spec = Gem::Specification.new + spec.name = "test-name" + + assert_nil spec.version + spec.version = nil + assert_nil spec.version + + spec.summary = "test gem" + spec.authors = ["test author"] + e = assert_raise Gem::InvalidSpecificationException do + spec.validate end - assert_empty actual_stdout - assert_equal(expected, actual_stderr) + assert_match("missing value for attribute version", e.message) + end + + def test_set_version_to_nil_after_setting_version + spec = Gem::Specification.new + spec.name = "test-name" + + assert_nil spec.version + spec.version = "1.0.0" + assert_equal "1.0.0", spec.version.to_s + spec.version = nil + assert_nil spec.version + + spec.summary = "test gem" + spec.authors = ["test author"] + e = assert_raise Gem::InvalidSpecificationException do + spec.validate + end + assert_match("missing value for attribute version", e.message) end def test__dump @@ -1554,13 +1578,21 @@ dependencies: [] ext_spec _, err = capture_output do - refute @ext.contains_requirable_file? "nonexistent" + if RUBY_ENGINE == "jruby" + refute @ext.ignored? + else + refute @ext.contains_requirable_file? "nonexistent" + end end - expected = "Ignoring ext-1 because its extensions are not built. " \ - "Try: gem pristine ext --version 1\n" + if RUBY_ENGINE == "jruby" + assert_equal "", err + else + expected = "Ignoring ext-1 because its extensions are not built. " \ + "Try: gem pristine ext --version 1\n" - assert_equal expected, err + assert_equal expected, err + end end def test_contains_requirable_file_eh_extension_java_platform @@ -2216,9 +2248,9 @@ dependencies: [] s1 = util_spec "a", "1" s2 = util_spec "b", "1" - assert_equal(-1, (s1 <=> s2)) - assert_equal(0, (s1 <=> s1)) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands - assert_equal(1, (s2 <=> s1)) + assert_equal(-1, s1 <=> s2) + assert_equal(0, s1 <=> s1) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + assert_equal(1, s2 <=> s1) end def test_spaceship_platform @@ -2227,18 +2259,18 @@ dependencies: [] s.platform = Gem::Platform.new "x86-my_platform1" end - assert_equal(-1, (s1 <=> s2)) - assert_equal(0, (s1 <=> s1)) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands - assert_equal(1, (s2 <=> s1)) + assert_equal(-1, s1 <=> s2) + assert_equal(0, s1 <=> s1) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + assert_equal(1, s2 <=> s1) end def test_spaceship_version s1 = util_spec "a", "1" s2 = util_spec "a", "2" - assert_equal(-1, (s1 <=> s2)) - assert_equal(0, (s1 <=> s1)) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands - assert_equal(1, (s2 <=> s1)) + assert_equal(-1, s1 <=> s2) + assert_equal(0, s1 <=> s1) # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + assert_equal(1, s2 <=> s1) end def test_spec_file @@ -2671,27 +2703,7 @@ end @a1.validate end - expected = <<-EXPECTED -#{w}: prerelease dependency on b (>= 1.0.rc1) is not recommended -#{w}: prerelease dependency on c (>= 2.0.rc2, development) is not recommended -#{w}: open-ended dependency on i (>= 1.2) is not recommended - if i is semantically versioned, use: - add_runtime_dependency "i", "~> 1.2" -#{w}: open-ended dependency on j (>= 1.2.3) is not recommended - if j is semantically versioned, use: - add_runtime_dependency "j", "~> 1.2", ">= 1.2.3" -#{w}: open-ended dependency on k (> 1.2) is not recommended - if k is semantically versioned, use: - add_runtime_dependency "k", "~> 1.2", "> 1.2" -#{w}: open-ended dependency on l (> 1.2.3) is not recommended - if l is semantically versioned, use: - add_runtime_dependency "l", "~> 1.2", "> 1.2.3" -#{w}: open-ended dependency on o (>= 0) is not recommended - use a bounded requirement, such as "~> x.y" -#{w}: See https://guides.rubygems.org/specification-reference/ for help - EXPECTED - - assert_equal expected, @ui.error, "warning" + assert_equal "", @ui.error, "warning" end end @@ -2808,14 +2820,13 @@ duplicate dependency on c (>= 1.2.3, development), (~> 1.2) use: Dir.chdir @tempdir do @a1.add_dependency @a1.name, "1" - use_ui @ui do + e = assert_raise Gem::InvalidSpecificationException do @a1.validate end - assert_equal <<-EXPECTED, @ui.error -#{w}: Self referencing dependency is unnecessary and strongly discouraged. -#{w}: See https://guides.rubygems.org/specification-reference/ for help - EXPECTED + expected = "Dependencies of this gem include a self-reference." + + assert_equal expected, e.message end end @@ -2883,6 +2894,61 @@ duplicate dependency on c (>= 1.2.3, development), (~> 1.2) use: end end + def test_validate_extension_require_relative_warning + util_setup_validate + + Dir.chdir @tempdir do + @a1.extensions = ["ext/a/extconf.rb"] + @a1.files = %w[lib/code.rb lib/a.rb ext/a/extconf.rb] + + File.write File.join("lib", "a.rb"), 'require_relative "a/a"' + + use_ui @ui do + @a1.validate + end + + assert_match(%r{require_relative "a/a"}, @ui.error) + assert_match(/will break in RubyGems 4\.2/, @ui.error) + assert_match(/Use `require` instead of `require_relative`/, @ui.error) + end + end + + def test_validate_extension_require_relative_no_warning_when_rb_exists + util_setup_validate + + Dir.chdir @tempdir do + @a1.extensions = ["ext/a/extconf.rb"] + @a1.files = %w[lib/code.rb lib/a.rb lib/a/a.rb ext/a/extconf.rb] + + FileUtils.mkdir_p File.join("lib", "a") + File.write File.join("lib", "a.rb"), 'require_relative "a/a"' + File.write File.join("lib", "a", "a.rb"), "" + + use_ui @ui do + @a1.validate + end + + refute_match(/require_relative/, @ui.error) + end + end + + def test_validate_extension_require_relative_no_warning_without_extensions + util_setup_validate + + Dir.chdir @tempdir do + @a1.extensions = [] + @a1.files = %w[lib/code.rb lib/a.rb] + + File.write File.join("lib", "a.rb"), 'require_relative "a/a"' + + use_ui @ui do + @a1.validate + end + + refute_match(/require_relative/, @ui.error) + end + end + def test_validate_description util_setup_validate @@ -3009,6 +3075,65 @@ duplicate dependency on c (>= 1.2.3, development), (~> 1.2) use: assert_match "#{w}: bin/exec is missing #! line\n", @ui.error, "error" end + def test_validate_executables_with_space + util_setup_validate + + FileUtils.mkdir_p File.join(@tempdir, "bin") + File.write File.join(@tempdir, "bin", "echo hax"), "#!/usr/bin/env ruby\n" + + @a1.executables = ["echo hax"] + + e = assert_raise Gem::InvalidSpecificationException do + use_ui @ui do + Dir.chdir @tempdir do + @a1.validate + end + end + end + + assert_match "executable \"echo hax\" contains invalid characters", e.message + end + + def test_validate_executables_with_path_separator + util_setup_validate + + FileUtils.mkdir_p File.join(@tempdir, "bin") + File.write File.join(@tempdir, "exe"), "#!/usr/bin/env ruby\n" + + @a1.executables = Gem.win_platform? ? ["..\\exe"] : ["../exe"] + + e = assert_raise Gem::InvalidSpecificationException do + use_ui @ui do + Dir.chdir @tempdir do + @a1.validate + end + end + end + + assert_match "executable \"#{Gem.win_platform? ? "..\\exe" : "../exe"}\" contains invalid characters", e.message + end + + def test_validate_executables_with_path_list_separator + sep = Gem.win_platform? ? ";" : ":" + + util_setup_validate + + FileUtils.mkdir_p File.join(@tempdir, "bin") + File.write File.join(@tempdir, "bin", "foo#{sep}bar"), "#!/usr/bin/env ruby\n" + + @a1.executables = ["foo#{sep}bar"] + + e = assert_raise Gem::InvalidSpecificationException do + use_ui @ui do + Dir.chdir @tempdir do + @a1.validate + end + end + end + + assert_match "executable \"foo#{sep}bar\" contains invalid characters", e.message + end + def test_validate_empty_require_paths util_setup_validate @@ -3664,8 +3789,6 @@ Did you mean 'Ruby'? end def test__load_fixes_Date_objects - pend "Marshal.load of links and floats is broken on truffleruby, see https://github.com/oracle/truffleruby/issues/3747" if RUBY_ENGINE == "truffleruby" - spec = util_spec "a", 1 spec.instance_variable_set :@date, Date.today @@ -3892,7 +4015,11 @@ end def test_missing_extensions_eh ext_spec - assert @ext.missing_extensions? + if RUBY_ENGINE == "jruby" + refute @ext.missing_extensions? + else + assert @ext.missing_extensions? + end extconf_rb = File.join @ext.gem_dir, @ext.extensions.first FileUtils.mkdir_p File.dirname extconf_rb diff --git a/test/rubygems/test_gem_stub_specification.rb b/test/rubygems/test_gem_stub_specification.rb index 4b2d4c570a..6c07480c7f 100644 --- a/test/rubygems/test_gem_stub_specification.rb +++ b/test/rubygems/test_gem_stub_specification.rb @@ -68,13 +68,21 @@ class TestStubSpecification < Gem::TestCase def test_contains_requirable_file_eh_extension stub_with_extension do |stub| _, err = capture_output do - refute stub.contains_requirable_file? "nonexistent" + if RUBY_ENGINE == "jruby" + refute stub.ignored? + else + refute stub.contains_requirable_file? "nonexistent" + end end - expected = "Ignoring stub_e-2 because its extensions are not built. " \ - "Try: gem pristine stub_e --version 2\n" + if RUBY_ENGINE == "jruby" + assert_equal "", err + else + expected = "Ignoring stub_e-2 because its extensions are not built. " \ + "Try: gem pristine stub_e --version 2\n" - assert_equal expected, err + assert_equal expected, err + end end end @@ -137,7 +145,11 @@ class TestStubSpecification < Gem::TestCase end end - assert stub.missing_extensions? + if RUBY_ENGINE == "jruby" + refute stub.missing_extensions? + else + assert stub.missing_extensions? + end stub.build_extensions @@ -209,7 +221,7 @@ class TestStubSpecification < Gem::TestCase end def stub_with_version - spec = File.join @gemhome, "specifications", "stub_e-2.gemspec" + spec = File.join @gemhome, "specifications", "stub_v-with-version.gemspec" File.open spec, "w" do |io| io.write <<~STUB # -*- encoding: utf-8 -*- @@ -232,7 +244,7 @@ class TestStubSpecification < Gem::TestCase end def stub_without_version - spec = File.join @gemhome, "specifications", "stub-2.gemspec" + spec = File.join @gemhome, "specifications", "stub_v-without-version.gemspec" File.open spec, "w" do |io| io.write <<~STUB # -*- encoding: utf-8 -*- diff --git a/test/rubygems/test_gem_text.rb b/test/rubygems/test_gem_text.rb index 8e99610946..60739e6131 100644 --- a/test/rubygems/test_gem_text.rb +++ b/test/rubygems/test_gem_text.rb @@ -100,4 +100,21 @@ Without the wrapping, the text might not look good in the RSS feed. def test_clean_text assert_equal ".]2;nyan.", clean_text("\e]2;nyan\a") end + + def test_clean_text_strips_c1_control_characters + text = [0x41, 0x9b, 0x42].pack("U*") # "A", CSI (U+009B), "B" + assert_equal "A.B", clean_text(text) + end + + def test_clean_text_preserves_multibyte_characters + # U+0400 encodes to bytes D0 80, whose 0x80 continuation byte must not be + # mistaken for a C1 control byte. NEL (U+0085) is stripped. + text = [0x400, 0x85].pack("U*") + assert_equal [0x400, 0x2e].pack("U*"), clean_text(text) + end + + def test_clean_text_passes_through_non_unicode_encodings + text = "x\x9by".dup.force_encoding("ISO-8859-1") + assert_equal text, clean_text(text) + end end diff --git a/test/rubygems/test_gem_uri.rb b/test/rubygems/test_gem_uri.rb index 1253ebc6de..ce633c99b6 100644 --- a/test/rubygems/test_gem_uri.rb +++ b/test/rubygems/test_gem_uri.rb @@ -21,7 +21,7 @@ class TestUri < Gem::TestCase end def test_redacted_with_user_x_oauth_basic - assert_equal "https://REDACTED:x-oauth-basic@example.com", Gem::Uri.new("https://token:x-oauth-basic@example.com").redacted.to_s + assert_equal "https://REDACTED@example.com", Gem::Uri.new("https://token:x-oauth-basic@example.com").redacted.to_s end def test_redacted_without_credential diff --git a/test/rubygems/test_gem_util.rb b/test/rubygems/test_gem_util.rb index 608210a903..9688d066db 100644 --- a/test/rubygems/test_gem_util.rb +++ b/test/rubygems/test_gem_util.rb @@ -13,17 +13,6 @@ class TestGemUtil < Gem::TestCase end end - def test_silent_system - pend if Gem.java_platform? - Gem::Deprecate.skip_during do - out, err = capture_output do - Gem::Util.silent_system(*ruby_with_rubygems_in_load_path, "-e", 'puts "hello"; warn "hello"') - end - assert_empty out - assert_empty err - end - end - def test_traverse_parents FileUtils.mkdir_p "a/b/c" diff --git a/test/rubygems/test_gem_util_atomic_file_writer.rb b/test/rubygems/test_gem_util_atomic_file_writer.rb new file mode 100644 index 0000000000..e011a38ad4 --- /dev/null +++ b/test/rubygems/test_gem_util_atomic_file_writer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative "helper" +require "rubygems/util/atomic_file_writer" + +class TestGemUtilAtomicFileWriter < Gem::TestCase + def test_external_encoding + Gem::AtomicFileWriter.open(File.join(@tempdir, "test.txt")) do |file| + assert_equal(Encoding::ASCII_8BIT, file.external_encoding) + end + end +end diff --git a/test/rubygems/test_gem_version.rb b/test/rubygems/test_gem_version.rb index cf771bc5a1..f58359e54c 100644 --- a/test/rubygems/test_gem_version.rb +++ b/test/rubygems/test_gem_version.rb @@ -7,6 +7,11 @@ class TestGemVersion < Gem::TestCase class V < ::Gem::Version end + def test_nil_is_zero + zero = Gem::Version.create nil + assert_equal Gem::Version.create(0), zero + end + def test_bump assert_bumped_version_equal "5.3", "5.2.4" end @@ -35,13 +40,6 @@ class TestGemVersion < Gem::TestCase assert_same real, Gem::Version.create(real) - expected = "nil versions are discouraged and will be deprecated in Rubygems 4\n" - actual_stdout, actual_stderr = capture_output do - assert_nil Gem::Version.create(nil) - end - assert_empty actual_stdout - assert_equal(expected, actual_stderr) - assert_equal v("5.1"), Gem::Version.create("5.1") ver = "1.1" @@ -51,13 +49,7 @@ class TestGemVersion < Gem::TestCase def test_class_correct assert_equal true, Gem::Version.correct?("5.1") assert_equal false, Gem::Version.correct?("an incorrect version") - - expected = "nil versions are discouraged and will be deprecated in Rubygems 4\n" - actual_stdout, actual_stderr = capture_output do - Gem::Version.correct?(nil) - end - assert_empty actual_stdout - assert_equal(expected, actual_stderr) + assert_equal true, Gem::Version.correct?(nil) end def test_class_new_subclass @@ -162,33 +154,36 @@ class TestGemVersion < Gem::TestCase assert_equal(-1, v("5.a") <=> v("5.0.0.rc2")) assert_equal(1, v("5.x") <=> v("5.0.0.rc2")) - assert_equal(0, v("1.9.3") <=> "1.9.3") - assert_equal(1, v("1.9.3") <=> "1.9.2.99") - assert_equal(-1, v("1.9.3") <=> "1.9.3.1") - - assert_nil v("1.0") <=> "whatever" + [ + [0, "1.9.3"], + [1, "1.9.2.99"], + [-1, "1.9.3.1"], + [nil, "whatever"], + ].each do |cmp, string_ver| + assert_equal(cmp, v("1.9.3") <=> string_ver) + end end def test_approximate_recommendation - assert_approximate_equal "~> 1.0", "1" + assert_approximate_equal ">= 1.0", "1" assert_approximate_satisfies_itself "1" - assert_approximate_equal "~> 1.0", "1.0" + assert_approximate_equal ">= 1.0", "1.0" assert_approximate_satisfies_itself "1.0" - assert_approximate_equal "~> 1.2", "1.2" + assert_approximate_equal ">= 1.2", "1.2" assert_approximate_satisfies_itself "1.2" - assert_approximate_equal "~> 1.2", "1.2.0" + assert_approximate_equal ">= 1.2", "1.2.0" assert_approximate_satisfies_itself "1.2.0" - assert_approximate_equal "~> 1.2", "1.2.3" + assert_approximate_equal ">= 1.2", "1.2.3" assert_approximate_satisfies_itself "1.2.3" - assert_approximate_equal "~> 1.2.a", "1.2.3.a.4" + assert_approximate_equal ">= 1.2.a", "1.2.3.a.4" assert_approximate_satisfies_itself "1.2.3.a.4" - assert_approximate_equal "~> 1.9.a", "1.9.0.dev" + assert_approximate_equal ">= 1.9.a", "1.9.0.dev" assert_approximate_satisfies_itself "1.9.0.dev" end @@ -205,6 +200,51 @@ class TestGemVersion < Gem::TestCase assert_less_than "1.0.0-1", "1" end + def test_sort_key_is_computed_on_regular_release + refute_nil v("9.8.7").send(:sort_key) + end + + def test_sort_key_is_computed_on_security_release + refute_nil v("9.8.7.1").send(:sort_key) + end + + def test_sort_key_is_not_computed_on_prerelease + assert_nil v("9.8.7.pre1").send(:sort_key) + end + + def test_sort_key_is_not_computed_on_version_with_more_segments + assert_nil v("1.1.1.1.1.1.1").send(:sort_key) + end + + def test_sort_key_is_not_computed_on_huge_numbers + assert_nil v("2.30.1.250000").send(:sort_key) + end + + def test_sort_key_on_timestamped_version + a = v("1.0.0") + b = v("0.0.1.20220404083012") + + assert_operator a, :>, b + end + + def test_sort_key_when_segment_is_higher_than_radix + a = v("0.7.0") + b = v("0.6.63000") + + assert_operator(a, :>, b) + end + + def test_sort_key_is_used_for_comparison + a = v("18.0.1") + b = v("18.0.2") + + # Ensure the slow path isn't getting hit + a.instance_variable_set(:@version, nil) + a.instance_variable_set(:@canonical_segments, nil) + + assert_operator(a, :<, b) + end + # modifying the segments of a version should not affect the segments of the cached version object def test_segments v("9.8.7").segments[2] += 1 diff --git a/test/rubygems/test_project_sanity.rb b/test/rubygems/test_project_sanity.rb index 8f23b2d8c0..3b08d1ec7b 100644 --- a/test/rubygems/test_project_sanity.rb +++ b/test/rubygems/test_project_sanity.rb @@ -12,6 +12,7 @@ class TestGemProjectSanity < Gem::TestCase def test_manifest_is_up_to_date pend unless File.exist?("#{root}/Rakefile") + omit "JRuby on Windows cannot exec the bin/rake shebang" if Gem.win_platform? && Gem.java_platform? rake = "#{root}/bin/rake" _, status = Open3.capture2e(rake, "check_manifest") @@ -37,6 +38,8 @@ class TestGemProjectSanity < Gem::TestCase end def test_require_rubygems_package + omit "JRuby on Windows fails to spawn ruby --disable-gems here" if Gem.win_platform? && Gem.java_platform? + err, status = Open3.capture2e(*ruby_with_rubygems_in_load_path, "--disable-gems", "-e", "require \"rubygems/package\"") assert status.success?, err diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index f63c23c315..db86a30905 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -431,6 +431,22 @@ class TestGemRequire < Gem::TestCase assert_equal %w[default-2.0.0.0], loaded_spec_names end + def test_multiple_gems_with_the_same_path_the_non_activated_spec_is_chosen + a1 = util_spec "a", "1", nil, "lib/ib.rb" + a2 = util_spec "a", "2", nil, "lib/foo.rb" + b1 = util_spec "b", "1", nil, "lib/ib.rb" + + install_specs a1, a2, b1 + + a2.activate + + assert_equal %w[a-2], loaded_spec_names + assert_empty unresolved_names + + assert_require "ib" + assert_equal %w[a-2 b-1], loaded_spec_names + end + def test_default_gem_require_activates_just_once default_gem_spec = new_default_spec("default", "2.0.0.0", nil, "default/gem.rb") @@ -460,6 +476,7 @@ class TestGemRequire < Gem::TestCase def test_realworld_default_gem omit "this test can't work under ruby-core setup" if ruby_repo? + omit "JRuby on Windows does not register json as a default gem the same way" if Gem.win_platform? && Gem.java_platform? cmd = <<-RUBY $stderr = $stdout @@ -770,6 +787,8 @@ class TestGemRequire < Gem::TestCase end def test_require_does_not_crash_when_utilizing_bundler_version_finder + omit "JRuby on Windows hits a different require path" if Gem.win_platform? && Gem.java_platform? + a1 = util_spec "a", "1.1", { "bundler" => ">= 0" } a2 = util_spec "a", "1.2", { "bundler" => ">= 0" } b1 = util_spec "bundler", "2.3.7" diff --git a/test/rubygems/test_rubygems.rb b/test/rubygems/test_rubygems.rb index ec195b65cd..6566b5981e 100644 --- a/test/rubygems/test_rubygems.rb +++ b/test/rubygems/test_rubygems.rb @@ -10,6 +10,7 @@ class GemTest < Gem::TestCase def test_operating_system_other_exceptions pend "does not apply to truffleruby" if RUBY_ENGINE == "truffleruby" + omit "JRuby on Windows loads a different operating_system defaults file" if Gem.win_platform? && Gem.java_platform? path = util_install_operating_system_rb <<-RUBY intentionally_not_implemented_method diff --git a/test/rubygems/test_webauthn_listener.rb b/test/rubygems/test_webauthn_listener.rb index 08edabceb2..ded4128928 100644 --- a/test/rubygems/test_webauthn_listener.rb +++ b/test/rubygems/test_webauthn_listener.rb @@ -17,7 +17,7 @@ class WebauthnListenerTest < Gem::TestCase super end - def test_listener_thread_retreives_otp_code + def test_listener_thread_retrieves_otp_code thread = Gem::GemcutterUtilities::WebauthnListener.listener_thread(Gem.host, @server) Gem::MockBrowser.get Gem::URI("http://localhost:#{@port}?code=xyz") |
