summaryrefslogtreecommitdiff
path: root/spec/ruby/core/array/pack/c_spec.rb
diff options
context:
space:
mode:
Diffstat (limited to 'spec/ruby/core/array/pack/c_spec.rb')
-rw-r--r--spec/ruby/core/array/pack/c_spec.rb77
1 files changed, 77 insertions, 0 deletions
diff --git a/spec/ruby/core/array/pack/c_spec.rb b/spec/ruby/core/array/pack/c_spec.rb
new file mode 100644
index 0000000000..de06207a23
--- /dev/null
+++ b/spec/ruby/core/array/pack/c_spec.rb
@@ -0,0 +1,77 @@
+# encoding: binary
+
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+
+describe :array_pack_8bit, shared: true do
+ it "encodes the least significant eight bits of a positive number" do
+ [ [[49], "1"],
+ [[0b11111111], "\xFF"],
+ [[0b100000000], "\x00"],
+ [[0b100000001], "\x01"]
+ ].should be_computed_by(:pack, pack_format)
+ end
+
+ it "encodes the least significant eight bits of a negative number" do
+ [ [[-1], "\xFF"],
+ [[-0b10000000], "\x80"],
+ [[-0b11111111], "\x01"],
+ [[-0b100000000], "\x00"],
+ [[-0b100000001], "\xFF"]
+ ].should be_computed_by(:pack, pack_format)
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[5.2], "\x05"],
+ [[5.8], "\x05"]
+ ].should be_computed_by(:pack, pack_format)
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(5)
+ [obj].pack(pack_format).should == "\x05"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [ [[1, 2, 3], pack_format(3), "\x01\x02\x03"],
+ [[1, 2, 3], pack_format(2) + pack_format(1), "\x01\x02\x03"]
+ ].should be_computed_by(:pack)
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [1, 2, 3, 4, 5].pack(pack_format('*')).should == "\x01\x02\x03\x04\x05"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [1, 2, 3].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [1, 2, 3].pack(pack_format(' ', 2)).should == "\x01\x02"
+ end
+end
+
+describe "Array#pack with format 'C'" do
+ it_behaves_like :array_pack_basic, 'C'
+ it_behaves_like :array_pack_basic_non_float, 'C'
+ it_behaves_like :array_pack_8bit, 'C'
+ it_behaves_like :array_pack_arguments, 'C'
+ it_behaves_like :array_pack_numeric_basic, 'C'
+ it_behaves_like :array_pack_integer, 'C'
+ it_behaves_like :array_pack_no_platform, 'C'
+end
+
+describe "Array#pack with format 'c'" do
+ it_behaves_like :array_pack_basic, 'c'
+ it_behaves_like :array_pack_basic_non_float, 'c'
+ it_behaves_like :array_pack_8bit, 'c'
+ it_behaves_like :array_pack_arguments, 'c'
+ it_behaves_like :array_pack_numeric_basic, 'c'
+ it_behaves_like :array_pack_integer, 'c'
+ it_behaves_like :array_pack_no_platform, 'c'
+end
='graph'>
-rw-r--r--spec/bundler/bundler/dependency_spec.rb49
-rw-r--r--spec/bundler/bundler/digest_spec.rb24
-rw-r--r--spec/bundler/bundler/dsl_spec.rb558
-rw-r--r--spec/bundler/bundler/endpoint_specification_spec.rb123
-rw-r--r--spec/bundler/bundler/env_spec.rb237
-rw-r--r--spec/bundler/bundler/environment_preserver_spec.rb87
-rw-r--r--spec/bundler/bundler/errors_spec.rb91
-rw-r--r--spec/bundler/bundler/fetcher/base_spec.rb77
-rw-r--r--spec/bundler/bundler/fetcher/compact_index_spec.rb109
-rw-r--r--spec/bundler/bundler/fetcher/dependency_spec.rb296
-rw-r--r--spec/bundler/bundler/fetcher/downloader_spec.rb302
-rw-r--r--spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb60
-rw-r--r--spec/bundler/bundler/fetcher/index_spec.rb75
-rw-r--r--spec/bundler/bundler/fetcher_spec.rb260
-rw-r--r--spec/bundler/bundler/friendly_errors_spec.rb231
-rw-r--r--spec/bundler/bundler/gem_helper_spec.rb456
-rw-r--r--spec/bundler/bundler/gem_version_promoter_spec.rb163
-rw-r--r--spec/bundler/bundler/index_spec.rb36
-rw-r--r--spec/bundler/bundler/installer/gem_installer_spec.rb47
-rw-r--r--spec/bundler/bundler/installer/parallel_installer_spec.rb79
-rw-r--r--spec/bundler/bundler/installer/spec_installation_spec.rb89
-rw-r--r--spec/bundler/bundler/lockfile_parser_spec.rb303
-rw-r--r--spec/bundler/bundler/mirror_spec.rb331
-rw-r--r--spec/bundler/bundler/override_spec.rb175
-rw-r--r--spec/bundler/bundler/plugin/api/source_spec.rb88
-rw-r--r--spec/bundler/bundler/plugin/api_spec.rb83
-rw-r--r--spec/bundler/bundler/plugin/dsl_spec.rb38
-rw-r--r--spec/bundler/bundler/plugin/events_spec.rb32
-rw-r--r--spec/bundler/bundler/plugin/index_spec.rb275
-rw-r--r--spec/bundler/bundler/plugin/installer_spec.rb137
-rw-r--r--spec/bundler/bundler/plugin/source_list_spec.rb25
-rw-r--r--spec/bundler/bundler/plugin_spec.rb368
-rw-r--r--spec/bundler/bundler/remote_specification_spec.rb187
-rw-r--r--spec/bundler/bundler/resolver/candidate_spec.rb20
-rw-r--r--spec/bundler/bundler/resolver/cooldown_spec.rb148
-rw-r--r--spec/bundler/bundler/retry_spec.rb190
-rw-r--r--spec/bundler/bundler/ruby_dsl_spec.rb248
-rw-r--r--spec/bundler/bundler/ruby_version_spec.rb500
-rw-r--r--spec/bundler/bundler/rubygems_ext_spec.rb39
-rw-r--r--spec/bundler/bundler/rubygems_integration_spec.rb126
-rw-r--r--spec/bundler/bundler/settings/validator_spec.rb111
-rw-r--r--spec/bundler/bundler/settings_spec.rb380
-rw-r--r--spec/bundler/bundler/shared_helpers_spec.rb518
-rw-r--r--spec/bundler/bundler/source/git/git_proxy_spec.rb354
-rw-r--r--spec/bundler/bundler/source/git_spec.rb123
-rw-r--r--spec/bundler/bundler/source/path_spec.rb31
-rw-r--r--spec/bundler/bundler/source/rubygems/remote_spec.rb207
-rw-r--r--spec/bundler/bundler/source/rubygems_spec.rb104
-rw-r--r--spec/bundler/bundler/source_list_spec.rb475
-rw-r--r--spec/bundler/bundler/source_spec.rb174
-rw-r--r--spec/bundler/bundler/spec_set_spec.rb148
-rw-r--r--spec/bundler/bundler/specifications/foo.gemspec13
-rw-r--r--spec/bundler/bundler/stub_specification_spec.rb82
-rw-r--r--spec/bundler/bundler/ui/shell_spec.rb112
-rw-r--r--spec/bundler/bundler/ui_spec.rb41
-rw-r--r--spec/bundler/bundler/uri_credentials_filter_spec.rb137
-rw-r--r--spec/bundler/bundler/uri_normalizer_spec.rb25
-rw-r--r--spec/bundler/bundler/worker_spec.rb89
-rw-r--r--spec/bundler/bundler/yaml_serializer_spec.rb235
-rw-r--r--spec/bundler/cache/cache_path_spec.rb32
-rw-r--r--spec/bundler/cache/gems_spec.rb427
-rw-r--r--spec/bundler/cache/git_spec.rb511
-rw-r--r--spec/bundler/cache/path_spec.rb121
-rw-r--r--spec/bundler/cache/platform_spec.rb49
-rw-r--r--spec/bundler/commands/add_spec.rb448
-rw-r--r--spec/bundler/commands/binstubs_spec.rb333
-rw-r--r--spec/bundler/commands/cache_spec.rb620
-rw-r--r--spec/bundler/commands/check_spec.rb601
-rw-r--r--spec/bundler/commands/clean_spec.rb936
-rw-r--r--spec/bundler/commands/config_spec.rb646
-rw-r--r--spec/bundler/commands/console_spec.rb214
-rw-r--r--spec/bundler/commands/doctor_spec.rb185
-rw-r--r--spec/bundler/commands/exec_spec.rb1272
-rw-r--r--spec/bundler/commands/fund_spec.rb118
-rw-r--r--spec/bundler/commands/help_spec.rb92
-rw-r--r--spec/bundler/commands/info_spec.rb249
-rw-r--r--spec/bundler/commands/init_spec.rb207
-rw-r--r--spec/bundler/commands/install_spec.rb2115
-rw-r--r--spec/bundler/commands/issue_spec.rb16
-rw-r--r--spec/bundler/commands/licenses_spec.rb37
-rw-r--r--spec/bundler/commands/list_spec.rb315
-rw-r--r--spec/bundler/commands/lock_spec.rb2877
-rw-r--r--spec/bundler/commands/newgem_spec.rb2139
-rw-r--r--spec/bundler/commands/open_spec.rb175
-rw-r--r--spec/bundler/commands/outdated_spec.rb1369
-rw-r--r--spec/bundler/commands/platform_spec.rb1260
-rw-r--r--spec/bundler/commands/post_bundle_message_spec.rb183
-rw-r--r--spec/bundler/commands/pristine_spec.rb275
-rw-r--r--spec/bundler/commands/remove_spec.rb736
-rw-r--r--spec/bundler/commands/show_spec.rb217
-rw-r--r--spec/bundler/commands/ssl_spec.rb373
-rw-r--r--spec/bundler/commands/update_spec.rb2095
-rw-r--r--spec/bundler/commands/version_spec.rb65
-rw-r--r--spec/bundler/install/allow_offline_install_spec.rb95
-rw-r--r--spec/bundler/install/binstubs_spec.rb49
-rw-r--r--spec/bundler/install/bundler_spec.rb268
-rw-r--r--spec/bundler/install/cooldown_spec.rb272
-rw-r--r--spec/bundler/install/deploy_spec.rb493
-rw-r--r--spec/bundler/install/failure_spec.rb85
-rw-r--r--spec/bundler/install/force_spec.rb71
-rw-r--r--spec/bundler/install/gemfile/eval_gemfile_spec.rb122
-rw-r--r--spec/bundler/install/gemfile/force_ruby_platform_spec.rb136
-rw-r--r--spec/bundler/install/gemfile/gemspec_spec.rb737
-rw-r--r--spec/bundler/install/gemfile/git_spec.rb1745
-rw-r--r--spec/bundler/install/gemfile/groups_spec.rb345
-rw-r--r--spec/bundler/install/gemfile/install_if_spec.rb51
-rw-r--r--spec/bundler/install/gemfile/lockfile_spec.rb42
-rw-r--r--spec/bundler/install/gemfile/override_spec.rb401
-rw-r--r--spec/bundler/install/gemfile/path_spec.rb1017
-rw-r--r--spec/bundler/install/gemfile/platform_spec.rb638
-rw-r--r--spec/bundler/install/gemfile/ruby_spec.rb157
-rw-r--r--spec/bundler/install/gemfile/sources_spec.rb1313
-rw-r--r--spec/bundler/install/gemfile/specific_platform_spec.rb1973
-rw-r--r--spec/bundler/install/gemfile_spec.rb192
-rw-r--r--spec/bundler/install/gems/compact_index_spec.rb1012
-rw-r--r--spec/bundler/install/gems/dependency_api_fallback_spec.rb19
-rw-r--r--spec/bundler/install/gems/dependency_api_spec.rb656
-rw-r--r--spec/bundler/install/gems/env_spec.rb107
-rw-r--r--spec/bundler/install/gems/flex_spec.rb403
-rw-r--r--spec/bundler/install/gems/fund_spec.rb164
-rw-r--r--spec/bundler/install/gems/gemfile_source_header_spec.rb24
-rw-r--r--spec/bundler/install/gems/mirror_probe_spec.rb68
-rw-r--r--spec/bundler/install/gems/mirror_spec.rb39
-rw-r--r--spec/bundler/install/gems/native_extensions_spec.rb184
-rw-r--r--spec/bundler/install/gems/no_build_extension_spec.rb54
-rw-r--r--spec/bundler/install/gems/no_install_plugin_spec.rb53
-rw-r--r--spec/bundler/install/gems/post_install_spec.rb150
-rw-r--r--spec/bundler/install/gems/resolving_spec.rb786
-rw-r--r--spec/bundler/install/gems/standalone_spec.rb524
-rw-r--r--spec/bundler/install/gems/win32_spec.rb25
-rw-r--r--spec/bundler/install/gemspecs_spec.rb179
-rw-r--r--spec/bundler/install/git_spec.rb369
-rw-r--r--spec/bundler/install/global_cache_spec.rb305
-rw-r--r--spec/bundler/install/path_spec.rb214
-rw-r--r--spec/bundler/install/prereleases_spec.rb54
-rw-r--r--spec/bundler/install/process_lock_spec.rb113
-rw-r--r--spec/bundler/install/security_policy_spec.rb72
-rw-r--r--spec/bundler/install/yanked_spec.rb254
-rw-r--r--spec/bundler/lock/git_spec.rb258
-rw-r--r--spec/bundler/lock/lockfile_spec.rb2416
-rw-r--r--spec/bundler/other/cli_dispatch_spec.rb20
-rw-r--r--spec/bundler/other/cli_man_pages_spec.rb100
-rw-r--r--spec/bundler/other/ext_spec.rb50
-rw-r--r--spec/bundler/other/major_deprecation_spec.rb798
-rw-r--r--spec/bundler/plugins/command_spec.rb112
-rw-r--r--spec/bundler/plugins/hook_spec.rb331
-rw-r--r--spec/bundler/plugins/install_spec.rb466
-rw-r--r--spec/bundler/plugins/list_spec.rb60
-rw-r--r--spec/bundler/plugins/source/example_spec.rb456
-rw-r--r--spec/bundler/plugins/source_spec.rb111
-rw-r--r--spec/bundler/plugins/uninstall_spec.rb74
-rw-r--r--spec/bundler/quality_es_spec.rb61
-rw-r--r--spec/bundler/quality_spec.rb261
-rw-r--r--spec/bundler/realworld/double_check_spec.rb40
-rw-r--r--spec/bundler/realworld/edgecases_spec.rb75
-rw-r--r--spec/bundler/realworld/ffi_spec.rb57
-rw-r--r--spec/bundler/realworld/fixtures/tapioca/Gemfile5
-rw-r--r--spec/bundler/realworld/fixtures/tapioca/Gemfile.lock49
-rw-r--r--spec/bundler/realworld/fixtures/warbler/.gitignore1
-rw-r--r--spec/bundler/realworld/fixtures/warbler/Gemfile7
-rw-r--r--spec/bundler/realworld/fixtures/warbler/Gemfile.lock35
-rw-r--r--spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb3
-rw-r--r--spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec10
-rw-r--r--spec/bundler/realworld/git_spec.rb11
-rw-r--r--spec/bundler/realworld/parallel_spec.rb66
-rw-r--r--spec/bundler/realworld/slow_perf_spec.rb35
-rw-r--r--spec/bundler/resolver/basic_spec.rb422
-rw-r--r--spec/bundler/resolver/platform_spec.rb423
-rw-r--r--spec/bundler/runtime/env_helpers_spec.rb236
-rw-r--r--spec/bundler/runtime/executable_spec.rb141
-rw-r--r--spec/bundler/runtime/gem_tasks_spec.rb158
-rw-r--r--spec/bundler/runtime/inline_spec.rb751
-rw-r--r--spec/bundler/runtime/load_spec.rb113
-rw-r--r--spec/bundler/runtime/platform_spec.rb468
-rw-r--r--spec/bundler/runtime/require_spec.rb480
-rw-r--r--spec/bundler/runtime/requiring_spec.rb15
-rw-r--r--spec/bundler/runtime/self_management_spec.rb279
-rw-r--r--spec/bundler/runtime/setup_spec.rb1710
-rw-r--r--spec/bundler/spec_helper.rb192
-rw-r--r--spec/bundler/support/activate.rb9
-rw-r--r--spec/bundler/support/artifice/compact_index.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_api_missing.rb13
-rw-r--r--spec/bundler/support/artifice/compact_index_basic_authentication.rb15
-rw-r--r--spec/bundler/support/artifice/compact_index_checksum_mismatch.rb16
-rw-r--r--spec/bundler/support/artifice/compact_index_concurrent_download.rb33
-rw-r--r--spec/bundler/support/artifice/compact_index_cooldown.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_creds_diff_host.rb39
-rw-r--r--spec/bundler/support/artifice/compact_index_etag_match.rb16
-rw-r--r--spec/bundler/support/artifice/compact_index_extra.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_extra_api.rb6
-rw-r--r--spec/bundler/support/artifice/compact_index_extra_api_missing.rb17
-rw-r--r--spec/bundler/support/artifice/compact_index_extra_missing.rb17
-rw-r--r--spec/bundler/support/artifice/compact_index_forbidden.rb13
-rw-r--r--spec/bundler/support/artifice/compact_index_host_redirect.rb21
-rw-r--r--spec/bundler/support/artifice/compact_index_mirror_down.rb21
-rw-r--r--spec/bundler/support/artifice/compact_index_no_checksums.rb16
-rw-r--r--spec/bundler/support/artifice/compact_index_no_gem.rb13
-rw-r--r--spec/bundler/support/artifice/compact_index_partial_update.rb38
-rw-r--r--spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb40
-rw-r--r--spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb42
-rw-r--r--spec/bundler/support/artifice/compact_index_precompiled_before.rb25
-rw-r--r--spec/bundler/support/artifice/compact_index_range_ignored.rb40
-rw-r--r--spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb34
-rw-r--r--spec/bundler/support/artifice/compact_index_rate_limited.rb48
-rw-r--r--spec/bundler/support/artifice/compact_index_redirects.rb21
-rw-r--r--spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb20
-rw-r--r--spec/bundler/support/artifice/compact_index_wrong_dependencies.rb17
-rw-r--r--spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb21
-rw-r--r--spec/bundler/support/artifice/endpoint.rb6
-rw-r--r--spec/bundler/support/artifice/endpoint_500.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_api_forbidden.rb13
-rw-r--r--spec/bundler/support/artifice/endpoint_basic_authentication.rb15
-rw-r--r--spec/bundler/support/artifice/endpoint_creds_diff_host.rb39
-rw-r--r--spec/bundler/support/artifice/endpoint_extra.rb33
-rw-r--r--spec/bundler/support/artifice/endpoint_extra_api.rb34
-rw-r--r--spec/bundler/support/artifice/endpoint_extra_missing.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_fallback.rb19
-rw-r--r--spec/bundler/support/artifice/endpoint_host_redirect.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_marshal_fail.rb6
-rw-r--r--spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb15
-rw-r--r--spec/bundler/support/artifice/endpoint_mirror_source.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_redirect.rb17
-rw-r--r--spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb20
-rw-r--r--spec/bundler/support/artifice/endpoint_timeout.rb15
-rw-r--r--spec/bundler/support/artifice/fail.rb27
-rw-r--r--spec/bundler/support/artifice/helpers/artifice.rb30
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index.rb128
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index_cooldown.rb13
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index_extra.rb33
-rw-r--r--spec/bundler/support/artifice/helpers/compact_index_extra_api.rb48
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint.rb113
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint_extra.rb29
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint_fallback.rb15
-rw-r--r--spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb9
-rw-r--r--spec/bundler/support/artifice/helpers/rack_request.rb100
-rw-r--r--spec/bundler/support/artifice/vcr.rb152
-rw-r--r--spec/bundler/support/artifice/windows.rb45
-rw-r--r--spec/bundler/support/build_metadata.rb53
-rw-r--r--spec/bundler/support/builders.rb749
-rwxr-xr-xspec/bundler/support/bundle6
-rw-r--r--spec/bundler/support/bundle.rb7
-rw-r--r--spec/bundler/support/checksums.rb135
-rw-r--r--spec/bundler/support/command_execution.rb78
-rw-r--r--spec/bundler/support/env.rb13
-rw-r--r--spec/bundler/support/filters.rb42
-rw-r--r--spec/bundler/support/hax.rb74
-rw-r--r--spec/bundler/support/helpers.rb631
-rw-r--r--spec/bundler/support/indexes.rb424
-rw-r--r--spec/bundler/support/matchers.rb227
-rw-r--r--spec/bundler/support/options.rb15
-rw-r--r--spec/bundler/support/path.rb381
-rw-r--r--spec/bundler/support/permissions.rb12
-rw-r--r--spec/bundler/support/platforms.rb75
-rw-r--r--spec/bundler/support/rubygems_ext.rb222
-rw-r--r--spec/bundler/support/rubygems_version_manager.rb124
-rw-r--r--spec/bundler/support/setup.rb9
-rw-r--r--spec/bundler/support/shards.rb200
-rw-r--r--spec/bundler/support/subprocess.rb115
-rw-r--r--spec/bundler/support/switch_rubygems.rb5
-rw-r--r--spec/bundler/support/the_bundle.rb41
-rw-r--r--spec/bundler/support/vendored_net_http.rb23
-rw-r--r--spec/bundler/update/force_spec.rb30
-rw-r--r--spec/bundler/update/gemfile_spec.rb47
-rw-r--r--spec/bundler/update/gems/fund_spec.rb50
-rw-r--r--spec/bundler/update/gems/post_install_spec.rb76
-rw-r--r--spec/bundler/update/git_spec.rb341
-rw-r--r--spec/bundler/update/path_spec.rb19
-rw-r--r--spec/default.mspec138
-rw-r--r--spec/lib/formatter_overrides.rb6
-rw-r--r--spec/lib/spec_coverage.rb1
-rw-r--r--spec/mmtk.mspec12
-rw-r--r--spec/mspec/.rspec1
-rw-r--r--spec/mspec/Gemfile4
-rw-r--r--spec/mspec/Gemfile.lock26
-rw-r--r--spec/mspec/LICENSE22
-rw-r--r--spec/mspec/README.md84
-rw-r--r--spec/mspec/Rakefile6
-rwxr-xr-xspec/mspec/bin/mkspec7
-rwxr-xr-xspec/mspec/bin/mkspec.bat1
-rwxr-xr-xspec/mspec/bin/mspec7
-rwxr-xr-xspec/mspec/bin/mspec-ci7
-rwxr-xr-xspec/mspec/bin/mspec-ci.bat1
-rwxr-xr-xspec/mspec/bin/mspec-run7
-rwxr-xr-xspec/mspec/bin/mspec-run.bat1
-rwxr-xr-xspec/mspec/bin/mspec-tag7
-rwxr-xr-xspec/mspec/bin/mspec-tag.bat1
-rwxr-xr-xspec/mspec/bin/mspec.bat1
-rw-r--r--spec/mspec/lib/mspec.rb8
-rw-r--r--spec/mspec/lib/mspec/commands/mkspec.rb143
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-ci.rb76
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-run.rb87
-rw-r--r--spec/mspec/lib/mspec/commands/mspec-tag.rb132
-rw-r--r--spec/mspec/lib/mspec/commands/mspec.rb113
-rw-r--r--spec/mspec/lib/mspec/expectations.rb2
-rw-r--r--spec/mspec/lib/mspec/expectations/expectations.rb39
-rw-r--r--spec/mspec/lib/mspec/expectations/should.rb41
-rw-r--r--spec/mspec/lib/mspec/guards.rb11
-rw-r--r--spec/mspec/lib/mspec/guards/block_device.rb16
-rw-r--r--spec/mspec/lib/mspec/guards/bug.rb29
-rw-r--r--spec/mspec/lib/mspec/guards/conflict.rb23
-rw-r--r--spec/mspec/lib/mspec/guards/endian.rb25
-rw-r--r--spec/mspec/lib/mspec/guards/feature.rb45
-rw-r--r--spec/mspec/lib/mspec/guards/guard.rb141
-rw-r--r--spec/mspec/lib/mspec/guards/platform.rb122
-rw-r--r--spec/mspec/lib/mspec/guards/quarantine.rb11
-rw-r--r--spec/mspec/lib/mspec/guards/superuser.rb25
-rw-r--r--spec/mspec/lib/mspec/guards/support.rb14
-rw-r--r--spec/mspec/lib/mspec/guards/version.rb72
-rw-r--r--spec/mspec/lib/mspec/helpers.rb13
-rw-r--r--spec/mspec/lib/mspec/helpers/argf.rb35
-rw-r--r--spec/mspec/lib/mspec/helpers/argv.rb44
-rw-r--r--spec/mspec/lib/mspec/helpers/datetime.rb48
-rw-r--r--spec/mspec/lib/mspec/helpers/fixture.rb24
-rw-r--r--spec/mspec/lib/mspec/helpers/flunk.rb3
-rw-r--r--spec/mspec/lib/mspec/helpers/fs.rb64
-rw-r--r--spec/mspec/lib/mspec/helpers/io.rb87
-rw-r--r--spec/mspec/lib/mspec/helpers/mock_to_path.rb6
-rw-r--r--spec/mspec/lib/mspec/helpers/numeric.rb98
-rw-r--r--spec/mspec/lib/mspec/helpers/ruby_exe.rb205
-rw-r--r--spec/mspec/lib/mspec/helpers/scratch.rb21
-rw-r--r--spec/mspec/lib/mspec/helpers/tmp.rb62
-rw-r--r--spec/mspec/lib/mspec/helpers/warning.rb21
-rw-r--r--spec/mspec/lib/mspec/matchers.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/base.rb95
-rw-r--r--spec/mspec/lib/mspec/matchers/be_an_instance_of.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/be_ancestor_of.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/be_close.rb29
-rw-r--r--spec/mspec/lib/mspec/matchers/be_computed_by.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/be_empty.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_false.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_kind_of.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/be_nan.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_nil.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_true.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/be_true_or_false.rb20
-rw-r--r--spec/mspec/lib/mspec/matchers/block_caller.rb37
-rw-r--r--spec/mspec/lib/mspec/matchers/complain.rb69
-rw-r--r--spec/mspec/lib/mspec/matchers/eql.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/equal.rb26
-rw-r--r--spec/mspec/lib/mspec/matchers/equal_element.rb78
-rw-r--r--spec/mspec/lib/mspec/matchers/have_class_variable.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_constant.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_instance_variable.rb12
-rw-r--r--spec/mspec/lib/mspec/matchers/have_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_private_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_public_instance_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/have_singleton_method.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/include.rb31
-rw-r--r--spec/mspec/lib/mspec/matchers/include_any_of.rb29
-rw-r--r--spec/mspec/lib/mspec/matchers/infinity.rb28
-rw-r--r--spec/mspec/lib/mspec/matchers/match_yaml.rb50
-rw-r--r--spec/mspec/lib/mspec/matchers/method.rb10
-rw-r--r--spec/mspec/lib/mspec/matchers/output.rb67
-rw-r--r--spec/mspec/lib/mspec/matchers/output_to_fd.rb71
-rw-r--r--spec/mspec/lib/mspec/matchers/raise_error.rb132
-rw-r--r--spec/mspec/lib/mspec/matchers/respond_to.rb24
-rw-r--r--spec/mspec/lib/mspec/matchers/signed_zero.rb28
-rw-r--r--spec/mspec/lib/mspec/matchers/skip.rb5
-rw-r--r--spec/mspec/lib/mspec/matchers/variable.rb24
-rw-r--r--spec/mspec/lib/mspec/mocks.rb3
-rw-r--r--spec/mspec/lib/mspec/mocks/mock.rb209
-rw-r--r--spec/mspec/lib/mspec/mocks/object.rb28
-rw-r--r--spec/mspec/lib/mspec/mocks/proxy.rb186
-rw-r--r--spec/mspec/lib/mspec/runner.rb12
-rw-r--r--spec/mspec/lib/mspec/runner/actions.rb6
-rw-r--r--spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb84
-rw-r--r--spec/mspec/lib/mspec/runner/actions/filter.rb40
-rw-r--r--spec/mspec/lib/mspec/runner/actions/leakchecker.rb377
-rw-r--r--spec/mspec/lib/mspec/runner/actions/profile.rb60
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tag.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/taglist.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tagpurge.rb56
-rw-r--r--spec/mspec/lib/mspec/runner/actions/tally.rb133
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timeout.rb145
-rw-r--r--spec/mspec/lib/mspec/runner/actions/timer.rb22
-rw-r--r--spec/mspec/lib/mspec/runner/context.rb237
-rw-r--r--spec/mspec/lib/mspec/runner/evaluate.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/example.rb34
-rw-r--r--spec/mspec/lib/mspec/runner/exception.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/filters.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/filters/match.rb18
-rw-r--r--spec/mspec/lib/mspec/runner/filters/profile.rb54
-rw-r--r--spec/mspec/lib/mspec/runner/filters/regexp.rb23
-rw-r--r--spec/mspec/lib/mspec/runner/filters/tag.rb29
-rw-r--r--spec/mspec/lib/mspec/runner/formatters.rb13
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/base.rb150
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/describe.rb23
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/dotted.rb23
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/file.rb24
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/html.rb81
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/junit.rb87
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/launchable.rb88
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/method.rb95
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/multi.rb47
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/profile.rb18
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/specdoc.rb41
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/spinner.rb111
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/stats.rb57
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/summary.rb4
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/unit.rb20
-rw-r--r--spec/mspec/lib/mspec/runner/formatters/yaml.rb38
-rw-r--r--spec/mspec/lib/mspec/runner/mspec.rb424
-rw-r--r--spec/mspec/lib/mspec/runner/object.rb26
-rw-r--r--spec/mspec/lib/mspec/runner/parallel.rb98
-rw-r--r--spec/mspec/lib/mspec/runner/shared.rb14
-rw-r--r--spec/mspec/lib/mspec/runner/tag.rb38
-rw-r--r--spec/mspec/lib/mspec/utils/deprecate.rb6
-rw-r--r--spec/mspec/lib/mspec/utils/format.rb24
-rw-r--r--spec/mspec/lib/mspec/utils/name_map.rb133
-rw-r--r--spec/mspec/lib/mspec/utils/options.rb521
-rw-r--r--spec/mspec/lib/mspec/utils/script.rb305
-rw-r--r--spec/mspec/lib/mspec/utils/version.rb52
-rw-r--r--spec/mspec/lib/mspec/utils/warnings.rb10
-rw-r--r--spec/mspec/lib/mspec/version.rb5
-rw-r--r--spec/mspec/spec/commands/fixtures/four.txt0
-rw-r--r--spec/mspec/spec/commands/fixtures/level2/three_spec.rb1
-rw-r--r--spec/mspec/spec/commands/fixtures/one_spec.rb1
-rw-r--r--spec/mspec/spec/commands/fixtures/three.rb1
-rw-r--r--spec/mspec/spec/commands/fixtures/two_spec.rb1
-rw-r--r--spec/mspec/spec/commands/mkspec_spec.rb363
-rw-r--r--spec/mspec/spec/commands/mspec_ci_spec.rb155
-rw-r--r--spec/mspec/spec/commands/mspec_run_spec.rb178
-rw-r--r--spec/mspec/spec/commands/mspec_spec.rb180
-rw-r--r--spec/mspec/spec/commands/mspec_tag_spec.rb414
-rw-r--r--spec/mspec/spec/expectations/expectations_spec.rb29
-rw-r--r--spec/mspec/spec/expectations/should_spec.rb61
-rw-r--r--spec/mspec/spec/fixtures/a_spec.rb15
-rw-r--r--spec/mspec/spec/fixtures/b_spec.rb7
-rw-r--r--spec/mspec/spec/fixtures/chatty_spec.rb8
-rw-r--r--spec/mspec/spec/fixtures/config.mspec8
-rw-r--r--spec/mspec/spec/fixtures/die_spec.rb7
-rwxr-xr-xspec/mspec/spec/fixtures/my_ruby4
-rw-r--r--spec/mspec/spec/fixtures/object_methods_spec.rb8
-rw-r--r--spec/mspec/spec/fixtures/print_interpreter_spec.rb4
-rw-r--r--spec/mspec/spec/fixtures/should.rb75
-rw-r--r--spec/mspec/spec/fixtures/tagging_spec.rb16
-rw-r--r--spec/mspec/spec/guards/block_device_spec.rb46
-rw-r--r--spec/mspec/spec/guards/bug_spec.rb151
-rw-r--r--spec/mspec/spec/guards/conflict_spec.rb53
-rw-r--r--spec/mspec/spec/guards/endian_spec.rb55
-rw-r--r--spec/mspec/spec/guards/feature_spec.rb120
-rw-r--r--spec/mspec/spec/guards/guard_spec.rb421
-rw-r--r--spec/mspec/spec/guards/platform_spec.rb337
-rw-r--r--spec/mspec/spec/guards/quarantine_spec.rb35
-rw-r--r--spec/mspec/spec/guards/superuser_spec.rb35
-rw-r--r--spec/mspec/spec/guards/support_spec.rb54
-rw-r--r--spec/mspec/spec/guards/user_spec.rb20
-rw-r--r--spec/mspec/spec/guards/version_spec.rb112
-rw-r--r--spec/mspec/spec/helpers/argf_spec.rb37
-rw-r--r--spec/mspec/spec/helpers/argv_spec.rb27
-rw-r--r--spec/mspec/spec/helpers/datetime_spec.rb44
-rw-r--r--spec/mspec/spec/helpers/fixture_spec.rb25
-rw-r--r--spec/mspec/spec/helpers/flunk_spec.rb20
-rw-r--r--spec/mspec/spec/helpers/fs_spec.rb195
-rw-r--r--spec/mspec/spec/helpers/io_spec.rb136
-rw-r--r--spec/mspec/spec/helpers/mock_to_path_spec.rb23
-rw-r--r--spec/mspec/spec/helpers/numeric_spec.rb31
-rw-r--r--spec/mspec/spec/helpers/ruby_exe_spec.rb256
-rw-r--r--spec/mspec/spec/helpers/scratch_spec.rb24
-rw-r--r--spec/mspec/spec/helpers/suppress_warning_spec.rb19
-rw-r--r--spec/mspec/spec/helpers/tmp_spec.rb27
-rw-r--r--spec/mspec/spec/integration/interpreter_spec.rb18
-rw-r--r--spec/mspec/spec/integration/object_methods_spec.rb18
-rw-r--r--spec/mspec/spec/integration/run_spec.rb72
-rw-r--r--spec/mspec/spec/integration/tag_spec.rb60
-rw-r--r--spec/mspec/spec/matchers/base_spec.rb228
-rw-r--r--spec/mspec/spec/matchers/be_an_instance_of_spec.rb50
-rw-r--r--spec/mspec/spec/matchers/be_ancestor_of_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/be_close_spec.rb48
-rw-r--r--spec/mspec/spec/matchers/be_computed_by_spec.rb42
-rw-r--r--spec/mspec/spec/matchers/be_empty_spec.rb26
-rw-r--r--spec/mspec/spec/matchers/be_false_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/be_kind_of_spec.rb31
-rw-r--r--spec/mspec/spec/matchers/be_nan_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/be_nil_spec.rb27
-rw-r--r--spec/mspec/spec/matchers/be_true_or_false_spec.rb19
-rw-r--r--spec/mspec/spec/matchers/be_true_spec.rb28
-rw-r--r--spec/mspec/spec/matchers/block_caller_spec.rb13
-rw-r--r--spec/mspec/spec/matchers/complain_spec.rb102
-rw-r--r--spec/mspec/spec/matchers/eql_spec.rb33
-rw-r--r--spec/mspec/spec/matchers/equal_element_spec.rb75
-rw-r--r--spec/mspec/spec/matchers/equal_spec.rb32
-rw-r--r--spec/mspec/spec/matchers/have_class_variable_spec.rb49
-rw-r--r--spec/mspec/spec/matchers/have_constant_spec.rb37
-rw-r--r--spec/mspec/spec/matchers/have_instance_method_spec.rb53
-rw-r--r--spec/mspec/spec/matchers/have_instance_variable_spec.rb50
-rw-r--r--spec/mspec/spec/matchers/have_method_spec.rb55
-rw-r--r--spec/mspec/spec/matchers/have_private_instance_method_spec.rb57
-rw-r--r--spec/mspec/spec/matchers/have_private_method_spec.rb44
-rw-r--r--spec/mspec/spec/matchers/have_protected_instance_method_spec.rb57
-rw-r--r--spec/mspec/spec/matchers/have_public_instance_method_spec.rb53
-rw-r--r--spec/mspec/spec/matchers/have_singleton_method_spec.rb45
-rw-r--r--spec/mspec/spec/matchers/include_any_of_spec.rb42
-rw-r--r--spec/mspec/spec/matchers/include_spec.rb37
-rw-r--r--spec/mspec/spec/matchers/infinity_spec.rb34
-rw-r--r--spec/mspec/spec/matchers/match_yaml_spec.rb39
-rw-r--r--spec/mspec/spec/matchers/output_spec.rb84
-rw-r--r--spec/mspec/spec/matchers/output_to_fd_spec.rb44
-rw-r--r--spec/mspec/spec/matchers/raise_error_spec.rb234
-rw-r--r--spec/mspec/spec/matchers/respond_to_spec.rb33
-rw-r--r--spec/mspec/spec/matchers/signed_zero_spec.rb32
-rw-r--r--spec/mspec/spec/mocks/mock_spec.rb529
-rw-r--r--spec/mspec/spec/mocks/proxy_spec.rb405
-rw-r--r--spec/mspec/spec/runner/actions/filter_spec.rb84
-rw-r--r--spec/mspec/spec/runner/actions/tag_spec.rb313
-rw-r--r--spec/mspec/spec/runner/actions/taglist_spec.rb152
-rw-r--r--spec/mspec/spec/runner/actions/tagpurge_spec.rb154
-rw-r--r--spec/mspec/spec/runner/actions/tally_spec.rb355
-rw-r--r--spec/mspec/spec/runner/actions/timer_spec.rb44
-rw-r--r--spec/mspec/spec/runner/context_spec.rb1028
-rw-r--r--spec/mspec/spec/runner/example_spec.rb117
-rw-r--r--spec/mspec/spec/runner/exception_spec.rb146
-rw-r--r--spec/mspec/spec/runner/filters/a.yaml4
-rw-r--r--spec/mspec/spec/runner/filters/b.yaml11
-rw-r--r--spec/mspec/spec/runner/filters/match_spec.rb34
-rw-r--r--spec/mspec/spec/runner/filters/profile_spec.rb117
-rw-r--r--spec/mspec/spec/runner/filters/regexp_spec.rb31
-rw-r--r--spec/mspec/spec/runner/filters/tag_spec.rb92
-rw-r--r--spec/mspec/spec/runner/formatters/describe_spec.rb67
-rw-r--r--spec/mspec/spec/runner/formatters/dotted_spec.rb284
-rw-r--r--spec/mspec/spec/runner/formatters/file_spec.rb84
-rw-r--r--spec/mspec/spec/runner/formatters/html_spec.rb220
-rw-r--r--spec/mspec/spec/runner/formatters/junit_spec.rb159
-rw-r--r--spec/mspec/spec/runner/formatters/method_spec.rb177
-rw-r--r--spec/mspec/spec/runner/formatters/multi_spec.rb68
-rw-r--r--spec/mspec/spec/runner/formatters/specdoc_spec.rb106
-rw-r--r--spec/mspec/spec/runner/formatters/spinner_spec.rb83
-rw-r--r--spec/mspec/spec/runner/formatters/summary_spec.rb26
-rw-r--r--spec/mspec/spec/runner/formatters/unit_spec.rb73
-rw-r--r--spec/mspec/spec/runner/formatters/yaml_spec.rb134
-rw-r--r--spec/mspec/spec/runner/mspec_spec.rb597
-rw-r--r--spec/mspec/spec/runner/shared_spec.rb90
-rw-r--r--spec/mspec/spec/runner/tag_spec.rb123
-rw-r--r--spec/mspec/spec/runner/tags.txt4
-rw-r--r--spec/mspec/spec/spec_helper.rb70
-rw-r--r--spec/mspec/spec/utils/deprecate_spec.rb17
-rw-r--r--spec/mspec/spec/utils/fixtures/this_file_raises.rb1
-rw-r--r--spec/mspec/spec/utils/fixtures/this_file_raises2.rb1
-rw-r--r--spec/mspec/spec/utils/name_map_spec.rb187
-rw-r--r--spec/mspec/spec/utils/options_spec.rb1302
-rw-r--r--spec/mspec/spec/utils/script_spec.rb470
-rw-r--r--spec/mspec/spec/utils/version_spec.rb45
-rwxr-xr-xspec/mspec/tool/check_require_spec_helper.rb34
-rwxr-xr-xspec/mspec/tool/find.rb10
-rwxr-xr-xspec/mspec/tool/pull-latest-mspec-spec26
-rwxr-xr-xspec/mspec/tool/remove_old_guards.rb145
-rw-r--r--spec/mspec/tool/sync/.gitignore4
-rw-r--r--spec/mspec/tool/sync/sync-rubyspec.rb254
-rwxr-xr-xspec/mspec/tool/tag_from_output.rb65
-rwxr-xr-xspec/mspec/tool/wrap_with_guard.rb28
-rw-r--r--spec/ruby/.gitignore5
-rw-r--r--spec/ruby/.mspec.constants236
-rw-r--r--spec/ruby/.rubocop.yml204
-rw-r--r--spec/ruby/.rubocop_todo.yml138
-rw-r--r--spec/ruby/CONTRIBUTING.md297
-rw-r--r--spec/ruby/LICENSE22
-rw-r--r--spec/ruby/README.md167
-rw-r--r--spec/ruby/TODO8
-rwxr-xr-xspec/ruby/bin/rubocop12
-rw-r--r--spec/ruby/command_line/backtrace_limit_spec.rb93
-rwxr-xr-xspec/ruby/command_line/dash_0_spec.rb13
-rw-r--r--spec/ruby/command_line/dash_a_spec.rb19
-rw-r--r--spec/ruby/command_line/dash_c_spec.rb13
-rw-r--r--spec/ruby/command_line/dash_d_spec.rb22
-rw-r--r--spec/ruby/command_line/dash_e_spec.rb41
-rw-r--r--spec/ruby/command_line/dash_encoding_spec.rb36
-rw-r--r--spec/ruby/command_line/dash_external_encoding_spec.rb15
-rw-r--r--spec/ruby/command_line/dash_internal_encoding_spec.rb15
-rw-r--r--spec/ruby/command_line/dash_l_spec.rb31
-rw-r--r--spec/ruby/command_line/dash_n_spec.rb36
-rw-r--r--spec/ruby/command_line/dash_p_spec.rb19
-rw-r--r--spec/ruby/command_line/dash_r_spec.rb28
-rw-r--r--spec/ruby/command_line/dash_s_spec.rb52
-rw-r--r--spec/ruby/command_line/dash_upper_c_spec.rb6
-rw-r--r--spec/ruby/command_line/dash_upper_e_spec.rb37
-rw-r--r--spec/ruby/command_line/dash_upper_f_spec.rb13
-rw-r--r--spec/ruby/command_line/dash_upper_i_spec.rb51
-rw-r--r--spec/ruby/command_line/dash_upper_k_spec.rb65
-rw-r--r--spec/ruby/command_line/dash_upper_s_spec.rb67
-rw-r--r--spec/ruby/command_line/dash_upper_u_spec.rb52
-rw-r--r--spec/ruby/command_line/dash_upper_w_spec.rb44
-rw-r--r--spec/ruby/command_line/dash_upper_x_spec.rb6
-rw-r--r--spec/ruby/command_line/dash_v_spec.rb15
-rw-r--r--spec/ruby/command_line/dash_w_spec.rb10
-rw-r--r--spec/ruby/command_line/dash_x_spec.rb21
-rw-r--r--spec/ruby/command_line/error_message_spec.rb16
-rw-r--r--spec/ruby/command_line/feature_spec.rb71
-rw-r--r--spec/ruby/command_line/fixtures/backtrace.rb35
-rw-r--r--spec/ruby/command_line/fixtures/bad_syntax.rb1
-rw-r--r--spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt3
-rw-r--r--spec/ruby/command_line/fixtures/bin/dash_s_fail1
-rw-r--r--spec/ruby/command_line/fixtures/bin/embedded_ruby.txt3
-rw-r--r--spec/ruby/command_line/fixtures/bin/hybrid_launcher.sh4
-rwxr-xr-xspec/ruby/command_line/fixtures/bin/launcher.rb2
-rw-r--r--spec/ruby/command_line/fixtures/change_directory_script.rb1
-rw-r--r--spec/ruby/command_line/fixtures/conditional_range.txt5
-rw-r--r--spec/ruby/command_line/fixtures/dash_s_script.rb12
-rw-r--r--spec/ruby/command_line/fixtures/debug.rb10
-rw-r--r--spec/ruby/command_line/fixtures/debug_info.rb10
-rw-r--r--spec/ruby/command_line/fixtures/freeze_flag_across_files.rb3
-rw-r--r--spec/ruby/command_line/fixtures/freeze_flag_across_files_diff_enc.rb3
-rw-r--r--spec/ruby/command_line/fixtures/freeze_flag_one_literal.rb2
-rw-r--r--spec/ruby/command_line/fixtures/freeze_flag_required.rb1
-rw-r--r--spec/ruby/command_line/fixtures/freeze_flag_required_diff_enc.rb3
-rw-r--r--spec/ruby/command_line/fixtures/freeze_flag_two_literals.rb1
-rw-r--r--spec/ruby/command_line/fixtures/full_names.txt3
-rw-r--r--spec/ruby/command_line/fixtures/loadpath.rb1
-rw-r--r--spec/ruby/command_line/fixtures/names.txt3
-rw-r--r--spec/ruby/command_line/fixtures/passwd_file.txt3
-rw-r--r--spec/ruby/command_line/fixtures/require.rb1
-rw-r--r--spec/ruby/command_line/fixtures/rubyopt.rb1
-rw-r--r--spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb4
-rw-r--r--spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb4
-rw-r--r--spec/ruby/command_line/fixtures/string_literal_raw.rb3
-rw-r--r--spec/ruby/command_line/fixtures/test_file.rb1
-rw-r--r--spec/ruby/command_line/fixtures/verbose.rb1
-rw-r--r--spec/ruby/command_line/frozen_strings_spec.rb94
-rw-r--r--spec/ruby/command_line/rubylib_spec.rb69
-rw-r--r--spec/ruby/command_line/rubyopt_spec.rb185
-rw-r--r--spec/ruby/command_line/shared/change_directory.rb21
-rw-r--r--spec/ruby/command_line/shared/verbose.rb9
-rw-r--r--spec/ruby/command_line/syntax_error_spec.rb13
-rw-r--r--spec/ruby/core/argf/argf_spec.rb11
-rw-r--r--spec/ruby/core/argf/argv_spec.rb19
-rw-r--r--spec/ruby/core/argf/binmode_spec.rb43
-rw-r--r--spec/ruby/core/argf/close_spec.rb35
-rw-r--r--spec/ruby/core/argf/closed_spec.rb18
-rw-r--r--spec/ruby/core/argf/each_byte_spec.rb60
-rw-r--r--spec/ruby/core/argf/each_char_spec.rb60
-rw-r--r--spec/ruby/core/argf/each_codepoint_spec.rb60
-rw-r--r--spec/ruby/core/argf/each_line_spec.rb64
-rw-r--r--spec/ruby/core/argf/each_spec.rb7
-rw-r--r--spec/ruby/core/argf/eof_spec.rb32
-rw-r--r--spec/ruby/core/argf/file_spec.rb21
-rw-r--r--spec/ruby/core/argf/filename_spec.rb30
-rw-r--r--spec/ruby/core/argf/fileno_spec.rb26
-rw-r--r--spec/ruby/core/argf/fixtures/bin_file.txt2
-rw-r--r--spec/ruby/core/argf/fixtures/file1.txt2
-rw-r--r--spec/ruby/core/argf/fixtures/file2.txt2
-rw-r--r--spec/ruby/core/argf/fixtures/filename.rb3
-rw-r--r--spec/ruby/core/argf/fixtures/lineno.rb5
-rw-r--r--spec/ruby/core/argf/fixtures/rewind.rb5
-rw-r--r--spec/ruby/core/argf/fixtures/stdin.txt2
-rw-r--r--spec/ruby/core/argf/getc_spec.rb20
-rw-r--r--spec/ruby/core/argf/gets_spec.rb49
-rw-r--r--spec/ruby/core/argf/inspect_spec.rb7
-rw-r--r--spec/ruby/core/argf/lineno_spec.rb30
-rw-r--r--spec/ruby/core/argf/path_spec.rb7
-rw-r--r--spec/ruby/core/argf/pos_spec.rb65
-rw-r--r--spec/ruby/core/argf/read_nonblock_spec.rb80
-rw-r--r--spec/ruby/core/argf/read_spec.rb85
-rw-r--r--spec/ruby/core/argf/readchar_spec.rb19
-rw-r--r--spec/ruby/core/argf/readline_spec.rb23
-rw-r--r--spec/ruby/core/argf/readlines_spec.rb24
-rw-r--r--spec/ruby/core/argf/readpartial_spec.rb75
-rw-r--r--spec/ruby/core/argf/rewind_spec.rb39
-rw-r--r--spec/ruby/core/argf/seek_spec.rb63
-rw-r--r--spec/ruby/core/argf/set_encoding_spec.rb41
-rw-r--r--spec/ruby/core/argf/shared/getc.rb17
-rw-r--r--spec/ruby/core/argf/shared/gets.rb99
-rw-r--r--spec/ruby/core/argf/shared/read.rb58
-rw-r--r--spec/ruby/core/argf/skip_spec.rb42
-rw-r--r--spec/ruby/core/argf/tell_spec.rb7
-rw-r--r--spec/ruby/core/argf/to_a_spec.rb7
-rw-r--r--spec/ruby/core/argf/to_i_spec.rb7
-rw-r--r--spec/ruby/core/argf/to_io_spec.rb23
-rw-r--r--spec/ruby/core/argf/to_s_spec.rb14
-rw-r--r--spec/ruby/core/array/all_spec.rb13
-rw-r--r--spec/ruby/core/array/allocate_spec.rb19
-rw-r--r--spec/ruby/core/array/any_spec.rb49
-rw-r--r--spec/ruby/core/array/append_spec.rb41
-rw-r--r--spec/ruby/core/array/array_spec.rb7
-rw-r--r--spec/ruby/core/array/assoc_spec.rb52
-rw-r--r--spec/ruby/core/array/at_spec.rb56
-rw-r--r--spec/ruby/core/array/bsearch_index_spec.rb81
-rw-r--r--spec/ruby/core/array/bsearch_spec.rb84
-rw-r--r--spec/ruby/core/array/clear_spec.rb32
-rw-r--r--spec/ruby/core/array/clone_spec.rb31
-rw-r--r--spec/ruby/core/array/collect_spec.rb143
-rw-r--r--spec/ruby/core/array/combination_spec.rb74
-rw-r--r--spec/ruby/core/array/compact_spec.rb51
-rw-r--r--spec/ruby/core/array/comparison_spec.rb97
-rw-r--r--spec/ruby/core/array/concat_spec.rb74
-rw-r--r--spec/ruby/core/array/constructor_spec.rb24
-rw-r--r--spec/ruby/core/array/count_spec.rb26
-rw-r--r--spec/ruby/core/array/cycle_spec.rb101
-rw-r--r--spec/ruby/core/array/deconstruct_spec.rb9
-rw-r--r--spec/ruby/core/array/delete_at_spec.rb41
-rw-r--r--spec/ruby/core/array/delete_if_spec.rb82
-rw-r--r--spec/ruby/core/array/delete_spec.rb46
-rw-r--r--spec/ruby/core/array/difference_spec.rb22
-rw-r--r--spec/ruby/core/array/dig_spec.rb52
-rw-r--r--spec/ruby/core/array/drop_spec.rb56
-rw-r--r--spec/ruby/core/array/drop_while_spec.rb24
-rw-r--r--spec/ruby/core/array/dup_spec.rb31
-rw-r--r--spec/ruby/core/array/each_index_spec.rb58
-rw-r--r--spec/ruby/core/array/each_spec.rb82
-rw-r--r--spec/ruby/core/array/element_reference_spec.rb903
-rw-r--r--spec/ruby/core/array/element_set_spec.rb537
-rw-r--r--spec/ruby/core/array/empty_spec.rb10
-rw-r--r--spec/ruby/core/array/eql_spec.rb19
-rw-r--r--spec/ruby/core/array/equal_value_spec.rb51
-rw-r--r--spec/ruby/core/array/fetch_spec.rb55
-rw-r--r--spec/ruby/core/array/fetch_values_spec.rb55
-rw-r--r--spec/ruby/core/array/fill_spec.rb374
-rw-r--r--spec/ruby/core/array/filter_spec.rb13
-rw-r--r--spec/ruby/core/array/find_index_spec.rb42
-rw-r--r--spec/ruby/core/array/first_spec.rb93
-rw-r--r--spec/ruby/core/array/fixtures/classes.rb592
-rw-r--r--spec/ruby/core/array/fixtures/encoded_strings.rb69
-rw-r--r--spec/ruby/core/array/flatten_spec.rb266
-rw-r--r--spec/ruby/core/array/frozen_spec.rb16
-rw-r--r--spec/ruby/core/array/hash_spec.rb83
-rw-r--r--spec/ruby/core/array/include_spec.rb33
-rw-r--r--spec/ruby/core/array/index_spec.rb7
-rw-r--r--spec/ruby/core/array/initialize_spec.rb158
-rw-r--r--spec/ruby/core/array/insert_spec.rb78
-rw-r--r--spec/ruby/core/array/inspect_spec.rb108
-rw-r--r--spec/ruby/core/array/intersect_spec.rb64
-rw-r--r--spec/ruby/core/array/intersection_spec.rb19
-rw-r--r--spec/ruby/core/array/join_spec.rb146
-rw-r--r--spec/ruby/core/array/keep_if_spec.rb11
-rw-r--r--spec/ruby/core/array/last_spec.rb87
-rw-r--r--spec/ruby/core/array/length_spec.rb14
-rw-r--r--spec/ruby/core/array/map_spec.rb13
-rw-r--r--spec/ruby/core/array/max_spec.rb116
-rw-r--r--spec/ruby/core/array/min_spec.rb121
-rw-r--r--spec/ruby/core/array/minmax_spec.rb14
-rw-r--r--spec/ruby/core/array/minus_spec.rb7
-rw-r--r--spec/ruby/core/array/multiply_spec.rb94
-rw-r--r--spec/ruby/core/array/new_spec.rb124
-rw-r--r--spec/ruby/core/array/none_spec.rb13
-rw-r--r--spec/ruby/core/array/one_spec.rb13
-rw-r--r--spec/ruby/core/array/pack/a_spec.rb73
-rw-r--r--spec/ruby/core/array/pack/at_spec.rb30
-rw-r--r--spec/ruby/core/array/pack/b_spec.rb113
-rw-r--r--spec/ruby/core/array/pack/buffer_spec.rb60
-rw-r--r--spec/ruby/core/array/pack/c_spec.rb77
-rw-r--r--spec/ruby/core/array/pack/comment_spec.rb25
-rw-r--r--spec/ruby/core/array/pack/d_spec.rb39
-rw-r--r--spec/ruby/core/array/pack/e_spec.rb25
-rw-r--r--spec/ruby/core/array/pack/empty_spec.rb11
-rw-r--r--spec/ruby/core/array/pack/f_spec.rb39
-rw-r--r--spec/ruby/core/array/pack/g_spec.rb25
-rw-r--r--spec/ruby/core/array/pack/h_spec.rb205
-rw-r--r--spec/ruby/core/array/pack/i_spec.rb133
-rw-r--r--spec/ruby/core/array/pack/j_spec.rb217
-rw-r--r--spec/ruby/core/array/pack/l_spec.rb221
-rw-r--r--spec/ruby/core/array/pack/m_spec.rb317
-rw-r--r--spec/ruby/core/array/pack/n_spec.rb25
-rw-r--r--spec/ruby/core/array/pack/p_spec.rb38
-rw-r--r--spec/ruby/core/array/pack/percent_spec.rb7
-rw-r--r--spec/ruby/core/array/pack/q_spec.rb61
-rw-r--r--spec/ruby/core/array/pack/r_spec.rb89
-rw-r--r--spec/ruby/core/array/pack/s_spec.rb133
-rw-r--r--spec/ruby/core/array/pack/shared/basic.rb73
-rw-r--r--spec/ruby/core/array/pack/shared/encodings.rb16
-rw-r--r--spec/ruby/core/array/pack/shared/float.rb255
-rw-r--r--spec/ruby/core/array/pack/shared/integer.rb387
-rw-r--r--spec/ruby/core/array/pack/shared/numeric_basic.rb50
-rw-r--r--spec/ruby/core/array/pack/shared/string.rb48
-rw-r--r--spec/ruby/core/array/pack/shared/taint.rb2
-rw-r--r--spec/ruby/core/array/pack/shared/unicode.rb96
-rw-r--r--spec/ruby/core/array/pack/u_spec.rb140
-rw-r--r--spec/ruby/core/array/pack/v_spec.rb25
-rw-r--r--spec/ruby/core/array/pack/w_spec.rb44
-rw-r--r--spec/ruby/core/array/pack/x_spec.rb65
-rw-r--r--spec/ruby/core/array/pack/z_spec.rb44
-rw-r--r--spec/ruby/core/array/partition_spec.rb43
-rw-r--r--spec/ruby/core/array/permutation_spec.rb138
-rw-r--r--spec/ruby/core/array/plus_spec.rb56
-rw-r--r--spec/ruby/core/array/pop_spec.rb124
-rw-r--r--spec/ruby/core/array/prepend_spec.rb7
-rw-r--r--spec/ruby/core/array/product_spec.rb73
-rw-r--r--spec/ruby/core/array/push_spec.rb36
-rw-r--r--spec/ruby/core/array/rassoc_spec.rb50
-rw-r--r--spec/ruby/core/array/reject_spec.rb158
-rw-r--r--spec/ruby/core/array/repeated_combination_spec.rb84
-rw-r--r--spec/ruby/core/array/repeated_permutation_spec.rb94
-rw-r--r--spec/ruby/core/array/replace_spec.rb63
-rw-r--r--spec/ruby/core/array/reverse_each_spec.rb57
-rw-r--r--spec/ruby/core/array/reverse_spec.rb42
-rw-r--r--spec/ruby/core/array/rindex_spec.rb95
-rw-r--r--spec/ruby/core/array/rotate_spec.rb129
-rw-r--r--spec/ruby/core/array/sample_spec.rb155
-rw-r--r--spec/ruby/core/array/select_spec.rb43
-rw-r--r--spec/ruby/core/array/shared/clone.rb20
-rw-r--r--spec/ruby/core/array/shared/delete_if.rb13
-rw-r--r--spec/ruby/core/array/shared/difference.rb78
-rw-r--r--spec/ruby/core/array/shared/enumeratorize.rb5
-rw-r--r--spec/ruby/core/array/shared/eql.rb92
-rw-r--r--spec/ruby/core/array/shared/intersection.rb85
-rw-r--r--spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb25
-rw-r--r--spec/ruby/core/array/shared/join.rb15
-rw-r--r--spec/ruby/core/array/shared/keep_if.rb95
-rw-r--r--spec/ruby/core/array/shared/union.rb79
-rw-r--r--spec/ruby/core/array/shift_spec.rb120
-rw-r--r--spec/ruby/core/array/shuffle_spec.rb119
-rw-r--r--spec/ruby/core/array/size_spec.rb7
-rw-r--r--spec/ruby/core/array/slice_spec.rb219
-rw-r--r--spec/ruby/core/array/sort_by_spec.rb85
-rw-r--r--spec/ruby/core/array/sort_spec.rb252
-rw-r--r--spec/ruby/core/array/sum_spec.rb88
-rw-r--r--spec/ruby/core/array/take_spec.rb32
-rw-r--r--spec/ruby/core/array/take_while_spec.rb26
-rw-r--r--spec/ruby/core/array/to_a_spec.rb24
-rw-r--r--spec/ruby/core/array/to_ary_spec.rb20
-rw-r--r--spec/ruby/core/array/to_h_spec.rb91
-rw-r--r--spec/ruby/core/array/to_s_spec.rb7
-rw-r--r--spec/ruby/core/array/transpose_spec.rb53
-rw-r--r--spec/ruby/core/array/try_convert_spec.rb50
-rw-r--r--spec/ruby/core/array/union_spec.rb25
-rw-r--r--spec/ruby/core/array/uniq_spec.rb243
-rw-r--r--spec/ruby/core/array/unshift_spec.rb67
-rw-r--r--spec/ruby/core/array/values_at_spec.rb74
-rw-r--r--spec/ruby/core/array/zip_spec.rb71
-rw-r--r--spec/ruby/core/basicobject/__id__spec.rb6
-rw-r--r--spec/ruby/core/basicobject/__send___spec.rb10
-rw-r--r--spec/ruby/core/basicobject/basicobject_spec.rb91
-rw-r--r--spec/ruby/core/basicobject/equal_spec.rb54
-rw-r--r--spec/ruby/core/basicobject/equal_value_spec.rb10
-rw-r--r--spec/ruby/core/basicobject/fixtures/classes.rb255
-rw-r--r--spec/ruby/core/basicobject/fixtures/common.rb9
-rw-r--r--spec/ruby/core/basicobject/fixtures/remove_method_missing.rb9
-rw-r--r--spec/ruby/core/basicobject/fixtures/singleton_method.rb10
-rw-r--r--spec/ruby/core/basicobject/initialize_spec.rb13
-rw-r--r--spec/ruby/core/basicobject/instance_eval_spec.rb327
-rw-r--r--spec/ruby/core/basicobject/instance_exec_spec.rb113
-rw-r--r--spec/ruby/core/basicobject/method_missing_spec.rb40
-rw-r--r--spec/ruby/core/basicobject/not_equal_spec.rb53
-rw-r--r--spec/ruby/core/basicobject/not_spec.rb11
-rw-r--r--spec/ruby/core/basicobject/singleton_method_added_spec.rb147
-rw-r--r--spec/ruby/core/basicobject/singleton_method_removed_spec.rb24
-rw-r--r--spec/ruby/core/basicobject/singleton_method_undefined_spec.rb24
-rw-r--r--spec/ruby/core/binding/clone_spec.rb13
-rw-r--r--spec/ruby/core/binding/dup_spec.rb30
-rw-r--r--spec/ruby/core/binding/eval_spec.rb105
-rw-r--r--spec/ruby/core/binding/fixtures/classes.rb66
-rw-r--r--spec/ruby/core/binding/fixtures/location.rb6
-rw-r--r--spec/ruby/core/binding/local_variable_defined_spec.rb46
-rw-r--r--spec/ruby/core/binding/local_variable_get_spec.rb56
-rw-r--r--spec/ruby/core/binding/local_variable_set_spec.rb71
-rw-r--r--spec/ruby/core/binding/local_variables_spec.rb35
-rw-r--r--spec/ruby/core/binding/receiver_spec.rb11
-rw-r--r--spec/ruby/core/binding/shared/clone.rb56
-rw-r--r--spec/ruby/core/binding/source_location_spec.rb14
-rw-r--r--spec/ruby/core/builtin_constants/builtin_constants_spec.rb149
-rw-r--r--spec/ruby/core/class/allocate_spec.rb41
-rw-r--r--spec/ruby/core/class/attached_object_spec.rb29
-rw-r--r--spec/ruby/core/class/dup_spec.rb69
-rw-r--r--spec/ruby/core/class/fixtures/classes.rb47
-rw-r--r--spec/ruby/core/class/inherited_spec.rb118
-rw-r--r--spec/ruby/core/class/initialize_spec.rb34
-rw-r--r--spec/ruby/core/class/new_spec.rb157
-rw-r--r--spec/ruby/core/class/subclasses_spec.rb85
-rw-r--r--spec/ruby/core/class/superclass_spec.rb27
-rw-r--r--spec/ruby/core/comparable/between_spec.rb25
-rw-r--r--spec/ruby/core/comparable/clamp_spec.rb223
-rw-r--r--spec/ruby/core/comparable/equal_value_spec.rb114
-rw-r--r--spec/ruby/core/comparable/fixtures/classes.rb37
-rw-r--r--spec/ruby/core/comparable/gt_spec.rb43
-rw-r--r--spec/ruby/core/comparable/gte_spec.rb47
-rw-r--r--spec/ruby/core/comparable/lt_spec.rb49
-rw-r--r--spec/ruby/core/comparable/lte_spec.rb46
-rw-r--r--spec/ruby/core/complex/abs2_spec.rb9
-rw-r--r--spec/ruby/core/complex/abs_spec.rb12
-rw-r--r--spec/ruby/core/complex/angle_spec.rb7
-rw-r--r--spec/ruby/core/complex/arg_spec.rb11
-rw-r--r--spec/ruby/core/complex/coerce_spec.rb70
-rw-r--r--spec/ruby/core/complex/comparison_spec.rb25
-rw-r--r--spec/ruby/core/complex/conj_spec.rb7
-rw-r--r--spec/ruby/core/complex/conjugate_spec.rb10
-rw-r--r--spec/ruby/core/complex/constants_spec.rb7
-rw-r--r--spec/ruby/core/complex/denominator_spec.rb13
-rw-r--r--spec/ruby/core/complex/divide_spec.rb84
-rw-r--r--spec/ruby/core/complex/eql_spec.rb31
-rw-r--r--spec/ruby/core/complex/equal_value_spec.rb93
-rw-r--r--spec/ruby/core/complex/exponent_spec.rb61
-rw-r--r--spec/ruby/core/complex/fdiv_spec.rb129
-rw-r--r--spec/ruby/core/complex/finite_spec.rb32
-rw-r--r--spec/ruby/core/complex/hash_spec.rb16
-rw-r--r--spec/ruby/core/complex/imag_spec.rb7
-rw-r--r--spec/ruby/core/complex/imaginary_spec.rb10
-rw-r--r--spec/ruby/core/complex/infinite_spec.rb32
-rw-r--r--spec/ruby/core/complex/inspect_spec.rb37
-rw-r--r--spec/ruby/core/complex/integer_spec.rb11
-rw-r--r--spec/ruby/core/complex/magnitude_spec.rb7
-rw-r--r--spec/ruby/core/complex/marshal_dump_spec.rb11
-rw-r--r--spec/ruby/core/complex/minus_spec.rb45
-rw-r--r--spec/ruby/core/complex/multiply_spec.rb49
-rw-r--r--spec/ruby/core/complex/negative_spec.rb13
-rw-r--r--spec/ruby/core/complex/numerator_spec.rb19
-rw-r--r--spec/ruby/core/complex/phase_spec.rb7
-rw-r--r--spec/ruby/core/complex/plus_spec.rb45
-rw-r--r--spec/ruby/core/complex/polar_spec.rb41
-rw-r--r--spec/ruby/core/complex/positive_spec.rb13
-rw-r--r--spec/ruby/core/complex/quo_spec.rb7
-rw-r--r--spec/ruby/core/complex/rationalize_spec.rb31
-rw-r--r--spec/ruby/core/complex/real_spec.rb28
-rw-r--r--spec/ruby/core/complex/rect_spec.rb13
-rw-r--r--spec/ruby/core/complex/rectangular_spec.rb114
-rw-r--r--spec/ruby/core/complex/to_c_spec.rb12
-rw-r--r--spec/ruby/core/complex/to_f_spec.rb41
-rw-r--r--spec/ruby/core/complex/to_i_spec.rb41
-rw-r--r--spec/ruby/core/complex/to_r_spec.rb49
-rw-r--r--spec/ruby/core/complex/to_s_spec.rb55
-rw-r--r--spec/ruby/core/complex/uminus_spec.rb11
-rw-r--r--spec/ruby/core/conditionvariable/broadcast_spec.rb39
-rw-r--r--spec/ruby/core/conditionvariable/marshal_dump_spec.rb8
-rw-r--r--spec/ruby/core/conditionvariable/signal_spec.rb76
-rw-r--r--spec/ruby/core/conditionvariable/wait_spec.rb174
-rw-r--r--spec/ruby/core/data/constants_spec.rb11
-rw-r--r--spec/ruby/core/data/deconstruct_keys_spec.rb110
-rw-r--r--spec/ruby/core/data/deconstruct_spec.rb8
-rw-r--r--spec/ruby/core/data/define_spec.rb34
-rw-r--r--spec/ruby/core/data/eql_spec.rb63
-rw-r--r--spec/ruby/core/data/equal_value_spec.rb63
-rw-r--r--spec/ruby/core/data/fixtures/classes.rb41
-rw-r--r--spec/ruby/core/data/hash_spec.rb25
-rw-r--r--spec/ruby/core/data/initialize_spec.rb204
-rw-r--r--spec/ruby/core/data/inspect_spec.rb63
-rw-r--r--spec/ruby/core/data/members_spec.rb21
-rw-r--r--spec/ruby/core/data/to_h_spec.rb63
-rw-r--r--spec/ruby/core/data/to_s_spec.rb9
-rw-r--r--spec/ruby/core/data/with_spec.rb55
-rw-r--r--spec/ruby/core/dir/chdir_spec.rb218
-rw-r--r--spec/ruby/core/dir/children_spec.rb147
-rw-r--r--spec/ruby/core/dir/chroot_spec.rb47
-rw-r--r--spec/ruby/core/dir/close_spec.rb53
-rw-r--r--spec/ruby/core/dir/delete_spec.rb15
-rw-r--r--spec/ruby/core/dir/dir_spec.rb7
-rw-r--r--spec/ruby/core/dir/each_child_spec.rb119
-rw-r--r--spec/ruby/core/dir/each_spec.rb75
-rw-r--r--spec/ruby/core/dir/element_reference_spec.rb33
-rw-r--r--spec/ruby/core/dir/empty_spec.rb31
-rw-r--r--spec/ruby/core/dir/entries_spec.rb70
-rw-r--r--spec/ruby/core/dir/exist_spec.rb74
-rw-r--r--spec/ruby/core/dir/fchdir_spec.rb71
-rw-r--r--spec/ruby/core/dir/fileno_spec.rb37
-rw-r--r--spec/ruby/core/dir/fixtures/common.rb203
-rw-r--r--spec/ruby/core/dir/for_fd_spec.rb77
-rw-r--r--spec/ruby/core/dir/foreach_spec.rb68
-rw-r--r--spec/ruby/core/dir/getwd_spec.rb7
-rw-r--r--spec/ruby/core/dir/glob_spec.rb362
-rw-r--r--spec/ruby/core/dir/home_spec.rb85
-rw-r--r--spec/ruby/core/dir/initialize_spec.rb23
-rw-r--r--spec/ruby/core/dir/inspect_spec.rb24
-rw-r--r--spec/ruby/core/dir/mkdir_spec.rb107
-rw-r--r--spec/ruby/core/dir/open_spec.rb84
-rw-r--r--spec/ruby/core/dir/path_spec.rb37
-rw-r--r--spec/ruby/core/dir/pos_spec.rb21
-rw-r--r--spec/ruby/core/dir/pwd_spec.rb80
-rw-r--r--spec/ruby/core/dir/read_spec.rb76
-rw-r--r--spec/ruby/core/dir/rewind_spec.rb36
-rw-r--r--spec/ruby/core/dir/rmdir_spec.rb15
-rw-r--r--spec/ruby/core/dir/scan_spec.rb224
-rw-r--r--spec/ruby/core/dir/seek_spec.rb19
-rw-r--r--spec/ruby/core/dir/shared/chroot.rb44
-rw-r--r--spec/ruby/core/dir/shared/closed.rb9
-rw-r--r--spec/ruby/core/dir/shared/delete.rb53
-rw-r--r--spec/ruby/core/dir/shared/glob.rb441
-rw-r--r--spec/ruby/core/dir/shared/pos.rb24
-rw-r--r--spec/ruby/core/dir/tell_spec.rb41
-rw-r--r--spec/ruby/core/dir/to_path_spec.rb7
-rw-r--r--spec/ruby/core/dir/unlink_spec.rb15
-rw-r--r--spec/ruby/core/encoding/_dump_spec.rb5
-rw-r--r--spec/ruby/core/encoding/_load_spec.rb5
-rw-r--r--spec/ruby/core/encoding/aliases_spec.rb43
-rw-r--r--spec/ruby/core/encoding/ascii_compatible_spec.rb22
-rw-r--r--spec/ruby/core/encoding/compatible_spec.rb772
-rw-r--r--spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb37
-rw-r--r--spec/ruby/core/encoding/converter/constants_spec.rb131
-rw-r--r--spec/ruby/core/encoding/converter/convert_spec.rb45
-rw-r--r--spec/ruby/core/encoding/converter/convpath_spec.rb24
-rw-r--r--spec/ruby/core/encoding/converter/destination_encoding_spec.rb11
-rw-r--r--spec/ruby/core/encoding/converter/finish_spec.rb36
-rw-r--r--spec/ruby/core/encoding/converter/insert_output_spec.rb5
-rw-r--r--spec/ruby/core/encoding/converter/inspect_spec.rb13
-rw-r--r--spec/ruby/core/encoding/converter/last_error_spec.rb91
-rw-r--r--spec/ruby/core/encoding/converter/new_spec.rb119
-rw-r--r--spec/ruby/core/encoding/converter/primitive_convert_spec.rb216
-rw-r--r--spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb69
-rw-r--r--spec/ruby/core/encoding/converter/putback_spec.rb56
-rw-r--r--spec/ruby/core/encoding/converter/replacement_spec.rb70
-rw-r--r--spec/ruby/core/encoding/converter/search_convpath_spec.rb30
-rw-r--r--spec/ruby/core/encoding/converter/source_encoding_spec.rb11
-rw-r--r--spec/ruby/core/encoding/default_external_spec.rb69
-rw-r--r--spec/ruby/core/encoding/default_internal_spec.rb74
-rw-r--r--spec/ruby/core/encoding/dummy_spec.rb25
-rw-r--r--spec/ruby/core/encoding/find_spec.rb82
-rw-r--r--spec/ruby/core/encoding/fixtures/classes.rb49
-rw-r--r--spec/ruby/core/encoding/inspect_spec.rb33
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb19
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb19
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb31
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb28
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb31
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb29
-rw-r--r--spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb34
-rw-r--r--spec/ruby/core/encoding/list_spec.rb49
-rw-r--r--spec/ruby/core/encoding/locale_charmap_spec.rb56
-rw-r--r--spec/ruby/core/encoding/name_list_spec.rb23
-rw-r--r--spec/ruby/core/encoding/name_spec.rb15
-rw-r--r--spec/ruby/core/encoding/names_spec.rb35
-rw-r--r--spec/ruby/core/encoding/replicate_spec.rb8
-rw-r--r--spec/ruby/core/encoding/to_s_spec.rb7
-rw-r--r--spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb16
-rw-r--r--spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb16
-rw-r--r--spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb28
-rw-r--r--spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb29
-rw-r--r--spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb30
-rw-r--r--spec/ruby/core/enumerable/all_spec.rb187
-rw-r--r--spec/ruby/core/enumerable/any_spec.rb200
-rw-r--r--spec/ruby/core/enumerable/chain_spec.rb23
-rw-r--r--spec/ruby/core/enumerable/chunk_spec.rb77
-rw-r--r--spec/ruby/core/enumerable/chunk_while_spec.rb42
-rw-r--r--spec/ruby/core/enumerable/collect_concat_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/collect_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/compact_spec.rb9
-rw-r--r--spec/ruby/core/enumerable/count_spec.rb59
-rw-r--r--spec/ruby/core/enumerable/cycle_spec.rb104
-rw-r--r--spec/ruby/core/enumerable/detect_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/drop_spec.rb43
-rw-r--r--spec/ruby/core/enumerable/drop_while_spec.rb50
-rw-r--r--spec/ruby/core/enumerable/each_cons_spec.rb103
-rw-r--r--spec/ruby/core/enumerable/each_entry_spec.rb41
-rw-r--r--spec/ruby/core/enumerable/each_slice_spec.rb105
-rw-r--r--spec/ruby/core/enumerable/each_with_index_spec.rb53
-rw-r--r--spec/ruby/core/enumerable/each_with_object_spec.rb41
-rw-r--r--spec/ruby/core/enumerable/entries_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/filter_map_spec.rb24
-rw-r--r--spec/ruby/core/enumerable/filter_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/find_all_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/find_index_spec.rb89
-rw-r--r--spec/ruby/core/enumerable/find_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/first_spec.rb28
-rw-r--r--spec/ruby/core/enumerable/fixtures/classes.rb350
-rw-r--r--spec/ruby/core/enumerable/flat_map_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/grep_spec.rb87
-rw-r--r--spec/ruby/core/enumerable/grep_v_spec.rb76
-rw-r--r--spec/ruby/core/enumerable/group_by_spec.rb37
-rw-r--r--spec/ruby/core/enumerable/include_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/inject_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/lazy_spec.rb10
-rw-r--r--spec/ruby/core/enumerable/map_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/max_by_spec.rb81
-rw-r--r--spec/ruby/core/enumerable/max_spec.rb119
-rw-r--r--spec/ruby/core/enumerable/member_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/min_by_spec.rb81
-rw-r--r--spec/ruby/core/enumerable/min_spec.rb123
-rw-r--r--spec/ruby/core/enumerable/minmax_by_spec.rb44
-rw-r--r--spec/ruby/core/enumerable/minmax_spec.rb20
-rw-r--r--spec/ruby/core/enumerable/none_spec.rb153
-rw-r--r--spec/ruby/core/enumerable/one_spec.rb154
-rw-r--r--spec/ruby/core/enumerable/partition_spec.rb20
-rw-r--r--spec/ruby/core/enumerable/reduce_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/reject_spec.rb25
-rw-r--r--spec/ruby/core/enumerable/reverse_each_spec.rb26
-rw-r--r--spec/ruby/core/enumerable/select_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/shared/collect.rb107
-rw-r--r--spec/ruby/core/enumerable/shared/collect_concat.rb54
-rw-r--r--spec/ruby/core/enumerable/shared/entries.rb16
-rw-r--r--spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb33
-rw-r--r--spec/ruby/core/enumerable/shared/enumeratorized.rb42
-rw-r--r--spec/ruby/core/enumerable/shared/find.rb77
-rw-r--r--spec/ruby/core/enumerable/shared/find_all.rb31
-rw-r--r--spec/ruby/core/enumerable/shared/include.rb34
-rw-r--r--spec/ruby/core/enumerable/shared/inject.rb142
-rw-r--r--spec/ruby/core/enumerable/shared/take.rb63
-rw-r--r--spec/ruby/core/enumerable/shared/value_packing.rb26
-rw-r--r--spec/ruby/core/enumerable/slice_after_spec.rb61
-rw-r--r--spec/ruby/core/enumerable/slice_before_spec.rb64
-rw-r--r--spec/ruby/core/enumerable/slice_when_spec.rb54
-rw-r--r--spec/ruby/core/enumerable/sort_by_spec.rb43
-rw-r--r--spec/ruby/core/enumerable/sort_spec.rb54
-rw-r--r--spec/ruby/core/enumerable/sum_spec.rb50
-rw-r--r--spec/ruby/core/enumerable/take_spec.rb21
-rw-r--r--spec/ruby/core/enumerable/take_while_spec.rb51
-rw-r--r--spec/ruby/core/enumerable/tally_spec.rb91
-rw-r--r--spec/ruby/core/enumerable/to_a_spec.rb7
-rw-r--r--spec/ruby/core/enumerable/to_h_spec.rb96
-rw-r--r--spec/ruby/core/enumerable/to_set_spec.rb30
-rw-r--r--spec/ruby/core/enumerable/uniq_spec.rb78
-rw-r--r--spec/ruby/core/enumerable/zip_spec.rb46
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb16
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb17
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb16
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb18
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb17
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb9
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb20
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb20
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb9
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb17
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb17
-rw-r--r--spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb11
-rw-r--r--spec/ruby/core/enumerator/chain/each_spec.rb15
-rw-r--r--spec/ruby/core/enumerator/chain/initialize_spec.rb31
-rw-r--r--spec/ruby/core/enumerator/chain/inspect_spec.rb18
-rw-r--r--spec/ruby/core/enumerator/chain/rewind_spec.rb51
-rw-r--r--spec/ruby/core/enumerator/chain/size_spec.rb22
-rw-r--r--spec/ruby/core/enumerator/each_spec.rb104
-rw-r--r--spec/ruby/core/enumerator/each_with_index_spec.rb36
-rw-r--r--spec/ruby/core/enumerator/each_with_object_spec.rb6
-rw-r--r--spec/ruby/core/enumerator/enum_for_spec.rb6
-rw-r--r--spec/ruby/core/enumerator/enumerator_spec.rb7
-rw-r--r--spec/ruby/core/enumerator/feed_spec.rb52
-rw-r--r--spec/ruby/core/enumerator/first_spec.rb7
-rw-r--r--spec/ruby/core/enumerator/fixtures/classes.rb15
-rw-r--r--spec/ruby/core/enumerator/fixtures/common.rb9
-rw-r--r--spec/ruby/core/enumerator/initialize_spec.rb57
-rw-r--r--spec/ruby/core/enumerator/inspect_spec.rb22
-rw-r--r--spec/ruby/core/enumerator/lazy/chunk_spec.rb67
-rw-r--r--spec/ruby/core/enumerator/lazy/chunk_while_spec.rb14
-rw-r--r--spec/ruby/core/enumerator/lazy/collect_concat_spec.rb8
-rw-r--r--spec/ruby/core/enumerator/lazy/collect_spec.rb8
-rw-r--r--spec/ruby/core/enumerator/lazy/compact_spec.rb14
-rw-r--r--spec/ruby/core/enumerator/lazy/drop_spec.rb58
-rw-r--r--spec/ruby/core/enumerator/lazy/drop_while_spec.rb66
-rw-r--r--spec/ruby/core/enumerator/lazy/eager_spec.rb27
-rw-r--r--spec/ruby/core/enumerator/lazy/enum_for_spec.rb8
-rw-r--r--spec/ruby/core/enumerator/lazy/filter_map_spec.rb14
-rw-r--r--spec/ruby/core/enumerator/lazy/filter_spec.rb6
-rw-r--r--spec/ruby/core/enumerator/lazy/find_all_spec.rb8
-rw-r--r--spec/ruby/core/enumerator/lazy/fixtures/classes.rb54
-rw-r--r--spec/ruby/core/enumerator/lazy/flat_map_spec.rb16
-rw-r--r--spec/ruby/core/enumerator/lazy/force_spec.rb36
-rw-r--r--spec/ruby/core/enumerator/lazy/grep_spec.rb121
-rw-r--r--spec/ruby/core/enumerator/lazy/grep_v_spec.rb123
-rw-r--r--spec/ruby/core/enumerator/lazy/initialize_spec.rb63
-rw-r--r--spec/ruby/core/enumerator/lazy/lazy_spec.rb27
-rw-r--r--spec/ruby/core/enumerator/lazy/map_spec.rb12
-rw-r--r--spec/ruby/core/enumerator/lazy/reject_spec.rb78
-rw-r--r--spec/ruby/core/enumerator/lazy/select_spec.rb47
-rw-r--r--spec/ruby/core/enumerator/lazy/shared/collect.rb62
-rw-r--r--spec/ruby/core/enumerator/lazy/shared/collect_concat.rb78
-rw-r--r--spec/ruby/core/enumerator/lazy/shared/select.rb66
-rw-r--r--spec/ruby/core/enumerator/lazy/shared/to_enum.rb55
-rw-r--r--spec/ruby/core/enumerator/lazy/slice_after_spec.rb14
-rw-r--r--spec/ruby/core/enumerator/lazy/slice_before_spec.rb14
-rw-r--r--spec/ruby/core/enumerator/lazy/slice_when_spec.rb14
-rw-r--r--spec/ruby/core/enumerator/lazy/take_spec.rb74
-rw-r--r--spec/ruby/core/enumerator/lazy/take_while_spec.rb60
-rw-r--r--spec/ruby/core/enumerator/lazy/to_enum_spec.rb8
-rw-r--r--spec/ruby/core/enumerator/lazy/uniq_spec.rb74
-rw-r--r--spec/ruby/core/enumerator/lazy/with_index_spec.rb36
-rw-r--r--spec/ruby/core/enumerator/lazy/zip_spec.rb86
-rw-r--r--spec/ruby/core/enumerator/new_spec.rb115
-rw-r--r--spec/ruby/core/enumerator/next_spec.rb38
-rw-r--r--spec/ruby/core/enumerator/next_values_spec.rb61
-rw-r--r--spec/ruby/core/enumerator/peek_spec.rb36
-rw-r--r--spec/ruby/core/enumerator/peek_values_spec.rb63
-rw-r--r--spec/ruby/core/enumerator/plus_spec.rb33
-rw-r--r--spec/ruby/core/enumerator/produce_spec.rb78
-rw-r--r--spec/ruby/core/enumerator/product/each_spec.rb85
-rw-r--r--spec/ruby/core/enumerator/product/initialize_copy_spec.rb52
-rw-r--r--spec/ruby/core/enumerator/product/initialize_spec.rb31
-rw-r--r--spec/ruby/core/enumerator/product/inspect_spec.rb20
-rw-r--r--spec/ruby/core/enumerator/product/rewind_spec.rb62
-rw-r--r--spec/ruby/core/enumerator/product/size_spec.rb64
-rw-r--r--spec/ruby/core/enumerator/product_spec.rb91
-rw-r--r--spec/ruby/core/enumerator/rewind_spec.rb70
-rw-r--r--spec/ruby/core/enumerator/shared/enum_for.rb57
-rw-r--r--spec/ruby/core/enumerator/shared/with_index.rb33
-rw-r--r--spec/ruby/core/enumerator/shared/with_object.rb42
-rw-r--r--spec/ruby/core/enumerator/size_spec.rb26
-rw-r--r--spec/ruby/core/enumerator/to_enum_spec.rb6
-rw-r--r--spec/ruby/core/enumerator/with_index_spec.rb89
-rw-r--r--spec/ruby/core/enumerator/with_object_spec.rb6
-rw-r--r--spec/ruby/core/env/assoc_spec.rb31
-rw-r--r--spec/ruby/core/env/clear_spec.rb20
-rw-r--r--spec/ruby/core/env/clone_spec.rb21
-rw-r--r--spec/ruby/core/env/delete_if_spec.rb54
-rw-r--r--spec/ruby/core/env/delete_spec.rb55
-rw-r--r--spec/ruby/core/env/dup_spec.rb9
-rw-r--r--spec/ruby/core/env/each_key_spec.rb34
-rw-r--r--spec/ruby/core/env/each_pair_spec.rb6
-rw-r--r--spec/ruby/core/env/each_spec.rb6
-rw-r--r--spec/ruby/core/env/each_value_spec.rb34
-rw-r--r--spec/ruby/core/env/element_reference_spec.rb76
-rw-r--r--spec/ruby/core/env/element_set_spec.rb6
-rw-r--r--spec/ruby/core/env/empty_spec.rb23
-rw-r--r--spec/ruby/core/env/except_spec.rb34
-rw-r--r--spec/ruby/core/env/fetch_spec.rb63
-rw-r--r--spec/ruby/core/env/fetch_values_spec.rb51
-rw-r--r--spec/ruby/core/env/filter_spec.rb13
-rw-r--r--spec/ruby/core/env/fixtures/common.rb9
-rw-r--r--spec/ruby/core/env/has_key_spec.rb6
-rw-r--r--spec/ruby/core/env/has_value_spec.rb6
-rw-r--r--spec/ruby/core/env/include_spec.rb6
-rw-r--r--spec/ruby/core/env/inspect_spec.rb11
-rw-r--r--spec/ruby/core/env/invert_spec.rb16
-rw-r--r--spec/ruby/core/env/keep_if_spec.rb54
-rw-r--r--spec/ruby/core/env/key_spec.rb39
-rw-r--r--spec/ruby/core/env/keys_spec.rb14
-rw-r--r--spec/ruby/core/env/length_spec.rb6
-rw-r--r--spec/ruby/core/env/member_spec.rb6
-rw-r--r--spec/ruby/core/env/merge_spec.rb6
-rw-r--r--spec/ruby/core/env/rassoc_spec.rb42
-rw-r--r--spec/ruby/core/env/rehash_spec.rb7
-rw-r--r--spec/ruby/core/env/reject_spec.rb101
-rw-r--r--spec/ruby/core/env/replace_spec.rb51
-rw-r--r--spec/ruby/core/env/select_spec.rb13
-rw-r--r--spec/ruby/core/env/shared/each.rb65
-rw-r--r--spec/ruby/core/env/shared/include.rb30
-rw-r--r--spec/ruby/core/env/shared/length.rb13
-rw-r--r--spec/ruby/core/env/shared/select.rb61
-rw-r--r--spec/ruby/core/env/shared/store.rb60
-rw-r--r--spec/ruby/core/env/shared/to_hash.rb33
-rw-r--r--spec/ruby/core/env/shared/update.rb104
-rw-r--r--spec/ruby/core/env/shared/value.rb29
-rw-r--r--spec/ruby/core/env/shift_spec.rb47
-rw-r--r--spec/ruby/core/env/size_spec.rb6
-rw-r--r--spec/ruby/core/env/slice_spec.rb37
-rw-r--r--spec/ruby/core/env/spec_helper.rb26
-rw-r--r--spec/ruby/core/env/store_spec.rb6
-rw-r--r--spec/ruby/core/env/to_a_spec.rb21
-rw-r--r--spec/ruby/core/env/to_h_spec.rb70
-rw-r--r--spec/ruby/core/env/to_hash_spec.rb6
-rw-r--r--spec/ruby/core/env/to_s_spec.rb7
-rw-r--r--spec/ruby/core/env/update_spec.rb6
-rw-r--r--spec/ruby/core/env/value_spec.rb6
-rw-r--r--spec/ruby/core/env/values_at_spec.rb38
-rw-r--r--spec/ruby/core/env/values_spec.rb14
-rw-r--r--spec/ruby/core/exception/backtrace_locations_spec.rb39
-rw-r--r--spec/ruby/core/exception/backtrace_spec.rb106
-rw-r--r--spec/ruby/core/exception/case_compare_spec.rb37
-rw-r--r--spec/ruby/core/exception/cause_spec.rb38
-rw-r--r--spec/ruby/core/exception/detailed_message_spec.rb50
-rw-r--r--spec/ruby/core/exception/dup_spec.rb74
-rw-r--r--spec/ruby/core/exception/equal_value_spec.rb68
-rw-r--r--spec/ruby/core/exception/errno_spec.rb69
-rw-r--r--spec/ruby/core/exception/exception_spec.rb69
-rw-r--r--spec/ruby/core/exception/exit_value_spec.rb13
-rw-r--r--spec/ruby/core/exception/fixtures/common.rb102
-rw-r--r--spec/ruby/core/exception/fixtures/syntax_error.rb3
-rw-r--r--spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb22
-rw-r--r--spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb25
-rw-r--r--spec/ruby/core/exception/frozen_error_spec.rb54
-rw-r--r--spec/ruby/core/exception/full_message_spec.rb226
-rw-r--r--spec/ruby/core/exception/hierarchy_spec.rb62
-rw-r--r--spec/ruby/core/exception/inspect_spec.rb24
-rw-r--r--spec/ruby/core/exception/interrupt_spec.rb60
-rw-r--r--spec/ruby/core/exception/io_error_spec.rb45
-rw-r--r--spec/ruby/core/exception/key_error_spec.rb19
-rw-r--r--spec/ruby/core/exception/load_error_spec.rb21
-rw-r--r--spec/ruby/core/exception/message_spec.rb27
-rw-r--r--spec/ruby/core/exception/name_error_spec.rb28
-rw-r--r--spec/ruby/core/exception/name_spec.rb43
-rw-r--r--spec/ruby/core/exception/new_spec.rb7
-rw-r--r--spec/ruby/core/exception/no_method_error_spec.rb224
-rw-r--r--spec/ruby/core/exception/reason_spec.rb13
-rw-r--r--spec/ruby/core/exception/receiver_spec.rb58
-rw-r--r--spec/ruby/core/exception/result_spec.rb21
-rw-r--r--spec/ruby/core/exception/set_backtrace_spec.rb23
-rw-r--r--spec/ruby/core/exception/shared/new.rb18
-rw-r--r--spec/ruby/core/exception/shared/set_backtrace.rb64
-rw-r--r--spec/ruby/core/exception/signal_exception_spec.rb123
-rw-r--r--spec/ruby/core/exception/signm_spec.rb9
-rw-r--r--spec/ruby/core/exception/signo_spec.rb9
-rw-r--r--spec/ruby/core/exception/standard_error_spec.rb23
-rw-r--r--spec/ruby/core/exception/status_spec.rb9
-rw-r--r--spec/ruby/core/exception/success_spec.rb15
-rw-r--r--spec/ruby/core/exception/syntax_error_spec.rb25
-rw-r--r--spec/ruby/core/exception/system_call_error_spec.rb163
-rw-r--r--spec/ruby/core/exception/system_exit_spec.rb59
-rw-r--r--spec/ruby/core/exception/to_s_spec.rb37
-rw-r--r--spec/ruby/core/exception/top_level_spec.rb65
-rw-r--r--spec/ruby/core/exception/uncaught_throw_error_spec.rb12
-rw-r--r--spec/ruby/core/false/and_spec.rb11
-rw-r--r--spec/ruby/core/false/case_compare_spec.rb14
-rw-r--r--spec/ruby/core/false/dup_spec.rb7
-rw-r--r--spec/ruby/core/false/falseclass_spec.rb15
-rw-r--r--spec/ruby/core/false/inspect_spec.rb7
-rw-r--r--spec/ruby/core/false/or_spec.rb11
-rw-r--r--spec/ruby/core/false/singleton_method_spec.rb13
-rw-r--r--spec/ruby/core/false/to_s_spec.rb15
-rw-r--r--spec/ruby/core/false/xor_spec.rb11
-rw-r--r--spec/ruby/core/fiber/alive_spec.rb44
-rw-r--r--spec/ruby/core/fiber/blocking_spec.rb73
-rw-r--r--spec/ruby/core/fiber/current_spec.rb50
-rw-r--r--spec/ruby/core/fiber/fixtures/classes.rb22
-rw-r--r--spec/ruby/core/fiber/fixtures/scheduler.rb35
-rw-r--r--spec/ruby/core/fiber/inspect_spec.rb35
-rw-r--r--spec/ruby/core/fiber/kill_spec.rb88
-rw-r--r--spec/ruby/core/fiber/new_spec.rb39
-rw-r--r--spec/ruby/core/fiber/raise_spec.rb141
-rw-r--r--spec/ruby/core/fiber/resume_spec.rb83
-rw-r--r--spec/ruby/core/fiber/scheduler_spec.rb8
-rw-r--r--spec/ruby/core/fiber/set_scheduler_spec.rb8
-rw-r--r--spec/ruby/core/fiber/shared/blocking.rb41
-rw-r--r--spec/ruby/core/fiber/shared/resume.rb58
-rw-r--r--spec/ruby/core/fiber/shared/scheduler.rb51
-rw-r--r--spec/ruby/core/fiber/storage_spec.rb177
-rw-r--r--spec/ruby/core/fiber/transfer_spec.rb84
-rw-r--r--spec/ruby/core/fiber/yield_spec.rb49
-rw-r--r--spec/ruby/core/file/absolute_path_spec.rb94
-rw-r--r--spec/ruby/core/file/atime_spec.rb60
-rw-r--r--spec/ruby/core/file/basename_spec.rb205
-rw-r--r--spec/ruby/core/file/birthtime_spec.rb56
-rw-r--r--spec/ruby/core/file/blockdev_spec.rb6
-rw-r--r--spec/ruby/core/file/chardev_spec.rb6
-rw-r--r--spec/ruby/core/file/chmod_spec.rb185
-rw-r--r--spec/ruby/core/file/chown_spec.rb144
-rw-r--r--spec/ruby/core/file/constants/constants_spec.rb31
-rw-r--r--spec/ruby/core/file/constants_spec.rb141
-rw-r--r--spec/ruby/core/file/ctime_spec.rb54
-rw-r--r--spec/ruby/core/file/delete_spec.rb6
-rw-r--r--spec/ruby/core/file/directory_spec.rb10
-rw-r--r--spec/ruby/core/file/dirname_spec.rb170
-rw-r--r--spec/ruby/core/file/empty_spec.rb7
-rw-r--r--spec/ruby/core/file/executable_real_spec.rb7
-rw-r--r--spec/ruby/core/file/executable_spec.rb7
-rw-r--r--spec/ruby/core/file/exist_spec.rb12
-rw-r--r--spec/ruby/core/file/expand_path_spec.rb265
-rw-r--r--spec/ruby/core/file/extname_spec.rb76
-rw-r--r--spec/ruby/core/file/file_spec.rb16
-rw-r--r--spec/ruby/core/file/fixtures/common.rb22
-rw-r--r--spec/ruby/core/file/fixtures/do_not_remove1
-rw-r--r--spec/ruby/core/file/fixtures/file_types.rb66
-rw-r--r--spec/ruby/core/file/flock_spec.rb74
-rw-r--r--spec/ruby/core/file/fnmatch_spec.rb10
-rw-r--r--spec/ruby/core/file/ftype_spec.rb82
-rw-r--r--spec/ruby/core/file/grpowned_spec.rb10
-rw-r--r--spec/ruby/core/file/identical_spec.rb6
-rw-r--r--spec/ruby/core/file/initialize_spec.rb19
-rw-r--r--spec/ruby/core/file/inspect_spec.rb17
-rw-r--r--spec/ruby/core/file/join_spec.rb148
-rw-r--r--spec/ruby/core/file/lchmod_spec.rb32
-rw-r--r--spec/ruby/core/file/lchown_spec.rb59
-rw-r--r--spec/ruby/core/file/link_spec.rb39
-rw-r--r--spec/ruby/core/file/lstat_spec.rb33
-rw-r--r--spec/ruby/core/file/lutime_spec.rb43
-rw-r--r--spec/ruby/core/file/mkfifo_spec.rb51
-rw-r--r--spec/ruby/core/file/mtime_spec.rb56
-rw-r--r--spec/ruby/core/file/new_spec.rb223
-rw-r--r--spec/ruby/core/file/null_spec.rb15
-rw-r--r--spec/ruby/core/file/open_spec.rb713
-rw-r--r--spec/ruby/core/file/owned_spec.rb35
-rw-r--r--spec/ruby/core/file/path_spec.rb81
-rw-r--r--spec/ruby/core/file/pipe_spec.rb32
-rw-r--r--spec/ruby/core/file/printf_spec.rb18
-rw-r--r--spec/ruby/core/file/read_spec.rb6
-rw-r--r--spec/ruby/core/file/readable_real_spec.rb7
-rw-r--r--spec/ruby/core/file/readable_spec.rb7
-rw-r--r--spec/ruby/core/file/readlink_spec.rb86
-rw-r--r--spec/ruby/core/file/realdirpath_spec.rb104
-rw-r--r--spec/ruby/core/file/realpath_spec.rb98
-rw-r--r--spec/ruby/core/file/rename_spec.rb37
-rw-r--r--spec/ruby/core/file/reopen_spec.rb32
-rw-r--r--spec/ruby/core/file/setgid_spec.rb36
-rw-r--r--spec/ruby/core/file/setuid_spec.rb34
-rw-r--r--spec/ruby/core/file/shared/fnmatch.rb294
-rw-r--r--spec/ruby/core/file/shared/open.rb12
-rw-r--r--spec/ruby/core/file/shared/path.rb82
-rw-r--r--spec/ruby/core/file/shared/read.rb15
-rw-r--r--spec/ruby/core/file/shared/stat.rb32
-rw-r--r--spec/ruby/core/file/shared/unlink.rb61
-rw-r--r--spec/ruby/core/file/shared/update_time.rb105
-rw-r--r--spec/ruby/core/file/size_spec.rb119
-rw-r--r--spec/ruby/core/file/socket_spec.rb10
-rw-r--r--spec/ruby/core/file/split_spec.rb64
-rw-r--r--spec/ruby/core/file/stat/atime_spec.rb18
-rw-r--r--spec/ruby/core/file/stat/birthtime_spec.rb29
-rw-r--r--spec/ruby/core/file/stat/blksize_spec.rb27
-rw-r--r--spec/ruby/core/file/stat/blockdev_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/blocks_spec.rb27
-rw-r--r--spec/ruby/core/file/stat/chardev_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/comparison_spec.rb66
-rw-r--r--spec/ruby/core/file/stat/ctime_spec.rb18
-rw-r--r--spec/ruby/core/file/stat/dev_major_spec.rb23
-rw-r--r--spec/ruby/core/file/stat/dev_minor_spec.rb23
-rw-r--r--spec/ruby/core/file/stat/dev_spec.rb15
-rw-r--r--spec/ruby/core/file/stat/directory_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/executable_real_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/executable_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/file_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/fixtures/classes.rb5
-rw-r--r--spec/ruby/core/file/stat/ftype_spec.rb64
-rw-r--r--spec/ruby/core/file/stat/gid_spec.rb19
-rw-r--r--spec/ruby/core/file/stat/grpowned_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/ino_spec.rb28
-rw-r--r--spec/ruby/core/file/stat/inspect_spec.rb26
-rw-r--r--spec/ruby/core/file/stat/mode_spec.rb19
-rw-r--r--spec/ruby/core/file/stat/mtime_spec.rb18
-rw-r--r--spec/ruby/core/file/stat/new_spec.rb32
-rw-r--r--spec/ruby/core/file/stat/nlink_spec.rb21
-rw-r--r--spec/ruby/core/file/stat/owned_spec.rb33
-rw-r--r--spec/ruby/core/file/stat/pipe_spec.rb32
-rw-r--r--spec/ruby/core/file/stat/rdev_major_spec.rb24
-rw-r--r--spec/ruby/core/file/stat/rdev_minor_spec.rb24
-rw-r--r--spec/ruby/core/file/stat/rdev_spec.rb15
-rw-r--r--spec/ruby/core/file/stat/readable_real_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/readable_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/setgid_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/setuid_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/size_spec.rb21
-rw-r--r--spec/ruby/core/file/stat/socket_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/sticky_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/symlink_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/uid_spec.rb18
-rw-r--r--spec/ruby/core/file/stat/world_readable_spec.rb11
-rw-r--r--spec/ruby/core/file/stat/world_writable_spec.rb11
-rw-r--r--spec/ruby/core/file/stat/writable_real_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/writable_spec.rb7
-rw-r--r--spec/ruby/core/file/stat/zero_spec.rb7
-rw-r--r--spec/ruby/core/file/stat_spec.rb55
-rw-r--r--spec/ruby/core/file/sticky_spec.rb50
-rw-r--r--spec/ruby/core/file/symlink_spec.rb57
-rw-r--r--spec/ruby/core/file/to_path_spec.rb6
-rw-r--r--spec/ruby/core/file/truncate_spec.rb177
-rw-r--r--spec/ruby/core/file/umask_spec.rb57
-rw-r--r--spec/ruby/core/file/unlink_spec.rb6
-rw-r--r--spec/ruby/core/file/utime_spec.rb6
-rw-r--r--spec/ruby/core/file/world_readable_spec.rb12
-rw-r--r--spec/ruby/core/file/world_writable_spec.rb12
-rw-r--r--spec/ruby/core/file/writable_real_spec.rb7
-rw-r--r--spec/ruby/core/file/writable_spec.rb7
-rw-r--r--spec/ruby/core/file/zero_spec.rb7
-rw-r--r--spec/ruby/core/filetest/blockdev_spec.rb6
-rw-r--r--spec/ruby/core/filetest/chardev_spec.rb6
-rw-r--r--spec/ruby/core/filetest/directory_spec.rb10
-rw-r--r--spec/ruby/core/filetest/executable_real_spec.rb7
-rw-r--r--spec/ruby/core/filetest/executable_spec.rb7
-rw-r--r--spec/ruby/core/filetest/exist_spec.rb12
-rw-r--r--spec/ruby/core/filetest/file_spec.rb10
-rw-r--r--spec/ruby/core/filetest/grpowned_spec.rb10
-rw-r--r--spec/ruby/core/filetest/identical_spec.rb6
-rw-r--r--spec/ruby/core/filetest/owned_spec.rb6
-rw-r--r--spec/ruby/core/filetest/pipe_spec.rb6
-rw-r--r--spec/ruby/core/filetest/readable_real_spec.rb7
-rw-r--r--spec/ruby/core/filetest/readable_spec.rb7
-rw-r--r--spec/ruby/core/filetest/setgid_spec.rb6
-rw-r--r--spec/ruby/core/filetest/setuid_spec.rb6
-rw-r--r--spec/ruby/core/filetest/size_spec.rb34
-rw-r--r--spec/ruby/core/filetest/socket_spec.rb10
-rw-r--r--spec/ruby/core/filetest/sticky_spec.rb7
-rw-r--r--spec/ruby/core/filetest/symlink_spec.rb10
-rw-r--r--spec/ruby/core/filetest/world_readable_spec.rb5
-rw-r--r--spec/ruby/core/filetest/world_writable_spec.rb5
-rw-r--r--spec/ruby/core/filetest/writable_real_spec.rb7
-rw-r--r--spec/ruby/core/filetest/writable_spec.rb7
-rw-r--r--spec/ruby/core/filetest/zero_spec.rb7
-rw-r--r--spec/ruby/core/float/abs_spec.rb6
-rw-r--r--spec/ruby/core/float/angle_spec.rb6
-rw-r--r--spec/ruby/core/float/arg_spec.rb6
-rw-r--r--spec/ruby/core/float/case_compare_spec.rb6
-rw-r--r--spec/ruby/core/float/ceil_spec.rb28
-rw-r--r--spec/ruby/core/float/coerce_spec.rb18
-rw-r--r--spec/ruby/core/float/comparison_spec.rb113
-rw-r--r--spec/ruby/core/float/constants_spec.rb55
-rw-r--r--spec/ruby/core/float/denominator_spec.rb29
-rw-r--r--spec/ruby/core/float/divide_spec.rb43
-rw-r--r--spec/ruby/core/float/divmod_spec.rb43
-rw-r--r--spec/ruby/core/float/dup_spec.rb8
-rw-r--r--spec/ruby/core/float/eql_spec.rb16
-rw-r--r--spec/ruby/core/float/equal_value_spec.rb6
-rw-r--r--spec/ruby/core/float/exponent_spec.rb15
-rw-r--r--spec/ruby/core/float/fdiv_spec.rb6
-rw-r--r--spec/ruby/core/float/finite_spec.rb19
-rw-r--r--spec/ruby/core/float/fixtures/classes.rb4
-rw-r--r--spec/ruby/core/float/fixtures/coerce.rb15
-rw-r--r--spec/ruby/core/float/float_spec.rb19
-rw-r--r--spec/ruby/core/float/floor_spec.rb28
-rw-r--r--spec/ruby/core/float/gt_spec.rb38
-rw-r--r--spec/ruby/core/float/gte_spec.rb38
-rw-r--r--spec/ruby/core/float/hash_spec.rb11
-rw-r--r--spec/ruby/core/float/infinite_spec.rb19
-rw-r--r--spec/ruby/core/float/inspect_spec.rb6
-rw-r--r--spec/ruby/core/float/lt_spec.rb38
-rw-r--r--spec/ruby/core/float/lte_spec.rb39
-rw-r--r--spec/ruby/core/float/magnitude_spec.rb6
-rw-r--r--spec/ruby/core/float/minus_spec.rb12
-rw-r--r--spec/ruby/core/float/modulo_spec.rb10
-rw-r--r--spec/ruby/core/float/multiply_spec.rb17
-rw-r--r--spec/ruby/core/float/nan_spec.rb9
-rw-r--r--spec/ruby/core/float/negative_spec.rb33
-rw-r--r--spec/ruby/core/float/next_float_spec.rb49
-rw-r--r--spec/ruby/core/float/numerator_spec.rb39
-rw-r--r--spec/ruby/core/float/phase_spec.rb6
-rw-r--r--spec/ruby/core/float/plus_spec.rb12
-rw-r--r--spec/ruby/core/float/positive_spec.rb33
-rw-r--r--spec/ruby/core/float/prev_float_spec.rb49
-rw-r--r--spec/ruby/core/float/quo_spec.rb6
-rw-r--r--spec/ruby/core/float/rationalize_spec.rb43
-rw-r--r--spec/ruby/core/float/round_spec.rb208
-rw-r--r--spec/ruby/core/float/shared/abs.rb21
-rw-r--r--spec/ruby/core/float/shared/arg.rb36
-rw-r--r--spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb11
-rw-r--r--spec/ruby/core/float/shared/comparison_exception_in_coerce.rb11
-rw-r--r--spec/ruby/core/float/shared/equal.rb38
-rw-r--r--spec/ruby/core/float/shared/modulo.rb48
-rw-r--r--spec/ruby/core/float/shared/quo.rb59
-rw-r--r--spec/ruby/core/float/shared/to_i.rb14
-rw-r--r--spec/ruby/core/float/shared/to_s.rb308
-rw-r--r--spec/ruby/core/float/to_f_spec.rb9
-rw-r--r--spec/ruby/core/float/to_i_spec.rb6
-rw-r--r--spec/ruby/core/float/to_int_spec.rb6
-rw-r--r--spec/ruby/core/float/to_r_spec.rb5
-rw-r--r--spec/ruby/core/float/to_s_spec.rb6
-rw-r--r--spec/ruby/core/float/truncate_spec.rb14
-rw-r--r--spec/ruby/core/float/uminus_spec.rb28
-rw-r--r--spec/ruby/core/float/uplus_spec.rb9
-rw-r--r--spec/ruby/core/float/zero_spec.rb9
-rw-r--r--spec/ruby/core/gc/auto_compact_spec.rb24
-rw-r--r--spec/ruby/core/gc/config_spec.rb97
-rw-r--r--spec/ruby/core/gc/count_spec.rb17
-rw-r--r--spec/ruby/core/gc/disable_spec.rb18
-rw-r--r--spec/ruby/core/gc/enable_spec.rb13
-rw-r--r--spec/ruby/core/gc/garbage_collect_spec.rb15
-rw-r--r--spec/ruby/core/gc/measure_total_time_spec.rb17
-rw-r--r--spec/ruby/core/gc/profiler/clear_spec.rb5
-rw-r--r--spec/ruby/core/gc/profiler/disable_spec.rb16
-rw-r--r--spec/ruby/core/gc/profiler/enable_spec.rb17
-rw-r--r--spec/ruby/core/gc/profiler/enabled_spec.rb21
-rw-r--r--spec/ruby/core/gc/profiler/report_spec.rb5
-rw-r--r--spec/ruby/core/gc/profiler/result_spec.rb7
-rw-r--r--spec/ruby/core/gc/profiler/total_time_spec.rb7
-rw-r--r--spec/ruby/core/gc/start_spec.rb12
-rw-r--r--spec/ruby/core/gc/stat_spec.rb62
-rw-r--r--spec/ruby/core/gc/stress_spec.rb27
-rw-r--r--spec/ruby/core/gc/total_time_spec.rb13
-rw-r--r--spec/ruby/core/hash/allocate_spec.rb15
-rw-r--r--spec/ruby/core/hash/any_spec.rb30
-rw-r--r--spec/ruby/core/hash/assoc_spec.rb50
-rw-r--r--spec/ruby/core/hash/clear_spec.rb32
-rw-r--r--spec/ruby/core/hash/clone_spec.rb12
-rw-r--r--spec/ruby/core/hash/compact_spec.rb81
-rw-r--r--spec/ruby/core/hash/compare_by_identity_spec.rb147
-rw-r--r--spec/ruby/core/hash/constructor_spec.rb127
-rw-r--r--spec/ruby/core/hash/deconstruct_keys_spec.rb23
-rw-r--r--spec/ruby/core/hash/default_proc_spec.rb80
-rw-r--r--spec/ruby/core/hash/default_spec.rb46
-rw-r--r--spec/ruby/core/hash/delete_if_spec.rb44
-rw-r--r--spec/ruby/core/hash/delete_spec.rb58
-rw-r--r--spec/ruby/core/hash/dig_spec.rb66
-rw-r--r--spec/ruby/core/hash/each_key_spec.rb23
-rw-r--r--spec/ruby/core/hash/each_pair_spec.rb111
-rw-r--r--spec/ruby/core/hash/each_spec.rb7
-rw-r--r--spec/ruby/core/hash/each_value_spec.rb23
-rw-r--r--spec/ruby/core/hash/element_reference_spec.rb134
-rw-r--r--spec/ruby/core/hash/element_set_spec.rb121
-rw-r--r--spec/ruby/core/hash/empty_spec.rb15
-rw-r--r--spec/ruby/core/hash/eql_spec.rb9
-rw-r--r--spec/ruby/core/hash/equal_value_spec.rb18
-rw-r--r--spec/ruby/core/hash/except_spec.rb42
-rw-r--r--spec/ruby/core/hash/fetch_spec.rb44
-rw-r--r--spec/ruby/core/hash/fetch_values_spec.rb35
-rw-r--r--spec/ruby/core/hash/filter_spec.rb13
-rw-r--r--spec/ruby/core/hash/fixtures/classes.rb75
-rw-r--r--spec/ruby/core/hash/fixtures/name.rb30
-rw-r--r--spec/ruby/core/hash/flatten_spec.rb62
-rw-r--r--spec/ruby/core/hash/gt_spec.rb42
-rw-r--r--spec/ruby/core/hash/gte_spec.rb42
-rw-r--r--spec/ruby/core/hash/has_key_spec.rb7
-rw-r--r--spec/ruby/core/hash/has_value_spec.rb16
-rw-r--r--spec/ruby/core/hash/hash_spec.rb51
-rw-r--r--spec/ruby/core/hash/include_spec.rb40
-rw-r--r--spec/ruby/core/hash/initialize_spec.rb61
-rw-r--r--spec/ruby/core/hash/inspect_spec.rb123
-rw-r--r--spec/ruby/core/hash/invert_spec.rb48
-rw-r--r--spec/ruby/core/hash/keep_if_spec.rb37
-rw-r--r--spec/ruby/core/hash/key_spec.rb32
-rw-r--r--spec/ruby/core/hash/keys_spec.rb23
-rw-r--r--spec/ruby/core/hash/length_spec.rb7
-rw-r--r--spec/ruby/core/hash/lt_spec.rb42
-rw-r--r--spec/ruby/core/hash/lte_spec.rb42
-rw-r--r--spec/ruby/core/hash/member_spec.rb7
-rw-r--r--spec/ruby/core/hash/merge_spec.rb123
-rw-r--r--spec/ruby/core/hash/new_spec.rb68
-rw-r--r--spec/ruby/core/hash/rassoc_spec.rb42
-rw-r--r--spec/ruby/core/hash/rehash_spec.rb114
-rw-r--r--spec/ruby/core/hash/reject_spec.rb116
-rw-r--r--spec/ruby/core/hash/replace_spec.rb79
-rw-r--r--spec/ruby/core/hash/ruby2_keywords_hash_spec.rb81
-rw-r--r--spec/ruby/core/hash/select_spec.rb112
-rw-r--r--spec/ruby/core/hash/shared/comparison.rb15
-rw-r--r--spec/ruby/core/hash/shared/eql.rb204
-rw-r--r--spec/ruby/core/hash/shared/greater_than.rb23
-rw-r--r--spec/ruby/core/hash/shared/iteration.rb19
-rw-r--r--spec/ruby/core/hash/shared/less_than.rb23
-rw-r--r--spec/ruby/core/hash/shift_spec.rb78
-rw-r--r--spec/ruby/core/hash/size_spec.rb14
-rw-r--r--spec/ruby/core/hash/slice_spec.rb74
-rw-r--r--spec/ruby/core/hash/sort_spec.rb17
-rw-r--r--spec/ruby/core/hash/store_spec.rb7
-rw-r--r--spec/ruby/core/hash/to_a_spec.rb29
-rw-r--r--spec/ruby/core/hash/to_h_spec.rb106
-rw-r--r--spec/ruby/core/hash/to_hash_spec.rb14
-rw-r--r--spec/ruby/core/hash/to_proc_spec.rb91
-rw-r--r--spec/ruby/core/hash/to_s_spec.rb7
-rw-r--r--spec/ruby/core/hash/transform_keys_spec.rb150
-rw-r--r--spec/ruby/core/hash/transform_values_spec.rb118
-rw-r--r--spec/ruby/core/hash/try_convert_spec.rb50
-rw-r--r--spec/ruby/core/hash/update_spec.rb79
-rw-r--r--spec/ruby/core/hash/value_spec.rb7
-rw-r--r--spec/ruby/core/hash/values_at_spec.rb11
-rw-r--r--spec/ruby/core/hash/values_spec.rb10
-rw-r--r--spec/ruby/core/integer/abs_spec.rb6
-rw-r--r--spec/ruby/core/integer/allbits_spec.rb37
-rw-r--r--spec/ruby/core/integer/anybits_spec.rb36
-rw-r--r--spec/ruby/core/integer/bit_and_spec.rb97
-rw-r--r--spec/ruby/core/integer/bit_length_spec.rb76
-rw-r--r--spec/ruby/core/integer/bit_or_spec.rb89
-rw-r--r--spec/ruby/core/integer/bit_xor_spec.rb93
-rw-r--r--spec/ruby/core/integer/case_compare_spec.rb6
-rw-r--r--spec/ruby/core/integer/ceil_spec.rb13
-rw-r--r--spec/ruby/core/integer/ceildiv_spec.rb20
-rw-r--r--spec/ruby/core/integer/chr_spec.rb257
-rw-r--r--spec/ruby/core/integer/coerce_spec.rb91
-rw-r--r--spec/ruby/core/integer/comparison_spec.rb185
-rw-r--r--spec/ruby/core/integer/complement_spec.rb20
-rw-r--r--spec/ruby/core/integer/constants_spec.rb13
-rw-r--r--spec/ruby/core/integer/denominator_spec.rb20
-rw-r--r--spec/ruby/core/integer/digits_spec.rb41
-rw-r--r--spec/ruby/core/integer/div_spec.rb154
-rw-r--r--spec/ruby/core/integer/divide_spec.rb126
-rw-r--r--spec/ruby/core/integer/divmod_spec.rb117
-rw-r--r--spec/ruby/core/integer/downto_spec.rb69
-rw-r--r--spec/ruby/core/integer/dup_spec.rb13
-rw-r--r--spec/ruby/core/integer/element_reference_spec.rb188
-rw-r--r--spec/ruby/core/integer/eql_spec.rb25
-rw-r--r--spec/ruby/core/integer/equal_value_spec.rb6
-rw-r--r--spec/ruby/core/integer/even_spec.rb40
-rw-r--r--spec/ruby/core/integer/exponent_spec.rb7
-rw-r--r--spec/ruby/core/integer/fdiv_spec.rb100
-rw-r--r--spec/ruby/core/integer/fixtures/classes.rb14
-rw-r--r--spec/ruby/core/integer/floor_spec.rb13
-rw-r--r--spec/ruby/core/integer/gcd_spec.rb69
-rw-r--r--spec/ruby/core/integer/gcdlcm_spec.rb53
-rw-r--r--spec/ruby/core/integer/gt_spec.rb48
-rw-r--r--spec/ruby/core/integer/gte_spec.rb48
-rw-r--r--spec/ruby/core/integer/integer_spec.rb20
-rw-r--r--spec/ruby/core/integer/lcm_spec.rb58
-rw-r--r--spec/ruby/core/integer/left_shift_spec.rb211
-rw-r--r--spec/ruby/core/integer/lt_spec.rb50
-rw-r--r--spec/ruby/core/integer/lte_spec.rb58
-rw-r--r--spec/ruby/core/integer/magnitude_spec.rb6
-rw-r--r--spec/ruby/core/integer/minus_spec.rb60
-rw-r--r--spec/ruby/core/integer/modulo_spec.rb10
-rw-r--r--spec/ruby/core/integer/multiply_spec.rb45
-rw-r--r--spec/ruby/core/integer/next_spec.rb6
-rw-r--r--spec/ruby/core/integer/nobits_spec.rb36
-rw-r--r--spec/ruby/core/integer/numerator_spec.rb18
-rw-r--r--spec/ruby/core/integer/odd_spec.rb38
-rw-r--r--spec/ruby/core/integer/ord_spec.rb17
-rw-r--r--spec/ruby/core/integer/plus_spec.rb75
-rw-r--r--spec/ruby/core/integer/pow_spec.rb51
-rw-r--r--spec/ruby/core/integer/pred_spec.rb11
-rw-r--r--spec/ruby/core/integer/rationalize_spec.rb39
-rw-r--r--spec/ruby/core/integer/remainder_spec.rb51
-rw-r--r--spec/ruby/core/integer/right_shift_spec.rb233
-rw-r--r--spec/ruby/core/integer/round_spec.rb81
-rw-r--r--spec/ruby/core/integer/shared/abs.rb18
-rw-r--r--spec/ruby/core/integer/shared/arithmetic_coerce.rb11
-rw-r--r--spec/ruby/core/integer/shared/comparison_coerce.rb11
-rw-r--r--spec/ruby/core/integer/shared/equal.rb63
-rw-r--r--spec/ruby/core/integer/shared/exponent.rb162
-rw-r--r--spec/ruby/core/integer/shared/integer_ceil_precision.rb54
-rw-r--r--spec/ruby/core/integer/shared/integer_floor_precision.rb42
-rw-r--r--spec/ruby/core/integer/shared/integer_rounding.rb19
-rw-r--r--spec/ruby/core/integer/shared/modulo.rb114
-rw-r--r--spec/ruby/core/integer/shared/next.rb25
-rw-r--r--spec/ruby/core/integer/shared/to_i.rb8
-rw-r--r--spec/ruby/core/integer/size_spec.rb34
-rw-r--r--spec/ruby/core/integer/sqrt_spec.rb31
-rw-r--r--spec/ruby/core/integer/succ_spec.rb6
-rw-r--r--spec/ruby/core/integer/times_spec.rb79
-rw-r--r--spec/ruby/core/integer/to_f_spec.rb23
-rw-r--r--spec/ruby/core/integer/to_i_spec.rb6
-rw-r--r--spec/ruby/core/integer/to_int_spec.rb6
-rw-r--r--spec/ruby/core/integer/to_r_spec.rb26
-rw-r--r--spec/ruby/core/integer/to_s_spec.rb95
-rw-r--r--spec/ruby/core/integer/truncate_spec.rb19
-rw-r--r--spec/ruby/core/integer/try_convert_spec.rb48
-rw-r--r--spec/ruby/core/integer/uminus_spec.rb30
-rw-r--r--spec/ruby/core/integer/upto_spec.rb69
-rw-r--r--spec/ruby/core/integer/zero_spec.rb13
-rw-r--r--spec/ruby/core/io/advise_spec.rb86
-rw-r--r--spec/ruby/core/io/autoclose_spec.rb77
-rw-r--r--spec/ruby/core/io/binmode_spec.rb64
-rw-r--r--spec/ruby/core/io/binread_spec.rb57
-rw-r--r--spec/ruby/core/io/binwrite_spec.rb6
-rw-r--r--spec/ruby/core/io/buffer/and_spec.rb62
-rw-r--r--spec/ruby/core/io/buffer/bit_count_spec.rb64
-rw-r--r--spec/ruby/core/io/buffer/empty_spec.rb27
-rw-r--r--spec/ruby/core/io/buffer/external_spec.rb23
-rw-r--r--spec/ruby/core/io/buffer/for_spec.rb95
-rw-r--r--spec/ruby/core/io/buffer/free_spec.rb102
-rw-r--r--spec/ruby/core/io/buffer/initialize_spec.rb119
-rw-r--r--spec/ruby/core/io/buffer/internal_spec.rb23
-rw-r--r--spec/ruby/core/io/buffer/locked_spec.rb75
-rw-r--r--spec/ruby/core/io/buffer/map_spec.rb343
-rw-r--r--spec/ruby/core/io/buffer/mapped_spec.rb23
-rw-r--r--spec/ruby/core/io/buffer/not_spec.rb37
-rw-r--r--spec/ruby/core/io/buffer/null_spec.rb27
-rw-r--r--spec/ruby/core/io/buffer/or_spec.rb62
-rw-r--r--spec/ruby/core/io/buffer/private_spec.rb23
-rw-r--r--spec/ruby/core/io/buffer/readonly_spec.rb28
-rw-r--r--spec/ruby/core/io/buffer/resize_spec.rb151
-rw-r--r--spec/ruby/core/io/buffer/shared/null_and_empty.rb57
-rw-r--r--spec/ruby/core/io/buffer/shared_spec.rb33
-rw-r--r--spec/ruby/core/io/buffer/string_spec.rb62
-rw-r--r--spec/ruby/core/io/buffer/transfer_spec.rb117
-rw-r--r--spec/ruby/core/io/buffer/valid_spec.rb99
-rw-r--r--spec/ruby/core/io/buffer/xor_spec.rb62
-rw-r--r--spec/ruby/core/io/close_on_exec_spec.rb76
-rw-r--r--spec/ruby/core/io/close_read_spec.rb61
-rw-r--r--spec/ruby/core/io/close_spec.rb118
-rw-r--r--spec/ruby/core/io/close_write_spec.rb68
-rw-r--r--spec/ruby/core/io/closed_spec.rb20
-rw-r--r--spec/ruby/core/io/constants_spec.rb19
-rw-r--r--spec/ruby/core/io/copy_stream_spec.rb351
-rw-r--r--spec/ruby/core/io/dup_spec.rb106
-rw-r--r--spec/ruby/core/io/each_byte_spec.rb57
-rw-r--r--spec/ruby/core/io/each_char_spec.rb12
-rw-r--r--spec/ruby/core/io/each_codepoint_spec.rb43
-rw-r--r--spec/ruby/core/io/each_line_spec.rb11
-rw-r--r--spec/ruby/core/io/each_spec.rb11
-rw-r--r--spec/ruby/core/io/eof_spec.rb107
-rw-r--r--spec/ruby/core/io/external_encoding_spec.rb223
-rw-r--r--spec/ruby/core/io/fcntl_spec.rb8
-rw-r--r--spec/ruby/core/io/fdatasync_spec.rb5
-rw-r--r--spec/ruby/core/io/fileno_spec.rb12
-rw-r--r--spec/ruby/core/io/fixtures/bom_UTF-16BE.txtbin0 -> 20 bytes-rw-r--r--spec/ruby/core/io/fixtures/bom_UTF-16LE.txtbin0 -> 20 bytes-rw-r--r--spec/ruby/core/io/fixtures/bom_UTF-32BE.txtbin0 -> 40 bytes-rw-r--r--spec/ruby/core/io/fixtures/bom_UTF-32LE.txtbin0 -> 40 bytes-rw-r--r--spec/ruby/core/io/fixtures/bom_UTF-8.txt1
-rw-r--r--spec/ruby/core/io/fixtures/classes.rb218
-rw-r--r--spec/ruby/core/io/fixtures/copy_in_out.rb2
-rw-r--r--spec/ruby/core/io/fixtures/copy_stream.txt6
-rw-r--r--spec/ruby/core/io/fixtures/empty.txt0
-rw-r--r--spec/ruby/core/io/fixtures/incomplete.txt1
-rw-r--r--spec/ruby/core/io/fixtures/lines.txt9
-rw-r--r--spec/ruby/core/io/fixtures/no_bom_UTF-8.txt1
-rw-r--r--spec/ruby/core/io/fixtures/numbered_lines.txt5
-rw-r--r--spec/ruby/core/io/fixtures/one_byte.txt1
-rw-r--r--spec/ruby/core/io/fixtures/read_binary.txt1
-rw-r--r--spec/ruby/core/io/fixtures/read_euc_jp.txt1
-rw-r--r--spec/ruby/core/io/fixtures/read_text.txt1
-rw-r--r--spec/ruby/core/io/fixtures/reopen_stdout.rb3
-rw-r--r--spec/ruby/core/io/flush_spec.rb37
-rw-r--r--spec/ruby/core/io/for_fd_spec.rb10
-rw-r--r--spec/ruby/core/io/foreach_spec.rb96
-rw-r--r--spec/ruby/core/io/fsync_spec.rb24
-rw-r--r--spec/ruby/core/io/getbyte_spec.rb58
-rw-r--r--spec/ruby/core/io/getc_spec.rb42
-rw-r--r--spec/ruby/core/io/gets_spec.rb348
-rw-r--r--spec/ruby/core/io/initialize_spec.rb60
-rw-r--r--spec/ruby/core/io/inspect_spec.rb23
-rw-r--r--spec/ruby/core/io/internal_encoding_spec.rb145
-rw-r--r--spec/ruby/core/io/io_spec.rb11
-rw-r--r--spec/ruby/core/io/ioctl_spec.rb32
-rw-r--r--spec/ruby/core/io/isatty_spec.rb6
-rw-r--r--spec/ruby/core/io/lineno_spec.rb138
-rw-r--r--spec/ruby/core/io/new_spec.rb18
-rw-r--r--spec/ruby/core/io/nonblock_spec.rb48
-rw-r--r--spec/ruby/core/io/open_spec.rb99
-rw-r--r--spec/ruby/core/io/output_spec.rb27
-rw-r--r--spec/ruby/core/io/path_spec.rb12
-rw-r--r--spec/ruby/core/io/pid_spec.rb35
-rw-r--r--spec/ruby/core/io/pipe_spec.rb225
-rw-r--r--spec/ruby/core/io/popen_spec.rb287
-rw-r--r--spec/ruby/core/io/pos_spec.rb11
-rw-r--r--spec/ruby/core/io/pread_spec.rb138
-rw-r--r--spec/ruby/core/io/print_spec.rb66
-rw-r--r--spec/ruby/core/io/printf_spec.rb32
-rw-r--r--spec/ruby/core/io/putc_spec.rb11
-rw-r--r--spec/ruby/core/io/puts_spec.rb139
-rw-r--r--spec/ruby/core/io/pwrite_spec.rb67
-rw-r--r--spec/ruby/core/io/read_nonblock_spec.rb148
-rw-r--r--spec/ruby/core/io/read_spec.rb735
-rw-r--r--spec/ruby/core/io/readbyte_spec.rb24
-rw-r--r--spec/ruby/core/io/readchar_spec.rb110
-rw-r--r--spec/ruby/core/io/readline_spec.rb84
-rw-r--r--spec/ruby/core/io/readlines_spec.rb257
-rw-r--r--spec/ruby/core/io/readpartial_spec.rb115
-rw-r--r--spec/ruby/core/io/reopen_spec.rb313
-rw-r--r--spec/ruby/core/io/rewind_spec.rb53
-rw-r--r--spec/ruby/core/io/seek_spec.rb79
-rw-r--r--spec/ruby/core/io/select_spec.rb190
-rw-r--r--spec/ruby/core/io/set_encoding_by_bom_spec.rb262
-rw-r--r--spec/ruby/core/io/set_encoding_spec.rb238
-rw-r--r--spec/ruby/core/io/shared/binwrite.rb91
-rw-r--r--spec/ruby/core/io/shared/chars.rb73
-rw-r--r--spec/ruby/core/io/shared/codepoints.rb54
-rw-r--r--spec/ruby/core/io/shared/each.rb251
-rw-r--r--spec/ruby/core/io/shared/gets_ascii.rb19
-rw-r--r--spec/ruby/core/io/shared/new.rb413
-rw-r--r--spec/ruby/core/io/shared/pos.rb78
-rw-r--r--spec/ruby/core/io/shared/readlines.rb257
-rw-r--r--spec/ruby/core/io/shared/tty.rb24
-rw-r--r--spec/ruby/core/io/shared/write.rb154
-rw-r--r--spec/ruby/core/io/stat_spec.rb25
-rw-r--r--spec/ruby/core/io/sync_spec.rb64
-rw-r--r--spec/ruby/core/io/sysopen_spec.rb50
-rw-r--r--spec/ruby/core/io/sysread_spec.rb137
-rw-r--r--spec/ruby/core/io/sysseek_spec.rb49
-rw-r--r--spec/ruby/core/io/syswrite_spec.rb82
-rw-r--r--spec/ruby/core/io/tell_spec.rb7
-rw-r--r--spec/ruby/core/io/to_i_spec.rb12
-rw-r--r--spec/ruby/core/io/to_io_spec.rb21
-rw-r--r--spec/ruby/core/io/try_convert_spec.rb49
-rw-r--r--spec/ruby/core/io/tty_spec.rb6
-rw-r--r--spec/ruby/core/io/ungetbyte_spec.rb54
-rw-r--r--spec/ruby/core/io/ungetc_spec.rb138
-rw-r--r--spec/ruby/core/io/write_nonblock_spec.rb96
-rw-r--r--spec/ruby/core/io/write_spec.rb304
-rw-r--r--spec/ruby/core/kernel/Array_spec.rb97
-rw-r--r--spec/ruby/core/kernel/Complex_spec.rb276
-rw-r--r--spec/ruby/core/kernel/Float_spec.rb413
-rw-r--r--spec/ruby/core/kernel/Hash_spec.rb63
-rw-r--r--spec/ruby/core/kernel/Integer_spec.rb845
-rw-r--r--spec/ruby/core/kernel/Rational_spec.rb236
-rw-r--r--spec/ruby/core/kernel/String_spec.rb106
-rw-r--r--spec/ruby/core/kernel/__callee___spec.rb48
-rw-r--r--spec/ruby/core/kernel/__dir___spec.rb27
-rw-r--r--spec/ruby/core/kernel/__method___spec.rb40
-rw-r--r--spec/ruby/core/kernel/abort_spec.rb15
-rw-r--r--spec/ruby/core/kernel/at_exit_spec.rb19
-rw-r--r--spec/ruby/core/kernel/autoload_relative_spec.rb114
-rw-r--r--spec/ruby/core/kernel/autoload_spec.rb178
-rw-r--r--spec/ruby/core/kernel/backtick_spec.rb84
-rw-r--r--spec/ruby/core/kernel/binding_spec.rb51
-rw-r--r--spec/ruby/core/kernel/block_given_spec.rb43
-rw-r--r--spec/ruby/core/kernel/caller_locations_spec.rb113
-rw-r--r--spec/ruby/core/kernel/caller_spec.rb121
-rw-r--r--spec/ruby/core/kernel/case_compare_spec.rb135
-rw-r--r--spec/ruby/core/kernel/catch_spec.rb127
-rw-r--r--spec/ruby/core/kernel/chomp_spec.rb65
-rw-r--r--spec/ruby/core/kernel/chop_spec.rb53
-rw-r--r--spec/ruby/core/kernel/class_spec.rb26
-rw-r--r--spec/ruby/core/kernel/clone_spec.rb177
-rw-r--r--spec/ruby/core/kernel/comparison_spec.rb31
-rw-r--r--spec/ruby/core/kernel/define_singleton_method_spec.rb120
-rw-r--r--spec/ruby/core/kernel/display_spec.rb6
-rw-r--r--spec/ruby/core/kernel/dup_spec.rb67
-rw-r--r--spec/ruby/core/kernel/enum_for_spec.rb5
-rw-r--r--spec/ruby/core/kernel/eql_spec.rb10
-rw-r--r--spec/ruby/core/kernel/equal_value_spec.rb15
-rw-r--r--spec/ruby/core/kernel/eval_spec.rb552
-rw-r--r--spec/ruby/core/kernel/exec_spec.rb18
-rw-r--r--spec/ruby/core/kernel/exit_spec.rb27
-rw-r--r--spec/ruby/core/kernel/extend_spec.rb91
-rw-r--r--spec/ruby/core/kernel/fail_spec.rb42
-rw-r--r--spec/ruby/core/kernel/fixtures/Complex.rb5
-rw-r--r--spec/ruby/core/kernel/fixtures/__callee__.rb34
-rw-r--r--spec/ruby/core/kernel/fixtures/__dir__.rb2
-rw-r--r--spec/ruby/core/kernel/fixtures/__method__.rb34
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_b.rb5
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_d.rb5
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb9
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb9
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_frozen.rb7
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_relative_b.rb7
-rw-r--r--spec/ruby/core/kernel/fixtures/autoload_relative_d.rb5
-rw-r--r--spec/ruby/core/kernel/fixtures/caller.rb7
-rw-r--r--spec/ruby/core/kernel/fixtures/caller_at_exit.rb7
-rw-r--r--spec/ruby/core/kernel/fixtures/caller_locations.rb7
-rw-r--r--spec/ruby/core/kernel/fixtures/chomp.rb4
-rw-r--r--spec/ruby/core/kernel/fixtures/chomp_f.rb4
-rw-r--r--spec/ruby/core/kernel/fixtures/chop.rb4
-rw-r--r--spec/ruby/core/kernel/fixtures/chop_f.rb4
-rw-r--r--spec/ruby/core/kernel/fixtures/classes.rb577
-rw-r--r--spec/ruby/core/kernel/fixtures/eval_locals.rb6
-rw-r--r--spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb12
-rw-r--r--spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb14
-rw-r--r--spec/ruby/core/kernel/fixtures/singleton_methods.rb13
-rw-r--r--spec/ruby/core/kernel/fixtures/test.rb362
-rw-r--r--spec/ruby/core/kernel/fixtures/warn_core_method.rb14
-rw-r--r--spec/ruby/core/kernel/fixtures/warn_require.rb1
-rw-r--r--spec/ruby/core/kernel/fixtures/warn_require_caller.rb2
-rw-r--r--spec/ruby/core/kernel/fork_spec.rb15
-rw-r--r--spec/ruby/core/kernel/format_spec.rb47
-rw-r--r--spec/ruby/core/kernel/freeze_spec.rb91
-rw-r--r--spec/ruby/core/kernel/frozen_spec.rb76
-rw-r--r--spec/ruby/core/kernel/gets_spec.rb17
-rw-r--r--spec/ruby/core/kernel/global_variables_spec.rb26
-rw-r--r--spec/ruby/core/kernel/gsub_spec.rb96
-rw-r--r--spec/ruby/core/kernel/initialize_clone_spec.rb26
-rw-r--r--spec/ruby/core/kernel/initialize_copy_spec.rb36
-rw-r--r--spec/ruby/core/kernel/initialize_dup_spec.rb20
-rw-r--r--spec/ruby/core/kernel/inspect_spec.rb103
-rw-r--r--spec/ruby/core/kernel/instance_of_spec.rb40
-rw-r--r--spec/ruby/core/kernel/instance_variable_defined_spec.rb41
-rw-r--r--spec/ruby/core/kernel/instance_variable_get_spec.rb111
-rw-r--r--spec/ruby/core/kernel/instance_variable_set_spec.rb105
-rw-r--r--spec/ruby/core/kernel/instance_variables_spec.rb40
-rw-r--r--spec/ruby/core/kernel/is_a_spec.rb6
-rw-r--r--spec/ruby/core/kernel/itself_spec.rb9
-rw-r--r--spec/ruby/core/kernel/kind_of_spec.rb6
-rw-r--r--spec/ruby/core/kernel/lambda_spec.rb110
-rw-r--r--spec/ruby/core/kernel/load_spec.rb40
-rw-r--r--spec/ruby/core/kernel/local_variables_spec.rb45
-rw-r--r--spec/ruby/core/kernel/loop_spec.rb79
-rw-r--r--spec/ruby/core/kernel/match_spec.rb7
-rw-r--r--spec/ruby/core/kernel/method_spec.rb88
-rw-r--r--spec/ruby/core/kernel/methods_spec.rb101
-rw-r--r--spec/ruby/core/kernel/nil_spec.rb12
-rw-r--r--spec/ruby/core/kernel/not_match_spec.rb25
-rw-r--r--spec/ruby/core/kernel/object_id_spec.rb6
-rw-r--r--spec/ruby/core/kernel/open_spec.rb192
-rw-r--r--spec/ruby/core/kernel/p_spec.rb85
-rw-r--r--spec/ruby/core/kernel/pp_spec.rb9
-rw-r--r--spec/ruby/core/kernel/print_spec.rb24
-rw-r--r--spec/ruby/core/kernel/printf_spec.rb70
-rw-r--r--spec/ruby/core/kernel/private_methods_spec.rb69
-rw-r--r--spec/ruby/core/kernel/proc_spec.rb48
-rw-r--r--spec/ruby/core/kernel/protected_methods_spec.rb69
-rw-r--r--spec/ruby/core/kernel/public_method_spec.rb32
-rw-r--r--spec/ruby/core/kernel/public_methods_spec.rb75
-rw-r--r--spec/ruby/core/kernel/public_send_spec.rb116
-rw-r--r--spec/ruby/core/kernel/putc_spec.rb39
-rw-r--r--spec/ruby/core/kernel/puts_spec.rb29
-rw-r--r--spec/ruby/core/kernel/raise_spec.rb222
-rw-r--r--spec/ruby/core/kernel/rand_spec.rb197
-rw-r--r--spec/ruby/core/kernel/readline_spec.rb12
-rw-r--r--spec/ruby/core/kernel/readlines_spec.rb12
-rw-r--r--spec/ruby/core/kernel/remove_instance_variable_spec.rb72
-rw-r--r--spec/ruby/core/kernel/require_relative_spec.rb437
-rw-r--r--spec/ruby/core/kernel/require_spec.rb67
-rw-r--r--spec/ruby/core/kernel/respond_to_missing_spec.rb100
-rw-r--r--spec/ruby/core/kernel/respond_to_spec.rb99
-rw-r--r--spec/ruby/core/kernel/select_spec.rb18
-rw-r--r--spec/ruby/core/kernel/send_spec.rb68
-rw-r--r--spec/ruby/core/kernel/set_trace_func_spec.rb12
-rw-r--r--spec/ruby/core/kernel/shared/dup_clone.rb91
-rw-r--r--spec/ruby/core/kernel/shared/kind_of.rb55
-rw-r--r--spec/ruby/core/kernel/shared/lambda.rb11
-rw-r--r--spec/ruby/core/kernel/shared/load.rb215
-rw-r--r--spec/ruby/core/kernel/shared/method.rb56
-rw-r--r--spec/ruby/core/kernel/shared/require.rb846
-rw-r--r--spec/ruby/core/kernel/shared/sprintf.rb1034
-rw-r--r--spec/ruby/core/kernel/shared/sprintf_encoding.rb67
-rw-r--r--spec/ruby/core/kernel/shared/then.rb20
-rw-r--r--spec/ruby/core/kernel/singleton_class_spec.rb74
-rw-r--r--spec/ruby/core/kernel/singleton_method_spec.rb85
-rw-r--r--spec/ruby/core/kernel/singleton_methods_spec.rb199
-rw-r--r--spec/ruby/core/kernel/sleep_spec.rb118
-rw-r--r--spec/ruby/core/kernel/spawn_spec.rb25
-rw-r--r--spec/ruby/core/kernel/sprintf_spec.rb64
-rw-r--r--spec/ruby/core/kernel/srand_spec.rb73
-rw-r--r--spec/ruby/core/kernel/sub_spec.rb26
-rw-r--r--spec/ruby/core/kernel/syscall_spec.rb12
-rw-r--r--spec/ruby/core/kernel/system_spec.rb132
-rw-r--r--spec/ruby/core/kernel/taint_spec.rb8
-rw-r--r--spec/ruby/core/kernel/tainted_spec.rb8
-rw-r--r--spec/ruby/core/kernel/tap_spec.rb13
-rw-r--r--spec/ruby/core/kernel/test_spec.rb109
-rw-r--r--spec/ruby/core/kernel/then_spec.rb6
-rw-r--r--spec/ruby/core/kernel/throw_spec.rb80
-rw-r--r--spec/ruby/core/kernel/to_enum_spec.rb5
-rw-r--r--spec/ruby/core/kernel/to_s_spec.rb8
-rw-r--r--spec/ruby/core/kernel/trace_var_spec.rb54
-rw-r--r--spec/ruby/core/kernel/trap_spec.rb9
-rw-r--r--spec/ruby/core/kernel/trust_spec.rb8
-rw-r--r--spec/ruby/core/kernel/untaint_spec.rb8
-rw-r--r--spec/ruby/core/kernel/untrace_var_spec.rb12
-rw-r--r--spec/ruby/core/kernel/untrust_spec.rb8
-rw-r--r--spec/ruby/core/kernel/untrusted_spec.rb8
-rw-r--r--spec/ruby/core/kernel/warn_spec.rb298
-rw-r--r--spec/ruby/core/kernel/yield_self_spec.rb6
-rw-r--r--spec/ruby/core/main/define_method_spec.rb28
-rw-r--r--spec/ruby/core/main/fixtures/classes.rb26
-rw-r--r--spec/ruby/core/main/fixtures/string_refinement.rb7
-rw-r--r--spec/ruby/core/main/fixtures/string_refinement_user.rb11
-rw-r--r--spec/ruby/core/main/fixtures/using.rb1
-rw-r--r--spec/ruby/core/main/fixtures/using_in_main.rb5
-rw-r--r--spec/ruby/core/main/fixtures/using_in_method.rb5
-rw-r--r--spec/ruby/core/main/fixtures/wrapped_include.rb1
-rw-r--r--spec/ruby/core/main/include_spec.rb16
-rw-r--r--spec/ruby/core/main/private_spec.rb42
-rw-r--r--spec/ruby/core/main/public_spec.rb43
-rw-r--r--spec/ruby/core/main/ruby2_keywords_spec.rb9
-rw-r--r--spec/ruby/core/main/to_s_spec.rb7
-rw-r--r--spec/ruby/core/main/using_spec.rb150
-rw-r--r--spec/ruby/core/marshal/dump_spec.rb1082
-rw-r--r--spec/ruby/core/marshal/fixtures/classes.rb4
-rw-r--r--spec/ruby/core/marshal/fixtures/marshal_data.rb567
-rw-r--r--spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb12
-rw-r--r--spec/ruby/core/marshal/fixtures/random.dumpbin0 -> 2520 bytes-rw-r--r--spec/ruby/core/marshal/float_spec.rb77
-rw-r--r--spec/ruby/core/marshal/load_spec.rb6
-rw-r--r--spec/ruby/core/marshal/major_version_spec.rb7
-rw-r--r--spec/ruby/core/marshal/minor_version_spec.rb7
-rw-r--r--spec/ruby/core/marshal/restore_spec.rb6
-rw-r--r--spec/ruby/core/marshal/shared/load.rb1291
-rw-r--r--spec/ruby/core/matchdata/allocate_spec.rb8
-rw-r--r--spec/ruby/core/matchdata/begin_spec.rb132
-rw-r--r--spec/ruby/core/matchdata/bytebegin_spec.rb132
-rw-r--r--spec/ruby/core/matchdata/byteend_spec.rb104
-rw-r--r--spec/ruby/core/matchdata/byteoffset_spec.rb93
-rw-r--r--spec/ruby/core/matchdata/captures_spec.rb6
-rw-r--r--spec/ruby/core/matchdata/deconstruct_keys_spec.rb78
-rw-r--r--spec/ruby/core/matchdata/deconstruct_spec.rb6
-rw-r--r--spec/ruby/core/matchdata/dup_spec.rb14
-rw-r--r--spec/ruby/core/matchdata/element_reference_spec.rb124
-rw-r--r--spec/ruby/core/matchdata/end_spec.rb104
-rw-r--r--spec/ruby/core/matchdata/eql_spec.rb6
-rw-r--r--spec/ruby/core/matchdata/equal_value_spec.rb6
-rw-r--r--spec/ruby/core/matchdata/fixtures/classes.rb3
-rw-r--r--spec/ruby/core/matchdata/hash_spec.rb5
-rw-r--r--spec/ruby/core/matchdata/inspect_spec.rb23
-rw-r--r--spec/ruby/core/matchdata/integer_at_spec.rb38
-rw-r--r--spec/ruby/core/matchdata/length_spec.rb6
-rw-r--r--spec/ruby/core/matchdata/match_length_spec.rb32
-rw-r--r--spec/ruby/core/matchdata/match_spec.rb32
-rw-r--r--spec/ruby/core/matchdata/named_captures_spec.rb25
-rw-r--r--spec/ruby/core/matchdata/names_spec.rb33
-rw-r--r--spec/ruby/core/matchdata/offset_spec.rb102
-rw-r--r--spec/ruby/core/matchdata/post_match_spec.rb24
-rw-r--r--spec/ruby/core/matchdata/pre_match_spec.rb24
-rw-r--r--spec/ruby/core/matchdata/regexp_spec.rb24
-rw-r--r--spec/ruby/core/matchdata/shared/captures.rb13
-rw-r--r--spec/ruby/core/matchdata/shared/eql.rb26
-rw-r--r--spec/ruby/core/matchdata/shared/length.rb5
-rw-r--r--spec/ruby/core/matchdata/size_spec.rb6
-rw-r--r--spec/ruby/core/matchdata/string_spec.rb26
-rw-r--r--spec/ruby/core/matchdata/to_a_spec.rb13
-rw-r--r--spec/ruby/core/matchdata/to_s_spec.rb13
-rw-r--r--spec/ruby/core/matchdata/values_at_spec.rb76
-rw-r--r--spec/ruby/core/math/acos_spec.rb56
-rw-r--r--spec/ruby/core/math/acosh_spec.rb41
-rw-r--r--spec/ruby/core/math/asin_spec.rb48
-rw-r--r--spec/ruby/core/math/asinh_spec.rb42
-rw-r--r--spec/ruby/core/math/atan2_spec.rb54
-rw-r--r--spec/ruby/core/math/atan_spec.rb40
-rw-r--r--spec/ruby/core/math/atanh_spec.rb14
-rw-r--r--spec/ruby/core/math/cbrt_spec.rb27
-rw-r--r--spec/ruby/core/math/constants_spec.rb22
-rw-r--r--spec/ruby/core/math/cos_spec.rb50
-rw-r--r--spec/ruby/core/math/cosh_spec.rb37
-rw-r--r--spec/ruby/core/math/erf_spec.rb44
-rw-r--r--spec/ruby/core/math/erfc_spec.rb43
-rw-r--r--spec/ruby/core/math/exp_spec.rb37
-rw-r--r--spec/ruby/core/math/expm1_spec.rb37
-rw-r--r--spec/ruby/core/math/fixtures/classes.rb28
-rw-r--r--spec/ruby/core/math/fixtures/common.rb3
-rw-r--r--spec/ruby/core/math/frexp_spec.rb37
-rw-r--r--spec/ruby/core/math/gamma_spec.rb69
-rw-r--r--spec/ruby/core/math/hypot_spec.rb41
-rw-r--r--spec/ruby/core/math/ldexp_spec.rb60
-rw-r--r--spec/ruby/core/math/lgamma_spec.rb51
-rw-r--r--spec/ruby/core/math/log10_spec.rb47
-rw-r--r--spec/ruby/core/math/log1p_spec.rb49
-rw-r--r--spec/ruby/core/math/log2_spec.rb41
-rw-r--r--spec/ruby/core/math/log_spec.rb57
-rw-r--r--spec/ruby/core/math/shared/atanh.rb44
-rw-r--r--spec/ruby/core/math/sin_spec.rb39
-rw-r--r--spec/ruby/core/math/sinh_spec.rb37
-rw-r--r--spec/ruby/core/math/sqrt_spec.rb40
-rw-r--r--spec/ruby/core/math/tan_spec.rb42
-rw-r--r--spec/ruby/core/math/tanh_spec.rb39
-rw-r--r--spec/ruby/core/method/arity_spec.rb222
-rw-r--r--spec/ruby/core/method/call_spec.rb7
-rw-r--r--spec/ruby/core/method/case_compare_spec.rb7
-rw-r--r--spec/ruby/core/method/clone_spec.rb13
-rw-r--r--spec/ruby/core/method/compose_spec.rb99
-rw-r--r--spec/ruby/core/method/curry_spec.rb36
-rw-r--r--spec/ruby/core/method/dup_spec.rb15
-rw-r--r--spec/ruby/core/method/element_reference_spec.rb7
-rw-r--r--spec/ruby/core/method/eql_spec.rb6
-rw-r--r--spec/ruby/core/method/equal_value_spec.rb6
-rw-r--r--spec/ruby/core/method/fixtures/classes.rb247
-rw-r--r--spec/ruby/core/method/hash_spec.rb15
-rw-r--r--spec/ruby/core/method/inspect_spec.rb8
-rw-r--r--spec/ruby/core/method/name_spec.rb22
-rw-r--r--spec/ruby/core/method/original_name_spec.rb59
-rw-r--r--spec/ruby/core/method/owner_spec.rb30
-rw-r--r--spec/ruby/core/method/parameters_spec.rb317
-rw-r--r--spec/ruby/core/method/private_spec.rb9
-rw-r--r--spec/ruby/core/method/protected_spec.rb9
-rw-r--r--spec/ruby/core/method/public_spec.rb9
-rw-r--r--spec/ruby/core/method/receiver_spec.rb22
-rw-r--r--spec/ruby/core/method/shared/aliased_inspect.rb31
-rw-r--r--spec/ruby/core/method/shared/call.rb51
-rw-r--r--spec/ruby/core/method/shared/dup.rb32
-rw-r--r--spec/ruby/core/method/shared/eql.rb94
-rw-r--r--spec/ruby/core/method/shared/to_s.rb81
-rw-r--r--spec/ruby/core/method/source_location_spec.rb120
-rw-r--r--spec/ruby/core/method/super_method_spec.rb64
-rw-r--r--spec/ruby/core/method/to_proc_spec.rb104
-rw-r--r--spec/ruby/core/method/to_s_spec.rb8
-rw-r--r--spec/ruby/core/method/unbind_spec.rb46
-rw-r--r--spec/ruby/core/module/alias_method_spec.rb171
-rw-r--r--spec/ruby/core/module/ancestors_spec.rb88
-rw-r--r--spec/ruby/core/module/append_features_spec.rb61
-rw-r--r--spec/ruby/core/module/attr_accessor_spec.rb112
-rw-r--r--spec/ruby/core/module/attr_reader_spec.rb73
-rw-r--r--spec/ruby/core/module/attr_spec.rb159
-rw-r--r--spec/ruby/core/module/attr_writer_spec.rb83
-rw-r--r--spec/ruby/core/module/autoload_relative_spec.rb128
-rw-r--r--spec/ruby/core/module/autoload_spec.rb1028
-rw-r--r--spec/ruby/core/module/case_compare_spec.rb31
-rw-r--r--spec/ruby/core/module/class_eval_spec.rb7
-rw-r--r--spec/ruby/core/module/class_exec_spec.rb7
-rw-r--r--spec/ruby/core/module/class_variable_defined_spec.rb72
-rw-r--r--spec/ruby/core/module/class_variable_get_spec.rb76
-rw-r--r--spec/ruby/core/module/class_variable_set_spec.rb62
-rw-r--r--spec/ruby/core/module/class_variables_spec.rb34
-rw-r--r--spec/ruby/core/module/comparison_spec.rb36
-rw-r--r--spec/ruby/core/module/const_added_spec.rb238
-rw-r--r--spec/ruby/core/module/const_defined_spec.rb169
-rw-r--r--spec/ruby/core/module/const_get_spec.rb273
-rw-r--r--spec/ruby/core/module/const_missing_spec.rb36
-rw-r--r--spec/ruby/core/module/const_set_spec.rb145
-rw-r--r--spec/ruby/core/module/const_source_location_spec.rb281
-rw-r--r--spec/ruby/core/module/constants_spec.rb98
-rw-r--r--spec/ruby/core/module/define_method_spec.rb846
-rw-r--r--spec/ruby/core/module/define_singleton_method_spec.rb15
-rw-r--r--spec/ruby/core/module/deprecate_constant_spec.rb70
-rw-r--r--spec/ruby/core/module/eql_spec.rb7
-rw-r--r--spec/ruby/core/module/equal_spec.rb7
-rw-r--r--spec/ruby/core/module/equal_value_spec.rb7
-rw-r--r--spec/ruby/core/module/extend_object_spec.rb56
-rw-r--r--spec/ruby/core/module/extended_spec.rb44
-rw-r--r--spec/ruby/core/module/fixtures/autoload.rb1
-rw-r--r--spec/ruby/core/module/fixtures/autoload_abc.rb11
-rw-r--r--spec/ruby/core/module/fixtures/autoload_c.rb11
-rw-r--r--spec/ruby/core/module/fixtures/autoload_callback.rb2
-rw-r--r--spec/ruby/core/module/fixtures/autoload_concur.rb9
-rw-r--r--spec/ruby/core/module/fixtures/autoload_const_source_location.rb6
-rw-r--r--spec/ruby/core/module/fixtures/autoload_d.rb11
-rw-r--r--spec/ruby/core/module/fixtures/autoload_during_autoload.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb6
-rw-r--r--spec/ruby/core/module/fixtures/autoload_during_require.rb4
-rw-r--r--spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb5
-rw-r--r--spec/ruby/core/module/fixtures/autoload_e.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_empty.rb1
-rw-r--r--spec/ruby/core/module/fixtures/autoload_ex1.rb16
-rw-r--r--spec/ruby/core/module/fixtures/autoload_exception.rb3
-rw-r--r--spec/ruby/core/module/fixtures/autoload_f.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_g.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_h.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_i.rb5
-rw-r--r--spec/ruby/core/module/fixtures/autoload_j.rb3
-rw-r--r--spec/ruby/core/module/fixtures/autoload_k.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_lm.rb4
-rw-r--r--spec/ruby/core/module/fixtures/autoload_location.rb3
-rw-r--r--spec/ruby/core/module/fixtures/autoload_nested.rb8
-rw-r--r--spec/ruby/core/module/fixtures/autoload_never_set.rb1
-rw-r--r--spec/ruby/core/module/fixtures/autoload_o.rb2
-rw-r--r--spec/ruby/core/module/fixtures/autoload_overridden.rb3
-rw-r--r--spec/ruby/core/module/fixtures/autoload_r.rb4
-rw-r--r--spec/ruby/core/module/fixtures/autoload_raise.rb2
-rw-r--r--spec/ruby/core/module/fixtures/autoload_relative_a.rb9
-rw-r--r--spec/ruby/core/module/fixtures/autoload_required_directly.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb1
-rw-r--r--spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb2
-rw-r--r--spec/ruby/core/module/fixtures/autoload_s.rb5
-rw-r--r--spec/ruby/core/module/fixtures/autoload_self_during_require.rb5
-rw-r--r--spec/ruby/core/module/fixtures/autoload_subclass.rb11
-rw-r--r--spec/ruby/core/module/fixtures/autoload_t.rb3
-rw-r--r--spec/ruby/core/module/fixtures/autoload_v.rb7
-rw-r--r--spec/ruby/core/module/fixtures/autoload_w.rb2
-rw-r--r--spec/ruby/core/module/fixtures/autoload_w2.rb1
-rw-r--r--spec/ruby/core/module/fixtures/autoload_x.rb3
-rw-r--r--spec/ruby/core/module/fixtures/autoload_z.rb5
-rw-r--r--spec/ruby/core/module/fixtures/classes.rb653
-rw-r--r--spec/ruby/core/module/fixtures/const_added.rb4
-rw-r--r--spec/ruby/core/module/fixtures/constant_unicode.rb5
-rw-r--r--spec/ruby/core/module/fixtures/constants_autoload.rb6
-rw-r--r--spec/ruby/core/module/fixtures/constants_autoload_a.rb2
-rw-r--r--spec/ruby/core/module/fixtures/constants_autoload_b.rb2
-rw-r--r--spec/ruby/core/module/fixtures/constants_autoload_c.rb3
-rw-r--r--spec/ruby/core/module/fixtures/constants_autoload_d.rb4
-rw-r--r--spec/ruby/core/module/fixtures/module.rb8
-rw-r--r--spec/ruby/core/module/fixtures/multi/foo.rb6
-rw-r--r--spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb11
-rw-r--r--spec/ruby/core/module/fixtures/name.rb13
-rw-r--r--spec/ruby/core/module/fixtures/path1/load_path.rb9
-rw-r--r--spec/ruby/core/module/fixtures/path2/load_path.rb1
-rw-r--r--spec/ruby/core/module/fixtures/refine.rb25
-rw-r--r--spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb8
-rw-r--r--spec/ruby/core/module/fixtures/set_temporary_name.rb4
-rw-r--r--spec/ruby/core/module/freeze_spec.rb6
-rw-r--r--spec/ruby/core/module/gt_spec.rb36
-rw-r--r--spec/ruby/core/module/gte_spec.rb33
-rw-r--r--spec/ruby/core/module/include_spec.rb628
-rw-r--r--spec/ruby/core/module/included_modules_spec.rb14
-rw-r--r--spec/ruby/core/module/included_spec.rb44
-rw-r--r--spec/ruby/core/module/initialize_copy_spec.rb18
-rw-r--r--spec/ruby/core/module/initialize_spec.rb18
-rw-r--r--spec/ruby/core/module/instance_method_spec.rb106
-rw-r--r--spec/ruby/core/module/instance_methods_spec.rb63
-rw-r--r--spec/ruby/core/module/lt_spec.rb36
-rw-r--r--spec/ruby/core/module/lte_spec.rb33
-rw-r--r--spec/ruby/core/module/method_added_spec.rb146
-rw-r--r--spec/ruby/core/module/method_defined_spec.rb98
-rw-r--r--spec/ruby/core/module/method_removed_spec.rb33
-rw-r--r--spec/ruby/core/module/method_undefined_spec.rb33
-rw-r--r--spec/ruby/core/module/module_eval_spec.rb7
-rw-r--r--spec/ruby/core/module/module_exec_spec.rb7
-rw-r--r--spec/ruby/core/module/module_function_spec.rb358
-rw-r--r--spec/ruby/core/module/name_spec.rb205
-rw-r--r--spec/ruby/core/module/nesting_spec.rb31
-rw-r--r--spec/ruby/core/module/new_spec.rb35
-rw-r--r--spec/ruby/core/module/prepend_features_spec.rb64
-rw-r--r--spec/ruby/core/module/prepend_spec.rb823
-rw-r--r--spec/ruby/core/module/prepended_spec.rb25
-rw-r--r--spec/ruby/core/module/private_class_method_spec.rb91
-rw-r--r--spec/ruby/core/module/private_constant_spec.rb32
-rw-r--r--spec/ruby/core/module/private_instance_methods_spec.rb54
-rw-r--r--spec/ruby/core/module/private_method_defined_spec.rb120
-rw-r--r--spec/ruby/core/module/private_spec.rb95
-rw-r--r--spec/ruby/core/module/protected_instance_methods_spec.rb57
-rw-r--r--spec/ruby/core/module/protected_method_defined_spec.rb120
-rw-r--r--spec/ruby/core/module/protected_spec.rb57
-rw-r--r--spec/ruby/core/module/public_class_method_spec.rb94
-rw-r--r--spec/ruby/core/module/public_constant_spec.rb38
-rw-r--r--spec/ruby/core/module/public_instance_method_spec.rb65
-rw-r--r--spec/ruby/core/module/public_instance_methods_spec.rb61
-rw-r--r--spec/ruby/core/module/public_method_defined_spec.rb72
-rw-r--r--spec/ruby/core/module/public_spec.rb45
-rw-r--r--spec/ruby/core/module/refine_spec.rb713
-rw-r--r--spec/ruby/core/module/refinements_spec.rb43
-rw-r--r--spec/ruby/core/module/remove_class_variable_spec.rb44
-rw-r--r--spec/ruby/core/module/remove_const_spec.rb107
-rw-r--r--spec/ruby/core/module/remove_method_spec.rb131
-rw-r--r--spec/ruby/core/module/ruby2_keywords_spec.rb248
-rw-r--r--spec/ruby/core/module/set_temporary_name_spec.rb145
-rw-r--r--spec/ruby/core/module/shared/attr_added.rb34
-rw-r--r--spec/ruby/core/module/shared/class_eval.rb172
-rw-r--r--spec/ruby/core/module/shared/class_exec.rb35
-rw-r--r--spec/ruby/core/module/shared/equal_value.rb14
-rw-r--r--spec/ruby/core/module/shared/set_visibility.rb184
-rw-r--r--spec/ruby/core/module/singleton_class_spec.rb27
-rw-r--r--spec/ruby/core/module/to_s_spec.rb70
-rw-r--r--spec/ruby/core/module/undef_method_spec.rb181
-rw-r--r--spec/ruby/core/module/undefined_instance_methods_spec.rb25
-rw-r--r--spec/ruby/core/module/used_refinements_spec.rb85
-rw-r--r--spec/ruby/core/module/using_spec.rb377
-rw-r--r--spec/ruby/core/mutex/lock_spec.rb92
-rw-r--r--spec/ruby/core/mutex/locked_spec.rb36
-rw-r--r--spec/ruby/core/mutex/owned_spec.rb53
-rw-r--r--spec/ruby/core/mutex/sleep_spec.rb111
-rw-r--r--spec/ruby/core/mutex/synchronize_spec.rb66
-rw-r--r--spec/ruby/core/mutex/try_lock_spec.rb32
-rw-r--r--spec/ruby/core/mutex/unlock_spec.rb38
-rw-r--r--spec/ruby/core/nil/and_spec.rb11
-rw-r--r--spec/ruby/core/nil/case_compare_spec.rb13
-rw-r--r--spec/ruby/core/nil/dup_spec.rb7
-rw-r--r--spec/ruby/core/nil/inspect_spec.rb7
-rw-r--r--spec/ruby/core/nil/match_spec.rb21
-rw-r--r--spec/ruby/core/nil/nil_spec.rb7
-rw-r--r--spec/ruby/core/nil/nilclass_spec.rb15
-rw-r--r--spec/ruby/core/nil/or_spec.rb11
-rw-r--r--spec/ruby/core/nil/rationalize_spec.rb16
-rw-r--r--spec/ruby/core/nil/singleton_method_spec.rb13
-rw-r--r--spec/ruby/core/nil/to_a_spec.rb7
-rw-r--r--spec/ruby/core/nil/to_c_spec.rb7
-rw-r--r--spec/ruby/core/nil/to_f_spec.rb11
-rw-r--r--spec/ruby/core/nil/to_h_spec.rb8
-rw-r--r--spec/ruby/core/nil/to_i_spec.rb11
-rw-r--r--spec/ruby/core/nil/to_r_spec.rb7
-rw-r--r--spec/ruby/core/nil/to_s_spec.rb15
-rw-r--r--spec/ruby/core/nil/xor_spec.rb11
-rw-r--r--spec/ruby/core/numeric/abs2_spec.rb34
-rw-r--r--spec/ruby/core/numeric/abs_spec.rb6
-rw-r--r--spec/ruby/core/numeric/angle_spec.rb6
-rw-r--r--spec/ruby/core/numeric/arg_spec.rb6
-rw-r--r--spec/ruby/core/numeric/ceil_spec.rb15
-rw-r--r--spec/ruby/core/numeric/clone_spec.rb30
-rw-r--r--spec/ruby/core/numeric/coerce_spec.rb59
-rw-r--r--spec/ruby/core/numeric/comparison_spec.rb48
-rw-r--r--spec/ruby/core/numeric/conj_spec.rb6
-rw-r--r--spec/ruby/core/numeric/conjugate_spec.rb6
-rw-r--r--spec/ruby/core/numeric/denominator_spec.rb24
-rw-r--r--spec/ruby/core/numeric/div_spec.rb22
-rw-r--r--spec/ruby/core/numeric/divmod_spec.rb15
-rw-r--r--spec/ruby/core/numeric/dup_spec.rb16
-rw-r--r--spec/ruby/core/numeric/eql_spec.rb22
-rw-r--r--spec/ruby/core/numeric/fdiv_spec.rb31
-rw-r--r--spec/ruby/core/numeric/finite_spec.rb8
-rw-r--r--spec/ruby/core/numeric/fixtures/classes.rb17
-rw-r--r--spec/ruby/core/numeric/floor_spec.rb14
-rw-r--r--spec/ruby/core/numeric/i_spec.rb15
-rw-r--r--spec/ruby/core/numeric/imag_spec.rb6
-rw-r--r--spec/ruby/core/numeric/imaginary_spec.rb6
-rw-r--r--spec/ruby/core/numeric/infinite_spec.rb8
-rw-r--r--spec/ruby/core/numeric/integer_spec.rb8
-rw-r--r--spec/ruby/core/numeric/magnitude_spec.rb6
-rw-r--r--spec/ruby/core/numeric/modulo_spec.rb24
-rw-r--r--spec/ruby/core/numeric/negative_spec.rb41
-rw-r--r--spec/ruby/core/numeric/nonzero_spec.rb18
-rw-r--r--spec/ruby/core/numeric/numerator_spec.rb33
-rw-r--r--spec/ruby/core/numeric/numeric_spec.rb7
-rw-r--r--spec/ruby/core/numeric/phase_spec.rb6
-rw-r--r--spec/ruby/core/numeric/polar_spec.rb50
-rw-r--r--spec/ruby/core/numeric/positive_spec.rb41
-rw-r--r--spec/ruby/core/numeric/quo_spec.rb63
-rw-r--r--spec/ruby/core/numeric/real_spec.rb37
-rw-r--r--spec/ruby/core/numeric/rect_spec.rb6
-rw-r--r--spec/ruby/core/numeric/rectangular_spec.rb6
-rw-r--r--spec/ruby/core/numeric/remainder_spec.rb68
-rw-r--r--spec/ruby/core/numeric/round_spec.rb14
-rw-r--r--spec/ruby/core/numeric/shared/abs.rb19
-rw-r--r--spec/ruby/core/numeric/shared/arg.rb38
-rw-r--r--spec/ruby/core/numeric/shared/conj.rb20
-rw-r--r--spec/ruby/core/numeric/shared/imag.rb26
-rw-r--r--spec/ruby/core/numeric/shared/rect.rb48
-rw-r--r--spec/ruby/core/numeric/shared/step.rb410
-rw-r--r--spec/ruby/core/numeric/singleton_method_added_spec.rb41
-rw-r--r--spec/ruby/core/numeric/step_spec.rb121
-rw-r--r--spec/ruby/core/numeric/to_c_spec.rb45
-rw-r--r--spec/ruby/core/numeric/to_int_spec.rb10
-rw-r--r--spec/ruby/core/numeric/truncate_spec.rb14
-rw-r--r--spec/ruby/core/numeric/uminus_spec.rb31
-rw-r--r--spec/ruby/core/numeric/uplus_spec.rb9
-rw-r--r--spec/ruby/core/numeric/zero_spec.rb18
-rw-r--r--spec/ruby/core/objectspace/_id2ref_spec.rb65
-rw-r--r--spec/ruby/core/objectspace/count_objects_spec.rb5
-rw-r--r--spec/ruby/core/objectspace/define_finalizer_spec.rb217
-rw-r--r--spec/ruby/core/objectspace/each_object_spec.rb213
-rw-r--r--spec/ruby/core/objectspace/fixtures/classes.rb64
-rw-r--r--spec/ruby/core/objectspace/garbage_collect_spec.rb22
-rw-r--r--spec/ruby/core/objectspace/undefine_finalizer_spec.rb33
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/clear_spec.rb25
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/delete_spec.rb49
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb105
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb80
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb5
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb26
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb19
-rw-r--r--spec/ruby/core/objectspace/weakkeymap/key_spec.rb42
-rw-r--r--spec/ruby/core/objectspace/weakmap/delete_spec.rb28
-rw-r--r--spec/ruby/core/objectspace/weakmap/each_key_spec.rb11
-rw-r--r--spec/ruby/core/objectspace/weakmap/each_pair_spec.rb11
-rw-r--r--spec/ruby/core/objectspace/weakmap/each_spec.rb11
-rw-r--r--spec/ruby/core/objectspace/weakmap/each_value_spec.rb11
-rw-r--r--spec/ruby/core/objectspace/weakmap/element_reference_spec.rb24
-rw-r--r--spec/ruby/core/objectspace/weakmap/element_set_spec.rb38
-rw-r--r--spec/ruby/core/objectspace/weakmap/include_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap/inspect_spec.rb25
-rw-r--r--spec/ruby/core/objectspace/weakmap/key_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap/keys_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap/length_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap/member_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap/shared/each.rb10
-rw-r--r--spec/ruby/core/objectspace/weakmap/shared/include.rb30
-rw-r--r--spec/ruby/core/objectspace/weakmap/shared/members.rb14
-rw-r--r--spec/ruby/core/objectspace/weakmap/shared/size.rb14
-rw-r--r--spec/ruby/core/objectspace/weakmap/size_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap/values_spec.rb6
-rw-r--r--spec/ruby/core/objectspace/weakmap_spec.rb12
-rw-r--r--spec/ruby/core/proc/allocate_spec.rb9
-rw-r--r--spec/ruby/core/proc/arity_spec.rb656
-rw-r--r--spec/ruby/core/proc/binding_spec.rb21
-rw-r--r--spec/ruby/core/proc/block_pass_spec.rb21
-rw-r--r--spec/ruby/core/proc/call_spec.rb16
-rw-r--r--spec/ruby/core/proc/case_compare_spec.rb16
-rw-r--r--spec/ruby/core/proc/clone_spec.rb28
-rw-r--r--spec/ruby/core/proc/compose_spec.rb142
-rw-r--r--spec/ruby/core/proc/curry_spec.rb179
-rw-r--r--spec/ruby/core/proc/dup_spec.rb26
-rw-r--r--spec/ruby/core/proc/element_reference_spec.rb27
-rw-r--r--spec/ruby/core/proc/eql_spec.rb6
-rw-r--r--spec/ruby/core/proc/equal_value_spec.rb6
-rw-r--r--spec/ruby/core/proc/fixtures/common.rb72
-rw-r--r--spec/ruby/core/proc/fixtures/proc_aref.rb10
-rw-r--r--spec/ruby/core/proc/fixtures/proc_aref_frozen.rb10
-rw-r--r--spec/ruby/core/proc/fixtures/source_location.rb55
-rw-r--r--spec/ruby/core/proc/hash_spec.rb17
-rw-r--r--spec/ruby/core/proc/inspect_spec.rb6
-rw-r--r--spec/ruby/core/proc/lambda_spec.rb55
-rw-r--r--spec/ruby/core/proc/new_spec.rb178
-rw-r--r--spec/ruby/core/proc/parameters_spec.rb183
-rw-r--r--spec/ruby/core/proc/ruby2_keywords_spec.rb66
-rw-r--r--spec/ruby/core/proc/shared/call.rb99
-rw-r--r--spec/ruby/core/proc/shared/call_arguments.rb29
-rw-r--r--spec/ruby/core/proc/shared/compose.rb22
-rw-r--r--spec/ruby/core/proc/shared/dup.rb39
-rw-r--r--spec/ruby/core/proc/shared/equal.rb83
-rw-r--r--spec/ruby/core/proc/shared/to_s.rb60
-rw-r--r--spec/ruby/core/proc/source_location_spec.rb91
-rw-r--r--spec/ruby/core/proc/to_proc_spec.rb9
-rw-r--r--spec/ruby/core/proc/to_s_spec.rb6
-rw-r--r--spec/ruby/core/proc/yield_spec.rb16
-rw-r--r--spec/ruby/core/process/_fork_spec.rb24
-rw-r--r--spec/ruby/core/process/abort_spec.rb6
-rw-r--r--spec/ruby/core/process/argv0_spec.rb23
-rw-r--r--spec/ruby/core/process/clock_getres_spec.rb33
-rw-r--r--spec/ruby/core/process/clock_gettime_spec.rb152
-rw-r--r--spec/ruby/core/process/constants_spec.rb126
-rw-r--r--spec/ruby/core/process/daemon_spec.rb118
-rw-r--r--spec/ruby/core/process/detach_spec.rb81
-rw-r--r--spec/ruby/core/process/egid_spec.rb58
-rw-r--r--spec/ruby/core/process/euid_spec.rb56
-rw-r--r--spec/ruby/core/process/exec_spec.rb241
-rw-r--r--spec/ruby/core/process/exit_spec.rb10
-rw-r--r--spec/ruby/core/process/fixtures/argv0.rb6
-rw-r--r--spec/ruby/core/process/fixtures/clocks.rb18
-rw-r--r--spec/ruby/core/process/fixtures/common.rb88
-rw-r--r--spec/ruby/core/process/fixtures/daemon.rb111
-rw-r--r--spec/ruby/core/process/fixtures/in.txt1
-rw-r--r--spec/ruby/core/process/fixtures/kill.rb43
-rw-r--r--spec/ruby/core/process/fixtures/map_fd.rb9
-rw-r--r--spec/ruby/core/process/fixtures/setpriority.rb12
-rw-r--r--spec/ruby/core/process/fork_spec.rb6
-rw-r--r--spec/ruby/core/process/getpgid_spec.rb17
-rw-r--r--spec/ruby/core/process/getpgrp_spec.rb7
-rw-r--r--spec/ruby/core/process/getpriority_spec.rb23
-rw-r--r--spec/ruby/core/process/getrlimit_spec.rb100
-rw-r--r--spec/ruby/core/process/gid/change_privilege_spec.rb5
-rw-r--r--spec/ruby/core/process/gid/eid_spec.rb9
-rw-r--r--spec/ruby/core/process/gid/grant_privilege_spec.rb5
-rw-r--r--spec/ruby/core/process/gid/re_exchange_spec.rb5
-rw-r--r--spec/ruby/core/process/gid/re_exchangeable_spec.rb5
-rw-r--r--spec/ruby/core/process/gid/rid_spec.rb5
-rw-r--r--spec/ruby/core/process/gid/sid_available_spec.rb5
-rw-r--r--spec/ruby/core/process/gid/switch_spec.rb5
-rw-r--r--spec/ruby/core/process/gid_spec.rb22
-rw-r--r--spec/ruby/core/process/groups_spec.rb67
-rw-r--r--spec/ruby/core/process/initgroups_spec.rb22
-rw-r--r--spec/ruby/core/process/kill_spec.rb132
-rw-r--r--spec/ruby/core/process/last_status_spec.rb18
-rw-r--r--spec/ruby/core/process/maxgroups_spec.rb19
-rw-r--r--spec/ruby/core/process/pid_spec.rb9
-rw-r--r--spec/ruby/core/process/ppid_spec.rb9
-rw-r--r--spec/ruby/core/process/set_proctitle_spec.rb23
-rw-r--r--spec/ruby/core/process/setpgid_spec.rb29
-rw-r--r--spec/ruby/core/process/setpgrp_spec.rb37
-rw-r--r--spec/ruby/core/process/setpriority_spec.rb60
-rw-r--r--spec/ruby/core/process/setrlimit_spec.rb237
-rw-r--r--spec/ruby/core/process/setsid_spec.rb16
-rw-r--r--spec/ruby/core/process/spawn_spec.rb796
-rw-r--r--spec/ruby/core/process/status/bit_and_spec.rb38
-rw-r--r--spec/ruby/core/process/status/coredump_spec.rb5
-rw-r--r--spec/ruby/core/process/status/equal_value_spec.rb15
-rw-r--r--spec/ruby/core/process/status/exited_spec.rb32
-rw-r--r--spec/ruby/core/process/status/exitstatus_spec.rb25
-rw-r--r--spec/ruby/core/process/status/inspect_spec.rb5
-rw-r--r--spec/ruby/core/process/status/pid_spec.rb15
-rw-r--r--spec/ruby/core/process/status/right_shift_spec.rb37
-rw-r--r--spec/ruby/core/process/status/signaled_spec.rb31
-rw-r--r--spec/ruby/core/process/status/stopped_spec.rb5
-rw-r--r--spec/ruby/core/process/status/stopsig_spec.rb5
-rw-r--r--spec/ruby/core/process/status/success_spec.rb41
-rw-r--r--spec/ruby/core/process/status/termsig_spec.rb43
-rw-r--r--spec/ruby/core/process/status/to_i_spec.rb13
-rw-r--r--spec/ruby/core/process/status/to_int_spec.rb5
-rw-r--r--spec/ruby/core/process/status/to_s_spec.rb5
-rw-r--r--spec/ruby/core/process/status/wait_spec.rb100
-rw-r--r--spec/ruby/core/process/sys/getegid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/geteuid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/getgid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/getuid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/issetugid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setegid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/seteuid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setgid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setregid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setresgid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setresuid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setreuid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setrgid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setruid_spec.rb5
-rw-r--r--spec/ruby/core/process/sys/setuid_spec.rb5
-rw-r--r--spec/ruby/core/process/times_spec.rb19
-rw-r--r--spec/ruby/core/process/tms/cstime_spec.rb17
-rw-r--r--spec/ruby/core/process/tms/cutime_spec.rb17
-rw-r--r--spec/ruby/core/process/tms/stime_spec.rb17
-rw-r--r--spec/ruby/core/process/tms/utime_spec.rb17
-rw-r--r--spec/ruby/core/process/uid/change_privilege_spec.rb5
-rw-r--r--spec/ruby/core/process/uid/eid_spec.rb9
-rw-r--r--spec/ruby/core/process/uid/grant_privilege_spec.rb5
-rw-r--r--spec/ruby/core/process/uid/re_exchange_spec.rb5
-rw-r--r--spec/ruby/core/process/uid/re_exchangeable_spec.rb5
-rw-r--r--spec/ruby/core/process/uid/rid_spec.rb5
-rw-r--r--spec/ruby/core/process/uid/sid_available_spec.rb5
-rw-r--r--spec/ruby/core/process/uid/switch_spec.rb5
-rw-r--r--spec/ruby/core/process/uid_spec.rb57
-rw-r--r--spec/ruby/core/process/wait2_spec.rb45
-rw-r--r--spec/ruby/core/process/wait_spec.rb91
-rw-r--r--spec/ruby/core/process/waitall_spec.rb48
-rw-r--r--spec/ruby/core/process/waitpid2_spec.rb5
-rw-r--r--spec/ruby/core/process/waitpid_spec.rb14
-rw-r--r--spec/ruby/core/process/warmup_spec.rb9
-rw-r--r--spec/ruby/core/queue/append_spec.rb6
-rw-r--r--spec/ruby/core/queue/clear_spec.rb6
-rw-r--r--spec/ruby/core/queue/close_spec.rb6
-rw-r--r--spec/ruby/core/queue/closed_spec.rb6
-rw-r--r--spec/ruby/core/queue/deq_spec.rb11
-rw-r--r--spec/ruby/core/queue/empty_spec.rb6
-rw-r--r--spec/ruby/core/queue/enq_spec.rb6
-rw-r--r--spec/ruby/core/queue/freeze_spec.rb6
-rw-r--r--spec/ruby/core/queue/initialize_spec.rb60
-rw-r--r--spec/ruby/core/queue/length_spec.rb6
-rw-r--r--spec/ruby/core/queue/num_waiting_spec.rb6
-rw-r--r--spec/ruby/core/queue/pop_spec.rb11
-rw-r--r--spec/ruby/core/queue/push_spec.rb6
-rw-r--r--spec/ruby/core/queue/shift_spec.rb11
-rw-r--r--spec/ruby/core/queue/size_spec.rb6
-rw-r--r--spec/ruby/core/random/bytes_spec.rb29
-rw-r--r--spec/ruby/core/random/default_spec.rb7
-rw-r--r--spec/ruby/core/random/equal_value_spec.rb37
-rw-r--r--spec/ruby/core/random/fixtures/classes.rb15
-rw-r--r--spec/ruby/core/random/new_seed_spec.rb24
-rw-r--r--spec/ruby/core/random/new_spec.rb38
-rw-r--r--spec/ruby/core/random/rand_spec.rb224
-rw-r--r--spec/ruby/core/random/random_number_spec.rb8
-rw-r--r--spec/ruby/core/random/seed_spec.rb29
-rw-r--r--spec/ruby/core/random/shared/bytes.rb17
-rw-r--r--spec/ruby/core/random/shared/rand.rb9
-rw-r--r--spec/ruby/core/random/srand_spec.rb39
-rw-r--r--spec/ruby/core/random/urandom_spec.rb25
-rw-r--r--spec/ruby/core/range/begin_spec.rb6
-rw-r--r--spec/ruby/core/range/bsearch_spec.rb466
-rw-r--r--spec/ruby/core/range/case_compare_spec.rb17
-rw-r--r--spec/ruby/core/range/clone_spec.rb26
-rw-r--r--spec/ruby/core/range/count_spec.rb12
-rw-r--r--spec/ruby/core/range/cover_spec.rb14
-rw-r--r--spec/ruby/core/range/dup_spec.rb23
-rw-r--r--spec/ruby/core/range/each_spec.rb101
-rw-r--r--spec/ruby/core/range/end_spec.rb6
-rw-r--r--spec/ruby/core/range/eql_spec.rb10
-rw-r--r--spec/ruby/core/range/equal_value_spec.rb18
-rw-r--r--spec/ruby/core/range/exclude_end_spec.rb19
-rw-r--r--spec/ruby/core/range/first_spec.rb53
-rw-r--r--spec/ruby/core/range/fixtures/classes.rb90
-rw-r--r--spec/ruby/core/range/frozen_spec.rb25
-rw-r--r--spec/ruby/core/range/hash_spec.rb24
-rw-r--r--spec/ruby/core/range/include_spec.rb14
-rw-r--r--spec/ruby/core/range/initialize_spec.rb41
-rw-r--r--spec/ruby/core/range/inspect_spec.rb29
-rw-r--r--spec/ruby/core/range/last_spec.rb57
-rw-r--r--spec/ruby/core/range/max_spec.rb115
-rw-r--r--spec/ruby/core/range/member_spec.rb10
-rw-r--r--spec/ruby/core/range/min_spec.rb88
-rw-r--r--spec/ruby/core/range/minmax_spec.rb130
-rw-r--r--spec/ruby/core/range/new_spec.rb77
-rw-r--r--spec/ruby/core/range/overlap_spec.rb87
-rw-r--r--spec/ruby/core/range/percent_spec.rb16
-rw-r--r--spec/ruby/core/range/range_spec.rb7
-rw-r--r--spec/ruby/core/range/reverse_each_spec.rb125
-rw-r--r--spec/ruby/core/range/shared/begin.rb10
-rw-r--r--spec/ruby/core/range/shared/cover.rb193
-rw-r--r--spec/ruby/core/range/shared/cover_and_include.rb86
-rw-r--r--spec/ruby/core/range/shared/end.rb10
-rw-r--r--spec/ruby/core/range/shared/equal_value.rb51
-rw-r--r--spec/ruby/core/range/shared/include.rb91
-rw-r--r--spec/ruby/core/range/size_spec.rb92
-rw-r--r--spec/ruby/core/range/step_spec.rb725
-rw-r--r--spec/ruby/core/range/to_a_spec.rb39
-rw-r--r--spec/ruby/core/range/to_s_spec.rb23
-rw-r--r--spec/ruby/core/range/to_set_spec.rb54
-rw-r--r--spec/ruby/core/rational/abs_spec.rb6
-rw-r--r--spec/ruby/core/rational/ceil_spec.rb48
-rw-r--r--spec/ruby/core/rational/comparison_spec.rb93
-rw-r--r--spec/ruby/core/rational/denominator_spec.rb14
-rw-r--r--spec/ruby/core/rational/div_spec.rb54
-rw-r--r--spec/ruby/core/rational/divide_spec.rb74
-rw-r--r--spec/ruby/core/rational/divmod_spec.rb42
-rw-r--r--spec/ruby/core/rational/equal_value_spec.rb39
-rw-r--r--spec/ruby/core/rational/exponent_spec.rb236
-rw-r--r--spec/ruby/core/rational/fdiv_spec.rb5
-rw-r--r--spec/ruby/core/rational/fixtures/rational.rb14
-rw-r--r--spec/ruby/core/rational/floor_spec.rb49
-rw-r--r--spec/ruby/core/rational/hash_spec.rb9
-rw-r--r--spec/ruby/core/rational/inspect_spec.rb14
-rw-r--r--spec/ruby/core/rational/integer_spec.rb13
-rw-r--r--spec/ruby/core/rational/magnitude_spec.rb6
-rw-r--r--spec/ruby/core/rational/marshal_dump_spec.rb11
-rw-r--r--spec/ruby/core/rational/minus_spec.rb51
-rw-r--r--spec/ruby/core/rational/modulo_spec.rb43
-rw-r--r--spec/ruby/core/rational/multiply_spec.rb65
-rw-r--r--spec/ruby/core/rational/numerator_spec.rb10
-rw-r--r--spec/ruby/core/rational/plus_spec.rb50
-rw-r--r--spec/ruby/core/rational/quo_spec.rb25
-rw-r--r--spec/ruby/core/rational/rational_spec.rb11
-rw-r--r--spec/ruby/core/rational/rationalize_spec.rb36
-rw-r--r--spec/ruby/core/rational/remainder_spec.rb5
-rw-r--r--spec/ruby/core/rational/round_spec.rb106
-rw-r--r--spec/ruby/core/rational/shared/abs.rb11
-rw-r--r--spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb11
-rw-r--r--spec/ruby/core/rational/to_f_spec.rb16
-rw-r--r--spec/ruby/core/rational/to_i_spec.rb12
-rw-r--r--spec/ruby/core/rational/to_r_spec.rb26
-rw-r--r--spec/ruby/core/rational/to_s_spec.rb14
-rw-r--r--spec/ruby/core/rational/truncate_spec.rb71
-rw-r--r--spec/ruby/core/rational/zero_spec.rb14
-rw-r--r--spec/ruby/core/refinement/append_features_spec.rb19
-rw-r--r--spec/ruby/core/refinement/extend_object_spec.rb21
-rw-r--r--spec/ruby/core/refinement/fixtures/classes.rb10
-rw-r--r--spec/ruby/core/refinement/import_methods_spec.rb287
-rw-r--r--spec/ruby/core/refinement/include_spec.rb13
-rw-r--r--spec/ruby/core/refinement/prepend_features_spec.rb19
-rw-r--r--spec/ruby/core/refinement/prepend_spec.rb13
-rw-r--r--spec/ruby/core/refinement/refined_class_spec.rb34
-rw-r--r--spec/ruby/core/refinement/shared/target.rb13
-rw-r--r--spec/ruby/core/refinement/target_spec.rb6
-rw-r--r--spec/ruby/core/regexp/case_compare_spec.rb35
-rw-r--r--spec/ruby/core/regexp/casefold_spec.rb8
-rw-r--r--spec/ruby/core/regexp/compile_spec.rb19
-rw-r--r--spec/ruby/core/regexp/encoding_spec.rb62
-rw-r--r--spec/ruby/core/regexp/eql_spec.rb6
-rw-r--r--spec/ruby/core/regexp/equal_value_spec.rb6
-rw-r--r--spec/ruby/core/regexp/escape_spec.rb6
-rw-r--r--spec/ruby/core/regexp/fixed_encoding_spec.rb36
-rw-r--r--spec/ruby/core/regexp/hash_spec.rb20
-rw-r--r--spec/ruby/core/regexp/initialize_spec.rb29
-rw-r--r--spec/ruby/core/regexp/inspect_spec.rb44
-rw-r--r--spec/ruby/core/regexp/last_match_spec.rb56
-rw-r--r--spec/ruby/core/regexp/linear_time_spec.rb80
-rw-r--r--spec/ruby/core/regexp/match_spec.rb146
-rw-r--r--spec/ruby/core/regexp/named_captures_spec.rb35
-rw-r--r--spec/ruby/core/regexp/names_spec.rb29
-rw-r--r--spec/ruby/core/regexp/new_spec.rb19
-rw-r--r--spec/ruby/core/regexp/options_spec.rb54
-rw-r--r--spec/ruby/core/regexp/quote_spec.rb6
-rw-r--r--spec/ruby/core/regexp/shared/equal_value.rb31
-rw-r--r--spec/ruby/core/regexp/shared/new.rb321
-rw-r--r--spec/ruby/core/regexp/shared/quote.rb41
-rw-r--r--spec/ruby/core/regexp/source_spec.rb47
-rw-r--r--spec/ruby/core/regexp/timeout_spec.rb33
-rw-r--r--spec/ruby/core/regexp/to_s_spec.rb62
-rw-r--r--spec/ruby/core/regexp/try_convert_spec.rb27
-rw-r--r--spec/ruby/core/regexp/union_spec.rb182
-rw-r--r--spec/ruby/core/set/add_spec.rb34
-rw-r--r--spec/ruby/core/set/append_spec.rb6
-rw-r--r--spec/ruby/core/set/case_compare_spec.rb11
-rw-r--r--spec/ruby/core/set/case_equality_spec.rb6
-rw-r--r--spec/ruby/core/set/classify_spec.rb26
-rw-r--r--spec/ruby/core/set/clear_spec.rb16
-rw-r--r--spec/ruby/core/set/collect_spec.rb6
-rw-r--r--spec/ruby/core/set/compare_by_identity_spec.rb153
-rw-r--r--spec/ruby/core/set/comparison_spec.rb26
-rw-r--r--spec/ruby/core/set/constructor_spec.rb14
-rw-r--r--spec/ruby/core/set/delete_if_spec.rb37
-rw-r--r--spec/ruby/core/set/delete_spec.rb36
-rw-r--r--spec/ruby/core/set/difference_spec.rb6
-rw-r--r--spec/ruby/core/set/disjoint_spec.rb22
-rw-r--r--spec/ruby/core/set/divide_spec.rb68
-rw-r--r--spec/ruby/core/set/each_spec.rb26
-rw-r--r--spec/ruby/core/set/empty_spec.rb9
-rw-r--r--spec/ruby/core/set/enumerable/to_set_spec.rb12
-rw-r--r--spec/ruby/core/set/eql_spec.rb14
-rw-r--r--spec/ruby/core/set/equal_value_spec.rb34
-rw-r--r--spec/ruby/core/set/exclusion_spec.rb17
-rw-r--r--spec/ruby/core/set/filter_spec.rb6
-rw-r--r--spec/ruby/core/set/fixtures/set_like.rb30
-rw-r--r--spec/ruby/core/set/flatten_merge_spec.rb24
-rw-r--r--spec/ruby/core/set/flatten_spec.rb49
-rw-r--r--spec/ruby/core/set/hash_spec.rb19
-rw-r--r--spec/ruby/core/set/include_spec.rb6
-rw-r--r--spec/ruby/core/set/initialize_clone_spec.rb15
-rw-r--r--spec/ruby/core/set/initialize_spec.rb88
-rw-r--r--spec/ruby/core/set/inspect_spec.rb6
-rw-r--r--spec/ruby/core/set/intersect_spec.rb22
-rw-r--r--spec/ruby/core/set/intersection_spec.rb10
-rw-r--r--spec/ruby/core/set/join_spec.rb30
-rw-r--r--spec/ruby/core/set/keep_if_spec.rb37
-rw-r--r--spec/ruby/core/set/length_spec.rb6
-rw-r--r--spec/ruby/core/set/map_spec.rb6
-rw-r--r--spec/ruby/core/set/member_spec.rb6
-rw-r--r--spec/ruby/core/set/merge_spec.rb29
-rw-r--r--spec/ruby/core/set/minus_spec.rb6
-rw-r--r--spec/ruby/core/set/plus_spec.rb6
-rw-r--r--spec/ruby/core/set/pretty_print_cycle_spec.rb14
-rw-r--r--spec/ruby/core/set/proper_subset_spec.rb35
-rw-r--r--spec/ruby/core/set/proper_superset_spec.rb42
-rw-r--r--spec/ruby/core/set/reject_spec.rb41
-rw-r--r--spec/ruby/core/set/replace_spec.rb24
-rw-r--r--spec/ruby/core/set/select_spec.rb6
-rw-r--r--spec/ruby/core/set/set_spec.rb10
-rw-r--r--spec/ruby/core/set/shared/add.rb14
-rw-r--r--spec/ruby/core/set/shared/collect.rb20
-rw-r--r--spec/ruby/core/set/shared/difference.rb15
-rw-r--r--spec/ruby/core/set/shared/include.rb29
-rw-r--r--spec/ruby/core/set/shared/inspect.rb45
-rw-r--r--spec/ruby/core/set/shared/intersection.rb15
-rw-r--r--spec/ruby/core/set/shared/length.rb6
-rw-r--r--spec/ruby/core/set/shared/select.rb41
-rw-r--r--spec/ruby/core/set/shared/union.rb15
-rw-r--r--spec/ruby/core/set/size_spec.rb6
-rw-r--r--spec/ruby/core/set/sortedset/sortedset_spec.rb13
-rw-r--r--spec/ruby/core/set/subset_spec.rb35
-rw-r--r--spec/ruby/core/set/subtract_spec.rb16
-rw-r--r--spec/ruby/core/set/superset_spec.rb42
-rw-r--r--spec/ruby/core/set/to_a_spec.rb7
-rw-r--r--spec/ruby/core/set/to_s_spec.rb11
-rw-r--r--spec/ruby/core/set/union_spec.rb10
-rw-r--r--spec/ruby/core/signal/fixtures/trap_all.rb15
-rw-r--r--spec/ruby/core/signal/list_spec.rb68
-rw-r--r--spec/ruby/core/signal/signame_spec.rb34
-rw-r--r--spec/ruby/core/signal/trap_spec.rb320
-rw-r--r--spec/ruby/core/sizedqueue/append_spec.rb16
-rw-r--r--spec/ruby/core/sizedqueue/clear_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/close_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/closed_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/deq_spec.rb11
-rw-r--r--spec/ruby/core/sizedqueue/empty_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/enq_spec.rb16
-rw-r--r--spec/ruby/core/sizedqueue/freeze_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/length_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/max_spec.rb10
-rw-r--r--spec/ruby/core/sizedqueue/new_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/num_waiting_spec.rb6
-rw-r--r--spec/ruby/core/sizedqueue/pop_spec.rb11
-rw-r--r--spec/ruby/core/sizedqueue/push_spec.rb16
-rw-r--r--spec/ruby/core/sizedqueue/shift_spec.rb11
-rw-r--r--spec/ruby/core/sizedqueue/size_spec.rb6
-rw-r--r--spec/ruby/core/string/allocate_spec.rb19
-rw-r--r--spec/ruby/core/string/append_as_bytes_spec.rb60
-rw-r--r--spec/ruby/core/string/append_spec.rb14
-rw-r--r--spec/ruby/core/string/ascii_only_spec.rb82
-rw-r--r--spec/ruby/core/string/b_spec.rb16
-rw-r--r--spec/ruby/core/string/byteindex_spec.rb298
-rw-r--r--spec/ruby/core/string/byterindex_spec.rb353
-rw-r--r--spec/ruby/core/string/bytes_spec.rb55
-rw-r--r--spec/ruby/core/string/bytesize_spec.rb33
-rw-r--r--spec/ruby/core/string/byteslice_spec.rb33
-rw-r--r--spec/ruby/core/string/bytesplice_spec.rb290
-rw-r--r--spec/ruby/core/string/capitalize_spec.rb207
-rw-r--r--spec/ruby/core/string/case_compare_spec.rb8
-rw-r--r--spec/ruby/core/string/casecmp_spec.rb204
-rw-r--r--spec/ruby/core/string/center_spec.rb117
-rw-r--r--spec/ruby/core/string/chars_spec.rb16
-rw-r--r--spec/ruby/core/string/chilled_string_spec.rb151
-rw-r--r--spec/ruby/core/string/chomp_spec.rb366
-rw-r--r--spec/ruby/core/string/chop_spec.rb119
-rw-r--r--spec/ruby/core/string/chr_spec.rb42
-rw-r--r--spec/ruby/core/string/clear_spec.rb38
-rw-r--r--spec/ruby/core/string/clone_spec.rb61
-rw-r--r--spec/ruby/core/string/codepoints_spec.rb18
-rw-r--r--spec/ruby/core/string/comparison_spec.rb112
-rw-r--r--spec/ruby/core/string/concat_spec.rb27
-rw-r--r--spec/ruby/core/string/count_spec.rb105
-rw-r--r--spec/ruby/core/string/crypt_spec.rb92
-rw-r--r--spec/ruby/core/string/dedup_spec.rb6
-rw-r--r--spec/ruby/core/string/delete_prefix_spec.rb83
-rw-r--r--spec/ruby/core/string/delete_spec.rb117
-rw-r--r--spec/ruby/core/string/delete_suffix_spec.rb83
-rw-r--r--spec/ruby/core/string/downcase_spec.rb195
-rw-r--r--spec/ruby/core/string/dump_spec.rb396
-rw-r--r--spec/ruby/core/string/dup_spec.rb65
-rw-r--r--spec/ruby/core/string/each_byte_spec.rb61
-rw-r--r--spec/ruby/core/string/each_char_spec.rb8
-rw-r--r--spec/ruby/core/string/each_codepoint_spec.rb8
-rw-r--r--spec/ruby/core/string/each_grapheme_cluster_spec.rb16
-rw-r--r--spec/ruby/core/string/each_line_spec.rb9
-rw-r--r--spec/ruby/core/string/element_reference_spec.rb35
-rw-r--r--spec/ruby/core/string/element_set_spec.rb589
-rw-r--r--spec/ruby/core/string/empty_spec.rb12
-rw-r--r--spec/ruby/core/string/encode_spec.rb240
-rw-r--r--spec/ruby/core/string/encoding_spec.rb184
-rw-r--r--spec/ruby/core/string/end_with_spec.rb8
-rw-r--r--spec/ruby/core/string/eql_spec.rb21
-rw-r--r--spec/ruby/core/string/equal_value_spec.rb8
-rw-r--r--spec/ruby/core/string/fixtures/classes.rb60
-rw-r--r--spec/ruby/core/string/fixtures/freeze_magic_comment.rb3
-rw-r--r--spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb9
-rw-r--r--spec/ruby/core/string/fixtures/to_c.rb5
-rw-r--r--spec/ruby/core/string/force_encoding_spec.rb72
-rw-r--r--spec/ruby/core/string/freeze_spec.rb18
-rw-r--r--spec/ruby/core/string/getbyte_spec.rb69
-rw-r--r--spec/ruby/core/string/grapheme_clusters_spec.rb14
-rw-r--r--spec/ruby/core/string/gsub_spec.rb615
-rw-r--r--spec/ruby/core/string/hash_spec.rb9
-rw-r--r--spec/ruby/core/string/hex_spec.rb49
-rw-r--r--spec/ruby/core/string/include_spec.rb49
-rw-r--r--spec/ruby/core/string/index_spec.rb337
-rw-r--r--spec/ruby/core/string/initialize_spec.rb26
-rw-r--r--spec/ruby/core/string/insert_spec.rb81
-rw-r--r--spec/ruby/core/string/inspect_spec.rb520
-rw-r--r--spec/ruby/core/string/intern_spec.rb7
-rw-r--r--spec/ruby/core/string/length_spec.rb7
-rw-r--r--spec/ruby/core/string/lines_spec.rb19
-rw-r--r--spec/ruby/core/string/ljust_spec.rb100
-rw-r--r--spec/ruby/core/string/lstrip_spec.rb74
-rw-r--r--spec/ruby/core/string/match_spec.rb175
-rw-r--r--spec/ruby/core/string/modulo_spec.rb797
-rw-r--r--spec/ruby/core/string/multiply_spec.rb7
-rw-r--r--spec/ruby/core/string/new_spec.rb61
-rw-r--r--spec/ruby/core/string/next_spec.rb11
-rw-r--r--spec/ruby/core/string/oct_spec.rb88
-rw-r--r--spec/ruby/core/string/ord_spec.rb33
-rw-r--r--spec/ruby/core/string/partition_spec.rb63
-rw-r--r--spec/ruby/core/string/plus_spec.rb37
-rw-r--r--spec/ruby/core/string/prepend_spec.rb55
-rw-r--r--spec/ruby/core/string/replace_spec.rb7
-rw-r--r--spec/ruby/core/string/reverse_spec.rb70
-rw-r--r--spec/ruby/core/string/rindex_spec.rb384
-rw-r--r--spec/ruby/core/string/rjust_spec.rb100
-rw-r--r--spec/ruby/core/string/rpartition_spec.rb71
-rw-r--r--spec/ruby/core/string/rstrip_spec.rb80
-rw-r--r--spec/ruby/core/string/scan_spec.rb173
-rw-r--r--spec/ruby/core/string/scrub_spec.rb164
-rw-r--r--spec/ruby/core/string/setbyte_spec.rb112
-rw-r--r--spec/ruby/core/string/shared/byte_index_common.rb63
-rw-r--r--spec/ruby/core/string/shared/chars.rb86
-rw-r--r--spec/ruby/core/string/shared/codepoints.rb67
-rw-r--r--spec/ruby/core/string/shared/concat.rb159
-rw-r--r--spec/ruby/core/string/shared/dedup.rb51
-rw-r--r--spec/ruby/core/string/shared/each_char_without_block.rb26
-rw-r--r--spec/ruby/core/string/shared/each_codepoint_without_block.rb33
-rw-r--r--spec/ruby/core/string/shared/each_line.rb198
-rw-r--r--spec/ruby/core/string/shared/each_line_without_block.rb17
-rw-r--r--spec/ruby/core/string/shared/encode.rb448
-rw-r--r--spec/ruby/core/string/shared/eql.rb38
-rw-r--r--spec/ruby/core/string/shared/equal_value.rb29
-rw-r--r--spec/ruby/core/string/shared/grapheme_clusters.rb25
-rw-r--r--spec/ruby/core/string/shared/length.rb55
-rw-r--r--spec/ruby/core/string/shared/partition.rb33
-rw-r--r--spec/ruby/core/string/shared/replace.rb48
-rw-r--r--spec/ruby/core/string/shared/slice.rb517
-rw-r--r--spec/ruby/core/string/shared/strip.rb14
-rw-r--r--spec/ruby/core/string/shared/succ.rb87
-rw-r--r--spec/ruby/core/string/shared/to_s.rb13
-rw-r--r--spec/ruby/core/string/shared/to_sym.rb72
-rw-r--r--spec/ruby/core/string/size_spec.rb7
-rw-r--r--spec/ruby/core/string/slice_spec.rb390
-rw-r--r--spec/ruby/core/string/split_spec.rb546
-rw-r--r--spec/ruby/core/string/squeeze_spec.rb111
-rw-r--r--spec/ruby/core/string/start_with_spec.rb18
-rw-r--r--spec/ruby/core/string/string_spec.rb7
-rw-r--r--spec/ruby/core/string/strip_spec.rb58
-rw-r--r--spec/ruby/core/string/sub_spec.rb512
-rw-r--r--spec/ruby/core/string/succ_spec.rb11
-rw-r--r--spec/ruby/core/string/sum_spec.rb22
-rw-r--r--spec/ruby/core/string/swapcase_spec.rb193
-rw-r--r--spec/ruby/core/string/to_c_spec.rb53
-rw-r--r--spec/ruby/core/string/to_f_spec.rb140
-rw-r--r--spec/ruby/core/string/to_i_spec.rb349
-rw-r--r--spec/ruby/core/string/to_r_spec.rb62
-rw-r--r--spec/ruby/core/string/to_s_spec.rb7
-rw-r--r--spec/ruby/core/string/to_str_spec.rb7
-rw-r--r--spec/ruby/core/string/to_sym_spec.rb7
-rw-r--r--spec/ruby/core/string/tr_s_spec.rb131
-rw-r--r--spec/ruby/core/string/tr_spec.rb126
-rw-r--r--spec/ruby/core/string/try_convert_spec.rb50
-rw-r--r--spec/ruby/core/string/uminus_spec.rb6
-rw-r--r--spec/ruby/core/string/undump_spec.rb441
-rw-r--r--spec/ruby/core/string/unicode_normalize_spec.rb116
-rw-r--r--spec/ruby/core/string/unicode_normalized_spec.rb75
-rw-r--r--spec/ruby/core/string/unpack/a_spec.rb66
-rw-r--r--spec/ruby/core/string/unpack/at_spec.rb29
-rw-r--r--spec/ruby/core/string/unpack/b_spec.rb201
-rw-r--r--spec/ruby/core/string/unpack/c_spec.rb65
-rw-r--r--spec/ruby/core/string/unpack/carret_spec.rb43
-rw-r--r--spec/ruby/core/string/unpack/comment_spec.rb25
-rw-r--r--spec/ruby/core/string/unpack/d_spec.rb28
-rw-r--r--spec/ruby/core/string/unpack/e_spec.rb14
-rw-r--r--spec/ruby/core/string/unpack/f_spec.rb28
-rw-r--r--spec/ruby/core/string/unpack/g_spec.rb14
-rw-r--r--spec/ruby/core/string/unpack/h_spec.rb139
-rw-r--r--spec/ruby/core/string/unpack/i_spec.rb152
-rw-r--r--spec/ruby/core/string/unpack/j_spec.rb272
-rw-r--r--spec/ruby/core/string/unpack/l_spec.rb265
-rw-r--r--spec/ruby/core/string/unpack/m_spec.rb192
-rw-r--r--spec/ruby/core/string/unpack/n_spec.rb18
-rw-r--r--spec/ruby/core/string/unpack/p_spec.rb44
-rw-r--r--spec/ruby/core/string/unpack/percent_spec.rb7
-rw-r--r--spec/ruby/core/string/unpack/q_spec.rb64
-rw-r--r--spec/ruby/core/string/unpack/r_spec.rb85
-rw-r--r--spec/ruby/core/string/unpack/s_spec.rb152
-rw-r--r--spec/ruby/core/string/unpack/shared/basic.rb27
-rw-r--r--spec/ruby/core/string/unpack/shared/float.rb277
-rw-r--r--spec/ruby/core/string/unpack/shared/integer.rb349
-rw-r--r--spec/ruby/core/string/unpack/shared/string.rb51
-rw-r--r--spec/ruby/core/string/unpack/shared/taint.rb2
-rw-r--r--spec/ruby/core/string/unpack/shared/unicode.rb62
-rw-r--r--spec/ruby/core/string/unpack/u_spec.rb97
-rw-r--r--spec/ruby/core/string/unpack/v_spec.rb18
-rw-r--r--spec/ruby/core/string/unpack/w_spec.rb37
-rw-r--r--spec/ruby/core/string/unpack/x_spec.rb62
-rw-r--r--spec/ruby/core/string/unpack/z_spec.rb28
-rw-r--r--spec/ruby/core/string/unpack1_spec.rb61
-rw-r--r--spec/ruby/core/string/unpack_spec.rb46
-rw-r--r--spec/ruby/core/string/upcase_spec.rb187
-rw-r--r--spec/ruby/core/string/uplus_spec.rb60
-rw-r--r--spec/ruby/core/string/upto_spec.rb110
-rw-r--r--spec/ruby/core/string/valid_encoding/utf_8_spec.rb214
-rw-r--r--spec/ruby/core/string/valid_encoding_spec.rb133
-rw-r--r--spec/ruby/core/struct/clone_spec.rb7
-rw-r--r--spec/ruby/core/struct/constants_spec.rb13
-rw-r--r--spec/ruby/core/struct/deconstruct_keys_spec.rb130
-rw-r--r--spec/ruby/core/struct/deconstruct_spec.rb10
-rw-r--r--spec/ruby/core/struct/dig_spec.rb52
-rw-r--r--spec/ruby/core/struct/dup_spec.rb23
-rw-r--r--spec/ruby/core/struct/each_pair_spec.rb33
-rw-r--r--spec/ruby/core/struct/each_spec.rb27
-rw-r--r--spec/ruby/core/struct/element_reference_spec.rb52
-rw-r--r--spec/ruby/core/struct/element_set_spec.rb36
-rw-r--r--spec/ruby/core/struct/eql_spec.rb13
-rw-r--r--spec/ruby/core/struct/equal_value_spec.rb7
-rw-r--r--spec/ruby/core/struct/filter_spec.rb10
-rw-r--r--spec/ruby/core/struct/fixtures/classes.rb35
-rw-r--r--spec/ruby/core/struct/hash_spec.rb64
-rw-r--r--spec/ruby/core/struct/initialize_spec.rb74
-rw-r--r--spec/ruby/core/struct/inspect_spec.rb7
-rw-r--r--spec/ruby/core/struct/instance_variable_get_spec.rb16
-rw-r--r--spec/ruby/core/struct/instance_variables_spec.rb16
-rw-r--r--spec/ruby/core/struct/keyword_init_spec.rb45
-rw-r--r--spec/ruby/core/struct/length_spec.rb12
-rw-r--r--spec/ruby/core/struct/members_spec.rb25
-rw-r--r--spec/ruby/core/struct/new_spec.rb255
-rw-r--r--spec/ruby/core/struct/select_spec.rb10
-rw-r--r--spec/ruby/core/struct/shared/accessor.rb7
-rw-r--r--spec/ruby/core/struct/shared/dup.rb9
-rw-r--r--spec/ruby/core/struct/shared/equal_value.rb37
-rw-r--r--spec/ruby/core/struct/shared/inspect.rb40
-rw-r--r--spec/ruby/core/struct/shared/select.rb26
-rw-r--r--spec/ruby/core/struct/size_spec.rb11
-rw-r--r--spec/ruby/core/struct/struct_spec.rb50
-rw-r--r--spec/ruby/core/struct/to_a_spec.rb12
-rw-r--r--spec/ruby/core/struct/to_h_spec.rb68
-rw-r--r--spec/ruby/core/struct/to_s_spec.rb12
-rw-r--r--spec/ruby/core/struct/values_at_spec.rb59
-rw-r--r--spec/ruby/core/struct/values_spec.rb11
-rw-r--r--spec/ruby/core/symbol/all_symbols_spec.rb19
-rw-r--r--spec/ruby/core/symbol/capitalize_spec.rb41
-rw-r--r--spec/ruby/core/symbol/case_compare_spec.rb11
-rw-r--r--spec/ruby/core/symbol/casecmp_spec.rb152
-rw-r--r--spec/ruby/core/symbol/comparison_spec.rb51
-rw-r--r--spec/ruby/core/symbol/downcase_spec.rb25
-rw-r--r--spec/ruby/core/symbol/dup_spec.rb7
-rw-r--r--spec/ruby/core/symbol/element_reference_spec.rb6
-rw-r--r--spec/ruby/core/symbol/empty_spec.rb11
-rw-r--r--spec/ruby/core/symbol/encoding_spec.rb23
-rw-r--r--spec/ruby/core/symbol/end_with_spec.rb8
-rw-r--r--spec/ruby/core/symbol/equal_value_spec.rb14
-rw-r--r--spec/ruby/core/symbol/fixtures/classes.rb3
-rw-r--r--spec/ruby/core/symbol/id2name_spec.rb6
-rw-r--r--spec/ruby/core/symbol/inspect_spec.rb131
-rw-r--r--spec/ruby/core/symbol/intern_spec.rb11
-rw-r--r--spec/ruby/core/symbol/length_spec.rb6
-rw-r--r--spec/ruby/core/symbol/match_spec.rb77
-rw-r--r--spec/ruby/core/symbol/name_spec.rb17
-rw-r--r--spec/ruby/core/symbol/next_spec.rb6
-rw-r--r--spec/ruby/core/symbol/shared/id2name.rb30
-rw-r--r--spec/ruby/core/symbol/shared/length.rb23
-rw-r--r--spec/ruby/core/symbol/shared/slice.rb262
-rw-r--r--spec/ruby/core/symbol/shared/succ.rb18
-rw-r--r--spec/ruby/core/symbol/size_spec.rb6
-rw-r--r--spec/ruby/core/symbol/slice_spec.rb6
-rw-r--r--spec/ruby/core/symbol/start_with_spec.rb8
-rw-r--r--spec/ruby/core/symbol/succ_spec.rb6
-rw-r--r--spec/ruby/core/symbol/swapcase_spec.rb29
-rw-r--r--spec/ruby/core/symbol/symbol_spec.rb19
-rw-r--r--spec/ruby/core/symbol/to_proc_spec.rb78
-rw-r--r--spec/ruby/core/symbol/to_s_spec.rb6
-rw-r--r--spec/ruby/core/symbol/to_sym_spec.rb9
-rw-r--r--spec/ruby/core/symbol/upcase_spec.rb21
-rw-r--r--spec/ruby/core/systemexit/initialize_spec.rb26
-rw-r--r--spec/ruby/core/systemexit/success_spec.rb13
-rw-r--r--spec/ruby/core/thread/abort_on_exception_spec.rb106
-rw-r--r--spec/ruby/core/thread/add_trace_func_spec.rb5
-rw-r--r--spec/ruby/core/thread/alive_spec.rb58
-rw-r--r--spec/ruby/core/thread/allocate_spec.rb9
-rw-r--r--spec/ruby/core/thread/backtrace/limit_spec.rb13
-rw-r--r--spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb93
-rw-r--r--spec/ruby/core/thread/backtrace/location/base_label_spec.rb49
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb4
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb2
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb10
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/classes.rb139
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb5
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb3
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/main.rb5
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/path.rb2
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb11
-rw-r--r--spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb1
-rw-r--r--spec/ruby/core/thread/backtrace/location/inspect_spec.rb13
-rw-r--r--spec/ruby/core/thread/backtrace/location/label_spec.rb227
-rw-r--r--spec/ruby/core/thread/backtrace/location/lineno_spec.rb23
-rw-r--r--spec/ruby/core/thread/backtrace/location/path_spec.rb124
-rw-r--r--spec/ruby/core/thread/backtrace/location/to_s_spec.rb13
-rw-r--r--spec/ruby/core/thread/backtrace_locations_spec.rb79
-rw-r--r--spec/ruby/core/thread/backtrace_spec.rb69
-rw-r--r--spec/ruby/core/thread/current_spec.rb31
-rw-r--r--spec/ruby/core/thread/each_caller_location_spec.rb47
-rw-r--r--spec/ruby/core/thread/element_reference_spec.rb55
-rw-r--r--spec/ruby/core/thread/element_set_spec.rb74
-rw-r--r--spec/ruby/core/thread/exit_spec.rb15
-rw-r--r--spec/ruby/core/thread/fetch_spec.rb66
-rw-r--r--spec/ruby/core/thread/fixtures/classes.rb322
-rw-r--r--spec/ruby/core/thread/fork_spec.rb9
-rw-r--r--spec/ruby/core/thread/group_spec.rb16
-rw-r--r--spec/ruby/core/thread/handle_interrupt_spec.rb125
-rw-r--r--spec/ruby/core/thread/ignore_deadlock_spec.rb19
-rw-r--r--spec/ruby/core/thread/initialize_spec.rb27
-rw-r--r--spec/ruby/core/thread/inspect_spec.rb6
-rw-r--r--spec/ruby/core/thread/join_spec.rb70
-rw-r--r--spec/ruby/core/thread/key_spec.rb60
-rw-r--r--spec/ruby/core/thread/keys_spec.rb44
-rw-r--r--spec/ruby/core/thread/kill_spec.rb21
-rw-r--r--spec/ruby/core/thread/list_spec.rb55
-rw-r--r--spec/ruby/core/thread/main_spec.rb10
-rw-r--r--spec/ruby/core/thread/name_spec.rb54
-rw-r--r--spec/ruby/core/thread/native_thread_id_spec.rb31
-rw-r--r--spec/ruby/core/thread/new_spec.rb83
-rw-r--r--spec/ruby/core/thread/pass_spec.rb8
-rw-r--r--spec/ruby/core/thread/pending_interrupt_spec.rb32
-rw-r--r--spec/ruby/core/thread/priority_spec.rb72
-rw-r--r--spec/ruby/core/thread/raise_spec.rb267
-rw-r--r--spec/ruby/core/thread/report_on_exception_spec.rb155
-rw-r--r--spec/ruby/core/thread/run_spec.rb8
-rw-r--r--spec/ruby/core/thread/set_trace_func_spec.rb5
-rw-r--r--spec/ruby/core/thread/shared/exit.rb219
-rw-r--r--spec/ruby/core/thread/shared/start.rb41
-rw-r--r--spec/ruby/core/thread/shared/to_s.rb53
-rw-r--r--spec/ruby/core/thread/shared/wakeup.rb62
-rw-r--r--spec/ruby/core/thread/start_spec.rb9
-rw-r--r--spec/ruby/core/thread/status_spec.rb60
-rw-r--r--spec/ruby/core/thread/stop_spec.rb54
-rw-r--r--spec/ruby/core/thread/terminate_spec.rb7
-rw-r--r--spec/ruby/core/thread/thread_variable_get_spec.rb60
-rw-r--r--spec/ruby/core/thread/thread_variable_set_spec.rb62
-rw-r--r--spec/ruby/core/thread/thread_variable_spec.rb60
-rw-r--r--spec/ruby/core/thread/thread_variables_spec.rb40
-rw-r--r--spec/ruby/core/thread/to_s_spec.rb6
-rw-r--r--spec/ruby/core/thread/value_spec.rb31
-rw-r--r--spec/ruby/core/thread/wakeup_spec.rb7
-rw-r--r--spec/ruby/core/threadgroup/add_spec.rb39
-rw-r--r--spec/ruby/core/threadgroup/default_spec.rb11
-rw-r--r--spec/ruby/core/threadgroup/enclose_spec.rb24
-rw-r--r--spec/ruby/core/threadgroup/enclosed_spec.rb14
-rw-r--r--spec/ruby/core/threadgroup/list_spec.rb23
-rw-r--r--spec/ruby/core/time/_dump_spec.rb55
-rw-r--r--spec/ruby/core/time/_load_spec.rb51
-rw-r--r--spec/ruby/core/time/asctime_spec.rb6
-rw-r--r--spec/ruby/core/time/at_spec.rb316
-rw-r--r--spec/ruby/core/time/ceil_spec.rb44
-rw-r--r--spec/ruby/core/time/comparison_spec.rb130
-rw-r--r--spec/ruby/core/time/ctime_spec.rb6
-rw-r--r--spec/ruby/core/time/day_spec.rb6
-rw-r--r--spec/ruby/core/time/deconstruct_keys_spec.rb43
-rw-r--r--spec/ruby/core/time/dst_spec.rb6
-rw-r--r--spec/ruby/core/time/dup_spec.rb46
-rw-r--r--spec/ruby/core/time/eql_spec.rb29
-rw-r--r--spec/ruby/core/time/fixtures/classes.rb105
-rw-r--r--spec/ruby/core/time/floor_spec.rb36
-rw-r--r--spec/ruby/core/time/friday_spec.rb11
-rw-r--r--spec/ruby/core/time/getgm_spec.rb6
-rw-r--r--spec/ruby/core/time/getlocal_spec.rb206
-rw-r--r--spec/ruby/core/time/getutc_spec.rb6
-rw-r--r--spec/ruby/core/time/gm_spec.rb10
-rw-r--r--spec/ruby/core/time/gmt_offset_spec.rb6
-rw-r--r--spec/ruby/core/time/gmt_spec.rb8
-rw-r--r--spec/ruby/core/time/gmtime_spec.rb6
-rw-r--r--spec/ruby/core/time/gmtoff_spec.rb6
-rw-r--r--spec/ruby/core/time/hash_spec.rb11
-rw-r--r--spec/ruby/core/time/hour_spec.rb17
-rw-r--r--spec/ruby/core/time/inspect_spec.rb33
-rw-r--r--spec/ruby/core/time/isdst_spec.rb6
-rw-r--r--spec/ruby/core/time/iso8601_spec.rb6
-rw-r--r--spec/ruby/core/time/local_spec.rb11
-rw-r--r--spec/ruby/core/time/localtime_spec.rb203
-rw-r--r--spec/ruby/core/time/mday_spec.rb6
-rw-r--r--spec/ruby/core/time/min_spec.rb17
-rw-r--r--spec/ruby/core/time/minus_spec.rb121
-rw-r--r--spec/ruby/core/time/mktime_spec.rb11
-rw-r--r--spec/ruby/core/time/mon_spec.rb6
-rw-r--r--spec/ruby/core/time/monday_spec.rb11
-rw-r--r--spec/ruby/core/time/month_spec.rb6
-rw-r--r--spec/ruby/core/time/new_spec.rb739
-rw-r--r--spec/ruby/core/time/now_spec.rb181
-rw-r--r--spec/ruby/core/time/nsec_spec.rb31
-rw-r--r--spec/ruby/core/time/plus_spec.rb118
-rw-r--r--spec/ruby/core/time/round_spec.rb35
-rw-r--r--spec/ruby/core/time/saturday_spec.rb11
-rw-r--r--spec/ruby/core/time/sec_spec.rb7
-rw-r--r--spec/ruby/core/time/shared/asctime.rb6
-rw-r--r--spec/ruby/core/time/shared/day.rb15
-rw-r--r--spec/ruby/core/time/shared/getgm.rb9
-rw-r--r--spec/ruby/core/time/shared/gm.rb70
-rw-r--r--spec/ruby/core/time/shared/gmt_offset.rb59
-rw-r--r--spec/ruby/core/time/shared/gmtime.rb40
-rw-r--r--spec/ruby/core/time/shared/inspect.rb21
-rw-r--r--spec/ruby/core/time/shared/isdst.rb8
-rw-r--r--spec/ruby/core/time/shared/local.rb42
-rw-r--r--spec/ruby/core/time/shared/month.rb15
-rw-r--r--spec/ruby/core/time/shared/now.rb33
-rw-r--r--spec/ruby/core/time/shared/time_params.rb271
-rw-r--r--spec/ruby/core/time/shared/to_i.rb16
-rw-r--r--spec/ruby/core/time/shared/xmlschema.rb31
-rw-r--r--spec/ruby/core/time/strftime_spec.rb91
-rw-r--r--spec/ruby/core/time/subsec_spec.rb27
-rw-r--r--spec/ruby/core/time/sunday_spec.rb11
-rw-r--r--spec/ruby/core/time/thursday_spec.rb11
-rw-r--r--spec/ruby/core/time/time_spec.rb7
-rw-r--r--spec/ruby/core/time/to_a_spec.rb12
-rw-r--r--spec/ruby/core/time/to_f_spec.rb7
-rw-r--r--spec/ruby/core/time/to_i_spec.rb6
-rw-r--r--spec/ruby/core/time/to_r_spec.rb11
-rw-r--r--spec/ruby/core/time/to_s_spec.rb6
-rw-r--r--spec/ruby/core/time/tuesday_spec.rb11
-rw-r--r--spec/ruby/core/time/tv_nsec_spec.rb5
-rw-r--r--spec/ruby/core/time/tv_sec_spec.rb6
-rw-r--r--spec/ruby/core/time/tv_usec_spec.rb5
-rw-r--r--spec/ruby/core/time/usec_spec.rb43
-rw-r--r--spec/ruby/core/time/utc_offset_spec.rb6
-rw-r--r--spec/ruby/core/time/utc_spec.rb66
-rw-r--r--spec/ruby/core/time/wday_spec.rb9
-rw-r--r--spec/ruby/core/time/wednesday_spec.rb11
-rw-r--r--spec/ruby/core/time/xmlschema_spec.rb6
-rw-r--r--spec/ruby/core/time/yday_spec.rb12
-rw-r--r--spec/ruby/core/time/year_spec.rb17
-rw-r--r--spec/ruby/core/time/zone_spec.rb111
-rw-r--r--spec/ruby/core/tracepoint/allow_reentry_spec.rb30
-rw-r--r--spec/ruby/core/tracepoint/binding_spec.rb21
-rw-r--r--spec/ruby/core/tracepoint/callee_id_spec.rb18
-rw-r--r--spec/ruby/core/tracepoint/defined_class_spec.rb27
-rw-r--r--spec/ruby/core/tracepoint/disable_spec.rb76
-rw-r--r--spec/ruby/core/tracepoint/enable_spec.rb543
-rw-r--r--spec/ruby/core/tracepoint/enabled_spec.rb15
-rw-r--r--spec/ruby/core/tracepoint/eval_script_spec.rb23
-rw-r--r--spec/ruby/core/tracepoint/event_spec.rb22
-rw-r--r--spec/ruby/core/tracepoint/fixtures/classes.rb40
-rw-r--r--spec/ruby/core/tracepoint/inspect_spec.rb141
-rw-r--r--spec/ruby/core/tracepoint/lineno_spec.rb20
-rw-r--r--spec/ruby/core/tracepoint/method_id_spec.rb15
-rw-r--r--spec/ruby/core/tracepoint/new_spec.rb72
-rw-r--r--spec/ruby/core/tracepoint/parameters_spec.rb28
-rw-r--r--spec/ruby/core/tracepoint/path_spec.rb26
-rw-r--r--spec/ruby/core/tracepoint/raised_exception_spec.rb36
-rw-r--r--spec/ruby/core/tracepoint/return_value_spec.rb17
-rw-r--r--spec/ruby/core/tracepoint/self_spec.rb26
-rw-r--r--spec/ruby/core/tracepoint/trace_spec.rb10
-rw-r--r--spec/ruby/core/true/and_spec.rb11
-rw-r--r--spec/ruby/core/true/case_compare_spec.rb13
-rw-r--r--spec/ruby/core/true/dup_spec.rb7
-rw-r--r--spec/ruby/core/true/inspect_spec.rb7
-rw-r--r--spec/ruby/core/true/or_spec.rb11
-rw-r--r--spec/ruby/core/true/singleton_method_spec.rb13
-rw-r--r--spec/ruby/core/true/to_s_spec.rb15
-rw-r--r--spec/ruby/core/true/trueclass_spec.rb15
-rw-r--r--spec/ruby/core/true/xor_spec.rb11
-rw-r--r--spec/ruby/core/unboundmethod/arity_spec.rb207
-rw-r--r--spec/ruby/core/unboundmethod/bind_call_spec.rb58
-rw-r--r--spec/ruby/core/unboundmethod/bind_spec.rb69
-rw-r--r--spec/ruby/core/unboundmethod/clone_spec.rb13
-rw-r--r--spec/ruby/core/unboundmethod/dup_spec.rb15
-rw-r--r--spec/ruby/core/unboundmethod/eql_spec.rb5
-rw-r--r--spec/ruby/core/unboundmethod/equal_value_spec.rb165
-rw-r--r--spec/ruby/core/unboundmethod/fixtures/classes.rb124
-rw-r--r--spec/ruby/core/unboundmethod/hash_spec.rb22
-rw-r--r--spec/ruby/core/unboundmethod/inspect_spec.rb9
-rw-r--r--spec/ruby/core/unboundmethod/name_spec.rb15
-rw-r--r--spec/ruby/core/unboundmethod/original_name_spec.rb59
-rw-r--r--spec/ruby/core/unboundmethod/owner_spec.rb31
-rw-r--r--spec/ruby/core/unboundmethod/parameters_spec.rb5
-rw-r--r--spec/ruby/core/unboundmethod/private_spec.rb9
-rw-r--r--spec/ruby/core/unboundmethod/protected_spec.rb9
-rw-r--r--spec/ruby/core/unboundmethod/public_spec.rb9
-rw-r--r--spec/ruby/core/unboundmethod/shared/dup.rb32
-rw-r--r--spec/ruby/core/unboundmethod/shared/to_s.rb33
-rw-r--r--spec/ruby/core/unboundmethod/source_location_spec.rb59
-rw-r--r--spec/ruby/core/unboundmethod/super_method_spec.rb49
-rw-r--r--spec/ruby/core/unboundmethod/to_s_spec.rb9
-rw-r--r--spec/ruby/core/warning/categories_spec.rb12
-rw-r--r--spec/ruby/core/warning/element_reference_spec.rb27
-rw-r--r--spec/ruby/core/warning/element_set_spec.rb39
-rw-r--r--spec/ruby/core/warning/performance_warning_spec.rb28
-rw-r--r--spec/ruby/core/warning/warn_spec.rb187
-rw-r--r--spec/ruby/default.mspec54
-rw-r--r--spec/ruby/fixtures/basicobject/method_missing.rb55
-rw-r--r--spec/ruby/fixtures/class.rb142
-rw-r--r--spec/ruby/fixtures/class_variables.rb58
-rw-r--r--spec/ruby/fixtures/code/a/load_fixture.bundle1
-rw-r--r--spec/ruby/fixtures/code/a/load_fixture.dll1
-rw-r--r--spec/ruby/fixtures/code/a/load_fixture.dylib1
-rw-r--r--spec/ruby/fixtures/code/a/load_fixture.so1
-rw-r--r--spec/ruby/fixtures/code/b/load_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/c/load_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/concurrent.rb12
-rw-r--r--spec/ruby/fixtures/code/concurrent2.rb8
-rw-r--r--spec/ruby/fixtures/code/concurrent3.rb2
-rw-r--r--spec/ruby/fixtures/code/concurrent_require_fixture.rb4
-rw-r--r--spec/ruby/fixtures/code/d/load_fixture.rb.rb1
-rw-r--r--spec/ruby/fixtures/code/file_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/gem/load_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/line_fixture.rb5
-rw-r--r--spec/ruby/fixtures/code/load_ext_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/load_fixture1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.bundle1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.dll1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.dylib1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.ext1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.ext.bundle1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.ext.dll1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.ext.dylib1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.ext.rb1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.ext.so1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/load_fixture.so1
-rw-r--r--spec/ruby/fixtures/code/load_fixture_and__FILE__.rb1
-rw-r--r--spec/ruby/fixtures/code/load_wrap_fixture.rb12
-rw-r--r--spec/ruby/fixtures/code/load_wrap_method_fixture.rb9
-rw-r--r--spec/ruby/fixtures/code/methods_fixture.rb364
-rw-r--r--spec/ruby/fixtures/code/raise_fixture.rb1
-rw-r--r--spec/ruby/fixtures/code/recursive_load_fixture.rb5
-rw-r--r--spec/ruby/fixtures/code/recursive_require_fixture.rb3
-rw-r--r--spec/ruby/fixtures/code/symlink/symlink1.rb1
-rw-r--r--spec/ruby/fixtures/code/symlink/symlink2/symlink2.rb1
-rw-r--r--spec/ruby/fixtures/code_loading.rb41
-rw-r--r--spec/ruby/fixtures/constants.rb324
-rw-r--r--spec/ruby/fixtures/io.rb12
-rw-r--r--spec/ruby/fixtures/reflection.rb352
-rw-r--r--spec/ruby/language/BEGIN_spec.rb41
-rw-r--r--spec/ruby/language/END_spec.rb33
-rw-r--r--spec/ruby/language/README30
-rw-r--r--spec/ruby/language/alias_spec.rb294
-rw-r--r--spec/ruby/language/and_spec.rb80
-rw-r--r--spec/ruby/language/array_spec.rb166
-rw-r--r--spec/ruby/language/assignments_spec.rb582
-rw-r--r--spec/ruby/language/block_spec.rb1148
-rw-r--r--spec/ruby/language/break_spec.rb402
-rw-r--r--spec/ruby/language/case_spec.rb544
-rw-r--r--spec/ruby/language/class_spec.rb396
-rw-r--r--spec/ruby/language/class_variable_spec.rb114
-rw-r--r--spec/ruby/language/comment_spec.rb13
-rw-r--r--spec/ruby/language/constants_spec.rb809
-rw-r--r--spec/ruby/language/def_spec.rb815
-rw-r--r--spec/ruby/language/defined_spec.rb1328
-rw-r--r--spec/ruby/language/delegation_spec.rb162
-rw-r--r--spec/ruby/language/encoding_spec.rb36
-rw-r--r--spec/ruby/language/ensure_spec.rb346
-rw-r--r--spec/ruby/language/execution_spec.rb93
-rw-r--r--spec/ruby/language/file_spec.rb21
-rw-r--r--spec/ruby/language/fixtures/argv_encoding.rb1
-rw-r--r--spec/ruby/language/fixtures/array.rb32
-rw-r--r--spec/ruby/language/fixtures/begin_file.rb3
-rw-r--r--spec/ruby/language/fixtures/binary_symbol.rb4
-rw-r--r--spec/ruby/language/fixtures/block.rb61
-rw-r--r--spec/ruby/language/fixtures/break.rb291
-rw-r--r--spec/ruby/language/fixtures/break_lambda_toplevel.rb9
-rw-r--r--spec/ruby/language/fixtures/break_lambda_toplevel_block.rb23
-rw-r--r--spec/ruby/language/fixtures/break_lambda_toplevel_method.rb17
-rw-r--r--spec/ruby/language/fixtures/bytes_magic_comment.rb2
-rw-r--r--spec/ruby/language/fixtures/case_magic_comment.rb2
-rw-r--r--spec/ruby/language/fixtures/class_with_class_variable.rb9
-rw-r--r--spec/ruby/language/fixtures/classes.rb31
-rw-r--r--spec/ruby/language/fixtures/coding_us_ascii.rb11
-rw-r--r--spec/ruby/language/fixtures/coding_utf_8.rb11
-rw-r--r--spec/ruby/language/fixtures/constant_visibility.rb114
-rw-r--r--spec/ruby/language/fixtures/constants_sclass.rb54
-rw-r--r--spec/ruby/language/fixtures/def.rb14
-rw-r--r--spec/ruby/language/fixtures/defined.rb339
-rw-r--r--spec/ruby/language/fixtures/delegation.rb11
-rw-r--r--spec/ruby/language/fixtures/dollar_zero.rb6
-rw-r--r--spec/ruby/language/fixtures/emacs_magic_comment.rb2
-rw-r--r--spec/ruby/language/fixtures/ensure.rb121
-rw-r--r--spec/ruby/language/fixtures/file.rb1
-rw-r--r--spec/ruby/language/fixtures/for_scope.rb15
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_across_files.rb6
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_across_files_diff_enc.rb6
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_across_files_no_comment.rb6
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_one_literal.rb4
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_required.rb3
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_required_diff_enc.rb4
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_required_no_comment.rb1
-rw-r--r--spec/ruby/language/fixtures/freeze_magic_comment_two_literals.rb3
-rw-r--r--spec/ruby/language/fixtures/hash_strings_binary.rb7
-rw-r--r--spec/ruby/language/fixtures/hash_strings_usascii.rb7
-rw-r--r--spec/ruby/language/fixtures/hash_strings_utf8.rb7
-rw-r--r--spec/ruby/language/fixtures/magic_comment.rb2
-rw-r--r--spec/ruby/language/fixtures/match_operators.rb9
-rw-r--r--spec/ruby/language/fixtures/metaclass.rb33
-rw-r--r--spec/ruby/language/fixtures/module.rb15
-rw-r--r--spec/ruby/language/fixtures/next.rb134
-rw-r--r--spec/ruby/language/fixtures/no_magic_comment.rb1
-rw-r--r--spec/ruby/language/fixtures/precedence.rb16
-rw-r--r--spec/ruby/language/fixtures/print_magic_comment_result_at_exit.rb3
-rw-r--r--spec/ruby/language/fixtures/private.rb59
-rw-r--r--spec/ruby/language/fixtures/rescue.rb67
-rw-r--r--spec/ruby/language/fixtures/rescue/top_level.rb7
-rw-r--r--spec/ruby/language/fixtures/rescue_captures.rb107
-rw-r--r--spec/ruby/language/fixtures/return.rb135
-rw-r--r--spec/ruby/language/fixtures/second_line_magic_comment.rb3
-rw-r--r--spec/ruby/language/fixtures/second_token_magic_comment.rb2
-rw-r--r--spec/ruby/language/fixtures/send.rb151
-rwxr-xr-xspec/ruby/language/fixtures/shebang_magic_comment.rb3
-rw-r--r--spec/ruby/language/fixtures/squiggly_heredoc.rb71
-rw-r--r--spec/ruby/language/fixtures/super.rb804
-rw-r--r--spec/ruby/language/fixtures/utf16-be-nobom.rbbin0 -> 68 bytes-rw-r--r--spec/ruby/language/fixtures/utf16-le-nobom.rbbin0 -> 69 bytes-rw-r--r--spec/ruby/language/fixtures/utf8-bom.rb2
-rw-r--r--spec/ruby/language/fixtures/utf8-nobom.rb2
-rw-r--r--spec/ruby/language/fixtures/variables.rb157
-rw-r--r--spec/ruby/language/fixtures/vim_magic_comment.rb2
-rw-r--r--spec/ruby/language/fixtures/yield.rb41
-rw-r--r--spec/ruby/language/for_spec.rb380
-rw-r--r--spec/ruby/language/hash_spec.rb333
-rw-r--r--spec/ruby/language/heredoc_spec.rb119
-rw-r--r--spec/ruby/language/if_spec.rb424
-rw-r--r--spec/ruby/language/it_parameter_spec.rb108
-rw-r--r--spec/ruby/language/keyword_arguments_spec.rb398
-rw-r--r--spec/ruby/language/lambda_spec.rb629
-rw-r--r--spec/ruby/language/line_spec.rb45
-rw-r--r--spec/ruby/language/loop_spec.rb67
-rw-r--r--spec/ruby/language/magic_comment_spec.rb93
-rw-r--r--spec/ruby/language/match_spec.rb89
-rw-r--r--spec/ruby/language/metaclass_spec.rb143
-rw-r--r--spec/ruby/language/method_spec.rb1669
-rw-r--r--spec/ruby/language/module_spec.rb123
-rw-r--r--spec/ruby/language/next_spec.rb410
-rw-r--r--spec/ruby/language/not_spec.rb51
-rw-r--r--spec/ruby/language/numbered_parameters_spec.rb113
-rw-r--r--spec/ruby/language/numbers_spec.rb105
-rw-r--r--spec/ruby/language/optional_assignments_spec.rb742
-rw-r--r--spec/ruby/language/or_spec.rb90
-rw-r--r--spec/ruby/language/order_spec.rb75
-rw-r--r--spec/ruby/language/pattern_matching_spec.rb1310
-rw-r--r--spec/ruby/language/precedence_spec.rb445
-rw-r--r--spec/ruby/language/predefined/data_spec.rb48
-rw-r--r--spec/ruby/language/predefined/fixtures/data1.rb4
-rw-r--r--spec/ruby/language/predefined/fixtures/data2.rb3
-rw-r--r--spec/ruby/language/predefined/fixtures/data3.rb6
-rw-r--r--spec/ruby/language/predefined/fixtures/data4.rb4
-rw-r--r--spec/ruby/language/predefined/fixtures/data5.rb5
-rw-r--r--spec/ruby/language/predefined/fixtures/data_offset.rb12
-rw-r--r--spec/ruby/language/predefined/fixtures/data_only.rb2
-rw-r--r--spec/ruby/language/predefined/fixtures/empty_data.rb3
-rw-r--r--spec/ruby/language/predefined/fixtures/print_data.rb3
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic.rb4
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_dynamic_required.rb2
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_id.rb4
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_id_required.rb1
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_required_before.rb2
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_values.rb9
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_variables.rb4
-rw-r--r--spec/ruby/language/predefined/fixtures/toplevel_binding_variables_required.rb2
-rw-r--r--spec/ruby/language/predefined/toplevel_binding_spec.rb34
-rw-r--r--spec/ruby/language/predefined_spec.rb1575
-rw-r--r--spec/ruby/language/private_spec.rb67
-rw-r--r--spec/ruby/language/proc_spec.rb267
-rw-r--r--spec/ruby/language/range_spec.rb30
-rw-r--r--spec/ruby/language/redo_spec.rb66
-rw-r--r--spec/ruby/language/regexp/anchors_spec.rb179
-rw-r--r--spec/ruby/language/regexp/back-references_spec.rb149
-rw-r--r--spec/ruby/language/regexp/character_classes_spec.rb647
-rw-r--r--spec/ruby/language/regexp/empty_checks_spec.rb135
-rw-r--r--spec/ruby/language/regexp/encoding_spec.rb152
-rw-r--r--spec/ruby/language/regexp/escapes_spec.rb169
-rw-r--r--spec/ruby/language/regexp/grouping_spec.rb63
-rw-r--r--spec/ruby/language/regexp/interpolation_spec.rb58
-rw-r--r--spec/ruby/language/regexp/modifiers_spec.rb115
-rw-r--r--spec/ruby/language/regexp/repetition_spec.rb138
-rw-r--r--spec/ruby/language/regexp/subexpression_call_spec.rb50
-rw-r--r--spec/ruby/language/regexp_spec.rb167
-rw-r--r--spec/ruby/language/rescue_spec.rb616
-rw-r--r--spec/ruby/language/reserved_keywords.rb149
-rw-r--r--spec/ruby/language/retry_spec.rb55
-rw-r--r--spec/ruby/language/return_spec.rb490
-rw-r--r--spec/ruby/language/safe_navigator_spec.rb147
-rw-r--r--spec/ruby/language/safe_spec.rb11
-rw-r--r--spec/ruby/language/send_spec.rb591
-rw-r--r--spec/ruby/language/shared/__FILE__.rb23
-rw-r--r--spec/ruby/language/shared/__LINE__.rb15
-rw-r--r--spec/ruby/language/singleton_class_spec.rb317
-rw-r--r--spec/ruby/language/source_encoding_spec.rb61
-rw-r--r--spec/ruby/language/string_spec.rb307
-rw-r--r--spec/ruby/language/super_spec.rb478
-rw-r--r--spec/ruby/language/symbol_spec.rb108
-rw-r--r--spec/ruby/language/throw_spec.rb81
-rw-r--r--spec/ruby/language/undef_spec.rb79
-rw-r--r--spec/ruby/language/unless_spec.rb43
-rw-r--r--spec/ruby/language/until_spec.rb234
-rw-r--r--spec/ruby/language/variables_spec.rb930
-rw-r--r--spec/ruby/language/while_spec.rb344
-rw-r--r--spec/ruby/language/yield_spec.rb226
-rw-r--r--spec/ruby/library/English/English_spec.rb161
-rw-r--r--spec/ruby/library/English/alias_spec.rb14
-rw-r--r--spec/ruby/library/abbrev/abbrev_spec.rb31
-rw-r--r--spec/ruby/library/base64/decode64_spec.rb29
-rw-r--r--spec/ruby/library/base64/encode64_spec.rb23
-rw-r--r--spec/ruby/library/base64/strict_decode64_spec.rb41
-rw-r--r--spec/ruby/library/base64/strict_encode64_spec.rb19
-rw-r--r--spec/ruby/library/base64/urlsafe_decode64_spec.rb19
-rw-r--r--spec/ruby/library/base64/urlsafe_encode64_spec.rb20
-rw-r--r--spec/ruby/library/bigdecimal/BigDecimal_spec.rb239
-rw-r--r--spec/ruby/library/bigdecimal/abs_spec.rb50
-rw-r--r--spec/ruby/library/bigdecimal/add_spec.rb185
-rw-r--r--spec/ruby/library/bigdecimal/case_compare_spec.rb7
-rw-r--r--spec/ruby/library/bigdecimal/ceil_spec.rb104
-rw-r--r--spec/ruby/library/bigdecimal/clone_spec.rb6
-rw-r--r--spec/ruby/library/bigdecimal/coerce_spec.rb26
-rw-r--r--spec/ruby/library/bigdecimal/comparison_spec.rb81
-rw-r--r--spec/ruby/library/bigdecimal/constants_spec.rb70
-rw-r--r--spec/ruby/library/bigdecimal/core_spec.rb62
-rw-r--r--spec/ruby/library/bigdecimal/div_spec.rb110
-rw-r--r--spec/ruby/library/bigdecimal/divide_spec.rb17
-rw-r--r--spec/ruby/library/bigdecimal/divmod_spec.rb212
-rw-r--r--spec/ruby/library/bigdecimal/double_fig_spec.rb9
-rw-r--r--spec/ruby/library/bigdecimal/dup_spec.rb6
-rw-r--r--spec/ruby/library/bigdecimal/eql_spec.rb6
-rw-r--r--spec/ruby/library/bigdecimal/equal_value_spec.rb7
-rw-r--r--spec/ruby/library/bigdecimal/exponent_spec.rb27
-rw-r--r--spec/ruby/library/bigdecimal/finite_spec.rb34
-rw-r--r--spec/ruby/library/bigdecimal/fix_spec.rb57
-rw-r--r--spec/ruby/library/bigdecimal/fixtures/classes.rb17
-rw-r--r--spec/ruby/library/bigdecimal/floor_spec.rb100
-rw-r--r--spec/ruby/library/bigdecimal/frac_spec.rb48
-rw-r--r--spec/ruby/library/bigdecimal/gt_spec.rb96
-rw-r--r--spec/ruby/library/bigdecimal/gte_spec.rb100
-rw-r--r--spec/ruby/library/bigdecimal/hash_spec.rb30
-rw-r--r--spec/ruby/library/bigdecimal/infinite_spec.rb32
-rw-r--r--spec/ruby/library/bigdecimal/inspect_spec.rb30
-rw-r--r--spec/ruby/library/bigdecimal/limit_spec.rb55
-rw-r--r--spec/ruby/library/bigdecimal/lt_spec.rb94
-rw-r--r--spec/ruby/library/bigdecimal/lte_spec.rb100
-rw-r--r--spec/ruby/library/bigdecimal/minus_spec.rb66
-rw-r--r--spec/ruby/library/bigdecimal/mode_spec.rb36
-rw-r--r--spec/ruby/library/bigdecimal/modulo_spec.rb12
-rw-r--r--spec/ruby/library/bigdecimal/mult_spec.rb24
-rw-r--r--spec/ruby/library/bigdecimal/multiply_spec.rb41
-rw-r--r--spec/ruby/library/bigdecimal/nan_spec.rb23
-rw-r--r--spec/ruby/library/bigdecimal/nonzero_spec.rb29
-rw-r--r--spec/ruby/library/bigdecimal/plus_spec.rb54
-rw-r--r--spec/ruby/library/bigdecimal/power_spec.rb6
-rw-r--r--spec/ruby/library/bigdecimal/quo_spec.rb12
-rw-r--r--spec/ruby/library/bigdecimal/remainder_spec.rb77
-rw-r--r--spec/ruby/library/bigdecimal/round_spec.rb234
-rw-r--r--spec/ruby/library/bigdecimal/shared/clone.rb13
-rw-r--r--spec/ruby/library/bigdecimal/shared/eql.rb61
-rw-r--r--spec/ruby/library/bigdecimal/shared/modulo.rb131
-rw-r--r--spec/ruby/library/bigdecimal/shared/mult.rb97
-rw-r--r--spec/ruby/library/bigdecimal/shared/power.rb72
-rw-r--r--spec/ruby/library/bigdecimal/shared/quo.rb68
-rw-r--r--spec/ruby/library/bigdecimal/shared/to_int.rb16
-rw-r--r--spec/ruby/library/bigdecimal/sign_spec.rb46
-rw-r--r--spec/ruby/library/bigdecimal/split_spec.rb86
-rw-r--r--spec/ruby/library/bigdecimal/sqrt_spec.rb114
-rw-r--r--spec/ruby/library/bigdecimal/sub_spec.rb62
-rw-r--r--spec/ruby/library/bigdecimal/to_d_spec.rb10
-rw-r--r--spec/ruby/library/bigdecimal/to_f_spec.rb54
-rw-r--r--spec/ruby/library/bigdecimal/to_i_spec.rb7
-rw-r--r--spec/ruby/library/bigdecimal/to_int_spec.rb8
-rw-r--r--spec/ruby/library/bigdecimal/to_r_spec.rb28
-rw-r--r--spec/ruby/library/bigdecimal/to_s_spec.rb98
-rw-r--r--spec/ruby/library/bigdecimal/truncate_spec.rb81
-rw-r--r--spec/ruby/library/bigdecimal/uminus_spec.rb58
-rw-r--r--spec/ruby/library/bigdecimal/uplus_spec.rb17
-rw-r--r--spec/ruby/library/bigdecimal/util_spec.rb40
-rw-r--r--spec/ruby/library/bigdecimal/zero_spec.rb27
-rw-r--r--spec/ruby/library/cgi/cookie/domain_spec.rb26
-rw-r--r--spec/ruby/library/cgi/cookie/expires_spec.rb26
-rw-r--r--spec/ruby/library/cgi/cookie/initialize_spec.rb150
-rw-r--r--spec/ruby/library/cgi/cookie/name_spec.rb26
-rw-r--r--spec/ruby/library/cgi/cookie/parse_spec.rb29
-rw-r--r--spec/ruby/library/cgi/cookie/path_spec.rb26
-rw-r--r--spec/ruby/library/cgi/cookie/secure_spec.rb73
-rw-r--r--spec/ruby/library/cgi/cookie/to_s_spec.rb36
-rw-r--r--spec/ruby/library/cgi/cookie/value_spec.rb79
-rw-r--r--spec/ruby/library/cgi/escapeElement_spec.rb26
-rw-r--r--spec/ruby/library/cgi/escapeHTML_spec.rb21
-rw-r--r--spec/ruby/library/cgi/escapeURIComponent_spec.rb78
-rw-r--r--spec/ruby/library/cgi/escape_spec.rb22
-rw-r--r--spec/ruby/library/cgi/htmlextension/a_spec.rb52
-rw-r--r--spec/ruby/library/cgi/htmlextension/base_spec.rb36
-rw-r--r--spec/ruby/library/cgi/htmlextension/blockquote_spec.rb36
-rw-r--r--spec/ruby/library/cgi/htmlextension/br_spec.rb25
-rw-r--r--spec/ruby/library/cgi/htmlextension/caption_spec.rb36
-rw-r--r--spec/ruby/library/cgi/htmlextension/checkbox_group_spec.rb79
-rw-r--r--spec/ruby/library/cgi/htmlextension/checkbox_spec.rb80
-rw-r--r--spec/ruby/library/cgi/htmlextension/doctype_spec.rb30
-rw-r--r--spec/ruby/library/cgi/htmlextension/file_field_spec.rb75
-rw-r--r--spec/ruby/library/cgi/htmlextension/fixtures/common.rb15
-rw-r--r--spec/ruby/library/cgi/htmlextension/form_spec.rb61
-rw-r--r--spec/ruby/library/cgi/htmlextension/frame_spec.rb17
-rw-r--r--spec/ruby/library/cgi/htmlextension/frameset_spec.rb17
-rw-r--r--spec/ruby/library/cgi/htmlextension/hidden_spec.rb62
-rw-r--r--spec/ruby/library/cgi/htmlextension/html_spec.rb69
-rw-r--r--spec/ruby/library/cgi/htmlextension/image_button_spec.rb72
-rw-r--r--spec/ruby/library/cgi/htmlextension/img_spec.rb86
-rw-r--r--spec/ruby/library/cgi/htmlextension/multipart_form_spec.rb67
-rw-r--r--spec/ruby/library/cgi/htmlextension/password_field_spec.rb87
-rw-r--r--spec/ruby/library/cgi/htmlextension/popup_menu_spec.rb11
-rw-r--r--spec/ruby/library/cgi/htmlextension/radio_button_spec.rb80
-rw-r--r--spec/ruby/library/cgi/htmlextension/radio_group_spec.rb80
-rw-r--r--spec/ruby/library/cgi/htmlextension/reset_spec.rb60
-rw-r--r--spec/ruby/library/cgi/htmlextension/scrolling_list_spec.rb11
-rw-r--r--spec/ruby/library/cgi/htmlextension/shared/popup_menu.rb94
-rw-r--r--spec/ruby/library/cgi/htmlextension/submit_spec.rb60
-rw-r--r--spec/ruby/library/cgi/htmlextension/text_field_spec.rb87
-rw-r--r--spec/ruby/library/cgi/htmlextension/textarea_spec.rb76
-rw-r--r--spec/ruby/library/cgi/http_header_spec.rb11
-rw-r--r--spec/ruby/library/cgi/initialize_spec.rb136
-rw-r--r--spec/ruby/library/cgi/out_spec.rb54
-rw-r--r--spec/ruby/library/cgi/parse_spec.rb27
-rw-r--r--spec/ruby/library/cgi/pretty_spec.rb27
-rw-r--r--spec/ruby/library/cgi/print_spec.rb29
-rw-r--r--spec/ruby/library/cgi/queryextension/accept_charset_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/accept_encoding_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/accept_language_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/accept_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/auth_type_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/cache_control_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/content_length_spec.rb29
-rw-r--r--spec/ruby/library/cgi/queryextension/content_type_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/cookies_spec.rb13
-rw-r--r--spec/ruby/library/cgi/queryextension/element_reference_spec.rb30
-rw-r--r--spec/ruby/library/cgi/queryextension/from_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/gateway_interface_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/has_key_spec.rb10
-rw-r--r--spec/ruby/library/cgi/queryextension/host_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/include_spec.rb10
-rw-r--r--spec/ruby/library/cgi/queryextension/key_spec.rb10
-rw-r--r--spec/ruby/library/cgi/queryextension/keys_spec.rb23
-rw-r--r--spec/ruby/library/cgi/queryextension/multipart_spec.rb43
-rw-r--r--spec/ruby/library/cgi/queryextension/negotiate_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/params_spec.rb40
-rw-r--r--spec/ruby/library/cgi/queryextension/path_info_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/path_translated_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/pragma_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/query_string_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/raw_cookie2_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/raw_cookie_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/referer_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/remote_addr_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/remote_host_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/remote_ident_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/remote_user_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/request_method_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/script_name_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/server_name_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/server_port_spec.rb29
-rw-r--r--spec/ruby/library/cgi/queryextension/server_protocol_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/server_software_spec.rb25
-rw-r--r--spec/ruby/library/cgi/queryextension/shared/has_key.rb19
-rw-r--r--spec/ruby/library/cgi/queryextension/user_agent_spec.rb25
-rw-r--r--spec/ruby/library/cgi/rfc1123_date_spec.rb13
-rw-r--r--spec/ruby/library/cgi/shared/http_header.rb112
-rw-r--r--spec/ruby/library/cgi/unescapeElement_spec.rb26
-rw-r--r--spec/ruby/library/cgi/unescapeHTML_spec.rb48
-rw-r--r--spec/ruby/library/cgi/unescapeURIComponent_spec.rb128
-rw-r--r--spec/ruby/library/cgi/unescape_spec.rb21
-rw-r--r--spec/ruby/library/coverage/fixtures/code_with_begin.rb3
-rw-r--r--spec/ruby/library/coverage/fixtures/eval_code.rb11
-rw-r--r--spec/ruby/library/coverage/fixtures/second_class.rb5
-rw-r--r--spec/ruby/library/coverage/fixtures/some_class.rb16
-rw-r--r--spec/ruby/library/coverage/fixtures/start_coverage.rb3
-rw-r--r--spec/ruby/library/coverage/peek_result_spec.rb64
-rw-r--r--spec/ruby/library/coverage/result_spec.rb343
-rw-r--r--spec/ruby/library/coverage/running_spec.rb20
-rw-r--r--spec/ruby/library/coverage/start_spec.rb87
-rw-r--r--spec/ruby/library/coverage/supported_spec.rb30
-rw-r--r--spec/ruby/library/csv/basicwriter/close_on_terminate_spec.rb6
-rw-r--r--spec/ruby/library/csv/basicwriter/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/basicwriter/terminate_spec.rb6
-rw-r--r--spec/ruby/library/csv/cell/data_spec.rb6
-rw-r--r--spec/ruby/library/csv/cell/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/fixtures/one_line.csv1
-rw-r--r--spec/ruby/library/csv/foreach_spec.rb6
-rw-r--r--spec/ruby/library/csv/generate_line_spec.rb30
-rw-r--r--spec/ruby/library/csv/generate_row_spec.rb6
-rw-r--r--spec/ruby/library/csv/generate_spec.rb32
-rw-r--r--spec/ruby/library/csv/iobuf/close_spec.rb6
-rw-r--r--spec/ruby/library/csv/iobuf/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/iobuf/read_spec.rb6
-rw-r--r--spec/ruby/library/csv/iobuf/terminate_spec.rb6
-rw-r--r--spec/ruby/library/csv/ioreader/close_on_terminate_spec.rb6
-rw-r--r--spec/ruby/library/csv/ioreader/get_row_spec.rb6
-rw-r--r--spec/ruby/library/csv/ioreader/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/ioreader/terminate_spec.rb6
-rw-r--r--spec/ruby/library/csv/liberal_parsing_spec.rb19
-rw-r--r--spec/ruby/library/csv/open_spec.rb6
-rw-r--r--spec/ruby/library/csv/parse_spec.rb93
-rw-r--r--spec/ruby/library/csv/read_spec.rb6
-rw-r--r--spec/ruby/library/csv/readlines_spec.rb35
-rw-r--r--spec/ruby/library/csv/streambuf/add_buf_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/buf_size_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/drop_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/element_reference_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/get_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/idx_is_eos_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/is_eos_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/read_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/rel_buf_spec.rb6
-rw-r--r--spec/ruby/library/csv/streambuf/terminate_spec.rb6
-rw-r--r--spec/ruby/library/csv/stringreader/get_row_spec.rb6
-rw-r--r--spec/ruby/library/csv/stringreader/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/add_row_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/append_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/close_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/create_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/generate_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/initialize_spec.rb6
-rw-r--r--spec/ruby/library/csv/writer/terminate_spec.rb6
-rw-r--r--spec/ruby/library/date/accessor_spec.rb91
-rw-r--r--spec/ruby/library/date/add_month_spec.rb38
-rw-r--r--spec/ruby/library/date/add_spec.rb30
-rw-r--r--spec/ruby/library/date/ajd_spec.rb6
-rw-r--r--spec/ruby/library/date/ajd_to_amjd_spec.rb6
-rw-r--r--spec/ruby/library/date/ajd_to_jd_spec.rb6
-rw-r--r--spec/ruby/library/date/amjd_spec.rb6
-rw-r--r--spec/ruby/library/date/amjd_to_ajd_spec.rb6
-rw-r--r--spec/ruby/library/date/append_spec.rb6
-rw-r--r--spec/ruby/library/date/asctime_spec.rb6
-rw-r--r--spec/ruby/library/date/boat_spec.rb24
-rw-r--r--spec/ruby/library/date/case_compare_spec.rb6
-rw-r--r--spec/ruby/library/date/civil_spec.rb7
-rw-r--r--spec/ruby/library/date/commercial_spec.rb17
-rw-r--r--spec/ruby/library/date/commercial_to_jd_spec.rb6
-rw-r--r--spec/ruby/library/date/comparison_spec.rb6
-rw-r--r--spec/ruby/library/date/constants_spec.rb48
-rw-r--r--spec/ruby/library/date/conversions_spec.rb43
-rw-r--r--spec/ruby/library/date/ctime_spec.rb6
-rw-r--r--spec/ruby/library/date/cwday_spec.rb6
-rw-r--r--spec/ruby/library/date/cweek_spec.rb6
-rw-r--r--spec/ruby/library/date/cwyear_spec.rb6
-rw-r--r--spec/ruby/library/date/day_fraction_spec.rb6
-rw-r--r--spec/ruby/library/date/day_fraction_to_time_spec.rb6
-rw-r--r--spec/ruby/library/date/day_spec.rb9
-rw-r--r--spec/ruby/library/date/deconstruct_keys_spec.rb42
-rw-r--r--spec/ruby/library/date/downto_spec.rb18
-rw-r--r--spec/ruby/library/date/england_spec.rb6
-rw-r--r--spec/ruby/library/date/eql_spec.rb12
-rw-r--r--spec/ruby/library/date/format/bag/method_missing_spec.rb6
-rw-r--r--spec/ruby/library/date/format/bag/to_hash_spec.rb6
-rw-r--r--spec/ruby/library/date/friday_spec.rb12
-rw-r--r--spec/ruby/library/date/gregorian_leap_spec.rb15
-rw-r--r--spec/ruby/library/date/gregorian_spec.rb16
-rw-r--r--spec/ruby/library/date/hash_spec.rb8
-rw-r--r--spec/ruby/library/date/infinity/abs_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/coerce_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/comparison_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/d_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/finite_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/infinite_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/nan_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/uminus_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/uplus_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity/zero_spec.rb6
-rw-r--r--spec/ruby/library/date/infinity_spec.rb67
-rw-r--r--spec/ruby/library/date/inspect_spec.rb6
-rw-r--r--spec/ruby/library/date/iso8601_spec.rb56
-rw-r--r--spec/ruby/library/date/italy_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_spec.rb15
-rw-r--r--spec/ruby/library/date/jd_to_ajd_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_to_civil_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_to_commercial_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_to_ld_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_to_mjd_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_to_ordinal_spec.rb6
-rw-r--r--spec/ruby/library/date/jd_to_wday_spec.rb6
-rw-r--r--spec/ruby/library/date/julian_leap_spec.rb15
-rw-r--r--spec/ruby/library/date/julian_spec.rb16
-rw-r--r--spec/ruby/library/date/ld_spec.rb6
-rw-r--r--spec/ruby/library/date/ld_to_jd_spec.rb6
-rw-r--r--spec/ruby/library/date/leap_spec.rb10
-rw-r--r--spec/ruby/library/date/mday_spec.rb6
-rw-r--r--spec/ruby/library/date/minus_month_spec.rb23
-rw-r--r--spec/ruby/library/date/minus_spec.rb30
-rw-r--r--spec/ruby/library/date/mjd_spec.rb6
-rw-r--r--spec/ruby/library/date/mjd_to_jd_spec.rb6
-rw-r--r--spec/ruby/library/date/mon_spec.rb7
-rw-r--r--spec/ruby/library/date/monday_spec.rb8
-rw-r--r--spec/ruby/library/date/month_spec.rb7
-rw-r--r--spec/ruby/library/date/new_spec.rb7
-rw-r--r--spec/ruby/library/date/new_start_spec.rb6
-rw-r--r--spec/ruby/library/date/next_day_spec.rb14
-rw-r--r--spec/ruby/library/date/next_month_spec.rb29
-rw-r--r--spec/ruby/library/date/next_spec.rb6
-rw-r--r--spec/ruby/library/date/next_year_spec.rb12
-rw-r--r--spec/ruby/library/date/ordinal_spec.rb7
-rw-r--r--spec/ruby/library/date/ordinal_to_jd_spec.rb6
-rw-r--r--spec/ruby/library/date/parse_spec.rb159
-rw-r--r--spec/ruby/library/date/plus_spec.rb20
-rw-r--r--spec/ruby/library/date/prev_day_spec.rb14
-rw-r--r--spec/ruby/library/date/prev_month_spec.rb29
-rw-r--r--spec/ruby/library/date/prev_year_spec.rb12
-rw-r--r--spec/ruby/library/date/relationship_spec.rb20
-rw-r--r--spec/ruby/library/date/rfc3339_spec.rb13
-rw-r--r--spec/ruby/library/date/right_shift_spec.rb6
-rw-r--r--spec/ruby/library/date/saturday_spec.rb8
-rw-r--r--spec/ruby/library/date/shared/civil.rb57
-rw-r--r--spec/ruby/library/date/shared/commercial.rb39
-rw-r--r--spec/ruby/library/date/shared/jd.rb14
-rw-r--r--spec/ruby/library/date/shared/month.rb6
-rw-r--r--spec/ruby/library/date/shared/ordinal.rb22
-rw-r--r--spec/ruby/library/date/shared/parse.rb54
-rw-r--r--spec/ruby/library/date/shared/parse_eu.rb37
-rw-r--r--spec/ruby/library/date/shared/parse_us.rb36
-rw-r--r--spec/ruby/library/date/shared/valid_civil.rb36
-rw-r--r--spec/ruby/library/date/shared/valid_commercial.rb34
-rw-r--r--spec/ruby/library/date/shared/valid_jd.rb20
-rw-r--r--spec/ruby/library/date/shared/valid_ordinal.rb26
-rw-r--r--spec/ruby/library/date/start_spec.rb6
-rw-r--r--spec/ruby/library/date/step_spec.rb56
-rw-r--r--spec/ruby/library/date/strftime_spec.rb41
-rw-r--r--spec/ruby/library/date/strptime_spec.rb149
-rw-r--r--spec/ruby/library/date/succ_spec.rb6
-rw-r--r--spec/ruby/library/date/sunday_spec.rb8
-rw-r--r--spec/ruby/library/date/thursday_spec.rb8
-rw-r--r--spec/ruby/library/date/time/to_date_spec.rb42
-rw-r--r--spec/ruby/library/date/time_to_day_fraction_spec.rb6
-rw-r--r--spec/ruby/library/date/to_s_spec.rb6
-rw-r--r--spec/ruby/library/date/today_spec.rb14
-rw-r--r--spec/ruby/library/date/tuesday_spec.rb8
-rw-r--r--spec/ruby/library/date/upto_spec.rb16
-rw-r--r--spec/ruby/library/date/valid_civil_spec.rb9
-rw-r--r--spec/ruby/library/date/valid_commercial_spec.rb8
-rw-r--r--spec/ruby/library/date/valid_date_spec.rb7
-rw-r--r--spec/ruby/library/date/valid_jd_spec.rb9
-rw-r--r--spec/ruby/library/date/valid_ordinal_spec.rb9
-rw-r--r--spec/ruby/library/date/valid_time_spec.rb6
-rw-r--r--spec/ruby/library/date/wday_spec.rb9
-rw-r--r--spec/ruby/library/date/wednesday_spec.rb8
-rw-r--r--spec/ruby/library/date/yday_spec.rb7
-rw-r--r--spec/ruby/library/date/year_spec.rb9
-rw-r--r--spec/ruby/library/date/zone_to_diff_spec.rb6
-rw-r--r--spec/ruby/library/datetime/_strptime_spec.rb6
-rw-r--r--spec/ruby/library/datetime/add_spec.rb9
-rw-r--r--spec/ruby/library/datetime/civil_spec.rb6
-rw-r--r--spec/ruby/library/datetime/commercial_spec.rb6
-rw-r--r--spec/ruby/library/datetime/deconstruct_keys_spec.rb44
-rw-r--r--spec/ruby/library/datetime/hour_spec.rb47
-rw-r--r--spec/ruby/library/datetime/httpdate_spec.rb6
-rw-r--r--spec/ruby/library/datetime/iso8601_spec.rb10
-rw-r--r--spec/ruby/library/datetime/jd_spec.rb6
-rw-r--r--spec/ruby/library/datetime/jisx0301_spec.rb10
-rw-r--r--spec/ruby/library/datetime/min_spec.rb6
-rw-r--r--spec/ruby/library/datetime/minute_spec.rb6
-rw-r--r--spec/ruby/library/datetime/new_offset_spec.rb6
-rw-r--r--spec/ruby/library/datetime/new_spec.rb52
-rw-r--r--spec/ruby/library/datetime/now_spec.rb25
-rw-r--r--spec/ruby/library/datetime/offset_spec.rb6
-rw-r--r--spec/ruby/library/datetime/ordinal_spec.rb6
-rw-r--r--spec/ruby/library/datetime/parse_spec.rb127
-rw-r--r--spec/ruby/library/datetime/rfc2822_spec.rb10
-rw-r--r--spec/ruby/library/datetime/rfc3339_spec.rb10
-rw-r--r--spec/ruby/library/datetime/rfc822_spec.rb6
-rw-r--r--spec/ruby/library/datetime/sec_fraction_spec.rb6
-rw-r--r--spec/ruby/library/datetime/sec_spec.rb6
-rw-r--r--spec/ruby/library/datetime/second_fraction_spec.rb6
-rw-r--r--spec/ruby/library/datetime/second_spec.rb6
-rw-r--r--spec/ruby/library/datetime/shared/min.rb40
-rw-r--r--spec/ruby/library/datetime/shared/sec.rb45
-rw-r--r--spec/ruby/library/datetime/strftime_spec.rb52
-rw-r--r--spec/ruby/library/datetime/strptime_spec.rb6
-rw-r--r--spec/ruby/library/datetime/subtract_spec.rb19
-rw-r--r--spec/ruby/library/datetime/time/to_datetime_spec.rb40
-rw-r--r--spec/ruby/library/datetime/to_date_spec.rb37
-rw-r--r--spec/ruby/library/datetime/to_datetime_spec.rb9
-rw-r--r--spec/ruby/library/datetime/to_s_spec.rb17
-rw-r--r--spec/ruby/library/datetime/to_time_spec.rb48
-rw-r--r--spec/ruby/library/datetime/xmlschema_spec.rb10
-rw-r--r--spec/ruby/library/datetime/yday_spec.rb7
-rw-r--r--spec/ruby/library/datetime/zone_spec.rb6
-rw-r--r--spec/ruby/library/delegate/delegate_class/instance_method_spec.rb52
-rw-r--r--spec/ruby/library/delegate/delegate_class/instance_methods_spec.rb26
-rw-r--r--spec/ruby/library/delegate/delegate_class/private_instance_methods_spec.rb23
-rw-r--r--spec/ruby/library/delegate/delegate_class/protected_instance_methods_spec.rb29
-rw-r--r--spec/ruby/library/delegate/delegate_class/public_instance_methods_spec.rb25
-rw-r--r--spec/ruby/library/delegate/delegate_class/respond_to_missing_spec.rb24
-rw-r--r--spec/ruby/library/delegate/delegator/case_compare_spec.rb11
-rw-r--r--spec/ruby/library/delegate/delegator/compare_spec.rb11
-rw-r--r--spec/ruby/library/delegate/delegator/complement_spec.rb11
-rw-r--r--spec/ruby/library/delegate/delegator/eql_spec.rb35
-rw-r--r--spec/ruby/library/delegate/delegator/equal_spec.rb13
-rw-r--r--spec/ruby/library/delegate/delegator/equal_value_spec.rb24
-rw-r--r--spec/ruby/library/delegate/delegator/frozen_spec.rb39
-rw-r--r--spec/ruby/library/delegate/delegator/hash_spec.rb11
-rw-r--r--spec/ruby/library/delegate/delegator/marshal_spec.rb21
-rw-r--r--spec/ruby/library/delegate/delegator/method_spec.rb69
-rw-r--r--spec/ruby/library/delegate/delegator/methods_spec.rb37
-rw-r--r--spec/ruby/library/delegate/delegator/not_equal_spec.rb24
-rw-r--r--spec/ruby/library/delegate/delegator/not_spec.rb11
-rw-r--r--spec/ruby/library/delegate/delegator/private_methods_spec.rb20
-rw-r--r--spec/ruby/library/delegate/delegator/protected_methods_spec.rb18
-rw-r--r--spec/ruby/library/delegate/delegator/public_methods_spec.rb18
-rw-r--r--spec/ruby/library/delegate/delegator/send_spec.rb26
-rw-r--r--spec/ruby/library/delegate/delegator/taint_spec.rb8
-rw-r--r--spec/ruby/library/delegate/delegator/tap_spec.rb16
-rw-r--r--spec/ruby/library/delegate/delegator/trust_spec.rb8
-rw-r--r--spec/ruby/library/delegate/delegator/untaint_spec.rb8
-rw-r--r--spec/ruby/library/delegate/delegator/untrust_spec.rb8
-rw-r--r--spec/ruby/library/delegate/fixtures/classes.rb60
-rw-r--r--spec/ruby/library/digest/bubblebabble_spec.rb29
-rw-r--r--spec/ruby/library/digest/hexencode_spec.rb31
-rw-r--r--spec/ruby/library/digest/instance/append_spec.rb7
-rw-r--r--spec/ruby/library/digest/instance/new_spec.rb19
-rw-r--r--spec/ruby/library/digest/instance/shared/update.rb8
-rw-r--r--spec/ruby/library/digest/instance/update_spec.rb7
-rw-r--r--spec/ruby/library/digest/md5/append_spec.rb7
-rw-r--r--spec/ruby/library/digest/md5/block_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/md5/digest_bang_spec.rb13
-rw-r--r--spec/ruby/library/digest/md5/digest_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/md5/digest_spec.rb32
-rw-r--r--spec/ruby/library/digest/md5/equal_spec.rb37
-rw-r--r--spec/ruby/library/digest/md5/file_spec.rb43
-rw-r--r--spec/ruby/library/digest/md5/hexdigest_bang_spec.rb14
-rw-r--r--spec/ruby/library/digest/md5/hexdigest_spec.rb32
-rw-r--r--spec/ruby/library/digest/md5/inspect_spec.rb11
-rw-r--r--spec/ruby/library/digest/md5/length_spec.rb7
-rw-r--r--spec/ruby/library/digest/md5/reset_spec.rb14
-rw-r--r--spec/ruby/library/digest/md5/shared/constants.rb17
-rw-r--r--spec/ruby/library/digest/md5/shared/length.rb8
-rw-r--r--spec/ruby/library/digest/md5/shared/update.rb7
-rw-r--r--spec/ruby/library/digest/md5/size_spec.rb7
-rw-r--r--spec/ruby/library/digest/md5/to_s_spec.rb24
-rw-r--r--spec/ruby/library/digest/md5/update_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha1/digest_spec.rb20
-rw-r--r--spec/ruby/library/digest/sha1/file_spec.rb43
-rw-r--r--spec/ruby/library/digest/sha1/shared/constants.rb18
-rw-r--r--spec/ruby/library/digest/sha2/hexdigest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha256/append_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha256/block_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha256/digest_bang_spec.rb13
-rw-r--r--spec/ruby/library/digest/sha256/digest_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha256/digest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha256/equal_spec.rb36
-rw-r--r--spec/ruby/library/digest/sha256/file_spec.rb47
-rw-r--r--spec/ruby/library/digest/sha256/hexdigest_bang_spec.rb14
-rw-r--r--spec/ruby/library/digest/sha256/hexdigest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha256/inspect_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha256/length_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha256/reset_spec.rb14
-rw-r--r--spec/ruby/library/digest/sha256/shared/constants.rb18
-rw-r--r--spec/ruby/library/digest/sha256/shared/length.rb8
-rw-r--r--spec/ruby/library/digest/sha256/shared/update.rb7
-rw-r--r--spec/ruby/library/digest/sha256/size_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha256/to_s_spec.rb21
-rw-r--r--spec/ruby/library/digest/sha256/update_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha384/append_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha384/block_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha384/digest_bang_spec.rb13
-rw-r--r--spec/ruby/library/digest/sha384/digest_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha384/digest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha384/equal_spec.rb36
-rw-r--r--spec/ruby/library/digest/sha384/file_spec.rb43
-rw-r--r--spec/ruby/library/digest/sha384/hexdigest_bang_spec.rb14
-rw-r--r--spec/ruby/library/digest/sha384/hexdigest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha384/inspect_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha384/length_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha384/reset_spec.rb14
-rw-r--r--spec/ruby/library/digest/sha384/shared/constants.rb19
-rw-r--r--spec/ruby/library/digest/sha384/shared/length.rb8
-rw-r--r--spec/ruby/library/digest/sha384/shared/update.rb7
-rw-r--r--spec/ruby/library/digest/sha384/size_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha384/to_s_spec.rb21
-rw-r--r--spec/ruby/library/digest/sha384/update_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha512/append_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha512/block_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha512/digest_bang_spec.rb13
-rw-r--r--spec/ruby/library/digest/sha512/digest_length_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha512/digest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha512/equal_spec.rb36
-rw-r--r--spec/ruby/library/digest/sha512/file_spec.rb43
-rw-r--r--spec/ruby/library/digest/sha512/hexdigest_bang_spec.rb14
-rw-r--r--spec/ruby/library/digest/sha512/hexdigest_spec.rb32
-rw-r--r--spec/ruby/library/digest/sha512/inspect_spec.rb11
-rw-r--r--spec/ruby/library/digest/sha512/length_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha512/reset_spec.rb14
-rw-r--r--spec/ruby/library/digest/sha512/shared/constants.rb18
-rw-r--r--spec/ruby/library/digest/sha512/shared/length.rb8
-rw-r--r--spec/ruby/library/digest/sha512/shared/update.rb7
-rw-r--r--spec/ruby/library/digest/sha512/size_spec.rb7
-rw-r--r--spec/ruby/library/digest/sha512/to_s_spec.rb21
-rw-r--r--spec/ruby/library/digest/sha512/update_spec.rb7
-rw-r--r--spec/ruby/library/drb/fixtures/test_server.rb8
-rw-r--r--spec/ruby/library/drb/start_service_spec.rb33
-rw-r--r--spec/ruby/library/erb/def_class_spec.rb31
-rw-r--r--spec/ruby/library/erb/def_method_spec.rb26
-rw-r--r--spec/ruby/library/erb/def_module_spec.rb30
-rw-r--r--spec/ruby/library/erb/defmethod/def_erb_method_spec.rb66
-rw-r--r--spec/ruby/library/erb/filename_spec.rb40
-rw-r--r--spec/ruby/library/erb/fixtures/classes.rb5
-rw-r--r--spec/ruby/library/erb/new_spec.rb157
-rw-r--r--spec/ruby/library/erb/result_spec.rb86
-rw-r--r--spec/ruby/library/erb/run_spec.rb96
-rw-r--r--spec/ruby/library/erb/src_spec.rb33
-rw-r--r--spec/ruby/library/erb/util/h_spec.rb7
-rw-r--r--spec/ruby/library/erb/util/html_escape_spec.rb7
-rw-r--r--spec/ruby/library/erb/util/shared/html_escape.rb42
-rw-r--r--spec/ruby/library/erb/util/shared/url_encode.rb42
-rw-r--r--spec/ruby/library/erb/util/u_spec.rb7
-rw-r--r--spec/ruby/library/erb/util/url_encode_spec.rb7
-rw-r--r--spec/ruby/library/etc/confstr_spec.rb14
-rw-r--r--spec/ruby/library/etc/endgrent_spec.rb7
-rw-r--r--spec/ruby/library/etc/endpwent_spec.rb7
-rw-r--r--spec/ruby/library/etc/getgrent_spec.rb7
-rw-r--r--spec/ruby/library/etc/getgrgid_spec.rb69
-rw-r--r--spec/ruby/library/etc/getgrnam_spec.rb30
-rw-r--r--spec/ruby/library/etc/getlogin_spec.rb43
-rw-r--r--spec/ruby/library/etc/getpwent_spec.rb7
-rw-r--r--spec/ruby/library/etc/getpwnam_spec.rb28
-rw-r--r--spec/ruby/library/etc/getpwuid_spec.rb36
-rw-r--r--spec/ruby/library/etc/group_spec.rb27
-rw-r--r--spec/ruby/library/etc/nprocessors_spec.rb9
-rw-r--r--spec/ruby/library/etc/passwd_spec.rb15
-rw-r--r--spec/ruby/library/etc/shared/windows.rb7
-rw-r--r--spec/ruby/library/etc/struct_group_spec.rb35
-rw-r--r--spec/ruby/library/etc/struct_passwd_spec.rb43
-rw-r--r--spec/ruby/library/etc/sysconf_spec.rb22
-rw-r--r--spec/ruby/library/etc/sysconfdir_spec.rb8
-rw-r--r--spec/ruby/library/etc/systmpdir_spec.rb8
-rw-r--r--spec/ruby/library/etc/uname_spec.rb14
-rw-r--r--spec/ruby/library/expect/expect_spec.rb63
-rw-r--r--spec/ruby/library/fiddle/handle/initialize_spec.rb10
-rw-r--r--spec/ruby/library/find/find_spec.rb30
-rw-r--r--spec/ruby/library/find/fixtures/common.rb178
-rw-r--r--spec/ruby/library/find/prune_spec.rb12
-rw-r--r--spec/ruby/library/getoptlong/each_option_spec.rb7
-rw-r--r--spec/ruby/library/getoptlong/each_spec.rb7
-rw-r--r--spec/ruby/library/getoptlong/error_message_spec.rb23
-rw-r--r--spec/ruby/library/getoptlong/get_option_spec.rb7
-rw-r--r--spec/ruby/library/getoptlong/get_spec.rb7
-rw-r--r--spec/ruby/library/getoptlong/initialize_spec.rb28
-rw-r--r--spec/ruby/library/getoptlong/ordering_spec.rb38
-rw-r--r--spec/ruby/library/getoptlong/set_options_spec.rb98
-rw-r--r--spec/ruby/library/getoptlong/shared/each.rb18
-rw-r--r--spec/ruby/library/getoptlong/shared/get.rb62
-rw-r--r--spec/ruby/library/getoptlong/terminate_spec.rb30
-rw-r--r--spec/ruby/library/getoptlong/terminated_spec.rb17
-rw-r--r--spec/ruby/library/io-wait/wait_readable_spec.rb42
-rw-r--r--spec/ruby/library/io-wait/wait_spec.rb162
-rw-r--r--spec/ruby/library/io-wait/wait_writable_spec.rb37
-rw-r--r--spec/ruby/library/ipaddr/hton_spec.rb30
-rw-r--r--spec/ruby/library/ipaddr/ipv4_conversion_spec.rb44
-rw-r--r--spec/ruby/library/ipaddr/new_spec.rb92
-rw-r--r--spec/ruby/library/ipaddr/operator_spec.rb82
-rw-r--r--spec/ruby/library/ipaddr/reverse_spec.rb27
-rw-r--r--spec/ruby/library/ipaddr/to_s_spec.rb20
-rw-r--r--spec/ruby/library/irb/fixtures/irb.rb3
-rw-r--r--spec/ruby/library/irb/irb_spec.rb19
-rw-r--r--spec/ruby/library/logger/device/close_spec.rb22
-rw-r--r--spec/ruby/library/logger/device/new_spec.rb47
-rw-r--r--spec/ruby/library/logger/device/write_spec.rb42
-rw-r--r--spec/ruby/library/logger/fixtures/common.rb9
-rw-r--r--spec/ruby/library/logger/logger/add_spec.rb81
-rw-r--r--spec/ruby/library/logger/logger/close_spec.rb20
-rw-r--r--spec/ruby/library/logger/logger/datetime_format_spec.rb60
-rw-r--r--spec/ruby/library/logger/logger/debug_spec.rb52
-rw-r--r--spec/ruby/library/logger/logger/error_spec.rb53
-rw-r--r--spec/ruby/library/logger/logger/fatal_spec.rb53
-rw-r--r--spec/ruby/library/logger/logger/info_spec.rb53
-rw-r--r--spec/ruby/library/logger/logger/new_spec.rb118
-rw-r--r--spec/ruby/library/logger/logger/unknown_spec.rb36
-rw-r--r--spec/ruby/library/logger/logger/warn_spec.rb53
-rw-r--r--spec/ruby/library/logger/severity_spec.rb13
-rw-r--r--spec/ruby/library/matrix/I_spec.rb6
-rw-r--r--spec/ruby/library/matrix/antisymmetric_spec.rb36
-rw-r--r--spec/ruby/library/matrix/build_spec.rb73
-rw-r--r--spec/ruby/library/matrix/clone_spec.rb25
-rw-r--r--spec/ruby/library/matrix/coerce_spec.rb8
-rw-r--r--spec/ruby/library/matrix/collect_spec.rb6
-rw-r--r--spec/ruby/library/matrix/column_size_spec.rb13
-rw-r--r--spec/ruby/library/matrix/column_spec.rb35
-rw-r--r--spec/ruby/library/matrix/column_vector_spec.rb25
-rw-r--r--spec/ruby/library/matrix/column_vectors_spec.rb26
-rw-r--r--spec/ruby/library/matrix/columns_spec.rb42
-rw-r--r--spec/ruby/library/matrix/conj_spec.rb6
-rw-r--r--spec/ruby/library/matrix/conjugate_spec.rb6
-rw-r--r--spec/ruby/library/matrix/constructor_spec.rb63
-rw-r--r--spec/ruby/library/matrix/det_spec.rb7
-rw-r--r--spec/ruby/library/matrix/determinant_spec.rb7
-rw-r--r--spec/ruby/library/matrix/diagonal_spec.rb72
-rw-r--r--spec/ruby/library/matrix/divide_spec.rb54
-rw-r--r--spec/ruby/library/matrix/each_spec.rb74
-rw-r--r--spec/ruby/library/matrix/each_with_index_spec.rb81
-rw-r--r--spec/ruby/library/matrix/eigenvalue_decomposition/eigenvalue_matrix_spec.rb9
-rw-r--r--spec/ruby/library/matrix/eigenvalue_decomposition/eigenvalues_spec.rb22
-rw-r--r--spec/ruby/library/matrix/eigenvalue_decomposition/eigenvector_matrix_spec.rb20
-rw-r--r--spec/ruby/library/matrix/eigenvalue_decomposition/eigenvectors_spec.rb22
-rw-r--r--spec/ruby/library/matrix/eigenvalue_decomposition/initialize_spec.rb24
-rw-r--r--spec/ruby/library/matrix/eigenvalue_decomposition/to_a_spec.rb18
-rw-r--r--spec/ruby/library/matrix/element_reference_spec.rb23
-rw-r--r--spec/ruby/library/matrix/empty_spec.rb68
-rw-r--r--spec/ruby/library/matrix/eql_spec.rb11
-rw-r--r--spec/ruby/library/matrix/equal_value_spec.rb11
-rw-r--r--spec/ruby/library/matrix/exponent_spec.rb62
-rw-r--r--spec/ruby/library/matrix/find_index_spec.rb146
-rw-r--r--spec/ruby/library/matrix/fixtures/classes.rb7
-rw-r--r--spec/ruby/library/matrix/hash_spec.rb15
-rw-r--r--spec/ruby/library/matrix/hermitian_spec.rb34
-rw-r--r--spec/ruby/library/matrix/identity_spec.rb6
-rw-r--r--spec/ruby/library/matrix/imag_spec.rb6
-rw-r--r--spec/ruby/library/matrix/imaginary_spec.rb6
-rw-r--r--spec/ruby/library/matrix/inspect_spec.rb27
-rw-r--r--spec/ruby/library/matrix/inv_spec.rb7
-rw-r--r--spec/ruby/library/matrix/inverse_from_spec.rb6
-rw-r--r--spec/ruby/library/matrix/inverse_spec.rb7
-rw-r--r--spec/ruby/library/matrix/lower_triangular_spec.rb24
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/determinant_spec.rb21
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/initialize_spec.rb13
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/l_spec.rb18
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/p_spec.rb18
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/solve_spec.rb53
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/to_a_spec.rb33
-rw-r--r--spec/ruby/library/matrix/lup_decomposition/u_spec.rb18
-rw-r--r--spec/ruby/library/matrix/map_spec.rb6
-rw-r--r--spec/ruby/library/matrix/minor_spec.rb85
-rw-r--r--spec/ruby/library/matrix/minus_spec.rb42
-rw-r--r--spec/ruby/library/matrix/multiply_spec.rb69
-rw-r--r--spec/ruby/library/matrix/new_spec.rb8
-rw-r--r--spec/ruby/library/matrix/normal_spec.rb26
-rw-r--r--spec/ruby/library/matrix/orthogonal_spec.rb26
-rw-r--r--spec/ruby/library/matrix/permutation_spec.rb32
-rw-r--r--spec/ruby/library/matrix/plus_spec.rb42
-rw-r--r--spec/ruby/library/matrix/rank_spec.rb19
-rw-r--r--spec/ruby/library/matrix/real_spec.rb43
-rw-r--r--spec/ruby/library/matrix/rect_spec.rb6
-rw-r--r--spec/ruby/library/matrix/rectangular_spec.rb6
-rw-r--r--spec/ruby/library/matrix/regular_spec.rb31
-rw-r--r--spec/ruby/library/matrix/round_spec.rb21
-rw-r--r--spec/ruby/library/matrix/row_size_spec.rb13
-rw-r--r--spec/ruby/library/matrix/row_spec.rb36
-rw-r--r--spec/ruby/library/matrix/row_vector_spec.rb24
-rw-r--r--spec/ruby/library/matrix/row_vectors_spec.rb26
-rw-r--r--spec/ruby/library/matrix/rows_spec.rb41
-rw-r--r--spec/ruby/library/matrix/scalar/Fail_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/Raise_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/divide_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/exponent_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/included_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/initialize_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/minus_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/multiply_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar/plus_spec.rb6
-rw-r--r--spec/ruby/library/matrix/scalar_spec.rb67
-rw-r--r--spec/ruby/library/matrix/shared/collect.rb26
-rw-r--r--spec/ruby/library/matrix/shared/conjugate.rb20
-rw-r--r--spec/ruby/library/matrix/shared/determinant.rb38
-rw-r--r--spec/ruby/library/matrix/shared/equal_value.rb33
-rw-r--r--spec/ruby/library/matrix/shared/identity.rb19
-rw-r--r--spec/ruby/library/matrix/shared/imaginary.rb20
-rw-r--r--spec/ruby/library/matrix/shared/inverse.rb38
-rw-r--r--spec/ruby/library/matrix/shared/rectangular.rb18
-rw-r--r--spec/ruby/library/matrix/shared/trace.rb12
-rw-r--r--spec/ruby/library/matrix/shared/transpose.rb19
-rw-r--r--spec/ruby/library/matrix/singular_spec.rb31
-rw-r--r--spec/ruby/library/matrix/spec_helper.rb35
-rw-r--r--spec/ruby/library/matrix/square_spec.rb28
-rw-r--r--spec/ruby/library/matrix/symmetric_spec.rb29
-rw-r--r--spec/ruby/library/matrix/t_spec.rb6
-rw-r--r--spec/ruby/library/matrix/to_a_spec.rb11
-rw-r--r--spec/ruby/library/matrix/to_s_spec.rb6
-rw-r--r--spec/ruby/library/matrix/tr_spec.rb7
-rw-r--r--spec/ruby/library/matrix/trace_spec.rb7
-rw-r--r--spec/ruby/library/matrix/transpose_spec.rb6
-rw-r--r--spec/ruby/library/matrix/unit_spec.rb6
-rw-r--r--spec/ruby/library/matrix/unitary_spec.rb32
-rw-r--r--spec/ruby/library/matrix/upper_triangular_spec.rb24
-rw-r--r--spec/ruby/library/matrix/vector/cross_product_spec.rb14
-rw-r--r--spec/ruby/library/matrix/vector/each2_spec.rb49
-rw-r--r--spec/ruby/library/matrix/vector/eql_spec.rb16
-rw-r--r--spec/ruby/library/matrix/vector/inner_product_spec.rb22
-rw-r--r--spec/ruby/library/matrix/vector/normalize_spec.rb18
-rw-r--r--spec/ruby/library/matrix/zero_spec.rb52
-rw-r--r--spec/ruby/library/mkmf/mkmf_spec.rb7
-rw-r--r--spec/ruby/library/monitor/enter_spec.rb28
-rw-r--r--spec/ruby/library/monitor/exit_spec.rb10
-rw-r--r--spec/ruby/library/monitor/mon_initialize_spec.rb31
-rw-r--r--spec/ruby/library/monitor/new_cond_spec.rb88
-rw-r--r--spec/ruby/library/monitor/synchronize_spec.rb41
-rw-r--r--spec/ruby/library/monitor/try_enter_spec.rb39
-rw-r--r--spec/ruby/library/net-ftp/FTPError_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/FTPPermError_spec.rb15
-rw-r--r--spec/ruby/library/net-ftp/FTPProtoError_spec.rb15
-rw-r--r--spec/ruby/library/net-ftp/FTPReplyError_spec.rb15
-rw-r--r--spec/ruby/library/net-ftp/FTPTempError_spec.rb15
-rw-r--r--spec/ruby/library/net-ftp/abort_spec.rb65
-rw-r--r--spec/ruby/library/net-ftp/acct_spec.rb61
-rw-r--r--spec/ruby/library/net-ftp/binary_spec.rb27
-rw-r--r--spec/ruby/library/net-ftp/chdir_spec.rb102
-rw-r--r--spec/ruby/library/net-ftp/close_spec.rb33
-rw-r--r--spec/ruby/library/net-ftp/closed_spec.rb24
-rw-r--r--spec/ruby/library/net-ftp/connect_spec.rb46
-rw-r--r--spec/ruby/library/net-ftp/debug_mode_spec.rb26
-rw-r--r--spec/ruby/library/net-ftp/default_passive_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/delete_spec.rb62
-rw-r--r--spec/ruby/library/net-ftp/dir_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/fixtures/default_passive.rb3
-rw-r--r--spec/ruby/library/net-ftp/fixtures/passive.rb2
-rw-r--r--spec/ruby/library/net-ftp/fixtures/putbinaryfile3
-rw-r--r--spec/ruby/library/net-ftp/fixtures/puttextfile3
-rw-r--r--spec/ruby/library/net-ftp/fixtures/server.rb279
-rw-r--r--spec/ruby/library/net-ftp/get_spec.rb24
-rw-r--r--spec/ruby/library/net-ftp/getbinaryfile_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/getdir_spec.rb10
-rw-r--r--spec/ruby/library/net-ftp/gettextfile_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/help_spec.rb69
-rw-r--r--spec/ruby/library/net-ftp/initialize_spec.rb408
-rw-r--r--spec/ruby/library/net-ftp/last_response_code_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/last_response_spec.rb28
-rw-r--r--spec/ruby/library/net-ftp/lastresp_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/list_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/login_spec.rb198
-rw-r--r--spec/ruby/library/net-ftp/ls_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/mdtm_spec.rb41
-rw-r--r--spec/ruby/library/net-ftp/mkdir_spec.rb64
-rw-r--r--spec/ruby/library/net-ftp/mtime_spec.rb53
-rw-r--r--spec/ruby/library/net-ftp/nlst_spec.rb95
-rw-r--r--spec/ruby/library/net-ftp/noop_spec.rb41
-rw-r--r--spec/ruby/library/net-ftp/open_spec.rb58
-rw-r--r--spec/ruby/library/net-ftp/passive_spec.rb31
-rw-r--r--spec/ruby/library/net-ftp/put_spec.rb24
-rw-r--r--spec/ruby/library/net-ftp/putbinaryfile_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/puttextfile_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/pwd_spec.rb56
-rw-r--r--spec/ruby/library/net-ftp/quit_spec.rb36
-rw-r--r--spec/ruby/library/net-ftp/rename_spec.rb97
-rw-r--r--spec/ruby/library/net-ftp/resume_spec.rb26
-rw-r--r--spec/ruby/library/net-ftp/retrbinary_spec.rb33
-rw-r--r--spec/ruby/library/net-ftp/retrlines_spec.rb37
-rw-r--r--spec/ruby/library/net-ftp/return_code_spec.rb27
-rw-r--r--spec/ruby/library/net-ftp/rmdir_spec.rb61
-rw-r--r--spec/ruby/library/net-ftp/sendcmd_spec.rb57
-rw-r--r--spec/ruby/library/net-ftp/set_socket_spec.rb11
-rw-r--r--spec/ruby/library/net-ftp/shared/getbinaryfile.rb152
-rw-r--r--spec/ruby/library/net-ftp/shared/gettextfile.rb102
-rw-r--r--spec/ruby/library/net-ftp/shared/last_response_code.rb27
-rw-r--r--spec/ruby/library/net-ftp/shared/list.rb106
-rw-r--r--spec/ruby/library/net-ftp/shared/putbinaryfile.rb169
-rw-r--r--spec/ruby/library/net-ftp/shared/puttextfile.rb130
-rw-r--r--spec/ruby/library/net-ftp/shared/pwd.rb5
-rw-r--r--spec/ruby/library/net-ftp/site_spec.rb56
-rw-r--r--spec/ruby/library/net-ftp/size_spec.rb51
-rw-r--r--spec/ruby/library/net-ftp/spec_helper.rb7
-rw-r--r--spec/ruby/library/net-ftp/status_spec.rb70
-rw-r--r--spec/ruby/library/net-ftp/storbinary_spec.rb52
-rw-r--r--spec/ruby/library/net-ftp/storlines_spec.rb47
-rw-r--r--spec/ruby/library/net-ftp/system_spec.rb51
-rw-r--r--spec/ruby/library/net-ftp/voidcmd_spec.rb57
-rw-r--r--spec/ruby/library/net-ftp/welcome_spec.rb28
-rw-r--r--spec/ruby/library/net-http/HTTPBadResponse_spec.rb8
-rw-r--r--spec/ruby/library/net-http/HTTPClientExcepton_spec.rb12
-rw-r--r--spec/ruby/library/net-http/HTTPError_spec.rb12
-rw-r--r--spec/ruby/library/net-http/HTTPFatalError_spec.rb12
-rw-r--r--spec/ruby/library/net-http/HTTPHeaderSyntaxError_spec.rb8
-rw-r--r--spec/ruby/library/net-http/HTTPRetriableError_spec.rb12
-rw-r--r--spec/ruby/library/net-http/HTTPServerException_spec.rb12
-rw-r--r--spec/ruby/library/net-http/http/Proxy_spec.rb35
-rw-r--r--spec/ruby/library/net-http/http/active_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/address_spec.rb9
-rw-r--r--spec/ruby/library/net-http/http/close_on_empty_response_spec.rb10
-rw-r--r--spec/ruby/library/net-http/http/copy_spec.rb21
-rw-r--r--spec/ruby/library/net-http/http/default_port_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/delete_spec.rb21
-rw-r--r--spec/ruby/library/net-http/http/finish_spec.rb29
-rw-r--r--spec/ruby/library/net-http/http/fixtures/http_server.rb123
-rw-r--r--spec/ruby/library/net-http/http/get2_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/get_print_spec.rb30
-rw-r--r--spec/ruby/library/net-http/http/get_response_spec.rb30
-rw-r--r--spec/ruby/library/net-http/http/get_spec.rb94
-rw-r--r--spec/ruby/library/net-http/http/head2_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/head_spec.rb25
-rw-r--r--spec/ruby/library/net-http/http/http_default_port_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/https_default_port_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/initialize_spec.rb46
-rw-r--r--spec/ruby/library/net-http/http/inspect_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/is_version_1_1_spec.rb7
-rw-r--r--spec/ruby/library/net-http/http/is_version_1_2_spec.rb7
-rw-r--r--spec/ruby/library/net-http/http/lock_spec.rb21
-rw-r--r--spec/ruby/library/net-http/http/mkcol_spec.rb21
-rw-r--r--spec/ruby/library/net-http/http/move_spec.rb25
-rw-r--r--spec/ruby/library/net-http/http/new_spec.rb86
-rw-r--r--spec/ruby/library/net-http/http/newobj_spec.rb48
-rw-r--r--spec/ruby/library/net-http/http/open_timeout_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/options_spec.rb25
-rw-r--r--spec/ruby/library/net-http/http/port_spec.rb9
-rw-r--r--spec/ruby/library/net-http/http/post2_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/post_form_spec.rb22
-rw-r--r--spec/ruby/library/net-http/http/post_spec.rb76
-rw-r--r--spec/ruby/library/net-http/http/propfind_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/proppatch_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/proxy_address_spec.rb31
-rw-r--r--spec/ruby/library/net-http/http/proxy_class_spec.rb9
-rw-r--r--spec/ruby/library/net-http/http/proxy_pass_spec.rb39
-rw-r--r--spec/ruby/library/net-http/http/proxy_port_spec.rb39
-rw-r--r--spec/ruby/library/net-http/http/proxy_user_spec.rb39
-rw-r--r--spec/ruby/library/net-http/http/put2_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/put_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/read_timeout_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/request_get_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/request_head_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/request_post_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/request_put_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/request_spec.rb109
-rw-r--r--spec/ruby/library/net-http/http/request_types_spec.rb254
-rw-r--r--spec/ruby/library/net-http/http/send_request_spec.rb61
-rw-r--r--spec/ruby/library/net-http/http/set_debug_output_spec.rb33
-rw-r--r--spec/ruby/library/net-http/http/shared/request_get.rb41
-rw-r--r--spec/ruby/library/net-http/http/shared/request_head.rb41
-rw-r--r--spec/ruby/library/net-http/http/shared/request_post.rb41
-rw-r--r--spec/ruby/library/net-http/http/shared/request_put.rb41
-rw-r--r--spec/ruby/library/net-http/http/shared/started.rb26
-rw-r--r--spec/ruby/library/net-http/http/shared/version_1_1.rb6
-rw-r--r--spec/ruby/library/net-http/http/shared/version_1_2.rb6
-rw-r--r--spec/ruby/library/net-http/http/socket_type_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/start_spec.rb111
-rw-r--r--spec/ruby/library/net-http/http/started_spec.rb8
-rw-r--r--spec/ruby/library/net-http/http/trace_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/unlock_spec.rb24
-rw-r--r--spec/ruby/library/net-http/http/use_ssl_spec.rb9
-rw-r--r--spec/ruby/library/net-http/http/version_1_1_spec.rb7
-rw-r--r--spec/ruby/library/net-http/http/version_1_2_spec.rb20
-rw-r--r--spec/ruby/library/net-http/httpexceptions/fixtures/classes.rb5
-rw-r--r--spec/ruby/library/net-http/httpexceptions/initialize_spec.rb17
-rw-r--r--spec/ruby/library/net-http/httpexceptions/response_spec.rb10
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/body_exist_spec.rb21
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/body_spec.rb30
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/body_stream_spec.rb32
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb135
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/inspect_spec.rb25
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/method_spec.rb15
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/path_spec.rb12
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/request_body_permitted_spec.rb12
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/response_body_permitted_spec.rb12
-rw-r--r--spec/ruby/library/net-http/httpgenericrequest/set_body_internal_spec.rb21
-rw-r--r--spec/ruby/library/net-http/httpheader/add_field_spec.rb31
-rw-r--r--spec/ruby/library/net-http/httpheader/basic_auth_spec.rb14
-rw-r--r--spec/ruby/library/net-http/httpheader/canonical_each_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/chunked_spec.rb22
-rw-r--r--spec/ruby/library/net-http/httpheader/content_length_spec.rb54
-rw-r--r--spec/ruby/library/net-http/httpheader/content_range_spec.rb32
-rw-r--r--spec/ruby/library/net-http/httpheader/content_type_spec.rb26
-rw-r--r--spec/ruby/library/net-http/httpheader/delete_spec.rb30
-rw-r--r--spec/ruby/library/net-http/httpheader/each_capitalized_name_spec.rb35
-rw-r--r--spec/ruby/library/net-http/httpheader/each_capitalized_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/each_header_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/each_key_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/each_name_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/each_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/each_value_spec.rb35
-rw-r--r--spec/ruby/library/net-http/httpheader/element_reference_spec.rb39
-rw-r--r--spec/ruby/library/net-http/httpheader/element_set_spec.rb41
-rw-r--r--spec/ruby/library/net-http/httpheader/fetch_spec.rb68
-rw-r--r--spec/ruby/library/net-http/httpheader/fixtures/classes.rb11
-rw-r--r--spec/ruby/library/net-http/httpheader/form_data_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/get_fields_spec.rb39
-rw-r--r--spec/ruby/library/net-http/httpheader/initialize_http_header_spec.rb21
-rw-r--r--spec/ruby/library/net-http/httpheader/key_spec.rb21
-rw-r--r--spec/ruby/library/net-http/httpheader/length_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/main_type_spec.rb24
-rw-r--r--spec/ruby/library/net-http/httpheader/proxy_basic_auth_spec.rb14
-rw-r--r--spec/ruby/library/net-http/httpheader/range_length_spec.rb32
-rw-r--r--spec/ruby/library/net-http/httpheader/range_spec.rb48
-rw-r--r--spec/ruby/library/net-http/httpheader/set_content_type_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/set_form_data_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/set_range_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/each_capitalized.rb31
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/each_header.rb31
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/each_name.rb31
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/set_content_type.rb18
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/set_form_data.rb27
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/set_range.rb89
-rw-r--r--spec/ruby/library/net-http/httpheader/shared/size.rb18
-rw-r--r--spec/ruby/library/net-http/httpheader/size_spec.rb8
-rw-r--r--spec/ruby/library/net-http/httpheader/sub_type_spec.rb32
-rw-r--r--spec/ruby/library/net-http/httpheader/to_hash_spec.rb25
-rw-r--r--spec/ruby/library/net-http/httpheader/type_params_spec.rb24
-rw-r--r--spec/ruby/library/net-http/httprequest/initialize_spec.rb45
-rw-r--r--spec/ruby/library/net-http/httpresponse/body_permitted_spec.rb13
-rw-r--r--spec/ruby/library/net-http/httpresponse/body_spec.rb7
-rw-r--r--spec/ruby/library/net-http/httpresponse/code_spec.rb24
-rw-r--r--spec/ruby/library/net-http/httpresponse/code_type_spec.rb24
-rw-r--r--spec/ruby/library/net-http/httpresponse/entity_spec.rb7
-rw-r--r--spec/ruby/library/net-http/httpresponse/error_spec.rb24
-rw-r--r--spec/ruby/library/net-http/httpresponse/error_type_spec.rb24
-rw-r--r--spec/ruby/library/net-http/httpresponse/exception_type_spec.rb13
-rw-r--r--spec/ruby/library/net-http/httpresponse/header_spec.rb9
-rw-r--r--spec/ruby/library/net-http/httpresponse/http_version_spec.rb12
-rw-r--r--spec/ruby/library/net-http/httpresponse/initialize_spec.rb11
-rw-r--r--spec/ruby/library/net-http/httpresponse/inspect_spec.rb15
-rw-r--r--spec/ruby/library/net-http/httpresponse/message_spec.rb9
-rw-r--r--spec/ruby/library/net-http/httpresponse/msg_spec.rb9
-rw-r--r--spec/ruby/library/net-http/httpresponse/read_body_spec.rb86
-rw-r--r--spec/ruby/library/net-http/httpresponse/read_header_spec.rb9
-rw-r--r--spec/ruby/library/net-http/httpresponse/read_new_spec.rb23
-rw-r--r--spec/ruby/library/net-http/httpresponse/reading_body_spec.rb58
-rw-r--r--spec/ruby/library/net-http/httpresponse/response_spec.rb9
-rw-r--r--spec/ruby/library/net-http/httpresponse/shared/body.rb20
-rw-r--r--spec/ruby/library/net-http/httpresponse/value_spec.rb24
-rw-r--r--spec/ruby/library/objectspace/dump_all_spec.rb112
-rw-r--r--spec/ruby/library/objectspace/dump_spec.rb70
-rw-r--r--spec/ruby/library/objectspace/fixtures/trace.rb6
-rw-r--r--spec/ruby/library/objectspace/memsize_of_all_spec.rb22
-rw-r--r--spec/ruby/library/objectspace/memsize_of_spec.rb34
-rw-r--r--spec/ruby/library/objectspace/reachable_objects_from_spec.rb59
-rw-r--r--spec/ruby/library/objectspace/trace_object_allocations_spec.rb163
-rw-r--r--spec/ruby/library/objectspace/trace_spec.rb13
-rw-r--r--spec/ruby/library/observer/add_observer_spec.rb23
-rw-r--r--spec/ruby/library/observer/count_observers_spec.rb23
-rw-r--r--spec/ruby/library/observer/delete_observer_spec.rb19
-rw-r--r--spec/ruby/library/observer/delete_observers_spec.rb19
-rw-r--r--spec/ruby/library/observer/fixtures/classes.rb17
-rw-r--r--spec/ruby/library/observer/notify_observers_spec.rb31
-rw-r--r--spec/ruby/library/open3/capture2_spec.rb6
-rw-r--r--spec/ruby/library/open3/capture2e_spec.rb6
-rw-r--r--spec/ruby/library/open3/capture3_spec.rb6
-rw-r--r--spec/ruby/library/open3/pipeline_r_spec.rb6
-rw-r--r--spec/ruby/library/open3/pipeline_rw_spec.rb6
-rw-r--r--spec/ruby/library/open3/pipeline_spec.rb6
-rw-r--r--spec/ruby/library/open3/pipeline_start_spec.rb6
-rw-r--r--spec/ruby/library/open3/pipeline_w_spec.rb6
-rw-r--r--spec/ruby/library/open3/popen2_spec.rb6
-rw-r--r--spec/ruby/library/open3/popen2e_spec.rb6
-rw-r--r--spec/ruby/library/open3/popen3_spec.rb41
-rw-r--r--spec/ruby/library/openssl/cipher_spec.rb9
-rw-r--r--spec/ruby/library/openssl/digest/append_spec.rb6
-rw-r--r--spec/ruby/library/openssl/digest/block_length_spec.rb44
-rw-r--r--spec/ruby/library/openssl/digest/digest_length_spec.rb44
-rw-r--r--spec/ruby/library/openssl/digest/digest_spec.rb62
-rw-r--r--spec/ruby/library/openssl/digest/initialize_spec.rb137
-rw-r--r--spec/ruby/library/openssl/digest/name_spec.rb16
-rw-r--r--spec/ruby/library/openssl/digest/reset_spec.rb36
-rw-r--r--spec/ruby/library/openssl/digest/shared/update.rb123
-rw-r--r--spec/ruby/library/openssl/digest/update_spec.rb6
-rw-r--r--spec/ruby/library/openssl/fixed_length_secure_compare_spec.rb42
-rw-r--r--spec/ruby/library/openssl/hmac/digest_spec.rb16
-rw-r--r--spec/ruby/library/openssl/hmac/hexdigest_spec.rb16
-rw-r--r--spec/ruby/library/openssl/kdf/pbkdf2_hmac_spec.rb162
-rw-r--r--spec/ruby/library/openssl/kdf/scrypt_spec.rb210
-rw-r--r--spec/ruby/library/openssl/random/pseudo_bytes_spec.rb8
-rw-r--r--spec/ruby/library/openssl/random/random_bytes_spec.rb6
-rw-r--r--spec/ruby/library/openssl/random/shared/random_bytes.rb29
-rw-r--r--spec/ruby/library/openssl/secure_compare_spec.rb38
-rw-r--r--spec/ruby/library/openssl/shared/constants.rb11
-rw-r--r--spec/ruby/library/openssl/x509/name/parse_spec.rb48
-rw-r--r--spec/ruby/library/openssl/x509/store/verify_spec.rb78
-rw-r--r--spec/ruby/library/openstruct/delete_field_spec.rb19
-rw-r--r--spec/ruby/library/openstruct/element_reference_spec.rb13
-rw-r--r--spec/ruby/library/openstruct/element_set_spec.rb13
-rw-r--r--spec/ruby/library/openstruct/equal_value_spec.rb28
-rw-r--r--spec/ruby/library/openstruct/fixtures/classes.rb4
-rw-r--r--spec/ruby/library/openstruct/frozen_spec.rb40
-rw-r--r--spec/ruby/library/openstruct/initialize_spec.rb8
-rw-r--r--spec/ruby/library/openstruct/inspect_spec.rb8
-rw-r--r--spec/ruby/library/openstruct/marshal_dump_spec.rb9
-rw-r--r--spec/ruby/library/openstruct/marshal_load_spec.rb12
-rw-r--r--spec/ruby/library/openstruct/method_missing_spec.rb24
-rw-r--r--spec/ruby/library/openstruct/new_spec.rb20
-rw-r--r--spec/ruby/library/openstruct/shared/inspect.rb20
-rw-r--r--spec/ruby/library/openstruct/to_h_spec.rb68
-rw-r--r--spec/ruby/library/openstruct/to_s_spec.rb8
-rw-r--r--spec/ruby/library/optionparser/order_spec.rb28
-rw-r--r--spec/ruby/library/optionparser/parse_spec.rb28
-rw-r--r--spec/ruby/library/pathname/absolute_spec.rb22
-rw-r--r--spec/ruby/library/pathname/birthtime_spec.rb16
-rw-r--r--spec/ruby/library/pathname/divide_spec.rb6
-rw-r--r--spec/ruby/library/pathname/empty_spec.rb32
-rw-r--r--spec/ruby/library/pathname/equal_value_spec.rb14
-rw-r--r--spec/ruby/library/pathname/glob_spec.rb92
-rw-r--r--spec/ruby/library/pathname/hash_spec.rb14
-rw-r--r--spec/ruby/library/pathname/inspect_spec.rb10
-rw-r--r--spec/ruby/library/pathname/join_spec.rb40
-rw-r--r--spec/ruby/library/pathname/new_spec.rb23
-rw-r--r--spec/ruby/library/pathname/parent_spec.rb18
-rw-r--r--spec/ruby/library/pathname/pathname_spec.rb19
-rw-r--r--spec/ruby/library/pathname/plus_spec.rb6
-rw-r--r--spec/ruby/library/pathname/realdirpath_spec.rb10
-rw-r--r--spec/ruby/library/pathname/realpath_spec.rb10
-rw-r--r--spec/ruby/library/pathname/relative_path_from_spec.rb55
-rw-r--r--spec/ruby/library/pathname/relative_spec.rb22
-rw-r--r--spec/ruby/library/pathname/root_spec.rb26
-rw-r--r--spec/ruby/library/pathname/shared/plus.rb8
-rw-r--r--spec/ruby/library/pathname/sub_spec.rb15
-rw-r--r--spec/ruby/library/pp/pp_spec.rb30
-rw-r--r--spec/ruby/library/prime/each_spec.rb167
-rw-r--r--spec/ruby/library/prime/instance_spec.rb21
-rw-r--r--spec/ruby/library/prime/int_from_prime_division_spec.rb13
-rw-r--r--spec/ruby/library/prime/integer/each_prime_spec.rb13
-rw-r--r--spec/ruby/library/prime/integer/from_prime_division_spec.rb13
-rw-r--r--spec/ruby/library/prime/integer/prime_division_spec.rb19
-rw-r--r--spec/ruby/library/prime/integer/prime_spec.rb17
-rw-r--r--spec/ruby/library/prime/next_spec.rb7
-rw-r--r--spec/ruby/library/prime/prime_division_spec.rb25
-rw-r--r--spec/ruby/library/prime/prime_spec.rb17
-rw-r--r--spec/ruby/library/prime/shared/next.rb8
-rw-r--r--spec/ruby/library/prime/succ_spec.rb7
-rw-r--r--spec/ruby/library/random/formatter/alphanumeric_spec.rb54
-rw-r--r--spec/ruby/library/rbconfig/rbconfig_spec.rb163
-rw-r--r--spec/ruby/library/rbconfig/sizeof/limits_spec.rb40
-rw-r--r--spec/ruby/library/rbconfig/sizeof/sizeof_spec.rb30
-rw-r--r--spec/ruby/library/rbconfig/unicode_emoji_version_spec.rb17
-rw-r--r--spec/ruby/library/rbconfig/unicode_version_spec.rb17
-rw-r--r--spec/ruby/library/readline/basic_quote_characters_spec.rb18
-rw-r--r--spec/ruby/library/readline/basic_word_break_characters_spec.rb16
-rw-r--r--spec/ruby/library/readline/completer_quote_characters_spec.rb16
-rw-r--r--spec/ruby/library/readline/completer_word_break_characters_spec.rb16
-rw-r--r--spec/ruby/library/readline/completion_append_character_spec.rb16
-rw-r--r--spec/ruby/library/readline/completion_case_fold_spec.rb18
-rw-r--r--spec/ruby/library/readline/completion_proc_spec.rb22
-rw-r--r--spec/ruby/library/readline/constants_spec.rb18
-rw-r--r--spec/ruby/library/readline/emacs_editing_mode_spec.rb11
-rw-r--r--spec/ruby/library/readline/filename_quote_characters_spec.rb18
-rw-r--r--spec/ruby/library/readline/history/append_spec.rb28
-rw-r--r--spec/ruby/library/readline/history/delete_at_spec.rb38
-rw-r--r--spec/ruby/library/readline/history/each_spec.rb23
-rw-r--r--spec/ruby/library/readline/history/element_reference_spec.rb35
-rw-r--r--spec/ruby/library/readline/history/element_set_spec.rb35
-rw-r--r--spec/ruby/library/readline/history/empty_spec.rb13
-rw-r--r--spec/ruby/library/readline/history/history_spec.rb9
-rw-r--r--spec/ruby/library/readline/history/length_spec.rb9
-rw-r--r--spec/ruby/library/readline/history/pop_spec.rb23
-rw-r--r--spec/ruby/library/readline/history/push_spec.rb26
-rw-r--r--spec/ruby/library/readline/history/shared/size.rb14
-rw-r--r--spec/ruby/library/readline/history/shift_spec.rb23
-rw-r--r--spec/ruby/library/readline/history/size_spec.rb9
-rw-r--r--spec/ruby/library/readline/history/to_s_spec.rb9
-rw-r--r--spec/ruby/library/readline/readline_spec.rb26
-rw-r--r--spec/ruby/library/readline/spec_helper.rb11
-rw-r--r--spec/ruby/library/readline/vi_editing_mode_spec.rb11
-rw-r--r--spec/ruby/library/resolv/fixtures/hosts1
-rw-r--r--spec/ruby/library/resolv/get_address_spec.rb19
-rw-r--r--spec/ruby/library/resolv/get_addresses_spec.rb12
-rw-r--r--spec/ruby/library/resolv/get_name_spec.rb18
-rw-r--r--spec/ruby/library/resolv/get_names_spec.rb11
-rw-r--r--spec/ruby/library/ripper/lex_spec.rb23
-rw-r--r--spec/ruby/library/ripper/sexp_spec.rb13
-rw-r--r--spec/ruby/library/rubygems/gem/bin_path_spec.rb35
-rw-r--r--spec/ruby/library/rubygems/gem/load_path_insert_index_spec.rb10
-rw-r--r--spec/ruby/library/securerandom/base64_spec.rb55
-rw-r--r--spec/ruby/library/securerandom/bytes_spec.rb8
-rw-r--r--spec/ruby/library/securerandom/hex_spec.rb54
-rw-r--r--spec/ruby/library/securerandom/random_bytes_spec.rb53
-rw-r--r--spec/ruby/library/securerandom/random_number_spec.rb97
-rw-r--r--spec/ruby/library/shellwords/shellwords_spec.rb33
-rw-r--r--spec/ruby/library/singleton/allocate_spec.rb8
-rw-r--r--spec/ruby/library/singleton/clone_spec.rb8
-rw-r--r--spec/ruby/library/singleton/dump_spec.rb14
-rw-r--r--spec/ruby/library/singleton/dup_spec.rb8
-rw-r--r--spec/ruby/library/singleton/fixtures/classes.rb18
-rw-r--r--spec/ruby/library/singleton/instance_spec.rb30
-rw-r--r--spec/ruby/library/singleton/load_spec.rb20
-rw-r--r--spec/ruby/library/singleton/new_spec.rb8
-rw-r--r--spec/ruby/library/socket/addrinfo/afamily_spec.rb35
-rw-r--r--spec/ruby/library/socket/addrinfo/bind_spec.rb28
-rw-r--r--spec/ruby/library/socket/addrinfo/canonname_spec.rb27
-rw-r--r--spec/ruby/library/socket/addrinfo/connect_from_spec.rb75
-rw-r--r--spec/ruby/library/socket/addrinfo/connect_spec.rb35
-rw-r--r--spec/ruby/library/socket/addrinfo/connect_to_spec.rb75
-rw-r--r--spec/ruby/library/socket/addrinfo/family_addrinfo_spec.rb113
-rw-r--r--spec/ruby/library/socket/addrinfo/foreach_spec.rb9
-rw-r--r--spec/ruby/library/socket/addrinfo/getaddrinfo_spec.rb87
-rw-r--r--spec/ruby/library/socket/addrinfo/getnameinfo_spec.rb40
-rw-r--r--spec/ruby/library/socket/addrinfo/initialize_spec.rb589
-rw-r--r--spec/ruby/library/socket/addrinfo/inspect_sockaddr_spec.rb48
-rw-r--r--spec/ruby/library/socket/addrinfo/inspect_spec.rb63
-rw-r--r--spec/ruby/library/socket/addrinfo/ip_address_spec.rb64
-rw-r--r--spec/ruby/library/socket/addrinfo/ip_port_spec.rb33
-rw-r--r--spec/ruby/library/socket/addrinfo/ip_spec.rb62
-rw-r--r--spec/ruby/library/socket/addrinfo/ip_unpack_spec.rb33
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv4_loopback_spec.rb41
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv4_multicast_spec.rb27
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv4_private_spec.rb45
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv4_spec.rb33
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_linklocal_spec.rb23
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_loopback_spec.rb43
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_mc_global_spec.rb20
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_mc_linklocal_spec.rb19
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_mc_nodelocal_spec.rb18
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_mc_orglocal_spec.rb18
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_mc_sitelocal_spec.rb18
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_multicast_spec.rb46
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_sitelocal_spec.rb23
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_spec.rb33
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_to_ipv4_spec.rb71
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_unique_local_spec.rb18
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_unspecified_spec.rb15
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_v4compat_spec.rb20
-rw-r--r--spec/ruby/library/socket/addrinfo/ipv6_v4mapped_spec.rb20
-rw-r--r--spec/ruby/library/socket/addrinfo/listen_spec.rb34
-rw-r--r--spec/ruby/library/socket/addrinfo/marshal_dump_spec.rb80
-rw-r--r--spec/ruby/library/socket/addrinfo/marshal_load_spec.rb33
-rw-r--r--spec/ruby/library/socket/addrinfo/pfamily_spec.rb41
-rw-r--r--spec/ruby/library/socket/addrinfo/protocol_spec.rb22
-rw-r--r--spec/ruby/library/socket/addrinfo/shared/to_sockaddr.rb47
-rw-r--r--spec/ruby/library/socket/addrinfo/socktype_spec.rb21
-rw-r--r--spec/ruby/library/socket/addrinfo/tcp_spec.rb34
-rw-r--r--spec/ruby/library/socket/addrinfo/to_s_spec.rb6
-rw-r--r--spec/ruby/library/socket/addrinfo/to_sockaddr_spec.rb6
-rw-r--r--spec/ruby/library/socket/addrinfo/udp_spec.rb34
-rw-r--r--spec/ruby/library/socket/addrinfo/unix_path_spec.rb35
-rw-r--r--spec/ruby/library/socket/addrinfo/unix_spec.rb69
-rw-r--r--spec/ruby/library/socket/ancillarydata/cmsg_is_spec.rb33
-rw-r--r--spec/ruby/library/socket/ancillarydata/data_spec.rb9
-rw-r--r--spec/ruby/library/socket/ancillarydata/family_spec.rb9
-rw-r--r--spec/ruby/library/socket/ancillarydata/initialize_spec.rb284
-rw-r--r--spec/ruby/library/socket/ancillarydata/int_spec.rb43
-rw-r--r--spec/ruby/library/socket/ancillarydata/ip_pktinfo_spec.rb145
-rw-r--r--spec/ruby/library/socket/ancillarydata/ipv6_pktinfo_addr_spec.rb11
-rw-r--r--spec/ruby/library/socket/ancillarydata/ipv6_pktinfo_ifindex_spec.rb11
-rw-r--r--spec/ruby/library/socket/ancillarydata/ipv6_pktinfo_spec.rb89
-rw-r--r--spec/ruby/library/socket/ancillarydata/level_spec.rb9
-rw-r--r--spec/ruby/library/socket/ancillarydata/type_spec.rb9
-rw-r--r--spec/ruby/library/socket/ancillarydata/unix_rights_spec.rb61
-rw-r--r--spec/ruby/library/socket/basicsocket/close_read_spec.rb43
-rw-r--r--spec/ruby/library/socket/basicsocket/close_write_spec.rb48
-rw-r--r--spec/ruby/library/socket/basicsocket/connect_address_spec.rb152
-rw-r--r--spec/ruby/library/socket/basicsocket/do_not_reverse_lookup_spec.rb103
-rw-r--r--spec/ruby/library/socket/basicsocket/for_fd_spec.rb38
-rw-r--r--spec/ruby/library/socket/basicsocket/getpeereid_spec.rb36
-rw-r--r--spec/ruby/library/socket/basicsocket/getpeername_spec.rb25
-rw-r--r--spec/ruby/library/socket/basicsocket/getsockname_spec.rb28
-rw-r--r--spec/ruby/library/socket/basicsocket/getsockopt_spec.rb188
-rw-r--r--spec/ruby/library/socket/basicsocket/ioctl_spec.rb42
-rw-r--r--spec/ruby/library/socket/basicsocket/local_address_spec.rb10
-rw-r--r--spec/ruby/library/socket/basicsocket/read_nonblock_spec.rb74
-rw-r--r--spec/ruby/library/socket/basicsocket/read_spec.rb47
-rw-r--r--spec/ruby/library/socket/basicsocket/recv_nonblock_spec.rb142
-rw-r--r--spec/ruby/library/socket/basicsocket/recv_spec.rb229
-rw-r--r--spec/ruby/library/socket/basicsocket/recvmsg_nonblock_spec.rb267
-rw-r--r--spec/ruby/library/socket/basicsocket/recvmsg_spec.rb257
-rw-r--r--spec/ruby/library/socket/basicsocket/remote_address_spec.rb10
-rw-r--r--spec/ruby/library/socket/basicsocket/send_spec.rb220
-rw-r--r--spec/ruby/library/socket/basicsocket/sendmsg_nonblock_spec.rb118
-rw-r--r--spec/ruby/library/socket/basicsocket/sendmsg_spec.rb111
-rw-r--r--spec/ruby/library/socket/basicsocket/setsockopt_spec.rb334
-rw-r--r--spec/ruby/library/socket/basicsocket/shutdown_spec.rb155
-rw-r--r--spec/ruby/library/socket/basicsocket/write_nonblock_spec.rb43
-rw-r--r--spec/ruby/library/socket/constants/constants_spec.rb108
-rw-r--r--spec/ruby/library/socket/fixtures/classes.rb166
-rw-r--r--spec/ruby/library/socket/fixtures/send_io.txt1
-rw-r--r--spec/ruby/library/socket/ipsocket/addr_spec.rb105
-rw-r--r--spec/ruby/library/socket/ipsocket/getaddress_spec.rb28
-rw-r--r--spec/ruby/library/socket/ipsocket/inspect_spec.rb24
-rw-r--r--spec/ruby/library/socket/ipsocket/peeraddr_spec.rb117
-rw-r--r--spec/ruby/library/socket/ipsocket/recvfrom_spec.rb183
-rw-r--r--spec/ruby/library/socket/option/bool_spec.rb27
-rw-r--r--spec/ruby/library/socket/option/initialize_spec.rb83
-rw-r--r--spec/ruby/library/socket/option/inspect_spec.rb19
-rw-r--r--spec/ruby/library/socket/option/int_spec.rb43
-rw-r--r--spec/ruby/library/socket/option/linger_spec.rb76
-rw-r--r--spec/ruby/library/socket/option/new_spec.rb35
-rw-r--r--spec/ruby/library/socket/shared/address.rb259
-rw-r--r--spec/ruby/library/socket/shared/pack_sockaddr.rb92
-rw-r--r--spec/ruby/library/socket/shared/partially_closable_sockets.rb13
-rw-r--r--spec/ruby/library/socket/shared/socketpair.rb138
-rw-r--r--spec/ruby/library/socket/socket/accept_loop_spec.rb84
-rw-r--r--spec/ruby/library/socket/socket/accept_nonblock_spec.rb141
-rw-r--r--spec/ruby/library/socket/socket/accept_spec.rb121
-rw-r--r--spec/ruby/library/socket/socket/bind_spec.rb150
-rw-r--r--spec/ruby/library/socket/socket/connect_nonblock_spec.rb147
-rw-r--r--spec/ruby/library/socket/socket/connect_spec.rb78
-rw-r--r--spec/ruby/library/socket/socket/for_fd_spec.rb30
-rw-r--r--spec/ruby/library/socket/socket/getaddrinfo_spec.rb381
-rw-r--r--spec/ruby/library/socket/socket/gethostbyaddr_spec.rb121
-rw-r--r--spec/ruby/library/socket/socket/gethostbyname_spec.rb135
-rw-r--r--spec/ruby/library/socket/socket/gethostname_spec.rb18
-rw-r--r--spec/ruby/library/socket/socket/getifaddrs_spec.rb117
-rw-r--r--spec/ruby/library/socket/socket/getnameinfo_spec.rb155
-rw-r--r--spec/ruby/library/socket/socket/getservbyname_spec.rb32
-rw-r--r--spec/ruby/library/socket/socket/getservbyport_spec.rb23
-rw-r--r--spec/ruby/library/socket/socket/initialize_spec.rb87
-rw-r--r--spec/ruby/library/socket/socket/ip_address_list_spec.rb50
-rw-r--r--spec/ruby/library/socket/socket/ipv6only_bang_spec.rb20
-rw-r--r--spec/ruby/library/socket/socket/listen_spec.rb66
-rw-r--r--spec/ruby/library/socket/socket/local_address_spec.rb43
-rw-r--r--spec/ruby/library/socket/socket/pack_sockaddr_in_spec.rb7
-rw-r--r--spec/ruby/library/socket/socket/pack_sockaddr_un_spec.rb7
-rw-r--r--spec/ruby/library/socket/socket/pair_spec.rb7
-rw-r--r--spec/ruby/library/socket/socket/recvfrom_nonblock_spec.rb188
-rw-r--r--spec/ruby/library/socket/socket/recvfrom_spec.rb157
-rw-r--r--spec/ruby/library/socket/socket/remote_address_spec.rb54
-rw-r--r--spec/ruby/library/socket/socket/sockaddr_in_spec.rb7
-rw-r--r--spec/ruby/library/socket/socket/sockaddr_un_spec.rb7
-rw-r--r--spec/ruby/library/socket/socket/socket_spec.rb38
-rw-r--r--spec/ruby/library/socket/socket/socketpair_spec.rb7
-rw-r--r--spec/ruby/library/socket/socket/sysaccept_spec.rb91
-rw-r--r--spec/ruby/library/socket/socket/tcp_server_loop_spec.rb54
-rw-r--r--spec/ruby/library/socket/socket/tcp_server_sockets_spec.rb39
-rw-r--r--spec/ruby/library/socket/socket/tcp_spec.rb88
-rw-r--r--spec/ruby/library/socket/socket/udp_server_loop_on_spec.rb47
-rw-r--r--spec/ruby/library/socket/socket/udp_server_loop_spec.rb59
-rw-r--r--spec/ruby/library/socket/socket/udp_server_recv_spec.rb35
-rw-r--r--spec/ruby/library/socket/socket/udp_server_sockets_spec.rb39
-rw-r--r--spec/ruby/library/socket/socket/unix_server_loop_spec.rb56
-rw-r--r--spec/ruby/library/socket/socket/unix_server_socket_spec.rb46
-rw-r--r--spec/ruby/library/socket/socket/unix_spec.rb43
-rw-r--r--spec/ruby/library/socket/socket/unpack_sockaddr_in_spec.rb44
-rw-r--r--spec/ruby/library/socket/socket/unpack_sockaddr_un_spec.rb24
-rw-r--r--spec/ruby/library/socket/spec_helper.rb14
-rw-r--r--spec/ruby/library/socket/tcpserver/accept_nonblock_spec.rb85
-rw-r--r--spec/ruby/library/socket/tcpserver/accept_spec.rb132
-rw-r--r--spec/ruby/library/socket/tcpserver/gets_spec.rb16
-rw-r--r--spec/ruby/library/socket/tcpserver/initialize_spec.rb101
-rw-r--r--spec/ruby/library/socket/tcpserver/listen_spec.rb22
-rw-r--r--spec/ruby/library/socket/tcpserver/new_spec.rb137
-rw-r--r--spec/ruby/library/socket/tcpserver/sysaccept_spec.rb66
-rw-r--r--spec/ruby/library/socket/tcpsocket/gethostbyname_spec.rb119
-rw-r--r--spec/ruby/library/socket/tcpsocket/initialize_spec.rb100
-rw-r--r--spec/ruby/library/socket/tcpsocket/local_address_spec.rb73
-rw-r--r--spec/ruby/library/socket/tcpsocket/open_spec.rb6
-rw-r--r--spec/ruby/library/socket/tcpsocket/partially_closable_spec.rb21
-rw-r--r--spec/ruby/library/socket/tcpsocket/recv_nonblock_spec.rb48
-rw-r--r--spec/ruby/library/socket/tcpsocket/recv_spec.rb28
-rw-r--r--spec/ruby/library/socket/tcpsocket/remote_address_spec.rb72
-rw-r--r--spec/ruby/library/socket/tcpsocket/setsockopt_spec.rb45
-rw-r--r--spec/ruby/library/socket/tcpsocket/shared/new.rb118
-rw-r--r--spec/ruby/library/socket/udpsocket/bind_spec.rb83
-rw-r--r--spec/ruby/library/socket/udpsocket/connect_spec.rb35
-rw-r--r--spec/ruby/library/socket/udpsocket/initialize_spec.rb53
-rw-r--r--spec/ruby/library/socket/udpsocket/local_address_spec.rb80
-rw-r--r--spec/ruby/library/socket/udpsocket/new_spec.rb40
-rw-r--r--spec/ruby/library/socket/udpsocket/open_spec.rb13
-rw-r--r--spec/ruby/library/socket/udpsocket/recvfrom_nonblock_spec.rb111
-rw-r--r--spec/ruby/library/socket/udpsocket/remote_address_spec.rb79
-rw-r--r--spec/ruby/library/socket/udpsocket/send_spec.rb154
-rw-r--r--spec/ruby/library/socket/udpsocket/write_spec.rb21
-rw-r--r--spec/ruby/library/socket/unixserver/accept_nonblock_spec.rb87
-rw-r--r--spec/ruby/library/socket/unixserver/accept_spec.rb126
-rw-r--r--spec/ruby/library/socket/unixserver/for_fd_spec.rb21
-rw-r--r--spec/ruby/library/socket/unixserver/initialize_spec.rb26
-rw-r--r--spec/ruby/library/socket/unixserver/listen_spec.rb19
-rw-r--r--spec/ruby/library/socket/unixserver/new_spec.rb12
-rw-r--r--spec/ruby/library/socket/unixserver/open_spec.rb24
-rw-r--r--spec/ruby/library/socket/unixserver/shared/new.rb20
-rw-r--r--spec/ruby/library/socket/unixserver/sysaccept_spec.rb50
-rw-r--r--spec/ruby/library/socket/unixsocket/addr_spec.rb33
-rw-r--r--spec/ruby/library/socket/unixsocket/initialize_spec.rb56
-rw-r--r--spec/ruby/library/socket/unixsocket/inspect_spec.rb15
-rw-r--r--spec/ruby/library/socket/unixsocket/local_address_spec.rb92
-rw-r--r--spec/ruby/library/socket/unixsocket/new_spec.rb12
-rw-r--r--spec/ruby/library/socket/unixsocket/open_spec.rb26
-rw-r--r--spec/ruby/library/socket/unixsocket/pair_spec.rb18
-rw-r--r--spec/ruby/library/socket/unixsocket/partially_closable_spec.rb21
-rw-r--r--spec/ruby/library/socket/unixsocket/path_spec.rb24
-rw-r--r--spec/ruby/library/socket/unixsocket/peeraddr_spec.rb26
-rw-r--r--spec/ruby/library/socket/unixsocket/recv_io_spec.rb84
-rw-r--r--spec/ruby/library/socket/unixsocket/recvfrom_spec.rb174
-rw-r--r--spec/ruby/library/socket/unixsocket/remote_address_spec.rb43
-rw-r--r--spec/ruby/library/socket/unixsocket/send_io_spec.rb55
-rw-r--r--spec/ruby/library/socket/unixsocket/shared/new.rb22
-rw-r--r--spec/ruby/library/socket/unixsocket/shared/pair.rb47
-rw-r--r--spec/ruby/library/socket/unixsocket/socketpair_spec.rb18
-rw-r--r--spec/ruby/library/stringio/append_spec.rb74
-rw-r--r--spec/ruby/library/stringio/binmode_spec.rb23
-rw-r--r--spec/ruby/library/stringio/close_read_spec.rb31
-rw-r--r--spec/ruby/library/stringio/close_spec.rb23
-rw-r--r--spec/ruby/library/stringio/close_write_spec.rb31
-rw-r--r--spec/ruby/library/stringio/closed_read_spec.rb12
-rw-r--r--spec/ruby/library/stringio/closed_spec.rb16
-rw-r--r--spec/ruby/library/stringio/closed_write_spec.rb12
-rw-r--r--spec/ruby/library/stringio/each_byte_spec.rb11
-rw-r--r--spec/ruby/library/stringio/each_char_spec.rb11
-rw-r--r--spec/ruby/library/stringio/each_codepoint_spec.rb9
-rw-r--r--spec/ruby/library/stringio/each_line_spec.rb27
-rw-r--r--spec/ruby/library/stringio/each_spec.rb31
-rw-r--r--spec/ruby/library/stringio/eof_spec.rb11
-rw-r--r--spec/ruby/library/stringio/external_encoding_spec.rb25
-rw-r--r--spec/ruby/library/stringio/fcntl_spec.rb8
-rw-r--r--spec/ruby/library/stringio/fileno_spec.rb8
-rw-r--r--spec/ruby/library/stringio/fixtures/classes.rb15
-rw-r--r--spec/ruby/library/stringio/flush_spec.rb9
-rw-r--r--spec/ruby/library/stringio/fsync_spec.rb9
-rw-r--r--spec/ruby/library/stringio/getbyte_spec.rb19
-rw-r--r--spec/ruby/library/stringio/getc_spec.rb19
-rw-r--r--spec/ruby/library/stringio/getch_spec.rb44
-rw-r--r--spec/ruby/library/stringio/getpass_spec.rb11
-rw-r--r--spec/ruby/library/stringio/gets_spec.rb61
-rw-r--r--spec/ruby/library/stringio/initialize_spec.rb328
-rw-r--r--spec/ruby/library/stringio/inspect_spec.rb19
-rw-r--r--spec/ruby/library/stringio/internal_encoding_spec.rb10
-rw-r--r--spec/ruby/library/stringio/isatty_spec.rb7
-rw-r--r--spec/ruby/library/stringio/length_spec.rb7
-rw-r--r--spec/ruby/library/stringio/lineno_spec.rb30
-rw-r--r--spec/ruby/library/stringio/new_spec.rb10
-rw-r--r--spec/ruby/library/stringio/open_spec.rb215
-rw-r--r--spec/ruby/library/stringio/path_spec.rb8
-rw-r--r--spec/ruby/library/stringio/pid_spec.rb8
-rw-r--r--spec/ruby/library/stringio/pos_spec.rb28
-rw-r--r--spec/ruby/library/stringio/print_spec.rb102
-rw-r--r--spec/ruby/library/stringio/printf_spec.rb91
-rw-r--r--spec/ruby/library/stringio/putc_spec.rb103
-rw-r--r--spec/ruby/library/stringio/puts_spec.rb184
-rw-r--r--spec/ruby/library/stringio/read_nonblock_spec.rb53
-rw-r--r--spec/ruby/library/stringio/read_spec.rb62
-rw-r--r--spec/ruby/library/stringio/readbyte_spec.rb20
-rw-r--r--spec/ruby/library/stringio/readchar_spec.rb20
-rw-r--r--spec/ruby/library/stringio/readline_spec.rb58
-rw-r--r--spec/ruby/library/stringio/readlines_spec.rb118
-rw-r--r--spec/ruby/library/stringio/readpartial_spec.rb102
-rw-r--r--spec/ruby/library/stringio/reopen_spec.rb251
-rw-r--r--spec/ruby/library/stringio/rewind_spec.rb24
-rw-r--r--spec/ruby/library/stringio/seek_spec.rb67
-rw-r--r--spec/ruby/library/stringio/set_encoding_by_bom_spec.rb237
-rw-r--r--spec/ruby/library/stringio/set_encoding_spec.rb28
-rw-r--r--spec/ruby/library/stringio/shared/codepoints.rb45
-rw-r--r--spec/ruby/library/stringio/shared/each.rb209
-rw-r--r--spec/ruby/library/stringio/shared/each_byte.rb48
-rw-r--r--spec/ruby/library/stringio/shared/each_char.rb36
-rw-r--r--spec/ruby/library/stringio/shared/eof.rb24
-rw-r--r--spec/ruby/library/stringio/shared/getc.rb43
-rw-r--r--spec/ruby/library/stringio/shared/gets.rb249
-rw-r--r--spec/ruby/library/stringio/shared/isatty.rb5
-rw-r--r--spec/ruby/library/stringio/shared/length.rb5
-rw-r--r--spec/ruby/library/stringio/shared/read.rb145
-rw-r--r--spec/ruby/library/stringio/shared/readchar.rb29
-rw-r--r--spec/ruby/library/stringio/shared/sysread.rb15
-rw-r--r--spec/ruby/library/stringio/shared/tell.rb12
-rw-r--r--spec/ruby/library/stringio/shared/write.rb135
-rw-r--r--spec/ruby/library/stringio/size_spec.rb7
-rw-r--r--spec/ruby/library/stringio/string_spec.rb50
-rw-r--r--spec/ruby/library/stringio/stringio_spec.rb8
-rw-r--r--spec/ruby/library/stringio/sync_spec.rb19
-rw-r--r--spec/ruby/library/stringio/sysread_spec.rb53
-rw-r--r--spec/ruby/library/stringio/syswrite_spec.rb19
-rw-r--r--spec/ruby/library/stringio/tell_spec.rb7
-rw-r--r--spec/ruby/library/stringio/truncate_spec.rb62
-rw-r--r--spec/ruby/library/stringio/tty_spec.rb7
-rw-r--r--spec/ruby/library/stringio/ungetbyte_spec.rb42
-rw-r--r--spec/ruby/library/stringio/ungetc_spec.rb72
-rw-r--r--spec/ruby/library/stringio/write_nonblock_spec.rb25
-rw-r--r--spec/ruby/library/stringio/write_spec.rb19
-rw-r--r--spec/ruby/library/stringscanner/append_spec.rb11
-rw-r--r--spec/ruby/library/stringscanner/beginning_of_line_spec.rb7
-rw-r--r--spec/ruby/library/stringscanner/bol_spec.rb7
-rw-r--r--spec/ruby/library/stringscanner/captures_spec.rb36
-rw-r--r--spec/ruby/library/stringscanner/charpos_spec.rb18
-rw-r--r--spec/ruby/library/stringscanner/check_spec.rb93
-rw-r--r--spec/ruby/library/stringscanner/check_until_spec.rb129
-rw-r--r--spec/ruby/library/stringscanner/concat_spec.rb11
-rw-r--r--spec/ruby/library/stringscanner/dup_spec.rb39
-rw-r--r--spec/ruby/library/stringscanner/element_reference_spec.rb67
-rw-r--r--spec/ruby/library/stringscanner/eos_spec.rb20
-rw-r--r--spec/ruby/library/stringscanner/exist_spec.rb119
-rw-r--r--spec/ruby/library/stringscanner/fixed_anchor_spec.rb17
-rw-r--r--spec/ruby/library/stringscanner/get_byte_spec.rb84
-rw-r--r--spec/ruby/library/stringscanner/getch_spec.rb96
-rw-r--r--spec/ruby/library/stringscanner/initialize_spec.rb32
-rw-r--r--spec/ruby/library/stringscanner/inspect_spec.rb20
-rw-r--r--spec/ruby/library/stringscanner/match_spec.rb51
-rw-r--r--spec/ruby/library/stringscanner/matched_size_spec.rb24
-rw-r--r--spec/ruby/library/stringscanner/matched_spec.rb41
-rw-r--r--spec/ruby/library/stringscanner/must_C_version_spec.rb8
-rw-r--r--spec/ruby/library/stringscanner/named_captures_spec.rb28
-rw-r--r--spec/ruby/library/stringscanner/peek_byte_spec.rb35
-rw-r--r--spec/ruby/library/stringscanner/peek_spec.rb42
-rw-r--r--spec/ruby/library/stringscanner/pointer_spec.rb11
-rw-r--r--spec/ruby/library/stringscanner/pos_spec.rb11
-rw-r--r--spec/ruby/library/stringscanner/post_match_spec.rb28
-rw-r--r--spec/ruby/library/stringscanner/pre_match_spec.rb41
-rw-r--r--spec/ruby/library/stringscanner/reset_spec.rb15
-rw-r--r--spec/ruby/library/stringscanner/rest_size_spec.rb30
-rw-r--r--spec/ruby/library/stringscanner/rest_spec.rb48
-rw-r--r--spec/ruby/library/stringscanner/scan_byte_spec.rb98
-rw-r--r--spec/ruby/library/stringscanner/scan_full_spec.rb44
-rw-r--r--spec/ruby/library/stringscanner/scan_integer_spec.rb157
-rw-r--r--spec/ruby/library/stringscanner/scan_spec.rb101
-rw-r--r--spec/ruby/library/stringscanner/scan_until_spec.rb135
-rw-r--r--spec/ruby/library/stringscanner/search_full_spec.rb133
-rw-r--r--spec/ruby/library/stringscanner/shared/bol.rb25
-rw-r--r--spec/ruby/library/stringscanner/shared/concat.rb30
-rw-r--r--spec/ruby/library/stringscanner/shared/extract_range.rb11
-rw-r--r--spec/ruby/library/stringscanner/shared/extract_range_matched.rb13
-rw-r--r--spec/ruby/library/stringscanner/shared/pos.rb59
-rw-r--r--spec/ruby/library/stringscanner/size_spec.rb17
-rw-r--r--spec/ruby/library/stringscanner/skip_spec.rb32
-rw-r--r--spec/ruby/library/stringscanner/skip_until_spec.rb132
-rw-r--r--spec/ruby/library/stringscanner/string_spec.rb40
-rw-r--r--spec/ruby/library/stringscanner/terminate_spec.rb11
-rw-r--r--spec/ruby/library/stringscanner/unscan_spec.rb28
-rw-r--r--spec/ruby/library/stringscanner/values_at_spec.rb68
-rw-r--r--spec/ruby/library/syslog/alert_spec.rb10
-rw-r--r--spec/ruby/library/syslog/close_spec.rb58
-rw-r--r--spec/ruby/library/syslog/constants_spec.rb41
-rw-r--r--spec/ruby/library/syslog/crit_spec.rb10
-rw-r--r--spec/ruby/library/syslog/debug_spec.rb10
-rw-r--r--spec/ruby/library/syslog/emerg_spec.rb16
-rw-r--r--spec/ruby/library/syslog/err_spec.rb10
-rw-r--r--spec/ruby/library/syslog/facility_spec.rb48
-rw-r--r--spec/ruby/library/syslog/ident_spec.rb35
-rw-r--r--spec/ruby/library/syslog/info_spec.rb10
-rw-r--r--spec/ruby/library/syslog/inspect_spec.rb39
-rw-r--r--spec/ruby/library/syslog/instance_spec.rb13
-rw-r--r--spec/ruby/library/syslog/log_spec.rb56
-rw-r--r--spec/ruby/library/syslog/mask_spec.rb113
-rw-r--r--spec/ruby/library/syslog/notice_spec.rb10
-rw-r--r--spec/ruby/library/syslog/open_spec.rb92
-rw-r--r--spec/ruby/library/syslog/opened_spec.rb39
-rw-r--r--spec/ruby/library/syslog/options_spec.rb48
-rw-r--r--spec/ruby/library/syslog/reopen_spec.rb10
-rw-r--r--spec/ruby/library/syslog/shared/log.rb39
-rw-r--r--spec/ruby/library/syslog/shared/reopen.rb40
-rw-r--r--spec/ruby/library/syslog/warning_spec.rb10
-rw-r--r--spec/ruby/library/tempfile/_close_spec.rb21
-rw-r--r--spec/ruby/library/tempfile/close_spec.rb57
-rw-r--r--spec/ruby/library/tempfile/create_spec.rb176
-rw-r--r--spec/ruby/library/tempfile/delete_spec.rb7
-rw-r--r--spec/ruby/library/tempfile/initialize_spec.rb46
-rw-r--r--spec/ruby/library/tempfile/length_spec.rb7
-rw-r--r--spec/ruby/library/tempfile/open_spec.rb97
-rw-r--r--spec/ruby/library/tempfile/path_spec.rb26
-rw-r--r--spec/ruby/library/tempfile/shared/length.rb21
-rw-r--r--spec/ruby/library/tempfile/shared/unlink.rb12
-rw-r--r--spec/ruby/library/tempfile/size_spec.rb7
-rw-r--r--spec/ruby/library/tempfile/unlink_spec.rb7
-rw-r--r--spec/ruby/library/thread/queue_spec.rb8
-rw-r--r--spec/ruby/library/thread/sizedqueue_spec.rb8
-rw-r--r--spec/ruby/library/time/httpdate_spec.rb21
-rw-r--r--spec/ruby/library/time/iso8601_spec.rb7
-rw-r--r--spec/ruby/library/time/rfc2822_spec.rb7
-rw-r--r--spec/ruby/library/time/rfc822_spec.rb7
-rw-r--r--spec/ruby/library/time/shared/rfc2822.rb65
-rw-r--r--spec/ruby/library/time/shared/xmlschema.rb53
-rw-r--r--spec/ruby/library/time/to_time_spec.rb15
-rw-r--r--spec/ruby/library/time/xmlschema_spec.rb7
-rw-r--r--spec/ruby/library/timeout/error_spec.rb8
-rw-r--r--spec/ruby/library/timeout/timeout_spec.rb50
-rw-r--r--spec/ruby/library/tmpdir/dir/mktmpdir_spec.rb117
-rw-r--r--spec/ruby/library/tmpdir/dir/tmpdir_spec.rb10
-rw-r--r--spec/ruby/library/uri/decode_www_form_component_spec.rb6
-rw-r--r--spec/ruby/library/uri/decode_www_form_spec.rb6
-rw-r--r--spec/ruby/library/uri/encode_www_form_component_spec.rb6
-rw-r--r--spec/ruby/library/uri/encode_www_form_spec.rb6
-rw-r--r--spec/ruby/library/uri/eql_spec.rb10
-rw-r--r--spec/ruby/library/uri/equality_spec.rb46
-rw-r--r--spec/ruby/library/uri/escape/decode_spec.rb6
-rw-r--r--spec/ruby/library/uri/escape/encode_spec.rb6
-rw-r--r--spec/ruby/library/uri/escape/escape_spec.rb6
-rw-r--r--spec/ruby/library/uri/escape/unescape_spec.rb6
-rw-r--r--spec/ruby/library/uri/extract_spec.rb86
-rw-r--r--spec/ruby/library/uri/fixtures/classes.rb11
-rw-r--r--spec/ruby/library/uri/fixtures/normalization.rb54
-rw-r--r--spec/ruby/library/uri/ftp/build_spec.rb6
-rw-r--r--spec/ruby/library/uri/ftp/merge_spec.rb6
-rw-r--r--spec/ruby/library/uri/ftp/new2_spec.rb6
-rw-r--r--spec/ruby/library/uri/ftp/path_spec.rb26
-rw-r--r--spec/ruby/library/uri/ftp/set_typecode_spec.rb6
-rw-r--r--spec/ruby/library/uri/ftp/to_s_spec.rb15
-rw-r--r--spec/ruby/library/uri/ftp/typecode_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/absolute_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/build2_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/build_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/coerce_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/component_ary_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/component_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/default_port_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/eql_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/equal_value_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/fragment_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/hash_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/hierarchical_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/host_spec.rb13
-rw-r--r--spec/ruby/library/uri/generic/inspect_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/merge_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/minus_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/normalize_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/opaque_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/password_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/path_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/plus_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/port_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/query_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/registry_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/relative_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/route_from_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/route_to_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/scheme_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/select_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_fragment_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_host_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_opaque_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_password_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_path_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_port_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_query_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_registry_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_scheme_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_user_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/set_userinfo_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/to_s_spec.rb9
-rw-r--r--spec/ruby/library/uri/generic/use_registry_spec.rb6
-rw-r--r--spec/ruby/library/uri/generic/user_spec.rb10
-rw-r--r--spec/ruby/library/uri/generic/userinfo_spec.rb10
-rw-r--r--spec/ruby/library/uri/http/build_spec.rb6
-rw-r--r--spec/ruby/library/uri/http/request_uri_spec.rb16
-rw-r--r--spec/ruby/library/uri/join_spec.rb59
-rw-r--r--spec/ruby/library/uri/ldap/attributes_spec.rb10
-rw-r--r--spec/ruby/library/uri/ldap/build_spec.rb6
-rw-r--r--spec/ruby/library/uri/ldap/dn_spec.rb10
-rw-r--r--spec/ruby/library/uri/ldap/extensions_spec.rb10
-rw-r--r--spec/ruby/library/uri/ldap/filter_spec.rb10
-rw-r--r--spec/ruby/library/uri/ldap/hierarchical_spec.rb6
-rw-r--r--spec/ruby/library/uri/ldap/scope_spec.rb10
-rw-r--r--spec/ruby/library/uri/ldap/set_attributes_spec.rb6
-rw-r--r--spec/ruby/library/uri/ldap/set_dn_spec.rb6
-rw-r--r--spec/ruby/library/uri/ldap/set_extensions_spec.rb6
-rw-r--r--spec/ruby/library/uri/ldap/set_filter_spec.rb6
-rw-r--r--spec/ruby/library/uri/ldap/set_scope_spec.rb6
-rw-r--r--spec/ruby/library/uri/mailto/build_spec.rb92
-rw-r--r--spec/ruby/library/uri/mailto/headers_spec.rb10
-rw-r--r--spec/ruby/library/uri/mailto/set_headers_spec.rb6
-rw-r--r--spec/ruby/library/uri/mailto/set_to_spec.rb6
-rw-r--r--spec/ruby/library/uri/mailto/to_mailtext_spec.rb6
-rw-r--r--spec/ruby/library/uri/mailto/to_rfc822text_spec.rb6
-rw-r--r--spec/ruby/library/uri/mailto/to_s_spec.rb6
-rw-r--r--spec/ruby/library/uri/mailto/to_spec.rb10
-rw-r--r--spec/ruby/library/uri/merge_spec.rb20
-rw-r--r--spec/ruby/library/uri/normalize_spec.rb35
-rw-r--r--spec/ruby/library/uri/parse_spec.rb203
-rw-r--r--spec/ruby/library/uri/parser/escape_spec.rb6
-rw-r--r--spec/ruby/library/uri/parser/extract_spec.rb7
-rw-r--r--spec/ruby/library/uri/parser/inspect_spec.rb6
-rw-r--r--spec/ruby/library/uri/parser/join_spec.rb7
-rw-r--r--spec/ruby/library/uri/parser/make_regexp_spec.rb6
-rw-r--r--spec/ruby/library/uri/parser/parse_spec.rb7
-rw-r--r--spec/ruby/library/uri/parser/split_spec.rb6
-rw-r--r--spec/ruby/library/uri/parser/unescape_spec.rb6
-rw-r--r--spec/ruby/library/uri/plus_spec.rb459
-rw-r--r--spec/ruby/library/uri/regexp_spec.rb18
-rw-r--r--spec/ruby/library/uri/route_from_spec.rb23
-rw-r--r--spec/ruby/library/uri/route_to_spec.rb26
-rw-r--r--spec/ruby/library/uri/select_spec.rb27
-rw-r--r--spec/ruby/library/uri/set_component_spec.rb47
-rw-r--r--spec/ruby/library/uri/shared/eql.rb17
-rw-r--r--spec/ruby/library/uri/shared/extract.rb83
-rw-r--r--spec/ruby/library/uri/shared/join.rb56
-rw-r--r--spec/ruby/library/uri/shared/parse.rb206
-rw-r--r--spec/ruby/library/uri/split_spec.rb6
-rw-r--r--spec/ruby/library/uri/uri_spec.rb29
-rw-r--r--spec/ruby/library/uri/util/make_components_hash_spec.rb6
-rw-r--r--spec/ruby/library/weakref/__getobj___spec.rb17
-rw-r--r--spec/ruby/library/weakref/allocate_spec.rb8
-rw-r--r--spec/ruby/library/weakref/fixtures/classes.rb26
-rw-r--r--spec/ruby/library/weakref/new_spec.rb13
-rw-r--r--spec/ruby/library/weakref/send_spec.rb37
-rw-r--r--spec/ruby/library/weakref/weakref_alive_spec.rb15
-rw-r--r--spec/ruby/library/win32ole/fixtures/classes.rb33
-rw-r--r--spec/ruby/library/win32ole/fixtures/event.xml4
-rw-r--r--spec/ruby/library/win32ole/win32ole/_getproperty_spec.rb15
-rw-r--r--spec/ruby/library/win32ole/win32ole/_invoke_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole/codepage_spec.rb14
-rw-r--r--spec/ruby/library/win32ole/win32ole/connect_spec.rb16
-rw-r--r--spec/ruby/library/win32ole/win32ole/const_load_spec.rb33
-rw-r--r--spec/ruby/library/win32ole/win32ole/constants_spec.rb43
-rw-r--r--spec/ruby/library/win32ole/win32ole/create_guid_spec.rb10
-rw-r--r--spec/ruby/library/win32ole/win32ole/invoke_spec.rb15
-rw-r--r--spec/ruby/library/win32ole/win32ole/locale_spec.rb30
-rw-r--r--spec/ruby/library/win32ole/win32ole/new_spec.rb26
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_func_methods_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_get_methods_spec.rb17
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_method_help_spec.rb11
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_method_spec.rb11
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_methods_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_obj_help_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole/ole_put_methods_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole/setproperty_spec.rb11
-rw-r--r--spec/ruby/library/win32ole/win32ole/shared/ole_method.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole/shared/setproperty.rb23
-rw-r--r--spec/ruby/library/win32ole/win32ole_event/new_spec.rb34
-rw-r--r--spec/ruby/library/win32ole/win32ole_event/on_event_spec.rb71
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/dispid_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/event_interface_spec.rb29
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/event_spec.rb23
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/helpcontext_spec.rb27
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/helpfile_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/helpstring_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/invkind_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/invoke_kind_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/name_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/new_spec.rb34
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/offset_vtbl_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/params_spec.rb29
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/return_type_detail_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/return_type_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/return_vtype_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/shared/name.rb20
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/size_opt_params_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/size_params_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/to_s_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_method/visible_spec.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/default_spec.rb32
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/input_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/name_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/ole_type_detail_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/ole_type_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/optional_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/retval_spec.rb22
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/shared/name.rb21
-rw-r--r--spec/ruby/library/win32ole/win32ole_param/to_s_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/guid_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/helpcontext_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/helpfile_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/helpstring_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/major_version_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/minor_version_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/name_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/new_spec.rb41
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/ole_classes_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/ole_methods_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/ole_type_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/progid_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/progids_spec.rb15
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/shared/name.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/src_type_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/to_s_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/typekind_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/typelibs_spec.rb23
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/variables_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_type/visible_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/name_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/ole_type_detail_spec.rb20
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/ole_type_spec.rb19
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/shared/name.rb18
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/to_s_spec.rb12
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/value_spec.rb20
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/variable_kind_spec.rb20
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/varkind_spec.rb20
-rw-r--r--spec/ruby/library/win32ole/win32ole_variable/visible_spec.rb19
-rw-r--r--spec/ruby/library/yaml/dump_spec.rb64
-rw-r--r--spec/ruby/library/yaml/dump_stream_spec.rb9
-rw-r--r--spec/ruby/library/yaml/fixtures/example_class.rb7
-rw-r--r--spec/ruby/library/yaml/fixtures/strings.rb26
-rw-r--r--spec/ruby/library/yaml/fixtures/test_yaml.yml2
-rw-r--r--spec/ruby/library/yaml/load_file_spec.rb18
-rw-r--r--spec/ruby/library/yaml/load_spec.rb10
-rw-r--r--spec/ruby/library/yaml/load_stream_spec.rb9
-rw-r--r--spec/ruby/library/yaml/parse_file_spec.rb10
-rw-r--r--spec/ruby/library/yaml/parse_spec.rb23
-rw-r--r--spec/ruby/library/yaml/shared/each_document.rb19
-rw-r--r--spec/ruby/library/yaml/shared/load.rb142
-rw-r--r--spec/ruby/library/yaml/to_yaml_spec.rb114
-rw-r--r--spec/ruby/library/yaml/unsafe_load_spec.rb9
-rw-r--r--spec/ruby/library/zlib/adler32_spec.rb46
-rw-r--r--spec/ruby/library/zlib/crc32_spec.rb54
-rw-r--r--spec/ruby/library/zlib/crc_table_spec.rb80
-rw-r--r--spec/ruby/library/zlib/deflate/deflate_spec.rb133
-rw-r--r--spec/ruby/library/zlib/deflate/params_spec.rb17
-rw-r--r--spec/ruby/library/zlib/deflate/set_dictionary_spec.rb14
-rw-r--r--spec/ruby/library/zlib/deflate_spec.rb8
-rw-r--r--spec/ruby/library/zlib/gunzip_spec.rb14
-rw-r--r--spec/ruby/library/zlib/gzip_spec.rb15
-rw-r--r--spec/ruby/library/zlib/gzipfile/close_spec.rb19
-rw-r--r--spec/ruby/library/zlib/gzipfile/closed_spec.rb16
-rw-r--r--spec/ruby/library/zlib/gzipfile/comment_spec.rb25
-rw-r--r--spec/ruby/library/zlib/gzipfile/orig_name_spec.rb25
-rw-r--r--spec/ruby/library/zlib/gzipreader/each_byte_spec.rb51
-rw-r--r--spec/ruby/library/zlib/gzipreader/each_char_spec.rb51
-rw-r--r--spec/ruby/library/zlib/gzipreader/each_line_spec.rb6
-rw-r--r--spec/ruby/library/zlib/gzipreader/each_spec.rb6
-rw-r--r--spec/ruby/library/zlib/gzipreader/eof_spec.rb54
-rw-r--r--spec/ruby/library/zlib/gzipreader/getc_spec.rb39
-rw-r--r--spec/ruby/library/zlib/gzipreader/gets_spec.rb22
-rw-r--r--spec/ruby/library/zlib/gzipreader/mtime_spec.rb11
-rw-r--r--spec/ruby/library/zlib/gzipreader/pos_spec.rb24
-rw-r--r--spec/ruby/library/zlib/gzipreader/read_spec.rb66
-rw-r--r--spec/ruby/library/zlib/gzipreader/readpartial_spec.rb17
-rw-r--r--spec/ruby/library/zlib/gzipreader/rewind_spec.rb47
-rw-r--r--spec/ruby/library/zlib/gzipreader/shared/each.rb49
-rw-r--r--spec/ruby/library/zlib/gzipreader/ungetbyte_spec.rb120
-rw-r--r--spec/ruby/library/zlib/gzipreader/ungetc_spec.rb284
-rw-r--r--spec/ruby/library/zlib/gzipwriter/append_spec.rb15
-rw-r--r--spec/ruby/library/zlib/gzipwriter/mtime_spec.rb37
-rw-r--r--spec/ruby/library/zlib/gzipwriter/write_spec.rb36
-rw-r--r--spec/ruby/library/zlib/inflate/append_spec.rb60
-rw-r--r--spec/ruby/library/zlib/inflate/finish_spec.rb29
-rw-r--r--spec/ruby/library/zlib/inflate/inflate_spec.rb159
-rw-r--r--spec/ruby/library/zlib/inflate/set_dictionary_spec.rb20
-rw-r--r--spec/ruby/library/zlib/inflate_spec.rb8
-rw-r--r--spec/ruby/library/zlib/zlib_version_spec.rb8
-rw-r--r--spec/ruby/library/zlib/zstream/adler_spec.rb11
-rw-r--r--spec/ruby/library/zlib/zstream/avail_in_spec.rb9
-rw-r--r--spec/ruby/library/zlib/zstream/avail_out_spec.rb9
-rw-r--r--spec/ruby/library/zlib/zstream/data_type_spec.rb9
-rw-r--r--spec/ruby/library/zlib/zstream/flush_next_out_spec.rb14
-rw-r--r--spec/ruby/optional/capi/README13
-rw-r--r--spec/ruby/optional/capi/array_spec.rb533
-rw-r--r--spec/ruby/optional/capi/basic_object_spec.rb24
-rw-r--r--spec/ruby/optional/capi/bignum_spec.rb226
-rw-r--r--spec/ruby/optional/capi/binding_spec.rb16
-rw-r--r--spec/ruby/optional/capi/boolean_spec.rb33
-rw-r--r--spec/ruby/optional/capi/class_spec.rb506
-rw-r--r--spec/ruby/optional/capi/complex_spec.rb45
-rw-r--r--spec/ruby/optional/capi/constants_spec.rb325
-rw-r--r--spec/ruby/optional/capi/data_spec.rb53
-rw-r--r--spec/ruby/optional/capi/debug_spec.rb74
-rw-r--r--spec/ruby/optional/capi/digest_spec.rb103
-rw-r--r--spec/ruby/optional/capi/encoding_spec.rb817
-rw-r--r--spec/ruby/optional/capi/enumerator_spec.rb66
-rw-r--r--spec/ruby/optional/capi/exception_spec.rb201
-rw-r--r--spec/ruby/optional/capi/ext/.gitignore9
-rw-r--r--spec/ruby/optional/capi/ext/array_spec.c333
-rw-r--r--spec/ruby/optional/capi/ext/basic_object_spec.c19
-rw-r--r--spec/ruby/optional/capi/ext/bignum_spec.c106
-rw-r--r--spec/ruby/optional/capi/ext/binding_spec.c19
-rw-r--r--spec/ruby/optional/capi/ext/boolean_spec.c33
-rw-r--r--spec/ruby/optional/capi/ext/class_id_under_autoload_spec.c13
-rw-r--r--spec/ruby/optional/capi/ext/class_spec.c180
-rw-r--r--spec/ruby/optional/capi/ext/class_under_autoload_spec.c13
-rw-r--r--spec/ruby/optional/capi/ext/complex_spec.c45
-rw-r--r--spec/ruby/optional/capi/ext/constants_spec.c172
-rw-r--r--spec/ruby/optional/capi/ext/data_spec.c93
-rw-r--r--spec/ruby/optional/capi/ext/debug_spec.c93
-rw-r--r--spec/ruby/optional/capi/ext/digest_spec.c168
-rw-r--r--spec/ruby/optional/capi/ext/encoding_spec.c402
-rw-r--r--spec/ruby/optional/capi/ext/enumerator_spec.c32
-rw-r--r--spec/ruby/optional/capi/ext/exception_spec.c84
-rw-r--r--spec/ruby/optional/capi/ext/fiber_spec.c64
-rw-r--r--spec/ruby/optional/capi/ext/file_spec.c29
-rw-r--r--spec/ruby/optional/capi/ext/finalizer_spec.c25
-rw-r--r--spec/ruby/optional/capi/ext/fixnum_spec.c26
-rw-r--r--spec/ruby/optional/capi/ext/float_spec.c47
-rw-r--r--spec/ruby/optional/capi/ext/gc_spec.c178
-rw-r--r--spec/ruby/optional/capi/ext/globals_spec.c161
-rw-r--r--spec/ruby/optional/capi/ext/hash_spec.c180
-rw-r--r--spec/ruby/optional/capi/ext/integer_spec.c40
-rw-r--r--spec/ruby/optional/capi/ext/io_spec.c416
-rw-r--r--spec/ruby/optional/capi/ext/kernel_spec.c463
-rw-r--r--spec/ruby/optional/capi/ext/language_spec.c42
-rw-r--r--spec/ruby/optional/capi/ext/marshal_spec.c24
-rw-r--r--spec/ruby/optional/capi/ext/module_spec.c176
-rw-r--r--spec/ruby/optional/capi/ext/module_under_autoload_spec.c15
-rw-r--r--spec/ruby/optional/capi/ext/mutex_spec.c76
-rw-r--r--spec/ruby/optional/capi/ext/numeric_spec.c130
-rw-r--r--spec/ruby/optional/capi/ext/object_spec.c475
-rw-r--r--spec/ruby/optional/capi/ext/proc_spec.c147
-rw-r--r--spec/ruby/optional/capi/ext/range_spec.c90
-rw-r--r--spec/ruby/optional/capi/ext/rational_spec.c54
-rw-r--r--spec/ruby/optional/capi/ext/rbasic_spec.c104
-rw-r--r--spec/ruby/optional/capi/ext/regexp_spec.c74
-rw-r--r--spec/ruby/optional/capi/ext/rubyspec.h50
-rw-r--r--spec/ruby/optional/capi/ext/set_spec.c65
-rw-r--r--spec/ruby/optional/capi/ext/st_spec.c83
-rw-r--r--spec/ruby/optional/capi/ext/string_spec.c737
-rw-r--r--spec/ruby/optional/capi/ext/struct_spec.c101
-rw-r--r--spec/ruby/optional/capi/ext/symbol_spec.c116
-rw-r--r--spec/ruby/optional/capi/ext/thread_spec.c195
-rw-r--r--spec/ruby/optional/capi/ext/time_spec.c81
-rw-r--r--spec/ruby/optional/capi/ext/tracepoint_spec.c49
-rw-r--r--spec/ruby/optional/capi/ext/typed_data_spec.c210
-rw-r--r--spec/ruby/optional/capi/ext/util_spec.c117
-rw-r--r--spec/ruby/optional/capi/fiber_spec.rb86
-rw-r--r--spec/ruby/optional/capi/file_spec.rb89
-rw-r--r--spec/ruby/optional/capi/finalizer_spec.rb40
-rw-r--r--spec/ruby/optional/capi/fixnum_spec.rb101
-rw-r--r--spec/ruby/optional/capi/fixtures/class.rb104
-rw-r--r--spec/ruby/optional/capi/fixtures/const_get.rb5
-rw-r--r--spec/ruby/optional/capi/fixtures/const_get_at.rb5
-rw-r--r--spec/ruby/optional/capi/fixtures/const_get_from.rb5
-rw-r--r--spec/ruby/optional/capi/fixtures/const_get_object.rb3
-rw-r--r--spec/ruby/optional/capi/fixtures/encoding.rb3
-rw-r--r--spec/ruby/optional/capi/fixtures/foo.rb1
-rw-r--r--spec/ruby/optional/capi/fixtures/kernel.rb19
-rw-r--r--spec/ruby/optional/capi/fixtures/module.rb39
-rw-r--r--spec/ruby/optional/capi/fixtures/module_autoload.rb4
-rw-r--r--spec/ruby/optional/capi/fixtures/object.rb29
-rw-r--r--spec/ruby/optional/capi/fixtures/path_to_class.rb6
-rw-r--r--spec/ruby/optional/capi/fixtures/proc.rb20
-rw-r--r--spec/ruby/optional/capi/fixtures/read.txt1
-rw-r--r--spec/ruby/optional/capi/float_spec.rb43
-rw-r--r--spec/ruby/optional/capi/gc_spec.rb154
-rw-r--r--spec/ruby/optional/capi/globals_spec.rb298
-rw-r--r--spec/ruby/optional/capi/hash_spec.rb343
-rw-r--r--spec/ruby/optional/capi/integer_spec.rb307
-rw-r--r--spec/ruby/optional/capi/io_spec.rb788
-rw-r--r--spec/ruby/optional/capi/kernel_spec.rb929
-rw-r--r--spec/ruby/optional/capi/language_spec.rb37
-rw-r--r--spec/ruby/optional/capi/marshal_spec.rb46
-rw-r--r--spec/ruby/optional/capi/module_spec.rb435
-rw-r--r--spec/ruby/optional/capi/mutex_spec.rb102
-rw-r--r--spec/ruby/optional/capi/numeric_spec.rb495
-rw-r--r--spec/ruby/optional/capi/object_spec.rb1021
-rw-r--r--spec/ruby/optional/capi/proc_spec.rb188
-rw-r--r--spec/ruby/optional/capi/rake_helper.rb22
-rw-r--r--spec/ruby/optional/capi/range_spec.rb231
-rw-r--r--spec/ruby/optional/capi/rational_spec.rb57
-rw-r--r--spec/ruby/optional/capi/rbasic_spec.rb48
-rw-r--r--spec/ruby/optional/capi/regexp_spec.rb128
-rw-r--r--spec/ruby/optional/capi/set_spec.rb96
-rw-r--r--spec/ruby/optional/capi/shared/rbasic.rb27
-rw-r--r--spec/ruby/optional/capi/spec_helper.rb172
-rw-r--r--spec/ruby/optional/capi/st_spec.rb41
-rw-r--r--spec/ruby/optional/capi/string_spec.rb1558
-rw-r--r--spec/ruby/optional/capi/struct_spec.rb314
-rw-r--r--spec/ruby/optional/capi/symbol_spec.rb180
-rw-r--r--spec/ruby/optional/capi/thread_spec.rb195
-rw-r--r--spec/ruby/optional/capi/time_spec.rb301
-rw-r--r--spec/ruby/optional/capi/tracepoint_spec.rb56
-rw-r--r--spec/ruby/optional/capi/typed_data_spec.rb102
-rw-r--r--spec/ruby/optional/capi/util_spec.rb302
-rw-r--r--spec/ruby/optional/thread_safety/fixtures/classes.rb39
-rw-r--r--spec/ruby/optional/thread_safety/hash_spec.rb210
-rw-r--r--spec/ruby/security/cve_2010_1330_spec.rb19
-rw-r--r--spec/ruby/security/cve_2011_4815_spec.rb47
-rw-r--r--spec/ruby/security/cve_2013_4164_spec.rb15
-rw-r--r--spec/ruby/security/cve_2018_16396_spec.rb7
-rw-r--r--spec/ruby/security/cve_2018_6914_spec.rb55
-rw-r--r--spec/ruby/security/cve_2018_8778_spec.rb10
-rw-r--r--spec/ruby/security/cve_2018_8779_spec.rb30
-rw-r--r--spec/ruby/security/cve_2018_8780_spec.rb43
-rw-r--r--spec/ruby/security/cve_2019_8321_spec.rb20
-rw-r--r--spec/ruby/security/cve_2019_8322_spec.rb24
-rw-r--r--spec/ruby/security/cve_2019_8323_spec.rb46
-rw-r--r--spec/ruby/security/cve_2019_8325_spec.rb46
-rw-r--r--spec/ruby/security/cve_2020_10663_spec.rb49
-rw-r--r--spec/ruby/security/cve_2024_49761_spec.rb7
-rw-r--r--spec/ruby/shared/basicobject/method_missing.rb124
-rw-r--r--spec/ruby/shared/basicobject/send.rb128
-rw-r--r--spec/ruby/shared/enumerable/minmax.rb24
-rw-r--r--spec/ruby/shared/file/blockdev.rb9
-rw-r--r--spec/ruby/shared/file/chardev.rb9
-rw-r--r--spec/ruby/shared/file/directory.rb66
-rw-r--r--spec/ruby/shared/file/executable.rb83
-rw-r--r--spec/ruby/shared/file/executable_real.rb81
-rw-r--r--spec/ruby/shared/file/exist.rb19
-rw-r--r--spec/ruby/shared/file/file.rb45
-rw-r--r--spec/ruby/shared/file/grpowned.rb39
-rw-r--r--spec/ruby/shared/file/identical.rb51
-rw-r--r--spec/ruby/shared/file/owned.rb3
-rw-r--r--spec/ruby/shared/file/pipe.rb3
-rw-r--r--spec/ruby/shared/file/readable.rb49
-rw-r--r--spec/ruby/shared/file/readable_real.rb39
-rw-r--r--spec/ruby/shared/file/setgid.rb2
-rw-r--r--spec/ruby/shared/file/setuid.rb2
-rw-r--r--spec/ruby/shared/file/size.rb124
-rw-r--r--spec/ruby/shared/file/socket.rb33
-rw-r--r--spec/ruby/shared/file/sticky.rb29
-rw-r--r--spec/ruby/shared/file/symlink.rb46
-rw-r--r--spec/ruby/shared/file/world_readable.rb49
-rw-r--r--spec/ruby/shared/file/world_writable.rb49
-rw-r--r--spec/ruby/shared/file/writable.rb44
-rw-r--r--spec/ruby/shared/file/writable_real.rb49
-rw-r--r--spec/ruby/shared/file/zero.rb68
-rw-r--r--spec/ruby/shared/hash/key_error.rb23
-rw-r--r--spec/ruby/shared/io/putc.rb57
-rw-r--r--spec/ruby/shared/kernel/at_exit.rb73
-rw-r--r--spec/ruby/shared/kernel/complex.rb133
-rw-r--r--spec/ruby/shared/kernel/equal.rb54
-rw-r--r--spec/ruby/shared/kernel/fixtures/END.rb3
-rw-r--r--spec/ruby/shared/kernel/fixtures/at_exit.rb3
-rw-r--r--spec/ruby/shared/kernel/object_id.rb100
-rw-r--r--spec/ruby/shared/kernel/raise.rb392
-rw-r--r--spec/ruby/shared/process/abort.rb36
-rw-r--r--spec/ruby/shared/process/exit.rb126
-rw-r--r--spec/ruby/shared/process/fork.rb93
-rw-r--r--spec/ruby/shared/queue/clear.rb12
-rw-r--r--spec/ruby/shared/queue/close.rb14
-rw-r--r--spec/ruby/shared/queue/closed.rb12
-rw-r--r--spec/ruby/shared/queue/deque.rb164
-rw-r--r--spec/ruby/shared/queue/empty.rb12
-rw-r--r--spec/ruby/shared/queue/enque.rb18
-rw-r--r--spec/ruby/shared/queue/freeze.rb8
-rw-r--r--spec/ruby/shared/queue/length.rb9
-rw-r--r--spec/ruby/shared/queue/num_waiting.rb16
-rw-r--r--spec/ruby/shared/sizedqueue/enque.rb129
-rw-r--r--spec/ruby/shared/sizedqueue/max.rb47
-rw-r--r--spec/ruby/shared/sizedqueue/new.rb23
-rw-r--r--spec/ruby/shared/sizedqueue/num_waiting.rb12
-rw-r--r--spec/ruby/shared/string/end_with.rb61
-rw-r--r--spec/ruby/shared/string/start_with.rb76
-rw-r--r--spec/ruby/shared/string/times.rb58
-rw-r--r--spec/ruby/shared/time/strftime_for_date.rb273
-rw-r--r--spec/ruby/shared/time/strftime_for_time.rb181
-rw-r--r--spec/ruby/shared/time/yday.rb18
-rw-r--r--spec/ruby/shared/types/rb_num2dbl_fails.rb17
-rw-r--r--spec/ruby/spec_helper.rb38
-rw-r--r--spec/syntax_suggest/fixtures/derailed_require_tree.rb.txt74
-rwxr-xr-xspec/syntax_suggest/fixtures/rexe.rb.txt569
-rw-r--r--spec/syntax_suggest/fixtures/routes.rb.txt121
-rw-r--r--spec/syntax_suggest/fixtures/ruby_buildpack.rb.txt1344
-rw-r--r--spec/syntax_suggest/fixtures/syntax_tree.rb.txt9234
-rw-r--r--spec/syntax_suggest/fixtures/this_project_extra_def.rb.txt64
-rw-r--r--spec/syntax_suggest/fixtures/webmock.rb.txt35
-rw-r--r--spec/syntax_suggest/integration/exe_cli_spec.rb27
-rw-r--r--spec/syntax_suggest/integration/ruby_command_line_spec.rb189
-rw-r--r--spec/syntax_suggest/integration/syntax_suggest_spec.rb260
-rw-r--r--spec/syntax_suggest/spec_helper.rb117
-rw-r--r--spec/syntax_suggest/unit/api_spec.rb104
-rw-r--r--spec/syntax_suggest/unit/around_block_scan_spec.rb165
-rw-r--r--spec/syntax_suggest/unit/block_expand_spec.rb230
-rw-r--r--spec/syntax_suggest/unit/capture/before_after_keyword_ends_spec.rb47
-rw-r--r--spec/syntax_suggest/unit/capture/falling_indent_lines_spec.rb44
-rw-r--r--spec/syntax_suggest/unit/capture_code_context_spec.rb229
-rw-r--r--spec/syntax_suggest/unit/clean_document_spec.rb260
-rw-r--r--spec/syntax_suggest/unit/cli_spec.rb224
-rw-r--r--spec/syntax_suggest/unit/code_block_spec.rb77
-rw-r--r--spec/syntax_suggest/unit/code_frontier_spec.rb135
-rw-r--r--spec/syntax_suggest/unit/code_line_spec.rb164
-rw-r--r--spec/syntax_suggest/unit/code_search_spec.rb505
-rw-r--r--spec/syntax_suggest/unit/core_ext_spec.rb32
-rw-r--r--spec/syntax_suggest/unit/display_invalid_blocks_spec.rb174
-rw-r--r--spec/syntax_suggest/unit/explain_syntax_spec.rb283
-rw-r--r--spec/syntax_suggest/unit/mini_stringio_spec.rb25
-rw-r--r--spec/syntax_suggest/unit/pathname_from_message_spec.rb65
-rw-r--r--spec/syntax_suggest/unit/priority_queue_spec.rb95
-rw-r--r--spec/syntax_suggest/unit/scan_history_spec.rb114
-rw-r--r--spec/syntax_suggest/unit/visitor_spec.rb119
5294 files changed, 369626 insertions, 0 deletions
diff --git a/spec/README.md b/spec/README.md
new file mode 100644
index 0000000000..6a88c06e09
--- /dev/null
+++ b/spec/README.md
@@ -0,0 +1,160 @@
+# spec/bundler
+
+spec/bundler is rspec examples for bundler library (`lib/bundler.rb`, `lib/bundler/*`).
+
+## Running spec/bundler
+
+To run rspec for bundler:
+
+```bash
+make test-bundler
+```
+
+or run rspec with parallel execution:
+
+```bash
+make test-bundler-parallel
+```
+
+If you specify `BUNDLER_SPECS=foo/bar_spec.rb` then only `spec/bundler/foo/bar_spec.rb` will be run.
+
+# spec/ruby
+
+ruby/spec (https://github.com/ruby/spec/) is
+a test suite for the Ruby language.
+
+Once a month, @eregon merges the in-tree copy under spec/ruby
+with the upstream repository, preserving the commits and history.
+The same happens for other implementations such as JRuby and TruffleRuby.
+
+Feel welcome to modify the in-tree spec/ruby.
+This is the purpose of the in-tree copy,
+to facilitate contributions to ruby/spec for MRI developers.
+
+New features, additional tests for existing features and
+regressions tests are all welcome in ruby/spec.
+There is very little behavior that is implementation-specific,
+as in the end user programs tend to rely on every behavior MRI exhibits.
+In other words: If adding a spec might reveal a bug in
+another implementation, then it is worth adding it.
+Currently, the only module which is MRI-specific is `RubyVM`.
+
+## Changing behavior and versions guards
+
+Version guards (`ruby_version_is`) must be added for new features or features
+which change behavior or are removed. This is necessary for other Ruby implementations
+to still be able to run the specs and contribute new specs.
+
+For example, change:
+
+```ruby
+describe "Some spec" do
+ it "some example" do
+ # Old behavior for Ruby < 2.7
+ end
+end
+```
+
+to:
+
+```ruby
+describe "Some spec" do
+ ruby_version_is ""..."2.7" do
+ it "some example" do
+ # Old behavior for Ruby < 2.7
+ end
+ end
+
+ ruby_version_is "2.7" do
+ it "some example" do
+ # New behavior for Ruby >= 2.7
+ end
+ end
+end
+```
+
+See `spec/ruby/CONTRIBUTING.md` for more documentation about guards.
+
+To verify specs are compatible with older Ruby versions:
+
+```bash
+cd spec/ruby
+$RUBY_MANAGER use 2.4.9
+../mspec/bin/mspec -j
+```
+
+## Running ruby/spec
+
+To run all specs:
+
+```bash
+make test-spec
+```
+
+Extra arguments can be added via `SPECOPTS`.
+For instance, to show the help:
+
+```bash
+make test-spec SPECOPTS=-h
+```
+
+You can also run the specs in parallel, which is currently experimental.
+It takes around 10s instead of 60s on a quad-core laptop.
+
+```bash
+make test-spec SPECOPTS=-j
+```
+
+To run a specific test, add its path to the command:
+
+```bash
+make test-spec SPECOPTS=spec/ruby/language/for_spec.rb
+```
+
+If ruby trunk is your current `ruby` in `$PATH`, you can also run `mspec` directly:
+
+```bash
+# change ruby to trunk
+ruby -v # => trunk
+spec/mspec/bin/mspec spec/ruby/language/for_spec.rb
+```
+
+## ruby/spec and test/
+
+The main difference between a "spec" under `spec/ruby/` and
+a test under `test/` is that specs are documenting what they test.
+This is extremely valuable when reading these tests, as it
+helps to quickly understand what specific behavior is tested,
+and how a method should behave. Basic English is fine for spec descriptions.
+Specs also tend to have few expectations (assertions) per spec,
+as they specify one aspect of the behavior and not everything at once.
+Beyond that, the syntax is slightly different but it does the same thing:
+`assert_equal 3, 1+2` is just `(1+2).should == 3`.
+
+Example:
+
+```ruby
+describe "The for expression" do
+ it "iterates over an Enumerable passing each element to the block" do
+ j = 0
+ for i in 1..3
+ j += i
+ end
+ j.should == 6
+ end
+end
+```
+
+For more details, see `spec/ruby/CONTRIBUTING.md`.
+
+# spec/syntax_suggest
+
+## Running spec/syntax_suggest
+
+To run rspec for syntax_suggest:
+
+```bash
+make test-syntax-suggest
+```
+
+If you specify `SYNTAX_SUGGEST_SPECS=foo/bar_spec.rb` then only `spec/syntax_suggest/foo/bar_spec.rb` will be run.
diff --git a/spec/bin/bundle b/spec/bin/bundle
new file mode 100755
index 0000000000..8f8b535295
--- /dev/null
+++ b/spec/bin/bundle
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../bundler/support/activate"
+
+load File.expand_path("bundle", Spec::Path.exedir)
diff --git a/spec/bin/parallel_rspec b/spec/bin/parallel_rspec
new file mode 100755
index 0000000000..412defa58b
--- /dev/null
+++ b/spec/bin/parallel_rspec
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../bundler/support/setup"
+
+require "turbo_tests"
+TurboTests::CLI.new(ARGV).run
diff --git a/spec/bin/rspec b/spec/bin/rspec
new file mode 100755
index 0000000000..a41aa4896a
--- /dev/null
+++ b/spec/bin/rspec
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../bundler/support/switch_rubygems"
+require_relative "../bundler/support/rubygems_ext"
+
+Spec::Rubygems.gem_load("rspec-core", "rspec")
diff --git a/spec/bundled_gems.mspec b/spec/bundled_gems.mspec
new file mode 100644
index 0000000000..f7edb34c75
--- /dev/null
+++ b/spec/bundled_gems.mspec
@@ -0,0 +1,14 @@
+load File.dirname(__FILE__) + '/default.mspec'
+
+class MSpecScript
+ test_bundled_gems = get(:stdlibs).to_a & get(:bundled_gems).to_a
+ unless ENV["BUNDLED_GEMS"].nil? || ENV["BUNDLED_GEMS"].empty?
+ test_bundled_gems = ENV["BUNDLED_GEMS"].split(",").map do |gem|
+ gem = "openstruct" if gem == "ostruct"
+ test_bundled_gems.find{|test_gem| test_gem.include?(gem) }
+ end.compact
+ exit if test_bundled_gems.empty?
+ end
+ set :library, test_bundled_gems
+ set :files, get(:library)
+end
diff --git a/spec/bundled_gems_spec.rb b/spec/bundled_gems_spec.rb
new file mode 100644
index 0000000000..9af06dd181
--- /dev/null
+++ b/spec/bundled_gems_spec.rb
@@ -0,0 +1,422 @@
+require "bundled_gems"
+
+require "bundler"
+require "fileutils"
+
+require_relative "bundler/support/builders"
+require_relative "bundler/support/helpers"
+require_relative "bundler/support/path"
+
+module Gem
+ def self.ruby=(ruby)
+ @ruby = ruby
+ end
+end
+
+RSpec.configure do |config|
+ config.include Spec::Builders
+ config.include Spec::Helpers
+ config.include Spec::Path
+
+ config.before(:suite) do
+ Gem.ruby = ENV["RUBY"] if ENV["RUBY"]
+
+ require_relative "bundler/support/rubygems_ext"
+ Spec::Helpers.install_dev_bundler
+ FileUtils.mkdir_p Spec::Path.gem_path
+ end
+
+ config.around(:each) do |example|
+ FileUtils.cp_r Spec::Path.pristine_system_gem_path, Spec::Path.system_gem_path
+
+ with_gem_path_as(system_gem_path) do
+ Bundler.ui.silence { example.run }
+
+ all_output = all_commands_output
+ if example.exception && !all_output.empty?
+ message = all_output + "\n" + example.exception.message
+ (class << example.exception; self; end).send(:define_method, :message) do
+ message
+ end
+ end
+ end
+ ensure
+ reset!
+ end
+
+ config.after :suite do
+ FileUtils.rm_rf Spec::Path.pristine_system_gem_path
+ end
+end
+
+RSpec.describe "bundled_gems.rb" do
+ let(:stub_code) {
+ source_lib_dir = File.realpath(Spec::Path.source_lib_dir.to_s)
+ <<~STUB
+ Gem::BUNDLED_GEMS.send(:remove_const, :LIBDIR)
+ Gem::BUNDLED_GEMS.send(:remove_const, :ARCHDIR)
+ Gem::BUNDLED_GEMS.send(:remove_const, :SINCE)
+ Gem::BUNDLED_GEMS.const_set(:LIBDIR, "#{source_lib_dir}/")
+ Gem::BUNDLED_GEMS.const_set(:ARCHDIR, File.expand_path($LOAD_PATH.find{|path| path.include?(".ext/common") }) + "/")
+ Gem::BUNDLED_GEMS.const_set(:SINCE, { "openssl" => RUBY_VERSION, "fileutils" => RUBY_VERSION, "csv" => "3.4.0", "net-smtp" => "3.1.0" })
+ STUB
+ }
+
+ def script(code, options = {})
+ options[:artifice] ||= "compact_index"
+ code = <<~RUBY
+ #{stub_code}
+ require 'bundler/inline'
+
+ #{code}
+ RUBY
+ ruby(code, options)
+ end
+
+ it "Show warning require and LoadError" do
+ script <<-RUBY
+ gemfile do
+ source "https://rubygems.org"
+ end
+
+ begin
+ require "csv"
+ rescue LoadError
+ end
+ require "openssl"
+ RUBY
+
+ expect(err).to include(/csv used to be loaded from (.*) since Ruby 3.4.0/)
+ expect(err).to include(/-e:15/)
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/-e:18/)
+ end
+
+ it "Show warning when bundled gems called as dependency" do
+ build_lib "activesupport", "7.0.7.2" do |s|
+ s.write "lib/active_support/all.rb", "require 'openssl'"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "activesupport", "7.0.7.2"
+ end
+ end
+
+ require "active_support/all"
+ RUBY
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/lib\/active_support\/all\.rb:1/)
+ end
+
+ it "Show warning dash gem like net/smtp" do
+ script <<-RUBY
+ gemfile do
+ source "https://rubygems.org"
+ end
+
+ begin
+ require "net/smtp"
+ rescue LoadError
+ end
+ RUBY
+
+ expect(err).to include(/net\/smtp used to be loaded from (.*) since Ruby 3.1.0/)
+ expect(err).to include(/-e:15/)
+ expect(err).to include("You can add net-smtp")
+ end
+
+ it "Show warning sub-feature like openssl/bn" do
+ skip "This test is not working on Windows" if Gem.win_platform?
+
+ script <<-RUBY
+ gemfile do
+ source "https://rubygems.org"
+ end
+
+ require "openssl/bn"
+ RUBY
+
+ expect(err).to include(/openssl\/bn is found in openssl, (.*) part of the default gems since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/-e:14/)
+ end
+
+ it "Show warning when bundle exec with ruby and script" do
+ code = <<-RUBY
+ #{stub_code}
+ require "openssl"
+ RUBY
+ create_file("script.rb", code)
+ create_file("Gemfile", "source 'https://rubygems.org'")
+
+ bundle "exec ruby script.rb"
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/script\.rb:8/)
+ end
+
+ it "Show warning when bundle exec with shebang's script" do
+ skip "This test is not working on Windows" if Gem.win_platform?
+
+ code = <<-RUBY
+ #!/usr/bin/env ruby
+ #{stub_code}
+ require "openssl"
+ RUBY
+ create_file("script.rb", code)
+ FileUtils.chmod(0o777, bundled_app("script.rb"))
+ create_file("Gemfile", "source 'https://rubygems.org'")
+
+ bundle "exec ./script.rb"
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/script\.rb:9/)
+ end
+
+ it "Show warning when bundle exec with -r option" do
+ create_file("stub.rb", stub_code)
+ create_file("Gemfile", "source 'https://rubygems.org'")
+ bundle "exec ruby -r./stub -ropenssl -e ''"
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ end
+
+ it "Show warning when warn is not the standard one in the current scope" do
+ script <<-RUBY
+ module My
+ def warn(msg)
+ end
+
+ def my
+ gemfile do
+ source "https://rubygems.org"
+ end
+
+ require "openssl"
+ end
+
+ extend self
+ end
+
+ My.my
+ RUBY
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/-e:19/)
+ end
+
+ it "Don't show warning when bundled gems called as dependency" do
+ build_lib "activesupport", "7.0.7.2" do |s|
+ s.write "lib/active_support/all.rb", "require 'openssl'"
+ end
+ build_lib "openssl", "1.0.0" do |s|
+ s.write "lib/openssl.rb", "puts 'openssl'"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "activesupport", "7.0.7.2"
+ gem "openssl"
+ end
+ end
+
+ require "active_support/all"
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "Show warning with bootsnap cases" do
+ script <<-RUBY
+ gemfile do
+ source "https://rubygems.org"
+ # gem "bootsnap", require: false
+ end
+
+ # require 'bootsnap'
+ # Bootsnap.setup(cache_dir: 'tmp/cache')
+
+ # bootsnap expand required feature to full path
+ # require 'openssl'
+ require Gem::BUNDLED_GEMS::ARCHDIR + 'openssl'
+ RUBY
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ # TODO: We should assert caller location like below:
+ # test_warn_bootsnap.rb:14: warning: ...
+ end
+
+ it "Show warning with bootsnap for gem with native extension" do
+ script <<-RUBY
+ gemfile do
+ source "https://rubygems.org"
+ # gem "bootsnap", require: false
+ end
+
+ # require 'bootsnap'
+ # Bootsnap.setup(cache_dir: 'tmp/cache')
+
+ # bootsnap expand required feature to full path
+ # require 'openssl'
+ require Gem::BUNDLED_GEMS::ARCHDIR + "openssl"
+ RUBY
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ # TODO: We should assert caller location like below:
+ # test_warn_bootsnap_rubyarchdir_gem.rb:14: warning: ...
+ end
+
+ it "Show warning with bootsnap and some gem in Gemfile" do
+ # Original issue is childprocess 5.0.0 and logger.
+ build_lib "fileutils2", "5.0.0" do |s|
+ # bootsnap expand required feature to full path
+ rubylibpath = File.realpath(File.join(__dir__, "..", "lib"))
+ s.write "lib/fileutils2.rb", "require '#{rubylibpath}/fileutils'"
+ end
+
+ script <<-RUBY
+ gemfile do
+ source "https://rubygems.org"
+ # gem "bootsnap", require: false
+ path "#{lib_path}" do
+ gem "fileutils2", "5.0.0"
+ end
+ end
+
+ # require 'bootsnap'
+ # Bootsnap.setup(cache_dir: 'tmp/cache')
+
+ # bootsnap expand required feature to full path
+ require Gem.loaded_specs["fileutils2"].full_gem_path + '/lib/fileutils2'
+ RUBY
+
+ expect(err).to include(/fileutils used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ # TODO: We should assert caller location like below:
+ # $GEM_HOME/gems/childprocess-5.0.0/lib/childprocess.rb:7: warning:
+ end
+
+ it "Show warning with zeitwerk" do
+ libpath = Dir[File.expand_path("../.bundle/gems/{zeitwerk}-*/lib", __dir__)].map(&:to_s).first
+ code = <<-RUBY
+ #{stub_code}
+ $LOAD_PATH.unshift("#{libpath}")
+ require "zeitwerk"
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
+ loader.setup
+
+ require 'openssl'
+ RUBY
+ create_file("script.rb", code)
+ create_file("Gemfile", "source 'https://rubygems.org'")
+ bundle "exec ruby script.rb"
+
+ expect(err).to include(/openssl used to be loaded from (.*) since Ruby #{RUBY_VERSION}/)
+ expect(err).to include(/script\.rb:13/)
+ end
+
+ it "Don't show warning openssl/bn when openssl on Gemfile" do
+ build_lib "openssl", "1.0.0" do |s|
+ s.write "lib/openssl.rb", "puts 'openssl'"
+ s.write "lib/openssl/bn.rb", "puts 'openssl/bn'"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "openssl"
+ end
+ end
+
+ require "openssl/bn"
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "Don't show warning with net/smtp when net-smtp on Gemfile" do
+ build_lib "net-smtp", "1.0.0" do |s|
+ s.write "lib/net/smtp.rb", "puts 'net-smtp'"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "net-smtp"
+ end
+ end
+
+ require "net/smtp"
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ describe ".force_activate" do
+ before do
+ allow_any_instance_of(Bundler::Runtime).to receive(:setup).and_raise(Bundler::GemNotFound)
+ end
+
+ context "with bundle environment" do
+ before do
+ code = <<-RUBY
+ #!/usr/bin/env ruby
+
+ Gem::BUNDLED_GEMS.force_activate("csv")
+ RUBY
+ create_file("script.rb", code)
+ create_file("Gemfile", "source 'https://rubygems.org'")
+ end
+
+ it "lockfile is available" do
+ bundle "install"
+ bundle "exec ./script.rb"
+
+ expect(err).to include("gem install csv")
+ end
+
+ it "lockfile is not available" do
+ bundle "exec ./script.rb"
+
+ expect(err).to include("gem install csv")
+ end
+ end
+
+ context "with bundler/inline" do
+ it "foo is available on LOAD_PATH" do
+ build_lib "foo", "1.0.0" do |s|
+ s.write "lib/foo.rb", "puts :foo"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ #!/usr/bin/env ruby
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "foo", "1.0.0"
+ end
+ end
+
+ Gem::BUNDLED_GEMS.force_activate("csv")
+ puts $LOAD_PATH
+ RUBY
+
+ expect(err).to include("gem install csv")
+ expect(out).to include("foo-1.0.0/lib")
+ end
+ end
+
+ context "without bundle environment" do
+ it "warns about installation requirement" do
+ expect_any_instance_of(Object).to receive(:warn)
+ Gem::BUNDLED_GEMS.force_activate("csv")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/build_metadata_spec.rb b/spec/bundler/bundler/build_metadata_spec.rb
new file mode 100644
index 0000000000..2e69821f68
--- /dev/null
+++ b/spec/bundler/bundler/build_metadata_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "bundler"
+require "bundler/build_metadata"
+
+RSpec.describe Bundler::BuildMetadata do
+ before do
+ allow(Time).to receive(:now).and_return(Time.at(0))
+ Bundler::BuildMetadata.instance_variable_set(:@timestamp, nil)
+ end
+
+ describe "#timestamp" do
+ it "returns %Y-%m-%d formatted current time if built_at not set" do
+ Bundler::BuildMetadata.instance_variable_set(:@built_at, nil)
+ expect(Bundler::BuildMetadata.timestamp).to eq "1970-01-01"
+ end
+
+ it "returns %Y-%m-%d formatted current time if built_at not set" do
+ Bundler::BuildMetadata.instance_variable_set(:@built_at, "2025-01-01")
+ expect(Bundler::BuildMetadata.timestamp).to eq "2025-01-01"
+ ensure
+ Bundler::BuildMetadata.instance_variable_set(:@built_at, nil)
+ end
+ end
+
+ describe "#git_commit_sha" do
+ context "if instance valuable is defined" do
+ before do
+ Bundler::BuildMetadata.instance_variable_set(:@git_commit_sha, "foo")
+ end
+
+ after do
+ Bundler::BuildMetadata.remove_instance_variable(:@git_commit_sha)
+ end
+
+ it "returns set value" do
+ expect(Bundler::BuildMetadata.git_commit_sha).to eq "foo"
+ end
+ end
+ end
+
+ describe "#to_h" do
+ subject { Bundler::BuildMetadata.to_h }
+
+ it "returns a hash includes Timestamp, and Git SHA" do
+ expect(subject["Timestamp"]).to eq "1970-01-01"
+ expect(subject["Git SHA"]).to be_instance_of(String)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/bundler_spec.rb b/spec/bundler/bundler/bundler_spec.rb
new file mode 100644
index 0000000000..bddcbdaef3
--- /dev/null
+++ b/spec/bundler/bundler/bundler_spec.rb
@@ -0,0 +1,344 @@
+# frozen_string_literal: true
+
+require "bundler"
+require "tmpdir"
+
+RSpec.describe Bundler do
+ describe "#load_marshal" do
+ it "is a private method and raises an error" do
+ data = Marshal.dump(Bundler)
+ expect { Bundler.load_marshal(data) }.to raise_error(NoMethodError, /private method [`']load_marshal' called/)
+ end
+
+ it "loads any data" do
+ data = Marshal.dump(Bundler)
+ expect(Bundler.send(:load_marshal, data)).to eq(Bundler)
+ end
+ end
+
+ describe "#safe_load_marshal" do
+ it "fails on unexpected class" do
+ data = Marshal.dump(Bundler)
+ expect { Bundler.safe_load_marshal(data) }.to raise_error(Bundler::MarshalError)
+ end
+
+ it "loads simple structure" do
+ simple_structure = { "name" => [:development] }
+ data = Marshal.dump(simple_structure)
+ expect(Bundler.safe_load_marshal(data)).to eq(simple_structure)
+ end
+
+ it "loads Gem::Specification" do
+ gem_spec = Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = Gem::Version.new("2.4.7")
+ s.installed_by_version = Gem::Version.new("0")
+ s.authors = ["André Arko",
+ "Samuel Giddins",
+ "Colby Swandale",
+ "Hiroshi Shibata",
+ "David Rodríguez",
+ "Grey Baker",
+ "Stephanie Morillo",
+ "Chris Morris",
+ "James Wen",
+ "Tim Moore",
+ "André Medeiros",
+ "Jessica Lynn Suttles",
+ "Terence Lee",
+ "Carl Lerche",
+ "Yehuda Katz"]
+ s.date = Time.utc(2023, 2, 15)
+ s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably"
+ s.email = ["team@bundler.io"]
+ s.homepage = "https://bundler.io"
+ s.metadata = { "bug_tracker_uri" => "https://github.com/ruby/rubygems/issues?q=is%3Aopen+is%3Aissue+label%3ABundler",
+ "changelog_uri" => "https://github.com/ruby/rubygems/blob/master/bundler/CHANGELOG.md",
+ "homepage_uri" => "https://bundler.io/",
+ "source_code_uri" => "https://github.com/ruby/rubygems/tree/master/bundler" }
+ s.require_paths = ["lib"]
+ s.required_ruby_version = Gem::Requirement.new([">= 2.6.0"])
+ s.required_rubygems_version = Gem::Requirement.new([">= 3.0.1"])
+ s.rubygems_version = "3.4.7"
+ s.specification_version = 4
+ s.summary = "The best way to manage your application's dependencies"
+ s.license = false
+ end
+ data = Marshal.dump(gem_spec)
+ expect(Bundler.safe_load_marshal(data)).to eq(gem_spec)
+ end
+ end
+
+ describe "#load_gemspec_uncached" do
+ let(:app_gemspec_path) { tmp("test.gemspec") }
+ subject { Bundler.load_gemspec_uncached(app_gemspec_path) }
+
+ context "with incorrect YAML file" do
+ before do
+ File.open(app_gemspec_path, "wb") do |f|
+ f.write <<~GEMSPEC
+ ---
+ {:!00 ao=gu\g1= 7~f
+ GEMSPEC
+ end
+ end
+
+ it "catches YAML syntax errors" do
+ expect { subject }.to raise_error(Bundler::GemspecError, /error while loading `test.gemspec`/)
+ end
+ end
+
+ context "with correct YAML file", if: defined?(Encoding) do
+ it "can load a gemspec with unicode characters with default ruby encoding" do
+ # spec_helper forces the external encoding to UTF-8 but that's not the
+ # default until Ruby 2.0
+ verbose = $VERBOSE
+ $VERBOSE = false
+ encoding = Encoding.default_external
+ Encoding.default_external = "ASCII"
+ $VERBOSE = verbose
+
+ File.open(app_gemspec_path, "wb") do |file|
+ file.puts <<~GEMSPEC
+ # -*- encoding: utf-8 -*-
+ Gem::Specification.new do |gem|
+ gem.author = "André the Giant"
+ end
+ GEMSPEC
+ end
+
+ expect(subject.author).to eq("André the Giant")
+
+ verbose = $VERBOSE
+ $VERBOSE = false
+ Encoding.default_external = encoding
+ $VERBOSE = verbose
+ end
+ end
+
+ it "sets loaded_from" do
+ app_gemspec_path.open("w") do |f|
+ f.puts <<-GEMSPEC
+ Gem::Specification.new do |gem|
+ gem.name = "validated"
+ end
+ GEMSPEC
+ end
+
+ expect(subject.loaded_from).to eq(app_gemspec_path.expand_path.to_s)
+ end
+
+ context "validate is true" do
+ subject { Bundler.load_gemspec_uncached(app_gemspec_path, true) }
+
+ it "validates the specification" do
+ app_gemspec_path.open("w") do |f|
+ f.puts <<-GEMSPEC
+ Gem::Specification.new do |gem|
+ gem.name = "validated"
+ end
+ GEMSPEC
+ end
+ expect(Bundler.rubygems).to receive(:validate).with have_attributes(name: "validated")
+ subject
+ end
+ end
+
+ context "with gemspec containing local variables" do
+ before do
+ File.open(app_gemspec_path, "wb") do |f|
+ f.write <<~GEMSPEC
+ must_not_leak = true
+ Gem::Specification.new do |gem|
+ gem.name = "leak check"
+ end
+ GEMSPEC
+ end
+ end
+
+ it "should not pollute the TOPLEVEL_BINDING" do
+ subject
+ expect(TOPLEVEL_BINDING.eval("local_variables")).to_not include(:must_not_leak)
+ end
+ end
+ end
+
+ describe "#which" do
+ it "can detect relative path" do
+ script_path = bundled_app("tmp/test_command")
+ create_file(script_path, "#!/usr/bin/env ruby\n")
+
+ result = Dir.chdir script_path.dirname.dirname do
+ Bundler.which("test_command")
+ end
+ expect(result).to eq(nil)
+
+ result = Dir.chdir script_path.dirname do
+ Bundler.which("test_command")
+ end
+
+ expect(result).to eq("test_command") unless Gem.win_platform?
+ expect(result).to eq("test_command.bat") if Gem.win_platform?
+ end
+
+ it "can detect absolute path" do
+ create_file("test_command", "#!/usr/bin/env ruby\n")
+
+ ENV["PATH"] = bundled_app("test_command").parent.to_s
+
+ result = Bundler.which("test_command")
+ expect(result).to eq(bundled_app("test_command").to_s) unless Gem.win_platform?
+ expect(result).to eq(bundled_app("test_command.bat").to_s) if Gem.win_platform?
+ end
+
+ it "returns nil when not found" do
+ result = Bundler.which("test_command")
+ expect(result).to eq(nil)
+ end
+ end
+
+ describe "configuration" do
+ context "disable_shared_gems" do
+ it "should unset GEM_PATH with empty string" do
+ expect(Bundler).to receive(:use_system_gems?).and_return(false)
+ Bundler.send(:configure_gem_path)
+ expect(ENV["GEM_PATH"]).to eq ""
+ end
+ end
+ end
+
+ describe "#mkdir_p" do
+ it "creates a folder at the given path" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ allow(Bundler).to receive(:root).and_return(bundled_app)
+
+ Bundler.mkdir_p(bundled_app("foo", "bar"))
+ expect(bundled_app("foo", "bar")).to exist
+ end
+ end
+
+ describe "#user_home" do
+ context "home directory is set" do
+ it "should return the user home" do
+ path = "/home/oggy"
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return true
+ allow(File).to receive(:writable?).with(path).and_return true
+ expect(Bundler.user_home).to eq(Pathname(path))
+ end
+
+ context "is not a directory" do
+ it "should issue a warning and return a temporary user home" do
+ path = "/home/oggy"
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return false
+ allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom"))
+ expect(Bundler.ui).to receive(:warn).with("`/home/oggy` is not a directory.\n")
+ expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n")
+ expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom"))
+ end
+ end
+
+ context "is not writable" do
+ let(:path) { "/home/oggy" }
+ let(:dotbundle) { "/home/oggy/.bundle" }
+
+ it "should issue a warning and return a temporary user home" do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return true
+ allow(File).to receive(:writable?).and_call_original
+ allow(File).to receive(:writable?).with(path).and_return false
+ allow(File).to receive(:directory?).with(dotbundle).and_return false
+ allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom"))
+ expect(Bundler.ui).to receive(:warn).with("`/home/oggy` is not writable.\n")
+ expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n")
+ expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom"))
+ end
+
+ context ".bundle exists and have correct permissions" do
+ it "should return the user home" do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(path)
+ allow(File).to receive(:directory?).with(path).and_return true
+ allow(File).to receive(:writable?).with(path).and_return false
+ allow(File).to receive(:directory?).with(dotbundle).and_return true
+ allow(File).to receive(:writable?).with(dotbundle).and_return true
+ expect(Bundler.user_home).to eq(Pathname(path))
+ end
+ end
+ end
+ end
+
+ context "home directory is not set" do
+ it "should issue warning and return a temporary user home" do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(nil)
+ allow(Bundler).to receive(:tmp).and_return(Pathname.new("/tmp/trulyrandom"))
+ expect(Bundler.ui).to receive(:warn).with("Your home directory is not set.\n")
+ expect(Bundler.ui).to receive(:warn).with("Bundler will use `/tmp/trulyrandom' as your home directory temporarily.\n")
+ expect(Bundler.user_home).to eq(Pathname("/tmp/trulyrandom"))
+ end
+ end
+ end
+
+ context "user cache dir" do
+ let(:home_path) { Pathname.new(ENV["HOME"]) }
+
+ let(:xdg_data_home) { home_path.join(".local") }
+ let(:xdg_cache_home) { home_path.join(".cache") }
+ let(:xdg_config_home) { home_path.join(".config") }
+
+ let(:bundle_user_home_default) { home_path.join(".bundle") }
+ let(:bundle_user_home_custom) { xdg_data_home.join("bundle") }
+
+ let(:bundle_user_cache_default) { bundle_user_home_default.join("cache") }
+ let(:bundle_user_cache_custom) { xdg_cache_home.join("bundle") }
+
+ let(:bundle_user_config_default) { bundle_user_home_default.join("config") }
+ let(:bundle_user_config_custom) { xdg_config_home.join("bundle") }
+
+ let(:bundle_user_plugin_default) { bundle_user_home_default.join("plugin") }
+ let(:bundle_user_plugin_custom) { xdg_data_home.join("bundle").join("plugin") }
+
+ describe "#user_bundle_path" do
+ before do
+ allow(Bundler.rubygems).to receive(:user_home).and_return(home_path)
+ end
+
+ it "should use the default home path" do
+ expect(Bundler.user_bundle_path).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_default)
+ expect(Bundler.user_cache).to eq(bundle_user_cache_default)
+ expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_default)
+ expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_default)
+ end
+
+ it "should use custom home path as root for other paths" do
+ ENV["BUNDLE_USER_HOME"] = bundle_user_home_custom.to_s
+ allow(Bundler.rubygems).to receive(:user_home).and_raise
+ expect(Bundler.user_bundle_path).to eq(bundle_user_home_custom)
+ expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_custom)
+ expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_home_custom.join("cache"))
+ expect(Bundler.user_cache).to eq(bundle_user_home_custom.join("cache"))
+ expect(Bundler.user_bundle_path("config")).to eq(bundle_user_home_custom.join("config"))
+ expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_home_custom.join("plugin"))
+ end
+
+ it "should use all custom paths, except home" do
+ ENV.delete("BUNDLE_USER_HOME")
+ ENV["BUNDLE_USER_CACHE"] = bundle_user_cache_custom.to_s
+ ENV["BUNDLE_USER_CONFIG"] = bundle_user_config_custom.to_s
+ ENV["BUNDLE_USER_PLUGIN"] = bundle_user_plugin_custom.to_s
+ expect(Bundler.user_bundle_path).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("home")).to eq(bundle_user_home_default)
+ expect(Bundler.user_bundle_path("cache")).to eq(bundle_user_cache_custom)
+ expect(Bundler.user_cache).to eq(bundle_user_cache_custom)
+ expect(Bundler.user_bundle_path("config")).to eq(bundle_user_config_custom)
+ expect(Bundler.user_bundle_path("plugin")).to eq(bundle_user_plugin_custom)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ci_detector_spec.rb b/spec/bundler/bundler/ci_detector_spec.rb
new file mode 100644
index 0000000000..299d8005e8
--- /dev/null
+++ b/spec/bundler/bundler/ci_detector_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::CIDetector do
+ # This is properly tested in rubygems, under the name Gem::CIDetector
+ # But the test that confirms that our version _stays in sync_ with that version
+ # will live here.
+
+ it "stays in sync with the rubygems implementation" do
+ rubygems_implementation_path = File.join(git_root, "lib", "rubygems", "ci_detector.rb")
+ expect(File.exist?(rubygems_implementation_path)).to be_truthy
+ rubygems_code = File.read(rubygems_implementation_path)
+ denamespaced_rubygems_code = rubygems_code.sub("Gem", "NAMESPACE")
+
+ bundler_implementation_path = File.join(source_lib_dir, "bundler", "ci_detector.rb")
+ expect(File.exist?(bundler_implementation_path)).to be_truthy
+ bundler_code = File.read(bundler_implementation_path)
+ denamespaced_bundler_code = bundler_code.sub("Bundler", "NAMESPACE")
+
+ expect(denamespaced_bundler_code).to eq(denamespaced_rubygems_code)
+ end
+end
diff --git a/spec/bundler/bundler/cli_common_spec.rb b/spec/bundler/bundler/cli_common_spec.rb
new file mode 100644
index 0000000000..015894b3a1
--- /dev/null
+++ b/spec/bundler/bundler/cli_common_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "bundler/cli"
+
+RSpec.describe Bundler::CLI::Common do
+ describe "gem_not_found_message" do
+ it "should suggest alternate gem names" do
+ message = subject.gem_not_found_message("ralis", ["BOGUS"])
+ expect(message).to match("Could not find gem 'ralis'.$")
+ message = subject.gem_not_found_message("ralis", ["rails"])
+ expect(message).to match("Did you mean 'rails'?")
+ message = subject.gem_not_found_message("Rails", ["rails"])
+ expect(message).to match("Did you mean 'rails'?")
+ message = subject.gem_not_found_message("meail", %w[email fail eval])
+ expect(message).to match("Did you mean 'email'?")
+ message = subject.gem_not_found_message("nokogri", %w[nokogiri rails sidekiq dog])
+ expect(message).to match("Did you mean 'nokogiri'?")
+ message = subject.gem_not_found_message("methosd", %w[method methods bogus])
+ expect(message).to match(/Did you mean 'method(|s)' or 'method(|s)'?/)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/cli_spec.rb b/spec/bundler/bundler/cli_spec.rb
new file mode 100644
index 0000000000..56caf9937e
--- /dev/null
+++ b/spec/bundler/bundler/cli_spec.rb
@@ -0,0 +1,298 @@
+# frozen_string_literal: true
+
+require "bundler/cli"
+
+RSpec.describe "bundle executable" do
+ it "returns non-zero exit status when passed unrecognized options" do
+ bundle "--invalid_argument", raise_on_error: false
+ expect(exitstatus).to_not be_zero
+ end
+
+ it "returns non-zero exit status when passed unrecognized task" do
+ bundle "unrecognized-task", raise_on_error: false
+ expect(exitstatus).to_not be_zero
+ end
+
+ it "looks for a binary and executes it if it's named bundler-<task>" do
+ skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform?
+
+ File.open(tmp("bundler-testtasks"), "w", 0o755) do |f|
+ ruby = ENV["RUBY"] || "/usr/bin/env ruby"
+ f.puts "#!#{ruby}\nputs 'Hello, world'\n"
+ end
+
+ with_path_added(tmp) do
+ bundle "testtasks"
+ end
+
+ expect(out).to eq("Hello, world")
+ end
+
+ describe "aliases" do
+ it "aliases e to exec" do
+ bundle "e --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-exec")
+ end
+
+ it "aliases ex to exec" do
+ bundle "ex --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-exec")
+ end
+
+ it "aliases exe to exec" do
+ bundle "exe --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-exec")
+ end
+
+ it "aliases c to check" do
+ bundle "c --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-check")
+ end
+
+ it "aliases i to install" do
+ bundle "i --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-install")
+ end
+
+ it "aliases ls to list" do
+ bundle "ls --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-list")
+ end
+
+ it "aliases package to cache" do
+ bundle "package --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-cache")
+ end
+
+ it "aliases pack to cache" do
+ bundle "pack --help"
+
+ expect(out_with_macos_man_workaround).to include("bundle-cache")
+ end
+
+ private
+
+ # Some `man` (e.g., on macOS) always highlights the output even to
+ # non-tty.
+ def out_with_macos_man_workaround
+ out.gsub(/.[\b]/, "")
+ end
+ end
+
+ context "with no arguments" do
+ it "tries to installs by default but print help on missing Gemfile" do
+ bundle "", raise_on_error: false
+ expect(err).to include("Could not locate Gemfile")
+ expect(out).to include("In a future version of Bundler")
+
+ expect(out).to include("Bundler version #{Bundler::VERSION}").
+ and include("\n\nBundler commands:\n\n").
+ and include("\n\n Primary commands:\n").
+ and include("\n\n Utilities:\n").
+ and include("\n\nOptions:\n")
+ end
+
+ it "runs bundle install when default_cli_command set to install" do
+ bundle_config "default_cli_command install"
+ bundle "", raise_on_error: false
+ expect(out).to_not include("In a future version of Bundler")
+ expect(err).to include("Could not locate Gemfile")
+ expect(exitstatus).to_not be_zero
+ end
+ end
+
+ context "when ENV['BUNDLE_GEMFILE'] is set to an empty string" do
+ it "ignores it" do
+ gemfile bundled_app_gemfile, <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, env: { "BUNDLE_GEMFILE" => "" }
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with --verbose" do
+ before do
+ gemfile "source 'https://gem.repo1'"
+ end
+
+ it "prints the running command" do
+ bundle "info bundler", verbose: true
+ expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION}")
+
+ bundle "install", verbose: true
+ expect(out).to start_with("Running `bundle install --verbose` with bundler #{Bundler::VERSION}")
+ end
+
+ it "prints the simulated version too when setting is enabled" do
+ bundle "config set simulate_version 4", verbose: true
+ bundle "info bundler", verbose: true
+ expect(out).to start_with("Running `bundle info bundler --verbose` with bundler #{Bundler::VERSION} (simulating Bundler 4)")
+ end
+ end
+
+ context "with verbose configuration" do
+ before do
+ bundle_config "verbose true"
+ end
+
+ it "prints the running command" do
+ gemfile "source 'https://gem.repo1'"
+ bundle "info bundler"
+ expect(out).to start_with("Running `bundle info bundler` with bundler #{Bundler::VERSION}")
+ end
+ end
+
+ describe "bundle outdated" do
+ let(:run_command) do
+ bundle "install"
+
+ bundle "outdated #{flags}", raise_on_error: false
+ end
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", '0.9.1'
+ G
+ end
+
+ context "with --groups flag" do
+ let(:flags) { "--groups" }
+
+ it "prints a message when there are outdated gems" do
+ run_command
+
+ expect(out).to include("Gem Current Latest Requested Groups")
+ expect(out).to include("myrack 0.9.1 1.0.0 = 0.9.1 default")
+ end
+ end
+
+ context "with --parseable" do
+ let(:flags) { "--parseable" }
+
+ it "prints a message when there are outdated gems" do
+ run_command
+
+ expect(out).to include("myrack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)")
+ end
+ end
+
+ context "with --groups and --parseable" do
+ let(:flags) { "--groups --parseable" }
+
+ it "prints a simplified message when there are outdated gems" do
+ run_command
+
+ expect(out).to include("myrack (newest 1.0.0, installed 0.9.1, requested = 0.9.1)")
+ end
+ end
+ end
+
+ describe "printing the outdated warning" do
+ shared_examples_for "no warning" do
+ it "prints no warning" do
+ bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(stdboth).to eq("Could not find command \"fail\".")
+ end
+ end
+
+ let(:bundler_version) { "2.0" }
+ let(:latest_version) { nil }
+ before do
+ bundle_config_global "disable_version_check false"
+
+ pristine_system_gems "bundler-#{bundler_version}"
+ if latest_version
+ info_path = home(".bundle/cache/compact_index/rubygems.org.443.29b0360b937aa4d161703e6160654e47/info/bundler")
+ info_path.parent.mkpath
+ info_path.open("w") {|f| f.write "#{latest_version}\n" }
+ end
+ end
+
+ context "when there is no latest version" do
+ include_examples "no warning"
+ end
+
+ context "when the latest version is equal to the current version" do
+ let(:latest_version) { bundler_version }
+ include_examples "no warning"
+ end
+
+ context "when the latest version is less than the current version" do
+ let(:latest_version) { "0.9" }
+ include_examples "no warning"
+ end
+
+ context "when the latest version is greater than the current version" do
+ let(:latest_version) { "222.0" }
+ it "prints the version warning" do
+ bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(err).to start_with(<<-EOS.strip)
+The latest bundler is #{latest_version}, but you are currently running #{bundler_version}.
+To update to the most recent version, run `bundle update --bundler`
+ EOS
+ end
+
+ context "and disable_version_check is set" do
+ before { bundle "config set disable_version_check true", env: { "BUNDLER_VERSION" => bundler_version } }
+ include_examples "no warning"
+ end
+
+ context "running a parseable command" do
+ it "prints no warning" do
+ bundle "config set foo value", env: { "BUNDLER_VERSION" => bundler_version }
+ bundle "config get --parseable foo", env: { "BUNDLER_VERSION" => bundler_version }
+ expect(out).to eq "foo=value"
+ expect(err).to eq ""
+
+ bundle "platform --ruby", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(stdboth).to eq "Could not locate Gemfile"
+ end
+ end
+
+ context "and is a pre-release" do
+ let(:latest_version) { "222.0.0.pre.4" }
+ it "prints the version warning" do
+ bundle "fail", env: { "BUNDLER_VERSION" => bundler_version }, raise_on_error: false
+ expect(err).to start_with(<<-EOS.strip)
+The latest bundler is #{latest_version}, but you are currently running #{bundler_version}.
+To update to the most recent version, run `bundle update --bundler`
+ EOS
+ end
+ end
+ end
+ end
+end
+
+RSpec.describe "bundler executable" do
+ it "shows the bundler version just as the `bundle` executable does" do
+ bundler "--version"
+ expect(out).to eq(Bundler::VERSION.to_s)
+
+ bundle_config "simulate_version 5"
+ bundler "--version"
+ expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)")
+ end
+
+ it "shows cli_help when bundler install and no Gemfile is found" do
+ bundler "install", raise_on_error: false
+ expect(err).to include("Could not locate Gemfile")
+
+ expect(out).to include("Bundler version #{Bundler::VERSION}").
+ and include("\n\nBundler commands:\n\n").
+ and include("\n\n Primary commands:\n").
+ and include("\n\n Utilities:\n").
+ and include("\n\nOptions:\n")
+ end
+end
diff --git a/spec/bundler/bundler/compact_index_client/parser_spec.rb b/spec/bundler/bundler/compact_index_client/parser_spec.rb
new file mode 100644
index 0000000000..6aa867f058
--- /dev/null
+++ b/spec/bundler/bundler/compact_index_client/parser_spec.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+require "bundler/compact_index_client"
+require "bundler/compact_index_client/parser"
+
+TestCompactIndexClient = Struct.new(:names, :versions, :info_data) do
+ # Requiring the checksum to match the input data helps ensure
+ # that we are parsing the correct checksum from the versions file
+ def info(name, checksum)
+ info_data.dig(name, checksum)
+ end
+
+ def set_info_data(name, value)
+ info_data[name] = value
+ end
+end
+
+RSpec.describe Bundler::CompactIndexClient::Parser do
+ subject(:parser) { described_class.new(compact_index) }
+
+ let(:compact_index) { TestCompactIndexClient.new(names, versions, info_data) }
+ let(:names) { "a\nb\nc\n" }
+ let(:versions) { <<~VERSIONS.dup }
+ created_at: 2024-05-01T00:00:04Z
+ ---
+ a 1.0.0,1.0.1,1.1.0 aaa111
+ b 2.0.0,2.0.0-java bbb222
+ c 3.0.0,3.0.3,3.3.3 ccc333
+ c -3.0.3 ccc333yanked
+ VERSIONS
+ let(:info_data) do
+ {
+ "a" => { "aaa111" => a_info },
+ "b" => { "bbb222" => b_info },
+ "c" => { "ccc333yanked" => c_info },
+ }
+ end
+ let(:a_info) { <<~INFO.dup }
+ ---
+ 1.0.0 |checksum:aaa1,ruby:>= 3.0.0,rubygems:>= 3.2.3
+ 1.0.1 |checksum:aaa2,ruby:>= 3.0.0,rubygems:>= 3.2.3
+ 1.1.0 |checksum:aaa3,ruby:>= 3.0.0,rubygems:>= 3.2.3
+ INFO
+ let(:b_info) { <<~INFO }
+ 2.0.0 a:~> 1.0&<= 3.0|checksum:bbb1
+ 2.0.0-java a:~> 1.0&<= 3.0|checksum:bbb2
+ INFO
+ let(:c_info) { <<~INFO }
+ 3.0.0 a:= 1.0.0,b:~> 2.0|checksum:ccc1,ruby:>= 2.7.0,rubygems:>= 3.0.0
+ 3.3.3 a:>= 1.1.0,b:~> 2.0|checksum:ccc3,ruby:>= 3.0.0,rubygems:>= 3.2.3,created_at:2026-05-12T10:00:00Z
+ INFO
+
+ describe "#available?" do
+ it "returns true versions are available" do
+ expect(parser).to be_available
+ end
+
+ it "returns true when versions has only one gem" do
+ compact_index.versions = +"a 1.0.0 aaa1\n"
+ expect(parser).to be_available
+ end
+
+ it "returns true when versions has a gem and a header" do
+ compact_index.versions = +"---\na 1.0.0 aaa1\n"
+ expect(parser).to be_available
+ end
+
+ it "returns true when versions has a gem and a header with header data" do
+ compact_index.versions = +"created_at: 2024-05-01T00:00:04Z\n---\na 1.0.0 aaa1\n"
+ expect(parser).to be_available
+ end
+
+ it "returns false when versions has only the header" do
+ compact_index.versions = +"---\n"
+ expect(parser).not_to be_available
+ end
+
+ it "returns false when versions has only the header with header data" do
+ compact_index.versions = +"created_at: 2024-05-01T00:00:04Z\n---\n"
+ expect(parser).not_to be_available
+ end
+
+ it "returns false when versions index is not available" do
+ compact_index.versions = nil
+ expect(parser).not_to be_available
+ end
+
+ it "returns false when versions is empty" do
+ compact_index.versions = +""
+ expect(parser).not_to be_available
+ end
+ end
+
+ describe "#names" do
+ it "returns the names" do
+ expect(parser.names).to eq(%w[a b c])
+ end
+
+ it "returns an empty array when names is empty" do
+ compact_index.names = ""
+ expect(parser.names).to eq([])
+ end
+
+ it "returns an empty array when names is not readable" do
+ compact_index.names = nil
+ expect(parser.names).to eq([])
+ end
+ end
+
+ describe "#versions" do
+ it "returns the versions" do
+ expect(parser.versions).to eq(
+ "a" => [
+ ["a", "1.0.0"],
+ ["a", "1.0.1"],
+ ["a", "1.1.0"],
+ ],
+ "b" => [
+ ["b", "2.0.0"],
+ ["b", "2.0.0", "java"],
+ ],
+ "c" => [
+ ["c", "3.0.0"],
+ ["c", "3.3.3"],
+ ],
+ )
+ end
+
+ it "returns an empty hash when versions is empty" do
+ compact_index.versions = ""
+ expect(parser.versions).to eq({})
+ end
+
+ it "returns an empty hash when versions is not readable" do
+ compact_index.versions = nil
+ expect(parser.versions).to eq({})
+ end
+ end
+
+ describe "#info" do
+ let(:a_result) do
+ [
+ [
+ "a",
+ "1.0.0",
+ nil,
+ [],
+ [["checksum", ["aaa1"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]],
+ ],
+ [
+ "a",
+ "1.0.1",
+ nil,
+ [],
+ [["checksum", ["aaa2"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]],
+ ],
+ [
+ "a",
+ "1.1.0",
+ nil,
+ [],
+ [["checksum", ["aaa3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]]],
+ ],
+ ]
+ end
+ let(:b_result) do
+ [
+ [
+ "b",
+ "2.0.0",
+ nil,
+ [["a", ["~> 1.0", "<= 3.0"]]],
+ [["checksum", ["bbb1"]]],
+ ],
+ [
+ "b",
+ "2.0.0",
+ "java",
+ [["a", ["~> 1.0", "<= 3.0"]]],
+ [["checksum", ["bbb2"]]],
+ ],
+ ]
+ end
+ let(:c_result) do
+ [
+ [
+ "c",
+ "3.0.0",
+ nil,
+ [["a", ["= 1.0.0"]], ["b", ["~> 2.0"]]],
+ [["checksum", ["ccc1"]], ["ruby", [">= 2.7.0"]], ["rubygems", [">= 3.0.0"]]],
+ ],
+ [
+ "c",
+ "3.3.3",
+ nil,
+ [["a", [">= 1.1.0"]], ["b", ["~> 2.0"]]],
+ [["checksum", ["ccc3"]], ["ruby", [">= 3.0.0"]], ["rubygems", [">= 3.2.3"]], ["created_at", ["2026-05-12T10:00:00Z"]]],
+ ],
+ ]
+ end
+
+ it "returns the info for example gem 'a' which has no deps" do
+ expect(parser.info("a")).to eq(a_result)
+ end
+
+ it "returns the info for example gem 'b' which has platform and compound deps" do
+ expect(parser.info("b")).to eq(b_result)
+ end
+
+ it "returns the info for example gem 'c' which has deps and yanked version (requires use of correct info checksum)" do
+ expect(parser.info("c")).to eq(c_result)
+ end
+
+ it "returns an empty array when the info is empty" do
+ compact_index.set_info_data("a", {})
+ expect(parser.info("a")).to eq([])
+ end
+
+ it "returns an empty array when the info is not readable" do
+ expect(parser.info("d")).to eq([])
+ end
+
+ it "handles empty lines in the versions file (Artifactory bug that they have yet to fix)" do
+ compact_index.versions = +<<~VERSIONS
+ created_at: 2024-05-01T00:00:04Z
+ ---
+ a 1.0.0,1.0.1,1.1.0 aaa111
+ b 2.0.0,2.0.0-java bbb222
+
+ c 3.0.0,3.0.3,3.3.3 ccc333
+ c -3.0.3 ccc333yanked
+ VERSIONS
+ expect(parser.info("a")).to eq(a_result)
+ end
+
+ it "handles lines without a checksum" do
+ compact_index.versions = <<~VERSIONS
+ created_at: 2024-05-01T00:00:04Z
+ ---
+ a 1.0.0,1.0.1,1.1.0 aaa111
+ b 2.0.0,2.0.0-java
+ c 3.0.0,3.0.3,3.3.3 ccc333
+ VERSIONS
+
+ expect(parser.info("a")).to eq(a_result)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/compact_index_client/updater_spec.rb b/spec/bundler/bundler/compact_index_client/updater_spec.rb
new file mode 100644
index 0000000000..fd63a652a4
--- /dev/null
+++ b/spec/bundler/bundler/compact_index_client/updater_spec.rb
@@ -0,0 +1,243 @@
+# frozen_string_literal: true
+
+require "bundler/vendored_net_http"
+require "bundler/compact_index_client"
+require "bundler/compact_index_client/updater"
+require "tmpdir"
+
+RSpec.describe Bundler::CompactIndexClient::Updater do
+ subject(:updater) { described_class.new(fetcher) }
+
+ let(:fetcher) { double(:fetcher) }
+ let(:local_path) { Pathname.new(Dir.mktmpdir("localpath")).join("versions") }
+ let(:etag_path) { Pathname.new(Dir.mktmpdir("localpath-etags")).join("versions.etag") }
+ let(:remote_path) { double(:remote_path) }
+
+ let(:full_body) { "abc123" }
+ let(:response) { double(:response, body: full_body, is_a?: false) }
+ let(:digest) { Digest::SHA256.base64digest(full_body) }
+
+ context "when the local path does not exist" do
+ before do
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { '"thisisanetag"' }
+ end
+
+ it "downloads the file without attempting append" do
+ expect(fetcher).to receive(:call).once.with(remote_path, {}) { response }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("thisisanetag")
+ end
+
+ it "fails immediately on bad checksum" do
+ expect(fetcher).to receive(:call).once.with(remote_path, {}) { response }
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+
+ expect do
+ updater.update(remote_path, local_path, etag_path)
+ end.to raise_error(Bundler::CompactIndexClient::Updater::MismatchedChecksumError)
+ end
+ end
+
+ context "when the local path exists" do
+ let(:local_body) { "abc" }
+
+ before do
+ local_path.open("w") {|f| f.write(local_body) }
+ end
+
+ context "with an etag" do
+ before do
+ etag_path.open("w") {|f| f.write("LocalEtag") }
+ end
+
+ let(:headers) do
+ {
+ "If-None-Match" => '"LocalEtag"',
+ "Range" => "bytes=2-",
+ }
+ end
+
+ it "does nothing if etags match" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { true }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq("abc")
+ expect(etag_path.read).to eq("LocalEtag")
+ end
+
+ it "appends the file if etags do not match" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false }
+ allow(response).to receive(:body) { "c123" }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+
+ it "replaces the file if response ignores range" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ allow(response).to receive(:body) { full_body }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+
+ it "tries the request again if the partial response fails digest check" do
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+ allow(response).to receive(:body) { "the beginning of the file changed" }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+
+ full_response = double(:full_response, body: full_body, is_a?: false)
+ allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response)
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+
+ it "tries the request again if the partial response is blank" do
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+ allow(response).to receive(:body) { "" }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+
+ full_response = double(:full_response, body: full_body, is_a?: false)
+ allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response)
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+ end
+
+ context "without an etag file" do
+ let(:headers) do
+ { "Range" => "bytes=2-" }
+ end
+
+ it "appends the file" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false }
+ allow(response).to receive(:body) { "c123" }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("OpaqueEtag")
+ end
+
+ it "replaces the file on full file response that ignores range request" do
+ expect(fetcher).to receive(:call).once.with(remote_path, headers).and_return(response)
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { '"OpaqueEtag"' }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { false }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified) { false }
+ allow(response).to receive(:body) { full_body }
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("OpaqueEtag")
+ end
+
+ it "tries the request again if the partial response fails digest check" do
+ allow(response).to receive(:[]).with("Repr-Digest") { "sha-256=:baddigest:" }
+ allow(response).to receive(:body) { "the beginning of the file changed" }
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPPartialContent) { true }
+ expect(fetcher).to receive(:call).once.with(remote_path, headers) do
+ # During the failed first request, we simulate another process writing the etag.
+ # This ensures the second request doesn't generate the md5 etag again but just uses whatever is written.
+ etag_path.open("w") {|f| f.write("LocalEtag") }
+ response
+ end
+
+ full_response = double(:full_response, body: full_body, is_a?: false)
+ allow(full_response).to receive(:[]).with("Repr-Digest") { "sha-256=:#{digest}:" }
+ allow(full_response).to receive(:[]).with("ETag") { '"NewEtag"' }
+ expect(fetcher).to receive(:call).once.with(remote_path, { "If-None-Match" => '"LocalEtag"' }).and_return(full_response)
+
+ updater.update(remote_path, local_path, etag_path)
+
+ expect(local_path.read).to eq(full_body)
+ expect(etag_path.read).to eq("NewEtag")
+ end
+ end
+ end
+
+ context "when the ETag header is missing" do
+ # Regression test for https://github.com/rubygems/bundler/issues/5463
+ let(:response) { double(:response, body: full_body) }
+
+ it "treats the response as an update" do
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { nil }
+ expect(fetcher).to receive(:call) { response }
+
+ updater.update(remote_path, local_path, etag_path)
+ end
+ end
+
+ context "when the download is corrupt" do
+ let(:response) { double(:response, body: "") }
+
+ it "raises HTTPError" do
+ expect(fetcher).to receive(:call).and_raise(Zlib::GzipFile::Error)
+
+ expect do
+ updater.update(remote_path, local_path, etag_path)
+ end.to raise_error(Bundler::HTTPError)
+ end
+ end
+
+ context "when receiving non UTF-8 data and default internal encoding set to ASCII" do
+ let(:response) { double(:response, body: "\x8B".b) }
+
+ it "works just fine" do
+ old_verbose = $VERBOSE
+ previous_internal_encoding = Encoding.default_internal
+
+ begin
+ $VERBOSE = false
+ Encoding.default_internal = "ASCII"
+ allow(response).to receive(:[]).with("Repr-Digest") { nil }
+ allow(response).to receive(:[]).with("Digest") { nil }
+ allow(response).to receive(:[]).with("ETag") { nil }
+ expect(fetcher).to receive(:call) { response }
+
+ updater.update(remote_path, local_path, etag_path)
+ ensure
+ Encoding.default_internal = previous_internal_encoding
+ $VERBOSE = old_verbose
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/current_ruby_spec.rb b/spec/bundler/bundler/current_ruby_spec.rb
new file mode 100644
index 0000000000..79eb802aa5
--- /dev/null
+++ b/spec/bundler/bundler/current_ruby_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::CurrentRuby do
+ describe "PLATFORM_MAP" do
+ subject { described_class::PLATFORM_MAP }
+
+ # rubocop:disable Naming/VariableNumber
+ let(:platforms) do
+ { ruby: Gem::Platform::RUBY,
+ ruby_18: Gem::Platform::RUBY,
+ ruby_19: Gem::Platform::RUBY,
+ ruby_20: Gem::Platform::RUBY,
+ ruby_21: Gem::Platform::RUBY,
+ ruby_22: Gem::Platform::RUBY,
+ ruby_23: Gem::Platform::RUBY,
+ ruby_24: Gem::Platform::RUBY,
+ ruby_25: Gem::Platform::RUBY,
+ ruby_26: Gem::Platform::RUBY,
+ ruby_27: Gem::Platform::RUBY,
+ ruby_30: Gem::Platform::RUBY,
+ ruby_31: Gem::Platform::RUBY,
+ ruby_32: Gem::Platform::RUBY,
+ ruby_33: Gem::Platform::RUBY,
+ ruby_34: Gem::Platform::RUBY,
+ ruby_40: Gem::Platform::RUBY,
+ ruby_41: Gem::Platform::RUBY,
+ mri: Gem::Platform::RUBY,
+ mri_18: Gem::Platform::RUBY,
+ mri_19: Gem::Platform::RUBY,
+ mri_20: Gem::Platform::RUBY,
+ mri_21: Gem::Platform::RUBY,
+ mri_22: Gem::Platform::RUBY,
+ mri_23: Gem::Platform::RUBY,
+ mri_24: Gem::Platform::RUBY,
+ mri_25: Gem::Platform::RUBY,
+ mri_26: Gem::Platform::RUBY,
+ mri_27: Gem::Platform::RUBY,
+ mri_30: Gem::Platform::RUBY,
+ mri_31: Gem::Platform::RUBY,
+ mri_32: Gem::Platform::RUBY,
+ mri_33: Gem::Platform::RUBY,
+ mri_34: Gem::Platform::RUBY,
+ mri_40: Gem::Platform::RUBY,
+ mri_41: Gem::Platform::RUBY,
+ rbx: Gem::Platform::RUBY,
+ truffleruby: Gem::Platform::RUBY,
+ jruby: Gem::Platform::JAVA,
+ jruby_18: Gem::Platform::JAVA,
+ jruby_19: Gem::Platform::JAVA,
+ windows: Gem::Platform::WINDOWS,
+ windows_18: Gem::Platform::WINDOWS,
+ windows_19: Gem::Platform::WINDOWS,
+ windows_20: Gem::Platform::WINDOWS,
+ windows_21: Gem::Platform::WINDOWS,
+ windows_22: Gem::Platform::WINDOWS,
+ windows_23: Gem::Platform::WINDOWS,
+ windows_24: Gem::Platform::WINDOWS,
+ windows_25: Gem::Platform::WINDOWS,
+ windows_26: Gem::Platform::WINDOWS,
+ windows_27: Gem::Platform::WINDOWS,
+ windows_30: Gem::Platform::WINDOWS,
+ windows_31: Gem::Platform::WINDOWS,
+ windows_32: Gem::Platform::WINDOWS,
+ windows_33: Gem::Platform::WINDOWS,
+ windows_34: Gem::Platform::WINDOWS,
+ windows_40: Gem::Platform::WINDOWS,
+ windows_41: Gem::Platform::WINDOWS }
+ end
+
+ let(:deprecated) do
+ { mswin: Gem::Platform::MSWIN,
+ mswin_18: Gem::Platform::MSWIN,
+ mswin_19: Gem::Platform::MSWIN,
+ mswin_20: Gem::Platform::MSWIN,
+ mswin_21: Gem::Platform::MSWIN,
+ mswin_22: Gem::Platform::MSWIN,
+ mswin_23: Gem::Platform::MSWIN,
+ mswin_24: Gem::Platform::MSWIN,
+ mswin_25: Gem::Platform::MSWIN,
+ mswin_26: Gem::Platform::MSWIN,
+ mswin_27: Gem::Platform::MSWIN,
+ mswin_30: Gem::Platform::MSWIN,
+ mswin_31: Gem::Platform::MSWIN,
+ mswin_32: Gem::Platform::MSWIN,
+ mswin_33: Gem::Platform::MSWIN,
+ mswin_34: Gem::Platform::MSWIN,
+ mswin_40: Gem::Platform::MSWIN,
+ mswin_41: Gem::Platform::MSWIN,
+ mswin64: Gem::Platform::MSWIN64,
+ mswin64_19: Gem::Platform::MSWIN64,
+ mswin64_20: Gem::Platform::MSWIN64,
+ mswin64_21: Gem::Platform::MSWIN64,
+ mswin64_22: Gem::Platform::MSWIN64,
+ mswin64_23: Gem::Platform::MSWIN64,
+ mswin64_24: Gem::Platform::MSWIN64,
+ mswin64_25: Gem::Platform::MSWIN64,
+ mswin64_26: Gem::Platform::MSWIN64,
+ mswin64_27: Gem::Platform::MSWIN64,
+ mswin64_30: Gem::Platform::MSWIN64,
+ mswin64_31: Gem::Platform::MSWIN64,
+ mswin64_32: Gem::Platform::MSWIN64,
+ mswin64_33: Gem::Platform::MSWIN64,
+ mswin64_34: Gem::Platform::MSWIN64,
+ mswin64_40: Gem::Platform::MSWIN64,
+ mswin64_41: Gem::Platform::MSWIN64,
+ mingw: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_18: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_19: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_20: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_21: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_22: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_23: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_24: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_25: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_26: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_27: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_30: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_31: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_32: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_33: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_34: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_40: Gem::Platform::UNIVERSAL_MINGW,
+ mingw_41: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_20: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_21: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_22: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_23: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_24: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_25: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_26: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_27: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_30: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_31: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_32: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_33: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_34: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_40: Gem::Platform::UNIVERSAL_MINGW,
+ x64_mingw_41: Gem::Platform::UNIVERSAL_MINGW }
+ end
+ # rubocop:enable Naming/VariableNumber
+
+ it "includes all platforms" do
+ expect(subject).to eq(platforms.merge(deprecated))
+ end
+ end
+
+ describe "Deprecated platform" do
+ it "outputs an error and aborts when calling maglev?" do
+ expect { Bundler.current_ruby.maglev? }.to raise_error(Bundler::RemovedError, /`CurrentRuby#maglev\?` was removed with no replacement./)
+ end
+
+ it "outputs an error and aborts when calling maglev_31?" do
+ expect { Bundler.current_ruby.maglev_31? }.to raise_error(Bundler::RemovedError, /`CurrentRuby#maglev_31\?` was removed with no replacement./)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/definition_spec.rb b/spec/bundler/bundler/definition_spec.rb
new file mode 100644
index 0000000000..8c4a5a0331
--- /dev/null
+++ b/spec/bundler/bundler/definition_spec.rb
@@ -0,0 +1,376 @@
+# frozen_string_literal: true
+
+require "bundler/definition"
+
+RSpec.describe Bundler::Definition do
+ describe "#overrides" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile }
+ end
+
+ subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) }
+
+ it "defaults to an empty array" do
+ expect(subject.overrides).to eq([])
+ end
+
+ it "is writable" do
+ override = Bundler::Override.new("rails", :version, ">= 8.0")
+ subject.overrides = [override]
+ expect(subject.overrides).to eq([override])
+ end
+ end
+
+ describe "#lock" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile) { bundled_app_gemfile }
+ allow(Bundler).to receive(:ui) { double("UI", info: "", debug: "") }
+ end
+
+ subject { Bundler::Definition.new(bundled_app_lock, [], Bundler::SourceList.new, {}) }
+
+ context "when it's not possible to write to the file" do
+ it "raises an PermissionError with explanation" do
+ allow(File).to receive(:open).and_call_original
+ expect(File).to receive(:open).with(bundled_app_lock, "wb").
+ and_raise(Errno::EACCES.new(bundled_app_lock.to_s))
+ expect { subject.lock }.
+ to raise_error(Bundler::PermissionError, /Gemfile\.lock/)
+ end
+ end
+ context "when a temporary resource access issue occurs" do
+ it "raises a TemporaryResourceError with explanation" do
+ allow(File).to receive(:open).and_call_original
+ expect(File).to receive(:open).with(bundled_app_lock, "wb").
+ and_raise(Errno::EAGAIN)
+ expect { subject.lock }.
+ to raise_error(Bundler::TemporaryResourceError, /temporarily unavailable/)
+ end
+ end
+ context "when Bundler::Definition.no_lock is set to true" do
+ before { Bundler::Definition.no_lock = true }
+ after { Bundler::Definition.no_lock = false }
+
+ it "does not create a lockfile" do
+ subject.lock
+ expect(bundled_app_lock).not_to be_file
+ end
+ end
+ end
+
+ describe "detects changes" do
+ it "for a path gem with changes" do
+ build_lib "foo", "1.0", path: lib_path("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ bundle :install, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/re-resolving dependencies/)
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "with an explicit update" do
+ build_repo4 do
+ build_gem("ffi", "1.9.23") {|s| s.platform = "java" }
+ build_gem("ffi", "1.9.23")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "ffi"
+ G
+
+ bundle "lock --add-platform java"
+
+ bundle "update ffi", env: { "DEBUG" => "1" }
+
+ expect(out).to match(/because bundler is unlocking gems: \(ffi\)/)
+ end
+
+ it "for a path gem with deps and no changes" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ s.add_development_dependency "net-ssh", "1.0"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ expected_lockfile = <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(lockfile).to eq(expected_lockfile)
+
+ bundle :check, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(lockfile).to eq(expected_lockfile)
+ end
+
+ it "for a locked gem for another platform" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "only_java", platform: :jruby
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "only_java", "1.1", "java"
+ end
+
+ bundle "lock --add-platform java"
+ bundle :check, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ only_java (1.1-java)
+
+ PLATFORMS
+ #{lockfile_platforms("java")}
+
+ DEPENDENCIES
+ only_java
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "for a rubygems gem" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ bundle :check, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+
+ describe "initialize" do
+ context "gem version promoter" do
+ context "eager unlock" do
+ let(:source_list) do
+ Bundler::SourceList.new.tap do |source_list|
+ source_list.add_global_rubygems_remote("https://gem.repo4")
+ end
+ end
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'isolated_owner'
+
+ gem 'shared_owner_a'
+ gem 'shared_owner_b'
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ isolated_dep (2.0.1)
+ isolated_owner (1.0.1)
+ isolated_dep (~> 2.0)
+ shared_dep (5.0.1)
+ shared_owner_a (3.0.1)
+ shared_dep (~> 5.0)
+ shared_owner_b (4.0.1)
+ shared_dep (~> 5.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ shared_owner_a
+ shared_owner_b
+ isolated_owner
+
+ BUNDLED WITH
+ 1.13.0
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "should not eagerly unlock shared dependency with bundle install conservative updating behavior" do
+ updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"),
+ Bundler::Dependency.new("shared_owner_a", "3.0.2"),
+ Bundler::Dependency.new("shared_owner_b", ">= 0")]
+ unlock_hash_for_bundle_install = {}
+ definition = Bundler::Definition.new(
+ bundled_app_lock,
+ updated_deps_in_gemfile,
+ source_list,
+ unlock_hash_for_bundle_install
+ )
+ locked = definition.send(:converge_locked_specs).map(&:name)
+ expect(locked).to include "shared_dep"
+ end
+
+ it "should not eagerly unlock shared dependency with bundle update conservative updating behavior" do
+ updated_deps_in_gemfile = [Bundler::Dependency.new("isolated_owner", ">= 0"),
+ Bundler::Dependency.new("shared_owner_a", ">= 0"),
+ Bundler::Dependency.new("shared_owner_b", ">= 0")]
+ definition = Bundler::Definition.new(
+ bundled_app_lock,
+ updated_deps_in_gemfile,
+ source_list,
+ gems: ["shared_owner_a"], conservative: true
+ )
+ locked = definition.send(:converge_locked_specs).map(&:name)
+ expect(locked).to eq %w[isolated_dep isolated_owner shared_dep shared_owner_b]
+ expect(locked.include?("shared_dep")).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe "#precompute_source_requirements_for_indirect_dependencies?" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile) { Pathname.new("Gemfile") }
+ end
+
+ let(:sources) { Bundler::SourceList.new }
+ subject { Bundler::Definition.new(nil, [], sources, []) }
+
+ before do
+ allow(sources).to receive(:non_global_rubygems_sources).and_return(non_global_rubygems_sources)
+ end
+
+ context "when all the scoped sources implement a dependency API" do
+ let(:non_global_rubygems_sources) do
+ [
+ double("non-global-source-0", "dependency_api_available?":true, to_s:"a"),
+ double("non-global-source-1", "dependency_api_available?":true, to_s:"b"),
+ ]
+ end
+
+ it "returns true without warning" do
+ expect(subject).not_to receive(:non_dependency_api_warning)
+
+ expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_truthy
+ end
+ end
+
+ context "when some scoped sources do not implement a dependency API" do
+ let(:non_global_rubygems_sources) do
+ [
+ double("non-global-source-0", "dependency_api_available?":true, to_s:"a"),
+ double("non-global-source-1", "dependency_api_available?":false, to_s:"b"),
+ double("non-global-source-2", "dependency_api_available?":false, to_s:"c"),
+ ]
+ end
+
+ it "returns false and warns about the non-API sources" do
+ expect(Bundler.ui).to receive(:warn).with(<<-W.strip)
+Your Gemfile contains scoped sources that don't implement a dependency API, namely:
+
+ * b
+ * c
+
+Using the above gem servers may result in installing unexpected gems. To resolve this warning, make sure you use gem servers that implement dependency APIs, such as gemstash or geminabox gem servers.
+ W
+
+ expect(subject.send(:precompute_source_requirements_for_indirect_dependencies?)).to be_falsy
+ end
+ end
+ end
+
+ def mock_source_list
+ Class.new do
+ def all_sources
+ []
+ end
+
+ def path_sources
+ []
+ end
+
+ def replace_sources!(arg)
+ nil
+ end
+ end.new
+ end
+end
diff --git a/spec/bundler/bundler/dependency_spec.rb b/spec/bundler/bundler/dependency_spec.rb
new file mode 100644
index 0000000000..f930459571
--- /dev/null
+++ b/spec/bundler/bundler/dependency_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Dependency do
+ let(:options) do
+ {}
+ end
+ let(:dependency) do
+ described_class.new(
+ "test_gem",
+ "1.0.0",
+ options
+ )
+ end
+
+ describe "to_lock" do
+ it "returns formatted string" do
+ expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)")
+ end
+
+ it "matches format of Gem::Dependency#to_lock" do
+ gem_dependency = Gem::Dependency.new("test_gem", "1.0.0")
+ expect(dependency.to_lock).to eq(gem_dependency.to_lock)
+ end
+
+ context "when source is passed" do
+ let(:options) do
+ {
+ "source" => Bundler::Source::Git.new({}),
+ }
+ end
+
+ it "returns formatted string with exclamation mark" do
+ expect(dependency.to_lock).to eq(" test_gem (= 1.0.0)!")
+ end
+ end
+ end
+
+ it "is on the current platform" do
+ engine = Gem.win_platform? ? "windows" : RUBY_ENGINE
+
+ dep = described_class.new(
+ "test_gem",
+ "1.0.0",
+ { "platforms" => "#{engine}_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}" },
+ )
+
+ expect(dep.current_platform?).to be_truthy
+ end
+end
diff --git a/spec/bundler/bundler/digest_spec.rb b/spec/bundler/bundler/digest_spec.rb
new file mode 100644
index 0000000000..f876827964
--- /dev/null
+++ b/spec/bundler/bundler/digest_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "openssl"
+require "bundler/digest"
+
+RSpec.describe Bundler::Digest do
+ context "SHA1" do
+ subject { Bundler::Digest }
+ let(:stdlib) { OpenSSL::Digest::SHA1 }
+
+ it "is compatible with stdlib" do
+ random_strings = ["foo", "skfjsdlkfjsdf", "3924m", "ldskfj"]
+
+ # https://www.rfc-editor.org/rfc/rfc3174#section-7.3
+ rfc3174_test_cases = ["abc", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", "a", "01234567" * 8]
+
+ (random_strings + rfc3174_test_cases).each do |payload|
+ sha1 = subject.sha1(payload)
+ sha1_stdlib = stdlib.hexdigest(payload)
+ expect(sha1).to be == sha1_stdlib, "#{payload}'s sha1 digest (#{sha1}) did not match stlib's result (#{sha1_stdlib})"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/dsl_spec.rb b/spec/bundler/bundler/dsl_spec.rb
new file mode 100644
index 0000000000..b6e67a312c
--- /dev/null
+++ b/spec/bundler/bundler/dsl_spec.rb
@@ -0,0 +1,558 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Dsl do
+ before do
+ @rubygems = double("rubygems")
+ allow(Bundler::Source::Rubygems).to receive(:new) { @rubygems }
+ end
+
+ describe "#git_source" do
+ it "registers custom hosts" do
+ subject.git_source(:example) {|repo_name| "git@git.example.com:#{repo_name}.git" }
+ subject.git_source(:foobar) {|repo_name| "git@foobar.com:#{repo_name}.git" }
+ subject.gem("dobry-pies", example: "strzalek/dobry-pies")
+ example_uri = "git@git.example.com:strzalek/dobry-pies.git"
+ expect(subject.dependencies.first.source.uri).to eq(example_uri)
+ end
+
+ it "raises exception on invalid hostname" do
+ expect do
+ subject.git_source(:group) {|repo_name| "git@git.example.com:#{repo_name}.git" }
+ end.to raise_error(Bundler::InvalidOption)
+ end
+
+ it "expects block passed" do
+ expect { subject.git_source(:example) }.to raise_error(Bundler::InvalidOption)
+ end
+
+ it "converts :github PR to URI using https" do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5")
+ github_uri = "https://github.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ expect(subject.dependencies.first.source.ref).to eq("refs/pull/5/head")
+ end
+
+ it "converts :gitlab PR to URI using https" do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5")
+ gitlab_uri = "https://gitlab.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(gitlab_uri)
+ expect(subject.dependencies.first.source.ref).to eq("refs/merge-requests/5/head")
+ end
+
+ it "rejects :github PR URI with a branch, ref or tag" do
+ expect do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", branch: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :branch option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", ref: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :ref option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", github: "https://github.com/indirect/sparks/pull/5", tag: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :tag option can't be used with `github: "https://github.com/indirect/sparks/pull/5"`),
+ )
+ end
+
+ it "rejects :gitlab PR URI with a branch, ref or tag" do
+ expect do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", branch: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :branch option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", ref: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :ref option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`),
+ )
+
+ expect do
+ subject.gem("sparks", gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5", tag: "foo")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :tag option can't be used with `gitlab: "https://gitlab.com/indirect/sparks/-/merge_requests/5"`),
+ )
+ end
+
+ it "rejects :github with :git" do
+ expect do
+ subject.gem("sparks", github: "indirect/sparks", git: "https://github.com/indirect/sparks.git")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :git option can't be used with `github: "indirect/sparks"`),
+ )
+ end
+
+ it "rejects :gitlab with :git" do
+ expect do
+ subject.gem("sparks", gitlab: "indirect/sparks", git: "https://gitlab.com/indirect/sparks.git")
+ end.to raise_error(
+ Bundler::GemfileError,
+ %(The :git option can't be used with `gitlab: "indirect/sparks"`),
+ )
+ end
+
+ context "default hosts" do
+ it "converts :github to URI using https" do
+ subject.gem("sparks", github: "indirect/sparks")
+ github_uri = "https://github.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :github shortcut to URI using https" do
+ subject.gem("sparks", github: "rails")
+ github_uri = "https://github.com/rails/rails.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :gitlab to URI using https" do
+ subject.gem("sparks", gitlab: "indirect/sparks")
+ gitlab_uri = "https://gitlab.com/indirect/sparks.git"
+ expect(subject.dependencies.first.source.uri).to eq(gitlab_uri)
+ end
+
+ it "converts :gitlab shortcut to URI using https" do
+ subject.gem("sparks", gitlab: "rails")
+ gitlab_uri = "https://gitlab.com/rails/rails.git"
+ expect(subject.dependencies.first.source.uri).to eq(gitlab_uri)
+ end
+
+ it "converts numeric :gist to :git" do
+ subject.gem("not-really-a-gem", gist: 2_859_988)
+ github_uri = "https://gist.github.com/2859988.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :gist to :git" do
+ subject.gem("not-really-a-gem", gist: "2859988")
+ github_uri = "https://gist.github.com/2859988.git"
+ expect(subject.dependencies.first.source.uri).to eq(github_uri)
+ end
+
+ it "converts :bitbucket to :git" do
+ subject.gem("not-really-a-gem", bitbucket: "mcorp/flatlab-rails")
+ bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/flatlab-rails.git"
+ expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri)
+ end
+
+ it "converts 'mcorp' to 'mcorp/mcorp'" do
+ subject.gem("not-really-a-gem", bitbucket: "mcorp")
+ bitbucket_uri = "https://mcorp@bitbucket.org/mcorp/mcorp.git"
+ expect(subject.dependencies.first.source.uri).to eq(bitbucket_uri)
+ end
+ end
+
+ context "default git sources" do
+ it "has bitbucket, gist, github, and gitlab" do
+ expect(subject.instance_variable_get(:@git_sources).keys.sort).to eq(%w[bitbucket gist github gitlab])
+ end
+ end
+ end
+
+ describe "#method_missing" do
+ it "raises an error for unknown DSL methods" do
+ expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).
+ and_return("unknown")
+
+ error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue."
+ expect { subject.eval_gemfile("Gemfile") }.
+ to raise_error(Bundler::GemfileError, Regexp.new(error_msg))
+ end
+ end
+
+ describe "#eval_gemfile" do
+ it "handles syntax errors with a useful message" do
+ expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).and_return("}")
+ expect { subject.eval_gemfile("Gemfile") }.
+ to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '\}'|.+?unexpected '}', ignoring it\n). Bundler cannot continue./m)
+ end
+
+ it "distinguishes syntax errors from evaluation errors" do
+ expect(Bundler).to receive(:read_file).with(git_root.join("Gemfile").to_s).and_return(
+ "ruby '2.1.5', :engine => 'ruby', :engine_version => '1.2.4'"
+ )
+ expect { subject.eval_gemfile("Gemfile") }.
+ to raise_error(Bundler::GemfileError, /There was an error evaluating `Gemfile`: ruby_version must match the :engine_version for MRI/)
+ end
+
+ it "populates __dir__ and __FILE__ correctly" do
+ abs_path = git_root.join("../fragment.rb").to_s
+ expect(Bundler).to receive(:read_file).with(abs_path).and_return(<<~RUBY)
+ @fragment_dir = __dir__
+ @fragment_file = __FILE__
+ RUBY
+ subject.eval_gemfile("../fragment.rb")
+ expect(subject.instance_variable_get(:@fragment_dir)).to eq(git_root.dirname.to_s)
+ expect(subject.instance_variable_get(:@fragment_file)).to eq(abs_path)
+ end
+ end
+
+ describe "#gem" do
+ # rubocop:disable Naming/VariableNumber
+ [:ruby, :ruby_18, :ruby_19, :ruby_20, :ruby_21, :ruby_22, :ruby_23, :ruby_24, :ruby_25, :ruby_26, :ruby_27,
+ :ruby_30, :ruby_31, :ruby_32, :ruby_33, :ruby_34, :ruby_40, :mri, :mri_18, :mri_19, :mri_20, :mri_21, :mri_22, :mri_23, :mri_24,
+ :mri_25, :mri_26, :mri_27, :mri_30, :mri_31, :mri_32, :mri_33, :mri_34, :mri_40, :jruby, :rbx, :truffleruby].each do |platform|
+ it "allows #{platform} as a valid platform" do
+ subject.gem("foo", platform: platform)
+ end
+ end
+ # rubocop:enable Naming/VariableNumber
+
+ it "allows platforms matching the running Ruby version" do
+ platform = "ruby_#{RbConfig::CONFIG["MAJOR"]}#{RbConfig::CONFIG["MINOR"]}"
+
+ expect { subject.gem("foo", platform: platform) }.not_to raise_error
+ expect(Bundler.current_ruby.respond_to?("#{platform}?")).to be_truthy
+ end
+
+ it "rejects invalid platforms" do
+ expect { subject.gem("foo", platform: :bogus) }.
+ to raise_error(Bundler::GemfileError, /is not a valid platform/)
+ end
+
+ it "warn for legacy windows platforms" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin, :x64_mingw will be removed in the future./)
+ subject.gem("foo", platforms: [:mswin, :jruby, :x64_mingw])
+ end
+
+ it "rejects empty gem name" do
+ expect { subject.gem("") }.
+ to raise_error(Bundler::GemfileError, /an empty gem name is not valid/)
+ end
+
+ it "rejects with a leading space in the name" do
+ expect { subject.gem(" foo") }.
+ to raise_error(Bundler::GemfileError, /' foo' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a trailing space in the name" do
+ expect { subject.gem("foo ") }.
+ to raise_error(Bundler::GemfileError, /'foo ' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a space in the gem name" do
+ expect { subject.gem("fo o") }.
+ to raise_error(Bundler::GemfileError, /'fo o' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a tab in the gem name" do
+ expect { subject.gem("fo\to") }.
+ to raise_error(Bundler::GemfileError, /'fo\to' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a newline in the gem name" do
+ expect { subject.gem("fo\no") }.
+ to raise_error(Bundler::GemfileError, /'fo\no' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a carriage return in the gem name" do
+ expect { subject.gem("fo\ro") }.
+ to raise_error(Bundler::GemfileError, /'fo\ro' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects with a form feed in the gem name" do
+ expect { subject.gem("fo\fo") }.
+ to raise_error(Bundler::GemfileError, /'fo\fo' is not a valid gem name because it contains whitespace/)
+ end
+
+ it "rejects symbols as gem name" do
+ expect { subject.gem(:foo) }.
+ to raise_error(Bundler::GemfileError, /You need to specify gem names as Strings. Use 'gem "foo"' instead/)
+ end
+
+ it "rejects branch option on non-git gems" do
+ expect { subject.gem("foo", branch: "test") }.
+ to raise_error(Bundler::GemfileError, /The `branch` option for `gem 'foo'` is not allowed. Only gems with a git source can specify a branch/)
+ end
+
+ it "allows specifying a branch on git gems" do
+ subject.gem("foo", branch: "test", git: "http://mytestrepo")
+ dep = subject.dependencies.last
+ expect(dep.name).to eq "foo"
+ end
+
+ it "allows specifying a branch on git gems with a git_source" do
+ subject.git_source(:test_source) {|n| "https://github.com/#{n}" }
+ subject.gem("foo", branch: "test", test_source: "bundler/bundler")
+ dep = subject.dependencies.last
+ expect(dep.name).to eq "foo"
+ end
+ end
+
+ describe "#platforms" do
+ it "warn for legacy windows platforms" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(/\APlatform :mswin64, :mingw will be removed in the future./)
+ subject.platforms(:mswin64, :jruby, :mingw) do
+ subject.gem("foo")
+ end
+ end
+ end
+
+ context "can bundle groups of gems with" do
+ # git "https://github.com/rails/rails.git" do
+ # gem "railties"
+ # gem "action_pack"
+ # gem "active_model"
+ # end
+ describe "#git" do
+ it "from a single repo" do
+ rails_gems = %w[railties action_pack active_model]
+ subject.git "https://github.com/rails/rails.git" do
+ rails_gems.each {|rails_gem| subject.send :gem, rails_gem }
+ end
+ expect(subject.dependencies.map(&:name)).to match_array rails_gems
+ end
+ end
+
+ # github 'spree' do
+ # gem 'spree_core'
+ # gem 'spree_api'
+ # gem 'spree_backend'
+ # end
+ describe "#github" do
+ it "from github" do
+ spree_gems = %w[spree_core spree_api spree_backend]
+ subject.github "spree" do
+ spree_gems.each {|spree_gem| subject.send :gem, spree_gem }
+ end
+
+ subject.dependencies.each do |d|
+ expect(d.source.uri).to eq("https://github.com/spree/spree.git")
+ end
+ end
+ end
+ end
+
+ describe "syntax errors" do
+ it "will raise a Bundler::GemfileError" do
+ gemfile "gem 'foo', :path => /unquoted/string/syntax/error"
+ expect { Bundler::Dsl.evaluate(bundled_app_gemfile, nil, true) }.
+ to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)?.+?unknown regexp options - trg.+ Bundler cannot continue./m)
+ end
+ end
+
+ describe "Runtime errors" do
+ it "will raise a Bundler::GemfileError" do
+ gemfile "raise RuntimeError, 'foo'"
+ expect { Bundler::Dsl.evaluate(bundled_app_gemfile, nil, true) }.
+ to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: foo. Bundler cannot continue./i)
+ end
+ end
+
+ describe "#with_source" do
+ context "if there was a rubygem source already defined" do
+ it "restores it after it's done" do
+ other_source = double("other-source")
+ allow(Bundler::Source::Rubygems).to receive(:new).and_return(other_source)
+ allow(Bundler).to receive(:default_gemfile).and_return(Pathname.new("./Gemfile"))
+
+ subject.source("https://other-source.org") do
+ subject.gem("dobry-pies", path: "foo")
+ subject.gem("foo")
+ end
+
+ expect(subject.dependencies.last.source).to eq(other_source)
+ end
+ end
+ end
+
+ describe "#source with cooldown" do
+ before do
+ allow(@rubygems).to receive(:add_remote)
+ end
+
+ it "accepts a non-negative integer" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: 7)
+ end.not_to raise_error
+ end
+
+ it "accepts 0 as an explicit disable" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: 0)
+ end.not_to raise_error
+ end
+
+ it "rejects a string" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: "7")
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+
+ it "rejects a float" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: 7.5)
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+
+ it "rejects a negative integer" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: -7)
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+
+ it "rejects an array" do
+ expect do
+ subject.source("https://rubygems.org", cooldown: [7])
+ end.to raise_error(Bundler::InvalidOption, /non-negative integer/)
+ end
+ end
+
+ describe "#override" do
+ it "stores an Override for a gem with a version: operation" do
+ subject.override("rails", version: ">= 8.0")
+
+ expect(subject.overrides.size).to eq(1)
+ override = subject.overrides.first
+ expect(override.target).to eq("rails")
+ expect(override.field).to eq(:version)
+ expect(override.operation).to eq(">= 8.0")
+ end
+
+ it "accepts :ignore_upper as the operation" do
+ subject.override("nokogiri", version: :ignore_upper)
+ expect(subject.overrides.first.operation).to eq(:ignore_upper)
+ end
+
+ it "accepts nil as the operation" do
+ subject.override("legacy", version: nil)
+ expect(subject.overrides.first.operation).to be_nil
+ end
+
+ it "appends to overrides across multiple statements" do
+ subject.override("rails", version: ">= 8.0")
+ subject.override("nokogiri", version: :ignore_upper)
+ expect(subject.overrides.map(&:target)).to eq(["rails", "nokogiri"])
+ end
+
+ it "is empty by default" do
+ expect(subject.overrides).to eq([])
+ end
+
+ it "raises ArgumentError when target is :all and version: is given" do
+ expect do
+ subject.override(:all, version: ">= 8.0")
+ end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/)
+ end
+
+ it "rejects :all + version: even when other fields are also given" do
+ expect do
+ subject.override(:all, required_ruby_version: :ignore_upper, version: ">= 8.0")
+ end.to raise_error(ArgumentError, /`override :all, version:` is not allowed/)
+ end
+
+ it "does not record any override when :all + version: is rejected" do
+ expect do
+ subject.override(:all, version: ">= 8.0")
+ end.to raise_error(ArgumentError)
+ expect(subject.overrides).to eq([])
+ end
+
+ it "raises ArgumentError when target is neither :all nor a string" do
+ expect do
+ subject.override(:rails, version: ">= 8.0")
+ end.to raise_error(ArgumentError, /target must be :all or a gem name string/)
+ end
+
+ it "raises ArgumentError for an unsupported field" do
+ expect do
+ subject.override("rails", as: "y")
+ end.to raise_error(ArgumentError, /unsupported override field `as:`/)
+ end
+
+ it "stores an Override for a gem with a required_ruby_version: operation" do
+ subject.override("rails", required_ruby_version: :ignore_upper)
+ override = subject.overrides.first
+ expect(override.target).to eq("rails")
+ expect(override.field).to eq(:required_ruby_version)
+ expect(override.operation).to eq(:ignore_upper)
+ end
+
+ it "stores an Override for a gem with a required_rubygems_version: operation" do
+ subject.override("rails", required_rubygems_version: nil)
+ override = subject.overrides.first
+ expect(override.field).to eq(:required_rubygems_version)
+ expect(override.operation).to be_nil
+ end
+
+ it "stores an Override targeting :all with a metadata field" do
+ subject.override(:all, required_ruby_version: :ignore_upper)
+ override = subject.overrides.first
+ expect(override.target).to eq(:all)
+ expect(override.field).to eq(:required_ruby_version)
+ expect(override.operation).to eq(:ignore_upper)
+ end
+
+ it "stores an Override targeting :all with required_rubygems_version" do
+ subject.override(:all, required_rubygems_version: nil)
+ override = subject.overrides.first
+ expect(override.target).to eq(:all)
+ expect(override.field).to eq(:required_rubygems_version)
+ end
+
+ it "raises ArgumentError for a non-string, non-symbol, non-nil operation" do
+ expect do
+ subject.override("rails", version: 42)
+ end.to raise_error(ArgumentError, /override operation must be a String, Symbol, or nil/)
+ end
+
+ it "raises ArgumentError for an unsupported symbol operation" do
+ expect do
+ subject.override("rails", version: :explode)
+ end.to raise_error(ArgumentError, /unsupported override operation/)
+ end
+
+ it "raises ArgumentError for an unparsable version string" do
+ expect do
+ subject.override("rails", version: "not a version")
+ end.to raise_error(ArgumentError, /invalid override version requirement/)
+ end
+
+ it "does not record an override when the version string is invalid" do
+ expect do
+ subject.override("rails", version: "not a version")
+ end.to raise_error(ArgumentError)
+ expect(subject.overrides).to eq([])
+ end
+
+ it "rejects atomically when one field in a multi-field call is invalid" do
+ expect do
+ subject.override("rails", version: ">= 8.0", as: "y")
+ end.to raise_error(ArgumentError, /unsupported override field/)
+ expect(subject.overrides).to eq([])
+ end
+
+ it "raises ArgumentError when the same target and field are overridden twice" do
+ subject.override("rails", version: ">= 8.0")
+ expect do
+ subject.override("rails", version: :ignore_upper)
+ end.to raise_error(ArgumentError, /duplicate override for "rails" `version:`/)
+ end
+
+ it "keeps the original override when a duplicate is rejected" do
+ subject.override("rails", version: ">= 8.0")
+ expect do
+ subject.override("rails", version: :ignore_upper)
+ end.to raise_error(ArgumentError)
+ expect(subject.overrides.size).to eq(1)
+ expect(subject.overrides.first.operation).to eq(">= 8.0")
+ end
+
+ it "allows different targets with the same field" do
+ subject.override("rails", version: ">= 8.0")
+ subject.override("nokogiri", version: :ignore_upper)
+ expect(subject.overrides.size).to eq(2)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/endpoint_specification_spec.rb b/spec/bundler/bundler/endpoint_specification_spec.rb
new file mode 100644
index 0000000000..4fbd59d48f
--- /dev/null
+++ b/spec/bundler/bundler/endpoint_specification_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::EndpointSpecification do
+ let(:name) { "foo" }
+ let(:version) { "1.0.0" }
+ let(:platform) { Gem::Platform::RUBY }
+ let(:dependencies) { [] }
+ let(:spec_fetcher) { double(:spec_fetcher) }
+ let(:metadata) { nil }
+
+ subject(:spec) { described_class.new(name, version, platform, spec_fetcher, dependencies, metadata) }
+
+ describe "#build_dependency" do
+ let(:name) { "foo" }
+ let(:requirement1) { "~> 1.1" }
+ let(:requirement2) { ">= 1.1.7" }
+
+ it "should return a Gem::Dependency" do
+ expect(subject.send(:build_dependency, name, [requirement1, requirement2])).
+ to eq(Gem::Dependency.new(name, requirement1, requirement2))
+ end
+
+ context "when an ArgumentError occurs" do
+ before do
+ allow(Gem::Dependency).to receive(:new).with(name, [requirement1, requirement2]) {
+ raise ArgumentError.new("Some error occurred")
+ }
+ end
+
+ it "should raise the original error" do
+ expect { subject.send(:build_dependency, name, [requirement1, requirement2]) }.to raise_error(
+ ArgumentError, "Some error occurred"
+ )
+ end
+ end
+ end
+
+ describe "#parse_metadata" do
+ context "when the metadata has malformed requirements" do
+ let(:metadata) { { "rubygems" => ">\n" } }
+ it "raises a helpful error message" do
+ expect { subject }.to raise_error(
+ Bundler::GemspecError,
+ a_string_including("There was an error parsing the metadata for the gem foo (1.0.0)").
+ and(a_string_including("The metadata was #{{ "rubygems" => ">\n" }.inspect}"))
+ )
+ end
+ end
+
+ context "when the metadata has created_at" do
+ let(:metadata) { { "created_at" => ["2026-05-12T10:00:00Z"] } }
+
+ it "parses created_at as a Time" do
+ expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0))
+ end
+ end
+
+ context "when the metadata has a string created_at (older rubygems shape)" do
+ let(:metadata) { { "created_at" => "2026-05-12T10:00:00Z" } }
+
+ it "still parses created_at" do
+ expect(subject.created_at).to eq(Time.utc(2026, 5, 12, 10, 0, 0))
+ end
+ end
+
+ context "when created_at is truncated (older rubygems splits on colons)" do
+ let(:metadata) { { "created_at" => "2026-05-12T10" } }
+
+ it "leaves created_at as nil instead of raising" do
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context "when the metadata has no created_at" do
+ let(:metadata) { { "checksum" => ["abc"] } }
+ let(:spec_fetcher) { double(:spec_fetcher, uri: "https://rubygems.org") }
+
+ it "leaves created_at as nil" do
+ allow(Bundler::Checksum).to receive(:from_api).and_return(nil)
+ expect(subject.created_at).to be_nil
+ end
+ end
+
+ context "when the metadata is nil" do
+ it "leaves created_at as nil" do
+ expect(subject.created_at).to be_nil
+ end
+ end
+ end
+
+ describe "#required_ruby_version" do
+ context "required_ruby_version is already set on endpoint specification" do
+ existing_value = "already set value"
+ let(:required_ruby_version) { existing_value }
+
+ it "should return the current value when already set on endpoint specification" do
+ expect(spec.required_ruby_version). eql?(existing_value)
+ end
+ end
+
+ it "should return the remote spec value when not set on endpoint specification and remote spec has one" do
+ remote_value = "remote_value"
+ remote_spec = double(:remote_spec, required_ruby_version: remote_value, required_rubygems_version: nil)
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+
+ expect(spec.required_ruby_version). eql?(remote_value)
+ end
+
+ it "should use the default Gem Requirement value when not set on endpoint specification and not set on remote spec" do
+ remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil)
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+ expect(spec.required_ruby_version). eql?(Gem::Requirement.default)
+ end
+ end
+
+ it "supports equality comparison" do
+ remote_spec = double(:remote_spec, required_ruby_version: nil, required_rubygems_version: nil)
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+ other_spec = described_class.new("bar", version, platform, spec_fetcher, dependencies, metadata)
+ expect(spec).to eql(spec)
+ expect(spec).to_not eql(other_spec)
+ end
+end
diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb
new file mode 100644
index 0000000000..2b7dbde217
--- /dev/null
+++ b/spec/bundler/bundler/env_spec.rb
@@ -0,0 +1,237 @@
+# frozen_string_literal: true
+
+require "bundler/settings"
+require "openssl"
+
+RSpec.describe Bundler::Env do
+ let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil) }
+
+ describe "#report" do
+ it "prints the environment" do
+ out = described_class.report
+
+ expect(out).to include("Environment")
+ expect(out).to include(Bundler::VERSION)
+ expect(out).to include(Gem::VERSION)
+ expect(out).to include(described_class.send(:ruby_version))
+ expect(out).to include(described_class.send(:git_version))
+ expect(out).to include(OpenSSL::OPENSSL_VERSION)
+ end
+
+ describe "rubygems paths" do
+ it "prints gem home" do
+ with_clear_paths("GEM_HOME", "/a/b/c") do
+ out = described_class.report
+ expect(out).to include("Gem Home /a/b/c")
+ end
+ end
+
+ it "prints gem path" do
+ with_clear_paths("GEM_PATH", "/a/b/c#{File::PATH_SEPARATOR}d/e/f") do
+ out = described_class.report
+ expect(out).to include("Gem Path /a/b/c#{File::PATH_SEPARATOR}d/e/f")
+ end
+ end
+
+ it "prints user home" do
+ with_clear_paths("HOME", "/a/b/c") do
+ out = described_class.report
+ expect(out).to include("User Home /a/b/c")
+ end
+ end
+
+ it "prints user path" do
+ with_clear_paths("HOME", "/a/b/c") do
+ allow(File).to receive(:exist?)
+ allow(File).to receive(:exist?).with("/a/b/c/.gem").and_return(true)
+ out = described_class.report
+ expect(out).to include("User Path /a/b/c/.gem")
+ end
+ end
+
+ it "prints bin dir" do
+ with_clear_paths("GEM_HOME", "/a/b/c") do
+ out = described_class.report
+ expect(out).to include("Bin Dir /a/b/c/bin")
+ end
+ end
+
+ private
+
+ def with_clear_paths(env_var, env_value)
+ old_env_var = ENV[env_var]
+ ENV[env_var] = env_value
+ Gem.clear_paths
+ yield
+ ensure
+ ENV[env_var] = old_env_var
+ end
+ end
+
+ context "when there is a Gemfile and a lockfile and print_gemfile is true" do
+ before do
+ gemfile "source 'https://gem.repo1'; gem 'myrack', '1.0.0'"
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ 1.10.0
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the Gemfile" do
+ expect(output).to include("Gemfile")
+ expect(output).to include("'myrack', '1.0.0'")
+ end
+
+ it "prints the lockfile" do
+ expect(output).to include("Gemfile.lock")
+ expect(output).to include("myrack (1.0.0)")
+ end
+ end
+
+ context "when there no Gemfile and print_gemfile is true" do
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the environment" do
+ expect(output).to start_with("## Environment")
+ end
+ end
+
+ context "when there's bundler config with credentials" do
+ before do
+ bundle "config set https://localgemserver.test/ user:pass"
+ end
+
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the config with redacted values" do
+ expect(output).to include("https://localgemserver.test")
+ expect(output).to include("user:[REDACTED]")
+ expect(output).to_not include("user:pass")
+ end
+ end
+
+ context "when there's bundler config with OAuth token credentials" do
+ before do
+ bundle "config set https://localgemserver.test/ api_token:x-oauth-basic"
+ end
+
+ let(:output) { described_class.report(print_gemfile: true) }
+
+ it "prints the config with redacted values" do
+ expect(output).to include("https://localgemserver.test")
+ expect(output).to include("[REDACTED]:x-oauth-basic")
+ expect(output).to_not include("api_token:x-oauth-basic")
+ end
+ end
+
+ context "when Gemfile contains a gemspec and print_gemspecs is true" do
+ let(:gemspec) do
+ <<~GEMSPEC
+ Gem::Specification.new do |gem|
+ gem.name = "foo"
+ gem.author = "Fumofu"
+ end
+ GEMSPEC
+ end
+
+ before do
+ gemfile("source 'https://gem.repo1'; gemspec")
+
+ File.open(bundled_app("foo.gemspec"), "wb") do |f|
+ f.write(gemspec)
+ end
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "prints the gemspec" do
+ output = described_class.report(print_gemspecs: true)
+
+ expect(output).to include("foo.gemspec")
+ expect(output).to include(gemspec)
+ end
+ end
+
+ context "when eval_gemfile is used" do
+ it "prints all gemfiles" do
+ gemfile bundled_app("other/Gemfile-other"), "gem 'myrack'"
+ gemfile bundled_app("other/Gemfile"), "eval_gemfile 'Gemfile-other'"
+ gemfile bundled_app("Gemfile-alt"), <<-G
+ source "https://gem.repo1"
+ eval_gemfile "other/Gemfile"
+ G
+ gemfile "eval_gemfile #{bundled_app("Gemfile-alt").to_s.dump}"
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app)
+
+ output = described_class.report(print_gemspecs: true)
+ expect(output).to include(<<~ENV)
+ ## Gemfile
+
+ ### Gemfile
+
+ ```ruby
+ eval_gemfile #{bundled_app("Gemfile-alt").to_s.dump}
+ ```
+
+ ### Gemfile-alt
+
+ ```ruby
+ source "https://gem.repo1"
+ eval_gemfile "other/Gemfile"
+ ```
+
+ ### other/Gemfile
+
+ ```ruby
+ eval_gemfile 'Gemfile-other'
+ ```
+
+ ### other/Gemfile-other
+
+ ```ruby
+ gem 'myrack'
+ ```
+
+ ### Gemfile.lock
+
+ ```
+ <No #{bundled_app_lock} found>
+ ```
+ ENV
+ end
+ end
+
+ context "when the git version is OS specific" do
+ it "includes OS specific information with the version number" do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3 (Apple Git-BS)", "", status])
+ expect(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub)
+
+ expect(described_class.report).to include("Git 1.2.3 (Apple Git-BS)")
+ end
+ end
+
+ it "no longer reports the Tools section or external tool versions" do
+ report = described_class.report
+ expect(report).not_to include("Tools")
+ ["rbenv", "RVM", "chruby"].each do |tool|
+ expect(report).not_to include(tool)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/environment_preserver_spec.rb b/spec/bundler/bundler/environment_preserver_spec.rb
new file mode 100644
index 0000000000..6c7066d0c6
--- /dev/null
+++ b/spec/bundler/bundler/environment_preserver_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::EnvironmentPreserver do
+ let(:preserver) { described_class.new(env, ["foo"]) }
+
+ describe "#backup" do
+ let(:env) { { "foo" => "my-foo", "bar" => "my-bar" } }
+ subject { preserver.backup }
+
+ it "should create backup entries" do
+ expect(subject["BUNDLER_ORIG_foo"]).to eq("my-foo")
+ end
+
+ it "should keep the original entry" do
+ expect(subject["foo"]).to eq("my-foo")
+ end
+
+ it "should not create backup entries for unspecified keys" do
+ expect(subject.key?("BUNDLER_ORIG_bar")).to eq(false)
+ end
+
+ it "should not affect the original env" do
+ subject
+ expect(env.keys.sort).to eq(%w[bar foo])
+ end
+
+ context "when a key is empty" do
+ let(:env) { { "foo" => "" } }
+
+ it "should keep the original entry" do
+ expect(subject["foo"]).to be_empty
+ end
+
+ it "should still create backup entries" do
+ expect(subject).to have_key "BUNDLER_ORIG_foo"
+ end
+ end
+
+ context "when an original key is set" do
+ let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } }
+
+ it "should keep the original value in the BUNDLER_ORIG_ variable" do
+ expect(subject["BUNDLER_ORIG_foo"]).to eq("orig-foo")
+ end
+
+ it "should keep the variable" do
+ expect(subject["foo"]).to eq("my-foo")
+ end
+ end
+ end
+
+ describe "#restore" do
+ subject { preserver.restore }
+
+ context "when an original key is set" do
+ let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "orig-foo" } }
+
+ it "should restore the original value" do
+ expect(subject["foo"]).to eq("orig-foo")
+ end
+
+ it "should delete the backup value" do
+ expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false)
+ end
+ end
+
+ context "when no original key is set" do
+ let(:env) { { "foo" => "my-foo" } }
+
+ it "should keep the current value" do
+ expect(subject["foo"]).to eq("my-foo")
+ end
+ end
+
+ context "when the original key is empty" do
+ let(:env) { { "foo" => "my-foo", "BUNDLER_ORIG_foo" => "" } }
+
+ it "should restore the original value" do
+ expect(subject["foo"]).to be_empty
+ end
+
+ it "should delete the backup value" do
+ expect(subject.key?("BUNDLER_ORIG_foo")).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/errors_spec.rb b/spec/bundler/bundler/errors_spec.rb
new file mode 100644
index 0000000000..b62d85d32b
--- /dev/null
+++ b/spec/bundler/bundler/errors_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::IncorrectLockfileDependencies do
+ describe "#message" do
+ let(:spec) do
+ double("LazySpecification", full_name: "rubocop-1.82.0")
+ end
+
+ context "without dependency details" do
+ subject { described_class.new(spec) }
+
+ it "provides a basic error message" do
+ expect(subject.message).to include("Bundler found incorrect dependencies in the lockfile for rubocop-1.82.0")
+ expect(subject.message).to include("Please run `bundle install` to regenerate the lockfile.")
+ end
+ end
+
+ context "with dependency details" do
+ let(:actual_dependencies) do
+ [
+ Gem::Dependency.new("json", [">= 2.3", "< 4.0"]),
+ Gem::Dependency.new("parallel", ["~> 1.10"]),
+ Gem::Dependency.new("parser", [">= 3.3.0.2"]),
+ ]
+ end
+
+ let(:lockfile_dependencies) do
+ [
+ Gem::Dependency.new("json", [">= 2.3", "< 3.0"]),
+ Gem::Dependency.new("parallel", ["~> 1.10"]),
+ Gem::Dependency.new("parser", [">= 3.2.0.0"]),
+ ]
+ end
+
+ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) }
+
+ it "shows only mismatched dependencies" do
+ message = subject.message
+
+ expect(message).to include("json: gemspec specifies")
+ expect(message).to include("parser: gemspec specifies")
+ expect(message).not_to include("parallel")
+ end
+ end
+
+ context "when gemspec has dependencies but lockfile has none" do
+ let(:actual_dependencies) do
+ [
+ Gem::Dependency.new("myrack-test", ["~> 1.0"]),
+ ]
+ end
+
+ let(:lockfile_dependencies) { [] }
+
+ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) }
+
+ it "shows the dependency as not in lockfile" do
+ message = subject.message
+
+ expect(message).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile")
+ end
+ end
+
+ context "when gemspec has no dependencies but lockfile has some" do
+ let(:actual_dependencies) { [] }
+
+ let(:lockfile_dependencies) do
+ [
+ Gem::Dependency.new("unexpected", ["~> 1.0"]),
+ ]
+ end
+
+ subject { described_class.new(spec, actual_dependencies, lockfile_dependencies) }
+
+ it "shows the dependency as not in gemspec" do
+ message = subject.message
+
+ expect(message).to include("unexpected: not in gemspec, lockfile has ~> 1.0")
+ end
+ end
+ end
+
+ describe "#status_code" do
+ let(:spec) { double("LazySpecification", full_name: "test-1.0.0") }
+ subject { described_class.new(spec) }
+
+ it "returns 41" do
+ expect(subject.status_code).to eq(41)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/base_spec.rb b/spec/bundler/bundler/fetcher/base_spec.rb
new file mode 100644
index 0000000000..b8c6b57b10
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/base_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Fetcher::Base do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote) }
+ let(:display_uri) { "http://sample_uri.com" }
+ let(:gem_remote_fetcher) { nil }
+
+ class TestClass < described_class; end
+
+ subject { TestClass.new(downloader, remote, display_uri, gem_remote_fetcher) }
+
+ describe "#initialize" do
+ context "with the abstract Base class" do
+ it "should raise an error" do
+ expect { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }.to raise_error(RuntimeError, "Abstract class")
+ end
+ end
+
+ context "with a class that inherits the Base class" do
+ it "should set the passed attributes" do
+ expect(subject.downloader).to eq(downloader)
+ expect(subject.remote).to eq(remote)
+ expect(subject.display_uri).to eq("http://sample_uri.com")
+ end
+ end
+ end
+
+ describe "#remote_uri" do
+ let(:remote_uri_obj) { double(:remote_uri_obj) }
+
+ before { allow(remote).to receive(:uri).and_return(remote_uri_obj) }
+
+ it "should return the remote's uri" do
+ expect(subject.remote_uri).to eq(remote_uri_obj)
+ end
+ end
+
+ describe "#fetch_uri" do
+ let(:remote_uri_obj) { Gem::URI("http://rubygems.org") }
+
+ before { allow(subject).to receive(:remote_uri).and_return(remote_uri_obj) }
+
+ context "when the remote uri's host is rubygems.org" do
+ it "should create a copy of the remote uri with index.rubygems.org as the host" do
+ fetched_uri = subject.fetch_uri
+ expect(fetched_uri.host).to eq("index.rubygems.org")
+ expect(fetched_uri).to_not be(remote_uri_obj)
+ end
+ end
+
+ context "when the remote uri's host is not rubygems.org" do
+ let(:remote_uri_obj) { Gem::URI("http://otherhost.org") }
+
+ it "should return the remote uri" do
+ expect(subject.fetch_uri).to eq(Gem::URI("http://otherhost.org"))
+ end
+ end
+
+ it "memoizes the fetched uri" do
+ expect(remote_uri_obj).to receive(:host).once
+ 2.times { subject.fetch_uri }
+ end
+ end
+
+ describe "#available?" do
+ it "should return whether the api is available" do
+ expect(subject.available?).to be_truthy
+ end
+ end
+
+ describe "#api_fetcher?" do
+ it "should return false" do
+ expect(subject.api_fetcher?).to be_falsey
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/compact_index_spec.rb b/spec/bundler/bundler/fetcher/compact_index_spec.rb
new file mode 100644
index 0000000000..aa536673d9
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/compact_index_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+# load CompactIndexClient upfront to prevent thread safety issues during parallel specs
+require "bundler/compact_index_client"
+
+RSpec.describe Bundler::Fetcher::CompactIndex do
+ let(:response) { double(:response) }
+ let(:downloader) { double(:downloader, fetch: response) }
+ let(:display_uri) { Gem::URI("http://sampleuri.com") }
+ let(:remote) { double(:remote, cache_slug: "lsjdf", uri: display_uri) }
+ let(:gem_remote_fetcher) { nil }
+ let(:compact_index) { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
+ let(:compact_index_client) { double(:compact_index_client, available?: true, info: [["lskdjf", "1", nil, [], []]]) }
+
+ before do
+ allow(response).to receive(:is_a?).with(Gem::Net::HTTPNotModified).and_return(true)
+ allow(compact_index).to receive(:log_specs) {}
+ allow(compact_index).to receive(:compact_index_client).and_return(compact_index_client)
+ end
+
+ describe "#specs_for_names" do
+ let(:thread_list) { Thread.list.select {|thread| thread.status == "run" } }
+ let(:thread_inspection) { thread_list.map {|th| " * #{th}:\n #{th.backtrace_locations.join("\n ")}" }.join("\n") }
+
+ it "has only one thread open at the end of the run" do
+ compact_index.specs_for_names(["lskdjf"])
+
+ thread_count = thread_list.count
+ expect(thread_count).to eq(1), "Expected 1 active thread after `#specs_for_names`, but found #{thread_count}. In particular, found:\n#{thread_inspection}"
+ end
+
+ it "calls worker#stop during the run" do
+ expect_any_instance_of(Bundler::Worker).to receive(:stop).at_least(:once).and_call_original
+
+ compact_index.specs_for_names(["lskdjf"])
+ end
+
+ describe "#available?" do
+ it "returns true" do
+ expect(compact_index).to be_available
+ end
+
+ context "when OpenSSL is not available" do
+ before do
+ allow(compact_index).to receive(:require).with("openssl").and_raise(LoadError)
+ end
+
+ it "returns true" do
+ expect(compact_index).to be_available
+ end
+ end
+
+ context "when OpenSSL is FIPS-enabled" do
+ def remove_cached_md5_availability
+ return unless Bundler::SharedHelpers.instance_variable_defined?(:@md5_available)
+ Bundler::SharedHelpers.remove_instance_variable(:@md5_available)
+ end
+
+ before do
+ remove_cached_md5_availability
+ stub_const("OpenSSL::OPENSSL_FIPS", true)
+ end
+
+ after { remove_cached_md5_availability }
+
+ context "when FIPS-mode is active" do
+ before do
+ allow(OpenSSL::Digest).to receive(:digest).with("MD5", "").
+ and_raise(OpenSSL::Digest::DigestError)
+ end
+
+ it "returns false" do
+ expect(compact_index).to_not be_available
+ end
+ end
+
+ it "returns true" do
+ expect(compact_index).to be_available
+ end
+ end
+ end
+
+ context "logging" do
+ before { allow(compact_index).to receive(:log_specs).and_call_original }
+
+ context "with debug on" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true)
+ end
+
+ it "should log at info level" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with('Looking up gems ["lskdjf"]')
+ compact_index.specs_for_names(["lskdjf"])
+ end
+ end
+
+ context "with debug off" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false)
+ end
+
+ it "should log at info level" do
+ expect(Bundler).to receive_message_chain(:ui, :info).with(".", false)
+ compact_index.specs_for_names(["lskdjf"])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/dependency_spec.rb b/spec/bundler/bundler/fetcher/dependency_spec.rb
new file mode 100644
index 0000000000..501bc269a5
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/dependency_spec.rb
@@ -0,0 +1,296 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Fetcher::Dependency do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote, uri: Gem::URI("http://localhost:5000")) }
+ let(:display_uri) { "http://sample_uri.com" }
+ let(:gem_remote_fetcher) { nil }
+
+ subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
+
+ describe "#available?" do
+ let(:dependency_api_uri) { double(:dependency_api_uri) }
+ let(:fetched_spec) { double(:fetched_spec) }
+
+ before do
+ allow(subject).to receive(:dependency_api_uri).and_return(dependency_api_uri)
+ allow(downloader).to receive(:fetch).with(dependency_api_uri).and_return(fetched_spec)
+ end
+
+ it "should be truthy" do
+ expect(subject.available?).to be_truthy
+ end
+
+ context "when there is no network access" do
+ before do
+ allow(downloader).to receive(:fetch).with(dependency_api_uri) {
+ raise Bundler::Fetcher::NetworkDownError.new("Network Down Message")
+ }
+ end
+
+ it "should raise an HTTPError with the original message" do
+ expect { subject.available? }.to raise_error(Bundler::HTTPError, "Network Down Message")
+ end
+ end
+
+ context "when authentication is required" do
+ let(:remote_uri) { "http://remote_uri.org" }
+
+ before do
+ allow(downloader).to receive(:fetch).with(dependency_api_uri) {
+ raise Bundler::Fetcher::AuthenticationRequiredError.new(remote_uri)
+ }
+ end
+
+ it "should raise the original error" do
+ expect { subject.available? }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ %r{Authentication is required for http://remote_uri.org})
+ end
+ end
+
+ context "when there is an http error" do
+ before { allow(downloader).to receive(:fetch).with(dependency_api_uri) { raise Bundler::HTTPError.new } }
+
+ it "should be falsey" do
+ expect(subject.available?).to be_falsey
+ end
+ end
+ end
+
+ describe "#api_fetcher?" do
+ it "should return true" do
+ expect(subject.api_fetcher?).to be_truthy
+ end
+ end
+
+ describe "#specs" do
+ let(:gem_names) { %w[foo bar] }
+ let(:full_dependency_list) { ["bar"] }
+ let(:last_spec_list) { [["boulder", gem_version1, "ruby", resque]] }
+ let(:fail_errors) { double(:fail_errors) }
+ let(:bundler_retry) { double(:bundler_retry) }
+ let(:gem_version1) { double(:gem_version1) }
+ let(:resque) { double(:resque) }
+ let(:remote_uri) { "http://remote-uri.org" }
+
+ before do
+ stub_const("Bundler::Fetcher::FAIL_ERRORS", fail_errors)
+ allow(Bundler::Retry).to receive(:new).with("dependency api", fail_errors).and_return(bundler_retry)
+ allow(bundler_retry).to receive(:attempts) {|&block| block.call }
+ allow(subject).to receive(:log_specs) {}
+ allow(subject).to receive(:remote_uri).and_return(remote_uri)
+ allow(Bundler).to receive_message_chain(:ui, :debug?)
+ allow(Bundler).to receive_message_chain(:ui, :info)
+ allow(Bundler).to receive_message_chain(:ui, :debug)
+ end
+
+ context "when there are given gem names that are not in the full dependency list" do
+ let(:spec_list) { [["top", gem_version2, "ruby", faraday]] }
+ let(:deps_list) { [] }
+ let(:dependency_specs) { [spec_list, deps_list] }
+ let(:gem_version2) { double(:gem_version2) }
+ let(:faraday) { double(:faraday) }
+
+ before { allow(subject).to receive(:dependency_specs).with(["foo"]).and_return(dependency_specs) }
+
+ it "should return a hash with the remote_uri and the list of specs" do
+ expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq([
+ ["top", gem_version2, "ruby", faraday],
+ ["boulder", gem_version1, "ruby", resque],
+ ])
+ end
+ end
+
+ context "when all given gem names are in the full dependency list" do
+ let(:gem_names) { ["foo"] }
+ let(:full_dependency_list) { %w[foo bar] }
+ let(:last_spec_list) { ["boulder"] }
+
+ it "should return a hash with the remote_uri and the last spec list" do
+ expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to eq(["boulder"])
+ end
+ end
+
+ context "logging" do
+ before { allow(subject).to receive(:log_specs).and_call_original }
+
+ context "with debug on" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(true)
+ allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []])
+ end
+
+ it "should log the query list at debug level" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: [\"foo\"]")
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("Query List: []")
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+
+ context "with debug off" do
+ before do
+ allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false)
+ allow(subject).to receive(:dependency_specs).with(["foo"]).and_return([[], []])
+ end
+
+ it "should log at info level" do
+ expect(Bundler).to receive_message_chain(:ui, :info).with(".", false)
+ expect(Bundler).to receive_message_chain(:ui, :info).with(".", false)
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+ end
+
+ shared_examples_for "the error is properly handled" do
+ it "should return nil" do
+ expect(subject.specs(gem_names, full_dependency_list, last_spec_list)).to be_nil
+ end
+
+ context "debug logging is not on" do
+ before { allow(Bundler).to receive_message_chain(:ui, :debug?).and_return(false) }
+
+ it "should log a new line to info" do
+ expect(Bundler).to receive_message_chain(:ui, :info).with("")
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+ end
+
+ shared_examples_for "the error is logged" do
+ it "should log the inability to fetch from API at debug level, and mention retrying" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("could not fetch from the dependency API, trying the full index")
+ subject.specs(gem_names, full_dependency_list, last_spec_list)
+ end
+ end
+
+ context "when an HTTPError occurs" do
+ before { allow(subject).to receive(:dependency_specs) { raise Bundler::HTTPError.new } }
+
+ it_behaves_like "the error is properly handled"
+ it_behaves_like "the error is logged"
+ end
+
+ context "when a GemspecError occurs" do
+ before { allow(subject).to receive(:dependency_specs) { raise Bundler::GemspecError.new } }
+
+ it_behaves_like "the error is properly handled"
+ it_behaves_like "the error is logged"
+ end
+
+ context "when a MarshalError occurs" do
+ before { allow(subject).to receive(:dependency_specs) { raise Bundler::MarshalError.new } }
+
+ it_behaves_like "the error is properly handled"
+ it_behaves_like "the error is logged"
+ end
+ end
+
+ describe "#dependency_specs" do
+ let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] }
+ let(:gem_list) { double(:gem_list) }
+ let(:formatted_specs_and_deps) { double(:formatted_specs_and_deps) }
+
+ before do
+ allow(subject).to receive(:unmarshalled_dep_gems).with(gem_names).and_return(gem_list)
+ allow(subject).to receive(:get_formatted_specs_and_deps).with(gem_list).and_return(formatted_specs_and_deps)
+ end
+
+ it "should log the query list at debug level" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with(
+ "Query Gemcutter Dependency Endpoint API: foo,bar,bundler,rubocop"
+ )
+ subject.dependency_specs(gem_names)
+ end
+
+ it "should return formatted specs and a unique list of dependencies" do
+ expect(subject.dependency_specs(gem_names)).to eq(formatted_specs_and_deps)
+ end
+ end
+
+ describe "#unmarshalled_dep_gems" do
+ let(:gem_names) { [%w[foo bar], %w[bundler rubocop]] }
+ let(:dep_api_uri) { double(:dep_api_uri) }
+ let(:unmarshalled_gems) { double(:unmarshalled_gems) }
+ let(:fetch_response) { double(:fetch_response, body: double(:body)) }
+ let(:rubygems_limit) { 100 }
+
+ before { allow(subject).to receive(:dependency_api_uri).with(gem_names).and_return(dep_api_uri) }
+
+ it "should fetch dependencies from RubyGems and unmarshal them" do
+ expect(gem_names).to receive(:each_slice).with(rubygems_limit).and_call_original
+ expect(downloader).to receive(:fetch).with(dep_api_uri).and_return(fetch_response)
+ expect(Bundler).to receive(:safe_load_marshal).with(fetch_response.body).and_return([unmarshalled_gems])
+ expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems])
+ end
+
+ it "should fetch as many dependencies as specified" do
+ allow(subject).to receive(:dependency_api_uri).with([%w[foo bar]]).and_return(dep_api_uri)
+ allow(subject).to receive(:dependency_api_uri).with([%w[bundler rubocop]]).and_return(dep_api_uri)
+
+ expect(downloader).to receive(:fetch).twice.with(dep_api_uri).and_return(fetch_response)
+ expect(Bundler).to receive(:safe_load_marshal).twice.with(fetch_response.body).and_return([unmarshalled_gems])
+
+ Bundler.settings.temporary(api_request_size: 1) do
+ expect(subject.unmarshalled_dep_gems(gem_names)).to eq([unmarshalled_gems, unmarshalled_gems])
+ end
+ end
+ end
+
+ describe "#get_formatted_specs_and_deps" do
+ let(:gem_list) do
+ [
+ {
+ dependencies: {
+ "resque" => "req3,req4",
+ },
+ name: "typhoeus",
+ number: "1.0.1",
+ platform: "ruby",
+ },
+ {
+ dependencies: {
+ "faraday" => "req1,req2",
+ },
+ name: "grape",
+ number: "2.0.2",
+ platform: "jruby",
+ },
+ ]
+ end
+
+ it "should return formatted specs and a unique list of dependencies" do
+ spec_list, deps_list = subject.get_formatted_specs_and_deps(gem_list)
+ expect(spec_list).to eq([["typhoeus", "1.0.1", "ruby", [["resque", ["req3,req4"]]]],
+ ["grape", "2.0.2", "jruby", [["faraday", ["req1,req2"]]]]])
+ expect(deps_list).to eq(%w[resque faraday])
+ end
+ end
+
+ describe "#dependency_api_uri" do
+ let(:uri) { Gem::URI("http://gem-api.com") }
+
+ context "with gem names" do
+ let(:gem_names) { %w[foo bar bundler rubocop] }
+
+ before { allow(subject).to receive(:fetch_uri).and_return(uri) }
+
+ it "should return an api calling uri with the gems in the query" do
+ expect(subject.dependency_api_uri(gem_names).to_s).to eq(
+ "http://gem-api.com/api/v1/dependencies?gems=bar%2Cbundler%2Cfoo%2Crubocop"
+ )
+ end
+ end
+
+ context "with no gem names" do
+ let(:gem_names) { [] }
+
+ before { allow(subject).to receive(:fetch_uri).and_return(uri) }
+
+ it "should return an api calling uri with no query" do
+ expect(subject.dependency_api_uri(gem_names).to_s).to eq(
+ "http://gem-api.com/api/v1/dependencies"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/downloader_spec.rb b/spec/bundler/bundler/fetcher/downloader_spec.rb
new file mode 100644
index 0000000000..edf426328a
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/downloader_spec.rb
@@ -0,0 +1,302 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Fetcher::Downloader do
+ let(:connection) { double(:connection) }
+ let(:redirect_limit) { 5 }
+ let(:uri) { Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint") }
+ let(:options) { double(:options) }
+
+ subject { described_class.new(connection, redirect_limit) }
+
+ describe "fetch" do
+ let(:counter) { 0 }
+ let(:httpv) { "1.1" }
+ let(:http_response) { double(:response) }
+
+ before do
+ allow(subject).to receive(:request).with(uri, options).and_return(http_response)
+ allow(http_response).to receive(:body).and_return("Body with info")
+ end
+
+ context "when the # requests counter is greater than the redirect limit" do
+ let(:counter) { redirect_limit + 1 }
+
+ it "should raise a Bundler::HTTPError specifying too many redirects" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Too many redirects")
+ end
+ end
+
+ context "logging" do
+ let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") }
+
+ it "should log the HTTP response code and message to debug" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP 200 Success #{uri}")
+ subject.fetch(uri, options, counter)
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPRedirection" do
+ let(:http_response) { Gem::Net::HTTPRedirection.new(httpv, 308, "Moved") }
+
+ before { http_response["location"] = "http://www.redirect-uri.com/api/v2/endpoint" }
+
+ it "should try to fetch the redirect uri and iterate the # requests counter" do
+ expect(subject).to receive(:fetch).with(Gem::URI("http://www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original
+ expect(subject).to receive(:fetch).with(Gem::URI("http://www.redirect-uri.com/api/v2/endpoint"), options, 1)
+ subject.fetch(uri, options, counter)
+ end
+
+ context "when the redirect uri and original uri are the same" do
+ let(:uri) { Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ before { http_response["location"] = "ssh://www.uri-to-fetch.com/api/v1/endpoint" }
+
+ it "should set the same user and password for the redirect uri" do
+ expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v2/endpoint"), options, 0).and_call_original
+ expect(subject).to receive(:fetch).with(Gem::URI("ssh://username:password@www.uri-to-fetch.com/api/v1/endpoint"), options, 1)
+ subject.fetch(uri, options, counter)
+ end
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPSuccess" do
+ let(:http_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") }
+
+ it "should return the response body" do
+ expect(subject.fetch(uri, options, counter)).to eq(http_response)
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPRequestEntityTooLarge" do
+ let(:http_response) { Gem::Net::HTTPRequestEntityTooLarge.new("1.1", 413, "Too Big") }
+
+ it "should raise a Bundler::Fetcher::FallbackError with the response body" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::FallbackError, "Body with info")
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPUnauthorized" do
+ let(:http_response) { Gem::Net::HTTPUnauthorized.new("1.1", 401, "Unauthorized") }
+
+ it "should raise a Bundler::Fetcher::AuthenticationRequiredError with the uri host" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ /Authentication is required for www.uri-to-fetch.com/)
+ end
+
+ it "should raise a Bundler::Fetcher::AuthenticationRequiredError with advice" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ /`bundle config set --global www\.uri-to-fetch\.com username:password`.*`BUNDLE_WWW__URI___TO___FETCH__COM`/m)
+ end
+
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") }
+
+ it "should raise a Bundler::Fetcher::BadAuthenticationError that doesn't contain the password" do
+ expect { subject.fetch(uri, options, counter) }.
+ to raise_error(Bundler::Fetcher::BadAuthenticationError, /Bad username or password for www.uri-to-fetch.com/)
+ end
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPForbidden" do
+ let(:http_response) { Gem::Net::HTTPForbidden.new("1.1", 403, "Forbidden") }
+ let(:uri) { Gem::URI("http://user:password@www.uri-to-fetch.com") }
+
+ it "should raise a Bundler::Fetcher::AuthenticationForbiddenError with the uri host" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError,
+ /Access token could not be authenticated for www.uri-to-fetch.com/)
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPNotFound" do
+ let(:http_response) { Gem::Net::HTTPNotFound.new("1.1", 404, "Not Found") }
+
+ it "should raise a Bundler::Fetcher::FallbackError with Gem::Net::HTTPNotFound" do
+ expect { subject.fetch(uri, options, counter) }.
+ to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://www.uri-to-fetch.com/api/v2/endpoint")
+ end
+
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should raise a Bundler::Fetcher::FallbackError that doesn't contain the password" do
+ expect { subject.fetch(uri, options, counter) }.
+ to raise_error(Bundler::Fetcher::FallbackError, "Gem::Net::HTTPNotFound: http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ end
+ end
+ end
+
+ context "when the request response is a Gem::Net::HTTPRequestedRangeNotSatisfiable" do
+ let(:http_response) { Gem::Net::HTTPRequestedRangeNotSatisfiable.new("1.1", 416, "Range Not Satisfiable") }
+ let(:success_response) { Gem::Net::HTTPSuccess.new("1.1", 200, "Success") }
+ let(:options) { { "Range" => "bytes=1000-", "If-None-Match" => "some-etag" } }
+
+ before do
+ # First request returns 416, retry request returns success
+ allow(subject).to receive(:request).with(uri, options).and_return(http_response)
+ allow(subject).to receive(:request).with(uri, { "If-None-Match" => "some-etag" }).and_return(success_response)
+ end
+
+ # The 416 handler removes the Range header and retries without incrementing the counter.
+ # Importantly, it does NOT add Accept-Encoding header, which would break Ruby's
+ # automatic gzip decompression (see issue #9271 for details on that bug).
+ it "should retry the request without the Range header" do
+ expect(subject).to receive(:request).with(uri, options).ordered
+ expect(subject).to receive(:request).with(uri, hash_excluding("Range", "Accept-Encoding")).ordered
+ subject.fetch(uri, options, counter)
+ end
+
+ it "should preserve other headers on retry" do
+ expect(subject).to receive(:request).with(uri, options).ordered
+ expect(subject).to receive(:request).with(uri, hash_including("If-None-Match" => "some-etag")).ordered
+ subject.fetch(uri, options, counter)
+ end
+
+ it "should return the successful response" do
+ result = subject.fetch(uri, options, counter)
+ expect(result).to eq(success_response)
+ end
+ end
+
+ context "when the request response is some other type" do
+ let(:http_response) { Gem::Net::HTTPBadGateway.new("1.1", 500, "Fatal Error") }
+
+ it "should raise a Bundler::HTTPError with the response class and body" do
+ expect { subject.fetch(uri, options, counter) }.to raise_error(Bundler::HTTPError, "Gem::Net::HTTPBadGateway: Body with info")
+ end
+ end
+ end
+
+ describe "request" do
+ let(:net_http_get) { double(:net_http_get) }
+ let(:response) { double(:response) }
+
+ before do
+ allow(Gem::Net::HTTP::Get).to receive(:new).with("/api/v2/endpoint", options).and_return(net_http_get)
+ allow(connection).to receive(:request).with(uri, net_http_get).and_return(response)
+ end
+
+ it "should log the HTTP GET request to debug" do
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+
+ context "when there is a user provided in the request" do
+ context "and there is also a password provided" do
+ context "that contains cgi escaped characters" do
+ let(:uri) { Gem::URI("http://username:password%24@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should request basic authentication with the username and password, and log the HTTP GET request to debug, without the password" do
+ expect(net_http_get).to receive(:basic_auth).with("username", "password$")
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+
+ context "that is all unescaped characters" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+ it "should request basic authentication with the username and proper cgi compliant password, and log the HTTP GET request to debug, without the password" do
+ expect(net_http_get).to receive(:basic_auth).with("username", "password")
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://username@www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+ end
+
+ context "and it's used as the authentication token" do
+ let(:uri) { Gem::URI("http://username@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should request basic authentication with just the user, and log the HTTP GET request to debug, without the token" do
+ expect(net_http_get).to receive(:basic_auth).with("username", nil)
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+
+ context "and it's used as the authentication token, and contains cgi escaped characters" do
+ let(:uri) { Gem::URI("http://username%24@www.uri-to-fetch.com/api/v2/endpoint") }
+
+ it "should request basic authentication with the proper cgi compliant password user, and log the HTTP GET request to debug, without the token" do
+ expect(net_http_get).to receive(:basic_auth).with("username$", nil)
+ expect(Bundler).to receive_message_chain(:ui, :debug).with("HTTP GET http://www.uri-to-fetch.com/api/v2/endpoint")
+ subject.request(uri, options)
+ end
+ end
+ end
+
+ context "when the request response causes a OpenSSL::SSL::SSLError" do
+ before { allow(connection).to receive(:request).with(uri, net_http_get) { raise OpenSSL::SSL::SSLError.new } }
+
+ it "should raise a LoadError about openssl" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
+ %r{Could not verify the SSL certificate for http://www.uri-to-fetch.com/api/v2/endpoint})
+ end
+ end
+
+ context "when the request response causes an HTTP error" do
+ let(:message) { "error about network" }
+ let(:error) { error_class.new(message) }
+
+ before do
+ allow(connection).to receive(:request).with(uri, net_http_get) { raise error }
+ end
+
+ context "that it's retryable" do
+ let(:error_class) { Gem::Timeout::Error }
+
+ it "should trace log the error" do
+ allow(Bundler).to receive_message_chain(:ui, :debug)
+ expect(Bundler).to receive_message_chain(:ui, :trace).with(error)
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError)
+ end
+
+ it "should raise a Bundler::HTTPError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError,
+ "Network error while fetching http://www.uri-to-fetch.com/api/v2/endpoint (error about network)")
+ end
+
+ context "when there are credentials provided in the request" do
+ let(:uri) { Gem::URI("http://username:password@www.uri-to-fetch.com/api/v2/endpoint") }
+ before do
+ allow(net_http_get).to receive(:basic_auth).with("username", "password")
+ end
+
+ it "should raise a Bundler::HTTPError that doesn't contain the password" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::HTTPError,
+ "Network error while fetching http://username@www.uri-to-fetch.com/api/v2/endpoint (error about network)")
+ end
+ end
+ end
+
+ context "when error is about the host being down" do
+ let(:error_class) { Gem::Net::HTTP::Persistent::Error }
+ let(:message) { "host down: http://www.uri-to-fetch.com" }
+
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
+ end
+ end
+
+ context "when error is about connection refused" do
+ let(:error_class) { Gem::Net::HTTP::Persistent::Error }
+ let(:message) { "connection refused down: http://www.uri-to-fetch.com" }
+
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
+ end
+ end
+
+ context "when error is about no route to host" do
+ let(:error_class) { SocketError }
+ let(:message) { "Failed to open TCP connection to www.uri-to-fetch.com:443 " }
+
+ it "should raise a Bundler::Fetcher::NetworkDownError" do
+ expect { subject.request(uri, options) }.to raise_error(Bundler::Fetcher::NetworkDownError,
+ /Could not reach host www.uri-to-fetch.com/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
new file mode 100644
index 0000000000..df1a58d843
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/gem_remote_fetcher_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "rubygems/remote_fetcher"
+require "bundler/fetcher/gem_remote_fetcher"
+require_relative "../../support/artifice/helpers/artifice"
+require "bundler/vendored_persistent.rb"
+
+RSpec.describe Bundler::Fetcher::GemRemoteFetcher do
+ describe "Parallel download" do
+ it "download using multiple connections from the pool" do
+ unless Bundler.rubygems.provides?(">= 4.0.0.dev")
+ skip "This example can only run when RubyGems supports multiple http connection pool"
+ end
+
+ require_relative "../../support/artifice/helpers/endpoint"
+ concurrent_ruby_path = Dir[scoped_base_system_gem_path.join("gems/concurrent-ruby-*/lib/concurrent-ruby")].first
+ $LOAD_PATH.unshift(concurrent_ruby_path)
+ require "concurrent-ruby"
+
+ require_rack_test
+ responses = []
+
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+ previous_client = Gem::Request::ConnectionPools.client
+ dummy_endpoint = Class.new(Endpoint) do
+ get "/foo" do
+ latch2.count_down
+ latch1.wait
+
+ responses << "foo"
+ end
+
+ get "/bar" do
+ responses << "bar"
+
+ latch1.count_down
+ end
+ end
+
+ Artifice.activate_with(dummy_endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ first_request = Thread.new do
+ subject.fetch_path("https://example.org/foo")
+ end
+ second_request = Thread.new do
+ latch2.wait
+ subject.fetch_path("https://example.org/bar")
+ end
+
+ [first_request, second_request].each(&:join)
+
+ expect(responses).to eq(["bar", "foo"])
+ ensure
+ Artifice.deactivate
+ Gem::Request::ConnectionPools.client = previous_client
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher/index_spec.rb b/spec/bundler/bundler/fetcher/index_spec.rb
new file mode 100644
index 0000000000..a6a18efd98
--- /dev/null
+++ b/spec/bundler/bundler/fetcher/index_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "rubygems/remote_fetcher"
+
+RSpec.describe Bundler::Fetcher::Index do
+ let(:downloader) { nil }
+ let(:remote) { double(:remote, uri: remote_uri) }
+ let(:remote_uri) { Gem::URI("http://#{userinfo}remote-uri.org") }
+ let(:userinfo) { "" }
+ let(:display_uri) { "http://sample_uri.com" }
+ let(:rubygems) { double(:rubygems) }
+ let(:gem_names) { %w[foo bar] }
+ let(:gem_remote_fetcher) { nil }
+
+ subject { described_class.new(downloader, remote, display_uri, gem_remote_fetcher) }
+
+ before { allow(Bundler).to receive(:rubygems).and_return(rubygems) }
+
+ it "fetches and returns the list of remote specs" do
+ expect(rubygems).to receive(:fetch_all_remote_specs) { nil }
+ subject.specs(gem_names)
+ end
+
+ context "error handling" do
+ before do
+ allow(rubygems).to receive(:fetch_all_remote_specs) { raise Gem::RemoteFetcher::FetchError.new(error_message, display_uri) }
+ end
+
+ context "when certificate verify failed" do
+ let(:error_message) { "certificate verify failed" }
+
+ it "should raise a Bundler::Fetcher::CertificateFailureError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::CertificateFailureError,
+ %r{Could not verify the SSL certificate for http://sample_uri.com})
+ end
+ end
+
+ context "when a 401 response occurs" do
+ let(:error_message) { "401" }
+
+ it "should raise a Bundler::Fetcher::AuthenticationRequiredError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationRequiredError,
+ %r{Authentication is required for http://remote-uri.org})
+ end
+
+ context "and there was userinfo" do
+ let(:userinfo) { "user:pass@" }
+
+ it "should raise a Bundler::Fetcher::BadAuthenticationError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::BadAuthenticationError,
+ %r{Bad username or password for http://user@remote-uri.org})
+ end
+ end
+ end
+
+ context "when a 403 response occurs" do
+ let(:error_message) { "403" }
+
+ it "should raise a Bundler::Fetcher::AuthenticationForbiddenError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::Fetcher::AuthenticationForbiddenError,
+ %r{Access token could not be authenticated for http://remote-uri.org})
+ end
+ end
+
+ context "any other message is returned" do
+ let(:error_message) { "You get an error, you get an error!" }
+
+ before { allow(Bundler).to receive(:ui).and_return(double(trace: nil)) }
+
+ it "should raise a Bundler::HTTPError" do
+ expect { subject.specs(gem_names) }.to raise_error(Bundler::HTTPError, "Could not fetch specs from http://sample_uri.com due to underlying error <You get an error, you get an error! (http://sample_uri.com)>")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/fetcher_spec.rb b/spec/bundler/bundler/fetcher_spec.rb
new file mode 100644
index 0000000000..e20f7e7c48
--- /dev/null
+++ b/spec/bundler/bundler/fetcher_spec.rb
@@ -0,0 +1,260 @@
+# frozen_string_literal: true
+
+require "bundler/fetcher"
+
+RSpec.describe Bundler::Fetcher do
+ let(:uri) { Gem::URI("https://example.com") }
+ let(:remote) { double("remote", uri: uri, original_uri: nil) }
+
+ subject(:fetcher) { Bundler::Fetcher.new(remote) }
+
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ describe "#connection" do
+ context "when Gem.configuration doesn't specify http_proxy" do
+ it "specify no http_proxy" do
+ expect(fetcher.http_proxy).to be_nil
+ end
+ it "consider environment vars when determine proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do
+ expect(fetcher.http_proxy).to match("http://proxy-example.com")
+ end
+ end
+ end
+ context "when Gem.configuration specifies http_proxy " do
+ let(:proxy) { "http://proxy-example2.com" }
+ before do
+ allow(Gem.configuration).to receive(:[]).with(:http_proxy).and_return(proxy)
+ end
+ it "consider Gem.configuration when determine proxy" do
+ expect(fetcher.http_proxy).to match("http://proxy-example2.com")
+ end
+ it "consider Gem.configuration when determine proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example.com") do
+ expect(fetcher.http_proxy).to match("http://proxy-example2.com")
+ end
+ end
+ context "when the proxy is :no_proxy" do
+ let(:proxy) { :no_proxy }
+ it "does not set a proxy" do
+ expect(fetcher.http_proxy).to be_nil
+ end
+ end
+ end
+
+ context "when a rubygems source mirror is set" do
+ let(:orig_uri) { Gem::URI("http://zombo.com") }
+ let(:remote_with_mirror) do
+ double("remote", uri: uri, original_uri: orig_uri, anonymized_uri: uri)
+ end
+
+ let(:fetcher) { Bundler::Fetcher.new(remote_with_mirror) }
+
+ it "sets the 'X-Gemfile-Source' header containing the original source" do
+ expect(
+ fetcher.send(:connection).override_headers["X-Gemfile-Source"]
+ ).to eq("http://zombo.com")
+ end
+ end
+
+ context "when there is no rubygems source mirror set" do
+ let(:remote_no_mirror) do
+ double("remote", uri: uri, original_uri: nil, anonymized_uri: uri)
+ end
+
+ let(:fetcher) { Bundler::Fetcher.new(remote_no_mirror) }
+
+ it "does not set the 'X-Gemfile-Source' header" do
+ expect(fetcher.send(:connection).override_headers["X-Gemfile-Source"]).to be_nil
+ end
+ end
+
+ context "when there are proxy environment variable(s) set" do
+ it "consider http_proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example3.com") do
+ expect(fetcher.http_proxy).to match("http://proxy-example3.com")
+ end
+ end
+ it "consider no_proxy" do
+ with_env_vars("HTTP_PROXY" => "http://proxy-example4.com", "NO_PROXY" => ".example.com,.example.net") do
+ expect(
+ fetcher.send(:connection).no_proxy
+ ).to eq([".example.com", ".example.net"])
+ end
+ end
+ end
+
+ context "when no ssl configuration is set" do
+ it "no cert" do
+ expect(fetcher.send(:connection).cert).to be_nil
+ expect(fetcher.send(:connection).key).to be_nil
+ end
+ end
+
+ context "when bunder ssl ssl configuration is set" do
+ before do
+ cert = File.join(Spec::Path.tmpdir, "cert")
+ File.open(cert, "w") {|f| f.write "PEM" }
+ allow(Bundler.settings).to receive(:[]).and_return(nil)
+ allow(Bundler.settings).to receive(:[]).with(:ssl_client_cert).and_return(cert)
+ expect(OpenSSL::X509::Certificate).to receive(:new).with("PEM").and_return("cert")
+ expect(OpenSSL::PKey::RSA).to receive(:new).with("PEM").and_return("key")
+ end
+ after do
+ FileUtils.rm File.join(Spec::Path.tmpdir, "cert")
+ end
+ it "use bundler configuration" do
+ expect(fetcher.send(:connection).cert).to eq("cert")
+ expect(fetcher.send(:connection).key).to eq("key")
+ end
+ end
+
+ context "when gem ssl configuration is set" do
+ before do
+ allow(Gem.configuration).to receive_messages(
+ http_proxy: nil,
+ ssl_client_cert: "cert",
+ ssl_ca_cert: "ca"
+ )
+ expect(File).to receive(:read).and_return("")
+ expect(OpenSSL::X509::Certificate).to receive(:new).and_return("cert")
+ expect(OpenSSL::PKey::RSA).to receive(:new).and_return("key")
+ store = double("ca store")
+ expect(store).to receive(:add_file)
+ expect(OpenSSL::X509::Store).to receive(:new).and_return(store)
+ end
+ it "use gem configuration" do
+ expect(fetcher.send(:connection).cert).to eq("cert")
+ expect(fetcher.send(:connection).key).to eq("key")
+ end
+ end
+ end
+
+ describe "#user_agent" do
+ it "builds user_agent with current ruby version and Bundler settings" do
+ allow(Bundler.settings).to receive(:all).and_return(%w[foo bar])
+ expect(fetcher.user_agent).to match(%r{bundler/(\d.)})
+ expect(fetcher.user_agent).to match(%r{rubygems/(\d.)})
+ expect(fetcher.user_agent).to match(%r{ruby/(\d.)})
+ expect(fetcher.user_agent).to match(%r{options/foo,bar})
+ end
+
+ describe "include CI information" do
+ it "from one CI" do
+ with_env_vars("CI" => nil, "JENKINS_URL" => "foo") do
+ ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") }
+ cis = ci_part.split("/").last.split(",")
+ expect(cis).to include("jenkins")
+ expect(cis).not_to include("ci")
+ end
+ end
+
+ it "from many CI" do
+ with_env_vars("CI" => "true", "SEMAPHORE" => nil, "TRAVIS" => "foo", "GITLAB_CI" => "gitlab", "CI_NAME" => "MY_ci") do
+ ci_part = fetcher.user_agent.split(" ").find {|x| x.start_with?("ci/") }
+ cis = ci_part.split("/").last.split(",")
+ expect(cis).to include("ci", "gitlab", "my_ci", "travis")
+ expect(cis).not_to include("semaphore")
+ end
+ end
+ end
+ end
+
+ describe "#fetch_spec" do
+ let(:name) { "name" }
+ let(:version) { "1.3.17" }
+ let(:platform) { "platform" }
+ let(:downloader) { double("downloader") }
+ let(:body) { double(Gem::Net::HTTP::Get, body: downloaded_data) }
+
+ context "when attempting to load a Gem::Specification" do
+ let(:spec) { Gem::Specification.new(name, version) }
+ let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(spec)) }
+
+ it "returns the spec" do
+ expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader)
+ expect(downloader).to receive(:fetch).once.and_return(body)
+ result = fetcher.fetch_spec([name, version, platform])
+ expect(result).to eq(spec)
+ end
+ end
+
+ context "when attempting to load an unexpected class" do
+ let(:downloaded_data) { Zlib::Deflate.deflate(Marshal.dump(3)) }
+
+ it "raises a HTTPError error" do
+ expect(Bundler::Fetcher::Downloader).to receive(:new).and_return(downloader)
+ expect(downloader).to receive(:fetch).once.and_return(body)
+ expect { fetcher.fetch_spec([name, version, platform]) }.to raise_error(Bundler::HTTPError, /Gemspec .* contained invalid data/i)
+ end
+ end
+ end
+
+ describe "#specs_with_retry" do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) }
+ let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: true, api_fetcher?: true) }
+ let(:dependency) { double(Bundler::Fetcher::Dependency, available?: true, api_fetcher?: true) }
+ let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) }
+
+ before do
+ allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index)
+ allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency)
+ allow(Bundler::Fetcher::Index).to receive(:new).and_return(index)
+ end
+
+ it "picks the first fetcher that works" do
+ expect(compact_index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]])
+ expect(dependency).not_to receive(:specs)
+ expect(index).not_to receive(:specs)
+ fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems))
+ end
+
+ context "when APIs are not available" do
+ before do
+ allow(compact_index).to receive(:available?).and_return(false)
+ allow(dependency).to receive(:available?).and_return(false)
+ end
+
+ it "uses the index" do
+ expect(compact_index).not_to receive(:specs)
+ expect(dependency).not_to receive(:specs)
+ expect(index).to receive(:specs).with("name").and_return([["name", "1.2.3", "ruby"]])
+
+ fetcher.specs_with_retry("name", double(Bundler::Source::Rubygems))
+ end
+ end
+ end
+
+ describe "#api_fetcher?" do
+ let(:downloader) { double(:downloader) }
+ let(:remote) { double(:remote, cache_slug: "slug", uri: uri, original_uri: nil, anonymized_uri: uri) }
+ let(:compact_index) { double(Bundler::Fetcher::CompactIndex, available?: false, api_fetcher?: true) }
+ let(:dependency) { double(Bundler::Fetcher::Dependency, available?: false, api_fetcher?: true) }
+ let(:index) { double(Bundler::Fetcher::Index, available?: true, api_fetcher?: false) }
+
+ before do
+ allow(Bundler::Fetcher::CompactIndex).to receive(:new).and_return(compact_index)
+ allow(Bundler::Fetcher::Dependency).to receive(:new).and_return(dependency)
+ allow(Bundler::Fetcher::Index).to receive(:new).and_return(index)
+ end
+
+ context "when an api fetcher is available" do
+ before do
+ allow(compact_index).to receive(:available?).and_return(true)
+ end
+
+ it "is truthy" do
+ expect(fetcher).to be_api_fetcher
+ end
+ end
+
+ context "when only the index fetcher is available" do
+ it "is falsey" do
+ expect(fetcher).not_to be_api_fetcher
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/friendly_errors_spec.rb b/spec/bundler/bundler/friendly_errors_spec.rb
new file mode 100644
index 0000000000..426e3c856d
--- /dev/null
+++ b/spec/bundler/bundler/friendly_errors_spec.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+require "bundler"
+require "bundler/friendly_errors"
+require "cgi/escape"
+require "cgi/util" unless defined?(CGI::EscapeExt)
+
+RSpec.describe Bundler, "friendly errors" do
+ context "with invalid YAML in .gemrc" do
+ before do
+ File.open(home(".gemrc"), "w") do |f|
+ f.write "invalid: yaml: hah"
+ end
+ end
+
+ after do
+ FileUtils.rm(home(".gemrc"))
+ end
+
+ it "reports a relevant friendly error message" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :install, env: { "DEBUG" => "true" }
+
+ expect(err).to include("Failed to load #{home(".gemrc")}")
+ end
+ end
+
+ it "calls log_error in case of exception" do
+ exception = Exception.new
+ expect(Bundler::FriendlyErrors).to receive(:exit_status).with(exception).and_return(1)
+ expect do
+ Bundler.with_friendly_errors do
+ raise exception
+ end
+ end.to raise_error(SystemExit)
+ end
+
+ it "calls exit_status on exception" do
+ exception = Exception.new
+ expect(Bundler::FriendlyErrors).to receive(:log_error).with(exception)
+ expect do
+ Bundler.with_friendly_errors do
+ raise exception
+ end
+ end.to raise_error(SystemExit)
+ end
+
+ describe "#log_error" do
+ shared_examples "Bundler.ui receive error" do |error, message|
+ it "" do
+ expect(Bundler.ui).to receive(:error).with(message || error.message)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ shared_examples "Bundler.ui receive trace" do |error|
+ it "" do
+ expect(Bundler.ui).to receive(:trace).with(error)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "YamlSyntaxError" do
+ it_behaves_like "Bundler.ui receive error", Bundler::YamlSyntaxError.new(StandardError.new, "sample_message")
+
+ it "Bundler.ui receive trace" do
+ std_error = StandardError.new
+ exception = Bundler::YamlSyntaxError.new(std_error, "sample_message")
+ expect(Bundler.ui).to receive(:trace).with(std_error)
+ Bundler::FriendlyErrors.log_error(exception)
+ end
+ end
+
+ context "Dsl::DSLError, GemspecError" do
+ it_behaves_like "Bundler.ui receive error", Bundler::Dsl::DSLError.new("description", "dsl_path", "backtrace")
+ it_behaves_like "Bundler.ui receive error", Bundler::GemspecError.new
+ end
+
+ context "GemRequireError" do
+ let(:orig_error) { StandardError.new }
+ let(:error) { Bundler::GemRequireError.new(orig_error, "sample_message") }
+
+ before do
+ allow(orig_error).to receive(:backtrace).and_return([])
+ end
+
+ it "Bundler.ui receive error" do
+ expect(Bundler.ui).to receive(:error).with(error.message)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+
+ it "writes to Bundler.ui.trace" do
+ expect(Bundler.ui).to receive(:trace).with(orig_error)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "BundlerError" do
+ it "Bundler.ui receive error" do
+ error = Bundler::BundlerError.new
+ expect(Bundler.ui).to receive(:error).with(error.message, wrap: true)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "Thor::Error" do
+ it_behaves_like "Bundler.ui receive error", Bundler::Thor::Error.new
+ end
+
+ context "Interrupt" do
+ it "Bundler.ui receive error" do
+ expect(Bundler.ui).to receive(:error).with("\nQuitting...")
+ Bundler::FriendlyErrors.log_error(Interrupt.new)
+ end
+ it_behaves_like "Bundler.ui receive trace", Interrupt.new
+ end
+
+ context "Gem::InvalidSpecificationException" do
+ it "Bundler.ui receive error" do
+ error = Gem::InvalidSpecificationException.new
+ expect(Bundler.ui).to receive(:error).with(error.message, wrap: true)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+
+ context "SystemExit" do
+ # Does nothing
+ end
+
+ context "Java::JavaLang::OutOfMemoryError", :jruby_only do
+ it "Bundler.ui receive error" do
+ install_gemfile <<-G, raise_on_error: false, env: { "JRUBY_OPTS" => "-J-Xmx32M" }, artifice: nil
+ source "https://gem.repo1"
+ G
+
+ expect(err).to include("JVM has run out of memory")
+ end
+ end
+
+ context "unexpected error" do
+ it "calls request_issue_report_for with error" do
+ error = StandardError.new
+ expect(Bundler::FriendlyErrors).to receive(:request_issue_report_for).with(error)
+ Bundler::FriendlyErrors.log_error(error)
+ end
+ end
+ end
+
+ describe "#exit_status" do
+ it "calls status_code for BundlerError" do
+ error = Bundler::BundlerError.new
+ expect(error).to receive(:status_code).and_return("sample_status_code")
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status_code")
+ end
+
+ it "returns 15 for Thor::Error" do
+ error = Bundler::Thor::Error.new
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq(15)
+ end
+
+ it "calls status for SystemExit" do
+ error = SystemExit.new
+ expect(error).to receive(:status).and_return("sample_status")
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq("sample_status")
+ end
+
+ it "returns 1 in other cases" do
+ error = StandardError.new
+ expect(Bundler::FriendlyErrors.exit_status(error)).to eq(1)
+ end
+ end
+
+ describe "#request_issue_report_for" do
+ it "calls relevant methods for Bundler.ui" do
+ expect(Bundler.ui).not_to receive(:info)
+ expect(Bundler.ui).to receive(:error).exactly(3).times
+ expect(Bundler.ui).not_to receive(:warn)
+ Bundler::FriendlyErrors.request_issue_report_for(StandardError.new)
+ end
+
+ it "includes error class, message and backlog" do
+ error = StandardError.new
+ allow(Bundler::FriendlyErrors).to receive(:issues_url).and_return("")
+
+ expect(error).to receive(:class).at_least(:once)
+ expect(error).to receive(:message).at_least(:once)
+ expect(error).to receive(:backtrace).at_least(:once)
+ Bundler::FriendlyErrors.request_issue_report_for(error)
+ end
+ end
+
+ describe "#issues_url" do
+ it "generates a search URL for the exception message" do
+ exception = Exception.new("Exception message")
+
+ expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/ruby/rubygems/search?q=Exception+message&type=Issues")
+ end
+
+ it "generates a search URL for only the first line of a multi-line exception message" do
+ exception = Exception.new(<<END)
+First line of the exception message
+Second line of the exception message
+END
+
+ expect(Bundler::FriendlyErrors.issues_url(exception)).to eq("https://github.com/ruby/rubygems/search?q=First+line+of+the+exception+message&type=Issues")
+ end
+
+ it "generates the url without colons" do
+ exception = Exception.new(<<END)
+Exception ::: with ::: colons :::
+END
+ issues_url = Bundler::FriendlyErrors.issues_url(exception)
+ expect(issues_url).not_to include("%3A")
+ expect(issues_url).to eq("https://github.com/ruby/rubygems/search?q=#{CGI.escape("Exception with colons ")}&type=Issues")
+ end
+
+ it "removes information after - for Errono::EACCES" do
+ exception = Exception.new(<<END)
+Errno::EACCES: Permission denied @ dir_s_mkdir - /Users/foo/bar/
+END
+ allow(exception).to receive(:is_a?).with(Errno).and_return(true)
+ issues_url = Bundler::FriendlyErrors.issues_url(exception)
+ expect(issues_url).not_to include("/Users/foo/bar")
+ expect(issues_url).to eq("https://github.com/ruby/rubygems/search?q=#{CGI.escape("Errno EACCES Permission denied @ dir_s_mkdir ")}&type=Issues")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/gem_helper_spec.rb b/spec/bundler/bundler/gem_helper_spec.rb
new file mode 100644
index 0000000000..b4ae2abdc5
--- /dev/null
+++ b/spec/bundler/bundler/gem_helper_spec.rb
@@ -0,0 +1,456 @@
+# frozen_string_literal: true
+
+require "rake"
+require "bundler/gem_helper"
+
+RSpec.describe Bundler::GemHelper do
+ let(:app_name) { "lorem__ipsum" }
+ let(:app_path) { bundled_app app_name }
+ let(:app_gemspec_path) { app_path.join("#{app_name}.gemspec") }
+
+ before(:each) do
+ bundle_config_global "gem.mit false"
+ bundle_config_global "gem.test false"
+ bundle_config_global "gem.coc false"
+ bundle_config_global "gem.linter false"
+ bundle_config_global "gem.ci false"
+ bundle_config_global "gem.changelog false"
+ git("config --global init.defaultBranch main")
+ bundle "gem #{app_name}"
+ prepare_gemspec(app_gemspec_path)
+ end
+
+ context "determining gemspec" do
+ subject { Bundler::GemHelper.new(app_path) }
+
+ context "fails" do
+ it "when there is no gemspec" do
+ FileUtils.rm app_gemspec_path
+ expect { subject }.to raise_error(/Unable to determine name/)
+ end
+
+ it "when there are two gemspecs and the name isn't specified" do
+ FileUtils.touch app_path.join("#{app_name}-2.gemspec")
+ expect { subject }.to raise_error(/Unable to determine name/)
+ end
+ end
+
+ context "interpolates the name" do
+ it "when there is only one gemspec" do
+ expect(subject.gemspec.name).to eq(app_name)
+ end
+
+ it "for a hidden gemspec" do
+ FileUtils.mv app_gemspec_path, app_path.join(".gemspec")
+ expect(subject.gemspec.name).to eq(app_name)
+ end
+ end
+
+ it "handles namespaces and converts them to CamelCase" do
+ bundle "gem #{app_name}-foo_bar"
+ underscore_path = bundled_app "#{app_name}-foo_bar"
+
+ lib = underscore_path.join("lib/#{app_name}/foo_bar.rb").read
+ expect(lib).to include("module LoremIpsum")
+ expect(lib).to include("module FooBar")
+ end
+ end
+
+ context "gem management" do
+ def mock_confirm_message(message)
+ expect(Bundler.ui).to receive(:confirm).with(message)
+ end
+
+ def mock_build_message(name, version)
+ message = "#{name} #{version} built to pkg/#{name}-#{version}.gem."
+ mock_confirm_message message
+ end
+
+ def mock_checksum_message(name, version)
+ message = "#{name} #{version} checksum written to checksums/#{name}-#{version}.gem.sha512."
+ mock_confirm_message message
+ end
+
+ def sha512_hexdigest(path)
+ Digest::SHA512.file(path).hexdigest
+ end
+
+ subject! { Bundler::GemHelper.new(app_path) }
+ let(:app_version) { "0.1.0" }
+ let(:app_gem_dir) { app_path.join("pkg") }
+ let(:app_gem_path) { app_gem_dir.join("#{app_name}-#{app_version}.gem") }
+ let(:app_sha_path) { app_path.join("checksums", "#{app_name}-#{app_version}.gem.sha512") }
+ let(:app_gemspec_content) { File.read(app_gemspec_path) }
+
+ before(:each) do
+ content = app_gemspec_content.gsub("TODO: ", "")
+ content.sub!(/homepage\s+= ".*"/, 'homepage = ""')
+ content.gsub!(/spec\.metadata.+\n/, "")
+ File.open(app_gemspec_path, "w") {|file| file << content }
+ end
+
+ it "uses a shell UI for output" do
+ expect(Bundler.ui).to be_a(Bundler::UI::Shell)
+ end
+
+ describe "#install" do
+ let!(:rake_application) { Rake.application }
+
+ before(:each) do
+ Rake.application = Rake::Application.new
+ end
+
+ after(:each) do
+ Rake.application = rake_application
+ end
+
+ context "defines Rake tasks" do
+ let(:task_names) do
+ %w[build install release release:guard_clean
+ release:source_control_push release:rubygem_push]
+ end
+
+ context "before installation" do
+ it "raises an error with appropriate message" do
+ task_names.each do |name|
+ skip "Rake::FileTask '#{name}' exists" if File.exist?(name)
+ expect { Rake.application[name] }.
+ to raise_error(/^Don't know how to build task '#{name}'/)
+ end
+ end
+ end
+
+ context "after installation" do
+ before do
+ subject.install
+ end
+
+ it "adds Rake tasks successfully" do
+ task_names.each do |name|
+ expect { Rake.application[name] }.not_to raise_error
+ expect(Rake.application[name]).to be_instance_of Rake::Task
+ end
+ end
+
+ it "provides a way to access the gemspec object" do
+ expect(subject.gemspec.name).to eq(app_name)
+ end
+ end
+ end
+ end
+
+ describe "#build_gem" do
+ context "when build failed" do
+ it "raises an error with appropriate message" do
+ # break the gemspec by adding back the TODOs
+ File.open(app_gemspec_path, "w") {|file| file << app_gemspec_content }
+ expect { subject.build_gem }.to raise_error(/TODO/)
+ end
+ end
+
+ context "when build was successful" do
+ it "creates .gem file" do
+ mock_build_message app_name, app_version
+ subject.build_gem
+ expect(app_gem_path).to exist
+ end
+ end
+
+ context "when building in the current working directory" do
+ it "creates .gem file" do
+ mock_build_message app_name, app_version
+ Dir.chdir app_path do
+ Bundler::GemHelper.new.build_gem
+ end
+ expect(app_gem_path).to exist
+ end
+ end
+
+ context "when building in a location relative to the current working directory" do
+ it "creates .gem file" do
+ mock_build_message app_name, app_version
+ Dir.chdir File.dirname(app_path) do
+ Bundler::GemHelper.new(File.basename(app_path)).build_gem
+ end
+ expect(app_gem_path).to exist
+ end
+ end
+ end
+
+ describe "#build_checksum" do
+ it "calculates SHA512 of the content" do
+ FileUtils.mkdir_p(app_gem_dir)
+ File.write(app_gem_path, "")
+ mock_checksum_message app_name, app_version
+ subject.build_checksum(app_gem_path)
+ expect(File.read(app_sha_path).chomp).to eql(Digest::SHA512.hexdigest(""))
+ end
+
+ context "when build was successful" do
+ it "creates .sha512 file" do
+ mock_build_message app_name, app_version
+ mock_checksum_message app_name, app_version
+ subject.build_checksum
+ expect(app_sha_path).to exist
+ expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path))
+ end
+ end
+ context "when building in the current working directory" do
+ it "creates a .sha512 file" do
+ mock_build_message app_name, app_version
+ mock_checksum_message app_name, app_version
+ Dir.chdir app_path do
+ Bundler::GemHelper.new.build_checksum
+ end
+ expect(app_sha_path).to exist
+ expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path))
+ end
+ end
+ context "when building in a location relative to the current working directory" do
+ it "creates a .sha512 file" do
+ mock_build_message app_name, app_version
+ mock_checksum_message app_name, app_version
+ Dir.chdir File.dirname(app_path) do
+ Bundler::GemHelper.new(File.basename(app_path)).build_checksum
+ end
+ expect(app_sha_path).to exist
+ expect(File.read(app_sha_path).chomp).to eql(sha512_hexdigest(app_gem_path))
+ end
+ end
+ end
+
+ describe "#install_gem" do
+ context "when installation was successful" do
+ it "gem is installed" do
+ mock_build_message app_name, app_version
+ mock_confirm_message "#{app_name} (#{app_version}) installed."
+ subject.install_gem(nil, :local)
+ expect(app_gem_path).to exist
+ installed_gems_list
+ expect(out).to include("#{app_name} (#{app_version})")
+ end
+ end
+
+ context "when installation fails" do
+ it "raises an error with appropriate message" do
+ # create empty gem file in order to simulate install failure
+ allow(subject).to receive(:build_gem) do
+ FileUtils.mkdir_p(app_gem_dir)
+ FileUtils.touch app_gem_path
+ app_gem_path
+ end
+ expect { subject.install_gem }.to raise_error(/Running `#{gem_bin} install #{app_gem_path}` failed/)
+ end
+ end
+ end
+
+ describe "rake release" do
+ let!(:rake_application) { Rake.application }
+
+ before(:each) do
+ Rake.application = Rake::Application.new
+ subject.install
+ end
+
+ after(:each) do
+ Rake.application = rake_application
+ end
+
+ before do
+ git("init", app_path)
+ git("config user.email \"you@example.com\"", app_path)
+ git("config user.name \"name\"", app_path)
+ git("config commit.gpgsign false", app_path)
+ git("config push.default simple", app_path)
+
+ # silence messages
+ allow(Bundler.ui).to receive(:confirm)
+ allow(Bundler.ui).to receive(:error)
+ end
+
+ context "fails" do
+ it "when there are unstaged files" do
+ expect { Rake.application["release"].invoke }.
+ to raise_error("There are files that need to be committed first.")
+ end
+
+ it "when there are uncommitted files" do
+ git("add .", app_path)
+ expect { Rake.application["release"].invoke }.
+ to raise_error("There are files that need to be committed first.")
+ end
+
+ it "when there is no git remote" do
+ git("commit -a -m \"initial commit\"", app_path)
+ expect { Rake.application["release"].invoke }.to raise_error(RuntimeError)
+ end
+ end
+
+ context "succeeds" do
+ let(:repo) { build_git("foo", bare: true) }
+
+ before do
+ git("remote add origin #{repo.path}", app_path)
+ git('commit -a -m "initial commit"', app_path)
+ end
+
+ context "on releasing" do
+ before do
+ mock_build_message app_name, app_version
+ mock_confirm_message "Tagged v#{app_version}."
+ mock_confirm_message "Pushed git commits and release tag."
+
+ git("push -u origin main", app_path)
+ end
+
+ it "calls rubygem_push with proper arguments" do
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ Rake.application["release"].invoke
+ end
+
+ it "uses Kernel.system" do
+ cmd = gem_bin.shellsplit
+ expect(Kernel).to receive(:system).with(*cmd, "push", app_gem_path.to_s, "--host", "http://example.org").and_return(true)
+
+ Rake.application["release"].invoke
+ end
+
+ it "also works when releasing from an ambiguous reference" do
+ # Create a branch with the same name as the tag
+ git("checkout -b v#{app_version}", app_path)
+ git("push -u origin v#{app_version}", app_path)
+
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ Rake.application["release"].invoke
+ end
+
+ it "also works with releasing from a branch not yet pushed" do
+ git("checkout -b module_function", app_path)
+
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ Rake.application["release"].invoke
+ end
+ end
+
+ context "on releasing with a custom tag prefix" do
+ before do
+ Bundler::GemHelper.tag_prefix = "foo-"
+ mock_build_message app_name, app_version
+ mock_confirm_message "Pushed git commits and release tag."
+
+ git("push -u origin main", app_path)
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+ end
+
+ it "prepends the custom prefix to the tag" do
+ mock_confirm_message "Tagged foo-v#{app_version}."
+
+ Rake.application["release"].invoke
+ end
+ end
+
+ it "even if tag already exists" do
+ mock_build_message app_name, app_version
+ mock_confirm_message "Tag v#{app_version} has already been created."
+ expect(subject).to receive(:rubygem_push).with(app_gem_path.to_s)
+
+ git("tag -a -m \"Version #{app_version}\" v#{app_version}", app_path)
+
+ Rake.application["release"].invoke
+ end
+ end
+ end
+
+ describe "release:rubygem_push" do
+ let!(:rake_application) { Rake.application }
+
+ before(:each) do
+ Rake.application = Rake::Application.new
+ subject.install
+ allow(subject).to receive(:sh_with_input)
+ end
+
+ after(:each) do
+ Rake.application = rake_application
+ end
+
+ before do
+ git("init", app_path)
+ git("config user.email \"you@example.com\"", app_path)
+ git("config user.name \"name\"", app_path)
+ git("config push.gpgsign simple", app_path)
+
+ # silence messages
+ allow(Bundler.ui).to receive(:confirm)
+ allow(Bundler.ui).to receive(:error)
+
+ credentials = double("credentials", "file?" => true)
+ allow(Bundler.user_home).to receive(:join).
+ with(".gem/credentials").and_return(credentials)
+ allow(Bundler.user_home).to receive(:join).and_call_original
+ end
+
+ describe "success messaging" do
+ context "No allowed_push_host set" do
+ before do
+ allow(subject).to receive(:allowed_push_host).and_return(nil)
+ end
+
+ around do |example|
+ orig_host = ENV["RUBYGEMS_HOST"]
+ ENV["RUBYGEMS_HOST"] = rubygems_host_env
+
+ example.run
+
+ ENV["RUBYGEMS_HOST"] = orig_host
+ end
+
+ context "RUBYGEMS_HOST env var is set" do
+ let(:rubygems_host_env) { "https://custom.env.gemhost.com" }
+
+ it "should report successful push to the host from the environment" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to #{rubygems_host_env}"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+
+ context "RUBYGEMS_HOST env var is not set" do
+ let(:rubygems_host_env) { nil }
+
+ it "should report successful push to rubygems.org" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+
+ context "RUBYGEMS_HOST env var is an empty string" do
+ let(:rubygems_host_env) { "" }
+
+ it "should report successful push to rubygems.org" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to rubygems.org"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+ end
+
+ context "allowed_push_host set in gemspec" do
+ before do
+ allow(subject).to receive(:allowed_push_host).and_return("https://my.gemhost.com")
+ end
+
+ it "should report successful push to the allowed gem host" do
+ mock_confirm_message "Pushed #{app_name} #{app_version} to https://my.gemhost.com"
+
+ Rake.application["release:rubygem_push"].invoke
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/gem_version_promoter_spec.rb b/spec/bundler/bundler/gem_version_promoter_spec.rb
new file mode 100644
index 0000000000..0e1b7c9cc8
--- /dev/null
+++ b/spec/bundler/bundler/gem_version_promoter_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::GemVersionPromoter do
+ let(:gvp) { described_class.new }
+
+ # Rightmost (highest array index) in result is most preferred.
+ # Leftmost (lowest array index) in result is least preferred.
+ # `build_candidates` has all versions of gem in index.
+ # `build_spec` is the version currently in the .lock file.
+ #
+ # In default (not strict) mode, all versions in the index will
+ # be returned, allowing Bundler the best chance to resolve all
+ # dependencies, but sometimes resulting in upgrades that some
+ # would not consider conservative.
+
+ describe "#sort_versions" do
+ def build_candidates(versions)
+ versions.map do |v|
+ Bundler::Resolver::Candidate.new(v)
+ end
+ end
+
+ def build_package(name, version, unlock)
+ Bundler::Resolver::Package.new(name, [], locked_specs: Bundler::SpecSet.new(build_spec(name, version)), unlock: unlock)
+ end
+
+ def sorted_versions(candidates:, current:, unlock: true)
+ gvp.sort_versions(
+ build_package("foo", current, unlock),
+ build_candidates(candidates)
+ ).flatten.map(&:version).map(&:to_s)
+ end
+
+ it "numerically sorts versions" do
+ versions = sorted_versions(candidates: %w[1.7.7 1.7.8 1.7.9 1.7.15 1.8.0], current: "1.7.8")
+ expect(versions).to eq %w[1.8.0 1.7.15 1.7.9 1.7.8 1.7.7]
+ end
+
+ context "with no options" do
+ it "defaults to level=:major, strict=false, pre=false" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
+ end
+ end
+
+ context "when strict" do
+ before { gvp.strict = true }
+
+ context "when level is major" do
+ before { gvp.level = :major }
+
+ it "keeps downgrades" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
+ end
+ end
+
+ context "when level is minor" do
+ before { gvp.level = :minor }
+
+ it "sorts highest minor within same major in first position" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0]
+ end
+ end
+
+ context "when level is patch" do
+ before { gvp.level = :patch }
+
+ it "sorts highest patch within same minor in first position" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0]
+ end
+ end
+ end
+
+ context "when not strict" do
+ before { gvp.strict = false }
+
+ context "when level is major" do
+ before { gvp.level = :major }
+
+ it "orders by version" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[2.1.0 2.0.1 1.0.0 0.9.0 0.3.1 0.3.0 0.2.0]
+ end
+ end
+
+ context "when level is minor" do
+ before { gvp.level = :minor }
+
+ it "favors minor upgrades, then patch upgrades, then major upgrades, then downgrades" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.9.0 0.3.1 0.3.0 1.0.0 2.1.0 2.0.1 0.2.0]
+ end
+ end
+
+ context "when level is patch" do
+ before { gvp.level = :patch }
+
+ it "favors patch upgrades, then minor upgrades, then major upgrades, then downgrades" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.0.1 2.1.0], current: "0.3.0")
+ expect(versions).to eq %w[0.3.1 0.3.0 0.9.0 1.0.0 2.0.1 2.1.0 0.2.0]
+ end
+ end
+ end
+
+ context "when pre" do
+ before { gvp.pre = true }
+
+ it "sorts regardless of prerelease status" do
+ versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0")
+ expect(versions).to eq %w[2.0.0 2.0.0.pre 1.8.1 1.8.1.pre 1.8.0 1.7.7.pre]
+ end
+ end
+
+ context "when not pre" do
+ before { gvp.pre = false }
+
+ it "deprioritizes prerelease gems" do
+ versions = sorted_versions(candidates: %w[1.7.7.pre 1.8.0 1.8.1.pre 1.8.1 2.0.0.pre 2.0.0], current: "1.8.0")
+ expect(versions).to eq %w[2.0.0 1.8.1 1.8.0 2.0.0.pre 1.8.1.pre 1.7.7.pre]
+ end
+ end
+
+ context "when locking and not major" do
+ before { gvp.level = :minor }
+
+ it "keeps the current version first" do
+ versions = sorted_versions(candidates: %w[0.2.0 0.3.0 0.3.1 0.9.0 1.0.0 2.1.0 2.0.1], current: "0.3.0", unlock: [])
+ expect(versions.first).to eq("0.3.0")
+ end
+ end
+ end
+
+ describe "#level=" do
+ subject { described_class.new }
+
+ it "should raise if not major, minor or patch is passed" do
+ expect { subject.level = :minjor }.to raise_error ArgumentError
+ end
+
+ it "should raise if invalid classes passed" do
+ [123, nil].each do |value|
+ expect { subject.level = value }.to raise_error ArgumentError
+ end
+ end
+
+ it "should accept major, minor patch symbols" do
+ [:major, :minor, :patch].each do |value|
+ subject.level = value
+ expect(subject.level).to eq value
+ end
+ end
+
+ it "should accept major, minor patch strings" do
+ %w[major minor patch].each do |value|
+ subject.level = value
+ expect(subject.level).to eq value.to_sym
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/index_spec.rb b/spec/bundler/bundler/index_spec.rb
new file mode 100644
index 0000000000..0f3f6e4944
--- /dev/null
+++ b/spec/bundler/bundler/index_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Index do
+ let(:specs) { [] }
+ subject { described_class.build {|i| i.use(specs) } }
+
+ context "specs with a nil platform" do
+ let(:spec) do
+ Gem::Specification.new do |s|
+ s.name = "json"
+ s.version = "1.8.3"
+ allow(s).to receive(:platform).and_return(nil)
+ end
+ end
+ let(:specs) { [spec] }
+
+ describe "#search_by_spec" do
+ it "finds the spec when a nil platform is specified" do
+ expect(subject.search(spec)).to eq([spec])
+ end
+
+ it "finds the spec when a ruby platform is specified" do
+ query = spec.dup.tap {|s| s.platform = "ruby" }
+ expect(subject.search(query)).to eq([spec])
+ end
+ end
+ end
+
+ context "with specs that include development dependencies" do
+ let(:specs) { [*build_spec("a", "1.0.0") {|s| s.development("b", "~> 1.0") }] }
+
+ it "does not include b in #dependency_names" do
+ expect(subject.dependency_names).not_to include("b")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/installer/gem_installer_spec.rb b/spec/bundler/bundler/installer/gem_installer_spec.rb
new file mode 100644
index 0000000000..dbd4e1d2c8
--- /dev/null
+++ b/spec/bundler/bundler/installer/gem_installer_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require "bundler/installer/gem_installer"
+
+RSpec.describe Bundler::GemInstaller do
+ let(:definition) { instance_double("Definition", locked_gems: nil) }
+ let(:installer) { instance_double("Installer", definition: definition) }
+ let(:spec_source) { instance_double("SpecSource") }
+ let(:spec) { instance_double("Specification", name: "dummy", version: "0.0.1", loaded_from: "dummy", source: spec_source) }
+ let(:base_options) { { force: false, local: false, previous_spec: nil } }
+
+ subject { described_class.new(spec, installer) }
+
+ context "spec_settings is nil" do
+ it "invokes install method with empty build_args" do
+ allow(spec_source).to receive(:install).with(
+ spec,
+ base_options.merge(build_args: [])
+ )
+ subject.install_from_spec
+ end
+ end
+
+ context "spec_settings is build option" do
+ it "invokes install method with build_args" do
+ allow(Bundler.settings).to receive(:[])
+ allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy")
+ expect(spec_source).to receive(:install).with(
+ spec,
+ base_options.merge(build_args: ["--with-dummy-config=dummy"])
+ )
+ subject.install_from_spec
+ end
+ end
+
+ context "spec_settings is build option with spaces" do
+ it "invokes install method with build_args" do
+ allow(Bundler.settings).to receive(:[])
+ allow(Bundler.settings).to receive(:[]).with("build.dummy").and_return("--with-dummy-config=dummy --with-another-dummy-config")
+ expect(spec_source).to receive(:install).with(
+ spec,
+ base_options.merge(build_args: ["--with-dummy-config=dummy", "--with-another-dummy-config"])
+ )
+ subject.install_from_spec
+ end
+ end
+end
diff --git a/spec/bundler/bundler/installer/parallel_installer_spec.rb b/spec/bundler/bundler/installer/parallel_installer_spec.rb
new file mode 100644
index 0000000000..49bcb5310b
--- /dev/null
+++ b/spec/bundler/bundler/installer/parallel_installer_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "bundler/installer/parallel_installer"
+require "bundler/rubygems_gem_installer"
+require "rubygems/remote_fetcher"
+require "bundler"
+
+RSpec.describe Bundler::ParallelInstaller do
+ describe "priority queue" do
+ before do
+ require "support/artifice/compact_index"
+
+ @previous_client = Gem::Request::ConnectionPools.client
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+ Gem::RemoteFetcher.fetcher.close_all
+
+ build_repo2 do
+ build_gem "gem_with_extension", &:add_c_extension
+ build_gem "gem_without_extension"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "gem_with_extension"
+ gem "gem_without_extension"
+ G
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ gem_with_extension (1.0)
+ gem_without_extension (1.0)
+
+ DEPENDENCIES
+ gem_with_extension
+ gem_without_extension
+ L
+
+ @old_ui = Bundler.ui
+ Bundler.ui = Bundler::UI::Silent.new
+ end
+
+ after do
+ Bundler.ui = @old_ui
+ Gem::Request::ConnectionPools.client = @previous_client
+ Artifice.deactivate
+ end
+
+ let(:definition) do
+ allow(Bundler).to receive(:root) { bundled_app }
+
+ definition = Bundler::Definition.build(bundled_app.join("Gemfile"), bundled_app.join("Gemfile.lock"), false)
+ definition.tap(&:setup_domain!)
+ end
+ let(:installer) { Bundler::Installer.new(bundled_app, definition) }
+
+ it "queues native extensions in priority" do
+ parallel_installer = Bundler::ParallelInstaller.new(installer, definition.specs, 2, false, true)
+ worker_pool = parallel_installer.send(:worker_pool)
+ expected = 6 # Enqueue to download bundler and the 2 gems. Enqueue to install Bundler and the 2 gems.
+
+ expect(worker_pool).to receive(:enq).exactly(expected).times.and_wrap_original do |original_enq, spec, opts|
+ unless opts.nil? # Enqueued for download, no priority
+ if spec.name == "gem_with_extension"
+ expect(opts).to eq({ priority: true })
+ else
+ expect(opts).to eq({ priority: false })
+ end
+ end
+
+ opts ||= {}
+ original_enq.call(spec, **opts)
+ end
+
+ parallel_installer.call
+ end
+ end
+end
diff --git a/spec/bundler/bundler/installer/spec_installation_spec.rb b/spec/bundler/bundler/installer/spec_installation_spec.rb
new file mode 100644
index 0000000000..57868766d9
--- /dev/null
+++ b/spec/bundler/bundler/installer/spec_installation_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "bundler/installer/parallel_installer"
+
+RSpec.describe Bundler::ParallelInstaller::SpecInstallation do
+ def build_spec(name, extensions: [])
+ spec = Object.new
+ spec.define_singleton_method(:name) { name }
+ spec.define_singleton_method(:full_name) { "#{name}-1.0" }
+ spec.define_singleton_method(:extensions) { extensions }
+ spec.define_singleton_method(:dependencies) { [] }
+ spec
+ end
+
+ let!(:dep) { build_spec("I like tests") }
+
+ describe "#ready_to_enqueue?" do
+ context "when in enqueued state" do
+ it "is falsey" do
+ spec = described_class.new(dep)
+ spec.state = :enqueued
+ expect(spec.ready_to_enqueue?).to be_falsey
+ end
+ end
+
+ context "when in installed state" do
+ it "returns falsey" do
+ spec = described_class.new(dep)
+ spec.state = :installed
+ expect(spec.ready_to_enqueue?).to be_falsey
+ end
+ end
+
+ it "returns truthy" do
+ spec = described_class.new(dep)
+ expect(spec.ready_to_enqueue?).to be_truthy
+ end
+ end
+
+ describe "#dependencies_installed?" do
+ it "returns true when all dependencies are installed" do
+ alpha = described_class.new(build_spec("alpha"))
+ alpha.dependencies = []
+
+ beta = described_class.new(build_spec("beta"))
+ beta.dependencies = [alpha]
+
+ gamma = described_class.new(build_spec("gamma"))
+ gamma.dependencies = [beta]
+
+ expect(gamma.dependencies_installed?({})).to be_falsey
+ expect(gamma.dependencies_installed?({ "beta" => true })).to be_falsey
+ expect(gamma.dependencies_installed?({ "alpha" => true, "beta" => true })).to be_truthy
+ end
+ end
+
+ describe "#ready_to_install?" do
+ context "when spec has no extensions" do
+ it "returns true regardless of dependencies" do
+ beta = described_class.new(build_spec("beta"))
+ beta.dependencies = []
+
+ spec = described_class.new(dep)
+ spec.state = :downloaded
+ spec.dependencies = [beta]
+
+ expect(spec.ready_to_install?({})).to be_truthy
+ end
+ end
+
+ context "when spec has extensions" do
+ it "returns true when all dependencies are installed" do
+ alpha = described_class.new(build_spec("alpha"))
+ alpha.dependencies = []
+
+ beta = described_class.new(build_spec("beta"))
+ beta.dependencies = [alpha]
+
+ gamma = described_class.new(build_spec("gamma", extensions: ["ext/Rakefile"]))
+ gamma.state = :downloaded
+ gamma.dependencies = [beta]
+
+ expect(gamma.ready_to_install?({})).to be_falsey
+ expect(gamma.ready_to_install?({ "beta" => true })).to be_falsey
+ expect(gamma.ready_to_install?({ "alpha" => true, "beta" => true })).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/lockfile_parser_spec.rb b/spec/bundler/bundler/lockfile_parser_spec.rb
new file mode 100644
index 0000000000..7364ab98e5
--- /dev/null
+++ b/spec/bundler/bundler/lockfile_parser_spec.rb
@@ -0,0 +1,303 @@
+# frozen_string_literal: true
+
+require "bundler/lockfile_parser"
+
+RSpec.describe Bundler::LockfileParser do
+ let(:lockfile_contents) { <<~L }
+ GIT
+ remote: https://github.com/alloy/peiji-san.git
+ revision: eca485d8dc95f12aaec1a434b49d295c7e91844b
+ specs:
+ peiji-san (1.2.0)
+
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (10.3.2)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ peiji-san!
+ rake
+
+ CHECKSUMS
+ rake (10.3.2) sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8
+
+ RUBY VERSION
+ ruby 2.1.3p242
+
+ BUNDLED WITH
+ 1.12.0.rc.2
+ L
+
+ describe ".sections_in_lockfile" do
+ it "returns the attributes" do
+ attributes = described_class.sections_in_lockfile(lockfile_contents)
+ expect(attributes).to contain_exactly(
+ "BUNDLED WITH", "CHECKSUMS", "DEPENDENCIES", "GEM", "GIT", "PLATFORMS", "RUBY VERSION"
+ )
+ end
+ end
+
+ describe ".unknown_sections_in_lockfile" do
+ let(:lockfile_contents) { <<~L }
+ UNKNOWN ATTR
+
+ UNKNOWN ATTR 2
+ random contents
+ L
+
+ it "returns the unknown attributes" do
+ attributes = described_class.unknown_sections_in_lockfile(lockfile_contents)
+ expect(attributes).to contain_exactly("UNKNOWN ATTR", "UNKNOWN ATTR 2")
+ end
+ end
+
+ describe ".sections_to_ignore" do
+ subject { described_class.sections_to_ignore(base_version) }
+
+ context "with a nil base version" do
+ let(:base_version) { nil }
+
+ it "returns the same as > 1.0" do
+ expect(subject).to contain_exactly(
+ described_class::BUNDLED, described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN
+ )
+ end
+ end
+
+ context "with a prerelease base version" do
+ let(:base_version) { Gem::Version.create("1.11.0.rc.1") }
+
+ it "returns the same as for the release version" do
+ expect(subject).to contain_exactly(
+ described_class::CHECKSUMS, described_class::RUBY, described_class::PLUGIN
+ )
+ end
+ end
+
+ context "with a current version" do
+ let(:base_version) { Gem::Version.create(Bundler::VERSION) }
+
+ it "returns an empty array" do
+ expect(subject).to eq([])
+ end
+ end
+
+ context "with a future version" do
+ let(:base_version) { Gem::Version.create("5.5.5") }
+
+ it "returns an empty array" do
+ expect(subject).to eq([])
+ end
+ end
+ end
+
+ describe "#initialize" do
+ before { allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app("gems.rb")) }
+ subject { described_class.new(lockfile_contents) }
+
+ let(:sources) do
+ [Bundler::Source::Git.new("uri" => "https://github.com/alloy/peiji-san.git", "revision" => "eca485d8dc95f12aaec1a434b49d295c7e91844b"),
+ Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"])]
+ end
+ let(:dependencies) do
+ {
+ "peiji-san" => Bundler::Dependency.new("peiji-san", ">= 0"),
+ "rake" => Bundler::Dependency.new("rake", ">= 0"),
+ }
+ end
+ let(:specs) do
+ [
+ Bundler::LazySpecification.new("peiji-san", v("1.2.0"), Gem::Platform::RUBY),
+ Bundler::LazySpecification.new("rake", v("10.3.2"), Gem::Platform::RUBY),
+ ]
+ end
+ let(:platforms) { [Gem::Platform::RUBY] }
+ let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") }
+ let(:ruby_version) { "ruby 2.1.3p242" }
+ let(:lockfile_path) { Bundler.default_lockfile.relative_path_from(Dir.pwd) }
+ let(:rake_sha256_checksum) do
+ Bundler::Checksum.from_lock(
+ "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
+ "#{lockfile_path}:20:17"
+ )
+ end
+ let(:rake_checksums) { [rake_sha256_checksum] }
+
+ shared_examples_for "parsing" do
+ it "parses correctly" do
+ expect(subject.valid?).to be(true)
+ expect(subject.sources).to eq sources
+ expect(subject.dependencies).to eq dependencies
+ expect(subject.specs).to eq specs
+ expect(Hash[subject.specs.map {|s| [s, s.dependencies] }]).to eq Hash[subject.specs.map {|s| [s, s.dependencies] }]
+ expect(subject.platforms).to eq platforms
+ expect(subject.bundler_version).to eq bundler_version
+ expect(subject.ruby_version).to eq ruby_version
+ rake_spec = specs.last
+ checksums = subject.sources.last.checksum_store.to_lock(specs.last)
+ expect(checksums).to eq("#{rake_spec.lock_name} #{rake_checksums.map(&:to_lock).sort.join(",")}")
+ end
+ end
+
+ include_examples "parsing"
+
+ context "when an extra section is at the end" do
+ let(:lockfile_contents) { super() + "\n\nFOO BAR\n baz\n baa\n qux\n" }
+ include_examples "parsing"
+ end
+
+ context "when an extra section is at the start" do
+ let(:lockfile_contents) { "FOO BAR\n baz\n baa\n qux\n\n" + super() }
+ include_examples "parsing"
+ end
+
+ context "when an extra section is in the middle" do
+ let(:lockfile_contents) { super().split(/(?=GEM)/).insert(1, "FOO BAR\n baz\n baa\n qux\n\n").join }
+ include_examples "parsing"
+ end
+
+ context "when a dependency has options" do
+ let(:lockfile_contents) { super().sub("peiji-san!", "peiji-san!\n foo: bar") }
+ include_examples "parsing"
+ end
+
+ context "when the checksum is urlsafe base64 encoded" do
+ let(:lockfile_contents) do
+ super().sub(
+ "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
+ "sha256=gUgow08TFdfnt-gpUYRXfMTpabrWFWrAadAtY_WNgug="
+ )
+ end
+ include_examples "parsing"
+ end
+
+ context "when the checksum is of an unknown algorithm" do
+ let(:rake_sha512_checksum) do
+ Bundler::Checksum.from_lock(
+ "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==",
+ "#{lockfile_path}:20:17"
+ )
+ end
+ let(:lockfile_contents) do
+ super().sub(
+ "sha256=",
+ "sha512=pVDn9GLmcFkz8vj1ueiVxj5uGKkAyaqYjEX8zG6L5O4BeVg3wANaKbQdpj/B82Nd/MHVszy6polHcyotUdwilQ==,sha256="
+ )
+ end
+ let(:rake_checksums) { [rake_sha256_checksum, rake_sha512_checksum] }
+ include_examples "parsing"
+ end
+
+ context "when the content does not contain any recognized lockfile sections" do
+ let(:lockfile_contents) { "hello world\nlorem ipsum\n" }
+
+ it "does not raise, is not valid, and deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+
+ it "does not raise when strict: true, and still deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents, strict: true)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+ end
+
+ context "when the content looks like a Gemfile DSL" do
+ let(:lockfile_contents) { <<~G }
+ source "https://rubygems.org"
+ gem "rake"
+ G
+
+ it "does not raise, is not valid, and deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+
+ it "does not raise when strict: true, and still deprecates" do
+ expect(Bundler::SharedHelpers).to receive(:feature_deprecated!).with(
+ /does not appear to be a valid lockfile.*future version of Bundler/m
+ )
+ parser = described_class.new(lockfile_contents, strict: true)
+ expect(parser.valid?).to be(false)
+ expect(parser.specs).to eq([])
+ expect(parser.dependencies).to eq({})
+ end
+ end
+
+ context "when the content is empty" do
+ let(:lockfile_contents) { "" }
+
+ it "does not raise and is valid" do
+ expect { subject }.not_to raise_error
+ expect(subject.valid?).to be(true)
+ end
+ end
+
+ context "when lockfile_path is given" do
+ it "uses the provided path in error messages instead of looking up Bundler.default_lockfile" do
+ expect(Bundler::SharedHelpers).not_to receive(:relative_lockfile_path)
+ parser = described_class.new(lockfile_contents, lockfile_path: "custom/path.lock")
+ expect(parser.valid?).to be(true)
+ rake_spec = parser.specs.last
+ checksums = parser.sources.last.checksum_store.to_lock(rake_spec)
+ expected_checksum = Bundler::Checksum.from_lock(
+ "sha256=814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
+ "custom/path.lock:20:17"
+ )
+ expect(checksums).to eq("#{rake_spec.lock_name} #{expected_checksum.to_lock}")
+ end
+
+ it "raises with the provided path when the lockfile contains merge conflicts" do
+ expect do
+ described_class.new("<<<<<<<\n", lockfile_path: "custom/path.lock")
+ end.to raise_error(Bundler::LockfileError, %r{custom/path\.lock contains merge conflicts})
+ end
+ end
+
+ context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do
+ let(:bad_checksum) { "sha256=c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" }
+ let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join }
+
+ it "raises a security error" do
+ expect { subject }.to raise_error(Bundler::SecurityError) do |e|
+ expect(e.message).to match <<~MESSAGE
+ Bundler found mismatched checksums. This is a potential security risk.
+ rake (10.3.2) #{bad_checksum}
+ from the lockfile CHECKSUMS at #{lockfile_path}:20:17
+ rake (10.3.2) #{rake_sha256_checksum.to_lock}
+ from the lockfile CHECKSUMS at #{lockfile_path}:21:17
+
+ To resolve this issue you can either:
+ 1. remove the matching checksum in #{lockfile_path}:21:17
+ 2. run `bundle install`
+ or if you are sure that the new checksum from the lockfile CHECKSUMS at #{lockfile_path}:21:17 is correct:
+ 1. remove the matching checksum in #{lockfile_path}:20:17
+ 2. run `bundle install`
+
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ MESSAGE
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/mirror_spec.rb b/spec/bundler/bundler/mirror_spec.rb
new file mode 100644
index 0000000000..ba1c6ed413
--- /dev/null
+++ b/spec/bundler/bundler/mirror_spec.rb
@@ -0,0 +1,331 @@
+# frozen_string_literal: true
+
+require "bundler/mirror"
+
+RSpec.describe Bundler::Settings::Mirror do
+ let(:mirror) { Bundler::Settings::Mirror.new }
+
+ it "returns zero when fallback_timeout is not set" do
+ expect(mirror.fallback_timeout).to eq(0)
+ end
+
+ it "takes a number as a fallback_timeout" do
+ mirror.fallback_timeout = 1
+ expect(mirror.fallback_timeout).to eq(1)
+ end
+
+ it "takes truthy as a default fallback timeout" do
+ mirror.fallback_timeout = true
+ expect(mirror.fallback_timeout).to eq(0.1)
+ end
+
+ it "takes falsey as a zero fallback timeout" do
+ mirror.fallback_timeout = false
+ expect(mirror.fallback_timeout).to eq(0)
+ end
+
+ it "takes a string with 'true' as a default fallback timeout" do
+ mirror.fallback_timeout = "true"
+ expect(mirror.fallback_timeout).to eq(0.1)
+ end
+
+ it "takes a string with 'false' as a zero fallback timeout" do
+ mirror.fallback_timeout = "false"
+ expect(mirror.fallback_timeout).to eq(0)
+ end
+
+ it "takes a string for the uri but returns an uri object" do
+ mirror.uri = "http://localhost:9292"
+ expect(mirror.uri).to eq(Gem::URI("http://localhost:9292"))
+ end
+
+ it "takes an uri object for the uri" do
+ mirror.uri = Gem::URI("http://localhost:9293")
+ expect(mirror.uri).to eq(Gem::URI("http://localhost:9293"))
+ end
+
+ context "without a uri" do
+ it "invalidates the mirror" do
+ mirror.validate!
+ expect(mirror.valid?).to be_falsey
+ end
+ end
+
+ context "with an uri" do
+ before { mirror.uri = "http://localhost:9292" }
+
+ context "without a fallback timeout" do
+ it "is not valid by default" do
+ expect(mirror.valid?).to be_falsey
+ end
+
+ context "when probed" do
+ let(:probe) { double }
+
+ context "with a replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(true)
+ mirror.validate!(probe)
+ end
+
+ it "is valid" do
+ expect(mirror.valid?).to be_truthy
+ end
+ end
+
+ context "with a non replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(false)
+ mirror.validate!(probe)
+ end
+
+ it "is still valid" do
+ expect(mirror.valid?).to be_truthy
+ end
+ end
+ end
+ end
+
+ context "with a fallback timeout" do
+ before { mirror.fallback_timeout = 1 }
+
+ it "is not valid by default" do
+ expect(mirror.valid?).to be_falsey
+ end
+
+ context "when probed" do
+ let(:probe) { double }
+
+ context "with a replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(true)
+ mirror.validate!(probe)
+ end
+
+ it "is valid" do
+ expect(mirror.valid?).to be_truthy
+ end
+
+ it "is validated only once" do
+ allow(probe).to receive(:replies?).and_raise("Only once!")
+ mirror.validate!(probe)
+ expect(mirror.valid?).to be_truthy
+ end
+ end
+
+ context "with a non replying mirror" do
+ before do
+ allow(probe).to receive(:replies?).and_return(false)
+ mirror.validate!(probe)
+ end
+
+ it "is not valid" do
+ expect(mirror.valid?).to be_falsey
+ end
+
+ it "is validated only once" do
+ allow(probe).to receive(:replies?).and_raise("Only once!")
+ mirror.validate!(probe)
+ expect(mirror.valid?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe "#==" do
+ it "returns true if uri and fallback timeout are the same" do
+ uri = "https://ruby.taobao.org"
+ mirror = Bundler::Settings::Mirror.new(uri, 1)
+ another_mirror = Bundler::Settings::Mirror.new(uri, 1)
+
+ expect(mirror == another_mirror).to be true
+ end
+ end
+ end
+end
+
+RSpec.describe Bundler::Settings::Mirrors do
+ let(:localhost_uri) { Gem::URI("http://localhost:9292") }
+
+ context "with a just created mirror" do
+ let(:mirrors) do
+ probe = double
+ allow(probe).to receive(:replies?).and_return(true)
+ Bundler::Settings::Mirrors.new(probe)
+ end
+
+ it "returns a mirror that contains the source uri for an unknown uri" do
+ mirror = mirrors.for("http://rubygems.org/")
+ expect(mirror).to eq(Bundler::Settings::Mirror.new("http://rubygems.org/"))
+ end
+
+ it "parses a mirror key and returns a mirror for the parsed uri" do
+ mirrors.parse("mirror.http://rubygems.org/", localhost_uri)
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri)
+ end
+
+ it "parses a relative mirror key and returns a mirror for the parsed http uri" do
+ mirrors.parse("mirror.rubygems.org", localhost_uri)
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri)
+ end
+
+ it "parses a relative mirror key and returns a mirror for the parsed https uri" do
+ mirrors.parse("mirror.rubygems.org", localhost_uri)
+ expect(mirrors.for("https://rubygems.org/").uri).to eq(localhost_uri)
+ end
+
+ context "with a uri parsed already" do
+ before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) }
+
+ it "takes a mirror fallback_timeout and assigns the timeout" do
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "2")
+ expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(2)
+ end
+
+ it "parses a 'true' fallback timeout and sets the default timeout" do
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true")
+ expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0.1)
+ end
+
+ it "parses a 'false' fallback timeout and sets it to zero" do
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "false")
+ expect(mirrors.for("http://rubygems.org/").fallback_timeout).to eq(0)
+ end
+ end
+ end
+
+ context "with a mirror prober that replies on time" do
+ let(:mirrors) do
+ probe = double
+ allow(probe).to receive(:replies?).and_return(true)
+ Bundler::Settings::Mirrors.new(probe)
+ end
+
+ context "with a default fallback_timeout for rubygems.org" do
+ before do
+ mirrors.parse("mirror.http://rubygems.org/", localhost_uri)
+ mirrors.parse("mirror.http://rubygems.org.fallback_timeout", "true")
+ end
+
+ it "returns localhost" do
+ expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri)
+ end
+ end
+
+ context "with a mirror for all" do
+ before do
+ mirrors.parse("mirror.all", localhost_uri)
+ end
+
+ context "without a fallback timeout" do
+ it "returns localhost uri for rubygems" do
+ expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri)
+ end
+
+ it "returns localhost for any other url" do
+ expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri)
+ end
+ end
+ context "with a fallback timeout" do
+ before { mirrors.parse("mirror.all.fallback_timeout", "1") }
+
+ it "returns localhost uri for rubygems" do
+ expect(mirrors.for("http://rubygems.org").uri).to eq(localhost_uri)
+ end
+
+ it "returns localhost for any other url" do
+ expect(mirrors.for("http://whatever.com/").uri).to eq(localhost_uri)
+ end
+ end
+ end
+ end
+
+ context "with a mirror prober that does not reply on time" do
+ let(:mirrors) do
+ probe = double
+ allow(probe).to receive(:replies?).and_return(false)
+ Bundler::Settings::Mirrors.new(probe)
+ end
+
+ context "with a localhost mirror for all" do
+ before { mirrors.parse("mirror.all", localhost_uri) }
+
+ context "without a fallback timeout" do
+ it "returns localhost" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(localhost_uri)
+ end
+ end
+
+ context "with a fallback timeout" do
+ before { mirrors.parse("mirror.all.fallback_timeout", "true") }
+
+ it "returns the source uri, not localhost" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/"))
+ end
+ end
+ end
+
+ context "with localhost as a mirror for rubygems.org" do
+ before { mirrors.parse("mirror.http://rubygems.org/", localhost_uri) }
+
+ context "without a fallback timeout" do
+ it "returns the uri that is not mirrored" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/"))
+ end
+
+ it "returns localhost for rubygems.org" do
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(localhost_uri)
+ end
+ end
+
+ context "with a fallback timeout" do
+ before { mirrors.parse("mirror.http://rubygems.org/.fallback_timeout", "true") }
+
+ it "returns the uri that is not mirrored" do
+ expect(mirrors.for("http://whatever.com").uri).to eq(Gem::URI("http://whatever.com/"))
+ end
+
+ it "returns rubygems.org for rubygems.org" do
+ expect(mirrors.for("http://rubygems.org/").uri).to eq(Gem::URI("http://rubygems.org/"))
+ end
+ end
+ end
+ end
+end
+
+RSpec.describe Bundler::Settings::TCPSocketProbe do
+ let(:probe) { Bundler::Settings::TCPSocketProbe.new }
+
+ context "with a listening TCP Server" do
+ def with_server_and_mirror
+ server = TCPServer.new("0.0.0.0", 0)
+ mirror = Bundler::Settings::Mirror.new("http://0.0.0.0:#{server.addr[1]}", 1)
+ yield server, mirror
+ server.close unless server.closed?
+ end
+
+ it "probes the server correctly" do
+ skip "obscure error" if Gem.win_platform?
+
+ with_server_and_mirror do |server, mirror|
+ expect(server.closed?).to be_falsey
+ expect(probe.replies?(mirror)).to be_truthy
+ end
+ end
+
+ it "probes falsey when the server is down" do
+ with_server_and_mirror do |server, mirror|
+ server.close
+ expect(probe.replies?(mirror)).to be_falsey
+ end
+ end
+ end
+
+ context "with an invalid mirror" do
+ let(:mirror) { Bundler::Settings::Mirror.new("http://127.0.0.127:9292", true) }
+
+ it "fails with a timeout when there is nothing to tcp handshake" do
+ expect(probe.replies?(mirror)).to be_falsey
+ end
+ end
+end
diff --git a/spec/bundler/bundler/override_spec.rb b/spec/bundler/bundler/override_spec.rb
new file mode 100644
index 0000000000..ad8be75520
--- /dev/null
+++ b/spec/bundler/bundler/override_spec.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+RSpec.describe "MatchMetadata override-aware checks" do
+ let(:spec_class) do
+ Class.new do
+ include Bundler::MatchMetadata
+ attr_accessor :name
+ def initialize(name, ruby_req, rubygems_req)
+ @name = name
+ @required_ruby_version = ruby_req
+ @required_rubygems_version = rubygems_req
+ end
+ end
+ end
+
+ it "matches_current_metadata? ignores overrides (strict path)" do
+ spec = spec_class.new("rails", Gem::Requirement.new("< #{Gem.ruby_version}"), Gem::Requirement.default)
+ overrides = [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)]
+ # Strict method MUST NOT apply overrides; guards SelfManager and other generic callers.
+ expect(spec.matches_current_metadata?).to be(false)
+ expect(spec.matches_current_metadata_with_overrides?(overrides)).to be(true)
+ end
+
+ it "matches_current_ruby_with_overrides? returns the strict result for an empty override list" do
+ spec = spec_class.new("rails", Gem::Requirement.new(">= #{Gem.ruby_version}"), Gem::Requirement.default)
+ expect(spec.matches_current_ruby_with_overrides?([])).to be(true)
+ expect(spec.matches_current_ruby_with_overrides?(nil)).to be(true)
+ end
+
+ it "matches_current_rubygems_with_overrides? honors :all override" do
+ spec = spec_class.new("rails", Gem::Requirement.default, Gem::Requirement.new("< #{Gem.rubygems_version}"))
+ overrides = [Bundler::Override.new(:all, :required_rubygems_version, :ignore_upper)]
+ expect(spec.matches_current_rubygems_with_overrides?(overrides)).to be(true)
+ end
+end
+
+RSpec.describe "LazySpecification override propagation" do
+ let(:overrides) { [Bundler::Override.new("rails", :required_ruby_version, :ignore_upper)] }
+
+ it "carries overrides forward from a source LazySpec via from_spec" do
+ src = Bundler::LazySpecification.new("rails", "8.0", Gem::Platform::RUBY)
+ src.overrides = overrides
+ derived = Bundler::LazySpecification.from_spec(src)
+ expect(derived.overrides).to eq(overrides)
+ end
+
+ it "does not call respond_to? on the source spec, avoiding gemspec lazy load" do
+ # If from_spec used respond_to?(:overrides), a RemoteSpec source would
+ # force-load the backing gemspec. Use a stand-in object whose
+ # respond_to? raises to prove it is never asked.
+ src = Object.new
+ src.define_singleton_method(:name) { "rails" }
+ src.define_singleton_method(:version) { Gem::Version.new("8.0") }
+ src.define_singleton_method(:platform) { Gem::Platform::RUBY }
+ src.define_singleton_method(:source) { nil }
+ src.define_singleton_method(:runtime_dependencies) { [] }
+ src.define_singleton_method(:required_ruby_version) { Gem::Requirement.default }
+ src.define_singleton_method(:required_rubygems_version) { Gem::Requirement.default }
+ src.define_singleton_method(:respond_to?) {|*| raise "from_spec must not call respond_to?" }
+ expect { Bundler::LazySpecification.from_spec(src) }.not_to raise_error
+ end
+end
+
+RSpec.describe Bundler::Override do
+ describe ".find_for" do
+ it "returns the matching override by target and field" do
+ a = described_class.new("rails", :version, ">= 8.0")
+ b = described_class.new("nokogiri", :version, :ignore_upper)
+ expect(described_class.find_for([a, b], "rails", :version)).to be(a)
+ end
+
+ it "returns nil when no override matches the target" do
+ a = described_class.new("rails", :version, ">= 8.0")
+ expect(described_class.find_for([a], "sinatra", :version)).to be_nil
+ end
+
+ it "returns nil when no override matches the field" do
+ a = described_class.new("rails", :version, ">= 8.0")
+ expect(described_class.find_for([a], "rails", :required_ruby_version)).to be_nil
+ end
+
+ it "returns nil for an empty overrides list" do
+ expect(described_class.find_for([], "rails", :version)).to be_nil
+ end
+
+ it "falls back to an :all override on the same field" do
+ a = described_class.new(:all, :required_ruby_version, :ignore_upper)
+ expect(described_class.find_for([a], "rails", :required_ruby_version)).to be(a)
+ end
+
+ it "prefers a per-gem override over a matching :all override" do
+ per_gem = described_class.new("rails", :required_ruby_version, ">= 3.4")
+ all_target = described_class.new(:all, :required_ruby_version, :ignore_upper)
+ expect(described_class.find_for([all_target, per_gem], "rails", :required_ruby_version)).to be(per_gem)
+ end
+
+ it "does not fall back to :all when the field differs" do
+ a = described_class.new(:all, :required_ruby_version, :ignore_upper)
+ expect(described_class.find_for([a], "rails", :required_rubygems_version)).to be_nil
+ end
+ end
+
+ describe "#apply_to" do
+ context "when operation is a version spec string" do
+ it "replaces the existing requirement entirely" do
+ override = described_class.new("rails", :version, ">= 8.0")
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.new(">= 8.0"))
+ end
+
+ it "ignores the existing requirement regardless of its content" do
+ override = described_class.new("rails", :version, "= 1.0")
+ result = override.apply_to(Gem::Requirement.new(">= 99.0"))
+ expect(result).to eq(Gem::Requirement.new("= 1.0"))
+ end
+ end
+
+ context "when operation is :ignore_upper" do
+ it "removes < and <= operators" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.new(">= 1.0"))
+ end
+
+ it "keeps >, >=, = operators" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new("> 1.0", "<= 2.0"))
+ expect(result).to eq(Gem::Requirement.new("> 1.0"))
+ end
+
+ it "converts ~> to >= preserving the lower bound" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new("~> 1.5"))
+ expect(result).to eq(Gem::Requirement.new(">= 1.5"))
+ end
+
+ it "preserves != exclusion constraints" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "!= 1.5.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.new(">= 1.0", "!= 1.5.0"))
+ end
+
+ it "returns the default requirement when only upper bounds remain" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ result = override.apply_to(Gem::Requirement.new("< 2.0"))
+ expect(result).to eq(Gem::Requirement.default)
+ end
+
+ it "returns the default requirement when the input is nil" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ expect(override.apply_to(nil)).to eq(Gem::Requirement.default)
+ end
+
+ it "returns the default requirement when the input is already the default" do
+ override = described_class.new("rails", :version, :ignore_upper)
+ expect(override.apply_to(Gem::Requirement.default)).to eq(Gem::Requirement.default)
+ end
+ end
+
+ context "when operation is nil" do
+ it "returns the default requirement" do
+ override = described_class.new("rails", :version, nil)
+ result = override.apply_to(Gem::Requirement.new(">= 1.0", "< 2.0"))
+ expect(result).to eq(Gem::Requirement.default)
+ end
+ end
+
+ context "when operation is unsupported" do
+ it "raises ArgumentError" do
+ override = described_class.new("rails", :version, 42)
+ expect { override.apply_to(Gem::Requirement.default) }.to raise_error(ArgumentError, /unsupported override operation/)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/api/source_spec.rb b/spec/bundler/bundler/plugin/api/source_spec.rb
new file mode 100644
index 0000000000..ae02e08bea
--- /dev/null
+++ b/spec/bundler/bundler/plugin/api/source_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::API::Source do
+ let(:uri) { "uri://to/test" }
+ let(:type) { "spec_type" }
+
+ subject(:source) do
+ klass = Class.new
+ klass.send :include, Bundler::Plugin::API::Source
+ klass.new("uri" => uri, "type" => type)
+ end
+
+ describe "attributes" do
+ it "allows access to uri" do
+ expect(source.uri).to eq("uri://to/test")
+ end
+
+ it "allows access to name" do
+ expect(source.name).to eq("spec_type at uri://to/test")
+ end
+ end
+
+ context "post_install" do
+ let(:installer) { double(:installer) }
+
+ before do
+ allow(Bundler::Source::Path::Installer).to receive(:new) { installer }
+ end
+
+ it "calls Path::Installer's post_install" do
+ expect(installer).to receive(:post_install).once
+
+ source.post_install(double(:spec))
+ end
+ end
+
+ context "install_path" do
+ let(:uri) { "uri://to/a/repository-name" }
+ let(:hash) { Digest(:SHA1).hexdigest(uri) }
+ let(:install_path) { Pathname.new "/bundler/install/path" }
+
+ before do
+ allow(Bundler).to receive(:install_path) { install_path }
+ end
+
+ it "returns basename with uri_hash" do
+ expected = Pathname.new "#{install_path}/repository-name-#{hash[0..11]}"
+ expect(source.install_path).to eq(expected)
+ end
+ end
+
+ context "to_lock" do
+ it "returns the string with remote and type" do
+ expected = <<~L
+ PLUGIN SOURCE
+ remote: #{uri}
+ type: #{type}
+ specs:
+ L
+
+ expect(source.to_lock).to eq(expected)
+ end
+
+ context "with additional options to lock" do
+ before do
+ allow(source).to receive(:options_to_lock) { { "first" => "option" } }
+ end
+
+ it "includes them" do
+ expected = <<~L
+ PLUGIN SOURCE
+ remote: #{uri}
+ type: #{type}
+ first: option
+ specs:
+ L
+
+ expect(source.to_lock).to eq(expected)
+ end
+ end
+ end
+
+ describe "to_s" do
+ it "returns the string with type and uri" do
+ expect(source.to_s).to eq("plugin source for spec_type with uri uri://to/test")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/api_spec.rb b/spec/bundler/bundler/plugin/api_spec.rb
new file mode 100644
index 0000000000..58fb908572
--- /dev/null
+++ b/spec/bundler/bundler/plugin/api_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::API do
+ context "plugin declarations" do
+ before do
+ stub_const "UserPluginClass", Class.new(Bundler::Plugin::API)
+ end
+
+ describe "#command" do
+ it "declares a command plugin with same class as handler" do
+ expect(Bundler::Plugin).
+ to receive(:add_command).with("meh", UserPluginClass).once
+
+ UserPluginClass.command "meh"
+ end
+
+ it "accepts another class as argument that handles the command" do
+ stub_const "NewClass", Class.new
+ expect(Bundler::Plugin).to receive(:add_command).with("meh", NewClass).once
+
+ UserPluginClass.command "meh", NewClass
+ end
+ end
+
+ describe "#source" do
+ it "declares a source plugin with same class as handler" do
+ expect(Bundler::Plugin).
+ to receive(:add_source).with("a_source", UserPluginClass).once
+
+ UserPluginClass.source "a_source"
+ end
+
+ it "accepts another class as argument that handles the command" do
+ stub_const "NewClass", Class.new
+ expect(Bundler::Plugin).to receive(:add_source).with("a_source", NewClass).once
+
+ UserPluginClass.source "a_source", NewClass
+ end
+ end
+
+ describe "#hook" do
+ it "accepts a block and passes it to Plugin module" do
+ foo = double("tester")
+ expect(foo).to receive(:called)
+
+ expect(Bundler::Plugin).to receive(:add_hook).with("post-foo").and_yield
+
+ Bundler::Plugin::API.hook("post-foo") { foo.called }
+ end
+ end
+ end
+
+ context "bundler interfaces provided" do
+ before do
+ stub_const "UserPluginClass", Class.new(Bundler::Plugin::API)
+ end
+
+ subject(:api) { UserPluginClass.new }
+
+ # A test of delegation
+ it "provides the Bundler's functions" do
+ expect(Bundler).to receive(:an_unknown_function).once
+
+ api.an_unknown_function
+ end
+
+ it "includes Bundler::SharedHelpers' functions" do
+ expect(Bundler::SharedHelpers).to receive(:an_unknown_helper).once
+
+ api.an_unknown_helper
+ end
+
+ context "#tmp" do
+ it "provides a tmp dir" do
+ expect(api.tmp("mytmp")).to be_directory
+ end
+
+ it "accepts multiple names for suffix" do
+ expect(api.tmp("myplugin", "download")).to be_directory
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/dsl_spec.rb b/spec/bundler/bundler/plugin/dsl_spec.rb
new file mode 100644
index 0000000000..235a549735
--- /dev/null
+++ b/spec/bundler/bundler/plugin/dsl_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::DSL do
+ DSL = Bundler::Plugin::DSL
+
+ subject(:dsl) { Bundler::Plugin::DSL.new }
+
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new "/" }
+ end
+
+ describe "it ignores only the methods defined in Bundler::Dsl" do
+ it "doesn't raises error for Dsl methods" do
+ expect { dsl.install_if }.not_to raise_error
+ end
+
+ it "raises error for other methods" do
+ expect { dsl.no_method }.to raise_error(DSL::PluginGemfileError)
+ end
+ end
+
+ describe "source block" do
+ it "adds #source with :type to list and also inferred_plugins list" do
+ expect(dsl).to receive(:plugin).with("bundler-source-news").once
+
+ dsl.source("some_random_url", type: "news") {}
+
+ expect(dsl.inferred_plugins).to eq(["bundler-source-news"])
+ end
+
+ it "registers a source type plugin only once for multiple declarations" do
+ expect(dsl).to receive(:plugin).with("bundler-source-news").and_call_original.once
+
+ dsl.source("some_random_url", type: "news") {}
+ dsl.source("another_random_url", type: "news") {}
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/events_spec.rb b/spec/bundler/bundler/plugin/events_spec.rb
new file mode 100644
index 0000000000..77e5fdb74c
--- /dev/null
+++ b/spec/bundler/bundler/plugin/events_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::Events do
+ context "plugin events" do
+ before do
+ @old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
+ Bundler::Plugin::Events.send :reset
+ end
+
+ after do
+ Bundler::Plugin::Events.send(:reset)
+ Hash[@old_constants].each do |name, value|
+ Bundler::Plugin::Events.send(:define, name, value)
+ end
+ end
+
+ describe "#define" do
+ it "raises when redefining a constant" do
+ Bundler::Plugin::Events.send(:define, :TEST_EVENT, "foo")
+
+ expect do
+ Bundler::Plugin::Events.send(:define, :TEST_EVENT, "bar")
+ end.to raise_error(ArgumentError)
+ end
+
+ it "can define a new constant" do
+ Bundler::Plugin::Events.send(:define, :NEW_CONSTANT, "value")
+ expect(Bundler::Plugin::Events::NEW_CONSTANT).to eq("value")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/index_spec.rb b/spec/bundler/bundler/plugin/index_spec.rb
new file mode 100644
index 0000000000..a28934269b
--- /dev/null
+++ b/spec/bundler/bundler/plugin/index_spec.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::Index do
+ Index = Bundler::Plugin::Index
+
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ gemfile "source 'https://gem.repo1'"
+ path = lib_path(plugin_name)
+ index.register_plugin("new-plugin", path.to_s, [path.join("lib").to_s], commands, sources, hooks)
+ end
+
+ let(:plugin_name) { "new-plugin" }
+ let(:commands) { [] }
+ let(:sources) { [] }
+ let(:hooks) { [] }
+
+ subject(:index) { Index.new }
+
+ describe "#register plugin" do
+ it "is available for retrieval" do
+ expect(index.plugin_path(plugin_name)).to eq(lib_path(plugin_name))
+ end
+
+ it "load_paths is available for retrieval" do
+ expect(index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s])
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.plugin_path(plugin_name)).to eq(lib_path(plugin_name))
+ end
+
+ it "load_paths are persistent" do
+ new_index = Index.new
+ expect(new_index.load_paths(plugin_name)).to eq([lib_path(plugin_name).join("lib").to_s])
+ end
+ end
+
+ describe "commands" do
+ let(:commands) { ["newco"] }
+
+ it "returns the plugins name on query" do
+ expect(index.command_plugin("newco")).to eq(plugin_name)
+ end
+
+ it "raises error on conflict" do
+ expect do
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, ["newco"], [], [])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.command_plugin("newco")).to eq(plugin_name)
+ end
+ end
+
+ describe "source" do
+ let(:sources) { ["new_source"] }
+
+ it "returns the plugins name on query" do
+ expect(index.source_plugin("new_source")).to eq(plugin_name)
+ end
+
+ it "raises error on conflict" do
+ expect do
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], ["new_source"], [])
+ end.to raise_error(Index::SourceConflict)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.source_plugin("new_source")).to eq(plugin_name)
+ end
+ end
+
+ describe "hook" do
+ let(:hooks) { ["after-bar"] }
+
+ it "returns the plugins name on query" do
+ expect(index.hook_plugins("after-bar")).to include(plugin_name)
+ end
+
+ it "is persistent" do
+ new_index = Index.new
+ expect(new_index.hook_plugins("after-bar")).to eq([plugin_name])
+ end
+
+ it "only registers a gem once for an event" do
+ path = lib_path(plugin_name)
+ index.register_plugin(plugin_name,
+ path.to_s,
+ [path.join("lib").to_s],
+ commands,
+ sources,
+ hooks + hooks)
+ expect(index.hook_plugins("after-bar")).to eq([plugin_name])
+ end
+
+ it "is gone after unregistration" do
+ expect(index.index_file.read).to include("after-bar:\n - \"new-plugin\"\n")
+ index.unregister_plugin(plugin_name)
+ expect(index.index_file.read).to_not include("after-bar:\n - \n")
+ end
+
+ context "that are not registered" do
+ let(:file) { double("index-file") }
+
+ before do
+ index.hook_plugins("not-there")
+ allow(File).to receive(:open).and_yield(file)
+ end
+
+ it "should not save it with next registered hook" do
+ expect(file).to receive(:puts) do |content|
+ expect(content).not_to include("not-there")
+ end
+
+ index.register_plugin("aplugin", lib_path("aplugin").to_s, lib_path("aplugin").join("lib").to_s, [], [], [])
+ end
+ end
+ end
+
+ describe "global index" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(nil)
+
+ Bundler::Plugin.reset!
+ path = lib_path("gplugin")
+ index.register_plugin("gplugin", path.to_s, [path.join("lib").to_s], [], ["glb_source"], [])
+ end
+
+ it "skips sources" do
+ new_index = Index.new
+ expect(new_index.source_plugin("glb_source")).to be_falsy
+ end
+ end
+
+ describe "after conflict" do
+ let(:commands) { ["foo"] }
+ let(:sources) { ["bar"] }
+ let(:hooks) { ["thehook"] }
+
+ shared_examples "it cleans up" do
+ it "the path" do
+ expect(index.installed?("cplugin")).to be_falsy
+ end
+
+ it "the command" do
+ expect(index.command_plugin("xfoo")).to be_falsy
+ end
+
+ it "the source" do
+ expect(index.source_plugin("xbar")).to be_falsy
+ end
+
+ it "the hook" do
+ expect(index.hook_plugins("xthehook")).to be_empty
+ end
+ end
+
+ context "on command conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["xbar"], ["xthehook"])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+
+ context "on source conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["xfoo"], ["bar"], ["xthehook"])
+ end.to raise_error(Index::SourceConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+
+ context "on command and source conflict it cleans up" do
+ before do
+ expect do
+ path = lib_path("cplugin")
+ index.register_plugin("cplugin", path.to_s, [path.join("lib").to_s], ["foo"], ["bar"], ["xthehook"])
+ end.to raise_error(Index::CommandConflict)
+ end
+
+ include_examples "it cleans up"
+ end
+ end
+
+ describe "relative plugin paths" do
+ let(:plugin_name) { "relative-plugin" }
+
+ before do
+ Bundler::Plugin.reset!
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ plugin_root = Bundler::Plugin.root
+ FileUtils.mkdir_p(plugin_root)
+
+ path = plugin_root.join(plugin_name)
+ FileUtils.mkdir_p(path.join("lib"))
+
+ index.register_plugin(plugin_name, path.to_s, [path.join("lib").to_s], [], [], [])
+ end
+
+ it "stores plugin paths relative to the plugin root" do
+ require "yaml"
+ data = YAML.load_file(index.index_file)
+
+ expect(data["plugin_paths"][plugin_name]).to eq(plugin_name)
+ expect(data["load_paths"][plugin_name]).to eq([File.join(plugin_name, "lib")])
+ end
+
+ it "expands relative paths to absolute on load" do
+ require "bundler/yaml_serializer"
+
+ plugin_root = Bundler::Plugin.root
+
+ relative_index = {
+ "commands" => {},
+ "hooks" => {},
+ "load_paths" => { plugin_name => [File.join(plugin_name, "lib")] },
+ "plugin_paths" => { plugin_name => plugin_name },
+ "sources" => {},
+ }
+
+ File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(relative_index) }
+
+ new_index = Index.new
+ expect(new_index.plugin_path(plugin_name)).to eq(plugin_root.join(plugin_name))
+ expect(new_index.load_paths(plugin_name)).to eq([plugin_root.join(plugin_name, "lib").to_s])
+ end
+
+ it "keeps paths outside the plugin root as absolute" do
+ outside_path = tmp.join("outside", "external-plugin")
+ FileUtils.mkdir_p(outside_path.join("lib"))
+
+ index.register_plugin("external-plugin", outside_path.to_s, [outside_path.join("lib").to_s], [], [], [])
+
+ require "yaml"
+ data = YAML.load_file(index.index_file)
+
+ expect(data["plugin_paths"]["external-plugin"]).to eq(outside_path.to_s)
+ expect(data["load_paths"]["external-plugin"]).to eq([outside_path.join("lib").to_s])
+ end
+
+ it "reads legacy index files with absolute paths" do
+ require "bundler/yaml_serializer"
+
+ plugin_root = Bundler::Plugin.root
+ absolute_path = plugin_root.join(plugin_name).to_s
+
+ legacy_index = {
+ "commands" => {},
+ "hooks" => {},
+ "load_paths" => { plugin_name => [File.join(absolute_path, "lib")] },
+ "plugin_paths" => { plugin_name => absolute_path },
+ "sources" => {},
+ }
+
+ File.open(index.index_file, "w") {|f| f.puts Bundler::YAMLSerializer.dump(legacy_index) }
+
+ new_index = Index.new
+ expect(new_index.plugin_path(plugin_name)).to eq(Pathname.new(absolute_path))
+ expect(new_index.load_paths(plugin_name)).to eq([File.join(absolute_path, "lib")])
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/installer_spec.rb b/spec/bundler/bundler/plugin/installer_spec.rb
new file mode 100644
index 0000000000..c200a98afa
--- /dev/null
+++ b/spec/bundler/bundler/plugin/installer_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::Installer do
+ subject(:installer) { Bundler::Plugin::Installer.new }
+
+ describe "cli install" do
+ it "uses Gem.sources when non of the source is provided" do
+ sources = double(:sources)
+ allow(Gem).to receive(:sources) { sources }
+
+ allow(installer).to receive(:install_rubygems).
+ with("new-plugin", [">= 0"], sources).once
+
+ installer.install("new-plugin", {})
+ end
+
+ describe "with mocked installers" do
+ let(:spec) { double(:spec) }
+ it "returns the installed spec after installing git plugins" do
+ allow(installer).to receive(:install_git).
+ and_return("new-plugin" => spec)
+
+ expect(installer.install(["new-plugin"], git: "https://some.ran/dom")).
+ to eq("new-plugin" => spec)
+ end
+
+ it "returns the installed spec after installing local git plugins" do
+ allow(installer).to receive(:install_git).
+ and_return("new-plugin" => spec)
+
+ expect(installer.install(["new-plugin"], git: "/phony/path/repo")).
+ to eq("new-plugin" => spec)
+ end
+
+ it "returns the installed spec after installing rubygems plugins" do
+ allow(installer).to receive(:install_rubygems).
+ and_return("new-plugin" => spec)
+
+ expect(installer.install(["new-plugin"], source: "https://some.ran/dom")).
+ to eq("new-plugin" => spec)
+ end
+ end
+
+ describe "with actual installers" do
+ before do
+ build_repo2 do
+ build_plugin "re-plugin"
+ build_plugin "ma-plugin"
+ end
+
+ @previous_ui = Bundler.ui
+ Bundler.ui = Bundler::UI::Silent.new
+ end
+
+ after do
+ Bundler.ui = @previous_ui
+ end
+
+ context "git plugins" do
+ before do
+ build_git "ga-plugin", path: lib_path("ga-plugin") do |s|
+ s.write "plugins.rb"
+ end
+ end
+
+ let(:result) do
+ installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s)
+ end
+
+ it "returns the installed spec after installing" do
+ spec = result["ga-plugin"]
+ expect(spec.full_name).to eq "ga-plugin-1.0"
+ end
+
+ it "has expected full_gem_path" do
+ rev = revision_for(lib_path("ga-plugin"))
+ expect(result["ga-plugin"].full_gem_path).
+ to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s)
+ end
+ end
+
+ context "local git plugins" do
+ before do
+ build_git "ga-plugin", path: lib_path("ga-plugin") do |s|
+ s.write "plugins.rb"
+ end
+ end
+
+ let(:result) do
+ installer.install(["ga-plugin"], git: lib_path("ga-plugin").to_s)
+ end
+
+ it "returns the installed spec after installing" do
+ spec = result["ga-plugin"]
+ expect(spec.full_name).to eq "ga-plugin-1.0"
+ end
+
+ it "has expected full_gem_path" do
+ rev = revision_for(lib_path("ga-plugin"))
+ expect(result["ga-plugin"].full_gem_path).
+ to eq(Bundler::Plugin.root.join("bundler", "gems", "ga-plugin-#{rev[0..11]}").to_s)
+ end
+ end
+
+ context "rubygems plugins" do
+ let(:result) do
+ installer.install(["re-plugin"], source: file_uri_for(gem_repo2))
+ end
+
+ it "returns the installed spec after installing " do
+ expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ end
+
+ it "has expected full_gem_path" do
+ expect(result["re-plugin"].full_gem_path).
+ to eq(global_plugin_gem("re-plugin-1.0").to_s)
+ end
+ end
+
+ context "multiple plugins" do
+ let(:result) do
+ installer.install(["re-plugin", "ma-plugin"], source: file_uri_for(gem_repo2))
+ end
+
+ it "returns the installed spec after installing " do
+ expect(result["re-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ expect(result["ma-plugin"]).to be_kind_of(Bundler::RemoteSpecification)
+ end
+
+ it "has expected full_gem_path" do
+ expect(result["re-plugin"].full_gem_path).to eq(global_plugin_gem("re-plugin-1.0").to_s)
+ expect(result["ma-plugin"].full_gem_path).to eq(global_plugin_gem("ma-plugin-1.0").to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin/source_list_spec.rb b/spec/bundler/bundler/plugin/source_list_spec.rb
new file mode 100644
index 0000000000..64a1233dd1
--- /dev/null
+++ b/spec/bundler/bundler/plugin/source_list_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin::SourceList do
+ SourceList = Bundler::Plugin::SourceList
+
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new "/" }
+ end
+
+ subject(:source_list) { SourceList.new }
+
+ describe "adding sources uses classes for plugin" do
+ it "uses Plugin::Installer::Rubygems for rubygems sources" do
+ source = source_list.
+ add_rubygems_source("remotes" => ["https://existing-rubygems.org"])
+ expect(source).to be_instance_of(Bundler::Plugin::Installer::Rubygems)
+ end
+
+ it "uses Plugin::Installer::Git for git sources" do
+ source = source_list.
+ add_git_source("uri" => "git://existing-git.org/path.git")
+ expect(source).to be_instance_of(Bundler::Plugin::Installer::Git)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/plugin_spec.rb b/spec/bundler/bundler/plugin_spec.rb
new file mode 100644
index 0000000000..b379594c6f
--- /dev/null
+++ b/spec/bundler/bundler/plugin_spec.rb
@@ -0,0 +1,368 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Plugin do
+ Plugin = Bundler::Plugin
+
+ let(:installer) { double(:installer) }
+ let(:index) { double(:index) }
+ let(:spec) { double(:spec) }
+ let(:spec2) { double(:spec2) }
+
+ before do
+ build_lib "new-plugin", path: lib_path("new-plugin") do |s|
+ s.write "plugins.rb"
+ end
+
+ build_lib "another-plugin", path: lib_path("another-plugin") do |s|
+ s.write "plugins.rb"
+ end
+
+ allow(spec).to receive(:full_gem_path).
+ and_return(lib_path("new-plugin").to_s)
+ allow(spec).to receive(:load_paths).
+ and_return([lib_path("new-plugin").join("lib").to_s])
+
+ allow(spec2).to receive(:full_gem_path).
+ and_return(lib_path("another-plugin").to_s)
+ allow(spec2).to receive(:load_paths).
+ and_return([lib_path("another-plugin").join("lib").to_s])
+
+ allow(Plugin::Installer).to receive(:new) { installer }
+ allow(Plugin).to receive(:index) { index }
+ allow(index).to receive(:register_plugin)
+ end
+
+ describe "list command" do
+ context "when no plugins are installed" do
+ before { allow(index).to receive(:installed_plugins) { [] } }
+ it "outputs no plugins installed" do
+ expect(Bundler.ui).to receive(:info).with("No plugins installed")
+ subject.list
+ end
+ end
+
+ context "with installed plugins" do
+ before do
+ allow(index).to receive(:installed_plugins) { %w[plug1 plug2] }
+ allow(index).to receive(:plugin_commands).with("plug1") { %w[c11 c12] }
+ allow(index).to receive(:plugin_commands).with("plug2") { %w[c21 c22] }
+ end
+ it "list plugins followed by commands" do
+ expected_output = "plug1\n-----\n c11\n c12\n\nplug2\n-----\n c21\n c22\n\n"
+ expect(Bundler.ui).to receive(:info).with(expected_output)
+ subject.list
+ end
+ end
+ end
+
+ describe "install command" do
+ let(:opts) { { "version" => "~> 1.0", "source" => "foo" } }
+
+ before do
+ allow(installer).to receive(:install).with(["new-plugin"], opts) do
+ { "new-plugin" => spec }
+ end
+ end
+
+ it "passes the name and options to installer" do
+ allow(index).to receive(:up_to_date?).
+ with(spec)
+ allow(installer).to receive(:install).with(["new-plugin"], opts) do
+ { "new-plugin" => spec }
+ end.once
+
+ subject.install ["new-plugin"], opts
+ end
+
+ it "validates the installed plugin" do
+ allow(index).to receive(:up_to_date?).
+ with(spec)
+ allow(subject).
+ to receive(:validate_plugin!).with(lib_path("new-plugin")).once
+
+ subject.install ["new-plugin"], opts
+ end
+
+ it "registers the plugin with index" do
+ allow(index).to receive(:up_to_date?).
+ with(spec)
+ allow(index).to receive(:register_plugin).
+ with("new-plugin", lib_path("new-plugin").to_s, [lib_path("new-plugin").join("lib").to_s], []).once
+ subject.install ["new-plugin"], opts
+ end
+
+ context "multiple plugins" do
+ it do
+ allow(installer).to receive(:install).
+ with(["new-plugin", "another-plugin"], opts) do
+ {
+ "new-plugin" => spec,
+ "another-plugin" => spec2,
+ }
+ end.once
+
+ allow(subject).to receive(:validate_plugin!).twice
+ allow(index).to receive(:up_to_date?).twice
+ allow(index).to receive(:register_plugin).twice
+ subject.install ["new-plugin", "another-plugin"], opts
+ end
+ end
+ end
+
+ describe "evaluate gemfile for plugins" do
+ let(:definition) { double("definition") }
+ let(:builder) { double("builder") }
+ let(:gemfile) { bundled_app_gemfile }
+
+ before do
+ allow(Plugin::DSL).to receive(:new) { builder }
+ allow(builder).to receive(:eval_gemfile).with(gemfile)
+ allow(builder).to receive(:check_primary_source_safety)
+ allow(builder).to receive(:to_definition) { definition }
+ allow(builder).to receive(:inferred_plugins) { [] }
+ end
+
+ it "doesn't calls installer without any plugins" do
+ allow(definition).to receive(:dependencies) { [] }
+ allow(installer).to receive(:install_definition).never
+
+ subject.gemfile_install(gemfile)
+ end
+
+ context "with dependencies" do
+ let(:plugin_specs) do
+ {
+ "new-plugin" => spec,
+ "another-plugin" => spec2,
+ }
+ end
+
+ before do
+ allow(index).to receive(:up_to_date?) { nil }
+ allow(definition).to receive(:dependencies) { [Bundler::Dependency.new("new-plugin", ">=0"), Bundler::Dependency.new("another-plugin", ">=0")] }
+ allow(installer).to receive(:install_definition) { plugin_specs }
+ end
+
+ it "should validate and register the plugins" do
+ expect(subject).to receive(:validate_plugin!).twice
+ expect(subject).to receive(:register_plugin).twice
+
+ subject.gemfile_install(gemfile)
+ end
+
+ it "should pass the optional plugins to #register_plugin" do
+ allow(builder).to receive(:inferred_plugins) { ["another-plugin"] }
+
+ expect(subject).to receive(:register_plugin).
+ with("new-plugin", spec, false).once
+
+ expect(subject).to receive(:register_plugin).
+ with("another-plugin", spec2, true).once
+
+ subject.gemfile_install(gemfile)
+ end
+ end
+ end
+
+ describe "#command?" do
+ it "returns true value for commands in index" do
+ allow(index).
+ to receive(:command_plugin).with("newcommand") { "my-plugin" }
+ result = subject.command? "newcommand"
+ expect(result).to be_truthy
+ end
+
+ it "returns false value for commands not in index" do
+ allow(index).to receive(:command_plugin).with("newcommand") { nil }
+ result = subject.command? "newcommand"
+ expect(result).to be_falsy
+ end
+ end
+
+ describe "#exec_command" do
+ it "raises UndefinedCommandError when command is not found" do
+ allow(index).to receive(:command_plugin).with("newcommand") { nil }
+ expect { subject.exec_command("newcommand", []) }.
+ to raise_error(Plugin::UndefinedCommandError)
+ end
+ end
+
+ describe "#source?" do
+ it "returns true value for sources in index" do
+ allow(index).
+ to receive(:command_plugin).with("foo-source") { "my-plugin" }
+ result = subject.command? "foo-source"
+ expect(result).to be_truthy
+ end
+
+ it "returns false value for source not in index" do
+ allow(index).to receive(:command_plugin).with("foo-source") { nil }
+ result = subject.command? "foo-source"
+ expect(result).to be_falsy
+ end
+ end
+
+ describe "#source" do
+ it "raises UnknownSourceError when source is not found" do
+ allow(index).to receive(:source_plugin).with("bar") { nil }
+ expect { subject.source("bar") }.
+ to raise_error(Plugin::UnknownSourceError)
+ end
+
+ it "loads the plugin, if not loaded" do
+ allow(index).to receive(:source_plugin).with("foo-bar") { "plugin_name" }
+
+ expect(subject).to receive(:load_plugin).with("plugin_name")
+ subject.source("foo-bar")
+ end
+
+ it "returns the class registered with #add_source" do
+ allow(index).to receive(:source_plugin).with("foo") { "plugin_name" }
+ stub_const "NewClass", Class.new
+
+ subject.add_source("foo", NewClass)
+ expect(subject.source("foo")).to be(NewClass)
+ end
+ end
+
+ describe "#from_lock" do
+ it "returns instance of registered class initialized with locked opts" do
+ opts = { "type" => "l_source", "remote" => "xyz", "other" => "random" }
+ allow(index).to receive(:source_plugin).with("l_source") { "plugin_name" }
+
+ stub_const "SClass", Class.new
+ s_instance = double(:s_instance)
+ subject.add_source("l_source", SClass)
+
+ expect(SClass).to receive(:new).
+ with(hash_including("type" => "l_source", "uri" => "xyz", "other" => "random")) { s_instance }
+ expect(subject.from_lock(opts)).to be(s_instance)
+ end
+ end
+
+ describe "#root" do
+ context "in app dir" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "returns plugin dir in app .bundle path" do
+ expect(subject.root).to eq(bundled_app(".bundle/plugin"))
+ end
+ end
+
+ context "outside app dir" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(nil)
+ end
+
+ it "returns plugin dir in global bundle path" do
+ expect(subject.root).to eq(home(".bundle/plugin"))
+ end
+ end
+ end
+
+ describe "#add_hook" do
+ it "raises an ArgumentError on an unregistered event" do
+ ran = false
+ expect do
+ Plugin.add_hook("unregistered-hook") { ran = true }
+ end.to raise_error(ArgumentError)
+ expect(ran).to be(false)
+ end
+ end
+
+ describe "#hook" do
+ before do
+ path = lib_path("foo-plugin")
+ build_lib "foo-plugin", path: path do |s|
+ s.write "plugins.rb", code
+ end
+
+ @old_constants = Bundler::Plugin::Events.constants.map {|name| [name, Bundler::Plugin::Events.const_get(name)] }
+ Bundler::Plugin::Events.send(:reset)
+ Bundler::Plugin::Events.send(:define, :EVENT1, "event-1")
+ Bundler::Plugin::Events.send(:define, :EVENT2, "event-2")
+
+ allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT1).
+ and_return(["foo-plugin", "", nil])
+ allow(index).to receive(:hook_plugins).with(Bundler::Plugin::Events::EVENT2).
+ and_return(["foo-plugin"])
+ allow(index).to receive(:plugin_path).with("foo-plugin").and_return(path)
+ allow(index).to receive(:load_paths).with("foo-plugin").and_return([])
+ end
+
+ after do
+ Bundler::Plugin::Events.send(:reset)
+ Hash[@old_constants].each do |name, value|
+ Bundler::Plugin::Events.send(:define, name, value)
+ end
+ end
+
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook("event-1") { puts "hook for event 1" }
+ RUBY
+
+ it "raises an ArgumentError on an unregistered event" do
+ expect do
+ Plugin.hook("unregistered-hook")
+ end.to raise_error(ArgumentError)
+ end
+
+ it "executes the hook" do
+ expect do
+ Plugin.hook(Bundler::Plugin::Events::EVENT1)
+ end.to output("hook for event 1\n").to_stdout
+ end
+
+ context "single plugin declaring more than one hook" do
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) {}
+ Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT2) {}
+ puts "loaded"
+ RUBY
+
+ it "evals plugins.rb once" do
+ expect do
+ Plugin.hook(Bundler::Plugin::Events::EVENT1)
+ Plugin.hook(Bundler::Plugin::Events::EVENT2)
+ end.to output("loaded\n").to_stdout
+ end
+ end
+
+ context "a block is passed" do
+ let(:code) { <<-RUBY }
+ Bundler::Plugin::API.hook(Bundler::Plugin::Events::EVENT1) { |&blk| blk.call }
+ RUBY
+
+ it "is passed to the hook" do
+ expect do
+ Plugin.hook(Bundler::Plugin::Events::EVENT1) { puts "win" }
+ end.to output("win\n").to_stdout
+ end
+ end
+
+ context "the plugin load_path is invalid" do
+ before do
+ allow(index).to receive(:load_paths).with("foo-plugin").
+ and_return(["invalid-file-name1", "invalid-file-name2"])
+ end
+
+ it "outputs a useful warning" do
+ msg =
+ "The following plugin paths don't exist: invalid-file-name1, invalid-file-name2.\n\n" \
+ "This can happen if the plugin was " \
+ "installed with a different version of Ruby that has since been uninstalled.\n\n" \
+ "If you would like to reinstall the plugin, run:\n\n" \
+ "bundler plugin uninstall foo-plugin && bundler plugin install foo-plugin\n\n" \
+ "Continuing without installing plugin foo-plugin.\n"
+
+ expect(Bundler.ui).to receive(:warn).with(msg)
+
+ Plugin.hook(Bundler::Plugin::Events::EVENT1)
+
+ expect(subject.loaded?("foo-plugin")).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/remote_specification_spec.rb b/spec/bundler/bundler/remote_specification_spec.rb
new file mode 100644
index 0000000000..f35b231d58
--- /dev/null
+++ b/spec/bundler/bundler/remote_specification_spec.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::RemoteSpecification do
+ let(:name) { "foo" }
+ let(:version) { Gem::Version.new("1.0.0") }
+ let(:platform) { Gem::Platform::RUBY }
+ let(:spec_fetcher) { double(:spec_fetcher) }
+
+ subject { described_class.new(name, version, platform, spec_fetcher) }
+
+ it "is Comparable" do
+ expect(described_class.ancestors).to include(Comparable)
+ end
+
+ it "can match platforms" do
+ expect(described_class.ancestors).to include(Bundler::MatchPlatform)
+ end
+
+ describe "#fetch_platform" do
+ let(:remote_spec) { double(:remote_spec, platform: "jruby") }
+
+ before { allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec) }
+
+ it "should return the spec platform" do
+ expect(subject.fetch_platform).to eq("jruby")
+ end
+ end
+
+ describe "#full_name" do
+ context "when platform is ruby" do
+ it "should return the spec name and version" do
+ expect(subject.full_name).to eq("foo-1.0.0")
+ end
+ end
+
+ context "when platform is nil" do
+ let(:platform) { nil }
+
+ it "should return the spec name and version" do
+ expect(subject.full_name).to eq("foo-1.0.0")
+ end
+ end
+
+ context "when platform is a non-ruby platform" do
+ let(:platform) { "jruby" }
+
+ it "should return the spec name, version, and platform" do
+ expect(subject.full_name).to eq("foo-1.0.0-java")
+ end
+ end
+ end
+
+ describe "#<=>" do
+ let(:other_name) { name }
+ let(:other_version) { version }
+ let(:other_platform) { platform }
+ let(:other_spec_fetcher) { spec_fetcher }
+
+ shared_examples_for "a comparison" do
+ context "which exactly matches" do
+ it "returns 0" do
+ expect(subject <=> other).to eq(0)
+ end
+ end
+
+ context "which is different by name" do
+ let(:other_name) { "a" }
+ it "returns 1" do
+ expect(subject <=> other).to eq(1)
+ end
+ end
+
+ context "which has a lower version" do
+ let(:other_version) { Gem::Version.new("0.9.0") }
+ it "returns 1" do
+ expect(subject <=> other).to eq(1)
+ end
+ end
+
+ context "which has a higher version" do
+ let(:other_version) { Gem::Version.new("1.1.0") }
+ it "returns -1" do
+ expect(subject <=> other).to eq(-1)
+ end
+ end
+
+ context "which has a different platform" do
+ let(:other_platform) { Gem::Platform.new("x86-mswin32") }
+ it "returns -1" do
+ expect(subject <=> other).to eq(-1)
+ end
+ end
+ end
+
+ context "comparing another Bundler::RemoteSpecification" do
+ let(:other) do
+ Bundler::RemoteSpecification.new(other_name, other_version,
+ other_platform, nil)
+ end
+
+ it_should_behave_like "a comparison"
+ end
+
+ context "comparing a Gem::Specification" do
+ let(:other) do
+ Gem::Specification.new(other_name, other_version).tap do |s|
+ s.platform = other_platform
+ end
+ end
+
+ it_should_behave_like "a comparison"
+ end
+
+ context "comparing a non sortable object" do
+ let(:other) { Object.new }
+ let(:remote_spec) { double(:remote_spec, platform: "jruby") }
+
+ before do
+ allow(spec_fetcher).to receive(:fetch_spec).and_return(remote_spec)
+ allow(remote_spec).to receive(:<=>).and_return(nil)
+ end
+
+ it "should use default object comparison" do
+ expect(subject <=> other).to eq(nil)
+ end
+ end
+ end
+
+ describe "#__swap__" do
+ let(:spec) { double(:spec, dependencies: []) }
+ let(:new_spec) { double(:new_spec, dependencies: [], runtime_dependencies: []) }
+
+ before { subject.instance_variable_set(:@_remote_specification, spec) }
+
+ it "should replace remote specification with the passed spec" do
+ expect(subject.instance_variable_get(:@_remote_specification)).to be(spec)
+ subject.__swap__(new_spec)
+ expect(subject.instance_variable_get(:@_remote_specification)).to be(new_spec)
+ end
+ end
+
+ describe "#sort_obj" do
+ context "when platform is ruby" do
+ it "should return a sorting delegate array with name, version, and -1" do
+ expect(subject.sort_obj).to match_array(["foo", version, -1])
+ end
+ end
+
+ context "when platform is not ruby" do
+ let(:platform) { "jruby" }
+
+ it "should return a sorting delegate array with name, version, and 1" do
+ expect(subject.sort_obj).to match_array(["foo", version, 1])
+ end
+ end
+ end
+
+ describe "method missing" do
+ context "and is present in Gem::Specification" do
+ let(:remote_spec) { double(:remote_spec, authors: "abcd") }
+
+ before do
+ allow(subject).to receive(:_remote_specification).and_return(remote_spec)
+ expect(subject.methods.map(&:to_sym)).not_to include(:authors)
+ end
+
+ it "should send through to Gem::Specification" do
+ expect(subject.authors).to eq("abcd")
+ end
+ end
+ end
+
+ describe "respond to missing?" do
+ context "and is present in Gem::Specification" do
+ let(:remote_spec) { double(:remote_spec, authors: "abcd") }
+
+ before do
+ allow(subject).to receive(:_remote_specification).and_return(remote_spec)
+ expect(subject.methods.map(&:to_sym)).not_to include(:authors)
+ end
+
+ it "should send through to Gem::Specification" do
+ expect(subject.respond_to?(:authors)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/resolver/candidate_spec.rb b/spec/bundler/bundler/resolver/candidate_spec.rb
new file mode 100644
index 0000000000..aefad3316e
--- /dev/null
+++ b/spec/bundler/bundler/resolver/candidate_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Resolver::Candidate do
+ it "compares fine" do
+ version1 = described_class.new("1.12.5", priority: -1)
+ version2 = described_class.new("1.12.5", priority: 1)
+
+ expect(version2 > version1).to be true
+
+ version1 = described_class.new("1.12.5")
+ version2 = described_class.new("1.12.5")
+
+ expect(version2 == version1).to be true
+
+ version1 = described_class.new("1.12.5", priority: 1)
+ version2 = described_class.new("1.12.5", priority: -1)
+
+ expect(version2 < version1).to be true
+ end
+end
diff --git a/spec/bundler/bundler/resolver/cooldown_spec.rb b/spec/bundler/bundler/resolver/cooldown_spec.rb
new file mode 100644
index 0000000000..37ec158cba
--- /dev/null
+++ b/spec/bundler/bundler/resolver/cooldown_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Resolver do
+ let(:resolver) { described_class.allocate }
+
+ def remote(cooldown:)
+ instance_double(Bundler::Source::Rubygems::Remote, effective_cooldown: cooldown)
+ end
+
+ def spec(created_at:, remote:, name: "myrack", version: "1.0.0")
+ Struct.new(:name, :version, :created_at, :remote).new(name, Gem::Version.new(version), created_at, remote)
+ end
+
+ describe "#filter_cooldown" do
+ let(:now) { Time.now }
+
+ context "with a 7-day cooldown" do
+ let(:r) { remote(cooldown: 7) }
+
+ it "rejects versions published within the window" do
+ recent = spec(version: "1.1.0", created_at: now - (2 * 86_400), remote: r)
+ old = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r)
+
+ expect(resolver.send(:filter_cooldown, [recent, old])).to eq([old])
+ end
+
+ it "keeps versions published exactly at the threshold" do
+ boundary = spec(created_at: now - (7 * 86_400), remote: r)
+
+ expect(resolver.send(:filter_cooldown, [boundary])).to eq([boundary])
+ end
+
+ it "leaves rolling-delay history intact" do
+ # 7-day cooldown with frequent releases must still expose an older candidate.
+ in_cooldown = spec(version: "1.2.0", created_at: now - 86_400, remote: r)
+ also_in_cooldown = spec(version: "1.1.0", created_at: now - (3 * 86_400), remote: r)
+ eligible = spec(version: "1.0.0", created_at: now - (10 * 86_400), remote: r)
+
+ result = resolver.send(:filter_cooldown, [in_cooldown, also_in_cooldown, eligible])
+
+ expect(result).to eq([eligible])
+ end
+
+ it "drops every spec sharing an excluded [name, version] tuple" do
+ # The cooldown check is by version, not per-spec: a StubSpecification for an
+ # in-cooldown release would otherwise slip through on local install paths.
+ endpoint = spec(version: "2.0.0", created_at: now - 86_400, remote: r)
+ local_stub = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0"))
+ eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r)
+
+ result = resolver.send(:filter_cooldown, [endpoint, local_stub, eligible])
+
+ expect(result).to eq([eligible])
+ end
+
+ it "keeps stub-only versions that no endpoint marks as in cooldown" do
+ # If no remote spec carries created_at for a version, cooldown cannot judge it;
+ # the stub stays in.
+ local_only = Struct.new(:name, :version).new("myrack", Gem::Version.new("2.0.0"))
+ eligible = spec(version: "1.0.0", created_at: now - (30 * 86_400), remote: r)
+
+ result = resolver.send(:filter_cooldown, [local_only, eligible])
+
+ expect(result).to eq([local_only, eligible])
+ end
+ end
+
+ context "when created_at is missing (blank metadata)" do
+ it "keeps the spec regardless of cooldown" do
+ s = spec(created_at: nil, remote: remote(cooldown: 7))
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ context "when the remote has no cooldown" do
+ it "keeps every spec" do
+ s = spec(created_at: now - 3600, remote: remote(cooldown: nil))
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ context "when cooldown is 0" do
+ it "keeps every spec (escape hatch)" do
+ s = spec(created_at: now - 3600, remote: remote(cooldown: 0))
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ context "when the spec does not respond to created_at" do
+ it "keeps the spec" do
+ bare = Struct.new(:version).new("1.0.0")
+
+ expect(resolver.send(:filter_cooldown, [bare])).to eq([bare])
+ end
+ end
+
+ context "when the spec has no remote" do
+ it "keeps the spec" do
+ s = spec(created_at: now - 86_400, remote: nil)
+
+ expect(resolver.send(:filter_cooldown, [s])).to eq([s])
+ end
+ end
+
+ it "returns the same array when input is empty" do
+ expect(resolver.send(:filter_cooldown, [])).to eq([])
+ end
+ end
+
+ describe "#cooldown_hint" do
+ let(:now) { Time.now }
+ let(:r) { remote(cooldown: 7) }
+
+ it "returns nil when no spec is excluded" do
+ expect(resolver.send(:cooldown_hint, [])).to be_nil
+ end
+
+ it "returns nil when every spec is outside the cooldown window" do
+ eligible = [spec(created_at: now - (30 * 86_400), remote: r)]
+
+ expect(resolver.send(:cooldown_hint, eligible)).to be_nil
+ end
+
+ it "mentions the count and the bypass flag for one excluded version" do
+ excluded = [spec(created_at: now - 86_400, remote: r)]
+
+ hint = resolver.send(:cooldown_hint, excluded)
+
+ expect(hint).to match(/1 version excluded by the cooldown setting/)
+ expect(hint).to match(/--cooldown 0/)
+ end
+
+ it "uses plural wording when multiple versions are excluded" do
+ excluded = %w[1.0.0 1.1.0 1.2.0].map {|v| spec(version: v, created_at: now - 86_400, remote: r) }
+
+ expect(resolver.send(:cooldown_hint, excluded)).to match(/3 versions excluded/)
+ end
+
+ it "counts each unique version once even when multiple spec instances share it" do
+ duplicates = Array.new(3) { spec(created_at: now - 86_400, remote: r) }
+
+ expect(resolver.send(:cooldown_hint, duplicates)).to match(/1 version excluded/)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/retry_spec.rb b/spec/bundler/bundler/retry_spec.rb
new file mode 100644
index 0000000000..5c84d0bea5
--- /dev/null
+++ b/spec/bundler/bundler/retry_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Retry do
+ it "return successful result if no errors" do
+ attempts = 0
+ result = Bundler::Retry.new(nil, nil, 3).attempt do
+ attempts += 1
+ :success
+ end
+ expect(result).to eq(:success)
+ expect(attempts).to eq(1)
+ end
+
+ it "returns the first valid result" do
+ jobs = [proc { raise "job 1 failed" }, proc { :bar }, proc { raise "job 2 failed" }]
+ attempts = 0
+ result = Bundler::Retry.new(nil, nil, 3).attempt do
+ attempts += 1
+ jobs.shift.call
+ end
+ expect(result).to eq(:bar)
+ expect(attempts).to eq(2)
+ end
+
+ it "raises the last error" do
+ errors = [StandardError, StandardError, StandardError, Bundler::GemfileNotFound]
+ attempts = 0
+ expect do
+ Bundler::Retry.new(nil, nil, 3).attempt do
+ attempts += 1
+ raise errors.shift
+ end
+ end.to raise_error(Bundler::GemfileNotFound)
+ expect(attempts).to eq(4)
+ end
+
+ it "raises exceptions" do
+ error = Bundler::GemfileNotFound
+ attempts = 0
+ expect do
+ Bundler::Retry.new(nil, error).attempt do
+ attempts += 1
+ raise error
+ end
+ end.to raise_error(error)
+ expect(attempts).to eq(1)
+ end
+
+ context "logging" do
+ let(:error) { Bundler::GemfileNotFound }
+ let(:failure_message) { "Retrying test due to error (2/2): #{error} #{error}" }
+
+ context "with debugging on" do
+ it "print error message with newline" do
+ allow(Bundler.ui).to receive(:debug?).and_return(true)
+ expect(Bundler.ui).to_not receive(:info)
+ expect(Bundler.ui).to receive(:warn).with(failure_message, true)
+
+ expect do
+ Bundler::Retry.new("test", [], 1).attempt do
+ raise error
+ end
+ end.to raise_error(error)
+ end
+ end
+
+ context "with debugging off" do
+ it "print error message with newlines" do
+ allow(Bundler.ui).to receive(:debug?).and_return(false)
+ expect(Bundler.ui).to receive(:info).with("").twice
+ expect(Bundler.ui).to receive(:warn).with(failure_message, true)
+
+ expect do
+ Bundler::Retry.new("test", [], 1).attempt do
+ raise error
+ end
+ end.to raise_error(error)
+ end
+ end
+ end
+
+ context "exponential backoff" do
+ it "can be disabled by setting base_delay to 0" do
+ attempts = 0
+ expect do
+ Bundler::Retry.new("test", [], 2, base_delay: 0).attempt do
+ attempts += 1
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ # Verify no sleep was called (implicitly - if sleep was called, timing would be different)
+ expect(attempts).to eq(3)
+ end
+
+ it "is enabled by default with 1 second base delay" do
+ original_base_delay = Bundler::Retry.default_base_delay
+ Bundler::Retry.default_base_delay = 1.0
+
+ attempts = 0
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 2, jitter: 0).attempt do
+ attempts += 1
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ expect(attempts).to eq(3)
+ expect(sleep_times.length).to eq(2)
+ # First retry: 1.0 * 2^0 = 1.0
+ expect(sleep_times[0]).to eq(1.0)
+ # Second retry: 1.0 * 2^1 = 2.0
+ expect(sleep_times[1]).to eq(2.0)
+ ensure
+ Bundler::Retry.default_base_delay = original_base_delay
+ end
+
+ it "sleeps with exponential backoff when base_delay is set" do
+ attempts = 0
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0).attempt do
+ attempts += 1
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ expect(attempts).to eq(3)
+ expect(sleep_times.length).to eq(2)
+ # First retry: 1.0 * 2^0 = 1.0
+ expect(sleep_times[0]).to eq(1.0)
+ # Second retry: 1.0 * 2^1 = 2.0
+ expect(sleep_times[1]).to eq(2.0)
+ end
+
+ it "respects max_delay" do
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 3, base_delay: 10.0, max_delay: 15.0, jitter: 0).attempt do
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ # First retry: 10.0 * 2^0 = 10.0
+ expect(sleep_times[0]).to eq(10.0)
+ # Second retry: 10.0 * 2^1 = 20.0, capped at 15.0
+ expect(sleep_times[1]).to eq(15.0)
+ # Third retry: 10.0 * 2^2 = 40.0, capped at 15.0
+ expect(sleep_times[2]).to eq(15.0)
+ end
+
+ it "adds jitter to delay" do
+ sleep_times = []
+
+ allow_any_instance_of(Bundler::Retry).to receive(:sleep) do |_instance, delay|
+ sleep_times << delay
+ end
+
+ expect do
+ Bundler::Retry.new("test", [], 2, base_delay: 1.0, jitter: 0.5).attempt do
+ raise "error"
+ end
+ end.to raise_error(StandardError)
+
+ expect(sleep_times.length).to eq(2)
+ # First retry should be between 1.0 and 1.5 (base + jitter)
+ expect(sleep_times[0]).to be >= 1.0
+ expect(sleep_times[0]).to be <= 1.5
+ # Second retry should be between 2.0 and 2.5
+ expect(sleep_times[1]).to be >= 2.0
+ expect(sleep_times[1]).to be <= 2.5
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ruby_dsl_spec.rb b/spec/bundler/bundler/ruby_dsl_spec.rb
new file mode 100644
index 0000000000..45a37c5795
--- /dev/null
+++ b/spec/bundler/bundler/ruby_dsl_spec.rb
@@ -0,0 +1,248 @@
+# frozen_string_literal: true
+
+require "bundler/ruby_dsl"
+
+RSpec.describe Bundler::RubyDsl do
+ class MockDSL
+ include Bundler::RubyDsl
+
+ attr_reader :ruby_version
+ attr_accessor :gemfile
+ end
+
+ let(:dsl) { MockDSL.new }
+ let(:ruby_version) { "2.0.0" }
+ let(:ruby_version_arg) { ruby_version }
+ let(:version) { "2.0.0" }
+ let(:engine) { "jruby" }
+ let(:engine_version) { "9000" }
+ let(:patchlevel) { "100" }
+ let(:options) do
+ { patchlevel: patchlevel,
+ engine: engine,
+ engine_version: engine_version }
+ end
+ let(:project_root) { Pathname.new("/path/to/project") }
+ let(:gemfile) { project_root.join("Gemfile") }
+ before { allow(Bundler).to receive(:root).and_return(project_root) }
+
+ let(:invoke) do
+ proc do
+ args = []
+ args << ruby_version_arg if ruby_version_arg
+ args << options
+
+ dsl.ruby(*args)
+ end
+ end
+
+ subject do
+ dsl.gemfile = gemfile
+ invoke.call
+ dsl.ruby_version
+ end
+
+ describe "#ruby_version" do
+ shared_examples_for "it stores the ruby version" do
+ it "stores the version" do
+ expect(subject.versions).to eq(Array(ruby_version))
+ expect(subject.gem_version.version).to eq(version)
+ end
+
+ it "stores the engine details" do
+ expect(subject.engine).to eq(engine)
+ expect(subject.engine_versions).to eq(Array(engine_version))
+ end
+
+ it "stores the patchlevel" do
+ expect(subject.patchlevel).to eq(patchlevel)
+ end
+ end
+
+ context "with a plain version" do
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with a single requirement" do
+ let(:ruby_version) { ">= 2.0.0" }
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with a preview version" do
+ let(:ruby_version) { "3.3.0-preview2" }
+
+ it "stores the version" do
+ expect(subject.versions).to eq(Array("3.3.0.preview2"))
+ expect(subject.gem_version.version).to eq("3.3.0.preview2")
+ end
+ end
+
+ context "with two requirements in the same string" do
+ let(:ruby_version) { ">= 2.0.0, < 3.0" }
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::InvalidArgumentError)
+ end
+ end
+
+ context "with two requirements" do
+ let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] }
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with multiple engine versions" do
+ let(:engine_version) { ["> 200", "< 300"] }
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with no options hash" do
+ let(:invoke) { proc { dsl.ruby(ruby_version) } }
+
+ let(:patchlevel) { nil }
+ let(:engine) { "ruby" }
+ let(:engine_version) { version }
+
+ it_behaves_like "it stores the ruby version"
+
+ context "and with multiple requirements" do
+ let(:ruby_version) { ["~> 2.0.0", "> 2.0.1"] }
+ let(:engine_version) { ruby_version }
+ it_behaves_like "it stores the ruby version"
+ end
+ end
+
+ context "with a file option" do
+ let(:file) { ".ruby-version" }
+ let(:ruby_version_file_path) { gemfile.dirname.join(file) }
+ let(:options) do
+ { file: file,
+ patchlevel: patchlevel,
+ engine: engine,
+ engine_version: engine_version }
+ end
+ let(:ruby_version_arg) { nil }
+ let(:file_content) { "#{version}\n" }
+
+ before do
+ allow(Bundler).to receive(:read_file) do |path|
+ raise Errno::ENOENT, <<~ERROR unless path == ruby_version_file_path
+ #{file} not found in specs:
+ expected: #{ruby_version_file_path}
+ received: #{path}
+ ERROR
+ file_content
+ end
+ end
+
+ it_behaves_like "it stores the ruby version"
+
+ context "with the Gemfile ruby file: path is relative to the Gemfile in a subdir" do
+ let(:gemfile) { project_root.join("subdir", "Gemfile") }
+ let(:file) { "../.ruby-version" }
+ let(:ruby_version_file_path) { gemfile.dirname.join(file) }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with bundler root in a subdir of the project" do
+ let(:project_root) { Pathname.new("/path/to/project/subdir") }
+ let(:gemfile) { project_root.parent.join("Gemfile") }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with the ruby- prefix in the file" do
+ let(:file_content) { "ruby-#{version}\n" }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "and a version" do
+ let(:ruby_version_arg) { version }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::GemfileError, "Do not pass version argument when using :file option")
+ end
+ end
+
+ context "with a @gemset" do
+ let(:file_content) { "ruby-#{version}@gemset\n" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::InvalidArgumentError, "2.0.0@gemset is not a valid requirement on the Ruby version")
+ end
+ end
+
+ context "with a mise.toml file format" do
+ let(:file) { "mise.toml" }
+ let(:ruby_version_arg) { nil }
+ let(:file_content) do
+ <<~TOML
+ [tools]
+ ruby = #{quote}#{version}#{quote}
+ TOML
+ end
+
+ context "with double quotes" do
+ let(:quote) { '"' }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with single quotes" do
+ let(:quote) { "'" }
+
+ it_behaves_like "it stores the ruby version"
+ end
+
+ context "with mismatched quotes" do
+ let(:file_content) do
+ <<~TOML
+ [tools]
+ ruby = "#{version}'
+ TOML
+ end
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::InvalidArgumentError, "= is not a valid requirement on the Ruby version")
+ end
+ end
+ end
+
+ context "with a .tool-versions file format" do
+ let(:file) { ".tool-versions" }
+ let(:ruby_version_arg) { nil }
+ let(:file_content) do
+ <<~TOOLS
+ nodejs 18.16.0
+ ruby #{version} # This is a comment
+ pnpm 8.6.12
+ TOOLS
+ end
+
+ it_behaves_like "it stores the ruby version"
+
+ context "with extra spaces and a very cozy comment" do
+ let(:file_content) do
+ <<~TOOLS
+ nodejs 18.16.0
+ ruby #{version}# This is a cozy comment
+ pnpm 8.6.12
+ TOOLS
+ end
+
+ it_behaves_like "it stores the ruby version"
+ end
+ end
+
+ context "when the file does not exist" do
+ let(:ruby_version_file_path) { nil }
+ let(:ruby_version_arg) { nil }
+ let(:file) { "nonexistent.txt" }
+
+ it "raises an error" do
+ expect { subject }.to raise_error(Bundler::GemfileError, /Could not find version file nonexistent.txt/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ruby_version_spec.rb b/spec/bundler/bundler/ruby_version_spec.rb
new file mode 100644
index 0000000000..0d41ec9901
--- /dev/null
+++ b/spec/bundler/bundler/ruby_version_spec.rb
@@ -0,0 +1,500 @@
+# frozen_string_literal: true
+
+require "bundler/ruby_version"
+
+RSpec.describe "Bundler::RubyVersion and its subclasses" do
+ let(:version) { "2.0.0" }
+ let(:patchlevel) { "645" }
+ let(:engine) { "jruby" }
+ let(:engine_version) { "2.0.1" }
+
+ describe Bundler::RubyVersion do
+ subject { Bundler::RubyVersion.new(version, patchlevel, engine, engine_version) }
+
+ let(:ruby_version) { subject }
+ let(:other_version) { version }
+ let(:other_patchlevel) { patchlevel }
+ let(:other_engine) { engine }
+ let(:other_engine_version) { engine_version }
+ let(:other_ruby_version) { Bundler::RubyVersion.new(other_version, other_patchlevel, other_engine, other_engine_version) }
+
+ describe "#initialize" do
+ context "no engine is passed" do
+ let(:engine) { nil }
+
+ it "should set ruby as the engine" do
+ expect(subject.engine).to eq("ruby")
+ end
+ end
+
+ context "no engine_version is passed" do
+ let(:engine_version) { nil }
+
+ it "should set engine version as the passed version" do
+ expect(subject.engine_versions).to eq(["2.0.0"])
+ end
+ end
+
+ context "with engine in symbol" do
+ let(:engine) { :jruby }
+
+ it "should coerce engine to string" do
+ expect(subject.engine).to eq("jruby")
+ end
+ end
+
+ context "is called with multiple requirements" do
+ let(:version) { ["<= 2.0.0", "> 1.9.3"] }
+ let(:engine_version) { nil }
+
+ it "sets the versions" do
+ expect(subject.versions).to eq(version)
+ end
+
+ it "sets the engine versions" do
+ expect(subject.engine_versions).to eq(version)
+ end
+ end
+
+ context "is called with multiple engine requirements" do
+ let(:engine_version) { [">= 2.0", "< 2.3"] }
+
+ it "sets the engine versions" do
+ expect(subject.engine_versions).to eq(engine_version)
+ end
+ end
+ end
+
+ describe ".from_string" do
+ shared_examples_for "returning" do
+ it "returns the original RubyVersion" do
+ expect(described_class.from_string(subject.to_s)).to eq(subject)
+ end
+ end
+
+ include_examples "returning"
+
+ context "no patchlevel" do
+ let(:patchlevel) { nil }
+
+ include_examples "returning"
+ end
+
+ context "engine is ruby" do
+ let(:engine) { "ruby" }
+ let(:engine_version) { version }
+
+ include_examples "returning"
+ end
+
+ context "with multiple requirements" do
+ let(:engine_version) { ["> 9", "< 11"] }
+ let(:version) { ["> 8", "< 10"] }
+ let(:patchlevel) { nil }
+
+ it "returns nil" do
+ expect(described_class.from_string(subject.to_s)).to be_nil
+ end
+ end
+ end
+
+ describe "#to_s" do
+ it "should return info string with the ruby version, patchlevel, engine, and engine version" do
+ expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)")
+ end
+
+ context "no patchlevel" do
+ let(:patchlevel) { nil }
+
+ it "should return info string with the version, engine, and engine version" do
+ expect(subject.to_s).to eq("ruby 2.0.0 (jruby 2.0.1)")
+ end
+ end
+
+ context "engine is ruby" do
+ let(:engine) { "ruby" }
+
+ it "should return info string with the ruby version and patchlevel" do
+ expect(subject.to_s).to eq("ruby 2.0.0")
+ end
+ end
+
+ context "with multiple requirements" do
+ let(:engine_version) { ["> 9", "< 11"] }
+ let(:version) { ["> 8", "< 10"] }
+ let(:patchlevel) { nil }
+
+ it "should return info string with all requirements" do
+ expect(subject.to_s).to eq("ruby > 8, < 10 (jruby > 9, < 11)")
+ end
+ end
+ end
+
+ describe "#==" do
+ shared_examples_for "two ruby versions are not equal" do
+ it "should return false" do
+ expect(subject).to_not eq(other_ruby_version)
+ end
+ end
+
+ shared_examples_for "the versions, engines, and engine_versions match" do
+ it "should return true" do
+ expect(subject).to eq(other_ruby_version)
+ end
+ end
+
+ context "the versions do not match" do
+ let(:other_version) { "1.21.6" }
+
+ it_behaves_like "two ruby versions are not equal"
+ end
+
+ context "the patchlevels do not match" do
+ let(:other_patchlevel) { "21" }
+
+ it_behaves_like "the versions, engines, and engine_versions match"
+ end
+
+ context "the engines do not match" do
+ let(:other_engine) { "ruby" }
+
+ it_behaves_like "two ruby versions are not equal"
+ end
+
+ context "the engine versions do not match" do
+ let(:other_engine_version) { "1.11.2" }
+
+ it_behaves_like "two ruby versions are not equal"
+ end
+ end
+
+ describe "#host" do
+ before do
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_cpu").and_return("x86_64")
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_vendor").and_return("apple")
+ allow(RbConfig::CONFIG).to receive(:[]).with("host_os").and_return("darwin14.5.0")
+ end
+
+ it "should return an info string with the host cpu, vendor, and os" do
+ expect(subject.host).to eq("x86_64-apple-darwin14.5.0")
+ end
+
+ it "memoizes the info string with the host cpu, vendor, and os" do
+ expect(RbConfig::CONFIG).to receive(:[]).with("host_cpu").once.and_call_original
+ expect(RbConfig::CONFIG).to receive(:[]).with("host_vendor").once.and_call_original
+ expect(RbConfig::CONFIG).to receive(:[]).with("host_os").once.and_call_original
+ 2.times { ruby_version.host }
+ end
+ end
+
+ describe "#gem_version" do
+ let(:gem_version) { "2.0.0" }
+ let(:gem_version_obj) { Gem::Version.new(gem_version) }
+
+ shared_examples_for "it parses the version from the requirement string" do |version|
+ let(:version) { version }
+ it "should return the underlying version" do
+ expect(ruby_version.gem_version).to eq(gem_version_obj)
+ expect(ruby_version.gem_version.version).to eq(gem_version)
+ end
+ end
+
+ it_behaves_like "it parses the version from the requirement string", "2.0.0"
+ it_behaves_like "it parses the version from the requirement string", ">= 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", "~> 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", "< 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", "= 2.0.0"
+ it_behaves_like "it parses the version from the requirement string", ["> 2.0.0", "< 2.4.5"]
+ end
+
+ describe "#diff" do
+ let(:engine) { "ruby" }
+
+ shared_examples_for "there is a difference in the engines" do
+ it "should return a tuple with :engine and the two different engines" do
+ expect(ruby_version.diff(other_ruby_version)).to eq([:engine, engine, other_engine])
+ end
+ end
+
+ shared_examples_for "there is a difference in the versions" do
+ it "should return a tuple with :version and the two different versions" do
+ expect(ruby_version.diff(other_ruby_version)).to eq([:version, Array(version).join(", "), Array(other_version).join(", ")])
+ end
+ end
+
+ shared_examples_for "there is a difference in the engine versions" do
+ it "should return a tuple with :engine_version and the two different engine versions" do
+ expect(ruby_version.diff(other_ruby_version)).to eq([:engine_version, Array(engine_version).join(", "), Array(other_engine_version).join(", ")])
+ end
+ end
+
+ shared_examples_for "even there is a difference in the patchlevels" do
+ it "should return nil" do
+ expect(ruby_version.diff(other_ruby_version)).to be_nil
+ end
+ end
+
+ shared_examples_for "there are no differences" do
+ it "should return nil" do
+ expect(ruby_version.diff(other_ruby_version)).to be_nil
+ end
+ end
+
+ context "all things match exactly" do
+ it_behaves_like "there are no differences"
+ end
+
+ context "detects engine discrepancies first" do
+ let(:other_version) { "2.0.1" }
+ let(:other_patchlevel) { "643" }
+ let(:other_engine) { "rbx" }
+ let(:other_engine_version) { "2.0.0" }
+
+ it_behaves_like "there is a difference in the engines"
+ end
+
+ context "detects version discrepancies second" do
+ let(:other_version) { "2.0.1" }
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "detects version discrepancies with multiple requirements second" do
+ let(:other_version) { "2.0.1" }
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ let(:version) { ["> 2.0.0", "< 1.0.0"] }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "detects engine version discrepancies third" do
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ it_behaves_like "there is a difference in the engine versions"
+ end
+
+ context "detects engine version discrepancies with multiple requirements third" do
+ let(:other_patchlevel) { "643" }
+ let(:other_engine_version) { "2.0.0" }
+
+ let(:engine_version) { ["> 2.0.0", "< 1.0.0"] }
+
+ it_behaves_like "there is a difference in the engine versions"
+ end
+
+ context "ignores patchlevel discrepancies last" do
+ let(:other_patchlevel) { "643" }
+
+ it_behaves_like "even there is a difference in the patchlevels"
+ end
+
+ context "successfully matches gem requirements" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there are no differences"
+ end
+
+ context "successfully matches multiple gem requirements" do
+ let(:version) { [">= 2.0.0", "< 2.4.5"] }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { ["~> 2.0.1", "< 2.4.5"] }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there are no differences"
+ end
+
+ context "successfully detects bad gem requirements with versions with multiple requirements" do
+ let(:version) { ["~> 2.0.0", "< 2.0.5"] }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.5" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "successfully detects bad gem requirements with versions" do
+ let(:version) { "~> 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.1.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "there is a difference in the versions"
+ end
+
+ context "successfully detects bad gem requirements with patchlevels" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "645" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.0.5" }
+
+ it_behaves_like "even there is a difference in the patchlevels"
+ end
+
+ context "successfully detects bad gem requirements with engine versions" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "< 643" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { "2.0.0" }
+ let(:other_patchlevel) { "642" }
+ let(:other_engine) { "ruby" }
+ let(:other_engine_version) { "2.1.0" }
+
+ it_behaves_like "there is a difference in the engine versions"
+ end
+
+ context "with a patchlevel of -1" do
+ let(:version) { ">= 2.0.0" }
+ let(:patchlevel) { "-1" }
+ let(:engine) { "ruby" }
+ let(:engine_version) { "~> 2.0.1" }
+ let(:other_version) { version }
+ let(:other_engine) { engine }
+ let(:other_engine_version) { engine_version }
+
+ context "and comparing with another patchlevel of -1" do
+ let(:other_patchlevel) { patchlevel }
+
+ it_behaves_like "there are no differences"
+ end
+
+ context "and comparing with a patchlevel that is not -1" do
+ let(:other_patchlevel) { "642" }
+
+ it_behaves_like "even there is a difference in the patchlevels"
+ end
+ end
+ end
+
+ describe "#system" do
+ subject { Bundler::RubyVersion.system }
+
+ let(:bundler_system_ruby_version) { subject }
+
+ around do |example|
+ if Bundler::RubyVersion.instance_variable_defined?("@system")
+ begin
+ old_ruby_version = Bundler::RubyVersion.instance_variable_get("@system")
+ Bundler::RubyVersion.remove_instance_variable("@system")
+ example.run
+ ensure
+ Bundler::RubyVersion.instance_variable_set("@system", old_ruby_version)
+ end
+ else
+ begin
+ example.run
+ ensure
+ Bundler::RubyVersion.remove_instance_variable("@system")
+ end
+ end
+ end
+
+ it "should return an instance of Bundler::RubyVersion" do
+ expect(subject).to be_kind_of(Bundler::RubyVersion)
+ end
+
+ it "memoizes the instance of Bundler::RubyVersion" do
+ expect(Bundler::RubyVersion).to receive(:new).once.and_call_original
+ 2.times { subject }
+ end
+
+ describe "#version" do
+ it "should return the value of Gem.ruby_version as a string" do
+ expect(subject.versions).to eq([Gem.ruby_version.to_s])
+ end
+ end
+
+ describe "#engine" do
+ before { stub_const("RUBY_ENGINE", "jruby") }
+ before { stub_const("RUBY_ENGINE_VERSION", "2.1.1") }
+
+ it "should return a copy of the value of RUBY_ENGINE" do
+ expect(subject.engine).to eq("jruby")
+ expect(subject.engine).to_not be(RUBY_ENGINE)
+ end
+ end
+
+ describe "#engine_version" do
+ context "engine is ruby" do
+ before do
+ allow(Gem).to receive(:ruby_version).and_return(Gem::Version.new("2.2.4"))
+ stub_const("RUBY_ENGINE", "ruby")
+ end
+
+ it "should return the value of Gem.ruby_version as a string" do
+ expect(bundler_system_ruby_version.engine_versions).to eq(["2.2.4"])
+ end
+ end
+
+ context "engine is rbx" do
+ before do
+ stub_const("RUBY_ENGINE", "rbx")
+ stub_const("RUBY_ENGINE_VERSION", "2.0.0")
+ end
+
+ it "should return a copy of the value of RUBY_ENGINE_VERSION" do
+ expect(bundler_system_ruby_version.engine_versions).to eq(["2.0.0"])
+ expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_ENGINE_VERSION)
+ end
+ end
+
+ context "engine is jruby" do
+ before do
+ stub_const("RUBY_ENGINE", "jruby")
+ stub_const("RUBY_ENGINE_VERSION", "2.1.1")
+ end
+
+ it "should return a copy of the value of RUBY_ENGINE_VERSION" do
+ expect(subject.engine_versions).to eq(["2.1.1"])
+ expect(bundler_system_ruby_version.engine_versions.first).to_not be(RUBY_ENGINE_VERSION)
+ end
+ end
+
+ context "engine is some other ruby engine" do
+ before do
+ stub_const("RUBY_ENGINE", "not_supported_ruby_engine")
+ stub_const("RUBY_ENGINE_VERSION", "1.2.3")
+ end
+
+ it "returns RUBY_ENGINE_VERSION" do
+ expect(bundler_system_ruby_version.engine_versions).to eq(["1.2.3"])
+ end
+ end
+ end
+
+ describe "#patchlevel" do
+ it "should return a string with the value of RUBY_PATCHLEVEL" do
+ expect(subject.patchlevel).to eq(RUBY_PATCHLEVEL.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/rubygems_ext_spec.rb b/spec/bundler/bundler/rubygems_ext_spec.rb
new file mode 100644
index 0000000000..0fc528f78c
--- /dev/null
+++ b/spec/bundler/bundler/rubygems_ext_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require "bundler/rubygems_ext"
+
+RSpec.describe Gem::SplitCompactIndexEntryOnFirstColon do
+ # Reproduces the RubyGems < 4.0.13 `Gem::Resolver::APISet::GemParser` that
+ # split each compact index entry on every colon, corrupting metadata values
+ # that themselves contain colons.
+ let(:legacy_parser_class) do
+ Class.new do
+ def parse_dependency(string)
+ dependency = string.split(":")
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
+ dependency[0] = -dependency[0]
+ dependency
+ end
+ end
+ end
+
+ before { legacy_parser_class.prepend(described_class) }
+
+ it "preserves colon-bearing metadata values such as created_at timestamps" do
+ parser = legacy_parser_class.new
+
+ expect(parser.send(:parse_dependency, "created_at:2026-05-12T10:00:00Z")).to eq(["created_at", ["2026-05-12T10:00:00Z"]])
+ end
+
+ it "still parses ordinary name:requirement entries" do
+ parser = legacy_parser_class.new
+
+ expect(parser.send(:parse_dependency, "myrack:>= 1.0")).to eq(["myrack", [">= 1.0"]])
+ end
+
+ it "keeps parse_dependency private" do
+ parser = legacy_parser_class.new
+
+ expect { parser.parse_dependency("created_at:x") }.to raise_error(NoMethodError, /private method/)
+ end
+end
diff --git a/spec/bundler/bundler/rubygems_integration_spec.rb b/spec/bundler/bundler/rubygems_integration_spec.rb
new file mode 100644
index 0000000000..a2c63a7ca0
--- /dev/null
+++ b/spec/bundler/bundler/rubygems_integration_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::RubygemsIntegration do
+ context "#validate" do
+ let(:spec) do
+ Gem::Specification.new do |s|
+ s.name = "to-validate"
+ s.version = "1.0.0"
+ s.loaded_from = __FILE__
+ end
+ end
+ subject { Bundler.rubygems.validate(spec) }
+
+ it "validates for resolution" do
+ expect(spec).to receive(:validate_for_resolution)
+ subject
+ end
+
+ context "with an invalid spec" do
+ before do
+ expect(spec).to receive(:validate_for_resolution).
+ and_raise(Gem::InvalidSpecificationException.new("TODO is not an author"))
+ end
+
+ it "should raise a Gem::InvalidSpecificationException and produce a helpful warning message" do
+ expect { subject }.to raise_error(Gem::InvalidSpecificationException,
+ "The gemspec at #{__FILE__} is not valid. "\
+ "Please fix this gemspec.\nThe validation error was 'TODO is not an author'\n")
+ end
+ end
+ end
+
+ describe "#download_gem" do
+ let(:bundler_retry) { double(Bundler::Retry) }
+ let(:cache_dir) { "#{Gem.path.first}/cache" }
+ let(:spec) do
+ spec = Gem::Specification.new("Foo", Gem::Version.new("2.5.2"))
+ spec.remote = Bundler::Source::Rubygems::Remote.new(uri.to_s)
+ spec
+ end
+ let(:fetcher) { double("gem_remote_fetcher") }
+
+ context "when uri is public" do
+ let(:uri) { Gem::URI.parse("https://foo.bar") }
+
+ it "successfully downloads gem with retries" do
+ expect(Bundler::Retry).to receive(:new).with("download gem from #{uri}/").
+ and_return(bundler_retry)
+ expect(bundler_retry).to receive(:attempts).and_yield
+ expect(fetcher).to receive(:cache_update_path)
+
+ Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
+ end
+ end
+
+ context "when uri contains userinfo part" do
+ let(:uri) { Gem::URI.parse("https://#{userinfo}@foo.bar") }
+
+ context "with user and password" do
+ let(:userinfo) { "user:password" }
+
+ it "successfully downloads gem with retries with filtered log" do
+ expect(Bundler::Retry).to receive(:new).with("download gem from https://user:REDACTED@foo.bar/").
+ and_return(bundler_retry)
+ expect(bundler_retry).to receive(:attempts).and_yield
+ expect(fetcher).to receive(:cache_update_path)
+
+ Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
+ end
+ end
+
+ context "with token [as user]" do
+ let(:userinfo) { "token" }
+
+ it "successfully downloads gem with retries with filtered log" do
+ expect(Bundler::Retry).to receive(:new).with("download gem from https://REDACTED@foo.bar/").
+ and_return(bundler_retry)
+ expect(bundler_retry).to receive(:attempts).and_yield
+ expect(fetcher).to receive(:cache_update_path)
+
+ Bundler.rubygems.download_gem(spec, uri, cache_dir, fetcher)
+ end
+ end
+ end
+ end
+
+ describe "#fetch_all_remote_specs" do
+ let(:uri) { "https://example.com" }
+ let(:fetcher) { double("gem_remote_fetcher") }
+ let(:specs_response) { Marshal.dump(["specs"]) }
+ let(:prerelease_specs_response) { Marshal.dump(["prerelease_specs"]) }
+
+ context "when a rubygems source mirror is set" do
+ let(:orig_uri) { Gem::URI("http://zombo.com") }
+ let(:remote_with_mirror) { double("remote", uri: uri, original_uri: orig_uri) }
+
+ it "sets the 'X-Gemfile-Source' header containing the original source" do
+ expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
+ expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
+ result = Bundler.rubygems.fetch_all_remote_specs(remote_with_mirror, fetcher)
+ expect(result).to eq(%w[specs prerelease_specs])
+ end
+ end
+
+ context "when there is no rubygems source mirror set" do
+ let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) }
+
+ it "does not set the 'X-Gemfile-Source' header" do
+ expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(specs_response)
+ expect(fetcher).to receive(:fetch_path).with(uri + "prerelease_specs.4.8.gz").and_return(prerelease_specs_response)
+ result = Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher)
+ expect(result).to eq(%w[specs prerelease_specs])
+ end
+ end
+
+ context "when loading an unexpected class" do
+ let(:remote_no_mirror) { double("remote", uri: uri, original_uri: nil) }
+ let(:unexpected_specs_response) { Marshal.dump(3) }
+
+ it "raises a MarshalError error" do
+ expect(fetcher).to receive(:fetch_path).with(uri + "specs.4.8.gz").and_return(unexpected_specs_response)
+ expect { Bundler.rubygems.fetch_all_remote_specs(remote_no_mirror, fetcher) }.to raise_error(Bundler::MarshalError, /unexpected class/i)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/settings/validator_spec.rb b/spec/bundler/bundler/settings/validator_spec.rb
new file mode 100644
index 0000000000..b252ba59a0
--- /dev/null
+++ b/spec/bundler/bundler/settings/validator_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Settings::Validator do
+ describe ".validate!" do
+ def validate!(key, value, settings)
+ transformed_key = Bundler.settings.key_for(key)
+ if value.nil?
+ settings.delete(transformed_key)
+ else
+ settings[transformed_key] = value
+ end
+ described_class.validate!(key, value, settings)
+ settings
+ end
+
+ it "path and path.system are mutually exclusive" do
+ expect(validate!("path", "bundle", {})).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path", "bundle", "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => true)).to eq("BUNDLE_PATH__SYSTEM" => true)
+ expect(validate!("path", nil, "BUNDLE_PATH__SYSTEM" => false)).to eq("BUNDLE_PATH__SYSTEM" => false)
+ expect(validate!("path", nil, {})).to eq({})
+
+ expect(validate!("path.system", true, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH__SYSTEM" => true)
+ expect(validate!("path.system", false, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle", "BUNDLE_PATH__SYSTEM" => false)
+ expect(validate!("path.system", nil, "BUNDLE_PATH" => "bundle")).to eq("BUNDLE_PATH" => "bundle")
+ expect(validate!("path.system", true, {})).to eq("BUNDLE_PATH__SYSTEM" => true)
+ expect(validate!("path.system", false, {})).to eq("BUNDLE_PATH__SYSTEM" => false)
+ expect(validate!("path.system", nil, {})).to eq({})
+ end
+
+ it "a group cannot be in both `with` & `without` simultaneously" do
+ expect do
+ validate!("with", "", {})
+ validate!("with", nil, {})
+ validate!("with", "", "BUNDLE_WITHOUT" => "a")
+ validate!("with", nil, "BUNDLE_WITHOUT" => "a")
+ validate!("with", "b:c", "BUNDLE_WITHOUT" => "a")
+
+ validate!("without", "", {})
+ validate!("without", nil, {})
+ validate!("without", "", "BUNDLE_WITH" => "a")
+ validate!("without", nil, "BUNDLE_WITH" => "a")
+ validate!("without", "b:c", "BUNDLE_WITH" => "a")
+ end.not_to raise_error
+
+ expect { validate!("with", "b:c", "BUNDLE_WITHOUT" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip
+ Setting `with` to "b:c" failed:
+ - a group cannot be in both `with` & `without` simultaneously
+ - `without` is current set to [:c, :d]
+ - the `c` groups conflict
+ EOS
+
+ expect { validate!("without", "b:c", "BUNDLE_WITH" => "c:d") }.to raise_error Bundler::InvalidOption, <<~EOS.strip
+ Setting `without` to "b:c" failed:
+ - a group cannot be in both `with` & `without` simultaneously
+ - `with` is current set to [:c, :d]
+ - the `c` groups conflict
+ EOS
+ end
+ end
+
+ describe described_class::Rule do
+ let(:keys) { %w[key] }
+ let(:description) { "rule description" }
+ let(:validate) { proc { raise "validate called!" } }
+ subject(:rule) { described_class.new(keys, description, &validate) }
+
+ describe "#validate!" do
+ it "calls the block" do
+ expect { rule.validate!("key", nil, {}) }.to raise_error(RuntimeError, /validate called!/)
+ end
+ end
+
+ describe "#fail!" do
+ it "raises with a helpful message" do
+ expect { subject.fail!("key", "value", "reason1", "reason2") }.to raise_error Bundler::InvalidOption, <<~EOS.strip
+ Setting `key` to "value" failed:
+ - rule description
+ - reason1
+ - reason2
+ EOS
+ end
+ end
+
+ describe "#set" do
+ it "works when the value has not changed" do
+ allow(Bundler.ui).to receive(:info).never
+
+ subject.set({}, "key", nil)
+ subject.set({ "BUNDLE_KEY" => "value" }, "key", "value")
+ end
+
+ it "prints out when the value is changing" do
+ settings = {}
+
+ expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value\", since rule description, reason1")
+ subject.set(settings, "key", "value", "reason1")
+ expect(settings).to eq("BUNDLE_KEY" => "value")
+
+ expect(Bundler.ui).to receive(:info).with("Setting `key` to \"value2\", since rule description, reason2")
+ subject.set(settings, "key", "value2", "reason2")
+ expect(settings).to eq("BUNDLE_KEY" => "value2")
+
+ expect(Bundler.ui).to receive(:info).with("Setting `key` to nil, since rule description, reason3")
+ subject.set(settings, "key", nil, "reason3")
+ expect(settings).to eq({})
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb
new file mode 100644
index 0000000000..5e1aaaa555
--- /dev/null
+++ b/spec/bundler/bundler/settings_spec.rb
@@ -0,0 +1,380 @@
+# frozen_string_literal: true
+
+require "bundler/settings"
+
+RSpec.describe Bundler::Settings do
+ subject(:settings) { described_class.new(bundled_app) }
+
+ describe "#set_local" do
+ context "root is nil" do
+ subject(:settings) { described_class.new(nil) }
+
+ before do
+ allow(Pathname).to receive(:new).and_call_original
+ allow(Pathname).to receive(:new).with(".bundle").and_return home(".bundle")
+ end
+
+ it "works" do
+ subject.set_local("foo", "bar")
+
+ expect(subject["foo"]).to eq("bar")
+ end
+ end
+ end
+
+ describe "load_config" do
+ let(:hash) do
+ {
+ "build.thrift" => "--with-cppflags=-D_FORTIFY_SOURCE=0",
+ "build.libv8" => "--with-system-v8",
+ "build.therubyracer" => "--with-v8-dir",
+ "build.pg" => "--with-pg-config=/usr/local/Cellar/postgresql92/9.2.8_1/bin/pg_config",
+ "gem.coc" => "false",
+ "gem.mit" => "false",
+ "gem.test" => "minitest",
+ "thingy" => <<-EOS.tr("\n", " "),
+--asdf --fdsa --ty=oh man i hope this doesn't break bundler because
+that would suck --ehhh=oh geez it looks like i might have broken bundler somehow
+--very-important-option=DontDeleteRoo
+--very-important-option=DontDeleteRoo
+--very-important-option=DontDeleteRoo
+--very-important-option=DontDeleteRoo
+ EOS
+ "xyz" => "zyx",
+ }
+ end
+
+ before do
+ hash.each do |key, value|
+ settings.set_local key, value
+ end
+ end
+
+ it "can load the config" do
+ loaded = settings.send(:load_config, bundled_app("config"))
+ expected = Hash[hash.map do |k, v|
+ [settings.send(:key_for, k), v.to_s]
+ end]
+ expect(loaded).to eq(expected)
+ end
+
+ context "when BUNDLE_IGNORE_CONFIG is set" do
+ before { ENV["BUNDLE_IGNORE_CONFIG"] = "TRUE" }
+
+ it "ignores the config" do
+ loaded = settings.send(:load_config, bundled_app("config"))
+ expect(loaded).to eq({})
+ end
+ end
+ end
+
+ describe "#global_config_file" do
+ context "when $HOME is not accessible" do
+ it "does not raise" do
+ expect(Bundler.rubygems).to receive(:user_home).twice.and_return(nil)
+
+ expect(subject.send(:global_config_file)).to be_nil
+ end
+ end
+ end
+
+ describe "#[]" do
+ context "when the local config file is not found" do
+ subject(:settings) { described_class.new }
+
+ it "does not raise" do
+ expect do
+ subject["foo"]
+ end.not_to raise_error
+ end
+ end
+
+ context "when not set" do
+ context "when default value present" do
+ it "retrieves value" do
+ expect(settings[:retry]).to be 3
+ end
+ end
+
+ it "returns nil" do
+ expect(settings[:buttermilk]).to be nil
+ end
+ end
+
+ context "when is boolean" do
+ it "returns a boolean" do
+ settings.set_local :frozen, "true"
+ expect(settings[:frozen]).to be true
+ end
+ context "when specific gem is configured" do
+ it "returns a boolean" do
+ settings.set_local "ignore_messages.foobar", "true"
+ expect(settings["ignore_messages.foobar"]).to be true
+ end
+ end
+ end
+
+ context "when is number" do
+ it "returns a number" do
+ settings.set_local :ssl_verify_mode, "1"
+ expect(settings[:ssl_verify_mode]).to be 1
+ end
+
+ it "coerces cooldown to integer" do
+ settings.set_local :cooldown, "7"
+ expect(settings[:cooldown]).to be 7
+ end
+ end
+
+ context "when it's not possible to create the settings directory" do
+ it "raises an PermissionError with explanation" do
+ settings_dir = settings.send(:local_config_file).dirname
+ expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings_dir).
+ and_raise(Errno::EACCES.new(settings_dir.to_s))
+ expect { settings.set_local :frozen, "1" }.
+ to raise_error(Bundler::PermissionError, /#{settings_dir}/)
+ end
+ end
+ end
+
+ describe "#temporary" do
+ it "reset after used" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ Bundler.settings.set_command_option :no_install, true
+
+ Bundler.settings.temporary(no_install: false) do
+ expect(Bundler.settings[:no_install]).to eq false
+ end
+
+ expect(Bundler.settings[:no_install]).to eq true
+
+ Bundler.settings.set_command_option :no_install, nil
+ end
+
+ it "returns the return value of the block" do
+ ret = Bundler.settings.temporary({}) { :ret }
+ expect(ret).to eq :ret
+ end
+
+ context "when called without a block" do
+ it "leaves the setting changed" do
+ Bundler.settings.temporary(foo: :random)
+ expect(Bundler.settings[:foo]).to eq "random"
+ end
+
+ it "returns nil" do
+ expect(Bundler.settings.temporary(foo: :bar)).to be_nil
+ end
+ end
+ end
+
+ describe "#set_global" do
+ context "when it's not possible to write to create the settings directory" do
+ it "raises an PermissionError with explanation" do
+ settings_dir = settings.send(:global_config_file).dirname
+ expect(::Bundler::FileUtils).to receive(:mkdir_p).with(settings_dir).
+ and_raise(Errno::EACCES.new(settings_dir.to_s))
+ expect { settings.set_global(:frozen, "1") }.
+ to raise_error(Bundler::PermissionError, /#{settings_dir}/)
+ end
+ end
+ end
+
+ describe "#pretty_values_for" do
+ it "prints the converted value rather than the raw string" do
+ bool_key = described_class::BOOL_KEYS.first
+ settings.set_local(bool_key, "false")
+ expect(subject.pretty_values_for(bool_key)).to eq [
+ "Set for your local app (#{bundled_app("config")}): false",
+ ]
+ end
+ end
+
+ describe "#mirror_for" do
+ let(:uri) { Gem::URI("https://rubygems.org/") }
+
+ context "with no configured mirror" do
+ it "returns the original URI" do
+ expect(settings.mirror_for(uri)).to eq(uri)
+ end
+
+ it "converts a string parameter to a URI" do
+ expect(settings.mirror_for("https://rubygems.org/")).to eq(uri)
+ end
+ end
+
+ context "with a configured mirror" do
+ let(:mirror_uri) { Gem::URI("https://example-mirror.rubygems.org/") }
+
+ before { settings.set_local "mirror.https://rubygems.org/", mirror_uri.to_s }
+
+ it "returns the mirror URI" do
+ expect(settings.mirror_for(uri)).to eq(mirror_uri)
+ end
+
+ it "converts a string parameter to a URI" do
+ expect(settings.mirror_for("https://rubygems.org/")).to eq(mirror_uri)
+ end
+
+ it "normalizes the URI" do
+ expect(settings.mirror_for("https://rubygems.org")).to eq(mirror_uri)
+ end
+
+ it "is case insensitive" do
+ expect(settings.mirror_for("HTTPS://RUBYGEMS.ORG/")).to eq(mirror_uri)
+ end
+
+ context "with a file URI" do
+ let(:mirror_uri) { Gem::URI("file:/foo/BAR/baz/qUx/") }
+
+ it "returns the mirror URI" do
+ expect(settings.mirror_for(uri)).to eq(mirror_uri)
+ end
+
+ it "converts a string parameter to a URI" do
+ expect(settings.mirror_for("file:/foo/BAR/baz/qUx/")).to eq(mirror_uri)
+ end
+
+ it "normalizes the URI" do
+ expect(settings.mirror_for("file:/foo/BAR/baz/qUx")).to eq(mirror_uri)
+ end
+ end
+ end
+ end
+
+ describe "#credentials_for" do
+ let(:uri) { Gem::URI("https://gemserver.example.org/") }
+ let(:credentials) { "username:password" }
+
+ context "with no configured credentials" do
+ it "returns nil" do
+ expect(settings.credentials_for(uri)).to be_nil
+ end
+ end
+
+ context "with credentials configured by URL" do
+ before { settings.set_local "https://gemserver.example.org/", credentials }
+
+ it "returns the configured credentials" do
+ expect(settings.credentials_for(uri)).to eq(credentials)
+ end
+ end
+
+ context "with credentials configured by hostname" do
+ before { settings.set_local "gemserver.example.org", credentials }
+
+ it "returns the configured credentials" do
+ expect(settings.credentials_for(uri)).to eq(credentials)
+ end
+ end
+ end
+
+ describe "URI normalization" do
+ it "normalizes HTTP URIs in credentials configuration" do
+ settings.set_local "http://gemserver.example.org", "username:password"
+ expect(settings.all).to include("http://gemserver.example.org/")
+ end
+
+ it "normalizes HTTPS URIs in credentials configuration" do
+ settings.set_local "https://gemserver.example.org", "username:password"
+ expect(settings.all).to include("https://gemserver.example.org/")
+ end
+
+ it "normalizes HTTP URIs in mirror configuration" do
+ settings.set_local "mirror.http://rubygems.org", "http://example-mirror.rubygems.org"
+ expect(settings.all).to include("mirror.http://rubygems.org/")
+ end
+
+ it "normalizes HTTPS URIs in mirror configuration" do
+ settings.set_local "mirror.https://rubygems.org", "http://example-mirror.rubygems.org"
+ expect(settings.all).to include("mirror.https://rubygems.org/")
+ end
+
+ it "does not normalize other config keys that happen to contain 'http'" do
+ settings.set_local "local.httparty", home("httparty")
+ expect(settings.all).to include("local.httparty")
+ end
+
+ it "does not normalize other config keys that happen to contain 'https'" do
+ settings.set_local "local.httpsmarty", home("httpsmarty")
+ expect(settings.all).to include("local.httpsmarty")
+ end
+
+ it "reads older keys without trailing slashes" do
+ settings.set_local "mirror.https://rubygems.org", "http://example-mirror.rubygems.org"
+ expect(settings.mirror_for("https://rubygems.org/")).to eq(
+ Gem::URI("http://example-mirror.rubygems.org/")
+ )
+ end
+
+ it "normalizes URIs with a fallback_timeout option" do
+ settings.set_local "mirror.https://rubygems.org/.fallback_timeout", "true"
+ expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout")
+ end
+
+ it "normalizes URIs with a fallback_timeout option without a trailing slash" do
+ settings.set_local "mirror.https://rubygems.org.fallback_timeout", "true"
+ expect(settings.all).to include("mirror.https://rubygems.org/.fallback_timeout")
+ end
+ end
+
+ describe "BUNDLE_ keys format" do
+ let(:settings) { described_class.new(bundled_app(".bundle")) }
+
+ it "converts older keys without double underscore" do
+ bundle_config("BUNDLE_MY__PERSONAL.MYRACK" => "~/Work/git/myrack")
+ expect(settings["my.personal.myrack"]).to eq("~/Work/git/myrack")
+ end
+
+ it "converts older keys without trailing slashes and double underscore" do
+ bundle_config("BUNDLE_MIRROR__HTTPS://RUBYGEMS.ORG" => "http://example-mirror.rubygems.org")
+ expect(settings["mirror.https://rubygems.org/"]).to eq("http://example-mirror.rubygems.org")
+ end
+
+ it "ignores commented out keys" do
+ create_file bundled_app(".bundle/config"), <<~C
+ # BUNDLE_MY-PERSONAL-SERVER__ORG: my-personal-server.org
+ C
+
+ expect(Bundler.ui).not_to receive(:warn)
+ expect(settings.all).to be_empty
+ end
+
+ it "converts older keys with dashes" do
+ bundle_config("BUNDLE_MY-PERSONAL-SERVER__ORG" => "my-personal-server.org")
+ expect(Bundler.ui).to receive(:warn).with(
+ "Your #{bundled_app(".bundle/config")} config includes `BUNDLE_MY-PERSONAL-SERVER__ORG`, which contains the dash character (`-`).\n" \
+ "This is deprecated, because configuration through `ENV` should be possible, but `ENV` keys cannot include dashes.\n" \
+ "Please edit #{bundled_app(".bundle/config")} and replace any dashes in configuration keys with a triple underscore (`___`)."
+ )
+ expect(settings["my-personal-server.org"]).to eq("my-personal-server.org")
+ end
+
+ it "reads newer keys format properly" do
+ bundle_config("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://example-mirror.rubygems.org")
+ expect(settings["mirror.https://rubygems.org/"]).to eq("http://example-mirror.rubygems.org")
+ end
+ end
+
+ describe "default_cli_command validation" do
+ it "accepts 'install' as a valid value" do
+ expect { settings.set_local("default_cli_command", "install") }.not_to raise_error
+ end
+
+ it "accepts 'cli_help' as a valid value" do
+ expect { settings.set_local("default_cli_command", "cli_help") }.not_to raise_error
+ end
+
+ it "rejects invalid values" do
+ expect { settings.set_local("default_cli_command", "invalid") }.to raise_error(
+ Bundler::InvalidOption,
+ /Setting `default_cli_command` to "invalid" failed:\n - default_cli_command must be either 'install' or 'cli_help'\n - must be one of: install, cli_help/
+ )
+ end
+
+ it "accepts nil values" do
+ expect { settings.set_local("default_cli_command", nil) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/bundler/bundler/shared_helpers_spec.rb b/spec/bundler/bundler/shared_helpers_spec.rb
new file mode 100644
index 0000000000..41115aa667
--- /dev/null
+++ b/spec/bundler/bundler/shared_helpers_spec.rb
@@ -0,0 +1,518 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::SharedHelpers do
+ before do
+ pwd_stub
+ end
+
+ let(:pwd_stub) { allow(subject).to receive(:pwd).and_return(bundled_app) }
+
+ subject { Bundler::SharedHelpers }
+
+ describe "#default_gemfile" do
+ before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" }
+
+ context "Gemfile is present" do
+ let(:expected_gemfile_path) { Pathname.new("/path/Gemfile").expand_path }
+
+ it "returns the Gemfile path" do
+ expect(subject.default_gemfile).to eq(expected_gemfile_path)
+ end
+ end
+
+ context "Gemfile is not present" do
+ before { ENV["BUNDLE_GEMFILE"] = nil }
+
+ it "raises a GemfileNotFound error" do
+ expect { subject.default_gemfile }.to raise_error(
+ Bundler::GemfileNotFound, "Could not locate Gemfile"
+ )
+ end
+ end
+
+ context "Gemfile is not an absolute path" do
+ before { ENV["BUNDLE_GEMFILE"] = "Gemfile" }
+
+ let(:expected_gemfile_path) { Pathname.new("Gemfile").expand_path }
+
+ it "returns the Gemfile path" do
+ expect(subject.default_gemfile).to eq(expected_gemfile_path)
+ end
+ end
+ end
+
+ describe "#default_lockfile" do
+ context "gemfile is gems.rb" do
+ let(:gemfile_path) { Pathname.new("/path/gems.rb") }
+ let(:expected_lockfile_path) { Pathname.new("/path/gems.locked") }
+
+ before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) }
+
+ it "returns the gems.locked path" do
+ expect(subject.default_lockfile).to eq(expected_lockfile_path)
+ end
+ end
+
+ context "is a regular Gemfile" do
+ let(:gemfile_path) { Pathname.new("/path/Gemfile") }
+ let(:expected_lockfile_path) { Pathname.new("/path/Gemfile.lock") }
+
+ before { allow(subject).to receive(:default_gemfile).and_return(gemfile_path) }
+
+ it "returns the lockfile path" do
+ expect(subject.default_lockfile).to eq(expected_lockfile_path)
+ end
+ end
+ end
+
+ describe "#default_bundle_dir" do
+ context ".bundle does not exist" do
+ it "returns nil" do
+ expect(subject.default_bundle_dir).to be_nil
+ end
+ end
+
+ context ".bundle is global .bundle" do
+ let(:global_rubygems_dir) { Pathname.new(bundled_app) }
+
+ before do
+ Dir.mkdir bundled_app(".bundle")
+ allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir)
+ end
+
+ it "returns nil" do
+ expect(subject.default_bundle_dir).to be_nil
+ end
+ end
+
+ context ".bundle is not global .bundle" do
+ let(:global_rubygems_dir) { Pathname.new("/path/rubygems") }
+ let(:expected_bundle_dir_path) { Pathname.new("#{bundled_app}/.bundle") }
+
+ before do
+ Dir.mkdir bundled_app(".bundle")
+ allow(Bundler.rubygems).to receive(:user_home).and_return(global_rubygems_dir)
+ end
+
+ it "returns the .bundle path" do
+ expect(subject.default_bundle_dir).to eq(expected_bundle_dir_path)
+ end
+ end
+ end
+
+ describe "#in_bundle?" do
+ it "calls the find_gemfile method" do
+ expect(subject).to receive(:find_gemfile)
+ subject.in_bundle?
+ end
+
+ shared_examples_for "correctly determines whether to return a Gemfile path" do
+ context "currently in directory with a Gemfile" do
+ before { FileUtils.touch(bundled_app_gemfile) }
+ after { FileUtils.rm(bundled_app_gemfile) }
+
+ it "returns path of the bundle Gemfile" do
+ expect(subject.in_bundle?).to eq("#{bundled_app}/Gemfile")
+ end
+ end
+
+ context "currently in directory without a Gemfile" do
+ it "returns nil" do
+ expect(subject.in_bundle?).to be_nil
+ end
+ end
+ end
+
+ context "ENV['BUNDLE_GEMFILE'] set" do
+ before { ENV["BUNDLE_GEMFILE"] = "/path/Gemfile" }
+
+ it "returns ENV['BUNDLE_GEMFILE']" do
+ expect(subject.in_bundle?).to eq("/path/Gemfile")
+ end
+ end
+
+ context "ENV['BUNDLE_GEMFILE'] not set" do
+ before { ENV["BUNDLE_GEMFILE"] = nil }
+
+ it_behaves_like "correctly determines whether to return a Gemfile path"
+ end
+
+ context "ENV['BUNDLE_GEMFILE'] is blank" do
+ before { ENV["BUNDLE_GEMFILE"] = "" }
+
+ it_behaves_like "correctly determines whether to return a Gemfile path"
+ end
+ end
+
+ describe "#chdir" do
+ let(:op_block) { proc { Dir.mkdir "nested_dir" } }
+
+ before { Dir.mkdir bundled_app("chdir_test_dir") }
+
+ it "executes the passed block while in the specified directory" do
+ subject.chdir(bundled_app("chdir_test_dir"), &op_block)
+ expect(bundled_app("chdir_test_dir/nested_dir")).to exist
+ end
+ end
+
+ describe "#pwd" do
+ let(:pwd_stub) { nil }
+
+ it "returns the current absolute path" do
+ expect(subject.pwd).to eq(git_root.to_s)
+ end
+ end
+
+ describe "#with_clean_git_env" do
+ let(:with_clean_git_env_block) { proc { Dir.mkdir bundled_app("with_clean_git_env_test_dir") } }
+
+ before do
+ ENV["GIT_DIR"] = "ORIGINAL_ENV_GIT_DIR"
+ ENV["GIT_WORK_TREE"] = "ORIGINAL_ENV_GIT_WORK_TREE"
+ end
+
+ it "executes the passed block" do
+ subject.with_clean_git_env(&with_clean_git_env_block)
+ expect(bundled_app("with_clean_git_env_test_dir")).to exist
+ end
+
+ context "when a block is passed" do
+ let(:with_clean_git_env_block) do
+ proc do
+ Dir.mkdir bundled_app("git_dir_test_dir") unless ENV["GIT_DIR"].nil?
+ Dir.mkdir bundled_app("git_work_tree_test_dir") unless ENV["GIT_WORK_TREE"].nil?
+ end end
+
+ it "uses a fresh git env for execution" do
+ subject.with_clean_git_env(&with_clean_git_env_block)
+ expect(bundled_app("git_dir_test_dir")).to_not exist
+ expect(bundled_app("git_work_tree_test_dir")).to_not exist
+ end
+ end
+
+ context "passed block does not throw errors" do
+ let(:with_clean_git_env_block) do
+ proc do
+ ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR"
+ ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE"
+ end end
+
+ it "restores the git env after" do
+ subject.with_clean_git_env(&with_clean_git_env_block)
+ expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR")
+ expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE")
+ end
+ end
+
+ context "passed block throws errors" do
+ let(:with_clean_git_env_block) do
+ proc do
+ ENV["GIT_DIR"] = "NEW_ENV_GIT_DIR"
+ ENV["GIT_WORK_TREE"] = "NEW_ENV_GIT_WORK_TREE"
+ raise RuntimeError.new
+ end end
+
+ it "restores the git env after" do
+ expect { subject.with_clean_git_env(&with_clean_git_env_block) }.to raise_error(RuntimeError)
+ expect(ENV["GIT_DIR"]).to eq("ORIGINAL_ENV_GIT_DIR")
+ expect(ENV["GIT_WORK_TREE"]).to eq("ORIGINAL_ENV_GIT_WORK_TREE")
+ end
+ end
+ end
+
+ describe "#set_bundle_environment" do
+ before do
+ ENV["BUNDLE_GEMFILE"] = "Gemfile"
+ end
+
+ shared_examples_for "ENV['PATH'] gets set correctly" do
+ before { Dir.mkdir bundled_app(".bundle") }
+
+ it "ensures bundle bin path is in ENV['PATH']" do
+ subject.set_bundle_environment
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR)
+ expect(paths).to include("#{Bundler.bundle_path}/bin")
+ end
+ end
+
+ shared_examples_for "ENV['RUBYOPT'] gets set correctly" do
+ it "ensures -rbundler/setup is at the beginning of ENV['RUBYOPT']" do
+ subject.set_bundle_environment
+ expect(ENV["RUBYOPT"].split(" ")).to start_with("-r#{install_path}/bundler/setup")
+ end
+ end
+
+ shared_examples_for "ENV['BUNDLER_SETUP'] gets set correctly" do
+ it "ensures bundler/setup is set in ENV['BUNDLER_SETUP']" do
+ subject.set_bundle_environment
+ expect(ENV["BUNDLER_SETUP"]).to eq("#{source_lib_dir}/bundler/setup")
+ end
+ end
+
+ shared_examples_for "ENV['RUBYLIB'] gets set correctly" do
+ let(:ruby_lib_path) { "stubbed_ruby_lib_dir" }
+
+ before do
+ allow(subject).to receive(:bundler_ruby_lib).and_return(ruby_lib_path)
+ end
+
+ it "ensures bundler's ruby version lib path is in ENV['RUBYLIB']" do
+ subject.set_bundle_environment
+ expect(rubylib).to include(ruby_lib_path)
+ end
+ end
+
+ it "calls the appropriate set methods" do
+ expect(subject).to receive(:set_bundle_variables)
+ expect(subject).to receive(:set_path)
+ expect(subject).to receive(:set_rubyopt)
+ expect(subject).to receive(:set_rubylib)
+ subject.set_bundle_environment
+ end
+
+ it "ignores if bundler_ruby_lib is same as rubylibdir" do
+ allow(subject).to receive(:bundler_ruby_lib).and_return(RbConfig::CONFIG["rubylibdir"])
+
+ subject.set_bundle_environment
+
+ expect(rubylib.count(RbConfig::CONFIG["rubylibdir"])).to eq(0)
+ end
+
+ it "exits if bundle path contains the unix-like path separator" do
+ if Gem.respond_to?(:path_separator)
+ allow(Gem).to receive(:path_separator).and_return(":")
+ else
+ stub_const("File::PATH_SEPARATOR", ":")
+ end
+ allow(Bundler).to receive(:bundle_path) { Pathname.new("so:me/dir/bin") }
+ expect { subject.send(:validate_bundle_path) }.to raise_error(
+ Bundler::PathError,
+ "Your bundle path contains text matching \":\", which is the " \
+ "path separator for your system. Bundler cannot " \
+ "function correctly when the Bundle path contains the " \
+ "system's PATH separator. Please change your " \
+ "bundle path to not match \":\".\nYour current bundle " \
+ "path is '#{Bundler.bundle_path}'."
+ )
+ end
+
+ context "with a jruby path_separator regex" do
+ # In versions of jruby that supported ruby 1.8, the path separator was the standard File::PATH_SEPARATOR
+ let(:regex) { Regexp.new("(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):") }
+ it "does not exit if bundle path is the standard uri path" do
+ allow(Bundler.rubygems).to receive(:path_separator).and_return(regex)
+ allow(Bundler).to receive(:bundle_path) { Pathname.new("uri:classloader:/WEB-INF/gems") }
+ expect { subject.send(:validate_bundle_path) }.not_to raise_error
+ end
+
+ it "exits if bundle path contains another directory" do
+ allow(Bundler.rubygems).to receive(:path_separator).and_return(regex)
+ allow(Bundler).to receive(:bundle_path) {
+ Pathname.new("uri:classloader:/WEB-INF/gems:other/dir")
+ }
+
+ expect { subject.send(:validate_bundle_path) }.to raise_error(
+ Bundler::PathError,
+ "Your bundle path contains text matching " \
+ "/(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):/, which is the " \
+ "path separator for your system. Bundler cannot " \
+ "function correctly when the Bundle path contains the " \
+ "system's PATH separator. Please change your " \
+ "bundle path to not match " \
+ "/(?<!jar:file|jar|file|classpath|uri:classloader|uri|http|https):/." \
+ "\nYour current bundle path is '#{Bundler.bundle_path}'."
+ )
+ end
+ end
+
+ context "ENV['PATH'] does not exist" do
+ before { ENV.delete("PATH") }
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+ end
+
+ context "ENV['PATH'] is empty" do
+ before { ENV["PATH"] = "" }
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+ end
+
+ context "ENV['PATH'] exists" do
+ before { ENV["PATH"] = "/some_path/bin" }
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+ end
+
+ context "ENV['PATH'] already contains the bundle bin path" do
+ let(:bundle_path) { "#{Bundler.bundle_path}/bin" }
+
+ before do
+ ENV["PATH"] = bundle_path
+ end
+
+ it_behaves_like "ENV['PATH'] gets set correctly"
+
+ it "ENV['PATH'] should only contain one instance of bundle bin path" do
+ subject.set_bundle_environment
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR)
+ expect(paths.count(bundle_path)).to eq(1)
+ end
+ end
+
+ context "when bundler install path is standard" do
+ let(:install_path) { source_lib_dir }
+
+ context "ENV['RUBYOPT'] does not exist" do
+ before { ENV.delete("RUBYOPT") }
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+
+ context "ENV['RUBYOPT'] exists without -rbundler/setup" do
+ before { ENV["RUBYOPT"] = "-I/some_app_path/lib" }
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+
+ context "ENV['RUBYOPT'] exists and contains -rbundler/setup" do
+ before { ENV["RUBYOPT"] = "-rbundler/setup" }
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+ end
+
+ context "when bundler install path contains special characters" do
+ let(:install_path) { "/opt/ruby3.3.0-preview2/lib/ruby/3.3.0+0" }
+
+ before do
+ ENV["RUBYOPT"] = "-r#{install_path}/bundler/setup"
+ allow(File).to receive(:expand_path).and_return("#{install_path}/bundler/setup")
+ allow(Gem).to receive(:bin_path).and_return("#{install_path}/bundler/setup")
+ end
+
+ it "ensures -rbundler/setup is not duplicated" do
+ subject.set_bundle_environment
+ expect(ENV["RUBYOPT"].split(" ").grep(%r{-r.*/bundler/setup}).length).to eq(1)
+ end
+
+ it_behaves_like "ENV['RUBYOPT'] gets set correctly"
+ end
+
+ context "ENV['RUBYLIB'] does not exist" do
+ before { ENV.delete("RUBYLIB") }
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+ end
+
+ context "ENV['RUBYLIB'] is empty" do
+ before { ENV["PATH"] = "" }
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+ end
+
+ context "ENV['RUBYLIB'] exists" do
+ before { ENV["PATH"] = "/some_path/bin" }
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+ end
+
+ context "bundle executable in ENV['BUNDLE_BIN_PATH'] does not exist" do
+ before { ENV["BUNDLE_BIN_PATH"] = "/does/not/exist" }
+ before { Bundler.rubygems.replace_bin_path [] }
+
+ it "sets BUNDLE_BIN_PATH to the bundle executable file" do
+ subject.set_bundle_environment
+ bin_path = ENV["BUNDLE_BIN_PATH"]
+ expect(bin_path).to eq(exedir.join("bundle").to_s)
+ expect(File.exist?(bin_path)).to be true
+ end
+ end
+
+ context "ENV['RUBYLIB'] already contains the bundler's ruby version lib path" do
+ let(:ruby_lib_path) { "stubbed_ruby_lib_dir" }
+
+ before do
+ ENV["RUBYLIB"] = ruby_lib_path
+ end
+
+ it_behaves_like "ENV['RUBYLIB'] gets set correctly"
+
+ it "ENV['RUBYLIB'] should only contain one instance of bundler's ruby version lib path" do
+ subject.set_bundle_environment
+ expect(rubylib.count(ruby_lib_path)).to eq(1)
+ end
+ end
+ end
+
+ describe "#filesystem_access" do
+ context "system has proper permission access" do
+ let(:file_op_block) { proc {|path| FileUtils.mkdir_p(path) } }
+
+ it "performs the operation in the passed block" do
+ subject.filesystem_access(bundled_app("test_dir"), &file_op_block)
+ expect(bundled_app("test_dir")).to exist
+ end
+ end
+
+ context "system throws Errno::EACESS" do
+ let(:file_op_block) { proc {|_path| raise Errno::EACCES.new("/path") } }
+
+ it "raises a PermissionError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::PermissionError
+ )
+ end
+ end
+
+ context "system throws Errno::EAGAIN" do
+ let(:file_op_block) { proc {|_path| raise Errno::EAGAIN } }
+
+ it "raises a TemporaryResourceError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::TemporaryResourceError
+ )
+ end
+ end
+
+ context "system throws Errno::EPROTO" do
+ let(:file_op_block) { proc {|_path| raise Errno::EPROTO } }
+
+ it "raises a VirtualProtocolError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::VirtualProtocolError
+ )
+ end
+ end
+
+ context "system throws Errno::ENOTSUP" do
+ let(:file_op_block) { proc {|_path| raise Errno::ENOTSUP } }
+
+ it "raises a OperationNotSupportedError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::OperationNotSupportedError
+ )
+ end
+ end
+
+ context "system throws Errno::ENOSPC" do
+ let(:file_op_block) { proc {|_path| raise Errno::ENOSPC } }
+
+ it "raises a NoSpaceOnDeviceError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::NoSpaceOnDeviceError
+ )
+ end
+ end
+
+ context "system throws an unhandled SystemCallError" do
+ let(:error) { SystemCallError.new("Shields down", 1337) }
+ let(:file_op_block) { proc {|_path| raise error } }
+
+ it "raises a GenericSystemCallError" do
+ expect { subject.filesystem_access("/path", &file_op_block) }.to raise_error(
+ Bundler::GenericSystemCallError, /error creating.+underlying.+Shields down/m
+ )
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb
new file mode 100644
index 0000000000..1f10ca4b07
--- /dev/null
+++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb
@@ -0,0 +1,354 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Git::GitProxy do
+ let(:path) { Pathname("path") }
+ let(:uri) { "https://github.com/ruby/rubygems.git" }
+ let(:ref) { nil }
+ let(:branch) { nil }
+ let(:tag) { nil }
+ let(:options) { { "ref" => ref, "branch" => branch, "tag" => tag }.compact }
+ let(:revision) { nil }
+ let(:git_source) { nil }
+ let(:clone_result) { double(Process::Status, success?: true) }
+ let(:fail_result) { double(Process::Status, success?: false) }
+ let(:base_clone_args) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch"] }
+ let(:base_fetch_args) { ["fetch", "--force", "--quiet", "--no-tags", "--depth", "1"] }
+ subject(:git_proxy) { described_class.new(path, uri, options, revision, git_source) }
+
+ context "with explicit ref" do
+ context "with branch only" do
+ let(:branch) { "main" }
+ it "sets explicit ref to branch" do
+ expect(git_proxy.explicit_ref).to eq(branch)
+ end
+ end
+
+ context "with ref only" do
+ let(:ref) { "HEAD" }
+ it "sets explicit ref to ref" do
+ expect(git_proxy.explicit_ref).to eq(ref)
+ end
+ end
+
+ context "with tag only" do
+ let(:tag) { "v1.0" }
+ it "sets explicit ref to ref" do
+ expect(git_proxy.explicit_ref).to eq(tag)
+ end
+ end
+
+ context "with tag and branch" do
+ let(:tag) { "v1.0" }
+ let(:branch) { "main" }
+ it "raises error" do
+ expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference)
+ end
+ end
+
+ context "with tag and ref" do
+ let(:tag) { "v1.0" }
+ let(:ref) { "HEAD" }
+ it "raises error" do
+ expect { git_proxy }.to raise_error(Bundler::Source::Git::AmbiguousGitReference)
+ end
+ end
+
+ context "with branch and ref" do
+ let(:branch) { "main" }
+ let(:ref) { "HEAD" }
+ it "honors ref over branch" do
+ expect(git_proxy.explicit_ref).to eq(ref)
+ end
+ end
+ end
+
+ context "with configured credentials" do
+ it "adds username and password to URI" do
+ Bundler.settings.temporary(uri => "u:p") do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ it "adds username and password to URI for host" do
+ Bundler.settings.temporary("github.com" => "u:p") do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", "https://u:p@github.com/ruby/rubygems.git", path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ it "does not add username and password to mismatched URI" do
+ Bundler.settings.temporary("https://u:p@github.com/ruby/rubygems-mismatch.git" => "u:p") do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ it "keeps original userinfo" do
+ Bundler.settings.temporary("github.com" => "u:p") do
+ original = "https://orig:info@github.com/ruby/rubygems.git"
+ git_proxy = described_class.new(Pathname("path"), original, options)
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", original, path.to_s], nil).and_return(["", "", clone_result])
+ git_proxy.checkout
+ end
+ end
+ end
+
+ describe "#version" do
+ context "with a normal version number" do
+ before do
+ expect(described_class).to receive(:full_version).
+ and_return("git version 1.2.3")
+ end
+
+ it "returns the git version number" do
+ expect(git_proxy.version).to eq("1.2.3")
+ end
+
+ it "does not raise an error when passed into Gem::Version.create" do
+ expect { Gem::Version.create git_proxy.version }.not_to raise_error
+ end
+ end
+
+ context "with a OSX version number" do
+ before do
+ expect(described_class).to receive(:full_version).
+ and_return("git version 1.2.3 (Apple Git-BS)")
+ end
+
+ it "strips out OSX specific additions in the version string" do
+ expect(git_proxy.version).to eq("1.2.3")
+ end
+
+ it "does not raise an error when passed into Gem::Version.create" do
+ expect { Gem::Version.create git_proxy.version }.not_to raise_error
+ end
+ end
+
+ context "with a msysgit version number" do
+ before do
+ expect(described_class).to receive(:full_version).
+ and_return("git version 1.2.3.msysgit.0")
+ end
+
+ it "strips out msysgit specific additions in the version string" do
+ expect(git_proxy.version).to eq("1.2.3")
+ end
+
+ it "does not raise an error when passed into Gem::Version.create" do
+ expect { Gem::Version.create git_proxy.version }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#full_version" do
+ context "with a normal version number" do
+ before do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3", "", status])
+ end
+
+ it "returns the git version number" do
+ expect(git_proxy.full_version).to eq("1.2.3")
+ end
+ end
+
+ context "with a OSX version number" do
+ before do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3 (Apple Git-BS)", "", status])
+ end
+
+ it "does not strip out OSX specific additions in the version string" do
+ expect(git_proxy.full_version).to eq("1.2.3 (Apple Git-BS)")
+ end
+ end
+
+ context "with a msysgit version number" do
+ before do
+ status = double("success?" => true)
+ expect(Open3).to receive(:capture3).with("git", "--version").
+ and_return(["git version 1.2.3.msysgit.0", "", status])
+ end
+
+ it "does not strip out msysgit specific additions in the version string" do
+ expect(git_proxy.full_version).to eq("1.2.3.msysgit.0")
+ end
+ end
+ end
+
+ it "doesn't allow arbitrary code execution through Gemfile uris with a leading dash" do
+ gemfile <<~G
+ gem "poc", git: "-u./pay:load.sh"
+ G
+
+ file = bundled_app("pay:load.sh")
+
+ create_file file, <<~RUBY
+ #!/bin/sh
+
+ touch #{bundled_app("canary")}
+ RUBY
+
+ FileUtils.chmod("+x", file)
+
+ bundle :lock, raise_on_error: false
+
+ expect(Pathname.new(bundled_app("canary"))).not_to exist
+ end
+
+ context "URI is HTTP" do
+ let(:uri) { "http://github.com/ruby/rubygems.git" }
+ let(:clone_args_without_depth) { ["clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--single-branch"] }
+
+ it "retries clone without --depth when dumb http transport fails" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "dumb http transport does not support shallow capabilities", fail_result])
+ expect(git_proxy).to receive(:capture).with([*clone_args_without_depth, "--", uri, path.to_s], nil).and_return(["", "", clone_result])
+
+ subject.checkout
+ end
+ end
+
+ describe "#installed_to?" do
+ let(:destination) { "install/dir" }
+ let(:destination_dir_exists) { true }
+ let(:children) { ["gem.gemspec", "README.me", ".git", "Rakefile"] }
+
+ before do
+ allow(Dir).to receive(:exist?).with(destination).and_return(destination_dir_exists)
+ allow(Dir).to receive(:children).with(destination).and_return(children)
+ end
+
+ context "when destination dir exists with children other than just .git" do
+ it "returns true" do
+ expect(git_proxy.installed_to?(destination)).to be true
+ end
+ end
+
+ context "when destination dir does not exist" do
+ let(:destination_dir_exists) { false }
+
+ it "returns false" do
+ expect(git_proxy.installed_to?(destination)).to be false
+ end
+ end
+
+ context "when destination dir is empty" do
+ let(:children) { [] }
+
+ it "returns false" do
+ expect(git_proxy.installed_to?(destination)).to be false
+ end
+ end
+
+ context "when destination dir has only .git directory" do
+ let(:children) { [".git"] }
+
+ it "returns false" do
+ expect(git_proxy.installed_to?(destination)).to be false
+ end
+ end
+ end
+
+ describe "#checkout" do
+ context "when the repository isn't cloned" do
+ before do
+ allow(path).to receive(:exist?).and_return(false)
+ end
+
+ it "clones the repository" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_clone_args, "--", uri, path.to_s], nil).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ context "when the repository is cloned" do
+ before do
+ allow(path).to receive(:exist?).and_return(true)
+ end
+
+ context "with a locked revision" do
+ let(:revision) { Digest::SHA1.hexdigest("ruby") }
+
+ context "when the revision exists locally" do
+ it "uses the cached revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", revision, dir: path).and_return(true)
+ subject.checkout
+ end
+ end
+
+ context "when the revision doesn't exist locally" do
+ it "fetches the specific revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", revision, dir: path).and_raise(Bundler::GitError)
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "#{revision}:refs/#{revision}-sha"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+ end
+
+ context "with no explicit ref" do
+ it "fetches the HEAD revision" do
+ parsed_revision = Digest::SHA1.hexdigest("ruby")
+ allow(git_proxy).to receive(:git_local).with("rev-parse", "--abbrev-ref", "HEAD", dir: path).and_return(parsed_revision)
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ context "with a commit ref" do
+ let(:ref) { Digest::SHA1.hexdigest("ruby") }
+
+ context "when the revision exists locally" do
+ it "uses the cached revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", ref, dir: path).and_return(true)
+ subject.checkout
+ end
+ end
+
+ context "when the revision doesn't exist locally" do
+ it "fetches the specific revision" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:git).with("cat-file", "-e", ref, dir: path).and_raise(Bundler::GitError)
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--depth", "1", "--", uri, "#{ref}:refs/#{ref}-sha"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+ end
+
+ context "with a non-commit ref" do
+ let(:ref) { "HEAD" }
+
+ it "fetches all revisions" do
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--", uri, "refs/*:refs/*"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+
+ context "URI is HTTP" do
+ let(:uri) { "http://github.com/ruby/rubygems.git" }
+
+ it "retries fetch without --depth when dumb http transport fails" do
+ parsed_revision = Digest::SHA1.hexdigest("ruby")
+ allow(git_proxy).to receive(:git_local).with("rev-parse", "--abbrev-ref", "HEAD", dir: path).and_return(parsed_revision)
+ allow(git_proxy).to receive(:git_local).with("--version").and_return("git version 2.14.0")
+ expect(git_proxy).to receive(:capture).with([*base_fetch_args, "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "dumb http transport does not support shallow capabilities", fail_result])
+ expect(git_proxy).to receive(:capture).with(["fetch", "--force", "--quiet", "--no-tags", "--", uri, "refs/heads/#{parsed_revision}:refs/heads/#{parsed_revision}"], path).and_return(["", "", clone_result])
+ subject.checkout
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/git_spec.rb b/spec/bundler/bundler/source/git_spec.rb
new file mode 100644
index 0000000000..14e91c6bdc
--- /dev/null
+++ b/spec/bundler/bundler/source/git_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Git do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ let(:uri) { "https://github.com/foo/bar.git" }
+ let(:options) do
+ { "uri" => uri }
+ end
+
+ subject { described_class.new(options) }
+
+ describe "#to_s" do
+ it "returns a description" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git"
+ end
+
+ context "when the URI contains credentials" do
+ let(:uri) { "https://my-secret-token:x-oauth-basic@github.com/foo/bar.git" }
+
+ it "filters credentials" do
+ expect(subject.to_s).to eq "https://x-oauth-basic@github.com/foo/bar.git"
+ end
+ end
+
+ context "when the source has a glob specifier" do
+ let(:glob) { "bar/baz/*.gemspec" }
+ let(:options) do
+ { "uri" => uri, "glob" => glob }
+ end
+
+ it "includes it" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git (glob: bar/baz/*.gemspec)"
+ end
+ end
+
+ context "when the source has a reference" do
+ let(:git_proxy_stub) do
+ instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0")
+ end
+ let(:options) do
+ { "uri" => uri, "ref" => "v1.0.0" }
+ end
+
+ before do
+ allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub)
+ end
+
+ it "includes it" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc)"
+ end
+ end
+
+ context "when the source has both reference and glob specifiers" do
+ let(:git_proxy_stub) do
+ instance_double(Bundler::Source::Git::GitProxy, revision: "123abc", branch: "v1.0.0")
+ end
+ let(:options) do
+ { "uri" => uri, "ref" => "v1.0.0", "glob" => "gems/foo/*.gemspec" }
+ end
+
+ before do
+ allow(Bundler::Source::Git::GitProxy).to receive(:new).and_return(git_proxy_stub)
+ end
+
+ it "includes both" do
+ expect(subject.to_s).to eq "https://github.com/foo/bar.git (at v1.0.0@123abc, glob: gems/foo/*.gemspec)"
+ end
+ end
+ end
+
+ describe "#locked_revision_checked_out?" do
+ let(:revision) { "abc" }
+ let(:git_proxy_revision) { revision }
+ let(:git_proxy_installed) { true }
+ let(:git_proxy) { subject.send(:git_proxy) }
+ let(:options) do
+ {
+ "uri" => uri,
+ "revision" => revision,
+ }
+ end
+
+ before do
+ allow(git_proxy).to receive(:revision).and_return(git_proxy_revision)
+ allow(git_proxy).to receive(:installed_to?).with(subject.install_path).and_return(git_proxy_installed)
+ end
+
+ context "when the locked revision is checked out" do
+ it "returns true" do
+ expect(subject.send(:locked_revision_checked_out?)).to be true
+ end
+ end
+
+ context "when no revision is provided" do
+ let(:options) do
+ { "uri" => uri }
+ end
+
+ it "returns falsey value" do
+ expect(subject.send(:locked_revision_checked_out?)).to be_falsey
+ end
+ end
+
+ context "when the git proxy revision is different than the git revision" do
+ let(:git_proxy_revision) { revision.next }
+
+ it "returns falsey value" do
+ expect(subject.send(:locked_revision_checked_out?)).to be_falsey
+ end
+ end
+
+ context "when the gem hasn't been installed" do
+ let(:git_proxy_installed) { false }
+
+ it "returns falsey value" do
+ expect(subject.send(:locked_revision_checked_out?)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/path_spec.rb b/spec/bundler/bundler/source/path_spec.rb
new file mode 100644
index 0000000000..1d13e03ec1
--- /dev/null
+++ b/spec/bundler/bundler/source/path_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Path do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ describe "#eql?" do
+ subject { described_class.new("path" => "gems/a") }
+
+ context "with two equivalent relative paths from different roots" do
+ let(:a_gem_opts) { { "path" => "../gems/a", "root_path" => Bundler.root.join("nested") } }
+ let(:a_gem) { described_class.new a_gem_opts }
+
+ it "returns true" do
+ expect(subject).to eq a_gem
+ end
+ end
+
+ context "with the same (but not equivalent) relative path from different roots" do
+ subject { described_class.new("path" => "gems/a") }
+
+ let(:a_gem_opts) { { "path" => "gems/a", "root_path" => Bundler.root.join("nested") } }
+ let(:a_gem) { described_class.new a_gem_opts }
+
+ it "returns false" do
+ expect(subject).to_not eq a_gem
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/rubygems/remote_spec.rb b/spec/bundler/bundler/source/rubygems/remote_spec.rb
new file mode 100644
index 0000000000..27430d4a3b
--- /dev/null
+++ b/spec/bundler/bundler/source/rubygems/remote_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require "bundler/source/rubygems/remote"
+
+RSpec.describe Bundler::Source::Rubygems::Remote do
+ def remote(uri)
+ Bundler::Source::Rubygems::Remote.new(uri)
+ end
+
+ before do
+ allow(Digest(:MD5)).to receive(:hexdigest).with(duck_type(:to_s)) {|string| "MD5HEX(#{string})" }
+ end
+
+ let(:uri_no_auth) { Gem::URI("https://gems.example.com") }
+ let(:uri_with_auth) { Gem::URI("https://#{credentials}@gems.example.com") }
+ let(:credentials) { "username:password" }
+
+ context "when the original URI has no credentials" do
+ describe "#uri" do
+ it "returns the original URI" do
+ expect(remote(uri_no_auth).uri).to eq(uri_no_auth)
+ end
+
+ it "applies configured credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => credentials) do
+ expect(remote(uri_no_auth).uri).to eq(uri_with_auth)
+ end
+ end
+ end
+
+ describe "#anonymized_uri" do
+ it "returns the original URI" do
+ expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+
+ it "does not apply given credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => credentials) do
+ expect(remote(uri_no_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+ end
+ end
+
+ describe "#cache_slug" do
+ it "returns the correct slug" do
+ expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.443.MD5HEX(gems.example.com.443./)")
+ end
+
+ it "only applies the given user" do
+ Bundler.settings.temporary(uri_no_auth.to_s => credentials) do
+ expect(remote(uri_no_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)")
+ end
+ end
+ end
+ end
+
+ context "when the original URI has a username and password" do
+ describe "#uri" do
+ it "returns the original URI" do
+ expect(remote(uri_with_auth).uri).to eq(uri_with_auth)
+ end
+
+ it "does not apply configured credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff")
+ expect(remote(uri_with_auth).uri).to eq(uri_with_auth)
+ end
+ end
+
+ describe "#anonymized_uri" do
+ it "returns the URI without username and password" do
+ expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+
+ it "does not apply given credentials" do
+ Bundler.settings.temporary(uri_no_auth.to_s => "other:stuff")
+ expect(remote(uri_with_auth).anonymized_uri).to eq(uri_no_auth)
+ end
+ end
+
+ describe "#cache_slug" do
+ it "returns the correct slug" do
+ expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)")
+ end
+
+ it "does not apply given credentials" do
+ Bundler.settings.temporary(uri_with_auth.to_s => credentials)
+ expect(remote(uri_with_auth).cache_slug).to eq("gems.example.com.username.443.MD5HEX(gems.example.com.username.443./)")
+ end
+ end
+ end
+
+ context "when the original URI has only a username" do
+ let(:uri) { Gem::URI("https://SeCrEt-ToKeN@gem.fury.io/me/") }
+
+ describe "#anonymized_uri" do
+ it "returns the URI without username and password" do
+ expect(remote(uri).anonymized_uri).to eq(Gem::URI("https://gem.fury.io/me/"))
+ end
+ end
+
+ describe "#cache_slug" do
+ it "returns the correct slug" do
+ expect(remote(uri).cache_slug).to eq("gem.fury.io.SeCrEt-ToKeN.443.MD5HEX(gem.fury.io.SeCrEt-ToKeN.443./me/)")
+ end
+ end
+ end
+
+ context "when a mirror with inline credentials is configured for the URI" do
+ let(:uri) { Gem::URI("https://rubygems.org/") }
+ let(:mirror_uri_with_auth) { Gem::URI("https://username:password@example-mirror.rubygems.org/") }
+ let(:mirror_uri_no_auth) { Gem::URI("https://example-mirror.rubygems.org/") }
+
+ before { Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_with_auth.to_s) }
+
+ after { Bundler.settings.temporary("mirror.https://rubygems.org/" => nil) }
+
+ specify "#uri returns the mirror URI with credentials" do
+ expect(remote(uri).uri).to eq(mirror_uri_with_auth)
+ end
+
+ specify "#anonymized_uri returns the mirror URI without credentials" do
+ expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth)
+ end
+
+ specify "#original_uri returns the original source" do
+ expect(remote(uri).original_uri).to eq(uri)
+ end
+
+ specify "#cache_slug returns the correct slug" do
+ expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)")
+ end
+ end
+
+ context "when a mirror with configured credentials is configured for the URI" do
+ let(:uri) { Gem::URI("https://rubygems.org/") }
+ let(:mirror_uri_with_auth) { Gem::URI("https://#{credentials}@example-mirror.rubygems.org/") }
+ let(:mirror_uri_no_auth) { Gem::URI("https://example-mirror.rubygems.org/") }
+
+ before do
+ Bundler.settings.temporary("mirror.https://rubygems.org/" => mirror_uri_no_auth.to_s)
+ Bundler.settings.temporary(mirror_uri_no_auth.to_s => credentials)
+ end
+
+ after do
+ Bundler.settings.temporary("mirror.https://rubygems.org/" => nil)
+ Bundler.settings.temporary(mirror_uri_no_auth.to_s => nil)
+ end
+
+ specify "#uri returns the mirror URI with credentials" do
+ expect(remote(uri).uri).to eq(mirror_uri_with_auth)
+ end
+
+ specify "#anonymized_uri returns the mirror URI without credentials" do
+ expect(remote(uri).anonymized_uri).to eq(mirror_uri_no_auth)
+ end
+
+ specify "#original_uri returns the original source" do
+ expect(remote(uri).original_uri).to eq(uri)
+ end
+
+ specify "#cache_slug returns the original source" do
+ expect(remote(uri).cache_slug).to eq("rubygems.org.443.MD5HEX(rubygems.org.443./)")
+ end
+ end
+
+ context "when there is no mirror set" do
+ describe "#original_uri" do
+ it "is not set" do
+ expect(remote(uri_no_auth).original_uri).to be_nil
+ end
+ end
+ end
+
+ describe "#cooldown" do
+ it "is nil by default" do
+ expect(remote(uri_no_auth).cooldown).to be_nil
+ end
+
+ it "returns the value passed to the constructor" do
+ r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
+ expect(r.cooldown).to eq(7)
+ end
+ end
+
+ describe "#effective_cooldown" do
+ it "returns the per-remote value when no override is set" do
+ r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
+ expect(r.effective_cooldown).to eq(7)
+ end
+
+ it "returns nil when neither override nor per-remote value is set" do
+ expect(remote(uri_no_auth).effective_cooldown).to be_nil
+ end
+
+ it "settings override per-remote value" do
+ r = Bundler::Source::Rubygems::Remote.new(uri_no_auth, cooldown: 7)
+ Bundler.settings.temporary(cooldown: 14) do
+ expect(r.effective_cooldown).to eq(14)
+ end
+ end
+
+ it "settings override even when per-remote value is absent" do
+ Bundler.settings.temporary(cooldown: 14) do
+ expect(remote(uri_no_auth).effective_cooldown).to eq(14)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source/rubygems_spec.rb b/spec/bundler/bundler/source/rubygems_spec.rb
new file mode 100644
index 0000000000..feb787498e
--- /dev/null
+++ b/spec/bundler/bundler/source/rubygems_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source::Rubygems do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new("root") }
+ end
+
+ describe "caches" do
+ it "includes Bundler.app_cache" do
+ expect(subject.caches).to include(Bundler.app_cache)
+ end
+
+ it "includes GEM_PATH entries" do
+ Gem.path.each do |path|
+ expect(subject.caches).to include(File.expand_path("#{path}/cache"))
+ end
+ end
+
+ it "is an array of strings or pathnames" do
+ subject.caches.each do |cache|
+ expect([String, Pathname]).to include(cache.class)
+ end
+ end
+ end
+
+ describe "#add_remote" do
+ context "when the source is an HTTP(s) URI with no host" do
+ it "raises error" do
+ expect { subject.add_remote("https:rubygems.org") }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ describe "#no_remotes?" do
+ context "when no remote provided" do
+ it "returns a truthy value" do
+ expect(described_class.new("remotes" => []).no_remotes?).to be_truthy
+ end
+ end
+
+ context "when a remote provided" do
+ it "returns a falsey value" do
+ expect(described_class.new("remotes" => ["https://rubygems.org"]).no_remotes?).to be_falsey
+ end
+ end
+ end
+
+ describe "#clear_cache" do
+ it "invalidates memoized indexes so subsequent reads rebuild them" do
+ source = described_class.new
+
+ first_specs = source.specs
+ first_installed = source.send(:installed_specs)
+ first_default = source.send(:default_specs)
+ first_cached = source.send(:cached_specs)
+
+ expect(source.specs).to equal(first_specs)
+ expect(source.send(:installed_specs)).to equal(first_installed)
+ expect(source.send(:default_specs)).to equal(first_default)
+ expect(source.send(:cached_specs)).to equal(first_cached)
+
+ source.clear_cache
+
+ expect(source.specs).not_to equal(first_specs)
+ expect(source.send(:installed_specs)).not_to equal(first_installed)
+ expect(source.send(:default_specs)).not_to equal(first_default)
+ expect(source.send(:cached_specs)).not_to equal(first_cached)
+ end
+
+ it "reflects newly-discovered installed gems after clear_cache" do
+ source = described_class.new
+ foo = Gem::Specification.new("foo", "1.0.0")
+ bar = Gem::Specification.new("bar", "1.0.0")
+
+ allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo])
+ expect(source.send(:installed_specs).search("bar")).to be_empty
+
+ allow(Bundler.rubygems).to receive(:installed_specs).and_return([foo, bar])
+ expect(source.send(:installed_specs).search("bar")).to be_empty
+
+ source.clear_cache
+
+ expect(source.send(:installed_specs).search("bar")).not_to be_empty
+ end
+ end
+
+ describe "log debug information" do
+ it "log the time spent downloading and installing a gem" do
+ build_repo2 do
+ build_gem "warning"
+ end
+
+ gemfile_content = <<~G
+ source "https://gem.repo2"
+ gem "warning"
+ G
+
+ stdout = install_gemfile(gemfile_content, env: { "DEBUG" => "1" })
+
+ expect(stdout).to match(/Downloaded warning in: \d+\.\d+s/)
+ expect(stdout).to match(/Installed warning in: \d+\.\d+s/)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source_list_spec.rb b/spec/bundler/bundler/source_list_spec.rb
new file mode 100644
index 0000000000..61bd99b063
--- /dev/null
+++ b/spec/bundler/bundler/source_list_spec.rb
@@ -0,0 +1,475 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::SourceList do
+ before do
+ allow(Bundler).to receive(:root) { Pathname.new "./tmp/bundled_app" }
+
+ stub_const "ASourcePlugin", Class.new(Bundler::Plugin::API)
+ ASourcePlugin.source "new_source"
+ allow(Bundler::Plugin).to receive(:source?).with("new_source").and_return(true)
+ end
+
+ subject(:source_list) { Bundler::SourceList.new }
+
+ let(:global_rubygems_source) { Bundler::Source::Rubygems.new }
+ let(:metadata_source) { Bundler::Source::Metadata.new }
+
+ describe "adding sources" do
+ before do
+ source_list.add_path_source("path" => "/existing/path/to/gem")
+ source_list.add_git_source("uri" => "git://existing-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://existing-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
+ end
+
+ describe "#add_path_source" do
+ before do
+ @duplicate = source_list.add_path_source("path" => "/path/to/gem")
+ @new_source = source_list.add_path_source("path" => "/path/to/gem")
+ end
+
+ it "returns the new path source" do
+ expect(@new_source).to be_instance_of(Bundler::Source::Path)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("path" => "/path/to/gem")
+ end
+
+ it "adds the source to the beginning of path_sources" do
+ expect(source_list.path_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ expect(source_list.path_sources).not_to include equal(@duplicate)
+ end
+ end
+
+ describe "#add_git_source" do
+ before do
+ @duplicate = source_list.add_git_source("uri" => "git://host/path.git")
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ end
+
+ it "returns the new git source" do
+ expect(@new_source).to be_instance_of(Bundler::Source::Git)
+ end
+
+ it "passes the provided options to the new source" do
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ expect(@new_source.options).to eq("uri" => "git://host/path.git")
+ end
+
+ it "adds the source to the beginning of git_sources" do
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ expect(source_list.git_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ @duplicate = source_list.add_git_source("uri" => "git://host/path.git")
+ @new_source = source_list.add_git_source("uri" => "git://host/path.git")
+ expect(source_list.git_sources).not_to include equal(@duplicate)
+ end
+
+ context "with the git: protocol" do
+ let(:msg) do
+ "The git source `git://existing-git.org/path.git` " \
+ "uses the `git` protocol, which transmits data without encryption. " \
+ "Disable this warning with `bundle config set --local git.allow_insecure true`, " \
+ "or switch to the `https` protocol to keep your data secure."
+ end
+
+ it "warns about git protocols" do
+ expect(Bundler.ui).to receive(:warn).with(msg)
+ source_list.add_git_source("uri" => "git://existing-git.org/path.git")
+ end
+
+ it "ignores git protocols on request" do
+ Bundler.settings.temporary("git.allow_insecure": true)
+ expect(Bundler.ui).to_not receive(:warn).with(msg)
+ source_list.add_git_source("uri" => "git://existing-git.org/path.git")
+ end
+ end
+ end
+
+ describe "#add_rubygems_source" do
+ before do
+ @duplicate = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"])
+ @new_source = source_list.add_rubygems_source("remotes" => ["https://rubygems.org/"])
+ end
+
+ it "returns the new rubygems source" do
+ expect(@new_source).to be_instance_of(Bundler::Source::Rubygems)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("remotes" => ["https://rubygems.org/"])
+ end
+
+ it "adds the source to the beginning of rubygems_sources" do
+ expect(source_list.rubygems_sources.first).to equal(@new_source)
+ end
+
+ it "removes duplicates" do
+ expect(source_list.rubygems_sources).not_to include equal(@duplicate)
+ end
+ end
+
+ describe "#add_global_rubygems_remote" do
+ let!(:returned_source) { source_list.add_global_rubygems_remote("https://rubygems.org/") }
+
+ it "returns the global rubygems source" do
+ expect(returned_source).to be_instance_of(Bundler::Source::Rubygems)
+ end
+
+ it "adds the provided remote to the beginning of the global source" do
+ source_list.add_global_rubygems_remote("https://othersource.org")
+ expect(returned_source.remotes).to eq [
+ Gem::URI("https://othersource.org/"),
+ Gem::URI("https://rubygems.org/"),
+ ]
+ end
+
+ it "records the per-remote cooldown when supplied" do
+ source_list.add_global_rubygems_remote("https://othersource.org", cooldown: 7)
+ expect(returned_source.cooldown_for(Gem::URI("https://othersource.org/"))).to eq(7)
+ expect(returned_source.cooldown_for(Gem::URI("https://rubygems.org/"))).to be_nil
+ end
+ end
+
+ describe "#add_plugin_source" do
+ before do
+ @duplicate = source_list.add_plugin_source("new_source", "uri" => "http://host/path.")
+ @new_source = source_list.add_plugin_source("new_source", "uri" => "http://host/path.")
+ end
+
+ it "returns the new plugin source" do
+ expect(@new_source).to be_a(Bundler::Plugin::API::Source)
+ end
+
+ it "passes the provided options to the new source" do
+ expect(@new_source.options).to eq("uri" => "http://host/path.")
+ end
+
+ it "adds the source to the beginning of git_sources" do
+ expect(source_list.plugin_sources.first).to equal(@new_source)
+ end
+
+ it "removes existing duplicates" do
+ expect(source_list.plugin_sources).not_to include equal(@duplicate)
+ end
+ end
+ end
+
+ describe "#all_sources" do
+ it "includes the global rubygems source when rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://rubygems.org"])
+ source_list.add_path_source("path" => "/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
+
+ expect(source_list.all_sources).to include global_rubygems_source
+ end
+
+ it "includes the global rubygems source when no rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_path_source("path" => "/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/a")
+
+ expect(source_list.all_sources).to include global_rubygems_source
+ end
+
+ it "returns sources of the same type in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"])
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/b")
+ source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"])
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://some.o.url/")
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_plugin_source("new_source", "uri" => "https://some.url/c")
+ source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.all_sources).to eq [
+ Bundler::Source::Path.new("path" => "/first/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/second/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/third/path/to/gem"),
+ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://some.url/c"),
+ ASourcePlugin.new("uri" => "https://some.o.url/"),
+ ASourcePlugin.new("uri" => "https://some.url/b"),
+ Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]),
+ global_rubygems_source,
+ metadata_source,
+ ]
+ end
+ end
+
+ describe "#path_sources" do
+ it "returns an empty array when no path sources have been added" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+ source_list.add_git_source("uri" => "git://host/path.git")
+ expect(source_list.path_sources).to be_empty
+ end
+
+ it "returns path sources in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://fifth-rubygems.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_global_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_global_rubygems_remote("https://third-rubygems.org")
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_global_rubygems_remote("https://first-rubygems.org")
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.path_sources).to eq [
+ Bundler::Source::Path.new("path" => "/first/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/second/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/third/path/to/gem"),
+ ]
+ end
+ end
+
+ describe "#git_sources" do
+ it "returns an empty array when no git sources have been added" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.git_sources).to be_empty
+ end
+
+ it "returns git sources in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://fifth-rubygems.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_global_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_global_rubygems_remote("https://third-rubygems.org")
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_global_rubygems_remote("https://first-rubygems.org")
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.git_sources).to eq [
+ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ]
+ end
+ end
+
+ describe "#plugin_sources" do
+ it "returns an empty array when no plugin sources have been added" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.plugin_sources).to be_empty
+ end
+
+ it "returns plugin sources in the reverse order that they were added" do
+ source_list.add_plugin_source("new_source", "uri" => "https://third-git.org/path.git")
+ source_list.add_git_source("https://new-git.org")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_global_rubygems_remote("https://fourth-rubygems.org")
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_global_rubygems_remote("https://third-rubygems.org")
+ source_list.add_plugin_source("new_source", "uri" => "git://second-git.org/path.git")
+ source_list.add_global_rubygems_remote("https://second-rubygems.org")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_global_rubygems_remote("https://first-rubygems.org")
+ source_list.add_plugin_source("new_source", "uri" => "git://first-git.org/path.git")
+
+ expect(source_list.plugin_sources).to eq [
+ ASourcePlugin.new("uri" => "git://first-git.org/path.git"),
+ ASourcePlugin.new("uri" => "git://second-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://third-git.org/path.git"),
+ ]
+ end
+ end
+
+ describe "#rubygems_sources" do
+ it "includes the global rubygems source when rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://rubygems.org"])
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.rubygems_sources).to include global_rubygems_source
+ end
+
+ it "returns only the global rubygems source when no rubygems sources have been added" do
+ source_list.add_git_source("uri" => "git://host/path.git")
+ source_list.add_path_source("path" => "/path/to/gem")
+
+ expect(source_list.rubygems_sources).to eq [global_rubygems_source]
+ end
+
+ it "returns rubygems sources in the reverse order that they were added" do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://fifth-rubygems.org"])
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://fourth-rubygems.org"])
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+
+ expect(source_list.rubygems_sources).to eq [
+ Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fourth-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://fifth-rubygems.org"]),
+ global_rubygems_source,
+ ]
+ end
+ end
+
+ describe "#get" do
+ context "when it includes an equal source" do
+ let(:rubygems_source) { Bundler::Source::Rubygems.new("remotes" => ["https://rubygems.org"]) }
+ before { @equal_source = source_list.add_global_rubygems_remote("https://rubygems.org") }
+
+ it "returns the equal source" do
+ expect(source_list.get(rubygems_source)).to be @equal_source
+ end
+ end
+
+ context "when it does not include an equal source" do
+ let(:path_source) { Bundler::Source::Path.new("path" => "/path/to/gem") }
+
+ it "returns nil" do
+ expect(source_list.get(path_source)).to be_nil
+ end
+ end
+ end
+
+ describe "#lock_sources" do
+ before do
+ source_list.add_git_source("uri" => "git://third-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://third-bar.org/foo")
+ source_list.add_path_source("path" => "/third/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://third-rubygems.org"])
+ source_list.add_path_source("path" => "/second/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://second-rubygems.org"])
+ source_list.add_git_source("uri" => "git://second-git.org/path.git")
+ source_list.add_rubygems_source("remotes" => ["https://first-rubygems.org"])
+ source_list.add_plugin_source("new_source", "uri" => "https://second-plugin.org/random")
+ source_list.add_path_source("path" => "/first/path/to/gem")
+ source_list.add_rubygems_source("remotes" => ["https://duplicate-rubygems.org"])
+ source_list.add_git_source("uri" => "git://first-git.org/path.git")
+ end
+
+ it "returns all sources, without combining rubygems sources" do
+ expect(source_list.lock_sources).to eq [
+ Bundler::Source::Git.new("uri" => "git://first-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://second-git.org/path.git"),
+ Bundler::Source::Git.new("uri" => "git://third-git.org/path.git"),
+ ASourcePlugin.new("uri" => "https://second-plugin.org/random"),
+ ASourcePlugin.new("uri" => "https://third-bar.org/foo"),
+ Bundler::Source::Path.new("path" => "/first/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/second/path/to/gem"),
+ Bundler::Source::Path.new("path" => "/third/path/to/gem"),
+ Bundler::Source::Rubygems.new,
+ Bundler::Source::Rubygems.new("remotes" => ["https://duplicate-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://first-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://second-rubygems.org"]),
+ Bundler::Source::Rubygems.new("remotes" => ["https://third-rubygems.org"]),
+ ]
+ end
+ end
+
+ describe "replace_sources!" do
+ let(:existing_locked_source) { Bundler::Source::Path.new("path" => "/existing/path") }
+ let(:removed_locked_source) { Bundler::Source::Path.new("path" => "/removed/path") }
+
+ let(:locked_sources) { [existing_locked_source, removed_locked_source] }
+
+ before do
+ @existing_source = source_list.add_path_source("path" => "/existing/path")
+ @new_source = source_list.add_path_source("path" => "/new/path")
+ source_list.replace_sources!(locked_sources)
+ end
+
+ it "maintains the order and number of sources" do
+ expect(source_list.path_sources).to eq [@new_source, @existing_source]
+ end
+
+ it "retains the same instance of the new source" do
+ expect(source_list.path_sources[0]).to be @new_source
+ end
+
+ it "replaces the instance of the existing source" do
+ expect(source_list.path_sources[1]).to be existing_locked_source
+ end
+ end
+
+ describe "#cached!" do
+ let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
+ let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") }
+ let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") }
+
+ it "calls #cached! on all the sources" do
+ expect(rubygems_source).to receive(:cached!)
+ expect(git_source).to receive(:cached!)
+ expect(path_source).to receive(:cached!)
+ source_list.cached!
+ end
+ end
+
+ describe "#remote!" do
+ let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
+ let(:git_source) { source_list.add_git_source("uri" => "git://host/path.git") }
+ let(:path_source) { source_list.add_path_source("path" => "/path/to/gem") }
+
+ it "calls #remote! on all the sources" do
+ expect(rubygems_source).to receive(:remote!)
+ expect(git_source).to receive(:remote!)
+ expect(path_source).to receive(:remote!)
+ source_list.remote!
+ end
+ end
+
+ describe "#clear_cache" do
+ let(:rubygems_source) { source_list.add_rubygems_source("remotes" => ["https://rubygems.org"]) }
+
+ it "calls #clear_cache on all rubygems sources" do
+ expect(rubygems_source).to receive(:clear_cache)
+ expect(source_list.global_rubygems_source).to receive(:clear_cache)
+ source_list.clear_cache
+ end
+ end
+
+ describe "implicit_global_source?" do
+ context "when a global rubygem source provided" do
+ it "returns a falsy value" do
+ source_list.add_global_rubygems_remote("https://rubygems.org")
+
+ expect(source_list.implicit_global_source?).to be_falsey
+ end
+ end
+ context "when no global rubygem source provided" do
+ it "returns a truthy value" do
+ expect(source_list.implicit_global_source?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/source_spec.rb b/spec/bundler/bundler/source_spec.rb
new file mode 100644
index 0000000000..01b57ce9e8
--- /dev/null
+++ b/spec/bundler/bundler/source_spec.rb
@@ -0,0 +1,174 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::Source do
+ class ExampleSource < Bundler::Source
+ end
+
+ subject { ExampleSource.new }
+
+ describe "#unmet_deps" do
+ let(:specs) { double(:specs) }
+ let(:unmet_dependency_names) { double(:unmet_dependency_names) }
+
+ before do
+ allow(subject).to receive(:specs).and_return(specs)
+ allow(specs).to receive(:unmet_dependency_names).and_return(unmet_dependency_names)
+ end
+
+ it "should return the names of unmet dependencies" do
+ expect(subject.unmet_deps).to eq(unmet_dependency_names)
+ end
+ end
+
+ describe "#version_message" do
+ let(:spec) { double(:spec, name: "nokogiri", version: ">= 1.6", platform: Gem::Platform::RUBY) }
+
+ shared_examples_for "the lockfile specs are not relevant" do
+ it "should return a string with the spec name and version" do
+ expect(subject.version_message(spec)).to eq("nokogiri >= 1.6")
+ end
+ end
+
+ context "when there are locked gems" do
+ context "that contain the relevant gem spec" do
+ context "without a version" do
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: nil) }
+
+ it_behaves_like "the lockfile specs are not relevant"
+ end
+
+ context "with the same version" do
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: ">= 1.6") }
+
+ it_behaves_like "the lockfile specs are not relevant"
+ end
+
+ context "with a different version" do
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "< 1.5") }
+
+ context "with color", :no_color_tty do
+ before do
+ allow($stdout).to receive(:tty?).and_return(true)
+ end
+
+ it "should return a string with the spec name and version and locked spec version" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6\e[32m (was < 1.5)\e[0m")
+ end
+ end
+
+ context "without color" do
+ around do |example|
+ with_ui(Bundler::UI::Shell.new("no-color" => true)) do
+ example.run
+ end
+ end
+
+ it "should return a string with the spec name and version and locked spec version" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri >= 1.6 (was < 1.5)")
+ end
+ end
+ end
+
+ context "with a more recent version" do
+ let(:spec) { double(:spec, name: "nokogiri", version: "1.6.1", platform: Gem::Platform::RUBY) }
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") }
+
+ context "with color", :no_color_tty do
+ before do
+ allow($stdout).to receive(:tty?).and_return(true)
+ end
+
+ it "should return a string with the locked spec version in yellow" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1\e[33m (was 1.7.0)\e[0m")
+ end
+ end
+
+ context "without color" do
+ around do |example|
+ with_ui(Bundler::UI::Shell.new("no-color" => true)) do
+ example.run
+ end
+ end
+
+ it "should return a string with the locked spec version in yellow" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.6.1 (was 1.7.0)")
+ end
+ end
+ end
+
+ context "with an older version" do
+ let(:spec) { double(:spec, name: "nokogiri", version: "1.7.1", platform: Gem::Platform::RUBY) }
+ let(:locked_gem) { double(:locked_gem, name: "nokogiri", version: "1.7.0") }
+
+ context "with color", :no_color_tty do
+ before do
+ allow($stdout).to receive(:tty?).and_return(true)
+ end
+
+ it "should return a string with the locked spec version in green" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1\e[32m (was 1.7.0)\e[0m")
+ end
+ end
+
+ context "without color" do
+ around do |example|
+ with_ui(Bundler::UI::Shell.new("no-color" => true)) do
+ example.run
+ end
+ end
+
+ it "should return a string with the locked spec version in yellow" do
+ expect(subject.version_message(spec, locked_gem)).to eq("nokogiri 1.7.1 (was 1.7.0)")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "#can_lock?" do
+ context "when the passed spec's source is equivalent" do
+ let(:spec) { double(:spec, source: subject) }
+
+ it "should return true" do
+ expect(subject.can_lock?(spec)).to be_truthy
+ end
+ end
+
+ context "when the passed spec's source is not equivalent" do
+ let(:spec) { double(:spec, source: double(:other_source)) }
+
+ it "should return false" do
+ expect(subject.can_lock?(spec)).to be_falsey
+ end
+ end
+ end
+
+ describe "#include?" do
+ context "when the passed source is equivalent" do
+ let(:source) { subject }
+
+ it "should return true" do
+ expect(subject).to include(source)
+ end
+ end
+
+ context "when the passed source is not equivalent" do
+ let(:source) { double(:source) }
+
+ it "should return false" do
+ expect(subject).to_not include(source)
+ end
+ end
+ end
+
+ private
+
+ def with_ui(ui)
+ old_ui = Bundler.ui
+ Bundler.ui = ui
+ yield
+ ensure
+ Bundler.ui = old_ui
+ end
+end
diff --git a/spec/bundler/bundler/spec_set_spec.rb b/spec/bundler/bundler/spec_set_spec.rb
new file mode 100644
index 0000000000..1e1ceadf26
--- /dev/null
+++ b/spec/bundler/bundler/spec_set_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::SpecSet do
+ let(:specs) do
+ [
+ build_spec("a", "1.0"),
+ build_spec("b", "1.0"),
+ build_spec("c", "1.1") do |s|
+ s.dep "a", "< 2.0"
+ s.dep "e", "> 0"
+ end,
+ build_spec("d", "2.0") do |s|
+ s.dep "a", "1.0"
+ s.dep "c", "~> 1.0"
+ end,
+ build_spec("e", "1.0.0.pre.1"),
+ ].flatten
+ end
+
+ subject { described_class.new(specs) }
+
+ context "enumerable methods" do
+ it "has a length" do
+ expect(subject.length).to eq(5)
+ end
+
+ it "has a size" do
+ expect(subject.size).to eq(5)
+ end
+ end
+
+ describe "#find_by_name_and_platform" do
+ let(:platform) { Gem::Platform.new("universal-darwin-64") }
+ let(:platform_spec) { build_spec("b", "2.0", platform).first }
+ let(:specs) do
+ [
+ build_spec("a", "1.0"),
+ platform_spec,
+ ].flatten
+ end
+
+ it "finds spec with given name and platform" do
+ spec = described_class.new(specs).find_by_name_and_platform("b", platform)
+ expect(spec).to eq platform_spec
+ end
+
+ it "returns nil when the name is not present" do
+ spec = described_class.new(specs).find_by_name_and_platform("missing", platform)
+ expect(spec).to be_nil
+ end
+
+ it "returns nil when the name exists but no spec is installable on the requested platform" do
+ incompatible_platform = Gem::Platform.new("java")
+ incompatible_spec = build_spec("a", "1.0", incompatible_platform).first
+
+ spec = described_class.new([incompatible_spec]).find_by_name_and_platform("a", platform)
+ expect(spec).to be_nil
+ end
+
+ it "returns the first installable spec for the given name in insertion order" do
+ later_platform_spec = build_spec("b", "3.0", platform).first
+ specs = [
+ platform_spec,
+ later_platform_spec,
+ ]
+
+ spec = described_class.new(specs).find_by_name_and_platform("b", platform)
+ expect(spec).to eq platform_spec
+ end
+ end
+
+ describe "#to_a" do
+ it "returns the specs in order" do
+ expect(subject.to_a.map(&:full_name)).to eq %w[
+ a-1.0
+ b-1.0
+ e-1.0.0.pre.1
+ c-1.1
+ d-2.0
+ ]
+ end
+
+ it "puts rake first when present" do
+ specs = [
+ build_spec("a", "1.0") {|s| s.dep "rake", ">= 0" },
+ build_spec("rake", "13.0"),
+ ].flatten
+
+ expect(described_class.new(specs).to_a.map(&:full_name)).to eq %w[
+ rake-13.0
+ a-1.0
+ ]
+ end
+ end
+
+ describe "#complete_platform" do
+ let(:platform) { Gem::Platform.new("x86_64-linux") }
+
+ let(:platform_variant) do
+ build_spec("needs_old_ruby", "1.0", platform).first.tap do |s|
+ s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}")
+ end
+ end
+
+ let(:lazy_spec) do
+ lazy = Bundler::LazySpecification.new("needs_old_ruby", Gem::Version.new("1.0"), Gem::Platform::RUBY)
+ lazy.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}")
+ source = double("source")
+ source_specs = double("source_specs")
+ allow(source).to receive(:specs).and_return(source_specs)
+ allow(source_specs).to receive(:search).
+ with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant])
+ lazy.source = source
+ lazy
+ end
+
+ it "rejects a platform variant whose strict metadata is incompatible when no override is attached" do
+ set = described_class.new([lazy_spec])
+ expect(set.send(:complete_platform, platform)).to be(false)
+ end
+
+ it "accepts a platform variant when the LazySpec carries an override that allows it" do
+ lazy_spec.overrides = [Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper)]
+ set = described_class.new([lazy_spec])
+ expect(set.send(:complete_platform, platform)).to be(true)
+ end
+
+ it "carries overrides onto a synthesized LazySpec so a follow-up complete_platform still honors them" do
+ override = Bundler::Override.new("needs_old_ruby", :required_ruby_version, :ignore_upper)
+ lazy_spec.overrides = [override]
+ second_platform = Gem::Platform.new("aarch64-linux")
+ second_variant = build_spec("needs_old_ruby", "1.0", second_platform).first.tap do |s|
+ s.required_ruby_version = Gem::Requirement.new("< #{Gem.ruby_version}")
+ end
+ allow(lazy_spec.source.specs).to receive(:search).
+ with(["needs_old_ruby", Gem::Version.new("1.0")]).and_return([platform_variant, second_variant])
+
+ set = described_class.new([lazy_spec])
+ expect(set.send(:complete_platform, platform)).to be(true)
+ # The synthesized x86_64-linux variant is now in the set. If lookup
+ # picks it as exemplar for the next platform check, the override list
+ # must still be reachable via its overrides accessor.
+ synthesized = set.to_a.find {|s| s.platform == platform }
+ expect(synthesized.overrides).to eq([override])
+ expect(set.send(:complete_platform, second_platform)).to be(true)
+ end
+ end
+end
diff --git a/spec/bundler/bundler/specifications/foo.gemspec b/spec/bundler/bundler/specifications/foo.gemspec
new file mode 100644
index 0000000000..19b7724e81
--- /dev/null
+++ b/spec/bundler/bundler/specifications/foo.gemspec
@@ -0,0 +1,13 @@
+# rubocop:disable Style/FrozenStringLiteralComment
+# stub: foo 1.0.0 ruby lib
+
+# The first line would be '# -*- encoding: utf-8 -*-' in a real stub gemspec
+
+Gem::Specification.new do |s|
+ s.name = "foo"
+ s.version = "1.0.0"
+ s.loaded_from = __FILE__
+ s.extensions = "ext/foo"
+ s.required_ruby_version = ">= 3.2.0"
+end
+# rubocop:enable Style/FrozenStringLiteralComment
diff --git a/spec/bundler/bundler/stub_specification_spec.rb b/spec/bundler/bundler/stub_specification_spec.rb
new file mode 100644
index 0000000000..f2faa2ea64
--- /dev/null
+++ b/spec/bundler/bundler/stub_specification_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::StubSpecification do
+ let(:with_bundler_stub_spec) do
+ gemspec = Gem::Specification.new do |s|
+ s.name = "gemname"
+ s.version = "1.0.0"
+ s.loaded_from = __FILE__
+ s.extensions = "ext/gemname"
+ end
+
+ described_class.from_stub(gemspec)
+ end
+
+ describe "#from_stub" do
+ it "returns the same stub if already a Bundler::StubSpecification" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ expect(stub).to be(with_bundler_stub_spec)
+ end
+ end
+
+ describe "#gem_build_complete_path" do
+ it "StubSpecification should have equal gem_build_complete_path as Specification" do
+ spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec")
+ spec = Gem::Specification.load(spec_path)
+ gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","")
+
+ stub = described_class.from_stub(gem_stub)
+ expect(stub.gem_build_complete_path).to eq spec.gem_build_complete_path
+ end
+ end
+
+ describe "#manually_installed?" do
+ it "returns true if installed_by_version is nil or 0" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ expect(stub.manually_installed?).to be true
+ end
+
+ it "returns false if installed_by_version is greater than 0" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ stub.installed_by_version = Gem::Version.new(1)
+ expect(stub.manually_installed?).to be false
+ end
+ end
+
+ describe "#missing_extensions?" do
+ it "returns false if manually_installed?" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ expect(stub.missing_extensions?).to be false
+ end
+
+ it "returns #{RUBY_ENGINE == "jruby" ? "false" : "true"} if not manually_installed?" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+ stub.installed_by_version = Gem::Version.new(1)
+ if RUBY_ENGINE == "jruby"
+ expect(stub.missing_extensions?).to be false
+ else
+ expect(stub.missing_extensions?).to be true
+ end
+ end
+ end
+
+ describe "#activated?" do
+ it "returns true after activation" do
+ stub = described_class.from_stub(with_bundler_stub_spec)
+
+ expect(stub.activated?).to be_falsey
+ stub.activated = true
+ expect(stub.activated?).to be true
+ end
+
+ it "returns true after activation if the underlying stub is a `Gem::StubSpecification`" do
+ spec_path = File.join(File.dirname(__FILE__), "specifications", "foo.gemspec")
+ gem_stub = Gem::StubSpecification.new(spec_path, File.dirname(__FILE__),"","")
+ stub = described_class.from_stub(gem_stub)
+
+ expect(stub.activated?).to be_falsey
+ stub.activated = true
+ expect(stub.activated?).to be true
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ui/shell_spec.rb b/spec/bundler/bundler/ui/shell_spec.rb
new file mode 100644
index 0000000000..83f147191e
--- /dev/null
+++ b/spec/bundler/bundler/ui/shell_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::UI::Shell do
+ subject { described_class.new }
+
+ before { subject.level = "debug" }
+
+ describe "#info" do
+ before { subject.level = "info" }
+ it "prints to stdout" do
+ expect { subject.info("info") }.to output("info\n").to_stdout
+ end
+
+ context "when output_stream is :stderr" do
+ before { subject.output_stream = :stderr }
+ it "prints to stderr" do
+ expect { subject.info("info") }.to output("info\n").to_stderr
+ end
+ end
+ end
+
+ describe "#confirm" do
+ before { subject.level = "confirm" }
+ it "prints to stdout" do
+ expect { subject.confirm("confirm") }.to output("confirm\n").to_stdout
+ end
+
+ context "when output_stream is :stderr" do
+ before { subject.output_stream = :stderr }
+ it "prints to stderr" do
+ expect { subject.confirm("confirm") }.to output("confirm\n").to_stderr
+ end
+ end
+ end
+
+ describe "#warn" do
+ before { subject.level = "warn" }
+ it "prints to stderr, implicitly adding a newline" do
+ expect { subject.warn("warning") }.to output("warning\n").to_stderr
+ end
+ it "can be told not to emit a newline" do
+ expect { subject.warn("warning", false) }.to output("warning").to_stderr
+ end
+ end
+
+ describe "#debug" do
+ it "prints to stdout" do
+ expect { subject.debug("debug") }.to output("debug\n").to_stdout
+ end
+
+ context "when output_stream is :stderr" do
+ before { subject.output_stream = :stderr }
+ it "prints to stderr" do
+ expect { subject.debug("debug") }.to output("debug\n").to_stderr
+ end
+ end
+ end
+
+ describe "#error" do
+ before { subject.level = "error" }
+
+ it "prints to stderr" do
+ expect { subject.error("error!!!") }.to output("error!!!\n").to_stderr
+ end
+
+ context "when stderr is closed" do
+ it "doesn't report anything" do
+ output = begin
+ result = StringIO.new
+ result.close
+
+ $stderr = result
+
+ subject.error("Something went wrong")
+
+ result.string
+ ensure
+ $stderr = STDERR
+ end
+ expect(output).to_not eq("Something went wrong")
+ end
+ end
+ end
+
+ describe "threads" do
+ it "is thread safe when using with_level" do
+ stop_thr1 = false
+ stop_thr2 = false
+
+ expect(subject.level).to eq("debug")
+
+ thr1 = Thread.new do
+ subject.silence do
+ sleep(0.1) until stop_thr1
+ end
+
+ stop_thr2 = true
+ end
+
+ thr2 = Thread.new do
+ subject.silence do
+ stop_thr1 = true
+ sleep(0.1) until stop_thr2
+ end
+ end
+
+ [thr1, thr2].each(&:join)
+
+ expect(subject.level).to eq("debug")
+ end
+ end
+end
diff --git a/spec/bundler/bundler/ui_spec.rb b/spec/bundler/bundler/ui_spec.rb
new file mode 100644
index 0000000000..6df0d2e290
--- /dev/null
+++ b/spec/bundler/bundler/ui_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::UI do
+ describe Bundler::UI::Silent do
+ it "has the same instance methods as Shell" do
+ shell = Bundler::UI::Shell
+ methods = proc do |cls|
+ cls.instance_methods.map do |i|
+ m = shell.instance_method(i)
+ [i, m.parameters]
+ end.sort_by(&:first)
+ end
+ expect(methods.call(described_class)).to eq(methods.call(shell))
+ end
+
+ it "has the same instance class as Shell" do
+ shell = Bundler::UI::Shell
+ methods = proc do |cls|
+ cls.methods.map do |i|
+ m = shell.method(i)
+ [i, m.parameters]
+ end.sort_by(&:first)
+ end
+ expect(methods.call(described_class)).to eq(methods.call(shell))
+ end
+ end
+
+ describe Bundler::UI::Shell do
+ let(:options) { {} }
+ subject { described_class.new(options) }
+ describe "debug?" do
+ it "returns a boolean" do
+ subject.level = :debug
+ expect(subject.debug?).to eq(true)
+
+ subject.level = :error
+ expect(subject.debug?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/uri_credentials_filter_spec.rb b/spec/bundler/bundler/uri_credentials_filter_spec.rb
new file mode 100644
index 0000000000..641f0addb4
--- /dev/null
+++ b/spec/bundler/bundler/uri_credentials_filter_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::URICredentialsFilter do
+ subject { described_class }
+
+ describe "#credential_filtered_uri" do
+ shared_examples_for "original type of uri is maintained" do
+ it "maintains same type for return value as uri input type" do
+ expect(subject.credential_filtered_uri(uri)).to be_kind_of(uri.class)
+ end
+ end
+
+ shared_examples_for "sensitive credentials in uri are filtered out" do
+ context "authentication using oauth credentials" do
+ context "specified via 'x-oauth-basic'" do
+ let(:credentials) { "oauth_token:x-oauth-basic@" }
+
+ it "returns the uri without the oauth token" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x-oauth-basic@github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "specified via 'x'" do
+ let(:credentials) { "oauth_token:x@" }
+
+ it "returns the uri without the oauth token" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://x@github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "specified without empty username" do
+ let(:credentials) { "oauth_token@" }
+
+ it "returns the uri without the oauth token" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+ end
+
+ context "authentication using login credentials" do
+ let(:credentials) { "username1:hunter3@" }
+
+ it "returns the uri without the password" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(Gem::URI("https://username1@github.com/company/private-repo").to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "authentication without credentials" do
+ let(:credentials) { "" }
+
+ it "returns the same uri" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+ end
+
+ context "uri is a uri object" do
+ let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") }
+
+ it_behaves_like "sensitive credentials in uri are filtered out"
+ end
+
+ context "uri is a uri string" do
+ let(:uri) { "https://#{credentials}github.com/company/private-repo" }
+
+ it_behaves_like "sensitive credentials in uri are filtered out"
+ end
+
+ context "uri is a non-uri format string (ex. path)" do
+ let(:uri) { "/path/to/repo" }
+
+ it "returns the same uri" do
+ expect(subject.credential_filtered_uri(uri).to_s).to eq(uri.to_s)
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+
+ context "uri is nil" do
+ let(:uri) { nil }
+
+ it "returns nil" do
+ expect(subject.credential_filtered_uri(uri)).to be_nil
+ end
+
+ it_behaves_like "original type of uri is maintained"
+ end
+ end
+
+ describe "#credential_filtered_string" do
+ let(:str_to_filter) { "This is a git message containing a uri #{uri}!" }
+ let(:credentials) { "" }
+ let(:uri) { Gem::URI("https://#{credentials}github.com/company/private-repo") }
+
+ context "with a uri that contains credentials" do
+ let(:credentials) { "oauth_token:x-oauth-basic@" }
+
+ it "returns the string without the sensitive credentials" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(
+ "This is a git message containing a uri https://x-oauth-basic@github.com/company/private-repo!"
+ )
+ end
+ end
+
+ context "that does not contains credentials" do
+ it "returns the same string" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter)
+ end
+ end
+
+ context "string to filter is nil" do
+ let(:str_to_filter) { nil }
+
+ it "returns nil" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to be_nil
+ end
+ end
+
+ context "uri to filter out is nil" do
+ let(:uri) { nil }
+
+ it "returns the same string" do
+ expect(subject.credential_filtered_string(str_to_filter, uri)).to eq(str_to_filter)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/uri_normalizer_spec.rb b/spec/bundler/bundler/uri_normalizer_spec.rb
new file mode 100644
index 0000000000..1308e86014
--- /dev/null
+++ b/spec/bundler/bundler/uri_normalizer_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe Bundler::URINormalizer do
+ describe ".normalize_suffix" do
+ context "when trailing_slash is true" do
+ it "adds a trailing slash when missing" do
+ expect(described_class.normalize_suffix("https://example.com", trailing_slash: true)).to eq("https://example.com/")
+ end
+
+ it "keeps the trailing slash when present" do
+ expect(described_class.normalize_suffix("https://example.com/", trailing_slash: true)).to eq("https://example.com/")
+ end
+ end
+
+ context "when trailing_slash is false" do
+ it "removes a trailing slash when present" do
+ expect(described_class.normalize_suffix("https://example.com/", trailing_slash: false)).to eq("https://example.com")
+ end
+
+ it "keeps the value unchanged when no trailing slash exists" do
+ expect(described_class.normalize_suffix("https://example.com", trailing_slash: false)).to eq("https://example.com")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/worker_spec.rb b/spec/bundler/bundler/worker_spec.rb
new file mode 100644
index 0000000000..2ad2845e37
--- /dev/null
+++ b/spec/bundler/bundler/worker_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require "bundler/worker"
+
+RSpec.describe Bundler::Worker do
+ let(:size) { 5 }
+ let(:name) { "Spec Worker" }
+ let(:function) { proc {|object, worker_number| [object, worker_number] } }
+ subject { described_class.new(size, name, function) }
+
+ after { subject.stop }
+
+ describe "#initialize" do
+ context "when Thread.start raises ThreadError" do
+ it "raises when no threads can be created" do
+ allow(Thread).to receive(:start).and_raise(ThreadError, "error creating thread")
+
+ expect { subject.enq "a" }.to raise_error(Bundler::ThreadCreationError, "Failed to create threads for the Spec Worker worker: error creating thread")
+ end
+ end
+ end
+
+ describe "priority queue" do
+ it "process elements from the priority queue first" do
+ processed_elements = []
+
+ function = proc do |element, _|
+ processed_elements << element
+ end
+
+ worker = described_class.new(1, "Spec Worker", function)
+ worker.instance_variable_set(:@threads, []) # Prevent the enqueueing from starting work.
+ worker.enq("Normal element")
+ worker.enq("Priority element", priority: true)
+ worker.send(:create_threads)
+
+ worker.stop
+
+ expect(processed_elements).to eq(["Priority element", "Normal element"])
+ end
+ end
+
+ describe "handling interrupts" do
+ let(:status) do
+ pid = Process.fork do
+ $stderr.reopen File.new("/dev/null", "w")
+ Signal.trap "INT", previous_interrupt_handler
+ subject.enq "a"
+ subject.stop unless interrupt_before_stopping
+ Process.kill "INT", Process.pid
+ end
+
+ Process.wait2(pid).last
+ end
+
+ before do
+ skip "requires Process.fork" unless Process.respond_to?(:fork)
+ end
+
+ context "when interrupted before stopping" do
+ let(:interrupt_before_stopping) { true }
+ let(:previous_interrupt_handler) { ->(*) { exit 0 } }
+
+ it "aborts" do
+ expect(status.exitstatus).to eq(1)
+ end
+ end
+
+ context "when interrupted after stopping" do
+ let(:interrupt_before_stopping) { false }
+
+ context "when the previous interrupt handler was the default" do
+ let(:previous_interrupt_handler) { "DEFAULT" }
+
+ it "uses the default interrupt handler" do
+ expect(status).to be_signaled
+ end
+ end
+
+ context "when the previous interrupt handler was customized" do
+ let(:previous_interrupt_handler) { ->(*) { exit 42 } }
+
+ it "restores the custom interrupt handler after stopping" do
+ expect(status.exitstatus).to eq(42)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/bundler/yaml_serializer_spec.rb b/spec/bundler/bundler/yaml_serializer_spec.rb
new file mode 100644
index 0000000000..9ff1579b76
--- /dev/null
+++ b/spec/bundler/bundler/yaml_serializer_spec.rb
@@ -0,0 +1,235 @@
+# frozen_string_literal: true
+
+require "bundler/yaml_serializer"
+
+RSpec.describe Bundler::YAMLSerializer do
+ subject(:serializer) { Bundler::YAMLSerializer }
+
+ describe "#dump" do
+ it "works for simple hash" do
+ hash = { "Q" => "Where does Thursday come before Wednesday? In the dictionary. :P" }
+
+ expected = <<~YAML
+ ---
+ Q: "Where does Thursday come before Wednesday? In the dictionary. :P"
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+
+ it "handles nested hash" do
+ hash = {
+ "nice-one" => {
+ "read_ahead" => "All generalizations are false, including this one",
+ },
+ }
+
+ expected = <<~YAML
+ ---
+ nice-one:
+ read_ahead: "All generalizations are false, including this one"
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+
+ it "array inside an hash" do
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Jack and Jill went up the hill",
+ "To fetch a pail of water.",
+ "Jack fell down and broke his crown,",
+ "And Jill came tumbling after.",
+ ],
+ },
+ }
+
+ expected = <<~YAML
+ ---
+ nested_hash:
+ contains_array:
+ - "Jack and Jill went up the hill"
+ - "To fetch a pail of water."
+ - "Jack fell down and broke his crown,"
+ - "And Jill came tumbling after."
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+
+ it "handles empty array" do
+ hash = {
+ "empty_array" => [],
+ }
+
+ expected = <<~YAML
+ ---
+ empty_array: []
+ YAML
+
+ expect(serializer.dump(hash)).to eq(expected)
+ end
+ end
+
+ describe "#load" do
+ it "works for simple hash" do
+ yaml = <<~YAML
+ ---
+ Jon: "Air is free dude!"
+ Jack: "Yes.. until you buy a bag of chips!"
+ YAML
+
+ hash = {
+ "Jon" => "Air is free dude!",
+ "Jack" => "Yes.. until you buy a bag of chips!",
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "works for nested hash" do
+ yaml = <<~YAML
+ ---
+ baa:
+ baa: "black sheep"
+ have: "you any wool?"
+ yes: "merry have I"
+ three: "bags full"
+ YAML
+
+ hash = {
+ "baa" => {
+ "baa" => "black sheep",
+ "have" => "you any wool?",
+ "yes" => "merry have I",
+ },
+ "three" => "bags full",
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "handles colon in key/value" do
+ yaml = <<~YAML
+ BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/: http://example-mirror.rubygems.org
+ YAML
+
+ expect(serializer.load(yaml)).to eq("BUNDLE_MIRROR__HTTPS://RUBYGEMS__ORG/" => "http://example-mirror.rubygems.org")
+ end
+
+ it "handles arrays inside hashes" do
+ yaml = <<~YAML
+ ---
+ nested_hash:
+ contains_array:
+ - "Why shouldn't you write with a broken pencil?"
+ - "Because it's pointless!"
+ YAML
+
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Why shouldn't you write with a broken pencil?",
+ "Because it's pointless!",
+ ],
+ },
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "handles windows-style CRLF line endings" do
+ yaml = <<~YAML.gsub("\n", "\r\n")
+ ---
+ nested_hash:
+ contains_array:
+ - "Why shouldn't you write with a broken pencil?"
+ - "Because it's pointless!"
+ - oh so silly
+ YAML
+
+ hash = {
+ "nested_hash" => {
+ "contains_array" => [
+ "Why shouldn't you write with a broken pencil?",
+ "Because it's pointless!",
+ "oh so silly",
+ ],
+ },
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "handles empty array" do
+ yaml = <<~YAML
+ ---
+ empty_array: []
+ YAML
+
+ hash = {
+ "empty_array" => [],
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+
+ it "skip commented out words" do
+ yaml = <<~YAML
+ ---
+ foo: bar
+ buzz: foo # bar
+ YAML
+
+ hash = {
+ "foo" => "bar",
+ "buzz" => "foo",
+ }
+
+ expect(serializer.load(yaml)).to eq(hash)
+ end
+ end
+
+ describe "against yaml lib" do
+ let(:hash) do
+ {
+ "a_joke" => {
+ "my-stand" => "I can totally keep secrets",
+ "but" => "The people I tell them to can't :P",
+ "wouldn't it be funny if this string were empty?" => "",
+ },
+ "more" => {
+ "first" => [
+ "Can a kangaroo jump higher than a house?",
+ "Of course, a house doesn't jump at all.",
+ ],
+ "second" => [
+ "What did the sea say to the sand?",
+ "Nothing, it simply waved.",
+ ],
+ "array with empty string" => [""],
+ },
+ "sales" => {
+ "item" => "A Parachute",
+ "description" => "Only used once, never opened.",
+ },
+ "one-more" => "I'd tell you a chemistry joke but I know I wouldn't get a reaction.",
+ }
+ end
+
+ context "#load" do
+ it "retrieves the original hash" do
+ require "yaml"
+ expect(serializer.load(YAML.dump(hash))).to eq(hash)
+ end
+ end
+
+ context "#dump" do
+ it "retrieves the original hash" do
+ require "yaml"
+ expect(YAML.load(serializer.dump(hash))).to eq(hash)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/cache/cache_path_spec.rb b/spec/bundler/cache/cache_path_spec.rb
new file mode 100644
index 0000000000..2a280ea858
--- /dev/null
+++ b/spec/bundler/cache/cache_path_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle package" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "with --cache-path" do
+ it "caches gems at given path" do
+ bundle :cache, "cache-path" => "vendor/cache-foo"
+ expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "with config cache_path" do
+ it "caches gems at given path" do
+ bundle_config "cache_path vendor/cache-foo"
+ bundle :cache
+ expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "with absolute --cache-path" do
+ it "caches gems at given path" do
+ bundle :cache, "cache-path" => bundled_app("vendor/cache-foo")
+ expect(bundled_app("vendor/cache-foo/myrack-1.0.0.gem")).to exist
+ end
+ end
+end
diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb
new file mode 100644
index 0000000000..198279d84c
--- /dev/null
+++ b/spec/bundler/cache/gems_spec.rb
@@ -0,0 +1,427 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache" do
+ shared_examples_for "when there are only gemsources" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ system_gems "myrack-1.0.0", path: path
+ bundle :cache
+ end
+
+ it "copies the .gem file to vendor/cache" do
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "uses the cache as a source when installing gems" do
+ build_gem "omg", path: bundled_app("vendor/cache")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "omg"
+ G
+
+ expect(the_bundle).to include_gems "omg 1.0.0"
+ end
+
+ it "uses the cache as a source when installing gems with --local" do
+ system_gems [], path: default_bundle_path
+ bundle "install --local"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "does not reinstall gems from the cache if they exist on the system" do
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "does not reinstall gems from the cache if they exist in the bundle" do
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ bundle :install, local: true
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "creates a lockfile" do
+ cache_gems "myrack-1.0.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache"
+
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ context "using system gems" do
+ before { bundle_config "path.system true" }
+ let(:path) { system_gem_path }
+ it_behaves_like "when there are only gemsources"
+ end
+
+ context "installing into a local path" do
+ before { bundle_config "path ./.bundle" }
+ let(:path) { local_gem_path }
+ it_behaves_like "when there are only gemsources"
+ end
+
+ describe "when there is a built-in gem", :ruby_repo do
+ let(:default_json_version) { ruby "gem 'json'; require 'json'; puts JSON::VERSION" }
+
+ before :each do
+ build_gem "json", default_json_version, to_system: true, default: true
+ end
+
+ context "when a remote gem is available for caching" do
+ before do
+ build_repo2 do
+ build_gem "json", default_json_version
+ end
+ end
+
+ it "uses remote gems when installing" do
+ install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true
+ expect(out).to include("Installing json #{default_json_version}")
+ end
+
+ it "does not use remote gems when installing with --local flag" do
+ install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true, local: true
+ expect(out).to include("Using json #{default_json_version}")
+ end
+
+ it "does not use remote gems when installing with --prefer-local flag" do
+ install_gemfile %(source "https://gem.repo2"; gem 'json', '#{default_json_version}'), verbose: true, "prefer-local": true
+ expect(out).to include("Using json #{default_json_version}")
+ end
+
+ it "caches remote and builtin gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'json', '#{default_json_version}'
+ gem 'myrack', '1.0.0'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/json-#{default_json_version}.gem")).to exist
+ end
+
+ it "caches builtin gems when cache_all_platforms is set" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "json"
+ G
+
+ bundle_config "cache_all_platforms true"
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/json-#{default_json_version}.gem")).to exist
+ end
+
+ it "doesn't make remote request after caching the gem" do
+ build_gem "builtin_gem_2", "1.0.2", path: bundled_app("vendor/cache"), default: true
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'builtin_gem_2', '1.0.2'
+ G
+
+ bundle "install --local"
+ expect(the_bundle).to include_gems("builtin_gem_2 1.0.2")
+ end
+ end
+
+ context "when a remote gem is not available for caching" do
+ it "warns, but uses builtin gems when installing to system gems" do
+ bundle_config "path.system true"
+ install_gemfile %(source "https://gem.repo1"; gem 'json', '#{default_json_version}'), verbose: true
+ expect(err).to include("json-#{default_json_version} is built in to Ruby, and can't be cached")
+ expect(out).to include("Using json #{default_json_version}")
+ end
+
+ it "errors when explicitly caching" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'json', '#{default_json_version}'
+ G
+
+ bundle :cache, raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("json-#{default_json_version} is built in to Ruby, and can't be cached")
+ end
+ end
+ end
+
+ describe "when there are also git sources" do
+ before do
+ build_git "foo"
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ gem 'myrack'
+ G
+ end
+
+ it "still works" do
+ bundle :cache
+
+ system_gems []
+ bundle "install --local"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0", "foo 1.0")
+ end
+
+ it "should not explode if the lockfile is not present" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle :cache
+
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ describe "when previously cached" do
+ let :setup_main_repo do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "actionpack"
+ G
+ bundle :cache
+ expect(cached_gem("myrack-1.0.0")).to exist
+ expect(cached_gem("actionpack-2.3.2")).to exist
+ expect(cached_gem("activesupport-2.3.2")).to exist
+ end
+
+ it "re-caches during install" do
+ setup_main_repo
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ bundle :install
+ expect(out).to include("Updating files in vendor/cache")
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "adds and removes when gems are updated" do
+ setup_main_repo
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ bundle "update", all: true
+ expect(cached_gem("myrack-1.2")).to exist
+ expect(cached_gem("myrack-1.0.0")).not_to exist
+ end
+
+ it "adds new gems and dependencies" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ G
+ expect(cached_gem("rails-2.3.2")).to exist
+ expect(cached_gem("activerecord-2.3.2")).to exist
+ end
+
+ it "removes .gems for removed gems and dependencies" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ expect(cached_gem("myrack-1.0.0")).to exist
+ expect(cached_gem("actionpack-2.3.2")).not_to exist
+ expect(cached_gem("activesupport-2.3.2")).not_to exist
+ end
+
+ it "removes .gems when gem changes to git source" do
+ setup_main_repo
+ build_git "myrack"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", :git => "#{lib_path("myrack-1.0")}"
+ gem "actionpack"
+ G
+ expect(cached_gem("myrack-1.0.0")).not_to exist
+ expect(cached_gem("actionpack-2.3.2")).to exist
+ expect(cached_gem("activesupport-2.3.2")).to exist
+ end
+
+ it "doesn't remove gems that are for another platform" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ bundle :cache
+ expect(cached_gem("platform_specific-1.0-java")).to exist
+ end
+
+ pristine_system_gems
+
+ simulate_platform "x86-darwin-100" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(cached_gem("platform_specific-1.0-x86-darwin-100")).to exist
+ expect(cached_gem("platform_specific-1.0-java")).to exist
+ end
+ end
+
+ it "doesn't remove gems cached gems that don't match their remote counterparts, but also refuses to install and prints an error" do
+ setup_main_repo
+ cached_myrack = cached_gem("myrack-1.0.0")
+ FileUtils.rm_rf cached_myrack
+ build_gem "myrack", "1.0.0",
+ path: cached_myrack.parent,
+ rubygems_version: "1.3.2"
+
+ FileUtils.rm_r default_bundle_path
+ default_system_gems
+
+ FileUtils.rm bundled_app_lock
+ bundle :install, raise_on_error: false
+
+ expect(err).to eq <<~E.strip
+ Bundler found mismatched checksums. This is a potential security risk.
+ #{checksum_to_lock(gem_repo2, "myrack", "1.0.0")}
+ from the API at https://gem.repo2/
+ #{checksum_from_package(cached_myrack, "myrack", "1.0.0")}
+ from the gem at #{cached_myrack}
+
+ If you trust the API at https://gem.repo2/, to resolve this issue you can:
+ 1. remove the gem at #{cached_myrack}
+ 2. run `bundle install`
+
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ E
+
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "raises an error when a cached gem is altered and produces a different checksum than the remote gem" do
+ setup_main_repo
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache")
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ FileUtils.rm_r default_bundle_path
+ default_system_gems
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ #{checksums}
+ L
+
+ bundle :install, raise_on_error: false
+ expect(exitstatus).to eq(37)
+ expect(err).to include("Bundler found mismatched checksums.")
+ expect(err).to include("1. remove the gem at #{cached_gem("myrack-1.0.0")}")
+
+ expect(cached_gem("myrack-1.0.0")).to exist
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ bundle :install
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "installs a modified gem with a non-matching checksum when the API implementation does not provide checksums" do
+ setup_main_repo
+ FileUtils.rm_rf cached_gem("myrack-1.0.0")
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache")
+ pristine_system_gems
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ L
+
+ bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(cached_gem("myrack-1.0.0")).to exist
+ end
+
+ it "handles directories and non .gem files in the cache" do
+ setup_main_repo
+ bundled_app("vendor/cache/foo").mkdir
+ File.open(bundled_app("vendor/cache/bar"), "w") {|f| f.write("not a gem") }
+ bundle :cache
+ end
+
+ it "does not say that it is removing gems when it isn't actually doing so" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle "cache"
+ bundle "install"
+ expect(out).not_to match(/removing/i)
+ end
+
+ it "does not warn about all if it doesn't have any git/path dependency" do
+ setup_main_repo
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle "cache"
+ expect(out).not_to match(/\-\-all/)
+ end
+
+ it "should install gems with the name bundler in them (that aren't bundler)" do
+ build_gem "foo-bundler", "1.0",
+ path: bundled_app("vendor/cache")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo-bundler"
+ G
+
+ expect(the_bundle).to include_gems "foo-bundler 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/cache/git_spec.rb b/spec/bundler/cache/git_spec.rb
new file mode 100644
index 0000000000..f0976ecac7
--- /dev/null
+++ b/spec/bundler/cache/git_spec.rb
@@ -0,0 +1,511 @@
+# frozen_string_literal: true
+
+RSpec.describe "git base name" do
+ it "base_name should strip private repo uris" do
+ source = Bundler::Source::Git.new("uri" => "git@github.com:bundler.git")
+ expect(source.send(:base_name)).to eq("bundler")
+ end
+
+ it "base_name should strip network share paths" do
+ source = Bundler::Source::Git.new("uri" => "//MachineName/ShareFolder")
+ expect(source.send(:base_name)).to eq("ShareFolder")
+ end
+end
+
+RSpec.describe "bundle cache with git" do
+ it "does not copy repository to vendor cache when cache_all set to false" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle_config "cache_all false"
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).not_to exist
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies repository to vendor cache and uses it" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies repository to vendor cache and uses it even when configured with `path`" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "runs twice without exploding" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ bundle :cache
+
+ expect(out).to include "Updating files in vendor/cache"
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "tracks updates" do
+ git = build_git "foo"
+ old_ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "puts :CACHE"
+ end
+
+ ref = git.ref_for("main", 11)
+ expect(ref).not_to eq(old_ref)
+
+ bundle "update", all: true
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ run "require 'foo'"
+ expect(out).to eq("CACHE")
+ end
+
+ it "tracks updates when specifying the gem" do
+ git = build_git "foo"
+ old_ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "puts :CACHE"
+ end
+
+ ref = git.ref_for("main", 11)
+ expect(ref).not_to eq(old_ref)
+
+ bundle "update foo"
+
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{old_ref}")).not_to exist
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ run "require 'foo'"
+ expect(out).to eq("CACHE")
+ end
+
+ it "uses the local repository to generate the cache" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-invalid")}', :branch => :main
+ G
+
+ bundle %(config set local.foo #{lib_path("foo-1.0")})
+ bundle "install"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-invalid-#{ref}")).to exist
+
+ # Updating the local still uses the local.
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "puts :LOCAL"
+ end
+
+ run "require 'foo'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "can use gems after copying install folder to a different machine with git not installed" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ pristine_system_gems
+ with_path_as "" do
+ bundle_config "deployment true"
+ bundle "install --local"
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+ end
+
+ it "can install after bundle cache without cloning remote repositories" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true
+
+ pristine_system_gems
+ bundle_config "frozen true"
+ bundle "install --local --verbose"
+ expect(out).to_not include("Fetching")
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "can install after bundle cache without cloning remote repositories even without the original cache" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true
+
+ pristine_system_gems
+ bundle_config "frozen true"
+ bundle "install --local --verbose"
+ expect(out).to_not include("Fetching")
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "can install after bundle cache without cloning remote repositories with only git tracked files" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true
+
+ pristine_system_gems
+ bundle_config "frozen true"
+
+ # Remove untracked files (including the empty refs dir in the cache)
+ Dir.chdir(bundled_app) do
+ system(*%W[git init --quiet])
+ system(*%W[git add --all])
+ system(*%W[git clean -d --force --quiet])
+ end
+
+ bundle "install --local --verbose"
+ expect(out).to_not include("Fetching")
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "installs properly a bundler 2.5.17-2.5.23 cache as a bare repository without cloning remote repositories" do
+ git = build_git "foo"
+
+ short_ref = git.ref_for("main", 11)
+ cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "global_gem_cache false"
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ # Simulate old cache by copying the real cache folder to vendor/cache
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir
+ FileUtils.rm_r bundled_app("vendor/bundle")
+
+ bundle "install --local --verbose"
+ expect(err).to include("Installing from cache in old \"bare repository\" format for compatibility")
+
+ expect(out).to_not include("Fetching")
+
+ # leaves old cache alone
+ expect(cache_dir.join("lib/foo.rb")).not_to exist
+ expect(cache_dir.join("HEAD")).to exist
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "migrates a bundler 2.5.17-2.5.23 cache as a bare repository when not running with --local" do
+ git = build_git "foo"
+
+ short_ref = git.ref_for("main", 11)
+ cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "global_gem_cache false"
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ # Simulate old cache by copying the real cache folder to vendor/cache
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir
+ FileUtils.rm_r bundled_app("vendor/bundle")
+
+ bundle "install --verbose"
+ expect(out).to include("Fetching")
+
+ # migrates old cache alone
+ expect(cache_dir.join("lib/foo.rb")).to exist
+ expect(cache_dir.join("HEAD")).not_to exist
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "migrates a bundler 2.5.17-2.5.23 cache as a bare repository when running `bundle cache`, even if gems already installed" do
+ git = build_git "foo"
+
+ short_ref = git.ref_for("main", 11)
+ cache_dir = bundled_app("vendor/cache/foo-1.0-#{short_ref}")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "global_gem_cache false"
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ # Simulate old cache by copying the real cache folder to vendor/cache
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r "#{Dir.glob(vendored_gems("cache/bundler/git/foo-1.0-*")).first}/.", cache_dir
+
+ bundle "cache"
+
+ # migrates old cache alone
+ expect(cache_dir.join("lib/foo.rb")).to exist
+ expect(cache_dir.join("HEAD")).not_to exist
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "copies repository to vendor cache, including submodules" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+
+ git = build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ ref = git.ref_for("main", 11)
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/has_submodule-1.0-#{ref}/submodule-1.0")).to exist
+ expect(the_bundle).to include_gems "has_submodule 1.0"
+ end
+
+ it "caches pre-evaluated gemspecs" do
+ git = build_git "foo"
+
+ # Insert a gemspec method that shells out
+ spec_lines = lib_path("foo-1.0/foo.gemspec").read.split("\n")
+ spec_lines.insert(-2, "s.description = `echo bob`")
+ update_git("foo") {|s| s.write "foo.gemspec", spec_lines.join("\n") }
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache
+
+ ref = git.ref_for("main", 11)
+ gemspec = bundled_app("vendor/cache/foo-1.0-#{ref}/foo.gemspec").read
+ expect(gemspec).to_not match("`echo bob`")
+ end
+
+ it "can install after bundle cache with git not installed" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :cache, "all-platforms" => true, :install => false
+
+ pristine_system_gems
+ with_path_as "" do
+ bundle_config "deployment true"
+ bundle :install, local: true
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+ end
+
+ it "can install after bundle cache generated with an older Bundler that kept checkouts in the cache" do
+ git = build_git("foo")
+ locked_revision = git.ref_for("main")
+ path_revision = git.ref_for("main", 11)
+
+ git_path = lib_path("foo-1.0")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{git_path}'
+ G
+ lockfile <<~L
+ GIT
+ remote: #{git_path}/
+ revision: #{locked_revision}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ # Simulate an old incorrect situation where vendor/cache would be the install location of git gems
+ FileUtils.mkdir_p bundled_app("vendor/cache")
+ FileUtils.cp_r git_path, bundled_app("vendor/cache/foo-1.0-#{path_revision}")
+ FileUtils.rm_r bundled_app("vendor/cache/foo-1.0-#{path_revision}/.git")
+
+ bundle :install, env: { "BUNDLE_DEPLOYMENT" => "true", "BUNDLE_CACHE_ALL" => "true" }
+ end
+
+ it "respects the --no-install flag" do
+ git = build_git "foo", &:add_c_extension
+ ref = git.ref_for("main", 11)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ # The algorithm for the cache location for a git checkout is
+ # in Bundle::Source::Git#cache_path
+ cache_path_name = "foo-1.0-#{Digest(:SHA1).hexdigest(lib_path("foo-1.0").to_s)}"
+
+ # Run this test twice. This is because materially different codepaths
+ # will get hit the second time around.
+ # The first time, Bundler::Sources::Git#install_path is set to the system
+ # wide cache directory bundler/gems; the second time, it's set to the
+ # vendor/cache directory. We don't want the native extension to appear in
+ # either of these places, so run the `bundle cache` command twice.
+ 2.times do
+ bundle :cache, "all-platforms" => true, :install => false
+
+ # it did _NOT_ actually install the gem - neither in $GEM_HOME (bundler 2 mode),
+ # nor in .bundle (bundler 4 mode)
+ expect(Pathname.new(File.join(default_bundle_path, "gems/foo-1.0-#{ref}"))).to_not exist
+ # it _did_ cache the gem in vendor/
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ # it did _NOT_ build the gems extensions in the vendor/ dir
+ expect(Dir[bundled_app("vendor/cache/foo-1.0-#{ref}/lib/foo_c*")]).to be_empty
+ # it _did_ cache the git checkout
+ expect(default_cache_path("git", cache_path_name)).to exist
+ # And the checkout is a bare checkout
+ expect(default_cache_path("git", cache_path_name, "HEAD")).to exist
+ end
+
+ # Subsequently installing the gem should compile it.
+ # _currently_, the gem gets compiled in vendor/cache, and vendor/cache is added
+ # to the $LOAD_PATH for git extensions, so it all kind of "works". However, in the
+ # future we would like to stop adding vendor/cache to the $LOAD_PATH for git extensions
+ # and instead treat them identically to normal gems (where the gem install location,
+ # not the cache location, is added to $LOAD_PATH).
+ # Verify that the compilation worked and the result is in $LOAD_PATH by simply attempting
+ # to require it; that should make sure this spec does not break if the load path behaviour
+ # is changed.
+ bundle :install, local: true
+ ruby <<~R, raise_on_error: false
+ require 'bundler/setup'
+ require 'foo_c'
+ R
+ expect(last_command).to_not be_failure
+ end
+
+ it "doesn't fail when git gem has extensions and an empty cache folder is present before bundle install" do
+ build_git "puma" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.executables = "puma"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("../lib", __FILE__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/puma.rb", "w") do |f|
+ f.puts "PUMA = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ FileUtils.mkdir_p(bundled_app("vendor/cache"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "puma", :git => "#{lib_path("puma-1.0")}"
+ G
+
+ bundle "exec puma"
+
+ expect(out).to eq("YES")
+ end
+end
diff --git a/spec/bundler/cache/path_spec.rb b/spec/bundler/cache/path_spec.rb
new file mode 100644
index 0000000000..42648aea1f
--- /dev/null
+++ b/spec/bundler/cache/path_spec.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache with path" do
+ it "is no-op when the path is within the bundle" do
+ build_lib "foo", path: bundled_app("lib/foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{bundled_app("lib/foo")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies when the path is outside the bundle " do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0/.bundlecache")).to be_file
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "copies when the path is outside the bundle and the paths intersect" do
+ libname = File.basename(bundled_app) + "_gem"
+ libpath = File.join(File.dirname(bundled_app), libname)
+
+ build_lib libname, path: libpath
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "#{libname}", :path => '#{libpath}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/#{libname}")).to exist
+ expect(bundled_app("vendor/cache/#{libname}/.bundlecache")).to be_file
+
+ expect(the_bundle).to include_gems "#{libname} 1.0"
+ end
+
+ it "updates the path on each cache" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ build_lib "foo" do |s|
+ s.write "lib/foo.rb", "puts :CACHE"
+ end
+
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+
+ run "require 'foo'"
+ expect(out).to eq("CACHE")
+ end
+
+ it "removes stale entries cache" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+
+ build_lib "bar"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :path => '#{lib_path("bar-1.0")}'
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist
+ end
+
+ it "does not cache path gems if cache_all is set to false" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+ bundle_config "cache_all false"
+
+ bundle :cache
+ expect(err).to be_empty
+ expect(bundled_app("vendor/cache/foo-1.0")).not_to exist
+ end
+
+ it "caches path gems by default" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :cache
+ expect(err).to be_empty
+ expect(bundled_app("vendor/cache/foo-1.0")).to exist
+ end
+end
diff --git a/spec/bundler/cache/platform_spec.rb b/spec/bundler/cache/platform_spec.rb
new file mode 100644
index 0000000000..71c0eaee8e
--- /dev/null
+++ b/spec/bundler/cache/platform_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache with multiple platforms" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :mri, :rbx do
+ gem "myrack", "1.0.0"
+ end
+
+ platforms :jruby do
+ gem "activesupport", "2.3.5"
+ end
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+ activesupport (2.3.5)
+
+ PLATFORMS
+ ruby
+ java
+
+ DEPENDENCIES
+ myrack (1.0.0)
+ activesupport (2.3.5)
+ G
+
+ cache_gems "myrack-1.0.0", "activesupport-2.3.5"
+ end
+
+ it "ensures that a successful bundle install does not delete gems for other platforms" do
+ bundle "install"
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist
+ end
+
+ it "ensures that a successful bundle update does not delete gems for other platforms" do
+ bundle "update", all: true
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/activesupport-2.3.5.gem")).to exist
+ end
+end
diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb
new file mode 100644
index 0000000000..162650f2e5
--- /dev/null
+++ b/spec/bundler/commands/add_spec.rb
@@ -0,0 +1,448 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle add" do
+ before :each do
+ build_repo2 do
+ build_gem "foo", "1.1"
+ build_gem "foo", "2.0"
+ build_gem "baz", "1.2.3"
+ build_gem "bar", "0.12.3"
+ build_gem "cat", "0.12.3.pre"
+ build_gem "dog", "1.1.3.pre"
+ build_gem "lemur", "3.1.1.pre.2023.1.1"
+ end
+
+ build_git "foo", "2.0"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "weakling", "~> 0.0.1"
+ G
+ end
+
+ context "when no gems are specified" do
+ it "shows error" do
+ bundle "add", raise_on_error: false
+
+ expect(err).to include("Please specify gems to add")
+ end
+ end
+
+ context "when Gemfile is empty, and frozen mode is set" do
+ it "shows error" do
+ gemfile 'source "https://gem.repo2"'
+ bundle "add bar", raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" }
+
+ expect(err).to include("Frozen mode is set, but there's no lockfile")
+ end
+ end
+
+ describe "without version specified" do
+ it "version requirement becomes >= major.minor.patch when resolved version is < 1.0" do
+ bundle "add 'bar'"
+ expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/)
+ expect(the_bundle).to include_gems "bar 0.12.3"
+ end
+
+ it "version requirement becomes >= major.minor when resolved version is > 1.0" do
+ bundle "add 'baz'"
+ expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/)
+ expect(the_bundle).to include_gems "baz 1.2.3"
+ end
+
+ it "version requirement becomes >= major.minor.patch.pre when resolved version is < 1.0" do
+ bundle "add 'cat'"
+ expect(bundled_app_gemfile.read).to match(/gem "cat", ">= 0.12.3.pre"/)
+ expect(the_bundle).to include_gems "cat 0.12.3.pre"
+ end
+
+ it "version requirement becomes >= major.minor.pre when resolved version is >= 1.0.pre" do
+ bundle "add 'dog'"
+ expect(bundled_app_gemfile.read).to match(/gem "dog", ">= 1.1.pre"/)
+ expect(the_bundle).to include_gems "dog 1.1.3.pre"
+ end
+
+ it "version requirement becomes >= major.minor.pre.tail when resolved version has a very long tail pre version" do
+ bundle "add 'lemur'"
+ # the trailing pre purposely matches the release version to ensure that subbing the release doesn't change the pre.version"
+ expect(bundled_app_gemfile.read).to match(/gem "lemur", ">= 3.1.pre.2023.1.1"/)
+ expect(the_bundle).to include_gems "lemur 3.1.1.pre.2023.1.1"
+ end
+ end
+
+ describe "with --version" do
+ it "adds dependency of specified version and runs install" do
+ bundle "add 'foo' --version='~> 1.0'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", "~> 1.0"/)
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "adds multiple version constraints when specified" do
+ requirements = ["< 3.0", "> 1.0"]
+ bundle "add 'foo' --version='#{requirements.join(", ")}'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", #{Gem::Requirement.new(requirements).as_list.map(&:dump).join(", ")}/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --require" do
+ it "adds the require param for the gem" do
+ bundle "add 'foo' --require=foo/engine"
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo",(?: .*,) require: "foo\/engine"})
+ end
+
+ it "converts false to a boolean" do
+ bundle "add 'foo' --require=false"
+ expect(bundled_app_gemfile.read).to match(/gem "foo",(?: .*,) require: false/)
+ end
+ end
+
+ describe "with --group" do
+ it "adds dependency for the specified group" do
+ bundle "add 'foo' --group='development'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", group: :development/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+
+ it "adds dependency to more than one group" do
+ bundle "add 'foo' --group='development, test'"
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", groups: \[:development, :test\]/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --source" do
+ it "adds dependency with specified source" do
+ bundle "add 'foo' --source='https://gem.repo2'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", source: "https://gem.repo2"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --path" do
+ it "adds dependency with specified path" do
+ bundle "add 'foo' --path='#{lib_path("foo-2.0")}'"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", path: "#{lib_path("foo-2.0")}"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git" do
+ it "adds dependency with specified git source" do
+ bundle "add foo --git=#{lib_path("foo-2.0")}"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --branch" do
+ before do
+ update_git "foo", "2.0", branch: "test"
+ end
+
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --branch=test"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", branch: "test"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --ref" do
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --ref=#{revision_for(lib_path("foo-2.0"))}"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", ">= 2\.0", git: "#{lib_path("foo-2.0")}", ref: "#{revision_for(lib_path("foo-2.0"))}"/)
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --github" do
+ before do
+ build_git "rake", "13.0"
+ git("config --global url.file://#{lib_path("rake-13.0")}.insteadOf https://github.com/ruby/rake.git")
+ end
+
+ it "adds dependency with specified github source" do
+ bundle "add rake --github=ruby/rake"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake"})
+ end
+
+ it "adds dependency with specified github source and branch" do
+ bundle "add rake --github=ruby/rake --branch=main"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", branch: "main"})
+ end
+
+ it "adds dependency with specified github source and ref" do
+ ref = revision_for(lib_path("rake-13.0"))
+ bundle "add rake --github=ruby/rake --ref=#{ref}"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", ref: "#{ref}"})
+ end
+
+ it "adds dependency with specified github source and glob" do
+ bundle "add rake --github=ruby/rake --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", glob: "\.\/\*\.gemspec"})
+ end
+
+ it "adds dependency with specified github source, branch and glob" do
+ bundle "add rake --github=ruby/rake --branch=main --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", branch: "main", glob: "\.\/\*\.gemspec"})
+ end
+
+ it "adds dependency with specified github source, ref and glob" do
+ ref = revision_for(lib_path("rake-13.0"))
+ bundle "add rake --github=ruby/rake --ref=#{ref} --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "rake", ">= 13\.\d+", github: "ruby\/rake", ref: "#{ref}", glob: "\.\/\*\.gemspec"})
+ end
+ end
+
+ describe "with --git and --glob" do
+ it "adds dependency with specified git source" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", glob: "\./\*\.gemspec"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --branch and --glob" do
+ before do
+ update_git "foo", "2.0", branch: "test"
+ end
+
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --branch=test --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2.0", git: "#{lib_path("foo-2.0")}", branch: "test", glob: "\./\*\.gemspec"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --git and --ref and --glob" do
+ it "adds dependency with specified git source and branch" do
+ bundle "add foo --git=#{lib_path("foo-2.0")} --ref=#{revision_for(lib_path("foo-2.0"))} --glob='./*.gemspec'"
+
+ expect(bundled_app_gemfile.read).to match(%r{gem "foo", ">= 2\.0", git: "#{lib_path("foo-2.0")}", ref: "#{revision_for(lib_path("foo-2.0"))}", glob: "\./\*\.gemspec"})
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with mismatched pair in --git/--github, --branch/--ref" do
+ describe "with --git and --github" do
+ it "throws error" do
+ bundle "add 'foo' --git x --github y", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--git` and `--github` at the same time.")
+ end
+ end
+
+ describe "with --branch and --ref with --git" do
+ it "throws error" do
+ bundle "add 'foo' --branch x --ref y --git file://git", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.")
+ end
+ end
+
+ describe "with --branch but without --git or --github" do
+ it "throws error" do
+ bundle "add 'foo' --branch x", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--branch` unless `--git` or `--github` is specified.")
+ end
+ end
+
+ describe "with --ref but without --git or --github" do
+ it "throws error" do
+ bundle "add 'foo' --ref y", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--ref` unless `--git` or `--github` is specified.")
+ end
+ end
+ end
+
+ describe "with --skip-install" do
+ it "adds gem to Gemfile but is not installed" do
+ bundle "add foo --skip-install --version=2.0"
+
+ expect(bundled_app_gemfile.read).to match(/gem "foo", "= 2.0"/)
+ expect(the_bundle).to_not include_gems "foo 2.0"
+ end
+ end
+
+ it "using combination of short form options works like long form" do
+ bundle "add 'foo' -s='https://gem.repo2' -g='development' -v='~>1.0'"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", "~> 1.0", group: :development, source: "https://gem.repo2")
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "shows error message when version is not formatted correctly" do
+ bundle "add 'foo' -v='~>1 . 0'", raise_on_error: false
+ expect(err).to match("Invalid gem requirement pattern '~>1 . 0'")
+ end
+
+ it "shows error message when gem cannot be found" do
+ bundle_config "force_ruby_platform true"
+ bundle "add 'werk_it'", raise_on_error: false
+ expect(err).to match("Could not find gem 'werk_it' in")
+
+ bundle "add 'werk_it' -s='https://gem.repo2'", raise_on_error: false
+ expect(err).to match("Could not find gem 'werk_it' in rubygems repository")
+ end
+
+ it "shows error message when source cannot be reached" do
+ bundle "add 'baz' --source='http://badhostasdf'", raise_on_error: false, artifice: "fail"
+ expect(err).to include("Could not reach host badhostasdf. Check your network connection and try again.")
+
+ bundle "add 'baz' --source='file://does/not/exist'", raise_on_error: false
+ expect(err).to include("Could not fetch specs from file://does/not/exist/")
+ end
+
+ describe "with --optimistic" do
+ it "ignores option" do
+ bundle "add 'foo' --optimistic"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", ">= 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --pessimistic" do
+ it "adds pessimistic version" do
+ bundle "add 'foo' --pessimistic"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", "~> 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --quiet option" do
+ it "is quiet when there are no warnings" do
+ bundle "add 'foo' --quiet"
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+
+ it "still displays warning and errors" do
+ create_file("add_with_warning.rb", <<~RUBY)
+ require "#{lib_dir}/bundler"
+ require "#{lib_dir}/bundler/cli"
+ require "#{lib_dir}/bundler/cli/add"
+
+ module RunWithWarning
+ def run
+ super
+ rescue
+ Bundler.ui.warn "This is a warning"
+ raise
+ end
+ end
+
+ Bundler::CLI::Add.prepend(RunWithWarning)
+ RUBY
+
+ bundle "add 'non-existing-gem' --quiet", raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("add_with_warning.rb")}" }
+ expect(out).to be_empty
+ expect(err).to include("Could not find gem 'non-existing-gem'")
+ expect(err).to include("This is a warning")
+ end
+ end
+
+ describe "with --strict option" do
+ it "adds strict version" do
+ bundle "add 'foo' --strict"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", "= 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with no option" do
+ it "adds optimistic version" do
+ bundle "add 'foo'"
+ expect(bundled_app_gemfile.read).to include %(gem "foo", ">= 2.0")
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+ end
+
+ describe "with --pessimistic and --strict" do
+ it "throws error" do
+ bundle "add 'foo' --strict --pessimistic", raise_on_error: false
+
+ expect(err).to include("You cannot specify `--strict` and `--pessimistic` at the same time")
+ end
+ end
+
+ context "multiple gems" do
+ it "adds multiple gems to gemfile" do
+ bundle "add bar baz"
+
+ expect(bundled_app_gemfile.read).to match(/gem "bar", ">= 0.12.3"/)
+ expect(bundled_app_gemfile.read).to match(/gem "baz", ">= 1.2"/)
+ end
+
+ it "throws error if any of the specified gems are present in the gemfile with different version" do
+ bundle "add weakling bar", raise_on_error: false
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("You specified: weakling (~> 0.0.1) and weakling (>= 0).")
+ end
+ end
+
+ describe "when a gem is added which is already specified in Gemfile with version" do
+ it "shows an error when added with different version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ G
+
+ bundle "add 'myrack' --version=1.1", raise_on_error: false
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("If you want to update the gem version, run `bundle update myrack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive")
+ end
+
+ it "shows error when added without version requirements" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ G
+
+ bundle "add 'myrack'", raise_on_error: false
+
+ expect(err).to include("Gem already added.")
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).not_to include("If you want to update the gem version, run `bundle update myrack`. You may also need to change the version requirement specified in the Gemfile if it's too restrictive")
+ end
+ end
+
+ describe "when a gem is added which is already specified in Gemfile without version" do
+ it "shows an error when added with different version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle "add 'myrack' --version=1.1", raise_on_error: false
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("If you want to update the gem version, run `bundle update myrack`.")
+ expect(err).not_to include("You may also need to change the version requirement specified in the Gemfile if it's too restrictive")
+ end
+ end
+
+ describe "when a gem is added and cache exists" do
+ it "caches all new dependencies added for the specified gem" do
+ bundle :cache
+
+ bundle "add 'myrack' --version=1.0.0"
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+ end
+end
diff --git a/spec/bundler/commands/binstubs_spec.rb b/spec/bundler/commands/binstubs_spec.rb
new file mode 100644
index 0000000000..af4d24a9e8
--- /dev/null
+++ b/spec/bundler/commands/binstubs_spec.rb
@@ -0,0 +1,333 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle binstubs <gem>" do
+ context "when the gem exists in the lockfile" do
+ it "sets up the binstub" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ end
+
+ it "does not install other binstubs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rails"
+ G
+
+ bundle "binstubs rails"
+
+ expect(bundled_app("bin/myrackup")).not_to exist
+ expect(bundled_app("bin/rails")).to exist
+ end
+
+ it "does install multiple binstubs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rails"
+ G
+
+ bundle "binstubs rails myrack"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/rails")).to exist
+ end
+
+ it "allows installing all binstubs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :binstubs, all: true
+
+ expect(bundled_app("bin/rails")).to exist
+ expect(bundled_app("bin/rake")).to exist
+ end
+
+ it "allows installing binstubs for all platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --all-platforms"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/myrackup.cmd")).to exist
+ end
+
+ it "displays an error when used without any gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs", raise_on_error: false
+ expect(exitstatus).to eq(1)
+ expect(err).to include("`bundle binstubs` needs at least one gem to run.")
+ end
+
+ it "displays an error when used with --all and gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack", all: true, raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("Cannot specify --all with specific gems")
+ end
+
+ it "installs binstubs from git gems" do
+ FileUtils.mkdir_p(lib_path("foo/bin"))
+ FileUtils.touch(lib_path("foo/bin/foo"))
+ build_git "foo", "1.0", path: lib_path("foo") do |s|
+ s.executables = %w[foo]
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ bundle "binstubs foo"
+
+ expect(bundled_app("bin/foo")).to exist
+ end
+
+ it "installs binstubs from path gems" do
+ FileUtils.mkdir_p(lib_path("foo/bin"))
+ FileUtils.touch(lib_path("foo/bin/foo"))
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.executables = %w[foo]
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ bundle "binstubs foo"
+
+ expect(bundled_app("bin/foo")).to exist
+ end
+
+ it "sets correct permissions for binstubs" do
+ with_umask(0o002) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+ binary = bundled_app("bin/myrackup")
+ expect(File.stat(binary).mode.to_s(8)).to eq(Gem.win_platform? ? "100644" : "100775")
+ end
+ end
+
+ context "when using --shebang" do
+ it "sets the specified shebang for the binstub" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --shebang jruby"
+ expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env jruby\n")
+ end
+ end
+ end
+
+ context "when the gem doesn't exist" do
+ it "displays an error with correct status" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "binstubs doesnt_exist", raise_on_error: false
+
+ expect(exitstatus).to eq(7)
+ expect(err).to include("Could not find gem 'doesnt_exist'.")
+ end
+ end
+
+ context "with the binstubs dir configured" do
+ before do
+ bundle_config "bin exec"
+ end
+
+ it "creates the binstubs in the configured dir" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+
+ expect(bundled_app("exec/myrackup")).to exist
+ end
+ end
+
+ context "with --standalone option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rails"
+ G
+ end
+
+ it "generates a standalone binstub" do
+ bundle "binstubs myrack --standalone"
+ expect(bundled_app("bin/myrackup")).to exist
+ end
+
+ it "generates a binstub that does not depend on rubygems or bundler" do
+ bundle "binstubs myrack --standalone"
+ expect(File.read(bundled_app("bin/myrackup"))).to_not include("Gem.bin_path")
+ end
+
+ it "generates a standalone binstub at the given path when configured" do
+ bundle_config "bin foo"
+ bundle "binstubs myrack --standalone"
+ expect(bundled_app("foo/myrackup")).to exist
+ end
+
+ context "when specified --all-platforms option" do
+ it "generates standalone binstubs for all platforms" do
+ bundle "binstubs myrack --standalone --all-platforms"
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/myrackup.cmd")).to exist
+ end
+ end
+
+ context "when the gem is bundler" do
+ it "warns without generating a standalone binstub" do
+ bundle "binstubs bundler --standalone"
+ expect(bundled_app("bin/bundle")).not_to exist
+ expect(bundled_app("bin/bundler")).not_to exist
+ expect(err).to include("Sorry, Bundler can only be run via RubyGems.")
+ end
+ end
+
+ context "when specified --all option" do
+ it "generates standalone binstubs for all gems except bundler" do
+ bundle "binstubs --standalone --all"
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(bundled_app("bin/rails")).to exist
+ expect(bundled_app("bin/bundle")).not_to exist
+ expect(bundled_app("bin/bundler")).not_to exist
+ expect(err).not_to include("Sorry, Bundler can only be run via RubyGems.")
+ end
+ end
+ end
+
+ context "when the bin already exists" do
+ it "doesn't overwrite and warns" do
+ FileUtils.mkdir_p(bundled_app("bin"))
+ File.open(bundled_app("bin/myrackup"), "wb") do |file|
+ file.print "OMG"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(File.read(bundled_app("bin/myrackup"))).to eq("OMG")
+ expect(err).to include("Skipped myrackup")
+ expect(err).to include("overwrite skipped stubs, use --force")
+ end
+
+ context "when using --force" do
+ it "overwrites the binstub" do
+ FileUtils.mkdir_p(bundled_app("bin"))
+ File.open(bundled_app("bin/myrackup"), "wb") do |file|
+ file.print "OMG"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --force"
+
+ expect(bundled_app("bin/myrackup")).to exist
+ expect(File.read(bundled_app("bin/myrackup"))).not_to eq("OMG")
+ end
+ end
+ end
+
+ context "when the gem has no bins" do
+ it "suggests child gems if they have bins" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack-obama"
+ G
+
+ bundle "binstubs myrack-obama"
+ expect(err).to include("myrack-obama has no executables")
+ expect(err).to include("myrack has: myrackup")
+ end
+
+ it "works if child gems don't have bins" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "actionpack"
+ G
+
+ bundle "binstubs actionpack"
+ expect(err).to include("no executables for the gem actionpack")
+ end
+
+ it "works if the gem has development dependencies" do
+ build_repo2 do
+ build_gem "with_development_dependency" do |s|
+ s.add_development_dependency "activesupport", "= 2.3.5"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_development_dependency"
+ G
+
+ bundle "binstubs with_development_dependency"
+ expect(err).to include("no executables for the gem with_development_dependency")
+ end
+ end
+
+ context "when BUNDLE_INSTALL is specified" do
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "binstubs myrack"
+ expect(out).to include("Installing myrack 1.0.0")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does nothing when already up to date" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "binstubs myrack", env: { "BUNDLE_INSTALL" => "1" }
+ expect(out).not_to include("Installing myrack 1.0.0")
+ end
+ end
+end
diff --git a/spec/bundler/commands/cache_spec.rb b/spec/bundler/commands/cache_spec.rb
new file mode 100644
index 0000000000..e223d07f7f
--- /dev/null
+++ b/spec/bundler/commands/cache_spec.rb
@@ -0,0 +1,620 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle cache" do
+ it "doesn't update the cache multiple times, even if it already exists" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :cache
+ expect(out).to include("Updating files in vendor/cache").once
+
+ bundle :cache
+ expect(out).to include("Updating files in vendor/cache").once
+ end
+
+ context "with --gemfile" do
+ it "finds the gemfile" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle "cache --gemfile=NotGemfile"
+
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with cache_all configured" do
+ context "without a gemspec" do
+ it "caches all dependencies except bundler itself" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gem 'bundler'
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+
+ context "with a gemspec" do
+ context "that has the same name as the gem" do
+ before do
+ File.open(bundled_app("mygem.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "nokogiri", "=1.4.2"
+ end
+ G
+ end
+ end
+
+ it "caches all dependencies except bundler and the gemspec specified gem" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gemspec
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist
+ expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+
+ context "that has a different name as the gem" do
+ before do
+ File.open(bundled_app("mygem_diffname.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "nokogiri", "=1.4.2"
+ end
+ G
+ end
+ end
+
+ it "caches all dependencies except bundler and the gemspec specified gem" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gemspec
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist
+ expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+ end
+
+ context "with multiple gemspecs" do
+ before do
+ File.open(bundled_app("mygem.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "nokogiri", "=1.4.2"
+ end
+ G
+ end
+ File.open(bundled_app("mygem_client.gemspec"), "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "mygem_test"
+ s.version = "0.1.1"
+ s.summary = ""
+ s.authors = ["gem author"]
+ s.add_development_dependency "weakling", "=0.0.3"
+ end
+ G
+ end
+ end
+
+ it "caches all dependencies except bundler and the gemspec specified gems" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ gemspec :name => 'mygem'
+ gemspec :name => 'mygem_test'
+ D
+
+ bundle_config "cache_all true"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/nokogiri-1.4.2.gem")).to exist
+ expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist
+ expect(bundled_app("vendor/cache/mygem-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/mygem_test-0.1.1.gem")).to_not exist
+ expect(bundled_app("vendor/cache/bundler-0.9.gem")).to_not exist
+ end
+ end
+ end
+
+ context "with --no-install" do
+ it "puts the gems in vendor/cache but does not install them" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ D
+
+ bundle "cache --no-install"
+
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "does not prevent installing gems with bundle install" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack'
+ D
+
+ bundle "cache --no-install"
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not prevent installing gems with bundle update" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ D
+
+ bundle "cache --no-install"
+ bundle "update --all"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with --all-platforms" do
+ it "puts the gems in vendor/cache even for other rubies" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack', :platforms => [:ruby_20, :windows_20]
+ D
+
+ bundle "cache --all-platforms"
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "prints a warn when using legacy windows rubies" do
+ gemfile <<-D
+ source "https://gem.repo1"
+ gem 'myrack', :platforms => [:ruby_20, :x64_mingw_20]
+ D
+
+ bundle "cache --all-platforms", raise_on_error: false
+ expect(err).to include("will be removed in the future")
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "does not attempt to install gems in without groups" do
+ build_repo4 do
+ build_gem "uninstallable", "2.0" do |s|
+ s.add_development_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", "task(:default) { raise 'CANNOT INSTALL' }"
+ end
+ end
+
+ bundle_config "without wo"
+ install_gemfile <<-G, artifice: "compact_index_extra_api"
+ source "https://main.repo"
+ gem "myrack"
+ group :wo do
+ gem "weakling"
+ gem "uninstallable", :source => "https://main.repo/extra"
+ end
+ G
+
+ bundle :cache, "all-platforms" => true, artifice: "compact_index_extra_api"
+ expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist
+ expect(bundled_app("vendor/cache/uninstallable-2.0.gem")).to exist
+ expect(the_bundle).to include_gem "myrack 1.0"
+ expect(the_bundle).not_to include_gems "weakling", "uninstallable"
+
+ bundle_config "without wo"
+ bundle :install, artifice: "compact_index_extra_api"
+ expect(the_bundle).to include_gem "myrack 1.0"
+ expect(the_bundle).not_to include_gems "weakling"
+ end
+
+ it "does not fail to cache gems in excluded groups when there's a lockfile but gems not previously installed" do
+ bundle_config "without wo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :wo do
+ gem "weakling"
+ end
+ G
+
+ bundle :lock
+ bundle :cache, "all-platforms" => true
+ expect(bundled_app("vendor/cache/weakling-0.0.3.gem")).to exist
+ end
+ end
+
+ context "with frozen configured" do
+ let(:app_cache) { bundled_app("vendor/cache") }
+
+ before do
+ bundle_config "frozen true"
+ end
+
+ it "tries to install but fails when the lockfile is out of sync" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ myrack-obama
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ bundle :cache, raise_on_error: false
+ expect(exitstatus).to eq(16)
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have deleted from the Gemfile")
+ expect(err).to include("* myrack-obama")
+ bundle "env"
+ expect(out).to include("frozen")
+ end
+
+ it "caches gems without installing when lockfile is in sync, and --no-install is passed, even if vendor/cache directory is initially empty" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ FileUtils.mkdir_p app_cache
+
+ bundle "cache --no-install"
+ expect(out).not_to include("Installing myrack 1.0.0")
+ expect(out).to include("Fetching myrack 1.0.0")
+ expect(app_cache.join("myrack-1.0.0.gem")).to exist
+ end
+
+ it "completes a partial cache when lockfile is in sync, even if the already cached gem is no longer available remotely" do
+ build_repo4 do
+ build_gem "foo", "1.0.0"
+ end
+
+ build_gem "bar", "1.0.0", path: bundled_app("vendor/cache")
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ gem "bar"
+ G
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0.0)
+ bar (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ bar
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "cache --no-install"
+ expect(out).to include("Fetching foo 1.0.0")
+ expect(out).not_to include("Fetching bar 1.0.0")
+ expect(app_cache.join("foo-1.0.0.gem")).to exist
+ expect(app_cache.join("bar-1.0.0.gem")).to exist
+ end
+ end
+
+ context "with gems with extensions" do
+ before do
+ build_repo2 do
+ build_gem "racc", "2.0" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", "task(:default) { puts 'INSTALLING myrack' }"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "racc"
+ G
+ end
+
+ it "installs them properly from cache to a different path" do
+ bundle "cache"
+ bundle_config "path vendor/bundle"
+ bundle "install --local"
+ end
+ end
+end
+
+RSpec.describe "bundle install with gem sources" do
+ describe "when cached and locked" do
+ it "does not hit the remote at all" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle "install --local"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not hit the remote at all in frozen mode" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle_config "deployment true"
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not hit the remote at all in non frozen mode either" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not hit the remote at all when cache_all_platforms configured" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle :cache
+ pristine_system_gems
+ FileUtils.rm_r gem_repo2
+
+ bundle_config "cache_all_platforms true"
+ bundle_config "path vendor/bundle"
+ bundle "install --local"
+ expect(out).not_to include("Fetching gem metadata")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "uses cached gems for secondary sources when cache_all_platforms configured" do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "foo", "1.0.0" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "foo"
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0.0-x86_64-linux)
+ foo (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle_config "cache_all_platforms true"
+ bundle_config "path vendor/bundle"
+ bundle :cache, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+
+ # simulate removal of all remote gems
+ empty_repo4
+
+ # delete compact index cache
+ FileUtils.rm_r home(".bundle/cache/compact_index")
+
+ bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+
+ expect(the_bundle).to include_gems "foo 1.0.0 x86_64-linux"
+ end
+ end
+
+ it "does not reinstall already-installed gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle :cache
+
+ build_gem "myrack", "1.0.0", path: bundled_app("vendor/cache") do |s|
+ s.write "lib/myrack.rb", "raise 'omg'"
+ end
+
+ bundle :install
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "ignores cached gems for the wrong platform" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+ bundle :cache
+ end
+
+ pristine_system_gems
+
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+ expect(the_bundle).to include_gems("platform_specific 1.0 ruby")
+ end
+
+ it "keeps gems that are locked and cached for the current platform, even if incompatible with the current ruby" do
+ build_repo4 do
+ build_gem "bcrypt_pbkdf", "1.1.1"
+ build_gem "bcrypt_pbkdf", "1.1.1" do |s|
+ s.platform = "arm64-darwin"
+ s.required_ruby_version = "< #{current_ruby_minor}"
+ end
+ end
+
+ app_cache = bundled_app("vendor/cache")
+ FileUtils.mkdir_p app_cache
+ FileUtils.cp gem_repo4("gems/bcrypt_pbkdf-1.1.1-arm64-darwin.gem"), app_cache
+ FileUtils.cp gem_repo4("gems/bcrypt_pbkdf-1.1.1.gem"), app_cache
+
+ bundle_config "cache_all_platforms true"
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bcrypt_pbkdf (1.1.1)
+ bcrypt_pbkdf (1.1.1-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+ ruby
+
+ DEPENDENCIES
+ bcrypt_pbkdf
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-23" do
+ install_gemfile <<~G, verbose: true
+ source "https://gem.repo4"
+ gem "bcrypt_pbkdf"
+ G
+
+ expect(out).to include("Updating files in vendor/cache")
+ expect(err).to be_empty
+ expect(app_cache.join("bcrypt_pbkdf-1.1.1-arm64-darwin.gem")).to exist
+ expect(app_cache.join("bcrypt_pbkdf-1.1.1.gem")).to exist
+ end
+ end
+
+ it "does not update the cache if --no-cache is passed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundled_app("vendor/cache").mkpath
+ expect(bundled_app("vendor/cache").children).to be_empty
+
+ bundle "install --no-cache"
+ expect(bundled_app("vendor/cache").children).to be_empty
+ end
+ end
+end
diff --git a/spec/bundler/commands/check_spec.rb b/spec/bundler/commands/check_spec.rb
new file mode 100644
index 0000000000..7fe6897ae3
--- /dev/null
+++ b/spec/bundler/commands/check_spec.rb
@@ -0,0 +1,601 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle check" do
+ it "returns success when the Gemfile is satisfied" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "works with the --gemfile flag when not in the directory" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle "check --gemfile bundled_app/Gemfile", dir: tmp
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "creates a Gemfile.lock by default if one does not exist" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "check"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "does not create a Gemfile.lock if --dry-run was passed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "check --dry-run"
+
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "prints an error that shows missing gems" do
+ system_gems ["rails-2.3.2"], path: default_bundle_path
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :check, raise_on_error: false
+ expect(err).to include("The following gems are missing")
+ expect(err).to include(" * rake (#{rake_version})")
+ expect(err).to include(" * actionpack (2.3.2)")
+ expect(err).to include(" * activerecord (2.3.2)")
+ expect(err).to include(" * actionmailer (2.3.2)")
+ expect(err).to include(" * activeresource (2.3.2)")
+ expect(err).to include(" * activesupport (2.3.2)")
+ expect(err).to include("Install missing gems with `bundle install`")
+ end
+
+ it "prints an error that shows missing gems if a Gemfile.lock does not exist and a toplevel dependency is missing" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to be > 0
+ expect(err).to include("The following gems are missing")
+ expect(err).to include(" * rails (2.3.2)")
+ expect(err).to include(" * rake (#{rake_version})")
+ expect(err).to include(" * actionpack (2.3.2)")
+ expect(err).to include(" * activerecord (2.3.2)")
+ expect(err).to include(" * actionmailer (2.3.2)")
+ expect(err).to include(" * activeresource (2.3.2)")
+ expect(err).to include(" * activesupport (2.3.2)")
+ expect(err).to include("Install missing gems with `bundle install`")
+ end
+
+ it "prints a generic error if gem git source is not checked out" do
+ build_git "foo", path: lib_path("foo")
+
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", git: "#{lib_path("foo")}"
+ G
+
+ FileUtils.rm_r bundled_app("vendor/bundle")
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to eq 1
+ expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.")
+ end
+
+ it "prints a generic message if you changed your lockfile" do
+ build_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'rails'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "rails_pinned_to_old_activesupport"
+ G
+
+ bundle :check, raise_on_error: false
+ expect(err).to include("Bundler can't satisfy your Gemfile's dependencies.")
+ end
+
+ it "uses the without setting" do
+ bundle_config "without foo"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ group :foo do
+ gem "myrack"
+ end
+ G
+
+ bundle "check"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "ensures that gems are actually installed and not just cached" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :foo
+ G
+
+ bundle_config "without foo"
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "check", raise_on_error: false
+ expect(err).to include("* myrack (1.0.0)")
+ expect(exitstatus).to eq(1)
+ end
+
+ it "ensures that gems are actually installed and not just cached in applications' cache" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle :cache
+
+ uninstall_gem("myrack", env: { "GEM_HOME" => vendored_gems.to_s })
+
+ bundle "check", raise_on_error: false
+ expect(err).to include("* myrack (1.0.0)")
+ expect(exitstatus).to eq(1)
+ end
+
+ it "ignores missing gems restricted to other platforms" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ platforms :#{not_local_tag} do
+ gem "activesupport"
+ end
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ activesupport (2.3.5)
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{generic_local_platform}
+ #{not_local}
+
+ DEPENDENCIES
+ myrack
+ activesupport
+ G
+
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "works with env conditionals" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ env :NOT_GOING_TO_BE_SET do
+ gem "activesupport"
+ end
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ activesupport (2.3.5)
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{generic_local_platform}
+ #{not_local}
+
+ DEPENDENCIES
+ myrack
+ activesupport
+ G
+
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "outputs an error when the default Gemfile is not found" do
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to eq(10)
+ expect(err).to include("Could not locate Gemfile")
+ end
+
+ it "does not output fatal error message" do
+ bundle :check, raise_on_error: false
+ expect(exitstatus).to eq(10)
+ expect(err).not_to include("Unfortunately, a fatal error has occurred. ")
+ end
+
+ it "fails when there's no lockfile and frozen is set" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ bundle_config "deployment true"
+ bundle "install"
+ FileUtils.rm(bundled_app_lock)
+
+ bundle :check, raise_on_error: false
+ expect(last_command).to be_failure
+ end
+
+ describe "when locked" do
+ before :each do
+ system_gems "myrack-1.0.0"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+ end
+
+ it "returns success when the Gemfile is satisfied" do
+ bundle :install
+ bundle :check
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "shows what is missing with the current Gemfile if it is not satisfied" do
+ FileUtils.rm_r default_bundle_path
+ default_system_gems
+ bundle :check, raise_on_error: false
+ expect(err).to match(/The following gems are missing/)
+ expect(err).to include("* myrack (1.0")
+ end
+ end
+
+ describe "when locked with multiple dependents with different requirements" do
+ before :each do
+ build_repo4 do
+ build_gem "depends_on_myrack" do |s|
+ s.add_dependency "myrack", ">= 1.0"
+ end
+ build_gem "also_depends_on_myrack" do |s|
+ s.add_dependency "myrack", "~> 1.0"
+ end
+ build_gem "myrack"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "depends_on_myrack"
+ gem "also_depends_on_myrack"
+ G
+
+ bundle "lock"
+ end
+
+ it "shows what is missing with the current Gemfile without duplications" do
+ bundle :check, raise_on_error: false
+ expect(err).to match(/The following gems are missing/)
+ expect(err).to include("* myrack (1.0").once
+ end
+ end
+
+ describe "when locked under multiple platforms" do
+ before :each do
+ build_repo4 do
+ build_gem "myrack"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ ruby
+ #{local_platform}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "shows what is missing with the current Gemfile without duplications" do
+ bundle :check, raise_on_error: false
+ expect(err).to match(/The following gems are missing/)
+ expect(err).to include("* myrack (1.0").once
+ end
+ end
+
+ describe "when using only scoped rubygems sources" do
+ before do
+ gemfile <<~G
+ source "https://gem.repo2"
+ source "https://gem.repo1" do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "returns success when the Gemfile is satisfied" do
+ system_gems "myrack-1.0.0", path: default_bundle_path
+ bundle :check, artifice: "compact_index"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+ end
+
+ describe "when using only scoped rubygems sources with indirect dependencies" do
+ before do
+ build_repo4 do
+ build_gem "depends_on_myrack" do |s|
+ s.add_dependency "myrack"
+ end
+
+ build_gem "myrack"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo1"
+ source "https://gem.repo4" do
+ gem "depends_on_myrack"
+ end
+ G
+ end
+
+ it "returns success when the Gemfile is satisfied and generates a correct lockfile" do
+ system_gems "depends_on_myrack-1.0", "myrack-1.0", gem_repo: gem_repo4, path: default_bundle_path
+ bundle :check, artifice: "compact_index"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "depends_on_myrack", "1.0"
+ c.checksum gem_repo4, "myrack", "1.0"
+ end
+
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ depends_on_myrack (1.0)
+ myrack
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ depends_on_myrack!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with gemspec directive and scoped sources" do
+ before do
+ build_repo4 do
+ build_gem "awesome_print"
+ end
+
+ build_repo2 do
+ build_gem "dex-dispatch-engine"
+ end
+
+ build_lib("bundle-check-issue", path: tmp("bundle-check-issue")) do |s|
+ s.write "Gemfile", <<-G
+ source "https://localgemserver.test"
+
+ gemspec
+
+ source "https://localgemserver.test/extra" do
+ gem "dex-dispatch-engine"
+ end
+ G
+
+ s.add_dependency "awesome_print"
+ end
+
+ bundle "install", artifice: "compact_index_extra", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, dir: tmp("bundle-check-issue")
+ end
+
+ it "does not corrupt lockfile when changing version" do
+ version_file = tmp("bundle-check-issue/bundle-check-issue.gemspec")
+ File.write(version_file, File.read(version_file).gsub(/s\.version = .+/, "s.version = '9999'"))
+
+ bundle "check --verbose", dir: tmp("bundle-check-issue")
+
+ lockfile = File.read(tmp("bundle-check-issue/Gemfile.lock"))
+
+ checksums = checksums_section_when_enabled(lockfile) do |c|
+ c.checksum gem_repo4, "awesome_print", "1.0"
+ c.no_checksum "bundle-check-issue", "9999"
+ c.checksum gem_repo2, "dex-dispatch-engine", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ bundle-check-issue (9999)
+ awesome_print
+
+ GEM
+ remote: https://localgemserver.test/
+ specs:
+ awesome_print (1.0)
+
+ GEM
+ remote: https://localgemserver.test/extra/
+ specs:
+ dex-dispatch-engine (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ bundle-check-issue!
+ dex-dispatch-engine!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with scoped and unscoped sources" do
+ it "does not corrupt lockfile" do
+ build_repo2 do
+ build_gem "foo"
+ build_gem "wadus"
+ build_gem("baz") {|s| s.add_dependency "wadus" }
+ end
+
+ build_repo4 do
+ build_gem "bar"
+ end
+
+ bundle_config "path.system true"
+
+ # Add all gems to ensure all gems are installed so that a bundle check
+ # would be successful
+ install_gemfile(<<-G, artifice: "compact_index_extra")
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "bar"
+ end
+
+ gem "foo"
+ gem "baz"
+ G
+
+ original_lockfile = lockfile
+
+ # Remove "baz" gem from the Gemfile, and bundle install again to generate
+ # a functional lockfile with no "baz" dependency or "wadus" transitive
+ # dependency
+ install_gemfile(<<-G, artifice: "compact_index_extra")
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "bar"
+ end
+
+ gem "foo"
+ G
+
+ # Add back "baz" gem back to the Gemfile, but _crucially_ we do not perform a
+ # bundle install
+ gemfile(gemfile + 'gem "baz"')
+
+ bundle :check, verbose: true
+
+ # Bundle check should succeed and restore the lockfile to its original
+ # state
+ expect(lockfile).to eq(original_lockfile)
+ end
+ end
+
+ describe "BUNDLED WITH" do
+ def lock_with(bundler_version = nil)
+ lock = <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ L
+
+ if bundler_version
+ lock += "\nBUNDLED WITH\n #{bundler_version}\n"
+ end
+
+ lock
+ end
+
+ before do
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "is not present" do
+ it "does not change the lock" do
+ lockfile lock_with(nil)
+ bundle :check
+ expect(lockfile).to eq lock_with(nil)
+ end
+ end
+
+ context "is newer" do
+ it "does not change the lock and does not warn" do
+ lockfile lock_with(Bundler::VERSION.succ)
+ bundle :check
+ expect(err).to be_empty
+ expect(lockfile).to eq lock_with(Bundler::VERSION.succ)
+ end
+ end
+
+ context "is older" do
+ it "does not change the lock" do
+ system_gems "bundler-1.18.0"
+ lockfile lock_with("1.18.0")
+ bundle :check
+ expect(lockfile).to eq lock_with("1.18.0")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/commands/clean_spec.rb b/spec/bundler/commands/clean_spec.rb
new file mode 100644
index 0000000000..c77859d378
--- /dev/null
+++ b/spec/bundler/commands/clean_spec.rb
@@ -0,0 +1,936 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle clean" do
+ def should_have_gems(*gems)
+ gems.each do |g|
+ expect(vendored_gems("gems/#{g}")).to exist
+ expect(vendored_gems("specifications/#{g}.gemspec")).to exist
+ expect(vendored_gems("cache/#{g}.gem")).to exist
+ end
+ end
+
+ def should_not_have_gems(*gems)
+ gems.each do |g|
+ expect(vendored_gems("gems/#{g}")).not_to exist
+ expect(vendored_gems("specifications/#{g}.gemspec")).not_to exist
+ expect(vendored_gems("cache/#{g}.gem")).not_to exist
+ end
+ end
+
+ it "removes unused gems that are different" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("Removing foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0"
+ should_not_have_gems "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "removes old version of gem if unused" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ gem "foo"
+ G
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("Removing myrack (0.9.1)")
+
+ should_have_gems "foo-1.0", "myrack-1.0.0"
+ should_not_have_gems "myrack-0.9.1"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "removes new version of gem if unused" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+ bundle "update myrack"
+
+ bundle :clean
+
+ expect(out).to include("Removing myrack (1.0.0)")
+
+ should_have_gems "foo-1.0", "myrack-0.9.1"
+ should_not_have_gems "myrack-1.0.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "removes gems in bundle without groups" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+
+ group :test_group do
+ gem "myrack", "1.0.0"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle_config "without test_group"
+ bundle "install"
+ bundle :clean
+
+ expect(out).to include("Removing myrack (1.0.0)")
+
+ should_have_gems "foo-1.0"
+ should_not_have_gems "myrack-1.0.0"
+
+ expect(vendored_gems("bin/myrackup")).to_not exist
+ end
+
+ it "does not remove cached git dir if it's being used" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+ git_path = lib_path("foo-1.0")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ bundle :clean
+
+ digest = Digest(:SHA1).hexdigest(git_path.to_s)
+ cache_path = vendored_gems("cache/bundler/git/foo-1.0-#{digest}")
+ expect(cache_path).to exist
+ end
+
+ it "removes unused git gems" do
+ build_git "foo", path: lib_path("foo")
+ git_path = lib_path("foo")
+ revision = revision_for(git_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("Removing foo (#{revision[0..11]})")
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to exist
+ expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).not_to exist
+ digest = Digest(:SHA1).hexdigest(git_path.to_s)
+ expect(vendored_gems("cache/bundler/git/foo-#{digest}")).not_to exist
+
+ expect(vendored_gems("specifications/myrack-1.0.0.gemspec")).to exist
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "keeps used git gems even if installed to a symlinked location" do
+ build_git "foo", path: lib_path("foo")
+ git_path = lib_path("foo")
+ revision = revision_for(git_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ G
+
+ FileUtils.mkdir_p(bundled_app("real-path"))
+ File.symlink(bundled_app("real-path"), bundled_app("symlink-path"))
+
+ bundle_config "path #{bundled_app("symlink-path")}"
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).not_to include("Removing foo (#{revision[0..11]})")
+
+ expect(bundled_app("symlink-path/#{Bundler.ruby_scope}/bundler/gems/foo-#{revision[0..11]}")).to exist
+ end
+
+ it "removes old git gems on bundle update" do
+ build_git "foo-bar", path: lib_path("foo-bar")
+ revision = revision_for(lib_path("foo-bar"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ git "#{lib_path("foo-bar")}" do
+ gem "foo-bar"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ update_git "foo-bar", path: lib_path("foo-bar")
+ revision2 = revision_for(lib_path("foo-bar"))
+
+ bundle "update", all: true
+ bundle :clean
+
+ expect(out).to include("Removing foo-bar (#{revision[0..11]})")
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to exist
+ expect(vendored_gems("bundler/gems/foo-bar-#{revision[0..11]}")).not_to exist
+ expect(vendored_gems("bundler/gems/foo-bar-#{revision2[0..11]}")).to exist
+
+ expect(vendored_gems("specifications/myrack-1.0.0.gemspec")).to exist
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "does not remove nested gems in a git repo" do
+ build_lib "activesupport", "3.0", path: lib_path("rails/activesupport")
+ build_git "rails", "3.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 3.0"
+ end
+ revision = revision_for(lib_path("rails"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", :git => "#{lib_path("rails")}", :ref => '#{revision}'
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle :clean
+ expect(out).to include("")
+
+ expect(vendored_gems("bundler/gems/rails-#{revision[0..11]}")).to exist
+ end
+
+ it "does not remove git sources that are in without groups" do
+ build_git "foo", path: lib_path("foo")
+ git_path = lib_path("foo")
+ revision = revision_for(git_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ group :test do
+ git "#{git_path}", :ref => "#{revision}" do
+ gem "foo"
+ end
+ end
+ G
+ bundle_config "path vendor/bundle"
+ bundle_config "without test"
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).to include("")
+ expect(vendored_gems("bundler/gems/foo-#{revision[0..11]}")).to exist
+ digest = Digest(:SHA1).hexdigest(git_path.to_s)
+ expect(vendored_gems("cache/bundler/git/foo-#{digest}")).to_not exist
+ end
+
+ it "does not blow up when using without groups" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+
+ group :development do
+ gem "foo"
+ end
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "without development"
+ bundle "install"
+
+ bundle :clean
+ end
+
+ it "displays an error when used without --path" do
+ bundle_config "path.system true"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+
+ bundle :clean, raise_on_error: false
+
+ expect(exitstatus).to eq(15)
+ expect(err).to include("--force")
+ end
+
+ # handling bundle clean upgrade path from the pre's
+ it "removes .gem/.gemspec file even if there's no corresponding gem dir" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ G
+ bundle "install"
+
+ FileUtils.rm(vendored_gems("bin/myrackup"))
+ FileUtils.rm_r(vendored_gems("gems/thin-1.0"))
+ FileUtils.rm_r(vendored_gems("gems/myrack-1.0.0"))
+
+ bundle :clean
+
+ should_not_have_gems "thin-1.0", "myrack-1.0"
+ should_have_gems "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).not_to exist
+ end
+
+ it "does not call clean automatically when using system gems" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ installed_gems_list
+ expect(out).to include("myrack (1.0.0)").and include("thin (1.0)")
+ end
+
+ it "does not clean on bundle update when path has not been set" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+
+ bundle "update", all: true
+
+ files = Pathname.glob(default_bundle_path("*", "*"))
+ files.map! {|f| f.to_s.sub(default_bundle_path.to_s, "") }
+ expected_files = %W[
+ /bin/bundle
+ /bin/bundler
+ /cache/bundler-#{Bundler::VERSION}.gem
+ /cache/foo-1.0.1.gem
+ /cache/foo-1.0.gem
+ /gems/bundler-#{Bundler::VERSION}
+ /gems/foo-1.0
+ /gems/foo-1.0.1
+ /specifications/bundler-#{Bundler::VERSION}.gemspec
+ /specifications/foo-1.0.1.gemspec
+ /specifications/foo-1.0.gemspec
+ ]
+ expected_files += ["/bin/bundle.bat", "/bin/bundler.bat"] if Gem.win_platform?
+
+ expect(files.sort).to eq(expected_files.sort)
+ end
+
+ it "will automatically clean on bundle update when path has not been set", bundler: "5" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+
+ bundle "update", all: true
+
+ files = Pathname.glob(local_gem_path("*", "*"))
+ files.map! {|f| f.to_s.sub(local_gem_path.to_s, "") }
+ expect(files.sort).to eq %w[
+ /cache/foo-1.0.1.gem
+ /gems/foo-1.0.1
+ /specifications/foo-1.0.1.gemspec
+ ]
+ end
+
+ it "does not clean automatically when path configured" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "myrack"
+ G
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle "install"
+
+ should_have_gems "myrack-1.0.0", "thin-1.0"
+ end
+
+ it "does not clean on bundle update when path configured" do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+
+ bundle :update, all: true
+ should_have_gems "foo-1.0", "foo-1.0.1"
+ end
+
+ it "does not clean on bundle update when installing to system gems" do
+ bundle_config "path.system true"
+
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+ G
+ bundle "install"
+
+ update_repo2 do
+ build_gem "foo", "1.0.1"
+ end
+ bundle :update, all: true
+
+ installed_gems_list
+ expect(out).to include("foo (1.0.1, 1.0)")
+ end
+
+ it "cleans system gems when --force is used" do
+ bundle_config "path.system true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ gem "myrack"
+ G
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle :install
+ bundle "clean --force"
+
+ expect(out).to include("Removing foo (1.0)")
+ installed_gems_list
+ expect(out).not_to include("foo (1.0)")
+ expect(out).to include("myrack (1.0.0)")
+ end
+
+ describe "when missing permissions", :permissions do
+ before { ENV["BUNDLE_PATH__SYSTEM"] = "true" }
+ let(:system_cache_path) { system_gem_path("cache") }
+ after do
+ FileUtils.chmod(0o755, system_cache_path)
+ end
+ it "returns a helpful error message" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ gem "myrack"
+ G
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle :install
+
+ FileUtils.chmod(0o500, system_cache_path)
+
+ bundle :clean, force: true, raise_on_error: false
+
+ expect(err).to include(system_gem_path.to_s)
+ expect(err).to include("grant write permissions")
+
+ installed_gems_list
+ expect(out).to include("foo (1.0)")
+ expect(out).to include("myrack (1.0.0)")
+ end
+ end
+
+ it "cleans git gems with a 7 length git revision" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ # mimic 7 length git revisions in Gemfile.lock
+ gemfile_lock = File.read(bundled_app_lock).split("\n")
+ gemfile_lock.each_with_index do |line, index|
+ gemfile_lock[index] = line[0..(11 + 7)] if line.include?(" revision:")
+ end
+ lockfile(bundled_app_lock, gemfile_lock.join("\n"))
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ bundle :clean
+
+ expect(out).not_to include("Removing foo (1.0 #{revision[0..6]})")
+
+ expect(vendored_gems("bundler/gems/foo-1.0-#{revision[0..6]}")).to exist
+ end
+
+ it "when using --force on system gems, it doesn't remove binaries" do
+ bundle_config "path.system true"
+
+ build_repo2 do
+ build_gem "bindir" do |s|
+ s.bindir = "exe"
+ s.executables = "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "bindir"
+ G
+ bundle :install
+
+ bundle "clean --force"
+
+ sys_exec "foo"
+
+ expect(out).to eq("1.0")
+ end
+
+ it "when using --force, it doesn't remove default gem binaries" do
+ default_irb_version = ruby "gem 'irb', '< 999999'; require 'irb'; puts IRB::VERSION", raise_on_error: false
+ skip "irb isn't a default gem" if default_irb_version.empty?
+
+ # simulate executable for default gem
+ build_gem "irb", default_irb_version, to_system: true, default: true do |s|
+ s.executables = "irb"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ G
+
+ bundle "clean --force", env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s }
+
+ expect(out).not_to include("Removing irb")
+ end
+
+ it "doesn't blow up on path gems without a .gemspec" do
+ relative_path = "vendor/private_gems/bar-1.0"
+ absolute_path = bundled_app(relative_path)
+ FileUtils.mkdir_p("#{absolute_path}/lib/bar")
+ File.open("#{absolute_path}/lib/bar/bar.rb", "wb") do |file|
+ file.puts "module Bar; end"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ gem "bar", "1.0", :path => "#{relative_path}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle :clean
+ end
+
+ it "doesn't remove gems in dry-run mode with path set" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+
+ bundle :install
+
+ bundle "clean --dry-run"
+
+ expect(out).not_to include("Removing foo (1.0)")
+ expect(out).to include("Would have removed foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "doesn't remove gems in dry-run mode with no path set" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+
+ bundle :install
+
+ bundle "clean --dry-run"
+
+ expect(out).not_to include("Removing foo (1.0)")
+ expect(out).to include("Would have removed foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0", "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "doesn't store dry run as a config setting" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+ bundle_config "dry_run false"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ G
+
+ bundle :install
+
+ bundle "clean"
+
+ expect(out).to include("Removing foo (1.0)")
+ expect(out).not_to include("Would have removed foo (1.0)")
+
+ should_have_gems "thin-1.0", "myrack-1.0.0"
+ should_not_have_gems "foo-1.0"
+
+ expect(vendored_gems("bin/myrackup")).to exist
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "foo"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean false"
+ bundle "install"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "weakling"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :clean
+ expect(out).to include("Installing weakling 0.0.3")
+ should_have_gems "thin-1.0", "myrack-1.0.0", "weakling-0.0.3"
+ should_not_have_gems "foo-1.0"
+ end
+
+ it "doesn't remove extensions artifacts from bundled git gems after clean" do
+ build_git "very_simple_git_binary", &:add_c_extension
+
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ expect(vendored_gems("bundler/gems/extensions")).to exist
+ expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist
+
+ bundle :clean
+ expect(out).to be_empty
+
+ expect(vendored_gems("bundler/gems/extensions")).to exist
+ expect(vendored_gems("bundler/gems/very_simple_git_binary-1.0-#{revision[0..11]}")).to exist
+ end
+
+ it "removes extension directories" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "very_simple_binary"
+ gem "simple_binary"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ very_simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/extensions/*/*/very_simple_binary-1.0").first
+
+ simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/extensions/*/*/simple_binary-1.0").first
+
+ expect(very_simple_binary_extensions_dir).to exist
+ expect(simple_binary_extensions_dir).to exist
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "simple_binary"
+ G
+
+ bundle "install"
+ bundle :clean
+ expect(out).to eq("Removing very_simple_binary (1.0)")
+
+ expect(very_simple_binary_extensions_dir).not_to exist
+ expect(simple_binary_extensions_dir).to exist
+ end
+
+ it "removes git extension directories" do
+ build_git "very_simple_git_binary", &:add_c_extension
+
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))
+ short_revision = revision[0..11]
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ very_simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/bundler/gems/extensions/*/*/very_simple_git_binary-1.0-#{short_revision}").first
+
+ expect(very_simple_binary_extensions_dir).to exist
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ G
+
+ bundle "install"
+ bundle :clean
+ expect(out).to include("Removing thin (1.0)")
+ expect(very_simple_binary_extensions_dir).to exist
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "install"
+ bundle :clean
+ expect(out).to eq("Removing very_simple_git_binary-1.0 (#{short_revision})")
+
+ expect(very_simple_binary_extensions_dir).not_to exist
+ end
+
+ it "keeps git extension directories when excluded by group" do
+ build_git "very_simple_git_binary", &:add_c_extension
+
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))
+ short_revision = revision[0..11]
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :development do
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}", :ref => "#{revision}"
+ end
+ G
+
+ bundle :lock
+ bundle_config "without development"
+ bundle_config "path vendor/bundle"
+ bundle "install", verbose: true
+ bundle :clean
+
+ very_simple_binary_extensions_dir =
+ Pathname.glob("#{vendored_gems}/bundler/gems/extensions/*/*/very_simple_git_binary-1.0-#{short_revision}").first
+
+ expect(very_simple_binary_extensions_dir).to be_nil
+ end
+
+ it "does not remove the bundler version currently running" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+
+ version = Bundler.gem_version.to_s
+ # Simulate that the locked bundler version is installed in the bundle path
+ # by creating the gem directory and gemspec (as would happen after bundle install with that version)
+ Pathname(vendored_gems("cache/bundler-#{version}.gem")).tap do |path|
+ FileUtils.touch(path)
+ end
+ FileUtils.touch(vendored_gems("gems/bundler-#{version}"))
+ Pathname(vendored_gems("specifications/bundler-#{version}.gemspec")).tap do |path|
+ path.write(<<~GEMSPEC)
+ Gem::Specification.new do |s|
+ s.name = "bundler"
+ s.version = "#{version}"
+ s.authors = ["bundler team"]
+ s.summary = "The best way to manage your application's dependencies"
+ end
+ GEMSPEC
+ end
+
+ should_have_gems "bundler-#{version}"
+
+ bundle :clean
+
+ should_have_gems "bundler-#{version}"
+ end
+end
diff --git a/spec/bundler/commands/config_spec.rb b/spec/bundler/commands/config_spec.rb
new file mode 100644
index 0000000000..e8ab32ca5d
--- /dev/null
+++ b/spec/bundler/commands/config_spec.rb
@@ -0,0 +1,646 @@
+# frozen_string_literal: true
+
+RSpec.describe ".bundle/config" do
+ describe "config" do
+ before { bundle "config set foo bar" }
+
+ it "prints a detailed report of local and user configuration" do
+ bundle "config list"
+
+ expect(out).to include("Settings are listed in order of priority. The top value will be used")
+ expect(out).to include("foo\nSet for the current user")
+ expect(out).to include(": \"bar\"")
+ end
+
+ context "given --parseable flag" do
+ it "prints a minimal report of local and user configuration" do
+ bundle "config list --parseable"
+ expect(out).to include("foo=bar")
+ end
+
+ context "with global config" do
+ it "prints config assigned to local scope" do
+ bundle "config set --local foo bar2"
+ bundle "config list --parseable"
+ expect(out).to include("foo=bar2")
+ end
+ end
+
+ context "with env overwrite" do
+ it "prints config with env" do
+ bundle "config list --parseable", env: { "BUNDLE_FOO" => "bar3" }
+ expect(out).to include("foo=bar3")
+ end
+ end
+ end
+ end
+
+ describe "location with a gemfile" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "is local by default" do
+ bundle "config set foo bar"
+ expect(bundled_app(".bundle/config")).to exist
+ expect(home(".bundle/config")).not_to exist
+ end
+
+ it "can be moved with an environment variable" do
+ ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s
+ bundle "config set --local path vendor/bundle"
+ bundle "install"
+
+ expect(bundled_app(".bundle")).not_to exist
+ expect(tmp("foo/bar/config")).to exist
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "can provide a relative path with the environment variable" do
+ FileUtils.mkdir_p bundled_app("omg")
+
+ ENV["BUNDLE_APP_CONFIG"] = "../foo"
+ bundle "config set --local path vendor/bundle"
+ bundle "install", dir: bundled_app("omg")
+
+ expect(bundled_app(".bundle")).not_to exist
+ expect(bundled_app("../foo/config")).to exist
+ expect(the_bundle).to include_gems "myrack 1.0.0", dir: bundled_app("omg")
+ end
+ end
+
+ describe "location without a gemfile" do
+ it "is global by default" do
+ bundle "config set foo bar"
+ expect(bundled_app(".bundle/config")).not_to exist
+ expect(home(".bundle/config")).to exist
+ end
+
+ it "does not list global settings as local" do
+ bundle "config set --global foo bar"
+ bundle "config list", dir: home
+
+ expect(out).to include("for the current user")
+ expect(out).not_to include("for your local app")
+ end
+
+ it "works with an absolute path" do
+ ENV["BUNDLE_APP_CONFIG"] = tmp("foo/bar").to_s
+ bundle "config set --local path vendor/bundle"
+
+ expect(bundled_app(".bundle")).not_to exist
+ expect(tmp("foo/bar/config")).to exist
+ end
+ end
+
+ describe "config location" do
+ let(:bundle_user_config) { File.join(Dir.home, ".config/bundler") }
+
+ before do
+ Dir.mkdir File.dirname(bundle_user_config)
+ end
+
+ it "can be configured through BUNDLE_USER_CONFIG" do
+ bundle "config set path vendor", env: { "BUNDLE_USER_CONFIG" => bundle_user_config }
+ bundle "config get path", env: { "BUNDLE_USER_CONFIG" => bundle_user_config }
+ expect(out).to include("Set for the current user (#{bundle_user_config}): \"vendor\"")
+ end
+
+ context "when not explicitly configured, but BUNDLE_USER_HOME set" do
+ let(:bundle_user_home) { bundled_app(".bundle").to_s }
+
+ it "uses the right location" do
+ bundle "config set path vendor", env: { "BUNDLE_USER_HOME" => bundle_user_home }
+ bundle "config get path", env: { "BUNDLE_USER_HOME" => bundle_user_home }
+ expect(out).to include("Set for the current user (#{bundle_user_home}/config): \"vendor\"")
+ end
+ end
+ end
+
+ describe "global" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "is the default" do
+ bundle "config set foo global"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("global")
+ end
+
+ it "can also be set explicitly" do
+ bundle "config set --global foo global"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("global")
+ end
+
+ it "has lower precedence than local" do
+ bundle "config set --local foo local"
+
+ bundle "config set --global foo global"
+ expect(out).to match(/Your application has set foo to "local"/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ end
+
+ it "has lower precedence than env" do
+ ENV["BUNDLE_FOO"] = "env"
+
+ bundle "config set --global foo global"
+ expect(out).to match(/You have a bundler environment variable for foo set to "env"/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("env")
+ ensure
+ ENV.delete("BUNDLE_FOO")
+ end
+
+ it "can be deleted" do
+ bundle "config set --global foo global"
+ bundle "config unset foo"
+
+ run "puts Bundler.settings[:foo] == nil"
+ expect(out).to eq("true")
+ end
+
+ it "warns when overriding" do
+ bundle "config set --global foo previous"
+ bundle "config set --global foo global"
+ expect(out).to match(/You are replacing the current global value of foo/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("global")
+ end
+
+ it "does not warn when using the same value twice" do
+ bundle "config set --global foo value"
+ bundle "config set --global foo value"
+ expect(out).not_to match(/You are replacing the current global value of foo/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("value")
+ end
+
+ it "expands the path at time of setting" do
+ bundle "config set --global local.foo .."
+ run "puts Bundler.settings['local.foo']"
+ expect(out).to eq(File.expand_path(bundled_app.to_s + "/.."))
+ end
+
+ it "saves with parseable option" do
+ bundle "config set --global --parseable foo value"
+ expect(out).to eq("foo=value")
+ run "puts Bundler.settings['foo']"
+ expect(out).to eq("value")
+ end
+
+ context "when replacing a current value with the parseable flag" do
+ before { bundle "config set --global foo value" }
+ it "prints the current value in a parseable format" do
+ bundle "config set --global --parseable foo value2"
+ expect(out).to eq "foo=value2"
+ run "puts Bundler.settings['foo']"
+ expect(out).to eq("value2")
+ end
+ end
+ end
+
+ describe "local" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "can also be set explicitly" do
+ bundle "config set --local foo local"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ end
+
+ it "has higher precedence than env" do
+ ENV["BUNDLE_FOO"] = "env"
+ bundle "config set --local foo local"
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ ensure
+ ENV.delete("BUNDLE_FOO")
+ end
+
+ it "can be deleted" do
+ bundle "config set --local foo local"
+ bundle "config unset foo"
+
+ run "puts Bundler.settings[:foo] == nil"
+ expect(out).to eq("true")
+ end
+
+ it "warns when overriding" do
+ bundle "config set --local foo previous"
+ bundle "config set --local foo local"
+ expect(out).to match(/You are replacing the current local value of foo/)
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("local")
+ end
+
+ it "expands the path at time of setting" do
+ bundle "config set --local local.foo .."
+ run "puts Bundler.settings['local.foo']"
+ expect(out).to eq(File.expand_path(bundled_app.to_s + "/.."))
+ end
+
+ it "can be deleted with parseable option" do
+ bundle "config set --local foo value"
+ bundle "config unset --parseable foo"
+ expect(out).to eq ""
+ run "puts Bundler.settings['foo'] == nil"
+ expect(out).to eq("true")
+ end
+ end
+
+ describe "env" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "can set boolean properties via the environment" do
+ ENV["BUNDLE_FROZEN"] = "true"
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("true")
+ end
+
+ it "can set negative boolean properties via the environment" do
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+
+ ENV["BUNDLE_FROZEN"] = "false"
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+
+ ENV["BUNDLE_FROZEN"] = "0"
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+
+ ENV["BUNDLE_FROZEN"] = ""
+
+ run "if Bundler.settings[:frozen]; puts 'true' else puts 'false' end"
+ expect(out).to eq("false")
+ end
+
+ it "can set properties with periods via the environment" do
+ ENV["BUNDLE_FOO__BAR"] = "baz"
+
+ run "puts Bundler.settings['foo.bar']"
+ expect(out).to eq("baz")
+ end
+ end
+
+ describe "parseable option" do
+ it "prints an empty string" do
+ bundle "config get foo --parseable", raise_on_error: false
+
+ expect(out).to eq ""
+ expect(last_command).to be_failure
+ end
+
+ it "only prints the value of the config" do
+ bundle "config set foo local"
+ bundle "config get foo --parseable"
+
+ expect(out).to eq "foo=local"
+ end
+
+ it "can print global config" do
+ bundle "config set --global bar value"
+ bundle "config get bar --parseable"
+
+ expect(out).to eq "bar=value"
+ end
+
+ it "prefers local config over global" do
+ bundle "config set --local bar value2"
+ bundle "config set --global bar value"
+ bundle "config get bar --parseable"
+
+ expect(out).to eq "bar=value2"
+ end
+ end
+
+ describe "gem mirrors" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ it "configures mirrors using keys with `mirror.`" do
+ bundle "config set --local mirror.http://gems.example.org http://gem-mirror.example.org"
+ run(<<-E)
+Bundler.settings.gem_mirrors.each do |k, v|
+ puts "\#{k} => \#{v}"
+end
+E
+ expect(out).to eq("http://gems.example.org/ => http://gem-mirror.example.org/")
+ end
+
+ it "allows configuring fallback timeout for each mirror, and does not duplicate configs", rubygems: ">= 3.5.12" do
+ bundle "config set --global mirror.https://rubygems.org.fallback_timeout 1"
+ bundle "config set --global mirror.https://rubygems.org.fallback_timeout 2"
+ expect(File.read(home(".bundle/config"))).to include("BUNDLE_MIRROR").once
+ end
+ end
+
+ describe "quoting" do
+ before(:each) { gemfile "source 'https://gem.repo1'" }
+ let(:long_string) do
+ "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \
+ "--with-xslt-dir=/usr/pkg"
+ end
+
+ it "saves quotes" do
+ bundle "config set foo something\\'"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("something'")
+ end
+
+ it "doesn't return quotes around values" do
+ bundle "config set foo '1'"
+ run "puts Bundler.settings.send(:local_config_file).read"
+ expect(out).to include('"1"')
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq("1")
+ end
+
+ it "doesn't duplicate quotes around values" do
+ bundled_app(".bundle").mkpath
+ File.open(bundled_app(".bundle/config"), "w") do |f|
+ f.write 'BUNDLE_FOO: "$BUILD_DIR"'
+ end
+
+ bundle "config set bar baz"
+ run "puts Bundler.settings.send(:local_config_file).read"
+
+ # Starting in Ruby 2.1, YAML automatically adds double quotes
+ # around some values, including $ and newlines.
+ expect(out).to include('BUNDLE_FOO: "$BUILD_DIR"')
+ end
+
+ it "doesn't duplicate quotes around long wrapped values" do
+ bundle "config set foo #{long_string}"
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq(long_string)
+
+ bundle "config set bar baz"
+
+ run "puts Bundler.settings[:foo]"
+ expect(out).to eq(long_string)
+ end
+ end
+
+ describe "very long lines" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+ end
+
+ let(:long_string) do
+ "--with-xml2-include=/usr/pkg/include/libxml2 --with-xml2-lib=/usr/pkg/lib " \
+ "--with-xslt-dir=/usr/pkg"
+ end
+
+ let(:long_string_without_special_characters) do
+ "here is quite a long string that will wrap to a second line but will not be " \
+ "surrounded by quotes"
+ end
+
+ it "doesn't wrap values" do
+ bundle "config set foo #{long_string}"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to match(long_string)
+ end
+
+ it "can read wrapped unquoted values" do
+ bundle "config set foo #{long_string_without_special_characters}"
+ run "puts Bundler.settings[:foo]"
+ expect(out).to match(long_string_without_special_characters)
+ end
+ end
+
+ describe "commented out settings with urls" do
+ before do
+ bundle "config set #mirror.https://rails-assets.org http://localhost:9292"
+ end
+
+ it "does not make bundler crash and ignores the configuration" do
+ bundle "config list --parseable"
+
+ expect(out).to be_empty
+ expect(err).to be_empty
+
+ ruby(<<~RUBY)
+ require "bundler"
+ print Bundler.settings.mirror_for("https://rails-assets.org")
+ RUBY
+ expect(out).to eq("https://rails-assets.org/")
+ expect(err).to be_empty
+
+ bundle "config set mirror.all http://localhost:9293"
+ ruby(<<~RUBY)
+ require "bundler"
+ print Bundler.settings.mirror_for("https://rails-assets.org")
+ RUBY
+ expect(out).to eq("http://localhost:9293/")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "subcommands" do
+ it "list" do
+ bundle "config list", env: { "BUNDLE_FOO" => "bar" }
+ expect(out).to eq "Settings are listed in order of priority. The top value will be used.\nfoo\nSet via BUNDLE_FOO: \"bar\""
+
+ bundle "config list", env: { "BUNDLE_FOO" => "bar" }, parseable: true
+ expect(out).to eq "foo=bar"
+ end
+
+ it "list with credentials" do
+ bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" }
+ expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"user:[REDACTED]\""
+
+ bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "user:password" }
+ expect(out).to eq "gems.myserver.com=user:password"
+ end
+
+ it "list with API token credentials" do
+ bundle "config list", env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" }
+ expect(out).to eq "Settings are listed in order of priority. The top value will be used.\ngems.myserver.com\nSet via BUNDLE_GEMS__MYSERVER__COM: \"[REDACTED]:x-oauth-basic\""
+
+ bundle "config list", parseable: true, env: { "BUNDLE_GEMS__MYSERVER__COM" => "api_token:x-oauth-basic" }
+ expect(out).to eq "gems.myserver.com=api_token:x-oauth-basic"
+ end
+
+ it "get" do
+ ENV["BUNDLE_BAR"] = "bar_val"
+
+ bundle "config get foo", raise_on_error: false
+ expect(out).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ ENV["BUNDLE_FOO"] = "foo_val"
+
+ bundle "config get foo --parseable"
+ expect(out).to eq "foo=foo_val"
+
+ bundle "config get foo"
+ expect(out).to eq "Settings for `foo` in order of priority. The top value will be used\nSet via BUNDLE_FOO: \"foo_val\""
+ end
+
+ it "set" do
+ bundle "config set foo 1"
+ expect(out).to eq ""
+
+ bundle "config set --local foo 2"
+ expect(out).to eq ""
+
+ bundle "config set --global foo 3"
+ expect(out).to eq "Your application has set foo to \"2\". This will override the global value you are currently setting"
+
+ bundle "config set --parseable --local foo 4"
+ expect(out).to eq "foo=4"
+
+ bundle "config set --local foo 4.1"
+ expect(out).to eq "You are replacing the current local value of foo, which is currently \"4\""
+
+ bundle "config set --global --local foo 5", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to eq "The options global and local were specified. Please only use one of the switches at a time."
+ end
+
+ it "unset" do
+ bundle "config unset foo"
+ expect(out).to eq ""
+
+ bundle "config set foo 1"
+ bundle "config unset foo --parseable"
+ expect(out).to eq ""
+
+ bundle "config set --local foo 1"
+ bundle "config set --global foo 2"
+
+ bundle "config unset foo"
+ expect(out).to eq ""
+ expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ bundle "config set --local foo 1"
+ bundle "config set --global foo 2"
+
+ bundle "config unset foo --local"
+ expect(out).to eq ""
+ expect(bundle("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for the current user (#{home(".bundle/config")}): \"2\""
+ bundle "config unset foo --global"
+ expect(out).to eq ""
+ expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ bundle "config set --local foo 1"
+ bundle "config set --global foo 2"
+
+ bundle "config unset foo --global"
+ expect(out).to eq ""
+ expect(bundle("config get foo")).to eq "Settings for `foo` in order of priority. The top value will be used\nSet for your local app (#{bundled_app(".bundle/config")}): \"1\""
+ bundle "config unset foo --local"
+ expect(out).to eq ""
+ expect(bundle("config get foo", raise_on_error: false)).to eq "Settings for `foo` in order of priority. The top value will be used\nYou have not configured a value for `foo`"
+ expect(last_command).to be_failure
+
+ bundle "config unset foo --local --global", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to eq "The options global and local were specified. Please only use one of the switches at a time."
+ end
+ end
+end
+
+RSpec.describe "setting gemfile via config" do
+ context "when a default Gemfile exists" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ gemfile bundled_app("foo/bar_gemfile"), <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "reports the local gemfile setting without promoting it to the environment" do
+ bundle "config set gemfile foo/bar_gemfile"
+
+ bundle "config list"
+ expect(out).to include("Set for your local app (#{bundled_app(".bundle/config")}): \"foo/bar_gemfile\"")
+ expect(out).not_to include("Set via BUNDLE_GEMFILE")
+ end
+
+ it "unsets the local gemfile setting from the app config" do
+ bundle "config set gemfile foo/bar_gemfile"
+
+ bundle "config unset gemfile"
+ bundle "config get gemfile", raise_on_error: false
+
+ expect(out).to include("You have not configured a value for `gemfile`")
+ expect(File.read(bundled_app(".bundle/config"))).not_to include("BUNDLE_GEMFILE")
+ end
+ end
+
+ context "when only the non-default Gemfile exists" do
+ it "persists the gemfile location to .bundle/config" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle "config set --local gemfile #{bundled_app("NotGemfile")}"
+ expect(File.exist?(bundled_app(".bundle/config"))).to eq(true)
+
+ bundle "config list"
+ expect(out).to include("NotGemfile")
+ end
+ end
+end
+
+RSpec.describe "setting lockfile via config" do
+ it "persists the lockfile location to .bundle/config" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle "config set --local gemfile #{bundled_app("NotGemfile")}"
+ bundle "config set --local lockfile #{bundled_app("ReallyNotGemfile.lock")}"
+ expect(File.exist?(bundled_app(".bundle/config"))).to eq(true)
+
+ bundle "config list"
+ expect(out).to include("NotGemfile")
+ expect(out).to include("ReallyNotGemfile.lock")
+ end
+end
diff --git a/spec/bundler/commands/console_spec.rb b/spec/bundler/commands/console_spec.rb
new file mode 100644
index 0000000000..a44f607546
--- /dev/null
+++ b/spec/bundler/commands/console_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle console", readline: true do
+ before :each do
+ build_repo2 do
+ # A minimal fake pry console
+ build_gem "pry" do |s|
+ s.write "lib/pry.rb", <<-RUBY
+ class Pry
+ class << self
+ def toplevel_binding
+ unless defined?(@toplevel_binding) && @toplevel_binding
+ TOPLEVEL_BINDING.eval %{
+ def self.__pry__; binding; end
+ Pry.instance_variable_set(:@toplevel_binding, __pry__)
+ class << self; undef __pry__; end
+ }
+ end
+ @toplevel_binding.eval('private')
+ @toplevel_binding
+ end
+
+ def __pry__
+ while line = gets
+ begin
+ puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1')
+ rescue Exception => e
+ puts "\#{e.class}: \#{e.message}"
+ puts e.backtrace.first
+ end
+ end
+ end
+ alias start __pry__
+ end
+ end
+ RUBY
+ end
+
+ build_dummy_irb
+ end
+ end
+
+ context "when the library requires a non-existent file" do
+ before do
+ build_lib "loadfuuu", "1.0.0" do |s|
+ s.write "lib/loadfuuu.rb", "require_relative 'loadfuuu/bar'"
+ s.write "lib/loadfuuu/bar.rb", "require 'not-in-bundle'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ path "#{lib_path}" do
+ gem "loadfuuu", require: true
+ end
+ G
+ end
+
+ it "does not show the bug report template" do
+ bundle("console", raise_on_error: false) do |input, _, _|
+ input.puts("exit")
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ end
+ end
+
+ context "when the library references a non-existent constant" do
+ before do
+ build_lib "loadfuuu", "1.0.0" do |s|
+ s.write "lib/loadfuuu.rb", "Some::NonExistent::Constant"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ path "#{lib_path}" do
+ gem "loadfuuu", require: true
+ end
+ G
+ end
+
+ it "does not show the bug report template" do
+ bundle("console", raise_on_error: false) do |input, _, _|
+ input.puts("exit")
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ end
+ end
+
+ context "when the library does not have any errors" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ gem "myrack_middleware", :group => :development
+ G
+ end
+
+ it "starts IRB with the default group loaded" do
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "uses IRB as default console" do
+ skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a")
+
+ bundle "console" do |input, _, _|
+ input.puts("__method__")
+ input.puts("exit")
+ end
+ expect(out).to include("__irb__")
+ end
+
+ it "starts another REPL if configured as such" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "pry"
+ G
+ bundle_config "console pry"
+
+ bundle "console" do |input, _, _|
+ input.puts("__method__")
+ input.puts("exit")
+ end
+ expect(out).to include(":__pry__")
+ end
+
+ it "falls back to IRB if the other REPL isn't available" do
+ skip "Does not work in a ruby-core context if irb is in the default $LOAD_PATH because it enables the real IRB, not our dummy one" if ruby_core? && Gem.ruby_version < Gem::Version.new("3.5.0.a")
+
+ bundle_config "console pry"
+ # make sure pry isn't there
+
+ bundle "console" do |input, _, _|
+ input.puts("__method__")
+ input.puts("exit")
+ end
+ expect(out).to include("__irb__")
+ end
+
+ it "does not try IRB twice if no console is configured and IRB is not available" do
+ create_file("irb.rb", "raise LoadError, 'irb is not available'")
+
+ bundle("console", env: { "RUBYOPT" => "-I#{bundled_app} #{ENV["RUBYOPT"]}" }, raise_on_error: false) do |input, _, _|
+ input.puts("puts ACTIVESUPPORT")
+ input.puts("exit")
+ end
+ expect(err).not_to include("falling back to irb")
+ expect(err).to include("irb is not available")
+ end
+
+ it "doesn't load any other groups" do
+ bundle "console" do |input, _, _|
+ input.puts("puts ACTIVESUPPORT")
+ input.puts("exit")
+ end
+ expect(out).to include("NameError")
+ end
+
+ describe "when given a group" do
+ it "loads the given group" do
+ bundle "console test" do |input, _, _|
+ input.puts("puts ACTIVESUPPORT")
+ input.puts("exit")
+ end
+ expect(out).to include("2.3.5")
+ end
+
+ it "loads the default group" do
+ bundle "console test" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "doesn't load other groups" do
+ bundle "console test" do |input, _, _|
+ input.puts("puts MYRACK_MIDDLEWARE")
+ input.puts("exit")
+ end
+ expect(out).to include("NameError")
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ gem "myrack_middleware", :group => :development
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :console do |input, _, _|
+ input.puts("puts 'hello'")
+ input.puts("exit")
+ end
+ expect(out).to include("Installing foo 1.0")
+ expect(out).to include("hello")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/commands/doctor_spec.rb b/spec/bundler/commands/doctor_spec.rb
new file mode 100644
index 0000000000..d350b4b3d1
--- /dev/null
+++ b/spec/bundler/commands/doctor_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require "find"
+require "stringio"
+require "bundler/cli"
+require "bundler/cli/doctor"
+require "bundler/cli/doctor/diagnose"
+
+RSpec.describe "bundle doctor" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ @stdout = StringIO.new
+
+ [:error, :warn, :info].each do |method|
+ allow(Bundler.ui).to receive(method).and_wrap_original do |m, message|
+ m.call message
+ @stdout.puts message
+ end
+ end
+ end
+
+ it "succeeds on a sane installation" do
+ bundle :doctor
+ end
+
+ context "when all files in home are readable/writable" do
+ before(:each) do
+ stat = double("stat")
+ unwritable_file = double("file")
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [unwritable_file] }
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:writable?).and_call_original
+ allow(File).to receive(:readable?).and_call_original
+ allow(File).to receive(:exist?).with(unwritable_file).and_return(true)
+ allow(File).to receive(:stat).with(unwritable_file) { stat }
+ allow(stat).to receive(:uid) { Process.uid }
+ allow(File).to receive(:writable?).with(unwritable_file) { true }
+ allow(File).to receive(:readable?).with(unwritable_file) { true }
+
+ # The following lines are for `Gem::PathSupport#initialize`.
+ allow(File).to receive(:exist?).with(Gem.default_dir)
+ allow(File).to receive(:writable?).with(Gem.default_dir)
+ allow(File).to receive(:writable?).with(File.expand_path("..", Gem.default_dir))
+ end
+
+ it "exits with a success message if the installed gem has no C extensions" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include("No issues")
+ end
+
+ it "exits with a success message if the installed gem's C extension dylib breakage is fine" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/myrack/myrack.bundle"]
+ expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/lib/libSystem.dylib"]
+ allow(doctor).to receive(:lookup_with_fiddle).with("/usr/lib/libSystem.dylib").and_return(false)
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include("No issues")
+ end
+
+ it "parses otool output correctly" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:`).with("/usr/bin/otool -L fake").and_return("/home/gem/ruby/3.4.3/gems/blake3-rb-1.5.4.4/lib/digest/blake3/blake3_ext.bundle:\n\t (compatibility version 0.0.0, current version 0.0.0)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)")
+ expect(doctor.dylibs_darwin("fake")).to eq(["/usr/lib/libSystem.B.dylib"])
+ end
+
+ it "exits with a message if one of the linked libraries is missing" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/myrack/myrack.bundle"]
+ expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"]
+ allow(doctor).to receive(:lookup_with_fiddle).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(true)
+ expect { doctor.run }.to raise_error(Bundler::ProductionError, <<~E.strip), @stdout.string
+ The following gems are missing OS dependencies:
+ * bundler: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib
+ * myrack: /usr/local/opt/icu4c/lib/libicui18n.57.1.dylib
+ E
+ end
+ end
+
+ context "when home contains broken symlinks" do
+ before(:each) do
+ @broken_symlink = double("file")
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@broken_symlink] }
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(@broken_symlink) { false }
+ end
+
+ it "exits with an error if home contains files that are not readable/writable" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Broken links exist in the Bundler home. Please report them to the offending gem's upstream repo. These files are:\n - #{@broken_symlink}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+ end
+
+ context "when home contains files that are not readable/writable" do
+ before(:each) do
+ @stat = double("stat")
+ @unwritable_file = double("file")
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ allow(Find).to receive(:find).with(Bundler.bundle_path.to_s) { [@unwritable_file] }
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:writable?).and_call_original
+ allow(File).to receive(:readable?).and_call_original
+ allow(File).to receive(:exist?).with(@unwritable_file) { true }
+ allow(File).to receive(:stat).with(@unwritable_file) { @stat }
+ end
+
+ it "exits with an error if home contains files that are not readable" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(@stat).to receive(:uid) { Process.uid }
+ allow(File).to receive(:writable?).with(@unwritable_file) { false }
+ allow(File).to receive(:readable?).with(@unwritable_file) { false }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Files exist in the Bundler home that are not readable by the current user. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+
+ it "exits without an error if home contains files that are not writable" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(@stat).to receive(:uid) { Process.uid }
+ allow(File).to receive(:writable?).with(@unwritable_file) { false }
+ allow(File).to receive(:readable?).with(@unwritable_file) { true }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).not_to include(
+ "Files exist in the Bundler home that are not readable by the current user. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).to include("No issues")
+ end
+
+ context "when home contains files that are not owned by the current process", :permissions do
+ before(:each) do
+ allow(@stat).to receive(:uid) { 0o0000 }
+ end
+
+ it "exits with an error if home contains files that are not readable/writable and are not owned by the current user" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(File).to receive(:writable?).with(@unwritable_file) { false }
+ allow(File).to receive(:readable?).with(@unwritable_file) { false }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Files exist in the Bundler home that are owned by another user, and are not readable. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+
+ it "exits with a warning if home contains files that are read/write but not owned by current user" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ allow(doctor).to receive(:lookup_with_fiddle).and_return(false)
+ allow(File).to receive(:writable?).with(@unwritable_file) { true }
+ allow(File).to receive(:readable?).with(@unwritable_file) { true }
+ expect { doctor.run }.not_to raise_error
+ expect(@stdout.string).to include(
+ "Files exist in the Bundler home that are owned by another user, but are still readable. These files are:\n - #{@unwritable_file}"
+ )
+ expect(@stdout.string).not_to include("No issues")
+ end
+ end
+ end
+
+ context "when home contains filenames with special characters" do
+ it "escape filename before command execute" do
+ doctor = Bundler::CLI::Doctor::Diagnose.new({})
+ expect(doctor).to receive(:`).with("/usr/bin/otool -L \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string")
+ doctor.dylibs_darwin('$(date) "\'\.bundle')
+ expect(doctor).to receive(:`).with("/usr/bin/ldd \\$\\(date\\)\\ \\\"\\'\\\\.bundle").and_return("dummy string")
+ doctor.dylibs_ldd('$(date) "\'\.bundle')
+ end
+ end
+end
diff --git a/spec/bundler/commands/exec_spec.rb b/spec/bundler/commands/exec_spec.rb
new file mode 100644
index 0000000000..aa35685be8
--- /dev/null
+++ b/spec/bundler/commands/exec_spec.rb
@@ -0,0 +1,1272 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle exec" do
+ it "works with --gemfile flag" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile "CustomGemfile", <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+
+ bundle "exec --gemfile CustomGemfile myrackup"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "activates the correct gem" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle "exec myrackup"
+ expect(out).to eq("0.9.1")
+ end
+
+ it "works and prints no warnings when HOME is not writable" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle "exec myrackup", env: { "HOME" => "/" }
+ expect(out).to eq("0.9.1")
+ expect(err).to be_empty
+ end
+
+ it "works when the bins are in ~/.bundle" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec myrackup"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works when running from a random directory" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec 'cd #{tmp("gems")} && myrackup'"
+
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works when exec'ing something else" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec echo exec"
+ expect(out).to eq("exec")
+ end
+
+ it "works when exec'ing to ruby" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec ruby -e 'puts %{hi}'"
+ expect(out).to eq("hi")
+ end
+
+ it "works when exec'ing to rubygems" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec #{gem_cmd} --version"
+ expect(out).to eq(Gem::VERSION)
+ end
+
+ it "works when exec'ing to rubygems through sh -c" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec sh -c '#{gem_cmd} --version'"
+ expect(out).to eq(Gem::VERSION)
+ end
+
+ it "works when exec'ing back to bundler to run a remote resolve" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle "exec bundle lock", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+
+ expect(out).to include("Writing lockfile")
+ end
+
+ it "respects custom process title when loading through ruby" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility = <<~'RUBY'
+ Process.setproctitle("1-2-3-4-5-6-7")
+ puts `ps -ocommand= -p#{$$}`
+ RUBY
+ gemfile "Gemfile", "source \"https://gem.repo1\""
+ create_file "a.rb", script_that_changes_its_own_title_and_checks_if_picked_up_by_ps_unix_utility
+ bundle "exec ruby a.rb"
+ expect(out).to eq("1-2-3-4-5-6-7")
+ end
+
+ it "accepts --verbose" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec --verbose echo foobar"
+ expect(out).to eq("foobar")
+ end
+
+ it "passes --verbose to command if it is given after the command" do
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ bundle "exec echo --verbose"
+ expect(out).to eq("--verbose")
+ end
+
+ it "handles --keep-file-descriptors" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ require "tempfile"
+
+ command = Tempfile.new("io-test")
+ command.sync = true
+ command.write <<-G
+ if ARGV[0]
+ IO.for_fd(ARGV[0].to_i)
+ else
+ require 'tempfile'
+ io = Tempfile.new("io-test-fd")
+ args = %W[#{Gem.ruby} -I#{lib_dir} #{bindir.join("bundle")} exec --keep-file-descriptors #{Gem.ruby} #{command.path} \#{io.to_i}]
+ args << { io.to_i => io }
+ exec(*args)
+ end
+ G
+
+ install_gemfile "source \"https://gem.repo1\""
+ in_bundled_app "#{Gem.ruby} #{command.path}"
+
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+
+ it "accepts --keep-file-descriptors" do
+ install_gemfile "source \"https://gem.repo1\""
+ bundle "exec --keep-file-descriptors echo foobar"
+
+ expect(err).to be_empty
+ end
+
+ it "can run a command named --verbose" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ install_gemfile "source \"https://gem.repo1\"; gem \"myrack\""
+ File.open(bundled_app("--verbose"), "w") do |f|
+ f.puts "#!/bin/sh"
+ f.puts "echo foobar"
+ end
+ File.chmod(0o744, bundled_app("--verbose"))
+ with_path_as(".") do
+ bundle "exec -- --verbose"
+ end
+ expect(out).to eq("foobar")
+ end
+
+ it "handles different versions in different bundles" do
+ build_repo2 do
+ build_gem "myrack_two", "1.0.0" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2
+ source "https://gem.repo2"
+ gem "myrack_two", "1.0.0"
+ G
+
+ bundle "exec myrackup"
+
+ expect(out).to eq("0.9.1")
+
+ bundle "exec myrackup", dir: bundled_app2
+ expect(out).to eq("1.0.0")
+ end
+
+ context "with default gems" do
+ # TODO: Switch to ERB::VERSION once Ruby 3.4 support is dropped, so all
+ # supported rubies include an `erb` gem version where `ERB::VERSION` is
+ # public
+ let(:default_erb_version) { ruby "require 'erb/version'; puts ERB.const_get(:VERSION)" }
+
+ context "when not specified in Gemfile" do
+ before do
+ install_gemfile "source \"https://gem.repo1\""
+ end
+
+ it "uses version provided by ruby" do
+ bundle "exec erb --version"
+
+ expect(stdboth).to eq(default_erb_version)
+ end
+ end
+
+ context "when specified in Gemfile directly" do
+ let(:specified_erb_version) { "2.0.0" }
+
+ before do
+ build_repo2 do
+ build_gem "erb", specified_erb_version do |s|
+ s.executables = "erb"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "erb", "#{specified_erb_version}"
+ G
+ end
+
+ it "uses version specified" do
+ bundle "exec erb --version"
+
+ expect(stdboth).to eq(specified_erb_version)
+ end
+ end
+
+ context "when specified in Gemfile indirectly" do
+ let(:indirect_erb_version) { "2.0.0" }
+
+ before do
+ build_repo2 do
+ build_gem "erb", indirect_erb_version do |s|
+ s.executables = "erb"
+ end
+
+ build_gem "gem_depending_on_old_erb" do |s|
+ s.add_dependency "erb", indirect_erb_version
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "gem_depending_on_old_erb"
+ G
+ end
+
+ it "uses resolved version" do
+ bundle "exec erb --version"
+
+ expect(stdboth).to eq(indirect_erb_version)
+ end
+ end
+ end
+
+ it "warns about executable conflicts" do
+ build_repo2 do
+ build_gem "myrack_two", "1.0.0" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ bundle_config_global "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2
+ source "https://gem.repo2"
+ gem "myrack_two", "1.0.0"
+ G
+
+ bundle "exec myrackup"
+
+ expect(last_command.stderr).to eq(
+ "Bundler is using a binstub that was created for a different gem (myrack).\n" \
+ "You should run `bundle binstub myrack_two` to work around a system/bundle conflict."
+ )
+ end
+
+ it "handles gems installed with --without" do
+ bundle_config "without middleware"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack" # myrack 0.9.1 and 1.0 exist
+
+ group :middleware do
+ gem "myrack_middleware" # myrack_middleware depends on myrack 0.9.1
+ end
+ G
+
+ bundle "exec myrackup"
+
+ expect(out).to eq("0.9.1")
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ end
+
+ it "does not duplicate already exec'ed RUBYOPT" do
+ create_file("echoopt", "#!/usr/bin/env ruby\nprint ENV['RUBYOPT']")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundler_setup_opt = "-r#{lib_dir}/bundler/setup"
+
+ rubyopt = opt_add(bundler_setup_opt, ENV["RUBYOPT"])
+
+ bundle "exec echoopt"
+ expect(out.split(" ").count(bundler_setup_opt)).to eq(1)
+
+ bundle "exec echoopt", env: { "RUBYOPT" => rubyopt }
+ expect(out.split(" ").count(bundler_setup_opt)).to eq(1)
+ end
+
+ it "does not duplicate already exec'ed RUBYLIB" do
+ create_file("echolib", "#!/usr/bin/env ruby\nprint ENV['RUBYLIB']")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ rubylib = ENV["RUBYLIB"]
+ rubylib = rubylib.to_s.split(File::PATH_SEPARATOR).unshift lib_dir.to_s
+ rubylib = rubylib.uniq.join(File::PATH_SEPARATOR)
+
+ bundle "exec echolib"
+ expect(out).to include(rubylib)
+
+ bundle "exec echolib", env: { "RUBYLIB" => rubylib }
+ expect(out).to include(rubylib)
+ end
+
+ it "errors nicely when the argument doesn't exist" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec foobarbaz", raise_on_error: false
+ expect(exitstatus).to eq(127)
+ expect(err).to include("bundler: command not found: foobarbaz")
+ expect(err).to include("Install missing gem executables with `bundle install`")
+ end
+
+ it "errors nicely when the argument is not executable" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundled_app("foo").write("")
+ bundle "exec ./foo", raise_on_error: false
+ expect(exitstatus).to eq(126)
+ expect(err).to include("bundler: not executable: ./foo")
+ end
+
+ it "errors nicely when no arguments are passed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "exec", raise_on_error: false
+ expect(exitstatus).to eq(128)
+ expect(err).to include("bundler: exec needs a command to run")
+ end
+
+ it "raises a helpful error when exec'ing to something outside of the bundle" do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ bundle_config "clean false" # want to keep the myrackup binstub
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+ [true, false].each do |l|
+ bundle_config "disable_exec_load #{l}"
+ bundle "exec myrackup", raise_on_error: false
+ expect(err).to include "can't find executable myrackup for gem myrack. myrack is not currently included in the bundle, perhaps you meant to add it to your Gemfile?"
+ end
+ end
+
+ describe "with help flags" do
+ each_prefix = proc do |string, &blk|
+ 1.upto(string.length) {|l| blk.call(string[0, l]) }
+ end
+ each_prefix.call("exec") do |exec|
+ describe "when #{exec} is used" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ create_file("print_args", <<-'RUBY')
+ #!/usr/bin/env ruby
+ puts "args: #{ARGV.inspect}"
+ RUBY
+ end
+
+ it "shows executable's man page when --help is after the executable" do
+ bundle "#{exec} print_args --help"
+ expect(out).to eq('args: ["--help"]')
+ end
+
+ it "shows executable's man page when --help is after the executable and an argument" do
+ bundle "#{exec} print_args foo --help"
+ expect(out).to eq('args: ["foo", "--help"]')
+
+ bundle "#{exec} print_args foo bar --help"
+ expect(out).to eq('args: ["foo", "bar", "--help"]')
+
+ bundle "#{exec} print_args foo --help bar"
+ expect(out).to eq('args: ["foo", "--help", "bar"]')
+ end
+
+ it "shows executable's man page when the executable has a -" do
+ FileUtils.mv(bundled_app("print_args"), bundled_app("docker-template"))
+ FileUtils.mv(bundled_app("print_args.bat"), bundled_app("docker-template.bat")) if Gem.win_platform?
+ bundle "#{exec} docker-template build discourse --help"
+ expect(out).to eq('args: ["build", "discourse", "--help"]')
+ end
+
+ it "shows executable's man page when --help is after another flag" do
+ bundle "#{exec} print_args --bar --help"
+ expect(out).to eq('args: ["--bar", "--help"]')
+ end
+
+ it "uses executable's original behavior for -h" do
+ bundle "#{exec} print_args -h"
+ expect(out).to eq('args: ["-h"]')
+ end
+
+ it "shows bundle-exec's man page when --help is between exec and the executable" do
+ with_fake_man do
+ bundle "#{exec} --help echo"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when --help is before exec" do
+ with_fake_man do
+ bundle "--help #{exec}"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when -h is before exec" do
+ with_fake_man do
+ bundle "-h #{exec}"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when --help is after exec" do
+ with_fake_man do
+ bundle "#{exec} --help"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+
+ it "shows bundle-exec's man page when -h is after exec" do
+ with_fake_man do
+ bundle "#{exec} -h"
+ end
+ expect(out).to include(%(["#{man_dir}/bundle-exec.1"]))
+ end
+ end
+ end
+ end
+
+ describe "with gem executables" do
+ describe "run from a random directory" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec 'cd #{tmp("gems")} && myrackup'"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+ bundle "exec 'cd #{tmp("gems")} && myrackup'"
+ expect(out).to eq("1.0.0")
+ end
+ end
+
+ describe "from gems bundled via :path" do
+ before(:each) do
+ build_lib "fizz", path: home("fizz") do |s|
+ s.executables = "fizz"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "fizz", :path => "#{File.expand_path(home("fizz"))}"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec fizz"
+ expect(out).to eq("1.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+
+ bundle "exec fizz"
+ expect(out).to eq("1.0")
+ end
+ end
+
+ describe "from gems bundled via :git" do
+ before(:each) do
+ build_git "fizz_git" do |s|
+ s.executables = "fizz_git"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "fizz_git", :git => "#{lib_path("fizz_git-1.0")}"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec fizz_git"
+ expect(out).to eq("1.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+ bundle "exec fizz_git"
+ expect(out).to eq("1.0")
+ end
+ end
+
+ describe "from gems bundled via :git with no gemspec" do
+ before(:each) do
+ build_git "fizz_no_gemspec", gemspec: false do |s|
+ s.executables = "fizz_no_gemspec"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "fizz_no_gemspec", "1.0", :git => "#{lib_path("fizz_no_gemspec-1.0")}"
+ G
+ end
+
+ it "works when unlocked" do
+ bundle "exec fizz_no_gemspec"
+ expect(out).to eq("1.0")
+ end
+
+ it "works when locked" do
+ expect(the_bundle).to be_locked
+ bundle "exec fizz_no_gemspec"
+ expect(out).to eq("1.0")
+ end
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "exec myrackup", artifice: "compact_index"
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ it "performs an automatic bundle install with git gems" do
+ build_git "foo" do |s|
+ s.executables = "foo"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "exec foo", artifice: "compact_index"
+ expect(out).to include("Fetching myrack 0.9.1")
+ expect(out).to include("Fetching #{lib_path("foo-1.0")}")
+ expect(out.lines).to end_with("1.0")
+ end
+
+ it "loads the correct optparse when `auto_install` is set, and optparse is a dependency" do
+ build_repo4 do
+ build_gem "fastlane", "2.192.0" do |s|
+ s.executables = "fastlane"
+ s.add_dependency "optparse", "~> 999.999.999"
+ end
+
+ build_gem "optparse", "999.999.998"
+ build_gem "optparse", "999.999.999"
+ end
+
+ system_gems "optparse-999.999.998", gem_repo: gem_repo4
+
+ bundle_config "auto_install 1"
+ bundle_config "path vendor/bundle"
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "fastlane"
+ G
+
+ bundle "exec fastlane", artifice: "compact_index"
+ expect(out).to include("Installing optparse 999.999.999")
+ expect(out).to include("2.192.0")
+ end
+
+ describe "with gems bundled via :path with invalid gemspecs" do
+ it "outputs the gemspec validation errors" do
+ build_lib "foo"
+
+ gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s
+ File.open(gemspec, "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = 'foo'
+ s.version = '1.0'
+ s.summary = 'TODO: Add summary'
+ s.authors = 'Me'
+ s.rubygems_version = nil
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "exec erb", raise_on_error: false
+
+ expect(err).to match("The gemspec at #{lib_path("foo-1.0").join("foo.gemspec")} is not valid")
+ expect(err).to match(/missing value for attribute rubygems_version|rubygems_version must not be nil/)
+ end
+ end
+
+ describe "with gems bundled for deployment" do
+ it "works when calling bundler from another script" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ module Monkey
+ def bin_path(a,b,c)
+ raise Gem::GemNotFoundException.new('Fail')
+ end
+ end
+ Bundler.rubygems.extend(Monkey)
+ G
+ bundle_config "path.system true"
+ bundle "install"
+ bundle "exec ruby -e '`bundle -v`; puts $?.success?'", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(out).to match("true")
+ end
+ end
+
+ describe "bundle exec gem uninstall" do
+ before do
+ build_repo4 do
+ build_gem "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "foo"
+ G
+ end
+
+ it "works" do
+ bundle "exec #{gem_cmd} uninstall foo"
+ expect(out).to eq("Successfully uninstalled foo-1.0")
+ end
+ end
+
+ describe "running gem commands in presence of rubygems plugins" do
+ before do
+ build_repo4 do
+ build_gem "foo" do |s|
+ s.write "lib/rubygems_plugin.rb", "puts 'FAIL'"
+ end
+ end
+
+ system_gems "foo-1.0", path: default_bundle_path, gem_repo: gem_repo4
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ G
+ end
+
+ it "does not load plugins outside of the bundle" do
+ bundle "exec #{gem_cmd} -v"
+ expect(out).not_to include("FAIL")
+ end
+ end
+
+ context "`load`ing a ruby file instead of `exec`ing" do
+ let(:path) { bundled_app("ruby_executable") }
+ let(:shebang) { "#!/usr/bin/env ruby" }
+ let(:executable) { <<~RUBY.strip }
+ #{shebang}
+
+ require "myrack"
+ puts "EXEC: \#{caller.grep(/load/).empty? ? 'exec' : 'load'}"
+ puts "ARGS: \#{$0} \#{ARGV.join(' ')}"
+ puts "MYRACK: \#{MYRACK}"
+ if Gem.win_platform?
+ process_title = "ruby"
+ else
+ process_title = `ps -o args -p \#{Process.pid}`.split("\n", 2).last.strip
+ end
+ puts "PROCESS: \#{process_title}"
+ RUBY
+
+ before do
+ create_file(bundled_app(path), executable)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ let(:exec) { "EXEC: load" }
+ let(:args) { "ARGS: #{path} arg1 arg2" }
+ let(:myrack) { "MYRACK: 1.0.0" }
+ let(:process) do
+ if Gem.win_platform?
+ "PROCESS: ruby"
+ else
+ "PROCESS: #{path} arg1 arg2"
+ end
+ end
+ let(:exit_code) { 0 }
+ let(:expected) { [exec, args, myrack, process].join("\n") }
+ let(:expected_err) { "" }
+
+ subject { bundle "exec #{path} arg1 arg2", raise_on_error: false }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+
+ context "the executable exits explicitly" do
+ let(:executable) { super() << "\nexit #{exit_code}\nputs 'POST_EXIT'\n" }
+
+ context "with exit 0" do
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "with exit 99" do
+ let(:exit_code) { 99 }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+ end
+
+ context "the executable exits by SignalException" do
+ let(:executable) do
+ ex = super()
+ ex << "\n"
+ ex << "raise SignalException, 'SIGTERM'\n"
+ ex
+ end
+ let(:expected_err) { "" }
+ let(:exit_code) do
+ exit_status_for_signal(Signal.list["TERM"])
+ end
+
+ it "runs" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "the executable is empty" do
+ let(:executable) { "" }
+
+ let(:exit_code) { 0 }
+ let(:expected_err) { "#{path} is empty" }
+ let(:expected) { "" }
+
+ it "runs" do
+ # it's empty, so `create_file` won't add executable permission and bat scripts on Windows
+ bundled_app(path).chmod(0o755)
+ path.sub_ext(".bat").write <<~SCRIPT if Gem.win_platform?
+ @ECHO OFF
+ @"ruby.exe" "%~dpn0" %*
+ SCRIPT
+
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "the executable raises" do
+ let(:executable) { super() << "\nraise 'ERROR'" }
+ let(:exit_code) { 1 }
+ let(:expected_err) do
+ /\Abundler: failed to load command: #{Regexp.quote(path.to_s)} \(#{Regexp.quote(path.to_s)}\)\n#{Regexp.quote(path.to_s)}:[0-9]+:in [`']<top \(required\)>': ERROR \(RuntimeError\)/
+ end
+
+ it "runs like a normally executed executable" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to match(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "the executable raises an error without a backtrace" do
+ let(:executable) { super() << "\nclass Err < Exception\ndef backtrace; end;\nend\nraise Err" }
+ let(:exit_code) { 1 }
+ let(:expected_err) { "bundler: failed to load command: #{path} (#{path})\n#{system_gem_path("bin/bundle")}: Err (Err)" }
+ let(:expected) { super() }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the file uses the current ruby shebang" do
+ let(:shebang) { "#!#{Gem.ruby}" }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when Bundler.setup fails" do
+ before do
+ system_gems(%w[myrack-1.0.0 myrack-0.9.1], path: default_bundle_path)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', '2'
+ G
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ end
+
+ let(:exit_code) { Bundler::GemNotFound.new.status_code }
+ let(:expected) { "" }
+ let(:expected_err) { <<~EOS.strip }
+ Could not find gem 'myrack (= 2)' in locally installed gems.
+
+ The source contains the following gems matching 'myrack':
+ * myrack-0.9.1
+ * myrack-1.0.0
+ Run `bundle install` to install missing gems.
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when Bundler.setup fails and Gemfile is not the default" do
+ before do
+ gemfile "CustomGemfile", <<-G
+ source "https://gem.repo1"
+ gem 'myrack', '2'
+ G
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ ENV["BUNDLE_GEMFILE"] = "CustomGemfile"
+ ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil
+ end
+
+ let(:exit_code) { Bundler::GemNotFound.new.status_code }
+ let(:expected) { "" }
+
+ it "prints proper suggestion" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to include("Run `bundle install --gemfile CustomGemfile` to install missing gems.")
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the executable exits non-zero via at_exit" do
+ let(:executable) { super() + "\n\nat_exit { $! ? raise($!) : exit(1) }" }
+ let(:exit_code) { 1 }
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when disable_exec_load is set" do
+ let(:exec) { "EXEC: exec" }
+ let(:process) do
+ if Gem.win_platform?
+ "PROCESS: ruby"
+ else
+ "PROCESS: ruby #{path} arg1 arg2"
+ end
+ end
+
+ before do
+ bundle_config "disable_exec_load true"
+ end
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "regarding $0 and __FILE__" do
+ let(:executable) { super() + <<-'RUBY' }
+
+ puts "$0: #{$0.inspect}"
+ puts "__FILE__: #{__FILE__.inspect}"
+ RUBY
+
+ context "when the path is absolute" do
+ let(:expected) { super() + <<~EOS.chomp }
+
+ $0: #{path.to_s.inspect}
+ __FILE__: #{path.to_s.inspect}
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the path is relative" do
+ let(:path) { super().relative_path_from(bundled_app) }
+ let(:expected) { super() + <<~EOS.chomp }
+
+ $0: #{path.to_s.inspect}
+ __FILE__: #{path.to_s.inspect}
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+
+ context "when the path is relative with a leading ./" do
+ let(:path) { Pathname.new("./#{super().relative_path_from(bundled_app)}") }
+ let(:expected) { super() + <<~EOS.chomp }
+
+ $0: #{path.to_s.inspect}
+ __FILE__: #{File.expand_path(path, bundled_app).inspect}
+ EOS
+
+ it "runs" do
+ subject
+ expect(exitstatus).to eq(exit_code)
+ expect(err).to eq(expected_err)
+ expect(out).to eq(expected)
+ end
+ end
+ end
+
+ context "signal handling" do
+ let(:test_signals) do
+ open3_reserved_signals = %w[CHLD CLD PIPE]
+ reserved_signals = %w[SEGV BUS ILL FPE ABRT IOT VTALRM KILL STOP EXIT]
+ bundler_signals = %w[INT]
+
+ Signal.list.keys - (bundler_signals + reserved_signals + open3_reserved_signals)
+ end
+
+ context "signals being trapped by bundler" do
+ let(:executable) { <<~RUBY }
+ #{shebang}
+ begin
+ Thread.new do
+ puts 'Started' # For process sync
+ STDOUT.flush
+ sleep 1 # ignore quality_spec
+ raise RuntimeError, "Didn't receive expected INT"
+ end.join
+ rescue Interrupt
+ puts "foo"
+ end
+ RUBY
+
+ it "receives the signal" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ bundle("exec #{path}") do |_, o, thr|
+ o.gets # Consumes 'Started' and ensures that thread has started
+ Process.kill("INT", thr.pid)
+ end
+
+ expect(out).to eq("foo")
+ end
+ end
+
+ context "signals not being trapped by bunder" do
+ let(:executable) { <<~RUBY }
+ #{shebang}
+
+ signals = #{test_signals.inspect}
+ result = signals.map do |sig|
+ Signal.trap(sig, "IGNORE")
+ end
+ puts result.select { |ret| ret == "IGNORE" }.count
+ RUBY
+
+ it "makes sure no unexpected signals are restored to DEFAULT" do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ test_signals.each do |n|
+ Signal.trap(n, "IGNORE")
+ end
+
+ bundle("exec #{path}")
+
+ expect(out).to eq(test_signals.count.to_s)
+ end
+ end
+ end
+ end
+
+ context "nested bundle exec" do
+ context "when bundle in a local path" do
+ before do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle_config "path vendor/bundler"
+ bundle :install
+ end
+
+ it "correctly shells out" do
+ file = bundled_app("file_that_bundle_execs.rb")
+ create_file(file, <<-RUBY)
+ #!#{Gem.ruby}
+ puts `bundle exec echo foo`
+ RUBY
+ file.chmod(0o777)
+ bundle "exec #{file}", env: { "PATH" => path }
+ expect(out).to eq("foo")
+ end
+ end
+
+ context "when Kernel.require uses extra monkeypatches" do
+ before do
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ install_gemfile "source \"https://gem.repo1\""
+ end
+
+ it "does not undo the monkeypatches" do
+ karafka = bundled_app("bin/karafka")
+ create_file(karafka, <<~RUBY)
+ #!#{Gem.ruby}
+
+ module Kernel
+ module_function
+
+ alias_method :require_before_extra_monkeypatches, :require
+
+ def require(path)
+ puts "requiring \#{path} used the monkeypatch"
+
+ require_before_extra_monkeypatches(path)
+ end
+ end
+
+ Bundler.setup(:default)
+
+ require "foo"
+ RUBY
+ karafka.chmod(0o777)
+
+ foreman = bundled_app("bin/foreman")
+ create_file(foreman, <<~RUBY)
+ #!#{Gem.ruby}
+
+ puts `bundle exec bin/karafka`
+ RUBY
+ foreman.chmod(0o777)
+
+ bundle "exec #{foreman}"
+ expect(out).to eq("requiring foo used the monkeypatch")
+ end
+ end
+
+ context "when gemfile and path are configured", :ruby_repo do
+ before do
+ build_repo2 do
+ build_gem "rails", "6.1.0" do |s|
+ s.executables = "rails"
+ end
+ end
+
+ bundle_config "path vendor/bundle"
+ bundle_config "gemfile gemfiles/myrack_6_1.gemfile"
+
+ gemfile(bundled_app("gemfiles/myrack_6_1.gemfile"), <<~RUBY)
+ source "https://gem.repo2"
+
+ gem "rails", "6.1.0"
+ RUBY
+
+ # A Gemfile needs to be in the root to trick bundler's root resolution
+ gemfile "source 'https://gem.repo1'"
+
+ bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ end
+
+ it "can still find gems after a nested subprocess" do
+ script = bundled_app("bin/myscript")
+
+ create_file(script, <<~RUBY)
+ #!#{Gem.ruby}
+
+ puts `bundle exec rails`
+ RUBY
+
+ script.chmod(0o777)
+
+ bundle "exec #{script}"
+
+ expect(err).to be_empty
+ expect(out).to eq("6.1.0")
+ end
+
+ it "can still find gems after a nested subprocess when using bundler (with a final r) executable" do
+ script = bundled_app("bin/myscript")
+
+ create_file(script, <<~RUBY)
+ #!#{Gem.ruby}
+
+ puts `bundler exec rails`
+ RUBY
+
+ script.chmod(0o777)
+
+ bundle "exec #{script}"
+
+ expect(err).to be_empty
+ expect(out).to eq("6.1.0")
+ end
+ end
+
+ context "with a system gem that shadows a default gem" do
+ let(:openssl_version) { "99.9.9" }
+
+ it "only leaves the default gem in the stdlib available" do
+ default_openssl_version = ruby "require 'openssl'; puts OpenSSL::VERSION"
+
+ skip "https://github.com/ruby/rubygems/issues/3351" if Gem.win_platform?
+
+ install_gemfile "source \"https://gem.repo1\"" # must happen before installing the broken system gem
+
+ build_repo4 do
+ build_gem "openssl", openssl_version do |s|
+ s.write("lib/openssl.rb", <<-RUBY)
+ raise ArgumentError, "custom openssl should not be loaded"
+ RUBY
+ end
+ end
+
+ system_gems("openssl-#{openssl_version}", gem_repo: gem_repo4)
+
+ file = bundled_app("require_openssl.rb")
+ create_file(file, <<-RUBY)
+ #!/usr/bin/env ruby
+ require "openssl"
+ puts OpenSSL::VERSION
+ warn Gem.loaded_specs.values.map(&:full_name)
+ RUBY
+ file.chmod(0o777)
+
+ env = { "PATH" => path }
+ aggregate_failures do
+ expect(bundle("exec #{file}", env: env)).to eq(default_openssl_version)
+ expect(bundle("exec bundle exec #{file}", env: env)).to eq(default_openssl_version)
+ expect(bundle("exec ruby #{file}", env: env)).to eq(default_openssl_version)
+ expect(run(file.read, artifice: nil, env: env)).to eq(default_openssl_version)
+ end
+
+ skip "ruby_core has openssl and rubygems in the same folder, and this test needs rubygems require but default openssl not in a directly added entry in $LOAD_PATH" if ruby_core?
+ # sanity check that we get the newer, custom version without bundler
+ sys_exec "#{Gem.ruby} #{file}", env: env, raise_on_error: false
+ expect(err).to include("custom openssl should not be loaded")
+ end
+ end
+
+ context "with a git gem that includes extensions", :ruby_repo do
+ before do
+ build_git "simple_git_binary", &:add_c_extension
+ bundle_config "path .bundle"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "simple_git_binary", :git => '#{lib_path("simple_git_binary-1.0")}'
+ G
+ end
+
+ it "allows calling bundle install" do
+ bundle "exec bundle install"
+ end
+
+ it "allows calling bundle install after removing gem.build_complete" do
+ FileUtils.rm_r Dir[bundled_app(".bundle/**/gem.build_complete")]
+ bundle "exec #{Gem.ruby} -S bundle install"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/commands/fund_spec.rb b/spec/bundler/commands/fund_spec.rb
new file mode 100644
index 0000000000..5883b8a63a
--- /dev/null
+++ b/spec/bundler/commands/fund_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle fund" do
+ before do
+ build_repo2 do
+ build_gem "has_funding_and_other_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+
+ build_gem "has_funding", "1.2.3" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/has_funding/funding",
+ }
+ end
+
+ build_gem "gem_with_dependent_funding", "1.0" do |s|
+ s.add_dependency "has_funding"
+ end
+ end
+ end
+
+ it "prints fund information for all gems in the bundle" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding")
+ expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("myrack-obama")
+ end
+
+ it "does not consider fund information for gem dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'gem_with_dependent_funding'
+ G
+
+ bundle "fund"
+
+ expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("gem_with_dependent_funding")
+ end
+
+ it "does not consider fund information for uninstalled optional dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ group :whatever, optional: true do
+ gem 'has_funding_and_other_metadata'
+ end
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("has_funding_and_other_metadata")
+ expect(out).to_not include("myrack-obama")
+ end
+
+ it "considers fund information for installed optional dependencies" do
+ bundle_config "with whatever"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ group :whatever, optional: true do
+ gem 'has_funding_and_other_metadata'
+ end
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding")
+ expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ expect(out).to_not include("myrack-obama")
+ end
+
+ it "prints message if none of the gems have fund information" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack-obama'
+ G
+
+ bundle "fund"
+
+ expect(out).to include("None of the installed gems you directly depend on are looking for funding.")
+ end
+
+ describe "with --group option" do
+ it "prints fund message for only specified group gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata', :group => :development
+ gem 'has_funding'
+ G
+
+ bundle "fund --group development"
+ expect(out).to include("* has_funding_and_other_metadata (1.0)\n Funding: https://example.com/has_funding_and_other_metadata/funding")
+ expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding")
+ end
+ end
+end
diff --git a/spec/bundler/commands/help_spec.rb b/spec/bundler/commands/help_spec.rb
new file mode 100644
index 0000000000..f9ad9fff14
--- /dev/null
+++ b/spec/bundler/commands/help_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle help" do
+ it "uses man when available" do
+ with_fake_man do
+ bundle "help gemfile"
+ end
+ expect(out).to eq(%(["#{man_dir}/gemfile.5"]))
+ end
+
+ it "prefixes bundle commands with bundle- when finding the man files" do
+ with_fake_man do
+ bundle "help install"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "prexifes bundle commands with bundle- and resolves aliases when finding the man files" do
+ with_fake_man do
+ bundle "help package"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-cache.1"]))
+ end
+
+ it "simply outputs the human readable file when there is no man on the path" do
+ with_path_as("") do
+ bundle "help install"
+ end
+ expect(out).to match(/bundle-install/)
+ end
+
+ it "looks for a binary and executes it with --help option if it's named bundler-<task>" do
+ skip "Could not find command testtasks, probably because not a windows friendly executable" if Gem.win_platform?
+
+ File.open(tmp("bundler-testtasks"), "w", 0o755) do |f|
+ f.puts "#!/usr/bin/env ruby\nputs ARGV.join(' ')\n"
+ end
+
+ with_path_added(tmp) do
+ bundle "help testtasks"
+ end
+
+ expect(out).to eq("--help")
+ end
+
+ it "is called when the --help flag is used after the command" do
+ with_fake_man do
+ bundle "install --help"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "is called when the --help flag is used before the command" do
+ with_fake_man do
+ bundle "--help install"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "is called when the -h flag is used before the command" do
+ with_fake_man do
+ bundle "-h install"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "is called when the -h flag is used after the command" do
+ with_fake_man do
+ bundle "install -h"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle-install.1"]))
+ end
+
+ it "has helpful output when using --help flag for a non-existent command" do
+ with_fake_man do
+ bundle "instill -h", raise_on_error: false
+ end
+ expect(err).to include('Could not find command "instill".')
+ end
+
+ it "is called when only using the --help flag" do
+ with_fake_man do
+ bundle "--help"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle.1"]))
+
+ with_fake_man do
+ bundle "-h"
+ end
+ expect(out).to eq(%(["#{man_dir}/bundle.1"]))
+ end
+end
diff --git a/spec/bundler/commands/info_spec.rb b/spec/bundler/commands/info_spec.rb
new file mode 100644
index 0000000000..a26b1696fb
--- /dev/null
+++ b/spec/bundler/commands/info_spec.rb
@@ -0,0 +1,249 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle info" do
+ context "with a standard Gemfile" do
+ before do
+ build_repo2 do
+ build_gem "has_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "has_metadata"
+ gem "thin"
+ G
+ end
+
+ it "creates a Gemfile.lock when invoked with a gem name" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "info rails"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "prints information if gem exists in bundle" do
+ bundle "info rails"
+ expect(out).to include "* rails (2.3.2)
+\tSummary: This is just a fake gem for testing
+\tHomepage: http://example.com
+\tPath: #{default_bundle_path("gems", "rails-2.3.2")}"
+ end
+
+ it "prints path if gem exists in bundle" do
+ bundle "info rails --path"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints the path to the running bundler" do
+ bundle "info bundler --path"
+ expect(out).to eq(root.to_s)
+ end
+
+ it "prints gem version if exists in bundle" do
+ bundle "info rails --version"
+ expect(out).to eq("2.3.2")
+ end
+
+ it "doesn't claim that bundler is missing, even if using a custom path without bundler there" do
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ bundle "info bundler"
+ expect(out).to include("\tPath: #{root}")
+ expect(err).not_to match(/The gem bundler is missing/i)
+ end
+
+ it "complains if gem not in bundle" do
+ bundle "info missing", raise_on_error: false
+ expect(err).to eq("Could not find gem 'missing'.")
+ end
+
+ it "warns if path does not exist on disk, but specification is there" do
+ FileUtils.rm_r(default_bundle_path("gems", "rails-2.3.2"))
+
+ bundle "info rails --path"
+
+ expect(err).to include("The gem rails is missing.")
+ expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+
+ bundle "info rail --path"
+ expect(err).to include("The gem rails is missing.")
+ expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+
+ bundle "info rails"
+ expect(err).to include("The gem rails is missing.")
+ expect(err).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ context "given a default gem shipped in ruby", :ruby_repo do
+ it "prints information about the default gem" do
+ bundle "info json"
+ expect(out).to include("* json")
+ expect(out).to include("Default Gem: yes")
+ end
+ end
+
+ context "given a gem with metadata" do
+ it "prints the gem metadata" do
+ bundle "info has_metadata"
+ expect(out).to include "* has_metadata (1.0)
+\tSummary: This is just a fake gem for testing
+\tHomepage: http://example.com
+\tDocumentation: https://www.example.info/gems/bestgemever/0.0.1
+\tSource Code: https://example.com/user/bestgemever
+\tWiki: https://example.com/user/bestgemever/wiki
+\tChangelog: https://example.com/user/bestgemever/CHANGELOG.md
+\tBug Tracker: https://example.com/user/bestgemever/issues
+\tMailing List: https://groups.example.com/bestgemever
+\tPath: #{default_bundle_path("gems", "has_metadata-1.0")}"
+ end
+ end
+
+ context "when gem does not have homepage" do
+ before do
+ build_repo2 do
+ build_gem "rails", "2.3.2" do |s|
+ s.executables = "rails"
+ s.summary = "Just another test gem"
+ end
+ end
+ end
+
+ it "excludes the homepage field from the output" do
+ expect(out).to_not include("Homepage:")
+ end
+ end
+
+ context "when gem has a reverse dependency on any version" do
+ it "prints the details" do
+ bundle "info myrack"
+
+ expect(out).to include("Reverse Dependencies: \n\t\tthin (1.0) depends on myrack (>= 0)")
+ end
+ end
+
+ context "when gem has a reverse dependency on a specific version" do
+ it "prints the details" do
+ bundle "info actionpack"
+
+ expect(out).to include("Reverse Dependencies: \n\t\trails (2.3.2) depends on actionpack (= 2.3.2)")
+ end
+ end
+
+ context "when gem has no reverse dependencies" do
+ it "excludes the reverse dependencies field from the output" do
+ bundle "info rails"
+
+ expect(out).not_to include("Reverse Dependencies:")
+ end
+ end
+ end
+
+ context "with a git repo in the Gemfile" do
+ before :each do
+ @git = build_git "foo", "1.0"
+ end
+
+ it "prints out git info" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}")
+ end
+
+ it "prints out branch names other than main" do
+ update_git "foo", branch: "omg" do |s|
+ s.write "lib/foo.rb", "FOO = '1.0.omg'"
+ end
+ @revision = revision_for(lib_path("foo-1.0"))[0...6]
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.omg"
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}")
+ end
+
+ it "doesn't print the branch when tied to a ref" do
+ sha = revision_for(lib_path("foo-1.0"))
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}"
+ G
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0 #{sha[0..6]})")
+ end
+
+ it "handles when a version is a '-' prerelease" do
+ @git = build_git("foo", "1.0.0-beta.1", path: lib_path("foo"))
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1"
+
+ bundle "info foo"
+ expect(out).to include("foo (1.0.0.pre.beta.1")
+ end
+ end
+
+ context "with a valid regexp for gem name" do
+ it "presents alternatives", :readline do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ bundle "info rac"
+ expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/)
+ end
+ end
+
+ context "with an invalid regexp for gem name" do
+ it "does not find the gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ invalid_regexp = "[]"
+
+ bundle "info #{invalid_regexp}", raise_on_error: false
+ expect(err).to include("Could not find gem '#{invalid_regexp}'.")
+ end
+ end
+
+ context "with without configured" do
+ it "does not find the gem, but gives a helpful error" do
+ bundle_config "without test"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", group: :test
+ G
+
+ bundle "info rails", raise_on_error: false
+ expect(err).to include("Could not find gem 'rails', because it's in the group 'test', configured to be ignored.")
+ end
+ end
+end
diff --git a/spec/bundler/commands/init_spec.rb b/spec/bundler/commands/init_spec.rb
new file mode 100644
index 0000000000..989d6fa812
--- /dev/null
+++ b/spec/bundler/commands/init_spec.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle init" do
+ it "generates a Gemfile" do
+ bundle :init
+ expect(out).to include("Writing new Gemfile")
+ expect(bundled_app_gemfile).to be_file
+ end
+
+ context "with a template with permission flags not matching current process umask" do
+ let(:template_file) do
+ gemfile = Bundler.preferred_gemfile_name
+ templates_dir.join(gemfile)
+ end
+
+ let(:target_dir) { bundled_app("init_permissions_test") }
+
+ around do |example|
+ old_chmod = File.stat(template_file).mode
+ FileUtils.chmod(old_chmod | 0o111, template_file) # chmod +x
+ example.run
+ FileUtils.chmod(old_chmod, template_file)
+ end
+
+ it "honours the current process umask when generating from a template" do
+ FileUtils.mkdir(target_dir)
+ bundle :init, dir: target_dir
+ generated_mode = File.stat(File.join(target_dir, "Gemfile")).mode & 0o111
+ expect(generated_mode).to be_zero
+ end
+ end
+
+ context "when a Gemfile already exists" do
+ before do
+ gemfile <<-G
+ gem "rails"
+ G
+ end
+
+ it "does not change existing Gemfiles" do
+ expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app_gemfile) }
+ end
+
+ it "notifies the user that an existing Gemfile already exists" do
+ bundle :init, raise_on_error: false
+ expect(err).to include("Gemfile already exists")
+ end
+ end
+
+ context "when a Gemfile exists in a parent directory" do
+ let(:subdir) { "child_dir" }
+
+ it "lets users generate a Gemfile in a child directory" do
+ bundle :init
+
+ FileUtils.mkdir bundled_app(subdir)
+
+ bundle :init, dir: bundled_app(subdir)
+
+ expect(out).to include("Writing new Gemfile")
+ expect(bundled_app("#{subdir}/Gemfile")).to be_file
+ end
+ end
+
+ context "when the dir is not writable by the current user" do
+ let(:subdir) { "child_dir" }
+
+ it "notifies the user that it cannot write to it" do
+ FileUtils.mkdir bundled_app(subdir)
+ # chmod a-w it
+ mode = File.stat(bundled_app(subdir)).mode ^ 0o222
+ FileUtils.chmod mode, bundled_app(subdir)
+
+ bundle :init, dir: bundled_app(subdir), raise_on_error: false
+
+ expect(err).to include("directory is not writable")
+ expect(Dir[bundled_app("#{subdir}/*")]).to be_empty
+ end
+ end
+
+ context "given --gemspec option" do
+ let(:spec_file) { tmp("test.gemspec") }
+
+ it "should generate from an existing gemspec" do
+ File.open(spec_file, "w") do |file|
+ file << <<-S
+ Gem::Specification.new do |s|
+ s.name = 'test'
+ s.add_dependency 'myrack', '= 1.0.1'
+ s.add_development_dependency 'rspec', '1.2'
+ end
+ S
+ end
+
+ bundle :init, gemspec: spec_file
+
+ gemfile = bundled_app_gemfile.read
+ expect(gemfile).to match(%r{source 'https://rubygems.org'})
+ expect(gemfile.scan(/gem "myrack", "= 1.0.1"/).size).to eq(1)
+ expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1)
+ expect(gemfile.scan(/group :development/).size).to eq(1)
+ end
+
+ context "when gemspec file is invalid" do
+ it "notifies the user that specification is invalid" do
+ File.open(spec_file, "w") do |file|
+ file << <<-S
+ Gem::Specification.new do |s|
+ s.name = 'test'
+ s.invalid_method_name
+ end
+ S
+ end
+
+ bundle :init, gemspec: spec_file, raise_on_error: false
+ expect(err).to include("There was an error while loading `test.gemspec`")
+ end
+ end
+ end
+
+ context "when init_gems_rb setting is enabled" do
+ before { bundle_config "init_gems_rb true" }
+
+ it "generates a gems.rb" do
+ bundle :init
+ expect(out).to include("Writing new gems.rb")
+ expect(bundled_app("gems.rb")).to be_file
+ end
+
+ context "when gems.rb already exists" do
+ before do
+ gemfile("gems.rb", <<-G)
+ gem "rails"
+ G
+ end
+
+ it "does not change existing Gemfiles" do
+ expect { bundle :init, raise_on_error: false }.not_to change { File.read(bundled_app("gems.rb")) }
+ end
+
+ it "notifies the user that an existing gems.rb already exists" do
+ bundle :init, raise_on_error: false
+ expect(err).to include("gems.rb already exists")
+ end
+ end
+
+ context "when a gems.rb file exists in a parent directory" do
+ let(:subdir) { "child_dir" }
+
+ it "lets users generate a Gemfile in a child directory" do
+ bundle :init
+
+ FileUtils.mkdir bundled_app(subdir)
+
+ bundle :init, dir: bundled_app(subdir)
+
+ expect(out).to include("Writing new gems.rb")
+ expect(bundled_app("#{subdir}/gems.rb")).to be_file
+ end
+ end
+
+ context "given --gemspec option" do
+ let(:spec_file) { tmp("test.gemspec") }
+
+ before do
+ File.open(spec_file, "w") do |file|
+ file << <<-S
+ Gem::Specification.new do |s|
+ s.name = 'test'
+ s.add_dependency 'myrack', '= 1.0.1'
+ s.add_development_dependency 'rspec', '1.2'
+ end
+ S
+ end
+ end
+
+ it "should generate from an existing gemspec" do
+ bundle :init, gemspec: spec_file
+
+ gemfile = bundled_app("gems.rb").read
+ expect(gemfile).to match(%r{source 'https://rubygems.org'})
+ expect(gemfile.scan(/gem "myrack", "= 1.0.1"/).size).to eq(1)
+ expect(gemfile.scan(/gem "rspec", "= 1.2"/).size).to eq(1)
+ expect(gemfile.scan(/group :development/).size).to eq(1)
+ end
+
+ it "prints message to user" do
+ bundle :init, gemspec: spec_file
+
+ expect(out).to include("Writing new gems.rb")
+ end
+ end
+ end
+
+ describe "using the --gemfile" do
+ it "should use the --gemfile value to name the gemfile" do
+ custom_gemfile_name = "NiceGemfileName"
+
+ bundle :init, gemfile: custom_gemfile_name
+
+ expect(out).to include("Writing new #{custom_gemfile_name}")
+ used_template = File.read("#{source_root}/lib/bundler/templates/Gemfile")
+ generated_gemfile = bundled_app(custom_gemfile_name).read
+ expect(generated_gemfile).to eq(used_template)
+ end
+ end
+end
diff --git a/spec/bundler/commands/install_spec.rb b/spec/bundler/commands/install_spec.rb
new file mode 100644
index 0000000000..3b24434dc7
--- /dev/null
+++ b/spec/bundler/commands/install_spec.rb
@@ -0,0 +1,2115 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with gem sources" do
+ describe "the simple case" do
+ it "prints output and returns if no dependencies are specified" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle :install
+ expect(err).to match(/no dependencies/)
+ end
+
+ it "does not make a lockfile if the install fails" do
+ install_gemfile <<-G, raise_on_error: false
+ raise StandardError, "FAIL"
+ G
+
+ expect(err).to include('StandardError, "FAIL"')
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "creates a Gemfile.lock" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "creates lockfile based on the lockfile method in Gemfile" do
+ install_gemfile <<-G
+ lockfile "OmgFile.lock"
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install"
+
+ expect(bundled_app("OmgFile.lock")).to exist
+ end
+
+ it "creates lockfile using BUNDLE_LOCKFILE instead of lockfile method" do
+ ENV["BUNDLE_LOCKFILE"] = "ReallyOmgFile.lock"
+ install_gemfile <<-G
+ lockfile "OmgFile.lock"
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ expect(bundled_app("ReallyOmgFile.lock")).to exist
+ expect(bundled_app("OmgFile.lock")).not_to exist
+ ensure
+ ENV.delete("BUNDLE_LOCKFILE")
+ end
+
+ it "creates lockfile based on --lockfile option is given" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile --lockfile ReallyOmgFile.lock"
+
+ expect(bundled_app("ReallyOmgFile.lock")).to exist
+ end
+
+ it "does not make a lockfile if lockfile false is used in Gemfile" do
+ install_gemfile <<-G
+ lockfile false
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "does not create ./.bundle by default" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app(".bundle")).not_to exist
+ end
+
+ it "will create a ./.bundle by default", bundler: "5" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app(".bundle")).to exist
+ end
+
+ it "does not create ./.bundle by default when installing to system gems" do
+ install_gemfile <<-G, env: { "BUNDLE_PATH__SYSTEM" => "true" }
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(bundled_app(".bundle")).not_to exist
+ end
+
+ it "creates lockfiles based on the Gemfile name" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile"
+
+ expect(bundled_app("OmgFile.lock")).to exist
+ end
+
+ it "doesn't create a lockfile if --no-lock option is given" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile --no-lock"
+
+ expect(bundled_app("OmgFile.lock")).not_to exist
+ end
+
+ it "doesn't create a lockfile if --no-lock and --lockfile options are given" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "install --gemfile OmgFile --no-lock --lockfile ReallyOmgFile.lock"
+
+ expect(bundled_app("OmgFile.lock")).not_to exist
+ expect(bundled_app("ReallyOmgFile.lock")).not_to exist
+ end
+
+ it "doesn't delete the lockfile if one already exists" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ lockfile = File.read(bundled_app_lock)
+
+ install_gemfile <<-G, raise_on_error: false
+ raise StandardError, "FAIL"
+ G
+
+ expect(File.read(bundled_app_lock)).to eq(lockfile)
+ end
+
+ it "does not touch the lockfile if nothing changed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect { run "1" }.not_to change { File.mtime(bundled_app_lock) }
+ end
+
+ it "fetches gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(default_bundle_path("gems/myrack-1.0.0")).to exist
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "auto-heals missing gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ FileUtils.rm_r(default_bundle_path("gems/myrack-1.0.0"))
+
+ bundle "install --verbose"
+
+ expect(out).to include("Installing myrack 1.0.0")
+ expect(default_bundle_path("gems/myrack-1.0.0")).to exist
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "does not state that it's constantly reinstalling empty gems" do
+ build_repo4 do
+ build_gem "empty", "1.0.0", no_default: true
+ end
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "empty"
+ G
+ gem_dir = default_bundle_path("gems/empty-1.0.0")
+ expect(gem_dir).to be_empty
+
+ bundle "install --verbose"
+ expect(out).not_to include("Installing empty")
+ end
+
+ it "fetches gems when multiple versions are specified" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', "> 0.9", "< 1.0"
+ G
+
+ expect(default_bundle_path("gems/myrack-0.9.1")).to exist
+ expect(the_bundle).to include_gems("myrack 0.9.1")
+ end
+
+ it "fetches gems when multiple versions are specified take 2" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', "< 1.0", "> 0.9"
+ G
+
+ expect(default_bundle_path("gems/myrack-0.9.1")).to exist
+ expect(the_bundle).to include_gems("myrack 0.9.1")
+ end
+
+ it "raises an appropriate error when gems are specified using symbols" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem :myrack
+ G
+ expect(exitstatus).to eq(4)
+ end
+
+ it "pulls in dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ expect(the_bundle).to include_gems "actionpack 2.3.2", "rails 2.3.2"
+ end
+
+ it "does the right version" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "does not install the development dependency" do
+ build_repo2 do
+ build_gem "with_development_dependency" do |s|
+ s.add_development_dependency "activesupport", "= 2.3.5"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_development_dependency"
+ G
+
+ expect(the_bundle).to include_gems("with_development_dependency 1.0.0").
+ and not_include_gems("activesupport 2.3.5")
+ end
+
+ it "resolves correctly" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activemerchant"
+ gem "rails"
+ G
+
+ expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2"
+ end
+
+ it "activates gem correctly according to the resolved gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.5"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activemerchant"
+ gem "rails"
+ G
+
+ expect(the_bundle).to include_gems "activemerchant 1.0", "activesupport 2.3.2", "actionpack 2.3.2"
+ end
+
+ it "does not reinstall any gem that is already available locally" do
+ system_gems "activesupport-2.3.2", path: default_bundle_path
+
+ build_repo2 do
+ build_gem "activesupport", "2.3.2" do |s|
+ s.write "lib/activesupport.rb", "ACTIVESUPPORT = 'fail'"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activerecord", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems "activesupport 2.3.2"
+ end
+
+ it "works when the gemfile specifies gems that only exist in the system" do
+ build_gem "foo", to_bundle: true
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "foo"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "foo 1.0.0"
+ end
+
+ it "prioritizes local gems over remote gems" do
+ build_gem "myrack", "9.0.0", to_bundle: true
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 9.0.0"
+ end
+
+ it "loads env plugins" do
+ plugin_msg = "hello from an env plugin!"
+ create_file "plugins/rubygems_plugin.rb", "puts '#{plugin_msg}'"
+ install_gemfile <<-G, env: { "RUBYLIB" => rubylib.unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR) }
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(stdboth).to include(plugin_msg)
+ end
+
+ describe "with a gem that installs multiple platforms" do
+ it "installs gems for the local platform as first choice" do
+ simulate_platform "x86-darwin-100" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 x86-darwin-100")
+ end
+ end
+
+ it "falls back on plain ruby" do
+ simulate_platform "foo-bar-baz" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 ruby")
+ end
+ end
+
+ it "installs gems for java" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 java")
+ end
+ end
+
+ it "installs gems for windows" do
+ simulate_platform "x86-mswin32" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems("platform_specific 1.0 x86-mswin32")
+ end
+ end
+
+ it "installs gems for aarch64-mingw-ucrt" do
+ simulate_platform "aarch64-mingw-ucrt" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+ end
+
+ expect(out).to include("Installing platform_specific 1.0 (aarch64-mingw-ucrt)")
+ end
+ end
+
+ it "gives useful errors if no global sources are set, and gems not installed locally, with and without a lockfile" do
+ install_gemfile <<-G, raise_on_error: false
+ gem "myrack"
+ G
+
+ expect(err).to eq("Could not find gem 'myrack' in locally installed gems.")
+
+ lockfile <<~L
+ GEM
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install", raise_on_error: false
+
+ expect(err).to include(
+ "Because your Gemfile specifies no global remote source, your bundle is locked to " \
+ "myrack (1.0.0) from locally installed gems. However, myrack (1.0.0) is not installed. " \
+ "You'll need to either add a global remote source to your Gemfile or make sure myrack (1.0.0) " \
+ "is available locally before rerunning Bundler."
+ )
+ end
+
+ it "creates a Gemfile.lock on a blank Gemfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ expect(File.exist?(bundled_app_lock)).to eq(true)
+ end
+
+ it "throws a warning if a gem is added twice in Gemfile without version requirements" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "myrack"
+ G
+
+ expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.")
+ expect(err).to include("Remove any duplicate entries and specify the gem only once.")
+ expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
+ end
+
+ it "throws a warning if a gem is added twice in Gemfile with same versions" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ gem "myrack", "1.0"
+ G
+
+ expect(err).to include("Your Gemfile lists the gem myrack (= 1.0) more than once.")
+ expect(err).to include("Remove any duplicate entries and specify the gem only once.")
+ expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
+ end
+
+ it "throws a warning if a gem is added twice under different platforms and does not crash when using the generated lockfile" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", :platform => :jruby
+ gem "myrack"
+ G
+
+ bundle "install"
+
+ expect(err).to include("Your Gemfile lists the gem myrack (>= 0) more than once.")
+ expect(err).to include("Remove any duplicate entries and specify the gem only once.")
+ expect(err).to include("While it's not a problem now, it could cause errors if you change the version of one of them later.")
+ end
+
+ it "does not throw a warning if a gem is added once in Gemfile and also inside a gemspec as a development dependency" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "my-private-gem"
+ end
+
+ build_repo2 do
+ build_gem "my-private-gem"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gemspec
+
+ gem "my-private-gem", :group => :development
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("my-private-gem 1.0")
+ end
+
+ it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with compatible requirements" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ build_gem "rubocop", "1.37.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rubocop", group: :development
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "raises an error if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with incompatible requirements" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ build_gem "rubocop", "1.37.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rubocop", "~> 1.37.0", group: :development
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("The rubocop dependency has conflicting requirements in Gemfile (~> 1.37.0) and gemspec (~> 1.36.0)")
+ end
+
+ it "includes the gem without warning if two gemspecs add it with the same requirement" do
+ gem1 = tmp("my-gem-1")
+ gem2 = tmp("my-gem-2")
+
+ build_lib "my-gem", path: gem1 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_lib "my-gem-2", path: gem2 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec path: "#{gem1}"
+ gemspec path: "#{gem2}"
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "includes the gem without warning if two gemspecs add it with compatible requirements" do
+ gem1 = tmp("my-gem-1")
+ gem2 = tmp("my-gem-2")
+
+ build_lib "my-gem", path: gem1 do |s|
+ s.add_development_dependency "rubocop", "~> 1.0"
+ end
+
+ build_lib "my-gem-2", path: gem2 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec path: "#{gem1}"
+ gemspec path: "#{gem2}"
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "errors out if two gemspecs add it with incompatible requirements" do
+ gem1 = tmp("my-gem-1")
+ gem2 = tmp("my-gem-2")
+
+ build_lib "my-gem", path: gem1 do |s|
+ s.add_development_dependency "rubocop", "~> 2.0"
+ end
+
+ build_lib "my-gem-2", path: gem2 do |s|
+ s.add_development_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec path: "#{gem1}"
+ gemspec path: "#{gem2}"
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("Two gemspec development dependencies have conflicting requirements on the same gem: rubocop (~> 1.36.0) and rubocop (~> 2.0). Bundler cannot continue.")
+ end
+
+ it "errors out if a gem is specified in a gemspec and in the Gemfile" do
+ gem = tmp("my-gem-1")
+
+ build_lib "rubocop", path: gem do |s|
+ s.add_development_dependency "rubocop", "~> 1.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rubocop", :path => "#{gem}"
+ gemspec path: "#{gem}"
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("There was an error parsing `Gemfile`: You cannot specify the same gem twice coming from different sources.")
+ expect(err).to include("You specified that rubocop (>= 0) should come from source at `#{gem}` and gemspec at `#{gem}`")
+ end
+
+ it "does not warn if a gem is added once in Gemfile and also inside a gemspec as a development dependency, with same requirements, and different sources" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_development_dependency "activesupport"
+ end
+
+ build_repo4 do
+ build_gem "activesupport"
+ end
+
+ build_git "activesupport", "1.0", path: lib_path("activesupport")
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "activesupport", :git => "#{lib_path("activesupport")}"
+ G
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}"
+
+ # if the Gemfile dependency is specified first
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "activesupport", :git => "#{lib_path("activesupport")}"
+
+ gemspec
+ G
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems "activesupport 1.0", source: "git@#{lib_path("activesupport")}"
+ end
+
+ it "considers both dependencies for resolution if a gem is added once in Gemfile and also inside a local gemspec as a runtime dependency, with different requirements" do
+ build_lib "my-gem", path: bundled_app do |s|
+ s.add_dependency "rubocop", "~> 1.36.0"
+ end
+
+ build_repo4 do
+ build_gem "rubocop", "1.36.0"
+ build_gem "rubocop", "1.37.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rubocop"
+ G
+
+ bundle :install
+
+ expect(err).to be_empty
+ expect(the_bundle).to include_gems("rubocop 1.36.0")
+ end
+
+ it "throws an error if a gem is added twice in Gemfile when version of one dependency is not specified" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "myrack", "1.0"
+ G
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("You specified: myrack (>= 0) and myrack (= 1.0).")
+ end
+
+ it "throws an error if a gem is added twice in Gemfile when different versions of both dependencies are specified" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack", "1.0"
+ gem "myrack", "1.1"
+ G
+
+ expect(err).to include("You cannot specify the same gem twice with different version requirements")
+ expect(err).to include("You specified: myrack (= 1.0) and myrack (= 1.1).")
+ end
+
+ it "gracefully handles error when rubygems server is unavailable" do
+ install_gemfile <<-G, artifice: nil, raise_on_error: false
+ source "https://gem.repo1"
+ source "http://0.0.0.0:9384" do
+ gem 'foo'
+ end
+ G
+
+ expect(err).to eq("Could not reach host 0.0.0.0:9384. Check your network connection and try again.")
+ expect(err).not_to include("file://")
+ end
+
+ it "fails gracefully when downloading an invalid specification from the full index" do
+ build_repo2(build_compact_index: false) do
+ build_gem "ajp-rails", "0.0.0", gemspec: false, skip_validation: true do |s|
+ invalid_deps = [["ruby-ajp", ">= 0.2.0"], ["rails", ">= 0.14"]]
+ s.
+ instance_variable_get(:@spec).
+ instance_variable_set(:@dependencies, invalid_deps)
+ end
+
+ build_gem "ruby-ajp", "1.0.0"
+ end
+
+ install_gemfile <<-G, full_index: true, raise_on_error: false
+ source "https://gem.repo2"
+
+ gem "ajp-rails", "0.0.0"
+ G
+
+ expect(stdboth).not_to match(/Error Report/i)
+ expect(err).to include("An error occurred while installing ajp-rails (0.0.0), and Bundler cannot continue.").
+ and include("Bundler::APIResponseInvalidDependenciesError")
+ end
+
+ it "doesn't blow up when the local .bundle/config is empty" do
+ FileUtils.mkdir_p(bundled_app(".bundle"))
+ FileUtils.touch(bundled_app(".bundle/config"))
+
+ install_gemfile(<<-G)
+ source "https://gem.repo1"
+
+ gem 'foo'
+ G
+ end
+
+ it "doesn't blow up when the global .bundle/config is empty" do
+ FileUtils.mkdir_p("#{Bundler.rubygems.user_home}/.bundle")
+ FileUtils.touch("#{Bundler.rubygems.user_home}/.bundle/config")
+
+ install_gemfile(<<-G)
+ source "https://gem.repo1"
+
+ gem 'foo'
+ G
+ end
+ end
+
+ describe "Ruby version in Gemfile.lock" do
+ context "and using an unsupported Ruby version" do
+ it "prints an error" do
+ install_gemfile <<-G, raise_on_error: false
+ ruby '~> 1.2'
+ source "https://gem.repo1"
+ G
+ expect(err).to include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified ~> 1.2")
+ end
+ end
+
+ context "and using a supported Ruby version" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+ end
+
+ it "writes current Ruby version to Gemfile.lock" do
+ checksums = checksums_section_when_enabled
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "updates Gemfile.lock with updated yet still compatible ruby version" do
+ install_gemfile <<-G
+ ruby '~> #{current_ruby_minor}'
+ source "https://gem.repo1"
+ G
+
+ checksums = checksums_section_when_enabled
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not crash when unlocking" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby '>= 2.1.0'
+ G
+
+ bundle "update"
+
+ expect(err).not_to include("Could not find gem 'Ruby")
+ end
+ end
+ end
+
+ describe "when Bundler root contains regex chars" do
+ it "doesn't blow up when using the `gem` DSL" do
+ root_dir = tmp("foo[]bar")
+
+ FileUtils.mkdir_p(root_dir)
+
+ build_lib "foo"
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gem 'foo', :path => "#{lib_path("foo-1.0")}"
+ G
+ File.open("#{root_dir}/Gemfile", "w") do |file|
+ file.puts gemfile
+ end
+
+ bundle :install, dir: root_dir
+ end
+
+ it "doesn't blow up when using the `gemspec` DSL" do
+ root_dir = tmp("foo[]bar")
+
+ FileUtils.mkdir_p(root_dir)
+
+ build_lib "foo", path: root_dir
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+ File.open("#{root_dir}/Gemfile", "w") do |file|
+ file.puts gemfile
+ end
+
+ bundle :install, dir: root_dir
+ end
+ end
+
+ describe "when requesting a quiet install via --quiet" do
+ it "should be quiet if there are no warnings" do
+ bundle_config "force_ruby_platform true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, quiet: true
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+
+ it "should still display warnings and errors" do
+ bundle_config "force_ruby_platform true"
+
+ create_file("install_with_warning.rb", <<~RUBY)
+ require "#{lib_dir}/bundler"
+ require "#{lib_dir}/bundler/cli"
+ require "#{lib_dir}/bundler/cli/install"
+
+ module RunWithWarning
+ def run
+ super
+ rescue
+ Bundler.ui.warn "BOOOOO"
+ raise
+ end
+ end
+
+ Bundler::CLI::Install.prepend(RunWithWarning)
+ RUBY
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'non-existing-gem'
+ G
+
+ bundle :install, quiet: true, raise_on_error: false, env: { "RUBYOPT" => "-r#{bundled_app("install_with_warning.rb")}" }
+ expect(out).to be_empty
+ expect(err).to include("Could not find gem 'non-existing-gem'")
+ expect(err).to include("BOOOOO")
+ end
+ end
+
+ describe "when bundle path does not have cd permission", :permissions do
+ let(:bundle_path) { bundled_app("vendor") }
+
+ before do
+ FileUtils.mkdir_p(bundle_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod(0o500, bundle_path)
+
+ bundle_config "path vendor"
+ bundle :install, raise_on_error: false
+ expect(err).to include(bundle_path.to_s)
+ expect(err).to include("grant executable permissions")
+ end
+ end
+
+ describe "when bundle gems path does not have cd permission", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+
+ before do
+ FileUtils.mkdir_p(gems_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", gems_path)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", gems_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to create `#{gems_path.join("myrack-1.0.0")}`. " \
+ "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{gems_path}`."
+ )
+ end
+ end
+
+ describe "when there's an empty install folder (like with default gems) without cd permissions", :permissions do
+ let(:full_gem_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems/myrack-1.0.0") }
+
+ before do
+ FileUtils.mkdir_p(full_gem_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", full_gem_path)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", full_gem_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to write to `#{full_gem_path}`. " \
+ "It is likely that you need to grant write permissions for that path."
+ )
+ end
+ end
+
+ describe "when bundle bin dir does not have cd permission", :permissions do
+ let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") }
+
+ before do
+ FileUtils.mkdir_p(bin_dir)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", bin_dir)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", bin_dir)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to write to `#{bin_dir}`. " \
+ "It is likely that you need to grant write permissions for that path."
+ )
+ end
+ end
+
+ describe "when bundle bin dir does not have write access", :permissions do
+ let(:bin_dir) { bundled_app("vendor/#{Bundler.ruby_scope}/bin") }
+
+ before do
+ FileUtils.mkdir_p(bin_dir)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-w", bin_dir)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+w", bin_dir)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to write to `#{bin_dir}`. " \
+ "It is likely that you need to grant write permissions for that path."
+ )
+ end
+ end
+
+ describe "when bundle extensions path does not have write access", :permissions do
+ let(:extensions_path) { bundled_app("vendor/#{Bundler.ruby_scope}/extensions/#{Gem::Platform.local}/#{Gem.extension_api_version}") }
+
+ before do
+ FileUtils.mkdir_p(extensions_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'simple_binary'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod("-x", extensions_path)
+ bundle_config "path vendor"
+
+ begin
+ bundle :install, raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", extensions_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ expect(err).to include(
+ "There was an error while trying to create `#{extensions_path.join("simple_binary-1.0")}`. " \
+ "It is likely that you need to grant executable permissions for all parent directories and write permissions for `#{extensions_path}`."
+ )
+ end
+ end
+
+ describe "when the path of a specific gem does not have cd permission", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+ let(:foo_path) { gems_path.join("foo-1.0.0") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod("-x", foo_path)
+
+ begin
+ bundle "install --force", raise_on_error: false
+ ensure
+ FileUtils.chmod("+x", foo_path)
+ end
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ expect(err).to include("Could not delete previous installation of `#{foo_path}`.")
+ expect(err).to include("The underlying error was Errno::EACCES")
+ end
+ end
+
+ describe "when gem home does not have the writable bit set, yet it's still writable", :permissions do
+ let(:gem_home) { bundled_app("vendor/#{Bundler.ruby_scope}") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "should still work" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod("-w", gem_home)
+
+ begin
+ bundle "install --force"
+ ensure
+ FileUtils.chmod("+w", gem_home)
+ end
+
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "when gems path is world writable (no sticky bit set)", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod(0o777, gems_path)
+
+ bundle "install --force", raise_on_error: false
+
+ expect(err).to include("Bundler cannot reinstall foo-1.0.0 because there's a previous installation of it at #{gems_path}/foo-1.0.0 that is unsafe to remove")
+ end
+ end
+
+ describe "when gems path is world writable (no sticky bit set), but previous install is just an empty dir (like it happens with default gems)", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+ let(:full_path) { gems_path.join("foo-1.0.0") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+ end
+
+ it "does not try to remove the directory and thus don't abort with an error about unsafe directory removal" do
+ bundle_config "path vendor"
+
+ FileUtils.mkdir_p(gems_path)
+ FileUtils.chmod(0o777, gems_path)
+ Dir.mkdir(full_path)
+
+ bundle "install"
+ end
+ end
+
+ describe "when bundle cache path does not have write access", :permissions do
+ let(:cache_path) { bundled_app("vendor/#{Bundler.ruby_scope}/cache") }
+
+ before do
+ FileUtils.mkdir_p(cache_path)
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "should display a proper message to explain the problem" do
+ FileUtils.chmod(0o500, cache_path)
+
+ bundle_config "path vendor"
+ bundle :install, raise_on_error: false
+ expect(err).to include(cache_path.to_s)
+ expect(err).to include("grant write permissions")
+ end
+ end
+
+ describe "when gemspecs are unreadable", :permissions do
+ let(:gemspec_path) { vendored_gems("specifications/myrack-1.0.0.gemspec") }
+
+ before do
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+
+ FileUtils.chmod("-r", gemspec_path)
+ end
+
+ it "shows a good error" do
+ bundle :install, raise_on_error: false
+ expect(err).to include(gemspec_path.to_s)
+ expect(err).to include("grant read permissions")
+ end
+ end
+
+ describe "when using umask 002 and setgid bit", :permissions do
+ let(:gems_path) { bundled_app("vendor/#{Bundler.ruby_scope}/gems") }
+ let(:foo_path) { gems_path.join("foo-1.0.0") }
+
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.write "CHANGELOG.md", "foo"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ G
+
+ FileUtils.mkdir_p(gems_path)
+ FileUtils.chmod("g+s", gems_path)
+ end
+
+ it "should create the gem directory with proper permissions" do
+ with_umask(0o002) do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include("Bundle complete!")
+ expect(err).to be_empty
+ # Linux's SysV-derived mkdir(2) propagates the set-group-ID bit
+ # from the parent directory to newly created subdirectories. BSD
+ # (including macOS) inherits the parent's group via mkdir(2) but
+ # does not copy the set-group-ID bit itself, so the expected
+ # mode differs by platform.
+ expected = RUBY_PLATFORM.include?("darwin") ? 0o0775 : 0o2775
+ expect(File.stat(foo_path).mode & 0o7777).to eq(expected)
+ end
+ end
+ end
+
+ describe "parallel make" do
+ before do
+ unless Gem::Installer.private_method_defined?(:build_jobs)
+ skip "This example is runnable when RubyGems::Installer implements `build_jobs`"
+ end
+
+ @old_makeflags = ENV["MAKEFLAGS"]
+ @gemspec = nil
+
+ extconf_code = <<~CODE
+ require "mkmf"
+ create_makefile("foo")
+ CODE
+
+ build_repo4 do
+ build_gem "mypsych", "4.0.6" do |s|
+ @gemspec = s
+ extension = "ext/mypsych/extconf.rb"
+ s.extensions = extension
+
+ s.write(extension, extconf_code)
+ end
+ end
+ end
+
+ after do
+ if @old_makeflags
+ ENV["MAKEFLAGS"] = @old_makeflags
+ else
+ ENV.delete("MAKEFLAGS")
+ end
+ end
+
+ it "doesn't pass down -j to make when MAKEFLAGS is set" do
+ ENV["MAKEFLAGS"] = "-j1"
+
+ install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" })
+ source "https://gem.repo4"
+ gem "mypsych"
+ G
+
+ gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))
+
+ expect(gem_make_out).not_to include("make -j8")
+ end
+
+ it "pass down the BUNDLE_JOBS to RubyGems when running the compilation of an extension" do
+ ENV.delete("MAKEFLAGS")
+
+ install_gemfile(<<~G, env: { "BUNDLE_JOBS" => "8" })
+ source "https://gem.repo4"
+ gem "mypsych"
+ G
+
+ gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))
+
+ expect(gem_make_out).to include("make -j8")
+ end
+
+ it "uses nprocessors by default" do
+ ENV.delete("MAKEFLAGS")
+
+ install_gemfile(<<~G)
+ source "https://gem.repo4"
+ gem "mypsych"
+ G
+
+ gem_make_out = File.read(File.join(@gemspec.extension_dir, "gem_make.out"))
+
+ expect(gem_make_out).to include("make -j#{Etc.nprocessors + 1}")
+ end
+ end
+
+ describe "when a native extension requires a transitive dependency at build time" do
+ before do
+ build_repo4 do
+ build_gem "alpha", "1.0.0" do |s|
+ extension = "ext/alpha/extconf.rb"
+ s.extensions = extension
+ s.write(extension, <<~CODE)
+ require "mkmf"
+ sleep 1
+ create_makefile("alpha")
+ CODE
+ s.write "lib/alpha.rb", "ALPHA = '1.0.0'"
+ end
+
+ build_gem "beta", "1.0.0" do |s|
+ s.add_dependency "alpha"
+ s.write "lib/beta.rb", "require 'alpha'\nBETA = '1.0.0'"
+ end
+
+ build_gem "gamma", "1.0.0" do |s|
+ s.add_dependency "beta"
+ extension = "ext/gamma/extconf.rb"
+ s.extensions = extension
+ s.write(extension, <<~EXTCONF)
+ require "beta"
+ require "mkmf"
+ create_makefile("gamma")
+ EXTCONF
+ end
+ end
+ end
+
+ it "installs successfully" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "gamma"
+ G
+
+ expect(the_bundle).to include_gems "alpha 1.0.0", "beta 1.0.0", "gamma 1.0.0"
+ end
+ end
+
+ describe "when configured path is UTF-8 and a file inside a gem package too" do
+ let(:app_path) do
+ path = tmp("♥")
+ FileUtils.mkdir_p(path)
+ path
+ end
+
+ let(:path) do
+ root.join("vendor/bundle")
+ end
+
+ before do
+ build_repo4 do
+ build_gem "mygem" do |s|
+ s.write "spec/fixtures/_posts/2016-04-01-错误.html"
+ end
+ end
+ end
+
+ it "works" do
+ bundle "config set path #{app_path}/vendor/bundle", dir: app_path
+
+ install_gemfile app_path.join("Gemfile"),<<~G, dir: app_path
+ source "https://gem.repo4"
+ gem "mygem", "1.0"
+ G
+ end
+ end
+
+ context "after installing with --standalone" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle_config "path bundle"
+ bundle "install", standalone: true
+ end
+
+ it "includes the standalone path" do
+ bundle "binstubs myrack", standalone: true
+ standalone_line = File.read(bundled_app("bin/myrackup")).each_line.find {|line| line.include? "$:.unshift" }.strip
+ expect(standalone_line).to eq %($:.unshift File.expand_path "../bundle", __dir__)
+ end
+ end
+
+ describe "when bundle install is executed with unencoded authentication" do
+ before do
+ gemfile <<-G
+ source 'https://rubygems.org/'
+ gem "."
+ G
+ end
+
+ it "should display a helpful message explaining how to fix it" do
+ bundle :install, env: { "BUNDLE_RUBYGEMS__ORG" => "user:pass{word" }, raise_on_error: false
+ expect(exitstatus).to eq(17)
+ expect(err).to eq("Please CGI escape your usernames and passwords before " \
+ "setting them for authentication.")
+ end
+ end
+
+ context "when current platform not included in the lockfile" do
+ around do |example|
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+
+ PLATFORMS
+ x86_64-darwin-19
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform("x86_64-linux", &example)
+ end
+
+ it "adds the current platform to the lockfile" do
+ bundle "install --verbose"
+
+ expect(out).to include("re-resolving dependencies because your lockfile is missing the current platform")
+ expect(out).not_to include("you are adding a new platform to your lockfile")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+ libv8 (8.4.255.0-x86_64-linux)
+
+ PLATFORMS
+ x86_64-darwin-19
+ x86_64-linux
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "fails loudly if frozen mode set" do
+ bundle_config "deployment true"
+ bundle "install", raise_on_error: false
+
+ expect(err).to eq(
+ "Your bundle only supports platforms [\"x86_64-darwin-19\"] but your local platform is x86_64-linux. " \
+ "Add the current platform to the lockfile with\n`bundle lock --add-platform x86_64-linux` and try again."
+ )
+ end
+ end
+
+ context "with missing platform specific gems in lockfile" do
+ before do
+ build_repo4 do
+ build_gem "racca", "1.5.2"
+
+ build_gem "nokogiri", "1.12.4" do |s|
+ s.platform = "x86_64-darwin"
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "nokogiri", "1.12.4" do |s|
+ s.platform = "x86_64-linux"
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "crass", "1.0.6"
+
+ build_gem "loofah", "2.12.0" do |s|
+ s.add_dependency "crass", "~> 1.0.2"
+ s.add_dependency "nokogiri", ">= 1.5.9"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ ruby "#{Gem.ruby_version}"
+
+ gem "loofah", "~> 2.12.0"
+ G
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "crass", "1.0.6"
+ c.checksum gem_repo4, "loofah", "2.12.0"
+ c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin"
+ c.checksum gem_repo4, "racca", "1.5.2"
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ crass (1.0.6)
+ loofah (2.12.0)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.5.9)
+ nokogiri (1.12.4-x86_64-darwin)
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ PLATFORMS
+ x86_64-darwin-20
+ x86_64-linux
+
+ DEPENDENCIES
+ loofah (~> 2.12.0)
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile" do
+ bundle_config "path vendor/bundle"
+
+ simulate_platform "x86_64-linux" do
+ bundle "install", artifice: "compact_index"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "crass", "1.0.6"
+ c.checksum gem_repo4, "loofah", "2.12.0"
+ c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-darwin"
+ c.checksum gem_repo4, "racca", "1.5.2"
+ c.checksum gem_repo4, "nokogiri", "1.12.4", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ crass (1.0.6)
+ loofah (2.12.0)
+ crass (~> 1.0.2)
+ nokogiri (>= 1.5.9)
+ nokogiri (1.12.4-x86_64-darwin)
+ racca (~> 1.4)
+ nokogiri (1.12.4-x86_64-linux)
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ PLATFORMS
+ x86_64-darwin-20
+ x86_64-linux
+
+ DEPENDENCIES
+ loofah (~> 2.12.0)
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when lockfile has incorrect dependencies" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ system_gems "myrack_middleware-1.0", path: default_bundle_path
+
+ # we want to raise when the 1.0 line should be followed by " myrack (= 0.9.1)" but isn't
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a clear error message when frozen" do
+ bundle_config "frozen true"
+ bundle "install", raise_on_error: false
+
+ expect(exitstatus).to eq(41)
+ expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0")
+ expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile")
+ end
+
+ it "updates the lockfile when not frozen" do
+ missing_dep = "myrack (0.9.1)"
+ expect(lockfile).not_to include(missing_dep)
+
+ bundle_config "frozen false"
+ bundle :install
+
+ expect(lockfile).to include(missing_dep)
+ expect(out).to include("now installed")
+ end
+ end
+
+ context "with --local flag" do
+ before do
+ system_gems "myrack-1.0.0", path: default_bundle_path
+ end
+
+ it "respects installed gems without fetching any remote sources" do
+ install_gemfile <<-G, local: true
+ source "https://gem.repo1"
+
+ source "https://not-existing-source" do
+ gem "myrack"
+ end
+ G
+
+ expect(last_command).to be_success
+ end
+ end
+
+ context "with only option" do
+ before do
+ bundle_config "only a:b"
+ end
+
+ it "installs only gems of the specified groups" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ gem "myrack", group: :a
+ gem "rake", group: :b
+ gem "yard", group: :c
+ G
+
+ expect(out).to include("Installing myrack")
+ expect(out).to include("Installing rake")
+ expect(out).not_to include("Installing yard")
+ end
+ end
+
+ context "with --prefer-local flag" do
+ context "and gems available locally" do
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.1"
+ build_gem "foo", "1.0.0"
+ build_gem "bar", "1.0.0"
+
+ build_gem "a", "1.0.0" do |s|
+ s.add_dependency "foo", "~> 1.0.0"
+ end
+
+ build_gem "b", "1.0.0" do |s|
+ s.add_dependency "foo", "~> 1.0.1"
+ end
+ end
+
+ system_gems "foo-1.0.0", path: default_bundle_path, gem_repo: gem_repo4
+ end
+
+ it "fetches remote sources when not available locally" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "foo"
+ gem "bar"
+ G
+
+ expect(out).to include("Using foo 1.0.0").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0")
+ expect(last_command).to be_success
+ end
+
+ it "fetches remote sources when local version does not match requirements" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "foo", "1.0.1"
+ gem "bar"
+ G
+
+ expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching bar 1.0.0").and include("Installing bar 1.0.0")
+ expect(last_command).to be_success
+ end
+
+ it "uses the locally available version for sub-dependencies when possible" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "a"
+ G
+
+ expect(out).to include("Using foo 1.0.0").and include("Fetching a 1.0.0").and include("Installing a 1.0.0")
+ expect(last_command).to be_success
+ end
+
+ it "fetches remote sources for sub-dependencies when the locally available version does not satisfy the requirement" do
+ install_gemfile <<-G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "b"
+ G
+
+ expect(out).to include("Fetching foo 1.0.1").and include("Installing foo 1.0.1").and include("Fetching b 1.0.0").and include("Installing b 1.0.0")
+ expect(last_command).to be_success
+ end
+ end
+
+ context "and no gems available locally" do
+ before do
+ build_repo4 do
+ build_gem "myreline", "0.3.8"
+ build_gem "debug", "0.2.1"
+
+ build_gem "debug", "1.10.0" do |s|
+ s.add_dependency "myreline"
+ end
+ end
+ end
+
+ it "resolves to the latest version if no gems are available locally" do
+ install_gemfile <<~G, "prefer-local": true, verbose: true
+ source "https://gem.repo4"
+
+ gem "debug"
+ G
+
+ expect(out).to include("Fetching debug 1.10.0").and include("Installing debug 1.10.0").and include("Fetching myreline 0.3.8").and include("Installing myreline 0.3.8")
+ expect(last_command).to be_success
+ end
+ end
+ end
+
+ context "with a symlinked configured as bundle path and a gem with symlinks" do
+ before do
+ symlinked_bundled_app = tmp("bundled_app-symlink")
+ File.symlink(bundled_app, symlinked_bundled_app)
+ bundle_config "path #{File.join(symlinked_bundled_app, ".vendor")}"
+
+ binman_path = tmp("binman")
+ FileUtils.mkdir_p binman_path
+
+ readme_path = File.join(binman_path, "README.markdown")
+ FileUtils.touch(readme_path)
+
+ man_path = File.join(binman_path, "man", "man0")
+ FileUtils.mkdir_p man_path
+
+ File.symlink("../../README.markdown", File.join(man_path, "README.markdown"))
+
+ build_repo4 do
+ build_gem "binman", path: gem_repo4("gems"), lib_path: binman_path, no_default: true do |s|
+ s.files = ["README.markdown", "man/man0/README.markdown"]
+ end
+ end
+ end
+
+ it "installs fine" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "binman"
+ G
+ end
+ end
+
+ context "when a gem has equivalent versions with inconsistent dependencies" do
+ before do
+ build_repo4 do
+ build_gem "autobuild", "1.10.rc2" do |s|
+ s.add_dependency "utilrb", ">= 1.6.0"
+ end
+
+ build_gem "autobuild", "1.10.0.rc2" do |s|
+ s.add_dependency "utilrb", ">= 2.0"
+ end
+ end
+ end
+
+ it "does not crash unexpectedly" do
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "autobuild", "1.10.rc2"
+ G
+
+ bundle "install --jobs 1", raise_on_error: false
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ expect(err).to include("Could not find compatible versions")
+ end
+ end
+
+ context "when a lockfile has unmet dependencies, and the Gemfile has no resolution" do
+ before do
+ build_repo4 do
+ build_gem "aaa", "0.2.0" do |s|
+ s.add_dependency "zzz", "< 0.2.0"
+ end
+
+ build_gem "zzz", "0.2.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "aaa"
+ gem "zzz"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ aaa (0.2.0)
+ zzz (< 0.2.0)
+ zzz (0.2.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ aaa!
+ zzz!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not install, but raises a resolution error" do
+ bundle "install", raise_on_error: false
+ expect(err).to include("Could not find compatible versions")
+ end
+ end
+
+ context "when --jobs option given" do
+ before do
+ install_gemfile "source 'https://gem.repo1'", jobs: 1
+ end
+
+ it "does not save the flag to config" do
+ expect(bundled_app(".bundle/config")).not_to exist
+ end
+ end
+
+ context "when bundler installation is corrupt" do
+ before do
+ system_gems "bundler-9.99.8"
+
+ replace_version_file("9.99.9", dir: system_gem_path("gems/bundler-9.99.8"))
+ end
+
+ it "shows a proper error" do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ 9.99.8
+ L
+
+ install_gemfile "source \"https://gem.repo1\"", env: { "BUNDLER_VERSION" => "9.99.8" }, raise_on_error: false
+
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ expect(err).to include("The running version of Bundler (9.99.9) does not match the version of the specification installed for it (9.99.8)")
+ end
+ end
+
+ it "only installs executable files in bin" do
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<~G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expected_executables = [vendored_gems("bin/myrackup").to_s]
+ expected_executables << vendored_gems("bin/myrackup.bat").to_s if Gem.win_platform?
+ expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables)
+ end
+
+ it "prevents removing binstubs when BUNDLE_CLEAN is set" do
+ build_repo4 do
+ build_gem "kamal", "4.0.6" do |s|
+ s.executables = ["kamal"]
+ end
+ end
+
+ gemfile = <<~G
+ source "https://gem.repo4"
+ gem "kamal"
+ G
+
+ install_gemfile(gemfile, env: { "BUNDLE_CLEAN" => "true", "BUNDLE_PATH" => "vendor/bundle" })
+
+ expected_executables = [vendored_gems("bin/kamal").to_s]
+ expected_executables << vendored_gems("bin/kamal.bat").to_s if Gem.win_platform?
+ expect(Dir.glob(vendored_gems("bin/*"))).to eq(expected_executables)
+ end
+
+ it "preserves lockfile versions conservatively" do
+ build_repo4 do
+ build_gem "mypsych", "4.0.6" do |s|
+ s.add_dependency "mystringio"
+ end
+
+ build_gem "mypsych", "5.1.2" do |s|
+ s.add_dependency "mystringio"
+ end
+
+ build_gem "mystringio", "3.1.0"
+ build_gem "mystringio", "3.1.1"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mypsych (4.0.6)
+ mystringio
+ mystringio (3.1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ mypsych (~> 4.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "mypsych", "~> 5.0"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mypsych (5.1.2)
+ mystringio
+ mystringio (3.1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ mypsych (~> 5.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+end
diff --git a/spec/bundler/commands/issue_spec.rb b/spec/bundler/commands/issue_spec.rb
new file mode 100644
index 0000000000..346cdedc42
--- /dev/null
+++ b/spec/bundler/commands/issue_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle issue" do
+ it "exits with a message" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ bundle "issue"
+ expect(out).to include "Did you find an issue with Bundler?"
+ expect(out).to include "## Environment"
+ expect(out).to include "## Gemfile"
+ expect(out).to include "## Bundle Doctor"
+ end
+end
diff --git a/spec/bundler/commands/licenses_spec.rb b/spec/bundler/commands/licenses_spec.rb
new file mode 100644
index 0000000000..ebfad5ed4a
--- /dev/null
+++ b/spec/bundler/commands/licenses_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle licenses" do
+ before :each do
+ build_repo2 do
+ build_gem "with_license" do |s|
+ s.license = "MIT"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "with_license"
+ G
+ end
+
+ it "prints license information for all gems in the bundle" do
+ bundle "licenses"
+
+ expect(out).to include("bundler: MIT")
+ expect(out).to include("with_license: MIT")
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ gem "with_license"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :licenses
+ expect(out).to include("Installing foo 1.0")
+ end
+end
diff --git a/spec/bundler/commands/list_spec.rb b/spec/bundler/commands/list_spec.rb
new file mode 100644
index 0000000000..c890646a81
--- /dev/null
+++ b/spec/bundler/commands/list_spec.rb
@@ -0,0 +1,315 @@
+# frozen_string_literal: true
+
+require "json"
+
+RSpec.describe "bundle list" do
+ def find_gem_name(json:, name:)
+ parse_json(json)["gems"].detect {|h| h["name"] == name }
+ end
+
+ def parse_json(json)
+ JSON.parse(json)
+ end
+
+ context "in verbose mode" do
+ it "logs the actual flags passed to the command" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "list --verbose"
+
+ expect(out).to include("Running `bundle list --verbose`")
+ end
+ end
+
+ context "with name-only and paths option" do
+ it "raises an error" do
+ bundle "list --name-only --paths", raise_on_error: false
+
+ expect(err).to eq "The `--name-only` and `--paths` options cannot be used together"
+ end
+ end
+
+ context "with without-group and only-group option" do
+ it "raises an error" do
+ bundle "list --without-group dev --only-group test", raise_on_error: false
+
+ expect(err).to eq "The `--only-group` and `--without-group` options cannot be used together"
+ end
+ end
+
+ context "with invalid format option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "raises an error" do
+ bundle "list --format=nope", raise_on_error: false
+
+ expect(err).to eq "Unknown option`--format=nope`. Supported formats: `json`"
+ end
+ end
+
+ describe "with without-group option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ gem "rails", :group => [:production]
+ G
+ end
+
+ context "when group is present" do
+ it "prints the gems not in the specified group" do
+ bundle "list --without-group test"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).to include(" * rails (2.3.2)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems not in the specified group with json" do
+ bundle "list --without-group test --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem["version"]).to eq("2.3.2")
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+
+ context "when group is not found" do
+ it "raises an error" do
+ bundle "list --without-group random", raise_on_error: false
+
+ expect(err).to eq "`random` group could not be found."
+ end
+ end
+
+ context "when multiple groups" do
+ it "prints the gems not in the specified groups" do
+ bundle "list --without-group test production"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).not_to include(" * rails (2.3.2)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems not in the specified groups with json" do
+ bundle "list --without-group test production --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem).to be_nil
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+ end
+
+ describe "with only-group option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ gem "rails", :group => [:production]
+ G
+ end
+
+ context "when group is present" do
+ it "prints the gems in the specified group" do
+ bundle "list --only-group default"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems in the specified group with json" do
+ bundle "list --only-group default --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+
+ context "when group is not found" do
+ it "raises an error" do
+ bundle "list --only-group random", raise_on_error: false
+
+ expect(err).to eq "`random` group could not be found."
+ end
+ end
+
+ context "when multiple groups" do
+ it "prints the gems in the specified groups" do
+ bundle "list --only-group default production"
+
+ expect(out).to include(" * myrack (1.0.0)")
+ expect(out).to include(" * rails (2.3.2)")
+ expect(out).not_to include(" * rspec (1.2.7)")
+ end
+
+ it "prints the gems in the specified groups with json" do
+ bundle "list --only-group default production --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem["version"]).to eq("2.3.2")
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem).to be_nil
+ end
+ end
+ end
+
+ context "with name-only option" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ G
+ end
+
+ it "prints only the name of the gems in the bundle" do
+ bundle "list --name-only"
+
+ expect(out).to include("myrack")
+ expect(out).to include("rspec")
+ end
+
+ it "prints only the name of the gems in the bundle with json" do
+ bundle "list --name-only --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem.keys).to eq(["name"])
+ gem = find_gem_name(json: out, name: "rspec")
+ expect(gem.keys).to eq(["name"])
+ end
+ end
+
+ context "with paths option" do
+ before do
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "bar"
+ end
+
+ build_git "git_test", "1.0.0", path: lib_path("git_test")
+
+ build_lib("gemspec_test", path: tmp("gemspec_test")) do |s|
+ s.add_dependency "bar", "=1.0.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ gem "rails"
+ gem "git_test", :git => "#{lib_path("git_test")}"
+ gemspec :path => "#{tmp("gemspec_test")}"
+ G
+ end
+
+ it "prints the path of each gem in the bundle" do
+ bundle "list --paths"
+ expect(out).to match(%r{.*\/rails\-2\.3\.2})
+ expect(out).to match(%r{.*\/myrack\-1\.2})
+ expect(out).to match(%r{.*\/git_test\-\w})
+ expect(out).to match(%r{.*\/gemspec_test})
+ end
+
+ it "prints the path of each gem in the bundle with json" do
+ bundle "list --paths --format=json"
+
+ gem = find_gem_name(json: out, name: "rails")
+ expect(gem["path"]).to match(%r{.*\/rails\-2\.3\.2})
+ expect(gem["git_version"]).to be_nil
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["path"]).to match(%r{.*\/myrack\-1\.2})
+ expect(gem["git_version"]).to be_nil
+
+ gem = find_gem_name(json: out, name: "git_test")
+ expect(gem["path"]).to match(%r{.*\/git_test\-\w})
+ expect(gem["git_version"]).to be_truthy
+ expect(gem["git_version"].strip).to eq(gem["git_version"])
+
+ gem = find_gem_name(json: out, name: "gemspec_test")
+ expect(gem["path"]).to match(%r{.*\/gemspec_test})
+ expect(gem["git_version"]).to be_nil
+ end
+ end
+
+ context "when no gems are in the gemfile" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "prints message saying no gems are in the bundle" do
+ bundle "list"
+ expect(out).to include("No gems in the Gemfile")
+ end
+
+ it "prints empty json" do
+ bundle "list --format=json"
+ expect(parse_json(out)["gems"]).to eq([])
+ end
+ end
+
+ context "without options" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ G
+ end
+
+ it "lists gems installed in the bundle" do
+ bundle "list"
+ expect(out).to include(" * myrack (1.0.0)")
+ end
+
+ it "lists gems installed in the bundle with json" do
+ bundle "list --format=json"
+
+ gem = find_gem_name(json: out, name: "myrack")
+ expect(gem["version"]).to eq("1.0.0")
+ end
+ end
+
+ context "when using the ls alias" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rspec", :group => [:test]
+ G
+ end
+
+ it "runs the list command" do
+ bundle "ls"
+ expect(out).to include("Gems included by the bundle")
+ end
+ end
+end
diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb
new file mode 100644
index 0000000000..8ab3cc7e8d
--- /dev/null
+++ b/spec/bundler/commands/lock_spec.rb
@@ -0,0 +1,2877 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle lock" do
+ let(:expected_lockfile) do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (1.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ let(:outdated_lockfile) do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.1"
+ c.checksum gem_repo4, "actionpack", "2.3.1"
+ c.checksum gem_repo4, "activerecord", "2.3.1"
+ c.checksum gem_repo4, "activeresource", "2.3.1"
+ c.checksum gem_repo4, "activesupport", "2.3.1"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.1"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.1)
+ activesupport (= 2.3.1)
+ actionpack (2.3.1)
+ activesupport (= 2.3.1)
+ activerecord (2.3.1)
+ activesupport (= 2.3.1)
+ activeresource (2.3.1)
+ activesupport (= 2.3.1)
+ activesupport (2.3.1)
+ foo (1.0)
+ rails (2.3.1)
+ actionmailer (= 2.3.1)
+ actionpack (= 2.3.1)
+ activerecord (= 2.3.1)
+ activeresource (= 2.3.1)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ let(:gemfile_with_rails_weakling_and_foo_from_repo4) do
+ build_repo4 do
+ build_gem "rake", "10.0.1"
+ build_gem "rake", rake_version
+
+ %w[2.3.1 2.3.2].each do |version|
+ build_gem "rails", version do |s|
+ s.executables = "rails"
+ s.add_dependency "rake", version == "2.3.1" ? "10.0.1" : rake_version
+ s.add_dependency "actionpack", version
+ s.add_dependency "activerecord", version
+ s.add_dependency "actionmailer", version
+ s.add_dependency "activeresource", version
+ end
+ build_gem "actionpack", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "activerecord", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "actionmailer", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "activeresource", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ build_gem "activesupport", version
+ end
+
+ build_gem "weakling", "0.0.3"
+
+ build_gem "foo"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "rails"
+ gem "weakling"
+ gem "foo"
+ G
+ end
+
+ it "prints a lockfile when there is no existing lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "prints a lockfile when there is an existing lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "prints a lockfile when there is an existing checksums lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "writes a lockfile when there is no existing lockfile" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock"
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "prints a lockfile without fetching new checksums if the existing lockfile had no checksums" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "touches the lockfile when there is an existing lockfile that does not need changes" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ expect do
+ bundle "lock"
+ end.to change { bundled_app_lock.mtime }
+ end
+
+ it "does not touch lockfile with --print" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ expect do
+ bundle "lock --print"
+ end.not_to change { bundled_app_lock.mtime }
+ end
+
+ it "writes a lockfile when there is an outdated lockfile using --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --update"
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "prints an updated lockfile when there is an outdated lockfile using --print --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --print --update"
+
+ expect(out).to eq(expected_lockfile.rstrip)
+ end
+
+ it "emits info messages to stderr when updating an outdated lockfile using --print --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --print --update"
+
+ expect(err).to eq(<<~STDERR.rstrip)
+ Fetching gem metadata from https://gem.repo4/...
+ Resolving dependencies...
+ STDERR
+ end
+
+ it "writes a lockfile when there is an outdated lockfile and bundle is frozen" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile outdated_lockfile
+
+ bundle "lock --update", env: { "BUNDLE_FROZEN" => "true" }
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "does not fetch remote specs when using the --local option" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --update --local", raise_on_error: false
+
+ expect(err).to match(/locally installed gems/)
+ end
+
+ it "does not fetch remote checksums with --local" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --print --local"
+
+ expect(out).to eq(expected_lockfile.chomp)
+ end
+
+ it "works with --gemfile flag" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ gemfile "CustomGemfile", <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ G
+ bundle "lock --gemfile CustomGemfile"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "foo", "1.0"
+ end
+
+ lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ expect(out).to match(/Writing lockfile to.+CustomGemfile\.lock/)
+ expect(read_lockfile("CustomGemfile.lock")).to eq(lockfile)
+ expect { read_lockfile }.to raise_error(Errno::ENOENT)
+ end
+
+ it "writes to a custom location using --lockfile" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --lockfile=lock"
+
+ expect(out).to match(/Writing lockfile to.+lock/)
+ expect(read_lockfile("lock")).to eq(expected_lockfile)
+ expect { read_lockfile }.to raise_error(Errno::ENOENT)
+ end
+
+ it "updates a specific gem and write to a custom location" do
+ build_repo4 do
+ build_gem "foo", %w[1.0.2 1.0.3]
+ build_gem "warning", %w[1.4.0 1.5.0]
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "foo"
+ gem "warning"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ foo (1.0.2)
+ warning (1.4.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ uri
+ warning
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update foo --lockfile=lock"
+
+ lockfile_content = read_lockfile("lock")
+ expect(lockfile_content).to include("foo (1.0.3)")
+ expect(lockfile_content).to include("warning (1.4.0)")
+ end
+
+ it "writes to custom location using --lockfile when a default lockfile is present" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "install"
+ bundle "lock --lockfile=lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (1.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(out).to match(/Writing lockfile to.+lock/)
+ expect(read_lockfile("lock")).to eq(lockfile)
+ end
+
+ it "update specific gems using --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.1"
+ c.checksum gem_repo4, "actionpack", "2.3.1"
+ c.checksum gem_repo4, "activerecord", "2.3.1"
+ c.checksum gem_repo4, "activeresource", "2.3.1"
+ c.checksum gem_repo4, "activesupport", "2.3.1"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.1"
+ c.checksum gem_repo4, "rake", "10.0.1"
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ lockfile_with_outdated_rails_and_rake = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.1)
+ activesupport (= 2.3.1)
+ actionpack (2.3.1)
+ activesupport (= 2.3.1)
+ activerecord (2.3.1)
+ activesupport (= 2.3.1)
+ activeresource (2.3.1)
+ activesupport (= 2.3.1)
+ activesupport (2.3.1)
+ foo (1.0)
+ rails (2.3.1)
+ actionmailer (= 2.3.1)
+ actionpack (= 2.3.1)
+ activerecord (= 2.3.1)
+ activeresource (= 2.3.1)
+ rake (= 10.0.1)
+ rake (10.0.1)
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_with_outdated_rails_and_rake
+
+ bundle "lock --update rails rake"
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "updates specific gems using --update, even if that requires unlocking other top level gems" do
+ build_repo4 do
+ build_gem "prism", "0.15.1"
+ build_gem "prism", "0.24.0"
+
+ build_gem "ruby-lsp", "0.12.0" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "ruby-lsp", "0.16.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+
+ build_gem "tapioca", "0.11.10" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "tapioca", "0.13.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "tapioca"
+ gem "ruby-lsp"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ prism (0.15.1)
+ ruby-lsp (0.12.0)
+ prism (< 0.24.0)
+ tapioca (0.11.10)
+ prism (< 0.24.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruby-lsp
+ tapioca
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update tapioca --verbose"
+
+ expect(lockfile).to include("tapioca (0.13.1)")
+ end
+
+ it "updates specific gems using --update, even if that requires unlocking other top level gems, but only as few as possible" do
+ build_repo4 do
+ build_gem "prism", "0.15.1"
+ build_gem "prism", "0.24.0"
+
+ build_gem "ruby-lsp", "0.12.0" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "ruby-lsp", "0.16.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+
+ build_gem "tapioca", "0.11.10" do |s|
+ s.add_dependency "prism", "< 0.24.0"
+ end
+
+ build_gem "tapioca", "0.13.1" do |s|
+ s.add_dependency "prism", ">= 0.24.0"
+ end
+
+ build_gem "other-prism-dependent", "1.0.0" do |s|
+ s.add_dependency "prism", ">= 0.15.1"
+ end
+
+ build_gem "other-prism-dependent", "1.1.0" do |s|
+ s.add_dependency "prism", ">= 0.15.1"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "tapioca"
+ gem "ruby-lsp"
+ gem "other-prism-dependent"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ other-prism-dependent (1.0.0)
+ prism (>= 0.15.1)
+ prism (0.15.1)
+ ruby-lsp (0.12.0)
+ prism (< 0.24.0)
+ tapioca (0.11.10)
+ prism (< 0.24.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruby-lsp
+ tapioca
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update tapioca"
+
+ expect(lockfile).to include("tapioca (0.13.1)")
+ expect(lockfile).to include("other-prism-dependent (1.0.0)")
+ end
+
+ it "preserves unknown checksum algorithms" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile.gsub(/(sha256=[a-f0-9]+)$/, "constant=true,\\1,xyz=123")
+
+ previous_lockfile = read_lockfile
+
+ bundle "lock"
+
+ expect(read_lockfile).to eq(previous_lockfile)
+ end
+
+ it "does not unlock git sources when only uri shape changes" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ build_git("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ # Change uri format to end with "/" and reinstall
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}/"
+ G
+
+ expect(out).to include("using resolution from the lockfile")
+ expect(out).not_to include("re-resolving dependencies because the list of sources changed")
+ end
+
+ it "updates specific gems using --update using the locked revision of unrelated git gems for resolving" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ ref = build_git("foo").ref_for("HEAD")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef"
+ G
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{ref}
+ branch: deadbeef
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ rake (10.0.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update rake --verbose"
+ expect(out).to match(/Writing lockfile to.+lock/)
+ expect(lockfile).to include("rake (#{rake_version})")
+ end
+
+ it "errors when updating a missing specific gems using --update" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile expected_lockfile
+
+ bundle "lock --update blahblah", raise_on_error: false
+ expect(err).to eq("Could not find gem 'blahblah'.")
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "can lock without downloading gems" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "thin"
+ gem "myrack_middleware", :group => "test"
+ G
+ bundle_config "without test"
+ bundle_config "path vendor/bundle"
+ bundle "lock", verbose: true
+ expect(bundled_app("vendor/bundle")).not_to exist
+ end
+
+ # see update_spec for more coverage on same options. logic is shared so it's not necessary
+ # to repeat coverage here.
+ context "conservative updates" do
+ before do
+ build_repo4 do
+ build_gem "foo", %w[1.4.3 1.4.4] do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ build_gem "foo", %w[1.4.5 1.5.0] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "foo", %w[1.5.1] do |s|
+ s.add_dependency "bar", "~> 3.0"
+ end
+ build_gem "foo", %w[2.0.0.pre] do |s|
+ s.add_dependency "bar"
+ end
+ build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre]
+ build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.4.3
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.4.3'
+ gem 'bar', '2.0.3'
+ gem 'qux', '1.0.0'
+ G
+
+ # remove 1.4.3 requirement and bar altogether
+ # to setup update specs below
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ gem 'qux'
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "single gem updates dependent gem to minor" do
+ bundle "lock --update foo --patch"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.1 qux-1.0.0].sort)
+ end
+
+ it "minor preferred with strict" do
+ bundle "lock --update --minor --strict"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.5.0 bar-2.1.1 qux-1.1.0].sort)
+ end
+
+ it "shows proper error when Gemfile changes forbid patch upgrades, and --patch --strict is given" do
+ # force next minor via Gemfile
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.5.0'
+ gem 'qux'
+ G
+
+ bundle "lock --update foo --patch --strict", raise_on_error: false
+
+ expect(err).to include(
+ "foo is locked to 1.4.3, while Gemfile is requesting foo (= 1.5.0). " \
+ "--strict --patch was specified, but there are no patch level upgrades from 1.4.3 satisfying foo (= 1.5.0), so version solving has failed"
+ )
+ end
+
+ context "pre" do
+ it "defaults to major" do
+ bundle "lock --update --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
+ end
+
+ it "patch preferred" do
+ bundle "lock --update --patch --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.4.5 bar-2.1.2.pre qux-1.0.1].sort)
+ end
+
+ it "minor preferred" do
+ bundle "lock --update --minor --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-1.5.1 bar-3.1.0.pre qux-1.1.0].sort)
+ end
+
+ it "major preferred" do
+ bundle "lock --update --major --pre"
+
+ expect(the_bundle.locked_specs).to eq(%w[foo-2.0.0.pre bar-4.0.0.pre qux-2.0.0].sort)
+ end
+ end
+ end
+
+ context "conservative updates when minor update adds a new dependency" do
+ before do
+ build_repo4 do
+ build_gem "sequel", "5.71.0"
+ build_gem "sequel", "5.72.0" do |s|
+ s.add_dependency "bigdecimal", ">= 0"
+ end
+ build_gem "bigdecimal", %w[1.4.4 99.1.4]
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem 'sequel'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sequel (5.71.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sequel
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "adds the latest version of the new dependency" do
+ bundle "lock --minor --update sequel"
+
+ expect(the_bundle.locked_specs).to eq(%w[sequel-5.72.0 bigdecimal-99.1.4].sort)
+ end
+ end
+
+ it "updates the bundler version in the lockfile to the latest bundler version" do
+ build_repo4 do
+ build_gem "bundler", "55"
+ end
+
+ system_gems "bundler-55", gem_repo: gem_repo4
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "https://gem.repo4"
+ G
+ lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')
+
+ bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ expect(lockfile).to end_with("BUNDLED WITH\n 55\n")
+
+ build_repo4 do
+ build_gem "bundler", "99"
+ end
+
+ bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ expect(lockfile).to end_with("BUNDLED WITH\n 99\n")
+ end
+
+ it "supports adding new platforms when there's no previous lockfile" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform java x86-mingw32 --verbose"
+ expect(out).to include("Resolving dependencies because there's no lockfile")
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
+ end
+
+ it "supports adding new platforms when a previous lockfile exists" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock"
+ bundle "lock --add-platform java x86-mingw32 --verbose"
+ expect(out).to include("Found changes from the lockfile, re-resolving dependencies because you are adding a new platform to your lockfile")
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
+ end
+
+ it "supports adding new platforms, when most specific locked platform is not the current platform, and current resolve is not compatible with the target platform" do
+ simulate_platform "arm64-darwin-23" do
+ build_repo4 do
+ build_gem "foo" do |s|
+ s.platform = "arm64-darwin"
+ end
+
+ build_gem "foo" do |s|
+ s.platform = "java"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "foo"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+
+ DEPENDENCIES
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --add-platform java"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0-arm64-darwin)
+ foo (1.0-java)
+
+ PLATFORMS
+ arm64-darwin
+ java
+
+ DEPENDENCIES
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "supports adding new platforms with force_ruby_platform = true" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-x86-64_linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ platform_specific
+ L
+
+ bundle_config "force_ruby_platform true"
+ bundle "lock --add-platform java x86-mingw32"
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to contain_exactly(Gem::Platform::RUBY, "x86_64-linux", "java", "x86-mingw32")
+ end
+
+ it "supports adding the `ruby` platform" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform ruby"
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("ruby"))
+ end
+
+ it "fails when adding an unknown platform" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform foobarbaz", raise_on_error: false
+ expect(err).to include("The platform `foobarbaz` is unknown to RubyGems and can't be added to the lockfile")
+ expect(last_command).to be_failure
+ end
+
+ it "allows removing platforms" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --add-platform java x86-mingw32"
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("java", "x86-mingw32"))
+
+ bundle "lock --remove-platform java"
+
+ expect(the_bundle.locked_platforms).to match_array(default_platform_list("x86-mingw32"))
+ end
+
+ it "also cleans up redundant platform gems when removing platforms" do
+ build_repo4 do
+ build_gem "nokogiri", "1.12.0"
+ build_gem "nokogiri", "1.12.0" do |s|
+ s.platform = "x86_64-darwin"
+ end
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.12.0"
+ c.checksum gem_repo4, "nokogiri", "1.12.0", "x86_64-darwin"
+ end
+
+ simulate_platform "x86_64-darwin-22" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.12.0)
+ nokogiri (1.12.0-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ checksums.delete("nokogiri", Gem::Platform::RUBY)
+
+ simulate_platform "x86_64-darwin-22" do
+ bundle "lock --remove-platform ruby"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.12.0-x86_64-darwin)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "errors when removing all platforms" do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ bundle "lock --remove-platform #{local_platform}", raise_on_error: false
+ expect(err).to include("Removing all platforms from the bundle is not allowed")
+ end
+
+ # from https://github.com/rubygems/bundler/issues/4896
+ it "properly adds platforms when platform requirements come from different dependencies" do
+ build_repo4 do
+ build_gem "ffi", "1.9.14"
+ build_gem "ffi", "1.9.14" do |s|
+ s.platform = "x86-mingw32"
+ end
+
+ build_gem "gssapi", "0.1"
+ build_gem "gssapi", "0.2"
+ build_gem "gssapi", "0.3"
+ build_gem "gssapi", "1.2.0" do |s|
+ s.add_dependency "ffi", ">= 1.0.1"
+ end
+
+ build_gem "mixlib-shellout", "2.2.6"
+ build_gem "mixlib-shellout", "2.2.6" do |s|
+ s.platform = "universal-mingw32"
+ s.add_dependency "win32-process", "~> 0.8.2"
+ end
+
+ # we need all these versions to get the sorting the same as it would be
+ # pulling from rubygems.org
+ %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v|
+ build_gem "win32-process", v do |s|
+ s.add_dependency "ffi", ">= 1.0.0"
+ end
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "mixlib-shellout"
+ gem "gssapi"
+ G
+
+ simulate_platform("x86-mingw32") { bundle :lock }
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "ffi", "1.9.14", "x86-mingw32"
+ c.checksum gem_repo4, "gssapi", "1.2.0"
+ c.checksum gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32"
+ c.checksum gem_repo4, "win32-process", "0.8.3"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.9.14-x86-mingw32)
+ gssapi (1.2.0)
+ ffi (>= 1.0.1)
+ mixlib-shellout (2.2.6-universal-mingw32)
+ win32-process (~> 0.8.2)
+ win32-process (0.8.3)
+ ffi (>= 1.0.0)
+
+ PLATFORMS
+ x86-mingw32
+
+ DEPENDENCIES
+ gssapi
+ mixlib-shellout
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ bundle_config "force_ruby_platform true"
+ bundle :lock
+
+ checksums.checksum gem_repo4, "ffi", "1.9.14"
+ checksums.checksum gem_repo4, "mixlib-shellout", "2.2.6"
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.9.14)
+ ffi (1.9.14-x86-mingw32)
+ gssapi (1.2.0)
+ ffi (>= 1.0.1)
+ mixlib-shellout (2.2.6)
+ mixlib-shellout (2.2.6-universal-mingw32)
+ win32-process (~> 0.8.2)
+ win32-process (0.8.3)
+ ffi (>= 1.0.0)
+
+ PLATFORMS
+ ruby
+ x86-mingw32
+
+ DEPENDENCIES
+ gssapi
+ mixlib-shellout
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "doesn't crash when an update candidate doesn't have any matching platform" do
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0"
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "15.0.71.48.1beta2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0)
+ libv8 (8.4.255.0-x86_64-darwin-19)
+
+ PLATFORMS
+ ruby
+ x86_64-darwin-19
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ simulate_platform("x86_64-darwin-19") { bundle "lock --update" }
+
+ expect(out).to match(/Writing lockfile to.+Gemfile\.lock/)
+ end
+
+ it "adds all more specific candidates when they all have the same dependencies" do
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-20"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ simulate_platform("x86_64-darwin-19") { bundle "lock" }
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19"
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+ libv8 (8.4.255.0-x86_64-darwin-20)
+
+ PLATFORMS
+ x86_64-darwin-19
+ x86_64-darwin-20
+
+ DEPENDENCIES
+ libv8
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "respects the previous lockfile if it had a matching less specific platform already locked, and installs the best variant for each platform" do
+ build_repo4 do
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-19"
+ end
+
+ build_gem "libv8", "8.4.255.0" do |s|
+ s.platform = "x86_64-darwin-20"
+ end
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19"
+ c.checksum gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "libv8"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (8.4.255.0-x86_64-darwin-19)
+ libv8 (8.4.255.0-x86_64-darwin-20)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ libv8
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ previous_lockfile = lockfile
+
+ %w[x86_64-darwin-19 x86_64-darwin-20].each do |platform|
+ simulate_platform(platform) do
+ bundle "lock"
+ expect(lockfile).to eq(previous_lockfile)
+
+ bundle "install"
+ expect(the_bundle).to include_gem("libv8 8.4.255.0 #{platform}")
+ end
+ end
+ end
+
+ it "does not conflict on ruby requirements when adding new platforms" do
+ build_repo4 do
+ build_gem "raygun-apm", "1.0.78" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{next_ruby_minor}.dev"
+ end
+
+ build_gem "raygun-apm", "1.0.78" do |s|
+ s.platform = "universal-darwin"
+ s.required_ruby_version = "< #{next_ruby_minor}.dev"
+ end
+
+ build_gem "raygun-apm", "1.0.78" do |s|
+ s.platform = "x64-mingw-ucrt"
+ s.required_ruby_version = "< #{next_ruby_minor}.dev"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "raygun-apm"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ raygun-apm (1.0.78-universal-darwin)
+
+ PLATFORMS
+ x86_64-darwin-19
+
+ DEPENDENCIES
+ raygun-apm
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --add-platform x86_64-linux"
+ end
+
+ it "adds platform specific gems as necessary, even when adding the current platform" do
+ build_repo4 do
+ build_gem "nokogiri", "1.16.0"
+
+ build_gem "nokogiri", "1.16.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-platform x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.0)
+ nokogiri (1.16.0-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "refuses to add platforms incompatible with the lockfile" do
+ build_repo4 do
+ build_gem "sorbet-static", "0.5.11989" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.11989-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ sorbet-static
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-platform ruby", raise_on_error: false
+ end
+
+ nice_error = <<~E.strip
+ Could not find gems matching 'sorbet-static' valid for all resolution platforms (x86_64-linux, ruby) in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static':
+ * sorbet-static-0.5.11989-x86_64-linux
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "respects lower bound ruby requirements" do
+ build_repo4 do
+ build_gem "our_private_gem", "0.1.0" do |s|
+ s.required_ruby_version = ">= #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://localgemserver.test"
+
+ gem "our_private_gem"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://localgemserver.test/
+ specs:
+ our_private_gem (0.1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ our_private_gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ end
+
+ context "when an update is available" do
+ before do
+ gemfile_with_rails_weakling_and_foo_from_repo4
+
+ build_repo4 do
+ build_gem "foo", "2.0"
+ end
+
+ lockfile(expected_lockfile)
+ end
+
+ it "does not implicitly update" do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (1.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+
+ it "accounts for changes in the gemfile" do
+ gemfile gemfile.gsub('"foo"', '"foo", "2.0"')
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "actionmailer", "2.3.2"
+ c.checksum gem_repo4, "actionpack", "2.3.2"
+ c.checksum gem_repo4, "activerecord", "2.3.2"
+ c.checksum gem_repo4, "activeresource", "2.3.2"
+ c.checksum gem_repo4, "activesupport", "2.3.2"
+ c.checksum gem_repo4, "foo", "2.0"
+ c.checksum gem_repo4, "rails", "2.3.2"
+ c.checksum gem_repo4, "rake", rake_version
+ c.checksum gem_repo4, "weakling", "0.0.3"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ foo (2.0)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+ weakling (0.0.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 2.0)
+ rails
+ weakling
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(read_lockfile).to eq(expected_lockfile)
+ end
+ end
+
+ context "when a system gem has incorrect dependencies, different from the lockfile" do
+ before do
+ build_repo4 do
+ build_gem "debug", "1.6.3" do |s|
+ s.add_dependency "irb", ">= 1.3.6"
+ end
+
+ build_gem "irb", "1.5.0"
+ end
+
+ system_gems "irb-1.5.0", gem_repo: gem_repo4
+ system_gems "debug-1.6.3", gem_repo: gem_repo4
+
+ # simulate gemspec with wrong empty dependencies
+ debug_gemspec_path = system_gem_path("specifications/debug-1.6.3.gemspec")
+ debug_gemspec = Gem::Specification.load(debug_gemspec_path.to_s)
+ debug_gemspec.dependencies.clear
+ File.write(debug_gemspec_path, debug_gemspec.to_ruby)
+ end
+
+ it "respects the existing lockfile, even when reresolving" do
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "debug"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "debug", "1.6.3"
+ c.checksum gem_repo4, "irb", "1.5.0"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ debug (1.6.3)
+ irb (>= 1.3.6)
+ irb (1.5.0)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ debug
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-22" do
+ bundle "lock"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ debug (1.6.3)
+ irb (>= 1.3.6)
+ irb (1.5.0)
+
+ PLATFORMS
+ arm64-darwin-22
+ x86_64-linux
+
+ DEPENDENCIES
+ debug
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when a system gem has incorrect dependencies, different from remote gems" do
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "bar", "1.0.0"
+ end
+
+ system_gems "foo-1.0.0", gem_repo: gem_repo4, path: default_bundle_path
+
+ # simulate gemspec with wrong empty dependencies
+ foo_gemspec_path = default_bundle_path("specifications/foo-1.0.0.gemspec")
+ foo_gemspec = Gem::Specification.load(foo_gemspec_path.to_s)
+ foo_gemspec.dependencies.clear
+ File.write(foo_gemspec_path, foo_gemspec.to_ruby)
+ end
+
+ it "generates a lockfile using remote dependencies, and prints a warning" do
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "foo"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "foo", "1.0.0"
+ c.checksum gem_repo4, "bar", "1.0.0"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --verbose"
+ end
+
+ expect(err).to eq("Local specification for foo-1.0.0 has different dependencies than the remote gem, ignoring it")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bar (1.0.0)
+ foo (1.0.0)
+ bar
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "properly shows resolution errors including OR requirements" do
+ build_repo4 do
+ build_gem "activeadmin", "2.13.1" do |s|
+ s.add_dependency "railties", ">= 6.1", "< 7.1"
+ end
+ build_gem "actionpack", "6.1.4"
+ build_gem "actionpack", "7.0.3.1"
+ build_gem "actionpack", "7.0.4"
+ build_gem "railties", "6.1.4" do |s|
+ s.add_dependency "actionpack", "6.1.4"
+ end
+ build_gem "rails", "7.0.3.1" do |s|
+ s.add_dependency "railties", "7.0.3.1"
+ end
+ build_gem "rails", "7.0.4" do |s|
+ s.add_dependency "railties", "7.0.4"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rails", ">= 7.0.3.1"
+ gem "activeadmin", "2.13.1"
+ G
+
+ bundle "lock", raise_on_error: false
+
+ expect(err).to eq <<~ERR.strip
+ Could not find compatible versions
+
+ Because rails >= 7.0.4 depends on railties = 7.0.4
+ and rails < 7.0.4 depends on railties = 7.0.3.1,
+ railties = 7.0.3.1 OR = 7.0.4 is required.
+ So, because railties = 7.0.3.1 OR = 7.0.4 could not be found in rubygems repository https://gem.repo4/ or installed locally,
+ version solving has failed.
+ ERR
+ end
+
+ it "is able to display some explanation on crazy irresolvable cases" do
+ build_repo4 do
+ build_gem "activeadmin", "2.13.1" do |s|
+ s.add_dependency "ransack", "= 3.1.0"
+ end
+
+ # Activemodel is missing as a dependency in lockfile
+ build_gem "ransack", "3.1.0" do |s|
+ s.add_dependency "activemodel", ">= 6.0.4"
+ s.add_dependency "activesupport", ">= 6.0.4"
+ end
+
+ %w[6.0.4 7.0.2.3 7.0.3.1 7.0.4].each do |version|
+ build_gem "activesupport", version
+
+ # Activemodel is only available on 6.0.4
+ if version == "6.0.4"
+ build_gem "activemodel", version do |s|
+ s.add_dependency "activesupport", version
+ end
+ end
+
+ build_gem "rails", version do |s|
+ # Depednencies of Rails 7.0.2.3 are in reverse order
+ if version == "7.0.2.3"
+ s.add_dependency "activesupport", version
+ s.add_dependency "activemodel", version
+ else
+ s.add_dependency "activemodel", version
+ s.add_dependency "activesupport", version
+ end
+ end
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rails", ">= 7.0.2.3"
+ gem "activeadmin", "= 2.13.1"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activeadmin (2.13.1)
+ ransack (= 3.1.0)
+ ransack (3.1.0)
+ activemodel (>= 6.0.4)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ activeadmin (= 2.13.1)
+ ransack (= 3.1.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expected_error = <<~ERR.strip
+ Could not find compatible versions
+
+ Because rails >= 7.0.4 depends on activemodel = 7.0.4
+ and rails >= 7.0.3.1, < 7.0.4 depends on activemodel = 7.0.3.1,
+ rails >= 7.0.3.1 requires activemodel = 7.0.3.1 OR = 7.0.4.
+ (1) So, because rails >= 7.0.2.3, < 7.0.3.1 depends on activemodel = 7.0.2.3
+ and every version of activemodel depends on activesupport = 6.0.4,
+ rails >= 7.0.2.3 requires activesupport = 6.0.4.
+
+ Because rails >= 7.0.2.3, < 7.0.3.1 depends on activesupport = 7.0.2.3
+ and rails >= 7.0.3.1, < 7.0.4 depends on activesupport = 7.0.3.1,
+ rails >= 7.0.2.3, < 7.0.4 requires activesupport = 7.0.2.3 OR = 7.0.3.1.
+ And because rails >= 7.0.4 depends on activesupport = 7.0.4,
+ rails >= 7.0.2.3 requires activesupport = 7.0.2.3 OR = 7.0.3.1 OR = 7.0.4.
+ And because rails >= 7.0.2.3 requires activesupport = 6.0.4 (1),
+ rails >= 7.0.2.3 cannot be used.
+ So, because Gemfile depends on rails >= 7.0.2.3,
+ version solving has failed.
+ ERR
+
+ bundle "lock", raise_on_error: false
+ expect(err).to eq(expected_error)
+
+ lockfile lockfile.gsub(/PLATFORMS\n #{local_platform}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}")
+
+ bundle "lock", raise_on_error: false
+ expect(err).to eq(expected_error)
+ end
+
+ it "does not accidentally resolves to prereleases" do
+ build_repo4 do
+ build_gem "autoproj", "2.0.3" do |s|
+ s.add_dependency "autobuild", ">= 1.10.0.a"
+ s.add_dependency "tty-prompt"
+ end
+
+ build_gem "tty-prompt", "0.6.0"
+ build_gem "tty-prompt", "0.7.0"
+
+ build_gem "autobuild", "1.10.0.b3"
+ build_gem "autobuild", "1.10.1" do |s|
+ s.add_dependency "tty-prompt", "~> 0.6.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "autoproj", ">= 2.0.0"
+ G
+
+ bundle "lock"
+ expect(lockfile).to_not include("autobuild (1.10.0.b3)")
+ expect(lockfile).to include("autobuild (1.10.1)")
+ end
+
+ # Newer rails depends on Bundler, while ancient Rails does not. Bundler tries
+ # a first resolution pass that does not consider pre-releases. However, when
+ # using a pre-release Bundler (like the .dev version), that results in that
+ # pre-release being ignored and resolving to a version that does not depend on
+ # Bundler at all. We should avoid that and still consider .dev Bundler.
+ #
+ it "does not ignore prereleases with there's only one candidate" do
+ build_repo4 do
+ build_gem "rails", "7.4.0.2" do |s|
+ s.add_dependency "bundler", ">= 1.15.0"
+ end
+
+ build_gem "rails", "2.3.18"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "rails"
+ G
+
+ bundle "lock"
+ expect(lockfile).to_not include("rails (2.3.18)")
+ expect(lockfile).to include("rails (7.4.0.2)")
+ end
+
+ it "deals with platform specific incompatibilities" do
+ build_repo4 do
+ build_gem "activerecord", "6.0.6"
+ build_gem "activerecord-jdbc-adapter", "60.4" do |s|
+ s.platform = "java"
+ s.add_dependency "activerecord", "~> 6.0.0"
+ end
+ build_gem "activerecord-jdbc-adapter", "61.0" do |s|
+ s.platform = "java"
+ s.add_dependency "activerecord", "~> 6.1.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "activerecord", "6.0.6"
+ gem "activerecord-jdbc-adapter", "61.0"
+ G
+
+ simulate_platform "universal-java-19" do
+ bundle "lock", raise_on_error: false
+ end
+
+ expect(err).to include("Could not find compatible versions")
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+ end
+
+ it "adds checksums to an existing lockfile, when re-resolving is necessary" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ # lockfile has a typo (nogokiri) in the dependencies section, so Bundler
+ # sees dependencies have changed, and re-resolves
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nogokiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-checksums"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "adds checksums to an existing lockfile, when no re-resolve is necessary" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-checksums"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "adds checksums when source is not specified" do
+ system_gems(%w[myrack-1.0.0], path: default_bundle_path)
+
+ gemfile <<-G
+ gem "myrack"
+ G
+
+ lockfile <<~L
+ GEM
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --add-checksums"
+ end
+
+ # myrack is coming from gem_repo1
+ # but it's simulated to install in the system gems path
+ checksums = checksums_section do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "adds checksums to an existing lockfile, when gems are already installed" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install"
+
+ bundle "lock --add-checksums"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "generates checksums by default" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "disables checksums if configured to do so" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ bundle_config "lockfile_checksums false"
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "add checksums for gems installed on disk" do
+ build_repo4 do
+ build_gem "warning", "18.0.0"
+ end
+
+ bundle_config "lockfile_checksums false"
+
+ simulate_platform "x86_64-linux" do
+ install_gemfile(<<-G, artifice: "endpoint")
+ source "https://gem.repo4"
+
+ gem "warning"
+ G
+
+ bundle "config --delete lockfile_checksums"
+ bundle("lock --add-checksums", artifice: "endpoint")
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "warning", "18.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ warning (18.0.0)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ warning
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "doesn't add checksum for gems not installed on disk" do
+ lockfile(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ warning (18.0.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ warning
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile(<<~G)
+ source "https://gem.repo4"
+
+ gem "warning"
+ G
+
+ build_repo4 do
+ build_gem "warning", "18.0.0"
+ end
+
+ FileUtils.rm_rf("#{gem_repo4}/gems")
+
+ bundle("lock --add-checksums", artifice: "endpoint")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "warning", "18.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ warning (18.0.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ warning
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ context "when re-resolving to include prereleases" do
+ before do
+ build_repo4 do
+ build_gem "tzinfo-data", "1.2022.7"
+ build_gem "rails", "7.1.0.alpha" do |s|
+ s.add_dependency "activesupport"
+ end
+ build_gem "activesupport", "7.1.0.alpha"
+ end
+ end
+
+ it "does not end up including gems scoped to other platforms in the lockfile" do
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "rails"
+ gem "tzinfo-data", platform: :windows
+ G
+
+ simulate_platform "x86_64-darwin-22" do
+ bundle "lock"
+ end
+
+ expect(lockfile).not_to include("tzinfo-data (1.2022.7)")
+ end
+ end
+
+ context "when resolving platform specific gems as indirect dependencies on truffleruby", :truffleruby_only do
+ before do
+ build_lib "foo", path: bundled_app do |s|
+ s.add_dependency "nokogiri"
+ end
+
+ build_repo4 do
+ build_gem "nokogiri", "1.14.2"
+ build_gem "nokogiri", "1.14.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gemspec
+ G
+ end
+
+ it "locks both ruby and platform specific specs" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ nokogiri
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ context "and a lockfile with platform specific gems only already exists" do
+ before do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ lockfile <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ nokogiri
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "keeps platform specific gems" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.2"
+ c.checksum gem_repo4, "nokogiri", "1.14.2", "x86_64-linux"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ nokogiri
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.2)
+ nokogiri (1.14.2-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+
+ context "when adding a new gem that requires unlocking other transitive deps" do
+ before do
+ build_repo4 do
+ build_gem "govuk_app_config", "0.1.0"
+
+ build_gem "govuk_app_config", "4.13.0" do |s|
+ s.add_dependency "railties", ">= 5.0"
+ end
+
+ %w[7.0.4.1 7.0.4.3].each do |v|
+ build_gem "railties", v do |s|
+ s.add_dependency "actionpack", v
+ s.add_dependency "activesupport", v
+ end
+
+ build_gem "activesupport", v
+ build_gem "actionpack", v
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "govuk_app_config"
+ gem "activesupport", "7.0.4.3"
+ G
+
+ # Simulate out of sync lockfile because top level dependency on
+ # activesuport has just been added to the Gemfile, and locked to a higher
+ # version
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionpack (7.0.4.1)
+ activesupport (7.0.4.1)
+ govuk_app_config (4.13.0)
+ railties (>= 5.0)
+ railties (7.0.4.1)
+ actionpack (= 7.0.4.1)
+ activesupport (= 7.0.4.1)
+
+ PLATFORMS
+ arm64-darwin-22
+
+ DEPENDENCIES
+ govuk_app_config
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not downgrade top level dependencies" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "actionpack", "7.0.4.3"
+ c.no_checksum "activesupport", "7.0.4.3"
+ c.no_checksum "govuk_app_config", "4.13.0"
+ c.no_checksum "railties", "7.0.4.3"
+ end
+
+ simulate_platform "arm64-darwin-22" do
+ bundle "lock"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actionpack (7.0.4.3)
+ activesupport (7.0.4.3)
+ govuk_app_config (4.13.0)
+ railties (>= 5.0)
+ railties (7.0.4.3)
+ actionpack (= 7.0.4.3)
+ activesupport (= 7.0.4.3)
+
+ PLATFORMS
+ arm64-darwin-22
+
+ DEPENDENCIES
+ activesupport (= 7.0.4.3)
+ govuk_app_config
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when lockfile has incorrectly indented platforms" do
+ before do
+ build_repo4 do
+ build_gem "ffi", "1.1.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "ffi", "1.1.0" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ffi"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.1.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+
+ DEPENDENCIES
+ ffi
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "does not remove any gems" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.1.0-arm64-darwin)
+ ffi (1.1.0-x86_64-linux)
+
+ PLATFORMS
+ arm64-darwin
+ x86_64-linux
+
+ DEPENDENCIES
+ ffi
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ describe "--normalize-platforms on linux" do
+ let(:normalized_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+ irb (1.0.0-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "irb", "1.0.0"
+
+ build_gem "irb", "1.0.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "irb"
+ G
+ end
+
+ context "when already normalized" do
+ before do
+ lockfile normalized_lockfile
+ end
+
+ it "is a noop" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when not already normalized" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms and native gems in the lockfile" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+ end
+
+ describe "--normalize-platforms on darwin" do
+ let(:normalized_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+ irb (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin
+ ruby
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "irb", "1.0.0"
+
+ build_gem "irb", "1.0.0" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "irb"
+ G
+ end
+
+ context "when already normalized" do
+ before do
+ lockfile normalized_lockfile
+ end
+
+ it "is a noop" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when having only ruby" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms and native gems in the lockfile" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when having only the current platform with version" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin-23
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms by removing version" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+
+ context "when having other platforms with version" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ irb (1.0.0-arm64-darwin)
+
+ PLATFORMS
+ arm64-darwin-22
+
+ DEPENDENCIES
+ irb
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "normalizes the list of platforms by removing version" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(normalized_lockfile)
+ end
+ end
+ end
+
+ describe "--normalize-platforms with gems without generic variant" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (1.0-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ sorbet-static
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "sorbet-static" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static"
+ G
+
+ lockfile original_lockfile
+ end
+
+ it "removes invalid platforms" do
+ simulate_platform "x86_64-linux" do
+ bundle "lock --normalize-platforms"
+ end
+
+ expect(lockfile).to eq(original_lockfile.gsub(/^ ruby\n/m, ""))
+ end
+ end
+end
diff --git a/spec/bundler/commands/newgem_spec.rb b/spec/bundler/commands/newgem_spec.rb
new file mode 100644
index 0000000000..65fbad05aa
--- /dev/null
+++ b/spec/bundler/commands/newgem_spec.rb
@@ -0,0 +1,2139 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle gem" do
+ def gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist
+ expect(bundled_app("#{gem_name}/README.md")).to exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to exist
+ expect(bundled_app("#{gem_name}/Rakefile")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist
+
+ expect(ignore_paths).to include("bin/")
+ expect(ignore_paths).to include("Gemfile")
+ end
+
+ def bundle_exec_rubocop
+ prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec"))
+ bundle "config set path #{rubocop_gem_path}", dir: bundled_app(gem_name)
+ bundle "exec rubocop --debug --config .rubocop.yml", dir: bundled_app(gem_name)
+ end
+
+ def bundle_exec_standardrb
+ prepare_gemspec(bundled_app(gem_name, "#{gem_name}.gemspec"))
+ bundle "config set path #{standard_gem_path}", dir: bundled_app(gem_name)
+ bundle "exec standardrb --debug", dir: bundled_app(gem_name)
+ end
+
+ def ignore_paths
+ generated = bundled_app("#{gem_name}/#{gem_name}.gemspec").read
+ matched = generated.match(/^\s+f\.start_with\?\(\*%w\[(?<ignored>.*)\]\)$/)
+ matched[:ignored]&.split(" ")
+ end
+
+ def installed_go?
+ sys_exec("go version", raise_on_error: true)
+ true
+ rescue StandardError
+ false
+ end
+
+ let(:generated_gemspec) { Bundler.load_gemspec_uncached(bundled_app(gem_name).join("#{gem_name}.gemspec")) }
+
+ let(:gem_name) { "mygem" }
+
+ before do
+ git("config --global user.name 'Bundler User'")
+ git("config --global user.email user@example.com")
+ git("config --global github.user bundleuser")
+
+ bundle_config_global "gem.mit false"
+ bundle_config_global "gem.test false"
+ bundle_config_global "gem.coc false"
+ bundle_config_global "gem.linter false"
+ bundle_config_global "gem.ci false"
+ bundle_config_global "gem.changelog false"
+ bundle_config_global "gem.bundle false"
+ end
+
+ describe "git repo initialization" do
+ it "generates a gem skeleton with a .git folder" do
+ bundle "gem #{gem_name}"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/.git")).to exist
+ end
+
+ it "generates a gem skeleton with a .git folder when passing --git" do
+ bundle "gem #{gem_name} --git"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/.git")).to exist
+ end
+
+ it "generates a gem skeleton without a .git folder when passing --no-git" do
+ bundle "gem #{gem_name} --no-git"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/.git")).not_to exist
+ end
+
+ context "on a path with spaces" do
+ before do
+ Dir.mkdir(bundled_app("path with spaces"))
+ end
+
+ it "properly initializes git repo" do
+ skip "path with spaces needs special handling on Windows" if Gem.win_platform?
+
+ bundle "gem #{gem_name}", dir: bundled_app("path with spaces")
+ expect(bundled_app("path with spaces/#{gem_name}/.git")).to exist
+ end
+ end
+ end
+
+ shared_examples_for "--mit flag" do
+ before do
+ bundle "gem #{gem_name} --mit"
+ end
+ it "generates a gem skeleton with MIT license" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/LICENSE.txt")).to exist
+ expect(generated_gemspec.license).to eq("MIT")
+ end
+ end
+
+ shared_examples_for "--no-mit flag" do
+ before do
+ bundle "gem #{gem_name} --no-mit"
+ end
+ it "generates a gem skeleton without MIT license" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/LICENSE.txt")).to_not exist
+ end
+ end
+
+ shared_examples_for "--coc flag" do
+ it "generates a gem skeleton with MIT license" do
+ bundle "gem #{gem_name} --coc"
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to exist
+ end
+
+ it "generates the README with a section for the Code of Conduct" do
+ bundle "gem #{gem_name} --coc"
+ expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct")
+ expect(bundled_app("#{gem_name}/README.md").read).to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md})
+ end
+
+ it "generates the README with a section for the Code of Conduct, respecting the configured git default branch", git: ">= 2.28.0" do
+ git("config --global init.defaultBranch main")
+ bundle "gem #{gem_name} --coc"
+
+ expect(bundled_app("#{gem_name}/README.md").read).to include("## Code of Conduct")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("https://github.com/bundleuser/#{gem_name}/blob/main/CODE_OF_CONDUCT.md")
+ end
+ end
+
+ shared_examples_for "--no-coc flag" do
+ before do
+ bundle "gem #{gem_name} --no-coc"
+ end
+ it "generates a gem skeleton without Code of Conduct" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CODE_OF_CONDUCT.md")).to_not exist
+ end
+
+ it "generates the README without a section for the Code of Conduct" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("## Code of Conduct")
+ expect(bundled_app("#{gem_name}/README.md").read).not_to match(%r{https://github\.com/bundleuser/#{gem_name}/blob/.*/CODE_OF_CONDUCT.md})
+ end
+ end
+
+ shared_examples_for "--changelog flag" do
+ before do
+ bundle "gem #{gem_name} --changelog"
+ end
+ it "generates a gem skeleton with a CHANGELOG" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CHANGELOG.md")).to exist
+ end
+ end
+
+ shared_examples_for "--no-changelog flag" do
+ before do
+ bundle "gem #{gem_name} --no-changelog"
+ end
+ it "generates a gem skeleton without a CHANGELOG" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/CHANGELOG.md")).to_not exist
+ end
+ end
+
+ shared_examples_for "--bundle flag" do
+ before do
+ bundle "gem #{gem_name} --bundle"
+ end
+ it "generates a gem skeleton with bundle install" do
+ gem_skeleton_assertions
+ expect(out).to include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ shared_examples_for "--no-bundle flag" do
+ before do
+ bundle "gem #{gem_name} --no-bundle"
+ end
+ it "generates a gem skeleton without bundle install" do
+ gem_skeleton_assertions
+ expect(out).to_not include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ shared_examples_for "--linter=rubocop flag" do
+ before do
+ bundle "gem #{gem_name} --linter=rubocop"
+ end
+
+ it "generates a gem skeleton with rubocop" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/Rakefile")).to read_as(
+ include("# frozen_string_literal: true").
+ and(include('require "rubocop/rake_task"').
+ and(include("RuboCop::RakeTask.new").
+ and(match(/default:.+:rubocop/))))
+ )
+ end
+
+ it "includes rubocop in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" }
+ expect(rubocop_dep).not_to be_specific
+ expect(rubocop_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "generates a default .rubocop.yml" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ end
+ end
+
+ shared_examples_for "--linter=standard flag" do
+ before do
+ bundle "gem #{gem_name} --linter=standard"
+ end
+
+ it "generates a gem skeleton with standard" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/Rakefile")).to read_as(
+ include('require "standard/rake"').
+ and(match(/default:.+:standard/))
+ )
+ end
+
+ it "includes standard in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ standard_dep = builder.dependencies.find {|d| d.name == "standard" }
+ expect(standard_dep).not_to be_specific
+ expect(standard_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "generates a default .standard.yml" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to exist
+ end
+
+ it "includes .standard.yml into ignore list" do
+ expect(ignore_paths).to include(".standard.yml")
+ end
+ end
+
+ shared_examples_for "--no-linter flag" do
+ define_negated_matcher :exclude, :include
+
+ before do
+ bundle "gem #{gem_name} --no-linter"
+ end
+
+ it "generates a gem skeleton without rubocop" do
+ gem_skeleton_assertions
+ expect(bundled_app("#{gem_name}/Rakefile")).to read_as(exclude("rubocop"))
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to read_as(exclude("rubocop"))
+ end
+
+ it "does not include rubocop in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ rubocop_dep = builder.dependencies.find {|d| d.name == "rubocop" }
+ expect(rubocop_dep).to be_nil
+ end
+
+ it "does not include standard in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ standard_dep = builder.dependencies.find {|d| d.name == "standard" }
+ expect(standard_dep).to be_nil
+ end
+
+ it "doesn't generate a default .rubocop.yml" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ end
+
+ it "does not add .rubocop.yml into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ end
+
+ it "doesn't generate a default .standard.yml" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add .standard.yml into ignore list" do
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ it "has no rubocop offenses when using --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c, --test=minitest, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --test=minitest --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c, --test=rspec, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --test=rspec --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=c, --test=test-unit, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --ext=c --test=test-unit --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no standard offenses when using --linter=standard flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+ bundle "gem #{gem_name} --linter=standard"
+ bundle_exec_standardrb
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust, --test=minitest, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --test=minitest --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust, --test=rspec, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --test=rspec --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ it "has no rubocop offenses when using --ext=rust, --test=test-unit, and --linter=rubocop flag" do
+ skip "ruby_core has an 'ast.rb' file that gets in the middle and breaks this spec" if ruby_core?
+
+ bundle "gem #{gem_name} --ext=rust --test=test-unit --linter=rubocop"
+ bundle_exec_rubocop
+ expect(last_command).to be_success
+ end
+
+ shared_examples_for "CI config is absent" do
+ it "does not create any CI files" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+ end
+
+ shared_examples_for "test framework is absent" do
+ it "does not create any test framework files" do
+ expect(bundled_app("#{gem_name}/.rspec")).to_not exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/test/#{gem_name}.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to_not exist
+ end
+
+ it "does not add any test framework files into ignore list" do
+ expect(ignore_paths).not_to include("test/")
+ expect(ignore_paths).not_to include(".rspec")
+ expect(ignore_paths).not_to include("spec/")
+ end
+ end
+
+ context "README.md" do
+ context "git config github.user present" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "contribute URL set to git username" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/bundleuser")
+ end
+ end
+
+ context "git config github.user is absent" do
+ before do
+ git("config --global --unset github.user")
+ bundle "gem #{gem_name}"
+ end
+
+ it "contribute URL set to [USERNAME]" do
+ expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser")
+ end
+ end
+
+ describe "test task name on readme" do
+ shared_examples_for "test task name on readme" do |framework, task_name|
+ before do
+ bundle "gem #{gem_name} --test=#{framework}"
+ end
+
+ it "renders with correct name" do
+ expect(bundled_app("#{gem_name}/README.md").read).to include("Then, run `rake #{task_name}` to run the tests.")
+ end
+ end
+
+ it_behaves_like "test task name on readme", "test-unit", "test"
+ it_behaves_like "test task name on readme", "minitest", "test"
+ it_behaves_like "test task name on readme", "rspec", "spec"
+ end
+ end
+
+ it "creates a new git repository" do
+ bundle "gem #{gem_name}"
+ expect(bundled_app("#{gem_name}/.git")).to exist
+ end
+
+ context "when git is not available" do
+ # This spec cannot have `git` available in the test env
+ before do
+ bundle "gem #{gem_name}", env: { "PATH" => "" }
+ end
+
+ it "creates the gem without the need for git" do
+ expect(bundled_app("#{gem_name}/README.md")).to exist
+ end
+
+ it "doesn't create a git repo" do
+ expect(bundled_app("#{gem_name}/.git")).to_not exist
+ end
+
+ it "doesn't create a .gitignore file" do
+ expect(bundled_app("#{gem_name}/.gitignore")).to_not exist
+ end
+
+ it "does not add .gitignore into ignore list" do
+ expect(ignore_paths).not_to include(".gitignore")
+ end
+ end
+
+ it "generates a valid gemspec" do
+ bundle "gem newgem --bin"
+
+ prepare_gemspec(bundled_app("newgem", "newgem.gemspec"))
+
+ build_repo2 do
+ build_dummy_irb "9.9.9"
+ end
+ gems = ["rake-#{rake_version}", "irb-9.9.9"]
+ system_gems gems, path: system_gem_path, gem_repo: gem_repo2
+ bundle "exec rake build", dir: bundled_app("newgem")
+
+ expect(stdboth).not_to include("ERROR")
+ end
+
+ context "gem naming with relative paths" do
+ it "resolves ." do
+ create_temporary_dir("tmp")
+
+ bundle "gem .", dir: bundled_app("tmp")
+
+ expect(bundled_app("tmp/lib/tmp.rb")).to exist
+ end
+
+ it "resolves .." do
+ create_temporary_dir("temp/empty_dir")
+
+ bundle "gem ..", dir: bundled_app("temp/empty_dir")
+
+ expect(bundled_app("temp/lib/temp.rb")).to exist
+ end
+
+ it "resolves relative directory" do
+ create_temporary_dir("tmp/empty/tmp")
+
+ bundle "gem ../../empty", dir: bundled_app("tmp/empty/tmp")
+
+ expect(bundled_app("tmp/empty/lib/empty.rb")).to exist
+ end
+
+ def create_temporary_dir(dir)
+ FileUtils.mkdir_p(bundled_app(dir))
+ end
+ end
+
+ shared_examples_for "--github-username option" do |github_username|
+ before do
+ bundle "gem #{gem_name} --github-username=#{github_username}"
+ end
+
+ it "generates a gem skeleton" do
+ gem_skeleton_assertions
+ end
+
+ it "contribute URL set to given github username" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/#{github_username}")
+ end
+ end
+
+ shared_examples_for "github_username configuration" do
+ context "with github_username setting set to some value" do
+ before do
+ bundle_config_global "gem.github_username different_username"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a gem skeleton" do
+ gem_skeleton_assertions
+ end
+
+ it "contribute URL set to bundle config setting" do
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).to include("github.com/different_username")
+ end
+ end
+
+ context "with github_username setting set to false" do
+ before do
+ bundle_config_global "gem.github_username false"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a gem skeleton" do
+ gem_skeleton_assertions
+ end
+
+ it "contribute URL set to [USERNAME]" do
+ expect(bundled_app("#{gem_name}/README.md").read).to include("[USERNAME]")
+ expect(bundled_app("#{gem_name}/README.md").read).not_to include("github.com/bundleuser")
+ end
+ end
+ end
+
+ it "generates a gem skeleton" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec")).to exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to exist
+ expect(bundled_app("#{gem_name}/Rakefile")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb")).to exist
+ expect(bundled_app("#{gem_name}/sig/#{gem_name}.rbs")).to exist
+ expect(bundled_app("#{gem_name}/.gitignore")).to exist
+
+ expect(bundled_app("#{gem_name}/bin/setup")).to exist
+ expect(bundled_app("#{gem_name}/bin/console")).to exist
+
+ unless Gem.win_platform?
+ expect(bundled_app("#{gem_name}/bin/setup")).to be_executable
+ expect(bundled_app("#{gem_name}/bin/console")).to be_executable
+ end
+
+ expect(bundled_app("#{gem_name}/bin/setup").read).to start_with("#!")
+ expect(bundled_app("#{gem_name}/bin/console").read).to start_with("#!")
+ end
+
+ it "includes bin/ into ignore list" do
+ bundle "gem #{gem_name}"
+
+ expect(ignore_paths).to include("bin/")
+ end
+
+ it "includes Gemfile into ignore list" do
+ bundle "gem #{gem_name}"
+
+ expect(ignore_paths).to include("Gemfile")
+ end
+
+ it "includes .gitignore into ignore list" do
+ bundle "gem #{gem_name}"
+
+ expect(ignore_paths).to include(".gitignore")
+ end
+
+ it "starts with version 0.1.0" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}/version.rb").read).to match(/VERSION = "0.1.0"/)
+ end
+
+ it "declare String type for VERSION constant" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/sig/#{gem_name}.rbs").read).to match(/VERSION: String/)
+ end
+
+ context "git config user.{name,email} is set" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "sets gemspec author to git user.name if available" do
+ expect(generated_gemspec.authors.first).to eq("Bundler User")
+ end
+
+ it "sets gemspec email to git user.email if available" do
+ expect(generated_gemspec.email.first).to eq("user@example.com")
+ end
+ end
+
+ context "git config user.{name,email} is not set" do
+ before do
+ git("config --global --unset user.name")
+ git("config --global --unset user.email")
+ bundle "gem #{gem_name}"
+ end
+
+ it "sets gemspec author to default message if git user.name is not set or empty" do
+ expect(generated_gemspec.authors.first).to eq("TODO: Write your name")
+ end
+
+ it "sets gemspec email to default message if git user.email is not set or empty" do
+ expect(generated_gemspec.email.first).to eq("TODO: Write your email address")
+ end
+ end
+
+ it "sets gemspec metadata['allowed_push_host']" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.metadata["allowed_push_host"]).
+ to match(/example\.com/)
+ end
+
+ it "includes a commented-out rubygems_mfa_required metadata hint" do
+ bundle "gem #{gem_name}"
+
+ gemspec_contents = bundled_app("#{gem_name}/#{gem_name}.gemspec").read
+
+ expect(gemspec_contents).to include('# spec.metadata["rubygems_mfa_required"] = "true"')
+ expect(gemspec_contents).to include("https://guides.rubygems.org/mfa-requirement-opt-in/")
+ end
+
+ it "sets a minimum ruby version" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.required_ruby_version.to_s).to start_with(">=")
+ end
+
+ it "does not include the gemspec file in files" do
+ bundle "gem #{gem_name}"
+
+ bundler_gemspec = Bundler::GemHelper.new(bundled_app(gem_name), gem_name).gemspec
+
+ expect(bundler_gemspec.files).not_to include("#{gem_name}.gemspec")
+ end
+
+ it "does not include the Gemfile file in files" do
+ bundle "gem #{gem_name}"
+
+ bundler_gemspec = Bundler::GemHelper.new(bundled_app(gem_name), gem_name).gemspec
+
+ expect(bundler_gemspec.files).not_to include("Gemfile")
+ end
+
+ it "runs rake without problems" do
+ bundle "gem #{gem_name}"
+
+ system_gems ["rake-#{rake_version}"]
+
+ rakefile = <<~RAKEFILE
+ task :default do
+ puts 'SUCCESS'
+ end
+ RAKEFILE
+ File.open(bundled_app("#{gem_name}/Rakefile"), "w") do |file|
+ file.puts rakefile
+ end
+
+ sys_exec("rake", dir: bundled_app(gem_name))
+ expect(out).to include("SUCCESS")
+ end
+
+ context "--exe parameter set" do
+ before do
+ bundle "gem #{gem_name} --exe"
+ end
+
+ it "builds exe skeleton" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ unless Gem.win_platform?
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to be_executable
+ end
+ end
+ end
+
+ context "--bin parameter set" do
+ before do
+ bundle "gem #{gem_name} --bin"
+ end
+
+ it "builds exe skeleton" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ end
+ end
+
+ context "no --test parameter" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "--test parameter set to rspec" do
+ before do
+ bundle "gem #{gem_name} --test=rspec"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/.rspec")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ end
+
+ it "includes .rspec and spec/ into ignore list" do
+ expect(ignore_paths).to include(".rspec")
+ expect(ignore_paths).to include("spec/")
+ end
+
+ it "depends on a non-specific version of rspec in generated Gemfile" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ rspec_dep = builder.dependencies.find {|d| d.name == "rspec" }
+ expect(rspec_dep).not_to be_specific
+ expect(rspec_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+ end
+
+ context "init_gems_rb setting to true" do
+ before do
+ bundle_config "init_gems_rb true"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates gems.rb instead of Gemfile" do
+ expect(bundled_app("#{gem_name}/gems.rb")).to exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to_not exist
+ end
+
+ it "includes gems.rb and gems.locked into ignore list" do
+ expect(ignore_paths).to include("gems.rb")
+ expect(ignore_paths).to include("gems.locked")
+ expect(ignore_paths).not_to include("Gemfile")
+ end
+ end
+
+ context "init_gems_rb setting to false" do
+ before do
+ bundle_config "init_gems_rb false"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates Gemfile instead of gems.rb" do
+ expect(bundled_app("#{gem_name}/gems.rb")).to_not exist
+ expect(bundled_app("#{gem_name}/Gemfile")).to exist
+ end
+
+ it "includes Gemfile into ignore list" do
+ expect(ignore_paths).to include("Gemfile")
+ expect(ignore_paths).not_to include("gems.rb")
+ expect(ignore_paths).not_to include("gems.locked")
+ end
+ end
+
+ context "gem.test setting set to rspec" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name}"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/.rspec")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ end
+
+ it "includes .rspec and spec/ into ignore list" do
+ expect(ignore_paths).to include(".rspec")
+ expect(ignore_paths).to include("spec/")
+ end
+ end
+
+ context "gem.test setting set to rspec and --test is set to minitest" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name} --test=minitest"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/test/test_#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ end
+
+ it "includes test/ into ignore list" do
+ expect(ignore_paths).to include("test/")
+ end
+ end
+
+ context "--test parameter set to minitest" do
+ before do
+ bundle "gem #{gem_name} --test=minitest"
+ end
+
+ it "depends on a non-specific version of minitest" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ minitest_dep = builder.dependencies.find {|d| d.name == "minitest" }
+ expect(minitest_dep).not_to be_specific
+ expect(minitest_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/test/test_#{gem_name}.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ end
+
+ it "includes test/ into ignore list" do
+ expect(ignore_paths).to include("test/")
+ end
+
+ it "creates a default rake task to run the test suite" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "minitest/test_task"
+
+ Minitest::TestTask.create
+
+ task default: :test
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "gem.test setting set to minitest" do
+ before do
+ bundle_config "gem.test minitest"
+ bundle "gem #{gem_name}"
+ end
+
+ it "creates a default rake task to run the test suite" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "minitest/test_task"
+
+ Minitest::TestTask.create
+
+ task default: :test
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "--test parameter set to test-unit" do
+ before do
+ bundle "gem #{gem_name} --test=test-unit"
+ end
+
+ it "depends on a non-specific version of test-unit" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ builder = Bundler::Dsl.new
+ builder.eval_gemfile(bundled_app("#{gem_name}/Gemfile"))
+ builder.dependencies
+ test_unit_dep = builder.dependencies.find {|d| d.name == "test-unit" }
+ expect(test_unit_dep).not_to be_specific
+ expect(test_unit_dep.requirement).to eq(Gem::Requirement.new([">= 0"]))
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/test/#{gem_name}_test.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ end
+
+ it "includes test/ into ignore list" do
+ expect(ignore_paths).to include("test/")
+ end
+
+ it "creates a default rake task to run the test suite" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rake/testtask"
+
+ Rake::TestTask.new(:test) do |t|
+ t.libs << "test"
+ t.libs << "lib"
+ t.test_files = FileList["test/**/*_test.rb"]
+ end
+
+ task default: :test
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "--test parameter set to an invalid value" do
+ before do
+ bundle "gem #{gem_name} --test=foo", raise_on_error: false
+ end
+
+ it "fails loudly" do
+ expect(last_command).to be_failure
+ expect(err).to match(/Expected '--test' to be one of .*; got foo/)
+ end
+ end
+
+ context "gem.test set to rspec and --test with no arguments" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name} --test"
+ end
+
+ it "builds spec skeleton" do
+ expect(bundled_app("#{gem_name}/.rspec")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{gem_name}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ end
+
+ it "includes .rspec and spec/ into ignore list" do
+ expect(ignore_paths).to include(".rspec")
+ expect(ignore_paths).to include("spec/")
+ end
+
+ it "hints that --test is already configured" do
+ expect(out).to match("rspec is already configured, ignoring --test flag.")
+ end
+ end
+
+ context "gem.test setting set to false and --test with no arguments", :readline do
+ before do
+ bundle_config "gem.test false"
+ bundle "gem #{gem_name} --test" do |input, _, _|
+ input.puts
+ end
+ end
+
+ it "asks to generate test files" do
+ expect(out).to match("Do you want to generate tests with your gem?")
+ end
+
+ it "hints that the choice will only be applied to the current gem" do
+ expect(out).to match("Your choice will only be applied to this gem.")
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "gem.test setting not set and --test with no arguments", :readline do
+ before do
+ bundle_config_global "BUNDLE_GEM__TEST" => nil
+ bundle "gem #{gem_name} --test" do |input, _, _|
+ input.puts
+ end
+ end
+
+ it "asks to generate test files" do
+ expect(out).to match("Do you want to generate tests with your gem?")
+ end
+
+ it "hints that the choice will be applied to future bundle gem calls" do
+ hint = "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.test`."
+ expect(out).to match(hint)
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "gem.test setting set to a test framework and --no-test" do
+ before do
+ bundle_config "gem.test rspec"
+ bundle "gem #{gem_name} --no-test"
+ end
+
+ it_behaves_like "test framework is absent"
+ end
+
+ context "--ci with no argument" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "does not generate any CI config" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+
+ it "does not add any CI config files into ignore list" do
+ expect(ignore_paths).not_to include(".github/")
+ expect(ignore_paths).not_to include(".gitlab-ci.yml")
+ expect(ignore_paths).not_to include(".circleci/")
+ end
+ end
+
+ context "--ci set to github" do
+ before do
+ bundle "gem #{gem_name} --ci=github"
+ end
+
+ it "generates a GitHub Actions config file" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ end
+
+ it "includes .github/ into ignore list" do
+ expect(ignore_paths).to include(".github/")
+ end
+ end
+
+ context "--ci set to gitlab" do
+ before do
+ bundle "gem #{gem_name} --ci=gitlab"
+ end
+
+ it "generates a GitLab CI config file" do
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
+ end
+
+ it "includes .gitlab-ci.yml into ignore list" do
+ expect(ignore_paths).to include(".gitlab-ci.yml")
+ end
+ end
+
+ context "--ci set to circle" do
+ before do
+ bundle "gem #{gem_name} --ci=circle"
+ end
+
+ it "generates a CircleCI config file" do
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
+ end
+
+ it "includes .circleci/ into ignore list" do
+ expect(ignore_paths).to include(".circleci/")
+ end
+ end
+
+ context "--ci set to an invalid value" do
+ before do
+ bundle "gem #{gem_name} --ci=foo", raise_on_error: false
+ end
+
+ it "fails loudly" do
+ expect(last_command).to be_failure
+ expect(err).to match(/Expected '--ci' to be one of .*; got foo/)
+ end
+ end
+
+ context "gem.ci setting set to none" do
+ it "doesn't generate any CI config" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+ end
+
+ context "gem.ci setting set to github" do
+ it "generates a GitHub Actions config file" do
+ bundle_config "gem.ci github"
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ end
+ end
+
+ context "gem.ci setting set to gitlab" do
+ it "generates a GitLab CI config file" do
+ bundle_config "gem.ci gitlab"
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
+ end
+ end
+
+ context "gem.ci setting set to circle" do
+ it "generates a CircleCI config file" do
+ bundle_config "gem.ci circle"
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
+ end
+ end
+
+ context "gem.ci set to github and --ci with no arguments" do
+ before do
+ bundle_config "gem.ci github"
+ bundle "gem #{gem_name} --ci"
+ end
+
+ it "generates a GitHub Actions config file" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ end
+
+ it "hints that --ci is already configured" do
+ expect(out).to match("github is already configured, ignoring --ci flag.")
+ end
+ end
+
+ context "gem.ci setting set to false and --ci with no arguments", :readline do
+ before do
+ bundle_config "gem.ci false"
+ bundle "gem #{gem_name} --ci" do |input, _, _|
+ input.puts "github"
+ end
+ end
+
+ it "asks to setup CI" do
+ expect(out).to match("Do you want to set up continuous integration for your gem?")
+ end
+
+ it "hints that the choice will only be applied to the current gem" do
+ expect(out).to match("Your choice will only be applied to this gem.")
+ end
+ end
+
+ context "gem.ci setting not set and --ci with no arguments", :readline do
+ before do
+ bundle_config_global "BUNDLE_GEM__CI" => nil
+ bundle "gem #{gem_name} --ci" do |input, _, _|
+ input.puts "github"
+ end
+ end
+
+ it "asks to setup CI" do
+ expect(out).to match("Do you want to set up continuous integration for your gem?")
+ end
+
+ it "hints that the choice will be applied to future bundle gem calls" do
+ hint = "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.ci`."
+ expect(out).to match(hint)
+ end
+ end
+
+ context "gem.ci setting set to a CI service and --no-ci" do
+ before do
+ bundle_config "gem.ci github"
+ bundle "gem #{gem_name} --no-ci"
+ end
+
+ it "does not generate any CI config" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to_not exist
+ end
+ end
+
+ context "--linter with no argument" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "does not generate any linter config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add any linter config files into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "--linter set to rubocop" do
+ before do
+ bundle "gem #{gem_name} --linter=rubocop"
+ end
+
+ it "generates a RuboCop config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "--linter set to standard" do
+ before do
+ bundle "gem #{gem_name} --linter=standard"
+ end
+
+ it "generates a Standard config" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to exist
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ end
+
+ it "includes .standard.yml into ignore list" do
+ expect(ignore_paths).to include(".standard.yml")
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ end
+ end
+
+ context "--linter set to an invalid value" do
+ before do
+ bundle "gem #{gem_name} --linter=foo", raise_on_error: false
+ end
+
+ it "fails loudly" do
+ expect(last_command).to be_failure
+ expect(err).to match(/Expected '--linter' to be one of .*; got foo/)
+ end
+ end
+
+ context "gem.linter setting set to none" do
+ before do
+ bundle "gem #{gem_name}"
+ end
+
+ it "doesn't generate any linter config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add any linter config files into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "gem.linter setting set to rubocop" do
+ before do
+ bundle_config "gem.linter rubocop"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a RuboCop config file" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ end
+ end
+
+ context "gem.linter setting set to standard" do
+ before do
+ bundle_config "gem.linter standard"
+ bundle "gem #{gem_name}"
+ end
+
+ it "generates a Standard config file" do
+ expect(bundled_app("#{gem_name}/.standard.yml")).to exist
+ end
+
+ it "includes .standard.yml into ignore list" do
+ expect(ignore_paths).to include(".standard.yml")
+ end
+ end
+
+ context "gem.linter set to rubocop and --linter with no arguments" do
+ before do
+ bundle_config "gem.linter rubocop"
+ bundle "gem #{gem_name} --linter"
+ end
+
+ it "generates a RuboCop config file" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to exist
+ end
+
+ it "includes .rubocop.yml into ignore list" do
+ expect(ignore_paths).to include(".rubocop.yml")
+ end
+
+ it "hints that --linter is already configured" do
+ expect(out).to match("rubocop is already configured, ignoring --linter flag.")
+ end
+ end
+
+ context "gem.linter setting set to false and --linter with no arguments", :readline do
+ before do
+ bundle_config "gem.linter false"
+ bundle "gem #{gem_name} --linter" do |input, _, _|
+ input.puts "rubocop"
+ end
+ end
+
+ it "asks to setup a linter" do
+ expect(out).to match("Do you want to add a code linter and formatter to your gem?")
+ end
+
+ it "hints that the choice will only be applied to the current gem" do
+ expect(out).to match("Your choice will only be applied to this gem.")
+ end
+ end
+
+ context "gem.linter setting not set and --linter with no arguments", :readline do
+ before do
+ bundle_config_global "BUNDLE_GEM__LINTER" => nil
+ bundle "gem #{gem_name} --linter" do |input, _, _|
+ input.puts "rubocop"
+ end
+ end
+
+ it "asks to setup a linter" do
+ expect(out).to match("Do you want to add a code linter and formatter to your gem?")
+ end
+
+ it "hints that the choice will be applied to future bundle gem calls" do
+ hint = "Future `bundle gem` calls will use your choice. " \
+ "This setting can be changed anytime with `bundle config gem.linter`."
+ expect(out).to match(hint)
+ end
+ end
+
+ context "gem.linter setting set to a linter and --no-linter" do
+ before do
+ bundle_config "gem.linter rubocop"
+ bundle "gem #{gem_name} --no-linter"
+ end
+
+ it "does not generate any linter config" do
+ expect(bundled_app("#{gem_name}/.rubocop.yml")).to_not exist
+ expect(bundled_app("#{gem_name}/.standard.yml")).to_not exist
+ end
+
+ it "does not add any linter config files into ignore list" do
+ expect(ignore_paths).not_to include(".rubocop.yml")
+ expect(ignore_paths).not_to include(".standard.yml")
+ end
+ end
+
+ context "--edit option" do
+ it "opens the generated gemspec in the user's text editor" do
+ output = bundle "gem #{gem_name} --edit=echo"
+ gemspec_path = File.join(bundled_app, gem_name, "#{gem_name}.gemspec")
+ expect(output).to include("echo \"#{gemspec_path}\"")
+ end
+ end
+
+ shared_examples_for "paths that depend on gem name" do
+ it "generates entrypoint, version file and signatures file at the proper path, with the proper content" do
+ bundle "gem #{gem_name}"
+
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb")).to exist
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(%r{require_relative "#{require_relative_path}/version"})
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/class Error < StandardError; end$/)
+
+ expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb")).to exist
+ expect(bundled_app("#{gem_name}/sig/#{require_path}.rbs")).to exist
+ end
+
+ context "--exe parameter set" do
+ before do
+ bundle "gem #{gem_name} --exe"
+ end
+
+ it "builds an exe file that requires the proper entrypoint" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/)
+ end
+ end
+
+ context "--bin parameter set" do
+ before do
+ bundle "gem #{gem_name} --bin"
+ end
+
+ it "builds an exe file that requires the proper entrypoint" do
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}")).to exist
+ expect(bundled_app("#{gem_name}/exe/#{gem_name}").read).to match(/require "#{require_path}"/)
+ end
+ end
+
+ context "--test parameter set to rspec" do
+ before do
+ bundle "gem #{gem_name} --test=rspec"
+ end
+
+ it "builds a spec helper that requires the proper entrypoint, and a default test in the proper path which fails" do
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/spec_helper.rb").read).to include(%(require "#{require_path}"))
+ expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb")).to exist
+ expect(bundled_app("#{gem_name}/spec/#{require_path}_spec.rb").read).to include("expect(false).to eq(true)")
+ end
+ end
+
+ context "--test parameter set to minitest" do
+ before do
+ bundle "gem #{gem_name} --test=minitest"
+ end
+
+ it "builds a test helper that requires the proper entrypoint, and default test file in the proper path that defines the proper test class name, requires helper, and fails" do
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}"))
+
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}")).to exist
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(minitest_test_class_name)
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include(%(require "test_helper"))
+ expect(bundled_app("#{gem_name}/#{minitest_test_file_path}").read).to include("assert false")
+ end
+ end
+
+ context "--test parameter set to test-unit" do
+ before do
+ bundle "gem #{gem_name} --test=test-unit"
+ end
+
+ it "builds a test helper that requires the proper entrypoint, and default test file in the proper path which requires helper and fails" do
+ expect(bundled_app("#{gem_name}/test/test_helper.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/test_helper.rb").read).to include(%(require "#{require_path}"))
+ expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb")).to exist
+ expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include(%(require "test_helper"))
+ expect(bundled_app("#{gem_name}/test/#{require_path}_test.rb").read).to include("assert_equal(\"expected\", \"actual\")")
+ end
+ end
+ end
+
+ context "with mit option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.mit true"
+ end
+ it_behaves_like "--mit flag"
+ it_behaves_like "--no-mit flag"
+ end
+
+ context "with mit option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.mit false"
+ end
+ it_behaves_like "--mit flag"
+ it_behaves_like "--no-mit flag"
+ end
+
+ context "with coc option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.coc true"
+ end
+ it_behaves_like "--coc flag"
+ it_behaves_like "--no-coc flag"
+ end
+
+ context "with coc option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.coc false"
+ end
+ it_behaves_like "--coc flag"
+ it_behaves_like "--no-coc flag"
+ end
+
+ context "with rubocop option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.rubocop true"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with rubocop option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.rubocop false"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with linter option in bundle config settings set to rubocop" do
+ before do
+ bundle_config_global "gem.linter rubocop"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with linter option in bundle config settings set to standard" do
+ before do
+ bundle_config_global "gem.linter standard"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with linter option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.linter false"
+ end
+ it_behaves_like "--linter=rubocop flag"
+ it_behaves_like "--linter=standard flag"
+ it_behaves_like "--no-linter flag"
+ end
+
+ context "with changelog option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.changelog true"
+ end
+ it_behaves_like "--changelog flag"
+ it_behaves_like "--no-changelog flag"
+ end
+
+ context "with changelog option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.changelog false"
+ end
+ it_behaves_like "--changelog flag"
+ it_behaves_like "--no-changelog flag"
+ end
+
+ context "with bundle option in bundle config settings set to true" do
+ before do
+ bundle_config_global "gem.bundle true"
+ end
+ it_behaves_like "--bundle flag"
+ it_behaves_like "--no-bundle flag"
+
+ it "runs bundle install" do
+ bundle "gem #{gem_name}"
+ expect(out).to include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ context "with bundle option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.bundle false"
+ end
+ it_behaves_like "--bundle flag"
+ it_behaves_like "--no-bundle flag"
+
+ it "does not run bundle install" do
+ bundle "gem #{gem_name}"
+ expect(out).to_not include("Running bundle install in the new gem directory.")
+ end
+ end
+
+ context "without git config github.user set" do
+ before do
+ git("config --global --unset github.user")
+ end
+ context "with github-username option in bundle config settings set to some value" do
+ before do
+ bundle_config_global "gem.github_username different_username"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ it_behaves_like "github_username configuration"
+
+ context "with github-username option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.github_username false"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ context "when changelog is enabled" do
+ it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri to TODOs" do
+ bundle "gem #{gem_name} --changelog"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).
+ to eq("TODO: Put your gem's CHANGELOG.md URL here.")
+ expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.")
+ end
+ end
+
+ context "when changelog is not enabled" do
+ it "sets gemspec homepage, homepage_uri, source_code_uri to TODOs and changelog_uri to nil" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).to be_nil
+ expect(generated_gemspec.homepage).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("TODO: Put your gem's website or public repo URL here.")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("TODO: Put your gem's public repo URL here.")
+ end
+ end
+ end
+
+ context "with git config github.user set" do
+ context "with github-username option in bundle config settings set to some value" do
+ before do
+ bundle_config_global "gem.github_username different_username"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ it_behaves_like "github_username configuration"
+
+ context "with github-username option in bundle config settings set to false" do
+ before do
+ bundle_config_global "gem.github_username false"
+ end
+ it_behaves_like "--github-username option", "gh_user"
+ end
+
+ context "when changelog is enabled" do
+ it "sets gemspec changelog_uri, homepage, homepage_uri, source_code_uri based on git username" do
+ bundle "gem #{gem_name} --changelog"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).
+ to eq("https://github.com/bundleuser/#{gem_name}/blob/main/CHANGELOG.md")
+ expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ end
+ end
+
+ context "when changelog is not enabled" do
+ it "sets gemspec source_code_uri, homepage, homepage_uri but not changelog_uri" do
+ bundle "gem #{gem_name}"
+
+ expect(generated_gemspec.metadata["changelog_uri"]).to be_nil
+ expect(generated_gemspec.homepage).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["homepage_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ expect(generated_gemspec.metadata["source_code_uri"]).to eq("https://github.com/bundleuser/#{gem_name}")
+ end
+ end
+ end
+
+ context "standard gem naming" do
+ let(:require_path) { gem_name }
+
+ let(:require_relative_path) { gem_name }
+
+ let(:minitest_test_file_path) { "test/test_#{gem_name}.rb" }
+
+ let(:minitest_test_class_name) { "class TestMygem < Minitest::Test" }
+
+ include_examples "paths that depend on gem name"
+ end
+
+ context "gem naming with underscore" do
+ let(:gem_name) { "test_gem" }
+
+ let(:require_path) { "test_gem" }
+
+ let(:require_relative_path) { "test_gem" }
+
+ let(:minitest_test_file_path) { "test/test_test_gem.rb" }
+
+ let(:minitest_test_class_name) { "class TestTestGem < Minitest::Test" }
+
+ let(:flags) { nil }
+
+ it "does not nest constants" do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module TestGem/)
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module TestGem/)
+ end
+
+ include_examples "paths that depend on gem name"
+
+ context "--ext parameter set with C" do
+ let(:flags) { "--ext=c" }
+
+ before do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ it "builds ext skeleton" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist
+ end
+
+ it "generates native extension loading code" do
+ expect(bundled_app("#{gem_name}/lib/#{gem_name}.rb").read).to include(<<~RUBY)
+ require_relative "test_gem/version"
+ require "#{gem_name}/#{gem_name}"
+ RUBY
+ end
+
+ it "includes rake-compiler, but no Rust related changes" do
+ expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"')
+
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "rb_sys"')
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.required_rubygems_version = ">= ')
+ end
+
+ it "depends on compile task for build" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rake/extensiontask"
+
+ task build: :compile
+
+ GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec")
+
+ Rake::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext|
+ ext.lib_dir = "lib/#{gem_name}"
+ end
+
+ task default: %i[clobber compile]
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+ end
+
+ context "--ext parameter set with rust" do
+ let(:flags) { "--ext=rust" }
+
+ before do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ it "is not deprecated" do
+ expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated."
+ end
+
+ it "builds ext skeleton" do
+ expect(bundled_app("#{gem_name}/Cargo.toml")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/Cargo.toml")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/src/lib.rs")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/build.rs")).to exist
+ end
+
+ it "includes rake-compiler and rb_sys gems constraint" do
+ expect(bundled_app("#{gem_name}/Gemfile").read).to include('gem "rake-compiler"')
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "rb_sys"')
+ end
+
+ it "depends on compile task for build" do
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rb_sys/extensiontask"
+
+ task build: :compile
+
+ GEMSPEC = Gem::Specification.load("#{gem_name}.gemspec")
+
+ RbSys::ExtensionTask.new("#{gem_name}", GEMSPEC) do |ext|
+ ext.lib_dir = "lib/#{gem_name}"
+ end
+
+ task default: :compile
+ RAKEFILE
+
+ expect(bundled_app("#{gem_name}/Rakefile").read).to eq(rakefile)
+ end
+
+ it "configures the crate such that `cargo test` works", :ruby_repo, :mri_only do
+ env = setup_rust_env
+ gem_path = bundled_app(gem_name)
+ result = sys_exec("cargo test", env: env, dir: gem_path, timeout: 300)
+
+ expect(result).to include("1 passed")
+ end
+
+ def setup_rust_env
+ skip "rust toolchain of mingw is broken" if RUBY_PLATFORM.match?("mingw")
+
+ env = {
+ "CARGO_HOME" => ENV.fetch("CARGO_HOME", File.join(ENV["HOME"], ".cargo")),
+ "RUSTUP_HOME" => ENV.fetch("RUSTUP_HOME", File.join(ENV["HOME"], ".rustup")),
+ "RUSTUP_TOOLCHAIN" => ENV.fetch("RUSTUP_TOOLCHAIN", "stable"),
+ }
+
+ system(env, "cargo", "-V", out: IO::NULL, err: [:child, :out])
+ skip "cargo not present" unless $?.success?
+ # Hermetic Cargo setup
+ RbConfig::CONFIG.each {|k, v| env["RBCONFIG_#{k}"] = v }
+ env
+ end
+ end
+
+ context "--ext parameter set with go" do
+ let(:flags) { "--ext=go" }
+
+ before do
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ after do
+ sys_exec("go clean -modcache", raise_on_error: true) if installed_go?
+ end
+
+ it "is not deprecated" do
+ expect(err).not_to include "[DEPRECATED] Option `--ext` without explicit value is deprecated."
+ end
+
+ it "builds ext skeleton" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.h")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb")).to exist
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod")).to exist
+ end
+
+ it "includes extconf.rb in gem_name.gemspec" do
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include(%(spec.extensions = ["ext/#{gem_name}/extconf.rb"]))
+ end
+
+ it "includes go_gem in gem_name.gemspec" do
+ expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "go_gem", ">= 0.2"')
+ end
+
+ it "includes go_gem extension in extconf.rb" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(<<~RUBY)
+ require "mkmf"
+ require "go_gem/mkmf"
+ RUBY
+
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).to include(%(create_go_makefile("#{gem_name}/#{gem_name}")))
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/extconf.rb").read).not_to include("create_makefile")
+ end
+
+ it "includes go_gem extension in gem_name.c" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.c").read).to eq(<<~C)
+ #include "#{gem_name}.h"
+ #include "_cgo_export.h"
+ C
+ end
+
+ it "includes skeleton code in gem_name.go" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
+ /*
+ #include "#{gem_name}.h"
+
+ VALUE rb_#{gem_name}_sum(VALUE self, VALUE a, VALUE b);
+ */
+ import "C"
+ GO
+
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
+ //export rb_#{gem_name}_sum
+ func rb_#{gem_name}_sum(_ C.VALUE, a C.VALUE, b C.VALUE) C.VALUE {
+ GO
+
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/#{gem_name}.go").read).to include(<<~GO)
+ //export Init_#{gem_name}
+ func Init_#{gem_name}() {
+ GO
+ end
+
+ it "includes valid module name in go.mod" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/bundleuser/#{gem_name}")
+ end
+
+ it "includes go_gem extension in Rakefile" do
+ expect(bundled_app("#{gem_name}/Rakefile").read).to include(<<~RUBY)
+ require "go_gem/rake_task"
+
+ GoGem::RakeTask.new("#{gem_name}")
+ RUBY
+ end
+
+ context "with --no-ci" do
+ let(:flags) { "--ext=go --no-ci" }
+
+ it_behaves_like "CI config is absent"
+ end
+
+ context "--ci set to github" do
+ let(:flags) { "--ext=go --ci=github" }
+
+ it "generates .github/workflows/main.yml" do
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml")).to exist
+ expect(bundled_app("#{gem_name}/.github/workflows/main.yml").read).to include("go-version-file: ext/#{gem_name}/go.mod")
+ end
+ end
+
+ context "--ci set to circle" do
+ let(:flags) { "--ext=go --ci=circle" }
+
+ it "generates a .circleci/config.yml" do
+ expect(bundled_app("#{gem_name}/.circleci/config.yml")).to exist
+
+ expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML.strip)
+ environment:
+ GO_VERSION:
+ YAML
+
+ expect(bundled_app("#{gem_name}/.circleci/config.yml").read).to include(<<-YAML)
+ - run:
+ name: Install Go
+ command: |
+ wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
+ tar -C /usr/local -xzf /tmp/go.tar.gz
+ echo 'export PATH=/usr/local/go/bin:"$PATH"' >> "$BASH_ENV"
+ YAML
+ end
+ end
+
+ context "--ci set to gitlab" do
+ let(:flags) { "--ext=go --ci=gitlab" }
+
+ it "generates a .gitlab-ci.yml" do
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml")).to exist
+
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML)
+ - wget https://go.dev/dl/go$GO_VERSION.linux-amd64.tar.gz -O /tmp/go.tar.gz
+ - tar -C /usr/local -xzf /tmp/go.tar.gz
+ - export PATH=/usr/local/go/bin:$PATH
+ YAML
+
+ expect(bundled_app("#{gem_name}/.gitlab-ci.yml").read).to include(<<-YAML.strip)
+ variables:
+ GO_VERSION:
+ YAML
+ end
+ end
+
+ context "without github.user" do
+ before do
+ # FIXME: GitHub Actions Windows Runner hang up here for some reason...
+ skip "Workaround for hung up" if Gem.win_platform?
+
+ git("config --global --unset github.user")
+ bundle ["gem", gem_name, flags].compact.join(" ")
+ end
+
+ it "includes valid module name in go.mod" do
+ expect(bundled_app("#{gem_name}/ext/#{gem_name}/go.mod").read).to include("module github.com/username/#{gem_name}")
+ end
+ end
+ end
+ end
+
+ context "gem naming with dashed" do
+ let(:gem_name) { "test-gem" }
+
+ let(:require_path) { "test/gem" }
+
+ let(:require_relative_path) { "gem" }
+
+ let(:minitest_test_file_path) { "test/test/test_gem.rb" }
+
+ let(:minitest_test_class_name) { "class Test::TestGem < Minitest::Test" }
+
+ it "nests constants so they work" do
+ bundle "gem #{gem_name}"
+ expect(bundled_app("#{gem_name}/lib/#{require_path}/version.rb").read).to match(/module Test\n module Gem/)
+ expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to match(/module Test\n module Gem/)
+ end
+
+ include_examples "paths that depend on gem name"
+ end
+
+ describe "uncommon gem names" do
+ it "can deal with two dashes" do
+ bundle "gem a--a"
+
+ expect(bundled_app("a--a/a--a.gemspec")).to exist
+ end
+
+ it "fails gracefully with a ." do
+ bundle "gem foo.gemspec", raise_on_error: false
+ expect(err).to end_with("Invalid gem name foo.gemspec -- `Foo.gemspec` is an invalid constant name")
+ end
+
+ it "fails gracefully with a ^" do
+ bundle "gem ^", raise_on_error: false
+ expect(err).to end_with("Invalid gem name ^ -- `^` is an invalid constant name")
+ end
+
+ it "fails gracefully with a space" do
+ bundle "gem 'foo bar'", raise_on_error: false
+ expect(err).to end_with("Invalid gem name foo bar -- `Foo bar` is an invalid constant name")
+ end
+
+ it "fails gracefully when multiple names are passed" do
+ bundle "gem foo bar baz", raise_on_error: false
+ expect(err).to eq(<<-E.strip)
+ERROR: "bundle gem" was called with arguments ["foo", "bar", "baz"]
+Usage: "bundle gem NAME [OPTIONS]"
+ E
+ end
+ end
+
+ describe "#ensure_safe_gem_name" do
+ before do
+ bundle "gem #{subject}", raise_on_error: false
+ end
+
+ context "with an existing const name" do
+ subject { "gem" }
+ it { expect(err).to include("Invalid gem name #{subject}") }
+ end
+
+ context "with an existing hyphenated const name" do
+ subject { "gem-specification" }
+ it { expect(err).to include("Invalid gem name #{subject}") }
+ end
+
+ context "starting with a number" do
+ subject { "1gem" }
+ it { expect(err).to include("Invalid gem name #{subject}") }
+ end
+
+ context "including capital letter" do
+ subject { "CAPITAL" }
+ it "should warn but not error" do
+ expect(err).to include("Gem names with capital letters are not recommended")
+ expect(bundled_app("#{subject}/#{subject}.gemspec")).to exist
+ end
+ end
+
+ context "starting with an existing const name" do
+ subject { "gem-somenewconstantname" }
+ it { expect(err).not_to include("Invalid gem name #{subject}") }
+ end
+
+ context "ending with an existing const name" do
+ subject { "somenewconstantname-gem" }
+ it { expect(err).not_to include("Invalid gem name #{subject}") }
+ end
+ end
+
+ context "on first run", :readline do
+ it "asks about test framework" do
+ bundle_config_global "BUNDLE_GEM__TEST" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "rspec"
+ end
+
+ expect(bundled_app("foobar/spec/spec_helper.rb")).to exist
+ rakefile = <<~RAKEFILE
+ # frozen_string_literal: true
+
+ require "bundler/gem_tasks"
+ require "rspec/core/rake_task"
+
+ RSpec::Core::RakeTask.new(:spec)
+
+ task default: :spec
+ RAKEFILE
+
+ expect(bundled_app("foobar/Rakefile").read).to eq(rakefile)
+ expect(bundled_app("foobar/Gemfile").read).to include('gem "rspec"')
+ end
+
+ it "asks about CI service" do
+ bundle_config_global "BUNDLE_GEM__CI" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "github"
+ end
+
+ expect(bundled_app("foobar/.github/workflows/main.yml")).to exist
+ end
+
+ it "asks about MIT license just once" do
+ bundle_config_global "BUNDLE_GEM__MIT" => nil
+
+ bundle "config list"
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "yes"
+ end
+
+ expect(bundled_app("foobar/LICENSE.txt")).to exist
+ expect(out).to include("Using a MIT license means").once
+ end
+
+ it "asks about CoC just once" do
+ bundle_config_global "BUNDLE_GEM__COC" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "yes"
+ end
+
+ expect(bundled_app("foobar/CODE_OF_CONDUCT.md")).to exist
+ expect(out).to include("Codes of conduct can increase contributions to your project").once
+ end
+
+ it "asks about CHANGELOG just once" do
+ bundle_config_global "BUNDLE_GEM__CHANGELOG" => nil
+
+ bundle "gem foobar" do |input, _, _|
+ input.puts "yes"
+ end
+
+ expect(bundled_app("foobar/CHANGELOG.md")).to exist
+ expect(out).to include("A changelog is a file which contains").once
+ end
+ end
+
+ context "on conflicts with a previously created file" do
+ it "should fail gracefully" do
+ FileUtils.touch(bundled_app("conflict-foobar"))
+ bundle "gem conflict-foobar", raise_on_error: false
+ expect(err).to eq("Couldn't create a new gem named `conflict-foobar` because there's an existing file named `conflict-foobar`.")
+ expect(exitstatus).to eql(32)
+ end
+ end
+
+ context "on conflicts with a previously created directory" do
+ it "should succeed" do
+ FileUtils.mkdir_p(bundled_app("conflict-foobar/Gemfile"))
+ bundle "gem conflict-foobar"
+ expect(out).to include("file_clash conflict-foobar/Gemfile").
+ and include "Initializing git repo in #{bundled_app("conflict-foobar")}"
+ end
+ end
+end
diff --git a/spec/bundler/commands/open_spec.rb b/spec/bundler/commands/open_spec.rb
new file mode 100644
index 0000000000..664dc58919
--- /dev/null
+++ b/spec/bundler/commands/open_spec.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle open" do
+ context "when opening a regular gem" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ end
+
+ it "opens the gem with BUNDLER_EDITOR as highest priority" do
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "rails-2.3.2")}")
+ end
+
+ it "opens the gem with VISUAL as 2nd highest priority" do
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("visual #{default_bundle_path("gems", "rails-2.3.2")}")
+ end
+
+ it "opens the gem with EDITOR as 3rd highest priority" do
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "rails-2.3.2")}")
+ end
+
+ it "complains if no EDITOR is set" do
+ bundle "open rails", env: { "EDITOR" => "", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to eq("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR")
+ end
+
+ it "complains if gem not in bundle" do
+ bundle "open missing", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/could not find gem 'missing'/i)
+ end
+
+ it "does not blow up if the gem to open does not have a Gemfile" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "open foo", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("bundler", "gems", "foo-1.0-#{ref}")}")
+ end
+
+ it "suggests alternatives for similar-sounding gems" do
+ bundle "open Rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/did you mean 'rails'\?/i)
+ end
+
+ it "opens the gem with short words" do
+ bundle "open rec", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}")
+ end
+
+ it "opens subpath of the gem" do
+ bundle "open activerecord --path lib/activerecord", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/activerecord")
+ end
+
+ it "opens subpath file of the gem" do
+ bundle "open activerecord --path lib/version.rb", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/version.rb")
+ end
+
+ it "opens deep subpath of the gem" do
+ bundle "open activerecord --path lib/active_record", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/active_record")
+ end
+
+ it "requires value for --path arg" do
+ bundle "open activerecord --path", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to eq "Cannot specify `--path` option without a value"
+ end
+
+ it "suggests alternatives for similar-sounding gems when using subpath" do
+ bundle "open Rails --path README.md", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/did you mean 'rails'\?/i)
+ end
+
+ it "suggests alternatives for similar-sounding gems when using deep subpath" do
+ bundle "open Rails --path some/path/here", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(err).to match(/did you mean 'rails'\?/i)
+ end
+
+ it "opens subpath of the short worded gem" do
+ bundle "open rec --path CHANGELOG.md", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/CHANGELOG.md")
+ end
+
+ it "opens deep subpath of the short worded gem" do
+ bundle "open rec --path lib/activerecord", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("editor #{default_bundle_path("gems", "activerecord-2.3.2")}/lib/activerecord")
+ end
+
+ it "opens subpath of the selected matching gem", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active --path CHANGELOG.md", env: env do |input, _, _|
+ input.puts "2"
+ end
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2").join("CHANGELOG.md")}")
+ end
+
+ it "opens deep subpath of the selected matching gem", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active --path lib/activerecord/version.rb", env: env do |input, _, _|
+ input.puts "2"
+ end
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2").join("lib", "activerecord", "version.rb")}")
+ end
+
+ it "select the gem from many match gems", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active", env: env do |input, _, _|
+ input.puts "2"
+ end
+
+ expect(out).to include("bundler_editor #{default_bundle_path("gems", "activerecord-2.3.2")}")
+ end
+
+ it "allows selecting exit from many match gems", :readline do
+ env = { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ bundle "open active", env: env do |input, _, _|
+ input.puts "0"
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle "open rails", env: { "EDITOR" => "echo editor", "VISUAL" => "", "BUNDLER_EDITOR" => "" }
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ it "opens the editor with a clean env" do
+ bundle "open", env: { "EDITOR" => "sh -c 'env'", "VISUAL" => "", "BUNDLER_EDITOR" => "" }, raise_on_error: false
+ expect(out).not_to include("BUNDLE_GEMFILE=")
+ end
+ end
+
+ context "when opening a default gem" do
+ let(:default_gems) do
+ ruby(<<-RUBY).split("\n")
+ if Gem::Specification.is_a?(Enumerable)
+ puts Gem::Specification.select(&:default_gem?).map(&:name)
+ end
+ RUBY
+ end
+
+ before do
+ skip "No default gems available on this test run" if default_gems.empty?
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "throws proper error when trying to open default gem" do
+ bundle "open json", env: { "EDITOR" => "echo editor", "VISUAL" => "echo visual", "BUNDLER_EDITOR" => "echo bundler_editor" }
+ expect(out).to include("Unable to open json because it's a default gem, so the directory it would normally be installed to does not exist.")
+ end
+ end
+end
diff --git a/spec/bundler/commands/outdated_spec.rb b/spec/bundler/commands/outdated_spec.rb
new file mode 100644
index 0000000000..28ed51d61e
--- /dev/null
+++ b/spec/bundler/commands/outdated_spec.rb
@@ -0,0 +1,1369 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle outdated" do
+ describe "with no arguments" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "returns a sorted list of outdated gems" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.2"
+ update_git "foo", path: lib_path("foo")
+ update_git "zebra", path: lib_path("zebra")
+ end
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ weakling 0.0.3 0.2 ~> 0.0.1 default
+ zebra 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "excludes header row from the sorting" do
+ update_repo2 do
+ build_gem "AAA", %w[1.0.0 2.0.0]
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "AAA", "1.0.0"
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE
+ Gem Current Latest Requested Groups Release Date
+ AAA 1.0.0 2.0.0 = 1.0.0 default
+ TABLE
+
+ expect(out).to include(expected_output.strip)
+ end
+
+ it "returns non zero exit status if outdated gems present" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ bundle "outdated", raise_on_error: false
+
+ expect(exitstatus).to_not be_zero
+ end
+
+ it "returns success exit status if no outdated gems present" do
+ bundle "outdated"
+ end
+
+ it "adds gem group to dependency output when repo is updated" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "terranova", '8'
+
+ group :development, :test do
+ gem 'activesupport', '2.3.5'
+ end
+ G
+
+ update_repo2 { build_gem "activesupport", "3.0" }
+ update_repo2 { build_gem "terranova", "9" }
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ terranova 8 9 = 8 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --verbose option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "shows the location of the latest version's gemspec if installed" do
+ bundle_config "clean false"
+
+ update_repo2 { build_gem "activesupport", "3.0" }
+ update_repo2 { build_gem "terranova", "9" }
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "terranova", '9'
+ gem 'activesupport', '2.3.5'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "terranova", '8'
+ gem 'activesupport', '2.3.5'
+ G
+
+ bundle "outdated --verbose", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date Path
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ terranova 8 9 = 8 default #{default_bundle_path("specifications/terranova-9.gemspec")}
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --group option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "weakling", "~> 0.0.1"
+ gem "terranova", '8'
+ group :development, :test do
+ gem "duradura", '7.0'
+ gem 'activesupport', '2.3.5'
+ end
+ G
+ end
+
+ def test_group_option(group)
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "terranova", "9"
+ build_gem "duradura", "8.0"
+ end
+
+ bundle "outdated --group #{group}", raise_on_error: false
+ end
+
+ it "works when the bundle is up to date" do
+ bundle "outdated --group"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "works when only out of date gems are not in given group" do
+ update_repo2 do
+ build_gem "terranova", "9"
+ end
+ bundle "outdated --group development"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "returns a sorted list of outdated gems from one group => 'default'" do
+ test_group_option("default")
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ terranova 8 9 = 8 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "returns a sorted list of outdated gems from one group => 'development'" do
+ test_group_option("development")
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ duradura 7.0 8.0 = 7.0 development, test
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "returns a sorted list of outdated gems from one group => 'test'" do
+ test_group_option("test")
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ duradura 7.0 8.0 = 7.0 development, test
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --groups option and outdated transitive dependencies" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+
+ build_gem "bar", %w[2.0.0]
+
+ build_gem "bar_dependant", "7.0" do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "bar_dependant", '7.0'
+ G
+
+ update_repo2 do
+ build_gem "bar", %w[3.0.0]
+ end
+ end
+
+ it "returns a sorted list of outdated gems" do
+ bundle "outdated --groups", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ bar 2.0.0 3.0.0
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --groups option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "weakling", "~> 0.0.1"
+ gem "terranova", '8'
+ group :development, :test do
+ gem 'activesupport', '2.3.5'
+ gem "duradura", '7.0'
+ end
+ G
+ end
+
+ it "not outdated gems" do
+ bundle "outdated --groups"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "returns a sorted list of outdated gems by groups" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "terranova", "9"
+ build_gem "duradura", "8.0"
+ end
+
+ bundle "outdated --groups", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 development, test
+ duradura 7.0 8.0 = 7.0 development, test
+ terranova 8 9 = 8 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --local option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "weakling", "~> 0.0.1"
+ gem "terranova", '8'
+ group :development, :test do
+ gem 'activesupport', '2.3.5'
+ gem "duradura", '7.0'
+ end
+ G
+ end
+
+ it "uses local cache to return a list of outdated gems" do
+ update_repo2 do
+ build_gem "activesupport", "2.3.4"
+ end
+
+ bundle_config "clean false"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.4"
+ G
+
+ bundle "outdated --local", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.4 2.3.5 = 2.3.4 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "doesn't hit repo2" do
+ FileUtils.rm_r(gem_repo2)
+
+ bundle "outdated --local"
+ expect(out).not_to match(/Fetching (gem|version|dependency) metadata from/)
+ end
+ end
+
+ shared_examples_for "a minimal output is desired" do
+ context "and gems are outdated" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.2"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "outputs a sorted list of outdated gems with a more minimal format to stdout" do
+ minimal_output = "activesupport (newest 3.0, installed 2.3.5, requested = 2.3.5)\n" \
+ "weakling (newest 0.2, installed 0.0.3, requested ~> 0.0.1)"
+ subject
+ expect(out).to eq(minimal_output)
+ end
+
+ it "outputs progress to stderr" do
+ subject
+ expect(err).to include("Fetching gem metadata")
+ end
+ end
+
+ context "and no gems are outdated" do
+ before do
+ build_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "3.0"
+ G
+ end
+
+ it "does not output to stdout" do
+ subject
+ expect(out).to be_empty
+ end
+
+ it "outputs progress to stderr" do
+ subject
+ expect(err).to include("Fetching gem metadata")
+ end
+ end
+ end
+
+ describe "with --parseable option" do
+ subject { bundle "outdated --parseable", raise_on_error: false }
+
+ it_behaves_like "a minimal output is desired"
+ end
+
+ describe "with aliased --porcelain option" do
+ subject { bundle "outdated --porcelain", raise_on_error: false }
+
+ it_behaves_like "a minimal output is desired"
+ end
+
+ describe "with specified gems" do
+ it "returns list of outdated gems" do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ bundle "outdated foo", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "does not require gems to be installed" do
+ build_repo4 do
+ build_gem "zeitwerk", "1.0.0"
+ build_gem "zeitwerk", "2.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "zeitwerk"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ zeitwerk (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ zeitwerk
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "outdated zeitwerk", raise_on_error: false
+
+ expected_output = <<~TABLE.tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ zeitwerk 1.0.0 2.0.0 >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ expect(err).to be_empty
+ end
+ end
+
+ describe "pre-release gems" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ context "without the --pre option" do
+ it "ignores pre-release versions" do
+ update_repo2 do
+ build_gem "activesupport", "3.0.0.beta"
+ end
+
+ bundle "outdated"
+
+ expect(out).to end_with("Bundle up to date!")
+ end
+ end
+
+ context "with the --pre option" do
+ it "includes pre-release versions" do
+ update_repo2 do
+ build_gem "activesupport", "3.0.0.beta"
+ end
+
+ bundle "outdated --pre", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0.0.beta = 2.3.5 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ context "when current gem is a pre-release" do
+ it "includes the gem" do
+ update_repo2 do
+ build_gem "activesupport", "3.0.0.beta.1"
+ build_gem "activesupport", "3.0.0.beta.2"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "3.0.0.beta.1"
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 3.0.0.beta.1 3.0.0.beta.2 = 3.0.0.beta.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+ end
+
+ describe "with --filter-strict option" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "only reports gems that have a newer version that matches the specified dependency version requirements" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.0.5"
+ end
+
+ bundle :outdated, "filter-strict": true, raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.0.5 ~> 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "only reports gems that have a newer version that matches the specified dependency version requirements, using --strict alias" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ build_gem "weakling", "0.0.5"
+ end
+
+ bundle :outdated, strict: true, raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.0.5 ~> 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "doesn't crash when some deps unused on the current platform" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", platforms: [:ruby_22]
+ G
+
+ bundle :outdated, "filter-strict": true
+
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "only reports gem dependencies when they can actually be updated" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware", "1.0"
+ G
+
+ bundle :outdated, "filter-strict": true
+
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ describe "and filter options" do
+ it "only reports gems that match requirement and patch filter level" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "~> 2.3"
+ gem "weakling", ">= 0.0.1"
+ G
+
+ update_repo2 do
+ build_gem "activesupport", %w[2.4.0 3.0.0]
+ build_gem "weakling", "0.0.5"
+ end
+
+ bundle :outdated, :"filter-strict" => true, "filter-patch" => true, :raise_on_error => false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.0.5 >= 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "only reports gems that match requirement and minor filter level" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "~> 2.3"
+ gem "weakling", ">= 0.0.1"
+ G
+
+ update_repo2 do
+ build_gem "activesupport", %w[2.3.9]
+ build_gem "weakling", "0.1.5"
+ end
+
+ bundle :outdated, :"filter-strict" => true, "filter-minor" => true, :raise_on_error => false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 0.1.5 >= 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "only reports gems that match requirement and major filter level" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "~> 2.3"
+ gem "weakling", ">= 0.0.1"
+ G
+
+ update_repo2 do
+ build_gem "activesupport", %w[2.4.0 2.5.0]
+ build_gem "weakling", "1.1.5"
+ end
+
+ bundle :outdated, :"filter-strict" => true, "filter-major" => true, :raise_on_error => false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.0.3 1.1.5 >= 0.0.1 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+ end
+
+ describe "with invalid gem name" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+ end
+
+ it "returns could not find gem name" do
+ bundle "outdated invalid_gem_name", raise_on_error: false
+ expect(err).to include("Could not find gem 'invalid_gem_name'.")
+ end
+
+ it "returns non-zero exit code" do
+ bundle "outdated invalid_gem_name", raise_on_error: false
+ expect(exitstatus).to_not be_zero
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :outdated, raise_on_error: false
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ context "in deployment mode" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack"
+ gem "foo"
+ G
+ bundle :lock
+ bundle_config "deployment true"
+ end
+
+ it "outputs a helpful message about being in deployment mode" do
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "outdated", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("You are trying to check outdated gems in deployment mode.")
+ expect(err).to include("Run `bundle outdated` elsewhere.")
+ expect(err).to include("If this is a development machine, remove the ")
+ expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.")
+ end
+ end
+
+ context "after bundle config set --local deployment true" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack"
+ gem "foo"
+ G
+ bundle_config "deployment true"
+ end
+
+ it "outputs a helpful message about being in deployment mode" do
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "outdated", raise_on_error: false
+ expect(last_command).to be_failure
+ expect(err).to include("You are trying to check outdated gems in deployment mode.")
+ expect(err).to include("Run `bundle outdated` elsewhere.")
+ expect(err).to include("If this is a development machine, remove the ")
+ expect(err).to include("Gemfile freeze\nby running `bundle config unset deployment`.")
+ end
+ end
+
+ context "update available for a gem on a different platform" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "laduradura", '= 5.15.2'
+ G
+ end
+
+ it "reports that no updates are available" do
+ bundle "outdated"
+ expect(out).to end_with("Bundle up to date!")
+ end
+ end
+
+ context "update available for a gem on the same platform while multiple platforms used for gem" do
+ before do
+ build_repo2
+ end
+
+ it "reports that updates are available if the Ruby platform is used" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby]
+ G
+
+ bundle "outdated"
+ expect(out).to end_with("Bundle up to date!")
+ end
+
+ it "reports that updates are available if the JRuby platform is used", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "laduradura", '= 5.15.2', :platforms => [:ruby, :jruby]
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ laduradura 5.15.2 5.15.3 = 5.15.2 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ shared_examples_for "version update is detected" do
+ it "reports that a gem has a newer version" do
+ subject
+
+ outdated_gems = out.split("\n").drop_while {|l| !l.start_with?("Gem") }[1..-1]
+
+ expect(outdated_gems.size).to be > 0
+ end
+ end
+
+ shared_examples_for "major version updates are detected" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "3.3.5"
+ build_gem "weakling", "0.8.0"
+ end
+ end
+
+ it_behaves_like "version update is detected"
+ end
+
+ context "when on a new machine" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ pristine_system_gems
+
+ update_git "foo", path: lib_path("foo")
+ update_repo2 do
+ build_gem "activesupport", "3.3.5"
+ build_gem "weakling", "0.8.0"
+ end
+ end
+
+ subject { bundle "outdated", raise_on_error: false }
+ it_behaves_like "version update is detected"
+ end
+
+ shared_examples_for "minor version updates are detected" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.7.5"
+ build_gem "weakling", "2.0.1"
+ end
+ end
+
+ it_behaves_like "version update is detected"
+ end
+
+ shared_examples_for "patch version updates are detected" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.3.7"
+ build_gem "weakling", "0.3.1"
+ end
+ end
+
+ it_behaves_like "version update is detected"
+ end
+
+ shared_examples_for "no version updates are detected" do
+ it "does not detect any version updates" do
+ subject
+ expect(out).to end_with("updates to display.")
+ end
+ end
+
+ shared_examples_for "major version is ignored" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "3.3.5"
+ build_gem "weakling", "1.0.1"
+ end
+ end
+
+ it_behaves_like "no version updates are detected"
+ end
+
+ shared_examples_for "minor version is ignored" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.4.5"
+ build_gem "weakling", "0.3.1"
+ end
+ end
+
+ it_behaves_like "no version updates are detected"
+ end
+
+ shared_examples_for "patch version is ignored" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ build_git "zebra", path: lib_path("zebra")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "zebra", :git => "#{lib_path("zebra")}"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "activesupport", "2.3.5"
+ gem "weakling", "~> 0.0.1"
+ gem "duradura", '7.0'
+ gem "terranova", '8'
+ G
+
+ update_repo2 do
+ build_gem "activesupport", "2.3.6"
+ build_gem "weakling", "0.0.4"
+ end
+ end
+
+ it_behaves_like "no version updates are detected"
+ end
+
+ describe "with --filter-major option" do
+ subject { bundle "outdated --filter-major", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "minor version is ignored"
+ it_behaves_like "patch version is ignored"
+ end
+
+ describe "with --filter-minor option" do
+ subject { bundle "outdated --filter-minor", raise_on_error: false }
+
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "major version is ignored"
+ it_behaves_like "patch version is ignored"
+ end
+
+ describe "with --filter-patch option" do
+ subject { bundle "outdated --filter-patch", raise_on_error: false }
+
+ it_behaves_like "patch version updates are detected"
+ it_behaves_like "major version is ignored"
+ it_behaves_like "minor version is ignored"
+ end
+
+ describe "with --filter-minor --filter-patch options" do
+ subject { bundle "outdated --filter-minor --filter-patch", raise_on_error: false }
+
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "patch version updates are detected"
+ it_behaves_like "major version is ignored"
+ end
+
+ describe "with --filter-major --filter-minor options" do
+ subject { bundle "outdated --filter-major --filter-minor", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "patch version is ignored"
+ end
+
+ describe "with --filter-major --filter-patch options" do
+ subject { bundle "outdated --filter-major --filter-patch", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "patch version updates are detected"
+ it_behaves_like "minor version is ignored"
+ end
+
+ describe "with --filter-major --filter-minor --filter-patch options" do
+ subject { bundle "outdated --filter-major --filter-minor --filter-patch", raise_on_error: false }
+
+ it_behaves_like "major version updates are detected"
+ it_behaves_like "minor version updates are detected"
+ it_behaves_like "patch version updates are detected"
+ end
+
+ context "conservative updates" do
+ before do
+ build_repo4 do
+ build_gem "patch", %w[1.0.0 1.0.1]
+ build_gem "minor", %w[1.0.0 1.0.1 1.1.0]
+ build_gem "major", %w[1.0.0 1.0.1 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.0.0
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'patch', '1.0.0'
+ gem 'minor', '1.0.0'
+ gem 'major', '1.0.0'
+ G
+
+ # remove all version requirements
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'patch'
+ gem 'minor'
+ gem 'major'
+ G
+ end
+
+ it "shows nothing when patching and filtering to minor" do
+ bundle "outdated --patch --filter-minor"
+
+ expect(out).to end_with("No minor updates to display.")
+ end
+
+ it "shows all gems when patching and filtering to patch" do
+ bundle "outdated --patch --filter-patch", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ major 1.0.0 1.0.1 >= 0 default
+ minor 1.0.0 1.0.1 >= 0 default
+ patch 1.0.0 1.0.1 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "shows minor and major when updating to minor and filtering to patch and minor" do
+ bundle "outdated --minor --filter-minor", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ major 1.0.0 1.1.0 >= 0 default
+ minor 1.0.0 1.1.0 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "shows minor when updating to major and filtering to minor with parseable" do
+ bundle "outdated --major --filter-minor --parseable", raise_on_error: false
+
+ expect(out).not_to include("patch (newest")
+ expect(out).to include("minor (newest")
+ expect(out).not_to include("major (newest")
+ end
+ end
+
+ context "tricky conservative updates" do
+ before do
+ build_repo4 do
+ build_gem "foo", %w[1.4.3 1.4.4] do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ build_gem "foo", %w[1.4.5 1.5.0] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "foo", %w[1.5.1] do |s|
+ s.add_dependency "bar", "~> 3.0"
+ end
+ build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0]
+ build_gem "qux", %w[1.0.0 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.4.3
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.4.3'
+ gem 'bar', '2.0.3'
+ gem 'qux', '1.0.0'
+ G
+
+ # remove 1.4.3 requirement and bar altogether
+ # to setup update specs below
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ gem 'qux'
+ G
+ end
+
+ it "shows gems updating to patch and filtering to patch" do
+ bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG_RESOLVER" => "1" }
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ bar 2.0.3 2.0.5
+ foo 1.4.3 1.4.4 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+
+ it "shows gems updating to patch and filtering to patch, in debug mode" do
+ bundle "outdated --patch --filter-patch", raise_on_error: false, env: { "DEBUG" => "1" }
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date Path
+ bar 2.0.3 2.0.5
+ foo 1.4.3 1.4.4 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with --only-explicit" do
+ it "does not report outdated dependent gems" do
+ build_repo4 do
+ build_gem "weakling", %w[0.2 0.3] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "bar", %w[2.1 2.2]
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'weakling', '0.2'
+ gem 'bar', '2.1'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'weakling'
+ G
+
+ bundle "outdated --only-explicit", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ weakling 0.2 0.3 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ describe "with a multiplatform lockfile" do
+ before do
+ build_repo4 do
+ build_gem "nokogiri", "1.11.1"
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.platform = Bundler.local_platform
+ end
+
+ build_gem "nokogiri", "1.11.2"
+ build_gem "nokogiri", "1.11.2" do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.11.1)
+ nokogiri (1.11.1-#{Bundler.local_platform})
+
+ PLATFORMS
+ ruby
+ #{Bundler.local_platform}
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "nokogiri"
+ G
+ end
+
+ it "reports a single entry per gem" do
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ nokogiri 1.11.1 1.11.2 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+
+ context "when a gem is no longer a dependency after a full update" do
+ before do
+ build_repo4 do
+ build_gem "mini_portile2", "2.5.2" do |s|
+ s.add_dependency "net-ftp", "~> 0.1"
+ end
+
+ build_gem "mini_portile2", "2.5.3"
+
+ build_gem "net-ftp", "0.1.2"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "mini_portile2"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mini_portile2 (2.5.2)
+ net-ftp (~> 0.1)
+ net-ftp (0.1.2)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ mini_portile2
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "works" do
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.strip
+ Gem Current Latest Requested Groups Release Date
+ mini_portile2 2.5.2 2.5.3 >= 0 default
+ TABLE
+
+ expect(out).to end_with(expected_output)
+ end
+ end
+end
diff --git a/spec/bundler/commands/platform_spec.rb b/spec/bundler/commands/platform_spec.rb
new file mode 100644
index 0000000000..9d7354c54f
--- /dev/null
+++ b/spec/bundler/commands/platform_spec.rb
@@ -0,0 +1,1260 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle platform" do
+ context "without flags" do
+ it "returns all the output" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ #{ruby_version_correct}
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile specifies a Ruby version requirement:
+* ruby #{Gem.ruby_version}
+
+Your current platform satisfies the Ruby version requirement.
+G
+ end
+
+ it "returns all the output including the patchlevel" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ #{ruby_version_correct_patchlevel}
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile specifies a Ruby version requirement:
+* #{Bundler::RubyVersion.system.single_version_string}
+
+Your current platform satisfies the Ruby version requirement.
+G
+ end
+
+ it "doesn't print ruby version requirement if it isn't specified" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile does not specify a Ruby version requirement.
+G
+ end
+
+ it "doesn't match the ruby version requirement" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ #{ruby_version_incorrect}
+
+ gem "foo"
+ G
+
+ bundle "platform"
+ expect(out).to eq(<<-G.chomp)
+Your platform is: #{Gem::Platform.local}
+
+Your app has gems that work on these platforms:
+* #{local_platform}
+
+Your Gemfile specifies a Ruby version requirement:
+* ruby #{not_local_ruby_version}
+
+Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified #{not_local_ruby_version}
+G
+ end
+ end
+
+ context "--ruby" do
+ it "returns ruby version when explicit" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.9.3", :engine => 'ruby', :engine_version => '1.9.3'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.9.3")
+ end
+
+ it "defaults to MRI" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.9.3"
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.9.3")
+ end
+
+ it "handles jruby" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'jruby', :engine_version => '1.6.5'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.8.7 (jruby 1.6.5)")
+ end
+
+ it "handles rbx" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'rbx', :engine_version => '1.2.4'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 1.8.7 (rbx 1.2.4)")
+ end
+
+ it "handles truffleruby" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "2.5.1", :engine => 'truffleruby', :engine_version => '1.0.0-rc6'
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("ruby 2.5.1 (truffleruby 1.0.0-rc6)")
+ end
+
+ it "raises an error if engine is used but engine version is not" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'rbx'
+
+ gem "foo"
+ G
+
+ bundle "platform", raise_on_error: false
+
+ expect(exitstatus).not_to eq(0)
+ end
+
+ it "raises an error if engine_version is used but engine is not" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine_version => '1.2.4'
+
+ gem "foo"
+ G
+
+ bundle "platform", raise_on_error: false
+
+ expect(exitstatus).not_to eq(0)
+ end
+
+ it "raises an error if engine version doesn't match ruby version for MRI" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "1.8.7", :engine => 'ruby', :engine_version => '1.2.4'
+
+ gem "foo"
+ G
+
+ bundle "platform", raise_on_error: false
+
+ expect(exitstatus).not_to eq(0)
+ end
+
+ it "should print if no ruby version is specified" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "foo"
+ G
+
+ bundle "platform --ruby"
+
+ expect(out).to eq("No ruby version specified")
+ end
+
+ it "handles when there is a locked requirement" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby "< 1.8.7"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ RUBY VERSION
+ ruby 1.0.0p127
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "platform --ruby"
+ expect(out).to eq("ruby 1.0.0")
+ end
+
+ it "handles when there is a lockfile with no requirement" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "platform --ruby"
+ expect(out).to eq("No ruby version specified")
+ end
+
+ it "handles when there is a requirement in the gemfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= 1.8.7"
+ G
+
+ bundle "platform --ruby"
+ expect(out).to eq("ruby 1.8.7")
+ end
+
+ it "handles when there are multiple requirements in the gemfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= 1.8.7", "< 2.0.0"
+ G
+
+ bundle "platform --ruby"
+ expect(out).to eq("ruby 1.8.7")
+ end
+ end
+
+ let(:ruby_version_correct) { "ruby \"#{Gem.ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{local_engine_version}\"" }
+ let(:ruby_version_correct_engineless) { "ruby \"#{Gem.ruby_version}\"" }
+ let(:ruby_version_correct_patchlevel) { "#{ruby_version_correct}, :patchlevel => '#{RUBY_PATCHLEVEL}'" }
+ let(:ruby_version_incorrect) { "ruby \"#{not_local_ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_ruby_version}\"" }
+ let(:engine_incorrect) { "ruby \"#{Gem.ruby_version}\", :engine => \"#{not_local_tag}\", :engine_version => \"#{Gem.ruby_version}\"" }
+ let(:engine_version_incorrect) { "ruby \"#{Gem.ruby_version}\", :engine => \"#{local_ruby_engine}\", :engine_version => \"#{not_local_engine_version}\"" }
+ let(:patchlevel_incorrect) { "#{ruby_version_correct}, :patchlevel => '#{not_local_patchlevel}'" }
+ let(:patchlevel_fixnum) { "#{ruby_version_correct}, :patchlevel => #{RUBY_PATCHLEVEL}1" }
+
+ def should_be_ruby_version_incorrect
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("Your Ruby version is #{Gem.ruby_version}, but your Gemfile specified #{not_local_ruby_version}")
+ end
+
+ def should_be_engine_incorrect
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("Your Ruby engine is #{local_ruby_engine}, but your Gemfile specified #{not_local_tag}")
+ end
+
+ def should_be_engine_version_incorrect
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("Your #{local_ruby_engine} version is #{local_engine_version}, but your Gemfile specified #{local_ruby_engine} #{not_local_engine_version}")
+ end
+
+ def should_ignore_patchlevel
+ expect(exitstatus).to eq(0)
+ expect(err).to eq("")
+ end
+
+ def should_be_patchlevel_fixnum
+ expect(exitstatus).to eq(18)
+ expect(err).to be_include("The Ruby patchlevel in your Gemfile must be a string")
+ end
+
+ context "bundle install" do
+ it "installs fine when the ruby version matches" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct}
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "installs fine with any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "installs fine when the patchlevel matches" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct_patchlevel}
+ G
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "doesn't install when the ruby version doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_incorrect}
+ G
+
+ expect(bundled_app_lock).not_to exist
+ should_be_ruby_version_incorrect
+ end
+
+ it "doesn't install when engine doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_incorrect}
+ G
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_incorrect
+ end
+
+ it "doesn't install when engine version doesn't match", :jruby_only do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_version_incorrect}
+ G
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_version_incorrect
+ end
+
+ it "does install even when patchlevel doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ expect(bundled_app_lock).to exist
+ should_ignore_patchlevel
+ end
+ end
+
+ context "bundle check" do
+ it "checks fine when the ruby version matches" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct}
+ G
+
+ bundle :check
+ expect(out).to match(/\AResolving dependencies\.\.\.\.*\nThe Gemfile's dependencies are satisfied\z/)
+ end
+
+ it "checks fine with any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle :check
+ expect(out).to match(/\AResolving dependencies\.\.\.\.*\nThe Gemfile's dependencies are satisfied\z/)
+ end
+
+ it "fails when ruby version doesn't match" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle :check, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when engine doesn't match" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_incorrect}
+ G
+
+ bundle :check, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when engine version doesn't match", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle :check, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "checks fine even when patchlevel doesn't match" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle :check
+ should_ignore_patchlevel
+ end
+ end
+
+ context "bundle update" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ G
+ end
+
+ it "updates successfully when the ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{ruby_version_correct}
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "updates fine with any engine", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{ruby_version_correct_engineless}
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "fails when ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{ruby_version_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when ruby engine doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{engine_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when ruby engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+
+ #{engine_version_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "updates fine even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+
+ #{patchlevel_incorrect}
+ G
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle :update, all: true
+ should_ignore_patchlevel
+ expect(the_bundle).to include_gems "activesupport 3.0"
+ end
+ end
+
+ context "bundle info" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ end
+
+ it "prints path if ruby version is correct" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{ruby_version_correct}
+ G
+
+ bundle "info rails --path"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints path if ruby version is correct for any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle "info rails --path"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "fails if ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle "show rails", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails if engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{engine_incorrect}
+ G
+
+ bundle "show rails", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails if engine version doesn't match", jruby_only: true do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle "show rails", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "prints path even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle "show rails"
+ should_ignore_patchlevel
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+ end
+
+ context "bundle cache" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches for any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "fails if the ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails if the engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails if the engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "copies the .gem file to vendor/cache even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle :cache
+ should_ignore_patchlevel
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "bundle pack" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "copies the .gem file to vendor/cache when ruby version matches any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+
+ it "fails if the ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails if the engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails if the engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+
+ #{engine_version_incorrect}
+ G
+
+ bundle :cache, raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "copies the .gem file to vendor/cache even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle :cache
+ should_ignore_patchlevel
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ end
+ end
+
+ context "bundle exec" do
+ before do
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ system_gems "myrack-1.0.0", "myrack-0.9.1", path: default_bundle_path
+ end
+
+ it "activates the correct gem when ruby version matches" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{ruby_version_correct}
+ G
+
+ bundle "exec myrackup"
+ expect(out).to include("0.9.1")
+ end
+
+ it "activates the correct gem when ruby version matches any engine", :jruby_only do
+ system_gems "myrack-1.0.0", "myrack-0.9.1", path: default_bundle_path
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle "exec myrackup"
+ expect(out).to include("0.9.1")
+ end
+
+ it "fails when the ruby version doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle "exec myrackup", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when the engine doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+
+ #{engine_incorrect}
+ G
+
+ bundle "exec myrackup", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when the engine version doesn't match", :jruby_only do
+ gemfile <<-G
+ gem "myrack", "0.9.1"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle "exec myrackup", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "activates the correct gem even when patchlevel doesn't match" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle "exec myrackup"
+ should_ignore_patchlevel
+ expect(out).to include("1.0.0")
+ end
+ end
+
+ context "bundle console" do
+ before do
+ build_repo2 do
+ build_dummy_irb
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "irb"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ gem "myrack_middleware", :group => :development
+ G
+ end
+
+ it "starts IRB with the default group loaded when ruby version matches", :readline do
+ gemfile gemfile + "\n\n#{ruby_version_correct}\n"
+
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "starts IRB with the default group loaded when ruby version matches", :readline, :jruby_only do
+ gemfile gemfile + "\n\n#{ruby_version_correct_engineless}\n"
+
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ expect(out).to include("0.9.1")
+ end
+
+ it "fails when ruby version doesn't match" do
+ gemfile gemfile + "\n\n#{ruby_version_incorrect}\n"
+
+ bundle "console", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when engine doesn't match" do
+ gemfile gemfile + "\n\n#{engine_incorrect}\n"
+
+ bundle "console", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when engine version doesn't match", :jruby_only do
+ gemfile gemfile + "\n\n#{engine_version_incorrect}\n"
+
+ bundle "console", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "starts IRB with the default group loaded even when patchlevel doesn't match", :readline do
+ gemfile gemfile + "\n\n#{patchlevel_incorrect}\n"
+
+ bundle "console" do |input, _, _|
+ input.puts("puts MYRACK")
+ input.puts("exit")
+ end
+ should_ignore_patchlevel
+ expect(out).to include("0.9.1")
+ end
+ end
+
+ context "Bundler.setup" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack", :group => :test
+ G
+
+ ENV["BUNDLER_FORCE_TTY"] = "true"
+ end
+
+ it "makes a Gemfile.lock if setup succeeds" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{ruby_version_correct}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ run "1"
+ expect(bundled_app_lock).to exist
+ end
+
+ it "makes a Gemfile.lock if setup succeeds for any engine", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ run "1"
+ expect(bundled_app_lock).to exist
+ end
+
+ it "fails when ruby version doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{ruby_version_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false
+
+ expect(bundled_app_lock).not_to exist
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when engine doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{engine_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_incorrect
+ end
+
+ it "fails when engine version doesn't match", :jruby_only do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{engine_version_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }, raise_on_error: false
+
+ expect(bundled_app_lock).not_to exist
+ should_be_engine_version_incorrect
+ end
+
+ it "makes a Gemfile.lock even when patchlevel doesn't match" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack"
+
+ #{patchlevel_incorrect}
+ G
+
+ FileUtils.rm(bundled_app_lock)
+
+ ruby "require 'bundler/setup'", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+
+ should_ignore_patchlevel
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ context "bundle outdated" do
+ before do
+ build_repo2 do
+ build_git "foo", path: lib_path("foo")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+ end
+
+ it "returns list of outdated gems when the ruby version matches" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{ruby_version_correct}
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "returns list of outdated gems when the ruby version matches for any engine", :jruby_only do
+ bundle :install
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{ruby_version_correct_engineless}
+ G
+
+ bundle "outdated", raise_on_error: false
+
+ expected_output = <<~TABLE.gsub("x", "\\\h").tr(".", "\.").strip
+ Gem Current Latest Requested Groups Release Date
+ activesupport 2.3.5 3.0 = 2.3.5 default
+ foo 1.0 xxxxxxx 1.0 xxxxxxx >= 0 default
+ TABLE
+
+ expect(out).to match(Regexp.new(expected_output))
+ end
+
+ it "fails when the ruby version doesn't match" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{ruby_version_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ should_be_ruby_version_incorrect
+ end
+
+ it "fails when the engine doesn't match" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{engine_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ should_be_engine_incorrect
+ end
+
+ it "fails when the engine version doesn't match", :jruby_only do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{engine_version_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ should_be_engine_version_incorrect
+ end
+
+ it "reports outdated gems even when patchlevel doesn't match" do
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ update_git "foo", path: lib_path("foo")
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", "2.3.5"
+ gem "foo", :git => "#{lib_path("foo")}"
+
+ #{patchlevel_incorrect}
+ G
+
+ bundle "outdated", raise_on_error: false
+ expect(err).not_to include("patchlevel")
+ expect(out).to include("activesupport")
+ expect(out).to include("foo")
+ end
+ end
+end
diff --git a/spec/bundler/commands/post_bundle_message_spec.rb b/spec/bundler/commands/post_bundle_message_spec.rb
new file mode 100644
index 0000000000..088fc29fe1
--- /dev/null
+++ b/spec/bundler/commands/post_bundle_message_spec.rb
@@ -0,0 +1,183 @@
+# frozen_string_literal: true
+
+RSpec.describe "post bundle message" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "activesupport", "2.3.5", :group => [:emo, :test]
+ group :test do
+ gem "rspec"
+ end
+ gem "myrack-obama", :group => :obama
+ G
+ end
+
+ let(:bundle_path) { "./.bundle" }
+ let(:bundle_show_system_message) { "Use `bundle info [gemname]` to see where a bundled gem is installed." }
+ let(:bundle_show_path_message) { "Bundled gems are installed into `#{bundle_path}`" }
+ let(:bundle_complete_message) { "Bundle complete!" }
+ let(:bundle_updated_message) { "Bundle updated!" }
+ let(:installed_gems_stats) { "4 Gemfile dependencies, 4 gems now installed." }
+
+ describe "when installing to system gems" do
+ before do
+ bundle_config "path.system true"
+ end
+
+ it "shows proper messages according to the configured groups" do
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).not_to include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include(installed_gems_stats)
+
+ bundle_config "without emo"
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to include("Gems in the group 'emo' were not installed")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include(installed_gems_stats)
+
+ bundle_config "without emo test"
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to include("Gems in the groups 'emo' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include("4 Gemfile dependencies, 2 gems now installed.")
+
+ bundle_config "without emo obama test"
+ bundle :install
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include("4 Gemfile dependencies, 1 gem now installed.")
+ end
+
+ describe "for second bundle install run" do
+ it "without any options" do
+ 2.times { bundle :install }
+ expect(out).to include(bundle_show_system_message)
+ expect(out).to_not include("Gems in the groups")
+ expect(out).to include(bundle_complete_message)
+ expect(out).to include(installed_gems_stats)
+ end
+ end
+ end
+
+ describe "with `path` configured" do
+ let(:bundle_path) { "./vendor" }
+
+ it "shows proper messages according to the configured groups" do
+ bundle_config "path vendor"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to_not include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+
+ bundle_config "path vendor"
+ bundle_config "without emo"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to include("Gems in the group 'emo' were not installed")
+ expect(out).to include(bundle_complete_message)
+
+ bundle_config "path vendor"
+ bundle_config "without emo test"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to include("Gems in the groups 'emo' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+
+ bundle_config "path vendor"
+ bundle_config "without emo obama test"
+ bundle :install
+ expect(out).to include(bundle_show_path_message)
+ expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not installed")
+ expect(out).to include(bundle_complete_message)
+ end
+ end
+
+ describe "with an absolute `path` inside the cwd configured" do
+ let(:bundle_path) { bundled_app("cache") }
+
+ it "shows proper messages according to the configured groups" do
+ bundle_config "path #{bundle_path}"
+ bundle :install
+ expect(out).to include("Bundled gems are installed into `./cache`")
+ expect(out).to_not include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+ end
+ end
+
+ describe "with `path` configured to an absolute path outside the cwd" do
+ let(:bundle_path) { tmp("not_bundled_app") }
+
+ it "shows proper messages according to the configured groups" do
+ bundle_config "path #{bundle_path}"
+ bundle :install
+ expect(out).to include("Bundled gems are installed into `#{tmp("not_bundled_app")}`")
+ expect(out).to_not include("Gems in the group")
+ expect(out).to include(bundle_complete_message)
+ end
+ end
+
+ describe "with misspelled or non-existent gem name" do
+ before do
+ bundle_config "force_ruby_platform true"
+ end
+
+ it "should report a helpful error message" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "not-a-gem", :group => :development
+ G
+ expect(err).to include <<~EOS.strip
+ Could not find gem 'not-a-gem' in rubygems repository https://gem.repo1/ or installed locally.
+ EOS
+ end
+
+ it "should report a helpful error message with reference to cache if available" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle :cache
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "not-a-gem", :group => :development
+ G
+ expect(err).to include("Could not find gem 'not-a-gem' in").
+ and include("or in gems cached in vendor/cache.")
+ end
+ end
+
+ describe "for bundle update" do
+ it "shows proper messages according to the configured groups" do
+ bundle :update, all: true
+ expect(out).not_to include("Gems in the groups")
+ expect(out).to include(bundle_updated_message)
+
+ bundle_config "without emo"
+ bundle :install
+ bundle :update, all: true
+ expect(out).to include("Gems in the group 'emo' were not updated")
+ expect(out).to include(bundle_updated_message)
+
+ bundle_config "without emo test"
+ bundle :install
+ bundle :update, all: true
+ expect(out).to include("Gems in the groups 'emo' and 'test' were not updated")
+ expect(out).to include(bundle_updated_message)
+
+ bundle_config "without emo obama test"
+ bundle :install
+ bundle :update, all: true
+ expect(out).to include("Gems in the groups 'emo', 'obama' and 'test' were not updated")
+ expect(out).to include(bundle_updated_message)
+ end
+ end
+end
diff --git a/spec/bundler/commands/pristine_spec.rb b/spec/bundler/commands/pristine_spec.rb
new file mode 100644
index 0000000000..5f80b9e534
--- /dev/null
+++ b/spec/bundler/commands/pristine_spec.rb
@@ -0,0 +1,275 @@
+# frozen_string_literal: true
+
+require "bundler/vendored_fileutils"
+
+RSpec.describe "bundle pristine" do
+ before :each do
+ build_lib "baz", path: bundled_app do |s|
+ s.version = "1.0.0"
+ s.add_development_dependency "baz-dev", "=1.0.0"
+ end
+
+ build_repo2 do
+ build_gem "weakling"
+ build_gem "baz-dev", "1.0.0"
+ build_gem "very_simple_binary", &:add_c_extension
+ build_git "foo", path: lib_path("foo")
+ build_git "git_with_ext", path: lib_path("git_with_ext"), &:add_c_extension
+ build_lib "bar", path: lib_path("bar")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "weakling"
+ gem "very_simple_binary"
+ gem "foo", :git => "#{lib_path("foo")}", :branch => "main"
+ gem "git_with_ext", :git => "#{lib_path("git_with_ext")}"
+ gem "bar", :path => "#{lib_path("bar")}"
+
+ gemspec
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ context "when sourced from RubyGems" do
+ it "reverts using cached .gem file" do
+ spec = find_spec("weakling")
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+
+ bundle "pristine"
+ expect(changes_txt).to_not be_file
+ end
+
+ it "does not delete the bundler gem" do
+ bundle "install"
+ bundle "pristine"
+ bundle "-v"
+
+ expect(out).to end_with(Bundler::VERSION)
+ end
+ end
+
+ context "when sourced from git repo" do
+ it "reverts by resetting to current revision`" do
+ spec = find_spec("foo")
+ changed_file = Pathname.new(spec.full_gem_path).join("lib/foo.rb")
+ diff = "#Pristine spec changes"
+
+ File.open(changed_file, "a") {|f| f.puts diff }
+ expect(File.read(changed_file)).to include(diff)
+
+ bundle "pristine"
+ expect(File.read(changed_file)).to_not include(diff)
+ end
+
+ it "removes added files" do
+ spec = find_spec("foo")
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+
+ bundle "pristine"
+ expect(changes_txt).not_to be_file
+ end
+
+ it "displays warning and ignores changes when a local config exists" do
+ spec = find_spec("foo")
+ bundle_config "local.#{spec.name} #{lib_path(spec.name)}"
+
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+
+ bundle "pristine"
+ expect(changes_txt).to be_file
+ expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is locally overridden.")
+ end
+
+ it "doesn't run multiple git processes for the same repository" do
+ nested_gems = [
+ "actioncable",
+ "actionmailer",
+ "actionpack",
+ "actionview",
+ "activejob",
+ "activemodel",
+ "activerecord",
+ "activestorage",
+ "activesupport",
+ "railties",
+ ]
+
+ build_repo2 do
+ nested_gems.each do |gem|
+ build_lib gem, path: lib_path("rails/#{gem}")
+ end
+
+ build_git "rails", path: lib_path("rails") do |s|
+ nested_gems.each do |gem|
+ s.add_dependency gem
+ end
+ end
+ end
+
+ install_gemfile <<-G
+ source 'https://rubygems.org'
+
+ git "#{lib_path("rails")}" do
+ gem "rails"
+ gem "actioncable"
+ gem "actionmailer"
+ gem "actionpack"
+ gem "actionview"
+ gem "activejob"
+ gem "activemodel"
+ gem "activerecord"
+ gem "activestorage"
+ gem "activesupport"
+ gem "railties"
+ end
+ G
+
+ changed_files = []
+ diff = "#Pristine spec changes"
+
+ nested_gems.each do |gem|
+ spec = find_spec(gem)
+ changed_files << Pathname.new(spec.full_gem_path).join("lib/#{gem}.rb")
+ File.open(changed_files.last, "a") {|f| f.puts diff }
+ end
+
+ bundle "pristine"
+
+ changed_files.each do |changed_file|
+ expect(File.read(changed_file)).to_not include(diff)
+ end
+ end
+ end
+
+ context "when sourced from gemspec" do
+ it "displays warning and ignores changes when sourced from gemspec" do
+ spec = find_spec("baz")
+ changed_file = Pathname.new(spec.full_gem_path).join("lib/baz.rb")
+ diff = "#Pristine spec changes"
+
+ File.open(changed_file, "a") {|f| f.puts diff }
+ expect(File.read(changed_file)).to include(diff)
+
+ bundle "pristine"
+ expect(File.read(changed_file)).to include(diff)
+ expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.")
+ end
+
+ it "reinstall gemspec dependency" do
+ spec = find_spec("baz-dev")
+ changed_file = Pathname.new(spec.full_gem_path).join("lib/baz/dev.rb")
+ diff = "#Pristine spec changes"
+
+ File.open(changed_file, "a") {|f| f.puts "#Pristine spec changes" }
+ expect(File.read(changed_file)).to include(diff)
+
+ bundle "pristine"
+ expect(File.read(changed_file)).to_not include(diff)
+ end
+ end
+
+ context "when sourced from path" do
+ it "displays warning and ignores changes when sourced from local path" do
+ spec = find_spec("bar")
+ changes_txt = Pathname.new(spec.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(changes_txt)
+ expect(changes_txt).to be_file
+ bundle "pristine"
+ expect(err).to include("Cannot pristine #{spec.name} (#{spec.version}#{spec.git_version}). Gem is sourced from local path.")
+ expect(changes_txt).to be_file
+ end
+ end
+
+ context "when passing a list of gems to pristine" do
+ it "resets them" do
+ foo = find_spec("foo")
+ foo_changes_txt = Pathname.new(foo.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(foo_changes_txt)
+ expect(foo_changes_txt).to be_file
+
+ bar = find_spec("bar")
+ bar_changes_txt = Pathname.new(bar.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(bar_changes_txt)
+ expect(bar_changes_txt).to be_file
+
+ weakling = find_spec("weakling")
+ weakling_changes_txt = Pathname.new(weakling.full_gem_path).join("lib/changes.txt")
+ FileUtils.touch(weakling_changes_txt)
+ expect(weakling_changes_txt).to be_file
+
+ bundle "pristine foo bar weakling"
+
+ expect(err).to include("Cannot pristine bar (1.0). Gem is sourced from local path.")
+ expect(out).to include("Installing weakling 1.0")
+
+ expect(weakling_changes_txt).not_to be_file
+ expect(foo_changes_txt).not_to be_file
+ expect(bar_changes_txt).to be_file
+ end
+
+ it "raises when one of them is not in the lockfile" do
+ bundle "pristine abcabcabc", raise_on_error: false
+ expect(err).to include("Could not find gem 'abcabcabc'.")
+ end
+ end
+
+ context "when a build config exists for one of the gems" do
+ let(:very_simple_binary) { find_spec("very_simple_binary") }
+ let(:c_ext_dir) { Pathname.new(very_simple_binary.full_gem_path).join("ext") }
+ let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" }
+ before { bundle_config "build.very_simple_binary -- #{build_opt}" }
+
+ # This just verifies that the generated Makefile from the c_ext gem makes
+ # use of the build_args from the bundle config
+ it "applies the config when installing the gem" do
+ bundle "pristine"
+
+ makefile_contents = File.read(c_ext_dir.join("Makefile").to_s)
+ expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/)
+ expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/)
+ end
+ end
+
+ context "when a build config exists for a git sourced gem" do
+ let(:git_with_ext) { find_spec("git_with_ext") }
+ let(:c_ext_dir) { Pathname.new(git_with_ext.full_gem_path).join("ext") }
+ let(:build_opt) { "--with-ext-lib=#{c_ext_dir}" }
+ before { bundle_config "build.git_with_ext -- #{build_opt}" }
+
+ # This just verifies that the generated Makefile from the c_ext gem makes
+ # use of the build_args from the bundle config
+ it "applies the config when installing the gem" do
+ bundle "pristine"
+
+ makefile_contents = File.read(c_ext_dir.join("Makefile").to_s)
+ expect(makefile_contents).to match(/libpath =.*#{Regexp.escape(c_ext_dir.to_s)}/)
+ expect(makefile_contents).to match(/LIBPATH =.*-L#{Regexp.escape(c_ext_dir.to_s)}/)
+ end
+ end
+
+ context "when BUNDLE_GEMFILE doesn't exist" do
+ before do
+ bundle "pristine", env: { "BUNDLE_GEMFILE" => "does/not/exist" }, raise_on_error: false
+ end
+
+ it "shows a meaningful error" do
+ expect(err).to eq("#{bundled_app("does/not/exist")} not found")
+ end
+ end
+
+ def find_spec(name)
+ without_env_side_effects do
+ Bundler.definition.specs[name].first
+ end
+ end
+end
diff --git a/spec/bundler/commands/remove_spec.rb b/spec/bundler/commands/remove_spec.rb
new file mode 100644
index 0000000000..8a2e6778ea
--- /dev/null
+++ b/spec/bundler/commands/remove_spec.rb
@@ -0,0 +1,736 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle remove" do
+ context "when no gems are specified" do
+ it "throws error" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "remove", raise_on_error: false
+
+ expect(err).to include("Please specify gems to remove.")
+ end
+ end
+
+ context "after 'bundle install' is run" do
+ describe "running 'bundle remove GEM_NAME'" do
+ it "removes it from the lockfile" do
+ myrack_dep = <<~L
+
+ DEPENDENCIES
+ myrack
+
+ L
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ bundle "install"
+
+ expect(lockfile).to include(myrack_dep)
+
+ bundle "remove myrack"
+
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ expect(lockfile).to_not include(myrack_dep)
+ end
+ end
+ end
+
+ describe "remove single gem from gemfile" do
+ context "when gem is present in gemfile" do
+ it "shows success for removed gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(the_bundle).to_not include_gems "myrack"
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+
+ context "when gem is specified in multiple lines" do
+ it "shows success for removed gem" do
+ build_git "myrack"
+
+ gemfile <<-G
+ source 'https://gem.repo1'
+
+ gem 'git'
+ gem 'myrack',
+ git: "#{lib_path("myrack-1.0")}",
+ branch: 'main'
+ gem 'nokogiri'
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source 'https://gem.repo1'
+
+ gem 'git'
+ gem 'nokogiri'
+ G
+ end
+ end
+ end
+
+ context "when gem is not present in gemfile" do
+ it "shows warning for gem that could not be removed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.")
+ end
+ end
+ end
+
+ describe "remove multiple gems from gemfile" do
+ context "when all gems are present in gemfile" do
+ it "shows success fir all removed gems" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ gem "rails"
+ G
+
+ bundle "remove myrack rails"
+
+ expect(out).to include("myrack was removed.")
+ expect(out).to include("rails was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when some gems are not present in the gemfile" do
+ it "shows warning for those not present and success for those that can be removed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rails"
+ gem "minitest"
+ gem "rspec"
+ G
+
+ bundle "remove rails myrack minitest", raise_on_error: false
+
+ expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ gem "rails"
+ gem "minitest"
+ gem "rspec"
+ G
+ end
+ end
+ end
+
+ context "with inline groups" do
+ it "removes the specified gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :group => [:dev]
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ describe "with group blocks" do
+ context "when single group block with gem to be removed is present" do
+ it "removes the group block" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "rspec"
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when gem to be removed is outside block" do
+ it "does not modify group" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ group :test do
+ gem "coffee-script-source"
+ end
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "coffee-script-source"
+ end
+ G
+ end
+ end
+
+ context "when an empty block is also present" do
+ it "removes all empty blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "rspec"
+ end
+
+ group :dev do
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when the gem belongs to multiple groups" do
+ it "removes the groups" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test, :serioustest do
+ gem "rspec"
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when the gem is present in multiple groups" do
+ it "removes all empty blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :one do
+ gem "rspec"
+ end
+
+ group :two do
+ gem "rspec"
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+ end
+
+ describe "nested group blocks" do
+ context "when all the groups will be empty after removal" do
+ it "removes the empty nested blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ group :serioustest do
+ gem "rspec"
+ end
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when outer group will not be empty after removal" do
+ it "removes only empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "myrack-test"
+
+ group :serioustest do
+ gem "rspec"
+ end
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ group :test do
+ gem "myrack-test"
+
+ end
+ G
+ end
+ end
+
+ context "when inner group will not be empty after removal" do
+ it "removes only empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ group :test do
+ group :serioustest do
+ gem "rspec"
+ gem "myrack-test"
+ end
+ end
+ G
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ group :test do
+ group :serioustest do
+ gem "myrack-test"
+ end
+ end
+ G
+ end
+ end
+ end
+
+ describe "arbitrary gemfile" do
+ context "when multiple gems are present in same line" do
+ it "shows warning for gems not removed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"; gem "rails"
+ G
+
+ bundle "remove rails", raise_on_error: false
+
+ expect(err).to include("Gems could not be removed. myrack (>= 0) would also have been removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ gem "myrack"; gem "rails"
+ G
+ end
+ end
+
+ context "when some gems could not be removed" do
+ it "shows warning for gems not removed and success for those removed" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem"myrack"
+ gem"rspec"
+ gem "rails"
+ gem "minitest"
+ G
+
+ bundle "remove rails myrack rspec minitest"
+
+ expect(out).to include("rails was removed.")
+ expect(out).to include("minitest was removed.")
+ expect(out).to include("myrack, rspec could not be removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ gem"myrack"
+ gem"rspec"
+ G
+ end
+ end
+ end
+
+ context "with sources" do
+ before do
+ build_repo3 do
+ build_gem "rspec"
+ end
+ end
+
+ it "removes gems and empty source blocks" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+
+ source "https://gem.repo3" do
+ gem "rspec"
+ end
+ G
+
+ bundle "install"
+
+ bundle "remove rspec"
+
+ expect(out).to include("rspec was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ end
+ end
+
+ describe "with eval_gemfile" do
+ context "when gems are present in both gemfiles" do
+ it "removes the gems" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ end
+ end
+
+ context "when gems are present in other gemfile" do
+ it "removes the gems" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+
+ bundle "remove myrack"
+
+ expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"")
+ expect(out).to include("myrack was removed.")
+ end
+ end
+
+ context "when gems to be removed are not specified in any of the gemfiles" do
+ it "throws error for the gems not present" do
+ # an empty gemfile
+ # indicating the gem is not present in the gemfile
+ create_file "Gemfile-other", <<-G
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(err).to include("`myrack` is not specified in #{bundled_app_gemfile} so it could not be removed.")
+ end
+ end
+
+ context "when the gem is present in parent file but not in gemfile specified by eval_gemfile" do
+ it "removes the gem" do
+ gemfile "Gemfile-other", <<-G
+ gem "rails"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "myrack"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(out).to include("myrack was removed.")
+ expect(err).to include("`myrack` is not specified in #{bundled_app("Gemfile-other")} so it could not be removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+ end
+ end
+
+ context "when gems cannot be removed from other gemfile" do
+ it "shows error" do
+ gemfile "Gemfile-other", <<-G
+ gem "rails"; gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "myrack"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(out).to include("myrack was removed.")
+ expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ G
+ end
+ end
+
+ context "when gems could not be removed from parent gemfile" do
+ it "shows error" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "rails"; gem "myrack"
+ G
+
+ bundle "remove myrack", raise_on_error: false
+
+ expect(err).to include("Gems could not be removed. rails (>= 0) would also have been removed.")
+ expect(bundled_app("Gemfile-other").read).to include("gem \"myrack\"")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem "rails"; gem "myrack"
+ G
+ end
+ end
+
+ context "when gem present in gemfiles but could not be removed from one from one of them" do
+ it "removes gem which can be removed and shows warning for file from which it cannot be removed" do
+ gemfile "Gemfile-other", <<-G
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ eval_gemfile "Gemfile-other"
+ gem"myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(bundled_app("Gemfile-other").read).to_not include("gem \"myrack\"")
+ end
+ end
+ end
+
+ context "with install_if" do
+ it "removes gems inside blocks and empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ install_if(lambda { false }) do
+ gem "myrack"
+ end
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "with env" do
+ it "removes gems inside blocks and empty blocks" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" do
+ gem "myrack"
+ end
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "with gemspec" do
+ it "should not remove the gem" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo.gemspec", "")
+ s.add_dependency "myrack"
+ end
+
+ install_gemfile(<<-G)
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ bundle "remove foo"
+
+ expect(out).to include("foo could not be removed.")
+ end
+ end
+
+ describe "with comments that mention gems" do
+ context "when comment is a separate line comment" do
+ it "does not remove the line comment" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ # gem "myrack" might be used in the future
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ # gem "myrack" might be used in the future
+ G
+ end
+ end
+
+ context "when gem specified for removal has an inline comment" do
+ it "removes the inline comment" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack" # this can be removed
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+
+ context "when gem specified for removal is mentioned in other gem's comment" do
+ it "does not remove other gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "puma" # implements interface provided by gem "myrack"
+
+ gem "myrack"
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to_not include("puma was removed.")
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ gem "puma" # implements interface provided by gem "myrack"
+ G
+ end
+ end
+
+ context "when gem specified for removal has a comment that mentions other gem" do
+ it "does not remove other gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "puma" # implements interface provided by gem "myrack"
+
+ gem "myrack"
+ G
+
+ bundle "remove puma"
+
+ expect(out).to include("puma was removed.")
+ expect(out).to_not include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ end
+ end
+ end
+
+ context "when gem definition has parentheses" do
+ it "removes the gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem("myrack")
+ gem("myrack", ">= 0")
+ gem("myrack", require: false)
+ G
+
+ bundle "remove myrack"
+
+ expect(out).to include("myrack was removed.")
+ expect(gemfile).to eq <<~G
+ source "https://gem.repo1"
+ G
+ end
+ end
+end
diff --git a/spec/bundler/commands/show_spec.rb b/spec/bundler/commands/show_spec.rb
new file mode 100644
index 0000000000..d0d55ffbb9
--- /dev/null
+++ b/spec/bundler/commands/show_spec.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle show" do
+ context "with a standard Gemfile" do
+ before :each do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails"
+ G
+ end
+
+ it "creates a Gemfile.lock if one did not exist" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "show"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "creates a Gemfile.lock when invoked with a gem name" do
+ FileUtils.rm(bundled_app_lock)
+
+ bundle "show rails"
+
+ expect(bundled_app_lock).to exist
+ end
+
+ it "prints path if gem exists in bundle" do
+ bundle "show rails"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints path if gem exists in bundle (with --paths option)" do
+ bundle "show rails --paths"
+ expect(out).to eq(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "warns if specification is installed, but path does not exist on disk" do
+ FileUtils.rm_r(default_bundle_path("gems", "rails-2.3.2"))
+
+ bundle "show rails"
+
+ expect(err).to match(/is missing/i)
+ expect(err).to match(default_bundle_path("gems", "rails-2.3.2").to_s)
+ end
+
+ it "prints the path to the running bundler" do
+ bundle "show bundler"
+ expect(out).to eq(root.to_s)
+ end
+
+ it "complains if gem not in bundle" do
+ bundle "show missing", raise_on_error: false
+ expect(err).to match(/could not find gem 'missing'/i)
+ end
+
+ it "prints path of all gems in bundle sorted by name" do
+ bundle "show --paths"
+
+ expect(out).to include(default_bundle_path("gems", "rake-#{rake_version}").to_s)
+ expect(out).to include(default_bundle_path("gems", "rails-2.3.2").to_s)
+
+ # Gem names are the last component of their path.
+ gem_list = out.split.map {|p| p.split("/").last }
+ expect(gem_list).to eq(gem_list.sort)
+ end
+
+ it "prints summary of gems" do
+ bundle "show --verbose"
+
+ expect(out).to include <<~MSG
+ * actionmailer (2.3.2)
+ \tSummary: This is just a fake gem for testing
+ \tHomepage: http://example.com
+ \tStatus: Up to date
+ MSG
+ end
+
+ it "includes bundler in the summary of gems" do
+ bundle "show --verbose"
+
+ expect(out).to include <<~MSG
+ * bundler (#{Bundler::VERSION})
+ \tSummary: The best way to manage your application's dependencies
+ \tHomepage: https://bundler.io
+ \tStatus: Up to date
+ MSG
+ end
+
+ it "includes up to date status in summary of gems" do
+ update_repo2 do
+ build_gem "rails", "3.0.0"
+ end
+
+ bundle "show --verbose"
+
+ expect(out).to include <<~MSG
+ * rails (2.3.2)
+ \tSummary: This is just a fake gem for testing
+ \tHomepage: http://example.com
+ \tStatus: Outdated - 2.3.2 < 3.0.0
+ MSG
+
+ # check lockfile is not accidentally updated
+ expect(lockfile).to include("actionmailer (2.3.2)")
+ end
+ end
+
+ context "with a git repo in the Gemfile" do
+ before :each do
+ @git = build_git "foo", "1.0"
+ end
+
+ it "prints out git info" do
+ install_gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle :show
+ expect(out).to include("foo (1.0 #{@git.ref_for("main", 6)}")
+ end
+
+ it "prints out branch names other than main" do
+ update_git "foo", branch: "omg" do |s|
+ s.write "lib/foo.rb", "FOO = '1.0.omg'"
+ end
+ @revision = revision_for(lib_path("foo-1.0"))[0...6]
+
+ install_gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.omg"
+
+ bundle :show
+ expect(out).to include("foo (1.0 #{@git.ref_for("omg", 6)}")
+ end
+
+ it "doesn't print the branch when tied to a ref" do
+ sha = revision_for(lib_path("foo-1.0"))
+ install_gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{sha}"
+ G
+
+ bundle :show
+ expect(out).to include("foo (1.0 #{sha[0..6]})")
+ end
+
+ it "handles when a version is a '-' prerelease" do
+ @git = build_git("foo", "1.0.0-beta.1", path: lib_path("foo"))
+ install_gemfile <<-G
+ gem "foo", "1.0.0-beta.1", :git => "#{lib_path("foo")}"
+ G
+ expect(the_bundle).to include_gems "foo 1.0.0.pre.beta.1"
+
+ bundle :show
+ expect(out).to include("foo (1.0.0.pre.beta.1")
+ end
+ end
+
+ context "in a fresh gem in a blank git repo" do
+ before :each do
+ build_git "foo", path: lib_path("foo")
+ File.open(lib_path("foo/Gemfile"), "w") {|f| f.puts "gemspec" }
+ sys_exec "rm -rf .git && git init", dir: lib_path("foo")
+ end
+
+ it "does not output git errors" do
+ bundle :show, dir: lib_path("foo")
+ expect(err_without_deprecations).to be_empty
+ end
+ end
+
+ it "performs an automatic bundle install" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ bundle_config "auto_install 1"
+ bundle :show
+ expect(out).to include("Installing foo 1.0")
+ end
+
+ context "with a valid regexp for gem name" do
+ it "presents alternatives", :readline do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ bundle "show rac"
+ expect(out).to match(/\A1 : myrack\n2 : myrack-obama\n0 : - exit -(\n>|\z)/)
+ end
+ end
+
+ context "with an invalid regexp for gem name" do
+ it "does not find the gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ invalid_regexp = "[]"
+
+ bundle "show #{invalid_regexp}", raise_on_error: false
+ expect(err).to include("Could not find gem '#{invalid_regexp}'.")
+ end
+ end
+end
+
+RSpec.describe "bundle show", bundler: "5" do
+ pending "shows a friendly error about the command removal"
+end
diff --git a/spec/bundler/commands/ssl_spec.rb b/spec/bundler/commands/ssl_spec.rb
new file mode 100644
index 0000000000..4220731b69
--- /dev/null
+++ b/spec/bundler/commands/ssl_spec.rb
@@ -0,0 +1,373 @@
+# frozen_string_literal: true
+
+require "bundler/cli"
+require "bundler/cli/doctor"
+require "bundler/cli/doctor/ssl"
+require_relative "../support/artifice/helpers/artifice"
+require "bundler/vendored_persistent.rb"
+
+RSpec.describe "bundle doctor ssl" do
+ before(:each) do
+ require_rack_test
+ require_relative "../support/artifice/helpers/endpoint"
+
+ @dummy_endpoint = Class.new(Endpoint) do
+ get "/" do
+ end
+ end
+
+ @previous_ui = Bundler.ui
+ Bundler.ui = Bundler::UI::Shell.new
+ Bundler.ui.level = "info"
+
+ @previous_client = Gem::Request::ConnectionPools.client
+ Artifice.activate_with(@dummy_endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+ end
+
+ after(:each) do
+ Bundler.ui = @previous_ui
+ Artifice.deactivate
+ Gem::Request::ConnectionPools.client = @previous_client
+ end
+
+ context "when a diagnostic fails" do
+ it "prints the diagnostic when openssl can't be loaded" do
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ allow(subject).to receive(:require).with("openssl").and_raise(LoadError)
+
+ expected_err = <<~MSG
+ Oh no! Your Ruby doesn't have OpenSSL, so it can't connect to rubygems.org.
+ You'll need to recompile or reinstall Ruby with OpenSSL support and try again.
+ MSG
+
+ expect { subject.run }.to output("").to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to certificate verification", :ruby_repo do
+ net_http = Class.new(Artifice::Net::HTTP) do
+ def connect
+ raise OpenSSL::SSL::SSLError, "certificate verify failed"
+ end
+ end
+
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = net_http
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (certificate verification)
+ RubyGems: failed (certificate verification)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to rubygems.org.
+
+ Below affect only Ruby net/http connections:
+ SSL_CERT_FILE: exists #{OpenSSL::X509::DEFAULT_CERT_FILE}
+ SSL_CERT_DIR: exists #{OpenSSL::X509::DEFAULT_CERT_DIR}
+
+ Your Ruby can't connect to rubygems.org because you are missing the certificate files OpenSSL needs to verify you are connecting to the genuine rubygems.org servers.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a too old tls version" do
+ subject = Bundler::CLI::Doctor::SSL.new({})
+
+ net_http = Class.new(Artifice::Net::HTTP) do
+ def connect
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to rubygems.org.
+
+ Your Ruby can't connect to rubygems.org because your version of OpenSSL is too old.
+ You'll need to upgrade your OpenSSL install and/or recompile Ruby to use a newer OpenSSL.
+
+ MSG
+
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to unsupported tls 1.3 version" do
+ net_http = Class.new(Artifice::Net::HTTP) do
+ def connect
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = net_http
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ Ruby net/http: failed
+
+ Unfortunately, this Ruby can't connect to rubygems.org.
+
+ Your Ruby can't connect to rubygems.org because TLS1_3 isn't supported yet.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3")
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a bundler and rubygems connection error" do
+ endpoint = Class.new(Endpoint) do
+ get "/" do
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+
+ Artifice.activate_with(endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Ruby net/http: success
+
+ For some reason, your Ruby installation can connect to rubygems.org, but neither RubyGems nor Bundler can.
+ The most likely fix is to manually upgrade RubyGems by following the instructions at http://ruby.to/ssl-check-failed.
+ After you've done that, run `gem install bundler` to upgrade Bundler, and then run this script again to make sure everything worked. â£
+
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a bundler connection error" do
+ endpoint = Class.new(Endpoint) do
+ get "/" do
+ if request.user_agent.include?("bundler")
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+ end
+
+ Artifice.activate_with(endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ RubyGems: success
+ Ruby net/http: success
+
+ Although your Ruby installation and RubyGems can both connect to rubygems.org, Bundler is having trouble.
+ The most likely way to fix this is to upgrade Bundler by running `gem install bundler`.
+ Run this script again after doing that to make sure everything is all set.
+ If you're still having trouble, check out the troubleshooting guide at http://ruby.to/ssl-check-failed.
+
+ MSG
+
+ expected_err = <<~MSG
+ Bundler: failed (SSL/TLS protocol version mismatch)
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+
+ it "fails due to a RubyGems connection error" do
+ endpoint = Class.new(Endpoint) do
+ get "/" do
+ if request.user_agent.include?("Ruby, RubyGems")
+ raise OpenSSL::SSL::SSLError, "read server hello A"
+ end
+ end
+ end
+
+ Artifice.activate_with(endpoint)
+ Gem::Request::ConnectionPools.client = Gem::Net::HTTP
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Bundler: success
+ Ruby net/http: success
+
+ It looks like Ruby and Bundler can connect to rubygems.org, but RubyGems itself cannot.
+ You can likely solve this by manually downloading and installing a RubyGems update.
+ Visit http://ruby.to/ssl-check-failed for instructions on how to manually upgrade RubyGems.
+
+ MSG
+
+ expected_err = <<~MSG
+ RubyGems: failed (SSL/TLS protocol version mismatch)
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ end
+ end
+
+ context "when no diagnostic fails" do
+ it "prints the SSL environment" do
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Bundler: success
+ RubyGems: success
+ Ruby net/http: success
+
+ Hooray! This Ruby can connect to rubygems.org.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr
+ end
+
+ it "uses the tls_version verify mode and host when given as option" do
+ net_http = Class.new(Artifice::Net::HTTP) do
+ class << self
+ attr_accessor :verify_mode, :min_version, :max_version
+ end
+
+ def connect
+ self.class.verify_mode = verify_mode
+ self.class.min_version = min_version
+ self.class.max_version = max_version
+
+ super
+ end
+ end
+
+ net_http.endpoint = @dummy_endpoint
+ Artifice.replace_net_http(net_http)
+ Gem::Request::ConnectionPools.client = net_http
+ Gem::RemoteFetcher.fetcher.close_all
+
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://example.org:
+ Bundler: success
+ RubyGems: success
+ Ruby net/http: success
+
+ Hooray! This Ruby can connect to example.org.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+
+ subject = Bundler::CLI::Doctor::SSL.new("tls-version": "1.3", "verify-mode": :none, host: "example.org")
+ expect { subject.run }.to output(expected_out).to_stdout.and output("").to_stderr
+ expect(net_http.verify_mode).to eq(0)
+ expect(net_http.min_version.to_s).to eq("TLS1_3")
+ expect(net_http.max_version.to_s).to eq("TLS1_3")
+ end
+
+ it "warns when TLS1.2 is not supported" do
+ expected_out = <<~MSG
+ Here's your OpenSSL environment:
+
+ OpenSSL: #{OpenSSL::VERSION}
+ Compiled with: #{OpenSSL::OPENSSL_VERSION}
+ Loaded with: #{OpenSSL::OPENSSL_LIBRARY_VERSION}
+
+ Trying connections to https://rubygems.org:
+ Bundler: success
+ RubyGems: success
+ Ruby net/http: success
+
+ Hooray! This Ruby can connect to rubygems.org.
+ You are all set to use Bundler and RubyGems.
+
+ MSG
+
+ expected_err = <<~MSG
+
+ WARNING: Although your Ruby can connect to rubygems.org today, your OpenSSL is very old!
+ WARNING: You will need to upgrade OpenSSL to use rubygems.org.
+
+ MSG
+
+ previous_version = OpenSSL::SSL::TLS1_2_VERSION
+ OpenSSL::SSL.send(:remove_const, :TLS1_2_VERSION)
+
+ subject = Bundler::CLI::Doctor::SSL.new({})
+ expect { subject.run }.to output(expected_out).to_stdout.and output(expected_err).to_stderr
+ ensure
+ OpenSSL::SSL.const_set(:TLS1_2_VERSION, previous_version)
+ end
+ end
+end
diff --git a/spec/bundler/commands/update_spec.rb b/spec/bundler/commands/update_spec.rb
new file mode 100644
index 0000000000..03a3786d80
--- /dev/null
+++ b/spec/bundler/commands/update_spec.rb
@@ -0,0 +1,2095 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ describe "with no arguments" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "updates the entire bundle" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update"
+ expect(out).to include("Bundle updated!")
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "doesn't delete the Gemfile.lock file if something goes wrong" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ exit!
+ G
+ bundle "update", raise_on_error: false
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ describe "with --verbose" do
+ before do
+ build_repo2
+
+ install_gemfile <<~G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ end
+
+ it "logs the reason for re-resolving" do
+ bundle "update --verbose"
+ expect(out).not_to include("Found changes from the lockfile")
+ expect(out).to include("Re-resolving dependencies because bundler is unlocking")
+ end
+ end
+
+ describe "with --all" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "updates the entire bundle" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(out).to include("Bundle updated!")
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 3.0"
+ end
+
+ it "doesn't delete the Gemfile.lock file if something goes wrong" do
+ install_gemfile "source 'https://gem.repo1'"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ exit!
+ G
+ bundle "update", all: true, raise_on_error: false
+ expect(bundled_app_lock).to exist
+ end
+ end
+
+ describe "with --gemfile" do
+ it "creates lockfiles based on the Gemfile name" do
+ gemfile bundled_app("OmgFile"), <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0"
+ G
+
+ bundle "update --gemfile OmgFile", all: true
+
+ expect(bundled_app("OmgFile.lock")).to exist
+ end
+ end
+
+ context "when update_requires_all_flag is set" do
+ before { bundle_config "update_requires_all_flag true" }
+
+ it "errors when passed nothing" do
+ install_gemfile "source 'https://gem.repo1'"
+ bundle :update, raise_on_error: false
+ expect(err).to eq("To update everything, pass the `--all` flag.")
+ end
+
+ it "errors when passed --all and another option" do
+ install_gemfile "source 'https://gem.repo1'"
+ bundle "update --all foo", raise_on_error: false
+ expect(err).to eq("Cannot specify --all along with specific options.")
+ end
+
+ it "updates everything when passed --all" do
+ install_gemfile "source 'https://gem.repo1'"
+ bundle "update --all"
+ expect(out).to include("Bundle updated!")
+ end
+ end
+
+ describe "--quiet argument" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "hides UI messages" do
+ bundle "update --quiet"
+ expect(out).not_to include("Bundle updated!")
+ end
+ end
+
+ describe "with a top level dependency" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "unlocks all child dependencies that are unrelated to other locked dependencies" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update myrack-obama"
+ expect(the_bundle).to include_gems "myrack 1.2", "myrack-obama 1.0", "activesupport 2.3.5"
+ end
+ end
+
+ describe "with an unknown dependency" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "should inform the user" do
+ bundle "update halting-problem-solver", raise_on_error: false
+ expect(err).to include "Could not find gem 'halting-problem-solver'"
+ end
+ it "should suggest alternatives" do
+ bundle "update platformspecific", raise_on_error: false
+ expect(err).to include "Did you mean 'platform_specific'?"
+ end
+ end
+
+ describe "with a child dependency" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "should update the child dependency" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ bundle "update myrack"
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+ end
+
+ describe "when a possible resolve requires an older version of a locked gem" do
+ it "does not go to an older version" do
+ build_repo4 do
+ build_gem "tilt", "2.0.8"
+ build_gem "slim", "3.0.9" do |s|
+ s.add_dependency "tilt", [">= 1.3.3", "< 2.1"]
+ end
+ build_gem "slim_lint", "0.16.1" do |s|
+ s.add_dependency "slim", [">= 3.0", "< 5.0"]
+ end
+ build_gem "slim-rails", "0.2.1" do |s|
+ s.add_dependency "slim", ">= 0.9.2"
+ end
+ build_gem "slim-rails", "3.1.3" do |s|
+ s.add_dependency "slim", "~> 3.0"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "slim-rails"
+ gem "slim_lint"
+ G
+
+ expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1")
+
+ build_repo4 do
+ build_gem "slim", "4.0.0" do |s|
+ s.add_dependency "tilt", [">= 2.0.6", "< 2.1"]
+ end
+ end
+
+ bundle "update", all: true
+
+ expect(the_bundle).to include_gems("slim 3.0.9", "slim-rails 3.1.3", "slim_lint 0.16.1")
+ end
+
+ it "does not go to an older version, even if the version upgrade that could cause another gem to downgrade is activated first" do
+ build_repo4 do
+ # countries is processed before country_select by the resolver due to having less spec groups (groups of versions with the same dependencies) (2 vs 3)
+
+ build_gem "countries", "2.1.4"
+ build_gem "countries", "3.1.0"
+
+ build_gem "countries", "4.0.0" do |s|
+ s.add_dependency "sixarm_ruby_unaccent", "~> 1.1"
+ end
+
+ build_gem "country_select", "1.2.0"
+
+ build_gem "country_select", "2.1.4" do |s|
+ s.add_dependency "countries", "~> 2.0"
+ end
+ build_gem "country_select", "3.1.1" do |s|
+ s.add_dependency "countries", "~> 2.0"
+ end
+
+ build_gem "country_select", "5.1.0" do |s|
+ s.add_dependency "countries", "~> 3.0"
+ end
+
+ build_gem "sixarm_ruby_unaccent", "1.1.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "country_select"
+ gem "countries"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "countries", "3.1.0")
+ c.checksum(gem_repo4, "country_select", "5.1.0")
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ countries (3.1.0)
+ country_select (5.1.0)
+ countries (~> 3.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ countries
+ country_select
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ previous_lockfile = lockfile
+
+ bundle "lock --update", env: { "DEBUG" => "1" }, verbose: true
+
+ expect(lockfile).to eq(previous_lockfile)
+ end
+
+ it "does not downgrade direct dependencies when run with --conservative" do
+ build_repo4 do
+ build_gem "oauth2", "2.0.6" do |s|
+ s.add_dependency "faraday", ">= 0.17.3", "< 3.0"
+ end
+
+ build_gem "oauth2", "1.4.10" do |s|
+ s.add_dependency "faraday", ">= 0.17.3", "< 3.0"
+ s.add_dependency "multi_json", "~> 1.3"
+ end
+
+ build_gem "faraday", "2.5.2"
+
+ build_gem "multi_json", "1.15.0"
+
+ build_gem "quickbooks-ruby", "1.0.19" do |s|
+ s.add_dependency "oauth2", "~> 1.4"
+ end
+
+ build_gem "quickbooks-ruby", "0.1.9" do |s|
+ s.add_dependency "oauth2"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "oauth2"
+ gem "quickbooks-ruby"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ faraday (2.5.2)
+ multi_json (1.15.0)
+ oauth2 (1.4.10)
+ faraday (>= 0.17.3, < 3.0)
+ multi_json (~> 1.3)
+ quickbooks-ruby (1.0.19)
+ oauth2 (~> 1.4)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ oauth2
+ quickbooks-ruby
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update --conservative --verbose"
+
+ expect(out).not_to include("Installing quickbooks-ruby 0.1.9")
+ expect(out).to include("Installing quickbooks-ruby 1.0.19").and include("Installing oauth2 1.4.10")
+ end
+
+ it "does not downgrade direct dependencies when using gemspec sources" do
+ create_file("rails.gemspec", <<-G)
+ Gem::Specification.new do |gem|
+ gem.name = "rails"
+ gem.version = "7.1.0.alpha"
+ gem.author = "DHH"
+ gem.summary = "Full-stack web application framework."
+ end
+ G
+
+ build_repo4 do
+ build_gem "rake", "12.3.3"
+ build_gem "rake", "13.0.6"
+
+ build_gem "sneakers", "2.11.0" do |s|
+ s.add_dependency "rake"
+ end
+
+ build_gem "sneakers", "2.12.0" do |s|
+ s.add_dependency "rake", "~> 12.3"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gemspec
+
+ gem "rake"
+ gem "sneakers"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: .
+ specs:
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rake (13.0.6)
+ sneakers (2.11.0)
+ rake
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rake
+ sneakers
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update --verbose"
+
+ expect(out).not_to include("Installing sneakers 2.12.0")
+ expect(out).not_to include("Installing rake 12.3.3")
+ expect(out).to include("Installing sneakers 2.11.0").and include("Installing rake 13.0.6")
+ end
+
+ it "downgrades indirect dependencies if required to fulfill an explicit upgrade request" do
+ build_repo4 do
+ build_gem "rbs", "3.6.1"
+ build_gem "rbs", "3.9.4"
+
+ build_gem "solargraph", "0.56.0" do |s|
+ s.add_dependency "rbs", "~> 3.3"
+ end
+
+ build_gem "solargraph", "0.56.2" do |s|
+ s.add_dependency "rbs", "~> 3.6.1"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem 'solargraph', '~> 0.56.0'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rbs (3.9.4)
+ solargraph (0.56.0)
+ rbs (~> 3.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ solargraph (~> 0.56.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update solargraph"
+
+ expect(lockfile).to include("solargraph (0.56.2)")
+ end
+
+ it "does not downgrade direct dependencies unnecessarily" do
+ build_repo4 do
+ build_gem "redis", "4.8.1"
+ build_gem "redis", "5.3.0"
+
+ build_gem "sidekiq", "6.5.5" do |s|
+ s.add_dependency "redis", ">= 4.5.0"
+ end
+
+ build_gem "sidekiq", "6.5.12" do |s|
+ s.add_dependency "redis", ">= 4.5.0", "< 5"
+ end
+
+ # one version of sidekiq above Gemfile's range is needed to make the
+ # resolver choose `redis` first and trying to upgrade it, reproducing
+ # the accidental sidekiq downgrade as a result
+ build_gem "sidekiq", "7.0.0 " do |s|
+ s.add_dependency "redis", ">= 4.2.0"
+ end
+
+ build_gem "sentry-sidekiq", "5.22.0" do |s|
+ s.add_dependency "sidekiq", ">= 3.0"
+ end
+
+ build_gem "sentry-sidekiq", "5.22.4" do |s|
+ s.add_dependency "sidekiq", ">= 3.0"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "redis"
+ gem "sidekiq", "~> 6.5"
+ gem "sentry-sidekiq"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ redis (4.8.1)
+ sentry-sidekiq (5.22.0)
+ sidekiq (>= 3.0)
+ sidekiq (6.5.12)
+ redis (>= 4.5.0, < 5)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ redis
+ sentry-sidekiq
+ sidekiq (~> 6.5)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ bundle "lock --update sentry-sidekiq"
+
+ expect(lockfile).to eq(original_lockfile.sub("sentry-sidekiq (5.22.0)", "sentry-sidekiq (5.22.4)"))
+ end
+
+ it "does not downgrade indirect dependencies unnecessarily" do
+ build_repo4 do
+ build_gem "a" do |s|
+ s.add_dependency "b"
+ s.add_dependency "c"
+ end
+ build_gem "b"
+ build_gem "c"
+ build_gem "c", "2.0"
+ end
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo4"
+ gem "a"
+ G
+
+ expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0")
+
+ build_repo4 do
+ build_gem "b", "2.0" do |s|
+ s.add_dependency "c", "< 2"
+ end
+ end
+
+ bundle "update", all: true, verbose: true
+ expect(the_bundle).to include_gems("a 1.0", "b 1.0", "c 2.0")
+ end
+
+ it "should still downgrade if forced by the Gemfile" do
+ build_repo4 do
+ build_gem "a"
+ build_gem "b", "1.0"
+ build_gem "b", "2.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "a"
+ gem "b"
+ G
+
+ expect(the_bundle).to include_gems("a 1.0", "b 2.0")
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "a"
+ gem "b", "1.0"
+ G
+
+ bundle "update b"
+
+ expect(the_bundle).to include_gems("a 1.0", "b 1.0")
+ end
+
+ it "should still downgrade if forced by the Gemfile, when transitive dependencies also need downgrade" do
+ build_repo4 do
+ build_gem "activesupport", "6.1.4.1" do |s|
+ s.add_dependency "tzinfo", "~> 2.0"
+ end
+
+ build_gem "activesupport", "6.0.4.1" do |s|
+ s.add_dependency "tzinfo", "~> 1.1"
+ end
+
+ build_gem "tzinfo", "2.0.4"
+ build_gem "tzinfo", "1.2.9"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "activesupport", "~> 6.1.0"
+ G
+
+ expect(the_bundle).to include_gems("activesupport 6.1.4.1", "tzinfo 2.0.4")
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "activesupport", "~> 6.0.0"
+ G
+
+ original_lockfile = lockfile
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "activesupport", "6.0.4.1"
+ c.checksum gem_repo4, "tzinfo", "1.2.9"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activesupport (6.0.4.1)
+ tzinfo (~> 1.1)
+ tzinfo (1.2.9)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport (~> 6.0.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update activesupport"
+ expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
+ expect(lockfile).to eq(expected_lockfile)
+
+ lockfile original_lockfile
+ bundle "update"
+ expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
+ expect(lockfile).to eq(expected_lockfile)
+
+ lockfile original_lockfile
+ bundle "lock --update"
+ expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
+ expect(lockfile).to eq(expected_lockfile)
+ end
+ end
+
+ describe "with --local option" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "doesn't hit repo2" do
+ simulate_platform "x86-darwin-100" do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ activesupport (2.3.5)
+ platform_specific (1.0-x86-darwin-100)
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ x86-darwin-100
+
+ DEPENDENCIES
+ activesupport
+ platform_specific
+ myrack-obama
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ FileUtils.rm_r(gem_repo2)
+
+ bundle "update --local --all"
+ expect(out).not_to include("Fetching source index")
+ end
+ end
+ end
+
+ describe "with --group option" do
+ before do
+ build_repo2
+ end
+
+ it "should update only specified group gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", :group => :development
+ gem "myrack"
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+ bundle "update --group development"
+ expect(the_bundle).to include_gems "activesupport 3.0"
+ expect(the_bundle).not_to include_gems "myrack 1.2"
+ end
+
+ context "when conservatively updating a group with non-group sub-deps" do
+ it "should update only specified group gems" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activemerchant", :group => :development
+ gem "activesupport"
+ G
+ update_repo2 do
+ build_gem "activemerchant", "2.0"
+ build_gem "activesupport", "3.0"
+ end
+ bundle "update --conservative --group development"
+ expect(the_bundle).to include_gems "activemerchant 2.0"
+ expect(the_bundle).not_to include_gems "activesupport 3.0"
+ end
+ end
+
+ context "when there is a source with the same name as a gem in a group" do
+ before do
+ build_git "foo", path: lib_path("activesupport")
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport", :group => :development
+ gem "foo", :git => "#{lib_path("activesupport")}"
+ G
+ end
+
+ it "should not update the gems from that source" do
+ update_repo2 { build_gem "activesupport", "3.0" }
+ update_git "foo", "2.0", path: lib_path("activesupport")
+
+ bundle "update --group development"
+ expect(the_bundle).to include_gems "activesupport 3.0"
+ expect(the_bundle).not_to include_gems "foo 2.0"
+ end
+ end
+
+ context "when bundler itself is a transitive dependency" do
+ it "executes without error" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", :group => :development
+ gem "myrack"
+ G
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "activesupport", "3.0"
+ end
+ bundle "update --group development"
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}"
+ expect(the_bundle).not_to include_gems "myrack 1.2"
+ end
+ end
+ end
+
+ describe "in a frozen bundle" do
+ before do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ gem "myrack-obama"
+ gem "platform_specific"
+ G
+ end
+
+ it "should fail loudly" do
+ bundle_config "deployment true"
+ bundle "update", all: true, raise_on_error: false
+
+ expect(last_command).to be_failure
+ expect(err).to eq <<~ERROR.strip
+ Bundler is unlocking, but the lockfile can't be updated because frozen mode is set
+
+ If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`.
+ ERROR
+ end
+
+ it "should fail loudly when frozen is set globally" do
+ bundle_config_global "frozen 1"
+ bundle "update", all: true, raise_on_error: false
+ expect(err).to eq <<~ERROR.strip
+ Bundler is unlocking, but the lockfile can't be updated because frozen mode is set
+
+ If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`.
+ ERROR
+ end
+
+ it "should fail loudly when deployment is set globally" do
+ bundle_config_global "deployment true"
+ bundle "update", all: true, raise_on_error: false
+ expect(err).to eq <<~ERROR.strip
+ Bundler is unlocking, but the lockfile can't be updated because frozen mode is set
+
+ If this is a development machine, remove the Gemfile.lock freeze by running `bundle config set frozen false`.
+ ERROR
+ end
+
+ it "should not suggest any command to unfreeze bundler if frozen is set through ENV" do
+ bundle "update", all: true, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" }
+ expect(err).to eq("Bundler is unlocking, but the lockfile can't be updated because frozen mode is set")
+ end
+ end
+
+ describe "with --source option" do
+ before do
+ build_repo2
+ end
+
+ it "should not update gems not included in the source that happen to have the same name" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "update --source activesupport"
+ expect(the_bundle).not_to include_gem "activesupport 3.0"
+ end
+
+ it "should not update gems not included in the source that happen to have the same name" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+ update_repo2 { build_gem "activesupport", "3.0" }
+
+ bundle "update --source activesupport"
+ expect(the_bundle).not_to include_gems "activesupport 3.0"
+ end
+ end
+
+ context "when there is a child dependency that is also in the gemfile" do
+ before do
+ build_repo2 do
+ build_gem "fred", "1.0"
+ build_gem "harry", "1.0" do |s|
+ s.add_dependency "fred"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "harry"
+ gem "fred"
+ G
+ end
+
+ it "should not update the child dependencies of a gem that has the same name as the source" do
+ update_repo2 do
+ build_gem "fred", "2.0"
+ build_gem "harry", "2.0" do |s|
+ s.add_dependency "fred"
+ end
+ end
+
+ bundle "update --source harry"
+ expect(the_bundle).to include_gems "harry 1.0", "fred 1.0"
+ end
+ end
+
+ context "when there is a child dependency that appears elsewhere in the dependency graph" do
+ before do
+ build_repo2 do
+ build_gem "fred", "1.0" do |s|
+ s.add_dependency "george"
+ end
+ build_gem "george", "1.0"
+ build_gem "harry", "1.0" do |s|
+ s.add_dependency "george"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "harry"
+ gem "fred"
+ G
+ end
+
+ it "should not update the child dependencies of a gem that has the same name as the source" do
+ update_repo2 do
+ build_gem "george", "2.0"
+ build_gem "harry", "2.0" do |s|
+ s.add_dependency "george"
+ end
+ end
+
+ bundle "update --source harry"
+ expect(the_bundle).to include_gems "harry 1.0", "fred 1.0", "george 1.0"
+ end
+ end
+
+ it "shows the previous version of the gem when updated from rubygems source" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+
+ bundle "update", all: true, verbose: true
+ expect(out).to include("Using activesupport 2.3.5")
+
+ update_repo2 do
+ build_gem "activesupport", "3.0"
+ end
+
+ bundle "update", all: true
+ expect(out).to include("Installing activesupport 3.0 (was 2.3.5)")
+ end
+
+ it "only prints `Using` for versions that have changed" do
+ build_repo4 do
+ build_gem "bar"
+ build_gem "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "bar"
+ gem "foo"
+ G
+
+ bundle "update", all: true
+ expect(out).to match(/Resolving dependencies\.\.\.\.*\nBundle updated!/)
+
+ build_repo4 do
+ build_gem "foo", "2.0"
+ end
+
+ bundle "update", all: true
+ expect(out.sub("Removing foo (1.0)\n", "")).to match(/Resolving dependencies\.\.\.\.*\nFetching foo 2\.0 \(was 1\.0\)\nInstalling foo 2\.0 \(was 1\.0\)\nBundle updated/)
+ end
+
+ it "shows error message when Gemfile.lock is not preset and gem is specified" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+
+ bundle "update nonexisting", raise_on_error: false
+ expect(err).to include("This Bundle hasn't been installed yet. Run `bundle install` to update and install the bundled gems.")
+ expect(exitstatus).to eq(22)
+ end
+
+ context "with multiple sources and caching enabled" do
+ before do
+ build_repo2 do
+ build_gem "myrack", "1.0.0"
+
+ build_gem "request_store", "1.0.0" do |s|
+ s.add_dependency "myrack", "1.0.0"
+ end
+ end
+
+ build_repo4 do
+ # set up repo with no gems
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "request_store"
+
+ source "https://gem.repo4" do
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ request_store (1.0.0)
+ myrack (= 1.0.0)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ request_store
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "works" do
+ bundle :install
+ bundle :cache
+
+ update_repo2 do
+ build_gem "request_store", "1.1.0" do |s|
+ s.add_dependency "myrack", "1.0.0"
+ end
+ end
+
+ bundle "update request_store"
+
+ expect(out).to include("Bundle updated!")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ request_store (1.1.0)
+ myrack (= 1.0.0)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ request_store
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+end
+
+RSpec.describe "bundle update in more complicated situations" do
+ before do
+ build_repo2
+ end
+
+ it "will eagerly unlock dependencies of a specified gem" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "thin"
+ gem "myrack-obama"
+ G
+
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "thin", "2.0" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ bundle "update thin"
+ expect(the_bundle).to include_gems "thin 2.0", "myrack 1.2", "myrack-obama 1.0"
+ end
+
+ it "will warn when some explicitly updated gems are not updated" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "thin"
+ gem "myrack-obama"
+ G
+
+ update_repo2 do
+ build_gem("thin", "2.0") {|s| s.add_dependency "myrack" }
+ build_gem "myrack", "10.0"
+ end
+
+ bundle "update thin myrack-obama"
+ expect(stdboth).to include "Bundler attempted to update myrack-obama but its version stayed the same"
+ expect(the_bundle).to include_gems "thin 2.0", "myrack 10.0", "myrack-obama 1.0"
+ end
+
+ it "will not warn when an explicitly updated git gem changes sha but not version" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => '#{lib_path("foo-1.0")}'
+ G
+
+ update_git "foo" do |s|
+ s.write "lib/foo2.rb", "puts :foo2"
+ end
+
+ bundle "update foo"
+
+ expect(stdboth).not_to include "attempted to update"
+ end
+
+ it "will not warn when changing gem sources but not versions" do
+ build_git "myrack"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack", :git => '#{lib_path("myrack-1.0")}'
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "update myrack"
+
+ expect(stdboth).not_to include "attempted to update"
+ end
+
+ it "will update only from pinned source" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo1" do
+ gem "thin"
+ end
+ G
+
+ update_repo2 do
+ build_gem "thin", "2.0"
+ end
+
+ bundle "update", artifice: "compact_index"
+ expect(the_bundle).to include_gems "thin 1.0"
+ end
+
+ context "when the lockfile is for a different platform" do
+ around do |example|
+ build_repo4 do
+ build_gem("a", "0.9")
+ build_gem("a", "0.9") {|s| s.platform = "java" }
+ build_gem("a", "1.1")
+ build_gem("a", "1.1") {|s| s.platform = "java" }
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "a"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ a (0.9-java)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ a
+ L
+
+ simulate_platform "x86_64-linux", &example
+ end
+
+ it "allows updating" do
+ bundle :update, all: true
+ expect(the_bundle).to include_gem "a 1.1"
+ end
+
+ it "allows updating a specific gem" do
+ bundle "update a"
+ expect(the_bundle).to include_gem "a 1.1"
+ end
+ end
+
+ context "when the dependency is for a different platform" do
+ before do
+ build_repo4 do
+ build_gem("a", "0.9") {|s| s.platform = "java" }
+ build_gem("a", "1.1") {|s| s.platform = "java" }
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "a", platform: :jruby
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ a (0.9-java)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ a
+ L
+ end
+
+ it "is not updated because it is not actually included in the bundle" do
+ simulate_platform "x86_64-linux" do
+ bundle "update a"
+ expect(stdboth).to include "Bundler attempted to update a but it was not considered because it is for a different platform from the current one"
+ expect(the_bundle).to_not include_gem "a"
+ end
+ end
+ end
+end
+
+RSpec.describe "bundle update without a Gemfile.lock" do
+ it "should not explode" do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack", "1.0"
+ G
+
+ bundle "update", all: true
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+end
+
+RSpec.describe "bundle update when a gem depends on a newer version of bundler" do
+ before do
+ build_repo2 do
+ build_gem "rails", "3.0.1" do |s|
+ s.add_dependency "bundler", "9.9.9"
+ end
+
+ build_gem "bundler", "9.9.9"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0.1"
+ G
+ end
+
+ it "should explain that bundler conflicted and how to resolve the conflict" do
+ bundle "update", all: true, raise_on_error: false
+ expect(stdboth).not_to match(/in snapshot/i)
+ expect(err).to match(/current Bundler version/i).
+ and match(/Install the necessary version with `gem install bundler:9\.9\.9`/i)
+ end
+end
+
+RSpec.describe "bundle update --ruby" do
+ context "when the Gemfile removes the ruby" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+ end
+
+ it "removes the Ruby from the Gemfile.lock" do
+ bundle "update --ruby"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums_section_when_enabled}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when the Gemfile specified an updated Ruby version" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+
+ gemfile <<-G
+ ruby '~> #{current_ruby_minor}'
+ source "https://gem.repo1"
+ G
+ end
+
+ it "updates the Gemfile.lock with the latest version" do
+ bundle "update --ruby"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums_section_when_enabled}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when a different Ruby is being used than has been versioned" do
+ before do
+ install_gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+
+ gemfile <<-G
+ ruby '~> 2.1.0'
+ source "https://gem.repo1"
+ G
+ end
+ it "shows a helpful error message" do
+ bundle "update --ruby", raise_on_error: false
+
+ expect(err).to include("Your Ruby version is #{Bundler::RubyVersion.system.gem_version}, but your Gemfile specified ~> 2.1.0")
+ end
+ end
+
+ context "when updating Ruby version and Gemfile `ruby`" do
+ before do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+
+ CHECKSUMS
+
+ RUBY VERSION
+ ruby 2.1.4p222
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ ruby '~> #{Gem.ruby_version}'
+ source "https://gem.repo1"
+ G
+ end
+
+ it "updates the Gemfile.lock with the latest version" do
+ bundle "update --ruby"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ #{checksums_section_when_enabled}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+end
+
+RSpec.describe "bundle update --bundler" do
+ it "updates the bundler version in the lockfile" do
+ build_repo4 do
+ build_gem "bundler", "2.5.9"
+ build_gem "myrack", "1.0"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')
+
+ bundle :update, bundler: true, verbose: true
+ expect(out).to include("Using bundler #{Bundler::VERSION}")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(the_bundle).to include_gem "myrack 1.0"
+ end
+
+ it "updates the bundler version in the lockfile without re-resolving if the highest version is already installed" do
+ build_repo4 do
+ build_gem "bundler", "2.3.9"
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+ lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, "2.3.9")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ end
+
+ bundle :update, bundler: true, verbose: true
+ expect(out).to include("Using bundler #{Bundler::VERSION}")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(the_bundle).to include_gem "myrack 1.0"
+ end
+
+ it "updates the bundler version in the lockfile even if the latest version is not installed", :ruby_repo do
+ bundle_config "path.system true"
+
+ pristine_system_gems "bundler-9.0.0"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+
+ build_bundler "999.0.0"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ c.checksum(gem_repo4, "bundler", "999.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: true, verbose: true
+
+ expect(out).to include("Updating bundler to 999.0.0")
+ expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0")
+ expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 999.0.0
+ L
+
+ expect(the_bundle).to include_gems "bundler 999.0.0"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "does not claim to update to Bundler version to a wrong version when cached gems are present" do
+ pristine_system_gems "bundler-4.99.0"
+
+ build_repo4 do
+ build_gem "myrack", "3.0.9.1"
+
+ build_bundler "4.99.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (3.0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ 2.99.0
+ L
+
+ bundle :cache, verbose: true
+
+ bundle :update, bundler: true, verbose: true
+
+ expect(out).not_to include("Updating bundler to")
+ end
+
+ it "does not update the bundler version in the lockfile if the latest version is not compatible with current ruby", :ruby_repo do
+ pristine_system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+
+ build_bundler "9.9.9"
+ build_bundler "999.0.0" do |s|
+ s.required_ruby_version = "> #{Gem.ruby_version}"
+ end
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ c.checksum(gem_repo4, "bundler", "9.9.9")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: true, verbose: true
+
+ expect(out).to include("Using bundler 9.9.9")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 9.9.9
+ L
+
+ expect(the_bundle).to include_gems "bundler 9.9.9"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "errors if the explicit target version does not exist" do
+ pristine_system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: "999.999.999", raise_on_error: false
+
+ expect(last_command).to be_failure
+ expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist")
+ end
+
+ it "errors if the explicit target version does not exist, even if auto switching is disabled" do
+ pristine_system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ bundle :update, bundler: "999.999.999", raise_on_error: false, env: { "BUNDLER_VERSION" => "9.9.9" }
+
+ expect(last_command).to be_failure
+ expect(err).to eq("The `bundle update --bundler` target version (999.999.999) does not exist")
+ end
+
+ it "allows updating to development versions if already installed locally" do
+ system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ system_gems "bundler-9.0.0.dev", path: local_gem_path
+ bundle :update, bundler: "9.0.0.dev", verbose: "true"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ end
+ checksums.delete("bundler")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 9.0.0.dev
+ L
+
+ expect(out).to include("Using bundler 9.0.0.dev")
+ end
+
+ it "does not touch the network if not necessary" do
+ system_gems "bundler-9.9.9"
+
+ build_repo4 do
+ build_gem "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+ system_gems "bundler-9.0.0", path: local_gem_path
+ bundle :update, bundler: "9.0.0", verbose: true
+
+ expect(out).not_to include("Fetching gem metadata from https://rubygems.org/")
+
+ # Only updates properly on modern RubyGems.
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo4, "myrack", "1.0")
+ c.checksum(local_gem_path, "bundler", "9.0.0", Gem::Platform::RUBY, "cache")
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ 9.0.0
+ L
+
+ expect(out).to include("Using bundler 9.0.0")
+ end
+
+ it "prints an error when trying to update bundler in frozen mode" do
+ system_gems "bundler-9.0.0"
+
+ gemfile <<~G
+ source "https://gem.repo2"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ 9.0.0
+ L
+
+ system_gems "bundler-9.9.9", path: local_gem_path
+
+ bundle "update --bundler=9.9.9", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ expect(err).to include("An update to the version of Bundler itself was requested, but the lockfile can't be updated because frozen mode is set")
+ end
+end
+
+# these specs are slow and focus on integration and therefore are not exhaustive. unit specs elsewhere handle that.
+RSpec.describe "bundle update conservative" do
+ context "patch and minor options" do
+ before do
+ build_repo4 do
+ build_gem "foo", %w[1.4.3 1.4.4] do |s|
+ s.add_dependency "bar", "~> 2.0"
+ end
+ build_gem "foo", %w[1.4.5 1.5.0] do |s|
+ s.add_dependency "bar", "~> 2.1"
+ end
+ build_gem "foo", %w[1.5.1] do |s|
+ s.add_dependency "bar", "~> 3.0"
+ end
+ build_gem "foo", %w[2.0.0.pre] do |s|
+ s.add_dependency "bar"
+ end
+ build_gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 2.1.2.pre 3.0.0 3.1.0.pre 4.0.0.pre]
+ build_gem "qux", %w[1.0.0 1.0.1 1.1.0 2.0.0]
+ end
+
+ # establish a lockfile set to 1.4.3
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo', '1.4.3'
+ gem 'bar', '2.0.3'
+ gem 'qux', '1.0.0'
+ G
+
+ # remove 1.4.3 requirement and bar altogether
+ # to setup update specs below
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'foo'
+ gem 'qux'
+ G
+ end
+
+ context "with patch set as default update level in config" do
+ it "should do a patch level update" do
+ bundle_config "prefer_patch true"
+ bundle "update foo"
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0"
+ end
+ end
+
+ context "patch preferred" do
+ it "single gem updates dependent gem to minor" do
+ bundle "update --patch foo"
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.0"
+ end
+
+ it "update all" do
+ bundle "update --patch", all: true
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.1", "qux 1.0.1"
+ end
+ end
+
+ context "minor preferred" do
+ it "single gem updates dependent gem to major" do
+ bundle "update --minor foo"
+
+ expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.0.0", "qux 1.0.0"
+ end
+ end
+
+ context "strict" do
+ it "patch preferred" do
+ bundle "update --patch foo bar --strict"
+
+ expect(the_bundle).to include_gems "foo 1.4.4", "bar 2.0.5", "qux 1.0.0"
+ end
+
+ it "minor preferred" do
+ bundle "update --minor --strict", all: true
+
+ expect(the_bundle).to include_gems "foo 1.5.0", "bar 2.1.1", "qux 1.1.0"
+ end
+ end
+
+ context "pre" do
+ it "defaults to major" do
+ bundle "update --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0"
+ end
+
+ it "patch preferred" do
+ bundle "update --patch --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 1.4.5", "bar 2.1.2.pre", "qux 1.0.0"
+ end
+
+ it "minor preferred" do
+ bundle "update --minor --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 1.5.1", "bar 3.1.0.pre", "qux 1.0.0"
+ end
+
+ it "major preferred" do
+ bundle "update --major --pre foo bar"
+
+ expect(the_bundle).to include_gems "foo 2.0.0.pre", "bar 4.0.0.pre", "qux 1.0.0"
+ end
+ end
+ end
+
+ context "eager unlocking" do
+ before do
+ build_repo4 do
+ build_gem "isolated_owner", %w[1.0.1 1.0.2] do |s|
+ s.add_dependency "isolated_dep", "~> 2.0"
+ end
+ build_gem "isolated_dep", %w[2.0.1 2.0.2]
+
+ build_gem "shared_owner_a", %w[3.0.1 3.0.2] do |s|
+ s.add_dependency "shared_dep", "~> 5.0"
+ end
+ build_gem "shared_owner_b", %w[4.0.1 4.0.2] do |s|
+ s.add_dependency "shared_dep", "~> 5.0"
+ end
+ build_gem "shared_dep", %w[5.0.1 5.0.2]
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'isolated_owner'
+
+ gem 'shared_owner_a'
+ gem 'shared_owner_b'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ isolated_dep (2.0.1)
+ isolated_owner (1.0.1)
+ isolated_dep (~> 2.0)
+ shared_dep (5.0.1)
+ shared_owner_a (3.0.1)
+ shared_dep (~> 5.0)
+ shared_owner_b (4.0.1)
+ shared_dep (~> 5.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ isolated_owner
+ shared_owner_a
+ shared_owner_b
+
+ CHECKSUMS
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should eagerly unlock isolated dependency" do
+ bundle "update isolated_owner"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.2", "shared_dep 5.0.1", "shared_owner_a 3.0.1", "shared_owner_b 4.0.1"
+ end
+
+ it "should eagerly unlock shared dependency" do
+ bundle "update shared_owner_a"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.1", "isolated_dep 2.0.1", "shared_dep 5.0.2", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1"
+ end
+
+ it "should not eagerly unlock with --conservative" do
+ bundle "update --conservative shared_owner_a isolated_owner"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1"
+ end
+
+ it "should only update direct dependencies when fully updating with --conservative" do
+ bundle "update --conservative"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.2"
+ end
+
+ it "should only change direct dependencies when updating the lockfile with --conservative" do
+ bundle "lock --update --conservative"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "isolated_dep", "2.0.1"
+ c.checksum gem_repo4, "isolated_owner", "1.0.2"
+ c.checksum gem_repo4, "shared_dep", "5.0.1"
+ c.checksum gem_repo4, "shared_owner_a", "3.0.2"
+ c.checksum gem_repo4, "shared_owner_b", "4.0.2"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ isolated_dep (2.0.1)
+ isolated_owner (1.0.2)
+ isolated_dep (~> 2.0)
+ shared_dep (5.0.1)
+ shared_owner_a (3.0.2)
+ shared_dep (~> 5.0)
+ shared_owner_b (4.0.2)
+ shared_dep (~> 5.0)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ isolated_owner
+ shared_owner_a
+ shared_owner_b
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should match bundle install conservative update behavior when not eagerly unlocking" do
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem 'isolated_owner', '1.0.2'
+
+ gem 'shared_owner_a', '3.0.2'
+ gem 'shared_owner_b'
+ G
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "isolated_owner 1.0.2", "isolated_dep 2.0.1", "shared_dep 5.0.1", "shared_owner_a 3.0.2", "shared_owner_b 4.0.1"
+ end
+ end
+
+ context "when Gemfile dependencies have changed" do
+ before do
+ build_repo4 do
+ build_gem "nokogiri", "1.16.4" do |s|
+ s.platform = "arm64-darwin"
+ end
+
+ build_gem "nokogiri", "1.16.4" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "prism", "0.25.0"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "nokogiri", ">=1.16.4"
+ gem "prism", ">=0.25.0"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.4-arm64-darwin)
+ nokogiri (1.16.4-x86_64-linux)
+
+ PLATFORMS
+ arm64-darwin
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri (>= 1.16.4)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "still works" do
+ simulate_platform "arm64-darwin-23" do
+ bundle "update"
+ end
+ end
+ end
+
+ context "error handling" do
+ before do
+ gemfile "source 'https://gem.repo1'"
+ end
+
+ it "raises if too many flags are provided" do
+ bundle "update --patch --minor", all: true, raise_on_error: false
+
+ expect(err).to eq "Provide only one of the following options: minor, patch"
+ end
+ end
+end
diff --git a/spec/bundler/commands/version_spec.rb b/spec/bundler/commands/version_spec.rb
new file mode 100644
index 0000000000..4320ad0611
--- /dev/null
+++ b/spec/bundler/commands/version_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require_relative "../support/path"
+
+RSpec.describe "bundle version" do
+ if Spec::Path.ruby_core?
+ COMMIT_HASH = /unknown|[a-fA-F0-9]{7,}/
+ else
+ COMMIT_HASH = /[a-fA-F0-9]{7,}/
+ end
+
+ context "with -v" do
+ it "outputs the version and virtual version if set" do
+ bundle "-v"
+ expect(out).to eq(Bundler::VERSION.to_s)
+
+ bundle_config "simulate_version 5"
+ bundle "-v"
+ expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)")
+ end
+ end
+
+ context "with --version" do
+ it "outputs the version and virtual version if set" do
+ bundle "--version"
+ expect(out).to eq(Bundler::VERSION.to_s)
+
+ bundle_config "simulate_version 5"
+ bundle "--version"
+ expect(out).to eq("#{Bundler::VERSION} (simulating Bundler 5)")
+ end
+ end
+
+ context "with version" do
+ context "when released", :ruby_repo do
+ before do
+ system_gems "bundler-4.9.9", released: true
+ end
+
+ it "outputs the version, virtual version if set, and build metadata" do
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(2100-01-01 commit #{COMMIT_HASH}\)\z/)
+
+ bundle_config "simulate_version 5"
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(simulating Bundler 5\) \(2100-01-01 commit #{COMMIT_HASH}\)\z/)
+ end
+ end
+
+ context "when not released" do
+ before do
+ system_gems "bundler-4.9.9", released: false
+ end
+
+ it "outputs the version, virtual version if set, and build metadata" do
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(20\d{2}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/)
+
+ bundle_config "simulate_version 5"
+ bundle "version"
+ expect(out).to match(/\A4\.9\.9 \(simulating Bundler 5\) \(20\d{2}-\d{2}-\d{2} commit #{COMMIT_HASH}\)\z/)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb
new file mode 100644
index 0000000000..c7ab7c3d7e
--- /dev/null
+++ b/spec/bundler/install/allow_offline_install_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install allows offline install" do
+ context "with no cached data locally" do
+ it "still installs" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ G
+ expect(the_bundle).to include_gem("myrack 1.0")
+ end
+
+ it "still fails when the network is down" do
+ install_gemfile <<-G, artifice: "fail", raise_on_error: false
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ G
+ expect(err).to include("Could not reach host testgemserver.local.")
+ expect(the_bundle).to_not be_locked
+ end
+ end
+
+ context "with cached data locally" do
+ it "will install from the compact index" do
+ system_gems ["myrack-1.0.0"], path: default_bundle_path
+
+ bundle_config "clean false"
+ install_gemfile <<-G, artifice: "compact_index"
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ gem "myrack", "< 1.0"
+ G
+
+ expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 0.9.1")
+
+ gemfile <<-G
+ source "http://testgemserver.local"
+ gem "myrack-obama"
+ G
+
+ bundle :update, artifice: "fail", all: true
+ expect(stdboth).to include "Using the cached data for the new index because of a network error"
+
+ expect(the_bundle).to include_gems("myrack-obama 1.0", "myrack 1.0.0")
+ end
+
+ def break_git_remote_ops!
+ FileUtils.mkdir_p(tmp("broken_path"))
+ File.open(tmp("broken_path/git"), "w", 0o755) do |f|
+ f.puts <<~RUBY
+ #!/usr/bin/env ruby
+ fetch_args = %w(fetch --force --quiet --no-tags)
+ clone_args = %w(clone --bare --no-hardlinks --quiet)
+
+ if (fetch_args.-(ARGV).empty? || clone_args.-(ARGV).empty?) && File.exist?(ARGV[ARGV.index("--") + 1])
+ warn "git remote ops have been disabled"
+ exit 1
+ end
+ ENV["PATH"] = ENV["PATH"].sub(/^.*?:/, "")
+ exec("git", *ARGV)
+ RUBY
+ end
+
+ old_path = ENV["PATH"]
+ ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}"
+ yield if block_given?
+ ensure
+ ENV["PATH"] = old_path if block_given?
+ end
+
+ it "will install from a cached git repo" do
+ skip "doesn't print errors" if Gem.win_platform?
+
+ git = build_git "a", "1.0.0", path: lib_path("a")
+ update_git("a", path: git.path, branch: "new_branch")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "a", :git => #{git.path.to_s.dump}
+ G
+
+ break_git_remote_ops! { bundle :update, all: true }
+ expect(err).to include("Using cached git data because of network errors")
+ expect(the_bundle).to be_locked
+
+ break_git_remote_ops! do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "a", :git => #{git.path.to_s.dump}, :branch => "new_branch"
+ G
+ end
+ expect(err).to include("Using cached git data because of network errors")
+ expect(the_bundle).to be_locked
+ end
+ end
+end
diff --git a/spec/bundler/install/binstubs_spec.rb b/spec/bundler/install/binstubs_spec.rb
new file mode 100644
index 0000000000..c2eccb3ef2
--- /dev/null
+++ b/spec/bundler/install/binstubs_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "when system_bindir is set" do
+ it "overrides Gem.bindir" do
+ expect(Pathname.new("/usr/bin")).not_to be_writable
+ gemfile <<-G
+ def Gem.bindir; "/usr/bin"; end
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "BUNDLE_SYSTEM_BINDIR" => system_gem_path("altbin").to_s
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(system_gem_path("altbin/myrackup")).to exist
+ end
+ end
+
+ describe "when multiple gems contain the same exe" do
+ before do
+ build_repo2 do
+ build_gem "fake", "14" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "fake"
+ gem "myrack"
+ G
+ end
+
+ it "warns about the situation" do
+ bundle "exec myrackup"
+
+ expect(last_command.stderr).to include(
+ "The `myrackup` executable in the `fake` gem is being loaded, but it's also present in other gems (myrack).\n" \
+ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \
+ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names."
+ ).or include(
+ "The `myrackup` executable in the `myrack` gem is being loaded, but it's also present in other gems (fake).\n" \
+ "If you meant to run the executable for another gem, make sure you use a project specific binstub (`bundle binstub <gem_name>`).\n" \
+ "If you plan to use multiple conflicting executables, generate binstubs for them and disambiguate their names."
+ )
+ end
+ end
+end
diff --git a/spec/bundler/install/bundler_spec.rb b/spec/bundler/install/bundler_spec.rb
new file mode 100644
index 0000000000..86c22dad55
--- /dev/null
+++ b/spec/bundler/install/bundler_spec.rb
@@ -0,0 +1,268 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "with bundler dependencies" do
+ before(:each) do
+ build_repo2 do
+ build_gem "rails", "3.0" do |s|
+ s.add_dependency "bundler", ">= 0.9.0"
+ end
+ build_gem "bundler", "0.9.1"
+ build_gem "bundler", Bundler::VERSION
+ end
+ end
+
+ it "are forced to the current bundler version" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}"
+ end
+
+ it "are forced to the current bundler version even if not already present" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ expect(the_bundle).to include_gems "bundler #{Bundler::VERSION}"
+ end
+
+ it "causes a conflict if explicitly requesting a different version of bundler" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ gem "bundler", "0.9.1"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.1
+ and Gemfile depends on bundler = 0.9.1,
+ version solving has failed.
+
+ Your bundle requires a different version of Bundler than the one you're running.
+ Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install`
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "causes a conflict if explicitly requesting a non matching requirement on bundler" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ gem "bundler", "~> 0.8"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because rails >= 3.0 depends on bundler >= 0.9.0
+ and the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler >= 0.9.0, < 1.A,
+ rails >= 3.0 requires bundler >= 1.A.
+ So, because Gemfile depends on rails = 3.0
+ and Gemfile depends on bundler ~> 0.8,
+ version solving has failed.
+
+ Your bundle requires a different version of Bundler than the one you're running.
+ Install the necessary version with `gem install bundler:0.9.1` and rerun bundler using `bundle _0.9.1_ install`
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "causes a conflict if explicitly requesting a version of bundler that doesn't exist" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ gem "bundler", "0.9.2"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because the current Bundler version (#{Bundler::VERSION}) does not satisfy bundler = 0.9.2
+ and Gemfile depends on bundler = 0.9.2,
+ version solving has failed.
+
+ Your bundle requires a different version of Bundler than the one you're running, and that version could not be found.
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "works for gems with multiple versions in its dependencies" do
+ build_repo2 do
+ build_gem "multiple_versioned_deps" do |s|
+ s.add_dependency "weakling", ">= 0.0.1", "< 0.1"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "multiple_versioned_deps"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "multiple_versioned_deps"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "multiple_versioned_deps 1.0.0"
+ end
+
+ it "includes bundler in the bundle when it's a child dependency" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError; puts 'FAIL'; end"
+ expect(out).to eq("WIN")
+ end
+
+ it "allows gem 'bundler' when Bundler is not in the Gemfile or its dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ run "begin; gem 'bundler'; puts 'WIN'; rescue Gem::LoadError => e; puts e.backtrace; end"
+ expect(out).to eq("WIN")
+ end
+
+ it "causes a conflict if child dependencies conflict" do
+ bundle_config "force_ruby_platform true"
+
+ update_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "activemerchant"
+ gem "rails_pinned_to_old_activesupport"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3
+ and every version of activemerchant depends on activesupport >= 2.0.0,
+ every version of rails_pinned_to_old_activesupport is incompatible with activemerchant >= 0.
+ So, because Gemfile depends on activemerchant >= 0
+ and Gemfile depends on rails_pinned_to_old_activesupport >= 0,
+ version solving has failed.
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "causes a conflict if a child dependency conflicts with the Gemfile" do
+ bundle_config "force_ruby_platform true"
+
+ update_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails_pinned_to_old_activesupport"
+ gem "activesupport", "2.3.5"
+ G
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of rails_pinned_to_old_activesupport depends on activesupport = 1.2.3
+ and Gemfile depends on rails_pinned_to_old_activesupport >= 0,
+ activesupport = 1.2.3 is required.
+ So, because Gemfile depends on activesupport = 2.3.5,
+ version solving has failed.
+ E
+ expect(err).to include(nice_error)
+ end
+
+ it "does not cause a conflict if new dependencies in the Gemfile require older dependencies than the lockfile" do
+ update_repo2 do
+ build_gem "rails_pinned_to_old_activesupport" do |s|
+ s.add_dependency "activesupport", "= 1.2.3"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'rails', "2.3.2"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails_pinned_to_old_activesupport"
+ G
+
+ expect(out).to include("Installing activesupport 1.2.3 (was 2.3.2)")
+ expect(err).to be_empty
+ end
+
+ it "prints the previous version when switching to a previously downloaded gem" do
+ build_repo4 do
+ build_gem "rails", "7.0.3"
+ build_gem "rails", "7.0.4"
+ end
+
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'rails', "7.0.4"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'rails', "7.0.3"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem 'rails', "7.0.4"
+ G
+
+ expect(out).to include("Using rails 7.0.4 (was 7.0.3)")
+ expect(err).to be_empty
+ end
+
+ it "can install dependencies with newer bundler version with system gems" do
+ bundle_config "path.system true"
+
+ system_gems "bundler-99999999.99.1"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ bundle "check"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+
+ it "can install dependencies with newer bundler version with a local path" do
+ bundle_config "path .bundle"
+
+ system_gems "bundler-99999999.99.1"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0"
+ G
+
+ bundle "check"
+ expect(out).to include("The Gemfile's dependencies are satisfied")
+ end
+ end
+end
diff --git a/spec/bundler/install/cooldown_spec.rb b/spec/bundler/install/cooldown_spec.rb
new file mode 100644
index 0000000000..b3f57d93cc
--- /dev/null
+++ b/spec/bundler/install/cooldown_spec.rb
@@ -0,0 +1,272 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with the cooldown setting" do
+ before do
+ build_repo2
+ end
+
+ context "Gemfile DSL" do
+ it "accepts `source ..., cooldown: N` without error" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2", cooldown: 5
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "accepts `cooldown: 0` to disable cooldown for a source" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2", cooldown: 0
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "CLI flag" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ end
+
+ it "accepts --cooldown N on install" do
+ bundle "install --cooldown 7", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "accepts --cooldown 0 as an escape hatch" do
+ bundle "install --cooldown 0", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "rejects a negative --cooldown value" do
+ bundle "install --cooldown=-7", artifice: "compact_index", raise_on_error: false
+
+ expect(err).to match(/non-negative integer/)
+ end
+ end
+
+ context "configuration" do
+ it "reads BUNDLE_COOLDOWN as an integer" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle "install", env: { "BUNDLE_COOLDOWN" => "7" }, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "reads `bundle config set cooldown N`" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ bundle "config set cooldown 7"
+ bundle "install", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "end-to-end with v2 compact index" do
+ before do
+ now = Time.now.utc
+ build_repo3 do
+ build_gem "ripe_gem", "1.0.0" do |s|
+ s.date = now - (30 * 86_400)
+ end
+ build_gem "ripe_gem", "2.0.0" do |s|
+ s.date = now - (1 * 86_400)
+ end
+ end
+ end
+
+ it "excludes versions within the cooldown window" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "selects the latest version when --cooldown 0 is passed" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 0", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 2.0.0")
+ end
+
+ it "applies cooldown declared per-source in the Gemfile" do
+ gemfile <<-G
+ source "https://gem.repo3", cooldown: 7
+ gem "ripe_gem"
+ G
+
+ bundle "install", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "is overridden by CLI --cooldown when Gemfile sets a different per-source value" do
+ gemfile <<-G
+ source "https://gem.repo3", cooldown: 0
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "bypasses cooldown when bundle install uses an existing lockfile" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (2.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 2.0.0")
+ end
+
+ it "annotates in-cooldown versions in bundle outdated table output" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem", "1.0.0"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem (= 1.0.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "outdated --cooldown 7", artifice: "compact_index_cooldown", raise_on_error: false
+
+ expect(out).to match(/ripe_gem.*\(cooldown \d+d\)/)
+ end
+
+ it "annotates in-cooldown versions in bundle outdated --parseable output" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem", "1.0.0"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem (= 1.0.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "outdated --cooldown 7 --parseable", artifice: "compact_index_cooldown", raise_on_error: false
+
+ expect(out).to match(/ripe_gem.*in cooldown for \d+ more day/)
+ end
+
+ it "excludes a locally-installed version that is still within the cooldown window" do
+ system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3
+
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 7", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 1.0.0")
+ end
+
+ it "selects a locally-installed in-cooldown version when --cooldown 0 bypasses the filter" do
+ system_gems "ripe_gem-2.0.0", gem_repo: gem_repo3
+
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ bundle "install --cooldown 0", artifice: "compact_index_cooldown"
+
+ expect(the_bundle).to include_gems("ripe_gem 2.0.0")
+ end
+
+ it "surfaces a cooldown hint when bundle update filters every candidate" do
+ gemfile <<-G
+ source "https://gem.repo3"
+ gem "ripe_gem"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ ripe_gem (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ripe_gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update ripe_gem --cooldown 99999", artifice: "compact_index_cooldown", raise_on_error: false
+
+ expect(err).to match(/excluded by the cooldown setting/)
+ expect(err).to match(/--cooldown 0/)
+ end
+ end
+end
diff --git a/spec/bundler/install/deploy_spec.rb b/spec/bundler/install/deploy_spec.rb
new file mode 100644
index 0000000000..a3b4a87ecf
--- /dev/null
+++ b/spec/bundler/install/deploy_spec.rb
@@ -0,0 +1,493 @@
+# frozen_string_literal: true
+
+RSpec.describe "install in deployment or frozen mode" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "fails without a lockfile and says that deployment requires a lock" do
+ bundle_config "deployment true"
+ bundle "install", raise_on_error: false
+ expect(err).to include("The deployment setting requires a lockfile")
+ end
+
+ it "fails without a lockfile and says that frozen requires a lock" do
+ bundle_config "frozen true"
+ bundle "install", raise_on_error: false
+ expect(err).to include("The frozen setting requires a lockfile")
+ end
+
+ it "still works if you are not in the app directory and specify --gemfile" do
+ bundle "install"
+ pristine_system_gems
+ bundle_config "deployment true"
+ bundle_config "path vendor/bundle"
+ bundle "install --gemfile #{tmp}/bundled_app/Gemfile", dir: tmp
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "works if you exclude a group with a git gem" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ group :test do
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ end
+ G
+ bundle :install
+ bundle_config "deployment true"
+ bundle_config "without test"
+ bundle :install
+ end
+
+ it "works when you bundle exec bundle" do
+ skip "doesn't find bundle" if Gem.win_platform?
+
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ bundle "exec bundle check", env: { "PATH" => path }
+ end
+
+ it "works when using path gems from the same path and the version is specified" do
+ build_lib "foo", path: lib_path("nested/foo")
+ build_lib "bar", path: lib_path("nested/bar")
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :path => "#{lib_path("nested")}"
+ gem "bar", :path => "#{lib_path("nested")}"
+ G
+
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ end
+
+ it "works when path gems are specified twice" do
+ build_lib "foo", path: lib_path("nested/foo")
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("nested/foo")}"
+ gem "foo", :path => "#{lib_path("nested/foo")}"
+ G
+
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ end
+
+ it "works when there are credentials in the source URL" do
+ install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true)
+ source "http://user:pass@localgemserver.test/"
+
+ gem "myrack-obama", ">= 1.0"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+ end
+
+ it "works with sources given by a block" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo1" do
+ gem "myrack"
+ end
+ G
+
+ bundle_config "deployment true"
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ context "when replacing a host with the same host with credentials" do
+ before do
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ gemfile <<-G
+ source "http://user_name:password@localgemserver.test/"
+ gem "myrack"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: http://localgemserver.test/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ myrack
+ G
+
+ bundle_config "deployment true"
+ end
+
+ it "allows the replace" do
+ bundle :install
+
+ expect(out).to match(/Bundle complete!/)
+ end
+ end
+
+ describe "with an existing lockfile" do
+ before do
+ bundle "install"
+ end
+
+ it "installs gems by default to vendor/bundle" do
+ bundle_config "deployment true"
+ expect do
+ bundle "install"
+ end.not_to change { bundled_app_lock.mtime }
+ expect(out).to include("vendor/bundle")
+ end
+
+ it "installs gems to custom path if specified" do
+ bundle_config "path vendor/bundle2"
+ bundle_config "deployment true"
+ bundle "install"
+ expect(out).to include("vendor/bundle2")
+ end
+
+ it "installs gems to custom path if specified, even when configured through ENV" do
+ bundle_config "deployment true"
+ bundle "install", env: { "BUNDLE_PATH" => "vendor/bundle2" }
+ expect(out).to include("vendor/bundle2")
+ end
+
+ it "works with the `frozen` setting" do
+ bundle_config "frozen true"
+ expect do
+ bundle "install"
+ end.not_to change { bundled_app_lock.mtime }
+ end
+
+ it "works with BUNDLE_FROZEN if you didn't change anything" do
+ expect do
+ bundle :install, env: { "BUNDLE_FROZEN" => "true" }
+ end.not_to change { bundled_app_lock.mtime }
+ end
+
+ it "explodes with the `deployment` setting if you make a change and don't check in the lockfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile")
+ expect(err).to include("* myrack-obama")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "works if a path gem is missing but is in a without group" do
+ build_lib "path_gem"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development
+ G
+ expect(the_bundle).to include_gems "path_gem 1.0"
+ FileUtils.rm_r lib_path("path_gem-1.0")
+
+ bundle_config "path .bundle"
+ bundle_config "without development"
+ bundle_config "deployment true"
+ bundle :install, env: { "DEBUG" => "1" }
+ run "puts :WIN"
+ expect(out).to eq("WIN")
+ end
+
+ it "works if a gem is missing, but it's on a different platform" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo1" do
+ gem "rake", platform: :#{not_local_tag}
+ end
+ G
+
+ bundle :install, env: { "BUNDLE_FROZEN" => "true" }
+ expect(last_command).to be_success
+ end
+
+ it "shows a good error if a gem is missing from the lockfile" do
+ build_repo4 do
+ build_gem "foo"
+ build_gem "bar"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "foo"
+ gem "bar"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ bar
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false, artifice: "compact_index"
+ expect(err).to include("Your lockfile is missing \"bar\", but can't be updated because frozen mode is set")
+ end
+
+ it "explodes if a path gem is missing" do
+ build_lib "path_gem"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "path_gem", :path => "#{lib_path("path_gem-1.0")}", :group => :development
+ G
+ expect(the_bundle).to include_gems "path_gem 1.0"
+ FileUtils.rm_r lib_path("path_gem-1.0")
+
+ bundle_config "path .bundle"
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("The path `#{lib_path("path_gem-1.0")}` does not exist.")
+ end
+
+ it "can have --frozen set via an environment variable" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ ENV["BUNDLE_FROZEN"] = "1"
+ bundle "install", raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile")
+ expect(err).to include("* myrack-obama")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "can have --deployment set via an environment variable" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ ENV["BUNDLE_DEPLOYMENT"] = "true"
+ bundle "install", raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile")
+ expect(err).to include("* myrack-obama")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "installs gems by default to vendor/bundle when deployment mode is set via an environment variable" do
+ ENV["BUNDLE_DEPLOYMENT"] = "true"
+ bundle "install"
+ expect(out).to include("vendor/bundle")
+ end
+
+ it "installs gems to custom path when deployment mode is set via an environment variable " do
+ ENV["BUNDLE_DEPLOYMENT"] = "true"
+ ENV["BUNDLE_PATH"] = "vendor/bundle2"
+ bundle "install"
+ expect(out).to include("vendor/bundle2")
+ end
+
+ it "can have --frozen set to false via an environment variable" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ ENV["BUNDLE_FROZEN"] = "false"
+ ENV["BUNDLE_DEPLOYMENT"] = "false"
+ bundle "install"
+ expect(out).not_to include("frozen mode")
+ expect(out).not_to include("You have added to the Gemfile")
+ expect(out).not_to include("* myrack-obama")
+ end
+
+ it "explodes if you replace a gem and don't check in the lockfile" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have added to the Gemfile:\n* activesupport\n\n")
+ expect(err).to include("You have deleted from the Gemfile:\n* myrack")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "explodes if you remove a gem and don't check in the lockfile" do
+ gemfile 'source "https://gem.repo1"'
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("Some dependencies were deleted")
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have deleted from the Gemfile:\n* myrack")
+ expect(err).not_to include("You have changed in the Gemfile")
+ end
+
+ it "explodes if you add a source" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "git://hubz.com"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `no specified source` to `git://hubz.com`")
+ end
+
+ it "explodes if you change a source from git to the default" do
+ build_git "myrack"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-1.0")}"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack-1.0")}` to `no specified source`")
+ end
+
+ it "explodes if you change a source from git to the default, in presence of other git sources" do
+ build_lib "foo", path: lib_path("myrack/foo")
+ build_git "myrack", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack")}"
+ gem "foo", :git => "#{lib_path("myrack")}"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "foo", :git => "#{lib_path("myrack")}"
+ G
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `no specified source`")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ end
+
+ it "explodes if you change a source from path to git" do
+ build_git "myrack", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :path => "#{lib_path("myrack")}"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "https:/my-git-repo-for-myrack"
+ G
+
+ bundle_config "frozen true"
+ bundle :install, raise_on_error: false
+ expect(err).to include("frozen mode")
+ expect(err).to include("You have changed in the Gemfile:\n* myrack from `#{lib_path("myrack")}` to `https:/my-git-repo-for-myrack`")
+ expect(err).not_to include("You have added to the Gemfile")
+ expect(err).not_to include("You have deleted from the Gemfile")
+ end
+
+ it "remembers that the bundle is frozen at runtime" do
+ bundle :lock
+
+ bundle_config "deployment true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ gem "myrack-obama"
+ G
+
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to include <<~E.strip
+ The dependencies in your gemfile changed, but the lockfile can't be updated because frozen mode is set (Bundler::ProductionError)
+
+ You have added to the Gemfile:
+ * myrack (= 1.0.0)
+ * myrack-obama
+
+ You have deleted from the Gemfile:
+ * myrack
+ E
+ end
+ end
+
+ context "with path in Gemfile and packed" do
+ it "works fine after bundle package and bundle install --local" do
+ build_lib "foo", path: lib_path("foo")
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ bundle :install
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo")).to be_directory
+
+ bundle "install --local"
+ expect(out).to include("Updating files in vendor/cache")
+
+ pristine_system_gems
+ bundle_config "deployment true"
+ bundle "install --verbose"
+ expect(out).not_to include("can't be updated because frozen mode is set")
+ expect(out).not_to include("You have added to the Gemfile")
+ expect(out).not_to include("You have deleted from the Gemfile")
+ expect(out).to include("vendor/cache/foo")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/failure_spec.rb b/spec/bundler/install/failure_spec.rb
new file mode 100644
index 0000000000..32ca455439
--- /dev/null
+++ b/spec/bundler/install/failure_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "installing a gem fails" do
+ it "prints out why that gem was being installed and the underlying error" do
+ build_repo2 do
+ build_gem "activesupport", "2.3.2" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ abort "make installing activesupport-2.3.2 fail"
+ end
+ RUBY
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "rails"
+ G
+ expect(err).to start_with("Gem::Ext::BuildError: ERROR: Failed to build gem native extension.")
+ expect(err).to end_with(<<-M.strip)
+An error occurred while installing activesupport (2.3.2), and Bundler cannot continue.
+
+In Gemfile:
+ rails was resolved to 2.3.2, which depends on
+ actionmailer was resolved to 2.3.2, which depends on
+ activesupport
+ M
+ end
+
+ context "because the downloaded .gem was invalid" do
+ before do
+ build_repo4 do
+ build_gem "a"
+ end
+
+ gem_repo4("gems", "a-1.0.gem").open("w") {|f| f << "<html></html>" }
+ end
+
+ it "removes the downloaded .gem" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "a"
+ G
+
+ expect(default_bundle_path("cache", "a-1.0.gem")).not_to exist
+ end
+ end
+ end
+
+ context "when lockfile dependencies don't match the gemspec" do
+ before do
+ build_repo4 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.add_dependency "myrack-test", "~> 1.0"
+ end
+
+ build_gem "myrack-test", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack"
+ G
+
+ # First install to generate lockfile
+ bundle :install
+
+ # Manually edit lockfile to have incorrect dependencies
+ lockfile_content = File.read(bundled_app_lock)
+ # Remove the myrack-test dependency from myrack
+ lockfile_content.gsub!(/^ myrack \(1\.0\.0\)\n myrack-test \(~> 1\.0\)\n/, " myrack (1.0.0)\n")
+ File.write(bundled_app_lock, lockfile_content)
+ end
+
+ it "reports the mismatch with detailed information" do
+ bundle :install, raise_on_error: false, env: { "BUNDLE_FROZEN" => "true" }
+
+ expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack-1.0.0")
+ expect(err).to include("myrack-test: gemspec specifies ~> 1.0, not in lockfile")
+ expect(err).to include("Please run `bundle install` to regenerate the lockfile.")
+ end
+ end
+end
diff --git a/spec/bundler/install/force_spec.rb b/spec/bundler/install/force_spec.rb
new file mode 100644
index 0000000000..e0f6fb6364
--- /dev/null
+++ b/spec/bundler/install/force_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ shared_examples_for "an option to force reinstalling gems" do
+ it "re-installs installed gems" do
+ myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb")
+
+ bundle :install
+ myrack_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :install, flag => true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "works on first bundle install" do
+ bundle :install, flag => true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ context "with a git gem" do
+ let!(:ref) { build_git("foo", "1.0").ref_for("HEAD", 11) }
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+ end
+
+ it "re-installs installed gems" do
+ foo_lib = default_bundle_path("bundler/gems/foo-1.0-#{ref}/lib/foo.rb")
+
+ bundle :install
+ foo_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :install, flag => true
+
+ expect(foo_lib.open(&:read)).to eq("FOO = '1.0'\n")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works on first bundle install" do
+ bundle :install, flag => true
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+ end
+
+ describe "with --force" do
+ it_behaves_like "an option to force reinstalling gems" do
+ let(:flag) { "force" }
+ end
+ end
+
+ describe "with --redownload" do
+ it_behaves_like "an option to force reinstalling gems" do
+ let(:flag) { "redownload" }
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/eval_gemfile_spec.rb b/spec/bundler/install/gemfile/eval_gemfile_spec.rb
new file mode 100644
index 0000000000..3afa4f5daa
--- /dev/null
+++ b/spec/bundler/install/gemfile/eval_gemfile_spec.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with gemfile that uses eval_gemfile" do
+ before do
+ build_lib("gunks", path: bundled_app("gems/gunks")) do |s|
+ s.name = "gunks"
+ s.version = "0.0.1"
+ end
+ end
+
+ context "eval-ed Gemfile points to an internal gemspec" do
+ before do
+ gemfile "Gemfile-other", <<-G
+ source "https://gem.repo1"
+ gemspec :path => 'gems/gunks'
+ G
+ end
+
+ it "installs the gemspec specified gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ eval_gemfile 'Gemfile-other'
+ G
+ expect(out).to include("Resolving dependencies")
+ expect(out).to include("Bundle complete")
+
+ expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}"
+ end
+ end
+
+ context "eval-ed Gemfile points to an internal gemspec and uses a scoped source that duplicates the main Gemfile global source" do
+ before do
+ build_repo2 do
+ build_gem "rails", "6.1.3.2"
+
+ build_gem "zip-zip", "0.3"
+ end
+
+ gemfile bundled_app("gems/Gemfile"), <<-G
+ source "https://gem.repo2"
+
+ gemspec :path => "\#{__dir__}/gunks"
+
+ source "https://gem.repo2" do
+ gem "zip-zip"
+ end
+ G
+ end
+
+ it "installs and finds gems correctly" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "rails"
+
+ eval_gemfile File.join(__dir__, "gems/Gemfile")
+ G
+ expect(out).to include("Resolving dependencies")
+ expect(out).to include("Bundle complete")
+
+ expect(the_bundle).to include_gem "rails 6.1.3.2"
+ end
+ end
+
+ context "eval-ed Gemfile has relative-path gems" do
+ before do
+ build_lib("a", path: bundled_app("gems/a"))
+ gemfile bundled_app("nested/Gemfile-nested"), <<-G
+ source "https://gem.repo1"
+ gem "a", :path => "../gems/a"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ eval_gemfile "nested/Gemfile-nested"
+ G
+ end
+
+ it "installs the path gem" do
+ bundle :install
+ expect(the_bundle).to include_gem("a 1.0")
+ end
+
+ # Make sure that we are properly comparing path based gems between the
+ # parsed lockfile and the evaluated gemfile.
+ it "bundles with deployment mode configured" do
+ bundle :install
+ bundle_config "deployment true"
+ bundle :install
+ end
+ end
+
+ context "Gemfile uses gemspec paths after eval-ing a Gemfile" do
+ before { create_file "other/Gemfile-other" }
+
+ it "installs the gemspec specified gem" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ eval_gemfile 'other/Gemfile-other'
+ gemspec :path => 'gems/gunks'
+ G
+ expect(out).to include("Resolving dependencies")
+ expect(out).to include("Bundle complete")
+
+ expect(the_bundle).to include_gem "gunks 0.0.1", source: "path@#{bundled_app("gems", "gunks")}"
+ end
+ end
+
+ context "eval-ed Gemfile references other gemfiles" do
+ it "works with relative paths" do
+ gemfile "other/Gemfile-other", "gem 'myrack'"
+ gemfile "other/Gemfile", "eval_gemfile 'Gemfile-other'"
+ gemfile "Gemfile-alt", <<-G
+ source "https://gem.repo1"
+ eval_gemfile "other/Gemfile"
+ G
+ install_gemfile "eval_gemfile File.expand_path('Gemfile-alt')"
+
+ expect(the_bundle).to include_gem "myrack 1.0.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/force_ruby_platform_spec.rb b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb
new file mode 100644
index 0000000000..bcc1f36823
--- /dev/null
+++ b/spec/bundler/install/gemfile/force_ruby_platform_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with force_ruby_platform DSL option", :jruby do
+ context "when no transitive deps" do
+ before do
+ build_repo4 do
+ # Build a gem with platform specific versions
+ build_gem("platform_specific")
+
+ build_gem("platform_specific") do |s|
+ s.platform = Bundler.local_platform
+ end
+
+ # Build the exact same gem with a different name to compare using vs not using the option
+ build_gem("platform_specific_forced")
+
+ build_gem("platform_specific_forced") do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+ end
+
+ it "pulls the pure ruby variant of the given gem" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "platform_specific_forced", :force_ruby_platform => true
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby"
+ expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}"
+ end
+
+ it "still respects a global `force_ruby_platform` config" do
+ install_gemfile <<-G, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" }
+ source "https://gem.repo4"
+
+ gem "platform_specific_forced", :force_ruby_platform => true
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific_forced 1.0 ruby"
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+ end
+
+ context "when also a transitive dependency" do
+ before do
+ build_repo4 do
+ build_gem("depends_on_platform_specific") {|s| s.add_dependency "platform_specific" }
+
+ build_gem("platform_specific")
+
+ build_gem("platform_specific") do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+ end
+
+ it "still pulls the ruby variant" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "depends_on_platform_specific"
+ gem "platform_specific", :force_ruby_platform => true
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+ end
+
+ context "with transitive dependencies with platform specific versions" do
+ before do
+ build_repo4 do
+ build_gem("depends_on_platform_specific") do |s|
+ s.add_dependency "platform_specific"
+ end
+
+ build_gem("depends_on_platform_specific") do |s|
+ s.add_dependency "platform_specific"
+ s.platform = Bundler.local_platform
+ end
+
+ build_gem("platform_specific")
+
+ build_gem("platform_specific") do |s|
+ s.platform = Bundler.local_platform
+ end
+ end
+ end
+
+ it "ignores ruby variants for the transitive dependencies" do
+ install_gemfile <<-G, env: { "DEBUG_RESOLVER" => "true" }
+ source "https://gem.repo4"
+
+ gem "depends_on_platform_specific", :force_ruby_platform => true
+ G
+
+ expect(the_bundle).to include_gems "depends_on_platform_specific 1.0 ruby"
+ expect(the_bundle).to include_gems "platform_specific 1.0 #{Bundler.local_platform}"
+ end
+
+ it "reinstalls the ruby variant when a platform specific variant is already installed, the lockile has only ruby platform, and :force_ruby_platform is used in the Gemfile" do
+ skip "Can't simulate platform reliably on JRuby, installing a platform specific gem fails to activate io-wait because only the -java version is present, and we're simulating a different platform" if RUBY_ENGINE == "jruby"
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ platform_specific (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86-darwin-100" do
+ system_gems "platform_specific-1.0-x86-darwin-100", path: default_bundle_path
+
+ install_gemfile <<-G, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }, artifice: "compact_index"
+ source "https://gem.repo4"
+
+ gem "platform_specific", :force_ruby_platform => true
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/gemspec_spec.rb b/spec/bundler/install/gemfile/gemspec_spec.rb
new file mode 100644
index 0000000000..e51fc9247d
--- /dev/null
+++ b/spec/bundler/install/gemfile/gemspec_spec.rb
@@ -0,0 +1,737 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install from an existing gemspec" do
+ before(:each) do
+ build_repo2 do
+ build_gem "bar"
+ build_gem "bar-dev"
+ end
+ end
+
+ it "should install runtime and development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source :rubygems\ngemspec")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development
+ end
+
+ it "that is hidden should install runtime and development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source :rubygems\ngemspec")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+ FileUtils.mv tmp("foo", "foo.gemspec"), tmp("foo", ".gemspec")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development
+ end
+
+ it "should handle a list of requirements" do
+ update_repo2 do
+ build_gem "baz", "1.0"
+ build_gem "baz", "1.1"
+ end
+
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source :rubygems\ngemspec")
+ s.add_dependency "baz", ">= 1.0", "< 1.1"
+ end
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "baz 1.0"
+ end
+
+ it "should raise if there are no gemspecs available" do
+ build_lib("foo", path: tmp("foo"), gemspec: false)
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+ expect(err).to match(/There are no gemspecs at #{tmp("foo")}/)
+ end
+
+ it "should raise if there are too many gemspecs available" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo2.gemspec", build_spec("foo", "4.0").first.to_ruby)
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+ expect(err).to match(/There are multiple gemspecs at #{tmp("foo")}/)
+ end
+
+ it "should pick a specific gemspec" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo2.gemspec", "")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+
+ install_gemfile(<<-G)
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :development
+ end
+
+ it "should use a specific group for development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("foo2.gemspec", "")
+ s.add_dependency "bar", "=1.0.0"
+ s.add_development_dependency "bar-dev", "=1.0.0"
+ end
+
+ install_gemfile(<<-G)
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo', :development_group => :dev
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0.0"
+ expect(the_bundle).not_to include_gems "bar-dev 1.0.0", groups: :development
+ expect(the_bundle).to include_gems "bar-dev 1.0.0", groups: :dev
+ end
+
+ it "should match a lockfile even if the gemspec defines development dependencies" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.write("Gemfile", "source 'https://gem.repo1'\ngemspec")
+ s.add_dependency "actionpack", "=2.3.2"
+ s.add_development_dependency "rake", rake_version
+ end
+
+ bundle "install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ # This should really be able to rely on $stderr, but, it's not written
+ # right, so we can't. In fact, this is a bug negation test, and so it'll
+ # ghost pass in future, and will only catch a regression if the message
+ # doesn't change. Exit codes should be used correctly (they can be more
+ # than just 0 and 1).
+ bundle_config "deployment true"
+ output = bundle("install", dir: tmp("foo"), artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s })
+ expect(output).not_to match(/You have added to the Gemfile/)
+ expect(output).not_to match(/You have deleted from the Gemfile/)
+ expect(output).not_to match(/the lockfile can't be updated because frozen mode is set/)
+ end
+
+ it "should match a lockfile without needing to re-resolve" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "myrack"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ bundle "install", verbose: true
+
+ message = "Found no changes, using resolution from the lockfile"
+ expect(out.scan(message).size).to eq(1)
+ end
+
+ it "should match a lockfile without needing to re-resolve with development dependencies" do
+ simulate_platform "java" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "myrack"
+ s.add_development_dependency "thin"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ bundle "install", verbose: true
+
+ message = "Found no changes, using resolution from the lockfile"
+ expect(out.scan(message).size).to eq(1)
+ end
+ end
+
+ it "should match a lockfile on non-ruby platforms with a transitive platform dependency", :jruby_only do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "platform_specific"
+ end
+
+ system_gems "platform_specific-1.0-java", path: default_bundle_path
+
+ install_gemfile <<-G
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ bundle "update --bundler", artifice: "compact_index", verbose: true
+ expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 java"
+ end
+
+ it "should evaluate the gemspec in its directory" do
+ build_lib("foo", path: tmp("foo"))
+ File.open(tmp("foo/foo.gemspec"), "w") do |s|
+ s.write "raise 'ahh' unless Dir.pwd == '#{tmp("foo")}'"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ gemspec :path => '#{tmp("foo")}'
+ G
+ expect(stdboth).not_to include("ahh")
+ end
+
+ it "allows the gemspec to activate other gems" do
+ ENV["BUNDLE_PATH__SYSTEM"] = "true"
+ # see https://github.com/rubygems/bundler/issues/5409
+ #
+ # issue was caused by rubygems having an unresolved gem during a require,
+ # so emulate that
+ system_gems %w[myrack-1.0.0 myrack-0.9.1 myrack-obama-1.0]
+
+ build_lib("foo", path: bundled_app)
+ gemspec = bundled_app("foo.gemspec").read
+ bundled_app("foo.gemspec").open("w") do |f|
+ f.write "#{gemspec.strip}.tap { gem 'myrack-obama'; require 'myrack/obama' }"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gem "foo 1.0"
+ end
+
+ it "allows conflicts" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_dependency "bar", "= 1.0.0"
+ end
+ build_gem "deps", to_bundle: true do |s|
+ s.add_dependency "foo", "= 0.0.1"
+ end
+ build_gem "foo", "0.0.1", to_bundle: true
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "deps"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+ end
+
+ it "does not break Gem.finish_resolve with conflicts" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_dependency "bar", "= 1.0.0"
+ end
+ update_repo2 do
+ build_gem "deps" do |s|
+ s.add_dependency "foo", "= 0.0.1"
+ end
+ build_gem "foo", "0.0.1"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "deps"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+
+ run "Gem.finish_resolve; puts 'WIN'"
+ expect(out).to eq("WIN")
+ end
+
+ it "does not make Gem.try_activate warn when local gem has extensions" do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_c_extension
+ end
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gemspec :path => '#{tmp("foo")}'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+
+ run "Gem.try_activate('irb/lc/es/error.rb'); puts 'WIN'"
+ expect(out).to eq("WIN")
+ expect(err).to be_empty
+ end
+
+ it "handles downgrades" do
+ build_lib "omg", "2.0", path: lib_path("omg")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => "#{lib_path("omg")}"
+ G
+
+ build_lib "omg", "1.0", path: lib_path("omg")
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "omg 1.0"
+ end
+
+ context "in deployment mode" do
+ context "when the lockfile was not updated after a change to the gemspec's dependencies" do
+ it "reports that installation failed" do
+ build_lib "cocoapods", path: bundled_app do |s|
+ s.add_dependency "activesupport", ">= 1"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gems("cocoapods 1.0", "activesupport 2.3.5")
+
+ build_lib "cocoapods", path: bundled_app do |s|
+ s.add_dependency "activesupport", ">= 1.0.1"
+ end
+
+ bundle_config "deployment true"
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("changed")
+ end
+ end
+ end
+
+ context "when child gemspecs conflict with a released gemspec" do
+ before do
+ # build the "parent" gem that depends on another gem in the same repo
+ build_lib "source_conflict", path: bundled_app do |s|
+ s.add_dependency "myrack_middleware"
+ end
+
+ # build the "child" gem that is the same version as a released gem, but
+ # has completely different and conflicting dependency requirements
+ build_lib "myrack_middleware", "1.0", path: bundled_app("myrack_middleware") do |s|
+ s.add_dependency "myrack", "1.0" # anything other than 0.9.1
+ end
+ end
+
+ it "should install the child gemspec's deps" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ context "with a lockfile and some missing dependencies" do
+ let(:source_uri) { "http://localgemserver.test" }
+
+ before do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.add_dependency "myrack", "=1.0.0"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gemspec :path => "../foo"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ lockfile <<-L
+ PATH
+ remote: ../foo
+ specs:
+ foo (1.0)
+ myrack (= 1.0.0)
+
+ GEM
+ remote: #{source_uri}
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ context "using JRuby with explicit platform", :jruby_only do
+ before do
+ create_file(
+ tmp("foo", "foo-java.gemspec"),
+ build_spec("foo", "1.0", "java") do
+ dep "myrack", "=1.0.0"
+ @spec.authors = "authors"
+ @spec.summary = "summary"
+ end.first.to_ruby
+ )
+ end
+
+ it "should install" do
+ results = bundle "install", artifice: "endpoint"
+ expect(results).to include("Installing myrack 1.0.0")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "should install", :jruby do
+ results = bundle "install", artifice: "endpoint"
+ expect(results).to include("Installing myrack 1.0.0")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ context "bundled for multiple platforms" do
+ let(:platform_specific_type) { :runtime }
+ let(:dependency) { "platform_specific" }
+ before do
+ build_repo2 do
+ build_gem "indirect_platform_specific" do |s|
+ s.add_runtime_dependency "platform_specific"
+ end
+ end
+
+ build_lib "foo", path: bundled_app do |s|
+ case platform_specific_type
+ when :runtime
+ s.add_runtime_dependency dependency
+ when :development
+ s.add_development_dependency dependency
+ else
+ raise ArgumentError, "wrong dependency type #{platform_specific_type}, can only be :development or :runtime"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gemspec
+ G
+
+ bundle_config "force_ruby_platform true"
+ bundle "install"
+
+ simulate_new_machine
+ simulate_platform("jruby") { bundle "install" }
+ expect(lockfile).to include("platform_specific (1.0-java)")
+ simulate_platform("x64-mingw-ucrt") { bundle "install" }
+ end
+
+ context "on ruby" do
+ before do
+ bundle_config "force_ruby_platform true"
+ bundle :install
+ end
+
+ context "as a runtime dependency" do
+ it "keeps all platform dependencies in the lockfile" do
+ expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0", "java"
+ c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+ platform_specific
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x64-mingw-ucrt)
+
+ PLATFORMS
+ java
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "as a development dependency" do
+ let(:platform_specific_type) { :development }
+
+ it "keeps all platform dependencies in the lockfile" do
+ expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 ruby"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0", "java"
+ c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x64-mingw-ucrt)
+
+ PLATFORMS
+ java
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ foo!
+ platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with an indirect platform-specific development dependency" do
+ let(:platform_specific_type) { :development }
+ let(:dependency) { "indirect_platform_specific" }
+
+ it "keeps all platform dependencies in the lockfile" do
+ expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 ruby"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo2, "indirect_platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0"
+ c.checksum gem_repo2, "platform_specific", "1.0", "java"
+ c.checksum gem_repo2, "platform_specific", "1.0", "x64-mingw-ucrt"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: .
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ indirect_platform_specific (1.0)
+ platform_specific
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x64-mingw-ucrt)
+
+ PLATFORMS
+ java
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ foo!
+ indirect_platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+ end
+ end
+
+ context "with multiple platforms" do
+ before do
+ build_lib("foo", path: tmp("foo")) do |s|
+ s.version = "1.0.0"
+ s.add_development_dependency "myrack"
+ s.write "foo-universal-java.gemspec", build_spec("foo", "1.0.0", "universal-java") {|sj| sj.runtime "myrack", "1.0.0" }.first.to_ruby
+ end
+ end
+
+ it "installs the ruby platform gemspec" do
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0.0", "myrack 1.0.0"
+ end
+
+ it "installs the ruby platform gemspec and skips dev deps with `without development` configured" do
+ bundle_config "force_ruby_platform true"
+
+ bundle_config "without development"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => '#{tmp("foo")}', :name => 'foo'
+ G
+
+ expect(the_bundle).to include_gem "foo 1.0.0"
+ expect(the_bundle).not_to include_gem "myrack"
+ end
+ end
+
+ context "with multiple platforms and resolving for more specific platforms" do
+ before do
+ build_lib("chef", path: tmp("chef")) do |s|
+ s.version = "17.1.17"
+ s.write "chef-universal-mingw-ucrt.gemspec", build_spec("chef", "17.1.17", "universal-mingw-ucrt") {|sw| sw.runtime "win32-api", "~> 1.5.3" }.first.to_ruby
+ end
+ end
+
+ it "does not remove the platform specific specs from the lockfile when updating" do
+ build_repo4 do
+ build_gem "win32-api", "1.5.3" do |s|
+ s.platform = "universal-mingw-ucrt"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gemspec :path => "../chef"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "chef", "17.1.17"
+ c.no_checksum "chef", "17.1.17", "universal-mingw-ucrt"
+ c.checksum gem_repo4, "win32-api", "1.5.3", "universal-mingw-ucrt"
+ end
+
+ initial_lockfile = <<~L
+ PATH
+ remote: ../chef
+ specs:
+ chef (17.1.17)
+ chef (17.1.17-universal-mingw-ucrt)
+ win32-api (~> 1.5.3)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ win32-api (1.5.3-universal-mingw-ucrt)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby", "x64-mingw-ucrt", "x86-mingw32")}
+
+ DEPENDENCIES
+ chef!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile initial_lockfile
+
+ bundle "update"
+
+ expect(lockfile).to eq initial_lockfile
+ end
+ end
+
+ context "with multiple locked platforms" do
+ before do
+ build_lib("activeadmin", path: tmp("activeadmin")) do |s|
+ s.version = "2.9.0"
+ s.add_dependency "railties", ">= 5.2", "< 6.2"
+ end
+
+ build_repo4 do
+ build_gem "railties", "6.1.4"
+
+ build_gem "jruby-openssl", "0.10.7" do |s|
+ s.platform = "java"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gemspec :path => "../activeadmin"
+ gem "jruby-openssl", :platform => :jruby
+ G
+
+ bundle "lock --add-platform java"
+ end
+
+ it "does not remove the platform specific specs from the lockfile when re-resolving due to gemspec changes" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "activeadmin", "2.9.0"
+ c.checksum gem_repo4, "jruby-openssl", "0.10.7", "java"
+ c.checksum gem_repo4, "railties", "6.1.4"
+ end
+
+ expect(lockfile).to eq <<~L
+ PATH
+ remote: ../activeadmin
+ specs:
+ activeadmin (2.9.0)
+ railties (>= 5.2, < 6.2)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ jruby-openssl (0.10.7-java)
+ railties (6.1.4)
+
+ PLATFORMS
+ #{lockfile_platforms("java")}
+
+ DEPENDENCIES
+ activeadmin!
+ jruby-openssl
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemspec = tmp("activeadmin/activeadmin.gemspec")
+ File.write(gemspec, File.read(gemspec).sub(">= 5.2", ">= 6.0"))
+
+ previous_lockfile = lockfile
+
+ bundle "install --local"
+
+ expect(lockfile).to eq(previous_lockfile.sub(">= 5.2", ">= 6.0"))
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb
new file mode 100644
index 0000000000..b2a82caf01
--- /dev/null
+++ b/spec/bundler/install/gemfile/git_spec.rb
@@ -0,0 +1,1745 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with git sources" do
+ describe "when floating on main" do
+ let(:base_gemfile) do
+ <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+ end
+
+ let(:install_base_gemfile) do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ end
+
+ install_gemfile base_gemfile
+ end
+
+ it "fetches gems" do
+ install_base_gemfile
+ expect(the_bundle).to include_gems("foo 1.0")
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not (yet?) enforce CHECKSUMS" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+
+ bundle_config "lockfile_checksums true"
+ gemfile base_gemfile
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{revision}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ CHECKSUMS
+ foo (1.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle_config "frozen true"
+
+ bundle "install"
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "caches the git repo" do
+ install_base_gemfile
+ expect(Dir["#{default_cache_path}/git/foo-1.0-*"]).to have_attributes size: 1
+ end
+
+ it "does not write to cache on bundler/setup" do
+ install_base_gemfile
+ FileUtils.rm_r(default_cache_path)
+ ruby "require 'bundler/setup'"
+ expect(default_cache_path).not_to exist
+ end
+
+ it "caches the git repo globally and properly uses the cached repo on the next invocation" do
+ install_base_gemfile
+ pristine_system_gems
+ bundle_config "global_gem_cache true"
+ bundle :install
+ expect(Dir["#{home}/.bundle/cache/git/foo-1.0-*"]).to have_attributes size: 1
+
+ bundle "install --verbose"
+ expect(err).to be_empty
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")}")
+ end
+
+ it "caches the evaluated gemspec" do
+ install_base_gemfile
+ git = update_git "foo" do |s|
+ s.executables = ["foobar"] # we added this the first time, so keep it now
+ s.files = ["bin/foobar"] # updating git nukes the files list
+ foospec = s.to_ruby.gsub(/s\.files.*/, 's.files = `git ls-files -z`.split("\x0")')
+ s.write "foo.gemspec", foospec
+ end
+
+ bundle "update foo"
+
+ sha = git.ref_for("main", 11)
+ spec_file = default_bundle_path("bundler/gems/foo-1.0-#{sha}/foo.gemspec")
+ expect(spec_file).to exist
+ ruby_code = Gem::Specification.load(spec_file.to_s).to_ruby
+ file_code = File.read(spec_file)
+ expect(file_code).to eq(ruby_code)
+ end
+
+ it "does not update the git source implicitly" do
+ install_base_gemfile
+ update_git "foo"
+
+ install_gemfile bundled_app2("Gemfile"), <<-G, dir: bundled_app2
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ run <<-RUBY
+ require 'foo'
+ puts "fail" if defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to be_empty
+ end
+
+ it "sets up git gem executables on the path" do
+ install_base_gemfile
+ bundle "exec foobar"
+ expect(out).to eq("1.0")
+ end
+
+ it "complains if pinned specs don't exist in the git repo" do
+ build_git "foo"
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", "1.1", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(err).to include("The source contains the following gems matching 'foo':\n * foo-1.0")
+ end
+
+ it "complains with version and platform if pinned specs don't exist in the git repo", :jruby_only do
+ build_git "only_java" do |s|
+ s.platform = "java"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ platforms :jruby do
+ gem "only_java", "1.2", :git => "#{lib_path("only_java-1.0-java")}"
+ end
+ G
+
+ expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java")
+ end
+
+ it "complains with multiple versions and platforms if pinned specs don't exist in the git repo", :jruby_only do
+ build_git "only_java", "1.0" do |s|
+ s.platform = "java"
+ end
+
+ build_git "only_java", "1.1" do |s|
+ s.platform = "java"
+ s.write "only_java1-0.gemspec", File.read("#{lib_path("only_java-1.0-java")}/only_java.gemspec")
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ platforms :jruby do
+ gem "only_java", "1.2", :git => "#{lib_path("only_java-1.1-java")}"
+ end
+ G
+
+ expect(err).to include("The source contains the following gems matching 'only_java':\n * only_java-1.0-java\n * only_java-1.1-java")
+ end
+
+ it "still works after moving the application directory" do
+ bundle_config "path vendor/bundle"
+ install_base_gemfile
+
+ FileUtils.mv bundled_app, tmp("bundled_app.bck")
+
+ expect(the_bundle).to include_gems "foo 1.0", dir: tmp("bundled_app.bck")
+ end
+
+ it "can still install after moving the application directory" do
+ bundle_config "path vendor/bundle"
+ install_base_gemfile
+
+ FileUtils.mv bundled_app, tmp("bundled_app.bck")
+
+ update_git "foo", "1.1", path: lib_path("foo-1.0")
+
+ gemfile tmp("bundled_app.bck/Gemfile"), <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+
+ gem "myrack", "1.0"
+ G
+
+ bundle "update foo", dir: tmp("bundled_app.bck")
+
+ expect(the_bundle).to include_gems "foo 1.1", "myrack 1.0", dir: tmp("bundled_app.bck")
+ end
+ end
+
+ describe "with an empty git block" do
+ before do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ git "#{lib_path("foo-1.0")}" do
+ # this page left intentionally blank
+ end
+ G
+ end
+
+ it "does not explode" do
+ bundle "install"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when specifying a revision" do
+ before(:each) do
+ build_git "foo"
+ @revision = revision_for(lib_path("foo-1.0"))
+ update_git "foo"
+ end
+
+ it "works" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "#{@revision}" do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "works when the revision is a symbol" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => #{@revision.to_sym.inspect} do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "works when an abbreviated revision is added after an initial, potentially shallow clone" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => #{@revision[0..7].inspect} do
+ gem "foo"
+ end
+ G
+ end
+
+ it "works when a tag that does not look like a commit hash is used as the value of :ref" do
+ build_git "foo"
+ @remote = build_git("bar", bare: true)
+ update_git "foo", remote: @remote.path
+ update_git "foo", push: "main"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}"
+ G
+
+ # Create a new tag on the remote that needs fetching
+ update_git "foo", tag: "v1.0.0"
+ update_git "foo", push: "v1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}", :ref => "v1.0.0"
+ G
+
+ expect(err).to be_empty
+ end
+
+ it "works when the revision is a non-head ref" do
+ # want to ensure we don't fallback to main
+ update_git "foo", path: lib_path("foo-1.0") do |s|
+ s.write("lib/foo.rb", "raise 'FAIL'")
+ end
+
+ git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0"))
+
+ # want to ensure we don't fallback to HEAD
+ update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s|
+ s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if defined?(FOO)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "works when the revision is a non-head ref and it was previously downloaded" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ # want to ensure we don't fallback to main
+ update_git "foo", path: lib_path("foo-1.0") do |s|
+ s.write("lib/foo.rb", "raise 'FAIL'")
+ end
+
+ git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0"))
+
+ # want to ensure we don't fallback to HEAD
+ update_git "foo", path: lib_path("foo-1.0"), branch: "rando" do |s|
+ s.write("lib/foo.rb", "raise 'FAIL_FROM_RANDO'")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "refs/bundler/1" do
+ gem "foo"
+ end
+ G
+ expect(err).to be_empty
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if defined?(FOO)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not download random non-head refs" do
+ git("update-ref -m \"Bundler Spec!\" refs/bundler/1 main~1", lib_path("foo-1.0"))
+
+ bundle_config "global_gem_cache true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ # ensure we also git fetch after cloning
+ bundle :update, all: true
+
+ git("ls-remote .", Dir[home(".bundle/cache/git/foo-*")].first)
+
+ expect(out).not_to include("refs/bundler/1")
+ end
+ end
+
+ describe "when specifying a branch" do
+ let(:branch) { "branch" }
+ let(:repo) { build_git("foo").path }
+
+ it "works" do
+ update_git("foo", path: repo, branch: branch)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :branch => #{branch.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ context "when the branch starts with a `#`" do
+ let(:branch) { "#149/redirect-url-fragment" }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, branch: branch)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :branch => #{branch.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+
+ context "when the branch includes quotes" do
+ let(:branch) { %('") }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, branch: branch)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :branch => #{branch.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+ end
+
+ describe "when specifying a tag" do
+ let(:tag) { "tag" }
+ let(:repo) { build_git("foo").path }
+
+ it "works" do
+ update_git("foo", path: repo, tag: tag)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :tag => #{tag.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ context "when the tag starts with a `#`" do
+ let(:tag) { "#149/redirect-url-fragment" }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, tag: tag)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :tag => #{tag.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+
+ context "when the tag includes quotes" do
+ let(:tag) { %('") }
+ it "works" do
+ skip "git does not accept this" if Gem.win_platform?
+
+ update_git("foo", path: repo, tag: tag)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{repo}", :tag => #{tag.dump} do
+ gem "foo"
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+ end
+ end
+
+ describe "when specifying local override" do
+ it "uses the local repository instead of checking a new one out" do
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ run "require 'myrack'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "chooses the local repository on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ update_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ run "require 'myrack'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "unlocks the source when the dependencies have changed while switching to the local" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ update_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "myrack.gemspec", build_spec("myrack", "0.8") { runtime "rspec", "> 0" }.first.to_ruby
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+ run "require 'myrack'"
+ expect(out).to eq("LOCAL")
+ end
+
+ it "updates specs on runtime" do
+ system_gems "nokogiri-1.4.2"
+
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ lockfile0 = File.read(bundled_app_lock)
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+ update_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.add_dependency "nokogiri", "1.4.2"
+ end
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ run "require 'myrack'"
+
+ lockfile1 = File.read(bundled_app_lock)
+ expect(lockfile1).not_to eq(lockfile0)
+ end
+
+ it "updates ref on install" do
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ lockfile0 = File.read(bundled_app_lock)
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+ update_git "myrack", "0.8", path: lib_path("local-myrack")
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ lockfile1 = File.read(bundled_app_lock)
+ expect(lockfile1).not_to eq(lockfile0)
+ end
+
+ it "explodes and gives correct solution if given path does not exist on install" do
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/)
+
+ solution = "config unset local.myrack"
+ expect(err).to match(/Run `bundle #{solution}` to remove the local override/)
+
+ bundle solution
+ bundle :install
+
+ expect(err).to be_empty
+ end
+
+ it "explodes and gives correct solution if branch is not given on install" do
+ build_git "myrack", "0.8"
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/Cannot use local override for myrack-0.8 at #{Regexp.escape(lib_path("local-myrack").to_s)} because :branch is not specified in Gemfile/)
+
+ solution = "config unset local.myrack"
+ expect(err).to match(/Specify a branch or run `bundle #{solution}` to remove the local override/)
+
+ bundle solution
+ bundle :install
+
+ expect(err).to be_empty
+ end
+
+ it "does not explode if disable_local_branch_check is given" do
+ build_git "myrack", "0.8"
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle %(config set disable_local_branch_check true)
+ bundle :install
+ expect(out).to match(/Bundle complete!/)
+ end
+
+ it "explodes on different branches on install" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ update_git "myrack", "0.8", path: lib_path("local-myrack"), branch: "another" do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/is using branch another but Gemfile specifies main/)
+ end
+
+ it "explodes on invalid revision on install" do
+ build_git "myrack", "0.8"
+
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install, raise_on_error: false
+ expect(err).to match(/The Gemfile lock is pointing to revision \w+/)
+ end
+
+ it "does not explode on invalid revision on install" do
+ build_git "myrack", "0.8"
+
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle %(config set disable_local_revision_check true)
+ bundle :install
+ expect(out).to match(/Bundle complete!/)
+ end
+ end
+
+ describe "specified inline" do
+ # TODO: Figure out how to write this test so that it is not flaky depending
+ # on the current network situation.
+ # it "supports private git URLs" do
+ # gemfile <<-G
+ # gem "thingy", :git => "git@notthere.fallingsnow.net:somebody/thingy.git"
+ # G
+ #
+ # bundle :install
+ #
+ # # p out
+ # # p err
+ # puts err unless err.empty? # This spec fails randomly every so often
+ # err.should include("notthere.fallingsnow.net")
+ # err.should include("ssh")
+ # end
+
+ it "installs from git even if a newer gem is available elsewhere" do
+ build_git "myrack", "0.8"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.8"
+ end
+
+ it "installs dependencies from git even if a newer gem is available elsewhere" do
+ system_gems "myrack-1.0.0"
+
+ build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s|
+ s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'"
+ end
+
+ build_git "foo", path: lib_path("nested") do |s|
+ s.add_dependency "myrack", "= 1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("nested")}"
+ G
+
+ run "require 'myrack'"
+ expect(out).to eq("WIN OVERRIDE")
+ end
+
+ it "correctly unlocks when changing to a git source" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ build_git "myrack", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0", :git => "#{lib_path("myrack")}"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "correctly unlocks when changing to a git source without versions" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ build_git "myrack", "1.2", path: lib_path("myrack")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack")}"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+ end
+
+ describe "block syntax" do
+ it "pulls all gems from a git block" do
+ build_lib "omg", path: lib_path("hi2u/omg")
+ build_lib "hi2u", path: lib_path("hi2u")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path("hi2u")}" do
+ gem "omg"
+ gem "hi2u"
+ end
+ G
+
+ expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0"
+ end
+ end
+
+ it "uses a ref if specified" do
+ build_git "foo"
+ @revision = revision_for(lib_path("foo-1.0"))
+ update_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{@revision}"
+ G
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "correctly handles cases with invalid gemspecs" do
+ build_git "foo" do |s|
+ s.summary = nil
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ gem "rails", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ expect(the_bundle).to include_gems "rails 2.3.2"
+ end
+
+ it "runs the gemspec in the context of its parent directory" do
+ build_lib "bar", path: lib_path("foo/bar"), gemspec: false do |s|
+ s.write lib_path("foo/bar/lib/version.rb"), %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ $:.unshift Dir.pwd
+ require 'lib/version'
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.author = 'no one'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ end
+ G
+ end
+
+ build_git "foo", path: lib_path("foo") do |s|
+ s.write "bin/foo", ""
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("foo")}"
+ gem "rails", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0"
+ expect(the_bundle).to include_gems "rails 2.3.2"
+ end
+
+ it "runs the gemspec in the context of its parent directory, when using local overrides" do
+ build_git "foo", path: lib_path("foo"), gemspec: false do |s|
+ s.write lib_path("foo/lib/foo/version.rb"), %(FOO_VERSION = '1.0')
+ s.write "foo.gemspec", <<-G
+ $:.unshift Dir.pwd
+ require 'lib/foo/version'
+ Gem::Specification.new do |s|
+ s.name = 'foo'
+ s.author = 'no one'
+ s.version = FOO_VERSION
+ s.summary = 'Foo'
+ s.files = Dir["lib/**/*.rb"]
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "https://github.com/gems/foo", branch: "main"
+ G
+
+ bundle %(config set local.foo #{lib_path("foo")})
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "installs from git even if a rubygems gem is present" do
+ build_gem "foo", "1.0", path: lib_path("fake_foo"), to_system: true do |s|
+ s.write "lib/foo.rb", "raise 'FAIL'"
+ end
+
+ build_git "foo", "1.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "fakes the gem out if there is no gemspec" do
+ build_git "foo", gemspec: false
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo-1.0")}"
+ gem "rails", "2.3.2"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "catches git errors and spits out useful output" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "omgomg"
+ G
+
+ bundle :install, raise_on_error: false
+
+ expect(err).to include("Git error:")
+ expect(err).to include("fatal")
+ expect(err).to include("omgomg")
+ end
+
+ it "works when the gem path has spaces in it" do
+ build_git "foo", path: lib_path("foo space-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo space-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "handles repos that have been force-pushed" do
+ build_git "forced", "1.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("forced-1.0")}" do
+ gem 'forced'
+ end
+ G
+ expect(the_bundle).to include_gems "forced 1.0"
+
+ update_git "forced" do |s|
+ s.write "lib/forced.rb", "FORCED = '1.1'"
+ end
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "forced 1.1"
+
+ git("reset --hard HEAD^", lib_path("forced-1.0"))
+
+ bundle "update", all: true
+ expect(the_bundle).to include_gems "forced 1.0"
+ end
+
+ it "ignores submodules if :submodule is not passed" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+ build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+ expect(err).to match(%r{submodule >= 0 could not be found in rubygems repository https://gem.repo1/ or installed locally})
+
+ expect(the_bundle).not_to include_gems "has_submodule 1.0"
+ end
+
+ it "handles repos with submodules" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+ build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ expect(the_bundle).to include_gems "has_submodule 1.0"
+ end
+
+ it "does not warn when deiniting submodules" do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_git "submodule", "1.0"
+ build_git "has_submodule", "1.0"
+
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+ expect(err).to be_empty
+
+ expect(the_bundle).to include_gems "has_submodule 1.0"
+ expect(the_bundle).to_not include_gems "submodule 1.0"
+ end
+
+ it "handles implicit updates when modifying the source info" do
+ git = build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ update_git "foo"
+ update_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :ref => "#{git.ref_for("HEAD^")}" do
+ gem "foo"
+ end
+ G
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if FOO_PREV_REF == '#{git.ref_for("HEAD^^")}'
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not do a remote fetch if the revision is cached locally" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ FileUtils.rm_r(lib_path("foo-1.0"))
+
+ bundle "install"
+ expect(out).not_to match(/updating/i)
+ end
+
+ it "doesn't blow up if bundle install is run twice in a row" do
+ build_git "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "install"
+ bundle "install"
+ end
+
+ it "prints a friendly error if a file blocks the git repo" do
+ build_git "foo"
+
+ FileUtils.mkdir_p(default_bundle_path)
+ FileUtils.touch(default_bundle_path("bundler"))
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(last_command).to be_failure
+ expect(err).to include("Bundler could not install a gem because it " \
+ "needs to create a directory, but a file exists " \
+ "- #{default_bundle_path("bundler")}")
+ end
+
+ it "does not duplicate git gem sources" do
+ build_lib "foo", path: lib_path("nested/foo")
+ build_lib "bar", path: lib_path("nested/bar")
+
+ build_git "foo", path: lib_path("nested")
+ build_git "bar", path: lib_path("nested")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("nested")}"
+ gem "bar", :git => "#{lib_path("nested")}"
+ G
+
+ expect(File.read(bundled_app_lock).scan("GIT").size).to eq(1)
+ end
+
+ describe "switching sources" do
+ it "doesn't explode when switching Path to Git sources" do
+ build_gem "foo", "1.0", to_system: true do |s|
+ s.write "lib/foo.rb", "raise 'fail'"
+ end
+ build_lib "foo", "1.0", path: lib_path("bar/foo")
+ build_git "bar", "1.0", path: lib_path("bar") do |s|
+ s.add_dependency "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :path => "#{lib_path("bar")}"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0", "bar 1.0"
+ end
+
+ it "doesn't explode when switching Gem to Git source" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack-obama"
+ gem "myrack", "1.0.0"
+ G
+
+ build_git "myrack", "1.0" do |s|
+ s.write "lib/new_file.rb", "puts 'USING GIT'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack-obama"
+ gem "myrack", "1.0.0", :git => "#{lib_path("myrack-1.0")}"
+ G
+
+ run "require 'new_file'"
+ expect(out).to eq("USING GIT")
+ end
+
+ it "doesn't explode when removing an explicit exact version from a git gem with dependencies" do
+ build_lib "activesupport", "7.1.4", path: lib_path("rails/activesupport")
+ build_git "rails", "7.1.4", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 7.1.4"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", "7.1.4", :git => "#{lib_path("rails")}"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_path("rails")}"
+ G
+
+ expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4"
+ end
+
+ it "doesn't explode when adding an explicit ref to a git gem with dependencies" do
+ lib_root = lib_path("rails")
+
+ build_lib "activesupport", "7.1.4", path: lib_root.join("activesupport")
+ build_git "rails", "7.1.4", path: lib_root do |s|
+ s.add_dependency "activesupport", "= 7.1.4"
+ end
+
+ old_revision = revision_for(lib_root)
+ update_git "rails", "7.1.4", path: lib_root
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", "7.1.4", :git => "#{lib_root}"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_root}", :ref => "#{old_revision}"
+ G
+
+ expect(the_bundle).to include_gem "rails 7.1.4", "activesupport 7.1.4"
+ end
+ end
+
+ describe "bundle install after the remote has been updated" do
+ it "installs" do
+ build_git "valim"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "valim", :git => "#{lib_path("valim-1.0")}"
+ G
+
+ old_revision = revision_for(lib_path("valim-1.0"))
+ update_git "valim"
+ new_revision = revision_for(lib_path("valim-1.0"))
+
+ old_lockfile = File.read(bundled_app_lock)
+ lockfile(bundled_app_lock, old_lockfile.gsub(/revision: #{old_revision}/, "revision: #{new_revision}"))
+
+ bundle "install"
+
+ run <<-R
+ require "valim"
+ puts VALIM_PREV_REF
+ R
+
+ expect(out).to eq(old_revision)
+ end
+
+ it "gives a helpful error message when the remote ref no longer exists" do
+ build_git "foo"
+ revision = revision_for(lib_path("foo-1.0"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{revision}"
+ G
+ expect(out).to_not match(/Revision.*does not exist/)
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "deadbeef"
+ G
+ expect(err).to include("Revision deadbeef does not exist in the repository")
+ end
+
+ it "gives a helpful error message when the remote branch no longer exists" do
+ build_git "foo"
+
+ install_gemfile <<-G, env: { "LANG" => "en" }, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "deadbeef"
+ G
+
+ expect(err).to include("Revision deadbeef does not exist in the repository")
+ end
+ end
+
+ describe "bundle install with deployment mode configured and git sources" do
+ it "works" do
+ build_git "valim", path: lib_path("valim")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "valim", "= 1.0", :git => "#{lib_path("valim")}"
+ G
+
+ pristine_system_gems
+
+ bundle_config "deployment true"
+ bundle :install
+ end
+ end
+
+ describe "gem install hooks" do
+ it "runs pre-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0")
+ end
+
+ it "runs post-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.post_install_hooks << lambda do |inst|
+ STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0")
+ end
+
+ it "complains if the install hook fails" do
+ build_git "foo"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ false
+ end
+ H
+ end
+
+ bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false
+ expect(err).to include("failed for foo-1.0")
+ end
+ end
+
+ context "with an extension" do
+ it "installs the extension" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).to eq("YES")
+
+ run <<-R
+ puts $:.grep(/ext/)
+ R
+ expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s)
+ end
+
+ it "does not use old extension after ref changes" do
+ git_reader = build_git "foo", no_default: true do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-RUBY
+ require "mkmf"
+ create_makefile("foo")
+ RUBY
+ s.write "ext/foo.c", "void Init_foo() {}"
+ end
+
+ 2.times do |i|
+ File.open(git_reader.path.join("ext/foo.c"), "w") do |file|
+ file.write <<-C
+ #include "ruby.h"
+ VALUE foo(VALUE self) { return INT2FIX(#{i}); }
+ void Init_foo() { rb_define_global_function("foo", &foo, 0); }
+ C
+ end
+ git("commit -m \"commit for iteration #{i}\" ext/foo.c", git_reader.path)
+
+ git_commit_sha = git_reader.ref_for("HEAD")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :ref => "#{git_commit_sha}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts foo
+ R
+
+ expect(out).to eq(i.to_s)
+ end
+ end
+
+ it "does not prompt to gem install if extension fails" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ raise
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(err).to end_with(<<-M.strip)
+An error occurred while installing foo (1.0), and Bundler cannot continue.
+
+In Gemfile:
+ foo
+ M
+ expect(out).not_to include("gem install foo")
+ end
+
+ it "does not reinstall the extension" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ cur_time = Time.now.to_f.to_s
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = \#{cur_time}"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+
+ installed_time = out
+ expect(installed_time).to match(/\A\d+\.\d+\z/)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).to eq(installed_time)
+ end
+
+ it "does not reinstall the extension when changing another gem" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ cur_time = Time.now.to_f.to_s
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = \#{cur_time}"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+
+ installed_time = out
+ expect(installed_time).to match(/\A\d+\.\d+\z/)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).to eq(installed_time)
+ end
+
+ it "does reinstall the extension when changing refs" do
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ cur_time = Time.now.to_f.to_s
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = \#{cur_time}"
+ end
+ end
+ RUBY
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+
+ installed_time = out
+
+ update_git("foo", branch: "branch2")
+
+ expect(installed_time).to match(/\A\d+\.\d+\z/)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "branch2"
+ G
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).not_to eq(installed_time)
+
+ installed_time = out
+
+ update_git("foo")
+ bundle "update foo"
+
+ run <<-R
+ require 'foo'
+ puts FOO
+ R
+ expect(out).not_to eq(installed_time)
+ end
+ end
+
+ it "ignores git environment variables" do
+ build_git "xxxxxx" do |s|
+ s.executables = "xxxxxxbar"
+ end
+
+ Bundler::SharedHelpers.with_clean_git_env do
+ ENV["GIT_DIR"] = "bar"
+ ENV["GIT_WORK_TREE"] = "bar"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("xxxxxx-1.0")}" do
+ gem 'xxxxxx'
+ end
+ G
+
+ expect(ENV["GIT_DIR"]).to eq("bar")
+ expect(ENV["GIT_WORK_TREE"]).to eq("bar")
+ end
+ end
+
+ describe "without git installed" do
+ it "prints a better error message when installing" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rake", git: "https://github.com/ruby/rake"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: https://github.com/ruby/rake
+ revision: 5c60da8644a9e4f655e819252e3b6ca77f42b7af
+ specs:
+ rake (13.0.6)
+
+ GEM
+ remote: https://rubygems.org/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rake!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ with_path_as("") do
+ bundle "install", raise_on_error: false
+ end
+ expect(err).
+ to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git")
+ end
+
+ it "prints a better error message when updating" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ with_path_as("") do
+ bundle "update", all: true, raise_on_error: false
+ end
+ expect(err).
+ to include("You need to install git to be able to use gems from git repositories. For help installing git, please refer to GitHub's tutorial at https://help.github.com/articles/set-up-git")
+ end
+
+ it "doesn't need git in the new machine if an installed git gem is copied to another machine" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+ bundle_config_global "path vendor/bundle"
+ bundle :install
+ pristine_system_gems
+
+ bundle "install", env: { "PATH" => "" }
+ expect(out).to_not include("You need to install git to be able to use gems from git repositories.")
+ end
+ end
+
+ describe "when the git source is overridden with a local git repo" do
+ before do
+ bundle_config_global "local.foo #{lib_path("foo")}"
+ end
+
+ describe "and git output is colorized" do
+ before do
+ File.open("#{ENV["HOME"]}/.gitconfig", "w") do |f|
+ f.write("[color]\n\tui = always\n")
+ end
+ end
+
+ it "installs successfully" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}", :branch => "main"
+ G
+
+ bundle :install
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+ end
+
+ context "git sources that include credentials" do
+ context "that are username and password" do
+ let(:credentials) { "user1:password1" }
+
+ it "does not display the password" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ git "https://#{credentials}@github.com/company/private-repo" do
+ gem "foo"
+ end
+ G
+
+ expect(stdboth).to_not include("password1")
+ expect(out).to include("Fetching https://user1@github.com/company/private-repo")
+ end
+ end
+
+ context "that is an oauth token" do
+ let(:credentials) { "oauth_token" }
+
+ it "displays the oauth scheme but not the oauth token" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ git "https://#{credentials}:x-oauth-basic@github.com/company/private-repo" do
+ gem "foo"
+ end
+ G
+
+ expect(stdboth).to_not include("oauth_token")
+ expect(out).to include("Fetching https://x-oauth-basic@github.com/company/private-repo")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/groups_spec.rb b/spec/bundler/install/gemfile/groups_spec.rb
new file mode 100644
index 0000000000..4013b112ec
--- /dev/null
+++ b/spec/bundler/install/gemfile/groups_spec.rb
@@ -0,0 +1,345 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with groups" do
+ describe "installing with no options" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo do
+ gem "activesupport", "2.3.5"
+ end
+ gem "thin", :groups => [:emo]
+ G
+ end
+
+ it "installs gems in the default group" do
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems in a group block into that group" do
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+
+ load_error_run <<-R, "activesupport", :default
+ require 'activesupport'
+ puts ACTIVESUPPORT
+ R
+
+ expect(err_without_deprecations).to match(/cannot load such file -- activesupport/)
+ end
+
+ it "installs gems with inline :groups into those groups" do
+ expect(the_bundle).to include_gems "thin 1.0"
+
+ load_error_run <<-R, "thin", :default
+ require 'thin'
+ puts THIN
+ R
+
+ expect(err_without_deprecations).to match(/cannot load such file -- thin/)
+ end
+
+ it "sets up everything if Bundler.setup is used with no groups" do
+ output = run("require 'myrack'; puts MYRACK")
+ expect(output).to eq("1.0.0")
+
+ output = run("require 'activesupport'; puts ACTIVESUPPORT")
+ expect(output).to eq("2.3.5")
+
+ output = run("require 'thin'; puts THIN")
+ expect(output).to eq("1.0")
+ end
+
+ it "removes old groups when new groups are set up" do
+ load_error_run <<-RUBY, "thin", :emo
+ Bundler.setup(:default)
+ require 'thin'
+ puts THIN
+ RUBY
+
+ expect(err_without_deprecations).to match(/cannot load such file -- thin/)
+ end
+
+ it "sets up old groups when they have previously been removed" do
+ output = run <<-RUBY, :emo
+ Bundler.setup(:default)
+ Bundler.setup(:default, :emo)
+ require 'thin'; puts THIN
+ RUBY
+ expect(output).to eq("1.0")
+ end
+ end
+
+ describe "without option" do
+ describe "with gems assigned to a single group" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo do
+ gem "activesupport", "2.3.5"
+ end
+ group :debugging, :optional => true do
+ gem "thin"
+ end
+ G
+ end
+
+ it "installs gems in the default group" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default]
+ end
+
+ it "respects global `without` configuration, but does not save it locally" do
+ bundle_config_global "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default]
+ bundle "config list"
+ expect(out).not_to include("Set for your local app (#{bundled_app(".bundle/config")}): [:emo]")
+ expect(out).to include("Set for the current user (#{home(".bundle/config")}): [:emo]")
+ end
+
+ it "allows running application where groups where configured by a different user" do
+ bundle_config "without emo"
+ bundle :install
+ bundle "exec ruby -e 'puts 42'", env: { "BUNDLE_USER_HOME" => tmp("new_home").to_s }
+ expect(out).to include("42")
+ end
+
+ it "does not install gems from the excluded group" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default]
+ end
+
+ it "does not say it installed gems from the excluded group" do
+ bundle_config "without emo"
+ bundle :install
+ expect(out).not_to include("activesupport")
+ end
+
+ it "allows Bundler.setup for specific groups" do
+ bundle_config "without emo"
+ bundle :install
+ run("require 'myrack'; puts MYRACK", :default)
+ expect(out).to eq("1.0.0")
+ end
+
+ it "does not effect the resolve" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ group :emo do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.2", groups: [:default]
+ end
+
+ it "still works when BUNDLE_WITHOUT is set" do
+ ENV["BUNDLE_WITHOUT"] = "emo"
+
+ bundle :install
+ expect(out).not_to include("activesupport")
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: [:default]
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5", groups: [:default]
+
+ ENV["BUNDLE_WITHOUT"] = nil
+ end
+
+ it "does not install gems from the optional group" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "thin 1.0"
+ end
+
+ it "installs gems from the optional group when requested" do
+ bundle_config "with debugging"
+ bundle :install
+ expect(the_bundle).to include_gems "thin 1.0"
+ end
+
+ it "installs gems from the optional groups requested with BUNDLE_WITH" do
+ ENV["BUNDLE_WITH"] = "debugging"
+ bundle :install
+ expect(the_bundle).to include_gems "thin 1.0"
+ ENV["BUNDLE_WITH"] = nil
+ end
+
+ it "allows the BUNDLE_WITH setting to override BUNDLE_WITHOUT" do
+ ENV["BUNDLE_WITH"] = "debugging"
+
+ bundle :install
+ expect(the_bundle).to include_gem "thin 1.0"
+
+ ENV["BUNDLE_WITHOUT"] = "debugging"
+ expect(the_bundle).to include_gem "thin 1.0"
+
+ bundle :install
+ expect(the_bundle).to include_gem "thin 1.0"
+ end
+
+ it "has no effect when listing a not optional group in with" do
+ bundle_config "with emo"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ end
+
+ it "has no effect when listing an optional group in without" do
+ bundle_config "without debugging"
+ bundle :install
+ expect(the_bundle).not_to include_gems "thin 1.0"
+ end
+ end
+
+ describe "with gems assigned to multiple groups" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo, :lolercoaster do
+ gem "activesupport", "2.3.5"
+ end
+ G
+ end
+
+ it "installs gems in the default group" do
+ bundle_config "without emo lolercoaster"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the gem if any of its groups are installed" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5"
+ end
+
+ describe "with a gem defined multiple times in different groups" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+
+ group :emo do
+ gem "activesupport", "2.3.5"
+ end
+
+ group :lolercoaster do
+ gem "activesupport", "2.3.5"
+ end
+ G
+ end
+
+ it "installs the gem unless all groups are excluded" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+
+ bundle_config "without lolercoaster"
+ bundle :install
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+
+ bundle_config "without emo lolercoaster"
+ bundle :install
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+
+ bundle "config set --local without 'emo lolercoaster'"
+ bundle :install
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+ end
+ end
+ end
+
+ describe "nesting groups" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ group :emo do
+ group :lolercoaster do
+ gem "activesupport", "2.3.5"
+ end
+ end
+ G
+ end
+
+ it "installs gems in the default group" do
+ bundle_config "without emo lolercoaster"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the gem if any of its groups are installed" do
+ bundle_config "without emo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5"
+ end
+ end
+ end
+
+ describe "when loading only the default group" do
+ it "should not load all groups" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "activesupport", :groups => :development
+ G
+
+ ruby <<-R
+ require "bundler"
+ Bundler.setup :default
+ Bundler.require :default
+ puts MYRACK
+ begin
+ require "activesupport"
+ rescue LoadError
+ puts "no activesupport"
+ end
+ R
+
+ expect(out).to include("1.0")
+ expect(out).to include("no activesupport")
+ end
+ end
+
+ describe "when locked and installed with `without` setting" do
+ before(:each) do
+ build_repo2
+
+ system_gems "myrack-0.9.1"
+
+ bundle_config "without myrack"
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+
+ group :myrack do
+ gem "myrack_middleware"
+ end
+ G
+ end
+
+ it "uses versions from excluded gems in a machine without the without configuration" do
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ simulate_new_machine
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ expect(the_bundle).to include_gems "myrack_middleware 1.0"
+ end
+
+ it "does not hit the remote a second time" do
+ FileUtils.rm_r gem_repo2
+ bundle_config "without myrack"
+ bundle :install, verbose: true
+ expect(stdboth).not_to match(/fetching/i)
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/install_if_spec.rb b/spec/bundler/install/gemfile/install_if_spec.rb
new file mode 100644
index 0000000000..05a6d15129
--- /dev/null
+++ b/spec/bundler/install/gemfile/install_if_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with install_if conditionals" do
+ it "follows the install_if DSL" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ install_if(lambda { true }) do
+ gem "activesupport", "2.3.5"
+ end
+ gem "thin", :install_if => false
+ install_if(lambda { false }) do
+ gem "foo"
+ end
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems("myrack 1.0", "activesupport 2.3.5")
+ expect(the_bundle).not_to include_gems("thin")
+ expect(the_bundle).not_to include_gems("foo")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "activesupport", "2.3.5"
+ c.checksum gem_repo1, "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ c.checksum gem_repo1, "thin", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ activesupport (2.3.5)
+ foo (1.0)
+ myrack (1.0.0)
+ thin (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport (= 2.3.5)
+ foo
+ myrack
+ thin
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+end
diff --git a/spec/bundler/install/gemfile/lockfile_spec.rb b/spec/bundler/install/gemfile/lockfile_spec.rb
new file mode 100644
index 0000000000..19bd7074b2
--- /dev/null
+++ b/spec/bundler/install/gemfile/lockfile_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with a lockfile present" do
+ let(:gf) { <<-G }
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+
+ it "touches the lockfile on install even when nothing has changed" do
+ install_gemfile(gf)
+ expect { bundle :install }.to change { bundled_app_lock.mtime }
+ end
+
+ context "gemfile evaluation" do
+ let(:gf) { super() + "\n\n File.open('evals', 'a') {|f| f << %(1\n) } unless ENV['BUNDLER_SPEC_NO_APPEND']" }
+
+ context "with plugins disabled" do
+ before do
+ bundle_config "plugins false"
+ end
+
+ it "does not evaluate the gemfile twice when the gem is already installed" do
+ install_gemfile(gf)
+ bundle :install
+
+ with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" }
+
+ expect(bundled_app("evals").read.lines.to_a.size).to eq(2)
+ end
+
+ it "does not evaluate the gemfile twice when the gem is not installed" do
+ gemfile(gf)
+ bundle :install
+
+ with_env_vars("BUNDLER_SPEC_NO_APPEND" => "1") { expect(the_bundle).to include_gem "myrack 1.0.0" }
+
+ expect(bundled_app("evals").read.lines.to_a.size).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/override_spec.rb b/spec/bundler/install/gemfile/override_spec.rb
new file mode 100644
index 0000000000..02b0e7d772
--- /dev/null
+++ b/spec/bundler/install/gemfile/override_spec.rb
@@ -0,0 +1,401 @@
+# frozen_string_literal: true
+
+RSpec.describe "override DSL" do
+ context "with a version: string operation" do
+ it "replaces a direct dependency requirement with the override version spec" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "replaces a transitive dependency requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 1.0.0"
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0"
+ end
+
+ it "replaces the requirement even when the Gemfile pins a different version" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack", "= 1.0.0"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "applies the override against an existing lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack"
+ G
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "pins a prerelease version that the Gemfile dependency would otherwise filter out" do
+ build_repo2 do
+ build_gem "has_prerelease", "1.0"
+ build_gem "has_prerelease", "1.1.pre"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override "has_prerelease", version: "= 1.1.pre"
+ gem "has_prerelease"
+ G
+
+ expect(the_bundle).to include_gems "has_prerelease 1.1.pre"
+ end
+ end
+
+ context "with a version: :ignore_upper operation" do
+ it "strips a < upper bound on a direct dependency" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: :ignore_upper
+ gem "myrack", "< 1.0"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "folds ~> into >= so newer versions become reachable" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: :ignore_upper
+ gem "myrack", "~> 0.9.1"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with a version: nil operation" do
+ it "drops a direct dependency's pin entirely" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: nil
+ gem "myrack", "= 0.9.1"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "drops a transitive dependency's pin entirely" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: nil
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0"
+ end
+
+ it "applies a transitive-only override against an existing lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1", "myrack_middleware 1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 1.0.0"
+ gem "myrack_middleware"
+ G
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack_middleware 1.0"
+ end
+ end
+
+ context "lockfile contents" do
+ it "does not record the override directive in Gemfile.lock" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ override "myrack", version: "= 0.9.1"
+ gem "myrack"
+ G
+
+ expect(lockfile).not_to match(/override/i)
+ end
+ end
+
+ context "with a required_ruby_version: operation" do
+ it "lets the resolver pick a gem whose required_ruby_version excludes the current Ruby with :ignore_upper" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: :ignore_upper
+ gem "needs_old_ruby"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby (1.0)")
+ end
+
+ it "lets the resolver pick the gem with required_ruby_version: nil" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: nil
+ gem "needs_old_ruby"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby (1.0)")
+ end
+
+ it "applies to a transitive dependency's required_ruby_version" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "wraps_old", "1.0" do |s|
+ s.add_dependency "needs_old_ruby"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: :ignore_upper
+ gem "wraps_old"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby (1.0)")
+ expect(lockfile).to include("wraps_old (1.0)")
+ end
+
+ it "re-resolves a direct dep when a metadata override is added against an existing lockfile" do
+ build_repo2 do
+ build_gem "selectable", "1.0"
+ build_gem "selectable", "2.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "selectable"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("selectable (1.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "selectable", required_ruby_version: :ignore_upper
+ gem "selectable"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("selectable (2.0)")
+ end
+ end
+
+ context "with a required_rubygems_version: operation" do
+ it "lets the resolver pick a gem whose required_rubygems_version excludes the current RubyGems with :ignore_upper" do
+ build_repo2 do
+ build_gem "needs_old_rubygems", "1.0" do |s|
+ s.required_rubygems_version = "< #{Gem.rubygems_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_rubygems", required_rubygems_version: :ignore_upper
+ gem "needs_old_rubygems"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_rubygems (1.0)")
+ end
+ end
+
+ context "with an :all target" do
+ it "applies required_ruby_version: :ignore_upper to every gem" do
+ build_repo2 do
+ build_gem "needs_old_ruby_a", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "needs_old_ruby_b", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ gem "needs_old_ruby_a"
+ gem "needs_old_ruby_b"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("needs_old_ruby_a (1.0)")
+ expect(lockfile).to include("needs_old_ruby_b (1.0)")
+ end
+
+ it "is overridden by a per-gem override on the same field" do
+ build_repo2 do
+ build_gem "permissive", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "still_blocked", "1.0" do |s|
+ s.required_ruby_version = "= #{Gem.ruby_version}.999"
+ end
+ end
+
+ # :all says ignore_upper (would unblock both), but per-gem on
+ # still_blocked nails it to a hard requirement that still fails.
+ gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ override "still_blocked", required_ruby_version: "= #{Gem.ruby_version}.999"
+ gem "permissive"
+ gem "still_blocked"
+ G
+
+ bundle :lock, raise_on_error: false
+ expect(err).to include("still_blocked")
+ end
+
+ it "preserves locked versions when an :all metadata override is added without bundle update" do
+ build_repo2 do
+ build_gem "selectable", "1.0"
+ build_gem "selectable", "2.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "selectable"
+ G
+
+ bundle :lock
+ expect(lockfile).to include("selectable (1.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ gem "selectable"
+ G
+
+ # :all override alone does not pre-unlock locked specs; narrow change
+ # should not trigger unrelated lockfile churn.
+ bundle :lock
+ expect(lockfile).to include("selectable (1.0)")
+
+ # bundle update opts the user into re-resolution under the override.
+ bundle "update selectable"
+ expect(lockfile).to include("selectable (2.0)")
+ end
+ end
+
+ context "diagnostic on resolve failure" do
+ it "lists active overrides with their Gemfile location" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "= #{Gem.ruby_version}.999"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: "= #{Gem.ruby_version}.999"
+ gem "needs_old_ruby"
+ G
+
+ bundle :lock, raise_on_error: false
+ expect(err).to include("Bundler applied the following overrides")
+ expect(err).to include("override \"needs_old_ruby\", required_ruby_version:")
+ expect(err).to match(/declared at Gemfile:\d+/)
+ end
+ end
+
+ context "install-time compatibility" do
+ it "installs a gem whose required_ruby_version excludes the current Ruby when an override removes the constraint" do
+ build_repo2 do
+ build_gem "needs_old_ruby", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_ruby", required_ruby_version: nil
+ gem "needs_old_ruby"
+ G
+
+ expect(the_bundle).to include_gems "needs_old_ruby 1.0"
+ end
+
+ it "installs a gem whose required_rubygems_version excludes the current RubyGems when an override removes it" do
+ build_repo2 do
+ build_gem "needs_old_rubygems", "1.0" do |s|
+ s.required_rubygems_version = "< #{Gem.rubygems_version}"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override "needs_old_rubygems", required_rubygems_version: nil
+ gem "needs_old_rubygems"
+ G
+
+ expect(the_bundle).to include_gems "needs_old_rubygems 1.0"
+ end
+
+ it "installs every gem when :all required_ruby_version override is in effect" do
+ build_repo2 do
+ build_gem "needs_old_ruby_a", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ build_gem "needs_old_ruby_b", "1.0" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ override :all, required_ruby_version: :ignore_upper
+ gem "needs_old_ruby_a"
+ gem "needs_old_ruby_b"
+ G
+
+ expect(the_bundle).to include_gems "needs_old_ruby_a 1.0", "needs_old_ruby_b 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/path_spec.rb b/spec/bundler/install/gemfile/path_spec.rb
new file mode 100644
index 0000000000..b069488531
--- /dev/null
+++ b/spec/bundler/install/gemfile/path_spec.rb
@@ -0,0 +1,1017 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with explicit source paths" do
+ it "fetches gems" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ path "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "supports pinned paths" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ gem 'foo', :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "supports relative paths" do
+ build_lib "foo"
+
+ relative_path = lib_path("foo-1.0").relative_path_from(bundled_app)
+
+ install_gemfile <<-G
+ gem 'foo', :path => "#{relative_path}"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "expands paths" do
+ build_lib "foo"
+
+ relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("~").expand_path)
+
+ install_gemfile <<-G
+ gem 'foo', :path => "~/#{relative_path}"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "expands paths raise error with not existing user's home dir" do
+ skip "problems with ~ expansion" if Gem.win_platform?
+
+ build_lib "foo"
+ username = "some_unexisting_user"
+ relative_path = lib_path("foo-1.0").relative_path_from(Pathname.new("/home/#{username}").expand_path)
+
+ install_gemfile <<-G, raise_on_error: false
+ gem 'foo', :path => "~#{username}/#{relative_path}"
+ G
+ expect(err).to match("There was an error while trying to use the path `~#{username}/#{relative_path}`.")
+ expect(err).to match("user #{username} doesn't exist")
+ end
+
+ it "expands paths relative to Bundler.root" do
+ build_lib "foo", path: bundled_app("foo-1.0")
+
+ install_gemfile <<-G
+ gem 'foo', :path => "./foo-1.0"
+ G
+
+ expect(the_bundle).to include_gems("foo 1.0", dir: bundled_app("subdir").mkpath)
+ end
+
+ it "sorts paths consistently on install and update when they start with ./" do
+ build_lib "demo", path: lib_path("demo")
+ build_lib "aaa", path: lib_path("demo/aaa")
+
+ gemfile lib_path("demo/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gemspec
+ gem "aaa", :path => "./aaa"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "aaa", "1.0"
+ c.no_checksum "demo", "1.0"
+ end
+
+ lockfile = <<~L
+ PATH
+ remote: .
+ specs:
+ demo (1.0)
+
+ PATH
+ remote: aaa
+ specs:
+ aaa (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ aaa!
+ demo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install, dir: lib_path("demo")
+ expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile)
+ bundle :update, all: true, dir: lib_path("demo")
+ expect(lib_path("demo/Gemfile.lock")).to read_as(lockfile)
+ end
+
+ it "expands paths when comparing locked paths to Gemfile paths" do
+ build_lib "foo", path: bundled_app("foo-1.0")
+
+ install_gemfile <<-G
+ gem 'foo', :path => File.expand_path("foo-1.0", __dir__)
+ G
+
+ bundle_config "frozen true"
+ bundle :install
+ end
+
+ it "installs dependencies from the path even if a newer gem is available elsewhere" do
+ system_gems "myrack-1.0.0"
+
+ build_lib "myrack", "1.0", path: lib_path("nested/bar") do |s|
+ s.write "lib/myrack.rb", "puts 'WIN OVERRIDE'"
+ end
+
+ build_lib "foo", path: lib_path("nested") do |s|
+ s.add_dependency "myrack", "= 1.0"
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("nested")}"
+ G
+
+ run "require 'myrack'"
+ expect(out).to eq("WIN OVERRIDE")
+ end
+
+ it "works" do
+ build_gem "foo", "1.0.0", to_system: true do |s|
+ s.write "lib/foo.rb", "puts 'FAIL'"
+ end
+
+ build_lib "omg", "1.0", path: lib_path("omg") do |s|
+ s.add_dependency "foo"
+ end
+
+ build_lib "foo", "1.0.0", path: lib_path("omg/foo")
+
+ install_gemfile <<-G
+ gem "omg", :path => "#{lib_path("omg")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works when using prereleases of 0.0.0" do
+ build_lib "foo", "0.0.0.dev", path: lib_path("foo")
+
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (0.0.0.dev)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "foo 0.0.0.dev"
+ end
+
+ it "works when using uppercase prereleases of 0.0.0" do
+ build_lib "foo", "0.0.0.SNAPSHOT", path: lib_path("foo")
+
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (0.0.0.SNAPSHOT)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "foo 0.0.0.SNAPSHOT"
+ end
+
+ it "handles downgrades" do
+ build_lib "omg", "2.0", path: lib_path("omg")
+
+ install_gemfile <<-G
+ gem "omg", :path => "#{lib_path("omg")}"
+ G
+
+ build_lib "omg", "1.0", path: lib_path("omg")
+
+ bundle :install
+
+ expect(the_bundle).to include_gems "omg 1.0"
+ end
+
+ it "prefers gemspecs closer to the path root" do
+ build_lib "premailer", "1.0.0", path: lib_path("premailer") do |s|
+ s.write "gemfiles/ruby187.gemspec", <<-G
+ Gem::Specification.new do |s|
+ s.name = 'premailer'
+ s.version = '1.0.0'
+ s.summary = 'Hi'
+ s.authors = 'Me'
+ end
+ G
+ end
+
+ install_gemfile <<-G
+ gem "premailer", :path => "#{lib_path("premailer")}"
+ G
+
+ # Installation of the 'gemfiles' gemspec would fail since it will be unable
+ # to require 'premailer.rb'
+ expect(the_bundle).to include_gems "premailer 1.0.0"
+ end
+
+ it "warns on invalid specs" do
+ build_lib "foo"
+
+ gemspec = lib_path("foo-1.0").join("foo.gemspec").to_s
+ File.open(gemspec, "w") do |f|
+ f.write <<-G
+ Gem::Specification.new do |s|
+ s.name = "foo"
+ end
+ G
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(err).to match(/is not valid. Please fix this gemspec./)
+ expect(err).to match(/The validation error was 'missing value for attribute version'/)
+ expect(err).to match(/You have one or more invalid gemspecs that need to be fixed/)
+ end
+
+ it "supports gemspec syntax" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ end
+
+ gemfile lib_path("foo/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ bundle "install", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo")
+ end
+
+ it "does not unlock dependencies of path sources" do
+ build_repo4 do
+ build_gem "graphql", "2.0.15"
+ build_gem "graphql", "2.0.16"
+ end
+
+ build_lib "foo", "0.1.0", path: lib_path("foo") do |s|
+ s.add_dependency "graphql", "~> 2.0"
+ end
+
+ gemfile_path = lib_path("foo/Gemfile")
+
+ gemfile gemfile_path, <<-G
+ source "https://gem.repo4"
+ gemspec
+ G
+
+ lockfile_path = lib_path("foo/Gemfile.lock")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "0.1.0"
+ c.checksum gem_repo4, "graphql", "2.0.15"
+ end
+
+ original_lockfile = <<~L
+ PATH
+ remote: .
+ specs:
+ foo (0.1.0)
+ graphql (~> 2.0)
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ graphql (2.0.15)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_path, original_lockfile
+
+ build_lib "foo", "0.1.1", path: lib_path("foo") do |s|
+ s.add_dependency "graphql", "~> 2.0"
+ end
+
+ bundle "install", dir: lib_path("foo")
+ expect(lockfile_path).to read_as(original_lockfile.gsub("foo (0.1.0)", "foo (0.1.1)"))
+ end
+
+ it "supports gemspec syntax with an alternative path" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => "#{lib_path("foo")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ it "doesn't automatically unlock dependencies when using the gemspec syntax" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", ">= 1.0"
+ end
+
+ install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo")
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ build_gem "myrack", "1.0.1", to_system: true
+
+ bundle "install", dir: lib_path("foo")
+
+ expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo")
+ end
+
+ it "doesn't automatically unlock dependencies when using the gemspec syntax and the gem has development dependencies" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", ">= 1.0"
+ s.add_development_dependency "activesupport"
+ end
+
+ install_gemfile lib_path("foo/Gemfile"), <<-G, dir: lib_path("foo")
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ build_gem "myrack", "1.0.1", to_system: true
+
+ bundle "install", dir: lib_path("foo")
+
+ expect(the_bundle).to include_gems "foo 1.0", dir: lib_path("foo")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("foo")
+ end
+
+ it "raises if there are multiple gemspecs" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.write "bar.gemspec", build_spec("bar", "1.0").first.to_ruby
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ gemspec :path => "#{lib_path("foo")}"
+ G
+
+ expect(exitstatus).to eq(15)
+ expect(err).to match(/There are multiple gemspecs/)
+ end
+
+ it "allows :name to be specified to resolve ambiguity" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.write "bar.gemspec"
+ end
+
+ install_gemfile <<-G
+ gemspec :path => "#{lib_path("foo")}", :name => "foo"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "sets up executables" do
+ build_lib "foo" do |s|
+ s.executables = "foobar"
+ end
+
+ install_gemfile <<-G, verbose: true
+ path "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+ expect(out).to include("Using foo 1.0 from source at `#{lib_path("foo-1.0")}` and installing its executables")
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ bundle "exec foobar"
+ expect(out).to eq("1.0")
+ end
+
+ it "handles directories in bin/" do
+ build_lib "foo"
+ FileUtils.rm_rf lib_path("foo-1.0").join("foo.gemspec")
+ lib_path("foo-1.0").join("bin/performance").mkpath
+
+ install_gemfile <<-G
+ gem 'foo', '1.0', :path => "#{lib_path("foo-1.0")}"
+ G
+ expect(err).to be_empty
+ end
+
+ it "removes the .gem file after installing" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ gem 'foo', :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(lib_path("foo-1.0").join("foo-1.0.gem")).not_to exist
+ end
+
+ describe "block syntax" do
+ it "pulls all gems from a path block" do
+ build_lib "omg"
+ build_lib "hi2u"
+
+ install_gemfile <<-G
+ path "#{lib_path}" do
+ gem "omg"
+ gem "hi2u"
+ end
+ G
+
+ expect(the_bundle).to include_gems "omg 1.0", "hi2u 1.0"
+ end
+ end
+
+ it "keeps source pinning" do
+ build_lib "foo", "1.0", path: lib_path("foo")
+ build_lib "omg", "1.0", path: lib_path("omg")
+ build_lib "foo", "1.0", path: lib_path("omg/foo") do |s|
+ s.write "lib/foo.rb", "puts 'FAIL'"
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo")}"
+ gem "omg", :path => "#{lib_path("omg")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works when the path does not have a gemspec" do
+ build_lib "foo", gemspec: false
+
+ gemfile <<-G
+ gem "foo", "1.0", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "works when the path does not have a gemspec but there is a lockfile" do
+ lockfile <<~L
+ PATH
+ remote: vendor/bar
+ specs:
+ L
+
+ FileUtils.mkdir_p(bundled_app("vendor/bar"))
+
+ install_gemfile <<-G
+ gem "bar", "1.0.0", path: "vendor/bar", require: "bar/nyard"
+ G
+ end
+
+ context "existing lockfile" do
+ it "rubygems gems don't re-resolve without changes" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack-obama', '1.0'
+ gem 'net-ssh', '1.0'
+ G
+
+ bundle :check, env: { "DEBUG" => "1" }
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0"
+ end
+
+ it "source path gems w/deps don't re-resolve without changes" do
+ build_lib "myrack-obama", "1.0", path: lib_path("omg") do |s|
+ s.add_dependency "yard"
+ end
+
+ build_lib "net-ssh", "1.0", path: lib_path("omg") do |s|
+ s.add_dependency "yard"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack-obama', :path => "#{lib_path("omg")}"
+ gem 'net-ssh', :path => "#{lib_path("omg")}"
+ G
+
+ bundle :check, env: { "DEBUG" => "1" }
+ expect(out).to match(/using resolution from the lockfile/)
+ expect(the_bundle).to include_gems "myrack-obama 1.0", "net-ssh 1.0"
+ end
+ end
+
+ it "installs executable stubs" do
+ build_lib "foo" do |s|
+ s.executables = ["foo"]
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "exec foo"
+ expect(out).to eq("1.0")
+ end
+
+ describe "when the gem version in the path is updated" do
+ before :each do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "bar"
+ end
+ build_lib "bar", "1.0", path: lib_path("foo/bar")
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+ end
+
+ it "unlocks all gems when the top level gem is updated" do
+ build_lib "foo", "2.0", path: lib_path("foo") do |s|
+ s.add_dependency "bar"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "foo 2.0", "bar 1.0"
+ end
+
+ it "unlocks all gems when a child dependency gem is updated" do
+ build_lib "bar", "2.0", path: lib_path("foo/bar")
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "foo 1.0", "bar 2.0"
+ end
+ end
+
+ describe "when dependencies in the path are updated" do
+ before :each do
+ build_lib "foo", "1.0", path: lib_path("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo")}"
+ G
+ end
+
+ it "gets dependencies that are updated in the path" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "keeps using the same version if it's compatible" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ end
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 0.9.1)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack"
+ end
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "keeps using the same version even when another dependency is added" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ end
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 0.9.1)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack"
+ s.add_dependency "rake", rake_version
+ end
+
+ bundle "install"
+
+ checksums.checksum gem_repo1, "rake", rake_version
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack
+ rake (= #{rake_version})
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+ rake (#{rake_version})
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "does not remove existing ruby platform" do
+ build_lib "foo", "1.0", path: lib_path("foo") do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ checksums.checksum gem_repo1, "myrack", "0.9.1"
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo")}
+ specs:
+ foo (1.0)
+ myrack (= 0.9.1)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+
+ context "when platform specific version locked, and having less dependencies that the generic version that's actually installed" do
+ before do
+ build_repo4 do
+ build_gem "racc", "1.8.1"
+ build_gem "mini_portile2", "2.8.2"
+ end
+
+ build_lib "nokogiri", "1.18.9", path: lib_path("nokogiri") do |s|
+ s.add_dependency "mini_portile2", "~> 2.8.2"
+ s.add_dependency "racc", "~> 1.4"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri", path: "#{lib_path("nokogiri")}"
+ G
+
+ lockfile <<~L
+ PATH
+ remote: #{lib_path("nokogiri")}
+ specs:
+ nokogiri (1.18.9)
+ mini_portile2 (~> 2.8.2)
+ racc (~> 1.4)
+ nokogiri (1.18.9-arm64-darwin)
+ racc (~> 1.4)
+
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ racc (1.8.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ nokogiri!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "works" do
+ bundle "install"
+ end
+ end
+
+ describe "switching sources" do
+ it "doesn't switch pinned git sources to rubygems when pinning the parent gem to a path source" do
+ build_gem "foo", "1.0", to_system: true do |s|
+ s.write "lib/foo.rb", "raise 'fail'"
+ end
+ build_lib "foo", "1.0", path: lib_path("bar/foo")
+ build_git "bar", "1.0", path: lib_path("bar") do |s|
+ s.add_dependency "foo"
+ end
+
+ install_gemfile <<-G
+ gem "bar", :git => "#{lib_path("bar")}"
+ G
+
+ install_gemfile <<-G
+ gem "bar", :path => "#{lib_path("bar")}"
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0", "bar 1.0"
+ end
+
+ it "switches the source when the gem existed in rubygems and the path was already being used for another gem" do
+ build_lib "foo", "1.0", path: lib_path("foo")
+ build_gem "bar", "1.0", to_bundle: true do |s|
+ s.write "lib/bar.rb", "raise 'fail'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar"
+ path "#{lib_path("foo")}" do
+ gem "foo"
+ end
+ G
+
+ build_lib "bar", "1.0", path: lib_path("foo/bar")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path("foo")}" do
+ gem "foo"
+ gem "bar"
+ end
+ G
+
+ expect(the_bundle).to include_gems "bar 1.0"
+ end
+ end
+
+ describe "when there are both a gemspec and remote gems" do
+ it "doesn't query rubygems for local gemspec name" do
+ build_lib "private_lib", "2.2", path: lib_path("private_lib")
+ gemfile lib_path("private_lib/Gemfile"), <<-G
+ source "http://localgemserver.test"
+ gemspec
+ gem 'myrack'
+ G
+ bundle :install, env: { "DEBUG" => "1" }, artifice: "endpoint", dir: lib_path("private_lib")
+ expect(out).to match(%r{^HTTP GET http://localgemserver\.test/api/v1/dependencies\?gems=myrack$})
+ expect(out).not_to match(/^HTTP GET.*private_lib/)
+ expect(the_bundle).to include_gems "private_lib 2.2", dir: lib_path("private_lib")
+ expect(the_bundle).to include_gems "myrack 1.0", dir: lib_path("private_lib")
+ end
+ end
+
+ describe "gem install hooks" do
+ it "runs pre-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ STDERR.puts "Ran pre-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran pre-install hook: foo-1.0")
+ end
+
+ it "runs post-install hooks" do
+ build_git "foo"
+ gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.post_install_hooks << lambda do |inst|
+ STDERR.puts "Ran post-install hook: \#{inst.spec.full_name}"
+ end
+ H
+ end
+
+ bundle :install,
+ requires: [lib_path("install_hooks.rb")]
+ expect(err_without_deprecations).to eq("Ran post-install hook: foo-1.0")
+ end
+
+ it "complains if the install hook fails" do
+ build_git "foo"
+ gemfile <<-G
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ File.open(lib_path("install_hooks.rb"), "w") do |h|
+ h.write <<-H
+ Gem.pre_install_hooks << lambda do |inst|
+ false
+ end
+ H
+ end
+
+ bundle :install, requires: [lib_path("install_hooks.rb")], raise_on_error: false
+ expect(err).to include("failed for foo-1.0")
+ end
+
+ it "loads plugins from the path gem" do
+ foo_file = home("foo_plugin_loaded")
+ bar_file = home("bar_plugin_loaded")
+ expect(foo_file).not_to be_file
+ expect(bar_file).not_to be_file
+
+ build_lib "foo" do |s|
+ s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{foo_file}')")
+ end
+
+ build_git "bar" do |s|
+ s.write("lib/rubygems_plugin.rb", "require 'fileutils'; FileUtils.touch('#{bar_file}')")
+ end
+
+ install_gemfile <<-G
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ gem "bar", :path => "#{lib_path("bar-1.0")}"
+ G
+
+ expect(foo_file).to be_file
+ expect(bar_file).to be_file
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/platform_spec.rb b/spec/bundler/install/gemfile/platform_spec.rb
new file mode 100644
index 0000000000..e12933ebcf
--- /dev/null
+++ b/spec/bundler/install/gemfile/platform_spec.rb
@@ -0,0 +1,638 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install across platforms" do
+ it "maintains the same lockfile if all gems are compatible across platforms" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{not_local}
+
+ DEPENDENCIES
+ myrack
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ end
+
+ it "pulls in the correct platform specific gem" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+ platform_specific (1.0-x86-mswin32)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+ G
+
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 java"
+ end
+ end
+
+ it "pulls the pure ruby version on jruby if the java platform is not present in the lockfile and bundler is run in frozen mode", :jruby_only do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1
+ specs:
+ platform_specific (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+ G
+
+ bundle_config "frozen true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ expect(err).to be_empty
+ end
+
+ context "on universal Rubies" do
+ before do
+ build_repo4 do
+ build_gem "darwin_single_arch" do |s|
+ s.platform = "ruby"
+ end
+ build_gem "darwin_single_arch" do |s|
+ s.platform = "arm64-darwin"
+ end
+ build_gem "darwin_single_arch" do |s|
+ s.platform = "x86_64-darwin"
+ end
+ end
+ end
+
+ it "pulls in the correct architecture gem" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4
+ specs:
+ darwin_single_arch (1.0)
+ darwin_single_arch (1.0-arm64-darwin)
+ darwin_single_arch (1.0-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ darwin_single_arch
+ G
+
+ simulate_platform "universal-darwin-21" do
+ simulate_ruby_platform "universal.x86_64-darwin21" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "darwin_single_arch"
+ G
+
+ expect(the_bundle).to include_gems "darwin_single_arch 1.0 x86_64-darwin"
+ end
+ end
+ end
+
+ it "pulls in the correct architecture gem on arm64e macOS Ruby" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4
+ specs:
+ darwin_single_arch (1.0)
+ darwin_single_arch (1.0-arm64-darwin)
+ darwin_single_arch (1.0-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ darwin_single_arch
+ G
+
+ simulate_platform "universal-darwin-21" do
+ simulate_ruby_platform "universal.arm64e-darwin21" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "darwin_single_arch"
+ G
+
+ expect(the_bundle).to include_gems "darwin_single_arch 1.0 arm64-darwin"
+ end
+ end
+ end
+ end
+
+ it "works with gems that have different dependencies" do
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "nokogiri"
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3"
+
+ pristine_system_gems
+ bundle_config "force_ruby_platform true"
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ expect(the_bundle).not_to include_gems "weakling"
+
+ simulate_new_machine
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2 java", "weakling 0.0.3"
+ end
+ end
+
+ it "does not keep unneeded platforms for gems that are used" do
+ build_repo4 do
+ build_gem "empyrean", "0.1.0"
+ build_gem "coderay", "1.1.2"
+ build_gem "method_source", "0.9.0"
+ build_gem("spoon", "0.0.6") {|s| s.add_dependency "ffi" }
+ build_gem "pry", "0.11.3" do |s|
+ s.platform = "java"
+ s.add_dependency "coderay", "~> 1.1.0"
+ s.add_dependency "method_source", "~> 0.9.0"
+ s.add_dependency "spoon", "~> 0.0"
+ end
+ build_gem "pry", "0.11.3" do |s|
+ s.add_dependency "coderay", "~> 1.1.0"
+ s.add_dependency "method_source", "~> 0.9.0"
+ end
+ build_gem("ffi", "1.9.23") {|s| s.platform = "java" }
+ build_gem("ffi", "1.9.23")
+ end
+
+ simulate_platform "java" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "empyrean", "0.1.0"
+ gem "pry"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "coderay", "1.1.2"
+ c.checksum gem_repo4, "empyrean", "0.1.0"
+ c.checksum gem_repo4, "ffi", "1.9.23", "java"
+ c.checksum gem_repo4, "method_source", "0.9.0"
+ c.checksum gem_repo4, "pry", "0.11.3", "java"
+ c.checksum gem_repo4, "spoon", "0.0.6"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ coderay (1.1.2)
+ empyrean (0.1.0)
+ ffi (1.9.23-java)
+ method_source (0.9.0)
+ pry (0.11.3-java)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ spoon (~> 0.0)
+ spoon (0.0.6)
+ ffi
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ empyrean (= 0.1.0)
+ pry
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --add-platform ruby"
+
+ checksums.checksum gem_repo4, "pry", "0.11.3"
+
+ good_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ coderay (1.1.2)
+ empyrean (0.1.0)
+ ffi (1.9.23-java)
+ method_source (0.9.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ pry (0.11.3-java)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ spoon (~> 0.0)
+ spoon (0.0.6)
+ ffi
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ empyrean (= 0.1.0)
+ pry
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ expect(lockfile).to eq good_lockfile
+
+ bad_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ coderay (1.1.2)
+ empyrean (0.1.0)
+ ffi (1.9.23)
+ ffi (1.9.23-java)
+ method_source (0.9.0)
+ pry (0.11.3)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ pry (0.11.3-java)
+ coderay (~> 1.1.0)
+ method_source (~> 0.9.0)
+ spoon (~> 0.0)
+ spoon (0.0.6)
+ ffi
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ empyrean (= 0.1.0)
+ pry
+ #{checksums}
+ BUNDLED WITH
+ 1.16.1
+ L
+
+ aggregate_failures do
+ lockfile bad_lockfile
+ bundle :install, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle :update, all: true, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle "update ffi", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle "update empyrean", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+
+ lockfile bad_lockfile
+ bundle :lock, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(lockfile).to eq good_lockfile
+ end
+ end
+ end
+
+ it "works with gems with platform-specific dependency having different requirements order" do
+ simulate_platform "x86_64-darwin-15" do
+ update_repo2 do
+ build_gem "fspath", "3"
+ build_gem "image_optim_pack", "1.2.3" do |s|
+ s.add_dependency "fspath", ">= 2.1", "< 4"
+ end
+ build_gem "image_optim_pack", "1.2.3" do |s|
+ s.platform = "universal-darwin"
+ s.add_dependency "fspath", "< 4", ">= 2.1"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "image_optim_pack"
+ G
+
+ expect(err).not_to include "Unable to use the platform-specific"
+
+ expect(the_bundle).to include_gem "image_optim_pack 1.2.3 universal-darwin"
+ end
+ end
+
+ it "fetches gems again after changing the version of Ruby" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", "1.0.0"
+ G
+
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ FileUtils.mv(vendored_gems, bundled_app("vendor/bundle", Gem.ruby_engine, "1.8"))
+
+ bundle :install
+ expect(vendored_gems("gems/myrack-1.0.0")).to exist
+ end
+
+ it "keeps existing platforms when installing with force_ruby_platform" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "platform_specific", "1.0"
+ c.checksum gem_repo1, "platform_specific", "1.0", "java"
+ end
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0-java)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ platform_specific
+ #{checksums}
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ checksums.checksum gem_repo1, "platform_specific", "1.0"
+
+ expect(the_bundle).to include_gem "platform_specific 1.0 ruby"
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0)
+ platform_specific (1.0-java)
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+end
+
+RSpec.describe "bundle install with platform conditionals" do
+ it "installs gems tagged w/ the current platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :#{local_tag} do
+ gem "nokogiri"
+ end
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+
+ it "does not install gems tagged w/ another platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ platforms :#{not_local_tag} do
+ gem "nokogiri"
+ end
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ expect(the_bundle).not_to include_gems "nokogiri 1.4.2"
+ end
+
+ it "installs gems tagged w/ another platform but also dependent on the current one transitively" do
+ build_repo4 do
+ build_gem "activesupport", "6.1.4.1" do |s|
+ s.add_dependency "tzinfo", "~> 2.0"
+ end
+
+ build_gem "tzinfo", "2.0.4"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "activesupport"
+
+ platforms :#{not_local_tag} do
+ gem "tzinfo", "~> 1.2"
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activesupport (6.1.4.1)
+ tzinfo (~> 2.0)
+ tzinfo (2.0.4)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ activesupport
+ tzinfo (~> 1.2)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+
+ expect(the_bundle).to include_gems "tzinfo 2.0.4"
+ end
+
+ it "installs gems tagged w/ the current platforms inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platforms => :#{local_tag}
+ G
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+
+ it "does not install gems tagged w/ another platforms inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "nokogiri", :platforms => :#{not_local_tag}
+ G
+ expect(the_bundle).to include_gems "myrack 1.0"
+ expect(the_bundle).not_to include_gems "nokogiri 1.4.2"
+ end
+
+ it "installs gems tagged w/ the current platform inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platform => :#{local_tag}
+ G
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+
+ it "doesn't install gems tagged w/ another platform inline" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platform => :#{not_local_tag}
+ G
+ expect(the_bundle).not_to include_gems "nokogiri 1.4.2"
+ end
+
+ it "does not blow up on sources with all platform-excluded specs" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ platform :#{not_local_tag} do
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ end
+ G
+
+ bundle :list
+ end
+
+ it "does not attempt to install gems from :rbx when using --local" do
+ bundle_config "force_ruby_platform true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "some_gem", :platform => :rbx
+ G
+
+ bundle "install --local"
+ expect(out).not_to match(/Could not find gem 'some_gem/)
+ end
+
+ it "does not attempt to install gems from other rubies when using --local" do
+ bundle_config "force_ruby_platform true"
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "some_gem", platform: :ruby_22
+ G
+
+ bundle "install --local"
+ expect(out).not_to match(/Could not find gem 'some_gem/)
+ end
+
+ it "does not print a warning when a dependency is unused on a platform different from the current one" do
+ bundle_config "force_ruby_platform true"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :platform => [:windows, :jruby]
+ G
+
+ bundle "install"
+
+ expect(err).to be_empty
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ myrack
+ #{checksums_section_when_enabled}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "resolves fine when a dependency is unused on a platform different from the current one, but reintroduced transitively" do
+ bundle_config "force_ruby_platform true"
+
+ build_repo4 do
+ build_gem "listen", "3.7.1" do |s|
+ s.add_dependency "ffi"
+ end
+
+ build_gem "ffi", "1.15.5"
+ end
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "listen"
+ gem "ffi", :platform => :windows
+ G
+ expect(err).to be_empty
+ end
+end
+
+RSpec.describe "when a gem has no architecture" do
+ it "still installs correctly" do
+ simulate_platform "x86-mswin32" do
+ build_repo2 do
+ # The rcov gem is platform mswin32, but has no arch
+ build_gem "rcov" do |s|
+ s.platform = Gem::Platform.new([nil, "mswin32", nil])
+ s.write "lib/rcov.rb", "RCOV = '1.0.0'"
+ end
+ end
+
+ gemfile <<-G
+ # Try to install gem with nil arch
+ source "http://localgemserver.test/"
+ gem "rcov"
+ G
+
+ bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(the_bundle).to include_gems "rcov 1.0.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/ruby_spec.rb b/spec/bundler/install/gemfile/ruby_spec.rb
new file mode 100644
index 0000000000..d937abd714
--- /dev/null
+++ b/spec/bundler/install/gemfile/ruby_spec.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+RSpec.describe "ruby requirement" do
+ def locked_ruby_version
+ Bundler::RubyVersion.from_string(Bundler::LockfileParser.new(File.read(bundled_app_lock)).ruby_version)
+ end
+
+ # As discovered by https://github.com/rubygems/bundler/issues/4147, there is
+ # no test coverage to ensure that adding a gem is possible with a ruby
+ # requirement. This test verifies the fix, committed in bfbad5c5.
+ it "allows adding gems" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{Gem.ruby_version}"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{Gem.ruby_version}"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ expect(the_bundle).to include_gems "myrack-obama 1.0"
+ end
+
+ it "allows removing the ruby version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "~> #{Gem.ruby_version}"
+ gem "myrack"
+ G
+
+ expect(lockfile).to include("RUBY VERSION")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(lockfile).not_to include("RUBY VERSION")
+ end
+
+ it "allows changing the ruby version requirement to something compatible" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= #{current_ruby_minor}"
+ gem "myrack"
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(locked_ruby_version).to eq(Bundler::RubyVersion.system)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= #{Gem.ruby_version}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(locked_ruby_version).to eq(Bundler::RubyVersion.system)
+ end
+
+ it "allows changing the ruby version requirement to something incompatible" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= 1.0.0"
+ gem "myrack"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ RUBY VERSION
+ ruby 2.1.4
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby ">= #{current_ruby_minor}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(locked_ruby_version).to eq(Bundler::RubyVersion.system)
+ end
+
+ it "allows requirements with trailing whitespace" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{Gem.ruby_version}\\n \t\\n"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "fails gracefully with malformed requirements" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+ ruby ">= 0", "-.\\0"
+ gem "myrack"
+ G
+
+ expect(err).to include("There was an error parsing") # i.e. DSL error, not error template
+ end
+
+ it "allows picking up ruby version from a file" do
+ create_file ".ruby-version", Gem.ruby_version.to_s
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby file: ".ruby-version"
+ gem "myrack"
+ G
+
+ expect(lockfile).to include("RUBY VERSION")
+ end
+
+ it "reads the ruby version file from the right folder when nested Gemfiles are involved" do
+ create_file ".ruby-version", Gem.ruby_version.to_s
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ ruby file: ".ruby-version"
+ gem "myrack"
+ G
+
+ nested_dir = bundled_app(".ruby-lsp")
+
+ FileUtils.mkdir nested_dir
+
+ gemfile ".ruby-lsp/Gemfile", <<-G
+ eval_gemfile(File.expand_path("../Gemfile", __dir__))
+ G
+
+ bundle "install", dir: nested_dir
+
+ expect(bundled_app(".ruby-lsp/Gemfile.lock").read).to include("RUBY VERSION")
+ end
+end
diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb
new file mode 100644
index 0000000000..654d638e1f
--- /dev/null
+++ b/spec/bundler/install/gemfile/sources_spec.rb
@@ -0,0 +1,1313 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with gems on multiple sources" do
+ # repo1 is built automatically before all of the specs run
+ # it contains myrack-obama 1.0.0 and myrack 0.9.1 & 1.0.0 amongst other gems
+
+ context "with source affinity" do
+ context "with sources given by a block" do
+ before do
+ # Oh no! Someone evil is trying to hijack myrack :(
+ # need this to be broken to check for correct source ordering
+ build_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ build_gem "myrack-obama" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo3"
+ source "https://gem.repo1" do
+ gem "thin" # comes first to test name sorting
+ gem "myrack"
+ end
+ gem "myrack-obama" # should come from repo3!
+ G
+ end
+
+ it "installs the gems without any warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("myrack-obama 1.0.0")
+ expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote1")
+ end
+
+ it "can cache and deploy" do
+ bundle :cache, artifice: "compact_index"
+
+ expect(bundled_app("vendor/cache/myrack-1.0.0.gem")).to exist
+ expect(bundled_app("vendor/cache/myrack-obama-1.0.gem")).to exist
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0")
+ end
+ end
+
+ context "with sources set by an option" do
+ before do
+ # Oh no! Someone evil is trying to hijack myrack :(
+ # need this to be broken to check for correct source ordering
+ build_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ build_gem "myrack-obama" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo3"
+ gem "myrack-obama" # should come from repo3!
+ gem "myrack", :source => "https://gem.repo1"
+ G
+ end
+
+ it "installs the gems without any warning" do
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("myrack-obama 1.0.0", "myrack 1.0.0")
+ end
+ end
+
+ context "when a pinned gem has an indirect dependency in the pinned source" do
+ before do
+ build_repo3 do
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ # we need a working myrack gem in repo3
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ source "https://gem.repo3" do
+ gem "depends_on_myrack"
+ end
+ G
+ end
+
+ context "and not in any other sources" do
+ before do
+ build_repo(gem_repo2) {}
+ end
+
+ it "installs from the same source without any warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+
+ context "and in another source" do
+ before do
+ # need this to be broken to check for correct source ordering
+ build_repo gem_repo2 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+ end
+
+ it "installs from the same source without any warning" do
+ bundle :install, artifice: "compact_index"
+
+ expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+
+ # In https://github.com/bundler/bundler/issues/3585 this failed
+ # when there is already a lockfile, and the gems are missing, so try again
+ system_gems []
+ bundle :install, artifice: "compact_index"
+
+ expect(err).not_to include("Warning: the gem 'myrack' was found in multiple sources.")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+ end
+
+ context "when a pinned gem has an indirect dependency in a different source" do
+ before do
+ # In these tests, we need a working myrack gem in repo2 and not repo3
+
+ build_repo3 do
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ build_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ context "and not in any other sources" do
+ before do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2"
+ source "https://gem.repo3" do
+ gem "depends_on_myrack"
+ end
+ G
+ end
+
+ it "installs from the other source without any warning" do
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0")
+ end
+ end
+ end
+
+ context "when a top-level gem can only be found in an scoped source" do
+ before do
+ build_repo2
+
+ build_repo3 do
+ build_gem "private_gem_1", "1.0.0"
+ build_gem "private_gem_2", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "private_gem_1"
+
+ source "https://gem.repo3" do
+ gem "private_gem_2"
+ end
+ G
+ end
+
+ it "fails" do
+ bundle :install, artifice: "compact_index", raise_on_error: false
+ expect(err).to include("Could not find gem 'private_gem_1' in rubygems repository https://gem.repo2/ or installed locally.")
+ end
+ end
+
+ context "when a top-level gem has an indirect dependency" do
+ before do
+ build_repo gem_repo2 do
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ build_repo3 do
+ build_gem "unrelated_gem", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "depends_on_myrack"
+
+ source "https://gem.repo3" do
+ gem "unrelated_gem"
+ end
+ G
+ end
+
+ context "and the dependency is only in the top-level source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the top-level source without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2")
+ expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3")
+ end
+ end
+
+ context "and the dependency is only in a pinned source" do
+ before do
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+ end
+
+ it "does not find the dependency" do
+ bundle :install, artifice: "compact_index", raise_on_error: false
+ expect(err).to end_with <<~E.strip
+ Could not find compatible versions
+
+ Because every version of depends_on_myrack depends on myrack >= 0
+ and myrack >= 0 could not be found in rubygems repository https://gem.repo2/ or installed locally,
+ depends_on_myrack cannot be used.
+ So, because Gemfile depends on depends_on_myrack >= 0,
+ version solving has failed.
+ E
+ end
+ end
+
+ context "and the dependency is in both the top-level and a pinned source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+ end
+
+ it "installs the dependency from the top-level source without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(run("require 'myrack'; puts MYRACK")).to eq("1.0.0")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", "unrelated_gem 1.0.0")
+ expect(the_bundle).to include_gems("depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote2")
+ expect(the_bundle).to include_gems("unrelated_gem 1.0.0", source: "remote3")
+ end
+ end
+ end
+
+ context "when a scoped gem has a deeply nested indirect dependency" do
+ before do
+ build_repo3 do
+ build_gem "depends_on_depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "depends_on_myrack"
+ end
+
+ build_gem "depends_on_myrack", "1.0.1" do |s|
+ s.add_dependency "myrack"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo3" do
+ gem "depends_on_depends_on_myrack"
+ end
+ G
+ end
+
+ context "and the dependency is only in the top-level source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the top-level source" do
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0")
+ expect(the_bundle).to include_gems("myrack 1.0.0", source: "remote2")
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", source: "remote3")
+ end
+ end
+
+ context "and the dependency is only in a pinned source" do
+ before do
+ build_repo2
+
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the pinned source" do
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+
+ context "and the dependency is in both the top-level and a pinned source" do
+ before do
+ update_repo gem_repo2 do
+ build_gem "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+ end
+
+ update_repo gem_repo3 do
+ build_gem "myrack", "1.0.0"
+ end
+ end
+
+ it "installs the dependency from the pinned source without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems("depends_on_depends_on_myrack 1.0.1", "depends_on_myrack 1.0.1", "myrack 1.0.0", source: "remote3")
+ end
+ end
+ end
+
+ context "when a top-level gem has an indirect dependency present in the default source, but with a different version from the one resolved" do
+ before do
+ build_lib "activesupport", "7.0.0.alpha", path: lib_path("rails/activesupport")
+ build_lib "rails", "7.0.0.alpha", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 7.0.0.alpha"
+ end
+
+ build_repo gem_repo2 do
+ build_gem "activesupport", "6.1.2"
+
+ build_gem "webpacker", "5.2.1" do |s|
+ s.add_dependency "activesupport", ">= 5.2"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gemspec :path => "#{lib_path("rails")}"
+
+ gem "webpacker", "~> 5.0"
+ G
+ end
+
+ it "installs all gems without warning" do
+ bundle :install, artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", "rails 7.0.0.alpha")
+ expect(the_bundle).to include_gems("activesupport 7.0.0.alpha", source: "path@#{lib_path("rails/activesupport")}")
+ expect(the_bundle).to include_gems("rails 7.0.0.alpha", source: "path@#{lib_path("rails")}")
+ end
+ end
+
+ context "when a pinned gem has an indirect dependency with more than one level of indirection in the default source " do
+ before do
+ build_repo3 do
+ build_gem "handsoap", "0.2.5.5" do |s|
+ s.add_dependency "nokogiri", ">= 1.2.3"
+ end
+ end
+
+ update_repo gem_repo2 do
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "racca", "1.5.2"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ source "https://gem.repo3" do
+ gem "handsoap"
+ end
+
+ gem "nokogiri"
+ G
+ end
+
+ it "installs from the default source without any warnings or errors and generates a proper lockfile" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo3, "handsoap", "0.2.5.5"
+ c.checksum gem_repo2, "nokogiri", "1.11.1"
+ c.checksum gem_repo2, "racca", "1.5.2"
+ end
+
+ expected_lockfile = <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ nokogiri (1.11.1)
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ GEM
+ remote: https://gem.repo3/
+ specs:
+ handsoap (0.2.5.5)
+ nokogiri (>= 1.2.3)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ handsoap!
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose", artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3")
+ expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2")
+ expect(lockfile).to eq(expected_lockfile)
+
+ # Even if the gems are already installed
+ FileUtils.rm bundled_app_lock
+ bundle "install --verbose", artifice: "compact_index"
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", "nokogiri 1.11.1", "racca 1.5.2")
+ expect(the_bundle).to include_gems("handsoap 0.2.5.5", source: "remote3")
+ expect(the_bundle).to include_gems("nokogiri 1.11.1", "racca 1.5.2", source: "remote2")
+ expect(lockfile).to eq(expected_lockfile)
+ end
+ end
+
+ context "with a gem that is only found in the wrong source" do
+ before do
+ build_repo3 do
+ build_gem "not_in_repo1", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", raise_on_error: false
+ source "https://gem.repo3"
+ gem "not_in_repo1", :source => "https://gem.repo1"
+ G
+ end
+
+ it "does not install the gem" do
+ expect(err).to include("Could not find gem 'not_in_repo1'")
+ end
+ end
+
+ context "with an existing lockfile" do
+ before do
+ system_gems "myrack-0.9.1", "myrack-1.0.0", path: default_bundle_path
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1
+ specs:
+
+ GEM
+ remote: https://gem.repo3
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack!
+ L
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo3" do
+ gem 'myrack'
+ end
+ G
+ end
+
+ # Reproduction of https://github.com/rubygems/bundler/issues/3298
+ it "does not unlock the installed gem on exec" do
+ expect(the_bundle).to include_gems("myrack 0.9.1")
+ end
+ end
+
+ context "with a path gem in the same Gemfile" do
+ before do
+ build_lib "foo"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :source => "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+ end
+
+ it "does not unlock the non-path gem after install" do
+ bundle :install, artifice: "compact_index"
+
+ bundle %(exec ruby -e 'puts "OK"')
+
+ expect(out).to include("OK")
+ end
+ end
+ end
+
+ context "when an older version of the same gem also ships with Ruby" do
+ before do
+ system_gems "myrack-0.9.1"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ gem "myrack" # should come from repo1!
+ G
+ end
+
+ it "installs the gems without any warning" do
+ expect(err).not_to include("Warning")
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "when a single source contains multiple locked gems" do
+ before do
+ # With these gems,
+ build_repo4 do
+ build_gem "foo", "0.1"
+ build_gem "bar", "0.1"
+ end
+
+ # Installing this gemfile...
+ gemfile <<-G
+ source 'https://gem.repo1'
+ gem 'myrack'
+ gem 'foo', '~> 0.1', :source => 'https://gem.repo4'
+ gem 'bar', '~> 0.1', :source => 'https://gem.repo4'
+ G
+
+ bundle_config "path ../gems/system"
+ bundle :install, artifice: "compact_index"
+
+ # And then we add some new versions...
+ build_repo4 do
+ build_gem "foo", "0.2"
+ build_gem "bar", "0.3"
+ end
+ end
+
+ it "allows them to be unlocked separately" do
+ # And install this gemfile, updating only foo.
+ install_gemfile <<-G, artifice: "compact_index"
+ source 'https://gem.repo1'
+ gem 'myrack'
+ gem 'foo', '~> 0.2', :source => 'https://gem.repo4'
+ gem 'bar', '~> 0.1', :source => 'https://gem.repo4'
+ G
+
+ # It should update foo to 0.2, but not the (locked) bar 0.1
+ expect(the_bundle).to include_gems("foo 0.2", "bar 0.1")
+ end
+ end
+
+ context "re-resolving" do
+ context "when there is a mix of sources in the gemfile" do
+ before do
+ build_repo3 do
+ build_gem "myrack"
+ end
+
+ build_lib "path1"
+ build_lib "path2"
+ build_git "git1"
+ build_git "git2"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ gem "rails"
+
+ source "https://gem.repo3" do
+ gem "myrack"
+ end
+
+ gem "path1", :path => "#{lib_path("path1-1.0")}"
+ gem "path2", :path => "#{lib_path("path2-1.0")}"
+ gem "git1", :git => "#{lib_path("git1-1.0")}"
+ gem "git2", :git => "#{lib_path("git2-1.0")}"
+ G
+ end
+
+ it "does not re-resolve" do
+ bundle :install, artifice: "compact_index", verbose: true
+ expect(out).to include("using resolution from the lockfile")
+ expect(out).not_to include("re-resolving dependencies")
+ end
+ end
+ end
+
+ context "when a gem is installed to system gems" do
+ before do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "and the gemfile changes" do
+ it "is still able to find that gem from remote sources" do
+ build_repo4 do
+ build_gem "myrack", "2.0.1.1.forked"
+ build_gem "thor", "0.19.1.1.forked"
+ end
+
+ # When this gemfile is installed...
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "myrack", "2.0.1.1.forked"
+ gem "thor"
+ end
+ gem "myrack-obama"
+ G
+
+ # Then we change the Gemfile by adding a version to thor
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "myrack", "2.0.1.1.forked"
+ gem "thor", "0.19.1.1.forked"
+ end
+ gem "myrack-obama"
+ G
+
+ # But we should still be able to find myrack 2.0.1.1.forked and install it
+ bundle :install, artifice: "compact_index"
+ end
+ end
+ end
+
+ describe "source changed to one containing a higher version of a dependency" do
+ before do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "bar"
+ end
+
+ build_lib("gemspec_test", path: tmp("gemspec_test")) do |s|
+ s.add_dependency "bar", "=1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo2"
+ gem "myrack"
+ gemspec :path => "#{tmp("gemspec_test")}"
+ G
+ end
+
+ it "conservatively installs the existing locked version" do
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+ end
+
+ context "when Gemfile overrides a gemspec development dependency to change the default source" do
+ before do
+ build_repo4 do
+ build_gem "bar"
+ end
+
+ build_lib("gemspec_test", path: tmp("gemspec_test")) do |s|
+ s.add_development_dependency "bar"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "bar"
+ end
+
+ gemspec :path => "#{tmp("gemspec_test")}"
+ G
+ end
+
+ it "does not print warnings" do
+ expect(err).to be_empty
+ end
+ end
+
+ it "doesn't update version when a gem uses a source block but a higher version from another source is already installed locally" do
+ build_repo2 do
+ build_gem "example", "0.1.0"
+ end
+
+ build_repo4 do
+ build_gem "example", "1.0.2"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo4"
+
+ gem "example", :source => "https://gem.repo2"
+ G
+
+ bundle "info example"
+ expect(out).to include("example (0.1.0)")
+
+ system_gems "example-1.0.2", path: default_bundle_path, gem_repo: gem_repo4
+
+ bundle "update example --verbose", artifice: "compact_index"
+ expect(out).not_to include("Using example 1.0.2")
+ expect(out).to include("Using example 0.1.0")
+ end
+
+ it "fails immediately with a helpful error when a rubygems source does not exist and bundler/setup is required" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "example"
+ end
+ G
+
+ ruby <<~R, raise_on_error: false
+ require 'bundler/setup'
+ R
+
+ expect(last_command).to be_failure
+ expect(err).to include("Could not find gem 'example' in locally installed gems.")
+ end
+
+ it "fails immediately with a helpful error when a non retriable network error happens while resolving sources" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ source "https://gem.repo4" do
+ gem "example"
+ end
+ G
+
+ bundle "install", artifice: nil, raise_on_error: false
+
+ expect(last_command).to be_failure
+ expect(err).to include("Could not reach host gem.repo4. Check your network connection and try again.")
+ end
+
+ context "when an indirect dependency is available from multiple ambiguous sources" do
+ it "raises, suggesting a source block" do
+ build_repo4 do
+ build_gem "depends_on_myrack" do |s|
+ s.add_dependency "myrack"
+ end
+ build_gem "myrack"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra_api", raise_on_error: false
+ source "https://global.source"
+
+ source "https://scoped.source/extra" do
+ gem "depends_on_myrack"
+ end
+
+ source "https://scoped.source" do
+ gem "thin"
+ end
+ G
+ expect(last_command).to be_failure
+ expect(err).to eq <<~EOS.strip
+ The gem 'myrack' was found in multiple relevant sources.
+ * rubygems repository https://scoped.source/
+ * rubygems repository https://scoped.source/extra/
+ You must add this gem to the source block for the source you wish it to be installed from.
+ EOS
+ expect(the_bundle).not_to be_locked
+ end
+ end
+
+ context "when default source includes old gems with nil required_ruby_version" do
+ before do
+ build_repo2 do
+ build_gem "ruport", "1.7.0.3" do |s|
+ s.add_dependency "pdf-writer", "1.1.8"
+ end
+ end
+
+ build_repo gem_repo4 do
+ build_gem "pdf-writer", "1.1.8"
+ end
+
+ path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.instance_variable_set(:@required_ruby_version, nil)
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra"
+ G
+ end
+
+ it "handles that fine" do
+ bundle "install", artifice: "compact_index_extra"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "pdf-writer", "1.1.8"
+ c.checksum gem_repo2, "ruport", "1.7.0.3"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ pdf-writer (1.1.8)
+
+ GEM
+ remote: https://gem.repo4/extra/
+ specs:
+ ruport (1.7.0.3)
+ pdf-writer (= 1.1.8)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruport (= 1.7.0.3)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when default source includes old gems with nil required_rubygems_version" do
+ before do
+ build_repo2 do
+ build_gem "ruport", "1.7.0.3" do |s|
+ s.add_dependency "pdf-writer", "1.1.8"
+ end
+ end
+
+ build_repo gem_repo4 do
+ build_gem "pdf-writer", "1.1.8"
+ end
+
+ path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.instance_variable_set(:@required_rubygems_version, nil)
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ruport", "= 1.7.0.3", :source => "https://gem.repo4/extra"
+ G
+ end
+
+ it "handles that fine" do
+ bundle "install", artifice: "compact_index_extra"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "pdf-writer", "1.1.8"
+ c.checksum gem_repo2, "ruport", "1.7.0.3"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ pdf-writer (1.1.8)
+
+ GEM
+ remote: https://gem.repo4/extra/
+ specs:
+ ruport (1.7.0.3)
+ pdf-writer (= 1.1.8)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruport (= 1.7.0.3)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when default source uses the old API and includes old gems with nil required_rubygems_version" do
+ before do
+ build_repo4 do
+ build_gem "pdf-writer", "1.1.8"
+ end
+
+ path = "#{gem_repo4}/#{Gem::MARSHAL_SPEC_DIR}/pdf-writer-1.1.8.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.instance_variable_set(:@required_rubygems_version, nil)
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "pdf-writer", "= 1.1.8"
+ G
+ end
+
+ it "handles that fine" do
+ bundle "install --verbose", artifice: "endpoint"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "pdf-writer", "1.1.8"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ pdf-writer (1.1.8)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ pdf-writer (= 1.1.8)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when mistakenly adding a top level gem already depended on and cached under the wrong source" do
+ before do
+ build_repo4 do
+ build_gem "some_private_gem", "0.1.0" do |s|
+ s.add_dependency "example", "~> 1.0"
+ end
+ end
+
+ build_repo2 do
+ build_gem "example", "1.0.0"
+ end
+
+ install_gemfile <<~G, artifice: "compact_index"
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "some_private_gem"
+ end
+ G
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ source "https://gem.repo4" do
+ gem "some_private_gem"
+ gem "example" # MISTAKE, example is not available at gem.repo4
+ end
+ G
+ end
+
+ it "shows a proper error message and does not generate a corrupted lockfile" do
+ expect do
+ bundle :install, artifice: "compact_index", raise_on_error: false, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ end.not_to change { lockfile }
+
+ expect(err).to include("Could not find gem 'example' in rubygems repository https://gem.repo4/")
+ end
+ end
+
+ context "when a gem has versions in two sources, but only the locked one has updates" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://main.source/
+ specs:
+ activesupport (1.0)
+ bigdecimal
+ bigdecimal (1.0.0)
+
+ GEM
+ remote: https://main.source/extra/
+ specs:
+ foo (1.0)
+ bigdecimal
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo3 do
+ build_gem "activesupport" do |s|
+ s.add_dependency "bigdecimal"
+ end
+
+ build_gem "bigdecimal", "1.0.0"
+ build_gem "bigdecimal", "3.3.1"
+ end
+
+ build_repo4 do
+ build_gem "foo" do |s|
+ s.add_dependency "bigdecimal"
+ end
+
+ build_gem "bigdecimal", "1.0.0"
+ end
+
+ gemfile <<~G
+ source "https://main.source"
+
+ gem "activesupport"
+
+ source "https://main.source/extra" do
+ gem "foo"
+ end
+ G
+
+ lockfile original_lockfile
+ end
+
+ it "properly upgrades the lockfile when updating that specific gem" do
+ bundle "update bigdecimal --conservative", artifice: "compact_index_extra_api", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo3.to_s }
+
+ expect(lockfile).to eq original_lockfile.gsub("bigdecimal (1.0.0)", "bigdecimal (3.3.1)")
+ end
+ end
+
+ context "when switching a gem with components from rubygems to git source" do
+ before do
+ build_repo2 do
+ build_gem "rails", "7.0.0" do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+ build_gem "actionpack", "7.0.0"
+ build_gem "activerecord", "7.0.0"
+ # propshaft also depends on actionpack, creating the conflict
+ build_gem "propshaft", "1.0.0" do |s|
+ s.add_dependency "actionpack", ">= 7.0.0"
+ end
+ end
+
+ build_git "rails", "7.0.0", path: lib_path("rails") do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+
+ build_git "actionpack", "7.0.0", path: lib_path("rails")
+ build_git "activerecord", "7.0.0", path: lib_path("rails")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "7.0.0"
+ gem "propshaft"
+ G
+ end
+
+ it "moves component gems to the git source in the lockfile" do
+ expect(lockfile).to include("remote: https://gem.repo2")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+ expect(lockfile).to include("propshaft (1.0.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", git: "#{lib_path("rails")}"
+ gem "propshaft"
+ G
+
+ bundle "install"
+
+ expect(lockfile).to include("remote: #{lib_path("rails")}")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+
+ # Component gems should NOT remain in the GEM section
+ # Extract just the GEM section by splitting on GIT first, then GEM
+ gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0]
+ expect(gem_section).not_to include("actionpack (7.0.0)")
+ expect(gem_section).not_to include("activerecord (7.0.0)")
+ end
+ end
+
+ context "when switching a gem with components from rubygems to path source" do
+ before do
+ build_repo2 do
+ build_gem "rails", "7.0.0" do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+ build_gem "actionpack", "7.0.0"
+ build_gem "activerecord", "7.0.0"
+ # propshaft also depends on actionpack, creating the conflict
+ build_gem "propshaft", "1.0.0" do |s|
+ s.add_dependency "actionpack", ">= 7.0.0"
+ end
+ end
+
+ build_lib "rails", "7.0.0", path: lib_path("rails") do |s|
+ s.add_dependency "actionpack", "7.0.0"
+ s.add_dependency "activerecord", "7.0.0"
+ end
+
+ build_lib "actionpack", "7.0.0", path: lib_path("rails")
+ build_lib "activerecord", "7.0.0", path: lib_path("rails")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "7.0.0"
+ gem "propshaft"
+ G
+ end
+
+ it "moves component gems to the path source in the lockfile" do
+ expect(lockfile).to include("remote: https://gem.repo2")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+ expect(lockfile).to include("propshaft (1.0.0)")
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", path: "#{lib_path("rails")}"
+ gem "propshaft"
+ G
+
+ bundle "install"
+
+ expect(lockfile).to include("remote: #{lib_path("rails")}")
+ expect(lockfile).to include("rails (7.0.0)")
+ expect(lockfile).to include("actionpack (7.0.0)")
+ expect(lockfile).to include("activerecord (7.0.0)")
+
+ # Component gems should NOT remain in the GEM section
+ # Extract just the GEM section by splitting appropriately
+ gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0]
+ expect(gem_section).not_to include("actionpack (7.0.0)")
+ expect(gem_section).not_to include("activerecord (7.0.0)")
+ end
+ end
+
+ context "when a scoped rubygems source is missing a transitive dependency" do
+ before do
+ build_repo2 do
+ build_gem "fallback_dep", "1.0.0"
+ build_gem "foo", "1.0.0"
+ end
+
+ build_repo3 do
+ build_gem "private_parent", "1.0.0" do |s|
+ s.add_dependency "fallback_dep"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+
+ source "https://gem.repo3" do
+ gem "private_parent", "1.0.0"
+ end
+ G
+
+ bundle :install, artifice: "compact_index"
+ end
+
+ it "falls back to the default rubygems source for that dependency" do
+ build_repo2 do
+ build_gem "foo", "2.0.0"
+ end
+
+ system_gems []
+
+ bundle "update foo", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("private_parent 1.0.0", "fallback_dep 1.0.0", "foo 2.0.0")
+ expect(the_bundle).to include_gems("private_parent 1.0.0", source: "remote3")
+ expect(the_bundle).to include_gems("fallback_dep 1.0.0", source: "remote2")
+ end
+ end
+
+ context "when a path gem has a transitive dependency that does not exist in the path source" do
+ before do
+ build_repo2 do
+ build_gem "missing_dep", "1.0.0"
+ build_gem "foo", "1.0.0"
+ end
+
+ build_lib "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s|
+ s.add_dependency "missing_dep"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+
+ gem "parent_gem", path: "#{lib_path("parent_gem")}"
+ G
+
+ bundle :install, artifice: "compact_index"
+ end
+
+ it "falls back to the default rubygems source for that dependency when updating" do
+ build_repo2 do
+ build_gem "foo", "2.0.0"
+ end
+
+ system_gems []
+
+ bundle "update foo", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0")
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "path@#{lib_path("parent_gem")}")
+ expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2")
+ end
+ end
+
+ context "when a git gem has a transitive dependency that does not exist in the git source" do
+ before do
+ build_repo2 do
+ build_gem "missing_dep", "1.0.0"
+ build_gem "foo", "1.0.0"
+ end
+
+ build_git "parent_gem", "1.0.0", path: lib_path("parent_gem") do |s|
+ s.add_dependency "missing_dep"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "foo"
+
+ gem "parent_gem", git: "#{lib_path("parent_gem")}"
+ G
+
+ bundle :install, artifice: "compact_index"
+ end
+
+ it "falls back to the default rubygems source for that dependency when updating" do
+ build_repo2 do
+ build_gem "foo", "2.0.0"
+ end
+
+ system_gems []
+
+ bundle "update foo", artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", "missing_dep 1.0.0", "foo 2.0.0")
+ expect(the_bundle).to include_gems("parent_gem 1.0.0", source: "git@#{lib_path("parent_gem")}")
+ expect(the_bundle).to include_gems("missing_dep 1.0.0", source: "remote2")
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb
new file mode 100644
index 0000000000..97b1d233bf
--- /dev/null
+++ b/spec/bundler/install/gemfile/specific_platform_spec.rb
@@ -0,0 +1,1973 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with specific platforms" do
+ let(:google_protobuf) { <<-G }
+ source "https://gem.repo2"
+ gem "google-protobuf"
+ G
+
+ it "locks to the specific darwin platform" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ install_gemfile(google_protobuf)
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ expect(the_bundle.locked_platforms).to include("universal-darwin")
+ expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(
+ "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin"
+ )
+ end
+ end
+
+ it "still installs the platform specific variant when locked only to ruby, and the platform specific variant has different dependencies" do
+ simulate_platform "x86_64-darwin-15" do
+ build_repo4 do
+ build_gem("sass-embedded", "1.72.0") do |s|
+ s.add_dependency "rake"
+ end
+
+ build_gem("sass-embedded", "1.72.0") do |s|
+ s.platform = "x86_64-darwin-15"
+ end
+
+ build_gem "rake"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sass-embedded"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rake (1.0)
+ sass-embedded (1.72.0)
+ rake
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sass-embedded
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+ expect(out).to include("Installing sass-embedded 1.72.0 (x86_64-darwin-15)")
+
+ expect(the_bundle).to include_gem("sass-embedded 1.72.0 x86_64-darwin-15")
+ end
+ end
+
+ it "understands that a non-platform specific gem in a old lockfile doesn't necessarily mean installing the non-specific variant" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+
+ # Consistent location to install and look for gems
+ bundle_config "path vendor/bundle"
+
+ install_gemfile(google_protobuf)
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ google-protobuf (3.0.0.alpha.5.0.5.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ google-protobuf
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ # force strict usage of the lockfile by setting frozen mode
+ bundle_config "frozen true"
+
+ # make sure the platform that got actually installed with the old bundler is used
+ expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin")
+ end
+ end
+
+ it "understands that a non-platform specific gem in a new lockfile locked only to ruby doesn't necessarily mean installing the non-specific variant" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+
+ # Consistent location to install and look for gems
+ bundle_config "path vendor/bundle"
+
+ gemfile google_protobuf
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.4.0"
+ end
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ google-protobuf (3.0.0.alpha.4.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ google-protobuf
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+
+ checksums.checksum gem_repo2, "google-protobuf", "3.0.0.alpha.5.0.5.1"
+
+ # make sure the platform that the platform specific dependency is used, since we're only locked to ruby
+ expect(the_bundle).to include_gem("google-protobuf 3.0.0.alpha.5.0.5.1 universal-darwin")
+
+ # make sure we're still only locked to ruby
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ google-protobuf (3.0.0.alpha.5.0.5.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ google-protobuf
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "when running on a legacy lockfile locked only to ruby" do
+ # Exercises the legacy lockfile path (use_exact_resolved_specifications? = false)
+ # because most_specific_locked_platform is ruby, matching the generic platform.
+ # Key insight: when target (arm64-darwin-22) != platform (ruby), the code tries
+ # both platforms before falling back, preserving lockfile integrity.
+
+ around do |example|
+ build_repo4 do
+ build_gem "nokogiri", "1.3.10"
+ build_gem "nokogiri", "1.3.10" do |s|
+ s.platform = "arm64-darwin"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.3.10)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-22", &example
+ end
+
+ it "still installs the generic ruby variant if necessary" do
+ bundle "install"
+ expect(the_bundle).to include_gem("nokogiri 1.3.10")
+ expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
+ end
+
+ it "still installs the generic ruby variant if necessary, even in frozen mode" do
+ bundle "install", env: { "BUNDLE_FROZEN" => "true" }
+ expect(the_bundle).to include_gem("nokogiri 1.3.10")
+ expect(the_bundle).not_to include_gem("nokogiri 1.3.10 arm64-darwin")
+ end
+ end
+
+ context "when platform-specific gem has incompatible required_ruby_version" do
+ # Key insight: candidate_platforms tries [target, platform, ruby] in order.
+ # Ruby platform is last since it requires compilation, but works when
+ # precompiled gems are incompatible with the current Ruby version.
+ #
+ # Note: This fix requires the lockfile to include both ruby and platform-
+ # specific variants (typical after `bundle lock --add-platform`). If the
+ # lockfile only has platform-specific gems, frozen mode cannot help because
+ # Bundler.setup would still expect the locked (incompatible) gem.
+
+ # Exercises the exact spec path (use_exact_resolved_specifications? = true)
+ # because lockfile has platform-specific entry as most_specific_locked_platform
+ it "falls back to ruby platform in frozen mode when lockfile includes both variants" do
+ build_repo4 do
+ build_gem "nokogiri", "1.18.10"
+ build_gem "nokogiri", "1.18.10" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ # Lockfile has both ruby and platform-specific gem (typical after `bundle lock --add-platform`)
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.10)
+ nokogiri (1.18.10-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install", env: { "BUNDLE_FROZEN" => "true" }
+ expect(the_bundle).to include_gem("nokogiri 1.18.10")
+ expect(the_bundle).not_to include_gem("nokogiri 1.18.10 x86_64-linux")
+ end
+ end
+ end
+
+ it "doesn't discard previously installed platform specific gem and fall back to ruby on subsequent bundles" do
+ simulate_platform "x86_64-darwin-15" do
+ build_repo2 do
+ build_gem("libv8", "8.4.255.0")
+ build_gem("libv8", "8.4.255.0") {|s| s.platform = "universal-darwin" }
+
+ build_gem("mini_racer", "1.0.0") do |s|
+ s.add_dependency "libv8"
+ end
+ end
+
+ # Consistent location to install and look for gems
+ bundle_config "path vendor/bundle"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "libv8"
+ G
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ libv8 (8.4.255.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+ expect(out).to include("Installing libv8 8.4.255.0 (universal-darwin)")
+
+ bundle "add mini_racer --verbose"
+ expect(out).to include("Using libv8 8.4.255.0 (universal-darwin)")
+ end
+ end
+
+ it "chooses platform specific gems even when resolving upon materialization and the API returns more specific platforms first" do
+ simulate_platform "x86_64-darwin-15" do
+ build_repo4 do
+ build_gem("grpc", "1.50.0")
+ build_gem("grpc", "1.50.0") {|s| s.platform = "universal-darwin" }
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "grpc"
+ G
+
+ # simulate lockfile created with old bundler, which only locks for ruby platform
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ grpc (1.50.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ grpc
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose", artifice: "compact_index_precompiled_before"
+ expect(err).to include("The following platform specific gems are getting installed, yet the lockfile includes only their generic ruby version")
+ expect(out).to include("Installing grpc 1.50.0 (universal-darwin)")
+ end
+ end
+
+ it "caches the universal-darwin gem when --all-platforms is passed and properly picks it up on further bundler invocations" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ gemfile(google_protobuf)
+ bundle "cache --all-platforms"
+ expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist
+
+ bundle "install --verbose"
+ expect(err).to be_empty
+ end
+ end
+
+ it "caches the universal-darwin gem when cache_all_platforms is configured and properly picks it up on further bundler invocations" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ gemfile(google_protobuf)
+ bundle_config "cache_all_platforms true"
+ bundle "cache"
+ expect(cached_gem("google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin")).to exist
+
+ bundle "install --verbose"
+ expect(err).to be_empty
+ end
+ end
+
+ it "caches multiplatform git gems with a single gemspec when --all-platforms is passed" do
+ git = build_git "pg_array_parser", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "pg_array_parser", :git => "#{lib_path("pg_array_parser-1.0")}"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("pg_array_parser-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ pg_array_parser (1.0-java)
+ pg_array_parser (1.0)
+
+ GEM
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms("java")}
+
+ DEPENDENCIES
+ pg_array_parser!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "cache --all-platforms"
+
+ expect(err).to be_empty
+ end
+
+ it "uses the platform-specific gem with extra dependencies" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem_with_different_dependencies_per_platform
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "facter"
+ G
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ expect(the_bundle.locked_platforms).to include("universal-darwin")
+ expect(the_bundle).to include_gems("facter 2.4.6 universal-darwin", "CFPropertyList 1.0")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include("CFPropertyList-1.0",
+ "facter-2.4.6-universal-darwin")
+ end
+ end
+
+ context "when adding a platform via lock --add_platform" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "adds the foreign platform" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ install_gemfile(google_protobuf)
+ bundle "lock --add-platform=x64-mingw-ucrt"
+
+ expect(the_bundle.locked_platforms).to include("x64-mingw-ucrt", "universal-darwin")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(*%w[
+ google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin
+ google-protobuf-3.0.0.alpha.5.0.5.1-x64-mingw-ucrt
+ ])
+ end
+ end
+
+ it "falls back on plain ruby when that version doesn't have a platform-specific gem" do
+ simulate_platform "x86_64-darwin-15" do
+ setup_multiplatform_gem
+ install_gemfile(google_protobuf)
+ bundle "lock --add-platform=java"
+
+ expect(the_bundle.locked_platforms).to include("java", "universal-darwin")
+ expect(the_bundle.locked_gems.specs.map(&:full_name)).to include(
+ "google-protobuf-3.0.0.alpha.5.0.5.1",
+ "google-protobuf-3.0.0.alpha.5.0.5.1-universal-darwin"
+ )
+ end
+ end
+ end
+
+ it "installs sorbet-static, which does not provide a pure ruby variant, in absence of a lockfile, just fine", :truffleruby do
+ skip "does not apply to Windows" if Gem.win_platform?
+
+ build_repo2 do
+ build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "sorbet-static", "0.5.6403"
+ G
+
+ bundle "install --verbose"
+ end
+
+ it "installs sorbet-static, which does not provide a pure ruby variant, in presence of a lockfile, just fine", :truffleruby do
+ skip "does not apply to Windows" if Gem.win_platform?
+
+ build_repo2 do
+ build_gem("sorbet-static", "0.5.6403") {|s| s.platform = Bundler.local_platform }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "sorbet-static", "0.5.6403"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ sorbet-static (0.5.6403-#{Bundler.local_platform})
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.6403)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ end
+
+ it "does not resolve if the current platform does not match any of available platform specific variants for a top level dependency" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" }
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "0.5.6433"
+ G
+
+ error_message = <<~ERROR.strip
+ Could not find gem 'sorbet-static (= 0.5.6433)' with platform 'arm64-darwin-21' in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.6433)':
+ * sorbet-static-0.5.6433-universal-darwin-20
+ * sorbet-static-0.5.6433-x86_64-linux
+ ERROR
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+
+ # Make sure it doesn't print error twice in verbose mode
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock --verbose", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+ end
+
+ it "shows a platform mismatch hint when the current platform is not in the lockfile's platforms" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux-musl" }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "0.5.6433"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.6433-x86_64-linux-musl)
+
+ PLATFORMS
+ x86_64-linux-musl
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.6433)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-linux" do
+ bundle "install", raise_on_error: false
+ end
+
+ expect(err).to include("Your current platform (x86_64-linux) is not included in the lockfile's platforms (x86_64-linux-musl)")
+ expect(err).to include("bundle lock --add-platform x86_64-linux")
+ end
+
+ it "does not resolve if the current platform does not match any of available platform specific variants for a transitive dependency" do
+ build_repo4 do
+ build_gem("sorbet", "0.5.6433") {|s| s.add_dependency "sorbet-static", "= 0.5.6433" }
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux" }
+ build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "universal-darwin-20" }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet", "0.5.6433"
+ G
+
+ error_message = <<~ERROR.strip
+ Could not find compatible versions
+
+ Because every version of sorbet depends on sorbet-static = 0.5.6433
+ and sorbet-static = 0.5.6433 could not be found in rubygems repository https://gem.repo4/ or installed locally for any resolution platforms (arm64-darwin-21),
+ sorbet cannot be used.
+ So, because Gemfile depends on sorbet = 0.5.6433,
+ version solving has failed.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.6433)':
+ * sorbet-static-0.5.6433-universal-darwin-20
+ * sorbet-static-0.5.6433-x86_64-linux
+ ERROR
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+
+ # Make sure it doesn't print error twice in verbose mode
+
+ simulate_platform "arm64-darwin-21" do
+ bundle "lock --verbose", raise_on_error: false
+ end
+
+ expect(err).to include(error_message).once
+ end
+
+ it "does not generate a lockfile if ruby platform is forced and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.9889") {|s| s.platform = Gem::Platform.local }
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "0.5.9889"
+ G
+
+ bundle "lock", raise_on_error: false, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "true" }
+
+ expect(err).to include <<~ERROR.rstrip
+ Could not find gem 'sorbet-static (= 0.5.9889)' with platform 'ruby' in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.9889)':
+ * sorbet-static-0.5.9889-#{Gem::Platform.local}
+ ERROR
+ end
+
+ it "automatically fixes the lockfile if ruby platform is locked and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem("sorbet-static-and-runtime", "0.5.10160") do |s|
+ s.add_dependency "sorbet", "= 0.5.10160"
+ s.add_dependency "sorbet-runtime", "= 0.5.10160"
+ end
+
+ build_gem("sorbet", "0.5.10160") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.10160"
+ end
+
+ build_gem("sorbet-runtime", "0.5.10160")
+
+ build_gem("sorbet-static", "0.5.10160") do |s|
+ s.platform = Gem::Platform.local
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static-and-runtime"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "sorbet", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-runtime", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local
+ c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile if both ruby platform and a more specific platform are locked, and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem "nokogiri", "1.12.0"
+ build_gem "nokogiri", "1.12.0" do |s|
+ s.platform = "x86_64-darwin"
+ end
+
+ build_gem "nokogiri", "1.13.0"
+ build_gem "nokogiri", "1.13.0" do |s|
+ s.platform = "x86_64-darwin"
+ end
+
+ build_gem("sorbet-static", "0.5.10601") do |s|
+ s.platform = "x86_64-darwin"
+ end
+ end
+
+ simulate_platform "x86_64-darwin-22" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+ end
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.13.0", "x86_64-darwin"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10601", "x86_64-darwin"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.12.0)
+ nokogiri (1.12.0-x86_64-darwin)
+ sorbet-static (0.5.10601-x86_64-darwin)
+
+ PLATFORMS
+ ruby
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "x86_64-darwin-22" do
+ bundle "update --conservative nokogiri"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.13.0-x86_64-darwin)
+ sorbet-static (0.5.10601-x86_64-darwin)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile if only ruby platform is locked and some gem has no ruby variant available" do
+ build_repo4 do
+ build_gem("sorbet-static-and-runtime", "0.5.10160") do |s|
+ s.add_dependency "sorbet", "= 0.5.10160"
+ s.add_dependency "sorbet-runtime", "= 0.5.10160"
+ end
+
+ build_gem("sorbet", "0.5.10160") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.10160"
+ end
+
+ build_gem("sorbet-runtime", "0.5.10160")
+
+ build_gem("sorbet-static", "0.5.10160") do |s|
+ s.platform = Gem::Platform.local
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static-and-runtime"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "sorbet", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-runtime", "0.5.10160"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10160", Gem::Platform.local
+ c.checksum gem_repo4, "sorbet-static-and-runtime", "0.5.10160"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-runtime (0.5.10160)
+ sorbet-static (0.5.10160-#{Gem::Platform.local})
+ sorbet-static-and-runtime (0.5.10160)
+ sorbet (= 0.5.10160)
+ sorbet-runtime (= 0.5.10160)
+
+ PLATFORMS
+ #{local_platform}
+
+ DEPENDENCIES
+ sorbet-static-and-runtime
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when adding a gem that introduces dependencies with no ruby platform variants transitively" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.18.2"
+
+ build_gem "nokogiri", "1.18.2" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem("sorbet", "0.5.11835") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.11835"
+ end
+
+ build_gem "sorbet-static", "0.5.11835" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.2)
+ nokogiri (1.18.2-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.18.2", "x86_64-linux"
+ c.checksum gem_repo4, "sorbet", "0.5.11835"
+ c.checksum gem_repo4, "sorbet-static", "0.5.11835", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.18.2)
+ nokogiri (1.18.2-x86_64-linux)
+ sorbet (0.5.11835)
+ sorbet-static (= 0.5.11835)
+ sorbet-static (0.5.11835-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile if multiple platforms locked, but no valid versions of direct dependencies for all of them" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "arm-linux"
+ end
+
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-arm-linux)
+ nokogiri (1.14.0-x86_64-linux)
+ sorbet-static (0.5.10696-x86_64-linux)
+
+ PLATFORMS
+ aarch64-linux
+ arm-linux
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "update"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-x86_64-linux)
+ sorbet-static (0.5.10696-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile without removing other variants if it's missing platform gems, but they are installed locally" do
+ simulate_platform "x86_64-darwin-21" do
+ build_repo4 do
+ build_gem("sorbet-static", "0.5.10549") do |s|
+ s.platform = "universal-darwin-20"
+ end
+
+ build_gem("sorbet-static", "0.5.10549") do |s|
+ s.platform = "universal-darwin-21"
+ end
+ end
+
+ # Make sure sorbet-static-0.5.10549-universal-darwin-21 is installed
+ install_gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet-static", "= 0.5.10549"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20"
+ c.checksum gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21"
+ end
+
+ # Make sure the lockfile is missing sorbet-static-0.5.10549-universal-darwin-21
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.10549-universal-darwin-20)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.10549)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet-static (0.5.10549-universal-darwin-20)
+ sorbet-static (0.5.10549-universal-darwin-21)
+
+ PLATFORMS
+ x86_64-darwin
+
+ DEPENDENCIES
+ sorbet-static (= 0.5.10549)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile if locked only to ruby, and some locked specs don't meet locked dependencies" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem("ibandit", "0.7.0") do |s|
+ s.add_dependency "i18n", "~> 0.7.0"
+ end
+
+ build_gem("i18n", "0.7.0.beta1")
+ build_gem("i18n", "0.7.0")
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ibandit", "~> 0.7.0"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ i18n (0.7.0.beta1)
+ ibandit (0.7.0)
+ i18n (~> 0.7.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ ibandit (~> 0.7.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock --update i18n"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ i18n (0.7.0)
+ ibandit (0.7.0)
+ i18n (~> 0.7.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ ibandit (~> 0.7.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile" do
+ build_repo4 do
+ build_gem "nokogiri", "1.13.8"
+ build_gem "nokogiri", "1.13.8" do |s|
+ s.platform = Gem::Platform.local
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+
+ gem "tzinfo", "~> 1.2", platform: :#{not_local_tag}
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.13.8"
+ c.checksum gem_repo4, "nokogiri", "1.13.8", Gem::Platform.local
+ end
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.13.8)
+ nokogiri (1.13.8-#{Gem::Platform.local})
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ nokogiri
+ tzinfo (~> 1.2)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ bundle "lock --update"
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+
+ it "does not remove ruby if gems for other platforms, and not present in the lockfile, exist in the Gemfile, and the lockfile only has ruby" do
+ build_repo4 do
+ build_gem "nokogiri", "1.13.8"
+ build_gem "nokogiri", "1.13.8" do |s|
+ s.platform = "arm64-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+
+ gem "tzinfo", "~> 1.2", platforms: %i[windows jruby]
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.13.8"
+ end
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.13.8)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+ tzinfo (~> 1.2)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "arm64-darwin-23" do
+ bundle "lock --update"
+ end
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+
+ it "does not remove ruby when adding a new gem to the Gemfile" do
+ build_repo4 do
+ build_gem "concurrent-ruby", "1.2.2"
+ build_gem "myrack", "3.0.7"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "concurrent-ruby"
+ gem "myrack"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "concurrent-ruby", "1.2.2"
+ c.checksum gem_repo4, "myrack", "3.0.7"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ concurrent-ruby (1.2.2)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ concurrent-ruby
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ concurrent-ruby (1.2.2)
+ myrack (3.0.7)
+
+ PLATFORMS
+ #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])}
+
+ DEPENDENCIES
+ concurrent-ruby
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "can fallback to a source gem when platform gems are incompatible with current ruby version" do
+ setup_multiplatform_gem_with_source_gem
+
+ gemfile <<~G
+ source "https://gem.repo2"
+
+ gem "my-precompiled-gem"
+ G
+
+ # simulate lockfile which includes both a precompiled gem with:
+ # - Gem the current platform (with incompatible ruby version)
+ # - A source gem with compatible ruby version
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ my-precompiled-gem (3.0.0)
+ my-precompiled-gem (3.0.0-#{Bundler.local_platform})
+
+ PLATFORMS
+ ruby
+ #{Bundler.local_platform}
+
+ DEPENDENCIES
+ my-precompiled-gem
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+ end
+
+ it "automatically adds the ruby variant to the lockfile if the specific platform is locked and we move to a newer ruby version for which a native package is not available" do
+ #
+ # Given an existing application using native gems (e.g., nokogiri)
+ # And a lockfile generated with a stable ruby version
+ # When want test the application against ruby-head and `bundle install`
+ # Then bundler should fall back to the generic ruby platform gem
+ #
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0"
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri", "1.14.0"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri (= 1.14.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle :install
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0)
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri (= 1.14.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "automatically fixes the lockfile when only ruby platform locked, and adding a dependency with subdependencies not valid for ruby" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem("sorbet", "0.5.10160") do |s|
+ s.add_dependency "sorbet-static", "= 0.5.10160"
+ end
+
+ build_gem("sorbet-static", "0.5.10160") do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "sorbet"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10160)
+ sorbet-static (= 0.5.10160)
+ sorbet-static (0.5.10160-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ sorbet
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "locks specific platforms automatically" do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0"
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "arm-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x64-mingw-ucrt"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "universal-darwin-22"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.14.0"
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "arm-linux"
+ c.checksum gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
+ end
+
+ # locks all compatible platforms, excluding Java and Windows
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0)
+ nokogiri (1.14.0-arm-linux)
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ arm-linux
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+
+ FileUtils.rm bundled_app_lock
+
+ bundle "lock"
+
+ checksums.delete "nokogiri", "arm-linux"
+ checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "universal-darwin-22"
+ checksums.checksum gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux"
+
+ # locks only platforms compatible with all gems in the bundle
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0)
+ nokogiri (1.14.0-x86_64-linux)
+ sorbet-static (0.5.10696-universal-darwin-22)
+ sorbet-static (0.5.10696-x86_64-linux)
+
+ PLATFORMS
+ universal-darwin-22
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sorbet-static
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not fail when a platform variant is incompatible with the current ruby and another equivalent platform specific variant is part of the resolution" do
+ build_repo4 do
+ build_gem "nokogiri", "1.15.5"
+
+ build_gem "nokogiri", "1.15.5" do |s|
+ s.platform = "x86_64-linux"
+ s.required_ruby_version = "< #{current_ruby_minor}.dev"
+ end
+
+ build_gem "sass-embedded", "1.69.5"
+
+ build_gem "sass-embedded", "1.69.5" do |s|
+ s.platform = "x86_64-linux-gnu"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sass-embedded"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.15.5"
+ c.checksum gem_repo4, "sass-embedded", "1.69.5"
+ c.checksum gem_repo4, "sass-embedded", "1.69.5", "x86_64-linux-gnu"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+
+ # locks all compatible platforms, excluding Java and Windows
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.15.5)
+ sass-embedded (1.69.5)
+ sass-embedded (1.69.5-x86_64-linux-gnu)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ sass-embedded
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not add ruby platform gem if it brings extra dependencies not resolved originally" do
+ build_repo4 do
+ build_gem "nokogiri", "1.15.5" do |s|
+ s.add_dependency "mini_portile2", "~> 2.8.2"
+ end
+
+ build_gem "nokogiri", "1.15.5" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "nokogiri", "1.15.5", "x86_64-linux"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.15.5-x86_64-linux)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ ["x86_64-linux", "x86_64-linux-musl"].each do |host_platform|
+ describe "on host platform #{host_platform}" do
+ it "adds current musl platform" do
+ build_repo4 do
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux-musl"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rcee_precompiled", "0.5.0"
+ G
+
+ simulate_platform host_platform do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux"
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rcee_precompiled (0.5.0-x86_64-linux)
+ rcee_precompiled (0.5.0-x86_64-linux-musl)
+
+ PLATFORMS
+ x86_64-linux
+ x86_64-linux-musl
+
+ DEPENDENCIES
+ rcee_precompiled (= 0.5.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+ end
+
+ it "adds current musl platform, when there are also gnu variants" do
+ build_repo4 do
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux-gnu"
+ end
+
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "x86_64-linux-musl"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rcee_precompiled", "0.5.0"
+ G
+
+ simulate_platform "x86_64-linux-musl" do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-gnu"
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "x86_64-linux-musl"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rcee_precompiled (0.5.0-x86_64-linux-gnu)
+ rcee_precompiled (0.5.0-x86_64-linux-musl)
+
+ PLATFORMS
+ x86_64-linux-gnu
+ x86_64-linux-musl
+
+ DEPENDENCIES
+ rcee_precompiled (= 0.5.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not add current platform if there's an equivalent less specific platform among the ones resolved" do
+ build_repo4 do
+ build_gem "rcee_precompiled", "0.5.0" do |s|
+ s.platform = "universal-darwin"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "rcee_precompiled", "0.5.0"
+ G
+
+ simulate_platform "x86_64-darwin-15" do
+ bundle "lock"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "rcee_precompiled", "0.5.0", "universal-darwin"
+ end
+
+ expect(lockfile).to eq(<<~L)
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ rcee_precompiled (0.5.0-universal-darwin)
+
+ PLATFORMS
+ universal-darwin
+
+ DEPENDENCIES
+ rcee_precompiled (= 0.5.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "does not re-resolve when a specific platform, but less specific than the current platform, is locked" do
+ build_repo4 do
+ build_gem "nokogiri"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.0)
+
+ PLATFORMS
+ arm64-darwin
+
+ DEPENDENCIES
+ nokogiri!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ simulate_platform "arm64-darwin-23" do
+ bundle "install --verbose"
+
+ expect(out).to include("Found no changes, using resolution from the lockfile")
+ end
+ end
+
+ it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem" do
+ build_repo4 do
+ build_gem "ffi"
+
+ build_gem "ffi" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "nokogiri"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ffi"
+ gem "nokogiri"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.0)
+ nokogiri (1.0)
+
+ PLATFORMS
+ x86_64-linux
+
+ DEPENDENCIES
+ ffi
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update nokogiri"
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+ end
+
+ it "does not remove generic platform gems locked for a specific platform from lockfile when unlocking an unrelated gem, and variants for other platform also locked" do
+ build_repo4 do
+ build_gem "ffi"
+
+ build_gem "ffi" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "ffi" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "nokogiri"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "ffi"
+ gem "nokogiri"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ ffi (1.0)
+ ffi (1.0-java)
+ nokogiri (1.0)
+
+ PLATFORMS
+ java
+ x86_64-linux
+
+ DEPENDENCIES
+ ffi
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update nokogiri"
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+ end
+
+ it "does not remove platform specific gems from lockfile when using a ruby version that does not match their ruby requirements, since they may be useful in other rubies" do
+ build_repo4 do
+ build_gem("google-protobuf", "3.25.5")
+ build_gem("google-protobuf", "3.25.5") do |s|
+ s.required_ruby_version = "< #{current_ruby_minor}.dev"
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "google-protobuf", "~> 3.0"
+ G
+
+ original_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ google-protobuf (3.25.5)
+ google-protobuf (3.25.5-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ google-protobuf (~> 3.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile original_lockfile
+
+ simulate_platform "x86_64-linux" do
+ bundle "lock --update"
+ end
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+
+ private
+
+ def setup_multiplatform_gem
+ build_repo2 do
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1")
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x86_64-linux" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "x64-mingw-ucrt" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5.1") {|s| s.platform = "universal-darwin" }
+
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x86_64-linux" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5") {|s| s.platform = "x64-mingw-ucrt" }
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.5")
+
+ build_gem("google-protobuf", "3.0.0.alpha.5.0.4") {|s| s.platform = "universal-darwin" }
+
+ build_gem("google-protobuf", "3.0.0.alpha.4.0")
+ build_gem("google-protobuf", "3.0.0.alpha.3.1.pre")
+ end
+ end
+
+ def setup_multiplatform_gem_with_different_dependencies_per_platform
+ build_repo2 do
+ build_gem("facter", "2.4.6")
+ build_gem("facter", "2.4.6") do |s|
+ s.platform = "universal-darwin"
+ s.add_dependency "CFPropertyList"
+ end
+ build_gem("CFPropertyList")
+ end
+ end
+
+ def setup_multiplatform_gem_with_source_gem
+ build_repo2 do
+ build_gem("my-precompiled-gem", "3.0.0")
+ build_gem("my-precompiled-gem", "3.0.0") do |s|
+ s.platform = Bundler.local_platform
+
+ # purposely unresolvable
+ s.required_ruby_version = ">= 1000.0.0"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gemfile_spec.rb b/spec/bundler/install/gemfile_spec.rb
new file mode 100644
index 0000000000..83875a3d0e
--- /dev/null
+++ b/spec/bundler/install/gemfile_spec.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "with duplicated gems" do
+ it "will display a warning" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo1"
+
+ gem 'rails', '~> 4.0.0'
+ gem 'rails', '~> 4.0.0'
+ G
+ expect(err).to include("more than once")
+ end
+ end
+
+ context "with --gemfile" do
+ it "finds the gemfile" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+
+ # Specify BUNDLE_GEMFILE for `the_bundle`
+ # to retrieve the proper Gemfile
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "respects lockfile and BUNDLE_LOCKFILE" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ lockfile "ReallyNotGemfile.lock"
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "respects BUNDLE_LOCKFILE during bundle install" do
+ ENV["BUNDLE_LOCKFILE"] = "ReallyNotGemfile.lock"
+
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+ expect(bundled_app("ReallyNotGemfile.lock")).to exist
+
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with gemfile set via config" do
+ before do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle_config "gemfile #{bundled_app("NotGemfile")}"
+ end
+ it "uses the gemfile to install" do
+ bundle "install"
+ bundle "list"
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+ it "uses the gemfile while in a subdirectory" do
+ bundled_app("subdir").mkpath
+ bundle "install", dir: bundled_app("subdir")
+ bundle "list", dir: bundled_app("subdir")
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+ end
+
+ it "reports that lib is an invalid option" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :lib => "myrack"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/You passed :lib as an option for gem 'myrack', but it is invalid/)
+ end
+
+ it "reports that type is an invalid option" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :type => "development"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/You passed :type as an option for gem 'myrack', but it is invalid/)
+ end
+
+ it "reports that gemfile is an invalid option" do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack", :gemfile => "foo"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/You passed :gemfile as an option for gem 'myrack', but it is invalid/)
+ end
+
+ context "when an internal error happens" do
+ let(:bundler_bug) do
+ create_file("bundler_bug.rb", <<~RUBY)
+ require "bundler"
+
+ module Bundler
+ class Dsl
+ def source(source, *args, &blk)
+ nil.name
+ end
+ end
+ end
+ RUBY
+
+ bundled_app("bundler_bug.rb").to_s
+ end
+
+ it "shows culprit file and line" do
+ skip "ruby-core test setup has always \"lib\" in $LOAD_PATH so `require \"bundler\"` always activates the local version rather than using RubyGems gem activation stuff, causing conflicts" if ruby_core?
+
+ install_gemfile "source 'https://gem.repo1'", requires: [bundler_bug], artifice: nil, raise_on_error: false
+ expect(err).to include("bundler_bug.rb:6")
+ end
+ end
+
+ context "with engine specified in symbol", :jruby_only do
+ it "does not raise any error parsing Gemfile" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}"
+ G
+
+ expect(out).to match(/Bundle complete!/)
+ end
+
+ it "installation succeeds" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ ruby "#{RUBY_VERSION}", :engine => :jruby, :engine_version => "#{RUBY_ENGINE_VERSION}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with a Gemfile containing non-US-ASCII characters" do
+ it "reads the Gemfile with the UTF-8 encoding by default" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ str = "Il était une fois ..."
+ puts "The source encoding is: " + str.encoding.name
+ G
+
+ expect(out).to include("The source encoding is: UTF-8")
+ expect(out).not_to include("The source encoding is: ASCII-8BIT")
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "respects the magic encoding comment" do
+ # NOTE: This works thanks to #eval interpreting the magic encoding comment
+ install_gemfile <<-G
+ # encoding: iso-8859-1
+ source "https://gem.repo1"
+
+ str = "Il #{"\xE9".dup.force_encoding("binary")}tait une fois ..."
+ puts "The source encoding is: " + str.encoding.name
+ G
+
+ expect(out).to include("The source encoding is: ISO-8859-1")
+ expect(out).to include("Bundle complete!")
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/compact_index_spec.rb b/spec/bundler/install/gems/compact_index_spec.rb
new file mode 100644
index 0000000000..9db73b84b5
--- /dev/null
+++ b/spec/bundler/install/gems/compact_index_spec.rb
@@ -0,0 +1,1012 @@
+# frozen_string_literal: true
+
+RSpec.describe "compact index api" do
+ let(:source_hostname) { "localgemserver.test" }
+ let(:source_uri) { "http://#{source_hostname}" }
+
+ it "should use the API" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "has a debug mode" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index", env: { "DEBUG_COMPACT_INDEX" => "true" }
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(err).to include("[Bundler::CompactIndexClient] available?")
+ expect(err).to include("[Bundler::CompactIndexClient] fetching versions")
+ expect(err).to include("[Bundler::CompactIndexClient] info(myrack)")
+ expect(err).to include("[Bundler::CompactIndexClient] fetching info/myrack")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should URI encode gem names" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem " sinatra"
+ G
+
+ bundle :install, artifice: "compact_index", raise_on_error: false
+ expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.")
+ end
+
+ it "should handle nested dependencies" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rails"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems(
+ "rails 2.3.2",
+ "actionpack 2.3.2",
+ "activerecord 2.3.2",
+ "actionmailer 2.3.2",
+ "activeresource 2.3.2",
+ "activesupport 2.3.2"
+ )
+ end
+
+ it "should handle case sensitivity conflicts" do
+ build_repo4(build_compact_index: false) do
+ build_gem "myrack", "1.0" do |s|
+ s.add_dependency("Myrack", "0.1")
+ end
+ build_gem "Myrack", "0.1"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem "myrack", "1.0"
+ gem "Myrack", "0.1"
+ G
+
+ # can't use `include_gems` here since the `require` will conflict on a
+ # case-insensitive FS
+ run "Bundler.require; puts Gem.loaded_specs.values_at('myrack', 'Myrack').map(&:full_name)"
+ expect(out).to eq("myrack-1.0\nMyrack-0.1")
+ end
+
+ it "should handle multiple gem dependencies on the same gem" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "net-sftp"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(the_bundle).to include_gems "net-sftp 1.1.1"
+ end
+
+ it "should use the endpoint when using deployment mode" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ bundle :install, artifice: "compact_index"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles git dependencies that are in rubygems" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "handles git dependencies that are in rubygems using deployment mode" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle :install, artifice: "compact_index"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "doesn't fail if you only have a git gem with no deps when using deployment mode" do
+ build_git "foo"
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "install", artifice: "compact_index"
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index"
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "falls back when the API URL returns 403 Forbidden" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "compact_index_forbidden"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "falls back when the versions endpoint has a checksum mismatch" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "compact_index_checksum_mismatch"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(out).to include("The checksum of /versions does not match the checksum provided by the server!")
+ expect(out).to include("Calculated checksums #{{ "sha-256" => "8KfZiM/fszVkqhP/m5s9lvE6M9xKu4I1bU4Izddp5Ms=" }.inspect} did not match expected #{{ "sha-256" => "ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=" }.inspect}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows proper path when permission errors happen", :permissions do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ versions = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+ versions.dirname.mkpath
+ versions.write("created_at")
+ FileUtils.chmod("-r", versions)
+
+ bundle :install, artifice: "compact_index", raise_on_error: false
+
+ expect(err).to include(
+ "There was an error while trying to read from `#{versions}`. It is likely that you need to grant read permissions for that path."
+ )
+ end
+
+ it "falls back when the user's home directory does not exist or is not writable" do
+ ENV["HOME"] = tmp("missing_home").to_s
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_host_redirect"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects without Gem::Net::HTTP::Persistent" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ FileUtils.mkdir_p lib_path
+ File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h|
+ h.write <<-H
+ module Kernel
+ alias require_without_disabled_net_http require
+ def require(*args)
+ raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty?
+ require_without_disabled_net_http(*args)
+ end
+ end
+ H
+ end
+
+ bundle :install, artifice: "compact_index_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")]
+ expect(out).to_not match(/Too many redirects/)
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "times out when Bundler::Fetcher redirects too much" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_redirects", raise_on_error: false
+ expect(err).to match(/Too many redirects/)
+ end
+
+ context "when --full-index is specified" do
+ it "should use the modern index for install" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "install --full-index", artifice: "compact_index"
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the modern index for update" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "update --full-index", artifice: "compact_index", all: true
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "does not double check for gems that are only installed locally" do
+ build_repo2 do
+ build_gem "net_a" do |s|
+ s.add_dependency "net_b"
+ s.add_dependency "net_build_extensions"
+ end
+
+ build_gem "net_b"
+
+ build_gem "net_build_extensions" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/net_build_extensions.rb", "w") do |f|
+ f.puts "NET_BUILD_EXTENSIONS = 'YES'"
+ end
+ end
+ RUBY
+ end
+ end
+
+ system_gems %w[myrack-1.0.0 thin-1.0 net_a-1.0], gem_repo: gem_repo2
+ bundle_config "path.system true"
+ ENV["BUNDLER_SPEC_ALL_REQUESTS"] = <<~EOS.strip
+ #{source_uri}/versions
+ #{source_uri}/info/myrack
+ EOS
+
+ install_gemfile <<-G, artifice: "compact_index", verbose: true, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ expect(stdboth).not_to include "Double checking"
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra", verbose: true
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0"
+ end
+
+ it "fetches gem versions even when those gems are already installed" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack", "1.0.0"
+ G
+ bundle :install, artifice: "compact_index_extra_api"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+
+ build_repo4 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}" do; end
+ source "#{source_uri}/extra"
+ gem "myrack", "1.2"
+ G
+ bundle :install, artifice: "compact_index_extra_api"
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+
+ it "resolves indirect dependencies to the most scoped source that includes them" do
+ # In this scenario, the gem "somegem" only exists in repo4. It depends on
+ # specific version of activesupport that exists only in repo1. There
+ # happens also be a version of activesupport in repo4, but not the one that
+ # version 1.0.0 of somegem wants. This test makes sure that bundler tries to
+ # use the version in the most scoped source, even if not compatible, and
+ # gives a resolution error
+ build_repo4 do
+ build_gem "activesupport", "1.2.0"
+ build_gem "somegem", "1.0.0" do |s|
+ s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem 'somegem', '1.0.0'
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra_api", raise_on_error: false
+
+ expect(err).to include("Could not find compatible versions")
+ end
+
+ it "prints API output properly with back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra"
+
+ expect(out).to include("Fetching gem metadata from http://localgemserver.test/")
+ expect(out).to include("Fetching source index from http://localgemserver.test/extra")
+ end
+
+ it "does not fetch every spec when doing back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ build_gem "missing"
+
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra_missing"
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "does not fetch every spec when doing back deps & everything is the compact index" do
+ build_repo4 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ build_gem "missing"
+
+ FileUtils.rm_r Dir[gem_repo4("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_extra_api_missing"
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gem "back_deps 1.0"
+ end
+
+ it "uses the endpoint if all sources support it" do
+ gemfile <<-G
+ source "#{source_uri}"
+
+ gem 'foo'
+ G
+
+ bundle :install, artifice: "compact_index_api_missing"
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources using deployment mode" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra"
+ bundle_config "deployment true"
+ bundle :install, artifice: "compact_index_extra"
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "does not refetch if the only unmet dependency is bundler" do
+ build_repo2 do
+ build_gem "bundler_dep" do |s|
+ s.add_dependency "bundler"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+
+ gem "bundler_dep"
+ G
+
+ bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ end
+
+ it "prints post_install_messages" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack-obama'
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Post-install message from myrack:")
+ end
+
+ it "should display the post install message for a dependency" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack_middleware'
+ G
+
+ bundle :install, artifice: "compact_index"
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+
+ context "when using basic authentication" do
+ let(:user) { "user" }
+ let(:password) { "pass" }
+ let(:basic_auth_source_uri) do
+ uri = Gem::URI.parse(source_uri)
+ uri.user = user
+ uri.password = password
+
+ uri
+ end
+
+ it "passes basic authentication details and strips out creds" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "passes basic authentication details and strips out creds also in verbose mode" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "compact_index_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not pass the user / password to different hosts on redirect" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_creds_diff_host"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ describe "with authentication details in bundle config" do
+ before do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+
+ it "reads authentication details by host name from bundle config" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "reads authentication details by full url from bundle config" do
+ # The trailing slash is necessary here; Fetcher canonicalizes the URI.
+ bundle "config set #{source_uri}/ #{user}:#{password}"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the API" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "prefers auth supplied in the source uri" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle "config set #{source_hostname} otheruser:wrong"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows instructions if auth is not provided for the source" do
+ bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("bundle config set --global #{source_hostname} username:password")
+ end
+
+ it "fails if authentication has already been provided, but failed" do
+ bundle "config set #{source_hostname} #{user}:wrong"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("Bad username or password")
+ end
+
+ it "does not fallback to old dependency API if bad authentication is provided" do
+ bundle "config set #{source_hostname} #{user}:wrong"
+
+ bundle :install, artifice: "compact_index_strict_basic_authentication", raise_on_error: false, verbose: true
+ expect(err).to include("Bad username or password")
+ expect(out).to include("HTTP 401 Unauthorized http://user@localgemserver.test/versions")
+ expect(out).not_to include("HTTP 401 Unauthorized http://user@localgemserver.test/api/v1/dependencies")
+ end
+ end
+
+ describe "with no password" do
+ let(:password) { nil }
+
+ it "passes basic authentication details" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ context "when ruby is compiled without openssl" do
+ before do
+ # Install a monkeypatch that reproduces the effects of openssl being
+ # missing when the fetcher runs, as happens in real life. The reason
+ # we can't just overwrite openssl.rb is that Artifice uses it.
+ bundled_app("broken_ssl").mkpath
+ bundled_app("broken_ssl/openssl.rb").open("w") do |f|
+ f.write <<-RUBY
+ raise LoadError, "cannot load such file -- openssl"
+ RUBY
+ end
+ end
+
+ it "explains what to do to get it, and includes original error" do
+ gemfile <<-G
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false, artifice: nil
+ expect(err).to include("recompile Ruby").and include("cannot load such file")
+ end
+ end
+
+ context "when SSL certificate verification fails" do
+ it "explains what happened" do
+ # Install a monkeypatch that reproduces the effects of openssl raising
+ # a certificate validation error when RubyGems tries to connect.
+ gemfile <<-G
+ class Gem::Net::HTTP
+ def start
+ raise OpenSSL::SSL::SSLError, "certificate verify failed"
+ end
+ end
+
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/could not verify the SSL certificate/i)
+ end
+ end
+
+ context ".gemrc with sources is present" do
+ it "uses other sources declared in the Gemfile" do
+ File.open(home(".gemrc"), "w") do |file|
+ file.puts({ sources: ["https://rubygems.org"] }.to_yaml)
+ end
+
+ begin
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack'
+ G
+
+ bundle :install, artifice: "compact_index_forbidden"
+ ensure
+ FileUtils.rm_rf home(".gemrc")
+ end
+ end
+ end
+
+ it "performs update with etag not-modified" do
+ versions_etag = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions.etag"
+ )
+ expect(versions_etag.file?).to eq(false)
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ # Initial install creates the cached versions file and etag file
+ bundle :install, artifice: "compact_index"
+
+ expect(versions_etag.file?).to eq(true)
+ previous_content = versions_etag.binread
+
+ # Update the Gemfile so we can check subsequent install was successful
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ # Second install should match etag
+ bundle :install, artifice: "compact_index_etag_match"
+
+ expect(versions_etag.binread).to eq(previous_content)
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs full update when range is ignored" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ # Initial install creates the cached versions file and etag file
+ bundle :install, artifice: "compact_index"
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ versions = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+ # Modify the cached file. The ranged request will be based on this but,
+ # in this test, the range is ignored so this gets overwritten, allowing install.
+ versions.write "ruining this file"
+
+ bundle :install, artifice: "compact_index_range_ignored"
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs partial update with a non-empty range" do
+ build_repo4 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ # Initial install creates the cached versions file
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_partial_update", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs partial update while local cache is updated by another process" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack'
+ G
+
+ # Create a partial cache versions file
+ versions = compact_index_cache_path.join(
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+ versions.dirname.mkpath
+ versions.write("created_at")
+
+ bundle :install, artifice: "compact_index_concurrent_download"
+
+ expect(versions.read).to start_with("created_at")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs a partial update that fails digest check, then a full update" do
+ build_repo4 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_partial_update_bad_digest", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs full update if server endpoints serve partial content responses but don't have incremental content and provide no digest" do
+ build_repo4 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_partial_update_no_digest_not_incremental", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "performs full update of compact index info cache if range is not satisfiable" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '0.9.1'
+ G
+
+ bundle :install, artifice: "compact_index"
+
+ cache_path = compact_index_cache_path.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5")
+
+ # We must remove the etag so that we don't ignore the range and get a 304 Not Modified.
+ myrack_info_etag_path = File.join(cache_path, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073")
+ File.unlink(myrack_info_etag_path) if File.exist?(myrack_info_etag_path)
+
+ myrack_info_path = File.join(cache_path, "info", "myrack")
+ expected_myrack_info_content = File.read(myrack_info_path)
+
+ # Modify the cache files to make the range not satisfiable
+ File.open(myrack_info_path, "a") {|f| f << "0.9.2 |checksum:c55b525b421fd833a93171ad3d7f04528ca8e87d99ac273f8933038942a5888c" }
+
+ # Update the Gemfile so the next install does its normal things
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack', '1.0.0'
+ G
+
+ # The cache files now being longer means the requested range is going to be not satisfiable
+ # Bundler must end up requesting the whole file to fix things up.
+ bundle :install, artifice: "compact_index_range_not_satisfiable"
+
+ resulting_myrack_info_content = File.read(myrack_info_path)
+
+ expect(resulting_myrack_info_content).to eq(expected_myrack_info_content)
+ end
+
+ it "fails gracefully when the source URI has an invalid scheme" do
+ install_gemfile <<-G, raise_on_error: false
+ source "htps://rubygems.org"
+ gem "myrack"
+ G
+ expect(exitstatus).to eq(15)
+ expect(err).to end_with(<<-E.strip)
+ The request uri `htps://index.rubygems.org/versions` has an invalid scheme (`htps`). Did you mean `http` or `https`?
+ E
+ end
+
+ describe "checksum validation" do
+ before do
+ lockfile <<-L
+ GEM
+ remote: #{source_uri}
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ #{checksums_section}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "handles checksums from the server in base64" do
+ api_checksum = checksum_digest(gem_repo1, "myrack", "1.0.0")
+ myrack_checksum = [[api_checksum].pack("H*")].pack("m0")
+ install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => myrack_checksum }
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ end
+
+ it "raises when the checksum does not match" do
+ install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", raise_on_error: false
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ gem_path = default_cache_path.dirname.join("myrack-1.0.0.gem")
+
+ expect(exitstatus).to eq(37)
+ expect(err).to eq <<~E.strip
+ Bundler found mismatched checksums. This is a potential security risk.
+ myrack (1.0.0) sha256=2222222222222222222222222222222222222222222222222222222222222222
+ from the API at http://localgemserver.test/
+ #{checksum_to_lock(gem_repo1, "myrack", "1.0.0")}
+ from the gem at #{gem_path}
+
+ If you trust the API at http://localgemserver.test/, to resolve this issue you can:
+ 1. remove the gem at #{gem_path}
+ 2. run `bundle install`
+
+ To ignore checksum security warnings, disable checksum validation with
+ `bundle config set --local disable_checksum_validation true`
+ E
+ end
+
+ it "raises when the checksum is the wrong length" do
+ install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum", env: { "BUNDLER_SPEC_MYRACK_CHECKSUM" => "checksum!", "DEBUG" => "1" }, verbose: true, raise_on_error: false
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ expect(exitstatus).to eq(14)
+ expect(err).to include('Invalid checksum for myrack-0.9.1: "checksum!" is not a valid SHA256 hex or base64 digest')
+ end
+
+ it "does not raise when disable_checksum_validation is set" do
+ bundle_config "disable_checksum_validation true"
+ install_gemfile <<-G, artifice: "compact_index_wrong_gem_checksum"
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+ end
+
+ it "works when cache dir is world-writable" do
+ install_gemfile <<-G, artifice: "compact_index"
+ File.umask(0000)
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+
+ it "doesn't explode when the API dependencies are wrong" do
+ install_gemfile <<-G, artifice: "compact_index_wrong_dependencies", env: { "DEBUG" => "true" }, raise_on_error: false
+ source "#{source_uri}"
+ gem "rails"
+ G
+ deps = [Gem::Dependency.new("rake", "= #{rake_version}"),
+ Gem::Dependency.new("actionpack", "= 2.3.2"),
+ Gem::Dependency.new("activerecord", "= 2.3.2"),
+ Gem::Dependency.new("actionmailer", "= 2.3.2"),
+ Gem::Dependency.new("activeresource", "= 2.3.2")]
+ expect(out).to include("rails-2.3.2 from rubygems remote at #{source_uri}/ has corrupted API dependencies")
+ expect(err).to include(<<-E.strip)
+Bundler::APIResponseMismatchError: Downloading rails-2.3.2 revealed dependencies not in the API (#{deps.map(&:to_s).join(", ")}).
+Running `bundle update rails` should fix the problem.
+ E
+ end
+
+ it "does not duplicate specs in the lockfile when updating and a dependency is not installed" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "https://gem.repo1"
+ source "#{source_uri}" do
+ gem "rails"
+ gem "activemerchant"
+ end
+ G
+ uninstall_gem("activemerchant")
+ bundle "update rails", artifice: "compact_index"
+ count = lockfile.match?("CHECKSUMS") ? 2 : 1 # Once in the specs, and once in CHECKSUMS
+ expect(lockfile.scan(/activemerchant \(/).size).to eq(count)
+ end
+
+ it "handles an API that does not provide checksums info (undocumented, support may get removed)" do
+ install_gemfile <<-G, artifice: "compact_index_no_checksums"
+ source "https://gem.repo1"
+ gem "rake"
+ G
+ end
+end
diff --git a/spec/bundler/install/gems/dependency_api_fallback_spec.rb b/spec/bundler/install/gems/dependency_api_fallback_spec.rb
new file mode 100644
index 0000000000..c7b0c537e4
--- /dev/null
+++ b/spec/bundler/install/gems/dependency_api_fallback_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.describe "gemcutter's dependency API" do
+ context "when Gemcutter API takes too long to respond" do
+ before do
+ bundle_config "timeout 1"
+ end
+
+ it "times out and falls back on the modern index" do
+ install_gemfile <<-G, artifice: "endpoint_timeout"
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(out).to include("Fetching source index from https://gem.repo1/")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/dependency_api_spec.rb b/spec/bundler/install/gems/dependency_api_spec.rb
new file mode 100644
index 0000000000..32a1b98b6d
--- /dev/null
+++ b/spec/bundler/install/gems/dependency_api_spec.rb
@@ -0,0 +1,656 @@
+# frozen_string_literal: true
+
+RSpec.describe "gemcutter's dependency API" do
+ let(:source_hostname) { "localgemserver.test" }
+ let(:source_uri) { "http://#{source_hostname}" }
+
+ it "should use the API" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should URI encode gem names" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem " sinatra"
+ G
+
+ bundle :install, artifice: "endpoint", raise_on_error: false
+ expect(err).to include("' sinatra' is not a valid gem name because it contains whitespace.")
+ end
+
+ it "should handle nested dependencies" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rails"
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Fetching gem metadata from #{source_uri}/...")
+ expect(the_bundle).to include_gems(
+ "rails 2.3.2",
+ "actionpack 2.3.2",
+ "activerecord 2.3.2",
+ "actionmailer 2.3.2",
+ "activeresource 2.3.2",
+ "activesupport 2.3.2"
+ )
+ end
+
+ it "should handle multiple gem dependencies on the same gem" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "net-sftp"
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(the_bundle).to include_gems "net-sftp 1.1.1"
+ end
+
+ it "should use the endpoint when using deployment mode" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ bundle :install, artifice: "endpoint"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles git dependencies that are in rubygems" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ git "#{lib_path("foo-1.0")}" do
+ gem 'foo'
+ end
+ G
+
+ bundle :install, artifice: "endpoint"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "handles git dependencies that are in rubygems using deployment mode" do
+ build_git "foo" do |s|
+ s.executables = "foobar"
+ s.add_dependency "rails", "2.3.2"
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle :install, artifice: "endpoint"
+
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint"
+
+ expect(the_bundle).to include_gems("rails 2.3.2")
+ end
+
+ it "doesn't fail if you only have a git gem with no deps when using deployment mode" do
+ build_git "foo"
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ bundle "install", artifice: "endpoint"
+ bundle_config "deployment true"
+ bundle :install, artifice: "endpoint"
+
+ expect(the_bundle).to include_gems("foo 1.0")
+ end
+
+ it "falls back when the API errors out" do
+ simulate_platform "x86-mswin32" do
+ build_repo2 do
+ # The rcov gem is platform mswin32, but has no arch
+ build_gem "rcov" do |s|
+ s.platform = Gem::Platform.new([nil, "mswin32", nil])
+ s.write "lib/rcov.rb", "RCOV = '1.0.0'"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rcov"
+ G
+
+ bundle :install, artifice: "windows", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "rcov 1.0.0"
+ end
+ end
+
+ it "falls back when hitting the Gemcutter Dependency Limit" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "activesupport"
+ gem "actionpack"
+ gem "actionmailer"
+ gem "activeresource"
+ gem "thin"
+ gem "myrack"
+ gem "rails"
+ G
+ bundle :install, artifice: "endpoint_fallback"
+ expect(out).to include("Fetching source index from #{source_uri}")
+
+ expect(the_bundle).to include_gems(
+ "activesupport 2.3.2",
+ "actionpack 2.3.2",
+ "actionmailer 2.3.2",
+ "activeresource 2.3.2",
+ "activesupport 2.3.2",
+ "thin 1.0.0",
+ "myrack 1.0.0",
+ "rails 2.3.2"
+ )
+ end
+
+ it "falls back when Gemcutter API doesn't return proper Marshal format" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "endpoint_marshal_fail"
+ expect(out).to include("could not fetch from the dependency API, trying the full index")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "falls back when the API URL returns 403 Forbidden" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "endpoint_api_forbidden"
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_host_redirect"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "handles host redirects without Gem::Net::HTTP::Persistent" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ FileUtils.mkdir_p lib_path
+ File.open(lib_path("disable_net_http_persistent.rb"), "w") do |h|
+ h.write <<-H
+ module Kernel
+ alias require_without_disabled_net_http require
+ def require(*args)
+ raise LoadError, 'simulated' if args.first == 'openssl' && !caller.grep(/vendored_persistent/).empty?
+ require_without_disabled_net_http(*args)
+ end
+ end
+ H
+ end
+
+ bundle :install, artifice: "endpoint_host_redirect", requires: [lib_path("disable_net_http_persistent.rb")]
+ expect(out).to_not match(/Too many redirects/)
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "timeouts when Bundler::Fetcher redirects too much" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_redirect", raise_on_error: false
+ expect(err).to match(/Too many redirects/)
+ end
+
+ context "when --full-index is specified" do
+ it "should use the modern index for install" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "install --full-index", artifice: "endpoint"
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the modern index for update" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+
+ bundle "update --full-index", artifice: "endpoint", all: true
+ expect(out).to include("Fetching source index from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "endpoint_extra"
+ expect(the_bundle).to include_gems "back_deps 1.0", "foo 1.0"
+ end
+
+ it "fetches gem versions even when those gems are already installed" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack", "1.0.0"
+ G
+ bundle :install, artifice: "endpoint_extra_api"
+
+ build_repo4 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}" do; end
+ source "#{source_uri}/extra"
+ gem "myrack", "1.2"
+ G
+ bundle :install, artifice: "endpoint_extra_api"
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+
+ it "resolves indirect dependencies to the most scoped source that includes them" do
+ # In this scenario, the gem "somegem" only exists in repo4. It depends on
+ # specific version of activesupport that exists only in repo1. There
+ # happens also be a version of activesupport in repo4, but not the one that
+ # version 1.0.0 of somegem wants. This test makes sure that bundler tries to
+ # use the version in the most scoped source, even if not compatible, and
+ # gives a resolution error
+ build_repo4 do
+ build_gem "activesupport", "1.2.0"
+ build_gem "somegem", "1.0.0" do |s|
+ s.add_dependency "activesupport", "1.2.3" # This version exists only in repo1
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem 'somegem', '1.0.0'
+ end
+ G
+
+ bundle :install, artifice: "compact_index_extra_api", raise_on_error: false
+
+ expect(err).to include("Could not find compatible versions")
+ end
+
+ it "prints API output properly with back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "endpoint_extra"
+
+ expect(out).to include("Fetching gem metadata from http://localgemserver.test/.")
+ expect(out).to include("Fetching source index from http://localgemserver.test/extra")
+ end
+
+ it "does not fetch every spec when doing back deps" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ build_gem "missing"
+
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ install_gemfile <<-G, artifice: "endpoint_extra_missing"
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "fetches again when more dependencies are found in subsequent sources using deployment mode" do
+ build_repo2 do
+ build_gem "back_deps" do |s|
+ s.add_dependency "foo"
+ end
+ FileUtils.rm_r Dir[gem_repo2("gems/foo-*.gem")]
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+ source "#{source_uri}/extra" do
+ gem "back_deps"
+ end
+ G
+
+ bundle :install, artifice: "endpoint_extra"
+ bundle_config "deployment true"
+ bundle "install", artifice: "endpoint_extra"
+ expect(the_bundle).to include_gems "back_deps 1.0"
+ end
+
+ it "does not fetch all marshaled specs" do
+ build_repo2 do
+ build_gem "foo", "1.0"
+ build_gem "foo", "2.0"
+ end
+
+ install_gemfile <<-G, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }, verbose: true
+ source "#{source_uri}"
+
+ gem "foo"
+ G
+
+ expect(out).to include("foo-2.0.gemspec.rz")
+ expect(out).not_to include("foo-1.0.gemspec.rz")
+ end
+
+ it "does not refetch if the only unmet dependency is bundler" do
+ build_repo2 do
+ build_gem "bundler_dep" do |s|
+ s.add_dependency "bundler"
+ end
+ end
+
+ gemfile <<-G
+ source "#{source_uri}"
+
+ gem "bundler_dep"
+ G
+
+ bundle :install, artifice: "endpoint", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ end
+
+ it "prints post_install_messages" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack-obama'
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Post-install message from myrack:")
+ end
+
+ it "should display the post install message for a dependency" do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack_middleware'
+ G
+
+ bundle :install, artifice: "endpoint"
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+
+ context "when using basic authentication" do
+ let(:user) { "user" }
+ let(:password) { "pass" }
+ let(:basic_auth_source_uri) do
+ uri = Gem::URI.parse(source_uri)
+ uri.user = user
+ uri.password = password
+
+ uri
+ end
+
+ it "passes basic authentication details and strips out creds" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "passes basic authentication details and strips out creds also in verbose mode" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, verbose: true, artifice: "endpoint_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "strips http basic authentication creds for modern index" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_marshal_fail_basic_authentication"
+ expect(out).not_to include("#{user}:#{password}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "strips http basic auth creds when it can't reach the server" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_500", raise_on_error: false
+ expect(out).not_to include("#{user}:#{password}")
+ end
+
+ it "does not pass the user / password to different hosts on redirect" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_creds_diff_host"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ describe "with host including dashes" do
+ before do
+ gemfile <<-G
+ source "http://local-gemserver.test"
+ gem "myrack"
+ G
+ end
+
+ it "reads authentication details from a valid ENV variable" do
+ bundle :install, artifice: "endpoint_strict_basic_authentication", env: { "BUNDLE_LOCAL___GEMSERVER__TEST" => "#{user}:#{password}" }
+
+ expect(out).to include("Fetching gem metadata from http://local-gemserver.test")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ describe "with authentication details in bundle config" do
+ before do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "myrack"
+ G
+ end
+
+ it "reads authentication details by host name from bundle config" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "reads authentication details by full url from bundle config" do
+ # The trailing slash is necessary here; Fetcher canonicalizes the URI.
+ bundle "config set #{source_uri}/ #{user}:#{password}"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "should use the API" do
+ bundle "config set #{source_hostname} #{user}:#{password}"
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+ expect(out).to include("Fetching gem metadata from #{source_uri}")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "prefers auth supplied in the source uri" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle "config set #{source_hostname} otheruser:wrong"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows instructions if auth is not provided for the source" do
+ bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("bundle config set --global #{source_hostname} username:password")
+ end
+
+ it "fails if authentication has already been provided, but failed" do
+ bundle "config set #{source_hostname} #{user}:wrong"
+
+ bundle :install, artifice: "endpoint_strict_basic_authentication", raise_on_error: false
+ expect(err).to include("Bad username or password")
+ end
+ end
+
+ describe "with no password" do
+ let(:password) { nil }
+
+ it "passes basic authentication details" do
+ gemfile <<-G
+ source "#{basic_auth_source_uri}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "endpoint_basic_authentication"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ context "when ruby is compiled without openssl" do
+ before do
+ # Install a monkeypatch that reproduces the effects of openssl being
+ # missing when the fetcher runs, as happens in real life. The reason
+ # we can't just overwrite openssl.rb is that Artifice uses it.
+ bundled_app("broken_ssl").mkpath
+ bundled_app("broken_ssl/openssl.rb").open("w") do |f|
+ f.write <<-RUBY
+ raise LoadError, "cannot load such file -- openssl"
+ RUBY
+ end
+ end
+
+ it "explains what to do to get it, and includes original error" do
+ gemfile <<-G
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "fail", env: { "RUBYOPT" => "-I#{bundled_app("broken_ssl")}" }, raise_on_error: false
+ expect(err).to include("recompile Ruby").and include("cannot load such file")
+ end
+ end
+
+ context "when SSL certificate verification fails" do
+ it "explains what happened" do
+ # Install a monkeypatch that reproduces the effects of openssl raising
+ # a certificate validation error when RubyGems tries to connect.
+ gemfile <<-G
+ class Gem::Net::HTTP
+ def start
+ raise OpenSSL::SSL::SSLError, "certificate verify failed"
+ end
+ end
+
+ source "#{source_uri.gsub(/http/, "https")}"
+ gem "myrack"
+ G
+
+ bundle :install, raise_on_error: false
+ expect(err).to match(/could not verify the SSL certificate/i)
+ end
+ end
+
+ context ".gemrc with sources is present" do
+ it "uses other sources declared in the Gemfile" do
+ File.open(home(".gemrc"), "w") do |file|
+ file.puts({ sources: ["https://rubygems.org"] }.to_yaml)
+ end
+
+ begin
+ gemfile <<-G
+ source "#{source_uri}"
+ gem 'myrack'
+ G
+
+ bundle "install", artifice: "endpoint_marshal_fail"
+ ensure
+ FileUtils.rm_rf home(".gemrc")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/env_spec.rb b/spec/bundler/install/gems/env_spec.rb
new file mode 100644
index 0000000000..6d5aa456fe
--- /dev/null
+++ b/spec/bundler/install/gems/env_spec.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with ENV conditionals" do
+ describe "when just setting an ENV key as a string" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set" do
+ ENV["BUNDLER_TEST"] = "1"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when just setting an ENV key as a symbol" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env :BUNDLER_TEST do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set" do
+ ENV["BUNDLER_TEST"] = "1"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when setting a string to match the env" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" => "foo" do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "excludes the gems when the ENV variable is set but does not match the condition" do
+ ENV["BUNDLER_TEST"] = "1"
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set and matches the condition" do
+ ENV["BUNDLER_TEST"] = "foo"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when setting a regex to match the env" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ env "BUNDLER_TEST" => /foo/ do
+ gem "myrack"
+ end
+ G
+ end
+
+ it "excludes the gems when the ENV variable is not set" do
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "excludes the gems when the ENV variable is set but does not match the condition" do
+ ENV["BUNDLER_TEST"] = "fo"
+ bundle :install
+ expect(the_bundle).not_to include_gems "myrack"
+ end
+
+ it "includes the gems when the ENV variable is set and matches the condition" do
+ ENV["BUNDLER_TEST"] = "foobar"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/flex_spec.rb b/spec/bundler/install/gems/flex_spec.rb
new file mode 100644
index 0000000000..a30b53d6ad
--- /dev/null
+++ b/spec/bundler/install/gems/flex_spec.rb
@@ -0,0 +1,403 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle flex_install" do
+ it "installs the gems as expected" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to be_locked
+ end
+
+ it "installs even when the lockfile is invalid" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to be_locked
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', '1.0'
+ G
+
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to be_locked
+ end
+
+ it "keeps child dependencies at the same version" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0"
+
+ update_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama", "1.0"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0"
+ end
+
+ describe "adding new gems" do
+ it "installs added gems without updating previously installed gems" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.5'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.5"
+ end
+
+ it "keeps child dependencies pinned" do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama"
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama"
+ gem "thin"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0", "thin 1.0"
+ end
+ end
+
+ describe "removing gems" do
+ it "removes gems without changing the versions of remaining gems" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.5'
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.2'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2"
+ end
+
+ it "removes top level dependencies when removed from the Gemfile while leaving other dependencies intact" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ gem 'activesupport', '2.3.5'
+ G
+
+ update_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+ end
+
+ it "removes child dependencies" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'myrack-obama'
+ gem 'activesupport'
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0", "myrack-obama 1.0.0", "activesupport 2.3.5"
+
+ update_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'activesupport'
+ G
+
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ expect(the_bundle).not_to include_gems "myrack-obama", "myrack"
+ end
+ end
+
+ describe "when running bundle install and Gemfile conflicts with lockfile" do
+ before(:each) do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(the_bundle).to include_gems "myrack_middleware 1.0", "myrack 0.9.1"
+
+ build_repo2 do
+ build_gem "myrack-obama", "2.0" do |s|
+ s.add_dependency "myrack", "=1.2"
+ end
+ build_gem "myrack_middleware", "2.0" do |s|
+ s.add_dependency "myrack", ">=1.0"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack-obama", "2.0"
+ gem "myrack_middleware"
+ G
+ end
+
+ it "does not install gems whose dependencies are not met" do
+ bundle :install, raise_on_error: false
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/setup'
+ RUBY
+ expect(err).to match(/could not find gem 'myrack-obama/i)
+ end
+
+ it "discards the locked gems when the Gemfile requires different versions than the lock" do
+ bundle_config "force_ruby_platform true"
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because myrack-obama >= 2.0 depends on myrack = 1.2
+ and myrack = 1.2 could not be found in rubygems repository https://gem.repo2/ or installed locally,
+ myrack-obama >= 2.0 cannot be used.
+ So, because Gemfile depends on myrack-obama = 2.0,
+ version solving has failed.
+ E
+
+ bundle :install, retry: 0, raise_on_error: false
+ expect(err).to end_with(nice_error)
+ end
+
+ it "does not include conflicts with a single requirement tree, because that can't possibly be a conflict" do
+ bundle_config "force_ruby_platform true"
+
+ bad_error = <<~E.strip
+ Bundler could not find compatible versions for gem "myrack-obama":
+ In Gemfile:
+ myrack-obama (= 2.0)
+ E
+
+ bundle "update myrack_middleware", retry: 0, raise_on_error: false
+ expect(err).not_to end_with(bad_error)
+ end
+ end
+
+ describe "when running bundle update and Gemfile conflicts with lockfile" do
+ before(:each) do
+ build_repo4 do
+ build_gem "jekyll-feed", "0.16.0"
+ build_gem "jekyll-feed", "0.15.1"
+
+ build_gem "github-pages", "226" do |s|
+ s.add_dependency "jekyll-feed", "0.15.1"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "jekyll-feed", "~> 0.12"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "github-pages", "~> 226"
+ gem "jekyll-feed", "~> 0.12"
+ G
+ end
+
+ it "discards the conflicting lockfile information and resolves properly" do
+ bundle :update, raise_on_error: false, all: true
+ expect(err).to be_empty
+ end
+ end
+
+ describe "subtler cases" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "myrack-obama"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "myrack-obama"
+ G
+ end
+
+ it "should work when you install" do
+ bundle "install"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ c.checksum gem_repo1, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ myrack-obama
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should work when you update" do
+ bundle "update myrack"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "myrack", "0.9.1"
+ c.checksum gem_repo1, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (0.9.1)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ myrack-obama
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ describe "when adding a new source" do
+ it "updates the lockfile" do
+ build_repo2
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo2" do
+ end
+ gem "myrack"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ # This was written to test github issue #636
+ describe "when a locked child dependency conflicts" do
+ before(:each) do
+ build_repo2 do
+ build_gem "capybara", "0.3.9" do |s|
+ s.add_dependency "myrack", ">= 1.0.0"
+ end
+
+ build_gem "myrack", "1.1.0"
+ build_gem "rails", "3.0.0.rc4" do |s|
+ s.add_dependency "myrack", "~> 1.1.0"
+ end
+
+ build_gem "myrack", "1.2.1"
+ build_gem "rails", "3.0.0" do |s|
+ s.add_dependency "myrack", "~> 1.2.1"
+ end
+ end
+ end
+
+ it "resolves them" do
+ # install Rails 3.0.0.rc
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0.0.rc4"
+ gem "capybara", "0.3.9"
+ G
+
+ # upgrade Rails to 3.0.0 and then install again
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "rails", "3.0.0"
+ gem "capybara", "0.3.9"
+ G
+ expect(err).to be_empty
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/fund_spec.rb b/spec/bundler/install/gems/fund_spec.rb
new file mode 100644
index 0000000000..8a3a51270a
--- /dev/null
+++ b/spec/bundler/install/gems/fund_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "with gem sources" do
+ before do
+ build_repo2 do
+ build_gem "has_funding_and_other_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+
+ build_gem "has_funding", "1.2.3" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/has_funding/funding",
+ }
+ end
+
+ build_gem "gem_with_dependent_funding", "1.0" do |s|
+ s.add_dependency "has_funding"
+ end
+ end
+ end
+
+ context "when gems include a fund URI" do
+ it "displays the plural fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).to include("2 installed gems you directly depend on are looking for funding.")
+ end
+
+ it "displays the singular fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+ end
+
+ context "when gems include a fund URI but `ignore_funding_requests` is configured" do
+ before do
+ bundle_config "ignore_funding_requests true"
+ end
+
+ it "does not display the plural fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).not_to include("2 installed gems you directly depend on are looking for funding.")
+ end
+
+ it "does not display the singular fund message after installing" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding'
+ gem 'myrack-obama'
+ G
+
+ expect(out).not_to include("1 installed gem you directly depend on is looking for funding.")
+ end
+ end
+
+ context "when gems do not include fund messages" do
+ it "does not display any fund messages" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "activesupport"
+ G
+
+ expect(out).not_to include("gem you depend on")
+ end
+ end
+
+ context "when a dependency includes a fund message" do
+ it "does not display the fund message" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'gem_with_dependent_funding'
+ G
+
+ expect(out).not_to include("gem you depend on")
+ end
+ end
+ end
+
+ context "with git sources" do
+ context "when gems include fund URI" do
+ it "displays the fund message after installing" do
+ build_git "also_has_funding" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}'
+ G
+
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+
+ it "displays the fund message if repo is updated" do
+ build_git "also_has_funding" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}'
+ G
+
+ build_git "also_has_funding", "1.1" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.1")}'
+ G
+
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+
+ it "displays the fund message if repo is not updated" do
+ build_git "also_has_funding" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/also_has_funding/funding",
+ }
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}'
+ G
+
+ bundle :install
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+
+ bundle :install
+ expect(out).to include("1 installed gem you directly depend on is looking for funding.")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/gemfile_source_header_spec.rb b/spec/bundler/install/gems/gemfile_source_header_spec.rb
new file mode 100644
index 0000000000..dc35c8d741
--- /dev/null
+++ b/spec/bundler/install/gems/gemfile_source_header_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+RSpec.describe "fetching dependencies with a mirrored source" do
+ let(:mirror) { "https://server.example.org" }
+
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "#{mirror}"
+ gem 'weakling'
+ G
+
+ bundle_config "mirror.#{mirror} https://gem.repo2"
+ end
+
+ it "sets the 'X-Gemfile-Source' and 'User-Agent' headers and bundles successfully" do
+ bundle :install, artifice: "endpoint_mirror_source"
+
+ expect(out).to include("Installing weakling")
+ expect(out).to include("Bundle complete")
+ expect(the_bundle).to include_gems "weakling 0.0.3"
+ end
+end
diff --git a/spec/bundler/install/gems/mirror_probe_spec.rb b/spec/bundler/install/gems/mirror_probe_spec.rb
new file mode 100644
index 0000000000..564062ccf6
--- /dev/null
+++ b/spec/bundler/install/gems/mirror_probe_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+RSpec.describe "fetching dependencies with a not available mirror" do
+ before do
+ build_repo2
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'weakling'
+ G
+ end
+
+ context "with a specific fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/__FALLBACK_TIMEOUT/" => "true",
+ "BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror")
+ end
+
+ it "install a gem using the original uri when the mirror is not responding" do
+ bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" }, verbose: true
+
+ expect(out).to include("Installing weakling")
+ expect(out).to include("Bundle complete")
+ expect(the_bundle).to include_gems "weakling 0.0.3"
+ end
+ end
+
+ context "with a global fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__ALL__FALLBACK_TIMEOUT/" => "1",
+ "BUNDLE_MIRROR__ALL" => "https://gem.mirror")
+ end
+
+ it "install a gem using the original uri when the mirror is not responding" do
+ bundle :install, env: { "BUNDLER_SPEC_FAKE_RESOLVE" => "gem.mirror" }
+
+ expect(out).to include("Installing weakling")
+ expect(out).to include("Bundle complete")
+ expect(the_bundle).to include_gems "weakling 0.0.3"
+ end
+ end
+
+ context "with a specific mirror without a fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__HTTPS://GEM__REPO2/" => "https://gem.mirror")
+ end
+
+ it "fails to install the gem with a timeout error when the mirror is not responding" do
+ bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false
+
+ expect(out).to be_empty
+ expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.")
+ end
+ end
+
+ context "with a global mirror without a fallback timeout" do
+ before do
+ bundle_config_global("BUNDLE_MIRROR__ALL" => "https://gem.mirror")
+ end
+
+ it "fails to install the gem with a timeout error when the mirror is not responding" do
+ bundle :install, artifice: "compact_index_mirror_down", raise_on_error: false
+
+ expect(out).to be_empty
+ expect(err).to eq("Could not reach host gem.mirror. Check your network connection and try again.")
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/mirror_spec.rb b/spec/bundler/install/gems/mirror_spec.rb
new file mode 100644
index 0000000000..e1fbeac454
--- /dev/null
+++ b/spec/bundler/install/gems/mirror_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with a mirror configured" do
+ describe "when the mirror does not match the gem source" do
+ before :each do
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ bundle_config "mirror.http://gems.example.org http://gem-mirror.example.org"
+ end
+
+ it "installs from the normal location" do
+ bundle :install
+ expect(out).to include("Fetching gem metadata from https://gem.repo1")
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ describe "when the gem source matches a configured mirror" do
+ before :each do
+ gemfile <<-G
+ # This source is bogus and doesn't have the gem we're looking for
+ source "https://gem.repo2"
+
+ gem "myrack"
+ G
+ bundle_config "mirror.https://gem.repo2 https://gem.repo1"
+ end
+
+ it "installs the gem from the mirror" do
+ bundle :install, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ expect(out).to include("Fetching gem metadata from https://gem.repo1")
+ expect(out).not_to include("Fetching gem metadata from https://gem.repo2")
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/native_extensions_spec.rb b/spec/bundler/install/gems/native_extensions_spec.rb
new file mode 100644
index 0000000000..d5b10d2c8f
--- /dev/null
+++ b/spec/bundler/install/gems/native_extensions_spec.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+RSpec.describe "installing a gem with native extensions" do
+ it "installs" do
+ build_repo2 do
+ build_gem "c_extension" do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension") == "hello"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_true(VALUE self) {
+ return Qtrue;
+ }
+
+ void Init_c_extension_bundle() {
+ VALUE c_Extension = rb_define_class("CExtension", rb_cObject);
+ rb_define_method(c_Extension, "its_true", c_extension_true, 0);
+ }
+ C
+
+ s.write "lib/c_extension.rb", <<-C
+ require "c_extension_bundle"
+ C
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "c_extension"
+ G
+
+ bundle_config "build.c_extension --with-c_extension=hello"
+ bundle "install"
+
+ expect(out).to include("Installing c_extension 1.0 with native extensions")
+
+ run "Bundler.require; puts CExtension.new.its_true"
+ expect(out).to eq("true")
+ end
+
+ it "installs from git" do
+ build_git "c_extension" do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension") == "hello"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_true(VALUE self) {
+ return Qtrue;
+ }
+
+ void Init_c_extension_bundle() {
+ VALUE c_Extension = rb_define_class("CExtension", rb_cObject);
+ rb_define_method(c_Extension, "its_true", c_extension_true, 0);
+ }
+ C
+
+ s.write "lib/c_extension.rb", <<-C
+ require "c_extension_bundle"
+ C
+ end
+
+ bundle_config "build.c_extension --with-c_extension=hello"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump}
+ G
+
+ expect(err).to_not include("warning: conflicting chdir during another chdir block")
+
+ run "Bundler.require; puts CExtension.new.its_true"
+ expect(out).to eq("true")
+ end
+
+ it "installs correctly from git when multiple gems with extensions share one repository" do
+ build_repo2 do
+ ["one", "two"].each do |n|
+ build_lib "c_extension_#{n}", "1.0", path: lib_path("gems/c_extension_#{n}") do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle_#{n}"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension_#{n}") == "#{n}"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension_#{n}.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_#{n}_value(VALUE self) {
+ return rb_str_new_cstr("#{n}");
+ }
+
+ void Init_c_extension_bundle_#{n}() {
+ VALUE c_Extension = rb_define_class("CExtension_#{n}", rb_cObject);
+ rb_define_method(c_Extension, "value", c_extension_#{n}_value, 0);
+ }
+ C
+
+ s.write "lib/c_extension_#{n}.rb", <<-C
+ require "c_extension_bundle_#{n}"
+ C
+ end
+ end
+ build_git "gems", path: lib_path("gems"), gemspec: false
+ end
+
+ bundle_config "build.c_extension_one --with-c_extension_one=one"
+ bundle_config "build.c_extension_two --with-c_extension_two=two"
+
+ # 1st time, require only one gem -- only one of the extensions gets built.
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension_one", :git => #{lib_path("gems").to_s.dump}
+ G
+
+ # 2nd time, require both gems -- we need both extensions to be built now.
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension_one", :git => #{lib_path("gems").to_s.dump}
+ gem "c_extension_two", :git => #{lib_path("gems").to_s.dump}
+ G
+
+ run "Bundler.require; puts CExtension_one.new.value; puts CExtension_two.new.value"
+ expect(out).to eq("one\ntwo")
+ end
+
+ it "install with multiple build flags" do
+ build_git "c_extension" do |s|
+ s.extensions = ["ext/extconf.rb"]
+ s.write "ext/extconf.rb", <<-E
+ require "mkmf"
+ name = "c_extension_bundle"
+ dir_config(name)
+ raise ArgumentError unless with_config("c_extension") == "hello" && with_config("c_extension_bundle-dir") == "hola"
+ create_makefile(name)
+ E
+
+ s.write "ext/c_extension.c", <<-C
+ #include "ruby.h"
+
+ VALUE c_extension_true(VALUE self) {
+ return Qtrue;
+ }
+
+ void Init_c_extension_bundle() {
+ VALUE c_Extension = rb_define_class("CExtension", rb_cObject);
+ rb_define_method(c_Extension, "its_true", c_extension_true, 0);
+ }
+ C
+
+ s.write "lib/c_extension.rb", <<-C
+ require "c_extension_bundle"
+ C
+ end
+
+ bundle_config "build.c_extension --with-c_extension=hello --with-c_extension_bundle-dir=hola"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "c_extension", :git => #{lib_path("c_extension-1.0").to_s.dump}
+ G
+
+ run "Bundler.require; puts CExtension.new.its_true"
+ expect(out).to eq("true")
+ end
+end
diff --git a/spec/bundler/install/gems/no_build_extension_spec.rb b/spec/bundler/install/gems/no_build_extension_spec.rb
new file mode 100644
index 0000000000..31f0170433
--- /dev/null
+++ b/spec/bundler/install/gems/no_build_extension_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with --no-build-extension" do
+ before do
+ build_repo2 do
+ build_gem "with_extension" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/with_extension.rb", "w") do |f|
+ f.puts "WITH_EXTENSION = 'YES'"
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ it "skips building native extensions and warns when no_build_extension is set" do
+ bundle_config "no_build_extension true"
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_extension"
+ gem "rake"
+ G
+
+ bundle :install
+
+ build_complete = default_bundle_path("extensions").join(
+ Gem::Platform.local.to_s,
+ Gem.extension_api_version.to_s,
+ "with_extension-1.0",
+ "gem.build_complete"
+ )
+ expect(build_complete).not_to exist
+ expect(err).to include("with_extension-1.0 contains native extensions that were not built")
+ expect(err).to include("unset no_build_extension and run `bundle pristine with_extension`")
+ end
+
+ it "builds native extensions by default" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_extension"
+ gem "rake"
+ G
+
+ bundle :install
+
+ expect(out).to include("Installing with_extension 1.0 with native extensions")
+ end
+end
diff --git a/spec/bundler/install/gems/no_install_plugin_spec.rb b/spec/bundler/install/gems/no_install_plugin_spec.rb
new file mode 100644
index 0000000000..e040e6b813
--- /dev/null
+++ b/spec/bundler/install/gems/no_install_plugin_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with --no-install-plugin" do
+ before do
+ build_repo2 do
+ build_gem "with_plugin", "1.0" do |s|
+ s.write "lib/rubygems_plugin.rb", "# plugin code"
+ end
+
+ build_gem "with_plugin", "2.0"
+ end
+ end
+
+ let(:plugin_path) { default_bundle_path("plugins", "with_plugin_plugin.rb") }
+
+ it "does not generate the plugin wrapper and warns when no_install_plugin is set" do
+ bundle_config "no_install_plugin true"
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "1.0"
+ G
+
+ expect(plugin_path).not_to exist
+ expect(err).to include("with_plugin-1.0 contains plugins that were not installed")
+ expect(err).to include("unset no_install_plugin and run `bundle pristine with_plugin`")
+ end
+
+ it "removes a stale plugin wrapper from a prior version when no_install_plugin is set" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "1.0"
+ G
+ expect(plugin_path).to exist
+
+ bundle_config "no_install_plugin true"
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "2.0"
+ G
+
+ expect(plugin_path).not_to exist
+ end
+
+ it "generates the plugin wrapper by default" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_plugin", "1.0"
+ G
+
+ expect(plugin_path).to exist
+ end
+end
diff --git a/spec/bundler/install/gems/post_install_spec.rb b/spec/bundler/install/gems/post_install_spec.rb
new file mode 100644
index 0000000000..e49fd2a9a3
--- /dev/null
+++ b/spec/bundler/install/gems/post_install_spec.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "with gem sources" do
+ context "when gems include post install messages" do
+ it "should display the post-install messages after installing" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ gem 'thin'
+ gem 'myrack-obama'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ expect(out).to include("Post-install message from thin:")
+ expect(out).to include("Thin's post install message")
+ expect(out).to include("Post-install message from myrack-obama:")
+ expect(out).to include("Myrack-obama's post install message")
+ end
+ end
+
+ context "when gems do not include post install messages" do
+ it "should not display any post-install messages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ G
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+
+ context "when a dependency includes a post install message" do
+ it "should display the post install message" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack_middleware'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+ end
+ end
+
+ context "with git sources" do
+ context "when gems include post install messages" do
+ it "should display the post-install messages after installing" do
+ build_git "foo" do |s|
+ s.post_install_message = "Foo's post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from foo:")
+ expect(out).to include("Foo's post install message")
+ end
+
+ it "should display the post-install messages if repo is updated" do
+ build_git "foo" do |s|
+ s.post_install_message = "Foo's post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+ bundle :install
+
+ build_git "foo", "1.1" do |s|
+ s.post_install_message = "Foo's 1.1 post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.1")}'
+ G
+ bundle :install
+
+ expect(out).to include("Post-install message from foo:")
+ expect(out).to include("Foo's 1.1 post install message")
+ end
+
+ it "should not display the post-install messages if repo is not updated" do
+ build_git "foo" do |s|
+ s.post_install_message = "Foo's post install message"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :install
+ expect(out).to include("Post-install message from foo:")
+ expect(out).to include("Foo's post install message")
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+
+ context "when gems do not include post install messages" do
+ it "should not display any post-install messages" do
+ build_git "foo" do |s|
+ s.post_install_message = nil
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => '#{lib_path("foo-1.0")}'
+ G
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+ end
+
+ context "when ignore post-install messages for gem is set" do
+ it "doesn't display any post-install messages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "ignore_messages.myrack true"
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+
+ context "when ignore post-install messages for all gems" do
+ it "doesn't display any post-install messages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "ignore_messages true"
+
+ bundle :install
+ expect(out).not_to include("Post-install message")
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb
new file mode 100644
index 0000000000..111d361aab
--- /dev/null
+++ b/spec/bundler/install/gems/resolving_spec.rb
@@ -0,0 +1,786 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with install-time dependencies" do
+ before do
+ build_repo2 do
+ build_gem "with_implicit_rake_dep" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/implicit_rake_dep.rb", "w") do |f|
+ f.puts "IMPLICIT_RAKE_DEP = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ build_gem "another_implicit_rake_dep" do |s|
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/another_implicit_rake_dep.rb", "w") do |f|
+ f.puts "ANOTHER_IMPLICIT_RAKE_DEP = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ # Test complicated gem dependencies for install
+ build_gem "net_a" do |s|
+ s.add_dependency "net_b"
+ s.add_dependency "net_build_extensions"
+ end
+
+ build_gem "net_b"
+
+ build_gem "net_build_extensions" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/net_build_extensions.rb", "w") do |f|
+ f.puts "NET_BUILD_EXTENSIONS = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ build_gem "net_c" do |s|
+ s.add_dependency "net_a"
+ s.add_dependency "net_d"
+ end
+
+ build_gem "net_d"
+
+ build_gem "net_e" do |s|
+ s.add_dependency "net_d"
+ end
+ end
+ end
+
+ it "installs gems with implicit rake dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_implicit_rake_dep"
+ gem "another_implicit_rake_dep"
+ gem "rake"
+ G
+
+ run <<-R
+ require 'implicit_rake_dep'
+ require 'another_implicit_rake_dep'
+ puts IMPLICIT_RAKE_DEP
+ puts ANOTHER_IMPLICIT_RAKE_DEP
+ R
+ expect(out).to eq("YES\nYES")
+ end
+
+ it "installs gems with implicit rake dependencies without rake previously installed" do
+ with_path_as("") do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "with_implicit_rake_dep"
+ gem "another_implicit_rake_dep"
+ gem "rake"
+ G
+ end
+
+ run <<-R
+ require 'implicit_rake_dep'
+ require 'another_implicit_rake_dep'
+ puts IMPLICIT_RAKE_DEP
+ puts ANOTHER_IMPLICIT_RAKE_DEP
+ R
+ expect(out).to eq("YES\nYES")
+ end
+
+ it "does not install gems with a dependency with no type" do
+ build_repo2
+
+ path = "#{gem_repo2}/#{Gem::MARSHAL_SPEC_DIR}/actionpack-2.3.2.gemspec.rz"
+ spec = Marshal.load(Bundler.rubygems.inflate(File.binread(path)))
+ spec.dependencies.each do |d|
+ d.instance_variable_set(:@type, "fail")
+ end
+ File.open(path, "wb") do |f|
+ f.write Gem.deflate(Marshal.dump(spec))
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem "actionpack", "2.3.2"
+ G
+
+ expect(err).to include("Downloading actionpack-2.3.2 revealed dependencies not in the API (activesupport (= 2.3.2)).")
+
+ expect(the_bundle).not_to include_gems "actionpack 2.3.2", "activesupport 2.3.2"
+ end
+
+ describe "with crazy rubygem plugin stuff" do
+ it "installs plugins" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_b"
+ G
+
+ expect(the_bundle).to include_gems "net_b 1.0"
+ end
+
+ it "installs plugins depended on by other plugins" do
+ install_gemfile <<-G, env: { "DEBUG" => "1" }
+ source "https://gem.repo2"
+ gem "net_a"
+ G
+
+ expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0"
+ end
+
+ it "installs multiple levels of dependencies" do
+ install_gemfile <<-G, env: { "DEBUG" => "1" }
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ expect(the_bundle).to include_gems "net_a 1.0", "net_b 1.0", "net_c 1.0", "net_d 1.0", "net_e 1.0"
+ end
+
+ context "with ENV['BUNDLER_DEBUG_RESOLVER'] set" do
+ it "produces debug output" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ bundle :install, env: { "BUNDLER_DEBUG_RESOLVER" => "1", "DEBUG" => "1" }
+
+ expect(out).to include("Resolving dependencies...")
+ end
+ end
+
+ context "with ENV['DEBUG_RESOLVER'] set" do
+ it "produces debug output" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ bundle :install, env: { "DEBUG_RESOLVER" => "1", "DEBUG" => "1" }
+
+ expect(out).to include("Resolving dependencies...")
+ end
+ end
+
+ context "with ENV['DEBUG_RESOLVER_TREE'] set" do
+ it "produces debug output" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "net_c"
+ gem "net_e"
+ G
+
+ bundle :install, env: { "DEBUG_RESOLVER_TREE" => "1", "DEBUG" => "1" }
+
+ expect(out).to include(" net_b").
+ and include("Resolving dependencies...").
+ and include("Solution found after 1 attempts:").
+ and include("selected net_b 1.0")
+ end
+ end
+ end
+
+ describe "when a required ruby version" do
+ context "allows only an older version" do
+ it "installs the older version" do
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ end
+
+ install_gemfile <<-G
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+
+ it "installs the older version when using servers not implementing the compact index API" do
+ build_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ end
+
+ install_gemfile <<-G, artifice: "endpoint"
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo2"
+ gem 'myrack'
+ G
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+
+ context "when there is a lockfile using the newer incompatible version" do
+ before do
+ build_repo2 do
+ build_gem "parallel_tests", "3.7.0" do |s|
+ s.required_ruby_version = ">= #{current_ruby_minor}"
+ end
+
+ build_gem "parallel_tests", "3.8.0" do |s|
+ s.required_ruby_version = ">= #{next_ruby_minor}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'parallel_tests'
+ G
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo2, "parallel_tests", "3.8.0"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ parallel_tests (3.8.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ parallel_tests
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically updates lockfile to use the older version" do
+ bundle "install --verbose"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "parallel_tests", "3.7.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ parallel_tests (3.7.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ parallel_tests
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "gives a meaningful error if we're in frozen mode" do
+ expect do
+ bundle "install", env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ end.not_to change { lockfile }
+
+ expect(err).to eq("parallel_tests-3.8.0 requires ruby version >= #{next_ruby_minor}, which is incompatible with the current version, #{Gem.ruby_version}")
+ end
+ end
+
+ context "with transitive dependencies in a lockfile" do
+ before do
+ build_repo2 do
+ build_gem "rubocop", "1.28.2" do |s|
+ s.required_ruby_version = ">= #{current_ruby_minor}"
+
+ s.add_dependency "rubocop-ast", ">= 1.17.0", "< 2.0"
+ end
+
+ build_gem "rubocop", "1.35.0" do |s|
+ s.required_ruby_version = ">= #{next_ruby_minor}"
+
+ s.add_dependency "rubocop-ast", ">= 1.20.1", "< 2.0"
+ end
+
+ build_gem "rubocop-ast", "1.17.0" do |s|
+ s.required_ruby_version = ">= #{current_ruby_minor}"
+ end
+
+ build_gem "rubocop-ast", "1.21.0" do |s|
+ s.required_ruby_version = ">= #{next_ruby_minor}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'rubocop'
+ G
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo2, "rubocop", "1.35.0"
+ c.checksum gem_repo2, "rubocop-ast", "1.21.0"
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ rubocop (1.35.0)
+ rubocop-ast (>= 1.20.1, < 2.0)
+ rubocop-ast (1.21.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rubocop
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically updates lockfile to use the older compatible versions" do
+ bundle "install --verbose"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "rubocop", "1.28.2"
+ c.checksum gem_repo2, "rubocop-ast", "1.17.0"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ rubocop (1.28.2)
+ rubocop-ast (>= 1.17.0, < 2.0)
+ rubocop-ast (1.17.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rubocop
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with a Gemfile and lockfile that don't resolve under the current platform" do
+ before do
+ build_repo4 do
+ build_gem "sorbet", "0.5.10554" do |s|
+ s.add_dependency "sorbet-static", "0.5.10554"
+ end
+
+ build_gem "sorbet-static", "0.5.10554" do |s|
+ s.platform = "universal-darwin-21"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem 'sorbet', '= 0.5.10554'
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ sorbet (0.5.10554)
+ sorbet-static (= 0.5.10554)
+ sorbet-static (0.5.10554-universal-darwin-21)
+
+ PLATFORMS
+ arm64-darwin-21
+
+ DEPENDENCIES
+ sorbet (= 0.5.10554)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a proper error" do
+ simulate_platform "aarch64-linux" do
+ bundle "install", raise_on_error: false
+ end
+
+ nice_error = <<~E.strip
+ Could not find gems matching 'sorbet-static (= 0.5.10554)' valid for all resolution platforms (arm64-darwin-21, aarch64-linux) in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static (= 0.5.10554)':
+ * sorbet-static-0.5.10554-universal-darwin-21
+ E
+ expect(err).to include(nice_error)
+ expect(err).to include("Your current platform (aarch64-linux) is not included in the lockfile's platforms (arm64-darwin-21)")
+ expect(err).to include("bundle lock --add-platform aarch64-linux")
+ end
+ end
+
+ context "when adding a new gem that does not resolve under all locked platforms" do
+ before do
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+ build_gem "nokogiri", "1.14.0" do |s|
+ s.platform = "arm-linux"
+ end
+
+ build_gem "sorbet-static", "0.5.10696" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.14.0-arm-linux)
+ nokogiri (1.14.0-x86_64-linux)
+
+ PLATFORMS
+ arm-linux
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ gem "sorbet-static"
+ G
+
+ bundle "lock", raise_on_error: false
+ end
+ end
+
+ it "raises a proper error" do
+ nice_error = <<~E.strip
+ Could not find gems matching 'sorbet-static' valid for all resolution platforms (arm-linux, x86_64-linux) in rubygems repository https://gem.repo4/ or installed locally.
+
+ The source contains the following gems matching 'sorbet-static':
+ * sorbet-static-0.5.10696-x86_64-linux
+ E
+ expect(err).to end_with(nice_error)
+ end
+ end
+
+ context "when locked generic variant supports current Ruby, but locked specific variant does not" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.16.3)
+ nokogiri (1.16.3-x86_64-linux)
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ build_repo4 do
+ build_gem "nokogiri", "1.16.3"
+ build_gem "nokogiri", "1.16.3" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "nokogiri"
+ G
+
+ lockfile original_lockfile
+ end
+
+ it "keeps both variants in the lockfile when installing, and uses the generic one since it's compatible" do
+ simulate_platform "x86_64-linux" do
+ bundle "install --verbose"
+
+ expect(lockfile).to eq(original_lockfile)
+ expect(the_bundle).to include_gems("nokogiri 1.16.3")
+ end
+ end
+
+ it "keeps both variants in the lockfile when updating, and uses the generic one since it's compatible" do
+ simulate_platform "x86_64-linux" do
+ bundle "update --verbose"
+
+ expect(lockfile).to eq(original_lockfile)
+ expect(the_bundle).to include_gems("nokogiri 1.16.3")
+ end
+ end
+ end
+
+ it "gives a meaningful error on ruby version mismatches between dependencies" do
+ build_repo4 do
+ build_gem "requires-old-ruby" do |s|
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = ">= #{Gem.ruby_version}"
+
+ s.add_dependency "requires-old-ruby"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gemspec
+ G
+
+ expect(err).to end_with <<~E.strip
+ Could not find compatible versions
+
+ Because every version of foo depends on requires-old-ruby >= 0
+ and every version of requires-old-ruby depends on Ruby < #{Gem.ruby_version},
+ every version of foo requires Ruby < #{Gem.ruby_version}.
+ So, because Gemfile depends on foo >= 0
+ and current Ruby version is = #{Gem.ruby_version},
+ version solving has failed.
+ E
+ end
+
+ it "installs the older version under rate limiting conditions" do
+ build_repo4 do
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ build_gem "myrack", "1.2"
+ build_gem "foo1", "1.0"
+ end
+
+ install_gemfile <<-G, artifice: "compact_index_rate_limited"
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo4"
+ gem 'myrack'
+ gem 'foo1'
+ G
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+
+ it "installs the older not platform specific version" do
+ build_repo4 do
+ build_gem "myrack", "9001.0.0" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ build_gem "myrack", "1.2" do |s|
+ s.platform = "x86-mingw32"
+ s.required_ruby_version = "> 9000"
+ end
+ build_gem "myrack", "1.2"
+ end
+
+ simulate_platform "x86-mingw32" do
+ install_gemfile <<-G, artifice: "compact_index"
+ ruby "#{Gem.ruby_version}"
+ source "https://gem.repo4"
+ gem 'myrack'
+ G
+ end
+
+ expect(err).to_not include("myrack-9001.0.0 requires ruby version > 9000")
+ expect(err).to_not include("myrack-1.2-#{Bundler.local_platform} requires ruby version > 9000")
+ expect(the_bundle).to include_gems("myrack 1.2")
+ end
+ end
+
+ context "allows no gems" do
+ before do
+ build_repo2 do
+ build_gem "require_ruby" do |s|
+ s.required_ruby_version = "> 9000"
+ end
+ end
+ end
+
+ let(:ruby_requirement) { %("#{Gem.ruby_version}") }
+ let(:error_message_requirement) { "= #{Gem.ruby_version}" }
+
+ it "raises a proper error that mentions the current Ruby version during resolution" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem 'require_ruby'
+ G
+
+ expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of require_ruby depends on Ruby > 9000
+ and Gemfile depends on require_ruby >= 0,
+ Ruby > 9000 is required.
+ So, because current Ruby version is #{error_message_requirement},
+ version solving has failed.
+ E
+ expect(err).to end_with(nice_error)
+ end
+
+ shared_examples_for "ruby version conflicts" do
+ it "raises an error during resolution" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ ruby #{ruby_requirement}
+ gem 'require_ruby'
+ G
+
+ expect(out).to_not include("Gem::InstallError: require_ruby requires Ruby version > 9000")
+
+ nice_error = <<~E.strip
+ Could not find compatible versions
+
+ Because every version of require_ruby depends on Ruby > 9000
+ and Gemfile depends on require_ruby >= 0,
+ Ruby > 9000 is required.
+ So, because current Ruby version is #{error_message_requirement},
+ version solving has failed.
+ E
+ expect(err).to end_with(nice_error)
+ end
+ end
+
+ it_behaves_like "ruby version conflicts"
+
+ describe "with a < requirement" do
+ let(:ruby_requirement) { %("< 5000") }
+
+ it_behaves_like "ruby version conflicts"
+ end
+
+ describe "with a compound requirement" do
+ let(:reqs) { ["> 0.1", "< 5000"] }
+ let(:ruby_requirement) { reqs.map(&:dump).join(", ") }
+
+ it_behaves_like "ruby version conflicts"
+ end
+ end
+ end
+
+ describe "when a required rubygems version disallows a gem" do
+ it "does not try to install those gems" do
+ build_repo2 do
+ build_gem "require_rubygems" do |s|
+ s.required_rubygems_version = "> 9000"
+ end
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2"
+ gem 'require_rubygems'
+ G
+
+ expect(err).to_not include("Gem::InstallError: require_rubygems requires RubyGems version > 9000")
+ nice_error = <<~E.strip
+ Because every version of require_rubygems depends on RubyGems > 9000
+ and Gemfile depends on require_rubygems >= 0,
+ RubyGems > 9000 is required.
+ So, because current RubyGems version is = #{Gem::VERSION},
+ version solving has failed.
+ E
+ expect(err).to end_with(nice_error)
+ end
+ end
+
+ context "when non platform specific gems bring more dependencies", :truffleruby_only do
+ before do
+ build_repo4 do
+ build_gem "foo", "1.0" do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "foo", "2.0" do |s|
+ s.platform = "x86_64-linux"
+ end
+
+ build_gem "bar"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ G
+ end
+
+ it "locks both ruby and current platform, and resolve to ruby variants that install on truffleruby" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "foo", "1.0"
+ c.checksum gem_repo4, "bar", "1.0"
+ end
+
+ simulate_platform "x86_64-linux" do
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bar (1.0)
+ foo (1.0)
+ bar
+
+ PLATFORMS
+ ruby
+ x86_64-linux
+
+ DEPENDENCIES
+ foo
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+ end
+end
diff --git a/spec/bundler/install/gems/standalone_spec.rb b/spec/bundler/install/gems/standalone_spec.rb
new file mode 100644
index 0000000000..96a305bb76
--- /dev/null
+++ b/spec/bundler/install/gems/standalone_spec.rb
@@ -0,0 +1,524 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install --standalone" do
+ shared_examples "common functionality" do
+ it "still makes the gems available to normal bundler" do
+ args = expected_gems.map {|k, v| "#{k} #{v}" }
+ expect(the_bundle).to include_gems(*args)
+ end
+
+ it "still makes system gems unavailable to normal bundler" do
+ system_gems "myrack-1.0.0"
+
+ expect(the_bundle).to_not include_gems("myrack")
+ end
+
+ it "generates a bundle/bundler/setup.rb" do
+ expect(bundled_app("bundle/bundler/setup.rb")).to exist
+ end
+
+ it "makes the gems available without bundler" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ ruby testrb
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "makes the gems available without bundler nor rubygems" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ in_bundled_app %(#{Gem.ruby} --disable-gems -w -e #{testrb.shellescape})
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "makes the gems available without bundler via Kernel.require" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nKernel.require \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ ruby testrb
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "makes system gems unavailable without bundler" do
+ system_gems "myrack-1.0.0"
+
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ begin
+ require "myrack"
+ rescue LoadError
+ puts "LoadError"
+ end
+ RUBY
+ ruby testrb
+
+ expect(out).to eq("LoadError")
+ end
+
+ it "works on a different system" do
+ begin
+ FileUtils.mv(bundled_app, "#{bundled_app}2")
+ rescue Errno::ENOTEMPTY
+ puts "Couldn't rename test app since the target folder has these files: #{Dir.glob("#{bundled_app}2/*")}"
+ raise
+ end
+
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ ruby testrb, dir: "#{bundled_app}2"
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+
+ it "skips activating gems" do
+ testrb = String.new <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ gem "do_not_activate_me"
+ RUBY
+ expected_gems.each do |k, _|
+ testrb << "\nrequire \"#{k}\""
+ testrb << "\nputs #{k.upcase}"
+ end
+ in_bundled_app %(#{Gem.ruby} -w -e #{testrb.shellescape})
+
+ expect(out).to eq(expected_gems.values.join("\n"))
+ end
+ end
+
+ describe "with simple gems" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+ end
+
+ describe "with default gems and a lockfile", :ruby_repo do
+ it "works and points to the vendored copies, not to the default copies" do
+ base_system_gems "stringio", "psych", "etc", path: scoped_gem_path(bundled_app("bundle"))
+
+ build_gem "foo", "1.0.0", to_system: true, default: true do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "bar", "1.0.0", to_system: true, default: true
+
+ build_repo4 do
+ build_gem "foo", "1.0.0" do |s|
+ s.add_dependency "bar"
+ end
+
+ build_gem "bar", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ G
+
+ bundle "lock", dir: cwd
+
+ bundle_config "path #{bundled_app("bundle")}"
+
+ bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s }
+
+ load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") }
+
+ expect(load_path_lines).to eq [
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/bar-1.0.0/lib")',
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/foo-1.0.0/lib")',
+ ]
+ end
+
+ it "works for gems with extensions and points to the vendored copies, not to the default copies" do
+ simulate_platform "arm64-darwin-23" do
+ base_system_gems "stringio", "psych", "etc", "shellwords", "open3", path: scoped_gem_path(bundled_app("bundle"))
+
+ build_gem "baz", "1.0.0", to_system: true, default: true, &:add_c_extension
+
+ build_repo4 do
+ build_gem "baz", "1.0.0", &:add_c_extension
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "baz"
+ G
+
+ bundle_config "path #{bundled_app("bundle")}"
+
+ bundle "lock", dir: cwd
+
+ bundle :install, standalone: true, dir: cwd, env: { "BUNDLER_GEM_DEFAULT_DIR" => system_gem_path.to_s }
+ end
+
+ load_path_lines = bundled_app("bundle/bundler/setup.rb").read.split("\n").select {|line| line.start_with?("$:.unshift") }
+
+ expect(load_path_lines).to eq [
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/arm64-darwin-23/#{Gem.extension_api_version}/baz-1.0.0")',
+ '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/gems/baz-1.0.0/lib")',
+ ]
+ end
+ end
+
+ describe "with Gemfiles using absolute path sources and resulting bundle moved to a folder hierarchy with different nesting" do
+ before do
+ build_lib "minitest", "1.0.0", path: lib_path("minitest")
+
+ Dir.mkdir bundled_app("app")
+
+ gemfile bundled_app("app/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gem "minitest", :path => "#{lib_path("minitest")}"
+ G
+
+ bundle "install", standalone: true, dir: bundled_app("app")
+
+ Dir.mkdir tmp("one_more_level")
+ FileUtils.mv bundled_app, tmp("one_more_level")
+ end
+
+ it "also works" do
+ ruby <<-RUBY, dir: tmp("one_more_level/bundled_app/app")
+ require "./bundle/bundler/setup"
+
+ require "minitest"
+ puts MINITEST
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+ end
+
+ let(:cwd) { bundled_app }
+
+ describe "with Gemfiles using relative path sources and app moved to a different root" do
+ before do
+ FileUtils.mkdir_p bundled_app("app/vendor")
+
+ build_lib "minitest", "1.0.0", path: bundled_app("app/vendor/minitest")
+
+ gemfile bundled_app("app/Gemfile"), <<-G
+ source "https://gem.repo1"
+ gem "minitest", :path => "vendor/minitest"
+ G
+
+ bundle "install", standalone: true, dir: bundled_app("app")
+
+ FileUtils.mv(bundled_app("app"), bundled_app2("app"))
+ end
+
+ it "also works" do
+ ruby <<-RUBY, dir: bundled_app2("app")
+ require "./bundle/bundler/setup"
+
+ require "minitest"
+ puts MINITEST
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "with gems with native extension" do
+ before do
+ bundle_config "path #{bundled_app("bundle")}"
+ install_gemfile <<-G, standalone: true, dir: cwd
+ source "https://gem.repo1"
+ gem "very_simple_binary"
+ G
+ end
+
+ it "generates a bundle/bundler/setup.rb with the proper paths" do
+ expected_path = bundled_app("bundle/bundler/setup.rb")
+ script_content = File.read(expected_path)
+ expect(script_content).to include("def self.ruby_api_version")
+ expect(script_content).to include("def self.extension_api_version")
+ extension_line = script_content.each_line.find {|line| line.include? "/extensions/" }.strip
+ platform = Gem::Platform.local
+ expect(extension_line).to start_with '$:.unshift File.expand_path("#{__dir__}/../#{RUBY_ENGINE}/#{Gem.ruby_api_version}/extensions/'
+ expect(extension_line).to end_with platform.to_s + '/#{Gem.extension_api_version}/very_simple_binary-1.0")'
+ end
+ end
+
+ describe "with gem that has an invalid gemspec" do
+ before do
+ build_git "bar", gemspec: false do |s|
+ s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ lib = File.expand_path('lib/', __dir__)
+ $:.unshift lib unless $:.include?(lib)
+ require 'bar/version'
+
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ s.author = 'Anonymous'
+ s.require_path = [1,2]
+ end
+ G
+ end
+ bundle_config "path #{bundled_app("bundle")}"
+ install_gemfile <<-G, standalone: true, dir: cwd, raise_on_error: false
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+ end
+
+ it "outputs a helpful error message" do
+ expect(err).to include("You have one or more invalid gemspecs that need to be fixed.")
+ expect(err).to include("bar.gemspec is not valid")
+ end
+ end
+
+ describe "with a combination of gems and git repos" do
+ before do
+ build_git "devise", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ gem "devise", :git => "#{lib_path("devise-1.0")}"
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "devise" => "1.0",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+ end
+
+ describe "with groups" do
+ before do
+ build_git "devise", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+
+ group :test do
+ gem "rspec"
+ gem "myrack-test"
+ end
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+
+ it "allows creating a standalone file with limited groups" do
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: "default", dir: cwd
+
+ load_error_ruby <<-RUBY, "spec"
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+
+ it "allows `without` configuration to limit the groups used in a standalone" do
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle_config "without test"
+ bundle :install, standalone: true, dir: cwd
+
+ load_error_ruby <<-RUBY, "spec"
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+
+ it "allows `path` configuration to change the location of the standalone bundle" do
+ bundle_config "path path/to/bundle"
+ bundle "install", standalone: true, dir: cwd
+
+ ruby <<-RUBY
+ $:.unshift File.expand_path("path/to/bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ end
+
+ it "allows `without` to limit the groups used in a standalone" do
+ bundle_config "without test"
+ bundle :install, dir: cwd
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, dir: cwd
+
+ load_error_ruby <<-RUBY, "spec"
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "actionpack"
+ puts ACTIONPACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("2.3.2")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+ end
+
+ describe "with gemcutter's dependency API" do
+ let(:source_uri) { "http://localgemserver.test" }
+
+ describe "simple gems" do
+ before do
+ gemfile <<-G
+ source "#{source_uri}"
+ gem "rails"
+ G
+ bundle_config "path #{bundled_app("bundle")}"
+ bundle :install, standalone: true, artifice: "endpoint", dir: cwd
+ end
+
+ let(:expected_gems) do
+ {
+ "actionpack" => "2.3.2",
+ "rails" => "2.3.2",
+ }
+ end
+
+ include_examples "common functionality"
+ end
+ end
+end
+
+RSpec.describe "bundle install --standalone run in a subdirectory" do
+ let(:cwd) { bundled_app("bob").tap(&:mkpath) }
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+ end
+
+ it "generates the script in the proper place" do
+ bundle :install, standalone: true, dir: cwd
+
+ expect(bundled_app("bundle/bundler/setup.rb")).to exist
+ end
+
+ context "when path set to a relative path" do
+ before do
+ bundle_config "path bundle"
+ end
+
+ it "generates the script in the proper place" do
+ bundle :install, standalone: true, dir: cwd
+
+ expect(bundled_app("bundle/bundler/setup.rb")).to exist
+ end
+ end
+end
+
+RSpec.describe "bundle install --standalone --local" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+ end
+
+ it "generates script pointing to system gems" do
+ bundle "install --standalone --local --verbose"
+
+ expect(out).to include("Using myrack 1.0.0")
+
+ load_error_ruby <<-RUBY, "spec"
+ require "./bundler/setup"
+
+ require "myrack"
+ puts MYRACK
+ require "spec"
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err_without_deprecations).to match(/cannot load such file -- spec/)
+ end
+end
diff --git a/spec/bundler/install/gems/win32_spec.rb b/spec/bundler/install/gems/win32_spec.rb
new file mode 100644
index 0000000000..be37673aa1
--- /dev/null
+++ b/spec/bundler/install/gems/win32_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install with win32-generated lockfile" do
+ it "should read lockfile" do
+ File.open(bundled_app_lock, "wb") do |f|
+ f << "GEM\r\n"
+ f << " remote: https://gem.repo1/\r\n"
+ f << " specs:\r\n"
+ f << "\r\n"
+ f << " myrack (1.0.0)\r\n"
+ f << "\r\n"
+ f << "PLATFORMS\r\n"
+ f << " ruby\r\n"
+ f << "\r\n"
+ f << "DEPENDENCIES\r\n"
+ f << " myrack\r\n"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "myrack"
+ G
+ end
+end
diff --git a/spec/bundler/install/gemspecs_spec.rb b/spec/bundler/install/gemspecs_spec.rb
new file mode 100644
index 0000000000..fb2271c830
--- /dev/null
+++ b/spec/bundler/install/gemspecs_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "when a gem has a YAML gemspec" do
+ before :each do
+ build_repo2 do
+ build_gem "yaml_spec", gemspec: :yaml
+ end
+ end
+
+ it "still installs correctly" do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "yaml_spec"
+ G
+ bundle :install
+ expect(err).to be_empty
+ end
+
+ it "still installs correctly when using path" do
+ build_lib "yaml_spec", gemspec: :yaml
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'yaml_spec', :path => "#{lib_path("yaml_spec-1.0")}"
+ G
+ expect(err).to be_empty
+ end
+ end
+
+ it "should use gemspecs in the system cache when available" do
+ gemfile <<-G
+ source "http://localgemserver.test"
+ gem 'myrack'
+ G
+
+ system_gems "myrack-1.0.0", path: default_bundle_path
+
+ FileUtils.mkdir_p "#{default_bundle_path}/specifications"
+ File.open("#{default_bundle_path}/specifications/myrack-1.0.0.gemspec", "w+") do |f|
+ spec = Gem::Specification.new do |s|
+ s.name = "myrack"
+ s.version = "1.0.0"
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ f.write spec.to_ruby
+ end
+ bundle :install, artifice: "endpoint_marshal_fail" # force gemspec load
+ expect(the_bundle).to include_gems "myrack 1.0.0", "activesupport 2.3.2"
+ end
+
+ it "does not hang when gemspec has incompatible encoding" do
+ create_file("foo.gemspec", <<-G)
+ Gem::Specification.new do |gem|
+ gem.name = "pry-byebug"
+ gem.version = "3.4.2"
+ gem.author = "David Rodríguez"
+ gem.summary = "Good stuff"
+ end
+ G
+
+ install_gemfile <<-G, env: { "LANG" => "C" }
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "reads gemspecs respecting their encoding" do
+ create_file "version.rb", <<-RUBY
+ module Persistent💎
+ VERSION = "0.0.1"
+ end
+ RUBY
+
+ create_file "persistent-dmnd.gemspec", <<-G
+ require_relative "version"
+
+ Gem::Specification.new do |gem|
+ gem.name = "persistent-dmnd"
+ gem.version = Persistent💎::VERSION
+ gem.author = "Ivo Anjo"
+ gem.summary = "Unscratchable stuff"
+ end
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ context "when ruby version is specified in gemspec and gemfile" do
+ it "installs when patch level is not specified and the version matches",
+ if: RUBY_PATCHLEVEL >= 0 do
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = "~> #{RUBY_VERSION}.0"
+ end
+
+ install_gemfile <<-G
+ ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby'
+ source "https://gem.repo1"
+ gemspec
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "installs when patch level is specified and the version still matches the current version",
+ if: RUBY_PATCHLEVEL >= 0 do
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = "#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{RUBY_PATCHLEVEL}'
+ source "https://gem.repo1"
+ gemspec
+ G
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "installs gems ignoring the mismatch even when patchlevel is mismatch",
+ if: RUBY_PATCHLEVEL >= 0 do
+ patchlevel = RUBY_PATCHLEVEL.to_i + 1
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = "#{RUBY_VERSION}.#{patchlevel}"
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ ruby '#{RUBY_VERSION}', :engine_version => '#{RUBY_VERSION}', :engine => 'ruby', :patchlevel => '#{patchlevel}'
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+
+ it "fails and complains about version on version mismatch" do
+ version = Gem::Requirement.create(RUBY_VERSION).requirements.first.last.bump.version
+
+ build_lib("foo", path: bundled_app) do |s|
+ s.required_ruby_version = version
+ end
+
+ install_gemfile <<-G, raise_on_error: false
+ ruby '#{version}', :engine_version => '#{version}', :engine => 'ruby'
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ expect(err).to include("Ruby version")
+ expect(err).to include("but your Gemfile specified")
+ expect(exitstatus).to eq(18)
+ end
+
+ it "validates gemspecs just once when everything installed and lockfile up to date" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec path: "#{lib_path("foo-1.0")}"
+
+ module Monkey
+ def validate(spec)
+ puts "Validate called on \#{spec.full_name}"
+ end
+ end
+ Bundler.rubygems.extend(Monkey)
+ G
+
+ bundle "install"
+
+ expect(out).to include("Validate called on foo-1.0").once
+ end
+ end
+end
diff --git a/spec/bundler/install/git_spec.rb b/spec/bundler/install/git_spec.rb
new file mode 100644
index 0000000000..1172d661ae
--- /dev/null
+++ b/spec/bundler/install/git_spec.rb
@@ -0,0 +1,369 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ context "git sources" do
+ it "displays the revision hash of the gem repository" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "displays the revision hash of the gem repository when passed a relative local path" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ relative_path = lib_path("foo").relative_path_from(bundled_app)
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{relative_path}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{relative_path} (at main@#{revision_for(lib_path("foo"))[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "displays the correct default branch", git: ">= 2.28.0" do
+ build_git "foo", "1.0", path: lib_path("foo"), default_branch: "non-standard"
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at non-standard@#{revision_for(lib_path("foo"))[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "displays the ref of the gem repository when using branch~num as a ref" do
+ skip "maybe branch~num notation doesn't work on Windows' git" if Gem.win_platform?
+
+ build_git "foo", "1.0", path: lib_path("foo")
+ rev = revision_for(lib_path("foo"))[0..6]
+ update_git "foo", "2.0", path: lib_path("foo"), gemspec: true
+ rev2 = revision_for(lib_path("foo"))[0..6]
+ update_git "foo", "3.0", path: lib_path("foo"), gemspec: true
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}", :ref => "main~2"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main~2@#{rev})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ update_git "foo", "4.0", path: lib_path("foo"), gemspec: true
+
+ bundle :update, all: true, verbose: true
+ expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main~2@#{rev2})")
+ expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}"
+ end
+
+ it "allows git repos that are missing but not being installed" do
+ revision = build_git("foo").ref_for("HEAD")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :group => :development
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{revision}
+ specs:
+ foo (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ foo!
+ L
+
+ bundle_config "path vendor/bundle"
+ bundle_config "without development"
+ bundle :install
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "allows multiple gems from the same git source" do
+ build_repo2 do
+ build_lib "foo", "1.0", path: lib_path("gems/foo")
+ build_lib "zebra", "2.0", path: lib_path("gems/zebra")
+ build_git "gems", path: lib_path("gems"), gemspec: false
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "foo", :git => "#{lib_path("gems")}", :glob => "foo/*.gemspec"
+ gem "zebra", :git => "#{lib_path("gems")}", :glob => "zebra/*.gemspec"
+ G
+
+ bundle "info foo"
+ expect(out).to include("* foo (1.0 #{revision_for(lib_path("gems"))[0..6]})")
+
+ bundle "info zebra"
+ expect(out).to include("* zebra (2.0 #{revision_for(lib_path("gems"))[0..6]})")
+ end
+
+ it "should always sort dependencies in the same order" do
+ # This Gemfile + lockfile had a problem where the first
+ # `bundle install` would change the order, but the second would
+ # change it back.
+
+ # NOTE: both gems MUST have the same path! It has to be two gems in one repo.
+
+ test = build_git "test", "1.0.0", path: lib_path("test-and-other")
+ other = build_git "other", "1.0.0", path: lib_path("test-and-other")
+ test_ref = test.ref_for("HEAD")
+ other_ref = other.ref_for("HEAD")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "test", git: #{test.path.to_s.inspect}
+ gem "other", ref: #{other_ref.inspect}, git: #{other.path.to_s.inspect}
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{test.path}
+ revision: #{test_ref}
+ specs:
+ test (1.0.0)
+
+ GIT
+ remote: #{other.path}
+ revision: #{other_ref}
+ ref: #{other_ref}
+ specs:
+ other (1.0.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ other!
+ test!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ # If GH#6743 is present, the first `bundle install` will change the
+ # lockfile, by flipping the order (`other` would be moved to the top).
+ #
+ # The second `bundle install` would then change the lockfile back
+ # to the original.
+ #
+ # The fix makes it so it may change it once, but it will not change
+ # it a second time.
+ #
+ # So, we run `bundle install` once, and store the value of the
+ # modified lockfile.
+ bundle :install
+ modified_lockfile = lockfile
+
+ # If GH#6743 is present, the second `bundle install` would change the
+ # lockfile back to what it was originally.
+ #
+ # This `expect` makes sure it doesn't change a second time.
+ bundle :install
+ expect(lockfile).to eq(modified_lockfile)
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "allows older revisions of git source when clean true" do
+ build_git "foo", "1.0", path: lib_path("foo")
+ rev = revision_for(lib_path("foo"))
+
+ bundle_config "path vendor/bundle"
+ bundle_config "clean true"
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ old_lockfile = lockfile
+
+ update_git "foo", "2.0", path: lib_path("foo"), gemspec: true
+ rev2 = revision_for(lib_path("foo"))
+
+ bundle :update, all: true, verbose: true
+ expect(out).to include("Using foo 2.0 (was 1.0) from #{lib_path("foo")} (at main@#{rev2[0..6]})")
+ expect(out).to include("Removing foo (#{rev[0..11]})")
+ expect(the_bundle).to include_gems "foo 2.0", source: "git@#{lib_path("foo")}"
+
+ lockfile(old_lockfile)
+
+ bundle :install, verbose: true
+ expect(out).to include("Using foo 1.0 from #{lib_path("foo")} (at main@#{rev[0..6]})")
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+ end
+
+ context "when install directory exists" do
+ let(:checkout_confirmation_log_message) { "Checking out revision" }
+ let(:using_foo_confirmation_log_message) { "Using foo 1.0 from #{lib_path("foo")} (at main@#{revision_for(lib_path("foo"))[0..6]})" }
+
+ context "and no contents besides .git directory are present" do
+ it "reinstalls gem" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ install_gemfile gemfile, verbose: true
+
+ expect(out).to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ # validate that the installed directory exists and has some expected contents
+ install_directory = default_bundle_path("bundler/gems/foo-#{revision_for(lib_path("foo"))[0..11]}")
+ dot_git_directory = install_directory.join(".git")
+ lib_directory = install_directory.join("lib")
+ gemspec = install_directory.join("foo.gemspec")
+ expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist
+
+ # remove all elements in the install directory except .git directory
+ FileUtils.rm_r(lib_directory)
+ gemspec.delete
+
+ expect(dot_git_directory).to exist
+ expect(lib_directory).not_to exist
+ expect(gemspec).not_to exist
+
+ # rerun bundle install
+ install_gemfile gemfile, verbose: true
+
+ expect(out).to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ # validate that it reinstalls all components
+ expect([install_directory, dot_git_directory, lib_directory, gemspec]).to all exist
+ end
+ end
+
+ context "and contents besides .git directory are present" do
+ # we want to confirm that the change to try to detect partial installs and reinstall does not
+ # result in repeatedly reinstalling the gem when it is fully installed
+ it "does not reinstall gem" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ gemfile = <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ G
+
+ install_gemfile gemfile, verbose: true
+
+ expect(out).to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ expect(the_bundle).to include_gems "foo 1.0", source: "git@#{lib_path("foo")}"
+
+ # rerun bundle install
+ install_gemfile gemfile, verbose: true
+
+ # it isn't altogether straight-forward to validate that bundle didn't do soething on the second run, however,
+ # the presence of the 2nd log message confirms install got past the point that it would have logged the above if
+ # it was going to
+ expect(out).not_to include(checkout_confirmation_log_message)
+ expect(out).to include(using_foo_confirmation_log_message)
+ end
+ end
+ end
+ end
+
+ describe "with excluded groups" do
+ it "works if you exclude a group with a git gem", ruby: ">= 3.3" do
+ build_git "production_gem", "1.0"
+ build_git "development_gem", "1.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "production_gem", :git => "#{lib_path("production_gem-1.0")}"
+
+ group :development do
+ gem "development_gem", :git => "#{lib_path("development_gem-1.0")}"
+ end
+ G
+
+ # First install all groups to create lockfile
+ bundle :install
+
+ # Set without and reinstall
+ bundle_config "without development"
+ bundle :install
+
+ # Verify only production gem is available
+ expect(the_bundle).to include_gems("production_gem 1.0")
+ expect(the_bundle).not_to include_gems("development_gem 1.0")
+ end
+
+ it "resolves indirect dependencies from a git source not in the requested groups" do
+ build_lib "activesupport", "1.0", path: lib_path("rails/activesupport")
+ build_git "activerecord", "1.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 1.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "activerecord", :git => "#{lib_path("rails")}"
+
+ group :ci do
+ gem "myrack"
+ end
+ G
+
+ bundle_config "only ci"
+ bundle :install
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ expect(the_bundle).not_to include_gems("activerecord 1.0")
+ end
+
+ it "resolves indirect dependencies from a git source not in the requested groups (without compact_index dependency API)" do
+ build_lib "activesupport", "1.0", path: lib_path("rails/activesupport")
+ build_git "activerecord", "1.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 1.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "activerecord", :git => "#{lib_path("rails")}"
+
+ group :ci do
+ gem "myrack"
+ end
+ G
+
+ # Force the RubygemsAggregate code path in find_source_requirements by
+ # making the dependency API unavailable.
+ bundle_config "only ci"
+ bundle :install, artifice: "endpoint_api_forbidden"
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ expect(the_bundle).not_to include_gems("activerecord 1.0")
+ end
+ end
+end
diff --git a/spec/bundler/install/global_cache_spec.rb b/spec/bundler/install/global_cache_spec.rb
new file mode 100644
index 0000000000..4cffa65b2a
--- /dev/null
+++ b/spec/bundler/install/global_cache_spec.rb
@@ -0,0 +1,305 @@
+# frozen_string_literal: true
+
+RSpec.describe "global gem caching" do
+ # Uses subprocess because this setting must apply across multiple app directories (bundled_app and bundled_app2)
+ before { bundle "config set global_gem_cache true" }
+
+ describe "using the cross-application user cache" do
+ let(:source) { "http://localgemserver.test" }
+ let(:source2) { "http://gemserver.example.org" }
+
+ def cache_base
+ # Use the unified global gem cache path if available (from RubyGems),
+ # otherwise fall back to the Bundler-specific cache location
+ if Gem.respond_to?(:global_gem_cache_path)
+ Pathname.new(Gem.global_gem_cache_path)
+ else
+ home(".bundle", "cache", "gems")
+ end
+ end
+
+ def source_global_cache(*segments)
+ cache_base.join("localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", *segments)
+ end
+
+ def source2_global_cache(*segments)
+ cache_base.join("gemserver.example.org.80.1ae1663619ffe0a3c9d97712f44c705b", *segments)
+ end
+
+ it "caches gems into the global cache on download" do
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ end
+
+ it "uses globally cached gems if they exist" do
+ source_global_cache.mkpath
+ FileUtils.cp(gem_repo1("gems/myrack-1.0.0.gem"), source_global_cache("myrack-1.0.0.gem"))
+
+ install_gemfile <<-G, artifice: "compact_index_no_gem"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "shows a proper error message if a cached gem is corrupted" do
+ skip "This example is not working on ruby/ruby repo" if ruby_core?
+
+ source_global_cache.mkpath
+ FileUtils.touch(source_global_cache("myrack-1.0.0.gem"))
+
+ install_gemfile <<-G, artifice: "compact_index_no_gem", raise_on_error: false
+ source "#{source}"
+ gem "myrack"
+ G
+
+ expect(err).to include("Gem::Package::FormatError: package metadata is missing in #{source_global_cache("myrack-1.0.0.gem")}")
+ end
+
+ it "uses a shorter path for the cache to not hit filesystem limits" do
+ install_gemfile <<-G, artifice: "compact_index", verbose: true
+ source "http://#{"a" * 255}.test"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ source_segment = "a" * 222 + ".a3cb26de2edfce9f509a65c611d99c4b"
+ source_cache = cache_base.join(source_segment)
+ cached_gem = source_cache.join("myrack-1.0.0.gem")
+ expect(cached_gem).to exist
+ ensure
+ # We cleanup dummy files created by this spec manually because due to a
+ # Ruby on Windows bug, `FileUtils.rm_rf` (run in our global after hook)
+ # cannot traverse directories with such long names. So we delete
+ # everything explicitly to workaround the bug. An alternative workaround
+ # would be to shell out to `rm -rf`. That also works fine, but I went with
+ # the more verbose and explicit approach. This whole ensure block can be
+ # removed once/if https://bugs.ruby-lang.org/issues/21177 is fixed, and
+ # once the fix propagates to all supported rubies.
+ File.delete cached_gem
+ Dir.rmdir source_cache
+
+ File.delete compact_index_cache_path.join(source_segment, "info", "myrack")
+ Dir.rmdir compact_index_cache_path.join(source_segment, "info")
+ File.delete compact_index_cache_path.join(source_segment, "info-etags", "myrack-92f3313ce5721296f14445c3a6b9c073")
+ Dir.rmdir compact_index_cache_path.join(source_segment, "info-etags")
+ Dir.rmdir compact_index_cache_path.join(source_segment, "info-special-characters")
+ File.delete compact_index_cache_path.join(source_segment, "versions")
+ File.delete compact_index_cache_path.join(source_segment, "versions.etag")
+ Dir.rmdir compact_index_cache_path.join(source_segment)
+ end
+
+ describe "when the same gem from different sources is installed" do
+ it "should use the appropriate one from the global cache" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ # myrack 1.0.0 is not installed and it is in the global cache
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source2}"
+ gem "myrack", "0.9.1"
+ G
+
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ # myrack 0.9.1 is not installed and it is in the global cache
+
+ gemfile <<-G
+ source "#{source}"
+ gem "myrack", "1.0.0"
+ G
+
+ bundle :install, artifice: "compact_index_no_gem"
+ # myrack 1.0.0 is installed and myrack 0.9.1 is not
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ pristine_system_gems
+
+ gemfile <<-G
+ source "#{source2}"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle :install, artifice: "compact_index_no_gem"
+ # myrack 0.9.1 is installed and myrack 1.0.0 is not
+ expect(the_bundle).to include_gems "myrack 0.9.1"
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ end
+
+ it "should not install if the wrong source is provided" do
+ bundle_config "path.system true"
+
+ gemfile <<-G
+ source "#{source}"
+ gem "myrack"
+ G
+
+ bundle :install, artifice: "compact_index"
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ # myrack 1.0.0 is not installed and it is in the global cache
+
+ gemfile <<-G
+ source "#{source2}"
+ gem "myrack", "0.9.1"
+ G
+
+ bundle :install, artifice: "compact_index"
+ pristine_system_gems
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ # myrack 0.9.1 is not installed and it is in the global cache
+
+ gemfile <<-G
+ source "#{source2}"
+ gem "myrack", "1.0.0"
+ G
+
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ bundle :install, artifice: "compact_index_no_gem", raise_on_error: false
+ expect(err).to include("Internal Server Error 500")
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ # myrack 1.0.0 is not installed and myrack 0.9.1 is not
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+
+ gemfile <<-G
+ source "#{source}"
+ gem "myrack", "0.9.1"
+ G
+
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source2_global_cache("myrack-0.9.1.gem")).to exist
+ bundle :install, artifice: "compact_index_no_gem", raise_on_error: false
+ expect(err).to include("Internal Server Error 500")
+ expect(err).not_to include("ERROR REPORT TEMPLATE")
+
+ # myrack 0.9.1 is not installed and myrack 1.0.0 is not
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ end
+ end
+
+ describe "when installing gems from a different directory" do
+ it "uses the global cache as a source" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G, artifice: "compact_index"
+ source "#{source}"
+ gem "myrack"
+ gem "activesupport"
+ G
+
+ # Both gems are installed and in the global cache
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+ pristine_system_gems
+ # Both gems are now only in the global cache
+ expect(the_bundle).not_to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+
+ install_gemfile <<-G, artifice: "compact_index_no_gem"
+ source "#{source}"
+ gem "myrack"
+ G
+
+ # myrack is installed and both are in the global cache
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5"
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+
+ create_file bundled_app2("gems.rb"), <<-G
+ source "#{source}"
+ gem "activesupport"
+ G
+
+ # Neither gem is installed and both are in the global cache
+ expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2
+ expect(the_bundle).not_to include_gems "activesupport 2.3.5", dir: bundled_app2
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+
+ # Install using the global cache instead of by downloading the .gem
+ # from the server
+ bundle :install, artifice: "compact_index_no_gem", dir: bundled_app2
+
+ # activesupport is installed and both are in the global cache
+ expect(the_bundle).not_to include_gems "myrack 1.0.0", dir: bundled_app2
+ expect(the_bundle).to include_gems "activesupport 2.3.5", dir: bundled_app2
+
+ expect(source_global_cache("myrack-1.0.0.gem")).to exist
+ expect(source_global_cache("activesupport-2.3.5.gem")).to exist
+ end
+ end
+ end
+
+ describe "extension caching" do
+ it "works" do
+ skip "gets incorrect ref in path" if Gem.win_platform?
+ skip "fails for unknown reason when run by ruby-core" if ruby_core?
+
+ build_git "very_simple_git_binary", &:add_c_extension
+ build_lib "very_simple_path_binary", &:add_c_extension
+ revision = revision_for(lib_path("very_simple_git_binary-1.0"))[0, 12]
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "very_simple_binary"
+ gem "very_simple_git_binary", :git => "#{lib_path("very_simple_git_binary-1.0")}"
+ gem "very_simple_path_binary", :path => "#{lib_path("very_simple_path_binary-1.0")}"
+ G
+
+ gem_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope,
+ "gem.repo1.443.#{Digest(:MD5).hexdigest("gem.repo1.443./")}", "very_simple_binary-1.0")
+ git_binary_cache = home(".bundle", "cache", "extensions", local_platform.to_s, Bundler.ruby_scope,
+ "very_simple_git_binary-1.0-#{revision}", "very_simple_git_binary-1.0")
+
+ cached_extensions = Pathname.glob(home(".bundle", "cache", "extensions", "*", "*", "*", "*", "*")).sort
+ expect(cached_extensions).to eq [gem_binary_cache, git_binary_cache].sort
+
+ run <<-R
+ require 'very_simple_binary_c'; puts ::VERY_SIMPLE_BINARY_IN_C
+ require 'very_simple_git_binary_c'; puts ::VERY_SIMPLE_GIT_BINARY_IN_C
+ R
+ expect(out).to eq "VERY_SIMPLE_BINARY_IN_C\nVERY_SIMPLE_GIT_BINARY_IN_C"
+
+ FileUtils.rm_r Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]
+
+ gem_binary_cache.join("very_simple_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" }
+ git_binary_cache.join("very_simple_git_binary_c.rb").open("w") {|f| f << "puts File.basename(__FILE__)" }
+
+ bundle_config "path different_path"
+ bundle :install
+
+ expect(Dir[home(".bundle", "cache", "extensions", "**", "*binary_c*")]).to all(end_with(".rb"))
+
+ run <<-R
+ require 'very_simple_binary_c'
+ require 'very_simple_git_binary_c'
+ R
+ expect(out).to eq "very_simple_binary_c.rb\nvery_simple_git_binary_c.rb"
+ end
+ end
+end
diff --git a/spec/bundler/install/path_spec.rb b/spec/bundler/install/path_spec.rb
new file mode 100644
index 0000000000..49360e511e
--- /dev/null
+++ b/spec/bundler/install/path_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ describe "with path configured" do
+ before :each do
+ build_gem "myrack", "1.0.0", to_system: true do |s|
+ s.write "lib/myrack.rb", "puts 'FAIL'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "does not use available system gems with `vendor/bundle" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "uses system gems with `path.system` configured with more priority than `path`" do
+ bundle_config "path.system true"
+ bundle_config_global "path vendor/bundle"
+ bundle :install
+ run "require 'myrack'", raise_on_error: false
+ expect(out).to include("FAIL")
+ end
+
+ it "handles paths with regex characters in them" do
+ dir = bundled_app("bun++dle")
+ dir.mkpath
+
+ bundle_config "path #{dir.join("vendor/bundle")}"
+ bundle :install, dir: dir
+ expect(out).to include("installed into `./vendor/bundle`")
+
+ FileUtils.rm_rf dir
+ end
+
+ it "prints a message to let the user know where gems where installed" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ expect(out).to include("gems are installed into `./vendor/bundle`")
+ end
+
+ it "installs the bundle relatively to repository root, when Bundler run from the same directory" do
+ bundle "config set path vendor/bundle", dir: bundled_app.parent
+ bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent
+ expect(out).to include("installed into `./bundled_app/vendor/bundle`")
+ expect(bundled_app("vendor/bundle")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the bundle relatively to repository root, when Bundler run from a different directory" do
+ bundle "config set path vendor/bundle", dir: bundled_app
+ bundle "install --gemfile='#{bundled_app}/Gemfile'", dir: bundled_app.parent
+ expect(out).to include("installed into `./bundled_app/vendor/bundle`")
+ expect(bundled_app("vendor/bundle")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs the standalone bundle relative to the cwd" do
+ bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app.parent
+ expect(out).to include("installed into `./bundled_app/bundle`")
+ expect(bundled_app("bundle")).to be_directory
+ expect(bundled_app("bundle/ruby")).to be_directory
+
+ bundle :install, gemfile: bundled_app_gemfile, standalone: true, dir: bundled_app("subdir").tap(&:mkpath)
+ expect(out).to include("installed into `../bundle`")
+ expect(bundled_app("bundle")).to be_directory
+ expect(bundled_app("bundle/ruby")).to be_directory
+ end
+ end
+
+ describe "when BUNDLE_PATH or the global path config is set" do
+ before :each do
+ build_lib "myrack", "1.0.0", to_system: true do |s|
+ s.write "lib/myrack.rb", "raise 'FAIL'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ def set_bundle_path(type, location)
+ if type == :env
+ ENV["BUNDLE_PATH"] = location
+ elsif type == :global
+ bundle "config set path #{location}", "no-color" => nil
+ end
+ end
+
+ [:env, :global].each do |type|
+ context "when set via #{type}" do
+ it "installs gems to a path if one is specified" do
+ set_bundle_path(type, bundled_app("vendor2").to_s)
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(bundled_app("vendor2")).not_to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems to ." do
+ set_bundle_path(type, ".")
+ bundle_config_global "disable_shared_gems true"
+
+ bundle :install
+
+ paths_to_exist = %w[cache/myrack-1.0.0.gem gems/myrack-1.0.0 specifications/myrack-1.0.0.gemspec].map {|path| bundled_app(Bundler.ruby_scope, path) }
+ expect(paths_to_exist).to all exist
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems to the path" do
+ set_bundle_path(type, bundled_app("vendor").to_s)
+
+ bundle :install
+
+ expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "installs gems to the path relative to root when relative" do
+ set_bundle_path(type, "vendor")
+
+ FileUtils.mkdir_p bundled_app("lol")
+ bundle :install, dir: bundled_app("lol")
+
+ expect(bundled_app("vendor", Bundler.ruby_scope, "gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ it "installs gems to BUNDLE_PATH from .bundle/config" do
+ bundle_config "BUNDLE_PATH" => bundled_app("vendor/bundle").to_s
+
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "sets BUNDLE_PATH as the first argument to bundle install" do
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "disables system gems when passing a path to install" do
+ # This is so that vendored gems can be distributed to others
+ build_gem "myrack", "1.1.0", to_system: true
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/myrack-1.0.0")).to be_directory
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "re-installs gems whose extensions have been deleted" do
+ build_lib "very_simple_binary", "1.0.0", to_system: true do |s|
+ s.write "lib/very_simple_binary.rb", "raise 'FAIL'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "very_simple_binary"
+ G
+
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory
+ expect(vendored_gems("extensions")).to be_directory
+ expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1"
+
+ FileUtils.rm_rf vendored_gems("extensions")
+
+ run "require 'very_simple_binary_c'", raise_on_error: false
+ expect(err).to include("Bundler::GemNotFound")
+
+ bundle_config "path ./vendor/bundle"
+ bundle :install
+
+ expect(vendored_gems("gems/very_simple_binary-1.0")).to be_directory
+ expect(vendored_gems("extensions")).to be_directory
+ expect(the_bundle).to include_gems "very_simple_binary 1.0", source: "remote1"
+ end
+ end
+
+ describe "to a file" do
+ before do
+ FileUtils.touch bundled_app("bundle")
+ end
+
+ it "reports the file exists" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle_config "path bundle"
+ bundle :install, raise_on_error: false
+ expect(err).to include("file already exists")
+ end
+ end
+end
diff --git a/spec/bundler/install/prereleases_spec.rb b/spec/bundler/install/prereleases_spec.rb
new file mode 100644
index 0000000000..9f764d127c
--- /dev/null
+++ b/spec/bundler/install/prereleases_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle install" do
+ before do
+ build_repo2 do
+ build_gem "not_released", "1.0.pre"
+
+ build_gem "has_prerelease", "1.0"
+ build_gem "has_prerelease", "1.1.pre"
+ end
+ end
+
+ describe "when prerelease gems are available" do
+ it "finds prereleases" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "not_released"
+ G
+ expect(the_bundle).to include_gems "not_released 1.0.pre"
+ end
+
+ it "uses regular releases if available" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "has_prerelease"
+ G
+ expect(the_bundle).to include_gems "has_prerelease 1.0"
+ end
+
+ it "uses prereleases if requested" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "has_prerelease", "1.1.pre"
+ G
+ expect(the_bundle).to include_gems "has_prerelease 1.1.pre"
+ end
+ end
+
+ describe "when prerelease gems are not available" do
+ it "still works" do
+ build_repo3 do
+ build_gem "myrack"
+ end
+ FileUtils.rm_r Dir[gem_repo3("prerelease*")]
+
+ install_gemfile <<-G
+ source "https://gem.repo3"
+ gem "myrack"
+ G
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+end
diff --git a/spec/bundler/install/process_lock_spec.rb b/spec/bundler/install/process_lock_spec.rb
new file mode 100644
index 0000000000..b096291d1a
--- /dev/null
+++ b/spec/bundler/install/process_lock_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+RSpec.describe "process lock spec" do
+ describe "when an install operation is already holding a process lock" do
+ before { FileUtils.mkdir_p(default_bundle_path) }
+
+ it "will not run a second concurrent bundle install until the lock is released" do
+ thread = Thread.new do
+ Bundler::ProcessLock.lock(default_bundle_path) do
+ sleep 1 # ignore quality_spec
+ expect(the_bundle).not_to include_gems "myrack 1.0"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ thread.join
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ context "when creating a lock raises Errno::ENOTSUP" do
+ before { allow(File).to receive(:open).and_raise(Errno::ENOTSUP) }
+
+ it "skips creating the lockfile and yields" do
+ processed = false
+ Bundler::ProcessLock.lock(default_bundle_path) { processed = true }
+
+ expect(processed).to eq true
+ end
+ end
+
+ context "when creating a lock raises Errno::EPERM" do
+ before { allow(File).to receive(:open).and_raise(Errno::EPERM) }
+
+ it "skips creating the lockfile and yields" do
+ processed = false
+ Bundler::ProcessLock.lock(default_bundle_path) { processed = true }
+
+ expect(processed).to eq true
+ end
+ end
+
+ context "when creating a lock raises Errno::EROFS" do
+ before { allow(File).to receive(:open).and_raise(Errno::EROFS) }
+
+ it "skips creating the lockfile and yields" do
+ processed = false
+ Bundler::ProcessLock.lock(default_bundle_path) { processed = true }
+
+ expect(processed).to eq true
+ end
+ end
+
+ it "refreshes gem specification cache after waiting for lock" do
+ build_repo2 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ # First, install the gem so it's available
+ bundle "install"
+ expect(out).to include("Installing myrack")
+
+ # Queue for thread-safe communication
+ lock_acquired = Queue.new
+ can_release_lock = Queue.new
+ install_output = Queue.new
+
+ # Thread holds lock (simulating another bundle process that just finished installing)
+ thread = Thread.new do
+ Bundler::ProcessLock.lock(default_bundle_path) do
+ # Signal that we have the lock
+ lock_acquired << true
+ # Wait until main thread signals we can release
+ can_release_lock.pop
+ end
+ end
+
+ # Wait for thread to acquire lock
+ lock_acquired.pop
+
+ # Start another install in a thread - it will wait for the lock
+ install_thread = Thread.new do
+ bundle "install", verbose: true
+ install_output << out
+ end
+
+ # Give subprocess time to start and begin waiting for lock
+ sleep 0.5
+
+ # Signal thread to release the lock
+ can_release_lock << true
+
+ # Wait for both threads to complete
+ thread.join
+ install_thread.join
+
+ second_install_out = install_output.pop
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ # The second install should have refreshed its cache after acquiring
+ # the lock and seen that myrack was already installed
+ expect(second_install_out).to include("Using myrack")
+ end
+ end
+end
diff --git a/spec/bundler/install/security_policy_spec.rb b/spec/bundler/install/security_policy_spec.rb
new file mode 100644
index 0000000000..e7f64dc227
--- /dev/null
+++ b/spec/bundler/install/security_policy_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "rubygems/security"
+
+# unfortunately, testing signed gems with a provided CA is extremely difficult
+# as 'gem cert' is currently the only way to add CAs to the system.
+
+RSpec.describe "policies with unsigned gems" do
+ before do
+ build_security_repo
+ gemfile <<-G
+ source "https://gems.security"
+ gem "myrack"
+ gem "signed_gem"
+ G
+ end
+
+ it "will work after you try to deploy without a lock" do
+ bundle "install --deployment", raise_on_error: false
+ bundle :install
+ expect(the_bundle).to include_gems "myrack 1.0", "signed_gem 1.0"
+ end
+
+ it "will fail when given invalid security policy" do
+ bundle "install --trust-policy=InvalidPolicyName", raise_on_error: false
+ expect(err).to include("RubyGems doesn't know about trust policy")
+ end
+
+ it "will fail with High Security setting due to presence of unsigned gem" do
+ bundle "install --trust-policy=HighSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will fail with Medium Security setting due to presence of unsigned gem" do
+ bundle "install --trust-policy=MediumSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will succeed with no policy" do
+ bundle "install"
+ end
+end
+
+RSpec.describe "policies with signed gems and no CA" do
+ before do
+ build_security_repo
+ gemfile <<-G
+ source "https://gems.security"
+ gem "signed_gem"
+ G
+ end
+
+ it "will fail with High Security setting, gem is self-signed" do
+ bundle "install --trust-policy=HighSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will fail with Medium Security setting, gem is self-signed" do
+ bundle "install --trust-policy=MediumSecurity", raise_on_error: false
+ expect(err).to include("security policy didn't allow")
+ end
+
+ it "will succeed with Low Security setting, low security accepts self signed gem" do
+ bundle "install --trust-policy=LowSecurity"
+ expect(the_bundle).to include_gems "signed_gem 1.0"
+ end
+
+ it "will succeed with no policy" do
+ bundle "install"
+ expect(the_bundle).to include_gems "signed_gem 1.0"
+ end
+end
diff --git a/spec/bundler/install/yanked_spec.rb b/spec/bundler/install/yanked_spec.rb
new file mode 100644
index 0000000000..c92af7bfb0
--- /dev/null
+++ b/spec/bundler/install/yanked_spec.rb
@@ -0,0 +1,254 @@
+# frozen_string_literal: true
+
+RSpec.context "when installing a bundle that includes yanked gems" do
+ it "throws an error when the original gem version is yanked" do
+ build_repo4 do
+ build_gem "foo", "9.0.0"
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ foo (10.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 10.0.0)
+
+ L
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "foo", "10.0.0"
+ G
+
+ expect(err).to include("Your bundle is locked to foo (10.0.0)")
+ end
+
+ context "when a platform specific yanked version is included in the lockfile, and a generic variant is available remotely" do
+ let(:original_lockfile) do
+ <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ actiontext (6.1.6)
+ nokogiri (>= 1.8)
+ foo (1.0.0)
+ nokogiri (1.13.8-#{Bundler.local_platform})
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ actiontext (= 6.1.6)
+ foo (= 1.0.0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ before do
+ skip "Materialization on Windows is not yet strict, so the example does not detect the gem has been yanked" if Gem.win_platform?
+
+ build_repo4 do
+ build_gem "foo", "1.0.0"
+ build_gem "foo", "1.0.1"
+ build_gem "actiontext", "6.1.7" do |s|
+ s.add_dependency "nokogiri", ">= 1.8"
+ end
+ build_gem "actiontext", "6.1.6" do |s|
+ s.add_dependency "nokogiri", ">= 1.8"
+ end
+ build_gem "actiontext", "6.1.7" do |s|
+ s.add_dependency "nokogiri", ">= 1.8"
+ end
+ build_gem "nokogiri", "1.13.8"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "foo", "1.0.0"
+ gem "actiontext", "6.1.6"
+ G
+
+ lockfile original_lockfile
+ end
+
+ context "and a re-resolve is necessary" do
+ before do
+ gemfile gemfile.sub('"foo", "1.0.0"', '"foo", "1.0.1"')
+ end
+
+ it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the old index is used" do
+ bundle "install", artifice: "endpoint", verbose: true
+
+ expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1")
+ expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1"))
+ expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...")
+ end
+
+ it "reresolves, and replaces the yanked gem with the generic version, printing a warning, when the compact index API is used" do
+ bundle "install", artifice: "compact_index", verbose: true
+
+ expect(out).to include("Installing nokogiri 1.13.8").and include("Installing foo 1.0.1")
+ expect(lockfile).to eq(original_lockfile.sub("nokogiri (1.13.8-#{Bundler.local_platform})", "nokogiri (1.13.8)").gsub("1.0.0", "1.0.1"))
+ expect(err).to include("Some locked specs have possibly been yanked (nokogiri-1.13.8-#{Bundler.local_platform}). Ignoring them...")
+ end
+ end
+
+ it "reports the yanked gem properly when the old index is used" do
+ bundle "install", artifice: "endpoint", raise_on_error: false
+
+ expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})")
+ end
+
+ it "reports the yanked gem properly when the compact index API is used" do
+ bundle "install", artifice: "compact_index", raise_on_error: false
+
+ expect(err).to include("Your bundle is locked to nokogiri (1.13.8-#{Bundler.local_platform})")
+ end
+ end
+
+ it "throws the original error when only the Gemfile specifies a gem version that doesn't exist" do
+ build_repo4 do
+ build_gem "foo", "9.0.0"
+ end
+
+ bundle_config "force_ruby_platform true"
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "foo", "10.0.0"
+ G
+
+ expect(err).not_to include("Your bundle is locked to foo (10.0.0)")
+ expect(err).to include("Could not find gem 'foo (= 10.0.0)' in")
+ end
+end
+
+RSpec.context "when resolving a bundle that includes yanked gems, but unlocking an unrelated gem" do
+ before(:each) do
+ build_repo4 do
+ build_gem "foo", "10.0.0"
+
+ build_gem "bar", "1.0.0"
+ build_gem "bar", "2.0.0"
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4
+ specs:
+ foo (9.0.0)
+ bar (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo
+ bar
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "foo"
+ gem "bar"
+ G
+ end
+
+ it "does not update the yanked gem" do
+ bundle "lock --update bar"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ bar (2.0.0)
+ foo (9.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ bar
+ foo
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+end
+
+RSpec.context "when using gem before installing" do
+ it "does not suggest the author has yanked the gem" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ L
+
+ bundle :list, raise_on_error: false
+
+ expect(err).to include("Could not find myrack-0.9.1 in locally installed gems")
+ expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from")
+ expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.")
+ expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.")
+
+ # Check error message is still correct when multiple platforms are locked
+ lockfile lockfile.gsub(/PLATFORMS\n #{lockfile_platforms}/m, "PLATFORMS\n #{lockfile_platforms("ruby")}")
+
+ bundle :list, raise_on_error: false
+ expect(err).to include("Could not find myrack-0.9.1 in locally installed gems")
+ end
+
+ it "does not suggest the author has yanked the gem when using more than one gem, but shows all gems that couldn't be found in the source" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ gem "myrack_middleware", "1.0"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 0.9.1)
+ myrack_middleware (1.0)
+ L
+
+ bundle :list, raise_on_error: false
+
+ expect(err).to include("Could not find myrack-0.9.1, myrack_middleware-1.0 in locally installed gems")
+ expect(err).to include("Install missing gems with `bundle install`.")
+ expect(err).to_not include("Your bundle is locked to myrack (0.9.1) from")
+ expect(err).to_not include("If you haven't changed sources, that means the author of myrack (0.9.1) has removed it.")
+ expect(err).to_not include("You'll need to update your bundle to a different version of myrack (0.9.1) that hasn't been removed in order to install.")
+ end
+end
diff --git a/spec/bundler/lock/git_spec.rb b/spec/bundler/lock/git_spec.rb
new file mode 100644
index 0000000000..c9f76115dc
--- /dev/null
+++ b/spec/bundler/lock/git_spec.rb
@@ -0,0 +1,258 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle lock with git gems" do
+ let(:install_gemfile_with_foo_as_a_git_dependency) do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+ end
+
+ it "doesn't break right after running lock" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ expect(the_bundle).to include_gems "foo 1.0.0"
+ end
+
+ it "doesn't print errors even if running lock after removing the cache" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ FileUtils.rm_r(Dir[default_cache_path("git/foo-1.0-*")].first)
+
+ bundle "lock --verbose"
+
+ expect(err).to be_empty
+ end
+
+ it "prints a proper error when changing a locked Gemfile to point to a bad branch" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}", :branch => "bad"
+ G
+
+ bundle "lock --update foo", env: { "LANG" => "en" }, raise_on_error: false
+
+ expect(err).to include("Revision bad does not exist in the repository")
+ end
+
+ it "prints a proper error when installing a Gemfile with a locked ref that does not exist" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{"a" * 40}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install", raise_on_error: false
+
+ expect(err).to include("Revision #{"a" * 40} does not exist in the repository")
+ end
+
+ it "locks a git source to the current ref" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ update_git "foo"
+ bundle :install
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" unless defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "properly clones a git source locked to an out of date ref" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ update_git "foo"
+
+ bundle :install, env: { "BUNDLE_PATH" => "foo" }
+ expect(err).to be_empty
+ end
+
+ it "properly fetches a git source locked to an unreachable ref" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ # Create a commit and make it unreachable
+ git "checkout -b foo ", lib_path("foo-1.0")
+ unreachable_sha = update_git("foo").ref_for("HEAD")
+ git "checkout main ", lib_path("foo-1.0")
+ git "branch -D foo ", lib_path("foo-1.0")
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{unreachable_sha}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(err).to be_empty
+ end
+
+ it "properly fetches a git source locked to an annotated tag" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ # Create an annotated tag
+ git("tag -a v1.0 -m 'Annotated v1.0'", lib_path("foo-1.0"))
+ annotated_tag = git("rev-parse v1.0", lib_path("foo-1.0"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{lib_path("foo-1.0")}"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{annotated_tag}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(err).to be_empty
+ end
+
+ it "provides correct #full_gem_path" do
+ install_gemfile_with_foo_as_a_git_dependency
+
+ run <<-RUBY
+ puts Bundler.rubygems.find_name('foo').first.full_gem_path
+ RUBY
+ expect(out).to eq(bundle("info foo --path"))
+ end
+
+ it "does not lock versions that don't exist in the repository when changing a GEM transitive dep to a GIT direct dep" do
+ build_repo4 do
+ build_gem "activesupport", "8.0.0" do |s|
+ s.add_dependency "securerandom"
+ end
+
+ build_gem "securerandom", "0.3.1"
+ end
+
+ path = lib_path("securerandom")
+
+ build_git "securerandom", "0.3.2", path: path
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ activesupport (8.0.0)
+ securerandom
+ securerandom (0.3.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+
+ gem "activesupport"
+ gem "securerandom", git: "#{path}"
+ G
+
+ bundle "lock"
+
+ expect(lockfile).to include("securerandom (0.3.2)")
+ end
+
+ it "does not lock versions that don't exist in the repository when changing a GIT direct dep to a GEM direct dep" do
+ build_repo4 do
+ build_gem "ruby-lsp", "0.16.1"
+ end
+
+ path = lib_path("ruby-lsp")
+ revision = build_git("ruby-lsp", "0.16.2", path: path).ref_for("HEAD")
+
+ lockfile <<~L
+ GIT
+ remote: #{path}
+ revision: #{revision}
+ specs:
+ ruby-lsp (0.16.2)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ruby-lsp!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "ruby-lsp"
+ G
+
+ bundle "lock"
+
+ expect(lockfile).to include("ruby-lsp (0.16.1)")
+ end
+end
diff --git a/spec/bundler/lock/lockfile_spec.rb b/spec/bundler/lock/lockfile_spec.rb
new file mode 100644
index 0000000000..654ac02aa7
--- /dev/null
+++ b/spec/bundler/lock/lockfile_spec.rb
@@ -0,0 +1,2416 @@
+# frozen_string_literal: true
+
+RSpec.describe "the lockfile format" do
+ before do
+ build_repo2
+ end
+
+ it "generates a simple lockfile for a single source, gem" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "updates the lockfile's bundler version if current ver. is newer, and version was forced through BUNDLER_VERSION" do
+ system_gems "bundler-1.8.2"
+
+ lockfile <<-L
+ GIT
+ remote: git://github.com/nex3/haml.git
+ revision: 8a2271f
+ specs:
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ omg!
+ myrack
+
+ BUNDLED WITH
+ 1.8.2
+ L
+
+ install_gemfile <<-G, verbose: true, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ source "https://gem.repo2"
+
+ gem "myrack"
+ G
+
+ expect(out).not_to include("Bundler #{Bundler::VERSION} is running, but your lockfile was generated with 1.8.2.")
+ expect(out).to include("Using bundler #{Bundler::VERSION}")
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not update the lockfile's bundler version if nothing changed during bundle install, but uses the locked version" do
+ version = "2.3.0"
+
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+
+ build_bundler version
+ end
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{version}
+ L
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo4"
+
+ gem "myrack"
+ G
+
+ expect(out).to include("Bundler #{Bundler::VERSION} is running, but your lockfile was generated with #{version}.")
+ expect(out).to include("Using bundler #{version}")
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{version}
+ G
+ end
+
+ it "adds the BUNDLED WITH section if not present" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ gem "myrack", "> 0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (> 0)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "update the bundler major version just fine" do
+ current_version = Bundler::VERSION
+ older_major = previous_major(current_version)
+
+ system_gems "bundler-#{older_major}"
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{older_major}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ source "https://gem.repo2/"
+
+ gem "myrack"
+ G
+
+ expect(err).to be_empty
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{current_version}
+ G
+ end
+
+ it "generates a simple lockfile for a single source, gem with dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a simple lockfile for a single source, gem with a version requirement" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama", ">= 1.0"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a lockfile without credentials" do
+ bundle "config set https://localgemserver.test/ user:pass"
+
+ install_gemfile(<<-G, artifice: "endpoint_strict_basic_authentication", quiet: true)
+ source "https://gem.repo1"
+
+ source "https://localgemserver.test/" do
+
+ end
+
+ source "https://user:pass@othergemserver.test/" do
+ gem "myrack-obama", ">= 1.0"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ GEM
+ remote: https://localgemserver.test/
+ specs:
+
+ GEM
+ remote: https://othergemserver.test/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add credentials to lockfile when it does not have them already" do
+ bundle "config set http://localgemserver.test/ user:pass"
+
+ gemfile <<~G
+ source "https://gem.repo1"
+
+ source "http://localgemserver.test/" do
+
+ end
+
+ source "http://user:pass@othergemserver.test/" do
+ gem "myrack-obama", ">= 1.0"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ lockfile_without_credentials = <<~L
+ GEM
+ remote: http://localgemserver.test/
+ specs:
+
+ GEM
+ remote: http://othergemserver.test/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_without_credentials
+
+ # when not re-resolving
+ bundle "install", artifice: "endpoint_strict_basic_authentication", quiet: true
+ expect(lockfile).to eq lockfile_without_credentials
+
+ # when re-resolving with full unlock
+ bundle "update", artifice: "endpoint_strict_basic_authentication"
+ expect(lockfile).to eq lockfile_without_credentials
+
+ # when re-resolving without ful unlocking
+ bundle "update myrack-obama", artifice: "endpoint_strict_basic_authentication"
+ expect(lockfile).to eq lockfile_without_credentials
+ end
+
+ it "keeps credentials in lockfile if already there" do
+ bundle "config set http://localgemserver.test/ user:pass"
+
+ gemfile <<~G
+ source "https://gem.repo1"
+
+ source "http://localgemserver.test/" do
+
+ end
+
+ source "http://user:pass@othergemserver.test/" do
+ gem "myrack-obama", ">= 1.0"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ lockfile_with_credentials = <<~L
+ GEM
+ remote: http://localgemserver.test/
+ specs:
+
+ GEM
+ remote: http://user:pass@othergemserver.test/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lockfile lockfile_with_credentials
+
+ bundle "install", artifice: "endpoint_strict_basic_authentication", quiet: true
+
+ expect(lockfile).to eq lockfile_with_credentials
+ end
+
+ it "generates lockfiles with multiple requirements" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "net-sftp"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "net-sftp", "1.1.1"
+ c.checksum gem_repo2, "net-ssh", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ net-sftp (1.1.1)
+ net-ssh (>= 1.0.0, < 1.99.0)
+ net-ssh (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ net-sftp
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ expect(the_bundle).to include_gems "net-sftp 1.1.1", "net-ssh 1.0.0"
+ end
+
+ it "generates a simple lockfile for a single pinned source, gem with a version requirement" do
+ git = build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not asplode when a platform specific dependency is present and the Gemfile has not been resolved on that platform" do
+ build_lib "omg", path: lib_path("omg")
+
+ gemfile <<-G
+ source "https://gem.repo2/"
+
+ platforms :#{not_local_tag} do
+ gem "omg", :path => "#{lib_path("omg")}"
+ end
+
+ gem "myrack"
+ G
+
+ lockfile <<-L
+ GIT
+ remote: git://github.com/nex3/haml.git
+ revision: 8a2271f
+ specs:
+
+ GEM
+ remote: https://gem.repo2//
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{not_local}
+
+ DEPENDENCIES
+ omg!
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "serializes global git sources" do
+ git = build_git "foo"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}" do
+ gem "foo"
+ end
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a lockfile with a ref for a single pinned source, git gem with a branch requirement" do
+ git = build_git "foo"
+ update_git "foo", branch: "omg"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :branch => "omg"
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("omg")}
+ branch: omg
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "generates a lockfile with a ref for a single pinned source, git gem with a tag requirement" do
+ git = build_git "foo"
+ update_git "foo", tag: "omg"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}", :tag => "omg"
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("omg")}
+ tag: omg
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "is conservative with dependencies of git gems" do
+ build_repo4 do
+ build_gem "orm_adapter", "0.4.1"
+ build_gem "orm_adapter", "0.5.0"
+ end
+
+ FileUtils.mkdir_p lib_path("ckeditor/lib")
+
+ @remote = build_git("ckeditor_remote", bare: true)
+
+ build_git "ckeditor", path: lib_path("ckeditor") do |s|
+ s.write "lib/ckeditor.rb", "CKEDITOR = '4.0.7'"
+ s.version = "4.0.7"
+ s.add_dependency "orm_adapter"
+ end
+
+ update_git "ckeditor", path: lib_path("ckeditor"), remote: @remote.path
+ update_git "ckeditor", path: lib_path("ckeditor"), tag: "v4.0.7"
+ old_git = update_git "ckeditor", path: lib_path("ckeditor"), push: "v4.0.7"
+
+ update_git "ckeditor", path: lib_path("ckeditor"), gemspec: true do |s|
+ s.write "lib/ckeditor.rb", "CKEDITOR = '4.0.8'"
+ s.version = "4.0.8"
+ s.add_dependency "orm_adapter"
+ end
+ update_git "ckeditor", path: lib_path("ckeditor"), tag: "v4.0.8"
+
+ new_git = update_git "ckeditor", path: lib_path("ckeditor"), push: "v4.0.8"
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "ckeditor", :git => "#{@remote.path}", :tag => "v4.0.8"
+ G
+
+ lockfile <<~L
+ GIT
+ remote: #{@remote.path}
+ revision: #{old_git.ref_for("v4.0.7")}
+ tag: v4.0.7
+ specs:
+ ckeditor (4.0.7)
+ orm_adapter
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ orm_adapter (0.4.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ckeditor!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "lock"
+
+ # Bumps the git gem, but keeps its dependency locked
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: #{@remote.path}
+ revision: #{new_git.ref_for("v4.0.8")}
+ tag: v4.0.8
+ specs:
+ ckeditor (4.0.8)
+ orm_adapter
+
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ orm_adapter (0.4.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ckeditor!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "serializes pinned path sources to the lockfile" do
+ build_lib "foo"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo-1.0")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "serializes pinned path sources to the lockfile even when packaging" do
+ build_lib "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ bundle :cache
+ bundle :install, local: true
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: #{lib_path("foo-1.0")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "sorts serialized sources by type" do
+ build_lib "foo"
+ bar = build_git "bar"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ c.no_checksum "bar", "1.0"
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack"
+ gem "foo", :path => "#{lib_path("foo-1.0")}"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{lib_path("bar-1.0")}
+ revision: #{bar.ref_for("main")}
+ specs:
+ bar (1.0)
+
+ PATH
+ remote: #{lib_path("foo-1.0")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ bar!
+ foo!
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "removes redundant sources" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack", :source => "https://gem.repo2/"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "lists gems alphabetically" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "thin"
+ gem "actionpack"
+ gem "myrack-obama"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "actionpack", "2.3.2"
+ c.checksum gem_repo2, "activesupport", "2.3.2"
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ c.checksum gem_repo2, "thin", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+ thin (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ actionpack
+ myrack-obama
+ thin
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "orders dependencies' dependencies in alphabetical order" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "rails"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "actionmailer", "2.3.2"
+ c.checksum gem_repo2, "actionpack", "2.3.2"
+ c.checksum gem_repo2, "activerecord", "2.3.2"
+ c.checksum gem_repo2, "activeresource", "2.3.2"
+ c.checksum gem_repo2, "activesupport", "2.3.2"
+ c.checksum gem_repo2, "rails", "2.3.2"
+ c.checksum gem_repo2, "rake", rake_version
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ actionmailer (2.3.2)
+ activesupport (= 2.3.2)
+ actionpack (2.3.2)
+ activesupport (= 2.3.2)
+ activerecord (2.3.2)
+ activesupport (= 2.3.2)
+ activeresource (2.3.2)
+ activesupport (= 2.3.2)
+ activesupport (2.3.2)
+ rails (2.3.2)
+ actionmailer (= 2.3.2)
+ actionpack (= 2.3.2)
+ activerecord (= 2.3.2)
+ activeresource (= 2.3.2)
+ rake (= #{rake_version})
+ rake (#{rake_version})
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ rails
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "orders dependencies by version" do
+ update_repo2 do
+ # Capistrano did this (at least until version 2.5.10)
+ # RubyGems 2.2 doesn't allow the specifying of a dependency twice
+ # See https://github.com/ruby/rubygems/commit/03dbac93a3396a80db258d9bc63500333c25bd2f
+ build_gem "double_deps", "1.0", skip_validation: true do |s|
+ s.add_dependency "net-ssh", ">= 1.0.0"
+ s.add_dependency "net-ssh"
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem 'double_deps'
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "double_deps", "1.0"
+ c.checksum gem_repo2, "net-ssh", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ double_deps (1.0)
+ net-ssh
+ net-ssh (>= 1.0.0)
+ net-ssh (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ double_deps
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add the :require option to the lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama", ">= 1.0", :require => "myrack/obama"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add the :group option to the lockfile" do
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack-obama", ">= 1.0", :group => :test
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ c.checksum gem_repo2, "myrack-obama", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+ myrack-obama (1.0)
+ myrack
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack-obama (>= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided in a relative fashion and in Gemfile dir" do
+ build_lib "foo", path: bundled_app("foo")
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "foo" do
+ gem "foo"
+ end
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided in a relative fashion and is above Gemfile dir" do
+ build_lib "foo", path: bundled_app(File.join("..", "foo"))
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path "../foo" do
+ gem "foo"
+ end
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: ../foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided in an absolute fashion but is relative" do
+ build_lib "foo", path: bundled_app("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ path File.expand_path("foo", __dir__) do
+ gem "foo"
+ end
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "stores relative paths when the path is provided for gemspec" do
+ build_lib("foo", path: tmp("foo"))
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "1.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec :path => "../foo"
+ G
+
+ expect(lockfile).to eq <<~G
+ PATH
+ remote: ../foo
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "keeps existing platforms in the lockfile" do
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "myrack", "1.0.0"
+ end
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+
+ gem "myrack"
+ G
+
+ checksums.checksum(gem_repo2, "myrack", "1.0.0")
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms("java", local_platform, defaults: [])}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "adds compatible platform specific variants to the lockfile, even if resolution fallback to ruby due to some other incompatible platform specific variant" do
+ simulate_platform "arm64-darwin-23" do
+ build_repo4 do
+ build_gem "google-protobuf", "3.25.1"
+ build_gem "google-protobuf", "3.25.1" do |s|
+ s.platform = "arm64-darwin-23"
+ end
+ build_gem "google-protobuf", "3.25.1" do |s|
+ s.platform = "x64-mingw-ucrt"
+ s.required_ruby_version = "> #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "google-protobuf"
+ G
+ bundle "lock --add-platform x64-mingw-ucrt"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo4, "google-protobuf", "3.25.1"
+ c.checksum gem_repo4, "google-protobuf", "3.25.1", "arm64-darwin-23"
+ end
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ google-protobuf (3.25.1)
+ google-protobuf (3.25.1-arm64-darwin-23)
+
+ PLATFORMS
+ arm64-darwin-23
+ ruby
+ x64-mingw-ucrt
+
+ DEPENDENCIES
+ google-protobuf
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ it "persists the spec's specific platform to the lockfile" do
+ build_repo2 do
+ build_gem "platform_specific", "1.0" do |s|
+ s.platform = Gem::Platform.new("universal-java-16")
+ end
+ end
+
+ simulate_platform "universal-java-16" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "platform_specific"
+ G
+
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum gem_repo2, "platform_specific", "1.0", "universal-java-16"
+ end
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0-universal-java-16)
+
+ PLATFORMS
+ universal-java-16
+
+ DEPENDENCIES
+ platform_specific
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+
+ it "does not add duplicate gems" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "activesupport", "2.3.5")
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack"
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack"
+ gem "activesupport"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ activesupport (2.3.5)
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ activesupport
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add duplicate dependencies" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack"
+ gem "myrack"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add duplicate dependencies with versions" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack", "1.0"
+ gem "myrack", "1.0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "does not add duplicate dependencies in different groups" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "1.0.0")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack", "1.0", :group => :one
+ gem "myrack", "1.0", :group => :two
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (= 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "raises if two different versions are used" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2/"
+ gem "myrack", "1.0"
+ gem "myrack", "1.1"
+ G
+
+ expect(bundled_app_lock).not_to exist
+ expect(err).to include "myrack (= 1.0) and myrack (= 1.1)"
+ end
+
+ it "raises if two different sources are used" do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2/"
+ gem "myrack"
+ gem "myrack", :git => "git://hubz.com"
+ G
+
+ expect(bundled_app_lock).not_to exist
+ expect(err).to include "myrack (>= 0) should come from an unspecified source and git://hubz.com"
+ end
+
+ it "works correctly with multiple version dependencies" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "0.9.1")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ gem "myrack", "> 0.9", "< 1.0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (> 0.9, < 1.0)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "captures the Ruby version in the lockfile" do
+ checksums = checksums_section_when_enabled do |c|
+ c.checksum(gem_repo2, "myrack", "0.9.1")
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2/"
+ ruby '#{Gem.ruby_version}'
+ gem "myrack", "> 0.9", "< 1.0"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack (> 0.9, < 1.0)
+ #{checksums}
+ RUBY VERSION
+ #{Bundler::RubyVersion.system}
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "automatically fixes the lockfile when it's missing deps" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a clear error when frozen mode is set and lockfile is missing deps, and does not install any gems" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(err).to include("Bundler found incorrect dependencies in the lockfile for myrack_middleware-1.0")
+ expect(err).to include("myrack: gemspec specifies = 0.9.1, not in lockfile")
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ end
+
+ it "raises a clear error when frozen mode is set and lockfile is missing entries in CHECKSUMS section, and does not install any gems" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ CHECKSUMS
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack_middleware"
+ G
+
+ expect(err).to eq <<~L.strip
+ Your lockfile is missing a CHECKSUMS entry for \"myrack_middleware\", but can't be updated because frozen mode is set
+
+ Run `bundle install` elsewhere and add the updated Gemfile.lock to version control.
+ L
+
+ expect(the_bundle).not_to include_gems "myrack_middleware 1.0"
+ end
+
+ it "raises a clear error when frozen mode is set and lockfile has empty checksums in CHECKSUMS section, and does not install any gems" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ CHECKSUMS
+ myrack (0.9.1)
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, env: { "BUNDLE_FROZEN" => "true" }, raise_on_error: false
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+
+ expect(err).to eq <<~L.strip
+ Your lockfile has an empty CHECKSUMS entry for \"myrack\", but can't be updated because frozen mode is set
+
+ Run `bundle install` elsewhere and add the updated Gemfile.lock to version control.
+ L
+
+ expect(the_bundle).not_to include_gems "myrack 0.9.1"
+ end
+
+ it "automatically fixes the lockfile when it's missing deps, they conflict with other locked deps, but conflicts are fixable" do
+ build_repo4 do
+ build_gem "other_dep", "0.9"
+ build_gem "other_dep", "1.0"
+
+ build_gem "myrack", "0.9.1"
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "= 0.9.1"
+ s.add_dependency "other_dep", "= 0.9"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack_middleware (1.0)
+ other_dep (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ other_dep
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack_middleware"
+ gem "other_dep"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+ other_dep (= 0.9)
+ other_dep (0.9)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ other_dep
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it's missing multiple deps, they conflict with other locked deps, but conflicts are fixable" do
+ build_repo4 do
+ build_gem "other_dep", "0.9"
+ build_gem "other_dep", "1.0"
+
+ build_gem "myrack", "0.9.1"
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "= 0.9.1"
+ end
+
+ build_gem "another_dep_middleware", "1.0" do |s|
+ s.add_dependency "other_dep", "= 0.9"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack_middleware (1.0)
+ another_dep_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ another_dep_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "myrack_middleware"
+ gem "another_dep_middleware"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ another_dep_middleware (1.0)
+ other_dep (= 0.9)
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+ other_dep (0.9)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ another_dep_middleware
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "raises a resolution error when lockfile is missing deps, they conflict with other locked deps, and conflicts are not fixable" do
+ build_repo4 do
+ build_gem "other_dep", "0.9"
+ build_gem "other_dep", "1.0"
+
+ build_gem "myrack", "0.9.1"
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "= 0.9.1"
+ s.add_dependency "other_dep", "= 0.9"
+ end
+ end
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack_middleware (1.0)
+ other_dep (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+ other_dep
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo4"
+ gem "myrack_middleware"
+ gem "other_dep", "1.0"
+ G
+
+ expect(err).to eq <<~ERROR.strip
+ Could not find compatible versions
+
+ Because every version of myrack_middleware depends on other_dep = 0.9
+ and Gemfile depends on myrack_middleware >= 0,
+ other_dep = 0.9 is required.
+ So, because Gemfile depends on other_dep = 1.0,
+ version solving has failed.
+ ERROR
+ end
+
+ it "regenerates a lockfile with no specs" do
+ build_repo4 do
+ build_gem "indirect_dependency", "1.2.3" do |s|
+ s.metadata["funding_uri"] = "https://example.com/donate"
+ end
+
+ build_gem "direct_dependency", "4.5.6" do |s|
+ s.add_dependency "indirect_dependency", ">= 0"
+ end
+ end
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ direct_dependency
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "direct_dependency"
+ G
+
+ expect(lockfile).to eq <<~G
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ direct_dependency (4.5.6)
+ indirect_dependency
+ indirect_dependency (1.2.3)
+
+ PLATFORMS
+ #{lockfile_platforms(generic_default_locked_platform || local_platform, defaults: ["ruby"])}
+
+ DEPENDENCIES
+ direct_dependency
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "automatically fixes the lockfile when it's missing deps and the full index is in use" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack_middleware (1.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G
+ source "#{file_uri_for(gem_repo2)}"
+ gem "myrack_middleware"
+ G
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: #{file_uri_for(gem_repo2)}/
+ specs:
+ myrack (0.9.1)
+ myrack_middleware (1.0)
+ myrack (= 0.9.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack_middleware
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it includes a gem under the correct GIT section, but also under an incorrect GEM section, with a higher version, and with no explicit Gemfile requirement" do
+ git = build_git "foo"
+
+ gemfile <<~G
+ source "https://gem.repo1/"
+ gem "foo", git: "#{lib_path("foo-1.0")}"
+ G
+
+ # If the lockfile erroneously lists platform versions of the gem
+ # that don't match the locked version of the git repo we should remove them.
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ foo (1.1-x86_64-linux-gnu)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it includes a gem under the correct GIT section, but also under an incorrect GEM section, with a higher version" do
+ git = build_git "foo"
+
+ gemfile <<~G
+ source "https://gem.repo1/"
+ gem "foo", "= 1.0", git: "#{lib_path("foo-1.0")}"
+ G
+
+ # If the lockfile erroneously lists platform versions of the gem
+ # that don't match the locked version of the git repo we should remove them.
+
+ lockfile <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ foo (1.1-x86_64-linux-gnu)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 1.0)!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: #{lib_path("foo-1.0")}
+ revision: #{git.ref_for("main")}
+ specs:
+ foo (1.0)
+
+ GEM
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo (= 1.0)!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "automatically fixes the lockfile when it has incorrect deps, keeping the locked version" do
+ build_repo4 do
+ build_gem "net-smtp", "0.5.0" do |s|
+ s.add_dependency "net-protocol"
+ end
+
+ build_gem "net-smtp", "0.5.1" do |s|
+ s.add_dependency "net-protocol"
+ end
+
+ build_gem "net-protocol", "0.2.2"
+ end
+
+ gemfile <<~G
+ source "#{file_uri_for(gem_repo4)}"
+ gem "net-smtp"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ net-protocol (0.2.2)
+ net-smtp (0.5.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ net-smtp
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ net-protocol (0.2.2)
+ net-smtp (0.5.0)
+ net-protocol
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ net-smtp
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "successfully updates the lockfile when a new gem is added in the Gemfile includes a gem that shouldn't be included" do
+ build_repo4 do
+ build_gem "logger", "1.7.0"
+ build_gem "rack", "3.2.0"
+ build_gem "net-smtp", "0.5.0"
+ end
+
+ gemfile <<~G
+ source "#{file_uri_for(gem_repo4)}"
+ gem "logger"
+ gem "net-smtp"
+
+ install_if -> { false } do
+ gem 'rack', github: 'rack/rack'
+ end
+ G
+
+ lockfile <<~L
+ GIT
+ remote: https://github.com/rack/rack.git
+ revision: 2fface9ac09fc582a81386becd939c987ad33f99
+ specs:
+ rack (3.2.0)
+
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ logger (1.7.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ logger
+ rack!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(lockfile).to eq <<~L
+ GIT
+ remote: https://github.com/rack/rack.git
+ revision: 2fface9ac09fc582a81386becd939c987ad33f99
+ specs:
+ rack (3.2.0)
+
+ GEM
+ remote: #{file_uri_for(gem_repo4)}/
+ specs:
+ logger (1.7.0)
+ net-smtp (0.5.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ logger
+ net-smtp
+ rack!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ shared_examples_for "a lockfile missing dependent specs" do
+ it "auto-heals" do
+ build_repo4 do
+ build_gem "minitest-bisect", "1.6.0" do |s|
+ s.add_dependency "path_expander", "~> 1.1"
+ end
+
+ build_gem "path_expander", "1.1.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "minitest-bisect"
+ G
+
+ # Corrupt lockfile (completely missing path_expander)
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+
+ PLATFORMS
+ #{platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ cache_gems "minitest-bisect-1.6.0", "path_expander-1.1.1", gem_repo: gem_repo4
+ bundle :install
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+ path_expander (~> 1.1)
+ path_expander (1.1.1)
+
+ PLATFORMS
+ #{platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+ end
+
+ context "with just specific platform" do
+ let(:platforms) { lockfile_platforms }
+
+ it_behaves_like "a lockfile missing dependent specs"
+ end
+
+ context "with both ruby and specific platform" do
+ let(:platforms) { lockfile_platforms("ruby") }
+
+ it_behaves_like "a lockfile missing dependent specs"
+ end
+
+ it "auto-heals when the lockfile is missing specs" do
+ build_repo4 do
+ build_gem "minitest-bisect", "1.6.0" do |s|
+ s.add_dependency "path_expander", "~> 1.1"
+ end
+
+ build_gem "path_expander", "1.1.1"
+ end
+
+ gemfile <<~G
+ source "https://gem.repo4"
+ gem "minitest-bisect"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+ path_expander (~> 1.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install --verbose"
+ expect(out).to include("re-resolving dependencies because your lockfile includes \"minitest-bisect\" but not some of its dependencies")
+
+ expect(lockfile).to eq <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ minitest-bisect (1.6.0)
+ path_expander (~> 1.1)
+ path_expander (1.1.1)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ minitest-bisect
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ describe "a line ending" do
+ def set_lockfile_mtime_to_known_value
+ time = Time.local(2000, 1, 1, 0, 0, 0)
+ File.utime(time, time, bundled_app_lock)
+ end
+ before(:each) do
+ build_repo2
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "myrack"
+ G
+ set_lockfile_mtime_to_known_value
+ end
+
+ it "generates Gemfile.lock with \\n line endings" do
+ expect(File.read(bundled_app_lock)).not_to match("\r\n")
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+
+ context "during updates" do
+ it "preserves Gemfile.lock \\n line endings" do
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ expect { bundle "update", all: true }.to change { File.mtime(bundled_app_lock) }
+ expect(File.read(bundled_app_lock)).not_to match("\r\n")
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+
+ it "preserves Gemfile.lock \\n\\r line endings" do
+ skip "needs to be adapted" if Gem.win_platform?
+
+ update_repo2 do
+ build_gem "myrack", "1.2" do |s|
+ s.executables = "myrackup"
+ end
+ end
+
+ win_lock = File.read(bundled_app_lock).gsub(/\n/, "\r\n")
+ File.open(bundled_app_lock, "wb") {|f| f.puts(win_lock) }
+ set_lockfile_mtime_to_known_value
+
+ expect { bundle "update", all: true }.to change { File.mtime(bundled_app_lock) }
+ expect(File.read(bundled_app_lock)).to match("\r\n")
+
+ expect(the_bundle).to include_gems "myrack 1.2"
+ end
+ end
+
+ context "when nothing changes" do
+ it "preserves Gemfile.lock \\n line endings" do
+ expect do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ RUBY
+ end.not_to change { File.mtime(bundled_app_lock) }
+ end
+
+ it "preserves Gemfile.lock \\n\\r line endings" do
+ win_lock = File.read(bundled_app_lock).gsub(/\n/, "\r\n")
+ File.open(bundled_app_lock, "wb") {|f| f.puts(win_lock) }
+ set_lockfile_mtime_to_known_value
+
+ expect do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ RUBY
+ end.not_to change { File.mtime(bundled_app_lock) }
+ end
+ end
+ end
+
+ it "refuses to install if Gemfile.lock contains conflict markers" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2//
+ specs:
+ <<<<<<<
+ myrack (1.0.0)
+ =======
+ myrack (1.0.1)
+ >>>>>>>
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo2/"
+ gem "myrack"
+ G
+
+ expect(err).to match(/your Gemfile.lock contains merge conflicts/i)
+ expect(err).to match(/git checkout HEAD -- Gemfile.lock/i)
+ end
+
+ private
+
+ def previous_major(version)
+ version.split(".").map.with_index {|v, i| i == 0 ? v.to_i - 1 : v }.join(".")
+ end
+end
diff --git a/spec/bundler/other/cli_dispatch_spec.rb b/spec/bundler/other/cli_dispatch_spec.rb
new file mode 100644
index 0000000000..a2c745b070
--- /dev/null
+++ b/spec/bundler/other/cli_dispatch_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle command names" do
+ it "work when given fully" do
+ bundle "install", raise_on_error: false
+ expect(err).to eq("Could not locate Gemfile")
+ expect(stdboth).not_to include("Ambiguous command")
+ end
+
+ it "work when not ambiguous" do
+ bundle "ins", raise_on_error: false
+ expect(err).to eq("Could not locate Gemfile")
+ expect(stdboth).not_to include("Ambiguous command")
+ end
+
+ it "print a friendly error when ambiguous" do
+ bundle "in", raise_on_error: false
+ expect(err).to eq("Ambiguous command in matches [info, init, install]")
+ end
+end
diff --git a/spec/bundler/other/cli_man_pages_spec.rb b/spec/bundler/other/cli_man_pages_spec.rb
new file mode 100644
index 0000000000..4e8f155309
--- /dev/null
+++ b/spec/bundler/other/cli_man_pages_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle commands" do
+ it "expects all commands to have all options and subcommands documented" do
+ check_commands!(Bundler::CLI)
+
+ Bundler::CLI.subcommand_classes.each_value do |klass|
+ check_commands!(klass)
+ end
+ end
+
+ private
+
+ def check_commands!(command_class)
+ command_class.commands.each do |command_name, command|
+ if command.is_a?(Bundler::Thor::HiddenCommand)
+ man_page = man_page(command_name)
+ expect(man_page).not_to exist
+ expect(main_man_page.read).not_to include("bundle #{command_name}")
+ elsif command_class == Bundler::CLI
+ man_page = man_page(command_name)
+ expect(man_page).to exist
+
+ check_options!(command, man_page)
+ else
+ man_page = man_page(command.ancestor_name)
+ expect(man_page).to exist
+
+ check_options!(command, man_page)
+ check_subcommand!(command_name, man_page)
+ end
+ end
+ end
+
+ def check_options!(command, man_page)
+ command.options.each do |_, option|
+ check_option!(option, man_page)
+ end
+ end
+
+ def check_option!(option, man_page)
+ man_page_content = man_page.read
+
+ aliases = option.aliases
+ formatted_aliases = aliases.sort.map {|name| "`#{name}`" }.join(", ") if aliases
+
+ help = if option.type == :boolean
+ "* #{append_aliases("`#{option.switch_name}`", formatted_aliases)}:"
+ elsif option.enum
+ formatted_aliases = "`#{option.switch_name}`" if aliases.empty? && option.lazy_default
+ "* #{prepend_aliases(option.enum.sort.map {|enum| "`#{option.switch_name}=#{enum}`" }.join(", "), formatted_aliases)}:"
+ else
+ names = [option.switch_name, *aliases]
+ value =
+ case option.type
+ when :array then "<list>"
+ when :numeric then "<number>"
+ else option.name.upcase
+ end
+
+ value = option.type != :numeric && option.lazy_default ? "[=#{value}]" : "=#{value}"
+
+ "* #{names.map {|name| "`#{name}#{value}`" }.join(", ")}:"
+ end
+
+ if option.banner.include?("(removed)")
+ expect(man_page_content).not_to include(help)
+ else
+ expect(man_page_content).to include(help)
+ end
+ end
+
+ def check_subcommand!(name, man_page)
+ expect(man_page.read).to match(name)
+ end
+
+ def append_aliases(text, aliases)
+ return text if aliases.empty?
+
+ "#{text}, #{aliases}"
+ end
+
+ def prepend_aliases(text, aliases)
+ return text if aliases.empty?
+
+ "#{aliases}, #{text}"
+ end
+
+ def man_page_content(command_name)
+ man_page(command_name).read
+ end
+
+ def man_page(command_name)
+ source_root.join("lib/bundler/man/bundle-#{command_name}.1.ronn")
+ end
+
+ def main_man_page
+ source_root.join("lib/bundler/man/bundle.1.ronn")
+ end
+end
diff --git a/spec/bundler/other/ext_spec.rb b/spec/bundler/other/ext_spec.rb
new file mode 100644
index 0000000000..a883eefe06
--- /dev/null
+++ b/spec/bundler/other/ext_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.describe "Gem::Specification#installable_on_platform?" do
+ it "does not match platforms other than the gem platform" do
+ darwin = gem "lol", "1.0", "platform_specific-1.0-x86-darwin-10"
+ expect(darwin.installable_on_platform?(pl("java"))).to eq(false)
+ end
+
+ context "when platform is a string" do
+ it "matches when platform is a string" do
+ lazy_spec = Bundler::LazySpecification.new("lol", "1.0", "universal-mingw32")
+ expect(lazy_spec.installable_on_platform?(pl("x86-mingw32"))).to eq(true)
+ expect(lazy_spec.installable_on_platform?(pl("x64-mingw32"))).to eq(true)
+ end
+ end
+end
+
+RSpec.describe "Gem::SourceIndex#refresh!" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "does not explode when called" do
+ run "Gem.source_index.refresh!", raise_on_error: false
+ run "Gem::SourceIndex.new([]).refresh!", raise_on_error: false
+ end
+end
+
+RSpec.describe "Gem::NameTuple" do
+ describe "#initialize" do
+ it "creates a Gem::NameTuple with equality regardless of platform type" do
+ gem_platform = Gem::NameTuple.new "a", v("1"), pl("x86_64-linux")
+ str_platform = Gem::NameTuple.new "a", v("1"), "x86_64-linux"
+ expect(gem_platform).to eq(str_platform)
+ expect(gem_platform.hash).to eq(str_platform.hash)
+ expect(gem_platform.to_a).to eq(str_platform.to_a)
+ end
+ end
+
+ describe "#lock_name" do
+ it "returns the lock name" do
+ expect(Gem::NameTuple.new("a", v("1.0.0"), pl("x86_64-linux")).lock_name).to eq("a (1.0.0-x86_64-linux)")
+ expect(Gem::NameTuple.new("a", v("1.0.0"), "ruby").lock_name).to eq("a (1.0.0)")
+ expect(Gem::NameTuple.new("a", v("1.0.0")).lock_name).to eq("a (1.0.0)")
+ end
+ end
+end
diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb
new file mode 100644
index 0000000000..ab7589d698
--- /dev/null
+++ b/spec/bundler/other/major_deprecation_spec.rb
@@ -0,0 +1,798 @@
+# frozen_string_literal: true
+
+RSpec.describe "major deprecations" do
+ let(:warnings) { err }
+
+ describe "Bundler" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ describe ".clean_env" do
+ before do
+ source = "Bundler.clean_env"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_env and shows a helpful error message about it" do
+ expect(err).to include \
+ "`Bundler.clean_env` has been removed in favor of `Bundler.unbundled_env`. " \
+ "If you instead want the environment before bundler was originally loaded, use `Bundler.original_env`" \
+ end
+ end
+
+ describe ".with_clean_env" do
+ before do
+ source = "Bundler.with_clean_env {}"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_env and shows a helpful error message about it" do
+ expect(err).to include(
+ "`Bundler.with_clean_env` has been removed in favor of `Bundler.with_unbundled_env`. " \
+ "If you instead want the environment before bundler was originally loaded, use `Bundler.with_original_env`"
+ )
+ end
+ end
+
+ describe ".clean_system" do
+ before do
+ source = "Bundler.clean_system('ls')"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_system and shows a helpful error message about it" do
+ expect(err).to include(
+ "`Bundler.clean_system` has been removed in favor of `Bundler.unbundled_system`. " \
+ "If you instead want to run the command in the environment before bundler was originally loaded, use `Bundler.original_system`" \
+ )
+ end
+ end
+
+ describe ".clean_exec" do
+ before do
+ source = "Bundler.clean_exec('ls')"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .unbundled_exec and shows a helpful error message about it" do
+ expect(err).to include(
+ "`Bundler.clean_exec` has been removed in favor of `Bundler.unbundled_exec`. " \
+ "If you instead want to exec to a command in the environment before bundler was originally loaded, use `Bundler.original_exec`" \
+ )
+ end
+ end
+
+ describe ".environment" do
+ before do
+ source = "Bundler.environment"
+ bundle "exec ruby -e #{source.dump}", raise_on_error: false
+ end
+
+ it "is removed in favor of .load and shows a helpful error message about it" do
+ expect(err).to include "Bundler.environment has been removed in favor of Bundler.load"
+ end
+ end
+ end
+
+ describe "bundle exec --no-keep-file-descriptors" do
+ before do
+ bundle "exec --no-keep-file-descriptors -e 1", raise_on_error: false
+ end
+
+ it "is removed and shows a helpful error message about it" do
+ expect(err).to include "The `--no-keep-file-descriptors` has been removed. `bundle exec` no longer mess with your file descriptors. Close them in the exec'd script if you need to"
+ end
+ end
+
+ describe "bundle update --quiet" do
+ it "does not print any deprecations" do
+ bundle :update, quiet: true, raise_on_error: false
+ expect(deprecations).to be_empty
+ end
+ end
+
+ context "bundle check --path" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "check --path vendor/bundle", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set path 'vendor/bundle'`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle check --path=" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "check --path=vendor/bundle", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set path 'vendor/bundle'`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle binstubs --path=" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "binstubs myrack --path=binpath", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set bin 'binpath'`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --all" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --all --verbose", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--all` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set cache_all true`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --no-all" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --no-all", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--no-all` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set cache_all false`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --path" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --path foo", raise_on_error: false
+ end
+
+ it "should print a removal error" do
+ expect(err).to include(
+ "The `--path` flag has been removed because its semantics were unclear. " \
+ "Use `bundle config cache_path` to configure the path of your cache of gems, " \
+ "and `bundle config path` to configure the path where your gems are installed, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --path=" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --path=foo", raise_on_error: false
+ end
+
+ it "should print a deprecation warning" do
+ expect(err).to include(
+ "The `--path` flag has been removed because its semantics were unclear. " \
+ "Use `bundle config cache_path` to configure the path of your cache of gems, " \
+ "and `bundle config path` to configure the path where your gems are installed, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --frozen" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --frozen", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--frozen` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set frozen true`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ context "bundle cache --no-prune" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "cache --no-prune", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `--no-prune` flag has been removed because it relied on being " \
+ "remembered across bundler invocations, which bundler no longer " \
+ "does. Instead please use `bundle config set no_prune true`, " \
+ "and stop using this flag"
+ )
+ end
+ end
+
+ describe "bundle config" do
+ describe "old list interface" do
+ before do
+ bundle "config"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config list` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old get interface" do
+ before do
+ bundle "config waka", raise_on_error: false
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config get waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old set interface" do
+ before do
+ bundle "config waka wakapun"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set waka wakapun` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old set interface with --local" do
+ before do
+ bundle "config --local waka wakapun"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set --local waka wakapun` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old set interface with --global" do
+ before do
+ bundle "config --global waka wakapun"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config set --global waka wakapun` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old unset interface" do
+ before do
+ bundle "config --delete waka"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old unset interface with --local" do
+ before do
+ bundle "config --delete --local waka"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset --local waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+
+ describe "old unset interface with --global" do
+ before do
+ bundle "config --delete --global waka"
+ end
+
+ it "warns", bundler: "4" do
+ expect(deprecations).to include("Using the `config` command without a subcommand [list, get, set, unset] is deprecated and will be removed in the future. Use `bundle config unset --global waka` instead.")
+ end
+
+ pending "fails with a helpful error", bundler: "5"
+ end
+ end
+
+ describe "bundle update" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "warns when no options are given", bundler: "4" do
+ bundle "update"
+ expect(deprecations).to include("Pass --all to `bundle update` to update everything")
+ end
+
+ pending "fails with a helpful error when no options are given", bundler: "5"
+
+ it "does not warn when --all is passed" do
+ bundle "update --all"
+ expect(deprecations).to be_empty
+ end
+ end
+
+ describe "bundle install --binstubs" do
+ before do
+ install_gemfile <<-G, binstubs: true, raise_on_error: false
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include("The --binstubs option has been removed in favor of `bundle binstubs --all`")
+ end
+ end
+
+ context "bundle install with both gems.rb and Gemfile present" do
+ it "should not warn about gems.rb" do
+ gemfile "gems.rb", <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :install
+ expect(deprecations).to be_empty
+ end
+
+ it "should print a proper warning, and use gems.rb" do
+ gemfile "gems.rb", "source 'https://gem.repo1'"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(warnings).to include(
+ "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked."
+ )
+
+ expect(the_bundle).not_to include_gem "myrack 1.0"
+ end
+ end
+
+ context "bundle install with flags" do
+ before do
+ bundle_config "path vendor/bundle"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ {
+ "clean" => ["clean", "true"],
+ "deployment" => ["deployment", "true"],
+ "frozen" => ["frozen", "true"],
+ "no-deployment" => ["deployment", "false"],
+ "no-prune" => ["no_prune", "true"],
+ "path" => ["path", "'vendor/bundle'"],
+ "shebang" => ["shebang", "'ruby27'"],
+ "system" => ["path.system", "true"],
+ "without" => ["without", "'development'"],
+ "with" => ["with", "'development'"],
+ }.each do |name, expectations|
+ option_name, value = *expectations
+ flag_name = "--#{name}"
+ args = %w[true false].include?(value) ? flag_name : "#{flag_name} #{value}"
+
+ context "with the #{flag_name} flag" do
+ before do
+ bundle "install" # to create a lockfile, which deployment or frozen need
+
+ bundle "install #{args}", raise_on_error: false
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "The `#{flag_name}` flag has been removed because it relied on " \
+ "being remembered across bundler invocations, which bundler no " \
+ "longer does. Instead please use `bundle config set " \
+ "#{option_name} #{value}`, and stop using this flag"
+ )
+ end
+ end
+ end
+ end
+
+ context "bundle install with multiple sources" do
+ before do
+ install_gemfile <<-G, raise_on_error: false
+ source "https://gem.repo3"
+ source "https://gem.repo1"
+ G
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include(
+ "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ )
+ end
+
+ it "doesn't show lockfile deprecations if there's a lockfile" do
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo3/
+ remote: https://gem.repo1/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ bundle "install", raise_on_error: false
+
+ expect(err).to include(
+ "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ )
+ expect(err).not_to include(
+ "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. " \
+ "Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure."
+ )
+ bundle_config "frozen true"
+ bundle "install", raise_on_error: false
+
+ expect(err).to include(
+ "This Gemfile contains multiple global sources. " \
+ "Each source after the first must include a block to indicate which gems " \
+ "should come from that source"
+ )
+ expect(err).not_to include(
+ "Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. " \
+ "Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure."
+ )
+ end
+ end
+
+ context "bundle install with a lockfile with a single rubygems section with multiple remotes" do
+ before do
+ build_repo3 do
+ build_gem "myrack", "0.9.1"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ source "https://gem.repo3" do
+ gem 'myrack'
+ end
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo1/
+ remote: https://gem.repo3/
+ specs:
+ myrack (0.9.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ myrack!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "shows an error" do
+ bundle "install", raise_on_error: false
+
+ expect(err).to include("Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure.")
+ end
+ end
+
+ context "bundle install with a lockfile including X64_MINGW_LEGACY platform" do
+ before do
+ gemfile <<~G
+ source "https://gem.repo1"
+ gem "rake"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (10.3.2)
+
+ PLATFORMS
+ ruby
+ x64-mingw32
+
+ DEPENDENCIES
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "warns a helpful error" do
+ bundle "install", raise_on_error: false
+
+ expect(err).to include("Found x64-mingw32 in lockfile, which is deprecated and will be removed in the future.")
+ end
+ end
+
+ context "with a global path source" do
+ before do
+ build_lib "foo"
+
+ install_gemfile <<-G, raise_on_error: false
+ path "#{lib_path("foo-1.0")}"
+ gem 'foo'
+ G
+ end
+
+ it "shows an error" do
+ expect(err).to include("You can no longer specify a path source by itself")
+ end
+ end
+
+ context "when Bundler.setup is run in a ruby script" do
+ before do
+ create_file "gems.rb", "source 'https://gem.repo1'"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :test
+ G
+
+ ruby <<-RUBY
+ require 'bundler'
+
+ Bundler.setup
+ Bundler.setup
+ RUBY
+ end
+
+ it "should print a single deprecation warning" do
+ expect(warnings).to include(
+ "Multiple gemfiles (gems.rb and Gemfile) detected. Make sure you remove Gemfile and Gemfile.lock since bundler is ignoring them in favor of gems.rb and gems.locked."
+ )
+ end
+ end
+
+ context "when `bundler/deployment` is required in a ruby script" do
+ before do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/deployment'
+ RUBY
+ end
+
+ it "should print a capistrano deprecation warning" do
+ expect(err).to include("Bundler no longer integrates " \
+ "with Capistrano, but Capistrano provides " \
+ "its own integration with Bundler via the " \
+ "capistrano-bundler gem. Use it instead.")
+ end
+ end
+
+ context "when `bundler/capistrano` is required in a ruby script" do
+ before do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/capistrano'
+ RUBY
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include("[REMOVED] The Bundler task for Capistrano. Please use https://github.com/capistrano/bundler")
+ end
+ end
+
+ context "when `bundler/vlad` is required in a ruby script" do
+ before do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler/vlad'
+ RUBY
+ end
+
+ it "fails with a helpful error" do
+ expect(err).to include("[REMOVED] The Bundler task for Vlad")
+ end
+ end
+
+ context "bundle show" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "with --outdated flag" do
+ before do
+ bundle "show --outdated", raise_on_error: false
+ end
+
+ it "fails with a helpful message" do
+ expect(err).to include("the `--outdated` flag to `bundle show` has been removed in favor of `bundle show --verbose`")
+ end
+ end
+ end
+
+ context "bundle remove" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "with --install" do
+ it "fails with a helpful message" do
+ bundle "remove myrack --install", raise_on_error: false
+
+ expect(err).to include "The `--install` flag has been removed. `bundle install` is triggered by default."
+ end
+ end
+ end
+
+ context "bundle viz" do
+ before do
+ bundle "viz", raise_on_error: false
+ end
+
+ it "fails with a helpful message" do
+ expect(err).to include "The `viz` command has been renamed to `graph` and moved to a plugin. See https://github.com/rubygems/bundler-graph"
+ end
+ end
+
+ context "bundle inject" do
+ before do
+ bundle "inject", raise_on_error: false
+ end
+
+ it "fails with a helpful message" do
+ expect(err).to include "The `inject` command has been replaced by the `add` command"
+ end
+ end
+
+ context "bundle plugin install --local_git" do
+ before do
+ build_git "foo" do |s|
+ s.write "plugins.rb"
+ end
+ end
+
+ it "fails with a helpful message" do
+ bundle "plugin install foo --local_git #{lib_path("foo-1.0")}", raise_on_error: false
+
+ expect(err).to include "--local_git has been removed, use --git"
+ end
+ end
+
+ describe "removing rubocop" do
+ before do
+ bundle_config_global "gem.mit false"
+ bundle_config_global "gem.test false"
+ bundle_config_global "gem.coc false"
+ bundle_config_global "gem.ci false"
+ bundle_config_global "gem.changelog false"
+ end
+
+ context "bundle gem --rubocop" do
+ before do
+ bundle "gem my_new_gem --rubocop", raise_on_error: false
+ end
+
+ it "prints an error" do
+ expect(err).to include \
+ "--rubocop has been removed, use --linter=rubocop"
+ end
+ end
+
+ context "bundle gem --no-rubocop" do
+ before do
+ bundle "gem my_new_gem --no-rubocop", raise_on_error: false
+ end
+
+ it "prints an error" do
+ expect(err).to include \
+ "--no-rubocop has been removed, use --no-linter"
+ end
+ end
+ end
+
+ context " bundle gem --ext parameter with no value" do
+ it "prints error when used before gem name" do
+ bundle "gem --ext foo", raise_on_error: false
+ expect(err).to include "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension."
+ end
+
+ it "prints error when used after gem name" do
+ bundle "gem foo --ext", raise_on_error: false
+ expect(err).to include "Extensions can now be generated using C or Rust, so `--ext` with no arguments has been removed. Please select a language, e.g. `--ext=rust` to generate a Rust extension."
+ end
+ end
+end
diff --git a/spec/bundler/plugins/command_spec.rb b/spec/bundler/plugins/command_spec.rb
new file mode 100644
index 0000000000..05d535a70c
--- /dev/null
+++ b/spec/bundler/plugins/command_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+RSpec.describe "command plugins" do
+ before do
+ build_repo2 do
+ build_plugin "command-mah" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module Mah
+ class Plugin < Bundler::Plugin::API
+ command "mahcommand" # declares the command
+
+ def exec(command, args)
+ puts "MahHello"
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install command-mah --source https://gem.repo2"
+ end
+
+ it "executes without arguments" do
+ expect(out).to include("Installed plugin command-mah")
+
+ bundle "mahcommand"
+ expect(out).to eq("MahHello")
+ end
+
+ it "accepts the arguments" do
+ update_repo2 do
+ build_plugin "the-echoer" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module Resonance
+ class Echoer
+ # Another method to declare the command
+ Bundler::Plugin::API.command "echo", self
+
+ def exec(command, args)
+ puts "You gave me \#{args.join(", ")}"
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install the-echoer --source https://gem.repo2"
+ expect(out).to include("Installed plugin the-echoer")
+
+ bundle "echo tacos tofu lasange"
+ expect(out).to eq("You gave me tacos, tofu, lasange")
+ end
+
+ it "passes help flag to plugin" do
+ update_repo2 do
+ build_plugin "helpful" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module Helpful
+ class Command
+ Bundler::Plugin::API.command "greet", self
+
+ def exec(command, args)
+ if args.include?("--help") || args.include?("-h")
+ puts "Usage: bundle greet [NAME]"
+ else
+ puts "Hello"
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install helpful --source https://gem.repo2"
+ expect(out).to include("Installed plugin helpful")
+
+ bundle "greet --help"
+ expect(out).to eq("Usage: bundle greet [NAME]")
+
+ bundle "greet -h"
+ expect(out).to eq("Usage: bundle greet [NAME]")
+
+ bundle "help greet"
+ expect(out).to eq("Usage: bundle greet [NAME]")
+ end
+
+ it "raises error on redeclaration of command" do
+ update_repo2 do
+ build_plugin "copycat" do |s|
+ s.write "plugins.rb", <<-RUBY
+ module CopyCat
+ class Cheater < Bundler::Plugin::API
+ command "mahcommand", self
+
+ def exec(command, args)
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install copycat --source https://gem.repo2", raise_on_error: false
+
+ expect(out).not_to include("Installed plugin copycat")
+
+ expect(err).to include("Failed to install plugin `copycat`, due to Bundler::Plugin::Index::CommandConflict (Command(s) `mahcommand` declared by copycat are already registered.)")
+ end
+end
diff --git a/spec/bundler/plugins/hook_spec.rb b/spec/bundler/plugins/hook_spec.rb
new file mode 100644
index 0000000000..ad8a4daeff
--- /dev/null
+++ b/spec/bundler/plugins/hook_spec.rb
@@ -0,0 +1,331 @@
+# frozen_string_literal: true
+
+RSpec.describe "hook plugins" do
+ context "before-install-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-install-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_INSTALL_ALL do |deps|
+ puts "gems to be installed \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-install-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs before all rubygems are installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "gems to be installed rake, myrack"
+ end
+ end
+
+ context "before-install hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-install-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_INSTALL do |spec_install|
+ puts "installing gem \#{spec_install.name}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-install-plugin --source https://gem.repo2"
+ end
+
+ it "runs before each rubygem is installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "installing gem rake"
+ expect(out).to include "installing gem myrack"
+ end
+ end
+
+ context "after-install-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-install-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_INSTALL_ALL do |deps|
+ puts "installed gems \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-install-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs after each all rubygems are installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "installed gems rake, myrack"
+ end
+ end
+
+ context "after-install hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-install-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_INSTALL do |spec_install|
+ puts "installed gem \#{spec_install.name} : \#{spec_install.state}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-install-plugin --source https://gem.repo2"
+ end
+
+ it "runs after each rubygem is installed" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "installed gem rake : installed"
+ expect(out).to include "installed gem myrack : installed"
+ end
+ end
+
+ context "before-require-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-require-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_REQUIRE_ALL do |deps|
+ puts "gems to be required \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-require-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs before all rubygems are required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "gems to be required rake, myrack"
+ end
+ end
+
+ context "before-require hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-require-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_REQUIRE do |dep|
+ puts "requiring gem \#{dep.name}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-require-plugin --source https://gem.repo2"
+ end
+
+ it "runs before each rubygem is required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "requiring gem rake"
+ expect(out).to include "requiring gem myrack"
+ end
+ end
+
+ context "after-require-all hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-require-all-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_REQUIRE_ALL do |deps|
+ puts "required gems \#{deps.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-require-all-plugin --source https://gem.repo2"
+ end
+
+ it "runs after all rubygems are required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "required gems rake, myrack"
+ end
+ end
+
+ context "after-require hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-require-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_REQUIRE do |dep|
+ puts "required gem \#{dep.name}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-require-plugin --source https://gem.repo2"
+ end
+
+ it "runs after each rubygem is required" do
+ install_gemfile_and_bundler_require
+ expect(out).to include "required gem rake"
+ expect(out).to include "required gem myrack"
+ end
+ end
+
+ context "before-eval hook" do
+ before do
+ build_repo2 do
+ build_plugin "before-eval-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_EVAL do |gemfile, lockfile|
+ puts "hooked eval start of \#{File.basename(gemfile)} to \#{File.basename(lockfile)}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install before-eval-plugin --source https://gem.repo2"
+ end
+
+ it "runs before the Gemfile is evaluated" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ G
+
+ expect(out).to include "hooked eval start of Gemfile to Gemfile.lock"
+ end
+ end
+
+ context "after-eval hook" do
+ before do
+ build_repo2 do
+ build_plugin "after-eval-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_EVAL do |defn|
+ puts "hooked eval after with gems \#{defn.dependencies.map(&:name).join(", ")}"
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install after-eval-plugin --source https://gem.repo2"
+ end
+
+ it "runs after the Gemfile is evaluated" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "rake"
+ G
+
+ expect(out).to include "hooked eval after with gems myrack, rake"
+ end
+ end
+
+ context "before-fetch and after-fetch hooks" do
+ before do
+ build_repo2 do
+ build_plugin "fetch-timing-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ @timing_start = nil
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_BEFORE_FETCH do |spec|
+ @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "gem \#{spec.name} started fetch at \#{@timing_start}"
+ end
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GEM_AFTER_FETCH do |spec|
+ timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "gem \#{spec.name} took \#{timing_end - @timing_start} to fetch"
+ @timing_start = nil
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install fetch-timing-plugin --source https://gem.repo2"
+ end
+
+ it "runs around each gem download" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ expect(out).to include "gem rake started fetch at"
+ expect(out).to match(/gem rake took \d+\.\d+ to fetch/)
+ expect(out).to include "gem myrack started fetch at"
+ expect(out).to match(/gem myrack took \d+\.\d+ to fetch/)
+ end
+ end
+
+ context "before-git-fetch and after-git-fetch hooks" do
+ before do
+ build_repo2 do
+ build_plugin "git-fetch-timing-plugin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ @timing_start = nil
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_BEFORE_FETCH do |source|
+ @timing_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "git source \#{source.name} started fetch at \#{@timing_start}"
+ end
+ Bundler::Plugin::API.hook Bundler::Plugin::Events::GIT_AFTER_FETCH do |source|
+ timing_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ puts "git source \#{source.name} took \#{timing_end - @timing_start} to fetch"
+ @timing_start = nil
+ end
+ RUBY
+ end
+ end
+
+ bundle "plugin install git-fetch-timing-plugin --source https://gem.repo2"
+ end
+
+ it "runs around each git source fetch" do
+ build_git "foo", "1.0", path: lib_path("foo")
+
+ relative_path = lib_path("foo").relative_path_from(bundled_app)
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "foo", :git => "#{relative_path}"
+ G
+
+ expect(out).to include "git source foo started fetch at"
+ expect(out).to match(/git source foo took \d+\.\d+ to fetch/)
+ end
+ end
+
+ def install_gemfile_and_bundler_require
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rake"
+ gem "myrack"
+ G
+
+ ruby <<-RUBY
+ require "bundler"
+ Bundler.require
+ RUBY
+ end
+end
diff --git a/spec/bundler/plugins/install_spec.rb b/spec/bundler/plugins/install_spec.rb
new file mode 100644
index 0000000000..dcacf764be
--- /dev/null
+++ b/spec/bundler/plugins/install_spec.rb
@@ -0,0 +1,466 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler plugin install" do
+ before do
+ build_repo2 do
+ build_plugin "foo"
+ build_plugin "kung-foo"
+ end
+ end
+
+ it "shows proper message when gem in not found in the source" do
+ bundle "plugin install no-foo --source https://gem.repo1", raise_on_error: false
+
+ expect(err).to include("Could not find")
+ plugin_should_not_be_installed("no-foo")
+ end
+
+ it "installs from rubygems source" do
+ bundle "plugin install foo --source https://gem.repo2"
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "installs from rubygems source in frozen mode" do
+ bundle "plugin install foo --source https://gem.repo2", env: { "BUNDLE_DEPLOYMENT" => "true" }
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "installs from sources configured as Gem.sources without any flags" do
+ bundle "plugin install foo", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_SOURCES" => "https://gem.repo2" }
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "shows help when --help flag is given" do
+ bundle "plugin install --help"
+
+ # The help message defined in ../../lib/bundler/man/bundle-plugin.1.ronn will be output.
+ expect(out).to include("You can install, uninstall, and list plugin(s)")
+ end
+
+ context "plugin is already installed" do
+ before do
+ bundle "plugin install foo --source https://gem.repo2"
+ end
+
+ it "doesn't install plugin again" do
+ bundle "plugin install foo --source https://gem.repo2"
+ expect(out).not_to include("Installing plugin foo")
+ expect(out).not_to include("Installed plugin foo")
+ end
+ end
+
+ it "installs multiple plugins" do
+ bundle "plugin install foo kung-foo --source https://gem.repo2"
+
+ expect(out).to include("Installed plugin foo")
+ expect(out).to include("Installed plugin kung-foo")
+
+ plugin_should_be_installed("foo", "kung-foo")
+ end
+
+ it "uses the same version for multiple plugins" do
+ update_repo2 do
+ build_plugin "foo", "1.1"
+ build_plugin "kung-foo", "1.1"
+ end
+
+ bundle "plugin install foo kung-foo --version '1.0' --source https://gem.repo2"
+
+ expect(out).to include("Installing foo 1.0")
+ expect(out).to include("Installing kung-foo 1.0")
+ plugin_should_be_installed("foo", "kung-foo")
+ end
+
+ it "installs the latest version if not installed" do
+ update_repo2 do
+ build_plugin "foo", "1.1"
+ end
+
+ bundle "plugin install foo --version 1.0 --source https://gem.repo2 --verbose"
+ expect(out).to include("Installing foo 1.0")
+
+ bundle "plugin install foo --source https://gem.repo2 --verbose"
+ expect(out).to include("Installing foo 1.1")
+
+ bundle "plugin install foo --source https://gem.repo2 --verbose"
+ expect(out).to include("Using foo 1.1")
+ end
+
+ it "raises an error when when --branch specified" do
+ bundle "plugin install foo --branch main --source https://gem.repo2", raise_on_error: false
+
+ expect(out).not_to include("Installed plugin foo")
+
+ expect(err).to include("--branch can only be used with git sources")
+ end
+
+ it "raises an error when --ref specified" do
+ bundle "plugin install foo --ref v1.2.3 --source https://gem.repo2", raise_on_error: false
+
+ expect(err).to include("--ref can only be used with git sources")
+ end
+
+ it "raises error when both --branch and --ref options are specified" do
+ bundle "plugin install foo --source https://gem.repo2 --branch main --ref v1.2.3", raise_on_error: false
+
+ expect(out).not_to include("Installed plugin foo")
+
+ expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.")
+ end
+
+ it "works with different load paths" do
+ build_repo2 do
+ build_plugin "testing" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "fubar"
+ class Test < Bundler::Plugin::API
+ command "check2"
+
+ def exec(command, args)
+ puts "mate"
+ end
+ end
+ RUBY
+ s.require_paths = %w[lib src]
+ s.write("src/fubar.rb")
+ end
+ end
+ bundle "plugin install testing --source https://gem.repo2"
+
+ bundle "check2", "no-color" => false
+ expect(out).to eq("mate")
+ end
+
+ context "malformatted plugin" do
+ it "fails when plugins.rb is missing" do
+ update_repo2 do
+ build_plugin "foo", "1.1"
+ build_plugin "kung-foo", "1.1"
+ end
+
+ bundle "plugin install foo kung-foo --version '1.0' --source https://gem.repo2"
+
+ expect(out).to include("Installing foo 1.0")
+ expect(out).to include("Installing kung-foo 1.0")
+ plugin_should_be_installed("foo", "kung-foo")
+
+ update_repo2 do
+ build_gem "charlie"
+ end
+
+ bundle "plugin install charlie --source https://gem.repo2", raise_on_error: false
+
+ expect(err).to include("Failed to install plugin `charlie`, due to Bundler::Plugin::MalformattedPlugin (plugins.rb was not found in the plugin.)")
+
+ expect(global_plugin_gem("charlie-1.0")).not_to be_directory
+
+ plugin_should_be_installed("foo", "kung-foo")
+ plugin_should_not_be_installed("charlie")
+ end
+
+ it "fails when plugins.rb throws exception on load" do
+ build_repo2 do
+ build_plugin "chaplin" do |s|
+ s.write "plugins.rb", <<-RUBY
+ raise RuntimeError, "threw exception on load"
+ RUBY
+ end
+ end
+
+ bundle "plugin install chaplin --source https://gem.repo2", raise_on_error: false
+
+ expect(global_plugin_gem("chaplin-1.0")).not_to be_directory
+
+ plugin_should_not_be_installed("chaplin")
+ end
+ end
+
+ context "git plugins" do
+ it "installs form a git source" do
+ build_git "foo" do |s|
+ s.write "plugins.rb"
+ end
+
+ bundle "plugin install foo --git #{lib_path("foo-1.0")}"
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+
+ it "installs form a local git source" do
+ build_git "foo" do |s|
+ s.write "plugins.rb"
+ end
+
+ bundle "plugin install foo --git #{lib_path("foo-1.0")}"
+
+ expect(out).to include("Installed plugin foo")
+ plugin_should_be_installed("foo")
+ end
+ end
+
+ context "path plugins" do
+ it "installs from a path source" do
+ build_lib "path_plugin" do |s|
+ s.write "plugins.rb"
+ end
+ bundle "plugin install path_plugin --path #{lib_path("path_plugin-1.0")}"
+
+ expect(out).to include("Installed plugin path_plugin")
+ plugin_should_be_installed("path_plugin")
+ end
+
+ it "installs from a relative path source" do
+ build_lib "path_plugin" do |s|
+ s.write "plugins.rb"
+ end
+ path = lib_path("path_plugin-1.0").relative_path_from(bundled_app)
+ bundle "plugin install path_plugin --path #{path}"
+
+ expect(out).to include("Installed plugin path_plugin")
+ plugin_should_be_installed("path_plugin")
+ end
+
+ it "installs from a relative path source when inside an app" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ gemfile ""
+
+ build_lib "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ path = lib_path("ga-plugin-1.0").relative_path_from(bundled_app)
+ bundle "plugin install ga-plugin --path #{path}"
+
+ plugin_should_be_installed("ga-plugin")
+ expect(local_plugin_gem("foo-1.0")).not_to be_directory
+ end
+ end
+
+ context "Gemfile eval" do
+ before do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ end
+
+ it "installs plugins listed in gemfile" do
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo'
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install"
+
+ expect(out).to include("Installed plugin foo")
+
+ expect(out).to include("Bundle complete!")
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ plugin_should_be_installed("foo")
+ end
+
+ it "overrides the index with the new plugin version" do
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo', "1.0"
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install"
+
+ update_repo2 do
+ build_plugin "foo", "2.0.0"
+ end
+
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo', "2.0"
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install"
+
+ expected = local_plugin_gem("foo-2.0.0", "lib").to_s
+ expect(Bundler::Plugin.index.load_paths("foo")).to eq([expected])
+ end
+
+ it "respects bundler groups" do
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo'
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle "install", env: { "BUNDLE_WITHOUT" => "default" }
+
+ expect(out).to include("Bundle complete! 1 Gemfile dependency, 0 gems now installed.")
+ end
+
+ it "accepts plugin version" do
+ update_repo2 do
+ build_plugin "foo", "1.1.0"
+ end
+
+ gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo', "1.0"
+ G
+
+ bundle "install"
+
+ expect(out).to include("Installing foo 1.0")
+
+ plugin_should_be_installed("foo")
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "accepts git sources" do
+ build_git "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ plugin 'ga-plugin', :git => "#{lib_path("ga-plugin-1.0")}"
+ G
+
+ expect(out).to include("Installed plugin ga-plugin")
+ plugin_should_be_installed("ga-plugin")
+ end
+
+ it "accepts path sources" do
+ build_lib "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ plugin 'ga-plugin', :path => "#{lib_path("ga-plugin-1.0")}"
+ G
+
+ expect(out).to include("Installed plugin ga-plugin")
+ plugin_should_be_installed("ga-plugin")
+ end
+
+ it "accepts relative path sources" do
+ build_lib "ga-plugin" do |s|
+ s.write "plugins.rb"
+ end
+
+ path = lib_path("ga-plugin-1.0").relative_path_from(bundled_app)
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ plugin 'ga-plugin', :path => "#{path}"
+ G
+
+ expect(out).to include("Installed plugin ga-plugin")
+ plugin_should_be_installed("ga-plugin")
+ end
+
+ context "in deployment mode" do
+ it "installs plugins" do
+ install_gemfile <<-G
+ source 'https://gem.repo2'
+ gem 'myrack', "1.0.0"
+ G
+
+ bundle_config "deployment true"
+ install_gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'foo'
+ gem 'myrack', "1.0.0"
+ G
+
+ expect(out).to include("Installed plugin foo")
+
+ expect(out).to include("Bundle complete!")
+
+ expect(the_bundle).to include_gems("myrack 1.0.0")
+ plugin_should_be_installed("foo")
+ end
+ end
+ end
+
+ context "inline gemfiles" do
+ it "installs the listed plugins" do
+ code = <<-RUBY
+ require "bundler/inline"
+
+ gemfile do
+ source 'https://gem.repo2'
+ plugin 'foo'
+ end
+ RUBY
+
+ ruby code, artifice: "compact_index", env: { "BUNDLER_VERSION" => Bundler::VERSION }
+ expect(local_plugin_gem("foo-1.0", "plugins.rb")).to exist
+ end
+ end
+
+ describe "local plugin" do
+ it "is installed when inside an app" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ gemfile ""
+ bundle "plugin install foo --source https://gem.repo2"
+
+ plugin_should_be_installed("foo")
+ expect(local_plugin_gem("foo-1.0")).to be_directory
+ end
+
+ context "conflict with global plugin" do
+ before do
+ update_repo2 do
+ build_plugin "fubar" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Fubar < Bundler::Plugin::API
+ command "shout"
+
+ def exec(command, args)
+ puts "local_one"
+ end
+ end
+ RUBY
+ end
+ end
+
+ # inside the app
+ gemfile "source 'https://gem.repo2'\nplugin 'fubar'"
+ bundle "install"
+
+ update_repo2 do
+ build_plugin "fubar", "1.1" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Fubar < Bundler::Plugin::API
+ command "shout"
+
+ def exec(command, args)
+ puts "global_one"
+ end
+ end
+ RUBY
+ end
+ end
+
+ # outside the app
+ bundle "plugin install fubar --source https://gem.repo2", dir: tmp
+ end
+
+ it "inside the app takes precedence over global plugin" do
+ bundle "shout"
+ expect(out).to eq("local_one")
+ end
+
+ it "outside the app global plugin is used" do
+ bundle "shout", dir: tmp
+ expect(out).to eq("global_one")
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugins/list_spec.rb b/spec/bundler/plugins/list_spec.rb
new file mode 100644
index 0000000000..30e3f82467
--- /dev/null
+++ b/spec/bundler/plugins/list_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler plugin list" do
+ before do
+ build_repo2 do
+ build_plugin "foo" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Foo < Bundler::Plugin::API
+ command "shout"
+
+ def exec(command, args)
+ puts "Foo shout"
+ end
+ end
+ RUBY
+ end
+ build_plugin "bar" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Bar < Bundler::Plugin::API
+ command "scream"
+
+ def exec(command, args)
+ puts "Bar scream"
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context "no plugins installed" do
+ it "shows proper no plugins installed message" do
+ bundle "plugin list"
+
+ expect(out).to include("No plugins installed")
+ end
+ end
+
+ context "single plugin installed" do
+ it "shows plugin name with commands list" do
+ bundle "plugin install foo --source https://gem.repo2"
+ plugin_should_be_installed("foo")
+ bundle "plugin list"
+
+ expected_output = "foo\n-----\n shout"
+ expect(out).to include(expected_output)
+ end
+ end
+
+ context "multiple plugins installed" do
+ it "shows plugin names with commands list" do
+ bundle "plugin install foo bar --source https://gem.repo2"
+ plugin_should_be_installed("foo", "bar")
+ bundle "plugin list"
+
+ expected_output = "foo\n-----\n shout\n\nbar\n-----\n scream"
+ expect(out).to include(expected_output)
+ end
+ end
+end
diff --git a/spec/bundler/plugins/source/example_spec.rb b/spec/bundler/plugins/source/example_spec.rb
new file mode 100644
index 0000000000..4cd4a1a931
--- /dev/null
+++ b/spec/bundler/plugins/source/example_spec.rb
@@ -0,0 +1,456 @@
+# frozen_string_literal: true
+
+RSpec.describe "real source plugins" do
+ context "with a minimal source plugin" do
+ before do
+ build_repo2 do
+ build_plugin "bundler-source-mpath" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "bundler-source-mpath"
+
+ class MPath < Bundler::Plugin::API
+ source "mpath"
+
+ attr_reader :path
+
+ def initialize(opts)
+ super
+
+ @path = Pathname.new options["uri"]
+ end
+
+ def fetch_gemspec_files
+ @spec_files ||= begin
+ glob = "{,*,*/*}.gemspec"
+ if installed?
+ search_path = install_path
+ else
+ search_path = path
+ end
+ Dir["\#{search_path.to_s}/\#{glob}"]
+ end
+ end
+
+ def install(spec, opts)
+ mkdir_p(install_path.parent)
+ require 'fileutils'
+ FileUtils.cp_r(path, install_path)
+
+ spec_path = install_path.join("\#{spec.full_name}.gemspec")
+ spec_path.open("wb") {|f| f.write spec.to_ruby }
+ spec.loaded_from = spec_path.to_s
+
+ post_install(spec)
+
+ nil
+ end
+ end
+ RUBY
+ end # build_plugin
+ end
+
+ build_lib "a-path-gem"
+
+ gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("a-path-gem-1.0")}", :type => :mpath do
+ gem "a-path-gem"
+ end
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+
+ expect(out).to include("Bundle complete!")
+
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+
+ it "writes to lockfile" do
+ bundle "install"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "a-path-gem", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ PLUGIN SOURCE
+ remote: #{lib_path("a-path-gem-1.0")}
+ type: mpath
+ specs:
+ a-path-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ a-path-gem!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "provides correct #full_gem_path" do
+ bundle "install"
+ run <<-RUBY
+ puts Bundler.rubygems.find_name('a-path-gem').first.full_gem_path
+ RUBY
+ expect(out).to eq(bundle("info a-path-gem --path"))
+ end
+
+ it "installs the gem executables" do
+ build_lib "gem_with_bin" do |s|
+ s.executables = ["foo"]
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("gem_with_bin-1.0")}", :type => :mpath do
+ gem "gem_with_bin"
+ end
+ G
+
+ bundle "exec foo"
+ expect(out).to eq("1.0")
+ end
+
+ describe "bundle cache/package" do
+ let(:uri_hash) { Digest(:SHA1).hexdigest(lib_path("a-path-gem-1.0").to_s) }
+ it "copies repository to vendor cache and uses it" do
+ bundle "install"
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}/.bundlecache")).to be_file
+
+ FileUtils.rm_r lib_path("a-path-gem-1.0")
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+
+ it "copies repository to vendor cache and uses it even when installed with `path` configured" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+
+ FileUtils.rm_r lib_path("a-path-gem-1.0")
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+
+ it "bundler package copies repository to vendor cache" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+ bundle :cache
+
+ expect(bundled_app("vendor/cache/a-path-gem-1.0-#{uri_hash}")).to exist
+
+ FileUtils.rm_r lib_path("a-path-gem-1.0")
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+ end
+
+ context "with lockfile" do
+ before do
+ lockfile <<-G
+ PLUGIN SOURCE
+ remote: #{lib_path("a-path-gem-1.0")}
+ type: mpath
+ specs:
+ a-path-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ a-path-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+
+ expect(the_bundle).to include_gems("a-path-gem 1.0")
+ end
+ end
+ end
+
+ context "with a more elaborate source plugin" do
+ before do
+ build_repo2 do
+ build_plugin "bundler-source-gitp" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "open3"
+
+ class SPlugin < Bundler::Plugin::API
+ source "gitp"
+
+ attr_reader :ref
+
+ def initialize(opts)
+ super
+
+ @ref = options["ref"] || options["branch"] || options["tag"] || "main"
+ @unlocked = false
+ end
+
+ def eql?(other)
+ other.is_a?(self.class) && uri == other.uri && ref == other.ref
+ end
+
+ alias_method :==, :eql?
+
+ def fetch_gemspec_files
+ @spec_files ||= begin
+ glob = "{,*,*/*}.gemspec"
+ if !cached?
+ cache_repo
+ end
+
+ if installed? && !@unlocked
+ path = install_path
+ else
+ path = cache_path
+ end
+
+ Dir["\#{path}/\#{glob}"]
+ end
+ end
+
+ def install(spec, opts)
+ mkdir_p(install_path.dirname)
+ rm_rf(install_path)
+ `git clone --no-checkout --quiet "\#{cache_path}" "\#{install_path}"`
+ Open3.capture2e("git reset --hard \#{revision}", :chdir => install_path)
+
+ spec_path = install_path.join("\#{spec.full_name}.gemspec")
+ spec_path.open("wb") {|f| f.write spec.to_ruby }
+ spec.loaded_from = spec_path.to_s
+
+ post_install(spec)
+
+ nil
+ end
+
+ def options_to_lock
+ opts = {"revision" => revision}
+ opts["ref"] = ref if ref != "main"
+ opts
+ end
+
+ def unlock!
+ @unlocked = true
+ @revision = latest_revision
+ end
+
+ def app_cache_dirname
+ "\#{base_name}-\#{shortref_for_path(revision)}"
+ end
+
+ private
+
+ def cache_path
+ @cache_path ||= cache_dir.join("gitp", base_name)
+ end
+
+ def cache_repo
+ `git clone --quiet \#{@options["uri"]} \#{cache_path}`
+ end
+
+ def cached?
+ File.directory?(cache_path)
+ end
+
+ def locked_revision
+ options["revision"]
+ end
+
+ def revision
+ @revision ||= locked_revision || latest_revision
+ end
+
+ def latest_revision
+ if !cached? || @unlocked
+ rm_rf(cache_path)
+ cache_repo
+ end
+
+ output, _status = Open3.capture2e("git rev-parse --verify \#{@ref}", :chdir => cache_path)
+ output.strip
+ end
+
+ def base_name
+ File.basename(uri.sub(%r{^(\w+://)?([^/:]+:)?(//\w*/)?(\w*/)*}, ""), ".git")
+ end
+
+ def shortref_for_path(ref)
+ ref[0..11]
+ end
+
+ def install_path
+ @install_path ||= begin
+ git_scope = "\#{base_name}-\#{shortref_for_path(revision)}"
+
+ gem_install_dir.join(git_scope)
+ end
+ end
+
+ def installed?
+ File.directory?(install_path)
+ end
+ end
+ RUBY
+ end
+ end
+
+ build_git "ma-gitp-gem"
+
+ gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
+ gem "ma-gitp-gem"
+ end
+ G
+ end
+
+ it "handles the source option" do
+ bundle "install"
+ expect(out).to include("Bundle complete!")
+ expect(the_bundle).to include_gems("ma-gitp-gem 1.0")
+ end
+
+ it "writes to lockfile" do
+ revision = revision_for(lib_path("ma-gitp-gem-1.0"))
+ bundle "install"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "ma-gitp-gem", "1.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ PLUGIN SOURCE
+ remote: #{lib_path("ma-gitp-gem-1.0")}
+ type: gitp
+ revision: #{revision}
+ specs:
+ ma-gitp-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ ma-gitp-gem!
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ context "with lockfile" do
+ before do
+ revision = revision_for(lib_path("ma-gitp-gem-1.0"))
+ lockfile <<-G
+ PLUGIN SOURCE
+ remote: #{lib_path("ma-gitp-gem-1.0")}
+ type: gitp
+ revision: #{revision}
+ specs:
+ ma-gitp-gem (1.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+
+ PLATFORMS
+ #{generic_local_platform}
+
+ DEPENDENCIES
+ ma-gitp-gem!
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+
+ it "installs" do
+ bundle "install"
+ expect(the_bundle).to include_gems("ma-gitp-gem 1.0")
+ end
+
+ it "uses the locked ref" do
+ update_git "ma-gitp-gem"
+ bundle "install"
+
+ run <<-RUBY
+ require 'ma/gitp/gem'
+ puts "WIN" unless defined?(MAGITPGEM_PREV_REF)
+ RUBY
+ expect(out).to eq("WIN")
+ end
+
+ it "updates the deps on bundler update" do
+ update_git "ma-gitp-gem"
+ bundle "update ma-gitp-gem"
+
+ run <<-RUBY
+ require 'ma/gitp/gem'
+ puts "WIN" if defined?(MAGITPGEM_PREV_REF)
+ RUBY
+ expect(out).to eq("WIN")
+ end
+
+ it "updates the deps on change in gemfile" do
+ update_git "ma-gitp-gem", "1.1", path: lib_path("ma-gitp-gem-1.0"), gemspec: true
+ gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source "#{lib_path("ma-gitp-gem-1.0")}", :type => :gitp do
+ gem "ma-gitp-gem", "1.1"
+ end
+ G
+ bundle "install"
+
+ expect(the_bundle).to include_gems("ma-gitp-gem 1.1")
+ end
+ end
+
+ describe "bundle cache with gitp" do
+ it "copies repository to vendor cache and uses it" do
+ git = build_git "foo"
+ ref = git.ref_for("main", 11)
+
+ install_gemfile <<-G
+ source "https://gem.repo2" # plugin source
+ source '#{lib_path("foo-1.0")}', :type => :gitp do
+ gem "foo"
+ end
+ G
+
+ bundle :cache
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.git")).not_to exist
+ expect(bundled_app("vendor/cache/foo-1.0-#{ref}/.bundlecache")).to be_file
+
+ FileUtils.rm_r lib_path("foo-1.0")
+ expect(the_bundle).to include_gems "foo 1.0"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugins/source_spec.rb b/spec/bundler/plugins/source_spec.rb
new file mode 100644
index 0000000000..995e50e653
--- /dev/null
+++ b/spec/bundler/plugins/source_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler source plugin" do
+ describe "plugins dsl eval for #source with :type option" do
+ before do
+ update_repo2 do
+ build_plugin "bundler-source-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class OPSource < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+ end
+
+ it "installs bundler-source-* gem when no handler for source is present" do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ plugin_should_be_installed("bundler-source-psource")
+ end
+
+ it "enables the plugin to require a lib path" do
+ update_repo2 do
+ build_plugin "bundler-source-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ require "bundler-source-psource"
+ class PSource < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+
+ expect(out).to include("Bundle complete!")
+ end
+
+ context "with an explicit handler" do
+ before do
+ update_repo2 do
+ build_plugin "another-psource" do |s|
+ s.write "plugins.rb", <<-RUBY
+ class Cheater < Bundler::Plugin::API
+ source "psource"
+ end
+ RUBY
+ end
+ end
+ end
+
+ context "explicit presence in gemfile" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ plugin "another-psource"
+
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "installs the explicit one" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ plugin_should_be_installed("another-psource")
+ end
+
+ it "doesn't install the default one" do
+ plugin_should_not_be_installed("bundler-source-psource")
+ end
+ end
+
+ context "explicit default source" do
+ before do
+ install_gemfile <<-G
+ source "https://gem.repo2"
+
+ plugin "bundler-source-psource"
+
+ source "#{lib_path("gitp")}", :type => :psource do
+ end
+ G
+ end
+
+ it "completes successfully" do
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "installs the default one" do
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ plugin_should_be_installed("bundler-source-psource")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/bundler/plugins/uninstall_spec.rb b/spec/bundler/plugins/uninstall_spec.rb
new file mode 100644
index 0000000000..dedcc9f37c
--- /dev/null
+++ b/spec/bundler/plugins/uninstall_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler plugin uninstall" do
+ before do
+ build_repo2 do
+ build_plugin "foo"
+ build_plugin "kung-foo"
+ end
+ end
+
+ it "shows proper error message when plugins are not specified" do
+ bundle "plugin uninstall"
+ expect(err).to include("No plugins to uninstall")
+ end
+
+ it "uninstalls specified plugins" do
+ bundle "plugin install foo kung-foo --source https://gem.repo2"
+ plugin_should_be_installed("foo")
+ plugin_should_be_installed("kung-foo")
+
+ bundle "plugin uninstall foo"
+ expect(out).to include("Uninstalled plugin foo")
+ plugin_should_not_be_installed("foo")
+ plugin_should_be_installed("kung-foo")
+ end
+
+ it "shows proper message when plugin is not installed" do
+ bundle "plugin uninstall foo"
+ expect(err).to include("Plugin foo is not installed")
+ plugin_should_not_be_installed("foo")
+ end
+
+ it "doesn't wipe out path plugins" do
+ build_lib "path_plugin" do |s|
+ s.write "plugins.rb"
+ end
+ path = lib_path("path_plugin-1.0")
+ expect(path).to be_a_directory
+
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+
+ install_gemfile <<-G
+ source 'https://gem.repo2'
+ plugin 'path_plugin', :path => "#{path}"
+ gem 'myrack', '1.0.0'
+ G
+
+ plugin_should_be_installed("path_plugin")
+ expect(Bundler::Plugin.index.plugin_path("path_plugin")).to eq path
+
+ bundle "plugin uninstall path_plugin"
+ expect(out).to include("Uninstalled plugin path_plugin")
+ plugin_should_not_be_installed("path_plugin")
+ # the actual gem still exists though
+ expect(path).to be_a_directory
+ end
+
+ describe "with --all" do
+ it "uninstalls all installed plugins" do
+ bundle "plugin install foo kung-foo --source https://gem.repo2"
+ plugin_should_be_installed("foo")
+ plugin_should_be_installed("kung-foo")
+
+ bundle "plugin uninstall --all"
+ plugin_should_not_be_installed("foo")
+ plugin_should_not_be_installed("kung-foo")
+ end
+
+ it "shows proper no plugins installed message when no plugins installed" do
+ bundle "plugin uninstall --all"
+ expect(out).to include("No plugins installed")
+ end
+ end
+end
diff --git a/spec/bundler/quality_es_spec.rb b/spec/bundler/quality_es_spec.rb
new file mode 100644
index 0000000000..e68674c030
--- /dev/null
+++ b/spec/bundler/quality_es_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+RSpec.describe "La biblioteca si misma" do
+ def check_for_expendable_words(filename)
+ failing_line_message = []
+ useless_words = %w[
+ básicamente
+ claramente
+ sólo
+ solamente
+ obvio
+ obviamente
+ fácil
+ fácilmente
+ sencillamente
+ simplemente
+ ]
+ pattern = /\b#{Regexp.union(useless_words)}\b/i
+
+ File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number|
+ next unless word_found = pattern.match(line)
+ failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Esta palabra tiene un significado subjetivo y es mejor obviarla en textos técnicos."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ def check_for_specific_pronouns(filename)
+ failing_line_message = []
+ specific_pronouns = /\b(él|ella|ellos|ellas)\b/i
+
+ File.readlines(File.expand_path(filename, source_root)).each_with_index do |line, number|
+ next unless word_found = specific_pronouns.match(line)
+ failing_line_message << "#{filename}:#{number.succ} contiene '#{word_found}'. Use pronombres más genéricos en la documentación."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ it "mantiene la calidad de lenguaje de la documentación" do
+ included = /ronn/
+ error_messages = []
+ man_tracked_files.each do |filename|
+ next unless filename&.match?(included)
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "mantiene la calidad de lenguaje de oraciones usadas en el código fuente" do
+ error_messages = []
+ exempt = /vendor/
+ lib_tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+end
diff --git a/spec/bundler/quality_spec.rb b/spec/bundler/quality_spec.rb
new file mode 100644
index 0000000000..16b7f18788
--- /dev/null
+++ b/spec/bundler/quality_spec.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require "set"
+
+RSpec.describe "The library itself" do
+ def check_for_git_merge_conflicts(filename)
+ merge_conflicts_regex = /
+ <<<<<<<|
+ =======|
+ >>>>>>>
+ /x
+
+ failing_lines = []
+ each_line(filename) do |line, number|
+ failing_lines << number + 1 if line&.match?(merge_conflicts_regex)
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has unresolved git merge conflicts on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_tab_characters(filename)
+ # Because Go uses hard tabs
+ return if filename.end_with?(".go.tt")
+
+ failing_lines = []
+ each_line(filename) do |line, number|
+ failing_lines << number + 1 if line.include?("\t")
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has tab characters on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_extra_spaces(filename)
+ failing_lines = []
+ each_line(filename) do |line, number|
+ next if /^\s+#.*\s+\n$/.match?(line)
+ failing_lines << number + 1 if /\s+\n$/.match?(line)
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has spaces on the EOL on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_extraneous_quotes(filename)
+ failing_lines = []
+ each_line(filename) do |line, number|
+ failing_lines << number + 1 if /\u{2019}/.match?(line)
+ end
+
+ return if failing_lines.empty?
+ "#{filename} has an extraneous quote on lines #{failing_lines.join(", ")}"
+ end
+
+ def check_for_expendable_words(filename)
+ failing_line_message = []
+ useless_words = %w[
+ actually
+ basically
+ clearly
+ just
+ obviously
+ really
+ simply
+ ]
+ pattern = /\b#{Regexp.union(useless_words)}\b/i
+
+ each_line(filename) do |line, number|
+ next unless word_found = pattern.match(line)
+ failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Avoid using these kinds of weak modifiers."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ def check_for_specific_pronouns(filename)
+ failing_line_message = []
+ specific_pronouns = /\b(he|she|his|hers|him|her|himself|herself)\b/i
+
+ each_line(filename) do |line, number|
+ next unless word_found = specific_pronouns.match(line)
+ failing_line_message << "#{filename}:#{number.succ} has '#{word_found}'. Use more generic pronouns in documentation."
+ end
+
+ failing_line_message unless failing_line_message.empty?
+ end
+
+ it "has no malformed whitespace" do
+ exempt = /\.gitmodules|fixtures|vendor|LICENSE|vcr_cassettes|rbreadline\.diff|index\.txt$/
+ error_messages = []
+ tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_tab_characters(filename)
+ error_messages << check_for_extra_spaces(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "has no extraneous quotes" do
+ exempt = /vendor|vcr_cassettes|LICENSE|rbreadline\.diff/
+ error_messages = []
+ tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_extraneous_quotes(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "does not include any unresolved merge conflicts" do
+ error_messages = []
+ exempt = %r{lock/lockfile_spec|quality_spec|vcr_cassettes|\.ronn|lockfile_parser}
+ tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_git_merge_conflicts(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "maintains language quality of the documentation" do
+ error_messages = []
+ man_tracked_files.each do |filename|
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "maintains language quality of sentences used in source code" do
+ error_messages = []
+ exempt = /vendor|vcr_cassettes|CODE_OF_CONDUCT/
+ lib_tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ error_messages << check_for_expendable_words(filename)
+ error_messages << check_for_specific_pronouns(filename)
+ end
+ expect(error_messages.compact).to be_well_formed
+ end
+
+ it "documents all used settings" do
+ exemptions = %w[
+ gem.changelog
+ gem.ci
+ gem.coc
+ gem.linter
+ gem.mit
+ gem.bundle
+ gem.rubocop
+ gem.test
+ git.allow_insecure
+ inline
+ trust-policy
+ ]
+
+ all_settings = Hash.new {|h, k| h[k] = [] }
+ documented_settings = []
+
+ Bundler::Settings::BOOL_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::BOOL_KEYS" }
+ Bundler::Settings::NUMBER_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::NUMBER_KEYS" }
+ Bundler::Settings::ARRAY_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::ARRAY_KEYS" }
+ Bundler::Settings::STRING_KEYS.each {|k| all_settings[k] << "in Bundler::Settings::STRING_KEYS" }
+
+ key_pattern = /([a-z\._-]+)/i
+ lib_tracked_files.each do |filename|
+ each_line(filename) do |line, number|
+ line.scan(/Bundler\.settings\[:#{key_pattern}\]/).flatten.each {|s| all_settings[s] << "referenced at `#{filename}:#{number.succ}`" }
+ end
+ end
+ settings_section = File.read(source_root.join("lib/bundler/man/bundle-config.1.ronn")).split(/^## /).find {|section| section.start_with?("LIST OF AVAILABLE KEYS") }
+ documented_settings = settings_section.scan(/^\* `#{key_pattern}`/).flatten
+
+ documented_settings.each do |s|
+ all_settings.delete(s)
+ expect(exemptions.delete(s)).to be_nil, "setting #{s} was exempted but was actually documented"
+ end
+
+ exemptions.each do |s|
+ expect(all_settings.delete(s)).to be_truthy, "setting #{s} was exempted but unused"
+ end
+ error_messages = all_settings.map do |setting, refs|
+ "The `#{setting}` setting is undocumented\n\t- #{refs.join("\n\t- ")}\n"
+ end
+
+ expect(error_messages.sort).to be_well_formed
+
+ expect(documented_settings).to be_sorted
+ end
+
+ it "can still be built" do
+ with_built_bundler do |gem_path|
+ expect(File.exist?(gem_path)).to be true
+ end
+ end
+
+ it "ships the correct set of files" do
+ git_list = tracked_files.reject {|f| f.start_with?("spec/") }
+
+ gem_list = loaded_gemspec.files
+ gem_list.map! {|f| f.sub(%r{\Aexe/}, "libexec/") } if ruby_core?
+
+ expect(git_list).to match_array(gem_list)
+ end
+
+ it "does not contain any warnings" do
+ exclusions = %w[
+ lib/bundler/capistrano.rb
+ lib/bundler/deployment.rb
+ lib/bundler/gem_tasks.rb
+ lib/bundler/vlad.rb
+ ]
+ files_to_require = lib_tracked_files.grep(/\.rb$/) - exclusions
+ files_to_require.reject! {|f| f.start_with?("lib/bundler/vendor") }
+ files_to_require.map! {|f| File.expand_path(f, source_root) }
+ files_to_require.sort!
+ sys_exec("ruby -w") do |input, _, _|
+ files_to_require.each do |f|
+ input.puts "require '#{f}'"
+ end
+ end
+
+ warnings = stdboth.split("\n")
+ # ignore warnings around deprecated Object#=~ method in RubyGems
+ warnings.reject! {|w| w =~ %r{rubygems\/version.rb.*deprecated\ Object#=~} }
+
+ expect(warnings).to be_well_formed
+ end
+
+ it "does not use require internally, but require_relative" do
+ exempt = %r{templates/|\.5|\.1|vendor/}
+ all_bad_requires = []
+ lib_tracked_files.each do |filename|
+ next if filename&.match?(exempt)
+ each_line(filename) do |line, number|
+ line.scan(/^ *require "bundler/).each { all_bad_requires << "#{filename}:#{number.succ}" }
+ end
+ end
+
+ expect(all_bad_requires).to be_empty, "#{all_bad_requires.size} internal requires that should use `require_relative`: #{all_bad_requires}"
+ end
+
+ # We don't want our artifice code to activate bundler, but it needs to use the
+ # namespaced implementation of `Net::HTTP`. So we duplicate the file in
+ # bundler that loads that.
+ it "keeps vendored_net_http spec code in sync with the lib implementation" do
+ lib_implementation_path = File.join(source_lib_dir, "bundler", "vendored_net_http.rb")
+ expect(File.exist?(lib_implementation_path)).to be_truthy
+ lib_code = File.read(lib_implementation_path)
+
+ spec_implementation_path = File.join(spec_dir, "support", "vendored_net_http.rb")
+ expect(File.exist?(spec_implementation_path)).to be_truthy
+ spec_code = File.read(spec_implementation_path)
+
+ expect(lib_code).to eq(spec_code)
+ end
+
+ private
+
+ def each_line(filename, &block)
+ File.readlines(File.expand_path(filename, source_root), encoding: "UTF-8").each_with_index(&block)
+ end
+end
diff --git a/spec/bundler/realworld/double_check_spec.rb b/spec/bundler/realworld/double_check_spec.rb
new file mode 100644
index 0000000000..0741560395
--- /dev/null
+++ b/spec/bundler/realworld/double_check_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+RSpec.describe "double checking sources", realworld: true do
+ it "finds already-installed gems" do
+ create_file("rails.gemspec", <<-RUBY)
+ Gem::Specification.new do |s|
+ s.name = "rails"
+ s.version = "5.1.4"
+ s.summary = ""
+ s.description = ""
+ s.author = ""
+ s.add_dependency "actionpack", "5.1.4"
+ end
+ RUBY
+
+ create_file("actionpack.gemspec", <<-RUBY)
+ Gem::Specification.new do |s|
+ s.name = "actionpack"
+ s.version = "5.1.4"
+ s.summary = ""
+ s.description = ""
+ s.author = ""
+ s.add_dependency "rack", "~> 2.0.0"
+ end
+ RUBY
+
+ cmd = <<-RUBY
+ require "bundler"
+ require "#{spec_dir}/support/artifice/vcr"
+ require "bundler/inline"
+ gemfile(true) do
+ source "https://rubygems.org"
+ gem "rails", path: "."
+ end
+ RUBY
+
+ ruby cmd
+ ruby cmd
+ end
+end
diff --git a/spec/bundler/realworld/edgecases_spec.rb b/spec/bundler/realworld/edgecases_spec.rb
new file mode 100644
index 0000000000..391aa0cef6
--- /dev/null
+++ b/spec/bundler/realworld/edgecases_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+RSpec.describe "real world edgecases", realworld: true do
+ def rubygems_version(name, requirement)
+ ruby <<-RUBY
+ require "#{spec_dir}/support/artifice/vcr"
+ require "bundler"
+ require "bundler/source/rubygems/remote"
+ require "bundler/fetcher"
+ rubygem = Bundler.ui.silence do
+ remote = Bundler::Source::Rubygems::Remote.new(Gem::URI("https://rubygems.org"))
+ source = Bundler::Source::Rubygems.new
+ fetcher = Bundler::Fetcher.new(remote)
+ index = fetcher.specs([#{name.dump}], source)
+ requirement = Gem::Requirement.create(#{requirement.dump})
+ index.search(#{name.dump}).select {|spec| requirement.satisfied_by?(spec.version) }.last
+ end
+ if rubygem.nil?
+ raise ArgumentError, "Could not find #{name} (#{requirement}) on rubygems.org!\n" \
+ "Found specs:\n\#{index.send(:specs).inspect}"
+ end
+ puts "#{name} (\#{rubygem.version})"
+ RUBY
+ end
+
+ it "resolves dependencies correctly" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem 'rails', '~> 5.0'
+ gem 'capybara', '~> 2.2.0'
+ gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9
+ G
+ bundle :lock
+ expect(lockfile).to include(rubygems_version("rails", "~> 5.0"))
+ expect(lockfile).to include("capybara (2.2.1)")
+ end
+
+ it "installs the latest version of gxapi_rails" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem "sass-rails"
+ gem "rails", "~> 5"
+ gem "gxapi_rails", "< 0.1.0" # 0.1.0 was released way after the test was written
+ gem 'rack-cache', '1.2.0' # last version that works on Ruby 1.9
+ G
+ bundle :lock
+ expect(lockfile).to include("gxapi_rails (0.0.6)")
+ end
+
+ it "installs the latest version of i18n" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem "i18n", "~> 0.6.0"
+ gem "activesupport", "~> 3.0"
+ gem "activerecord", "~> 3.0"
+ gem "builder", "~> 2.1.2"
+ G
+ bundle :lock
+ expect(lockfile).to include(rubygems_version("i18n", "~> 0.6.0"))
+ expect(lockfile).to include(rubygems_version("activesupport", "~> 3.0"))
+ end
+
+ it "outputs a helpful warning when gems have a gemspec with invalid `require_paths`" do
+ install_gemfile <<-G, standalone: true, env: { "BUNDLE_FORCE_RUBY_PLATFORM" => "1" }
+ source 'https://rubygems.org'
+ gem "resque-scheduler", "2.2.0"
+ gem "redis-namespace", "1.6.0" # for a consistent resolution including ruby 2.3.0
+ gem "ruby2_keywords", "0.0.5"
+ G
+ expect(err).to include("resque-scheduler 2.2.0 includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this").once
+ end
+end
diff --git a/spec/bundler/realworld/ffi_spec.rb b/spec/bundler/realworld/ffi_spec.rb
new file mode 100644
index 0000000000..bede372b41
--- /dev/null
+++ b/spec/bundler/realworld/ffi_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+RSpec.describe "loading dynamically linked library on a bundle exec context", realworld: true do
+ it "passes ENV right after argv in memory" do
+ create_file "foo.rb", <<~RUBY
+ require 'ffi'
+
+ module FOO
+ extend FFI::Library
+ ffi_lib './libfoo.so'
+
+ attach_function :Hello, [], :void
+ end
+
+ FOO.Hello()
+ RUBY
+
+ create_file "libfoo.c", <<~'C'
+ #include <stdio.h>
+
+ static int foo_init(int argc, char** argv, char** envp) {
+ if (argv[argc+1] == NULL) {
+ printf("FAIL\n");
+ } else {
+ printf("OK\n");
+ }
+
+ return 0;
+ }
+
+ #if defined(__APPLE__) && defined(__MACH__)
+ __attribute__((section("__DATA,__mod_init_func"), used, aligned(sizeof(void*))))
+ #else
+ __attribute__((section(".init_array")))
+ #endif
+ static void *ctr = &foo_init;
+
+ extern char** environ;
+
+ void Hello() {
+ return;
+ }
+ C
+
+ in_bundled_app "gcc -g -o libfoo.so -shared -fpic libfoo.c"
+
+ install_gemfile <<-G
+ source "https://rubygems.org"
+
+ gem 'ffi', force_ruby_platform: true
+ G
+
+ bundle "exec ruby foo.rb"
+
+ expect(out).to eq("OK")
+ end
+end
diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile b/spec/bundler/realworld/fixtures/tapioca/Gemfile
new file mode 100644
index 0000000000..447d715706
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "tapioca"
diff --git a/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
new file mode 100644
index 0000000000..c2df2f9229
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/tapioca/Gemfile.lock
@@ -0,0 +1,49 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ erubi (1.13.1)
+ netrc (0.11.0)
+ parallel (1.26.3)
+ prism (1.3.0)
+ rbi (0.2.2)
+ prism (~> 1.0)
+ sorbet-runtime (>= 0.5.9204)
+ sorbet (0.5.11725)
+ sorbet-static (= 0.5.11725)
+ sorbet-runtime (0.5.11725)
+ sorbet-static (0.5.11725-aarch64-linux)
+ sorbet-static (0.5.11725-universal-darwin)
+ sorbet-static (0.5.11725-x86_64-linux)
+ sorbet-static-and-runtime (0.5.11725)
+ sorbet (= 0.5.11725)
+ sorbet-runtime (= 0.5.11725)
+ spoom (1.5.0)
+ erubi (>= 1.10.0)
+ prism (>= 0.28.0)
+ sorbet-static-and-runtime (>= 0.5.10187)
+ thor (>= 0.19.2)
+ tapioca (0.16.6)
+ bundler (>= 2.2.25)
+ netrc (>= 0.11.0)
+ parallel (>= 1.21.0)
+ rbi (~> 0.2)
+ sorbet-static-and-runtime (>= 0.5.11087)
+ spoom (>= 1.2.0)
+ thor (>= 1.2.0)
+ yard-sorbet
+ thor (1.4.0)
+ yard (0.9.42)
+ yard-sorbet (0.9.0)
+ sorbet-runtime
+ yard
+
+PLATFORMS
+ aarch64-linux
+ universal-darwin
+ x86_64-linux
+
+DEPENDENCIES
+ tapioca
+
+BUNDLED WITH
+ 4.1.0.dev
diff --git a/spec/bundler/realworld/fixtures/warbler/.gitignore b/spec/bundler/realworld/fixtures/warbler/.gitignore
new file mode 100644
index 0000000000..d392f0e82c
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/.gitignore
@@ -0,0 +1 @@
+*.jar
diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile b/spec/bundler/realworld/fixtures/warbler/Gemfile
new file mode 100644
index 0000000000..5687bbd975
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/Gemfile
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem "demo", path: "./demo"
+gem "jruby-jars", "~> 10.0"
+gem "warbler", "~> 2.1"
diff --git a/spec/bundler/realworld/fixtures/warbler/Gemfile.lock b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
new file mode 100644
index 0000000000..05f3bc4e3f
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/Gemfile.lock
@@ -0,0 +1,35 @@
+PATH
+ remote: demo
+ specs:
+ demo (1.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ jruby-jars (10.0.0.1)
+ jruby-rack (1.2.7)
+ ostruct (0.6.3)
+ rake (13.3.0)
+ rexml (3.4.2)
+ rubyzip (3.3.0)
+ warbler (2.1.0)
+ jruby-jars (>= 9.4, < 10.1)
+ jruby-rack (>= 1.2.3, < 1.3)
+ ostruct (~> 0.6.2)
+ rake (~> 13.0, >= 13.0.3)
+ rexml (~> 3.0)
+ rubyzip (>= 3.0.0)
+
+PLATFORMS
+ arm64-darwin
+ java
+ ruby
+ universal-java
+
+DEPENDENCIES
+ demo!
+ jruby-jars (~> 10.0)
+ warbler (~> 2.1)
+
+BUNDLED WITH
+ 4.1.0.dev
diff --git a/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb b/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb
new file mode 100644
index 0000000000..25f614ecc2
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/bin/warbler-example.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+puts require "bundler/setup"
diff --git a/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec b/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec
new file mode 100644
index 0000000000..ed5a0dc080
--- /dev/null
+++ b/spec/bundler/realworld/fixtures/warbler/demo/demo.gemspec
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+Gem::Specification.new do |spec|
+ spec.name = "demo"
+ spec.version = "1.0"
+ spec.author = "Somebody"
+ spec.summary = "A demo gem"
+ spec.license = "MIT"
+ spec.homepage = "https://example.org"
+end
diff --git a/spec/bundler/realworld/git_spec.rb b/spec/bundler/realworld/git_spec.rb
new file mode 100644
index 0000000000..9eff74f1c9
--- /dev/null
+++ b/spec/bundler/realworld/git_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.describe "github source", realworld: true do
+ it "properly fetches PRs" do
+ install_gemfile <<-G
+ source "https://rubygems.org"
+
+ gem "reline", github: "https://github.com/ruby/reline/pull/488"
+ G
+ end
+end
diff --git a/spec/bundler/realworld/parallel_spec.rb b/spec/bundler/realworld/parallel_spec.rb
new file mode 100644
index 0000000000..b57fdfd0ee
--- /dev/null
+++ b/spec/bundler/realworld/parallel_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.describe "parallel", realworld: true do
+ it "installs" do
+ gemfile <<-G
+ source "https://rubygems.org"
+ gem 'activesupport', '~> 3.2.13'
+ gem 'faker', '~> 1.1.2'
+ gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+
+ G
+
+ bundle :install, jobs: 4, env: { "DEBUG" => "1" }
+
+ expect(out).to match(/[1-3]: /)
+
+ bundle "info activesupport --path"
+ expect(out).to match(/activesupport/)
+
+ bundle "info faker --path"
+ expect(out).to match(/faker/)
+ end
+
+ it "updates" do
+ install_gemfile <<-G
+ source "https://rubygems.org"
+ gem 'activesupport', '3.2.12'
+ gem 'faker', '~> 1.1.2'
+ G
+
+ gemfile <<-G
+ source "https://rubygems.org"
+ gem 'activesupport', '~> 3.2.12'
+ gem 'faker', '~> 1.1.2'
+ gem 'i18n', '~> 0.6.0' # Because 0.7+ requires Ruby 1.9.3+
+ G
+
+ bundle :update, jobs: 4, env: { "DEBUG" => "1" }, all: true
+
+ expect(out).to match(/[1-3]: /)
+
+ bundle "info activesupport --path"
+ expect(out).to match(/activesupport-3\.2\.\d+/)
+
+ bundle "info faker --path"
+ expect(out).to match(/faker/)
+ end
+
+ it "works with --standalone" do
+ gemfile <<-G
+ source "https://rubygems.org"
+ gem "diff-lcs"
+ G
+
+ bundle :install, standalone: true, jobs: 4
+
+ ruby <<-RUBY
+ $:.unshift File.expand_path("bundle")
+ require "bundler/setup"
+
+ require "diff/lcs"
+ puts Diff::LCS
+ RUBY
+
+ expect(out).to eq("Diff::LCS")
+ end
+end
diff --git a/spec/bundler/realworld/slow_perf_spec.rb b/spec/bundler/realworld/slow_perf_spec.rb
new file mode 100644
index 0000000000..5d36ba7455
--- /dev/null
+++ b/spec/bundler/realworld/slow_perf_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "bundle install with complex dependencies", realworld: true do
+ it "resolves quickly" do
+ gemfile <<-G
+ source 'https://rubygems.org'
+
+ gem "actionmailer"
+ gem "mongoid", ">= 0.10.2"
+ G
+
+ bundle "lock", env: { "DEBUG_RESOLVER" => "1" }
+ expect(out).to include("Solution found after 1 attempts")
+ end
+
+ it "resolves quickly (case 2)" do
+ gemfile <<-G
+ source "https://rubygems.org"
+
+ gem 'metasploit-erd'
+ gem 'rails-erd'
+ gem 'yard'
+
+ gem 'coveralls'
+ gem 'rails'
+ gem 'simplecov'
+ gem 'rspec-rails'
+ G
+
+ bundle "lock", env: { "DEBUG_RESOLVER" => "1" }
+ expect(out).to include("Solution found after 2 attempts")
+ end
+end
diff --git a/spec/bundler/resolver/basic_spec.rb b/spec/bundler/resolver/basic_spec.rb
new file mode 100644
index 0000000000..185df1b1c7
--- /dev/null
+++ b/spec/bundler/resolver/basic_spec.rb
@@ -0,0 +1,422 @@
+# frozen_string_literal: true
+
+RSpec.describe "Resolving" do
+ before :each do
+ @index = an_awesome_index
+ end
+
+ it "resolves a single gem" do
+ dep "myrack"
+
+ should_resolve_as %w[myrack-1.1]
+ end
+
+ it "resolves a gem with dependencies" do
+ dep "actionpack"
+
+ should_resolve_as %w[actionpack-2.3.5 activesupport-2.3.5 myrack-1.0]
+ end
+
+ it "resolves a conflicting index" do
+ @index = a_conflict_index
+ dep "my_app"
+ should_resolve_as %w[activemodel-3.2.11 builder-3.0.4 grape-0.2.6 my_app-1.0.0]
+ end
+
+ it "resolves a complex conflicting index" do
+ @index = a_complex_conflict_index
+ dep "my_app"
+ should_resolve_as %w[a-1.4.0 b-0.3.5 c-3.2 d-0.9.8 my_app-1.1.0]
+ end
+
+ it "resolves a index with conflict on child" do
+ @index = index_with_conflict_on_child
+ dep "chef_app"
+ should_resolve_as %w[berkshelf-2.0.7 chef-10.26 chef_app-1.0.0 json-1.7.7]
+ end
+
+ it "prefers explicitly requested dependencies when resolving an index which would otherwise be ambiguous" do
+ @index = an_ambiguous_index
+ dep "a"
+ dep "b"
+ should_resolve_as %w[a-1.0.0 b-2.0.0 c-1.0.0 d-1.0.0]
+ end
+
+ it "prefers non-prerelease resolutions in sort order" do
+ @index = optional_prereleases_index
+ dep "a"
+ dep "b"
+ should_resolve_as %w[a-1.0.0 b-1.5.0]
+ end
+
+ it "resolves a index with root level conflict on child" do
+ @index = a_index_with_root_conflict_on_child
+ dep "i18n", "~> 0.4"
+ dep "activesupport", "~> 3.0"
+ dep "activerecord", "~> 3.0"
+ dep "builder", "~> 2.1.2"
+ should_resolve_as %w[activesupport-3.0.5 i18n-0.4.2 builder-2.1.2 activerecord-3.0.5 activemodel-3.0.5]
+ end
+
+ it "resolves a gem specified with a pre-release version" do
+ dep "activesupport", "~> 3.0.0.beta"
+ dep "activemerchant"
+ should_resolve_as %w[activemerchant-2.3.5 activesupport-3.0.0.beta1]
+ end
+
+ it "doesn't select a pre-release if not specified in the Gemfile" do
+ dep "activesupport"
+ dep "reform"
+ should_resolve_as %w[reform-1.0.0 activesupport-2.3.5]
+ end
+
+ it "doesn't select a pre-release for sub-dependencies" do
+ dep "reform"
+ should_resolve_as %w[reform-1.0.0 activesupport-2.3.5]
+ end
+
+ it "selects a pre-release for sub-dependencies if it's the only option" do
+ dep "need-pre"
+ should_resolve_as %w[need-pre-1.0.0 activesupport-3.0.0.beta1]
+ end
+
+ it "selects a pre-release if it's specified in the Gemfile" do
+ dep "activesupport", "= 3.0.0.beta"
+ dep "actionpack"
+
+ should_resolve_as %w[activesupport-3.0.0.beta actionpack-3.0.0.beta myrack-1.1 myrack-mount-0.6]
+ end
+
+ it "prefers non-pre-releases when doing conservative updates" do
+ @index = build_index do
+ gem "mail", "2.7.0"
+ gem "mail", "2.7.1.rc1"
+ gem "RubyGems\0", Gem::VERSION
+ end
+ dep "mail"
+ @locked = locked ["mail", "2.7.0"]
+ @base = locked
+ should_conservative_resolve_and_include [:patch], [], ["mail-2.7.0"]
+ end
+
+ it "raises an exception if a child dependency is not resolved" do
+ @index = a_unresolvable_child_index
+ dep "chef_app_error"
+ expect do
+ resolve
+ end.to raise_error(Bundler::SolveFailure)
+ end
+
+ it "does not try to re-resolve including prereleases if gems involved don't have prereleases" do
+ @index = a_unresolvable_child_index
+ dep "chef_app_error"
+ expect(Bundler.ui).not_to receive(:debug).with("Retrying resolution...", any_args)
+ expect do
+ resolve
+ end.to raise_error(Bundler::SolveFailure)
+ end
+
+ it "raises an exception with the minimal set of conflicting dependencies" do
+ @index = build_index do
+ %w[0.9 1.0 2.0].each {|v| gem("a", v) }
+ gem("b", "1.0") { dep "a", ">= 2" }
+ gem("c", "1.0") { dep "a", "< 1" }
+ end
+ dep "a"
+ dep "b"
+ dep "c"
+ expect do
+ resolve
+ end.to raise_error(Bundler::SolveFailure, <<~E.strip)
+ Could not find compatible versions
+
+ Because every version of c depends on a < 1
+ and every version of b depends on a >= 2,
+ every version of c is incompatible with b >= 0.
+ So, because Gemfile depends on b >= 0
+ and Gemfile depends on c >= 0,
+ version solving has failed.
+ E
+ end
+
+ it "should throw error in case of circular dependencies" do
+ @index = a_circular_index
+ dep "circular_app"
+
+ expect do
+ Bundler::SpecSet.new(resolve).sort
+ end.to raise_error(Bundler::CyclicDependencyError, /please remove either gem 'bar' or gem 'foo'/i)
+ end
+
+ # Issue #3459
+ it "should install the latest possible version of a direct requirement with no constraints given" do
+ @index = a_complicated_index
+ dep "foo"
+ should_resolve_and_include %w[foo-3.0.5]
+ end
+
+ # Issue #3459
+ it "should install the latest possible version of a direct requirement with constraints given" do
+ @index = a_complicated_index
+ dep "foo", ">= 3.0.0"
+ should_resolve_and_include %w[foo-3.0.5]
+ end
+
+ it "takes into account required_ruby_version" do
+ @index = build_index do
+ gem "foo", "1.0.0" do
+ dep "bar", ">= 0"
+ end
+
+ gem "foo", "2.0.0" do |s|
+ dep "bar", ">= 0"
+ s.required_ruby_version = "~> 2.0.0"
+ end
+
+ gem "bar", "1.0.0"
+
+ gem "bar", "2.0.0" do |s|
+ s.required_ruby_version = "~> 2.0.0"
+ end
+
+ gem "Ruby\0", "1.8.7"
+ end
+ dep "foo"
+ dep "Ruby\0", "1.8.7"
+
+ should_resolve_and_include %w[foo-1.0.0 bar-1.0.0]
+ end
+
+ context "conservative" do
+ before :each do
+ @index = build_index do
+ gem("foo", "1.3.7") { dep "bar", "~> 2.0" }
+ gem("foo", "1.3.8") { dep "bar", "~> 2.0" }
+ gem("foo", "1.4.3") { dep "bar", "~> 2.0" }
+ gem("foo", "1.4.4") { dep "bar", "~> 2.0" }
+ gem("foo", "1.4.5") { dep "bar", "~> 2.1" }
+ gem("foo", "1.5.0") { dep "bar", "~> 2.1" }
+ gem("foo", "1.5.1") { dep "bar", "~> 3.0" }
+ gem("foo", "2.0.0") { dep "bar", "~> 3.0" }
+ gem "bar", %w[2.0.3 2.0.4 2.0.5 2.1.0 2.1.1 3.0.0]
+ end
+ dep "foo"
+
+ # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile
+ @base = Bundler::SpecSet.new([])
+
+ # locked represents versions in lockfile
+ @locked = locked(%w[foo 1.4.3], %w[bar 2.0.3])
+ end
+
+ it "resolves all gems to latest patch" do
+ # strict is not set, so bar goes up a minor version due to dependency from foo 1.4.5
+ should_conservative_resolve_and_include :patch, true, %w[foo-1.4.5 bar-2.1.1]
+ end
+
+ it "resolves all gems to latest patch strict" do
+ # strict is set, so foo can only go up to 1.4.4 to avoid bar going up a minor version, and bar can go up to 2.0.5
+ should_conservative_resolve_and_include [:patch, :strict], true, %w[foo-1.4.4 bar-2.0.5]
+ end
+
+ it "resolves foo only to latest patch - same dependency case" do
+ @locked = locked(%w[foo 1.3.7], %w[bar 2.0.3])
+ # bar is locked, and the lock holds here because the dependency on bar doesn't change on the matching foo version.
+ should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.3.8 bar-2.0.3]
+ end
+
+ it "resolves foo only to latest patch - changing dependency not declared case" do
+ # foo is the only gem being requested for update, therefore bar is locked, but bar is NOT
+ # declared as a dependency in the Gemfile. In this case, locks don't apply to _changing_
+ # dependencies and since the dependency of the selected foo gem changes, the latest matching
+ # dependency of "bar", "~> 2.1" -- bar-2.1.1 -- is selected. This is not a bug and follows
+ # the long-standing documented Conservative Updating behavior of bundle install.
+ # https://bundler.io/v1.12/man/bundle-install.1.html#CONSERVATIVE-UPDATING
+ should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.5 bar-2.1.1]
+ end
+
+ it "resolves foo only to latest patch - changing dependency declared case" do
+ # bar is locked AND a declared dependency in the Gemfile, so it will not move, and therefore
+ # foo can only move up to 1.4.4.
+ @base = Bundler::SpecSet.new([Bundler::LazySpecification.new("bar", Gem::Version.new("2.0.3"), nil)])
+ should_conservative_resolve_and_include :patch, ["foo"], %w[foo-1.4.4 bar-2.0.3]
+ end
+
+ it "resolves foo only to latest patch strict" do
+ # adding strict helps solve the possibly unexpected behavior of bar changing in the prior test case,
+ # because no versions will be returned for bar ~> 2.1, so the engine falls back to ~> 2.0 (turn on
+ # debugging to see this happen).
+ should_conservative_resolve_and_include [:patch, :strict], ["foo"], %w[foo-1.4.4 bar-2.0.3]
+ end
+
+ it "resolves bar only to latest patch" do
+ # bar is locked, so foo can only go up to 1.4.4
+ should_conservative_resolve_and_include :patch, ["bar"], %w[foo-1.4.3 bar-2.0.5]
+ end
+
+ it "resolves all gems to latest minor" do
+ # strict is not set, so bar goes up a major version due to dependency from foo 1.4.5
+ should_conservative_resolve_and_include :minor, true, %w[foo-1.5.1 bar-3.0.0]
+ end
+
+ it "resolves all gems to latest minor strict" do
+ # strict is set, so foo can only go up to 1.5.0 to avoid bar going up a major version
+ should_conservative_resolve_and_include [:minor, :strict], true, %w[foo-1.5.0 bar-2.1.1]
+ end
+
+ it "resolves all gems to latest major" do
+ should_conservative_resolve_and_include :major, true, %w[foo-2.0.0 bar-3.0.0]
+ end
+
+ it "resolves all gems to latest major strict" do
+ should_conservative_resolve_and_include [:major, :strict], true, %w[foo-2.0.0 bar-3.0.0]
+ end
+
+ # Why would this happen in real life? If bar 2.2 has a bug that the author of foo wants to bypass
+ # by reverting the dependency, the author of foo could release a new gem with an older requirement.
+ context "revert to previous" do
+ before :each do
+ @index = build_index do
+ gem("foo", "1.4.3") { dep "bar", "~> 2.2" }
+ gem("foo", "1.4.4") { dep "bar", "~> 2.1.0" }
+ gem("foo", "1.5.0") { dep "bar", "~> 2.0.0" }
+ gem "bar", %w[2.0.5 2.1.1 2.2.3]
+ end
+ dep "foo"
+
+ # base represents declared dependencies in the Gemfile that are still satisfied by the lockfile
+ @base = Bundler::SpecSet.new([])
+
+ # locked represents versions in lockfile
+ @locked = locked(%w[foo 1.4.3], %w[bar 2.2.3])
+ end
+
+ it "could revert to a previous version level patch" do
+ should_conservative_resolve_and_include :patch, true, %w[foo-1.4.4 bar-2.1.1]
+ end
+
+ it "cannot revert to a previous version in strict mode level patch" do
+ # fall back to the locked resolution since strict means we can't regress either version
+ should_conservative_resolve_and_include [:patch, :strict], true, %w[foo-1.4.3 bar-2.2.3]
+ end
+
+ it "could revert to a previous version level minor" do
+ should_conservative_resolve_and_include :minor, true, %w[foo-1.5.0 bar-2.0.5]
+ end
+
+ it "cannot revert to a previous version in strict mode level minor" do
+ # fall back to the locked resolution since strict means we can't regress either version
+ should_conservative_resolve_and_include [:minor, :strict], true, %w[foo-1.4.3 bar-2.2.3]
+ end
+ end
+ end
+
+ it "handles versions that redundantly depend on themselves" do
+ @index = build_index do
+ gem "myrack", "3.0.0"
+
+ gem "standalone_migrations", "7.1.0" do
+ dep "myrack", "~> 2.0"
+ end
+
+ gem "standalone_migrations", "2.0.4" do
+ dep "standalone_migrations", ">= 0"
+ end
+
+ gem "standalone_migrations", "1.0.13" do
+ dep "myrack", ">= 0"
+ end
+ end
+
+ dep "myrack", "~> 3.0"
+ dep "standalone_migrations"
+
+ should_resolve_as %w[myrack-3.0.0 standalone_migrations-2.0.4]
+ end
+
+ it "ignores versions that incorrectly depend on themselves" do
+ @index = build_index do
+ gem "myrack", "3.0.0"
+
+ gem "standalone_migrations", "7.1.0" do
+ dep "myrack", "~> 2.0"
+ end
+
+ gem "standalone_migrations", "2.0.4" do
+ dep "standalone_migrations", ">= 2.0.5"
+ end
+
+ gem "standalone_migrations", "1.0.13" do
+ dep "myrack", ">= 0"
+ end
+ end
+
+ dep "myrack", "~> 3.0"
+ dep "standalone_migrations"
+
+ should_resolve_as %w[myrack-3.0.0 standalone_migrations-1.0.13]
+ end
+
+ it "does not ignore versions that incorrectly depend on themselves when dependency_api is not available" do
+ @index = build_index do
+ gem "myrack", "3.0.0"
+
+ gem "standalone_migrations", "7.1.0" do
+ dep "myrack", "~> 2.0"
+ end
+
+ gem "standalone_migrations", "2.0.4" do
+ dep "standalone_migrations", ">= 2.0.5"
+ end
+
+ gem "standalone_migrations", "1.0.13" do
+ dep "myrack", ">= 0"
+ end
+ end
+
+ dep "myrack", "~> 3.0"
+ dep "standalone_migrations"
+
+ should_resolve_without_dependency_api %w[myrack-3.0.0 standalone_migrations-2.0.4]
+ end
+
+ it "resolves fine cases that need joining unbounded disjoint ranges" do
+ @index = build_index do
+ gem "inspec", "5.22.3" do
+ dep "ruby", ">= 3.2.2"
+ dep "train-kubernetes", ">= 0.1.7"
+ end
+
+ gem "ruby", "3.2.2"
+
+ gem "train-kubernetes", "0.1.12" do
+ dep "k8s-ruby", ">= 0.14.0"
+ end
+
+ gem "train-kubernetes", "0.1.10" do
+ dep "k8s-ruby", "= 0.10.5"
+ end
+
+ gem "train-kubernetes", "0.1.7" do
+ dep "k8s-ruby", ">= 0.10.5"
+ end
+
+ gem "k8s-ruby", "0.10.5" do
+ dep "ruby","< 3.2.2"
+ end
+
+ gem "k8s-ruby", "0.11.0" do
+ dep "ruby", ">= 3.2.2"
+ end
+
+ gem "k8s-ruby", "0.14.0" do
+ dep "ruby", "< 3.2.2"
+ end
+ end
+
+ dep "inspec", "5.22.3"
+ dep "ruby", "3.2.2"
+
+ should_resolve_as %w[inspec-5.22.3 ruby-3.2.2 train-kubernetes-0.1.7 k8s-ruby-0.11.0]
+ end
+end
diff --git a/spec/bundler/resolver/platform_spec.rb b/spec/bundler/resolver/platform_spec.rb
new file mode 100644
index 0000000000..a1d095d024
--- /dev/null
+++ b/spec/bundler/resolver/platform_spec.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+RSpec.describe "Resolving platform craziness" do
+ describe "with cross-platform gems" do
+ before :each do
+ @index = an_awesome_index
+ end
+
+ it "resolves a simple multi platform gem" do
+ dep "nokogiri"
+ platforms "ruby", "java"
+
+ should_resolve_as %w[nokogiri-1.4.2 nokogiri-1.4.2-java weakling-0.0.3]
+ end
+
+ it "doesn't pull gems that don't exist for the current platform" do
+ dep "nokogiri"
+ platforms "ruby"
+
+ should_resolve_as %w[nokogiri-1.4.2]
+ end
+
+ it "doesn't pull gems when the version is available for all requested platforms" do
+ dep "nokogiri"
+ platforms "mswin32"
+
+ should_resolve_as %w[nokogiri-1.4.2.1-x86-mswin32]
+ end
+ end
+
+ it "resolves multiplatform gems with redundant platforms correctly" do
+ @index = build_index do
+ gem "zookeeper", "1.4.11"
+ gem "zookeeper", "1.4.11", "java" do
+ dep "slyphon-log4j", "= 1.2.15"
+ dep "slyphon-zookeeper_jar", "= 3.3.5"
+ end
+ gem "slyphon-log4j", "1.2.15"
+ gem "slyphon-zookeeper_jar", "3.3.5", "java"
+ end
+
+ dep "zookeeper"
+ platforms "java", "ruby", "universal-java-11"
+
+ should_resolve_as %w[zookeeper-1.4.11 zookeeper-1.4.11-java slyphon-log4j-1.2.15 slyphon-zookeeper_jar-3.3.5-java]
+ end
+
+ it "takes the latest ruby gem, even if an older platform specific version is available" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0"
+ end
+ dep "foo"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "takes the ruby version if the platform version is incompatible" do
+ @index = build_index do
+ gem "bar", "1.0.0"
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt" do
+ dep "bar", "< 1"
+ end
+ end
+ dep "foo"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.0.0]
+ end
+
+ it "prefers the platform specific gem to the ruby version" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ end
+ dep "foo"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.0.0-x64-mingw-ucrt]
+ end
+
+ describe "on a linux platform" do
+ # Ruby's platform is *-linux => platform's libc is glibc, so not musl
+ # Ruby's platform is *-linux-musl => platform's libc is musl, so not glibc
+ # Gem's platform is *-linux => gem is glibc + maybe musl compatible
+ # Gem's platform is *-linux-musl => gem is musl compatible but not glibc
+
+ it "favors the platform version-specific gem on a version-specifying linux platform" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux"
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux-musl"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux-musl]
+ end
+
+ it "favors the version-less gem over the version-specific gem on a gnu linux platform" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux"
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ end
+
+ it "ignores the platform version-specific gem on a gnu linux platform" do
+ @index = build_index do
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux"
+
+ should_not_resolve
+ end
+
+ it "falls back to the platform version-less gem on a linux platform with a version" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux"
+ end
+ dep "foo"
+ platforms "x86_64-linux-musl"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ end
+
+ it "falls back to the ruby platform gem on a gnu linux platform when only a version-specifying gem is available" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x86_64-linux-musl"
+ end
+ dep "foo"
+ platforms "x86_64-linux"
+
+ should_resolve_as %w[foo-1.0.0]
+ end
+
+ it "falls back to the platform version-less gem on a version-specifying linux platform and no ruby platform gem is available" do
+ @index = build_index do
+ gem "foo", "1.0.0", "x86_64-linux"
+ end
+ dep "foo"
+ platforms "x86_64-linux-musl"
+
+ should_resolve_as %w[foo-1.0.0-x86_64-linux]
+ end
+ end
+
+ context "when the platform specific gem doesn't match the required_ruby_version" do
+ before do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0"
+ gem "foo", "1.1.0", "x64-mingw-ucrt" do |s|
+ s.required_ruby_version = [">= 2.0", "< 2.4"]
+ end
+ gem "Ruby\0", "2.5.1"
+ end
+ dep "Ruby\0", "2.5.1"
+ platforms "x64-mingw-ucrt"
+ end
+
+ it "takes the latest ruby gem" do
+ dep "foo"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "takes the latest ruby gem, even if requirement does not match previous versions with the same ruby requirement" do
+ dep "foo", "1.1.0"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+ end
+
+ it "takes the latest ruby gem with required_ruby_version if the platform specific gem doesn't match the required_ruby_version" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0" do |s|
+ s.required_ruby_version = [">= 2.0"]
+ end
+ gem "foo", "1.1.0", "x64-mingw-ucrt" do |s|
+ s.required_ruby_version = [">= 2.0", "< 2.4"]
+ end
+ gem "Ruby\0", "2.5.1"
+ end
+ dep "foo"
+ dep "Ruby\0", "2.5.1"
+ platforms "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "takes the latest ruby gem if the platform specific gem doesn't match the required_ruby_version with multiple platforms" do
+ @index = build_index do
+ gem "foo", "1.0.0"
+ gem "foo", "1.0.0", "x64-mingw-ucrt"
+ gem "foo", "1.1.0" do |s|
+ s.required_ruby_version = [">= 2.0"]
+ end
+ gem "foo", "1.1.0", "x64-mingw-ucrt" do |s|
+ s.required_ruby_version = [">= 2.0", "< 2.4"]
+ end
+ gem "Ruby\0", "2.5.1"
+ end
+ dep "foo"
+ dep "Ruby\0", "2.5.1"
+ platforms "x86_64-linux", "x64-mingw-ucrt"
+
+ should_resolve_as %w[foo-1.1.0]
+ end
+
+ it "includes gems needed for at least one platform" do
+ @index = build_index do
+ gem "empyrean", "0.1.0"
+ gem "coderay", "1.1.2"
+ gem "method_source", "0.9.0"
+
+ gem "spoon", "0.0.6" do
+ dep "ffi", ">= 0"
+ end
+
+ gem "pry", "0.11.3", "java" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ dep "spoon", "~> 0.0"
+ end
+
+ gem "pry", "0.11.3" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ end
+
+ gem "ffi", "1.9.23", "java"
+ gem "ffi", "1.9.23"
+
+ gem "extra", "1.0.0" do
+ dep "ffi", ">= 0"
+ end
+ end
+
+ dep "empyrean", "0.1.0"
+ dep "pry"
+ dep "extra"
+
+ platforms "ruby", "java"
+
+ should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 extra-1.0.0 ffi-1.9.23 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6]
+ end
+
+ it "includes gems needed for at least one platform even when the platform specific requirement is processed earlier than the generic requirement" do
+ @index = build_index do
+ gem "empyrean", "0.1.0"
+ gem "coderay", "1.1.2"
+ gem "method_source", "0.9.0"
+
+ gem "spoon", "0.0.6" do
+ dep "ffi", ">= 0"
+ end
+
+ gem "pry", "0.11.3", "java" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ dep "spoon", "~> 0.0"
+ end
+
+ gem "pry", "0.11.3" do
+ dep "coderay", "~> 1.1.0"
+ dep "method_source", "~> 0.9.0"
+ end
+
+ gem "ffi", "1.9.23", "java"
+ gem "ffi", "1.9.23"
+
+ gem "extra", "1.0.0" do
+ dep "extra2", ">= 0"
+ end
+
+ gem "extra2", "1.0.0" do
+ dep "extra3", ">= 0"
+ end
+
+ gem "extra3", "1.0.0" do
+ dep "ffi", ">= 0"
+ end
+ end
+
+ dep "empyrean", "0.1.0"
+ dep "pry"
+ dep "extra"
+
+ platforms "ruby", "java"
+
+ should_resolve_as %w[coderay-1.1.2 empyrean-0.1.0 extra-1.0.0 extra2-1.0.0 extra3-1.0.0 ffi-1.9.23 ffi-1.9.23-java method_source-0.9.0 pry-0.11.3 pry-0.11.3-java spoon-0.0.6]
+ end
+
+ it "properly adds platforms when platform requirements come from different dependencies" do
+ @index = build_index do
+ gem "ffi", "1.9.14"
+ gem "ffi", "1.9.14", "universal-mingw32"
+
+ gem "gssapi", "0.1"
+ gem "gssapi", "0.2"
+ gem "gssapi", "0.3"
+ gem "gssapi", "1.2.0" do
+ dep "ffi", ">= 1.0.1"
+ end
+
+ gem "mixlib-shellout", "2.2.6"
+ gem "mixlib-shellout", "2.2.6", "universal-mingw32" do
+ dep "win32-process", "~> 0.8.2"
+ end
+
+ # we need all these versions to get the sorting the same as it would be
+ # pulling from rubygems.org
+ %w[0.8.3 0.8.2 0.8.1 0.8.0].each do |v|
+ gem "win32-process", v do
+ dep "ffi", ">= 1.0.0"
+ end
+ end
+ end
+
+ dep "mixlib-shellout"
+ dep "gssapi"
+
+ platforms "universal-mingw32", "ruby"
+
+ should_resolve_as %w[ffi-1.9.14 ffi-1.9.14-universal-mingw32 gssapi-1.2.0 mixlib-shellout-2.2.6 mixlib-shellout-2.2.6-universal-mingw32 win32-process-0.8.3]
+ end
+
+ describe "with mingw32" do
+ before :each do
+ @index = build_index do
+ platforms "mingw32 mswin32 x64-mingw-ucrt" do |platform|
+ gem "thin", "1.2.7", platform
+ end
+ gem "win32-api", "1.5.1", "universal-mingw32"
+ end
+ end
+
+ it "finds mswin gems" do
+ # win32 is hardcoded to get CPU x86 in rubygems
+ platforms "mswin32"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-x86-mswin32]
+ end
+
+ it "finds mingw gems" do
+ # mingw is _not_ hardcoded to add CPU x86 in rubygems
+ platforms "x86-mingw32"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-mingw32]
+ end
+
+ it "finds x64-mingw-ucrt gems" do
+ platforms "x64-mingw-ucrt"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-x64-mingw-ucrt]
+ end
+
+ it "finds universal-mingw gems on x86-mingw" do
+ platform "x86-mingw32"
+ dep "win32-api"
+ should_resolve_as %w[win32-api-1.5.1-universal-mingw32]
+ end
+
+ it "finds universal-mingw gems on x64-mingw" do
+ platform "x64-mingw-ucrt"
+ dep "win32-api"
+ should_resolve_as %w[win32-api-1.5.1-universal-mingw32]
+ end
+
+ it "finds x64-mingw-ucrt gems" do
+ platforms "x64-mingw-ucrt"
+ dep "thin"
+ should_resolve_as %w[thin-1.2.7-x64-mingw-ucrt]
+ end
+
+ it "finds universal-mingw gems on x64-mingw-ucrt" do
+ platform "x64-mingw-ucrt"
+ dep "win32-api"
+ should_resolve_as %w[win32-api-1.5.1-universal-mingw32]
+ end
+ end
+
+ describe "with conflicting cases" do
+ before :each do
+ @index = build_index do
+ gem "foo", "1.0.0" do
+ dep "bar", ">= 0"
+ end
+
+ gem "bar", "1.0.0" do
+ dep "baz", "~> 1.0.0"
+ end
+
+ gem "bar", "1.0.0", "java" do
+ dep "baz", " ~> 1.1.0"
+ end
+
+ gem "baz", %w[1.0.0 1.1.0 1.2.0]
+ end
+ end
+
+ it "takes the ruby version as fallback" do
+ platforms "ruby", "java"
+ dep "foo"
+
+ should_resolve_as %w[bar-1.0.0 baz-1.0.0 foo-1.0.0]
+ end
+ end
+end
diff --git a/spec/bundler/runtime/env_helpers_spec.rb b/spec/bundler/runtime/env_helpers_spec.rb
new file mode 100644
index 0000000000..c4ebdd1fd2
--- /dev/null
+++ b/spec/bundler/runtime/env_helpers_spec.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+RSpec.describe "env helpers" do
+ def bundle_exec_ruby(args, options = {})
+ build_bundler_context options.dup
+ bundle "exec '#{Gem.ruby}' #{args}", options
+ end
+
+ def build_bundler_context(options = {})
+ bundle "config set path vendor/bundle", options.dup
+ gemfile "source 'https://gem.repo1'"
+ bundle "install", options
+ end
+
+ def run_bundler_script(env, script)
+ system(env, "ruby", "-I#{lib_dir}", "-rbundler", script.to_s)
+ end
+
+ describe "Bundler.original_env" do
+ it "should return the PATH present before bundle was activated" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.original_env["PATH"]
+ RUBY
+ path = `getconf PATH`.strip + "#{File::PATH_SEPARATOR}/foo"
+ with_path_as(path) do
+ bundle_exec_ruby(bundled_app("source.rb").to_s)
+ expect(stdboth).to eq(path)
+ end
+ end
+
+ it "should return the GEM_PATH present before bundle was activated" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.original_env['GEM_PATH']
+ RUBY
+ gem_path = ENV["GEM_PATH"] + "#{File::PATH_SEPARATOR}/foo"
+ with_gem_path_as(gem_path) do
+ bundle_exec_ruby(bundled_app("source.rb").to_s)
+ expect(stdboth).to eq(gem_path)
+ end
+ end
+
+ it "works with nested bundle exec invocations", :ruby_repo do
+ create_file("exe.rb", <<-'RUBY')
+ count = ARGV.first.to_i
+ exit if count < 0
+ STDERR.puts "#{count} #{ENV["PATH"].end_with?("#{File::PATH_SEPARATOR}/foo")}"
+ if count == 2
+ ENV["PATH"] = "#{ENV["PATH"]}#{File::PATH_SEPARATOR}/foo"
+ end
+ exec(Gem.ruby, __FILE__, (count - 1).to_s)
+ RUBY
+ path = `getconf PATH`.strip + File::PATH_SEPARATOR + File.dirname(Gem.ruby)
+ with_path_as(path) do
+ build_bundler_context
+ bundle_exec_ruby("#{bundled_app("exe.rb")} 2")
+ end
+ expect(err).to eq <<-EOS.strip
+2 false
+1 true
+0 true
+ EOS
+ end
+
+ it "removes variables that bundler added", :ruby_repo do
+ original = ruby('puts ENV.to_a.map {|e| e.join("=") }.sort.join("\n")', artifice: "fail")
+ create_file("source.rb", <<-RUBY)
+ puts Bundler.original_env.to_a.map {|e| e.join("=") }.sort.join("\n")
+ RUBY
+ bundle_exec_ruby bundled_app("source.rb"), artifice: "fail"
+ expect(out).to eq original
+ end
+ end
+
+ describe "Bundler.unbundled_env" do
+ it "should delete BUNDLE_PATH" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env.has_key?('BUNDLE_PATH')
+ RUBY
+ ENV["BUNDLE_PATH"] = "./foo"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include "false"
+ end
+
+ it "should remove absolute path to 'bundler/setup' from RUBYOPT even if it was present in original env" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['RUBYOPT']
+ RUBY
+ setup_require = "-r#{lib_dir}/bundler/setup"
+ ENV["BUNDLER_ORIG_RUBYOPT"] = "-W2 #{setup_require} #{ENV["RUBYOPT"]}"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).not_to include(setup_require)
+ end
+
+ it "should remove relative path to 'bundler/setup' from RUBYOPT even if it was present in original env" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['RUBYOPT']
+ RUBY
+ ENV["BUNDLER_ORIG_RUBYOPT"] = "-W2 -rbundler/setup #{ENV["RUBYOPT"]}"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).not_to include("-rbundler/setup")
+ end
+
+ it "should delete BUNDLER_SETUP even if it was present in original env" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env.has_key?('BUNDLER_SETUP')
+ RUBY
+ ENV["BUNDLER_ORIG_BUNDLER_SETUP"] = system_gem_path("gems/bundler-#{Bundler::VERSION}/lib/bundler/setup").to_s
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include "false"
+ end
+
+ it "should restore RUBYLIB", :ruby_repo do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['RUBYLIB']
+ RUBY
+ ENV["RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo"
+ ENV["BUNDLER_ORIG_RUBYLIB"] = lib_dir.to_s + File::PATH_SEPARATOR + "/foo-original"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include("/foo-original")
+ end
+
+ it "should restore the original MANPATH" do
+ create_file("source.rb", <<-RUBY)
+ print Bundler.unbundled_env['MANPATH']
+ RUBY
+ ENV["MANPATH"] = "/foo"
+ ENV["BUNDLER_ORIG_MANPATH"] = "/foo-original"
+ bundle_exec_ruby bundled_app("source.rb")
+ expect(stdboth).to include("/foo-original")
+ end
+ end
+
+ describe "Bundler.with_original_env" do
+ it "should set ENV to original_env in the block" do
+ expected = Bundler.original_env
+ actual = Bundler.with_original_env { ENV.to_hash }
+ expect(actual).to eq(expected)
+ end
+
+ it "should restore the environment after execution" do
+ Bundler.with_original_env do
+ ENV["FOO"] = "hello"
+ end
+
+ expect(ENV).not_to have_key("FOO")
+ end
+ end
+
+ describe "Bundler.with_unbundled_env" do
+ it "should set ENV to unbundled_env in the block" do
+ expected = Bundler.unbundled_env
+ actual = Bundler.with_unbundled_env { ENV.to_hash }
+ expect(actual).to eq(expected)
+ end
+
+ it "should restore the environment after execution" do
+ Bundler.with_unbundled_env do
+ ENV["FOO"] = "hello"
+ end
+
+ expect(ENV).not_to have_key("FOO")
+ end
+ end
+
+ describe "Bundler.original_system" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Bundler.original_system("ruby", "-e", "exit(42) if ENV['BUNDLE_FOO'] == 'bar'")
+
+ exit $?.exitstatus
+ RUBY
+ end
+
+ it "runs system inside with_original_env" do
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(42)
+ end
+ end
+
+ describe "Bundler.unbundled_system" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Bundler.unbundled_system("ruby", "-e", "exit(42) unless ENV['BUNDLE_FOO'] == 'bar'")
+
+ exit $?.exitstatus
+ RUBY
+ end
+
+ it "runs system inside with_unbundled_env" do
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(42)
+ end
+ end
+
+ describe "Bundler.original_exec" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Process.fork do
+ exit Bundler.original_exec(%(test "\$BUNDLE_FOO" = "bar"))
+ end
+
+ _, status = Process.wait2
+
+ exit(status.exitstatus)
+ RUBY
+ end
+
+ it "runs exec inside with_original_env" do
+ skip "Fork not implemented" if Gem.win_platform?
+
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(0)
+ end
+ end
+
+ describe "Bundler.unbundled_exec" do
+ before do
+ create_file("source.rb", <<-'RUBY')
+ Process.fork do
+ exit Bundler.unbundled_exec(%(test "\$BUNDLE_FOO" = "bar"))
+ end
+
+ _, status = Process.wait2
+
+ exit(status.exitstatus)
+ RUBY
+ end
+
+ it "runs exec inside with_unbundled_env" do
+ skip "Fork not implemented" if Gem.win_platform?
+
+ run_bundler_script({ "BUNDLE_FOO" => "bar" }, bundled_app("source.rb"))
+ expect($?.exitstatus).to eq(1)
+ end
+ end
+end
diff --git a/spec/bundler/runtime/executable_spec.rb b/spec/bundler/runtime/executable_spec.rb
new file mode 100644
index 0000000000..89cee21b00
--- /dev/null
+++ b/spec/bundler/runtime/executable_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+RSpec.describe "Running bin/* commands" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "runs the bundled command when in the bundle" do
+ bundle "binstubs myrack"
+
+ build_gem "myrack", "2.0", to_system: true do |s|
+ s.executables = "myrackup"
+ end
+
+ gembin "myrackup"
+ expect(out).to eq("1.0.0")
+ end
+
+ it "allows the location of the gem stubs to be configured" do
+ bundle_config "bin gbin"
+ bundle "binstubs myrack"
+
+ expect(bundled_app("bin")).not_to exist
+ expect(bundled_app("gbin/myrackup")).to exist
+
+ gembin bundled_app("gbin/myrackup")
+ expect(out).to eq("1.0.0")
+ end
+
+ it "allows absolute paths as a specification of where to install bin stubs" do
+ bundle_config "bin #{tmp("bin")}"
+ bundle "binstubs myrack"
+
+ gembin tmp("bin/myrackup")
+ expect(out).to eq("1.0.0")
+ end
+
+ it "uses the default ruby install name when shebang is not specified" do
+ bundle "binstubs myrack"
+ expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env #{RbConfig::CONFIG["ruby_install_name"]}\n")
+ end
+
+ it "allows the name of the shebang executable to be specified" do
+ bundle "binstubs myrack", shebang: "ruby-foo"
+ expect(File.readlines(bundled_app("bin/myrackup")).first).to eq("#!/usr/bin/env ruby-foo\n")
+ end
+
+ it "runs the bundled command when out of the bundle" do
+ bundle "binstubs myrack"
+
+ build_gem "myrack", "2.0", to_system: true do |s|
+ s.executables = "myrackup"
+ end
+
+ gembin "myrackup", dir: tmp
+ expect(out).to eq("1.0.0")
+ end
+
+ it "works with gems in path" do
+ build_lib "myrack", path: lib_path("myrack") do |s|
+ s.executables = "myrackup"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :path => "#{lib_path("myrack")}"
+ G
+
+ bundle "binstubs myrack"
+
+ build_gem "myrack", "2.0", to_system: true do |s|
+ s.executables = "myrackup"
+ end
+
+ gembin "myrackup"
+ expect(out).to eq("1.0")
+ end
+
+ it "does not create a bundle binstub" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "bundler"
+ G
+
+ bundle "binstubs bundler"
+
+ expect(bundled_app("bin/bundle")).not_to exist
+
+ expect(err).to include("Bundler itself does not use binstubs because its version is selected by RubyGems")
+ end
+
+ it "does not generate bin stubs if the option was not specified" do
+ bundle "install"
+
+ expect(bundled_app("bin/myrackup")).not_to exist
+ end
+
+ it "rewrites bins on binstubs with --force option" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ create_file("bin/myrackup", "OMG")
+
+ bundle "binstubs myrack", { force: true }
+
+ expect(bundled_app("bin/myrackup").read.strip).to_not eq("OMG")
+ end
+
+ it "use BUNDLE_GEMFILE gemfile for binstub" do
+ # context with bin/bundler w/ default Gemfile
+ bundle "binstubs bundler"
+
+ # generate other Gemfile with executable gem
+ build_repo2 do
+ build_gem("bindir") {|s| s.executables = "foo" }
+ end
+
+ gemfile("OtherGemfile", <<-G)
+ source "https://gem.repo2"
+ gem 'bindir'
+ G
+
+ # generate binstub for executable from non default Gemfile (other then bin/bundler version)
+ ENV["BUNDLE_GEMFILE"] = "OtherGemfile"
+ bundle "install"
+ bundle "binstubs bindir"
+
+ # remove user settings
+ ENV["BUNDLE_GEMFILE"] = nil
+
+ # run binstub for non default Gemfile
+ gembin "foo"
+
+ expect(out).to eq("1.0")
+ end
+end
diff --git a/spec/bundler/runtime/gem_tasks_spec.rb b/spec/bundler/runtime/gem_tasks_spec.rb
new file mode 100644
index 0000000000..b855142e60
--- /dev/null
+++ b/spec/bundler/runtime/gem_tasks_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+RSpec.describe "require 'bundler/gem_tasks'" do
+ let(:define_local_gem_using_gem_tasks) do
+ bundled_app("foo.gemspec").open("w") do |f|
+ f.write <<-GEMSPEC
+ Gem::Specification.new do |s|
+ s.name = "foo"
+ s.version = "1.0"
+ s.summary = "dummy"
+ s.author = "Perry Mason"
+ end
+ GEMSPEC
+ end
+
+ bundled_app("Rakefile").open("w") do |f|
+ f.write <<-RAKEFILE
+ require "bundler/gem_tasks"
+ RAKEFILE
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rake"
+ G
+ end
+
+ let(:define_local_gem_with_extensions_using_gem_tasks_and_gemspec_dsl) do
+ bundled_app("foo.gemspec").open("w") do |f|
+ f.write <<-GEMSPEC
+ Gem::Specification.new do |s|
+ s.name = "foo"
+ s.version = "1.0"
+ s.summary = "dummy"
+ s.author = "Perry Mason"
+ s.extensions = "ext/extconf.rb"
+ end
+ GEMSPEC
+ end
+
+ bundled_app("Rakefile").open("w") do |f|
+ f.write <<-RAKEFILE
+ require "bundler/gem_tasks"
+ RAKEFILE
+ end
+
+ Dir.mkdir bundled_app("ext")
+
+ bundled_app("ext/extconf.rb").open("w") do |f|
+ f.write <<-EXTCONF
+ require "mkmf"
+ File.write("Makefile", dummy_makefile($srcdir).join)
+ EXTCONF
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gemspec
+
+ gem "rake"
+ G
+ end
+
+ it "includes the relevant tasks" do
+ define_local_gem_using_gem_tasks
+
+ in_bundled_app "rake -T"
+
+ expect(err).to be_empty
+ expected_tasks = [
+ "rake build",
+ "rake clean",
+ "rake clobber",
+ "rake install",
+ "rake release[remote]",
+ ]
+ tasks = out.lines.to_a.map {|s| s.split("#").first.strip }
+ expect(tasks & expected_tasks).to eq(expected_tasks)
+ end
+
+ it "defines a working `rake install` task", :ruby_repo do
+ define_local_gem_using_gem_tasks
+
+ in_bundled_app "rake install"
+
+ expect(err).to be_empty
+
+ bundle "exec rake install"
+
+ expect(err).to be_empty
+ end
+
+ it "defines a working `rake install` task for local gems with extensions", :ruby_repo do
+ define_local_gem_with_extensions_using_gem_tasks_and_gemspec_dsl
+
+ bundle "exec rake install"
+
+ expect(err).to be_empty
+ end
+
+ context "rake build when path has spaces", :ruby_repo do
+ before do
+ define_local_gem_using_gem_tasks
+
+ spaced_bundled_app = tmp("bundled app")
+ FileUtils.cp_r bundled_app, spaced_bundled_app
+ bundle "exec rake build", dir: spaced_bundled_app
+ end
+
+ it "still runs successfully" do
+ expect(err).to be_empty
+ end
+ end
+
+ context "rake build when path has brackets", :ruby_repo do
+ before do
+ define_local_gem_using_gem_tasks
+
+ bracketed_bundled_app = tmp("bundled[app")
+ FileUtils.cp_r bundled_app, bracketed_bundled_app
+ bundle "exec rake build", dir: bracketed_bundled_app
+ end
+
+ it "still runs successfully" do
+ expect(err).to be_empty
+ end
+ end
+
+ context "bundle path configured locally" do
+ before do
+ define_local_gem_using_gem_tasks
+
+ bundle_config "path vendor/bundle"
+ end
+
+ it "works", :ruby_repo do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "rake"
+ G
+
+ bundle "exec rake -T"
+
+ expect(err).to be_empty
+ end
+ end
+
+ it "adds 'pkg' to rake/clean's CLOBBER" do
+ define_local_gem_using_gem_tasks
+
+ in_bundled_app %(rake -e 'load "Rakefile"; puts CLOBBER.inspect')
+
+ expect(out).to eq '["pkg"]'
+ end
+end
diff --git a/spec/bundler/runtime/inline_spec.rb b/spec/bundler/runtime/inline_spec.rb
new file mode 100644
index 0000000000..c6f9bbdbd7
--- /dev/null
+++ b/spec/bundler/runtime/inline_spec.rb
@@ -0,0 +1,751 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundler/inline#gemfile" do
+ def script(code, options = {})
+ options[:artifice] ||= "compact_index"
+ options[:env] ||= { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ ruby("require 'bundler/inline'\n\n" + code, options)
+ end
+
+ before :each do
+ build_lib "one", "1.0.0" do |s|
+ s.write "lib/baz.rb", "puts 'baz'"
+ s.write "lib/qux.rb", "puts 'qux'"
+ end
+
+ build_lib "two", "1.0.0" do |s|
+ s.write "lib/two.rb", "puts 'two'"
+ s.add_dependency "three", "= 1.0.0"
+ end
+
+ build_lib "three", "1.0.0" do |s|
+ s.write "lib/three.rb", "puts 'three'"
+ s.add_dependency "seven", "= 1.0.0"
+ end
+
+ build_lib "four", "1.0.0" do |s|
+ s.write "lib/four.rb", "puts 'four'"
+ end
+
+ build_lib "five", "1.0.0", no_default: true do |s|
+ s.write "lib/mofive.rb", "puts 'five'"
+ end
+
+ build_lib "six", "1.0.0" do |s|
+ s.write "lib/six.rb", "puts 'six'"
+ end
+
+ build_lib "seven", "1.0.0" do |s|
+ s.write "lib/seven.rb", "puts 'seven'"
+ end
+
+ build_lib "eight", "1.0.0" do |s|
+ s.write "lib/eight.rb", "puts 'eight'"
+ end
+ end
+
+ it "requires the gems" do
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two"
+ end
+ end
+ RUBY
+
+ expect(out).to eq("two")
+
+ script <<-RUBY, raise_on_error: false
+ gemfile do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "eleven"
+ end
+ end
+
+ puts "success"
+ RUBY
+
+ expect(err).to include "Could not find gem 'eleven'"
+ expect(out).not_to include "success"
+
+ script <<-RUBY
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(out).to include("Myrack's post install message")
+
+ script <<-RUBY, artifice: "endpoint"
+ gemfile(true) do
+ source "https://notaserver.test"
+ gem "activesupport", :require => true
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ err_lines = err.split("\n")
+ err_lines.reject! {|line| line =~ /\.rb:\d+: warning: / }
+ expect(err_lines).to be_empty
+ end
+
+ it "lets me use my own ui object" do
+ script <<-RUBY, artifice: "endpoint"
+ require 'bundler'
+ class MyBundlerUI < Bundler::UI::Shell
+ def confirm(msg, newline = nil)
+ puts "CONFIRMED!"
+ end
+ end
+ my_ui = MyBundlerUI.new
+ my_ui.level = "confirm"
+ gemfile(true, :ui => my_ui) do
+ source "https://notaserver.test"
+ gem "activesupport", :require => true
+ end
+ RUBY
+
+ expect(out).to eq("CONFIRMED!\nCONFIRMED!")
+ end
+
+ it "has an option for quiet installation" do
+ script <<-RUBY, artifice: "endpoint"
+ require 'bundler/inline'
+
+ gemfile(true, :quiet => true) do
+ source "https://notaserver.test"
+ gem "activesupport", :require => true
+ end
+ RUBY
+
+ expect(out).to be_empty
+ end
+
+ it "raises an exception if passed unknown arguments" do
+ script <<-RUBY, raise_on_error: false
+ gemfile(true, :arglebargle => true) do
+ path "#{lib_path}"
+ gem "two"
+ end
+
+ puts "success"
+ RUBY
+ expect(err).to include "Unknown options: arglebargle"
+ expect(out).not_to include "success"
+ end
+
+ it "does not mutate the option argument" do
+ script <<-RUBY
+ require 'bundler'
+ options = { :ui => Bundler::UI::Shell.new }
+ gemfile(false, options) do
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two"
+ end
+ end
+ puts "OKAY" if options.key?(:ui)
+ RUBY
+
+ expect(out).to match("OKAY")
+ end
+
+ it "installs quietly if necessary when the install option is not set" do
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs subdependencies quietly if necessary when the install option is not set" do
+ build_repo4 do
+ build_gem "myrack" do |s|
+ s.add_dependency "myrackdep"
+ end
+
+ build_gem "myrackdep", "1.0.0"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ gemfile do
+ source "https://gem.repo4"
+ gem "myrack"
+ end
+
+ require "myrackdep"
+ puts MYRACKDEP
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs subdependencies quietly if necessary when the install option is not set, and multiple sources used" do
+ build_repo4 do
+ build_gem "myrack" do |s|
+ s.add_dependency "myrackdep"
+ end
+
+ build_gem "myrackdep", "1.0.0"
+ end
+
+ script <<-RUBY, artifice: "compact_index_extra_api"
+ gemfile do
+ source "https://test.repo"
+ source "https://test.repo/extra" do
+ gem "myrack"
+ end
+ end
+
+ require "myrackdep"
+ puts MYRACKDEP
+ RUBY
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs quietly from git if necessary when the install option is not set" do
+ build_git "foo", "1.0.0"
+ baz_ref = build_git("baz", "2.0.0").ref_for("HEAD")
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump}
+ gem "baz", :git => #{lib_path("baz-2.0.0").to_s.dump}, :ref => #{baz_ref.dump}
+ end
+
+ puts FOO
+ puts BAZ
+ RUBY
+
+ expect(out).to eq("1.0.0\n2.0.0")
+ expect(err).to be_empty
+ end
+
+ it "allows calling gemfile twice" do
+ script <<-RUBY
+ gemfile do
+ path "#{lib_path}" do
+ source "https://gem.repo1"
+ gem "two"
+ end
+ end
+
+ gemfile do
+ path "#{lib_path}" do
+ source "https://gem.repo1"
+ gem "four"
+ end
+ end
+ RUBY
+
+ expect(out).to eq("two\nfour")
+ expect(err).to be_empty
+ end
+
+ it "doesn't reinstall already installed gems" do
+ system_gems "myrack-1.0.0"
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "activesupport"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ expect(out).not_to include("Installing myrack")
+ expect(err).to be_empty
+ end
+
+ it "installs gems in later gemfile calls" do
+ system_gems "myrack-1.0.0"
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "activesupport"
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ expect(out).not_to include("Installing myrack")
+ expect(err).to be_empty
+ end
+
+ it "doesn't reinstall already installed gems in later gemfile calls" do
+ system_gems "myrack-1.0.0"
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "activesupport"
+ end
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(out).to include("Installing activesupport")
+ expect(out).not_to include("Installing myrack")
+ expect(err).to be_empty
+ end
+
+ it "installs gems with native extensions in later gemfile calls" do
+ system_gems "myrack-1.0.0"
+
+ build_git "foo" do |s|
+ s.add_dependency "rake"
+ s.extensions << "Rakefile"
+ s.write "Rakefile", <<-RUBY
+ task :default do
+ path = File.expand_path("lib", __dir__)
+ FileUtils.mkdir_p(path)
+ File.open("\#{path}/foo.rb", "w") do |f|
+ f.puts "FOO = 'YES'"
+ end
+ end
+ RUBY
+ end
+
+ script <<-RUBY
+ require 'bundler'
+ ui = Bundler::UI::Shell.new
+ ui.level = "confirm"
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ gemfile(true, ui: ui) do
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ end
+
+ require 'foo'
+ puts FOO
+ puts $:.grep(/ext/)
+ RUBY
+
+ expect(out).to include("YES")
+ expect(out).to include(Pathname.glob(default_bundle_path("bundler/gems/extensions/**/foo-1.0-*")).first.to_s)
+ expect(err).to be_empty
+ end
+
+ it "installs inline gems when a Gemfile.lock is present" do
+ gemfile <<-G
+ source "https://notaserver.test"
+ gem "rake"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (11.3.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "does not leak Gemfile.lock versions to the installation output" do
+ gemfile <<-G
+ source "https://notaserver.test"
+ gem "rake"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (11.3.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ rake
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ script <<-RUBY
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "rake", "#{rake_version}"
+ end
+ RUBY
+
+ expect(out).to include("Installing rake #{rake_version}")
+ expect(out).not_to include("was 11.3.0")
+ expect(err).to be_empty
+ end
+
+ it "installs inline gems when frozen is set" do
+ script <<-RUBY, env: { "BUNDLE_FROZEN" => "true", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(last_command.stderr).to be_empty
+ end
+
+ it "installs inline gems when deployment is set" do
+ script <<-RUBY, env: { "BUNDLE_DEPLOYMENT" => "true", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(last_command.stderr).to be_empty
+ end
+
+ it "installs inline gems when BUNDLE_GEMFILE is set to an empty string" do
+ ENV["BUNDLE_GEMFILE"] = ""
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "installs inline gems when BUNDLE_BIN is set" do
+ ENV["BUNDLE_BIN"] = "/usr/local/bundle/bin"
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack" # has the myrackup executable
+ end
+
+ puts MYRACK
+ RUBY
+ expect(last_command).to be_success
+ expect(out).to eq "1.0.0"
+ end
+
+ context "when BUNDLE_PATH is set" do
+ it "installs inline gems to the system path regardless" do
+ script <<-RUBY, env: { "BUNDLE_PATH" => "./vendor/inline", "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+ expect(last_command).to be_success
+ expect(system_gem_path("gems/myrack-1.0.0")).to exist
+ end
+ end
+
+ it "skips platform warnings" do
+ bundle_config "force_ruby_platform true"
+
+ script <<-RUBY
+ gemfile(true) do
+ source "https://gem.repo1"
+ gem "myrack", platform: :jruby
+ end
+ RUBY
+
+ expect(err).to be_empty
+ end
+
+ it "still installs if the application has `bundle package` no_install config set" do
+ bundle_config "no_install true"
+
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+ RUBY
+
+ expect(last_command).to be_success
+ expect(system_gem_path("gems/myrack-1.0.0")).to exist
+ end
+
+ it "preserves previous BUNDLE_GEMFILE value" do
+ ENV["BUNDLE_GEMFILE"] = ""
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts "BUNDLE_GEMFILE is empty" if ENV["BUNDLE_GEMFILE"].empty?
+ system("#{Gem.ruby} -w -e '42'") # this should see original value of BUNDLE_GEMFILE
+ exit $?.exitstatus
+ RUBY
+
+ expect(last_command).to be_success
+ expect(out).to include("BUNDLE_GEMFILE is empty")
+ end
+
+ it "resets BUNDLE_GEMFILE to the empty string if it wasn't set previously" do
+ ENV["BUNDLE_GEMFILE"] = nil
+ script <<-RUBY
+ gemfile do
+ source "https://gem.repo1"
+ gem "myrack"
+ end
+
+ puts "BUNDLE_GEMFILE is empty" if ENV["BUNDLE_GEMFILE"].empty?
+ system("#{Gem.ruby} -w -e '42'") # this should see original value of BUNDLE_GEMFILE
+ exit $?.exitstatus
+ RUBY
+
+ expect(last_command).to be_success
+ expect(out).to include("BUNDLE_GEMFILE is empty")
+ end
+
+ it "does not error out if library requires optional dependencies" do
+ Dir.mkdir tmp("path_without_gemfile")
+
+ foo_code = <<~RUBY
+ begin
+ gem "bar"
+ rescue LoadError
+ end
+
+ puts "WIN"
+ RUBY
+
+ build_lib "foo", "1.0.0" do |s|
+ s.write "lib/foo.rb", foo_code
+ end
+
+ script <<-RUBY, dir: tmp("path_without_gemfile"), env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo2.to_s }
+ gemfile do
+ source "https://gem.repo2"
+ path "#{lib_path}" do
+ gem "foo", require: false
+ end
+ end
+
+ require "foo"
+ RUBY
+
+ expect(out).to eq("WIN")
+ expect(err).to be_empty
+ end
+
+ it "does not load default timeout", rubygems: ">= 3.5.0" do
+ default_timeout_version = ruby "gem 'timeout', '< 999999'; require 'timeout'; puts Timeout::VERSION", raise_on_error: false
+ skip "timeout isn't a default gem" if default_timeout_version.empty?
+
+ build_repo4 do
+ build_gem "timeout", "999"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ require "bundler/inline"
+
+ gemfile(true) do
+ source "https://gem.repo4"
+
+ gem "timeout"
+ end
+ RUBY
+
+ expect(out).to include("Installing timeout 999")
+ end
+
+ it "does not upcase ENV" do
+ script <<-RUBY
+ require 'bundler/inline'
+
+ ENV['Test_Variable'] = 'value string'
+ puts("before: \#{ENV.each_key.select { |key| key.match?(/test_variable/i) }}")
+
+ gemfile do
+ source "https://gem.repo1"
+ end
+
+ puts("after: \#{ENV.each_key.select { |key| key.match?(/test_variable/i) }}")
+ RUBY
+
+ expect(out).to include("before: [\"Test_Variable\"]")
+ expect(out).to include("after: [\"Test_Variable\"]")
+ end
+
+ it "does not create a lockfile" do
+ script <<-RUBY
+ require 'bundler/inline'
+
+ gemfile do
+ source "https://gem.repo1"
+ end
+
+ puts Dir.glob("Gemfile.lock")
+ RUBY
+
+ expect(out).to be_empty
+ end
+
+ it "does not reset ENV" do
+ script <<-RUBY
+ require 'bundler/inline'
+
+ gemfile do
+ source "https://gem.repo1"
+
+ ENV['FOO'] = 'bar'
+ end
+
+ puts ENV['FOO']
+ RUBY
+
+ expect(out).to eq("bar")
+ end
+
+ it "does not load specified version of psych and stringio", :ruby_repo do
+ build_repo4 do
+ build_gem "psych", "999"
+ build_gem "stringio", "999"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ require "bundler/inline"
+
+ gemfile(true) do
+ source "https://gem.repo4"
+
+ gem "psych"
+ gem "stringio"
+ end
+ RUBY
+
+ expect(out).to include("Installing psych 999")
+ expect(out).to include("Installing stringio 999")
+ if Gem.respond_to?(:use_psych?) && Gem.use_psych?
+ expect(out).to include("The psych gem was resolved to 999")
+ expect(out).to include("The stringio gem was resolved to 999")
+ end
+ end
+
+ it "installs a conflicting default gem and non-default gems together" do
+ build_repo4 do
+ build_gem "securerandom", "999"
+ build_gem "myrack", "1.0.0"
+ end
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ gemfile(true) do
+ source "https://gem.repo4"
+ gem "securerandom"
+ gem "myrack"
+ end
+
+ puts MYRACK
+ RUBY
+
+ expect(out).to include("Installing securerandom 999")
+ expect(out).to include("Installing myrack 1.0.0")
+ expect(out).to include("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "installs a conflicting default gem alongside git sources" do
+ build_repo4 do
+ build_gem "securerandom", "999"
+ end
+
+ build_git "foo", "1.0.0"
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s }
+ gemfile(true) do
+ source "https://gem.repo4"
+ gem "securerandom"
+ gem "foo", :git => #{lib_path("foo-1.0.0").to_s.dump}
+ end
+
+ puts FOO
+ RUBY
+
+ expect(out).to include("Installing securerandom 999")
+ expect(out).to include("1.0.0")
+ expect(err).to be_empty
+ end
+
+ it "leaves a lockfile in the same directory as the inline script alone" do
+ install_gemfile <<~G
+ source "https://gem.repo1"
+ gem "foo"
+ G
+
+ original_lockfile = lockfile
+
+ script <<-RUBY, env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo1.to_s }
+ require "bundler/inline"
+
+ gemfile(true) do
+ source "https://gem.repo1"
+
+ gem "myrack"
+ end
+ RUBY
+
+ expect(lockfile).to eq(original_lockfile)
+ end
+end
diff --git a/spec/bundler/runtime/load_spec.rb b/spec/bundler/runtime/load_spec.rb
new file mode 100644
index 0000000000..472cde87c5
--- /dev/null
+++ b/spec/bundler/runtime/load_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+RSpec.describe "Bundler.load" do
+ describe "with a gemfile" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app)
+ end
+
+ it "provides a list of the env dependencies" do
+ expect(Bundler.load.dependencies).to have_dep("myrack", ">= 0")
+ end
+
+ it "provides a list of the resolved gems" do
+ expect(Bundler.load.gems).to have_gem("myrack-1.0.0", "bundler-#{Bundler::VERSION}")
+ end
+
+ it "ignores blank BUNDLE_GEMFILEs" do
+ expect do
+ ENV["BUNDLE_GEMFILE"] = ""
+ Bundler.load
+ end.not_to raise_error
+ end
+ end
+
+ describe "with a gems.rb file" do
+ before(:each) do
+ gemfile "gems.rb", <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ bundle :install
+ allow(Bundler::SharedHelpers).to receive(:pwd).and_return(bundled_app)
+ end
+
+ it "provides a list of the env dependencies" do
+ expect(Bundler.load.dependencies).to have_dep("myrack", ">= 0")
+ end
+
+ it "provides a list of the resolved gems" do
+ expect(Bundler.load.gems).to have_gem("myrack-1.0.0", "bundler-#{Bundler::VERSION}")
+ end
+ end
+
+ describe "without a gemfile" do
+ it "raises an exception if the default gemfile is not found" do
+ expect do
+ Bundler.load
+ end.to raise_error(Bundler::GemfileNotFound, /could not locate gemfile/i)
+ end
+
+ it "raises an exception if a specified gemfile is not found" do
+ expect do
+ ENV["BUNDLE_GEMFILE"] = "omg.rb"
+ Bundler.load
+ end.to raise_error(Bundler::GemfileNotFound, /omg\.rb/)
+ end
+
+ it "does not find a Gemfile above the testing directory" do
+ bundler_gemfile = Pathname.new(__dir__).join("../../Gemfile")
+ unless File.exist?(bundler_gemfile)
+ FileUtils.touch(bundler_gemfile)
+ @remove_bundler_gemfile = true
+ end
+ begin
+ expect { Bundler.load }.to raise_error(Bundler::GemfileNotFound)
+ ensure
+ FileUtils.rm_rf bundler_gemfile if @remove_bundler_gemfile
+ end
+ end
+ end
+
+ describe "when called twice" do
+ it "doesn't try to load the runtime twice" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "activesupport", :group => :test
+ G
+
+ ruby <<-RUBY
+ require "bundler"
+ Bundler.setup :default
+ Bundler.require :default
+ puts MYRACK
+ begin
+ require "activesupport"
+ rescue LoadError
+ puts "no activesupport"
+ end
+ RUBY
+
+ expect(out.split("\n")).to eq(["1.0.0", "no activesupport"])
+ end
+ end
+
+ describe "not hurting brittle rubygems" do
+ it "does not inject #source into the generated YAML of the gem specs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activerecord"
+ G
+ allow(Bundler::SharedHelpers).to receive(:find_gemfile).and_return(bundled_app_gemfile)
+ Bundler.load.specs.each do |spec|
+ expect(spec.to_yaml).not_to match(/^\s+source:/)
+ expect(spec.to_yaml).not_to match(/^\s+groups:/)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/runtime/platform_spec.rb b/spec/bundler/runtime/platform_spec.rb
new file mode 100644
index 0000000000..6d96758956
--- /dev/null
+++ b/spec/bundler/runtime/platform_spec.rb
@@ -0,0 +1,468 @@
+# frozen_string_literal: true
+
+RSpec.describe "Bundler.setup with multi platform stuff" do
+ it "raises a friendly error when gems are missing locally" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0)
+
+ PLATFORMS
+ #{local_tag}
+
+ DEPENDENCIES
+ myrack
+ G
+
+ ruby <<-R
+ begin
+ require 'bundler'
+ Bundler.ui.silence { Bundler.setup }
+ rescue Bundler::GemNotFound => e
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "will resolve correctly on the current platform when the lockfile was targeted for a different one" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ nokogiri (1.4.2-java)
+ weakling (= 0.0.3)
+ weakling (0.0.3)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ nokogiri
+ G
+
+ simulate_platform "x86-darwin-10" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2"
+ end
+ end
+
+ it "will keep both platforms when both ruby and a specific ruby platform are locked and the bundle is unlocked" do
+ build_repo4 do
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.add_dependency "mini_portile2", "~> 2.5.0"
+ s.add_dependency "racca", "~> 1.5.2"
+ end
+
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.platform = Bundler.local_platform
+ s.add_dependency "racca", "~> 1.4"
+ end
+
+ build_gem "mini_portile2", "2.5.0"
+ build_gem "racca", "1.5.2"
+ end
+
+ checksums = checksums_section do |c|
+ c.checksum gem_repo4, "mini_portile2", "2.5.0"
+ c.checksum gem_repo4, "nokogiri", "1.11.1"
+ c.checksum gem_repo4, "nokogiri", "1.11.1", Bundler.local_platform
+ c.checksum gem_repo4, "racca", "1.5.2"
+ end
+
+ good_lockfile = <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ mini_portile2 (2.5.0)
+ nokogiri (1.11.1)
+ mini_portile2 (~> 2.5.0)
+ racca (~> 1.5.2)
+ nokogiri (1.11.1-#{Bundler.local_platform})
+ racca (~> 1.4)
+ racca (1.5.2)
+
+ PLATFORMS
+ #{lockfile_platforms("ruby")}
+
+ DEPENDENCIES
+ nokogiri (~> 1.11)
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "nokogiri", "~> 1.11"
+ G
+
+ lockfile good_lockfile
+
+ bundle "update nokogiri"
+
+ expect(lockfile).to eq(good_lockfile)
+ end
+
+ it "will not try to install platform specific gems when they don't match the current ruby if locked only to ruby" do
+ build_repo4 do
+ build_gem "nokogiri", "1.11.1"
+
+ build_gem "nokogiri", "1.11.1" do |s|
+ s.platform = Bundler.local_platform
+ s.required_ruby_version = "< #{Gem.ruby_version}"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "nokogiri"
+ G
+
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ nokogiri (1.11.1)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(out).to include("Fetching nokogiri 1.11.1")
+ expect(the_bundle).to include_gems "nokogiri 1.11.1"
+ expect(the_bundle).not_to include_gems "nokogiri 1.11.1 #{Bundler.local_platform}"
+ end
+
+ it "will use the java platform if both generic java and generic ruby platforms are locked", :jruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ G
+
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ nokogiri (1.4.2)
+ nokogiri (1.4.2-java)
+ weakling (>= 0.0.3)
+ weakling (0.0.3)
+
+ PLATFORMS
+ java
+ ruby
+
+ DEPENDENCIES
+ nokogiri
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+
+ bundle "install"
+
+ expect(out).to include("Fetching nokogiri 1.4.2 (java)")
+ expect(the_bundle).to include_gems "nokogiri 1.4.2 java"
+ end
+
+ it "will add the resolve for the current platform" do
+ lockfile <<-G
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ nokogiri (1.4.2-java)
+ weakling (= 0.0.3)
+ weakling (0.0.3)
+
+ PLATFORMS
+ java
+
+ DEPENDENCIES
+ nokogiri
+ G
+
+ simulate_platform "x86-darwin-100" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 x86-darwin-100"
+ end
+ end
+
+ it "allows specifying only-ruby-platform on jruby", :jruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby"
+ end
+
+ it "allows specifying only-ruby-platform" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby"
+ end
+
+ it "allows specifying only-ruby-platform even if the lockfile is locked to a specific compatible platform" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri"
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "nokogiri 1.4.2", "platform_specific 1.0 ruby"
+ end
+
+ it "doesn't pull platform specific gems on truffleruby", :truffleruby_only do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+
+ it "doesn't pull platform specific gems on truffleruby (except when whitelisted) even if lockfile was generated with an older version that declared ruby as platform", :truffleruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ platform_specific
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+
+ simulate_platform "x86_64-linux" do
+ build_repo4 do
+ build_gem "libv8"
+
+ build_gem "libv8" do |s|
+ s.platform = "x86_64-linux"
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "libv8"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ libv8 (1.0)
+
+ PLATFORMS
+ ruby
+
+ DEPENDENCIES
+ libv8
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "libv8 1.0 x86_64-linux"
+ end
+ end
+
+ it "doesn't pull platform specific gems on truffleruby, even if lockfile only includes those", :truffleruby_only do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0-x86-darwin-100)
+
+ PLATFORMS
+ x86-darwin-100
+
+ DEPENDENCIES
+ platform_specific
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ end
+
+ it "pulls platform specific gems correctly on musl" do
+ build_repo4 do
+ build_gem "nokogiri", "1.13.8" do |s|
+ s.platform = "aarch64-linux"
+ end
+ end
+
+ simulate_platform "aarch64-linux-musl" do
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo4"
+ gem "nokogiri"
+ G
+ end
+
+ expect(out).to include("Fetching nokogiri 1.13.8 (aarch64-linux)")
+ end
+
+ it "allows specifying only-ruby-platform on windows with dependency platforms" do
+ simulate_platform "x86-mswin32" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "nokogiri", :platforms => [:windows, :jruby]
+ gem "platform_specific"
+ G
+
+ bundle_config "force_ruby_platform true"
+
+ bundle "install"
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 ruby"
+ expect(the_bundle).to not_include_gems "nokogiri"
+ end
+ end
+
+ it "allows specifying only-ruby-platform on windows with gemspec dependency" do
+ build_lib("foo", "1.0", path: bundled_app) do |s|
+ s.add_dependency "myrack"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+ bundle :lock
+
+ simulate_platform "x86-mswin32" do
+ bundle_config "force_ruby_platform true"
+ bundle "install"
+
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ it "recovers when the lockfile is missing a platform-specific gem" do
+ build_repo2 do
+ build_gem "requires_platform_specific" do |s|
+ s.add_dependency "platform_specific"
+ end
+ end
+ simulate_platform "x64-mingw-ucrt" do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ platform_specific (1.0-x86-mingw32)
+ requires_platform_specific (1.0)
+ platform_specific
+
+ PLATFORMS
+ x64-mingw-ucrt
+ x86-mingw32
+
+ DEPENDENCIES
+ requires_platform_specific
+ L
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo2"
+ gem "requires_platform_specific"
+ G
+
+ expect(out).to include("lockfile does not have all gems needed for the current platform")
+ expect(the_bundle).to include_gem "platform_specific 1.0 x64-mingw-ucrt"
+ end
+ end
+
+ %w[x86-mswin32 x64-mswin64 x86-mingw32 x64-mingw-ucrt aarch64-mingw-ucrt].each do |platform|
+ it "allows specifying platform windows on #{platform} platform" do
+ simulate_platform platform do
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ platform_specific (1.0-#{platform})
+ requires_platform_specific (1.0)
+ platform_specific
+
+ PLATFORMS
+ #{platform}
+
+ DEPENDENCIES
+ requires_platform_specific
+ L
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "platform_specific", :platforms => [:windows]
+ G
+
+ expect(the_bundle).to include_gems "platform_specific 1.0 #{platform}"
+ end
+ end
+ end
+end
diff --git a/spec/bundler/runtime/require_spec.rb b/spec/bundler/runtime/require_spec.rb
new file mode 100644
index 0000000000..46613286d2
--- /dev/null
+++ b/spec/bundler/runtime/require_spec.rb
@@ -0,0 +1,480 @@
+# frozen_string_literal: true
+
+RSpec.describe "Bundler.require" do
+ before :each do
+ build_lib "one", "1.0.0" do |s|
+ s.write "lib/baz.rb", "puts 'baz'"
+ s.write "lib/qux.rb", "puts 'qux'"
+ end
+
+ build_lib "two", "1.0.0" do |s|
+ s.write "lib/two.rb", "puts 'two'"
+ s.add_dependency "three", "= 1.0.0"
+ end
+
+ build_lib "three", "1.0.0" do |s|
+ s.write "lib/three.rb", "puts 'three'"
+ s.add_dependency "seven", "= 1.0.0"
+ end
+
+ build_lib "four", "1.0.0" do |s|
+ s.write "lib/four.rb", "puts 'four'"
+ end
+
+ build_lib "five", "1.0.0", no_default: true do |s|
+ s.write "lib/mofive.rb", "puts 'five'"
+ end
+
+ build_lib "six", "1.0.0" do |s|
+ s.write "lib/six.rb", "puts 'six'"
+ end
+
+ build_lib "seven", "1.0.0" do |s|
+ s.write "lib/seven.rb", "puts 'seven'"
+ end
+
+ build_lib "eight", "1.0.0" do |s|
+ s.write "lib/eight.rb", "puts 'eight'"
+ end
+
+ build_lib "nine", "1.0.0" do |s|
+ s.write "lib/nine.rb", "puts 'nine'"
+ end
+
+ build_lib "ten", "1.0.0" do |s|
+ s.write "lib/ten.rb", "puts 'ten'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "one", :group => :bar, :require => %w[baz qux]
+ gem "two"
+ gem "three", :group => :not
+ gem "four", :require => false
+ gem "five"
+ gem "six", :group => "string"
+ gem "seven", :group => :not
+ gem "eight", :require => true, :group => :require_true
+ env "BUNDLER_TEST" => "nine" do
+ gem "nine", :require => true
+ end
+ gem "ten", :install_if => lambda { ENV["BUNDLER_TEST"] == "ten" }
+ end
+ G
+ end
+
+ it "requires the gems" do
+ # default group
+ run "Bundler.require"
+ expect(out).to eq("two")
+
+ # specific group
+ run "Bundler.require(:bar)"
+ expect(out).to eq("baz\nqux")
+
+ # default and specific group
+ run "Bundler.require(:default, :bar)"
+ expect(out).to eq("baz\nqux\ntwo")
+
+ # specific group given as a string
+ run "Bundler.require('bar')"
+ expect(out).to eq("baz\nqux")
+
+ # specific group declared as a string
+ run "Bundler.require(:string)"
+ expect(out).to eq("six")
+
+ # required in resolver order instead of gemfile order
+ run("Bundler.require(:not)")
+ expect(out.split("\n").sort).to eq(%w[seven three])
+
+ # test require: true
+ run "Bundler.require(:require_true)"
+ expect(out).to eq("eight")
+ end
+
+ it "allows requiring gems with non standard names explicitly" do
+ run "Bundler.require ; require 'mofive'"
+ expect(out).to eq("two\nfive")
+ end
+
+ it "allows requiring gems which are scoped by env" do
+ ENV["BUNDLER_TEST"] = "nine"
+ run "Bundler.require"
+ expect(out).to eq("two\nnine")
+ end
+
+ it "allows requiring gems which are scoped by install_if" do
+ ENV["BUNDLER_TEST"] = "ten"
+ run "Bundler.require"
+ expect(out).to eq("two\nten")
+ end
+
+ it "raises an exception if a require is specified but the file does not exist" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two", :require => 'fail'
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- fail")
+ end
+
+ it "displays a helpful message if the required gem throws an error" do
+ build_lib "faulty", "1.0.0" do |s|
+ s.write "lib/faulty.rb", "raise RuntimeError.new(\"Gem Internal Error Message\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "faulty"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+ expect(err).to match("error while trying to load the gem 'faulty'")
+ expect(err).to match("Gem Internal Error Message")
+ end
+
+ it "doesn't swallow the error when the library has an unrelated error" do
+ build_lib "loadfuuu", "1.0.0" do |s|
+ s.write "lib/loadfuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "loadfuuu"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- load-bar")
+ end
+
+ describe "with namespaced gems" do
+ before :each do
+ build_lib "jquery-rails", "1.0.0" do |s|
+ s.write "lib/jquery/rails.rb", "puts 'jquery/rails'"
+ end
+ end
+
+ it "requires gem names that are namespaced" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path '#{lib_path}' do
+ gem 'jquery-rails'
+ end
+ G
+
+ run "Bundler.require"
+ expect(out).to eq("jquery/rails")
+ end
+
+ it "silently passes if the require fails" do
+ build_lib "bcrypt-ruby", "1.0.0", no_default: true do |s|
+ s.write "lib/brcrypt.rb", "BCrypt = '1.0.0'"
+ end
+ gemfile <<-G
+ source "https://gem.repo1"
+
+ path "#{lib_path}" do
+ gem "bcrypt-ruby"
+ end
+ G
+
+ cmd = <<-RUBY
+ require 'bundler'
+ Bundler.require
+ RUBY
+ ruby(cmd)
+
+ expect(err).to be_empty
+ end
+
+ it "does not mangle explicitly given requires" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem 'jquery-rails', :require => 'jquery-rails'
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- jquery-rails")
+ end
+
+ it "handles the case where regex fails" do
+ build_lib "load-fuuu", "1.0.0" do |s|
+ s.write "lib/load-fuuu.rb", "raise LoadError.new(\"Could not open library 'libfuuu-1.0': libfuuu-1.0: cannot open shared object file: No such file or directory.\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "load-fuuu"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("libfuuu-1.0").and include("cannot open shared object file")
+ end
+
+ it "doesn't swallow the error when the library has an unrelated error" do
+ build_lib "load-fuuu", "1.0.0" do |s|
+ s.write "lib/load/fuuu.rb", "raise LoadError.new(\"cannot load such file -- load-bar\")"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "load-fuuu"
+ end
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- load-bar")
+ end
+ end
+
+ describe "using bundle exec" do
+ it "requires the locked gems" do
+ bundle "exec ruby -e 'Bundler.require'"
+ expect(out).to eq("two")
+
+ bundle "exec ruby -e 'Bundler.require(:bar)'"
+ expect(out).to eq("baz\nqux")
+
+ bundle "exec ruby -e 'Bundler.require(:default, :bar)'"
+ expect(out).to eq("baz\nqux\ntwo")
+ end
+ end
+
+ describe "order" do
+ before(:each) do
+ build_lib "one", "1.0.0" do |s|
+ s.write "lib/one.rb", <<-ONE
+ if defined?(Two)
+ Two.two
+ else
+ puts "two_not_loaded"
+ end
+ puts 'one'
+ ONE
+ end
+
+ build_lib "two", "1.0.0" do |s|
+ s.write "lib/two.rb", <<-TWO
+ module Two
+ def self.two
+ puts 'module_two'
+ end
+ end
+ puts 'two'
+ TWO
+ end
+ end
+
+ it "works when the gems are in the Gemfile in the correct order" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "two"
+ gem "one"
+ end
+ G
+
+ run "Bundler.require"
+ expect(out).to eq("two\nmodule_two\none")
+ end
+
+ describe "a gem with different requires for different envs" do
+ before(:each) do
+ build_gem "multi_gem", to_bundle: true do |s|
+ s.write "lib/one.rb", "puts 'ONE'"
+ s.write "lib/two.rb", "puts 'TWO'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "multi_gem", :require => "one", :group => :one
+ gem "multi_gem", :require => "two", :group => :two
+ G
+ end
+
+ it "requires both with Bundler.require(both)" do
+ run "Bundler.require(:one, :two)"
+ expect(out).to eq("ONE\nTWO")
+ end
+
+ it "requires one with Bundler.require(:one)" do
+ run "Bundler.require(:one)"
+ expect(out).to eq("ONE")
+ end
+
+ it "requires :two with Bundler.require(:two)" do
+ run "Bundler.require(:two)"
+ expect(out).to eq("TWO")
+ end
+ end
+
+ it "fails when the gems are in the Gemfile in the wrong order" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path}" do
+ gem "one"
+ gem "two"
+ end
+ G
+
+ run "Bundler.require"
+ expect(out).to eq("two_not_loaded\none\ntwo")
+ end
+
+ describe "with busted gems" do
+ it "should be busted" do
+ build_gem "busted_require", to_bundle: true do |s|
+ s.write "lib/busted_require.rb", "require 'no_such_file_omg'"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "busted_require"
+ G
+
+ run "Bundler.require", raise_on_error: false
+
+ expect(err_without_deprecations).to include("cannot load such file -- no_such_file_omg")
+ end
+ end
+ end
+
+ it "does not load rubygems gemspecs that are used" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ run <<-R
+ path = File.join(Gem.dir, "specifications", "myrack-1.0.0.gemspec")
+ contents = File.read(path)
+ contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join
+ File.open(path, "w") do |f|
+ f.write contents
+ end
+ R
+
+ run <<-R
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not load git gemspecs that are used" do
+ build_git "foo"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ run <<-R
+ path = Gem.loaded_specs["foo"].loaded_from
+ contents = File.read(path)
+ contents = contents.lines.to_a.insert(-2, "\n raise 'broken gemspec'\n").join
+ File.open(path, "w") do |f|
+ f.write contents
+ end
+ R
+
+ run <<-R
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not load plugins" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ create_file "plugins/rubygems_plugin.rb", "puts 'FAIL'"
+
+ run <<~R, env: { "RUBYLIB" => rubylib.unshift(bundled_app("plugins").to_s).join(File::PATH_SEPARATOR) }
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "does not extract gemspecs from application cache packages" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle :cache
+
+ path = cached_gem("myrack-1.0.0")
+
+ run <<-R
+ File.open("#{path}", "w") do |f|
+ f.write "broken package"
+ end
+ R
+
+ run <<-R
+ Bundler.require
+ puts "WIN"
+ R
+
+ expect(out).to eq("WIN")
+ end
+end
+
+RSpec.describe "Bundler.require with platform specific dependencies" do
+ it "does not require the gems that are pinned to other platforms" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :#{not_local_tag} do
+ gem "platform_specific", :require => "omgomg"
+ end
+
+ gem "myrack", "1.0.0"
+ G
+
+ run "Bundler.require"
+ expect(err).to be_empty
+ end
+
+ it "requires gems pinned to multiple platforms, including the current one" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ platforms :#{not_local_tag}, :#{local_tag} do
+ gem "myrack", :require => "myrack"
+ end
+ G
+
+ run "Bundler.require; puts MYRACK"
+
+ expect(out).to eq("1.0.0")
+ expect(err).to be_empty
+ end
+end
diff --git a/spec/bundler/runtime/requiring_spec.rb b/spec/bundler/runtime/requiring_spec.rb
new file mode 100644
index 0000000000..f0e0aeacaf
--- /dev/null
+++ b/spec/bundler/runtime/requiring_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+RSpec.describe "Requiring bundler" do
+ it "takes care of requiring rubygems when entrypoint is bundler/setup" do
+ sys_exec("#{Gem.ruby} -I#{lib_dir} -rbundler/setup -e'puts true'", env: { "RUBYOPT" => "--disable=gems" })
+
+ expect(stdboth).to eq("true")
+ end
+
+ it "takes care of requiring rubygems when requiring just bundler" do
+ sys_exec("#{Gem.ruby} -I#{lib_dir} -rbundler -e'puts true'", env: { "RUBYOPT" => "--disable=gems" })
+
+ expect(stdboth).to eq("true")
+ end
+end
diff --git a/spec/bundler/runtime/self_management_spec.rb b/spec/bundler/runtime/self_management_spec.rb
new file mode 100644
index 0000000000..176c2a3121
--- /dev/null
+++ b/spec/bundler/runtime/self_management_spec.rb
@@ -0,0 +1,279 @@
+# frozen_string_literal: true
+
+RSpec.describe "Self management" do
+ describe "auto switching" do
+ let(:previous_minor) do
+ "9.3.0"
+ end
+
+ let(:current_version) do
+ "9.4.0"
+ end
+
+ before do
+ build_repo4 do
+ build_bundler previous_minor
+
+ build_bundler current_version
+
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo4"
+
+ gem "myrack"
+ G
+
+ pristine_system_gems "bundler-#{current_version}"
+ end
+
+ it "installs locked version when using system path and uses it" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "path.system true"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+
+ # It uninstalls the older system bundler
+ bundle "clean --force", artifice: nil
+ expect(out).to eq("Removing bundler (#{current_version})")
+
+ # App now uses locked version
+ bundle "-v", artifice: nil
+ expect(out).to eq(previous_minor)
+
+ # ruby-core test setup has always "lib" in $LOAD_PATH so `require "bundler/setup"` always activate the local version rather than using RubyGems gem activation stuff
+ unless ruby_core?
+ # App now uses locked version, even when not using the CLI directly
+ file = bundled_app("bin/bundle_version.rb")
+ create_file file, <<-RUBY
+ #!#{Gem.ruby}
+ require 'bundler/setup'
+ puts '#{previous_minor}'
+ RUBY
+ file.chmod(0o777)
+ cmd = Gem.win_platform? ? "#{Gem.ruby} bin/bundle_version.rb" : "bin/bundle_version.rb"
+ in_bundled_app cmd
+ expect(out).to eq(previous_minor)
+ end
+
+ # Subsequent installs use the locked version without reinstalling
+ bundle "install --verbose", artifice: nil
+ expect(out).to include("Using bundler #{previous_minor}")
+ expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ end
+
+ it "installs locked version when using local path and uses it" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "path vendor/bundle"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ expect(vendored_gems("gems/bundler-#{previous_minor}")).to exist
+
+ # It does not uninstall the locked bundler
+ bundle "clean"
+ expect(out).to be_empty
+
+ # App now uses locked version
+ bundle "-v"
+ expect(out).to eq(previous_minor)
+
+ # Preserves original gem home when auto-switching
+ bundle "exec ruby -e 'puts Bundler.original_env[\"GEM_HOME\"]'"
+ expect(out).to eq(ENV["GEM_HOME"])
+
+ # ruby-core test setup has always "lib" in $LOAD_PATH so `require "bundler/setup"` always activate the local version rather than using RubyGems gem activation stuff
+ unless ruby_core?
+ # App now uses locked version, even when not using the CLI directly
+ file = bundled_app("bin/bundle_version.rb")
+ create_file file, <<-RUBY
+ #!#{Gem.ruby}
+ require 'bundler/setup'
+ puts '#{previous_minor}'
+ RUBY
+ file.chmod(0o777)
+ cmd = Gem.win_platform? ? "#{Gem.ruby} bin/bundle_version.rb" : "bin/bundle_version.rb"
+ in_bundled_app cmd
+ expect(out).to eq(previous_minor)
+ end
+
+ # Subsequent installs use the locked version without reinstalling
+ bundle "install --verbose"
+ expect(out).to include("Using bundler #{previous_minor}")
+ expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ end
+
+ it "installs locked version when using deployment option and uses it" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "deployment true"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ expect(vendored_gems("gems/bundler-#{previous_minor}")).to exist
+
+ # It does not uninstall the locked bundler
+ bundle "clean"
+ expect(out).to be_empty
+
+ # App now uses locked version
+ bundle "-v"
+ expect(out).to eq(previous_minor)
+
+ # Subsequent installs use the locked version without reinstalling
+ bundle "install --verbose"
+ expect(out).to include("Using bundler #{previous_minor}")
+ expect(out).not_to include("Bundler #{current_version} is running, but your lockfile was generated with #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+ end
+
+ it "does not try to install a development version" do
+ lockfile_bundled_with("#{previous_minor}.dev")
+
+ bundle "install --verbose"
+ expect(out).not_to match(/restarting using that version/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "does not try to install when --local is passed" do
+ lockfile_bundled_with(previous_minor)
+ system_gems "myrack-1.0.0", path: local_gem_path
+
+ bundle "install --local"
+ expect(out).not_to match(/Installing Bundler/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "shows a discrete message if locked bundler does not exist" do
+ missing_minor = "#{current_version[0]}.999.999"
+
+ lockfile_bundled_with(missing_minor)
+
+ bundle "install"
+ expect(err).to eq("Your lockfile is locked to a version of bundler (#{missing_minor}) that doesn't exist at https://rubygems.org/. Going on using #{current_version}")
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "installs BUNDLE_VERSION version when using bundle config version x.y.z" do
+ lockfile_bundled_with(current_version)
+
+ bundle_config "version #{previous_minor}"
+ bundle "install"
+ expect(out).to include("Bundler #{current_version} is running, but your configuration was #{previous_minor}. Installing Bundler #{previous_minor} and restarting using that version.")
+
+ bundle "-v"
+ expect(out).to eq(previous_minor)
+ end
+
+ it "requires the right bundler version from the config and run bundle CLI without re-exec" do
+ unless Bundler.rubygems.provides?(">= 4.1.0.dev")
+ skip "This spec can only run when Gem::BundlerVersionFinder.bundler_versions reads bundler configs"
+ end
+
+ lockfile_bundled_with(current_version)
+
+ bundle_config "version #{previous_minor}"
+ bundle_config "path.system true"
+ bundle "install"
+
+ script = bundled_app("script.rb")
+ create_file(script, "p 'executed once'")
+
+ bundle "-v", env: { "RUBYOPT" => "-r#{script}" }
+ expect(out).to eq(%("executed once"\n9.3.0))
+ end
+
+ it "does not try to install when using bundle config version global" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "version system"
+ bundle "install"
+ expect(out).not_to match(/restarting using that version/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "does not try to install when using bundle config version <dev-version>" do
+ lockfile_bundled_with(previous_minor)
+
+ bundle_config "version #{previous_minor}.dev"
+ bundle "install"
+ expect(out).not_to match(/restarting using that version/)
+
+ bundle "-v"
+ expect(out).to eq(current_version)
+ end
+
+ it "ignores malformed lockfile version" do
+ lockfile_bundled_with("2.3.")
+
+ bundle "install --verbose"
+ expect(out).to include("Using bundler #{current_version}")
+ end
+
+ it "uses the right original script when re-execing, if `$0` has been changed to something that's not a script", :ruby_repo do
+ system_gems "bundler-9.9.9", path: local_gem_path
+
+ test = bundled_app("test.rb")
+
+ create_file test, <<~RUBY
+ $0 = "this is the program name"
+ require "bundler/setup"
+ RUBY
+
+ lockfile_bundled_with("9.9.9")
+
+ in_bundled_app "#{Gem.ruby} #{test}", raise_on_error: false
+ expect(err).to include("Could not find myrack-1.0.0")
+ expect(err).not_to include("this is the program name")
+ end
+
+ it "uses modified $0 when re-execing, if `$0` has been changed to a script", :ruby_repo do
+ system_gems "bundler-9.9.9", path: local_gem_path
+
+ runner = bundled_app("runner.rb")
+
+ create_file runner, <<~RUBY
+ $0 = ARGV.shift
+ load $0
+ RUBY
+
+ script = bundled_app("script.rb")
+ create_file script, <<~RUBY
+ require "bundler/setup"
+ RUBY
+
+ lockfile_bundled_with("9.9.9")
+
+ in_bundled_app "#{Gem.ruby} #{runner} #{script}", raise_on_error: false
+ expect(err).to include("Could not find myrack-1.0.0")
+ end
+
+ private
+
+ def lockfile_bundled_with(version)
+ lockfile <<~L
+ GEM
+ remote: https://gem.repo4/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+
+ BUNDLED WITH
+ #{version}
+ L
+ end
+ end
+end
diff --git a/spec/bundler/runtime/setup_spec.rb b/spec/bundler/runtime/setup_spec.rb
new file mode 100644
index 0000000000..ceb6fcf66a
--- /dev/null
+++ b/spec/bundler/runtime/setup_spec.rb
@@ -0,0 +1,1710 @@
+# frozen_string_literal: true
+
+require "tmpdir"
+
+RSpec.describe "Bundler.setup" do
+ describe "with no arguments" do
+ it "makes all groups available" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :test
+ G
+
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+
+ require 'myrack'
+ puts MYRACK
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("1.0.0")
+ end
+ end
+
+ describe "when called with groups" do
+ before(:each) do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack", :group => :test
+ G
+ end
+
+ it "doesn't make all groups available" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup(:default)
+
+ begin
+ require 'myrack'
+ rescue LoadError
+ puts "WIN"
+ end
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("WIN")
+ end
+
+ it "accepts string for group name" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup(:default, 'test')
+
+ require 'myrack'
+ puts MYRACK
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("1.0.0")
+ end
+
+ it "leaves all groups available if they were already" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ Bundler.setup(:default)
+
+ require 'myrack'
+ puts MYRACK
+ RUBY
+ expect(err).to be_empty
+ expect(out).to eq("1.0.0")
+ end
+
+ it "leaves :default available if setup is called twice" do
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup(:default)
+ Bundler.setup(:default, :test)
+
+ begin
+ require 'yard'
+ puts "WIN"
+ rescue LoadError
+ puts "FAIL"
+ end
+ RUBY
+ expect(err).to be_empty
+ expect(out).to match("WIN")
+ end
+
+ it "handles multiple non-additive invocations" do
+ ruby <<-RUBY, raise_on_error: false
+ require 'bundler'
+ Bundler.setup(:default, :test)
+ Bundler.setup(:default)
+ require 'myrack'
+
+ puts "FAIL"
+ RUBY
+
+ expect(err).to match("myrack")
+ expect(err).to match("LoadError")
+ expect(out).not_to match("FAIL")
+ end
+ end
+
+ context "load order" do
+ def clean_load_path(lp)
+ without_bundler_load_path = ruby("puts $LOAD_PATH").split("\n")
+ lp -= [*without_bundler_load_path, lib_dir.to_s]
+ lp.map! {|p| p.sub(system_gem_path.to_s, "") }
+ end
+
+ it "puts loaded gems after -I and RUBYLIB", :ruby_repo do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ENV["RUBYOPT"] = "#{ENV["RUBYOPT"]} -Idash_i_dir"
+ ENV["RUBYLIB"] = "rubylib_dir"
+
+ ruby <<-RUBY
+ require 'bundler'
+ Bundler.setup
+ puts $LOAD_PATH
+ RUBY
+
+ load_path = out.split("\n")
+ myrack_load_order = load_path.index {|path| path.include?("myrack") }
+
+ expect(err).to be_empty
+ expect(load_path).to include(a_string_ending_with("dash_i_dir"), "rubylib_dir")
+ expect(myrack_load_order).to be > 0
+ end
+
+ it "orders the load path correctly when there are dependencies" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails"
+ G
+
+ ruby <<-RUBY
+ require 'bundler'
+ gem "bundler", "#{Bundler::VERSION}" if #{ruby_core?}
+ Bundler.setup
+ puts $LOAD_PATH
+ RUBY
+
+ load_path = clean_load_path(out.split("\n"))
+
+ expect(load_path).to start_with(
+ "/gems/rails-2.3.2/lib",
+ "/gems/activeresource-2.3.2/lib",
+ "/gems/activerecord-2.3.2/lib",
+ "/gems/actionpack-2.3.2/lib",
+ "/gems/actionmailer-2.3.2/lib",
+ "/gems/activesupport-2.3.2/lib",
+ "/gems/rake-#{rake_version}/lib"
+ )
+ end
+
+ it "falls back to order the load path alphabetically for backwards compatibility" do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "weakling"
+ gem "duradura"
+ gem "terranova"
+ G
+
+ ruby <<-RUBY
+ require 'bundler/setup'
+ puts $LOAD_PATH
+ RUBY
+
+ load_path = clean_load_path(out.split("\n"))
+
+ expect(load_path).to start_with(
+ "/gems/weakling-0.0.3/lib",
+ "/gems/terranova-8/lib",
+ "/gems/duradura-7.0/lib"
+ )
+ end
+ end
+
+ it "raises if the Gemfile was not yet installed" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-R
+ require 'bundler'
+
+ begin
+ Bundler.setup
+ puts "FAIL"
+ rescue Bundler::GemNotFound
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "doesn't create a Gemfile.lock if the setup fails" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-R, raise_on_error: false
+ require 'bundler'
+
+ Bundler.setup
+ R
+
+ expect(bundled_app_lock).not_to exist
+ end
+
+ it "doesn't change the Gemfile.lock if the setup fails" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ lockfile = File.read(bundled_app_lock)
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "nosuchgem", "10.0"
+ G
+
+ ruby <<-R, raise_on_error: false
+ require 'bundler'
+
+ Bundler.setup
+ R
+
+ expect(File.read(bundled_app_lock)).to eq(lockfile)
+ end
+
+ it "makes a Gemfile.lock if setup succeeds" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ File.read(bundled_app_lock)
+
+ FileUtils.rm(bundled_app_lock)
+
+ run "1"
+ expect(bundled_app_lock).to exist
+ end
+
+ describe "$BUNDLE_GEMFILE" do
+ context "user provides an absolute path" do
+ it "uses BUNDLE_GEMFILE to locate the gemfile if present" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile bundled_app("4realz"), <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.5"
+ G
+
+ ENV["BUNDLE_GEMFILE"] = bundled_app("4realz").to_s
+ bundle :install
+
+ expect(the_bundle).to include_gems "activesupport 2.3.5"
+ end
+ end
+
+ context "an absolute path is not provided" do
+ it "uses BUNDLE_GEMFILE to locate the gemfile if present and doesn't fail in deployment mode" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ bundle "install"
+ bundle_config "deployment true"
+
+ ENV["BUNDLE_GEMFILE"] = "Gemfile"
+ ruby <<-R
+ require 'bundler'
+
+ begin
+ Bundler.setup
+ puts "WIN"
+ rescue ArgumentError => e
+ puts "FAIL"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+ end
+
+ context "user sets it via `config set --local gemfile`" do
+ it "uses the value in the config" do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ gemfile bundled_app("CustomGemfile"), <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.5"
+ G
+
+ bundle_config "gemfile #{bundled_app("CustomGemfile")}"
+ bundle "install"
+
+ ruby <<-R
+ require 'bundler'
+ Bundler.setup
+ require 'activesupport'
+ puts ACTIVESUPPORT
+ R
+
+ expect(out).to eq("2.3.5")
+ end
+ end
+ end
+
+ it "prioritizes gems in BUNDLE_PATH over gems in GEM_HOME" do
+ ENV["BUNDLE_PATH"] = bundled_app(".bundle").to_s
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ G
+
+ build_gem "myrack", "1.0", to_system: true do |s|
+ s.write "lib/myrack.rb", "MYRACK = 'FAIL'"
+ end
+
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ describe "integrate with rubygems" do
+ describe "by replacing #gem" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "0.9.1"
+ G
+ end
+
+ it "replaces #gem but raises when the gem is missing" do
+ run <<-R
+ begin
+ gem "activesupport"
+ puts "FAIL"
+ rescue LoadError
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "replaces #gem but raises when the version is wrong" do
+ run <<-R
+ begin
+ gem "myrack", "1.0.0"
+ puts "FAIL"
+ rescue LoadError
+ puts "WIN"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+ end
+
+ describe "by hiding system gems" do
+ before :each do
+ system_gems "activesupport-2.3.5"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "yard"
+ G
+ end
+
+ it "removes system gems from Gem.source_index" do
+ run "require 'yard'"
+ expect(out).to include("bundler-#{Bundler::VERSION}").and include("yard-1.0")
+ expect(out).not_to include("activesupport-2.3.5")
+ end
+
+ context "when the ruby stdlib is a substring of Gem.path" do
+ it "does not reject the stdlib from $LOAD_PATH" do
+ substring = "/" + $LOAD_PATH.find {|p| p.include?("vendor_ruby") }.split("/")[2]
+ run "puts 'worked!'", env: { "GEM_PATH" => substring }
+ expect(out).to eq("worked!")
+ end
+ end
+ end
+ end
+
+ describe "with paths" do
+ it "activates the gems in the path source" do
+ system_gems "myrack-1.0.0"
+
+ build_lib "myrack", "1.0.0" do |s|
+ s.write "lib/myrack.rb", "puts 'WIN'"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ path "#{lib_path("myrack-1.0.0")}" do
+ gem "myrack"
+ end
+ G
+
+ run "require 'myrack'"
+ expect(out).to eq("WIN")
+ end
+ end
+
+ describe "with git" do
+ before do
+ build_git "myrack", "1.0.0"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-1.0.0")}"
+ G
+ end
+
+ it "provides a useful exception when the git repo is not checked out yet" do
+ run "1", raise_on_error: false
+ expect(err).to match(/the git source #{lib_path("myrack-1.0.0")} is not yet checked out. Please run `bundle install`/i)
+ end
+
+ it "does not hit the git binary if the lockfile is available and up to date" do
+ bundle "install"
+
+ break_git!
+
+ ruby <<-R
+ require 'bundler'
+
+ begin
+ Bundler.setup
+ puts "WIN"
+ rescue Exception => e
+ puts "FAIL"
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "provides a good exception if the lockfile is unavailable" do
+ bundle "install"
+
+ FileUtils.rm(bundled_app_lock)
+
+ break_git!
+
+ ruby <<-R
+ require "bundler"
+
+ begin
+ Bundler.setup
+ puts "FAIL"
+ rescue Bundler::GitError => e
+ puts e.message
+ end
+ R
+
+ run "puts 'FAIL'", raise_on_error: false
+
+ expect(err).not_to include "This is not the git you are looking for"
+ end
+
+ it "works even when the cache directory has been deleted" do
+ bundle :install
+ FileUtils.rm_r default_cache_path
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "does not randomly change the path when specifying --path and the bundle directory becomes read only" do
+ bundle_config "path vendor/bundle"
+ bundle :install
+
+ with_read_only("#{bundled_app}/**/*") do
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ it "finds git gem when default bundle path becomes read only" do
+ bundle_config "path .bundle"
+ bundle "install"
+
+ with_read_only("#{bundled_app(".bundle")}/**/*") do
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+ end
+
+ describe "when specifying local override" do
+ it "explodes if given path does not exist on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ FileUtils.rm_r(lib_path("local-myrack"))
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/Cannot use local override for myrack-0.8 because #{Regexp.escape(lib_path("local-myrack").to_s)} does not exist/)
+ end
+
+ it "explodes if branch is not given on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}"
+ G
+
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/because :branch is not specified in Gemfile/)
+ end
+
+ it "explodes on different branches on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle :install
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "changed"
+ G
+
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/is using branch main but Gemfile specifies changed/)
+ end
+
+ it "explodes on refs with different branches on runtime" do
+ build_git "myrack", "0.8"
+
+ FileUtils.cp_r("#{lib_path("myrack-0.8")}/.", lib_path("local-myrack"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :ref => "main", :branch => "main"
+ G
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :ref => "main", :branch => "nonexistent"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ run "require 'myrack'", raise_on_error: false
+ expect(err).to match(/is using branch main but Gemfile specifies nonexistent/)
+ end
+ end
+
+ describe "when excluding groups" do
+ it "doesn't change the resolve if --without is used" do
+ bundle_config "without rails"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+
+ group :rails do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ system_gems "activesupport-2.3.5"
+
+ expect(the_bundle).to include_gems "activesupport 2.3.2", groups: :default
+ end
+
+ it "remembers --without and does not bail on bare Bundler.setup" do
+ bundle_config "without rails"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+
+ group :rails do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ system_gems "activesupport-2.3.5"
+
+ expect(the_bundle).to include_gems "activesupport 2.3.2"
+ end
+
+ it "remembers --without and does not bail on bare Bundler.setup, even in the case of path gems no longer available" do
+ bundle_config "without development"
+
+ path = bundled_app(File.join("vendor", "foo"))
+ build_lib "foo", path: path
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", "2.3.2"
+ gem 'foo', :path => 'vendor/foo', :group => :development
+ G
+
+ FileUtils.rm_r(path)
+
+ ruby "require 'bundler'; Bundler.setup", env: { "DEBUG" => "1" }
+ expect(out).to include("Assuming that source at `vendor/foo` has not changed since fetching its specs errored")
+ expect(out).to include("Found no changes, using resolution from the lockfile")
+ expect(err).to be_empty
+ end
+
+ it "doesn't re-resolve when a pre-release bundler is used and a dependency includes a dependency on bundler" do
+ system_gems "bundler-9.99.9.beta1"
+
+ build_repo4 do
+ build_gem "depends_on_bundler", "1.0" do |s|
+ s.add_dependency "bundler", ">= 1.5.0"
+ end
+ end
+
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "depends_on_bundler"
+ G
+
+ ruby "require '#{system_gem_path("gems/bundler-9.99.9.beta1/lib/bundler.rb")}'; Bundler.setup", env: { "DEBUG" => "1" }
+ expect(out).to include("Found no changes, using resolution from the lockfile")
+ expect(out).not_to include("lockfile does not have all gems needed for the current platform")
+ expect(err).to be_empty
+ end
+
+ it "doesn't fail in frozen mode when bundler is a Gemfile dependency" do
+ install_gemfile <<~G
+ source "https://gem.repo4"
+ gem "bundler"
+ G
+
+ bundle "install --verbose", env: { "BUNDLE_FROZEN" => "true" }
+ expect(err).to be_empty
+ end
+
+ it "doesn't re-resolve when deleting dependencies" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "actionpack"
+ G
+
+ install_gemfile <<-G, verbose: true
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ expect(out).to include("Some dependencies were deleted, using a subset of the resolution from the lockfile")
+ expect(err).to be_empty
+ end
+
+ it "remembers --without and does not include groups passed to Bundler.setup" do
+ bundle_config "without rails"
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+
+ group :myrack do
+ gem "myrack"
+ end
+
+ group :rails do
+ gem "rails", "2.3.2"
+ end
+ G
+
+ expect(the_bundle).not_to include_gems "activesupport 2.3.2", groups: :myrack
+ expect(the_bundle).to include_gems "myrack 1.0.0", groups: :myrack
+ end
+ end
+
+ # RubyGems returns loaded_from as a string
+ it "has loaded_from as a string on all specs" do
+ build_git "foo"
+ build_git "no-gemspec", gemspec: false
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ gem "no-gemspec", "1.0", :git => "#{lib_path("no-gemspec-1.0")}"
+ G
+
+ run <<-R
+ Gem.loaded_specs.each do |n, s|
+ puts "FAIL" unless s.loaded_from.is_a?(String)
+ end
+ R
+
+ expect(out).to be_empty
+ end
+
+ it "has gem_dir pointing to local repo" do
+ build_lib "foo", "1.0", path: bundled_app
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gemspec
+ G
+
+ run <<-R
+ puts Gem.loaded_specs['foo'].gem_dir
+ R
+
+ expect(out).to eq(bundled_app.to_s)
+ end
+
+ it "does not load all gemspecs" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ run <<-R
+ File.open(File.join(Gem.dir, "specifications", "invalid.gemspec"), "w") do |f|
+ f.write <<-RUBY
+# -*- encoding: utf-8 -*-
+# stub: invalid 1.0.0 ruby lib
+
+Gem::Specification.new do |s|
+ s.name = "invalid"
+ s.version = "1.0.0"
+ s.authors = ["Invalid Author"]
+ s.files = ["lib/invalid.rb"]
+ s.add_dependency "nonexistent-gem", "~> 999.999.999"
+ s.validate!
+end
+ RUBY
+ end
+ R
+
+ run <<-R
+ File.open(File.join(Gem.dir, "specifications", "invalid-ext.gemspec"), "w") do |f|
+ f.write <<-RUBY
+# -*- encoding: utf-8 -*-
+# stub: invalid-ext 1.0.0 ruby lib
+# stub: a.ext\\0b.ext
+
+Gem::Specification.new do |s|
+ s.name = "invalid-ext"
+ s.version = "1.0.0"
+ s.authors = ["Invalid Author"]
+ s.files = ["lib/invalid.rb"]
+ s.required_ruby_version = "~> 0.8.0"
+ s.validate!
+end
+ RUBY
+ end
+ # Need to write the gem.build_complete file,
+ # otherwise the full spec is loaded to check the installed_by_version
+ extensions_dir = Gem.default_ext_dir_for(Gem.dir) || File.join(Gem.dir, "extensions", Gem::Platform.local.to_s, Gem.extension_api_version)
+ Bundler::FileUtils.mkdir_p(File.join(extensions_dir, "invalid-ext-1.0.0"))
+ File.open(File.join(extensions_dir, "invalid-ext-1.0.0", "gem.build_complete"), "w") {}
+ R
+
+ run <<-R
+ puts "Success"
+ R
+
+ expect(out).to eq("Success")
+ end
+
+ it "ignores empty gem paths" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ENV["GEM_HOME"] = ""
+ bundle %(exec ruby -e "require 'set'")
+
+ expect(err).to be_empty
+ end
+
+ it "can require rubygems without warnings, when using a local cache", :truffleruby do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ bundle "package"
+ bundle %(exec ruby -w -e "require 'rubygems'")
+
+ expect(err).to be_empty
+ end
+
+ context "when the user has `MANPATH` set", :man do
+ before { ENV["MANPATH"] = "/foo#{File::PATH_SEPARATOR}" }
+
+ it "adds the gem's man dir to the MANPATH" do
+ build_repo4 do
+ build_gem "with_man" do |s|
+ s.write("man/man1/page.1", "MANPAGE")
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "with_man"
+ G
+
+ run "puts ENV['MANPATH']"
+ expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}#{File::PATH_SEPARATOR}/foo")
+ end
+ end
+
+ context "when the user does not have `MANPATH` set", :man do
+ before { ENV.delete("MANPATH") }
+
+ it "adds the gem's man dir to the MANPATH, leaving : in the end so that system man pages still work" do
+ build_repo4 do
+ build_gem "with_man" do |s|
+ s.write("man/man1/page.1", "MANPAGE")
+ end
+
+ build_gem "with_man_overriding_system_man" do |s|
+ s.write("man/man1/ls.1", "LS MANPAGE")
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "with_man"
+ G
+
+ run <<~RUBY
+ puts ENV['MANPATH']
+ require "open3"
+ puts Open3.capture2e("man", "ls")[1].success?
+ RUBY
+
+ expect(out).to eq("#{default_bundle_path("gems/with_man-1.0/man")}#{File::PATH_SEPARATOR}\ntrue")
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "with_man_overriding_system_man"
+ G
+
+ run <<~RUBY
+ puts ENV['MANPATH']
+ require "open3"
+ puts Open3.capture2e({ "LC_ALL" => "C" }, "man", "ls")[0]
+ RUBY
+
+ lines = out.split("\n")
+
+ expect(lines).to include("#{default_bundle_path("gems/with_man_overriding_system_man-1.0/man")}#{File::PATH_SEPARATOR}")
+ expect(lines).to include("LS MANPAGE")
+ end
+ end
+
+ it "should prepend gemspec require paths to $LOAD_PATH in order" do
+ update_repo2 do
+ build_gem("requirepaths") do |s|
+ s.write("lib/rq.rb", "puts 'yay'")
+ s.write("src/rq.rb", "puts 'nooo'")
+ s.require_paths = %w[lib src]
+ end
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ gem "requirepaths", :require => nil
+ G
+
+ run "require 'rq'"
+ expect(out).to eq("yay")
+ end
+
+ it "should clean $LOAD_PATH properly" do
+ gem_name = "very_simple_binary"
+ full_gem_name = gem_name + "-1.0"
+
+ system_gems full_gem_name
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ G
+
+ ruby <<-R
+ require 'bundler'
+ gem '#{gem_name}'
+
+ puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} >= 2
+
+ Bundler.setup
+
+ puts $LOAD_PATH.count {|path| path =~ /#{gem_name}/} == 0
+ R
+
+ expect(out).to eq("true\ntrue")
+ end
+
+ context "with bundler is located in symlinked GEM_HOME" do
+ let(:gem_home) { Dir.mktmpdir }
+ let(:symlinked_gem_home) { tmp("gem_home-symlink").to_s }
+ let(:full_name) { "bundler-#{Bundler::VERSION}" }
+
+ before do
+ File.symlink(gem_home, symlinked_gem_home)
+ gems_dir = File.join(gem_home, "gems")
+ specifications_dir = File.join(gem_home, "specifications")
+ Dir.mkdir(gems_dir)
+ Dir.mkdir(specifications_dir)
+
+ File.symlink(source_root, File.join(gems_dir, full_name))
+
+ gemspec_content = File.binread(gemspec).
+ sub("Bundler::VERSION", %("#{Bundler::VERSION}")).
+ lines.reject {|line| line.include?("lib/bundler/version") }.join
+
+ File.open(File.join(specifications_dir, "#{full_name}.gemspec"), "wb") do |f|
+ f.write(gemspec_content)
+ end
+ end
+
+ it "should not remove itself from the LOAD_PATH and require a different copy of 'bundler/setup'" do
+ install_gemfile "source 'https://gem.repo1'"
+
+ ruby <<-R, env: { "GEM_PATH" => symlinked_gem_home }
+ TracePoint.trace(:class) do |tp|
+ if tp.path.include?("bundler") && !tp.path.start_with?("#{source_root}")
+ puts "OMG. Defining a class from another bundler at \#{tp.path}:\#{tp.lineno}"
+ end
+ end
+ gem 'bundler', '#{Bundler::VERSION}'
+ require 'bundler/setup'
+ R
+
+ expect(out).to be_empty
+ end
+ end
+
+ it "does not reveal system gems even when Gem.refresh is called" do
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport"
+ G
+
+ run <<-R
+ puts Bundler.rubygems.installed_specs.map(&:name)
+ Gem.refresh
+ puts Bundler.rubygems.installed_specs.map(&:name)
+ R
+
+ expect(out).to eq("activesupport\nbundler\nactivesupport\nbundler")
+ end
+
+ describe "when a vendored gem specification uses the :path option" do
+ let(:filesystem_root) do
+ current = Pathname.new(Dir.pwd)
+ current = current.parent until current == current.parent
+ current
+ end
+
+ it "should resolve paths relative to the Gemfile" do
+ path = bundled_app(File.join("vendor", "foo"))
+ build_lib "foo", path: path
+
+ # If the .gemspec exists, then Bundler handles the path differently.
+ # See Source::Path.load_spec_files for details.
+ FileUtils.rm(File.join(path, "foo.gemspec"))
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', '1.2.3', :path => 'vendor/foo'
+ G
+
+ run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent
+ require 'foo'
+ R
+ expect(err).to be_empty
+ end
+
+ it "should make sure the Bundler.root is really included in the path relative to the Gemfile" do
+ relative_path = File.join("vendor", Dir.pwd.gsub(/^#{filesystem_root}/, ""))
+ absolute_path = bundled_app(relative_path)
+ FileUtils.mkdir_p(absolute_path)
+ build_lib "foo", path: absolute_path
+
+ # If the .gemspec exists, then Bundler handles the path differently.
+ # See Source::Path.load_spec_files for details.
+ FileUtils.rm(File.join(absolute_path, "foo.gemspec"))
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', '1.2.3', :path => '#{relative_path}'
+ G
+
+ bundle :install
+
+ run <<-R, env: { "BUNDLE_GEMFILE" => bundled_app_gemfile.to_s }, dir: bundled_app.parent
+ require 'foo'
+ R
+
+ expect(err).to be_empty
+ end
+ end
+
+ describe "with git gems that don't have gemspecs" do
+ before :each do
+ build_git "no_gemspec", gemspec: false
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "no_gemspec", "1.0", :git => "#{lib_path("no_gemspec-1.0")}"
+ G
+ end
+
+ it "loads the library via a virtual spec" do
+ run <<-R
+ require 'no_gemspec'
+ puts NO_GEMSPEC
+ R
+
+ expect(out).to eq("1.0")
+ end
+ end
+
+ describe "with bundled and system gems" do
+ before :each do
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+
+ gem "activesupport", "2.3.5"
+ G
+ end
+
+ it "does not pull in system gems" do
+ run <<-R
+ begin;
+ require 'myrack'
+ rescue LoadError
+ puts 'WIN'
+ end
+ R
+
+ expect(out).to eq("WIN")
+ end
+
+ it "provides a gem method" do
+ run <<-R
+ gem 'activesupport'
+ require 'activesupport'
+ puts ACTIVESUPPORT
+ R
+
+ expect(out).to eq("2.3.5")
+ end
+
+ it "raises an exception if gem is used to invoke a system gem not in the bundle" do
+ run <<-R
+ begin
+ gem 'myrack'
+ rescue LoadError => e
+ puts e.message
+ end
+ R
+
+ expect(out).to eq("myrack is not part of the bundle. Add it to your Gemfile.")
+ end
+
+ it "sets GEM_HOME appropriately" do
+ run "puts ENV['GEM_HOME']"
+ expect(out).to eq(default_bundle_path.to_s)
+ end
+ end
+
+ describe "with system gems in the bundle" do
+ before :each do
+ bundle_config "path.system true"
+ system_gems "myrack-1.0.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", "1.0.0"
+ gem "activesupport", "2.3.5"
+ G
+ end
+
+ it "sets GEM_PATH appropriately" do
+ run "puts Gem.path"
+ paths = out.split("\n")
+ expect(paths).to include(system_gem_path.to_s)
+ end
+ end
+
+ describe "with a gemspec that requires other files" do
+ before :each do
+ build_git "bar", gemspec: false do |s|
+ s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ require_relative 'lib/bar/version'
+
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ s.author = 'no one'
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+ end
+
+ it "evals each gemspec in the context of its parent directory" do
+ bundle :install
+ run "require 'bar'; puts BAR"
+ expect(out).to eq("1.0")
+ end
+
+ it "error intelligently if the gemspec has a LoadError" do
+ skip "whitespace issue?" if Gem.win_platform?
+
+ ref = update_git "bar", gemspec: false do |s|
+ s.write "bar.gemspec", "require 'foobarbaz'"
+ end.ref_for("HEAD")
+ bundle :install, raise_on_error: false
+
+ expect(err.lines.map(&:chomp)).to include(
+ a_string_starting_with("[!] There was an error while loading `bar.gemspec`:"),
+ " # from #{default_bundle_path "bundler", "gems", "bar-1.0-#{ref[0, 12]}", "bar.gemspec"}:1",
+ " > require 'foobarbaz'"
+ )
+ end
+
+ it "evals each gemspec with a binding from the top level" do
+ bundle "install"
+
+ ruby <<-RUBY
+ require 'bundler'
+ bundler_module = class << Bundler; self; end
+ bundler_module.send(:remove_method, :require)
+ def Bundler.require(path)
+ raise StandardError, "didn't use binding from top level"
+ end
+ Bundler.load
+ RUBY
+
+ expect(err).to be_empty
+ expect(out).to be_empty
+ end
+ end
+
+ describe "when Bundler is bundled" do
+ it "doesn't blow up" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "bundler", :path => "#{root}"
+ G
+
+ bundle %(exec ruby -e "require 'bundler'; Bundler.setup")
+ expect(err).to be_empty
+ end
+ end
+
+ describe "when BUNDLED WITH" do
+ def lock_with(bundler_version = nil)
+ lock = <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ L
+
+ if bundler_version
+ lock += "\nBUNDLED WITH\n #{bundler_version}\n"
+ end
+
+ lock
+ end
+
+ before do
+ bundle_config "path.system true"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ context "is not present" do
+ it "does not change the lock" do
+ lockfile lock_with(nil)
+ ruby "require 'bundler/setup'"
+ expect(lockfile).to eq lock_with(nil)
+ end
+ end
+
+ context "is newer" do
+ it "does not change the lock or warn" do
+ lockfile lock_with(Bundler::VERSION.succ)
+ ruby "require 'bundler/setup'"
+ expect(out).to be_empty
+ expect(err).to be_empty
+ expect(lockfile).to eq lock_with(Bundler::VERSION.succ)
+ end
+ end
+
+ context "is older" do
+ it "does not change the lock" do
+ system_gems "bundler-1.10.1"
+ lockfile lock_with("1.10.1")
+ ruby "require 'bundler/setup'"
+ expect(lockfile).to eq lock_with("1.10.1")
+ end
+ end
+ end
+
+ describe "when RUBY VERSION" do
+ let(:ruby_version) { nil }
+
+ def lock_with(ruby_version = nil)
+ checksums = checksums_section do |c|
+ c.checksum gem_repo1, "myrack", "1.0.0"
+ end
+
+ lock = <<~L
+ GEM
+ remote: https://gem.repo1/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ myrack
+ #{checksums}
+ L
+
+ if ruby_version
+ lock += "\nRUBY VERSION\n ruby #{ruby_version}\n"
+ end
+
+ lock += <<~L
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+
+ lock
+ end
+
+ before do
+ install_gemfile <<-G
+ ruby ">= 0"
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ lockfile lock_with(ruby_version)
+ end
+
+ context "is not present" do
+ # Skipped on ruby-core because `ruby "require 'bundler/setup'"` does not
+ # activate bundler as a gem there, so Source::Metadata falls back to a
+ # synthetic spec whose cache_file does not exist on disk and
+ # LockfileGenerator#bundler_checksum drops the bundler checksum, while
+ # the on-disk lockfile still has it.
+ it "does not change the lock", :ruby_repo do
+ expect { ruby "require 'bundler/setup'" }.not_to change { lockfile }
+ end
+ end
+
+ context "is newer" do
+ let(:ruby_version) { "5.5.5" }
+ it "does not change the lock or warn" do
+ expect { ruby "require 'bundler/setup'" }.not_to change { lockfile }
+ expect(out).to be_empty
+ expect(err).to be_empty
+ end
+ end
+
+ context "is older" do
+ let(:ruby_version) { "1.0.0" }
+ it "does not change the lock" do
+ expect { ruby "require 'bundler/setup'" }.not_to change { lockfile }
+ end
+ end
+ end
+
+ describe "with gemified standard libraries" do
+ it "does not load Digest", :ruby_repo do
+ build_git "bar", gemspec: false do |s|
+ s.write "lib/bar/version.rb", %(BAR_VERSION = '1.0')
+ s.write "bar.gemspec", <<-G
+ require_relative 'lib/bar/version'
+
+ Gem::Specification.new do |s|
+ s.name = 'bar'
+ s.version = BAR_VERSION
+ s.summary = 'Bar'
+ s.files = Dir["lib/**/*.rb"]
+ s.author = 'no one'
+
+ s.add_dependency 'digest'
+ end
+ G
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "bar", :git => "#{lib_path("bar-1.0")}"
+ G
+
+ bundle :install, env: { "BUNDLE_LOCKFILE_CHECKSUMS" => "false" }
+
+ ruby <<-RUBY, artifice: nil
+ require 'bundler/setup'
+ puts defined?(::Digest) ? "Digest defined" : "Digest undefined"
+ require 'digest'
+ RUBY
+ expect(out).to eq("Digest undefined")
+ end
+
+ it "does not load Psych" do
+ gemfile "source 'https://gem.repo1'"
+ ruby <<-RUBY
+ require 'bundler/setup'
+ puts defined?(Psych::VERSION) ? Psych::VERSION : "undefined"
+ require 'psych'
+ puts Psych::VERSION
+ RUBY
+ pre_bundler, post_bundler = out.split("\n")
+ expect(pre_bundler).to eq("undefined")
+ expect(post_bundler).to match(/\d+\.\d+\.\d+/)
+ end
+
+ it "does not load openssl" do
+ install_gemfile "source 'https://gem.repo1'"
+ ruby <<-RUBY, artifice: nil
+ require "bundler/setup"
+ puts defined?(OpenSSL) || "undefined"
+ require "openssl"
+ puts defined?(OpenSSL) || "undefined"
+ RUBY
+ expect(out).to eq("undefined\nconstant")
+ end
+
+ it "does not load uri while reading gemspecs", rubygems: ">= 3.6.0.dev" do
+ Dir.mkdir bundled_app("test")
+
+ create_file(bundled_app("test/test.gemspec"), <<-G)
+ Gem::Specification.new do |s|
+ s.name = "test"
+ s.version = "1.0.0"
+ s.summary = "test"
+ s.authors = ['John Doe']
+ s.homepage = 'https://example.com'
+ end
+ G
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "test", path: "#{bundled_app("test")}"
+ G
+
+ ruby <<-RUBY, artifice: nil
+ require "bundler/setup"
+ puts defined?(URI) || "undefined"
+ require "uri"
+ puts defined?(URI) || "undefined"
+ RUBY
+ expect(out).to eq("undefined\nconstant")
+ end
+
+ it "activates default gems when they are part of the bundle, but not installed explicitly", :ruby_repo do
+ default_delegate_version = ruby "gem 'delegate'; require 'delegate'; puts Delegator::VERSION"
+
+ build_repo2 do
+ build_gem "delegate", default_delegate_version
+ end
+
+ gemfile "source \"https://gem.repo2\"; gem 'delegate'"
+
+ ruby <<-RUBY
+ require "bundler/setup"
+ require "delegate"
+ puts defined?(::Delegator) ? "Delegator defined" : "Delegator undefined"
+ RUBY
+
+ expect(out).to eq("Delegator defined")
+ expect(err).to be_empty
+ end
+
+ describe "default gem activation" do
+ let(:exemptions) do
+ exempts = %w[did_you_mean bundler uri pathname]
+ exempts << "error_highlight" # added in Ruby 3.1 as a default gem
+ exempts << "ruby2_keywords" # added in Ruby 3.1 as a default gem
+ exempts << "syntax_suggest" # added in Ruby 3.2 as a default gem
+ exempts
+ end
+
+ let(:activation_warning_hack) { <<~RUBY }
+ require #{spec_dir.join("support/hax").to_s.dump}
+
+ Gem::Specification.send(:alias_method, :bundler_spec_activate, :activate)
+ Gem::Specification.send(:define_method, :activate) do
+ unless #{exemptions.inspect}.include?(name)
+ warn '-' * 80
+ warn "activating \#{full_name}"
+ warn(*caller)
+ warn '*' * 80
+ end
+ bundler_spec_activate
+ end
+ RUBY
+
+ let(:activation_warning_hack_rubyopt) do
+ create_file("activation_warning_hack.rb", activation_warning_hack)
+ "-r#{bundled_app("activation_warning_hack.rb")} #{ENV["RUBYOPT"]}"
+ end
+
+ let(:code) { <<~RUBY }
+ require "pp"
+ loaded_specs = Gem.loaded_specs.dup
+ #{exemptions.inspect}.each {|s| loaded_specs.delete(s) }
+ pp loaded_specs
+
+ # not a default gem, but harmful to have loaded
+ open_uri = $LOADED_FEATURES.grep(/open.uri/)
+ unless open_uri.empty?
+ warn "open_uri: \#{open_uri}"
+ end
+ RUBY
+
+ it "activates no gems with -rbundler/setup" do
+ install_gemfile "source 'https://gem.repo1'"
+ ruby code, env: { "RUBYOPT" => activation_warning_hack_rubyopt + " -rbundler/setup" }, artifice: nil
+ expect(out).to eq("{}")
+ end
+
+ it "activates no gems with bundle exec" do
+ install_gemfile "source 'https://gem.repo1'"
+ create_file("script.rb", code)
+ bundle "exec ruby ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt }
+ expect(out).to eq("{}")
+ end
+
+ it "activates no gems with bundle exec that is loaded" do
+ skip "not executable" if Gem.win_platform?
+
+ install_gemfile "source 'https://gem.repo1'"
+ create_file("script.rb", "#!/usr/bin/env ruby\n\n#{code}")
+ FileUtils.chmod(0o777, bundled_app("script.rb"))
+ bundle "exec ./script.rb", env: { "RUBYOPT" => activation_warning_hack_rubyopt }
+ expect(out).to eq("{}")
+ end
+
+ it "does not load net-http-pipeline too early" do
+ build_repo4 do
+ build_gem "net-http-pipeline", "1.0.1"
+ end
+
+ system_gems "net-http-pipeline-1.0.1", gem_repo: gem_repo4
+
+ gemfile <<-G
+ source "https://gem.repo4"
+ gem "net-http-pipeline", "1.0.1"
+ G
+
+ bundle_config "path vendor/bundle"
+
+ bundle :install
+
+ bundle :check
+
+ expect(out).to eq("The Gemfile's dependencies are satisfied")
+ end
+
+ Gem::Specification.select(&:default_gem?).map(&:name).each do |g|
+ it "activates newer versions of #{g}", :ruby_repo do
+ skip if exemptions.include?(g)
+
+ build_repo4 do
+ build_gem g, "999999"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "#{g}", "999999"
+ G
+
+ expect(the_bundle).to include_gem("#{g} 999999", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil)
+ end
+
+ it "activates older versions of #{g}", :ruby_repo do
+ skip if exemptions.include?(g)
+
+ build_repo4 do
+ build_gem g, "0.0.0.a"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ gem "#{g}", "0.0.0.a"
+ G
+
+ expect(the_bundle).to include_gem("#{g} 0.0.0.a", env: { "RUBYOPT" => activation_warning_hack_rubyopt }, artifice: nil)
+ end
+ end
+ end
+ end
+
+ describe "after setup" do
+ it "keeps Kernel#gem private" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-RUBY, raise_on_error: false
+ require "bundler/setup"
+ Object.new.gem "myrack"
+ puts "FAIL"
+ RUBY
+
+ expect(stdboth).not_to include "FAIL"
+ expect(err).to match(/private method [`']gem'/)
+ end
+
+ it "keeps Kernel#require private" do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+
+ ruby <<-RUBY, raise_on_error: false
+ require "bundler/setup"
+ Object.new.require "myrack"
+ puts "FAIL"
+ RUBY
+
+ expect(stdboth).not_to include "FAIL"
+ expect(err).to match(/private method [`']require'/)
+ end
+
+ it "memoizes initial set of specs when requiring bundler/setup, so that even if further code mutates dependencies, Bundler.definition.specs is not affected" do
+ install_gemfile <<~G
+ source "https://gem.repo1"
+ gem "yard"
+ gem "myrack", :group => :test
+ G
+
+ ruby <<-RUBY, raise_on_error: false
+ require "bundler/setup"
+ Bundler.require(:test).select! {|d| (d.groups & [:test]).any? }
+ puts Bundler.definition.specs.map(&:name).join(", ")
+ RUBY
+
+ expect(out).to include("myrack, yard")
+ end
+
+ it "does not cause double loads when higher versions of default gems are activated before bundler" do
+ build_repo2 do
+ build_gem "json", "999.999.999" do |s|
+ s.write "lib/json.rb", <<~RUBY
+ module JSON
+ VERSION = "999.999.999"
+ end
+ RUBY
+ end
+ end
+
+ system_gems "json-999.999.999", gem_repo: gem_repo2
+
+ install_gemfile "source 'https://gem.repo1'"
+ ruby <<-RUBY
+ require "json"
+ require "bundler/setup"
+ require "json"
+ RUBY
+
+ expect(err).to be_empty
+ end
+ end
+
+ it "does not undo the Kernel.require decorations", rubygems: ">= 3.4.6" do
+ install_gemfile "source 'https://gem.repo1'"
+ script = bundled_app("bin/script")
+ create_file(script, <<~RUBY)
+ module Kernel
+ module_function
+
+ alias_method :require_before_extra_monkeypatches, :require
+
+ def require(path)
+ puts "requiring \#{path} used the monkeypatch"
+
+ require_before_extra_monkeypatches(path)
+ end
+ end
+
+ require "bundler/setup"
+
+ require "foo"
+ RUBY
+
+ sys_exec "#{Gem.ruby} #{script}", raise_on_error: false
+ expect(out).to include("requiring foo used the monkeypatch")
+ end
+
+ it "performs an automatic bundle install" do
+ build_repo4 do
+ build_gem "myrack", "1.0.0"
+ end
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :group => :test
+ G
+
+ bundle_config "auto_install 1"
+
+ ruby <<-RUBY, artifice: "compact_index"
+ require 'bundler/setup'
+ RUBY
+ expect(err).to be_empty
+ expect(out).to include("Installing myrack 1.0.0")
+ end
+
+ context "in a read-only filesystem" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo4"
+ G
+
+ lockfile <<-L
+ GEM
+ remote: https://gem.repo4/
+
+ PLATFORMS
+ x86_64-darwin-19
+
+ DEPENDENCIES
+
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ L
+ end
+
+ it "should fail loudly if the lockfile platforms don't include the current platform" do
+ simulate_platform "x86_64-linux" do
+ ruby <<-RUBY, raise_on_error: false, env: { "BUNDLER_SPEC_READ_ONLY" => "true", "BUNDLER_FORCE_TTY" => "true" }
+ require "bundler/setup"
+ RUBY
+ end
+
+ expect(err).to include("Your lockfile is missing the current platform, but can't be updated because file system is read-only")
+ end
+ end
+end
diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb
new file mode 100644
index 0000000000..27ddc6a771
--- /dev/null
+++ b/spec/bundler/spec_helper.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+require "psych"
+require "bundler/vendored_fileutils"
+require "bundler/vendored_uri"
+require "digest"
+
+if File.expand_path(__FILE__) =~ %r{([^\w/\.:\-])}
+ abort "The bundler specs cannot be run from a path that contains special characters (particularly #{$1.inspect})"
+end
+
+# Bundler CLI will have different help text depending on whether any of these
+# variables is set, since the `-e` flag `bundle gem` with require an explicit
+# value if they are not set, but will use their value by default if set. So make
+# sure they are `nil` before loading bundler to get a consistent help text,
+# since some tests rely on that.
+ENV["EDITOR"] = nil
+ENV["VISUAL"] = nil
+ENV["BUNDLER_EDITOR"] = nil
+require "bundler"
+
+# If we use shared GEM_HOME and install multiple versions, it may cause
+# unexpected test failures.
+gem "diff-lcs", "< 2.0"
+
+require "rspec/core"
+require "rspec/expectations"
+require "rspec/mocks"
+require "rspec/support/differ"
+gem "rubygems-generate_index"
+require "rubygems/indexer"
+
+require_relative "support/builders"
+require_relative "support/checksums"
+require_relative "support/filters"
+require_relative "support/helpers"
+require_relative "support/indexes"
+require_relative "support/matchers"
+require_relative "support/permissions"
+require_relative "support/platforms"
+require_relative "support/shards"
+
+begin
+ raise LoadError if File.exist?(File.expand_path("../../lib/bundler/bundler.gemspec", __dir__))
+
+ gem "simplecov_json_formatter"
+ require "simplecov"
+
+ SimpleCov.start do
+ command_name "bundler:#{Process.pid}"
+ root File.expand_path("../bundler", __dir__)
+ coverage_dir File.expand_path("../coverage", __dir__)
+
+ add_filter "/spec/"
+ add_filter "/test/"
+ add_filter "/lib/rubygems/"
+ add_filter "/lib/bundler/vendor/"
+ add_filter "/tool/"
+ add_filter "/tmp/"
+ add_filter ".gemspec"
+ end
+
+ SimpleCov.print_error_status = false
+ SimpleCov.at_exit do
+ $stdout = File.open(File::NULL, "w")
+ SimpleCov.result.format!
+ ensure
+ $stdout = STDOUT
+ end
+rescue LoadError
+ # SimpleCov is not installed
+end
+
+$debug = false
+
+module Gem
+ def self.ruby=(ruby)
+ @ruby = ruby
+ end
+end
+
+RSpec.configure do |config|
+ config.include Spec::Builders
+ config.include Spec::Checksums
+ config.include Spec::Helpers
+ config.include Spec::Indexes
+ config.include Spec::Matchers
+ config.include Spec::Path
+ config.include Spec::Platforms
+ config.include Spec::Permissions
+ config.include Spec::Shards
+
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = ".rspec_status"
+
+ config.silence_filter_announcements = !ENV["TEST_ENV_NUMBER"].nil?
+
+ config.backtrace_exclusion_patterns <<
+ %r{./spec/(spec_helper\.rb|support/.+)}
+
+ config.disable_monkey_patching!
+
+ # Since failures cause us to keep a bunch of long strings in memory, stop
+ # once we have a large number of failures (indicative of core pieces of
+ # bundler being broken) so that running the full test suite doesn't take
+ # forever due to memory constraints
+ config.fail_fast ||= 25 if ENV["CI"]
+
+ config.bisect_runner = :shell
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+
+ c.max_formatted_output_length = 1000
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.allow_message_expectations_on_nil = false
+ end
+
+ config.before :suite do
+ Gem.ruby = ENV["RUBY"] if ENV["RUBY"]
+
+ require_relative "support/rubygems_ext"
+ Spec::Rubygems.test_setup
+
+ # Disable retry delays in tests to speed them up
+ Bundler::Retry.default_base_delay = 0
+
+ # Simulate bundler has not yet been loaded
+ ENV.replace(ENV.to_hash.delete_if {|k, _v| k.start_with?(Bundler::EnvironmentPreserver::BUNDLER_PREFIX) })
+
+ ENV["BUNDLER_SPEC_RUN"] = "true"
+ ENV["BUNDLE_USER_CONFIG"] = ENV["BUNDLE_USER_CACHE"] = ENV["BUNDLE_USER_PLUGIN"] = nil
+ ENV["BUNDLE_APP_CONFIG"] = nil
+ ENV["BUNDLE_SILENCE_ROOT_WARNING"] = nil
+ ENV["RUBYGEMS_GEMDEPS"] = nil
+ ENV["XDG_CONFIG_HOME"] = nil
+ ENV["XDG_CACHE_HOME"] = nil
+ ENV["GEMRC"] = nil
+
+ # Prevent tests from modifying the user's global git config.
+ # GIT_CONFIG_GLOBAL and GIT_CONFIG_NOSYSTEM are available since Git 2.32.
+ git_version = `git --version`[/(\d+\.\d+\.\d+)/, 1]
+ if Gem::Version.new(git_version) >= Gem::Version.new("2.32")
+ ENV["GIT_CONFIG_GLOBAL"] = File.join(ENV["HOME"], ".gitconfig")
+ ENV["GIT_CONFIG_NOSYSTEM"] = "1"
+ end
+
+ # Don't wrap output in tests
+ ENV["THOR_COLUMNS"] = "10000"
+
+ extend(Spec::Builders)
+
+ build_repo1
+
+ reset!
+ end
+
+ config.around :each do |example|
+ default_system_gems
+
+ with_gem_path_as(system_gem_path) do
+ Bundler.ui.silence { example.run }
+
+ all_output = all_commands_output
+ if example.exception && !all_output.empty?
+ message = all_output + "\n" + example.exception.message
+ (class << example.exception; self; end).send(:define_method, :message) do
+ message
+ end
+ end
+ end
+ ensure
+ reset!
+ end
+
+ Spec::Shards::EXAMPLE_MAPPINGS.each do |tag, file_paths|
+ file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") })
+
+ config.define_derived_metadata(file_path: file_pattern) do |metadata|
+ metadata[tag] = true
+ end
+ end
+
+ config.before(:context) do |example|
+ metadata = example.class.metadata
+ if metadata[:type] != :aruba && !metadata[:realworld] && metadata.keys.none? {|k| Spec::Shards::EXAMPLE_MAPPINGS.keys.include?(k) }
+ warn "#{metadata[:file_path]} is not assigned to any shard. see spec/support/shards.rb for details."
+ end
+ end unless Spec::Path.ruby_core?
+end
diff --git a/spec/bundler/support/activate.rb b/spec/bundler/support/activate.rb
new file mode 100644
index 0000000000..143b77833d
--- /dev/null
+++ b/spec/bundler/support/activate.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require "rubygems"
+Gem.instance_variable_set(:@ruby, ENV["RUBY"]) if ENV["RUBY"]
+
+require_relative "path"
+bundler_gemspec = Spec::Path.loaded_gemspec
+bundler_gemspec.instance_variable_set(:@full_gem_path, Spec::Path.source_root.to_s)
+bundler_gemspec.activate if bundler_gemspec.respond_to?(:activate)
diff --git a/spec/bundler/support/artifice/compact_index.rb b/spec/bundler/support/artifice/compact_index.rb
new file mode 100644
index 0000000000..ebc4d0ae5b
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexAPI)
diff --git a/spec/bundler/support/artifice/compact_index_api_missing.rb b/spec/bundler/support/artifice/compact_index_api_missing.rb
new file mode 100644
index 0000000000..f771f7d1f0
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_api_missing.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexApiMissing < CompactIndexAPI
+ get "/fetch/actual/gem/:id" do
+ halt 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexApiMissing)
diff --git a/spec/bundler/support/artifice/compact_index_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_basic_authentication.rb
new file mode 100644
index 0000000000..b9115cdd86
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_basic_authentication.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexBasicAuthentication < CompactIndexAPI
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexBasicAuthentication)
diff --git a/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb
new file mode 100644
index 0000000000..83b147d2ae
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_checksum_mismatch.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexChecksumMismatch < CompactIndexAPI
+ get "/versions" do
+ headers "Repr-Digest" => "sha-256=:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0=:"
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ body "content does not match the checksum"
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexChecksumMismatch)
diff --git a/spec/bundler/support/artifice/compact_index_concurrent_download.rb b/spec/bundler/support/artifice/compact_index_concurrent_download.rb
new file mode 100644
index 0000000000..5d55b8a72b
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_concurrent_download.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexConcurrentDownload < CompactIndexAPI
+ get "/versions" do
+ versions = File.join(Bundler.rubygems.user_home, ".bundle", "cache", "compact_index",
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions")
+
+ # Verify the original content hasn't been deleted, e.g. on a retry
+ data = File.binread(versions)
+ data == "created_at" || raise("Original file should be present with expected content")
+
+ # Verify this is only requested once for a partial download
+ env["HTTP_RANGE"] == "bytes=#{data.bytesize - 1}-" || raise("Missing Range header for expected partial download")
+
+ # Overwrite the file in parallel, which should be then overwritten
+ # after a successful download to prevent corruption
+ File.open(versions, "w") {|f| f.puts "another process" }
+
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexConcurrentDownload)
diff --git a/spec/bundler/support/artifice/compact_index_cooldown.rb b/spec/bundler/support/artifice/compact_index_cooldown.rb
new file mode 100644
index 0000000000..85e3173c98
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_cooldown.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_cooldown"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexCooldownAPI)
diff --git a/spec/bundler/support/artifice/compact_index_creds_diff_host.rb b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb
new file mode 100644
index 0000000000..282e9c8961
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_creds_diff_host.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexCredsDiffHost < CompactIndexAPI
+ helpers do
+ def auth
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
+ end
+
+ def authorized?
+ auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass]
+ end
+
+ def protected!
+ return if authorized?
+ response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth")
+ throw(:halt, [401, "Not authorized\n"])
+ end
+ end
+
+ before do
+ protected! unless request.path_info.include?("/no/creds/")
+ end
+
+ get "/gems/:id" do
+ redirect "http://diffhost.test/no/creds/#{params[:id]}"
+ end
+
+ get "/no/creds/:id" do
+ if request.host.include?("diffhost") && !auth.provided?
+ File.binread("#{gem_repo1}/gems/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexCredsDiffHost)
diff --git a/spec/bundler/support/artifice/compact_index_etag_match.rb b/spec/bundler/support/artifice/compact_index_etag_match.rb
new file mode 100644
index 0000000000..6c62166051
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_etag_match.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexEtagMatch < CompactIndexAPI
+ get "/versions" do
+ raise ArgumentError, "ETag header should be present" unless env["HTTP_IF_NONE_MATCH"]
+ headers "ETag" => env["HTTP_IF_NONE_MATCH"]
+ status 304
+ body ""
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexEtagMatch)
diff --git a/spec/bundler/support/artifice/compact_index_extra.rb b/spec/bundler/support/artifice/compact_index_extra.rb
new file mode 100644
index 0000000000..cd41b3ecca
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtra)
diff --git a/spec/bundler/support/artifice/compact_index_extra_api.rb b/spec/bundler/support/artifice/compact_index_extra_api.rb
new file mode 100644
index 0000000000..8b9d304ab4
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra_api.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra_api"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtraApi)
diff --git a/spec/bundler/support/artifice/compact_index_extra_api_missing.rb b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb
new file mode 100644
index 0000000000..df6ede584c
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra_api_missing.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra_api"
+
+class CompactIndexExtraAPIMissing < CompactIndexExtraApi
+ get "/extra/fetch/actual/gem/:id" do
+ if params[:id] == "missing-1.0.gemspec.rz"
+ halt 404
+ else
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtraAPIMissing)
diff --git a/spec/bundler/support/artifice/compact_index_extra_missing.rb b/spec/bundler/support/artifice/compact_index_extra_missing.rb
new file mode 100644
index 0000000000..255c89afdb
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_extra_missing.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index_extra"
+
+class CompactIndexExtraMissing < CompactIndexExtra
+ get "/extra/fetch/actual/gem/:id" do
+ if params[:id] == "missing-1.0.gemspec.rz"
+ halt 404
+ else
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexExtraMissing)
diff --git a/spec/bundler/support/artifice/compact_index_forbidden.rb b/spec/bundler/support/artifice/compact_index_forbidden.rb
new file mode 100644
index 0000000000..18c30ed9a2
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_forbidden.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexForbidden < CompactIndexAPI
+ get "/versions" do
+ halt 403
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexForbidden)
diff --git a/spec/bundler/support/artifice/compact_index_host_redirect.rb b/spec/bundler/support/artifice/compact_index_host_redirect.rb
new file mode 100644
index 0000000000..4f82bf3812
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_host_redirect.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexHostRedirect < CompactIndexAPI
+ get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do
+ redirect "http://bundler.localgemserver.test#{request.path_info}"
+ end
+
+ get "/versions" do
+ status 404
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexHostRedirect)
diff --git a/spec/bundler/support/artifice/compact_index_mirror_down.rb b/spec/bundler/support/artifice/compact_index_mirror_down.rb
new file mode 100644
index 0000000000..88983c715d
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_mirror_down.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+require_relative "helpers/artifice"
+require_relative "helpers/rack_request"
+
+module Artifice
+ module Net
+ class HTTPMirrorDown < HTTP
+ def connect
+ raise SocketError if address == "gem.mirror"
+
+ super
+ end
+ end
+
+ HTTP.endpoint = CompactIndexAPI
+ end
+
+ replace_net_http(Net::HTTPMirrorDown)
+end
diff --git a/spec/bundler/support/artifice/compact_index_no_checksums.rb b/spec/bundler/support/artifice/compact_index_no_checksums.rb
new file mode 100644
index 0000000000..ecb7fc7d7c
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_no_checksums.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexNoChecksums < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ gem.versions.map(&:number).join("\n")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexNoChecksums)
diff --git a/spec/bundler/support/artifice/compact_index_no_gem.rb b/spec/bundler/support/artifice/compact_index_no_gem.rb
new file mode 100644
index 0000000000..71f6629688
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_no_gem.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexNoGem < CompactIndexAPI
+ get "/gems/:id" do
+ halt 500
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexNoGem)
diff --git a/spec/bundler/support/artifice/compact_index_partial_update.rb b/spec/bundler/support/artifice/compact_index_partial_update.rb
new file mode 100644
index 0000000000..f111d91ef9
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_partial_update.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexPartialUpdate < CompactIndexAPI
+ # Stub the server to never return 304s. This simulates the behaviour of
+ # Fastly / Rubygems ignoring ETag headers.
+ def not_modified?(_checksum)
+ false
+ end
+
+ get "/versions" do
+ cached_versions_path = File.join(
+ Bundler.rubygems.user_home, ".bundle", "cache", "compact_index",
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+
+ # Verify a cached copy of the versions file exists
+ unless File.binread(cached_versions_path).start_with?("created_at: ")
+ raise("Cached versions file should be present and have content")
+ end
+
+ # Verify that a partial request is made, starting from the index of the
+ # final byte of the cached file.
+ unless env["HTTP_RANGE"] == "bytes=#{File.binread(cached_versions_path).bytesize - 1}-"
+ raise("Range header should be present, and start from the index of the final byte of the cache. #{env["HTTP_RANGE"].inspect}")
+ end
+
+ etag_response do
+ # Return the exact contents of the cache.
+ File.binread(cached_versions_path)
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPartialUpdate)
diff --git a/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb
new file mode 100644
index 0000000000..ac04336636
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_partial_update_bad_digest.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+# The purpose of this Artifice is to test that an incremental response is invalidated
+# and a second request is issued for the full content.
+class CompactIndexPartialUpdateBadDigest < CompactIndexAPI
+ def partial_update_bad_digest
+ response_body = yield
+ if request.env["HTTP_RANGE"]
+ headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest("wrong digest on ranged request")}:"
+ else
+ headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:"
+ end
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ requested_range_for(response_body)
+ end
+
+ get "/versions" do
+ partial_update_bad_digest do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents([], calculate_info_checksums: true)
+ end
+ end
+
+ get "/info/:name" do
+ partial_update_bad_digest do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPartialUpdateBadDigest)
diff --git a/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb
new file mode 100644
index 0000000000..99bae039f0
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_partial_update_no_digest_not_incremental.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+# The purpose of this Artifice is to test that an incremental response is ignored
+# when the digest is not present to verify that the partial response is valid.
+class CompactIndexPartialUpdateNoDigestNotIncremental < CompactIndexAPI
+ def partial_update_no_digest
+ response_body = yield
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ requested_range_for(response_body)
+ end
+
+ get "/versions" do
+ partial_update_no_digest do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ lines = file.contents([], calculate_info_checksums: true).split("\n")
+ name, versions, checksum = lines.last.split(" ")
+
+ # shuffle versions so new versions are not appended to the end
+ [*lines[0..-2], [name, versions.split(",").reverse.join(","), checksum].join(" ")].join("\n")
+ end
+ end
+
+ get "/info/:name" do
+ partial_update_no_digest do
+ gem = gems.find {|g| g.name == params[:name] }
+ lines = CompactIndex.info(gem ? gem.versions : []).split("\n")
+
+ # shuffle versions so new versions are not appended to the end
+ [lines.first, lines.last, *lines[1..-2]].join("\n")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPartialUpdateNoDigestNotIncremental)
diff --git a/spec/bundler/support/artifice/compact_index_precompiled_before.rb b/spec/bundler/support/artifice/compact_index_precompiled_before.rb
new file mode 100644
index 0000000000..b5f72f546a
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_precompiled_before.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexPrecompiledBefore < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ move_ruby_variant_to_the_end(CompactIndex.info(gem ? gem.versions : []))
+ end
+ end
+
+ private
+
+ def move_ruby_variant_to_the_end(response)
+ lines = response.split("\n")
+ ruby = lines.find {|line| /\A\d+\.\d+\.\d* \|/.match(line) }
+ lines.delete(ruby)
+ lines.push(ruby).join("\n")
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexPrecompiledBefore)
diff --git a/spec/bundler/support/artifice/compact_index_range_ignored.rb b/spec/bundler/support/artifice/compact_index_range_ignored.rb
new file mode 100644
index 0000000000..2303682c1f
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_range_ignored.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRangeIgnored < CompactIndexAPI
+ # Stub the server to not return 304 so that we don't bypass all the logic
+ def not_modified?(_checksum)
+ false
+ end
+
+ get "/versions" do
+ cached_versions_path = File.join(
+ Bundler.rubygems.user_home, ".bundle", "cache", "compact_index",
+ "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "versions"
+ )
+
+ # Verify a cached copy of the versions file exists
+ unless File.binread(cached_versions_path).size > 0
+ raise("Cached versions file should be present and have content")
+ end
+
+ # Verify that a partial request is made, starting from the index of the
+ # final byte of the cached file.
+ unless env.delete("HTTP_RANGE")
+ raise("Expected client to write the full response on the first try")
+ end
+
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRangeIgnored)
diff --git a/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb
new file mode 100644
index 0000000000..8a7c4b79b0
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_range_not_satisfiable.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRangeNotSatisfiable < CompactIndexAPI
+ get "/versions" do
+ if env["HTTP_RANGE"]
+ status 416
+ else
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+ end
+
+ get "/info/:name" do
+ if env["HTTP_RANGE"]
+ status 416
+ else
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRangeNotSatisfiable)
diff --git a/spec/bundler/support/artifice/compact_index_rate_limited.rb b/spec/bundler/support/artifice/compact_index_rate_limited.rb
new file mode 100644
index 0000000000..4495491635
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_rate_limited.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRateLimited < CompactIndexAPI
+ class RequestCounter
+ def self.queue
+ @queue ||= Thread::Queue.new
+ end
+
+ def self.size
+ @queue.size
+ end
+
+ def self.enq(name)
+ @queue.enq(name)
+ end
+
+ def self.deq
+ @queue.deq
+ end
+ end
+
+ configure do
+ RequestCounter.queue
+ end
+
+ get "/info/:name" do
+ RequestCounter.enq(params[:name])
+
+ begin
+ if RequestCounter.size == 1
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ else
+ status 429
+ end
+ ensure
+ RequestCounter.deq
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRateLimited)
diff --git a/spec/bundler/support/artifice/compact_index_redirects.rb b/spec/bundler/support/artifice/compact_index_redirects.rb
new file mode 100644
index 0000000000..f7ba393239
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_redirects.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexRedirect < CompactIndexAPI
+ get "/fetch/actual/gem/:id" do
+ redirect "/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/versions" do
+ status 404
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexRedirect)
diff --git a/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb
new file mode 100644
index 0000000000..96259385e7
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_strict_basic_authentication.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexStrictBasicAuthentication < CompactIndexAPI
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+
+ # Only accepts password == "password"
+ unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz"
+ halt 401, "Authentication failed"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexStrictBasicAuthentication)
diff --git a/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb
new file mode 100644
index 0000000000..15850599b6
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_wrong_dependencies.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexWrongDependencies < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ gem.versions.each {|gv| gv.dependencies.clear } if gem
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexWrongDependencies)
diff --git a/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb
new file mode 100644
index 0000000000..9bd2ca0a9d
--- /dev/null
+++ b/spec/bundler/support/artifice/compact_index_wrong_gem_checksum.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "helpers/compact_index"
+
+class CompactIndexWrongGemChecksum < CompactIndexAPI
+ get "/info/:name" do
+ etag_response do
+ name = params[:name]
+ gem = gems.find {|g| g.name == name }
+ # This generates the hexdigest "2222222222222222222222222222222222222222222222222222222222222222"
+ checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" }
+ versions = gem ? gem.versions : []
+ versions.each {|v| v.checksum = checksum }
+ CompactIndex.info(versions)
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(CompactIndexWrongGemChecksum)
diff --git a/spec/bundler/support/artifice/endpoint.rb b/spec/bundler/support/artifice/endpoint.rb
new file mode 100644
index 0000000000..15242a7942
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(Endpoint)
diff --git a/spec/bundler/support/artifice/endpoint_500.rb b/spec/bundler/support/artifice/endpoint_500.rb
new file mode 100644
index 0000000000..9dd373bbf6
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_500.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "../path"
+
+$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths)
+
+require "sinatra/base"
+
+class Endpoint500 < Sinatra::Base
+ before do
+ halt 500
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(Endpoint500)
diff --git a/spec/bundler/support/artifice/endpoint_api_forbidden.rb b/spec/bundler/support/artifice/endpoint_api_forbidden.rb
new file mode 100644
index 0000000000..6bdc5896d6
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_api_forbidden.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointApiForbidden < Endpoint
+ get "/api/v1/dependencies" do
+ halt 403
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointApiForbidden)
diff --git a/spec/bundler/support/artifice/endpoint_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_basic_authentication.rb
new file mode 100644
index 0000000000..e8e3569e63
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_basic_authentication.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointBasicAuthentication < Endpoint
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointBasicAuthentication)
diff --git a/spec/bundler/support/artifice/endpoint_creds_diff_host.rb b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb
new file mode 100644
index 0000000000..9cbb4de61a
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_creds_diff_host.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointCredsDiffHost < Endpoint
+ helpers do
+ def auth
+ @auth ||= Rack::Auth::Basic::Request.new(request.env)
+ end
+
+ def authorized?
+ auth.provided? && auth.basic? && auth.credentials && auth.credentials == %w[user pass]
+ end
+
+ def protected!
+ return if authorized?
+ response["WWW-Authenticate"] = %(Basic realm="Testing HTTP Auth")
+ throw(:halt, [401, "Not authorized\n"])
+ end
+ end
+
+ before do
+ protected! unless request.path_info.include?("/no/creds/")
+ end
+
+ get "/gems/:id" do
+ redirect "http://diffhost.test/no/creds/#{params[:id]}"
+ end
+
+ get "/no/creds/:id" do
+ if request.host.include?("diffhost") && !auth.provided?
+ File.binread("#{gem_repo1}/gems/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointCredsDiffHost)
diff --git a/spec/bundler/support/artifice/endpoint_extra.rb b/spec/bundler/support/artifice/endpoint_extra.rb
new file mode 100644
index 0000000000..021fd435fe
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_extra.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointExtra < Endpoint
+ get "/extra/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo2}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo2}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo2}/gems/#{params[:id]}")
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointExtra)
diff --git a/spec/bundler/support/artifice/endpoint_extra_api.rb b/spec/bundler/support/artifice/endpoint_extra_api.rb
new file mode 100644
index 0000000000..a965af6e73
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_extra_api.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointExtraApi < Endpoint
+ get "/extra/api/v1/dependencies" do
+ deps = dependencies_for(params[:gems], gem_repo4)
+ Marshal.dump(deps)
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo4}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo4}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo4}/gems/#{params[:id]}")
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointExtraApi)
diff --git a/spec/bundler/support/artifice/endpoint_extra_missing.rb b/spec/bundler/support/artifice/endpoint_extra_missing.rb
new file mode 100644
index 0000000000..73e2defb32
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_extra_missing.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_extra"
+
+class EndpointExtraMissing < EndpointExtra
+ get "/extra/fetch/actual/gem/:id" do
+ if params[:id] == "missing-1.0.gemspec.rz"
+ halt 404
+ else
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointExtraMissing)
diff --git a/spec/bundler/support/artifice/endpoint_fallback.rb b/spec/bundler/support/artifice/endpoint_fallback.rb
new file mode 100644
index 0000000000..742e563f07
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_fallback.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointFallback < Endpoint
+ DEPENDENCY_LIMIT = 60
+
+ get "/api/v1/dependencies" do
+ if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT
+ Marshal.dump(dependencies_for(params[:gems]))
+ else
+ halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointFallback)
diff --git a/spec/bundler/support/artifice/endpoint_host_redirect.rb b/spec/bundler/support/artifice/endpoint_host_redirect.rb
new file mode 100644
index 0000000000..6ce51bed93
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_host_redirect.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointHostRedirect < Endpoint
+ get "/fetch/actual/gem/:id", host_name: "localgemserver.test" do
+ redirect "http://bundler.localgemserver.test#{request.path_info}"
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointHostRedirect)
diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/endpoint_marshal_fail.rb
new file mode 100644
index 0000000000..74ce321de6
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_marshal_fail.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_marshal_fail"
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointMarshalFail)
diff --git a/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb
new file mode 100644
index 0000000000..ea4cfbe965
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_marshal_fail_basic_authentication.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_marshal_fail"
+
+class EndpointMarshalFailBasicAuthentication < EndpointMarshalFail
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointMarshalFailBasicAuthentication)
diff --git a/spec/bundler/support/artifice/endpoint_mirror_source.rb b/spec/bundler/support/artifice/endpoint_mirror_source.rb
new file mode 100644
index 0000000000..fed7a746b9
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_mirror_source.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointMirrorSource < Endpoint
+ get "/gems/:id" do
+ if request.env["HTTP_X_GEMFILE_SOURCE"] == "https://server.example.org/" && request.env["HTTP_USER_AGENT"].start_with?("bundler")
+ File.binread("#{gem_repo1}/gems/#{params[:id]}")
+ else
+ halt 500
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointMirrorSource)
diff --git a/spec/bundler/support/artifice/endpoint_redirect.rb b/spec/bundler/support/artifice/endpoint_redirect.rb
new file mode 100644
index 0000000000..84f546ba9d
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_redirect.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointRedirect < Endpoint
+ get "/fetch/actual/gem/:id" do
+ redirect "/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/api/v1/dependencies" do
+ status 404
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointRedirect)
diff --git a/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb
new file mode 100644
index 0000000000..dff360c5c5
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_strict_basic_authentication.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint"
+
+class EndpointStrictBasicAuthentication < Endpoint
+ before do
+ unless env["HTTP_AUTHORIZATION"]
+ halt 401, "Authentication info not supplied"
+ end
+
+ # Only accepts password == "password"
+ unless env["HTTP_AUTHORIZATION"] == "Basic dXNlcjpwYXNz"
+ halt 401, "Authentication failed"
+ end
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointStrictBasicAuthentication)
diff --git a/spec/bundler/support/artifice/endpoint_timeout.rb b/spec/bundler/support/artifice/endpoint_timeout.rb
new file mode 100644
index 0000000000..86b793e499
--- /dev/null
+++ b/spec/bundler/support/artifice/endpoint_timeout.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "helpers/endpoint_fallback"
+
+class EndpointTimeout < EndpointFallback
+ SLEEP_TIMEOUT = 3
+
+ get "/api/v1/dependencies" do
+ sleep(SLEEP_TIMEOUT)
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(EndpointTimeout)
diff --git a/spec/bundler/support/artifice/fail.rb b/spec/bundler/support/artifice/fail.rb
new file mode 100644
index 0000000000..5ddbc4e590
--- /dev/null
+++ b/spec/bundler/support/artifice/fail.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_net_http"
+
+class Fail < Gem::Net::HTTP
+ # Gem::Net::HTTP uses a @newimpl instance variable to decide whether
+ # to use a legacy implementation. Since we are subclassing
+ # Gem::Net::HTTP, we must set it
+ @newimpl = true
+
+ def request(req, body = nil, &block)
+ raise(exception(req))
+ end
+
+ # Ensure we don't start a connect here
+ def connect
+ end
+
+ def exception(req)
+ Errno::ENETUNREACH.new("host down: Bundler spec artifice fail! #{req["PATH_INFO"]}")
+ end
+end
+
+require_relative "helpers/artifice"
+
+# Replace Gem::Net::HTTP with our failing subclass
+Artifice.replace_net_http(::Fail)
diff --git a/spec/bundler/support/artifice/helpers/artifice.rb b/spec/bundler/support/artifice/helpers/artifice.rb
new file mode 100644
index 0000000000..788268295c
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/artifice.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# This module was initially borrowed from https://github.com/wycats/artifice
+module Artifice
+ # Activate Artifice with a particular Rack endpoint.
+ #
+ # Calling this method will replace the Gem::Net::HTTP system
+ # with a replacement that routes all requests to the
+ # Rack endpoint.
+ #
+ # @param [#call] endpoint A valid Rack endpoint
+ def self.activate_with(endpoint)
+ require_relative "rack_request"
+
+ Net::HTTP.endpoint = endpoint
+ replace_net_http(Artifice::Net::HTTP)
+ end
+
+ # Deactivate the Artifice replacement.
+ def self.deactivate
+ replace_net_http(::Gem::Net::HTTP)
+ end
+
+ def self.replace_net_http(value)
+ ::Gem::Net.class_eval do
+ remove_const(:HTTP)
+ const_set(:HTTP, value)
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index.rb b/spec/bundler/support/artifice/helpers/compact_index.rb
new file mode 100644
index 0000000000..e684aa8628
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require_relative "endpoint"
+
+$LOAD_PATH.unshift Spec::Path.tmp_root.join("compact_index/lib").to_s
+require "compact_index"
+require "digest"
+
+class CompactIndexAPI < Endpoint
+ helpers do
+ include Spec::Path
+
+ def load_spec(name, version, platform, gem_repo)
+ full_name = "#{name}-#{version}"
+ full_name += "-#{platform}" if platform != "ruby"
+ Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz"))))
+ end
+
+ def etag_response
+ response_body = yield
+ etag = Digest::MD5.hexdigest(response_body)
+ headers "ETag" => quote(etag)
+ return if not_modified?(etag)
+ headers "Repr-Digest" => "sha-256=:#{Digest::SHA256.base64digest(response_body)}:"
+ headers "Surrogate-Control" => "max-age=2592000, stale-while-revalidate=60"
+ content_type "text/plain"
+ requested_range_for(response_body)
+ rescue StandardError => e
+ puts e
+ puts e.backtrace
+ raise
+ end
+
+ def not_modified?(etag)
+ etags = parse_etags(request.env["HTTP_IF_NONE_MATCH"])
+
+ return unless etags.include?(etag)
+ status 304
+ body ""
+ end
+
+ def requested_range_for(response_body)
+ ranges = Rack::Utils.get_byte_ranges(env["HTTP_RANGE"], response_body.bytesize)
+
+ if ranges
+ status 206
+ body ranges.map! {|range| slice_body(response_body, range) }.join
+ else
+ status 200
+ body response_body
+ end
+ end
+
+ def quote(string)
+ %("#{string}")
+ end
+
+ def parse_etags(value)
+ value ? value.split(/, ?/).select {|s| s.sub!(/"(.*)"/, '\1') } : []
+ end
+
+ def slice_body(body, range)
+ body.byteslice(range)
+ end
+
+ def gems(gem_repo = default_gem_repo)
+ @gems ||= {}
+ @gems[gem_repo] ||= begin
+ specs = Bundler::Deprecate.skip_during do
+ %w[specs.4.8 prerelease_specs.4.8].flat_map do |filename|
+ spec_index = gem_repo.join(filename)
+ next [] unless File.exist?(spec_index)
+
+ Marshal.load(File.binread(spec_index)).map do |name, version, platform|
+ load_spec(name, version, platform, gem_repo)
+ end
+ end
+ end
+
+ specs.group_by(&:name).map do |name, versions|
+ gem_versions = versions.map do |spec|
+ deps = spec.runtime_dependencies.map do |d|
+ reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ")
+ CompactIndex::Dependency.new(d.name, reqs)
+ end
+ begin
+ checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do
+ Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest
+ end
+ rescue StandardError
+ checksum = nil
+ end
+ build_gem_version(spec, deps, checksum)
+ end
+ CompactIndex::Gem.new(name, gem_versions)
+ end
+ end
+ end
+
+ def build_gem_version(spec, deps, checksum)
+ CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil,
+ deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s)
+ end
+ end
+
+ get "/names" do
+ etag_response do
+ CompactIndex.names(gems.map(&:name))
+ end
+ end
+
+ get "/versions" do
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems)
+ file.contents
+ end
+ end
+
+ get "/info/:name" do
+ etag_response do
+ gem = gems.find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb
new file mode 100644
index 0000000000..9920fd2c95
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index_cooldown.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "compact_index"
+
+class CompactIndexCooldownAPI < CompactIndexAPI
+ helpers do
+ def build_gem_version(spec, deps, checksum)
+ created_at = spec.date&.utc&.iso8601
+ CompactIndex::GemVersionV2.new(spec.version.version, spec.platform.to_s, checksum, nil,
+ deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s, created_at)
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra.rb b/spec/bundler/support/artifice/helpers/compact_index_extra.rb
new file mode 100644
index 0000000000..9e742630dd
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index_extra.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "compact_index"
+
+class CompactIndexExtra < CompactIndexAPI
+ get "/extra/versions" do
+ halt 404
+ end
+
+ get "/extra/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo2}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo2}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo2}/gems/#{params[:id]}")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb
new file mode 100644
index 0000000000..d9a7d83d23
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/compact_index_extra_api.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "compact_index"
+
+class CompactIndexExtraApi < CompactIndexAPI
+ get "/extra/names" do
+ etag_response do
+ CompactIndex.names(gems(gem_repo4).map(&:name))
+ end
+ end
+
+ get "/extra/versions" do
+ etag_response do
+ file = tmp("versions.list")
+ FileUtils.rm_f(file)
+ file = CompactIndex::VersionsFile.new(file.to_s)
+ file.create(gems(gem_repo4))
+ file.contents
+ end
+ end
+
+ get "/extra/info/:name" do
+ etag_response do
+ gem = gems(gem_repo4).find {|g| g.name == params[:name] }
+ CompactIndex.info(gem ? gem.versions : [])
+ end
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo4}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo4}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo4}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo4}/gems/#{params[:id]}")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint.rb b/spec/bundler/support/artifice/helpers/endpoint.rb
new file mode 100644
index 0000000000..9590611dfe
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require_relative "../../path"
+
+$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths)
+
+require "sinatra/base"
+
+ALL_REQUESTS = [] # rubocop:disable Style/MutableConstant
+ALL_REQUESTS_MUTEX = Thread::Mutex.new
+
+at_exit do
+ if expected = ENV["BUNDLER_SPEC_ALL_REQUESTS"]
+ expected = expected.split("\n").sort
+ actual = ALL_REQUESTS.sort
+
+ unless expected == actual
+ raise "Unexpected requests!\nExpected:\n\t#{expected.join("\n\t")}\n\nActual:\n\t#{actual.join("\n\t")}"
+ end
+ end
+end
+
+class Endpoint < Sinatra::Base
+ def self.all_requests
+ @all_requests ||= []
+ end
+
+ set :raise_errors, true
+ set :show_exceptions, false
+ set :host_authorization, permitted_hosts: [".example.org", ".local", ".mirror", ".repo", ".repo1", ".repo2", ".repo3", ".repo4", ".rubygems.org", ".security", ".source", ".test", "127.0.0.1"]
+
+ def call!(*)
+ super.tap do
+ ALL_REQUESTS_MUTEX.synchronize do
+ ALL_REQUESTS << @request.url
+ end
+ end
+ end
+
+ helpers do
+ include Spec::Path
+
+ def default_gem_repo
+ if ENV["BUNDLER_SPEC_GEM_REPO"]
+ Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"])
+ else
+ case request.host
+ when "gem.repo1"
+ Spec::Path.gem_repo1
+ when "gem.repo2"
+ Spec::Path.gem_repo2
+ when "gem.repo3"
+ Spec::Path.gem_repo3
+ when "gem.repo4"
+ Spec::Path.gem_repo4
+ else
+ Spec::Path.gem_repo1
+ end
+ end
+ end
+
+ def dependencies_for(gem_names, gem_repo = default_gem_repo)
+ return [] if gem_names.nil? || gem_names.empty?
+
+ all_specs = %w[specs.4.8 prerelease_specs.4.8].map do |filename|
+ Marshal.load(File.binread(gem_repo.join(filename)))
+ end.inject(:+)
+
+ all_specs.filter_map do |name, version, platform|
+ spec = load_spec(name, version, platform, gem_repo)
+ next unless gem_names.include?(spec.name)
+ {
+ name: spec.name,
+ number: spec.version.version,
+ platform: spec.platform.to_s,
+ dependencies: spec.runtime_dependencies.map do |dep|
+ [dep.name, dep.requirement.requirements.map {|a| a.join(" ") }.join(", ")]
+ end,
+ }
+ end
+ end
+
+ def load_spec(name, version, platform, gem_repo)
+ full_name = "#{name}-#{version}"
+ full_name += "-#{platform}" if platform != "ruby"
+ Marshal.load(Bundler.rubygems.inflate(File.binread(gem_repo.join("quick/Marshal.4.8/#{full_name}.gemspec.rz"))))
+ end
+ end
+
+ get "/quick/Marshal.4.8/:id" do
+ redirect "/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/fetch/actual/gem/:id" do
+ File.binread("#{default_gem_repo}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/gems/:id" do
+ File.binread("#{default_gem_repo}/gems/#{params[:id]}")
+ end
+
+ get "/api/v1/dependencies" do
+ Marshal.dump(dependencies_for(params[:gems]))
+ end
+
+ get "/specs.4.8.gz" do
+ File.binread("#{default_gem_repo}/specs.4.8.gz")
+ end
+
+ get "/prerelease_specs.4.8.gz" do
+ File.binread("#{default_gem_repo}/prerelease_specs.4.8.gz")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint_extra.rb b/spec/bundler/support/artifice/helpers/endpoint_extra.rb
new file mode 100644
index 0000000000..ad08495b50
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint_extra.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "endpoint"
+
+class EndpointExtra < Endpoint
+ get "/extra/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/extra/specs.4.8.gz" do
+ File.binread("#{gem_repo2}/specs.4.8.gz")
+ end
+
+ get "/extra/prerelease_specs.4.8.gz" do
+ File.binread("#{gem_repo2}/prerelease_specs.4.8.gz")
+ end
+
+ get "/extra/quick/Marshal.4.8/:id" do
+ redirect "/extra/fetch/actual/gem/#{params[:id]}"
+ end
+
+ get "/extra/fetch/actual/gem/:id" do
+ File.binread("#{gem_repo2}/quick/Marshal.4.8/#{params[:id]}")
+ end
+
+ get "/extra/gems/:id" do
+ File.binread("#{gem_repo2}/gems/#{params[:id]}")
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint_fallback.rb b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb
new file mode 100644
index 0000000000..a232930b67
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint_fallback.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "endpoint"
+
+class EndpointFallback < Endpoint
+ DEPENDENCY_LIMIT = 60
+
+ get "/api/v1/dependencies" do
+ if params[:gems] && params[:gems].size <= DEPENDENCY_LIMIT
+ Marshal.dump(dependencies_for(params[:gems]))
+ else
+ halt 413, "Too many gems to resolve, please request less than #{DEPENDENCY_LIMIT} gems"
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb
new file mode 100644
index 0000000000..c409d39d99
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/endpoint_marshal_fail.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "endpoint_fallback"
+
+class EndpointMarshalFail < EndpointFallback
+ get "/api/v1/dependencies" do
+ "f0283y01hasf"
+ end
+end
diff --git a/spec/bundler/support/artifice/helpers/rack_request.rb b/spec/bundler/support/artifice/helpers/rack_request.rb
new file mode 100644
index 0000000000..05ff034463
--- /dev/null
+++ b/spec/bundler/support/artifice/helpers/rack_request.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require "rack/test"
+require_relative "../../vendored_net_http"
+
+module Artifice
+ module Net
+ # This is an internal object that can receive Rack requests
+ # to the application using the Rack::Test API
+ class RackRequest
+ include Rack::Test::Methods
+ attr_reader :app
+
+ def initialize(app)
+ @app = app
+ end
+ end
+
+ class HTTP < ::Gem::Net::HTTP
+ class << self
+ attr_accessor :endpoint
+ end
+
+ # Gem::Net::HTTP uses a @newimpl instance variable to decide whether
+ # to use a legacy implementation. Since we are subclassing
+ # Gem::Net::HTTP, we must set it
+ @newimpl = true
+
+ # We don't need to connect, so blank out this method
+ def connect
+ end
+
+ # Replace the Gem::Net::HTTP request method with a method
+ # that converts the request into a Rack request and
+ # dispatches it to the Rack endpoint.
+ #
+ # @param [Net::HTTPRequest] req A Gem::Net::HTTPRequest
+ # object, or one if its subclasses
+ # @param [optional, String, #read] body This should
+ # be sent as "rack.input". If it's a String, it will
+ # be converted to a StringIO.
+ # @return [Net::HTTPResponse]
+ #
+ # @yield [Net::HTTPResponse] If a block is provided,
+ # this method will yield the Gem::Net::HTTPResponse to
+ # it after the body is read.
+ def request(req, body = nil, &block)
+ rack_request = RackRequest.new(self.class.endpoint)
+
+ req.each_header do |header, value|
+ rack_request.header(header, value)
+ end
+
+ scheme = use_ssl? ? "https" : "http"
+ prefix = "#{scheme}://#{addr_port}"
+ body_stream_contents = req.body_stream.read if req.body_stream
+
+ response = rack_request.request("#{prefix}#{req.path}",
+ { method: req.method, input: body || req.body || body_stream_contents })
+
+ make_net_http_response(response, &block)
+ end
+
+ private
+
+ # This method takes a Rack response and creates a Gem::Net::HTTPResponse
+ # Instead of trying to mock HTTPResponse directly, we just convert
+ # the Rack response into a String that looks like a normal HTTP
+ # response and call Gem::Net::HTTPResponse.read_new
+ #
+ # @param [Array(#to_i, Hash, #each)] response a Rack response
+ # @return [Net::HTTPResponse]
+ # @yield [Net::HTTPResponse] If a block is provided, yield the
+ # response to it after the body is read
+ def make_net_http_response(response)
+ status = response.status
+ headers = response.headers
+ body = response.body
+
+ response_string = []
+ response_string << "HTTP/1.1 #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]}"
+
+ headers.each do |header, value|
+ response_string << "#{header}: #{value}"
+ end
+
+ response_string << "" << body
+
+ response_io = ::Gem::Net::BufferedIO.new(StringIO.new(response_string.join("\n")))
+ res = ::Gem::Net::HTTPResponse.read_new(response_io)
+
+ res.reading_body(response_io, true) do
+ yield res if block_given?
+ end
+
+ res
+ end
+ end
+ end
+end
diff --git a/spec/bundler/support/artifice/vcr.rb b/spec/bundler/support/artifice/vcr.rb
new file mode 100644
index 0000000000..0bf5ade8f6
--- /dev/null
+++ b/spec/bundler/support/artifice/vcr.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require_relative "../vendored_net_http"
+require_relative "../path"
+
+CASSETTE_PATH = "#{Spec::Path.spec_dir}/support/artifice/vcr_cassettes".freeze
+USED_CASSETTES_PATH = "#{Spec::Path.spec_dir}/support/artifice/used_cassettes.txt".freeze
+CASSETTE_NAME = ENV.fetch("BUNDLER_SPEC_VCR_CASSETTE_NAME") { "realworld" }
+
+class BundlerVCRHTTP < Gem::Net::HTTP
+ class RequestHandler
+ attr_reader :http, :request, :body, :response_block
+ def initialize(http, request, body = nil, &response_block)
+ @http = http
+ @request = request
+ @body = body
+ @response_block = response_block
+ end
+
+ def handle_request
+ handler = self
+ request.instance_eval do
+ @__vcr_request_handler = handler
+ end
+
+ File.open(USED_CASSETTES_PATH, "a+") do |f|
+ f.puts request_pair_paths.map {|path| Pathname.new(path).relative_path_from(Spec::Path.git_root).to_s }.join("\n")
+ end
+
+ if recorded_response?
+ recorded_response
+ else
+ record_response
+ end
+ end
+
+ def recorded_response?
+ return true if ENV["BUNDLER_SPEC_PRE_RECORDED"]
+ request_pair_paths.all? {|f| File.exist?(f) }
+ end
+
+ def recorded_response
+ File.open(request_pair_paths.last, "rb:ASCII-8BIT") do |response_file|
+ response_io = ::Gem::Net::BufferedIO.new(response_file)
+ ::Gem::Net::HTTPResponse.read_new(response_io).tap do |response|
+ response.decode_content = request.decode_content if request.respond_to?(:decode_content)
+ response.uri = request.uri
+
+ response.reading_body(response_io, request.response_body_permitted?) do
+ response_block&.call(response)
+ end
+ end
+ end
+ end
+
+ def record_response
+ request_path, response_path = *request_pair_paths
+
+ @recording = true
+
+ response = http.request_without_vcr(request, body, &response_block)
+ @recording = false
+ unless @recording
+ require "fileutils"
+ FileUtils.mkdir_p(File.dirname(request_path))
+ binwrite(request_path, request_to_string(request))
+ binwrite(response_path, response_to_string(response))
+ end
+ response
+ end
+
+ def key
+ [request["host"] || http.address, request.path, request.method].compact
+ end
+
+ def file_name_for_key(key)
+ File.join(*key).gsub(/[\:*?"<>|]/, "-")
+ end
+
+ def request_pair_paths
+ %w[request response].map do |kind|
+ File.join(CASSETTE_PATH, CASSETTE_NAME, file_name_for_key(key), kind)
+ end
+ end
+
+ def request_to_string(request)
+ request_string = []
+ request_string << "> #{request.method.upcase} #{request.path}"
+ request.to_hash.each do |key, value|
+ request_string << "> #{key}: #{Array(value).first}"
+ end
+ request << "" << request.body if request.body
+ request_string.join("\n")
+ end
+
+ def response_to_string(response)
+ headers = response.to_hash
+ body = response.body
+
+ response_string = []
+ response_string << "HTTP/1.1 #{response.code} #{response.message}"
+
+ headers["content-length"] = [body.bytesize.to_s] if body
+
+ headers.each do |header, value|
+ response_string << "#{header}: #{value.join(", ")}"
+ end
+
+ response_string << "" << body
+
+ response_string = response_string.join("\n")
+ if response_string.respond_to?(:force_encoding)
+ response_string.force_encoding("ASCII-8BIT")
+ else
+ response_string
+ end
+ end
+
+ def binwrite(path, contents)
+ File.open(path, "wb:ASCII-8BIT") {|f| f.write(contents) }
+ end
+ end
+
+ def start_with_vcr
+ if ENV["BUNDLER_SPEC_PRE_RECORDED"]
+ raise IOError, "HTTP session already opened" if @started
+ @socket = nil
+ @started = true
+ else
+ start_without_vcr
+ end
+ end
+
+ alias_method :start_without_vcr, :start
+ alias_method :start, :start_with_vcr
+
+ def request_with_vcr(request, *args, &block)
+ handler = request.instance_eval do
+ remove_instance_variable(:@__vcr_request_handler) if defined?(@__vcr_request_handler)
+ end || RequestHandler.new(self, request, *args, &block)
+
+ handler.handle_request
+ end
+
+ alias_method :request_without_vcr, :request
+ alias_method :request, :request_with_vcr
+end
+
+require_relative "helpers/artifice"
+
+# Replace Gem::Net::HTTP with our VCR subclass
+Artifice.replace_net_http(BundlerVCRHTTP)
diff --git a/spec/bundler/support/artifice/windows.rb b/spec/bundler/support/artifice/windows.rb
new file mode 100644
index 0000000000..3056540beb
--- /dev/null
+++ b/spec/bundler/support/artifice/windows.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require_relative "../path"
+
+$LOAD_PATH.unshift(*Spec::Path.sinatra_dependency_paths)
+
+require "sinatra/base"
+
+class Windows < Sinatra::Base
+ set :raise_errors, true
+ set :show_exceptions, false
+
+ helpers do
+ def default_gem_repo
+ Pathname.new(ENV["BUNDLER_SPEC_GEM_REPO"] || Spec::Path.gem_repo1)
+ end
+ end
+
+ files = ["specs.4.8.gz",
+ "prerelease_specs.4.8.gz",
+ "quick/Marshal.4.8/rcov-1.0-mswin32.gemspec.rz",
+ "gems/rcov-1.0-mswin32.gem"]
+
+ files.each do |file|
+ get "/#{file}" do
+ File.binread default_gem_repo.join(file)
+ end
+ end
+
+ get "/gems/rcov-1.0-x86-mswin32.gem" do
+ halt 404
+ end
+
+ get "/api/v1/dependencies" do
+ halt 404
+ end
+
+ get "/versions" do
+ halt 500
+ end
+end
+
+require_relative "helpers/artifice"
+
+Artifice.activate_with(Windows)
diff --git a/spec/bundler/support/build_metadata.rb b/spec/bundler/support/build_metadata.rb
new file mode 100644
index 0000000000..2eade4137b
--- /dev/null
+++ b/spec/bundler/support/build_metadata.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative "path"
+require_relative "helpers"
+
+module Spec
+ module BuildMetadata
+ include Spec::Path
+ include Spec::Helpers
+
+ def write_build_metadata(dir: source_root, version: Bundler::VERSION)
+ build_metadata = {
+ git_commit_sha: git_commit_sha,
+ built_at: release_date_for(version, dir: dir),
+ }
+
+ replace_build_metadata(build_metadata, dir: dir)
+ end
+
+ def reset_build_metadata(dir: source_root)
+ build_metadata = {
+ built_at: nil,
+ }
+
+ replace_build_metadata(build_metadata, dir: dir)
+ end
+
+ private
+
+ def replace_build_metadata(build_metadata, dir:)
+ build_metadata_file = File.expand_path("lib/bundler/build_metadata.rb", dir)
+
+ ivars = build_metadata.sort.map do |k, v|
+ " @#{k} = #{loaded_gemspec.send(:ruby_code, v)}"
+ end.join("\n")
+
+ contents = File.read(build_metadata_file)
+ contents.sub!(/^(\s+# begin ivars).+(^\s+# end ivars)/m, "\\1\n#{ivars}\n\\2")
+ File.open(build_metadata_file, "w") {|f| f << contents }
+ end
+
+ def git_commit_sha
+ ruby_core_tarball? ? "unknown" : git("rev-parse --short HEAD", source_root).strip
+ end
+
+ def release_date_for(version, dir:)
+ changelog = File.expand_path("CHANGELOG.md", dir)
+ File.readlines(changelog)[2].scan(/^## #{Regexp.escape(version)} \((.*)\)/).first&.first if File.exist?(changelog)
+ end
+
+ extend self
+ end
+end
diff --git a/spec/bundler/support/builders.rb b/spec/bundler/support/builders.rb
new file mode 100644
index 0000000000..43ab7e053d
--- /dev/null
+++ b/spec/bundler/support/builders.rb
@@ -0,0 +1,749 @@
+# frozen_string_literal: true
+
+require "bundler/shared_helpers"
+require "shellwords"
+require "fileutils"
+require "rubygems/package"
+
+require_relative "build_metadata"
+
+module Spec
+ module Builders
+ def self.extended(mod)
+ mod.extend Path
+ mod.extend Helpers
+ end
+
+ def self.constantize(name)
+ name.delete("-").upcase
+ end
+
+ def v(version)
+ Gem::Version.new(version)
+ end
+
+ def pl(platform)
+ Gem::Platform.new(platform)
+ end
+
+ def build_repo1
+ build_repo gem_repo1 do
+ FileUtils.cp rake_path, "#{gem_repo1}/gems/"
+
+ build_gem "coffee-script-source"
+ build_gem "git"
+ build_gem "puma"
+ build_gem "minitest"
+
+ build_gem "myrack", %w[0.9.1 1.0.0] do |s|
+ s.executables = "myrackup"
+ s.post_install_message = "Myrack's post install message"
+ end
+
+ build_gem "thin" do |s|
+ s.add_dependency "myrack"
+ s.post_install_message = "Thin's post install message"
+ end
+
+ build_gem "myrack-obama" do |s|
+ s.add_dependency "myrack"
+ s.post_install_message = "Myrack-obama's post install message"
+ end
+
+ build_gem "myrack_middleware", "1.0" do |s|
+ s.add_dependency "myrack", "0.9.1"
+ end
+
+ build_gem "rails", "2.3.2" do |s|
+ s.executables = "rails"
+ s.add_dependency "rake", rake_version
+ s.add_dependency "actionpack", "2.3.2"
+ s.add_dependency "activerecord", "2.3.2"
+ s.add_dependency "actionmailer", "2.3.2"
+ s.add_dependency "activeresource", "2.3.2"
+ end
+ build_gem "actionpack", "2.3.2" do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "activerecord", ["2.3.1", "2.3.2"] do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "actionmailer", "2.3.2" do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "activeresource", "2.3.2" do |s|
+ s.add_dependency "activesupport", "2.3.2"
+ end
+ build_gem "activesupport", %w[1.2.3 2.3.2 2.3.5]
+
+ build_gem "activemerchant" do |s|
+ s.add_dependency "activesupport", ">= 2.0.0"
+ end
+
+ build_gem "rspec", "1.2.7", no_default: true do |s|
+ s.write "lib/spec.rb", "SPEC = '1.2.7'"
+ end
+
+ build_gem "myrack-test", no_default: true do |s|
+ s.write "lib/myrack/test.rb", "MYRACK_TEST = '1.0'"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "ruby"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x86-mswin32"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x64-mswin64"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x86-mingw32"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x64-mingw-ucrt"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "aarch64-mingw-ucrt"
+ end
+
+ build_gem "platform_specific" do |s|
+ s.platform = "x86-darwin-100"
+ end
+
+ build_gem "only_java", "1.0" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "only_java", "1.1" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "nokogiri", "1.4.2"
+ build_gem "nokogiri", "1.4.2" do |s|
+ s.platform = "java"
+ s.add_dependency "weakling", ">= 0.0.3"
+ end
+
+ build_gem "laduradura", "5.15.2"
+ build_gem "laduradura", "5.15.2" do |s|
+ s.platform = "java"
+ end
+ build_gem "laduradura", "5.15.3" do |s|
+ s.platform = "java"
+ end
+
+ build_gem "weakling", "0.0.3"
+
+ build_gem "terranova", "8"
+
+ build_gem "duradura", "7.0"
+
+ build_gem "very_simple_binary", &:add_c_extension
+ build_gem "simple_binary", &:add_c_extension
+
+ build_gem "bundler", "0.9" do |s|
+ s.executables = "bundle"
+ s.write "bin/bundle", "#!/usr/bin/env ruby\nputs 'FAIL'"
+ end
+
+ # The bundler 0.8 gem has a rubygems plugin that always loads :(
+ build_gem "bundler", "0.8.1" do |s|
+ s.write "lib/bundler/omg.rb", ""
+ s.write "lib/rubygems_plugin.rb", "require 'bundler/omg' ; puts 'FAIL'"
+ end
+
+ # The yard gem iterates over Gem.source_index looking for plugins
+ build_gem "yard" do |s|
+ s.write "lib/yard.rb", <<-Y
+ Gem::Specification.sort_by(&:name).each do |gem|
+ puts gem.full_name
+ end
+ Y
+ end
+
+ build_gem "net-ssh"
+ build_gem "net-sftp", "1.1.1" do |s|
+ s.add_dependency "net-ssh", ">= 1.0.0", "< 1.99.0"
+ end
+
+ build_gem "foo"
+ end
+ end
+
+ def build_repo2(**kwargs, &blk)
+ FileUtils.cp_r gem_repo1, gem_repo2, remove_destination: true
+ update_repo2(**kwargs, &blk) if block_given?
+ end
+
+ # A repo that has no pre-installed gems included. (The caller completely
+ # determines the contents with the block.)
+ #
+ # If the repo already exists, `#update_repo` will be called.
+ def build_repo3(**kwargs, &blk)
+ if File.exist?(gem_repo3)
+ update_repo(gem_repo3, &blk)
+ else
+ build_repo gem_repo3, **kwargs, &blk
+ end
+ end
+
+ # Like build_repo3, this is a repo that has no pre-installed gems included.
+ #
+ # If the repo already exists, `#udpate_repo` will be called
+ def build_repo4(**kwargs, &blk)
+ if File.exist?(gem_repo4)
+ update_repo gem_repo4, &blk
+ else
+ build_repo gem_repo4, **kwargs, &blk
+ end
+ end
+
+ def update_repo2(**kwargs, &blk)
+ update_repo(gem_repo2, **kwargs, &blk)
+ end
+
+ def update_repo3(&blk)
+ update_repo(gem_repo3, &blk)
+ end
+
+ def build_security_repo
+ build_repo security_repo do
+ build_gem "myrack"
+
+ build_gem "signed_gem" do |s|
+ cert = "signing-cert.pem"
+ pkey = "signing-pkey.pem"
+ s.write cert, TEST_CERT
+ s.write pkey, TEST_PKEY
+ s.signing_key = pkey
+ s.cert_chain = [cert]
+ end
+ end
+ end
+
+ # A minimal fake irb console
+ def build_dummy_irb(version = "9.9.9")
+ build_gem "irb", version do |s|
+ s.write "lib/irb.rb", <<-RUBY
+ class IRB
+ class << self
+ def toplevel_binding
+ unless defined?(@toplevel_binding) && @toplevel_binding
+ TOPLEVEL_BINDING.eval %{
+ def self.__irb__; binding; end
+ IRB.instance_variable_set(:@toplevel_binding, __irb__)
+ class << self; undef __irb__; end
+ }
+ end
+ @toplevel_binding.eval('private')
+ @toplevel_binding
+ end
+
+ def __irb__
+ while line = gets
+ begin
+ puts eval(line, toplevel_binding).inspect.sub(/^"(.*)"$/, '=> \\1')
+ rescue Exception => e
+ puts "\#{e.class}: \#{e.message}"
+ puts e.backtrace.first
+ end
+ end
+ end
+ alias start __irb__
+ end
+ end
+ RUBY
+ end
+ end
+
+ def build_repo(path, **kwargs, &blk)
+ return if File.directory?(path)
+
+ FileUtils.mkdir_p("#{path}/gems")
+
+ update_repo(path,**kwargs, &blk)
+ end
+
+ def update_repo(path, build_compact_index: true)
+ exempted_caller = Gem.ruby_version >= Gem::Version.new("3.4.0.dev") && RUBY_ENGINE != "jruby" ? "#{Module.nesting.first}#build_repo" : "build_repo"
+ if path == gem_repo1 && caller_locations(1, 1).first.label != exempted_caller
+ raise "Updating gem_repo1 is unsupported -- use gem_repo2 instead"
+ end
+ return unless block_given?
+ @_build_path = "#{path}/gems"
+ @_build_repo = File.basename(path)
+ yield
+ options = { build_compact: build_compact_index }
+ Gem::Indexer.new(path, options).generate_index
+ ensure
+ @_build_path = nil
+ @_build_repo = nil
+ end
+
+ def build_index(&block)
+ index = Bundler::Index.new
+ IndexBuilder.run(index, &block) if block_given?
+ index
+ end
+
+ def build_spec(name, version = "0.0.1", platform = nil, &block)
+ Array(version).map do |v|
+ Gem::Specification.new do |s|
+ s.name = name
+ s.version = Gem::Version.new(v)
+ s.platform = platform
+ s.authors = ["no one in particular"]
+ s.summary = "a gemspec used only for testing"
+ DepBuilder.run(s, &block) if block_given?
+ end
+ end
+ end
+
+ def build_lib(name, *args, &blk)
+ build_with(LibBuilder, name, args, &blk)
+ end
+
+ def build_bundler(*args, &blk)
+ build_with(BundlerBuilder, "bundler", args, &blk)
+ end
+
+ def build_gem(name, *args, &blk)
+ build_with(GemBuilder, name, args, &blk)
+ end
+
+ def build_git(name, *args, &block)
+ opts = args.last.is_a?(Hash) ? args.last : {}
+ builder = opts[:bare] ? GitBareBuilder : GitBuilder
+ spec = build_with(builder, name, args, &block)
+ GitReader.new(self, opts[:path] || lib_path(spec.full_name))
+ end
+
+ def update_git(name, *args, &block)
+ opts = args.last.is_a?(Hash) ? args.last : {}
+ spec = build_with(GitUpdater, name, args, &block)
+ GitReader.new(self, opts[:path] || lib_path(spec.full_name))
+ end
+
+ def build_plugin(name, *args, &blk)
+ build_with(PluginBuilder, name, args, &blk)
+ end
+
+ private
+
+ def build_with(builder, name, args, &blk)
+ @_build_path ||= nil
+ @_build_repo ||= nil
+ options = args.last.is_a?(Hash) ? args.pop : {}
+ versions = args.last || "1.0"
+ spec = nil
+
+ options[:path] ||= @_build_path
+ options[:source] ||= @_build_repo
+
+ Array(versions).each do |version|
+ spec = builder.new(self, name, version)
+ yield spec if block_given?
+ spec._build(options)
+ end
+
+ spec
+ end
+
+ class IndexBuilder
+ include Builders
+
+ def self.run(index, &block)
+ new(index).run(&block)
+ end
+
+ def initialize(index)
+ @index = index
+ end
+
+ def run(&block)
+ instance_eval(&block)
+ end
+
+ def gem(*args, &block)
+ build_spec(*args, &block).each do |s|
+ @index << s
+ end
+ end
+
+ def platforms(platforms)
+ platforms.split(/\s+/).each do |platform|
+ platform.gsub!(/^(mswin32)$/, 'x86-\1')
+ yield Gem::Platform.new(platform)
+ end
+ end
+
+ def versions(versions)
+ versions.split(/\s+/).each {|version| yield v(version) }
+ end
+ end
+
+ class DepBuilder
+ include Builders
+
+ def self.run(spec, &block)
+ new(spec).run(&block)
+ end
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def run(&block)
+ instance_eval(&block)
+ end
+
+ def runtime(name, requirements)
+ @spec.add_runtime_dependency(name, requirements)
+ end
+
+ def development(name, requirements)
+ @spec.add_development_dependency(name, requirements)
+ end
+
+ def required_ruby_version=(*reqs)
+ @spec.required_ruby_version = *reqs
+ end
+
+ alias_method :dep, :runtime
+ end
+
+ class BundlerBuilder
+ def initialize(context, name, version)
+ @context = context
+ @spec = Spec::Path.loaded_gemspec.dup
+ @spec.version = version || Bundler::VERSION
+ end
+
+ def required_ruby_version
+ @spec.required_ruby_version
+ end
+
+ def required_ruby_version=(x)
+ @spec.required_ruby_version = x
+ end
+
+ def _build(options = {})
+ full_name = "bundler-#{@spec.version}"
+ build_path = (options[:build_path] || @context.tmp) + full_name
+ bundler_path = build_path + "#{full_name}.gem"
+
+ FileUtils.mkdir_p build_path
+
+ @context.shipped_files.each do |shipped_file|
+ target_shipped_file = shipped_file
+ target_shipped_file = shipped_file.sub(/\Alibexec/, "exe") if @context.ruby_core?
+ target_shipped_file = build_path + target_shipped_file
+ target_shipped_dir = File.dirname(target_shipped_file)
+ FileUtils.mkdir_p target_shipped_dir unless File.directory?(target_shipped_dir)
+ FileUtils.cp File.expand_path(shipped_file, @context.source_root), target_shipped_file, preserve: true
+ end
+
+ @context.replace_version_file(@spec.version, dir: build_path)
+ @context.replace_changelog(@spec.version, dir: build_path) if options[:released]
+
+ Spec::BuildMetadata.write_build_metadata(dir: build_path, version: @spec.version.to_s)
+
+ Dir.chdir build_path do
+ Gem::DefaultUserInteraction.use_ui(Gem::SilentUI.new) do
+ Gem::Package.build(@spec)
+ end
+ end
+
+ if block_given?
+ yield(bundler_path)
+ else
+ FileUtils.mv bundler_path, options[:path]
+ end
+ ensure
+ FileUtils.rm_rf build_path
+ end
+ end
+
+ class LibBuilder
+ def initialize(context, name, version)
+ @context = context
+ @name = name
+ @spec = Gem::Specification.new do |s|
+ s.name = name
+ s.version = version
+ s.summary = "This is just a fake gem for testing"
+ s.description = "This is a completely fake gem, for testing purposes."
+ s.author = "no one"
+ s.email = "foo@bar.baz"
+ s.homepage = "http://example.com"
+ s.license = "MIT"
+ s.required_ruby_version = ">= 3.0"
+ end
+ @files = {}
+ end
+
+ def method_missing(*args, &blk)
+ @spec.send(*args, &blk)
+ end
+
+ def write(file, source = "")
+ @files[file] = source
+ end
+
+ def executables=(val)
+ @spec.executables = Array(val)
+ @spec.executables.each do |file|
+ executable = "#{@spec.bindir}/#{file}"
+ shebang = "#!/usr/bin/env ruby\n"
+ @spec.files << executable
+ write executable, "#{shebang}require_relative '../lib/#{@name}' ; puts #{Builders.constantize(@name)}"
+ end
+ end
+
+ def add_c_extension
+ extensions << "ext/extconf.rb"
+ write "ext/extconf.rb", <<-RUBY
+ require "mkmf"
+
+ extension_name = "#{name}_c"
+ if extra_lib_dir = with_config("ext-lib")
+ # add extra libpath if --with-ext-lib is
+ # passed in as a build_arg
+ dir_config extension_name, nil, extra_lib_dir
+ else
+ dir_config extension_name
+ end
+ create_makefile extension_name
+ RUBY
+ write "ext/#{name}.c", <<-C
+ #include "ruby.h"
+
+ void Init_#{name}_c(void) {
+ rb_define_module("#{Builders.constantize(name)}_IN_C");
+ }
+ C
+ end
+
+ def _build(options)
+ path = options[:path] || _default_path
+
+ if options[:rubygems_version]
+ @spec.rubygems_version = options[:rubygems_version]
+
+ def @spec.validate(*); end
+ end
+
+ unless options[:no_default]
+ gem_source = options[:source] || "path@#{path}"
+ @files = _default_files.
+ merge("lib/#{entrypoint}/source.rb" => "#{Builders.constantize(name)}_SOURCE = #{gem_source.to_s.dump}").
+ merge(@files)
+ end
+
+ @spec.authors = ["no one"]
+ @spec.files += @files.keys
+
+ case options[:gemspec]
+ when false
+ # do nothing
+ when :yaml
+ @files["#{name}.gemspec"] = @spec.to_yaml
+ else
+ @files["#{name}.gemspec"] = @spec.to_ruby
+ end
+
+ @files.each do |file, source|
+ full_path = Pathname.new(path).join(file)
+ FileUtils.mkdir_p(full_path.dirname)
+ File.open(full_path, "w") {|f| f.puts source }
+ FileUtils.chmod("+x", full_path) if @spec.executables.map {|exe| "#{@spec.bindir}/#{exe}" }.include?(file)
+ end
+ path
+ end
+
+ def _default_files
+ @_default_files ||= { "lib/#{entrypoint}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'" }
+ end
+
+ def entrypoint
+ name.tr("-", "/")
+ end
+
+ def _default_path
+ @context.tmp("libs", @spec.full_name)
+ end
+
+ def platform_string
+ " #{@spec.platform}" unless @spec.platform == Gem::Platform::RUBY
+ end
+ end
+
+ class GitBuilder < LibBuilder
+ def _build(options)
+ default_branch = options[:default_branch] || "main"
+ path = options[:path] || _default_path
+ source = options[:source] || "git@#{path}"
+ super(options.merge(path: path, source: source))
+ @context.git("config --global init.defaultBranch #{default_branch}", path)
+ @context.git("init", path)
+ @context.git("add *", path)
+ @context.git("config user.email lol@wut.com", path)
+ @context.git("config user.name lolwut", path)
+ @context.git("config commit.gpgsign false", path)
+ @context.git("commit -m OMG_INITIAL_COMMIT", path)
+ end
+ end
+
+ class GitBareBuilder < LibBuilder
+ def _build(options)
+ path = options[:path] || _default_path
+ super(options.merge(path: path))
+ @context.git("init --bare", path)
+ end
+ end
+
+ class GitUpdater < LibBuilder
+ def _build(options)
+ libpath = options[:path] || _default_path
+ update_gemspec = options[:gemspec] || false
+ source = options[:source] || "git@#{libpath}"
+
+ if branch = options[:branch]
+ @context.git("checkout -b #{Shellwords.shellescape(branch)}", libpath)
+ elsif tag = options[:tag]
+ @context.git("tag #{Shellwords.shellescape(tag)}", libpath)
+ elsif options[:remote]
+ @context.git("remote add origin #{options[:remote]}", libpath)
+ elsif options[:push]
+ @context.git("push origin #{options[:push]}", libpath)
+ end
+
+ current_ref = @context.git("rev-parse HEAD", libpath).strip
+ _default_files.keys.each do |path|
+ _default_files[path] += "\n#{Builders.constantize(name)}_PREV_REF = '#{current_ref}'"
+ end
+ super(options.merge(path: libpath, gemspec: update_gemspec, source: source))
+ @context.git("commit -am BUMP", libpath)
+ end
+ end
+
+ class GitReader
+ attr_reader :context, :path
+
+ def initialize(context, path)
+ @context = context
+ @path = path
+ end
+
+ def ref_for(ref, len = nil)
+ ref = context.git "rev-parse #{ref}", path
+ ref = ref[0..len] if len
+ ref
+ end
+ end
+
+ class GemBuilder < LibBuilder
+ def _build(opts)
+ lib_path = opts[:lib_path] || @context.tmp(".tmp/#{@spec.full_name}")
+ lib_path = super(opts.merge(path: lib_path, no_default: opts[:no_default]))
+ destination = opts[:path] || _default_path
+ FileUtils.mkdir_p(lib_path.join(destination))
+
+ if [:yaml, false].include?(opts[:gemspec])
+ Dir.chdir(lib_path) do
+ Bundler.rubygems.build(@spec, opts[:skip_validation])
+ end
+ elsif opts[:skip_validation]
+ Dir.chdir(lib_path) { Gem::Package.build(@spec, true) }
+ else
+ Dir.chdir(lib_path) { Gem::Package.build(@spec) }
+ end
+
+ gem_path = File.expand_path("#{@spec.full_name}.gem", lib_path)
+ if opts[:to_system]
+ @context.system_gems gem_path, default: opts[:default]
+ elsif opts[:to_bundle]
+ @context.system_gems gem_path, path: @context.default_bundle_path
+ else
+ FileUtils.mv(gem_path, destination)
+ end
+ end
+
+ def _default_path
+ @context.gem_repo1("gems")
+ end
+ end
+
+ class PluginBuilder < GemBuilder
+ def _default_files
+ @_default_files ||= {
+ "lib/#{name}.rb" => "#{Builders.constantize(name)} = '#{version}#{platform_string}'",
+ "plugins.rb" => "",
+ }
+ end
+ end
+
+ TEST_CERT = <<~CERT
+ -----BEGIN CERTIFICATE-----
+ MIIDNTCCAh2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAnMQwwCgYDVQQDDAN5b3Ux
+ FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMB4XDTE1MDIwODAwMTIyM1oXDTQyMDYy
+ NTAwMTIyM1owJzEMMAoGA1UEAwwDeW91MRcwFQYKCZImiZPyLGQBGRYHZXhhbXBs
+ ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMkupYkg3Nd1oXM3fo0d
+ mVJBWNrni88lKDuIIQXwcKe6XCgiloZG708ecLTOws9+o9MkTl9Wtpf/WGXT98NK
+ EPUYakd2Fv1SuD1jWYlP7iDR6hB3RkWBm5ziujYftVJ4ZrPD42PLjDASvlh75Tvr
+ MeM7yq/qkcgNsd9dQyUvMNPks3tla9je7Dt7Auli2IN3CNXys7gIOfwJH0Bb/M6t
+ y7oUfpoUKAfLzwe61abztgDu1lSNgdFBM1kcxYflyh/FkX5TlAcWeAXzLrnxAXGR
+ UxXrxW4oPC+kZi/pDRBd7X4zQDx7bCmr1+FsS3M05i3w5E08Tt9iKRk4V8nCmE4i
+ k6UCAwEAAaNsMGowCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYE
+ FOOOFw5TNAqt/TcRRZEU3Dg/58XuMBYGA1UdEQQPMA2BC3lvdUBleGFtcGxlMBYG
+ A1UdEgQPMA2BC3lvdUBleGFtcGxlMA0GCSqGSIb3DQEBCwUAA4IBAQAy3xnmobxU
+ 1SyhHvoIXTJmG0wt1DQ/Dqwjy362LpEf1UHt29wtg1Mph58eVtl93z5Vd2t4/O77
+ E2BHpSu9ujc6/Br4+2uA/Qk/xRyLBtZAwty6J4uFvOOg985HonN+RCUZbKSUTmtA
+ TZvNtIDAZFQ8Tu75K4gIBxDcz7biGi4i1VJ3F3GNCNeossr9IQwKvb+UWFq14U5R
+ IzUnGgMIzcjUG2kKQvddRD1CjS+egtcLvShbOfm5bs4w4rfQ2FPF+Aaf9v7fxa/c
+ Jrf3K+cB19eAy7O4nlPG1xurvnZd0QpqRk++werrBuKe1Pgga7YBLePfJhzwqcZv
+ wVOSsB870yeO
+ -----END CERTIFICATE-----
+ CERT
+
+ TEST_PKEY = <<~PKEY
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEowIBAAKCAQEAyS6liSDc13Whczd+jR2ZUkFY2ueLzyUoO4ghBfBwp7pcKCKW
+ hkbvTx5wtM7Cz36j0yROX1a2l/9YZdP3w0oQ9RhqR3YW/VK4PWNZiU/uINHqEHdG
+ RYGbnOK6Nh+1Unhms8PjY8uMMBK+WHvlO+sx4zvKr+qRyA2x311DJS8w0+Sze2Vr
+ 2N7sO3sC6WLYg3cI1fKzuAg5/AkfQFv8zq3LuhR+mhQoB8vPB7rVpvO2AO7WVI2B
+ 0UEzWRzFh+XKH8WRflOUBxZ4BfMuufEBcZFTFevFbig8L6RmL+kNEF3tfjNAPHts
+ KavX4WxLczTmLfDkTTxO32IpGThXycKYTiKTpQIDAQABAoIBABpyrHEWRed5X7aN
+ kXCBzKSN/LLChT8VNnB6bppLnV501yVbmV2hDlg2EJZkfCMvwIptwnPcKs2uqZ4G
+ u2gMC6X9Bgkg/YK4u4nZJBiIzoMNYEUL48wYGYS1dcokaapO3nQ8M1+XjyAexrFL
+ 5btL1IIisScRTQWiGe6FtzcN43sSNkBISyDF5zG4Kodynqi0ekITmMl2q5XLWcsM
+ KBnmZcRFEmFae2YYczVy8SXNApkZEvN69znvAX1iDNnZ3sJFchXo1nRPt4stOOKw
+ mydgIYqaNQ22aF3OkblvoA4Y4m+X2Qt1sfkryKa5xTT7DSE81GmmazNI64EWqtES
+ 6Xde6P0CgYEA+V1vuSnE5fWX188abWMbVwNMC71WfHbntFmI+qwWYPEpickm+RGX
+ DDfXs5unlVX4KUmjfplgavO29op1GZTuD9TlRnUAV0+0aJnNq4DY6XsHfD84qsBr
+ gQGEHeJ1cMGNDnZR/EV3eudMalj9Qjpx9NoXNzMykb0/SUYZQemiqwcCgYEAzokC
+ s0GoHVJqan4dfU0h0G5QPncrajW9DGG1ySxK/A2eqbVB8W2ZQx39OS26/Gydb31p
+ cR7zm8PZpNbzLqlIMEbD4F6q22xxvYVtDx/HHPjxHMi87yxwQ9uLDUHoMa/LciTO
+ djv3D1xTDDGxbpjmsdmINetunAs3htxku7JY5PMCgYBs3/TVvXzwgmhHm28Ib4sS
+ VKgxP/uw4CGORsFd4SDsNp9SP3c6rAltFjyheMaUlzKApFwz/DdyuvIZdp5mCvZe
+ BzALsS3y8SPtv6lixiDu3/6GqvvM4bKOYuESQzvPfVJfDB4DrTjben2MuUnqTqZO
+ p6IXQc1EgIJPNcH1W1LgpQKBgAKZlPAevngIBpDqn4JpSyititMOevxuSr/yJvCu
+ Xw9HOJ0YTAk3APvoT7y9h6IP1/eEU6R56EUotP+vOQZ4WRFKgsK7TllOxyvElzfe
+ hYom1BoxqLc2Dv+7rsdu8fZWKTB5qCOy44xM9DquEXa79AN/IojTOuQ5++v1sErw
+ ls/jAoGBANneGe9ogN51mYkrLyg1fhU1i24gFRq+sPGEvsCUoE6Vjw/lawQQ80T8
+ v45TFqvhoGpgznqy3qxDJyguquZg6HN2yW6HE2Dvk7uk3XogcjdXgNDmWqb2j0eE
+ z9pKzHCqfwNVPuYf44Znyo2YeyZ2kHn42MU73oXuFshUs3QHcH+P
+ -----END RSA PRIVATE KEY-----
+ PKEY
+ end
+end
diff --git a/spec/bundler/support/bundle b/spec/bundler/support/bundle
new file mode 100755
index 0000000000..8f8b535295
--- /dev/null
+++ b/spec/bundler/support/bundle
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative "../bundler/support/activate"
+
+load File.expand_path("bundle", Spec::Path.exedir)
diff --git a/spec/bundler/support/bundle.rb b/spec/bundler/support/bundle.rb
new file mode 100644
index 0000000000..aa7b121706
--- /dev/null
+++ b/spec/bundler/support/bundle.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require_relative "path"
+
+warn "#{__FILE__} is deprecated. Please use #{Spec::Path.dev_binstub} instead"
+
+load Spec::Path.dev_binstub
diff --git a/spec/bundler/support/checksums.rb b/spec/bundler/support/checksums.rb
new file mode 100644
index 0000000000..7b69bba668
--- /dev/null
+++ b/spec/bundler/support/checksums.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+module Spec
+ module Checksums
+ class ChecksumsBuilder
+ attr_reader :bundler_registered
+
+ def initialize(enabled = true, &block)
+ @enabled = enabled
+ @checksums = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(original)
+ super
+ @checksums = @checksums.dup
+ end
+
+ def checksum(repo, name, version, platform = Gem::Platform::RUBY, folder = "gems")
+ @bundler_registered = true if name == "bundler"
+
+ name_tuple = Gem::NameTuple.new(name, version, platform)
+ gem_file = File.join(repo, folder, "#{name_tuple.full_name}.gem")
+ File.open(gem_file, "rb") do |f|
+ register(name_tuple, Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#checksum)"))
+ end
+ end
+
+ def no_checksum(name, version, platform = Gem::Platform::RUBY)
+ name_tuple = Gem::NameTuple.new(name, version, platform)
+ register(name_tuple, nil)
+ end
+
+ def delete(name, platform = nil)
+ @checksums.reject! {|k, _| k.name == name && (platform.nil? || k.platform == platform) }
+ end
+
+ def to_s
+ return "" unless @enabled
+
+ locked_checksums = @checksums.map do |name_tuple, checksum|
+ checksum &&= " #{checksum.to_lock}"
+ " #{name_tuple.lock_name}#{checksum}\n"
+ end
+
+ "\nCHECKSUMS\n#{locked_checksums.sort.join}"
+ end
+
+ private
+
+ def register(name_tuple, checksum)
+ delete(name_tuple.name, name_tuple.platform)
+ @checksums[name_tuple] = checksum
+ end
+ end
+
+ def checksums_section(enabled = true, bundler_checksum: true, &block)
+ ChecksumsBuilder.new(enabled, &block).tap do |builder|
+ next if builder.bundler_registered || !bundler_checksum
+
+ next if Bundler::VERSION.to_s.end_with?(".dev")
+ builder.checksum(system_gem_path, "bundler", Bundler::VERSION, Gem::Platform::RUBY, "cache")
+ end
+ end
+
+ def checksums_section_when_enabled(target_lockfile = nil, &block)
+ begin
+ enabled = (target_lockfile || lockfile).match?(/^CHECKSUMS$/)
+ rescue Errno::ENOENT
+ enabled = true
+ end
+ checksums_section(enabled, &block)
+ end
+
+ def checksum_to_lock(*args)
+ checksums_section(true, bundler_checksum: false) do |c|
+ c.checksum(*args)
+ end.to_s.sub(/^CHECKSUMS\n/, "").strip
+ end
+
+ def checksum_digest(*args)
+ checksum_to_lock(*args).split(Bundler::Checksum::ALGO_SEPARATOR, 2).last
+ end
+
+ # if prefixes is given, removes all checksums where the line
+ # has any of the prefixes on the line before the checksum
+ # otherwise, removes all checksums from the lockfile
+ def remove_checksums_from_lockfile(lockfile, *prefixes)
+ head, remaining = lockfile.split(/^CHECKSUMS$/, 2)
+ return lockfile unless remaining
+ checksums, tail = remaining.split("\n\n", 2)
+
+ prefixes =
+ if prefixes.empty?
+ nil
+ else
+ /(#{prefixes.map {|p| Regexp.escape(p) }.join("|")})/
+ end
+
+ checksums = checksums.each_line.map do |line|
+ if prefixes.nil? || line.match?(prefixes)
+ line.gsub(/ sha256=[a-f0-9]{64}/i, "")
+ else
+ line
+ end
+ end
+
+ head.concat(
+ "CHECKSUMS",
+ checksums.join,
+ "\n\n",
+ tail
+ )
+ end
+
+ def remove_checksums_section_from_lockfile(lockfile)
+ head, remaining = lockfile.split(/^CHECKSUMS$/, 2)
+ return lockfile unless remaining
+ _checksums, tail = remaining.split("\n\n", 2)
+ head.concat(tail)
+ end
+
+ def checksum_from_package(gem_file, name, version)
+ name_tuple = Gem::NameTuple.new(name, version)
+
+ checksum = nil
+
+ File.open(gem_file, "rb") do |f|
+ checksum = Bundler::Checksum.from_gem(f, gemfile)
+ end
+
+ "#{name_tuple.lock_name} #{checksum.to_lock}"
+ end
+ end
+end
diff --git a/spec/bundler/support/command_execution.rb b/spec/bundler/support/command_execution.rb
new file mode 100644
index 0000000000..e2915b996d
--- /dev/null
+++ b/spec/bundler/support/command_execution.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Spec
+ class CommandExecution
+ def initialize(command, timeout:)
+ @command = command
+ @timeout = timeout
+ @original_stdout = String.new
+ @original_stderr = String.new
+ end
+
+ attr_accessor :exitstatus, :command, :original_stdout, :original_stderr
+ attr_reader :timeout
+ attr_writer :failure_reason
+
+ def raise_error!
+ return unless failure?
+
+ error_header = if failure_reason == :timeout
+ "Invoking `#{command}` was aborted after #{timeout} seconds with output:"
+ else
+ "Invoking `#{command}` failed with output:"
+ end
+
+ raise <<~ERROR
+ #{error_header}
+
+ ----------------------------------------------------------------------
+ #{stdboth}
+ ----------------------------------------------------------------------
+ ERROR
+ end
+
+ def to_s
+ "$ #{command}"
+ end
+ alias_method :inspect, :to_s
+
+ def stdboth
+ @stdboth ||= [stderr, stdout].join("\n").strip
+ end
+
+ def stdout
+ normalize(original_stdout)
+ end
+
+ def stderr
+ normalize(original_stderr)
+ end
+
+ def to_s_verbose
+ [
+ to_s,
+ stdout,
+ stderr,
+ exitstatus ? "# $? => #{exitstatus}" : "",
+ ].reject(&:empty?).join("\n")
+ end
+
+ def success?
+ return true unless exitstatus
+ exitstatus == 0
+ end
+
+ def failure?
+ return true unless exitstatus
+ exitstatus > 0
+ end
+
+ private
+
+ attr_reader :failure_reason
+
+ def normalize(string)
+ string.dup.force_encoding(Encoding::UTF_8).scrub.strip.gsub("\r\n", "\n")
+ end
+ end
+end
diff --git a/spec/bundler/support/env.rb b/spec/bundler/support/env.rb
new file mode 100644
index 0000000000..0899bd82a3
--- /dev/null
+++ b/spec/bundler/support/env.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Spec
+ module Env
+ def ruby_core?
+ File.exist?(File.expand_path("../../../lib/bundler/bundler.gemspec", __dir__))
+ end
+
+ def rubylib
+ ENV["RUBYLIB"].to_s.split(File::PATH_SEPARATOR)
+ end
+ end
+end
diff --git a/spec/bundler/support/filters.rb b/spec/bundler/support/filters.rb
new file mode 100644
index 0000000000..2be25b4a78
--- /dev/null
+++ b/spec/bundler/support/filters.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class RequirementChecker < Proc
+ def self.against(provided)
+ new do |required|
+ requirement = Gem::Requirement.new(required)
+
+ !requirement.satisfied_by?(provided)
+ end.tap do |checker|
+ checker.provided = provided
+ end
+ end
+
+ attr_accessor :provided
+
+ def inspect
+ "\"#{provided}\""
+ end
+end
+
+git_version = Gem::Version.new(`git --version`[/(\d+\.\d+\.\d+)/, 1])
+
+RSpec.configure do |config|
+ config.filter_run_excluding realworld: true
+
+ config.filter_run_excluding rubygems: RequirementChecker.against(Gem.rubygems_version)
+ config.filter_run_excluding git: RequirementChecker.against(git_version)
+ config.filter_run_excluding ruby_repo: !ENV["GEM_COMMAND"].nil?
+ config.filter_run_excluding no_color_tty: Gem.win_platform? || !ENV["GITHUB_ACTION"].nil?
+ config.filter_run_excluding permissions: Gem.win_platform?
+ config.filter_run_excluding readline: Gem.win_platform?
+ config.filter_run_excluding jruby_only: RUBY_ENGINE != "jruby"
+ config.filter_run_excluding truffleruby_only: RUBY_ENGINE != "truffleruby"
+ config.filter_run_excluding man: Gem.win_platform?
+ config.filter_run_excluding mri_only: RUBY_ENGINE != "ruby"
+
+ config.filter_run_when_matching :focus unless ENV["CI"]
+
+ config.before(:each, :bundler) do |example|
+ bundle_config "simulate_version #{example.metadata[:bundler]}"
+ end
+end
diff --git a/spec/bundler/support/hax.rb b/spec/bundler/support/hax.rb
new file mode 100644
index 0000000000..46718f5fa4
--- /dev/null
+++ b/spec/bundler/support/hax.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+if ENV["BUNDLER_SPEC_RUBY_PLATFORM"]
+ Object.send(:remove_const, :RUBY_PLATFORM)
+ RUBY_PLATFORM = ENV["BUNDLER_SPEC_RUBY_PLATFORM"]
+end
+
+module Gem
+ def self.ruby=(ruby)
+ @ruby = ruby
+ end
+
+ if ENV["RUBY"]
+ Gem.ruby = ENV["RUBY"]
+ end
+
+ if ENV["BUNDLER_GEM_DEFAULT_DIR"]
+ @default_dir = ENV["BUNDLER_GEM_DEFAULT_DIR"]
+ @default_specifications_dir = nil
+ end
+
+ spec_platform = ENV["BUNDLER_SPEC_PLATFORM"]
+ if spec_platform
+ if /mingw|mswin/.match?(spec_platform)
+ @@win_platform = nil # rubocop:disable Style/ClassVars
+ RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_")
+ end
+
+ RbConfig::CONFIG["arch"] = spec_platform
+
+ class Platform
+ @local = nil
+ end
+ @platforms = []
+ end
+
+ if ENV["BUNDLER_SPEC_GEM_SOURCES"]
+ self.sources = [ENV["BUNDLER_SPEC_GEM_SOURCES"]]
+ end
+
+ if ENV["BUNDLER_SPEC_READ_ONLY"]
+ module ReadOnly
+ def open(file, mode)
+ if file != IO::NULL && mode == "wb"
+ raise Errno::EROFS
+ else
+ super
+ end
+ end
+ end
+
+ File.singleton_class.prepend ReadOnly
+ end
+
+ if ENV["BUNDLER_SPEC_FAKE_RESOLVE"]
+ module FakeResolv
+ def getaddrinfo(host, port)
+ if host == ENV["BUNDLER_SPEC_FAKE_RESOLVE"]
+ [["AF_INET", port, "127.0.0.1", "127.0.0.1", 2, 2, 17]]
+ else
+ super
+ end
+ end
+ end
+
+ Socket.singleton_class.prepend FakeResolv
+ end
+end
+
+# 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/spec/bundler/support/helpers.rb b/spec/bundler/support/helpers.rb
new file mode 100644
index 0000000000..b0d4b5008b
--- /dev/null
+++ b/spec/bundler/support/helpers.rb
@@ -0,0 +1,631 @@
+# frozen_string_literal: true
+
+require_relative "the_bundle"
+require_relative "path"
+require_relative "options"
+require_relative "subprocess"
+
+module Spec
+ module Helpers
+ include Spec::Path
+ include Spec::Options
+ include Spec::Subprocess
+
+ def self.extended(mod)
+ mod.extend Spec::Path
+ mod.extend Spec::Options
+ mod.extend Spec::Subprocess
+ end
+
+ def reset!
+ Dir.glob("#{tmp}/{gems/*,*}", File::FNM_DOTMATCH).each do |dir|
+ next if %w[base base_system remote1 rubocop standard gems rubygems . ..].include?(File.basename(dir))
+ FileUtils.rm_r(dir)
+ end
+ FileUtils.mkdir_p(home)
+ FileUtils.mkdir_p(tmpdir)
+ Bundler.reset!
+ Bundler::Source::Git::GitProxy.reset
+ Gem.clear_paths
+ end
+
+ def the_bundle
+ TheBundle.new
+ end
+
+ MAJOR_DEPRECATION = /^\[DEPRECATED\]\s*/
+
+ def err_without_deprecations
+ err.gsub(/#{MAJOR_DEPRECATION}.+[\n]?/, "")
+ end
+
+ def deprecations
+ err.split("\n").filter_map {|l| l.sub(MAJOR_DEPRECATION, "") if l.match?(MAJOR_DEPRECATION) }
+ end
+
+ def run(cmd, *args)
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ groups = args.map(&:inspect).join(", ")
+ setup = "require 'bundler' ; Bundler.ui.silence { Bundler.setup(#{groups}) }"
+ ruby([setup, cmd].join(" ; "), opts)
+ end
+
+ def load_error_run(ruby, name, *args)
+ cmd = <<-RUBY
+ begin
+ #{ruby}
+ rescue LoadError => e
+ warn e.message if e.message.include?("-- #{name}")
+ end
+ RUBY
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ args += [opts]
+ run(cmd, *args)
+ end
+
+ def in_bundled_app(cmd, options = {})
+ sys_exec(cmd, dir: bundled_app, raise_on_error: options[:raise_on_error])
+ end
+
+ def bundle(cmd, options = {}, &block)
+ bundle_bin = options.delete(:bundle_bin)
+ bundle_bin ||= installed_bindir.join("bundle")
+
+ env = options.delete(:env) || {}
+
+ requires = options.delete(:requires) || []
+
+ dir = options.delete(:dir) || bundled_app
+ custom_load_path = options.delete(:load_path)
+
+ load_path = []
+ load_path << custom_load_path if custom_load_path
+
+ build_env_options = { load_path: load_path, requires: requires, env: env }
+ build_env_options.merge!(artifice: options.delete(:artifice)) if options.key?(:artifice) || cmd.start_with?("exec")
+
+ match_source(cmd)
+
+ env = build_env(build_env_options)
+
+ raise_on_error = options.delete(:raise_on_error)
+
+ args = options.map do |k, v|
+ case v
+ when true
+ " --#{k}"
+ when false
+ " --no-#{k}"
+ else
+ " --#{k} #{v}"
+ end
+ end.join
+
+ cmd = "#{Gem.ruby} #{bundle_bin} #{cmd}#{args}"
+ sys_exec(cmd, { env: env, dir: dir, raise_on_error: raise_on_error }, &block)
+ end
+
+ def main_source(dir)
+ gemfile = File.expand_path("Gemfile", dir)
+ return unless File.exist?(gemfile)
+
+ match = File.readlines(gemfile).first.match(/source ["'](?<source>[^"']+)["']/)
+ return unless match
+
+ match[:source]
+ end
+
+ def bundler(cmd, options = {})
+ options[:bundle_bin] = system_gem_path("bin/bundler")
+ bundle(cmd, options)
+ end
+
+ def ruby(ruby, options = {})
+ env = build_env({ artifice: nil }.merge(options))
+ escaped_ruby = ruby.shellescape
+ options[:env] = env if env
+ options[:dir] ||= bundled_app
+ sys_exec(%(#{Gem.ruby} -w -e #{escaped_ruby}), options)
+ end
+
+ def load_error_ruby(ruby, name, opts = {})
+ ruby(<<-R)
+ begin
+ #{ruby}
+ rescue LoadError => e
+ warn e.message if e.message.include?("-- #{name}")
+ end
+ R
+ end
+
+ def build_env(options = {})
+ env = options.delete(:env) || {}
+ libs = options.delete(:load_path) || []
+ env["RUBYOPT"] = opt_add("-I#{libs.join(File::PATH_SEPARATOR)}", env["RUBYOPT"]) if libs.any?
+
+ current_example = RSpec.current_example
+
+ main_source = @gemfile_source if defined?(@gemfile_source)
+ compact_index_main_source = main_source&.start_with?("https://gem.repo", "https://gems.security")
+
+ requires = options.delete(:requires) || []
+ requires << hax
+
+ artifice = options.delete(:artifice) do
+ if current_example && current_example.metadata[:realworld]
+ "vcr"
+ elsif compact_index_main_source
+ env["BUNDLER_SPEC_GEM_REPO"] ||=
+ case main_source
+ when "https://gem.repo1" then gem_repo1.to_s
+ when "https://gem.repo2" then gem_repo2.to_s
+ when "https://gem.repo3" then gem_repo3.to_s
+ when "https://gem.repo4" then gem_repo4.to_s
+ when "https://gems.security" then security_repo.to_s
+ end
+
+ "compact_index"
+ else
+ "fail"
+ end
+ end
+ if artifice
+ requires << "#{Path.spec_dir}/support/artifice/#{artifice}.rb"
+ end
+
+ requires.each {|r| env["RUBYOPT"] = opt_add("-r#{r}", env["RUBYOPT"]) }
+
+ env
+ end
+
+ def gembin(cmd, options = {})
+ cmd = bundled_app("bin/#{cmd}") unless cmd.to_s.include?("/")
+ sys_exec(cmd.to_s, options)
+ end
+
+ def sys_exec(cmd, options = {}, &block)
+ env = options[:env] || {}
+ env["RUBYOPT"] = opt_add(opt_add("-r#{spec_dir}/support/switch_rubygems.rb", env["RUBYOPT"]), ENV["RUBYOPT"])
+ options[:env] = env
+
+ sh(cmd, options, &block)
+ end
+
+ def bundle_config(config = nil, path = bundled_app(".bundle/config"))
+ if config.is_a?(String)
+ key, value = config.split(" ", 2)
+ config = { Bundler::Settings.key_for(key) => value }
+ end
+
+ current = File.exist?(path) ? Psych.load_file(path) : {}
+ return current unless config
+
+ current = {} if current.empty?
+
+ FileUtils.mkdir_p(File.dirname(path))
+
+ new_config = current.merge(config).compact
+
+ File.open(path, "w+") do |f|
+ f.puts new_config.to_yaml
+ end
+
+ new_config
+ end
+
+ def bundle_config_global(config = nil)
+ bundle_config(config, home(".bundle/config"))
+ end
+
+ def create_file(path, contents = "")
+ contents = strip_whitespace(contents)
+ path = Pathname.new(path).expand_path(bundled_app) unless path.is_a?(Pathname)
+ path.dirname.mkpath
+ path.write(contents)
+
+ # if the file is a script, create respective bat file on Windows
+ if contents.start_with?("#!")
+ path.chmod(0o755)
+ if Gem.win_platform?
+ path.sub_ext(".bat").write <<~SCRIPT
+ @ECHO OFF
+ @"ruby.exe" "%~dpn0" %*
+ SCRIPT
+ end
+ end
+ end
+
+ def gemfile(*args)
+ contents = args.pop
+
+ if contents.nil?
+ read_gemfile
+ else
+ match_source(contents)
+ create_file(args.pop || "Gemfile", contents)
+ end
+ end
+
+ def lockfile(*args)
+ contents = args.pop
+
+ if contents.nil?
+ read_lockfile
+ else
+ create_file(args.pop || "Gemfile.lock", contents)
+ end
+ end
+
+ def read_gemfile(file = "Gemfile")
+ read_bundled_app_file(file)
+ end
+
+ def read_lockfile(file = "Gemfile.lock")
+ read_bundled_app_file(file)
+ end
+
+ def read_bundled_app_file(file)
+ bundled_app(file).read
+ end
+
+ def strip_whitespace(str)
+ # Trim the leading spaces
+ spaces = str[/\A\s+/, 0] || ""
+ str.gsub(/^#{spaces}/, "")
+ end
+
+ def install_gemfile(*args)
+ opts = args.last.is_a?(Hash) ? args.pop : {}
+ gemfile(*args)
+ bundle :install, opts
+ end
+
+ def lock_gemfile(*args)
+ gemfile(*args)
+ opts = args.last.is_a?(Hash) ? args.last : {}
+ bundle :lock, opts
+ end
+
+ def base_system_gems(*names, **options)
+ system_gems names.map {|name| find_base_path(name) }, **options
+ end
+
+ def system_gems(*gems)
+ gems = gems.flatten
+ options = gems.last.is_a?(Hash) ? gems.pop : {}
+ install_dir = options.fetch(:path, system_gem_path)
+ default = options.fetch(:default, false)
+ gems.each do |g|
+ gem_name = g.to_s
+ bundler = gem_name.match(/\Abundler-(?<version>.*)\z/)
+
+ if bundler
+ with_built_bundler(bundler[:version], released: options.fetch(:released, false)) {|gem_path| install_gem(gem_path, install_dir, default) }
+ elsif %r{\A(?:[a-zA-Z]:)?/.*\.gem\z}.match?(gem_name)
+ install_gem(gem_name, install_dir, default)
+ else
+ gem_repo = options.fetch(:gem_repo, gem_repo1)
+ install_gem("#{gem_repo}/gems/#{gem_name}.gem", install_dir, default)
+ end
+ end
+ end
+
+ def self.install_dev_bundler
+ extend self
+
+ with_built_bundler(nil, build_path: tmp_root) {|gem_path| install_gem(gem_path, pristine_system_gem_path) }
+ end
+
+ def install_gem(path, install_dir, default = false)
+ raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path)
+
+ require "rubygems/installer"
+
+ with_simulated_platform do
+ installer = Gem::Installer.at(
+ path.to_s,
+ install_dir: install_dir.to_s,
+ document: [],
+ ignore_dependencies: true,
+ wrappers: true,
+ env_shebang: true,
+ force: true
+ )
+ installer.install
+ end
+
+ if default
+ gem = Pathname.new(path).basename.to_s.match(/(.*)\.gem/)[1]
+
+ # Revert Gem::Installer#write_spec and apply Gem::Installer#write_default_spec
+ FileUtils.mkdir_p File.join(install_dir, "specifications", "default")
+ File.rename File.join(install_dir, "specifications", gem + ".gemspec"),
+ File.join(install_dir, "specifications", "default", gem + ".gemspec")
+
+ # Revert Gem::Installer#write_cache_file
+ File.delete File.join(install_dir, "cache", gem + ".gem")
+ end
+ end
+
+ def uninstall_gem(name, options = {})
+ require "rubygems/uninstaller"
+
+ gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s
+
+ with_env_vars("GEM_HOME" => gem_home) do
+ Gem.clear_paths
+
+ uninstaller = Gem::Uninstaller.new(
+ name,
+ ignore: true,
+ executables: true,
+ all: true
+ )
+ uninstaller.uninstall
+ ensure
+ Gem.clear_paths
+ end
+ end
+
+ def installed_gems_list(options = {})
+ gem_home = options.dig(:env, "GEM_HOME") || system_gem_path.to_s
+
+ # Temporarily set GEM_HOME for the command
+ old_gem_home = ENV["GEM_HOME"]
+ ENV["GEM_HOME"] = gem_home
+ Gem.clear_paths
+
+ begin
+ require "rubygems/commands/list_command"
+
+ # Capture output from the list command
+ require "stringio"
+ output_io = StringIO.new
+ cmd = Gem::Commands::ListCommand.new
+ cmd.ui = Gem::StreamUI.new(StringIO.new, output_io, StringIO.new, false)
+ cmd.invoke
+ output = output_io.string.strip
+ ensure
+ ENV["GEM_HOME"] = old_gem_home
+ Gem.clear_paths
+ end
+
+ # Create a fake command execution so `out` helper works
+ command_execution = Spec::CommandExecution.new("gem list", timeout: 60)
+ command_execution.original_stdout << output
+ command_execution.exitstatus = 0
+ command_executions << command_execution
+
+ output
+ end
+
+ def with_built_bundler(version = nil, opts = {}, &block)
+ require_relative "builders"
+
+ Builders::BundlerBuilder.new(self, "bundler", version)._build(opts, &block)
+ end
+
+ def with_gem_path_as(path)
+ without_env_side_effects do
+ ENV["GEM_HOME"] = path.to_s
+ ENV["GEM_PATH"] = path.to_s
+ ENV["BUNDLER_ORIG_GEM_HOME"] = nil
+ ENV["BUNDLER_ORIG_GEM_PATH"] = nil
+ yield
+ end
+ end
+
+ def with_path_as(path)
+ without_env_side_effects do
+ ENV["PATH"] = path.to_s
+ ENV["BUNDLER_ORIG_PATH"] = nil
+ yield
+ end
+ end
+
+ def without_env_side_effects
+ backup = ENV.to_hash
+ yield
+ ensure
+ ENV.replace(backup)
+ end
+
+ # Simulate the platform set by BUNDLER_SPEC_PLATFORM for in-process
+ # operations, mirroring what hax.rb does for subprocesses.
+ def with_simulated_platform
+ spec_platform = ENV["BUNDLER_SPEC_PLATFORM"]
+ unless spec_platform
+ return yield
+ end
+
+ old_arch = RbConfig::CONFIG["arch"]
+ old_host_os = RbConfig::CONFIG["host_os"]
+
+ if /mingw|mswin/.match?(spec_platform)
+ Gem.class_variable_set(:@@win_platform, nil) # rubocop:disable Style/ClassVars
+ RbConfig::CONFIG["host_os"] = spec_platform.gsub(/^[^-]+-/, "").tr("-", "_")
+ end
+
+ RbConfig::CONFIG["arch"] = spec_platform
+ Gem::Platform.instance_variable_set(:@local, nil)
+ Gem.instance_variable_set(:@platforms, [])
+
+ yield
+ ensure
+ if spec_platform
+ RbConfig::CONFIG["arch"] = old_arch
+ RbConfig::CONFIG["host_os"] = old_host_os
+ Gem::Platform.instance_variable_set(:@local, nil)
+ Gem.instance_variable_set(:@platforms, [])
+ end
+ end
+
+ def with_path_added(path)
+ with_path_as([path.to_s, ENV["PATH"]].join(File::PATH_SEPARATOR)) do
+ yield
+ end
+ end
+
+ def break_git!
+ FileUtils.mkdir_p(tmp("broken_path"))
+ File.open(tmp("broken_path/git"), "w", 0o755) do |f|
+ f.puts "#!/usr/bin/env ruby\nSTDERR.puts 'This is not the git you are looking for'\nexit 1"
+ end
+
+ ENV["PATH"] = "#{tmp("broken_path")}:#{ENV["PATH"]}"
+ end
+
+ def with_fake_man
+ FileUtils.mkdir_p(tmp("fake_man"))
+ create_file(tmp("fake_man/man"), <<~SCRIPT)
+ #!/usr/bin/env ruby
+ puts ARGV.inspect
+ SCRIPT
+ with_path_added(tmp("fake_man")) { yield }
+ end
+
+ def pristine_system_gems(*gems)
+ FileUtils.rm_r(system_gem_path)
+
+ if gems.any?
+ system_gems(*gems)
+ else
+ default_system_gems
+ end
+ end
+
+ def cache_gems(*gems, gem_repo: gem_repo1)
+ gems = gems.flatten
+
+ FileUtils.mkdir_p("#{bundled_app}/vendor/cache")
+
+ gems.each do |g|
+ path = "#{gem_repo}/gems/#{g}.gem"
+ raise ArgumentError, "`#{path}` does not exist!" unless File.exist?(path)
+ FileUtils.cp(path, "#{bundled_app}/vendor/cache")
+ end
+ end
+
+ def simulate_new_machine
+ FileUtils.rm_r bundled_app(".bundle")
+ pristine_system_gems
+ end
+
+ def default_system_gems
+ FileUtils.cp_r pristine_system_gem_path, system_gem_path
+ end
+
+ def simulate_ruby_platform(ruby_platform)
+ old = ENV["BUNDLER_SPEC_RUBY_PLATFORM"]
+ ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = ruby_platform.to_s
+ yield
+ ensure
+ ENV["BUNDLER_SPEC_RUBY_PLATFORM"] = old
+ end
+
+ def simulate_platform(platform)
+ old = ENV["BUNDLER_SPEC_PLATFORM"]
+ ENV["BUNDLER_SPEC_PLATFORM"] = platform.to_s
+ yield
+ ensure
+ ENV["BUNDLER_SPEC_PLATFORM"] = old if block_given?
+ end
+
+ def current_ruby_minor
+ Gem.ruby_version.segments.tap {|s| s.delete_at(2) }.join(".")
+ end
+
+ def next_ruby_minor
+ ruby_major_minor.map.with_index {|s, i| i == 1 ? s + 1 : s }.join(".")
+ end
+
+ def ruby_major_minor
+ Gem.ruby_version.segments[0..1]
+ end
+
+ def revision_for(path)
+ git("rev-parse HEAD", path).strip
+ end
+
+ def with_read_only(pattern)
+ chmod = lambda do |dirmode, filemode|
+ lambda do |f|
+ mode = File.directory?(f) ? dirmode : filemode
+ File.chmod(mode, f)
+ end
+ end
+
+ Dir[pattern].each(&chmod[0o555, 0o444])
+ yield
+ ensure
+ Dir[pattern].each(&chmod[0o755, 0o644])
+ end
+
+ # Simulate replacing TODOs with real values
+ def prepare_gemspec(pathname)
+ process_file(pathname) do |line|
+ case line
+ when /spec\.metadata\["(?:allowed_push_host|homepage_uri|source_code_uri|changelog_uri)"\]/, /spec\.homepage/
+ line.gsub(/\=.*$/, '= "http://example.org"')
+ when /spec\.summary/
+ line.gsub(/\=.*$/, '= "A short summary of my new gem."')
+ when /spec\.description/
+ line.gsub(/\=.*$/, '= "A longer description of my new gem."')
+ else
+ line
+ end
+ end
+ end
+
+ def process_file(pathname)
+ changed_lines = pathname.readlines.map do |line|
+ yield line
+ end
+ File.open(pathname, "w") {|file| file.puts(changed_lines.join) }
+ end
+
+ def with_env_vars(env_hash, &block)
+ current_values = {}
+ env_hash.each do |k, v|
+ current_values[k] = ENV[k]
+ ENV[k] = v
+ end
+ block.call if block_given?
+ env_hash.each do |k, _|
+ ENV[k] = current_values[k]
+ end
+ end
+
+ def require_rack_test
+ # need to hack, so we can require rack for testing
+ old_gem_home = ENV["GEM_HOME"]
+ ENV["GEM_HOME"] = Spec::Path.scoped_base_system_gem_path.to_s
+ require "rack/test"
+ ENV["GEM_HOME"] = old_gem_home
+ end
+
+ def exit_status_for_signal(signal_number)
+ # For details see: https://en.wikipedia.org/wiki/Exit_status#Shell_and_scripts
+ 128 + signal_number
+ end
+
+ def empty_repo4
+ FileUtils.rm_r gem_repo4
+
+ build_repo4 {}
+ end
+
+ private
+
+ def match_source(contents)
+ match = /source ["']?(?<source>http[^"']+)["']?/.match(contents)
+ return unless match
+
+ @gemfile_source = match[:source]
+ end
+
+ def git_root_dir?
+ root.to_s == `git rev-parse --show-toplevel`.chomp
+ end
+ end
+end
diff --git a/spec/bundler/support/indexes.rb b/spec/bundler/support/indexes.rb
new file mode 100644
index 0000000000..1fbdd49abe
--- /dev/null
+++ b/spec/bundler/support/indexes.rb
@@ -0,0 +1,424 @@
+# frozen_string_literal: true
+
+module Spec
+ module Indexes
+ def dep(name, reqs = nil)
+ @deps ||= []
+ @deps << Bundler::Dependency.new(name, reqs)
+ end
+
+ def platform(*args)
+ @platforms ||= []
+ @platforms.concat args.map {|p| Gem::Platform.new(p) }
+ end
+
+ alias_method :platforms, :platform
+
+ def resolve(args = [], dependency_api_available: true)
+ @platforms ||= ["ruby"]
+ default_source = instance_double("Bundler::Source::Rubygems", specs: @index, to_s: "locally install gems", dependency_api_available?: dependency_api_available)
+ source_requirements = { default: default_source }
+ base = args[0] || Bundler::SpecSet.new([])
+ base.each {|ls| ls.source = default_source }
+ gem_version_promoter = args[1] || Bundler::GemVersionPromoter.new
+ originally_locked = args[2] || Bundler::SpecSet.new([])
+ unlock = args[3] || []
+ @deps.each do |d|
+ name = d.name
+ source_requirements[name] = d.source = default_source
+ end
+ packages = Bundler::Resolver::Base.new(source_requirements, @deps, base, @platforms, locked_specs: originally_locked, unlock: unlock)
+ Bundler::Resolver.new(packages, gem_version_promoter).start
+ end
+
+ def should_not_resolve
+ expect { resolve }.to raise_error(Bundler::GemNotFound)
+ end
+
+ def should_resolve_as(specs)
+ got = resolve
+ got = got.map(&:full_name).sort
+ expect(got).to eq(specs.sort)
+ end
+
+ def should_resolve_without_dependency_api(specs)
+ got = resolve(dependency_api_available: false)
+ got = got.map(&:full_name).sort
+ expect(got).to eq(specs.sort)
+ end
+
+ def should_resolve_and_include(specs, args = [])
+ got = resolve(args)
+ got = got.map(&:full_name).sort
+ specs.each do |s|
+ expect(got).to include(s)
+ end
+ end
+
+ def gem(*args, &blk)
+ build_spec(*args, &blk).first
+ end
+
+ def locked(*args)
+ Bundler::SpecSet.new(args.map do |name, version|
+ gem(name, version)
+ end)
+ end
+
+ def should_conservative_resolve_and_include(opts, unlock, specs)
+ opts = Array(opts)
+ search = Bundler::GemVersionPromoter.new.tap do |s|
+ s.level = opts.first
+ s.strict = opts.include?(:strict)
+ end
+ should_resolve_and_include specs, [@base, search, @locked, unlock]
+ end
+
+ def an_awesome_index
+ build_index do
+ gem "myrack", %w[0.8 0.9 0.9.1 0.9.2 1.0 1.1]
+ gem "myrack-mount", %w[0.4 0.5 0.5.1 0.5.2 0.6]
+
+ # --- Pre-release support
+ gem "RubyGems\0", ["1.3.2"]
+
+ # --- Rails
+ versions "1.2.3 2.2.3 2.3.5 3.0.0.beta 3.0.0.beta1" do |version|
+ gem "activesupport", version
+ gem "actionpack", version do
+ dep "activesupport", version
+ if version >= v("3.0.0.beta")
+ dep "myrack", "~> 1.1"
+ dep "myrack-mount", ">= 0.5"
+ elsif version > v("2.3") then dep "myrack", "~> 1.0.0"
+ elsif version > v("2.0.0") then dep "myrack", "~> 0.9.0"
+ end
+ end
+ gem "activerecord", version do
+ dep "activesupport", version
+ dep "arel", ">= 0.2" if version >= v("3.0.0.beta")
+ end
+ gem "actionmailer", version do
+ dep "activesupport", version
+ dep "actionmailer", version
+ end
+ if version < v("3.0.0.beta")
+ gem "railties", version do
+ dep "activerecord", version
+ dep "actionpack", version
+ dep "actionmailer", version
+ dep "activesupport", version
+ end
+ else
+ gem "railties", version
+ gem "rails", version do
+ dep "activerecord", version
+ dep "actionpack", version
+ dep "actionmailer", version
+ dep "activesupport", version
+ dep "railties", version
+ end
+ end
+ end
+
+ versions "1.0 1.2 1.2.1 1.2.2 1.3 1.3.0.1 1.3.5 1.4.0 1.4.2 1.4.2.1" do |version|
+ platforms "ruby java mswin32 mingw32 x64-mingw-ucrt" do |platform|
+ next if version == v("1.4.2.1") && platform != pl("x86-mswin32")
+ next if version == v("1.4.2") && platform == pl("x86-mswin32")
+ gem "nokogiri", version, platform do
+ dep "weakling", ">= 0.0.3" if platform =~ pl("java") # rubocop:disable Performance/RegexpMatch
+ end
+ end
+ end
+
+ versions "0.0.1 0.0.2 0.0.3" do |version|
+ gem "weakling", version
+ end
+
+ # --- Rails related
+ versions "1.2.3 2.2.3 2.3.5" do |version|
+ gem "activemerchant", version do
+ dep "activesupport", ">= #{version}"
+ end
+ end
+
+ gem "reform", ["1.0.0"] do
+ dep "activesupport", ">= 1.0.0.beta1"
+ end
+
+ gem "need-pre", ["1.0.0"] do
+ dep "activesupport", "~> 3.0.0.beta1"
+ end
+ end
+ end
+
+ # Builder 3.1.4 will activate first, but if all
+ # goes well, it should resolve to 3.0.4
+ def a_conflict_index
+ build_index do
+ gem "builder", %w[3.0.4 3.1.4]
+ gem("grape", "0.2.6") do
+ dep "builder", ">= 0"
+ end
+
+ versions "3.2.8 3.2.9 3.2.10 3.2.11" do |version|
+ gem("activemodel", version) do
+ dep "builder", "~> 3.0.0"
+ end
+ end
+
+ gem("my_app", "1.0.0") do
+ dep "activemodel", ">= 0"
+ dep "grape", ">= 0"
+ end
+ end
+ end
+
+ def a_complex_conflict_index
+ build_index do
+ gem("a", %w[1.0.2 1.1.4 1.2.0 1.4.0]) do
+ dep "d", ">= 0"
+ end
+
+ gem("d", %w[1.3.0 1.4.1]) do
+ dep "x", ">= 0"
+ end
+
+ gem "d", "0.9.8"
+
+ gem("b", "0.3.4") do
+ dep "a", ">= 1.5.0"
+ end
+
+ gem("b", "0.3.5") do
+ dep "a", ">= 1.2"
+ end
+
+ gem("b", "0.3.3") do
+ dep "a", "> 1.0"
+ end
+
+ versions "3.2 3.3" do |version|
+ gem("c", version) do
+ dep "a", "~> 1.0"
+ end
+ end
+
+ gem("my_app", "1.3.0") do
+ dep "c", ">= 4.0"
+ dep "b", ">= 0"
+ end
+
+ gem("my_app", "1.2.0") do
+ dep "c", "~> 3.3.0"
+ dep "b", "0.3.4"
+ end
+
+ gem("my_app", "1.1.0") do
+ dep "c", "~> 3.2.0"
+ dep "b", "0.3.5"
+ end
+ end
+ end
+
+ def index_with_conflict_on_child
+ build_index do
+ gem "json", %w[1.6.5 1.7.7 1.8.0]
+
+ gem("chef", "10.26") do
+ dep "json", [">= 1.4.4", "<= 1.7.7"]
+ end
+
+ gem("berkshelf", "2.0.7") do
+ dep "json", ">= 1.7.7"
+ end
+
+ gem("chef_app", "1.0.0") do
+ dep "berkshelf", "~> 2.0"
+ dep "chef", "~> 10.26"
+ end
+ end
+ end
+
+ # Issue #3459
+ def a_complicated_index
+ build_index do
+ gem "foo", %w[3.0.0 3.0.5] do
+ dep "qux", ["~> 3.1"]
+ dep "baz", ["< 9.0", ">= 5.0"]
+ dep "bar", ["~> 1.0"]
+ dep "grault", ["~> 3.1"]
+ end
+
+ gem "foo", "1.2.1" do
+ dep "baz", ["~> 4.2"]
+ dep "bar", ["~> 1.0"]
+ dep "qux", ["~> 3.1"]
+ dep "grault", ["~> 2.0"]
+ end
+
+ gem "bar", "1.0.5" do
+ dep "grault", ["~> 3.1"]
+ dep "baz", ["< 9", ">= 4.2"]
+ end
+
+ gem "bar", "1.0.3" do
+ dep "baz", ["< 9", ">= 4.2"]
+ dep "grault", ["~> 2.0"]
+ end
+
+ gem "baz", "8.2.10" do
+ dep "grault", ["~> 3.0"]
+ dep "garply", [">= 0.5.1", "~> 0.5"]
+ end
+
+ gem "baz", "5.0.2" do
+ dep "grault", ["~> 2.0"]
+ dep "garply", [">= 0.3.1"]
+ end
+
+ gem "baz", "4.2.0" do
+ dep "grault", ["~> 2.0"]
+ dep "garply", [">= 0.3.1"]
+ end
+
+ gem "grault", %w[2.6.3 3.1.1]
+
+ gem "garply", "0.5.1" do
+ dep "waldo", ["~> 0.1.3"]
+ end
+
+ gem "waldo", "0.1.5" do
+ dep "plugh", ["~> 0.6.0"]
+ end
+
+ gem "plugh", %w[0.6.3 0.6.11 0.7.0]
+
+ gem "qux", "3.2.21" do
+ dep "plugh", [">= 0.6.4", "~> 0.6"]
+ dep "corge", ["~> 1.0"]
+ end
+
+ gem "corge", "1.10.1"
+ end
+ end
+
+ def a_unresolvable_child_index
+ build_index do
+ gem "json", %w[1.8.0]
+
+ gem("chef", "10.26") do
+ dep "json", [">= 1.4.4", "<= 1.7.7"]
+ end
+
+ gem("berkshelf", "2.0.7") do
+ dep "json", ">= 1.7.7"
+ end
+
+ gem("chef_app_error", "1.0.0") do
+ dep "berkshelf", "~> 2.0"
+ dep "chef", "~> 10.26"
+ end
+ end
+ end
+
+ def a_index_with_root_conflict_on_child
+ build_index do
+ gem "builder", %w[2.1.2 3.0.1 3.1.3]
+ gem "i18n", %w[0.4.1 0.4.2]
+
+ gem "activesupport", %w[3.0.0 3.0.1 3.0.5 3.1.7]
+
+ gem("activemodel", "3.0.5") do
+ dep "activesupport", "= 3.0.5"
+ dep "builder", "~> 2.1.2"
+ dep "i18n", "~> 0.4"
+ end
+
+ gem("activemodel", "3.0.0") do
+ dep "activesupport", "= 3.0.0"
+ dep "builder", "~> 2.1.2"
+ dep "i18n", "~> 0.4.1"
+ end
+
+ gem("activemodel", "3.1.3") do
+ dep "activesupport", "= 3.1.3"
+ dep "builder", "~> 2.1.2"
+ dep "i18n", "~> 0.5"
+ end
+
+ gem("activerecord", "3.0.0") do
+ dep "activesupport", "= 3.0.0"
+ dep "activemodel", "= 3.0.0"
+ end
+
+ gem("activerecord", "3.0.5") do
+ dep "activesupport", "= 3.0.5"
+ dep "activemodel", "= 3.0.5"
+ end
+
+ gem("activerecord", "3.0.9") do
+ dep "activesupport", "= 3.1.5"
+ dep "activemodel", "= 3.1.5"
+ end
+ end
+ end
+
+ def a_circular_index
+ build_index do
+ gem "myrack", "1.0.1"
+ gem("foo", "0.2.6") do
+ dep "bar", ">= 0"
+ end
+
+ gem("bar", "1.0.0") do
+ dep "foo", ">= 0"
+ end
+
+ gem("circular_app", "1.0.0") do
+ dep "foo", ">= 0"
+ dep "bar", ">= 0"
+ end
+ end
+ end
+
+ def an_ambiguous_index
+ build_index do
+ gem("a", "1.0.0") do
+ dep "c", ">= 0"
+ end
+
+ gem("b", %w[0.5.0 1.0.0])
+
+ gem("b", "2.0.0") do
+ dep "c", "< 2.0.0"
+ end
+
+ gem("c", "1.0.0") do
+ dep "d", "1.0.0"
+ end
+
+ gem("c", "2.0.0") do
+ dep "d", "2.0.0"
+ end
+
+ gem("d", %w[1.0.0 2.0.0])
+ end
+ end
+
+ def optional_prereleases_index
+ build_index do
+ gem("a", %w[1.0.0])
+
+ gem("a", "2.0.0") do
+ dep "b", ">= 2.0.0.pre"
+ end
+
+ gem("b", %w[0.9.0 1.5.0 2.0.0.pre])
+
+ # --- Pre-release support
+ gem "RubyGems\0", ["1.3.2"]
+ end
+ end
+ end
+end
diff --git a/spec/bundler/support/matchers.rb b/spec/bundler/support/matchers.rb
new file mode 100644
index 0000000000..5a3c38a4db
--- /dev/null
+++ b/spec/bundler/support/matchers.rb
@@ -0,0 +1,227 @@
+# frozen_string_literal: true
+
+require "forwardable"
+require_relative "the_bundle"
+
+module Spec
+ module Matchers
+ extend RSpec::Matchers
+
+ class Precondition
+ include RSpec::Matchers::Composable
+ extend Forwardable
+ def_delegators :failing_matcher,
+ :failure_message,
+ :actual,
+ :description,
+ :diffable?,
+ :expected,
+ :failure_message_when_negated
+
+ def initialize(matcher, preconditions)
+ @matcher = with_matchers_cloned(matcher)
+ @preconditions = with_matchers_cloned(preconditions)
+ @failure_index = nil
+ end
+
+ def matches?(target, &blk)
+ return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) }
+ @matcher.matches?(target, &blk)
+ end
+
+ def does_not_match?(target, &blk)
+ return false if @failure_index = @preconditions.index {|pc| !pc.matches?(target, &blk) }
+ if @matcher.respond_to?(:does_not_match?)
+ @matcher.does_not_match?(target, &blk)
+ else
+ !@matcher.matches?(target, &blk)
+ end
+ end
+
+ def expects_call_stack_jump?
+ @matcher.expects_call_stack_jump? || @preconditions.any?(&:expects_call_stack_jump)
+ end
+
+ def supports_block_expectations?
+ @matcher.supports_block_expectations? || @preconditions.any?(&:supports_block_expectations)
+ end
+
+ def failing_matcher
+ @failure_index ? @preconditions[@failure_index] : @matcher
+ end
+ end
+
+ def self.define_compound_matcher(matcher, preconditions, &declarations)
+ raise ArgumentError, "Must have preconditions to define a compound matcher" if preconditions.empty?
+ define_method(matcher) do |*expected, &block_arg|
+ Precondition.new(
+ RSpec::Matchers::DSL::Matcher.new(matcher, declarations, self, *expected, &block_arg),
+ preconditions
+ )
+ end
+ end
+
+ RSpec::Matchers.define :have_dep do |*args|
+ dep = Bundler::Dependency.new(*args)
+
+ match do |actual|
+ actual.length == 1 && actual.all? {|d| d == dep }
+ end
+ end
+
+ RSpec::Matchers.define :have_gem do |*args|
+ match do |actual|
+ actual.length == args.length && actual.all? {|a| args.include?(a.full_name) }
+ end
+ end
+
+ RSpec::Matchers.define :be_sorted do
+ diffable
+ attr_reader :expected
+ match do |actual|
+ expected = block_arg ? actual.sort_by(&block_arg) : actual.sort
+ actual.==(expected).tap do
+ # HACK: since rspec won't show a diff when everything is a string
+ differ = RSpec::Support::Differ.new
+ @actual = differ.send(:object_to_string, actual)
+ @expected = differ.send(:object_to_string, expected)
+ end
+ end
+ end
+
+ RSpec::Matchers.define :be_well_formed do
+ match(&:empty?)
+
+ failure_message do |actual|
+ actual.join("\n")
+ end
+ end
+
+ define_compound_matcher :read_as, [exist] do |file_contents|
+ diffable
+
+ match do |actual|
+ @actual = Bundler.read_file(actual)
+ values_match?(file_contents, @actual)
+ end
+ end
+
+ def indent(string, padding = 4, indent_character = " ")
+ string.to_s.gsub(/^/, indent_character * padding).gsub("\t", " ")
+ end
+
+ define_compound_matcher :include_gems, [be_an_instance_of(Spec::TheBundle)] do |*names|
+ match do
+ opts = names.last.is_a?(Hash) ? names.pop : {}
+ source = opts.delete(:source)
+ groups = Array(opts.delete(:groups)).map(&:inspect).join(", ")
+ opts[:raise_on_error] = false
+ @errors = names.filter_map do |full_name|
+ name, version, platform = full_name.split(/\s+/)
+ platform ||= "ruby"
+ require_path = name.tr("-", "/")
+ version_const = name == "bundler" ? "Bundler::VERSION" : Spec::Builders.constantize(name)
+ source_const = "#{Spec::Builders.constantize(name)}_SOURCE"
+ ruby <<~R, opts
+ require 'bundler'
+ Bundler.setup(#{groups})
+
+ require '#{require_path}'
+ actual_version, actual_platform = #{version_const}.split(/\s+/, 2)
+ actual_platform ||= "ruby"
+ unless Gem::Version.new(actual_version) == Gem::Version.new('#{version}')
+ puts actual_version
+ exit 64
+ end
+ unless actual_platform.to_s == '#{platform}'
+ puts actual_platform
+ exit 65
+ end
+ require '#{require_path}/source'
+ exit 0 if #{source.nil?}
+ actual_source = #{source_const}
+ unless actual_source == '#{source}'
+ puts actual_source
+ exit 66
+ end
+ R
+ next if exitstatus == 0
+ if exitstatus == 64
+ actual_version = out.split("\n").last
+ next "#{name} was expected to be at version #{version} but was #{actual_version}"
+ end
+ if exitstatus == 65
+ actual_platform = out.split("\n").last
+ next "#{name} was expected to be of platform #{platform} but was #{actual_platform}"
+ end
+ if exitstatus == 66
+ actual_source = out.split("\n").last
+ next "Expected #{name} (#{version}) to be installed from `#{source}`, was actually from `#{actual_source}`"
+ end
+ next "Command to check for inclusion of gem #{full_name} failed"
+ end
+
+ @errors.empty?
+ end
+
+ match_when_negated do
+ opts = names.last.is_a?(Hash) ? names.pop : {}
+ groups = Array(opts.delete(:groups)).map(&:inspect).join(", ")
+ opts[:raise_on_error] = false
+ @errors = names.filter_map do |name|
+ name, version = name.split(/\s+/, 2)
+ ruby <<-R, opts
+ begin
+ require 'bundler'
+ Bundler.setup(#{groups})
+ rescue Bundler::GemNotFound, Bundler::GitError
+ exit 0
+ end
+
+ begin
+ require '#{name}'
+ name_constant = #{Spec::Builders.constantize(name)}
+ if #{version.nil?} || name_constant == '#{version}'
+ exit 64
+ else
+ exit 0
+ end
+ rescue LoadError, NameError
+ exit 0
+ end
+ R
+ next if exitstatus == 0
+ next "command to check version of #{name} installed failed" unless exitstatus == 64
+ next "expected #{name} to not be installed, but it was" if version.nil?
+ next "expected #{name} (#{version}) not to be installed, but it was"
+ end
+
+ @errors.empty?
+ end
+
+ failure_message do
+ super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n")
+ end
+
+ failure_message_when_negated do
+ super() + " but:\n" + @errors.map {|e| indent(e) }.join("\n")
+ end
+ end
+ RSpec::Matchers.define_negated_matcher :not_include_gems, :include_gems
+ RSpec::Matchers.alias_matcher :include_gem, :include_gems
+
+ def plugin_should_be_installed(*names)
+ names.each do |name|
+ expect(Bundler::Plugin).to be_installed(name)
+ path = Pathname.new(Bundler::Plugin.installed?(name))
+ expect(path + "plugins.rb").to exist
+ end
+ end
+
+ def plugin_should_not_be_installed(*names)
+ names.each do |name|
+ expect(Bundler::Plugin).not_to be_installed(name)
+ end
+ end
+ end
+end
diff --git a/spec/bundler/support/options.rb b/spec/bundler/support/options.rb
new file mode 100644
index 0000000000..551fa1acd8
--- /dev/null
+++ b/spec/bundler/support/options.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Spec
+ module Options
+ def opt_add(option, options)
+ [option.strip, options].compact.reject(&:empty?).join(" ")
+ end
+
+ def opt_remove(option, options)
+ return unless options
+
+ options.split(" ").reject {|opt| opt.strip == option.strip }.join(" ")
+ end
+ end
+end
diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb
new file mode 100644
index 0000000000..2e6486412f
--- /dev/null
+++ b/spec/bundler/support/path.rb
@@ -0,0 +1,381 @@
+# frozen_string_literal: true
+
+require "pathname" unless defined?(Pathname)
+require "rbconfig"
+
+require_relative "env"
+
+module Spec
+ module Path
+ include Spec::Env
+
+ def source_root
+ @source_root ||= Pathname.new(ruby_core? ? "../../.." : "../../bundler").expand_path(__dir__)
+ end
+
+ def root
+ @root ||= system_gem_path("gems/bundler-#{Bundler::VERSION}")
+ end
+
+ def gemspec
+ @gemspec ||= source_root.join(relative_gemspec)
+ end
+
+ def relative_gemspec
+ @relative_gemspec ||= ruby_core? ? "lib/bundler/bundler.gemspec" : "bundler.gemspec"
+ end
+
+ def loaded_gemspec
+ @loaded_gemspec ||= Dir.chdir(source_root) { Gem::Specification.load(gemspec.to_s) }
+ end
+
+ def test_gemfile
+ @test_gemfile ||= tool_dir.join("test_gems.rb")
+ end
+
+ def rubocop_gemfile
+ @rubocop_gemfile ||= source_root.join(rubocop_gemfile_basename)
+ end
+
+ def standard_gemfile
+ @standard_gemfile ||= source_root.join(standard_gemfile_basename)
+ end
+
+ def dev_gemfile
+ @dev_gemfile ||= tool_dir.join("dev_gems.rb")
+ end
+
+ def dev_binstub
+ @dev_binstub ||= bindir.join("bundle")
+ end
+
+ def bindir
+ @bindir ||= source_root.join(ruby_core? ? "spec/bin" : "../bin")
+ end
+
+ def exedir
+ @exedir ||= source_root.join(ruby_core? ? "libexec" : "exe")
+ end
+
+ def installed_bindir
+ @installed_bindir ||= system_gem_path("bin")
+ end
+
+ def gem_cmd
+ @gem_cmd ||= ruby_core? ? source_root.join("bin/gem") : "gem"
+ end
+
+ def gem_bin
+ @gem_bin ||= ENV["GEM_COMMAND"] || "gem"
+ end
+
+ def path
+ env_path = ENV["PATH"]
+ env_path = env_path.split(File::PATH_SEPARATOR).reject {|path| path == exedir.to_s }.join(File::PATH_SEPARATOR) if ruby_core?
+ env_path
+ end
+
+ def spec_dir
+ @spec_dir ||= source_root.join(ruby_core? ? "spec/bundler" : "../spec")
+ end
+
+ def man_dir
+ @man_dir ||= lib_dir.join("bundler/man")
+ end
+
+ def hax
+ @hax ||= spec_dir.join("support/hax.rb")
+ end
+
+ def tracked_files
+ @tracked_files ||= git_ls_files(tracked_files_glob)
+ end
+
+ def shipped_files
+ @shipped_files ||= if ruby_core_tarball?
+ loaded_gemspec.files.map {|f| f.gsub(%r{^exe/}, "libexec/") }
+ elsif ruby_core?
+ tracked_files
+ else
+ loaded_gemspec.files
+ end
+ end
+
+ def lib_tracked_files
+ @lib_tracked_files ||= git_ls_files(lib_tracked_files_glob)
+ end
+
+ def man_tracked_files
+ @man_tracked_files ||= git_ls_files(man_tracked_files_glob)
+ end
+
+ def tmp(*path)
+ tmp_root.join("#{test_env_version}.#{scope}").join(*path)
+ end
+
+ def tmp_root
+ if ruby_core? && (tmpdir = ENV["TMPDIR"])
+ # Use realpath to resolve any symlinks in TMPDIR (e.g., on macOS /var -> /private/var)
+ real = begin
+ File.realpath(tmpdir)
+ rescue Errno::ENOENT, Errno::EACCES
+ tmpdir
+ end
+ Pathname(real)
+ else
+ (ruby_core? ? source_root : source_root.parent).join("tmp")
+ end
+ end
+
+ # Bump this version whenever you make a breaking change to the spec setup
+ # that requires regenerating tmp/.
+
+ def test_env_version
+ 2
+ end
+
+ def scope
+ test_number = ENV["TEST_ENV_NUMBER"]
+ return "1" if test_number.nil?
+
+ test_number.empty? ? "1" : test_number
+ end
+
+ def home(*path)
+ tmp("home", *path)
+ end
+
+ def default_bundle_path(*path)
+ system_gem_path(*path)
+ end
+
+ def default_cache_path(*path)
+ default_bundle_path("cache/bundler", *path)
+ end
+
+ def compact_index_cache_path
+ home(".bundle/cache/compact_index")
+ end
+
+ def bundled_app(*path)
+ root = tmp("bundled_app")
+ FileUtils.mkdir_p(root)
+ root.join(*path)
+ end
+
+ def bundled_app2(*path)
+ root = tmp("bundled_app2")
+ FileUtils.mkdir_p(root)
+ root.join(*path)
+ end
+
+ def vendored_gems(path = nil)
+ scoped_gem_path(bundled_app("vendor/bundle")).join(*[path].compact)
+ end
+
+ def cached_gem(path)
+ bundled_app("vendor/cache/#{path}.gem")
+ end
+
+ def bundled_app_gemfile
+ bundled_app("Gemfile")
+ end
+
+ def bundled_app_lock
+ bundled_app("Gemfile.lock")
+ end
+
+ def scoped_base_system_gem_path
+ scoped_gem_path(base_system_gem_path)
+ end
+
+ def base_system_gem_path
+ tmp_root.join("gems/base")
+ end
+
+ def rubocop_gem_path
+ tmp_root.join("gems/rubocop")
+ end
+
+ def standard_gem_path
+ tmp_root.join("gems/standard")
+ end
+
+ def file_uri_for(path)
+ protocol = "file://"
+ root = Gem.win_platform? ? "/" : ""
+
+ protocol + root + path.to_s
+ end
+
+ def gem_repo1(*args)
+ gem_path("remote1", *args)
+ end
+
+ def gem_repo_missing(*args)
+ gem_path("missing", *args)
+ end
+
+ def gem_repo2(*args)
+ gem_path("remote2", *args)
+ end
+
+ def gem_repo3(*args)
+ gem_path("remote3", *args)
+ end
+
+ def gem_repo4(*args)
+ gem_path("remote4", *args)
+ end
+
+ def security_repo(*args)
+ gem_path("security_repo", *args)
+ end
+
+ def system_gem_path(*path)
+ gem_path("system", *path)
+ end
+
+ def pristine_system_gem_path
+ tmp_root.join("gems/pristine_system")
+ end
+
+ def local_gem_path(*path, base: bundled_app)
+ scoped_gem_path(base.join(".bundle")).join(*path)
+ end
+
+ def scoped_gem_path(base)
+ base.join(Gem.ruby_engine, RbConfig::CONFIG["ruby_version"])
+ end
+
+ def gem_path(*args)
+ tmp("gems", *args)
+ end
+
+ def lib_path(*args)
+ tmp("libs", *args)
+ end
+
+ def source_lib_dir
+ source_root.join("lib")
+ end
+
+ def lib_dir
+ root.join("lib")
+ end
+
+ def global_plugin_gem(*args)
+ home ".bundle", "plugin", "gems", *args
+ end
+
+ def local_plugin_gem(*args)
+ bundled_app ".bundle", "plugin", "gems", *args
+ end
+
+ def tmpdir(*args)
+ tmp "tmpdir", *args
+ end
+
+ def replace_version_file(version, dir: source_root)
+ version_file = File.expand_path("lib/bundler/version.rb", dir)
+ contents = File.read(version_file)
+ contents.sub!(/(^\s+VERSION\s*=\s*).*$/, %(\\1"#{version}"))
+ File.open(version_file, "w") {|f| f << contents }
+ end
+
+ def replace_required_ruby_version(version, dir:)
+ gemspec_file = File.expand_path("bundler.gemspec", dir)
+ contents = File.read(gemspec_file)
+ contents.sub!(/(^\s+s\.required_ruby_version\s*=\s*)"[^"]+"/, %(\\1"#{version}"))
+ File.open(gemspec_file, "w") {|f| f << contents }
+ end
+
+ def replace_changelog(version, dir:)
+ changelog = File.expand_path("CHANGELOG.md", dir)
+ contents = File.readlines(changelog)
+ contents = [contents[0], contents[1], "## #{version} (2100-01-01)\n", *contents[3..-1]].join
+ File.open(changelog, "w") {|f| f << contents }
+ end
+
+ def git_root
+ ruby_core? ? source_root : source_root.parent
+ end
+
+ def rake_path
+ find_base_path("rake")
+ end
+
+ def rake_version
+ File.basename(rake_path).delete_prefix("rake-").delete_suffix(".gem")
+ end
+
+ def sinatra_dependency_paths
+ deps = %w[
+ mustermann
+ rack
+ rack-protection
+ rack-session
+ tilt
+ sinatra
+ base64
+ logger
+ compact_index
+ ]
+ path = if deps.all? {|dep| !Dir[scoped_base_system_gem_path.join("gems/#{dep}-*")].empty? }
+ scoped_base_system_gem_path
+ elsif ruby_core? && deps.all? {|dep| !Dir[source_root.join(".bundle/gems/#{dep}-*")].empty? }
+ source_root.join(".bundle")
+ else
+ scoped_base_system_gem_path
+ end
+
+ Dir[path.join("gems/{#{deps.join(",")}}-*/lib")].map(&:to_s)
+ end
+
+ private
+
+ def find_base_path(name)
+ Dir["#{scoped_base_system_gem_path}/**/#{name}-*.gem"].first
+ end
+
+ def git_ls_files(glob)
+ skip "Not running on a git context, since running tests from a tarball" if ruby_core_tarball?
+
+ git("ls-files -z -- #{glob}", source_root).split("\x0")
+ end
+
+ def tracked_files_glob
+ ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb spec/bundler man/bundle*" : "lib exe CHANGELOG.md LICENSE.md README.md bundler.gemspec"
+ end
+
+ def lib_tracked_files_glob
+ ruby_core? ? "lib/bundler lib/bundler.rb" : "lib"
+ end
+
+ def man_tracked_files_glob
+ "lib/bundler/man/bundle*.1.ronn lib/bundler/man/gemfile*.5.ronn"
+ end
+
+ def ruby_core_tarball?
+ !git_root.join(".git").directory?
+ end
+
+ def rubocop_gemfile_basename
+ tool_dir.join("rubocop_gems.rb")
+ end
+
+ def standard_gemfile_basename
+ tool_dir.join("standard_gems.rb")
+ end
+
+ def tool_dir
+ ruby_core? ? source_root.join("tool/bundler") : source_root.join("../tool/bundler")
+ end
+
+ def templates_dir
+ lib_dir.join("bundler", "templates")
+ end
+
+ extend self
+ end
+end
diff --git a/spec/bundler/support/permissions.rb b/spec/bundler/support/permissions.rb
new file mode 100644
index 0000000000..b21ce3848d
--- /dev/null
+++ b/spec/bundler/support/permissions.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Spec
+ module Permissions
+ def with_umask(new_umask)
+ old_umask = File.umask(new_umask)
+ yield if block_given?
+ ensure
+ File.umask(old_umask)
+ end
+ end
+end
diff --git a/spec/bundler/support/platforms.rb b/spec/bundler/support/platforms.rb
new file mode 100644
index 0000000000..56a0843005
--- /dev/null
+++ b/spec/bundler/support/platforms.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Spec
+ module Platforms
+ def not_local
+ generic_local_platform == Gem::Platform::RUBY ? "java" : Gem::Platform::RUBY
+ end
+
+ def local_platform
+ Bundler.local_platform
+ end
+
+ def generic_local_platform
+ Gem::Platform.generic(local_platform)
+ end
+
+ def local_tag
+ if Gem.java_platform?
+ :jruby
+ elsif Gem.win_platform?
+ :windows
+ else
+ :ruby
+ end
+ end
+
+ def not_local_tag
+ [:jruby, :windows, :ruby].find {|tag| tag != local_tag }
+ end
+
+ def local_ruby_engine
+ RUBY_ENGINE
+ end
+
+ def local_engine_version
+ RUBY_ENGINE == "ruby" ? Gem.ruby_version : RUBY_ENGINE_VERSION
+ end
+
+ def not_local_engine_version
+ case not_local_tag
+ when :ruby, :windows
+ not_local_ruby_version
+ when :jruby
+ "1.6.1"
+ end
+ end
+
+ def not_local_ruby_version
+ "1.12"
+ end
+
+ def not_local_patchlevel
+ 9999
+ end
+
+ def default_platform_list(*extra, defaults: default_locked_platforms)
+ defaults.concat(extra).map(&:to_s).uniq
+ end
+
+ def lockfile_platforms(*extra, defaults: default_locked_platforms)
+ platforms = default_platform_list(*extra, defaults: defaults)
+ platforms.sort.join("\n ")
+ end
+
+ def default_locked_platforms
+ [local_platform, generic_default_locked_platform].compact
+ end
+
+ def generic_default_locked_platform
+ return unless Bundler::MatchPlatform.generic_local_platform_is_ruby?
+
+ Gem::Platform::RUBY
+ end
+ end
+end
diff --git a/spec/bundler/support/rubygems_ext.rb b/spec/bundler/support/rubygems_ext.rb
new file mode 100644
index 0000000000..812dc4deaa
--- /dev/null
+++ b/spec/bundler/support/rubygems_ext.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+abort "RubyGems only supports Ruby 3.2 or higher" if RUBY_VERSION < "3.2.0"
+
+require_relative "path"
+
+$LOAD_PATH.unshift(Spec::Path.source_lib_dir.to_s)
+
+module Spec
+ module Rubygems
+ extend self
+
+ def gem_load(gem_name, bin_container)
+ require_relative "switch_rubygems"
+
+ gem_load_and_activate(gem_name, bin_container)
+ end
+
+ def gem_load_and_possibly_install(gem_name, bin_container)
+ require_relative "switch_rubygems"
+
+ gem_load_activate_and_possibly_install(gem_name, bin_container)
+ end
+
+ def gem_require(gem_name, entrypoint)
+ gem_activate(gem_name)
+ require entrypoint
+ end
+
+ def test_setup
+ # Install test dependencies unless parallel-rspec is being used, since in that case they should be setup already
+ install_test_deps unless ENV["RSPEC_FORMATTER_OUTPUT_ID"]
+
+ setup_test_paths
+
+ require "fileutils"
+
+ FileUtils.mkdir_p(Path.home)
+ FileUtils.mkdir_p(Path.tmpdir)
+
+ ENV["HOME"] = Path.home.to_s
+ # Remove "RUBY_CODESIGN", which is used by mkmf-generated Makefile to
+ # sign extension bundles on macOS, to avoid trying to find the specified key
+ # from the fake $HOME/Library/Keychains directory.
+ ENV.delete "RUBY_CODESIGN"
+ if Path.ruby_core?
+ if (tmpdir = ENV["TMPDIR"])
+ tmpdir_real = begin
+ File.realpath(tmpdir)
+ rescue Errno::ENOENT, Errno::EACCES
+ tmpdir
+ end
+ ENV["TMPDIR"] = tmpdir_real if tmpdir_real != tmpdir
+ end
+ else
+ ENV["TMPDIR"] = Path.tmpdir.to_s
+ end
+
+ require "rubygems/user_interaction"
+ Gem::DefaultUserInteraction.ui = Gem::SilentUI.new
+ end
+
+ def setup_test_paths
+ ENV["BUNDLE_PATH"] = nil
+ ENV["PATH"] = [Path.system_gem_path("bin"), ENV["PATH"]].join(File::PATH_SEPARATOR)
+ ENV["PATH"] = [Path.exedir, ENV["PATH"]].join(File::PATH_SEPARATOR) if Path.ruby_core?
+ end
+
+ def install_test_deps
+ dev_bundle("install", gemfile: test_gemfile, path: Path.base_system_gem_path.to_s)
+ dev_bundle("install", gemfile: rubocop_gemfile, path: Path.rubocop_gem_path.to_s)
+ dev_bundle("install", gemfile: standard_gemfile, path: Path.standard_gem_path.to_s)
+
+ require_relative "helpers"
+ Helpers.install_dev_bundler
+
+ install_vendored_compact_index
+ end
+
+ # Vendor `rubygems/rubygems.org#lib/compact_index/` under `tmp/compact_index/`
+ # so the artifice can serve compact-index responses without a runtime gem
+ # dependency. Pinned to a reviewed commit; override with COMPACT_INDEX_REF
+ # to refresh against another ref (the existing vendor copy is discarded).
+ def install_vendored_compact_index
+ target_root = Path.tmp_root.join("compact_index")
+ require "fileutils"
+ FileUtils.mkdir_p(Path.tmp_root)
+
+ files = %w[
+ lib/compact_index.rb
+ lib/compact_index/dependency.rb
+ lib/compact_index/gem.rb
+ lib/compact_index/gem_version.rb
+ lib/compact_index/versions_file.rb
+ ]
+
+ # Serialize installs so parallel test setups don't race on the same
+ # vendor tree, and only skip the download when every file is present so
+ # an interrupted run can't leave a partial copy behind.
+ File.open(Path.tmp_root.join("compact_index.lock"), File::CREAT | File::RDWR) do |lock|
+ lock.flock(File::LOCK_EX)
+
+ FileUtils.rm_rf(target_root) if ENV["COMPACT_INDEX_REF"]
+
+ next if files.all? {|path| File.exist?(target_root.join(path)) }
+
+ require "open-uri"
+ ref = ENV["COMPACT_INDEX_REF"] || "7c68a7b39761c61a66f9299f85b889ec39afc02c"
+ files.each do |path|
+ url = "https://raw.githubusercontent.com/rubygems/rubygems.org/#{ref}/#{path}"
+ target = target_root.join(path)
+ FileUtils.mkdir_p(File.dirname(target))
+ tmp = "#{target}.tmp"
+ File.write(tmp, URI.parse(url).open(&:read))
+ File.rename(tmp, target)
+ end
+ end
+ end
+
+ def check_source_control_changes(success_message:, error_message:)
+ require "open3"
+
+ output, status = Open3.capture2e("git status --porcelain")
+
+ if status.success? && output.empty?
+ puts
+ puts success_message
+ puts
+ else
+ system("git diff")
+
+ puts
+ puts error_message
+ puts
+
+ exit(1)
+ end
+ end
+
+ def dev_bundle(*args, gemfile: dev_gemfile, path: nil)
+ old_gemfile = ENV["BUNDLE_GEMFILE"]
+ old_orig_gemfile = ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"]
+ ENV["BUNDLE_GEMFILE"] = gemfile.to_s
+ ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = nil
+
+ if path
+ old_path = ENV["BUNDLE_PATH"]
+ ENV["BUNDLE_PATH"] = path
+ else
+ old_path__system = ENV["BUNDLE_PATH__SYSTEM"]
+ ENV["BUNDLE_PATH__SYSTEM"] = "true"
+ end
+
+ require "shellwords"
+ # We don't use `Open3` here because it does not work on JRuby + Windows
+ output = `ruby #{Path.dev_binstub} #{args.shelljoin}`
+ raise output unless $?.success?
+ output
+ ensure
+ if path
+ ENV["BUNDLE_PATH"] = old_path
+ else
+ ENV["BUNDLE_PATH__SYSTEM"] = old_path__system
+ end
+
+ ENV["BUNDLER_ORIG_BUNDLE_GEMFILE"] = old_orig_gemfile
+ ENV["BUNDLE_GEMFILE"] = old_gemfile
+ end
+
+ private
+
+ def gem_load_and_activate(gem_name, bin_container)
+ gem_activate(gem_name)
+ load Gem.bin_path(gem_name, bin_container)
+ rescue Gem::LoadError => e
+ abort "We couldn't activate #{gem_name} (#{e.requirement}). Run `gem install #{gem_name}:'#{e.requirement}'`"
+ end
+
+ def gem_load_activate_and_possibly_install(gem_name, bin_container)
+ gem_activate_and_possibly_install(gem_name)
+ load Gem.bin_path(gem_name, bin_container)
+ end
+
+ def gem_activate_and_possibly_install(gem_name)
+ gem_activate(gem_name)
+ rescue Gem::LoadError => e
+ Gem.install(gem_name, e.requirement)
+ retry
+ end
+
+ def gem_activate(gem_name)
+ require_relative "activate"
+ require "bundler"
+ gem_requirement = Bundler::LockfileParser.new(File.read(dev_lockfile)).specs.find {|spec| spec.name == gem_name }.version
+ gem gem_name, gem_requirement
+ end
+
+ def test_gemfile
+ Path.test_gemfile
+ end
+
+ def rubocop_gemfile
+ Path.rubocop_gemfile
+ end
+
+ def standard_gemfile
+ Path.standard_gemfile
+ end
+
+ def dev_gemfile
+ Path.dev_gemfile
+ end
+
+ def dev_lockfile
+ lockfile_for(dev_gemfile)
+ end
+
+ def lockfile_for(gemfile)
+ Pathname.new("#{gemfile.expand_path}.lock")
+ end
+ end
+end
diff --git a/spec/bundler/support/rubygems_version_manager.rb b/spec/bundler/support/rubygems_version_manager.rb
new file mode 100644
index 0000000000..c174c461f0
--- /dev/null
+++ b/spec/bundler/support/rubygems_version_manager.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require_relative "options"
+require_relative "env"
+require_relative "subprocess"
+
+class RubygemsVersionManager
+ include Spec::Options
+ include Spec::Env
+ include Spec::Subprocess
+
+ def initialize(source)
+ @source = source
+ end
+
+ def switch
+ return if use_system?
+
+ assert_system_features_not_loaded!
+
+ switch_local_copy_if_needed
+
+ reexec_if_needed
+ end
+
+ def assert_system_features_not_loaded!
+ at_exit do
+ rubylibdir = RbConfig::CONFIG["rubylibdir"]
+
+ rubygems_path = rubylibdir + "/rubygems"
+ rubygems_default_path = rubygems_path + "/defaults"
+
+ bundler_path = rubylibdir + "/bundler"
+
+ bad_loaded_features = $LOADED_FEATURES.select do |loaded_feature|
+ (loaded_feature.start_with?(rubygems_path) && !loaded_feature.start_with?(rubygems_default_path)) ||
+ loaded_feature.start_with?(bundler_path)
+ end
+
+ errors = if bad_loaded_features.any?
+ all_commands_output + "the following features were incorrectly loaded:\n#{bad_loaded_features.join("\n")}"
+ end
+
+ raise errors if errors
+ end
+ end
+
+ private
+
+ def use_system?
+ @source.nil?
+ end
+
+ def reexec_if_needed
+ return unless rubygems_unrequire_needed?
+
+ require "rbconfig"
+
+ cmd = [RbConfig.ruby, $0, *ARGV].compact
+
+ ENV["RUBYOPT"] = opt_add("-I#{File.join(local_copy_path, "lib")}", opt_remove("--disable-gems", ENV["RUBYOPT"]))
+
+ exec(ENV, *cmd)
+ end
+
+ def switch_local_copy_if_needed
+ return unless local_copy_switch_needed?
+
+ git("checkout #{target_tag}", local_copy_path)
+
+ ENV["RGV"] = local_copy_path
+ end
+
+ def rubygems_unrequire_needed?
+ require "rubygems"
+ !$LOADED_FEATURES.include?(File.join(local_copy_path, "lib/rubygems.rb"))
+ end
+
+ def local_copy_switch_needed?
+ !source_is_path? && target_tag != local_copy_tag
+ end
+
+ def target_tag
+ @target_tag ||= resolve_target_tag
+ end
+
+ def local_copy_tag
+ git("rev-parse --abbrev-ref HEAD", local_copy_path)
+ end
+
+ def local_copy_path
+ @local_copy_path ||= resolve_local_copy_path
+ end
+
+ def resolve_local_copy_path
+ return expanded_source if source_is_path?
+
+ rubygems_path = File.join(source_root, "tmp/rubygems")
+
+ unless File.directory?(rubygems_path)
+ git("clone .. #{rubygems_path}", source_root)
+ end
+
+ rubygems_path
+ end
+
+ def source_is_path?
+ File.directory?(expanded_source)
+ end
+
+ def expanded_source
+ @expanded_source ||= File.expand_path(@source, source_root)
+ end
+
+ def source_root
+ @source_root ||= File.expand_path(ruby_core? ? "../../.." : "../..", __dir__)
+ end
+
+ def resolve_target_tag
+ return "v#{@source}" if @source.match?(/^\d/)
+
+ @source
+ end
+end
diff --git a/spec/bundler/support/setup.rb b/spec/bundler/support/setup.rb
new file mode 100644
index 0000000000..4ac2e5b472
--- /dev/null
+++ b/spec/bundler/support/setup.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative "switch_rubygems"
+
+require_relative "rubygems_ext"
+Spec::Rubygems.install_test_deps
+
+require_relative "path"
+$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__)) if Spec::Path.ruby_core?
diff --git a/spec/bundler/support/shards.rb b/spec/bundler/support/shards.rb
new file mode 100644
index 0000000000..ce33896539
--- /dev/null
+++ b/spec/bundler/support/shards.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+# This classifies test files into 4 shards by running `bin/rspec --profile 10000`
+# to ensure balanced execution times. When adding new test files, it is recommended to
+# re-aggregate and adjust the shards to keep them balanced.
+# For now, please add new files to shard 'shard_d'.
+
+module Spec
+ module Shards
+ EXAMPLE_MAPPINGS = {
+ shard_a: [
+ "spec/runtime/setup_spec.rb",
+ "spec/commands/install_spec.rb",
+ "spec/commands/add_spec.rb",
+ "spec/install/gems/compact_index_spec.rb",
+ "spec/commands/config_spec.rb",
+ "spec/commands/pristine_spec.rb",
+ "spec/install/gemfile/path_spec.rb",
+ "spec/update/git_spec.rb",
+ "spec/commands/open_spec.rb",
+ "spec/commands/remove_spec.rb",
+ "spec/commands/show_spec.rb",
+ "spec/plugins/source/example_spec.rb",
+ "spec/commands/console_spec.rb",
+ "spec/runtime/require_spec.rb",
+ "spec/runtime/env_helpers_spec.rb",
+ "spec/runtime/gem_tasks_spec.rb",
+ "spec/install/gemfile_spec.rb",
+ "spec/commands/fund_spec.rb",
+ "spec/commands/init_spec.rb",
+ "spec/bundler/ruby_dsl_spec.rb",
+ "spec/bundler/mirror_spec.rb",
+ "spec/bundler/source/git/git_proxy_spec.rb",
+ "spec/bundler/source_list_spec.rb",
+ "spec/bundler/plugin/installer_spec.rb",
+ "spec/bundler/errors_spec.rb",
+ "spec/bundler/friendly_errors_spec.rb",
+ "spec/resolver/platform_spec.rb",
+ "spec/bundler/fetcher/downloader_spec.rb",
+ "spec/update/force_spec.rb",
+ "spec/bundler/env_spec.rb",
+ "spec/install/gems/mirror_spec.rb",
+ "spec/install/failure_spec.rb",
+ "spec/bundler/yaml_serializer_spec.rb",
+ "spec/bundler/environment_preserver_spec.rb",
+ "spec/install/gemfile/install_if_spec.rb",
+ "spec/install/gems/gemfile_source_header_spec.rb",
+ "spec/bundler/fetcher/base_spec.rb",
+ "spec/bundler/rubygems_integration_spec.rb",
+ "spec/bundler/worker_spec.rb",
+ "spec/bundler/dependency_spec.rb",
+ "spec/bundler/ui_spec.rb",
+ "spec/bundler/plugin/source_list_spec.rb",
+ "spec/bundler/source/path_spec.rb",
+ ],
+ shard_b: [
+ "spec/install/gemfile/git_spec.rb",
+ "spec/install/gems/standalone_spec.rb",
+ "spec/commands/lock_spec.rb",
+ "spec/cache/gems_spec.rb",
+ "spec/other/major_deprecation_spec.rb",
+ "spec/install/gems/dependency_api_spec.rb",
+ "spec/install/gemfile/gemspec_spec.rb",
+ "spec/plugins/install_spec.rb",
+ "spec/commands/binstubs_spec.rb",
+ "spec/install/gems/flex_spec.rb",
+ "spec/runtime/inline_spec.rb",
+ "spec/commands/post_bundle_message_spec.rb",
+ "spec/runtime/executable_spec.rb",
+ "spec/lock/git_spec.rb",
+ "spec/plugins/hook_spec.rb",
+ "spec/install/allow_offline_install_spec.rb",
+ "spec/install/gems/post_install_spec.rb",
+ "spec/install/gemfile/ruby_spec.rb",
+ "spec/install/security_policy_spec.rb",
+ "spec/install/yanked_spec.rb",
+ "spec/update/gemfile_spec.rb",
+ "spec/runtime/load_spec.rb",
+ "spec/plugins/command_spec.rb",
+ "spec/commands/version_spec.rb",
+ "spec/install/prereleases_spec.rb",
+ "spec/bundler/uri_credentials_filter_spec.rb",
+ "spec/bundler/plugin_spec.rb",
+ "spec/install/gems/mirror_probe_spec.rb",
+ "spec/plugins/list_spec.rb",
+ "spec/bundler/compact_index_client/parser_spec.rb",
+ "spec/bundler/gem_version_promoter_spec.rb",
+ "spec/other/cli_dispatch_spec.rb",
+ "spec/bundler/source/rubygems_spec.rb",
+ "spec/cache/platform_spec.rb",
+ "spec/update/gems/fund_spec.rb",
+ "spec/bundler/stub_specification_spec.rb",
+ "spec/bundler/retry_spec.rb",
+ "spec/bundler/installer/spec_installation_spec.rb",
+ "spec/bundler/spec_set_spec.rb",
+ "spec/quality_es_spec.rb",
+ "spec/bundler/index_spec.rb",
+ "spec/other/cli_man_pages_spec.rb",
+ ],
+ shard_c: [
+ "spec/commands/newgem_spec.rb",
+ "spec/commands/exec_spec.rb",
+ "spec/commands/clean_spec.rb",
+ "spec/commands/platform_spec.rb",
+ "spec/cache/git_spec.rb",
+ "spec/install/gemfile/groups_spec.rb",
+ "spec/commands/cache_spec.rb",
+ "spec/commands/check_spec.rb",
+ "spec/commands/list_spec.rb",
+ "spec/install/path_spec.rb",
+ "spec/bundler/cli_spec.rb",
+ "spec/install/bundler_spec.rb",
+ "spec/install/git_spec.rb",
+ "spec/commands/doctor_spec.rb",
+ "spec/bundler/dsl_spec.rb",
+ "spec/install/gems/fund_spec.rb",
+ "spec/install/gems/env_spec.rb",
+ "spec/bundler/ruby_version_spec.rb",
+ "spec/bundler/definition_spec.rb",
+ "spec/install/gemfile/eval_gemfile_spec.rb",
+ "spec/plugins/source_spec.rb",
+ "spec/install/gems/dependency_api_fallback_spec.rb",
+ "spec/plugins/uninstall_spec.rb",
+ "spec/bundler/plugin/index_spec.rb",
+ "spec/bundler/bundler_spec.rb",
+ "spec/bundler/fetcher_spec.rb",
+ "spec/bundler/source/rubygems/remote_spec.rb",
+ "spec/bundler/lockfile_parser_spec.rb",
+ "spec/cache/cache_path_spec.rb",
+ "spec/bundler/source/git_spec.rb",
+ "spec/bundler/source_spec.rb",
+ "spec/commands/ssl_spec.rb",
+ "spec/bundler/fetcher/compact_index_spec.rb",
+ "spec/bundler/plugin/api_spec.rb",
+ "spec/bundler/endpoint_specification_spec.rb",
+ "spec/bundler/fetcher/index_spec.rb",
+ "spec/bundler/settings/validator_spec.rb",
+ "spec/bundler/build_metadata_spec.rb",
+ "spec/bundler/current_ruby_spec.rb",
+ "spec/bundler/installer/gem_installer_spec.rb",
+ "spec/bundler/installer/parallel_installer_spec.rb",
+ "spec/bundler/cli_common_spec.rb",
+ "spec/bundler/ci_detector_spec.rb",
+ ],
+ shard_d: [
+ "spec/bundler/rubygems_ext_spec.rb",
+ "spec/bundler/resolver/cooldown_spec.rb",
+ "spec/install/cooldown_spec.rb",
+ "spec/commands/outdated_spec.rb",
+ "spec/commands/update_spec.rb",
+ "spec/lock/lockfile_spec.rb",
+ "spec/install/deploy_spec.rb",
+ "spec/install/gemfile/sources_spec.rb",
+ "spec/runtime/self_management_spec.rb",
+ "spec/install/gemfile/specific_platform_spec.rb",
+ "spec/commands/info_spec.rb",
+ "spec/install/gems/resolving_spec.rb",
+ "spec/install/gemfile/platform_spec.rb",
+ "spec/bundler/gem_helper_spec.rb",
+ "spec/install/global_cache_spec.rb",
+ "spec/runtime/platform_spec.rb",
+ "spec/update/gems/post_install_spec.rb",
+ "spec/install/gems/native_extensions_spec.rb",
+ "spec/install/force_spec.rb",
+ "spec/cache/path_spec.rb",
+ "spec/install/gemspecs_spec.rb",
+ "spec/commands/help_spec.rb",
+ "spec/bundler/shared_helpers_spec.rb",
+ "spec/bundler/settings_spec.rb",
+ "spec/resolver/basic_spec.rb",
+ "spec/install/gemfile/force_ruby_platform_spec.rb",
+ "spec/commands/licenses_spec.rb",
+ "spec/install/gemfile/lockfile_spec.rb",
+ "spec/bundler/fetcher/dependency_spec.rb",
+ "spec/quality_spec.rb",
+ "spec/bundler/remote_specification_spec.rb",
+ "spec/install/process_lock_spec.rb",
+ "spec/install/binstubs_spec.rb",
+ "spec/bundler/compact_index_client/updater_spec.rb",
+ "spec/bundler/ui/shell_spec.rb",
+ "spec/other/ext_spec.rb",
+ "spec/commands/issue_spec.rb",
+ "spec/update/path_spec.rb",
+ "spec/bundler/plugin/api/source_spec.rb",
+ "spec/install/gems/win32_spec.rb",
+ "spec/bundler/plugin/dsl_spec.rb",
+ "spec/runtime/requiring_spec.rb",
+ "spec/bundler/plugin/events_spec.rb",
+ "spec/bundler/resolver/candidate_spec.rb",
+ "spec/bundler/digest_spec.rb",
+ "spec/bundler/fetcher/gem_remote_fetcher_spec.rb",
+ "spec/bundler/uri_normalizer_spec.rb",
+ "spec/install/gems/no_build_extension_spec.rb",
+ "spec/install/gems/no_install_plugin_spec.rb",
+ "spec/bundler/override_spec.rb",
+ "spec/install/gemfile/override_spec.rb",
+ ],
+ }.freeze
+ end
+end
diff --git a/spec/bundler/support/subprocess.rb b/spec/bundler/support/subprocess.rb
new file mode 100644
index 0000000000..91db80da48
--- /dev/null
+++ b/spec/bundler/support/subprocess.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require_relative "command_execution"
+
+module Spec
+ module Subprocess
+ class TimeoutExceeded < StandardError; end
+
+ def command_executions
+ @command_executions ||= []
+ end
+
+ def last_command
+ command_executions.last || raise("There is no last command")
+ end
+
+ def out
+ last_command.stdout
+ end
+
+ def err
+ last_command.stderr
+ end
+
+ def stdboth
+ last_command.stdboth
+ end
+
+ def exitstatus
+ last_command.exitstatus
+ end
+
+ def git(cmd, path = Dir.pwd, options = {})
+ sh("git #{cmd}", options.merge(dir: path))
+ end
+
+ def sh(cmd, options = {})
+ dir = options[:dir]
+ env = options[:env] || {}
+
+ command_execution = CommandExecution.new(cmd.to_s, timeout: options[:timeout] || 60)
+
+ open3_opts = {}
+ open3_opts[:chdir] = dir if dir
+
+ require "open3"
+ require "shellwords"
+ Open3.popen3(env, *cmd.shellsplit, **open3_opts) do |stdin, stdout, stderr, wait_thr|
+ yield stdin, stdout, wait_thr if block_given?
+ stdin.close
+
+ stdout_handler = ->(data) { command_execution.original_stdout << data }
+ stderr_handler = ->(data) { command_execution.original_stderr << data }
+
+ stdout_thread = read_stream(stdout, stdout_handler, timeout: command_execution.timeout)
+ stderr_thread = read_stream(stderr, stderr_handler, timeout: command_execution.timeout)
+
+ stdout_thread.join
+ stderr_thread.join
+
+ status = wait_thr.value
+ command_execution.exitstatus = if status.exited?
+ status.exitstatus
+ elsif status.signaled?
+ exit_status_for_signal(status.termsig)
+ end
+ rescue TimeoutExceeded
+ command_execution.failure_reason = :timeout
+ command_execution.exitstatus = exit_status_for_signal(Signal.list["INT"])
+ end
+
+ unless options[:raise_on_error] == false || command_execution.success?
+ command_execution.raise_error!
+ end
+
+ command_executions << command_execution
+
+ command_execution.stdout
+ end
+
+ # Mostly copied from https://github.com/piotrmurach/tty-command/blob/49c37a895ccea107e8b78d20e4cb29de6a1a53c8/lib/tty/command/process_runner.rb#L165-L193
+ def read_stream(stream, handler, timeout:)
+ Thread.new do
+ Thread.current.report_on_exception = false
+ cmd_start = Time.now
+ readers = [stream]
+
+ while readers.any?
+ ready = IO.select(readers, nil, readers, timeout)
+ raise TimeoutExceeded if ready.nil?
+
+ ready[0].each do |reader|
+ chunk = reader.readpartial(16 * 1024)
+ handler.call(chunk)
+
+ # control total time spent reading
+ runtime = Time.now - cmd_start
+ time_left = timeout - runtime
+ raise TimeoutExceeded if time_left < 0.0
+ rescue Errno::EAGAIN, Errno::EINTR
+ rescue EOFError, Errno::EPIPE, Errno::EIO
+ readers.delete(reader)
+ reader.close
+ end
+ end
+ end
+ end
+
+ def all_commands_output
+ return "" if command_executions.empty?
+
+ "\n\nCommands:\n#{command_executions.map(&:to_s_verbose).join("\n\n")}"
+ end
+ end
+end
diff --git a/spec/bundler/support/switch_rubygems.rb b/spec/bundler/support/switch_rubygems.rb
new file mode 100644
index 0000000000..640b9f83b7
--- /dev/null
+++ b/spec/bundler/support/switch_rubygems.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+require_relative "rubygems_version_manager"
+ENV["RGV"] ||= "."
+RubygemsVersionManager.new(ENV["RGV"]).switch
diff --git a/spec/bundler/support/the_bundle.rb b/spec/bundler/support/the_bundle.rb
new file mode 100644
index 0000000000..452abd7d41
--- /dev/null
+++ b/spec/bundler/support/the_bundle.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "path"
+
+module Spec
+ class TheBundle
+ include Spec::Path
+
+ attr_accessor :bundle_dir
+
+ def initialize
+ @bundle_dir = Pathname.new(bundled_app)
+ end
+
+ def to_s
+ "the bundle"
+ end
+ alias_method :inspect, :to_s
+
+ def locked?
+ lockfile.file?
+ end
+
+ def lockfile
+ bundle_dir.join("Gemfile.lock")
+ end
+
+ def locked_gems
+ raise ArgumentError, "Cannot read lockfile if it doesn't exist" unless locked?
+ Bundler::LockfileParser.new(lockfile.read)
+ end
+
+ def locked_specs
+ locked_gems.specs.map(&:full_name)
+ end
+
+ def locked_platforms
+ locked_gems.platforms.map(&:to_s)
+ end
+ end
+end
diff --git a/spec/bundler/support/vendored_net_http.rb b/spec/bundler/support/vendored_net_http.rb
new file mode 100644
index 0000000000..8ff2ccd1fe
--- /dev/null
+++ b/spec/bundler/support/vendored_net_http.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# This defined? guard can be removed once RubyGems 3.4 support is dropped.
+#
+# Bundler specs load this code from `spec/support/vendored_net_http.rb` to avoid
+# activating the Bundler gem too early. Without this guard, we get redefinition
+# warnings once Bundler is actually activated and
+# `lib/bundler/vendored_net_http.rb` is required. This is not an issue in
+# RubyGems versions including `rubygems/vendored_net_http` since `require` takes
+# care of avoiding the double load.
+#
+unless defined?(Gem::Net)
+ begin
+ require "rubygems/vendored_net_http"
+ rescue LoadError
+ begin
+ require "rubygems/net/http"
+ rescue LoadError
+ require "net/http"
+ Gem::Net = Net
+ end
+ end
+end
diff --git a/spec/bundler/update/force_spec.rb b/spec/bundler/update/force_spec.rb
new file mode 100644
index 0000000000..325f58088a
--- /dev/null
+++ b/spec/bundler/update/force_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ before :each do
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack"
+ G
+ end
+
+ it "re-installs installed gems with --force" do
+ myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb")
+ myrack_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :update, force: true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+
+ it "re-installs installed gems with --redownload" do
+ myrack_lib = default_bundle_path("gems/myrack-1.0.0/lib/myrack.rb")
+ myrack_lib.open("w") {|f| f.write("blah blah blah") }
+ bundle :update, redownload: true
+
+ expect(out).to include "Installing myrack 1.0.0"
+ expect(myrack_lib.open(&:read)).to eq("MYRACK = '1.0.0'\n")
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+end
diff --git a/spec/bundler/update/gemfile_spec.rb b/spec/bundler/update/gemfile_spec.rb
new file mode 100644
index 0000000000..f8849640b6
--- /dev/null
+++ b/spec/bundler/update/gemfile_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ context "with --gemfile" do
+ it "finds the gemfile" do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle :install, gemfile: bundled_app("NotGemfile")
+ bundle :update, gemfile: bundled_app("NotGemfile"), all: true
+
+ # Specify BUNDLE_GEMFILE for `the_bundle`
+ # to retrieve the proper Gemfile
+ ENV["BUNDLE_GEMFILE"] = "NotGemfile"
+ expect(the_bundle).to include_gems "myrack 1.0.0"
+ end
+ end
+
+ context "with gemfile set via config" do
+ before do
+ gemfile bundled_app("NotGemfile"), <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ G
+
+ bundle_config "gemfile #{bundled_app("NotGemfile")}"
+ bundle :install
+ end
+
+ it "uses the gemfile to update" do
+ bundle "update", all: true
+ bundle "list"
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+
+ it "uses the gemfile while in a subdirectory" do
+ bundled_app("subdir").mkpath
+ bundle "update", all: true, dir: bundled_app("subdir")
+ bundle "list", dir: bundled_app("subdir")
+
+ expect(out).to include("myrack (1.0.0)")
+ end
+ end
+end
diff --git a/spec/bundler/update/gems/fund_spec.rb b/spec/bundler/update/gems/fund_spec.rb
new file mode 100644
index 0000000000..a5624d3e0a
--- /dev/null
+++ b/spec/bundler/update/gems/fund_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ before do
+ build_repo2 do
+ build_gem "has_funding_and_other_metadata" do |s|
+ s.metadata = {
+ "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ "homepage_uri" => "https://bestgemever.example.io",
+ "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ "funding_uri" => "https://example.com/has_funding_and_other_metadata/funding",
+ "source_code_uri" => "https://example.com/user/bestgemever",
+ "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ }
+ end
+
+ build_gem "has_funding", "1.2.3" do |s|
+ s.metadata = {
+ "funding_uri" => "https://example.com/has_funding/funding",
+ }
+ end
+ end
+
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding', '< 2.0'
+ G
+
+ bundle :install
+ end
+
+ context "when listed gems are updated" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo2"
+ gem 'has_funding_and_other_metadata'
+ gem 'has_funding'
+ G
+
+ bundle :update, all: true
+ end
+
+ it "displays fund message" do
+ expect(out).to include("2 installed gems you directly depend on are looking for funding.")
+ end
+ end
+end
diff --git a/spec/bundler/update/gems/post_install_spec.rb b/spec/bundler/update/gems/post_install_spec.rb
new file mode 100644
index 0000000000..9c71f6e0e3
--- /dev/null
+++ b/spec/bundler/update/gems/post_install_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ let(:config) {}
+
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack', "< 1.0"
+ gem 'thin'
+ G
+
+ bundle "config set #{config}" if config
+
+ bundle :install
+ end
+
+ shared_examples "a config observer" do
+ context "when ignore post-install messages for gem is set" do
+ let(:config) { "ignore_messages.myrack true" }
+
+ it "doesn't display gem's post-install message" do
+ expect(out).not_to include("Myrack's post install message")
+ end
+ end
+
+ context "when ignore post-install messages for all gems" do
+ let(:config) { "ignore_messages true" }
+
+ it "doesn't display any post-install messages" do
+ expect(out).not_to include("Post-install message")
+ end
+ end
+ end
+
+ shared_examples "a post-install message outputter" do
+ it "should display post-install messages for updated gems" do
+ expect(out).to include("Post-install message from myrack:")
+ expect(out).to include("Myrack's post install message")
+ end
+
+ it "should not display the post-install message for non-updated gems" do
+ expect(out).not_to include("Thin's post install message")
+ end
+ end
+
+ context "when listed gem is updated" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack'
+ gem 'thin'
+ G
+
+ bundle :update, all: true
+ end
+
+ it_behaves_like "a post-install message outputter"
+ it_behaves_like "a config observer"
+ end
+
+ context "when dependency triggers update" do
+ before do
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'myrack-obama'
+ gem 'thin'
+ G
+
+ bundle :update, all: true
+ end
+
+ it_behaves_like "a post-install message outputter"
+ it_behaves_like "a config observer"
+ end
+end
diff --git a/spec/bundler/update/git_spec.rb b/spec/bundler/update/git_spec.rb
new file mode 100644
index 0000000000..526e988ab7
--- /dev/null
+++ b/spec/bundler/update/git_spec.rb
@@ -0,0 +1,341 @@
+# frozen_string_literal: true
+
+RSpec.describe "bundle update" do
+ describe "git sources" do
+ it "floats on a branch when :branch is used" do
+ build_git "foo", "1.0"
+ update_git "foo", branch: "omg"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo-1.0")}", :branch => "omg" do
+ gem 'foo'
+ end
+ G
+
+ update_git "foo" do |s|
+ s.write "lib/foo.rb", "FOO = '1.1'"
+ end
+
+ bundle "update", all: true
+
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "updates correctly when you have like craziness" do
+ build_lib "activesupport", "3.0", path: lib_path("rails/activesupport")
+ build_git "rails", "3.0", path: lib_path("rails") do |s|
+ s.add_dependency "activesupport", "= 3.0"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_path("rails")}"
+ G
+
+ bundle "update rails"
+ expect(the_bundle).to include_gems "rails 3.0", "activesupport 3.0"
+ end
+
+ it "floats on a branch when :branch is used and the source is specified in the update" do
+ build_git "foo", "1.0", path: lib_path("foo")
+ update_git "foo", branch: "omg", path: lib_path("foo")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ git "#{lib_path("foo")}", :branch => "omg" do
+ gem 'foo'
+ end
+ G
+
+ update_git "foo", path: lib_path("foo") do |s|
+ s.write "lib/foo.rb", "FOO = '1.1'"
+ end
+
+ bundle "update --source foo"
+
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "floats on main when updating all gems that are pinned to the source even if you have child dependencies" do
+ build_git "foo", path: lib_path("foo")
+ build_gem "bar", to_bundle: true do |s|
+ s.add_dependency "foo"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo")}"
+ gem "bar"
+ G
+
+ update_git "foo", path: lib_path("foo") do |s|
+ s.write "lib/foo.rb", "FOO = '1.1'"
+ end
+
+ bundle "update foo"
+
+ expect(the_bundle).to include_gems "foo 1.1"
+ end
+
+ it "notices when you change the repo url in the Gemfile" do
+ build_git "foo", path: lib_path("foo_one")
+ build_git "foo", path: lib_path("foo_two")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo_one")}"
+ G
+
+ FileUtils.rm_r lib_path("foo_one")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", "1.0", :git => "#{lib_path("foo_two")}"
+ G
+
+ expect(err).to be_empty
+ expect(out).to include("Fetching #{lib_path}/foo_two")
+ expect(out).to include("Bundle complete!")
+ end
+
+ it "fetches tags from the remote" do
+ build_git "foo"
+ @remote = build_git("bar", bare: true)
+ update_git "foo", remote: @remote.path
+ update_git "foo", push: "main"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}"
+ G
+
+ # Create a new tag on the remote that needs fetching
+ update_git "foo", tag: "fubar"
+ update_git "foo", push: "fubar"
+
+ gemfile <<-G
+ source "https://gem.repo1"
+ gem 'foo', :git => "#{@remote.path}", :tag => "fubar"
+ G
+
+ bundle "update", all: true
+ expect(err).to be_empty
+ end
+
+ describe "with submodules" do
+ before :each do
+ # CVE-2022-39253: https://lore.kernel.org/lkml/xmqq4jw1uku5.fsf@gitster.g/
+ system(*%W[git config --global protocol.file.allow always])
+
+ build_repo4 do
+ build_gem "submodule" do |s|
+ s.write "lib/submodule.rb", "puts 'GEM'"
+ end
+ end
+
+ build_git "submodule", "1.0" do |s|
+ s.write "lib/submodule.rb", "puts 'GIT'"
+ end
+
+ build_git "has_submodule", "1.0" do |s|
+ s.add_dependency "submodule"
+ end
+
+ git "submodule add #{lib_path("submodule-1.0")} submodule-1.0", lib_path("has_submodule-1.0")
+ git "commit -m \"submodulator\"", lib_path("has_submodule-1.0")
+ end
+
+ it "it unlocks the source when submodules are added to a git source" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GEM")
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GIT")
+ end
+
+ it "unlocks the source when submodules are removed from git source", git: ">= 2.9.0" do
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}", :submodules => true do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GIT")
+
+ install_gemfile <<-G
+ source "https://gem.repo4"
+ git "#{lib_path("has_submodule-1.0")}" do
+ gem "has_submodule"
+ end
+ G
+
+ run "require 'submodule'"
+ expect(out).to eq("GEM")
+ end
+ end
+
+ it "errors with a message when the .git repo is gone" do
+ build_git "foo", "1.0"
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "foo", :git => "#{lib_path("foo-1.0")}"
+ G
+
+ FileUtils.rm_rf lib_path("foo-1.0").join(".git")
+
+ bundle :update, all: true, raise_on_error: false
+ expect(err).to include(lib_path("foo-1.0").to_s).
+ and match(/Git error: command `git fetch.+has failed/)
+ end
+
+ it "should not explode on invalid revision on update of gem by name" do
+ build_git "myrack", "0.8"
+
+ build_git "myrack", "0.8", path: lib_path("local-myrack") do |s|
+ s.write "lib/myrack.rb", "puts :LOCAL"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "myrack", :git => "#{lib_path("myrack-0.8")}", :branch => "main"
+ G
+
+ bundle %(config set local.myrack #{lib_path("local-myrack")})
+ bundle "update myrack"
+ expect(out).to include("Bundle updated!")
+ end
+
+ it "shows the previous version of the gem" do
+ build_git "rails", "2.3.2", path: lib_path("rails")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "rails", :git => "#{lib_path("rails")}"
+ G
+
+ update_git "rails", "3.0", path: lib_path("rails"), gemspec: true
+
+ bundle "update", all: true
+ expect(out).to include("Using rails 3.0 (was 2.3.2) from #{lib_path("rails")} (at main@#{revision_for(lib_path("rails"))[0..6]})")
+ end
+ end
+
+ describe "with --source flag" do
+ before :each do
+ build_repo2
+ @git = build_git "foo", path: lib_path("foo") do |s|
+ s.executables = "foobar"
+ end
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ git "#{lib_path("foo")}" do
+ gem 'foo'
+ end
+ gem 'myrack'
+ G
+ end
+
+ it "updates the source" do
+ update_git "foo", path: @git.path
+
+ bundle "update --source foo"
+
+ run <<-RUBY
+ require 'foo'
+ puts "WIN" if defined?(FOO_PREV_REF)
+ RUBY
+
+ expect(out).to eq("WIN")
+ end
+
+ it "unlocks gems that were originally pulled in by the source" do
+ update_git "foo", "2.0", path: @git.path
+
+ bundle "update --source foo"
+ expect(the_bundle).to include_gems "foo 2.0"
+ end
+
+ it "leaves all other gems frozen" do
+ update_repo2
+ update_git "foo", path: @git.path
+
+ bundle "update --source foo"
+ expect(the_bundle).to include_gems "myrack 1.0"
+ end
+ end
+
+ context "when the gem and the repository have different names" do
+ before :each do
+ build_repo2
+ @git = build_git "foo", path: lib_path("bar")
+
+ install_gemfile <<-G
+ source "https://gem.repo2"
+ git "#{lib_path("bar")}" do
+ gem 'foo'
+ end
+ gem 'myrack'
+ G
+ end
+
+ it "the --source flag updates version of gems that were originally pulled in by the source" do
+ spec_lines = lib_path("bar/foo.gemspec").read.split("\n")
+ spec_lines[5] = "s.version = '2.0'"
+
+ update_git "foo", "2.0", path: @git.path do |s|
+ s.write "foo.gemspec", spec_lines.join("\n")
+ end
+
+ ref = @git.ref_for "main"
+
+ bundle "update --source bar"
+
+ checksums = checksums_section_when_enabled do |c|
+ c.no_checksum "foo", "2.0"
+ c.checksum gem_repo2, "myrack", "1.0.0"
+ end
+
+ expect(lockfile).to eq <<~G
+ GIT
+ remote: #{@git.path}
+ revision: #{ref}
+ specs:
+ foo (2.0)
+
+ GEM
+ remote: https://gem.repo2/
+ specs:
+ myrack (1.0.0)
+
+ PLATFORMS
+ #{lockfile_platforms}
+
+ DEPENDENCIES
+ foo!
+ myrack
+ #{checksums}
+ BUNDLED WITH
+ #{Bundler::VERSION}
+ G
+ end
+ end
+end
diff --git a/spec/bundler/update/path_spec.rb b/spec/bundler/update/path_spec.rb
new file mode 100644
index 0000000000..8c76c94e1a
--- /dev/null
+++ b/spec/bundler/update/path_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+RSpec.describe "path sources" do
+ describe "bundle update --source" do
+ it "shows the previous version of the gem when updated from path source" do
+ build_lib "activesupport", "2.3.5", path: lib_path("rails/activesupport")
+
+ install_gemfile <<-G
+ source "https://gem.repo1"
+ gem "activesupport", :path => "#{lib_path("rails/activesupport")}"
+ G
+
+ build_lib "activesupport", "3.0", path: lib_path("rails/activesupport")
+
+ bundle "update --source activesupport"
+ expect(out).to include("Using activesupport 3.0 (was 2.3.5) from source at `#{lib_path("rails/activesupport")}`")
+ end
+ end
+end
diff --git a/spec/default.mspec b/spec/default.mspec
new file mode 100644
index 0000000000..d756dc31ff
--- /dev/null
+++ b/spec/default.mspec
@@ -0,0 +1,138 @@
+# -*- ruby -*-
+$VERBOSE = false
+if (opt = ENV["RUBYOPT"]) and (opt = opt.dup).sub!(/(?:\A|\s)-w(?=\z|\s)/, '')
+ ENV["RUBYOPT"] = opt
+end
+
+# Enable constant leak checks by ruby/mspec
+ENV["CHECK_CONSTANT_LEAKS"] ||= "true"
+
+require "./rbconfig" unless defined?(RbConfig)
+require_relative "../tool/test-coverage" if ENV.key?("COVERAGE")
+require_relative "../tool/lib/test/jobserver"
+load File.dirname(__FILE__) + '/ruby/default.mspec'
+OBJDIR = File.expand_path("spec/ruby/optional/capi/ext") unless defined?(OBJDIR)
+class MSpecScript
+ @testing_ruby = true
+
+ builddir = Dir.pwd
+ srcdir = ENV['SRCDIR']
+ srcdir ||= File.read("Makefile", encoding: "US-ASCII")[/^\s*srcdir\s*=\s*(.+)/i, 1] rescue nil
+ config = RbConfig::CONFIG
+
+ # The default implementation to run the specs.
+ set :target, File.join(builddir, "miniruby#{config['exeext']}")
+ set :prefix, File.expand_path('ruby', File.dirname(__FILE__))
+ if srcdir
+ srcdir = File.expand_path(srcdir)
+ set :flags, %W[
+ -I#{srcdir}/lib
+ #{srcdir}/tool/runruby.rb --archdir=#{builddir} --extout=#{config['EXTOUT']}
+ --
+ ]
+ end
+
+ # Disable to run for bundled gems in test-spec
+ set :bundled_gems, (File.readlines("#{srcdir}/gems/bundled_gems").map do |line|
+ next if /^\s*(?:#|$)/ =~ line
+ gem = line.split.first
+ gem = "openstruct" if gem == "ostruct"
+ "#{srcdir}/spec/ruby/library/#{gem}"
+ end.compact)
+ set :stdlibs, Dir.glob("#{srcdir}/spec/ruby/library/*")
+ set :library, get(:stdlibs).to_a - get(:bundled_gems).to_a
+
+ set :files, get(:command_line) + get(:language) + get(:core) + get(:library) + get(:security) + get(:optional)
+
+ if ENV.key?("COVERAGE")
+ set :excludes, ["Coverage"]
+ end
+end
+
+module MSpecScript::JobServer
+ def cores(max = 1)
+ MSpecScript::JobServer.remove_method :cores
+ if cores = Test::JobServer.max_jobs(max)
+ MSpecScript::JobServer.define_method(:cores) { cores }
+ return cores
+ end
+ super
+ end
+end
+
+class MSpecScript
+ prepend JobServer
+end
+
+require 'mspec/runner/formatters/dotted'
+
+class DottedFormatter
+ prepend Module.new {
+ BASE = __dir__ + "/ruby/" unless defined?(BASE)
+ COUNT_WIDTH = 6 unless defined?(COUNT_WIDTH)
+
+ def initialize(out = nil)
+ super
+ if out
+ @columns = nil
+ else
+ columns = ENV["COLUMNS"]&.to_i
+ columns = 80 unless columns&.nonzero?
+ w = COUNT_WIDTH + 1
+ round = 20
+ @columns = (columns - w) / round * round + w
+ end
+ @dotted = 0
+ @loaded = false
+ @count = 0
+ end
+
+ def register
+ super
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def after(*)
+ if @columns
+ if @dotted == 0
+ s = sprintf("%*d ", COUNT_WIDTH, @count)
+ print(s)
+ @dotted += s.size
+ end
+ @count +=1
+ end
+ super
+ if @columns and (@dotted += 1) >= @columns
+ print "\n"
+ @dotted = 0
+ end
+ end
+
+ def load(*)
+ file = MSpec.file || MSpec.files_array.first
+ @loaded = true
+ s = "#{file.delete_prefix(BASE)}:"
+ print s
+ if @columns
+ if (@dotted += s.size) >= @columns
+ print "\n"
+ @dotted = 0
+ else
+ print " "
+ @dotted += 1
+ end
+ end
+ @count = 0
+ end
+
+ def unload
+ super
+ if @loaded
+ print "\n" if @dotted > 0
+ @dotted = 0
+ @loaded = nil
+ end
+ end
+ }
+end
diff --git a/spec/lib/formatter_overrides.rb b/spec/lib/formatter_overrides.rb
new file mode 100644
index 0000000000..4a61d425d1
--- /dev/null
+++ b/spec/lib/formatter_overrides.rb
@@ -0,0 +1,6 @@
+module FormatterOverrides
+ def example_pending(_); end
+ def dump_pending(_); end
+end
+
+RSpec::Core::Formatters::ProgressFormatter.prepend FormatterOverrides
diff --git a/spec/lib/spec_coverage.rb b/spec/lib/spec_coverage.rb
new file mode 100644
index 0000000000..34aed32f72
--- /dev/null
+++ b/spec/lib/spec_coverage.rb
@@ -0,0 +1 @@
+require_relative "../../tool/test-coverage" if ENV.key?('COVERAGE')
diff --git a/spec/mmtk.mspec b/spec/mmtk.mspec
new file mode 100644
index 0000000000..ac0998708c
--- /dev/null
+++ b/spec/mmtk.mspec
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Testing behaviour specific to default GC
+MSpec.register(:exclude, "GC.stat increases major_gc_count after GC is run")
+MSpec.register(:exclude, "GC.stat provides some number for heap_free_slots")
+# GC::Profiler is not implemented
+MSpec.register(:exclude, "GC::Profiler.disable disables the profiler")
+MSpec.register(:exclude, "GC::Profiler.enable enables the profiler")
+MSpec.register(:exclude, "GC::Profiler.enabled? reports as disabled when disabled")
+MSpec.register(:exclude, "GC::Profiler.enabled? reports as enabled when enabled")
+MSpec.register(:exclude, "GC::Profiler.result returns a string")
+MSpec.register(:exclude, "GC::Profiler.total_time returns an float")
diff --git a/spec/mspec/.rspec b/spec/mspec/.rspec
new file mode 100644
index 0000000000..4e1e0d2f72
--- /dev/null
+++ b/spec/mspec/.rspec
@@ -0,0 +1 @@
+--color
diff --git a/spec/mspec/Gemfile b/spec/mspec/Gemfile
new file mode 100644
index 0000000000..617a995cad
--- /dev/null
+++ b/spec/mspec/Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+gem "rake", "~> 12.3"
+gem "rspec", "~> 3.0"
diff --git a/spec/mspec/Gemfile.lock b/spec/mspec/Gemfile.lock
new file mode 100644
index 0000000000..cd39906044
--- /dev/null
+++ b/spec/mspec/Gemfile.lock
@@ -0,0 +1,26 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ diff-lcs (1.4.4)
+ rake (12.3.3)
+ rspec (3.10.0)
+ rspec-core (~> 3.10.0)
+ rspec-expectations (~> 3.10.0)
+ rspec-mocks (~> 3.10.0)
+ rspec-core (3.10.1)
+ rspec-support (~> 3.10.0)
+ rspec-expectations (3.10.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.10.0)
+ rspec-mocks (3.10.2)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.10.0)
+ rspec-support (3.10.2)
+
+PLATFORMS
+ java
+ ruby
+
+DEPENDENCIES
+ rake (~> 12.3)
+ rspec (~> 3.0)
diff --git a/spec/mspec/LICENSE b/spec/mspec/LICENSE
new file mode 100644
index 0000000000..d581dd1c9f
--- /dev/null
+++ b/spec/mspec/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2008 Engine Yard, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/spec/mspec/README.md b/spec/mspec/README.md
new file mode 100644
index 0000000000..94ab608031
--- /dev/null
+++ b/spec/mspec/README.md
@@ -0,0 +1,84 @@
+## Overview
+
+MSpec is a specialized framework that is syntax-compatible with RSpec 2 for
+basic things like `describe`, `it` blocks and `before`, `after` actions.
+MSpec contains additional features that assist in writing specs for
+Ruby implementations in [ruby/spec](https://github.com/ruby/spec).
+
+MSpec attempts to use the simplest Ruby language features so that beginning
+Ruby implementations can run the Ruby specs. For example, no file from the
+standard library or RubyGems is necessary to run MSpec.
+
+MSpec is not intended as a replacement for RSpec. MSpec attempts to provide a
+subset of RSpec's features in some cases and a superset in others. It does not
+provide all the matchers, for instance.
+
+However, MSpec provides several extensions to facilitate writing the Ruby
+specs in a manner compatible with multiple Ruby implementations.
+
+ 1. MSpec offers a set of guards to control execution of the specs. These
+ guards not only enable or disable execution but also annotate the specs
+ with additional information about why they are run or not run.
+
+ 2. MSpec provides a different shared spec implementation specifically
+ designed to ease writing specs for the numerous aliased methods in Ruby.
+
+ 3. MSpec provides various helper methods to simplify some specs, for
+ example, creating temporary file names.
+
+ 4. MSpec has several specialized runner scripts that includes a
+ configuration facility with a default project file and user-specific
+ overrides.
+
+ 5. MSpec support "tagging", that is excluding specs known as failing on
+ a particular Ruby implementation, and automatically adding and removing tags
+ while running the specs.
+
+## Requirements
+
+MSpec requires Ruby 2.6 or more recent.
+
+## Bundler
+
+A Gemfile is provided. Use Bundler to install gem dependencies. To install
+Bundler, run the following:
+
+```bash
+gem install bundler
+```
+
+To install the gem dependencies with Bundler, run the following:
+
+```bash
+ruby -S bundle install
+```
+
+## Development
+
+Use RSpec to run the MSpec specs. There are no plans currently to make the
+MSpec specs runnable by MSpec: https://github.com/ruby/mspec/issues/19.
+
+After installing the gem dependencies, the specs can be run as follows:
+
+```bash
+ruby -S bundle exec rspec
+```
+
+To run an individual spec file, use the following example:
+
+```bash
+ruby -S bundle exec rspec spec/helpers/ruby_exe_spec.rb
+```
+
+## Documentation
+
+See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md) in ruby/spec
+for a list of matchers and how to use `mspec`.
+
+## Source Code
+
+See https://github.com/ruby/mspec
+
+## License
+
+See the LICENSE in the source code.
diff --git a/spec/mspec/Rakefile b/spec/mspec/Rakefile
new file mode 100644
index 0000000000..6a9de7a95e
--- /dev/null
+++ b/spec/mspec/Rakefile
@@ -0,0 +1,6 @@
+require 'bundler/setup'
+require 'rspec/core/rake_task'
+
+RSpec::Core::RakeTask.new(:spec)
+
+task :default => :spec
diff --git a/spec/mspec/bin/mkspec b/spec/mspec/bin/mkspec
new file mode 100755
index 0000000000..00f1fdff47
--- /dev/null
+++ b/spec/mspec/bin/mkspec
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mkspec'
+
+MkSpec.main
diff --git a/spec/mspec/bin/mkspec.bat b/spec/mspec/bin/mkspec.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mkspec.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec b/spec/mspec/bin/mspec
new file mode 100755
index 0000000000..5bd753c06d
--- /dev/null
+++ b/spec/mspec/bin/mspec
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec'
+
+MSpecMain.main(false)
diff --git a/spec/mspec/bin/mspec-ci b/spec/mspec/bin/mspec-ci
new file mode 100755
index 0000000000..d7cd50a827
--- /dev/null
+++ b/spec/mspec/bin/mspec-ci
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec-ci'
+
+MSpecCI.main
diff --git a/spec/mspec/bin/mspec-ci.bat b/spec/mspec/bin/mspec-ci.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec-ci.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec-run b/spec/mspec/bin/mspec-run
new file mode 100755
index 0000000000..010ecefe35
--- /dev/null
+++ b/spec/mspec/bin/mspec-run
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec-run'
+
+MSpecRun.main
diff --git a/spec/mspec/bin/mspec-run.bat b/spec/mspec/bin/mspec-run.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec-run.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec-tag b/spec/mspec/bin/mspec-tag
new file mode 100755
index 0000000000..a5f9fffaaa
--- /dev/null
+++ b/spec/mspec/bin/mspec-tag
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+
+require 'mspec/commands/mspec-tag'
+
+MSpecTag.main
diff --git a/spec/mspec/bin/mspec-tag.bat b/spec/mspec/bin/mspec-tag.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec-tag.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/bin/mspec.bat b/spec/mspec/bin/mspec.bat
new file mode 100755
index 0000000000..1073d20a9b
--- /dev/null
+++ b/spec/mspec/bin/mspec.bat
@@ -0,0 +1 @@
+@"ruby.exe" "%~dpn0" %*
diff --git a/spec/mspec/lib/mspec.rb b/spec/mspec/lib/mspec.rb
new file mode 100644
index 0000000000..d24abd96f1
--- /dev/null
+++ b/spec/mspec/lib/mspec.rb
@@ -0,0 +1,8 @@
+require 'mspec/utils/format'
+require 'mspec/matchers'
+require 'mspec/expectations'
+require 'mspec/mocks'
+require 'mspec/runner'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/version'
diff --git a/spec/mspec/lib/mspec/commands/mkspec.rb b/spec/mspec/lib/mspec/commands/mkspec.rb
new file mode 100644
index 0000000000..f75e683b19
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mkspec.rb
@@ -0,0 +1,143 @@
+require 'rbconfig'
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/name_map'
+require 'mspec/helpers/fs'
+
+class MkSpec
+ attr_reader :config
+
+ def initialize
+ @config = {
+ :constants => [],
+ :requires => [],
+ :base => "core",
+ :version => nil
+ }
+ @map = NameMap.new true
+ end
+
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mkspec [options]", 32
+
+ options.on("-c", "--constant", "CONSTANT",
+ "Class or Module to generate spec stubs for") do |name|
+ config[:constants] << name
+ end
+ options.on("-b", "--base", "DIR",
+ "Directory to generate specs into") do |directory|
+ config[:base] = File.expand_path directory
+ end
+ options.on("-r", "--require", "LIBRARY",
+ "A library to require") do |file|
+ config[:requires] << file
+ end
+ options.on("-V", "--version-guard", "VERSION",
+ "Specify version for ruby_version_is guards") do |version|
+ config[:version] = version
+ end
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n How might this work in the real world?\n"
+ options.doc " 1. To create spec stubs for every class or module in Object\n"
+ options.doc " $ mkspec\n"
+ options.doc " 2. To create spec stubs for Fixnum\n"
+ options.doc " $ mkspec -c Fixnum\n"
+ options.doc " 3. To create spec stubs for Complex in 'superspec/complex'\n"
+ options.doc " $ mkspec -c Complex -r complex -b superspec"
+ options.doc ""
+
+ options.parse argv
+ end
+
+ def create_directory(mod)
+ subdir = @map.dir_name mod, config[:base]
+
+ if File.exist? subdir
+ unless File.directory? subdir
+ puts "#{subdir} already exists and is not a directory."
+ return nil
+ end
+ else
+ mkdir_p subdir
+ end
+
+ subdir
+ end
+
+ def write_requires(dir, file)
+ prefix = config[:base] + '/'
+ raise dir unless dir.start_with? prefix
+ sub = dir[prefix.size..-1]
+ parents = '../' * (sub.split('/').length + 1)
+
+ File.open(file, 'w') do |f|
+ f.puts "require_relative '#{parents}spec_helper'"
+ config[:requires].each do |lib|
+ f.puts "require '#{lib}'"
+ end
+ end
+ end
+
+ def write_version(f)
+ f.puts ""
+ if version = config[:version]
+ f.puts "ruby_version_is #{version} do"
+ yield " "
+ f.puts "end"
+ else
+ yield ""
+ end
+ end
+
+ def write_spec(file, meth, exists)
+ if exists
+ command = "#{RbConfig.ruby} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e '#{meth}' #{file}"
+ puts "$ #{command}" if $DEBUG
+ out = `#{command}`
+ return if out.include?(meth)
+ end
+
+ File.open file, 'a' do |f|
+ write_version(f) do |indent|
+ f.puts <<-EOS
+#{indent}describe "#{meth}" do
+#{indent} it "needs to be reviewed for spec completeness"
+#{indent}end
+EOS
+ end
+ end
+
+ puts file
+ end
+
+ def create_file(dir, mod, meth, name)
+ file = File.join dir, @map.file_name(meth, mod)
+ exists = File.exist? file
+
+ write_requires dir, file unless exists
+ write_spec file, name, exists
+ end
+
+ def run
+ config[:requires].each { |lib| require lib }
+ constants = config[:constants]
+ constants = Object.constants if constants.empty?
+
+ @map.map({}, constants).each do |mod, methods|
+ name = mod.chop
+ next unless dir = create_directory(name)
+
+ methods.each { |method| create_file dir, name, method, mod + method }
+ end
+ end
+
+ def self.main
+ ENV['MSPEC_RUNNER'] = '1'
+
+ script = new
+ script.options
+ script.run
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-ci.rb b/spec/mspec/lib/mspec/commands/mspec-ci.rb
new file mode 100644
index 0000000000..8951572f69
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-ci.rb
@@ -0,0 +1,76 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecCI < MSpecScript
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mspec ci [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. How to run the specs?"
+ options.doc " 2. How to modify the guard behavior?"
+ options.doc " 2. How to display the output?"
+ options.doc " 3. What action to perform?"
+ options.doc " 4. When to perform it?"
+
+ options.doc "\n How to run the specs"
+ options.chdir
+ options.prefix
+ options.configure { |f| load f }
+ options.repeat
+ options.pretend
+ options.interrupt
+ options.timeout
+
+ options.doc "\n How to modify the guard behavior"
+ options.unguarded
+ options.verify
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform"
+ options.actions
+
+ options.doc "\n When to perform it"
+ options.action_filters
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To simply run the known good specs"
+ options.doc "\n $ mspec ci"
+ options.doc "\n 2. To run a subset of the known good specs"
+ options.doc "\n $ mspec ci path/to/specs"
+ options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
+ options.doc "\n $ mspec ci --spec-debug -S 'this crashes'"
+ options.doc ""
+
+ patterns = options.parse argv
+ patterns = config[:ci_files] if patterns.empty?
+ @files = files patterns
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ tags = ["fails", "critical", "unstable", "incomplete", "unsupported"]
+ tags += Array(config[:ci_xtags])
+
+ require 'mspec/runner/filters/tag'
+ filter = TagFilter.new(:exclude, *tags)
+ filter.register
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-run.rb b/spec/mspec/lib/mspec/commands/mspec-run.rb
new file mode 100644
index 0000000000..0fb338fa23
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-run.rb
@@ -0,0 +1,87 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecRun < MSpecScript
+ def initialize
+ super
+
+ config[:files] = []
+ end
+
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mspec run [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. What specs to run?"
+ options.doc " 2. How to modify the execution?"
+ options.doc " 3. How to modify the guard behavior?"
+ options.doc " 4. How to display the output?"
+ options.doc " 5. What action to perform?"
+ options.doc " 6. When to perform it?"
+
+ options.doc "\n What specs to run"
+ options.filters
+
+ options.doc "\n How to modify the execution"
+ options.chdir
+ options.prefix
+ options.configure { |f| load f }
+ options.env
+ options.randomize
+ options.repeat
+ options.pretend
+ options.interrupt
+ options.timeout
+
+ options.doc "\n How to modify the guard behavior"
+ options.unguarded
+ options.verify
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform"
+ options.actions
+
+ options.doc "\n When to perform it"
+ options.action_filters
+
+ options.doc "\n Launchable"
+ options.launchable
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To simply run some specs"
+ options.doc "\n $ mspec path/to/the/specs"
+ options.doc " mspec path/to/the_file_spec.rb"
+ options.doc "\n 2. To run specs tagged with 'fails'"
+ options.doc "\n $ mspec -g fails path/to/the_file_spec.rb"
+ options.doc "\n 3. To start the debugger before the spec matching 'this crashes'"
+ options.doc "\n $ mspec --spec-debug -S 'this crashes' path/to/the_file_spec.rb"
+ options.doc "\n 4. To run some specs matching 'this crashes'"
+ options.doc "\n $ mspec -e 'this crashes' path/to/the_file_spec.rb"
+
+ options.doc ""
+
+ patterns = options.parse argv
+ @files = files_from_patterns(patterns)
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
diff --git a/spec/mspec/lib/mspec/commands/mspec-tag.rb b/spec/mspec/lib/mspec/commands/mspec-tag.rb
new file mode 100644
index 0000000000..9ce9f048c6
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec-tag.rb
@@ -0,0 +1,132 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+
+
+class MSpecTag < MSpecScript
+ def initialize
+ super
+
+ config[:tagger] = :add
+ config[:tag] = 'fails:'
+ config[:outcome] = :fail
+ config[:ltags] = []
+ end
+
+ def options(argv = ARGV)
+ options = MSpecOptions.new "mspec tag [options] (FILE|DIRECTORY|GLOB)+", 30, config
+
+ options.doc " Ask yourself:"
+ options.doc " 1. What specs to run?"
+ options.doc " 2. How to modify the execution?"
+ options.doc " 3. How to display the output?"
+ options.doc " 4. What tag action to perform?"
+ options.doc " 5. When to perform it?"
+
+ options.doc "\n What specs to run"
+ options.filters
+
+ options.doc "\n How to modify the execution"
+ options.configure { |f| load f }
+ options.pretend
+ options.unguarded
+ options.interrupt
+ options.timeout
+
+ options.doc "\n How to display their output"
+ options.formatters
+ options.verbose
+
+ options.doc "\n What action to perform and when to perform it"
+ options.on("-N", "--add", "TAG",
+ "Add TAG with format 'tag' or 'tag(comment)' (see -Q, -F, -L)") do |o|
+ config[:tagger] = :add
+ config[:tag] = "#{o}:"
+ end
+ options.on("-R", "--del", "TAG",
+ "Delete TAG (see -Q, -F, -L)") do |o|
+ config[:tagger] = :del
+ config[:tag] = "#{o}:"
+ config[:outcome] = :pass
+ end
+ options.on("-Q", "--pass", "Apply action to specs that pass (default for --del)") do
+ config[:outcome] = :pass
+ end
+ options.on("-F", "--fail", "Apply action to specs that fail (default for --add)") do
+ config[:outcome] = :fail
+ end
+ options.on("-L", "--all", "Apply action to all specs") do
+ config[:outcome] = :all
+ end
+ options.on("--list", "TAG", "Display descriptions of any specs tagged with TAG") do |t|
+ config[:tagger] = :list
+ config[:ltags] << t
+ end
+ options.on("--list-all", "Display descriptions of any tagged specs") do
+ config[:tagger] = :list_all
+ end
+ options.on("--purge", "Remove all tags not matching any specs") do
+ config[:tagger] = :purge
+ end
+
+ options.doc "\n Help!"
+ options.debug
+ options.version MSpec::VERSION
+ options.help
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ options.doc "\n How might this work in the real world?"
+ options.doc "\n 1. To add the 'fails' tag to failing specs"
+ options.doc "\n $ mspec tag path/to/the_file_spec.rb"
+ options.doc "\n 2. To remove the 'fails' tag from passing specs"
+ options.doc "\n $ mspec tag --del fails path/to/the_file_spec.rb"
+ options.doc "\n 3. To display the descriptions for all specs tagged with 'fails'"
+ options.doc "\n $ mspec tag --list fails path/to/the/specs"
+ options.doc ""
+
+ patterns = options.parse argv
+ if patterns.empty?
+ puts options
+ puts "No files specified."
+ exit 1
+ end
+ @files = files patterns
+ end
+
+ def register
+ require 'mspec/runner/actions'
+
+ case config[:tagger]
+ when :add, :del
+ tag = SpecTag.new config[:tag]
+ tagger = TagAction.new(config[:tagger], config[:outcome], tag.tag, tag.comment,
+ config[:atags], config[:astrings])
+ when :list, :list_all
+ tagger = TagListAction.new config[:tagger] == :list_all ? nil : config[:ltags]
+ MSpec.register_mode :pretend
+ config[:formatter] = false
+ when :purge
+ tagger = TagPurgeAction.new
+ MSpec.register_mode :pretend
+ MSpec.register_mode :unguarded
+ config[:formatter] = false
+ config[:xtags] = []
+ else
+ raise ArgumentError, "No recognized action given"
+ end
+ tagger.register
+
+ super
+ end
+
+ def run
+ MSpec.register_tags_patterns config[:tags_patterns]
+ MSpec.register_files @files
+
+ MSpec.process
+ exit MSpec.exit_code
+ end
+end
+
diff --git a/spec/mspec/lib/mspec/commands/mspec.rb b/spec/mspec/lib/mspec/commands/mspec.rb
new file mode 100644
index 0000000000..a9d94ca354
--- /dev/null
+++ b/spec/mspec/lib/mspec/commands/mspec.rb
@@ -0,0 +1,113 @@
+require 'mspec/version'
+require 'mspec/utils/options'
+require 'mspec/utils/script'
+require 'mspec/helpers/tmp'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/timer'
+
+
+class MSpecMain < MSpecScript
+ def initialize
+ super
+
+ config[:loadpath] = []
+ config[:requires] = []
+ config[:target] = ENV['RUBY'] || 'ruby'
+ config[:flags] = []
+ config[:command] = nil
+ config[:options] = []
+ config[:launch] = []
+ end
+
+ def options(argv = ARGV)
+ config[:command] = argv.shift if ["ci", "run", "tag"].include?(argv[0])
+
+ options = MSpecOptions.new "mspec [COMMAND] [options] (FILE|DIRECTORY|GLOB)+", 30, config
+ @options = options
+
+ options.doc " The mspec command sets up and invokes the sub-commands"
+ options.doc " (see below) to enable, for instance, running the specs"
+ options.doc " with different implementations like ruby, jruby, rbx, etc.\n"
+
+ options.configure do |f|
+ load f
+ config[:options] << '-B' << f
+ end
+
+ options.targets
+
+ options.on("-j", "--multi", "Run multiple (possibly parallel) subprocesses") do
+ config[:multi] = true
+ end
+
+ options.version MSpec::VERSION do
+ if config[:command]
+ config[:options] << "-v"
+ else
+ puts "#{File.basename $0} #{MSpec::VERSION}"
+ exit
+ end
+ end
+
+ options.help do
+ if config[:command]
+ config[:options] << "-h"
+ else
+ puts options
+ exit 1
+ end
+ end
+
+ options.doc "\n Custom options"
+ custom_options options
+
+ # The rest of the help output
+ options.doc "\n where COMMAND is one of:\n"
+ options.doc " run - Run the specified specs (default)"
+ options.doc " ci - Run the known good specs"
+ options.doc " tag - Add or remove tags\n"
+ options.doc " mspec COMMAND -h for more options\n"
+ options.doc " example: $ mspec run -h\n"
+
+ options.on_extra { |o| config[:options] << o }
+ options.parse(argv)
+
+ if config[:multi]
+ options = MSpecOptions.new "mspec", 30, config
+ options.all
+ patterns = options.parse(config[:options])
+ @files = files_from_patterns(patterns)
+ end
+ end
+
+ def register; end
+
+ def multi_exec(argv)
+ require 'mspec/runner/formatters/multi'
+ formatter = config_formatter.extend(MultiFormatter)
+
+ require 'mspec/runner/parallel'
+ processes = cores(@files.size)
+ ParallelRunner.new(@files, processes, formatter, argv).run
+ end
+
+ def run
+ argv = config[:target].split(/\s+/)
+
+ argv.concat config[:launch]
+ argv.concat config[:flags]
+ argv.concat config[:loadpath]
+ argv.concat config[:requires]
+ argv << "#{MSPEC_HOME}/bin/mspec-#{config[:command] || 'run'}"
+ argv.concat config[:options]
+
+ if config[:multi]
+ exit multi_exec(argv)
+ else
+ log = config[:options].include?('--error-output') ? $stdout : $stderr
+ log.puts "$ #{argv.join(' ')}"
+ log.flush
+ exec(*argv, close_others: false)
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/expectations.rb b/spec/mspec/lib/mspec/expectations.rb
new file mode 100644
index 0000000000..d07f959b27
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations.rb
@@ -0,0 +1,2 @@
+require 'mspec/expectations/expectations'
+require 'mspec/expectations/should'
diff --git a/spec/mspec/lib/mspec/expectations/expectations.rb b/spec/mspec/lib/mspec/expectations/expectations.rb
new file mode 100644
index 0000000000..09852ab557
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations/expectations.rb
@@ -0,0 +1,39 @@
+class SpecExpectationNotMetError < StandardError
+end
+
+class SpecExpectationNotFoundError < StandardError
+ def message
+ "No behavior expectation was found in the example"
+ end
+end
+
+class SkippedSpecError < StandardError
+end
+
+class SpecExpectation
+ def self.fail_with(expected, actual)
+ expected_to_s = expected.to_s
+ actual_to_s = actual.to_s
+ if expected_to_s.size + actual_to_s.size > 80
+ message = "#{expected_to_s}\n#{actual_to_s}"
+ else
+ message = "#{expected_to_s} #{actual_to_s}"
+ end
+ raise SpecExpectationNotMetError, message
+ end
+
+ def self.fail_predicate(receiver, predicate, args, block, result, expectation)
+ receiver_to_s = MSpec.format(receiver)
+ before_method = predicate.to_s =~ /^[a-z]/ ? "." : " "
+ predicate_to_s = "#{before_method}#{predicate}"
+ predicate_to_s += " " unless args.empty?
+ args_to_s = args.map { |arg| MSpec.format(arg) }.join(', ')
+ args_to_s += " { ... }" if block
+ result_to_s = MSpec.format(result)
+ raise SpecExpectationNotMetError, "Expected #{receiver_to_s}#{predicate_to_s}#{args_to_s}\n#{expectation} but was #{result_to_s}"
+ end
+
+ def self.fail_single_arg_predicate(receiver, predicate, arg, result, expectation)
+ fail_predicate(receiver, predicate, [arg], nil, result, expectation)
+ end
+end
diff --git a/spec/mspec/lib/mspec/expectations/should.rb b/spec/mspec/lib/mspec/expectations/should.rb
new file mode 100644
index 0000000000..c1790e0ac8
--- /dev/null
+++ b/spec/mspec/lib/mspec/expectations/should.rb
@@ -0,0 +1,41 @@
+class Object
+ NO_MATCHER_GIVEN = Object.new
+
+ def should(matcher = NO_MATCHER_GIVEN, &block)
+ MSpec.expectation
+ state = MSpec.current.state
+ raise "should outside example" unless state
+ MSpec.actions :expectation, state
+
+ if NO_MATCHER_GIVEN.equal?(matcher)
+ SpecPositiveOperatorMatcher.new(self)
+ else
+ # The block was given to #should syntactically, but it was intended for a matcher like #raise_error
+ matcher.block = block if block
+
+ unless matcher.matches? self
+ expected, actual = matcher.failure_message
+ SpecExpectation.fail_with(expected, actual)
+ end
+ end
+ end
+
+ def should_not(matcher = NO_MATCHER_GIVEN, &block)
+ MSpec.expectation
+ state = MSpec.current.state
+ raise "should_not outside example" unless state
+ MSpec.actions :expectation, state
+
+ if NO_MATCHER_GIVEN.equal?(matcher)
+ SpecNegativeOperatorMatcher.new(self)
+ else
+ # The block was given to #should_not syntactically, but it was intended for the matcher
+ matcher.block = block if block
+
+ if matcher.matches? self
+ expected, actual = matcher.negative_failure_message
+ SpecExpectation.fail_with(expected, actual)
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/guards.rb b/spec/mspec/lib/mspec/guards.rb
new file mode 100644
index 0000000000..454ac0c776
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards.rb
@@ -0,0 +1,11 @@
+require 'mspec/guards/block_device'
+require 'mspec/guards/bug'
+require 'mspec/guards/conflict'
+require 'mspec/guards/endian'
+require 'mspec/guards/feature'
+require 'mspec/guards/guard'
+require 'mspec/guards/platform'
+require 'mspec/guards/quarantine'
+require 'mspec/guards/support'
+require 'mspec/guards/superuser'
+require 'mspec/guards/version'
diff --git a/spec/mspec/lib/mspec/guards/block_device.rb b/spec/mspec/lib/mspec/guards/block_device.rb
new file mode 100644
index 0000000000..ae736a2d4e
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/block_device.rb
@@ -0,0 +1,16 @@
+require 'mspec/guards/guard'
+
+class BlockDeviceGuard < SpecGuard
+ def match?
+ platform_is_not :freebsd, :windows, :opal do
+ block = `find /dev /devices -type b 2> /dev/null`
+ return !(block.nil? || block.empty?)
+ end
+
+ false
+ end
+end
+
+def with_block_device(&block)
+ BlockDeviceGuard.new.run_if(:with_block_device, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/bug.rb b/spec/mspec/lib/mspec/guards/bug.rb
new file mode 100644
index 0000000000..a6af0ef964
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/bug.rb
@@ -0,0 +1,29 @@
+require 'mspec/guards/version'
+
+class BugGuard < VersionGuard
+ def initialize(bug, requirement)
+ @bug = bug
+ if String === requirement
+ MSpec.deprecate "ruby_bug with a single version", 'an exclusive range ("2.1"..."2.3")'
+ super(FULL_RUBY_VERSION, requirement)
+ @requirement = SpecVersion.new requirement, true
+ else
+ super(FULL_RUBY_VERSION, requirement)
+ end
+ end
+
+ def match?
+ return false if MSpec.mode? :no_ruby_bug
+ return false unless PlatformGuard.standard?
+
+ if Range === @requirement
+ super
+ else
+ FULL_RUBY_VERSION <= @requirement
+ end
+ end
+end
+
+def ruby_bug(bug, requirement, &block)
+ BugGuard.new(bug, requirement).run_unless(:ruby_bug, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/conflict.rb b/spec/mspec/lib/mspec/guards/conflict.rb
new file mode 100644
index 0000000000..4930e5734d
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/conflict.rb
@@ -0,0 +1,23 @@
+require 'mspec/guards/guard'
+require 'mspec/utils/deprecate'
+
+class ConflictsGuard < SpecGuard
+ def initialize(*args)
+ MSpec.deprecate 'conflicts_with', 'guard -> { condition } do'
+ super(*args)
+ end
+
+ def match?
+ # Always convert constants to symbols regardless of version.
+ constants = Object.constants.map { |x| x.to_sym }
+ @parameters.any? { |mod| constants.include? mod }
+ end
+end
+
+# In some cases, libraries will modify another Ruby method's
+# behavior. The specs for the method's behavior will then fail
+# if that library is loaded. This guard will not run if any of
+# the specified constants exist in Object.constants.
+def conflicts_with(*modules, &block)
+ ConflictsGuard.new(*modules).run_unless(:conflicts_with, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/endian.rb b/spec/mspec/lib/mspec/guards/endian.rb
new file mode 100644
index 0000000000..79335a8933
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/endian.rb
@@ -0,0 +1,25 @@
+require 'mspec/guards/guard'
+
+# Despite that these are inverses, the two classes are
+# used to simplify MSpec guard reporting modes
+
+class EndianGuard < SpecGuard
+ def pattern
+ @pattern ||= [1].pack('L')
+ end
+ private :pattern
+end
+
+class BigEndianGuard < EndianGuard
+ def match?
+ pattern[-1] == ?\001
+ end
+end
+
+def big_endian(&block)
+ BigEndianGuard.new.run_if(:big_endian, &block)
+end
+
+def little_endian(&block)
+ BigEndianGuard.new.run_unless(:little_endian, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/feature.rb b/spec/mspec/lib/mspec/guards/feature.rb
new file mode 100644
index 0000000000..d4c6dd1cde
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/feature.rb
@@ -0,0 +1,45 @@
+require 'mspec/guards/guard'
+
+class FeatureGuard < SpecGuard
+ def self.enabled?(*features)
+ new(*features).match?
+ end
+
+ def match?
+ @parameters.all? { |f| MSpec.feature_enabled? f }
+ end
+end
+
+# Provides better documentation in the specs by
+# naming sets of features that work together as
+# a whole. Examples include :encoding, :fiber,
+# :continuation, :fork.
+#
+# Usage example:
+#
+# with_feature :encoding do
+# # specs for a method that provides aspects
+# # of the encoding feature
+# end
+#
+# Multiple features must all be enabled for the
+# guard to run:
+#
+# with_feature :one, :two do
+# # these specs will run if features :one AND
+# # :two are enabled.
+# end
+#
+# The implementation must explicitly enable a feature
+# by adding code like the following to the .mspec
+# configuration file:
+#
+# MSpec.enable_feature :encoding
+#
+def with_feature(*features, &block)
+ FeatureGuard.new(*features).run_if(:with_feature, &block)
+end
+
+def without_feature(*features, &block)
+ FeatureGuard.new(*features).run_unless(:without_feature, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/guard.rb b/spec/mspec/lib/mspec/guards/guard.rb
new file mode 100644
index 0000000000..3a6372a660
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/guard.rb
@@ -0,0 +1,141 @@
+require 'mspec/runner/mspec'
+require 'mspec/runner/actions/tally'
+
+class SpecGuard
+ def self.report
+ @report ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.clear
+ @report = nil
+ end
+
+ def self.finish
+ report.keys.sort.each do |key|
+ desc = report[key]
+ size = desc.size
+ spec = size == 1 ? "spec" : "specs"
+ print "\n\n#{size} #{spec} omitted by guard: #{key}:\n"
+ desc.each { |description| print "\n", description; }
+ end
+
+ print "\n\n"
+ end
+
+ def self.guards
+ @guards ||= []
+ end
+
+ def self.clear_guards
+ @guards = []
+ end
+
+ # Returns a partial Ruby version string based on +which+.
+ # For example, if RUBY_VERSION = 8.2.3:
+ #
+ # :major => "8"
+ # :minor => "8.2"
+ # :tiny => "8.2.3"
+ # :teeny => "8.2.3"
+ # :full => "8.2.3"
+ def self.ruby_version(which = :minor)
+ case which
+ when :major
+ n = 1
+ when :minor
+ n = 2
+ when :tiny, :teeny, :full
+ n = 3
+ end
+
+ RUBY_VERSION.split('.')[0,n].join('.')
+ end
+
+ attr_accessor :name
+
+ def initialize(*args)
+ @parameters = args
+ end
+
+ def yield?(invert = false)
+ return true if MSpec.mode? :unguarded
+
+ allow = match? ^ invert
+
+ if !allow and reporting?
+ MSpec.guard
+ MSpec.register :finish, SpecGuard
+ MSpec.register :add, self
+ return true
+ elsif MSpec.mode? :verify
+ return true
+ end
+
+ allow
+ end
+
+ def run_if(name, &block)
+ @name = name
+ if block
+ yield if yield?(false)
+ else
+ yield?(false)
+ end
+ ensure
+ unregister
+ end
+
+ def run_unless(name, &block)
+ @name = name
+ if block
+ yield if yield?(true)
+ else
+ yield?(true)
+ end
+ ensure
+ unregister
+ end
+
+ def reporting?
+ MSpec.mode?(:report) or
+ (MSpec.mode?(:report_on) and SpecGuard.guards.include?(name))
+ end
+
+ def report_key
+ "#{name} #{@parameters.join(", ")}"
+ end
+
+ def record(description)
+ SpecGuard.report[report_key] << description
+ end
+
+ def add(example)
+ record example.description
+ MSpec.formatter.tally.counter.guards!
+ end
+
+ def unregister
+ MSpec.unguard
+ MSpec.unregister :add, self
+ end
+
+ def match?
+ raise "must be implemented by the subclass"
+ end
+end
+
+# Combined guards
+
+def guard(condition, &block)
+ raise "condition must be a Proc" unless condition.is_a?(Proc)
+ raise LocalJumpError, "no block given" unless block
+ return yield if MSpec.mode? :unguarded or MSpec.mode? :verify or MSpec.mode? :report
+ yield if condition.call
+end
+
+def guard_not(condition, &block)
+ raise "condition must be a Proc" unless condition.is_a?(Proc)
+ raise LocalJumpError, "no block given" unless block
+ return yield if MSpec.mode? :unguarded or MSpec.mode? :verify or MSpec.mode? :report
+ yield unless condition.call
+end
diff --git a/spec/mspec/lib/mspec/guards/platform.rb b/spec/mspec/lib/mspec/guards/platform.rb
new file mode 100644
index 0000000000..fadd8d75ef
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/platform.rb
@@ -0,0 +1,122 @@
+require 'mspec/guards/guard'
+
+class PlatformGuard < SpecGuard
+ def self.implementation?(*args)
+ args.any? do |name|
+ case name
+ when :rubinius
+ RUBY_ENGINE.start_with?('rbx')
+ else
+ RUBY_ENGINE.start_with?(name.to_s)
+ end
+ end
+ end
+
+ def self.standard?
+ implementation? :ruby
+ end
+
+ PLATFORM = if RUBY_ENGINE == "jruby"
+ require 'rbconfig'
+ "#{RbConfig::CONFIG['host_cpu']}-#{RbConfig::CONFIG['host_os']}"
+ else
+ RUBY_PLATFORM
+ end
+
+ def self.os?(*oses)
+ oses.any? do |os|
+ raise ":java is not a valid OS" if os == :java
+ case os
+ when :windows
+ PLATFORM =~ /(mswin|mingw)/
+ when :wsl
+ wsl?
+ else
+ PLATFORM.include?(os.to_s)
+ end
+ end
+ end
+
+ def self.windows?
+ os?(:windows)
+ end
+
+ def self.wasi?
+ os?(:wasi)
+ end
+
+ def self.wsl?
+ if defined?(@wsl_p)
+ @wsl_p
+ else
+ @wsl_p = `uname -r`.match?(/microsoft/i)
+ end
+ end
+
+ # In bits
+ WORD_SIZE = 1.size * 8
+ deprecate_constant :WORD_SIZE
+
+ # In bits
+ POINTER_SIZE = begin
+ require 'rbconfig/sizeof'
+ RbConfig::SIZEOF["void*"] * 8
+ rescue LoadError
+ [0].pack('j').size * 8
+ end
+
+ # In bits
+ C_LONG_SIZE = if defined?(RbConfig::SIZEOF[])
+ RbConfig::SIZEOF["long"] * 8
+ else
+ [0].pack('l!').size * 8
+ end
+
+ def self.wordsize?(size)
+ warn "#wordsize? is deprecated, use #c_long_size?"
+ size == WORD_SIZE
+ end
+
+ def self.pointer_size?(size)
+ size == POINTER_SIZE
+ end
+
+ def self.c_long_size?(size)
+ size == C_LONG_SIZE
+ end
+
+ def initialize(*args)
+ if args.last.is_a?(Hash)
+ @options, @platforms = args.last, args[0..-2]
+ else
+ @options, @platforms = {}, args
+ end
+ @parameters = args
+ end
+
+ def match?
+ match = @platforms.empty? ? true : PlatformGuard.os?(*@platforms)
+ @options.each do |key, value|
+ case key
+ when :os
+ match &&= PlatformGuard.os?(*value)
+ when :pointer_size
+ match &&= PlatformGuard.pointer_size? value
+ when :wordsize
+ warn ":wordsize is deprecated, use :c_long_size"
+ match &&= PlatformGuard.wordsize? value
+ when :c_long_size
+ match &&= PlatformGuard::c_long_size? value
+ end
+ end
+ match
+ end
+end
+
+def platform_is(*args, &block)
+ PlatformGuard.new(*args).run_if(:platform_is, &block)
+end
+
+def platform_is_not(*args, &block)
+ PlatformGuard.new(*args).run_unless(:platform_is_not, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/quarantine.rb b/spec/mspec/lib/mspec/guards/quarantine.rb
new file mode 100644
index 0000000000..ec4d01f9ea
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/quarantine.rb
@@ -0,0 +1,11 @@
+require 'mspec/guards/guard'
+
+class QuarantineGuard < SpecGuard
+ def match?
+ true
+ end
+end
+
+def quarantine!(&block)
+ QuarantineGuard.new.run_unless(:quarantine!, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/superuser.rb b/spec/mspec/lib/mspec/guards/superuser.rb
new file mode 100644
index 0000000000..24daf9b26c
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/superuser.rb
@@ -0,0 +1,25 @@
+require 'mspec/guards/guard'
+
+class SuperUserGuard < SpecGuard
+ def match?
+ Process.euid == 0
+ end
+end
+
+class RealSuperUserGuard < SpecGuard
+ def match?
+ Process.uid == 0
+ end
+end
+
+def as_superuser(&block)
+ SuperUserGuard.new.run_if(:as_superuser, &block)
+end
+
+def as_real_superuser(&block)
+ RealSuperUserGuard.new.run_if(:as_real_superuser, &block)
+end
+
+def as_user(&block)
+ SuperUserGuard.new.run_unless(:as_user, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/support.rb b/spec/mspec/lib/mspec/guards/support.rb
new file mode 100644
index 0000000000..790bea1077
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/support.rb
@@ -0,0 +1,14 @@
+require 'mspec/guards/platform'
+
+class SupportedGuard < SpecGuard
+ def match?
+ if @parameters.include? :ruby
+ raise Exception, "improper use of not_supported_on guard"
+ end
+ !PlatformGuard.standard? and PlatformGuard.implementation?(*@parameters)
+ end
+end
+
+def not_supported_on(*args, &block)
+ SupportedGuard.new(*args).run_unless(:not_supported_on, &block)
+end
diff --git a/spec/mspec/lib/mspec/guards/version.rb b/spec/mspec/lib/mspec/guards/version.rb
new file mode 100644
index 0000000000..f5ea1988ae
--- /dev/null
+++ b/spec/mspec/lib/mspec/guards/version.rb
@@ -0,0 +1,72 @@
+require 'mspec/utils/deprecate'
+require 'mspec/utils/version'
+require 'mspec/guards/guard'
+
+class VersionGuard < SpecGuard
+ FULL_RUBY_VERSION = SpecVersion.new SpecGuard.ruby_version(:full)
+
+ def initialize(version, requirement)
+ version = SpecVersion.new(version) unless SpecVersion === version
+ @version = version
+
+ case requirement
+ when String
+ @requirement = SpecVersion.new requirement
+ when Range
+ MSpec.deprecate "an empty version range end", 'a specific version' if requirement.end.empty?
+ a = SpecVersion.new requirement.begin
+ b = SpecVersion.new requirement.end
+ unless requirement.exclude_end?
+ MSpec.deprecate "ruby_version_is with an inclusive range", 'an exclusive range ("2.1"..."2.3")'
+ end
+ @requirement = requirement.exclude_end? ? a...b : a..b
+ else
+ raise "version must be a String or Range but was a #{requirement.class}"
+ end
+ super(@version, @requirement)
+ end
+
+ def match?
+ if Range === @requirement
+ @requirement.include? @version
+ else
+ @version >= @requirement
+ end
+ end
+
+ @kernel_version = nil
+ def self.kernel_version
+ if @kernel_version
+ @kernel_version
+ else
+ if v = RUBY_PLATFORM[/darwin(\d+)/, 1] # build time version
+ uname = v
+ else
+ begin
+ require 'etc'
+ etc = true
+ rescue LoadError
+ etc = false
+ end
+ if etc and Etc.respond_to?(:uname)
+ uname = Etc.uname.fetch(:release)
+ else
+ uname = `uname -r`.chomp
+ end
+ end
+ @kernel_version = uname
+ end
+ end
+end
+
+def version_is(base_version, requirement, &block)
+ VersionGuard.new(base_version, requirement).run_if(:version_is, &block)
+end
+
+def ruby_version_is(requirement, &block)
+ VersionGuard.new(VersionGuard::FULL_RUBY_VERSION, requirement).run_if(:ruby_version_is, &block)
+end
+
+def kernel_version_is(requirement, &block)
+ VersionGuard.new(VersionGuard.kernel_version, requirement).run_if(:kernel_version_is, &block)
+end
diff --git a/spec/mspec/lib/mspec/helpers.rb b/spec/mspec/lib/mspec/helpers.rb
new file mode 100644
index 0000000000..90f9fd3fd4
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers.rb
@@ -0,0 +1,13 @@
+require 'mspec/helpers/argf'
+require 'mspec/helpers/argv'
+require 'mspec/helpers/datetime'
+require 'mspec/helpers/fixture'
+require 'mspec/helpers/flunk'
+require 'mspec/helpers/fs'
+require 'mspec/helpers/io'
+require 'mspec/helpers/mock_to_path'
+require 'mspec/helpers/numeric'
+require 'mspec/helpers/ruby_exe'
+require 'mspec/helpers/scratch'
+require 'mspec/helpers/tmp'
+require 'mspec/helpers/warning'
diff --git a/spec/mspec/lib/mspec/helpers/argf.rb b/spec/mspec/lib/mspec/helpers/argf.rb
new file mode 100644
index 0000000000..4d3e0f46b3
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/argf.rb
@@ -0,0 +1,35 @@
+# Convenience helper for specs using ARGF.
+# Set @argf to an instance of ARGF.class with the given +argv+.
+# That instance must be used instead of ARGF as ARGF is global
+# and it is not always possible to reset its state correctly.
+#
+# The helper yields to the block and then close
+# the files open by the instance. Example:
+#
+# describe "That" do
+# it "does something" do
+# argf ['a', 'b'] do
+# # do something
+# end
+# end
+# end
+def argf(argv)
+ if argv.empty? or argv.length > 2
+ raise "Only 1 or 2 filenames are allowed for the argf helper so files can be properly closed: #{argv.inspect}"
+ end
+ @argf ||= nil
+ raise "Cannot nest calls to the argf helper" if @argf
+
+ @argf = ARGF.class.new(*argv)
+ @__mspec_saved_argf_file__ = @argf.file
+ begin
+ yield
+ ensure
+ file1 = @__mspec_saved_argf_file__
+ file2 = @argf.file # Either the first file or the second
+ file1.close if !file1.closed? and file1 != STDIN
+ file2.close if !file2.closed? and file2 != STDIN
+ @argf = nil
+ @__mspec_saved_argf_file__ = nil
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/argv.rb b/spec/mspec/lib/mspec/helpers/argv.rb
new file mode 100644
index 0000000000..9dac384dbd
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/argv.rb
@@ -0,0 +1,44 @@
+# Convenience helper for altering ARGV. Saves the
+# value of ARGV and sets it to +args+. If a block
+# is given, yields to the block and then restores
+# the value of ARGV. The previously saved value of
+# ARGV can be restored by passing +:restore+. The
+# former is useful in a single spec. The latter is
+# useful in before/after actions. For example:
+#
+# describe "This" do
+# before do
+# argv ['a', 'b']
+# end
+#
+# after do
+# argv :restore
+# end
+#
+# it "does something" do
+# # do something
+# end
+# end
+#
+# describe "That" do
+# it "does something" do
+# argv ['a', 'b'] do
+# # do something
+# end
+# end
+# end
+def argv(args)
+ if args == :restore
+ ARGV.replace(@__mspec_saved_argv__ || [])
+ else
+ @__mspec_saved_argv__ = ARGV.dup
+ ARGV.replace args
+ if block_given?
+ begin
+ yield
+ ensure
+ argv :restore
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/datetime.rb b/spec/mspec/lib/mspec/helpers/datetime.rb
new file mode 100644
index 0000000000..84ac86b686
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/datetime.rb
@@ -0,0 +1,48 @@
+# The new_datetime helper makes writing DateTime specs more simple by
+# providing default constructor values and accepting a Hash of only the
+# constructor values needed for the particular spec. For example:
+#
+# new_datetime :hour => 1, :minute => 20
+#
+# Possible keys are:
+# :year, :month, :day, :hour, :minute, :second, :offset and :sg.
+def new_datetime(opts = {})
+ require 'date'
+
+ value = {
+ :year => -4712,
+ :month => 1,
+ :day => 1,
+ :hour => 0,
+ :minute => 0,
+ :second => 0,
+ :offset => 0,
+ :sg => Date::ITALY
+ }.merge opts
+
+ DateTime.new value[:year], value[:month], value[:day], value[:hour],
+ value[:minute], value[:second], value[:offset], value[:sg]
+end
+
+def with_timezone(name, offset = nil, daylight_saving_zone = "")
+ skip "WASI doesn't have TZ concept" if PlatformGuard.wasi?
+ zone = name.dup
+
+ if offset
+ # TZ convention is backwards
+ offset = -offset
+
+ zone += offset.to_s
+ zone += ":00:00"
+ end
+ zone += daylight_saving_zone
+
+ old = ENV["TZ"]
+ ENV["TZ"] = zone
+
+ begin
+ yield
+ ensure
+ ENV["TZ"] = old
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/fixture.rb b/spec/mspec/lib/mspec/helpers/fixture.rb
new file mode 100644
index 0000000000..f3bbe423bd
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/fixture.rb
@@ -0,0 +1,24 @@
+# Returns the name of a fixture file by adjoining the directory
+# of the +file+ argument with "fixtures" and the contents of the
+# +args+ array. For example,
+#
+# +file+ == "some/example_spec.rb"
+#
+# and
+#
+# +args+ == ["subdir", "file.txt"]
+#
+# then the result is the expanded path of
+#
+# "some/fixtures/subdir/file.txt".
+def fixture(file, *args)
+ path = File.dirname(file)
+ path = path[0..-7] if path[-7..-1] == "/shared"
+ fixtures = path[-9..-1] == "/fixtures" ? "" : "fixtures"
+ if File.respond_to?(:realpath)
+ path = File.realpath(path)
+ else
+ path = File.expand_path(path)
+ end
+ File.join(path, fixtures, args)
+end
diff --git a/spec/mspec/lib/mspec/helpers/flunk.rb b/spec/mspec/lib/mspec/helpers/flunk.rb
new file mode 100644
index 0000000000..84fb3ab39c
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/flunk.rb
@@ -0,0 +1,3 @@
+def flunk(msg = "This example is a failure")
+ SpecExpectation.fail_with "Failed:", msg
+end
diff --git a/spec/mspec/lib/mspec/helpers/fs.rb b/spec/mspec/lib/mspec/helpers/fs.rb
new file mode 100644
index 0000000000..67453eb302
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/fs.rb
@@ -0,0 +1,64 @@
+# Copies a file
+def cp(source, dest)
+ IO.copy_stream source, dest
+end
+
+# Creates each directory in path that does not exist.
+def mkdir_p(path)
+ parts = File.expand_path(path).split %r[/|\\]
+ name = parts.shift
+ parts.each do |part|
+ name = File.join name, part
+
+ if File.file? name
+ raise ArgumentError, "path component of #{path} is a file"
+ end
+
+ unless File.directory? name
+ begin
+ Dir.mkdir name
+ rescue Errno::EEXIST => e
+ if File.directory? name
+ # OK, another process/thread created the same directory
+ else
+ raise e
+ end
+ end
+ end
+ end
+end
+
+# Recursively removes all files and directories in +path+
+# if +path+ is a directory. Removes the file if +path+ is
+# a file.
+def rm_r(*paths)
+ paths.each do |path|
+ path = File.expand_path path
+
+ prefix = SPEC_TEMP_DIR
+ unless path[0, prefix.size] == prefix
+ raise ArgumentError, "#{path} is not prefixed by #{prefix}"
+ end
+
+ # File.symlink? needs to be checked first as
+ # File.exist? returns false for dangling symlinks
+ if File.symlink? path
+ File.unlink path
+ elsif File.directory? path
+ Dir.entries(path).each { |x| rm_r "#{path}/#{x}" unless x =~ /^\.\.?$/ }
+ Dir.rmdir path
+ elsif File.exist? path
+ File.delete path
+ end
+ end
+end
+
+# Creates a file +name+. Creates the directory for +name+
+# if it does not exist.
+def touch(name, mode = "w")
+ mkdir_p File.dirname(name)
+
+ File.open(name, mode) do |f|
+ yield f if block_given?
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/io.rb b/spec/mspec/lib/mspec/helpers/io.rb
new file mode 100644
index 0000000000..2ad14f47a1
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/io.rb
@@ -0,0 +1,87 @@
+require 'mspec/guards/feature'
+
+class IOStub
+ def initialize
+ @buffer = []
+ @output = ''
+ end
+
+ def write(*str)
+ self << str.join('')
+ end
+
+ def << str
+ @buffer << str
+ self
+ end
+
+ def print(*str)
+ write(str.join('') + $\.to_s)
+ end
+
+ def method_missing(name, *args, &block)
+ to_s.send(name, *args, &block)
+ end
+
+ def == other
+ to_s == other
+ end
+
+ def =~ other
+ to_s =~ other
+ end
+
+ def puts(*str)
+ if str.empty?
+ write "\n"
+ else
+ write(str.collect { |s| s.to_s.chomp }.concat([nil]).join("\n"))
+ end
+ end
+
+ def printf(format, *args)
+ self << sprintf(format, *args)
+ end
+
+ def flush
+ @output += @buffer.join('')
+ @buffer.clear
+ self
+ end
+
+ def to_s
+ flush
+ @output
+ end
+
+ alias_method :to_str, :to_s
+
+ def inspect
+ to_s.inspect
+ end
+end
+
+# Creates a "bare" file descriptor (i.e. one that is not associated
+# with any Ruby object). The file descriptor can safely be passed
+# to IO.new without creating a Ruby object alias to the fd.
+def new_fd(name, mode = "w:utf-8")
+ if mode.kind_of? Hash
+ if mode.key? :mode
+ mode = mode[:mode]
+ else
+ raise ArgumentError, "new_fd options Hash must include :mode"
+ end
+ end
+
+ IO.sysopen name, mode
+end
+
+# Creates an IO instance for a temporary file name. The file
+# must be deleted.
+def new_io(name, mode = "w:utf-8")
+ if Hash === mode # Avoid kwargs warnings on Ruby 2.7+
+ File.new(name, **mode)
+ else
+ File.new(name, mode)
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/mock_to_path.rb b/spec/mspec/lib/mspec/helpers/mock_to_path.rb
new file mode 100644
index 0000000000..2780afc54a
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/mock_to_path.rb
@@ -0,0 +1,6 @@
+def mock_to_path(path)
+ # Cannot use our Object#mock here since it conflicts with RSpec
+ obj = MockObject.new('path')
+ obj.should_receive(:to_path).and_return(path)
+ obj
+end
diff --git a/spec/mspec/lib/mspec/helpers/numeric.rb b/spec/mspec/lib/mspec/helpers/numeric.rb
new file mode 100644
index 0000000000..0b47855cd2
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/numeric.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+require 'mspec/guards/platform'
+
+def nan_value
+ 0/0.0
+end
+
+def infinity_value
+ 1/0.0
+end
+
+def bignum_value(plus = 0)
+ # Must be >= fixnum_max + 2, so -bignum_value is < fixnum_min
+ # A fixed value has the advantage to be the same numeric value for all Rubies and is much easier to spec
+ (2**64) + plus
+end
+
+def max_long
+ 2**(PlatformGuard::C_LONG_SIZE - 1) - 1
+end
+
+def min_long
+ -(2**(PlatformGuard::C_LONG_SIZE - 1))
+end
+
+# This is a bit hairy, but we need to be able to write specs that cover the
+# boundary between Fixnum and Bignum for operations like Fixnum#<<. Since
+# this boundary is implementation-dependent, we use these helpers to write
+# specs based on the relationship between values rather than specific
+# values.
+if PlatformGuard.standard? or PlatformGuard.implementation? :topaz
+ limits_available = begin
+ require 'rbconfig/sizeof'
+ defined?(RbConfig::LIMITS.[]) && ['FIXNUM_MAX', 'FIXNUM_MIN'].all? do |key|
+ Integer === RbConfig::LIMITS[key]
+ end
+ rescue LoadError
+ false
+ end
+
+ if limits_available
+ def fixnum_max
+ RbConfig::LIMITS['FIXNUM_MAX']
+ end
+
+ def fixnum_min
+ RbConfig::LIMITS['FIXNUM_MIN']
+ end
+ elsif PlatformGuard.c_long_size? 32
+ def fixnum_max
+ (2**30) - 1
+ end
+
+ def fixnum_min
+ -(2**30)
+ end
+ elsif PlatformGuard.c_long_size? 64
+ def fixnum_max
+ (2**62) - 1
+ end
+
+ def fixnum_min
+ -(2**62)
+ end
+ end
+elsif PlatformGuard.implementation? :opal
+ def fixnum_max
+ Integer::MAX
+ end
+
+ def fixnum_min
+ Integer::MIN
+ end
+elsif PlatformGuard.implementation? :rubinius
+ def fixnum_max
+ Fixnum::MAX
+ end
+
+ def fixnum_min
+ Fixnum::MIN
+ end
+elsif PlatformGuard.implementation?(:jruby) || PlatformGuard.implementation?(:truffleruby)
+ def fixnum_max
+ 9223372036854775807
+ end
+
+ def fixnum_min
+ -9223372036854775808
+ end
+else
+ def fixnum_max
+ raise "unknown implementation for fixnum_max() helper"
+ end
+
+ def fixnum_min
+ raise "unknown implementation for fixnum_min() helper"
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/ruby_exe.rb b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
new file mode 100644
index 0000000000..2e499d6f9a
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/ruby_exe.rb
@@ -0,0 +1,205 @@
+require 'mspec/guards/platform'
+require 'mspec/helpers/tmp'
+
+# The ruby_exe helper provides a wrapper for invoking the
+# same Ruby interpreter with the same flags as the one running
+# the specs and getting the output from running the code.
+#
+# If +code+ is a file that exists, it will be run.
+# Otherwise, +code+ will be written to a temporary file and be run.
+# For example:
+#
+# ruby_exe('path/to/some/file.rb')
+#
+# will be executed as
+#
+# `#{RUBY_EXE} 'path/to/some/file.rb'`
+#
+# The ruby_exe helper also accepts an options hash with four
+# keys: :options, :args :env and :exception.
+#
+# For example:
+#
+# ruby_exe('file.rb', :options => "-w",
+# :args => "arg1 arg2",
+# :env => { :FOO => "bar" })
+#
+# will be executed as
+#
+# `#{RUBY_EXE} -w file.rb arg1 arg2`
+#
+# with access to ENV["FOO"] with value "bar".
+#
+# When `exception: false` and Ruby command fails then exception will not be
+# raised.
+#
+# If +nil+ is passed for the first argument, the command line
+# will be built only from the options hash.
+#
+# If no arguments are passed to ruby_exe, it returns an Array
+# containing the interpreter executable and the flags:
+#
+# spawn(*ruby_exe, "-e", "puts :hello")
+#
+# This avoids spawning an extra shell, and ensure the pid returned by spawn
+# corresponds to the ruby process and not the shell.
+#
+# The RUBY_EXE constant is setup by mspec automatically
+# and is used by ruby_exe and ruby_cmd. The mspec runner script
+# will set ENV['RUBY_EXE'] to the name of the executable used
+# to invoke the mspec-run script.
+#
+# The value will only be used if the file exists and is executable.
+# The flags will then be appended to the resulting value, such that
+# the RUBY_EXE constant contains both the executable and the flags.
+#
+# Additionally, the flags passed to mspec
+# (with -T on the command line or in the config with set :flags)
+# will be appended to RUBY_EXE so that the interpreter
+# is always called with those flags.
+#
+# Failure of a Ruby command leads to raising exception by default.
+
+def ruby_exe_options(option)
+ case option
+ when :env
+ ENV['RUBY_EXE']
+ when :engine
+ case RUBY_ENGINE
+ when 'rbx'
+ "bin/rbx"
+ when 'jruby'
+ "bin/jruby"
+ when 'maglev'
+ "maglev-ruby"
+ when 'topaz'
+ "topaz"
+ when 'ironruby'
+ "ir"
+ end
+ when :name
+ require 'rbconfig'
+ bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || '')
+ File.join(".", bin)
+ when :install_name
+ require 'rbconfig'
+ bin = RbConfig::CONFIG["RUBY_INSTALL_NAME"] || RbConfig::CONFIG["ruby_install_name"]
+ bin << (RbConfig::CONFIG['EXEEXT'] || '')
+ File.join(RbConfig::CONFIG['bindir'], bin)
+ end
+end
+
+def resolve_ruby_exe
+ [:env, :engine, :name, :install_name].each do |option|
+ next unless exe = ruby_exe_options(option)
+
+ if File.file?(exe) and File.executable?(exe)
+ exe = File.expand_path(exe)
+ exe = exe.tr('/', '\\') if PlatformGuard.windows?
+ flags = ENV['RUBY_FLAGS']
+ if flags and !flags.empty?
+ return exe + ' ' + flags
+ else
+ return exe
+ end
+ end
+ end
+ raise Exception, "Unable to find a suitable ruby executable."
+end
+
+unless Object.const_defined?(:RUBY_EXE) and RUBY_EXE
+ RUBY_EXE = resolve_ruby_exe
+end
+
+def ruby_exe(code = :not_given, opts = {})
+ skip "WASI doesn't provide subprocess" if PlatformGuard.wasi?
+
+ if opts[:dir]
+ raise "ruby_exe(..., dir: dir) is no longer supported, use Dir.chdir"
+ end
+
+ if code == :not_given
+ return RUBY_EXE.split(' ')
+ end
+
+ env = opts[:env] || {}
+ saved_env = {}
+ env.each do |key, value|
+ key = key.to_s
+ saved_env[key] = ENV[key] if ENV.key? key
+ ENV[key] = value
+ end
+
+ escape = opts.delete(:escape)
+ if code and !File.exist?(code) and escape != false
+ tmpfile = tmp("rubyexe.rb")
+ File.open(tmpfile, "w") { |f| f.write(code) }
+ code = tmpfile
+ end
+
+ expected_status = opts.fetch(:exit_status, 0)
+
+ begin
+ command = ruby_cmd(code, opts)
+
+ # Try to avoid the extra shell for 2>&1
+ # This is notably useful for TimeoutAction which can then signal the ruby subprocess and not the shell
+ popen_options = []
+ if command.end_with?(' 2>&1')
+ command = command[0...-5]
+ popen_options = [{ err: [:child, :out] }]
+ end
+
+ output = IO.popen(command, *popen_options) do |io|
+ pid = io.pid
+ MSpec.subprocesses << pid
+ begin
+ io.read
+ ensure
+ MSpec.subprocesses.delete(pid)
+ end
+ end
+
+ status = Process.last_status
+
+ exit_status = if status.exited?
+ status.exitstatus
+ elsif status.signaled?
+ signame = Signal.signame status.termsig
+ raise "No signal name?" unless signame
+ :"SIG#{signame}"
+ else
+ raise SpecExpectationNotMetError, "#{exit_status.inspect} is neither exited? nor signaled?"
+ end
+ if exit_status != expected_status
+ formatted_output = output.lines.map { |line| " #{line}" }.join
+ raise SpecExpectationNotMetError,
+ "Expected exit status is #{expected_status.inspect} but actual is #{exit_status.inspect} for command ruby_exe(#{command.inspect})\nOutput:\n#{formatted_output}"
+ end
+
+ output
+ ensure
+ saved_env.each { |key, value| ENV[key] = value }
+ env.keys.each do |key|
+ key = key.to_s
+ ENV.delete key unless saved_env.key? key
+ end
+ File.delete tmpfile if tmpfile
+ end
+end
+
+def ruby_cmd(code, opts = {})
+ body = code
+
+ if opts[:escape]
+ raise "escape: true is no longer supported in ruby_cmd, use ruby_exe or a fixture"
+ end
+
+ if code and !File.exist?(code)
+ body = "-e #{code.inspect}"
+ end
+
+ command = [RUBY_EXE, opts[:options], body, opts[:args]].compact.join(' ')
+ STDERR.puts "\nruby_cmd: #{command}" if ENV["DEBUG_MSPEC_RUBY_CMD"] == "true"
+ command
+end
diff --git a/spec/mspec/lib/mspec/helpers/scratch.rb b/spec/mspec/lib/mspec/helpers/scratch.rb
new file mode 100644
index 0000000000..0da3315cd8
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/scratch.rb
@@ -0,0 +1,21 @@
+module ScratchPad
+ def self.clear
+ @record = nil
+ end
+
+ def self.record(arg)
+ @record = arg
+ end
+
+ def self.<<(arg)
+ @record << arg
+ end
+
+ def self.recorded
+ @record
+ end
+
+ def self.inspect
+ "<ScratchPad @record=#{@record.inspect}>"
+ end
+end
diff --git a/spec/mspec/lib/mspec/helpers/tmp.rb b/spec/mspec/lib/mspec/helpers/tmp.rb
new file mode 100644
index 0000000000..e903dd9f50
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/tmp.rb
@@ -0,0 +1,62 @@
+# Creates a temporary directory in the current working directory
+# for temporary files created while running the specs. All specs
+# should clean up any temporary files created so that the temp
+# directory is empty when the process exits.
+
+SPEC_TEMP_DIR_PID = Process.pid
+
+if spec_temp_dir = ENV["SPEC_TEMP_DIR"]
+ spec_temp_dir = File.realdirpath(spec_temp_dir)
+else
+ spec_temp_dir = "#{File.realpath(Dir.pwd)}/rubyspec_temp/#{SPEC_TEMP_DIR_PID}"
+end
+SPEC_TEMP_DIR = spec_temp_dir
+
+SPEC_TEMP_UNIQUIFIER = +"0"
+
+at_exit do
+ begin
+ if SPEC_TEMP_DIR_PID == Process.pid
+ Dir.delete SPEC_TEMP_DIR if File.directory? SPEC_TEMP_DIR
+ end
+ rescue SystemCallError
+ STDERR.puts <<-EOM
+
+-----------------------------------------------------
+The rubyspec temp directory is not empty. Ensure that
+all specs are cleaning up temporary files:
+ #{SPEC_TEMP_DIR}
+-----------------------------------------------------
+
+ EOM
+ rescue Object => e
+ STDERR.puts "failed to remove spec temp directory"
+ STDERR.puts e.message
+ end
+end
+
+def tmp(name, uniquify = true)
+ if Dir.exist? SPEC_TEMP_DIR
+ stat = File.stat(SPEC_TEMP_DIR)
+ if stat.world_writable? and !stat.sticky?
+ raise ArgumentError, "SPEC_TEMP_DIR (#{SPEC_TEMP_DIR}) is world writable but not sticky"
+ end
+ else
+ platform_is_not :windows do
+ umask = File.umask
+ if (umask & 0002) == 0 # o+w
+ raise ArgumentError, "File.umask #=> #{umask.to_s(8)} (world-writable)"
+ end
+ end
+ mkdir_p SPEC_TEMP_DIR
+ end
+
+ if uniquify and !name.empty?
+ slash = name.rindex "/"
+ index = slash ? slash + 1 : 0
+ name = +name
+ name.insert index, "#{SPEC_TEMP_UNIQUIFIER.succ!}-"
+ end
+
+ File.join SPEC_TEMP_DIR, name
+end
diff --git a/spec/mspec/lib/mspec/helpers/warning.rb b/spec/mspec/lib/mspec/helpers/warning.rb
new file mode 100644
index 0000000000..e3d72b78bd
--- /dev/null
+++ b/spec/mspec/lib/mspec/helpers/warning.rb
@@ -0,0 +1,21 @@
+require 'mspec/guards/version'
+
+# You might be looking for #silence_warnings, use #suppress_warning instead.
+# MSpec calls it #suppress_warning for consistency with EnvUtil.suppress_warning in CRuby test/.
+def suppress_warning
+ verbose = $VERBOSE
+ $VERBOSE = nil
+ yield
+ensure
+ $VERBOSE = verbose
+end
+
+if ruby_version_is("2.7")
+ def suppress_keyword_warning(&block)
+ suppress_warning(&block)
+ end
+else
+ def suppress_keyword_warning
+ yield
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers.rb b/spec/mspec/lib/mspec/matchers.rb
new file mode 100644
index 0000000000..356e4a9f32
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers.rb
@@ -0,0 +1,37 @@
+require 'mspec/matchers/base'
+require 'mspec/matchers/be_an_instance_of'
+require 'mspec/matchers/be_ancestor_of'
+require 'mspec/matchers/be_close'
+require 'mspec/matchers/be_computed_by'
+require 'mspec/matchers/be_empty'
+require 'mspec/matchers/be_false'
+require 'mspec/matchers/be_kind_of'
+require 'mspec/matchers/be_nan'
+require 'mspec/matchers/be_nil'
+require 'mspec/matchers/be_true'
+require 'mspec/matchers/be_true_or_false'
+require 'mspec/matchers/complain'
+require 'mspec/matchers/eql'
+require 'mspec/matchers/equal'
+require 'mspec/matchers/equal_element'
+require 'mspec/matchers/have_constant'
+require 'mspec/matchers/have_class_variable'
+require 'mspec/matchers/have_instance_method'
+require 'mspec/matchers/have_instance_variable'
+require 'mspec/matchers/have_method'
+require 'mspec/matchers/have_private_instance_method'
+require 'mspec/matchers/have_private_method'
+require 'mspec/matchers/have_protected_instance_method'
+require 'mspec/matchers/have_public_instance_method'
+require 'mspec/matchers/have_singleton_method'
+require 'mspec/matchers/include'
+require 'mspec/matchers/include_any_of'
+require 'mspec/matchers/infinity'
+require 'mspec/matchers/match_yaml'
+require 'mspec/matchers/raise_error'
+require 'mspec/matchers/output'
+require 'mspec/matchers/output_to_fd'
+require 'mspec/matchers/respond_to'
+require 'mspec/matchers/signed_zero'
+require 'mspec/matchers/block_caller'
+require 'mspec/matchers/skip'
diff --git a/spec/mspec/lib/mspec/matchers/base.rb b/spec/mspec/lib/mspec/matchers/base.rb
new file mode 100644
index 0000000000..3534520d88
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/base.rb
@@ -0,0 +1,95 @@
+module MSpecMatchers
+end
+
+class MSpecEnv
+ include MSpecMatchers
+end
+
+# Expectations are sometimes used in a module body
+class Module
+ include MSpecMatchers
+end
+
+class SpecPositiveOperatorMatcher < BasicObject
+ def initialize(actual)
+ @actual = actual
+ end
+
+ def ==(expected)
+ result = @actual == expected
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be truthy")
+ end
+ end
+
+ def !=(expected)
+ result = @actual != expected
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be truthy")
+ end
+ end
+
+ def equal?(expected)
+ result = @actual.equal?(expected)
+ unless result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be truthy")
+ end
+ end
+
+ def raise(exception = ::Exception, message = nil, options = nil, &block)
+ matcher = ::RaiseErrorMatcher.new(exception, message, options, &block)
+ unless matcher.matches? @actual
+ expected, actual = matcher.failure_message
+ ::SpecExpectation.fail_with(expected, actual)
+ end
+ end
+
+ def method_missing(name, *args, &block)
+ result = @actual.__send__(name, *args, &block)
+ unless result
+ ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be truthy")
+ end
+ end
+end
+
+class SpecNegativeOperatorMatcher < BasicObject
+ def initialize(actual)
+ @actual = actual
+ end
+
+ def ==(expected)
+ result = @actual == expected
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :==, expected, result, "to be falsy")
+ end
+ end
+
+ def !=(expected)
+ result = @actual != expected
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :!=, expected, result, "to be falsy")
+ end
+ end
+
+ def equal?(expected)
+ result = @actual.equal?(expected)
+ if result
+ ::SpecExpectation.fail_single_arg_predicate(@actual, :equal?, expected, result, "to be falsy")
+ end
+ end
+
+ def raise(exception = ::Exception, message = nil, options = nil, &block)
+ matcher = ::RaiseErrorMatcher.new(exception, message, options, &block)
+ if matcher.matches? @actual
+ expected, actual = matcher.negative_failure_message
+ ::SpecExpectation.fail_with(expected, actual)
+ end
+ end
+
+ def method_missing(name, *args, &block)
+ result = @actual.__send__(name, *args, &block)
+ if result
+ ::SpecExpectation.fail_predicate(@actual, name, args, block, result, "to be falsy")
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb
new file mode 100644
index 0000000000..fdf3736ac2
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_an_instance_of.rb
@@ -0,0 +1,26 @@
+class BeAnInstanceOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.instance_of?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})",
+ "to be an instance of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})",
+ "not to be an instance of #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_an_instance_of(expected)
+ BeAnInstanceOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb
new file mode 100644
index 0000000000..05f72099e4
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_ancestor_of.rb
@@ -0,0 +1,24 @@
+class BeAncestorOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.ancestors.include? @actual
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be an ancestor of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be an ancestor of #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_ancestor_of(expected)
+ BeAncestorOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_close.rb b/spec/mspec/lib/mspec/matchers/be_close.rb
new file mode 100644
index 0000000000..d6a6626f31
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_close.rb
@@ -0,0 +1,29 @@
+TOLERANCE = 0.00003 unless Object.const_defined?(:TOLERANCE)
+# To account for GC, context switches, other processes, load, etc.
+TIME_TOLERANCE = 20.0 unless Object.const_defined?(:TIME_TOLERANCE)
+
+class BeCloseMatcher
+ def initialize(expected, tolerance)
+ @expected = expected
+ @tolerance = tolerance
+ end
+
+ def matches?(actual)
+ @actual = actual
+ (@actual - @expected).abs <= @tolerance
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be within #{@expected} +/- #{@tolerance}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be within #{@expected} +/- #{@tolerance}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_close(expected, tolerance)
+ BeCloseMatcher.new(expected, tolerance)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_computed_by.rb b/spec/mspec/lib/mspec/matchers/be_computed_by.rb
new file mode 100644
index 0000000000..2e31bc93af
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_computed_by.rb
@@ -0,0 +1,37 @@
+class BeComputedByMatcher
+ def initialize(sym, *args)
+ @method = sym
+ @args = args
+ end
+
+ def matches?(array)
+ array.each do |line|
+ @receiver = line.shift
+ @value = line.pop
+ @arguments = line
+ @arguments += @args
+ @actual = @receiver.send(@method, *@arguments)
+ return false unless @actual == @value
+ end
+
+ return true
+ end
+
+ def method_call
+ method_call = "#{@receiver.inspect}.#{@method}"
+ unless @arguments.empty?
+ method_call = "#{method_call} from #{@arguments.map { |x| x.inspect }.join(", ")}"
+ end
+ method_call
+ end
+
+ def failure_message
+ ["Expected #{@value.inspect}", "to be computed by #{method_call} (computed #{@actual.inspect} instead)"]
+ end
+end
+
+module MSpecMatchers
+ private def be_computed_by(sym, *args)
+ BeComputedByMatcher.new(sym, *args)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_empty.rb b/spec/mspec/lib/mspec/matchers/be_empty.rb
new file mode 100644
index 0000000000..5abd5c9485
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_empty.rb
@@ -0,0 +1,20 @@
+class BeEmptyMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.empty?
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be empty"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be empty"]
+ end
+end
+
+module MSpecMatchers
+ private def be_empty
+ BeEmptyMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_false.rb b/spec/mspec/lib/mspec/matchers/be_false.rb
new file mode 100644
index 0000000000..9e9a2608e1
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_false.rb
@@ -0,0 +1,20 @@
+class BeFalseMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be false"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be false"]
+ end
+end
+
+module MSpecMatchers
+ private def be_false
+ BeFalseMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_kind_of.rb b/spec/mspec/lib/mspec/matchers/be_kind_of.rb
new file mode 100644
index 0000000000..a69906f210
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_kind_of.rb
@@ -0,0 +1,24 @@
+class BeKindOfMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.is_a?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "to be kind of #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "not to be kind of #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def be_kind_of(expected)
+ BeKindOfMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_nan.rb b/spec/mspec/lib/mspec/matchers/be_nan.rb
new file mode 100644
index 0000000000..b279d8f1cf
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_nan.rb
@@ -0,0 +1,20 @@
+class BeNaNMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.kind_of?(Float) && @actual.nan?
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be NaN"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be NaN"]
+ end
+end
+
+module MSpecMatchers
+ private def be_nan
+ BeNaNMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_nil.rb b/spec/mspec/lib/mspec/matchers/be_nil.rb
new file mode 100644
index 0000000000..049b1e3a53
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_nil.rb
@@ -0,0 +1,20 @@
+class BeNilMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual.nil?
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be nil"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be nil"]
+ end
+end
+
+module MSpecMatchers
+ private def be_nil
+ BeNilMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_true.rb b/spec/mspec/lib/mspec/matchers/be_true.rb
new file mode 100644
index 0000000000..52f5013752
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_true.rb
@@ -0,0 +1,20 @@
+class BeTrueMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == true
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be true"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be true"]
+ end
+end
+
+module MSpecMatchers
+ private def be_true
+ BeTrueMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/be_true_or_false.rb b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb
new file mode 100644
index 0000000000..4294b08d1b
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/be_true_or_false.rb
@@ -0,0 +1,20 @@
+class BeTrueOrFalseMatcher
+ def matches?(actual)
+ @actual = actual
+ @actual == true || @actual == false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to be true or false"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to be true or false"]
+ end
+end
+
+module MSpecMatchers
+ private def be_true_or_false
+ BeTrueOrFalseMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/block_caller.rb b/spec/mspec/lib/mspec/matchers/block_caller.rb
new file mode 100644
index 0000000000..30fab4fc68
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/block_caller.rb
@@ -0,0 +1,37 @@
+class BlockingMatcher
+ def matches?(block)
+ t = Thread.new do
+ block.call
+ end
+
+ loop do
+ case t.status
+ when "sleep" # blocked
+ t.kill
+ t.join
+ return true
+ when false # terminated normally, so never blocked
+ t.join
+ return false
+ when nil # terminated exceptionally
+ t.value
+ else
+ Thread.pass
+ end
+ end
+ end
+
+ def failure_message
+ ['Expected the given Proc', 'to block the caller']
+ end
+
+ def negative_failure_message
+ ['Expected the given Proc', 'to not block the caller']
+ end
+end
+
+module MSpecMatchers
+ private def block_caller
+ BlockingMatcher.new
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/complain.rb b/spec/mspec/lib/mspec/matchers/complain.rb
new file mode 100644
index 0000000000..19310c0bbb
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/complain.rb
@@ -0,0 +1,69 @@
+require 'mspec/helpers/io'
+
+class ComplainMatcher
+ def initialize(complaint = nil, options = nil)
+ # the proper solution is to use double splat operator e.g.
+ # def initialize(complaint = nil, **options)
+ # but we are trying to minimize language features required to run MSpec
+ if complaint.is_a?(Hash)
+ @complaint = nil
+ @options = complaint
+ else
+ @complaint = complaint
+ @options = options || {}
+ end
+ end
+
+ def matches?(proc)
+ @saved_err = $stderr
+ @verbose = $VERBOSE
+ err = IOStub.new
+
+ $stderr = err
+ $VERBOSE = @options.key?(:verbose) ? @options[:verbose] : false
+ begin
+ proc.call
+ ensure
+ $VERBOSE = @verbose
+ $stderr = @saved_err
+ end
+
+ @warning = err.to_s
+ unless @complaint.nil?
+ case @complaint
+ when Regexp
+ return false unless @warning =~ @complaint
+ else
+ return false unless @warning == @complaint
+ end
+ end
+
+ return @warning.empty? ? false : true
+ end
+
+ def failure_message
+ if @complaint.nil?
+ ["Expected a warning", "but received none"]
+ elsif @complaint.kind_of? Regexp
+ ["Expected warning to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ else
+ ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ end
+ end
+
+ def negative_failure_message
+ if @complaint.nil?
+ ["Unexpected warning: ", @warning.chomp.inspect]
+ elsif @complaint.kind_of? Regexp
+ ["Expected warning not to match: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ else
+ ["Expected warning: #{@complaint.inspect}", "but got: #{@warning.chomp.inspect}"]
+ end
+ end
+end
+
+module MSpecMatchers
+ private def complain(complaint = nil, options = nil)
+ ComplainMatcher.new(complaint, options)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/eql.rb b/spec/mspec/lib/mspec/matchers/eql.rb
new file mode 100644
index 0000000000..bcab88ebee
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/eql.rb
@@ -0,0 +1,26 @@
+class EqlMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.eql?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "to have same value and type as #{MSpec.format(@expected)}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "not to have same value or type as #{MSpec.format(@expected)}"]
+ end
+end
+
+module MSpecMatchers
+ private def eql(expected)
+ EqlMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/equal.rb b/spec/mspec/lib/mspec/matchers/equal.rb
new file mode 100644
index 0000000000..5ba4856d82
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/equal.rb
@@ -0,0 +1,26 @@
+class EqualMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.equal?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "to be identical to #{MSpec.format(@expected)}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "not to be identical to #{MSpec.format(@expected)}"]
+ end
+end
+
+module MSpecMatchers
+ private def equal(expected)
+ EqualMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/equal_element.rb b/spec/mspec/lib/mspec/matchers/equal_element.rb
new file mode 100644
index 0000000000..8da2567fcf
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/equal_element.rb
@@ -0,0 +1,78 @@
+class EqualElementMatcher
+ def initialize(element, attributes = nil, content = nil, options = {})
+ @element = element
+ @attributes = attributes
+ @content = content
+ @options = options
+ end
+
+ def matches?(actual)
+ @actual = actual
+
+ matched = true
+
+ if @options[:not_closed]
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}.*#{Regexp.quote(">" + (@content || ''))}$/
+ else
+ matched &&= actual =~ /^#{Regexp.quote("<" + @element)}/
+ matched &&= actual =~ /#{Regexp.quote("</" + @element + ">")}$/
+ matched &&= actual =~ /#{Regexp.quote(">" + @content + "</")}/ if @content
+ end
+
+ if @attributes
+ if @attributes.empty?
+ matched &&= actual.scan(/\w+\=\"(.*)\"/).size == 0
+ else
+ @attributes.each do |key, value|
+ if value == true
+ matched &&= (actual.scan(/#{Regexp.quote(key)}(\s|>)/).size == 1)
+ else
+ matched &&= (actual.scan(%Q{ #{key}="#{value}"}).size == 1)
+ end
+ end
+ end
+ end
+
+ !!matched
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}",
+ "not to be a '#{@element}' element with #{attributes_for_failure_message} and #{content_for_failure_message}"]
+ end
+
+ def attributes_for_failure_message
+ if @attributes
+ if @attributes.empty?
+ "no attributes"
+ else
+ @attributes.inject([]) { |memo, n| memo << %Q{#{n[0]}="#{n[1]}"} }.join(" ")
+ end
+ else
+ "any attributes"
+ end
+ end
+
+ def content_for_failure_message
+ if @content
+ if @content.empty?
+ "no content"
+ else
+ "#{@content.inspect} as content"
+ end
+ else
+ "any content"
+ end
+ end
+end
+
+module MSpecMatchers
+ private def equal_element(*args)
+ EqualElementMatcher.new(*args)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_class_variable.rb b/spec/mspec/lib/mspec/matchers/have_class_variable.rb
new file mode 100644
index 0000000000..dd43ced621
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_class_variable.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveClassVariableMatcher < VariableMatcher
+ self.variables_method = :class_variables
+ self.description = 'class variable'
+end
+
+module MSpecMatchers
+ private def have_class_variable(variable)
+ HaveClassVariableMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_constant.rb b/spec/mspec/lib/mspec/matchers/have_constant.rb
new file mode 100644
index 0000000000..6ec7c75b85
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_constant.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveConstantMatcher < VariableMatcher
+ self.variables_method = :constants
+ self.description = 'constant'
+end
+
+module MSpecMatchers
+ private def have_constant(variable)
+ HaveConstantMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
new file mode 100644
index 0000000000..9a5a31aa0f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_instance_method(method, include_super = true)
+ HaveInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_instance_variable.rb b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb
new file mode 100644
index 0000000000..de51b3209d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_instance_variable.rb
@@ -0,0 +1,12 @@
+require 'mspec/matchers/variable'
+
+class HaveInstanceVariableMatcher < VariableMatcher
+ self.variables_method = :instance_variables
+ self.description = 'instance variable'
+end
+
+module MSpecMatchers
+ private def have_instance_variable(variable)
+ HaveInstanceVariableMatcher.new(variable)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_method.rb b/spec/mspec/lib/mspec/matchers/have_method.rb
new file mode 100644
index 0000000000..e962e69e0a
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ @mod.methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_method(method, include_super = true)
+ HaveMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
new file mode 100644
index 0000000000..d32db76c6a
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_private_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePrivateInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.private_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have private instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have private instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_private_instance_method(method, include_super = true)
+ HavePrivateInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_private_method.rb b/spec/mspec/lib/mspec/matchers/have_private_method.rb
new file mode 100644
index 0000000000..c74165cfc7
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_private_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePrivateMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.private_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have private method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have private method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_private_method(method, include_super = true)
+ HavePrivateMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
new file mode 100644
index 0000000000..1deb2f995d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_protected_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveProtectedInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.protected_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have protected instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have protected instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_protected_instance_method(method, include_super = true)
+ HaveProtectedInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
new file mode 100644
index 0000000000..0e620532c0
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_public_instance_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HavePublicInstanceMethodMatcher < MethodMatcher
+ def matches?(mod)
+ @mod = mod
+ mod.public_instance_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@mod} to have public instance method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@mod} NOT to have public instance method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_public_instance_method(method, include_super = true)
+ HavePublicInstanceMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/have_singleton_method.rb b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
new file mode 100644
index 0000000000..b60dd2536b
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/have_singleton_method.rb
@@ -0,0 +1,24 @@
+require 'mspec/matchers/method'
+
+class HaveSingletonMethodMatcher < MethodMatcher
+ def matches?(obj)
+ @obj = obj
+ obj.singleton_methods(@include_super).include? @method
+ end
+
+ def failure_message
+ ["Expected #{@obj} to have singleton method '#{@method.to_s}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@obj} NOT to have singleton method '#{@method.to_s}'",
+ "but it does"]
+ end
+end
+
+module MSpecMatchers
+ private def have_singleton_method(method, include_super = true)
+ HaveSingletonMethodMatcher.new method, include_super
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/include.rb b/spec/mspec/lib/mspec/matchers/include.rb
new file mode 100644
index 0000000000..3f07f35548
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/include.rb
@@ -0,0 +1,31 @@
+class IncludeMatcher
+ def initialize(*expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.each do |e|
+ @element = e
+ unless @actual.include?(e)
+ return false
+ end
+ end
+ return true
+ end
+
+ def failure_message
+ ["Expected #{MSpec.format(@actual)}", "to include #{MSpec.format(@element)}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{MSpec.format(@actual)}", "not to include #{MSpec.format(@element)}"]
+ end
+end
+
+# Cannot override #include at the toplevel in MRI
+module MSpecMatchers
+ private def include(*expected)
+ IncludeMatcher.new(*expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/include_any_of.rb b/spec/mspec/lib/mspec/matchers/include_any_of.rb
new file mode 100644
index 0000000000..ce097ccf0f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/include_any_of.rb
@@ -0,0 +1,29 @@
+class IncludeAnyOfMatcher
+ def initialize(*expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @expected.each do |e|
+ if @actual.include?(e)
+ return true
+ end
+ end
+ return false
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", "to include any of #{@expected.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", "not to include any of #{@expected.inspect}"]
+ end
+end
+
+module MSpecMatchers
+ private def include_any_of(*expected)
+ IncludeAnyOfMatcher.new(*expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/infinity.rb b/spec/mspec/lib/mspec/matchers/infinity.rb
new file mode 100644
index 0000000000..8bfa6dbd10
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/infinity.rb
@@ -0,0 +1,28 @@
+class InfinityMatcher
+ def initialize(expected_sign)
+ @expected_sign = expected_sign
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.kind_of?(Float) && @actual.infinite? == @expected_sign
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}Infinity"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}Infinity"]
+ end
+end
+
+module MSpecMatchers
+ private def be_positive_infinity
+ InfinityMatcher.new(1)
+ end
+
+ private def be_negative_infinity
+ InfinityMatcher.new(-1)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/match_yaml.rb b/spec/mspec/lib/mspec/matchers/match_yaml.rb
new file mode 100644
index 0000000000..30561627c3
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/match_yaml.rb
@@ -0,0 +1,50 @@
+class MatchYAMLMatcher
+
+ def initialize(expected)
+ if valid_yaml?(expected)
+ @expected = expected
+ else
+ @expected = expected.to_yaml
+ end
+ end
+
+ def matches?(actual)
+ @actual = actual
+ clean_yaml(@actual) == clean_yaml(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect}", " to match #{@expected.inspect}"]
+ end
+
+ protected
+
+ def clean_yaml(yaml)
+ yaml.gsub(/([^-]|^---)\s+\n/, "\\1\n").sub(/\n\.\.\.\n$/, "\n")
+ end
+
+ def valid_yaml?(obj)
+ require 'yaml'
+ begin
+ if YAML.respond_to?(:unsafe_load)
+ YAML.unsafe_load(obj)
+ else
+ YAML.load(obj)
+ end
+ rescue
+ false
+ else
+ true
+ end
+ end
+end
+
+module MSpecMatchers
+ private def match_yaml(expected)
+ MatchYAMLMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/method.rb b/spec/mspec/lib/mspec/matchers/method.rb
new file mode 100644
index 0000000000..2b54419faa
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/method.rb
@@ -0,0 +1,10 @@
+class MethodMatcher
+ def initialize(method, include_super = true)
+ @include_super = include_super
+ @method = method.to_sym
+ end
+
+ def matches?(mod)
+ raise Exception, "define #matches? in the subclass"
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/output.rb b/spec/mspec/lib/mspec/matchers/output.rb
new file mode 100644
index 0000000000..5bb5d55027
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/output.rb
@@ -0,0 +1,67 @@
+require 'mspec/helpers/io'
+
+class OutputMatcher
+ def initialize(stdout, stderr)
+ @out = stdout
+ @err = stderr
+ end
+
+ def matches?(proc)
+ @saved_out = $stdout
+ @saved_err = $stderr
+ @stdout = $stdout = IOStub.new
+ @stderr = $stderr = IOStub.new
+
+ proc.call
+
+ unless @out.nil?
+ case @out
+ when Regexp
+ return false unless $stdout =~ @out
+ else
+ return false unless $stdout == @out
+ end
+ end
+
+ unless @err.nil?
+ case @err
+ when Regexp
+ return false unless $stderr =~ @err
+ else
+ return false unless $stderr == @err
+ end
+ end
+
+ return true
+ ensure
+ $stdout = @saved_out
+ $stderr = @saved_err
+ end
+
+ def failure_message
+ expected_out = "\n"
+ actual_out = "\n"
+ unless @out.nil?
+ expected_out += " $stdout: #{MSpec.format(@out)}\n"
+ actual_out += " $stdout: #{MSpec.format(@stdout.to_s)}\n"
+ end
+ unless @err.nil?
+ expected_out += " $stderr: #{MSpec.format(@err)}\n"
+ actual_out += " $stderr: #{MSpec.format(@stderr.to_s)}\n"
+ end
+ ["Expected:#{expected_out}", " got:#{actual_out}"]
+ end
+
+ def negative_failure_message
+ out = ""
+ out += " $stdout: #{@stdout.chomp.dump}\n" unless @out.nil?
+ out += " $stderr: #{@stderr.chomp.dump}\n" unless @err.nil?
+ ["Expected output not to be:\n", out]
+ end
+end
+
+module MSpecMatchers
+ private def output(stdout = nil, stderr = nil)
+ OutputMatcher.new(stdout, stderr)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/output_to_fd.rb b/spec/mspec/lib/mspec/matchers/output_to_fd.rb
new file mode 100644
index 0000000000..f4d7b4ea1f
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/output_to_fd.rb
@@ -0,0 +1,71 @@
+require 'mspec/helpers/tmp'
+
+# Lower-level output speccing mechanism for a single
+# output stream. Unlike OutputMatcher which provides
+# methods to capture the output, we actually replace
+# the FD itself so that there is no reliance on a
+# certain method being used.
+class OutputToFDMatcher
+ def initialize(expected, to)
+ @to, @expected = to, expected
+
+ case @to
+ when STDOUT
+ @to_name = "STDOUT"
+ when STDERR
+ @to_name = "STDERR"
+ when IO
+ @to_name = @to.object_id.to_s
+ else
+ raise ArgumentError, "#{@to.inspect} is not a supported output target"
+ end
+ end
+
+ def with_tmp
+ path = tmp("mspec_output_to_#{$$}_#{Time.now.to_i}")
+ File.open(path, 'w+') { |io|
+ yield(io)
+ }
+ ensure
+ File.delete path if path
+ end
+
+ def matches?(block)
+ old_to = @to.dup
+ with_tmp do |out|
+ # Replacing with a file handle so that Readline etc. work
+ @to.reopen out
+ begin
+ block.call
+ ensure
+ @to.reopen old_to
+ old_to.close
+ end
+
+ out.rewind
+ @actual = out.read
+
+ case @expected
+ when Regexp
+ !(@actual =~ @expected).nil?
+ else
+ @actual == @expected
+ end
+ end
+ end
+
+ def failure_message()
+ ["Expected (#{@to_name}): #{@expected.inspect}\n",
+ "#{'but got'.rjust(@to_name.length + 10)}: #{@actual.inspect}\nBacktrace"]
+ end
+
+ def negative_failure_message()
+ ["Expected output (#{@to_name}) to NOT be:\n", @actual.inspect]
+ end
+end
+
+module MSpecMatchers
+ private def output_to_fd(what, where = STDOUT)
+ OutputToFDMatcher.new what, where
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/raise_error.rb b/spec/mspec/lib/mspec/matchers/raise_error.rb
new file mode 100644
index 0000000000..8cba842ce3
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/raise_error.rb
@@ -0,0 +1,132 @@
+class RaiseErrorMatcher
+ FAILURE_MESSAGE_FOR_EXCEPTION = {}.compare_by_identity
+ UNDEF_CAUSE = Object.new
+
+ attr_writer :block
+
+ def initialize(exception, message = nil, options = nil, &block)
+ if message.is_a? Hash
+ @message = nil
+ options = message
+ else
+ @message = message
+ end
+ @cause = options ? options.fetch(:cause, UNDEF_CAUSE) : UNDEF_CAUSE
+ @exception = exception
+ @block = block
+ @actual = nil
+ end
+
+ # This #matches? method is unusual because it doesn't always return a boolean but instead
+ # re-raises the original exception if proc.call raises an exception and #matching_exception? is false.
+ # The reasoning is the original exception class matters and we don't want to change it by raising another exception,
+ # so instead we attach the #failure_message and extract it in ExceptionState#message.
+ def matches?(proc)
+ @result = proc.call
+ return false
+ rescue Object => actual
+ @actual = actual
+
+ if matching_exception?(actual)
+ # The block has its own expectations and will throw an exception if it fails
+ @block[actual] if @block
+ return true
+ else
+ FAILURE_MESSAGE_FOR_EXCEPTION[actual] = failure_message
+ raise actual
+ end
+ end
+
+ def matching_class?(exc)
+ @exception === exc
+ end
+
+ def matching_message?(exc)
+ case @message
+ when String
+ @message == exc.message
+ when Regexp
+ @message =~ exc.message
+ else
+ true
+ end
+ end
+
+ def matching_cause?(exc)
+ case @cause
+ when UNDEF_CAUSE
+ true
+ else
+ @cause == exc.cause
+ end
+ end
+
+ def matching_exception?(exc)
+ matching_class?(exc) and matching_message?(exc) and matching_cause?(exc)
+ end
+
+ def exception_class_and_message_and_cause(exception_class, message, cause)
+ string = "#{exception_class}"
+ prefixed = false
+ prefix = -> { prefixed ? ", " : prefixed = "(" }
+
+ if message != nil
+ string << "#{prefix.()}#{message.inspect}"
+ end
+
+ if cause != UNDEF_CAUSE
+ string << "#{prefix.()}cause: #{cause.inspect}"
+ end
+
+ string << ")" if prefixed
+
+ string
+ end
+
+ def format_expected_exception
+ exception_class_and_message_and_cause(@exception, @message, @cause)
+ end
+
+ def format_exception(exception)
+ exception_class_and_message_and_cause(exception.class,
+ @message == nil ? nil : exception.message,
+ @cause == UNDEF_CAUSE ? UNDEF_CAUSE : exception.cause)
+ end
+
+ def failure_message
+ message = ["Expected #{format_expected_exception}"]
+
+ if @actual
+ message << "but got: #{format_exception(@actual)}"
+ else
+ message << "but no exception was raised (#{MSpec.format(@result)} was returned)"
+ end
+
+ message
+ end
+
+ def negative_failure_message
+ message = ["Expected to not get #{format_expected_exception}", ""]
+ unless @actual.class == @exception
+ message[1] = "but got: #{format_exception(@actual)}"
+ end
+ message
+ end
+end
+
+module MSpecMatchers
+ private def raise_error(exception = Exception, message = nil, options = nil, &block)
+ RaiseErrorMatcher.new(exception, message, options, &block)
+ end
+
+ # CRuby < 4.1 has inconsistent coercion errors:
+ # https://bugs.ruby-lang.org/issues/21864
+ # This matcher ignores the message on CRuby < 4.1
+ # and checks the message for all other cases, including other Rubies
+ private def raise_consistent_error(exception = Exception, message = nil, options = nil, &block)
+ if RUBY_ENGINE == "ruby" and ruby_version_is ""..."4.1"
+ message = nil
+ end
+ RaiseErrorMatcher.new(exception, message, options, &block)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/respond_to.rb b/spec/mspec/lib/mspec/matchers/respond_to.rb
new file mode 100644
index 0000000000..6b35ae2d3c
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/respond_to.rb
@@ -0,0 +1,24 @@
+class RespondToMatcher
+ def initialize(expected)
+ @expected = expected
+ end
+
+ def matches?(actual)
+ @actual = actual
+ @actual.respond_to?(@expected)
+ end
+
+ def failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "to respond to #{@expected}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual.inspect} (#{@actual.class})", "not to respond to #{@expected}"]
+ end
+end
+
+module MSpecMatchers
+ private def respond_to(expected)
+ RespondToMatcher.new(expected)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/signed_zero.rb b/spec/mspec/lib/mspec/matchers/signed_zero.rb
new file mode 100644
index 0000000000..2ff90f4994
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/signed_zero.rb
@@ -0,0 +1,28 @@
+class SignedZeroMatcher
+ def initialize(expected_sign)
+ @expected_sign = expected_sign
+ end
+
+ def matches?(actual)
+ @actual = actual
+ (1.0/actual).infinite? == @expected_sign
+ end
+
+ def failure_message
+ ["Expected #{@actual}", "to be #{"-" if @expected_sign == -1}0.0"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual}", "not to be #{"-" if @expected_sign == -1}0.0"]
+ end
+end
+
+module MSpecMatchers
+ private def be_positive_zero
+ SignedZeroMatcher.new(1)
+ end
+
+ private def be_negative_zero
+ SignedZeroMatcher.new(-1)
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/skip.rb b/spec/mspec/lib/mspec/matchers/skip.rb
new file mode 100644
index 0000000000..7c175d358d
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/skip.rb
@@ -0,0 +1,5 @@
+module MSpecMatchers
+ private def skip(reason = 'no reason')
+ raise SkippedSpecError, reason
+ end
+end
diff --git a/spec/mspec/lib/mspec/matchers/variable.rb b/spec/mspec/lib/mspec/matchers/variable.rb
new file mode 100644
index 0000000000..4d801ea337
--- /dev/null
+++ b/spec/mspec/lib/mspec/matchers/variable.rb
@@ -0,0 +1,24 @@
+class VariableMatcher
+ class << self
+ attr_accessor :variables_method, :description
+ end
+
+ def initialize(variable)
+ @variable = variable.to_sym
+ end
+
+ def matches?(object)
+ @object = object
+ @object.send(self.class.variables_method).include? @variable
+ end
+
+ def failure_message
+ ["Expected #{@object} to have #{self.class.description} '#{@variable}'",
+ "but it does not"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@object} NOT to have #{self.class.description} '#{@variable}'",
+ "but it does"]
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks.rb b/spec/mspec/lib/mspec/mocks.rb
new file mode 100644
index 0000000000..6a029c7b53
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks.rb
@@ -0,0 +1,3 @@
+require 'mspec/mocks/mock'
+require 'mspec/mocks/proxy'
+require 'mspec/mocks/object'
diff --git a/spec/mspec/lib/mspec/mocks/mock.rb b/spec/mspec/lib/mspec/mocks/mock.rb
new file mode 100644
index 0000000000..c61ba35ea7
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/mock.rb
@@ -0,0 +1,209 @@
+require 'mspec/expectations/expectations'
+require 'mspec/helpers/warning'
+
+module Mock
+ def self.reset
+ @mocks = @stubs = @objects = nil
+ end
+
+ def self.objects
+ @objects ||= {}
+ end
+
+ def self.mocks
+ @mocks ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.stubs
+ @stubs ||= Hash.new { |h,k| h[k] = [] }
+ end
+
+ def self.replaced_name(key)
+ :"__mspec_#{key.last}__"
+ end
+
+ def self.replaced_key(obj, sym)
+ [obj.__id__, sym]
+ end
+
+ def self.replaced?(key)
+ mocks.include?(key) or stubs.include?(key)
+ end
+
+ def self.clear_replaced(key)
+ mocks.delete key
+ stubs.delete key
+ end
+
+ def self.mock_respond_to?(obj, sym, include_private = false)
+ key = replaced_key(obj, :respond_to?)
+ if replaced? key
+ name = replaced_name(key)
+ obj.__send__ name, sym, include_private
+ else
+ obj.respond_to? sym, include_private
+ end
+ end
+
+ def self.install_method(obj, sym, type = nil)
+ meta = obj.singleton_class
+
+ key = replaced_key obj, sym
+ sym = sym.to_sym
+
+ if type == :stub and mocks.key?(key)
+ # Defining a stub and there is already a mock, ignore the stub
+ return
+ end
+
+ if (sym == :respond_to? or mock_respond_to?(obj, sym, true)) and !replaced?(key)
+ meta.__send__ :alias_method, replaced_name(key), sym
+ end
+
+ suppress_warning {
+ meta.class_eval {
+ define_method(sym) do |*args, &block|
+ Mock.verify_call self, sym, *args, &block
+ end
+ }
+ }
+
+ proxy = MockProxy.new type
+
+ if proxy.mock?
+ MSpec.expectation
+ MSpec.actions :expectation, MSpec.current.state
+ end
+
+ if proxy.mock? and stubs.key?(key)
+ # Defining a mock and there is already a stub, remove the stub
+ stubs.delete key
+ end
+
+ if proxy.stub?
+ stubs[key].unshift proxy
+ else
+ mocks[key] << proxy
+ end
+ objects[key] = obj
+
+ proxy
+ end
+
+ def self.name_or_inspect(obj)
+ obj.instance_variable_get(:@name) || obj.inspect
+ end
+
+ def self.inspect_args(args)
+ "(#{Array(args).map(&:inspect).join(', ')})"
+ end
+
+ def self.verify_count
+ mocks.each do |key, proxies|
+ obj = objects[key]
+ proxies.each do |proxy|
+ qualifier, count = proxy.count
+ pass = case qualifier
+ when :at_least
+ proxy.calls >= count
+ when :at_most
+ proxy.calls <= count
+ when :exactly
+ proxy.calls == count
+ when :any_number_of_times
+ true
+ else
+ false
+ end
+ unless pass
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' expected to receive #{key.last}#{inspect_args proxy.arguments} " + \
+ "#{qualifier.to_s.sub('_', ' ')} #{count} times",
+ "but received it #{proxy.calls} times")
+ end
+ end
+ end
+ end
+
+ def self.verify_call(obj, sym, *args, &block)
+ compare = *args
+ compare = compare.first if compare.length <= 1
+
+ key = replaced_key obj, sym
+ [mocks, stubs].each do |proxies|
+ proxies.fetch(key, []).each do |proxy|
+ pass = case proxy.arguments
+ when :any_args
+ true
+ when :no_args
+ compare.nil?
+ else
+ proxy.arguments == compare
+ end
+
+ if proxy.yielding?
+ if block
+ proxy.yielding.each do |args_to_yield|
+ if block.arity == -1 || block.arity == args_to_yield.size
+ block.call(*args_to_yield)
+ else
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' asked to yield " + \
+ "|#{proxy.yielding.join(', ')}| on #{sym}\n",
+ "but a block with arity #{block.arity} was passed")
+ end
+ end
+ else
+ SpecExpectation.fail_with(
+ "Mock '#{name_or_inspect obj}' asked to yield " + \
+ "|[#{proxy.yielding.join('], [')}]| on #{sym}\n",
+ "but no block was passed")
+ end
+ end
+
+ if pass
+ proxy.called
+
+ if proxy.raising?
+ raise proxy.raising
+ else
+ return proxy.returning
+ end
+ end
+ end
+ end
+
+ if sym.to_sym == :respond_to?
+ mock_respond_to? obj, *args
+ else
+ SpecExpectation.fail_with("Mock '#{name_or_inspect obj}': method #{sym}\n",
+ "called with unexpected arguments #{inspect_args args}")
+ end
+ end
+
+ def self.cleanup
+ objects.each do |key, obj|
+ if obj.kind_of? MockIntObject
+ clear_replaced key
+ next
+ end
+
+ replaced = replaced_name(key)
+ sym = key.last
+ meta = obj.singleton_class
+
+ if mock_respond_to? obj, replaced, true
+ suppress_warning do
+ meta.__send__ :alias_method, sym, replaced
+ end
+ meta.__send__ :remove_method, replaced
+ else
+ meta.__send__ :remove_method, sym
+ end
+
+ clear_replaced key
+ end
+ ensure
+ reset
+ end
+end
diff --git a/spec/mspec/lib/mspec/mocks/object.rb b/spec/mspec/lib/mspec/mocks/object.rb
new file mode 100644
index 0000000000..fcaa1caef0
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/object.rb
@@ -0,0 +1,28 @@
+require 'mspec/mocks/proxy'
+
+class Object
+ def should_receive(sym)
+ Mock.install_method self, sym
+ end
+
+ def should_not_receive(sym)
+ proxy = Mock.install_method self, sym
+ proxy.exactly(0).times
+ end
+
+ def stub!(sym)
+ Mock.install_method self, sym, :stub
+ end
+end
+
+def mock(name, options = {})
+ MockObject.new name, options
+end
+
+def mock_int(val)
+ MockIntObject.new(val)
+end
+
+def mock_numeric(name, options = {})
+ NumericMockObject.new name, options
+end
diff --git a/spec/mspec/lib/mspec/mocks/proxy.rb b/spec/mspec/lib/mspec/mocks/proxy.rb
new file mode 100644
index 0000000000..8473132b0b
--- /dev/null
+++ b/spec/mspec/lib/mspec/mocks/proxy.rb
@@ -0,0 +1,186 @@
+class MockObject
+ def initialize(name, options = {})
+ @name = name
+ @null = options[:null_object]
+ end
+
+ def method_missing(sym, *args, &block)
+ @null ? self : super
+ end
+ private :method_missing
+end
+
+class NumericMockObject < Numeric
+ def initialize(name, options = {})
+ @name = name
+ @null = options[:null_object]
+ end
+
+ def method_missing(sym, *args, &block)
+ @null ? self : super
+ end
+
+ def singleton_method_added(val)
+ end
+end
+
+class MockIntObject
+ def initialize(val)
+ @value = val
+ @calls = 0
+
+ key = [self, :to_int]
+
+ Mock.objects[key] = self
+ Mock.mocks[key] << self
+ end
+
+ attr_reader :calls
+
+ def to_int
+ @calls += 1
+ @value.to_int
+ end
+
+ def count
+ [:at_least, 1]
+ end
+end
+
+class MockProxy
+ attr_reader :raising, :yielding
+
+ def initialize(type = nil)
+ @multiple_returns = nil
+ @returning = nil
+ @raising = nil
+ @yielding = []
+ @arguments = :any_args
+ @type = type || :mock
+ end
+
+ def mock?
+ @type == :mock
+ end
+
+ def stub?
+ @type == :stub
+ end
+
+ def count
+ @count ||= mock? ? [:exactly, 1] : [:any_number_of_times, 0]
+ end
+
+ def arguments
+ @arguments
+ end
+
+ def returning
+ if @multiple_returns
+ if @returning.size == 1
+ @multiple_returns = false
+ return @returning = @returning.shift
+ end
+ return @returning.shift
+ end
+ @returning
+ end
+
+ def times
+ self
+ end
+
+ def calls
+ @calls ||= 0
+ end
+
+ def called
+ @calls = calls + 1
+ end
+
+ def exactly(n)
+ @count = [:exactly, n_times(n)]
+ self
+ end
+
+ def at_least(n)
+ @count = [:at_least, n_times(n)]
+ self
+ end
+
+ def at_most(n)
+ @count = [:at_most, n_times(n)]
+ self
+ end
+
+ def once
+ exactly 1
+ end
+
+ def twice
+ exactly 2
+ end
+
+ def any_number_of_times
+ @count = [:any_number_of_times, 0]
+ self
+ end
+
+ def with(*args)
+ raise ArgumentError, "you must specify the expected arguments" if args.empty?
+ if args.length == 1
+ @arguments = args.first
+ else
+ @arguments = args
+ end
+ self
+ end
+
+ def and_return(*args)
+ case args.size
+ when 0
+ @returning = nil
+ when 1
+ @returning = args[0]
+ else
+ @multiple_returns = true
+ @returning = args
+ count[1] = args.size if count[1] < args.size
+ end
+ self
+ end
+
+ def and_raise(exception)
+ if exception.kind_of? String
+ @raising = RuntimeError.new exception
+ else
+ @raising = exception
+ end
+ end
+
+ def raising?
+ @raising != nil
+ end
+
+ def and_yield(*args)
+ @yielding << args
+ self
+ end
+
+ def yielding?
+ !@yielding.empty?
+ end
+
+ private
+
+ def n_times(n)
+ case n
+ when :once
+ 1
+ when :twice
+ 2
+ else
+ Integer n
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner.rb b/spec/mspec/lib/mspec/runner.rb
new file mode 100644
index 0000000000..df57b9f69b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner.rb
@@ -0,0 +1,12 @@
+require 'mspec/mocks'
+require 'mspec/runner/mspec'
+require 'mspec/runner/context'
+require 'mspec/runner/evaluate'
+require 'mspec/runner/example'
+require 'mspec/runner/exception'
+require 'mspec/runner/object'
+require 'mspec/runner/formatters'
+require 'mspec/runner/actions'
+require 'mspec/runner/filters'
+require 'mspec/runner/shared'
+require 'mspec/runner/tag'
diff --git a/spec/mspec/lib/mspec/runner/actions.rb b/spec/mspec/lib/mspec/runner/actions.rb
new file mode 100644
index 0000000000..0a5a05fbd1
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions.rb
@@ -0,0 +1,6 @@
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/actions/tagpurge'
diff --git a/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb
new file mode 100644
index 0000000000..abfb6dd0ee
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/constants_leak_checker.rb
@@ -0,0 +1,84 @@
+class ConstantsLockFile
+ LOCK_FILE_NAME = '.mspec.constants'
+
+ def self.lock_file
+ @prefix ||= File.expand_path(MSpecScript.get(:prefix) || '.')
+ "#{@prefix}/#{LOCK_FILE_NAME}"
+ end
+
+ def self.load
+ if File.exist?(lock_file)
+ File.readlines(lock_file).map(&:chomp)
+ else
+ []
+ end
+ end
+
+ def self.dump(ary)
+ contents = ary.map(&:to_s).uniq.sort.join("\n") + "\n"
+ File.write(lock_file, contents)
+ end
+end
+
+class ConstantLeakError < StandardError
+end
+
+class ConstantsLeakCheckerAction
+ def initialize(save)
+ @save = save
+ @check = !save
+ @constants_locked = ConstantsLockFile.load
+ @exclude_patterns = MSpecScript.get(:toplevel_constants_excludes) || []
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @constants_start = constants_now
+ end
+
+ def before(state)
+ @constants_before = constants_now
+ end
+
+ def after(state)
+ constants = remove_excludes(constants_now - @constants_before - @constants_locked)
+
+ if @check && !constants.empty?
+ MSpec.protect 'Constants leak check' do
+ raise ConstantLeakError, "Top level constants leaked: #{constants.join(', ')}"
+ end
+ end
+ end
+
+ def finish
+ constants = remove_excludes(constants_now - @constants_start - @constants_locked)
+
+ if @save
+ ConstantsLockFile.dump(@constants_locked + constants)
+ end
+
+ if @check && !constants.empty?
+ MSpec.protect 'Global constants leak check' do
+ raise ConstantLeakError, "Top level constants leaked in the whole test suite: #{constants.join(', ')}"
+ end
+ end
+ end
+
+ private
+
+ def constants_now
+ Object.constants.map(&:to_s)
+ end
+
+ def remove_excludes(constants)
+ constants.reject { |name|
+ @exclude_patterns.any? { |pattern| pattern === name }
+ }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/filter.rb b/spec/mspec/lib/mspec/runner/actions/filter.rb
new file mode 100644
index 0000000000..b0ad7080da
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/filter.rb
@@ -0,0 +1,40 @@
+require 'mspec/runner/filters/match'
+
+# ActionFilter is a base class for actions that are triggered by
+# specs that match the filter. The filter may be specified by
+# strings that match spec descriptions or by tags for strings
+# that match spec descriptions.
+#
+# Unlike TagFilter and RegexpFilter, ActionFilter instances do
+# not affect the specs that are run. The filter is only used to
+# trigger the action.
+
+class ActionFilter
+ def initialize(tags = nil, descs = nil)
+ @tags = Array(tags)
+ descs = Array(descs)
+ @sfilter = descs.empty? ? nil : MatchFilter.new(nil, *descs)
+ @tfilter = nil
+ end
+
+ def ===(string)
+ @sfilter === string or @tfilter === string
+ end
+
+ def load
+ return if @tags.empty?
+
+ desc = MSpec.read_tags(@tags).map { |t| t.description }
+ return if desc.empty?
+
+ @tfilter = MatchFilter.new(nil, *desc)
+ end
+
+ def register
+ MSpec.register :load, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/leakchecker.rb b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
new file mode 100644
index 0000000000..0a8c9c3252
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/leakchecker.rb
@@ -0,0 +1,377 @@
+# Adapted from ruby's test/lib/leakchecker.rb.
+# Ruby's 2-clause BSDL follows.
+
+# Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+class LeakError < StandardError
+end
+
+class LeakChecker
+ attr_reader :leaks
+
+ def initialize
+ @fd_info = find_fds
+ @tempfile_info = find_tempfiles
+ @thread_info = find_threads
+ @env_info = find_env
+ @argv_info = find_argv
+ @globals_info = find_globals
+ @encoding_info = find_encodings
+ end
+
+ def check(state)
+ @state = state
+ @leaks = []
+ check_fd_leak
+ check_tempfile_leak
+ check_thread_leak
+ check_process_leak
+ check_env
+ check_argv
+ check_globals
+ check_encodings
+ check_tracepoints
+ GC.start unless @leaks.empty?
+ @leaks.empty?
+ end
+
+ private
+ def find_fds
+ fd_dir = "/proc/self/fd"
+ if File.directory?(fd_dir)
+ fds = Dir.open(fd_dir) {|d|
+ a = d.grep(/\A\d+\z/, &:to_i)
+ if d.respond_to? :fileno
+ a -= [d.fileno]
+ end
+ a
+ }
+ fds.sort
+ else
+ []
+ end
+ end
+
+ def check_fd_leak
+ live1 = @fd_info
+ if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
+ m[:close]
+ end
+ live2 = find_fds
+ fd_closed = live1 - live2
+ if !fd_closed.empty?
+ fd_closed.each {|fd|
+ leak "Closed file descriptor: #{fd}"
+ }
+ end
+ fd_leaked = live2 - live1
+ if !fd_leaked.empty?
+ h = {}
+ ObjectSpace.each_object(IO) {|io|
+ inspect = io.inspect
+ begin
+ autoclose = io.autoclose?
+ fd = io.fileno
+ rescue IOError # closed IO object
+ next
+ end
+ (h[fd] ||= []) << [io, autoclose, inspect]
+ }
+ fd_leaked.each {|fd|
+ str = ''
+ if h[fd]
+ str << ' :'
+ h[fd].map {|io, autoclose, inspect|
+ s = ' ' + inspect
+ s << "(not-autoclose)" if !autoclose
+ s
+ }.sort.each {|s|
+ str << s
+ }
+ end
+ leak "Leaked file descriptor: #{fd}#{str}"
+ }
+ #system("lsof -p #$$") if !fd_leaked.empty?
+ h.each {|fd, list|
+ next if list.length <= 1
+ if 1 < list.count {|io, autoclose, inspect| autoclose }
+ str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
+ leak "Multiple autoclose IO object for a file descriptor:#{str}"
+ end
+ }
+ end
+ @fd_info = live2
+ end
+
+ def extend_tempfile_counter
+ return if defined? LeakChecker::TempfileCounter
+ m = Module.new {
+ @count = 0
+ class << self
+ attr_accessor :count
+ end
+
+ def new(...)
+ LeakChecker::TempfileCounter.count += 1
+ super
+ end
+ }
+ LeakChecker.const_set(:TempfileCounter, m)
+
+ class << Tempfile
+ prepend LeakChecker::TempfileCounter
+ end
+ end
+
+ def find_tempfiles(prev_count = -1)
+ return [prev_count, []] unless defined? Tempfile
+ extend_tempfile_counter
+ count = TempfileCounter.count
+ if prev_count == count
+ [prev_count, []]
+ else
+ tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t| t.path }
+ [count, tempfiles]
+ end
+ end
+
+ def check_tempfile_leak
+ return false unless defined? Tempfile
+ count1, initial_tempfiles = @tempfile_info
+ count2, current_tempfiles = find_tempfiles(count1)
+ tempfiles_leaked = current_tempfiles - initial_tempfiles
+ if !tempfiles_leaked.empty?
+ list = tempfiles_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ leak "Leaked tempfile: #{str}"
+ }
+ tempfiles_leaked.each {|t| t.close! }
+ end
+ @tempfile_info = [count2, initial_tempfiles]
+ end
+
+ def find_threads
+ Thread.list.find_all {|t|
+ t != Thread.current && t.alive? &&
+ !(t.thread_variable?(:"\0__detached_thread__") && t.thread_variable_get(:"\0__detached_thread__"))
+ }
+ end
+
+ def check_thread_leak
+ live1 = @thread_info
+ live2 = find_threads
+ thread_finished = live1 - live2
+ if !thread_finished.empty?
+ list = thread_finished.map {|t| t.inspect }.sort
+ list.each {|str|
+ leak "Finished thread: #{str}"
+ }
+ end
+ thread_leaked = live2 - live1
+ if !thread_leaked.empty?
+ list = thread_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ leak "Leaked thread: #{str}"
+ }
+ end
+ @thread_info = live2
+ end
+
+ def check_process_leak
+ subprocesses_leaked = Process.waitall
+ subprocesses_leaked.each { |pid, status|
+ leak "Leaked subprocess: #{pid}: #{status}"
+ }
+ end
+
+ def find_env
+ ENV.to_h
+ end
+
+ def check_env
+ old_env = @env_info
+ new_env = find_env
+ return if old_env == new_env
+
+ (old_env.keys | new_env.keys).sort.each {|k|
+ if old_env.has_key?(k)
+ if new_env.has_key?(k)
+ if old_env[k] != new_env[k]
+ leak "Environment variable changed : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ end
+ else
+ leak "Environment variable changed: #{k.inspect} deleted"
+ end
+ else
+ if new_env.has_key?(k)
+ leak "Environment variable changed: #{k.inspect} added"
+ else
+ flunk "unreachable"
+ end
+ end
+ }
+ @env_info = new_env
+ end
+
+ def find_argv
+ ARGV.map { |e| e.dup }
+ end
+
+ def check_argv
+ old_argv = @argv_info
+ new_argv = find_argv
+ if new_argv != old_argv
+ leak "ARGV changed: #{old_argv.inspect} to #{new_argv.inspect}"
+ @argv_info = new_argv
+ end
+ end
+
+ def find_globals
+ { verbose: $VERBOSE, debug: $DEBUG }
+ end
+
+ def check_globals
+ old_globals = @globals_info
+ new_globals = find_globals
+ if new_globals != old_globals
+ leak "Globals changed: #{old_globals.inspect} to #{new_globals.inspect}"
+ @globals_info = new_globals
+ end
+ end
+
+ def find_encodings
+ [Encoding.default_internal, Encoding.default_external]
+ end
+
+ def check_encodings
+ old_internal, old_external = @encoding_info
+ new_internal, new_external = find_encodings
+ if new_internal != old_internal
+ leak "Encoding.default_internal changed: #{old_internal.inspect} to #{new_internal.inspect}"
+ end
+ if new_external != old_external
+ leak "Encoding.default_external changed: #{old_external.inspect} to #{new_external.inspect}"
+ end
+ @encoding_info = [new_internal, new_external]
+ end
+
+ def check_tracepoints
+ ObjectSpace.each_object(TracePoint) do |tp|
+ if tp.enabled?
+ leak "TracePoint is still enabled: #{tp.inspect}"
+ end
+ end
+ end
+
+ def leak(message)
+ if @leaks.empty?
+ $stderr.puts "\n"
+ $stderr.puts @state.description
+ end
+ @leaks << message
+ $stderr.puts message
+ end
+end
+
+class LeakCheckerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :after, self
+ end
+
+ def start
+ disable_nss_modules
+ @checker = LeakChecker.new
+ end
+
+ def after(state)
+ unless @checker.check(state)
+ leak_messages = @checker.leaks
+ location = state.description
+ if state.example
+ location = "#{location}\n#{state.example.source_location.join(':')}"
+ end
+ MSpec.protect(location) do
+ raise LeakError, leak_messages.join("\n")
+ end
+ end
+ end
+
+ private
+
+ # This function is intended to disable all NSS modules when ruby is compiled
+ # against glibc. NSS modules allow the system administrator to load custom
+ # shared objects into all processes using glibc, and use them to customise
+ # the behaviour of username, groupname, hostname, etc lookups. This is
+ # normally configured in the file /etc/nsswitch.conf.
+ # These modules often do things like open cache files or connect to system
+ # daemons like sssd or dbus, which of course means they have open file
+ # descriptors of their own. This can cause the leak-checking functionality
+ # in this file to report that such descriptors have been leaked, and fail
+ # the test suite.
+ # This function uses glibc's __nss_configure_lookup function to override any
+ # configuration in /etc/nsswitch.conf, and just use the built in files/dns
+ # name lookup functionality (which is of course perfectly sufficient for
+ # running ruby/spec).
+ def disable_nss_modules
+ begin
+ require 'fiddle'
+ rescue LoadError
+ # Make sure it's possible to run the test suite on a ruby implementation
+ # which does not (yet?) have Fiddle.
+ return
+ end
+
+ begin
+ libc = Fiddle.dlopen(nil)
+ # Older versions of fiddle don't have Fiddle::Type (and instead rely on Fiddle::TYPE_)
+ # Even older versions of fiddle don't have CONST_STRING,
+ string_type = defined?(Fiddle::TYPE_CONST_STRING) ? Fiddle::TYPE_CONST_STRING : Fiddle::TYPE_VOIDP
+ nss_configure_lookup = Fiddle::Function.new(
+ libc['__nss_configure_lookup'],
+ [string_type, string_type],
+ Fiddle::TYPE_INT
+ )
+ rescue Fiddle::DLError
+ # We're not running with glibc - no need to do this.
+ return
+ end
+
+ nss_configure_lookup.call 'passwd', 'files'
+ nss_configure_lookup.call 'shadow', 'files'
+ nss_configure_lookup.call 'group', 'files'
+ nss_configure_lookup.call 'hosts', 'files dns'
+ nss_configure_lookup.call 'services', 'files'
+ nss_configure_lookup.call 'netgroup', 'files'
+ nss_configure_lookup.call 'automount', 'files'
+ nss_configure_lookup.call 'aliases', 'files'
+ nss_configure_lookup.call 'ethers', 'files'
+ nss_configure_lookup.call 'gshadow', 'files'
+ nss_configure_lookup.call 'initgroups', 'files'
+ nss_configure_lookup.call 'networks', 'files dns'
+ nss_configure_lookup.call 'protocols', 'files'
+ nss_configure_lookup.call 'publickey', 'files'
+ nss_configure_lookup.call 'rpc', 'files'
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/profile.rb b/spec/mspec/lib/mspec/runner/actions/profile.rb
new file mode 100644
index 0000000000..c743d6e3e8
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/profile.rb
@@ -0,0 +1,60 @@
+class ProfileAction
+ def initialize
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ MSpec.register :enter, self
+ MSpec.register :before,self
+ MSpec.register :after, self
+ MSpec.register :finish,self
+ end
+
+ def enter(describe)
+ if @describe_time
+ @describes << [@describe_name, now - @describe_time]
+ end
+
+ @describe_name = describe
+ @describe_time = now
+ end
+
+ def before(state)
+ @it_name = state.it
+ @it_time = now
+ end
+
+ def after(state = nil)
+ @its << [@describe_name, @it_name, now - @it_time]
+ end
+
+ def finish
+ puts "\nProfiling info:"
+
+ desc = @describes.sort { |a,b| b.last <=> a.last }
+ desc.delete_if { |a| a.last <= 0.001 }
+ show = desc[0, 100]
+
+ puts "Top #{show.size} describes:"
+
+ show.each do |des, time|
+ printf "%3.3f - %s\n", time, des
+ end
+
+ its = @its.sort { |a,b| b.last <=> a.last }
+ its.delete_if { |a| a.last <= 0.001 }
+ show = its[0, 100]
+
+ puts "\nTop #{show.size} its:"
+ show.each do |des, it, time|
+ printf "%3.3f - %s %s\n", time, des, it
+ end
+ end
+
+ def now
+ Time.now.to_f
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tag.rb b/spec/mspec/lib/mspec/runner/actions/tag.rb
new file mode 100644
index 0000000000..d40d562451
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tag.rb
@@ -0,0 +1,133 @@
+require 'mspec/runner/actions/filter'
+
+# TagAction - Write tagged spec description string to a
+# tag file associated with each spec file.
+#
+# The action is triggered by specs whose descriptions
+# match the filter created with 'tags' and/or 'desc'
+#
+# The action fires in the :after event, after the spec
+# had been run. The action fires if the outcome of
+# running the spec matches 'outcome'.
+#
+# The arguments are:
+#
+# action: :add, :del
+# outcome: :pass, :fail, :all
+# tag: the tag to create/delete
+# comment: the comment to create
+# tags: zero or more tags to get matching
+# spec description strings from
+# desc: zero or more strings to match the
+# spec description strings
+
+class TagAction < ActionFilter
+ def initialize(action, outcome, tag, comment, tags = nil, descs = nil)
+ super tags, descs
+ @action = action
+ @outcome = outcome
+ @tag = tag
+ @comment = comment
+ @report = []
+ @exception = false
+ end
+
+ # Returns true if there are no _tag_ or _description_ filters. This
+ # means that a TagAction matches any example by default. Otherwise,
+ # returns true if either the _tag_ or the _description_ filter
+ # matches +string+.
+ def ===(string)
+ return true unless @sfilter or @tfilter
+ @sfilter === string or @tfilter === string
+ end
+
+ # Callback for the MSpec :before event. Resets the +#exception?+
+ # flag to false.
+ def before(state)
+ @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Sets the +#exception?+
+ # flag to true.
+ def exception(exception)
+ @exception = true
+ end
+
+ # Callback for the MSpec :after event. Performs the tag action
+ # depending on the type of action and the outcome of evaluating
+ # the example. See +TagAction+ for a description of the actions.
+ def after(state)
+ if self === state.description and outcome?
+ tag = SpecTag.new
+ tag.tag = @tag
+ tag.comment = @comment
+ tag.description = state.description
+
+ case @action
+ when :add
+ changed = MSpec.write_tag tag
+ when :del
+ changed = MSpec.delete_tag tag
+ end
+
+ @report << state.description if changed
+ end
+ end
+
+ # Returns true if the result of evaluating the example matches
+ # the _outcome_ registered for this tag action. See +TagAction+
+ # for a description of the _outcome_ types.
+ def outcome?
+ @outcome == :all or
+ (@outcome == :pass and not exception?) or
+ (@outcome == :fail and exception?)
+ end
+
+ # Returns true if an exception was raised while evaluating the
+ # current example.
+ def exception?
+ @exception
+ end
+
+ def report
+ @report.join("\n") + "\n"
+ end
+ private :report
+
+ # Callback for the MSpec :finish event. Prints the actions
+ # performed while evaluating the examples.
+ def finish
+ case @action
+ when :add
+ if @report.empty?
+ print "\nTagAction: no specs were tagged with '#{@tag}'\n"
+ else
+ print "\nTagAction: specs tagged with '#{@tag}':\n\n"
+ print report
+ end
+ when :del
+ if @report.empty?
+ print "\nTagAction: no tags '#{@tag}' were deleted\n"
+ else
+ print "\nTagAction: tag '#{@tag}' deleted for specs:\n\n"
+ print report
+ end
+ end
+ end
+
+ def register
+ super
+ MSpec.register :before, self
+ MSpec.register :exception, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :before, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :after, self
+ MSpec.unregister :finish, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/taglist.rb b/spec/mspec/lib/mspec/runner/actions/taglist.rb
new file mode 100644
index 0000000000..3097e655d5
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/taglist.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+
+# TagListAction - prints out the descriptions for any specs
+# tagged with +tags+. If +tags+ is an empty list, prints out
+# descriptions for any specs that are tagged.
+class TagListAction
+ def initialize(tags = nil)
+ @tags = tags.nil? || tags.empty? ? nil : Array(tags)
+ @filter = nil
+ end
+
+ # Returns true. This enables us to match any tag when loading
+ # tags from the file.
+ def include?(arg)
+ true
+ end
+
+ # Returns true if any tagged descriptions matches +string+.
+ def ===(string)
+ @filter === string
+ end
+
+ # Prints a banner about matching tagged specs.
+ def start
+ if @tags
+ print "\nListing specs tagged with #{@tags.map { |t| "'#{t}'" }.join(", ") }\n\n"
+ else
+ print "\nListing all tagged specs\n\n"
+ end
+ end
+
+ # Creates a MatchFilter for specific tags or for all tags.
+ def load
+ @filter = nil
+ desc = MSpec.read_tags(@tags || self).map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Prints the spec description if it matches the filter.
+ def after(state)
+ return unless self === state.description
+ print state.description, "\n"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :load, self
+ MSpec.register :after, self
+ end
+
+ def unregister
+ MSpec.unregister :start, self
+ MSpec.unregister :load, self
+ MSpec.unregister :after, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tagpurge.rb b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
new file mode 100644
index 0000000000..f4587de6bc
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tagpurge.rb
@@ -0,0 +1,56 @@
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/actions/taglist'
+
+# TagPurgeAction - removes all tags not matching any spec
+# descriptions.
+class TagPurgeAction < TagListAction
+ attr_reader :matching
+
+ def initialize
+ @matching = []
+ @filter = nil
+ @tags = nil
+ end
+
+ # Prints a banner about purging tags.
+ def start
+ print "\nRemoving tags not matching any specs\n\n"
+ end
+
+ # Creates a MatchFilter for all tags.
+ def load
+ @filter = nil
+ @tags = MSpec.read_tags self
+ desc = @tags.map { |t| t.description }
+ @filter = MatchFilter.new(nil, *desc) unless desc.empty?
+ end
+
+ # Saves any matching tags
+ def after(state)
+ @matching << state.description if self === state.description
+ end
+
+ # Rewrites any matching tags. Prints non-matching tags.
+ # Deletes the tag file if there were no tags (this cleans
+ # up empty or malformed tag files).
+ def unload
+ if @filter
+ matched = @tags.select { |t| @matching.any? { |s| s == t.description } }
+ MSpec.write_tags matched
+
+ (@tags - matched).each { |t| print t.description, "\n" }
+ else
+ MSpec.delete_tags
+ end
+ end
+
+ def register
+ super
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ super
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/tally.rb b/spec/mspec/lib/mspec/runner/actions/tally.rb
new file mode 100644
index 0000000000..d6ada53bab
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/tally.rb
@@ -0,0 +1,133 @@
+class Tally
+ attr_accessor :files, :examples, :expectations, :failures, :errors, :guards, :tagged
+
+ def initialize
+ @files = @examples = @expectations = @failures = @errors = @guards = @tagged = 0
+ end
+
+ def files!(add = 1)
+ @files += add
+ end
+
+ def examples!(add = 1)
+ @examples += add
+ end
+
+ def expectations!(add = 1)
+ @expectations += add
+ end
+
+ def failures!(add = 1)
+ @failures += add
+ end
+
+ def errors!(add = 1)
+ @errors += add
+ end
+
+ def guards!(add = 1)
+ @guards += add
+ end
+
+ def tagged!(add = 1)
+ @tagged += add
+ end
+
+ def file
+ pluralize files, "file"
+ end
+
+ def example
+ pluralize examples, "example"
+ end
+
+ def expectation
+ pluralize expectations, "expectation"
+ end
+
+ def failure
+ pluralize failures, "failure"
+ end
+
+ def error
+ pluralize errors, "error"
+ end
+
+ def guard
+ pluralize guards, "guard"
+ end
+
+ def tag
+ "#{tagged} tagged"
+ end
+
+ def format
+ results = [ file, example, expectation, failure, error, tag ]
+ if [:report, :report_on, :verify].any? { |m| MSpec.mode? m }
+ results << guard
+ end
+ results.join(", ")
+ end
+
+ alias_method :to_s, :format
+
+ def pluralize(count, singular)
+ "#{count} #{singular}#{'s' unless count == 1}"
+ end
+ private :pluralize
+end
+
+class TallyAction
+ attr_reader :counter
+
+ def initialize
+ @counter = Tally.new
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :exception, self
+ MSpec.register :example, self
+ MSpec.register :tagged, self
+ MSpec.register :expectation, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :exception, self
+ MSpec.unregister :example, self
+ MSpec.unregister :tagged, self
+ MSpec.unregister :expectation, self
+ end
+
+ def load
+ @counter.files!
+ end
+
+ # Callback for the MSpec :expectation event. Increments the
+ # tally of expectations (e.g. #should, #should_receive, etc.).
+ def expectation(state)
+ @counter.expectations!
+ end
+
+ # Callback for the MSpec :exception event. Increments the
+ # tally of errors and failures.
+ def exception(exception)
+ exception.failure? ? @counter.failures! : @counter.errors!
+ end
+
+ # Callback for the MSpec :example event. Increments the tally
+ # of examples.
+ def example(state, block)
+ @counter.examples!
+ end
+
+ def tagged(state)
+ @counter.examples!
+ @counter.tagged!
+ end
+
+ def format
+ @counter.format
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/timeout.rb b/spec/mspec/lib/mspec/runner/actions/timeout.rb
new file mode 100644
index 0000000000..1200926872
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timeout.rb
@@ -0,0 +1,145 @@
+class TimeoutAction
+ def initialize(timeout)
+ @timeout = timeout
+ @queue = Queue.new
+ @started = now
+ @fail = false
+ @error_message = "took longer than the configured timeout of #{@timeout}s"
+ end
+
+ def register
+ MSpec.register :start, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :finish, self
+ end
+
+ private def now
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
+
+ private def fetch_item
+ @queue.pop(true)
+ rescue ThreadError
+ nil
+ end
+
+ def start
+ @thread = Thread.new do
+ loop do
+ if action = fetch_item
+ action.call
+ else
+ wakeup_at = @started + @timeout
+ left = wakeup_at - now
+ sleep left if left > 0
+ Thread.pass # Let the main thread run
+
+ if @queue.empty?
+ elapsed = now - @started
+ if elapsed > @timeout
+ if @current_state
+ STDERR.puts "\nExample #{@error_message}:"
+ STDERR.puts "#{@current_state.description}"
+ else
+ STDERR.puts "\nSome code outside an example #{@error_message}"
+ end
+ STDERR.flush
+
+ show_backtraces
+ if MSpec.subprocesses.empty?
+ exit! 2
+ else
+ # Do not exit but signal the subprocess so we can get their output
+ MSpec.subprocesses.each do |pid|
+ kill_wait_one_second :SIGTERM, pid
+ hard_kill :SIGKILL, pid
+ end
+ @fail = true
+ @current_state = nil
+ break # stop this thread, will fail in #after
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ def before(state = nil)
+ time = now
+ @queue << -> do
+ @current_state = state
+ @started = time
+ end
+ end
+
+ def after(state = nil)
+ @queue << -> do
+ @current_state = nil
+ end
+
+ if @fail
+ STDERR.puts "\n\nThe last example #{@error_message}. See above for the subprocess stacktrace."
+ exit! 2
+ end
+ end
+
+ def finish
+ @thread.kill
+ @thread.join
+ end
+
+ private def hard_kill(signal, pid)
+ begin
+ Process.kill signal, pid
+ rescue Errno::ESRCH
+ # Process already terminated
+ end
+ end
+
+ private def kill_wait_one_second(signal, pid)
+ begin
+ Process.kill signal, pid
+ sleep 1
+ rescue Errno::ESRCH
+ # Process already terminated
+ end
+ end
+
+ private def show_backtraces
+ java_stacktraces = -> pid {
+ if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby'
+ STDERR.puts 'Java stacktraces:'
+ kill_wait_one_second :SIGQUIT, pid
+ end
+ }
+
+ if MSpec.subprocesses.empty?
+ java_stacktraces.call Process.pid
+
+ STDERR.puts "\nRuby backtraces:"
+ if defined?(Truffle::Debug.show_backtraces)
+ Truffle::Debug.show_backtraces
+ else
+ Thread.list.each do |thread|
+ unless thread == Thread.current
+ STDERR.puts thread.inspect, thread.backtrace, ''
+ end
+ end
+ end
+ else
+ MSpec.subprocesses.each do |pid|
+ STDERR.puts "\nFor subprocess #{pid}"
+ java_stacktraces.call pid
+
+ if RUBY_ENGINE == 'truffleruby'
+ STDERR.puts "\nRuby backtraces:"
+ kill_wait_one_second :SIGALRM, pid
+ else
+ STDERR.puts "Don't know how to print backtraces of a subprocess on #{RUBY_ENGINE}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/actions/timer.rb b/spec/mspec/lib/mspec/runner/actions/timer.rb
new file mode 100644
index 0000000000..e7ebfebe0d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/actions/timer.rb
@@ -0,0 +1,22 @@
+class TimerAction
+ def register
+ MSpec.register :start, self
+ MSpec.register :finish, self
+ end
+
+ def start
+ @start = Time.now
+ end
+
+ def finish
+ @stop = Time.now
+ end
+
+ def elapsed
+ @stop - @start
+ end
+
+ def format
+ "Finished in %f seconds" % elapsed
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/context.rb b/spec/mspec/lib/mspec/runner/context.rb
new file mode 100644
index 0000000000..bcd83b2465
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/context.rb
@@ -0,0 +1,237 @@
+# Holds the state of the +describe+ block that is being
+# evaluated. Every example (i.e. +it+ block) is evaluated
+# in a context, which may include state set up in <tt>before
+# :each</tt> or <tt>before :all</tt> blocks.
+#
+#--
+# A note on naming: this is named _ContextState_ rather
+# than _DescribeState_ because +describe+ is the keyword
+# in the DSL for referring to the context in which an example
+# is evaluated, just as +it+ refers to the example itself.
+#++
+class ContextState
+ attr_reader :state, :parent, :parents, :children, :examples, :to_s
+
+ MOCK_VERIFY = -> { Mock.verify_count }
+ MOCK_CLEANUP = -> { Mock.cleanup }
+ EXPECTATION_MISSING = -> { raise SpecExpectationNotFoundError }
+
+ def initialize(description, options = nil)
+ raise "#describe options should be a Hash or nil" unless Hash === options or options.nil?
+ @to_s = description.to_s
+ @shared = options && options[:shared]
+
+ @parsed = false
+ @before = { :all => [], :each => [] }
+ @after = { :all => [], :each => [] }
+ @pre = {}
+ @post = {}
+ @examples = []
+ @state = nil
+ @parent = nil
+ @parents = [self]
+ @children = []
+ end
+
+ # Remove caching when a ContextState is dup'd for shared specs.
+ def initialize_copy(other)
+ @pre = {}
+ @post = {}
+ end
+
+ # Returns true if this is a shared +ContextState+. Essentially, when
+ # created with: describe "Something", :shared => true { ... }
+ def shared?
+ @shared
+ end
+
+ # Set the parent (enclosing) +ContextState+ for this state. Creates
+ # the +parents+ list.
+ def parent=(parent)
+ @description = nil
+
+ if shared?
+ @parent = nil
+ else
+ @parent = parent
+ parent.child self if parent
+
+ @parents = [self]
+ state = parent
+ while state
+ @parents.unshift state
+ state = state.parent
+ end
+ end
+ end
+
+ # Add the ContextState instance +child+ to the list of nested
+ # describe blocks.
+ def child(child)
+ @children << child
+ end
+
+ # Adds a nested ContextState in a shared ContextState to a containing
+ # ContextState.
+ #
+ # Normal adoption is from the parent's perspective. But adopt is a good
+ # verb and it's reasonable for the child to adopt the parent as well. In
+ # this case, manipulating state from inside the child avoids needlessly
+ # exposing the state to manipulate it externally in the dup. (See
+ # #it_should_behave_like)
+ def adopt(parent)
+ self.parent = parent
+
+ @examples = @examples.map do |example|
+ example = example.dup
+ example.context = self
+ example
+ end
+
+ children = @children
+ @children = []
+
+ children.each { |child| child.dup.adopt self }
+ end
+
+ # Returns a list of all before(+what+) blocks from self and any parents.
+ def pre(what)
+ @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
+ end
+
+ # Returns a list of all after(+what+) blocks from self and any parents.
+ # The list is in reverse order. In other words, the blocks defined in
+ # inner describes are in the list before those defined in outer describes,
+ # and in a particular describe block those defined later are in the list
+ # before those defined earlier.
+ def post(what)
+ @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
+ end
+
+ # Records before(:each) and before(:all) blocks.
+ def before(what, &block)
+ return if MSpec.guarded?
+ block ? @before[what].push(block) : @before[what]
+ end
+
+ # Records after(:each) and after(:all) blocks.
+ def after(what, &block)
+ return if MSpec.guarded?
+ block ? @after[what].unshift(block) : @after[what]
+ end
+
+ # Creates an ExampleState instance for the block and stores it
+ # in a list of examples to evaluate unless the example is filtered.
+ def it(desc, &block)
+ raise "nested #it" if @state
+ example = ExampleState.new(self, desc, block)
+ MSpec.actions :add, example
+ return if MSpec.guarded?
+ @examples << example
+ end
+
+ # Evaluates the block and resets the toplevel +ContextState+ to #parent.
+ def describe(&block)
+ @parsed = protect @to_s, block, false
+ MSpec.register_current parent
+ MSpec.register_shared self if shared?
+ end
+
+ # Returns a description string generated from self and all parents
+ def description
+ @description ||= parents.map { |p| p.to_s }.compact.join(" ")
+ end
+
+ # Injects the before/after blocks and examples from the shared
+ # describe block into this +ContextState+ instance.
+ def it_should_behave_like(desc)
+ return if MSpec.guarded?
+
+ unless state = MSpec.retrieve_shared(desc)
+ raise Exception, "Unable to find shared 'describe' for #{desc}"
+ end
+
+ state.before(:all).each { |b| before :all, &b }
+ state.before(:each).each { |b| before :each, &b }
+ state.after(:each).each { |b| after :each, &b }
+ state.after(:all).each { |b| after :all, &b }
+
+ state.examples.each do |example|
+ example = example.dup
+ example.context = self
+ @examples << example
+ end
+
+ state.children.each do |child|
+ child.dup.adopt self
+ end
+ end
+
+ # Evaluates each block in +blocks+ using the +MSpec.protect+ method
+ # so that exceptions are handled and tallied. Returns true and does
+ # NOT evaluate any blocks if +check+ is true and
+ # <tt>MSpec.mode?(:pretend)</tt> is true.
+ def protect(what, blocks, check = true)
+ return true if check and MSpec.mode? :pretend
+ Array(blocks).all? { |block| MSpec.protect what, &block }
+ end
+
+ # Removes filtered examples. Returns true if there are examples
+ # left to evaluate.
+ def filter_examples
+ filtered, @examples = @examples.partition do |ex|
+ ex.filtered?
+ end
+
+ filtered.each do |ex|
+ MSpec.actions :tagged, ex
+ end
+
+ !@examples.empty?
+ end
+
+ # Evaluates the examples in a +ContextState+. Invokes the MSpec events
+ # for :enter, :before, :after, :leave.
+ def process
+ MSpec.register_current self
+
+ if @parsed and filter_examples
+ MSpec.shuffle @examples if MSpec.randomize?
+ MSpec.actions :enter, description
+
+ if protect "before :all", pre(:all)
+ @examples.each do |state|
+ MSpec.repeat do
+ @state = state
+ example = state.example
+ MSpec.actions :before, state
+
+ if protect "before :each", pre(:each)
+ MSpec.clear_expectations
+ if example
+ passed = protect nil, example
+ passed = protect nil, -> { MSpec.actions :passed, state, example } if passed
+ MSpec.actions :example, state, example
+ protect nil, EXPECTATION_MISSING if !MSpec.expectation? and passed
+ end
+ end
+ protect "after :each", post(:each)
+ protect "Mock.verify_count", MOCK_VERIFY
+
+ protect "Mock.cleanup", MOCK_CLEANUP
+ MSpec.actions :after, state
+ @state = nil
+ end
+ end
+ protect "after :all", post(:all)
+ else
+ protect "Mock.cleanup", MOCK_CLEANUP
+ end
+
+ MSpec.actions :leave
+ end
+
+ MSpec.register_current nil
+ children.each { |child| child.process }
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/evaluate.rb b/spec/mspec/lib/mspec/runner/evaluate.rb
new file mode 100644
index 0000000000..396a84c118
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/evaluate.rb
@@ -0,0 +1,54 @@
+class SpecEvaluate
+ include MSpecMatchers
+
+ def self.desc=(desc)
+ @desc = desc
+ end
+
+ def self.desc
+ @desc ||= "evaluates "
+ end
+
+ def initialize(ruby, desc)
+ @ruby = ruby.rstrip
+ @desc = desc || self.class.desc
+ end
+
+ # Formats the Ruby source code for reabable output in the -fs formatter
+ # option. If the source contains no newline characters, wraps the source in
+ # single quotes to set if off from the rest of the description string. If
+ # the source does contain newline characters, sets the indent level to four
+ # characters.
+ def format(ruby, newline = true)
+ if ruby.include?("\n")
+ lines = ruby.each_line.to_a
+ if /( *)/ =~ lines.first
+ if $1.size > 4
+ dedent = $1.size - 4
+ ruby = lines.map { |l| l[dedent..-1] }.join
+ else
+ indent = " " * (4 - $1.size)
+ ruby = lines.map { |l| "#{indent}#{l}" }.join
+ end
+ end
+ "\n#{ruby}"
+ else
+ "'#{ruby.lstrip}'"
+ end
+ end
+
+ def define(&block)
+ ruby = @ruby
+ desc = @desc
+ evaluator = self
+
+ specify "#{desc} #{format ruby}" do
+ evaluator.instance_eval(ruby)
+ evaluator.instance_eval(&block)
+ end
+ end
+end
+
+def evaluate(str, desc = nil, &block)
+ SpecEvaluate.new(str, desc).define(&block)
+end
diff --git a/spec/mspec/lib/mspec/runner/example.rb b/spec/mspec/lib/mspec/runner/example.rb
new file mode 100644
index 0000000000..0d9f0d618c
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/example.rb
@@ -0,0 +1,34 @@
+require 'mspec/runner/mspec'
+
+# Holds some of the state of the example (i.e. +it+ block) that is
+# being evaluated. See also +ContextState+.
+class ExampleState
+ attr_reader :context, :it, :example
+
+ def initialize(context, it, example = nil)
+ @context = context
+ @it = it
+ @example = example
+ end
+
+ def context=(context)
+ @description = nil
+ @context = context
+ end
+
+ def describe
+ @context.description
+ end
+
+ def description
+ @description ||= "#{describe} #{@it}"
+ end
+
+ def filtered?
+ incl = MSpec.include
+ excl = MSpec.exclude
+ included = incl.empty? || incl.any? { |f| f === description }
+ included &&= excl.empty? || !excl.any? { |f| f === description }
+ !included
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/exception.rb b/spec/mspec/lib/mspec/runner/exception.rb
new file mode 100644
index 0000000000..23375733e6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/exception.rb
@@ -0,0 +1,54 @@
+# Initialize $MSPEC_DEBUG
+$MSPEC_DEBUG ||= false
+
+class ExceptionState
+ attr_reader :description, :describe, :it, :exception
+
+ def initialize(state, location, exception)
+ @exception = exception
+ @failure = exception.class == SpecExpectationNotMetError || exception.class == SpecExpectationNotFoundError
+
+ @description = location ? "An exception occurred during: #{location}" : ""
+ if state
+ @description += "\n" unless @description.empty?
+ @description += state.description
+ @describe = state.describe
+ @it = state.it
+ else
+ @describe = @it = ""
+ end
+ end
+
+ def failure?
+ @failure
+ end
+
+ def message
+ message = @exception.message
+ message = "<No message>" if message.empty?
+
+ if @failure
+ message
+ elsif raise_error_message = RaiseErrorMatcher::FAILURE_MESSAGE_FOR_EXCEPTION[@exception]
+ raise_error_message.join("\n")
+ else
+ "#{@exception.class}: #{message}"
+ end
+ end
+
+ def backtrace
+ @backtrace_filter ||= MSpecScript.config[:backtrace_filter] || %r{(?:/bin/mspec|/lib/mspec/)}
+
+ bt = @exception.backtrace || []
+ unless $MSPEC_DEBUG
+ # Exclude <internal: entries inside MSpec code, so only after the first ignored entry
+ first_excluded_line = bt.index { |line| @backtrace_filter =~ line }
+ if first_excluded_line
+ bt = bt[0...first_excluded_line] + bt[first_excluded_line..-1].reject { |line|
+ @backtrace_filter =~ line || /^<internal:/ =~ line
+ }
+ end
+ end
+ bt.join("\n")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters.rb b/spec/mspec/lib/mspec/runner/filters.rb
new file mode 100644
index 0000000000..d0420faca6
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters.rb
@@ -0,0 +1,4 @@
+require 'mspec/runner/filters/match'
+require 'mspec/runner/filters/regexp'
+require 'mspec/runner/filters/tag'
+require 'mspec/runner/filters/profile'
diff --git a/spec/mspec/lib/mspec/runner/filters/match.rb b/spec/mspec/lib/mspec/runner/filters/match.rb
new file mode 100644
index 0000000000..539fd02d01
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/match.rb
@@ -0,0 +1,18 @@
+class MatchFilter
+ def initialize(what, *strings)
+ @what = what
+ @strings = strings
+ end
+
+ def ===(string)
+ @strings.any? { |s| string.include?(s) }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/profile.rb b/spec/mspec/lib/mspec/runner/filters/profile.rb
new file mode 100644
index 0000000000..a59722c451
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/profile.rb
@@ -0,0 +1,54 @@
+class ProfileFilter
+ def initialize(what, *files)
+ @what = what
+ @methods = load(*files)
+ @pattern = /([^ .#]+[.#])([^ ]+)/
+ end
+
+ def find(name)
+ return name if File.exist?(File.expand_path(name))
+
+ ["spec/profiles", "spec", "profiles", "."].each do |dir|
+ file = File.join dir, name
+ return file if File.exist? file
+ end
+ end
+
+ def parse(file)
+ pattern = /(\S+):\s*/
+ key = ""
+ file.inject(Hash.new { |h,k| h[k] = [] }) do |hash, line|
+ line.chomp!
+ if line[0,2] == "- "
+ hash[key] << line[2..-1].gsub(/[ '"]/, "")
+ elsif m = pattern.match(line)
+ key = m[1]
+ end
+ hash
+ end
+ end
+
+ def load(*files)
+ files.inject({}) do |hash, file|
+ next hash unless name = find(file)
+
+ File.open name, "r" do |f|
+ hash.merge parse(f)
+ end
+ end
+ end
+
+ def ===(string)
+ return false unless m = @pattern.match(string)
+ return false unless l = @methods[m[1]]
+ l.include? m[2]
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/regexp.rb b/spec/mspec/lib/mspec/runner/filters/regexp.rb
new file mode 100644
index 0000000000..097ec6a755
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/regexp.rb
@@ -0,0 +1,23 @@
+class RegexpFilter
+ def initialize(what, *regexps)
+ @what = what
+ @regexps = to_regexp(*regexps)
+ end
+
+ def ===(string)
+ @regexps.any? { |regexp| regexp === string }
+ end
+
+ def register
+ MSpec.register @what, self
+ end
+
+ def unregister
+ MSpec.unregister @what, self
+ end
+
+ def to_regexp(*regexps)
+ regexps.map { |str| Regexp.new str }
+ end
+ private :to_regexp
+end
diff --git a/spec/mspec/lib/mspec/runner/filters/tag.rb b/spec/mspec/lib/mspec/runner/filters/tag.rb
new file mode 100644
index 0000000000..c641c01606
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/filters/tag.rb
@@ -0,0 +1,29 @@
+class TagFilter
+ def initialize(what, *tags)
+ @what = what
+ @tags = tags
+ end
+
+ def load
+ @descriptions = MSpec.read_tags(@tags).map { |t| t.description }
+ MSpec.register @what, self
+ end
+
+ def unload
+ MSpec.unregister @what, self
+ end
+
+ def ===(string)
+ @descriptions.include?(string)
+ end
+
+ def register
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def unregister
+ MSpec.unregister :load, self
+ MSpec.unregister :unload, self
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters.rb b/spec/mspec/lib/mspec/runner/formatters.rb
new file mode 100644
index 0000000000..66f515ddff
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters.rb
@@ -0,0 +1,13 @@
+require 'mspec/runner/formatters/describe'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/file'
+require 'mspec/runner/formatters/specdoc'
+require 'mspec/runner/formatters/html'
+require 'mspec/runner/formatters/summary'
+require 'mspec/runner/formatters/unit'
+require 'mspec/runner/formatters/spinner'
+require 'mspec/runner/formatters/method'
+require 'mspec/runner/formatters/stats'
+require 'mspec/runner/formatters/yaml'
+require 'mspec/runner/formatters/profile'
+require 'mspec/runner/formatters/junit'
diff --git a/spec/mspec/lib/mspec/runner/formatters/base.rb b/spec/mspec/lib/mspec/runner/formatters/base.rb
new file mode 100644
index 0000000000..882e15c8c2
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/base.rb
@@ -0,0 +1,150 @@
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/actions/tally'
+require 'mspec/utils/options'
+
+if ENV['CHECK_LEAKS']
+ require 'mspec/runner/actions/leakchecker'
+end
+
+if ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']
+ require 'mspec/runner/actions/constants_leak_checker'
+end
+
+class BaseFormatter
+ attr_reader :exceptions, :timer, :tally
+
+ def initialize(out = nil)
+ @current_state = nil
+ @exception = false
+ @failure = false
+ @exceptions = []
+
+ @count = 0 # For subclasses
+
+ if out
+ @out = File.open out, "w"
+ else
+ @out = $stdout
+ end
+
+ err = MSpecOptions.latest && MSpecOptions.latest.config[:error_output]
+ if err
+ @err = (err == 'stderr') ? $stderr : File.open(err, "w")
+ else
+ @err = @out
+ end
+ end
+
+ # Creates the +TimerAction+ and +TallyAction+ instances and registers them.
+ def register
+ (@timer = TimerAction.new).register
+ (@tally = TallyAction.new).register
+ @counter = @tally.counter
+
+ if ENV['CHECK_LEAKS']
+ LeakCheckerAction.new.register
+ end
+
+ if (ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']) && ENV['CHECK_CONSTANT_LEAKS'] != 'false'
+ save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save'
+ ConstantsLeakCheckerAction.new(save).register
+ end
+
+ MSpec.register :abort, self
+ MSpec.register :before, self
+ MSpec.register :after, self
+ MSpec.register :exception, self
+ MSpec.register :finish, self
+ end
+
+ def abort
+ if @current_state
+ puts "\naborting example: #{@current_state.description}"
+ end
+ end
+
+ # Returns true if any exception is raised while running
+ # an example. This flag is reset before each example
+ # is evaluated.
+ def exception?
+ @exception
+ end
+
+ # Returns true if all exceptions during the evaluation
+ # of an example are failures rather than errors. See
+ # <tt>ExceptionState#failure</tt>. This flag is reset
+ # before each example is evaluated.
+ def failure?
+ @failure
+ end
+
+ # Callback for the MSpec :before event. Resets the
+ # +#exception?+ and +#failure+ flags.
+ def before(state = nil)
+ @current_state = state
+ @failure = @exception = false
+ end
+
+ # Callback for the MSpec :exception event. Stores the
+ # +ExceptionState+ object to generate the list of backtraces
+ # after all the specs are run. Also updates the internal
+ # +#exception?+ and +#failure?+ flags.
+ def exception(exception)
+ @count += 1
+ @failure = @exception ? @failure && exception.failure? : exception.failure?
+ @exception = true
+ @exceptions << exception
+ end
+
+ # Callback for the MSpec :after event.
+ def after(state = nil)
+ @current_state = nil
+ end
+
+ # Callback for the MSpec :start event. Calls :after event.
+ # Defined here, in the base class, and used by MultiFormatter.
+ def start
+ after
+ end
+
+ # Callback for the MSpec :unload event. Calls :after event.
+ # Defined here, in the base class, and used by MultiFormatter.
+ def unload
+ after
+ end
+
+ # Callback for the MSpec :finish event. Prints a description
+ # and backtrace for every exception that occurred while
+ # evaluating the examples.
+ def finish
+ print "\n"
+
+ if MSpecOptions.latest && MSpecOptions.latest.config[:print_skips]
+ print "\nSkips:\n" unless MSpec.skips.empty?
+ MSpec.skips.each do |skip, block|
+ print "#{skip.message} in #{(block.source_location || ['?']).join(':')}\n"
+ end
+ end
+
+ count = 0
+ @exceptions.each do |exc|
+ count += 1
+ print_exception(exc, count)
+ end
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+
+ def print_exception(exc, count)
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ @err.print "\n#{count})\n#{exc.description} #{outcome}\n"
+ @err.print exc.message, "\n"
+ @err.print exc.backtrace, "\n"
+ end
+
+ # A convenience method to allow printing to different outputs.
+ def print(*args)
+ @out.print(*args)
+ @out.flush
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/describe.rb b/spec/mspec/lib/mspec/runner/formatters/describe.rb
new file mode 100644
index 0000000000..fc4122d13b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/describe.rb
@@ -0,0 +1,23 @@
+require 'mspec/runner/formatters/dotted'
+
+class DescribeFormatter < DottedFormatter
+ # Callback for the MSpec :finish event. Prints a summary of
+ # the number of errors and failures for each +describe+ block.
+ def finish
+ describes = Hash.new { |h,k| h[k] = Tally.new }
+
+ @exceptions.each do |exc|
+ desc = describes[exc.describe]
+ exc.failure? ? desc.failures! : desc.errors!
+ end
+
+ print "\n"
+ describes.each do |d, t|
+ text = d.size > 40 ? "#{d[0,37]}..." : d.ljust(40)
+ print "\n#{text} #{t.failure}, #{t.error}"
+ end
+ print "\n" unless describes.empty?
+
+ print "\n#{@timer.format}\n\n#{@tally.format}\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/dotted.rb b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
new file mode 100644
index 0000000000..672cdf81dc
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/dotted.rb
@@ -0,0 +1,23 @@
+require 'mspec/runner/formatters/base'
+
+class DottedFormatter < BaseFormatter
+ def register
+ super
+ MSpec.register :after, self
+ end
+
+ # Callback for the MSpec :after event. Prints an indicator
+ # for the result of evaluating this example as follows:
+ # . = No failure or error
+ # F = An SpecExpectationNotMetError was raised
+ # E = Any exception other than SpecExpectationNotMetError
+ def after(state = nil)
+ super(state)
+
+ if exception?
+ print failure? ? "F" : "E"
+ else
+ print "."
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/file.rb b/spec/mspec/lib/mspec/runner/formatters/file.rb
new file mode 100644
index 0000000000..65cfb1f75b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/file.rb
@@ -0,0 +1,24 @@
+require 'mspec/runner/formatters/dotted'
+
+class FileFormatter < DottedFormatter
+ # Unregisters DottedFormatter#before, #after methods and
+ # registers #load, #unload, which perform the same duties
+ # as #before, #after in DottedFormatter.
+ def register
+ super
+
+ MSpec.unregister :before, self
+ MSpec.unregister :after, self
+
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ def load(state = nil)
+ before(state)
+ end
+
+ def unload(state = nil)
+ after(state)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/html.rb b/spec/mspec/lib/mspec/runner/formatters/html.rb
new file mode 100644
index 0000000000..e37e89a088
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/html.rb
@@ -0,0 +1,81 @@
+require 'mspec/runner/formatters/base'
+
+class HtmlFormatter < BaseFormatter
+ def register
+ super
+ MSpec.register :start, self
+ MSpec.register :enter, self
+ MSpec.register :leave, self
+ end
+
+ def start
+ print <<-EOH
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Spec Output For #{RUBY_ENGINE} (#{RUBY_VERSION})</title>
+<style type="text/css">
+ul {
+ list-style: none;
+}
+.fail {
+ color: red;
+}
+.pass {
+ color: green;
+}
+#details :target {
+ background-color: #ffffe0;
+}
+</style>
+</head>
+<body>
+EOH
+ end
+
+ def enter(describe)
+ print "<div><p>#{describe}</p>\n<ul>\n"
+ end
+
+ def leave
+ print "</ul>\n</div>\n"
+ end
+
+ def exception(exception)
+ super(exception)
+ outcome = exception.failure? ? "FAILED" : "ERROR"
+ print %[<li class="fail">- #{exception.it} (<a href="#details-#{@count}">]
+ print %[#{outcome} - #{@count}</a>)</li>\n]
+ end
+
+ def after(state = nil)
+ super(state)
+ print %[<li class="pass">- #{state.it}</li>\n] unless exception?
+ end
+
+ def finish
+ success = @exceptions.empty?
+ unless success
+ print "<hr>\n"
+ print %[<ol id="details">]
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print %[\n<li id="details-#{count += 1}"><p>#{escape(exc.description)} #{outcome}</p>\n<p>]
+ print escape(exc.message)
+ print "</p>\n<pre>\n"
+ print escape(exc.backtrace)
+ print "</pre>\n</li>\n"
+ end
+ print "</ol>\n"
+ end
+ print %[<p>#{@timer.format}</p>\n]
+ print %[<p class="#{success ? "pass" : "fail"}">#{@tally.format}</p>\n]
+ print "</body>\n</html>\n"
+ end
+
+ def escape(string)
+ string.gsub("&", "&nbsp;").gsub("<", "&lt;").gsub(">", "&gt;")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/junit.rb b/spec/mspec/lib/mspec/runner/formatters/junit.rb
new file mode 100644
index 0000000000..6351ccbce9
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/junit.rb
@@ -0,0 +1,87 @@
+require 'mspec/runner/formatters/yaml'
+
+class JUnitFormatter < YamlFormatter
+ def initialize(out = nil)
+ super(out)
+ @tests = []
+ end
+
+ def after(state = nil)
+ super(state)
+ @tests << {:test => state, :exception => false} unless exception?
+ end
+
+ def exception(exception)
+ super(exception)
+ @tests << {:test => exception, :exception => true}
+ end
+
+ def finish
+ switch
+
+ time = @timer.elapsed
+ tests = @tally.counter.examples
+ errors = @tally.counter.errors
+ failures = @tally.counter.failures
+
+ print <<-XML
+
+<?xml version="1.0" encoding="UTF-8" ?>
+ <testsuites
+ testCount="#{tests}"
+ errorCount="#{errors}"
+ failureCount="#{failures}"
+ timeCount="#{time}" time="#{time}">
+ <testsuite
+ tests="#{tests}"
+ errors="#{errors}"
+ failures="#{failures}"
+ time="#{time}"
+ name="Spec Output For #{::RUBY_ENGINE} (#{::RUBY_VERSION})">
+ XML
+ @tests.each do |h|
+ description = encode_for_xml h[:test].description
+
+ print <<-XML
+ <testcase classname="Spec" name="#{description}" time="0.0">
+ XML
+ if h[:exception]
+ outcome = h[:test].failure? ? "failure" : "error"
+ message = encode_for_xml h[:test].message
+ backtrace = encode_for_xml h[:test].backtrace
+ print <<-XML
+ <#{outcome} message="error in #{description}" type="#{outcome}">
+ #{message}
+ #{backtrace}
+ </#{outcome}>
+ XML
+ end
+ print <<-XML
+ </testcase>
+ XML
+ end
+
+ print <<-XML
+ </testsuite>
+ </testsuites>
+ XML
+ end
+
+ private
+ LT = "&lt;"
+ GT = "&gt;"
+ QU = "&quot;"
+ AP = "&apos;"
+ AM = "&amp;"
+ TARGET_ENCODING = "ISO-8859-1"
+
+ def encode_for_xml(str)
+ encode_as_latin1(str).gsub("<", LT).gsub(">", GT).
+ gsub('"', QU).gsub("'", AP).gsub("&", AM).
+ tr("\x00-\x08", "?")
+ end
+
+ def encode_as_latin1(str)
+ str.encode(TARGET_ENCODING, :undef => :replace, :invalid => :replace)
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/launchable.rb b/spec/mspec/lib/mspec/runner/formatters/launchable.rb
new file mode 100644
index 0000000000..f738781c71
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/launchable.rb
@@ -0,0 +1,88 @@
+module LaunchableFormatter
+ def self.extend_object(obj)
+ super
+ obj.init
+ end
+
+ def self.setDir(dir)
+ @@path = File.join(dir, "#{rand.to_s}.json")
+ self
+ end
+
+ def init
+ @timer = nil
+ @tests = []
+ end
+
+ def before(state = nil)
+ super
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def after(state = nil)
+ super
+ @timer.finish
+ file = MSpec.file
+ return if file.nil? || state&.example.nil? || exception?
+
+ @tests << {:test => state, :file => file, :exception => false, duration: @timer.elapsed}
+ end
+
+ def exception(exception)
+ super
+ @timer.finish
+ file = MSpec.file
+ return if file.nil?
+
+ @tests << {:test => exception, :file => file, :exception => true, duration: @timer.elapsed}
+ end
+
+ def finish
+ super
+
+ require_relative '../../../../../../tool/lib/launchable'
+
+ @writer = writer = Launchable::JsonStreamWriter.new(@@path)
+ @writer.write_array('testCases')
+ at_exit {
+ @writer.close
+ }
+
+ repo_path = File.expand_path("#{__dir__}/../../../../../../")
+
+ @tests.each do |t|
+ testcase = t[:test].description
+ relative_path = t[:file].delete_prefix("#{repo_path}/")
+ # The test path is a URL-encoded representation.
+ # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18
+ test_path = {file: relative_path, testcase: testcase}.map{|key, val|
+ "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}"
+ }.join('#')
+
+ status = 'TEST_PASSED'
+ if t[:exception]
+ message = t[:test].message
+ backtrace = t[:test].backtrace
+ e = "#{message}\n#{backtrace}"
+ status = 'TEST_FAILED'
+ end
+
+ @writer.write_object(
+ {
+ testPath: test_path,
+ status: status,
+ duration: t[:duration],
+ createdAt: Time.now.to_s,
+ stderr: e,
+ stdout: nil
+ }
+ )
+ end
+ end
+
+ private
+ def encode_test_path_component component
+ component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26').tr("\x00-\x08", "")
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/method.rb b/spec/mspec/lib/mspec/runner/formatters/method.rb
new file mode 100644
index 0000000000..925858c845
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/method.rb
@@ -0,0 +1,95 @@
+require 'mspec/runner/formatters/base'
+
+class MethodFormatter < BaseFormatter
+ attr_accessor :methods
+
+ def initialize(out = nil)
+ super(out)
+ @methods = Hash.new do |h, k|
+ h[k] = {
+ examples: 0,
+ expectations: 0,
+ failures: 0,
+ errors: 0,
+ exceptions: []
+ }
+ end
+ end
+
+ # Returns the type of method as a "class", "instance",
+ # or "unknown".
+ def method_type(sep)
+ case sep
+ when '.', '::'
+ "class"
+ when '#'
+ "instance"
+ else
+ "unknown"
+ end
+ end
+
+ # Callback for the MSpec :before event. Parses the
+ # describe string into class and method if possible.
+ # Resets the tallies so the counts are only for this
+ # example.
+ def before(state)
+ super(state)
+
+ # The pattern for a method name is not correctly
+ # restrictive but it is simplistic and useful
+ # for our purpose.
+ /^([A-Za-z_]+\w*)(\.|#|::)([^ ]+)/ =~ state.describe
+ @key = $1 && $2 && $3 ? "#{$1}#{$2}#{$3}" : state.describe
+
+ unless methods.key? @key
+ h = methods[@key]
+ h[:class] = "#{$1}"
+ h[:method] = "#{$3}"
+ h[:type] = method_type $2
+ h[:description] = state.description
+ end
+
+ tally.counter.examples = 0
+ tally.counter.expectations = 0
+ tally.counter.failures = 0
+ tally.counter.errors = 0
+
+ @exceptions = []
+ end
+
+ # Callback for the MSpec :after event. Sets or adds to
+ # tallies for the example block.
+ def after(state = nil)
+ super(state)
+
+ h = methods[@key]
+ h[:examples] += tally.counter.examples
+ h[:expectations] += tally.counter.expectations
+ h[:failures] += tally.counter.failures
+ h[:errors] += tally.counter.errors
+ @exceptions.each do |exc|
+ h[:exceptions] << "#{exc.message}\n#{exc.backtrace}\n"
+ end
+ end
+
+ # Callback for the MSpec :finish event. Prints out the
+ # summary information in YAML format for all the methods.
+ def finish
+ print "---\n"
+
+ methods.each do |key, hash|
+ print key.inspect, ":\n"
+ print " class: ", hash[:class].inspect, "\n"
+ print " method: ", hash[:method].inspect, "\n"
+ print " type: ", hash[:type], "\n"
+ print " description: ", hash[:description].inspect, "\n"
+ print " examples: ", hash[:examples], "\n"
+ print " expectations: ", hash[:expectations], "\n"
+ print " failures: ", hash[:failures], "\n"
+ print " errors: ", hash[:errors], "\n"
+ print " exceptions:\n"
+ hash[:exceptions].each { |exc| print " - ", exc.inspect, "\n" }
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/multi.rb b/spec/mspec/lib/mspec/runner/formatters/multi.rb
new file mode 100644
index 0000000000..fa1da3766b
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/multi.rb
@@ -0,0 +1,47 @@
+module MultiFormatter
+ def self.extend_object(obj)
+ super
+ obj.multi_initialize
+ end
+
+ def multi_initialize
+ @tally = TallyAction.new
+ @counter = @tally.counter
+ @timer = TimerAction.new
+ @timer.start
+ end
+
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ MSpec.unregister :before, self
+ end
+
+ def aggregate_results(files)
+ require 'yaml'
+
+ @timer.finish
+ @exceptions = []
+
+ files.each do |file|
+ contents = File.read(file)
+ d = YAML.load(contents)
+ File.delete file
+
+ if d # The file might be empty if the child process died
+ @exceptions += Array(d['exceptions'])
+ @counter.files! d['files']
+ @counter.examples! d['examples']
+ @counter.expectations! d['expectations']
+ @counter.errors! d['errors']
+ @counter.failures! d['failures']
+ end
+ end
+ end
+
+ def print_exception(exc, count)
+ @err.print "\n#{count})\n#{exc}\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/profile.rb b/spec/mspec/lib/mspec/runner/formatters/profile.rb
new file mode 100644
index 0000000000..38ef5b12ed
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/profile.rb
@@ -0,0 +1,18 @@
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/actions/profile'
+
+class ProfileFormatter < DottedFormatter
+ def initialize(out = nil)
+ super(out)
+
+ @describe_name = nil
+ @describe_time = nil
+ @describes = []
+ @its = []
+ end
+
+ def register
+ (@profile = ProfileAction.new).register
+ super
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/specdoc.rb b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
new file mode 100644
index 0000000000..d3a5c3d729
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/specdoc.rb
@@ -0,0 +1,41 @@
+require 'mspec/runner/formatters/base'
+
+class SpecdocFormatter < BaseFormatter
+ def register
+ super
+ MSpec.register :enter, self
+ end
+
+ # Callback for the MSpec :enter event. Prints the
+ # +describe+ block string.
+ def enter(describe)
+ print "\n#{describe}\n"
+ end
+
+ # Callback for the MSpec :before event. Prints the
+ # +it+ block string.
+ def before(state)
+ super(state)
+ print "- #{state.it}"
+ end
+
+ # Callback for the MSpec :exception event. Prints
+ # either 'ERROR - X' or 'FAILED - X' where _X_ is
+ # the sequential number of the exception raised. If
+ # there has already been an exception raised while
+ # evaluating this example, it prints another +it+
+ # block description string so that each description
+ # string has an associated 'ERROR' or 'FAILED'
+ def exception(exception)
+ print "\n- #{exception.it}" if exception?
+ super(exception)
+ print " (#{exception.failure? ? 'FAILED' : 'ERROR'} - #{@count})"
+ end
+
+ # Callback for the MSpec :after event. Prints a
+ # newline to finish the description string output.
+ def after(state = nil)
+ super(state)
+ print "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/spinner.rb b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
new file mode 100644
index 0000000000..817d8c02be
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/spinner.rb
@@ -0,0 +1,111 @@
+require 'mspec/runner/formatters/base'
+
+class SpinnerFormatter < BaseFormatter
+ attr_reader :length
+
+ Spins = %w!| / - \\!
+ HOUR = 3600
+ MIN = 60
+
+ def initialize(out = nil)
+ super(nil)
+
+ @which = 0
+ @loaded = 0
+ self.length = 40
+ @percent = 0
+ @start = Time.now
+
+ term = ENV['TERM']
+ @color = (term != "dumb")
+ @fail_color = "32"
+ @error_color = "32"
+ end
+
+ def register
+ super
+
+ MSpec.register :start, self
+ MSpec.register :unload, self
+ end
+
+ def length=(length)
+ @length = length
+ @ratio = 100.0 / length
+ @position = length / 2 - 2
+ end
+
+ def compute_etr
+ return @etr = "00:00:00" if @percent == 0
+ elapsed = Time.now - @start
+ remain = (100 * elapsed / @percent) - elapsed
+
+ hour = remain >= HOUR ? (remain / HOUR).to_i : 0
+ remain -= hour * HOUR
+ min = remain >= MIN ? (remain / MIN).to_i : 0
+ sec = remain - min * MIN
+
+ @etr = "%02d:%02d:%02d" % [hour, min, sec]
+ end
+
+ def compute_percentage
+ @percent = @loaded * 100 / @total
+ bar = ("=" * (@percent / @ratio)).ljust @length
+ label = "%d%%" % @percent
+ bar[@position, label.size] = label
+ @bar = bar
+ end
+
+ def compute_progress
+ compute_percentage
+ compute_etr
+ end
+
+ def progress_line
+ @which = (@which + 1) % Spins.size
+ data = [Spins[@which], @bar, @etr, @counter.failures, @counter.errors]
+ if @color
+ "\r[%s | %s | %s] \e[0;#{@fail_color}m%6dF \e[0;#{@error_color}m%6dE\e[0m " % data
+ else
+ "\r[%s | %s | %s] %6dF %6dE " % data
+ end
+ end
+
+ def clear_progress_line
+ print "\r#{' '*progress_line.length}"
+ end
+
+ # Callback for the MSpec :start event. Stores the total
+ # number of files that will be processed.
+ def start
+ @total = MSpec.files_array.size
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :unload event. Increments the number
+ # of files that have been run.
+ def unload
+ @loaded += 1
+ compute_progress
+ print progress_line
+ end
+
+ # Callback for the MSpec :exception event. Changes the color
+ # used to display the tally of errors and failures
+ def exception(exception)
+ super
+ @fail_color = "31" if exception.failure?
+ @error_color = "33" unless exception.failure?
+
+ clear_progress_line
+ print_exception(exception, @count)
+ exceptions.clear
+ end
+
+ # Callback for the MSpec :after event. Updates the spinner.
+ def after(state = nil)
+ super(state)
+ print progress_line
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/stats.rb b/spec/mspec/lib/mspec/runner/formatters/stats.rb
new file mode 100644
index 0000000000..8cff96d145
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/stats.rb
@@ -0,0 +1,57 @@
+require 'mspec/runner/formatters/base'
+
+class StatsPerFileFormatter < BaseFormatter
+ def initialize(out = nil)
+ super(out)
+ @data = {}
+ @root = File.expand_path(MSpecScript.get(:prefix) || '.')
+ end
+
+ def register
+ super
+ MSpec.register :load, self
+ MSpec.register :unload, self
+ end
+
+ # Resets the tallies so the counts are only for this file.
+ def load
+ tally.counter.examples = 0
+ tally.counter.errors = 0
+ tally.counter.failures = 0
+ tally.counter.tagged = 0
+ end
+
+ def unload
+ file = format_file MSpec.file
+
+ raise if @data.key?(file)
+ @data[file] = {
+ examples: tally.counter.examples,
+ errors: tally.counter.errors,
+ failures: tally.counter.failures,
+ tagged: tally.counter.tagged,
+ }
+ end
+
+ def finish
+ width = @data.keys.max_by(&:size).size
+ f = "%3d"
+ @data.each_pair do |file, data|
+ total = data[:examples]
+ passing = total - data[:errors] - data[:failures] - data[:tagged]
+ puts "#{file.ljust(width)} #{f % passing}/#{f % total}"
+ end
+
+ require 'yaml'
+ yaml = YAML.dump(@data)
+ File.write "results-#{RUBY_ENGINE}-#{RUBY_ENGINE_VERSION}.yml", yaml
+ end
+
+ private def format_file(file)
+ if file.start_with?(@root)
+ file[@root.size+1..-1]
+ else
+ raise file
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/summary.rb b/spec/mspec/lib/mspec/runner/formatters/summary.rb
new file mode 100644
index 0000000000..41819d2158
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/summary.rb
@@ -0,0 +1,4 @@
+require 'mspec/runner/formatters/base'
+
+class SummaryFormatter < BaseFormatter
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/unit.rb b/spec/mspec/lib/mspec/runner/formatters/unit.rb
new file mode 100644
index 0000000000..d03ae79e9f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/unit.rb
@@ -0,0 +1,20 @@
+require 'mspec/runner/formatters/dotted'
+
+class UnitdiffFormatter < DottedFormatter
+ def finish
+ print "\n\n#{@timer.format}\n"
+ count = 0
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ print "\n#{count += 1})\n#{exc.description} #{outcome}\n"
+ print exc.message, ":\n"
+ print exc.backtrace, "\n"
+ end
+ print "\n#{@tally.format}\n"
+ end
+
+ def backtrace(exc)
+ exc.backtrace && exc.backtrace.join("\n")
+ end
+ private :backtrace
+end
diff --git a/spec/mspec/lib/mspec/runner/formatters/yaml.rb b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
new file mode 100644
index 0000000000..6c05cc902f
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/formatters/yaml.rb
@@ -0,0 +1,38 @@
+require 'mspec/runner/formatters/base'
+
+class YamlFormatter < BaseFormatter
+ def initialize(out = nil)
+ super(nil)
+
+ if out.nil?
+ @finish = $stdout
+ else
+ @finish = File.open out, "w"
+ end
+ end
+
+ def switch
+ @out = @finish
+ end
+
+ def finish
+ switch
+
+ print "---\n"
+ print "exceptions:\n"
+ @exceptions.each do |exc|
+ outcome = exc.failure? ? "FAILED" : "ERROR"
+ str = "#{exc.description} #{outcome}\n"
+ str << exc.message << "\n" << exc.backtrace
+ print "- ", str.inspect, "\n"
+ end
+
+ print "time: ", @timer.elapsed, "\n"
+ print "files: ", @tally.counter.files, "\n"
+ print "examples: ", @tally.counter.examples, "\n"
+ print "expectations: ", @tally.counter.expectations, "\n"
+ print "failures: ", @tally.counter.failures, "\n"
+ print "errors: ", @tally.counter.errors, "\n"
+ print "tagged: ", @tally.counter.tagged, "\n"
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/mspec.rb b/spec/mspec/lib/mspec/runner/mspec.rb
new file mode 100644
index 0000000000..0e016c67a7
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/mspec.rb
@@ -0,0 +1,424 @@
+require 'mspec/runner/context'
+require 'mspec/runner/exception'
+require 'mspec/runner/tag'
+
+module MSpec
+end
+
+class MSpecEnv
+ include MSpec
+end
+
+module MSpec
+ @exit = nil
+ @abort = nil
+ @start = nil
+ @enter = nil
+ @before = nil
+ @add = nil
+ @after = nil
+ @leave = nil
+ @finish = nil
+ @exclude = []
+ @include = []
+ @leave = nil
+ @load = nil
+ @unload = nil
+ @tagged = nil
+ @current = nil
+ @passed = nil
+ @example = nil
+ @modes = []
+ @shared = {}
+ @guarded = []
+ @features = {}
+ @exception = nil
+ @randomize = false
+ @repeat = 1
+ @expectation = nil
+ @expectations = false
+ @skips = []
+ @subprocesses = []
+
+ class << self
+ attr_reader :file, :include, :exclude, :skips, :subprocesses
+ attr_writer :repeat, :randomize
+ attr_accessor :formatter
+ end
+
+ def self.describe(description, options = nil, &block)
+ state = ContextState.new description, options
+ state.parent = current
+
+ MSpec.register_current state
+ state.describe(&block)
+
+ state.process unless state.shared? or current
+ end
+
+ def self.process
+ STDOUT.puts RUBY_DESCRIPTION
+ STDOUT.flush
+
+ actions :start
+ files
+ actions :finish
+ end
+
+ def self.files_array
+ @files
+ end
+
+ def self.each_file(&block)
+ if ENV["MSPEC_MULTI"]
+ while file = STDIN.gets
+ file = file.chomp
+ return if file == "QUIT"
+ yield file
+ begin
+ STDOUT.print "."
+ STDOUT.flush
+ rescue Errno::EPIPE
+ # The parent died
+ exit 1
+ end
+ end
+ # The parent closed the connection without QUIT
+ abort "the parent did not send QUIT"
+ else
+ return unless files = @files
+ shuffle files if randomize?
+ files.each(&block)
+ end
+ end
+
+ def self.files
+ each_file do |file|
+ setup_env
+ @file = file
+ actions :load
+ protect("loading #{file}") { Kernel.load file }
+ actions :unload
+ raise "#{file} was executed but did not reset the current example: #{@current}" if @current
+ end
+ end
+
+ def self.setup_env
+ @env = MSpecEnv.new
+ end
+
+ def self.actions(action, *args)
+ actions = retrieve(action)
+ actions.each { |obj| obj.send action, *args } if actions
+ end
+
+ def self.protect(location, &block)
+ begin
+ @env.instance_exec(&block)
+ return true
+ rescue SystemExit => e
+ raise e
+ rescue SkippedSpecError => e
+ @skips << [e, block]
+ return false
+ rescue Object => exc
+ register_exit 1
+ actions :exception, ExceptionState.new(current && current.state, location, exc)
+ return false
+ end
+ end
+
+ # Guards can be nested, so a stack is necessary to know when we have
+ # exited the toplevel guard.
+ def self.guard
+ @guarded << true
+ end
+
+ def self.unguard
+ @guarded.pop
+ end
+
+ def self.guarded?
+ !@guarded.empty?
+ end
+
+ # Sets the toplevel ContextState to +state+.
+ def self.register_current(state)
+ @current = state
+ end
+
+ # Sets the toplevel ContextState to +nil+.
+ def self.clear_current
+ @current = nil
+ end
+
+ # Returns the toplevel ContextState.
+ def self.current
+ @current
+ end
+
+ # Stores the shared ContextState keyed by description.
+ def self.register_shared(state)
+ name = state.to_s
+ raise "duplicated shared #describe: #{name}" if @shared.key?(name)
+ @shared[name] = state
+ end
+
+ # Returns the shared ContextState matching description.
+ def self.retrieve_shared(desc)
+ @shared[desc.to_s]
+ end
+
+ # Stores the exit code used by the runner scripts.
+ def self.register_exit(code)
+ @exit = code
+ end
+
+ # Retrieves the stored exit code.
+ def self.exit_code
+ @exit.to_i
+ end
+
+ # Stores the list of files to be evaluated.
+ def self.register_files(files)
+ @files = files
+ end
+
+ # Stores one or more substitution patterns for transforming
+ # a spec filename into a tags filename, where each pattern
+ # has the form:
+ #
+ # [Regexp, String]
+ #
+ # See also +tags_file+.
+ def self.register_tags_patterns(patterns)
+ @tags_patterns = patterns
+ end
+
+ # Registers an operating mode. Modes recognized by MSpec:
+ #
+ # :pretend - actions execute but specs are not run
+ # :verify - specs are run despite guards and the result is
+ # verified to match the expectation of the guard
+ # :report - specs that are guarded are reported
+ # :unguarded - all guards are forced off
+ def self.register_mode(mode)
+ modes = @modes
+ modes << mode unless modes.include? mode
+ end
+
+ # Clears all registered modes.
+ def self.clear_modes
+ @modes = []
+ end
+
+ # Returns +true+ if +mode+ is registered.
+ def self.mode?(mode)
+ @modes.include? mode
+ end
+
+ def self.enable_feature(feature)
+ @features[feature] = true
+ end
+
+ def self.disable_feature(feature)
+ @features[feature] = false
+ end
+
+ def self.feature_enabled?(feature)
+ @features[feature] || false
+ end
+
+ def self.retrieve(symbol)
+ instance_variable_get :"@#{symbol}"
+ end
+
+ def self.store(symbol, value)
+ instance_variable_set :"@#{symbol}", value
+ end
+
+ # This method is used for registering actions that are
+ # run at particular points in the spec cycle:
+ # :start before any specs are run
+ # :load before a spec file is loaded
+ # :enter before a describe block is run
+ # :before before a single spec is run
+ # :add while a describe block is adding examples to run later
+ # :expectation before a 'should', 'should_receive', etc.
+ # :passed after an example block is run and passes, passed the block, run before :example action
+ # :example after an example block is run, passed the block
+ # :exception after an exception is rescued
+ # :after after a single spec is run
+ # :leave after a describe block is run
+ # :unload after a spec file is run
+ # :finish after all specs are run
+ #
+ # Objects registered as actions above should respond to
+ # a method of the same name. For example, if an object
+ # is registered as a :start action, it should respond to
+ # a #start method call.
+ #
+ # Additionally, there are two "action" lists for
+ # filtering specs:
+ # :include return true if the spec should be run
+ # :exclude return true if the spec should NOT be run
+ #
+ def self.register(symbol, action)
+ unless value = retrieve(symbol)
+ value = store symbol, []
+ end
+ value << action unless value.include? action
+ end
+
+ def self.unregister(symbol, action)
+ if value = retrieve(symbol)
+ value.delete action
+ end
+ end
+
+ def self.randomize?
+ @randomize
+ end
+
+ def self.repeat
+ if @repeat == 1
+ yield
+ else
+ @repeat.times do
+ yield
+ end
+ end
+ end
+
+ def self.shuffle(ary)
+ return if ary.empty?
+
+ size = ary.size
+ size.times do |i|
+ r = rand(size - i - 1)
+ ary[i], ary[r] = ary[r], ary[i]
+ end
+ end
+
+ # Records that an expectation has been encountered in an example.
+ def self.expectation
+ @expectations = true
+ end
+
+ # Returns true if an expectation has been encountered
+ def self.expectation?
+ @expectations
+ end
+
+ # Resets the flag that an expectation has been encountered in an example.
+ def self.clear_expectations
+ @expectations = false
+ end
+
+ # Transforms a spec filename into a tags filename by applying each
+ # substitution pattern in :tags_pattern. The default patterns are:
+ #
+ # [%r(/spec/), '/spec/tags/'], [/_spec.rb$/, '_tags.txt']
+ #
+ # which will perform the following transformation:
+ #
+ # path/to/spec/class/method_spec.rb => path/to/spec/tags/class/method_tags.txt
+ #
+ # See also +register_tags_patterns+.
+ def self.tags_file
+ patterns = @tags_patterns ||
+ [[%r(spec/), 'spec/tags/'], [/_spec.rb$/, '_tags.txt']]
+ patterns.inject(@file.dup) do |file, pattern|
+ file.gsub(*pattern)
+ end
+ end
+
+ # Returns a list of tags matching any tag string in +keys+ based
+ # on the return value of <tt>keys.include?("tag_name")</tt>
+ def self.read_tags(keys)
+ tags = []
+ file = tags_file
+ if File.exist? file
+ File.open(file, "r:utf-8") do |f|
+ f.each_line do |line|
+ line.chomp!
+ next if line.empty?
+ tag = SpecTag.new line
+ tags << tag if keys.include? tag.tag
+ end
+ end
+ end
+ tags
+ end
+
+ def self.make_tag_dir(path)
+ parent = File.dirname(path)
+ return if File.exist? parent
+ begin
+ Dir.mkdir(parent)
+ rescue SystemCallError
+ make_tag_dir(parent)
+ Dir.mkdir(parent)
+ end
+ end
+
+ # Writes each tag in +tags+ to the tag file. Overwrites the
+ # tag file if it exists.
+ def self.write_tags(tags)
+ return delete_tags if tags.empty?
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "w:utf-8") do |f|
+ tags.each { |t| f.puts t }
+ end
+ end
+
+ # Writes +tag+ to the tag file if it does not already exist.
+ # Returns +true+ if the tag is written, +false+ otherwise.
+ def self.write_tag(tag)
+ tags = read_tags([tag.tag])
+ tags.each do |t|
+ if t.tag == tag.tag and t.description == tag.description
+ return false
+ end
+ end
+
+ file = tags_file
+ make_tag_dir(file)
+ File.open(file, "a:utf-8") { |f| f.puts tag.to_s }
+ return true
+ end
+
+ # Deletes +tag+ from the tag file if it exists. Returns +true+
+ # if the tag is deleted, +false+ otherwise. Deletes the tag
+ # file if it is empty.
+ def self.delete_tag(tag)
+ deleted = false
+ desc = tag.escape(tag.description)
+ file = tags_file
+ if File.exist? file
+ lines = File.readlines(file)
+ File.open(file, "w:utf-8") do |f|
+ lines.each do |line|
+ line = line.chomp
+ if line.start_with?(tag.tag) and line.end_with?(desc)
+ deleted = true
+ else
+ f.puts line unless line.empty?
+ end
+ end
+ end
+ File.delete file unless File.size? file
+ end
+ return deleted
+ end
+
+ # Removes the tag file associated with a spec file.
+ def self.delete_tags
+ file = tags_file
+ File.delete file if File.exist? file
+ end
+
+ # Initialize @env
+ setup_env
+end
diff --git a/spec/mspec/lib/mspec/runner/object.rb b/spec/mspec/lib/mspec/runner/object.rb
new file mode 100644
index 0000000000..58d98cc4df
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/object.rb
@@ -0,0 +1,26 @@
+class Object
+ private def before(at = :each, &block)
+ MSpec.current.before at, &block
+ end
+
+ private def after(at = :each, &block)
+ MSpec.current.after at, &block
+ end
+
+ private def describe(description, options = nil, &block)
+ MSpec.describe description, options, &block
+ end
+
+ private def it(desc, &block)
+ MSpec.current.it desc, &block
+ end
+
+ private def it_should_behave_like(desc)
+ MSpec.current.it_should_behave_like desc
+ end
+
+ alias_method :context, :describe
+ private :context
+ alias_method :specify, :it
+ private :specify
+end
diff --git a/spec/mspec/lib/mspec/runner/parallel.rb b/spec/mspec/lib/mspec/runner/parallel.rb
new file mode 100644
index 0000000000..6a9ecd155d
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/parallel.rb
@@ -0,0 +1,98 @@
+class ParallelRunner
+ def initialize(files, processes, formatter, argv)
+ @files = files
+ @processes = processes
+ @formatter = formatter
+ @argv = argv
+ @last_files = {}
+ @output_files = []
+ @success = true
+ end
+
+ def launch_children
+ @children = @processes.times.map { |i|
+ name = tmp "mspec-multi-#{i}"
+ @output_files << name
+
+ env = {
+ "SPEC_TEMP_DIR" => "#{SPEC_TEMP_DIR}_#{i}",
+ "MSPEC_MULTI" => i.to_s
+ }
+ command = @argv + ["-fy", "-o", name]
+ $stderr.puts "$ #{command.join(' ')}" if $MSPEC_DEBUG
+ IO.popen([env, *command, close_others: false], "rb+")
+ }
+ end
+
+ def handle(child, message)
+ case message
+ when '.'
+ @formatter.unload
+ send_new_file_or_quit(child)
+ else
+ if message == nil
+ msg = "A child mspec-run process died unexpectedly"
+ else
+ msg = "A child mspec-run process printed unexpected output on STDOUT"
+ while chunk = (child.read_nonblock(4096) rescue nil)
+ message += chunk
+ end
+ message.chomp!('.')
+ msg += ": #{message.inspect}"
+ end
+
+ if last_file = @last_files[child]
+ msg += " while running #{last_file}"
+ end
+
+ @success = false
+ quit(child)
+ abort "\n#{msg}"
+ end
+ end
+
+ def quit(child)
+ begin
+ child.puts "QUIT"
+ rescue Errno::EPIPE
+ # The child process already died
+ end
+ _pid, status = Process.wait2(child.pid)
+ @success &&= status.success?
+ child.close
+ @children.delete(child)
+ end
+
+ def send_new_file_or_quit(child)
+ if @files.empty?
+ quit(child)
+ else
+ file = @files.shift
+ @last_files[child] = file
+ child.puts file
+ end
+ end
+
+ def run
+ MSpec.register_files @files
+ launch_children
+
+ puts @children.map { |child| child.gets }.uniq
+ @formatter.start
+ begin
+ @children.each { |child| send_new_file_or_quit(child) }
+
+ until @children.empty?
+ IO.select(@children)[0].each { |child|
+ handle(child, child.read(1))
+ }
+ end
+ ensure
+ @children.dup.each { |child| quit(child) }
+ @formatter.aggregate_results(@output_files)
+ @formatter.finish
+ end
+
+ @success
+ end
+end
diff --git a/spec/mspec/lib/mspec/runner/shared.rb b/spec/mspec/lib/mspec/runner/shared.rb
new file mode 100644
index 0000000000..283711c1d7
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/shared.rb
@@ -0,0 +1,14 @@
+require 'mspec/runner/mspec'
+
+def it_behaves_like(desc, meth, obj = nil)
+ before :all do
+ @method = meth
+ @object = obj
+ end
+ after :all do
+ @method = nil
+ @object = nil
+ end
+
+ it_should_behave_like desc.to_s
+end
diff --git a/spec/mspec/lib/mspec/runner/tag.rb b/spec/mspec/lib/mspec/runner/tag.rb
new file mode 100644
index 0000000000..820df9159e
--- /dev/null
+++ b/spec/mspec/lib/mspec/runner/tag.rb
@@ -0,0 +1,38 @@
+class SpecTag
+ attr_accessor :tag, :comment, :description
+
+ def initialize(string = nil)
+ parse(string) if string
+ end
+
+ def parse(string)
+ m = /^([^()#:]+)(\(([^)]+)?\))?:(.*)$/.match string
+ @tag, @comment, description = m.values_at(1, 3, 4) if m
+ @description = unescape description
+ end
+
+ def unescape(str)
+ return unless str
+ if str[0] == ?" and str[-1] == ?"
+ str[1..-2].gsub('\n', "\n")
+ else
+ str
+ end
+ end
+
+ def escape(str)
+ if str.include? "\n"
+ %["#{str.gsub("\n", '\n')}"]
+ else
+ str
+ end
+ end
+
+ def to_s
+ "#{@tag}#{ "(#{@comment})" if @comment }:#{escape @description}"
+ end
+
+ def ==(o)
+ @tag == o.tag and @comment == o.comment and @description == o.description
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/deprecate.rb b/spec/mspec/lib/mspec/utils/deprecate.rb
new file mode 100644
index 0000000000..1db843b329
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/deprecate.rb
@@ -0,0 +1,6 @@
+module MSpec
+ def self.deprecate(what, replacement)
+ user_caller = caller.find { |line| !line.include?('lib/mspec') }
+ $stderr.puts "\n#{what} is deprecated, use #{replacement} instead.\nfrom #{user_caller}"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/format.rb b/spec/mspec/lib/mspec/utils/format.rb
new file mode 100644
index 0000000000..425dd4d11c
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/format.rb
@@ -0,0 +1,24 @@
+# If the implementation on which the specs are run cannot
+# load pp from the standard library, add a pp.rb file that
+# defines the #pretty_inspect method on Object or Kernel.
+begin
+ require 'pp'
+rescue LoadError
+ module Kernel
+ def pretty_inspect
+ inspect
+ end
+ end
+end
+
+module MSpec
+ def self.format(obj)
+ if String === obj and obj.include?("\n")
+ "\n#{obj.inspect.gsub('\n', "\n")}"
+ else
+ obj.pretty_inspect.chomp
+ end
+ rescue => e
+ "#<#{obj.class}>(#pretty_inspect raised #{e.inspect})"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/name_map.rb b/spec/mspec/lib/mspec/utils/name_map.rb
new file mode 100644
index 0000000000..9b04112e2e
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/name_map.rb
@@ -0,0 +1,133 @@
+class NameMap
+ MAP = {
+ '`' => 'backtick',
+ '+' => 'plus',
+ '-' => 'minus',
+ '+@' => 'uplus',
+ '-@' => 'uminus',
+ '*' => 'multiply',
+ '/' => 'divide',
+ '%' => 'modulo',
+ '<<' => {'Integer' => 'left_shift',
+ 'IO' => 'output',
+ :default => 'append' },
+ '>>' => 'right_shift',
+ '<' => 'lt',
+ '<=' => 'lte',
+ '>' => 'gt',
+ '>=' => 'gte',
+ '=' => 'assignment',
+ '==' => 'equal_value',
+ '===' => 'case_compare',
+ '<=>' => 'comparison',
+ '[]' => 'element_reference',
+ '[]=' => 'element_set',
+ '**' => 'exponent',
+ '!' => 'not',
+ '~' => {'Integer' => 'complement',
+ :default => 'match' },
+ '!=' => 'not_equal',
+ '!~' => 'not_match',
+ '=~' => 'match',
+ '&' => {'Integer' => 'bit_and',
+ 'Array' => 'intersection',
+ 'Set' => 'intersection',
+ :default => 'and' },
+ '|' => {'Integer' => 'bit_or',
+ 'Array' => 'union',
+ 'Set' => 'union',
+ :default => 'or' },
+ '^' => {'Integer' => 'bit_xor',
+ 'Set' => 'exclusion',
+ :default => 'xor' },
+ }
+
+ EXCLUDED = %w[
+ MSpecScript
+ MkSpec
+ MSpecOption
+ MSpecOptions
+ NameMap
+ SpecVersion
+ ]
+
+ ALWAYS_PRIVATE = %w[
+ initialize initialize_copy initialize_clone initialize_dup respond_to_missing?
+ ].map(&:to_sym)
+
+ def initialize(filter = false)
+ @seen = {}
+ @filter = filter
+ end
+
+ def exception?(name)
+ return false unless c = class_or_module(name)
+ c == Errno or c.ancestors.include? Exception
+ end
+
+ def class_or_module(c)
+ begin
+ const = Object.const_get(c, false)
+ rescue NameError, RuntimeError
+ # Either the constant doesn't exist or it is
+ # explicitly raising an error, like `SortedSet`.
+ return nil
+ end
+ return nil unless Module === const
+
+ filtered = @filter && EXCLUDED.include?(const.name)
+ return const unless filtered
+ end
+
+ def namespace(mod, const)
+ return const.to_s if mod.nil? or %w[Object Class Module].include? mod
+ "#{mod}::#{const}"
+ end
+
+ def map(hash, constants, mod = nil)
+ @seen = {} unless mod
+
+ constants.each do |const|
+ name = namespace mod, const
+ m = class_or_module name
+ next unless m and !@seen[m]
+ @seen[m] = true
+
+ ms = m.methods(false).map { |x| x.to_s }
+ hash["#{name}."] = ms.sort unless ms.empty?
+
+ ms = m.public_instance_methods(false) +
+ m.protected_instance_methods(false) +
+ (m.private_instance_methods(false) & ALWAYS_PRIVATE)
+ ms.map! { |x| x.to_s }
+ hash["#{name}#"] = ms.sort unless ms.empty?
+
+ map hash, m.constants(false), name
+ end
+
+ hash
+ end
+
+ def dir_name(c, base)
+ return File.join(base, 'exception') if exception? c
+
+ c.split('::').inject(base) do |dir, name|
+ name.gsub!(/Class/, '') unless name == 'Class'
+ File.join dir, name.downcase
+ end
+ end
+
+ def file_name(m, c)
+ if MAP.key?(m)
+ mapping = MAP[m]
+ if mapping.is_a?(Hash)
+ name = mapping[c.split('::').last] || mapping.fetch(:default)
+ else
+ name = mapping
+ end
+ else
+ name = m.gsub(/[?!=]/, '')
+ end
+ "#{name}_spec.rb"
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/options.rb b/spec/mspec/lib/mspec/utils/options.rb
new file mode 100644
index 0000000000..adeafa1f81
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/options.rb
@@ -0,0 +1,521 @@
+require 'mspec/version'
+
+MSPEC_HOME = File.expand_path('../../../..', __FILE__)
+
+class MSpecOption
+ attr_reader :short, :long, :arg, :description, :block
+
+ def initialize(short, long, arg, description, block)
+ @short = short
+ @long = long
+ @arg = arg
+ @description = description
+ @block = block
+ end
+
+ def arg?
+ @arg != nil
+ end
+
+ def match?(opt)
+ opt == @short or opt == @long
+ end
+end
+
+# MSpecOptions provides a parser for command line options. It also
+# provides a composable set of options from which the runner scripts
+# can select for their particular functionality.
+class MSpecOptions
+ # Raised if incorrect or incomplete formats are passed to #on.
+ class OptionError < Exception; end
+
+ # Raised if an unrecognized option is encountered.
+ class ParseError < Exception; end
+
+ class << self
+ attr_accessor :latest
+ end
+
+ attr_accessor :config, :banner, :width, :options
+
+ def initialize(banner = "", width = 30, config = nil)
+ @banner = banner
+ @config = config
+ @width = width
+ @options = []
+ @doc = []
+ @extra = []
+ @on_extra = lambda { |x|
+ raise ParseError, "Unrecognized option: #{x}" if x[0] == ?-
+ @extra << x
+ }
+
+ MSpecOptions.latest = self
+ end
+
+ # Registers an option. Acceptable formats for arguments are:
+ #
+ # on "-a", "description"
+ # on "-a", "--abdc", "description"
+ # on "-a", "ARG", "description"
+ # on "--abdc", "ARG", "description"
+ # on "-a", "--abdc", "ARG", "description"
+ #
+ # If an block is passed, it will be invoked when the option is
+ # matched. Not passing a block is permitted, but nonsensical.
+ def on(*args, &block)
+ raise OptionError, "option and description are required" if args.size < 2
+
+ description = args.pop
+ short, long, argument = nil
+ args.each do |arg|
+ if arg[0] == ?-
+ if arg[1] == ?-
+ long = arg
+ else
+ short = arg
+ end
+ else
+ argument = arg
+ end
+ end
+
+ add short, long, argument, description, block
+ end
+
+ # Adds documentation text for an option and adds an +MSpecOption+
+ # instance to the list of registered options.
+ def add(short, long, arg, description, block)
+ s = short ? short.dup : " "
+ s += (short ? ", " : " ") if long
+ doc " #{s}#{long} #{arg}".ljust(@width-1) + " #{description}"
+ @options << MSpecOption.new(short, long, arg, description, block)
+ end
+
+ # Searches all registered options to find a match for +opt+. Returns
+ # +nil+ if no registered options match.
+ def match?(opt)
+ @options.find { |o| o.match? opt }
+ end
+
+ # Processes an option. Calls the #on_extra block (or default) for
+ # unrecognized options. For registered options, possibly fetches an
+ # argument and invokes the option's block if it is not nil.
+ def process(argv, entry, opt, arg)
+ unless option = match?(opt)
+ @on_extra[entry]
+ else
+ if option.arg?
+ arg = argv.shift if arg.nil?
+ raise ParseError, "No argument provided for #{opt}" unless arg
+ option.block[arg] if option.block
+ else
+ option.block[] if option.block
+ end
+ end
+ option
+ end
+
+ # Splits a string at +n+ characters into the +opt+ and the +rest+.
+ # The +arg+ is set to +nil+ if +rest+ is an empty string.
+ def split(str, n)
+ opt = str[0, n]
+ rest = str[n, str.size]
+ arg = rest == "" ? nil : rest
+ return opt, arg, rest
+ end
+
+ # Parses an array of command line entries, calling blocks for
+ # registered options.
+ def parse(argv = ARGV)
+ argv = Array(argv).dup
+
+ while entry = argv.shift
+ # collect everything that is not an option
+ if entry[0] != ?- or entry.size < 2
+ @on_extra[entry]
+ next
+ end
+
+ # this is a long option
+ if entry[1] == ?-
+ opt, arg = entry.split "="
+ process argv, entry, opt, arg
+ next
+ end
+
+ # disambiguate short option group from short option with argument
+ opt, arg, rest = split entry, 2
+
+ # process first option
+ option = process argv, entry, opt, arg
+ next unless option and !option.arg?
+
+ # process the rest of the options
+ while rest.size > 0
+ opt, arg, rest = split rest, 1
+ opt = "-" + opt
+ option = process argv, opt, opt, arg
+ break if !option or option.arg?
+ end
+ end
+
+ @extra
+ rescue ParseError => e
+ puts self
+ puts e
+ exit 1
+ end
+
+ # Adds a string of documentation text inline in the text generated
+ # from the options. See #on and #add.
+ def doc(str)
+ @doc << str
+ end
+
+ # Convenience method for providing -v, --version options.
+ def version(version, &block)
+ show = block || lambda { puts "#{File.basename $0} #{version}"; exit }
+ on "-v", "--version", "Show version", &show
+ end
+
+ # Convenience method for providing -h, --help options.
+ def help(&block)
+ help = block || lambda { puts self; exit 1 }
+ on "-h", "--help", "Show this message", &help
+ end
+
+ # Stores a block that will be called with unrecognized options
+ def on_extra(&block)
+ @on_extra = block
+ end
+
+ # Returns a string representation of the options and doc strings.
+ def to_s
+ @banner + "\n\n" + @doc.join("\n") + "\n"
+ end
+
+ # The methods below provide groups of options that
+ # are composed by the particular runners to provide
+ # their functionality
+
+ def configure(&block)
+ on("-B", "--config", "FILE",
+ "Load FILE containing configuration options", &block)
+ end
+
+ def env
+ on("--env", "KEY=VALUE", "Set environment variable") do |env|
+ key, value = env.split('=', 2)
+ ENV[key] = value
+ end
+ end
+
+ def targets
+ on("-t", "--target", "TARGET",
+ "Implementation to run the specs, where TARGET is:") do |t|
+ case t
+ when 'r', 'ruby'
+ config[:target] = 'ruby'
+ when 'x', 'rubinius'
+ config[:target] = './bin/rbx'
+ when 'X', 'rbx'
+ config[:target] = 'rbx'
+ when 'j', 'jruby'
+ config[:target] = 'jruby'
+ when 'i','ironruby'
+ config[:target] = 'ir'
+ when 'm','maglev'
+ config[:target] = 'maglev-ruby'
+ when 't','topaz'
+ config[:target] = 'topaz'
+ when 'o','opal'
+ mspec_lib = File.expand_path('../../../', __FILE__)
+ config[:target] = "./bin/opal -syaml -sfileutils -rnodejs -rnodejs/require -rnodejs/yaml -rprocess -Derror -I#{mspec_lib} -I./lib/ -I. "
+ else
+ config[:target] = t
+ end
+ end
+
+ doc ""
+ doc " r or ruby invokes ruby in PATH"
+ doc " x or rubinius invokes ./bin/rbx"
+ doc " X or rbx invokes rbx in PATH"
+ doc " j or jruby invokes jruby in PATH"
+ doc " i or ironruby invokes ir in PATH"
+ doc " m or maglev invokes maglev-ruby in PATH"
+ doc " t or topaz invokes topaz in PATH"
+ doc " o or opal invokes ./bin/opal with options"
+ doc " full path to EXE invokes EXE directly\n"
+
+ on("-T", "--target-opt", "OPT",
+ "Pass OPT as a flag to the target implementation") do |t|
+ config[:flags] << t
+ end
+ on("-I", "--include", "DIR",
+ "Pass DIR through as the -I option to the target") do |d|
+ config[:loadpath] << "-I#{d}"
+ end
+ on("-r", "--require", "LIBRARY",
+ "Pass LIBRARY through as the -r option to the target") do |f|
+ config[:requires] << "-r#{f}"
+ end
+ end
+
+ def formatters
+ on("-f", "--format", "FORMAT",
+ "Formatter for reporting, where FORMAT is one of:") do |o|
+ require 'mspec/runner/formatters'
+ case o
+ when 's', 'spec', 'specdoc'
+ config[:formatter] = SpecdocFormatter
+ when 'h', 'html'
+ config[:formatter] = HtmlFormatter
+ when 'd', 'dot', 'dotted'
+ config[:formatter] = DottedFormatter
+ when 'b', 'describe'
+ config[:formatter] = DescribeFormatter
+ when 'f', 'file'
+ config[:formatter] = FileFormatter
+ when 'u', 'unit', 'unitdiff'
+ config[:formatter] = UnitdiffFormatter
+ when 'm', 'summary'
+ config[:formatter] = SummaryFormatter
+ when 'a', '*', 'spin'
+ config[:formatter] = SpinnerFormatter
+ when 't', 'method'
+ config[:formatter] = MethodFormatter
+ when 'e', 'stats'
+ config[:formatter] = StatsPerFileFormatter
+ when 'y', 'yaml'
+ config[:formatter] = YamlFormatter
+ when 'p', 'profile'
+ config[:formatter] = ProfileFormatter
+ when 'j', 'junit'
+ config[:formatter] = JUnitFormatter
+ else
+ abort "Unknown format: #{o}" unless File.exist?(o)
+ require File.expand_path(o)
+ if Object.const_defined?(:CUSTOM_MSPEC_FORMATTER)
+ config[:formatter] = CUSTOM_MSPEC_FORMATTER
+ else
+ abort "You must define CUSTOM_MSPEC_FORMATTER in your custom formatter file"
+ end
+ end
+ end
+
+ doc ""
+ doc " s, spec, specdoc SpecdocFormatter"
+ doc " h, html, HtmlFormatter"
+ doc " d, dot, dotted DottedFormatter"
+ doc " f, file FileFormatter"
+ doc " u, unit, unitdiff UnitdiffFormatter"
+ doc " m, summary SummaryFormatter"
+ doc " a, *, spin SpinnerFormatter"
+ doc " t, method MethodFormatter"
+ doc " e, stats StatsPerFileFormatter"
+ doc " y, yaml YamlFormatter"
+ doc " p, profile ProfileFormatter"
+ doc " j, junit JUnitFormatter\n"
+
+ on("-o", "--output", "FILE",
+ "Write formatter output to FILE") do |f|
+ config[:output] = f
+ end
+
+ on("--error-output", "FILE",
+ "Write error output of failing specs to FILE, or $stderr if value is 'stderr'.") do |f|
+ config[:error_output] = f
+ end
+ end
+
+ def filters
+ on("-e", "--example", "STR",
+ "Run examples with descriptions matching STR") do |o|
+ config[:includes] << o
+ end
+ on("-E", "--exclude", "STR",
+ "Exclude examples with descriptions matching STR") do |o|
+ config[:excludes] << o
+ end
+ on("-p", "--pattern", "PATTERN",
+ "Run examples with descriptions matching PATTERN") do |o|
+ config[:patterns] << Regexp.new(o)
+ end
+ on("-P", "--excl-pattern", "PATTERN",
+ "Exclude examples with descriptions matching PATTERN") do |o|
+ config[:xpatterns] << Regexp.new(o)
+ end
+ on("-g", "--tag", "TAG",
+ "Run examples with descriptions matching ones tagged with TAG") do |o|
+ config[:tags] << o
+ end
+ on("-G", "--excl-tag", "TAG",
+ "Exclude examples with descriptions matching ones tagged with TAG") do |o|
+ config[:xtags] << o
+ end
+ on("-w", "--profile", "FILE",
+ "Run examples for methods listed in the profile FILE") do |f|
+ config[:profiles] << f
+ end
+ on("-W", "--excl-profile", "FILE",
+ "Exclude examples for methods listed in the profile FILE") do |f|
+ config[:xprofiles] << f
+ end
+ end
+
+ def chdir
+ on("-C", "--chdir", "DIR",
+ "Change the working directory to DIR before running specs") do |d|
+ Dir.chdir d
+ end
+ end
+
+ def prefix
+ on("--prefix", "STR", "Prepend STR when resolving spec file names") do |p|
+ config[:prefix] = p
+ end
+ end
+
+ def pretend
+ on("-Z", "--dry-run",
+ "Invoke formatters and other actions, but don't execute the specs") do
+ MSpec.register_mode :pretend
+ end
+ end
+
+ def unguarded
+ on("--unguarded", "Turn off all guards") do
+ MSpec.register_mode :unguarded
+ end
+ on("--no-ruby_bug", "Turn off the ruby_bug guard") do
+ MSpec.register_mode :no_ruby_bug
+ end
+ end
+
+ def randomize
+ on("-H", "--random",
+ "Randomize the list of spec files") do
+ MSpec.randomize = true
+ end
+ end
+
+ def repeat
+ on("-R", "--repeat", "NUMBER",
+ "Repeatedly run an example NUMBER times") do |o|
+ MSpec.repeat = Integer(o)
+ end
+ end
+
+ def verbose
+ on("-V", "--verbose", "Output the name of each file processed") do
+ obj = Object.new
+ def obj.start
+ @width = MSpec.files_array.inject(0) { |max, f| f.size > max ? f.size : max }
+ end
+ def obj.load
+ file = MSpec.file
+ STDERR.print "\n#{file.ljust(@width)}\n"
+ end
+ MSpec.register :start, obj
+ MSpec.register :load, obj
+ end
+
+ on("-m", "--marker", "MARKER",
+ "Output MARKER for each file processed") do |o|
+ obj = Object.new
+ obj.instance_variable_set :@marker, o
+ def obj.load
+ STDERR.print @marker
+ end
+ MSpec.register :load, obj
+ end
+
+ on("--print-skips", "Print skips") do
+ config[:print_skips] = true
+ end
+ end
+
+ def interrupt
+ on("--int-spec", "Control-C interrupts the current spec only") do
+ config[:abort] = false
+ end
+ end
+
+ def timeout
+ on("--timeout", "TIMEOUT", "Abort if a spec takes longer than TIMEOUT seconds") do |timeout|
+ require 'mspec/runner/actions/timeout'
+ timeout = Float(timeout)
+ TimeoutAction.new(timeout).register
+ end
+ end
+
+ def verify
+ on("--report-on", "GUARD", "Report specs guarded by GUARD") do |g|
+ MSpec.register_mode :report_on
+ SpecGuard.guards << g.to_sym
+ end
+ on("-O", "--report", "Report guarded specs") do
+ MSpec.register_mode :report
+ end
+ on("-Y", "--verify",
+ "Verify that guarded specs pass and fail as expected") do
+ MSpec.register_mode :verify
+ end
+ end
+
+ def action_filters
+ on("-K", "--action-tag", "TAG",
+ "Spec descriptions marked with TAG will trigger the specified action") do |o|
+ config[:atags] << o
+ end
+ on("-S", "--action-string", "STR",
+ "Spec descriptions matching STR will trigger the specified action") do |o|
+ config[:astrings] << o
+ end
+ end
+
+ def actions
+ on("--spec-debug",
+ "Invoke the debugger when a spec description matches (see -K, -S)") do
+ config[:debugger] = true
+ end
+ end
+
+ def debug
+ on("-d", "--debug",
+ "Disable MSpec backtrace filtering") do
+ $MSPEC_DEBUG = true
+ end
+ end
+
+ def launchable
+ on("--launchable-test-reports", "DIR",
+ "DIR The directory for reporting test results in Launchable JSON format") do |o|
+ require 'mspec/runner/formatters/launchable'
+ config[:launchable] = LaunchableFormatter.setDir(o)
+ end
+ end
+
+ def all
+ configure {}
+ env
+ targets
+ formatters
+ filters
+ chdir
+ prefix
+ pretend
+ unguarded
+ randomize
+ repeat
+ verbose
+ interrupt
+ timeout
+ verify
+ action_filters
+ actions
+ debug
+ launchable
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/script.rb b/spec/mspec/lib/mspec/utils/script.rb
new file mode 100644
index 0000000000..15fd23fabf
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/script.rb
@@ -0,0 +1,305 @@
+require 'mspec/guards/guard'
+require 'mspec/guards/version'
+require 'mspec/utils/warnings'
+
+# MSpecScript provides a skeleton for all the MSpec runner scripts.
+class MSpecScript
+ # Returns the config object. Maintained at the class
+ # level to easily enable simple config files. See the
+ # class method +set+.
+ def self.config
+ @config ||= {
+ :path => ['.', 'spec'],
+ :config_ext => '.mspec'
+ }
+ end
+
+ # Associates +value+ with +key+ in the config object. Enables
+ # simple config files of the form:
+ #
+ # class MSpecScript
+ # set :target, "ruby"
+ # set :files, ["one_spec.rb", "two_spec.rb"]
+ # end
+ def self.set(key, value)
+ config[key] = value
+ end
+
+ # Gets the value of +key+ from the config object. Simplifies
+ # getting values in a config file:
+ #
+ # class MSpecScript
+ # set :a, 1
+ # set :b, 2
+ # set :c, get(:a) + get(:b)
+ # end
+ def self.get(key)
+ config[key]
+ end
+
+ class << self
+ attr_accessor :child_process
+ end
+
+ # True if the current process is the one going to run the specs with `MSpec.process`.
+ # False for e.g. `mspec` which exec's to `mspec-run`.
+ # This is useful in .mspec config files.
+ def self.child_process?
+ MSpecScript.child_process
+ end
+
+ def initialize
+ check_version!
+
+ config[:formatter] = nil
+ config[:includes] = []
+ config[:excludes] = []
+ config[:patterns] = []
+ config[:xpatterns] = []
+ config[:tags] = []
+ config[:xtags] = []
+ config[:profiles] = []
+ config[:xprofiles] = []
+ config[:atags] = []
+ config[:astrings] = []
+ config[:ltags] = []
+ config[:abort] = true
+ @loaded = []
+ end
+
+ # Returns the config object maintained by the instance's class.
+ # See the class methods +set+ and +config+.
+ def config
+ MSpecScript.config
+ end
+
+ # Returns +true+ if the file was located in +config[:path]+,
+ # possibly appending +config[:config_ext]. Returns +false+
+ # otherwise.
+ def try_load(target)
+ names = [target]
+ unless target[-6..-1] == config[:config_ext]
+ names << target + config[:config_ext]
+ end
+
+ names.each do |name|
+ config[:path].each do |dir|
+ file = File.expand_path name, dir
+ if @loaded.include?(file)
+ return true
+ elsif File.exist? file
+ value = Kernel.load(file)
+ @loaded << file
+ return value
+ end
+ end
+ end
+
+ false
+ end
+
+ def load(target)
+ try_load(target) or abort "Could not load config file #{target}"
+ end
+
+ # Attempts to load a default config file. First tries to load
+ # 'default.mspec'. If that fails, attempts to load a config
+ # file name constructed from the value of RUBY_ENGINE and the
+ # first two numbers in RUBY_VERSION. For example, on MRI 1.8.6,
+ # the file name would be 'ruby.1.8.mspec'.
+ def load_default
+ try_load 'default.mspec'
+
+ if Object.const_defined?(:RUBY_ENGINE)
+ engine = RUBY_ENGINE
+ else
+ engine = 'ruby'
+ end
+ try_load "#{engine}.#{SpecGuard.ruby_version}.mspec"
+ try_load "#{engine}.mspec"
+ end
+
+ # Callback for enabling custom options. This version is a no-op.
+ # Provide an implementation specific version in a config file.
+ # Called by #options after the MSpec-provided options are added.
+ def custom_options(options)
+ options.doc " No custom options registered"
+ end
+
+ # Registers all filters and actions.
+ def register
+ require 'mspec/runner/formatters/dotted'
+ require 'mspec/runner/formatters/spinner'
+ require 'mspec/runner/formatters/file'
+ require 'mspec/runner/filters'
+
+ if formatter = config_formatter
+ formatter.register
+ MSpec.formatter = formatter
+ end
+
+ MatchFilter.new(:include, *config[:includes]).register unless config[:includes].empty?
+ MatchFilter.new(:exclude, *config[:excludes]).register unless config[:excludes].empty?
+ RegexpFilter.new(:include, *config[:patterns]).register unless config[:patterns].empty?
+ RegexpFilter.new(:exclude, *config[:xpatterns]).register unless config[:xpatterns].empty?
+ TagFilter.new(:include, *config[:tags]).register unless config[:tags].empty?
+ TagFilter.new(:exclude, *config[:xtags]).register unless config[:xtags].empty?
+ ProfileFilter.new(:include, *config[:profiles]).register unless config[:profiles].empty?
+ ProfileFilter.new(:exclude, *config[:xprofiles]).register unless config[:xprofiles].empty?
+
+ DebugAction.new(config[:atags], config[:astrings]).register if config[:debugger]
+
+ custom_register
+ end
+
+ # Makes a formatter specified by :formatter option.
+ def config_formatter
+ if config[:formatter].nil?
+ config[:formatter] = STDOUT.tty? ? SpinnerFormatter : @files.size < 50 ? DottedFormatter : FileFormatter
+ end
+
+ if config[:formatter]
+ config[:formatter] = config[:formatter].new(config[:output])
+ end
+
+ if config[:launchable]
+ config[:formatter].extend config[:launchable]
+ end
+
+ config[:formatter]
+ end
+
+ # Callback for enabling custom actions, etc. This version is a
+ # no-op. Provide an implementation specific version in a config
+ # file. Called by #register.
+ def custom_register
+ end
+
+ # Sets up signal handlers. Only a handler for SIGINT is
+ # registered currently.
+ def signals
+ if config[:abort]
+ Signal.trap "INT" do
+ MSpec.actions :abort
+ puts "\nProcess aborted!"
+ exit! 1
+ end
+ end
+ end
+
+ # Attempts to resolve +partial+ as a file or directory name in the
+ # following order:
+ #
+ # 1. +partial+
+ # 2. +partial+ + "_spec.rb"
+ # 3. <tt>File.join(config[:prefix], partial)</tt>
+ # 4. <tt>File.join(config[:prefix], partial + "_spec.rb")</tt>
+ #
+ # If it is a file name, returns the name as an entry in an array.
+ # If it is a directory, returns all *_spec.rb files in the
+ # directory and subdirectories.
+ #
+ # If unable to resolve +partial+, +Kernel.abort+ is called.
+ def entries(partial)
+ file = partial + "_spec.rb"
+ patterns = [partial, file]
+ if config[:prefix]
+ patterns << File.join(config[:prefix], partial)
+ patterns << File.join(config[:prefix], file)
+ end
+
+ patterns.each do |pattern|
+ begin
+ expanded = File.realpath(pattern)
+ rescue Errno::ENOENT, Errno::ENOTDIR
+ next
+ end
+ if File.file?(expanded) && expanded.end_with?('.rb')
+ return [expanded]
+ elsif File.directory?(expanded)
+ specs = Dir["#{expanded}/**/*_spec.rb"].sort
+ return specs unless specs.empty?
+ end
+ end
+
+ abort "Could not find spec file #{partial}"
+ end
+
+ # Resolves each entry in +patterns+ to a set of files.
+ #
+ # If the pattern has a leading '^' character, the list of files
+ # is subtracted from the list of files accumulated to that point.
+ #
+ # If the entry has a leading ':' character, the corresponding
+ # key is looked up in the config object and the entries in the
+ # value retrieved are processed through #entries.
+ def files(patterns)
+ list = []
+ patterns.each do |pattern|
+ case pattern[0]
+ when ?^
+ list -= entries(pattern[1..-1])
+ when ?:
+ key = pattern[1..-1].to_sym
+ value = config[key]
+ abort "Key #{pattern} not found in mspec config." unless value
+ list += files(Array(value))
+ else
+ list += entries(pattern)
+ end
+ end
+ list
+ end
+
+ def files_from_patterns(patterns)
+ unless $0.end_with?("_spec.rb")
+ if patterns.empty?
+ patterns = config[:files]
+ end
+ if patterns.empty? and File.directory? "./spec"
+ patterns = ["spec/"]
+ end
+ end
+ list = files(patterns)
+ abort "No files specified." if list.empty?
+ list
+ end
+
+ def cores(max)
+ require 'etc'
+ [Etc.nprocessors, max].min
+ end
+
+ def setup_env
+ ENV['MSPEC_RUNNER'] = '1'
+
+ unless ENV['RUBY_EXE']
+ ENV['RUBY_EXE'] = config[:target] if config[:target]
+ end
+
+ unless ENV['RUBY_FLAGS']
+ ENV['RUBY_FLAGS'] = config[:flags].join(" ") if config[:flags]
+ end
+ end
+
+ # Instantiates an instance and calls the series of methods to
+ # invoke the script.
+ def self.main(child_process = true)
+ MSpecScript.child_process = child_process
+
+ script = new
+ script.load_default
+ script.options
+ script.signals
+ script.register
+ script.setup_env
+ require 'mspec'
+ script.run
+ end
+
+ private def check_version!
+ ruby_version_is ""..."2.6" do
+ warn "MSpec is supported for Ruby 2.6 and above only"
+ end
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/version.rb b/spec/mspec/lib/mspec/utils/version.rb
new file mode 100644
index 0000000000..9c1c58b8df
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/version.rb
@@ -0,0 +1,52 @@
+class SpecVersion
+ # If beginning implementations have a problem with this include, we can
+ # manually implement the relational operators that are needed.
+ include Comparable
+
+ # SpecVersion handles comparison correctly for the context by filling in
+ # missing version parts according to the value of +ceil+. If +ceil+ is
+ # +false+, 0 digits fill in missing version parts. If +ceil+ is +true+, 9
+ # digits fill in missing parts. (See e.g. VersionGuard and BugGuard.)
+ def initialize(version, ceil = false)
+ @version = version
+ @ceil = ceil
+ @integer = nil
+ end
+
+ def to_s
+ @version
+ end
+
+ def to_str
+ to_s
+ end
+
+ # Converts a string representation of a version major.minor.tiny
+ # to an integer representation so that comparisons can be made. For example,
+ # "2.2.10" < "2.2.2" would be false if compared as strings.
+ def to_i
+ unless @integer
+ major, minor, tiny = @version.split "."
+ if @ceil
+ tiny = 99 unless tiny
+ end
+ parts = [major, minor, tiny].map { |x| x.to_i }
+ @integer = ("1%02d%02d%02d" % parts).to_i
+ end
+ @integer
+ end
+
+ def to_int
+ to_i
+ end
+
+ def <=>(other)
+ if other.respond_to? :to_int
+ other = Integer(other.to_int)
+ else
+ other = SpecVersion.new(String(other)).to_i
+ end
+
+ self.to_i <=> other
+ end
+end
diff --git a/spec/mspec/lib/mspec/utils/warnings.rb b/spec/mspec/lib/mspec/utils/warnings.rb
new file mode 100644
index 0000000000..23efc696a5
--- /dev/null
+++ b/spec/mspec/lib/mspec/utils/warnings.rb
@@ -0,0 +1,10 @@
+require 'mspec/guards/version'
+
+# Always enable deprecation warnings when running MSpec, as ruby/spec tests for them,
+# and like in most test frameworks, deprecation warnings should be enabled by default,
+# so that deprecations are noticed before the breaking change.
+# Disable experimental warnings, we want to test new experimental features in ruby/spec.
+if Object.const_defined?(:Warning) and Warning.respond_to?(:[]=)
+ Warning[:deprecated] = true
+ Warning[:experimental] = false
+end
diff --git a/spec/mspec/lib/mspec/version.rb b/spec/mspec/lib/mspec/version.rb
new file mode 100644
index 0000000000..9126f5366e
--- /dev/null
+++ b/spec/mspec/lib/mspec/version.rb
@@ -0,0 +1,5 @@
+require 'mspec/utils/version'
+
+module MSpec
+ VERSION = SpecVersion.new "1.8.0"
+end
diff --git a/spec/mspec/spec/commands/fixtures/four.txt b/spec/mspec/spec/commands/fixtures/four.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/four.txt
diff --git a/spec/mspec/spec/commands/fixtures/level2/three_spec.rb b/spec/mspec/spec/commands/fixtures/level2/three_spec.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/level2/three_spec.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/fixtures/one_spec.rb b/spec/mspec/spec/commands/fixtures/one_spec.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/one_spec.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/fixtures/three.rb b/spec/mspec/spec/commands/fixtures/three.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/three.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/fixtures/two_spec.rb b/spec/mspec/spec/commands/fixtures/two_spec.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/mspec/spec/commands/fixtures/two_spec.rb
@@ -0,0 +1 @@
+
diff --git a/spec/mspec/spec/commands/mkspec_spec.rb b/spec/mspec/spec/commands/mkspec_spec.rb
new file mode 100644
index 0000000000..32262723de
--- /dev/null
+++ b/spec/mspec/spec/commands/mkspec_spec.rb
@@ -0,0 +1,363 @@
+require 'spec_helper'
+require 'mspec/commands/mkspec'
+require 'fileutils'
+
+RSpec.describe "The -c, --constant CONSTANT option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-c", "--constant", "CONSTANT",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "adds CONSTANT to the list of constants" do
+ ["-c", "--constant"].each do |opt|
+ @config[:constants] = []
+ @script.options [opt, "Object"]
+ expect(@config[:constants]).to include("Object")
+ end
+ end
+end
+
+RSpec.describe "The -b, --base DIR option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-b", "--base", "DIR",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "sets the base directory relative to which the spec directories are created" do
+ ["-b", "--base"].each do |opt|
+ @config[:base] = nil
+ @script.options [opt, "superspec"]
+ expect(@config[:base]).to eq(File.expand_path("superspec"))
+ end
+ end
+end
+
+RSpec.describe "The -r, --require LIBRARY option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-r", "--require", "LIBRARY",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "adds CONSTANT to the list of constants" do
+ ["-r", "--require"].each do |opt|
+ @config[:requires] = []
+ @script.options [opt, "libspec"]
+ expect(@config[:requires]).to include("libspec")
+ end
+ end
+end
+
+RSpec.describe "The -V, --version-guard VERSION option" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ @config = @script.config
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-V", "--version-guard", "VERSION",
+ an_instance_of(String))
+ @script.options []
+ end
+
+ it "sets the version for the ruby_version_is guards to VERSION" do
+ ["-r", "--require"].each do |opt|
+ @config[:requires] = []
+ @script.options [opt, "libspec"]
+ expect(@config[:requires]).to include("libspec")
+ end
+ end
+end
+
+RSpec.describe MkSpec, "#options" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MkSpec.new
+ end
+
+ it "parses the command line options" do
+ expect(@options).to receive(:parse).with(["--this", "and", "--that"])
+ @script.options ["--this", "and", "--that"]
+ end
+
+ it "parses ARGV unless passed other options" do
+ expect(@options).to receive(:parse).with(ARGV)
+ @script.options
+ end
+
+ it "prints help and exits if passed an unrecognized option" do
+ expect(@options).to receive(:raise).with(MSpecOptions::ParseError, an_instance_of(String))
+ allow(@options).to receive(:puts)
+ allow(@options).to receive(:exit)
+ @script.options ["--iunknown"]
+ end
+end
+
+RSpec.describe MkSpec, "#create_directory" do
+ before :each do
+ @script = MkSpec.new
+ @script.config[:base] = "spec"
+ end
+
+ it "prints a warning if a file with the directory name exists" do
+ expect(File).to receive(:exist?).and_return(true)
+ expect(File).to receive(:directory?).and_return(false)
+ expect(FileUtils).not_to receive(:mkdir_p)
+ expect(@script).to receive(:puts).with("spec/class already exists and is not a directory.")
+ expect(@script.create_directory("Class")).to eq(nil)
+ end
+
+ it "does nothing if the directory already exists" do
+ expect(File).to receive(:exist?).and_return(true)
+ expect(File).to receive(:directory?).and_return(true)
+ expect(FileUtils).not_to receive(:mkdir_p)
+ expect(@script.create_directory("Class")).to eq("spec/class")
+ end
+
+ it "creates the directory if it does not exist" do
+ expect(File).to receive(:exist?).and_return(false)
+ expect(@script).to receive(:mkdir_p).with("spec/class")
+ expect(@script.create_directory("Class")).to eq("spec/class")
+ end
+
+ it "creates the directory for a namespaced module if it does not exist" do
+ expect(File).to receive(:exist?).and_return(false)
+ expect(@script).to receive(:mkdir_p).with("spec/struct/tms")
+ expect(@script.create_directory("Struct::Tms")).to eq("spec/struct/tms")
+ end
+end
+
+RSpec.describe MkSpec, "#write_requires" do
+ before :each do
+ @script = MkSpec.new
+ @script.config[:base] = "spec"
+
+ @file = double("file")
+ allow(File).to receive(:open).and_yield(@file)
+ end
+
+ it "writes the spec_helper require line" do
+ expect(@file).to receive(:puts).with("require_relative '../../../spec_helper'")
+ @script.write_requires("spec/core/tcejbo", "spec/core/tcejbo/inspect_spec.rb")
+ end
+
+ it "writes require lines for each library specified on the command line" do
+ allow(@file).to receive(:puts)
+ expect(@file).to receive(:puts).with("require_relative '../../../spec_helper'")
+ expect(@file).to receive(:puts).with("require 'complex'")
+ @script.config[:requires] << 'complex'
+ @script.write_requires("spec/core/tcejbo", "spec/core/tcejbo/inspect_spec.rb")
+ end
+end
+
+RSpec.describe MkSpec, "#write_spec" do
+ before :each do
+ @file = IOStub.new
+ allow(File).to receive(:open).and_yield(@file)
+
+ @script = MkSpec.new
+ allow(@script).to receive(:puts)
+
+ @response = double("system command response")
+ allow(@response).to receive(:include?).and_return(false)
+ allow(@script).to receive(:`).and_return(@response)
+ end
+
+ it "checks if specs exist for the method if the spec file exists" do
+ name = Regexp.escape(RbConfig.ruby)
+ expect(@script).to receive(:`).with(
+ %r"#{name} #{MSPEC_HOME}/bin/mspec-run --dry-run --unguarded -fs -e 'Object#inspect' spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "checks for the method name in the spec file output" do
+ expect(@response).to receive(:include?).with("Array#[]=")
+ @script.write_spec("spec/core/yarra/element_set_spec.rb", "Array#[]=", true)
+ end
+
+ it "returns nil if the spec file exists and contains a spec for the method" do
+ allow(@response).to receive(:include?).and_return(true)
+ expect(@script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)).to eq(nil)
+ end
+
+ it "does not print the spec file name if it exists and contains a spec for the method" do
+ allow(@response).to receive(:include?).and_return(true)
+ expect(@script).not_to receive(:puts)
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "prints the spec file name if a template spec is written" do
+ expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "writes a template spec to the file if the spec file does not exist" do
+ expect(@file).to receive(:puts).twice
+ expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", false)
+ end
+
+ it "writes a template spec to the file if it exists but contains no spec for the method" do
+ expect(@response).to receive(:include?).and_return(false)
+ expect(@file).to receive(:puts).twice
+ expect(@script).to receive(:puts).with("spec/core/tcejbo/inspect_spec.rb")
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ end
+
+ it "writes a template spec" do
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ expect(@file).to eq <<EOS
+
+describe "Object#inspect" do
+ it "needs to be reviewed for spec completeness"
+end
+EOS
+ end
+
+ it "writes a template spec with version guard" do
+ @script.config[:version] = '""..."1.9"'
+ @script.write_spec("spec/core/tcejbo/inspect_spec.rb", "Object#inspect", true)
+ expect(@file).to eq <<EOS
+
+ruby_version_is ""..."1.9" do
+ describe "Object#inspect" do
+ it "needs to be reviewed for spec completeness"
+ end
+end
+EOS
+
+ end
+end
+
+RSpec.describe MkSpec, "#create_file" do
+ before :each do
+ @script = MkSpec.new
+ allow(@script).to receive(:write_requires)
+ allow(@script).to receive(:write_spec)
+
+ allow(File).to receive(:exist?).and_return(false)
+ end
+
+ it "generates a file name based on the directory, class/module, and method" do
+ expect(File).to receive(:join).with("spec/tcejbo", "inspect_spec.rb"
+ ).and_return("spec/tcejbo/inspect_spec.rb")
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+
+ it "does not call #write_requires if the spec file already exists" do
+ expect(File).to receive(:exist?).and_return(true)
+ expect(@script).not_to receive(:write_requires)
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+
+ it "calls #write_requires if the spec file does not exist" do
+ expect(File).to receive(:exist?).and_return(false)
+ expect(@script).to receive(:write_requires).with(
+ "spec/tcejbo", "spec/tcejbo/inspect_spec.rb")
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+
+ it "calls #write_spec with the file, method name" do
+ expect(@script).to receive(:write_spec).with(
+ "spec/tcejbo/inspect_spec.rb", "Object#inspect", false)
+ @script.create_file("spec/tcejbo", "Object", "inspect", "Object#inspect")
+ end
+end
+
+RSpec.describe MkSpec, "#run" do
+ before :each do
+ @options = MSpecOptions.new
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @map = NameMap.new
+ allow(NameMap).to receive(:new).and_return(@map)
+
+ @script = MkSpec.new
+ allow(@script).to receive(:create_directory).and_return("spec/mkspec")
+ allow(@script).to receive(:create_file)
+ @script.config[:constants] = [MkSpec]
+ end
+
+ it "loads files in the requires list" do
+ allow(@script).to receive(:require)
+ expect(@script).to receive(:require).with("alib")
+ expect(@script).to receive(:require).with("blib")
+ @script.config[:requires] = ["alib", "blib"]
+ @script.run
+ end
+
+ it "creates a map of constants to methods" do
+ expect(@map).to receive(:map).with({}, @script.config[:constants]).and_return({})
+ @script.run
+ end
+
+ it "calls #create_directory for each class/module in the map" do
+ expect(@script).to receive(:create_directory).with("MkSpec").twice
+ @script.run
+ end
+
+ it "calls #create_file for each method on each class/module in the map" do
+ expect(@map).to receive(:map).with({}, @script.config[:constants]
+ ).and_return({"MkSpec#" => ["run"]})
+ expect(@script).to receive(:create_file).with("spec/mkspec", "MkSpec", "run", "MkSpec#run")
+ @script.run
+ end
+end
+
+RSpec.describe MkSpec, ".main" do
+ before :each do
+ @script = double("MkSpec").as_null_object
+ allow(MkSpec).to receive(:new).and_return(@script)
+ end
+
+ it "sets MSPEC_RUNNER = '1' in the environment" do
+ ENV["MSPEC_RUNNER"] = "0"
+ MkSpec.main
+ expect(ENV["MSPEC_RUNNER"]).to eq("1")
+ end
+
+ it "creates an instance of MSpecScript" do
+ expect(MkSpec).to receive(:new).and_return(@script)
+ MkSpec.main
+ end
+
+ it "calls the #options method on the script" do
+ expect(@script).to receive(:options)
+ MkSpec.main
+ end
+
+ it "calls the #run method on the script" do
+ expect(@script).to receive(:run)
+ MkSpec.main
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_ci_spec.rb b/spec/mspec/spec/commands/mspec_ci_spec.rb
new file mode 100644
index 0000000000..b8dc9d062f
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_ci_spec.rb
@@ -0,0 +1,155 @@
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/tag'
+require 'mspec/commands/mspec-ci'
+
+RSpec.describe MSpecCI, "#options" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecCI.new
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:files).and_return([])
+ end
+
+ it "enables the chdir option" do
+ expect(@options).to receive(:chdir)
+ @script.options []
+ end
+
+ it "enables the prefix option" do
+ expect(@options).to receive(:prefix)
+ @script.options []
+ end
+
+ it "enables the config option" do
+ expect(@options).to receive(:configure)
+ @script.options []
+ end
+
+ it "provides a custom action (block) to the config option" do
+ expect(@script).to receive(:load).with("cfg.mspec")
+ @script.options ["-B", "cfg.mspec"]
+ end
+
+ it "enables the dry run option" do
+ expect(@options).to receive(:pretend)
+ @script.options []
+ end
+
+ it "enables the unguarded option" do
+ expect(@options).to receive(:unguarded)
+ @script.options []
+ end
+
+ it "enables the interrupt single specs option" do
+ expect(@options).to receive(:interrupt)
+ @script.options []
+ end
+
+ it "enables the formatter options" do
+ expect(@options).to receive(:formatters)
+ @script.options []
+ end
+
+ it "enables the verbose option" do
+ expect(@options).to receive(:verbose)
+ @script.options []
+ end
+
+ it "enables the action options" do
+ expect(@options).to receive(:actions)
+ @script.options []
+ end
+
+ it "enables the action filter options" do
+ expect(@options).to receive(:action_filters)
+ @script.options []
+ end
+
+ it "enables the version option" do
+ expect(@options).to receive(:version)
+ @script.options []
+ end
+
+ it "enables the help option" do
+ expect(@options).to receive(:help)
+ @script.options []
+ end
+
+ it "enables the repeat option" do
+ expect(@options).to receive(:repeat)
+ @script.options @argv
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options []
+ end
+end
+
+RSpec.describe MSpecCI, "#run" do
+ before :each do
+ allow(MSpec).to receive(:process)
+
+ @filter = double("TagFilter")
+ allow(TagFilter).to receive(:new).and_return(@filter)
+ allow(@filter).to receive(:register)
+
+ @tags = ["fails", "critical", "unstable", "incomplete", "unsupported"]
+
+ @config = { :ci_files => ["one", "two"] }
+ @script = MSpecCI.new
+ allow(@script).to receive(:exit)
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:files).and_return(["one", "two"])
+ @script.options []
+ end
+
+ it "registers the tags patterns" do
+ @config[:tags_patterns] = [/spec/, "tags"]
+ expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"])
+ @script.run
+ end
+
+ it "registers the files to process" do
+ expect(MSpec).to receive(:register_files).with(["one", "two"])
+ @script.run
+ end
+
+ it "registers a tag filter for 'fails', 'unstable', 'incomplete', 'critical', 'unsupported'" do
+ filter = double("fails filter")
+ expect(TagFilter).to receive(:new).with(:exclude, *@tags).and_return(filter)
+ expect(filter).to receive(:register)
+ @script.run
+ end
+
+ it "registers an additional exclude tag specified by :ci_xtags" do
+ @config[:ci_xtags] = "windows"
+ filter = double("fails filter")
+ expect(TagFilter).to receive(:new).with(:exclude, *(@tags + ["windows"])).and_return(filter)
+ expect(filter).to receive(:register)
+ @script.run
+ end
+
+ it "registers additional exclude tags specified by a :ci_xtags array" do
+ @config[:ci_xtags] = ["windows", "windoze"]
+ filter = double("fails filter")
+ expect(TagFilter).to receive(:new).with(:exclude,
+ *(@tags + ["windows", "windoze"])).and_return(filter)
+ expect(filter).to receive(:register)
+ @script.run
+ end
+
+ it "processes the files" do
+ expect(MSpec).to receive(:process)
+ @script.run
+ end
+
+ it "exits with the exit code registered with MSpec" do
+ allow(MSpec).to receive(:exit_code).and_return(7)
+ expect(@script).to receive(:exit).with(7)
+ @script.run
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_run_spec.rb b/spec/mspec/spec/commands/mspec_run_spec.rb
new file mode 100644
index 0000000000..f96be2b43e
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_run_spec.rb
@@ -0,0 +1,178 @@
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/commands/mspec-run'
+
+one_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/one_spec.rb'
+two_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/two_spec.rb'
+
+RSpec.describe MSpecRun, ".new" do
+ before :each do
+ @script = MSpecRun.new
+ end
+
+ it "sets config[:files] to an empty list" do
+ expect(@script.config[:files]).to eq([])
+ end
+end
+
+RSpec.describe MSpecRun, "#options" do
+ before :each do
+ @argv = [one_spec, two_spec]
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecRun.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "enables the filter options" do
+ expect(@options).to receive(:filters)
+ @script.options @argv
+ end
+
+ it "enables the chdir option" do
+ expect(@options).to receive(:chdir)
+ @script.options @argv
+ end
+
+ it "enables the prefix option" do
+ expect(@options).to receive(:prefix)
+ @script.options @argv
+ end
+
+ it "enables the configure option" do
+ expect(@options).to receive(:configure)
+ @script.options @argv
+ end
+
+ it "provides a custom action (block) to the config option" do
+ expect(@script).to receive(:load).with("cfg.mspec")
+ @script.options ["-B", "cfg.mspec", one_spec]
+ end
+
+ it "enables the randomize option to runs specs in random order" do
+ expect(@options).to receive(:randomize)
+ @script.options @argv
+ end
+
+ it "enables the dry run option" do
+ expect(@options).to receive(:pretend)
+ @script.options @argv
+ end
+
+ it "enables the unguarded option" do
+ expect(@options).to receive(:unguarded)
+ @script.options @argv
+ end
+
+ it "enables the interrupt single specs option" do
+ expect(@options).to receive(:interrupt)
+ @script.options @argv
+ end
+
+ it "enables the formatter options" do
+ expect(@options).to receive(:formatters)
+ @script.options @argv
+ end
+
+ it "enables the verbose option" do
+ expect(@options).to receive(:verbose)
+ @script.options @argv
+ end
+
+ it "enables the verify options" do
+ expect(@options).to receive(:verify)
+ @script.options @argv
+ end
+
+ it "enables the action options" do
+ expect(@options).to receive(:actions)
+ @script.options @argv
+ end
+
+ it "enables the action filter options" do
+ expect(@options).to receive(:action_filters)
+ @script.options @argv
+ end
+
+ it "enables the version option" do
+ expect(@options).to receive(:version)
+ @script.options @argv
+ end
+
+ it "enables the help option" do
+ expect(@options).to receive(:help)
+ @script.options @argv
+ end
+
+ it "enables the repeat option" do
+ expect(@options).to receive(:repeat)
+ @script.options @argv
+ end
+
+ it "exits if there are no files to process and './spec' is not a directory" do
+ expect(File).to receive(:directory?).with("./spec").and_return(false)
+ expect(@options).to receive(:parse).and_return([])
+ expect(@script).to receive(:abort).with("No files specified.")
+ @script.options
+ end
+
+ it "process 'spec/' if it is a directory and no files were specified" do
+ expect(File).to receive(:directory?).with("./spec").and_return(true)
+ expect(@options).to receive(:parse).and_return([])
+ expect(@script).to receive(:files).with(["spec/"]).and_return(["spec/a_spec.rb"])
+ @script.options
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options @argv
+ end
+end
+
+RSpec.describe MSpecRun, "#run" do
+ before :each do
+ @script = MSpecRun.new
+ allow(@script).to receive(:exit)
+ @spec_dir = File.expand_path(File.dirname(__FILE__)+"/fixtures")
+ @file_patterns = [
+ @spec_dir+"/level2",
+ @spec_dir+"/one_spec.rb",
+ @spec_dir+"/two_spec.rb"]
+ @files = [
+ @spec_dir+"/level2/three_spec.rb",
+ @spec_dir+"/one_spec.rb",
+ @spec_dir+"/two_spec.rb"]
+ @script.options @file_patterns
+ allow(MSpec).to receive :process
+ end
+
+ it "registers the tags patterns" do
+ @script.config[:tags_patterns] = [/spec/, "tags"]
+ expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"])
+ @script.run
+ end
+
+ it "registers the files to process" do
+ expect(MSpec).to receive(:register_files).with(@files)
+ @script.run
+ end
+
+ it "uses config[:files] if no files are given on the command line" do
+ @script.config[:files] = @file_patterns
+ expect(MSpec).to receive(:register_files).with(@files)
+ @script.options []
+ @script.run
+ end
+
+ it "processes the files" do
+ expect(MSpec).to receive(:process)
+ @script.run
+ end
+
+ it "exits with the exit code registered with MSpec" do
+ allow(MSpec).to receive(:exit_code).and_return(7)
+ expect(@script).to receive(:exit).with(7)
+ @script.run
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_spec.rb b/spec/mspec/spec/commands/mspec_spec.rb
new file mode 100644
index 0000000000..d19bebb2d6
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_spec.rb
@@ -0,0 +1,180 @@
+require 'spec_helper'
+require 'yaml'
+require 'mspec/commands/mspec'
+
+RSpec.describe MSpecMain, "#options" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:load)
+ end
+
+ it "enables the configure option" do
+ expect(@options).to receive(:configure)
+ @script.options
+ end
+
+ it "provides a custom action (block) to the config option" do
+ @script.options ["-B", "config"]
+ expect(@config[:options]).to include("-B", "config")
+ end
+
+ it "loads the file specified by the config option" do
+ expect(@script).to receive(:load).with("config")
+ @script.options ["-B", "config"]
+ end
+
+ it "enables the target options" do
+ expect(@options).to receive(:targets)
+ @script.options
+ end
+
+ it "sets config[:options] to all argv entries that are not registered options" do
+ @options.on "-X", "--exclude", "ARG", "description"
+ @script.options [".", "-G", "fail", "-X", "ARG", "--list", "unstable", "some/file.rb"]
+ expect(@config[:options]).to eq([".", "-G", "fail", "--list", "unstable", "some/file.rb"])
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options
+ end
+end
+
+RSpec.describe MSpecMain, "#run" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:exec)
+ @err = $stderr
+ $stderr = IOStub.new
+ end
+
+ after :each do
+ $stderr = @err
+ end
+
+ it "uses exec to invoke the runner script" do
+ expect(@script).to receive(:exec).with("ruby", "#{MSPEC_HOME}/bin/mspec-run", close_others: false)
+ @script.options []
+ @script.run
+ end
+
+ it "shows the command line on stderr" do
+ expect(@script).to receive(:exec).with("ruby", "#{MSPEC_HOME}/bin/mspec-run", close_others: false)
+ @script.options []
+ @script.run
+ expect($stderr.to_s).to eq("$ ruby #{Dir.pwd}/bin/mspec-run\n")
+ end
+
+ it "adds config[:launch] to the exec options" do
+ expect(@script).to receive(:exec).with("ruby",
+ "-Xlaunch.option", "#{MSPEC_HOME}/bin/mspec-run", close_others: false)
+ @config[:launch] << "-Xlaunch.option"
+ @script.options []
+ @script.run
+ expect($stderr.to_s).to eq("$ ruby -Xlaunch.option #{Dir.pwd}/bin/mspec-run\n")
+ end
+
+ it "calls #multi_exec if the command is 'ci' and the multi option is passed" do
+ expect(@script).to receive(:multi_exec) do |argv|
+ expect(argv).to eq(["ruby", "#{MSPEC_HOME}/bin/mspec-ci"])
+ end
+ @script.options ["ci", "-j"]
+ expect do
+ @script.run
+ end.to raise_error(SystemExit)
+ end
+end
+
+RSpec.describe "The -j, --multi option" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-j", "--multi", an_instance_of(String))
+ @script.options
+ end
+
+ it "sets the multiple process option" do
+ ["-j", "--multi"].each do |opt|
+ @config[:multi] = nil
+ @script.options [opt]
+ expect(@config[:multi]).to eq(true)
+ end
+ end
+end
+
+RSpec.describe "The -h, --help option" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-h", "--help", an_instance_of(String))
+ @script.options
+ end
+
+ it "passes the option to the subscript" do
+ ["-h", "--help"].each do |opt|
+ @config[:options] = []
+ @script.options ["ci", opt]
+ expect(@config[:options].sort).to eq(["-h"])
+ end
+ end
+
+ it "prints help and exits" do
+ expect(@script).to receive(:puts).twice
+ expect(@script).to receive(:exit).twice
+ ["-h", "--help"].each do |opt|
+ @script.options [opt]
+ end
+ end
+end
+
+RSpec.describe "The -v, --version option" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecMain.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ it "is enabled by #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-v", "--version", an_instance_of(String))
+ @script.options
+ end
+
+ it "passes the option to the subscripts" do
+ ["-v", "--version"].each do |opt|
+ @config[:options] = []
+ @script.options ["ci", opt]
+ expect(@config[:options].sort).to eq(["-v"])
+ end
+ end
+
+ it "prints the version and exits if no subscript is invoked" do
+ @config[:command] = nil
+ allow(File).to receive(:basename).and_return("mspec")
+ expect(@script).to receive(:puts).twice.with("mspec #{MSpec::VERSION}")
+ expect(@script).to receive(:exit).twice
+ ["-v", "--version"].each do |opt|
+ @script.options [opt]
+ end
+ end
+end
diff --git a/spec/mspec/spec/commands/mspec_tag_spec.rb b/spec/mspec/spec/commands/mspec_tag_spec.rb
new file mode 100644
index 0000000000..1ab5f6ea58
--- /dev/null
+++ b/spec/mspec/spec/commands/mspec_tag_spec.rb
@@ -0,0 +1,414 @@
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/commands/mspec-tag'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/actions/tagpurge'
+
+one_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/one_spec.rb'
+two_spec = File.expand_path(File.dirname(__FILE__)) + '/fixtures/two_spec.rb'
+
+RSpec.describe MSpecTag, ".new" do
+ before :each do
+ @script = MSpecTag.new
+ end
+
+ it "sets config[:ltags] to an empty list" do
+ expect(@script.config[:ltags]).to eq([])
+ end
+
+ it "sets config[:tagger] to :add" do
+ @script.config[:tagger] = :add
+ end
+
+ it "sets config[:tag] to 'fails:'" do
+ @script.config[:tag] = 'fails:'
+ end
+
+ it "sets config[:outcome] to :fail" do
+ @script.config[:outcome] = :fail
+ end
+end
+
+RSpec.describe MSpecTag, "#options" do
+ before :each do
+ @stdout, $stdout = $stdout, IOStub.new
+
+ @argv = [one_spec, two_spec]
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+
+ @script = MSpecTag.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "enables the filter options" do
+ expect(@options).to receive(:filters)
+ @script.options @argv
+ end
+
+ it "enables the configure option" do
+ expect(@options).to receive(:configure)
+ @script.options @argv
+ end
+
+ it "provides a custom action (block) to the config option" do
+ expect(@script).to receive(:load).with("cfg.mspec")
+ @script.options ["-B", "cfg.mspec", one_spec]
+ end
+
+ it "enables the dry run option" do
+ expect(@options).to receive(:pretend)
+ @script.options @argv
+ end
+
+ it "enables the unguarded option" do
+ expect(@options).to receive(:unguarded)
+ @script.options @argv
+ end
+
+ it "enables the interrupt single specs option" do
+ expect(@options).to receive(:interrupt)
+ @script.options @argv
+ end
+
+ it "enables the formatter options" do
+ expect(@options).to receive(:formatters)
+ @script.options @argv
+ end
+
+ it "enables the verbose option" do
+ expect(@options).to receive(:verbose)
+ @script.options @argv
+ end
+
+ it "enables the version option" do
+ expect(@options).to receive(:version)
+ @script.options @argv
+ end
+
+ it "enables the help option" do
+ expect(@options).to receive(:help)
+ @script.options @argv
+ end
+
+ it "calls #custom_options" do
+ expect(@script).to receive(:custom_options).with(@options)
+ @script.options @argv
+ end
+
+ it "exits if there are no files to process" do
+ expect(@options).to receive(:parse).and_return([])
+ expect(@script).to receive(:exit)
+ @script.options
+ expect($stdout.to_s).to include "No files specified"
+ end
+end
+
+RSpec.describe MSpecTag, "options" do
+ before :each do
+ @options, @config = new_option
+ allow(MSpecOptions).to receive(:new).and_return(@options)
+ @script = MSpecTag.new
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ describe "-N, --add TAG" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-N", "--add", "TAG", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :add and sets the tag to TAG" do
+ ["-N", "--add"].each do |opt|
+ @config[:tagger] = nil
+ @config[:tag] = nil
+ @script.options [opt, "taggit", one_spec]
+ expect(@config[:tagger]).to eq(:add)
+ expect(@config[:tag]).to eq("taggit:")
+ end
+ end
+ end
+
+ describe "-R, --del TAG" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-R", "--del", "TAG",
+ an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "it sets the mode to :del, the tag to TAG, and the outcome to :pass" do
+ ["-R", "--del"].each do |opt|
+ @config[:tagger] = nil
+ @config[:tag] = nil
+ @config[:outcome] = nil
+ @script.options [opt, "taggit", one_spec]
+ expect(@config[:tagger]).to eq(:del)
+ expect(@config[:tag]).to eq("taggit:")
+ expect(@config[:outcome]).to eq(:pass)
+ end
+ end
+ end
+
+ describe "-Q, --pass" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-Q", "--pass", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the outcome to :pass" do
+ ["-Q", "--pass"].each do |opt|
+ @config[:outcome] = nil
+ @script.options [opt, one_spec]
+ expect(@config[:outcome]).to eq(:pass)
+ end
+ end
+ end
+
+ describe "-F, --fail" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-F", "--fail", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the outcome to :fail" do
+ ["-F", "--fail"].each do |opt|
+ @config[:outcome] = nil
+ @script.options [opt, one_spec]
+ expect(@config[:outcome]).to eq(:fail)
+ end
+ end
+ end
+
+ describe "-L, --all" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-L", "--all", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the outcome to :all" do
+ ["-L", "--all"].each do |opt|
+ @config[:outcome] = nil
+ @script.options [opt, one_spec]
+ expect(@config[:outcome]).to eq(:all)
+ end
+ end
+ end
+
+ describe "--list TAG" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--list", "TAG", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :list" do
+ @config[:tagger] = nil
+ @script.options ["--list", "TAG", one_spec]
+ expect(@config[:tagger]).to eq(:list)
+ end
+
+ it "sets ltags to include TAG" do
+ @config[:tag] = nil
+ @script.options ["--list", "TAG", one_spec]
+ expect(@config[:ltags]).to eq(["TAG"])
+ end
+ end
+
+ describe "--list-all" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--list-all", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :list_all" do
+ @config[:tagger] = nil
+ @script.options ["--list-all", one_spec]
+ expect(@config[:tagger]).to eq(:list_all)
+ end
+ end
+
+ describe "--purge" do
+ it "is enabled with #options" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--purge", an_instance_of(String))
+ @script.options [one_spec]
+ end
+
+ it "sets the mode to :purge" do
+ @config[:tagger] = nil
+ @script.options ["--purge", one_spec]
+ expect(@config[:tagger]).to eq(:purge)
+ end
+ end
+end
+
+RSpec.describe MSpecTag, "#run" do
+ before :each do
+ allow(MSpec).to receive(:process)
+
+ options = double("MSpecOptions").as_null_object
+ allow(options).to receive(:parse).and_return(["one", "two"])
+ allow(MSpecOptions).to receive(:new).and_return(options)
+
+ @config = { }
+ @script = MSpecTag.new
+ allow(@script).to receive(:exit)
+ allow(@script).to receive(:config).and_return(@config)
+ allow(@script).to receive(:files).and_return(["one", "two"])
+ @script.options
+ end
+
+ it "registers the tags patterns" do
+ @config[:tags_patterns] = [/spec/, "tags"]
+ expect(MSpec).to receive(:register_tags_patterns).with([/spec/, "tags"])
+ @script.run
+ end
+
+ it "registers the files to process" do
+ expect(MSpec).to receive(:register_files).with(["one", "two"])
+ @script.run
+ end
+
+ it "processes the files" do
+ expect(MSpec).to receive(:process)
+ @script.run
+ end
+
+ it "exits with the exit code registered with MSpec" do
+ allow(MSpec).to receive(:exit_code).and_return(7)
+ expect(@script).to receive(:exit).with(7)
+ @script.run
+ end
+end
+
+RSpec.describe MSpecTag, "#register" do
+ before :each do
+ @script = MSpecTag.new
+ @config = @script.config
+ @config[:tag] = "fake:"
+ @config[:atags] = []
+ @config[:astrings] = []
+ @config[:ltags] = ["fails", "unstable"]
+
+ allow(@script).to receive(:files).and_return([])
+ @script.options "fake"
+
+ @t = double("TagAction")
+ allow(@t).to receive(:register)
+
+ @tl = double("TagListAction")
+ allow(@tl).to receive(:register)
+ end
+
+ it "raises an ArgumentError if no recognized action is given" do
+ @config[:tagger] = :totally_whack
+ expect { @script.register }.to raise_error(ArgumentError)
+ end
+
+ describe "when config[:tagger] is the default (:add)" do
+ before :each do
+ @config[:formatter] = false
+ end
+
+ it "creates a TagAction" do
+ expect(TagAction).to receive(:new).and_return(@t)
+ @script.register
+ end
+
+ it "creates a TagAction if config[:tagger] is :del" do
+ @config[:tagger] = :del
+ @config[:outcome] = :pass
+ expect(TagAction).to receive(:new).with(:del, :pass, "fake", nil, [], []).and_return(@t)
+ @script.register
+ end
+
+ it "calls #register on the TagAction instance" do
+ expect(TagAction).to receive(:new).and_return(@t)
+ expect(@t).to receive(:register)
+ @script.register
+ end
+ end
+
+ describe "when config[:tagger] is :list" do
+ before :each do
+ expect(TagListAction).to receive(:new).with(@config[:ltags]).and_return(@tl)
+ @config[:tagger] = :list
+ end
+
+ it "creates a TagListAction" do
+ expect(@tl).to receive(:register)
+ @script.register
+ end
+
+ it "registers MSpec pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend)
+ @script.register
+ end
+
+ it "sets config[:formatter] to false" do
+ @script.register
+ expect(@config[:formatter]).to be_falsey
+ end
+ end
+
+ describe "when config[:tagger] is :list_all" do
+ before :each do
+ expect(TagListAction).to receive(:new).with(nil).and_return(@tl)
+ @config[:tagger] = :list_all
+ end
+
+ it "creates a TagListAction" do
+ expect(@tl).to receive(:register)
+ @script.register
+ end
+
+ it "registers MSpec pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend)
+ @script.register
+ end
+
+ it "sets config[:formatter] to false" do
+ @script.register
+ expect(@config[:formatter]).to be_falsey
+ end
+ end
+
+ describe "when config[:tagger] is :purge" do
+ before :each do
+ expect(TagPurgeAction).to receive(:new).and_return(@tl)
+ allow(MSpec).to receive(:register_mode)
+ @config[:tagger] = :purge
+ end
+
+ it "creates a TagPurgeAction" do
+ expect(@tl).to receive(:register)
+ @script.register
+ end
+
+ it "registers MSpec in pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend)
+ @script.register
+ end
+
+ it "registers MSpec in unguarded mode" do
+ expect(MSpec).to receive(:register_mode).with(:unguarded)
+ @script.register
+ end
+
+ it "sets config[:formatter] to false" do
+ @script.register
+ expect(@config[:formatter]).to be_falsey
+ end
+ end
+end
diff --git a/spec/mspec/spec/expectations/expectations_spec.rb b/spec/mspec/spec/expectations/expectations_spec.rb
new file mode 100644
index 0000000000..371829d4f9
--- /dev/null
+++ b/spec/mspec/spec/expectations/expectations_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+
+RSpec.describe SpecExpectationNotMetError do
+ it "is a subclass of StandardError" do
+ expect(SpecExpectationNotMetError.ancestors).to include(StandardError)
+ end
+end
+
+RSpec.describe SpecExpectationNotFoundError do
+ it "is a subclass of StandardError" do
+ expect(SpecExpectationNotFoundError.ancestors).to include(StandardError)
+ end
+end
+
+RSpec.describe SpecExpectationNotFoundError, "#message" do
+ it "returns 'No behavior expectation was found in the example'" do
+ m = SpecExpectationNotFoundError.new.message
+ expect(m).to eq("No behavior expectation was found in the example")
+ end
+end
+
+RSpec.describe SpecExpectation, "#fail_with" do
+ it "raises an SpecExpectationNotMetError" do
+ expect {
+ SpecExpectation.fail_with "expected this", "to equal that"
+ }.to raise_error(SpecExpectationNotMetError, "expected this to equal that")
+ end
+end
diff --git a/spec/mspec/spec/expectations/should_spec.rb b/spec/mspec/spec/expectations/should_spec.rb
new file mode 100644
index 0000000000..472890979d
--- /dev/null
+++ b/spec/mspec/spec/expectations/should_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+require 'rbconfig'
+
+RSpec.describe "MSpec" do
+ before :all do
+ path = RbConfig::CONFIG['bindir']
+ exe = RbConfig::CONFIG['ruby_install_name']
+ file = File.expand_path('../../fixtures/should.rb', __FILE__)
+ @out = `#{path}/#{exe} #{file}`
+ end
+
+ describe "#should" do
+ it "records failures" do
+ expect(@out).to include <<-EOS
+1)
+MSpec expectation method #should causes a failure to be recorded FAILED
+Expected 1 == 2
+to be truthy but was false
+EOS
+ end
+
+ it "raises exceptions for examples with no expectations" do
+ expect(@out).to include <<-EOS
+2)
+MSpec expectation method #should registers that an expectation has been encountered FAILED
+No behavior expectation was found in the example
+EOS
+ end
+ end
+
+ describe "#should_not" do
+ it "records failures" do
+ expect(@out).to include <<-EOS
+3)
+MSpec expectation method #should_not causes a failure to be recorded FAILED
+Expected 1 == 1
+to be falsy but was true
+EOS
+ end
+
+ it "raises exceptions for examples with no expectations" do
+ expect(@out).to include <<-EOS
+4)
+MSpec expectation method #should_not registers that an expectation has been encountered FAILED
+No behavior expectation was found in the example
+EOS
+ end
+ end
+
+ it "prints status information" do
+ expect(@out).to include ".FF..FF."
+ end
+
+ it "prints out a summary" do
+ expect(@out).to include "0 files, 8 examples, 6 expectations, 4 failures, 0 errors"
+ end
+
+ it "records expectations" do
+ expect(@out).to include "I was called 6 times"
+ end
+end
diff --git a/spec/mspec/spec/fixtures/a_spec.rb b/spec/mspec/spec/fixtures/a_spec.rb
new file mode 100644
index 0000000000..17a7e8b664
--- /dev/null
+++ b/spec/mspec/spec/fixtures/a_spec.rb
@@ -0,0 +1,15 @@
+unless defined?(RSpec)
+ describe "Foo#bar" do
+ it "passes" do
+ 1.should == 1
+ end
+
+ it "errors" do
+ 1.should == 2
+ end
+
+ it "fails" do
+ raise "failure"
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/b_spec.rb b/spec/mspec/spec/fixtures/b_spec.rb
new file mode 100644
index 0000000000..f1f63317cb
--- /dev/null
+++ b/spec/mspec/spec/fixtures/b_spec.rb
@@ -0,0 +1,7 @@
+unless defined?(RSpec)
+ describe "Bar#baz" do
+ it "works" do
+ 1.should == 1
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/chatty_spec.rb b/spec/mspec/spec/fixtures/chatty_spec.rb
new file mode 100644
index 0000000000..2d110d8ce4
--- /dev/null
+++ b/spec/mspec/spec/fixtures/chatty_spec.rb
@@ -0,0 +1,8 @@
+unless defined?(RSpec)
+ describe "Chatty#spec" do
+ it "prints too much" do
+ STDOUT.puts "Hello\nIt's me!"
+ 1.should == 1
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/config.mspec b/spec/mspec/spec/fixtures/config.mspec
new file mode 100644
index 0000000000..01654c5094
--- /dev/null
+++ b/spec/mspec/spec/fixtures/config.mspec
@@ -0,0 +1,8 @@
+class MSpecScript
+ set :target, 'ruby'
+
+ set :tags_patterns, [
+ [%r(spec/fixtures/), 'spec/fixtures/tags/'],
+ [/_spec.rb$/, '_tags.txt']
+ ]
+end
diff --git a/spec/mspec/spec/fixtures/die_spec.rb b/spec/mspec/spec/fixtures/die_spec.rb
new file mode 100644
index 0000000000..0f66793274
--- /dev/null
+++ b/spec/mspec/spec/fixtures/die_spec.rb
@@ -0,0 +1,7 @@
+unless defined?(RSpec)
+ describe "Deadly#spec" do
+ it "dies" do
+ abort "DEAD"
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/my_ruby b/spec/mspec/spec/fixtures/my_ruby
new file mode 100755
index 0000000000..eeda3eeeec
--- /dev/null
+++ b/spec/mspec/spec/fixtures/my_ruby
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo $RUBY_EXE
+exec ruby "$@"
diff --git a/spec/mspec/spec/fixtures/object_methods_spec.rb b/spec/mspec/spec/fixtures/object_methods_spec.rb
new file mode 100644
index 0000000000..9b7c1523e5
--- /dev/null
+++ b/spec/mspec/spec/fixtures/object_methods_spec.rb
@@ -0,0 +1,8 @@
+unless defined?(RSpec)
+ describe "Object" do
+ it ".public_instance_methods(false) is empty" do
+ Object.public_instance_methods(false).sort.should ==
+ [:should, :should_not, :should_not_receive, :should_receive, :stub!]
+ end
+ end
+end
diff --git a/spec/mspec/spec/fixtures/print_interpreter_spec.rb b/spec/mspec/spec/fixtures/print_interpreter_spec.rb
new file mode 100644
index 0000000000..a662346d0a
--- /dev/null
+++ b/spec/mspec/spec/fixtures/print_interpreter_spec.rb
@@ -0,0 +1,4 @@
+unless defined?(RSpec)
+ puts ENV["RUBY_EXE"]
+ puts ruby_cmd(nil).split.first
+end
diff --git a/spec/mspec/spec/fixtures/should.rb b/spec/mspec/spec/fixtures/should.rb
new file mode 100644
index 0000000000..f494775c5f
--- /dev/null
+++ b/spec/mspec/spec/fixtures/should.rb
@@ -0,0 +1,75 @@
+$: << File.dirname(__FILE__) + '/../../lib'
+require 'mspec'
+require 'mspec/utils/script'
+
+# The purpose of these specs is to confirm that the #should
+# and #should_not methods are functioning appropriately. We
+# use a separate spec file that is invoked from the MSpec
+# specs but is run by MSpec. This avoids conflicting with
+# RSpec's #should and #should_not methods.
+
+raise "RSpec should not be loaded" if defined?(RSpec)
+
+class ShouldSpecsMonitor
+ def initialize
+ @called = 0
+ end
+
+ def expectation(state)
+ @called += 1
+ end
+
+ def finish
+ puts "I was called #{@called} times"
+ end
+end
+
+# Simplistic runner
+formatter = DottedFormatter.new
+formatter.register
+
+monitor = ShouldSpecsMonitor.new
+MSpec.register :expectation, monitor
+MSpec.register :finish, monitor
+
+at_exit { MSpec.actions :finish }
+
+MSpec.actions :start
+MSpec.setup_env
+
+# Specs
+describe "MSpec expectation method #should" do
+ it "accepts a matcher" do
+ :sym.should be_kind_of(Symbol)
+ end
+
+ it "causes a failure to be recorded" do
+ 1.should == 2
+ end
+
+ it "registers that an expectation has been encountered" do
+ # an empty example block causes an exception because
+ # no expectation was encountered
+ end
+
+ it "invokes the MSpec :expectation actions" do
+ 1.should == 1
+ end
+end
+
+describe "MSpec expectation method #should_not" do
+ it "accepts a matcher" do
+ "sym".should_not be_kind_of(Symbol)
+ end
+
+ it "causes a failure to be recorded" do
+ 1.should_not == 1
+ end
+
+ it "registers that an expectation has been encountered" do
+ end
+
+ it "invokes the MSpec :expectation actions" do
+ 1.should_not == 2
+ end
+end
diff --git a/spec/mspec/spec/fixtures/tagging_spec.rb b/spec/mspec/spec/fixtures/tagging_spec.rb
new file mode 100644
index 0000000000..0097fd1808
--- /dev/null
+++ b/spec/mspec/spec/fixtures/tagging_spec.rb
@@ -0,0 +1,16 @@
+# encoding: utf-8
+unless defined?(RSpec)
+ describe "Tag#me" do
+ it "passes" do
+ 1.should == 1
+ end
+
+ it "errors" do
+ 1.should == 2
+ end
+
+ it "érròrs in unicode" do
+ 1.should == 2
+ end
+ end
+end
diff --git a/spec/mspec/spec/guards/block_device_spec.rb b/spec/mspec/spec/guards/block_device_spec.rb
new file mode 100644
index 0000000000..dd420d4a81
--- /dev/null
+++ b/spec/mspec/spec/guards/block_device_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#with_block_device" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = BlockDeviceGuard.new
+ allow(BlockDeviceGuard).to receive(:new).and_return(@guard)
+ end
+
+ platform_is_not :freebsd, :windows do
+ it "yields if block device is available" do
+ expect(@guard).to receive(:`).and_return("block devices")
+ with_block_device { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if block device is not available" do
+ expect(@guard).to receive(:`).and_return(nil)
+ with_block_device { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+ end
+
+ platform_is :freebsd, :windows do
+ it "does not yield, since platform does not support block devices" do
+ expect(@guard).not_to receive(:`)
+ with_block_device { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+ end
+
+ it "sets the name of the guard to :with_block_device" do
+ with_block_device { }
+ expect(@guard.name).to eq(:with_block_device)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ with_block_device { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/bug_spec.rb b/spec/mspec/spec/guards/bug_spec.rb
new file mode 100644
index 0000000000..72a3405dbc
--- /dev/null
+++ b/spec/mspec/spec/guards/bug_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe BugGuard, "#match? when #implementation? is 'ruby'" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ hide_deprecation_warnings
+ stub_const "VersionGuard::FULL_RUBY_VERSION", SpecVersion.new('1.8.6')
+ @ruby_engine = Object.const_get :RUBY_ENGINE
+ Object.const_set :RUBY_ENGINE, 'ruby'
+ end
+
+ after :each do
+ Object.const_set :RUBY_ENGINE, @ruby_engine
+ end
+
+ it "returns false when version argument is less than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.5").match?).to eq(false)
+ end
+
+ it "returns true when version argument is equal to RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(true)
+ end
+
+ it "returns true when version argument is greater than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.7").match?).to eq(true)
+ end
+
+ it "returns true when version argument implicitly includes RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8").match?).to eq(true)
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(true)
+ end
+
+ it "returns true when the argument range includes RUBY_VERSION" do
+ expect(BugGuard.new("#1", '1.8.5'..'1.8.7').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8'..'1.9').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8'...'1.9').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8'..'1.8.6').match?).to eq(true)
+ expect(BugGuard.new("#1", '1.8.5'..'1.8.6').match?).to eq(true)
+ expect(BugGuard.new("#1", ''...'1.8.7').match?).to eq(true)
+ end
+
+ it "returns false when the argument range does not include RUBY_VERSION" do
+ expect(BugGuard.new("#1", '1.8.7'..'1.8.9').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.4'..'1.8.5').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.4'...'1.8.6').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.5'...'1.8.6').match?).to eq(false)
+ expect(BugGuard.new("#1", ''...'1.8.6').match?).to eq(false)
+ end
+
+ it "returns false when MSpec.mode?(:no_ruby_bug) is true" do
+ expect(MSpec).to receive(:mode?).with(:no_ruby_bug).twice.and_return(:true)
+ expect(BugGuard.new("#1", "1.8.5").match?).to eq(false)
+ expect(BugGuard.new("#1", "1.8").match?).to eq(false)
+ end
+end
+
+RSpec.describe BugGuard, "#match? when #implementation? is not 'ruby'" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ hide_deprecation_warnings
+ @ruby_version = Object.const_get :RUBY_VERSION
+ @ruby_engine = Object.const_get :RUBY_ENGINE
+
+ Object.const_set :RUBY_VERSION, '1.8.6'
+ Object.const_set :RUBY_ENGINE, 'jruby'
+ end
+
+ after :each do
+ Object.const_set :RUBY_VERSION, @ruby_version
+ Object.const_set :RUBY_ENGINE, @ruby_engine
+ end
+
+ it "returns false when version argument is less than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8").match?).to eq(false)
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(false)
+ end
+
+ it "returns false when version argument is equal to RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(false)
+ end
+
+ it "returns false when version argument is greater than RUBY_VERSION" do
+ expect(BugGuard.new("#1", "1.8.7").match?).to eq(false)
+ end
+
+ it "returns false no matter if the argument range includes RUBY_VERSION" do
+ expect(BugGuard.new("#1", '1.8'...'1.9').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.5'...'1.8.7').match?).to eq(false)
+ expect(BugGuard.new("#1", '1.8.4'...'1.8.6').match?).to eq(false)
+ end
+
+ it "returns false when MSpec.mode?(:no_ruby_bug) is true" do
+ allow(MSpec).to receive(:mode?).and_return(:true)
+ expect(BugGuard.new("#1", "1.8.6").match?).to eq(false)
+ end
+end
+
+RSpec.describe Object, "#ruby_bug" do
+ before :each do
+ hide_deprecation_warnings
+ @guard = BugGuard.new "#1234", "x.x.x"
+ allow(BugGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ ruby_bug("#1234", "1.8.6") { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when #match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ ruby_bug("#1234", "1.8.6") { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "requires a bug tracker number and a version number" do
+ expect { ruby_bug { } }.to raise_error(ArgumentError)
+ expect { ruby_bug("#1234") { } }.to raise_error(ArgumentError)
+ end
+
+ it "sets the name of the guard to :ruby_bug" do
+ ruby_bug("#1234", "1.8.6") { }
+ expect(@guard.name).to eq(:ruby_bug)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:unregister)
+ expect do
+ ruby_bug("", "") { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/conflict_spec.rb b/spec/mspec/spec/guards/conflict_spec.rb
new file mode 100644
index 0000000000..7dbe83153d
--- /dev/null
+++ b/spec/mspec/spec/guards/conflict_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#conflicts_with" do
+ before :each do
+ hide_deprecation_warnings
+ ScratchPad.clear
+ end
+
+ it "does not yield if Object.constants includes any of the arguments" do
+ allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"])
+ conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "does not yield if Object.constants (as Symbols) includes any of the arguments" do
+ allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass])
+ conflicts_with(:SomeClass, :AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields if Object.constants does not include any of the arguments" do
+ allow(Object).to receive(:constants).and_return(["SomeClass", "OtherClass"])
+ conflicts_with(:AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "yields if Object.constants (as Symbols) does not include any of the arguments" do
+ allow(Object).to receive(:constants).and_return([:SomeClass, :OtherClass])
+ conflicts_with(:AClass, :BClass) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+end
+
+RSpec.describe Object, "#conflicts_with" do
+ before :each do
+ hide_deprecation_warnings
+ @guard = ConflictsGuard.new
+ allow(ConflictsGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :conflicts_with" do
+ conflicts_with(:AClass, :BClass) { }
+ expect(@guard.name).to eq(:conflicts_with)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:unregister)
+ expect do
+ conflicts_with(:AClass, :BClass) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/endian_spec.rb b/spec/mspec/spec/guards/endian_spec.rb
new file mode 100644
index 0000000000..943b558ed3
--- /dev/null
+++ b/spec/mspec/spec/guards/endian_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#big_endian" do
+ before :each do
+ @guard = BigEndianGuard.new
+ allow(BigEndianGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields on big-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\001])
+ big_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield on little-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\000])
+ big_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "sets the name of the guard to :big_endian" do
+ big_endian { }
+ expect(@guard.name).to eq(:big_endian)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ allow(@guard).to receive(:pattern).and_return([?\001])
+ expect(@guard).to receive(:unregister)
+ expect do
+ big_endian { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#little_endian" do
+ before :each do
+ @guard = BigEndianGuard.new
+ allow(BigEndianGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields on little-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\000])
+ little_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield on big-endian platforms" do
+ allow(@guard).to receive(:pattern).and_return([?\001])
+ little_endian { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
diff --git a/spec/mspec/spec/guards/feature_spec.rb b/spec/mspec/spec/guards/feature_spec.rb
new file mode 100644
index 0000000000..fcb8997591
--- /dev/null
+++ b/spec/mspec/spec/guards/feature_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe FeatureGuard, ".enabled?" do
+ it "returns true if the feature is enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true)
+ expect(FeatureGuard.enabled?(:encoding)).to be_truthy
+ end
+
+ it "returns false if the feature is not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false)
+ expect(FeatureGuard.enabled?(:encoding)).to be_falsey
+ end
+
+ it "returns true if all the features are enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(true)
+ expect(FeatureGuard.enabled?(:one, :two)).to be_truthy
+ end
+
+ it "returns false if any of the features are not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(false)
+ expect(FeatureGuard.enabled?(:one, :two)).to be_falsey
+ end
+end
+
+RSpec.describe Object, "#with_feature" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = FeatureGuard.new :encoding
+ allow(FeatureGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :with_feature" do
+ with_feature(:encoding) { }
+ expect(@guard.name).to eq(:with_feature)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ with_feature { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#with_feature" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "yields if the feature is enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true)
+ with_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "yields if all the features are enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(true)
+ with_feature(:one, :two) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if the feature is not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false)
+ with_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to be_nil
+ end
+
+ it "does not yield if any of the features are not enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:one).and_return(true)
+ expect(MSpec).to receive(:feature_enabled?).with(:two).and_return(false)
+ with_feature(:one, :two) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to be_nil
+ end
+end
+
+RSpec.describe Object, "#without_feature" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = FeatureGuard.new :encoding
+ allow(FeatureGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :without_feature" do
+ without_feature(:encoding) { }
+ expect(@guard.name).to eq(:without_feature)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ without_feature { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#without_feature" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "does not yield if the feature is enabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(true)
+ without_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to be_nil
+ end
+
+ it "yields if the feature is disabled" do
+ expect(MSpec).to receive(:feature_enabled?).with(:encoding).and_return(false)
+ without_feature(:encoding) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+end
diff --git a/spec/mspec/spec/guards/guard_spec.rb b/spec/mspec/spec/guards/guard_spec.rb
new file mode 100644
index 0000000000..e29d235747
--- /dev/null
+++ b/spec/mspec/spec/guards/guard_spec.rb
@@ -0,0 +1,421 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'rbconfig'
+
+RSpec.describe SpecGuard, ".ruby_version" do
+ before :each do
+ stub_const "RUBY_VERSION", "8.2.3"
+ end
+
+ it "returns the full version for :full" do
+ expect(SpecGuard.ruby_version(:full)).to eq("8.2.3")
+ end
+
+ it "returns major.minor.tiny for :tiny" do
+ expect(SpecGuard.ruby_version(:tiny)).to eq("8.2.3")
+ end
+
+ it "returns major.minor.tiny for :teeny" do
+ expect(SpecGuard.ruby_version(:tiny)).to eq("8.2.3")
+ end
+
+ it "returns major.minor for :minor" do
+ expect(SpecGuard.ruby_version(:minor)).to eq("8.2")
+ end
+
+ it "defaults to :minor" do
+ expect(SpecGuard.ruby_version).to eq("8.2")
+ end
+
+ it "returns major for :major" do
+ expect(SpecGuard.ruby_version(:major)).to eq("8")
+ end
+end
+
+RSpec.describe SpecGuard, "#yield?" do
+ before :each do
+ MSpec.clear_modes
+ @guard = SpecGuard.new
+ allow(@guard).to receive(:match?).and_return(false)
+ end
+
+ after :each do
+ MSpec.unregister :add, @guard
+ MSpec.clear_modes
+ SpecGuard.clear_guards
+ end
+
+ it "returns true if MSpec.mode?(:unguarded) is true" do
+ MSpec.register_mode :unguarded
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:verify) is true" do
+ MSpec.register_mode :verify
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:verify) is true regardless of invert being true" do
+ MSpec.register_mode :verify
+ expect(@guard.yield?(true)).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:report) is true" do
+ MSpec.register_mode :report
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:report) is true regardless of invert being true" do
+ MSpec.register_mode :report
+ expect(@guard.yield?(true)).to eq(true)
+ end
+
+ it "returns true if MSpec.mode?(:report_on) is true and SpecGuards.guards contains the named guard" do
+ MSpec.register_mode :report_on
+ SpecGuard.guards << :guard_name
+ expect(@guard.yield?).to eq(false)
+ @guard.name = :guard_name
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns #match? if neither report nor verify mode are true" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.yield?).to eq(false)
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.yield?).to eq(true)
+ end
+
+ it "returns #match? if invert is true and neither report nor verify mode are true" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.yield?(true)).to eq(true)
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.yield?(true)).to eq(false)
+ end
+end
+
+RSpec.describe SpecGuard, "#match?" do
+ before :each do
+ @guard = SpecGuard.new
+ end
+
+ it "must be implemented in subclasses" do
+ expect {
+ @guard.match?
+ }.to raise_error("must be implemented by the subclass")
+ end
+end
+
+RSpec.describe SpecGuard, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ @guard = SpecGuard.new
+ end
+
+ it "unregisters from MSpec :add actions" do
+ expect(MSpec).to receive(:unregister).with(:add, @guard)
+ @guard.unregister
+ end
+end
+
+RSpec.describe SpecGuard, "#record" do
+ after :each do
+ SpecGuard.clear
+ end
+
+ it "saves the name of the guarded spec under the name of the guard" do
+ guard = SpecGuard.new "a", "1.8"..."1.9"
+ guard.name = :named_guard
+ guard.record "SomeClass#action returns true"
+ expect(SpecGuard.report).to eq({
+ 'named_guard a, 1.8...1.9' => ["SomeClass#action returns true"]
+ })
+ end
+end
+
+RSpec.describe SpecGuard, ".guards" do
+ it "returns an Array" do
+ expect(SpecGuard.guards).to be_kind_of(Array)
+ end
+end
+
+RSpec.describe SpecGuard, ".clear_guards" do
+ it "resets the array to empty" do
+ SpecGuard.guards << :guard
+ expect(SpecGuard.guards).to eq([:guard])
+ SpecGuard.clear_guards
+ expect(SpecGuard.guards).to eq([])
+ end
+end
+
+RSpec.describe SpecGuard, ".finish" do
+ before :each do
+ $stdout = @out = IOStub.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ SpecGuard.clear
+ end
+
+ it "prints the descriptions of the guarded specs" do
+ guard = SpecGuard.new "a", "1.8"..."1.9"
+ guard.name = :named_guard
+ guard.record "SomeClass#action returns true"
+ guard.record "SomeClass#reverse returns false"
+ SpecGuard.finish
+ expect($stdout).to eq(%[
+
+2 specs omitted by guard: named_guard a, 1.8...1.9:
+
+SomeClass#action returns true
+SomeClass#reverse returns false
+
+])
+ end
+end
+
+RSpec.describe SpecGuard, ".run_if" do
+ before :each do
+ @guard = SpecGuard.new
+ ScratchPad.clear
+ end
+
+ it "yields if match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ @guard.run_if(:name) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ @guard.run_if(:name) { fail }
+ end
+
+ it "returns the result of the block if match? is true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_if(:name) { 42 }).to eq(42)
+ end
+
+ it "returns nil if given a block and match? is false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_if(:name) { 42 }).to eq(nil)
+ end
+
+ it "returns what #match? returns when no block is given" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_if(:name)).to eq(true)
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_if(:name)).to eq(false)
+ end
+end
+
+RSpec.describe SpecGuard, ".run_unless" do
+ before :each do
+ @guard = SpecGuard.new
+ ScratchPad.clear
+ end
+
+ it "yields if match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ @guard.run_unless(:name) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield if match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ @guard.run_unless(:name) { fail }
+ end
+
+ it "returns the result of the block if match? is false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_unless(:name) { 42 }).to eq(42)
+ end
+
+ it "returns nil if given a block and match? is true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_unless(:name) { 42 }).to eq(nil)
+ end
+
+ it "returns the opposite of what #match? returns when no block is given" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(@guard.run_unless(:name)).to eq(false)
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(@guard.run_unless(:name)).to eq(true)
+ end
+end
+
+RSpec.describe Object, "#guard" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ after :each do
+ MSpec.clear_modes
+ end
+
+ it "allows to combine guards" do
+ guard1 = VersionGuard.new '1.2.3', 'x.x.x'
+ allow(VersionGuard).to receive(:new).and_return(guard1)
+ guard2 = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(guard2)
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield
+ end
+ expect(ScratchPad.recorded).to eq(:yield)
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+ end
+
+ it "yields when the Proc returns true" do
+ guard -> { true } do
+ ScratchPad.record :yield
+ end
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when the Proc returns false" do
+ guard -> { false } do
+ fail
+ end
+ end
+
+ it "yields if MSpec.mode?(:unguarded) is true" do
+ MSpec.register_mode :unguarded
+
+ guard -> { false } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ guard -> { true } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+ end
+
+ it "yields if MSpec.mode?(:verify) is true" do
+ MSpec.register_mode :verify
+
+ guard -> { false } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ guard -> { true } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+ end
+
+ it "yields if MSpec.mode?(:report) is true" do
+ MSpec.register_mode :report
+
+ guard -> { false } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ guard -> { true } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+ end
+
+ it "raises an error if no Proc is given" do
+ expect { guard :foo }.to raise_error(RuntimeError)
+ end
+
+ it "requires a block" do
+ expect {
+ guard(-> { true })
+ }.to raise_error(LocalJumpError)
+ expect {
+ guard(-> { false })
+ }.to raise_error(LocalJumpError)
+ end
+end
+
+RSpec.describe Object, "#guard_not" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "allows to combine guards" do
+ guard1 = VersionGuard.new '1.2.3', 'x.x.x'
+ allow(VersionGuard).to receive(:new).and_return(guard1)
+ guard2 = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(guard2)
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ fail
+ end
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(true)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield1
+ end
+ expect(ScratchPad.recorded).to eq(:yield1)
+
+ allow(guard1).to receive(:match?).and_return(true)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield2
+ end
+ expect(ScratchPad.recorded).to eq(:yield2)
+
+ allow(guard1).to receive(:match?).and_return(false)
+ allow(guard2).to receive(:match?).and_return(false)
+ guard_not -> { ruby_version_is "2.4" and platform_is :linux } do
+ ScratchPad.record :yield3
+ end
+ expect(ScratchPad.recorded).to eq(:yield3)
+ end
+
+ it "yields when the Proc returns false" do
+ guard_not -> { false } do
+ ScratchPad.record :yield
+ end
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when the Proc returns true" do
+ guard_not -> { true } do
+ fail
+ end
+ end
+
+ it "raises an error if no Proc is given" do
+ expect { guard_not :foo }.to raise_error(RuntimeError)
+ end
+
+ it "requires a block" do
+ expect {
+ guard_not(-> { true })
+ }.to raise_error(LocalJumpError)
+ expect {
+ guard_not(-> { false })
+ }.to raise_error(LocalJumpError)
+ end
+end
diff --git a/spec/mspec/spec/guards/platform_spec.rb b/spec/mspec/spec/guards/platform_spec.rb
new file mode 100644
index 0000000000..bd37432800
--- /dev/null
+++ b/spec/mspec/spec/guards/platform_spec.rb
@@ -0,0 +1,337 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#platform_is" do
+ before :each do
+ @guard = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "does not yield when #os? returns false" do
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ platform_is(:ruby) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when #os? returns true" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ platform_is(:solarce) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "returns what #os? returns when no block is given" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ expect(platform_is(:solarce)).to eq(true)
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ expect(platform_is(:solarce)).to eq(false)
+ end
+
+ it "sets the name of the guard to :platform_is" do
+ platform_is(:solarce) { }
+ expect(@guard.name).to eq(:platform_is)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ platform_is(:solarce) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#platform_is_not" do
+ before :each do
+ @guard = PlatformGuard.new :dummy
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "does not yield when #os? returns true" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ platform_is_not(:ruby) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when #os? returns false" do
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ platform_is_not(:solarce) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "returns the opposite of what #os? returns when no block is given" do
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ expect(platform_is_not(:solarce)).to eq(false)
+ allow(PlatformGuard).to receive(:os?).and_return(false)
+ expect(platform_is_not(:solarce)).to eq(true)
+ end
+
+ it "sets the name of the guard to :platform_is_not" do
+ platform_is_not(:solarce) { }
+ expect(@guard.name).to eq(:platform_is_not)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ platform_is_not(:solarce) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#platform_is :c_long_size => SIZE_SPEC" do
+ before :each do
+ @guard = PlatformGuard.new :darwin, :c_long_size => 32
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #c_long_size? returns true" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(true)
+ platform_is(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "doesn not yield when #c_long_size? returns false" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(false)
+ platform_is(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
+
+RSpec.describe Object, "#platform_is_not :c_long_size => SIZE_SPEC" do
+ before :each do
+ @guard = PlatformGuard.new :darwin, :c_long_size => 32
+ allow(PlatformGuard).to receive(:os?).and_return(true)
+ allow(PlatformGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #c_long_size? returns false" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(false)
+ platform_is_not(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "doesn not yield when #c_long_size? returns true" do
+ allow(PlatformGuard).to receive(:c_long_size?).and_return(true)
+ platform_is_not(:c_long_size => 32) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
+
+RSpec.describe PlatformGuard, ".implementation?" do
+ it "returns true if passed :ruby and RUBY_ENGINE == 'ruby'" do
+ stub_const 'RUBY_ENGINE', 'ruby'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+ end
+
+ it "returns true if passed :rubinius and RUBY_ENGINE == 'rbx'" do
+ stub_const 'RUBY_ENGINE', 'rbx'
+ expect(PlatformGuard.implementation?(:rubinius)).to eq(true)
+ end
+
+ it "returns true if passed :jruby and RUBY_ENGINE == 'jruby'" do
+ stub_const 'RUBY_ENGINE', 'jruby'
+ expect(PlatformGuard.implementation?(:jruby)).to eq(true)
+ end
+
+ it "returns true if passed :ironruby and RUBY_ENGINE == 'ironruby'" do
+ stub_const 'RUBY_ENGINE', 'ironruby'
+ expect(PlatformGuard.implementation?(:ironruby)).to eq(true)
+ end
+
+ it "returns true if passed :maglev and RUBY_ENGINE == 'maglev'" do
+ stub_const 'RUBY_ENGINE', 'maglev'
+ expect(PlatformGuard.implementation?(:maglev)).to eq(true)
+ end
+
+ it "returns true if passed :topaz and RUBY_ENGINE == 'topaz'" do
+ stub_const 'RUBY_ENGINE', 'topaz'
+ expect(PlatformGuard.implementation?(:topaz)).to eq(true)
+ end
+
+ it "returns true if passed :ruby and RUBY_ENGINE matches /^ruby/" do
+ stub_const 'RUBY_ENGINE', 'ruby'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+
+ stub_const 'RUBY_ENGINE', 'ruby1.8'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+
+ stub_const 'RUBY_ENGINE', 'ruby1.9'
+ expect(PlatformGuard.implementation?(:ruby)).to eq(true)
+ end
+
+ it "works for an unrecognized name" do
+ stub_const 'RUBY_ENGINE', 'myrubyimplementation'
+ expect(PlatformGuard.implementation?(:myrubyimplementation)).to eq(true)
+ expect(PlatformGuard.implementation?(:other)).to eq(false)
+ end
+end
+
+RSpec.describe PlatformGuard, ".standard?" do
+ it "returns true if implementation? returns true" do
+ expect(PlatformGuard).to receive(:implementation?).with(:ruby).and_return(true)
+ expect(PlatformGuard.standard?).to be_truthy
+ end
+
+ it "returns false if implementation? returns false" do
+ expect(PlatformGuard).to receive(:implementation?).with(:ruby).and_return(false)
+ expect(PlatformGuard.standard?).to be_falsey
+ end
+end
+
+RSpec.describe PlatformGuard, ".c_long_size?" do
+ it "returns true when arg is 32 and 1.size is 4" do
+ expect(PlatformGuard.c_long_size?(32)).to eq(1.size == 4)
+ end
+
+ it "returns true when arg is 64 and 1.size is 8" do
+ expect(PlatformGuard.c_long_size?(64)).to eq(1.size == 8)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os?" do
+ before :each do
+ stub_const 'PlatformGuard::PLATFORM', 'solarce'
+ end
+
+ it "returns false when arg does not match the platform" do
+ expect(PlatformGuard.os?(:ruby)).to eq(false)
+ end
+
+ it "returns false when no arg matches the platform" do
+ expect(PlatformGuard.os?(:ruby, :jruby, :rubinius, :maglev)).to eq(false)
+ end
+
+ it "returns true when arg matches the platform" do
+ expect(PlatformGuard.os?(:solarce)).to eq(true)
+ end
+
+ it "returns true when any arg matches the platform" do
+ expect(PlatformGuard.os?(:ruby, :jruby, :solarce, :rubinius, :maglev)).to eq(true)
+ end
+
+ it "returns true when arg is :windows and the platform contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'mswin32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns true when arg is :windows and the platform contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mswin32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os?" do
+ it "returns true if called with the current OS or architecture" do
+ os = RbConfig::CONFIG["host_os"].sub("-gnu", "")
+ arch = RbConfig::CONFIG["host_arch"]
+ expect(PlatformGuard.os?(os)).to eq(true)
+ expect(PlatformGuard.os?(arch)).to eq(true)
+ expect(PlatformGuard.os?("#{arch}-#{os}")).to eq(true)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os? on JRuby" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ @ruby_platform = Object.const_get :RUBY_PLATFORM
+ Object.const_set :RUBY_PLATFORM, 'java'
+ end
+
+ after :each do
+ Object.const_set :RUBY_PLATFORM, @ruby_platform
+ end
+
+ it "raises an error when testing for a :java platform" do
+ expect {
+ PlatformGuard.os?(:java)
+ }.to raise_error(":java is not a valid OS")
+ end
+
+ it "returns true when arg is :windows and RUBY_PLATFORM contains 'java' and os?(:windows) is true" do
+ stub_const 'PlatformGuard::PLATFORM', 'mswin32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns true when RUBY_PLATFORM contains 'java' and os?(argument) is true" do
+ stub_const 'PlatformGuard::PLATFORM', 'amiga'
+ expect(PlatformGuard.os?(:amiga)).to eq(true)
+ end
+end
+
+RSpec.describe PlatformGuard, ".os?" do
+ before :each do
+ stub_const 'PlatformGuard::PLATFORM', 'unreal'
+ end
+
+ it "returns true if argument matches RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:unreal)).to eq(true)
+ end
+
+ it "returns true if any argument matches RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:bsd, :unreal, :amiga)).to eq(true)
+ end
+
+ it "returns false if no argument matches RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:bsd, :netbsd, :amiga, :msdos)).to eq(false)
+ end
+
+ it "returns false if argument does not match RbConfig::CONFIG['host_os']" do
+ expect(PlatformGuard.os?(:amiga)).to eq(false)
+ end
+
+ it "returns true when arg is :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mswin32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns true when arg is :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:windows)).to eq(true)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mswin'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+
+ it "returns false when arg is not :windows and RbConfig::CONFIG['host_os'] contains 'mingw'" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.os?(:linux)).to eq(false)
+ end
+end
+
+RSpec.describe PlatformGuard, ".windows?" do
+ it "returns true on windows" do
+ stub_const 'PlatformGuard::PLATFORM', 'i386-mingw32'
+ expect(PlatformGuard.windows?).to eq(true)
+ end
+
+ it "returns false on non-windows" do
+ stub_const 'PlatformGuard::PLATFORM', 'i586-linux'
+ expect(PlatformGuard.windows?).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/guards/quarantine_spec.rb b/spec/mspec/spec/guards/quarantine_spec.rb
new file mode 100644
index 0000000000..eb5ff1da27
--- /dev/null
+++ b/spec/mspec/spec/guards/quarantine_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe QuarantineGuard, "#match?" do
+ it "returns true" do
+ expect(QuarantineGuard.new.match?).to eq(true)
+ end
+end
+
+RSpec.describe Object, "#quarantine!" do
+ before :each do
+ ScratchPad.clear
+
+ @guard = QuarantineGuard.new
+ allow(QuarantineGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "does not yield" do
+ quarantine! { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "sets the name of the guard to :quarantine!" do
+ quarantine! { }
+ expect(@guard.name).to eq(:quarantine!)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ quarantine! { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/superuser_spec.rb b/spec/mspec/spec/guards/superuser_spec.rb
new file mode 100644
index 0000000000..aba2cc2bb0
--- /dev/null
+++ b/spec/mspec/spec/guards/superuser_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#as_superuser" do
+ before :each do
+ @guard = SuperUserGuard.new
+ allow(SuperUserGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "does not yield when Process.euid is not 0" do
+ allow(Process).to receive(:euid).and_return(501)
+ as_superuser { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when Process.euid is 0" do
+ allow(Process).to receive(:euid).and_return(0)
+ as_superuser { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "sets the name of the guard to :as_superuser" do
+ as_superuser { }
+ expect(@guard.name).to eq(:as_superuser)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ as_superuser { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/support_spec.rb b/spec/mspec/spec/guards/support_spec.rb
new file mode 100644
index 0000000000..a61d003d6c
--- /dev/null
+++ b/spec/mspec/spec/guards/support_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#not_supported_on" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "raises an Exception when passed :ruby" do
+ stub_const "RUBY_ENGINE", "jruby"
+ expect {
+ not_supported_on(:ruby) { ScratchPad.record :yield }
+ }.to raise_error(Exception)
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "does not yield when #implementation? returns true" do
+ stub_const "RUBY_ENGINE", "jruby"
+ not_supported_on(:jruby) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "yields when #standard? returns true" do
+ stub_const "RUBY_ENGINE", "ruby"
+ not_supported_on(:rubinius) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "yields when #implementation? returns false" do
+ stub_const "RUBY_ENGINE", "jruby"
+ not_supported_on(:rubinius) { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+end
+
+RSpec.describe Object, "#not_supported_on" do
+ before :each do
+ @guard = SupportedGuard.new
+ allow(SupportedGuard).to receive(:new).and_return(@guard)
+ end
+
+ it "sets the name of the guard to :not_supported_on" do
+ not_supported_on(:rubinius) { }
+ expect(@guard.name).to eq(:not_supported_on)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(false)
+ expect(@guard).to receive(:unregister)
+ expect do
+ not_supported_on(:rubinius) { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
diff --git a/spec/mspec/spec/guards/user_spec.rb b/spec/mspec/spec/guards/user_spec.rb
new file mode 100644
index 0000000000..2526504656
--- /dev/null
+++ b/spec/mspec/spec/guards/user_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+RSpec.describe Object, "#as_user" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "yields when the Process.euid is not 0" do
+ allow(Process).to receive(:euid).and_return(501)
+ as_user { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when the Process.euid is 0" do
+ allow(Process).to receive(:euid).and_return(0)
+ as_user { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+end
diff --git a/spec/mspec/spec/guards/version_spec.rb b/spec/mspec/spec/guards/version_spec.rb
new file mode 100644
index 0000000000..5a5f4ddc3b
--- /dev/null
+++ b/spec/mspec/spec/guards/version_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+require 'mspec/guards'
+
+# The VersionGuard specifies a version of Ruby with a String of
+# the form: v = 'major.minor.tiny'.
+#
+# A VersionGuard instance can be created with a single String,
+# which means any version >= each component of v.
+# Or, the guard can be created with a Range, a..b, or a...b,
+# where a, b are of the same form as v. The meaning of the Range
+# is as typically understood: a..b means v >= a and v <= b;
+# a...b means v >= a and v < b.
+
+RSpec.describe VersionGuard, "#match?" do
+ before :each do
+ hide_deprecation_warnings
+ @current = '1.8.6'
+ end
+
+ it "returns true when the argument is equal to RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.6').match?).to eq(true)
+ end
+
+ it "returns true when the argument is less than RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8.5').match?).to eq(true)
+ end
+
+ it "returns false when the argument is greater than RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.7').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.9.2').match?).to eq(false)
+ end
+
+ it "returns true when the argument range includes RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.5'..'1.8.7').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8'..'1.9').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8'...'1.9').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8'..'1.8.6').match?).to eq(true)
+ expect(VersionGuard.new(@current, '1.8.5'..'1.8.6').match?).to eq(true)
+ expect(VersionGuard.new(@current, ''...'1.8.7').match?).to eq(true)
+ end
+
+ it "returns false when the argument range does not include RUBY_VERSION" do
+ expect(VersionGuard.new(@current, '1.8.7'..'1.8.9').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.8.4'..'1.8.5').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.8.4'...'1.8.6').match?).to eq(false)
+ expect(VersionGuard.new(@current, '1.8.5'...'1.8.6').match?).to eq(false)
+ expect(VersionGuard.new(@current, ''...'1.8.6').match?).to eq(false)
+ end
+end
+
+RSpec.describe Object, "#ruby_version_is" do
+ before :each do
+ @guard = VersionGuard.new '1.2.3', 'x.x.x'
+ allow(VersionGuard).to receive(:new).and_return(@guard)
+ ScratchPad.clear
+ end
+
+ it "yields when #match? returns true" do
+ allow(@guard).to receive(:match?).and_return(true)
+ ruby_version_is('x.x.x') { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).to eq(:yield)
+ end
+
+ it "does not yield when #match? returns false" do
+ allow(@guard).to receive(:match?).and_return(false)
+ ruby_version_is('x.x.x') { ScratchPad.record :yield }
+ expect(ScratchPad.recorded).not_to eq(:yield)
+ end
+
+ it "returns what #match? returns when no block is given" do
+ allow(@guard).to receive(:match?).and_return(true)
+ expect(ruby_version_is('x.x.x')).to eq(true)
+ allow(@guard).to receive(:match?).and_return(false)
+ expect(ruby_version_is('x.x.x')).to eq(false)
+ end
+
+ it "sets the name of the guard to :ruby_version_is" do
+ ruby_version_is("") { }
+ expect(@guard.name).to eq(:ruby_version_is)
+ end
+
+ it "calls #unregister even when an exception is raised in the guard block" do
+ expect(@guard).to receive(:match?).and_return(true)
+ expect(@guard).to receive(:unregister)
+ expect do
+ ruby_version_is("") { raise Exception }
+ end.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#version_is" do
+ before :each do
+ hide_deprecation_warnings
+ end
+
+ it "returns the expected values" do
+ expect(version_is('1.2.3', '1.2.2')).to eq(true)
+ expect(version_is('1.2.3', '1.2.3')).to eq(true)
+ expect(version_is('1.2.3', '1.2.4')).to eq(false)
+
+ expect(version_is('1.2.3', '1')).to eq(true)
+ expect(version_is('1.2.3', '1.0')).to eq(true)
+ expect(version_is('1.2.3', '2')).to eq(false)
+ expect(version_is('1.2.3', '2.0')).to eq(false)
+
+ expect(version_is('1.2.3', '1.2.2'..'1.2.4')).to eq(true)
+ expect(version_is('1.2.3', '1.2.2'..'1.2.3')).to eq(true)
+ expect(version_is('1.2.3', '1.2.2'...'1.2.3')).to eq(false)
+ expect(version_is('1.2.3', '1.2.3'..'1.2.4')).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/helpers/argf_spec.rb b/spec/mspec/spec/helpers/argf_spec.rb
new file mode 100644
index 0000000000..1412d71f84
--- /dev/null
+++ b/spec/mspec/spec/helpers/argf_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#argf" do
+ before :each do
+ @saved_argv = ARGV.dup
+ @argv = [__FILE__]
+ end
+
+ it "sets @argf to an instance of ARGF.class with the given argv" do
+ argf @argv do
+ expect(@argf).to be_an_instance_of ARGF.class
+ expect(@argf.filename).to eq(@argv.first)
+ end
+ expect(@argf).to be_nil
+ end
+
+ it "does not alter ARGV nor ARGF" do
+ argf @argv do
+ end
+ expect(ARGV).to eq(@saved_argv)
+ expect(ARGF.argv).to eq(@saved_argv)
+ end
+
+ it "does not close STDIN" do
+ argf ['-'] do
+ end
+ expect(STDIN).not_to be_closed
+ end
+
+ it "disallows nested calls" do
+ argf @argv do
+ expect { argf @argv }.to raise_error
+ end
+ end
+end
diff --git a/spec/mspec/spec/helpers/argv_spec.rb b/spec/mspec/spec/helpers/argv_spec.rb
new file mode 100644
index 0000000000..1db7e38650
--- /dev/null
+++ b/spec/mspec/spec/helpers/argv_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#argv" do
+ before :each do
+ ScratchPad.clear
+
+ @saved_argv = ARGV.dup
+ @argv = ["a", "b"]
+ end
+
+ it "replaces and restores the value of ARGV" do
+ argv @argv
+ expect(ARGV).to eq(@argv)
+ argv :restore
+ expect(ARGV).to eq(@saved_argv)
+ end
+
+ it "yields to the block after setting ARGV" do
+ argv @argv do
+ ScratchPad.record ARGV.dup
+ end
+ expect(ScratchPad.recorded).to eq(@argv)
+ expect(ARGV).to eq(@saved_argv)
+ end
+end
diff --git a/spec/mspec/spec/helpers/datetime_spec.rb b/spec/mspec/spec/helpers/datetime_spec.rb
new file mode 100644
index 0000000000..af4f557376
--- /dev/null
+++ b/spec/mspec/spec/helpers/datetime_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#new_datetime" do
+ it "returns a default DateTime instance" do
+ expect(new_datetime).to eq(DateTime.new)
+ end
+
+ it "returns a DateTime instance with the specified year value" do
+ d = new_datetime :year => 1970
+ expect(d.year).to eq(1970)
+ end
+
+ it "returns a DateTime instance with the specified month value" do
+ d = new_datetime :month => 11
+ expect(d.mon).to eq(11)
+ end
+
+ it "returns a DateTime instance with the specified day value" do
+ d = new_datetime :day => 23
+ expect(d.day).to eq(23)
+ end
+
+ it "returns a DateTime instance with the specified hour value" do
+ d = new_datetime :hour => 10
+ expect(d.hour).to eq(10)
+ end
+
+ it "returns a DateTime instance with the specified minute value" do
+ d = new_datetime :minute => 10
+ expect(d.min).to eq(10)
+ end
+
+ it "returns a DateTime instance with the specified second value" do
+ d = new_datetime :second => 2
+ expect(d.sec).to eq(2)
+ end
+
+ it "returns a DateTime instance with the specified offset value" do
+ d = new_datetime :offset => Rational(3,24)
+ expect(d.offset).to eq(Rational(3,24))
+ end
+end
diff --git a/spec/mspec/spec/helpers/fixture_spec.rb b/spec/mspec/spec/helpers/fixture_spec.rb
new file mode 100644
index 0000000000..d8e2ae7be4
--- /dev/null
+++ b/spec/mspec/spec/helpers/fixture_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#fixture" do
+ before :each do
+ @dir = File.realpath("..", __FILE__)
+ end
+
+ it "returns the expanded path to a fixture file" do
+ name = fixture(__FILE__, "subdir", "file.txt")
+ expect(name).to eq("#{@dir}/fixtures/subdir/file.txt")
+ end
+
+ it "omits '/shared' if it is the suffix of the directory string" do
+ name = fixture("#{@dir}/shared/file.rb", "subdir", "file.txt")
+ expect(name).to eq("#{@dir}/fixtures/subdir/file.txt")
+ end
+
+ it "does not append '/fixtures' if it is the suffix of the directory string" do
+ commands_dir = "#{File.dirname(@dir)}/commands"
+ name = fixture("#{commands_dir}/fixtures/file.rb", "subdir", "file.txt")
+ expect(name).to eq("#{commands_dir}/fixtures/subdir/file.txt")
+ end
+end
diff --git a/spec/mspec/spec/helpers/flunk_spec.rb b/spec/mspec/spec/helpers/flunk_spec.rb
new file mode 100644
index 0000000000..b6a1f21c12
--- /dev/null
+++ b/spec/mspec/spec/helpers/flunk_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/runner/mspec'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#flunk" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ it "raises an SpecExpectationNotMetError unconditionally" do
+ expect { flunk }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "accepts on argument for an optional message" do
+ expect {flunk "test"}.to raise_error(SpecExpectationNotMetError)
+ end
+end
diff --git a/spec/mspec/spec/helpers/fs_spec.rb b/spec/mspec/spec/helpers/fs_spec.rb
new file mode 100644
index 0000000000..15bb43903a
--- /dev/null
+++ b/spec/mspec/spec/helpers/fs_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#cp" do
+ before :each do
+ @source = tmp("source.txt")
+ @copy = tmp("copied.txt")
+
+ @contents = "This is a copy."
+ File.open(@source, "w") { |f| f.write @contents }
+ end
+
+ after :each do
+ File.delete @source if File.exist? @source
+ File.delete @copy if File.exist? @copy
+ end
+
+ it "copies a file" do
+ cp @source, @copy
+ data = IO.read(@copy)
+ expect(data).to eq(@contents)
+ expect(data).to eq(IO.read(@source))
+ end
+end
+
+RSpec.describe Object, "#touch" do
+ before :all do
+ @name = tmp("touched.txt")
+ end
+
+ after :each do
+ File.delete @name if File.exist? @name
+ end
+
+ it "creates a file" do
+ touch @name
+ expect(File.exist?(@name)).to be_truthy
+ end
+
+ it "accepts an optional mode argument" do
+ touch @name, "wb"
+ expect(File.exist?(@name)).to be_truthy
+ end
+
+ it "overwrites an existing file" do
+ File.open(@name, "w") { |f| f.puts "used" }
+ expect(File.size(@name)).to be > 0
+
+ touch @name
+ expect(File.size(@name)).to eq(0)
+ end
+
+ it "yields the open file if passed a block" do
+ touch(@name) { |f| f.write "touching" }
+ expect(IO.read(@name)).to eq("touching")
+ end
+end
+
+RSpec.describe Object, "#touch" do
+ before :all do
+ @name = tmp("subdir/touched.txt")
+ end
+
+ after :each do
+ rm_r File.dirname(@name)
+ end
+
+ it "creates all the directories in the path to the file" do
+ touch @name
+ expect(File.exist?(@name)).to be_truthy
+ end
+end
+
+RSpec.describe Object, "#mkdir_p" do
+ before :all do
+ @dir1 = tmp("/nested")
+ @dir2 = @dir1 + "/directory"
+ @paths = [ @dir2, @dir1 ]
+ end
+
+ after :each do
+ File.delete @dir1 if File.file? @dir1
+ @paths.each { |path| Dir.rmdir path if File.directory? path }
+ end
+
+ it "creates all the directories in a path" do
+ mkdir_p @dir2
+ expect(File.directory?(@dir2)).to be_truthy
+ end
+
+ it "raises an ArgumentError if a path component is a file" do
+ File.open(@dir1, "w") { |f| }
+ expect { mkdir_p @dir2 }.to raise_error(ArgumentError)
+ end
+
+ it "works if multiple processes try to create the same directory concurrently" do
+ original = File.method(:directory?)
+ expect(File).to receive(:directory?).at_least(:once) { |dir|
+ ret = original.call(dir)
+ if !ret and dir == @dir1
+ Dir.mkdir(dir) # Simulate race
+ end
+ ret
+ }
+ mkdir_p @dir1
+ expect(original.call(@dir1)).to be_truthy
+ end
+end
+
+RSpec.describe Object, "#rm_r" do
+ before :all do
+ @topdir = tmp("rm_r_tree")
+ @topfile = @topdir + "/file.txt"
+ @link = @topdir + "/file.lnk"
+ @socket = @topdir + "/socket.sck"
+ @subdir1 = @topdir + "/subdir1"
+ @subdir2 = @subdir1 + "/subdir2"
+ @subfile = @subdir1 + "/subfile.txt"
+ end
+
+ before :each do
+ mkdir_p @subdir2
+ touch @topfile
+ touch @subfile
+ end
+
+ after :each do
+ File.delete @link if File.exist? @link or File.symlink? @link
+ File.delete @socket if File.exist? @socket
+ File.delete @subfile if File.exist? @subfile
+ File.delete @topfile if File.exist? @topfile
+
+ Dir.rmdir @subdir2 if File.directory? @subdir2
+ Dir.rmdir @subdir1 if File.directory? @subdir1
+ Dir.rmdir @topdir if File.directory? @topdir
+ end
+
+ it "raises an ArgumentError if the path is not prefixed by MSPEC_RM_PREFIX" do
+ expect { rm_r "some_file.txt" }.to raise_error(ArgumentError)
+ end
+
+ it "removes a single file" do
+ rm_r @subfile
+ expect(File.exist?(@subfile)).to be_falsey
+ end
+
+ it "removes multiple files" do
+ rm_r @topfile, @subfile
+ expect(File.exist?(@topfile)).to be_falsey
+ expect(File.exist?(@subfile)).to be_falsey
+ end
+
+ platform_is_not :windows do
+ it "removes a symlink to a file" do
+ File.symlink @topfile, @link
+ rm_r @link
+ expect(File.exist?(@link)).to be_falsey
+ end
+
+ it "removes a symlink to a directory" do
+ File.symlink @subdir1, @link
+ rm_r @link
+ expect do
+ File.lstat(@link)
+ end.to raise_error(Errno::ENOENT)
+ expect(File.exist?(@subdir1)).to be_truthy
+ end
+
+ it "removes a dangling symlink" do
+ File.symlink "non_existent_file", @link
+ rm_r @link
+ expect do
+ File.lstat(@link)
+ end.to raise_error(Errno::ENOENT)
+ end
+
+ it "removes a socket" do
+ require 'socket'
+ UNIXServer.new(@socket).close
+ rm_r @socket
+ expect(File.exist?(@socket)).to be_falsey
+ end
+ end
+
+ it "removes a single directory" do
+ rm_r @subdir2
+ expect(File.directory?(@subdir2)).to be_falsey
+ end
+
+ it "recursively removes a directory tree" do
+ rm_r @topdir
+ expect(File.directory?(@topdir)).to be_falsey
+ end
+end
diff --git a/spec/mspec/spec/helpers/io_spec.rb b/spec/mspec/spec/helpers/io_spec.rb
new file mode 100644
index 0000000000..14c1a2d6b5
--- /dev/null
+++ b/spec/mspec/spec/helpers/io_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe IOStub do
+ before :each do
+ @out = IOStub.new
+ @sep = $\
+ end
+
+ after :each do
+ $\ = @sep
+ end
+
+ it "provides a write method" do
+ @out.write "this"
+ expect(@out).to eq("this")
+ end
+
+ it "concatenates the arguments sent to write" do
+ @out.write "flim ", "flam"
+ expect(@out).to eq("flim flam")
+ end
+
+ it "provides a print method that appends the default separator" do
+ $\ = " [newline] "
+ @out.print "hello"
+ @out.print "world"
+ expect(@out).to eq("hello [newline] world [newline] ")
+ end
+
+ it "provides a puts method that appends the default separator" do
+ @out.puts "hello", 1, 2, 3
+ expect(@out).to eq("hello\n1\n2\n3\n")
+ end
+
+ it "provides a puts method that appends separator if argument not given" do
+ @out.puts
+ expect(@out).to eq("\n")
+ end
+
+ it "provides a printf method" do
+ @out.printf "%-10s, %03d, %2.1f", "test", 42, 4.2
+ expect(@out).to eq("test , 042, 4.2")
+ end
+
+ it "provides a flush method that does nothing and returns self" do
+ expect(@out.flush).to eq(@out)
+ end
+end
+
+RSpec.describe Object, "#new_fd" do
+ before :each do
+ @name = tmp("io_specs")
+ @io = nil
+ end
+
+ after :each do
+ @io.close if @io and not @io.closed?
+ rm_r @name
+ end
+
+ it "returns an Integer that can be used to create an IO instance" do
+ fd = new_fd @name
+ expect(fd).to be_kind_of(Integer)
+
+ @io = IO.new fd, 'w:utf-8'
+ @io.sync = true
+ @io.print "io data"
+
+ expect(IO.read(@name)).to eq("io data")
+ end
+
+ it "accepts an options Hash" do
+ allow(FeatureGuard).to receive(:enabled?).and_return(true)
+ fd = new_fd @name, { :mode => 'w:utf-8' }
+ expect(fd).to be_kind_of(Integer)
+
+ @io = IO.new fd, 'w:utf-8'
+ @io.sync = true
+ @io.print "io data"
+
+ expect(IO.read(@name)).to eq("io data")
+ end
+
+ it "raises an ArgumentError if the options Hash does not include :mode" do
+ allow(FeatureGuard).to receive(:enabled?).and_return(true)
+ expect { new_fd @name, { :encoding => "utf-8" } }.to raise_error(ArgumentError)
+ end
+end
+
+RSpec.describe Object, "#new_io" do
+ before :each do
+ @name = tmp("io_specs.txt")
+ end
+
+ after :each do
+ @io.close if @io and !@io.closed?
+ rm_r @name
+ end
+
+ it "returns a File instance" do
+ @io = new_io @name
+ expect(@io).to be_an_instance_of(File)
+ end
+
+ it "opens the IO for reading if passed 'r'" do
+ touch(@name) { |f| f.print "io data" }
+ @io = new_io @name, "r"
+ expect(@io.read).to eq("io data")
+ expect { @io.puts "more data" }.to raise_error(IOError)
+ end
+
+ it "opens the IO for writing if passed 'w'" do
+ @io = new_io @name, "w"
+ @io.sync = true
+
+ @io.print "io data"
+ expect(IO.read(@name)).to eq("io data")
+ end
+
+ it "opens the IO for reading if passed { :mode => 'r' }" do
+ touch(@name) { |f| f.print "io data" }
+ @io = new_io @name, { :mode => "r" }
+ expect(@io.read).to eq("io data")
+ expect { @io.puts "more data" }.to raise_error(IOError)
+ end
+
+ it "opens the IO for writing if passed { :mode => 'w' }" do
+ @io = new_io @name, { :mode => "w" }
+ @io.sync = true
+
+ @io.print "io data"
+ expect(IO.read(@name)).to eq("io data")
+ end
+end
diff --git a/spec/mspec/spec/helpers/mock_to_path_spec.rb b/spec/mspec/spec/helpers/mock_to_path_spec.rb
new file mode 100644
index 0000000000..c2ce985190
--- /dev/null
+++ b/spec/mspec/spec/helpers/mock_to_path_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/mocks'
+
+RSpec.describe Object, "#mock_to_path" do
+ before :each do
+ state = double("run state").as_null_object
+ expect(MSpec).to receive(:current).and_return(state)
+ end
+
+ it "returns an object that responds to #to_path" do
+ obj = mock_to_path("foo")
+ expect(obj).to be_a(MockObject)
+ expect(obj).to respond_to(:to_path)
+ obj.to_path
+ end
+
+ it "returns the provided path when #to_path is called" do
+ obj = mock_to_path("/tmp/foo")
+ expect(obj.to_path).to eq("/tmp/foo")
+ end
+end
diff --git a/spec/mspec/spec/helpers/numeric_spec.rb b/spec/mspec/spec/helpers/numeric_spec.rb
new file mode 100644
index 0000000000..64495b7276
--- /dev/null
+++ b/spec/mspec/spec/helpers/numeric_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#bignum_value" do
+ it "returns a value that is an instance of Bignum on any platform" do
+ expect(bignum_value).to be > fixnum_max
+ end
+
+ it "returns the default value incremented by the argument" do
+ expect(bignum_value(42)).to eq(bignum_value + 42)
+ end
+end
+
+RSpec.describe Object, "-bignum_value" do
+ it "returns a value that is an instance of Bignum on any platform" do
+ expect(-bignum_value).to be < fixnum_min
+ end
+end
+
+RSpec.describe Object, "#nan_value" do
+ it "returns NaN" do
+ expect(nan_value.nan?).to be_truthy
+ end
+end
+
+RSpec.describe Object, "#infinity_value" do
+ it "returns Infinity" do
+ expect(infinity_value.infinite?).to eq(1)
+ end
+end
diff --git a/spec/mspec/spec/helpers/ruby_exe_spec.rb b/spec/mspec/spec/helpers/ruby_exe_spec.rb
new file mode 100644
index 0000000000..56bade1ba9
--- /dev/null
+++ b/spec/mspec/spec/helpers/ruby_exe_spec.rb
@@ -0,0 +1,256 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'rbconfig'
+
+class RubyExeSpecs
+ public :ruby_exe_options
+ public :resolve_ruby_exe
+ public :ruby_cmd
+ public :ruby_exe
+end
+
+RSpec.describe "#ruby_exe_options" do
+ before :each do
+ @ruby_exe_env = ENV['RUBY_EXE']
+ @script = RubyExeSpecs.new
+ end
+
+ after :each do
+ ENV['RUBY_EXE'] = @ruby_exe_env
+ end
+
+ it "returns ENV['RUBY_EXE'] when passed :env" do
+ ENV['RUBY_EXE'] = "kowabunga"
+ expect(@script.ruby_exe_options(:env)).to eq("kowabunga")
+ end
+
+ it "returns 'bin/jruby' when passed :engine and RUBY_ENGINE is 'jruby'" do
+ stub_const "RUBY_ENGINE", 'jruby'
+ expect(@script.ruby_exe_options(:engine)).to eq('bin/jruby')
+ end
+
+ it "returns 'bin/rbx' when passed :engine, RUBY_ENGINE is 'rbx'" do
+ stub_const "RUBY_ENGINE", 'rbx'
+ expect(@script.ruby_exe_options(:engine)).to eq('bin/rbx')
+ end
+
+ it "returns 'ir' when passed :engine and RUBY_ENGINE is 'ironruby'" do
+ stub_const "RUBY_ENGINE", 'ironruby'
+ expect(@script.ruby_exe_options(:engine)).to eq('ir')
+ end
+
+ it "returns 'maglev-ruby' when passed :engine and RUBY_ENGINE is 'maglev'" do
+ stub_const "RUBY_ENGINE", 'maglev'
+ expect(@script.ruby_exe_options(:engine)).to eq('maglev-ruby')
+ end
+
+ it "returns 'topaz' when passed :engine and RUBY_ENGINE is 'topaz'" do
+ stub_const "RUBY_ENGINE", 'topaz'
+ expect(@script.ruby_exe_options(:engine)).to eq('topaz')
+ end
+
+ it "returns RUBY_ENGINE + $(EXEEXT) when passed :name" do
+ bin = RUBY_ENGINE + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ name = File.join ".", bin
+ expect(@script.ruby_exe_options(:name)).to eq(name)
+ end
+
+ it "returns $(bindir)/$(RUBY_INSTALL_NAME) + $(EXEEXT) when passed :install_name" do
+ bin = RbConfig::CONFIG['RUBY_INSTALL_NAME'] + (RbConfig::CONFIG['EXEEXT'] || RbConfig::CONFIG['exeext'] || '')
+ name = File.join RbConfig::CONFIG['bindir'], bin
+ expect(@script.ruby_exe_options(:install_name)).to eq(name)
+ end
+end
+
+RSpec.describe "#resolve_ruby_exe" do
+ before :each do
+ @name = "ruby_spec_exe"
+ @script = RubyExeSpecs.new
+ end
+
+ it "returns the value returned by #ruby_exe_options if it exists and is executable" do
+ expect(@script).to receive(:ruby_exe_options).and_return(@name)
+ expect(File).to receive(:file?).with(@name).and_return(true)
+ expect(File).to receive(:executable?).with(@name).and_return(true)
+ expect(File).to receive(:expand_path).with(@name).and_return(@name)
+ expect(@script.resolve_ruby_exe).to eq(@name)
+ end
+
+ it "expands the path portion of the result of #ruby_exe_options" do
+ expect(@script).to receive(:ruby_exe_options).and_return("#{@name}")
+ expect(File).to receive(:file?).with(@name).and_return(true)
+ expect(File).to receive(:executable?).with(@name).and_return(true)
+ expect(File).to receive(:expand_path).with(@name).and_return("/usr/bin/#{@name}")
+ expect(@script.resolve_ruby_exe).to eq("/usr/bin/#{@name}")
+ end
+
+ it "adds the flags after the executable" do
+ @name = 'bin/rbx'
+ expect(@script).to receive(:ruby_exe_options).and_return(@name)
+ expect(File).to receive(:file?).with(@name).and_return(true)
+ expect(File).to receive(:executable?).with(@name).and_return(true)
+ expect(File).to receive(:expand_path).with(@name).and_return(@name)
+
+ expect(ENV).to receive(:[]).with("RUBY_FLAGS").and_return('-X19')
+ expect(@script.resolve_ruby_exe).to eq('bin/rbx -X19')
+ end
+
+ it "raises an exception if no exe is found" do
+ expect(File).to receive(:file?).at_least(:once).and_return(false)
+ expect {
+ @script.resolve_ruby_exe
+ }.to raise_error(Exception)
+ end
+end
+
+RSpec.describe Object, "#ruby_cmd" do
+ before :each do
+ stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q'
+
+ @file = "some/ruby/file.rb"
+ @code = %(some "real" 'ruby' code)
+
+ @script = RubyExeSpecs.new
+ end
+
+ it "returns a command that runs the given file if it is a file that exists" do
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(@script.ruby_cmd(@file)).to eq("ruby_spec_exe -w -Q some/ruby/file.rb")
+ end
+
+ it "includes the given options and arguments with a file" do
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(@script.ruby_cmd(@file, :options => "-w -Cdir", :args => "< file.txt")).to eq(
+ "ruby_spec_exe -w -Q -w -Cdir some/ruby/file.rb < file.txt"
+ )
+ end
+
+ it "includes the given options and arguments with -e" do
+ expect(File).to receive(:exist?).with(@code).and_return(false)
+ expect(@script.ruby_cmd(@code, :options => "-W0 -Cdir", :args => "< file.txt")).to eq(
+ %(ruby_spec_exe -w -Q -W0 -Cdir -e "some \\"real\\" 'ruby' code" < file.txt)
+ )
+ end
+
+ it "returns a command with options and arguments but without code or file" do
+ expect(@script.ruby_cmd(nil, :options => "-c", :args => "> file.txt")).to eq(
+ "ruby_spec_exe -w -Q -c > file.txt"
+ )
+ end
+end
+
+RSpec.describe Object, "#ruby_exe" do
+ before :each do
+ stub_const 'RUBY_EXE', 'ruby_spec_exe -w -Q'
+
+ @script = RubyExeSpecs.new
+ allow(IO).to receive(:popen).and_return('OUTPUT')
+
+ status_successful = double(Process::Status, exited?: true, exitstatus: 0)
+ allow(Process).to receive(:last_status).and_return(status_successful)
+ end
+
+ it "returns command STDOUT when given command" do
+ code = "code"
+ options = {}
+ output = "output"
+ expect(IO).to receive(:popen).and_return(output)
+
+ expect(@script.ruby_exe(code, options)).to eq output
+ end
+
+ it "returns an Array containing the interpreter executable and flags when given no arguments" do
+ expect(@script.ruby_exe).to eq(['ruby_spec_exe', '-w', '-Q'])
+ end
+
+ it "executes (using `) the result of calling #ruby_cmd with the given arguments" do
+ code = "code"
+ options = {}
+ expect(@script).to receive(:ruby_cmd).and_return("ruby_cmd")
+ expect(IO).to receive(:popen).with("ruby_cmd")
+ @script.ruby_exe(code, options)
+ end
+
+ it "raises exception when command exit status is not successful" do
+ code = "code"
+ options = {}
+
+ status_failed = double(Process::Status, exited?: true, exitstatus: 4)
+ allow(Process).to receive(:last_status).and_return(status_failed)
+
+ expect {
+ @script.ruby_exe(code, options)
+ }.to raise_error(%r{Expected exit status is 0 but actual is 4 for command ruby_exe\(.+\)})
+ end
+
+ it "shows in the exception message if a signal killed the process" do
+ code = "code"
+ options = {}
+
+ status_failed = double(Process::Status, exited?: false, signaled?: true, termsig: Signal.list.fetch('TERM'))
+ allow(Process).to receive(:last_status).and_return(status_failed)
+
+ expect {
+ @script.ruby_exe(code, options)
+ }.to raise_error(%r{Expected exit status is 0 but actual is :SIGTERM for command ruby_exe\(.+\)})
+ end
+
+ describe "with :dir option" do
+ it "is deprecated" do
+ expect {
+ @script.ruby_exe nil, :dir => "tmp"
+ }.to raise_error(/no longer supported, use Dir\.chdir/)
+ end
+ end
+
+ describe "with :env option" do
+ it "preserves the values of existing ENV keys" do
+ ENV["ABC"] = "123"
+ allow(ENV).to receive(:[])
+ expect(ENV).to receive(:[]).with("ABC")
+ @script.ruby_exe nil, :env => { :ABC => "xyz" }
+ end
+
+ it "adds the :env entries to ENV" do
+ expect(ENV).to receive(:[]=).with("ABC", "xyz")
+ @script.ruby_exe nil, :env => { :ABC => "xyz" }
+ end
+
+ it "deletes the :env entries in ENV when an exception is raised" do
+ expect(ENV).to receive(:delete).with("XYZ")
+ @script.ruby_exe nil, :env => { :XYZ => "xyz" }
+ end
+
+ it "resets the values of existing ENV keys when an exception is raised" do
+ ENV["ABC"] = "123"
+ expect(ENV).to receive(:[]=).with("ABC", "xyz")
+ expect(ENV).to receive(:[]=).with("ABC", "123")
+
+ expect(IO).to receive(:popen).and_raise(Exception)
+ expect do
+ @script.ruby_exe nil, :env => { :ABC => "xyz" }
+ end.to raise_error(Exception)
+ end
+ end
+
+ describe "with :exit_status option" do
+ before do
+ status_failed = double(Process::Status, exited?: true, exitstatus: 4)
+ allow(Process).to receive(:last_status).and_return(status_failed)
+ end
+
+ it "raises exception when command ends with not expected status" do
+ expect {
+ @script.ruby_exe("path", exit_status: 1)
+ }.to raise_error(%r{Expected exit status is 1 but actual is 4 for command ruby_exe\(.+\)})
+ end
+
+ it "does not raise exception when command ends with expected status" do
+ output = "output"
+ expect(IO).to receive(:popen).and_return(output)
+
+ expect(@script.ruby_exe("path", exit_status: 4)).to eq output
+ end
+ end
+end
diff --git a/spec/mspec/spec/helpers/scratch_spec.rb b/spec/mspec/spec/helpers/scratch_spec.rb
new file mode 100644
index 0000000000..9dbef94aa3
--- /dev/null
+++ b/spec/mspec/spec/helpers/scratch_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe ScratchPad do
+ it "records an object and returns a previously recorded object" do
+ ScratchPad.record :this
+ expect(ScratchPad.recorded).to eq(:this)
+ end
+
+ it "clears the recorded object" do
+ ScratchPad.record :that
+ expect(ScratchPad.recorded).to eq(:that)
+ ScratchPad.clear
+ expect(ScratchPad.recorded).to eq(nil)
+ end
+
+ it "provides a convenience shortcut to append to a previously recorded object" do
+ ScratchPad.record []
+ ScratchPad << :new
+ ScratchPad << :another
+ expect(ScratchPad.recorded).to eq([:new, :another])
+ end
+end
diff --git a/spec/mspec/spec/helpers/suppress_warning_spec.rb b/spec/mspec/spec/helpers/suppress_warning_spec.rb
new file mode 100644
index 0000000000..4cae189bd3
--- /dev/null
+++ b/spec/mspec/spec/helpers/suppress_warning_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#suppress_warning" do
+ it "hides warnings" do
+ suppress_warning do
+ warn "should not be shown"
+ end
+ end
+
+ it "yields the block" do
+ a = 0
+ suppress_warning do
+ a = 1
+ end
+ expect(a).to eq(1)
+ end
+end
diff --git a/spec/mspec/spec/helpers/tmp_spec.rb b/spec/mspec/spec/helpers/tmp_spec.rb
new file mode 100644
index 0000000000..c41dcd57b3
--- /dev/null
+++ b/spec/mspec/spec/helpers/tmp_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'mspec/guards'
+require 'mspec/helpers'
+
+RSpec.describe Object, "#tmp" do
+ before :all do
+ @dir = SPEC_TEMP_DIR
+ end
+
+ it "returns a name relative to the current working directory" do
+ expect(tmp("test.txt")).to eq("#{@dir}/#{SPEC_TEMP_UNIQUIFIER}-test.txt")
+ end
+
+ it "returns a 'unique' name on repeated calls" do
+ a = tmp("text.txt")
+ b = tmp("text.txt")
+ expect(a).not_to eq(b)
+ end
+
+ it "does not 'uniquify' the name if requested not to" do
+ expect(tmp("test.txt", false)).to eq("#{@dir}/test.txt")
+ end
+
+ it "returns the name of the temporary directory when passed an empty string" do
+ expect(tmp("")).to eq("#{@dir}/")
+ end
+end
diff --git a/spec/mspec/spec/integration/interpreter_spec.rb b/spec/mspec/spec/integration/interpreter_spec.rb
new file mode 100644
index 0000000000..dbf3987a08
--- /dev/null
+++ b/spec/mspec/spec/integration/interpreter_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+RSpec.describe "The interpreter passed with -t" do
+ it "is used in subprocess" do
+ fixtures = "spec/fixtures"
+ interpreter = "#{fixtures}/my_ruby"
+ out, ret = run_mspec("run", "#{fixtures}/print_interpreter_spec.rb -t #{interpreter}")
+ out = out.lines.map(&:chomp).reject { |line|
+ line == 'RUBY_DESCRIPTION'
+ }.take(3)
+ expect(out).to eq([
+ interpreter,
+ interpreter,
+ "CWD/#{interpreter}"
+ ])
+ expect(ret.success?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/integration/object_methods_spec.rb b/spec/mspec/spec/integration/object_methods_spec.rb
new file mode 100644
index 0000000000..697fbd10fa
--- /dev/null
+++ b/spec/mspec/spec/integration/object_methods_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+expected_output = <<EOS
+RUBY_DESCRIPTION
+.
+
+Finished in D.DDDDDD seconds
+
+1 file, 1 example, 1 expectation, 0 failures, 0 errors, 0 tagged
+EOS
+
+RSpec.describe "MSpec" do
+ it "does not define public methods on Object" do
+ out, ret = run_mspec("run", "spec/fixtures/object_methods_spec.rb")
+ expect(out).to eq(expected_output)
+ expect(ret.success?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/integration/run_spec.rb b/spec/mspec/spec/integration/run_spec.rb
new file mode 100644
index 0000000000..ea0735e9b2
--- /dev/null
+++ b/spec/mspec/spec/integration/run_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+RSpec.describe "Running mspec" do
+ q = BACKTRACE_QUOTE
+ a_spec_output = <<EOS
+
+1)
+Foo#bar errors FAILED
+Expected 1 == 2
+to be truthy but was false
+CWD/spec/fixtures/a_spec.rb:8:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>'
+
+2)
+Foo#bar fails ERROR
+RuntimeError: failure
+CWD/spec/fixtures/a_spec.rb:12:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/a_spec.rb:2:in #{q}<top (required)>'
+
+Finished in D.DDDDDD seconds
+EOS
+
+ a_stats = "1 file, 3 examples, 2 expectations, 1 failure, 1 error, 0 tagged\n"
+ ab_stats = "2 files, 4 examples, 3 expectations, 1 failure, 1 error, 0 tagged\n"
+ fixtures = "spec/fixtures"
+
+ it "runs the specs" do
+ out, ret = run_mspec("run", "#{fixtures}/a_spec.rb")
+ expect(out).to eq("RUBY_DESCRIPTION\n.FE\n#{a_spec_output}\n#{a_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "directly with mspec-run runs the specs" do
+ out, ret = run_mspec("-run", "#{fixtures}/a_spec.rb")
+ expect(out).to eq("RUBY_DESCRIPTION\n.FE\n#{a_spec_output}\n#{a_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "runs the specs in parallel with -j using the dotted formatter" do
+ out, ret = run_mspec("run", "-j #{fixtures}/a_spec.rb #{fixtures}/b_spec.rb")
+ expect(out).to eq("RUBY_DESCRIPTION\n...\n#{a_spec_output}\n#{ab_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "runs the specs in parallel with -j -fa" do
+ out, ret = run_mspec("run", "-j -fa #{fixtures}/a_spec.rb #{fixtures}/b_spec.rb")
+ progress_bar =
+ "\r[/ | 0% | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " +
+ "\r[- | ==================50% | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m " +
+ "\r[\\ | ==================100%================== | 00:00:00] \e[0;32m 0F \e[0;32m 0E\e[0m "
+ expect(out).to eq("RUBY_DESCRIPTION\n#{progress_bar}\n#{a_spec_output}\n#{ab_stats}")
+ expect(ret.success?).to eq(false)
+ end
+
+ it "gives a useful error message when a subprocess dies in parallel mode" do
+ out, ret = run_mspec("run", "-j #{fixtures}/b_spec.rb #{fixtures}/die_spec.rb")
+ lines = out.lines
+ expect(lines).to include "A child mspec-run process died unexpectedly while running CWD/spec/fixtures/die_spec.rb\n"
+ expect(lines).to include "Finished in D.DDDDDD seconds\n"
+ expect(lines.last).to match(/^\d files?, \d examples?, \d expectations?, 0 failures, 0 errors, 0 tagged$/)
+ expect(ret.success?).to eq(false)
+ end
+
+ it "gives a useful error message when a subprocess prints unexpected output on STDOUT in parallel mode" do
+ out, ret = run_mspec("run", "-j #{fixtures}/b_spec.rb #{fixtures}/chatty_spec.rb")
+ lines = out.lines
+ expect(lines).to include "A child mspec-run process printed unexpected output on STDOUT: #{'"Hello\nIt\'s me!\n"'} while running CWD/spec/fixtures/chatty_spec.rb\n"
+ expect(lines).to include "Finished in D.DDDDDD seconds\n"
+ expect(lines.last).to eq("2 files, 2 examples, 2 expectations, 0 failures, 0 errors, 0 tagged\n")
+ expect(ret.success?).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/integration/tag_spec.rb b/spec/mspec/spec/integration/tag_spec.rb
new file mode 100644
index 0000000000..ae08e9d45f
--- /dev/null
+++ b/spec/mspec/spec/integration/tag_spec.rb
@@ -0,0 +1,60 @@
+# encoding: utf-8
+require 'spec_helper'
+
+RSpec.describe "Running mspec tag" do
+ before :all do
+ FileUtils.rm_rf 'spec/fixtures/tags'
+ end
+
+ after :all do
+ FileUtils.rm_rf 'spec/fixtures/tags'
+ end
+
+ it "tags the failing specs" do
+ fixtures = "spec/fixtures"
+ out, ret = run_mspec("tag", "--add fails --fail #{fixtures}/tagging_spec.rb")
+ q = BACKTRACE_QUOTE
+ expect(out).to eq <<EOS
+RUBY_DESCRIPTION
+.FF
+TagAction: specs tagged with 'fails':
+
+Tag#me errors
+Tag#me érròrs in unicode
+
+
+1)
+Tag#me errors FAILED
+Expected 1 == 2
+to be truthy but was false
+CWD/spec/fixtures/tagging_spec.rb:9:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>'
+
+2)
+Tag#me érròrs in unicode FAILED
+Expected 1 == 2
+to be truthy but was false
+CWD/spec/fixtures/tagging_spec.rb:13:in #{q}block (2 levels) in <top (required)>'
+CWD/spec/fixtures/tagging_spec.rb:3:in #{q}<top (required)>'
+
+Finished in D.DDDDDD seconds
+
+1 file, 3 examples, 3 expectations, 2 failures, 0 errors, 0 tagged
+EOS
+ expect(ret.success?).to eq(false)
+ end
+
+ it "does not run already tagged specs" do
+ fixtures = "spec/fixtures"
+ out, ret = run_mspec("run", "--excl-tag fails #{fixtures}/tagging_spec.rb")
+ expect(out).to eq <<EOS
+RUBY_DESCRIPTION
+.
+
+Finished in D.DDDDDD seconds
+
+1 file, 3 examples, 1 expectation, 0 failures, 0 errors, 2 tagged
+EOS
+ expect(ret.success?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/matchers/base_spec.rb b/spec/mspec/spec/matchers/base_spec.rb
new file mode 100644
index 0000000000..6b5a3fbd72
--- /dev/null
+++ b/spec/mspec/spec/matchers/base_spec.rb
@@ -0,0 +1,228 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+require 'time'
+
+RSpec.describe SpecPositiveOperatorMatcher, "== operator" do
+ it "provides a failure message that 'Expected x to equal y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(1) == 2
+ }.to raise_error(SpecExpectationNotMetError, "Expected 1 == 2\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when == returns true" do
+ SpecPositiveOperatorMatcher.new(1) == 1
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "=~ operator" do
+ it "provides a failure message that 'Expected \"x\" to match y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new('real') =~ /fake/
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"real\" =~ /fake/\nto be truthy but was nil")
+ end
+
+ it "does not raise an exception when =~ returns true" do
+ SpecPositiveOperatorMatcher.new('real') =~ /real/
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "> operator" do
+ it "provides a failure message that 'Expected x to be greater than y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(4) > 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 > 5\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when > returns true" do
+ SpecPositiveOperatorMatcher.new(5) > 4
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, ">= operator" do
+ it "provides a failure message that 'Expected x to be greater than or equal to y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(4) >= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 >= 5\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when > returns true" do
+ SpecPositiveOperatorMatcher.new(5) >= 4
+ SpecPositiveOperatorMatcher.new(5) >= 5
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "< operator" do
+ it "provides a failure message that 'Expected x to be less than y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(5) < 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 < 4\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when < returns true" do
+ SpecPositiveOperatorMatcher.new(4) < 5
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "<= operator" do
+ it "provides a failure message that 'Expected x to be less than or equal to y'" do
+ expect {
+ SpecPositiveOperatorMatcher.new(5) <= 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 <= 4\nto be truthy but was false")
+ end
+
+ it "does not raise an exception when < returns true" do
+ SpecPositiveOperatorMatcher.new(4) <= 5
+ SpecPositiveOperatorMatcher.new(4) <= 4
+ end
+end
+
+RSpec.describe SpecPositiveOperatorMatcher, "arbitrary predicates" do
+ it "do not raise an exception when the predicate is truthy" do
+ SpecPositiveOperatorMatcher.new(2).eql?(2)
+ SpecPositiveOperatorMatcher.new(2).equal?(2)
+ SpecPositiveOperatorMatcher.new([1, 2, 3]).include?(2)
+ SpecPositiveOperatorMatcher.new("abc").start_with?("ab")
+ SpecPositiveOperatorMatcher.new("abc").start_with?("d", "a")
+ SpecPositiveOperatorMatcher.new(3).odd?
+ SpecPositiveOperatorMatcher.new([1, 2]).any? { |e| e.even? }
+ end
+
+ it "provide a failure message when the predicate returns a falsy value" do
+ expect {
+ SpecPositiveOperatorMatcher.new(2).eql?(3)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.eql? 3\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new(2).equal?(3)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.equal? 3\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new([1, 2, 3]).include?(4)
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2, 3].include? 4\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new("abc").start_with?("de")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"de\"\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new("abc").start_with?("d", "e")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"d\", \"e\"\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new(2).odd?
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.odd?\nto be truthy but was false")
+ expect {
+ SpecPositiveOperatorMatcher.new([1, 3]).any? { |e| e.even? }
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 3].any? { ... }\nto be truthy but was false")
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "arbitrary predicates" do
+ it "do not raise an exception when the predicate returns a falsy value" do
+ SpecNegativeOperatorMatcher.new(2).eql?(3)
+ SpecNegativeOperatorMatcher.new(2).equal?(3)
+ SpecNegativeOperatorMatcher.new([1, 2, 3]).include?(4)
+ SpecNegativeOperatorMatcher.new("abc").start_with?("de")
+ SpecNegativeOperatorMatcher.new("abc").start_with?("d", "e")
+ SpecNegativeOperatorMatcher.new(2).odd?
+ SpecNegativeOperatorMatcher.new([1, 3]).any? { |e| e.even? }
+ end
+
+ it "provide a failure message when the predicate returns a truthy value" do
+ expect {
+ SpecNegativeOperatorMatcher.new(2).eql?(2)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.eql? 2\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(2).equal?(2)
+ }.to raise_error(SpecExpectationNotMetError, "Expected 2.equal? 2\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new([1, 2, 3]).include?(2)
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2, 3].include? 2\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new("abc").start_with?("ab")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"ab\"\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new("abc").start_with?("d", "a")
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"abc\".start_with? \"d\", \"a\"\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(3).odd?
+ }.to raise_error(SpecExpectationNotMetError, "Expected 3.odd?\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new([1, 2]).any? { |e| e.even? }
+ }.to raise_error(SpecExpectationNotMetError, "Expected [1, 2].any? { ... }\nto be falsy but was true")
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "== operator" do
+ it "provides a failure message that 'Expected x not to equal y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(1) == 1
+ }.to raise_error(SpecExpectationNotMetError, "Expected 1 == 1\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when == returns false" do
+ SpecNegativeOperatorMatcher.new(1) == 2
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "=~ operator" do
+ it "provides a failure message that 'Expected \"x\" not to match /y/'" do
+ expect {
+ SpecNegativeOperatorMatcher.new('real') =~ /real/
+ }.to raise_error(SpecExpectationNotMetError, "Expected \"real\" =~ /real/\nto be falsy but was 0")
+ end
+
+ it "does not raise an exception when =~ returns false" do
+ SpecNegativeOperatorMatcher.new('real') =~ /fake/
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "< operator" do
+ it "provides a failure message that 'Expected x not to be less than y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(4) < 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 < 5\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when < returns false" do
+ SpecNegativeOperatorMatcher.new(5) < 4
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "<= operator" do
+ it "provides a failure message that 'Expected x not to be less than or equal to y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(4) <= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 4 <= 5\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(5) <= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 <= 5\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when <= returns false" do
+ SpecNegativeOperatorMatcher.new(5) <= 4
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, "> operator" do
+ it "provides a failure message that 'Expected x not to be greater than y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(5) > 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 > 4\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when > returns false" do
+ SpecNegativeOperatorMatcher.new(4) > 5
+ end
+end
+
+RSpec.describe SpecNegativeOperatorMatcher, ">= operator" do
+ it "provides a failure message that 'Expected x not to be greater than or equal to y'" do
+ expect {
+ SpecNegativeOperatorMatcher.new(5) >= 4
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 >= 4\nto be falsy but was true")
+ expect {
+ SpecNegativeOperatorMatcher.new(5) >= 5
+ }.to raise_error(SpecExpectationNotMetError, "Expected 5 >= 5\nto be falsy but was true")
+ end
+
+ it "does not raise an exception when >= returns false" do
+ SpecNegativeOperatorMatcher.new(4) >= 5
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_an_instance_of_spec.rb b/spec/mspec/spec/matchers/be_an_instance_of_spec.rb
new file mode 100644
index 0000000000..7c74249d24
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_an_instance_of_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+module BeAnInOfSpecs
+ class A
+ end
+
+ class B < A
+ end
+
+ class C < B
+ end
+end
+
+RSpec.describe BeAnInstanceOfMatcher do
+ it "matches when actual is an instance_of? expected" do
+ a = BeAnInOfSpecs::A.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(a)).to be_truthy
+
+ b = BeAnInOfSpecs::B.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(b)).to be_truthy
+ end
+
+ it "does not match when actual is not an instance_of? expected" do
+ a = BeAnInOfSpecs::A.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(a)).to be_falsey
+
+ b = BeAnInOfSpecs::B.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(b)).to be_falsey
+
+ c = BeAnInOfSpecs::C.new
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::A).matches?(c)).to be_falsey
+ expect(BeAnInstanceOfMatcher.new(BeAnInOfSpecs::B).matches?(c)).to be_falsey
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeAnInstanceOfMatcher.new(Numeric)
+ matcher.matches?("string")
+ expect(matcher.failure_message).to eq([
+ "Expected \"string\" (String)", "to be an instance of Numeric"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeAnInstanceOfMatcher.new(Numeric)
+ matcher.matches?(4.0)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected 4.0 (Float)", "not to be an instance of Numeric"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_ancestor_of_spec.rb b/spec/mspec/spec/matchers/be_ancestor_of_spec.rb
new file mode 100644
index 0000000000..abc05e0f7a
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_ancestor_of_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class Parent; end
+class Child < Parent; end
+
+RSpec.describe BeAncestorOfMatcher do
+ it "matches when actual is an ancestor of expected" do
+ expect(BeAncestorOfMatcher.new(Child).matches?(Parent)).to eq(true)
+ end
+
+ it "does not match when actual is not an ancestor of expected" do
+ expect(BeAncestorOfMatcher.new(Parent).matches?(Child)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeAncestorOfMatcher.new(Parent)
+ matcher.matches?(Child)
+ expect(matcher.failure_message).to eq(["Expected Child", "to be an ancestor of Parent"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeAncestorOfMatcher.new(Child)
+ matcher.matches?(Parent)
+ expect(matcher.negative_failure_message).to eq(["Expected Parent", "not to be an ancestor of Child"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_close_spec.rb b/spec/mspec/spec/matchers/be_close_spec.rb
new file mode 100644
index 0000000000..dfd4f4ddbb
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_close_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+# Adapted from RSpec 1.0.8
+RSpec.describe BeCloseMatcher do
+ it "matches when actual == expected" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.0)).to eq(true)
+ end
+
+ it "matches when actual < (expected + tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.49)).to eq(true)
+ end
+
+ it "matches when actual > (expected - tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.51)).to eq(true)
+ end
+
+ it "matches when actual == (expected + tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.5)).to eq(true)
+ expect(BeCloseMatcher.new(3, 2).matches?(5)).to eq(true)
+ end
+
+ it "matches when actual == (expected - tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.5)).to eq(true)
+ expect(BeCloseMatcher.new(3, 2).matches?(1)).to eq(true)
+ end
+
+ it "does not match when actual < (expected - tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(4.49)).to eq(false)
+ end
+
+ it "does not match when actual > (expected + tolerance)" do
+ expect(BeCloseMatcher.new(5.0, 0.5).matches?(5.51)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeCloseMatcher.new(5.0, 0.5)
+ matcher.matches?(6.5)
+ expect(matcher.failure_message).to eq(["Expected 6.5", "to be within 5.0 +/- 0.5"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeCloseMatcher.new(5.0, 0.5)
+ matcher.matches?(4.9)
+ expect(matcher.negative_failure_message).to eq(["Expected 4.9", "not to be within 5.0 +/- 0.5"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_computed_by_spec.rb b/spec/mspec/spec/matchers/be_computed_by_spec.rb
new file mode 100644
index 0000000000..f73861a576
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_computed_by_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require 'mspec/matchers'
+
+RSpec.describe BeComputedByMatcher do
+ it "matches when all entries in the Array compute" do
+ array = [ [65, "A"],
+ [90, "Z"] ]
+ expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_truthy
+ end
+
+ it "matches when all entries in the Array with arguments compute" do
+ array = [ [1, 2, 3],
+ [2, 4, 6] ]
+ expect(BeComputedByMatcher.new(:+).matches?(array)).to be_truthy
+ end
+
+ it "does not match when any entry in the Array does not compute" do
+ array = [ [65, "A" ],
+ [91, "Z" ] ]
+ expect(BeComputedByMatcher.new(:chr).matches?(array)).to be_falsey
+ end
+
+ it "accepts an argument list to apply to each method call" do
+ array = [ [65, "1000001" ],
+ [90, "1011010" ] ]
+ expect(BeComputedByMatcher.new(:to_s, 2).matches?(array)).to be_truthy
+ end
+
+ it "does not match when any entry in the Array with arguments does not compute" do
+ array = [ [1, 2, 3],
+ [2, 4, 7] ]
+ expect(BeComputedByMatcher.new(:+).matches?(array)).to be_falsey
+ end
+
+ it "provides a useful failure message" do
+ array = [ [65, "A" ],
+ [91, "Z" ] ]
+ matcher = BeComputedByMatcher.new(:chr)
+ matcher.matches?(array)
+ expect(matcher.failure_message).to eq(["Expected \"Z\"", "to be computed by 91.chr (computed \"[\" instead)"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_empty_spec.rb b/spec/mspec/spec/matchers/be_empty_spec.rb
new file mode 100644
index 0000000000..30678fe85f
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_empty_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeEmptyMatcher do
+ it "matches when actual is empty" do
+ expect(BeEmptyMatcher.new.matches?("")).to eq(true)
+ end
+
+ it "does not match when actual is not empty" do
+ expect(BeEmptyMatcher.new.matches?([10])).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeEmptyMatcher.new
+ matcher.matches?("not empty string")
+ expect(matcher.failure_message).to eq(["Expected \"not empty string\"", "to be empty"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeEmptyMatcher.new
+ matcher.matches?("")
+ expect(matcher.negative_failure_message).to eq(["Expected \"\"", "not to be empty"])
+ end
+end
+
diff --git a/spec/mspec/spec/matchers/be_false_spec.rb b/spec/mspec/spec/matchers/be_false_spec.rb
new file mode 100644
index 0000000000..46d7253220
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_false_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeFalseMatcher do
+ it "matches when actual is false" do
+ expect(BeFalseMatcher.new.matches?(false)).to eq(true)
+ end
+
+ it "does not match when actual is not false" do
+ expect(BeFalseMatcher.new.matches?("")).to eq(false)
+ expect(BeFalseMatcher.new.matches?(true)).to eq(false)
+ expect(BeFalseMatcher.new.matches?(nil)).to eq(false)
+ expect(BeFalseMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeFalseMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be false"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeFalseMatcher.new
+ matcher.matches?(false)
+ expect(matcher.negative_failure_message).to eq(["Expected false", "not to be false"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_kind_of_spec.rb b/spec/mspec/spec/matchers/be_kind_of_spec.rb
new file mode 100644
index 0000000000..1e19058411
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_kind_of_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeKindOfMatcher do
+ it "matches when actual is a kind_of? expected" do
+ expect(BeKindOfMatcher.new(Numeric).matches?(1)).to eq(true)
+ expect(BeKindOfMatcher.new(Integer).matches?(2)).to eq(true)
+ expect(BeKindOfMatcher.new(Regexp).matches?(/m/)).to eq(true)
+ end
+
+ it "does not match when actual is not a kind_of? expected" do
+ expect(BeKindOfMatcher.new(Integer).matches?(1.5)).to eq(false)
+ expect(BeKindOfMatcher.new(String).matches?(:a)).to eq(false)
+ expect(BeKindOfMatcher.new(Hash).matches?([])).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeKindOfMatcher.new(Numeric)
+ matcher.matches?('string')
+ expect(matcher.failure_message).to eq([
+ "Expected \"string\" (String)", "to be kind of Numeric"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeKindOfMatcher.new(Numeric)
+ matcher.matches?(4.0)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected 4.0 (Float)", "not to be kind of Numeric"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_nan_spec.rb b/spec/mspec/spec/matchers/be_nan_spec.rb
new file mode 100644
index 0000000000..baa7447943
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_nan_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/matchers'
+
+RSpec.describe BeNaNMatcher do
+ it "matches when actual is NaN" do
+ expect(BeNaNMatcher.new.matches?(nan_value)).to eq(true)
+ end
+
+ it "does not match when actual is not NaN" do
+ expect(BeNaNMatcher.new.matches?(1.0)).to eq(false)
+ expect(BeNaNMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeNaNMatcher.new
+ matcher.matches?(0)
+ expect(matcher.failure_message).to eq(["Expected 0", "to be NaN"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeNaNMatcher.new
+ matcher.matches?(nan_value)
+ expect(matcher.negative_failure_message).to eq(["Expected NaN", "not to be NaN"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_nil_spec.rb b/spec/mspec/spec/matchers/be_nil_spec.rb
new file mode 100644
index 0000000000..e2768acf83
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_nil_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeNilMatcher do
+ it "matches when actual is nil" do
+ expect(BeNilMatcher.new.matches?(nil)).to eq(true)
+ end
+
+ it "does not match when actual is not nil" do
+ expect(BeNilMatcher.new.matches?("")).to eq(false)
+ expect(BeNilMatcher.new.matches?(false)).to eq(false)
+ expect(BeNilMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeNilMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be nil"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeNilMatcher.new
+ matcher.matches?(nil)
+ expect(matcher.negative_failure_message).to eq(["Expected nil", "not to be nil"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_true_or_false_spec.rb b/spec/mspec/spec/matchers/be_true_or_false_spec.rb
new file mode 100644
index 0000000000..e4b456eafc
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_true_or_false_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeTrueOrFalseMatcher do
+ it "matches when actual is true" do
+ expect(BeTrueOrFalseMatcher.new.matches?(true)).to eq(true)
+ end
+
+ it "matches when actual is false" do
+ expect(BeTrueOrFalseMatcher.new.matches?(false)).to eq(true)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeTrueOrFalseMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true or false"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/be_true_spec.rb b/spec/mspec/spec/matchers/be_true_spec.rb
new file mode 100644
index 0000000000..39ef05a0f8
--- /dev/null
+++ b/spec/mspec/spec/matchers/be_true_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BeTrueMatcher do
+ it "matches when actual is true" do
+ expect(BeTrueMatcher.new.matches?(true)).to eq(true)
+ end
+
+ it "does not match when actual is not true" do
+ expect(BeTrueMatcher.new.matches?("")).to eq(false)
+ expect(BeTrueMatcher.new.matches?(false)).to eq(false)
+ expect(BeTrueMatcher.new.matches?(nil)).to eq(false)
+ expect(BeTrueMatcher.new.matches?(0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = BeTrueMatcher.new
+ matcher.matches?("some string")
+ expect(matcher.failure_message).to eq(["Expected \"some string\"", "to be true"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = BeTrueMatcher.new
+ matcher.matches?(true)
+ expect(matcher.negative_failure_message).to eq(["Expected true", "not to be true"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/block_caller_spec.rb b/spec/mspec/spec/matchers/block_caller_spec.rb
new file mode 100644
index 0000000000..5d7085fa63
--- /dev/null
+++ b/spec/mspec/spec/matchers/block_caller_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe BlockingMatcher do
+ it 'matches when a Proc blocks the caller' do
+ expect(BlockingMatcher.new.matches?(proc { sleep })).to eq(true)
+ end
+
+ it 'does not match when a Proc does not block the caller' do
+ expect(BlockingMatcher.new.matches?(proc { 1 })).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/matchers/complain_spec.rb b/spec/mspec/spec/matchers/complain_spec.rb
new file mode 100644
index 0000000000..399ef3105b
--- /dev/null
+++ b/spec/mspec/spec/matchers/complain_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe ComplainMatcher do
+ it "matches when executing the proc results in output to $stderr" do
+ proc = lambda { warn "I'm gonna tell yo mama" }
+ expect(ComplainMatcher.new(nil).matches?(proc)).to eq(true)
+ end
+
+ it "matches when executing the proc results in the expected output to $stderr" do
+ proc = lambda { warn "Que haces?" }
+ expect(ComplainMatcher.new("Que haces?\n").matches?(proc)).to eq(true)
+ expect(ComplainMatcher.new("Que pasa?\n").matches?(proc)).to eq(false)
+ expect(ComplainMatcher.new(/Que/).matches?(proc)).to eq(true)
+ expect(ComplainMatcher.new(/Quoi/).matches?(proc)).to eq(false)
+ end
+
+ it "does not match when there is no output to $stderr" do
+ expect(ComplainMatcher.new(nil).matches?(lambda {})).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = ComplainMatcher.new(nil)
+ matcher.matches?(lambda { })
+ expect(matcher.failure_message).to eq(["Expected a warning", "but received none"])
+ matcher = ComplainMatcher.new("listen here")
+ matcher.matches?(lambda { warn "look out" })
+ expect(matcher.failure_message).to eq(
+ ["Expected warning: \"listen here\"", "but got: \"look out\""]
+ )
+ matcher = ComplainMatcher.new(/talk/)
+ matcher.matches?(lambda { warn "listen up" })
+ expect(matcher.failure_message).to eq(
+ ["Expected warning to match: /talk/", "but got: \"listen up\""]
+ )
+ end
+
+ it "provides a useful negative failure message" do
+ proc = lambda { warn "ouch" }
+ matcher = ComplainMatcher.new(nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Unexpected warning: ", "\"ouch\""]
+ )
+ matcher = ComplainMatcher.new("ouchy")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected warning: \"ouchy\"", "but got: \"ouch\""]
+ )
+ matcher = ComplainMatcher.new(/ou/)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected warning not to match: /ou/", "but got: \"ouch\""]
+ )
+ end
+
+ context "`verbose` option specified" do
+ before do
+ $VERBOSE, @verbose = nil, $VERBOSE
+ end
+
+ after do
+ $VERBOSE = @verbose
+ end
+
+ it "sets $VERBOSE with specified second optional parameter" do
+ verbose = nil
+ proc = lambda { verbose = $VERBOSE }
+
+ ComplainMatcher.new(nil, verbose: true).matches?(proc)
+ expect(verbose).to eq(true)
+
+ ComplainMatcher.new(nil, verbose: false).matches?(proc)
+ expect(verbose).to eq(false)
+ end
+
+ it "sets $VERBOSE with false by default" do
+ verbose = nil
+ proc = lambda { verbose = $VERBOSE }
+
+ ComplainMatcher.new(nil).matches?(proc)
+ expect(verbose).to eq(false)
+ end
+
+ it "does not have side effect" do
+ proc = lambda { safe_value = $VERBOSE }
+
+ expect do
+ ComplainMatcher.new(nil, verbose: true).matches?(proc)
+ end.not_to change { $VERBOSE }
+ end
+
+ it "accepts a verbose level as single argument" do
+ verbose = nil
+ proc = lambda { verbose = $VERBOSE }
+
+ ComplainMatcher.new(verbose: true).matches?(proc)
+ expect(verbose).to eq(true)
+ end
+ end
+end
diff --git a/spec/mspec/spec/matchers/eql_spec.rb b/spec/mspec/spec/matchers/eql_spec.rb
new file mode 100644
index 0000000000..66307d2a9d
--- /dev/null
+++ b/spec/mspec/spec/matchers/eql_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe EqlMatcher do
+ it "matches when actual is eql? to expected" do
+ expect(EqlMatcher.new(1).matches?(1)).to eq(true)
+ expect(EqlMatcher.new(1.5).matches?(1.5)).to eq(true)
+ expect(EqlMatcher.new("red").matches?("red")).to eq(true)
+ expect(EqlMatcher.new(:blue).matches?(:blue)).to eq(true)
+ expect(EqlMatcher.new(Object).matches?(Object)).to eq(true)
+
+ o = Object.new
+ expect(EqlMatcher.new(o).matches?(o)).to eq(true)
+ end
+
+ it "does not match when actual is not eql? to expected" do
+ expect(EqlMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(EqlMatcher.new(Hash).matches?(Object)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = EqlMatcher.new("red")
+ matcher.matches?("red")
+ expect(matcher.failure_message).to eq(["Expected \"red\"", "to have same value and type as \"red\""])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = EqlMatcher.new(1)
+ matcher.matches?(1.0)
+ expect(matcher.negative_failure_message).to eq(["Expected 1.0", "not to have same value or type as 1"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/equal_element_spec.rb b/spec/mspec/spec/matchers/equal_element_spec.rb
new file mode 100644
index 0000000000..3a5ae4ede2
--- /dev/null
+++ b/spec/mspec/spec/matchers/equal_element_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe EqualElementMatcher do
+ it "matches if it finds an element with the passed name, no matter what attributes/content" do
+ expect(EqualElementMatcher.new("A").matches?('<A></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A").matches?('<A HREF="http://example.com"></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A").matches?('<A HREF="http://example.com"></A>')).to be_truthy
+
+ expect(EqualElementMatcher.new("BASE").matches?('<BASE></A>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A></BASE>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A></A>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("BASE").matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ end
+
+ it "matches if it finds an element with the passed name and the passed attributes" do
+ expect(EqualElementMatcher.new("A", {}).matches?('<A></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A", nil).matches?('<A HREF="http://example.com"></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://example.com"></A>')).to be_truthy
+
+ expect(EqualElementMatcher.new("A", {}).matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://test.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", "HREF" => "http://example.com").matches?('<A HREF="http://example.com" HREF="http://example.com"></A>')).to be_falsey
+ end
+
+ it "matches if it finds an element with the passed name, the passed attributes and the passed content" do
+ expect(EqualElementMatcher.new("A", {}, "").matches?('<A></A>')).to be_truthy
+ expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com">Example</A>')).to be_truthy
+
+ expect(EqualElementMatcher.new("A", {}, "Test").matches?('<A></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com"></A>')).to be_falsey
+ expect(EqualElementMatcher.new("A", {"HREF" => "http://example.com"}, "Example").matches?('<A HREF="http://example.com">Test</A>')).to be_falsey
+ end
+
+ it "can match unclosed elements" do
+ expect(EqualElementMatcher.new("BASE", nil, nil, :not_closed => true).matches?('<BASE>')).to be_truthy
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, nil, :not_closed => true).matches?('<BASE HREF="http://example.com">')).to be_truthy
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "Example", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_truthy
+
+ expect(EqualElementMatcher.new("BASE", {}, nil, :not_closed => true).matches?('<BASE HREF="http://example.com">')).to be_falsey
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_falsey
+ expect(EqualElementMatcher.new("BASE", {"HREF" => "http://example.com"}, "Test", :not_closed => true).matches?('<BASE HREF="http://example.com">Example')).to be_falsey
+ end
+
+ it "provides a useful failure message" do
+ equal_element = EqualElementMatcher.new("A", {}, "Test")
+ expect(equal_element.matches?('<A></A>')).to be_falsey
+ expect(equal_element.failure_message).to eq([%{Expected "<A></A>"}, %{to be a 'A' element with no attributes and "Test" as content}])
+
+ equal_element = EqualElementMatcher.new("A", {}, "")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.failure_message).to eq([%{Expected "<A>Test</A>"}, %{to be a 'A' element with no attributes and no content}])
+
+ equal_element = EqualElementMatcher.new("A", "HREF" => "http://www.example.com")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.failure_message).to eq([%{Expected "<A>Test</A>"}, %{to be a 'A' element with HREF="http://www.example.com" and any content}])
+ end
+
+ it "provides a useful negative failure message" do
+ equal_element = EqualElementMatcher.new("A", {}, "Test")
+ expect(equal_element.matches?('<A></A>')).to be_falsey
+ expect(equal_element.negative_failure_message).to eq([%{Expected "<A></A>"}, %{not to be a 'A' element with no attributes and "Test" as content}])
+
+ equal_element = EqualElementMatcher.new("A", {}, "")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.negative_failure_message).to eq([%{Expected "<A>Test</A>"}, %{not to be a 'A' element with no attributes and no content}])
+
+ equal_element = EqualElementMatcher.new("A", "HREF" => "http://www.example.com")
+ expect(equal_element.matches?('<A>Test</A>')).to be_falsey
+ expect(equal_element.negative_failure_message).to eq([%{Expected "<A>Test</A>"}, %{not to be a 'A' element with HREF="http://www.example.com" and any content}])
+ end
+end
diff --git a/spec/mspec/spec/matchers/equal_spec.rb b/spec/mspec/spec/matchers/equal_spec.rb
new file mode 100644
index 0000000000..2df1de54b4
--- /dev/null
+++ b/spec/mspec/spec/matchers/equal_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe EqualMatcher do
+ it "matches when actual is equal? to expected" do
+ expect(EqualMatcher.new(1).matches?(1)).to eq(true)
+ expect(EqualMatcher.new(:blue).matches?(:blue)).to eq(true)
+ expect(EqualMatcher.new(Object).matches?(Object)).to eq(true)
+
+ o = Object.new
+ expect(EqualMatcher.new(o).matches?(o)).to eq(true)
+ end
+
+ it "does not match when actual is not a equal? to expected" do
+ expect(EqualMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(EqualMatcher.new("blue").matches?("blue")).to eq(false)
+ expect(EqualMatcher.new(Hash).matches?(Object)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = EqualMatcher.new("red")
+ matcher.matches?("red")
+ expect(matcher.failure_message).to eq(["Expected \"red\"", "to be identical to \"red\""])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = EqualMatcher.new(1)
+ matcher.matches?(1)
+ expect(matcher.negative_failure_message).to eq(["Expected 1", "not to be identical to 1"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_class_variable_spec.rb b/spec/mspec/spec/matchers/have_class_variable_spec.rb
new file mode 100644
index 0000000000..d6fcf9d4e2
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_class_variable_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class IVarModMock
+ def self.class_variables
+ [:@foo]
+ end
+end
+
+RSpec.describe HaveClassVariableMatcher, "on RUBY_VERSION >= 1.9" do
+ it "matches when mod has the class variable, given as string" do
+ matcher = HaveClassVariableMatcher.new('@foo')
+ expect(matcher.matches?(IVarModMock)).to be_truthy
+ end
+
+ it "matches when mod has the class variable, given as symbol" do
+ matcher = HaveClassVariableMatcher.new(:@foo)
+ expect(matcher.matches?(IVarModMock)).to be_truthy
+ end
+
+ it "does not match when mod hasn't got the class variable, given as string" do
+ matcher = HaveClassVariableMatcher.new('@bar')
+ expect(matcher.matches?(IVarModMock)).to be_falsey
+ end
+
+ it "does not match when mod hasn't got the class variable, given as symbol" do
+ matcher = HaveClassVariableMatcher.new(:@bar)
+ expect(matcher.matches?(IVarModMock)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveClassVariableMatcher.new(:@bar)
+ matcher.matches?(IVarModMock)
+ expect(matcher.failure_message).to eq([
+ "Expected IVarModMock to have class variable '@bar'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveClassVariableMatcher.new(:@bar)
+ matcher.matches?(IVarModMock)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected IVarModMock NOT to have class variable '@bar'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_constant_spec.rb b/spec/mspec/spec/matchers/have_constant_spec.rb
new file mode 100644
index 0000000000..0bf44dbe2b
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_constant_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HCMSpecs
+ X = :x
+end
+
+RSpec.describe HaveConstantMatcher do
+ it "matches when mod has the constant" do
+ matcher = HaveConstantMatcher.new :X
+ expect(matcher.matches?(HCMSpecs)).to be_truthy
+ end
+
+ it "does not match when mod does not have the constant" do
+ matcher = HaveConstantMatcher.new :A
+ expect(matcher.matches?(HCMSpecs)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveConstantMatcher.new :A
+ matcher.matches?(HCMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HCMSpecs to have constant 'A'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveConstantMatcher.new :X
+ matcher.matches?(HCMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HCMSpecs NOT to have constant 'X'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_instance_method_spec.rb b/spec/mspec/spec/matchers/have_instance_method_spec.rb
new file mode 100644
index 0000000000..7c2e50dba6
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_instance_method_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HIMMSpecs
+ def instance_method
+ end
+
+ class Subclass < HIMMSpecs
+ def instance_sub_method
+ end
+ end
+end
+
+RSpec.describe HaveInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the instance method" do
+ matcher = HaveInstanceMethodMatcher.new :instance_method
+ expect(matcher.matches?(HIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the instance method" do
+ matcher = HaveInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HaveInstanceMethodMatcher.new :instance_method, false
+ expect(matcher.matches?(HIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveInstanceMethodMatcher.new :some_method
+ matcher.matches?(HIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HIMMSpecs to have instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveInstanceMethodMatcher.new :some_method
+ matcher.matches?(HIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HIMMSpecs NOT to have instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_instance_variable_spec.rb b/spec/mspec/spec/matchers/have_instance_variable_spec.rb
new file mode 100644
index 0000000000..12e2470f14
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_instance_variable_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe HaveInstanceVariableMatcher do
+ before :each do
+ @object = Object.new
+ def @object.instance_variables
+ [:@foo]
+ end
+ end
+
+ it "matches when object has the instance variable, given as string" do
+ matcher = HaveInstanceVariableMatcher.new('@foo')
+ expect(matcher.matches?(@object)).to be_truthy
+ end
+
+ it "matches when object has the instance variable, given as symbol" do
+ matcher = HaveInstanceVariableMatcher.new(:@foo)
+ expect(matcher.matches?(@object)).to be_truthy
+ end
+
+ it "does not match when object hasn't got the instance variable, given as string" do
+ matcher = HaveInstanceVariableMatcher.new('@bar')
+ expect(matcher.matches?(@object)).to be_falsey
+ end
+
+ it "does not match when object hasn't got the instance variable, given as symbol" do
+ matcher = HaveInstanceVariableMatcher.new(:@bar)
+ expect(matcher.matches?(@object)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveInstanceVariableMatcher.new(:@bar)
+ matcher.matches?(@object)
+ expect(matcher.failure_message).to eq([
+ "Expected #{@object.inspect} to have instance variable '@bar'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveInstanceVariableMatcher.new(:@bar)
+ matcher.matches?(@object)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected #{@object.inspect} NOT to have instance variable '@bar'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_method_spec.rb b/spec/mspec/spec/matchers/have_method_spec.rb
new file mode 100644
index 0000000000..4fc0bf5e45
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_method_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HMMSpecs
+ def instance_method
+ end
+
+ class Subclass < HMMSpecs
+ def instance_sub_method
+ end
+ end
+end
+
+RSpec.describe HaveMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the method" do
+ matcher = HaveMethodMatcher.new :instance_method
+ expect(matcher.matches?(HMMSpecs)).to be_truthy
+ expect(matcher.matches?(HMMSpecs.new)).to be_truthy
+ expect(matcher.matches?(HMMSpecs::Subclass)).to be_truthy
+ expect(matcher.matches?(HMMSpecs::Subclass.new)).to be_truthy
+ end
+
+ it "does not match when mod does not have the method" do
+ matcher = HaveMethodMatcher.new :another_method
+ expect(matcher.matches?(HMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HaveMethodMatcher.new :instance_method, false
+ expect(matcher.matches?(HMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveMethodMatcher.new :some_method
+ matcher.matches?(HMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HMMSpecs to have method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveMethodMatcher.new :some_method
+ matcher.matches?(HMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HMMSpecs NOT to have method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_private_instance_method_spec.rb b/spec/mspec/spec/matchers/have_private_instance_method_spec.rb
new file mode 100644
index 0000000000..0e65c264d9
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_private_instance_method_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPIMMSpecs
+ private
+
+ def private_method
+ end
+
+ class Subclass < HPIMMSpecs
+ private
+
+ def private_sub_method
+ end
+ end
+end
+
+RSpec.describe HavePrivateInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HavePrivateInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the private instance method" do
+ matcher = HavePrivateInstanceMethodMatcher.new :private_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the private instance method" do
+ matcher = HavePrivateInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HavePrivateInstanceMethodMatcher.new :private_method, false
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HavePrivateInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPIMMSpecs to have private instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure message for #should_not" do
+ matcher = HavePrivateInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPIMMSpecs NOT to have private instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_private_method_spec.rb b/spec/mspec/spec/matchers/have_private_method_spec.rb
new file mode 100644
index 0000000000..f433288057
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_private_method_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPMMSpecs
+ def self.private_method
+ end
+
+ private_class_method :private_method
+end
+
+RSpec.describe HavePrivateMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HavePrivateMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the private method" do
+ matcher = HavePrivateMethodMatcher.new :private_method
+ expect(matcher.matches?(HPMMSpecs)).to be_truthy
+ end
+
+ it "does not match when mod does not have the private method" do
+ matcher = HavePrivateMethodMatcher.new :another_method
+ expect(matcher.matches?(HPMMSpecs)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HavePrivateMethodMatcher.new :some_method
+ matcher.matches?(HPMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPMMSpecs to have private method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure message for #should_not" do
+ matcher = HavePrivateMethodMatcher.new :private_method
+ matcher.matches?(HPMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPMMSpecs NOT to have private method 'private_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb b/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb
new file mode 100644
index 0000000000..45b39004a3
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_protected_instance_method_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPIMMSpecs
+ protected
+
+ def protected_method
+ end
+
+ class Subclass < HPIMMSpecs
+ protected
+
+ def protected_sub_method
+ end
+ end
+end
+
+RSpec.describe HaveProtectedInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveProtectedInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the protected instance method" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :protected_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the protected instance method" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :protected_method, false
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPIMMSpecs to have protected instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HaveProtectedInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPIMMSpecs NOT to have protected instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_public_instance_method_spec.rb b/spec/mspec/spec/matchers/have_public_instance_method_spec.rb
new file mode 100644
index 0000000000..771d5b7911
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_public_instance_method_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HPIMMSpecs
+ def public_method
+ end
+
+ class Subclass < HPIMMSpecs
+ def public_sub_method
+ end
+ end
+end
+
+RSpec.describe HavePublicInstanceMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HavePublicInstanceMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when mod has the public instance method" do
+ matcher = HavePublicInstanceMethodMatcher.new :public_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_truthy
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_truthy
+ end
+
+ it "does not match when mod does not have the public instance method" do
+ matcher = HavePublicInstanceMethodMatcher.new :another_method
+ expect(matcher.matches?(HPIMMSpecs)).to be_falsey
+ end
+
+ it "does not match if the method is in a superclass and include_super is false" do
+ matcher = HavePublicInstanceMethodMatcher.new :public_method, false
+ expect(matcher.matches?(HPIMMSpecs::Subclass)).to be_falsey
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HavePublicInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HPIMMSpecs to have public instance method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure messoge for #should_not" do
+ matcher = HavePublicInstanceMethodMatcher.new :some_method
+ matcher.matches?(HPIMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HPIMMSpecs NOT to have public instance method 'some_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/have_singleton_method_spec.rb b/spec/mspec/spec/matchers/have_singleton_method_spec.rb
new file mode 100644
index 0000000000..61ef00d49c
--- /dev/null
+++ b/spec/mspec/spec/matchers/have_singleton_method_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+class HSMMSpecs
+ def self.singleton_method
+ end
+end
+
+RSpec.describe HaveSingletonMethodMatcher do
+ it "inherits from MethodMatcher" do
+ expect(HaveSingletonMethodMatcher.new(:m)).to be_kind_of(MethodMatcher)
+ end
+
+ it "matches when the class has a singleton method" do
+ matcher = HaveSingletonMethodMatcher.new :singleton_method
+ expect(matcher.matches?(HSMMSpecs)).to be_truthy
+ end
+
+ it "matches when the object has a singleton method" do
+ obj = double("HSMMSpecs")
+ def obj.singleton_method; end
+
+ matcher = HaveSingletonMethodMatcher.new :singleton_method
+ expect(matcher.matches?(obj)).to be_truthy
+ end
+
+ it "provides a failure message for #should" do
+ matcher = HaveSingletonMethodMatcher.new :some_method
+ matcher.matches?(HSMMSpecs)
+ expect(matcher.failure_message).to eq([
+ "Expected HSMMSpecs to have singleton method 'some_method'",
+ "but it does not"
+ ])
+ end
+
+ it "provides a failure message for #should_not" do
+ matcher = HaveSingletonMethodMatcher.new :singleton_method
+ matcher.matches?(HSMMSpecs)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected HSMMSpecs NOT to have singleton method 'singleton_method'",
+ "but it does"
+ ])
+ end
+end
diff --git a/spec/mspec/spec/matchers/include_any_of_spec.rb b/spec/mspec/spec/matchers/include_any_of_spec.rb
new file mode 100644
index 0000000000..1473bb6d0b
--- /dev/null
+++ b/spec/mspec/spec/matchers/include_any_of_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe IncludeAnyOfMatcher do
+ it "matches when actual includes expected" do
+ expect(IncludeAnyOfMatcher.new(2).matches?([1,2,3])).to eq(true)
+ expect(IncludeAnyOfMatcher.new("b").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include expected" do
+ expect(IncludeAnyOfMatcher.new(4).matches?([1,2,3])).to eq(false)
+ expect(IncludeAnyOfMatcher.new("d").matches?("abc")).to eq(false)
+ end
+
+ it "matches when actual includes all expected" do
+ expect(IncludeAnyOfMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true)
+ expect(IncludeAnyOfMatcher.new("a", "b", "c").matches?("abc")).to eq(true)
+ end
+
+ it "matches when actual includes any expected" do
+ expect(IncludeAnyOfMatcher.new(3, 4, 5).matches?([1,2,3])).to eq(true)
+ expect(IncludeAnyOfMatcher.new("c", "d", "e").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include any expected" do
+ expect(IncludeAnyOfMatcher.new(4, 5).matches?([1,2,3])).to eq(false)
+ expect(IncludeAnyOfMatcher.new("de").matches?("abc")).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = IncludeAnyOfMatcher.new(5, 6)
+ matcher.matches?([1,2,3])
+ expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include any of [5, 6]"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = IncludeAnyOfMatcher.new(1, 2, 3)
+ matcher.matches?([1,2])
+ expect(matcher.negative_failure_message).to eq(["Expected [1, 2]", "not to include any of [1, 2, 3]"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/include_spec.rb b/spec/mspec/spec/matchers/include_spec.rb
new file mode 100644
index 0000000000..6bf1bef085
--- /dev/null
+++ b/spec/mspec/spec/matchers/include_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe IncludeMatcher do
+ it "matches when actual includes expected" do
+ expect(IncludeMatcher.new(2).matches?([1,2,3])).to eq(true)
+ expect(IncludeMatcher.new("b").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include expected" do
+ expect(IncludeMatcher.new(4).matches?([1,2,3])).to eq(false)
+ expect(IncludeMatcher.new("d").matches?("abc")).to eq(false)
+ end
+
+ it "matches when actual includes all expected" do
+ expect(IncludeMatcher.new(3, 2, 1).matches?([1,2,3])).to eq(true)
+ expect(IncludeMatcher.new("a", "b", "c").matches?("abc")).to eq(true)
+ end
+
+ it "does not match when actual does not include all expected" do
+ expect(IncludeMatcher.new(3, 2, 4).matches?([1,2,3])).to eq(false)
+ expect(IncludeMatcher.new("a", "b", "c", "d").matches?("abc")).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = IncludeMatcher.new(5, 2)
+ matcher.matches?([1,2,3])
+ expect(matcher.failure_message).to eq(["Expected [1, 2, 3]", "to include 5"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = IncludeMatcher.new(1, 2, 3)
+ matcher.matches?([1,2,3])
+ expect(matcher.negative_failure_message).to eq(["Expected [1, 2, 3]", "not to include 3"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/infinity_spec.rb b/spec/mspec/spec/matchers/infinity_spec.rb
new file mode 100644
index 0000000000..78c4194526
--- /dev/null
+++ b/spec/mspec/spec/matchers/infinity_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/guards'
+require 'mspec/helpers'
+require 'mspec/matchers'
+
+RSpec.describe InfinityMatcher do
+ it "matches when actual is infinite and has the correct sign" do
+ expect(InfinityMatcher.new(1).matches?(infinity_value)).to eq(true)
+ expect(InfinityMatcher.new(-1).matches?(-infinity_value)).to eq(true)
+ end
+
+ it "does not match when actual is not infinite" do
+ expect(InfinityMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(InfinityMatcher.new(-1).matches?(-1.0)).to eq(false)
+ end
+
+ it "does not match when actual is infinite but has the incorrect sign" do
+ expect(InfinityMatcher.new(1).matches?(-infinity_value)).to eq(false)
+ expect(InfinityMatcher.new(-1).matches?(infinity_value)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = InfinityMatcher.new(-1)
+ matcher.matches?(0)
+ expect(matcher.failure_message).to eq(["Expected 0", "to be -Infinity"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = InfinityMatcher.new(1)
+ matcher.matches?(infinity_value)
+ expect(matcher.negative_failure_message).to eq(["Expected Infinity", "not to be Infinity"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/match_yaml_spec.rb b/spec/mspec/spec/matchers/match_yaml_spec.rb
new file mode 100644
index 0000000000..85123bb87d
--- /dev/null
+++ b/spec/mspec/spec/matchers/match_yaml_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe MatchYAMLMatcher do
+ before :each do
+ @matcher = MatchYAMLMatcher.new("--- \nfoo: bar\n")
+ end
+
+ it "compares YAML documents and matches if they're equivalent" do
+ expect(@matcher.matches?("--- \nfoo: bar\n")).to eq(true)
+ end
+
+ it "compares YAML documents and does not match if they're not equivalent" do
+ expect(@matcher.matches?("--- \nbar: foo\n")).to eq(false)
+ expect(@matcher.matches?("--- \nfoo: \nbar\n")).to eq(false)
+ end
+
+ it "also receives objects that respond_to to_yaml" do
+ matcher = MatchYAMLMatcher.new("some string")
+ expect(matcher.matches?("some string")).to eq(true)
+
+ matcher = MatchYAMLMatcher.new(['a', 'b'])
+ expect(matcher.matches?("--- \n- a\n- b\n")).to eq(true)
+
+ matcher = MatchYAMLMatcher.new("foo" => "bar")
+ expect(matcher.matches?("--- \nfoo: bar\n")).to eq(true)
+ end
+
+ it "matches documents with trailing whitespace" do
+ expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true)
+ expect(@matcher.matches?("--- \nfoo: bar \n")).to eq(true)
+ end
+
+ it "fails with a descriptive error message" do
+ expect(@matcher.matches?("foo")).to eq(false)
+ expect(@matcher.failure_message).to eq(["Expected \"foo\"", " to match \"--- \\nfoo: bar\\n\""])
+ end
+end
diff --git a/spec/mspec/spec/matchers/output_spec.rb b/spec/mspec/spec/matchers/output_spec.rb
new file mode 100644
index 0000000000..3baad9a4b2
--- /dev/null
+++ b/spec/mspec/spec/matchers/output_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe OutputMatcher do
+ it "matches when executing the proc results in the expected output to $stdout" do
+ proc = Proc.new { puts "bang!" }
+ expect(OutputMatcher.new("bang!\n", nil).matches?(proc)).to eq(true)
+ expect(OutputMatcher.new("pop", nil).matches?(proc)).to eq(false)
+ expect(OutputMatcher.new(/bang/, nil).matches?(proc)).to eq(true)
+ expect(OutputMatcher.new(/po/, nil).matches?(proc)).to eq(false)
+ end
+
+ it "matches when executing the proc results in the expected output to $stderr" do
+ proc = Proc.new { $stderr.write "boom!" }
+ expect(OutputMatcher.new(nil, "boom!").matches?(proc)).to eq(true)
+ expect(OutputMatcher.new(nil, "fizzle").matches?(proc)).to eq(false)
+ expect(OutputMatcher.new(nil, /boom/).matches?(proc)).to eq(true)
+ expect(OutputMatcher.new(nil, /fizzl/).matches?(proc)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ proc = Proc.new { print "unexpected"; $stderr.print "unerror" }
+ matcher = OutputMatcher.new("expected", "error")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stdout: \"expected\"\n $stderr: \"error\"\n",
+ " got:\n $stdout: \"unexpected\"\n $stderr: \"unerror\"\n"]
+ )
+ matcher = OutputMatcher.new("expected", nil)
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stdout: \"expected\"\n",
+ " got:\n $stdout: \"unexpected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, "error")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stderr: \"error\"\n",
+ " got:\n $stderr: \"unerror\"\n"]
+ )
+ matcher = OutputMatcher.new(/base/, nil)
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stdout: /base/\n",
+ " got:\n $stdout: \"unexpected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, /octave/)
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ["Expected:\n $stderr: /octave/\n",
+ " got:\n $stderr: \"unerror\"\n"]
+ )
+ end
+
+ it "provides a useful negative failure message" do
+ proc = Proc.new { puts "expected"; $stderr.puts "error" }
+ matcher = OutputMatcher.new("expected", "error")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stdout: \"expected\"\n $stderr: \"error\"\n"]
+ )
+ matcher = OutputMatcher.new("expected", nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stdout: \"expected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, "error")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stderr: \"error\"\n"]
+ )
+ matcher = OutputMatcher.new(/expect/, nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stdout: \"expected\"\n"]
+ )
+ matcher = OutputMatcher.new(nil, /err/)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ["Expected output not to be:\n", " $stderr: \"error\"\n"]
+ )
+ end
+end
diff --git a/spec/mspec/spec/matchers/output_to_fd_spec.rb b/spec/mspec/spec/matchers/output_to_fd_spec.rb
new file mode 100644
index 0000000000..a39cab3206
--- /dev/null
+++ b/spec/mspec/spec/matchers/output_to_fd_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe OutputToFDMatcher do
+ # Figure out how in the hell to achieve this
+ it "matches when running the block produces the expected output to the given FD" do
+ expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.print "Hi\n" })).to eq(true)
+ end
+
+ it "does not match if running the block does not produce the expected output to the FD" do
+ expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda { $stderr.puts("Hello\n") })).to eq(false)
+ end
+
+ it "propagate the exception if one is thrown while matching" do
+ exc = RuntimeError.new("propagates")
+ expect {
+ expect(OutputToFDMatcher.new("Hi\n", STDERR).matches?(lambda {
+ raise exc
+ })).to eq(false)
+ }.to raise_error(exc)
+ end
+
+ it "defaults to matching against STDOUT" do
+ object = Object.new
+ object.extend MSpecMatchers
+ expect(object.send(:output_to_fd, "Hi\n").matches?(lambda { $stdout.print "Hi\n" })).to eq(true)
+ end
+
+ it "accepts any IO instance" do
+ io = IO.new STDOUT.fileno
+ expect(OutputToFDMatcher.new("Hi\n", io).matches?(lambda { io.print "Hi\n" })).to eq(true)
+ end
+
+ it "allows matching with a Regexp" do
+ s = "Hi there\n"
+ expect(OutputToFDMatcher.new(/Hi/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/Hi?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/[hH]i?/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/.*/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/H.*?here/, STDERR).matches?(lambda { $stderr.print s })).to eq(true)
+ expect(OutputToFDMatcher.new(/Ahoy/, STDERR).matches?(lambda { $stderr.print s })).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/matchers/raise_error_spec.rb b/spec/mspec/spec/matchers/raise_error_spec.rb
new file mode 100644
index 0000000000..3849c7dd2a
--- /dev/null
+++ b/spec/mspec/spec/matchers/raise_error_spec.rb
@@ -0,0 +1,234 @@
+require 'spec_helper'
+
+class ExpectedException < Exception; end
+class UnexpectedException < Exception; end
+
+RSpec.describe RaiseErrorMatcher do
+ before :each do
+ state = double("run state").as_null_object
+ allow(MSpec).to receive(:current).and_return(state)
+ end
+
+ it "matches when the proc raises the expected exception" do
+ proc = Proc.new { raise ExpectedException }
+ matcher = RaiseErrorMatcher.new(ExpectedException, nil)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "executes its optional {/} block if matched" do
+ ensure_mspec_method(-> {}.method(:should))
+
+ run = false
+ -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error { |error|
+ expect(error.class).to eq(ExpectedException)
+ run = true
+ }
+ expect(run).to eq(true)
+ end
+
+ it "executes its optional do/end block if matched" do
+ ensure_mspec_method(-> {}.method(:should))
+
+ run = false
+ -> { raise ExpectedException }.should PublicMSpecMatchers.raise_error do |error|
+ expect(error.class).to eq(ExpectedException)
+ run = true
+ end
+ expect(run).to eq(true)
+ end
+
+ it "matches when the proc raises the expected exception with the expected message" do
+ proc = Proc.new { raise ExpectedException, "message" }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "message")
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "matches when the proc raises the expected exception with a matching message" do
+ proc = Proc.new { raise ExpectedException, "some message" }
+ matcher = RaiseErrorMatcher.new(ExpectedException, /some/)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "does not match when the proc does not raise the expected exception" do
+ exc = UnexpectedException.new
+ matcher = RaiseErrorMatcher.new(ExpectedException, nil)
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ expect {
+ matcher.matches?(Proc.new { raise exc })
+ }.to raise_error(UnexpectedException)
+ end
+
+ it "does not match when the proc raises the expected exception with an unexpected message" do
+ exc = ExpectedException.new("unexpected")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ expect {
+ matcher.matches?(Proc.new { raise exc })
+ }.to raise_error(ExpectedException)
+ end
+
+ it "does not match when the proc does not raise an exception" do
+ proc = Proc.new {}
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ expect(matcher.matches?(proc)).to eq(false)
+ end
+
+ it "provides a useful failure message when the exception class differs" do
+ exc = UnexpectedException.new("message")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "message")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ begin
+ matcher.matches?(Proc.new { raise exc })
+ rescue UnexpectedException => e
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("message")', 'but got: UnexpectedException("message")']
+ )
+ expect(ExceptionState.new(nil, nil, e).message).to eq(
+ "Expected ExpectedException(\"message\")\nbut got: UnexpectedException(\"message\")"
+ )
+ else
+ raise "no exception"
+ end
+ end
+
+ it "provides a useful failure message when the proc raises the expected exception with an unexpected message" do
+ exc = ExpectedException.new("unexpected")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ begin
+ matcher.matches?(Proc.new { raise exc })
+ rescue ExpectedException => e
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', 'but got: ExpectedException("unexpected")']
+ )
+ expect(ExceptionState.new(nil, nil, e).message).to eq(
+ "Expected ExpectedException(\"expected\")\nbut got: ExpectedException(\"unexpected\")"
+ )
+ else
+ raise "no exception"
+ end
+ end
+
+ it "provides a useful failure message when both the exception class and message differ" do
+ exc = UnexpectedException.new("unexpected")
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+
+ expect(matcher.matching_exception?(exc)).to eq(false)
+ begin
+ matcher.matches?(Proc.new { raise exc })
+ rescue UnexpectedException => e
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', 'but got: UnexpectedException("unexpected")']
+ )
+ expect(ExceptionState.new(nil, nil, e).message).to eq(
+ "Expected ExpectedException(\"expected\")\nbut got: UnexpectedException(\"unexpected\")"
+ )
+ else
+ raise "no exception"
+ end
+ end
+
+ it "provides a useful failure message when no exception is raised" do
+ proc = Proc.new { 120 }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', "but no exception was raised (120 was returned)"]
+ )
+ end
+
+ it "provides a useful failure message when no exception is raised and nil is returned" do
+ proc = Proc.new { nil }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', "but no exception was raised (nil was returned)"]
+ )
+ end
+
+ it "provides a useful failure message when no exception is raised and the result raises in #pretty_inspect" do
+ result = Object.new
+ def result.pretty_inspect
+ raise ArgumentError, "bad"
+ end
+ proc = Proc.new { result }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.failure_message).to eq(
+ ['Expected ExpectedException("expected")', 'but no exception was raised (#<Object>(#pretty_inspect raised #<ArgumentError: bad>) was returned)']
+ )
+ end
+
+ it "provides a useful negative failure message" do
+ proc = Proc.new { raise ExpectedException, "expected" }
+ matcher = RaiseErrorMatcher.new(ExpectedException, "expected")
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ['Expected to not get ExpectedException("expected")', ""]
+ )
+ end
+
+ it "provides a useful negative failure message for strict subclasses of the matched exception class" do
+ proc = Proc.new { raise UnexpectedException, "unexpected" }
+ matcher = RaiseErrorMatcher.new(Exception, nil)
+ matcher.matches?(proc)
+ expect(matcher.negative_failure_message).to eq(
+ ['Expected to not get Exception', 'but got: UnexpectedException']
+ )
+ end
+
+ it "matches cause if given" do
+ cause = RuntimeError.new("foo")
+ proc = -> do
+ raise cause
+ rescue
+ raise "bar"
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "matches message and cause if given" do
+ cause = RuntimeError.new("foo")
+ proc = -> do
+ raise cause
+ rescue
+ raise "bar"
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, "bar", cause: cause)
+ expect(matcher.matches?(proc)).to eq(true)
+ end
+
+ it "provides useful negative failure message when cause does not match" do
+ cause = RuntimeError.new("bar")
+ proc = -> do
+ raise "foo"
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, cause: cause)
+
+ begin
+ matcher.matches?(proc)
+ rescue RuntimeError
+ expect(matcher.failure_message).to eq(
+ ['Expected RuntimeError(cause: #<RuntimeError: bar>)', 'but got: RuntimeError(cause: nil)']
+ )
+ end
+
+ matcher = RaiseErrorMatcher.new(RuntimeError, "foo", cause: cause)
+
+ begin
+ matcher.matches?(proc)
+ rescue RuntimeError
+ expect(matcher.failure_message).to eq(
+ ['Expected RuntimeError("foo", cause: #<RuntimeError: bar>)', 'but got: RuntimeError("foo", cause: nil)']
+ )
+ end
+ end
+end
diff --git a/spec/mspec/spec/matchers/respond_to_spec.rb b/spec/mspec/spec/matchers/respond_to_spec.rb
new file mode 100644
index 0000000000..6f1cd8d148
--- /dev/null
+++ b/spec/mspec/spec/matchers/respond_to_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe RespondToMatcher do
+ it "matches when actual does respond_to? expected" do
+ expect(RespondToMatcher.new(:to_s).matches?(Object.new)).to eq(true)
+ expect(RespondToMatcher.new(:inject).matches?([])).to eq(true)
+ expect(RespondToMatcher.new(:[]).matches?(1)).to eq(true)
+ expect(RespondToMatcher.new(:[]=).matches?("string")).to eq(true)
+ end
+
+ it "does not match when actual does not respond_to? expected" do
+ expect(RespondToMatcher.new(:to_i).matches?(Object.new)).to eq(false)
+ expect(RespondToMatcher.new(:inject).matches?(1)).to eq(false)
+ expect(RespondToMatcher.new(:non_existent_method).matches?([])).to eq(false)
+ expect(RespondToMatcher.new(:[]=).matches?(1)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = RespondToMatcher.new(:non_existent_method)
+ matcher.matches?('string')
+ expect(matcher.failure_message).to eq([
+ "Expected \"string\" (String)", "to respond to non_existent_method"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = RespondToMatcher.new(:to_i)
+ matcher.matches?(4.0)
+ expect(matcher.negative_failure_message).to eq([
+ "Expected 4.0 (Float)", "not to respond to to_i"])
+ end
+end
diff --git a/spec/mspec/spec/matchers/signed_zero_spec.rb b/spec/mspec/spec/matchers/signed_zero_spec.rb
new file mode 100644
index 0000000000..6d1c1007bc
--- /dev/null
+++ b/spec/mspec/spec/matchers/signed_zero_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers'
+
+RSpec.describe SignedZeroMatcher do
+ it "matches when actual is zero and has the correct sign" do
+ expect(SignedZeroMatcher.new(1).matches?(0.0)).to eq(true)
+ expect(SignedZeroMatcher.new(-1).matches?(-0.0)).to eq(true)
+ end
+
+ it "does not match when actual is non-zero" do
+ expect(SignedZeroMatcher.new(1).matches?(1.0)).to eq(false)
+ expect(SignedZeroMatcher.new(-1).matches?(-1.0)).to eq(false)
+ end
+
+ it "does not match when actual is zero but has the incorrect sign" do
+ expect(SignedZeroMatcher.new(1).matches?(-0.0)).to eq(false)
+ expect(SignedZeroMatcher.new(-1).matches?(0.0)).to eq(false)
+ end
+
+ it "provides a useful failure message" do
+ matcher = SignedZeroMatcher.new(-1)
+ matcher.matches?(0.0)
+ expect(matcher.failure_message).to eq(["Expected 0.0", "to be -0.0"])
+ end
+
+ it "provides a useful negative failure message" do
+ matcher = SignedZeroMatcher.new(-1)
+ matcher.matches?(-0.0)
+ expect(matcher.negative_failure_message).to eq(["Expected -0.0", "not to be -0.0"])
+ end
+end
diff --git a/spec/mspec/spec/mocks/mock_spec.rb b/spec/mspec/spec/mocks/mock_spec.rb
new file mode 100644
index 0000000000..7426e0ff88
--- /dev/null
+++ b/spec/mspec/spec/mocks/mock_spec.rb
@@ -0,0 +1,529 @@
+# This is a bit awkward. Currently the way to verify that the
+# opposites are true (for example a failure when the specified
+# arguments are NOT provided) is to simply alter the particular
+# spec to a failure condition.
+require 'spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/mocks/mock'
+require 'mspec/mocks/proxy'
+
+RSpec.describe Mock, ".mocks" do
+ it "returns a Hash" do
+ expect(Mock.mocks).to be_kind_of(Hash)
+ end
+end
+
+RSpec.describe Mock, ".stubs" do
+ it "returns a Hash" do
+ expect(Mock.stubs).to be_kind_of(Hash)
+ end
+end
+
+RSpec.describe Mock, ".replaced_name" do
+ it "returns the name for a method that is being replaced by a mock method" do
+ m = double('a fake id')
+ expect(Mock.replaced_name(Mock.replaced_key(m, :method_call))).to eq(:"__mspec_method_call__")
+ end
+end
+
+RSpec.describe Mock, ".replaced_key" do
+ it "returns a key used internally by Mock" do
+ m = double('a fake id')
+ expect(Mock.replaced_key(m, :method_call)).to eq([m.object_id, :method_call])
+ end
+end
+
+RSpec.describe Mock, ".replaced?" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ it "returns true if a method has been stubbed on an object" do
+ Mock.install_method @mock, :method_call
+ expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy
+ end
+
+ it "returns true if a method has been mocked on an object" do
+ Mock.install_method @mock, :method_call, :stub
+ expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_truthy
+ end
+
+ it "returns false if a method has not been stubbed or mocked" do
+ expect(Mock.replaced?(Mock.replaced_key(@mock, :method_call))).to be_falsey
+ end
+end
+
+RSpec.describe Mock, ".name_or_inspect" do
+ before :each do
+ @mock = double("I have a #name")
+ end
+
+ it "returns the value of @name if set" do
+ @mock.instance_variable_set(:@name, "Myself")
+ expect(Mock.name_or_inspect(@mock)).to eq("Myself")
+ end
+end
+
+RSpec.describe Mock, ".install_method for mocks" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ after :each do
+ Mock.reset
+ end
+
+ it "returns a MockProxy instance" do
+ expect(Mock.install_method(@mock, :method_call)).to be_an_instance_of(MockProxy)
+ end
+
+ it "does not override a previously mocked method with the same name" do
+ Mock.install_method(@mock, :method_call).with(:a, :b).and_return(1)
+ Mock.install_method(@mock, :method_call).with(:c).and_return(2)
+ @mock.method_call(:a, :b)
+ @mock.method_call(:c)
+ expect { @mock.method_call(:d) }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ # This illustrates RSpec's behavior. This spec fails in mock call count verification
+ # on RSpec (i.e. Mock 'foo' expected :foo with (any args) once, but received it 0 times)
+ # and we mimic the behavior of RSpec.
+ #
+ # describe "A mock receiving multiple calls to #should_receive" do
+ # it "returns the first value mocked" do
+ # m = mock 'multiple #should_receive'
+ # m.should_receive(:foo).and_return(true)
+ # m.foo.should == true
+ # m.should_receive(:foo).and_return(false)
+ # m.foo.should == true
+ # end
+ # end
+ #
+ it "does not override a previously mocked method having the same arguments" do
+ Mock.install_method(@mock, :method_call).with(:a).and_return(true)
+ expect(@mock.method_call(:a)).to eq(true)
+ Mock.install_method(@mock, :method_call).with(:a).and_return(false)
+ expect(@mock.method_call(:a)).to eq(true)
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "properly sends #respond_to? calls to the aliased respond_to? method when not matching mock expectations" do
+ Mock.install_method(@mock, :respond_to?).with(:to_str).and_return('mock to_str')
+ Mock.install_method(@mock, :respond_to?).with(:to_int).and_return('mock to_int')
+ expect(@mock.respond_to?(:to_str)).to eq('mock to_str')
+ expect(@mock.respond_to?(:to_int)).to eq('mock to_int')
+ expect(@mock.respond_to?(:to_s)).to eq(true)
+ expect(@mock.respond_to?(:not_really_a_real_method_seriously)).to eq(false)
+ end
+
+ it "adds to the expectation tally" do
+ state = double("run state").as_null_object
+ allow(state).to receive(:state).and_return(double("spec state"))
+ expect(MSpec).to receive(:current).and_return(state)
+ expect(MSpec).to receive(:actions).with(:expectation, state.state)
+ Mock.install_method(@mock, :method_call).and_return(1)
+ expect(@mock.method_call).to eq(1)
+ end
+
+ it "registers that an expectation has been encountered" do
+ state = double("run state").as_null_object
+ allow(state).to receive(:state).and_return(double("spec state"))
+ expect(MSpec).to receive(:expectation)
+ Mock.install_method(@mock, :method_call).and_return(1)
+ expect(@mock.method_call).to eq(1)
+ end
+end
+
+RSpec.describe Mock, ".install_method for stubs" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "returns a MockProxy instance" do
+ expect(Mock.install_method(@mock, :method_call, :stub)).to be_an_instance_of(MockProxy)
+ end
+
+ # This illustrates RSpec's behavior. This spec passes on RSpec and we mimic it
+ #
+ # describe "A mock receiving multiple calls to #stub" do
+ # it "returns the last value stubbed" do
+ # m = mock 'multiple #stub'
+ # m.stub(:foo).and_return(true)
+ # m.foo.should == true
+ # m.stub(:foo).and_return(false)
+ # m.foo.should == false
+ # end
+ # end
+ it "inserts new stubs before old stubs" do
+ Mock.install_method(@mock, :method_call, :stub).with(:a).and_return(true)
+ expect(@mock.method_call(:a)).to eq(true)
+ Mock.install_method(@mock, :method_call, :stub).with(:a).and_return(false)
+ expect(@mock.method_call(:a)).to eq(false)
+ Mock.verify_count
+ end
+
+ it "does not add to the expectation tally" do
+ state = double("run state").as_null_object
+ allow(state).to receive(:state).and_return(double("spec state"))
+ expect(MSpec).not_to receive(:actions)
+ Mock.install_method(@mock, :method_call, :stub).and_return(1)
+ expect(@mock.method_call).to eq(1)
+ end
+end
+
+RSpec.describe Mock, ".install_method" do
+ before :each do
+ @mock = double('install_method')
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "does not alias a mocked or stubbed method when installing a new mock or stub" do
+ expect(@mock).not_to respond_to(:method_call)
+
+ Mock.install_method @mock, :method_call
+ expect(@mock).to respond_to(:method_call)
+ expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call)))
+
+ Mock.install_method @mock, :method_call, :stub
+ expect(@mock).to respond_to(:method_call)
+ expect(@mock).not_to respond_to(Mock.replaced_name(Mock.replaced_key(@mock, :method_call)))
+ end
+end
+
+class MockAndRaiseError < Exception; end
+
+RSpec.describe Mock, ".verify_call" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_call')
+ @proxy = Mock.install_method @mock, :method_call
+ end
+
+ after :each do
+ ScratchPad.clear
+ Mock.cleanup
+ end
+
+ it "does not raise an exception when the mock method receives the expected arguments" do
+ @proxy.with(1, 'two', :three)
+ Mock.verify_call @mock, :method_call, 1, 'two', :three
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock method does not receive the expected arguments" do
+ @proxy.with(4, 2)
+ expect {
+ Mock.verify_call @mock, :method_call, 42
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock method is called with arguments but expects none" do
+ expect {
+ @proxy.with(:no_args)
+ Mock.verify_call @mock, :method_call, "hello"
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock method is called with no arguments but expects some" do
+ @proxy.with("hello", "beautiful", "world")
+ expect {
+ Mock.verify_call @mock, :method_call
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when the mock method is called with arguments and is expecting :any_args" do
+ @proxy.with(:any_args)
+ Mock.verify_call @mock, :method_call, 1, 2, 3
+ end
+
+ it "yields a passed block when it is expected to" do
+ @proxy.and_yield()
+ Mock.verify_call @mock, :method_call do
+ ScratchPad.record true
+ end
+ expect(ScratchPad.recorded).to eq(true)
+ end
+
+ it "does not yield a passed block when it is not expected to" do
+ Mock.verify_call @mock, :method_call do
+ ScratchPad.record true
+ end
+ expect(ScratchPad.recorded).to eq(nil)
+ end
+
+ it "can yield subsequently" do
+ @proxy.and_yield(1).and_yield(2).and_yield(3)
+
+ ScratchPad.record []
+ Mock.verify_call @mock, :method_call do |arg|
+ ScratchPad << arg
+ end
+ expect(ScratchPad.recorded).to eq([1, 2, 3])
+ end
+
+ it "can yield and return an expected value" do
+ @proxy.and_yield(1).and_return(3)
+
+ expect(Mock.verify_call(@mock, :method_call) { |arg| ScratchPad.record arg }).to eq(3)
+ expect(ScratchPad.recorded).to eq(1)
+ end
+
+ it "raises an exception when it is expected to yield but no block is given" do
+ @proxy.and_yield(1, 2, 3)
+ expect {
+ Mock.verify_call(@mock, :method_call)
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an exception when it is expected to yield more arguments than the block can take" do
+ @proxy.and_yield(1, 2, 3)
+ expect {
+ Mock.verify_call(@mock, :method_call) {|a, b|}
+ }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when it is expected to yield to a block that can take any number of arguments" do
+ @proxy.and_yield(1, 2, 3)
+ expect {
+ Mock.verify_call(@mock, :method_call) {|*a|}
+ }.not_to raise_error
+ end
+
+ it "raises an exception when expected to" do
+ @proxy.and_raise(MockAndRaiseError)
+ expect {
+ Mock.verify_call @mock, :method_call
+ }.to raise_error(MockAndRaiseError)
+ end
+end
+
+RSpec.describe Mock, ".verify_call mixing mocks and stubs" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_call')
+ end
+
+ after :each do
+ ScratchPad.clear
+ Mock.cleanup
+ end
+
+ it "checks the mock arguments when a mock is defined after a stub" do
+ Mock.install_method @mock, :method_call, :stub
+ Mock.install_method(@mock, :method_call, :mock).with("arg")
+
+ expect {
+ @mock.method_call
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \(\)/)
+
+ expect {
+ @mock.method_call("a", "b")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("a", "b"\)/)
+
+ expect {
+ @mock.method_call("foo")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("foo"\)/)
+
+ @mock.method_call("arg")
+ end
+
+ it "checks the mock arguments when a stub is defined after a mock" do
+ Mock.install_method(@mock, :method_call, :mock).with("arg")
+ Mock.install_method @mock, :method_call, :stub
+
+ expect {
+ @mock.method_call
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \(\)/)
+
+ expect {
+ @mock.method_call("a", "b")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("a", "b"\)/)
+
+ expect {
+ @mock.method_call("foo")
+ }.to raise_error(SpecExpectationNotMetError, /called with unexpected arguments \("foo"\)/)
+
+ @mock.method_call("arg")
+ end
+end
+
+RSpec.describe Mock, ".verify_count" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_count')
+ @proxy = Mock.install_method @mock, :method_call
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "does not raise an exception when the mock receives at least the expected number of calls" do
+ @proxy.at_least(2)
+ @mock.method_call
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives less than at least the expected number of calls" do
+ @proxy.at_least(2)
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when the mock receives at most the expected number of calls" do
+ @proxy.at_most(2)
+ @mock.method_call
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives more than at most the expected number of calls" do
+ @proxy.at_most(2)
+ @mock.method_call
+ @mock.method_call
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "does not raise an exception when the mock receives exactly the expected number of calls" do
+ @proxy.exactly(2)
+ @mock.method_call
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives less than exactly the expected number of calls" do
+ @proxy.exactly(2)
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+
+ it "raises an SpecExpectationNotMetError when the mock receives more than exactly the expected number of calls" do
+ @proxy.exactly(2)
+ @mock.method_call
+ @mock.method_call
+ @mock.method_call
+ expect { Mock.verify_count }.to raise_error(SpecExpectationNotMetError)
+ end
+end
+
+RSpec.describe Mock, ".verify_count mixing mocks and stubs" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('verify_count')
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "does not raise an exception for a stubbed method that is never called" do
+ Mock.install_method @mock, :method_call, :stub
+ Mock.verify_count
+ end
+
+ it "verifies the calls to the mocked method when a mock is defined after a stub" do
+ Mock.install_method @mock, :method_call, :stub
+ Mock.install_method @mock, :method_call, :mock
+
+ expect {
+ Mock.verify_count
+ }.to raise_error(SpecExpectationNotMetError, /received it 0 times/)
+
+ @mock.method_call
+ Mock.verify_count
+ end
+
+ it "verifies the calls to the mocked method when a mock is defined before a stub" do
+ Mock.install_method @mock, :method_call, :mock
+ Mock.install_method @mock, :method_call, :stub
+
+ expect {
+ Mock.verify_count
+ }.to raise_error(SpecExpectationNotMetError, /received it 0 times/)
+
+ @mock.method_call
+ Mock.verify_count
+ end
+end
+
+RSpec.describe Mock, ".cleanup" do
+ before :each do
+ allow(MSpec).to receive(:actions)
+ allow(MSpec).to receive(:current).and_return(double("spec state").as_null_object)
+
+ @mock = double('cleanup')
+ @proxy = Mock.install_method @mock, :method_call
+ end
+
+ after :each do
+ Mock.cleanup
+ end
+
+ it "removes the mock method call if it did not override an existing method" do
+ expect(@mock).to respond_to(:method_call)
+
+ Mock.cleanup
+ expect(@mock).not_to respond_to(:method_call)
+ end
+
+ it "removes the replaced method if the mock method overrides an existing method" do
+ def @mock.already_here() :hey end
+ expect(@mock).to respond_to(:already_here)
+ replaced_name = Mock.replaced_name(Mock.replaced_key(@mock, :already_here))
+ Mock.install_method @mock, :already_here
+ expect(@mock).to respond_to(replaced_name)
+
+ Mock.cleanup
+ expect(@mock).not_to respond_to(replaced_name)
+ expect(@mock).to respond_to(:already_here)
+ expect(@mock.already_here).to eq(:hey)
+ end
+
+ it "removes all mock expectations" do
+ expect(Mock.mocks).to eq({ Mock.replaced_key(@mock, :method_call) => [@proxy] })
+ Mock.cleanup
+ expect(Mock.mocks).to eq({})
+ end
+
+ it "removes all stubs" do
+ Mock.cleanup # remove @proxy
+ @stub = Mock.install_method @mock, :method_call, :stub
+ expect(Mock.stubs).to eq({ Mock.replaced_key(@mock, :method_call) => [@stub] })
+ Mock.cleanup
+ expect(Mock.stubs).to eq({})
+ end
+
+ it "removes the replaced name for mocks" do
+ replaced_key = Mock.replaced_key(@mock, :method_call)
+ expect(Mock).to receive(:clear_replaced).with(replaced_key)
+
+ expect(Mock.replaced?(replaced_key)).to be_truthy
+
+ Mock.cleanup
+ expect(Mock.replaced?(replaced_key)).to be_falsey
+ end
+end
diff --git a/spec/mspec/spec/mocks/proxy_spec.rb b/spec/mspec/spec/mocks/proxy_spec.rb
new file mode 100644
index 0000000000..b994634694
--- /dev/null
+++ b/spec/mspec/spec/mocks/proxy_spec.rb
@@ -0,0 +1,405 @@
+require 'spec_helper'
+require 'mspec/mocks/proxy'
+
+RSpec.describe MockObject, ".new" do
+ it "creates a new mock object" do
+ m = MockObject.new('not a null object')
+ expect { m.not_a_method }.to raise_error(NoMethodError)
+ end
+
+ it "creates a new mock object that follows the NullObject pattern" do
+ m = MockObject.new('null object', :null_object => true)
+ expect(m.not_really_a_method).to equal(m)
+ end
+end
+
+RSpec.describe MockProxy, ".new" do
+ it "creates a mock proxy by default" do
+ expect(MockProxy.new.mock?).to be_truthy
+ end
+
+ it "creates a stub proxy by request" do
+ expect(MockProxy.new(:stub).stub?).to be_truthy
+ end
+
+ it "sets the call expectation to 1 call for a mock" do
+ expect(MockProxy.new.count).to eq([:exactly, 1])
+ end
+
+ it "sets the call expectation to any number of times for a stub" do
+ expect(MockProxy.new(:stub).count).to eq([:any_number_of_times, 0])
+ end
+end
+
+RSpec.describe MockProxy, "#count" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns the expected number of calls the mock should receive" do
+ expect(@proxy.count).to eq([:exactly, 1])
+ expect(@proxy.at_least(3).count).to eq([:at_least, 3])
+ end
+end
+
+RSpec.describe MockProxy, "#arguments" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns the expected arguments" do
+ expect(@proxy.arguments).to eq(:any_args)
+ end
+end
+
+RSpec.describe MockProxy, "#with" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.with(:a)).to be_equal(@proxy)
+ end
+
+ it "raises an ArgumentError if no arguments are given" do
+ expect { @proxy.with }.to raise_error(ArgumentError)
+ end
+
+ it "accepts any number of arguments" do
+ expect(@proxy.with(1, 2, 3)).to be_an_instance_of(MockProxy)
+ expect(@proxy.arguments).to eq([1,2,3])
+ end
+end
+
+RSpec.describe MockProxy, "#once" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.once).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to 1" do
+ @proxy.once
+ expect(@proxy.count).to eq([:exactly, 1])
+ end
+
+ it "accepts no arguments" do
+ expect { @proxy.once(:a) }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#twice" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.twice).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to 2" do
+ @proxy.twice
+ expect(@proxy.count).to eq([:exactly, 2])
+ end
+
+ it "accepts no arguments" do
+ expect { @proxy.twice(:b) }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#exactly" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.exactly(2)).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to exactly n" do
+ @proxy.exactly(5)
+ expect(@proxy.count).to eq([:exactly, 5])
+ end
+
+ it "does not accept an argument that Integer() cannot convert" do
+ expect { @proxy.exactly('x') }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#at_least" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.at_least(3)).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to at least n" do
+ @proxy.at_least(3)
+ expect(@proxy.count).to eq([:at_least, 3])
+ end
+
+ it "accepts :once :twice" do
+ @proxy.at_least(:once)
+ expect(@proxy.count).to eq([:at_least, 1])
+ @proxy.at_least(:twice)
+ expect(@proxy.count).to eq([:at_least, 2])
+ end
+
+ it "does not accept an argument that Integer() cannot convert" do
+ expect { @proxy.at_least('x') }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#at_most" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.at_most(2)).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to at most n" do
+ @proxy.at_most(2)
+ expect(@proxy.count).to eq([:at_most, 2])
+ end
+
+ it "accepts :once, :twice" do
+ @proxy.at_most(:once)
+ expect(@proxy.count).to eq([:at_most, 1])
+ @proxy.at_most(:twice)
+ expect(@proxy.count).to eq([:at_most, 2])
+ end
+
+ it "does not accept an argument that Integer() cannot convert" do
+ expect { @proxy.at_most('x') }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#any_number_of_times" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.any_number_of_times).to be_equal(@proxy)
+ end
+
+ it "sets the expected calls to any number of times" do
+ @proxy.any_number_of_times
+ expect(@proxy.count).to eq([:any_number_of_times, 0])
+ end
+
+ it "does not accept an argument" do
+ expect { @proxy.any_number_of_times(2) }.to raise_error
+ end
+end
+
+RSpec.describe MockProxy, "#and_return" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.and_return(false)).to equal(@proxy)
+ end
+
+ it "sets the expected return value" do
+ @proxy.and_return(false)
+ expect(@proxy.returning).to eq(false)
+ end
+
+ it "accepts any number of return values" do
+ @proxy.and_return(1, 2, 3)
+ expect(@proxy.returning).to eq(1)
+ expect(@proxy.returning).to eq(2)
+ expect(@proxy.returning).to eq(3)
+ end
+
+ it "implicitly sets the expected number of calls" do
+ @proxy.and_return(1, 2, 3)
+ expect(@proxy.count).to eq([:exactly, 3])
+ end
+
+ it "only sets the expected number of calls if it is higher than what is already set" do
+ @proxy.at_least(5).times.and_return(1, 2, 3)
+ expect(@proxy.count).to eq([:at_least, 5])
+
+ @proxy.at_least(2).times.and_return(1, 2, 3)
+ expect(@proxy.count).to eq([:at_least, 3])
+ end
+end
+
+RSpec.describe MockProxy, "#returning" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns nil by default" do
+ expect(@proxy.returning).to be_nil
+ end
+
+ it "returns the value set by #and_return" do
+ @proxy.and_return(2)
+ expect(@proxy.returning).to eq(2)
+ expect(@proxy.returning).to eq(2)
+ end
+
+ it "returns a sequence of values set by #and_return" do
+ @proxy.and_return(1,2,3,4)
+ expect(@proxy.returning).to eq(1)
+ expect(@proxy.returning).to eq(2)
+ expect(@proxy.returning).to eq(3)
+ expect(@proxy.returning).to eq(4)
+ expect(@proxy.returning).to eq(4)
+ expect(@proxy.returning).to eq(4)
+ end
+end
+
+RSpec.describe MockProxy, "#calls" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns the number of times the proxy is called" do
+ expect(@proxy.calls).to eq(0)
+ end
+end
+
+RSpec.describe MockProxy, "#called" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "increments the number of times the proxy is called" do
+ @proxy.called
+ @proxy.called
+ expect(@proxy.calls).to eq(2)
+ end
+end
+
+RSpec.describe MockProxy, "#times" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "is a no-op" do
+ expect(@proxy.times).to eq(@proxy)
+ end
+end
+
+RSpec.describe MockProxy, "#stub?" do
+ it "returns true if the proxy is created as a stub" do
+ expect(MockProxy.new(:stub).stub?).to be_truthy
+ end
+
+ it "returns false if the proxy is created as a mock" do
+ expect(MockProxy.new(:mock).stub?).to be_falsey
+ end
+end
+
+RSpec.describe MockProxy, "#mock?" do
+ it "returns true if the proxy is created as a mock" do
+ expect(MockProxy.new(:mock).mock?).to be_truthy
+ end
+
+ it "returns false if the proxy is created as a stub" do
+ expect(MockProxy.new(:stub).mock?).to be_falsey
+ end
+end
+
+RSpec.describe MockProxy, "#and_yield" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns self" do
+ expect(@proxy.and_yield(false)).to equal(@proxy)
+ end
+
+ it "sets the expected values to yield" do
+ expect(@proxy.and_yield(1).yielding).to eq([[1]])
+ end
+
+ it "accepts multiple values to yield" do
+ expect(@proxy.and_yield(1, 2, 3).yielding).to eq([[1, 2, 3]])
+ end
+end
+
+RSpec.describe MockProxy, "#raising" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns nil by default" do
+ expect(@proxy.raising).to be_nil
+ end
+
+ it "returns the exception object passed to #and_raise" do
+ exc = double("exception")
+ @proxy.and_raise(exc)
+ expect(@proxy.raising).to equal(exc)
+ end
+
+ it "returns an instance of RuntimeError when a String is passed to #and_raise" do
+ @proxy.and_raise("an error")
+ exc = @proxy.raising
+ expect(exc).to be_an_instance_of(RuntimeError)
+ expect(exc.message).to eq("an error")
+ end
+end
+
+RSpec.describe MockProxy, "#yielding" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns an empty array by default" do
+ expect(@proxy.yielding).to eq([])
+ end
+
+ it "returns an array of arrays of values the proxy should yield" do
+ @proxy.and_yield(3)
+ expect(@proxy.yielding).to eq([[3]])
+ end
+
+ it "returns an accumulation of arrays of values the proxy should yield" do
+ @proxy.and_yield(1).and_yield(2, 3)
+ expect(@proxy.yielding).to eq([[1], [2, 3]])
+ end
+end
+
+RSpec.describe MockProxy, "#yielding?" do
+ before :each do
+ @proxy = MockProxy.new
+ end
+
+ it "returns false if the proxy is not yielding" do
+ expect(@proxy.yielding?).to be_falsey
+ end
+
+ it "returns true if the proxy is yielding" do
+ @proxy.and_yield(1)
+ expect(@proxy.yielding?).to be_truthy
+ end
+end
+
+RSpec.describe MockIntObject, "#to_int" do
+ before :each do
+ @int = MockIntObject.new(10)
+ end
+
+ it "returns the number if to_int is called" do
+ expect(@int.to_int).to eq(10)
+ expect(@int.count).to eq([:at_least, 1])
+ end
+
+ it "tries to convert the target to int if to_int is called" do
+ expect(MockIntObject.new(@int).to_int).to eq(10)
+ expect(@int.count).to eq([:at_least, 1])
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/filter_spec.rb b/spec/mspec/spec/runner/actions/filter_spec.rb
new file mode 100644
index 0000000000..7582b31c1d
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/filter_spec.rb
@@ -0,0 +1,84 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/filter'
+require 'mspec/runner/mspec'
+require 'mspec/runner/tag'
+
+RSpec.describe ActionFilter do
+ it "creates a filter when not passed a description" do
+ expect(MatchFilter).not_to receive(:new)
+ ActionFilter.new(nil, nil)
+ end
+
+ it "creates a filter from a single description" do
+ expect(MatchFilter).to receive(:new).with(nil, "match me")
+ ActionFilter.new(nil, "match me")
+ end
+
+ it "creates a filter from an array of descriptions" do
+ expect(MatchFilter).to receive(:new).with(nil, "match me", "again")
+ ActionFilter.new(nil, ["match me", "again"])
+ end
+end
+
+RSpec.describe ActionFilter, "#===" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return(["match"])
+ @action = ActionFilter.new(nil, ["catch", "if you"])
+ end
+
+ it "returns false if there are no filters" do
+ action = ActionFilter.new
+ expect(action.===("anything")).to eq(false)
+ end
+
+ it "returns true if the argument matches any of the descriptions" do
+ expect(@action.===("catch")).to eq(true)
+ expect(@action.===("if you can")).to eq(true)
+ end
+
+ it "returns false if the argument does not match any of the descriptions" do
+ expect(@action.===("patch me")).to eq(false)
+ expect(@action.===("if I can")).to eq(false)
+ end
+end
+
+RSpec.describe ActionFilter, "#load" do
+ before :each do
+ @tag = SpecTag.new "tag(comment):description"
+ end
+
+ it "creates a filter from a single tag" do
+ expect(MSpec).to receive(:read_tags).with(["tag"]).and_return([@tag])
+ expect(MatchFilter).to receive(:new).with(nil, "description")
+ ActionFilter.new("tag", nil).load
+ end
+
+ it "creates a filter from an array of tags" do
+ expect(MSpec).to receive(:read_tags).with(["tag", "key"]).and_return([@tag])
+ expect(MatchFilter).to receive(:new).with(nil, "description")
+ ActionFilter.new(["tag", "key"], nil).load
+ end
+
+ it "creates a filter from both tags and descriptions" do
+ expect(MSpec).to receive(:read_tags).and_return([@tag])
+ filter = ActionFilter.new("tag", ["match me", "again"])
+ expect(MatchFilter).to receive(:new).with(nil, "description")
+ filter.load
+ end
+end
+
+RSpec.describe ActionFilter, "#register" do
+ it "registers itself with MSpec for the :load actions" do
+ filter = ActionFilter.new
+ expect(MSpec).to receive(:register).with(:load, filter)
+ filter.register
+ end
+end
+
+RSpec.describe ActionFilter, "#unregister" do
+ it "unregisters itself with MSpec for the :load actions" do
+ filter = ActionFilter.new
+ expect(MSpec).to receive(:unregister).with(:load, filter)
+ filter.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/tag_spec.rb b/spec/mspec/spec/runner/actions/tag_spec.rb
new file mode 100644
index 0000000000..738e9a18c9
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/tag_spec.rb
@@ -0,0 +1,313 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/tag'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/runner/tag'
+
+RSpec.describe TagAction, ".new" do
+ it "creates an MatchFilter with its tag and desc arguments" do
+ filter = double('action filter').as_null_object
+ expect(MatchFilter).to receive(:new).with(nil, "some", "thing").and_return(filter)
+ TagAction.new :add, :all, nil, nil, ["tag", "key"], ["some", "thing"]
+ end
+end
+
+RSpec.describe TagAction, "#===" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return(["match"])
+ @action = TagAction.new :add, :fail, nil, nil, nil, ["catch", "if you"]
+ end
+
+ it "returns true if there are no filters" do
+ action = TagAction.new :add, :all, nil, nil
+ expect(action.===("anything")).to eq(true)
+ end
+
+ it "returns true if the argument matches any of the descriptions" do
+ expect(@action.===("catch")).to eq(true)
+ expect(@action.===("if you can")).to eq(true)
+ end
+
+ it "returns false if the argument does not match any of the descriptions" do
+ expect(@action.===("patch me")).to eq(false)
+ expect(@action.===("if I can")).to eq(false)
+ end
+end
+
+RSpec.describe TagAction, "#exception?" do
+ before :each do
+ @action = TagAction.new :add, :fail, nil, nil, nil, nil
+ end
+
+ it "returns false if no exception has been raised while evaluating an example" do
+ expect(@action.exception?).to be_falsey
+ end
+
+ it "returns true if an exception was raised while evaluating an example" do
+ @action.exception ExceptionState.new nil, nil, Exception.new("failed")
+ expect(@action.exception?).to be_truthy
+ end
+end
+
+RSpec.describe TagAction, "#outcome?" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ @exception = ExceptionState.new nil, nil, Exception.new("failed")
+ end
+
+ it "returns true if outcome is :fail and the spec fails" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ action.exception @exception
+ expect(action.outcome?).to eq(true)
+ end
+
+ it "returns false if the outcome is :fail and the spec passes" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ expect(action.outcome?).to eq(false)
+ end
+
+ it "returns true if the outcome is :pass and the spec passes" do
+ action = TagAction.new :del, :pass, nil, nil, nil, nil
+ expect(action.outcome?).to eq(true)
+ end
+
+ it "returns false if the outcome is :pass and the spec fails" do
+ action = TagAction.new :del, :pass, nil, nil, nil, nil
+ action.exception @exception
+ expect(action.outcome?).to eq(false)
+ end
+
+ it "returns true if the outcome is :all" do
+ action = TagAction.new :add, :all, nil, nil, nil, nil
+ action.exception @exception
+ expect(action.outcome?).to eq(true)
+ end
+end
+
+RSpec.describe TagAction, "#before" do
+ it "resets the #exception? flag to false" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ expect(action.exception?).to be_falsey
+ action.exception ExceptionState.new(nil, nil, Exception.new("Fail!"))
+ expect(action.exception?).to be_truthy
+ action.before(ExampleState.new(ContextState.new("describe"), "it"))
+ expect(action.exception?).to be_falsey
+ end
+end
+
+RSpec.describe TagAction, "#exception" do
+ it "sets the #exception? flag" do
+ action = TagAction.new :add, :fail, nil, nil, nil, nil
+ expect(action.exception?).to be_falsey
+ action.exception ExceptionState.new(nil, nil, Exception.new("Fail!"))
+ expect(action.exception?).to be_truthy
+ end
+end
+
+RSpec.describe TagAction, "#after when action is :add" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ context = ContextState.new "Catch#me"
+ @state = ExampleState.new context, "if you can"
+ @tag = SpecTag.new "tag(comment):Catch#me if you can"
+ allow(SpecTag).to receive(:new).and_return(@tag)
+ @exception = ExceptionState.new nil, nil, Exception.new("failed")
+ end
+
+ it "does not write a tag if the description does not match" do
+ expect(MSpec).not_to receive(:write_tag)
+ action = TagAction.new :add, :all, "tag", "comment", nil, "match"
+ action.after @state
+ end
+
+ it "does not write a tag if outcome is :fail and the spec passed" do
+ expect(MSpec).not_to receive(:write_tag)
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "writes a tag if the outcome is :fail and the spec failed" do
+ expect(MSpec).to receive(:write_tag).with(@tag)
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "does not write a tag if outcome is :pass and the spec failed" do
+ expect(MSpec).not_to receive(:write_tag)
+ action = TagAction.new :add, :pass, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "writes a tag if the outcome is :pass and the spec passed" do
+ expect(MSpec).to receive(:write_tag).with(@tag)
+ action = TagAction.new :add, :pass, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "writes a tag if the outcome is :all" do
+ expect(MSpec).to receive(:write_tag).with(@tag)
+ action = TagAction.new :add, :all, "tag", "comment", nil, "can"
+ action.after @state
+ end
+end
+
+RSpec.describe TagAction, "#after when action is :del" do
+ before :each do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ context = ContextState.new "Catch#me"
+ @state = ExampleState.new context, "if you can"
+ @tag = SpecTag.new "tag(comment):Catch#me if you can"
+ allow(SpecTag).to receive(:new).and_return(@tag)
+ @exception = ExceptionState.new nil, nil, Exception.new("failed")
+ end
+
+ it "does not delete a tag if the description does not match" do
+ expect(MSpec).not_to receive(:delete_tag)
+ action = TagAction.new :del, :all, "tag", "comment", nil, "match"
+ action.after @state
+ end
+
+ it "does not delete a tag if outcome is :fail and the spec passed" do
+ expect(MSpec).not_to receive(:delete_tag)
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "deletes a tag if the outcome is :fail and the spec failed" do
+ expect(MSpec).to receive(:delete_tag).with(@tag)
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "does not delete a tag if outcome is :pass and the spec failed" do
+ expect(MSpec).not_to receive(:delete_tag)
+ action = TagAction.new :del, :pass, "tag", "comment", nil, "can"
+ action.exception @exception
+ action.after @state
+ end
+
+ it "deletes a tag if the outcome is :pass and the spec passed" do
+ expect(MSpec).to receive(:delete_tag).with(@tag)
+ action = TagAction.new :del, :pass, "tag", "comment", nil, "can"
+ action.after @state
+ end
+
+ it "deletes a tag if the outcome is :all" do
+ expect(MSpec).to receive(:delete_tag).with(@tag)
+ action = TagAction.new :del, :all, "tag", "comment", nil, "can"
+ action.after @state
+ end
+end
+
+RSpec.describe TagAction, "#finish" do
+ before :each do
+ $stdout = @out = IOStub.new
+ context = ContextState.new "Catch#me"
+ @state = ExampleState.new context, "if you can"
+ allow(MSpec).to receive(:write_tag).and_return(true)
+ allow(MSpec).to receive(:delete_tag).and_return(true)
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "reports no specs tagged if none where tagged" do
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(false)
+ action.after @state
+ action.finish
+ expect(@out).to eq("\nTagAction: no specs were tagged with 'tag'\n")
+ end
+
+ it "reports no specs tagged if none where tagged" do
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(false)
+ action.after @state
+ action.finish
+ expect(@out).to eq("\nTagAction: no tags 'tag' were deleted\n")
+ end
+
+ it "reports the spec descriptions that were tagged" do
+ action = TagAction.new :add, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(true)
+ action.after @state
+ action.finish
+ expect(@out).to eq(%[
+TagAction: specs tagged with 'tag':
+
+Catch#me if you can
+])
+ end
+
+ it "reports the spec descriptions for the tags that were deleted" do
+ action = TagAction.new :del, :fail, "tag", "comment", nil, "can"
+ allow(action).to receive(:outcome?).and_return(true)
+ action.after @state
+ action.finish
+ expect(@out).to eq(%[
+TagAction: tag 'tag' deleted for specs:
+
+Catch#me if you can
+])
+ end
+end
+
+RSpec.describe TagAction, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ allow(MSpec).to receive(:read_tags).and_return([])
+ @action = TagAction.new :add, :all, nil, nil, nil, nil
+ end
+
+ it "registers itself with MSpec for the :before event" do
+ expect(MSpec).to receive(:register).with(:before, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:register).with(:after, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :exception event" do
+ expect(MSpec).to receive(:register).with(:exception, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :finish event" do
+ expect(MSpec).to receive(:register).with(:finish, @action)
+ @action.register
+ end
+end
+
+RSpec.describe TagAction, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ allow(MSpec).to receive(:read_tags).and_return([])
+ @action = TagAction.new :add, :all, nil, nil, nil, nil
+ end
+
+ it "unregisters itself with MSpec for the :before event" do
+ expect(MSpec).to receive(:unregister).with(:before, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:unregister).with(:after, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :exception event" do
+ expect(MSpec).to receive(:unregister).with(:exception, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :finish event" do
+ expect(MSpec).to receive(:unregister).with(:finish, @action)
+ @action.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/taglist_spec.rb b/spec/mspec/spec/runner/actions/taglist_spec.rb
new file mode 100644
index 0000000000..b6a5400f7d
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/taglist_spec.rb
@@ -0,0 +1,152 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/taglist'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/runner/tag'
+
+RSpec.describe TagListAction, "#include?" do
+ it "returns true" do
+ expect(TagListAction.new.include?(:anything)).to be_truthy
+ end
+end
+
+RSpec.describe TagListAction, "#===" do
+ before :each do
+ tag = SpecTag.new "fails:description"
+ allow(MSpec).to receive(:read_tags).and_return([tag])
+ @filter = double("MatchFilter").as_null_object
+ allow(MatchFilter).to receive(:new).and_return(@filter)
+ @action = TagListAction.new
+ @action.load
+ end
+
+ it "returns true if filter === string returns true" do
+ expect(@filter).to receive(:===).with("str").and_return(true)
+ expect(@action.===("str")).to be_truthy
+ end
+
+ it "returns false if filter === string returns false" do
+ expect(@filter).to receive(:===).with("str").and_return(false)
+ expect(@action.===("str")).to be_falsey
+ end
+end
+
+RSpec.describe TagListAction, "#start" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "prints a banner for specific tags" do
+ action = TagListAction.new ["fails", "unstable"]
+ action.start
+ expect($stdout).to eq("\nListing specs tagged with 'fails', 'unstable'\n\n")
+ end
+
+ it "prints a banner for all tags" do
+ action = TagListAction.new
+ action.start
+ expect($stdout).to eq("\nListing all tagged specs\n\n")
+ end
+end
+
+RSpec.describe TagListAction, "#load" do
+ before :each do
+ @t1 = SpecTag.new "fails:I fail"
+ @t2 = SpecTag.new "unstable:I'm unstable"
+ end
+
+ it "creates a MatchFilter for matching tags" do
+ expect(MSpec).to receive(:read_tags).with(["fails"]).and_return([@t1])
+ expect(MatchFilter).to receive(:new).with(nil, "I fail")
+ TagListAction.new(["fails"]).load
+ end
+
+ it "creates a MatchFilter for all tags" do
+ expect(MSpec).to receive(:read_tags).and_return([@t1, @t2])
+ expect(MatchFilter).to receive(:new).with(nil, "I fail", "I'm unstable")
+ TagListAction.new.load
+ end
+
+ it "does not create a MatchFilter if there are no matching tags" do
+ allow(MSpec).to receive(:read_tags).and_return([])
+ expect(MatchFilter).not_to receive(:new)
+ TagListAction.new(["fails"]).load
+ end
+end
+
+RSpec.describe TagListAction, "#after" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("str")
+
+ @action = TagListAction.new
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "prints nothing if the filter does not match" do
+ expect(@action).to receive(:===).with("str").and_return(false)
+ @action.after(@state)
+ expect($stdout).to eq("")
+ end
+
+ it "prints the example description if the filter matches" do
+ expect(@action).to receive(:===).with("str").and_return(true)
+ @action.after(@state)
+ expect($stdout).to eq("str\n")
+ end
+end
+
+RSpec.describe TagListAction, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ @action = TagListAction.new
+ end
+
+ it "registers itself with MSpec for the :start event" do
+ expect(MSpec).to receive(:register).with(:start, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :load event" do
+ expect(MSpec).to receive(:register).with(:load, @action)
+ @action.register
+ end
+
+ it "registers itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:register).with(:after, @action)
+ @action.register
+ end
+end
+
+RSpec.describe TagListAction, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ @action = TagListAction.new
+ end
+
+ it "unregisters itself with MSpec for the :start event" do
+ expect(MSpec).to receive(:unregister).with(:start, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :load event" do
+ expect(MSpec).to receive(:unregister).with(:load, @action)
+ @action.unregister
+ end
+
+ it "unregisters itself with MSpec for the :after event" do
+ expect(MSpec).to receive(:unregister).with(:after, @action)
+ @action.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/tagpurge_spec.rb b/spec/mspec/spec/runner/actions/tagpurge_spec.rb
new file mode 100644
index 0000000000..37df0afd5a
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/tagpurge_spec.rb
@@ -0,0 +1,154 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/tagpurge'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/runner/tag'
+
+RSpec.describe TagPurgeAction, "#start" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "prints a banner" do
+ action = TagPurgeAction.new
+ action.start
+ expect($stdout).to eq("\nRemoving tags not matching any specs\n\n")
+ end
+end
+
+RSpec.describe TagPurgeAction, "#load" do
+ before :each do
+ @t1 = SpecTag.new "fails:I fail"
+ @t2 = SpecTag.new "unstable:I'm unstable"
+ end
+
+ it "creates a MatchFilter for all tags" do
+ expect(MSpec).to receive(:read_tags).and_return([@t1, @t2])
+ expect(MatchFilter).to receive(:new).with(nil, "I fail", "I'm unstable")
+ TagPurgeAction.new.load
+ end
+end
+
+RSpec.describe TagPurgeAction, "#after" do
+ before :each do
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("str")
+
+ @action = TagPurgeAction.new
+ end
+
+ it "does not save the description if the filter does not match" do
+ expect(@action).to receive(:===).with("str").and_return(false)
+ @action.after @state
+ expect(@action.matching).to eq([])
+ end
+
+ it "saves the description if the filter matches" do
+ expect(@action).to receive(:===).with("str").and_return(true)
+ @action.after @state
+ expect(@action.matching).to eq(["str"])
+ end
+end
+
+RSpec.describe TagPurgeAction, "#unload" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+
+ @t1 = SpecTag.new "fails:I fail"
+ @t2 = SpecTag.new "unstable:I'm unstable"
+ @t3 = SpecTag.new "fails:I'm unstable"
+
+ allow(MSpec).to receive(:read_tags).and_return([@t1, @t2, @t3])
+ allow(MSpec).to receive(:write_tags)
+
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("I'm unstable")
+
+ @action = TagPurgeAction.new
+ @action.load
+ @action.after @state
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "does not rewrite any tags if there were no tags for the specs" do
+ expect(MSpec).to receive(:read_tags).and_return([])
+ expect(MSpec).to receive(:delete_tags)
+ expect(MSpec).not_to receive(:write_tags)
+
+ @action.load
+ @action.after @state
+ @action.unload
+
+ expect($stdout).to eq("")
+ end
+
+ it "rewrites tags that were matched" do
+ expect(MSpec).to receive(:write_tags).with([@t2, @t3])
+ @action.unload
+ end
+
+ it "prints tags that were not matched" do
+ @action.unload
+ expect($stdout).to eq("I fail\n")
+ end
+end
+
+RSpec.describe TagPurgeAction, "#unload" do
+ before :each do
+ @stdout = $stdout
+ $stdout = IOStub.new
+
+ allow(MSpec).to receive(:read_tags).and_return([])
+
+ @state = double("ExampleState")
+ allow(@state).to receive(:description).and_return("I'm unstable")
+
+ @action = TagPurgeAction.new
+ @action.load
+ @action.after @state
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "deletes the tag file if no tags were found" do
+ expect(MSpec).not_to receive(:write_tags)
+ expect(MSpec).to receive(:delete_tags)
+ @action.unload
+ expect($stdout).to eq("")
+ end
+end
+
+RSpec.describe TagPurgeAction, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ @action = TagPurgeAction.new
+ end
+
+ it "registers itself with MSpec for the :unload event" do
+ expect(MSpec).to receive(:register).with(:unload, @action)
+ @action.register
+ end
+end
+
+RSpec.describe TagPurgeAction, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ @action = TagPurgeAction.new
+ end
+
+ it "unregisters itself with MSpec for the :unload event" do
+ expect(MSpec).to receive(:unregister).with(:unload, @action)
+ @action.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/tally_spec.rb b/spec/mspec/spec/runner/actions/tally_spec.rb
new file mode 100644
index 0000000000..d80ab1164a
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/tally_spec.rb
@@ -0,0 +1,355 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/runner/actions/tally'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe Tally, "#files!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #files" do
+ @tally.files! 3
+ expect(@tally.files).to eq(3)
+ @tally.files!
+ expect(@tally.files).to eq(4)
+ end
+end
+
+RSpec.describe Tally, "#examples!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #examples" do
+ @tally.examples! 2
+ expect(@tally.examples).to eq(2)
+ @tally.examples! 2
+ expect(@tally.examples).to eq(4)
+ end
+end
+
+RSpec.describe Tally, "#expectations!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #expectations" do
+ @tally.expectations!
+ expect(@tally.expectations).to eq(1)
+ @tally.expectations! 3
+ expect(@tally.expectations).to eq(4)
+ end
+end
+
+RSpec.describe Tally, "#failures!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #failures" do
+ @tally.failures! 1
+ expect(@tally.failures).to eq(1)
+ @tally.failures!
+ expect(@tally.failures).to eq(2)
+ end
+end
+
+RSpec.describe Tally, "#errors!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #errors" do
+ @tally.errors!
+ expect(@tally.errors).to eq(1)
+ @tally.errors! 2
+ expect(@tally.errors).to eq(3)
+ end
+end
+
+RSpec.describe Tally, "#guards!" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "increments the count returned by #guards" do
+ @tally.guards!
+ expect(@tally.guards).to eq(1)
+ @tally.guards! 2
+ expect(@tally.guards).to eq(3)
+ end
+end
+
+RSpec.describe Tally, "#file" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #files" do
+ expect(@tally.file).to eq("0 files")
+ @tally.files!
+ expect(@tally.file).to eq("1 file")
+ @tally.files!
+ expect(@tally.file).to eq("2 files")
+ end
+end
+
+RSpec.describe Tally, "#example" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #examples" do
+ expect(@tally.example).to eq("0 examples")
+ @tally.examples!
+ expect(@tally.example).to eq("1 example")
+ @tally.examples!
+ expect(@tally.example).to eq("2 examples")
+ end
+end
+
+RSpec.describe Tally, "#expectation" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #expectations" do
+ expect(@tally.expectation).to eq("0 expectations")
+ @tally.expectations!
+ expect(@tally.expectation).to eq("1 expectation")
+ @tally.expectations!
+ expect(@tally.expectation).to eq("2 expectations")
+ end
+end
+
+RSpec.describe Tally, "#failure" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #failures" do
+ expect(@tally.failure).to eq("0 failures")
+ @tally.failures!
+ expect(@tally.failure).to eq("1 failure")
+ @tally.failures!
+ expect(@tally.failure).to eq("2 failures")
+ end
+end
+
+RSpec.describe Tally, "#error" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #errors" do
+ expect(@tally.error).to eq("0 errors")
+ @tally.errors!
+ expect(@tally.error).to eq("1 error")
+ @tally.errors!
+ expect(@tally.error).to eq("2 errors")
+ end
+end
+
+RSpec.describe Tally, "#guard" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ it "returns a formatted string of the number of #guards" do
+ expect(@tally.guard).to eq("0 guards")
+ @tally.guards!
+ expect(@tally.guard).to eq("1 guard")
+ @tally.guards!
+ expect(@tally.guard).to eq("2 guards")
+ end
+end
+
+RSpec.describe Tally, "#format" do
+ before :each do
+ @tally = Tally.new
+ end
+
+ after :each do
+ MSpec.clear_modes
+ end
+
+ it "returns a formatted string of counts" do
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.tagged!
+ expect(@tally.format).to eq("1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged")
+ end
+
+ it "includes guards if MSpec is in verify mode" do
+ MSpec.register_mode :verify
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.tagged!
+ @tally.guards!
+ expect(@tally.format).to eq(
+ "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged, 1 guard"
+ )
+ end
+
+ it "includes guards if MSpec is in report mode" do
+ MSpec.register_mode :report
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.tagged!
+ @tally.guards! 2
+ expect(@tally.format).to eq(
+ "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 1 tagged, 2 guards"
+ )
+ end
+
+ it "includes guards if MSpec is in report_on mode" do
+ MSpec.register_mode :report_on
+ @tally.files!
+ @tally.examples! 2
+ @tally.expectations! 4
+ @tally.errors!
+ @tally.guards! 2
+ expect(@tally.format).to eq(
+ "1 file, 2 examples, 4 expectations, 0 failures, 1 error, 0 tagged, 2 guards"
+ )
+ end
+end
+
+RSpec.describe TallyAction, "#counter" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "returns the Tally object" do
+ expect(@tally.counter).to be_kind_of(Tally)
+ end
+end
+
+RSpec.describe TallyAction, "#load" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments the count returned by Tally#files" do
+ @tally.load
+ expect(@tally.counter.files).to eq(1)
+ end
+end
+
+RSpec.describe TallyAction, "#expectation" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments the count returned by Tally#expectations" do
+ @tally.expectation @state
+ expect(@tally.counter.expectations).to eq(1)
+ end
+end
+
+RSpec.describe TallyAction, "#example" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments counts returned by Tally#examples" do
+ @tally.example @state, nil
+ expect(@tally.counter.examples).to eq(1)
+ expect(@tally.counter.expectations).to eq(0)
+ expect(@tally.counter.failures).to eq(0)
+ expect(@tally.counter.errors).to eq(0)
+ end
+end
+
+RSpec.describe TallyAction, "#exception" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments counts returned by Tally#failures" do
+ exc = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("Failed!")
+ @tally.exception exc
+ expect(@tally.counter.examples).to eq(0)
+ expect(@tally.counter.expectations).to eq(0)
+ expect(@tally.counter.failures).to eq(1)
+ expect(@tally.counter.errors).to eq(0)
+ end
+end
+
+RSpec.describe TallyAction, "#exception" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "increments counts returned by Tally#errors" do
+ exc = ExceptionState.new nil, nil, Exception.new("Error!")
+ @tally.exception exc
+ expect(@tally.counter.examples).to eq(0)
+ expect(@tally.counter.expectations).to eq(0)
+ expect(@tally.counter.failures).to eq(0)
+ expect(@tally.counter.errors).to eq(1)
+ end
+end
+
+RSpec.describe TallyAction, "#format" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "returns a readable string of counts" do
+ @tally.load
+ @tally.example @state, nil
+ @tally.expectation @state
+ @tally.expectation @state
+ exc = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("Failed!")
+ @tally.exception exc
+ expect(@tally.format).to eq("1 file, 1 example, 2 expectations, 1 failure, 0 errors, 0 tagged")
+ end
+end
+
+RSpec.describe TallyAction, "#register" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "registers itself with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:load, @tally)
+ expect(MSpec).to receive(:register).with(:exception, @tally)
+ expect(MSpec).to receive(:register).with(:example, @tally)
+ expect(MSpec).to receive(:register).with(:tagged, @tally)
+ expect(MSpec).to receive(:register).with(:expectation, @tally)
+ @tally.register
+ end
+end
+
+RSpec.describe TallyAction, "#unregister" do
+ before :each do
+ @tally = TallyAction.new
+ @state = ExampleState.new("describe", "it")
+ end
+
+ it "unregisters itself with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:unregister).with(:load, @tally)
+ expect(MSpec).to receive(:unregister).with(:exception, @tally)
+ expect(MSpec).to receive(:unregister).with(:example, @tally)
+ expect(MSpec).to receive(:unregister).with(:tagged, @tally)
+ expect(MSpec).to receive(:unregister).with(:expectation, @tally)
+ @tally.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/actions/timer_spec.rb b/spec/mspec/spec/runner/actions/timer_spec.rb
new file mode 100644
index 0000000000..28a317177b
--- /dev/null
+++ b/spec/mspec/spec/runner/actions/timer_spec.rb
@@ -0,0 +1,44 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/actions/timer'
+require 'mspec/runner/mspec'
+require 'time'
+
+RSpec.describe TimerAction do
+ before :each do
+ @timer = TimerAction.new
+ @start_time = Time.utc(2009, 3, 30, 14, 5, 19)
+ @stop_time = Time.utc(2009, 3, 30, 14, 5, 52)
+ end
+
+ it "responds to #start by recording the current time" do
+ expect(Time).to receive(:now)
+ @timer.start
+ end
+
+ it "responds to #finish by recording the current time" do
+ expect(Time).to receive(:now)
+ @timer.finish
+ end
+
+ it "responds to #elapsed by returning the difference between stop and start" do
+ allow(Time).to receive(:now).and_return(@start_time)
+ @timer.start
+ allow(Time).to receive(:now).and_return(@stop_time)
+ @timer.finish
+ expect(@timer.elapsed).to eq(33)
+ end
+
+ it "responds to #format by returning a readable string of elapsed time" do
+ allow(Time).to receive(:now).and_return(@start_time)
+ @timer.start
+ allow(Time).to receive(:now).and_return(@stop_time)
+ @timer.finish
+ expect(@timer.format).to eq("Finished in 33.000000 seconds")
+ end
+
+ it "responds to #register by registering itself with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:start, @timer)
+ expect(MSpec).to receive(:register).with(:finish, @timer)
+ @timer.register
+ end
+end
diff --git a/spec/mspec/spec/runner/context_spec.rb b/spec/mspec/spec/runner/context_spec.rb
new file mode 100644
index 0000000000..9ebc708c0c
--- /dev/null
+++ b/spec/mspec/spec/runner/context_spec.rb
@@ -0,0 +1,1028 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/matchers/base'
+require 'mspec/runner/mspec'
+require 'mspec/mocks/mock'
+require 'mspec/runner/context'
+require 'mspec/runner/example'
+
+RSpec.describe ContextState, "#describe" do
+ before :each do
+ @state = ContextState.new "C#m"
+ @proc = proc { ScratchPad.record :a }
+ ScratchPad.clear
+ end
+
+ it "evaluates the passed block" do
+ @state.describe(&@proc)
+ expect(ScratchPad.recorded).to eq(:a)
+ end
+
+ it "evaluates the passed block via #protect" do
+ expect(@state).to receive(:protect).with("C#m", @proc, false)
+ @state.describe(&@proc)
+ end
+
+ it "registers #parent as the current MSpec ContextState" do
+ parent = ContextState.new ""
+ @state.parent = parent
+ expect(MSpec).to receive(:register_current).with(parent)
+ @state.describe { }
+ end
+
+ it "registers self with MSpec when #shared? is true" do
+ state = ContextState.new "something shared", :shared => true
+ expect(MSpec).to receive(:register_shared).with(state)
+ state.describe { }
+ end
+end
+
+RSpec.describe ContextState, "#shared?" do
+ it "returns false when the ContextState is not shared" do
+ expect(ContextState.new("").shared?).to be_falsey
+ end
+
+ it "returns true when the ContextState is shared" do
+ expect(ContextState.new("", {:shared => true}).shared?).to be_truthy
+ end
+end
+
+RSpec.describe ContextState, "#to_s" do
+ it "returns a description string for self when passed a Module" do
+ expect(ContextState.new(Object).to_s).to eq("Object")
+ end
+
+ it "returns a description string for self when passed a String" do
+ expect(ContextState.new("SomeClass").to_s).to eq("SomeClass")
+ end
+end
+
+RSpec.describe ContextState, "#description" do
+ before :each do
+ @state = ContextState.new "when empty"
+ @parent = ContextState.new "Toplevel"
+ end
+
+ it "returns a composite description string from self and all parents" do
+ expect(@parent.description).to eq("Toplevel")
+ expect(@state.description).to eq("when empty")
+ @state.parent = @parent
+ expect(@state.description).to eq("Toplevel when empty")
+ end
+end
+
+RSpec.describe ContextState, "#it" do
+ before :each do
+ @state = ContextState.new ""
+ @proc = lambda {|*| }
+
+ @ex = ExampleState.new("", "", &@proc)
+ end
+
+ it "creates an ExampleState instance for the block" do
+ expect(ExampleState).to receive(:new).with(@state, "it", @proc).and_return(@ex)
+ @state.describe(&@proc)
+ @state.it("it", &@proc)
+ end
+
+ it "calls registered :add actions" do
+ expect(ExampleState).to receive(:new).with(@state, "it", @proc).and_return(@ex)
+
+ add_action = double("add")
+ expect(add_action).to receive(:add).with(@ex) { ScratchPad.record :add }
+ MSpec.register :add, add_action
+
+ @state.it("it", &@proc)
+ expect(ScratchPad.recorded).to eq(:add)
+ MSpec.unregister :add, add_action
+ end
+end
+
+RSpec.describe ContextState, "#examples" do
+ before :each do
+ @state = ContextState.new ""
+ end
+
+ it "returns a list of all examples in this ContextState" do
+ @state.it("first") { }
+ @state.it("second") { }
+ expect(@state.examples.size).to eq(2)
+ end
+end
+
+RSpec.describe ContextState, "#before" do
+ before :each do
+ @state = ContextState.new ""
+ @proc = lambda {|*| }
+ end
+
+ it "records the block for :each" do
+ @state.before(:each, &@proc)
+ expect(@state.before(:each)).to eq([@proc])
+ end
+
+ it "records the block for :all" do
+ @state.before(:all, &@proc)
+ expect(@state.before(:all)).to eq([@proc])
+ end
+end
+
+RSpec.describe ContextState, "#after" do
+ before :each do
+ @state = ContextState.new ""
+ @proc = lambda {|*| }
+ end
+
+ it "records the block for :each" do
+ @state.after(:each, &@proc)
+ expect(@state.after(:each)).to eq([@proc])
+ end
+
+ it "records the block for :all" do
+ @state.after(:all, &@proc)
+ expect(@state.after(:all)).to eq([@proc])
+ end
+end
+
+RSpec.describe ContextState, "#pre" do
+ before :each do
+ @a = lambda {|*| }
+ @b = lambda {|*| }
+ @c = lambda {|*| }
+
+ parent = ContextState.new ""
+ parent.before(:each, &@c)
+ parent.before(:all, &@c)
+
+ @state = ContextState.new ""
+ @state.parent = parent
+ end
+
+ it "returns before(:each) actions in the order they were defined" do
+ @state.before(:each, &@a)
+ @state.before(:each, &@b)
+ expect(@state.pre(:each)).to eq([@c, @a, @b])
+ end
+
+ it "returns before(:all) actions in the order they were defined" do
+ @state.before(:all, &@a)
+ @state.before(:all, &@b)
+ expect(@state.pre(:all)).to eq([@c, @a, @b])
+ end
+end
+
+RSpec.describe ContextState, "#post" do
+ before :each do
+ @a = lambda {|*| }
+ @b = lambda {|*| }
+ @c = lambda {|*| }
+
+ parent = ContextState.new ""
+ parent.after(:each, &@c)
+ parent.after(:all, &@c)
+
+ @state = ContextState.new ""
+ @state.parent = parent
+ end
+
+ it "returns after(:each) actions in the reverse order they were defined" do
+ @state.after(:each, &@a)
+ @state.after(:each, &@b)
+ expect(@state.post(:each)).to eq([@b, @a, @c])
+ end
+
+ it "returns after(:all) actions in the reverse order they were defined" do
+ @state.after(:all, &@a)
+ @state.after(:all, &@b)
+ expect(@state.post(:all)).to eq([@b, @a, @c])
+ end
+end
+
+RSpec.describe ContextState, "#protect" do
+ before :each do
+ ScratchPad.record []
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ @c = lambda {|*| raise Exception, "Fail!" }
+ end
+
+ it "returns true and does execute any blocks if check and MSpec.mode?(:pretend) are true" do
+ expect(MSpec).to receive(:mode?).with(:pretend).and_return(true)
+ expect(ContextState.new("").protect("message", [@a, @b])).to be_truthy
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "executes the blocks if MSpec.mode?(:pretend) is false" do
+ expect(MSpec).to receive(:mode?).with(:pretend).and_return(false)
+ ContextState.new("").protect("message", [@a, @b])
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "executes the blocks if check is false" do
+ ContextState.new("").protect("message", [@a, @b], false)
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "returns true if none of the blocks raise an exception" do
+ expect(ContextState.new("").protect("message", [@a, @b])).to be_truthy
+ end
+
+ it "returns false if any of the blocks raise an exception" do
+ expect(ContextState.new("").protect("message", [@a, @c, @b])).to be_falsey
+ end
+end
+
+RSpec.describe ContextState, "#parent=" do
+ before :each do
+ @state = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "does not set self as a child of parent if shared" do
+ expect(@parent).not_to receive(:child)
+ state = ContextState.new "", :shared => true
+ state.parent = @parent
+ end
+
+ it "does not set parents if shared" do
+ state = ContextState.new "", :shared => true
+ state.parent = @parent
+ expect(state.parents).to eq([state])
+ end
+
+ it "sets self as a child of parent" do
+ expect(@parent).to receive(:child).with(@state)
+ @state.parent = @parent
+ end
+
+ it "creates the list of parents" do
+ @state.parent = @parent
+ expect(@state.parents).to eq([@parent, @state])
+ end
+end
+
+RSpec.describe ContextState, "#parent" do
+ before :each do
+ @state = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "returns nil if parent has not been set" do
+ expect(@state.parent).to be_nil
+ end
+
+ it "returns the parent" do
+ @state.parent = @parent
+ expect(@state.parent).to eq(@parent)
+ end
+end
+
+RSpec.describe ContextState, "#parents" do
+ before :each do
+ @first = ContextState.new ""
+ @second = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "returns a list of all enclosing ContextState instances" do
+ @first.parent = @parent
+ @second.parent = @first
+ expect(@second.parents).to eq([@parent, @first, @second])
+ end
+end
+
+RSpec.describe ContextState, "#child" do
+ before :each do
+ @first = ContextState.new ""
+ @second = ContextState.new ""
+ @parent = double("describe")
+ allow(@parent).to receive(:parent).and_return(nil)
+ allow(@parent).to receive(:child)
+ end
+
+ it "adds the ContextState to the list of contained ContextStates" do
+ @first.child @second
+ expect(@first.children).to eq([@second])
+ end
+end
+
+RSpec.describe ContextState, "#children" do
+ before :each do
+ @parent = ContextState.new ""
+ @first = ContextState.new ""
+ @second = ContextState.new ""
+ end
+
+ it "returns the list of directly contained ContextStates" do
+ @first.parent = @parent
+ @second.parent = @first
+ expect(@parent.children).to eq([@first])
+ expect(@first.children).to eq([@second])
+ end
+end
+
+RSpec.describe ContextState, "#state" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ end
+
+ it "returns nil if no spec is being executed" do
+ expect(@state.state).to eq(nil)
+ end
+
+ it "returns a ExampleState instance if an example is being executed" do
+ ScratchPad.record @state
+ @state.describe { }
+ @state.it("") { ScratchPad.record ScratchPad.recorded.state }
+ @state.process
+ expect(@state.state).to eq(nil)
+ expect(ScratchPad.recorded).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+ allow(MSpec).to receive(:register_current)
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+ end
+
+ it "calls each before(:all) block" do
+ @state.before(:all, &@a)
+ @state.before(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "calls each after(:all) block" do
+ @state.after(:all, &@a)
+ @state.after(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:b, :a])
+ end
+
+ it "calls each it block" do
+ @state.it("one", &@a)
+ @state.it("two", &@b)
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "does not call the #it block if #filtered? returns true" do
+ @state.it("one", &@a)
+ @state.it("two", &@b)
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ @state.process
+ expect(ScratchPad.recorded).to eq([:b])
+ end
+
+ it "calls each before(:each) block" do
+ @state.before(:each, &@a)
+ @state.before(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a, :b])
+ end
+
+ it "calls each after(:each) block" do
+ @state.after(:each, &@a)
+ @state.after(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:b, :a])
+ end
+
+ it "calls Mock.cleanup for each it block" do
+ @state.it("") { }
+ @state.it("") { }
+ expect(Mock).to receive(:cleanup).twice
+ @state.process
+ end
+
+ it "calls Mock.verify_count for each it block" do
+ @state.it("") { }
+ @state.it("") { }
+ expect(Mock).to receive(:verify_count).twice
+ @state.process
+ end
+
+ it "calls the describe block" do
+ ScratchPad.record []
+ @state.describe { ScratchPad << :a }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a])
+ end
+
+ it "creates a new ExampleState instance for each example" do
+ ScratchPad.record @state
+ @state.describe { }
+ @state.it("it") { ScratchPad.record ScratchPad.recorded.state }
+ @state.process
+ expect(ScratchPad.recorded).to be_kind_of(ExampleState)
+ end
+
+ it "clears the expectations flag before evaluating the #it block" do
+ MSpec.clear_expectations
+ expect(MSpec).to receive(:clear_expectations)
+ @state.it("it") { ScratchPad.record MSpec.expectation? }
+ @state.process
+ expect(ScratchPad.recorded).to be_falsey
+ end
+
+ it "shuffles the spec list if MSpec.randomize? is true" do
+ MSpec.randomize = true
+ begin
+ expect(MSpec).to receive(:shuffle)
+ @state.it("") { }
+ @state.process
+ ensure
+ MSpec.randomize = false
+ end
+ end
+
+ it "sets the current MSpec ContextState" do
+ expect(MSpec).to receive(:register_current).with(@state)
+ @state.process
+ end
+
+ it "resets the current MSpec ContextState to nil when there are examples" do
+ expect(MSpec).to receive(:register_current).with(nil)
+ @state.it("") { }
+ @state.process
+ end
+
+ it "resets the current MSpec ContextState to nil when there are no examples" do
+ expect(MSpec).to receive(:register_current).with(nil)
+ @state.process
+ end
+
+ it "call #process on children when there are examples" do
+ child = ContextState.new ""
+ expect(child).to receive(:process)
+ @state.child child
+ @state.it("") { }
+ @state.process
+ end
+
+ it "call #process on children when there are no examples" do
+ child = ContextState.new ""
+ expect(child).to receive(:process)
+ @state.child child
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :exception, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ action = double("action")
+ def action.exception(exc)
+ ScratchPad.record :exception if exc.exception.is_a? SpecExpectationNotFoundError
+ end
+ MSpec.register :exception, action
+
+ MSpec.clear_expectations
+ ScratchPad.clear
+ end
+
+ after :each do
+ MSpec.store :exception, nil
+ end
+
+ it "raises an SpecExpectationNotFoundError if an #it block does not contain an expectation" do
+ @state.it("it") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq(:exception)
+ end
+
+ it "does not raise an SpecExpectationNotFoundError if an #it block does contain an expectation" do
+ @state.it("it") { MSpec.expectation }
+ @state.process
+ expect(ScratchPad.recorded).to be_nil
+ end
+
+ it "does not raise an SpecExpectationNotFoundError if the #it block causes a failure" do
+ @state.it("it") { raise Exception, "Failed!" }
+ @state.process
+ expect(ScratchPad.recorded).to be_nil
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :example, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ example = double("example")
+ def example.example(state, spec)
+ ScratchPad << state << spec
+ end
+ MSpec.register :example, example
+
+ ScratchPad.record []
+ end
+
+ after :each do
+ MSpec.store :example, nil
+ end
+
+ it "calls registered :example actions with the current ExampleState and block" do
+ @state.it("") { MSpec.expectation }
+ @state.process
+
+ expect(ScratchPad.recorded.first).to be_kind_of(ExampleState)
+ expect(ScratchPad.recorded.last).to be_kind_of(Proc)
+ end
+
+ it "does not call registered example actions if the example has no block" do
+ @state.it("empty example")
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+ @state.it("") { MSpec.expectation }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "calls registered :before actions with the current ExampleState instance" do
+ before = double("before")
+ expect(before).to receive(:before) {
+ ScratchPad.record :before
+ @spec_state = @state.state
+ }
+ MSpec.register :before, before
+ @state.process
+ expect(ScratchPad.recorded).to eq(:before)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+
+ it "calls registered :after actions with the current ExampleState instance" do
+ after = double("after")
+ expect(after).to receive(:after) {
+ ScratchPad.record :after
+ @spec_state = @state.state
+ }
+ MSpec.register :after, after
+ @state.process
+ expect(ScratchPad.recorded).to eq(:after)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ContextState, "#process" do
+ before :each do
+ MSpec.store :enter, []
+ MSpec.store :leave, []
+
+ @state = ContextState.new "C#m"
+ @state.describe { }
+ @state.it("") { MSpec.expectation }
+ end
+
+ after :each do
+ MSpec.store :enter, nil
+ MSpec.store :leave, nil
+ end
+
+ it "calls registered :enter actions with the current #describe string" do
+ enter = double("enter")
+ expect(enter).to receive(:enter).with("C#m") { ScratchPad.record :enter }
+ MSpec.register :enter, enter
+ @state.process
+ expect(ScratchPad.recorded).to eq(:enter)
+ end
+
+ it "calls registered :leave actions" do
+ leave = double("leave")
+ expect(leave).to receive(:leave) { ScratchPad.record :leave }
+ MSpec.register :leave, leave
+ @state.process
+ expect(ScratchPad.recorded).to eq(:leave)
+ end
+end
+
+RSpec.describe ContextState, "#process when an exception is raised in before(:all)" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+
+ @state.before(:all) { raise Exception, "Fail!" }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "does not call before(:each)" do
+ @state.before(:each, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call the it block" do
+ @state.it("one", &@a)
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call after(:each)" do
+ @state.after(:each, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call after(:each)" do
+ @state.after(:all, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call Mock.verify_count" do
+ @state.it("") { }
+ expect(Mock).not_to receive(:verify_count)
+ @state.process
+ end
+
+ it "calls Mock.cleanup" do
+ @state.it("") { }
+ expect(Mock).to receive(:cleanup)
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process when an exception is raised in before(:each)" do
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+
+ @state.before(:each) { raise Exception, "Fail!" }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "does not call the it block" do
+ @state.it("one", &@a)
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "calls after(:each)" do
+ @state.after(:each, &@a)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a])
+ end
+
+ it "calls Mock.verify_count" do
+ @state.it("") { }
+ expect(Mock).to receive(:verify_count)
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process in pretend mode" do
+ before :all do
+ MSpec.register_mode :pretend
+ end
+
+ after :all do
+ MSpec.clear_modes
+ end
+
+ before :each do
+ ScratchPad.clear
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+ @state.it("") { }
+ end
+
+ after :each do
+ MSpec.store :before, nil
+ MSpec.store :after, nil
+ end
+
+ it "calls registered :before actions with the current ExampleState instance" do
+ before = double("before")
+ expect(before).to receive(:before) {
+ ScratchPad.record :before
+ @spec_state = @state.state
+ }
+ MSpec.register :before, before
+ @state.process
+ expect(ScratchPad.recorded).to eq(:before)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+
+ it "calls registered :after actions with the current ExampleState instance" do
+ after = double("after")
+ expect(after).to receive(:after) {
+ ScratchPad.record :after
+ @spec_state = @state.state
+ }
+ MSpec.register :after, after
+ @state.process
+ expect(ScratchPad.recorded).to eq(:after)
+ expect(@spec_state).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ContextState, "#process in pretend mode" do
+ before :all do
+ MSpec.register_mode :pretend
+ end
+
+ after :all do
+ MSpec.clear_modes
+ end
+
+ before :each do
+ MSpec.store :before, []
+ MSpec.store :after, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+
+ @a = lambda {|*| ScratchPad << :a }
+ @b = lambda {|*| ScratchPad << :b }
+ ScratchPad.record []
+ end
+
+ it "calls the describe block" do
+ ScratchPad.record []
+ @state.describe { ScratchPad << :a }
+ @state.process
+ expect(ScratchPad.recorded).to eq([:a])
+ end
+
+ it "does not call any before(:all) block" do
+ @state.before(:all, &@a)
+ @state.before(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any after(:all) block" do
+ @state.after(:all, &@a)
+ @state.after(:all, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any it block" do
+ @state.it("one", &@a)
+ @state.it("two", &@b)
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any before(:each) block" do
+ @state.before(:each, &@a)
+ @state.before(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call any after(:each) block" do
+ @state.after(:each, &@a)
+ @state.after(:each, &@b)
+ @state.it("") { }
+ @state.process
+ expect(ScratchPad.recorded).to eq([])
+ end
+
+ it "does not call Mock.cleanup" do
+ @state.it("") { }
+ @state.it("") { }
+ expect(Mock).not_to receive(:cleanup)
+ @state.process
+ end
+end
+
+RSpec.describe ContextState, "#process in pretend mode" do
+ before :all do
+ MSpec.register_mode :pretend
+ end
+
+ after :all do
+ MSpec.clear_modes
+ end
+
+ before :each do
+ MSpec.store :enter, []
+ MSpec.store :leave, []
+
+ @state = ContextState.new ""
+ @state.describe { }
+ @state.it("") { }
+ end
+
+ after :each do
+ MSpec.store :enter, nil
+ MSpec.store :leave, nil
+ end
+
+ it "calls registered :enter actions with the current #describe string" do
+ enter = double("enter")
+ expect(enter).to receive(:enter) { ScratchPad.record :enter }
+ MSpec.register :enter, enter
+ @state.process
+ expect(ScratchPad.recorded).to eq(:enter)
+ end
+
+ it "calls registered :leave actions" do
+ leave = double("leave")
+ expect(leave).to receive(:leave) { ScratchPad.record :leave }
+ MSpec.register :leave, leave
+ @state.process
+ expect(ScratchPad.recorded).to eq(:leave)
+ end
+end
+
+RSpec.describe ContextState, "#it_should_behave_like" do
+ before :each do
+ @shared_desc = :shared_context
+ @shared = ContextState.new(@shared_desc, :shared => true)
+ allow(MSpec).to receive(:retrieve_shared).and_return(@shared)
+
+ @state = ContextState.new "Top level"
+ @a = lambda {|*| }
+ @b = lambda {|*| }
+ end
+
+ it "raises an Exception if unable to find the shared ContextState" do
+ expect(MSpec).to receive(:retrieve_shared).and_return(nil)
+ expect { @state.it_should_behave_like :this }.to raise_error(Exception)
+ end
+
+ describe "for nested ContextState instances" do
+ before :each do
+ @nested = ContextState.new "nested context"
+ @nested.parents.unshift @shared
+
+ @shared.children << @nested
+
+ @nested_dup = @nested.dup
+ allow(@nested).to receive(:dup).and_return(@nested_dup)
+ end
+
+ it "duplicates the nested ContextState" do
+ @state.it_should_behave_like @shared_desc
+ expect(@state.children.first).to equal(@nested_dup)
+ end
+
+ it "sets the parent of the nested ContextState to the containing ContextState" do
+ @state.it_should_behave_like @shared_desc
+ expect(@nested_dup.parent).to equal(@state)
+ end
+
+ it "sets the context for nested examples to the nested ContextState's dup" do
+ @shared.it "an example", &@a
+ @shared.it "another example", &@b
+ @state.it_should_behave_like @shared_desc
+ @nested_dup.examples.each { |x| expect(x.context).to equal(@nested_dup) }
+ end
+
+ it "omits the shored ContextState's description" do
+ @nested.it "an example", &@a
+ @nested.it "another example", &@b
+ @state.it_should_behave_like @shared_desc
+
+ expect(@nested_dup.description).to eq("Top level nested context")
+ expect(@nested_dup.examples.first.description).to eq("Top level nested context an example")
+ expect(@nested_dup.examples.last.description).to eq("Top level nested context another example")
+ end
+ end
+
+ it "adds duped examples from the shared ContextState" do
+ @shared.it "some method", &@a
+ ex_dup = @shared.examples.first.dup
+ allow(@shared.examples.first).to receive(:dup).and_return(ex_dup)
+
+ @state.it_should_behave_like @shared_desc
+ expect(@state.examples).to eq([ex_dup])
+ end
+
+ it "sets the context for examples to the containing ContextState" do
+ @shared.it "an example", &@a
+ @shared.it "another example", &@b
+ @state.it_should_behave_like @shared_desc
+ @state.examples.each { |x| expect(x.context).to equal(@state) }
+ end
+
+ it "adds before(:all) blocks from the shared ContextState" do
+ @shared.before :all, &@a
+ @shared.before :all, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.before(:all)).to include(*@shared.before(:all))
+ end
+
+ it "adds before(:each) blocks from the shared ContextState" do
+ @shared.before :each, &@a
+ @shared.before :each, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.before(:each)).to include(*@shared.before(:each))
+ end
+
+ it "adds after(:each) blocks from the shared ContextState" do
+ @shared.after :each, &@a
+ @shared.after :each, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.after(:each)).to include(*@shared.after(:each))
+ end
+
+ it "adds after(:all) blocks from the shared ContextState" do
+ @shared.after :all, &@a
+ @shared.after :all, &@b
+ @state.it_should_behave_like @shared_desc
+ expect(@state.after(:all)).to include(*@shared.after(:all))
+ end
+end
+
+RSpec.describe ContextState, "#filter_examples" do
+ before :each do
+ @state = ContextState.new ""
+ @state.it("one") { }
+ @state.it("two") { }
+ end
+
+ it "removes examples that are filtered" do
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ expect(@state.examples.size).to eq(2)
+ @state.filter_examples
+ expect(@state.examples.size).to eq(1)
+ end
+
+ it "returns true if there are remaining examples to evaluate" do
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ expect(@state.filter_examples).to be_truthy
+ end
+
+ it "returns false if there are no remaining examples to evaluate" do
+ allow(@state.examples.first).to receive(:filtered?).and_return(true)
+ allow(@state.examples.last).to receive(:filtered?).and_return(true)
+ expect(@state.filter_examples).to be_falsey
+ end
+end
diff --git a/spec/mspec/spec/runner/example_spec.rb b/spec/mspec/spec/runner/example_spec.rb
new file mode 100644
index 0000000000..8bac166da8
--- /dev/null
+++ b/spec/mspec/spec/runner/example_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+require 'mspec/matchers/base'
+require 'mspec/runner/mspec'
+require 'mspec/mocks/mock'
+require 'mspec/runner/example'
+
+RSpec.describe ExampleState do
+ it "is initialized with the ContextState, #it string, and #it block" do
+ prc = lambda { }
+ context = ContextState.new ""
+ expect(ExampleState.new(context, "does", prc)).to be_kind_of(ExampleState)
+ end
+end
+
+RSpec.describe ExampleState, "#describe" do
+ before :each do
+ @context = ContextState.new "Object#to_s"
+ @state = ExampleState.new @context, "it"
+ end
+
+ it "returns the ContextState#description" do
+ expect(@state.describe).to eq(@context.description)
+ end
+end
+
+RSpec.describe ExampleState, "#it" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ it "returns the argument to the #it block" do
+ expect(@state.it).to eq("it")
+ end
+end
+
+RSpec.describe ExampleState, "#context=" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @context = ContextState.new "New#context"
+ end
+
+ it "sets the containing ContextState" do
+ @state.context = @context
+ expect(@state.context).to eq(@context)
+ end
+
+ it "resets the description" do
+ expect(@state.description).to eq("describe it")
+ @state.context = @context
+ expect(@state.description).to eq("New#context it")
+ end
+end
+
+RSpec.describe ExampleState, "#example" do
+ before :each do
+ @proc = lambda { }
+ @state = ExampleState.new ContextState.new("describe"), "it", @proc
+ end
+
+ it "returns the #it block" do
+ expect(@state.example).to eq(@proc)
+ end
+end
+
+RSpec.describe ExampleState, "#filtered?" do
+ before :each do
+ MSpec.store :include, []
+ MSpec.store :exclude, []
+
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @filter = double("filter")
+ end
+
+ after :each do
+ MSpec.store :include, []
+ MSpec.store :exclude, []
+ end
+
+ it "returns false if MSpec include filters list is empty" do
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns false if MSpec include filters match this spec" do
+ expect(@filter).to receive(:===).and_return(true)
+ MSpec.register :include, @filter
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns true if MSpec include filters do not match this spec" do
+ expect(@filter).to receive(:===).and_return(false)
+ MSpec.register :include, @filter
+ expect(@state.filtered?).to eq(true)
+ end
+
+ it "returns false if MSpec exclude filters list is empty" do
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns false if MSpec exclude filters do not match this spec" do
+ expect(@filter).to receive(:===).and_return(false)
+ MSpec.register :exclude, @filter
+ expect(@state.filtered?).to eq(false)
+ end
+
+ it "returns true if MSpec exclude filters match this spec" do
+ expect(@filter).to receive(:===).and_return(true)
+ MSpec.register :exclude, @filter
+ expect(@state.filtered?).to eq(true)
+ end
+
+ it "returns true if MSpec include and exclude filters match this spec" do
+ expect(@filter).to receive(:===).twice.and_return(true)
+ MSpec.register :include, @filter
+ MSpec.register :exclude, @filter
+ expect(@state.filtered?).to eq(true)
+ end
+end
diff --git a/spec/mspec/spec/runner/exception_spec.rb b/spec/mspec/spec/runner/exception_spec.rb
new file mode 100644
index 0000000000..a77a2c9cf4
--- /dev/null
+++ b/spec/mspec/spec/runner/exception_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/runner/example'
+require 'mspec/runner/exception'
+require 'mspec/utils/script'
+
+RSpec.describe ExceptionState, "#initialize" do
+ it "takes a state, location (e.g. before :each), and exception" do
+ context = ContextState.new "Class#method"
+ state = ExampleState.new context, "does something"
+ exc = Exception.new "Fail!"
+ expect(ExceptionState.new(state, "location", exc)).to be_kind_of(ExceptionState)
+ end
+end
+
+RSpec.describe ExceptionState, "#description" do
+ before :each do
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new context, "does something"
+ end
+
+ it "returns the state description if state was not nil" do
+ exc = ExceptionState.new(@state, nil, nil)
+ expect(exc.description).to eq("Class#method does something")
+ end
+
+ it "returns the location if it is not nil and description is nil" do
+ exc = ExceptionState.new(nil, "location", nil)
+ expect(exc.description).to eq("An exception occurred during: location")
+ end
+
+ it "returns both description and location if neither are nil" do
+ exc = ExceptionState.new(@state, "location", nil)
+ expect(exc.description).to eq("An exception occurred during: location\nClass#method does something")
+ end
+end
+
+RSpec.describe ExceptionState, "#describe" do
+ before :each do
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new context, "does something"
+ end
+
+ it "returns the ExampleState#describe string if created with a non-nil state" do
+ expect(ExceptionState.new(@state, nil, nil).describe).to eq(@state.describe)
+ end
+
+ it "returns an empty string if created with a nil state" do
+ expect(ExceptionState.new(nil, nil, nil).describe).to eq("")
+ end
+end
+
+RSpec.describe ExceptionState, "#it" do
+ before :each do
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new context, "does something"
+ end
+
+ it "returns the ExampleState#it string if created with a non-nil state" do
+ expect(ExceptionState.new(@state, nil, nil).it).to eq(@state.it)
+ end
+
+ it "returns an empty string if created with a nil state" do
+ expect(ExceptionState.new(nil, nil, nil).it).to eq("")
+ end
+end
+
+RSpec.describe ExceptionState, "#failure?" do
+ before :each do
+ @state = ExampleState.new ContextState.new("C#m"), "works"
+ end
+
+ it "returns true if the exception is an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, "", SpecExpectationNotMetError.new("Fail!")
+ expect(exc.failure?).to be_truthy
+ end
+
+ it "returns true if the exception is an SpecExpectationNotFoundError" do
+ exc = ExceptionState.new @state, "", SpecExpectationNotFoundError.new("Fail!")
+ expect(exc.failure?).to be_truthy
+ end
+
+ it "returns false if the exception is not an SpecExpectationNotMetError or an SpecExpectationNotFoundError" do
+ exc = ExceptionState.new @state, "", Exception.new("Fail!")
+ expect(exc.failure?).to be_falsey
+ end
+end
+
+RSpec.describe ExceptionState, "#message" do
+ before :each do
+ @state = ExampleState.new ContextState.new("C#m"), "works"
+ end
+
+ it "returns <No message> if the exception message is empty" do
+ exc = ExceptionState.new @state, "", Exception.new("")
+ expect(exc.message).to eq("Exception: <No message>")
+ end
+
+ it "returns the message without exception class when the exception is an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, "", SpecExpectationNotMetError.new("Fail!")
+ expect(exc.message).to eq("Fail!")
+ end
+
+ it "returns SpecExpectationNotFoundError#message when the exception is an SpecExpectationNotFoundError" do
+ e = SpecExpectationNotFoundError.new
+ exc = ExceptionState.new @state, "", e
+ expect(exc.message).to eq(e.message)
+ end
+
+ it "returns the message with exception class when the exception is not an SpecExpectationNotMetError or an SpecExpectationNotFoundError" do
+ exc = ExceptionState.new @state, "", Exception.new("Fail!")
+ expect(exc.message).to eq("Exception: Fail!")
+ end
+end
+
+RSpec.describe ExceptionState, "#backtrace" do
+ before :each do
+ @state = ExampleState.new ContextState.new("C#m"), "works"
+ begin
+ raise Exception
+ rescue Exception => @exception
+ @exc = ExceptionState.new @state, "", @exception
+ end
+ end
+
+ after :each do
+ $MSPEC_DEBUG = nil
+ end
+
+ it "returns a string representation of the exception backtrace" do
+ expect(@exc.backtrace).to be_kind_of(String)
+ end
+
+ it "does not filter files from the backtrace if $MSPEC_DEBUG is true" do
+ $MSPEC_DEBUG = true
+ expect(@exc.backtrace).to eq(@exception.backtrace.join("\n"))
+ end
+
+ it "filters files matching config[:backtrace_filter]" do
+ MSpecScript.set :backtrace_filter, %r[mspec/lib]
+ $MSPEC_DEBUG = nil
+ @exc.backtrace.split("\n").each do |line|
+ expect(line).not_to match(%r[mspec/lib])
+ end
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/a.yaml b/spec/mspec/spec/runner/filters/a.yaml
new file mode 100644
index 0000000000..1940e3cba6
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/a.yaml
@@ -0,0 +1,4 @@
+---
+A#:
+- a
+- aa
diff --git a/spec/mspec/spec/runner/filters/b.yaml b/spec/mspec/spec/runner/filters/b.yaml
new file mode 100644
index 0000000000..a24bdb2f19
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/b.yaml
@@ -0,0 +1,11 @@
+---
+B.:
+- b
+- bb
+B::C#:
+- b!
+- b=
+- b?
+- "-"
+- "[]"
+- "[]="
diff --git a/spec/mspec/spec/runner/filters/match_spec.rb b/spec/mspec/spec/runner/filters/match_spec.rb
new file mode 100644
index 0000000000..970da00446
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/match_spec.rb
@@ -0,0 +1,34 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/match'
+
+RSpec.describe MatchFilter, "#===" do
+ before :each do
+ @filter = MatchFilter.new nil, 'a', 'b', 'c'
+ end
+
+ it "returns true if the argument matches any of the #initialize strings" do
+ expect(@filter.===('aaa')).to eq(true)
+ expect(@filter.===('bccb')).to eq(true)
+ end
+
+ it "returns false if the argument matches none of the #initialize strings" do
+ expect(@filter.===('d')).to eq(false)
+ end
+end
+
+RSpec.describe MatchFilter, "#register" do
+ it "registers itself with MSpec for the designated action list" do
+ filter = MatchFilter.new :include
+ expect(MSpec).to receive(:register).with(:include, filter)
+ filter.register
+ end
+end
+
+RSpec.describe MatchFilter, "#unregister" do
+ it "unregisters itself with MSpec for the designated action list" do
+ filter = MatchFilter.new :exclude
+ expect(MSpec).to receive(:unregister).with(:exclude, filter)
+ filter.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/profile_spec.rb b/spec/mspec/spec/runner/filters/profile_spec.rb
new file mode 100644
index 0000000000..25f5e07aef
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/profile_spec.rb
@@ -0,0 +1,117 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/profile'
+
+RSpec.describe ProfileFilter, "#find" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ allow(File).to receive(:exist?).and_return(false)
+ @file = "rails.yaml"
+ end
+
+ it "attempts to locate the file through the expanded path name" do
+ expect(File).to receive(:expand_path).with(@file).and_return(@file)
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(@filter.find(@file)).to eq(@file)
+ end
+
+ it "attempts to locate the file in 'spec/profiles'" do
+ path = File.join "spec/profiles", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+
+ it "attempts to locate the file in 'spec'" do
+ path = File.join "spec", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+
+ it "attempts to locate the file in 'profiles'" do
+ path = File.join "profiles", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+
+ it "attempts to locate the file in '.'" do
+ path = File.join ".", @file
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(@filter.find(@file)).to eq(path)
+ end
+end
+
+RSpec.describe ProfileFilter, "#parse" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ @file = File.open(File.dirname(__FILE__) + "/b.yaml", "r")
+ end
+
+ after :each do
+ @file.close
+ end
+
+ it "creates a Hash of the contents of the YAML file" do
+ expect(@filter.parse(@file)).to eq({
+ "B." => ["b", "bb"],
+ "B::C#" => ["b!", "b=", "b?", "-", "[]", "[]="]
+ })
+ end
+end
+
+RSpec.describe ProfileFilter, "#load" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ @files = [
+ File.dirname(__FILE__) + "/a.yaml",
+ File.dirname(__FILE__) + "/b.yaml"
+ ]
+ end
+
+ it "generates a composite hash from multiple YAML files" do
+ expect(@filter.load(*@files)).to eq({
+ "A#" => ["a", "aa"],
+ "B." => ["b", "bb"],
+ "B::C#" => ["b!", "b=", "b?", "-", "[]", "[]="]
+ })
+ end
+end
+
+RSpec.describe ProfileFilter, "#===" do
+ before :each do
+ @filter = ProfileFilter.new nil
+ allow(@filter).to receive(:load).and_return({ "A#" => ["[]=", "a", "a!", "a?", "aa="]})
+ @filter.send :initialize, nil
+ end
+
+ it "returns true if the spec description is for a method in the profile" do
+ expect(@filter.===("The A#[]= method")).to eq(true)
+ expect(@filter.===("A#a returns")).to eq(true)
+ expect(@filter.===("A#a! replaces")).to eq(true)
+ expect(@filter.===("A#a? returns")).to eq(true)
+ expect(@filter.===("A#aa= raises")).to eq(true)
+ end
+
+ it "returns false if the spec description is for a method not in the profile" do
+ expect(@filter.===("The A#[] method")).to eq(false)
+ expect(@filter.===("B#a returns")).to eq(false)
+ expect(@filter.===("A.a! replaces")).to eq(false)
+ expect(@filter.===("AA#a? returns")).to eq(false)
+ expect(@filter.===("A#aa raises")).to eq(false)
+ end
+end
+
+RSpec.describe ProfileFilter, "#register" do
+ it "registers itself with MSpec for the designated action list" do
+ filter = ProfileFilter.new :include
+ expect(MSpec).to receive(:register).with(:include, filter)
+ filter.register
+ end
+end
+
+RSpec.describe ProfileFilter, "#unregister" do
+ it "unregisters itself with MSpec for the designated action list" do
+ filter = ProfileFilter.new :exclude
+ expect(MSpec).to receive(:unregister).with(:exclude, filter)
+ filter.unregister
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/regexp_spec.rb b/spec/mspec/spec/runner/filters/regexp_spec.rb
new file mode 100644
index 0000000000..1d1d3554f6
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/regexp_spec.rb
@@ -0,0 +1,31 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/regexp'
+
+RSpec.describe MatchFilter, "#===" do
+ before :each do
+ @filter = RegexpFilter.new nil, 'a(b|c)', 'b[^ab]', 'cc?'
+ end
+
+ it "returns true if the argument matches any of the #initialize strings" do
+ expect(@filter.===('ab')).to eq(true)
+ expect(@filter.===('bc suffix')).to eq(true)
+ expect(@filter.===('prefix cc')).to eq(true)
+ end
+
+ it "returns false if the argument matches none of the #initialize strings" do
+ expect(@filter.===('aa')).to eq(false)
+ expect(@filter.===('ba')).to eq(false)
+ expect(@filter.===('prefix d suffix')).to eq(false)
+ end
+end
+
+RSpec.describe RegexpFilter, "#to_regexp" do
+ before :each do
+ @filter = RegexpFilter.new nil
+ end
+
+ it "converts its arguments to Regexp instances" do
+ expect(@filter.send(:to_regexp, 'a(b|c)', 'b[^ab]', 'cc?')).to eq([/a(b|c)/, /b[^ab]/, /cc?/])
+ end
+end
diff --git a/spec/mspec/spec/runner/filters/tag_spec.rb b/spec/mspec/spec/runner/filters/tag_spec.rb
new file mode 100644
index 0000000000..356175a754
--- /dev/null
+++ b/spec/mspec/spec/runner/filters/tag_spec.rb
@@ -0,0 +1,92 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters/match'
+require 'mspec/runner/filters/tag'
+
+RSpec.describe TagFilter, "#load" do
+ before :each do
+ @match = double("match filter").as_null_object
+ @filter = TagFilter.new :include, "tag", "key"
+ @tag = SpecTag.new "tag(comment):description"
+ allow(MSpec).to receive(:read_tags).and_return([@tag])
+ allow(MSpec).to receive(:register)
+ end
+
+ it "loads tags from the tag file" do
+ expect(MSpec).to receive(:read_tags).with(["tag", "key"]).and_return([])
+ @filter.load
+ end
+
+
+ it "registers itself with MSpec for the :include action" do
+ filter = TagFilter.new(:include)
+ expect(MSpec).to receive(:register).with(:include, filter)
+ filter.load
+ end
+
+ it "registers itself with MSpec for the :exclude action" do
+ filter = TagFilter.new(:exclude)
+ expect(MSpec).to receive(:register).with(:exclude, filter)
+ filter.load
+ end
+end
+
+RSpec.describe TagFilter, "#unload" do
+ before :each do
+ @filter = TagFilter.new :include, "tag", "key"
+ @tag = SpecTag.new "tag(comment):description"
+ allow(MSpec).to receive(:read_tags).and_return([@tag])
+ allow(MSpec).to receive(:register)
+ end
+
+ it "unregisters itself" do
+ @filter.load
+ expect(MSpec).to receive(:unregister).with(:include, @filter)
+ @filter.unload
+ end
+end
+
+RSpec.describe TagFilter, "#register" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ end
+
+ it "registers itself with MSpec for the :load, :unload actions" do
+ filter = TagFilter.new(nil)
+ expect(MSpec).to receive(:register).with(:load, filter)
+ expect(MSpec).to receive(:register).with(:unload, filter)
+ filter.register
+ end
+end
+
+RSpec.describe TagFilter, "#unregister" do
+ before :each do
+ allow(MSpec).to receive(:unregister)
+ end
+
+ it "unregisters itself with MSpec for the :load, :unload actions" do
+ filter = TagFilter.new(nil)
+ expect(MSpec).to receive(:unregister).with(:load, filter)
+ expect(MSpec).to receive(:unregister).with(:unload, filter)
+ filter.unregister
+ end
+end
+
+RSpec.describe TagFilter, "#===" do
+ before :each do
+ @filter = TagFilter.new nil, "tag", "key"
+ @tag = SpecTag.new "tag(comment):description"
+ allow(MSpec).to receive(:read_tags).and_return([@tag])
+ allow(MSpec).to receive(:register)
+ @filter.load
+ end
+
+ it "returns true if the argument matches any of the descriptions" do
+ expect(@filter.===('description')).to eq(true)
+ end
+
+ it "returns false if the argument matches none of the descriptions" do
+ expect(@filter.===('descriptionA')).to eq(false)
+ expect(@filter.===('adescription')).to eq(false)
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/describe_spec.rb b/spec/mspec/spec/runner/formatters/describe_spec.rb
new file mode 100644
index 0000000000..55f497aca6
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/describe_spec.rb
@@ -0,0 +1,67 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/describe'
+require 'mspec/runner/example'
+
+RSpec.describe DescribeFormatter, "#finish" do
+ before :each do
+ allow(MSpec).to receive(:register)
+ allow(MSpec).to receive(:unregister)
+
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+ allow(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+
+ $stdout = @out = IOStub.new
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new(context, "runs")
+
+ @formatter = DescribeFormatter.new
+ @formatter.register
+
+ @tally = @formatter.tally
+ @counter = @tally.counter
+
+ @counter.files!
+ @counter.examples!
+ @counter.expectations! 2
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a summary of elapsed time" do
+ @formatter.finish
+ expect(@out).to match(/^Finished in 2.0 seconds$/)
+ end
+
+ it "prints a tally of counts" do
+ @formatter.finish
+ expect(@out).to match(/^1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged$/)
+ end
+
+ it "does not print exceptions" do
+ @formatter.finish
+ expect(@out).to eq(%[
+
+Finished in 2.0 seconds
+
+1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+
+ it "prints a summary of failures and errors for each describe block" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.finish
+ expect(@out).to eq(%[
+
+Class#method 0 failures, 1 error
+
+Finished in 2.0 seconds
+
+1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/dotted_spec.rb b/spec/mspec/spec/runner/formatters/dotted_spec.rb
new file mode 100644
index 0000000000..336b1227ed
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/dotted_spec.rb
@@ -0,0 +1,284 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+
+RSpec.describe DottedFormatter, "#initialize" do
+ it "permits zero arguments" do
+ DottedFormatter.new
+ end
+
+ it "accepts one argument" do
+ DottedFormatter.new nil
+ end
+end
+
+RSpec.describe DottedFormatter, "#register" do
+ before :each do
+ @formatter = DottedFormatter.new
+ allow(MSpec).to receive(:register)
+ end
+
+ it "registers self with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:exception, @formatter)
+ expect(MSpec).to receive(:register).with(:before, @formatter)
+ expect(MSpec).to receive(:register).with(:after, @formatter)
+ expect(MSpec).to receive(:register).with(:finish, @formatter)
+ @formatter.register
+ end
+
+ it "creates TimerAction and TallyAction" do
+ timer = double("timer")
+ tally = double("tally")
+ expect(timer).to receive(:register)
+ expect(tally).to receive(:register)
+ expect(tally).to receive(:counter)
+ expect(TimerAction).to receive(:new).and_return(timer)
+ expect(TallyAction).to receive(:new).and_return(tally)
+ @formatter.register
+ end
+end
+
+RSpec.describe DottedFormatter, "#print" do
+ before :each do
+ $stdout = IOStub.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "writes to $stdout by default" do
+ formatter = DottedFormatter.new
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ end
+
+ it "writes to the file specified when the formatter was created" do
+ out = IOStub.new
+ expect(File).to receive(:open).with("some/file", "w").and_return(out)
+ formatter = DottedFormatter.new "some/file"
+ formatter.print "begonias"
+ expect(out).to eq("begonias")
+ end
+
+ it "flushes the IO output" do
+ expect($stdout).to receive(:flush)
+ formatter = DottedFormatter.new
+ formatter.print "begonias"
+ end
+end
+
+RSpec.describe DottedFormatter, "#exception" do
+ before :each do
+ @formatter = DottedFormatter.new
+ @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed")
+ @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!")
+ end
+
+ it "sets the #failure? flag" do
+ @formatter.exception @failure
+ expect(@formatter.failure?).to be_truthy
+ @formatter.exception @error
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "sets the #exception? flag" do
+ @formatter.exception @error
+ expect(@formatter.exception?).to be_truthy
+ @formatter.exception @failure
+ expect(@formatter.exception?).to be_truthy
+ end
+
+ it "adds the exception to the list of exceptions" do
+ expect(@formatter.exceptions).to eq([])
+ @formatter.exception @error
+ @formatter.exception @failure
+ expect(@formatter.exceptions).to eq([@error, @failure])
+ end
+end
+
+RSpec.describe DottedFormatter, "#exception?" do
+ before :each do
+ @formatter = DottedFormatter.new
+ @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed")
+ @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!")
+ end
+
+ it "returns false if there have been no exceptions" do
+ expect(@formatter.exception?).to be_falsey
+ end
+
+ it "returns true if any exceptions are errors" do
+ @formatter.exception @failure
+ @formatter.exception @error
+ expect(@formatter.exception?).to be_truthy
+ end
+
+ it "returns true if all exceptions are failures" do
+ @formatter.exception @failure
+ @formatter.exception @failure
+ expect(@formatter.exception?).to be_truthy
+ end
+
+ it "returns true if all exceptions are errors" do
+ @formatter.exception @error
+ @formatter.exception @error
+ expect(@formatter.exception?).to be_truthy
+ end
+end
+
+RSpec.describe DottedFormatter, "#failure?" do
+ before :each do
+ @formatter = DottedFormatter.new
+ @failure = ExceptionState.new nil, nil, SpecExpectationNotMetError.new("failed")
+ @error = ExceptionState.new nil, nil, MSpecExampleError.new("boom!")
+ end
+
+ it "returns false if there have been no exceptions" do
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "returns false if any exceptions are errors" do
+ @formatter.exception @failure
+ @formatter.exception @error
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "returns true if all exceptions are failures" do
+ @formatter.exception @failure
+ @formatter.exception @failure
+ expect(@formatter.failure?).to be_truthy
+ end
+end
+
+RSpec.describe DottedFormatter, "#before" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter = DottedFormatter.new
+ @formatter.exception ExceptionState.new(nil, nil, SpecExpectationNotMetError.new("Failed!"))
+ end
+
+ it "resets the #failure? flag to false" do
+ expect(@formatter.failure?).to be_truthy
+ @formatter.before @state
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "resets the #exception? flag to false" do
+ expect(@formatter.exception?).to be_truthy
+ @formatter.before @state
+ expect(@formatter.exception?).to be_falsey
+ end
+end
+
+RSpec.describe DottedFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = DottedFormatter.new
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a '.' if there was no exception raised" do
+ @formatter.after(@state)
+ expect(@out).to eq(".")
+ end
+
+ it "prints an 'F' if there was an expectation failure" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.after(@state)
+ expect(@out).to eq("F")
+ end
+
+ it "prints an 'E' if there was an exception other than expectation failure" do
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.after(@state)
+ expect(@out).to eq("E")
+ end
+
+ it "prints an 'E' if there are mixed exceptions and exepctation failures" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.after(@state)
+ expect(@out).to eq("E")
+ end
+end
+
+RSpec.describe DottedFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ allow(TallyAction).to receive(:new).and_return(@tally)
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ $stdout = @out = IOStub.new
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new(context, "runs")
+ allow(MSpec).to receive(:register)
+ @formatter = DottedFormatter.new
+ @formatter.register
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a failure message for an exception" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ @formatter.exception exc
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to match(/^1\)\nClass#method runs ERROR$/)
+ end
+
+ it "prints a backtrace for an exception" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to match(%r[path/to/some/file.rb:35:in method$])
+ end
+
+ it "prints a summary of elapsed time" do
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ @formatter.finish
+ expect(@out).to match(/^Finished in 2.0 seconds$/)
+ end
+
+ it "prints a tally of counts" do
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ expect(@out).to match(/^1 example, 0 failures$/)
+ end
+
+ it "prints errors, backtraces, elapsed time, and tallies" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ expect(@tally).to receive(:format).and_return("1 example, 1 failure")
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to eq(%[E
+
+1)
+Class#method runs ERROR
+MSpecExampleError: broken
+path/to/some/file.rb:35:in method
+
+Finished in 2.0 seconds
+
+1 example, 1 failure
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/file_spec.rb b/spec/mspec/spec/runner/formatters/file_spec.rb
new file mode 100644
index 0000000000..ae11d60845
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/file_spec.rb
@@ -0,0 +1,84 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/file'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe FileFormatter, "#register" do
+ before :each do
+ @formatter = FileFormatter.new
+ allow(MSpec).to receive(:register)
+ allow(MSpec).to receive(:unregister)
+ end
+
+ it "registers self with MSpec for :load, :unload actions" do
+ expect(MSpec).to receive(:register).with(:load, @formatter)
+ expect(MSpec).to receive(:register).with(:unload, @formatter)
+ @formatter.register
+ end
+
+ it "unregisters self with MSpec for :before, :after actions" do
+ expect(MSpec).to receive(:unregister).with(:before, @formatter)
+ expect(MSpec).to receive(:unregister).with(:after, @formatter)
+ @formatter.register
+ end
+end
+
+RSpec.describe FileFormatter, "#load" do
+ before :each do
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter = FileFormatter.new
+ @formatter.exception ExceptionState.new(nil, nil, SpecExpectationNotMetError.new("Failed!"))
+ end
+
+ it "resets the #failure? flag to false" do
+ expect(@formatter.failure?).to be_truthy
+ @formatter.load @state
+ expect(@formatter.failure?).to be_falsey
+ end
+
+ it "resets the #exception? flag to false" do
+ expect(@formatter.exception?).to be_truthy
+ @formatter.load @state
+ expect(@formatter.exception?).to be_falsey
+ end
+end
+
+RSpec.describe FileFormatter, "#unload" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = FileFormatter.new
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a '.' if there was no exception raised" do
+ @formatter.unload(@state)
+ expect(@out).to eq(".")
+ end
+
+ it "prints an 'F' if there was an expectation failure" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.unload(@state)
+ expect(@out).to eq("F")
+ end
+
+ it "prints an 'E' if there was an exception other than expectation failure" do
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.unload(@state)
+ expect(@out).to eq("E")
+ end
+
+ it "prints an 'E' if there are mixed exceptions and exepctation failures" do
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ exc = MSpecExampleError.new("boom!")
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.unload(@state)
+ expect(@out).to eq("E")
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/html_spec.rb b/spec/mspec/spec/runner/formatters/html_spec.rb
new file mode 100644
index 0000000000..ed973ad93f
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/html_spec.rb
@@ -0,0 +1,220 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/guards/guard'
+require 'mspec/runner/formatters/html'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+require 'mspec/helpers'
+
+RSpec.describe HtmlFormatter do
+ before :each do
+ @formatter = HtmlFormatter.new
+ end
+
+ it "responds to #register by registering itself with MSpec for appropriate actions" do
+ allow(MSpec).to receive(:register)
+ expect(MSpec).to receive(:register).with(:start, @formatter)
+ expect(MSpec).to receive(:register).with(:enter, @formatter)
+ expect(MSpec).to receive(:register).with(:leave, @formatter)
+ @formatter.register
+ end
+end
+
+RSpec.describe HtmlFormatter, "#start" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the HTML head" do
+ @formatter.start
+ ruby_engine = RUBY_ENGINE
+ expect(ruby_engine).to match(/^#{ruby_engine}/)
+ expect(@out).to eq(%[<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
+ "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>Spec Output For #{ruby_engine} (#{RUBY_VERSION})</title>
+<style type="text/css">
+ul {
+ list-style: none;
+}
+.fail {
+ color: red;
+}
+.pass {
+ color: green;
+}
+#details :target {
+ background-color: #ffffe0;
+}
+</style>
+</head>
+<body>
+])
+ end
+end
+
+RSpec.describe HtmlFormatter, "#enter" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #describe string" do
+ @formatter.enter "describe"
+ expect(@out).to eq("<div><p>describe</p>\n<ul>\n")
+ end
+end
+
+RSpec.describe HtmlFormatter, "#leave" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the closing tags for the #describe string" do
+ @formatter.leave
+ expect(@out).to eq("</ul>\n</div>\n")
+ end
+end
+
+RSpec.describe HtmlFormatter, "#exception" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ @formatter.register
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #it string once for each exception raised" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ expect(@out).to eq(%[<li class="fail">- it (<a href="#details-1">FAILED - 1</a>)</li>
+<li class="fail">- it (<a href="#details-2">ERROR - 2</a>)</li>
+])
+ end
+end
+
+RSpec.describe HtmlFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = HtmlFormatter.new
+ @formatter.register
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #it once when there are no exceptions raised" do
+ @formatter.after @state
+ expect(@out).to eq(%[<li class="pass">- it</li>\n])
+ end
+
+ it "does not print any output if an exception is raised" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ out = @out.dup
+ @formatter.after @state
+ expect(@out).to eq(out)
+ end
+end
+
+RSpec.describe HtmlFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ allow(TallyAction).to receive(:new).and_return(@tally)
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ @out = tmp("HtmlFormatter")
+
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+ allow(MSpec).to receive(:register)
+ @formatter = HtmlFormatter.new(@out)
+ @formatter.register
+ @exception = MSpecExampleError.new("broken")
+ allow(@exception).to receive(:backtrace).and_return(["file.rb:1", "file.rb:2"])
+ end
+
+ after :each do
+ rm_r @out
+ end
+
+ it "prints a failure message for an exception" do
+ exc = ExceptionState.new @state, nil, @exception
+ @formatter.exception exc
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "<p>describe it ERROR</p>"
+ end
+
+ it "prints a backtrace for an exception" do
+ exc = ExceptionState.new @state, nil, @exception
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to match(%r[<pre>.*path/to/some/file.rb:35:in method.*</pre>]m)
+ end
+
+ it "prints a summary of elapsed time" do
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "<p>Finished in 2.0 seconds</p>\n"
+ end
+
+ it "prints a tally of counts" do
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include '<p class="pass">1 example, 0 failures</p>'
+ end
+
+ it "prints errors, backtraces, elapsed time, and tallies" do
+ exc = ExceptionState.new @state, nil, @exception
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ expect(@tally).to receive(:format).and_return("1 example, 1 failures")
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to eq(%[<li class=\"fail\">- it (<a href=\"#details-1\">ERROR - 1</a>)</li>
+<hr>
+<ol id="details">
+<li id="details-1"><p>describe it ERROR</p>
+<p>MSpecExampleError: broken</p>
+<pre>
+path/to/some/file.rb:35:in method</pre>
+</li>
+</ol>
+<p>Finished in 2.0 seconds</p>
+<p class="fail">1 example, 1 failures</p>
+</body>
+</html>
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/junit_spec.rb b/spec/mspec/spec/runner/formatters/junit_spec.rb
new file mode 100644
index 0000000000..3b3da73849
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/junit_spec.rb
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/junit'
+require 'mspec/runner/example'
+require 'mspec/helpers'
+
+RSpec.describe JUnitFormatter, "#initialize" do
+ it "permits zero arguments" do
+ expect { JUnitFormatter.new }.not_to raise_error
+ end
+
+ it "accepts one argument" do
+ expect { JUnitFormatter.new nil }.not_to raise_error
+ end
+end
+
+RSpec.describe JUnitFormatter, "#print" do
+ before :each do
+ $stdout = IOStub.new
+ @out = IOStub.new
+ allow(File).to receive(:open).and_return(@out)
+ @formatter = JUnitFormatter.new "some/file"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "writes to $stdout if #switch has not been called" do
+ @formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+
+ it "writes to the file passed to #initialize once #switch has been called" do
+ @formatter.switch
+ @formatter.print "begonias"
+ expect($stdout).to eq("")
+ expect(@out).to eq("begonias")
+ end
+
+ it "writes to $stdout once #switch is called if no file was passed to #initialize" do
+ formatter = JUnitFormatter.new
+ formatter.switch
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+end
+
+RSpec.describe JUnitFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ @counter = double("counter").as_null_object
+ allow(@tally).to receive(:counter).and_return(@counter)
+ allow(TallyAction).to receive(:new).and_return(@tally)
+
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ @out = tmp("JUnitFormatter")
+
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+
+ @formatter = JUnitFormatter.new(@out)
+ allow(@formatter).to receive(:backtrace).and_return("")
+ allow(MSpec).to receive(:register)
+ @formatter.register
+
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ end
+
+ after :each do
+ rm_r @out
+ end
+
+ it "calls #switch" do
+ expect(@formatter).to receive(:switch).and_call_original
+ @formatter.finish
+ end
+
+ it "outputs a failure message and backtrace" do
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'message="error in describe it" type="error"'
+ expect(output).to include "MSpecExampleError: broken\n"
+ expect(output).to include "path/to/some/file.rb:35:in method"
+ end
+
+ it "encodes message and backtrace in latin1 for jenkins" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken…")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in methød")
+ @formatter.exception exc
+ @formatter.finish
+ output = File.binread(@out)
+ expect(output).to match(/MSpecExampleError: broken((\.\.\.)|\?)\n/)
+ expect(output).to match(/path\/to\/some\/file\.rb:35:in meth(\?|o)d/)
+ end
+
+ it "outputs an elapsed time" do
+ expect(@timer).to receive(:elapsed).and_return(4.2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'time="4.2"'
+ end
+
+ it "outputs overall elapsed time" do
+ expect(@timer).to receive(:elapsed).and_return(4.2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'timeCount="4.2"'
+ end
+
+ it "outputs the number of examples as test count" do
+ expect(@counter).to receive(:examples).and_return(9)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'tests="9"'
+ end
+
+ it "outputs overall number of examples as test count" do
+ expect(@counter).to receive(:examples).and_return(9)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'testCount="9"'
+ end
+
+ it "outputs a failure count" do
+ expect(@counter).to receive(:failures).and_return(2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'failureCount="2"'
+ end
+
+ it "outputs overall failure count" do
+ expect(@counter).to receive(:failures).and_return(2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'failures="2"'
+ end
+
+ it "outputs an error count" do
+ expect(@counter).to receive(:errors).and_return(1)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'errors="1"'
+ end
+
+ it "outputs overall error count" do
+ expect(@counter).to receive(:errors).and_return(1)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include 'errorCount="1"'
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/method_spec.rb b/spec/mspec/spec/runner/formatters/method_spec.rb
new file mode 100644
index 0000000000..02bf47d538
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/method_spec.rb
@@ -0,0 +1,177 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/method'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+
+RSpec.describe MethodFormatter, "#method_type" do
+ before :each do
+ @formatter = MethodFormatter.new
+ end
+
+ it "returns 'class' if the separator is '.' or '::'" do
+ expect(@formatter.method_type('.')).to eq("class")
+ expect(@formatter.method_type('::')).to eq("class")
+ end
+
+ it "returns 'instance' if the separator is '#'" do
+ expect(@formatter.method_type('#')).to eq("instance")
+ end
+
+ it "returns 'unknown' for all other cases" do
+ expect(@formatter.method_type(nil)).to eq("unknown")
+ end
+end
+
+RSpec.describe MethodFormatter, "#before" do
+ before :each do
+ @formatter = MethodFormatter.new
+ allow(MSpec).to receive(:register)
+ @formatter.register
+ end
+
+ it "resets the tally counters to 0" do
+ @formatter.tally.counter.examples = 3
+ @formatter.tally.counter.expectations = 4
+ @formatter.tally.counter.failures = 2
+ @formatter.tally.counter.errors = 1
+
+ state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter.before state
+ expect(@formatter.tally.counter.examples).to eq(0)
+ expect(@formatter.tally.counter.expectations).to eq(0)
+ expect(@formatter.tally.counter.failures).to eq(0)
+ expect(@formatter.tally.counter.errors).to eq(0)
+ end
+
+ it "records the class, method if available" do
+ state = ExampleState.new ContextState.new("Some#method"), "it"
+ @formatter.before state
+ key = "Some#method"
+ expect(@formatter.methods.keys).to include(key)
+ h = @formatter.methods[key]
+ expect(h[:class]).to eq("Some")
+ expect(h[:method]).to eq("method")
+ expect(h[:description]).to eq("Some#method it")
+ end
+
+ it "does not record class, method unless both are available" do
+ state = ExampleState.new ContextState.new("Some method"), "it"
+ @formatter.before state
+ key = "Some method"
+ expect(@formatter.methods.keys).to include(key)
+ h = @formatter.methods[key]
+ expect(h[:class]).to eq("")
+ expect(h[:method]).to eq("")
+ expect(h[:description]).to eq("Some method it")
+ end
+
+ it "sets the method type to unknown if class and method are not available" do
+ state = ExampleState.new ContextState.new("Some method"), "it"
+ @formatter.before state
+ key = "Some method"
+ h = @formatter.methods[key]
+ expect(h[:type]).to eq("unknown")
+ end
+
+ it "sets the method type based on the class, method separator" do
+ [["C#m", "instance"], ["C.m", "class"], ["C::m", "class"]].each do |k, t|
+ state = ExampleState.new ContextState.new(k), "it"
+ @formatter.before state
+ h = @formatter.methods[k]
+ expect(h[:type]).to eq(t)
+ end
+ end
+
+ it "clears the list of exceptions" do
+ state = ExampleState.new ContextState.new("describe"), "it"
+ @formatter.exceptions << "stuff"
+ @formatter.before state
+ expect(@formatter.exceptions).to be_empty
+ end
+end
+
+RSpec.describe MethodFormatter, "#after" do
+ before :each do
+ @formatter = MethodFormatter.new
+ allow(MSpec).to receive(:register)
+ @formatter.register
+ end
+
+ it "sets the tally counts" do
+ state = ExampleState.new ContextState.new("Some#method"), "it"
+ @formatter.before state
+
+ @formatter.tally.counter.examples = 3
+ @formatter.tally.counter.expectations = 4
+ @formatter.tally.counter.failures = 2
+ @formatter.tally.counter.errors = 1
+
+ @formatter.after state
+ h = @formatter.methods["Some#method"]
+ expect(h[:examples]).to eq(3)
+ expect(h[:expectations]).to eq(4)
+ expect(h[:failures]).to eq(2)
+ expect(h[:errors]).to eq(1)
+ end
+
+ it "renders the list of exceptions" do
+ state = ExampleState.new ContextState.new("Some#method"), "it"
+ @formatter.before state
+
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(state, nil, exc)
+ @formatter.exception ExceptionState.new(state, nil, exc)
+
+ @formatter.after state
+ h = @formatter.methods["Some#method"]
+ expect(h[:exceptions]).to eq([
+ %[failed\n\n],
+ %[failed\n\n]
+ ])
+ end
+end
+
+RSpec.describe MethodFormatter, "#after" do
+ before :each do
+ $stdout = IOStub.new
+ context = ContextState.new "Class#method"
+ @state = ExampleState.new(context, "runs")
+ @formatter = MethodFormatter.new
+ allow(MSpec).to receive(:register)
+ @formatter.register
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a summary of the results of an example in YAML format" do
+ @formatter.before @state
+ @formatter.tally.counter.examples = 3
+ @formatter.tally.counter.expectations = 4
+ @formatter.tally.counter.failures = 2
+ @formatter.tally.counter.errors = 1
+
+ exc = SpecExpectationNotMetError.new "failed"
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+ @formatter.exception ExceptionState.new(@state, nil, exc)
+
+ @formatter.after @state
+ @formatter.finish
+ expect($stdout).to eq(%[---
+"Class#method":
+ class: "Class"
+ method: "method"
+ type: instance
+ description: "Class#method runs"
+ examples: 3
+ expectations: 4
+ failures: 2
+ errors: 1
+ exceptions:
+ - "failed\\n\\n"
+ - "failed\\n\\n"
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/multi_spec.rb b/spec/mspec/spec/runner/formatters/multi_spec.rb
new file mode 100644
index 0000000000..2d13c05836
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/multi_spec.rb
@@ -0,0 +1,68 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/dotted'
+require 'mspec/runner/formatters/multi'
+require 'mspec/runner/example'
+require 'yaml'
+
+RSpec.describe MultiFormatter, "#aggregate_results" do
+ before :each do
+ @stdout, $stdout = $stdout, IOStub.new
+
+ @file = double("file").as_null_object
+
+ allow(File).to receive(:delete)
+ allow(File).to receive(:read)
+
+ @hash = { "files"=>1, "examples"=>1, "expectations"=>2, "failures"=>0, "errors"=>0 }
+ allow(YAML).to receive(:load).and_return(@hash)
+
+ @formatter = DottedFormatter.new.extend(MultiFormatter)
+ allow(@formatter.timer).to receive(:format).and_return("Finished in 42 seconds")
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it "outputs a summary without errors" do
+ @formatter.aggregate_results(["a", "b"])
+ @formatter.finish
+ expect($stdout).to eq(%[
+
+Finished in 42 seconds
+
+2 files, 2 examples, 4 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+
+ it "outputs a summary with errors" do
+ @hash["exceptions"] = [
+ "Some#method works real good FAILED\nExpected real good\n to equal fail\n\nfoo.rb:1\nfoo.rb:2",
+ "Some#method never fails ERROR\nExpected 5\n to equal 3\n\nfoo.rb:1\nfoo.rb:2"
+ ]
+ @formatter.aggregate_results(["a"])
+ @formatter.finish
+ expect($stdout).to eq(%[
+
+1)
+Some#method works real good FAILED
+Expected real good
+ to equal fail
+
+foo.rb:1
+foo.rb:2
+
+2)
+Some#method never fails ERROR
+Expected 5
+ to equal 3
+
+foo.rb:1
+foo.rb:2
+
+Finished in 42 seconds
+
+1 file, 1 example, 2 expectations, 0 failures, 0 errors, 0 tagged
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/specdoc_spec.rb b/spec/mspec/spec/runner/formatters/specdoc_spec.rb
new file mode 100644
index 0000000000..54b5e2cf0d
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/specdoc_spec.rb
@@ -0,0 +1,106 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/specdoc'
+require 'mspec/runner/example'
+
+RSpec.describe SpecdocFormatter do
+ before :each do
+ @formatter = SpecdocFormatter.new
+ end
+
+ it "responds to #register by registering itself with MSpec for appropriate actions" do
+ allow(MSpec).to receive(:register)
+ expect(MSpec).to receive(:register).with(:enter, @formatter)
+ @formatter.register
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#enter" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #describe string" do
+ @formatter.enter("describe")
+ expect(@out).to eq("\ndescribe\n")
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#before" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ @state = ExampleState.new ContextState.new("describe"), "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints the #it string" do
+ @formatter.before @state
+ expect(@out).to eq("- it")
+ end
+
+ it "resets the #exception? flag" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ expect(@formatter.exception?).to be_truthy
+ @formatter.before @state
+ expect(@formatter.exception?).to be_falsey
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#exception" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ context = ContextState.new "describe"
+ @state = ExampleState.new context, "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints 'ERROR' if an exception is not an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ expect(@out).to eq(" (ERROR - 1)")
+ end
+
+ it "prints 'FAILED' if an exception is an SpecExpectationNotMetError" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ expect(@out).to eq(" (FAILED - 1)")
+ end
+
+ it "prints the #it string if an exception has already been raised" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ expect(@out).to eq(" (FAILED - 1)\n- it (ERROR - 2)")
+ end
+end
+
+RSpec.describe SpecdocFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SpecdocFormatter.new
+ @state = ExampleState.new "describe", "it"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a newline character" do
+ @formatter.after @state
+ expect(@out).to eq("\n")
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/spinner_spec.rb b/spec/mspec/spec/runner/formatters/spinner_spec.rb
new file mode 100644
index 0000000000..5c93d38822
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/spinner_spec.rb
@@ -0,0 +1,83 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/spinner'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe SpinnerFormatter, "#initialize" do
+ it "permits zero arguments" do
+ SpinnerFormatter.new
+ end
+
+ it "accepts one argument" do
+ SpinnerFormatter.new nil
+ end
+end
+
+RSpec.describe SpinnerFormatter, "#register" do
+ before :each do
+ @formatter = SpinnerFormatter.new
+ allow(MSpec).to receive(:register)
+ end
+
+ it "registers self with MSpec for appropriate actions" do
+ expect(MSpec).to receive(:register).with(:start, @formatter)
+ expect(MSpec).to receive(:register).with(:unload, @formatter)
+ expect(MSpec).to receive(:register).with(:after, @formatter)
+ expect(MSpec).to receive(:register).with(:finish, @formatter)
+ @formatter.register
+ end
+
+ it "creates TimerAction and TallyAction" do
+ timer = double("timer")
+ tally = double("tally")
+ expect(timer).to receive(:register)
+ expect(tally).to receive(:register)
+ expect(tally).to receive(:counter)
+ expect(TimerAction).to receive(:new).and_return(timer)
+ expect(TallyAction).to receive(:new).and_return(tally)
+ @formatter.register
+ end
+end
+
+RSpec.describe SpinnerFormatter, "#print" do
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "ignores the argument to #initialize and writes to $stdout" do
+ $stdout = IOStub.new
+ formatter = SpinnerFormatter.new "some/file"
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ end
+end
+
+RSpec.describe SpinnerFormatter, "#after" do
+ before :each do
+ $stdout = IOStub.new
+ MSpec.store(:files, ["a", "b", "c", "d"])
+ @formatter = SpinnerFormatter.new
+ @formatter.register
+ @state = ExampleState.new("describe", "it")
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "updates the spinner" do
+ @formatter.start
+ @formatter.after @state
+ @formatter.unload
+
+ if ENV["TERM"] != "dumb"
+ green = "\e[0;32m"
+ reset = "\e[0m"
+ end
+
+ output = "\r[/ | 0% | 00:00:00] #{green} 0F #{green} 0E#{reset} " \
+ "\r[- | 0% | 00:00:00] #{green} 0F #{green} 0E#{reset} " \
+ "\r[\\ | ========== 25% | 00:00:00] #{green} 0F #{green} 0E#{reset} "
+ expect($stdout).to eq(output)
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/summary_spec.rb b/spec/mspec/spec/runner/formatters/summary_spec.rb
new file mode 100644
index 0000000000..c87d940042
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/summary_spec.rb
@@ -0,0 +1,26 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/summary'
+require 'mspec/runner/example'
+
+RSpec.describe SummaryFormatter, "#after" do
+ before :each do
+ $stdout = @out = IOStub.new
+ @formatter = SummaryFormatter.new
+ @formatter.register
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "does not print anything" do
+ exc = ExceptionState.new @state, nil, SpecExpectationNotMetError.new("disappointing")
+ @formatter.exception exc
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("painful")
+ @formatter.exception exc
+ @formatter.after(@state)
+ expect(@out).to eq("")
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/unit_spec.rb b/spec/mspec/spec/runner/formatters/unit_spec.rb
new file mode 100644
index 0000000000..d349e6871d
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/unit_spec.rb
@@ -0,0 +1,73 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/unit'
+require 'mspec/runner/example'
+require 'mspec/utils/script'
+
+RSpec.describe UnitdiffFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ allow(TallyAction).to receive(:new).and_return(@tally)
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ $stdout = @out = IOStub.new
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+ allow(MSpec).to receive(:register)
+ @formatter = UnitdiffFormatter.new
+ @formatter.register
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "prints a failure message for an exception" do
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ @formatter.exception exc
+ @formatter.after @state
+ @formatter.finish
+ expect(@out).to match(/^1\)\ndescribe it ERROR$/)
+ end
+
+ it "prints a backtrace for an exception" do
+ exc = ExceptionState.new @state, nil, Exception.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.finish
+ expect(@out).to match(%r[path/to/some/file.rb:35:in method$])
+ end
+
+ it "prints a summary of elapsed time" do
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ @formatter.finish
+ expect(@out).to match(/^Finished in 2.0 seconds$/)
+ end
+
+ it "prints a tally of counts" do
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ expect(@out).to match(/^1 example, 0 failures$/)
+ end
+
+ it "prints errors, backtraces, elapsed time, and tallies" do
+ exc = ExceptionState.new @state, nil, Exception.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ expect(@timer).to receive(:format).and_return("Finished in 2.0 seconds")
+ expect(@tally).to receive(:format).and_return("1 example, 0 failures")
+ @formatter.finish
+ expect(@out).to eq(%[E
+
+Finished in 2.0 seconds
+
+1)
+describe it ERROR
+Exception: broken:
+path/to/some/file.rb:35:in method
+
+1 example, 0 failures
+])
+ end
+end
diff --git a/spec/mspec/spec/runner/formatters/yaml_spec.rb b/spec/mspec/spec/runner/formatters/yaml_spec.rb
new file mode 100644
index 0000000000..2e334fdbb9
--- /dev/null
+++ b/spec/mspec/spec/runner/formatters/yaml_spec.rb
@@ -0,0 +1,134 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+require 'mspec/runner/formatters/yaml'
+require 'mspec/runner/example'
+require 'mspec/helpers'
+
+RSpec.describe YamlFormatter, "#initialize" do
+ it "permits zero arguments" do
+ YamlFormatter.new
+ end
+
+ it "accepts one argument" do
+ YamlFormatter.new nil
+ end
+end
+
+RSpec.describe YamlFormatter, "#print" do
+ before :each do
+ $stdout = IOStub.new
+ @out = IOStub.new
+ allow(File).to receive(:open).and_return(@out)
+ @formatter = YamlFormatter.new "some/file"
+ end
+
+ after :each do
+ $stdout = STDOUT
+ end
+
+ it "writes to $stdout if #switch has not been called" do
+ @formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+
+ it "writes to the file passed to #initialize once #switch has been called" do
+ @formatter.switch
+ @formatter.print "begonias"
+ expect($stdout).to eq("")
+ expect(@out).to eq("begonias")
+ end
+
+ it "writes to $stdout once #switch is called if no file was passed to #initialize" do
+ formatter = YamlFormatter.new
+ formatter.switch
+ formatter.print "begonias"
+ expect($stdout).to eq("begonias")
+ expect(@out).to eq("")
+ end
+end
+
+RSpec.describe YamlFormatter, "#finish" do
+ before :each do
+ @tally = double("tally").as_null_object
+ @counter = double("counter").as_null_object
+ allow(@tally).to receive(:counter).and_return(@counter)
+ allow(TallyAction).to receive(:new).and_return(@tally)
+
+ @timer = double("timer").as_null_object
+ allow(TimerAction).to receive(:new).and_return(@timer)
+
+ @out = tmp("YamlFormatter")
+
+ context = ContextState.new "describe"
+ @state = ExampleState.new(context, "it")
+
+ @formatter = YamlFormatter.new(@out)
+ allow(@formatter).to receive(:backtrace).and_return("")
+ allow(MSpec).to receive(:register)
+ @formatter.register
+
+ exc = ExceptionState.new @state, nil, MSpecExampleError.new("broken")
+ allow(exc).to receive(:backtrace).and_return("path/to/some/file.rb:35:in method")
+ @formatter.exception exc
+ @formatter.after @state
+ end
+
+ after :each do
+ rm_r @out
+ end
+
+ it "calls #switch" do
+ expect(@formatter).to receive(:switch).and_call_original
+ @formatter.finish
+ end
+
+ it "outputs a failure message and backtrace" do
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "describe it ERROR"
+ expect(output).to include "MSpecExampleError: broken\\n"
+ expect(output).to include "path/to/some/file.rb:35:in method"
+ end
+
+ it "outputs an elapsed time" do
+ expect(@timer).to receive(:elapsed).and_return(4.2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "time: 4.2"
+ end
+
+ it "outputs a file count" do
+ expect(@counter).to receive(:files).and_return(3)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "files: 3"
+ end
+
+ it "outputs an example count" do
+ expect(@counter).to receive(:examples).and_return(3)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "examples: 3"
+ end
+
+ it "outputs an expectation count" do
+ expect(@counter).to receive(:expectations).and_return(9)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "expectations: 9"
+ end
+
+ it "outputs a failure count" do
+ expect(@counter).to receive(:failures).and_return(2)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "failures: 2"
+ end
+
+ it "outputs an error count" do
+ expect(@counter).to receive(:errors).and_return(1)
+ @formatter.finish
+ output = File.read(@out)
+ expect(output).to include "errors: 1"
+ end
+end
diff --git a/spec/mspec/spec/runner/mspec_spec.rb b/spec/mspec/spec/runner/mspec_spec.rb
new file mode 100644
index 0000000000..4af01806c0
--- /dev/null
+++ b/spec/mspec/spec/runner/mspec_spec.rb
@@ -0,0 +1,597 @@
+require 'spec_helper'
+require 'mspec/expectations/expectations'
+require 'mspec/helpers/tmp'
+require 'mspec/helpers/fs'
+require 'mspec/matchers/base'
+require 'mspec/runner/mspec'
+require 'mspec/runner/example'
+
+RSpec.describe MSpec, ".register_files" do
+ it "records which spec files to run" do
+ MSpec.register_files [:one, :two, :three]
+ expect(MSpec.files_array).to eq([:one, :two, :three])
+ end
+end
+
+RSpec.describe MSpec, ".register_mode" do
+ before :each do
+ MSpec.clear_modes
+ end
+
+ it "sets execution mode flags" do
+ MSpec.register_mode :verify
+ expect(MSpec.retrieve(:modes)).to eq([:verify])
+ end
+end
+
+RSpec.describe MSpec, ".register_tags_patterns" do
+ it "records the patterns for generating a tag file from a spec file" do
+ MSpec.register_tags_patterns [[/spec\/ruby/, "spec/tags"], [/frozen/, "ruby"]]
+ expect(MSpec.retrieve(:tags_patterns)).to eq([[/spec\/ruby/, "spec/tags"], [/frozen/, "ruby"]])
+ end
+end
+
+RSpec.describe MSpec, ".register_exit" do
+ before :each do
+ MSpec.store :exit, 0
+ end
+
+ it "records the exit code" do
+ expect(MSpec.exit_code).to eq(0)
+ MSpec.register_exit 1
+ expect(MSpec.exit_code).to eq(1)
+ end
+end
+
+RSpec.describe MSpec, ".exit_code" do
+ it "retrieves the code set with .register_exit" do
+ MSpec.register_exit 99
+ expect(MSpec.exit_code).to eq(99)
+ end
+end
+
+RSpec.describe MSpec, ".store" do
+ it "records data for MSpec settings" do
+ MSpec.store :anything, :value
+ expect(MSpec.retrieve(:anything)).to eq(:value)
+ end
+end
+
+RSpec.describe MSpec, ".retrieve" do
+ it "accesses .store'd data" do
+ MSpec.register :retrieve, :first
+ expect(MSpec.retrieve(:retrieve)).to eq([:first])
+ end
+end
+
+RSpec.describe MSpec, ".randomize" do
+ it "sets the flag to randomize spec execution order" do
+ expect(MSpec.randomize?).to eq(false)
+ MSpec.randomize = true
+ expect(MSpec.randomize?).to eq(true)
+ MSpec.randomize = false
+ expect(MSpec.randomize?).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".register" do
+ it "is the gateway behind the register(symbol, action) facility" do
+ MSpec.register :bonus, :first
+ MSpec.register :bonus, :second
+ MSpec.register :bonus, :second
+ expect(MSpec.retrieve(:bonus)).to eq([:first, :second])
+ end
+end
+
+RSpec.describe MSpec, ".unregister" do
+ it "is the gateway behind the unregister(symbol, actions) facility" do
+ MSpec.register :unregister, :first
+ MSpec.register :unregister, :second
+ MSpec.unregister :unregister, :second
+ expect(MSpec.retrieve(:unregister)).to eq([:first])
+ end
+end
+
+RSpec.describe MSpec, ".protect" do
+ before :each do
+ MSpec.clear_current
+ @cs = ContextState.new "C#m"
+ @cs.parent = MSpec.current
+
+ @es = ExampleState.new @cs, "runs"
+ ScratchPad.record Exception.new("Sharp!")
+ end
+
+ it "returns true if no exception is raised" do
+ expect(MSpec.protect("passed") { 1 }).to be_truthy
+ end
+
+ it "returns false if an exception is raised" do
+ expect(MSpec.protect("testing") { raise ScratchPad.recorded }).to be_falsey
+ end
+
+ it "rescues any exceptions raised when evaluating the block argument" do
+ MSpec.protect("") { raise Exception, "Now you see me..." }
+ end
+
+ it "does not rescue SystemExit" do
+ begin
+ MSpec.protect("") { exit 1 }
+ rescue SystemExit
+ ScratchPad.record :system_exit
+ end
+ expect(ScratchPad.recorded).to eq(:system_exit)
+ end
+
+ it "calls all the exception actions" do
+ exc = ExceptionState.new @es, "testing", ScratchPad.recorded
+ allow(ExceptionState).to receive(:new).and_return(exc)
+ action = double("exception")
+ expect(action).to receive(:exception).with(exc)
+ MSpec.register :exception, action
+ MSpec.protect("testing") { raise ScratchPad.recorded }
+ MSpec.unregister :exception, action
+ end
+
+ it "registers a non-zero exit code when an exception is raised" do
+ expect(MSpec).to receive(:register_exit).with(1)
+ MSpec.protect("testing") { raise ScratchPad.recorded }
+ end
+end
+
+RSpec.describe MSpec, ".register_current" do
+ before :each do
+ MSpec.clear_current
+ end
+
+ it "sets the value returned by MSpec.current" do
+ expect(MSpec.current).to be_nil
+ MSpec.register_current :a
+ expect(MSpec.current).to eq(:a)
+ end
+end
+
+RSpec.describe MSpec, ".clear_current" do
+ it "sets the value returned by MSpec.current to nil" do
+ MSpec.register_current :a
+ expect(MSpec.current).not_to be_nil
+ MSpec.clear_current
+ expect(MSpec.current).to be_nil
+ end
+end
+
+RSpec.describe MSpec, ".current" do
+ before :each do
+ MSpec.clear_current
+ end
+
+ it "returns nil if no ContextState has been registered" do
+ expect(MSpec.current).to be_nil
+ end
+
+ it "returns the most recently registered ContextState" do
+ first = ContextState.new ""
+ second = ContextState.new ""
+ MSpec.register_current first
+ expect(MSpec.current).to eq(first)
+ MSpec.register_current second
+ expect(MSpec.current).to eq(second)
+ end
+end
+
+RSpec.describe MSpec, ".actions" do
+ before :each do
+ MSpec.store :start, []
+ ScratchPad.record []
+ start_one = double("one")
+ allow(start_one).to receive(:start) { ScratchPad << :one }
+ start_two = double("two")
+ allow(start_two).to receive(:start) { ScratchPad << :two }
+ MSpec.register :start, start_one
+ MSpec.register :start, start_two
+ end
+
+ it "does not attempt to run any actions if none have been registered" do
+ MSpec.store :finish, nil
+ expect { MSpec.actions :finish }.not_to raise_error
+ end
+
+ it "runs each action registered as a start action" do
+ MSpec.actions :start
+ expect(ScratchPad.recorded).to eq([:one, :two])
+ end
+end
+
+RSpec.describe MSpec, ".mode?" do
+ before :each do
+ MSpec.clear_modes
+ end
+
+ it "returns true if the mode has been set" do
+ expect(MSpec.mode?(:verify)).to eq(false)
+ MSpec.register_mode :verify
+ expect(MSpec.mode?(:verify)).to eq(true)
+ end
+end
+
+RSpec.describe MSpec, ".clear_modes" do
+ it "clears all registered modes" do
+ MSpec.register_mode(:pretend)
+ MSpec.register_mode(:verify)
+
+ expect(MSpec.mode?(:pretend)).to eq(true)
+ expect(MSpec.mode?(:verify)).to eq(true)
+
+ MSpec.clear_modes
+
+ expect(MSpec.mode?(:pretend)).to eq(false)
+ expect(MSpec.mode?(:verify)).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".guarded?" do
+ before :each do
+ MSpec.instance_variable_set :@guarded, []
+ end
+
+ it "returns false if no guard has run" do
+ expect(MSpec.guarded?).to eq(false)
+ end
+
+ it "returns true if a single guard has run" do
+ MSpec.guard
+ expect(MSpec.guarded?).to eq(true)
+ end
+
+ it "returns true if more than one guard has run" do
+ MSpec.guard
+ MSpec.guard
+ expect(MSpec.guarded?).to eq(true)
+ end
+
+ it "returns true until all guards have finished" do
+ MSpec.guard
+ MSpec.guard
+ expect(MSpec.guarded?).to eq(true)
+ MSpec.unguard
+ expect(MSpec.guarded?).to eq(true)
+ MSpec.unguard
+ expect(MSpec.guarded?).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".describe" do
+ before :each do
+ MSpec.clear_current
+ @cs = ContextState.new ""
+ allow(ContextState).to receive(:new).and_return(@cs)
+ allow(MSpec).to receive(:current).and_return(nil)
+ allow(MSpec).to receive(:register_current)
+ end
+
+ it "creates a new ContextState for the block" do
+ expect(ContextState).to receive(:new).and_return(@cs)
+ MSpec.describe(Object) { }
+ end
+
+ it "accepts an optional second argument" do
+ expect(ContextState).to receive(:new).and_return(@cs)
+ MSpec.describe(Object, "msg") { }
+ end
+
+ it "registers the newly created ContextState" do
+ expect(MSpec).to receive(:register_current).with(@cs).twice
+ MSpec.describe(Object) { }
+ end
+
+ it "invokes the ContextState#describe method" do
+ expect(@cs).to receive(:describe)
+ MSpec.describe(Object, "msg") {}
+ end
+end
+
+RSpec.describe MSpec, ".process" do
+ before :each do
+ allow(MSpec).to receive(:files)
+ MSpec.store :start, []
+ MSpec.store :finish, []
+ allow(STDOUT).to receive(:puts)
+ end
+
+ it "prints the RUBY_DESCRIPTION" do
+ expect(STDOUT).to receive(:puts).with(RUBY_DESCRIPTION)
+ MSpec.process
+ end
+
+ it "calls all start actions" do
+ start = double("start")
+ allow(start).to receive(:start) { ScratchPad.record :start }
+ MSpec.register :start, start
+ MSpec.process
+ expect(ScratchPad.recorded).to eq(:start)
+ end
+
+ it "calls all finish actions" do
+ finish = double("finish")
+ allow(finish).to receive(:finish) { ScratchPad.record :finish }
+ MSpec.register :finish, finish
+ MSpec.process
+ expect(ScratchPad.recorded).to eq(:finish)
+ end
+
+ it "calls the files method" do
+ expect(MSpec).to receive(:files)
+ MSpec.process
+ end
+end
+
+RSpec.describe MSpec, ".files" do
+ before :each do
+ MSpec.store :load, []
+ MSpec.store :unload, []
+ MSpec.register_files [:one, :two, :three]
+ allow(Kernel).to receive(:load)
+ end
+
+ it "calls load actions before each file" do
+ load = double("load")
+ allow(load).to receive(:load) { ScratchPad.record :load }
+ MSpec.register :load, load
+ MSpec.files
+ expect(ScratchPad.recorded).to eq(:load)
+ end
+
+ it "shuffles the file list if .randomize? is true" do
+ MSpec.randomize = true
+ expect(MSpec).to receive(:shuffle)
+ MSpec.files
+ MSpec.randomize = false
+ end
+
+ it "registers the current file" do
+ load = double("load")
+ files = []
+ allow(load).to receive(:load) { files << MSpec.file }
+ MSpec.register :load, load
+ MSpec.files
+ expect(files).to eq([:one, :two, :three])
+ end
+end
+
+RSpec.describe MSpec, ".shuffle" do
+ before :each do
+ @base = (0..100).to_a
+ @list = @base.clone
+ MSpec.shuffle @list
+ end
+
+ it "does not alter the elements in the list" do
+ @base.each do |elt|
+ expect(@list).to include(elt)
+ end
+ end
+
+ it "changes the order of the list" do
+ # obviously, this spec has a certain probability
+ # of failing. If it fails, run it again.
+ expect(@list).not_to eq(@base)
+ end
+end
+
+RSpec.describe MSpec, ".tags_file" do
+ before :each do
+ MSpec.store :file, "path/to/spec/something/some_spec.rb"
+ MSpec.store :tags_patterns, nil
+ end
+
+ it "returns the default tags file for the current spec file" do
+ expect(MSpec.tags_file).to eq("path/to/spec/tags/something/some_tags.txt")
+ end
+
+ it "returns the tags file for the current spec file with custom tags_patterns" do
+ MSpec.register_tags_patterns [[/^(.*)\/spec/, '\1/tags'], [/_spec.rb/, "_tags.txt"]]
+ expect(MSpec.tags_file).to eq("path/to/tags/something/some_tags.txt")
+ end
+
+ it "performs multiple substitutions" do
+ MSpec.register_tags_patterns [
+ [%r(/spec/something/), "/spec/other/"],
+ [%r(/spec/), "/spec/tags/"],
+ [/_spec.rb/, "_tags.txt"]
+ ]
+ expect(MSpec.tags_file).to eq("path/to/spec/tags/other/some_tags.txt")
+ end
+
+ it "handles cases where no substitution is performed" do
+ MSpec.register_tags_patterns [[/nothing/, "something"]]
+ expect(MSpec.tags_file).to eq("path/to/spec/something/some_spec.rb")
+ end
+end
+
+RSpec.describe MSpec, ".read_tags" do
+ before :each do
+ allow(MSpec).to receive(:tags_file).and_return(File.dirname(__FILE__) + '/tags.txt')
+ end
+
+ it "returns a list of tag instances for matching tag names found" do
+ one = SpecTag.new "fail(broken):Some#method? works"
+ expect(MSpec.read_tags(["fail", "pass"])).to eq([one])
+ end
+
+ it "returns [] if no tags names match" do
+ expect(MSpec.read_tags("super")).to eq([])
+ end
+end
+
+RSpec.describe MSpec, ".read_tags" do
+ before :each do
+ @tag = SpecTag.new "fails:Some#method"
+ File.open(tmp("tags.txt", false), "w") do |f|
+ f.puts ""
+ f.puts @tag
+ f.puts ""
+ end
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ end
+
+ it "does not return a tag object for empty lines" do
+ expect(MSpec.read_tags(["fails"])).to eq([@tag])
+ end
+end
+
+RSpec.describe MSpec, ".write_tags" do
+ before :each do
+ FileUtils.cp File.dirname(__FILE__) + "/tags.txt", tmp("tags.txt", false)
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ @tag1 = SpecTag.new "check(broken):Tag#rewrite works"
+ @tag2 = SpecTag.new "broken:Tag#write_tags fails"
+ end
+
+ after :all do
+ rm_r tmp("tags.txt", false)
+ end
+
+ it "overwrites the tags in the tag file" do
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():\"Multi-line\\ntext\\ntag\"
+])
+ MSpec.write_tags [@tag1, @tag2]
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[check(broken):Tag#rewrite works
+broken:Tag#write_tags fails
+])
+ end
+end
+
+RSpec.describe MSpec, ".write_tag" do
+ before :each do
+ allow(FileUtils).to receive(:mkdir_p)
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ @tag = SpecTag.new "fail(broken):Some#method works"
+ end
+
+ after :all do
+ rm_r tmp("tags.txt", false)
+ end
+
+ it "writes a tag to the tags file for the current spec file" do
+ MSpec.write_tag @tag
+ expect(IO.read(tmp("tags.txt", false))).to eq("fail(broken):Some#method works\n")
+ end
+
+ it "does not write a duplicate tag" do
+ File.open(tmp("tags.txt", false), "w") { |f| f.puts @tag }
+ MSpec.write_tag @tag
+ expect(IO.read(tmp("tags.txt", false))).to eq("fail(broken):Some#method works\n")
+ end
+end
+
+RSpec.describe MSpec, ".delete_tag" do
+ before :each do
+ FileUtils.cp File.dirname(__FILE__) + "/tags.txt", tmp("tags.txt", false)
+ allow(MSpec).to receive(:tags_file).and_return(tmp("tags.txt", false))
+ @tag = SpecTag.new "fail(Comments don't matter):Some#method? works"
+ end
+
+ after :each do
+ rm_r tmp("tags.txt", false)
+ end
+
+ it "deletes the tag if it exists" do
+ expect(MSpec.delete_tag(@tag)).to eq(true)
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():\"Multi-line\\ntext\\ntag\"
+])
+ end
+
+ it "deletes a tag with escaped newlines" do
+ expect(MSpec.delete_tag(SpecTag.new('extended:"Multi-line\ntext\ntag"'))).to eq(true)
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+])
+ end
+
+ it "does not change the tags file contents if the tag doesn't exist" do
+ @tag.tag = "failed"
+ expect(MSpec.delete_tag(@tag)).to eq(false)
+ expect(IO.read(tmp("tags.txt", false))).to eq(%[fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():\"Multi-line\\ntext\\ntag\"
+])
+ end
+
+ it "deletes the tag file if it is empty" do
+ expect(MSpec.delete_tag(@tag)).to eq(true)
+ expect(MSpec.delete_tag(SpecTag.new("incomplete:The#best method ever"))).to eq(true)
+ expect(MSpec.delete_tag(SpecTag.new("benchmark:The#fastest method today"))).to eq(true)
+ expect(MSpec.delete_tag(SpecTag.new('extended:"Multi-line\ntext\ntag"'))).to eq(true)
+ expect(File.exist?(tmp("tags.txt", false))).to eq(false)
+ end
+end
+
+RSpec.describe MSpec, ".delete_tags" do
+ before :each do
+ @tags = tmp("tags.txt", false)
+ FileUtils.cp File.dirname(__FILE__) + "/tags.txt", @tags
+ allow(MSpec).to receive(:tags_file).and_return(@tags)
+ end
+
+ it "deletes the tag file" do
+ MSpec.delete_tags
+ expect(File.exist?(@tags)).to be_falsey
+ end
+end
+
+RSpec.describe MSpec, ".expectation" do
+ it "sets the flag that an expectation has been reported" do
+ MSpec.clear_expectations
+ expect(MSpec.expectation?).to be_falsey
+ MSpec.expectation
+ expect(MSpec.expectation?).to be_truthy
+ end
+end
+
+RSpec.describe MSpec, ".expectation?" do
+ it "returns true if an expectation has been reported" do
+ MSpec.expectation
+ expect(MSpec.expectation?).to be_truthy
+ end
+
+ it "returns false if an expectation has not been reported" do
+ MSpec.clear_expectations
+ expect(MSpec.expectation?).to be_falsey
+ end
+end
+
+RSpec.describe MSpec, ".clear_expectations" do
+ it "clears the flag that an expectation has been reported" do
+ MSpec.expectation
+ expect(MSpec.expectation?).to be_truthy
+ MSpec.clear_expectations
+ expect(MSpec.expectation?).to be_falsey
+ end
+end
+
+RSpec.describe MSpec, ".register_shared" do
+ it "stores a shared ContextState by description" do
+ parent = ContextState.new "container"
+ state = ContextState.new "shared"
+ state.parent = parent
+ prc = lambda { }
+ state.describe(&prc)
+ MSpec.register_shared(state)
+ expect(MSpec.retrieve(:shared)["shared"]).to eq(state)
+ end
+end
+
+RSpec.describe MSpec, ".retrieve_shared" do
+ it "retrieves the shared ContextState matching description" do
+ state = ContextState.new ""
+ MSpec.retrieve(:shared)["shared"] = state
+ expect(MSpec.retrieve_shared(:shared)).to eq(state)
+ end
+end
diff --git a/spec/mspec/spec/runner/shared_spec.rb b/spec/mspec/spec/runner/shared_spec.rb
new file mode 100644
index 0000000000..153b8f0698
--- /dev/null
+++ b/spec/mspec/spec/runner/shared_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+require 'mspec/runner/shared'
+require 'mspec/runner/context'
+require 'mspec/runner/example'
+
+RSpec.describe Object, "#it_behaves_like" do
+ before :each do
+ ScratchPad.clear
+
+ MSpec.setup_env
+
+ @state = ContextState.new "Top level"
+ @state.instance_variable_set :@parsed, true
+ @state.singleton_class.send(:public, :it_behaves_like)
+
+ @shared = ContextState.new :shared_spec, :shared => true
+ allow(MSpec).to receive(:retrieve_shared).and_return(@shared)
+ end
+
+ it "creates @method set to the name of the aliased method" do
+ @shared.it("an example") { ScratchPad.record @method }
+ @state.it_behaves_like :shared_spec, :some_method
+ @state.process
+ expect(ScratchPad.recorded).to eq(:some_method)
+ end
+
+ it "creates @object if the passed object" do
+ object = Object.new
+ @shared.it("an example") { ScratchPad.record @object }
+ @state.it_behaves_like :shared_spec, :some_method, object
+ @state.process
+ expect(ScratchPad.recorded).to eq(object)
+ end
+
+ it "creates @object if the passed false" do
+ object = false
+ @shared.it("an example") { ScratchPad.record @object }
+ @state.it_behaves_like :shared_spec, :some_method, object
+ @state.process
+ expect(ScratchPad.recorded).to eq(object)
+ end
+
+ it "sends :it_should_behave_like" do
+ expect(@state).to receive(:it_should_behave_like)
+ @state.it_behaves_like :shared_spec, :some_method
+ end
+
+ describe "with multiple shared contexts" do
+ before :each do
+ @obj = Object.new
+ @obj2 = Object.new
+
+ @state2 = ContextState.new "Second top level"
+ @state2.instance_variable_set :@parsed, true
+ @state2.singleton_class.send(:public, :it_behaves_like)
+ end
+
+ it "ensures the shared spec state is distinct" do
+ @shared.it("an example") { ScratchPad.record [@method, @object] }
+
+ @state.it_behaves_like :shared_spec, :some_method, @obj
+
+ @state.process
+ expect(ScratchPad.recorded).to eq([:some_method, @obj])
+
+ @state2.it_behaves_like :shared_spec, :another_method, @obj2
+
+ @state2.process
+ expect(ScratchPad.recorded).to eq([:another_method, @obj2])
+ end
+
+ it "ensures the shared spec state is distinct for nested shared specs" do
+ nested = ContextState.new "nested context"
+ nested.instance_variable_set :@parsed, true
+ nested.parent = @shared
+
+ nested.it("another example") { ScratchPad.record [:shared, @method, @object] }
+
+ @state.it_behaves_like :shared_spec, :some_method, @obj
+
+ @state.process
+ expect(ScratchPad.recorded).to eq([:shared, :some_method, @obj])
+
+ @state2.it_behaves_like :shared_spec, :another_method, @obj2
+
+ @state2.process
+ expect(ScratchPad.recorded).to eq([:shared, :another_method, @obj2])
+ end
+ end
+end
diff --git a/spec/mspec/spec/runner/tag_spec.rb b/spec/mspec/spec/runner/tag_spec.rb
new file mode 100644
index 0000000000..bda9ac4280
--- /dev/null
+++ b/spec/mspec/spec/runner/tag_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+require 'mspec/runner/tag'
+
+RSpec.describe SpecTag do
+ it "accepts an optional string to parse into fields" do
+ tag = SpecTag.new "tag(comment):description"
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq("comment")
+ expect(tag.description).to eq("description")
+ end
+end
+
+RSpec.describe SpecTag, "#parse" do
+ before :each do
+ @tag = SpecTag.new
+ end
+
+ it "accepts 'tag(comment):description'" do
+ @tag.parse "tag(I'm real):Some#method returns a value"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("I'm real")
+ expect(@tag.description).to eq("Some#method returns a value")
+ end
+
+ it "accepts 'tag:description'" do
+ @tag.parse "tag:Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag():description'" do
+ @tag.parse "tag():Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag:'" do
+ @tag.parse "tag:"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq("")
+ end
+
+ it "accepts 'tag(bug:555):Another#method'" do
+ @tag.parse "tag(bug:555):Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("bug:555")
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag(http://someplace.com/neato):Another#method'" do
+ @tag.parse "tag(http://someplace.com/neato):Another#method"
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("http://someplace.com/neato")
+ expect(@tag.description).to eq("Another#method")
+ end
+
+ it "accepts 'tag(comment):\"Multi-line\\ntext\"'" do
+ @tag.parse 'tag(comment):"Multi-line\ntext"'
+ expect(@tag.tag).to eq("tag")
+ expect(@tag.comment).to eq("comment")
+ expect(@tag.description).to eq("Multi-line\ntext")
+ end
+
+ it "ignores '#anything'" do
+ @tag.parse "# this could be a comment"
+ expect(@tag.tag).to eq(nil)
+ expect(@tag.comment).to eq(nil)
+ expect(@tag.description).to eq(nil)
+ end
+end
+
+RSpec.describe SpecTag, "#to_s" do
+ it "formats itself as 'tag(comment):description'" do
+ txt = "tag(comment):description"
+ tag = SpecTag.new txt
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq("comment")
+ expect(tag.description).to eq("description")
+ expect(tag.to_s).to eq(txt)
+ end
+
+ it "formats itself as 'tag:description" do
+ txt = "tag:description"
+ tag = SpecTag.new txt
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq(nil)
+ expect(tag.description).to eq("description")
+ expect(tag.to_s).to eq(txt)
+ end
+
+ it "formats itself as 'tag(comment):\"multi-line\\ntext\\ntag\"'" do
+ txt = 'tag(comment):"multi-line\ntext\ntag"'
+ tag = SpecTag.new txt
+ expect(tag.tag).to eq("tag")
+ expect(tag.comment).to eq("comment")
+ expect(tag.description).to eq("multi-line\ntext\ntag")
+ expect(tag.to_s).to eq(txt)
+ end
+end
+
+RSpec.describe SpecTag, "#==" do
+ it "returns true if the tags have the same fields" do
+ one = SpecTag.new "tag(this):unicorn"
+ two = SpecTag.new "tag(this):unicorn"
+ expect(one.==(two)).to eq(true)
+ expect([one].==([two])).to eq(true)
+ end
+end
+
+RSpec.describe SpecTag, "#unescape" do
+ it "replaces \\n by LF when the description is quoted" do
+ tag = SpecTag.new 'tag:"desc with\nnew line"'
+ expect(tag.description).to eq("desc with\nnew line")
+ end
+
+ it "does not replaces \\n by LF when the description is not quoted " do
+ tag = SpecTag.new 'tag:desc with\nnew line'
+ expect(tag.description).to eq("desc with\\nnew line")
+ end
+end
diff --git a/spec/mspec/spec/runner/tags.txt b/spec/mspec/spec/runner/tags.txt
new file mode 100644
index 0000000000..f4eb6ad034
--- /dev/null
+++ b/spec/mspec/spec/runner/tags.txt
@@ -0,0 +1,4 @@
+fail(broken):Some#method? works
+incomplete(20%):The#best method ever
+benchmark(0.01825):The#fastest method today
+extended():"Multi-line\ntext\ntag"
diff --git a/spec/mspec/spec/spec_helper.rb b/spec/mspec/spec/spec_helper.rb
new file mode 100644
index 0000000000..5cabfe5626
--- /dev/null
+++ b/spec/mspec/spec/spec_helper.rb
@@ -0,0 +1,70 @@
+RSpec.configure do |config|
+ config.disable_monkey_patching!
+ config.raise_errors_for_deprecations!
+end
+
+require 'mspec'
+
+# Remove this when MRI has intelligent warnings
+$VERBOSE = nil unless $VERBOSE
+
+class MOSConfig < Hash
+ def initialize
+ self[:loadpath] = []
+ self[:requires] = []
+ self[:flags] = []
+ self[:options] = []
+ self[:includes] = []
+ self[:excludes] = []
+ self[:patterns] = []
+ self[:xpatterns] = []
+ self[:tags] = []
+ self[:xtags] = []
+ self[:atags] = []
+ self[:astrings] = []
+ self[:target] = 'ruby'
+ self[:command] = nil
+ self[:ltags] = []
+ self[:files] = []
+ self[:launch] = []
+ end
+end
+
+def new_option
+ config = MOSConfig.new
+ return MSpecOptions.new("spec", 20, config), config
+end
+
+# Just to have an exception name output not be "Exception"
+class MSpecExampleError < Exception
+end
+
+def hide_deprecation_warnings
+ allow(MSpec).to receive(:deprecate)
+end
+
+def run_mspec(command, args)
+ cwd = Dir.pwd
+ command = " #{command}" unless command.start_with?('-')
+ cmd = "#{cwd}/bin/mspec#{command} -B spec/fixtures/config.mspec #{args}"
+ out = `#{cmd} 2>&1`
+ ret = $?
+ out = out.sub(/\A\$.+\n/, '') # Remove printed command line
+ out = out.sub(RUBY_DESCRIPTION, "RUBY_DESCRIPTION")
+ out = out.gsub(/\d+\.\d{6}/, "D.DDDDDD") # Specs total time
+ out = out.gsub(/\d{2}:\d{2}:\d{2}/, "00:00:00") # Progress bar time
+ out = out.gsub(cwd, "CWD")
+ return out, ret
+end
+
+def ensure_mspec_method(method)
+ file, _line = method.source_location
+ expect(file).to start_with(File.expand_path('../../lib/mspec', __FILE__ ))
+end
+
+PublicMSpecMatchers = Class.new {
+ include MSpecMatchers
+ public :raise_error
+}.new
+
+BACKTRACE_QUOTE = RUBY_VERSION >= "3.4" ? "'" : "`"
diff --git a/spec/mspec/spec/utils/deprecate_spec.rb b/spec/mspec/spec/utils/deprecate_spec.rb
new file mode 100644
index 0000000000..73eaf7d04e
--- /dev/null
+++ b/spec/mspec/spec/utils/deprecate_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+require 'mspec/utils/deprecate'
+
+RSpec.describe MSpec, "#deprecate" do
+ it "warns when using a deprecated method" do
+ warning = nil
+ allow($stderr).to receive(:puts) { |str| warning = str }
+ MSpec.deprecate(:some_method, :other_method)
+ expect(warning).to start_with(<<-EOS.chomp)
+
+some_method is deprecated, use other_method instead.
+from
+EOS
+ expect(warning).to include(__FILE__)
+ expect(warning).to include('8')
+ end
+end
diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises.rb b/spec/mspec/spec/utils/fixtures/this_file_raises.rb
new file mode 100644
index 0000000000..8e37a587bf
--- /dev/null
+++ b/spec/mspec/spec/utils/fixtures/this_file_raises.rb
@@ -0,0 +1 @@
+raise "This is a BAD file"
diff --git a/spec/mspec/spec/utils/fixtures/this_file_raises2.rb b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb
new file mode 100644
index 0000000000..8efc10199a
--- /dev/null
+++ b/spec/mspec/spec/utils/fixtures/this_file_raises2.rb
@@ -0,0 +1 @@
+raise "This is a BAD file 2"
diff --git a/spec/mspec/spec/utils/name_map_spec.rb b/spec/mspec/spec/utils/name_map_spec.rb
new file mode 100644
index 0000000000..a42dc9ffec
--- /dev/null
+++ b/spec/mspec/spec/utils/name_map_spec.rb
@@ -0,0 +1,187 @@
+require 'spec_helper'
+require 'mspec/utils/name_map'
+
+module NameMapSpecs
+ class A
+ A = self
+
+ def self.a; end
+ def a; end
+ def c; end
+
+ class B
+ def b; end
+ end
+ end
+
+ class Error
+ end
+
+ class Fixnum
+ def f; end
+ end
+
+ autoload :BadFile, "#{__dir__}/fixtures/this_file_raises.rb"
+ autoload :BadFile2, "#{__dir__}/fixtures/this_file_raises2.rb"
+
+ def self.n; end
+ def n; end
+end
+
+RSpec.describe NameMap, "#exception?" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "returns true if the constant is Errno" do
+ expect(@map.exception?("Errno")).to eq(true)
+ end
+
+ it "returns true if the constant is a kind of Exception" do
+ expect(@map.exception?("Errno::EBADF")).to eq(true)
+ expect(@map.exception?("LoadError")).to eq(true)
+ expect(@map.exception?("SystemExit")).to eq(true)
+ end
+
+ it "returns false if the constant is not a kind of Exception" do
+ expect(@map.exception?("NameMapSpecs::Error")).to eq(false)
+ expect(@map.exception?("NameMapSpecs")).to eq(false)
+ end
+
+ it "returns false if the constant does not exist" do
+ expect(@map.exception?("Nonexistent")).to eq(false)
+ end
+end
+
+RSpec.describe NameMap, "#class_or_module" do
+ before :each do
+ @map = NameMap.new true
+ end
+
+ it "returns the constant specified by the string" do
+ expect(@map.class_or_module("NameMapSpecs")).to eq(NameMapSpecs)
+ end
+
+ it "returns the constant specified by the 'A::B' string" do
+ expect(@map.class_or_module("NameMapSpecs::A")).to eq(NameMapSpecs::A)
+ end
+
+ it "returns nil if the constant is not a class or module" do
+ expect(@map.class_or_module("Float::MAX")).to eq(nil)
+ end
+
+ it "returns nil if the constant is in the set of excluded constants" do
+ excluded = %w[
+ MSpecScript
+ MkSpec
+ NameMap
+ ]
+
+ excluded.each do |const|
+ expect(@map.class_or_module(const)).to eq(nil)
+ end
+ end
+
+ it "returns nil if the constant does not exist" do
+ expect(@map.class_or_module("Heaven")).to eq(nil)
+ expect(@map.class_or_module("Hell")).to eq(nil)
+ expect(@map.class_or_module("Bush::Brain")).to eq(nil)
+ end
+
+ it "returns nil if accessing the constant raises RuntimeError" do
+ expect { NameMapSpecs::BadFile }.to raise_error(RuntimeError)
+ expect(@map.class_or_module("NameMapSpecs::BadFile")).to eq(nil)
+ end
+
+ it "returns nil if accessing the constant raises RuntimeError when not triggering the autoload before" do
+ expect(@map.class_or_module("NameMapSpecs::BadFile2")).to eq(nil)
+ end
+end
+
+RSpec.describe NameMap, "#dir_name" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "returns a directory name from the base name and constant" do
+ expect(@map.dir_name("NameMapSpecs", 'spec/core')).to eq('spec/core/namemapspecs')
+ end
+
+ it "returns a directory name from the components in the constants name" do
+ expect(@map.dir_name("NameMapSpecs::A", 'spec')).to eq('spec/namemapspecs/a')
+ expect(@map.dir_name("NameMapSpecs::A::B", 'spec')).to eq('spec/namemapspecs/a/b')
+ end
+
+ it "returns a directory name without 'class' for constants like TrueClass" do
+ expect(@map.dir_name("TrueClass", 'spec')).to eq('spec/true')
+ expect(@map.dir_name("FalseClass", 'spec')).to eq('spec/false')
+ end
+
+ it "returns 'exception' for the directory name of any Exception subclass" do
+ expect(@map.dir_name("SystemExit", 'spec')).to eq('spec/exception')
+ expect(@map.dir_name("Errno::EBADF", 'spec')).to eq('spec/exception')
+ end
+
+ it "returns 'class' for Class" do
+ expect(@map.dir_name("Class", 'spec')).to eq('spec/class')
+ end
+end
+
+# These specs do not cover all the mappings, but only describe how the
+# name is derived when the hash item maps to a single value, a hash with
+# a specific item, or a hash with a :default item.
+RSpec.describe NameMap, "#file_name" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "returns the name of the spec file based on the constant and method" do
+ expect(@map.file_name("[]=", "Array")).to eq("element_set_spec.rb")
+ end
+
+ it "returns the name of the spec file based on the special entry for the method" do
+ expect(@map.file_name("~", "Regexp")).to eq("match_spec.rb")
+ expect(@map.file_name("~", "Integer")).to eq("complement_spec.rb")
+ end
+
+ it "returns the name of the spec file based on the default entry for the method" do
+ expect(@map.file_name("<<", "NameMapSpecs")).to eq("append_spec.rb")
+ end
+
+ it "uses the last component of the constant to look up the method name" do
+ expect(@map.file_name("^", "NameMapSpecs::Integer")).to eq("bit_xor_spec.rb")
+ end
+end
+
+RSpec.describe NameMap, "#namespace" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "prepends the module to the constant name" do
+ expect(@map.namespace("SubModule", Integer)).to eq("SubModule::Integer")
+ end
+
+ it "does not prepend Object, Class, or Module to the constant name" do
+ expect(@map.namespace("Object", String)).to eq("String")
+ expect(@map.namespace("Module", Integer)).to eq("Integer")
+ expect(@map.namespace("Class", Float)).to eq("Float")
+ end
+end
+
+RSpec.describe NameMap, "#map" do
+ before :each do
+ @map = NameMap.new
+ end
+
+ it "flattens an object hierarchy into a single Hash" do
+ expect(@map.map({}, [NameMapSpecs])).to eq({
+ "NameMapSpecs." => ["n"],
+ "NameMapSpecs#" => ["n"],
+ "NameMapSpecs::A." => ["a"],
+ "NameMapSpecs::A#" => ["a", "c"],
+ "NameMapSpecs::A::B#" => ["b"],
+ "NameMapSpecs::Fixnum#" => ["f"]
+ })
+ end
+end
diff --git a/spec/mspec/spec/utils/options_spec.rb b/spec/mspec/spec/utils/options_spec.rb
new file mode 100644
index 0000000000..2e3925f579
--- /dev/null
+++ b/spec/mspec/spec/utils/options_spec.rb
@@ -0,0 +1,1302 @@
+require 'spec_helper'
+require 'mspec/utils/options'
+require 'mspec/version'
+require 'mspec/guards/guard'
+require 'mspec/runner/mspec'
+require 'mspec/runner/formatters'
+
+RSpec.describe MSpecOption, ".new" do
+ before :each do
+ @opt = MSpecOption.new("-a", "--bdc", "ARG", "desc", :block)
+ end
+
+ it "sets the short attribute" do
+ expect(@opt.short).to eq("-a")
+ end
+
+ it "sets the long attribute" do
+ expect(@opt.long).to eq("--bdc")
+ end
+
+ it "sets the arg attribute" do
+ expect(@opt.arg).to eq("ARG")
+ end
+
+ it "sets the description attribute" do
+ expect(@opt.description).to eq("desc")
+ end
+
+ it "sets the block attribute" do
+ expect(@opt.block).to eq(:block)
+ end
+end
+
+RSpec.describe MSpecOption, "#arg?" do
+ it "returns true if arg attribute is not nil" do
+ expect(MSpecOption.new(nil, nil, "ARG", nil, nil).arg?).to be_truthy
+ end
+
+ it "returns false if arg attribute is nil" do
+ expect(MSpecOption.new(nil, nil, nil, nil, nil).arg?).to be_falsey
+ end
+end
+
+RSpec.describe MSpecOption, "#match?" do
+ before :each do
+ @opt = MSpecOption.new("-a", "--bdc", "ARG", "desc", :block)
+ end
+
+ it "returns true if the argument matches the short option" do
+ expect(@opt.match?("-a")).to be_truthy
+ end
+
+ it "returns true if the argument matches the long option" do
+ expect(@opt.match?("--bdc")).to be_truthy
+ end
+
+ it "returns false if the argument matches neither the short nor long option" do
+ expect(@opt.match?("-b")).to be_falsey
+ expect(@opt.match?("-abdc")).to be_falsey
+ end
+end
+
+RSpec.describe MSpecOptions, ".new" do
+ before :each do
+ @opt = MSpecOptions.new("cmd", 20, :config)
+ end
+
+ it "sets the banner attribute" do
+ expect(@opt.banner).to eq("cmd")
+ end
+
+ it "sets the config attribute" do
+ expect(@opt.config).to eq(:config)
+ end
+
+ it "sets the width attribute" do
+ expect(@opt.width).to eq(20)
+ end
+
+ it "sets the default width attribute" do
+ expect(MSpecOptions.new.width).to eq(30)
+ end
+end
+
+RSpec.describe MSpecOptions, "#on" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "adds a short option" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a short option taking an argument" do
+ expect(@opt).to receive(:add).with("-a", nil, "ARG", "desc", nil)
+ @opt.on("-a", "ARG", "desc")
+ end
+
+ it "adds a long option" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a long option taking an argument" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a short and long option" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "adds a short and long option taking an argument" do
+ expect(@opt).to receive(:add).with("-a", nil, nil, "desc", nil)
+ @opt.on("-a", "desc")
+ end
+
+ it "raises MSpecOptions::OptionError if pass less than 2 arguments" do
+ expect { @opt.on }.to raise_error(MSpecOptions::OptionError)
+ expect { @opt.on "" }.to raise_error(MSpecOptions::OptionError)
+ end
+end
+
+RSpec.describe MSpecOptions, "#add" do
+ before :each do
+ @opt = MSpecOptions.new "cmd", 20
+ @prc = lambda { }
+ end
+
+ it "adds documentation for an option" do
+ expect(@opt).to receive(:doc).with(" -t, --typo ARG Correct typo ARG")
+ @opt.add("-t", "--typo", "ARG", "Correct typo ARG", @prc)
+ end
+
+ it "leaves spaces in the documentation for a missing short option" do
+ expect(@opt).to receive(:doc).with(" --typo ARG Correct typo ARG")
+ @opt.add(nil, "--typo", "ARG", "Correct typo ARG", @prc)
+ end
+
+ it "handles a short option with argument but no long argument" do
+ expect(@opt).to receive(:doc).with(" -t ARG Correct typo ARG")
+ @opt.add("-t", nil, "ARG", "Correct typo ARG", @prc)
+ end
+
+ it "registers an option" do
+ option = MSpecOption.new "-t", "--typo", "ARG", "Correct typo ARG", @prc
+ expect(MSpecOption).to receive(:new).with(
+ "-t", "--typo", "ARG", "Correct typo ARG", @prc).and_return(option)
+ @opt.add("-t", "--typo", "ARG", "Correct typo ARG", @prc)
+ expect(@opt.options).to eq([option])
+ end
+end
+
+RSpec.describe MSpecOptions, "#match?" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "returns the MSpecOption instance matching the argument" do
+ @opt.on "-a", "--abdc", "desc"
+ option = @opt.match? "-a"
+ expect(@opt.match?("--abdc")).to be(option)
+ expect(option).to be_kind_of(MSpecOption)
+ expect(option.short).to eq("-a")
+ expect(option.long).to eq("--abdc")
+ expect(option.description).to eq("desc")
+ end
+end
+
+RSpec.describe MSpecOptions, "#process" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "calls the on_extra block if the argument does not match any option" do
+ @opt.on_extra { ScratchPad.record :extra }
+ @opt.process ["-a"], "-a", "-a", nil
+ expect(ScratchPad.recorded).to eq(:extra)
+ end
+
+ it "returns the matching option" do
+ @opt.on "-a", "ARG", "desc"
+ option = @opt.process [], "-a", "-a", "ARG"
+ expect(option).to be_kind_of(MSpecOption)
+ expect(option.short).to eq("-a")
+ expect(option.arg).to eq("ARG")
+ expect(option.description).to eq("desc")
+ end
+
+ it "raises an MSpecOptions::ParseError if arg is nil and there are no more entries in argv" do
+ @opt.on "-a", "ARG", "desc"
+ expect { @opt.process [], "-a", "-a", nil }.to raise_error(MSpecOptions::ParseError)
+ end
+
+ it "fetches the argument for the option from argv if arg is nil" do
+ @opt.on("-a", "ARG", "desc") { |o| ScratchPad.record o }
+ @opt.process ["ARG"], "-a", "-a", nil
+ expect(ScratchPad.recorded).to eq("ARG")
+ end
+
+ it "calls the option's block" do
+ @opt.on("-a", "ARG", "desc") { ScratchPad.record :option }
+ @opt.process [], "-a", "-a", "ARG"
+ expect(ScratchPad.recorded).to eq(:option)
+ end
+
+ it "does not call the option's block if it is nil" do
+ @opt.on "-a", "ARG", "desc"
+ expect { @opt.process [], "-a", "-a", "ARG" }.not_to raise_error
+ end
+end
+
+RSpec.describe MSpecOptions, "#split" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "breaks a string at the nth character" do
+ opt, arg, rest = @opt.split "-bdc", 2
+ expect(opt).to eq("-b")
+ expect(arg).to eq("dc")
+ expect(rest).to eq("dc")
+ end
+
+ it "returns nil for arg if there are no characters left" do
+ opt, arg, rest = @opt.split "-b", 2
+ expect(opt).to eq("-b")
+ expect(arg).to eq(nil)
+ expect(rest).to eq("")
+ end
+end
+
+RSpec.describe MSpecOptions, "#parse" do
+ before :each do
+ @opt = MSpecOptions.new
+ @prc = lambda { ScratchPad.record :parsed }
+ @arg_prc = lambda { |o| ScratchPad.record [:parsed, o] }
+ ScratchPad.clear
+ end
+
+ it "parses a short option" do
+ @opt.on "-a", "desc", &@prc
+ @opt.parse ["-a"]
+ expect(ScratchPad.recorded).to eq(:parsed)
+ end
+
+ it "parse a long option" do
+ @opt.on "--abdc", "desc", &@prc
+ @opt.parse ["--abdc"]
+ expect(ScratchPad.recorded).to eq(:parsed)
+ end
+
+ it "parses a short option group" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ @opt.parse ["-a", "ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a short option with an argument" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ @opt.parse ["-a", "ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a short option with connected argument" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ @opt.parse ["-aARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a long option with an argument" do
+ @opt.on "--abdc", "ARG", "desc", &@arg_prc
+ @opt.parse ["--abdc", "ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a long option with an '=' argument" do
+ @opt.on "--abdc", "ARG", "desc", &@arg_prc
+ @opt.parse ["--abdc=ARG"]
+ expect(ScratchPad.recorded).to eq([:parsed, "ARG"])
+ end
+
+ it "parses a short option group with the final option taking an argument" do
+ ScratchPad.record []
+ @opt.on("-a", "desc") { |o| ScratchPad << :a }
+ @opt.on("-b", "ARG", "desc") { |o| ScratchPad << [:b, o] }
+ @opt.parse ["-ab", "ARG"]
+ expect(ScratchPad.recorded).to eq([:a, [:b, "ARG"]])
+ end
+
+ it "parses a short option group with a connected argument" do
+ ScratchPad.record []
+ @opt.on("-a", "desc") { |o| ScratchPad << :a }
+ @opt.on("-b", "ARG", "desc") { |o| ScratchPad << [:b, o] }
+ @opt.on("-c", "desc") { |o| ScratchPad << :c }
+ @opt.parse ["-acbARG"]
+ expect(ScratchPad.recorded).to eq([:a, :c, [:b, "ARG"]])
+ end
+
+ it "returns the unprocessed entries" do
+ @opt.on "-a", "ARG", "desc", &@arg_prc
+ expect(@opt.parse(["abdc", "-a", "ilny"])).to eq(["abdc"])
+ end
+
+ it "calls the on_extra handler with unrecognized options" do
+ ScratchPad.record []
+ @opt.on_extra { |o| ScratchPad << o }
+ @opt.on "-a", "desc"
+ @opt.parse ["-a", "-b"]
+ expect(ScratchPad.recorded).to eq(["-b"])
+ end
+
+ it "does not attempt to call the block if it is nil" do
+ @opt.on "-a", "ARG", "desc"
+ expect(@opt.parse(["-a", "ARG"])).to eq([])
+ end
+
+ it "raises MSpecOptions::ParseError if passed an unrecognized option" do
+ expect(@opt).to receive(:raise).with(MSpecOptions::ParseError, an_instance_of(String))
+ allow(@opt).to receive(:puts)
+ allow(@opt).to receive(:exit)
+ @opt.parse "-u"
+ end
+end
+
+RSpec.describe MSpecOptions, "#banner=" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "sets the banner attribute" do
+ expect(@opt.banner).to eq("")
+ @opt.banner = "banner"
+ expect(@opt.banner).to eq("banner")
+ end
+end
+
+RSpec.describe MSpecOptions, "#width=" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "sets the width attribute" do
+ expect(@opt.width).to eq(30)
+ @opt.width = 20
+ expect(@opt.width).to eq(20)
+ end
+end
+
+RSpec.describe MSpecOptions, "#config=" do
+ before :each do
+ @opt = MSpecOptions.new
+ end
+
+ it "sets the config attribute" do
+ expect(@opt.config).to be_nil
+ @opt.config = :config
+ expect(@opt.config).to eq(:config)
+ end
+end
+
+RSpec.describe MSpecOptions, "#doc" do
+ before :each do
+ @opt = MSpecOptions.new "command"
+ end
+
+ it "adds text to be displayed with #to_s" do
+ @opt.doc "Some message"
+ @opt.doc "Another message"
+ expect(@opt.to_s).to eq <<-EOD
+command
+
+Some message
+Another message
+EOD
+ end
+end
+
+RSpec.describe MSpecOptions, "#version" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "installs a basic -v, --version option" do
+ expect(@opt).to receive(:puts)
+ expect(@opt).to receive(:exit)
+ @opt.version "1.0.0"
+ @opt.parse "-v"
+ end
+
+ it "accepts a block instead of using the default block" do
+ @opt.version("1.0.0") { |o| ScratchPad.record :version }
+ @opt.parse "-v"
+ expect(ScratchPad.recorded).to eq(:version)
+ end
+end
+
+RSpec.describe MSpecOptions, "#help" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "installs a basic -h, --help option" do
+ expect(@opt).to receive(:puts)
+ expect(@opt).to receive(:exit).with(1)
+ @opt.help
+ @opt.parse "-h"
+ end
+
+ it "accepts a block instead of using the default block" do
+ @opt.help { |o| ScratchPad.record :help }
+ @opt.parse "-h"
+ expect(ScratchPad.recorded).to eq(:help)
+ end
+end
+
+RSpec.describe MSpecOptions, "#on_extra" do
+ before :each do
+ @opt = MSpecOptions.new
+ ScratchPad.clear
+ end
+
+ it "registers a block to be called when an option is not recognized" do
+ @opt.on_extra { ScratchPad.record :extra }
+ @opt.parse "-g"
+ expect(ScratchPad.recorded).to eq(:extra)
+ end
+end
+
+RSpec.describe MSpecOptions, "#to_s" do
+ before :each do
+ @opt = MSpecOptions.new "command"
+ end
+
+ it "returns the banner and descriptive strings for all registered options" do
+ @opt.on "-t", "--this ARG", "Adds this ARG to the list"
+ expect(@opt.to_s).to eq <<-EOD
+command
+
+ -t, --this ARG Adds this ARG to the list
+EOD
+ end
+end
+
+RSpec.describe "The -B, --config FILE option" do
+ before :each do
+ @options, @config = new_option
+ end
+
+ it "is enabled with #configure { }" do
+ expect(@options).to receive(:on).with("-B", "--config", "FILE",
+ an_instance_of(String))
+ @options.configure {}
+ end
+
+ it "calls the passed block" do
+ ["-B", "--config"].each do |opt|
+ ScratchPad.clear
+
+ @options.configure { |x| ScratchPad.record x }
+ @options.parse [opt, "file"]
+ expect(ScratchPad.recorded).to eq("file")
+ end
+ end
+end
+
+RSpec.describe "The -C, --chdir DIR option" do
+ before :each do
+ @options, @config = new_option
+ @options.chdir
+ end
+
+ it "is enabled with #chdir" do
+ expect(@options).to receive(:on).with("-C", "--chdir", "DIR",
+ an_instance_of(String))
+ @options.chdir
+ end
+
+ it "changes the working directory to DIR" do
+ expect(Dir).to receive(:chdir).with("dir").twice
+ ["-C", "--chdir"].each do |opt|
+ @options.parse [opt, "dir"]
+ end
+ end
+end
+
+RSpec.describe "The --prefix STR option" do
+ before :each do
+ @options, @config = new_option
+ end
+
+ it "is enabled with #prefix" do
+ expect(@options).to receive(:on).with("--prefix", "STR",
+ an_instance_of(String))
+ @options.prefix
+ end
+
+ it "sets the prefix config value" do
+ @options.prefix
+ @options.parse ["--prefix", "some/dir"]
+ expect(@config[:prefix]).to eq("some/dir")
+ end
+end
+
+RSpec.describe "The -t, --target TARGET option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-t", "--target", "TARGET",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "sets the target to 'ruby' and flags to verbose with TARGET 'r' or 'ruby'" do
+ ["-t", "--target"].each do |opt|
+ ["r", "ruby"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("ruby")
+ end
+ end
+ end
+
+ it "sets the target to 'jruby' with TARGET 'j' or 'jruby'" do
+ ["-t", "--target"].each do |opt|
+ ["j", "jruby"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("jruby")
+ end
+ end
+ end
+
+ it "sets the target to 'shotgun/rubinius' with TARGET 'x' or 'rubinius'" do
+ ["-t", "--target"].each do |opt|
+ ["x", "rubinius"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("./bin/rbx")
+ end
+ end
+ end
+
+ it "set the target to 'rbx' with TARGET 'rbx'" do
+ ["-t", "--target"].each do |opt|
+ ["X", "rbx"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("rbx")
+ end
+ end
+ end
+
+ it "sets the target to 'maglev' with TARGET 'm' or 'maglev'" do
+ ["-t", "--target"].each do |opt|
+ ["m", "maglev"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("maglev-ruby")
+ end
+ end
+ end
+
+ it "sets the target to 'topaz' with TARGET 't' or 'topaz'" do
+ ["-t", "--target"].each do |opt|
+ ["t", "topaz"].each do |t|
+ @config[:target] = nil
+ @options.parse [opt, t]
+ expect(@config[:target]).to eq("topaz")
+ end
+ end
+ end
+
+ it "sets the target to TARGET" do
+ ["-t", "--target"].each do |opt|
+ @config[:target] = nil
+ @options.parse [opt, "whateva"]
+ expect(@config[:target]).to eq("whateva")
+ end
+ end
+end
+
+RSpec.describe "The -T, --target-opt OPT option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-T", "--target-opt", "OPT",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "adds OPT to flags" do
+ ["-T", "--target-opt"].each do |opt|
+ @config[:flags].delete "--whateva"
+ @options.parse [opt, "--whateva"]
+ expect(@config[:flags]).to include("--whateva")
+ end
+ end
+end
+
+RSpec.describe "The -I, --include DIR option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-I", "--include", "DIR",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "add DIR to the load path" do
+ ["-I", "--include"].each do |opt|
+ @config[:loadpath].delete "-Ipackage"
+ @options.parse [opt, "package"]
+ expect(@config[:loadpath]).to include("-Ipackage")
+ end
+ end
+end
+
+RSpec.describe "The -r, --require LIBRARY option" do
+ before :each do
+ @options, @config = new_option
+ @options.targets
+ end
+
+ it "is enabled with #targets" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-r", "--require", "LIBRARY",
+ an_instance_of(String))
+ @options.targets
+ end
+
+ it "adds LIBRARY to the requires list" do
+ ["-r", "--require"].each do |opt|
+ @config[:requires].delete "-rlibrick"
+ @options.parse [opt, "librick"]
+ expect(@config[:requires]).to include("-rlibrick")
+ end
+ end
+end
+
+RSpec.describe "The -f, --format FORMAT option" do
+ before :each do
+ @options, @config = new_option
+ @options.formatters
+ end
+
+ it "is enabled with #formatters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-f", "--format", "FORMAT",
+ an_instance_of(String))
+ @options.formatters
+ end
+
+ it "sets the SpecdocFormatter with FORMAT 's' or 'specdoc'" do
+ ["-f", "--format"].each do |opt|
+ ["s", "specdoc"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(SpecdocFormatter)
+ end
+ end
+ end
+
+ it "sets the HtmlFormatter with FORMAT 'h' or 'html'" do
+ ["-f", "--format"].each do |opt|
+ ["h", "html"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(HtmlFormatter)
+ end
+ end
+ end
+
+ it "sets the DottedFormatter with FORMAT 'd', 'dot' or 'dotted'" do
+ ["-f", "--format"].each do |opt|
+ ["d", "dot", "dotted"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(DottedFormatter)
+ end
+ end
+ end
+
+ it "sets the DescribeFormatter with FORMAT 'b' or 'describe'" do
+ ["-f", "--format"].each do |opt|
+ ["b", "describe"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(DescribeFormatter)
+ end
+ end
+ end
+
+ it "sets the FileFormatter with FORMAT 'f', 'file'" do
+ ["-f", "--format"].each do |opt|
+ ["f", "file"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(FileFormatter)
+ end
+ end
+ end
+
+ it "sets the UnitdiffFormatter with FORMAT 'u', 'unit', or 'unitdiff'" do
+ ["-f", "--format"].each do |opt|
+ ["u", "unit", "unitdiff"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(UnitdiffFormatter)
+ end
+ end
+ end
+
+ it "sets the SummaryFormatter with FORMAT 'm' or 'summary'" do
+ ["-f", "--format"].each do |opt|
+ ["m", "summary"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(SummaryFormatter)
+ end
+ end
+ end
+
+ it "sets the SpinnerFormatter with FORMAT 'a', '*', or 'spin'" do
+ ["-f", "--format"].each do |opt|
+ ["a", "*", "spin"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(SpinnerFormatter)
+ end
+ end
+ end
+
+ it "sets the MethodFormatter with FORMAT 't' or 'method'" do
+ ["-f", "--format"].each do |opt|
+ ["t", "method"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(MethodFormatter)
+ end
+ end
+ end
+
+ it "sets the YamlFormatter with FORMAT 'y' or 'yaml'" do
+ ["-f", "--format"].each do |opt|
+ ["y", "yaml"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(YamlFormatter)
+ end
+ end
+ end
+
+ it "sets the JUnitFormatter with FORMAT 'j' or 'junit'" do
+ ["-f", "--format"].each do |opt|
+ ["j", "junit"].each do |f|
+ @config[:formatter] = nil
+ @options.parse [opt, f]
+ expect(@config[:formatter]).to eq(JUnitFormatter)
+ end
+ end
+ end
+end
+
+RSpec.describe "The -o, --output FILE option" do
+ before :each do
+ @options, @config = new_option
+ @options.formatters
+ end
+
+ it "is enabled with #formatters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-o", "--output", "FILE",
+ an_instance_of(String))
+ @options.formatters
+ end
+
+ it "sets the output to FILE" do
+ ["-o", "--output"].each do |opt|
+ @config[:output] = nil
+ @options.parse [opt, "some/file"]
+ expect(@config[:output]).to eq("some/file")
+ end
+ end
+end
+
+RSpec.describe "The -e, --example STR" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-e", "--example", "STR",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds STR to the includes list" do
+ ["-e", "--example"].each do |opt|
+ @config[:includes] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:includes]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -E, --exclude STR" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-E", "--exclude", "STR",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds STR to the excludes list" do
+ ["-E", "--exclude"].each do |opt|
+ @config[:excludes] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:excludes]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -p, --pattern PATTERN" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-p", "--pattern", "PATTERN",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds PATTERN to the included patterns list" do
+ ["-p", "--pattern"].each do |opt|
+ @config[:patterns] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:patterns]).to include(/this spec/)
+ end
+ end
+end
+
+RSpec.describe "The -P, --excl-pattern PATTERN" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-P", "--excl-pattern", "PATTERN",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds PATTERN to the excluded patterns list" do
+ ["-P", "--excl-pattern"].each do |opt|
+ @config[:xpatterns] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:xpatterns]).to include(/this spec/)
+ end
+ end
+end
+
+RSpec.describe "The -g, --tag TAG" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-g", "--tag", "TAG",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds TAG to the included tags list" do
+ ["-g", "--tag"].each do |opt|
+ @config[:tags] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:tags]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -G, --excl-tag TAG" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-G", "--excl-tag", "TAG",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds TAG to the excluded tags list" do
+ ["-G", "--excl-tag"].each do |opt|
+ @config[:xtags] = []
+ @options.parse [opt, "this spec"]
+ expect(@config[:xtags]).to include("this spec")
+ end
+ end
+end
+
+RSpec.describe "The -w, --profile FILE option" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-w", "--profile", "FILE",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds FILE to the included profiles list" do
+ ["-w", "--profile"].each do |opt|
+ @config[:profiles] = []
+ @options.parse [opt, "spec/profiles/rails.yaml"]
+ expect(@config[:profiles]).to include("spec/profiles/rails.yaml")
+ end
+ end
+end
+
+RSpec.describe "The -W, --excl-profile FILE option" do
+ before :each do
+ @options, @config = new_option
+ @options.filters
+ end
+
+ it "is enabled with #filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-W", "--excl-profile", "FILE",
+ an_instance_of(String))
+ @options.filters
+ end
+
+ it "adds FILE to the excluded profiles list" do
+ ["-W", "--excl-profile"].each do |opt|
+ @config[:xprofiles] = []
+ @options.parse [opt, "spec/profiles/rails.yaml"]
+ expect(@config[:xprofiles]).to include("spec/profiles/rails.yaml")
+ end
+ end
+end
+
+RSpec.describe "The -Z, --dry-run option" do
+ before :each do
+ @options, @config = new_option
+ @options.pretend
+ end
+
+ it "is enabled with #pretend" do
+ expect(@options).to receive(:on).with("-Z", "--dry-run", an_instance_of(String))
+ @options.pretend
+ end
+
+ it "registers the MSpec pretend mode" do
+ expect(MSpec).to receive(:register_mode).with(:pretend).twice
+ ["-Z", "--dry-run"].each do |opt|
+ @options.parse opt
+ end
+ end
+end
+
+RSpec.describe "The --unguarded option" do
+ before :each do
+ @options, @config = new_option
+ @options.unguarded
+ end
+
+ it "is enabled with #unguarded" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--unguarded", an_instance_of(String))
+ @options.unguarded
+ end
+
+ it "registers the MSpec unguarded mode" do
+ expect(MSpec).to receive(:register_mode).with(:unguarded)
+ @options.parse "--unguarded"
+ end
+end
+
+RSpec.describe "The --no-ruby_guard option" do
+ before :each do
+ @options, @config = new_option
+ @options.unguarded
+ end
+
+ it "is enabled with #unguarded" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--no-ruby_bug", an_instance_of(String))
+ @options.unguarded
+ end
+
+ it "registers the MSpec no_ruby_bug mode" do
+ expect(MSpec).to receive(:register_mode).with(:no_ruby_bug)
+ @options.parse "--no-ruby_bug"
+ end
+end
+
+RSpec.describe "The -H, --random option" do
+ before :each do
+ @options, @config = new_option
+ @options.randomize
+ end
+
+ it "is enabled with #randomize" do
+ expect(@options).to receive(:on).with("-H", "--random", an_instance_of(String))
+ @options.randomize
+ end
+
+ it "registers the MSpec randomize mode" do
+ expect(MSpec).to receive(:randomize=).twice
+ ["-H", "--random"].each do |opt|
+ @options.parse opt
+ end
+ end
+end
+
+RSpec.describe "The -R, --repeat option" do
+ before :each do
+ @options, @config = new_option
+ @options.repeat
+ end
+
+ it "is enabled with #repeat" do
+ expect(@options).to receive(:on).with("-R", "--repeat", "NUMBER", an_instance_of(String))
+ @options.repeat
+ end
+
+ it "registers the MSpec repeat mode" do
+ ["-R", "--repeat"].each do |opt|
+ MSpec.repeat = 1
+ @options.parse [opt, "10"]
+ repeat_count = 0
+ MSpec.repeat do
+ repeat_count += 1
+ end
+ expect(repeat_count).to eq(10)
+ end
+ end
+end
+
+RSpec.describe "The -V, --verbose option" do
+ before :each do
+ @options, @config = new_option
+ @options.verbose
+ end
+
+ it "is enabled with #verbose" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-V", "--verbose", an_instance_of(String))
+ @options.verbose
+ end
+
+ it "registers a verbose output object with MSpec" do
+ expect(MSpec).to receive(:register).with(:start, anything()).twice
+ expect(MSpec).to receive(:register).with(:load, anything()).twice
+ ["-V", "--verbose"].each do |opt|
+ @options.parse opt
+ end
+ end
+end
+
+RSpec.describe "The -m, --marker MARKER option" do
+ before :each do
+ @options, @config = new_option
+ @options.verbose
+ end
+
+ it "is enabled with #verbose" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-m", "--marker", "MARKER",
+ an_instance_of(String))
+ @options.verbose
+ end
+
+ it "registers a marker output object with MSpec" do
+ expect(MSpec).to receive(:register).with(:load, anything()).twice
+ ["-m", "--marker"].each do |opt|
+ @options.parse [opt, ","]
+ end
+ end
+end
+
+RSpec.describe "The --int-spec option" do
+ before :each do
+ @options, @config = new_option
+ @options.interrupt
+ end
+
+ it "is enabled with #interrupt" do
+ expect(@options).to receive(:on).with("--int-spec", an_instance_of(String))
+ @options.interrupt
+ end
+
+ it "sets the abort config option to false to only abort the running spec with ^C" do
+ @config[:abort] = true
+ @options.parse "--int-spec"
+ expect(@config[:abort]).to eq(false)
+ end
+end
+
+RSpec.describe "The -Y, --verify option" do
+ before :each do
+ @options, @config = new_option
+ @options.verify
+ end
+
+ it "is enabled with #interrupt" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-Y", "--verify", an_instance_of(String))
+ @options.verify
+ end
+
+ it "sets the MSpec mode to :verify" do
+ expect(MSpec).to receive(:register_mode).with(:verify).twice
+ ["-Y", "--verify"].each do |m|
+ @options.parse m
+ end
+ end
+end
+
+RSpec.describe "The -O, --report option" do
+ before :each do
+ @options, @config = new_option
+ @options.verify
+ end
+
+ it "is enabled with #interrupt" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-O", "--report", an_instance_of(String))
+ @options.verify
+ end
+
+ it "sets the MSpec mode to :report" do
+ expect(MSpec).to receive(:register_mode).with(:report).twice
+ ["-O", "--report"].each do |m|
+ @options.parse m
+ end
+ end
+end
+
+RSpec.describe "The --report-on GUARD option" do
+ before :each do
+ allow(MSpec).to receive(:register_mode)
+
+ @options, @config = new_option
+ @options.verify
+
+ SpecGuard.clear_guards
+ end
+
+ after :each do
+ SpecGuard.clear_guards
+ end
+
+ it "is enabled with #interrupt" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("--report-on", "GUARD", an_instance_of(String))
+ @options.verify
+ end
+
+ it "sets the MSpec mode to :report_on" do
+ expect(MSpec).to receive(:register_mode).with(:report_on)
+ @options.parse ["--report-on", "ruby_bug"]
+ end
+
+ it "converts the guard name to a symbol" do
+ name = double("ruby_bug")
+ expect(name).to receive(:to_sym)
+ @options.parse ["--report-on", name]
+ end
+
+ it "saves the name of the guard" do
+ @options.parse ["--report-on", "ruby_bug"]
+ expect(SpecGuard.guards).to eq([:ruby_bug])
+ end
+end
+
+RSpec.describe "The -K, --action-tag TAG option" do
+ before :each do
+ @options, @config = new_option
+ @options.action_filters
+ end
+
+ it "is enabled with #action_filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-K", "--action-tag", "TAG",
+ an_instance_of(String))
+ @options.action_filters
+ end
+
+ it "adds TAG to the list of tags that trigger actions" do
+ ["-K", "--action-tag"].each do |opt|
+ @config[:atags] = []
+ @options.parse [opt, "action-tag"]
+ expect(@config[:atags]).to include("action-tag")
+ end
+ end
+end
+
+RSpec.describe "The -S, --action-string STR option" do
+ before :each do
+ @options, @config = new_option
+ @options.action_filters
+ end
+
+ it "is enabled with #action_filters" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-S", "--action-string", "STR",
+ an_instance_of(String))
+ @options.action_filters
+ end
+
+ it "adds STR to the list of spec descriptions that trigger actions" do
+ ["-S", "--action-string"].each do |opt|
+ @config[:astrings] = []
+ @options.parse [opt, "action-str"]
+ expect(@config[:astrings]).to include("action-str")
+ end
+ end
+end
+
+RSpec.describe "The -d, --debug option" do
+ before :each do
+ @options, @config = new_option
+ @options.debug
+ end
+
+ after :each do
+ $MSPEC_DEBUG = nil
+ end
+
+ it "is enabled with #debug" do
+ allow(@options).to receive(:on)
+ expect(@options).to receive(:on).with("-d", "--debug", an_instance_of(String))
+ @options.debug
+ end
+
+ it "sets $MSPEC_DEBUG to true" do
+ ["-d", "--debug"].each do |opt|
+ expect($MSPEC_DEBUG).not_to be_truthy
+ @options.parse opt
+ expect($MSPEC_DEBUG).to be_truthy
+ $MSPEC_DEBUG = nil
+ end
+ end
+end
+
+RSpec.describe "MSpecOptions#all" do
+ it "includes all options" do
+ meth = MSpecOptions.instance_method(:all)
+ file, line = meth.source_location
+ contents = File.read(file)
+ lines = contents.lines
+
+ from = line
+ to = from
+ to += 1 until /^\s*end\s*$/ =~ lines[to]
+ calls = lines[from...to].map(&:strip)
+
+ option_methods = contents.scan(/def (\w+).*\n\s*on\(/).map(&:first)
+ option_methods[0].sub!("configure", "configure {}")
+
+ expect(calls).to eq(option_methods)
+ end
+end
diff --git a/spec/mspec/spec/utils/script_spec.rb b/spec/mspec/spec/utils/script_spec.rb
new file mode 100644
index 0000000000..c35bda8b47
--- /dev/null
+++ b/spec/mspec/spec/utils/script_spec.rb
@@ -0,0 +1,470 @@
+require 'spec_helper'
+require 'mspec/utils/script'
+require 'mspec/runner/mspec'
+require 'mspec/runner/filters'
+require 'mspec/runner/actions/filter'
+
+RSpec.describe MSpecScript, ".config" do
+ it "returns a Hash" do
+ expect(MSpecScript.config).to be_kind_of(Hash)
+ end
+end
+
+RSpec.describe MSpecScript, ".set" do
+ it "sets the config hash key, value" do
+ MSpecScript.set :a, 10
+ expect(MSpecScript.config[:a]).to eq(10)
+ end
+end
+
+RSpec.describe MSpecScript, ".get" do
+ it "gets the config hash value for a key" do
+ MSpecScript.set :a, 10
+ expect(MSpecScript.get(:a)).to eq(10)
+ end
+end
+
+RSpec.describe MSpecScript, "#config" do
+ it "returns the MSpecScript config hash" do
+ MSpecScript.set :b, 5
+ expect(MSpecScript.new.config[:b]).to eq(5)
+ end
+
+ it "returns the MSpecScript config hash from subclasses" do
+ class MSSClass < MSpecScript; end
+ MSpecScript.set :b, 5
+ expect(MSSClass.new.config[:b]).to eq(5)
+ end
+end
+
+RSpec.describe MSpecScript, "#load_default" do
+ before :all do
+ @verbose = $VERBOSE
+ $VERBOSE = nil
+ end
+
+ after :all do
+ $VERBOSE = @verbose
+ end
+
+ before :each do
+ @version = RUBY_VERSION
+ if Object.const_defined? :RUBY_ENGINE
+ @engine = Object.const_get :RUBY_ENGINE
+ end
+ @script = MSpecScript.new
+ allow(MSpecScript).to receive(:new).and_return(@script)
+ end
+
+ after :each do
+ Object.const_set :RUBY_VERSION, @version
+ Object.const_set :RUBY_ENGINE, @engine if @engine
+ end
+
+ it "attempts to load 'default.mspec'" do
+ allow(@script).to receive(:try_load)
+ expect(@script).to receive(:try_load).with('default.mspec').and_return(true)
+ @script.load_default
+ end
+
+ it "attempts to load a config file based on RUBY_ENGINE and RUBY_VERSION" do
+ Object.const_set :RUBY_ENGINE, "ybur"
+ Object.const_set :RUBY_VERSION, "1.8.9"
+ default = "ybur.1.8.mspec"
+ expect(@script).to receive(:try_load).with('default.mspec').and_return(false)
+ expect(@script).to receive(:try_load).with(default)
+ expect(@script).to receive(:try_load).with('ybur.mspec')
+ @script.load_default
+ end
+end
+
+RSpec.describe MSpecScript, ".main" do
+ before :each do
+ @script = double("MSpecScript").as_null_object
+ allow(MSpecScript).to receive(:new).and_return(@script)
+ # Do not require full mspec as it would conflict with RSpec
+ expect(MSpecScript).to receive(:require).with('mspec')
+ end
+
+ it "creates an instance of MSpecScript" do
+ expect(MSpecScript).to receive(:new).and_return(@script)
+ MSpecScript.main
+ end
+
+ it "attempts to load the default config" do
+ expect(@script).to receive(:load_default)
+ MSpecScript.main
+ end
+
+ it "calls the #options method on the script" do
+ expect(@script).to receive(:options)
+ MSpecScript.main
+ end
+
+ it "calls the #signals method on the script" do
+ expect(@script).to receive(:signals)
+ MSpecScript.main
+ end
+
+ it "calls the #register method on the script" do
+ expect(@script).to receive(:register)
+ MSpecScript.main
+ end
+
+ it "calls the #setup_env method on the script" do
+ expect(@script).to receive(:setup_env)
+ MSpecScript.main
+ end
+
+ it "calls the #run method on the script" do
+ expect(@script).to receive(:run)
+ MSpecScript.main
+ end
+end
+
+RSpec.describe MSpecScript, "#initialize" do
+ before :each do
+ @config = MSpecScript.new.config
+ end
+
+ it "sets the default config values" do
+ expect(@config[:formatter]).to eq(nil)
+ expect(@config[:includes]).to eq([])
+ expect(@config[:excludes]).to eq([])
+ expect(@config[:patterns]).to eq([])
+ expect(@config[:xpatterns]).to eq([])
+ expect(@config[:tags]).to eq([])
+ expect(@config[:xtags]).to eq([])
+ expect(@config[:atags]).to eq([])
+ expect(@config[:astrings]).to eq([])
+ expect(@config[:abort]).to eq(true)
+ expect(@config[:config_ext]).to eq('.mspec')
+ end
+end
+
+RSpec.describe MSpecScript, "#load" do
+ before :each do
+ allow(File).to receive(:exist?).and_return(false)
+ @script = MSpecScript.new
+ @file = "default.mspec"
+ @base = "default"
+ end
+
+ it "attempts to locate the file through the expanded path name" do
+ expect(File).to receive(:expand_path).with(@file, ".").and_return(@file)
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(Kernel).to receive(:load).with(@file).and_return(:loaded)
+ expect(@script.load(@file)).to eq(:loaded)
+ end
+
+ it "appends config[:config_ext] to the name and attempts to locate the file through the expanded path name" do
+ expect(File).to receive(:expand_path).with(@base, ".").and_return(@base)
+ expect(File).to receive(:expand_path).with(@base, "spec").and_return(@base)
+ expect(File).to receive(:expand_path).with(@file, ".").and_return(@file)
+ expect(File).to receive(:exist?).with(@base).and_return(false)
+ expect(File).to receive(:exist?).with(@file).and_return(true)
+ expect(Kernel).to receive(:load).with(@file).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ end
+
+ it "attempts to locate the file in '.'" do
+ path = File.expand_path @file, "."
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@file)).to eq(:loaded)
+ end
+
+ it "appends config[:config_ext] to the name and attempts to locate the file in '.'" do
+ path = File.expand_path @file, "."
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ end
+
+ it "attempts to locate the file in 'spec'" do
+ path = File.expand_path @file, "spec"
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@file)).to eq(:loaded)
+ end
+
+ it "appends config[:config_ext] to the name and attempts to locate the file in 'spec'" do
+ path = File.expand_path @file, "spec"
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).with(path).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ end
+
+ it "loads a given file only once" do
+ path = File.expand_path @file, "spec"
+ expect(File).to receive(:exist?).with(path).and_return(true)
+ expect(Kernel).to receive(:load).once.with(path).and_return(:loaded)
+ expect(@script.load(@base)).to eq(:loaded)
+ expect(@script.load(@base)).to eq(true)
+ end
+end
+
+RSpec.describe MSpecScript, "#custom_options" do
+ before :each do
+ @script = MSpecScript.new
+ end
+
+ after :each do
+ end
+
+ it "prints 'None'" do
+ options = double("options")
+ expect(options).to receive(:doc).with(" No custom options registered")
+ @script.custom_options options
+ end
+end
+
+RSpec.describe MSpecScript, "#register" do
+ before :each do
+ @script = MSpecScript.new
+
+ @formatter = double("formatter").as_null_object
+ @script.config[:formatter] = @formatter
+ end
+
+ it "creates and registers the formatter" do
+ expect(@formatter).to receive(:new).and_return(@formatter)
+ expect(@formatter).to receive(:register)
+ @script.register
+ end
+
+ it "does not register the formatter if config[:formatter] is false" do
+ @script.config[:formatter] = false
+ @script.register
+ end
+
+ it "calls #custom_register" do
+ expect(@script).to receive(:custom_register)
+ @script.register
+ end
+
+ it "registers :formatter with the formatter instance" do
+ allow(@formatter).to receive(:new).and_return(@formatter)
+ @script.register
+ expect(MSpec.formatter).to be(@formatter)
+ end
+
+ it "does not register :formatter if config[:formatter] is false" do
+ @script.config[:formatter] = false
+ expect(MSpec).not_to receive(:store)
+ @script.register
+ end
+end
+
+RSpec.describe MSpecScript, "#register" do
+ before :each do
+ @script = MSpecScript.new
+
+ @formatter = double("formatter").as_null_object
+ @script.config[:formatter] = @formatter
+
+ @filter = double("filter")
+ expect(@filter).to receive(:register)
+
+ @ary = ["some", "spec"]
+ end
+
+ it "creates and registers a MatchFilter for include specs" do
+ expect(MatchFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:includes] = @ary
+ @script.register
+ end
+
+ it "creates and registers a MatchFilter for excluded specs" do
+ expect(MatchFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:excludes] = @ary
+ @script.register
+ end
+
+ it "creates and registers a RegexpFilter for include specs" do
+ expect(RegexpFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:patterns] = @ary
+ @script.register
+ end
+
+ it "creates and registers a RegexpFilter for excluded specs" do
+ expect(RegexpFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:xpatterns] = @ary
+ @script.register
+ end
+
+ it "creates and registers a TagFilter for include specs" do
+ expect(TagFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:tags] = @ary
+ @script.register
+ end
+
+ it "creates and registers a TagFilter for excluded specs" do
+ expect(TagFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:xtags] = @ary
+ @script.register
+ end
+
+ it "creates and registers a ProfileFilter for include specs" do
+ expect(ProfileFilter).to receive(:new).with(:include, *@ary).and_return(@filter)
+ @script.config[:profiles] = @ary
+ @script.register
+ end
+
+ it "creates and registers a ProfileFilter for excluded specs" do
+ expect(ProfileFilter).to receive(:new).with(:exclude, *@ary).and_return(@filter)
+ @script.config[:xprofiles] = @ary
+ @script.register
+ end
+end
+
+RSpec.describe MSpecScript, "#signals" do
+ before :each do
+ @script = MSpecScript.new
+ @abort = @script.config[:abort]
+ end
+
+ after :each do
+ @script.config[:abort] = @abort
+ end
+
+ it "traps the INT signal if config[:abort] is true" do
+ expect(Signal).to receive(:trap).with("INT")
+ @script.config[:abort] = true
+ @script.signals
+ end
+
+ it "does not trap the INT signal if config[:abort] is not true" do
+ expect(Signal).not_to receive(:trap).with("INT")
+ @script.config[:abort] = false
+ @script.signals
+ end
+end
+
+RSpec.describe MSpecScript, "#entries" do
+ before :each do
+ @script = MSpecScript.new
+
+ allow(File).to receive(:realpath).and_return("name")
+ allow(File).to receive(:file?).and_return(false)
+ allow(File).to receive(:directory?).and_return(false)
+ end
+
+ it "returns the pattern in an array if it is a file" do
+ expect(File).to receive(:realpath).with("file").and_return("file/expanded.rb")
+ expect(File).to receive(:file?).with("file/expanded.rb").and_return(true)
+ expect(@script.entries("file")).to eq(["file/expanded.rb"])
+ end
+
+ it "returns Dir['pattern/**/*_spec.rb'] if pattern is a directory" do
+ expect(File).to receive(:directory?).with("name").and_return(true)
+ allow(File).to receive(:realpath).and_return("name", "name/**/*_spec.rb")
+ expect(Dir).to receive(:[]).with("name/**/*_spec.rb").and_return(["dir1", "dir2"])
+ expect(@script.entries("name")).to eq(["dir1", "dir2"])
+ end
+
+ it "aborts if pattern cannot be resolved to a file nor a directory" do
+ expect(@script).to receive(:abort)
+ @script.entries("pattern")
+ end
+
+ describe "with config[:prefix] set" do
+ before :each do
+ prefix = "prefix/dir"
+ @script.config[:prefix] = prefix
+ @name = prefix + "/name"
+ end
+
+ it "returns the pattern in an array if it is a file" do
+ name = "#{@name}.rb"
+ expect(File).to receive(:realpath).with(name).and_return(name)
+ expect(File).to receive(:file?).with(name).and_return(true)
+ expect(@script.entries("name.rb")).to eq([name])
+ end
+
+ it "returns Dir['pattern/**/*_spec.rb'] if pattern is a directory" do
+ allow(File).to receive(:realpath).and_return(@name, @name+"/**/*_spec.rb")
+ expect(File).to receive(:directory?).with(@name).and_return(true)
+ expect(Dir).to receive(:[]).with(@name + "/**/*_spec.rb").and_return(["dir1", "dir2"])
+ expect(@script.entries("name")).to eq(["dir1", "dir2"])
+ end
+
+ it "aborts if pattern cannot be resolved to a file nor a directory" do
+ expect(@script).to receive(:abort)
+ @script.entries("pattern")
+ end
+ end
+end
+
+RSpec.describe MSpecScript, "#files" do
+ before :each do
+ @script = MSpecScript.new
+ end
+
+ it "accumulates the values returned by #entries" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file2"])
+ expect(@script.files(["a", "b"])).to eq(["file1", "file2"])
+ end
+
+ it "strips a leading '^' and removes the values returned by #entries" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file2"], ["file1"])
+ expect(@script.files(["a", "b", "^a"])).to eq(["file2"])
+ end
+
+ it "processes the array elements in order" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file1"], ["file2"])
+ expect(@script.files(["^a", "a", "b"])).to eq(["file1", "file2"])
+ end
+end
+
+RSpec.describe MSpecScript, "#files" do
+ before :each do
+ MSpecScript.set :files, ["file1", "file2"]
+
+ @script = MSpecScript.new
+ end
+
+ after :each do
+ MSpecScript.config.delete :files
+ end
+
+ it "looks up items with leading ':' in the config object" do
+ expect(@script).to receive(:entries).and_return(["file1"], ["file2"])
+ expect(@script.files([":files"])).to eq(["file1", "file2"])
+ end
+
+ it "aborts if the config key is not set" do
+ expect(@script).to receive(:abort).with("Key :all_files not found in mspec config.")
+ @script.files([":all_files"])
+ end
+end
+
+RSpec.describe MSpecScript, "#setup_env" do
+ before :each do
+ @script = MSpecScript.new
+ @options, @config = new_option
+ allow(@script).to receive(:config).and_return(@config)
+ end
+
+ after :each do
+ end
+
+ it "sets MSPEC_RUNNER = '1' in the environment" do
+ ENV["MSPEC_RUNNER"] = "0"
+ @script.setup_env
+ expect(ENV["MSPEC_RUNNER"]).to eq("1")
+ end
+
+ it "sets RUBY_EXE = config[:target] in the environment" do
+ ENV["RUBY_EXE"] = nil
+ @script.setup_env
+ expect(ENV["RUBY_EXE"]).to eq(@config[:target])
+ end
+
+ it "sets RUBY_FLAGS = config[:flags] in the environment" do
+ ENV["RUBY_FLAGS"] = nil
+ @config[:flags] = ["-w", "-Q"]
+ @script.setup_env
+ expect(ENV["RUBY_FLAGS"]).to eq("-w -Q")
+ end
+end
diff --git a/spec/mspec/spec/utils/version_spec.rb b/spec/mspec/spec/utils/version_spec.rb
new file mode 100644
index 0000000000..ec367d2a1e
--- /dev/null
+++ b/spec/mspec/spec/utils/version_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+require 'mspec/utils/version'
+
+RSpec.describe SpecVersion, "#to_s" do
+ it "returns the string with which it was initialized" do
+ expect(SpecVersion.new("1.8").to_s).to eq("1.8")
+ expect(SpecVersion.new("2.118.9").to_s).to eq("2.118.9")
+ end
+end
+
+RSpec.describe SpecVersion, "#to_str" do
+ it "returns the same string as #to_s" do
+ version = SpecVersion.new("2.118.9")
+ expect(version.to_str).to eq(version.to_s)
+ end
+end
+
+RSpec.describe SpecVersion, "#to_i with ceil = false" do
+ it "returns an integer representation of the version string" do
+ expect(SpecVersion.new("2.23.10").to_i).to eq(1022310)
+ end
+
+ it "replaces missing version parts with zeros" do
+ expect(SpecVersion.new("1.8").to_i).to eq(1010800)
+ expect(SpecVersion.new("1.8.6").to_i).to eq(1010806)
+ end
+end
+
+RSpec.describe SpecVersion, "#to_i with ceil = true" do
+ it "returns an integer representation of the version string" do
+ expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806)
+ end
+
+ it "fills in 9s for missing tiny values" do
+ expect(SpecVersion.new("1.8", true).to_i).to eq(1010899)
+ expect(SpecVersion.new("1.8.6", true).to_i).to eq(1010806)
+ end
+end
+
+RSpec.describe SpecVersion, "#to_int" do
+ it "returns the same value as #to_i" do
+ version = SpecVersion.new("4.16.87")
+ expect(version.to_int).to eq(version.to_i)
+ end
+end
diff --git a/spec/mspec/tool/check_require_spec_helper.rb b/spec/mspec/tool/check_require_spec_helper.rb
new file mode 100755
index 0000000000..07126e68dc
--- /dev/null
+++ b/spec/mspec/tool/check_require_spec_helper.rb
@@ -0,0 +1,34 @@
+#!/usr/bin/env ruby
+
+# This script is used to check that each *_spec.rb file has
+# a relative_require for spec_helper which should live higher
+# up in the ruby/spec repo directory tree.
+#
+# Prints errors to $stderr and returns a non-zero exit code when
+# errors are found.
+#
+# Related to https://github.com/ruby/spec/pull/992
+
+def check_file(fn)
+ File.foreach(fn) do |line|
+ return $1 if line =~ /^\s*require_relative\s*['"](.*spec_helper)['"]/
+ end
+ nil
+end
+
+rootdir = ARGV[0] || "."
+fglob = File.join(rootdir, "**", "*_spec.rb")
+specfiles = Dir.glob(fglob)
+raise "No spec files found in #{fglob.inspect}. Give an argument to specify the root-directory of ruby/spec" if specfiles.empty?
+
+errors = 0
+specfiles.sort.each do |fn|
+ result = check_file(fn)
+ if result.nil?
+ warn "Missing require_relative for *spec_helper for file: #{fn}"
+ errors += 1
+ end
+end
+
+puts "# Found #{errors} files with require_relative spec_helper issues."
+exit 1 if errors > 0
diff --git a/spec/mspec/tool/find.rb b/spec/mspec/tool/find.rb
new file mode 100755
index 0000000000..322b023f15
--- /dev/null
+++ b/spec/mspec/tool/find.rb
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+Dir.chdir('../rubyspec') do
+ regexp = Regexp.new(ARGV[0])
+ Dir.glob('**/*.rb') do |file|
+ contents = File.read(file)
+ if regexp =~ contents
+ puts file
+ end
+ end
+end
diff --git a/spec/mspec/tool/pull-latest-mspec-spec b/spec/mspec/tool/pull-latest-mspec-spec
new file mode 100755
index 0000000000..154a353e64
--- /dev/null
+++ b/spec/mspec/tool/pull-latest-mspec-spec
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+# Assumes all commits have been synchronized to https://github.com/ruby/spec
+# See spec/mspec/tool/sync/sync-rubyspec.rb
+
+function sync {
+ dir="$1"
+ repo="$2"
+ short_repo_name="ruby/$(basename "$repo" .git)"
+
+ rm -rf "$dir"
+ git clone --depth 1 "$repo" "$dir"
+ commit=$(git -C "$dir" log -n 1 --format='%h')
+ rm -rf "$dir/.git"
+
+ # Remove CI files to avoid confusion
+ rm -f "$dir/appveyor.yml"
+ rm -f "$dir/.travis.yml"
+ rm -rf "$dir/.github"
+
+ git add "$dir"
+ git commit -m "Update to ${short_repo_name}@${commit}"
+}
+
+sync spec/mspec https://github.com/ruby/mspec.git
+sync spec/ruby https://github.com/ruby/spec.git
diff --git a/spec/mspec/tool/remove_old_guards.rb b/spec/mspec/tool/remove_old_guards.rb
new file mode 100755
index 0000000000..bc5612c78d
--- /dev/null
+++ b/spec/mspec/tool/remove_old_guards.rb
@@ -0,0 +1,145 @@
+#!/usr/bin/env ruby
+
+# Removes old version guards in ruby/spec.
+# Run it from the ruby/spec repository root.
+# The argument is the new minimum supported version.
+#
+# cd spec
+# ../mspec/tool/remove_old_guards.rb <ruby-version>
+#
+# where <ruby-version> is a version guard with which should be removed
+#
+# Example:
+# tool/remove_old_guards.rb 3.1
+#
+# As a result guards like
+# ruby_version_is "3.1" do
+# # ...
+# end
+#
+# will be removed.
+
+def dedent(line)
+ if line.start_with?(" ")
+ line[2..-1]
+ else
+ line
+ end
+end
+
+def each_spec_file(&block)
+ Dir["*/**/*.rb"].each(&block)
+end
+
+def each_file(&block)
+ Dir["**/*"].each { |path|
+ yield path if File.file?(path)
+ }
+end
+
+def remove_guards(guard, keep)
+ each_spec_file do |file|
+ contents = File.binread(file)
+ if contents =~ guard
+ puts file
+ lines = contents.lines.to_a
+ while first = lines.find_index { |line| line =~ guard }
+ comment = first
+ while comment > 0 and lines[comment-1] =~ /^(\s*)#/
+ comment -= 1
+ end
+ indent = lines[first][/^(\s*)/, 1].length
+ last = (first+1...lines.size).find { |i|
+ space = lines[i][/^(\s*)end$/, 1] and space.length == indent
+ }
+ raise file unless last
+ if keep
+ lines[comment..last] = lines[first+1..last-1].map { |l| dedent(l) }
+ else
+ if comment > 0 and lines[comment-1] == "\n"
+ comment -= 1
+ elsif lines[last+1] == "\n"
+ last += 1
+ end
+ lines[comment..last] = []
+ end
+ end
+ File.binwrite file, lines.join
+ end
+ end
+end
+
+def remove_empty_files
+ each_spec_file do |file|
+ unless file.include?("fixtures/")
+ lines = File.readlines(file)
+ if lines.all? { |line| line.chomp.empty? or line.start_with?('require', '#') }
+ puts "Removing empty file #{file}"
+ File.delete(file)
+ end
+ end
+ end
+end
+
+def remove_unused_shared_specs
+ shared_groups = {}
+ # Dir["**/shared/**/*.rb"].each do |shared|
+ each_spec_file do |shared|
+ next if File.basename(shared) == 'constants.rb'
+ contents = File.binread(shared)
+ found = false
+ contents.scan(/^\s*describe (:[\w_?]+), shared: true do$/) {
+ shared_groups[$1] = 0
+ found = true
+ }
+ if !found and shared.include?('shared/') and !shared.include?('fixtures/') and !shared.end_with?('/constants.rb')
+ puts "no shared describe in #{shared} ?"
+ end
+ end
+
+ each_spec_file do |file|
+ contents = File.binread(file)
+ contents.scan(/(?:it_behaves_like|it_should_behave_like) (:[\w_?]+)[,\s]/) do
+ puts $1 unless shared_groups.key?($1)
+ shared_groups[$1] += 1
+ end
+ end
+
+ shared_groups.each_pair do |group, value|
+ if value == 0
+ puts "Shared describe #{group} seems unused"
+ elsif value == 1
+ puts "Shared describe #{group} seems used only once" if $VERBOSE
+ end
+ end
+end
+
+def search(regexp)
+ each_file do |file|
+ contents = File.binread(file)
+ if contents =~ regexp
+ puts file
+ contents.each_line do |line|
+ if line =~ regexp
+ puts line
+ end
+ end
+ end
+ end
+end
+
+abort "usage: #{$0} <ruby-version>" if ARGV.empty?
+
+version = Regexp.escape(ARGV.fetch(0))
+version += "(?:\\.0)?" if version.count(".") < 2
+remove_guards(/ruby_version_is (["'])#{version}\1 do/, true)
+remove_guards(/ruby_version_is (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, false)
+remove_guards(/ruby_bug ["']#\d+["'], (["'])[0-9.]*\1 *... *(["'])#{version}\2 do/, true)
+
+remove_empty_files
+remove_unused_shared_specs
+
+puts "Search:"
+search(/(["'])#{version}\1/)
+search(/^\s*#.+#{version}/)
+search(/RUBY_VERSION_IS_#{version.tr('.', '_')}/)
diff --git a/spec/mspec/tool/sync/.gitignore b/spec/mspec/tool/sync/.gitignore
new file mode 100644
index 0000000000..e64f1e8542
--- /dev/null
+++ b/spec/mspec/tool/sync/.gitignore
@@ -0,0 +1,4 @@
+/jruby
+/rubinius
+/ruby
+/truffleruby
diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb
new file mode 100644
index 0000000000..86c43d0dc8
--- /dev/null
+++ b/spec/mspec/tool/sync/sync-rubyspec.rb
@@ -0,0 +1,254 @@
+# This script is based on commands from the wiki:
+# https://github.com/ruby/spec/wiki/Merging-specs-from-JRuby-and-other-sources
+
+IMPLS = {
+ truffleruby: {
+ git: "https://github.com/truffleruby/truffleruby.git",
+ from_commit: "f10ab6988d",
+ },
+ jruby: {
+ git: "https://github.com/jruby/jruby.git",
+ from_commit: "f10ab6988d",
+ },
+ rbx: {
+ git: "https://github.com/rubinius/rubinius.git",
+ },
+ mri: {
+ git: "https://github.com/ruby/ruby.git",
+ },
+}
+
+MSPEC = ARGV.delete('--mspec')
+
+CHECK_LAST_MERGE = !MSPEC && ENV['CHECK_LAST_MERGE'] != 'false'
+TEST_MASTER = ENV['TEST_MASTER'] != 'false'
+
+ONLY_FILTER = ENV['ONLY_FILTER'] == 'true'
+
+MSPEC_REPO = File.expand_path("../../..", __FILE__)
+raise MSPEC_REPO if !Dir.exist?(MSPEC_REPO) or !Dir.exist?("#{MSPEC_REPO}/.git")
+
+# Assuming the rubyspec repo is a sibling of the mspec repo
+RUBYSPEC_REPO = File.expand_path("../rubyspec", MSPEC_REPO)
+raise RUBYSPEC_REPO unless Dir.exist?(RUBYSPEC_REPO)
+
+SOURCE_REPO = MSPEC ? MSPEC_REPO : RUBYSPEC_REPO
+
+# LAST_MERGE is a commit of ruby/spec or ruby/mspec
+# which is the spec/mspec commit that was last imported in the Ruby implementation
+# (i.e. the commit in "Update to ruby/spec@commit").
+# It is normally automatically computed, but can be manually set when
+# e.g. the last update of specs wasn't merged in the Ruby implementation.
+LAST_MERGE = ENV["LAST_MERGE"]
+
+NOW = Time.now
+
+BRIGHT_RED = "\e[31;1m"
+BRIGHT_YELLOW = "\e[33;1m"
+RESET = "\e[0m"
+
+# git filter-branch --subdirectory-filter works fine for our use case
+ENV['FILTER_BRANCH_SQUELCH_WARNING'] = '1'
+
+class RubyImplementation
+ attr_reader :name
+
+ def initialize(name, data)
+ @name = name.to_s
+ @data = data
+ end
+
+ def git_url
+ @data[:git]
+ end
+
+ def repo_name
+ File.basename(git_url, ".git")
+ end
+
+ def repo_path
+ "#{__dir__}/#{repo_name}"
+ end
+
+ def repo_org
+ File.basename(File.dirname(git_url))
+ end
+
+ def from_commit
+ from = @data[:from_commit]
+ "#{from}..." if from
+ end
+
+ def last_merge_message
+ message = @data[:merge_message] || "Update to ruby/spec@"
+ message.gsub!("ruby/spec", "ruby/mspec") if MSPEC
+ message
+ end
+
+ def prefix
+ MSPEC ? "spec/mspec" : "spec/ruby"
+ end
+
+ def rebased_branch
+ "#{@name}-rebased"
+ end
+end
+
+def sh(*args)
+ puts args.join(' ')
+ system(*args)
+ raise unless $?.success?
+end
+
+def branch?(name)
+ branches = `git branch`.sub('*', '').lines.map(&:strip)
+ branches.include?(name)
+end
+
+def update_repo(impl)
+ unless File.directory? impl.repo_name
+ sh "git", "clone", impl.git_url
+ end
+
+ Dir.chdir(impl.repo_name) do
+ puts Dir.pwd
+
+ sh "git", "checkout", "master"
+ sh "git", "pull"
+ end
+end
+
+def filter_commits(impl)
+ Dir.chdir(impl.repo_name) do
+ date = NOW.strftime("%F")
+ branch = "#{MSPEC ? :mspec : :specs}-#{date}"
+
+ unless branch?(branch)
+ sh "git", "checkout", "-b", branch
+ sh "git", "filter-branch", "-f", "--subdirectory-filter", impl.prefix, *impl.from_commit
+ sh "git", "push", "-f", SOURCE_REPO, "#{branch}:#{impl.name}"
+ end
+ end
+end
+
+def rebase_commits(impl)
+ Dir.chdir(SOURCE_REPO) do
+ sh "git", "checkout", "master"
+ sh "git", "pull"
+
+ rebased = impl.rebased_branch
+ if branch?(rebased)
+ last_commit = Time.at(Integer(`git log -n 1 --format='%ct' #{rebased}`))
+ days_since_last_commit = (NOW-last_commit) / 86400
+ if days_since_last_commit > 7
+ abort "#{BRIGHT_RED}#{rebased} exists but last commit is old (#{last_commit}), delete the branch if it was merged#{RESET}"
+ else
+ puts "#{BRIGHT_YELLOW}#{rebased} already exists, last commit on #{last_commit}, assuming it correct#{RESET}"
+ sh "git", "checkout", rebased
+ end
+ else
+ sh "git", "checkout", impl.name
+
+ if LAST_MERGE
+ last_merge = `git log -n 1 --format='%H %ct' #{LAST_MERGE}`
+ else
+ last_merge = `git log --grep='^#{impl.last_merge_message}' -n 1 --format='%H %ct'`
+ end
+ last_merge, commit_timestamp = last_merge.split(' ')
+
+ raise "Could not find last merge" unless last_merge
+ puts "Last merge is #{last_merge}"
+
+ commit_date = Time.at(Integer(commit_timestamp))
+ days_since_last_merge = (NOW-commit_date) / 86400
+ if CHECK_LAST_MERGE and days_since_last_merge > 60
+ raise "#{days_since_last_merge.floor} days since last merge, probably wrong commit"
+ end
+
+ puts "Checking if the last merge is consistent with upstream files"
+ rubyspec_commit = `git log -n 1 --format='%s' #{last_merge}`.chomp.split('@', 2)[-1]
+ sh "git", "checkout", last_merge
+ sh "git", "diff", "--exit-code", rubyspec_commit, "--", ":!.github"
+
+ puts "Rebasing..."
+ sh "git", "branch", "-D", rebased if branch?(rebased)
+ sh "git", "checkout", "-b", rebased, impl.name
+ sh "git", "rebase", "--onto", "master", last_merge
+ end
+ end
+end
+
+def new_commits?(impl)
+ Dir.chdir(SOURCE_REPO) do
+ diff = `git diff master #{impl.rebased_branch}`
+ !diff.empty?
+ end
+end
+
+def test_new_specs
+ require "yaml"
+ Dir.chdir(SOURCE_REPO) do
+ workflow = YAML.load_file(".github/workflows/ci.yml")
+ job_name = MSPEC ? "test" : "specs"
+ versions = workflow.dig("jobs", job_name, "strategy", "matrix", "ruby").map(&:to_s)
+ versions = versions.grep(/^\d+\./) # Test on MRI
+ min_version, max_version = versions.minmax
+
+ test_command = MSPEC ? "bundle install && bundle exec rspec" : "../mspec/bin/mspec -j"
+
+ run_test = -> version {
+ command = "chruby ruby-#{version} && #{test_command}"
+ sh ENV["SHELL"], "-c", command
+ }
+
+ run_test[min_version]
+ run_test[max_version]
+ run_test["master"] if TEST_MASTER
+ end
+end
+
+def fast_forward_master(impl)
+ Dir.chdir(SOURCE_REPO) do
+ sh "git", "checkout", "master"
+ sh "git", "merge", "--ff-only", impl.rebased_branch
+ sh "git", "branch", "--delete", impl.rebased_branch
+ end
+end
+
+def check_ci
+ puts
+ puts <<-EOS
+ Push to master, and check that the CI passes:
+ https://github.com/ruby/#{:m if MSPEC}spec/commits/master
+
+ EOS
+end
+
+def main(impls)
+ impls.each_pair do |impl, data|
+ impl = RubyImplementation.new(impl, data)
+ update_repo(impl)
+ filter_commits(impl)
+ unless ONLY_FILTER
+ rebase_commits(impl)
+ if new_commits?(impl)
+ test_new_specs
+ fast_forward_master(impl)
+ check_ci
+ else
+ STDERR.puts "#{BRIGHT_YELLOW}No new commits#{RESET}"
+ fast_forward_master(impl)
+ end
+ end
+ end
+end
+
+if ARGV == ["all"]
+ impls = IMPLS
+else
+ args = ARGV.map { |arg| arg.to_sym }
+ raise ARGV.to_s unless (args - IMPLS.keys).empty?
+ impls = IMPLS.select { |impl| args.include?(impl) }
+end
+
+main(impls)
diff --git a/spec/mspec/tool/tag_from_output.rb b/spec/mspec/tool/tag_from_output.rb
new file mode 100755
index 0000000000..41aa70f932
--- /dev/null
+++ b/spec/mspec/tool/tag_from_output.rb
@@ -0,0 +1,65 @@
+#!/usr/bin/env ruby
+
+# Adds tags based on error and failures output (e.g., from a CI log),
+# without running any spec code.
+
+tag = ENV["TAG"] || "fails"
+
+tags_dir = %w[
+ spec/tags
+ spec/tags/ruby
+].find { |dir| Dir.exist?("#{dir}/language") }
+abort 'Could not find tags directory' unless tags_dir
+
+output = ARGF.readlines
+
+# Automatically strip datetime of GitHub Actions
+if output.first =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z /
+ output = output.map { |line| line.split(' ', 2).last }
+end
+
+NUMBER = /^\d+\)$/
+ERROR_OR_FAILED = / (ERROR|FAILED)$/
+SPEC_FILE = /^((?:\/|[CD]:\/).+_spec\.rb)\:\d+/
+
+output.slice_before(NUMBER).select { |number, *rest|
+ number =~ NUMBER and rest.any? { |line| line =~ ERROR_OR_FAILED }
+}.each { |number, *rest|
+ error_line = rest.find { |line| line =~ ERROR_OR_FAILED }
+ description = error_line.match(ERROR_OR_FAILED).pre_match
+
+ spec_file = rest.find { |line| line =~ SPEC_FILE }
+ if spec_file
+ spec_file = spec_file[SPEC_FILE, 1] or raise
+ else
+ if error_line =~ /^([\w:]+)[#\.](\w+) /
+ mod, method = $1, $2
+ file = "#{mod.downcase.gsub('::', '/')}/#{method}_spec.rb"
+ spec_file = ['spec/ruby/core', 'spec/ruby/library', *Dir.glob('spec/ruby/library/*')].find { |dir|
+ path = "#{dir}/#{file}"
+ break path if File.exist?(path)
+ }
+ end
+
+ unless spec_file
+ warn "Could not find file for:\n#{error_line}"
+ next
+ end
+ end
+
+ prefix = spec_file.index('spec/ruby/') || spec_file.index('spec/truffle/')
+ spec_file = spec_file[prefix..-1]
+
+ tags_file = spec_file.sub('spec/ruby/', "#{tags_dir}/").sub('spec/truffle/', "#{tags_dir}/truffle/")
+ tags_file = tags_file.sub(/_spec\.rb$/, '_tags.txt')
+
+ dir = File.dirname(tags_file)
+ Dir.mkdir(dir) unless Dir.exist?(dir)
+
+ tag_line = "#{tag}:#{description}"
+ lines = File.exist?(tags_file) ? File.readlines(tags_file, chomp: true) : []
+ unless lines.include?(tag_line)
+ puts tags_file
+ File.write(tags_file, (lines + [tag_line]).join("\n") + "\n")
+ end
+}
diff --git a/spec/mspec/tool/wrap_with_guard.rb b/spec/mspec/tool/wrap_with_guard.rb
new file mode 100755
index 0000000000..5b1bf4d7f7
--- /dev/null
+++ b/spec/mspec/tool/wrap_with_guard.rb
@@ -0,0 +1,28 @@
+#!/usr/bin/env ruby
+# Wrap the passed the files with a guard (e.g., `ruby_version_is ""..."3.0"`).
+# Notably if some methods are removed, this is a convenient way to skip such file from a given version.
+# Example usage:
+# $ spec/mspec/tool/wrap_with_guard.rb 'ruby_version_is ""..."3.0"' spec/ruby/library/set/sortedset/**/*_spec.rb
+
+guard, *files = ARGV
+abort "Usage: #{$0} GUARD FILES..." if files.empty?
+
+files.each do |file|
+ contents = File.binread(file)
+ lines = contents.lines.to_a
+
+ lines = lines.map { |line| line.chomp.empty? ? line : " #{line}" }
+
+ version_line = "#{guard} do\n"
+ if lines[0] =~ /^\s*require.+spec_helper/
+ lines[0] = lines[0].sub(/^ /, '')
+ lines.insert 1, "\n", version_line
+ else
+ warn "Could not find 'require spec_helper' line in #{file}"
+ lines.insert 0, version_line
+ end
+
+ lines << "end\n"
+
+ File.binwrite file, lines.join
+end
diff --git a/spec/ruby/.gitignore b/spec/ruby/.gitignore
new file mode 100644
index 0000000000..3f1206a16e
--- /dev/null
+++ b/spec/ruby/.gitignore
@@ -0,0 +1,5 @@
+/Gemfile.lock
+/rubyspec_temp
+/ext
+/.ruby-version
+/.ruby-gemset
diff --git a/spec/ruby/.mspec.constants b/spec/ruby/.mspec.constants
new file mode 100644
index 0000000000..4da3633715
--- /dev/null
+++ b/spec/ruby/.mspec.constants
@@ -0,0 +1,236 @@
+Abbrev
+Addrinfo
+AliasObject
+AliasObject2
+AnonWithConstant
+ArbitraryException
+ArraySub
+ArraySubPush
+AryChild
+Base64
+BaseClass
+BasicSocket
+BeCloseToMatrixMatcher
+BigDecimal
+BigMath
+BitwiseAndTest
+BreakTest
+BreakTest2
+CAPI_SIZEOF_LONG
+CApiModuleSpecsAutoload
+CApiModuleSpecsModuleA
+CGI
+CMath
+CODE_LOADING_DIR
+CSAutoloadA
+CSAutoloadB
+CSAutoloadC
+CSAutoloadD
+CSV
+ChainedNextTest
+ChildClass
+ClassIdUnderAutoload
+ClassSpecDefineClass
+ClassSpecsKeywordWithSemicolon
+ClassSpecsKeywordWithoutSemicolon
+ClassSpecsNumber
+ClassUnderAutoload
+CodingUS_ASCII
+CodingUTF_8
+ComparisonTest
+ConstantSpecsIncludedModule
+ConstantSpecsTwo
+ConstantSpecsThree
+ConstantVisibility
+Coverage
+CoverageSpecs
+CustomArgumentError
+DRb
+DRbIdConv
+DRbObject
+DRbUndumped
+Date
+DateTime
+DefSpecNested
+DefSpecNestedB
+DefSpecSingleton
+DefSpecsLambdaVisibility
+DefineMethodByProcClass
+DefineMethodSpecClass
+DefineSingletonMethodSpecClass
+Delegator
+DescArray
+DescObjectTest
+Digest
+DumpableDir
+ERB
+EnsureInClassExample
+EnumerableSpecGrep
+EnumerableSpecGrep2
+EnumerableSpecIncludeP
+EnumerableSpecIncludeP11
+Etc
+EvalBindingA
+EvalBindingProcA
+Exception2MessageMapper
+ExceptionForMatrix
+Fcntl
+Fiddle
+FileStat
+FileUtils
+Find
+Forwardable
+GetoptLong
+HMACConstants
+HashStringsBinary
+HashStringsUSASCII
+HashStringsUTF8
+IPAddr
+IPSocket
+Importer
+IncludeSpecsClass
+IncludeSpecsMiddle
+IncludeSpecsTop
+IncludesMath
+JSON
+KSAutoloadA
+KSAutoloadB
+KSAutoloadBB
+KSAutoloadCallsRequire
+KSAutoloadD
+Logger
+MD5Constants
+MY_INPUT4_FOR_ERB
+Matrix
+MatrixSub
+MethodArity
+Meths
+MethsMore
+Mixin
+ModuleSpecsKeywordWithoutSemicolon
+ModuleSpecsToplevel
+ModuleSpecs_CS1
+ModuleSpecs_CS2
+ModuleSpecs_CS3
+MyClass
+MyClass0ForErb
+MyClass1ForErb
+MyClass1ForErb_
+MyClass2ForErb
+MyClass4ForErb
+MyFiber
+MyModule2ForErb
+MyString
+NamespaceTest
+Net
+OBJDIR
+OBJECT_SPACE_TOP_LEVEL_CONSTANT
+OFor
+ObjectSpaceFixtures
+ObjectSpecDup
+ObjectSpecDupInitCopy
+ObjectTest
+Observable
+Open3
+OpenSSL
+OpenStruct
+OperatorImplementor
+OptParse
+OptionParser
+OrAndXorTest
+OtherCustomException
+ParentClass
+Pathname
+Person
+Prime
+Private
+ProcFromMethod
+Psych
+RactorLocalSingleton
+REXML
+RUBY_SIGNALS
+RbReadline
+Readline
+ReceiverClass
+RegexpSpecsSubclass
+RegexpSpecsSubclassTwo
+Reline
+RescueInClassExample
+Resolv
+Ripper
+SHA1Constants
+SHA256Constants
+SHA384Constants
+SHA512Constants
+SameName
+ScanError
+Scanf
+SecondClass
+SecureRandom
+Set
+Shellwords
+SimpleDelegator
+SingleForwardable
+Singleton
+Socket
+SocketError
+SomeClass
+SortedSet
+SpecificExampleException
+Specs
+StrChild
+StrangeEach
+StringIO
+StringRefinement
+StringScanner
+StringSubclass
+StructClasses
+Syck
+Syslog
+TCPServer
+TCPSocket
+TSort
+Tempfile
+TestServer
+Timeout
+TimeoutError
+UDPSocket
+UNIXServer
+UNIXSocket
+URI
+UnaryMinusTest
+UnicodeNormalize
+UnloadableDumpableDir
+UserArray
+UserCustomConstructorString
+UserDefined
+UserDefinedImmediate
+UserDefinedString
+UserDefinedWithIvar
+UserHash
+UserHashInitParams
+UserMarshal
+UserMarshalWithClassName
+UserMarshalWithIvar
+UserObject
+UserPreviouslyDefinedWithInitializedIvar
+UserRegexp
+UserString
+Vector
+WEBrick
+WIN32OLE
+WIN32OLEQueryInterfaceError
+WIN32OLERuntimeError
+WIN32OLE_EVENT
+WIN32OLE_METHOD
+WIN32OLE_PARAM
+WIN32OLE_RECORD
+WIN32OLE_RUBYSPEC
+WIN32OLE_TYPE
+WIN32OLE_TYPELIB
+WIN32OLE_VARIABLE
+WIN32OLE_VARIANT
+WeakRef
+Win32
+YAML
+Zlib
diff --git a/spec/ruby/.rubocop.yml b/spec/ruby/.rubocop.yml
new file mode 100644
index 0000000000..0b5dcb80a2
--- /dev/null
+++ b/spec/ruby/.rubocop.yml
@@ -0,0 +1,204 @@
+inherit_from: .rubocop_todo.yml
+
+AllCops:
+ TargetRubyVersion: 3.2
+ DisplayCopNames: true
+ Exclude:
+ - command_line/fixtures/bad_syntax.rb
+ - core/exception/fixtures/syntax_error.rb
+ DisabledByDefault: true
+ NewCops: disable
+
+Layout/IndentationConsistency:
+ Enabled: true
+
+Layout/TrailingWhitespace:
+ Enabled: true
+
+Layout/TrailingEmptyLines:
+ Enabled: true
+ Exclude:
+ - library/coverage/fixtures/some_class.rb
+
+Layout/SpaceInLambdaLiteral:
+ Enabled: true
+ EnforcedStyle: require_space
+
+Lint:
+ Enabled: true
+
+# {...} has higher precedence than do ... end, on purpose
+Lint/AmbiguousBlockAssociation:
+ Enabled: false
+
+Lint/AssignmentInCondition:
+ Enabled: false
+
+Lint/BooleanSymbol:
+ Enabled: false
+
+Lint/DeprecatedOpenSSLConstant:
+ Exclude:
+ - library/openssl/digest/**/*.rb
+
+Lint/InterpolationCheck:
+ Enabled: false
+
+Lint/LiteralAsCondition:
+ Enabled: false
+
+# Required to support Ruby 3.0
+Lint/RedundantRequireStatement:
+ Exclude:
+ - core/fiber/**/*.rb
+ - library/fiber/**/*.rb
+ - optional/capi/fiber_spec.rb
+
+Lint/RedundantSplatExpansion:
+ Enabled: false
+
+Lint/RescueException:
+ Enabled: false
+
+Lint/UnifiedInteger:
+ Enabled: false
+
+Lint/UnusedBlockArgument:
+ Enabled: false
+
+Lint/UnusedMethodArgument:
+ Enabled: false
+
+Lint/UselessAssignment:
+ Enabled: false
+
+Lint/BinaryOperatorWithIdenticalOperands:
+ Enabled: false
+
+Lint/EmptyConditionalBody:
+ Enabled: false # buggy
+
+Lint/Void:
+ Enabled: false
+
+Lint/ConstantDefinitionInBlock:
+ Enabled: false
+
+Lint/RaiseException:
+ Enabled: false
+
+Lint/FloatComparison:
+ Enabled: false
+
+Lint/DeprecatedClassMethods:
+ Enabled: false
+
+Lint/UnreachableLoop:
+ Enabled: false
+
+Lint/MissingSuper:
+ Enabled: false
+
+Lint/UselessMethodDefinition:
+ Enabled: false
+
+Lint/UselessTimes:
+ Enabled: false
+
+Lint/MixedRegexpCaptureTypes:
+ Enabled: false
+
+Lint/DuplicateElsifCondition:
+ Enabled: false
+
+Lint/OutOfRangeRegexpRef:
+ Enabled: false
+
+Lint/InheritException:
+ Enabled: false
+
+Lint/SafeNavigationChain:
+ Enabled: false
+
+Lint/ElseLayout:
+ Exclude:
+ - 'language/if_spec.rb'
+
+Lint/EmptyExpression:
+ Exclude:
+ - 'language/**/*.rb'
+
+Lint/EmptyWhen:
+ Exclude:
+ - language/case_spec.rb
+ - optional/capi/spec_helper.rb
+
+Lint/ErbNewArguments:
+ Exclude:
+ - 'library/erb/new_spec.rb'
+
+Lint/FormatParameterMismatch:
+ Exclude:
+ - 'core/kernel/shared/sprintf.rb'
+ - 'core/string/modulo_spec.rb'
+
+Lint/NestedMethodDefinition:
+ Exclude:
+ - language/def_spec.rb
+ - language/fixtures/def.rb
+
+Lint/ShadowingOuterLocalVariable:
+ Exclude:
+ - 'core/binding/local_variables_spec.rb'
+ - 'core/kernel/local_variables_spec.rb'
+ - 'language/block_spec.rb'
+ - 'language/proc_spec.rb'
+
+Lint/UnreachableCode:
+ Exclude:
+ - 'core/enumerator/lazy/fixtures/classes.rb'
+ - 'core/kernel/catch_spec.rb'
+ - 'core/kernel/raise_spec.rb'
+ - 'core/kernel/throw_spec.rb'
+ - 'language/break_spec.rb'
+ - 'language/optional_assignments_spec.rb'
+ - 'language/fixtures/break.rb'
+ - 'language/fixtures/break_lambda_toplevel.rb'
+ - 'language/fixtures/break_lambda_toplevel_block.rb'
+ - 'language/fixtures/break_lambda_toplevel_method.rb'
+ - 'language/fixtures/return.rb'
+ - 'language/next_spec.rb'
+ - 'language/return_spec.rb'
+ - 'optional/capi/kernel_spec.rb'
+ - 'shared/kernel/raise.rb'
+
+Lint/UriRegexp:
+ Exclude:
+ - 'library/uri/regexp_spec.rb'
+
+Lint/Debugger:
+ Exclude:
+ - 'core/binding/fixtures/irb.rb'
+
+Lint/Loop:
+ Enabled: false
+
+Style/BlockComments:
+ Enabled: true
+
+Style/Lambda:
+ Enabled: true
+ EnforcedStyle: literal
+ Exclude:
+ - 'language/lambda_spec.rb'
+ - 'language/proc_spec.rb'
+ - 'language/numbered_parameters_spec.rb'
+ - 'language/it_parameter_spec.rb'
+ - 'core/kernel/lambda_spec.rb'
+
+Style/EmptyLambdaParameter:
+ Enabled: true
+
+Style/StabbyLambdaParentheses:
+ Enabled: true
+ EnforcedStyle: require_no_parentheses
diff --git a/spec/ruby/.rubocop_todo.yml b/spec/ruby/.rubocop_todo.yml
new file mode 100644
index 0000000000..f998002c6d
--- /dev/null
+++ b/spec/ruby/.rubocop_todo.yml
@@ -0,0 +1,138 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# on 2026-05-29 08:10:07 UTC using RuboCop version 1.86.2.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 2
+Lint/DuplicateCaseCondition:
+ Exclude:
+ - 'language/case_spec.rb'
+
+# Offense count: 20
+Lint/DuplicateMethods:
+ Exclude:
+ - 'core/array/fixtures/encoded_strings.rb'
+ - 'core/method/fixtures/classes.rb'
+ - 'core/module/const_added_spec.rb'
+ - 'core/module/define_method_spec.rb'
+ - 'core/module/fixtures/classes.rb'
+ - 'core/module/method_added_spec.rb'
+ - 'core/module/name_spec.rb'
+ - 'core/proc/new_spec.rb'
+ - 'core/unboundmethod/fixtures/classes.rb'
+ - 'fixtures/class.rb'
+ - 'language/assignments_spec.rb'
+
+# Offense count: 8
+Lint/EnsureReturn:
+ Exclude:
+ - 'language/fixtures/ensure.rb'
+ - 'language/fixtures/return.rb'
+ - 'language/return_spec.rb'
+
+# Offense count: 10
+Lint/FlipFlop:
+ Exclude:
+ - 'language/if_spec.rb'
+ - 'language/precedence_spec.rb'
+
+# Offense count: 10
+Lint/FloatOutOfRange:
+ Exclude:
+ - 'core/string/modulo_spec.rb'
+
+# Offense count: 3
+# This cop supports safe autocorrection (--autocorrect).
+Lint/ImplicitStringConcatenation:
+ Exclude:
+ - 'core/string/chilled_string_spec.rb'
+ - 'language/string_spec.rb'
+
+# Offense count: 4
+Lint/IneffectiveAccessModifier:
+ Exclude:
+ - 'core/kernel/fixtures/classes.rb'
+ - 'core/module/fixtures/classes.rb'
+ - 'language/fixtures/private.rb'
+
+# Offense count: 12
+# This cop supports safe autocorrection (--autocorrect).
+Lint/LiteralInInterpolation:
+ Exclude:
+ - 'core/module/refine_spec.rb'
+ - 'core/string/shared/to_sym.rb'
+ - 'language/alias_spec.rb'
+ - 'language/defined_spec.rb'
+ - 'language/fixtures/squiggly_heredoc.rb'
+ - 'language/string_spec.rb'
+ - 'language/symbol_spec.rb'
+ - 'language/undef_spec.rb'
+
+# Offense count: 8
+# This cop supports safe autocorrection (--autocorrect).
+Lint/MultipleComparison:
+ Exclude:
+ - 'language/precedence_spec.rb'
+
+# Offense count: 9
+# This cop supports safe autocorrection (--autocorrect).
+Lint/ParenthesesAsGroupedExpression:
+ Exclude:
+ - 'language/block_spec.rb'
+ - 'language/method_spec.rb'
+
+# Offense count: 1
+# This cop supports unsafe autocorrection (--autocorrect-all).
+# Configuration parameters: AllowedMethods, InferNonNilReceiver, AdditionalNilMethods.
+# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal?
+# AdditionalNilMethods: present?, blank?, try, try!
+Lint/RedundantSafeNavigation:
+ Exclude:
+ - 'language/safe_navigator_spec.rb'
+ - 'language/fixtures/rescue_captures.rb'
+
+# Offense count: 2
+# This cop supports safe autocorrection (--autocorrect).
+Lint/RedundantStringCoercion:
+ Exclude:
+ - 'core/io/print_spec.rb'
+
+# Offense count: 1
+# Configuration parameters: AllowRBSInlineAnnotation.
+Lint/SelfAssignment:
+ Exclude:
+ - 'core/gc/auto_compact_spec.rb'
+
+# Offense count: 4
+# Configuration parameters: IgnoreImplicitReferences.
+Lint/ShadowedArgument:
+ Exclude:
+ - 'language/fixtures/super.rb'
+
+# Offense count: 49
+# Configuration parameters: AllowComments, AllowNil.
+Lint/SuppressedException:
+ Enabled: false
+
+# Offense count: 9
+# Configuration parameters: AllowKeywordBlockArguments.
+Lint/UnderscorePrefixedVariableName:
+ Exclude:
+ - 'core/io/pipe_spec.rb'
+ - 'core/io/popen_spec.rb'
+ - 'language/block_spec.rb'
+
+# Offense count: 9
+# This cop supports safe autocorrection (--autocorrect).
+# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
+Lint/UselessAccessModifier:
+ Exclude:
+ - 'core/module/define_method_spec.rb'
+ - 'core/module/fixtures/classes.rb'
+ - 'core/module/module_function_spec.rb'
+ - 'core/module/private_class_method_spec.rb'
+ - 'language/fixtures/def.rb'
+ - 'language/fixtures/send.rb'
diff --git a/spec/ruby/CONTRIBUTING.md b/spec/ruby/CONTRIBUTING.md
new file mode 100644
index 0000000000..a474e205f0
--- /dev/null
+++ b/spec/ruby/CONTRIBUTING.md
@@ -0,0 +1,297 @@
+Contributions are much appreciated.
+Please open a pull request or add an issue to discuss what you intend to work on.
+If the pull requests passes the CI and conforms to the existing style of specs, it will be merged.
+
+### File organization
+
+Spec are grouped in 5 separate top-level groups:
+
+* `command_line`: for the ruby executable command-line flags (`-v`, `-e`, etc)
+* `language`: for the language keywords and syntax constructs (`if`, `def`, `A::B`, etc)
+* `core`: for the core methods (`Integer#+`, `String#upcase`, no need to require anything)
+* `library`: for the standard libraries methods (`CSV.new`, `YAML.parse`, need to require the stdlib)
+* `optional/capi`: for functions available to the Ruby C-extension API
+
+The exact file for methods is decided by the `#owner` of a method, for instance for `#group_by`:
+
+```ruby
+> [].method(:group_by)
+=> #<Method: Array(Enumerable)#group_by>
+> [].method(:group_by).owner
+=> Enumerable
+```
+
+Which should therefore be specified in `core/enumerable/group_by_spec.rb`.
+
+### MkSpec - a tool to generate the spec structure
+
+If you want to create new specs, you should use `mkspec`, part of [MSpec](http://github.com/ruby/mspec).
+
+ $ ../mspec/bin/mkspec -h
+
+#### Creating files for unspecified modules or classes
+
+For instance, to create specs for `forwardable`:
+
+ $ ../mspec/bin/mkspec -b library -rforwardable -c Forwardable
+
+Specify `core` or `library` as the `base`.
+
+#### Finding unspecified core methods
+
+This is very easy, just run the command below in your `spec` directory.
+`ruby` must be a recent version of MRI.
+
+ $ ruby --disable-gem ../mspec/bin/mkspec
+
+You might also want to search for:
+
+ it "needs to be reviewed for spec completeness"
+
+which indicates the file was generated but the method unspecified.
+
+### Matchers and expectations
+
+Here is a list of frequently-used matchers, which should be enough for most specs.
+There are a few extra specific matchers used in the couple specs that need it.
+
+The general idea is: add `.should` just before the predicate you expect to be truthy, and done!
+This works for most needs and provides a great error when it fails.
+It's immediately clear which method is used and there no need to remember a mapping like in RSpec between e.g. `eq` and `==`.
+See [this blog post](https://eregon.me/blog/2019/10/07/a-new-should-syntax.html) for the motivation behind that syntax.
+
+#### Comparison matchers
+
+```ruby
+(1 + 2).should == 3 # Calls #==
+(1 + 2).should_not == 5
+
+File.should.equal?(File) # Calls #equal? (tests identity)
+(1 + 2).should.eql?(3) # Calls #eql? (Hash equality)
+
+1.should < 2
+2.should <= 2
+3.should >= 3
+4.should > 3
+
+"Hello".should =~ /l{2}/ # Calls #=~ (Regexp match)
+```
+
+#### Predicate matchers
+
+```ruby
+[].should.empty?
+[1,2,3].should.include?(2)
+
+"hello".should.start_with?("h")
+"hello".should.end_with?("o")
+
+(0.1 + 0.2).should be_close(0.3, TOLERANCE) # (0.2-0.1).abs < TOLERANCE
+(0.0/0.0).should.nan?
+
+3.14.should.instance_of?(Float) # Calls #instance_of?
+3.14.should.is_a?(Numeric) # Calls #is_a?
+
+3.14.should.respond_to?(:to_i)
+Integer.should.method_defined?(:+, false)
+```
+
+#### Exception matchers
+
+```ruby
+-> {
+ raise "oops"
+}.should.raise(RuntimeError, /oops/)
+
+-> {
+ raise "oops"
+}.should.raise(RuntimeError) { |e|
+ # Custom checks on the Exception object
+ e.message.should.include?("oops")
+ e.cause.should == nil
+}
+```
+
+##### `should_not.raise`
+
+**Avoid this!** Instead, use an expectation testing what the code in the lambda does.
+If an exception is raised, it will fail the example anyway.
+
+```ruby
+-> { ... }.should_not.raise
+```
+
+#### Warning matcher
+
+```ruby
+-> {
+ Fixnum
+}.should complain(/constant ::Fixnum is deprecated/) # Expect a warning
+```
+
+### Guards
+
+Different guards are available as defined by mspec.
+Here is a list of the most commonly-used guards:
+
+#### Version guards
+
+```ruby
+ruby_version_is ""..."3.2" do
+ # Specs for RUBY_VERSION < 3.2
+end
+
+ruby_version_is "3.2" do
+ # Specs for RUBY_VERSION >= 3.2
+end
+```
+
+#### Platform guards
+
+```ruby
+platform_is :windows do
+ # Specs only valid on Windows
+end
+
+platform_is_not :windows do
+ # Specs valid on platforms other than Windows
+end
+
+platform_is :linux, :darwin do # OR
+end
+
+platform_is_not :linux, :darwin do # Not Linux and not Darwin
+end
+
+platform_is pointer_size: 64 do
+ # 64-bit platform
+end
+
+big_endian do
+ # Big-endian platform
+end
+```
+
+#### Guard for bug
+
+In case there is a bug in MRI and the fix will be backported to previous versions.
+If it is not backported or not likely, use `ruby_version_is` instead.
+First, file a bug at https://bugs.ruby-lang.org/.
+The problem is `ruby_bug` would make non-MRI implementations fail this spec while MRI itself does not pass it, so it should only be used if the bug is/will be fixed and backported.
+Otherwise, non-MRI implementations would have to choose between being incompatible with the latest release of MRI (which has the bug) to pass the spec, or behave the same as the latest release of MRI (which has the bug) and fail the spec, both which make no sense.
+
+IOW, `ruby_bug '#NN', ''...'X.Y' do` is equivalent to `guard_not { RUBY_ENGINE == "ruby" && ruby_version_is ''...'X.Y' } do`. So it skips tests on MRI on specified versions (where a bug is present) and runs tests on alternative implementations only.
+
+```ruby
+ruby_bug '#13669', ''...'3.2' do
+ it "works like this" do
+ # Specify the expected behavior here, not the bug
+ end
+end
+```
+
+#### Combining guards
+
+```ruby
+guard -> { platform_is :windows and ruby_version_is ""..."3.2" } do
+ # Windows and RUBY_VERSION < 3.2
+end
+
+guard_not -> { platform_is :windows and ruby_version_is ""..."3.2" } do
+ # The opposite
+end
+```
+
+#### Custom guard
+
+```ruby
+max_uint = (1 << 32) - 1
+guard -> { max_uint <= fixnum_max } do
+end
+```
+
+Custom guards are better than a simple `if` as they allow [mspec commands](https://github.com/ruby/mspec/issues/30#issuecomment-312487779) to work properly.
+
+#### Implementation-specific behaviors
+
+In general, the usage of guards should be minimized as possible.
+
+There are no guards to define implementation-specific behavior because
+the Ruby Spec Suite defines common behavior and not implementation details.
+Use the implementation test suite for these.
+
+If an implementation does not support some feature, simply tag the related specs as failing instead.
+
+### Shared Specs
+
+Often throughout Ruby, identical functionality is used by different methods and modules. In order
+to avoid duplication of specs, we have shared specs that are re-used in other specs. The use is a
+bit tricky however, so let's go over it.
+
+Commonly, if a shared spec is only reused within its own module, the shared spec will live within a
+shared directory inside that module's directory. For example, the `core/hash/shared/iteration.rb` spec is
+only used by `Hash` specs, and so it lives inside `core/hash/shared/`.
+
+When a shared spec is used across multiple modules or classes, it lives within the `shared/` directory.
+An example of this is the `shared/file/socket.rb` which is used by `core/file/socket_spec.rb`,
+`core/filetest/socket_spec.rb`, and `core/file/state/socket_spec.rb` and so it lives in the root `shared/`.
+
+Defining a shared spec involves adding a `shared: true` option to the top-level `describe` block. This
+will signal not to run the specs directly by the runner. Shared specs have access to two instance
+variables from the implementor spec: `@method` and `@object`, which the implementor spec will pass in.
+
+Here's an example of a snippet of a shared spec and two specs which integrates it:
+
+```ruby
+# core/hash/shared/iteration.rb
+describe :hash_iteration_no_block, shared: true do
+ it "returns an Enumerator if called on a non-empty hash without a block" do
+ { 1 => 2 }.send(@method).should.instance_of?(Enumerator)
+ end
+end
+
+# core/hash/select_spec.rb
+describe "Hash#select" do
+ it_behaves_like :hash_iteration_no_block, :select
+end
+
+# core/hash/reject_spec.rb
+describe "Hash#reject" do
+ it_behaves_like :hash_iteration_no_block, :reject
+end
+```
+
+In the example, the first `describe` defines the shared spec `:hash_iteration_no_block`, which defines a spec that
+calls the `@method` method with an expectation. In the implementor spec, we use `it_behaves_like` to
+integrate the shared spec. `it_behaves_like` takes 3 parameters: the key of the shared spec, a method,
+and an object. These last two parameters are accessible via `@method` and `@object` in the shared spec.
+
+Sometimes, shared specs require more context from the implementor class than a simple object. We can address
+this by passing a lambda as the method, which will have the scope of the implementor. Here's an example of
+how this is used currently:
+
+```ruby
+describe :kernel_sprintf, shared: true do
+ it "raises TypeError exception if cannot convert to Integer" do
+ -> { @method.call("%b", Object.new) }.should.raise(TypeError)
+ end
+end
+
+describe "Kernel#sprintf" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ sprintf(format, *args)
+ }
+end
+
+describe "Kernel.sprintf" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ Kernel.sprintf(format, *args)
+ }
+end
+```
+
+In the above example, the method being passed is a lambda that triggers the specific conditions of the shared spec.
+
+### Style
+
+Do not leave any trailing space and follow the existing style.
diff --git a/spec/ruby/LICENSE b/spec/ruby/LICENSE
new file mode 100644
index 0000000000..d581dd1c9f
--- /dev/null
+++ b/spec/ruby/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2008 Engine Yard, Inc. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/spec/ruby/README.md b/spec/ruby/README.md
new file mode 100644
index 0000000000..b259a97e21
--- /dev/null
+++ b/spec/ruby/README.md
@@ -0,0 +1,167 @@
+# The Ruby Spec Suite
+
+[![Actions Build Status](https://github.com/ruby/spec/workflows/CI/badge.svg)](https://github.com/ruby/spec/actions)
+
+The Ruby Spec Suite, abbreviated `ruby/spec`, is a test suite for the behavior of the Ruby programming language.
+
+### Description and Motivation
+
+It is not a standardized specification like the ISO one, and does not aim to become one.
+Instead, it is a practical tool to describe and test the behavior of Ruby with code.
+
+Every example code has a textual description, which presents several advantages:
+
+* It is easier to understand the intent of the author
+* It documents how recent versions of Ruby should behave
+* It helps Ruby implementations to agree on a common behavior
+
+The specs are written with syntax similar to RSpec 2.
+They are run with MSpec, the purpose-built framework for running the Ruby Spec Suite.
+For more information, see the [MSpec](https://github.com/ruby/mspec) project.
+
+The specs describe the [language syntax](language/), the [core library](core/), the [standard library](library/), the [C API for extensions](optional/capi) and the [command line flags](command_line/).
+The language specs are grouped by keyword while the core and standard library specs are grouped by class and method.
+
+ruby/spec is known to be tested in these implementations for every commit:
+
+* [MRI](https://rubyci.org/) on 30 platforms and 4 versions
+* [JRuby](https://github.com/jruby/jruby/tree/master/spec/ruby) for both 1.7 and 9.x
+* [TruffleRuby](https://github.com/truffleruby/truffleruby/tree/master/spec/ruby)
+* [Opal](https://github.com/opal/opal/tree/master/spec)
+* [Artichoke](https://github.com/artichoke/spec/tree/artichoke-vendor)
+
+ruby/spec describes the behavior of Ruby 3.3 and more recent Ruby versions.
+More precisely, every latest stable MRI release should [pass](https://github.com/ruby/spec/actions/workflows/ci.yml) all specs of ruby/spec (3.3.x, 3.4.x, etc), and those are tested in CI.
+
+### Synchronization with Ruby Implementations
+
+The specs are synchronized both ways around once a month by @andrykonchin between ruby/spec, MRI, JRuby and TruffleRuby,
+using [this script](https://github.com/ruby/mspec/blob/master/tool/sync/sync-rubyspec.rb).
+Each of these repositories has a full copy of the specs under `spec/ruby` to ease editing specs.
+Any of these repositories can be used to add or edit specs, use what is most convenient for you.
+
+For *testing* the development version of a Ruby implementation, one should always test against that implementation's copy of the specs under `spec/ruby`, as that's what the Ruby implementation tests against in their CI.
+Also, this repository doesn't always contain the latest spec changes from MRI (it's synchronized monthly), and does not contain tags (specs marked as failing on that Ruby implementation).
+Running specs on a Ruby implementation can be done with:
+
+```console
+$ cd ruby_implementation/spec/ruby
+# Add ../ruby_implementation/bin in PATH, or pass -t /path/to/bin/ruby
+$ ../mspec/bin/mspec
+```
+
+### Specs for old Ruby versions
+
+For older specs try these commits:
+
+* Ruby 2.0.0-p647 - [Suite](https://github.com/ruby/spec/commit/245862558761d5abc676843ef74f86c9bcc8ea8d) using [MSpec](https://github.com/ruby/mspec/commit/f90efa068791064f955de7a843e96e2d7d3041c2) (may encounter 2 failures)
+* Ruby 2.1.9 - [Suite](https://github.com/ruby/spec/commit/f029e65241374386077ac500add557ae65069b55) using [MSpec](https://github.com/ruby/mspec/commit/55568ea3918c6380e64db8c567d732fa5781efed)
+* Ruby 2.2.10 - [Suite](https://github.com/ruby/spec/commit/cbaa0e412270c944df0c2532fc500c920dba0e92) using [MSpec](https://github.com/ruby/mspec/commit/d84d7668449e96856c5f6bac8cb1526b6d357ce3)
+* Ruby 2.3.8 - [Suite](https://github.com/ruby/spec/commit/dc733114d8ae66a3368ba3a98422c50147a76ba5) using [MSpec](https://github.com/ruby/mspec/commit/4599bc195fb109f2a482a01c32a7d659518369ea)
+* Ruby 2.4.10 - [Suite](https://github.com/ruby/spec/commit/bce4f2b81d6c31db67cf4d023a0625ceadde59bd) using [MSpec](https://github.com/ruby/mspec/commit/e7eb8aa4c26495b7b461e687d950b96eb08b3ff2)
+* Ruby 2.5.9 - [Suite](https://github.com/ruby/spec/commit/c503335d3d9f6ec6ef24de60a0716c34af69b64f) using [MSpec](https://github.com/ruby/mspec/commit/0091e8a62e954717cd54641f935eaf1403692041)
+* Ruby 2.6.10 - [Suite](https://github.com/ruby/spec/commit/aaf998fb8c92c4e63ad423a2e7ca6e6921818c6e) using [MSpec](https://github.com/ruby/mspec/commit/5e36c684e9e2b92b1187589bba1df22c640a8661)
+* Ruby 2.7.8 - [Suite](https://github.com/ruby/spec/commit/93787e6035c925b593a9c0c6fb0e7e07a6f1df1f) using [MSpec](https://github.com/ruby/mspec/commit/1d8cf64722d8a7529f7cd205be5f16a89b7a67fd)
+* Ruby 3.0.7 - [Suite](https://github.com/ruby/spec/commit/affef93d9940f615e4836f64b011da211f570913) using [MSpec](https://github.com/ruby/mspec/commit/0aabb3e548eb5ea6cad0125f8f46cee34542b6b7)
+* Ruby 3.1.6 - [Suite](https://github.com/ruby/spec/commit/ec960f2389d1c2265d32397fa8afa6d462014efc) using [MSpec](https://github.com/ruby/mspec/commit/484310dbed35b84c74484fd674602f88c42d063a)
+* Ruby 3.2.9 - [Suite](https://github.com/ruby/spec/commit/97f076242b7fc6e60703e6a6053365065cd6fc30) using [MSpec](https://github.com/ruby/mspec/commit/54704795e21128a930af2021c72c49cb87065134)
+
+### Running the specs
+
+First, clone this repository:
+
+ $ git clone https://github.com/ruby/spec.git
+
+Then move to it:
+
+ $ cd spec
+
+Clone [MSpec](https://github.com/ruby/mspec):
+
+ $ git clone https://github.com/ruby/mspec.git ../mspec
+
+And run the spec suite:
+
+ $ ../mspec/bin/mspec
+
+This will execute all the specs using the executable named `ruby` on your current PATH.
+
+### Running Specs with a Specific Ruby Implementation
+
+Use the `-t` option to specify the Ruby implementation with which to run the specs.
+The argument is either a full path to the Ruby binary, or an executable in `$PATH`.
+
+ $ ../mspec/bin/mspec -t /path/to/some/bin/ruby
+
+### Running Selected Specs
+
+To run a single spec file, pass the filename to `mspec`:
+
+ $ ../mspec/bin/mspec core/kernel/kind_of_spec.rb
+
+You can also pass a directory, in which case all specs in that directories will be run:
+
+ $ ../mspec/bin/mspec core/kernel
+
+Finally, you can also run them per group as defined in `default.mspec`.
+The following command will run all language specs:
+
+ $ ../mspec/bin/mspec :language
+
+In similar fashion, the following commands run the respective specs:
+
+ $ ../mspec/bin/mspec :core
+ $ ../mspec/bin/mspec :library
+ $ ../mspec/bin/mspec :capi
+
+### Sanity Checks When Running Specs
+
+A number of checks for various kind of "leaks" (file descriptors, temporary files,
+threads, subprocesses, `ENV`, `ARGV`, global encodings, top-level constants) can be
+enabled with `CHECK_LEAKS=true`:
+
+ $ CHECK_LEAKS=true ../mspec/bin/mspec
+
+New top-level constants should only be introduced when needed or follow the
+pattern `<ClassBeingTested>Specs` such as `module StringSpecs`.
+Other constants used for testing should be nested under such a module.
+
+Exceptions to these rules are contained in the file `.mspec.constants`.
+MSpec can automatically add new top-level constants in this file with:
+
+ $ CHECK_LEAKS=save mspec ../mspec/bin/mspec file
+
+### Running Specs on S390x CPU Architecture
+
+Run the specs with `DFLTCC=0` if you see failing specs related to the zlib library on s390x CPU architecture. The failures can happen with the zlib library applying the patch madler/zlib#410 to enable the deflate algorithm producing a different compressed byte stream.
+
+ $ DFLTCC=0 ../mspec/bin/mspec
+
+### Contributing and Writing Specs
+
+See [CONTRIBUTING.md](https://github.com/ruby/spec/blob/master/CONTRIBUTING.md) for documentation about contributing and writing specs (guards, matchers, etc).
+
+### Dependencies
+
+These command-line executables are needed to run the specs.
+
+* `echo`
+* `stat` for `core/file/*time_spec.rb`
+* `find` for `core/file/fixtures/file_types.rb` (package `findutils`, not needed on Windows)
+
+The file `/etc/services` is required for socket specs (package `netbase` on Debian, not needed on Windows).
+
+### Socket specs from rubysl-socket
+
+Most specs under `library/socket` were imported from the rubysl-socket project (which is no longer on GitHub).
+The 3 copyright holders of rubysl-socket, Yorick Peterse, Chuck Remes and
+Brian Shirai, agreed to relicense those specs under the MIT license in ruby/spec.
+
+### History and RubySpec
+
+This project was originally born from [Rubinius](https://github.com/rubinius/rubinius) tests being converted to the spec style.
+The revision history of these specs is available [here](https://github.com/ruby/spec/blob/2b886623/CHANGES.before-2008-05-10).
+These specs were later extracted to their own project, RubySpec, with a specific vision and principles.
+At the end of 2014, Brian Shirai, the creator of RubySpec, decided to [end RubySpec](http://rubinius.com/2014/12/31/matz-s-ruby-developers-don-t-use-rubyspec/).
+A couple months later, the different repositories were merged and [the project was revived](https://eregon.github.io/rubyspec/2015/07/29/rubyspec-is-reborn.html).
+On 12 January 2016, the name was changed to "The Ruby Spec Suite" for clarity and to let the RubySpec ideology rest in peace.
diff --git a/spec/ruby/TODO b/spec/ruby/TODO
new file mode 100644
index 0000000000..f81f070d71
--- /dev/null
+++ b/spec/ruby/TODO
@@ -0,0 +1,8 @@
+* Decide a way to test methods that are only visible given a specific
+ command-line option. For example, Kernel#gsub with -n/-p on 1.9.
+* Look at automating discovery of guarded bugs which have been fixed.
+* Use mocks for all Math functions that coerce with #to_f; currently a fixture
+ is used.
+* investigate slow specs (run with -fp) and make them faster.
+* restore some caller specs from 642bf529
+* restore refinements specs and update. See 56c5528f and f20a62e8.
diff --git a/spec/ruby/bin/rubocop b/spec/ruby/bin/rubocop
new file mode 100755
index 0000000000..0937c47906
--- /dev/null
+++ b/spec/ruby/bin/rubocop
@@ -0,0 +1,12 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'bundler/inline'
+
+gemfile do
+ source 'https://rubygems.org'
+
+ gem 'rubocop', '1.86.2'
+end
+
+exec(Gem.bin_path('rubocop', 'rubocop'), *ARGV)
diff --git a/spec/ruby/command_line/backtrace_limit_spec.rb b/spec/ruby/command_line/backtrace_limit_spec.rb
new file mode 100644
index 0000000000..4d57bc268b
--- /dev/null
+++ b/spec/ruby/command_line/backtrace_limit_spec.rb
@@ -0,0 +1,93 @@
+require_relative '../spec_helper'
+
+describe "The --backtrace-limit command line option" do
+ ruby_version_is ""..."3.4" do
+ it "limits top-level backtraces to a given number of entries" do
+ file = fixture(__FILE__ , "backtrace.rb")
+ out = ruby_exe(file, options: "--backtrace-limit=2", args: "top 2>&1", exit_status: 1)
+ out = out.gsub(__dir__, '')
+
+ out.should == <<-MSG
+top
+/fixtures/backtrace.rb:2:in `a': oops (RuntimeError)
+\tfrom /fixtures/backtrace.rb:6:in `b'
+\tfrom /fixtures/backtrace.rb:10:in `c'
+\t ... 2 levels...
+ MSG
+ end
+
+ it "affects Exception#full_message" do
+ file = fixture(__FILE__ , "backtrace.rb")
+ out = ruby_exe(file, options: "--backtrace-limit=2", args: "full_message 2>&1")
+ out = out.gsub(__dir__, '')
+
+ out.should == <<-MSG
+full_message
+/fixtures/backtrace.rb:2:in `a': oops (RuntimeError)
+\tfrom /fixtures/backtrace.rb:6:in `b'
+\tfrom /fixtures/backtrace.rb:10:in `c'
+\t ... 2 levels...
+ MSG
+ end
+
+ it "does not affect Exception#backtrace" do
+ file = fixture(__FILE__ , "backtrace.rb")
+ out = ruby_exe(file, options: "--backtrace-limit=2", args: "backtrace 2>&1")
+ out = out.gsub(__dir__, '')
+
+ out.should == <<-MSG
+backtrace
+/fixtures/backtrace.rb:2:in `a'
+/fixtures/backtrace.rb:6:in `b'
+/fixtures/backtrace.rb:10:in `c'
+/fixtures/backtrace.rb:14:in `d'
+/fixtures/backtrace.rb:29:in `<main>'
+ MSG
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "limits top-level backtraces to a given number of entries" do
+ file = fixture(__FILE__ , "backtrace.rb")
+ out = ruby_exe(file, options: "--backtrace-limit=2", args: "top 2>&1", exit_status: 1)
+ out = out.gsub(__dir__, '')
+
+ out.should == <<-MSG
+top
+/fixtures/backtrace.rb:2:in 'Object#a': oops (RuntimeError)
+\tfrom /fixtures/backtrace.rb:6:in 'Object#b'
+\tfrom /fixtures/backtrace.rb:10:in 'Object#c'
+\t ... 2 levels...
+ MSG
+ end
+
+ it "affects Exception#full_message" do
+ file = fixture(__FILE__ , "backtrace.rb")
+ out = ruby_exe(file, options: "--backtrace-limit=2", args: "full_message 2>&1")
+ out = out.gsub(__dir__, '')
+
+ out.should == <<-MSG
+full_message
+/fixtures/backtrace.rb:2:in 'Object#a': oops (RuntimeError)
+\tfrom /fixtures/backtrace.rb:6:in 'Object#b'
+\tfrom /fixtures/backtrace.rb:10:in 'Object#c'
+\t ... 2 levels...
+ MSG
+ end
+
+ it "does not affect Exception#backtrace" do
+ file = fixture(__FILE__ , "backtrace.rb")
+ out = ruby_exe(file, options: "--backtrace-limit=2", args: "backtrace 2>&1")
+ out = out.gsub(__dir__, '')
+
+ out.should == <<-MSG
+backtrace
+/fixtures/backtrace.rb:2:in 'Object#a'
+/fixtures/backtrace.rb:6:in 'Object#b'
+/fixtures/backtrace.rb:10:in 'Object#c'
+/fixtures/backtrace.rb:14:in 'Object#d'
+/fixtures/backtrace.rb:29:in '<main>'
+ MSG
+ end
+ end
+end
diff --git a/spec/ruby/command_line/dash_0_spec.rb b/spec/ruby/command_line/dash_0_spec.rb
new file mode 100755
index 0000000000..2ce4f49b5e
--- /dev/null
+++ b/spec/ruby/command_line/dash_0_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../spec_helper'
+
+describe "The -0 command line option" do
+ it "sets $/ and $-0" do
+ ruby_exe("puts $/, $-0", options: "-072").should == ":\n:\n"
+ end
+
+ ruby_version_is "4.0" do
+ it "sets $/ and $-0 as a frozen string" do
+ ruby_exe("puts $/.frozen?, $-0.frozen?", options: "-072").should == "true\ntrue\n"
+ end
+ end
+end
diff --git a/spec/ruby/command_line/dash_a_spec.rb b/spec/ruby/command_line/dash_a_spec.rb
new file mode 100644
index 0000000000..43d923ce16
--- /dev/null
+++ b/spec/ruby/command_line/dash_a_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../spec_helper'
+
+describe "The -a command line option" do
+ before :each do
+ @names = fixture __FILE__, "full_names.txt"
+ end
+
+ it "runs the code in loop conditional on Kernel.gets()" do
+ ruby_exe("puts $F.last", options: "-n -a",
+ args: " < #{@names}").should ==
+ "jones\nfield\ngrey\n"
+ end
+
+ it "sets $-a" do
+ ruby_exe("puts $-a", options: "-n -a",
+ args: " < #{@names}").should ==
+ "true\ntrue\ntrue\n"
+ end
+end
diff --git a/spec/ruby/command_line/dash_c_spec.rb b/spec/ruby/command_line/dash_c_spec.rb
new file mode 100644
index 0000000000..6b3a5de685
--- /dev/null
+++ b/spec/ruby/command_line/dash_c_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../spec_helper'
+
+describe "The -c command line option" do
+ it "checks syntax in given file" do
+ ruby_exe(nil, args: "-c #{__FILE__}").chomp.should == "Syntax OK"
+ end
+
+ it "checks syntax in -e strings" do
+ ruby_exe(nil, args: "-c -e 'puts 1' -e 'hello world'").chomp.should == "Syntax OK"
+ end
+
+ #Also needs spec for reading from STDIN
+end
diff --git a/spec/ruby/command_line/dash_d_spec.rb b/spec/ruby/command_line/dash_d_spec.rb
new file mode 100644
index 0000000000..26891b4791
--- /dev/null
+++ b/spec/ruby/command_line/dash_d_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../spec_helper'
+
+describe "The -d command line option" do
+ before :each do
+ @script = fixture __FILE__, "debug.rb"
+ end
+
+ it "sets $DEBUG to true" do
+ ruby_exe(@script, options: "-d",
+ args: "0 2> #{File::NULL}").chomp.should == "$DEBUG true"
+ end
+
+ it "sets $VERBOSE to true" do
+ ruby_exe(@script, options: "-d",
+ args: "1 2> #{File::NULL}").chomp.should == "$VERBOSE true"
+ end
+
+ it "sets $-d to true" do
+ ruby_exe(@script, options: "-d",
+ args: "2 2> #{File::NULL}").chomp.should == "$-d true"
+ end
+end
diff --git a/spec/ruby/command_line/dash_e_spec.rb b/spec/ruby/command_line/dash_e_spec.rb
new file mode 100644
index 0000000000..24ed34376d
--- /dev/null
+++ b/spec/ruby/command_line/dash_e_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../spec_helper'
+
+describe "The -e command line option" do
+ it "evaluates the given string" do
+ ruby_exe("puts 'foo'").chomp.should == "foo"
+ end
+
+ it "joins multiple strings with newlines" do
+ ruby_exe(nil, args: %Q{-e "puts 'hello" -e "world'" 2>&1}).chomp.should == "hello\nworld"
+ end
+
+ it "uses 'main' as self" do
+ ruby_exe("puts self", escape: false).chomp.should == "main"
+ end
+
+ it "uses '-e' as file" do
+ ruby_exe("puts __FILE__", escape: false).chomp.should == "-e"
+ end
+
+ it "uses '-e' in $0" do
+ system(*ruby_exe, '-e', 'exit $0 == "-e"').should == true
+ end
+
+ #needs to test return => LocalJumpError
+
+ describe "with -n and an Integer range" do
+ before :each do
+ @script = "-ne 'print if %s' #{fixture(__FILE__, "conditional_range.txt")}"
+ end
+
+ it "mimics an awk conditional by comparing an inclusive-end range with $." do
+ ruby_exe(nil, args: (@script % "2..3")).should == "2\n3\n"
+ ruby_exe(nil, args: (@script % "2..2")).should == "2\n"
+ end
+
+ it "mimics a sed conditional by comparing an exclusive-end range with $." do
+ ruby_exe(nil, args: (@script % "2...3")).should == "2\n3\n"
+ ruby_exe(nil, args: (@script % "2...2")).should == "2\n3\n4\n5\n"
+ end
+ end
+end
diff --git a/spec/ruby/command_line/dash_encoding_spec.rb b/spec/ruby/command_line/dash_encoding_spec.rb
new file mode 100644
index 0000000000..5803d328c1
--- /dev/null
+++ b/spec/ruby/command_line/dash_encoding_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../spec_helper'
+
+describe "The --encoding command line option" do
+ before :each do
+ @test_string = "print [Encoding.default_external.name, Encoding.default_internal&.name].inspect"
+ @enc2 = Encoding::ISO_8859_1
+ end
+
+ describe "sets Encoding.default_external and optionally Encoding.default_internal" do
+ it "if given a single encoding with an =" do
+ ruby_exe(@test_string, options: "--disable-gems --encoding=big5").should == [Encoding::Big5.name, nil].inspect
+ end
+
+ it "if given a single encoding as a separate argument" do
+ ruby_exe(@test_string, options: "--disable-gems --encoding big5").should == [Encoding::Big5.name, nil].inspect
+ end
+
+ it "if given two encodings with an =" do
+ ruby_exe(@test_string, options: "--disable-gems --encoding=big5:#{@enc2}").should == [Encoding::Big5.name, @enc2.name].inspect
+ end
+
+ it "if given two encodings as a separate argument" do
+ ruby_exe(@test_string, options: "--disable-gems --encoding big5:#{@enc2}").should == [Encoding::Big5.name, @enc2.name].inspect
+ end
+ end
+
+ it "does not accept a third encoding" do
+ options = {
+ options: "--disable-gems --encoding big5:#{@enc2}:utf-32le",
+ args: "2>&1",
+ exit_status: 1
+ }
+
+ ruby_exe(@test_string, options).should =~ /extra argument for --encoding: utf-32le/
+ end
+end
diff --git a/spec/ruby/command_line/dash_external_encoding_spec.rb b/spec/ruby/command_line/dash_external_encoding_spec.rb
new file mode 100644
index 0000000000..f052674dc8
--- /dev/null
+++ b/spec/ruby/command_line/dash_external_encoding_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../spec_helper'
+
+describe 'The --external-encoding command line option sets Encoding.default_external' do
+ before :each do
+ @test_string = "print Encoding.default_external.name"
+ end
+
+ it "if given an encoding with an =" do
+ ruby_exe(@test_string, options: '--external-encoding=big5').should == Encoding::Big5.name
+ end
+
+ it "if given an encoding as a separate argument" do
+ ruby_exe(@test_string, options: '--external-encoding big5').should == Encoding::Big5.name
+ end
+end
diff --git a/spec/ruby/command_line/dash_internal_encoding_spec.rb b/spec/ruby/command_line/dash_internal_encoding_spec.rb
new file mode 100644
index 0000000000..3049040bb4
--- /dev/null
+++ b/spec/ruby/command_line/dash_internal_encoding_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../spec_helper'
+
+describe 'The --internal-encoding command line option sets Encoding.default_internal' do
+ before :each do
+ @test_string = "print Encoding.default_internal.name"
+ end
+
+ it "if given an encoding with an =" do
+ ruby_exe(@test_string, options: '--internal-encoding=big5').should == Encoding::Big5.name
+ end
+
+ it "if given an encoding as a separate argument" do
+ ruby_exe(@test_string, options: '--internal-encoding big5').should == Encoding::Big5.name
+ end
+end
diff --git a/spec/ruby/command_line/dash_l_spec.rb b/spec/ruby/command_line/dash_l_spec.rb
new file mode 100644
index 0000000000..44a98445f3
--- /dev/null
+++ b/spec/ruby/command_line/dash_l_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../spec_helper'
+
+describe "The -l command line option" do
+ before :each do
+ @names = fixture __FILE__, "full_names.txt"
+ end
+
+ it "chomps lines with default separator" do
+ ruby_exe('puts $_.end_with?("\n")', options: "-n -l",
+ args: " < #{@names}").should ==
+ "false\nfalse\nfalse\n"
+ end
+
+ it "chomps last line based on $/" do
+ ruby_exe('BEGIN { $/ = "ones\n" }; puts $_', options: "-W0 -n -l",
+ args: " < #{@names}").should ==
+ "alice j\nbob field\njames grey\n"
+ end
+
+ it "sets $\\ to the value of $/" do
+ ruby_exe("puts $\\ == $/", options: "-W0 -n -l",
+ args: " < #{@names}").should ==
+ "true\ntrue\ntrue\n"
+ end
+
+ it "sets $-l" do
+ ruby_exe("puts $-l", options: "-n -l",
+ args: " < #{@names}").should ==
+ "true\ntrue\ntrue\n"
+ end
+end
diff --git a/spec/ruby/command_line/dash_n_spec.rb b/spec/ruby/command_line/dash_n_spec.rb
new file mode 100644
index 0000000000..1dd9379259
--- /dev/null
+++ b/spec/ruby/command_line/dash_n_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../spec_helper'
+
+describe "The -n command line option" do
+ before :each do
+ @names = fixture __FILE__, "names.txt"
+ end
+
+ it "runs the code in loop conditional on Kernel.gets()" do
+ ruby_exe("puts $_", options: "-n",
+ args: " < #{@names}").should ==
+ "alice\nbob\njames\n"
+ end
+
+ it "only evaluates BEGIN blocks once" do
+ ruby_exe("BEGIN { puts \"hi\" }; puts $_", options: "-n",
+ args: " < #{@names}").should ==
+ "hi\nalice\nbob\njames\n"
+ end
+
+ it "only evaluates END blocks once" do
+ ruby_exe("puts $_; END {puts \"bye\"}", options: "-n",
+ args: " < #{@names}").should ==
+ "alice\nbob\njames\nbye\n"
+ end
+
+ it "allows summing over a whole file" do
+ script = <<-script
+ BEGIN { $total = 0 }
+ $total += 1
+ END { puts $total }
+ script
+ ruby_exe(script, options: "-n",
+ args: " < #{@names}").should ==
+ "3\n"
+ end
+end
diff --git a/spec/ruby/command_line/dash_p_spec.rb b/spec/ruby/command_line/dash_p_spec.rb
new file mode 100644
index 0000000000..967e3796de
--- /dev/null
+++ b/spec/ruby/command_line/dash_p_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../spec_helper'
+
+describe "The -p command line option" do
+ before :each do
+ @names = fixture __FILE__, "names.txt"
+ end
+
+ it "runs the code in loop conditional on Kernel.gets() and prints $_" do
+ ruby_exe("$_ = $_.upcase", options: "-p",
+ args: " < #{@names}").should ==
+ "ALICE\nBOB\nJAMES\n"
+ end
+
+ it "sets $-p" do
+ ruby_exe("$_ = $-p", options: "-p",
+ args: " < #{@names}").should ==
+ "truetruetrue"
+ end
+end
diff --git a/spec/ruby/command_line/dash_r_spec.rb b/spec/ruby/command_line/dash_r_spec.rb
new file mode 100644
index 0000000000..0de9ba2e24
--- /dev/null
+++ b/spec/ruby/command_line/dash_r_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../spec_helper'
+
+describe "The -r command line option" do
+ before :each do
+ @script = fixture __FILE__, "require.rb"
+ @test_file = fixture __FILE__, "test_file"
+ end
+
+ it "requires the specified file" do
+ out = ruby_exe(@script, options: "-r #{@test_file}")
+ out.should.include?("REQUIRED")
+ out.should.include?(@test_file + ".rb")
+ end
+
+ it "requires the file before parsing the main script" do
+ out = ruby_exe(fixture(__FILE__, "bad_syntax.rb"), options: "-r #{@test_file}", args: "2>&1", exit_status: 1)
+ $?.should_not.success?
+ out.should.include?("REQUIRED")
+ out.should.include?("SyntaxError")
+ end
+
+ it "does not require the file if the main script file does not exist" do
+ out = `#{ruby_exe.to_a.join(' ')} -r #{@test_file} #{fixture(__FILE__, "does_not_exist.rb")} 2>&1`
+ $?.should_not.success?
+ out.should_not.include?("REQUIRED")
+ out.should.include?("No such file or directory")
+ end
+end
diff --git a/spec/ruby/command_line/dash_s_spec.rb b/spec/ruby/command_line/dash_s_spec.rb
new file mode 100644
index 0000000000..eaaeea7c96
--- /dev/null
+++ b/spec/ruby/command_line/dash_s_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../spec_helper'
+
+describe "The -s command line option" do
+ describe "when using -- to stop parsing" do
+ it "sets the value to true without an explicit value" do
+ ruby_exe(nil, options: "-s -e 'p $n'",
+ args: "-- -n").chomp.should == "true"
+ end
+
+ it "parses single letter args into globals" do
+ ruby_exe(nil, options: "-s -e 'puts $n'",
+ args: "-- -n=blah").chomp.should == "blah"
+ end
+
+ it "parses long args into globals" do
+ ruby_exe(nil, options: "-s -e 'puts $_name'",
+ args: "-- --name=blah").chomp.should == "blah"
+ end
+
+ it "converts extra dashes into underscores" do
+ ruby_exe(nil, options: "-s -e 'puts $___name__test__'",
+ args: "-- ----name--test--=blah").chomp.should == "blah"
+ end
+ end
+
+ describe "when running a script" do
+ before :all do
+ @script = fixture __FILE__, "dash_s_script.rb"
+ end
+
+ it "sets the value to true without an explicit value" do
+ ruby_exe(@script, options: "-s",
+ args: "-n 0").chomp.should == "true"
+ end
+
+ it "parses single letter args into globals" do
+ ruby_exe(@script, options: "-s",
+ args: "-n=blah 1").chomp.should == "blah"
+ end
+
+ it "parses long args into globals" do
+ ruby_exe(@script, options: "-s",
+ args: "--name=blah 2").chomp.should == "blah"
+ end
+
+ it "converts extra dashes into underscores" do
+ ruby_exe(@script, options: "-s",
+ args: "----name--test--=blah 3").chomp.should == "blah"
+ end
+
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_c_spec.rb b/spec/ruby/command_line/dash_upper_c_spec.rb
new file mode 100644
index 0000000000..ece1b32105
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_c_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../spec_helper'
+require_relative 'shared/change_directory'
+
+describe "The -C command line option" do
+ it_behaves_like :command_line_change_directory, "-C"
+end
diff --git a/spec/ruby/command_line/dash_upper_e_spec.rb b/spec/ruby/command_line/dash_upper_e_spec.rb
new file mode 100644
index 0000000000..5a83962583
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_e_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../spec_helper'
+
+describe "ruby -E" do
+ it "sets the external encoding with '-E external'" do
+ result = ruby_exe("print Encoding.default_external", options: '-E euc-jp')
+ result.should == "EUC-JP"
+ end
+
+ platform_is_not :windows do
+ it "also sets the filesystem encoding with '-E external'" do
+ result = ruby_exe("print Encoding.find('filesystem')", options: '-E euc-jp')
+ result.should == "EUC-JP"
+ end
+ end
+
+ it "sets the external encoding with '-E external:'" do
+ result = ruby_exe("print Encoding.default_external", options: '-E Shift_JIS:')
+ result.should == "Shift_JIS"
+ end
+
+ it "sets the internal encoding with '-E :internal'" do
+ ruby_exe("print Encoding.default_internal", options: '-E :SHIFT_JIS').
+ should == 'Shift_JIS'
+ end
+
+ it "sets the external and internal encodings with '-E external:internal'" do
+ ruby_exe("puts Encoding.default_external, Encoding.default_internal", options: '-E euc-jp:SHIFT_JIS').
+ should == "EUC-JP\nShift_JIS\n"
+ end
+
+ it "raises a RuntimeError if used with -U" do
+ ruby_exe("p 1",
+ options: '-Eascii:ascii -U',
+ args: '2>&1',
+ exit_status: 1).should =~ /RuntimeError/
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_f_spec.rb b/spec/ruby/command_line/dash_upper_f_spec.rb
new file mode 100644
index 0000000000..5c10a7140d
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_f_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../spec_helper'
+
+describe "the -F command line option" do
+ before :each do
+ @passwd = fixture __FILE__, "passwd_file.txt"
+ end
+
+ it "specifies the field separator pattern for -a" do
+ ruby_exe("puts $F[0]", options: "-naF:",
+ args: " < #{@passwd}").should ==
+ "nobody\nroot\ndaemon\n"
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_i_spec.rb b/spec/ruby/command_line/dash_upper_i_spec.rb
new file mode 100644
index 0000000000..4005a27d23
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_i_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../spec_helper'
+
+describe "The -I command line option" do
+ before :each do
+ @script = fixture __FILE__, "loadpath.rb"
+ end
+
+ it "adds the path to the load path ($:)" do
+ ruby_exe(@script, options: "-I fixtures").should.include?("fixtures")
+ end
+
+ it "adds the path at the front of $LOAD_PATH" do
+ lines = ruby_exe(@script, options: "-I fixtures").lines
+ if PlatformGuard.implementation? :ruby
+ # In a MRI checkout, $PWD ends up as the first entry in $LOAD_PATH.
+ # So just assert that it's at the beginning.
+ idx = lines.index { |l| l.include?("fixtures") }
+ idx.should < 2
+ idx.should < lines.size-1
+ else
+ lines[0].should.include?("fixtures")
+ end
+ end
+
+ it "adds the path expanded from CWD to $LOAD_PATH" do
+ ruby_exe(@script, options: "-I fixtures").lines.should.include? "#{Dir.pwd}/fixtures\n"
+ end
+
+ it "expands a path from CWD even if it does not exist" do
+ ruby_exe(@script, options: "-I not_exist/not_exist").lines.should.include? "#{Dir.pwd}/not_exist/not_exist\n"
+ end
+end
+
+platform_is_not :windows do
+ describe "The -I command line option" do
+ before :each do
+ @script = fixture __FILE__, "loadpath.rb"
+ @fixtures = File.dirname(@script)
+ @symlink = tmp("loadpath_symlink")
+ File.symlink(@fixtures, @symlink)
+ end
+
+ after :each do
+ rm_r @symlink
+ end
+
+ it "does not expand symlinks" do
+ ruby_exe(@script, options: "-I #{@symlink}").lines.should.include? "#{@symlink}\n"
+ end
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_k_spec.rb b/spec/ruby/command_line/dash_upper_k_spec.rb
new file mode 100644
index 0000000000..7e71532295
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_k_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../spec_helper'
+
+describe 'The -K command line option' do
+ before :each do
+ @test_string = "print [__ENCODING__&.name, Encoding.default_external&.name, Encoding.default_internal&.name].inspect"
+ end
+
+ describe 'sets __ENCODING__ and Encoding.default_external' do
+ it "to Encoding::BINARY with -Ka" do
+ ruby_exe(@test_string, options: '-Ka').should ==
+ [Encoding::BINARY.name, Encoding::BINARY.name, nil].inspect
+ end
+
+ it "to Encoding::BINARY with -KA" do
+ ruby_exe(@test_string, options: '-KA').should ==
+ [Encoding::BINARY.name, Encoding::BINARY.name, nil].inspect
+ end
+
+ it "to Encoding::BINARY with -Kn" do
+ ruby_exe(@test_string, options: '-Kn').should ==
+ [Encoding::BINARY.name, Encoding::BINARY.name, nil].inspect
+ end
+
+ it "to Encoding::BINARY with -KN" do
+ ruby_exe(@test_string, options: '-KN').should ==
+ [Encoding::BINARY.name, Encoding::BINARY.name, nil].inspect
+ end
+
+ it "to Encoding::EUC_JP with -Ke" do
+ ruby_exe(@test_string, options: '-Ke').should ==
+ [Encoding::EUC_JP.name, Encoding::EUC_JP.name, nil].inspect
+ end
+
+ it "to Encoding::EUC_JP with -KE" do
+ ruby_exe(@test_string, options: '-KE').should ==
+ [Encoding::EUC_JP.name, Encoding::EUC_JP.name, nil].inspect
+ end
+
+ it "to Encoding::UTF_8 with -Ku" do
+ ruby_exe(@test_string, options: '-Ku').should ==
+ [Encoding::UTF_8.name, Encoding::UTF_8.name, nil].inspect
+ end
+
+ it "to Encoding::UTF_8 with -KU" do
+ ruby_exe(@test_string, options: '-KU').should ==
+ [Encoding::UTF_8.name, Encoding::UTF_8.name, nil].inspect
+ end
+
+ it "to Encoding::Windows_31J with -Ks" do
+ ruby_exe(@test_string, options: '-Ks').should ==
+ [Encoding::Windows_31J.name, Encoding::Windows_31J.name, nil].inspect
+ end
+
+ it "to Encoding::Windows_31J with -KS" do
+ ruby_exe(@test_string, options: '-KS').should ==
+ [Encoding::Windows_31J.name, Encoding::Windows_31J.name, nil].inspect
+ end
+ end
+
+ it "ignores unknown codes" do
+ external = Encoding.find('external')
+ ruby_exe(@test_string, options: '-KZ').should ==
+ [Encoding::UTF_8.name, external.name, nil].inspect
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_s_spec.rb b/spec/ruby/command_line/dash_upper_s_spec.rb
new file mode 100644
index 0000000000..71c6016659
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_s_spec.rb
@@ -0,0 +1,67 @@
+require_relative '../spec_helper'
+
+describe 'The -S command line option' do
+ before :each do
+ @bin = fixture(__FILE__, "bin")
+ @path = [ENV['PATH'], @bin].join(File::PATH_SEPARATOR)
+ end
+
+ platform_is_not :windows do
+ # On VirtualBox shared directory (vboxsf) all files are world writable
+ # and MRI shows warning when including world writable path in ENV['PATH'].
+ # This is why we are using /success$/ matching in the following cases.
+
+ it "runs launcher found in RUBYPATH, but only code after the first /\#!.*ruby.*/-ish line in target file" do
+ result = ruby_exe(nil, options: '-S hybrid_launcher.sh', env: { 'RUBYPATH' => @bin }, args: '2>&1')
+ result.should =~ /success$/
+ end
+
+ it "runs launcher found in PATH, but only code after the first /\#!.*ruby.*/-ish line in target file" do
+ result = ruby_exe(nil, options: '-S hybrid_launcher.sh', env: { 'PATH' => @path }, args: '2>&1')
+ result.should =~ /success$/
+ end
+
+ it "runs launcher found in RUBYPATH" do
+ result = ruby_exe(nil, options: '-S launcher.rb', env: { 'RUBYPATH' => @bin }, args: '2>&1')
+ result.should =~ /success$/
+ end
+
+ it "runs launcher found in PATH" do
+ result = ruby_exe(nil, options: '-S launcher.rb', env: { 'PATH' => @path }, args: '2>&1')
+ result.should =~ /success$/
+ end
+
+ it "runs launcher found in RUBYPATH and sets the exit status to 1 if it fails" do
+ result = ruby_exe(nil, options: '-S dash_s_fail', env: { 'RUBYPATH' => @bin }, args: '2>&1', exit_status: 1)
+ result.should =~ /\bdie\b/
+ $?.exitstatus.should == 1
+ end
+
+ it "runs launcher found in PATH and sets the exit status to 1 if it fails" do
+ result = ruby_exe(nil, options: '-S dash_s_fail', env: { 'PATH' => @path }, args: '2>&1', exit_status: 1)
+ result.should =~ /\bdie\b/
+ $?.exitstatus.should == 1
+ end
+
+ ruby_version_is "4.1" do
+ describe "if the script name contains separator" do
+ before(:each) do
+ @bin = File.dirname(@bin)
+ @path = [ENV['PATH'], @bin].join(File::PATH_SEPARATOR)
+ end
+
+ it "does not search launcher in RUBYPATH" do
+ result = ruby_exe(nil, options: '-S bin/launcher.rb', env: { 'RUBYPATH' => @bin }, args: '2>&1', exit_status: 1)
+ result.should =~ /LoadError/
+ $?.should_not.success?
+ end
+
+ it "does not search launcher in PATH" do
+ result = ruby_exe(nil, options: '-S bin/launcher.rb', env: { 'PATH' => @path }, args: '2>&1', exit_status: 1)
+ result.should =~ /LoadError/
+ $?.should_not.success?
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_u_spec.rb b/spec/ruby/command_line/dash_upper_u_spec.rb
new file mode 100644
index 0000000000..2c210eb603
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_u_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../spec_helper'
+
+describe "ruby -U" do
+ it "sets Encoding.default_internal to UTF-8" do
+ ruby_exe('print Encoding.default_internal.name',
+ options: '-U').should == 'UTF-8'
+ end
+
+ it "sets Encoding.default_internal to UTF-8 when RUBYOPT is empty or only spaces" do
+ ruby_exe('p Encoding.default_internal',
+ options: '-U', env: { 'RUBYOPT' => '' }).should == "#<Encoding:UTF-8>\n"
+ ruby_exe('p Encoding.default_internal',
+ options: '-U', env: { 'RUBYOPT' => ' ' }).should == "#<Encoding:UTF-8>\n"
+ end
+
+ it "does nothing different if specified multiple times" do
+ ruby_exe('print Encoding.default_internal.name',
+ options: '-U -U').should == 'UTF-8'
+ end
+
+ it "is overruled by Encoding.default_internal=" do
+ ruby_exe('Encoding.default_internal="ascii"; print Encoding.default_internal.name',
+ options: '-U').should == 'US-ASCII'
+ end
+
+ it "does not affect the default external encoding" do
+ ruby_exe('Encoding.default_external="ascii"; print Encoding.default_external.name',
+ options: '-U').should == 'US-ASCII'
+ end
+
+ it "does not affect the source encoding" do
+ ruby_exe("print __ENCODING__.name",
+ options: '-U -KE').should == 'EUC-JP'
+ ruby_exe("print __ENCODING__.name",
+ options: '-KE -U').should == 'EUC-JP'
+ end
+
+ # I assume IO redirection will break on Windows...
+ it "raises a RuntimeError if used with -Eext:int" do
+ ruby_exe("p 1",
+ options: '-U -Eascii:ascii',
+ args: '2>&1',
+ exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError if used with -E:int" do
+ ruby_exe("p 1",
+ options: '-U -E:ascii',
+ args: '2>&1',
+ exit_status: 1).should =~ /RuntimeError/
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_w_spec.rb b/spec/ruby/command_line/dash_upper_w_spec.rb
new file mode 100644
index 0000000000..4019510d42
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_w_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../spec_helper'
+require_relative 'shared/verbose'
+
+describe "The -W command line option" do
+ before :each do
+ @script = fixture __FILE__, "verbose.rb"
+ end
+
+ it "with 0 sets $VERBOSE to nil" do
+ ruby_exe(@script, options: "-W0").chomp.should == "nil"
+ end
+
+ it "with 1 sets $VERBOSE to false" do
+ ruby_exe(@script, options: "-W1").chomp.should == "false"
+ end
+end
+
+describe "The -W command line option with 2" do
+ it_behaves_like :command_line_verbose, "-W2"
+end
+
+describe "The -W command line option with :deprecated" do
+ it "enables deprecation warnings" do
+ ruby_exe('p Warning[:deprecated]', options: '-W:deprecated').should == "true\n"
+ end
+end
+
+describe "The -W command line option with :no-deprecated" do
+ it "suppresses deprecation warnings" do
+ ruby_exe('p Warning[:deprecated]', options: '-w -W:no-deprecated').should == "false\n"
+ end
+end
+
+describe "The -W command line option with :experimental" do
+ it "enables experimental warnings" do
+ ruby_exe('p Warning[:experimental]', options: '-W:experimental').should == "true\n"
+ end
+end
+
+describe "The -W command line option with :no-experimental" do
+ it "suppresses experimental warnings" do
+ ruby_exe('p Warning[:experimental]', options: '-w -W:no-experimental').should == "false\n"
+ end
+end
diff --git a/spec/ruby/command_line/dash_upper_x_spec.rb b/spec/ruby/command_line/dash_upper_x_spec.rb
new file mode 100644
index 0000000000..8ef9aae4b1
--- /dev/null
+++ b/spec/ruby/command_line/dash_upper_x_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../spec_helper'
+require_relative 'shared/change_directory'
+
+describe "The -X command line option" do
+ it_behaves_like :command_line_change_directory, "-X"
+end
diff --git a/spec/ruby/command_line/dash_v_spec.rb b/spec/ruby/command_line/dash_v_spec.rb
new file mode 100644
index 0000000000..6a4f7d3a15
--- /dev/null
+++ b/spec/ruby/command_line/dash_v_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../spec_helper'
+require_relative 'shared/verbose'
+
+describe "The -v command line option" do
+ it_behaves_like :command_line_verbose, "-v"
+
+ describe "when used alone" do
+ it "prints version and ends" do
+ ruby_exe(nil, args: '-v').gsub("+PRISM ", "").should.include?(RUBY_DESCRIPTION.gsub("+PRISM ", ""))
+ end unless (defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?) ||
+ (defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled?) ||
+ (ENV['RUBY_GC_LIBRARY'] && ENV['RUBY_GC_LIBRARY'].length > 0) ||
+ (ENV['RUBY_MN_THREADS'] == '1')
+ end
+end
diff --git a/spec/ruby/command_line/dash_w_spec.rb b/spec/ruby/command_line/dash_w_spec.rb
new file mode 100644
index 0000000000..f310dca3ed
--- /dev/null
+++ b/spec/ruby/command_line/dash_w_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../spec_helper'
+require_relative 'shared/verbose'
+
+describe "The -w command line option" do
+ it_behaves_like :command_line_verbose, "-w"
+
+ it "enables both deprecated and experimental warnings" do
+ ruby_exe('p Warning[:deprecated]; p Warning[:experimental]', options: '-w').should == "true\ntrue\n"
+ end
+end
diff --git a/spec/ruby/command_line/dash_x_spec.rb b/spec/ruby/command_line/dash_x_spec.rb
new file mode 100644
index 0000000000..38f97a5ab1
--- /dev/null
+++ b/spec/ruby/command_line/dash_x_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../spec_helper'
+
+describe "The -x command line option" do
+ it "runs code after the first /\#!.*ruby.*/-ish line in target file" do
+ embedded_ruby = fixture __FILE__, "bin/embedded_ruby.txt"
+ result = ruby_exe(embedded_ruby, options: '-x')
+ result.should == "success\n"
+ end
+
+ it "fails when /\#!.*ruby.*/-ish line in target file is not found" do
+ bad_embedded_ruby = fixture __FILE__, "bin/bad_embedded_ruby.txt"
+ result = ruby_exe(bad_embedded_ruby, options: '-x', args: '2>&1', exit_status: 1)
+ result.should.include? "no Ruby script found in input"
+ end
+
+ it "behaves as -x was set when non-ruby shebang is encountered on first line" do
+ embedded_ruby = fixture __FILE__, "bin/hybrid_launcher.sh"
+ result = ruby_exe(embedded_ruby)
+ result.should == "success\n"
+ end
+end
diff --git a/spec/ruby/command_line/error_message_spec.rb b/spec/ruby/command_line/error_message_spec.rb
new file mode 100644
index 0000000000..157cb8969c
--- /dev/null
+++ b/spec/ruby/command_line/error_message_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../spec_helper'
+
+describe "The error message caused by an exception" do
+ it "is not printed to stdout" do
+ out = ruby_exe("this_does_not_exist", args: "2> #{File::NULL}", exit_status: 1)
+ out.chomp.should.empty?
+
+ out = ruby_exe("end #syntax error", args: "2> #{File::NULL}", exit_status: 1)
+ out.chomp.should.empty?
+ end
+
+ it "is not modified with extra escaping of control characters and backslashes" do
+ out = ruby_exe('raise "\e[31mRed\e[0m error\\\\message"', args: "2>&1", exit_status: 1)
+ out.chomp.should.include?("\e[31mRed\e[0m error\\message")
+ end
+end
diff --git a/spec/ruby/command_line/feature_spec.rb b/spec/ruby/command_line/feature_spec.rb
new file mode 100644
index 0000000000..0a3252b88d
--- /dev/null
+++ b/spec/ruby/command_line/feature_spec.rb
@@ -0,0 +1,71 @@
+require_relative '../spec_helper'
+
+describe "The --enable and --disable flags" do
+ before :all do
+ # Since some specs disable reading RUBYOPT, we instead pass its contents as :options for those specs
+ rubyopt = [ENV["RUBYOPT"]]
+ rubyopt << ENV["#{RUBY_ENGINE.upcase}OPT"] unless RUBY_ENGINE == 'ruby'
+ @rubyopt = RUBY_ENGINE == "ruby" ? "" : rubyopt.compact.join(" ")
+ end
+
+ it "can be used with gems" do
+ ruby_exe("p defined?(Gem)", options: "--enable=gems").chomp.should == "\"constant\""
+ ruby_exe("p defined?(Gem)", options: "--disable=gems").chomp.should == "nil"
+ ruby_exe("p defined?(Gem)", options: "--enable-gems").chomp.should == "\"constant\""
+ ruby_exe("p defined?(Gem)", options: "--disable-gems").chomp.should == "nil"
+ end
+
+ it "can be used with gem" do
+ ruby_exe("p defined?(Gem)", options: "--enable=gem").chomp.should == "\"constant\""
+ ruby_exe("p defined?(Gem)", options: "--disable=gem").chomp.should == "nil"
+ ruby_exe("p defined?(Gem)", options: "--enable-gem").chomp.should == "\"constant\""
+ ruby_exe("p defined?(Gem)", options: "--disable-gem").chomp.should == "nil"
+ end
+
+ it "can be used with did_you_mean" do
+ ruby_exe("p defined?(DidYouMean)", options: "--enable=did_you_mean").chomp.should == "\"constant\""
+ ruby_exe("p defined?(DidYouMean)", options: "--disable=did_you_mean").chomp.should == "nil"
+ ruby_exe("p defined?(DidYouMean)", options: "--enable-did_you_mean").chomp.should == "\"constant\""
+ ruby_exe("p defined?(DidYouMean)", options: "--disable-did_you_mean").chomp.should == "nil"
+ end
+
+ it "can be used with rubyopt" do
+ ruby_exe("p $VERBOSE", options: "--enable=rubyopt", env: {'RUBYOPT' => '-w'}).chomp.should == "true"
+ ruby_exe("p $VERBOSE", options: "#{@rubyopt} --disable=rubyopt", env: {'RUBYOPT' => '-w'}).chomp.should == "false"
+ ruby_exe("p $VERBOSE", options: "--enable-rubyopt", env: {'RUBYOPT' => '-w'}).chomp.should == "true"
+ ruby_exe("p $VERBOSE", options: "#{@rubyopt} --disable-rubyopt", env: {'RUBYOPT' => '-w'}).chomp.should == "false"
+ end
+
+ it "can be used with frozen-string-literal" do
+ ruby_exe("p 'foo'.frozen?", options: "--enable=frozen-string-literal").chomp.should == "true"
+ ruby_exe("p 'foo'.frozen?", options: "--disable=frozen-string-literal").chomp.should == "false"
+ ruby_exe("p 'foo'.frozen?", options: "--enable-frozen-string-literal").chomp.should == "true"
+ ruby_exe("p 'foo'.frozen?", options: "--disable-frozen-string-literal").chomp.should == "false"
+ end
+
+ # frequently hangs for >60s on GitHub Actions macos-latest
+ # MinGW's YJIT support seems broken
+ platform_is_not :darwin, :mingw do
+ it "can be used with all for enable" do
+ e = "p [defined?(Gem), defined?(DidYouMean), $VERBOSE, 'foo'.frozen?]"
+ env = {'RUBYOPT' => '-w'}
+ # Use a single variant here because it can be quite slow as it might enable jit, etc
+ ruby_exe(e, options: "--enable-all", env: env).chomp.should == "[\"constant\", \"constant\", true, true]"
+ end unless defined?(RubyVM::YJIT) && defined?(RubyVM::ZJIT) && RubyVM::ZJIT.enabled? # You're not supposed to enable YJIT with --enable-all when ZJIT options are passed.
+ end
+
+ it "can be used with all for disable" do
+ e = "p [defined?(Gem), defined?(DidYouMean), $VERBOSE, 'foo'.frozen?]"
+ env = {'RUBYOPT' => '-w'}
+ ruby_exe(e, options: "#{@rubyopt} --disable=all", env: env).chomp.should == "[nil, nil, false, false]"
+ ruby_exe(e, options: "#{@rubyopt} --disable-all", env: env).chomp.should == "[nil, nil, false, false]"
+ end
+
+ it "prints a warning for unknown features" do
+ ruby_exe("p 14", options: "--enable=ruby-spec-feature-does-not-exist 2>&1").chomp.should.include?('warning: unknown argument for --enable')
+ ruby_exe("p 14", options: "--disable=ruby-spec-feature-does-not-exist 2>&1").chomp.should.include?('warning: unknown argument for --disable')
+ ruby_exe("p 14", options: "--enable-ruby-spec-feature-does-not-exist 2>&1").chomp.should.include?('warning: unknown argument for --enable')
+ ruby_exe("p 14", options: "--disable-ruby-spec-feature-does-not-exist 2>&1").chomp.should.include?('warning: unknown argument for --disable')
+ end
+
+end
diff --git a/spec/ruby/command_line/fixtures/backtrace.rb b/spec/ruby/command_line/fixtures/backtrace.rb
new file mode 100644
index 0000000000..99acae95c8
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/backtrace.rb
@@ -0,0 +1,35 @@
+def a
+ raise 'oops'
+end
+
+def b
+ a
+end
+
+def c
+ b
+end
+
+def d
+ c
+end
+
+arg = ARGV.first
+$stderr.puts arg
+
+case arg
+when 'full_message'
+ begin
+ d
+ rescue => exc
+ puts exc.full_message
+ end
+when 'backtrace'
+ begin
+ d
+ rescue => exc
+ puts exc.backtrace
+ end
+else
+ d
+end
diff --git a/spec/ruby/command_line/fixtures/bad_syntax.rb b/spec/ruby/command_line/fixtures/bad_syntax.rb
new file mode 100644
index 0000000000..e7b8c7a357
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/bad_syntax.rb
@@ -0,0 +1 @@
+f {
diff --git a/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt b/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt
new file mode 100644
index 0000000000..61b946977a
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/bin/bad_embedded_ruby.txt
@@ -0,0 +1,3 @@
+@@@This line is not valid Ruby
+#!rub_y
+puts 'success'
diff --git a/spec/ruby/command_line/fixtures/bin/dash_s_fail b/spec/ruby/command_line/fixtures/bin/dash_s_fail
new file mode 100644
index 0000000000..70c1b8759c
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/bin/dash_s_fail
@@ -0,0 +1 @@
+raise 'die'
diff --git a/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt b/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt
new file mode 100644
index 0000000000..0ec0f358db
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/bin/embedded_ruby.txt
@@ -0,0 +1,3 @@
+@@@This line is not valid Ruby
+#!ruby
+puts 'success'
diff --git a/spec/ruby/command_line/fixtures/bin/hybrid_launcher.sh b/spec/ruby/command_line/fixtures/bin/hybrid_launcher.sh
new file mode 100644
index 0000000000..fd3249f0e5
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/bin/hybrid_launcher.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+echo 'error' && exit 1
+#!ruby
+puts 'success'
diff --git a/spec/ruby/command_line/fixtures/bin/launcher.rb b/spec/ruby/command_line/fixtures/bin/launcher.rb
new file mode 100755
index 0000000000..92a0ee2b49
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/bin/launcher.rb
@@ -0,0 +1,2 @@
+#!ruby
+puts 'success'
diff --git a/spec/ruby/command_line/fixtures/change_directory_script.rb b/spec/ruby/command_line/fixtures/change_directory_script.rb
new file mode 100644
index 0000000000..abe244705f
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/change_directory_script.rb
@@ -0,0 +1 @@
+print Dir.pwd
diff --git a/spec/ruby/command_line/fixtures/conditional_range.txt b/spec/ruby/command_line/fixtures/conditional_range.txt
new file mode 100644
index 0000000000..8a1218a102
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/conditional_range.txt
@@ -0,0 +1,5 @@
+1
+2
+3
+4
+5
diff --git a/spec/ruby/command_line/fixtures/dash_s_script.rb b/spec/ruby/command_line/fixtures/dash_s_script.rb
new file mode 100644
index 0000000000..500eccbb84
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/dash_s_script.rb
@@ -0,0 +1,12 @@
+which = ARGV.shift.to_i
+
+case which
+when 0
+ p $n
+when 1
+ puts $n
+when 2
+ puts $_name
+when 3
+ puts $___name__test__
+end
diff --git a/spec/ruby/command_line/fixtures/debug.rb b/spec/ruby/command_line/fixtures/debug.rb
new file mode 100644
index 0000000000..2d84c5faf6
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/debug.rb
@@ -0,0 +1,10 @@
+which = ARGV.first.to_i
+
+case which
+when 0
+ puts "$DEBUG #{$DEBUG}"
+when 1
+ puts "$VERBOSE #{$VERBOSE}"
+when 2
+ puts "$-d #{$-d}"
+end
diff --git a/spec/ruby/command_line/fixtures/debug_info.rb b/spec/ruby/command_line/fixtures/debug_info.rb
new file mode 100644
index 0000000000..f02b041920
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/debug_info.rb
@@ -0,0 +1,10 @@
+a = 'string'
+b = a
+c = b
+d = c
+e = d
+begin
+ a << 'new part'
+rescue Exception => e
+ print e.message
+end
diff --git a/spec/ruby/command_line/fixtures/freeze_flag_across_files.rb b/spec/ruby/command_line/fixtures/freeze_flag_across_files.rb
new file mode 100644
index 0000000000..b258249f3a
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/freeze_flag_across_files.rb
@@ -0,0 +1,3 @@
+require_relative 'freeze_flag_required'
+
+p "abc".object_id == $second_literal_id
diff --git a/spec/ruby/command_line/fixtures/freeze_flag_across_files_diff_enc.rb b/spec/ruby/command_line/fixtures/freeze_flag_across_files_diff_enc.rb
new file mode 100644
index 0000000000..e9f045e9ea
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/freeze_flag_across_files_diff_enc.rb
@@ -0,0 +1,3 @@
+require_relative 'freeze_flag_required_diff_enc'
+
+p "abc".object_id != $second_literal_id
diff --git a/spec/ruby/command_line/fixtures/freeze_flag_one_literal.rb b/spec/ruby/command_line/fixtures/freeze_flag_one_literal.rb
new file mode 100644
index 0000000000..3718899d61
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/freeze_flag_one_literal.rb
@@ -0,0 +1,2 @@
+ids = Array.new(2) { "abc".object_id }
+p ids.first == ids.last
diff --git a/spec/ruby/command_line/fixtures/freeze_flag_required.rb b/spec/ruby/command_line/fixtures/freeze_flag_required.rb
new file mode 100644
index 0000000000..e09232a5f4
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/freeze_flag_required.rb
@@ -0,0 +1 @@
+$second_literal_id = "abc".object_id
diff --git a/spec/ruby/command_line/fixtures/freeze_flag_required_diff_enc.rb b/spec/ruby/command_line/fixtures/freeze_flag_required_diff_enc.rb
new file mode 100644
index 0000000000..df4b952c46
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/freeze_flag_required_diff_enc.rb
@@ -0,0 +1,3 @@
+# encoding: euc-jp # built-in for old regexp option
+
+$second_literal_id = "abc".object_id
diff --git a/spec/ruby/command_line/fixtures/freeze_flag_two_literals.rb b/spec/ruby/command_line/fixtures/freeze_flag_two_literals.rb
new file mode 100644
index 0000000000..f5547a5bae
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/freeze_flag_two_literals.rb
@@ -0,0 +1 @@
+p "abc".equal?("abc")
diff --git a/spec/ruby/command_line/fixtures/full_names.txt b/spec/ruby/command_line/fixtures/full_names.txt
new file mode 100644
index 0000000000..602a20b9dd
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/full_names.txt
@@ -0,0 +1,3 @@
+alice jones
+bob field
+james grey
diff --git a/spec/ruby/command_line/fixtures/loadpath.rb b/spec/ruby/command_line/fixtures/loadpath.rb
new file mode 100644
index 0000000000..d7fdf45d46
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/loadpath.rb
@@ -0,0 +1 @@
+puts $:
diff --git a/spec/ruby/command_line/fixtures/names.txt b/spec/ruby/command_line/fixtures/names.txt
new file mode 100644
index 0000000000..ae4bf4c8ad
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/names.txt
@@ -0,0 +1,3 @@
+alice
+bob
+james
diff --git a/spec/ruby/command_line/fixtures/passwd_file.txt b/spec/ruby/command_line/fixtures/passwd_file.txt
new file mode 100644
index 0000000000..08a4b23bbd
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/passwd_file.txt
@@ -0,0 +1,3 @@
+nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false
+root:*:0:0:System Administrator:/var/root:/bin/sh
+daemon:*:1:1:System Services:/var/root:/usr/bin/false
diff --git a/spec/ruby/command_line/fixtures/require.rb b/spec/ruby/command_line/fixtures/require.rb
new file mode 100644
index 0000000000..0be7049c66
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/require.rb
@@ -0,0 +1 @@
+puts $"
diff --git a/spec/ruby/command_line/fixtures/rubyopt.rb b/spec/ruby/command_line/fixtures/rubyopt.rb
new file mode 100644
index 0000000000..48d81e1bca
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/rubyopt.rb
@@ -0,0 +1 @@
+puts "rubyopt.rb required"
diff --git a/spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb b/spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb
new file mode 100644
index 0000000000..fb84b546c0
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/string_literal_frozen_comment.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+frozen = "test".frozen?
+interned = "test".equal?("test")
+puts "frozen:#{frozen} interned:#{interned}"
diff --git a/spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb b/spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb
new file mode 100644
index 0000000000..381a742001
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/string_literal_mutable_comment.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: false
+frozen = "test".frozen?
+interned = "test".equal?("test")
+puts "frozen:#{frozen} interned:#{interned}"
diff --git a/spec/ruby/command_line/fixtures/string_literal_raw.rb b/spec/ruby/command_line/fixtures/string_literal_raw.rb
new file mode 100644
index 0000000000..56b1841296
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/string_literal_raw.rb
@@ -0,0 +1,3 @@
+frozen = "test".frozen?
+interned = "test".equal?("test")
+puts "frozen:#{frozen} interned:#{interned}"
diff --git a/spec/ruby/command_line/fixtures/test_file.rb b/spec/ruby/command_line/fixtures/test_file.rb
new file mode 100644
index 0000000000..30a832299e
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/test_file.rb
@@ -0,0 +1 @@
+puts "REQUIRED"
diff --git a/spec/ruby/command_line/fixtures/verbose.rb b/spec/ruby/command_line/fixtures/verbose.rb
new file mode 100644
index 0000000000..2aa99ed44d
--- /dev/null
+++ b/spec/ruby/command_line/fixtures/verbose.rb
@@ -0,0 +1 @@
+puts $VERBOSE.inspect
diff --git a/spec/ruby/command_line/frozen_strings_spec.rb b/spec/ruby/command_line/frozen_strings_spec.rb
new file mode 100644
index 0000000000..32ff7d0371
--- /dev/null
+++ b/spec/ruby/command_line/frozen_strings_spec.rb
@@ -0,0 +1,94 @@
+require_relative '../spec_helper'
+
+describe "The --enable-frozen-string-literal flag causes string literals to" do
+
+ it "produce the same object each time" do
+ ruby_exe(fixture(__FILE__, "freeze_flag_one_literal.rb"), options: "--enable-frozen-string-literal").chomp.should == "true"
+ end
+
+ it "produce the same object for literals with the same content" do
+ ruby_exe(fixture(__FILE__, "freeze_flag_two_literals.rb"), options: "--enable-frozen-string-literal").chomp.should == "true"
+ end
+
+ it "produce the same object for literals with the same content in different files" do
+ ruby_exe(fixture(__FILE__, "freeze_flag_across_files.rb"), options: "--enable-frozen-string-literal").chomp.should == "true"
+ end
+
+ it "produce different objects for literals with the same content in different files if they have different encodings" do
+ ruby_exe(fixture(__FILE__, "freeze_flag_across_files_diff_enc.rb"), options: "--enable-frozen-string-literal").chomp.should == "true"
+ end
+end
+
+describe "The --disable-frozen-string-literal flag causes string literals to" do
+
+ it "produce a different object each time" do
+ ruby_exe(fixture(__FILE__, "freeze_flag_one_literal.rb"), options: "--disable-frozen-string-literal").chomp.should == "false"
+ end
+
+end
+
+describe "With neither --enable-frozen-string-literal nor --disable-frozen-string-literal flag set" do
+ before do
+ # disable --enable-frozen-string-literal and --disable-frozen-string-literal passed in $RUBYOPT
+ @rubyopt = ENV["RUBYOPT"]
+ ENV["RUBYOPT"] = ""
+ end
+
+ after do
+ ENV["RUBYOPT"] = @rubyopt
+ end
+
+ it "produce a different object each time" do
+ ruby_exe(fixture(__FILE__, "freeze_flag_one_literal.rb")).chomp.should == "false"
+ end
+
+ context "if file has no frozen_string_literal comment" do
+ it "produce different mutable strings each time" do
+ ruby_exe(fixture(__FILE__, "string_literal_raw.rb")).chomp.should == "frozen:false interned:false"
+ end
+
+ guard -> { ruby_version_is "3.4" and !"test".frozen? } do
+ it "complain about modification of produced mutable strings" do
+ -> { eval(<<~RUBY) }.should complain(/warning: literal string will be frozen in the future \(run with --debug-frozen-string-literal for more information\)/)
+ "test" << "!"
+ RUBY
+ end
+
+ it "does not complain about modification if Warning[:deprecated] is false" do
+ deprecated = Warning[:deprecated]
+ Warning[:deprecated] = false
+ -> { eval(<<~RUBY) }.should_not complain
+ "test" << "!"
+ RUBY
+ ensure
+ Warning[:deprecated] = deprecated
+ end
+ end
+ end
+
+ it "if file has frozen_string_literal:true comment produce same frozen strings each time" do
+ ruby_exe(fixture(__FILE__, "string_literal_frozen_comment.rb")).chomp.should == "frozen:true interned:true"
+ end
+
+ it "if file has frozen_string_literal:false comment produce different mutable strings each time" do
+ ruby_exe(fixture(__FILE__, "string_literal_mutable_comment.rb")).chomp.should == "frozen:false interned:false"
+ end
+end
+
+describe "The --debug flag produces" do
+ it "debugging info on attempted frozen string modification" do
+ error_str = ruby_exe(fixture(__FILE__, 'debug_info.rb'), options: '--enable-frozen-string-literal --debug', args: "2>&1")
+ error_str.should.include?("can't modify frozen String")
+ error_str.should.include?("created at")
+ error_str.should.include?("command_line/fixtures/debug_info.rb:1")
+ end
+
+ guard -> { ruby_version_is "3.4" and !"test".frozen? } do
+ it "debugging info on mutating chilled string" do
+ error_str = ruby_exe(fixture(__FILE__, 'debug_info.rb'), options: '-w --debug', args: "2>&1")
+ error_str.should.include?("literal string will be frozen in the future")
+ error_str.should.include?("the string was created here")
+ error_str.should.include?("command_line/fixtures/debug_info.rb:1")
+ end
+ end
+end
diff --git a/spec/ruby/command_line/rubylib_spec.rb b/spec/ruby/command_line/rubylib_spec.rb
new file mode 100644
index 0000000000..1bedd146e3
--- /dev/null
+++ b/spec/ruby/command_line/rubylib_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../spec_helper'
+
+describe "The RUBYLIB environment variable" do
+ before :each do
+ @rubylib, ENV["RUBYLIB"] = ENV["RUBYLIB"], nil
+ @pre = @rubylib.nil? ? '' : @rubylib + File::PATH_SEPARATOR
+ end
+
+ after :each do
+ ENV["RUBYLIB"] = @rubylib
+ end
+
+ it "adds a directory to $LOAD_PATH" do
+ dir = tmp("rubylib/incl")
+ ENV["RUBYLIB"] = @pre + dir
+ paths = ruby_exe("puts $LOAD_PATH").lines.map(&:chomp)
+ paths.should.include?(dir)
+ end
+
+ it "adds a File::PATH_SEPARATOR-separated list of directories to $LOAD_PATH" do
+ dir1, dir2 = tmp("rubylib/incl1"), tmp("rubylib/incl2")
+ ENV["RUBYLIB"] = @pre + "#{dir1}#{File::PATH_SEPARATOR}#{dir2}"
+ paths = ruby_exe("puts $LOAD_PATH").lines.map(&:chomp)
+ paths.should.include?(dir1)
+ paths.should.include?(dir2)
+ paths.index(dir1).should < paths.index(dir2)
+ end
+
+ it "adds the directory at the front of $LOAD_PATH" do
+ dir = tmp("rubylib/incl_front")
+ ENV["RUBYLIB"] = @pre + dir
+ paths = ruby_exe("puts $LOAD_PATH").lines.map(&:chomp)
+ paths.shift if paths.first.end_with?('/gem-rehash')
+ if PlatformGuard.implementation? :ruby
+ # In a MRI checkout, $PWD and some extra -I entries end up as
+ # the first entries in $LOAD_PATH. So just assert that it's not last.
+ idx = paths.index(dir)
+ idx.should < paths.size-1
+ else
+ paths[0].should == dir
+ end
+ end
+
+ it "adds the directory after directories added by -I" do
+ dash_i_dir = tmp("dash_I_include")
+ rubylib_dir = tmp("rubylib_include")
+ ENV["RUBYLIB"] = @pre + rubylib_dir
+ paths = ruby_exe("puts $LOAD_PATH", options: "-I #{dash_i_dir}").lines.map(&:chomp)
+ paths.should.include?(dash_i_dir)
+ paths.should.include?(rubylib_dir)
+ paths.index(dash_i_dir).should < paths.index(rubylib_dir)
+ end
+
+ it "adds the directory after directories added by -I within RUBYOPT" do
+ rubyopt_dir = tmp("rubyopt_include")
+ rubylib_dir = tmp("rubylib_include")
+ ENV["RUBYLIB"] = @pre + rubylib_dir
+ paths = ruby_exe("puts $LOAD_PATH", env: { "RUBYOPT" => "-I#{rubyopt_dir}" }).lines.map(&:chomp)
+ paths.should.include?(rubyopt_dir)
+ paths.should.include?(rubylib_dir)
+ paths.index(rubyopt_dir).should < paths.index(rubylib_dir)
+ end
+
+ it "keeps spaces in the value" do
+ ENV["RUBYLIB"] = @pre + " rubylib/incl "
+ out = ruby_exe("puts $LOAD_PATH")
+ out.should.include?(" rubylib/incl ")
+ end
+end
diff --git a/spec/ruby/command_line/rubyopt_spec.rb b/spec/ruby/command_line/rubyopt_spec.rb
new file mode 100644
index 0000000000..eb297cd6fe
--- /dev/null
+++ b/spec/ruby/command_line/rubyopt_spec.rb
@@ -0,0 +1,185 @@
+require_relative '../spec_helper'
+
+describe "Processing RUBYOPT" do
+ before :each do
+ @rubyopt, ENV["RUBYOPT"] = ENV["RUBYOPT"], nil
+ end
+
+ after :each do
+ ENV["RUBYOPT"] = @rubyopt
+ end
+
+ it "adds the -I path to $LOAD_PATH" do
+ ENV["RUBYOPT"] = "-Ioptrubyspecincl"
+ result = ruby_exe("puts $LOAD_PATH.grep(/byspecin/)")
+ result.chomp[-15..-1].should == "optrubyspecincl"
+ end
+
+ it "sets $DEBUG to true for '-d'" do
+ ENV["RUBYOPT"] = '-d'
+ command = %[puts "value of $DEBUG is \#{$DEBUG}"]
+ result = ruby_exe(command, args: "2>&1")
+ result.should =~ /value of \$DEBUG is true/
+ end
+
+ guard -> { RbConfig::CONFIG["CROSS_COMPILING"] != "yes" } do
+ it "prints the version number for '-v'" do
+ ENV["RUBYOPT"] = '-v'
+ ruby_exe("")[/\A.*/].gsub(/\s\+(YJIT( \w+)?|ZJIT( \w+)?|PRISM|GC(\[\w+\])?)(?=\s)/, "").should == RUBY_DESCRIPTION.gsub(/\s\+(YJIT( \w+)?|ZJIT( \w+)?|PRISM|GC(\[\w+\])?)(?=\s)/, "")
+ end
+
+ it "ignores whitespace around the option" do
+ ENV["RUBYOPT"] = ' -v '
+ ruby_exe("")[/\A.*/].gsub(/\s\+(YJIT( \w+)?|ZJIT( \w+)?|PRISM|GC(\[\w+\])?)(?=\s)/, "").should == RUBY_DESCRIPTION.gsub(/\s\+(YJIT( \w+)?|ZJIT( \w+)?|PRISM|GC(\[\w+\])?)(?=\s)/, "")
+ end
+ end
+
+ it "sets $VERBOSE to true for '-w'" do
+ ENV["RUBYOPT"] = '-w'
+ ruby_exe("p $VERBOSE").chomp.should == "true"
+ end
+
+ it "sets $VERBOSE to true for '-W'" do
+ ENV["RUBYOPT"] = '-W'
+ ruby_exe("p $VERBOSE").chomp.should == "true"
+ end
+
+ it "sets $VERBOSE to nil for '-W0'" do
+ ENV["RUBYOPT"] = '-W0'
+ ruby_exe("p $VERBOSE").chomp.should == "nil"
+ end
+
+ it "sets $VERBOSE to false for '-W1'" do
+ ENV["RUBYOPT"] = '-W1'
+ ruby_exe("p $VERBOSE").chomp.should == "false"
+ end
+
+ it "sets $VERBOSE to true for '-W2'" do
+ ENV["RUBYOPT"] = '-W2'
+ ruby_exe("p $VERBOSE").chomp.should == "true"
+ end
+
+ it "suppresses deprecation warnings for '-W:no-deprecated'" do
+ ENV["RUBYOPT"] = '-W:no-deprecated'
+ result = ruby_exe('$; = ""', args: '2>&1')
+ result.should == ""
+ end
+
+ it "suppresses experimental warnings for '-W:no-experimental'" do
+ ENV["RUBYOPT"] = '-W:no-experimental'
+ result = ruby_exe('case 0; in a; end', args: '2>&1')
+ result.should == ""
+ end
+
+ it "suppresses deprecation and experimental warnings for '-W:no-deprecated -W:no-experimental'" do
+ ENV["RUBYOPT"] = '-W:no-deprecated -W:no-experimental'
+ result = ruby_exe('case ($; = ""); in a; end', args: '2>&1')
+ result.should == ""
+ end
+
+ it "requires the file for '-r'" do
+ f = fixture __FILE__, "rubyopt"
+ ENV["RUBYOPT"] = "-r#{f}"
+ ruby_exe("0", args: '2>&1').should =~ /^rubyopt.rb required/
+ end
+
+ it "raises a RuntimeError for '-a'" do
+ ENV["RUBYOPT"] = '-a'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-p'" do
+ ENV["RUBYOPT"] = '-p'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-n'" do
+ ENV["RUBYOPT"] = '-n'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-y'" do
+ ENV["RUBYOPT"] = '-y'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-c'" do
+ ENV["RUBYOPT"] = '-c'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-s'" do
+ ENV["RUBYOPT"] = '-s'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-h'" do
+ ENV["RUBYOPT"] = '-h'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '--help'" do
+ ENV["RUBYOPT"] = '--help'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-l'" do
+ ENV["RUBYOPT"] = '-l'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-S'" do
+ ENV["RUBYOPT"] = '-S irb'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-e'" do
+ ENV["RUBYOPT"] = '-e0'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-i'" do
+ ENV["RUBYOPT"] = '-i.bak'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-x'" do
+ ENV["RUBYOPT"] = '-x'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-C'" do
+ ENV["RUBYOPT"] = '-C'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-X'" do
+ ENV["RUBYOPT"] = '-X.'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-F'" do
+ ENV["RUBYOPT"] = '-F'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '-0'" do
+ ENV["RUBYOPT"] = '-0'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '--copyright'" do
+ ENV["RUBYOPT"] = '--copyright'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '--version'" do
+ ENV["RUBYOPT"] = '--version'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+
+ it "raises a RuntimeError for '--yydebug'" do
+ ENV["RUBYOPT"] = '--yydebug'
+ ruby_exe("", args: '2>&1', exit_status: 1).should =~ /RuntimeError/
+ end
+end
diff --git a/spec/ruby/command_line/shared/change_directory.rb b/spec/ruby/command_line/shared/change_directory.rb
new file mode 100644
index 0000000000..9cb6e90ac6
--- /dev/null
+++ b/spec/ruby/command_line/shared/change_directory.rb
@@ -0,0 +1,21 @@
+describe :command_line_change_directory, shared: true do
+ before :all do
+ @script = fixture(__FILE__, 'change_directory_script.rb')
+ @tempdir = File.dirname(@script)
+ end
+
+ it 'changes the PWD when using a file' do
+ output = ruby_exe(@script, options: "#{@method} #{@tempdir}")
+ output.should == @tempdir
+ end
+
+ it 'does not need a space after -C for the argument' do
+ output = ruby_exe(@script, options: "#{@method}#{@tempdir}")
+ output.should == @tempdir
+ end
+
+ it 'changes the PWD when using -e' do
+ output = ruby_exe(nil, options: "#{@method} #{@tempdir} -e 'print Dir.pwd'")
+ output.should == @tempdir
+ end
+end
diff --git a/spec/ruby/command_line/shared/verbose.rb b/spec/ruby/command_line/shared/verbose.rb
new file mode 100644
index 0000000000..c5c44c269e
--- /dev/null
+++ b/spec/ruby/command_line/shared/verbose.rb
@@ -0,0 +1,9 @@
+describe :command_line_verbose, shared: true do
+ before :each do
+ @script = fixture __FILE__, "verbose.rb"
+ end
+
+ it "sets $VERBOSE to true" do
+ ruby_exe(@script, options: @method).chomp.b.split.last.should == "true"
+ end
+end
diff --git a/spec/ruby/command_line/syntax_error_spec.rb b/spec/ruby/command_line/syntax_error_spec.rb
new file mode 100644
index 0000000000..88864c048e
--- /dev/null
+++ b/spec/ruby/command_line/syntax_error_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../spec_helper'
+
+describe "The interpreter" do
+ it "prints an error when given a file with invalid syntax" do
+ out = ruby_exe(fixture(__FILE__, "bad_syntax.rb"), args: "2>&1", exit_status: 1)
+ out.should.include?("SyntaxError")
+ end
+
+ it "prints an error when given code via -e with invalid syntax" do
+ out = ruby_exe(nil, args: "-e 'a{' 2>&1", exit_status: 1)
+ out.should.include?("SyntaxError")
+ end
+end
diff --git a/spec/ruby/core/argf/argf_spec.rb b/spec/ruby/core/argf/argf_spec.rb
new file mode 100644
index 0000000000..f9468539bb
--- /dev/null
+++ b/spec/ruby/core/argf/argf_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "ARGF" do
+ it "is extended by the Enumerable module" do
+ ARGF.should.is_a?(Enumerable)
+ end
+
+ it "is an instance of ARGF.class" do
+ ARGF.should.instance_of?(ARGF.class)
+ end
+end
diff --git a/spec/ruby/core/argf/argv_spec.rb b/spec/ruby/core/argf/argv_spec.rb
new file mode 100644
index 0000000000..77dfe78c21
--- /dev/null
+++ b/spec/ruby/core/argf/argv_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.argv" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ it "returns ARGV for the initial ARGF" do
+ ARGF.argv.should.equal? ARGV
+ end
+
+ it "returns the remaining arguments to treat" do
+ argf [@file1, @file2] do
+ # @file1 is stored in current file
+ @argf.argv.should == [@file2]
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/binmode_spec.rb b/spec/ruby/core/argf/binmode_spec.rb
new file mode 100644
index 0000000000..5288e3199d
--- /dev/null
+++ b/spec/ruby/core/argf/binmode_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.binmode" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ @bin_file = fixture __FILE__, "bin_file.txt"
+ end
+
+ it "returns self" do
+ argf [@bin_file] do
+ @argf.binmode.should.equal? @argf
+ end
+ end
+
+ platform_is :windows do
+ it "puts reading into binmode" do
+ argf [@bin_file, @bin_file] do
+ @argf.gets.should == "test\n"
+ @argf.binmode
+ @argf.gets.should == "test\r\n"
+ end
+ end
+
+ it "puts all subsequent streams reading through ARGF into binmode" do
+ argf [@bin_file, @bin_file] do
+ @argf.binmode
+ @argf.gets.should == "test\r\n"
+ @argf.gets.should == "test\r\n"
+ end
+ end
+ end
+
+ it "sets the file's encoding to BINARY" do
+ argf [@bin_file, @file1] do
+ @argf.binmode
+ @argf.should.binmode?
+ @argf.gets.encoding.should == Encoding::BINARY
+ @argf.skip
+ @argf.read.encoding.should == Encoding::BINARY
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/close_spec.rb b/spec/ruby/core/argf/close_spec.rb
new file mode 100644
index 0000000000..8ca7d71dc2
--- /dev/null
+++ b/spec/ruby/core/argf/close_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.close" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+ end
+
+ it "closes the current open stream" do
+ argf [@file1_name, @file2_name] do
+ io = @argf.to_io
+ @argf.close
+ io.closed?.should == true
+ end
+ end
+
+ it "returns self" do
+ argf [@file1_name, @file2_name] do
+ @argf.close.should.equal?(@argf)
+ end
+ end
+
+ it "doesn't raise an IOError if called on a closed stream" do
+ argf [@file1_name] do
+ -> { @argf.close }.should_not.raise
+ -> { @argf.close }.should_not.raise
+ end
+ end
+end
+
+describe "ARGF.close" do
+ it "does not close STDIN" do
+ ruby_exe("ARGV.replace(['-']); ARGF.close; print ARGF.closed?").should == "false"
+ end
+end
diff --git a/spec/ruby/core/argf/closed_spec.rb b/spec/ruby/core/argf/closed_spec.rb
new file mode 100644
index 0000000000..769381e8c3
--- /dev/null
+++ b/spec/ruby/core/argf/closed_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.closed?" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+ end
+
+ it "returns true if the current stream has been closed" do
+ argf [@file1_name, @file2_name] do
+ stream = @argf.to_io
+ stream.close
+
+ @argf.closed?.should == true
+ stream.reopen(@argf.filename, 'r')
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/each_byte_spec.rb b/spec/ruby/core/argf/each_byte_spec.rb
new file mode 100644
index 0000000000..d9e4e7fe5b
--- /dev/null
+++ b/spec/ruby/core/argf/each_byte_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.each_byte" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @bytes = []
+ File.read(@file1_name).each_byte { |b| @bytes << b }
+ File.read(@file2_name).each_byte { |b| @bytes << b }
+ end
+
+ it "yields each byte of all streams to the passed block" do
+ argf [@file1_name, @file2_name] do
+ bytes = []
+ @argf.each_byte { |b| bytes << b }
+ bytes.should == @bytes
+ end
+ end
+
+ it "returns self when passed a block" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_byte {}.should.equal?(@argf)
+ end
+ end
+
+ it "returns an Enumerator when passed no block" do
+ argf [@file1_name, @file2_name] do
+ enum = @argf.each_byte
+ enum.should.instance_of?(Enumerator)
+
+ bytes = []
+ enum.each { |b| bytes << b }
+ bytes.should == @bytes
+ end
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ argf [@file1_name, @file2_name] do
+ enum = @argf.each_byte
+ enum.should.instance_of?(Enumerator)
+
+ bytes = []
+ enum.each { |b| bytes << b }
+ bytes.should == @bytes
+ end
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_byte.size.should == nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/each_char_spec.rb b/spec/ruby/core/argf/each_char_spec.rb
new file mode 100644
index 0000000000..62d19b6574
--- /dev/null
+++ b/spec/ruby/core/argf/each_char_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.each_char" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @chars = []
+ File.read(@file1_name).each_char { |c| @chars << c }
+ File.read(@file2_name).each_char { |c| @chars << c }
+ end
+
+ it "yields each char of all streams to the passed block" do
+ argf [@file1_name, @file2_name] do
+ chars = []
+ @argf.each_char { |c| chars << c }
+ chars.should == @chars
+ end
+ end
+
+ it "returns self when passed a block" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_char {}.should.equal?(@argf)
+ end
+ end
+
+ it "returns an Enumerator when passed no block" do
+ argf [@file1_name, @file2_name] do
+ enum = @argf.each_char
+ enum.should.instance_of?(Enumerator)
+
+ chars = []
+ enum.each { |c| chars << c }
+ chars.should == @chars
+ end
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ argf [@file1_name, @file2_name] do
+ enum = @argf.each_char
+ enum.should.instance_of?(Enumerator)
+
+ chars = []
+ enum.each { |c| chars << c }
+ chars.should == @chars
+ end
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_char.size.should == nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/each_codepoint_spec.rb b/spec/ruby/core/argf/each_codepoint_spec.rb
new file mode 100644
index 0000000000..ad55785cba
--- /dev/null
+++ b/spec/ruby/core/argf/each_codepoint_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.each_codepoint" do
+ before :each do
+ file1_name = fixture __FILE__, "file1.txt"
+ file2_name = fixture __FILE__, "file2.txt"
+ @filenames = [file1_name, file2_name]
+
+ @codepoints = File.read(file1_name).codepoints
+ @codepoints.concat File.read(file2_name).codepoints
+ end
+
+ it "is a public method" do
+ argf @filenames do
+ @argf.public_methods(false).should.include?(:each_codepoint)
+ end
+ end
+
+ it "does not require arguments" do
+ argf @filenames do
+ @argf.method(:each_codepoint).arity.should == 0
+ end
+ end
+
+ it "returns self when passed a block" do
+ argf @filenames do
+ @argf.each_codepoint {}.should.equal?(@argf)
+ end
+ end
+
+ it "returns an Enumerator when passed no block" do
+ argf @filenames do
+ @argf.each_codepoint.should.instance_of?(Enumerator)
+ end
+ end
+
+ it "yields each codepoint of all streams" do
+ argf @filenames do
+ @argf.each_codepoint.to_a.should == @codepoints
+ end
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ argf @filenames do
+ @argf.each_codepoint.should.instance_of?(Enumerator)
+ end
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ argf @filenames do
+ @argf.each_codepoint.size.should == nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/each_line_spec.rb b/spec/ruby/core/argf/each_line_spec.rb
new file mode 100644
index 0000000000..fc4d6433b8
--- /dev/null
+++ b/spec/ruby/core/argf/each_line_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.each_line" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @lines = File.readlines @file1_name
+ @lines += File.readlines @file2_name
+ end
+
+ it "is a public method" do
+ argf [@file1_name, @file2_name] do
+ @argf.public_methods(false).should.include?(:each_line)
+ end
+ end
+
+ it "requires multiple arguments" do
+ argf [@file1_name, @file2_name] do
+ @argf.method(:each_line).arity.should < 0
+ end
+ end
+
+ it "reads each line of files" do
+ argf [@file1_name, @file2_name] do
+ lines = []
+ @argf.each_line { |b| lines << b }
+ lines.should == @lines
+ end
+ end
+
+ it "returns self when passed a block" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_line {}.should.equal?(@argf)
+ end
+ end
+
+ describe "with a separator" do
+ it "yields each separated section of all streams" do
+ argf [@file1_name, @file2_name] do
+ @argf.send(:each_line, '.').to_a.should ==
+ (File.readlines(@file1_name, '.') + File.readlines(@file2_name, '.'))
+ end
+ end
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_line.should.instance_of?(Enumerator)
+ end
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ argf [@file1_name, @file2_name] do
+ @argf.each_line.size.should == nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/each_spec.rb b/spec/ruby/core/argf/each_spec.rb
new file mode 100644
index 0000000000..25f60b31d2
--- /dev/null
+++ b/spec/ruby/core/argf/each_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.each" do
+ it "is an alias of ARGF.each_line" do
+ ARGF.method(:each).should == ARGF.method(:each_line)
+ end
+end
diff --git a/spec/ruby/core/argf/eof_spec.rb b/spec/ruby/core/argf/eof_spec.rb
new file mode 100644
index 0000000000..940d104d69
--- /dev/null
+++ b/spec/ruby/core/argf/eof_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.eof" do
+ it "is an alias of ARGF.eof?" do
+ ARGF.method(:eof).should == ARGF.method(:eof?)
+ end
+end
+
+describe "ARGF.eof?" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "returns true when reaching the end of a file" do
+ argf [@file1, @file2] do
+ result = []
+ while @argf.gets
+ result << @argf.eof?
+ end
+ result.should == [false, true, false, true]
+ end
+ end
+
+ it "raises IOError when called on a closed stream" do
+ argf [@file1] do
+ @argf.read
+ -> { @argf.eof? }.should.raise(IOError)
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/file_spec.rb b/spec/ruby/core/argf/file_spec.rb
new file mode 100644
index 0000000000..df8552d457
--- /dev/null
+++ b/spec/ruby/core/argf/file_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.file" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "returns the current file object on each file" do
+ argf [@file1, @file2] do
+ result = []
+ # returns first current file even when not yet open
+ result << @argf.file.path
+ result << @argf.file.path while @argf.gets
+ # returns last current file even when closed
+ result << @argf.file.path
+ result.should == [@file1, @file1, @file1, @file2, @file2, @file2]
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/filename_spec.rb b/spec/ruby/core/argf/filename_spec.rb
new file mode 100644
index 0000000000..f4b6e922c6
--- /dev/null
+++ b/spec/ruby/core/argf/filename_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.filename" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "returns the current file name on each file" do
+ argf [@file1, @file2] do
+ result = []
+ # returns first current file even when not yet open
+ result << @argf.filename
+ result << @argf.filename while @argf.gets
+ # returns last current file even when closed
+ result << @argf.filename
+
+ result.map! { |f| File.expand_path(f) }
+ result.should == [@file1, @file1, @file1, @file2, @file2, @file2]
+ end
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "sets the $FILENAME global variable with the current file name on each file" do
+ script = fixture __FILE__, "filename.rb"
+ out = ruby_exe(script, args: [@file1, @file2])
+ out.should == "#{@file1}\n#{@file1}\n#{@file2}\n#{@file2}\n#{@file2}\n"
+ end
+end
diff --git a/spec/ruby/core/argf/fileno_spec.rb b/spec/ruby/core/argf/fileno_spec.rb
new file mode 100644
index 0000000000..99245f043c
--- /dev/null
+++ b/spec/ruby/core/argf/fileno_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.fileno" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "returns the current file number on each file" do
+ argf [@file1, @file2] do
+ result = []
+ # returns first current file even when not yet open
+ result << @argf.fileno while @argf.gets
+ # returns last current file even when closed
+ result.map { |d| d.class }.should == [Integer, Integer, Integer, Integer]
+ end
+ end
+
+ it "raises an ArgumentError when called on a closed stream" do
+ argf [@file1] do
+ @argf.read
+ -> { @argf.fileno }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/fixtures/bin_file.txt b/spec/ruby/core/argf/fixtures/bin_file.txt
new file mode 100644
index 0000000000..2545e9037e
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/bin_file.txt
@@ -0,0 +1,2 @@
+test
+test
diff --git a/spec/ruby/core/argf/fixtures/file1.txt b/spec/ruby/core/argf/fixtures/file1.txt
new file mode 100644
index 0000000000..1c89bfbd82
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/file1.txt
@@ -0,0 +1,2 @@
+file1.1
+file1.2
diff --git a/spec/ruby/core/argf/fixtures/file2.txt b/spec/ruby/core/argf/fixtures/file2.txt
new file mode 100644
index 0000000000..62e8dba00b
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/file2.txt
@@ -0,0 +1,2 @@
+line2.1
+line2.2
diff --git a/spec/ruby/core/argf/fixtures/filename.rb b/spec/ruby/core/argf/fixtures/filename.rb
new file mode 100644
index 0000000000..599c97dd57
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/filename.rb
@@ -0,0 +1,3 @@
+puts $FILENAME while ARGF.gets
+# returns last current file even when closed
+puts $FILENAME
diff --git a/spec/ruby/core/argf/fixtures/lineno.rb b/spec/ruby/core/argf/fixtures/lineno.rb
new file mode 100644
index 0000000000..079cc92e8e
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/lineno.rb
@@ -0,0 +1,5 @@
+puts $.
+ARGF.gets
+puts $.
+ARGF.gets
+puts $.
diff --git a/spec/ruby/core/argf/fixtures/rewind.rb b/spec/ruby/core/argf/fixtures/rewind.rb
new file mode 100644
index 0000000000..90a334cd89
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/rewind.rb
@@ -0,0 +1,5 @@
+puts ARGF.lineno
+ARGF.gets
+puts ARGF.lineno
+ARGF.rewind
+puts ARGF.lineno
diff --git a/spec/ruby/core/argf/fixtures/stdin.txt b/spec/ruby/core/argf/fixtures/stdin.txt
new file mode 100644
index 0000000000..063cdcb1d4
--- /dev/null
+++ b/spec/ruby/core/argf/fixtures/stdin.txt
@@ -0,0 +1,2 @@
+stdin.1
+stdin.2
diff --git a/spec/ruby/core/argf/getc_spec.rb b/spec/ruby/core/argf/getc_spec.rb
new file mode 100644
index 0000000000..dc5de9b7df
--- /dev/null
+++ b/spec/ruby/core/argf/getc_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+require_relative 'shared/getc'
+
+describe "ARGF.getc" do
+ it_behaves_like :argf_getc, :getc
+end
+
+describe "ARGF.getc" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ it "returns nil when end of stream reached" do
+ argf [@file1, @file2] do
+ @argf.read
+ @argf.getc.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/gets_spec.rb b/spec/ruby/core/argf/gets_spec.rb
new file mode 100644
index 0000000000..cc7673b190
--- /dev/null
+++ b/spec/ruby/core/argf/gets_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'shared/gets'
+
+describe "ARGF.gets" do
+ it_behaves_like :argf_gets, :gets
+end
+
+describe "ARGF.gets" do
+ it_behaves_like :argf_gets_inplace_edit, :gets
+end
+
+describe "ARGF.gets" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @file1 = File.readlines @file1_name
+ @file2 = File.readlines @file2_name
+ end
+
+ it "returns nil when reaching end of files" do
+ argf [@file1_name, @file2_name] do
+ total = @file1.size + @file2.size
+ total.times { @argf.gets }
+ @argf.gets.should == nil
+ end
+ end
+
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::UTF_8
+ Encoding.default_internal = nil
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "reads the contents of the file with default encoding" do
+ Encoding.default_external = Encoding::US_ASCII
+ argf [@file1_name, @file2_name] do
+ @argf.gets.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+end
diff --git a/spec/ruby/core/argf/inspect_spec.rb b/spec/ruby/core/argf/inspect_spec.rb
new file mode 100644
index 0000000000..df0e3ba8dc
--- /dev/null
+++ b/spec/ruby/core/argf/inspect_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.inspect" do
+ it "is an alias of ARGF.to_s" do
+ ARGF.method(:inspect).should == ARGF.method(:to_s)
+ end
+end
diff --git a/spec/ruby/core/argf/lineno_spec.rb b/spec/ruby/core/argf/lineno_spec.rb
new file mode 100644
index 0000000000..72a108c187
--- /dev/null
+++ b/spec/ruby/core/argf/lineno_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.lineno" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ # TODO: break this into four specs
+ it "returns the current line number on each file" do
+ argf [@file1, @file2] do
+ @argf.lineno = 0
+ @argf.gets
+ @argf.lineno.should == 1
+ @argf.gets
+ @argf.lineno.should == 2
+ @argf.gets
+ @argf.lineno.should == 3
+ @argf.gets
+ @argf.lineno.should == 4
+ end
+ end
+
+ it "aliases to $." do
+ script = fixture __FILE__, "lineno.rb"
+ out = ruby_exe(script, args: [@file1, @file2])
+ out.should == "0\n1\n2\n"
+ end
+end
diff --git a/spec/ruby/core/argf/path_spec.rb b/spec/ruby/core/argf/path_spec.rb
new file mode 100644
index 0000000000..2f7b91999f
--- /dev/null
+++ b/spec/ruby/core/argf/path_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.path" do
+ it "is an alias of ARGF.filename" do
+ ARGF.method(:path).should == ARGF.method(:filename)
+ end
+end
diff --git a/spec/ruby/core/argf/pos_spec.rb b/spec/ruby/core/argf/pos_spec.rb
new file mode 100644
index 0000000000..1ff16e4f17
--- /dev/null
+++ b/spec/ruby/core/argf/pos_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.pos" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ it "gives the correct position for each read operation" do
+ argf [@file1, @file2] do
+ size1 = File.size(@file1)
+ size2 = File.size(@file2)
+
+ @argf.read(2)
+ @argf.pos.should == 2
+ @argf.read(size1-2)
+ @argf.pos.should == size1
+ @argf.read(6)
+ @argf.pos.should == 6
+ @argf.rewind
+ @argf.pos.should == 0
+ @argf.read(size2)
+ @argf.pos.should == size2
+ end
+ end
+
+ it "raises an ArgumentError when called on a closed stream" do
+ argf [@file1] do
+ @argf.read
+ -> { @argf.pos }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe "ARGF.pos=" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @file1 = File.readlines @file1_name
+ @file2 = File.readlines @file2_name
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "sets the correct position in files" do
+ argf [@file1_name, @file2_name] do
+ @argf.pos = @file1.first.size
+ @argf.gets.should == @file1.last
+ @argf.pos = 0
+ @argf.gets.should == @file1.first
+
+ # finish reading file1
+ @argf.gets
+
+ @argf.gets
+ @argf.pos = 1
+ @argf.gets.should == @file2.first[1..-1]
+
+ @argf.pos = @file2.first.size + @file2.last.size - 1
+ @argf.gets.should == @file2.last[-1,1]
+ @argf.pos = 1000
+ @argf.read.should == ""
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/read_nonblock_spec.rb b/spec/ruby/core/argf/read_nonblock_spec.rb
new file mode 100644
index 0000000000..5c6bd52d80
--- /dev/null
+++ b/spec/ruby/core/argf/read_nonblock_spec.rb
@@ -0,0 +1,80 @@
+require_relative '../../spec_helper'
+require_relative 'shared/read'
+
+platform_is_not :windows do
+ describe 'ARGF.read_nonblock' do
+ it_behaves_like :argf_read, :read_nonblock
+
+ before do
+ @file1_name = fixture(__FILE__, 'file1.txt')
+ @file2_name = fixture(__FILE__, 'file2.txt')
+
+ @file1 = File.read(@file1_name)
+ @file2 = File.read(@file2_name)
+
+ @chunk1 = File.read(@file1_name, 4)
+ @chunk2 = File.read(@file2_name, 4)
+ end
+
+ it 'reads up to the given amount of bytes' do
+ argf [@file1_name] do
+ @argf.read_nonblock(4).should == @chunk1
+ end
+ end
+
+ describe 'when using multiple files' do
+ it 'reads up to the given amount of bytes from the first file' do
+ argf [@file1_name, @file2_name] do
+ @argf.read_nonblock(4).should == @chunk1
+ end
+ end
+
+ it 'returns an empty String when reading after having read the first file in its entirety' do
+ argf [@file1_name, @file2_name] do
+ @argf.read_nonblock(File.size(@file1_name)).should == @file1
+ @argf.read_nonblock(4).should == ''
+ end
+ end
+ end
+
+ it 'reads up to the given bytes from STDIN' do
+ stdin = ruby_exe('print ARGF.read_nonblock(4)', :args => "< #{@file1_name}")
+
+ stdin.should == @chunk1
+ end
+
+ it 'reads up to the given bytes from a file when a file and STDIN are present' do
+ stdin = ruby_exe("print ARGF.read_nonblock(4)", :args => "#{@file1_name} - < #{@file2_name}")
+
+ stdin.should == @chunk1
+ end
+
+ context "with STDIN" do
+ before do
+ @r, @w = IO.pipe
+ @stdin = $stdin
+ $stdin = @r
+ end
+
+ after do
+ $stdin = @stdin
+ @w.close
+ @r.close unless @r.closed?
+ end
+
+ it 'raises IO::EAGAINWaitReadable when empty' do
+ argf ['-'] do
+ -> {
+ @argf.read_nonblock(4)
+ }.should.raise(IO::EAGAINWaitReadable)
+ end
+ end
+
+ it 'returns :wait_readable when the :exception is set to false' do
+ argf ['-'] do
+ @argf.read_nonblock(4, nil, exception: false).should == :wait_readable
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/read_spec.rb b/spec/ruby/core/argf/read_spec.rb
new file mode 100644
index 0000000000..bbeef95456
--- /dev/null
+++ b/spec/ruby/core/argf/read_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+require_relative 'shared/read'
+
+describe "ARGF.read" do
+ it_behaves_like :argf_read, :read
+
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+ @stdin_name = fixture __FILE__, "stdin.txt"
+
+ @file1 = File.read @file1_name
+ @file2 = File.read @file2_name
+ @stdin = File.read @stdin_name
+ end
+
+ it "reads the contents of a file" do
+ argf [@file1_name] do
+ @argf.read().should == @file1
+ end
+ end
+
+ it "treats first nil argument as no length limit" do
+ argf [@file1_name] do
+ @argf.read(nil).should == @file1
+ end
+ end
+
+ it "reads the contents of two files" do
+ argf [@file1_name, @file2_name] do
+ @argf.read.should == @file1 + @file2
+ end
+ end
+
+ it "reads the contents of one file and some characters from the second" do
+ argf [@file1_name, @file2_name] do
+ len = @file1.size + (@file2.size / 2)
+ @argf.read(len).should == (@file1 + @file2)[0,len]
+ end
+ end
+
+ it "reads across two files consecutively" do
+ argf [@file1_name, @file2_name] do
+ @argf.read(@file1.size - 2).should == @file1[0..-3]
+ @argf.read(2+5).should == @file1[-2..-1] + @file2[0,5]
+ end
+ end
+
+ it "reads the contents of stdin" do
+ stdin = ruby_exe("print ARGF.read", args: "< #{@stdin_name}")
+ stdin.should == @stdin
+ end
+
+ it "reads the contents of one file and stdin" do
+ stdin = ruby_exe("print ARGF.read", args: "#{@file1_name} - < #{@stdin_name}")
+ stdin.should == @file1 + @stdin
+ end
+
+ it "reads the contents of the same file twice" do
+ argf [@file1_name, @file1_name] do
+ @argf.read.should == @file1 + @file1
+ end
+ end
+
+
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::UTF_8
+ Encoding.default_internal = nil
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "reads the contents of the file with default encoding" do
+ Encoding.default_external = Encoding::US_ASCII
+ argf [@file1_name, @file2_name] do
+ @argf.read.encoding.should == Encoding::US_ASCII
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/readchar_spec.rb b/spec/ruby/core/argf/readchar_spec.rb
new file mode 100644
index 0000000000..63632721ec
--- /dev/null
+++ b/spec/ruby/core/argf/readchar_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'shared/getc'
+
+describe "ARGF.getc" do
+ it_behaves_like :argf_getc, :readchar
+end
+
+describe "ARGF.readchar" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ it "raises EOFError when end of stream reached" do
+ argf [@file1, @file2] do
+ -> { while @argf.readchar; end }.should.raise(EOFError)
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/readline_spec.rb b/spec/ruby/core/argf/readline_spec.rb
new file mode 100644
index 0000000000..8c23549b1b
--- /dev/null
+++ b/spec/ruby/core/argf/readline_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'shared/gets'
+
+describe "ARGF.readline" do
+ it_behaves_like :argf_gets, :readline
+end
+
+describe "ARGF.readline" do
+ it_behaves_like :argf_gets_inplace_edit, :readline
+end
+
+describe "ARGF.readline" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ it "raises an EOFError when reaching end of files" do
+ argf [@file1, @file2] do
+ -> { while @argf.readline; end }.should.raise(EOFError)
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/readlines_spec.rb b/spec/ruby/core/argf/readlines_spec.rb
new file mode 100644
index 0000000000..156bb6a33f
--- /dev/null
+++ b/spec/ruby/core/argf/readlines_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.readlines" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+
+ @lines = File.readlines(@file1)
+ @lines += File.readlines(@file2)
+ end
+
+ it "reads all lines of all files" do
+ argf [@file1, @file2] do
+ @argf.readlines.should == @lines
+ end
+ end
+
+ it "returns an empty Array when end of stream reached" do
+ argf [@file1, @file2] do
+ @argf.read
+ @argf.readlines.should == []
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/readpartial_spec.rb b/spec/ruby/core/argf/readpartial_spec.rb
new file mode 100644
index 0000000000..9f04e72cc2
--- /dev/null
+++ b/spec/ruby/core/argf/readpartial_spec.rb
@@ -0,0 +1,75 @@
+require_relative '../../spec_helper'
+require_relative 'shared/read'
+
+describe "ARGF.readpartial" do
+ it_behaves_like :argf_read, :readpartial
+
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+ @stdin_name = fixture __FILE__, "stdin.txt"
+
+ @file1 = File.read @file1_name
+ @file2 = File.read @file2_name
+ @stdin = File.read @stdin_name
+ end
+
+ it "raises an ArgumentError if called without a maximum read length" do
+ argf [@file1_name] do
+ -> { @argf.readpartial }.should.raise(ArgumentError)
+ end
+ end
+
+ it "reads maximum number of bytes from one file at a time" do
+ argf [@file1_name, @file2_name] do
+ len = @file1.size + @file2.size
+ @argf.readpartial(len).should == @file1
+ end
+ end
+
+ it "clears output buffer even if EOFError is raised because @argf is at end" do
+ begin
+ output = +"to be cleared"
+
+ argf [@file1_name] do
+ @argf.read
+ @argf.readpartial(1, output)
+ end
+ rescue EOFError
+ output.should == ""
+ end
+ end
+
+ it "reads maximum number of bytes from one file at a time" do
+ argf [@file1_name, @file2_name] do
+ len = @file1.size + @file2.size
+ @argf.readpartial(len).should == @file1
+ end
+ end
+
+ it "returns an empty string if EOFError is raised while reading any but the last file" do
+ argf [@file1_name, @file2_name] do
+ @argf.readpartial(@file1.size)
+ @argf.readpartial(1).should == ""
+ end
+ end
+
+ it "raises an EOFError if the exception was raised while reading the last file" do
+ argf [@file1_name, @file2_name] do
+ @argf.readpartial(@file1.size)
+ @argf.readpartial(1)
+ @argf.readpartial(@file2.size)
+ -> { @argf.readpartial(1) }.should.raise(EOFError)
+ -> { @argf.readpartial(1) }.should.raise(EOFError)
+ end
+ end
+
+ it "raises an EOFError if the exception was raised while reading STDIN" do
+ ruby_str = <<-STR
+ print ARGF.readpartial(#{@stdin.size})
+ ARGF.readpartial(1) rescue print $!.class
+ STR
+ stdin = ruby_exe(ruby_str, args: "< #{@stdin_name}")
+ stdin.should == @stdin + "EOFError"
+ end
+end
diff --git a/spec/ruby/core/argf/rewind_spec.rb b/spec/ruby/core/argf/rewind_spec.rb
new file mode 100644
index 0000000000..9255f790fe
--- /dev/null
+++ b/spec/ruby/core/argf/rewind_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.rewind" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @file1 = File.readlines @file1_name
+ @file2 = File.readlines @file2_name
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "goes back to beginning of current file" do
+ argf [@file1_name, @file2_name] do
+ @argf.gets
+ @argf.rewind
+ @argf.gets.should == @file1.first
+
+ @argf.gets # finish reading file1
+
+ @argf.gets
+ @argf.rewind
+ @argf.gets.should == @file2.first
+ end
+ end
+
+ it "resets ARGF.lineno to 0" do
+ script = fixture __FILE__, "rewind.rb"
+ out = ruby_exe(script, args: [@file1_name, @file2_name])
+ out.should == "0\n1\n0\n"
+ end
+
+ it "raises an ArgumentError when end of stream reached" do
+ argf [@file1_name, @file2_name] do
+ @argf.read
+ -> { @argf.rewind }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/seek_spec.rb b/spec/ruby/core/argf/seek_spec.rb
new file mode 100644
index 0000000000..c1ea1ea438
--- /dev/null
+++ b/spec/ruby/core/argf/seek_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.seek" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @file1 = File.readlines @file1_name
+ @file2 = File.readlines @file2_name
+ end
+
+ it "sets the absolute position relative to beginning of file" do
+ argf [@file1_name, @file2_name] do
+ @argf.seek 2
+ @argf.gets.should == @file1.first[2..-1]
+ @argf.seek @file1.first.size
+ @argf.gets.should == @file1.last
+ @argf.seek 0, IO::SEEK_END
+ @argf.gets.should == @file2.first
+ end
+ end
+
+ it "sets the position relative to current position in file" do
+ argf [@file1_name, @file2_name] do
+ @argf.seek(0, IO::SEEK_CUR)
+ @argf.gets.should == @file1.first
+ @argf.seek(-@file1.first.size+2, IO::SEEK_CUR)
+ @argf.gets.should == @file1.first[2..-1]
+ @argf.seek(1, IO::SEEK_CUR)
+ @argf.gets.should == @file1.last[1..-1]
+ @argf.seek(3, IO::SEEK_CUR)
+ @argf.gets.should == @file2.first
+ @argf.seek(@file1.last.size, IO::SEEK_CUR)
+ @argf.gets.should == nil
+ end
+ end
+
+ it "sets the absolute position relative to end of file" do
+ argf [@file1_name, @file2_name] do
+ @argf.seek(-@file1.first.size-@file1.last.size, IO::SEEK_END)
+ @argf.gets.should == @file1.first
+ @argf.seek(-6, IO::SEEK_END)
+ @argf.gets.should == @file1.last[-6..-1]
+ @argf.seek(-4, IO::SEEK_END)
+ @argf.gets.should == @file1.last[4..-1]
+ @argf.gets.should == @file2.first
+ @argf.seek(-6, IO::SEEK_END)
+ @argf.gets.should == @file2.last[-6..-1]
+ end
+ end
+end
+
+describe "ARGF.seek" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ end
+
+ it "takes at least one argument (offset)" do
+ argf [@file1_name] do
+ -> { @argf.seek }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/set_encoding_spec.rb b/spec/ruby/core/argf/set_encoding_spec.rb
new file mode 100644
index 0000000000..a871e084b6
--- /dev/null
+++ b/spec/ruby/core/argf/set_encoding_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.set_encoding" do
+ before :each do
+ @file = fixture __FILE__, "file1.txt"
+ end
+
+ it "sets the external encoding when passed an encoding instance" do
+ argf [@file] do
+ @argf.set_encoding(Encoding::US_ASCII)
+ @argf.external_encoding.should == Encoding::US_ASCII
+ @argf.gets.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+ it "sets the external encoding when passed an encoding name" do
+ argf [@file] do
+ @argf.set_encoding("us-ascii")
+ @argf.external_encoding.should == Encoding::US_ASCII
+ @argf.gets.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+ it "sets the external, internal encoding when passed two encoding instances" do
+ argf [@file] do
+ @argf.set_encoding(Encoding::US_ASCII, Encoding::EUC_JP)
+ @argf.external_encoding.should == Encoding::US_ASCII
+ @argf.internal_encoding.should == Encoding::EUC_JP
+ @argf.gets.encoding.should == Encoding::EUC_JP
+ end
+ end
+
+ it "sets the external, internal encoding when passed 'ext:int' String" do
+ argf [@file] do
+ @argf.set_encoding("us-ascii:euc-jp")
+ @argf.external_encoding.should == Encoding::US_ASCII
+ @argf.internal_encoding.should == Encoding::EUC_JP
+ @argf.gets.encoding.should == Encoding::EUC_JP
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/shared/getc.rb b/spec/ruby/core/argf/shared/getc.rb
new file mode 100644
index 0000000000..d63372d9d7
--- /dev/null
+++ b/spec/ruby/core/argf/shared/getc.rb
@@ -0,0 +1,17 @@
+describe :argf_getc, shared: true do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+
+ @chars = File.read @file1
+ @chars += File.read @file2
+ end
+
+ it "reads each char of files" do
+ argf [@file1, @file2] do
+ chars = +""
+ @chars.size.times { chars << @argf.send(@method) }
+ chars.should == @chars
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/shared/gets.rb b/spec/ruby/core/argf/shared/gets.rb
new file mode 100644
index 0000000000..160d24c27b
--- /dev/null
+++ b/spec/ruby/core/argf/shared/gets.rb
@@ -0,0 +1,99 @@
+describe :argf_gets, shared: true do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+ @stdin_name = fixture __FILE__, "stdin.txt"
+
+ @file1 = File.readlines @file1_name
+ @file2 = File.readlines @file2_name
+ @stdin = File.read @stdin_name
+ end
+
+ it "reads one line of a file" do
+ argf [@file1_name] do
+ @argf.send(@method).should == @file1.first
+ end
+ end
+
+ it "reads all lines of a file" do
+ argf [@file1_name] do
+ lines = []
+ @file1.size.times { lines << @argf.send(@method) }
+ lines.should == @file1
+ end
+ end
+
+ it "reads all lines of stdin" do
+ total = @stdin.count $/
+ stdin = ruby_exe(
+ "#{total}.times { print ARGF.send(#{@method.inspect}) }",
+ args: "< #{@stdin_name}")
+ stdin.should == @stdin
+ end
+
+ it "reads all lines of two files" do
+ argf [@file1_name, @file2_name] do
+ total = @file1.size + @file2.size
+ lines = []
+ total.times { lines << @argf.send(@method) }
+ lines.should == @file1 + @file2
+ end
+ end
+
+ it "sets $_ global variable with each line read" do
+ argf [@file1_name, @file2_name] do
+ total = @file1.size + @file2.size
+ total.times do
+ line = @argf.send(@method)
+ $_.should == line
+ end
+ end
+ end
+end
+
+describe :argf_gets_inplace_edit, shared: true do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @tmp1_name = tmp "file1.txt"
+ @tmp2_name = tmp "file2.txt"
+
+ @tmp1_name_bak = @tmp1_name + ".bak"
+ @tmp2_name_bak = @tmp2_name + ".bak"
+
+ cp @file1_name, @tmp1_name
+ cp @file2_name, @tmp2_name
+
+ method = "ARGF.send(#{@method.inspect})"
+ @code = "begin while line = #{method} do puts 'x' end rescue EOFError; end"
+ end
+
+ after :each do
+ rm_r @tmp1_name, @tmp2_name, @tmp1_name_bak, @tmp2_name_bak
+ end
+
+ # -i with no backup extension is not supported on Windows
+ platform_is_not :windows do
+ it "modifies the files when in place edit mode is on" do
+ ruby_exe(@code,
+ options: "-i",
+ args: "#{@tmp1_name} #{@tmp2_name}")
+
+ File.read(@tmp1_name).should == "x\nx\n"
+ File.read(@tmp2_name).should == "x\nx\n"
+ end
+ end
+
+ it "modifies and backups two files when in place edit mode is on" do
+ ruby_exe(@code,
+ options: "-i.bak",
+ args: "#{@tmp1_name} #{@tmp2_name}")
+
+ File.read(@tmp1_name).should == "x\nx\n"
+ File.read(@tmp2_name).should == "x\nx\n"
+
+ File.read(@tmp1_name_bak).should == "file1.1\nfile1.2\n"
+ File.read(@tmp2_name_bak).should == "line2.1\nline2.2\n"
+ end
+end
diff --git a/spec/ruby/core/argf/shared/read.rb b/spec/ruby/core/argf/shared/read.rb
new file mode 100644
index 0000000000..e76d022139
--- /dev/null
+++ b/spec/ruby/core/argf/shared/read.rb
@@ -0,0 +1,58 @@
+describe :argf_read, shared: true do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @stdin_name = fixture __FILE__, "stdin.txt"
+
+ @file1 = File.read @file1_name
+ @stdin = File.read @stdin_name
+ end
+
+ it "treats second nil argument as no output buffer" do
+ argf [@file1_name] do
+ @argf.send(@method, @file1.size, nil).should == @file1
+ end
+ end
+
+ it "treats second argument as an output buffer" do
+ argf [@file1_name] do
+ buffer = +""
+ @argf.send(@method, @file1.size, buffer)
+ buffer.should == @file1
+ end
+ end
+
+ it "clears output buffer before appending to it" do
+ argf [@file1_name] do
+ buffer = +"to be cleared"
+ @argf.send(@method, @file1.size, buffer)
+ buffer.should == @file1
+ end
+ end
+
+ it "reads a number of bytes from the first file" do
+ argf [@file1_name] do
+ @argf.send(@method, 5).should == @file1[0, 5]
+ end
+ end
+
+ it "reads from a single file consecutively" do
+ argf [@file1_name] do
+ @argf.send(@method, 1).should == @file1[0, 1]
+ @argf.send(@method, 2).should == @file1[1, 2]
+ @argf.send(@method, 3).should == @file1[3, 3]
+ end
+ end
+
+ it "reads a number of bytes from stdin" do
+ stdin = ruby_exe("print ARGF.#{@method}(10)", :args => "< #{@stdin_name}")
+ stdin.should == @stdin[0, 10]
+ end
+
+ platform_is_not :windows do
+ it "reads the contents of a special device file" do
+ argf ['/dev/zero'] do
+ @argf.send(@method, 100).should == "\000" * 100
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/skip_spec.rb b/spec/ruby/core/argf/skip_spec.rb
new file mode 100644
index 0000000000..bb1c0ae110
--- /dev/null
+++ b/spec/ruby/core/argf/skip_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.skip" do
+ before :each do
+ @file1_name = fixture __FILE__, "file1.txt"
+ @file2_name = fixture __FILE__, "file2.txt"
+
+ @file2 = File.readlines @file2_name
+ end
+
+ it "skips the current file" do
+ argf [@file1_name, @file2_name] do
+ @argf.read(1)
+ @argf.skip
+ @argf.gets.should == @file2.first
+ end
+ end
+
+ it "has no effect when called twice in a row" do
+ argf [@file1_name, @file2_name] do
+ @argf.read(1)
+ @argf.skip
+ @argf.skip
+ @argf.gets.should == @file2.first
+ end
+ end
+
+ it "has no effect at end of stream" do
+ argf [@file1_name, @file2_name] do
+ @argf.read
+ @argf.skip
+ @argf.gets.should == nil
+ end
+ end
+
+ # This bypasses argf helper because the helper will call argf.file
+ # which as a side-effect calls argf.file which will initialize
+ # internals of ARGF enough for this to work.
+ it "has no effect when nothing has been processed yet" do
+ -> { ARGF.class.new(@file1_name).skip }.should_not.raise
+ end
+end
diff --git a/spec/ruby/core/argf/tell_spec.rb b/spec/ruby/core/argf/tell_spec.rb
new file mode 100644
index 0000000000..bb28df74a2
--- /dev/null
+++ b/spec/ruby/core/argf/tell_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.tell" do
+ it "is an alias of ARGF.pos" do
+ ARGF.method(:tell).should == ARGF.method(:pos)
+ end
+end
diff --git a/spec/ruby/core/argf/to_a_spec.rb b/spec/ruby/core/argf/to_a_spec.rb
new file mode 100644
index 0000000000..d95dc732ec
--- /dev/null
+++ b/spec/ruby/core/argf/to_a_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.to_a" do
+ it "is an alias of ARGF.readlines" do
+ ARGF.method(:to_a).should == ARGF.method(:readlines)
+ end
+end
diff --git a/spec/ruby/core/argf/to_i_spec.rb b/spec/ruby/core/argf/to_i_spec.rb
new file mode 100644
index 0000000000..e8df378f4e
--- /dev/null
+++ b/spec/ruby/core/argf/to_i_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.to_i" do
+ it "is an alias of ARGF.fileno" do
+ ARGF.method(:to_i).should == ARGF.method(:fileno)
+ end
+end
diff --git a/spec/ruby/core/argf/to_io_spec.rb b/spec/ruby/core/argf/to_io_spec.rb
new file mode 100644
index 0000000000..ab5de58bcf
--- /dev/null
+++ b/spec/ruby/core/argf/to_io_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.to_io" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ # NOTE: this test assumes that fixtures files have two lines each
+ it "returns the IO of the current file" do
+ argf [@file1, @file2] do
+ result = []
+ 4.times do
+ @argf.gets
+ result << @argf.to_io
+ end
+
+ result.each { |io| io.should.is_a?(IO) }
+ result[0].should == result[1]
+ result[2].should == result[3]
+ end
+ end
+end
diff --git a/spec/ruby/core/argf/to_s_spec.rb b/spec/ruby/core/argf/to_s_spec.rb
new file mode 100644
index 0000000000..3f505898f4
--- /dev/null
+++ b/spec/ruby/core/argf/to_s_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "ARGF.to_s" do
+ before :each do
+ @file1 = fixture __FILE__, "file1.txt"
+ @file2 = fixture __FILE__, "file2.txt"
+ end
+
+ it "returns 'ARGF'" do
+ argf [@file1, @file2] do
+ @argf.to_s.should == "ARGF"
+ end
+ end
+end
diff --git a/spec/ruby/core/array/all_spec.rb b/spec/ruby/core/array/all_spec.rb
new file mode 100644
index 0000000000..680e8c26fa
--- /dev/null
+++ b/spec/ruby/core/array/all_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#all?" do
+ @value_to_return = -> _ { true }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :all?
+
+ it "ignores the block if there is an argument" do
+ -> {
+ ['bar', 'foobar'].all?(/bar/) { false }.should == true
+ }.should complain(/given block not used/)
+ end
+end
diff --git a/spec/ruby/core/array/allocate_spec.rb b/spec/ruby/core/array/allocate_spec.rb
new file mode 100644
index 0000000000..c9eceef590
--- /dev/null
+++ b/spec/ruby/core/array/allocate_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Array.allocate" do
+ it "returns an instance of Array" do
+ ary = Array.allocate
+ ary.should.instance_of?(Array)
+ end
+
+ it "returns a fully-formed instance of Array" do
+ ary = Array.allocate
+ ary.size.should == 0
+ ary << 1
+ ary.should == [1]
+ end
+
+ it "does not accept any arguments" do
+ -> { Array.allocate(1) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/array/any_spec.rb b/spec/ruby/core/array/any_spec.rb
new file mode 100644
index 0000000000..b51ce62f0f
--- /dev/null
+++ b/spec/ruby/core/array/any_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#any?" do
+ describe 'with no block given (a default block of { |x| x } is implicit)' do
+ it "is false if the array is empty" do
+ empty_array = []
+ empty_array.should_not.any?
+ end
+
+ it "is false if the array is not empty, but all the members of the array are falsy" do
+ falsy_array = [false, nil, false]
+ falsy_array.should_not.any?
+ end
+
+ it "is true if the array has any truthy members" do
+ not_empty_array = ['anything', nil]
+ not_empty_array.should.any?
+ end
+ end
+
+ describe 'with a block given' do
+ @value_to_return = -> _ { false }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :any?
+
+ it 'is false if the array is empty' do
+ empty_array = []
+ empty_array.any? {|v| 1 == 1 }.should == false
+ end
+
+ it 'is true if the block returns true for any member of the array' do
+ array_with_members = [false, false, true, false]
+ array_with_members.any? {|v| v == true }.should == true
+ end
+
+ it 'is false if the block returns false for all members of the array' do
+ array_with_members = [false, false, true, false]
+ array_with_members.any? {|v| v == 42 }.should == false
+ end
+ end
+
+ describe 'when given a pattern argument' do
+ it "ignores the block if there is an argument" do
+ -> {
+ ['bar', 'foobar'].any?(/bar/) { false }.should == true
+ }.should complain(/given block not used/)
+ end
+ end
+end
diff --git a/spec/ruby/core/array/append_spec.rb b/spec/ruby/core/array/append_spec.rb
new file mode 100644
index 0000000000..5480d9f65e
--- /dev/null
+++ b/spec/ruby/core/array/append_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#<<" do
+ it "pushes the object onto the end of the array" do
+ ([ 1, 2 ] << "c" << "d" << [ 3, 4 ]).should == [1, 2, "c", "d", [3, 4]]
+ end
+
+ it "returns self to allow chaining" do
+ a = []
+ b = a
+ (a << 1).should.equal?(b)
+ (a << 2 << 3).should.equal?(b)
+ end
+
+ it "correctly resizes the Array" do
+ a = []
+ a.size.should == 0
+ a << :foo
+ a.size.should == 1
+ a << :bar << :baz
+ a.size.should == 3
+
+ a = [1, 2, 3]
+ a.shift
+ a.shift
+ a.shift
+ a << :foo
+ a.should == [:foo]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array << 5 }.should.raise(FrozenError)
+ end
+end
+
+describe "Array#append" do
+ it "is an alias of Array#push" do
+ Array.instance_method(:append).should == Array.instance_method(:push)
+ end
+end
diff --git a/spec/ruby/core/array/array_spec.rb b/spec/ruby/core/array/array_spec.rb
new file mode 100644
index 0000000000..855f17348f
--- /dev/null
+++ b/spec/ruby/core/array/array_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Array" do
+ it "includes Enumerable" do
+ Array.include?(Enumerable).should == true
+ end
+end
diff --git a/spec/ruby/core/array/assoc_spec.rb b/spec/ruby/core/array/assoc_spec.rb
new file mode 100644
index 0000000000..a5026cf5d4
--- /dev/null
+++ b/spec/ruby/core/array/assoc_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#assoc" do
+ it "returns the first array whose 1st item is == obj or nil" do
+ s1 = ["colors", "red", "blue", "green"]
+ s2 = [:letters, "a", "b", "c"]
+ s3 = [4]
+ s4 = ["colors", "cyan", "yellow", "magenta"]
+ s5 = [:letters, "a", "i", "u"]
+ s_nil = [nil, nil]
+ a = [s1, s2, s3, s4, s5, s_nil]
+ a.assoc(s1.first).should.equal?(s1)
+ a.assoc(s2.first).should.equal?(s2)
+ a.assoc(s3.first).should.equal?(s3)
+ a.assoc(s4.first).should.equal?(s1)
+ a.assoc(s5.first).should.equal?(s2)
+ a.assoc(s_nil.first).should.equal?(s_nil)
+ a.assoc(4).should.equal?(s3)
+ a.assoc("key not in array").should == nil
+ end
+
+ it "calls == on first element of each array" do
+ key1 = 'it'
+ key2 = mock('key2')
+ items = [['not it', 1], [ArraySpecs::AssocKey.new, 2], ['na', 3]]
+
+ items.assoc(key1).should.equal?(items[1])
+ items.assoc(key2).should == nil
+ end
+
+ it "ignores any non-Array elements" do
+ [1, 2, 3].assoc(2).should == nil
+ s1 = [4]
+ s2 = [5, 4, 3]
+ a = ["foo", [], s1, s2, nil, []]
+ a.assoc(s1.first).should.equal?(s1)
+ a.assoc(s2.first).should.equal?(s2)
+ end
+
+ it "calls to_ary on non-array elements" do
+ s1 = [1, 2]
+ s2 = ArraySpecs::ArrayConvertible.new(2, 3)
+ a = [s1, s2]
+
+ s1.should_not_receive(:to_ary)
+ a.assoc(s1.first).should.equal?(s1)
+
+ a.assoc(2).should == [2, 3]
+ s2.called.should.equal?(:to_ary)
+ end
+end
diff --git a/spec/ruby/core/array/at_spec.rb b/spec/ruby/core/array/at_spec.rb
new file mode 100644
index 0000000000..3c7c99fdff
--- /dev/null
+++ b/spec/ruby/core/array/at_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#at" do
+ it "returns the (n+1)'th element for the passed index n" do
+ a = [1, 2, 3, 4, 5, 6]
+ a.at(0).should == 1
+ a.at(1).should == 2
+ a.at(5).should == 6
+ end
+
+ it "returns nil if the given index is greater than or equal to the array's length" do
+ a = [1, 2, 3, 4, 5, 6]
+ a.at(6).should == nil
+ a.at(7).should == nil
+ end
+
+ it "returns the (-n)'th element from the last, for the given negative index n" do
+ a = [1, 2, 3, 4, 5, 6]
+ a.at(-1).should == 6
+ a.at(-2).should == 5
+ a.at(-6).should == 1
+ end
+
+ it "returns nil if the given index is less than -len, where len is length of the array" do
+ a = [1, 2, 3, 4, 5, 6]
+ a.at(-7).should == nil
+ a.at(-8).should == nil
+ end
+
+ it "does not extend the array unless the given index is out of range" do
+ a = [1, 2, 3, 4, 5, 6]
+ a.length.should == 6
+ a.at(100)
+ a.length.should == 6
+ a.at(-100)
+ a.length.should == 6
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ a = ["a", "b", "c"]
+ a.at(0.5).should == "a"
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ a.at(obj).should == "c"
+ end
+
+ it "raises a TypeError when the passed argument can't be coerced to Integer" do
+ -> { [].at("cat") }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when 2 or more arguments are passed" do
+ -> { [:a, :b].at(0,1) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/array/bsearch_index_spec.rb b/spec/ruby/core/array/bsearch_index_spec.rb
new file mode 100644
index 0000000000..e1d5eb66bb
--- /dev/null
+++ b/spec/ruby/core/array/bsearch_index_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Array#bsearch_index" do
+ context "when not passed a block" do
+ before :each do
+ @enum = [1, 2, 42, 100, 666].bsearch_index
+ end
+
+ it "returns an Enumerator" do
+ @enum.should.instance_of?(Enumerator)
+ end
+
+ it "returns an Enumerator with unknown size" do
+ @enum.size.should == nil
+ end
+
+ it "returns index of element when block condition is satisfied" do
+ @enum.each { |x| x >= 33 }.should == 2
+ end
+ end
+
+ it "raises a TypeError when block returns a String" do
+ -> { [1, 2, 3].bsearch_index { "not ok" } }.should.raise(TypeError)
+ end
+
+ it "returns nil when block is empty" do
+ [1, 2, 3].bsearch_index {}.should == nil
+ end
+
+ context "minimum mode" do
+ before :each do
+ @array = [0, 4, 7, 10, 12]
+ end
+
+ it "returns index of first element which satisfies the block" do
+ @array.bsearch_index { |x| x >= 4 }.should == 1
+ @array.bsearch_index { |x| x >= 6 }.should == 2
+ @array.bsearch_index { |x| x >= -1 }.should == 0
+ end
+
+ it "returns nil when block condition is never satisfied" do
+ @array.bsearch_index { false }.should == nil
+ @array.bsearch_index { |x| x >= 100 }.should == nil
+ end
+ end
+
+ context "find any mode" do
+ before :each do
+ @array = [0, 4, 7, 10, 12]
+ end
+
+ it "returns the index of any matched elements where element is between 4 <= x < 8" do
+ [1, 2].should.include?(@array.bsearch_index { |x| 1 - x / 4 })
+ end
+
+ it "returns the index of any matched elements where element is between 8 <= x < 10" do
+ @array.bsearch_index { |x| 4 - x / 2 }.should == nil
+ end
+
+ it "returns nil when block never returns 0" do
+ @array.bsearch_index { |x| 1 }.should == nil
+ @array.bsearch_index { |x| -1 }.should == nil
+ end
+
+ context "magnitude does not effect the result" do
+ it "returns the index of any matched elements where element is between 4n <= xn < 8n" do
+ [1, 2].should.include?(@array.bsearch_index { |x| (1 - x / 4) * (2**100) })
+ end
+
+ it "returns nil when block never returns 0" do
+ @array.bsearch_index { |x| 1 * (2**100) }.should == nil
+ @array.bsearch_index { |x| (-1) * (2**100) }.should == nil
+ end
+
+ it "handles values from Integer#coerce" do
+ [1, 2].should.include?(@array.bsearch_index { |x| (2**100).coerce((1 - x / 4) * (2**100)).first })
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/array/bsearch_spec.rb b/spec/ruby/core/array/bsearch_spec.rb
new file mode 100644
index 0000000000..12aec60654
--- /dev/null
+++ b/spec/ruby/core/array/bsearch_spec.rb
@@ -0,0 +1,84 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Array#bsearch" do
+ it "returns an Enumerator when not passed a block" do
+ [1].bsearch.should.instance_of?(Enumerator)
+ end
+
+ it_behaves_like :enumeratorized_with_unknown_size, :bsearch, [1,2,3]
+
+ it "raises a TypeError if the block returns an Object" do
+ -> { [1].bsearch { Object.new } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the block returns a String" do
+ -> { [1].bsearch { "1" } }.should.raise(TypeError)
+ end
+
+ context "with a block returning true or false" do
+ it "returns nil if the block returns false for every element" do
+ [0, 1, 2, 3].bsearch { |x| x > 3 }.should == nil
+ end
+
+ it "returns nil if the block returns nil for every element" do
+ [0, 1, 2, 3].bsearch { |x| nil }.should == nil
+ end
+
+ it "returns element at zero if the block returns true for every element" do
+ [0, 1, 2, 3].bsearch { |x| x < 4 }.should == 0
+
+ end
+
+ it "returns the element at the smallest index for which block returns true" do
+ [0, 1, 3, 4].bsearch { |x| x >= 2 }.should == 3
+ [0, 1, 3, 4].bsearch { |x| x >= 1 }.should == 1
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns less than zero for every element" do
+ [0, 1, 2, 3].bsearch { |x| x <=> 5 }.should == nil
+ end
+
+ it "returns nil if the block returns greater than zero for every element" do
+ [0, 1, 2, 3].bsearch { |x| x <=> -1 }.should == nil
+
+ end
+
+ it "returns nil if the block never returns zero" do
+ [0, 1, 3, 4].bsearch { |x| x <=> 2 }.should == nil
+ end
+
+ it "accepts (+/-)Float::INFINITY from the block" do
+ [0, 1, 3, 4].bsearch { |x| Float::INFINITY }.should == nil
+ [0, 1, 3, 4].bsearch { |x| -Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = [0, 1, 2, 3, 4].bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = [0, 1, 2, 3, 4].bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ [1, 2].should.include?(result)
+ end
+ end
+
+ context "with a block that calls break" do
+ it "returns nil if break is called without a value" do
+ ['a', 'b', 'c'].bsearch { |v| break }.should == nil
+ end
+
+ it "returns nil if break is called with a nil value" do
+ ['a', 'b', 'c'].bsearch { |v| break nil }.should == nil
+ end
+
+ it "returns object if break is called with an object" do
+ ['a', 'b', 'c'].bsearch { |v| break 1234 }.should == 1234
+ ['a', 'b', 'c'].bsearch { |v| break 'hi' }.should == 'hi'
+ ['a', 'b', 'c'].bsearch { |v| break [42] }.should == [42]
+ end
+ end
+end
diff --git a/spec/ruby/core/array/clear_spec.rb b/spec/ruby/core/array/clear_spec.rb
new file mode 100644
index 0000000000..15778f864f
--- /dev/null
+++ b/spec/ruby/core/array/clear_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#clear" do
+ it "removes all elements" do
+ a = [1, 2, 3, 4]
+ a.clear.should.equal?(a)
+ a.should == []
+ end
+
+ it "returns self" do
+ a = [1]
+ a.should.equal? a.clear
+ end
+
+ it "leaves the Array empty" do
+ a = [1]
+ a.clear
+ a.should.empty?
+ a.size.should == 0
+ end
+
+ it "does not accept any arguments" do
+ -> { [1].clear(true) }.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ a = [1]
+ a.freeze
+ -> { a.clear }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/clone_spec.rb b/spec/ruby/core/array/clone_spec.rb
new file mode 100644
index 0000000000..7ce9d40a81
--- /dev/null
+++ b/spec/ruby/core/array/clone_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/clone'
+
+describe "Array#clone" do
+ it_behaves_like :array_clone, :clone
+
+ it "copies frozen status from the original" do
+ a = [1, 2, 3, 4]
+ b = [1, 2, 3, 4]
+ a.freeze
+ aa = a.clone
+ bb = b.clone
+
+ aa.should.frozen?
+ bb.should_not.frozen?
+ end
+
+ it "copies singleton methods" do
+ a = [1, 2, 3, 4]
+ b = [1, 2, 3, 4]
+ def a.a_singleton_method; end
+ aa = a.clone
+ bb = b.clone
+
+ a.respond_to?(:a_singleton_method).should == true
+ b.respond_to?(:a_singleton_method).should == false
+ aa.respond_to?(:a_singleton_method).should == true
+ bb.respond_to?(:a_singleton_method).should == false
+ end
+end
diff --git a/spec/ruby/core/array/collect_spec.rb b/spec/ruby/core/array/collect_spec.rb
new file mode 100644
index 0000000000..43a539f805
--- /dev/null
+++ b/spec/ruby/core/array/collect_spec.rb
@@ -0,0 +1,143 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../enumerable/shared/enumeratorized'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#collect" do
+ it "returns a copy of array with each element replaced by the value returned by block" do
+ a = ['a', 'b', 'c', 'd']
+ b = a.collect { |i| i + '!' }
+ b.should == ["a!", "b!", "c!", "d!"]
+ b.should_not.equal? a
+ end
+
+ it "does not return subclass instance" do
+ ArraySpecs::MyArray[1, 2, 3].collect { |x| x + 1 }.should.instance_of?(Array)
+ end
+
+ it "does not change self" do
+ a = ['a', 'b', 'c', 'd']
+ a.collect { |i| i + '!' }
+ a.should == ['a', 'b', 'c', 'd']
+ end
+
+ it "returns the evaluated value of block if it broke in the block" do
+ a = ['a', 'b', 'c', 'd']
+ b = a.collect {|i|
+ if i == 'c'
+ break 0
+ else
+ i + '!'
+ end
+ }
+ b.should == 0
+ end
+
+ it "returns an Enumerator when no block given" do
+ a = [1, 2, 3]
+ a.collect.should.instance_of?(Enumerator)
+ end
+
+ it "raises an ArgumentError when no block and with arguments" do
+ a = [1, 2, 3]
+ -> {
+ a.collect(:foo)
+ }.should.raise(ArgumentError)
+ end
+
+ before :each do
+ @object = [1, 2, 3, 4]
+ end
+ it_behaves_like :enumeratorized_with_origin_size, :collect
+
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :collect
+end
+
+describe "Array#collect!" do
+ it "replaces each element with the value returned by block" do
+ a = [7, 9, 3, 5]
+ a.collect! { |i| i - 1 }.should.equal?(a)
+ a.should == [6, 8, 2, 4]
+ end
+
+ it "returns self" do
+ a = [1, 2, 3, 4, 5]
+ b = a.collect! {|i| i+1 }
+ a.should.equal? b
+ end
+
+ it "returns the evaluated value of block but its contents is partially modified, if it broke in the block" do
+ a = ['a', 'b', 'c', 'd']
+ b = a.collect! {|i|
+ if i == 'c'
+ break 0
+ else
+ i + '!'
+ end
+ }
+ b.should == 0
+ a.should == ['a!', 'b!', 'c', 'd']
+ end
+
+ it "returns an Enumerator when no block given, and the enumerator can modify the original array" do
+ a = [1, 2, 3]
+ enum = a.collect!
+ enum.should.instance_of?(Enumerator)
+ enum.each{|i| "#{i}!" }
+ a.should == ["1!", "2!", "3!"]
+ end
+
+ describe "when frozen" do
+ it "raises a FrozenError" do
+ -> { ArraySpecs.frozen_array.collect! {} }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError when empty" do
+ -> { ArraySpecs.empty_frozen_array.collect! {} }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError when calling #each on the returned Enumerator" do
+ enumerator = ArraySpecs.frozen_array.collect!
+ -> { enumerator.each {|x| x } }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError when calling #each on the returned Enumerator when empty" do
+ enumerator = ArraySpecs.empty_frozen_array.collect!
+ -> { enumerator.each {|x| x } }.should.raise(FrozenError)
+ end
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.collect! { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "only changes elements before error is raised, keeping the element which raised an error." do
+ a = [1, 2, 3, 4]
+ begin
+ a.collect! do |e|
+ case e
+ when 1 then -1
+ when 2 then -2
+ when 3 then raise StandardError, 'Oops'
+ else 0
+ end
+ end
+ rescue StandardError
+ end
+
+ a.should == [-1, -2, 3, 4]
+ end
+
+ before :each do
+ @object = [1, 2, 3, 4]
+ end
+ it_behaves_like :enumeratorized_with_origin_size, :collect!
+
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :collect!
+end
diff --git a/spec/ruby/core/array/combination_spec.rb b/spec/ruby/core/array/combination_spec.rb
new file mode 100644
index 0000000000..ac570687ca
--- /dev/null
+++ b/spec/ruby/core/array/combination_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+
+describe "Array#combination" do
+ before :each do
+ @array = [1, 2, 3, 4]
+ end
+
+ it "returns an enumerator when no block is provided" do
+ @array.combination(2).should.instance_of?(Enumerator)
+ end
+
+ it "returns self when a block is given" do
+ @array.combination(2){}.should.equal?(@array)
+ end
+
+ it "yields nothing for out of bounds length and return self" do
+ @array.combination(5).to_a.should == []
+ @array.combination(-1).to_a.should == []
+ end
+
+ it "yields the expected combinations" do
+ @array.combination(3).to_a.sort.should == [[1,2,3],[1,2,4],[1,3,4],[2,3,4]]
+ end
+
+ it "yields nothing if the argument is out of bounds" do
+ @array.combination(-1).to_a.should == []
+ @array.combination(5).to_a.should == []
+ end
+
+ it "yields a copy of self if the argument is the size of the receiver" do
+ r = @array.combination(4).to_a
+ r.should == [@array]
+ r[0].should_not.equal?(@array)
+ end
+
+ it "yields [] when length is 0" do
+ @array.combination(0).to_a.should == [[]] # one combination of length 0
+ [].combination(0).to_a.should == [[]] # one combination of length 0
+ end
+
+ it "yields a partition consisting of only singletons" do
+ @array.combination(1).to_a.sort.should == [[1],[2],[3],[4]]
+ end
+
+ it "generates from a defensive copy, ignoring mutations" do
+ accum = []
+ @array.combination(2) do |x|
+ accum << x
+ @array[0] = 1
+ end
+ accum.should == [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns 0 when the number of combinations is < 0" do
+ @array.combination(-1).size.should == 0
+ [].combination(-2).size.should == 0
+ end
+ it "returns the binomial coefficient between the array size the number of combinations" do
+ @array.combination(5).size.should == 0
+ @array.combination(4).size.should == 1
+ @array.combination(3).size.should == 4
+ @array.combination(2).size.should == 6
+ @array.combination(1).size.should == 4
+ @array.combination(0).size.should == 1
+ [].combination(0).size.should == 1
+ [].combination(1).size.should == 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/array/compact_spec.rb b/spec/ruby/core/array/compact_spec.rb
new file mode 100644
index 0000000000..dbcd16da35
--- /dev/null
+++ b/spec/ruby/core/array/compact_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#compact" do
+ it "returns a copy of array with all nil elements removed" do
+ a = [1, 2, 4]
+ a.compact.should == [1, 2, 4]
+ a = [1, nil, 2, 4]
+ a.compact.should == [1, 2, 4]
+ a = [1, 2, 4, nil]
+ a.compact.should == [1, 2, 4]
+ a = [nil, 1, 2, 4]
+ a.compact.should == [1, 2, 4]
+ end
+
+ it "does not return self" do
+ a = [1, 2, 3]
+ a.compact.should_not.equal?(a)
+ end
+
+ it "does not return subclass instance for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3, nil].compact.should.instance_of?(Array)
+ end
+end
+
+describe "Array#compact!" do
+ it "removes all nil elements" do
+ a = ['a', nil, 'b', false, 'c']
+ a.compact!.should.equal?(a)
+ a.should == ["a", "b", false, "c"]
+ a = [nil, 'a', 'b', false, 'c']
+ a.compact!.should.equal?(a)
+ a.should == ["a", "b", false, "c"]
+ a = ['a', 'b', false, 'c', nil]
+ a.compact!.should.equal?(a)
+ a.should == ["a", "b", false, "c"]
+ end
+
+ it "returns self if some nil elements are removed" do
+ a = ['a', nil, 'b', false, 'c']
+ a.compact!.should.equal? a
+ end
+
+ it "returns nil if there are no nil elements to remove" do
+ [1, 2, false, 3].compact!.should == nil
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.compact! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/comparison_spec.rb b/spec/ruby/core/array/comparison_spec.rb
new file mode 100644
index 0000000000..14e8931e5a
--- /dev/null
+++ b/spec/ruby/core/array/comparison_spec.rb
@@ -0,0 +1,97 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#<=>" do
+ it "calls <=> left to right and return first non-0 result" do
+ [-1, +1, nil, "foobar"].each do |result|
+ lhs = Array.new(3) { mock("#{result}") }
+ rhs = Array.new(3) { mock("#{result}") }
+
+ lhs[0].should_receive(:<=>).with(rhs[0]).and_return(0)
+ lhs[1].should_receive(:<=>).with(rhs[1]).and_return(result)
+ lhs[2].should_not_receive(:<=>)
+
+ (lhs <=> rhs).should == result
+ end
+ end
+
+ it "returns 0 if the arrays are equal" do
+ ([] <=> []).should == 0
+ ([1, 2, 3, 4, 5, 6] <=> [1, 2, 3, 4, 5.0, 6.0]).should == 0
+ end
+
+ it "returns -1 if the array is shorter than the other array" do
+ ([] <=> [1]).should == -1
+ ([1, 1] <=> [1, 1, 1]).should == -1
+ end
+
+ it "returns +1 if the array is longer than the other array" do
+ ([1] <=> []).should == +1
+ ([1, 1, 1] <=> [1, 1]).should == +1
+ end
+
+ it "returns -1 if the arrays have same length and a pair of corresponding elements returns -1 for <=>" do
+ eq_l = mock('an object equal to the other')
+ eq_r = mock('an object equal to the other')
+ eq_l.should_receive(:<=>).with(eq_r).any_number_of_times.and_return(0)
+
+ less = mock('less than the other')
+ greater = mock('greater then the other')
+ less.should_receive(:<=>).with(greater).any_number_of_times.and_return(-1)
+
+ rest = mock('an rest element of the arrays')
+ rest.should_receive(:<=>).with(rest).any_number_of_times.and_return(0)
+ lhs = [eq_l, eq_l, less, rest]
+ rhs = [eq_r, eq_r, greater, rest]
+
+ (lhs <=> rhs).should == -1
+ end
+
+ it "returns +1 if the arrays have same length and a pair of corresponding elements returns +1 for <=>" do
+ eq_l = mock('an object equal to the other')
+ eq_r = mock('an object equal to the other')
+ eq_l.should_receive(:<=>).with(eq_r).any_number_of_times.and_return(0)
+
+ greater = mock('greater then the other')
+ less = mock('less than the other')
+ greater.should_receive(:<=>).with(less).any_number_of_times.and_return(+1)
+
+ rest = mock('an rest element of the arrays')
+ rest.should_receive(:<=>).with(rest).any_number_of_times.and_return(0)
+ lhs = [eq_l, eq_l, greater, rest]
+ rhs = [eq_r, eq_r, less, rest]
+
+ (lhs <=> rhs).should == +1
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ (empty <=> empty).should == 0
+ (empty <=> []).should == 1
+ ([] <=> empty).should == -1
+
+ (ArraySpecs.recursive_array <=> []).should == 1
+ ([] <=> ArraySpecs.recursive_array).should == -1
+
+ (ArraySpecs.recursive_array <=> ArraySpecs.empty_recursive_array).should == nil
+
+ array = ArraySpecs.recursive_array
+ (array <=> array).should == 0
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('to_ary')
+ obj.stub!(:to_ary).and_return([1, 2, 3])
+ ([4, 5] <=> obj).should == ([4, 5] <=> obj.to_ary)
+ end
+
+ it "does not call #to_ary on Array subclasses" do
+ obj = ArraySpecs::ToAryArray[5, 6, 7]
+ obj.should_not_receive(:to_ary)
+ ([5, 6, 7] <=> obj).should == 0
+ end
+
+ it "returns nil when the argument is not array-like" do
+ ([] <=> false).should == nil
+ end
+end
diff --git a/spec/ruby/core/array/concat_spec.rb b/spec/ruby/core/array/concat_spec.rb
new file mode 100644
index 0000000000..1e8d20c36c
--- /dev/null
+++ b/spec/ruby/core/array/concat_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#concat" do
+ it "returns the array itself" do
+ ary = [1,2,3]
+ ary.concat([4,5,6]).equal?(ary).should == true
+ end
+
+ it "appends the elements in the other array" do
+ ary = [1, 2, 3]
+ ary.concat([9, 10, 11]).should.equal?(ary)
+ ary.should == [1, 2, 3, 9, 10, 11]
+ ary.concat([])
+ ary.should == [1, 2, 3, 9, 10, 11]
+ end
+
+ it "does not loop endlessly when argument is self" do
+ ary = ["x", "y"]
+ ary.concat(ary).should == ["x", "y", "x", "y"]
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('to_ary')
+ obj.should_receive(:to_ary).and_return(["x", "y"])
+ [4, 5, 6].concat(obj).should == [4, 5, 6, "x", "y"]
+ end
+
+ it "does not call #to_ary on Array subclasses" do
+ obj = ArraySpecs::ToAryArray[5, 6, 7]
+ obj.should_not_receive(:to_ary)
+ [].concat(obj).should == [5, 6, 7]
+ end
+
+ it "raises a FrozenError when Array is frozen and modification occurs" do
+ -> { ArraySpecs.frozen_array.concat [1] }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError when Array is frozen and no modification occurs" do
+ -> { ArraySpecs.frozen_array.concat([]) }.should.raise(FrozenError)
+ end
+
+ it "appends elements to an Array with enough capacity that has been shifted" do
+ ary = [1, 2, 3, 4, 5]
+ 2.times { ary.shift }
+ 2.times { ary.pop }
+ ary.concat([5, 6]).should == [3, 5, 6]
+ end
+
+ it "appends elements to an Array without enough capacity that has been shifted" do
+ ary = [1, 2, 3, 4]
+ 3.times { ary.shift }
+ ary.concat([5, 6]).should == [4, 5, 6]
+ end
+
+ it "takes multiple arguments" do
+ ary = [1, 2]
+ ary.concat [3, 4]
+ ary.should == [1, 2, 3, 4]
+ end
+
+ it "concatenates the initial value when given arguments contain 2 self" do
+ ary = [1, 2]
+ ary.concat ary, ary
+ ary.should == [1, 2, 1, 2, 1, 2]
+ end
+
+ it "returns self when given no arguments" do
+ ary = [1, 2]
+ ary.concat.should.equal?(ary)
+ ary.should == [1, 2]
+ end
+end
diff --git a/spec/ruby/core/array/constructor_spec.rb b/spec/ruby/core/array/constructor_spec.rb
new file mode 100644
index 0000000000..c4398c535d
--- /dev/null
+++ b/spec/ruby/core/array/constructor_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array.[]" do
+ it "returns a new array populated with the given elements" do
+ obj = Object.new
+ Array.[](5, true, nil, 'a', "Ruby", obj).should == [5, true, nil, "a", "Ruby", obj]
+
+ a = ArraySpecs::MyArray.[](5, true, nil, 'a', "Ruby", obj)
+ a.should.instance_of?(ArraySpecs::MyArray)
+ a.inspect.should == [5, true, nil, "a", "Ruby", obj].inspect
+ end
+end
+
+describe "Array[]" do
+ it "is a synonym for .[]" do
+ obj = Object.new
+ Array[5, true, nil, 'a', "Ruby", obj].should == Array.[](5, true, nil, "a", "Ruby", obj)
+
+ a = ArraySpecs::MyArray[5, true, nil, 'a', "Ruby", obj]
+ a.should.instance_of?(ArraySpecs::MyArray)
+ a.inspect.should == [5, true, nil, "a", "Ruby", obj].inspect
+ end
+end
diff --git a/spec/ruby/core/array/count_spec.rb b/spec/ruby/core/array/count_spec.rb
new file mode 100644
index 0000000000..e778233c16
--- /dev/null
+++ b/spec/ruby/core/array/count_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#count" do
+ it "returns the number of elements" do
+ [:a, :b, :c].count.should == 3
+ end
+
+ it "returns the number of elements that equal the argument" do
+ [:a, :b, :b, :c].count(:b).should == 2
+ end
+
+ it "returns the number of element for which the block evaluates to true" do
+ [:a, :b, :c].count { |s| s != :b }.should == 2
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ [:a, :b, :b, :c].count(:b) { |e| e.size > 10 }.should == 2
+ }.should complain(/given block not used/)
+ end
+
+ context "when a block argument given" do
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :count
+ end
+end
diff --git a/spec/ruby/core/array/cycle_spec.rb b/spec/ruby/core/array/cycle_spec.rb
new file mode 100644
index 0000000000..29284257e9
--- /dev/null
+++ b/spec/ruby/core/array/cycle_spec.rb
@@ -0,0 +1,101 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Array#cycle" do
+ before :each do
+ ScratchPad.record []
+
+ @array = [1, 2, 3]
+ @prc = -> x { ScratchPad << x }
+ end
+
+ it "does not yield and returns nil when the array is empty and passed value is an integer" do
+ [].cycle(6, &@prc).should == nil
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not yield and returns nil when the array is empty and passed value is nil" do
+ [].cycle(nil, &@prc).should == nil
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not yield and returns nil when passed 0" do
+ @array.cycle(0, &@prc).should == nil
+ ScratchPad.recorded.should == []
+ end
+
+ it "iterates the array 'count' times yielding each item to the block" do
+ @array.cycle(2, &@prc)
+ ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3]
+ end
+
+ it "iterates indefinitely when not passed a count" do
+ @array.cycle do |x|
+ ScratchPad << x
+ break if ScratchPad.recorded.size > 7
+ end
+ ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3, 1, 2]
+ end
+
+ it "iterates indefinitely when passed nil" do
+ @array.cycle(nil) do |x|
+ ScratchPad << x
+ break if ScratchPad.recorded.size > 7
+ end
+ ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3, 1, 2]
+ end
+
+ it "does not rescue StopIteration when not passed a count" do
+ -> do
+ @array.cycle { raise StopIteration }
+ end.should.raise(StopIteration)
+ end
+
+ it "does not rescue StopIteration when passed a count" do
+ -> do
+ @array.cycle(3) { raise StopIteration }
+ end.should.raise(StopIteration)
+ end
+
+ it "iterates the array Integer(count) times when passed a Float count" do
+ @array.cycle(2.7, &@prc)
+ ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3]
+ end
+
+ it "calls #to_int to convert count to an Integer" do
+ count = mock("cycle count 2")
+ count.should_receive(:to_int).and_return(2)
+
+ @array.cycle(count, &@prc)
+ ScratchPad.recorded.should == [1, 2, 3, 1, 2, 3]
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ count = mock("cycle count 2")
+ count.should_receive(:to_int).and_return("2")
+
+ -> { @array.cycle(count, &@prc) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a String" do
+ -> { @array.cycle("4") { } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Object" do
+ -> { @array.cycle(mock("cycle count")) { } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed true" do
+ -> { @array.cycle(true) { } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed false" do
+ -> { @array.cycle(false) { } }.should.raise(TypeError)
+ end
+
+ before :all do
+ @object = [1, 2, 3, 4]
+ @empty_object = []
+ end
+ it_should_behave_like :enumeratorized_with_cycle_size
+end
diff --git a/spec/ruby/core/array/deconstruct_spec.rb b/spec/ruby/core/array/deconstruct_spec.rb
new file mode 100644
index 0000000000..11bb8e72c4
--- /dev/null
+++ b/spec/ruby/core/array/deconstruct_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Array#deconstruct" do
+ it "returns self" do
+ array = [1]
+
+ array.deconstruct.should.equal? array
+ end
+end
diff --git a/spec/ruby/core/array/delete_at_spec.rb b/spec/ruby/core/array/delete_at_spec.rb
new file mode 100644
index 0000000000..1e298b6730
--- /dev/null
+++ b/spec/ruby/core/array/delete_at_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#delete_at" do
+ it "removes the element at the specified index" do
+ a = [1, 2, 3, 4]
+ a.delete_at(2)
+ a.should == [1, 2, 4]
+ a.delete_at(-1)
+ a.should == [1, 2]
+ end
+
+ it "returns the removed element at the specified index" do
+ a = [1, 2, 3, 4]
+ a.delete_at(2).should == 3
+ a.delete_at(-1).should == 4
+ end
+
+ it "returns nil and makes no modification if the index is out of range" do
+ a = [1, 2]
+ a.delete_at(3).should == nil
+ a.should == [1, 2]
+ a.delete_at(-3).should == nil
+ a.should == [1, 2]
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(-1)
+ [1, 2].delete_at(obj).should == 2
+ end
+
+ it "accepts negative indices" do
+ a = [1, 2]
+ a.delete_at(-2).should == 1
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { [1,2,3].freeze.delete_at(0) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/delete_if_spec.rb b/spec/ruby/core/array/delete_if_spec.rb
new file mode 100644
index 0000000000..701a612395
--- /dev/null
+++ b/spec/ruby/core/array/delete_if_spec.rb
@@ -0,0 +1,82 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorize'
+require_relative 'shared/delete_if'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Array#delete_if" do
+ before do
+ @a = [ "a", "b", "c" ]
+ end
+
+ it "removes each element for which block returns true" do
+ @a = [ "a", "b", "c" ]
+ @a.delete_if { |x| x >= "b" }
+ @a.should == ["a"]
+ end
+
+ it "returns self" do
+ @a.delete_if{ true }.equal?(@a).should == true
+ end
+
+ it_behaves_like :enumeratorize, :delete_if
+
+ it "returns self when called on an Array emptied with #shift" do
+ array = [1]
+ array.shift
+ array.delete_if { |x| true }.should.equal?(array)
+ end
+
+ it "returns an Enumerator if no block given, and the enumerator can modify the original array" do
+ enum = @a.delete_if
+ enum.should.instance_of?(Enumerator)
+ @a.should_not.empty?
+ enum.each { true }
+ @a.should.empty?
+ end
+
+ it "returns an Enumerator if no block given, and the array is frozen" do
+ @a.freeze.delete_if.should.instance_of?(Enumerator)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.delete_if {} }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on an empty frozen array" do
+ -> { ArraySpecs.empty_frozen_array.delete_if {} }.should.raise(FrozenError)
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.delete_if { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "only removes elements for which the block returns true, keeping the element which raised an error." do
+ a = [1, 2, 3, 4]
+ begin
+ a.delete_if do |e|
+ case e
+ when 2 then true
+ when 3 then raise StandardError, 'Oops'
+ else false
+ end
+ end
+ rescue StandardError
+ end
+
+ a.should == [1, 3, 4]
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :delete_if, [1,2,3]
+ it_behaves_like :delete_if, :delete_if
+
+ @value_to_return = -> _ { false }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :delete_if
+end
diff --git a/spec/ruby/core/array/delete_spec.rb b/spec/ruby/core/array/delete_spec.rb
new file mode 100644
index 0000000000..0d80b2839d
--- /dev/null
+++ b/spec/ruby/core/array/delete_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#delete" do
+ it "removes elements that are #== to object" do
+ x = mock('delete')
+ def x.==(other) 3 == other end
+
+ a = [1, 2, 3, x, 4, 3, 5, x]
+ a.delete mock('not contained')
+ a.should == [1, 2, 3, x, 4, 3, 5, x]
+
+ a.delete 3
+ a.should == [1, 2, 4, 5]
+ end
+
+ it "calculates equality correctly for reference values" do
+ a = ["foo", "bar", "foo", "quux", "foo"]
+ a.delete "foo"
+ a.should == ["bar","quux"]
+ end
+
+ it "returns object or nil if no elements match object" do
+ [1, 2, 4, 5].delete(1).should == 1
+ [1, 2, 4, 5].delete(3).should == nil
+ end
+
+ it "may be given a block that is executed if no element matches object" do
+ [1].delete(1) {:not_found}.should == 1
+ [].delete('a') {:not_found}.should == :not_found
+ end
+
+ it "returns nil if the array is empty due to a shift" do
+ a = [1]
+ a.shift
+ a.delete(nil).should == nil
+ end
+
+ it "returns nil on a frozen array if a modification does not take place" do
+ [1, 2, 3].freeze.delete(0).should == nil
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { [1, 2, 3].freeze.delete(1) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/difference_spec.rb b/spec/ruby/core/array/difference_spec.rb
new file mode 100644
index 0000000000..63e32feca0
--- /dev/null
+++ b/spec/ruby/core/array/difference_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/difference'
+
+describe "Array#difference" do
+ it_behaves_like :array_binary_difference, :difference
+
+ it "returns a copy when called without any parameter" do
+ x = [1, 2, 3, 2]
+ x.difference.should == x
+ x.difference.should_not.equal? x
+ end
+
+ it "does not return subclass instances for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].difference.should.instance_of?(Array)
+ end
+
+ it "accepts multiple arguments" do
+ x = [1, 2, 3, 1]
+ x.difference([], [0, 1], [3, 4], [3]).should == [2]
+ end
+end
diff --git a/spec/ruby/core/array/dig_spec.rb b/spec/ruby/core/array/dig_spec.rb
new file mode 100644
index 0000000000..4166ff9f1f
--- /dev/null
+++ b/spec/ruby/core/array/dig_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../../spec_helper'
+
+describe "Array#dig" do
+
+ it "returns #at with one arg" do
+ ['a'].dig(0).should == 'a'
+ ['a'].dig(1).should == nil
+ end
+
+ it "recurses array elements" do
+ a = [ [ 1, [2, '3'] ] ]
+ a.dig(0, 0).should == 1
+ a.dig(0, 1, 1).should == '3'
+ a.dig(0, -1, 0).should == 2
+ end
+
+ it "returns the nested value specified if the sequence includes a key" do
+ a = [42, { foo: :bar }]
+ a.dig(1, :foo).should == :bar
+ end
+
+ it "raises a TypeError for a non-numeric index" do
+ -> {
+ ['a'].dig(:first)
+ }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if any intermediate step does not respond to #dig" do
+ a = [1, 2]
+ -> {
+ a.dig(0, 1)
+ }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if no arguments provided" do
+ -> {
+ [10].dig()
+ }.should.raise(ArgumentError)
+ end
+
+ it "returns nil if any intermediate step is nil" do
+ a = [[1, [2, 3]]]
+ a.dig(1, 2, 3).should == nil
+ end
+
+ it "calls #dig on the result of #at with the remaining arguments" do
+ h = [[nil, [nil, nil, 42]]]
+ h[0].should_receive(:dig).with(1, 2).and_return(42)
+ h.dig(0, 1, 2).should == 42
+ end
+
+end
diff --git a/spec/ruby/core/array/drop_spec.rb b/spec/ruby/core/array/drop_spec.rb
new file mode 100644
index 0000000000..c0e1c9edce
--- /dev/null
+++ b/spec/ruby/core/array/drop_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#drop" do
+ it "removes the specified number of elements from the start of the array" do
+ [1, 2, 3, 4, 5].drop(2).should == [3, 4, 5]
+ end
+
+ it "raises an ArgumentError if the number of elements specified is negative" do
+ -> { [1, 2].drop(-3) }.should.raise(ArgumentError)
+ end
+
+ it "returns an empty Array if all elements are dropped" do
+ [1, 2].drop(2).should == []
+ end
+
+ it "returns an empty Array when called on an empty Array" do
+ [].drop(0).should == []
+ end
+
+ it "does not remove any elements when passed zero" do
+ [1, 2].drop(0).should == [1, 2]
+ end
+
+ it "returns an empty Array if more elements than exist are dropped" do
+ [1, 2].drop(3).should == []
+ end
+
+ it 'acts correctly after a shift' do
+ ary = [nil, 1, 2]
+ ary.shift
+ ary.drop(1).should == [2]
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return(2)
+
+ [1, 2, 3].drop(obj).should == [3]
+ end
+
+ it "raises a TypeError when the passed argument can't be coerced to Integer" do
+ -> { [1, 2].drop("cat") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the passed argument isn't an integer and #to_int returns non-Integer" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return("cat")
+
+ -> { [1, 2].drop(obj) }.should.raise(TypeError)
+ end
+
+ it 'returns a Array instance for Array subclasses' do
+ ArraySpecs::MyArray[1, 2, 3, 4, 5].drop(1).should.instance_of?(Array)
+ end
+end
diff --git a/spec/ruby/core/array/drop_while_spec.rb b/spec/ruby/core/array/drop_while_spec.rb
new file mode 100644
index 0000000000..4fead3ff06
--- /dev/null
+++ b/spec/ruby/core/array/drop_while_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#drop_while" do
+ @value_to_return = -> _ { true }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :drop_while
+
+ it "removes elements from the start of the array while the block evaluates to true" do
+ [1, 2, 3, 4].drop_while { |n| n < 4 }.should == [4]
+ end
+
+ it "removes elements from the start of the array until the block returns nil" do
+ [1, 2, 3, nil, 5].drop_while { |n| n }.should == [nil, 5]
+ end
+
+ it "removes elements from the start of the array until the block returns false" do
+ [1, 2, 3, false, 5].drop_while { |n| n }.should == [false, 5]
+ end
+
+ it 'returns a Array instance for Array subclasses' do
+ ArraySpecs::MyArray[1, 2, 3, 4, 5].drop_while { |n| n < 4 }.should.instance_of?(Array)
+ end
+end
diff --git a/spec/ruby/core/array/dup_spec.rb b/spec/ruby/core/array/dup_spec.rb
new file mode 100644
index 0000000000..f14aeca3b5
--- /dev/null
+++ b/spec/ruby/core/array/dup_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/clone'
+
+describe "Array#dup" do
+ it_behaves_like :array_clone, :dup # FIX: no, clone and dup are not alike
+
+ it "does not copy frozen status from the original" do
+ a = [1, 2, 3, 4]
+ b = [1, 2, 3, 4]
+ a.freeze
+ aa = a.dup
+ bb = b.dup
+
+ aa.frozen?.should == false
+ bb.frozen?.should == false
+ end
+
+ it "does not copy singleton methods" do
+ a = [1, 2, 3, 4]
+ b = [1, 2, 3, 4]
+ def a.a_singleton_method; end
+ aa = a.dup
+ bb = b.dup
+
+ a.respond_to?(:a_singleton_method).should == true
+ b.respond_to?(:a_singleton_method).should == false
+ aa.respond_to?(:a_singleton_method).should == false
+ bb.respond_to?(:a_singleton_method).should == false
+ end
+end
diff --git a/spec/ruby/core/array/each_index_spec.rb b/spec/ruby/core/array/each_index_spec.rb
new file mode 100644
index 0000000000..b238a89d8a
--- /dev/null
+++ b/spec/ruby/core/array/each_index_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorize'
+require_relative '../enumerable/shared/enumeratorized'
+
+# Modifying a collection while the contents are being iterated
+# gives undefined behavior. See
+# https://blade.ruby-lang.org/ruby-core/23633
+
+describe "Array#each_index" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "passes the index of each element to the block" do
+ a = ['a', 'b', 'c', 'd']
+ a.each_index { |i| ScratchPad << i }
+ ScratchPad.recorded.should == [0, 1, 2, 3]
+ end
+
+ it "returns self" do
+ a = [:a, :b, :c]
+ a.each_index { |i| }.should.equal?(a)
+ end
+
+ it "is not confused by removing elements from the front" do
+ a = [1, 2, 3]
+
+ a.shift
+ ScratchPad.record []
+ a.each_index { |i| ScratchPad << i }
+ ScratchPad.recorded.should == [0, 1]
+
+ a.shift
+ ScratchPad.record []
+ a.each_index { |i| ScratchPad << i }
+ ScratchPad.recorded.should == [0]
+ end
+
+ it_behaves_like :enumeratorize, :each_index
+ it_behaves_like :enumeratorized_with_origin_size, :each_index, [1,2,3]
+end
+
+describe "Array#each_index" do
+ it "tolerates increasing an array size during iteration" do
+ array = [:a, :b, :c]
+ ScratchPad.record []
+ i = 0
+
+ array.each_index do |index|
+ ScratchPad << index
+ array << i if i < 100
+ i += 1
+ end
+
+ ScratchPad.recorded.should == (0..102).to_a # element indices
+ end
+end
diff --git a/spec/ruby/core/array/each_spec.rb b/spec/ruby/core/array/each_spec.rb
new file mode 100644
index 0000000000..73a4c36b17
--- /dev/null
+++ b/spec/ruby/core/array/each_spec.rb
@@ -0,0 +1,82 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorize'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+require_relative '../enumerable/shared/enumeratorized'
+
+# Mutating the array while it is being iterated is discouraged as it can result in confusing behavior.
+# Yet a Ruby implementation must not crash in such a case, and following the simple CRuby behavior makes sense.
+# CRuby simply reads the array storage and checks the size for every iteration;
+# like `i = 0; while i < size; yield self[i]; i += 1; end`
+
+describe "Array#each" do
+ it "yields each element to the block" do
+ a = []
+ x = [1, 2, 3]
+ x.each { |item| a << item }.should.equal?(x)
+ a.should == [1, 2, 3]
+ end
+
+ it "yields each element to the block even if the array is changed during iteration" do
+ a = [1, 2, 3, 4, 5]
+ iterated = []
+ a.each { |x| iterated << x; a << x+5 if x.even? }
+ iterated.should == [1, 2, 3, 4, 5, 7, 9]
+ end
+
+ it "yields only elements that are still in the array" do
+ a = [0, 1, 2, 3, 4]
+ iterated = []
+ a.each { |x| iterated << x; a.pop if x.even? }
+ iterated.should == [0, 1, 2]
+ end
+
+ it "yields elements based on an internal index" do
+ a = [0, 1, 2, 3, 4]
+ iterated = []
+ a.each { |x| iterated << x; a.shift if x.even? }
+ iterated.should == [0, 2, 4]
+ end
+
+ it "yields the same element multiple times if inserting while iterating" do
+ a = [1, 2]
+ iterated = []
+ a.each { |x| iterated << x; a.unshift(0) if a.size == 2 }
+ iterated.should == [1, 1, 2]
+ end
+
+ it "yields each element to a block that takes multiple arguments" do
+ a = [[1, 2], :a, [3, 4]]
+ b = []
+
+ a.each { |x, y| b << x }
+ b.should == [1, :a, 3]
+
+ b = []
+ a.each { |x, y| b << y }
+ b.should == [2, nil, 4]
+ end
+
+ it "yields elements added to the end of the array by the block" do
+ a = [2]
+ iterated = []
+ a.each { |x| iterated << x; x.times { a << 0 } }
+
+ iterated.should == [2, 0, 0]
+ end
+
+ it "does not yield elements deleted from the end of the array" do
+ a = [2, 3, 1]
+ iterated = []
+ a.each { |x| iterated << x; a.delete_at(2) if x == 3 }
+
+ iterated.should == [2, 3]
+ end
+
+ it_behaves_like :enumeratorize, :each
+ it_behaves_like :enumeratorized_with_origin_size, :each, [1,2,3]
+end
+
+describe "Array#each" do
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :each
+end
diff --git a/spec/ruby/core/array/element_reference_spec.rb b/spec/ruby/core/array/element_reference_spec.rb
new file mode 100644
index 0000000000..d5f4b54961
--- /dev/null
+++ b/spec/ruby/core/array/element_reference_spec.rb
@@ -0,0 +1,903 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#[]" do
+ it "returns the element at index with [index]" do
+ [ "a", "b", "c", "d", "e" ][1].should == "b"
+
+ a = [1, 2, 3, 4]
+
+ a[0].should == 1
+ a[1].should == 2
+ a[2].should == 3
+ a[3].should == 4
+ a[4].should == nil
+ a[10].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns the element at index from the end of the array with [-index]" do
+ [ "a", "b", "c", "d", "e" ][-2].should == "d"
+
+ a = [1, 2, 3, 4]
+
+ a[-1].should == 4
+ a[-2].should == 3
+ a[-3].should == 2
+ a[-4].should == 1
+ a[-5].should == nil
+ a[-10].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns count elements starting from index with [index, count]" do
+ [ "a", "b", "c", "d", "e" ][2, 3].should == ["c", "d", "e"]
+
+ a = [1, 2, 3, 4]
+
+ a[0, 0].should == []
+ a[0, 1].should == [1]
+ a[0, 2].should == [1, 2]
+ a[0, 4].should == [1, 2, 3, 4]
+ a[0, 6].should == [1, 2, 3, 4]
+ a[0, -1].should == nil
+ a[0, -2].should == nil
+ a[0, -4].should == nil
+
+ a[2, 0].should == []
+ a[2, 1].should == [3]
+ a[2, 2].should == [3, 4]
+ a[2, 4].should == [3, 4]
+ a[2, -1].should == nil
+
+ a[4, 0].should == []
+ a[4, 2].should == []
+ a[4, -1].should == nil
+
+ a[5, 0].should == nil
+ a[5, 2].should == nil
+ a[5, -1].should == nil
+
+ a[6, 0].should == nil
+ a[6, 2].should == nil
+ a[6, -1].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns count elements starting at index from the end of array with [-index, count]" do
+ [ "a", "b", "c", "d", "e" ][-2, 2].should == ["d", "e"]
+
+ a = [1, 2, 3, 4]
+
+ a[-1, 0].should == []
+ a[-1, 1].should == [4]
+ a[-1, 2].should == [4]
+ a[-1, -1].should == nil
+
+ a[-2, 0].should == []
+ a[-2, 1].should == [3]
+ a[-2, 2].should == [3, 4]
+ a[-2, 4].should == [3, 4]
+ a[-2, -1].should == nil
+
+ a[-4, 0].should == []
+ a[-4, 1].should == [1]
+ a[-4, 2].should == [1, 2]
+ a[-4, 4].should == [1, 2, 3, 4]
+ a[-4, 6].should == [1, 2, 3, 4]
+ a[-4, -1].should == nil
+
+ a[-5, 0].should == nil
+ a[-5, 1].should == nil
+ a[-5, 10].should == nil
+ a[-5, -1].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns the first count elements with [0, count]" do
+ [ "a", "b", "c", "d", "e" ][0, 3].should == ["a", "b", "c"]
+ end
+
+ it "returns the subarray which is independent to self with [index,count]" do
+ a = [1, 2, 3]
+ sub = a[1, 2]
+ sub.replace([:a, :b])
+ a.should == [1, 2, 3]
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.stub!(:to_int).and_return(2)
+
+ a = [1, 2, 3, 4]
+ a[obj].should == 3
+ a[obj, 1].should == [3]
+ a[obj, obj].should == [3, 4]
+ a[0, obj].should == [1, 2]
+ end
+
+ it "raises TypeError if to_int returns non-integer" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ a = [1, 2, 3, 4, 5]
+
+ def from.to_int() 'cat' end
+ def to.to_int() -2 end
+
+ -> { a[from..to] }.should.raise(TypeError)
+
+ def from.to_int() 1 end
+ def to.to_int() 'cat' end
+
+ -> { a[from..to] }.should.raise(TypeError)
+ end
+
+ it "returns the elements specified by Range indexes with [m..n]" do
+ [ "a", "b", "c", "d", "e" ][1..3].should == ["b", "c", "d"]
+ [ "a", "b", "c", "d", "e" ][4..-1].should == ['e']
+ [ "a", "b", "c", "d", "e" ][3..3].should == ['d']
+ [ "a", "b", "c", "d", "e" ][3..-2].should == ['d']
+ ['a'][0..-1].should == ['a']
+
+ a = [1, 2, 3, 4]
+
+ a[0..-10].should == []
+ a[0..0].should == [1]
+ a[0..1].should == [1, 2]
+ a[0..2].should == [1, 2, 3]
+ a[0..3].should == [1, 2, 3, 4]
+ a[0..4].should == [1, 2, 3, 4]
+ a[0..10].should == [1, 2, 3, 4]
+
+ a[2..-10].should == []
+ a[2..0].should == []
+ a[2..2].should == [3]
+ a[2..3].should == [3, 4]
+ a[2..4].should == [3, 4]
+
+ a[3..0].should == []
+ a[3..3].should == [4]
+ a[3..4].should == [4]
+
+ a[4..0].should == []
+ a[4..4].should == []
+ a[4..5].should == []
+
+ a[5..0].should == nil
+ a[5..5].should == nil
+ a[5..6].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns elements specified by Range indexes except the element at index n with [m...n]" do
+ [ "a", "b", "c", "d", "e" ][1...3].should == ["b", "c"]
+
+ a = [1, 2, 3, 4]
+
+ a[0...-10].should == []
+ a[0...0].should == []
+ a[0...1].should == [1]
+ a[0...2].should == [1, 2]
+ a[0...3].should == [1, 2, 3]
+ a[0...4].should == [1, 2, 3, 4]
+ a[0...10].should == [1, 2, 3, 4]
+
+ a[2...-10].should == []
+ a[2...0].should == []
+ a[2...2].should == []
+ a[2...3].should == [3]
+ a[2...4].should == [3, 4]
+
+ a[3...0].should == []
+ a[3...3].should == []
+ a[3...4].should == [4]
+
+ a[4...0].should == []
+ a[4...4].should == []
+ a[4...5].should == []
+
+ a[5...0].should == nil
+ a[5...5].should == nil
+ a[5...6].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns elements that exist if range start is in the array but range end is not with [m..n]" do
+ [ "a", "b", "c", "d", "e" ][4..7].should == ["e"]
+ end
+
+ it "accepts Range instances having a negative m and both signs for n with [m..n] and [m...n]" do
+ a = [1, 2, 3, 4]
+
+ a[-1..-1].should == [4]
+ a[-1...-1].should == []
+ a[-1..3].should == [4]
+ a[-1...3].should == []
+ a[-1..4].should == [4]
+ a[-1...4].should == [4]
+ a[-1..10].should == [4]
+ a[-1...10].should == [4]
+ a[-1..0].should == []
+ a[-1..-4].should == []
+ a[-1...-4].should == []
+ a[-1..-6].should == []
+ a[-1...-6].should == []
+
+ a[-2..-2].should == [3]
+ a[-2...-2].should == []
+ a[-2..-1].should == [3, 4]
+ a[-2...-1].should == [3]
+ a[-2..10].should == [3, 4]
+ a[-2...10].should == [3, 4]
+
+ a[-4..-4].should == [1]
+ a[-4..-2].should == [1, 2, 3]
+ a[-4...-2].should == [1, 2]
+ a[-4..-1].should == [1, 2, 3, 4]
+ a[-4...-1].should == [1, 2, 3]
+ a[-4..3].should == [1, 2, 3, 4]
+ a[-4...3].should == [1, 2, 3]
+ a[-4..4].should == [1, 2, 3, 4]
+ a[-4...4].should == [1, 2, 3, 4]
+ a[-4...4].should == [1, 2, 3, 4]
+ a[-4..0].should == [1]
+ a[-4...0].should == []
+ a[-4..1].should == [1, 2]
+ a[-4...1].should == [1]
+
+ a[-5..-5].should == nil
+ a[-5...-5].should == nil
+ a[-5..-4].should == nil
+ a[-5..-1].should == nil
+ a[-5..10].should == nil
+
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "returns the subarray which is independent to self with [m..n]" do
+ a = [1, 2, 3]
+ sub = a[1..2]
+ sub.replace([:a, :b])
+ a.should == [1, 2, 3]
+ end
+
+ it "tries to convert Range elements to Integers using #to_int with [m..n] and [m...n]" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ def from.to_int() 1 end
+ def to.to_int() -2 end
+
+ a = [1, 2, 3, 4]
+
+ a[from..to].should == [2, 3]
+ a[from...to].should == [2]
+ a[1..0].should == []
+ a[1...0].should == []
+
+ -> { a["a" .. "b"] }.should.raise(TypeError)
+ -> { a["a" ... "b"] }.should.raise(TypeError)
+ -> { a[from .. "b"] }.should.raise(TypeError)
+ -> { a[from ... "b"] }.should.raise(TypeError)
+ end
+
+ it "returns the same elements as [m..n] and [m...n] with Range subclasses" do
+ a = [1, 2, 3, 4]
+ range_incl = ArraySpecs::MyRange.new(1, 2)
+ range_excl = ArraySpecs::MyRange.new(-3, -1, true)
+
+ a[range_incl].should == [2, 3]
+ a[range_excl].should == [2, 3]
+ end
+
+ it "returns nil for a requested index not in the array with [index]" do
+ [ "a", "b", "c", "d", "e" ][5].should == nil
+ end
+
+ it "returns [] if the index is valid but length is zero with [index, length]" do
+ [ "a", "b", "c", "d", "e" ][0, 0].should == []
+ [ "a", "b", "c", "d", "e" ][2, 0].should == []
+ end
+
+ it "returns nil if length is zero but index is invalid with [index, length]" do
+ [ "a", "b", "c", "d", "e" ][100, 0].should == nil
+ [ "a", "b", "c", "d", "e" ][-50, 0].should == nil
+ end
+
+ # This is by design. It is in the official documentation.
+ it "returns [] if index == array.size with [index, length]" do
+ %w|a b c d e|[5, 2].should == []
+ end
+
+ it "returns nil if index > array.size with [index, length]" do
+ %w|a b c d e|[6, 2].should == nil
+ end
+
+ it "returns nil if length is negative with [index, length]" do
+ %w|a b c d e|[3, -1].should == nil
+ %w|a b c d e|[2, -2].should == nil
+ %w|a b c d e|[1, -100].should == nil
+ end
+
+ it "returns nil if no requested index is in the array with [m..n]" do
+ [ "a", "b", "c", "d", "e" ][6..10].should == nil
+ end
+
+ it "returns nil if range start is not in the array with [m..n]" do
+ [ "a", "b", "c", "d", "e" ][-10..2].should == nil
+ [ "a", "b", "c", "d", "e" ][10..12].should == nil
+ end
+
+ it "returns an empty array when m == n with [m...n]" do
+ [1, 2, 3, 4, 5][1...1].should == []
+ end
+
+ it "returns an empty array with [0...0]" do
+ [1, 2, 3, 4, 5][0...0].should == []
+ end
+
+ it "returns a subarray where m, n negatives and m < n with [m..n]" do
+ [ "a", "b", "c", "d", "e" ][-3..-2].should == ["c", "d"]
+ end
+
+ it "returns an array containing the first element with [0..0]" do
+ [1, 2, 3, 4, 5][0..0].should == [1]
+ end
+
+ it "returns the entire array with [0..-1]" do
+ [1, 2, 3, 4, 5][0..-1].should == [1, 2, 3, 4, 5]
+ end
+
+ it "returns all but the last element with [0...-1]" do
+ [1, 2, 3, 4, 5][0...-1].should == [1, 2, 3, 4]
+ end
+
+ it "returns [3] for [2..-1] out of [1, 2, 3]" do
+ [1,2,3][2..-1].should == [3]
+ end
+
+ it "returns an empty array when m > n and m, n are positive with [m..n]" do
+ [1, 2, 3, 4, 5][3..2].should == []
+ end
+
+ it "returns an empty array when m > n and m, n are negative with [m..n]" do
+ [1, 2, 3, 4, 5][-2..-3].should == []
+ end
+
+ it "does not expand array when the indices are outside of the array bounds" do
+ a = [1, 2]
+ a[4].should == nil
+ a.should == [1, 2]
+ a[4, 0].should == nil
+ a.should == [1, 2]
+ a[6, 1].should == nil
+ a.should == [1, 2]
+ a[8...8].should == nil
+ a.should == [1, 2]
+ a[10..10].should == nil
+ a.should == [1, 2]
+ end
+
+ describe "with a subclass of Array" do
+ before :each do
+ ScratchPad.clear
+
+ @array = ArraySpecs::MyArray[1, 2, 3, 4, 5]
+ end
+
+ it "returns a Array instance with [n, m]" do
+ @array[0, 2].should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [-n, m]" do
+ @array[-3, 2].should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [n..m]" do
+ @array[1..3].should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [n...m]" do
+ @array[1...3].should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [-n..-m]" do
+ @array[-3..-1].should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [-n...-m]" do
+ @array[-3...-1].should.instance_of?(Array)
+ end
+
+ it "returns an empty array when m == n with [m...n]" do
+ @array[1...1].should == []
+ ScratchPad.recorded.should == nil
+ end
+
+ it "returns an empty array with [0...0]" do
+ @array[0...0].should == []
+ ScratchPad.recorded.should == nil
+ end
+
+ it "returns an empty array when m > n and m, n are positive with [m..n]" do
+ @array[3..2].should == []
+ ScratchPad.recorded.should == nil
+ end
+
+ it "returns an empty array when m > n and m, n are negative with [m..n]" do
+ @array[-2..-3].should == []
+ ScratchPad.recorded.should == nil
+ end
+
+ it "returns [] if index == array.size with [index, length]" do
+ @array[5, 2].should == []
+ ScratchPad.recorded.should == nil
+ end
+
+ it "returns [] if the index is valid but length is zero with [index, length]" do
+ @array[0, 0].should == []
+ @array[2, 0].should == []
+ ScratchPad.recorded.should == nil
+ end
+
+ it "does not call #initialize on the subclass instance" do
+ @array[0, 3].should == [1, 2, 3]
+ ScratchPad.recorded.should == nil
+ end
+ end
+
+ it "raises a RangeError when the start index is out of range of Fixnum" do
+ array = [1, 2, 3, 4, 5, 6]
+ obj = mock('large value')
+ obj.should_receive(:to_int).and_return(bignum_value)
+ -> { array[obj] }.should.raise(RangeError)
+
+ obj = 8e19
+ -> { array[obj] }.should.raise(RangeError)
+
+ # boundary value when longs are 64 bits
+ -> { array[2.0**63] }.should.raise(RangeError)
+
+ # just under the boundary value when longs are 64 bits
+ array[max_long.to_f.prev_float].should == nil
+ end
+
+ it "raises a RangeError when the length is out of range of Fixnum" do
+ array = [1, 2, 3, 4, 5, 6]
+ obj = mock('large value')
+ obj.should_receive(:to_int).and_return(bignum_value)
+ -> { array[1, obj] }.should.raise(RangeError)
+
+ obj = 8e19
+ -> { array[1, obj] }.should.raise(RangeError)
+ end
+
+ it "raises a type error if a range is passed with a length" do
+ ->{ [1, 2, 3][1..2, 1] }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError if passed a range with a bound that is too large" do
+ array = [1, 2, 3, 4, 5, 6]
+ -> { array[bignum_value..(bignum_value + 1)] }.should.raise(RangeError)
+ -> { array[0..bignum_value] }.should.raise(RangeError)
+ end
+
+ it "can accept endless ranges" do
+ a = [0, 1, 2, 3, 4, 5]
+ a[eval("(2..)")].should == [2, 3, 4, 5]
+ a[eval("(2...)")].should == [2, 3, 4, 5]
+ a[eval("(-2..)")].should == [4, 5]
+ a[eval("(-2...)")].should == [4, 5]
+ a[eval("(9..)")].should == nil
+ a[eval("(9...)")].should == nil
+ a[eval("(-9..)")].should == nil
+ a[eval("(-9...)")].should == nil
+ end
+
+ describe "can be sliced with Enumerator::ArithmeticSequence" do
+ before :each do
+ @array = [0, 1, 2, 3, 4, 5]
+ end
+
+ it "has endless range and positive steps" do
+ @array[eval("(0..).step(1)")].should == [0, 1, 2, 3, 4, 5]
+ @array[eval("(0..).step(2)")].should == [0, 2, 4]
+ @array[eval("(0..).step(10)")].should == [0]
+
+ @array[eval("(2..).step(1)")].should == [2, 3, 4, 5]
+ @array[eval("(2..).step(2)")].should == [2, 4]
+ @array[eval("(2..).step(10)")].should == [2]
+
+ @array[eval("(-3..).step(1)")].should == [3, 4, 5]
+ @array[eval("(-3..).step(2)")].should == [3, 5]
+ @array[eval("(-3..).step(10)")].should == [3]
+ end
+
+ it "has beginless range and positive steps" do
+ # end with zero index
+ @array[(..0).step(1)].should == [0]
+ @array[(...0).step(1)].should == []
+
+ @array[(..0).step(2)].should == [0]
+ @array[(...0).step(2)].should == []
+
+ @array[(..0).step(10)].should == [0]
+ @array[(...0).step(10)].should == []
+
+ # end with positive index
+ @array[(..3).step(1)].should == [0, 1, 2, 3]
+ @array[(...3).step(1)].should == [0, 1, 2]
+
+ @array[(..3).step(2)].should == [0, 2]
+ @array[(...3).step(2)].should == [0, 2]
+
+ @array[(..3).step(10)].should == [0]
+ @array[(...3).step(10)].should == [0]
+
+ # end with negative index
+ @array[(..-2).step(1)].should == [0, 1, 2, 3, 4,]
+ @array[(...-2).step(1)].should == [0, 1, 2, 3]
+
+ @array[(..-2).step(2)].should == [0, 2, 4]
+ @array[(...-2).step(2)].should == [0, 2]
+
+ @array[(..-2).step(10)].should == [0]
+ @array[(...-2).step(10)].should == [0]
+ end
+
+ it "has endless range and negative steps" do
+ @array[eval("(0..).step(-1)")].should == [0]
+ @array[eval("(0..).step(-2)")].should == [0]
+ @array[eval("(0..).step(-10)")].should == [0]
+
+ @array[eval("(2..).step(-1)")].should == [2, 1, 0]
+ @array[eval("(2..).step(-2)")].should == [2, 0]
+
+ @array[eval("(-3..).step(-1)")].should == [3, 2, 1, 0]
+ @array[eval("(-3..).step(-2)")].should == [3, 1]
+ end
+
+ it "has closed range and positive steps" do
+ # start and end with 0
+ @array[eval("(0..0).step(1)")].should == [0]
+ @array[eval("(0...0).step(1)")].should == []
+
+ @array[eval("(0..0).step(2)")].should == [0]
+ @array[eval("(0...0).step(2)")].should == []
+
+ @array[eval("(0..0).step(10)")].should == [0]
+ @array[eval("(0...0).step(10)")].should == []
+
+ # start and end with positive index
+ @array[eval("(1..3).step(1)")].should == [1, 2, 3]
+ @array[eval("(1...3).step(1)")].should == [1, 2]
+
+ @array[eval("(1..3).step(2)")].should == [1, 3]
+ @array[eval("(1...3).step(2)")].should == [1]
+
+ @array[eval("(1..3).step(10)")].should == [1]
+ @array[eval("(1...3).step(10)")].should == [1]
+
+ # start with positive index, end with negative index
+ @array[eval("(1..-2).step(1)")].should == [1, 2, 3, 4]
+ @array[eval("(1...-2).step(1)")].should == [1, 2, 3]
+
+ @array[eval("(1..-2).step(2)")].should == [1, 3]
+ @array[eval("(1...-2).step(2)")].should == [1, 3]
+
+ @array[eval("(1..-2).step(10)")].should == [1]
+ @array[eval("(1...-2).step(10)")].should == [1]
+
+ # start with negative index, end with positive index
+ @array[eval("(-4..4).step(1)")].should == [2, 3, 4]
+ @array[eval("(-4...4).step(1)")].should == [2, 3]
+
+ @array[eval("(-4..4).step(2)")].should == [2, 4]
+ @array[eval("(-4...4).step(2)")].should == [2]
+
+ @array[eval("(-4..4).step(10)")].should == [2]
+ @array[eval("(-4...4).step(10)")].should == [2]
+
+ # start with negative index, end with negative index
+ @array[eval("(-4..-2).step(1)")].should == [2, 3, 4]
+ @array[eval("(-4...-2).step(1)")].should == [2, 3]
+
+ @array[eval("(-4..-2).step(2)")].should == [2, 4]
+ @array[eval("(-4...-2).step(2)")].should == [2]
+
+ @array[eval("(-4..-2).step(10)")].should == [2]
+ @array[eval("(-4...-2).step(10)")].should == [2]
+ end
+
+ it "has closed range and negative steps" do
+ # start and end with 0
+ @array[eval("(0..0).step(-1)")].should == [0]
+ @array[eval("(0...0).step(-1)")].should == []
+
+ @array[eval("(0..0).step(-2)")].should == [0]
+ @array[eval("(0...0).step(-2)")].should == []
+
+ @array[eval("(0..0).step(-10)")].should == [0]
+ @array[eval("(0...0).step(-10)")].should == []
+
+ # start and end with positive index
+ @array[eval("(1..3).step(-1)")].should == []
+ @array[eval("(1...3).step(-1)")].should == []
+
+ @array[eval("(1..3).step(-2)")].should == []
+ @array[eval("(1...3).step(-2)")].should == []
+
+ @array[eval("(1..3).step(-10)")].should == []
+ @array[eval("(1...3).step(-10)")].should == []
+
+ # start with positive index, end with negative index
+ @array[eval("(1..-2).step(-1)")].should == []
+ @array[eval("(1...-2).step(-1)")].should == []
+
+ @array[eval("(1..-2).step(-2)")].should == []
+ @array[eval("(1...-2).step(-2)")].should == []
+
+ @array[eval("(1..-2).step(-10)")].should == []
+ @array[eval("(1...-2).step(-10)")].should == []
+
+ # start with negative index, end with positive index
+ @array[eval("(-4..4).step(-1)")].should == []
+ @array[eval("(-4...4).step(-1)")].should == []
+
+ @array[eval("(-4..4).step(-2)")].should == []
+ @array[eval("(-4...4).step(-2)")].should == []
+
+ @array[eval("(-4..4).step(-10)")].should == []
+ @array[eval("(-4...4).step(-10)")].should == []
+
+ # start with negative index, end with negative index
+ @array[eval("(-4..-2).step(-1)")].should == []
+ @array[eval("(-4...-2).step(-1)")].should == []
+
+ @array[eval("(-4..-2).step(-2)")].should == []
+ @array[eval("(-4...-2).step(-2)")].should == []
+
+ @array[eval("(-4..-2).step(-10)")].should == []
+ @array[eval("(-4...-2).step(-10)")].should == []
+ end
+
+ it "has inverted closed range and positive steps" do
+ # start and end with positive index
+ @array[eval("(3..1).step(1)")].should == []
+ @array[eval("(3...1).step(1)")].should == []
+
+ @array[eval("(3..1).step(2)")].should == []
+ @array[eval("(3...1).step(2)")].should == []
+
+ @array[eval("(3..1).step(10)")].should == []
+ @array[eval("(3...1).step(10)")].should == []
+
+ # start with negative index, end with positive index
+ @array[eval("(-2..1).step(1)")].should == []
+ @array[eval("(-2...1).step(1)")].should == []
+
+ @array[eval("(-2..1).step(2)")].should == []
+ @array[eval("(-2...1).step(2)")].should == []
+
+ @array[eval("(-2..1).step(10)")].should == []
+ @array[eval("(-2...1).step(10)")].should == []
+
+ # start with positive index, end with negative index
+ @array[eval("(4..-4).step(1)")].should == []
+ @array[eval("(4...-4).step(1)")].should == []
+
+ @array[eval("(4..-4).step(2)")].should == []
+ @array[eval("(4...-4).step(2)")].should == []
+
+ @array[eval("(4..-4).step(10)")].should == []
+ @array[eval("(4...-4).step(10)")].should == []
+
+ # start with negative index, end with negative index
+ @array[eval("(-2..-4).step(1)")].should == []
+ @array[eval("(-2...-4).step(1)")].should == []
+
+ @array[eval("(-2..-4).step(2)")].should == []
+ @array[eval("(-2...-4).step(2)")].should == []
+
+ @array[eval("(-2..-4).step(10)")].should == []
+ @array[eval("(-2...-4).step(10)")].should == []
+ end
+
+ it "has range with bounds outside of array" do
+ # end is equal to array's length
+ @array[(0..6).step(1)].should == [0, 1, 2, 3, 4, 5]
+ -> { @array[(0..6).step(2)] }.should.raise(RangeError)
+
+ # end is greater than length with positive steps
+ @array[(1..6).step(2)].should == [1, 3, 5]
+ @array[(2..7).step(2)].should == [2, 4]
+ -> { @array[(2..8).step(2)] }.should.raise(RangeError)
+
+ # begin is greater than length with negative steps
+ @array[(6..1).step(-2)].should == [5, 3, 1]
+ @array[(7..2).step(-2)].should == [5, 3]
+ -> { @array[(8..2).step(-2)] }.should.raise(RangeError)
+ end
+
+ it "has endless range with start outside of array's bounds" do
+ @array[eval("(6..).step(1)")].should == []
+ @array[eval("(7..).step(1)")].should == nil
+
+ @array[eval("(6..).step(2)")].should == []
+ -> { @array[eval("(7..).step(2)")] }.should.raise(RangeError)
+ end
+ end
+
+ it "can accept beginless ranges" do
+ a = [0, 1, 2, 3, 4, 5]
+ a[(..3)].should == [0, 1, 2, 3]
+ a[(...3)].should == [0, 1, 2]
+ a[(..-3)].should == [0, 1, 2, 3]
+ a[(...-3)].should == [0, 1, 2]
+ a[(..0)].should == [0]
+ a[(...0)].should == []
+ a[(..9)].should == [0, 1, 2, 3, 4, 5]
+ a[(...9)].should == [0, 1, 2, 3, 4, 5]
+ a[(..-9)].should == []
+ a[(...-9)].should == []
+ end
+
+ describe "can be sliced with Enumerator::ArithmeticSequence" do
+ it "with infinite/inverted ranges and negative steps" do
+ array = [0, 1, 2, 3, 4, 5]
+ array[(2..).step(-1)].should == [2, 1, 0]
+ array[(2..).step(-2)].should == [2, 0]
+ array[(2..).step(-3)].should == [2]
+ array[(2..).step(-4)].should == [2]
+
+ array[(-3..).step(-1)].should == [3, 2, 1, 0]
+ array[(-3..).step(-2)].should == [3, 1]
+ array[(-3..).step(-3)].should == [3, 0]
+ array[(-3..).step(-4)].should == [3]
+ array[(-3..).step(-5)].should == [3]
+
+ array[(..0).step(-1)].should == [5, 4, 3, 2, 1, 0]
+ array[(..0).step(-2)].should == [5, 3, 1]
+ array[(..0).step(-3)].should == [5, 2]
+ array[(..0).step(-4)].should == [5, 1]
+ array[(..0).step(-5)].should == [5, 0]
+ array[(..0).step(-6)].should == [5]
+ array[(..0).step(-7)].should == [5]
+
+ array[(...0).step(-1)].should == [5, 4, 3, 2, 1]
+ array[(...0).step(-2)].should == [5, 3, 1]
+ array[(...0).step(-3)].should == [5, 2]
+ array[(...0).step(-4)].should == [5, 1]
+ array[(...0).step(-5)].should == [5]
+ array[(...0).step(-6)].should == [5]
+
+ array[(...1).step(-1)].should == [5, 4, 3, 2]
+ array[(...1).step(-2)].should == [5, 3]
+ array[(...1).step(-3)].should == [5, 2]
+ array[(...1).step(-4)].should == [5]
+ array[(...1).step(-5)].should == [5]
+
+ array[(..-5).step(-1)].should == [5, 4, 3, 2, 1]
+ array[(..-5).step(-2)].should == [5, 3, 1]
+ array[(..-5).step(-3)].should == [5, 2]
+ array[(..-5).step(-4)].should == [5, 1]
+ array[(..-5).step(-5)].should == [5]
+ array[(..-5).step(-6)].should == [5]
+
+ array[(...-5).step(-1)].should == [5, 4, 3, 2]
+ array[(...-5).step(-2)].should == [5, 3]
+ array[(...-5).step(-3)].should == [5, 2]
+ array[(...-5).step(-4)].should == [5]
+ array[(...-5).step(-5)].should == [5]
+
+ array[(4..1).step(-1)].should == [4, 3, 2, 1]
+ array[(4..1).step(-2)].should == [4, 2]
+ array[(4..1).step(-3)].should == [4, 1]
+ array[(4..1).step(-4)].should == [4]
+ array[(4..1).step(-5)].should == [4]
+
+ array[(4...1).step(-1)].should == [4, 3, 2]
+ array[(4...1).step(-2)].should == [4, 2]
+ array[(4...1).step(-3)].should == [4]
+ array[(4...1).step(-4)].should == [4]
+
+ array[(-2..1).step(-1)].should == [4, 3, 2, 1]
+ array[(-2..1).step(-2)].should == [4, 2]
+ array[(-2..1).step(-3)].should == [4, 1]
+ array[(-2..1).step(-4)].should == [4]
+ array[(-2..1).step(-5)].should == [4]
+
+ array[(-2...1).step(-1)].should == [4, 3, 2]
+ array[(-2...1).step(-2)].should == [4, 2]
+ array[(-2...1).step(-3)].should == [4]
+ array[(-2...1).step(-4)].should == [4]
+
+ array[(4..-5).step(-1)].should == [4, 3, 2, 1]
+ array[(4..-5).step(-2)].should == [4, 2]
+ array[(4..-5).step(-3)].should == [4, 1]
+ array[(4..-5).step(-4)].should == [4]
+ array[(4..-5).step(-5)].should == [4]
+
+ array[(4...-5).step(-1)].should == [4, 3, 2]
+ array[(4...-5).step(-2)].should == [4, 2]
+ array[(4...-5).step(-3)].should == [4]
+ array[(4...-5).step(-4)].should == [4]
+
+ array[(-2..-5).step(-1)].should == [4, 3, 2, 1]
+ array[(-2..-5).step(-2)].should == [4, 2]
+ array[(-2..-5).step(-3)].should == [4, 1]
+ array[(-2..-5).step(-4)].should == [4]
+ array[(-2..-5).step(-5)].should == [4]
+
+ array[(-2...-5).step(-1)].should == [4, 3, 2]
+ array[(-2...-5).step(-2)].should == [4, 2]
+ array[(-2...-5).step(-3)].should == [4]
+ array[(-2...-5).step(-4)].should == [4]
+ end
+ end
+
+ it "can accept nil...nil ranges" do
+ a = [0, 1, 2, 3, 4, 5]
+ a[eval("(nil...nil)")].should == a
+ a[(...nil)].should == a
+ a[eval("(nil..)")].should == a
+ end
+end
+
+describe "Array.[]" do
+ it "[] should return a new array populated with the given elements" do
+ array = Array[1, 'a', nil]
+ array[0].should == 1
+ array[1].should == 'a'
+ array[2].should == nil
+ end
+
+ it "when applied to a literal nested array, unpacks its elements into the containing array" do
+ Array[1, 2, *[3, 4, 5]].should == [1, 2, 3, 4, 5]
+ end
+
+ it "when applied to a nested referenced array, unpacks its elements into the containing array" do
+ splatted_array = Array[3, 4, 5]
+ Array[1, 2, *splatted_array].should == [1, 2, 3, 4, 5]
+ end
+
+ it "can unpack 2 or more nested referenced array" do
+ splatted_array = Array[3, 4, 5]
+ splatted_array2 = Array[6, 7, 8]
+ Array[1, 2, *splatted_array, *splatted_array2].should == [1, 2, 3, 4, 5, 6, 7, 8]
+ end
+
+ it "constructs a nested Hash for tailing key-value pairs" do
+ Array[1, 2, 3 => 4, 5 => 6].should == [1, 2, { 3 => 4, 5 => 6 }]
+ end
+
+ describe "with a subclass of Array" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "returns an instance of the subclass" do
+ ArraySpecs::MyArray[1, 2, 3].should.instance_of?(ArraySpecs::MyArray)
+ end
+
+ it "does not call #initialize on the subclass instance" do
+ ArraySpecs::MyArray[1, 2, 3].should == [1, 2, 3]
+ ScratchPad.recorded.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/array/element_set_spec.rb b/spec/ruby/core/array/element_set_spec.rb
new file mode 100644
index 0000000000..671e4338de
--- /dev/null
+++ b/spec/ruby/core/array/element_set_spec.rb
@@ -0,0 +1,537 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#[]=" do
+ it "sets the value of the element at index" do
+ a = [1, 2, 3, 4]
+ a[2] = 5
+ a[-1] = 6
+ a[5] = 3
+ a.should == [1, 2, 5, 6, nil, 3]
+
+ a = []
+ a[4] = "e"
+ a.should == [nil, nil, nil, nil, "e"]
+ a[3] = "d"
+ a.should == [nil, nil, nil, "d", "e"]
+ a[0] = "a"
+ a.should == ["a", nil, nil, "d", "e"]
+ a[-3] = "C"
+ a.should == ["a", nil, "C", "d", "e"]
+ a[-1] = "E"
+ a.should == ["a", nil, "C", "d", "E"]
+ a[-5] = "A"
+ a.should == ["A", nil, "C", "d", "E"]
+ a[5] = "f"
+ a.should == ["A", nil, "C", "d", "E", "f"]
+ a[1] = []
+ a.should == ["A", [], "C", "d", "E", "f"]
+ a[-1] = nil
+ a.should == ["A", [], "C", "d", "E", nil]
+ end
+
+ it "sets the section defined by [start,length] to other" do
+ a = [1, 2, 3, 4, 5, 6]
+ a[0, 1] = 2
+ a[3, 2] = ['a', 'b', 'c', 'd']
+ a.should == [2, 2, 3, "a", "b", "c", "d", 6]
+ end
+
+ it "replaces the section defined by [start,length] with the given values" do
+ a = [1, 2, 3, 4, 5, 6]
+ a[3, 2] = 'a', 'b', 'c', 'd'
+ a.should == [1, 2, 3, "a", "b", "c", "d", 6]
+ end
+
+ it "just sets the section defined by [start,length] to other even if other is nil" do
+ a = ['a', 'b', 'c', 'd', 'e']
+ a[1, 3] = nil
+ a.should == ["a", nil, "e"]
+ end
+
+ it "returns nil if the rhs is nil" do
+ a = [1, 2, 3]
+ (a[1, 3] = nil).should == nil
+ (a[1..3] = nil).should == nil
+ end
+
+ it "sets the section defined by range to other" do
+ a = [6, 5, 4, 3, 2, 1]
+ a[1...2] = 9
+ a[3..6] = [6, 6, 6]
+ a.should == [6, 9, 4, 6, 6, 6]
+ end
+
+ it "replaces the section defined by range with the given values" do
+ a = [6, 5, 4, 3, 2, 1]
+ a[3..6] = :a, :b, :c
+ a.should == [6, 5, 4, :a, :b, :c]
+ end
+
+ it "just sets the section defined by range to other even if other is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[0..1] = nil
+ a.should == [nil, 3, 4, 5]
+ end
+
+ it 'expands and nil-pads the array if section assigned by range is outside array boundaries' do
+ a = ['a']
+ a[3..4] = ['b', 'c']
+ a.should == ['a', nil, nil, 'b', 'c']
+ end
+
+ it "calls to_int on its start and length arguments" do
+ obj = mock('to_int')
+ obj.stub!(:to_int).and_return(2)
+
+ a = [1, 2, 3, 4]
+ a[obj, 0] = [9]
+ a.should == [1, 2, 9, 3, 4]
+ a[obj, obj] = []
+ a.should == [1, 2, 4]
+ a[obj] = -1
+ a.should == [1, 2, -1]
+ end
+
+ it "checks frozen before attempting to coerce arguments" do
+ a = [1,2,3,4].freeze
+ -> {a[:foo] = 1}.should.raise(FrozenError)
+ -> {a[:foo, :bar] = 1}.should.raise(FrozenError)
+ end
+
+ it "sets elements in the range arguments when passed ranges" do
+ ary = [1, 2, 3]
+ rhs = [nil, [], ["x"], ["x", "y"]]
+ (0 .. ary.size + 2).each do |a|
+ (a .. ary.size + 3).each do |b|
+ rhs.each do |c|
+ ary1 = ary.dup
+ ary1[a .. b] = c
+ ary2 = ary.dup
+ ary2[a, 1 + b-a] = c
+ ary1.should == ary2
+
+ ary1 = ary.dup
+ ary1[a ... b] = c
+ ary2 = ary.dup
+ ary2[a, b-a] = c
+ ary1.should == ary2
+ end
+ end
+ end
+ end
+
+ it "inserts the given elements with [range] which the range is zero-width" do
+ ary = [1, 2, 3]
+ ary[1...1] = 0
+ ary.should == [1, 0, 2, 3]
+ ary[1...1] = [5]
+ ary.should == [1, 5, 0, 2, 3]
+ ary[1...1] = :a, :b, :c
+ ary.should == [1, :a, :b, :c, 5, 0, 2, 3]
+ end
+
+ it "inserts the given elements with [start, length] which length is zero" do
+ ary = [1, 2, 3]
+ ary[1, 0] = 0
+ ary.should == [1, 0, 2, 3]
+ ary[1, 0] = [5]
+ ary.should == [1, 5, 0, 2, 3]
+ ary[1, 0] = :a, :b, :c
+ ary.should == [1, :a, :b, :c, 5, 0, 2, 3]
+ end
+
+ # Now we only have to test cases where the start, length interface would
+ # have raise an exception because of negative size
+ it "inserts the given elements with [range] which the range has negative width" do
+ ary = [1, 2, 3]
+ ary[1..0] = 0
+ ary.should == [1, 0, 2, 3]
+ ary[1..0] = [4, 3]
+ ary.should == [1, 4, 3, 0, 2, 3]
+ ary[1..0] = :a, :b, :c
+ ary.should == [1, :a, :b, :c, 4, 3, 0, 2, 3]
+ end
+
+ it "just inserts nil if the section defined by range is zero-width and the rhs is nil" do
+ ary = [1, 2, 3]
+ ary[1...1] = nil
+ ary.should == [1, nil, 2, 3]
+ end
+
+ it "just inserts nil if the section defined by range has negative width and the rhs is nil" do
+ ary = [1, 2, 3]
+ ary[1..0] = nil
+ ary.should == [1, nil, 2, 3]
+ end
+
+ it "does nothing if the section defined by range is zero-width and the rhs is an empty array" do
+ ary = [1, 2, 3]
+ ary[1...1] = []
+ ary.should == [1, 2, 3]
+ end
+
+ it "does nothing if the section defined by range has negative width and the rhs is an empty array" do
+ ary = [1, 2, 3, 4, 5]
+ ary[1...0] = []
+ ary.should == [1, 2, 3, 4, 5]
+ ary[-2..2] = []
+ ary.should == [1, 2, 3, 4, 5]
+ end
+
+ it "tries to convert Range elements to Integers using #to_int with [m..n] and [m...n]" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ def from.to_int() 1 end
+ def to.to_int() -2 end
+
+ a = [1, 2, 3, 4]
+
+ a[from .. to] = ["a", "b", "c"]
+ a.should == [1, "a", "b", "c", 4]
+
+ a[to .. from] = ["x"]
+ a.should == [1, "a", "b", "x", "c", 4]
+ -> { a["a" .. "b"] = [] }.should.raise(TypeError)
+ -> { a[from .. "b"] = [] }.should.raise(TypeError)
+ end
+
+ it "raises an IndexError when passed indexes out of bounds" do
+ a = [1, 2, 3, 4]
+ -> { a[-5] = "" }.should.raise(IndexError)
+ -> { a[-5, -1] = "" }.should.raise(IndexError)
+ -> { a[-5, 0] = "" }.should.raise(IndexError)
+ -> { a[-5, 1] = "" }.should.raise(IndexError)
+ -> { a[-5, 2] = "" }.should.raise(IndexError)
+ -> { a[-5, 10] = "" }.should.raise(IndexError)
+
+ -> { a[-5..-5] = "" }.should.raise(RangeError)
+ -> { a[-5...-5] = "" }.should.raise(RangeError)
+ -> { a[-5..-4] = "" }.should.raise(RangeError)
+ -> { a[-5...-4] = "" }.should.raise(RangeError)
+ -> { a[-5..10] = "" }.should.raise(RangeError)
+ -> { a[-5...10] = "" }.should.raise(RangeError)
+
+ # ok
+ a[0..-9] = [1]
+ a.should == [1, 1, 2, 3, 4]
+ end
+
+ it "calls to_ary on its rhs argument for multi-element sets" do
+ obj = mock('to_ary')
+ def obj.to_ary() [1, 2, 3] end
+ ary = [1, 2]
+ ary[0, 0] = obj
+ ary.should == [1, 2, 3, 1, 2]
+ ary[1, 10] = obj
+ ary.should == [1, 1, 2, 3]
+ end
+
+ it "does not call to_ary on rhs array subclasses for multi-element sets" do
+ ary = []
+ ary[0, 0] = ArraySpecs::ToAryArray[5, 6, 7]
+ ary.should == [5, 6, 7]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array[0, 0] = [] }.should.raise(FrozenError)
+ end
+end
+
+describe "Array#[]= with [index]" do
+ it "returns value assigned if idx is inside array" do
+ a = [1, 2, 3, 4, 5]
+ (a[3] = 6).should == 6
+ end
+
+ it "returns value assigned if idx is right beyond right array boundary" do
+ a = [1, 2, 3, 4, 5]
+ (a[5] = 6).should == 6
+ end
+
+ it "returns value assigned if idx far beyond right array boundary" do
+ a = [1, 2, 3, 4, 5]
+ (a[10] = 6).should == 6
+ end
+
+ it "sets the value of the element at index" do
+ a = [1, 2, 3, 4]
+ a[2] = 5
+ a[-1] = 6
+ a[5] = 3
+ a.should == [1, 2, 5, 6, nil, 3]
+ end
+
+ it "sets the value of the element if it is right beyond the array boundary" do
+ a = [1, 2, 3, 4]
+ a[4] = 8
+ a.should == [1, 2, 3, 4, 8]
+ end
+
+end
+
+describe "Array#[]= with [index, count]" do
+ it "returns non-array value if non-array value assigned" do
+ a = [1, 2, 3, 4, 5]
+ (a[2, 3] = 10).should == 10
+ end
+
+ it "returns array if array assigned" do
+ a = [1, 2, 3, 4, 5]
+ (a[2, 3] = [4, 5]).should == [4, 5]
+ end
+
+ it "accepts a frozen String literal as RHS" do
+ a = ['a', 'b', 'c']
+ a[0, 2] = 'd'.freeze
+ a.should == ['d', 'c']
+ end
+
+ it "just sets the section defined by [start,length] to nil even if the rhs is nil" do
+ a = ['a', 'b', 'c', 'd', 'e']
+ a[1, 3] = nil
+ a.should == ["a", nil, "e"]
+ end
+
+ it "just sets the section defined by [start,length] to nil if negative index within bounds, cnt > 0 and the rhs is nil" do
+ a = ['a', 'b', 'c', 'd', 'e']
+ a[-3, 2] = nil
+ a.should == ["a", "b", nil, "e"]
+ end
+
+ it "replaces the section defined by [start,length] to other" do
+ a = [1, 2, 3, 4, 5, 6]
+ a[0, 1] = 2
+ a[3, 2] = ['a', 'b', 'c', 'd']
+ a.should == [2, 2, 3, "a", "b", "c", "d", 6]
+ end
+
+ it "replaces the section to other if idx < 0 and cnt > 0" do
+ a = [1, 2, 3, 4, 5, 6]
+ a[-3, 2] = ["x", "y", "z"]
+ a.should == [1, 2, 3, "x", "y", "z", 6]
+ end
+
+ it "replaces the section to other even if cnt spanning beyond the array boundary" do
+ a = [1, 2, 3, 4, 5]
+ a[-1, 3] = [7, 8]
+ a.should == [1, 2, 3, 4, 7, 8]
+ end
+
+ it "pads the Array with nils if the span is past the end" do
+ a = [1, 2, 3, 4, 5]
+ a[10, 1] = [1]
+ a.should == [1, 2, 3, 4, 5, nil, nil, nil, nil, nil, 1]
+
+ b = [1, 2, 3, 4, 5]
+ b[10, 0] = [1]
+ a.should == [1, 2, 3, 4, 5, nil, nil, nil, nil, nil, 1]
+
+ c = [1, 2, 3, 4, 5]
+ c[10, 0] = []
+ c.should == [1, 2, 3, 4, 5, nil, nil, nil, nil, nil]
+ end
+
+ it "inserts other section in place defined by idx" do
+ a = [1, 2, 3, 4, 5]
+ a[3, 0] = [7, 8]
+ a.should == [1, 2, 3, 7, 8, 4, 5]
+
+ b = [1, 2, 3, 4, 5]
+ b[1, 0] = b
+ b.should == [1, 1, 2, 3, 4, 5, 2, 3, 4, 5]
+ end
+
+ it "raises an IndexError when passed start and negative length" do
+ a = [1, 2, 3, 4]
+ -> { a[-2, -1] = "" }.should.raise(IndexError)
+ -> { a[0, -1] = "" }.should.raise(IndexError)
+ -> { a[2, -1] = "" }.should.raise(IndexError)
+ -> { a[4, -1] = "" }.should.raise(IndexError)
+ -> { a[10, -1] = "" }.should.raise(IndexError)
+ -> { [1, 2, 3, 4, 5][2, -1] = [7, 8] }.should.raise(IndexError)
+ end
+end
+
+describe "Array#[]= with [m..n]" do
+ it "returns non-array value if non-array value assigned" do
+ a = [1, 2, 3, 4, 5]
+ (a[2..4] = 10).should == 10
+ (a.[]=(2..4, 10)).should == 10
+ end
+
+ it "returns array if array assigned" do
+ a = [1, 2, 3, 4, 5]
+ (a[2..4] = [7, 8]).should == [7, 8]
+ (a.[]=(2..4, [7, 8])).should == [7, 8]
+ end
+
+ it "just sets the section defined by range to nil even if the rhs is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[0..1] = nil
+ a.should == [nil, 3, 4, 5]
+ end
+
+ it "just sets the section defined by range to nil if m and n < 0 and the rhs is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[-3..-2] = nil
+ a.should == [1, 2, nil, 5]
+ end
+
+ it "replaces the section defined by range" do
+ a = [6, 5, 4, 3, 2, 1]
+ a[1...2] = 9
+ a[3..6] = [6, 6, 6]
+ a.should == [6, 9, 4, 6, 6, 6]
+ end
+
+ it "replaces the section if m and n < 0" do
+ a = [1, 2, 3, 4, 5]
+ a[-3..-2] = [7, 8, 9]
+ a.should == [1, 2, 7, 8, 9, 5]
+ end
+
+ it "replaces the section if m < 0 and n > 0" do
+ a = [1, 2, 3, 4, 5]
+ a[-4..3] = [8]
+ a.should == [1, 8, 5]
+ end
+
+ it "inserts the other section at m if m > n" do
+ a = [1, 2, 3, 4, 5]
+ a[3..1] = [8]
+ a.should == [1, 2, 3, 8, 4, 5]
+ end
+
+ it "inserts at the end if m > the array size" do
+ a = [1, 2, 3]
+ a[3..3] = [4]
+ a.should == [1, 2, 3, 4]
+ a[5..7] = [6]
+ a.should == [1, 2, 3, 4, nil, 6]
+ end
+
+ describe "Range subclasses" do
+ before :each do
+ @range_incl = ArraySpecs::MyRange.new(1, 2)
+ @range_excl = ArraySpecs::MyRange.new(-3, -1, true)
+ end
+
+ it "accepts Range subclasses" do
+ a = [1, 2, 3, 4]
+
+ a[@range_incl] = ["a", "b"]
+ a.should == [1, "a", "b", 4]
+ a[@range_excl] = ["A", "B"]
+ a.should == [1, "A", "B", 4]
+ end
+
+ it "returns non-array value if non-array value assigned" do
+ a = [1, 2, 3, 4, 5]
+ (a[@range_incl] = 10).should == 10
+ (a.[]=(@range_incl, 10)).should == 10
+ end
+
+ it "returns array if array assigned" do
+ a = [1, 2, 3, 4, 5]
+ (a[@range_incl] = [7, 8]).should == [7, 8]
+ a.[]=(@range_incl, [7, 8]).should == [7, 8]
+ end
+ end
+end
+
+describe "Array#[]= with [m..]" do
+ it "just sets the section defined by range to nil even if the rhs is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[eval("(2..)")] = nil
+ a.should == [1, 2, nil]
+ end
+
+ it "just sets the section defined by range to nil if m and n < 0 and the rhs is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[eval("(-3..)")] = nil
+ a.should == [1, 2, nil]
+ end
+
+ it "replaces the section defined by range" do
+ a = [6, 5, 4, 3, 2, 1]
+ a[eval("(3...)")] = 9
+ a.should == [6, 5, 4, 9]
+ a[eval("(2..)")] = [7, 7, 7]
+ a.should == [6, 5, 7, 7, 7]
+ end
+
+ it "replaces the section if m and n < 0" do
+ a = [1, 2, 3, 4, 5]
+ a[eval("(-3..)")] = [7, 8, 9]
+ a.should == [1, 2, 7, 8, 9]
+ end
+
+ it "inserts at the end if m > the array size" do
+ a = [1, 2, 3]
+ a[eval("(3..)")] = [4]
+ a.should == [1, 2, 3, 4]
+ a[eval("(5..)")] = [6]
+ a.should == [1, 2, 3, 4, nil, 6]
+ end
+end
+
+describe "Array#[]= with [..n] and [...n]" do
+ it "just sets the section defined by range to nil even if the rhs is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[(..2)] = nil
+ a.should == [nil, 4, 5]
+ a[(...2)] = nil
+ a.should == [nil, 5]
+ end
+
+ it "just sets the section defined by range to nil if n < 0 and the rhs is nil" do
+ a = [1, 2, 3, 4, 5]
+ a[(..-3)] = nil
+ a.should == [nil, 4, 5]
+ a[(...-1)] = [nil, 5]
+ end
+
+ it "replaces the section defined by range" do
+ a = [6, 5, 4, 3, 2, 1]
+ a[(...3)] = 9
+ a.should == [9, 3, 2, 1]
+ a[(..2)] = [7, 7, 7, 7, 7]
+ a.should == [7, 7, 7, 7, 7, 1]
+ end
+
+ it "replaces the section if n < 0" do
+ a = [1, 2, 3, 4, 5]
+ a[(..-2)] = [7, 8, 9]
+ a.should == [7, 8, 9, 5]
+ end
+
+ it "replaces everything if n > the array size" do
+ a = [1, 2, 3]
+ a[(...7)] = [4]
+ a.should == [4]
+ end
+
+ it "inserts at the beginning if n < negative the array size" do
+ a = [1, 2, 3]
+ a[(..-7)] = [4]
+ a.should == [4, 1, 2, 3]
+ a[(...-10)] = [6]
+ a.should == [6, 4, 1, 2, 3]
+ end
+end
+
+describe "Array#[] after a shift" do
+ it "works for insertion" do
+ a = [1,2]
+ a.shift
+ a.shift
+ a[0,0] = [3,4]
+ a.should == [3,4]
+ end
+end
diff --git a/spec/ruby/core/array/empty_spec.rb b/spec/ruby/core/array/empty_spec.rb
new file mode 100644
index 0000000000..f70b1b6ebe
--- /dev/null
+++ b/spec/ruby/core/array/empty_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#empty?" do
+ it "returns true if the array has no elements" do
+ [].should.empty?
+ [1].should_not.empty?
+ [1, 2].should_not.empty?
+ end
+end
diff --git a/spec/ruby/core/array/eql_spec.rb b/spec/ruby/core/array/eql_spec.rb
new file mode 100644
index 0000000000..9a7447c2e8
--- /dev/null
+++ b/spec/ruby/core/array/eql_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/eql'
+
+describe "Array#eql?" do
+ it_behaves_like :array_eql, :eql?
+
+ it "returns false if any corresponding elements are not #eql?" do
+ [1, 2, 3, 4].should_not.eql?([1, 2, 3, 4.0])
+ end
+
+ it "returns false if other is not a kind of Array" do
+ obj = mock("array eql?")
+ obj.should_not_receive(:to_ary)
+ obj.should_not_receive(:eql?)
+
+ [1, 2, 3].should_not.eql?(obj)
+ end
+end
diff --git a/spec/ruby/core/array/equal_value_spec.rb b/spec/ruby/core/array/equal_value_spec.rb
new file mode 100644
index 0000000000..4f7c0ce5e3
--- /dev/null
+++ b/spec/ruby/core/array/equal_value_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/eql'
+
+describe "Array#==" do
+ it_behaves_like :array_eql, :==
+
+ it "compares with an equivalent Array-like object using #to_ary" do
+ obj = mock('array-like')
+ obj.should_receive(:respond_to?).at_least(1).with(:to_ary).and_return(true)
+ obj.should_receive(:==).with([1]).at_least(1).and_return(true)
+
+ ([1] == obj).should == true
+ ([[1]] == [obj]).should == true
+ ([[[1], 3], 2] == [[obj, 3], 2]).should == true
+
+ # recursive arrays
+ arr1 = [[1]]
+ arr1 << arr1
+ arr2 = [obj]
+ arr2 << arr2
+ (arr1 == arr2).should == true
+ (arr2 == arr1).should == true
+ end
+
+ it "returns false if any corresponding elements are not #==" do
+ a = ["a", "b", "c"]
+ b = ["a", "b", "not equal value"]
+ a.should_not == b
+
+ c = mock("c")
+ c.should_receive(:==).and_return(false)
+ ["a", "b", c].should_not == a
+ end
+
+ it "returns true if corresponding elements are #==" do
+ [].should == []
+ ["a", "c", 7].should == ["a", "c", 7]
+
+ [1, 2, 3].should == [1.0, 2.0, 3.0]
+
+ obj = mock('5')
+ obj.should_receive(:==).and_return(true)
+ [obj].should == [5]
+ end
+
+ # See https://bugs.ruby-lang.org/issues/1720
+ it "returns true for [NaN] == [NaN] because Array#== first checks with #equal? and NaN.equal?(NaN) is true" do
+ [Float::NAN].should == [Float::NAN]
+ end
+end
diff --git a/spec/ruby/core/array/fetch_spec.rb b/spec/ruby/core/array/fetch_spec.rb
new file mode 100644
index 0000000000..ee94cfcbb2
--- /dev/null
+++ b/spec/ruby/core/array/fetch_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#fetch" do
+ it "returns the element at the passed index" do
+ [1, 2, 3].fetch(1).should == 2
+ [nil].fetch(0).should == nil
+ end
+
+ it "counts negative indices backwards from end" do
+ [1, 2, 3, 4].fetch(-1).should == 4
+ end
+
+ it "raises an IndexError if there is no element at index" do
+ -> { [1, 2, 3].fetch(3) }.should.raise(IndexError, "index 3 outside of array bounds: -3...3")
+ -> { [1, 2, 3].fetch(-4) }.should.raise(IndexError, "index -4 outside of array bounds: -3...3")
+ -> { [].fetch(0) }.should.raise(IndexError, "index 0 outside of array bounds: 0...0")
+ end
+
+ it "returns default if there is no element at index if passed a default value" do
+ [1, 2, 3].fetch(5, :not_found).should == :not_found
+ [1, 2, 3].fetch(5, nil).should == nil
+ [1, 2, 3].fetch(-4, :not_found).should == :not_found
+ [nil].fetch(0, :not_found).should == nil
+ end
+
+ it "returns the value of block if there is no element at index if passed a block" do
+ [1, 2, 3].fetch(9) { |i| i * i }.should == 81
+ [1, 2, 3].fetch(-9) { |i| i * i }.should == 81
+ end
+
+ it "passes the original index argument object to the block, not the converted Integer" do
+ o = mock('5')
+ def o.to_int(); 5; end
+
+ [1, 2, 3].fetch(o) { |i| i }.should.equal?(o)
+ end
+
+ it "gives precedence to the default block over the default argument" do
+ -> {
+ @result = [1, 2, 3].fetch(9, :foo) { |i| i * i }
+ }.should complain(/block supersedes default value argument/)
+ @result.should == 81
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ ["a", "b", "c"].fetch(obj).should == "c"
+ end
+
+ it "raises a TypeError when the passed argument can't be coerced to Integer" do
+ -> { [].fetch("cat") }.should.raise(TypeError, "no implicit conversion of String into Integer")
+ end
+end
diff --git a/spec/ruby/core/array/fetch_values_spec.rb b/spec/ruby/core/array/fetch_values_spec.rb
new file mode 100644
index 0000000000..237d6ad7f7
--- /dev/null
+++ b/spec/ruby/core/array/fetch_values_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#fetch_values" do
+ before :each do
+ @array = [:a, :b, :c]
+ end
+
+ ruby_version_is "3.4" do
+ describe "with matched indexes" do
+ it "returns the values for indexes" do
+ @array.fetch_values(0).should == [:a]
+ @array.fetch_values(0, 2).should == [:a, :c]
+ @array.fetch_values(-1).should == [:c]
+ end
+
+ it "returns the values for indexes ordered in the order of the requested indexes" do
+ @array.fetch_values(2, 0).should == [:c, :a]
+ end
+ end
+
+ describe "with unmatched indexes" do
+ it "raises a index error if no block is provided" do
+ -> { @array.fetch_values(0, 1, 44) }.should.raise(IndexError, "index 44 outside of array bounds: -3...3")
+ end
+
+ it "returns the default value from block" do
+ @array.fetch_values(44) { |index| "`#{index}' is not found" }.should == ["`44' is not found"]
+ @array.fetch_values(0, 44) { |index| "`#{index}' is not found" }.should == [:a, "`44' is not found"]
+ end
+ end
+
+ describe "without keys" do
+ it "returns an empty Array" do
+ @array.fetch_values.should == []
+ end
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ @array.fetch_values(obj).should == [:c]
+ end
+
+ it "does not support a Range object as argument" do
+ -> {
+ @array.fetch_values(1..2)
+ }.should.raise(TypeError, "no implicit conversion of Range into Integer")
+ end
+
+ it "raises a TypeError when the passed argument can't be coerced to Integer" do
+ -> { [].fetch_values("cat") }.should.raise(TypeError, "no implicit conversion of String into Integer")
+ end
+ end
+end
diff --git a/spec/ruby/core/array/fill_spec.rb b/spec/ruby/core/array/fill_spec.rb
new file mode 100644
index 0000000000..e4d51bd998
--- /dev/null
+++ b/spec/ruby/core/array/fill_spec.rb
@@ -0,0 +1,374 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#fill" do
+ before :all do
+ @never_passed = -> i do
+ raise ExpectationNotMetError, "the control path should not pass here"
+ end
+ end
+
+ it "returns self" do
+ ary = [1, 2, 3]
+ ary.fill(:a).should.equal?(ary)
+ end
+
+ it "is destructive" do
+ ary = [1, 2, 3]
+ ary.fill(:a)
+ ary.should == [:a, :a, :a]
+ end
+
+ it "does not replicate the filler" do
+ ary = [1, 2, 3, 4]
+ str = +"x"
+ ary.fill(str).should == [str, str, str, str]
+ str << "y"
+ ary.should == [str, str, str, str]
+ ary[0].should.equal?(str)
+ ary[1].should.equal?(str)
+ ary[2].should.equal?(str)
+ ary[3].should.equal?(str)
+ end
+
+ it "replaces all elements in the array with the filler if not given a index nor a length" do
+ ary = ['a', 'b', 'c', 'duh']
+ ary.fill(8).should == [8, 8, 8, 8]
+
+ str = "x"
+ ary.fill(str).should == [str, str, str, str]
+ end
+
+ it "replaces all elements with the value of block (index given to block)" do
+ [nil, nil, nil, nil].fill { |i| i * 2 }.should == [0, 2, 4, 6]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.fill('x') }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on an empty frozen array" do
+ -> { ArraySpecs.empty_frozen_array.fill('x') }.should.raise(FrozenError)
+ end
+
+ it "raises an ArgumentError if 4 or more arguments are passed when no block given" do
+ [].fill('a').should == []
+ [].fill('a', 1).should == []
+ [].fill('a', 1, 2).should == [nil, 'a', 'a']
+ -> { [].fill('a', 1, 2, true) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if no argument passed and no block given" do
+ -> { [].fill }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if 3 or more arguments are passed when a block given" do
+ [].fill() {|i|}.should == []
+ [].fill(1) {|i|}.should == []
+ [].fill(1, 2) {|i|}.should == [nil, nil, nil]
+ -> { [].fill(1, 2, true) {|i|} }.should.raise(ArgumentError)
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.fill { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "only changes elements before error is raised, keeping the element which raised an error." do
+ a = [1, 2, 3, 4]
+ begin
+ a.fill do |i|
+ case i
+ when 0 then -1
+ when 1 then -2
+ when 2 then raise StandardError, 'Oops'
+ else 0
+ end
+ end
+ rescue StandardError
+ end
+
+ a.should == [-1, -2, 3, 4]
+ end
+
+ it "tolerates increasing an array size during iteration" do
+ array = [:a, :b, :c]
+ ScratchPad.record []
+ i = 0
+
+ array.fill do |index|
+ ScratchPad << index
+ array << i if i < 100
+ i++
+ index
+ end
+
+ ScratchPad.recorded.should == [0, 1, 2]
+ end
+end
+
+describe "Array#fill with (filler, index, length)" do
+ it "replaces length elements beginning with the index with the filler if given an index and a length" do
+ ary = [1, 2, 3, 4, 5, 6]
+ ary.fill('x', 2, 3).should == [1, 2, 'x', 'x', 'x', 6]
+ end
+
+ it "replaces length elements beginning with the index with the value of block" do
+ [true, false, true, false, true, false, true].fill(1, 4) { |i| i + 3 }.should == [true, 4, 5, 6, 7, false, true]
+ end
+
+ it "replaces all elements after the index if given an index and no length" do
+ ary = [1, 2, 3]
+ ary.fill('x', 1).should == [1, 'x', 'x']
+ ary.fill(1){|i| i*2}.should == [1, 2, 4]
+ end
+
+ it "replaces all elements after the index if given an index and nil as a length" do
+ a = [1, 2, 3]
+ a.fill('x', 1, nil).should == [1, 'x', 'x']
+ a.fill(1, nil){|i| i*2}.should == [1, 2, 4]
+ a.fill('y', nil).should == ['y', 'y', 'y']
+ end
+
+ it "replaces the last (-n) elements if given an index n which is negative and no length" do
+ a = [1, 2, 3, 4, 5]
+ a.fill('x', -2).should == [1, 2, 3, 'x', 'x']
+ a.fill(-2){|i| i.to_s}.should == [1, 2, 3, '3', '4']
+ end
+
+ it "replaces the last (-n) elements if given an index n which is negative and nil as a length" do
+ a = [1, 2, 3, 4, 5]
+ a.fill('x', -2, nil).should == [1, 2, 3, 'x', 'x']
+ a.fill(-2, nil){|i| i.to_s}.should == [1, 2, 3, '3', '4']
+ end
+
+ it "makes no modifications if given an index greater than end and no length" do
+ [1, 2, 3, 4, 5].fill('a', 5).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill(5, &@never_passed).should == [1, 2, 3, 4, 5]
+ end
+
+ it "makes no modifications if given an index greater than end and nil as a length" do
+ [1, 2, 3, 4, 5].fill('a', 5, nil).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill(5, nil, &@never_passed).should == [1, 2, 3, 4, 5]
+ end
+
+ it "replaces length elements beginning with start index if given an index >= 0 and a length >= 0" do
+ [1, 2, 3, 4, 5].fill('a', 2, 0).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill('a', 2, 2).should == [1, 2, "a", "a", 5]
+
+ [1, 2, 3, 4, 5].fill(2, 0, &@never_passed).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill(2, 2){|i| i*2}.should == [1, 2, 4, 6, 5]
+ end
+
+ it "increases the Array size when necessary" do
+ a = [1, 2, 3]
+ a.size.should == 3
+ a.fill 'a', 0, 10
+ a.size.should == 10
+ end
+
+ it "pads between the last element and the index with nil if given an index which is greater than size of the array" do
+ [1, 2, 3, 4, 5].fill('a', 8, 5).should == [1, 2, 3, 4, 5, nil, nil, nil, 'a', 'a', 'a', 'a', 'a']
+ [1, 2, 3, 4, 5].fill(8, 5){|i| 'a'}.should == [1, 2, 3, 4, 5, nil, nil, nil, 'a', 'a', 'a', 'a', 'a']
+ end
+
+ it "replaces length elements beginning with the (-n)th if given an index n < 0 and a length > 0" do
+ [1, 2, 3, 4, 5].fill('a', -2, 2).should == [1, 2, 3, "a", "a"]
+ [1, 2, 3, 4, 5].fill('a', -2, 4).should == [1, 2, 3, "a", "a", "a", "a"]
+
+ [1, 2, 3, 4, 5].fill(-2, 2){|i| 'a'}.should == [1, 2, 3, "a", "a"]
+ [1, 2, 3, 4, 5].fill(-2, 4){|i| 'a'}.should == [1, 2, 3, "a", "a", "a", "a"]
+ end
+
+ it "starts at 0 if the negative index is before the start of the array" do
+ [1, 2, 3, 4, 5].fill('a', -25, 3).should == ['a', 'a', 'a', 4, 5]
+ [1, 2, 3, 4, 5].fill('a', -10, 10).should == %w|a a a a a a a a a a|
+
+ [1, 2, 3, 4, 5].fill(-25, 3){|i| 'a'}.should == ['a', 'a', 'a', 4, 5]
+ [1, 2, 3, 4, 5].fill(-10, 10){|i| 'a'}.should == %w|a a a a a a a a a a|
+ end
+
+ it "makes no modifications if the given length <= 0" do
+ [1, 2, 3, 4, 5].fill('a', 2, 0).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill('a', -2, 0).should == [1, 2, 3, 4, 5]
+
+ [1, 2, 3, 4, 5].fill('a', 2, -2).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill('a', -2, -2).should == [1, 2, 3, 4, 5]
+
+ [1, 2, 3, 4, 5].fill(2, 0, &@never_passed).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill(-2, 0, &@never_passed).should == [1, 2, 3, 4, 5]
+
+ [1, 2, 3, 4, 5].fill(2, -2, &@never_passed).should == [1, 2, 3, 4, 5]
+ [1, 2, 3, 4, 5].fill(-2, -2, &@never_passed).should == [1, 2, 3, 4, 5]
+ end
+
+ # See: https://blade.ruby-lang.org/ruby-core/17481
+ it "does not raise an exception if the given length is negative and its absolute value does not exceed the index" do
+ [1, 2, 3, 4].fill('a', 3, -1).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill('a', 3, -2).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill('a', 3, -3).should == [1, 2, 3, 4]
+
+ [1, 2, 3, 4].fill(3, -1, &@never_passed).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill(3, -2, &@never_passed).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill(3, -3, &@never_passed).should == [1, 2, 3, 4]
+ end
+
+ it "does not raise an exception even if the given length is negative and its absolute value exceeds the index" do
+ [1, 2, 3, 4].fill('a', 3, -4).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill('a', 3, -5).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill('a', 3, -10000).should == [1, 2, 3, 4]
+
+ [1, 2, 3, 4].fill(3, -4, &@never_passed).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill(3, -5, &@never_passed).should == [1, 2, 3, 4]
+ [1, 2, 3, 4].fill(3, -10000, &@never_passed).should == [1, 2, 3, 4]
+ end
+
+ it "tries to convert the second and third arguments to Integers using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2, 2)
+ filler = mock('filler')
+ filler.should_not_receive(:to_int)
+ [1, 2, 3, 4, 5].fill(filler, obj, obj).should == [1, 2, filler, filler, 5]
+ end
+
+ it "raises a TypeError if the index is not numeric" do
+ -> { [].fill 'a', true }.should.raise(TypeError)
+
+ obj = mock('nonnumeric')
+ -> { [].fill('a', obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the length is not numeric" do
+ -> { [1, 2, 3].fill("x", 1, "foo") }.should.raise(TypeError, /no implicit conversion of String into Integer/)
+ -> { [1, 2, 3].fill("x", 1, :"foo") }.should.raise(TypeError, /no implicit conversion of Symbol into Integer/)
+ -> { [1, 2, 3].fill("x", 1, Object.new) }.should.raise(TypeError, /no implicit conversion of Object into Integer/)
+ end
+
+ not_supported_on :opal do
+ it "raises an ArgumentError or RangeError for too-large sizes" do
+ error_types = [RangeError, ArgumentError]
+ arr = [1, 2, 3]
+ -> { arr.fill(10, 1, fixnum_max) }.should.raise { |err| error_types.should.include?(err.class) }
+ -> { arr.fill(10, 1, bignum_value) }.should.raise(RangeError)
+ end
+ end
+end
+
+describe "Array#fill with (filler, range)" do
+ it "replaces elements in range with object" do
+ [1, 2, 3, 4, 5, 6].fill(8, 0..3).should == [8, 8, 8, 8, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(8, 0...3).should == [8, 8, 8, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', 4..6).should == [1, 2, 3, 4, 'x', 'x', 'x']
+ [1, 2, 3, 4, 5, 6].fill('x', 4...6).should == [1, 2, 3, 4, 'x', 'x']
+ [1, 2, 3, 4, 5, 6].fill('x', -2..-1).should == [1, 2, 3, 4, 'x', 'x']
+ [1, 2, 3, 4, 5, 6].fill('x', -2...-1).should == [1, 2, 3, 4, 'x', 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -2...-2).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -2..-2).should == [1, 2, 3, 4, 'x', 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -2..0).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', 0...0).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', 1..1).should == [1, 'x', 3, 4, 5, 6]
+ end
+
+ it "replaces all elements in range with the value of block" do
+ [1, 1, 1, 1, 1, 1].fill(1..6) { |i| i + 1 }.should == [1, 2, 3, 4, 5, 6, 7]
+ end
+
+ it "increases the Array size when necessary" do
+ [1, 2, 3].fill('x', 1..6).should == [1, 'x', 'x', 'x', 'x', 'x', 'x']
+ [1, 2, 3].fill(1..6){|i| i+1}.should == [1, 2, 3, 4, 5, 6, 7]
+ end
+
+ it "raises a TypeError with range and length argument" do
+ -> { [].fill('x', 0 .. 2, 5) }.should.raise(TypeError)
+ end
+
+ it "replaces elements between the (-m)th to the last and the (n+1)th from the first if given an range m..n where m < 0 and n >= 0" do
+ [1, 2, 3, 4, 5, 6].fill('x', -4..4).should == [1, 2, 'x', 'x', 'x', 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -4...4).should == [1, 2, 'x', 'x', 5, 6]
+
+ [1, 2, 3, 4, 5, 6].fill(-4..4){|i| (i+1).to_s}.should == [1, 2, '3', '4', '5', 6]
+ [1, 2, 3, 4, 5, 6].fill(-4...4){|i| (i+1).to_s}.should == [1, 2, '3', '4', 5, 6]
+ end
+
+ it "replaces elements between the (-m)th and (-n)th to the last if given an range m..n where m < 0 and n < 0" do
+ [1, 2, 3, 4, 5, 6].fill('x', -4..-2).should == [1, 2, 'x', 'x', 'x', 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -4...-2).should == [1, 2, 'x', 'x', 5, 6]
+
+ [1, 2, 3, 4, 5, 6].fill(-4..-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', '5', 6]
+ [1, 2, 3, 4, 5, 6].fill(-4...-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', 5, 6]
+ end
+
+ it "replaces elements between the (m+1)th from the first and (-n)th to the last if given an range m..n where m >= 0 and n < 0" do
+ [1, 2, 3, 4, 5, 6].fill('x', 2..-2).should == [1, 2, 'x', 'x', 'x', 6]
+ [1, 2, 3, 4, 5, 6].fill('x', 2...-2).should == [1, 2, 'x', 'x', 5, 6]
+
+ [1, 2, 3, 4, 5, 6].fill(2..-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', '5', 6]
+ [1, 2, 3, 4, 5, 6].fill(2...-2){|i| (i+1).to_s}.should == [1, 2, '3', '4', 5, 6]
+ end
+
+ it "makes no modifications if given an range which implies a section of zero width" do
+ [1, 2, 3, 4, 5, 6].fill('x', 2...2).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -4...2).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -4...-4).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', 2...-4).should == [1, 2, 3, 4, 5, 6]
+
+ [1, 2, 3, 4, 5, 6].fill(2...2, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(-4...2, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(-4...-4, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(2...-4, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "makes no modifications if given an range which implies a section of negative width" do
+ [1, 2, 3, 4, 5, 6].fill('x', 2..1).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -4..1).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', -2..-4).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill('x', 2..-5).should == [1, 2, 3, 4, 5, 6]
+
+ [1, 2, 3, 4, 5, 6].fill(2..1, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(-4..1, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(-2..-4, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ [1, 2, 3, 4, 5, 6].fill(2..-5, &@never_passed).should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "raises an exception if some of the given range lies before the first of the array" do
+ -> { [1, 2, 3].fill('x', -5..-3) }.should.raise(RangeError)
+ -> { [1, 2, 3].fill('x', -5...-3) }.should.raise(RangeError)
+ -> { [1, 2, 3].fill('x', -5..-4) }.should.raise(RangeError)
+
+ -> { [1, 2, 3].fill(-5..-3, &@never_passed) }.should.raise(RangeError)
+ -> { [1, 2, 3].fill(-5...-3, &@never_passed) }.should.raise(RangeError)
+ -> { [1, 2, 3].fill(-5..-4, &@never_passed) }.should.raise(RangeError)
+ end
+
+ it "tries to convert the start and end of the passed range to Integers using #to_int" do
+ obj = mock('to_int')
+ def obj.<=>(rhs); rhs == self ? 0 : nil end
+ obj.should_receive(:to_int).twice.and_return(2)
+ filler = mock('filler')
+ filler.should_not_receive(:to_int)
+ [1, 2, 3, 4, 5].fill(filler, obj..obj).should == [1, 2, filler, 4, 5]
+ end
+
+ it "raises a TypeError if the start or end of the passed range is not numeric" do
+ obj = mock('nonnumeric')
+ def obj.<=>(rhs); rhs == self ? 0 : nil end
+ -> { [].fill('a', obj..obj) }.should.raise(TypeError)
+ end
+
+ it "works with endless ranges" do
+ [1, 2, 3, 4].fill('x', eval("(1..)")).should == [1, 'x', 'x', 'x']
+ [1, 2, 3, 4].fill('x', eval("(3...)")).should == [1, 2, 3, 'x']
+ [1, 2, 3, 4].fill(eval("(1..)")) { |x| x + 2 }.should == [1, 3, 4, 5]
+ [1, 2, 3, 4].fill(eval("(3...)")) { |x| x + 2 }.should == [1, 2, 3, 5]
+ end
+
+ it "works with beginless ranges" do
+ [1, 2, 3, 4].fill('x', (..2)).should == ["x", "x", "x", 4]
+ [1, 2, 3, 4].fill((...2)) { |x| x + 2 }.should == [2, 3, 3, 4]
+ end
+end
diff --git a/spec/ruby/core/array/filter_spec.rb b/spec/ruby/core/array/filter_spec.rb
new file mode 100644
index 0000000000..6ebda61069
--- /dev/null
+++ b/spec/ruby/core/array/filter_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Array#filter" do
+ it "is an alias of Array#select" do
+ Array.instance_method(:filter).should == Array.instance_method(:select)
+ end
+end
+
+describe "Array#filter!" do
+ it "is an alias of Array#select!" do
+ Array.instance_method(:filter!).should == Array.instance_method(:select!)
+ end
+end
diff --git a/spec/ruby/core/array/find_index_spec.rb b/spec/ruby/core/array/find_index_spec.rb
new file mode 100644
index 0000000000..17ff6c04a7
--- /dev/null
+++ b/spec/ruby/core/array/find_index_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#find_index" do
+ it "returns the index of the first element == to object" do
+ x = mock('3')
+ def x.==(obj) 3 == obj; end
+
+ [2, x, 3, 1, 3, 1].find_index(3).should == 1
+ [2, 3.0, 3, x, 1, 3, 1].find_index(x).should == 1
+ end
+
+ it "returns 0 if first element == to object" do
+ [2, 1, 3, 2, 5].find_index(2).should == 0
+ end
+
+ it "returns size-1 if only last element == to object" do
+ [2, 1, 3, 1, 5].find_index(5).should == 4
+ end
+
+ it "returns nil if no element == to object" do
+ [2, 1, 1, 1, 1].find_index(3).should == nil
+ end
+
+ it "accepts a block instead of an argument" do
+ [4, 2, 1, 5, 1, 3].find_index {|x| x < 2}.should == 2
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ [4, 2, 1, 5, 1, 3].find_index(5) {|x| x < 2}.should == 3
+ }.should complain(/given block not used/)
+ end
+
+ describe "given no argument and no block" do
+ it "produces an Enumerator" do
+ [].find_index.should.instance_of?(Enumerator)
+ end
+ end
+
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :find_index
+end
diff --git a/spec/ruby/core/array/first_spec.rb b/spec/ruby/core/array/first_spec.rb
new file mode 100644
index 0000000000..2c343ac8f6
--- /dev/null
+++ b/spec/ruby/core/array/first_spec.rb
@@ -0,0 +1,93 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#first" do
+ it "returns the first element" do
+ %w{a b c}.first.should == 'a'
+ [nil].first.should == nil
+ end
+
+ it "returns nil if self is empty" do
+ [].first.should == nil
+ end
+
+ it "returns the first count elements if given a count" do
+ [true, false, true, nil, false].first(2).should == [true, false]
+ end
+
+ it "returns an empty array when passed count on an empty array" do
+ [].first(0).should == []
+ [].first(1).should == []
+ [].first(2).should == []
+ end
+
+ it "returns an empty array when passed count == 0" do
+ [1, 2, 3, 4, 5].first(0).should == []
+ end
+
+ it "returns an array containing the first element when passed count == 1" do
+ [1, 2, 3, 4, 5].first(1).should == [1]
+ end
+
+ it "raises an ArgumentError when count is negative" do
+ -> { [1, 2].first(-1) }.should.raise(ArgumentError)
+ end
+
+ it "raises a RangeError when count is a Bignum" do
+ -> { [].first(bignum_value) }.should.raise(RangeError)
+ end
+
+ it "returns the entire array when count > length" do
+ [1, 2, 3, 4, 5, 9].first(10).should == [1, 2, 3, 4, 5, 9]
+ end
+
+ it "returns an array which is independent to the original when passed count" do
+ ary = [1, 2, 3, 4, 5]
+ ary.first(0).replace([1,2])
+ ary.should == [1, 2, 3, 4, 5]
+ ary.first(1).replace([1,2])
+ ary.should == [1, 2, 3, 4, 5]
+ ary.first(6).replace([1,2])
+ ary.should == [1, 2, 3, 4, 5]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.first.should.equal?(empty)
+
+ ary = ArraySpecs.head_recursive_array
+ ary.first.should.equal?(ary)
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ [1, 2, 3, 4, 5].first(obj).should == [1, 2]
+ end
+
+ it "raises a TypeError if the passed argument is not numeric" do
+ -> { [1,2].first(nil) }.should.raise(TypeError)
+ -> { [1,2].first("a") }.should.raise(TypeError)
+
+ obj = mock("nonnumeric")
+ -> { [1,2].first(obj) }.should.raise(TypeError)
+ end
+
+ it "does not return subclass instance when passed count on Array subclasses" do
+ ArraySpecs::MyArray[].first(0).should.instance_of?(Array)
+ ArraySpecs::MyArray[].first(2).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].first(0).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].first(1).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].first(2).should.instance_of?(Array)
+ end
+
+ it "is not destructive" do
+ a = [1, 2, 3]
+ a.first
+ a.should == [1, 2, 3]
+ a.first(2)
+ a.should == [1, 2, 3]
+ a.first(3)
+ a.should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/array/fixtures/classes.rb b/spec/ruby/core/array/fixtures/classes.rb
new file mode 100644
index 0000000000..05283c0f74
--- /dev/null
+++ b/spec/ruby/core/array/fixtures/classes.rb
@@ -0,0 +1,592 @@
+class Object
+ # This helper is defined here rather than in MSpec because
+ # it is only used in #pack specs.
+ def pack_format(count=nil, repeat=nil)
+ format = instance_variable_get(:@method)
+ format += count.to_s unless format == 'P' || format == 'p'
+ format *= repeat if repeat
+ format.dup # because it may then become tainted
+ end
+end
+
+module ArraySpecs
+ SampleRange = 0..1000
+ SampleCount = 1000
+
+ def self.frozen_array
+ [1,2,3].freeze
+ end
+
+ def self.empty_frozen_array
+ [].freeze
+ end
+
+ def self.recursive_array
+ a = [1, 'two', 3.0]
+ 5.times { a << a }
+ a
+ end
+
+ def self.head_recursive_array
+ a = []
+ 5.times { a << a }
+ a << 1 << 'two' << 3.0
+ a
+ end
+
+ def self.empty_recursive_array
+ a = []
+ a << a
+ a
+ end
+
+ # Chi squared critical values for tests with n degrees of freedom at 99% confidence.
+ # Values obtained from NIST Engineering Statistic Handbook at
+ # https://www.itl.nist.gov/div898/handbook/eda/section3/eda3674.htm
+
+ CHI_SQUARED_CRITICAL_VALUES = [
+ 0,
+ 6.635, 9.210, 11.345, 13.277, 15.086, 16.812, 18.475, 20.090, 21.666, 23.209,
+ 24.725, 26.217, 27.688, 29.141, 30.578, 32.000, 33.409, 34.805, 36.191, 37.566,
+ 38.932, 40.289, 41.638, 42.980, 44.314, 45.642, 46.963, 48.278, 49.588, 50.892,
+ 52.191, 53.486, 54.776, 56.061, 57.342, 58.619, 59.893, 61.162, 62.428, 63.691,
+ 64.950, 66.206, 67.459, 68.710, 69.957, 71.201, 72.443, 73.683, 74.919, 76.154,
+ 77.386, 78.616, 79.843, 81.069, 82.292, 83.513, 84.733, 85.950, 87.166, 88.379,
+ 89.591, 90.802, 92.010, 93.217, 94.422, 95.626, 96.828, 98.028, 99.228, 100.425,
+ 101.621, 102.816, 104.010, 105.202, 106.393, 107.583, 108.771, 109.958, 111.144, 112.329,
+ 113.512, 114.695, 115.876, 117.057, 118.236, 119.414, 120.591, 121.767, 122.942, 124.116,
+ 125.289, 126.462, 127.633, 128.803, 129.973, 131.141, 132.309, 133.476, 134.642, 135.807,
+ ]
+
+ def self.measure_sample_fairness(size, samples, iters)
+ ary = Array.new(size) { |x| x }
+ expected = iters.fdiv size
+ (samples).times do |i|
+ chi_results = []
+ 3.times do
+ counts = Array.new(size, 0)
+ iters.times do
+ x = ary.sample(samples)[i]
+ counts[x] += 1
+ end
+ chi_squared = counts.sum {|count| (count - expected) ** 2} / expected
+ chi_results << chi_squared
+ break if chi_squared <= CHI_SQUARED_CRITICAL_VALUES[size]
+ end
+
+ chi_results.min.should <= CHI_SQUARED_CRITICAL_VALUES[size]
+ end
+ end
+
+ def self.measure_sample_fairness_large_sample_size(size, samples, iters)
+ ary = Array.new(size) { |x| x }
+ counts = Array.new(size, 0)
+ expected = (iters * samples).fdiv size
+ iters.times do
+ ary.sample(samples).each do |sample|
+ counts[sample] += 1
+ end
+ end
+ chi_squared = counts.sum {|count| (count - expected) ** 2} / expected
+
+ # Chi squared critical values for tests with 4 degrees of freedom
+ # Values obtained from NIST Engineering Statistic Handbook at
+ # https://www.itl.nist.gov/div898/handbook/eda/section3/eda3674.htm
+
+ chi_squared.should <= CHI_SQUARED_CRITICAL_VALUES[size]
+ end
+
+ class MyArray < Array
+ # The #initialize method has a different signature than Array to help
+ # catch places in the specs that do not assert the #initialize is not
+ # called when Array methods make new instances.
+ def initialize(a, b)
+ self << a << b
+ ScratchPad.record :my_array_initialize
+ end
+ end
+
+ class Sexp < Array
+ def initialize(*args)
+ super(args)
+ end
+ end
+
+ # TODO: replace specs that use this with #should_not_receive(:to_ary)
+ # expectations on regular objects (e.g. Array instances).
+ class ToAryArray < Array
+ def to_ary() ["to_ary", "was", "called!"] end
+ end
+
+ class MyRange < Range; end
+
+ class AssocKey
+ def ==(other); other == 'it'; end
+ end
+
+ class D
+ def <=>(obj)
+ return 4 <=> obj unless obj.class == D
+ 0
+ end
+ end
+
+ class SubArray < Array
+ def initialize(*args)
+ ScratchPad.record args
+ end
+ end
+
+ class ArrayConvertible
+ attr_accessor :called
+ def initialize(*values, &block)
+ @values = values;
+ end
+
+ def to_a
+ self.called = :to_a
+ @values
+ end
+
+ def to_ary
+ self.called = :to_ary
+ @values
+ end
+ end
+
+ class ArrayMethodMissing
+ def initialize(*values, &block)
+ @values = values;
+ end
+
+ def method_missing(name, *args)
+ @values
+ end
+ end
+
+ class SortSame
+ def <=>(other); 0; end
+ def ==(other); true; end
+ end
+
+ class UFOSceptic
+ def <=>(other); raise "N-uh, UFO:s do not exist!"; end
+ end
+
+ class MockForCompared
+ @@count = 0
+ @@compared = false
+ def initialize
+ @@compared = false
+ @order = (@@count += 1)
+ end
+ def <=>(rhs)
+ @@compared = true
+ return rhs.order <=> self.order
+ end
+ def self.compared?
+ @@compared
+ end
+
+ protected
+ attr_accessor :order
+ end
+
+ class ComparableWithInteger
+ include Comparable
+ def initialize(num)
+ @num = num
+ end
+
+ def <=>(fixnum)
+ @num <=> fixnum
+ end
+ end
+
+ class Uncomparable
+ def <=>(obj)
+ nil
+ end
+ end
+
+ def self.universal_pack_object
+ obj = mock("string float int".freeze)
+ obj.stub!(:to_int).and_return(1)
+ obj.stub!(:to_str).and_return("1")
+ obj.stub!(:to_f).and_return(1.0)
+ obj
+ end
+
+ LargeArray = [
+ "test_create_table_with_force_true_does_not_drop_nonexisting_table",
+ "test_add_table",
+ "assert_difference",
+ "assert_operator",
+ "instance_variables",
+ "class",
+ "instance_variable_get",
+ "__class__",
+ "expects",
+ "assert_no_difference",
+ "name",
+ "assert_blank",
+ "assert_not_same",
+ "is_a?",
+ "test_add_table_with_decimals",
+ "test_create_table_with_timestamps_should_create_datetime_columns",
+ "assert_present",
+ "assert_no_match",
+ "__instance_of__",
+ "assert_deprecated",
+ "assert",
+ "assert_throws",
+ "kind_of?",
+ "try",
+ "__instance_variable_get__",
+ "object_id",
+ "timeout",
+ "instance_variable_set",
+ "assert_nothing_thrown",
+ "__instance_variable_set__",
+ "copy_object",
+ "test_create_table_with_timestamps_should_create_datetime_columns_with_options",
+ "assert_not_deprecated",
+ "assert_in_delta",
+ "id",
+ "copy_metaclass",
+ "test_create_table_without_a_block",
+ "dup",
+ "assert_not_nil",
+ "send",
+ "__instance_variables__",
+ "to_sql",
+ "mock",
+ "assert_send",
+ "instance_variable_defined?",
+ "clone",
+ "require",
+ "test_migrator",
+ "__instance_variable_defined_eh__",
+ "frozen?",
+ "test_add_column_not_null_with_default",
+ "freeze",
+ "test_migrator_one_up",
+ "test_migrator_one_down",
+ "singleton_methods",
+ "method_exists?",
+ "create_fixtures",
+ "test_migrator_one_up_one_down",
+ "test_native_decimal_insert_manual_vs_automatic",
+ "instance_exec",
+ "__is_a__",
+ "test_migrator_double_up",
+ "stub",
+ "private_methods",
+ "stubs",
+ "test_migrator_double_down",
+ "fixture_path",
+ "private_singleton_methods",
+ "stub_everything",
+ "test_migrator_one_up_with_exception_and_rollback",
+ "sequence",
+ "protected_methods",
+ "enum_for",
+ "test_finds_migrations",
+ "run_before_mocha",
+ "states",
+ "protected_singleton_methods",
+ "to_json",
+ "instance_values",
+ "==",
+ "mocha_setup",
+ "public_methods",
+ "test_finds_pending_migrations",
+ "mocha_verify",
+ "assert_kind_of",
+ "===",
+ "=~",
+ "test_relative_migrations",
+ "mocha_teardown",
+ "gem",
+ "mocha",
+ "test_only_loads_pending_migrations",
+ "test_add_column_with_precision_and_scale",
+ "require_or_load",
+ "eql?",
+ "require_dependency",
+ "test_native_types",
+ "test_target_version_zero_should_run_only_once",
+ "extend",
+ "to_matcher",
+ "unloadable",
+ "require_association",
+ "hash",
+ "__id__",
+ "load_dependency",
+ "equals",
+ "test_migrator_db_has_no_schema_migrations_table",
+ "test_migrator_verbosity",
+ "kind_of",
+ "to_yaml",
+ "to_bool",
+ "test_migrator_verbosity_off",
+ "taint",
+ "test_migrator_going_down_due_to_version_target",
+ "tainted?",
+ "mocha_inspect",
+ "test_migrator_rollback",
+ "vim",
+ "untaint",
+ "taguri=",
+ "test_migrator_forward",
+ "test_schema_migrations_table_name",
+ "test_proper_table_name",
+ "all_of",
+ "test_add_drop_table_with_prefix_and_suffix",
+ "_setup_callbacks",
+ "setup",
+ "Not",
+ "test_create_table_with_binary_column",
+ "assert_not_equal",
+ "enable_warnings",
+ "acts_like?",
+ "Rational",
+ "_removed_setup_callbacks",
+ "Table",
+ "bind",
+ "any_of",
+ "__method__",
+ "test_migrator_with_duplicates",
+ "_teardown_callbacks",
+ "method",
+ "test_migrator_with_duplicate_names",
+ "_removed_teardown_callbacks",
+ "any_parameters",
+ "test_migrator_with_missing_version_numbers",
+ "test_add_remove_single_field_using_string_arguments",
+ "test_create_table_with_custom_sequence_name",
+ "test_add_remove_single_field_using_symbol_arguments",
+ "_one_time_conditions_valid_14?",
+ "_one_time_conditions_valid_16?",
+ "run_callbacks",
+ "anything",
+ "silence_warnings",
+ "instance_variable_names",
+ "_fixture_path",
+ "copy_instance_variables_from",
+ "fixture_path?",
+ "has_entry",
+ "__marshal__",
+ "_fixture_table_names",
+ "__kind_of__",
+ "fixture_table_names?",
+ "test_add_rename",
+ "assert_equal",
+ "_fixture_class_names",
+ "fixture_class_names?",
+ "has_entries",
+ "_use_transactional_fixtures",
+ "people",
+ "test_rename_column_using_symbol_arguments",
+ "use_transactional_fixtures?",
+ "instance_eval",
+ "blank?",
+ "with_warnings",
+ "__nil__",
+ "load",
+ "metaclass",
+ "_use_instantiated_fixtures",
+ "has_key",
+ "class_eval",
+ "present?",
+ "test_rename_column",
+ "teardown",
+ "use_instantiated_fixtures?",
+ "method_name",
+ "silence_stderr",
+ "presence",
+ "test_rename_column_preserves_default_value_not_null",
+ "silence_stream",
+ "_pre_loaded_fixtures",
+ "__metaclass__",
+ "__fixnum__",
+ "pre_loaded_fixtures?",
+ "has_value",
+ "suppress",
+ "to_yaml_properties",
+ "test_rename_nonexistent_column",
+ "test_add_index",
+ "includes",
+ "find_correlate_in",
+ "equality_predicate_sql",
+ "assert_nothing_raised",
+ "let",
+ "not_predicate_sql",
+ "test_rename_column_with_sql_reserved_word",
+ "singleton_class",
+ "test_rename_column_with_an_index",
+ "display",
+ "taguri",
+ "to_yaml_style",
+ "test_remove_column_with_index",
+ "size",
+ "current_adapter?",
+ "test_remove_column_with_multi_column_index",
+ "respond_to?",
+ "test_change_type_of_not_null_column",
+ "is_a",
+ "to_a",
+ "test_rename_table_for_sqlite_should_work_with_reserved_words",
+ "require_library_or_gem",
+ "setup_fixtures",
+ "equal?",
+ "teardown_fixtures",
+ "nil?",
+ "fixture_table_names",
+ "fixture_class_names",
+ "test_create_table_without_id",
+ "use_transactional_fixtures",
+ "test_add_column_with_primary_key_attribute",
+ "repair_validations",
+ "use_instantiated_fixtures",
+ "instance_of?",
+ "test_create_table_adds_id",
+ "test_rename_table",
+ "pre_loaded_fixtures",
+ "to_enum",
+ "test_create_table_with_not_null_column",
+ "instance_of",
+ "test_change_column_nullability",
+ "optionally",
+ "test_rename_table_with_an_index",
+ "run",
+ "test_change_column",
+ "default_test",
+ "assert_raise",
+ "test_create_table_with_defaults",
+ "assert_nil",
+ "flunk",
+ "regexp_matches",
+ "duplicable?",
+ "reset_mocha",
+ "stubba_method",
+ "filter_backtrace",
+ "test_create_table_with_limits",
+ "responds_with",
+ "stubba_object",
+ "test_change_column_with_nil_default",
+ "assert_block",
+ "__show__",
+ "assert_date_from_db",
+ "__respond_to_eh__",
+ "run_in_transaction?",
+ "inspect",
+ "assert_sql",
+ "test_change_column_with_new_default",
+ "yaml_equivalent",
+ "build_message",
+ "to_s",
+ "test_change_column_default",
+ "assert_queries",
+ "pending",
+ "as_json",
+ "assert_no_queries",
+ "test_change_column_quotes_column_names",
+ "assert_match",
+ "test_keeping_default_and_notnull_constraint_on_change",
+ "methods",
+ "connection_allow_concurrency_setup",
+ "connection_allow_concurrency_teardown",
+ "test_create_table_with_primary_key_prefix_as_table_name_with_underscore",
+ "__send__",
+ "make_connection",
+ "assert_raises",
+ "tap",
+ "with_kcode",
+ "assert_instance_of",
+ "test_create_table_with_primary_key_prefix_as_table_name",
+ "assert_respond_to",
+ "test_change_column_default_to_null",
+ "assert_same",
+ "__extend__",
+ ]
+
+ LargeTestArraySorted = [
+ "test_add_column_not_null_with_default",
+ "test_add_column_with_precision_and_scale",
+ "test_add_column_with_primary_key_attribute",
+ "test_add_drop_table_with_prefix_and_suffix",
+ "test_add_index",
+ "test_add_remove_single_field_using_string_arguments",
+ "test_add_remove_single_field_using_symbol_arguments",
+ "test_add_rename",
+ "test_add_table",
+ "test_add_table_with_decimals",
+ "test_change_column",
+ "test_change_column_default",
+ "test_change_column_default_to_null",
+ "test_change_column_nullability",
+ "test_change_column_quotes_column_names",
+ "test_change_column_with_new_default",
+ "test_change_column_with_nil_default",
+ "test_change_type_of_not_null_column",
+ "test_create_table_adds_id",
+ "test_create_table_with_binary_column",
+ "test_create_table_with_custom_sequence_name",
+ "test_create_table_with_defaults",
+ "test_create_table_with_force_true_does_not_drop_nonexisting_table",
+ "test_create_table_with_limits",
+ "test_create_table_with_not_null_column",
+ "test_create_table_with_primary_key_prefix_as_table_name",
+ "test_create_table_with_primary_key_prefix_as_table_name_with_underscore",
+ "test_create_table_with_timestamps_should_create_datetime_columns",
+ "test_create_table_with_timestamps_should_create_datetime_columns_with_options",
+ "test_create_table_without_a_block",
+ "test_create_table_without_id",
+ "test_finds_migrations",
+ "test_finds_pending_migrations",
+ "test_keeping_default_and_notnull_constraint_on_change",
+ "test_migrator",
+ "test_migrator_db_has_no_schema_migrations_table",
+ "test_migrator_double_down",
+ "test_migrator_double_up",
+ "test_migrator_forward",
+ "test_migrator_going_down_due_to_version_target",
+ "test_migrator_one_down",
+ "test_migrator_one_up",
+ "test_migrator_one_up_one_down",
+ "test_migrator_one_up_with_exception_and_rollback",
+ "test_migrator_rollback",
+ "test_migrator_verbosity",
+ "test_migrator_verbosity_off",
+ "test_migrator_with_duplicate_names",
+ "test_migrator_with_duplicates",
+ "test_migrator_with_missing_version_numbers",
+ "test_native_decimal_insert_manual_vs_automatic",
+ "test_native_types",
+ "test_only_loads_pending_migrations",
+ "test_proper_table_name",
+ "test_relative_migrations",
+ "test_remove_column_with_index",
+ "test_remove_column_with_multi_column_index",
+ "test_rename_column",
+ "test_rename_column_preserves_default_value_not_null",
+ "test_rename_column_using_symbol_arguments",
+ "test_rename_column_with_an_index",
+ "test_rename_column_with_sql_reserved_word",
+ "test_rename_nonexistent_column",
+ "test_rename_table",
+ "test_rename_table_for_sqlite_should_work_with_reserved_words",
+ "test_rename_table_with_an_index",
+ "test_schema_migrations_table_name",
+ "test_target_version_zero_should_run_only_once",
+ ]
+
+ class PrivateToAry
+ private
+
+ def to_ary
+ [1, 2, 3]
+ end
+ end
+end
diff --git a/spec/ruby/core/array/fixtures/encoded_strings.rb b/spec/ruby/core/array/fixtures/encoded_strings.rb
new file mode 100644
index 0000000000..b5888d86ae
--- /dev/null
+++ b/spec/ruby/core/array/fixtures/encoded_strings.rb
@@ -0,0 +1,69 @@
+# encoding: utf-8
+module ArraySpecs
+ def self.array_with_usascii_and_7bit_utf8_strings
+ [
+ 'foo'.dup.force_encoding('US-ASCII'),
+ 'bar'
+ ]
+ end
+
+ def self.array_with_usascii_and_utf8_strings
+ [
+ 'foo'.dup.force_encoding('US-ASCII'),
+ 'báz'
+ ]
+ end
+
+ def self.array_with_7bit_utf8_and_usascii_strings
+ [
+ 'bar',
+ 'foo'.dup.force_encoding('US-ASCII')
+ ]
+ end
+
+ def self.array_with_utf8_and_usascii_strings
+ [
+ 'báz',
+ 'bar',
+ 'foo'.dup.force_encoding('US-ASCII')
+ ]
+ end
+
+ def self.array_with_usascii_and_utf8_strings
+ [
+ 'foo'.dup.force_encoding('US-ASCII'),
+ 'bar',
+ 'báz'
+ ]
+ end
+
+ def self.array_with_utf8_and_7bit_binary_strings
+ [
+ 'bar',
+ 'báz',
+ 'foo'.dup.force_encoding('BINARY')
+ ]
+ end
+
+ def self.array_with_utf8_and_binary_strings
+ [
+ 'bar',
+ 'báz',
+ [255].pack('C').force_encoding('BINARY')
+ ]
+ end
+
+ def self.array_with_usascii_and_7bit_binary_strings
+ [
+ 'bar'.dup.force_encoding('US-ASCII'),
+ 'foo'.dup.force_encoding('BINARY')
+ ]
+ end
+
+ def self.array_with_usascii_and_binary_strings
+ [
+ 'bar'.dup.force_encoding('US-ASCII'),
+ [255].pack('C').force_encoding('BINARY')
+ ]
+ end
+end
diff --git a/spec/ruby/core/array/flatten_spec.rb b/spec/ruby/core/array/flatten_spec.rb
new file mode 100644
index 0000000000..272406b8f9
--- /dev/null
+++ b/spec/ruby/core/array/flatten_spec.rb
@@ -0,0 +1,266 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#flatten" do
+ it "returns a one-dimensional flattening recursively" do
+ [[[1, [2, 3]],[2, 3, [4, [4, [5, 5]], [1, 2, 3]]], [4]], []].flatten.should == [1, 2, 3, 2, 3, 4, 4, 5, 5, 1, 2, 3, 4]
+ end
+
+ it "takes an optional argument that determines the level of recursion" do
+ [ 1, 2, [3, [4, 5] ] ].flatten(1).should == [1, 2, 3, [4, 5]]
+ end
+
+ it "returns dup when the level of recursion is 0" do
+ a = [ 1, 2, [3, [4, 5] ] ]
+ a.flatten(0).should == a
+ a.flatten(0).should_not.equal?(a)
+ end
+
+ it "ignores negative levels" do
+ [ 1, 2, [ 3, 4, [5, 6] ] ].flatten(-1).should == [1, 2, 3, 4, 5, 6]
+ [ 1, 2, [ 3, 4, [5, 6] ] ].flatten(-10).should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "tries to convert passed Objects to Integers using #to_int" do
+ obj = mock("Converted to Integer")
+ obj.should_receive(:to_int).and_return(1)
+
+ [ 1, 2, [3, [4, 5] ] ].flatten(obj).should == [1, 2, 3, [4, 5]]
+ end
+
+ it "raises a TypeError when the passed Object can't be converted to an Integer" do
+ obj = mock("Not converted")
+ -> { [ 1, 2, [3, [4, 5] ] ].flatten(obj) }.should.raise(TypeError)
+ end
+
+ it "does not call flatten on elements" do
+ obj = mock('[1,2]')
+ obj.should_not_receive(:flatten)
+ [obj, obj].flatten.should == [obj, obj]
+
+ obj = [5, 4]
+ obj.should_not_receive(:flatten)
+ [obj, obj].flatten.should == [5, 4, 5, 4]
+ end
+
+ it "raises an ArgumentError on recursive arrays" do
+ x = []
+ x << x
+ -> { x.flatten }.should.raise(ArgumentError)
+
+ x = []
+ y = []
+ x << y
+ y << x
+ -> { x.flatten }.should.raise(ArgumentError)
+ end
+
+ it "flattens any element which responds to #to_ary, using the return value of said method" do
+ x = mock("[3,4]")
+ x.should_receive(:to_ary).at_least(:once).and_return([3, 4])
+ [1, 2, x, 5].flatten.should == [1, 2, 3, 4, 5]
+
+ y = mock("MyArray[]")
+ y.should_receive(:to_ary).at_least(:once).and_return(ArraySpecs::MyArray[])
+ [y].flatten.should == []
+
+ z = mock("[2,x,y,5]")
+ z.should_receive(:to_ary).and_return([2, x, y, 5])
+ [1, z, 6].flatten.should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "does not call #to_ary on elements beyond the given level" do
+ obj = mock("1")
+ obj.should_not_receive(:to_ary)
+ [[obj]].flatten(1)
+ end
+
+ it "returns Array instance for Array subclasses" do
+ ArraySpecs::MyArray[].flatten.should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].flatten.should.instance_of?(Array)
+ ArraySpecs::MyArray[1, [2], 3].flatten.should.instance_of?(Array)
+ ArraySpecs::MyArray[1, [2, 3], 4].flatten.should == [1, 2, 3, 4]
+ [ArraySpecs::MyArray[1, 2, 3]].flatten.should.instance_of?(Array)
+ end
+
+ it "is not destructive" do
+ ary = [1, [2, 3]]
+ ary.flatten
+ ary.should == [1, [2, 3]]
+ end
+
+ describe "with a non-Array object in the Array" do
+ before :each do
+ @obj = mock("Array#flatten")
+ ScratchPad.record []
+ end
+
+ it "does not call #to_ary if the method is not defined" do
+ [@obj].flatten.should == [@obj]
+ end
+
+ it "does not raise an exception if #to_ary returns nil" do
+ @obj.should_receive(:to_ary).and_return(nil)
+ [@obj].flatten.should == [@obj]
+ end
+
+ it "raises a TypeError if #to_ary does not return an Array" do
+ @obj.should_receive(:to_ary).and_return(1)
+ -> { [@obj].flatten }.should.raise(TypeError)
+ end
+
+ it "calls respond_to_missing?(:to_ary, true) to try coercing" do
+ def @obj.respond_to_missing?(*args) ScratchPad << args; false end
+ [@obj].flatten.should == [@obj]
+ ScratchPad.recorded.should == [[:to_ary, true]]
+ end
+
+ it "does not call #to_ary if not defined when #respond_to_missing? returns false" do
+ def @obj.respond_to_missing?(name, priv) ScratchPad << name; false end
+
+ [@obj].flatten.should == [@obj]
+ ScratchPad.recorded.should == [:to_ary]
+ end
+
+ it "calls #to_ary if not defined when #respond_to_missing? returns true" do
+ def @obj.respond_to_missing?(name, priv) ScratchPad << name; true end
+
+ -> { [@obj].flatten }.should.raise(NoMethodError)
+ ScratchPad.recorded.should == [:to_ary]
+ end
+
+ it "calls #method_missing if defined" do
+ @obj.should_receive(:method_missing).with(:to_ary).and_return([1, 2, 3])
+ [@obj].flatten.should == [1, 2, 3]
+ end
+ end
+
+ it "performs respond_to? and method_missing-aware checks when coercing elements to array" do
+ bo = BasicObject.new
+ [bo].flatten.should == [bo]
+
+ def bo.method_missing(name, *)
+ [1,2]
+ end
+
+ [bo].flatten.should == [1,2]
+
+ def bo.respond_to?(name, *)
+ false
+ end
+
+ [bo].flatten.should == [bo]
+
+ def bo.respond_to?(name, *)
+ true
+ end
+
+ [bo].flatten.should == [1,2]
+ end
+end
+
+describe "Array#flatten!" do
+ it "modifies array to produce a one-dimensional flattening recursively" do
+ a = [[[1, [2, 3]],[2, 3, [4, [4, [5, 5]], [1, 2, 3]]], [4]], []]
+ a.flatten!
+ a.should == [1, 2, 3, 2, 3, 4, 4, 5, 5, 1, 2, 3, 4]
+ end
+
+ it "returns self if made some modifications" do
+ a = [[[1, [2, 3]],[2, 3, [4, [4, [5, 5]], [1, 2, 3]]], [4]], []]
+ a.flatten!.should.equal?(a)
+ end
+
+ it "returns nil if no modifications took place" do
+ a = [1, 2, 3]
+ a.flatten!.should == nil
+ a = [1, [2, 3]]
+ a.flatten!.should_not == nil
+ end
+
+ it "should not check modification by size" do
+ a = [1, 2, [3]]
+ a.flatten!.should_not == nil
+ a.should == [1, 2, 3]
+ end
+
+ it "takes an optional argument that determines the level of recursion" do
+ [ 1, 2, [3, [4, 5] ] ].flatten!(1).should == [1, 2, 3, [4, 5]]
+ end
+
+ # redmine #1440
+ it "returns nil when the level of recursion is 0" do
+ a = [ 1, 2, [3, [4, 5] ] ]
+ a.flatten!(0).should == nil
+ end
+
+ it "treats negative levels as no arguments" do
+ [ 1, 2, [ 3, 4, [5, 6] ] ].flatten!(-1).should == [1, 2, 3, 4, 5, 6]
+ [ 1, 2, [ 3, 4, [5, 6] ] ].flatten!(-10).should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "tries to convert passed Objects to Integers using #to_int" do
+ obj = mock("Converted to Integer")
+ obj.should_receive(:to_int).and_return(1)
+
+ [ 1, 2, [3, [4, 5] ] ].flatten!(obj).should == [1, 2, 3, [4, 5]]
+ end
+
+ it "raises a TypeError when the passed Object can't be converted to an Integer" do
+ obj = mock("Not converted")
+ -> { [ 1, 2, [3, [4, 5] ] ].flatten!(obj) }.should.raise(TypeError)
+ end
+
+ it "does not call flatten! on elements" do
+ obj = mock('[1,2]')
+ obj.should_not_receive(:flatten!)
+ [obj, obj].flatten!.should == nil
+
+ obj = [5, 4]
+ obj.should_not_receive(:flatten!)
+ [obj, obj].flatten!.should == [5, 4, 5, 4]
+ end
+
+ it "raises an ArgumentError on recursive arrays" do
+ x = []
+ x << x
+ -> { x.flatten! }.should.raise(ArgumentError)
+
+ x = []
+ y = []
+ x << y
+ y << x
+ -> { x.flatten! }.should.raise(ArgumentError)
+ end
+
+ it "flattens any elements which responds to #to_ary, using the return value of said method" do
+ x = mock("[3,4]")
+ x.should_receive(:to_ary).at_least(:once).and_return([3, 4])
+ [1, 2, x, 5].flatten!.should == [1, 2, 3, 4, 5]
+
+ y = mock("MyArray[]")
+ y.should_receive(:to_ary).at_least(:once).and_return(ArraySpecs::MyArray[])
+ [y].flatten!.should == []
+
+ z = mock("[2,x,y,5]")
+ z.should_receive(:to_ary).and_return([2, x, y, 5])
+ [1, z, 6].flatten!.should == [1, 2, 3, 4, 5, 6]
+
+ ary = [ArraySpecs::MyArray[1, 2, 3]]
+ ary.flatten!
+ ary.should.instance_of?(Array)
+ ary.should == [1, 2, 3]
+ end
+
+ it "raises a FrozenError on frozen arrays when the array is modified" do
+ nested_ary = [1, 2, []]
+ nested_ary.freeze
+ -> { nested_ary.flatten! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23663]
+ it "raises a FrozenError on frozen arrays when the array would not be modified" do
+ -> { ArraySpecs.frozen_array.flatten! }.should.raise(FrozenError)
+ -> { ArraySpecs.empty_frozen_array.flatten! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/frozen_spec.rb b/spec/ruby/core/array/frozen_spec.rb
new file mode 100644
index 0000000000..3ba54be46b
--- /dev/null
+++ b/spec/ruby/core/array/frozen_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#frozen?" do
+ it "returns true if array is frozen" do
+ a = [1, 2, 3]
+ a.should_not.frozen?
+ a.freeze
+ a.should.frozen?
+ end
+
+ it "returns false for an array being sorted by #sort" do
+ a = [1, 2, 3]
+ a.sort { |x,y| a.should_not.frozen?; x <=> y }
+ end
+end
diff --git a/spec/ruby/core/array/hash_spec.rb b/spec/ruby/core/array/hash_spec.rb
new file mode 100644
index 0000000000..3b7b6d5bed
--- /dev/null
+++ b/spec/ruby/core/array/hash_spec.rb
@@ -0,0 +1,83 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#hash" do
+ it "returns the same fixnum for arrays with the same content" do
+ [].respond_to?(:hash).should == true
+
+ [[], [1, 2, 3]].each do |ary|
+ ary.hash.should == ary.dup.hash
+ ary.hash.should.instance_of?(Integer)
+ end
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ -> { empty.hash }.should_not.raise
+
+ array = ArraySpecs.recursive_array
+ -> { array.hash }.should_not.raise
+ end
+
+ it "returns the same hash for equal recursive arrays" do
+ rec = []; rec << rec
+ rec.hash.should == [rec].hash
+ rec.hash.should == [[rec]].hash
+ # This is because rec.eql?([[rec]])
+ # Remember that if two objects are eql?
+ # then the need to have the same hash
+ # Check the Array#eql? specs!
+ end
+
+ it "returns the same hash for equal recursive arrays through hashes" do
+ h = {} ; rec = [h] ; h[:x] = rec
+ rec.hash.should == [h].hash
+ rec.hash.should == [{x: rec}].hash
+ # Like above, this is because rec.eql?([{x: rec}])
+ end
+
+ it "calls to_int on result of calling hash on each element" do
+ ary = Array.new(5) do
+ obj = mock('0')
+ obj.should_receive(:hash).and_return(obj)
+ obj.should_receive(:to_int).and_return(0)
+ obj
+ end
+
+ ary.hash
+
+
+ hash = mock('1')
+ hash.should_receive(:to_int).and_return(1.hash)
+
+ obj = mock('@hash')
+ obj.instance_variable_set(:@hash, hash)
+ def obj.hash() @hash end
+
+ [obj].hash.should == [1].hash
+ end
+
+ it "ignores array class differences" do
+ ArraySpecs::MyArray[].hash.should == [].hash
+ ArraySpecs::MyArray[1, 2].hash.should == [1, 2].hash
+ end
+
+ it "returns same hash code for arrays with the same content" do
+ a = [1, 2, 3, 4]
+ a.fill 'a', 0..3
+ b = %w|a a a a|
+ a.hash.should == b.hash
+ end
+
+ it "returns the same value if arrays are #eql?" do
+ a = [1, 2, 3, 4]
+ a.fill 'a', 0..3
+ b = %w|a a a a|
+ a.hash.should == b.hash
+ a.should.eql?(b)
+ end
+
+ it "produces different hashes for nested arrays with different values and empty terminator" do
+ [1, [1, []]].hash.should_not == [2, [2, []]].hash
+ end
+end
diff --git a/spec/ruby/core/array/include_spec.rb b/spec/ruby/core/array/include_spec.rb
new file mode 100644
index 0000000000..227173218f
--- /dev/null
+++ b/spec/ruby/core/array/include_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#include?" do
+ it "returns true if object is present, false otherwise" do
+ [1, 2, "a", "b"].include?("c").should == false
+ [1, 2, "a", "b"].include?("a").should == true
+ end
+
+ it "determines presence by using element == obj" do
+ o = mock('')
+
+ [1, 2, "a", "b"].include?(o).should == false
+
+ def o.==(other); other == 'a'; end
+
+ [1, 2, o, "b"].include?('a').should == true
+
+ [1, 2.0, 3].include?(2).should == true
+ end
+
+ it "calls == on elements from left to right until success" do
+ key = "x"
+ one = mock('one')
+ two = mock('two')
+ three = mock('three')
+ one.should_receive(:==).any_number_of_times.and_return(false)
+ two.should_receive(:==).any_number_of_times.and_return(true)
+ three.should_not_receive(:==)
+ ary = [one, two, three]
+ ary.include?(key).should == true
+ end
+end
diff --git a/spec/ruby/core/array/index_spec.rb b/spec/ruby/core/array/index_spec.rb
new file mode 100644
index 0000000000..b66cb6eb53
--- /dev/null
+++ b/spec/ruby/core/array/index_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Array#index" do
+ it "is an alias of Array#find_index" do
+ Array.instance_method(:index).should == Array.instance_method(:find_index)
+ end
+end
diff --git a/spec/ruby/core/array/initialize_spec.rb b/spec/ruby/core/array/initialize_spec.rb
new file mode 100644
index 0000000000..19ee37825e
--- /dev/null
+++ b/spec/ruby/core/array/initialize_spec.rb
@@ -0,0 +1,158 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#initialize" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is private" do
+ Array.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "is called on subclasses" do
+ b = ArraySpecs::SubArray.new :size_or_array, :obj
+
+ b.should == []
+ ScratchPad.recorded.should == [:size_or_array, :obj]
+ end
+
+ it "preserves the object's identity even when changing its value" do
+ a = [1, 2, 3]
+ a.send(:initialize).should.equal?(a)
+ a.should_not == [1, 2, 3]
+ end
+
+ it "raises an ArgumentError if passed 3 or more arguments" do
+ -> do
+ [1, 2].send :initialize, 1, 'x', true
+ end.should.raise(ArgumentError)
+ -> do
+ [1, 2].send(:initialize, 1, 'x', true) {}
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError on frozen arrays" do
+ -> do
+ ArraySpecs.frozen_array.send :initialize
+ end.should.raise(FrozenError)
+ -> do
+ ArraySpecs.frozen_array.send :initialize, ArraySpecs.frozen_array
+ end.should.raise(FrozenError)
+ end
+
+ it "calls #to_ary to convert the value to an array, even if it's private" do
+ a = ArraySpecs::PrivateToAry.new
+ [].send(:initialize, a).should == [1, 2, 3]
+ end
+end
+
+describe "Array#initialize with no arguments" do
+ it "makes the array empty" do
+ [1, 2, 3].send(:initialize).should.empty?
+ end
+
+ it "does not use the given block" do
+ -> {
+ -> { [1, 2, 3].send(:initialize) { raise } }.should_not.raise
+ }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true)
+ end
+end
+
+describe "Array#initialize with (array)" do
+ it "replaces self with the other array" do
+ b = [4, 5, 6]
+ [1, 2, 3].send(:initialize, b).should == b
+ end
+
+ it "does not use the given block" do
+ ->{ [1, 2, 3].send(:initialize) { raise } }.should_not.raise
+ end
+
+ it "calls #to_ary to convert the value to an array" do
+ a = mock("array")
+ a.should_receive(:to_ary).and_return([1, 2])
+ a.should_not_receive(:to_int)
+ [].send(:initialize, a).should == [1, 2]
+ end
+
+ it "does not call #to_ary on instances of Array or subclasses of Array" do
+ a = [1, 2]
+ a.should_not_receive(:to_ary)
+ [].send(:initialize, a).should == a
+ end
+
+ it "raises a TypeError if an Array type argument and a default object" do
+ -> { [].send(:initialize, [1, 2], 1) }.should.raise(TypeError)
+ end
+end
+
+describe "Array#initialize with (size, object=nil)" do
+ it "sets the array to size and fills with the object" do
+ a = []
+ obj = [3]
+ a.send(:initialize, 2, obj).should == [obj, obj]
+ a[0].should.equal?(obj)
+ a[1].should.equal?(obj)
+
+ b = []
+ b.send(:initialize, 3, 14).should == [14, 14, 14]
+ b.should == [14, 14, 14]
+ end
+
+ it "sets the array to size and fills with nil when object is omitted" do
+ [].send(:initialize, 3).should == [nil, nil, nil]
+ end
+
+ it "raises an ArgumentError if size is negative" do
+ -> { [].send(:initialize, -1, :a) }.should.raise(ArgumentError)
+ -> { [].send(:initialize, -1) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if size is too large" do
+ -> { [].send(:initialize, fixnum_max+1) }.should.raise(ArgumentError)
+ end
+
+ it "calls #to_int to convert the size argument to an Integer when object is given" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ [].send(:initialize, obj, :a).should == [:a]
+ end
+
+ it "calls #to_int to convert the size argument to an Integer when object is not given" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ [].send(:initialize, obj).should == [nil]
+ end
+
+ it "raises a TypeError if the size argument is not an Integer type" do
+ obj = mock('nonnumeric')
+ obj.stub!(:to_ary).and_return([1, 2])
+ ->{ [].send(:initialize, obj, :a) }.should.raise(TypeError)
+ end
+
+ it "yields the index of the element and sets the element to the value of the block" do
+ [].send(:initialize, 3) { |i| i.to_s }.should == ['0', '1', '2']
+ end
+
+ it "uses the block value instead of using the default value" do
+ -> {
+ @result = [].send(:initialize, 3, :obj) { |i| i.to_s }
+ }.should complain(/block supersedes default value argument/)
+ @result.should == ['0', '1', '2']
+ end
+
+ it "returns the value passed to break" do
+ [].send(:initialize, 3) { break :a }.should == :a
+ end
+
+ it "sets the array to the values returned by the block before break is executed" do
+ a = [1, 2, 3]
+ a.send(:initialize, 3) do |i|
+ break if i == 2
+ i.to_s
+ end
+
+ a.should == ['0', '1']
+ end
+end
diff --git a/spec/ruby/core/array/insert_spec.rb b/spec/ruby/core/array/insert_spec.rb
new file mode 100644
index 0000000000..38e132fd25
--- /dev/null
+++ b/spec/ruby/core/array/insert_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#insert" do
+ it "returns self" do
+ ary = []
+ ary.insert(0).should.equal?(ary)
+ ary.insert(0, :a).should.equal?(ary)
+ end
+
+ it "inserts objects before the element at index for non-negative index" do
+ ary = []
+ ary.insert(0, 3).should == [3]
+ ary.insert(0, 1, 2).should == [1, 2, 3]
+ ary.insert(0).should == [1, 2, 3]
+
+ # Let's just assume insert() always modifies the array from now on.
+ ary.insert(1, 'a').should == [1, 'a', 2, 3]
+ ary.insert(0, 'b').should == ['b', 1, 'a', 2, 3]
+ ary.insert(5, 'c').should == ['b', 1, 'a', 2, 3, 'c']
+ ary.insert(7, 'd').should == ['b', 1, 'a', 2, 3, 'c', nil, 'd']
+ ary.insert(10, 5, 4).should == ['b', 1, 'a', 2, 3, 'c', nil, 'd', nil, nil, 5, 4]
+ end
+
+ it "appends objects to the end of the array for index == -1" do
+ [1, 3, 3].insert(-1, 2, 'x', 0.5).should == [1, 3, 3, 2, 'x', 0.5]
+ end
+
+ it "inserts objects after the element at index with negative index" do
+ ary = []
+ ary.insert(-1, 3).should == [3]
+ ary.insert(-2, 2).should == [2, 3]
+ ary.insert(-3, 1).should == [1, 2, 3]
+ ary.insert(-2, -3).should == [1, 2, -3, 3]
+ ary.insert(-1, []).should == [1, 2, -3, 3, []]
+ ary.insert(-2, 'x', 'y').should == [1, 2, -3, 3, 'x', 'y', []]
+ ary = [1, 2, 3]
+ end
+
+ it "pads with nils if the index to be inserted to is past the end" do
+ [].insert(5, 5).should == [nil, nil, nil, nil, nil, 5]
+ end
+
+ it "can insert before the first element with a negative index" do
+ [1, 2, 3].insert(-4, -3).should == [-3, 1, 2, 3]
+ end
+
+ it "raises an IndexError if the negative index is out of bounds" do
+ -> { [].insert(-2, 1) }.should.raise(IndexError)
+ -> { [1].insert(-3, 2) }.should.raise(IndexError)
+ end
+
+ it "does nothing of no object is passed" do
+ [].insert(0).should == []
+ [].insert(-1).should == []
+ [].insert(10).should == []
+ [].insert(-2).should == []
+ end
+
+ it "tries to convert the passed position argument to an Integer using #to_int" do
+ obj = mock('2')
+ obj.should_receive(:to_int).and_return(2)
+ [].insert(obj, 'x').should == [nil, nil, 'x']
+ end
+
+ it "raises an ArgumentError if no argument passed" do
+ -> { [].insert() }.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError on frozen arrays when the array is modified" do
+ -> { ArraySpecs.frozen_array.insert(0, 'x') }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on frozen arrays when the array would not be modified" do
+ -> { ArraySpecs.frozen_array.insert(0) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/inspect_spec.rb b/spec/ruby/core/array/inspect_spec.rb
new file mode 100644
index 0000000000..e5dca82889
--- /dev/null
+++ b/spec/ruby/core/array/inspect_spec.rb
@@ -0,0 +1,108 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#inspect" do
+ it "returns a string" do
+ [1, 2, 3].inspect.should.instance_of?(String)
+ end
+
+ it "returns '[]' for an empty Array" do
+ [].inspect.should == "[]"
+ end
+
+ it "calls inspect on its elements and joins the results with commas" do
+ items = Array.new(3) do |i|
+ obj = mock(i.to_s)
+ obj.should_receive(:inspect).and_return(i.to_s)
+ obj
+ end
+ items.inspect.should == "[0, 1, 2]"
+ end
+
+ it "does not call #to_s on a String returned from #inspect" do
+ str = +"abc"
+ str.should_not_receive(:to_s)
+
+ [str].inspect.should == '["abc"]'
+ end
+
+ it "calls #to_s on the object returned from #inspect if the Object isn't a String" do
+ obj = mock("Array#inspect/to_s calls #to_s")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_receive(:to_s).and_return("abc")
+
+ [obj].inspect.should == "[abc]"
+ end
+
+ it "does not call #to_str on the object returned from #inspect when it is not a String" do
+ obj = mock("Array#inspect/to_s does not call #to_str")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_not_receive(:to_str)
+
+ [obj].inspect.should =~ /^\[#<MockObject:0x[0-9a-f]+>\]$/
+ end
+
+ it "does not call #to_str on the object returned from #to_s when it is not a String" do
+ obj = mock("Array#inspect/to_s does not call #to_str on #to_s result")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_receive(:to_s).and_return(obj)
+ obj.should_not_receive(:to_str)
+
+ [obj].inspect.should =~ /^\[#<MockObject:0x[0-9a-f]+>\]$/
+ end
+
+ it "does not swallow exceptions raised by #to_s" do
+ obj = mock("Array#inspect/to_s does not swallow #to_s exceptions")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_receive(:to_s).and_raise(Exception)
+
+ -> { [obj].inspect }.should.raise(Exception)
+ end
+
+ it "represents a recursive element with '[...]'" do
+ ArraySpecs.recursive_array.inspect.should == "[1, \"two\", 3.0, [...], [...], [...], [...], [...]]"
+ ArraySpecs.head_recursive_array.inspect.should == "[[...], [...], [...], [...], [...], 1, \"two\", 3.0]"
+ ArraySpecs.empty_recursive_array.inspect.should == "[[...]]"
+ end
+
+ describe "with encoding" do
+ before :each do
+ @default_external_encoding = Encoding.default_external
+ end
+
+ after :each do
+ Encoding.default_external = @default_external_encoding
+ end
+
+ it "returns a US-ASCII string for an empty Array" do
+ [].inspect.encoding.should == Encoding::US_ASCII
+ end
+
+ it "use the default external encoding if it is ascii compatible" do
+ Encoding.default_external = Encoding.find('UTF-8')
+
+ utf8 = "utf8".encode("UTF-8")
+ jp = "jp".encode("EUC-JP")
+ array = [jp, utf8]
+
+ array.inspect.encoding.name.should == "UTF-8"
+ end
+
+ it "use US-ASCII encoding if the default external encoding is not ascii compatible" do
+ Encoding.default_external = Encoding.find('UTF-32')
+
+ utf8 = "utf8".encode("UTF-8")
+ jp = "jp".encode("EUC-JP")
+ array = [jp, utf8]
+
+ array.inspect.encoding.name.should == "US-ASCII"
+ end
+
+ it "does not raise if inspected result is not default external encoding" do
+ utf_16be = mock(+"utf_16be")
+ utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE))
+
+ [utf_16be].inspect.should == '["utf_16be \u3042"]'
+ end
+ end
+end
diff --git a/spec/ruby/core/array/intersect_spec.rb b/spec/ruby/core/array/intersect_spec.rb
new file mode 100644
index 0000000000..456aa26c6e
--- /dev/null
+++ b/spec/ruby/core/array/intersect_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Array#intersect?' do
+ describe 'when at least one element in two Arrays is the same' do
+ it 'returns true' do
+ [1, 2].intersect?([2, 3, 4]).should == true
+ [2, 3, 4].intersect?([1, 2]).should == true
+ end
+ end
+
+ describe 'when there are no elements in common between two Arrays' do
+ it 'returns false' do
+ [0, 1, 2].intersect?([3, 4]).should == false
+ [3, 4].intersect?([0, 1, 2]).should == false
+ [3, 4].intersect?([]).should == false
+ [].intersect?([0, 1, 2]).should == false
+ end
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('[1,2,3]')
+ obj.should_receive(:to_ary).and_return([1, 2, 3])
+
+ [1, 2].intersect?(obj).should == true
+ end
+
+ it "determines equivalence between elements in the sense of eql?" do
+ obj1 = mock('1')
+ obj2 = mock('2')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj1.stub!(:eql?).and_return(true)
+ obj2.stub!(:eql?).and_return(true)
+
+ [obj1].intersect?([obj2]).should == true
+
+ obj1 = mock('3')
+ obj2 = mock('4')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj1.stub!(:eql?).and_return(false)
+ obj2.stub!(:eql?).and_return(false)
+
+ [obj1].intersect?([obj2]).should == false
+ end
+
+ it "does not call to_ary on array subclasses" do
+ [5, 6].intersect?(ArraySpecs::ToAryArray[1, 2, 5, 6]).should == true
+ end
+
+ it "properly handles an identical item even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.stub!(:hash).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ [x].intersect?([x]).should == true
+ end
+
+ it "has semantic of !(a & b).empty?" do
+ [].intersect?([]).should == false
+ [nil].intersect?([nil]).should == true
+ end
+end
diff --git a/spec/ruby/core/array/intersection_spec.rb b/spec/ruby/core/array/intersection_spec.rb
new file mode 100644
index 0000000000..e01a68d389
--- /dev/null
+++ b/spec/ruby/core/array/intersection_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/intersection'
+
+describe "Array#&" do
+ it_behaves_like :array_intersection, :&
+end
+
+describe "Array#intersection" do
+ it_behaves_like :array_intersection, :intersection
+
+ it "accepts multiple arguments" do
+ [1, 2, 3, 4].intersection([1, 2, 3], [2, 3, 4]).should == [2, 3]
+ end
+
+ it "preserves elements order from original array" do
+ [1, 2, 3, 4].intersection([3, 2, 1]).should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/array/join_spec.rb b/spec/ruby/core/array/join_spec.rb
new file mode 100644
index 0000000000..3b4946a99f
--- /dev/null
+++ b/spec/ruby/core/array/join_spec.rb
@@ -0,0 +1,146 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/join'
+
+describe "Array#join" do
+ it_behaves_like :array_join_with_string_separator, :join
+
+ it "does not separate elements when the passed separator is nil" do
+ [1, 2, 3].join(nil).should == '123'
+ end
+
+ it "calls #to_str to convert the separator to a String" do
+ sep = mock("separator")
+ sep.should_receive(:to_str).and_return(", ")
+ [1, 2].join(sep).should == "1, 2"
+ end
+
+ it "does not call #to_str on the separator if the array is empty" do
+ sep = mock("separator")
+ sep.should_not_receive(:to_str)
+ [].join(sep).should == ""
+ end
+
+ it "raises a TypeError if the separator cannot be coerced to a String by calling #to_str" do
+ obj = mock("not a string")
+ -> { [1, 2].join(obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed false as the separator" do
+ -> { [1, 2].join(false) }.should.raise(TypeError)
+ end
+end
+
+describe "Array#join with default separator" do
+ before :each do
+ @separator = $,
+ end
+
+ after :each do
+ $, = @separator
+ end
+
+ it "returns an empty string if the Array is empty" do
+ [].join.should == ''
+ end
+
+ it "returns a US-ASCII string for an empty Array" do
+ [].join.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a string formed by concatenating each String element separated by $," do
+ suppress_warning {
+ $, = " | "
+ ["1", "2", "3"].join.should == "1 | 2 | 3"
+ }
+ end
+
+ it "attempts coercion via #to_str first" do
+ obj = mock('foo')
+ obj.should_receive(:to_str).any_number_of_times.and_return("foo")
+ [obj].join.should == "foo"
+ end
+
+ it "attempts coercion via #to_ary second" do
+ obj = mock('foo')
+ obj.should_receive(:to_str).any_number_of_times.and_return(nil)
+ obj.should_receive(:to_ary).any_number_of_times.and_return(["foo"])
+ [obj].join.should == "foo"
+ end
+
+ it "attempts coercion via #to_s third" do
+ obj = mock('foo')
+ obj.should_receive(:to_str).any_number_of_times.and_return(nil)
+ obj.should_receive(:to_ary).any_number_of_times.and_return(nil)
+ obj.should_receive(:to_s).any_number_of_times.and_return("foo")
+ [obj].join.should == "foo"
+ end
+
+ it "raises a NoMethodError if an element does not respond to #to_str, #to_ary, or #to_s" do
+ obj = mock('o')
+ class << obj; undef :to_s; end
+ -> { [1, obj].join }.should.raise(NoMethodError)
+ end
+
+ it "raises an ArgumentError when the Array is recursive" do
+ -> { ArraySpecs.recursive_array.join }.should.raise(ArgumentError)
+ -> { ArraySpecs.head_recursive_array.join }.should.raise(ArgumentError)
+ -> { ArraySpecs.empty_recursive_array.join }.should.raise(ArgumentError)
+ end
+
+ it "uses the first encoding when other strings are compatible" do
+ ary1 = ArraySpecs.array_with_7bit_utf8_and_usascii_strings
+ ary2 = ArraySpecs.array_with_usascii_and_7bit_utf8_strings
+ ary3 = ArraySpecs.array_with_utf8_and_7bit_binary_strings
+ ary4 = ArraySpecs.array_with_usascii_and_7bit_binary_strings
+
+ ary1.join.encoding.should == Encoding::UTF_8
+ ary2.join.encoding.should == Encoding::US_ASCII
+ ary3.join.encoding.should == Encoding::UTF_8
+ ary4.join.encoding.should == Encoding::US_ASCII
+ end
+
+ it "uses the widest common encoding when other strings are incompatible" do
+ ary1 = ArraySpecs.array_with_utf8_and_usascii_strings
+ ary2 = ArraySpecs.array_with_usascii_and_utf8_strings
+
+ ary1.join.encoding.should == Encoding::UTF_8
+ ary2.join.encoding.should == Encoding::UTF_8
+ end
+
+ it "fails for arrays with incompatibly-encoded strings" do
+ ary_utf8_bad_binary = ArraySpecs.array_with_utf8_and_binary_strings
+
+ -> { ary_utf8_bad_binary.join }.should.raise(EncodingError)
+ end
+
+ context "when $, is not nil" do
+ before do
+ suppress_warning do
+ $, = '*'
+ end
+ end
+
+ it "warns" do
+ -> { [].join }.should complain(/warning: \$, is set to non-nil value/)
+ -> { [].join(nil) }.should complain(/warning: \$, is set to non-nil value/)
+ end
+ end
+end
+
+describe "Array#join with $," do
+ before :each do
+ @before_separator = $,
+ end
+
+ after :each do
+ suppress_warning {$, = @before_separator}
+ end
+
+ it "separates elements with default separator when the passed separator is nil" do
+ suppress_warning {
+ $, = "_"
+ [1, 2, 3].join(nil).should == '1_2_3'
+ }
+ end
+end
diff --git a/spec/ruby/core/array/keep_if_spec.rb b/spec/ruby/core/array/keep_if_spec.rb
new file mode 100644
index 0000000000..62a65a04e8
--- /dev/null
+++ b/spec/ruby/core/array/keep_if_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'shared/keep_if'
+
+describe "Array#keep_if" do
+ it "returns the same array if no changes were made" do
+ array = [1, 2, 3]
+ array.keep_if { true }.should.equal?(array)
+ end
+
+ it_behaves_like :keep_if, :keep_if
+end
diff --git a/spec/ruby/core/array/last_spec.rb b/spec/ruby/core/array/last_spec.rb
new file mode 100644
index 0000000000..ed417bcd2a
--- /dev/null
+++ b/spec/ruby/core/array/last_spec.rb
@@ -0,0 +1,87 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#last" do
+ it "returns the last element" do
+ [1, 1, 1, 1, 2].last.should == 2
+ end
+
+ it "returns nil if self is empty" do
+ [].last.should == nil
+ end
+
+ it "returns the last count elements if given a count" do
+ [1, 2, 3, 4, 5, 9].last(3).should == [4, 5, 9]
+ end
+
+ it "returns an empty array when passed a count on an empty array" do
+ [].last(0).should == []
+ [].last(1).should == []
+ end
+
+ it "returns an empty array when count == 0" do
+ [1, 2, 3, 4, 5].last(0).should == []
+ end
+
+ it "returns an array containing the last element when passed count == 1" do
+ [1, 2, 3, 4, 5].last(1).should == [5]
+ end
+
+ it "raises an ArgumentError when count is negative" do
+ -> { [1, 2].last(-1) }.should.raise(ArgumentError)
+ end
+
+ it "returns the entire array when count > length" do
+ [1, 2, 3, 4, 5, 9].last(10).should == [1, 2, 3, 4, 5, 9]
+ end
+
+ it "returns an array which is independent to the original when passed count" do
+ ary = [1, 2, 3, 4, 5]
+ ary.last(0).replace([1,2])
+ ary.should == [1, 2, 3, 4, 5]
+ ary.last(1).replace([1,2])
+ ary.should == [1, 2, 3, 4, 5]
+ ary.last(6).replace([1,2])
+ ary.should == [1, 2, 3, 4, 5]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.last.should.equal?(empty)
+
+ array = ArraySpecs.recursive_array
+ array.last.should.equal?(array)
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ [1, 2, 3, 4, 5].last(obj).should == [4, 5]
+ end
+
+ it "raises a TypeError if the passed argument is not numeric" do
+ -> { [1,2].last(nil) }.should.raise(TypeError)
+ -> { [1,2].last("a") }.should.raise(TypeError)
+
+ obj = mock("nonnumeric")
+ -> { [1,2].last(obj) }.should.raise(TypeError)
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ArraySpecs::MyArray[].last(0).should.instance_of?(Array)
+ ArraySpecs::MyArray[].last(2).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].last(0).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].last(1).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].last(2).should.instance_of?(Array)
+ end
+
+ it "is not destructive" do
+ a = [1, 2, 3]
+ a.last
+ a.should == [1, 2, 3]
+ a.last(2)
+ a.should == [1, 2, 3]
+ a.last(3)
+ a.should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/array/length_spec.rb b/spec/ruby/core/array/length_spec.rb
new file mode 100644
index 0000000000..74b2eb3a08
--- /dev/null
+++ b/spec/ruby/core/array/length_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#length" do
+ it "returns the number of elements" do
+ [].length.should == 0
+ [1, 2, 3].length.should == 3
+ end
+
+ it "properly handles recursive arrays" do
+ ArraySpecs.empty_recursive_array.length.should == 1
+ ArraySpecs.recursive_array.length.should == 8
+ end
+end
diff --git a/spec/ruby/core/array/map_spec.rb b/spec/ruby/core/array/map_spec.rb
new file mode 100644
index 0000000000..f5e88c8624
--- /dev/null
+++ b/spec/ruby/core/array/map_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Array#map" do
+ it "is an alias of Array#collect" do
+ Array.instance_method(:map).should == Array.instance_method(:collect)
+ end
+end
+
+describe "Array#map!" do
+ it "is an alias of Array#collect!" do
+ Array.instance_method(:map!).should == Array.instance_method(:collect!)
+ end
+end
diff --git a/spec/ruby/core/array/max_spec.rb b/spec/ruby/core/array/max_spec.rb
new file mode 100644
index 0000000000..868275a748
--- /dev/null
+++ b/spec/ruby/core/array/max_spec.rb
@@ -0,0 +1,116 @@
+require_relative '../../spec_helper'
+
+describe "Array#max" do
+ it "is defined on Array" do
+ [1].method(:max).owner.should.equal? Array
+ end
+
+ it "returns nil with no values" do
+ [].max.should == nil
+ end
+
+ it "returns only element in one element array" do
+ [1].max.should == 1
+ end
+
+ it "returns largest value with multiple elements" do
+ [1,2].max.should == 2
+ [2,1].max.should == 2
+ end
+
+ describe "given a block with one argument" do
+ it "yields in turn the last length-1 values from the array" do
+ ary = []
+ result = [1,2,3,4,5].max {|x| ary << x; x}
+
+ ary.should == [2,3,4,5]
+ result.should == 5
+ end
+ end
+end
+
+# From Enumerable#max, copied for better readability
+describe "Array#max" do
+ before :each do
+ @a = [2, 4, 6, 8, 10]
+
+ @e_strs = ["333", "22", "666666", "1", "55555", "1010101010"]
+ @e_ints = [333, 22, 666666, 55555, 1010101010]
+ end
+
+ it "max should return the maximum element" do
+ [18, 42].max.should == 42
+ [2, 5, 3, 6, 1, 4].max.should == 6
+ end
+
+ it "returns the maximum element (basics cases)" do
+ [55].max.should == 55
+
+ [11,99].max.should == 99
+ [99,11].max.should == 99
+ [2, 33, 4, 11].max.should == 33
+
+ [1,2,3,4,5].max.should == 5
+ [5,4,3,2,1].max.should == 5
+ [1,4,3,5,2].max.should == 5
+ [5,5,5,5,5].max.should == 5
+
+ ["aa","tt"].max.should == "tt"
+ ["tt","aa"].max.should == "tt"
+ ["2","33","4","11"].max.should == "4"
+
+ @e_strs.max.should == "666666"
+ @e_ints.max.should == 1010101010
+ end
+
+ it "returns nil for an empty Enumerable" do
+ [].max.should == nil
+ end
+
+ it "raises a NoMethodError for elements without #<=>" do
+ -> do
+ [BasicObject.new, BasicObject.new].max
+ end.should.raise(NoMethodError)
+ end
+
+ it "raises an ArgumentError for incomparable elements" do
+ -> do
+ [11,"22"].max
+ end.should.raise(ArgumentError)
+ -> do
+ [11,12,22,33].max{|a, b| nil}
+ end.should.raise(ArgumentError)
+ end
+
+ it "returns the maximum element (with block)" do
+ # with a block
+ ["2","33","4","11"].max {|a,b| a <=> b }.should == "4"
+ [ 2 , 33 , 4 , 11 ].max {|a,b| a <=> b }.should == 33
+
+ ["2","33","4","11"].max {|a,b| b <=> a }.should == "11"
+ [ 2 , 33 , 4 , 11 ].max {|a,b| b <=> a }.should == 2
+
+ @e_strs.max {|a,b| a.length <=> b.length }.should == "1010101010"
+
+ @e_strs.max {|a,b| a <=> b }.should == "666666"
+ @e_strs.max {|a,b| a.to_i <=> b.to_i }.should == "1010101010"
+
+ @e_ints.max {|a,b| a <=> b }.should == 1010101010
+ @e_ints.max {|a,b| a.to_s <=> b.to_s }.should == 666666
+ end
+
+ it "returns the minimum for enumerables that contain nils" do
+ arr = [nil, nil, true]
+ arr.max { |a, b|
+ x = a.nil? ? 1 : a ? 0 : -1
+ y = b.nil? ? 1 : b ? 0 : -1
+ x <=> y
+ }.should == nil
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = [[1,2], [3,4,5], [6,7,8,9]]
+ multi.max.should == [6, 7, 8, 9]
+ end
+
+end
diff --git a/spec/ruby/core/array/min_spec.rb b/spec/ruby/core/array/min_spec.rb
new file mode 100644
index 0000000000..5913e08cf8
--- /dev/null
+++ b/spec/ruby/core/array/min_spec.rb
@@ -0,0 +1,121 @@
+require_relative '../../spec_helper'
+
+describe "Array#min" do
+ it "is defined on Array" do
+ [1].method(:max).owner.should.equal? Array
+ end
+
+ it "returns nil with no values" do
+ [].min.should == nil
+ end
+
+ it "returns only element in one element array" do
+ [1].min.should == 1
+ end
+
+ it "returns smallest value with multiple elements" do
+ [1,2].min.should == 1
+ [2,1].min.should == 1
+ end
+
+ describe "given a block with one argument" do
+ it "yields in turn the last length-1 values from the array" do
+ ary = []
+ result = [1,2,3,4,5].min {|x| ary << x; x}
+
+ ary.should == [2,3,4,5]
+ result.should == 1
+ end
+ end
+end
+
+# From Enumerable#min, copied for better readability
+describe "Array#min" do
+ before :each do
+ @a = [2, 4, 6, 8, 10]
+
+ @e_strs = ["333", "22", "666666", "1", "55555", "1010101010"]
+ @e_ints = [ 333, 22, 666666, 55555, 1010101010]
+ end
+
+ it "min should return the minimum element" do
+ [18, 42].min.should == 18
+ [2, 5, 3, 6, 1, 4].min.should == 1
+ end
+
+ it "returns the minimum (basic cases)" do
+ [55].min.should == 55
+
+ [11,99].min.should == 11
+ [99,11].min.should == 11
+ [2, 33, 4, 11].min.should == 2
+
+ [1,2,3,4,5].min.should == 1
+ [5,4,3,2,1].min.should == 1
+ [4,1,3,5,2].min.should == 1
+ [5,5,5,5,5].min.should == 5
+
+ ["aa","tt"].min.should == "aa"
+ ["tt","aa"].min.should == "aa"
+ ["2","33","4","11"].min.should == "11"
+
+ @e_strs.min.should == "1"
+ @e_ints.min.should == 22
+ end
+
+ it "returns nil for an empty Enumerable" do
+ [].min.should == nil
+ end
+
+ it "raises a NoMethodError for elements without #<=>" do
+ -> do
+ [BasicObject.new, BasicObject.new].min
+ end.should.raise(NoMethodError)
+ end
+
+ it "raises an ArgumentError for incomparable elements" do
+ -> do
+ [11,"22"].min
+ end.should.raise(ArgumentError)
+ -> do
+ [11,12,22,33].min{|a, b| nil}
+ end.should.raise(ArgumentError)
+ end
+
+ it "returns the minimum when using a block rule" do
+ ["2","33","4","11"].min {|a,b| a <=> b }.should == "11"
+ [ 2 , 33 , 4 , 11 ].min {|a,b| a <=> b }.should == 2
+
+ ["2","33","4","11"].min {|a,b| b <=> a }.should == "4"
+ [ 2 , 33 , 4 , 11 ].min {|a,b| b <=> a }.should == 33
+
+ [ 1, 2, 3, 4 ].min {|a,b| 15 }.should == 1
+
+ [11,12,22,33].min{|a, b| 2 }.should == 11
+ @i = -2
+ [11,12,22,33].min{|a, b| @i += 1 }.should == 12
+
+ @e_strs.min {|a,b| a.length <=> b.length }.should == "1"
+
+ @e_strs.min {|a,b| a <=> b }.should == "1"
+ @e_strs.min {|a,b| a.to_i <=> b.to_i }.should == "1"
+
+ @e_ints.min {|a,b| a <=> b }.should == 22
+ @e_ints.min {|a,b| a.to_s <=> b.to_s }.should == 1010101010
+ end
+
+ it "returns the minimum for enumerables that contain nils" do
+ arr = [nil, nil, true]
+ arr.min { |a, b|
+ x = a.nil? ? -1 : a ? 0 : 1
+ y = b.nil? ? -1 : b ? 0 : 1
+ x <=> y
+ }.should == nil
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = [[1,2], [3,4,5], [6,7,8,9]]
+ multi.min.should == [1, 2]
+ end
+
+end
diff --git a/spec/ruby/core/array/minmax_spec.rb b/spec/ruby/core/array/minmax_spec.rb
new file mode 100644
index 0000000000..e11fe63347
--- /dev/null
+++ b/spec/ruby/core/array/minmax_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/enumerable/minmax'
+
+describe "Array#minmax" do
+ before :each do
+ @enum = [6, 4, 5, 10, 8]
+ @empty_enum = []
+ @incomparable_enum = [BasicObject.new, BasicObject.new]
+ @incompatible_enum = [11, "22"]
+ @strs = ["333", "2", "60", "55555", "1010", "111"]
+ end
+
+ it_behaves_like :enumerable_minmax, :minmax
+end
diff --git a/spec/ruby/core/array/minus_spec.rb b/spec/ruby/core/array/minus_spec.rb
new file mode 100644
index 0000000000..cb1bf56d76
--- /dev/null
+++ b/spec/ruby/core/array/minus_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/difference'
+
+describe "Array#-" do
+ it_behaves_like :array_binary_difference, :-
+end
diff --git a/spec/ruby/core/array/multiply_spec.rb b/spec/ruby/core/array/multiply_spec.rb
new file mode 100644
index 0000000000..1ac14e1b09
--- /dev/null
+++ b/spec/ruby/core/array/multiply_spec.rb
@@ -0,0 +1,94 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/join'
+
+describe "Array#*" do
+ it "tries to convert the passed argument to a String using #to_str" do
+ obj = mock('separator')
+ obj.should_receive(:to_str).and_return('::')
+ ([1, 2, 3, 4] * obj).should == '1::2::3::4'
+ end
+
+ it "tires to convert the passed argument to an Integer using #to_int" do
+ obj = mock('count')
+ obj.should_receive(:to_int).and_return(2)
+ ([1, 2, 3, 4] * obj).should == [1, 2, 3, 4, 1, 2, 3, 4]
+ end
+
+ it "raises a TypeError if the argument can neither be converted to a string nor an integer" do
+ obj = mock('not a string or integer')
+ ->{ [1,2] * obj }.should.raise(TypeError)
+ end
+
+ it "converts the passed argument to a String rather than an Integer" do
+ obj = mock('2')
+ def obj.to_int() 2 end
+ def obj.to_str() "2" end
+ ([:a, :b, :c] * obj).should == "a2b2c"
+ end
+
+ it "raises a TypeError is the passed argument is nil" do
+ ->{ [1,2] * nil }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when passed 2 or more arguments" do
+ ->{ [1,2].send(:*, 1, 2) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed no arguments" do
+ ->{ [1,2].send(:*) }.should.raise(ArgumentError)
+ end
+end
+
+describe "Array#* with an integer" do
+ it "concatenates n copies of the array when passed an integer" do
+ ([ 1, 2, 3 ] * 0).should == []
+ ([ 1, 2, 3 ] * 1).should == [1, 2, 3]
+ ([ 1, 2, 3 ] * 3).should == [1, 2, 3, 1, 2, 3, 1, 2, 3]
+ ([] * 10).should == []
+ end
+
+ it "does not return self even if the passed integer is 1" do
+ ary = [1, 2, 3]
+ (ary * 1).should_not.equal?(ary)
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ (empty * 0).should == []
+ (empty * 1).should == empty
+ (empty * 3).should == [empty, empty, empty]
+
+ array = ArraySpecs.recursive_array
+ (array * 0).should == []
+ (array * 1).should == array
+ end
+
+ it "raises an ArgumentError when passed a negative integer" do
+ -> { [ 1, 2, 3 ] * -1 }.should.raise(ArgumentError)
+ -> { [] * -1 }.should.raise(ArgumentError)
+ end
+
+ describe "with a subclass of Array" do
+ before :each do
+ ScratchPad.clear
+
+ @array = ArraySpecs::MyArray[1, 2, 3, 4, 5]
+ end
+
+ it "returns an Array instance" do
+ (@array * 0).should.instance_of?(Array)
+ (@array * 1).should.instance_of?(Array)
+ (@array * 2).should.instance_of?(Array)
+ end
+
+ it "does not call #initialize on the subclass instance" do
+ (@array * 2).should == [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
+ ScratchPad.recorded.should == nil
+ end
+ end
+end
+
+describe "Array#* with a string" do
+ it_behaves_like :array_join_with_string_separator, :*
+end
diff --git a/spec/ruby/core/array/new_spec.rb b/spec/ruby/core/array/new_spec.rb
new file mode 100644
index 0000000000..b2f23e2f6b
--- /dev/null
+++ b/spec/ruby/core/array/new_spec.rb
@@ -0,0 +1,124 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array.new" do
+ it "returns an instance of Array" do
+ Array.new.should.instance_of?(Array)
+ end
+
+ it "returns an instance of a subclass" do
+ ArraySpecs::MyArray.new(1, 2).should.instance_of?(ArraySpecs::MyArray)
+ end
+
+ it "raises an ArgumentError if passed 3 or more arguments" do
+ -> do
+ [1, 2].send :initialize, 1, 'x', true
+ end.should.raise(ArgumentError)
+ -> do
+ [1, 2].send(:initialize, 1, 'x', true) {}
+ end.should.raise(ArgumentError)
+ end
+end
+
+describe "Array.new with no arguments" do
+ it "returns an empty array" do
+ Array.new.should.empty?
+ end
+
+ it "does not use the given block" do
+ -> {
+ -> { Array.new { raise } }.should_not.raise
+ }.should complain(/warning: given block not used/, verbose: true)
+ end
+end
+
+describe "Array.new with (array)" do
+ it "returns an array initialized to the other array" do
+ b = [4, 5, 6]
+ Array.new(b).should == b
+ end
+
+ it "does not use the given block" do
+ ->{ Array.new([1, 2]) { raise } }.should_not.raise
+ end
+
+ it "calls #to_ary to convert the value to an array" do
+ a = mock("array")
+ a.should_receive(:to_ary).and_return([1, 2])
+ a.should_not_receive(:to_int)
+ Array.new(a).should == [1, 2]
+ end
+
+ it "does not call #to_ary on instances of Array or subclasses of Array" do
+ a = [1, 2]
+ a.should_not_receive(:to_ary)
+ Array.new(a)
+ end
+
+ it "raises a TypeError if an Array type argument and a default object" do
+ -> { Array.new([1, 2], 1) }.should.raise(TypeError)
+ end
+end
+
+describe "Array.new with (size, object=nil)" do
+ it "returns an array of size filled with object" do
+ obj = [3]
+ a = Array.new(2, obj)
+ a.should == [obj, obj]
+ a[0].should.equal?(obj)
+ a[1].should.equal?(obj)
+
+ Array.new(3, 14).should == [14, 14, 14]
+ end
+
+ it "returns an array of size filled with nil when object is omitted" do
+ Array.new(3).should == [nil, nil, nil]
+ end
+
+ it "raises an ArgumentError if size is negative" do
+ -> { Array.new(-1, :a) }.should.raise(ArgumentError)
+ -> { Array.new(-1) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if size is too large" do
+ -> { Array.new(fixnum_max+1) }.should.raise(ArgumentError)
+ end
+
+ it "calls #to_int to convert the size argument to an Integer when object is given" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ Array.new(obj, :a).should == [:a]
+ end
+
+ it "calls #to_int to convert the size argument to an Integer when object is not given" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ Array.new(obj).should == [nil]
+ end
+
+ it "raises a TypeError if the size argument is not an Integer type" do
+ obj = mock('nonnumeric')
+ obj.stub!(:to_ary).and_return([1, 2])
+ ->{ Array.new(obj, :a) }.should.raise(TypeError)
+ end
+
+ it "yields the index of the element and sets the element to the value of the block" do
+ Array.new(3) { |i| i.to_s }.should == ['0', '1', '2']
+ end
+
+ it "uses the block value instead of using the default value" do
+ -> {
+ @result = Array.new(3, :obj) { |i| i.to_s }
+ }.should complain(/block supersedes default value argument/)
+ @result.should == ['0', '1', '2']
+ end
+
+ it "returns the value passed to break" do
+ a = Array.new(3) do |i|
+ break if i == 2
+ i.to_s
+ end
+
+ a.should == nil
+ end
+end
diff --git a/spec/ruby/core/array/none_spec.rb b/spec/ruby/core/array/none_spec.rb
new file mode 100644
index 0000000000..31cd8c46d6
--- /dev/null
+++ b/spec/ruby/core/array/none_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#none?" do
+ @value_to_return = -> _ { false }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :none?
+
+ it "ignores the block if there is an argument" do
+ -> {
+ ['bar', 'foobar'].none?(/baz/) { true }.should == true
+ }.should complain(/given block not used/)
+ end
+end
diff --git a/spec/ruby/core/array/one_spec.rb b/spec/ruby/core/array/one_spec.rb
new file mode 100644
index 0000000000..0c61907881
--- /dev/null
+++ b/spec/ruby/core/array/one_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#one?" do
+ @value_to_return = -> _ { false }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :one?
+
+ it "ignores the block if there is an argument" do
+ -> {
+ ['bar', 'foobar'].one?(/foo/) { false }.should == true
+ }.should complain(/given block not used/)
+ end
+end
diff --git a/spec/ruby/core/array/pack/a_spec.rb b/spec/ruby/core/array/pack/a_spec.rb
new file mode 100644
index 0000000000..03bfd8214c
--- /dev/null
+++ b/spec/ruby/core/array/pack/a_spec.rb
@@ -0,0 +1,73 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/string'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'A'" do
+ it_behaves_like :array_pack_basic, 'A'
+ it_behaves_like :array_pack_basic_non_float, 'A'
+ it_behaves_like :array_pack_no_platform, 'A'
+ it_behaves_like :array_pack_string, 'A'
+ it_behaves_like :array_pack_taint, 'A'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack A string")
+ obj.should_receive(:to_str).and_return("``abcdef")
+ [obj].pack("A*").should == "``abcdef"
+ end
+
+ it "will not implicitly convert a number to a string" do
+ -> { [0].pack('A') }.should.raise(TypeError)
+ -> { [0].pack('a') }.should.raise(TypeError)
+ end
+
+ it "adds all the bytes to the output when passed the '*' modifier" do
+ ["abc"].pack("A*").should == "abc"
+ end
+
+ it "pads the output with spaces when the count exceeds the size of the String" do
+ ["abc"].pack("A6").should == "abc "
+ end
+
+ it "adds a space when the value is nil" do
+ [nil].pack("A").should == " "
+ end
+
+ it "pads the output with spaces when the value is nil" do
+ [nil].pack("A3").should == " "
+ end
+
+ it "does not pad with spaces when passed the '*' modifier and the value is nil" do
+ [nil].pack("A*").should == ""
+ end
+end
+
+describe "Array#pack with format 'a'" do
+ it_behaves_like :array_pack_basic, 'a'
+ it_behaves_like :array_pack_basic_non_float, 'a'
+ it_behaves_like :array_pack_no_platform, 'a'
+ it_behaves_like :array_pack_string, 'a'
+ it_behaves_like :array_pack_taint, 'a'
+
+ it "adds all the bytes to the output when passed the '*' modifier" do
+ ["abc"].pack("a*").should == "abc"
+ end
+
+ it "pads the output with NULL bytes when the count exceeds the size of the String" do
+ ["abc"].pack("a6").should == "abc\x00\x00\x00"
+ end
+
+ it "adds a NULL byte when the value is nil" do
+ [nil].pack("a").should == "\x00"
+ end
+
+ it "pads the output with NULL bytes when the value is nil" do
+ [nil].pack("a3").should == "\x00\x00\x00"
+ end
+
+ it "does not pad with NULL bytes when passed the '*' modifier and the value is nil" do
+ [nil].pack("a*").should == ""
+ end
+end
diff --git a/spec/ruby/core/array/pack/at_spec.rb b/spec/ruby/core/array/pack/at_spec.rb
new file mode 100644
index 0000000000..bb9801440a
--- /dev/null
+++ b/spec/ruby/core/array/pack/at_spec.rb
@@ -0,0 +1,30 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+describe "Array#pack with format '@'" do
+ it_behaves_like :array_pack_basic, '@'
+ it_behaves_like :array_pack_basic_non_float, '@'
+ it_behaves_like :array_pack_no_platform, '@'
+
+ it "moves the insertion point to the index specified by the count modifier" do
+ [1, 2, 3, 4, 5].pack("C4@2C").should == "\x01\x02\x05"
+ end
+
+ it "does not consume any elements" do
+ [1, 2, 3].pack("C@3C").should == "\x01\x00\x00\x02"
+ end
+
+ it "extends the string with NULL bytes if the string size is less than the count" do
+ [1, 2, 3].pack("@3C*").should == "\x00\x00\x00\x01\x02\x03"
+ end
+
+ it "truncates the string if the string size is greater than the count" do
+ [1, 2, 3].pack("Cx5@2C").should == "\x01\x00\x02"
+ end
+
+ it "implicitly has a count of one when no count modifier is passed" do
+ [1, 2, 3].pack("C*@").should == "\x01"
+ end
+end
diff --git a/spec/ruby/core/array/pack/b_spec.rb b/spec/ruby/core/array/pack/b_spec.rb
new file mode 100644
index 0000000000..f7576846ef
--- /dev/null
+++ b/spec/ruby/core/array/pack/b_spec.rb
@@ -0,0 +1,113 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/encodings'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'B'" do
+ it_behaves_like :array_pack_basic, 'B'
+ it_behaves_like :array_pack_basic_non_float, 'B'
+ it_behaves_like :array_pack_arguments, 'B'
+ it_behaves_like :array_pack_hex, 'B'
+ it_behaves_like :array_pack_taint, 'B'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack B string")
+ obj.should_receive(:to_str).and_return("``abcdef")
+ [obj].pack("B*").should == "\x2a"
+ end
+
+ it "will not implicitly convert a number to a string" do
+ -> { [0].pack('B') }.should.raise(TypeError)
+ -> { [0].pack('b') }.should.raise(TypeError)
+ end
+
+ it "encodes one bit for each character starting with the most significant bit" do
+ [ [["0"], "\x00"],
+ [["1"], "\x80"]
+ ].should be_computed_by(:pack, "B")
+ end
+
+ it "implicitly has a count of one when not passed a count modifier" do
+ ["1"].pack("B").should == "\x80"
+ end
+
+ it "implicitly has count equal to the string length when passed the '*' modifier" do
+ [ [["00101010"], "\x2a"],
+ [["00000000"], "\x00"],
+ [["11111111"], "\xff"],
+ [["10000000"], "\x80"],
+ [["00000001"], "\x01"]
+ ].should be_computed_by(:pack, "B*")
+ end
+
+ it "encodes the least significant bit of a character other than 0 or 1" do
+ [ [["bbababab"], "\x2a"],
+ [["^&#&#^#^"], "\x2a"],
+ [["(()()()("], "\x2a"],
+ [["@@%@%@%@"], "\x2a"],
+ [["ppqrstuv"], "\x2a"],
+ [["rqtvtrqp"], "\x42"]
+ ].should be_computed_by(:pack, "B*")
+ end
+
+ it "returns a binary string" do
+ ["1"].pack("B").encoding.should == Encoding::BINARY
+ end
+
+ it "encodes the string as a sequence of bytes" do
+ ["ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚"].pack("B*").should == "\xdbm\xb6"
+ end
+end
+
+describe "Array#pack with format 'b'" do
+ it_behaves_like :array_pack_basic, 'b'
+ it_behaves_like :array_pack_basic_non_float, 'b'
+ it_behaves_like :array_pack_arguments, 'b'
+ it_behaves_like :array_pack_hex, 'b'
+ it_behaves_like :array_pack_taint, 'b'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack H string")
+ obj.should_receive(:to_str).and_return("`abcdef`")
+ [obj].pack("b*").should == "\x2a"
+ end
+
+ it "encodes one bit for each character starting with the least significant bit" do
+ [ [["0"], "\x00"],
+ [["1"], "\x01"]
+ ].should be_computed_by(:pack, "b")
+ end
+
+ it "implicitly has a count of one when not passed a count modifier" do
+ ["1"].pack("b").should == "\x01"
+ end
+
+ it "implicitly has count equal to the string length when passed the '*' modifier" do
+ [ [["0101010"], "\x2a"],
+ [["00000000"], "\x00"],
+ [["11111111"], "\xff"],
+ [["10000000"], "\x01"],
+ [["00000001"], "\x80"]
+ ].should be_computed_by(:pack, "b*")
+ end
+
+ it "encodes the least significant bit of a character other than 0 or 1" do
+ [ [["bababab"], "\x2a"],
+ [["&#&#^#^"], "\x2a"],
+ [["()()()("], "\x2a"],
+ [["@%@%@%@"], "\x2a"],
+ [["pqrstuv"], "\x2a"],
+ [["qrtrtvs"], "\x41"]
+ ].should be_computed_by(:pack, "b*")
+ end
+
+ it "returns a binary string" do
+ ["1"].pack("b").encoding.should == Encoding::BINARY
+ end
+
+ it "encodes the string as a sequence of bytes" do
+ ["ã‚ã‚ã‚ã‚ã‚ã‚ã‚ã‚"].pack("b*").should == "\xdb\xb6m"
+ end
+end
diff --git a/spec/ruby/core/array/pack/buffer_spec.rb b/spec/ruby/core/array/pack/buffer_spec.rb
new file mode 100644
index 0000000000..d104c80186
--- /dev/null
+++ b/spec/ruby/core/array/pack/buffer_spec.rb
@@ -0,0 +1,60 @@
+# encoding: binary
+
+require_relative '../../../spec_helper'
+
+describe "Array#pack with :buffer option" do
+ it "returns specified buffer" do
+ n = [ 65, 66, 67 ]
+ buffer = " "*3
+ result = n.pack("ccc", buffer: buffer) #=> "ABC"
+ result.should.equal?(buffer)
+ end
+
+ it "adds result at the end of buffer content" do
+ n = [ 65, 66, 67 ] # result without buffer is "ABC"
+
+ buffer = +""
+ n.pack("ccc", buffer: buffer).should == "ABC"
+
+ buffer = +"123"
+ n.pack("ccc", buffer: buffer).should == "123ABC"
+
+ buffer = +"12345"
+ n.pack("ccc", buffer: buffer).should == "12345ABC"
+ end
+
+ it "raises TypeError exception if buffer is not String" do
+ -> { [65].pack("ccc", buffer: []) }.should.raise(
+ TypeError, "buffer must be String, not Array")
+ end
+
+ it "raise FrozenError if buffer is frozen" do
+ -> { [65].pack("c", buffer: "frozen-string".freeze) }.should.raise(FrozenError)
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ [65, 66, 67].pack("ccc", buffer: buffer)
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
+
+ context "offset (@) is specified" do
+ it 'keeps buffer content if it is longer than offset' do
+ n = [ 65, 66, 67 ]
+ buffer = +"123456"
+ n.pack("@3ccc", buffer: buffer).should == "123ABC"
+ end
+
+ it "fills the gap with \\0 if buffer content is shorter than offset" do
+ n = [ 65, 66, 67 ]
+ buffer = +"123"
+ n.pack("@6ccc", buffer: buffer).should == "123\0\0\0ABC"
+ end
+
+ it 'does not keep buffer content if it is longer than offset + result' do
+ n = [ 65, 66, 67 ]
+ buffer = +"1234567890"
+ n.pack("@3ccc", buffer: buffer).should == "123ABC"
+ end
+ end
+end
diff --git a/spec/ruby/core/array/pack/c_spec.rb b/spec/ruby/core/array/pack/c_spec.rb
new file mode 100644
index 0000000000..de06207a23
--- /dev/null
+++ b/spec/ruby/core/array/pack/c_spec.rb
@@ -0,0 +1,77 @@
+# encoding: binary
+
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+
+describe :array_pack_8bit, shared: true do
+ it "encodes the least significant eight bits of a positive number" do
+ [ [[49], "1"],
+ [[0b11111111], "\xFF"],
+ [[0b100000000], "\x00"],
+ [[0b100000001], "\x01"]
+ ].should be_computed_by(:pack, pack_format)
+ end
+
+ it "encodes the least significant eight bits of a negative number" do
+ [ [[-1], "\xFF"],
+ [[-0b10000000], "\x80"],
+ [[-0b11111111], "\x01"],
+ [[-0b100000000], "\x00"],
+ [[-0b100000001], "\xFF"]
+ ].should be_computed_by(:pack, pack_format)
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[5.2], "\x05"],
+ [[5.8], "\x05"]
+ ].should be_computed_by(:pack, pack_format)
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(5)
+ [obj].pack(pack_format).should == "\x05"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [ [[1, 2, 3], pack_format(3), "\x01\x02\x03"],
+ [[1, 2, 3], pack_format(2) + pack_format(1), "\x01\x02\x03"]
+ ].should be_computed_by(:pack)
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [1, 2, 3, 4, 5].pack(pack_format('*')).should == "\x01\x02\x03\x04\x05"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [1, 2, 3].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [1, 2, 3].pack(pack_format(' ', 2)).should == "\x01\x02"
+ end
+end
+
+describe "Array#pack with format 'C'" do
+ it_behaves_like :array_pack_basic, 'C'
+ it_behaves_like :array_pack_basic_non_float, 'C'
+ it_behaves_like :array_pack_8bit, 'C'
+ it_behaves_like :array_pack_arguments, 'C'
+ it_behaves_like :array_pack_numeric_basic, 'C'
+ it_behaves_like :array_pack_integer, 'C'
+ it_behaves_like :array_pack_no_platform, 'C'
+end
+
+describe "Array#pack with format 'c'" do
+ it_behaves_like :array_pack_basic, 'c'
+ it_behaves_like :array_pack_basic_non_float, 'c'
+ it_behaves_like :array_pack_8bit, 'c'
+ it_behaves_like :array_pack_arguments, 'c'
+ it_behaves_like :array_pack_numeric_basic, 'c'
+ it_behaves_like :array_pack_integer, 'c'
+ it_behaves_like :array_pack_no_platform, 'c'
+end
diff --git a/spec/ruby/core/array/pack/comment_spec.rb b/spec/ruby/core/array/pack/comment_spec.rb
new file mode 100644
index 0000000000..daf1cff06a
--- /dev/null
+++ b/spec/ruby/core/array/pack/comment_spec.rb
@@ -0,0 +1,25 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe "Array#pack" do
+ it "ignores directives text from '#' to the first newline" do
+ [1, 2, 3].pack("c#this is a comment\nc").should == "\x01\x02"
+ end
+
+ it "ignores directives text from '#' to the end if no newline is present" do
+ [1, 2, 3].pack("c#this is a comment c").should == "\x01"
+ end
+
+ it "ignores comments at the start of the directives string" do
+ [1, 2, 3].pack("#this is a comment\nc").should == "\x01"
+ end
+
+ it "ignores the entire directive string if it is a comment" do
+ [1, 2, 3].pack("#this is a comment").should == ""
+ end
+
+ it "ignores multiple comments" do
+ [1, 2, 3].pack("c#comment\nc#comment\nc#c").should == "\x01\x02\x03"
+ end
+end
diff --git a/spec/ruby/core/array/pack/d_spec.rb b/spec/ruby/core/array/pack/d_spec.rb
new file mode 100644
index 0000000000..8bb3654633
--- /dev/null
+++ b/spec/ruby/core/array/pack/d_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/float'
+
+describe "Array#pack with format 'D'" do
+ it_behaves_like :array_pack_basic, 'D'
+ it_behaves_like :array_pack_basic_float, 'D'
+ it_behaves_like :array_pack_arguments, 'D'
+ it_behaves_like :array_pack_no_platform, 'D'
+ it_behaves_like :array_pack_numeric_basic, 'D'
+ it_behaves_like :array_pack_float, 'D'
+
+ little_endian do
+ it_behaves_like :array_pack_double_le, 'D'
+ end
+
+ big_endian do
+ it_behaves_like :array_pack_double_be, 'D'
+ end
+end
+
+describe "Array#pack with format 'd'" do
+ it_behaves_like :array_pack_basic, 'd'
+ it_behaves_like :array_pack_basic_float, 'd'
+ it_behaves_like :array_pack_arguments, 'd'
+ it_behaves_like :array_pack_no_platform, 'd'
+ it_behaves_like :array_pack_numeric_basic, 'd'
+ it_behaves_like :array_pack_float, 'd'
+
+ little_endian do
+ it_behaves_like :array_pack_double_le, 'd'
+ end
+
+ big_endian do
+ it_behaves_like :array_pack_double_be, 'd'
+ end
+end
diff --git a/spec/ruby/core/array/pack/e_spec.rb b/spec/ruby/core/array/pack/e_spec.rb
new file mode 100644
index 0000000000..ab61ef578f
--- /dev/null
+++ b/spec/ruby/core/array/pack/e_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/float'
+
+describe "Array#pack with format 'E'" do
+ it_behaves_like :array_pack_basic, 'E'
+ it_behaves_like :array_pack_basic_float, 'E'
+ it_behaves_like :array_pack_arguments, 'E'
+ it_behaves_like :array_pack_no_platform, 'E'
+ it_behaves_like :array_pack_numeric_basic, 'E'
+ it_behaves_like :array_pack_float, 'E'
+ it_behaves_like :array_pack_double_le, 'E'
+end
+
+describe "Array#pack with format 'e'" do
+ it_behaves_like :array_pack_basic, 'e'
+ it_behaves_like :array_pack_basic_float, 'e'
+ it_behaves_like :array_pack_arguments, 'e'
+ it_behaves_like :array_pack_no_platform, 'e'
+ it_behaves_like :array_pack_numeric_basic, 'e'
+ it_behaves_like :array_pack_float, 'e'
+ it_behaves_like :array_pack_float_le, 'e'
+end
diff --git a/spec/ruby/core/array/pack/empty_spec.rb b/spec/ruby/core/array/pack/empty_spec.rb
new file mode 100644
index 0000000000..d635d6a563
--- /dev/null
+++ b/spec/ruby/core/array/pack/empty_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+
+describe "Array#pack with empty format" do
+ it "returns an empty String" do
+ [1, 2, 3].pack("").should == ""
+ end
+
+ it "returns a String with US-ASCII encoding" do
+ [1, 2, 3].pack("").encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/array/pack/f_spec.rb b/spec/ruby/core/array/pack/f_spec.rb
new file mode 100644
index 0000000000..d436e0787c
--- /dev/null
+++ b/spec/ruby/core/array/pack/f_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/float'
+
+describe "Array#pack with format 'F'" do
+ it_behaves_like :array_pack_basic, 'F'
+ it_behaves_like :array_pack_basic_float, 'F'
+ it_behaves_like :array_pack_arguments, 'F'
+ it_behaves_like :array_pack_no_platform, 'F'
+ it_behaves_like :array_pack_numeric_basic, 'F'
+ it_behaves_like :array_pack_float, 'F'
+
+ little_endian do
+ it_behaves_like :array_pack_float_le, 'F'
+ end
+
+ big_endian do
+ it_behaves_like :array_pack_float_be, 'F'
+ end
+end
+
+describe "Array#pack with format 'f'" do
+ it_behaves_like :array_pack_basic, 'f'
+ it_behaves_like :array_pack_basic_float, 'f'
+ it_behaves_like :array_pack_arguments, 'f'
+ it_behaves_like :array_pack_no_platform, 'f'
+ it_behaves_like :array_pack_numeric_basic, 'f'
+ it_behaves_like :array_pack_float, 'f'
+
+ little_endian do
+ it_behaves_like :array_pack_float_le, 'f'
+ end
+
+ big_endian do
+ it_behaves_like :array_pack_float_be, 'f'
+ end
+end
diff --git a/spec/ruby/core/array/pack/g_spec.rb b/spec/ruby/core/array/pack/g_spec.rb
new file mode 100644
index 0000000000..83b7f81acc
--- /dev/null
+++ b/spec/ruby/core/array/pack/g_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/float'
+
+describe "Array#pack with format 'G'" do
+ it_behaves_like :array_pack_basic, 'G'
+ it_behaves_like :array_pack_basic_float, 'G'
+ it_behaves_like :array_pack_arguments, 'G'
+ it_behaves_like :array_pack_no_platform, 'G'
+ it_behaves_like :array_pack_numeric_basic, 'G'
+ it_behaves_like :array_pack_float, 'G'
+ it_behaves_like :array_pack_double_be, 'G'
+end
+
+describe "Array#pack with format 'g'" do
+ it_behaves_like :array_pack_basic, 'g'
+ it_behaves_like :array_pack_basic_float, 'g'
+ it_behaves_like :array_pack_arguments, 'g'
+ it_behaves_like :array_pack_no_platform, 'g'
+ it_behaves_like :array_pack_numeric_basic, 'g'
+ it_behaves_like :array_pack_float, 'g'
+ it_behaves_like :array_pack_float_be, 'g'
+end
diff --git a/spec/ruby/core/array/pack/h_spec.rb b/spec/ruby/core/array/pack/h_spec.rb
new file mode 100644
index 0000000000..1492d02b1f
--- /dev/null
+++ b/spec/ruby/core/array/pack/h_spec.rb
@@ -0,0 +1,205 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/encodings'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'H'" do
+ it_behaves_like :array_pack_basic, 'H'
+ it_behaves_like :array_pack_basic_non_float, 'H'
+ it_behaves_like :array_pack_arguments, 'H'
+ it_behaves_like :array_pack_hex, 'H'
+ it_behaves_like :array_pack_taint, 'H'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack H string")
+ obj.should_receive(:to_str).and_return("a")
+ [obj].pack("H").should == "\xa0"
+ end
+
+ it "will not implicitly convert a number to a string" do
+ -> { [0].pack('H') }.should.raise(TypeError)
+ -> { [0].pack('h') }.should.raise(TypeError)
+ end
+
+ it "encodes the first character as the most significant nibble when passed no count modifier" do
+ ["ab"].pack("H").should == "\xa0"
+ end
+
+ it "implicitly has count equal to the string length when passed the '*' modifier" do
+ ["deadbeef"].pack("H*").should == "\xde\xad\xbe\xef"
+ end
+
+ it "encodes count nibbles when passed a count modifier exceeding the string length" do
+ ["ab"].pack('H8').should == "\xab\x00\x00\x00"
+ end
+
+ it "encodes the first character as the most significant nibble of a hex value" do
+ [ [["0"], "\x00"],
+ [["1"], "\x10"],
+ [["2"], "\x20"],
+ [["3"], "\x30"],
+ [["4"], "\x40"],
+ [["5"], "\x50"],
+ [["6"], "\x60"],
+ [["7"], "\x70"],
+ [["8"], "\x80"],
+ [["9"], "\x90"],
+ [["a"], "\xa0"],
+ [["b"], "\xb0"],
+ [["c"], "\xc0"],
+ [["d"], "\xd0"],
+ [["e"], "\xe0"],
+ [["f"], "\xf0"],
+ [["A"], "\xa0"],
+ [["B"], "\xb0"],
+ [["C"], "\xc0"],
+ [["D"], "\xd0"],
+ [["E"], "\xe0"],
+ [["F"], "\xf0"]
+ ].should be_computed_by(:pack, "H")
+ end
+
+ it "encodes the second character as the least significant nibble of a hex value" do
+ [ [["00"], "\x00"],
+ [["01"], "\x01"],
+ [["02"], "\x02"],
+ [["03"], "\x03"],
+ [["04"], "\x04"],
+ [["05"], "\x05"],
+ [["06"], "\x06"],
+ [["07"], "\x07"],
+ [["08"], "\x08"],
+ [["09"], "\x09"],
+ [["0a"], "\x0a"],
+ [["0b"], "\x0b"],
+ [["0c"], "\x0c"],
+ [["0d"], "\x0d"],
+ [["0e"], "\x0e"],
+ [["0f"], "\x0f"],
+ [["0A"], "\x0a"],
+ [["0B"], "\x0b"],
+ [["0C"], "\x0c"],
+ [["0D"], "\x0d"],
+ [["0E"], "\x0e"],
+ [["0F"], "\x0f"]
+ ].should be_computed_by(:pack, "H2")
+ end
+
+ it "encodes the least significant nibble of a non alphanumeric character as the most significant nibble of the hex value" do
+ [ [["^"], "\xe0"],
+ [["*"], "\xa0"],
+ [["#"], "\x30"],
+ [["["], "\xb0"],
+ [["]"], "\xd0"],
+ [["@"], "\x00"],
+ [["!"], "\x10"],
+ [["H"], "\x10"],
+ [["O"], "\x80"],
+ [["T"], "\xd0"],
+ [["Z"], "\x30"],
+ ].should be_computed_by(:pack, "H")
+ end
+
+ it "returns a binary string" do
+ ["41"].pack("H").encoding.should == Encoding::BINARY
+ end
+end
+
+describe "Array#pack with format 'h'" do
+ it_behaves_like :array_pack_basic, 'h'
+ it_behaves_like :array_pack_basic_non_float, 'h'
+ it_behaves_like :array_pack_arguments, 'h'
+ it_behaves_like :array_pack_hex, 'h'
+ it_behaves_like :array_pack_taint, 'h'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack H string")
+ obj.should_receive(:to_str).and_return("a")
+ [obj].pack("h").should == "\x0a"
+ end
+
+ it "encodes the first character as the least significant nibble when passed no count modifier" do
+ ["ab"].pack("h").should == "\x0a"
+ end
+
+ it "implicitly has count equal to the string length when passed the '*' modifier" do
+ ["deadbeef"].pack("h*").should == "\xed\xda\xeb\xfe"
+ end
+
+ it "encodes count nibbles when passed a count modifier exceeding the string length" do
+ ["ab"].pack('h8').should == "\xba\x00\x00\x00"
+ end
+
+ it "encodes the first character as the least significant nibble of a hex value" do
+ [ [["0"], "\x00"],
+ [["1"], "\x01"],
+ [["2"], "\x02"],
+ [["3"], "\x03"],
+ [["4"], "\x04"],
+ [["5"], "\x05"],
+ [["6"], "\x06"],
+ [["7"], "\x07"],
+ [["8"], "\x08"],
+ [["9"], "\x09"],
+ [["a"], "\x0a"],
+ [["b"], "\x0b"],
+ [["c"], "\x0c"],
+ [["d"], "\x0d"],
+ [["e"], "\x0e"],
+ [["f"], "\x0f"],
+ [["A"], "\x0a"],
+ [["B"], "\x0b"],
+ [["C"], "\x0c"],
+ [["D"], "\x0d"],
+ [["E"], "\x0e"],
+ [["F"], "\x0f"]
+ ].should be_computed_by(:pack, "h")
+ end
+
+ it "encodes the second character as the most significant nibble of a hex value" do
+ [ [["00"], "\x00"],
+ [["01"], "\x10"],
+ [["02"], "\x20"],
+ [["03"], "\x30"],
+ [["04"], "\x40"],
+ [["05"], "\x50"],
+ [["06"], "\x60"],
+ [["07"], "\x70"],
+ [["08"], "\x80"],
+ [["09"], "\x90"],
+ [["0a"], "\xa0"],
+ [["0b"], "\xb0"],
+ [["0c"], "\xc0"],
+ [["0d"], "\xd0"],
+ [["0e"], "\xe0"],
+ [["0f"], "\xf0"],
+ [["0A"], "\xa0"],
+ [["0B"], "\xb0"],
+ [["0C"], "\xc0"],
+ [["0D"], "\xd0"],
+ [["0E"], "\xe0"],
+ [["0F"], "\xf0"]
+ ].should be_computed_by(:pack, "h2")
+ end
+
+ it "encodes the least significant nibble of a non alphanumeric character as the least significant nibble of the hex value" do
+ [ [["^"], "\x0e"],
+ [["*"], "\x0a"],
+ [["#"], "\x03"],
+ [["["], "\x0b"],
+ [["]"], "\x0d"],
+ [["@"], "\x00"],
+ [["!"], "\x01"],
+ [["H"], "\x01"],
+ [["O"], "\x08"],
+ [["T"], "\x0d"],
+ [["Z"], "\x03"],
+ ].should be_computed_by(:pack, "h")
+ end
+
+ it "returns a binary string" do
+ ["41"].pack("h").encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/array/pack/i_spec.rb b/spec/ruby/core/array/pack/i_spec.rb
new file mode 100644
index 0000000000..a237071227
--- /dev/null
+++ b/spec/ruby/core/array/pack/i_spec.rb
@@ -0,0 +1,133 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+describe "Array#pack with format 'I'" do
+ it_behaves_like :array_pack_basic, 'I'
+ it_behaves_like :array_pack_basic_non_float, 'I'
+ it_behaves_like :array_pack_arguments, 'I'
+ it_behaves_like :array_pack_numeric_basic, 'I'
+ it_behaves_like :array_pack_integer, 'I'
+end
+
+describe "Array#pack with format 'i'" do
+ it_behaves_like :array_pack_basic, 'i'
+ it_behaves_like :array_pack_basic_non_float, 'i'
+ it_behaves_like :array_pack_arguments, 'i'
+ it_behaves_like :array_pack_numeric_basic, 'i'
+ it_behaves_like :array_pack_integer, 'i'
+end
+
+describe "Array#pack with format 'I'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_32bit_le, 'I<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_32bit_le, 'I<_'
+ it_behaves_like :array_pack_32bit_le, 'I_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_32bit_le, 'I<!'
+ it_behaves_like :array_pack_32bit_le, 'I!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_32bit_be, 'I>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_32bit_be, 'I>_'
+ it_behaves_like :array_pack_32bit_be, 'I_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_32bit_be, 'I>!'
+ it_behaves_like :array_pack_32bit_be, 'I!>'
+ end
+end
+
+describe "Array#pack with format 'i'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_32bit_le, 'i<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_32bit_le, 'i<_'
+ it_behaves_like :array_pack_32bit_le, 'i_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_32bit_le, 'i<!'
+ it_behaves_like :array_pack_32bit_le, 'i!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_32bit_be, 'i>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_32bit_be, 'i>_'
+ it_behaves_like :array_pack_32bit_be, 'i_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_32bit_be, 'i>!'
+ it_behaves_like :array_pack_32bit_be, 'i!>'
+ end
+end
+
+little_endian do
+ describe "Array#pack with format 'I'" do
+ it_behaves_like :array_pack_32bit_le, 'I'
+ end
+
+ describe "Array#pack with format 'I' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_le_platform, 'I_'
+ end
+
+ describe "Array#pack with format 'I' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_le_platform, 'I!'
+ end
+
+ describe "Array#pack with format 'i'" do
+ it_behaves_like :array_pack_32bit_le, 'i'
+ end
+
+ describe "Array#pack with format 'i' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_le_platform, 'i_'
+ end
+
+ describe "Array#pack with format 'i' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_le_platform, 'i!'
+ end
+end
+
+big_endian do
+ describe "Array#pack with format 'I'" do
+ it_behaves_like :array_pack_32bit_be, 'I'
+ end
+
+ describe "Array#pack with format 'I' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_be_platform, 'I_'
+ end
+
+ describe "Array#pack with format 'I' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_be_platform, 'I!'
+ end
+
+ describe "Array#pack with format 'i'" do
+ it_behaves_like :array_pack_32bit_be, 'i'
+ end
+
+ describe "Array#pack with format 'i' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_be_platform, 'i_'
+ end
+
+ describe "Array#pack with format 'i' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_be_platform, 'i!'
+ end
+end
diff --git a/spec/ruby/core/array/pack/j_spec.rb b/spec/ruby/core/array/pack/j_spec.rb
new file mode 100644
index 0000000000..7b62d5efdf
--- /dev/null
+++ b/spec/ruby/core/array/pack/j_spec.rb
@@ -0,0 +1,217 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+platform_is pointer_size: 64 do
+ describe "Array#pack with format 'J'" do
+ it_behaves_like :array_pack_basic, 'J'
+ it_behaves_like :array_pack_basic_non_float, 'J'
+ it_behaves_like :array_pack_arguments, 'J'
+ it_behaves_like :array_pack_numeric_basic, 'J'
+ it_behaves_like :array_pack_integer, 'J'
+ end
+
+ describe "Array#pack with format 'j'" do
+ it_behaves_like :array_pack_basic, 'j'
+ it_behaves_like :array_pack_basic_non_float, 'j'
+ it_behaves_like :array_pack_arguments, 'j'
+ it_behaves_like :array_pack_numeric_basic, 'j'
+ it_behaves_like :array_pack_integer, 'j'
+ end
+
+ little_endian do
+ describe "Array#pack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_64bit_le, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_64bit_le, 'J!'
+ end
+ end
+
+ describe "Array#pack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_64bit_le, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_64bit_le, 'j!'
+ end
+ end
+ end
+
+ big_endian do
+ describe "Array#pack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_64bit_be, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_64bit_be, 'J!'
+ end
+ end
+
+ describe "Array#pack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_64bit_be, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_64bit_be, 'j!'
+ end
+ end
+ end
+
+ describe "Array#pack with format 'J'" do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_64bit_le, 'J<_'
+ it_behaves_like :array_pack_64bit_le, 'J_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_64bit_le, 'J<!'
+ it_behaves_like :array_pack_64bit_le, 'J!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_64bit_be, 'J>_'
+ it_behaves_like :array_pack_64bit_be, 'J_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_64bit_be, 'J>!'
+ it_behaves_like :array_pack_64bit_be, 'J!>'
+ end
+ end
+
+ describe "Array#pack with format 'j'" do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_64bit_le, 'j<_'
+ it_behaves_like :array_pack_64bit_le, 'j_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_64bit_le, 'j<!'
+ it_behaves_like :array_pack_64bit_le, 'j!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_64bit_be, 'j>_'
+ it_behaves_like :array_pack_64bit_be, 'j_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_64bit_be, 'j>!'
+ it_behaves_like :array_pack_64bit_be, 'j!>'
+ end
+ end
+end
+
+platform_is pointer_size: 32 do
+ describe "Array#pack with format 'J'" do
+ it_behaves_like :array_pack_basic, 'J'
+ it_behaves_like :array_pack_basic_non_float, 'J'
+ it_behaves_like :array_pack_arguments, 'J'
+ it_behaves_like :array_pack_numeric_basic, 'J'
+ it_behaves_like :array_pack_integer, 'J'
+ end
+
+ describe "Array#pack with format 'j'" do
+ it_behaves_like :array_pack_basic, 'j'
+ it_behaves_like :array_pack_basic_non_float, 'j'
+ it_behaves_like :array_pack_arguments, 'j'
+ it_behaves_like :array_pack_numeric_basic, 'j'
+ it_behaves_like :array_pack_integer, 'j'
+ end
+
+ big_endian do
+ describe "Array#pack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_32bit_be, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_32bit_be, 'J!'
+ end
+ end
+
+ describe "Array#pack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_32bit_be, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_32bit_be, 'j!'
+ end
+ end
+ end
+
+ little_endian do
+ describe "Array#pack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_32bit_le, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_32bit_le, 'J!'
+ end
+ end
+
+ describe "Array#pack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :array_pack_32bit_le, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :array_pack_32bit_le, 'j!'
+ end
+ end
+ end
+
+ describe "Array#pack with format 'J'" do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_32bit_le, 'J<_'
+ it_behaves_like :array_pack_32bit_le, 'J_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_32bit_le, 'J<!'
+ it_behaves_like :array_pack_32bit_le, 'J!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_32bit_be, 'J>_'
+ it_behaves_like :array_pack_32bit_be, 'J_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_32bit_be, 'J>!'
+ it_behaves_like :array_pack_32bit_be, 'J!>'
+ end
+ end
+
+ describe "Array#pack with format 'j'" do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_32bit_le, 'j<_'
+ it_behaves_like :array_pack_32bit_le, 'j_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_32bit_le, 'j<!'
+ it_behaves_like :array_pack_32bit_le, 'j!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_32bit_be, 'j>_'
+ it_behaves_like :array_pack_32bit_be, 'j_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_32bit_be, 'j>!'
+ it_behaves_like :array_pack_32bit_be, 'j!>'
+ end
+ end
+end
diff --git a/spec/ruby/core/array/pack/l_spec.rb b/spec/ruby/core/array/pack/l_spec.rb
new file mode 100644
index 0000000000..f6dfb1da83
--- /dev/null
+++ b/spec/ruby/core/array/pack/l_spec.rb
@@ -0,0 +1,221 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+describe "Array#pack with format 'L'" do
+ it_behaves_like :array_pack_basic, 'L'
+ it_behaves_like :array_pack_basic_non_float, 'L'
+ it_behaves_like :array_pack_arguments, 'L'
+ it_behaves_like :array_pack_numeric_basic, 'L'
+ it_behaves_like :array_pack_integer, 'L'
+end
+
+describe "Array#pack with format 'l'" do
+ it_behaves_like :array_pack_basic, 'l'
+ it_behaves_like :array_pack_basic_non_float, 'l'
+ it_behaves_like :array_pack_arguments, 'l'
+ it_behaves_like :array_pack_numeric_basic, 'l'
+ it_behaves_like :array_pack_integer, 'l'
+end
+
+describe "Array#pack with format 'L'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_32bit_le, 'L<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_32bit_be, 'L>'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_32bit_le, 'L<_'
+ it_behaves_like :array_pack_32bit_le, 'L_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_32bit_le, 'L<!'
+ it_behaves_like :array_pack_32bit_le, 'L!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_32bit_be, 'L>_'
+ it_behaves_like :array_pack_32bit_be, 'L_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_32bit_be, 'L>!'
+ it_behaves_like :array_pack_32bit_be, 'L!>'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_64bit_le, 'L<_'
+ it_behaves_like :array_pack_64bit_le, 'L_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_64bit_le, 'L<!'
+ it_behaves_like :array_pack_64bit_le, 'L!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_64bit_be, 'L>_'
+ it_behaves_like :array_pack_64bit_be, 'L_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_64bit_be, 'L>!'
+ it_behaves_like :array_pack_64bit_be, 'L!>'
+ end
+ end
+end
+
+describe "Array#pack with format 'l'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_32bit_le, 'l<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_32bit_be, 'l>'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_32bit_le, 'l<_'
+ it_behaves_like :array_pack_32bit_le, 'l_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_32bit_le, 'l<!'
+ it_behaves_like :array_pack_32bit_le, 'l!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_32bit_be, 'l>_'
+ it_behaves_like :array_pack_32bit_be, 'l_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_32bit_be, 'l>!'
+ it_behaves_like :array_pack_32bit_be, 'l!>'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_64bit_le, 'l<_'
+ it_behaves_like :array_pack_64bit_le, 'l_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_64bit_le, 'l<!'
+ it_behaves_like :array_pack_64bit_le, 'l!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_64bit_be, 'l>_'
+ it_behaves_like :array_pack_64bit_be, 'l_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_64bit_be, 'l>!'
+ it_behaves_like :array_pack_64bit_be, 'l!>'
+ end
+ end
+end
+
+little_endian do
+ describe "Array#pack with format 'L'" do
+ it_behaves_like :array_pack_32bit_le, 'L'
+ end
+
+ describe "Array#pack with format 'l'" do
+ it_behaves_like :array_pack_32bit_le, 'l'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "Array#pack with format 'L' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_le, 'L_'
+ end
+
+ describe "Array#pack with format 'L' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_le, 'L!'
+ end
+
+ describe "Array#pack with format 'l' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_le, 'l_'
+ end
+
+ describe "Array#pack with format 'l' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_le, 'l!'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "Array#pack with format 'L' with modifier '_'" do
+ it_behaves_like :array_pack_64bit_le, 'L_'
+ end
+
+ describe "Array#pack with format 'L' with modifier '!'" do
+ it_behaves_like :array_pack_64bit_le, 'L!'
+ end
+
+ describe "Array#pack with format 'l' with modifier '_'" do
+ it_behaves_like :array_pack_64bit_le, 'l_'
+ end
+
+ describe "Array#pack with format 'l' with modifier '!'" do
+ it_behaves_like :array_pack_64bit_le, 'l!'
+ end
+ end
+end
+
+big_endian do
+ describe "Array#pack with format 'L'" do
+ it_behaves_like :array_pack_32bit_be, 'L'
+ end
+
+ describe "Array#pack with format 'l'" do
+ it_behaves_like :array_pack_32bit_be, 'l'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "Array#pack with format 'L' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_be, 'L_'
+ end
+
+ describe "Array#pack with format 'L' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_be, 'L!'
+ end
+
+ describe "Array#pack with format 'l' with modifier '_'" do
+ it_behaves_like :array_pack_32bit_be, 'l_'
+ end
+
+ describe "Array#pack with format 'l' with modifier '!'" do
+ it_behaves_like :array_pack_32bit_be, 'l!'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "Array#pack with format 'L' with modifier '_'" do
+ it_behaves_like :array_pack_64bit_be, 'L_'
+ end
+
+ describe "Array#pack with format 'L' with modifier '!'" do
+ it_behaves_like :array_pack_64bit_be, 'L!'
+ end
+
+ describe "Array#pack with format 'l' with modifier '_'" do
+ it_behaves_like :array_pack_64bit_be, 'l_'
+ end
+
+ describe "Array#pack with format 'l' with modifier '!'" do
+ it_behaves_like :array_pack_64bit_be, 'l!'
+ end
+ end
+end
diff --git a/spec/ruby/core/array/pack/m_spec.rb b/spec/ruby/core/array/pack/m_spec.rb
new file mode 100644
index 0000000000..fb670d120e
--- /dev/null
+++ b/spec/ruby/core/array/pack/m_spec.rb
@@ -0,0 +1,317 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'M'" do
+ it_behaves_like :array_pack_basic, 'M'
+ it_behaves_like :array_pack_basic_non_float, 'M'
+ it_behaves_like :array_pack_arguments, 'M'
+ it_behaves_like :array_pack_taint, 'M'
+
+ it "encodes an empty string as an empty string" do
+ [""].pack("M").should == ""
+ end
+
+ it "encodes nil as an empty string" do
+ [nil].pack("M").should == ""
+ end
+
+ it "appends a soft line break at the end of an encoded string" do
+ ["a"].pack("M").should == "a=\n"
+ end
+
+ it "does not append a soft break if the string ends with a newline" do
+ ["a\n"].pack("M").should == "a\n"
+ end
+
+ it "encodes one element for each directive" do
+ ["a", "b", "c"].pack("MM").should == "a=\nb=\n"
+ end
+
+ it "encodes byte values 33..60 directly" do
+ [ [["!\"\#$%&'()*+,-./"], "!\"\#$%&'()*+,-./=\n"],
+ [["0123456789"], "0123456789=\n"],
+ [[":;<"], ":;<=\n"]
+ ].should be_computed_by(:pack, "M")
+ end
+
+ it "encodes byte values 62..126 directly" do
+ [ [[">?@"], ">?@=\n"],
+ [["ABCDEFGHIJKLMNOPQRSTUVWXYZ"], "ABCDEFGHIJKLMNOPQRSTUVWXYZ=\n"],
+ [["[\\]^_`"], "[\\]^_`=\n"],
+ [["abcdefghijklmnopqrstuvwxyz"], "abcdefghijklmnopqrstuvwxyz=\n"],
+ [["{|}~"], "{|}~=\n"]
+ ].should be_computed_by(:pack, "M")
+ end
+
+ it "encodes an '=' character in hex format" do
+ ["="].pack("M").should == "=3D=\n"
+ end
+
+ it "encodes an embedded space directly" do
+ ["a b"].pack("M").should == "a b=\n"
+ end
+
+ it "encodes a space at the end of the string directly" do
+ ["a "].pack("M").should == "a =\n"
+ end
+
+ it "encodes an embedded tab directly" do
+ ["a\tb"].pack("M").should == "a\tb=\n"
+ end
+
+ it "encodes a tab at the end of the string directly" do
+ ["a\t"].pack("M").should == "a\t=\n"
+ end
+
+ it "encodes an embedded newline directly" do
+ ["a\nb"].pack("M").should == "a\nb=\n"
+ end
+
+ it "encodes 0..31 except tab and newline in hex format" do
+ [ [["\x00\x01\x02\x03\x04\x05\x06"], "=00=01=02=03=04=05=06=\n"],
+ [["\a\b\v\f\r"], "=07=08=0B=0C=0D=\n"],
+ [["\x0e\x0f\x10\x11\x12\x13\x14"], "=0E=0F=10=11=12=13=14=\n"],
+ [["\x15\x16\x17\x18\x19\x1a"], "=15=16=17=18=19=1A=\n"],
+ [["\e"], "=1B=\n"],
+ [["\x1c\x1d\x1e\x1f"], "=1C=1D=1E=1F=\n"]
+ ].should be_computed_by(:pack, "M")
+ end
+
+ it "encodes a tab at the end of a line with an encoded newline" do
+ ["\t"].pack("M").should == "\t=\n"
+ ["\t\n"].pack("M").should == "\t=\n\n"
+ ["abc\t\nxyz"].pack("M").should == "abc\t=\n\nxyz=\n"
+ end
+
+ it "encodes a space at the end of a line with an encoded newline" do
+ [" "].pack("M").should == " =\n"
+ [" \n"].pack("M").should == " =\n\n"
+ ["abc \nxyz"].pack("M").should == "abc =\n\nxyz=\n"
+ end
+
+ it "encodes 127..255 in hex format" do
+ [ [["\x7f\x80\x81\x82\x83\x84\x85\x86"], "=7F=80=81=82=83=84=85=86=\n"],
+ [["\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"], "=87=88=89=8A=8B=8C=8D=8E=\n"],
+ [["\x8f\x90\x91\x92\x93\x94\x95\x96"], "=8F=90=91=92=93=94=95=96=\n"],
+ [["\x97\x98\x99\x9a\x9b\x9c\x9d\x9e"], "=97=98=99=9A=9B=9C=9D=9E=\n"],
+ [["\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6"], "=9F=A0=A1=A2=A3=A4=A5=A6=\n"],
+ [["\xa7\xa8\xa9\xaa\xab\xac\xad\xae"], "=A7=A8=A9=AA=AB=AC=AD=AE=\n"],
+ [["\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6"], "=AF=B0=B1=B2=B3=B4=B5=B6=\n"],
+ [["\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe"], "=B7=B8=B9=BA=BB=BC=BD=BE=\n"],
+ [["\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6"], "=BF=C0=C1=C2=C3=C4=C5=C6=\n"],
+ [["\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce"], "=C7=C8=C9=CA=CB=CC=CD=CE=\n"],
+ [["\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6"], "=CF=D0=D1=D2=D3=D4=D5=D6=\n"],
+ [["\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde"], "=D7=D8=D9=DA=DB=DC=DD=DE=\n"],
+ [["\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6"], "=DF=E0=E1=E2=E3=E4=E5=E6=\n"],
+ [["\xe7\xe8\xe9\xea\xeb\xec\xed\xee"], "=E7=E8=E9=EA=EB=EC=ED=EE=\n"],
+ [["\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"], "=EF=F0=F1=F2=F3=F4=F5=F6=\n"],
+ [["\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe"], "=F7=F8=F9=FA=FB=FC=FD=FE=\n"],
+ [["\xff"], "=FF=\n"]
+ ].should be_computed_by(:pack, "M")
+ end
+
+ it "emits a soft line break when the output exceeds 72 characters when passed '*', 0, 1, or no count modifier" do
+ s1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ r1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\na=\n"
+ s2 = "\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19\x19"
+ r2 = "=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=19=\n=19=\n"
+ s3 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x15a"
+ r3 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=15=\na=\n"
+ s4 = "\x15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\x15a"
+ r4 = "=15aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=\na=15a=\n"
+
+ [ [[s1], "M", r1],
+ [[s1], "M0", r1],
+ [[s1], "M1", r1],
+ [[s2], "M", r2],
+ [[s2], "M0", r2],
+ [[s2], "M1", r2],
+ [[s3], "M", r3],
+ [[s3], "M0", r3],
+ [[s3], "M1", r3],
+ [[s4], "M", r4],
+ [[s4], "M0", r4],
+ [[s4], "M1", r4]
+ ].should be_computed_by(:pack)
+ end
+
+ it "emits a soft line break when the output exceeds count characters" do
+ [ [["abcdefghi"], "M2", "abc=\ndef=\nghi=\n"],
+ [["abcdefghi"], "M3", "abcd=\nefgh=\ni=\n"],
+ [["abcdefghi"], "M4", "abcde=\nfghi=\n"],
+ [["abcdefghi"], "M5", "abcdef=\nghi=\n"],
+ [["abcdefghi"], "M6", "abcdefg=\nhi=\n"],
+ [["\x19\x19\x19\x19"], "M2", "=19=\n=19=\n=19=\n=19=\n"],
+ [["\x19\x19\x19\x19"], "M3", "=19=19=\n=19=19=\n"],
+ [["\x19\x19\x19\x19"], "M4", "=19=19=\n=19=19=\n"],
+ [["\x19\x19\x19\x19"], "M5", "=19=19=\n=19=19=\n"],
+ [["\x19\x19\x19\x19"], "M6", "=19=19=19=\n=19=\n"],
+ [["\x19\x19\x19\x19"], "M7", "=19=19=19=\n=19=\n"]
+ ].should be_computed_by(:pack)
+ end
+
+ it "encodes a recursive array" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.pack('M').should.instance_of?(String)
+
+ array = ArraySpecs.recursive_array
+ array.pack('M').should == "1=\n"
+ end
+
+ it "calls #to_s to convert an object to a String" do
+ obj = mock("pack M string")
+ obj.should_receive(:to_s).and_return("packing")
+
+ [obj].pack("M").should == "packing=\n"
+ end
+
+ it "converts the object to a String representation if #to_s does not return a String" do
+ obj = mock("pack M non-string")
+ obj.should_receive(:to_s).and_return(2)
+
+ [obj].pack("M").should.instance_of?(String)
+ end
+
+ it "encodes a Symbol as a String" do
+ [:symbol].pack("M").should == "symbol=\n"
+ end
+
+ it "encodes an Integer as a String" do
+ [ [[1], "1=\n"],
+ [[bignum_value], "#{bignum_value}=\n"]
+ ].should be_computed_by(:pack, "M")
+ end
+
+ it "encodes a Float as a String" do
+ [1.0].pack("M").should == "1.0=\n"
+ end
+
+ it "converts Floats to the minimum unique representation" do
+ [1.0 / 3.0].pack("M").should == "0.3333333333333333=\n"
+ end
+
+ it "sets the output string to US-ASCII encoding" do
+ ["abcd"].pack("M").encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "Array#pack with format 'm'" do
+ it_behaves_like :array_pack_basic, 'm'
+ it_behaves_like :array_pack_basic_non_float, 'm'
+ it_behaves_like :array_pack_arguments, 'm'
+ it_behaves_like :array_pack_taint, 'm'
+
+ it "encodes an empty string as an empty string" do
+ [""].pack("m").should == ""
+ end
+
+ it "appends a newline to the end of the encoded string" do
+ ["a"].pack("m").should == "YQ==\n"
+ end
+
+ it "encodes one element per directive" do
+ ["abc", "DEF"].pack("mm").should == "YWJj\nREVG\n"
+ end
+
+ it "encodes 1, 2, or 3 characters in 4 output characters (Base64 encoding)" do
+ [ [["a"], "YQ==\n"],
+ [["ab"], "YWI=\n"],
+ [["abc"], "YWJj\n"],
+ [["abcd"], "YWJjZA==\n"],
+ [["abcde"], "YWJjZGU=\n"],
+ [["abcdef"], "YWJjZGVm\n"],
+ [["abcdefg"], "YWJjZGVmZw==\n"],
+ ].should be_computed_by(:pack, "m")
+ end
+
+ it "emits a newline after complete groups of count / 3 input characters when passed a count modifier" do
+ ["abcdefg"].pack("m3").should == "YWJj\nZGVm\nZw==\n"
+ end
+
+ it "implicitly has a count of 45 when passed '*', 1, 2 or no count modifier" do
+ s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ r = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh\nYWFhYWE=\n"
+ [ [[s], "m", r],
+ [[s], "m*", r],
+ [[s], "m1", r],
+ [[s], "m2", r],
+ ].should be_computed_by(:pack)
+ end
+
+ it "encodes all ascii characters" do
+ [ [["\x00\x01\x02\x03\x04\x05\x06"], "AAECAwQFBg==\n"],
+ [["\a\b\t\n\v\f\r"], "BwgJCgsMDQ==\n"],
+ [["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"], "Dg8QERITFBUW\n"],
+ [["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"], "FxgZGhscHR4f\n"],
+ [["!\"\#$%&'()*+,-./"], "ISIjJCUmJygpKissLS4v\n"],
+ [["0123456789"], "MDEyMzQ1Njc4OQ==\n"],
+ [[":;<=>?@"], "Ojs8PT4/QA==\n"],
+ [["ABCDEFGHIJKLMNOPQRSTUVWXYZ"], "QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=\n"],
+ [["[\\]^_`"], "W1xdXl9g\n"],
+ [["abcdefghijklmnopqrstuvwxyz"], "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=\n"],
+ [["{|}~"], "e3x9fg==\n"],
+ [["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"], "f8KAwoHCgsKD\n"],
+ [["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"], "woTChcKGwofC\n"],
+ [["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"], "iMKJworCi8KM\n"],
+ [["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"], "wo3CjsKPwpDC\n"],
+ [["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"], "kcKSwpPClMKV\n"],
+ [["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"], "wpbCl8KYwpnC\n"],
+ [["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"], "msKbwpzCncKe\n"],
+ [["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"], "wp/CoMKhwqLC\n"],
+ [["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"], "o8KkwqXCpsKn\n"],
+ [["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"], "wqjCqcKqwqvC\n"],
+ [["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"], "rMKtwq7Cr8Kw\n"],
+ [["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"], "wrHCssKzwrTC\n"],
+ [["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"], "tcK2wrfCuMK5\n"],
+ [["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"], "wrrCu8K8wr3C\n"],
+ [["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"], "vsK/w4DDgcOC\n"],
+ [["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"], "w4PDhMOFw4bD\n"],
+ [["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"], "h8OIw4nDisOL\n"],
+ [["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"], "w4zDjcOOw4/D\n"],
+ [["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"], "kMORw5LDk8OU\n"],
+ [["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"], "w5XDlsOXw5jD\n"],
+ [["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"], "mcOaw5vDnMOd\n"],
+ [["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"], "w57Dn8Ogw6HD\n"],
+ [["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"], "osOjw6TDpcOm\n"],
+ [["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"], "w6fDqMOpw6rD\n"],
+ [["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"], "q8Osw63DrsOv\n"],
+ [["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"], "w7DDscOyw7PD\n"],
+ [["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"], "tMO1w7bDt8O4\n"],
+ [["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"], "w7nDusO7w7zD\n"],
+ [["\xbd\xc3\xbe\xc3\xbf"], "vcO+w78=\n"]
+ ].should be_computed_by(:pack, "m")
+ end
+
+ it "calls #to_str to convert an object to a String" do
+ obj = mock("pack m string")
+ obj.should_receive(:to_str).and_return("abc")
+ [obj].pack("m").should == "YWJj\n"
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ obj = mock("pack m non-string")
+ -> { [obj].pack("m") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> { [nil].pack("m") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> { [0].pack("m") }.should.raise(TypeError)
+ -> { [bignum_value].pack("m") }.should.raise(TypeError)
+ end
+
+ it "does not emit a newline if passed zero as the count modifier" do
+ s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ r = "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE="
+ [s].pack("m0").should == r
+ end
+
+ it "sets the output string to US-ASCII encoding" do
+ ["abcd"].pack("m").encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/array/pack/n_spec.rb b/spec/ruby/core/array/pack/n_spec.rb
new file mode 100644
index 0000000000..ab9409fc1e
--- /dev/null
+++ b/spec/ruby/core/array/pack/n_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+describe "Array#pack with format 'N'" do
+ it_behaves_like :array_pack_basic, 'N'
+ it_behaves_like :array_pack_basic_non_float, 'N'
+ it_behaves_like :array_pack_arguments, 'N'
+ it_behaves_like :array_pack_numeric_basic, 'N'
+ it_behaves_like :array_pack_integer, 'N'
+ it_behaves_like :array_pack_no_platform, 'N'
+ it_behaves_like :array_pack_32bit_be, 'N'
+end
+
+describe "Array#pack with format 'n'" do
+ it_behaves_like :array_pack_basic, 'n'
+ it_behaves_like :array_pack_basic_non_float, 'n'
+ it_behaves_like :array_pack_arguments, 'n'
+ it_behaves_like :array_pack_numeric_basic, 'n'
+ it_behaves_like :array_pack_integer, 'n'
+ it_behaves_like :array_pack_no_platform, 'n'
+ it_behaves_like :array_pack_16bit_be, 'n'
+end
diff --git a/spec/ruby/core/array/pack/p_spec.rb b/spec/ruby/core/array/pack/p_spec.rb
new file mode 100644
index 0000000000..b023bf9110
--- /dev/null
+++ b/spec/ruby/core/array/pack/p_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'P'" do
+ it_behaves_like :array_pack_basic_non_float, 'P'
+ it_behaves_like :array_pack_taint, 'P'
+
+ it "produces as many bytes as there are in a pointer" do
+ ["hello"].pack("P").size.should == [0].pack("J").size
+ end
+
+ it "round-trips a string through pack and unpack" do
+ ["hello"].pack("P").unpack("P5").should == ["hello"]
+ end
+
+ it "with nil gives a null pointer" do
+ [nil].pack("P").unpack("J").should == [0]
+ end
+end
+
+describe "Array#pack with format 'p'" do
+ it_behaves_like :array_pack_basic_non_float, 'p'
+ it_behaves_like :array_pack_taint, 'p'
+
+ it "produces as many bytes as there are in a pointer" do
+ ["hello"].pack("p").size.should == [0].pack("J").size
+ end
+
+ it "round-trips a string through pack and unpack" do
+ ["hello"].pack("p").unpack("p").should == ["hello"]
+ end
+
+ it "with nil gives a null pointer" do
+ [nil].pack("p").unpack("J").should == [0]
+ end
+end
diff --git a/spec/ruby/core/array/pack/percent_spec.rb b/spec/ruby/core/array/pack/percent_spec.rb
new file mode 100644
index 0000000000..29b119732a
--- /dev/null
+++ b/spec/ruby/core/array/pack/percent_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+
+describe "Array#pack with format '%'" do
+ it "raises an Argument Error" do
+ -> { [1].pack("%") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/array/pack/q_spec.rb b/spec/ruby/core/array/pack/q_spec.rb
new file mode 100644
index 0000000000..bd6b2a4b71
--- /dev/null
+++ b/spec/ruby/core/array/pack/q_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+describe "Array#pack with format 'Q'" do
+ it_behaves_like :array_pack_basic, 'Q'
+ it_behaves_like :array_pack_basic_non_float, 'Q'
+ it_behaves_like :array_pack_arguments, 'Q'
+ it_behaves_like :array_pack_numeric_basic, 'Q'
+ it_behaves_like :array_pack_integer, 'Q'
+end
+
+describe "Array#pack with format 'q'" do
+ it_behaves_like :array_pack_basic, 'q'
+ it_behaves_like :array_pack_basic_non_float, 'q'
+ it_behaves_like :array_pack_arguments, 'q'
+ it_behaves_like :array_pack_numeric_basic, 'q'
+ it_behaves_like :array_pack_integer, 'q'
+end
+
+describe "Array#pack with format 'Q'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_64bit_le, 'Q<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_64bit_be, 'Q>'
+ end
+end
+
+describe "Array#pack with format 'q'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_64bit_le, 'q<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_64bit_be, 'q>'
+ end
+end
+
+little_endian do
+ describe "Array#pack with format 'Q'" do
+ it_behaves_like :array_pack_64bit_le, 'Q'
+ end
+
+ describe "Array#pack with format 'q'" do
+ it_behaves_like :array_pack_64bit_le, 'q'
+ end
+end
+
+big_endian do
+ describe "Array#pack with format 'Q'" do
+ it_behaves_like :array_pack_64bit_be, 'Q'
+ end
+
+ describe "Array#pack with format 'q'" do
+ it_behaves_like :array_pack_64bit_be, 'q'
+ end
+end
diff --git a/spec/ruby/core/array/pack/r_spec.rb b/spec/ruby/core/array/pack/r_spec.rb
new file mode 100644
index 0000000000..cefe1388d2
--- /dev/null
+++ b/spec/ruby/core/array/pack/r_spec.rb
@@ -0,0 +1,89 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+ruby_version_is "4.1" do
+ describe "Array#pack with format 'R'" do
+ it_behaves_like :array_pack_basic, 'R'
+ it_behaves_like :array_pack_basic_non_float, 'R'
+ it_behaves_like :array_pack_arguments, 'R'
+ it_behaves_like :array_pack_numeric_basic, 'R'
+ it_behaves_like :array_pack_integer, 'R'
+
+ it "encodes a ULEB128 integer" do
+ [ [[0], "\x00"],
+ [[1], "\x01"],
+ [[127], "\x7f"],
+ [[128], "\x80\x01"],
+ [[0x3fff], "\xff\x7f"],
+ [[0x4000], "\x80\x80\x01"],
+ [[0xffffffff], "\xff\xff\xff\xff\x0f"],
+ [[0x100000000], "\x80\x80\x80\x80\x10"],
+ [[0xffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01"],
+ [[0xffff_ffff_ffff_ffff_ffff_ffff], "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x1f"],
+ ].should be_computed_by(:pack, "R")
+ end
+
+ it "encodes multiple values with '*' modifier" do
+ [1, 2].pack("R*").should == "\x01\x02"
+ [127, 128].pack("R*").should == "\x7f\x80\x01"
+ end
+
+ it "raises an ArgumentError when passed a negative value" do
+ -> { [-1].pack("R") }.should.raise(ArgumentError)
+ -> { [-100].pack("R") }.should.raise(ArgumentError)
+ end
+
+ it "round-trips values through pack and unpack" do
+ values = [0, 1, 127, 128, 0x3fff, 0x4000, 0xffffffff, 0x100000000]
+ values.pack("R*").unpack("R*").should == values
+ end
+ end
+
+ describe "Array#pack with format 'r'" do
+ it_behaves_like :array_pack_basic, 'r'
+ it_behaves_like :array_pack_basic_non_float, 'r'
+ it_behaves_like :array_pack_arguments, 'r'
+ it_behaves_like :array_pack_numeric_basic, 'r'
+ it_behaves_like :array_pack_integer, 'r'
+
+ it "encodes a SLEB128 integer" do
+ [ [[0], "\x00"],
+ [[1], "\x01"],
+ [[-1], "\x7f"],
+ [[-2], "\x7e"],
+ [[127], "\xff\x00"],
+ [[128], "\x80\x01"],
+ [[-127], "\x81\x7f"],
+ [[-128], "\x80\x7f"],
+ ].should be_computed_by(:pack, "r")
+ end
+
+ it "encodes larger positive numbers" do
+ [0x3fff].pack("r").should == "\xff\xff\x00"
+ [0x4000].pack("r").should == "\x80\x80\x01"
+ end
+
+ it "encodes larger negative numbers" do
+ [-0x3fff].pack("r").should == "\x81\x80\x7f"
+ [-0x4000].pack("r").should == "\x80\x80\x7f"
+ end
+
+ it "encodes very large numbers" do
+ [0xffff_ffff_ffff_ffff_ffff_ffff].pack("r").should == "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x1F"
+ [-0xffff_ffff_ffff_ffff_ffff_ffff].pack("r").should == "\x81\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x60"
+ end
+
+ it "encodes multiple values with '*' modifier" do
+ [0, 1, -1].pack("r*").should == "\x00\x01\x7f"
+ end
+
+ it "round-trips values through pack and unpack" do
+ values = [0, 1, -1, 127, -127, 128, -128, 0x3fff, -0x3fff, 0x4000, -0x4000]
+ values.pack("r*").unpack("r*").should == values
+ end
+ end
+end
diff --git a/spec/ruby/core/array/pack/s_spec.rb b/spec/ruby/core/array/pack/s_spec.rb
new file mode 100644
index 0000000000..4212d6a0b1
--- /dev/null
+++ b/spec/ruby/core/array/pack/s_spec.rb
@@ -0,0 +1,133 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+describe "Array#pack with format 'S'" do
+ it_behaves_like :array_pack_basic, 'S'
+ it_behaves_like :array_pack_basic_non_float, 'S'
+ it_behaves_like :array_pack_arguments, 'S'
+ it_behaves_like :array_pack_numeric_basic, 'S'
+ it_behaves_like :array_pack_integer, 'S'
+end
+
+describe "Array#pack with format 's'" do
+ it_behaves_like :array_pack_basic, 's'
+ it_behaves_like :array_pack_basic_non_float, 's'
+ it_behaves_like :array_pack_arguments, 's'
+ it_behaves_like :array_pack_numeric_basic, 's'
+ it_behaves_like :array_pack_integer, 's'
+end
+
+describe "Array#pack with format 'S'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_16bit_le, 'S<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_16bit_le, 'S<_'
+ it_behaves_like :array_pack_16bit_le, 'S_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_16bit_le, 'S<!'
+ it_behaves_like :array_pack_16bit_le, 'S!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_16bit_be, 'S>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_16bit_be, 'S>_'
+ it_behaves_like :array_pack_16bit_be, 'S_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_16bit_be, 'S>!'
+ it_behaves_like :array_pack_16bit_be, 'S!>'
+ end
+end
+
+describe "Array#pack with format 's'" do
+ describe "with modifier '<'" do
+ it_behaves_like :array_pack_16bit_le, 's<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :array_pack_16bit_le, 's<_'
+ it_behaves_like :array_pack_16bit_le, 's_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :array_pack_16bit_le, 's<!'
+ it_behaves_like :array_pack_16bit_le, 's!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :array_pack_16bit_be, 's>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :array_pack_16bit_be, 's>_'
+ it_behaves_like :array_pack_16bit_be, 's_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :array_pack_16bit_be, 's>!'
+ it_behaves_like :array_pack_16bit_be, 's!>'
+ end
+end
+
+little_endian do
+ describe "Array#pack with format 'S'" do
+ it_behaves_like :array_pack_16bit_le, 'S'
+ end
+
+ describe "Array#pack with format 'S' with modifier '_'" do
+ it_behaves_like :array_pack_16bit_le, 'S_'
+ end
+
+ describe "Array#pack with format 'S' with modifier '!'" do
+ it_behaves_like :array_pack_16bit_le, 'S!'
+ end
+
+ describe "Array#pack with format 's'" do
+ it_behaves_like :array_pack_16bit_le, 's'
+ end
+
+ describe "Array#pack with format 's' with modifier '_'" do
+ it_behaves_like :array_pack_16bit_le, 's_'
+ end
+
+ describe "Array#pack with format 's' with modifier '!'" do
+ it_behaves_like :array_pack_16bit_le, 's!'
+ end
+end
+
+big_endian do
+ describe "Array#pack with format 'S'" do
+ it_behaves_like :array_pack_16bit_be, 'S'
+ end
+
+ describe "Array#pack with format 'S' with modifier '_'" do
+ it_behaves_like :array_pack_16bit_be, 'S_'
+ end
+
+ describe "Array#pack with format 'S' with modifier '!'" do
+ it_behaves_like :array_pack_16bit_be, 'S!'
+ end
+
+ describe "Array#pack with format 's'" do
+ it_behaves_like :array_pack_16bit_be, 's'
+ end
+
+ describe "Array#pack with format 's' with modifier '_'" do
+ it_behaves_like :array_pack_16bit_be, 's_'
+ end
+
+ describe "Array#pack with format 's' with modifier '!'" do
+ it_behaves_like :array_pack_16bit_be, 's!'
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/basic.rb b/spec/ruby/core/array/pack/shared/basic.rb
new file mode 100644
index 0000000000..2894369c71
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/basic.rb
@@ -0,0 +1,73 @@
+describe :array_pack_arguments, shared: true do
+ it "raises an ArgumentError if there are fewer elements than the format requires" do
+ -> { [].pack(pack_format(1)) }.should.raise(ArgumentError)
+ end
+end
+
+describe :array_pack_basic, shared: true do
+ before :each do
+ @obj = ArraySpecs.universal_pack_object
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { [@obj].pack(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed an Integer" do
+ -> { [@obj].pack(1) }.should.raise(TypeError)
+ end
+end
+
+describe :array_pack_basic_non_float, shared: true do
+ before :each do
+ @obj = ArraySpecs.universal_pack_object
+ end
+
+ it "ignores whitespace in the format string" do
+ [@obj, @obj].pack("a \t\n\v\f\r"+pack_format).should.instance_of?(String)
+ end
+
+ it "ignores comments in the format string" do
+ # 2 additional directives ('a') are required for the X directive
+ [@obj, @obj, @obj, @obj].pack("aa #{pack_format} # some comment \n#{pack_format}").should.instance_of?(String)
+ end
+
+ it "raise ArgumentError when a directive is unknown" do
+ # additional directive ('a') is required for the X directive
+ -> { [@obj, @obj].pack("a K" + pack_format) }.should.raise(ArgumentError, /unknown pack directive 'K'/)
+ -> { [@obj, @obj].pack("a 0" + pack_format) }.should.raise(ArgumentError, /unknown pack directive '0'/)
+ -> { [@obj, @obj].pack("a :" + pack_format) }.should.raise(ArgumentError, /unknown pack directive ':'/)
+ end
+
+ it "calls #to_str to coerce the directives string" do
+ d = mock("pack directive")
+ d.should_receive(:to_str).and_return("x"+pack_format)
+ [@obj, @obj].pack(d).should.instance_of?(String)
+ end
+end
+
+describe :array_pack_basic_float, shared: true do
+ it "ignores whitespace in the format string" do
+ [9.3, 4.7].pack(" \t\n\v\f\r"+pack_format).should.instance_of?(String)
+ end
+
+ it "ignores comments in the format string" do
+ [9.3, 4.7].pack(pack_format + "# some comment \n" + pack_format).should.instance_of?(String)
+ end
+
+ it "calls #to_str to coerce the directives string" do
+ d = mock("pack directive")
+ d.should_receive(:to_str).and_return("x"+pack_format)
+ [1.2, 4.7].pack(d).should.instance_of?(String)
+ end
+end
+
+describe :array_pack_no_platform, shared: true do
+ it "raises ArgumentError when the format modifier is '_'" do
+ ->{ [1].pack(pack_format("_")) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when the format modifier is '!'" do
+ ->{ [1].pack(pack_format("!")) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/encodings.rb b/spec/ruby/core/array/pack/shared/encodings.rb
new file mode 100644
index 0000000000..0b5a5cc8a0
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/encodings.rb
@@ -0,0 +1,16 @@
+describe :array_pack_hex, shared: true do
+ it "encodes no bytes when passed zero as the count modifier" do
+ ["abc"].pack(pack_format(0)).should == ""
+ end
+
+ it "raises a TypeError if the object does not respond to #to_str" do
+ obj = mock("pack hex non-string")
+ -> { [obj].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ obj = mock("pack hex non-string")
+ obj.should_receive(:to_str).and_return(1)
+ -> { [obj].pack(pack_format) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/float.rb b/spec/ruby/core/array/pack/shared/float.rb
new file mode 100644
index 0000000000..c1efcd7677
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/float.rb
@@ -0,0 +1,255 @@
+# encoding: binary
+
+describe :array_pack_float_le, shared: true do
+ it "encodes a positive Float" do
+ [1.42].pack(pack_format).should == "\x8f\xc2\xb5?"
+ end
+
+ it "encodes a negative Float" do
+ [-34.2].pack(pack_format).should == "\xcd\xcc\x08\xc2"
+ end
+
+ it "converts an Integer to a Float" do
+ [8].pack(pack_format).should == "\x00\x00\x00A"
+ end
+
+ it "raises a TypeError if passed a String representation of a floating point number" do
+ -> { ["13"].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "\x9a\x999@33\xb3?"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format("*")).should == "\x9a\x999@33\xb3?33\x03A"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [5.3, 9.2].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [5.3, 9.2].pack(pack_format(" ", 2)).should == "\x9a\x99\xa9@33\x13A"
+ end
+
+ it "encodes positive Infinity" do
+ [infinity_value].pack(pack_format).should == "\x00\x00\x80\x7f"
+ end
+
+ it "encodes negative Infinity" do
+ [-infinity_value].pack(pack_format).should == "\x00\x00\x80\xff"
+ end
+
+ it "encodes NaN" do
+ nans = ["\x00\x00\xc0\xff", "\x00\x00\xc0\x7f", "\xFF\xFF\xFF\x7F"]
+ nans.should.include?([nan_value].pack(pack_format))
+ end
+
+ it "encodes a positive Float outside the range of a single precision float" do
+ [1e150].pack(pack_format).should == "\x00\x00\x80\x7f"
+ end
+
+ it "encodes a negative Float outside the range of a single precision float" do
+ [-1e150].pack(pack_format).should == "\x00\x00\x80\xff"
+ end
+
+ it "encodes a bignum as a float" do
+ [2 ** 65].pack(pack_format).should == [(2 ** 65).to_f].pack(pack_format)
+ end
+
+ it "encodes a rational as a float" do
+ [Rational(3, 4)].pack(pack_format).should == [Rational(3, 4).to_f].pack(pack_format)
+ end
+end
+
+describe :array_pack_float_be, shared: true do
+ it "encodes a positive Float" do
+ [1.42].pack(pack_format).should == "?\xb5\xc2\x8f"
+ end
+
+ it "encodes a negative Float" do
+ [-34.2].pack(pack_format).should == "\xc2\x08\xcc\xcd"
+ end
+
+ it "converts an Integer to a Float" do
+ [8].pack(pack_format).should == "A\x00\x00\x00"
+ [bignum_value].pack(pack_format).should == "_\x80\x00\x00"
+ end
+
+ it "converts a Rational to a Float" do
+ [Rational(8)].pack(pack_format).should == "A\x00\x00\x00"
+ end
+
+ it "raises a TypeError if passed a String representation of a floating point number" do
+ -> { ["13"].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "@9\x99\x9a?\xb333"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format("*")).should == "@9\x99\x9a?\xb333A\x0333"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [5.3, 9.2].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [5.3, 9.2].pack(pack_format(" ", 2)).should == "@\xa9\x99\x9aA\x1333"
+ end
+
+ it "encodes positive Infinity" do
+ [infinity_value].pack(pack_format).should == "\x7f\x80\x00\x00"
+ end
+
+ it "encodes negative Infinity" do
+ [-infinity_value].pack(pack_format).should == "\xff\x80\x00\x00"
+ end
+
+ it "encodes NaN" do
+ nans = ["\xff\xc0\x00\x00", "\x7f\xc0\x00\x00", "\x7F\xFF\xFF\xFF"]
+ nans.should.include?([nan_value].pack(pack_format))
+ end
+
+ it "encodes a positive Float outside the range of a single precision float" do
+ [1e150].pack(pack_format).should == "\x7f\x80\x00\x00"
+ end
+
+ it "encodes a negative Float outside the range of a single precision float" do
+ [-1e150].pack(pack_format).should == "\xff\x80\x00\x00"
+ end
+end
+
+describe :array_pack_double_le, shared: true do
+ it "encodes a positive Float" do
+ [1.42].pack(pack_format).should == "\xb8\x1e\x85\xebQ\xb8\xf6?"
+ end
+
+ it "encodes a negative Float" do
+ [-34.2].pack(pack_format).should == "\x9a\x99\x99\x99\x99\x19A\xc0"
+ end
+
+ it "converts an Integer to a Float" do
+ [8].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\x20@"
+ [bignum_value].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\xF0C"
+ end
+
+ it "converts a Rational to a Float" do
+ [Rational(8)].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00 @"
+ end
+
+ it "raises a TypeError if passed a String representation of a floating point number" do
+ -> { ["13"].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "333333\x07@ffffff\xf6?"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format("*")).should == "333333\x07@ffffff\xf6?ffffff\x20@"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [5.3, 9.2].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [5.3, 9.2].pack(pack_format(" ", 2)).should == "333333\x15@ffffff\x22@"
+ end
+
+ it "encodes positive Infinity" do
+ [infinity_value].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\xf0\x7f"
+ end
+
+ it "encodes negative Infinity" do
+ [-infinity_value].pack(pack_format).should == "\x00\x00\x00\x00\x00\x00\xf0\xff"
+ end
+
+ it "encodes NaN" do
+ nans = [
+ "\x00\x00\x00\x00\x00\x00\xf8\xff",
+ "\x00\x00\x00\x00\x00\x00\xf8\x7f",
+ "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F"
+ ]
+ nans.should.include?([nan_value].pack(pack_format))
+ end
+
+ it "encodes a positive Float outside the range of a single precision float" do
+ [1e150].pack(pack_format).should == "\xaf\x96P\x2e5\x8d\x13_"
+ end
+
+ it "encodes a negative Float outside the range of a single precision float" do
+ [-1e150].pack(pack_format).should == "\xaf\x96P\x2e5\x8d\x13\xdf"
+ end
+end
+
+describe :array_pack_double_be, shared: true do
+ it "encodes a positive Float" do
+ [1.42].pack(pack_format).should == "?\xf6\xb8Q\xeb\x85\x1e\xb8"
+ end
+
+ it "encodes a negative Float" do
+ [-34.2].pack(pack_format).should == "\xc0A\x19\x99\x99\x99\x99\x9a"
+ end
+
+ it "converts an Integer to a Float" do
+ [8].pack(pack_format).should == "@\x20\x00\x00\x00\x00\x00\x00"
+ end
+
+ it "raises a TypeError if passed a String representation of a floating point number" do
+ -> { ["13"].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format(nil, 2)).should == "@\x07333333?\xf6ffffff"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [2.9, 1.4, 8.2].pack(pack_format("*")).should == "@\x07333333?\xf6ffffff@\x20ffffff"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [5.3, 9.2].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [5.3, 9.2].pack(pack_format(" ", 2)).should == "@\x15333333@\x22ffffff"
+ end
+
+ it "encodes positive Infinity" do
+ [infinity_value].pack(pack_format).should == "\x7f\xf0\x00\x00\x00\x00\x00\x00"
+ end
+
+ it "encodes negative Infinity" do
+ [-infinity_value].pack(pack_format).should == "\xff\xf0\x00\x00\x00\x00\x00\x00"
+ end
+
+ it "encodes NaN" do
+ nans = [
+ "\xff\xf8\x00\x00\x00\x00\x00\x00",
+ "\x7f\xf8\x00\x00\x00\x00\x00\x00",
+ "\x7F\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+ ]
+ nans.should.include?([nan_value].pack(pack_format))
+ end
+
+ it "encodes a positive Float outside the range of a single precision float" do
+ [1e150].pack(pack_format).should == "_\x13\x8d5\x2eP\x96\xaf"
+ end
+
+ it "encodes a negative Float outside the range of a single precision float" do
+ [-1e150].pack(pack_format).should == "\xdf\x13\x8d5\x2eP\x96\xaf"
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/integer.rb b/spec/ruby/core/array/pack/shared/integer.rb
new file mode 100644
index 0000000000..1cdd386cc1
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/integer.rb
@@ -0,0 +1,387 @@
+# encoding: binary
+
+describe :array_pack_16bit_le, shared: true do
+ it "encodes the least significant 16 bits of a positive number" do
+ [ [[0x0000_0021], "\x21\x00"],
+ [[0x0000_4321], "\x21\x43"],
+ [[0x0065_4321], "\x21\x43"],
+ [[0x7865_4321], "\x21\x43"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the least significant 16 bits of a negative number" do
+ [ [[-0x0000_0021], "\xdf\xff"],
+ [[-0x0000_4321], "\xdf\xbc"],
+ [[-0x0065_4321], "\xdf\xbc"],
+ [[-0x7865_4321], "\xdf\xbc"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[2019902241.2], "\x21\x43"],
+ [[2019902241.8], "\x21\x43"],
+ [[-2019902241.2], "\xdf\xbc"],
+ [[-2019902241.8], "\xdf\xbc"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(0x1234_5678)
+ [obj].pack(pack_format()).should == "\x78\x56"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2))
+ str.should == "\x78\x65\xcd\xab"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*'))
+ str.should == "\x78\x65\xcd\xab\x21\x43"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2))
+ str.should == "\x78\x65\xcd\xab"
+ end
+end
+
+describe :array_pack_16bit_be, shared: true do
+ it "encodes the least significant 16 bits of a positive number" do
+ [ [[0x0000_0021], "\x00\x21"],
+ [[0x0000_4321], "\x43\x21"],
+ [[0x0065_4321], "\x43\x21"],
+ [[0x7865_4321], "\x43\x21"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the least significant 16 bits of a negative number" do
+ [ [[-0x0000_0021], "\xff\xdf"],
+ [[-0x0000_4321], "\xbc\xdf"],
+ [[-0x0065_4321], "\xbc\xdf"],
+ [[-0x7865_4321], "\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[2019902241.2], "\x43\x21"],
+ [[2019902241.8], "\x43\x21"],
+ [[-2019902241.2], "\xbc\xdf"],
+ [[-2019902241.8], "\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(0x1234_5678)
+ [obj].pack(pack_format()).should == "\x56\x78"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2))
+ str.should == "\x65\x78\xab\xcd"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*'))
+ str.should == "\x65\x78\xab\xcd\x43\x21"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2))
+ str.should == "\x65\x78\xab\xcd"
+ end
+end
+
+describe :array_pack_32bit_le, shared: true do
+ it "encodes the least significant 32 bits of a positive number" do
+ [ [[0x0000_0021], "\x21\x00\x00\x00"],
+ [[0x0000_4321], "\x21\x43\x00\x00"],
+ [[0x0065_4321], "\x21\x43\x65\x00"],
+ [[0x7865_4321], "\x21\x43\x65\x78"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the least significant 32 bits of a negative number" do
+ [ [[-0x0000_0021], "\xdf\xff\xff\xff"],
+ [[-0x0000_4321], "\xdf\xbc\xff\xff"],
+ [[-0x0065_4321], "\xdf\xbc\x9a\xff"],
+ [[-0x7865_4321], "\xdf\xbc\x9a\x87"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[2019902241.2], "\x21\x43\x65\x78"],
+ [[2019902241.8], "\x21\x43\x65\x78"],
+ [[-2019902241.2], "\xdf\xbc\x9a\x87"],
+ [[-2019902241.8], "\xdf\xbc\x9a\x87"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(0x1234_5678)
+ [obj].pack(pack_format()).should == "\x78\x56\x34\x12"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2))
+ str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*'))
+ str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde\x21\x43\x65\x78"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2))
+ str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde"
+ end
+end
+
+describe :array_pack_32bit_be, shared: true do
+ it "encodes the least significant 32 bits of a positive number" do
+ [ [[0x0000_0021], "\x00\x00\x00\x21"],
+ [[0x0000_4321], "\x00\x00\x43\x21"],
+ [[0x0065_4321], "\x00\x65\x43\x21"],
+ [[0x7865_4321], "\x78\x65\x43\x21"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the least significant 32 bits of a negative number" do
+ [ [[-0x0000_0021], "\xff\xff\xff\xdf"],
+ [[-0x0000_4321], "\xff\xff\xbc\xdf"],
+ [[-0x0065_4321], "\xff\x9a\xbc\xdf"],
+ [[-0x7865_4321], "\x87\x9a\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[2019902241.2], "\x78\x65\x43\x21"],
+ [[2019902241.8], "\x78\x65\x43\x21"],
+ [[-2019902241.2], "\x87\x9a\xbc\xdf"],
+ [[-2019902241.8], "\x87\x9a\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(0x1234_5678)
+ [obj].pack(pack_format()).should == "\x12\x34\x56\x78"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2))
+ str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*'))
+ str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd\x78\x65\x43\x21"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [0x1243_6578, 0xdef0_abcd].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ str = [0x1243_6578, 0xdef0_abcd].pack(pack_format(' ', 2))
+ str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd"
+ end
+end
+
+describe :array_pack_32bit_le_platform, shared: true do
+ it "encodes the least significant 32 bits of a number" do
+ [ [[0x7865_4321], "\x21\x43\x65\x78"],
+ [[-0x7865_4321], "\xdf\xbc\x9a\x87"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2))
+ str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*'))
+ str.should == "\x78\x65\x43\x12\xcd\xab\xf0\xde\x21\x43\x65\x78"
+ end
+
+ platform_is c_long_size: 64 do
+ it "encodes the least significant 32 bits of a number that is greater than 32 bits" do
+ [ [[0xff_7865_4321], "\x21\x43\x65\x78"],
+ [[-0xff_7865_4321], "\xdf\xbc\x9a\x87"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+ end
+end
+
+describe :array_pack_32bit_be_platform, shared: true do
+ it "encodes the least significant 32 bits of a number" do
+ [ [[0x7865_4321], "\x78\x65\x43\x21"],
+ [[-0x7865_4321], "\x87\x9a\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format(2))
+ str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0x1243_6578, 0xdef0_abcd, 0x7865_4321].pack(pack_format('*'))
+ str.should == "\x12\x43\x65\x78\xde\xf0\xab\xcd\x78\x65\x43\x21"
+ end
+
+ platform_is c_long_size: 64 do
+ it "encodes the least significant 32 bits of a number that is greater than 32 bits" do
+ [ [[0xff_7865_4321], "\x78\x65\x43\x21"],
+ [[-0xff_7865_4321], "\x87\x9a\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+ end
+end
+
+describe :array_pack_64bit_le, shared: true do
+ it "encodes the least significant 64 bits of a positive number" do
+ [ [[0x0000_0000_0000_0021], "\x21\x00\x00\x00\x00\x00\x00\x00"],
+ [[0x0000_0000_0000_4321], "\x21\x43\x00\x00\x00\x00\x00\x00"],
+ [[0x0000_0000_0065_4321], "\x21\x43\x65\x00\x00\x00\x00\x00"],
+ [[0x0000_0000_7865_4321], "\x21\x43\x65\x78\x00\x00\x00\x00"],
+ [[0x0000_0090_7865_4321], "\x21\x43\x65\x78\x90\x00\x00\x00"],
+ [[0x0000_ba90_7865_4321], "\x21\x43\x65\x78\x90\xba\x00\x00"],
+ [[0x00dc_ba90_7865_4321], "\x21\x43\x65\x78\x90\xba\xdc\x00"],
+ [[0x7edc_ba90_7865_4321], "\x21\x43\x65\x78\x90\xba\xdc\x7e"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the least significant 64 bits of a negative number" do
+ [ [[-0x0000_0000_0000_0021], "\xdf\xff\xff\xff\xff\xff\xff\xff"],
+ [[-0x0000_0000_0000_4321], "\xdf\xbc\xff\xff\xff\xff\xff\xff"],
+ [[-0x0000_0000_0065_4321], "\xdf\xbc\x9a\xff\xff\xff\xff\xff"],
+ [[-0x0000_0000_7865_4321], "\xdf\xbc\x9a\x87\xff\xff\xff\xff"],
+ [[-0x0000_0090_7865_4321], "\xdf\xbc\x9a\x87\x6f\xff\xff\xff"],
+ [[-0x0000_ba90_7865_4321], "\xdf\xbc\x9a\x87\x6f\x45\xff\xff"],
+ [[-0x00dc_ba90_7865_4321], "\xdf\xbc\x9a\x87\x6f\x45\x23\xff"],
+ [[-0x7edc_ba90_7865_4321], "\xdf\xbc\x9a\x87\x6f\x45\x23\x81"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[9.14138647331322368e+18], "\x00\x44\x65\x78\x90\xba\xdc\x7e"],
+ [[-9.14138647331322368e+18], "\x00\xbc\x9a\x87\x6f\x45\x23\x81"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(0x1234_5678_90ab_cdef)
+ [obj].pack(pack_format()).should == "\xef\xcd\xab\x90\x78\x56\x34\x12"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1234_5678_90ab_cdef,
+ 0xdef0_abcd_3412_7856,
+ 0x7865_4321_dcba_def0].pack(pack_format(2))
+ str.should == "\xef\xcd\xab\x90\x78\x56\x34\x12\x56\x78\x12\x34\xcd\xab\xf0\xde"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format('*'))
+ str.should == "\x56\x78\x12\x34\xcd\xab\xf0\xde\xf0\xde\xba\xdc\x21\x43\x65\x78"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format(' ', 2))
+ str.should == "\x56\x78\x12\x34\xcd\xab\xf0\xde\xf0\xde\xba\xdc\x21\x43\x65\x78"
+ end
+end
+
+describe :array_pack_64bit_be, shared: true do
+ it "encodes the least significant 64 bits of a positive number" do
+ [ [[0x0000_0000_0000_0021], "\x00\x00\x00\x00\x00\x00\x00\x21"],
+ [[0x0000_0000_0000_4321], "\x00\x00\x00\x00\x00\x00\x43\x21"],
+ [[0x0000_0000_0065_4321], "\x00\x00\x00\x00\x00\x65\x43\x21"],
+ [[0x0000_0000_7865_4321], "\x00\x00\x00\x00\x78\x65\x43\x21"],
+ [[0x0000_0090_7865_4321], "\x00\x00\x00\x90\x78\x65\x43\x21"],
+ [[0x0000_ba90_7865_4321], "\x00\x00\xba\x90\x78\x65\x43\x21"],
+ [[0x00dc_ba90_7865_4321], "\x00\xdc\xba\x90\x78\x65\x43\x21"],
+ [[0x7edc_ba90_7865_4321], "\x7e\xdc\xba\x90\x78\x65\x43\x21"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes the least significant 64 bits of a negative number" do
+ [ [[-0x0000_0000_0000_0021], "\xff\xff\xff\xff\xff\xff\xff\xdf"],
+ [[-0x0000_0000_0000_4321], "\xff\xff\xff\xff\xff\xff\xbc\xdf"],
+ [[-0x0000_0000_0065_4321], "\xff\xff\xff\xff\xff\x9a\xbc\xdf"],
+ [[-0x0000_0000_7865_4321], "\xff\xff\xff\xff\x87\x9a\xbc\xdf"],
+ [[-0x0000_0090_7865_4321], "\xff\xff\xff\x6f\x87\x9a\xbc\xdf"],
+ [[-0x0000_ba90_7865_4321], "\xff\xff\x45\x6f\x87\x9a\xbc\xdf"],
+ [[-0x00dc_ba90_7865_4321], "\xff\x23\x45\x6f\x87\x9a\xbc\xdf"],
+ [[-0x7edc_ba90_7865_4321], "\x81\x23\x45\x6f\x87\x9a\xbc\xdf"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "encodes a Float truncated as an Integer" do
+ [ [[9.14138647331322368e+18], "\x7e\xdc\xba\x90\x78\x65\x44\x00"],
+ [[-9.14138647331322368e+18], "\x81\x23\x45\x6f\x87\x9a\xbc\x00"]
+ ].should be_computed_by(:pack, pack_format())
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(0x1234_5678_90ab_cdef)
+ [obj].pack(pack_format()).should == "\x12\x34\x56\x78\x90\xab\xcd\xef"
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ str = [0x1234_5678_90ab_cdef,
+ 0xdef0_abcd_3412_7856,
+ 0x7865_4321_dcba_def0].pack(pack_format(2))
+ str.should == "\x12\x34\x56\x78\x90\xab\xcd\xef\xde\xf0\xab\xcd\x34\x12\x78\x56"
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format('*'))
+ str.should == "\xde\xf0\xab\xcd\x34\x12\x78\x56\x78\x65\x43\x21\xdc\xba\xde\xf0"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ str = [0xdef0_abcd_3412_7856, 0x7865_4321_dcba_def0].pack(pack_format(' ', 2))
+ str.should == "\xde\xf0\xab\xcd\x34\x12\x78\x56\x78\x65\x43\x21\xdc\xba\xde\xf0"
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/numeric_basic.rb b/spec/ruby/core/array/pack/shared/numeric_basic.rb
new file mode 100644
index 0000000000..6594914933
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/numeric_basic.rb
@@ -0,0 +1,50 @@
+describe :array_pack_numeric_basic, shared: true do
+ it "returns an empty String if count is zero" do
+ [1].pack(pack_format(0)).should == ""
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { [nil].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed true" do
+ -> { [true].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed false" do
+ -> { [false].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "returns a binary string" do
+ [0xFF].pack(pack_format).encoding.should == Encoding::BINARY
+ [0xE3, 0x81, 0x82].pack(pack_format(3)).encoding.should == Encoding::BINARY
+ end
+end
+
+describe :array_pack_integer, shared: true do
+ it "raises a TypeError when the object does not respond to #to_int" do
+ obj = mock('not an integer')
+ -> { [obj].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { ["5"].pack(pack_format) }.should.raise(TypeError)
+ end
+end
+
+describe :array_pack_float, shared: true do
+ it "raises a TypeError if a String does not represent a floating point number" do
+ -> { ["a"].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the object is not Numeric" do
+ obj = Object.new
+ -> { [obj].pack(pack_format) }.should.raise(TypeError, /can't convert Object into Float/)
+ end
+
+ it "raises a TypeError when the Numeric object does not respond to #to_f" do
+ klass = Class.new(Numeric)
+ obj = klass.new
+ -> { [obj].pack(pack_format) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/string.rb b/spec/ruby/core/array/pack/shared/string.rb
new file mode 100644
index 0000000000..b02257059f
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/string.rb
@@ -0,0 +1,48 @@
+# encoding: binary
+describe :array_pack_string, shared: true do
+ it "adds count bytes of a String to the output" do
+ ["abc"].pack(pack_format(2)).should == "ab"
+ end
+
+ it "implicitly has a count of one when no count is specified" do
+ ["abc"].pack(pack_format).should == "a"
+ end
+
+ it "does not add any bytes when the count is zero" do
+ ["abc"].pack(pack_format(0)).should == ""
+ end
+
+ it "is not affected by a previous count modifier" do
+ ["abcde", "defg"].pack(pack_format(3)+pack_format).should == "abcd"
+ end
+
+ it "raises an ArgumentError when the Array is empty" do
+ -> { [].pack(pack_format) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the Array has too few elements" do
+ -> { ["a"].pack(pack_format(nil, 2)) }.should.raise(ArgumentError)
+ end
+
+ it "calls #to_str to convert the element to a String" do
+ obj = mock('pack string')
+ obj.should_receive(:to_str).and_return("abc")
+
+ [obj].pack(pack_format).should == "a"
+ end
+
+ it "raises a TypeError when the object does not respond to #to_str" do
+ obj = mock("not a string")
+ -> { [obj].pack(pack_format) }.should.raise(TypeError)
+ end
+
+ it "returns a string in encoding of common to the concatenated results" do
+ f = pack_format("*")
+ [ [["\u{3042 3044 3046 3048}", 0x2000B].pack(f+"U"), Encoding::BINARY],
+ [["abcde\xd1", "\xFF\xFe\x81\x82"].pack(f+"u"), Encoding::BINARY],
+ [["a".dup.force_encoding("ascii"), "\xFF\xFe\x81\x82"].pack(f+"u"), Encoding::BINARY],
+ # under discussion [ruby-dev:37294]
+ [["\u{3042 3044 3046 3048}", 1].pack(f+"N"), Encoding::BINARY]
+ ].should be_computed_by(:encoding)
+ end
+end
diff --git a/spec/ruby/core/array/pack/shared/taint.rb b/spec/ruby/core/array/pack/shared/taint.rb
new file mode 100644
index 0000000000..2c2b011c34
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/taint.rb
@@ -0,0 +1,2 @@
+describe :array_pack_taint, shared: true do
+end
diff --git a/spec/ruby/core/array/pack/shared/unicode.rb b/spec/ruby/core/array/pack/shared/unicode.rb
new file mode 100644
index 0000000000..58ba8a8b23
--- /dev/null
+++ b/spec/ruby/core/array/pack/shared/unicode.rb
@@ -0,0 +1,96 @@
+# -*- encoding: utf-8 -*-
+
+describe :array_pack_unicode, shared: true do
+ it "encodes ASCII values as a Unicode codepoint" do
+ [ [[0], "\x00"],
+ [[1], "\x01"],
+ [[8], "\x08"],
+ [[15], "\x0f"],
+ [[24], "\x18"],
+ [[31], "\x1f"],
+ [[127], "\x7f"],
+ [[128], "\xc2\x80"],
+ [[129], "\xc2\x81"],
+ [[255], "\xc3\xbf"]
+ ].should be_computed_by(:pack, "U")
+ end
+
+ it "encodes UTF-8 BMP codepoints" do
+ [ [[0x80], "\xc2\x80"],
+ [[0x7ff], "\xdf\xbf"],
+ [[0x800], "\xe0\xa0\x80"],
+ [[0xffff], "\xef\xbf\xbf"]
+ ].should be_computed_by(:pack, "U")
+ end
+
+ it "constructs strings with valid encodings" do
+ str = [0x85].pack("U*")
+ str.should == "\xc2\x85"
+ str.valid_encoding?.should == true
+ end
+
+ it "encodes values larger than UTF-8 max codepoints" do
+ [
+ [[0x00110000], [244, 144, 128, 128].pack('C*').force_encoding('utf-8')],
+ [[0x04000000], [252, 132, 128, 128, 128, 128].pack('C*').force_encoding('utf-8')],
+ [[0x7FFFFFFF], [253, 191, 191, 191, 191, 191].pack('C*').force_encoding('utf-8')]
+ ].should be_computed_by(:pack, "U")
+ end
+
+ it "encodes UTF-8 max codepoints" do
+ [ [[0x10000], "\xf0\x90\x80\x80"],
+ [[0xfffff], "\xf3\xbf\xbf\xbf"],
+ [[0x100000], "\xf4\x80\x80\x80"],
+ [[0x10ffff], "\xf4\x8f\xbf\xbf"]
+ ].should be_computed_by(:pack, "U")
+ end
+
+ it "encodes the number of array elements specified by the count modifier" do
+ [ [[0x41, 0x42, 0x43, 0x44], "U2", "\x41\x42"],
+ [[0x41, 0x42, 0x43, 0x44], "U2U", "\x41\x42\x43"]
+ ].should be_computed_by(:pack)
+ end
+
+ it "encodes all remaining elements when passed the '*' modifier" do
+ [0x41, 0x42, 0x43, 0x44].pack("U*").should == "\x41\x42\x43\x44"
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(5)
+ [obj].pack("U").should == "\x05"
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return("5")
+ -> { [obj].pack("U") }.should.raise(TypeError)
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [1, 2, 3].pack("U\x00U")
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [1, 2, 3].pack("U U").should == "\x01\x02"
+ end
+
+ it "raises a RangeError if passed a negative number" do
+ -> { [-1].pack("U") }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if passed a number larger than an unsigned 32-bit integer" do
+ -> { [2**32].pack("U") }.should.raise(RangeError)
+ end
+
+ it "sets the output string to UTF-8 encoding" do
+ [ [[0x00].pack("U"), Encoding::UTF_8],
+ [[0x41].pack("U"), Encoding::UTF_8],
+ [[0x7F].pack("U"), Encoding::UTF_8],
+ [[0x80].pack("U"), Encoding::UTF_8],
+ [[0x10FFFF].pack("U"), Encoding::UTF_8]
+ ].should be_computed_by(:encoding)
+ end
+end
diff --git a/spec/ruby/core/array/pack/u_spec.rb b/spec/ruby/core/array/pack/u_spec.rb
new file mode 100644
index 0000000000..c6a0d77eb2
--- /dev/null
+++ b/spec/ruby/core/array/pack/u_spec.rb
@@ -0,0 +1,140 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/unicode'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'U'" do
+ it_behaves_like :array_pack_basic, 'U'
+ it_behaves_like :array_pack_basic_non_float, 'U'
+ it_behaves_like :array_pack_arguments, 'U'
+ it_behaves_like :array_pack_unicode, 'U'
+end
+
+describe "Array#pack with format 'u'" do
+ it_behaves_like :array_pack_basic, 'u'
+ it_behaves_like :array_pack_basic_non_float, 'u'
+ it_behaves_like :array_pack_arguments, 'u'
+ it_behaves_like :array_pack_taint, 'u'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack u string")
+ obj.should_receive(:to_str).and_return("``abcdef")
+ [obj].pack("u*").should == "(8&!A8F-D968`\n"
+ end
+
+ it "will not implicitly convert a number to a string" do
+ -> { [0].pack('u') }.should.raise(TypeError)
+ end
+
+ it "encodes an empty string as an empty string" do
+ [""].pack("u").should == ""
+ end
+
+ it "appends a newline to the end of the encoded string" do
+ ["a"].pack("u").should == "!80``\n"
+ end
+
+ it "encodes one element per directive" do
+ ["abc", "DEF"].pack("uu").should == "#86)C\n#1$5&\n"
+ end
+
+ it "prepends the length of each segment of the input string as the first character (+32) in each line of the output" do
+ ["abcdefghijklm"].pack("u7").should == "&86)C9&5F\n&9VAI:FML\n!;0``\n"
+ end
+
+ it "encodes 1, 2, or 3 characters in 4 output characters (uuencoding)" do
+ [ [["a"], "!80``\n"],
+ [["ab"], "\"86(`\n"],
+ [["abc"], "#86)C\n"],
+ [["abcd"], "$86)C9```\n"],
+ [["abcde"], "%86)C9&4`\n"],
+ [["abcdef"], "&86)C9&5F\n"],
+ [["abcdefg"], "'86)C9&5F9P``\n"],
+ ].should be_computed_by(:pack, "u")
+ end
+
+ it "emits a newline after complete groups of count / 3 input characters when passed a count modifier" do
+ ["abcdefg"].pack("u3").should == "#86)C\n#9&5F\n!9P``\n"
+ end
+
+ it "implicitly has a count of 45 when passed '*', 0, 1, 2 or no count modifier" do
+ s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ r = "M86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A86%A\n%86%A86$`\n"
+ [ [[s], "u", r],
+ [[s], "u*", r],
+ [[s], "u0", r],
+ [[s], "u1", r],
+ [[s], "u2", r],
+ ].should be_computed_by(:pack)
+ end
+
+ it "encodes all ascii characters" do
+ [ [["\x00\x01\x02\x03\x04\x05\x06"], "'``$\"`P0%!@``\n"],
+ [["\a\b\t\n\v\f\r"], "'!P@)\"@L,#0``\n"],
+ [["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"], ")\#@\\0$1(3%!46\n"],
+ [["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"], ")%Q@9&AL<'1X?\n"],
+ [["!\"\#$%&'()*+,-./"], "/(2(C)\"4F)R@I*BLL+2XO\n"],
+ [["0123456789"], "*,\#$R,S0U-C<X.0``\n"],
+ [[":;<=>?@"], "'.CL\\/3X_0```\n"],
+ [["ABCDEFGHIJKLMNOPQRSTUVWXYZ"], ":04)#1$5&1TA)2DM,34Y/4%%24U155E=865H`\n"],
+ [["[\\]^_`"], "&6UQ=7E]@\n"],
+ [["abcdefghijklmnopqrstuvwxyz"], ":86)C9&5F9VAI:FML;6YO<'%R<W1U=G=X>7H`\n"],
+ [["{|}~"], "$>WQ]?@``\n"],
+ [["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"], ")?\\*`PH'\"@L*#\n"],
+ [["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"], ")PH3\"A<*&PH?\"\n"],
+ [["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"], ")B,*)PHK\"B\\*,\n"],
+ [["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"], ")PHW\"CL*/PI#\"\n"],
+ [["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"], ")D<*2PI/\"E,*5\n"],
+ [["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"], ")PI;\"E\\*8PIG\"\n"],
+ [["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"], ")FL*;PIS\"G<*>\n"],
+ [["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"], ")PI_\"H,*APJ+\"\n"],
+ [["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"], ")H\\*DPJ7\"IL*G\n"],
+ [["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"], ")PJC\"J<*JPJO\"\n"],
+ [["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"], ")K,*MPJ[\"K\\*P\n"],
+ [["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"], ")PK'\"LL*SPK3\"\n"],
+ [["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"], ")M<*VPK?\"N,*Y\n"],
+ [["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"], ")PKK\"N\\*\\PKW\"\n"],
+ [["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"], ")OL*_PX#\#@<.\"\n"],
+ [["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"], ")PX/#A,.%PX;#\n"],
+ [["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"], ")A\\.(PXG#BL.+\n"],
+ [["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"], ")PXS#C<..PX_#\n"],
+ [["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"], ")D,.1PY+#D\\.4\n"],
+ [["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"], ")PY7#EL.7PYC#\n"],
+ [["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"], ")F<.:PYO#G,.=\n"],
+ [["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"], ")PY[#G\\.@PZ'#\n"],
+ [["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"], ")HL.CPZ3#I<.F\n"],
+ [["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"], ")PZ?#J,.IPZK#\n"],
+ [["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"], ")J\\.LPZW#KL.O\n"],
+ [["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"], ")P[##L<.RP[/#\n"],
+ [["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"], ")M,.UP[;#M\\.X\n"],
+ [["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"], ")P[G#NL.[P[S#\n"],
+ [["\xbd\xc3\xbe\xc3\xbf"], "%O<.^P[\\`\n"]
+ ].should be_computed_by(:pack, "u")
+ end
+
+ it "calls #to_str to convert an object to a String" do
+ obj = mock("pack m string")
+ obj.should_receive(:to_str).and_return("abc")
+ [obj].pack("u").should == "#86)C\n"
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ obj = mock("pack m non-string")
+ -> { [obj].pack("u") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> { [nil].pack("u") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> { [0].pack("u") }.should.raise(TypeError)
+ -> { [bignum_value].pack("u") }.should.raise(TypeError)
+ end
+
+ it "sets the output string to US-ASCII encoding" do
+ ["abcd"].pack("u").encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/array/pack/v_spec.rb b/spec/ruby/core/array/pack/v_spec.rb
new file mode 100644
index 0000000000..d3932c84af
--- /dev/null
+++ b/spec/ruby/core/array/pack/v_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+require_relative 'shared/integer'
+
+describe "Array#pack with format 'V'" do
+ it_behaves_like :array_pack_basic, 'V'
+ it_behaves_like :array_pack_basic_non_float, 'V'
+ it_behaves_like :array_pack_arguments, 'V'
+ it_behaves_like :array_pack_numeric_basic, 'V'
+ it_behaves_like :array_pack_integer, 'V'
+ it_behaves_like :array_pack_no_platform, 'V'
+ it_behaves_like :array_pack_32bit_le, 'V'
+end
+
+describe "Array#pack with format 'v'" do
+ it_behaves_like :array_pack_basic, 'v'
+ it_behaves_like :array_pack_basic_non_float, 'v'
+ it_behaves_like :array_pack_arguments, 'v'
+ it_behaves_like :array_pack_numeric_basic, 'v'
+ it_behaves_like :array_pack_integer, 'v'
+ it_behaves_like :array_pack_no_platform, 'v'
+ it_behaves_like :array_pack_16bit_le, 'v'
+end
diff --git a/spec/ruby/core/array/pack/w_spec.rb b/spec/ruby/core/array/pack/w_spec.rb
new file mode 100644
index 0000000000..263e2a2288
--- /dev/null
+++ b/spec/ruby/core/array/pack/w_spec.rb
@@ -0,0 +1,44 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/numeric_basic'
+
+describe "Array#pack with format 'w'" do
+ it_behaves_like :array_pack_basic, 'w'
+ it_behaves_like :array_pack_basic_non_float, 'w'
+ it_behaves_like :array_pack_arguments, 'w'
+ it_behaves_like :array_pack_numeric_basic, 'w'
+
+ it "encodes a BER-compressed integer" do
+ [ [[0], "\x00"],
+ [[1], "\x01"],
+ [[9999], "\xce\x0f"],
+ [[2**65], "\x84\x80\x80\x80\x80\x80\x80\x80\x80\x00"]
+ ].should be_computed_by(:pack, "w")
+ end
+
+ it "calls #to_int to convert the pack argument to an Integer" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(5)
+ [obj].pack("w").should == "\x05"
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ [1, 2, 3].pack("w\x00w")
+ }.should.raise(ArgumentError, /unknown pack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ [1, 2, 3].pack("w w").should == "\x01\x02"
+ end
+
+ it "raises an ArgumentError when passed a negative value" do
+ -> { [-1].pack("w") }.should.raise(ArgumentError)
+ end
+
+ it "returns a binary string" do
+ [1].pack('w').encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/array/pack/x_spec.rb b/spec/ruby/core/array/pack/x_spec.rb
new file mode 100644
index 0000000000..7ff587a01e
--- /dev/null
+++ b/spec/ruby/core/array/pack/x_spec.rb
@@ -0,0 +1,65 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+describe "Array#pack with format 'x'" do
+ it_behaves_like :array_pack_basic, 'x'
+ it_behaves_like :array_pack_basic_non_float, 'x'
+ it_behaves_like :array_pack_no_platform, 'x'
+
+ it "adds a NULL byte with an empty array" do
+ [].pack("x").should == "\x00"
+ end
+
+ it "adds a NULL byte without consuming an element" do
+ [1, 2].pack("CxC").should == "\x01\x00\x02"
+ end
+
+ it "is not affected by a previous count modifier" do
+ [].pack("x3x").should == "\x00\x00\x00\x00"
+ end
+
+ it "adds multiple NULL bytes when passed a count modifier" do
+ [].pack("x3").should == "\x00\x00\x00"
+ end
+
+ it "does not add a NULL byte if the count modifier is zero" do
+ [].pack("x0").should == ""
+ end
+
+ it "does not add a NULL byte when passed the '*' modifier" do
+ [].pack("x*").should == ""
+ [1, 2].pack("Cx*C").should == "\x01\x02"
+ end
+end
+
+describe "Array#pack with format 'X'" do
+ it_behaves_like :array_pack_basic, 'X'
+ it_behaves_like :array_pack_basic_non_float, 'X'
+ it_behaves_like :array_pack_no_platform, 'X'
+
+ it "reduces the output string by one byte at the point it is encountered" do
+ [1, 2, 3].pack("C2XC").should == "\x01\x03"
+ end
+
+ it "does not consume any elements" do
+ [1, 2, 3].pack("CXC").should == "\x02"
+ end
+
+ it "reduces the output string by multiple bytes when passed a count modifier" do
+ [1, 2, 3, 4, 5].pack("C2X2C").should == "\x03"
+ end
+
+ it "has no affect when passed the '*' modifier" do
+ [1, 2, 3].pack("C2X*C").should == "\x01\x02\x03"
+ end
+
+ it "raises an ArgumentError if the output string is empty" do
+ -> { [1, 2, 3].pack("XC") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the count modifier is greater than the bytes in the string" do
+ -> { [1, 2, 3].pack("C2X3") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/array/pack/z_spec.rb b/spec/ruby/core/array/pack/z_spec.rb
new file mode 100644
index 0000000000..5cd084c825
--- /dev/null
+++ b/spec/ruby/core/array/pack/z_spec.rb
@@ -0,0 +1,44 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/string'
+require_relative 'shared/taint'
+
+describe "Array#pack with format 'Z'" do
+ it_behaves_like :array_pack_basic, 'Z'
+ it_behaves_like :array_pack_basic_non_float, 'Z'
+ it_behaves_like :array_pack_no_platform, 'Z'
+ it_behaves_like :array_pack_string, 'Z'
+ it_behaves_like :array_pack_taint, 'Z'
+
+ it "calls #to_str to convert an Object to a String" do
+ obj = mock("pack Z string")
+ obj.should_receive(:to_str).and_return("``abcdef")
+ [obj].pack("Z*").should == "``abcdef\x00"
+ end
+
+ it "will not implicitly convert a number to a string" do
+ -> { [0].pack('Z') }.should.raise(TypeError)
+ end
+
+ it "adds all the bytes and appends a NULL byte when passed the '*' modifier" do
+ ["abc"].pack("Z*").should == "abc\x00"
+ end
+
+ it "pads the output with NULL bytes when the count exceeds the size of the String" do
+ ["abc"].pack("Z6").should == "abc\x00\x00\x00"
+ end
+
+ it "adds a NULL byte when the value is nil" do
+ [nil].pack("Z").should == "\x00"
+ end
+
+ it "pads the output with NULL bytes when the value is nil" do
+ [nil].pack("Z3").should == "\x00\x00\x00"
+ end
+
+ it "does not append a NULL byte when passed the '*' modifier and the value is nil" do
+ [nil].pack("Z*").should == "\x00"
+ end
+end
diff --git a/spec/ruby/core/array/partition_spec.rb b/spec/ruby/core/array/partition_spec.rb
new file mode 100644
index 0000000000..bd3f3a6b6f
--- /dev/null
+++ b/spec/ruby/core/array/partition_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#partition" do
+ it "returns two arrays" do
+ [].partition {}.should == [[], []]
+ end
+
+ it "returns in the left array values for which the block evaluates to true" do
+ ary = [0, 1, 2, 3, 4, 5]
+
+ ary.partition { |i| true }.should == [ary, []]
+ ary.partition { |i| 5 }.should == [ary, []]
+ ary.partition { |i| false }.should == [[], ary]
+ ary.partition { |i| nil }.should == [[], ary]
+ ary.partition { |i| i % 2 == 0 }.should == [[0, 2, 4], [1, 3, 5]]
+ ary.partition { |i| i / 3 == 0 }.should == [[0, 1, 2], [3, 4, 5]]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.partition { true }.should == [[empty], []]
+ empty.partition { false }.should == [[], [empty]]
+
+ array = ArraySpecs.recursive_array
+ array.partition { true }.should == [
+ [1, 'two', 3.0, array, array, array, array, array],
+ []
+ ]
+ condition = true
+ array.partition { condition = !condition }.should == [
+ ['two', array, array, array],
+ [1, 3.0, array, array]
+ ]
+ end
+
+ it "does not return subclass instances on Array subclasses" do
+ result = ArraySpecs::MyArray[1, 2, 3].partition { |x| x % 2 == 0 }
+ result.should.instance_of?(Array)
+ result[0].should.instance_of?(Array)
+ result[1].should.instance_of?(Array)
+ end
+end
diff --git a/spec/ruby/core/array/permutation_spec.rb b/spec/ruby/core/array/permutation_spec.rb
new file mode 100644
index 0000000000..b5df84b52b
--- /dev/null
+++ b/spec/ruby/core/array/permutation_spec.rb
@@ -0,0 +1,138 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+
+describe "Array#permutation" do
+
+ before :each do
+ @numbers = (1..3).to_a
+ @yielded = []
+ end
+
+ it "returns an Enumerator of all permutations when called without a block or arguments" do
+ enum = @numbers.permutation
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.sort.should == [
+ [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
+ ].sort
+ end
+
+ it "returns an Enumerator of permutations of given length when called with an argument but no block" do
+ enum = @numbers.permutation(1)
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.sort.should == [[1],[2],[3]]
+ end
+
+ it "yields all permutations to the block then returns self when called with block but no arguments" do
+ array = @numbers.permutation {|n| @yielded << n}
+ array.should.instance_of?(Array)
+ array.sort.should == @numbers.sort
+ @yielded.sort.should == [
+ [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
+ ].sort
+ end
+
+ it "yields all permutations of given length to the block then returns self when called with block and argument" do
+ array = @numbers.permutation(2) {|n| @yielded << n}
+ array.should.instance_of?(Array)
+ array.sort.should == @numbers.sort
+ @yielded.sort.should == [[1,2],[1,3],[2,1],[2,3],[3,1],[3,2]].sort
+ end
+
+ it "returns the empty permutation ([[]]) when the given length is 0" do
+ @numbers.permutation(0).to_a.should == [[]]
+ @numbers.permutation(0) { |n| @yielded << n }
+ @yielded.should == [[]]
+ end
+
+ it "returns the empty permutation([]) when called on an empty Array" do
+ [].permutation.to_a.should == [[]]
+ [].permutation { |n| @yielded << n }
+ @yielded.should == [[]]
+ end
+
+ it "returns no permutations when the given length has no permutations" do
+ @numbers.permutation(9).entries.size.should == 0
+ @numbers.permutation(9) { |n| @yielded << n }
+ @yielded.should == []
+ end
+
+ it "handles duplicate elements correctly" do
+ @numbers << 1
+ @numbers.permutation(2).sort.should == [
+ [1,1],[1,1],[1,2],[1,2],[1,3],[1,3],
+ [2,1],[2,1],[2,3],
+ [3,1],[3,1],[3,2]
+ ].sort
+ end
+
+ it "handles nested Arrays correctly" do
+ # The ugliness is due to the order of permutations returned by
+ # permutation being undefined combined with #sort croaking on Arrays of
+ # Arrays.
+ @numbers << [4,5]
+ got = @numbers.permutation(2).to_a
+ expected = [
+ [1, 2], [1, 3], [1, [4, 5]],
+ [2, 1], [2, 3], [2, [4, 5]],
+ [3, 1], [3, 2], [3, [4, 5]],
+ [[4, 5], 1], [[4, 5], 2], [[4, 5], 3]
+ ]
+ expected.each {|e| got.include?(e).should == true}
+ got.size.should == expected.size
+ end
+
+ it "truncates Float arguments" do
+ @numbers.permutation(3.7).to_a.sort.should ==
+ @numbers.permutation(3).to_a.sort
+ end
+
+ it "returns an Enumerator which works as expected even when the array was modified" do
+ @numbers = [1, 2]
+ enum = @numbers.permutation
+ @numbers << 3
+ enum.to_a.sort.should == [
+ [1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]
+ ].sort
+ end
+
+ it "generates from a defensive copy, ignoring mutations" do
+ accum = []
+ ary = [1,2,3]
+ ary.permutation(3) do |x|
+ accum << x
+ ary[0] = 5
+ end
+
+ accum.should == [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ describe "with an array size greater than 0" do
+ it "returns the descending factorial of array size and given length" do
+ @numbers.permutation(4).size.should == 0
+ @numbers.permutation(3).size.should == 6
+ @numbers.permutation(2).size.should == 6
+ @numbers.permutation(1).size.should == 3
+ @numbers.permutation(0).size.should == 1
+ end
+ it "returns the descending factorial of array size with array size when there's no param" do
+ @numbers.permutation.size.should == 6
+ [1,2,3,4].permutation.size.should == 24
+ [1].permutation.size.should == 1
+ end
+ end
+ describe "with an empty array" do
+ it "returns 1 when the given length is 0" do
+ [].permutation(0).size.should == 1
+ end
+ it "returns 1 when there's param" do
+ [].permutation.size.should == 1
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/array/plus_spec.rb b/spec/ruby/core/array/plus_spec.rb
new file mode 100644
index 0000000000..7ead927fc0
--- /dev/null
+++ b/spec/ruby/core/array/plus_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#+" do
+ it "concatenates two arrays" do
+ ([ 1, 2, 3 ] + [ 3, 4, 5 ]).should == [1, 2, 3, 3, 4, 5]
+ ([ 1, 2, 3 ] + []).should == [1, 2, 3]
+ ([] + [ 1, 2, 3 ]).should == [1, 2, 3]
+ ([] + []).should == []
+ end
+
+ it "can concatenate an array with itself" do
+ ary = [1, 2, 3]
+ (ary + ary).should == [1, 2, 3, 1, 2, 3]
+ end
+
+ describe "converts the passed argument to an Array using #to_ary" do
+ it "successfully concatenates the resulting array from the #to_ary call" do
+ obj = mock('["x", "y"]')
+ obj.should_receive(:to_ary).and_return(["x", "y"])
+ ([1, 2, 3] + obj).should == [1, 2, 3, "x", "y"]
+ end
+
+ it "raises a TypeError if the given argument can't be converted to an array" do
+ -> { [1, 2, 3] + nil }.should.raise(TypeError)
+ -> { [1, 2, 3] + "abc" }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to an Array" do
+ obj = mock("hello")
+ obj.should_receive(:to_ary).and_raise(NoMethodError)
+ -> { [1, 2, 3] + obj }.should.raise(NoMethodError)
+ end
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ (empty + empty).should == [empty, empty]
+
+ array = ArraySpecs.recursive_array
+ (empty + array).should == [empty, 1, 'two', 3.0, array, array, array, array, array]
+ (array + array).should == [
+ 1, 'two', 3.0, array, array, array, array, array,
+ 1, 'two', 3.0, array, array, array, array, array]
+ end
+
+ it "does return subclass instances with Array subclasses" do
+ (ArraySpecs::MyArray[1, 2, 3] + []).should.instance_of?(Array)
+ (ArraySpecs::MyArray[1, 2, 3] + ArraySpecs::MyArray[]).should.instance_of?(Array)
+ ([1, 2, 3] + ArraySpecs::MyArray[]).should.instance_of?(Array)
+ end
+
+ it "does not call to_ary on array subclasses" do
+ ([5, 6] + ArraySpecs::ToAryArray[1, 2]).should == [5, 6, 1, 2]
+ end
+end
diff --git a/spec/ruby/core/array/pop_spec.rb b/spec/ruby/core/array/pop_spec.rb
new file mode 100644
index 0000000000..069083331c
--- /dev/null
+++ b/spec/ruby/core/array/pop_spec.rb
@@ -0,0 +1,124 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#pop" do
+ it "removes and returns the last element of the array" do
+ a = ["a", 1, nil, true]
+
+ a.pop.should == true
+ a.should == ["a", 1, nil]
+
+ a.pop.should == nil
+ a.should == ["a", 1]
+
+ a.pop.should == 1
+ a.should == ["a"]
+
+ a.pop.should == "a"
+ a.should == []
+ end
+
+ it "returns nil if there are no more elements" do
+ [].pop.should == nil
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.pop.should == []
+
+ array = ArraySpecs.recursive_array
+ array.pop.should == [1, 'two', 3.0, array, array, array, array]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.pop }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on an empty frozen array" do
+ -> { ArraySpecs.empty_frozen_array.pop }.should.raise(FrozenError)
+ end
+
+ describe "passed a number n as an argument" do
+ it "removes and returns an array with the last n elements of the array" do
+ a = [1, 2, 3, 4, 5, 6]
+
+ a.pop(0).should == []
+ a.should == [1, 2, 3, 4, 5, 6]
+
+ a.pop(1).should == [6]
+ a.should == [1, 2, 3, 4, 5]
+
+ a.pop(2).should == [4, 5]
+ a.should == [1, 2, 3]
+
+ a.pop(3).should == [1, 2, 3]
+ a.should == []
+ end
+
+ it "returns an array with the last n elements even if shift was invoked" do
+ a = [1, 2, 3, 4]
+ a.shift
+ a.pop(3).should == [2, 3, 4]
+ end
+
+ it "returns a new empty array if there are no more elements" do
+ a = []
+ popped1 = a.pop(1)
+ popped1.should == []
+ a.should == []
+
+ popped2 = a.pop(2)
+ popped2.should == []
+ a.should == []
+
+ popped1.should_not.equal?(popped2)
+ end
+
+ it "returns whole elements if n exceeds size of the array" do
+ a = [1, 2, 3, 4, 5]
+ a.pop(6).should == [1, 2, 3, 4, 5]
+ a.should == []
+ end
+
+ it "does not return self even when it returns whole elements" do
+ a = [1, 2, 3, 4, 5]
+ a.pop(5).should_not.equal?(a)
+
+ a = [1, 2, 3, 4, 5]
+ a.pop(6).should_not.equal?(a)
+ end
+
+ it "raises an ArgumentError if n is negative" do
+ ->{ [1, 2, 3].pop(-1) }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert n to an Integer using #to_int" do
+ a = [1, 2, 3, 4]
+ a.pop(2.3).should == [3, 4]
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ a.should == [1, 2]
+ a.pop(obj).should == [1, 2]
+ a.should == []
+ end
+
+ it "raises a TypeError when the passed n cannot be coerced to Integer" do
+ ->{ [1, 2].pop("cat") }.should.raise(TypeError)
+ ->{ [1, 2].pop(nil) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if more arguments are passed" do
+ ->{ [1, 2].pop(1, 2) }.should.raise(ArgumentError)
+ end
+
+ it "does not return subclass instances with Array subclass" do
+ ArraySpecs::MyArray[1, 2, 3].pop(2).should.instance_of?(Array)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.pop(2) }.should.raise(FrozenError)
+ -> { ArraySpecs.frozen_array.pop(0) }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/array/prepend_spec.rb b/spec/ruby/core/array/prepend_spec.rb
new file mode 100644
index 0000000000..2d0ce31c71
--- /dev/null
+++ b/spec/ruby/core/array/prepend_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Array#prepend" do
+ it "is an alias of Array#unshift" do
+ Array.instance_method(:prepend).should == Array.instance_method(:unshift)
+ end
+end
diff --git a/spec/ruby/core/array/product_spec.rb b/spec/ruby/core/array/product_spec.rb
new file mode 100644
index 0000000000..837f0eaf34
--- /dev/null
+++ b/spec/ruby/core/array/product_spec.rb
@@ -0,0 +1,73 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#product" do
+ it "returns converted arguments using :to_ary" do
+ ->{ [1].product(2..3) }.should.raise(TypeError)
+ ar = ArraySpecs::ArrayConvertible.new(2,3)
+ [1].product(ar).should == [[1,2],[1,3]]
+ ar.called.should == :to_ary
+ end
+
+ it "returns converted arguments using :method_missing" do
+ ar = ArraySpecs::ArrayMethodMissing.new(2,3)
+ [1].product(ar).should == [[1,2],[1,3]]
+ end
+
+ it "returns the expected result" do
+ [1,2].product([3,4,5],[6,8]).should == [[1, 3, 6], [1, 3, 8], [1, 4, 6], [1, 4, 8], [1, 5, 6], [1, 5, 8],
+ [2, 3, 6], [2, 3, 8], [2, 4, 6], [2, 4, 8], [2, 5, 6], [2, 5, 8]]
+ end
+
+ it "has no required argument" do
+ [1,2].product.should == [[1],[2]]
+ end
+
+ it "returns an empty array when the argument is an empty array" do
+ [1, 2].product([]).should == []
+ end
+
+ it "does not attempt to produce an unreasonable number of products" do
+ a = (0..100).to_a
+ -> do
+ a.product(a, a, a, a, a, a, a, a, a, a)
+ end.should.raise(RangeError)
+ end
+
+ describe "when given a block" do
+ it "yields all combinations in turn" do
+ acc = []
+ [1,2].product([3,4,5],[6,8]){|array| acc << array}
+ acc.should == [[1, 3, 6], [1, 3, 8], [1, 4, 6], [1, 4, 8], [1, 5, 6], [1, 5, 8],
+ [2, 3, 6], [2, 3, 8], [2, 4, 6], [2, 4, 8], [2, 5, 6], [2, 5, 8]]
+
+ acc = []
+ [1,2].product([3,4,5],[],[6,8]){|array| acc << array}
+ acc.should.empty?
+ end
+
+ it "returns self" do
+ a = [1, 2, 3].freeze
+
+ a.product([1, 2]) { |p| p.first }.should == a
+ end
+
+ it "will ignore unreasonable numbers of products and yield anyway" do
+ a = (0..100).to_a
+ -> do
+ a.product(a, a, a, a, a, a, a, a, a, a)
+ end.should.raise(RangeError)
+ end
+ end
+
+ describe "when given an empty block" do
+ it "returns self" do
+ arr = [1,2]
+ arr.product([3,4,5],[6,8]){}.should.equal?(arr)
+ arr = []
+ arr.product([3,4,5],[6,8]){}.should.equal?(arr)
+ arr = [1,2]
+ arr.product([]){}.should.equal?(arr)
+ end
+ end
+end
diff --git a/spec/ruby/core/array/push_spec.rb b/spec/ruby/core/array/push_spec.rb
new file mode 100644
index 0000000000..6255a84371
--- /dev/null
+++ b/spec/ruby/core/array/push_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#push" do
+ it "appends the arguments to the array" do
+ a = [ "a", "b", "c" ]
+ a.push("d", "e", "f").should.equal?(a)
+ a.push.should == ["a", "b", "c", "d", "e", "f"]
+ a.push(5)
+ a.should == ["a", "b", "c", "d", "e", "f", 5]
+
+ a = [0, 1]
+ a.push(2)
+ a.should == [0, 1, 2]
+ end
+
+ it "isn't confused by previous shift" do
+ a = [ "a", "b", "c" ]
+ a.shift
+ a.push("foo")
+ a.should == ["b", "c", "foo"]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.push(:last).should == [empty, :last]
+
+ array = ArraySpecs.recursive_array
+ array.push(:last).should == [1, 'two', 3.0, array, array, array, array, array, :last]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.push(1) }.should.raise(FrozenError)
+ -> { ArraySpecs.frozen_array.push }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/rassoc_spec.rb b/spec/ruby/core/array/rassoc_spec.rb
new file mode 100644
index 0000000000..95e4ed1892
--- /dev/null
+++ b/spec/ruby/core/array/rassoc_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#rassoc" do
+ it "returns the first contained array whose second element is == object" do
+ ary = [[1, "a", 0.5], [2, "b"], [3, "b"], [4, "c"], [], [5], [6, "d"]]
+ ary.rassoc("a").should == [1, "a", 0.5]
+ ary.rassoc("b").should == [2, "b"]
+ ary.rassoc("d").should == [6, "d"]
+ ary.rassoc("z").should == nil
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.rassoc([]).should == nil
+ [[empty, empty]].rassoc(empty).should == [empty, empty]
+
+ array = ArraySpecs.recursive_array
+ array.rassoc(array).should == nil
+ [[empty, array]].rassoc(array).should == [empty, array]
+ end
+
+ it "calls elem == obj on the second element of each contained array" do
+ key = 'foobar'
+ o = mock('foobar')
+ def o.==(other); other == 'foobar'; end
+
+ [[1, :foobar], [2, o], [3, mock('foo')]].rassoc(key).should == [2, o]
+ end
+
+ it "does not check the last element in each contained but specifically the second" do
+ key = 'foobar'
+ o = mock('foobar')
+ def o.==(other); other == 'foobar'; end
+
+ [[1, :foobar, o], [2, o, 1], [3, mock('foo')]].rassoc(key).should == [2, o, 1]
+ end
+
+ it "calls to_ary on non-array elements" do
+ s1 = [1, 2]
+ s2 = ArraySpecs::ArrayConvertible.new(2, 3)
+ a = [s1, s2]
+
+ s1.should_not_receive(:to_ary)
+ a.rassoc(2).should.equal?(s1)
+
+ a.rassoc(3).should == [2, 3]
+ s2.called.should.equal?(:to_ary)
+ end
+end
diff --git a/spec/ruby/core/array/reject_spec.rb b/spec/ruby/core/array/reject_spec.rb
new file mode 100644
index 0000000000..8d237b3a75
--- /dev/null
+++ b/spec/ruby/core/array/reject_spec.rb
@@ -0,0 +1,158 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorize'
+require_relative 'shared/delete_if'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Array#reject" do
+ it "returns a new array without elements for which block is true" do
+ ary = [1, 2, 3, 4, 5]
+ ary.reject { true }.should == []
+ ary.reject { false }.should == ary
+ ary.reject { false }.should_not.equal? ary
+ ary.reject { nil }.should == ary
+ ary.reject { nil }.should_not.equal? ary
+ ary.reject { 5 }.should == []
+ ary.reject { |i| i < 3 }.should == [3, 4, 5]
+ ary.reject { |i| i % 2 == 0 }.should == [1, 3, 5]
+ end
+
+ it "returns self when called on an Array emptied with #shift" do
+ array = [1]
+ array.shift
+ array.reject { |x| true }.should == []
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.reject { false }.should == [empty]
+ empty.reject { true }.should == []
+
+ array = ArraySpecs.recursive_array
+ array.reject { false }.should == [1, 'two', 3.0, array, array, array, array, array]
+ array.reject { true }.should == []
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].reject { |x| x % 2 == 0 }.should.instance_of?(Array)
+ end
+
+ it "does not retain instance variables" do
+ array = []
+ array.instance_variable_set("@variable", "value")
+ array.reject { false }.instance_variable_get("@variable").should == nil
+ end
+
+ it_behaves_like :enumeratorize, :reject
+ it_behaves_like :enumeratorized_with_origin_size, :reject, [1,2,3]
+end
+
+describe "Array#reject" do
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :reject
+end
+
+describe "Array#reject!" do
+ it "removes elements for which block is true" do
+ a = [3, 4, 5, 6, 7, 8, 9, 10, 11]
+ a.reject! { |i| i % 2 == 0 }.should.equal?(a)
+ a.should == [3, 5, 7, 9, 11]
+ a.reject! { |i| i > 8 }
+ a.should == [3, 5, 7]
+ a.reject! { |i| i < 4 }
+ a.should == [5, 7]
+ a.reject! { |i| i == 5 }
+ a.should == [7]
+ a.reject! { true }
+ a.should == []
+ a.reject! { true }
+ a.should == []
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty_dup = empty.dup
+ empty.reject! { false }.should == nil
+ empty.should == empty_dup
+
+ empty = ArraySpecs.empty_recursive_array
+ empty.reject! { true }.should == []
+ empty.should == []
+
+ array = ArraySpecs.recursive_array
+ array_dup = array.dup
+ array.reject! { false }.should == nil
+ array.should == array_dup
+
+ array = ArraySpecs.recursive_array
+ array.reject! { true }.should == []
+ array.should == []
+ end
+
+ it "returns nil when called on an Array emptied with #shift" do
+ array = [1]
+ array.shift
+ array.reject! { |x| true }.should == nil
+ end
+
+ it "returns nil if no changes are made" do
+ a = [1, 2, 3]
+
+ a.reject! { |i| i < 0 }.should == nil
+
+ a.reject! { true }
+ a.reject! { true }.should == nil
+ end
+
+ it "returns an Enumerator if no block given, and the array is frozen" do
+ ArraySpecs.frozen_array.reject!.should.instance_of?(Enumerator)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.reject! {} }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on an empty frozen array" do
+ -> { ArraySpecs.empty_frozen_array.reject! {} }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on a frozen array only during iteration if called without a block" do
+ enum = ArraySpecs.frozen_array.reject!
+ -> { enum.each {} }.should.raise(FrozenError)
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.reject! { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "only removes elements for which the block returns true, keeping the element which raised an error." do
+ a = [1, 2, 3, 4]
+ begin
+ a.reject! do |x|
+ case x
+ when 2 then true
+ when 3 then raise StandardError, 'Oops'
+ else false
+ end
+ end
+ rescue StandardError
+ end
+
+ a.should == [1, 3, 4]
+ end
+
+ it_behaves_like :enumeratorize, :reject!
+ it_behaves_like :enumeratorized_with_origin_size, :reject!, [1,2,3]
+ it_behaves_like :delete_if, :reject!
+end
+
+describe "Array#reject!" do
+ @value_to_return = -> _ { false }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :reject!
+end
diff --git a/spec/ruby/core/array/repeated_combination_spec.rb b/spec/ruby/core/array/repeated_combination_spec.rb
new file mode 100644
index 0000000000..a714f05f54
--- /dev/null
+++ b/spec/ruby/core/array/repeated_combination_spec.rb
@@ -0,0 +1,84 @@
+require_relative '../../spec_helper'
+
+describe "Array#repeated_combination" do
+ before :each do
+ @array = [10, 11, 12]
+ end
+
+ it "returns an enumerator when no block is provided" do
+ @array.repeated_combination(2).should.instance_of?(Enumerator)
+ end
+
+ it "returns self when a block is given" do
+ @array.repeated_combination(2){}.should.equal?(@array)
+ end
+
+ it "yields nothing for negative length and return self" do
+ @array.repeated_combination(-1){ fail }.should.equal?(@array)
+ @array.repeated_combination(-10){ fail }.should.equal?(@array)
+ end
+
+ it "yields the expected repeated_combinations" do
+ @array.repeated_combination(2).to_a.sort.should == [[10, 10], [10, 11], [10, 12], [11, 11], [11, 12], [12, 12]]
+ @array.repeated_combination(3).to_a.sort.should == [[10, 10, 10], [10, 10, 11], [10, 10, 12], [10, 11, 11], [10, 11, 12],
+ [10, 12, 12], [11, 11, 11], [11, 11, 12], [11, 12, 12], [12, 12, 12]]
+ end
+
+ it "yields [] when length is 0" do
+ @array.repeated_combination(0).to_a.should == [[]] # one repeated_combination of length 0
+ [].repeated_combination(0).to_a.should == [[]] # one repeated_combination of length 0
+ end
+
+ it "yields nothing when the array is empty and num is non zero" do
+ [].repeated_combination(5).to_a.should == [] # one repeated_combination of length 0
+ end
+
+ it "yields a partition consisting of only singletons" do
+ @array.repeated_combination(1).sort.to_a.should == [[10],[11],[12]]
+ end
+
+ it "accepts sizes larger than the original array" do
+ @array.repeated_combination(4).to_a.sort.should ==
+ [[10, 10, 10, 10], [10, 10, 10, 11], [10, 10, 10, 12],
+ [10, 10, 11, 11], [10, 10, 11, 12], [10, 10, 12, 12],
+ [10, 11, 11, 11], [10, 11, 11, 12], [10, 11, 12, 12],
+ [10, 12, 12, 12], [11, 11, 11, 11], [11, 11, 11, 12],
+ [11, 11, 12, 12], [11, 12, 12, 12], [12, 12, 12, 12]]
+ end
+
+ it "generates from a defensive copy, ignoring mutations" do
+ accum = []
+ @array.repeated_combination(2) do |x|
+ accum << x
+ @array[0] = 1
+ end
+ accum.sort.should == [[10, 10], [10, 11], [10, 12], [11, 11], [11, 12], [12, 12]]
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns 0 when the combination_size is < 0" do
+ @array.repeated_combination(-1).size.should == 0
+ [].repeated_combination(-2).size.should == 0
+ end
+
+ it "returns 1 when the combination_size is 0" do
+ @array.repeated_combination(0).size.should == 1
+ [].repeated_combination(0).size.should == 1
+ end
+
+ it "returns the binomial coefficient between combination_size and array size + combination_size -1" do
+ @array.repeated_combination(5).size.should == 21
+ @array.repeated_combination(4).size.should == 15
+ @array.repeated_combination(3).size.should == 10
+ @array.repeated_combination(2).size.should == 6
+ @array.repeated_combination(1).size.should == 3
+ @array.repeated_combination(0).size.should == 1
+ [].repeated_combination(0).size.should == 1
+ [].repeated_combination(1).size.should == 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/array/repeated_permutation_spec.rb b/spec/ruby/core/array/repeated_permutation_spec.rb
new file mode 100644
index 0000000000..c54a8c0c2b
--- /dev/null
+++ b/spec/ruby/core/array/repeated_permutation_spec.rb
@@ -0,0 +1,94 @@
+require_relative '../../spec_helper'
+
+
+describe "Array#repeated_permutation" do
+
+ before :each do
+ @numbers = [10, 11, 12]
+ @permutations = [[10, 10], [10, 11], [10, 12], [11, 10], [11, 11], [11, 12], [12, 10], [12, 11], [12, 12]]
+ end
+
+ it "returns an Enumerator of all repeated permutations of given length when called without a block" do
+ enum = @numbers.repeated_permutation(2)
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.sort.should == @permutations
+ end
+
+ it "yields all repeated_permutations to the block then returns self when called with block but no arguments" do
+ yielded = []
+ @numbers.repeated_permutation(2) {|n| yielded << n}.should.equal?(@numbers)
+ yielded.sort.should == @permutations
+ end
+
+ it "yields the empty repeated_permutation ([[]]) when the given length is 0" do
+ @numbers.repeated_permutation(0).to_a.should == [[]]
+ [].repeated_permutation(0).to_a.should == [[]]
+ end
+
+ it "does not yield when called on an empty Array with a nonzero argument" do
+ [].repeated_permutation(10).to_a.should == []
+ end
+
+ it "handles duplicate elements correctly" do
+ @numbers[-1] = 10
+ @numbers.repeated_permutation(2).sort.should ==
+ [[10, 10], [10, 10], [10, 10], [10, 10], [10, 11], [10, 11], [11, 10], [11, 10], [11, 11]]
+ end
+
+ it "truncates Float arguments" do
+ @numbers.repeated_permutation(3.7).to_a.sort.should ==
+ @numbers.repeated_permutation(3).to_a.sort
+ end
+
+ it "returns an Enumerator which works as expected even when the array was modified" do
+ @numbers.shift
+ enum = @numbers.repeated_permutation(2)
+ @numbers.unshift 10
+ enum.to_a.sort.should == @permutations
+ end
+
+ it "allows permutations larger than the number of elements" do
+ [1,2].repeated_permutation(3).sort.should ==
+ [[1, 1, 1], [1, 1, 2], [1, 2, 1],
+ [1, 2, 2], [2, 1, 1], [2, 1, 2],
+ [2, 2, 1], [2, 2, 2]]
+ end
+
+ it "generates from a defensive copy, ignoring mutations" do
+ accum = []
+ ary = [1,2]
+ ary.repeated_permutation(3) do |x|
+ accum << x
+ ary[0] = 5
+ end
+
+ accum.sort.should ==
+ [[1, 1, 1], [1, 1, 2], [1, 2, 1],
+ [1, 2, 2], [2, 1, 1], [2, 1, 2],
+ [2, 2, 1], [2, 2, 2]]
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns 0 when combination_size is < 0" do
+ @numbers.repeated_permutation(-1).size.should == 0
+ [].repeated_permutation(-1).size.should == 0
+ end
+
+ it "returns array size ** combination_size" do
+ @numbers.repeated_permutation(4).size.should == 81
+ @numbers.repeated_permutation(3).size.should == 27
+ @numbers.repeated_permutation(2).size.should == 9
+ @numbers.repeated_permutation(1).size.should == 3
+ @numbers.repeated_permutation(0).size.should == 1
+ [].repeated_permutation(4).size.should == 0
+ [].repeated_permutation(3).size.should == 0
+ [].repeated_permutation(2).size.should == 0
+ [].repeated_permutation(1).size.should == 0
+ [].repeated_permutation(0).size.should == 1
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/array/replace_spec.rb b/spec/ruby/core/array/replace_spec.rb
new file mode 100644
index 0000000000..ee6a98a646
--- /dev/null
+++ b/spec/ruby/core/array/replace_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#replace" do
+ it "replaces the elements with elements from other array" do
+ a = [1, 2, 3, 4, 5]
+ b = ['a', 'b', 'c']
+ a.replace(b).should.equal?(a)
+ a.should == b
+ a.should_not.equal?(b)
+
+ a.replace([4] * 10)
+ a.should == [4] * 10
+
+ a.replace([])
+ a.should == []
+ end
+
+ it "properly handles recursive arrays" do
+ orig = [1, 2, 3]
+ empty = ArraySpecs.empty_recursive_array
+ orig.replace(empty)
+ orig.should == empty
+
+ array = ArraySpecs.recursive_array
+ orig.replace(array)
+ orig.should == array
+ end
+
+ it "returns self" do
+ ary = [1, 2, 3]
+ other = [:a, :b, :c]
+ ary.replace(other).should.equal?(ary)
+ end
+
+ it "does not make self dependent to the original array" do
+ ary = [1, 2, 3]
+ other = [:a, :b, :c]
+ ary.replace(other)
+ ary.should == [:a, :b, :c]
+ ary << :d
+ ary.should == [:a, :b, :c, :d]
+ other.should == [:a, :b, :c]
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('to_ary')
+ obj.stub!(:to_ary).and_return([1, 2, 3])
+ [].replace(obj).should == [1, 2, 3]
+ end
+
+ it "does not call #to_ary on Array subclasses" do
+ obj = ArraySpecs::ToAryArray[5, 6, 7]
+ obj.should_not_receive(:to_ary)
+ [].replace(ArraySpecs::ToAryArray[5, 6, 7]).should == [5, 6, 7]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> {
+ ArraySpecs.frozen_array.replace(ArraySpecs.frozen_array)
+ }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/reverse_each_spec.rb b/spec/ruby/core/array/reverse_each_spec.rb
new file mode 100644
index 0000000000..8fa5ce6da1
--- /dev/null
+++ b/spec/ruby/core/array/reverse_each_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorize'
+require_relative '../enumerable/shared/enumeratorized'
+
+# Modifying a collection while the contents are being iterated
+# gives undefined behavior. See
+# https://blade.ruby-lang.org/ruby-core/23633
+
+describe "Array#reverse_each" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "traverses array in reverse order and pass each element to block" do
+ [1, 3, 4, 6].reverse_each { |i| ScratchPad << i }
+ ScratchPad.recorded.should == [6, 4, 3, 1]
+ end
+
+ it "returns self" do
+ a = [:a, :b, :c]
+ a.reverse_each { |x| }.should.equal?(a)
+ end
+
+ it "yields only the top level element of an empty recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.reverse_each { |i| ScratchPad << i }
+ ScratchPad.recorded.should == [empty]
+ end
+
+ it "yields only the top level element of a recursive array" do
+ array = ArraySpecs.recursive_array
+ array.reverse_each { |i| ScratchPad << i }
+ ScratchPad.recorded.should == [array, array, array, array, array, 3.0, 'two', 1]
+ end
+
+ it "returns the correct size when no block is given" do
+ [1, 2, 3].reverse_each.size.should == 3
+ end
+
+ it "tolerates increasing an array size during iteration" do
+ array = [:a, :b, :c]
+ ScratchPad.record []
+ i = 0
+
+ array.reverse_each do |e|
+ ScratchPad << e
+ array.prepend i if i < 100
+ i += 1
+ end
+
+ ScratchPad.recorded.should == [:c, :a, 1]
+ end
+
+ it_behaves_like :enumeratorize, :reverse_each
+ it_behaves_like :enumeratorized_with_origin_size, :reverse_each, [1,2,3]
+end
diff --git a/spec/ruby/core/array/reverse_spec.rb b/spec/ruby/core/array/reverse_spec.rb
new file mode 100644
index 0000000000..f25a484be8
--- /dev/null
+++ b/spec/ruby/core/array/reverse_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#reverse" do
+ it "returns a new array with the elements in reverse order" do
+ [].reverse.should == []
+ [1, 3, 5, 2].reverse.should == [2, 5, 3, 1]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.reverse.should == empty
+
+ array = ArraySpecs.recursive_array
+ array.reverse.should == [array, array, array, array, array, 3.0, 'two', 1]
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].reverse.should.instance_of?(Array)
+ end
+end
+
+describe "Array#reverse!" do
+ it "reverses the elements in place" do
+ a = [6, 3, 4, 2, 1]
+ a.reverse!.should.equal?(a)
+ a.should == [1, 2, 4, 3, 6]
+ [].reverse!.should == []
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.reverse!.should == [empty]
+
+ array = ArraySpecs.recursive_array
+ array.reverse!.should == [array, array, array, array, array, 3.0, 'two', 1]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.reverse! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/rindex_spec.rb b/spec/ruby/core/array/rindex_spec.rb
new file mode 100644
index 0000000000..858c39dc92
--- /dev/null
+++ b/spec/ruby/core/array/rindex_spec.rb
@@ -0,0 +1,95 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../enumerable/shared/enumeratorized'
+
+# Modifying a collection while the contents are being iterated
+# gives undefined behavior. See
+# https://blade.ruby-lang.org/ruby-core/23633
+
+describe "Array#rindex" do
+ it "returns the first index backwards from the end where element == to object" do
+ key = 3
+ uno = mock('one')
+ dos = mock('two')
+ tres = mock('three')
+ tres.should_receive(:==).any_number_of_times.and_return(false)
+ dos.should_receive(:==).any_number_of_times.and_return(true)
+ uno.should_not_receive(:==)
+ ary = [uno, dos, tres]
+
+ ary.rindex(key).should == 1
+ end
+
+ it "returns size-1 if last element == to object" do
+ [2, 1, 3, 2, 5].rindex(5).should == 4
+ end
+
+ it "returns 0 if only first element == to object" do
+ [2, 1, 3, 1, 5].rindex(2).should == 0
+ end
+
+ it "returns nil if no element == to object" do
+ [1, 1, 3, 2, 1, 3].rindex(4).should == nil
+ end
+
+ it "returns correct index even after delete_at" do
+ array = ["fish", "bird", "lion", "cat"]
+ array.delete_at(0)
+ array.rindex("lion").should == 1
+ end
+
+ it "properly handles empty recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.rindex(empty).should == 0
+ empty.rindex(1).should == nil
+ end
+
+ it "properly handles recursive arrays" do
+ array = ArraySpecs.recursive_array
+ array.rindex(1).should == 0
+ array.rindex(array).should == 7
+ end
+
+ it "accepts a block instead of an argument" do
+ [4, 2, 1, 5, 1, 3].rindex { |x| x < 2 }.should == 4
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ [4, 2, 1, 5, 1, 3].rindex(5) { |x| x < 2 }.should == 3
+ }.should complain(/given block not used/)
+ end
+
+ it "rechecks the array size during iteration" do
+ ary = [4, 2, 1, 5, 1, 3]
+ seen = []
+ ary.rindex { |x| seen << x; ary.clear; false }
+
+ seen.should == [3]
+ end
+
+ it "tolerates increasing an array size during iteration" do
+ array = [:a, :b, :c]
+ ScratchPad.record []
+ i = 0
+
+ array.rindex do |e|
+ ScratchPad << e
+ array.prepend i if i < 100
+ i += 1
+ false
+ end
+
+ ScratchPad.recorded.should == [:c, :a, 1]
+ end
+
+ describe "given no argument and no block" do
+ it "produces an Enumerator" do
+ enum = [4, 2, 1, 5, 1, 3].rindex
+ enum.should.instance_of?(Enumerator)
+ enum.each { |x| x < 2 }.should == 4
+ end
+ end
+
+ it_behaves_like :enumeratorized_with_unknown_size, :bsearch, [1,2,3]
+end
diff --git a/spec/ruby/core/array/rotate_spec.rb b/spec/ruby/core/array/rotate_spec.rb
new file mode 100644
index 0000000000..009ce5ed49
--- /dev/null
+++ b/spec/ruby/core/array/rotate_spec.rb
@@ -0,0 +1,129 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#rotate" do
+ describe "when passed no argument" do
+ it "returns a copy of the array with the first element moved at the end" do
+ [1, 2, 3, 4, 5].rotate.should == [2, 3, 4, 5, 1]
+ end
+ end
+
+ describe "with an argument n" do
+ it "returns a copy of the array with the first (n % size) elements moved at the end" do
+ a = [1, 2, 3, 4, 5]
+ a.rotate( 2).should == [3, 4, 5, 1, 2]
+ a.rotate( -1).should == [5, 1, 2, 3, 4]
+ a.rotate(-21).should == [5, 1, 2, 3, 4]
+ a.rotate( 13).should == [4, 5, 1, 2, 3]
+ a.rotate( 0).should == a
+ end
+
+ it "coerces the argument using to_int" do
+ [1, 2, 3].rotate(2.6).should == [3, 1, 2]
+
+ obj = mock('integer_like')
+ obj.should_receive(:to_int).and_return(2)
+ [1, 2, 3].rotate(obj).should == [3, 1, 2]
+ end
+
+ it "raises a TypeError if not passed an integer-like argument" do
+ -> {
+ [1, 2].rotate(nil)
+ }.should.raise(TypeError)
+ -> {
+ [1, 2].rotate("4")
+ }.should.raise(TypeError)
+ end
+ end
+
+ it "returns a copy of the array when its length is one or zero" do
+ [1].rotate.should == [1]
+ [1].rotate(2).should == [1]
+ [1].rotate(-42).should == [1]
+ [ ].rotate.should == []
+ [ ].rotate(2).should == []
+ [ ].rotate(-42).should == []
+ end
+
+ it "does not mutate the receiver" do
+ -> {
+ [].freeze.rotate
+ [2].freeze.rotate(2)
+ [1,2,3].freeze.rotate(-3)
+ }.should_not.raise
+ end
+
+ it "does not return self" do
+ a = [1, 2, 3]
+ a.rotate.should_not.equal?(a)
+ a = []
+ a.rotate(0).should_not.equal?(a)
+ end
+
+ it "does not return subclass instance for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].rotate.should.instance_of?(Array)
+ end
+end
+
+describe "Array#rotate!" do
+ describe "when passed no argument" do
+ it "moves the first element to the end and returns self" do
+ a = [1, 2, 3, 4, 5]
+ a.rotate!.should.equal?(a)
+ a.should == [2, 3, 4, 5, 1]
+ end
+ end
+
+ describe "with an argument n" do
+ it "moves the first (n % size) elements at the end and returns self" do
+ a = [1, 2, 3, 4, 5]
+ a.rotate!(2).should.equal?(a)
+ a.should == [3, 4, 5, 1, 2]
+ a.rotate!(-12).should.equal?(a)
+ a.should == [1, 2, 3, 4, 5]
+ a.rotate!(13).should.equal?(a)
+ a.should == [4, 5, 1, 2, 3]
+ end
+
+ it "coerces the argument using to_int" do
+ [1, 2, 3].rotate!(2.6).should == [3, 1, 2]
+
+ obj = mock('integer_like')
+ obj.should_receive(:to_int).and_return(2)
+ [1, 2, 3].rotate!(obj).should == [3, 1, 2]
+ end
+
+ it "raises a TypeError if not passed an integer-like argument" do
+ -> {
+ [1, 2].rotate!(nil)
+ }.should.raise(TypeError)
+ -> {
+ [1, 2].rotate!("4")
+ }.should.raise(TypeError)
+ end
+ end
+
+ it "does nothing and returns self when the length is zero or one" do
+ a = [1]
+ a.rotate!.should.equal?(a)
+ a.should == [1]
+ a.rotate!(2).should.equal?(a)
+ a.should == [1]
+ a.rotate!(-21).should.equal?(a)
+ a.should == [1]
+
+ a = []
+ a.rotate!.should.equal?(a)
+ a.should == []
+ a.rotate!(2).should.equal?(a)
+ a.should == []
+ a.rotate!(-21).should.equal?(a)
+ a.should == []
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { [1, 2, 3].freeze.rotate!(0) }.should.raise(FrozenError)
+ -> { [1].freeze.rotate!(42) }.should.raise(FrozenError)
+ -> { [].freeze.rotate! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/array/sample_spec.rb b/spec/ruby/core/array/sample_spec.rb
new file mode 100644
index 0000000000..fd443b47de
--- /dev/null
+++ b/spec/ruby/core/array/sample_spec.rb
@@ -0,0 +1,155 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#sample" do
+ it "samples evenly" do
+ ArraySpecs.measure_sample_fairness(4, 1, 400)
+ ArraySpecs.measure_sample_fairness(4, 2, 400)
+ ArraySpecs.measure_sample_fairness(4, 3, 400)
+ ArraySpecs.measure_sample_fairness(40, 3, 400)
+ ArraySpecs.measure_sample_fairness(40, 4, 400)
+ ArraySpecs.measure_sample_fairness(40, 8, 400)
+ ArraySpecs.measure_sample_fairness(40, 16, 400)
+ ArraySpecs.measure_sample_fairness_large_sample_size(100, 80, 4000)
+ end
+
+ it "returns nil for an empty Array" do
+ [].sample.should == nil
+ end
+
+ it "returns nil for an empty array when called without n and a Random is given" do
+ [].sample(random: Random.new(42)).should == nil
+ end
+
+ it "returns a single value when not passed a count" do
+ [4].sample.should.equal?(4)
+ end
+
+ it "returns a single value when not passed a count and a Random is given" do
+ [4].sample(random: Random.new(42)).should.equal?(4)
+ end
+
+ it "returns a single value when not passed a count and a Random class is given" do
+ [4].sample(random: Random).should.equal?(4)
+ end
+
+ it "returns an empty Array when passed zero" do
+ [4].sample(0).should == []
+ end
+
+ it "returns an Array of elements when passed a count" do
+ [1, 2, 3, 4].sample(3).should.instance_of?(Array)
+ end
+
+ it "returns elements from the Array" do
+ array = [1, 2, 3, 4]
+ array.sample(3).all? { |x| array.should.include?(x) }
+ end
+
+ it "returns at most the number of elements in the Array" do
+ array = [1, 2, 3, 4]
+ result = array.sample(20)
+ result.size.should == 4
+ end
+
+ it "does not return the same value if the Array has unique values" do
+ array = [1, 2, 3, 4]
+ result = array.sample(20)
+ result.sort.should == array
+ end
+
+ it "may return the same value if the array is not unique" do
+ [4, 4].sample(2).should == [4,4]
+ end
+
+ it "calls #to_int to convert the count when passed an Object" do
+ [1, 2, 3, 4].sample(mock_int(2)).size.should == 2
+ end
+
+ it "raises ArgumentError when passed a negative count" do
+ -> { [1, 2].sample(-1) }.should.raise(ArgumentError)
+ end
+
+ it "does not return subclass instances with Array subclass" do
+ ArraySpecs::MyArray[1, 2, 3].sample(2).should.instance_of?(Array)
+ end
+
+ describe "with options" do
+ it "calls #rand on the Object passed by the :random key in the arguments Hash" do
+ obj = mock("array_sample_random")
+ obj.should_receive(:rand).and_return(0.5)
+
+ [1, 2].sample(random: obj).should.instance_of?(Integer)
+ end
+
+ it "raises a NoMethodError if an object passed for the RNG does not define #rand" do
+ obj = BasicObject.new
+
+ -> { [1, 2].sample(random: obj) }.should.raise(NoMethodError)
+ end
+
+ describe "when the object returned by #rand is an Integer" do
+ it "uses the integer as index" do
+ random = mock("array_sample_random_ret")
+ random.should_receive(:rand).and_return(0)
+
+ [1, 2].sample(random: random).should == 1
+
+ random = mock("array_sample_random_ret")
+ random.should_receive(:rand).and_return(1)
+
+ [1, 2].sample(random: random).should == 2
+ end
+
+ it "raises a RangeError if the value is less than zero" do
+ random = mock("array_sample_random")
+ random.should_receive(:rand).and_return(-1)
+
+ -> { [1, 2].sample(random: random) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if the value is equal to the Array size" do
+ random = mock("array_sample_random")
+ random.should_receive(:rand).and_return(2)
+
+ -> { [1, 2].sample(random: random) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if the value is greater than the Array size" do
+ random = mock("array_sample_random")
+ random.should_receive(:rand).and_return(3)
+
+ -> { [1, 2].sample(random: random) }.should.raise(RangeError)
+ end
+ end
+ end
+
+ describe "when the object returned by #rand is not an Integer but responds to #to_int" do
+ it "calls #to_int on the Object" do
+ value = mock("array_sample_random_value")
+ value.should_receive(:to_int).and_return(1)
+ random = mock("array_sample_random")
+ random.should_receive(:rand).and_return(value)
+
+ [1, 2].sample(random: random).should == 2
+ end
+
+ it "raises a RangeError if the value is less than zero" do
+ value = mock("array_sample_random_value")
+ value.should_receive(:to_int).and_return(-1)
+ random = mock("array_sample_random")
+ random.should_receive(:rand).and_return(value)
+
+ -> { [1, 2].sample(random: random) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if the value is equal to the Array size" do
+ value = mock("array_sample_random_value")
+ value.should_receive(:to_int).and_return(2)
+ random = mock("array_sample_random")
+ random.should_receive(:rand).and_return(value)
+
+ -> { [1, 2].sample(random: random) }.should.raise(RangeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/array/select_spec.rb b/spec/ruby/core/array/select_spec.rb
new file mode 100644
index 0000000000..57ec0b2540
--- /dev/null
+++ b/spec/ruby/core/array/select_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorize'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+require_relative 'shared/keep_if'
+
+describe "Array#select" do
+ it_behaves_like :enumeratorize, :select
+
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :select
+
+ before :each do
+ @object = [1,2,3]
+ end
+ it_behaves_like :enumeratorized_with_origin_size, :select
+
+ it "returns a new array of elements for which block is true" do
+ [1, 3, 4, 5, 6, 9].select { |i| i % ((i + 1) / 2) == 0}.should == [1, 4, 6]
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].select { true }.should.instance_of?(Array)
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.select { true }.should == empty
+ empty.select { false }.should == []
+
+ array = ArraySpecs.recursive_array
+ array.select { true }.should == [1, 'two', 3.0, array, array, array, array, array]
+ array.select { false }.should == []
+ end
+end
+
+describe "Array#select!" do
+ it "returns nil if no changes were made in the array" do
+ [1, 2, 3].select! { true }.should == nil
+ end
+
+ it_behaves_like :keep_if, :select!
+end
diff --git a/spec/ruby/core/array/shared/clone.rb b/spec/ruby/core/array/shared/clone.rb
new file mode 100644
index 0000000000..1a45c2fe2c
--- /dev/null
+++ b/spec/ruby/core/array/shared/clone.rb
@@ -0,0 +1,20 @@
+describe :array_clone, shared: true do
+ it "returns an Array or a subclass instance" do
+ [].send(@method).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2].send(@method).should.instance_of?(ArraySpecs::MyArray)
+ end
+
+ it "produces a shallow copy where the references are directly copied" do
+ a = [mock('1'), mock('2')]
+ b = a.send @method
+ b.first.should.equal? a.first
+ b.last.should.equal? a.last
+ end
+
+ it "creates a new array containing all elements or the original" do
+ a = [1, 2, 3, 4]
+ b = a.send @method
+ b.should == a
+ b.__id__.should_not == a.__id__
+ end
+end
diff --git a/spec/ruby/core/array/shared/delete_if.rb b/spec/ruby/core/array/shared/delete_if.rb
new file mode 100644
index 0000000000..a3fdcf4fac
--- /dev/null
+++ b/spec/ruby/core/array/shared/delete_if.rb
@@ -0,0 +1,13 @@
+describe :delete_if, shared: true do
+ before :each do
+ @object = [1,2,3]
+ end
+
+ it "updates the receiver after all blocks" do
+ @object.send(@method) do |e|
+ @object.length.should == 3
+ true
+ end
+ @object.length.should == 0
+ end
+end
diff --git a/spec/ruby/core/array/shared/difference.rb b/spec/ruby/core/array/shared/difference.rb
new file mode 100644
index 0000000000..3fe22331bd
--- /dev/null
+++ b/spec/ruby/core/array/shared/difference.rb
@@ -0,0 +1,78 @@
+describe :array_binary_difference, shared: true do
+ it "creates an array minus any items from other array" do
+ [].send(@method, [ 1, 2, 4 ]).should == []
+ [1, 2, 4].send(@method, []).should == [1, 2, 4]
+ [ 1, 2, 3, 4, 5 ].send(@method, [ 1, 2, 4 ]).should == [3, 5]
+ end
+
+ it "removes multiple items on the lhs equal to one on the rhs" do
+ [1, 1, 2, 2, 3, 3, 4, 5].send(@method, [1, 2, 4]).should == [3, 3, 5]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.send(@method, empty).should == []
+
+ [].send(@method, ArraySpecs.recursive_array).should == []
+
+ array = ArraySpecs.recursive_array
+ array.send(@method, array).should == []
+ end
+
+ it "tries to convert the passed arguments to Arrays using #to_ary" do
+ obj = mock('[2,3,3,4]')
+ obj.should_receive(:to_ary).and_return([2, 3, 3, 4])
+ [1, 1, 2, 2, 3, 4].send(@method, obj).should == [1, 1]
+ end
+
+ it "raises a TypeError if the argument cannot be coerced to an Array by calling #to_ary" do
+ obj = mock('not an array')
+ -> { [1, 2, 3].send(@method, obj) }.should.raise(TypeError)
+ end
+
+ it "does not return subclass instance for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].send(@method, []).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[]).should.instance_of?(Array)
+ [1, 2, 3].send(@method, ArraySpecs::MyArray[]).should.instance_of?(Array)
+ end
+
+ it "does not call to_ary on array subclasses" do
+ [5, 6, 7].send(@method, ArraySpecs::ToAryArray[7]).should == [5, 6]
+ end
+
+ it "removes an item identified as equivalent via #hash and #eql?" do
+ obj1 = mock('1')
+ obj2 = mock('2')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj1.should_receive(:eql?).at_least(1).and_return(true)
+
+ [obj1].send(@method, [obj2]).should == []
+ [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == []
+ end
+
+ it "doesn't remove an item with the same hash but not #eql?" do
+ obj1 = mock('1')
+ obj2 = mock('2')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj1.should_receive(:eql?).at_least(1).and_return(false)
+
+ [obj1].send(@method, [obj2]).should == [obj1]
+ [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1, obj1]
+ end
+
+ it "removes an identical item even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.stub!(:hash).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ [x].send(@method, [x]).should == []
+ end
+
+ it "is not destructive" do
+ a = [1, 2, 3]
+ a.send(@method, [1])
+ a.should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/array/shared/enumeratorize.rb b/spec/ruby/core/array/shared/enumeratorize.rb
new file mode 100644
index 0000000000..5beab5c4c4
--- /dev/null
+++ b/spec/ruby/core/array/shared/enumeratorize.rb
@@ -0,0 +1,5 @@
+describe :enumeratorize, shared: true do
+ it "returns an Enumerator if no block given" do
+ [1,2].send(@method).should.instance_of?(Enumerator)
+ end
+end
diff --git a/spec/ruby/core/array/shared/eql.rb b/spec/ruby/core/array/shared/eql.rb
new file mode 100644
index 0000000000..5e770bf167
--- /dev/null
+++ b/spec/ruby/core/array/shared/eql.rb
@@ -0,0 +1,92 @@
+describe :array_eql, shared: true do
+ it "returns true if other is the same array" do
+ a = [1]
+ a.send(@method, a).should == true
+ end
+
+ it "returns true if corresponding elements are #eql?" do
+ [].send(@method, []).should == true
+ [1, 2, 3, 4].send(@method, [1, 2, 3, 4]).should == true
+ end
+
+ it "returns false if other is shorter than self" do
+ [1, 2, 3, 4].send(@method, [1, 2, 3]).should == false
+ end
+
+ it "returns false if other is longer than self" do
+ [1, 2, 3, 4].send(@method, [1, 2, 3, 4, 5]).should == false
+ end
+
+ it "returns false immediately when sizes of the arrays differ" do
+ obj = mock('1')
+ obj.should_not_receive(@method)
+
+ [] .send(@method, [obj] ).should == false
+ [obj] .send(@method, [] ).should == false
+ end
+
+ it "handles well recursive arrays" do
+ a = ArraySpecs.empty_recursive_array
+ a .send(@method, [a] ).should == true
+ a .send(@method, [[a]] ).should == true
+ [a] .send(@method, a ).should == true
+ [[a]] .send(@method, a ).should == true
+ # These may be surprising, but no difference can be
+ # found between these arrays, so they are ==.
+ # There is no "path" that will lead to a difference
+ # (contrary to other examples below)
+
+ a2 = ArraySpecs.empty_recursive_array
+ a .send(@method, a2 ).should == true
+ a .send(@method, [a2] ).should == true
+ a .send(@method, [[a2]] ).should == true
+ [a] .send(@method, a2 ).should == true
+ [[a]] .send(@method, a2 ).should == true
+
+ back = []
+ forth = [back]; back << forth;
+ back .send(@method, a ).should == true
+
+ x = []; x << x << x
+ x .send(@method, a ).should == false # since x.size != a.size
+ x .send(@method, [a, a] ).should == false # since x[0].size != [a, a][0].size
+ x .send(@method, [x, a] ).should == false # since x[1].size != [x, a][1].size
+ [x, a] .send(@method, [a, x] ).should == false # etc...
+ x .send(@method, [x, x] ).should == true
+ x .send(@method, [[x, x], [x, x]] ).should == true
+
+ tree = [];
+ branch = []; branch << tree << tree; tree << branch
+ tree2 = [];
+ branch2 = []; branch2 << tree2 << tree2; tree2 << branch2
+ forest = [tree, branch, :bird, a]; forest << forest
+ forest2 = [tree2, branch2, :bird, a2]; forest2 << forest2
+
+ forest .send(@method, forest2 ).should == true
+ forest .send(@method, [tree2, branch, :bird, a, forest2]).should == true
+
+ diffforest = [branch2, tree2, :bird, a2]; diffforest << forest2
+ forest .send(@method, diffforest ).should == false # since forest[0].size == 1 != 3 == diffforest[0]
+ forest .send(@method, [nil] ).should == false
+ forest .send(@method, [forest] ).should == false
+ end
+
+ it "does not call #to_ary on its argument" do
+ obj = mock('to_ary')
+ obj.should_not_receive(:to_ary)
+
+ [1, 2, 3].send(@method, obj).should == false
+ end
+
+ it "does not call #to_ary on Array subclasses" do
+ ary = ArraySpecs::ToAryArray[5, 6, 7]
+ ary.should_not_receive(:to_ary)
+ [5, 6, 7].send(@method, ary).should == true
+ end
+
+ it "ignores array class differences" do
+ ArraySpecs::MyArray[1, 2, 3].send(@method, [1, 2, 3]).should == true
+ ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should == true
+ [1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should == true
+ end
+end
diff --git a/spec/ruby/core/array/shared/intersection.rb b/spec/ruby/core/array/shared/intersection.rb
new file mode 100644
index 0000000000..dda72e8bd7
--- /dev/null
+++ b/spec/ruby/core/array/shared/intersection.rb
@@ -0,0 +1,85 @@
+describe :array_intersection, shared: true do
+ it "creates an array with elements common to both arrays (intersection)" do
+ [].send(@method, []).should == []
+ [1, 2].send(@method, []).should == []
+ [].send(@method, [1, 2]).should == []
+ [ 1, 3, 5 ].send(@method, [ 1, 2, 3 ]).should == [1, 3]
+ end
+
+ it "creates an array with no duplicates" do
+ [ 1, 1, 3, 5 ].send(@method, [ 1, 2, 3 ]).uniq!.should == nil
+ end
+
+ it "creates an array with elements in order they are first encountered" do
+ [ 1, 2, 3, 2, 5, 6, 7, 8 ].send(@method, [ 5, 2, 3, 4 ]).should == [2, 3, 5] # array > other
+ [ 5, 2, 3, 4 ].send(@method, [ 1, 2, 3, 2, 5, 6, 7, 8 ]).should == [5, 2, 3] # array < other
+ end
+
+ it "does not modify the original Array" do
+ a = [1, 1, 3, 5]
+ a.send(@method, [1, 2, 3]).should == [1, 3]
+ a.should == [1, 1, 3, 5]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.send(@method, empty).should == empty
+
+ ArraySpecs.recursive_array.send(@method, []).should == []
+ [].send(@method, ArraySpecs.recursive_array).should == []
+
+ ArraySpecs.recursive_array.send(@method, ArraySpecs.recursive_array).should == [1, 'two', 3.0, ArraySpecs.recursive_array]
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('[1,2,3]')
+ obj.should_receive(:to_ary).and_return([1, 2, 3])
+ [1, 2].send(@method, obj).should == ([1, 2])
+ end
+
+ it "determines equivalence between elements in the sense of eql?" do
+ not_supported_on :opal do
+ [5.0, 4.0].send(@method, [5, 4]).should == []
+ end
+
+ str = "x"
+ [str].send(@method, [str.dup]).should == [str]
+
+ obj1 = mock('1')
+ obj2 = mock('2')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj1.should_receive(:eql?).at_least(1).and_return(true)
+ obj2.stub!(:eql?).and_return(true)
+
+ [obj1].send(@method, [obj2]).should == [obj1]
+ [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1]
+
+ obj1 = mock('3')
+ obj2 = mock('4')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj1.should_receive(:eql?).at_least(1).and_return(false)
+
+ [obj1].send(@method, [obj2]).should == []
+ [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj2]
+ end
+
+ it "does return subclass instances for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].send(@method, []).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should.instance_of?(Array)
+ [].send(@method, ArraySpecs::MyArray[1, 2, 3]).should.instance_of?(Array)
+ end
+
+ it "does not call to_ary on array subclasses" do
+ [5, 6].send(@method, ArraySpecs::ToAryArray[1, 2, 5, 6]).should == [5, 6]
+ end
+
+ it "properly handles an identical item even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.stub!(:hash).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ [x].send(@method, [x]).should == [x]
+ end
+end
diff --git a/spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb b/spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb
new file mode 100644
index 0000000000..3e73bad44b
--- /dev/null
+++ b/spec/ruby/core/array/shared/iterable_and_tolerating_size_increasing.rb
@@ -0,0 +1,25 @@
+describe :array_iterable_and_tolerating_size_increasing, shared: true do
+ before do
+ @value_to_return ||= -> _ { nil }
+ end
+
+ it "tolerates increasing an array size during iteration" do
+ # The goal is to trigger potential reallocation of internal array storage, so we:
+ # - use elements of different types, starting with the less generic (Integer)
+ # - add reasonably big number of new elements (~ 100)
+ array = [1, 2, 3] # to test some methods we need several uniq elements
+ array_to_join = [:a, :b, :c] + (4..100).to_a
+
+ ScratchPad.record []
+ i = 0
+
+ array.send(@method) do |e|
+ ScratchPad << e
+ array << array_to_join[i] if i < array_to_join.size
+ i += 1
+ @value_to_return.call(e)
+ end
+
+ ScratchPad.recorded.should == [1, 2, 3] + array_to_join
+ end
+end
diff --git a/spec/ruby/core/array/shared/join.rb b/spec/ruby/core/array/shared/join.rb
new file mode 100644
index 0000000000..93d5329ee3
--- /dev/null
+++ b/spec/ruby/core/array/shared/join.rb
@@ -0,0 +1,15 @@
+require_relative '../fixtures/classes'
+require_relative '../fixtures/encoded_strings'
+
+describe :array_join_with_string_separator, shared: true do
+ it "returns a string formed by concatenating each element.to_str separated by separator" do
+ obj = mock('foo')
+ obj.should_receive(:to_str).and_return("foo")
+ [1, 2, 3, 4, obj].send(@method, ' | ').should == '1 | 2 | 3 | 4 | foo'
+ end
+
+ it "uses the same separator with nested arrays" do
+ [1, [2, [3, 4], 5], 6].send(@method, ":").should == "1:2:3:4:5:6"
+ [1, [2, ArraySpecs::MyArray[3, 4], 5], 6].send(@method, ":").should == "1:2:3:4:5:6"
+ end
+end
diff --git a/spec/ruby/core/array/shared/keep_if.rb b/spec/ruby/core/array/shared/keep_if.rb
new file mode 100644
index 0000000000..44625eebd1
--- /dev/null
+++ b/spec/ruby/core/array/shared/keep_if.rb
@@ -0,0 +1,95 @@
+require_relative '../../enumerable/shared/enumeratorized'
+require_relative '../shared/iterable_and_tolerating_size_increasing'
+
+describe :keep_if, shared: true do
+ it "deletes elements for which the block returns a false value" do
+ array = [1, 2, 3, 4, 5]
+ array.send(@method) {|item| item > 3 }.should.equal?(array)
+ array.should == [4, 5]
+ end
+
+ it "returns an enumerator if no block is given" do
+ [1, 2, 3].send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "updates the receiver after all blocks" do
+ a = [1, 2, 3]
+ a.send(@method) do |e|
+ a.length.should == 3
+ false
+ end
+ a.length.should == 0
+ end
+
+ before :all do
+ @object = [1,2,3]
+ end
+ it_should_behave_like :enumeratorized_with_origin_size
+
+ describe "on frozen objects" do
+ before :each do
+ @origin = [true, false]
+ @frozen = @origin.dup.freeze
+ end
+
+ it "returns an Enumerator if no block is given" do
+ @frozen.send(@method).should.instance_of?(Enumerator)
+ end
+
+ describe "with truthy block" do
+ it "keeps elements after any exception" do
+ -> { @frozen.send(@method) { true } }.should.raise(Exception)
+ @frozen.should == @origin
+ end
+
+ it "raises a FrozenError" do
+ -> { @frozen.send(@method) { true } }.should.raise(FrozenError)
+ end
+ end
+
+ describe "with falsy block" do
+ it "keeps elements after any exception" do
+ -> { @frozen.send(@method) { false } }.should.raise(Exception)
+ @frozen.should == @origin
+ end
+
+ it "raises a FrozenError" do
+ -> { @frozen.send(@method) { false } }.should.raise(FrozenError)
+ end
+ end
+
+ it "raises a FrozenError on a frozen array only during iteration if called without a block" do
+ enum = @frozen.send(@method)
+ -> { enum.each {} }.should.raise(FrozenError)
+ end
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.send(@method) { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "only changes elements before error is raised, keeping the element which raised an error." do
+ a = [1, 2, 3, 4]
+ begin
+ a.send(@method) do |e|
+ case e
+ when 2 then false
+ when 3 then raise StandardError, 'Oops'
+ else true
+ end
+ end
+ rescue StandardError
+ end
+
+ a.should == [1, 3, 4]
+ end
+
+ @value_to_return = -> _ { true }
+ it_should_behave_like :array_iterable_and_tolerating_size_increasing
+end
diff --git a/spec/ruby/core/array/shared/union.rb b/spec/ruby/core/array/shared/union.rb
new file mode 100644
index 0000000000..0b225b9a31
--- /dev/null
+++ b/spec/ruby/core/array/shared/union.rb
@@ -0,0 +1,79 @@
+describe :array_binary_union, shared: true do
+ it "returns an array of elements that appear in either array (union)" do
+ [].send(@method, []).should == []
+ [1, 2].send(@method, []).should == [1, 2]
+ [].send(@method, [1, 2]).should == [1, 2]
+ [ 1, 2, 3, 4 ].send(@method, [ 3, 4, 5 ]).should == [1, 2, 3, 4, 5]
+ end
+
+ it "creates an array with no duplicates" do
+ [ 1, 2, 3, 1, 4, 5 ].send(@method, [ 1, 3, 4, 5, 3, 6 ]).should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "creates an array with elements in order they are first encountered" do
+ [ 1, 2, 3, 1 ].send(@method, [ 1, 3, 4, 5 ]).should == [1, 2, 3, 4, 5]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.send(@method, empty).should == empty
+
+ array = ArraySpecs.recursive_array
+ array.send(@method, []).should == [1, 'two', 3.0, array]
+ [].send(@method, array).should == [1, 'two', 3.0, array]
+ array.send(@method, array).should == [1, 'two', 3.0, array]
+ array.send(@method, empty).should == [1, 'two', 3.0, array, empty]
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('[1,2,3]')
+ obj.should_receive(:to_ary).and_return([1, 2, 3])
+ [0].send(@method, obj).should == ([0] | [1, 2, 3])
+ end
+
+ # MRI follows hashing semantics here, so doesn't actually call eql?/hash for Integer/Symbol
+ it "acts as if using an intermediate hash to collect values" do
+ not_supported_on :opal do
+ [5.0, 4.0].send(@method, [5, 4]).should == [5.0, 4.0, 5, 4]
+ end
+
+ str = "x"
+ [str].send(@method, [str.dup]).should == [str]
+
+ obj1 = mock('1')
+ obj2 = mock('2')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj2.should_receive(:eql?).at_least(1).and_return(true)
+
+ [obj1].send(@method, [obj2]).should == [obj1]
+ [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1]
+
+ obj1 = mock('3')
+ obj2 = mock('4')
+ obj1.stub!(:hash).and_return(0)
+ obj2.stub!(:hash).and_return(0)
+ obj2.should_receive(:eql?).at_least(1).and_return(false)
+
+ [obj1].send(@method, [obj2]).should == [obj1, obj2]
+ [obj1, obj1, obj2, obj2].send(@method, [obj2]).should == [obj1, obj2]
+ end
+
+ it "does not return subclass instances for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].send(@method, []).should.instance_of?(Array)
+ ArraySpecs::MyArray[1, 2, 3].send(@method, ArraySpecs::MyArray[1, 2, 3]).should.instance_of?(Array)
+ [].send(@method, ArraySpecs::MyArray[1, 2, 3]).should.instance_of?(Array)
+ end
+
+ it "does not call to_ary on array subclasses" do
+ [1, 2].send(@method, ArraySpecs::ToAryArray[5, 6]).should == [1, 2, 5, 6]
+ end
+
+ it "properly handles an identical item even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.stub!(:hash).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ [x].send(@method, [x]).should == [x]
+ end
+end
diff --git a/spec/ruby/core/array/shift_spec.rb b/spec/ruby/core/array/shift_spec.rb
new file mode 100644
index 0000000000..09dfa79c45
--- /dev/null
+++ b/spec/ruby/core/array/shift_spec.rb
@@ -0,0 +1,120 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#shift" do
+ it "removes and returns the first element" do
+ a = [5, 1, 1, 5, 4]
+ a.shift.should == 5
+ a.should == [1, 1, 5, 4]
+ a.shift.should == 1
+ a.should == [1, 5, 4]
+ a.shift.should == 1
+ a.should == [5, 4]
+ a.shift.should == 5
+ a.should == [4]
+ a.shift.should == 4
+ a.should == []
+ end
+
+ it "returns nil when the array is empty" do
+ [].shift.should == nil
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.shift.should == []
+ empty.should == []
+
+ array = ArraySpecs.recursive_array
+ array.shift.should == 1
+ array[0..2].should == ['two', 3.0, array]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.shift }.should.raise(FrozenError)
+ end
+ it "raises a FrozenError on an empty frozen array" do
+ -> { ArraySpecs.empty_frozen_array.shift }.should.raise(FrozenError)
+ end
+
+ describe "passed a number n as an argument" do
+ it "removes and returns an array with the first n element of the array" do
+ a = [1, 2, 3, 4, 5, 6]
+
+ a.shift(0).should == []
+ a.should == [1, 2, 3, 4, 5, 6]
+
+ a.shift(1).should == [1]
+ a.should == [2, 3, 4, 5, 6]
+
+ a.shift(2).should == [2, 3]
+ a.should == [4, 5, 6]
+
+ a.shift(3).should == [4, 5, 6]
+ a.should == []
+ end
+
+ it "does not corrupt the array when shift without arguments is followed by shift with an argument" do
+ a = [1, 2, 3, 4, 5]
+
+ a.shift.should == 1
+ a.shift(3).should == [2, 3, 4]
+ a.should == [5]
+ end
+
+ it "returns a new empty array if there are no more elements" do
+ a = []
+ popped1 = a.shift(1)
+ popped1.should == []
+ a.should == []
+
+ popped2 = a.shift(2)
+ popped2.should == []
+ a.should == []
+
+ popped1.should_not.equal?(popped2)
+ end
+
+ it "returns whole elements if n exceeds size of the array" do
+ a = [1, 2, 3, 4, 5]
+ a.shift(6).should == [1, 2, 3, 4, 5]
+ a.should == []
+ end
+
+ it "does not return self even when it returns whole elements" do
+ a = [1, 2, 3, 4, 5]
+ a.shift(5).should_not.equal?(a)
+
+ a = [1, 2, 3, 4, 5]
+ a.shift(6).should_not.equal?(a)
+ end
+
+ it "raises an ArgumentError if n is negative" do
+ ->{ [1, 2, 3].shift(-1) }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert n to an Integer using #to_int" do
+ a = [1, 2, 3, 4]
+ a.shift(2.3).should == [1, 2]
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ a.should == [3, 4]
+ a.shift(obj).should == [3, 4]
+ a.should == []
+ end
+
+ it "raises a TypeError when the passed n cannot be coerced to Integer" do
+ ->{ [1, 2].shift("cat") }.should.raise(TypeError)
+ ->{ [1, 2].shift(nil) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if more arguments are passed" do
+ ->{ [1, 2].shift(1, 2) }.should.raise(ArgumentError)
+ end
+
+ it "does not return subclass instances with Array subclass" do
+ ArraySpecs::MyArray[1, 2, 3].shift(2).should.instance_of?(Array)
+ end
+ end
+end
diff --git a/spec/ruby/core/array/shuffle_spec.rb b/spec/ruby/core/array/shuffle_spec.rb
new file mode 100644
index 0000000000..9bc9df73ac
--- /dev/null
+++ b/spec/ruby/core/array/shuffle_spec.rb
@@ -0,0 +1,119 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#shuffle" do
+ it "returns the same values, in a usually different order" do
+ a = [1, 2, 3, 4]
+ different = false
+ 10.times do
+ s = a.shuffle
+ s.sort.should == a
+ different ||= (a != s)
+ end
+ different.should == true # Will fail once in a blue moon (4!^10)
+ end
+
+ it "is not destructive" do
+ a = [1, 2, 3]
+ 10.times do
+ a.shuffle
+ a.should == [1, 2, 3]
+ end
+ end
+
+ it "does not return subclass instances with Array subclass" do
+ ArraySpecs::MyArray[1, 2, 3].shuffle.should.instance_of?(Array)
+ end
+
+ it "calls #rand on the Object passed by the :random key in the arguments Hash" do
+ obj = mock("array_shuffle_random")
+ obj.should_receive(:rand).at_least(1).times.and_return(0.5)
+
+ result = [1, 2].shuffle(random: obj)
+ result.size.should == 2
+ result.sort.should == [1, 2]
+ end
+
+ it "raises a NoMethodError if an object passed for the RNG does not define #rand" do
+ obj = BasicObject.new
+
+ -> { [1, 2].shuffle(random: obj) }.should.raise(NoMethodError)
+ end
+
+ it "accepts a Float for the value returned by #rand" do
+ random = mock("array_shuffle_random")
+ random.should_receive(:rand).at_least(1).times.and_return(0.3)
+
+ [1, 2].shuffle(random: random).should.instance_of?(Array)
+ end
+
+ it "accepts a Random class for the value for random: argument" do
+ [1, 2].shuffle(random: Random).should.instance_of?(Array)
+ end
+
+ it "calls #to_int on the Object returned by #rand" do
+ value = mock("array_shuffle_random_value")
+ value.should_receive(:to_int).at_least(1).times.and_return(0)
+ random = mock("array_shuffle_random")
+ random.should_receive(:rand).at_least(1).times.and_return(value)
+
+ [1, 2].shuffle(random: random).should.instance_of?(Array)
+ end
+
+ it "raises a RangeError if the value is less than zero" do
+ value = mock("array_shuffle_random_value")
+ value.should_receive(:to_int).and_return(-1)
+ random = mock("array_shuffle_random")
+ random.should_receive(:rand).and_return(value)
+
+ -> { [1, 2].shuffle(random: random) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if the value is equal to the Array size" do
+ value = mock("array_shuffle_random_value")
+ value.should_receive(:to_int).at_least(1).times.and_return(2)
+ random = mock("array_shuffle_random")
+ random.should_receive(:rand).at_least(1).times.and_return(value)
+
+ -> { [1, 2].shuffle(random: random) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if the value is greater than the Array size" do
+ value = mock("array_shuffle_random_value")
+ value.should_receive(:to_int).at_least(1).times.and_return(3)
+ random = mock("array_shuffle_random")
+ random.should_receive(:rand).at_least(1).times.and_return(value)
+
+ -> { [1, 2].shuffle(random: random) }.should.raise(RangeError)
+ end
+end
+
+describe "Array#shuffle!" do
+ it "returns the same values, in a usually different order" do
+ a = [1, 2, 3, 4]
+ original = a
+ different = false
+ 10.times do
+ a = a.shuffle!
+ a.sort.should == [1, 2, 3, 4]
+ different ||= (a != [1, 2, 3, 4])
+ end
+ different.should == true # Will fail once in a blue moon (4!^10)
+ a.should.equal?(original)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.shuffle! }.should.raise(FrozenError)
+ -> { ArraySpecs.empty_frozen_array.shuffle! }.should.raise(FrozenError)
+ end
+
+ it "matches CRuby with random:" do
+ %w[a b c].shuffle(random: Random.new(1)).should == %w[a c b]
+ (0..10).to_a.shuffle(random: Random.new(10)).should == [2, 6, 8, 5, 7, 10, 3, 1, 0, 4, 9]
+ end
+
+ it "matches CRuby with srand" do
+ srand(123)
+ %w[a b c d e f g h i j k].shuffle.should == %w[a e f h i j d b g k c]
+ end
+end
diff --git a/spec/ruby/core/array/size_spec.rb b/spec/ruby/core/array/size_spec.rb
new file mode 100644
index 0000000000..83e8969012
--- /dev/null
+++ b/spec/ruby/core/array/size_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Array#size" do
+ it "is an alias of Array#length" do
+ Array.instance_method(:size).should == Array.instance_method(:length)
+ end
+end
diff --git a/spec/ruby/core/array/slice_spec.rb b/spec/ruby/core/array/slice_spec.rb
new file mode 100644
index 0000000000..230d1dc5d1
--- /dev/null
+++ b/spec/ruby/core/array/slice_spec.rb
@@ -0,0 +1,219 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#slice!" do
+ it "removes and return the element at index" do
+ a = [1, 2, 3, 4]
+ a.slice!(10).should == nil
+ a.should == [1, 2, 3, 4]
+ a.slice!(-10).should == nil
+ a.should == [1, 2, 3, 4]
+ a.slice!(2).should == 3
+ a.should == [1, 2, 4]
+ a.slice!(-1).should == 4
+ a.should == [1, 2]
+ a.slice!(1).should == 2
+ a.should == [1]
+ a.slice!(-1).should == 1
+ a.should == []
+ a.slice!(-1).should == nil
+ a.should == []
+ a.slice!(0).should == nil
+ a.should == []
+ end
+
+ it "removes and returns length elements beginning at start" do
+ a = [1, 2, 3, 4, 5, 6]
+ a.slice!(2, 3).should == [3, 4, 5]
+ a.should == [1, 2, 6]
+ a.slice!(1, 1).should == [2]
+ a.should == [1, 6]
+ a.slice!(1, 0).should == []
+ a.should == [1, 6]
+ a.slice!(2, 0).should == []
+ a.should == [1, 6]
+ a.slice!(0, 4).should == [1, 6]
+ a.should == []
+ a.slice!(0, 4).should == []
+ a.should == []
+
+ a = [1]
+ a.slice!(0, 1).should == [1]
+ a.should == []
+ a[-1].should == nil
+
+ a = [1, 2, 3]
+ a.slice!(0,1).should == [1]
+ a.should == [2, 3]
+ end
+
+ it "returns nil if length is negative" do
+ a = [1, 2, 3]
+ a.slice!(2, -1).should == nil
+ a.should == [1, 2, 3]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.slice(0).should == empty
+
+ array = ArraySpecs.recursive_array
+ array.slice(4).should == array
+ array.slice(0..3).should == [1, 'two', 3.0, array]
+ end
+
+ it "calls to_int on start and length arguments" do
+ obj = mock('2')
+ def obj.to_int() 2 end
+
+ a = [1, 2, 3, 4, 5]
+ a.slice!(obj).should == 3
+ a.should == [1, 2, 4, 5]
+ a.slice!(obj, obj).should == [4, 5]
+ a.should == [1, 2]
+ a.slice!(0, obj).should == [1, 2]
+ a.should == []
+ end
+
+ it "removes and return elements in range" do
+ a = [1, 2, 3, 4, 5, 6, 7, 8]
+ a.slice!(1..4).should == [2, 3, 4, 5]
+ a.should == [1, 6, 7, 8]
+ a.slice!(1...3).should == [6, 7]
+ a.should == [1, 8]
+ a.slice!(-1..-1).should == [8]
+ a.should == [1]
+ a.slice!(0...0).should == []
+ a.should == [1]
+ a.slice!(0..0).should == [1]
+ a.should == []
+
+ a = [1,2,3]
+ a.slice!(0..3).should == [1,2,3]
+ a.should == []
+ end
+
+ it "removes and returns elements in end-exclusive ranges" do
+ a = [1, 2, 3, 4, 5, 6, 7, 8]
+ a.slice!(4...a.length).should == [5, 6, 7, 8]
+ a.should == [1, 2, 3, 4]
+ end
+
+ it "calls to_int on range arguments" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ def from.to_int() 1 end
+ def to.to_int() -2 end
+
+ a = [1, 2, 3, 4, 5]
+
+ a.slice!(from .. to).should == [2, 3, 4]
+ a.should == [1, 5]
+
+ -> { a.slice!("a" .. "b") }.should.raise(TypeError)
+ -> { a.slice!(from .. "b") }.should.raise(TypeError)
+ end
+
+ it "returns last element for consecutive calls at zero index" do
+ a = [ 1, 2, 3 ]
+ a.slice!(0).should == 1
+ a.slice!(0).should == 2
+ a.slice!(0).should == 3
+ a.should == []
+ end
+
+ it "does not expand array with indices out of bounds" do
+ a = [1, 2]
+ a.slice!(4).should == nil
+ a.should == [1, 2]
+ a.slice!(4, 0).should == nil
+ a.should == [1, 2]
+ a.slice!(6, 1).should == nil
+ a.should == [1, 2]
+ a.slice!(8...8).should == nil
+ a.should == [1, 2]
+ a.slice!(10..10).should == nil
+ a.should == [1, 2]
+ end
+
+ it "does not expand array with negative indices out of bounds" do
+ a = [1, 2]
+ a.slice!(-3, 1).should == nil
+ a.should == [1, 2]
+ a.slice!(-3..2).should == nil
+ a.should == [1, 2]
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.slice!(0, 0) }.should.raise(FrozenError)
+ end
+
+ it "works with endless ranges" do
+ a = [1, 2, 3]
+ a.slice!(eval("(1..)")).should == [2, 3]
+ a.should == [1]
+
+ a = [1, 2, 3]
+ a.slice!(eval("(2...)")).should == [3]
+ a.should == [1, 2]
+
+ a = [1, 2, 3]
+ a.slice!(eval("(-2..)")).should == [2, 3]
+ a.should == [1]
+
+ a = [1, 2, 3]
+ a.slice!(eval("(-1...)")).should == [3]
+ a.should == [1, 2]
+ end
+
+ it "works with beginless ranges" do
+ a = [0,1,2,3,4]
+ a.slice!((..3)).should == [0, 1, 2, 3]
+ a.should == [4]
+
+ a = [0,1,2,3,4]
+ a.slice!((...-2)).should == [0, 1, 2]
+ a.should == [3, 4]
+ end
+
+ describe "with a subclass of Array" do
+ before :each do
+ @array = ArraySpecs::MyArray[1, 2, 3, 4, 5]
+ end
+
+ it "returns a Array instance with [n, m]" do
+ @array.slice!(0, 2).should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [-n, m]" do
+ @array.slice!(-3, 2).should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [n..m]" do
+ @array.slice!(1..3).should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [n...m]" do
+ @array.slice!(1...3).should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [-n..-m]" do
+ @array.slice!(-3..-1).should.instance_of?(Array)
+ end
+
+ it "returns a Array instance with [-n...-m]" do
+ @array.slice!(-3...-1).should.instance_of?(Array)
+ end
+ end
+end
+
+describe "Array#slice" do
+ it "is an alias of Array#[]" do
+ Array.instance_method(:slice).should == Array.instance_method(:[])
+ end
+end
diff --git a/spec/ruby/core/array/sort_by_spec.rb b/spec/ruby/core/array/sort_by_spec.rb
new file mode 100644
index 0000000000..132abb028a
--- /dev/null
+++ b/spec/ruby/core/array/sort_by_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Array#sort_by!" do
+ it "sorts array in place by passing each element to the given block" do
+ a = [-100, -2, 1, 200, 30000]
+ a.sort_by!{ |e| e.to_s.size }
+ a.should == [1, -2, 200, -100, 30000]
+ end
+
+ it "returns an Enumerator if not given a block" do
+ (1..10).to_a.sort_by!.should.instance_of?(Enumerator)
+ end
+
+ it "completes when supplied a block that always returns the same result" do
+ a = [2, 3, 5, 1, 4]
+ a.sort_by!{ 1 }
+ a.should.instance_of?(Array)
+ a.sort_by!{ 0 }
+ a.should.instance_of?(Array)
+ a.sort_by!{ -1 }
+ a.should.instance_of?(Array)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.sort_by! {}}.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on an empty frozen array" do
+ -> { ArraySpecs.empty_frozen_array.sort_by! {}}.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on a frozen array only during iteration if called without a block" do
+ enum = ArraySpecs.frozen_array.sort_by!
+ -> { enum.each {} }.should.raise(FrozenError)
+ end
+
+ it "returns the specified value when it would break in the given block" do
+ [1, 2, 3].sort_by!{ break :a }.should == :a
+ end
+
+ it "makes some modification even if finished sorting when it would break in the given block" do
+ partially_sorted = (1..5).map{|i|
+ ary = [5, 4, 3, 2, 1]
+ ary.sort_by!{|x,y| break if x==i; x<=>y}
+ ary
+ }
+ partially_sorted.any?{|ary| ary != [1, 2, 3, 4, 5]}.should == true
+ end
+
+ it "changes nothing when called on a single element array" do
+ [1].sort_by!(&:to_s).should == [1]
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.sort_by! { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "doesn't change array if error is raised" do
+ a = [4, 3, 2, 1]
+ begin
+ a.sort_by! do |e|
+ raise StandardError, 'Oops' if e == 1
+ e
+ end
+ rescue StandardError
+ end
+
+ a.should == [4, 3, 2, 1]
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :sort_by!, [1,2,3]
+end
+
+describe "Array#sort_by!" do
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :sort_by!
+end
diff --git a/spec/ruby/core/array/sort_spec.rb b/spec/ruby/core/array/sort_spec.rb
new file mode 100644
index 0000000000..27300c3385
--- /dev/null
+++ b/spec/ruby/core/array/sort_spec.rb
@@ -0,0 +1,252 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#sort" do
+ it "returns a new array sorted based on comparing elements with <=>" do
+ a = [1, -2, 3, 9, 1, 5, -5, 1000, -5, 2, -10, 14, 6, 23, 0]
+ a.sort.should == [-10, -5, -5, -2, 0, 1, 1, 2, 3, 5, 6, 9, 14, 23, 1000]
+ end
+
+ it "does not affect the original Array" do
+ a = [3, 1, 2]
+ a.sort.should == [1, 2, 3]
+ a.should == [3, 1, 2]
+
+ a = [0, 15, 2, 3, 4, 6, 14, 5, 7, 12, 8, 9, 1, 10, 11, 13]
+ b = a.sort
+ a.should == [0, 15, 2, 3, 4, 6, 14, 5, 7, 12, 8, 9, 1, 10, 11, 13]
+ b.should == (0..15).to_a
+ end
+
+ it "sorts already-sorted Arrays" do
+ (0..15).to_a.sort.should == (0..15).to_a
+ end
+
+ it "sorts reverse-sorted Arrays" do
+ (0..15).to_a.reverse.sort.should == (0..15).to_a
+ end
+
+ it "sorts Arrays that consist entirely of equal elements" do
+ a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ a.sort.should == a
+ b = Array.new(15).map { ArraySpecs::SortSame.new }
+ b.sort.should == b
+ end
+
+ it "sorts Arrays that consist mostly of equal elements" do
+ a = [1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ a.sort.should == [0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
+ end
+
+ it "does not return self even if the array would be already sorted" do
+ a = [1, 2, 3]
+ sorted = a.sort
+ sorted.should == a
+ sorted.should_not.equal?(a)
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.sort.should == empty
+
+ array = [[]]; array << array
+ array.sort.should == [[], array]
+ end
+
+ it "uses #<=> of elements in order to sort" do
+ a = ArraySpecs::MockForCompared.new
+ b = ArraySpecs::MockForCompared.new
+ c = ArraySpecs::MockForCompared.new
+
+ ArraySpecs::MockForCompared.should_not.compared?
+ [a, b, c].sort.should == [c, b, a]
+ ArraySpecs::MockForCompared.should.compared?
+ end
+
+ it "does not deal with exceptions raised by unimplemented or incorrect #<=>" do
+ o = Object.new
+
+ -> {
+ [o, 1].sort
+ }.should.raise(ArgumentError)
+ end
+
+ it "may take a block which is used to determine the order of objects a and b described as -1, 0 or +1" do
+ a = [5, 1, 4, 3, 2]
+ a.sort.should == [1, 2, 3, 4, 5]
+ a.sort {|x, y| y <=> x}.should == [5, 4, 3, 2, 1]
+ end
+
+ it "raises an error when a given block returns nil" do
+ -> { [1, 2].sort {} }.should.raise(ArgumentError)
+ end
+
+ it "does not call #<=> on contained objects when invoked with a block" do
+ a = Array.new(25)
+ (0...25).each {|i| a[i] = ArraySpecs::UFOSceptic.new }
+
+ a.sort { -1 }.should.instance_of?(Array)
+ end
+
+ it "does not call #<=> on elements when invoked with a block even if Array is large (Rubinius #412)" do
+ a = Array.new(1500)
+ (0...1500).each {|i| a[i] = ArraySpecs::UFOSceptic.new }
+
+ a.sort { -1 }.should.instance_of?(Array)
+ end
+
+ it "completes when supplied a block that always returns the same result" do
+ a = [2, 3, 5, 1, 4]
+ a.sort { 1 }.should.instance_of?(Array)
+ a.sort { 0 }.should.instance_of?(Array)
+ a.sort { -1 }.should.instance_of?(Array)
+ end
+
+ it "does not freezes self during being sorted" do
+ a = [1, 2, 3]
+ a.sort { |x,y| a.should_not.frozen?; x <=> y }
+ end
+
+ it "returns the specified value when it would break in the given block" do
+ [1, 2, 3].sort{ break :a }.should == :a
+ end
+
+ it "uses the sign of Integer block results as the sort result" do
+ a = [1, 2, 5, 10, 7, -4, 12]
+ begin
+ class Integer
+ alias old_spaceship <=>
+ def <=>(other)
+ raise
+ end
+ end
+ a.sort {|n, m| (n - m) * (2 ** 200)}.should == [-4, 1, 2, 5, 7, 10, 12]
+ ensure
+ class Integer
+ alias <=> old_spaceship
+ end
+ end
+ end
+
+ it "compares values returned by block with 0" do
+ a = [1, 2, 5, 10, 7, -4, 12]
+ a.sort { |n, m| n - m }.should == [-4, 1, 2, 5, 7, 10, 12]
+ a.sort { |n, m|
+ ArraySpecs::ComparableWithInteger.new(n-m)
+ }.should == [-4, 1, 2, 5, 7, 10, 12]
+ -> {
+ a.sort { |n, m| (n - m).to_s }
+ }.should.raise(ArgumentError)
+ end
+
+ it "sorts an array that has a value shifted off without a block" do
+ a = Array.new(20, 1)
+ a.shift
+ a[0] = 2
+ a.sort.last.should == 2
+ end
+
+ it "sorts an array that has a value shifted off with a block" do
+ a = Array.new(20, 1)
+ a.shift
+ a[0] = 2
+ a.sort {|x, y| x <=> y }.last.should == 2
+ end
+
+ it "raises an error if objects can't be compared" do
+ a=[ArraySpecs::Uncomparable.new, ArraySpecs::Uncomparable.new]
+ -> {a.sort}.should.raise(ArgumentError)
+ end
+
+ # From a strange Rubinius bug
+ it "handles a large array that has been pruned" do
+ pruned = ArraySpecs::LargeArray.dup.delete_if { |n| n !~ /^test./ }
+ pruned.sort.should == ArraySpecs::LargeTestArraySorted
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ary = ArraySpecs::MyArray[1, 2, 3]
+ ary.sort.should.instance_of?(Array)
+ end
+end
+
+describe "Array#sort!" do
+ it "sorts array in place using <=>" do
+ a = [1, -2, 3, 9, 1, 5, -5, 1000, -5, 2, -10, 14, 6, 23, 0]
+ a.sort!
+ a.should == [-10, -5, -5, -2, 0, 1, 1, 2, 3, 5, 6, 9, 14, 23, 1000]
+ end
+
+ it "sorts array in place using block value if a block given" do
+ a = [0, 15, 2, 3, 4, 6, 14, 5, 7, 12, 8, 9, 1, 10, 11, 13]
+ a.sort! { |x, y| y <=> x }.should == (0..15).to_a.reverse
+ end
+
+ it "returns self if the order of elements changed" do
+ a = [6, 7, 2, 3, 7]
+ a.sort!.should.equal?(a)
+ a.should == [2, 3, 6, 7, 7]
+ end
+
+ it "returns self even if makes no modification" do
+ a = [1, 2, 3, 4, 5]
+ a.sort!.should.equal?(a)
+ a.should == [1, 2, 3, 4, 5]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.sort!.should == empty
+
+ array = [[]]; array << array
+ array.sort!.should == array
+ end
+
+ it "uses #<=> of elements in order to sort" do
+ a = ArraySpecs::MockForCompared.new
+ b = ArraySpecs::MockForCompared.new
+ c = ArraySpecs::MockForCompared.new
+
+ ArraySpecs::MockForCompared.should_not.compared?
+ [a, b, c].sort!.should == [c, b, a]
+ ArraySpecs::MockForCompared.should.compared?
+ end
+
+ it "does not call #<=> on contained objects when invoked with a block" do
+ a = Array.new(25)
+ (0...25).each {|i| a[i] = ArraySpecs::UFOSceptic.new }
+
+ a.sort! { -1 }.should.instance_of?(Array)
+ end
+
+ it "does not call #<=> on elements when invoked with a block even if Array is large (Rubinius #412)" do
+ a = Array.new(1500)
+ (0...1500).each {|i| a[i] = ArraySpecs::UFOSceptic.new }
+
+ a.sort! { -1 }.should.instance_of?(Array)
+ end
+
+ it "completes when supplied a block that always returns the same result" do
+ a = [2, 3, 5, 1, 4]
+ a.sort!{ 1 }.should.instance_of?(Array)
+ a.sort!{ 0 }.should.instance_of?(Array)
+ a.sort!{ -1 }.should.instance_of?(Array)
+ end
+
+ it "raises a FrozenError on a frozen array" do
+ -> { ArraySpecs.frozen_array.sort! }.should.raise(FrozenError)
+ end
+
+ it "returns the specified value when it would break in the given block" do
+ [1, 2, 3].sort{ break :a }.should == :a
+ end
+
+ it "makes some modification even if finished sorting when it would break in the given block" do
+ partially_sorted = (1..5).map{|i|
+ ary = [5, 4, 3, 2, 1]
+ ary.sort!{|x,y| break if x==i; x<=>y}
+ ary
+ }
+ partially_sorted.any?{|ary| ary != [1, 2, 3, 4, 5]}.should == true
+ end
+end
diff --git a/spec/ruby/core/array/sum_spec.rb b/spec/ruby/core/array/sum_spec.rb
new file mode 100644
index 0000000000..cd4ba4c2d8
--- /dev/null
+++ b/spec/ruby/core/array/sum_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#sum" do
+ it "returns the sum of elements" do
+ [1, 2, 3].sum.should == 6
+ end
+
+ it "applies a block to each element before adding if it's given" do
+ [1, 2, 3].sum { |i| i * 10 }.should == 60
+ end
+
+ it "doesn't apply the block init" do
+ [1, 2, 3].sum(1) { |i| i * 10 }.should == 61
+ end
+
+ # https://bugs.ruby-lang.org/issues/12217
+ # https://github.com/ruby/ruby/blob/master/doc/ChangeLog/ChangeLog-2.4.0#L6208-L6214
+ it "uses Kahan's compensated summation algorithm for precise sum of float numbers" do
+ floats = [2.7800000000000002, 5.0, 2.5, 4.44, 3.89, 3.89, 4.44, 7.78, 5.0, 2.7800000000000002, 5.0, 2.5]
+ naive_sum = floats.reduce { |sum, e| sum + e }
+ naive_sum.should == 50.00000000000001
+ floats.sum.should == 50.0
+ end
+
+ it "handles infinite values and NaN" do
+ [1.0, Float::INFINITY].sum.should == Float::INFINITY
+ [1.0, -Float::INFINITY].sum.should == -Float::INFINITY
+ [1.0, Float::NAN].sum.should.nan?
+
+ [Float::INFINITY, 1.0].sum.should == Float::INFINITY
+ [-Float::INFINITY, 1.0].sum.should == -Float::INFINITY
+ [Float::NAN, 1.0].sum.should.nan?
+
+ [Float::NAN, Float::INFINITY].sum.should.nan?
+ [Float::INFINITY, Float::NAN].sum.should.nan?
+
+ [Float::INFINITY, -Float::INFINITY].sum.should.nan?
+ [-Float::INFINITY, Float::INFINITY].sum.should.nan?
+
+ [Float::INFINITY, Float::INFINITY].sum.should == Float::INFINITY
+ [-Float::INFINITY, -Float::INFINITY].sum.should == -Float::INFINITY
+ [Float::NAN, Float::NAN].sum.should.nan?
+ end
+
+ it "returns init value if array is empty" do
+ [].sum(-1).should == -1
+ end
+
+ it "returns 0 if array is empty and init is omitted" do
+ [].sum.should == 0
+ end
+
+ it "adds init value to the sum of elements" do
+ [1, 2, 3].sum(10).should == 16
+ end
+
+ it "can be used for non-numeric objects by providing init value" do
+ ["a", "b", "c"].sum("").should == "abc"
+ end
+
+ it 'raises TypeError if any element are not numeric' do
+ -> { ["a"].sum }.should.raise(TypeError)
+ end
+
+ it 'raises TypeError if any element cannot be added to init value' do
+ -> { [1].sum([]) }.should.raise(TypeError)
+ end
+
+ it "calls + to sum the elements" do
+ a = mock("a")
+ b = mock("b")
+ a.should_receive(:+).with(b).and_return(42)
+ [b].sum(a).should == 42
+ end
+
+ it "calls + on the init value" do
+ a = mock("a")
+ b = mock("b")
+ a.should_receive(:+).with(42).and_return(b)
+ [42].sum(a).should == b
+ end
+end
+
+describe "Array#sum" do
+ @value_to_return = -> _ { 1 }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :sum
+end
diff --git a/spec/ruby/core/array/take_spec.rb b/spec/ruby/core/array/take_spec.rb
new file mode 100644
index 0000000000..837c734b77
--- /dev/null
+++ b/spec/ruby/core/array/take_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#take" do
+ it "returns the first specified number of elements" do
+ [1, 2, 3].take(2).should == [1, 2]
+ end
+
+ it "returns all elements when the argument is greater than the Array size" do
+ [1, 2].take(99).should == [1, 2]
+ end
+
+ it "returns all elements when the argument is less than the Array size" do
+ [1, 2].take(4).should == [1, 2]
+ end
+
+ it "returns an empty Array when passed zero" do
+ [1].take(0).should == []
+ end
+
+ it "returns an empty Array when called on an empty Array" do
+ [].take(3).should == []
+ end
+
+ it "raises an ArgumentError when the argument is negative" do
+ ->{ [1].take(-3) }.should.raise(ArgumentError)
+ end
+
+ it 'returns a Array instance for Array subclasses' do
+ ArraySpecs::MyArray[1, 2, 3, 4, 5].take(1).should.instance_of?(Array)
+ end
+end
diff --git a/spec/ruby/core/array/take_while_spec.rb b/spec/ruby/core/array/take_while_spec.rb
new file mode 100644
index 0000000000..7811edab9e
--- /dev/null
+++ b/spec/ruby/core/array/take_while_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#take_while" do
+ it "returns all elements until the block returns false" do
+ [1, 2, 3].take_while{ |element| element < 3 }.should == [1, 2]
+ end
+
+ it "returns all elements until the block returns nil" do
+ [1, 2, nil, 4].take_while{ |element| element }.should == [1, 2]
+ end
+
+ it "returns all elements until the block returns false" do
+ [1, 2, false, 4].take_while{ |element| element }.should == [1, 2]
+ end
+
+ it 'returns a Array instance for Array subclasses' do
+ ArraySpecs::MyArray[1, 2, 3, 4, 5].take_while { |n| n < 4 }.should.instance_of?(Array)
+ end
+end
+
+describe "Array#take_while" do
+ @value_to_return = -> _ { true }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :take_while
+end
diff --git a/spec/ruby/core/array/to_a_spec.rb b/spec/ruby/core/array/to_a_spec.rb
new file mode 100644
index 0000000000..078de1638a
--- /dev/null
+++ b/spec/ruby/core/array/to_a_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#to_a" do
+ it "returns self" do
+ a = [1, 2, 3]
+ a.to_a.should == [1, 2, 3]
+ a.should.equal?(a.to_a)
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ e = ArraySpecs::MyArray.new(1, 2)
+ e.to_a.should.instance_of?(Array)
+ e.to_a.should == [1, 2]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.to_a.should == empty
+
+ array = ArraySpecs.recursive_array
+ array.to_a.should == array
+ end
+end
diff --git a/spec/ruby/core/array/to_ary_spec.rb b/spec/ruby/core/array/to_ary_spec.rb
new file mode 100644
index 0000000000..dc5193158d
--- /dev/null
+++ b/spec/ruby/core/array/to_ary_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#to_ary" do
+ it "returns self" do
+ a = [1, 2, 3]
+ a.should.equal?(a.to_ary)
+ a = ArraySpecs::MyArray[1, 2, 3]
+ a.should.equal?(a.to_ary)
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.to_ary.should == empty
+
+ array = ArraySpecs.recursive_array
+ array.to_ary.should == array
+ end
+
+end
diff --git a/spec/ruby/core/array/to_h_spec.rb b/spec/ruby/core/array/to_h_spec.rb
new file mode 100644
index 0000000000..1d626763c2
--- /dev/null
+++ b/spec/ruby/core/array/to_h_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#to_h" do
+ it "converts empty array to empty hash" do
+ [].to_h.should == {}
+ end
+
+ it "converts [key, value] pairs to a hash" do
+ hash = [[:a, 1], [:b, 2]].to_h
+ hash.should == { a: 1, b: 2 }
+ end
+
+ it "uses the last value of a duplicated key" do
+ hash = [[:a, 1], [:b, 2], [:a, 3]].to_h
+ hash.should == { a: 3, b: 2 }
+ end
+
+ it "calls #to_ary on contents" do
+ pair = mock('to_ary')
+ pair.should_receive(:to_ary).and_return([:b, 2])
+ hash = [[:a, 1], pair].to_h
+ hash.should == { a: 1, b: 2 }
+ end
+
+ it "raises TypeError if an element is not an array" do
+ -> { [:x].to_h }.should.raise(TypeError)
+ end
+
+ it "raises ArgumentError if an element is not a [key, value] pair" do
+ -> { [[:x]].to_h }.should.raise(ArgumentError)
+ end
+
+ it "does not accept arguments" do
+ -> { [].to_h(:a, :b) }.should.raise(ArgumentError)
+ end
+
+ it "produces a hash that returns nil for a missing element" do
+ [[:a, 1], [:b, 2]].to_h[:c].should == nil
+ end
+
+ context "with block" do
+ it "converts [key, value] pairs returned by the block to a Hash" do
+ [:a, :b].to_h { |k| [k, k.to_s] }.should == { a: 'a', b: 'b' }
+ end
+
+ it "passes to a block each element as a single argument" do
+ ScratchPad.record []
+ [[:a, 1], [:b, 2]].to_h { |*args| ScratchPad << args; [args[0], args[1]] }
+ ScratchPad.recorded.sort.should == [[[:a, 1]], [[:b, 2]]]
+ end
+
+ it "raises ArgumentError if block returns longer or shorter array" do
+ -> do
+ [:a, :b].to_h { |k| [k, k.to_s, 1] }
+ end.should.raise(ArgumentError, /wrong array length at 0/)
+
+ -> do
+ [:a, :b].to_h { |k| [k] }
+ end.should.raise(ArgumentError, /wrong array length at 0/)
+ end
+
+ it "raises TypeError if block returns something other than Array" do
+ -> do
+ [:a, :b].to_h { |k| "not-array" }
+ end.should.raise(TypeError, /wrong element type String at 0/)
+ end
+
+ it "coerces returned pair to Array with #to_ary" do
+ x = mock('x')
+ x.stub!(:to_ary).and_return([:b, 'b'])
+
+ [:a].to_h { |k| x }.should == { :b => 'b' }
+ end
+
+ it "does not coerce returned pair to Array with #to_a" do
+ x = mock('x')
+ x.stub!(:to_a).and_return([:b, 'b'])
+
+ -> do
+ [:a].to_h { |k| x }
+ end.should.raise(TypeError, /wrong element type MockObject at 0/)
+ end
+ end
+end
+
+describe "Array#to_h" do
+ @value_to_return = -> e { [e, e.to_s] }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :to_h
+end
diff --git a/spec/ruby/core/array/to_s_spec.rb b/spec/ruby/core/array/to_s_spec.rb
new file mode 100644
index 0000000000..483e14f902
--- /dev/null
+++ b/spec/ruby/core/array/to_s_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Array#to_s" do
+ it "is an alias of Array#inspect" do
+ Array.instance_method(:to_s).should == Array.instance_method(:inspect)
+ end
+end
diff --git a/spec/ruby/core/array/transpose_spec.rb b/spec/ruby/core/array/transpose_spec.rb
new file mode 100644
index 0000000000..d45e9c351c
--- /dev/null
+++ b/spec/ruby/core/array/transpose_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#transpose" do
+ it "assumes an array of arrays and returns the result of transposing rows and columns" do
+ [[1, 'a'], [2, 'b'], [3, 'c']].transpose.should == [[1, 2, 3], ["a", "b", "c"]]
+ [[1, 2, 3], ["a", "b", "c"]].transpose.should == [[1, 'a'], [2, 'b'], [3, 'c']]
+ [].transpose.should == []
+ [[]].transpose.should == []
+ [[], []].transpose.should == []
+ [[0]].transpose.should == [[0]]
+ [[0], [1]].transpose.should == [[0, 1]]
+ end
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('[1,2]')
+ obj.should_receive(:to_ary).and_return([1, 2])
+ [obj, [:a, :b]].transpose.should == [[1, :a], [2, :b]]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.transpose.should == empty
+
+ a = []; a << a
+ b = []; b << b
+ [a, b].transpose.should == [[a, b]]
+
+ a = [1]; a << a
+ b = [2]; b << b
+ [a, b].transpose.should == [ [1, 2], [a, b] ]
+ end
+
+ it "raises a TypeError if the passed Argument does not respond to #to_ary" do
+ -> { [Object.new, [:a, :b]].transpose }.should.raise(TypeError)
+ end
+
+ it "does not call to_ary on array subclass elements" do
+ ary = [ArraySpecs::ToAryArray[1, 2], ArraySpecs::ToAryArray[4, 6]]
+ ary.transpose.should == [[1, 4], [2, 6]]
+ end
+
+ it "raises an IndexError if the arrays are not of the same length" do
+ -> { [[1, 2], [:a]].transpose }.should.raise(IndexError)
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ result = ArraySpecs::MyArray[ArraySpecs::MyArray[1, 2, 3], ArraySpecs::MyArray[4, 5, 6]].transpose
+ result.should.instance_of?(Array)
+ result[0].should.instance_of?(Array)
+ result[1].should.instance_of?(Array)
+ end
+end
diff --git a/spec/ruby/core/array/try_convert_spec.rb b/spec/ruby/core/array/try_convert_spec.rb
new file mode 100644
index 0000000000..3eaa0f4b7c
--- /dev/null
+++ b/spec/ruby/core/array/try_convert_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array.try_convert" do
+ it "returns the argument if it's an Array" do
+ x = Array.new
+ Array.try_convert(x).should.equal?(x)
+ end
+
+ it "returns the argument if it's a kind of Array" do
+ x = ArraySpecs::MyArray[]
+ Array.try_convert(x).should.equal?(x)
+ end
+
+ it "returns nil when the argument does not respond to #to_ary" do
+ Array.try_convert(Object.new).should == nil
+ end
+
+ it "sends #to_ary to the argument and returns the result if it's nil" do
+ obj = mock("to_ary")
+ obj.should_receive(:to_ary).and_return(nil)
+ Array.try_convert(obj).should == nil
+ end
+
+ it "sends #to_ary to the argument and returns the result if it's an Array" do
+ x = Array.new
+ obj = mock("to_ary")
+ obj.should_receive(:to_ary).and_return(x)
+ Array.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_ary to the argument and returns the result if it's a kind of Array" do
+ x = ArraySpecs::MyArray[]
+ obj = mock("to_ary")
+ obj.should_receive(:to_ary).and_return(x)
+ Array.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_ary to the argument and raises TypeError if it's not a kind of Array" do
+ obj = mock("to_ary")
+ obj.should_receive(:to_ary).and_return(Object.new)
+ -> { Array.try_convert obj }.should raise_consistent_error(TypeError, "can't convert MockObject into Array (MockObject#to_ary gives Object)")
+ end
+
+ it "does not rescue exceptions raised by #to_ary" do
+ obj = mock("to_ary")
+ obj.should_receive(:to_ary).and_raise(RuntimeError)
+ -> { Array.try_convert obj }.should.raise(RuntimeError)
+ end
+end
diff --git a/spec/ruby/core/array/union_spec.rb b/spec/ruby/core/array/union_spec.rb
new file mode 100644
index 0000000000..110894e83d
--- /dev/null
+++ b/spec/ruby/core/array/union_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/union'
+
+describe "Array#|" do
+ it_behaves_like :array_binary_union, :|
+end
+
+describe "Array#union" do
+ it_behaves_like :array_binary_union, :union
+
+ it "returns unique elements when given no argument" do
+ x = [1, 2, 3, 2]
+ x.union.should == [1, 2, 3]
+ end
+
+ it "does not return subclass instances for Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].union.should.instance_of?(Array)
+ end
+
+ it "accepts multiple arguments" do
+ x = [1, 2, 3]
+ x.union(x, x, x, x, [3, 4], x).should == [1, 2, 3, 4]
+ end
+end
diff --git a/spec/ruby/core/array/uniq_spec.rb b/spec/ruby/core/array/uniq_spec.rb
new file mode 100644
index 0000000000..0289bee7c2
--- /dev/null
+++ b/spec/ruby/core/array/uniq_spec.rb
@@ -0,0 +1,243 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iterable_and_tolerating_size_increasing'
+
+describe "Array#uniq" do
+ it "returns an array with no duplicates" do
+ ["a", "a", "b", "b", "c"].uniq.should == ["a", "b", "c"]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.uniq.should == [empty]
+
+ array = ArraySpecs.recursive_array
+ array.uniq.should == [1, 'two', 3.0, array]
+ end
+
+ it "uses eql? semantics" do
+ [1.0, 1].uniq.should == [1.0, 1]
+ end
+
+ it "compares elements first with hash" do
+ x = mock('0')
+ x.should_receive(:hash).at_least(1).and_return(0)
+ y = mock('0')
+ y.should_receive(:hash).at_least(1).and_return(0)
+
+ [x, y].uniq.should == [x, y]
+ end
+
+ it "does not compare elements with different hash codes via eql?" do
+ x = mock('0')
+ x.should_not_receive(:eql?)
+ y = mock('1')
+ y.should_not_receive(:eql?)
+
+ x.should_receive(:hash).at_least(1).and_return(0)
+ y.should_receive(:hash).at_least(1).and_return(1)
+
+ [x, y].uniq.should == [x, y]
+ end
+
+ it "compares elements with matching hash codes with #eql?" do
+ a = Array.new(2) do
+ obj = mock('0')
+ obj.should_receive(:hash).at_least(1).and_return(0)
+
+ def obj.eql?(o)
+ false
+ end
+
+ obj
+ end
+
+ a.uniq.should == a
+
+ a = Array.new(2) do
+ obj = mock('0')
+ obj.should_receive(:hash).at_least(1).and_return(0)
+
+ def obj.eql?(o)
+ true
+ end
+
+ obj
+ end
+
+ a.uniq.size.should == 1
+ end
+
+ it "compares elements based on the value returned from the block" do
+ a = [1, 2, 3, 4]
+ a.uniq { |x| x >= 2 ? 1 : 0 }.should == [1, 2]
+ end
+
+ it "yields items in order" do
+ a = [1, 2, 3]
+ yielded = []
+ a.uniq { |v| yielded << v }
+ yielded.should == a
+ end
+
+ it "handles nil and false like any other values" do
+ [nil, false, 42].uniq { :foo }.should == [nil]
+ [false, nil, 42].uniq { :bar }.should == [false]
+ end
+
+ it "returns Array instance on Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].uniq.should.instance_of?(Array)
+ end
+
+ it "properly handles an identical item even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.should_receive(:hash).at_least(1).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ [x, x].uniq.should == [x]
+ end
+
+ describe "given an array of BasicObject subclasses that define ==, eql?, and hash" do
+ # jruby/jruby#3227
+ it "filters equivalent elements using those definitions" do
+
+ basic = Class.new(BasicObject) do
+ attr_reader :x
+
+ def initialize(x)
+ @x = x
+ end
+
+ def ==(rhs)
+ @x == rhs.x
+ end
+ alias_method :eql?, :==
+
+ def hash
+ @x.hash
+ end
+ end
+
+ a = [basic.new(3), basic.new(2), basic.new(1), basic.new(4), basic.new(1), basic.new(2), basic.new(3)]
+ a.uniq.should == [basic.new(3), basic.new(2), basic.new(1), basic.new(4)]
+ end
+ end
+end
+
+describe "Array#uniq" do
+ @value_to_return = -> e { e }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :uniq
+end
+
+describe "Array#uniq!" do
+ it "modifies the array in place" do
+ a = [ "a", "a", "b", "b", "c" ]
+ a.uniq!
+ a.should == ["a", "b", "c"]
+ end
+
+ it "returns self" do
+ a = [ "a", "a", "b", "b", "c" ]
+ a.should.equal?(a.uniq!)
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty_dup = empty.dup
+ empty.uniq!
+ empty.should == empty_dup
+
+ array = ArraySpecs.recursive_array
+ expected = array[0..3]
+ array.uniq!
+ array.should == expected
+ end
+
+ it "compares elements first with hash" do
+ x = mock('0')
+ x.should_receive(:hash).at_least(1).and_return(0)
+ y = mock('0')
+ y.should_receive(:hash).at_least(1).and_return(0)
+
+ a = [x, y]
+ a.uniq!
+ a.should == [x, y]
+ end
+
+ it "does not compare elements with different hash codes via eql?" do
+ x = mock('0')
+ x.should_not_receive(:eql?)
+ y = mock('1')
+ y.should_not_receive(:eql?)
+
+ x.should_receive(:hash).at_least(1).and_return(0)
+ y.should_receive(:hash).at_least(1).and_return(1)
+
+ a = [x, y]
+ a.uniq!
+ a.should == [x, y]
+ end
+
+ it "returns nil if no changes are made to the array" do
+ [ "a", "b", "c" ].uniq!.should == nil
+ end
+
+ it "raises a FrozenError on a frozen array when the array is modified" do
+ dup_ary = [1, 1, 2]
+ dup_ary.freeze
+ -> { dup_ary.uniq! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen array when the array would not be modified" do
+ -> { ArraySpecs.frozen_array.uniq!}.should.raise(FrozenError)
+ -> { ArraySpecs.empty_frozen_array.uniq!}.should.raise(FrozenError)
+ end
+
+ it "doesn't yield to the block on a frozen array" do
+ -> { ArraySpecs.frozen_array.uniq!{ raise RangeError, "shouldn't yield"}}.should.raise(FrozenError)
+ end
+
+ it "compares elements based on the value returned from the block" do
+ a = [1, 2, 3, 4]
+ a.uniq! { |x| x >= 2 ? 1 : 0 }.should == [1, 2]
+ end
+
+ it "properly handles an identical item even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.should_receive(:hash).at_least(1).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ a = [x, x]
+ a.uniq!
+ a.should == [x]
+ end
+
+ it "does not truncate the array is the block raises an exception" do
+ a = [1, 2, 3]
+ begin
+ a.send(@method) { raise StandardError, 'Oops' }
+ rescue
+ end
+
+ a.should == [1, 2, 3]
+ end
+
+ it "doesn't change array if error is raised" do
+ a = [1, 1, 2, 2, 3, 3, 4, 4]
+ begin
+ a.send(@method) do |e|
+ raise StandardError, 'Oops' if e == 3
+ e
+ end
+ rescue StandardError
+ end
+
+ a.should == [1, 1, 2, 2, 3, 3, 4, 4]
+ end
+end
+
+describe "Array#uniq!" do
+ @value_to_return = -> e { e }
+ it_behaves_like :array_iterable_and_tolerating_size_increasing, :uniq!
+end
diff --git a/spec/ruby/core/array/unshift_spec.rb b/spec/ruby/core/array/unshift_spec.rb
new file mode 100644
index 0000000000..c190db4d02
--- /dev/null
+++ b/spec/ruby/core/array/unshift_spec.rb
@@ -0,0 +1,67 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#unshift" do
+ it "prepends object to the original array" do
+ a = [1, 2, 3]
+ a.unshift("a").should.equal?(a)
+ a.should == ['a', 1, 2, 3]
+ a.unshift.should.equal?(a)
+ a.should == ['a', 1, 2, 3]
+ a.unshift(5, 4, 3)
+ a.should == [5, 4, 3, 'a', 1, 2, 3]
+
+ # shift all but one element
+ a = [1, 2]
+ a.shift
+ a.unshift(3, 4)
+ a.should == [3, 4, 2]
+
+ # now shift all elements
+ a.shift
+ a.shift
+ a.shift
+ a.unshift(3, 4)
+ a.should == [3, 4]
+ end
+
+ it "returns self" do
+ a = [1, 2, 3]
+ a.unshift("a").should.equal?(a)
+ end
+
+ it "quietly ignores unshifting nothing" do
+ [].unshift.should == []
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.unshift(:new).should == [:new, empty]
+
+ array = ArraySpecs.recursive_array
+ array.unshift(:new)
+ array[0..5].should == [:new, 1, 'two', 3.0, array, array]
+ end
+
+ it "raises a FrozenError on a frozen array when the array is modified" do
+ -> { ArraySpecs.frozen_array.unshift(1) }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen array when the array would not be modified" do
+ -> { ArraySpecs.frozen_array.unshift }.should.raise(FrozenError)
+ end
+
+ # https://github.com/truffleruby/truffleruby/issues/2772
+ it "doesn't rely on Array#[]= so it can be overridden" do
+ subclass = Class.new(Array) do
+ def []=(*)
+ raise "[]= is called"
+ end
+ end
+
+ array = subclass.new
+ array.unshift(1)
+ array.should == [1]
+ end
+end
diff --git a/spec/ruby/core/array/values_at_spec.rb b/spec/ruby/core/array/values_at_spec.rb
new file mode 100644
index 0000000000..e11e7e4451
--- /dev/null
+++ b/spec/ruby/core/array/values_at_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Should be synchronized with core/struct/values_at_spec.rb
+describe "Array#values_at" do
+ it "returns an array of elements at the indexes when passed indexes" do
+ [1, 2, 3, 4, 5].values_at().should == []
+ [1, 2, 3, 4, 5].values_at(1, 0, 5, -1, -8, 10).should == [2, 1, nil, 5, nil, nil]
+ end
+
+ it "calls to_int on its indices" do
+ obj = mock('1')
+ def obj.to_int() 1 end
+ [1, 2].values_at(obj, obj, obj).should == [2, 2, 2]
+ end
+
+ it "properly handles recursive arrays" do
+ empty = ArraySpecs.empty_recursive_array
+ empty.values_at(0, 1, 2).should == [empty, nil, nil]
+
+ array = ArraySpecs.recursive_array
+ array.values_at(0, 1, 2, 3).should == [1, 'two', 3.0, array]
+ end
+
+ describe "when passed ranges" do
+ it "returns an array of elements in the ranges" do
+ [1, 2, 3, 4, 5].values_at(0..2, 1...3, 2..-2).should == [1, 2, 3, 2, 3, 3, 4]
+ [1, 2, 3, 4, 5].values_at(6..4).should == []
+ end
+
+ it "calls to_int on arguments of ranges" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ def from.to_int() 1 end
+ def to.to_int() -2 end
+
+ ary = [1, 2, 3, 4, 5]
+ ary.values_at(from .. to, from ... to, to .. from).should == [2, 3, 4, 2, 3]
+ end
+ end
+
+ describe "when passed a range" do
+ it "fills with nil if the index is out of the range" do
+ [0, 1].values_at(0..3).should == [0, 1, nil, nil]
+ [0, 1].values_at(2..4).should == [nil, nil, nil]
+ end
+
+ describe "on an empty array" do
+ it "fills with nils if the index is out of the range" do
+ [].values_at(0..2).should == [nil, nil, nil]
+ [].values_at(1..3).should == [nil, nil, nil]
+ end
+ end
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].values_at(0, 1..2, 1).should.instance_of?(Array)
+ end
+
+ it "works when given endless ranges" do
+ [1, 2, 3, 4].values_at(eval("(1..)")).should == [2, 3, 4]
+ [1, 2, 3, 4].values_at(eval("(3...)")).should == [4]
+ end
+
+ it "works when given beginless ranges" do
+ [1, 2, 3, 4].values_at((..2)).should == [1, 2, 3]
+ [1, 2, 3, 4].values_at((...2)).should == [1, 2]
+ end
+end
diff --git a/spec/ruby/core/array/zip_spec.rb b/spec/ruby/core/array/zip_spec.rb
new file mode 100644
index 0000000000..3ccdf143d6
--- /dev/null
+++ b/spec/ruby/core/array/zip_spec.rb
@@ -0,0 +1,71 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Array#zip" do
+ it "returns an array of arrays containing corresponding elements of each array" do
+ [1, 2, 3, 4].zip(["a", "b", "c", "d", "e"]).should ==
+ [[1, "a"], [2, "b"], [3, "c"], [4, "d"]]
+ end
+
+ it "fills in missing values with nil" do
+ [1, 2, 3, 4, 5].zip(["a", "b", "c", "d"]).should ==
+ [[1, "a"], [2, "b"], [3, "c"], [4, "d"], [5, nil]]
+ end
+
+ it "properly handles recursive arrays" do
+ a = []; a << a
+ b = [1]; b << b
+
+ a.zip(a).should == [ [a[0], a[0]] ]
+ a.zip(b).should == [ [a[0], b[0]] ]
+ b.zip(a).should == [ [b[0], a[0]], [b[1], a[1]] ]
+ b.zip(b).should == [ [b[0], b[0]], [b[1], b[1]] ]
+ end
+
+ it "calls #to_ary to convert the argument to an Array" do
+ obj = mock('[3,4]')
+ obj.should_receive(:to_ary).and_return([3, 4])
+ [1, 2].zip(obj).should == [[1, 3], [2, 4]]
+ end
+
+ it "uses #each to extract arguments' elements when #to_ary fails" do
+ obj = Class.new do
+ def each(&b)
+ [3,4].each(&b)
+ end
+ end.new
+
+ [1, 2].zip(obj).should == [[1, 3], [2, 4]]
+ end
+
+ it "stops at own size when given an infinite enumerator" do
+ [1, 2].zip(10.upto(Float::INFINITY)).should == [[1, 10], [2, 11]]
+ end
+
+ it "fills nil when the given enumerator is shorter than self" do
+ obj = Object.new
+ def obj.each
+ yield 10
+ end
+ [1, 2].zip(obj).should == [[1, 10], [2, nil]]
+ end
+
+ it "calls block if supplied" do
+ values = []
+ [1, 2, 3, 4].zip(["a", "b", "c", "d", "e"]) { |value|
+ values << value
+ }.should == nil
+
+ values.should == [[1, "a"], [2, "b"], [3, "c"], [4, "d"]]
+ end
+
+ it "does not return subclass instance on Array subclasses" do
+ ArraySpecs::MyArray[1, 2, 3].zip(["a", "b"]).should.instance_of?(Array)
+ end
+
+ it "raises TypeError when some argument isn't Array and doesn't respond to #to_ary and #to_enum" do
+ -> { [1, 2, 3].zip(Object.new) }.should.raise(TypeError, "wrong argument type Object (must respond to :each)")
+ -> { [1, 2, 3].zip(1) }.should.raise(TypeError, "wrong argument type Integer (must respond to :each)")
+ -> { [1, 2, 3].zip(true) }.should.raise(TypeError, "wrong argument type TrueClass (must respond to :each)")
+ end
+end
diff --git a/spec/ruby/core/basicobject/__id__spec.rb b/spec/ruby/core/basicobject/__id__spec.rb
new file mode 100644
index 0000000000..6766db4e82
--- /dev/null
+++ b/spec/ruby/core/basicobject/__id__spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/object_id'
+
+describe "BasicObject#__id__" do
+ it_behaves_like :object_id, :__id__, BasicObject
+end
diff --git a/spec/ruby/core/basicobject/__send___spec.rb b/spec/ruby/core/basicobject/__send___spec.rb
new file mode 100644
index 0000000000..2f814e448c
--- /dev/null
+++ b/spec/ruby/core/basicobject/__send___spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/basicobject/send'
+
+describe "BasicObject#__send__" do
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:__send__)
+ end
+
+ it_behaves_like :basicobject_send, :__send__
+end
diff --git a/spec/ruby/core/basicobject/basicobject_spec.rb b/spec/ruby/core/basicobject/basicobject_spec.rb
new file mode 100644
index 0000000000..af28de0687
--- /dev/null
+++ b/spec/ruby/core/basicobject/basicobject_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "BasicObject" do
+ it "raises NoMethodError for nonexistent methods after #method_missing is removed" do
+ script = fixture __FILE__, "remove_method_missing.rb"
+ ruby_exe(script).chomp.should == "NoMethodError"
+ end
+
+ it "raises NameError when referencing built-in constants" do
+ -> { class BasicObjectSpecs::BOSubclass; Kernel; end }.should.raise(NameError)
+ end
+
+ it "does not define built-in constants (according to const_defined?)" do
+ BasicObject.const_defined?(:Kernel).should == false
+ end
+
+ it "does not define built-in constants (according to defined?)" do
+ BasicObjectSpecs::BOSubclass.kernel_defined?.should == nil
+ end
+
+ it "is included in Object's list of constants" do
+ Object.constants(false).should.include?(:BasicObject)
+ end
+
+ it "includes itself in its list of constants" do
+ BasicObject.constants(false).should.include?(:BasicObject)
+ end
+end
+
+describe "BasicObject metaclass" do
+ before :each do
+ @meta = class << BasicObject; self; end
+ end
+
+ it "is an instance of Class" do
+ @meta.should.instance_of?(Class)
+ end
+
+ it "has Class as superclass" do
+ @meta.superclass.should.equal?(Class)
+ end
+
+ it "contains methods for the BasicObject class" do
+ @meta.class_eval do
+ def rubyspec_test_method() :test end
+ end
+
+ BasicObject.rubyspec_test_method.should == :test
+ end
+end
+
+describe "BasicObject instance metaclass" do
+ before :each do
+ @object = BasicObject.new
+ @meta = class << @object; self; end
+ end
+
+ it "is an instance of Class" do
+ @meta.should.instance_of?(Class)
+ end
+
+ it "has BasicObject as superclass" do
+ @meta.superclass.should.equal?(BasicObject)
+ end
+
+ it "contains methods defined for the BasicObject instance" do
+ @meta.class_eval do
+ def test_method() :test end
+ end
+
+ @object.test_method.should == :test
+ end
+end
+
+describe "BasicObject subclass" do
+ it "contains Kernel methods when including Kernel" do
+ obj = BasicObjectSpecs::BOSubclass.new
+
+ obj.instance_variable_set(:@test, :value)
+ obj.instance_variable_get(:@test).should == :value
+
+ obj.respond_to?(:hash).should == true
+ end
+
+ describe "BasicObject references" do
+ it "can refer to BasicObject from within itself" do
+ -> { BasicObject::BasicObject }.should_not.raise
+ end
+ end
+end
diff --git a/spec/ruby/core/basicobject/equal_spec.rb b/spec/ruby/core/basicobject/equal_spec.rb
new file mode 100644
index 0000000000..c0f41dc0c0
--- /dev/null
+++ b/spec/ruby/core/basicobject/equal_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/equal'
+
+describe "BasicObject#equal?" do
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:equal?)
+ end
+
+ it_behaves_like :object_equal, :equal?
+
+ it "is unaffected by overriding __id__" do
+ o1 = mock("object")
+ o2 = mock("object")
+ suppress_warning {
+ def o1.__id__; 10; end
+ def o2.__id__; 10; end
+ }
+ o1.equal?(o2).should == false
+ end
+
+ it "is unaffected by overriding object_id" do
+ o1 = mock("object")
+ o1.stub!(:object_id).and_return(10)
+ o2 = mock("object")
+ o2.stub!(:object_id).and_return(10)
+ o1.equal?(o2).should == false
+ end
+
+ it "is unaffected by overriding ==" do
+ # different objects, overriding == to return true
+ o1 = mock("object")
+ o1.stub!(:==).and_return(true)
+ o2 = mock("object")
+ o1.equal?(o2).should == false
+
+ # same objects, overriding == to return false
+ o3 = mock("object")
+ o3.stub!(:==).and_return(false)
+ o3.equal?(o3).should == true
+ end
+
+ it "is unaffected by overriding eql?" do
+ # different objects, overriding eql? to return true
+ o1 = mock("object")
+ o1.stub!(:eql?).and_return(true)
+ o2 = mock("object")
+ o1.equal?(o2).should == false
+
+ # same objects, overriding eql? to return false
+ o3 = mock("object")
+ o3.stub!(:eql?).and_return(false)
+ o3.equal?(o3).should == true
+ end
+end
diff --git a/spec/ruby/core/basicobject/equal_value_spec.rb b/spec/ruby/core/basicobject/equal_value_spec.rb
new file mode 100644
index 0000000000..eb951a8305
--- /dev/null
+++ b/spec/ruby/core/basicobject/equal_value_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/equal'
+
+describe "BasicObject#==" do
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:==)
+ end
+
+ it_behaves_like :object_equal, :==
+end
diff --git a/spec/ruby/core/basicobject/fixtures/classes.rb b/spec/ruby/core/basicobject/fixtures/classes.rb
new file mode 100644
index 0000000000..ed5a2dda17
--- /dev/null
+++ b/spec/ruby/core/basicobject/fixtures/classes.rb
@@ -0,0 +1,255 @@
+module BasicObjectSpecs
+ class IVars
+ def initialize
+ @secret = 99
+ end
+ end
+
+ module InstExec
+ def self.included(base)
+ base.instance_exec { @@count = 2 }
+ end
+ end
+
+ module InstExecIncluded
+ include InstExec
+ end
+
+ module InstEval
+ module CVar
+ module Get
+ class ReceiverScope
+ @@cvar = :value_defined_in_receiver_scope
+ end
+
+ class BlockDefinitionScope
+ @@cvar = :value_defined_in_block_definition_scope
+
+ def block
+ -> * { @@cvar }
+ end
+ end
+
+ class CallerScope
+ @@cvar = :value_defined_in_caller_scope
+
+ def get_class_variable_with_string(obj)
+ obj.instance_eval("@@cvar")
+ end
+
+ def get_class_variable_with_block(obj, block)
+ obj.instance_eval(&block)
+ end
+ end
+
+ class CallerWithoutCVarScope
+ def get_class_variable_with_string(obj)
+ obj.instance_eval("@@cvar")
+ end
+ end
+
+ ReceiverWithCVarDefinedInSingletonClass = Class.new.new.tap do |obj|
+ obj.singleton_class.class_variable_set(:@@cvar, :value_defined_in_receiver_singleton_class)
+ end
+ end
+
+ module Set
+ class ReceiverScope
+ end
+
+ class BlockDefinitionScope
+ def self.get_class_variable
+ @@cvar
+ end
+
+ def block_to_assign(value)
+ -> * { @@cvar = value }
+ end
+ end
+
+ class CallerScope
+ def self.get_class_variable
+ @@cvar
+ end
+
+ def set_class_variable_with_string(obj, value)
+ obj.instance_eval("@@cvar=#{value.inspect}")
+ end
+
+ def set_class_variable_with_block(obj, block)
+ obj.instance_eval(&block)
+ end
+ end
+ end
+ end
+ end
+
+ module InstEval
+ module Constants
+ module ConstantInReceiverSingletonClass
+ module ReceiverScope
+ FOO = :ReceiverScope
+
+ class ReceiverParent
+ FOO = :ReceiverParent
+ end
+
+ class Receiver < ReceiverParent
+ FOO = :Receiver
+
+ def initialize
+ self.singleton_class.const_set(:FOO, :singleton_class)
+ end
+ end
+ end
+
+ module CallerScope
+ FOO = :CallerScope
+
+ class CallerParent
+ FOO = :CallerParent
+ end
+
+ class Caller < CallerParent
+ FOO = :Caller
+
+ def get_constant_with_string(receiver)
+ receiver.instance_eval("FOO")
+ end
+ end
+ end
+ end
+
+ module ConstantInReceiverClass
+ module ReceiverScope
+ FOO = :ReceiverScope
+
+ class ReceiverParent
+ FOO = :ReceiverParent
+ end
+
+ class Receiver < ReceiverParent
+ FOO = :Receiver
+ end
+ end
+
+ module CallerScope
+ FOO = :CallerScope
+
+ class CallerParent
+ FOO = :CallerParent
+ end
+
+ class Caller < CallerParent
+ FOO = :Caller
+
+ def get_constant_with_string(receiver)
+ receiver.instance_eval("FOO")
+ end
+ end
+ end
+ end
+
+ module ConstantInCallerClass
+ module ReceiverScope
+ FOO = :ReceiverScope
+
+ class ReceiverParent
+ FOO = :ReceiverParent
+ end
+
+ class Receiver < ReceiverParent
+ # FOO is not declared in a receiver class
+ end
+ end
+
+ module CallerScope
+ FOO = :CallerScope
+
+ class CallerParent
+ FOO = :CallerParent
+ end
+
+ class Caller < CallerParent
+ FOO = :Caller
+
+ def get_constant_with_string(receiver)
+ receiver.instance_eval("FOO")
+ end
+ end
+ end
+ end
+
+ module ConstantInCallerOuterScopes
+ module ReceiverScope
+ FOO = :ReceiverScope
+
+ class ReceiverParent
+ FOO = :ReceiverParent
+ end
+
+ class Receiver < ReceiverParent
+ # FOO is not declared in a receiver class
+ end
+ end
+
+ module CallerScope
+ FOO = :CallerScope
+
+ class CallerParent
+ FOO = :CallerParent
+ end
+
+ class Caller < CallerParent
+ # FOO is not declared in a caller class
+
+ def get_constant_with_string(receiver)
+ receiver.instance_eval("FOO")
+ end
+ end
+ end
+ end
+
+ module ConstantInReceiverParentClass
+ module ReceiverScope
+ FOO = :ReceiverScope
+
+ class ReceiverParent
+ FOO = :ReceiverParent
+ end
+
+ class Receiver < ReceiverParent
+ # FOO is not declared in a receiver class
+ end
+ end
+
+ module CallerScope
+ # FOO is not declared in a caller outer scopes
+
+ class CallerParent
+ FOO = :CallerParent
+ end
+
+ class Caller < CallerParent
+ # FOO is not declared in a caller class
+
+ def get_constant_with_string(receiver)
+ receiver.instance_eval("FOO")
+ end
+ end
+ end
+ end
+ end
+ end
+
+ class InstEvalConst
+ INST_EVAL_CONST_X = 2
+ end
+
+ module InstEvalOuter
+ module Inner
+ obj = InstEvalConst.new
+ X_BY_BLOCK = obj.instance_eval { INST_EVAL_CONST_X } rescue nil
+ end
+ end
+end
diff --git a/spec/ruby/core/basicobject/fixtures/common.rb b/spec/ruby/core/basicobject/fixtures/common.rb
new file mode 100644
index 0000000000..3447a3a5e7
--- /dev/null
+++ b/spec/ruby/core/basicobject/fixtures/common.rb
@@ -0,0 +1,9 @@
+module BasicObjectSpecs
+ class BOSubclass < BasicObject
+ def self.kernel_defined?
+ defined?(Kernel)
+ end
+
+ include ::Kernel
+ end
+end
diff --git a/spec/ruby/core/basicobject/fixtures/remove_method_missing.rb b/spec/ruby/core/basicobject/fixtures/remove_method_missing.rb
new file mode 100644
index 0000000000..095b982d3a
--- /dev/null
+++ b/spec/ruby/core/basicobject/fixtures/remove_method_missing.rb
@@ -0,0 +1,9 @@
+class BasicObject
+ remove_method :method_missing
+end
+
+begin
+ Object.new.test_method
+rescue NoMethodError => e
+ puts e.class.name
+end
diff --git a/spec/ruby/core/basicobject/fixtures/singleton_method.rb b/spec/ruby/core/basicobject/fixtures/singleton_method.rb
new file mode 100644
index 0000000000..0e00e035fa
--- /dev/null
+++ b/spec/ruby/core/basicobject/fixtures/singleton_method.rb
@@ -0,0 +1,10 @@
+module BasicObjectSpecs
+ class SingletonMethod
+ def self.singleton_method_added name
+ ScratchPad.record [:singleton_method_added, name]
+ end
+
+ def self.singleton_method_to_alias
+ end
+ end
+end
diff --git a/spec/ruby/core/basicobject/initialize_spec.rb b/spec/ruby/core/basicobject/initialize_spec.rb
new file mode 100644
index 0000000000..09e86676b6
--- /dev/null
+++ b/spec/ruby/core/basicobject/initialize_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "BasicObject#initialize" do
+ it "is a private instance method" do
+ BasicObject.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "does not accept arguments" do
+ -> {
+ BasicObject.new("This", "makes it easier", "to call super", "from other constructors")
+ }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/basicobject/instance_eval_spec.rb b/spec/ruby/core/basicobject/instance_eval_spec.rb
new file mode 100644
index 0000000000..124d179b5a
--- /dev/null
+++ b/spec/ruby/core/basicobject/instance_eval_spec.rb
@@ -0,0 +1,327 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "BasicObject#instance_eval" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:instance_eval)
+ end
+
+ it "sets self to the receiver in the context of the passed block" do
+ a = BasicObject.new
+ a.instance_eval { self }.equal?(a).should == true
+ end
+
+ it "evaluates strings" do
+ a = BasicObject.new
+ a.instance_eval('self').equal?(a).should == true
+ end
+
+ it "raises an ArgumentError when no arguments and no block are given" do
+ -> { "hola".instance_eval }.should.raise(ArgumentError, "wrong number of arguments (given 0, expected 1..3)")
+ end
+
+ it "raises an ArgumentError when a block and normal arguments are given" do
+ -> { "hola".instance_eval(4, 5) {|a,b| a + b } }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 0)")
+ end
+
+ it "raises an ArgumentError when more than 3 arguments are given" do
+ -> {
+ "hola".instance_eval("1 + 1", "some file", 0, "bogus")
+ }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)")
+ end
+
+ it "yields the object to the block" do
+ "hola".instance_eval {|o| ScratchPad.record o }
+ ScratchPad.recorded.should == "hola"
+ end
+
+ it "returns the result of the block" do
+ "hola".instance_eval { :result }.should == :result
+ end
+
+ it "only binds the eval to the receiver" do
+ f = Object.new
+ f.instance_eval do
+ def foo
+ 1
+ end
+ end
+ f.foo.should == 1
+ -> { Object.new.foo }.should.raise(NoMethodError)
+ end
+
+ it "preserves self in the original block when passed a block argument" do
+ prc = proc { self }
+
+ old_self = prc.call
+
+ new_self = Object.new
+ new_self.instance_eval(&prc).should == new_self
+
+ prc.call.should == old_self
+ end
+
+ # TODO: This should probably be replaced with a "should behave like" that uses
+ # the many scoping/binding specs from kernel/eval_spec, since most of those
+ # behaviors are the same for instance_eval. See also module_eval/class_eval.
+
+ it "binds self to the receiver" do
+ s = "hola"
+ (s == s.instance_eval { self }).should == true
+ o = mock('o')
+ (o == o.instance_eval("self")).should == true
+ end
+
+ it "executes in the context of the receiver" do
+ "Ruby-fu".instance_eval { size }.should == 7
+ "hola".instance_eval("size").should == 4
+ Object.class_eval { "hola".instance_eval("to_s") }.should == "hola"
+ Object.class_eval { "Ruby-fu".instance_eval{ to_s } }.should == "Ruby-fu"
+
+ end
+
+ it "uses the caller location as default location" do
+ f = Object.new
+ f.instance_eval("[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1]
+ end
+
+ it "has access to receiver's instance variables" do
+ BasicObjectSpecs::IVars.new.instance_eval { @secret }.should == 99
+ BasicObjectSpecs::IVars.new.instance_eval("@secret").should == 99
+ end
+
+ it "raises TypeError for frozen objects when tries to set receiver's instance variables" do
+ -> { nil.instance_eval { @foo = 42 } }.should.raise(FrozenError, "can't modify frozen NilClass: nil")
+ -> { true.instance_eval { @foo = 42 } }.should.raise(FrozenError, "can't modify frozen TrueClass: true")
+ -> { false.instance_eval { @foo = 42 } }.should.raise(FrozenError, "can't modify frozen FalseClass: false")
+ -> { 1.instance_eval { @foo = 42 } }.should.raise(FrozenError, "can't modify frozen Integer: 1")
+ -> { :symbol.instance_eval { @foo = 42 } }.should.raise(FrozenError, "can't modify frozen Symbol: :symbol")
+
+ obj = Object.new
+ obj.freeze
+ -> { obj.instance_eval { @foo = 42 } }.should.raise(FrozenError)
+ end
+
+ it "treats block-local variables as local to the block" do
+ prc = instance_eval <<-CODE
+ proc do |x, prc|
+ if x
+ n = 2
+ else
+ n = 1
+ prc.call(true, prc)
+ n
+ end
+ end
+ CODE
+
+ prc.call(false, prc).should == 1
+ end
+
+ it "makes the receiver metaclass the scoped class when used with a string" do
+ obj = Object.new
+ obj.instance_eval %{
+ class B; end
+ B
+ }
+ obj.singleton_class.const_get(:B).should.instance_of?(Class)
+ end
+
+ describe "constants lookup when a String given" do
+ it "looks in the receiver singleton class first" do
+ receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::ReceiverScope::Receiver.new
+ caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverSingletonClass::CallerScope::Caller.new
+
+ caller.get_constant_with_string(receiver).should == :singleton_class
+ end
+
+ it "looks in the receiver class next" do
+ receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::ReceiverScope::Receiver.new
+ caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverClass::CallerScope::Caller.new
+
+ caller.get_constant_with_string(receiver).should == :Receiver
+ end
+
+ it "looks in the caller class next" do
+ receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::ReceiverScope::Receiver.new
+ caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerClass::CallerScope::Caller.new
+
+ caller.get_constant_with_string(receiver).should == :Caller
+ end
+
+ it "looks in the caller outer scopes next" do
+ receiver = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::ReceiverScope::Receiver.new
+ caller = BasicObjectSpecs::InstEval::Constants::ConstantInCallerOuterScopes::CallerScope::Caller.new
+
+ caller.get_constant_with_string(receiver).should == :CallerScope
+ end
+
+ it "looks in the receiver class hierarchy next" do
+ receiver = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::ReceiverScope::Receiver.new
+ caller = BasicObjectSpecs::InstEval::Constants::ConstantInReceiverParentClass::CallerScope::Caller.new
+
+ caller.get_constant_with_string(receiver).should == :ReceiverParent
+ end
+ end
+
+ it "doesn't get constants in the receiver if a block given" do
+ BasicObjectSpecs::InstEvalOuter::Inner::X_BY_BLOCK.should == nil
+ end
+
+ it "raises a TypeError when defining methods on an immediate" do
+ -> do
+ 1.instance_eval { def foo; end }
+ end.should.raise(TypeError)
+ -> do
+ :foo.instance_eval { def foo; end }
+ end.should.raise(TypeError)
+ end
+
+ describe "class variables lookup" do
+ it "gets class variables in the caller class when called with a String" do
+ receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
+ caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new
+
+ caller.get_class_variable_with_string(receiver).should == :value_defined_in_caller_scope
+ end
+
+ it "gets class variables in the block definition scope when called with a block" do
+ receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
+ caller = BasicObjectSpecs::InstEval::CVar::Get::CallerScope.new
+ block = BasicObjectSpecs::InstEval::CVar::Get::BlockDefinitionScope.new.block
+
+ caller.get_class_variable_with_block(receiver, block).should == :value_defined_in_block_definition_scope
+ end
+
+ it "sets class variables in the caller class when called with a String" do
+ receiver = BasicObjectSpecs::InstEval::CVar::Set::ReceiverScope.new
+ caller = BasicObjectSpecs::InstEval::CVar::Set::CallerScope.new
+
+ caller.set_class_variable_with_string(receiver, 1)
+ BasicObjectSpecs::InstEval::CVar::Set::CallerScope.get_class_variable.should == 1
+ end
+
+ it "sets class variables in the block definition scope when called with a block" do
+ receiver = BasicObjectSpecs::InstEval::CVar::Set::ReceiverScope.new
+ caller = BasicObjectSpecs::InstEval::CVar::Set::CallerScope.new
+ block = BasicObjectSpecs::InstEval::CVar::Set::BlockDefinitionScope.new.block_to_assign(1)
+
+ caller.set_class_variable_with_block(receiver, block)
+ BasicObjectSpecs::InstEval::CVar::Set::BlockDefinitionScope.get_class_variable.should == 1
+ end
+
+ it "does not have access to class variables in the receiver class when called with a String" do
+ receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverScope.new
+ caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
+ -> { caller.get_class_variable_with_string(receiver) }.should.raise(NameError, /uninitialized class variable @@cvar/)
+ end
+
+ it "does not have access to class variables in the receiver's singleton class when called with a String" do
+ receiver = BasicObjectSpecs::InstEval::CVar::Get::ReceiverWithCVarDefinedInSingletonClass
+ caller = BasicObjectSpecs::InstEval::CVar::Get::CallerWithoutCVarScope.new
+ -> { caller.get_class_variable_with_string(receiver) }.should.raise(NameError, /uninitialized class variable @@cvar/)
+ end
+ end
+
+ it "raises a TypeError when defining methods on numerics" do
+ -> do
+ (1.0).instance_eval { def foo; end }
+ end.should.raise(TypeError)
+ -> do
+ (1 << 64).instance_eval { def foo; end }
+ end.should.raise(TypeError)
+ end
+
+ it "evaluates procs originating from methods" do
+ def meth(arg); arg; end
+
+ m = method(:meth)
+ obj = Object.new
+
+ obj.instance_eval(&m).should == obj
+ end
+
+ it "evaluates string with given filename and linenumber" do
+ err = begin
+ Object.new.instance_eval("raise", "a_file", 10)
+ rescue => e
+ e
+ end
+ err.backtrace.first.split(":")[0..1].should == ["a_file", "10"]
+ end
+
+ it "evaluates string with given filename and negative linenumber" do
+ err = begin
+ Object.new.instance_eval("\n\nraise\n", "b_file", -100)
+ rescue => e
+ e
+ end
+ err.backtrace.first.split(":")[0..1].should == ["b_file", "-98"]
+ end
+
+ it "has access to the caller's local variables" do
+ x = nil
+
+ instance_eval "x = :value"
+
+ x.should == :value
+ end
+
+ it "converts string argument with #to_str method" do
+ source_code = Object.new
+ def source_code.to_str() "1" end
+
+ a = BasicObject.new
+ a.instance_eval(source_code).should == 1
+ end
+
+ it "raises ArgumentError if returned value is not String" do
+ source_code = Object.new
+ def source_code.to_str() :symbol end
+
+ a = BasicObject.new
+ -> { a.instance_eval(source_code) }.should raise_consistent_error(TypeError, /can't convert Object into String/)
+ end
+
+ it "converts filename argument with #to_str method" do
+ filename = Object.new
+ def filename.to_str() "file.rb" end
+
+ err = begin
+ Object.new.instance_eval("raise", filename)
+ rescue => e
+ e
+ end
+ err.backtrace.first.split(":")[0].should == "file.rb"
+ end
+
+ it "raises ArgumentError if returned value is not String" do
+ filename = Object.new
+ def filename.to_str() :symbol end
+
+ -> { Object.new.instance_eval("raise", filename) }.should raise_consistent_error(TypeError, /can't convert Object into String/)
+ end
+
+ it "converts lineno argument with #to_int method" do
+ lineno = Object.new
+ def lineno.to_int() 15 end
+
+ err = begin
+ Object.new.instance_eval("raise", "file.rb", lineno)
+ rescue => e
+ e
+ end
+ err.backtrace.first.split(":")[1].should == "15"
+ end
+
+ it "raises ArgumentError if returned value is not Integer" do
+ lineno = Object.new
+ def lineno.to_int() :symbol end
+
+ -> { Object.new.instance_eval("raise", "file.rb", lineno) }.should raise_consistent_error(TypeError, /can't convert Object into Integer/)
+ end
+end
diff --git a/spec/ruby/core/basicobject/instance_exec_spec.rb b/spec/ruby/core/basicobject/instance_exec_spec.rb
new file mode 100644
index 0000000000..cfce9a65ad
--- /dev/null
+++ b/spec/ruby/core/basicobject/instance_exec_spec.rb
@@ -0,0 +1,113 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "BasicObject#instance_exec" do
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:instance_exec)
+ end
+
+ it "sets self to the receiver in the context of the passed block" do
+ a = BasicObject.new
+ a.instance_exec { self }.equal?(a).should == true
+ end
+
+ it "passes arguments to the block" do
+ a = BasicObject.new
+ a.instance_exec(1) { |b| b }.should.equal?(1)
+ end
+
+ it "raises a LocalJumpError unless given a block" do
+ -> { "hola".instance_exec }.should.raise(LocalJumpError)
+ end
+
+ it "has an arity of -1" do
+ Object.new.method(:instance_exec).arity.should == -1
+ end
+
+ it "accepts arguments with a block" do
+ -> { "hola".instance_exec(4, 5) { |a,b| a + b } }.should_not.raise
+ end
+
+ it "doesn't pass self to the block as an argument" do
+ "hola".instance_exec { |o| o }.should == nil
+ end
+
+ it "passes any arguments to the block" do
+ Object.new.instance_exec(1,2) {|one, two| one + two}.should == 3
+ end
+
+ describe "with optional argument" do
+ it "does not destructure a single array argument" do
+ Object.new.instance_exec([1, 2, 3]) { |a = 99| a }.should == [1, 2, 3]
+ end
+ end
+
+ it "only binds the exec to the receiver" do
+ f = Object.new
+ f.instance_exec do
+ def foo
+ 1
+ end
+ end
+ f.foo.should == 1
+ -> { Object.new.foo }.should.raise(NoMethodError)
+ end
+
+ # TODO: This should probably be replaced with a "should behave like" that uses
+ # the many scoping/binding specs from kernel/eval_spec, since most of those
+ # behaviors are the same for instance_exec. See also module_eval/class_eval.
+
+ it "binds self to the receiver" do
+ s = "hola"
+ (s == s.instance_exec { self }).should == true
+ end
+
+ it "binds the block's binding self to the receiver" do
+ s = "hola"
+ (s == s.instance_exec { eval "self", binding }).should == true
+ end
+
+ it "executes in the context of the receiver" do
+ "Ruby-fu".instance_exec { size }.should == 7
+ Object.class_eval { "Ruby-fu".instance_exec{ to_s } }.should == "Ruby-fu"
+ end
+
+ it "has access to receiver's instance variables" do
+ BasicObjectSpecs::IVars.new.instance_exec { @secret }.should == 99
+ end
+
+ it "sets class variables in the receiver" do
+ BasicObjectSpecs::InstExec.class_variables.should.include?(:@@count)
+ BasicObjectSpecs::InstExec.send(:class_variable_get, :@@count).should == 2
+ end
+
+ it "raises a TypeError when defining methods on an immediate" do
+ -> do
+ 1.instance_exec { def foo; end }
+ end.should.raise(TypeError)
+ -> do
+ :foo.instance_exec { def foo; end }
+ end.should.raise(TypeError)
+ end
+
+ quarantine! do # Not clean, leaves cvars lying around to break other specs
+ it "scopes class var accesses in the caller when called on an Integer" do
+ # Integer can take instance vars
+ Integer.class_eval "@@__tmp_instance_exec_spec = 1"
+ (defined? @@__tmp_instance_exec_spec).should == nil
+
+ @@__tmp_instance_exec_spec = 2
+ 1.instance_exec { @@__tmp_instance_exec_spec }.should == 2
+ Integer.__send__(:remove_class_variable, :@@__tmp_instance_exec_spec)
+ end
+ end
+
+ it "raises a TypeError when defining methods on numerics" do
+ -> do
+ (1.0).instance_exec { def foo; end }
+ end.should.raise(TypeError)
+ -> do
+ (1 << 64).instance_exec { def foo; end }
+ end.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/basicobject/method_missing_spec.rb b/spec/ruby/core/basicobject/method_missing_spec.rb
new file mode 100644
index 0000000000..8785b41b21
--- /dev/null
+++ b/spec/ruby/core/basicobject/method_missing_spec.rb
@@ -0,0 +1,40 @@
+require_relative "../../spec_helper"
+require_relative '../../shared/basicobject/method_missing'
+
+describe "BasicObject#method_missing" do
+ it "is a private method" do
+ BasicObject.private_instance_methods(false).should.include?(:method_missing)
+ end
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_class, nil, BasicObject
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_instance, nil, BasicObject
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_defined_module, nil, KernelSpecs::ModuleMM
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_module, nil, KernelSpecs::ModuleNoMM
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_defined_class, nil, KernelSpecs::ClassMM
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_class, nil, KernelSpecs::ClassNoMM
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_defined_instance, nil, KernelSpecs::ClassMM
+end
+
+describe "BasicObject#method_missing" do
+ it_behaves_like :method_missing_instance, nil, KernelSpecs::ClassNoMM
+end
diff --git a/spec/ruby/core/basicobject/not_equal_spec.rb b/spec/ruby/core/basicobject/not_equal_spec.rb
new file mode 100644
index 0000000000..29b14b0fff
--- /dev/null
+++ b/spec/ruby/core/basicobject/not_equal_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+
+describe "BasicObject#!=" do
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:'!=')
+ end
+
+ it "returns true if other is not identical to self" do
+ a = BasicObject.new
+ b = BasicObject.new
+ (a != b).should == true
+ end
+
+ it "returns true if other is an Object" do
+ a = BasicObject.new
+ b = Object.new
+ (a != b).should == true
+ end
+
+ it "returns false if other is identical to self" do
+ a = BasicObject.new
+ (a != a).should == false
+ end
+
+ it "dispatches to #==" do
+ a = mock("not_equal")
+ b = BasicObject.new
+ a.should_receive(:==).and_return(true)
+
+ (a != b).should == false
+ end
+
+ describe "when invoked using Kernel#send" do
+ it "returns true if other is not identical to self" do
+ a = Object.new
+ b = Object.new
+ a.send(:!=, b).should == true
+ end
+
+ it "returns false if other is identical to self" do
+ a = Object.new
+ a.send(:!=, a).should == false
+ end
+
+ it "dispatches to #==" do
+ a = mock("not_equal")
+ b = Object.new
+ a.should_receive(:==).and_return(true)
+
+ a.send(:!=, b).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/basicobject/not_spec.rb b/spec/ruby/core/basicobject/not_spec.rb
new file mode 100644
index 0000000000..a6f58ae6f5
--- /dev/null
+++ b/spec/ruby/core/basicobject/not_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "BasicObject#!" do
+ it "is a public instance method" do
+ BasicObject.public_instance_methods(false).should.include?(:'!')
+ end
+
+ it "returns false" do
+ (!BasicObject.new).should == false
+ end
+end
diff --git a/spec/ruby/core/basicobject/singleton_method_added_spec.rb b/spec/ruby/core/basicobject/singleton_method_added_spec.rb
new file mode 100644
index 0000000000..f39b91768c
--- /dev/null
+++ b/spec/ruby/core/basicobject/singleton_method_added_spec.rb
@@ -0,0 +1,147 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/singleton_method'
+
+describe "BasicObject#singleton_method_added" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is a private method" do
+ BasicObject.private_instance_methods(false).should.include?(:singleton_method_added)
+ end
+
+ it "is called when a singleton method is defined on an object" do
+ obj = BasicObject.new
+
+ def obj.singleton_method_added(name)
+ ScratchPad.record [:singleton_method_added, name]
+ end
+
+ def obj.new_singleton_method
+ end
+
+ ScratchPad.recorded.should == [:singleton_method_added, :new_singleton_method]
+ end
+
+ it "is not called for instance methods" do
+ ScratchPad.record []
+
+ Module.new do
+ def self.singleton_method_added(name)
+ ScratchPad << name
+ end
+
+ def new_instance_method
+ end
+ end
+
+ ScratchPad.recorded.should_not.include?(:new_instance_method)
+ end
+
+ it "is called when a singleton method is defined on a module" do
+ class BasicObjectSpecs::SingletonMethod
+ def self.new_method_on_self
+ end
+ end
+ ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_self]
+ end
+
+ it "is called when a method is defined in the singleton class" do
+ class BasicObjectSpecs::SingletonMethod
+ class << self
+ def new_method_on_singleton
+ end
+ end
+ end
+ ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_singleton]
+ end
+
+ it "is called when a method is defined with alias_method in the singleton class" do
+ class BasicObjectSpecs::SingletonMethod
+ class << self
+ alias_method :new_method_on_singleton_with_alias_method, :singleton_method_to_alias
+ end
+ end
+ ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_singleton_with_alias_method]
+ end
+
+ it "is called when a method is defined with syntax alias in the singleton class" do
+ class BasicObjectSpecs::SingletonMethod
+ class << self
+ alias new_method_on_singleton_with_syntax_alias singleton_method_to_alias
+ end
+ end
+ ScratchPad.recorded.should == [:singleton_method_added, :new_method_on_singleton_with_syntax_alias]
+ end
+
+ it "is called when define_method is used in the singleton class" do
+ class BasicObjectSpecs::SingletonMethod
+ class << self
+ define_method :new_method_with_define_method do
+ end
+ end
+ end
+ ScratchPad.recorded.should == [:singleton_method_added, :new_method_with_define_method]
+ end
+
+ describe "when singleton_method_added is undefined" do
+ it "raises NoMethodError for a metaclass" do
+ class BasicObjectSpecs::NoSingletonMethodAdded
+ class << self
+ undef_method :singleton_method_added
+ end
+
+ -> {
+ def self.foo
+ end
+ }.should.raise(NoMethodError, /undefined method [`']singleton_method_added' for/)
+ end
+ ensure
+ BasicObjectSpecs.send(:remove_const, :NoSingletonMethodAdded)
+ end
+
+ it "raises NoMethodError for a singleton instance" do
+ object = Object.new
+ class << object
+ undef_method :singleton_method_added
+
+ -> {
+ def foo
+ end
+ }.should.raise(NoMethodError, /undefined method [`']singleton_method_added' for #<Object:/)
+
+ -> {
+ define_method(:bar) {}
+ }.should.raise(NoMethodError, /undefined method [`']singleton_method_added' for #<Object:/)
+ end
+
+ -> {
+ object.define_singleton_method(:baz) {}
+ }.should.raise(NoMethodError, /undefined method [`']singleton_method_added' for #<Object:/)
+ end
+
+ it "calls #method_missing" do
+ ScratchPad.record []
+ object = Object.new
+ class << object
+ def method_missing(*args)
+ ScratchPad << args
+ end
+
+ undef_method :singleton_method_added
+
+ def foo
+ end
+
+ define_method(:bar) {}
+ end
+ object.define_singleton_method(:baz) {}
+
+ ScratchPad.recorded.should == [
+ [:singleton_method_added, :foo],
+ [:singleton_method_added, :bar],
+ [:singleton_method_added, :baz],
+ ]
+ end
+ end
+end
diff --git a/spec/ruby/core/basicobject/singleton_method_removed_spec.rb b/spec/ruby/core/basicobject/singleton_method_removed_spec.rb
new file mode 100644
index 0000000000..1831e3c070
--- /dev/null
+++ b/spec/ruby/core/basicobject/singleton_method_removed_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "BasicObject#singleton_method_removed" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is a private method" do
+ BasicObject.private_instance_methods(false).should.include?(:singleton_method_removed)
+ end
+
+ it "is called when a method is removed on self" do
+ klass = Class.new
+ def klass.singleton_method_removed(name)
+ ScratchPad.record [:singleton_method_removed, name]
+ end
+ def klass.singleton_method_to_remove
+ end
+ class << klass
+ remove_method :singleton_method_to_remove
+ end
+ ScratchPad.recorded.should == [:singleton_method_removed, :singleton_method_to_remove]
+ end
+end
diff --git a/spec/ruby/core/basicobject/singleton_method_undefined_spec.rb b/spec/ruby/core/basicobject/singleton_method_undefined_spec.rb
new file mode 100644
index 0000000000..cc47341878
--- /dev/null
+++ b/spec/ruby/core/basicobject/singleton_method_undefined_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "BasicObject#singleton_method_undefined" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is a private method" do
+ BasicObject.private_instance_methods(false).should.include?(:singleton_method_undefined)
+ end
+
+ it "is called when a method is removed on self" do
+ klass = Class.new
+ def klass.singleton_method_undefined(name)
+ ScratchPad.record [:singleton_method_undefined, name]
+ end
+ def klass.singleton_method_to_undefine
+ end
+ class << klass
+ undef_method :singleton_method_to_undefine
+ end
+ ScratchPad.recorded.should == [:singleton_method_undefined, :singleton_method_to_undefine]
+ end
+end
diff --git a/spec/ruby/core/binding/clone_spec.rb b/spec/ruby/core/binding/clone_spec.rb
new file mode 100644
index 0000000000..f1769ac6de
--- /dev/null
+++ b/spec/ruby/core/binding/clone_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/clone'
+
+describe "Binding#clone" do
+ it_behaves_like :binding_clone, :clone
+
+ it "preserves frozen status" do
+ bind = binding.freeze
+ bind.frozen?.should == true
+ bind.clone.frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/binding/dup_spec.rb b/spec/ruby/core/binding/dup_spec.rb
new file mode 100644
index 0000000000..f5f0c72d5d
--- /dev/null
+++ b/spec/ruby/core/binding/dup_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/clone'
+
+describe "Binding#dup" do
+ it_behaves_like :binding_clone, :dup
+
+ it "resets frozen status" do
+ bind = binding.freeze
+ bind.frozen?.should == true
+ bind.dup.frozen?.should == false
+ end
+
+ it "retains original binding variables but the list is distinct" do
+ bind1 = binding
+ eval "a = 1", bind1
+
+ bind2 = bind1.dup
+ eval("a = 2", bind2)
+ eval("a", bind1).should == 2
+ eval("a", bind2).should == 2
+
+ eval("b = 2", bind2)
+ -> { eval("b", bind1) }.should.raise(NameError)
+ eval("b", bind2).should == 2
+
+ bind1.local_variables.sort.should == [:a, :bind1, :bind2]
+ bind2.local_variables.sort.should == [:a, :b, :bind1, :bind2]
+ end
+end
diff --git a/spec/ruby/core/binding/eval_spec.rb b/spec/ruby/core/binding/eval_spec.rb
new file mode 100644
index 0000000000..f1d8591320
--- /dev/null
+++ b/spec/ruby/core/binding/eval_spec.rb
@@ -0,0 +1,105 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Binding#eval" do
+ it "behaves like Kernel.eval(..., self)" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_binding
+
+ bind.eval("@secret += square(3)").should == 10
+ bind.eval("a").should == true
+
+ bind.eval("class Inside; end")
+ bind.eval("Inside.name").should == "BindingSpecs::Demo::Inside"
+ end
+
+ it "does not leak variables to cloned bindings" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_empty_binding
+ bind2 = bind.dup
+
+ bind.eval("x = 72")
+ bind.local_variables.should == [:x]
+ bind2.local_variables.should == []
+ end
+
+ it "starts with line 1 if single argument is given" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_binding
+ bind.eval("__LINE__").should == 1
+ end
+
+ it "preserves __LINE__ across multiple calls to eval" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_binding
+ bind.eval("__LINE__").should == 1
+ bind.eval("__LINE__").should == 1
+ end
+
+ it "increments __LINE__ on each line of a multiline eval" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_binding
+ bind.eval("#foo\n__LINE__").should == 2
+ end
+
+ it "starts with line 1 if the Binding is created with #send" do
+ obj = BindingSpecs::Demo.new(1)
+ bind, line = obj.get_binding_with_send_and_line
+ bind.eval("__LINE__").should == 1
+ end
+
+ it "starts with a __LINE__ of 1 if a filename is passed" do
+ bind = BindingSpecs::Demo.new(1).get_binding
+ bind.eval("__LINE__", "(test)").should == 1
+ bind.eval("#foo\n__LINE__", "(test)").should == 2
+ end
+
+ it "starts with a __LINE__ from the third argument if passed" do
+ bind = BindingSpecs::Demo.new(1).get_binding
+ bind.eval("__LINE__", "(test)", 88).should == 88
+ bind.eval("#foo\n__LINE__", "(test)", 88).should == 89
+ end
+
+ it "uses 1 as __LINE__" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_binding
+ suppress_warning { bind.eval("__LINE__") }.should == 1
+ end
+
+ it "uses the __FILE__ that is passed in" do
+ bind = BindingSpecs::Demo.new(1).get_binding
+ bind.eval("__FILE__", "(test)").should == "(test)"
+ end
+
+ describe "with a file given" do
+ it "does not store the filename permanently" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_binding
+
+ bind.eval("__FILE__", "test.rb").should == "test.rb"
+ suppress_warning {bind.eval("__FILE__")}.should_not == "test.rb"
+ end
+ end
+
+ it "with __method__ returns the method where the Binding was created" do
+ obj = BindingSpecs::Demo.new(1)
+ bind, meth = obj.get_binding_and_method
+ bind.eval("__method__").should == meth
+ end
+
+ it "with __method__ returns the method where the Binding was created, ignoring #send" do
+ obj = BindingSpecs::Demo.new(1)
+ bind, meth = obj.get_binding_with_send_and_method
+ bind.eval("__method__").should == meth
+ end
+
+ it "reflects refinements activated in the binding scope" do
+ bind = BindingSpecs::Refined.refined_binding
+
+ bind.eval("'bar'.foo").should == "foo"
+ end
+
+ it "uses the caller location as default filename" do
+ binding.eval("[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1]
+ end
+end
diff --git a/spec/ruby/core/binding/fixtures/classes.rb b/spec/ruby/core/binding/fixtures/classes.rb
new file mode 100644
index 0000000000..b5f3ce9008
--- /dev/null
+++ b/spec/ruby/core/binding/fixtures/classes.rb
@@ -0,0 +1,66 @@
+module BindingSpecs
+ class Demo
+ def initialize(n)
+ @secret = n
+ end
+
+ def square(n)
+ n * n
+ end
+
+ def get_binding_and_line
+ a = true
+ [binding, __LINE__]
+ end
+
+ def get_binding
+ get_binding_and_line[0]
+ end
+
+ def get_line_of_binding
+ get_binding_and_line[1]
+ end
+
+ def get_file_of_binding
+ __FILE__
+ end
+
+ def get_binding_with_send_and_line
+ [send(:binding), __LINE__]
+ end
+
+ def get_binding_and_method
+ [binding, :get_binding_and_method]
+ end
+
+ def get_binding_with_send_and_method
+ [send(:binding), :get_binding_with_send_and_method]
+ end
+
+ def get_empty_binding
+ binding
+ end
+
+ def get_binding_in_block
+ a = true
+ 1.times do
+ b = false
+ return binding
+ end
+ end
+ end
+
+ module AddFooToString
+ refine(String) do
+ def foo
+ "foo"
+ end
+ end
+ end
+ class Refined
+ using AddFooToString
+ def self.refined_binding
+ binding
+ end
+ end
+end
diff --git a/spec/ruby/core/binding/fixtures/location.rb b/spec/ruby/core/binding/fixtures/location.rb
new file mode 100644
index 0000000000..a78ae75731
--- /dev/null
+++ b/spec/ruby/core/binding/fixtures/location.rb
@@ -0,0 +1,6 @@
+module BindingSpecs
+ module LocationMethod
+ FILE_PATH = __FILE__
+ TEST_BINDING = binding
+ end
+end
diff --git a/spec/ruby/core/binding/local_variable_defined_spec.rb b/spec/ruby/core/binding/local_variable_defined_spec.rb
new file mode 100644
index 0000000000..2fc6504ee5
--- /dev/null
+++ b/spec/ruby/core/binding/local_variable_defined_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+
+describe 'Binding#local_variable_defined?' do
+ it 'returns false when a variable is not defined' do
+ binding.local_variable_defined?(:foo).should == false
+ end
+
+ it 'returns true when a regular local variable is defined' do
+ foo = 10
+ binding.local_variable_defined?(:foo).should == true
+ end
+
+ it 'returns true when a local variable is defined using eval()' do
+ bind = binding
+ bind.eval('foo = 10')
+
+ bind.local_variable_defined?(:foo).should == true
+ end
+
+ it 'returns true when a local variable is defined using Binding#local_variable_set' do
+ bind = binding
+ bind.local_variable_set(:foo, 10)
+
+ bind.local_variable_defined?(:foo).should == true
+ end
+
+ it 'returns true when a local variable is defined in a parent scope' do
+ foo = 10
+ -> {
+ binding.local_variable_defined?(:foo)
+ }.call.should == true
+ end
+
+ it 'allows usage of a String as the variable name' do
+ foo = 10
+ binding.local_variable_defined?('foo').should == true
+ end
+
+ it 'allows usage of an object responding to #to_str as the variable name' do
+ foo = 10
+ name = mock(:obj)
+ name.stub!(:to_str).and_return('foo')
+
+ binding.local_variable_defined?(name).should == true
+ end
+end
diff --git a/spec/ruby/core/binding/local_variable_get_spec.rb b/spec/ruby/core/binding/local_variable_get_spec.rb
new file mode 100644
index 0000000000..d97100deda
--- /dev/null
+++ b/spec/ruby/core/binding/local_variable_get_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Binding#local_variable_get" do
+ it "reads local variables captured in the binding" do
+ a = 42
+ bind = binding
+ bind.local_variable_get(:a).should == 42
+ end
+
+ it "raises a NameError for missing variables" do
+ bind = BindingSpecs::Demo.new(1).get_empty_binding
+
+ -> {
+ bind.local_variable_get(:no_such_variable)
+ }.should.raise(NameError)
+ end
+
+ it "reads variables added later to the binding" do
+ bind = BindingSpecs::Demo.new(1).get_empty_binding
+
+ -> {
+ bind.local_variable_get(:a)
+ }.should.raise(NameError)
+
+ bind.local_variable_set(:a, 42)
+
+ bind.local_variable_get(:a).should == 42
+ end
+
+ it 'gets a local variable defined in a parent scope' do
+ number = 10
+
+ -> {
+ binding.local_variable_get(:number)
+ }.call.should == 10
+ end
+
+ it 'gets a local variable defined using eval()' do
+ bind = binding
+ bind.eval('number = 10')
+
+ bind.local_variable_get(:number).should == 10
+ end
+
+ it "raises a NameError on global access" do
+ bind = binding
+ -> { bind.local_variable_get(:$0) }.should.raise(NameError)
+ end
+
+ it "raises a NameError on special variable access" do
+ bind = binding
+ -> { bind.local_variable_get(:$~) }.should.raise(NameError)
+ -> { bind.local_variable_get(:$_) }.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/binding/local_variable_set_spec.rb b/spec/ruby/core/binding/local_variable_set_spec.rb
new file mode 100644
index 0000000000..3e4f407fc3
--- /dev/null
+++ b/spec/ruby/core/binding/local_variable_set_spec.rb
@@ -0,0 +1,71 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Binding#local_variable_set" do
+ it "adds nonexistent variables to the binding's eval scope" do
+ obj = BindingSpecs::Demo.new(1)
+ bind = obj.get_empty_binding
+ bind.eval('local_variables').should == []
+ bind.local_variable_set :foo, 1
+ bind.eval('local_variables').should == [:foo]
+ bind.eval('foo').should == 1
+ end
+
+ it 'sets a new local variable' do
+ bind = binding
+
+ bind.local_variable_set(:number, 10)
+ bind.local_variable_get(:number).should == 10
+ end
+
+ it 'sets a local variable using a String as the variable name' do
+ bind = binding
+
+ bind.local_variable_set('number', 10)
+ bind.local_variable_get('number').should == 10
+ end
+
+ it 'sets a local variable using an object responding to #to_str as the variable name' do
+ bind = binding
+ name = mock(:obj)
+ name.stub!(:to_str).and_return('number')
+
+ bind.local_variable_set(name, 10)
+ bind.local_variable_get(name).should == 10
+ end
+
+ it 'scopes new local variables to the receiving Binding' do
+ bind = binding
+ bind.local_variable_set(:number, 10)
+
+ -> { number }.should.raise(NameError)
+ end
+
+ it 'overwrites an existing local variable defined before a Binding' do
+ number = 10
+ bind = binding
+
+ bind.local_variable_set(:number, 20)
+ number.should == 20
+ end
+
+ it 'overwrites a local variable defined using eval()' do
+ bind = binding
+ bind.eval('number = 10')
+
+ bind.local_variable_set(:number, 20)
+ bind.local_variable_get(:number).should == 20
+ end
+
+ it "raises a NameError on global access" do
+ bind = binding
+ -> { bind.local_variable_set(:$0, "") }.should.raise(NameError)
+ end
+
+ it "raises a NameError on special variable access" do
+ bind = binding
+ -> { bind.local_variable_set(:$~, "") }.should.raise(NameError)
+ -> { bind.local_variable_set(:$_, "") }.should.raise(NameError)
+ end
+
+end
diff --git a/spec/ruby/core/binding/local_variables_spec.rb b/spec/ruby/core/binding/local_variables_spec.rb
new file mode 100644
index 0000000000..0f59681342
--- /dev/null
+++ b/spec/ruby/core/binding/local_variables_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "Binding#local_variables" do
+ it "returns an Array" do
+ binding.local_variables.should.is_a?(Array)
+ end
+
+ it "includes local variables in the current scope" do
+ a = 1
+ b = nil
+ binding.local_variables.should == [:a, :b]
+ end
+
+ it "includes local variables defined after calling binding.local_variables" do
+ binding.local_variables.should == [:a, :b]
+ a = 1
+ b = 2
+ end
+
+ it "includes local variables of inherited scopes and eval'ed context" do
+ p = proc { |a| b = 1; eval("c = 2; binding.local_variables") }
+ p.call.should == [:c, :a, :b, :p]
+ end
+
+ it "includes shadowed local variables only once" do
+ a = 1
+ proc { |a| binding.local_variables }.call(2).should == [:a]
+ end
+
+ it "includes new variables defined in the binding" do
+ b = binding
+ b.local_variable_set :a, 42
+ b.local_variables.should == [:a, :b]
+ end
+end
diff --git a/spec/ruby/core/binding/receiver_spec.rb b/spec/ruby/core/binding/receiver_spec.rb
new file mode 100644
index 0000000000..4bf5e7a7bd
--- /dev/null
+++ b/spec/ruby/core/binding/receiver_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Binding#receiver" do
+ it "returns the object to which binding is bound" do
+ obj = BindingSpecs::Demo.new(1)
+ obj.get_binding.receiver.should == obj
+
+ binding.receiver.should == self
+ end
+end
diff --git a/spec/ruby/core/binding/shared/clone.rb b/spec/ruby/core/binding/shared/clone.rb
new file mode 100644
index 0000000000..2d854fce96
--- /dev/null
+++ b/spec/ruby/core/binding/shared/clone.rb
@@ -0,0 +1,56 @@
+describe :binding_clone, shared: true do
+ before :each do
+ @b1 = BindingSpecs::Demo.new(99).get_binding
+ @b2 = @b1.send(@method)
+ @b3 = BindingSpecs::Demo.new(99).get_binding_in_block
+ @b4 = @b3.send(@method)
+ end
+
+ it "returns a copy of the Binding object" do
+ [[@b1, @b2, "a"],
+ [@b3, @b4, "a", "b"]].each do |b1, b2, *vars|
+ b1.should_not == b2
+
+ eval("@secret", b1).should == eval("@secret", b2)
+ eval("square(2)", b1).should == eval("square(2)", b2)
+ eval("self.square(2)", b1).should == eval("self.square(2)", b2)
+ vars.each do |v|
+ eval("#{v}", b1).should == eval("#{v}", b2)
+ end
+ end
+ end
+
+ it "is a shallow copy of the Binding object" do
+ [[@b1, @b2, "a"],
+ [@b3, @b4, "a", "b"]].each do |b1, b2, *vars|
+ vars.each do |v|
+ eval("#{v} = false", b1)
+ eval("#{v}", b2).should == false
+ end
+ b1.local_variable_set(:x, 37)
+ b2.local_variable_defined?(:x).should == false
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "copies instance variables" do
+ @b1.instance_variable_set(:@ivar, 1)
+ cl = @b1.send(@method)
+ cl.instance_variables.should == [:@ivar]
+ end
+
+ it "copies the finalizer" do
+ code = <<-'RUBY'
+ obj = binding
+
+ ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" })
+
+ obj.clone
+
+ exit 0
+ RUBY
+
+ ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"]
+ end
+ end
+end
diff --git a/spec/ruby/core/binding/source_location_spec.rb b/spec/ruby/core/binding/source_location_spec.rb
new file mode 100644
index 0000000000..d1c8191ea8
--- /dev/null
+++ b/spec/ruby/core/binding/source_location_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/location'
+
+describe "Binding#source_location" do
+ it "returns an [file, line] pair" do
+ b = BindingSpecs::LocationMethod::TEST_BINDING
+ b.source_location.should == [BindingSpecs::LocationMethod::FILE_PATH, 4]
+ end
+
+ it "works for eval with a given line" do
+ b = eval('binding', nil, "foo", 100)
+ b.source_location.should == ["foo", 100]
+ end
+end
diff --git a/spec/ruby/core/builtin_constants/builtin_constants_spec.rb b/spec/ruby/core/builtin_constants/builtin_constants_spec.rb
new file mode 100644
index 0000000000..67b3339aa6
--- /dev/null
+++ b/spec/ruby/core/builtin_constants/builtin_constants_spec.rb
@@ -0,0 +1,149 @@
+require_relative '../../spec_helper'
+
+describe "RUBY_VERSION" do
+ it "is a String" do
+ RUBY_VERSION.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_VERSION.should.frozen?
+ end
+end
+
+describe "RUBY_PATCHLEVEL" do
+ it "is an Integer" do
+ RUBY_PATCHLEVEL.should.is_a?(Integer)
+ end
+end
+
+describe "RUBY_COPYRIGHT" do
+ it "is a String" do
+ RUBY_COPYRIGHT.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_COPYRIGHT.should.frozen?
+ end
+end
+
+describe "RUBY_DESCRIPTION" do
+ it "is a String" do
+ RUBY_DESCRIPTION.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_DESCRIPTION.should.frozen?
+ end
+end
+
+describe "RUBY_ENGINE" do
+ it "is a String" do
+ RUBY_ENGINE.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_ENGINE.should.frozen?
+ end
+end
+
+describe "RUBY_ENGINE_VERSION" do
+ it "is a String" do
+ RUBY_ENGINE_VERSION.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_ENGINE_VERSION.should.frozen?
+ end
+end
+
+describe "RUBY_PLATFORM" do
+ it "is a String" do
+ RUBY_PLATFORM.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_PLATFORM.should.frozen?
+ end
+end
+
+describe "RUBY_RELEASE_DATE" do
+ it "is a String" do
+ RUBY_RELEASE_DATE.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_RELEASE_DATE.should.frozen?
+ end
+end
+
+describe "RUBY_REVISION" do
+ it "is a String" do
+ RUBY_REVISION.should.is_a?(String)
+ end
+
+ it "is frozen" do
+ RUBY_REVISION.should.frozen?
+ end
+end
+
+ruby_version_is "4.0" do
+ describe "Ruby" do
+ it "is a Module" do
+ Ruby.should.instance_of?(Module)
+ end
+ end
+
+ describe "Ruby::VERSION" do
+ it "is equal to RUBY_VERSION" do
+ Ruby::VERSION.should.equal?(RUBY_VERSION)
+ end
+ end
+
+ describe "RUBY::PATCHLEVEL" do
+ it "is equal to RUBY_PATCHLEVEL" do
+ Ruby::PATCHLEVEL.should.equal?(RUBY_PATCHLEVEL)
+ end
+ end
+
+ describe "Ruby::COPYRIGHT" do
+ it "is equal to RUBY_COPYRIGHT" do
+ Ruby::COPYRIGHT.should.equal?(RUBY_COPYRIGHT)
+ end
+ end
+
+ describe "Ruby::DESCRIPTION" do
+ it "is equal to RUBY_DESCRIPTION" do
+ Ruby::DESCRIPTION.should.equal?(RUBY_DESCRIPTION)
+ end
+ end
+
+ describe "Ruby::ENGINE" do
+ it "is equal to RUBY_ENGINE" do
+ Ruby::ENGINE.should.equal?(RUBY_ENGINE)
+ end
+ end
+
+ describe "Ruby::ENGINE_VERSION" do
+ it "is equal to RUBY_ENGINE_VERSION" do
+ Ruby::ENGINE_VERSION.should.equal?(RUBY_ENGINE_VERSION)
+ end
+ end
+
+ describe "Ruby::PLATFORM" do
+ it "is equal to RUBY_PLATFORM" do
+ Ruby::PLATFORM.should.equal?(RUBY_PLATFORM)
+ end
+ end
+
+ describe "Ruby::RELEASE_DATE" do
+ it "is equal to RUBY_RELEASE_DATE" do
+ Ruby::RELEASE_DATE.should.equal?(RUBY_RELEASE_DATE)
+ end
+ end
+
+ describe "Ruby::REVISION" do
+ it "is equal to RUBY_REVISION" do
+ Ruby::REVISION.should.equal?(RUBY_REVISION)
+ end
+ end
+end
diff --git a/spec/ruby/core/class/allocate_spec.rb b/spec/ruby/core/class/allocate_spec.rb
new file mode 100644
index 0000000000..b8950a678e
--- /dev/null
+++ b/spec/ruby/core/class/allocate_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Class#allocate" do
+ it "returns an instance of self" do
+ klass = Class.new
+ klass.allocate.should.instance_of?(klass)
+ end
+
+ it "returns a fully-formed instance of Module" do
+ klass = Class.allocate
+ klass.constants.should_not == nil
+ klass.methods.should_not == nil
+ end
+
+ it "throws an exception when calling a method on a new instance" do
+ klass = Class.allocate
+ -> do
+ klass.new
+ end.should.raise(Exception)
+ end
+
+ it "does not call initialize on the new instance" do
+ klass = Class.new do
+ def initialize(*args)
+ @initialized = true
+ end
+
+ def initialized?
+ @initialized || false
+ end
+ end
+
+ klass.allocate.should_not.initialized?
+ end
+
+ it "raises TypeError for #superclass" do
+ -> do
+ Class.allocate.superclass
+ end.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/class/attached_object_spec.rb b/spec/ruby/core/class/attached_object_spec.rb
new file mode 100644
index 0000000000..b75e61ca07
--- /dev/null
+++ b/spec/ruby/core/class/attached_object_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Class#attached_object" do
+ it "returns the object that is attached to a singleton class" do
+ a = Class.new
+
+ a_obj = a.new
+ a_obj.singleton_class.attached_object.should == a_obj
+ end
+
+ it "returns the class object that is attached to a class's singleton class" do
+ a = Class.new
+ singleton_class = (class << a; self; end)
+
+ singleton_class.attached_object.should == a
+ end
+
+ it "raises TypeError if the class is not a singleton class" do
+ a = Class.new
+
+ -> { a.attached_object }.should.raise(TypeError, /is not a singleton class/)
+ end
+
+ it "raises TypeError for special singleton classes" do
+ -> { nil.singleton_class.attached_object }.should.raise(TypeError, /[`']NilClass' is not a singleton class/)
+ -> { true.singleton_class.attached_object }.should.raise(TypeError, /[`']TrueClass' is not a singleton class/)
+ -> { false.singleton_class.attached_object }.should.raise(TypeError, /[`']FalseClass' is not a singleton class/)
+ end
+end
diff --git a/spec/ruby/core/class/dup_spec.rb b/spec/ruby/core/class/dup_spec.rb
new file mode 100644
index 0000000000..17c0171ceb
--- /dev/null
+++ b/spec/ruby/core/class/dup_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# NOTE: This is actually implemented by Module#initialize_copy
+describe "Class#dup" do
+ it "duplicates both the class and the singleton class" do
+ klass = Class.new do
+ def hello
+ "hello"
+ end
+
+ def self.message
+ "text"
+ end
+ end
+
+ klass_dup = klass.dup
+
+ klass_dup.new.hello.should == "hello"
+ klass_dup.message.should == "text"
+ end
+
+ it "retains an included module in the ancestor chain for the singleton class" do
+ klass = Class.new
+ mod = Module.new do
+ def hello
+ "hello"
+ end
+ end
+
+ klass.extend(mod)
+ klass_dup = klass.dup
+ klass_dup.hello.should == "hello"
+ end
+
+ it "retains the correct ancestor chain for the singleton class" do
+ super_klass = Class.new do
+ def hello
+ "hello"
+ end
+
+ def self.message
+ "text"
+ end
+ end
+
+ klass = Class.new(super_klass)
+ klass_dup = klass.dup
+
+ klass_dup.new.hello.should == "hello"
+ klass_dup.message.should == "text"
+ end
+
+ it "sets the name from the class to nil if not assigned to a constant" do
+ copy = CoreClassSpecs::Record.dup
+ copy.name.should == nil
+ end
+
+ it "stores the new name if assigned to a constant" do
+ CoreClassSpecs::RecordCopy = CoreClassSpecs::Record.dup
+ CoreClassSpecs::RecordCopy.name.should == "CoreClassSpecs::RecordCopy"
+ ensure
+ CoreClassSpecs.send(:remove_const, :RecordCopy)
+ end
+
+ it "raises TypeError if called on BasicObject" do
+ -> { BasicObject.dup }.should.raise(TypeError, "can't copy the root class")
+ end
+end
diff --git a/spec/ruby/core/class/fixtures/classes.rb b/spec/ruby/core/class/fixtures/classes.rb
new file mode 100644
index 0000000000..f96db90795
--- /dev/null
+++ b/spec/ruby/core/class/fixtures/classes.rb
@@ -0,0 +1,47 @@
+module CoreClassSpecs
+ class Record
+ end
+
+ module M
+ def inherited(klass)
+ ScratchPad.record klass
+ super
+ end
+ end
+
+ class F; end
+ class << F
+ include M
+ end
+
+ class A
+ def self.inherited(klass)
+ ScratchPad.record klass
+ end
+ end
+
+ class H < A
+ def self.inherited(klass)
+ super
+ end
+ end
+
+ module Inherited
+ class A
+ SUBCLASSES = []
+ def self.inherited(subclass)
+ SUBCLASSES << [self, subclass]
+ end
+ end
+
+ class B < A; end
+ class B < A; end # reopen
+ class C < B; end
+
+ class D
+ def self.inherited(subclass)
+ ScratchPad << self
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/class/inherited_spec.rb b/spec/ruby/core/class/inherited_spec.rb
new file mode 100644
index 0000000000..c9acc740a1
--- /dev/null
+++ b/spec/ruby/core/class/inherited_spec.rb
@@ -0,0 +1,118 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Class.inherited" do
+
+ before :each do
+ ScratchPad.record nil
+ end
+
+ it "is invoked with the child Class when self is subclassed" do
+ begin
+ top = Class.new do
+ def self.inherited(cls)
+ $child_class = cls
+ end
+ end
+
+ child = Class.new(top)
+ $child_class.should == child
+
+ other_child = Class.new(top)
+ $child_class.should == other_child
+ ensure
+ $child_class = nil
+ end
+ end
+
+ it "is invoked only once per subclass" do
+ expected = [
+ [CoreClassSpecs::Inherited::A, CoreClassSpecs::Inherited::B],
+ [CoreClassSpecs::Inherited::B, CoreClassSpecs::Inherited::C],
+ ]
+
+ CoreClassSpecs::Inherited::A::SUBCLASSES.should == expected
+ end
+
+ it "is called when marked as a private class method" do
+ a = Class.new do
+ def self.inherited(klass)
+ ScratchPad.record klass
+ end
+ end
+ a.private_class_method :inherited
+ ScratchPad.recorded.should == nil
+ b = Class.new(a)
+ ScratchPad.recorded.should == b
+ end
+
+ it "is called when marked as a protected class method" do
+ a = Class.new
+ class << a
+ def inherited(klass)
+ ScratchPad.record klass
+ end
+ protected :inherited
+ end
+ ScratchPad.recorded.should == nil
+ b = Class.new(a)
+ ScratchPad.recorded.should == b
+ end
+
+ it "is called when marked as a public class method" do
+ a = Class.new do
+ def self.inherited(klass)
+ ScratchPad.record klass
+ end
+ end
+ a.public_class_method :inherited
+ ScratchPad.recorded.should == nil
+ b = Class.new(a)
+ ScratchPad.recorded.should == b
+ end
+
+ it "is called by super from a method provided by an included module" do
+ ScratchPad.recorded.should == nil
+ e = Class.new(CoreClassSpecs::F)
+ ScratchPad.recorded.should == e
+ end
+
+ it "is called by super even when marked as a private class method" do
+ ScratchPad.recorded.should == nil
+ CoreClassSpecs::H.private_class_method :inherited
+ i = Class.new(CoreClassSpecs::H)
+ ScratchPad.recorded.should == i
+ end
+
+ it "will be invoked by child class regardless of visibility" do
+ top = Class.new do
+ class << self
+ def inherited(cls); end
+ end
+ end
+
+ class << top; private :inherited; end
+ -> { Class.new(top) }.should_not.raise
+
+ class << top; protected :inherited; end
+ -> { Class.new(top) }.should_not.raise
+ end
+
+ it "if the subclass is assigned to a constant, it is all set" do
+ ScratchPad.record []
+
+ parent = Class.new do
+ def self.inherited(subclass)
+ ScratchPad << defined?(self::C)
+ ScratchPad << const_defined?(:C)
+ ScratchPad << constants
+ ScratchPad << const_get(:C)
+ ScratchPad << subclass.name.match?(/\A#<Class:0x\w+>::C\z/)
+ end
+ end
+
+ class parent::C < parent; end
+
+ ScratchPad.recorded.should == ["constant", true, [:C], parent::C, true]
+ end
+end
diff --git a/spec/ruby/core/class/initialize_spec.rb b/spec/ruby/core/class/initialize_spec.rb
new file mode 100644
index 0000000000..ab8f0a157e
--- /dev/null
+++ b/spec/ruby/core/class/initialize_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+
+describe "Class#initialize" do
+ it "is private" do
+ Class.private_methods(false).should.include?(:initialize)
+ end
+
+ it "raises a TypeError when called on already initialized classes" do
+ ->{
+ Integer.send :initialize
+ }.should.raise(TypeError)
+
+ ->{
+ Object.send :initialize
+ }.should.raise(TypeError)
+ end
+
+ # See [redmine:2601]
+ it "raises a TypeError when called on BasicObject" do
+ ->{
+ BasicObject.send :initialize
+ }.should.raise(TypeError)
+ end
+
+ describe "when given the Class" do
+ before :each do
+ @uninitialized = Class.allocate
+ end
+
+ it "raises a TypeError" do
+ ->{@uninitialized.send(:initialize, Class)}.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/class/new_spec.rb b/spec/ruby/core/class/new_spec.rb
new file mode 100644
index 0000000000..111e31252e
--- /dev/null
+++ b/spec/ruby/core/class/new_spec.rb
@@ -0,0 +1,157 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Class.new with a block given" do
+ it "yields the new class as self in the block" do
+ self_in_block = nil
+ klass = Class.new do
+ self_in_block = self
+ end
+ self_in_block.should.equal? klass
+ end
+
+ it "uses the given block as the class' body" do
+ klass = Class.new do
+ def self.message
+ "text"
+ end
+
+ def hello
+ "hello again"
+ end
+ end
+
+ klass.message.should == "text"
+ klass.new.hello.should == "hello again"
+ end
+
+ it "creates a subclass of the given superclass" do
+ sc = Class.new do
+ def self.body
+ @body
+ end
+ @body = self
+ def message; "text"; end
+ end
+ klass = Class.new(sc) do
+ def self.body
+ @body
+ end
+ @body = self
+ def message2; "hello"; end
+ end
+
+ klass.body.should == klass
+ sc.body.should == sc
+ klass.superclass.should == sc
+ klass.new.message.should == "text"
+ klass.new.message2.should == "hello"
+ end
+
+ it "runs the inherited hook after yielding the block" do
+ ScratchPad.record []
+ klass = Class.new(CoreClassSpecs::Inherited::D) do
+ ScratchPad << self
+ end
+
+ ScratchPad.recorded.should == [CoreClassSpecs::Inherited::D, klass]
+ end
+end
+
+describe "Class.new" do
+ it "creates a new anonymous class" do
+ klass = Class.new
+ klass.is_a?(Class).should == true
+
+ klass_instance = klass.new
+ klass_instance.is_a?(klass).should == true
+ end
+
+ it "raises a TypeError if passed a metaclass" do
+ obj = mock("Class.new metaclass")
+ meta = obj.singleton_class
+ -> { Class.new meta }.should.raise(TypeError)
+ end
+
+ it "creates a class without a name" do
+ Class.new.name.should == nil
+ end
+
+ it "creates a class that can be given a name by assigning it to a constant" do
+ ::MyClass = Class.new
+ ::MyClass.name.should == "MyClass"
+ a = Class.new
+ MyClass::NestedClass = a
+ MyClass::NestedClass.name.should == "MyClass::NestedClass"
+ ensure
+ Object.send(:remove_const, :MyClass)
+ end
+
+ it "sets the new class' superclass to the given class" do
+ top = Class.new
+ Class.new(top).superclass.should == top
+ end
+
+ it "sets the new class' superclass to Object when no class given" do
+ Class.new.superclass.should == Object
+ end
+
+ it "raises a TypeError when given a non-Class" do
+ error_msg = /superclass must be a.*Class/
+ -> { Class.new("") }.should.raise(TypeError, error_msg)
+ -> { Class.new(1) }.should.raise(TypeError, error_msg)
+ -> { Class.new(:symbol) }.should.raise(TypeError, error_msg)
+ -> { Class.new(mock('o')) }.should.raise(TypeError, error_msg)
+ -> { Class.new(Module.new) }.should.raise(TypeError, error_msg)
+ -> { Class.new(BasicObject.new) }.should.raise(TypeError, error_msg)
+ end
+end
+
+describe "Class#new" do
+ it "returns a new instance of self" do
+ klass = Class.new
+ klass.new.is_a?(klass).should == true
+ end
+
+ it "invokes #initialize on the new instance with the given args" do
+ klass = Class.new do
+ def initialize(*args)
+ @initialized = true
+ @args = args
+ end
+
+ def args
+ @args
+ end
+
+ def initialized?
+ @initialized || false
+ end
+ end
+
+ klass.new.should.initialized?
+ klass.new(1, 2, 3).args.should == [1, 2, 3]
+ end
+
+ it "uses the internal allocator and does not call #allocate" do
+ klass = Class.new do
+ def self.allocate
+ raise "allocate should not be called"
+ end
+ end
+
+ instance = klass.new
+ instance.should.is_a? klass
+ instance.class.should.equal? klass
+ end
+
+ it "passes the block to #initialize" do
+ klass = Class.new do
+ def initialize
+ yield
+ end
+ end
+
+ klass.new { break 42 }.should == 42
+ end
+end
diff --git a/spec/ruby/core/class/subclasses_spec.rb b/spec/ruby/core/class/subclasses_spec.rb
new file mode 100644
index 0000000000..c3d7b07e12
--- /dev/null
+++ b/spec/ruby/core/class/subclasses_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+require_relative '../module/fixtures/classes'
+
+describe "Class#subclasses" do
+ it "returns a list of classes directly inheriting from self" do
+ assert_subclasses(ModuleSpecs::Parent, [ModuleSpecs::Child, ModuleSpecs::Child2])
+ end
+
+ it "does not return included modules from the parent" do
+ parent = Class.new
+ child = Class.new(parent)
+ mod = Module.new
+ parent.include(mod)
+
+ assert_subclasses(parent, [child])
+ end
+
+ it "does not return included modules from the child" do
+ parent = Class.new
+ child = Class.new(parent)
+ mod = Module.new
+ parent.include(mod)
+
+ assert_subclasses(parent, [child])
+ end
+
+ it "does not return prepended modules from the parent" do
+ parent = Class.new
+ child = Class.new(parent)
+ mod = Module.new
+ parent.prepend(mod)
+
+ assert_subclasses(parent, [child])
+ end
+
+ it "does not return prepended modules from the child" do
+ parent = Class.new
+ child = Class.new(parent)
+ mod = Module.new
+ child.prepend(mod)
+
+ assert_subclasses(parent, [child])
+ end
+
+ it "does not return singleton classes" do
+ a = Class.new
+
+ a_obj = a.new
+ def a_obj.force_singleton_class
+ 42
+ end
+
+ a.subclasses.should_not.include?(a_obj.singleton_class)
+ end
+
+ it "has 1 entry per module or class" do
+ ModuleSpecs::Parent.subclasses.should == ModuleSpecs::Parent.subclasses.uniq
+ end
+
+ it "works when creating subclasses concurrently" do
+ t = 16
+ n = 1000
+ go = false
+ superclass = Class.new
+
+ threads = t.times.map do
+ Thread.new do
+ Thread.pass until go
+ n.times.map do
+ Class.new(superclass)
+ end
+ end
+ end
+
+ go = true
+ classes = threads.map(&:value)
+
+ superclass.subclasses.size.should == t * n
+ superclass.subclasses.each { |c| c.should.is_a?(Class) }
+ end
+
+ def assert_subclasses(mod,subclasses)
+ mod.subclasses.sort_by(&:inspect).should == subclasses.sort_by(&:inspect)
+ end
+end
diff --git a/spec/ruby/core/class/superclass_spec.rb b/spec/ruby/core/class/superclass_spec.rb
new file mode 100644
index 0000000000..87d9b20490
--- /dev/null
+++ b/spec/ruby/core/class/superclass_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Class#superclass" do
+ it "returns the superclass of self" do
+ BasicObject.superclass.should == nil
+ Object.superclass.should == BasicObject
+ Class.superclass.should == Module
+ Class.new.superclass.should == Object
+ Class.new(String).superclass.should == String
+ Class.new(Integer).superclass.should == Integer
+ end
+
+ # redmine:567
+ describe "for a singleton class" do
+ it "of an object returns the class of the object" do
+ a = CoreClassSpecs::A.new
+ sc = class << a; self; end
+ sc.superclass.should == CoreClassSpecs::A
+ end
+
+ it "of a class returns the singleton class of its superclass" do # sorry, can't find a simpler way to express this...
+ sc = class << CoreClassSpecs::H; self; end
+ sc.superclass.should == class << CoreClassSpecs::A; self; end
+ end
+ end
+end
diff --git a/spec/ruby/core/comparable/between_spec.rb b/spec/ruby/core/comparable/between_spec.rb
new file mode 100644
index 0000000000..fd79bb9b4c
--- /dev/null
+++ b/spec/ruby/core/comparable/between_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Comparable#between?" do
+ it "returns true if self is greater than or equal to the first and less than or equal to the second argument" do
+ a = ComparableSpecs::Weird.new(-1)
+ b = ComparableSpecs::Weird.new(0)
+ c = ComparableSpecs::Weird.new(1)
+ d = ComparableSpecs::Weird.new(2)
+
+ a.between?(a, a).should == true
+ a.between?(a, b).should == true
+ a.between?(a, c).should == true
+ a.between?(a, d).should == true
+ c.between?(c, d).should == true
+ d.between?(d, d).should == true
+ c.between?(a, d).should == true
+
+ a.between?(b, b).should == false
+ a.between?(b, c).should == false
+ a.between?(b, d).should == false
+ c.between?(a, a).should == false
+ c.between?(a, b).should == false
+ end
+end
diff --git a/spec/ruby/core/comparable/clamp_spec.rb b/spec/ruby/core/comparable/clamp_spec.rb
new file mode 100644
index 0000000000..eb1dc1ff98
--- /dev/null
+++ b/spec/ruby/core/comparable/clamp_spec.rb
@@ -0,0 +1,223 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Comparable#clamp' do
+ it 'raises an Argument error unless the 2 parameters are correctly ordered' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(3)
+
+ -> { c.clamp(two, one) }.should.raise(ArgumentError)
+ one.should_receive(:<=>).any_number_of_times.and_return(nil)
+ -> { c.clamp(one, two) }.should.raise(ArgumentError)
+ end
+
+ it 'returns self if within the given parameters' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ three = ComparableSpecs::WithOnlyCompareDefined.new(3)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(one, two).should.equal?(c)
+ c.clamp(two, two).should.equal?(c)
+ c.clamp(one, three).should.equal?(c)
+ c.clamp(two, three).should.equal?(c)
+ end
+
+ it 'returns the min parameter if less than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(0)
+
+ c.clamp(one, two).should.equal?(one)
+ end
+
+ it 'returns the max parameter if greater than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(3)
+
+ c.clamp(one, two).should.equal?(two)
+ end
+
+ context 'max is nil' do
+ it 'returns min if less than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(0)
+ c.clamp(one, nil).should.equal?(one)
+ end
+
+ it 'always returns self if greater than min' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(2)
+ c.clamp(one, nil).should.equal?(c)
+ end
+ end
+
+ context 'min is nil' do
+ it 'returns max if greater than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(2)
+ c.clamp(nil, one).should.equal?(one)
+ end
+
+ it 'always returns self if less than max' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(0)
+ c.clamp(nil, one).should.equal?(c)
+ end
+ end
+
+ it 'always returns self when min is nil and max is nil' do
+ c = ComparableSpecs::Weird.new(1)
+ c.clamp(nil, nil).should.equal?(c)
+ end
+
+ it 'returns self if within the given range parameters' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ three = ComparableSpecs::WithOnlyCompareDefined.new(3)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(one..two).should.equal?(c)
+ c.clamp(two..two).should.equal?(c)
+ c.clamp(one..three).should.equal?(c)
+ c.clamp(two..three).should.equal?(c)
+ end
+
+ it 'returns the minimum value of the range parameters if less than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(0)
+
+ c.clamp(one..two).should.equal?(one)
+ end
+
+ it 'returns the maximum value of the range parameters if greater than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(3)
+
+ c.clamp(one..two).should.equal?(two)
+ end
+
+ it 'raises an Argument error if the range parameter is exclusive' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(3)
+
+ -> { c.clamp(one...two) }.should.raise(ArgumentError)
+ end
+
+ context 'with nil as the max argument' do
+ it 'returns min argument if less than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ zero = ComparableSpecs::WithOnlyCompareDefined.new(0)
+ c = ComparableSpecs::Weird.new(0)
+
+ c.clamp(one, nil).should.equal?(one)
+ c.clamp(zero, nil).should.equal?(c)
+ end
+
+ it 'always returns self if greater than min argument' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(one, nil).should.equal?(c)
+ c.clamp(two, nil).should.equal?(c)
+ end
+ end
+
+ context 'with endless range' do
+ it 'returns minimum value of the range parameters if less than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ zero = ComparableSpecs::WithOnlyCompareDefined.new(0)
+ c = ComparableSpecs::Weird.new(0)
+
+ c.clamp(one..).should.equal?(one)
+ c.clamp(zero..).should.equal?(c)
+ end
+
+ it 'always returns self if greater than minimum value of the range parameters' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ two = ComparableSpecs::WithOnlyCompareDefined.new(2)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(one..).should.equal?(c)
+ c.clamp(two..).should.equal?(c)
+ end
+
+ it 'works with exclusive range' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(one...).should.equal?(c)
+ end
+ end
+
+ context 'with nil as the min argument' do
+ it 'returns max argument if greater than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(nil, one).should.equal?(one)
+ end
+
+ it 'always returns self if less than max argument' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ zero = ComparableSpecs::WithOnlyCompareDefined.new(0)
+ c = ComparableSpecs::Weird.new(0)
+
+ c.clamp(nil, one).should.equal?(c)
+ c.clamp(nil, zero).should.equal?(c)
+ end
+ end
+
+ context 'with beginless range' do
+ it 'returns maximum value of the range parameters if greater than it' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(..one).should.equal?(one)
+ end
+
+ it 'always returns self if less than maximum value of the range parameters' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ zero = ComparableSpecs::WithOnlyCompareDefined.new(0)
+ c = ComparableSpecs::Weird.new(0)
+
+ c.clamp(..one).should.equal?(c)
+ c.clamp(..zero).should.equal?(c)
+ end
+
+ it 'raises an Argument error if the range parameter is exclusive' do
+ one = ComparableSpecs::WithOnlyCompareDefined.new(1)
+ c = ComparableSpecs::Weird.new(0)
+
+ -> { c.clamp(...one) }.should.raise(ArgumentError)
+ end
+ end
+
+ context 'with nil as the min and the max argument' do
+ it 'always returns self' do
+ c = ComparableSpecs::Weird.new(1)
+
+ c.clamp(nil, nil).should.equal?(c)
+ end
+ end
+
+ context 'with beginless-and-endless range' do
+ it 'always returns self' do
+ c = ComparableSpecs::Weird.new(1)
+
+ c.clamp(nil..nil).should.equal?(c)
+ end
+
+ it 'works with exclusive range' do
+ c = ComparableSpecs::Weird.new(2)
+
+ c.clamp(nil...nil).should.equal?(c)
+ end
+ end
+end
diff --git a/spec/ruby/core/comparable/equal_value_spec.rb b/spec/ruby/core/comparable/equal_value_spec.rb
new file mode 100644
index 0000000000..3af40d1c7e
--- /dev/null
+++ b/spec/ruby/core/comparable/equal_value_spec.rb
@@ -0,0 +1,114 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Comparable#==" do
+ a = b = nil
+ before :each do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(10)
+ end
+
+ it "returns true if other is the same as self" do
+ (a == a).should == true
+ (b == b).should == true
+ end
+
+ it "calls #<=> on self with other and returns true if #<=> returns 0" do
+ a.should_receive(:<=>).once.and_return(0)
+ (a == b).should == true
+ end
+
+ it "calls #<=> on self with other and returns true if #<=> returns 0.0" do
+ a.should_receive(:<=>).once.and_return(0.0)
+ (a == b).should == true
+ end
+
+ it "returns false if calling #<=> on self returns a positive Integer" do
+ a.should_receive(:<=>).once.and_return(1)
+ (a == b).should == false
+ end
+
+ it "returns false if calling #<=> on self returns a negative Integer" do
+ a.should_receive(:<=>).once.and_return(-1)
+ (a == b).should == false
+ end
+
+ context "when #<=> returns nil" do
+ before :each do
+ a.should_receive(:<=>).once.and_return(nil)
+ end
+
+ it "returns false" do
+ (a == b).should == false
+ end
+ end
+
+ context "when #<=> returns nor nil neither an Integer" do
+ before :each do
+ a.should_receive(:<=>).once.and_return("abc")
+ end
+
+ it "raises an ArgumentError" do
+ -> { (a == b) }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when #<=> raises an exception" do
+ context "if it is a StandardError" do
+ before :each do
+ a.should_receive(:<=>).once.and_raise(StandardError)
+ end
+
+ it "lets it go through" do
+ -> { (a == b) }.should.raise(StandardError)
+ end
+ end
+
+ context "if it is a subclass of StandardError" do
+ # TypeError < StandardError
+ before :each do
+ a.should_receive(:<=>).once.and_raise(TypeError)
+ end
+
+ it "lets it go through" do
+ -> { (a == b) }.should.raise(TypeError)
+ end
+ end
+
+ it "lets it go through if it is not a StandardError" do
+ a.should_receive(:<=>).once.and_raise(Exception)
+ -> { (a == b) }.should.raise(Exception)
+ end
+ end
+
+ context "when #<=> is not defined" do
+ before :each do
+ @a = ComparableSpecs::WithoutCompareDefined.new
+ @b = ComparableSpecs::WithoutCompareDefined.new
+ end
+
+ it "returns true for identical objects" do
+ @a.should == @a
+ end
+
+ it "returns false and does not recurse infinitely" do
+ @a.should_not == @b
+ end
+ end
+
+ context "when #<=> calls super" do
+ before :each do
+ @a = ComparableSpecs::CompareCallingSuper.new
+ @b = ComparableSpecs::CompareCallingSuper.new
+ end
+
+ it "returns true for identical objects" do
+ @a.should == @a
+ end
+
+ it "calls the defined #<=> only once for different objects" do
+ @a.should_not == @b
+ @a.calls.should == 1
+ end
+ end
+end
diff --git a/spec/ruby/core/comparable/fixtures/classes.rb b/spec/ruby/core/comparable/fixtures/classes.rb
new file mode 100644
index 0000000000..2bdabbf014
--- /dev/null
+++ b/spec/ruby/core/comparable/fixtures/classes.rb
@@ -0,0 +1,37 @@
+module ComparableSpecs
+ class WithOnlyCompareDefined
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def <=>(other)
+ return nil if other.nil?
+ self.value <=> other.value
+ end
+ end
+
+ class Weird < WithOnlyCompareDefined
+ include Comparable
+ end
+
+ class WithoutCompareDefined
+ include Comparable
+ end
+
+ class CompareCallingSuper
+ include Comparable
+
+ attr_reader :calls
+
+ def initialize
+ @calls = 0
+ end
+
+ def <=>(other)
+ @calls += 1
+ super(other)
+ end
+ end
+end
diff --git a/spec/ruby/core/comparable/gt_spec.rb b/spec/ruby/core/comparable/gt_spec.rb
new file mode 100644
index 0000000000..cebb5464ad
--- /dev/null
+++ b/spec/ruby/core/comparable/gt_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Comparable#>" do
+ it "calls #<=> on self with other and returns true if #<=> returns any Integer greater than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(1)
+ (a > b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.1)
+ (a > b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(10000000)
+ (a > b).should == true
+ end
+
+ it "returns false if calling #<=> on self returns 0 or any Integer less than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(10)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0)
+ (a > b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.0)
+ (a > b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-1.0)
+ (a > b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-10000000)
+ (a > b).should == false
+ end
+
+ it "raises an ArgumentError if calling #<=> on self returns nil" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(nil)
+ -> { (a > b) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/comparable/gte_spec.rb b/spec/ruby/core/comparable/gte_spec.rb
new file mode 100644
index 0000000000..16da81b3ea
--- /dev/null
+++ b/spec/ruby/core/comparable/gte_spec.rb
@@ -0,0 +1,47 @@
+require_relative 'fixtures/classes'
+require_relative '../../spec_helper'
+
+describe "Comparable#>=" do
+ it "calls #<=> on self with other and returns true if #<=> returns 0 or any Integer greater than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0)
+ (a >= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.0)
+ (a >= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(1)
+ (a >= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.1)
+ (a >= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(10000000)
+ (a >= b).should == true
+ end
+
+ it "returns false if calling #<=> on self returns any Integer less than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(10)
+
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-0.1)
+ (a >= b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-1.0)
+ (a >= b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-10000000)
+ (a >= b).should == false
+ end
+
+ it "raises an ArgumentError if calling #<=> on self returns nil" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(nil)
+ -> { (a >= b) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/comparable/lt_spec.rb b/spec/ruby/core/comparable/lt_spec.rb
new file mode 100644
index 0000000000..175646d0d7
--- /dev/null
+++ b/spec/ruby/core/comparable/lt_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Comparable#<" do
+ it "calls #<=> on self with other and returns true if #<=> returns any Integer less than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-1)
+ (a < b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-0.1)
+ (a < b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-10000000)
+ (a < b).should == true
+ end
+
+ it "returns false if calling #<=> on self returns 0 or any Integer greater than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(10)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0)
+ (a < b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.0)
+ (a < b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(1.0)
+ (a < b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(10000000)
+ (a < b).should == false
+ end
+
+ it "raises an ArgumentError if calling #<=> on self returns nil" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(nil)
+ -> { (a < b) }.should.raise(ArgumentError)
+ end
+
+ it "raises an argument error with a message containing the value" do
+ -> { ("foo" < 7) }.should.raise(ArgumentError) { |e|
+ e.message.should.include? "String with 7 failed"
+ }
+ end
+end
diff --git a/spec/ruby/core/comparable/lte_spec.rb b/spec/ruby/core/comparable/lte_spec.rb
new file mode 100644
index 0000000000..8cbbd5ebb4
--- /dev/null
+++ b/spec/ruby/core/comparable/lte_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Comparable#<=" do
+ it "calls #<=> on self with other and returns true if #<=> returns 0 or any Integer less than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0)
+ (a <= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.0)
+ (a <= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-1)
+ (a <= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-0.1)
+ (a <= b).should == true
+
+ a.should_receive(:<=>).any_number_of_times.and_return(-10000000)
+ (a <= b).should == true
+ end
+
+ it "returns false if calling #<=> on self returns any Integer greater than 0" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(10)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(0.1)
+ (a <= b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(1.0)
+ (a <= b).should == false
+
+ a.should_receive(:<=>).any_number_of_times.and_return(10000000)
+ (a <= b).should == false
+ end
+
+ it "raises an ArgumentError if calling #<=> on self returns nil" do
+ a = ComparableSpecs::Weird.new(0)
+ b = ComparableSpecs::Weird.new(20)
+
+ a.should_receive(:<=>).any_number_of_times.and_return(nil)
+ -> { (a <= b) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/complex/abs2_spec.rb b/spec/ruby/core/complex/abs2_spec.rb
new file mode 100644
index 0000000000..3e5c5fd225
--- /dev/null
+++ b/spec/ruby/core/complex/abs2_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Complex#abs2" do
+ it "returns the sum of the squares of the real and imaginary parts" do
+ Complex(1, -2).abs2.should == 1 + 4
+ Complex(-0.1, 0.2).abs2.should be_close(0.01 + 0.04, TOLERANCE)
+ Complex(0).abs2.should == 0
+ end
+end
diff --git a/spec/ruby/core/complex/abs_spec.rb b/spec/ruby/core/complex/abs_spec.rb
new file mode 100644
index 0000000000..ed5aebb11d
--- /dev/null
+++ b/spec/ruby/core/complex/abs_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "Complex#abs" do
+ it "returns the modulus: |a + bi| = sqrt((a ^ 2) + (b ^ 2))" do
+ Complex(0, 0).abs.should == 0
+ Complex(3, 4).abs.should == 5 # well-known integer case
+ Complex(-3, 4).abs.should == 5
+ Complex(1, -1).abs.should be_close(Math.sqrt(2), TOLERANCE)
+ Complex(6.5, 0).abs.should be_close(6.5, TOLERANCE)
+ Complex(0, -7.2).abs.should be_close(7.2, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/complex/angle_spec.rb b/spec/ruby/core/complex/angle_spec.rb
new file mode 100644
index 0000000000..7551214d2b
--- /dev/null
+++ b/spec/ruby/core/complex/angle_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex#angle" do
+ it "is an alias of Complex#arg" do
+ Complex.instance_method(:angle).should == Complex.instance_method(:arg)
+ end
+end
diff --git a/spec/ruby/core/complex/arg_spec.rb b/spec/ruby/core/complex/arg_spec.rb
new file mode 100644
index 0000000000..dd64102d77
--- /dev/null
+++ b/spec/ruby/core/complex/arg_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Complex#arg" do
+ it "returns the argument -- i.e., the angle from (1, 0) in the complex plane" do
+ two_pi = 2 * Math::PI
+ (Complex(1, 0).arg % two_pi).should be_close(0, TOLERANCE)
+ (Complex(0, 2).arg % two_pi).should be_close(Math::PI * 0.5, TOLERANCE)
+ (Complex(-100, 0).arg % two_pi).should be_close(Math::PI, TOLERANCE)
+ (Complex(0, -75.3).arg % two_pi).should be_close(Math::PI * 1.5, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/complex/coerce_spec.rb b/spec/ruby/core/complex/coerce_spec.rb
new file mode 100644
index 0000000000..d4ea85a713
--- /dev/null
+++ b/spec/ruby/core/complex/coerce_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../spec_helper'
+
+describe "Complex#coerce" do
+ before :each do
+ @one = Complex(1)
+ end
+
+ it "returns an array containing other and self as Complex when other is an Integer" do
+ result = @one.coerce(2)
+ result.should == [2, 1]
+ result.first.should.is_a?(Complex)
+ result.last.should.is_a?(Complex)
+ end
+
+ it "returns an array containing other and self as Complex when other is a Float" do
+ result = @one.coerce(20.5)
+ result.should == [20.5, 1]
+ result.first.should.is_a?(Complex)
+ result.last.should.is_a?(Complex)
+ end
+
+ it "returns an array containing other and self as Complex when other is a Bignum" do
+ result = @one.coerce(4294967296)
+ result.should == [4294967296, 1]
+ result.first.should.is_a?(Complex)
+ result.last.should.is_a?(Complex)
+ end
+
+ it "returns an array containing other and self as Complex when other is a Rational" do
+ result = @one.coerce(Rational(5,6))
+ result.should == [Rational(5,6), 1]
+ result.first.should.is_a?(Complex)
+ result.last.should.is_a?(Complex)
+ end
+
+ it "returns an array containing other and self when other is a Complex" do
+ other = Complex(2)
+ result = @one.coerce(other)
+ result.should == [other, @one]
+ result.first.should.equal?(other)
+ result.last.should.equal?(@one)
+ end
+
+ it "returns an array containing other as Complex and self when other is a Numeric which responds to #real? with true" do
+ other = mock_numeric('other')
+ other.should_receive(:real?).any_number_of_times.and_return(true)
+ result = @one.coerce(other)
+ result.should == [other, @one]
+ result.first.should.eql?(Complex(other))
+ result.last.should.equal?(@one)
+ end
+
+ it "raises TypeError when other is a Numeric which responds to #real? with false" do
+ other = mock_numeric('other')
+ other.should_receive(:real?).any_number_of_times.and_return(false)
+ -> { @one.coerce(other) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when other is a String" do
+ -> { @one.coerce("20") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when other is nil" do
+ -> { @one.coerce(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when other is false" do
+ -> { @one.coerce(false) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/complex/comparison_spec.rb b/spec/ruby/core/complex/comparison_spec.rb
new file mode 100644
index 0000000000..3115f4e205
--- /dev/null
+++ b/spec/ruby/core/complex/comparison_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+describe "Complex#<=>" do
+ it "returns nil if either self or argument has imaginary part" do
+ (Complex(5, 1) <=> Complex(2)).should == nil
+ (Complex(1) <=> Complex(2, 1)).should == nil
+ (5 <=> Complex(2, 1)).should == nil
+ end
+
+ it "returns nil if argument is not numeric" do
+ (Complex(5, 1) <=> "cmp").should == nil
+ (Complex(1) <=> "cmp").should == nil
+ (Complex(1) <=> Object.new).should == nil
+ end
+
+ it "returns 0, 1, or -1 if self and argument do not have imaginary part" do
+ (Complex(5) <=> Complex(2)).should == 1
+ (Complex(2) <=> Complex(3)).should == -1
+ (Complex(2) <=> Complex(2)).should == 0
+
+ (Complex(5) <=> 2).should == 1
+ (Complex(2) <=> 3).should == -1
+ (Complex(2) <=> 2).should == 0
+ end
+end
diff --git a/spec/ruby/core/complex/conj_spec.rb b/spec/ruby/core/complex/conj_spec.rb
new file mode 100644
index 0000000000..063c85faec
--- /dev/null
+++ b/spec/ruby/core/complex/conj_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex#conj" do
+ it "is an alias of Complex#conjugate" do
+ Complex.instance_method(:conj).should == Complex.instance_method(:conjugate)
+ end
+end
diff --git a/spec/ruby/core/complex/conjugate_spec.rb b/spec/ruby/core/complex/conjugate_spec.rb
new file mode 100644
index 0000000000..256fe4b3be
--- /dev/null
+++ b/spec/ruby/core/complex/conjugate_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+
+describe "Complex#conjugate" do
+ it "returns the complex conjugate: conj a + bi = a - bi" do
+ Complex(3, 5).conjugate.should == Complex(3, -5)
+ Complex(3, -5).conjugate.should == Complex(3, 5)
+ Complex(-3.0, 5.2).conjugate.should be_close(Complex(-3.0, -5.2), TOLERANCE)
+ Complex(3.0, -5.2).conjugate.should be_close(Complex(3.0, 5.2), TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/complex/constants_spec.rb b/spec/ruby/core/complex/constants_spec.rb
new file mode 100644
index 0000000000..200e97731a
--- /dev/null
+++ b/spec/ruby/core/complex/constants_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex::I" do
+ it "is Complex(0, 1)" do
+ Complex::I.should.eql?(Complex(0, 1))
+ end
+end
diff --git a/spec/ruby/core/complex/denominator_spec.rb b/spec/ruby/core/complex/denominator_spec.rb
new file mode 100644
index 0000000000..c1a2003820
--- /dev/null
+++ b/spec/ruby/core/complex/denominator_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Complex#denominator" do
+ it "returns the least common multiple denominator of the real and imaginary parts" do
+ Complex(3, 4).denominator.should == 1
+ Complex(3, bignum_value).denominator.should == 1
+
+ Complex(3, Rational(3,4)).denominator.should == 4
+
+ Complex(Rational(4,8), Rational(3,4)).denominator.should == 4
+ Complex(Rational(3,8), Rational(3,4)).denominator.should == 8
+ end
+end
diff --git a/spec/ruby/core/complex/divide_spec.rb b/spec/ruby/core/complex/divide_spec.rb
new file mode 100644
index 0000000000..d5b4aa3885
--- /dev/null
+++ b/spec/ruby/core/complex/divide_spec.rb
@@ -0,0 +1,84 @@
+require_relative '../../spec_helper'
+
+describe "Complex#/" do
+ describe "with Complex" do
+ it "divides according to the usual rule for complex numbers" do
+ a = Complex((1 * 10) - (2 * 20), (1 * 20) + (2 * 10))
+ b = Complex(1, 2)
+ (a / b).should == Complex(10, 20)
+
+ c = Complex((1.5 * 100.2) - (2.1 * -30.3), (1.5 * -30.3) + (2.1 * 100.2))
+ d = Complex(1.5, 2.1)
+ # remember the floating-point arithmetic
+ (c / d).should be_close(Complex(100.2, -30.3), TOLERANCE)
+ end
+ end
+
+ describe "with Fixnum" do
+ it "divides both parts of the Complex number" do
+ (Complex(20, 40) / 2).should == Complex(10, 20)
+ (Complex(30, 30) / 10).should == Complex(3, 3)
+ end
+
+ it "raises a ZeroDivisionError when given zero" do
+ -> { Complex(20, 40) / 0 }.should.raise(ZeroDivisionError)
+ end
+
+ it "produces Rational parts" do
+ (Complex(5, 9) / 2).should.eql?(Complex(Rational(5,2), Rational(9,2)))
+ end
+ end
+
+ describe "with Bignum" do
+ it "divides both parts of the Complex number" do
+ (Complex(20, 40) / 2).should == Complex(10, 20)
+ (Complex(15, 16) / 2.0).should be_close(Complex(7.5, 8), TOLERANCE)
+ end
+ end
+
+ describe "with Float" do
+ it "divides both parts of the Complex number" do
+ (Complex(3, 9) / 1.5).should == Complex(2, 6)
+ (Complex(15, 16) / 2.0).should be_close(Complex(7.5, 8), TOLERANCE)
+ end
+
+ it "returns Complex(Infinity, Infinity) when given zero" do
+ (Complex(20, 40) / 0.0).real.infinite?.should == 1
+ (Complex(20, 40) / 0.0).imag.infinite?.should == 1
+ (Complex(-20, 40) / 0.0).real.infinite?.should == -1
+ (Complex(-20, 40) / 0.0).imag.infinite?.should == 1
+ end
+ end
+
+ describe "with Object" do
+ it "tries to coerce self into other" do
+ value = Complex(3, 9)
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(value).and_return([4, 2])
+ (value / obj).should == 2
+ end
+ end
+
+ describe "with a Numeric which responds to #real? with true" do
+ it "returns Complex(real.quo(other), imag.quo(other))" do
+ other = mock_numeric('other')
+ real = mock_numeric('real')
+ imag = mock_numeric('imag')
+ other.should_receive(:real?).and_return(true)
+ real.should_receive(:quo).with(other).and_return(1)
+ imag.should_receive(:quo).with(other).and_return(2)
+ (Complex(real, imag) / other).should == Complex(1, 2)
+ end
+ end
+
+ describe "with a Numeric which responds to #real? with false" do
+ it "coerces the passed argument to Complex and divides the resulting elements" do
+ complex = Complex(3, 0)
+ other = mock_numeric('other')
+ other.should_receive(:real?).any_number_of_times.and_return(false)
+ other.should_receive(:coerce).with(complex).and_return([5, 2])
+ (complex / other).should.eql?(Rational(5, 2))
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/eql_spec.rb b/spec/ruby/core/complex/eql_spec.rb
new file mode 100644
index 0000000000..2082a22feb
--- /dev/null
+++ b/spec/ruby/core/complex/eql_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Complex#eql?" do
+ it "returns false if other is not Complex" do
+ Complex(1).eql?(1).should == false
+ end
+
+ it "returns true when the respective parts are of the same classes and self == other" do
+ Complex(1, 2).eql?(Complex(1, 2)).should == true
+ end
+
+ it "returns false when the real parts are of different classes" do
+ Complex(1).eql?(Complex(1.0)).should == false
+ end
+
+ it "returns false when the imaginary parts are of different classes" do
+ Complex(1, 2).eql?(Complex(1, 2.0)).should == false
+ end
+
+ it "returns false when self == other is false" do
+ Complex(1, 2).eql?(Complex(2, 3)).should == false
+ end
+
+ it "does NOT send #eql? to real or imaginary parts" do
+ real = mock_numeric('real')
+ imag = mock_numeric('imag')
+ real.should_not_receive(:eql?)
+ imag.should_not_receive(:eql?)
+ Complex(real, imag).eql?(Complex(real, imag)).should == true
+ end
+end
diff --git a/spec/ruby/core/complex/equal_value_spec.rb b/spec/ruby/core/complex/equal_value_spec.rb
new file mode 100644
index 0000000000..b3562ff3fb
--- /dev/null
+++ b/spec/ruby/core/complex/equal_value_spec.rb
@@ -0,0 +1,93 @@
+require_relative '../../spec_helper'
+
+describe "Complex#==" do
+ describe "with Complex" do
+ it "returns true when self and other have numerical equality" do
+ Complex(1, 2).should == Complex(1, 2)
+ Complex(3, 9).should == Complex(3, 9)
+ Complex(-3, -9).should == Complex(-3, -9)
+
+ Complex(1, 2).should_not == Complex(3, 4)
+ Complex(3, 9).should_not == Complex(9, 3)
+
+ Complex(1.0, 2.0).should == Complex(1, 2)
+ Complex(3.0, 9.0).should_not == Complex(9.0, 3.0)
+
+ Complex(1.5, 2.5).should == Complex(1.5, 2.5)
+ Complex(1.5, 2.5).should == Complex(1.5, 2.5)
+ Complex(-1.5, 2.5).should == Complex(-1.5, 2.5)
+
+ Complex(1.5, 2.5).should_not == Complex(2.5, 1.5)
+ Complex(3.75, 2.5).should_not == Complex(1.5, 2.5)
+
+ Complex(bignum_value, 2.5).should == Complex(bignum_value, 2.5)
+ Complex(3.75, bignum_value).should_not == Complex(1.5, bignum_value)
+
+ Complex(nan_value).should_not == Complex(nan_value)
+ end
+ end
+
+ describe "with Numeric" do
+ it "returns true when self's imaginary part is 0 and the real part and other have numerical equality" do
+ Complex(3, 0).should == 3
+ Complex(-3, 0).should == -3
+
+ Complex(3.5, 0).should == 3.5
+ Complex(-3.5, 0).should == -3.5
+
+ Complex(bignum_value, 0).should == bignum_value
+ Complex(-bignum_value, 0).should == -bignum_value
+
+ Complex(3.0, 0).should == 3
+ Complex(-3.0, 0).should == -3
+
+ Complex(3, 0).should_not == 4
+ Complex(-3, 0).should_not == -4
+
+ Complex(3.5, 0).should_not == -4.5
+ Complex(-3.5, 0).should_not == 2.5
+
+ Complex(bignum_value, 0).should_not == bignum_value(10)
+ Complex(-bignum_value, 0).should_not == -bignum_value(20)
+ end
+ end
+
+ describe "with Object" do
+ # Integer#== and Float#== only return booleans - Bug?
+ it "calls other#== with self" do
+ value = Complex(3, 0)
+
+ obj = mock("Object")
+ obj.should_receive(:==).with(value).and_return(:expected)
+
+ (value == obj).should_not == false
+ end
+ end
+
+ describe "with a Numeric which responds to #real? with true" do
+ before do
+ @other = mock_numeric('other')
+ @other.should_receive(:real?).any_number_of_times.and_return(true)
+ end
+
+ it "returns real == other when the imaginary part is zero" do
+ real = mock_numeric('real')
+ real.should_receive(:==).with(@other).and_return(true)
+ (Complex(real, 0) == @other).should == true
+ end
+
+ it "returns false when the imaginary part is not zero" do
+ (Complex(3, 1) == @other).should == false
+ end
+ end
+
+ describe "with a Numeric which responds to #real? with false" do
+ it "returns other == self" do
+ complex = Complex(3, 0)
+ other = mock_numeric('other')
+ other.should_receive(:real?).any_number_of_times.and_return(false)
+ other.should_receive(:==).with(complex).and_return(true)
+ (complex == other).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/exponent_spec.rb b/spec/ruby/core/complex/exponent_spec.rb
new file mode 100644
index 0000000000..d0db0a40c2
--- /dev/null
+++ b/spec/ruby/core/complex/exponent_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+
+describe "Complex#**" do
+ describe "with Integer 0" do
+ it "returns Complex(1)" do
+ (Complex(3, 4) ** 0).should.eql?(Complex(1))
+ end
+ end
+
+ describe "with Float 0.0" do
+ it "returns Complex(1.0, 0.0)" do
+ (Complex(3, 4) ** 0.0).should.eql?(Complex(1.0, 0.0))
+ end
+ end
+
+ describe "with Complex" do
+ it "returns self raised to the given power" do
+ (Complex(2, 1) ** Complex(2, 1)).should be_close(Complex(-0.504824688978319, 3.10414407699553), TOLERANCE)
+ (Complex(2, 1) ** Complex(3, 4)).should be_close(Complex(-0.179174656916581, -1.74071656397662), TOLERANCE)
+
+ (Complex(2, 1) ** Complex(-2, -1)).should be_close(Complex(-0.051041070450869, -0.313849223270419), TOLERANCE)
+ (Complex(-2, -1) ** Complex(2, 1)).should be_close(Complex(-11.6819929610857, 71.8320439736158), TOLERANCE)
+ end
+ end
+
+ describe "with Integer" do
+ it "returns self raised to the given power" do
+ (Complex(2, 1) ** 2).should == Complex(3, 4)
+ (Complex(3, 4) ** 2).should == Complex(-7, 24)
+ (Complex(3, 4) ** -2).should be_close(Complex(-0.0112, -0.0384), TOLERANCE)
+
+
+ (Complex(2, 1) ** 2.5).should be_close(Complex(2.99179707178602, 6.85206901006896), TOLERANCE)
+ (Complex(3, 4) ** 2.5).should be_close(Complex(-38.0, 41.0), TOLERANCE)
+ (Complex(3, 4) ** -2.5).should be_close(Complex(-0.01216, -0.01312), TOLERANCE)
+
+ (Complex(1) ** 1).should == Complex(1)
+
+ # NOTE: Takes way too long...
+ #(Complex(2, 1) ** bignum_value)
+ end
+ end
+
+ describe "with Rational" do
+ it "returns self raised to the given power" do
+ (Complex(2, 1) ** Rational(3, 4)).should be_close(Complex(1.71913265276568, 0.623124744394697), TOLERANCE)
+ (Complex(2, 1) ** Rational(4, 3)).should be_close(Complex(2.3828547125173, 1.69466313833091), TOLERANCE)
+ (Complex(2, 1) ** Rational(-4, 3)).should be_close(Complex(0.278700377879388, -0.198209003071003), TOLERANCE)
+ end
+ end
+
+ describe "with Object" do
+ it "tries to coerce self into other" do
+ value = Complex(3, 9)
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(value).and_return([2, 5])
+ (value ** obj).should == 2 ** 5
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/fdiv_spec.rb b/spec/ruby/core/complex/fdiv_spec.rb
new file mode 100644
index 0000000000..fdcbc6a95d
--- /dev/null
+++ b/spec/ruby/core/complex/fdiv_spec.rb
@@ -0,0 +1,129 @@
+require_relative '../../spec_helper'
+
+describe "Complex#fdiv" do
+ it "accepts a numeric argument" do
+ -> { Complex(20).fdiv(2) }.should_not.raise(TypeError)
+ -> { Complex(20).fdiv(2.0) }.should_not.raise(TypeError)
+ -> { Complex(20).fdiv(bignum_value) }.should_not.raise(TypeError)
+ end
+
+ it "accepts a negative numeric argument" do
+ -> { Complex(20).fdiv(-2) }.should_not.raise(TypeError)
+ -> { Complex(20).fdiv(-2.0) }.should_not.raise(TypeError)
+ -> { Complex(20).fdiv(-bignum_value) }.should_not.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a non-numeric argument" do
+ -> { Complex(20).fdiv([]) }.should.raise(TypeError)
+ -> { Complex(20).fdiv(:sym) }.should.raise(TypeError)
+ -> { Complex(20).fdiv('s') }.should.raise(TypeError)
+ end
+
+ it "sets the real part to NaN if self's real part is NaN" do
+ Complex(nan_value).fdiv(2).real.nan?.should == true
+ end
+
+ it "sets the imaginary part to NaN if self's imaginary part is NaN" do
+ Complex(2, nan_value).fdiv(2).imag.nan?.should == true
+ end
+
+ it "sets the real and imaginary part to NaN if self's real and imaginary parts are NaN" do
+ Complex(nan_value, nan_value).fdiv(2).imag.nan?.should == true
+ Complex(nan_value, nan_value).fdiv(2).real.nan?.should == true
+ end
+
+ it "sets the real and imaginary part to NaN if self's real part and the argument are both NaN" do
+ Complex(nan_value, 2).fdiv(nan_value).imag.nan?.should == true
+ Complex(nan_value, 2).fdiv(nan_value).real.nan?.should == true
+ end
+
+ it "sets the real and imaginary part to NaN if self's real part, self's imaginary part, and the argument are NaN" do
+ Complex(nan_value, nan_value).fdiv(nan_value).imag.nan?.should == true
+ Complex(nan_value, nan_value).fdiv(nan_value).real.nan?.should == true
+ end
+
+ it "sets the real part to Infinity if self's real part is Infinity" do
+ Complex(infinity_value).fdiv(2).real.infinite?.should == 1
+ Complex(infinity_value,2).fdiv(2).real.infinite?.should == 1
+ end
+
+ it "sets the imaginary part to Infinity if self's imaginary part is Infinity" do
+ Complex(2, infinity_value).fdiv(2).imag.infinite?.should == 1
+ Complex(2, infinity_value).fdiv(2).imag.infinite?.should == 1
+ end
+
+ it "sets the imaginary and real part to Infinity if self's imaginary and real parts are Infinity" do
+ Complex(infinity_value, infinity_value).fdiv(2).real.infinite?.should == 1
+ Complex(infinity_value, infinity_value).fdiv(2).imag.infinite?.should == 1
+ end
+
+ it "sets the real part to NaN and the imaginary part to NaN if self's imaginary part, self's real part, and the argument are Infinity" do
+ Complex(infinity_value, infinity_value).fdiv(infinity_value).real.nan?.should == true
+ Complex(infinity_value, infinity_value).fdiv(infinity_value).imag.nan?.should == true
+ end
+end
+
+describe "Complex#fdiv with no imaginary part" do
+ before :each do
+ @numbers = [1, 5.43, 10, bignum_value, 99872.2918710].map{|n| [n,-n]}.flatten
+ end
+
+ it "returns a Complex number" do
+ @numbers.each do |real|
+ @numbers.each do |other|
+ Complex(real).fdiv(other).should.instance_of?(Complex)
+ end
+ end
+ end
+
+ it "sets the real part to self's real part fdiv'd with the argument" do
+ @numbers.each do |real|
+ @numbers.each do |other|
+ Complex(real).fdiv(other).real.should == real.fdiv(other)
+ end
+ end
+ end
+
+ it "sets the imaginary part to 0.0" do
+ @numbers.each do |real|
+ @numbers.each do |other|
+ Complex(real).fdiv(other).imaginary.should == 0.0
+ end
+ end
+ end
+end
+
+describe "Complex#fdiv with an imaginary part" do
+ before :each do
+ @numbers = [1, 5.43, 10, bignum_value, 99872.2918710].map{|n| [n,-n]}.flatten
+ end
+
+ it "returns a Complex number" do
+ @numbers.each do |real|
+ @numbers.each_with_index do |other,idx|
+ Complex(
+ real,@numbers[idx == 0 ? -1 : idx-1]
+ ).fdiv(other).should.instance_of?(Complex)
+ end
+ end
+ end
+
+ it "sets the real part to self's real part fdiv'd with the argument" do
+ @numbers.each do |real|
+ @numbers.each_with_index do |other,idx|
+ Complex(
+ real,@numbers[idx == 0 ? -1 : idx-1]
+ ).fdiv(other).real.should == real.fdiv(other)
+ end
+ end
+ end
+
+ it "sets the imaginary part to the imaginary part fdiv'd with the argument" do
+ @numbers.each do |real|
+ @numbers.each_with_index do |other,idx|
+ im = @numbers[idx == 0 ? -1 : idx-1]
+ Complex(real, im).fdiv(other).imag.should == im.fdiv(other)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/finite_spec.rb b/spec/ruby/core/complex/finite_spec.rb
new file mode 100644
index 0000000000..7d9f82404e
--- /dev/null
+++ b/spec/ruby/core/complex/finite_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "Complex#finite?" do
+ it "returns true if magnitude is finite" do
+ (1+1i).should.finite?
+ end
+
+ it "returns false for positive infinity" do
+ value = Complex(Float::INFINITY, 42)
+ value.should_not.finite?
+ end
+
+ it "returns false for positive complex with infinite imaginary" do
+ value = Complex(1, Float::INFINITY)
+ value.should_not.finite?
+ end
+
+ it "returns false for negative infinity" do
+ value = -Complex(Float::INFINITY, 42)
+ value.should_not.finite?
+ end
+
+ it "returns false for negative complex with infinite imaginary" do
+ value = -Complex(1, Float::INFINITY)
+ value.should_not.finite?
+ end
+
+ it "returns false for NaN" do
+ value = Complex(Float::NAN, Float::NAN)
+ value.should_not.finite?
+ end
+end
diff --git a/spec/ruby/core/complex/hash_spec.rb b/spec/ruby/core/complex/hash_spec.rb
new file mode 100644
index 0000000000..cad283309d
--- /dev/null
+++ b/spec/ruby/core/complex/hash_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Complex#hash" do
+ it "is static" do
+ Complex(1).hash.should == Complex(1).hash
+ Complex(1, 0).hash.should == Complex(1).hash
+ Complex(1, 1).hash.should == Complex(1, 1).hash
+ end
+
+ it "is different for different instances" do
+ Complex(1, 2).hash.should_not == Complex(1, 1).hash
+ Complex(2, 1).hash.should_not == Complex(1, 1).hash
+
+ Complex(1, 2).hash.should_not == Complex(2, 1).hash
+ end
+end
diff --git a/spec/ruby/core/complex/imag_spec.rb b/spec/ruby/core/complex/imag_spec.rb
new file mode 100644
index 0000000000..225f168e78
--- /dev/null
+++ b/spec/ruby/core/complex/imag_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex#imag" do
+ it "is an alias of Complex#imaginary" do
+ Complex.instance_method(:imag).should == Complex.instance_method(:imaginary)
+ end
+end
diff --git a/spec/ruby/core/complex/imaginary_spec.rb b/spec/ruby/core/complex/imaginary_spec.rb
new file mode 100644
index 0000000000..ac9284e934
--- /dev/null
+++ b/spec/ruby/core/complex/imaginary_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+
+describe "Complex#imaginary" do
+ it "returns the imaginary part of self" do
+ Complex(1, 0).imaginary.should == 0
+ Complex(2, 1).imaginary.should == 1
+ Complex(6.7, 8.9).imaginary.should == 8.9
+ Complex(1, bignum_value).imaginary.should == bignum_value
+ end
+end
diff --git a/spec/ruby/core/complex/infinite_spec.rb b/spec/ruby/core/complex/infinite_spec.rb
new file mode 100644
index 0000000000..9e48860dee
--- /dev/null
+++ b/spec/ruby/core/complex/infinite_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "Complex#infinite?" do
+ it "returns nil if magnitude is finite" do
+ (1+1i).infinite?.should == nil
+ end
+
+ it "returns 1 for positive infinity" do
+ value = Complex(Float::INFINITY, 42).infinite?
+ value.should == 1
+ end
+
+ it "returns 1 for positive complex with infinite imaginary" do
+ value = Complex(1, Float::INFINITY).infinite?
+ value.should == 1
+ end
+
+ it "returns -1 for negative infinity" do
+ value = -Complex(Float::INFINITY, 42).infinite?
+ value.should == -1
+ end
+
+ it "returns -1 for negative complex with infinite imaginary" do
+ value = -Complex(1, Float::INFINITY).infinite?
+ value.should == -1
+ end
+
+ it "returns nil for NaN" do
+ value = Complex(0, Float::NAN).infinite?
+ value.should == nil
+ end
+end
diff --git a/spec/ruby/core/complex/inspect_spec.rb b/spec/ruby/core/complex/inspect_spec.rb
new file mode 100644
index 0000000000..045be94b22
--- /dev/null
+++ b/spec/ruby/core/complex/inspect_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative '../numeric/fixtures/classes'
+
+describe "Complex#inspect" do
+ it "returns (${real}+${image}i) for positive imaginary parts" do
+ Complex(1).inspect.should == "(1+0i)"
+ Complex(7).inspect.should == "(7+0i)"
+ Complex(-1, 4).inspect.should == "(-1+4i)"
+ Complex(-7, 6.7).inspect.should == "(-7+6.7i)"
+ end
+
+ it "returns (${real}-${image}i) for negative imaginary parts" do
+ Complex(0, -1).inspect.should == "(0-1i)"
+ Complex(-1, -4).inspect.should == "(-1-4i)"
+ Complex(-7, -6.7).inspect.should == "(-7-6.7i)"
+ end
+
+ it "calls #inspect on real and imaginary" do
+ real = NumericSpecs::Subclass.new
+ # + because of https://bugs.ruby-lang.org/issues/20337
+ real.should_receive(:inspect).and_return(+"1")
+ imaginary = NumericSpecs::Subclass.new
+ imaginary.should_receive(:inspect).and_return("2")
+ imaginary.should_receive(:<).any_number_of_times.and_return(false)
+ Complex(real, imaginary).inspect.should == "(1+2i)"
+ end
+
+ it "adds an `*' before the `i' if the last character of the imaginary part is not numeric" do
+ real = NumericSpecs::Subclass.new
+ # + because of https://bugs.ruby-lang.org/issues/20337
+ real.should_receive(:inspect).and_return(+"(1)")
+ imaginary = NumericSpecs::Subclass.new
+ imaginary.should_receive(:inspect).and_return("(2)")
+ imaginary.should_receive(:<).any_number_of_times.and_return(false)
+ Complex(real, imaginary).inspect.should == "((1)+(2)*i)"
+ end
+end
diff --git a/spec/ruby/core/complex/integer_spec.rb b/spec/ruby/core/complex/integer_spec.rb
new file mode 100644
index 0000000000..559bfbccfd
--- /dev/null
+++ b/spec/ruby/core/complex/integer_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Complex#integer?" do
+ it "returns false for a Complex with no imaginary part" do
+ Complex(20).integer?.should == false
+ end
+
+ it "returns false for a Complex with an imaginary part" do
+ Complex(20,3).integer?.should == false
+ end
+end
diff --git a/spec/ruby/core/complex/magnitude_spec.rb b/spec/ruby/core/complex/magnitude_spec.rb
new file mode 100644
index 0000000000..6341b4eec8
--- /dev/null
+++ b/spec/ruby/core/complex/magnitude_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex#magnitude" do
+ it "is an alias of Complex#abs" do
+ Complex.instance_method(:magnitude).should == Complex.instance_method(:abs)
+ end
+end
diff --git a/spec/ruby/core/complex/marshal_dump_spec.rb b/spec/ruby/core/complex/marshal_dump_spec.rb
new file mode 100644
index 0000000000..201d55e9e5
--- /dev/null
+++ b/spec/ruby/core/complex/marshal_dump_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Complex#marshal_dump" do
+ it "is a private method" do
+ Complex.private_instance_methods(false).should.include?(:marshal_dump)
+ end
+
+ it "dumps real and imaginary parts" do
+ Complex(1, 2).send(:marshal_dump).should == [1, 2]
+ end
+end
diff --git a/spec/ruby/core/complex/minus_spec.rb b/spec/ruby/core/complex/minus_spec.rb
new file mode 100644
index 0000000000..7c104ce784
--- /dev/null
+++ b/spec/ruby/core/complex/minus_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+
+describe "Complex#-" do
+ describe "with Complex" do
+ it "subtracts both the real and imaginary components" do
+ (Complex(1, 2) - Complex(10, 20)).should == Complex(1 - 10, 2 - 20)
+ (Complex(1.5, 2.1) - Complex(100.2, -30.3)).should == Complex(1.5 - 100.2, 2.1 - (-30.3))
+ end
+ end
+
+ describe "with Integer" do
+ it "subtracts the real number from the real component of self" do
+ (Complex(1, 2) - 50).should == Complex(-49, 2)
+ (Complex(1, 2) - 50.5).should == Complex(-49.5, 2)
+ end
+ end
+
+ describe "with Object" do
+ it "tries to coerce self into other" do
+ value = Complex(3, 9)
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(value).and_return([2, 5])
+ (value - obj).should == 2 - 5
+ end
+ end
+
+ describe "passed Numeric which responds to #real? with true" do
+ it "coerces the passed argument to the type of the real part and subtracts the resulting elements" do
+ n = mock_numeric('n')
+ n.should_receive(:real?).and_return(true)
+ n.should_receive(:coerce).with(1).and_return([1, 4])
+ (Complex(1, 2) - n).should == Complex(-3, 2)
+ end
+ end
+
+ describe "passed Numeric which responds to #real? with false" do
+ it "coerces the passed argument to Complex and subtracts the resulting elements" do
+ n = mock_numeric('n')
+ n.should_receive(:real?).and_return(false)
+ n.should_receive(:coerce).with(Complex(1, 2)).and_return([Complex(1, 2), Complex(3, 4)])
+ (Complex(1, 2) - n).should == Complex(-2, -2)
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/multiply_spec.rb b/spec/ruby/core/complex/multiply_spec.rb
new file mode 100644
index 0000000000..35bf7c8455
--- /dev/null
+++ b/spec/ruby/core/complex/multiply_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+
+describe "Complex#*" do
+ describe "with Complex" do
+ it "multiplies according to the usual rule for complex numbers: (a + bi) * (c + di) = ac - bd + (ad + bc)i" do
+ (Complex(1, 2) * Complex(10, 20)).should == Complex((1 * 10) - (2 * 20), (1 * 20) + (2 * 10))
+ (Complex(1.5, 2.1) * Complex(100.2, -30.3)).should == Complex((1.5 * 100.2) - (2.1 * -30.3), (1.5 * -30.3) + (2.1 * 100.2))
+ end
+ end
+
+ describe "with Integer" do
+ it "multiplies both parts of self by the given Integer" do
+ (Complex(3, 2) * 50).should == Complex(150, 100)
+ (Complex(-3, 2) * 50.5).should == Complex(-151.5, 101)
+ end
+ end
+
+ describe "with Object" do
+ it "tries to coerce self into other" do
+ value = Complex(3, 9)
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(value).and_return([2, 5])
+ (value * obj).should == 2 * 5
+ end
+ end
+
+ describe "with a Numeric which responds to #real? with true" do
+ it "multiples both parts of self by other" do
+ other = mock_numeric('other')
+ real = mock_numeric('real')
+ imag = mock_numeric('imag')
+ other.should_receive(:real?).and_return(true)
+ real.should_receive(:*).with(other).and_return(1)
+ imag.should_receive(:*).with(other).and_return(2)
+ (Complex(real, imag) * other).should == Complex(1, 2)
+ end
+
+ describe "with a Numeric which responds to #real? with false" do
+ it "coerces the passed argument to Complex and multiplies the resulting elements" do
+ complex = Complex(3, 0)
+ other = mock_numeric('other')
+ other.should_receive(:real?).any_number_of_times.and_return(false)
+ other.should_receive(:coerce).with(complex).and_return([5, 2])
+ (complex * other).should == 10
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/negative_spec.rb b/spec/ruby/core/complex/negative_spec.rb
new file mode 100644
index 0000000000..566975b8e1
--- /dev/null
+++ b/spec/ruby/core/complex/negative_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Complex#negative?" do
+ it "is undefined" do
+ c = Complex(1)
+
+ c.methods.should_not.include?(:negative?)
+
+ -> {
+ c.negative?
+ }.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/complex/numerator_spec.rb b/spec/ruby/core/complex/numerator_spec.rb
new file mode 100644
index 0000000000..7ab66e6a61
--- /dev/null
+++ b/spec/ruby/core/complex/numerator_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Complex#numerator" do
+ it "returns self's numerator" do
+ Complex(2).numerator.should == Complex(2)
+ Complex(3, 4).numerator.should == Complex(3, 4)
+
+ Complex(Rational(3, 4), Rational(3, 4)).numerator.should == Complex(3, 3)
+ Complex(Rational(7, 4), Rational(8, 4)).numerator.should == Complex(7, 8)
+
+ Complex(Rational(7, 8), Rational(8, 4)).numerator.should == Complex(7, 16)
+ Complex(Rational(7, 4), Rational(8, 8)).numerator.should == Complex(7, 4)
+
+ # NOTE:
+ # Bug? - Fails with a MethodMissingError
+ # (undefined method `denominator' for 3.5:Float)
+ # Complex(3.5, 3.7).numerator
+ end
+end
diff --git a/spec/ruby/core/complex/phase_spec.rb b/spec/ruby/core/complex/phase_spec.rb
new file mode 100644
index 0000000000..2ab90989e1
--- /dev/null
+++ b/spec/ruby/core/complex/phase_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex#phase" do
+ it "is an alias of Complex#arg" do
+ Complex.instance_method(:phase).should == Complex.instance_method(:arg)
+ end
+end
diff --git a/spec/ruby/core/complex/plus_spec.rb b/spec/ruby/core/complex/plus_spec.rb
new file mode 100644
index 0000000000..2056ca786c
--- /dev/null
+++ b/spec/ruby/core/complex/plus_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+
+describe "Complex#+" do
+ describe "with Complex" do
+ it "adds both the real and imaginary components" do
+ (Complex(1, 2) + Complex(10, 20)).should == Complex(1 + 10, 2 + 20)
+ (Complex(1.5, 2.1) + Complex(100.2, -30.3)).should == Complex(1.5 + 100.2, 2.1 + (-30.3))
+ end
+ end
+
+ describe "with Integer" do
+ it "adds the real number to the real component of self" do
+ (Complex(1, 2) + 50).should == Complex(51, 2)
+ (Complex(1, 2) + 50.5).should == Complex(51.5, 2)
+ end
+ end
+
+ describe "with Object" do
+ it "tries to coerce self into other" do
+ value = Complex(3, 9)
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(value).and_return([2, 5])
+ (value + obj).should == 2 + 5
+ end
+ end
+
+ describe "passed Numeric which responds to #real? with true" do
+ it "coerces the passed argument to the type of the real part and adds the resulting elements" do
+ n = mock_numeric('n')
+ n.should_receive(:real?).and_return(true)
+ n.should_receive(:coerce).with(1).and_return([1, 4])
+ (Complex(1, 2) + n).should == Complex(5, 2)
+ end
+ end
+
+ describe "passed Numeric which responds to #real? with false" do
+ it "coerces the passed argument to Complex and adds the resulting elements" do
+ n = mock_numeric('n')
+ n.should_receive(:real?).and_return(false)
+ n.should_receive(:coerce).with(Complex(1, 2)).and_return([Complex(1, 2), Complex(3, 4)])
+ (Complex(1, 2) + n).should == Complex(4, 6)
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/polar_spec.rb b/spec/ruby/core/complex/polar_spec.rb
new file mode 100644
index 0000000000..824211fcd0
--- /dev/null
+++ b/spec/ruby/core/complex/polar_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Complex.polar" do
+ it "returns a complex number in terms of radius and angle" do
+ Complex.polar(50, 60).should be_close(Complex(-47.6206490207578, -15.2405310551108), TOLERANCE)
+ Complex.polar(-10, -20).should be_close(Complex(-4.08082061813392, 9.12945250727628), TOLERANCE)
+ end
+
+ it "raises a TypeError when given non real arguments" do
+ ->{ Complex.polar(nil) }.should.raise(TypeError)
+ ->{ Complex.polar(nil, nil) }.should.raise(TypeError)
+ end
+
+ it "computes the real values of the real & imaginary parts from the polar form" do
+ a = Complex.polar(1.0+0.0i, Math::PI/2+0.0i)
+ a.real.should be_close(0.0, TOLERANCE)
+ a.imag.should be_close(1.0, TOLERANCE)
+ a.real.real?.should == true
+ a.imag.real?.should == true
+
+ b = Complex.polar(1+0.0i)
+ b.real.should be_close(1.0, TOLERANCE)
+ b.imag.should be_close(0.0, TOLERANCE)
+ b.real.real?.should == true
+ b.imag.real?.should == true
+ end
+end
+
+describe "Complex#polar" do
+ it "returns the absolute value and the argument" do
+ a = Complex(3, 4)
+ a.polar.size.should == 2
+ a.polar.first.should == 5.0
+ a.polar.last.should be_close(0.927295218001612, TOLERANCE)
+
+ b = Complex(-3.5, 4.7)
+ b.polar.size.should == 2
+ b.polar.first.should be_close(5.86003412959345, TOLERANCE)
+ b.polar.last.should be_close(2.21088447955664, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/complex/positive_spec.rb b/spec/ruby/core/complex/positive_spec.rb
new file mode 100644
index 0000000000..d2fb256538
--- /dev/null
+++ b/spec/ruby/core/complex/positive_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Complex#positive?" do
+ it "is undefined" do
+ c = Complex(1)
+
+ c.methods.should_not.include?(:positive?)
+
+ -> {
+ c.positive?
+ }.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/complex/quo_spec.rb b/spec/ruby/core/complex/quo_spec.rb
new file mode 100644
index 0000000000..be0a44d532
--- /dev/null
+++ b/spec/ruby/core/complex/quo_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Complex#quo" do
+ it "is an alias of Complex#/" do
+ Complex.instance_method(:quo).should == Complex.instance_method(:/)
+ end
+end
diff --git a/spec/ruby/core/complex/rationalize_spec.rb b/spec/ruby/core/complex/rationalize_spec.rb
new file mode 100644
index 0000000000..d49bb52def
--- /dev/null
+++ b/spec/ruby/core/complex/rationalize_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Complex#rationalize" do
+ it "raises RangeError if self has non-zero imaginary part" do
+ -> { Complex(1,5).rationalize }.should.raise(RangeError)
+ end
+
+ it "raises RangeError if self has 0.0 imaginary part" do
+ -> { Complex(1,0.0).rationalize }.should.raise(RangeError)
+ end
+
+ it "returns a Rational if self has zero imaginary part" do
+ Complex(1,0).rationalize.should == Rational(1,1)
+ Complex(2<<63+5).rationalize.should == Rational(2<<63+5,1)
+ end
+
+ it "sends #rationalize to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:rationalize).with(0.1).and_return(:result)
+ Complex(real, 0).rationalize(0.1).should == :result
+ end
+
+ it "ignores a single argument" do
+ Complex(1,0).rationalize(0.1).should == Rational(1,1)
+ end
+
+ it "raises ArgumentError when passed more than one argument" do
+ -> { Complex(1,0).rationalize(0.1, 0.1) }.should.raise(ArgumentError)
+ -> { Complex(1,0).rationalize(0.1, 0.1, 2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/complex/real_spec.rb b/spec/ruby/core/complex/real_spec.rb
new file mode 100644
index 0000000000..41734b23f0
--- /dev/null
+++ b/spec/ruby/core/complex/real_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+
+describe "Complex#real" do
+ it "returns the real part of self" do
+ Complex(1, 0).real.should == 1
+ Complex(2, 1).real.should == 2
+ Complex(6.7, 8.9).real.should == 6.7
+ Complex(bignum_value, 3).real.should == bignum_value
+ end
+end
+
+describe "Complex#real?" do
+ it "returns false if there is an imaginary part" do
+ Complex(2,3).real?.should == false
+ end
+
+ it "returns false if there is not an imaginary part" do
+ Complex(2).real?.should == false
+ end
+
+ it "returns false if the real part is Infinity" do
+ Complex(infinity_value).real?.should == false
+ end
+
+ it "returns false if the real part is NaN" do
+ Complex(nan_value).real?.should == false
+ end
+end
diff --git a/spec/ruby/core/complex/rect_spec.rb b/spec/ruby/core/complex/rect_spec.rb
new file mode 100644
index 0000000000..72f2bf9930
--- /dev/null
+++ b/spec/ruby/core/complex/rect_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Complex#rect" do
+ it "is an alias of Complex#rectangular" do
+ Complex.instance_method(:rect).should == Complex.instance_method(:rectangular)
+ end
+end
+
+describe "Complex.rect" do
+ it "is an alias of Complex#rectangular" do
+ Complex.method(:rect).should == Complex.method(:rectangular)
+ end
+end
diff --git a/spec/ruby/core/complex/rectangular_spec.rb b/spec/ruby/core/complex/rectangular_spec.rb
new file mode 100644
index 0000000000..7789bf925e
--- /dev/null
+++ b/spec/ruby/core/complex/rectangular_spec.rb
@@ -0,0 +1,114 @@
+require_relative '../../spec_helper'
+
+describe "Complex#rectangular" do
+ before :each do
+ @numbers = [
+ Complex(1),
+ Complex(0, 20),
+ Complex(0, 0),
+ Complex(0.0),
+ Complex(9999999**99),
+ Complex(-20),
+ Complex.polar(76, 10)
+ ]
+ end
+
+ it "returns an Array" do
+ @numbers.each do |number|
+ number.rectangular.should.instance_of?(Array)
+ end
+ end
+
+ it "returns a two-element Array" do
+ @numbers.each do |number|
+ number.rectangular.size.should == 2
+ end
+ end
+
+ it "returns the real part of self as the first element" do
+ @numbers.each do |number|
+ number.rectangular.first.should == number.real
+ end
+ end
+
+ it "returns the imaginary part of self as the last element" do
+ @numbers.each do |number|
+ number.rectangular.last.should == number.imaginary
+ end
+ end
+
+ it "raises an ArgumentError if given any arguments" do
+ @numbers.each do |number|
+ -> { number.rectangular(number) }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe "Complex.rectangular" do
+ describe "passed a Numeric n which responds to #real? with true" do
+ it "returns a Complex with real part n and imaginary part 0" do
+ n = mock_numeric('n')
+ n.should_receive(:real?).any_number_of_times.and_return(true)
+ result = Complex.rectangular(n)
+ result.real.should == n
+ result.imag.should == 0
+ end
+ end
+
+ describe "passed a Numeric which responds to #real? with false" do
+ it "raises TypeError" do
+ n = mock_numeric('n')
+ n.should_receive(:real?).any_number_of_times.and_return(false)
+ -> { Complex.rectangular(n) }.should.raise(TypeError)
+ end
+ end
+
+ describe "passed Numerics n1 and n2 and at least one responds to #real? with false" do
+ [[false, false], [false, true], [true, false]].each do |r1, r2|
+ it "raises TypeError" do
+ n1 = mock_numeric('n1')
+ n2 = mock_numeric('n2')
+ n1.should_receive(:real?).any_number_of_times.and_return(r1)
+ n2.should_receive(:real?).any_number_of_times.and_return(r2)
+ -> { Complex.rectangular(n1, n2) }.should.raise(TypeError)
+ end
+ end
+ end
+
+ describe "passed Numerics n1 and n2 and both respond to #real? with true" do
+ it "returns a Complex with real part n1 and imaginary part n2" do
+ n1 = mock_numeric('n1')
+ n2 = mock_numeric('n2')
+ n1.should_receive(:real?).any_number_of_times.and_return(true)
+ n2.should_receive(:real?).any_number_of_times.and_return(true)
+ result = Complex.rectangular(n1, n2)
+ result.real.should == n1
+ result.imag.should == n2
+ end
+ end
+
+ describe "when passed a Complex" do
+ it "raises a TypeError when the imaginary part is not zero" do
+ -> {
+ Complex.rectangular(1.0+1i, 2)
+ }.should.raise(TypeError)
+
+ -> {
+ Complex.rectangular(1.0, 2i)
+ }.should.raise(TypeError)
+ end
+
+ it "ignores the imaginary part if it is zero" do
+ result = Complex.rectangular(1.0+0i, 2+0.0i)
+ result.real.should == 1.0
+ result.imag.should == 2
+ end
+ end
+
+ describe "passed a non-Numeric" do
+ it "raises TypeError" do
+ -> { Complex.rectangular(:sym) }.should.raise(TypeError)
+ -> { Complex.rectangular(0, :sym) }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/to_c_spec.rb b/spec/ruby/core/complex/to_c_spec.rb
new file mode 100644
index 0000000000..cd7195556c
--- /dev/null
+++ b/spec/ruby/core/complex/to_c_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "Complex#to_c" do
+ it "returns self" do
+ value = Complex(1, 5)
+ value.to_c.should.equal?(value)
+ end
+
+ it 'returns the same value' do
+ Complex(1, 5).to_c.should == Complex(1, 5)
+ end
+end
diff --git a/spec/ruby/core/complex/to_f_spec.rb b/spec/ruby/core/complex/to_f_spec.rb
new file mode 100644
index 0000000000..9f3265cdb9
--- /dev/null
+++ b/spec/ruby/core/complex/to_f_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Complex#to_f" do
+ describe "when the imaginary part is Integer 0" do
+ it "returns the result of sending #to_f to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_f).and_return(:f)
+ Complex(real, 0).to_f.should == :f
+ end
+ end
+
+ describe "when the imaginary part is Rational 0" do
+ it "returns the result of sending #to_f to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_f).and_return(:f)
+ Complex(real, Rational(0)).to_f.should == :f
+ end
+ end
+
+ describe "when the imaginary part responds to #== 0 with true" do
+ it "returns the result of sending #to_f to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_f).and_return(:f)
+ imag = mock_numeric('imag')
+ imag.should_receive(:==).with(0).any_number_of_times.and_return(true)
+ Complex(real, imag).to_f.should == :f
+ end
+ end
+
+ describe "when the imaginary part is non-zero" do
+ it "raises RangeError" do
+ -> { Complex(0, 1).to_f }.should.raise(RangeError)
+ end
+ end
+
+ describe "when the imaginary part is Float 0.0" do
+ it "raises RangeError" do
+ -> { Complex(0, 0.0).to_f }.should.raise(RangeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/to_i_spec.rb b/spec/ruby/core/complex/to_i_spec.rb
new file mode 100644
index 0000000000..9149ffbbaa
--- /dev/null
+++ b/spec/ruby/core/complex/to_i_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Complex#to_i" do
+ describe "when the imaginary part is Integer 0" do
+ it "returns the result of sending #to_i to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_i).and_return(:i)
+ Complex(real, 0).to_i.should == :i
+ end
+ end
+
+ describe "when the imaginary part is Rational 0" do
+ it "returns the result of sending #to_i to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_i).and_return(:i)
+ Complex(real, Rational(0)).to_i.should == :i
+ end
+ end
+
+ describe "when the imaginary part responds to #== 0 with true" do
+ it "returns the result of sending #to_i to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_i).and_return(:i)
+ imag = mock_numeric('imag')
+ imag.should_receive(:==).with(0).any_number_of_times.and_return(true)
+ Complex(real, imag).to_i.should == :i
+ end
+ end
+
+ describe "when the imaginary part is non-zero" do
+ it "raises RangeError" do
+ -> { Complex(0, 1).to_i }.should.raise(RangeError)
+ end
+ end
+
+ describe "when the imaginary part is Float 0.0" do
+ it "raises RangeError" do
+ -> { Complex(0, 0.0).to_i }.should.raise(RangeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/to_r_spec.rb b/spec/ruby/core/complex/to_r_spec.rb
new file mode 100644
index 0000000000..6587ae9e2e
--- /dev/null
+++ b/spec/ruby/core/complex/to_r_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+
+describe "Complex#to_r" do
+ describe "when the imaginary part is Integer 0" do
+ it "returns the result of sending #to_r to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_r).and_return(:r)
+ Complex(real, 0).to_r.should == :r
+ end
+ end
+
+ describe "when the imaginary part is Rational 0" do
+ it "returns the result of sending #to_r to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_r).and_return(:r)
+ Complex(real, Rational(0)).to_r.should == :r
+ end
+ end
+
+ describe "when the imaginary part responds to #== 0 with true" do
+ it "returns the result of sending #to_r to the real part" do
+ real = mock_numeric('real')
+ real.should_receive(:to_r).and_return(:r)
+ imag = mock_numeric('imag')
+ imag.should_receive(:==).with(0).any_number_of_times.and_return(true)
+ Complex(real, imag).to_r.should == :r
+ end
+ end
+
+ describe "when the imaginary part is non-zero" do
+ it "raises RangeError" do
+ -> { Complex(0, 1).to_r }.should.raise(RangeError)
+ end
+ end
+
+ describe "when the imaginary part is Float 0.0" do
+ ruby_version_is ''...'3.4' do
+ it "raises RangeError" do
+ -> { Complex(0, 0.0).to_r }.should.raise(RangeError)
+ end
+ end
+
+ ruby_version_is '3.4' do
+ it "returns a Rational" do
+ Complex(0, 0.0).to_r.should == 0r
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/complex/to_s_spec.rb b/spec/ruby/core/complex/to_s_spec.rb
new file mode 100644
index 0000000000..ceccffe470
--- /dev/null
+++ b/spec/ruby/core/complex/to_s_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative '../numeric/fixtures/classes'
+
+describe "Complex#to_s" do
+ describe "when self's real component is 0" do
+ it "returns both the real and imaginary component even when the real is 0" do
+ Complex(0, 5).to_s.should == "0+5i"
+ Complex(0, -3.2).to_s.should == "0-3.2i"
+ end
+ end
+
+ it "returns self as String" do
+ Complex(1, 5).to_s.should == "1+5i"
+ Complex(-2.5, 1.5).to_s.should == "-2.5+1.5i"
+
+ Complex(1, -5).to_s.should == "1-5i"
+ Complex(-2.5, -1.5).to_s.should == "-2.5-1.5i"
+
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ Complex(1, 0).to_s.should == "1+0i"
+ Complex(1, -0).to_s.should == "1+0i"
+ end
+ end
+
+ it "returns 1+0.0i for Complex(1, 0.0)" do
+ Complex(1, 0.0).to_s.should == "1+0.0i"
+ end
+
+ it "returns 1-0.0i for Complex(1, -0.0)" do
+ Complex(1, -0.0).to_s.should == "1-0.0i"
+ end
+
+ it "returns 1+Infinity*i for Complex(1, Infinity)" do
+ Complex(1, infinity_value).to_s.should == "1+Infinity*i"
+ end
+
+ it "returns 1-Infinity*i for Complex(1, -Infinity)" do
+ Complex(1, -infinity_value).to_s.should == "1-Infinity*i"
+ end
+
+ it "returns 1+NaN*i for Complex(1, NaN)" do
+ Complex(1, nan_value).to_s.should == "1+NaN*i"
+ end
+
+ it "treats real and imaginary parts as strings" do
+ real = NumericSpecs::Subclass.new
+ # + because of https://bugs.ruby-lang.org/issues/20337
+ real.should_receive(:to_s).and_return(+"1")
+ imaginary = NumericSpecs::Subclass.new
+ imaginary.should_receive(:to_s).and_return("2")
+ imaginary.should_receive(:<).any_number_of_times.and_return(false)
+ Complex(real, imaginary).to_s.should == "1+2i"
+ end
+end
diff --git a/spec/ruby/core/complex/uminus_spec.rb b/spec/ruby/core/complex/uminus_spec.rb
new file mode 100644
index 0000000000..c0184e11de
--- /dev/null
+++ b/spec/ruby/core/complex/uminus_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Complex#-@" do
+ it "sends #-@ to the real and imaginary parts and returns a Complex with the resulting respective parts" do
+ real = mock_numeric('real')
+ imag = mock_numeric('imag')
+ real.should_receive(:-@).and_return(-1)
+ imag.should_receive(:-@).and_return(-2)
+ Complex(real, imag).send(:-@).should == Complex(-1, -2)
+ end
+end
diff --git a/spec/ruby/core/conditionvariable/broadcast_spec.rb b/spec/ruby/core/conditionvariable/broadcast_spec.rb
new file mode 100644
index 0000000000..16c7b4cc8d
--- /dev/null
+++ b/spec/ruby/core/conditionvariable/broadcast_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "ConditionVariable#broadcast" do
+ it "releases all threads waiting in line for this resource" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ threads = []
+ r1 = []
+ r2 = []
+
+ # large number to attempt to cause race conditions
+ 100.times do |i|
+ threads << Thread.new(i) do |tid|
+ m.synchronize do
+ r1 << tid
+ cv.wait(m)
+ r2 << tid
+ end
+ end
+ end
+
+ # wait for all threads to acquire the mutex the first time
+ Thread.pass until m.synchronize { r1.size == threads.size }
+ # wait until all threads are sleeping (ie waiting)
+ Thread.pass until threads.all?(&:stop?)
+
+ r2.should.empty?
+ m.synchronize do
+ cv.broadcast
+ end
+
+ threads.each {|t| t.join }
+
+ # ensure that all threads that enter cv.wait are released
+ r2.sort.should == r1.sort
+ # note that order is not specified as broadcast results in a race
+ # condition on regaining the lock m
+ end
+end
diff --git a/spec/ruby/core/conditionvariable/marshal_dump_spec.rb b/spec/ruby/core/conditionvariable/marshal_dump_spec.rb
new file mode 100644
index 0000000000..594c178e88
--- /dev/null
+++ b/spec/ruby/core/conditionvariable/marshal_dump_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "ConditionVariable#marshal_dump" do
+ it "raises a TypeError" do
+ cv = ConditionVariable.new
+ -> { cv.marshal_dump }.should.raise(TypeError, /can't dump/)
+ end
+end
diff --git a/spec/ruby/core/conditionvariable/signal_spec.rb b/spec/ruby/core/conditionvariable/signal_spec.rb
new file mode 100644
index 0000000000..3b266cf8c6
--- /dev/null
+++ b/spec/ruby/core/conditionvariable/signal_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+
+describe "ConditionVariable#signal" do
+ it "releases the first thread waiting in line for this resource" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ threads = []
+ r1 = []
+ r2 = []
+
+ # large number to attempt to cause race conditions
+ 100.times do |i|
+ threads << Thread.new(i) do |tid|
+ m.synchronize do
+ r1 << tid
+ cv.wait(m)
+ r2 << tid
+ end
+ end
+ end
+
+ # wait for all threads to acquire the mutex the first time
+ Thread.pass until m.synchronize { r1.size == threads.size }
+ # wait until all threads are sleeping (ie waiting)
+ Thread.pass until threads.all?(&:stop?)
+
+ r2.should.empty?
+ 100.times do |i|
+ m.synchronize do
+ cv.signal
+ end
+ Thread.pass until r2.size == i+1
+ end
+
+ threads.each {|t| t.join }
+
+ # ensure that all the threads that went into the cv.wait are
+ # released in the same order
+ r2.should == r1
+ end
+
+ it "allows control to be passed between a pair of threads" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ repeats = 100
+ in_synchronize = false
+
+ t1 = Thread.new do
+ m.synchronize do
+ in_synchronize = true
+ repeats.times do
+ cv.wait(m)
+ cv.signal
+ end
+ end
+ end
+
+ # Make sure t1 is waiting for a signal before launching t2.
+ Thread.pass until in_synchronize
+ Thread.pass until t1.stop?
+
+ t2 = Thread.new do
+ m.synchronize do
+ repeats.times do
+ cv.signal
+ cv.wait(m)
+ end
+ end
+ end
+
+ # Check that both threads terminated without exception
+ t1.join
+ t2.join
+ m.should_not.locked?
+ end
+end
diff --git a/spec/ruby/core/conditionvariable/wait_spec.rb b/spec/ruby/core/conditionvariable/wait_spec.rb
new file mode 100644
index 0000000000..1af53a15a2
--- /dev/null
+++ b/spec/ruby/core/conditionvariable/wait_spec.rb
@@ -0,0 +1,174 @@
+require_relative '../../spec_helper'
+
+describe "ConditionVariable#wait" do
+ it "calls #sleep on the given object" do
+ o = Object.new
+ o.should_receive(:sleep).with(1234)
+
+ cv = ConditionVariable.new
+
+ cv.wait(o, 1234)
+ end
+
+ it "can be woken up by ConditionVariable#signal" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ in_synchronize = false
+
+ th = Thread.new do
+ m.synchronize do
+ in_synchronize = true
+ cv.wait(m)
+ end
+ :success
+ end
+
+ # wait for m to acquire the mutex
+ Thread.pass until in_synchronize
+ # wait until th is sleeping (ie waiting)
+ Thread.pass until th.stop?
+
+ m.synchronize { cv.signal }
+ th.value.should == :success
+ end
+
+ it "can be interrupted by Thread#run" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ in_synchronize = false
+
+ th = Thread.new do
+ m.synchronize do
+ in_synchronize = true
+ cv.wait(m)
+ end
+ :success
+ end
+
+ # wait for m to acquire the mutex
+ Thread.pass until in_synchronize
+ # wait until th is sleeping (ie waiting)
+ Thread.pass until th.stop?
+
+ th.run
+ th.value.should == :success
+ end
+
+ it "can be interrupted by Thread#wakeup" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ in_synchronize = false
+
+ th = Thread.new do
+ m.synchronize do
+ in_synchronize = true
+ cv.wait(m)
+ end
+ :success
+ end
+
+ # wait for m to acquire the mutex
+ Thread.pass until in_synchronize
+ # wait until th is sleeping (ie waiting)
+ Thread.pass until th.stop?
+
+ th.wakeup
+ th.value.should == :success
+ end
+
+ it "reacquires the lock even if the thread is killed" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ in_synchronize = false
+ owned = nil
+
+ th = Thread.new do
+ m.synchronize do
+ in_synchronize = true
+ begin
+ cv.wait(m)
+ ensure
+ owned = m.owned?
+ $stderr.puts "\nThe Thread doesn't own the Mutex!" unless owned
+ end
+ end
+ end
+
+ # wait for m to acquire the mutex
+ Thread.pass until in_synchronize
+ # wait until th is sleeping (ie waiting)
+ Thread.pass until th.stop?
+
+ th.kill
+ th.join
+
+ owned.should == true
+ end
+
+ it "reacquires the lock even if the thread is killed after being signaled" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ in_synchronize = false
+ owned = nil
+
+ th = Thread.new do
+ m.synchronize do
+ in_synchronize = true
+ begin
+ cv.wait(m)
+ ensure
+ owned = m.owned?
+ $stderr.puts "\nThe Thread doesn't own the Mutex!" unless owned
+ end
+ end
+ end
+
+ # wait for m to acquire the mutex
+ Thread.pass until in_synchronize
+ # wait until th is sleeping (ie waiting)
+ Thread.pass until th.stop?
+
+ m.synchronize {
+ cv.signal
+ # Wait that the thread is blocked on acquiring the Mutex
+ sleep 0.001
+ # Kill the thread, yet the thread should first acquire the Mutex before going on
+ th.kill
+ }
+
+ th.join
+ owned.should == true
+ end
+
+ it "supports multiple Threads waiting on the same ConditionVariable and Mutex" do
+ m = Mutex.new
+ cv = ConditionVariable.new
+ n_threads = 4
+ events = []
+
+ threads = n_threads.times.map {
+ Thread.new {
+ m.synchronize {
+ events << :t_in_synchronize
+ cv.wait(m)
+ }
+ }
+ }
+
+ Thread.pass until m.synchronize { events.size } == n_threads
+ Thread.pass until threads.any?(&:stop?)
+ m.synchronize do
+ threads.each { |t|
+ # Cause interactions with the waiting threads.
+ # On TruffleRuby, this causes a safepoint which has interesting
+ # interactions with the ConditionVariable.
+ bt = t.backtrace
+ bt.should.is_a?(Array)
+ bt.size.should >= 2
+ }
+ end
+
+ cv.broadcast
+ threads.each(&:join)
+ end
+end
diff --git a/spec/ruby/core/data/constants_spec.rb b/spec/ruby/core/data/constants_spec.rb
new file mode 100644
index 0000000000..ad0b1ddea7
--- /dev/null
+++ b/spec/ruby/core/data/constants_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Data" do
+ it "is a new constant" do
+ Data.superclass.should == Object
+ end
+
+ it "is not deprecated" do
+ -> { Data }.should_not complain
+ end
+end
diff --git a/spec/ruby/core/data/deconstruct_keys_spec.rb b/spec/ruby/core/data/deconstruct_keys_spec.rb
new file mode 100644
index 0000000000..7e81f966ea
--- /dev/null
+++ b/spec/ruby/core/data/deconstruct_keys_spec.rb
@@ -0,0 +1,110 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#deconstruct_keys" do
+ it "returns a hash of attributes" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ d.deconstruct_keys([:x, :y]).should == {x: 1, y: 2}
+ end
+
+ it "requires one argument" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ -> {
+ d.deconstruct_keys
+ }.should.raise(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/)
+ end
+
+ it "returns only specified keys" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ d.deconstruct_keys([:x, :y]).should == {x: 1, y: 2}
+ d.deconstruct_keys([:x] ).should == {x: 1}
+ d.deconstruct_keys([] ).should == {}
+ end
+
+ it "accepts string attribute names" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ d.deconstruct_keys(['x', 'y']).should == {'x' => 1, 'y' => 2}
+ end
+
+ it "returns an empty hash when there are more keys than attributes" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ d.deconstruct_keys([:x, :y, :x]).should == {}
+ end
+
+ it "returns at first not existing attribute name" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ d.deconstruct_keys([:a, :x]).should == {}
+ d.deconstruct_keys([:x, :a]).should == {x: 1}
+ end
+
+ it "accepts nil argument and return all the attributes" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ d.deconstruct_keys(nil).should == {x: 1, y: 2}
+ end
+
+ ruby_version_is "4.0" do # https://bugs.ruby-lang.org/issues/21844
+ it "tries to convert a key with #to_str if index is not a String nor a Symbol, but responds to #to_str" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ key = mock("to_str")
+ key.should_receive(:to_str).and_return("y")
+
+ d.deconstruct_keys([key]).should == { "y" => 2 }
+ end
+
+ it "raise an error on argument position number" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ -> {
+ d.deconstruct_keys([0, 1])
+ }.should.raise(TypeError, "0 is not a symbol nor a string")
+ end
+
+ it "raises a TypeError if the conversion with #to_str does not return a String" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ key = mock("to_str")
+ key.should_receive(:to_str).and_return(0)
+
+ -> {
+ d.deconstruct_keys([key])
+ }.should raise_consistent_error(TypeError, /can't convert MockObject into String/)
+ end
+
+ it "raises TypeError if index is not a Symbol and not convertible to String" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ -> {
+ d.deconstruct_keys([0, []])
+ }.should.raise(TypeError, "0 is not a symbol nor a string")
+ end
+ end
+
+ it "raise TypeError if passed anything except nil or array" do
+ klass = Data.define(:x, :y)
+ d = klass.new(1, 2)
+
+ -> { d.deconstruct_keys('x') }.should.raise(TypeError, /expected Array or nil/)
+ -> { d.deconstruct_keys(1) }.should.raise(TypeError, /expected Array or nil/)
+ -> { d.deconstruct_keys(:x) }.should.raise(TypeError, /expected Array or nil/)
+ -> { d.deconstruct_keys({}) }.should.raise(TypeError, /expected Array or nil/)
+ end
+end
diff --git a/spec/ruby/core/data/deconstruct_spec.rb b/spec/ruby/core/data/deconstruct_spec.rb
new file mode 100644
index 0000000000..4ca0b87039
--- /dev/null
+++ b/spec/ruby/core/data/deconstruct_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#deconstruct" do
+ it "returns an array of attribute values" do
+ DataSpecs::Measure.new(42, "km").deconstruct.should == [42, "km"]
+ end
+end
diff --git a/spec/ruby/core/data/define_spec.rb b/spec/ruby/core/data/define_spec.rb
new file mode 100644
index 0000000000..c0b4671e39
--- /dev/null
+++ b/spec/ruby/core/data/define_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data.define" do
+ it "accepts no arguments" do
+ empty_data = Data.define
+ empty_data.members.should == []
+ end
+
+ it "accepts symbols" do
+ movie = Data.define(:title, :year)
+ movie.members.should == [:title, :year]
+ end
+
+ it "accepts strings" do
+ movie = Data.define("title", "year")
+ movie.members.should == [:title, :year]
+ end
+
+ it "accepts a mix of strings and symbols" do
+ movie = Data.define("title", :year, "genre")
+ movie.members.should == [:title, :year, :genre]
+ end
+
+ it "accepts a block" do
+ movie = Data.define(:title, :year) do
+ def title_with_year
+ "#{title} (#{year})"
+ end
+ end
+ movie.members.should == [:title, :year]
+ movie.new("Matrix", 1999).title_with_year.should == "Matrix (1999)"
+ end
+end
diff --git a/spec/ruby/core/data/eql_spec.rb b/spec/ruby/core/data/eql_spec.rb
new file mode 100644
index 0000000000..6958d5de4a
--- /dev/null
+++ b/spec/ruby/core/data/eql_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#eql?" do
+ it "returns true if the other is the same object" do
+ a = DataSpecs::Measure.new(42, "km")
+ a.should.eql?(a)
+ end
+
+ it "returns true if the other has all the same fields" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42, "km")
+ a.should.eql?(b)
+ end
+
+ it "returns false if the other is a different object or has different fields" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42, "mi")
+ a.should_not.eql?(b)
+ end
+
+ it "returns false if other is of a different class" do
+ a = DataSpecs::Measure.new(42, "km")
+ klass = Data.define(*DataSpecs::Measure.members)
+ b = klass.new(42, "km")
+ a.should_not.eql?(b)
+ end
+
+ it "returns false if any corresponding elements are not equal with #eql?" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42.0, "mi")
+ a.should_not.eql?(b)
+ end
+
+ context "recursive structure" do
+ it "returns true the other is the same object" do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: 42, unit: a)
+
+ a.should.eql?(a)
+ end
+
+ it "returns true if the other has all the same fields" do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: 42, unit: a)
+
+ b = DataSpecs::Measure.allocate
+ b.send(:initialize, amount: 42, unit: b)
+
+ a.should.eql?(b)
+ end
+
+ it "returns false if any corresponding elements are not equal with #eql?" do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: a, unit: "km")
+
+ b = DataSpecs::Measure.allocate
+ b.send(:initialize, amount: b, unit: "mi")
+
+ a.should_not.eql?(b)
+ end
+ end
+end
diff --git a/spec/ruby/core/data/equal_value_spec.rb b/spec/ruby/core/data/equal_value_spec.rb
new file mode 100644
index 0000000000..d9a0dcff3e
--- /dev/null
+++ b/spec/ruby/core/data/equal_value_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#==" do
+ it "returns true if the other is the same object" do
+ a = DataSpecs::Measure.new(42, "km")
+ a.should == a
+ end
+
+ it "returns true if the other has all the same fields" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42, "km")
+ a.should == b
+ end
+
+ it "returns false if the other is a different object or has different fields" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42, "mi")
+ a.should_not == b
+ end
+
+ it "returns false if other is of a different class" do
+ a = DataSpecs::Measure.new(42, "km")
+ klass = Data.define(*DataSpecs::Measure.members)
+ b = klass.new(42, "km")
+ a.should_not == b
+ end
+
+ it "returns false if any corresponding elements are not equal with #==" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42.0, "mi")
+ a.should_not == b
+ end
+
+ context "recursive structure" do
+ it "returns true the other is the same object" do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: 42, unit: a)
+
+ a.should == a
+ end
+
+ it "returns true if the other has all the same fields" do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: 42, unit: a)
+
+ b = DataSpecs::Measure.allocate
+ b.send(:initialize, amount: 42, unit: b)
+
+ a.should == b
+ end
+
+ it "returns false if any corresponding elements are not equal with #==" do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: a, unit: "km")
+
+ b = DataSpecs::Measure.allocate
+ b.send(:initialize, amount: b, unit: "mi")
+
+ a.should_not == b
+ end
+ end
+end
diff --git a/spec/ruby/core/data/fixtures/classes.rb b/spec/ruby/core/data/fixtures/classes.rb
new file mode 100644
index 0000000000..147293ee45
--- /dev/null
+++ b/spec/ruby/core/data/fixtures/classes.rb
@@ -0,0 +1,41 @@
+module DataSpecs
+ if Data.respond_to?(:define)
+ Measure = Data.define(:amount, :unit)
+ Single = Data.define(:value)
+
+ class MeasureWithOverriddenName < Measure
+ def self.name
+ "A"
+ end
+ end
+
+ class SingleWithOverriddenName < Single
+ def self.name
+ "A"
+ end
+ end
+
+ class DataSubclass < Data; end
+
+ MeasureSubclass = Class.new(Measure) do
+ def initialize(amount:, unit:)
+ super
+ end
+ end
+
+ Empty = Data.define()
+
+ DataWithOverriddenInitialize = Data.define(:amount, :unit) do
+ def initialize(*rest, **kw)
+ super
+ ScratchPad.record [:initialize, rest, kw]
+ end
+ end
+
+ Area = Data.define(:width, :height, :area) do
+ def initialize(width:, height:)
+ super(width: width, height: height, area: width * height)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/data/hash_spec.rb b/spec/ruby/core/data/hash_spec.rb
new file mode 100644
index 0000000000..bab146c92e
--- /dev/null
+++ b/spec/ruby/core/data/hash_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#hash" do
+ it "returns the same integer for objects with the same content" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42, "km")
+ a.hash.should == b.hash
+ a.hash.should.instance_of?(Integer)
+ end
+
+ it "returns different hashes for objects with different values" do
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(42, "ml")
+ a.hash.should_not == b.hash
+
+ a = DataSpecs::Measure.new(42, "km")
+ b = DataSpecs::Measure.new(13, "km")
+ a.hash.should_not == b.hash
+ end
+
+ it "returns different hashes for different classes" do
+ Data.define(:x).new(1).hash.should != Data.define(:x).new(1).hash
+ end
+end
diff --git a/spec/ruby/core/data/initialize_spec.rb b/spec/ruby/core/data/initialize_spec.rb
new file mode 100644
index 0000000000..0320ca880c
--- /dev/null
+++ b/spec/ruby/core/data/initialize_spec.rb
@@ -0,0 +1,204 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#initialize" do
+ context "with no members" do
+ ruby_bug "#21819", ""..."4.0.1" do
+ it "is frozen" do
+ data = Data.define
+
+ data.new.should.frozen?
+ end
+ end
+ end
+
+ it "accepts positional arguments" do
+ data = DataSpecs::Measure.new(42, "km")
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ it "accepts alternative positional arguments" do
+ data = DataSpecs::Measure[42, "km"]
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ it "accepts keyword arguments" do
+ data = DataSpecs::Measure.new(amount: 42, unit: "km")
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ it "accepts alternative keyword arguments" do
+ data = DataSpecs::Measure[amount: 42, unit: "km"]
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ it "accepts String keyword arguments" do
+ data = DataSpecs::Measure.new("amount" => 42, "unit" => "km")
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ it "accepts the last entry when a keyword is given as both String and Symbol" do
+ data = DataSpecs::Single.new("value" => -1, value: 42)
+
+ data.value.should == 42
+ end
+
+ it "accepts positional arguments with empty keyword arguments" do
+ data = DataSpecs::Single.new(42, **{})
+
+ data.value.should == 42
+
+ data = DataSpecs::Measure.new(42, "km", **{})
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ it "raises ArgumentError if no arguments are given" do
+ -> {
+ DataSpecs::Measure.new
+ }.should.raise(ArgumentError) { |e|
+ e.message.should.include?("missing keywords: :amount, :unit")
+ }
+ end
+
+ it "raises ArgumentError if at least one argument is missing" do
+ -> {
+ DataSpecs::Measure.new(unit: "km")
+ }.should.raise(ArgumentError) { |e|
+ e.message.should.include?("missing keyword: :amount")
+ }
+ end
+
+ ruby_version_is "4.0" do # https://bugs.ruby-lang.org/issues/21844
+ it "raises ArgumentError if at least one argument is missing and other is provided as both String and Symbol" do
+ -> {
+ DataSpecs::Measure.new(unit: "km", "unit" => "km")
+ }.should.raise(ArgumentError) { |e|
+ e.message.should.include?("missing keyword: :amount")
+ }
+ end
+ end
+
+ it "raises ArgumentError if unknown keyword is given" do
+ -> {
+ DataSpecs::Measure.new(amount: 42, unit: "km", system: "metric")
+ }.should.raise(ArgumentError) { |e|
+ e.message.should.include?("unknown keyword: :system")
+ }
+ end
+
+ ruby_version_is "4.0" do # https://bugs.ruby-lang.org/issues/21844
+ it "raises ArgumentError if unknown keyword is given which is convertable to String" do
+ key = mock("to_str")
+ key.should_receive(:to_str).and_return("system")
+
+ -> {
+ DataSpecs::Measure.new(amount: 42, unit: "km", key => "metric")
+ }.should.raise(ArgumentError) { |e|
+ e.message.should.include?('unknown keyword: "system"')
+ }
+ end
+
+ it "raises TypeError when the keyword is not convertable to String" do
+ -> {
+ DataSpecs::Measure.new(1 => 2)
+ }.should.raise(TypeError) { |e|
+ e.message.should == "1 is not a symbol nor a string"
+ }
+ end
+
+ it "raises TypeError if the conversion with #to_str does not return a String" do
+ klass = Data.define(:x, :y)
+
+ key = mock("to_str")
+ key.should_receive(:to_str).and_return(0)
+
+ -> {
+ klass.new(key => 2)
+ }.should raise_consistent_error(TypeError, /can't convert MockObject into String/)
+ end
+ end
+
+ it "supports super from a subclass" do
+ ms = DataSpecs::MeasureSubclass.new(amount: 1, unit: "km")
+
+ ms.amount.should == 1
+ ms.unit.should == "km"
+ end
+
+ it "supports Data with no fields" do
+ -> { DataSpecs::Empty.new }.should_not.raise
+ end
+
+ it "can be overridden" do
+ ScratchPad.record []
+
+ measure_class = Data.define(:amount, :unit) do
+ def initialize(*, **)
+ super
+ ScratchPad << :initialize
+ end
+ end
+
+ measure_class.new(42, "m")
+ ScratchPad.recorded.should == [:initialize]
+ end
+
+ context "when it is overridden" do
+ it "is called with keyword arguments when given positional arguments" do
+ ScratchPad.clear
+ DataSpecs::DataWithOverriddenInitialize.new(42, "m")
+ ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}]
+ end
+
+ it "is called with keyword arguments when given keyword arguments" do
+ ScratchPad.clear
+ DataSpecs::DataWithOverriddenInitialize.new(amount: 42, unit: "m")
+ ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}]
+ end
+
+ it "is called with keyword arguments when given alternative positional arguments" do
+ ScratchPad.clear
+ DataSpecs::DataWithOverriddenInitialize[42, "m"]
+ ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}]
+ end
+
+ it "is called with keyword arguments when given alternative keyword arguments" do
+ ScratchPad.clear
+ DataSpecs::DataWithOverriddenInitialize[amount: 42, unit: "m"]
+ ScratchPad.recorded.should == [:initialize, [], {amount: 42, unit: "m"}]
+ end
+
+ it "accepts positional arguments with empty keyword arguments" do
+ data = DataSpecs::SingleWithOverriddenName.new(42, **{})
+
+ data.value.should == 42
+
+ data = DataSpecs::MeasureWithOverriddenName.new(42, "km", **{})
+
+ data.amount.should == 42
+ data.unit.should == "km"
+ end
+
+ # See https://github.com/ruby/psych/pull/765
+ it "can be deserialized by calling Data.instance_method(:initialize)" do
+ d1 = DataSpecs::Area.new(width: 2, height: 3)
+ d1.area.should == 6
+
+ d2 = DataSpecs::Area.allocate
+ Data.instance_method(:initialize).bind_call(d2, **d1.to_h)
+ d2.should == d1
+ end
+ end
+end
diff --git a/spec/ruby/core/data/inspect_spec.rb b/spec/ruby/core/data/inspect_spec.rb
new file mode 100644
index 0000000000..6c97a719e3
--- /dev/null
+++ b/spec/ruby/core/data/inspect_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#inspect" do
+ it "returns a string representation showing members and values" do
+ a = DataSpecs::Measure.new(42, "km")
+ a.inspect.should == '#<data DataSpecs::Measure amount=42, unit="km">'
+ end
+
+ it "returns a string representation without the class name for anonymous structs" do
+ Data.define(:a).new("").inspect.should == '#<data a="">'
+ end
+
+ it "returns a string representation without the class name for structs nested in anonymous classes" do
+ c = Class.new
+ c.class_eval <<~DOC
+ Foo = Data.define(:a)
+ DOC
+
+ c::Foo.new("").inspect.should == '#<data a="">'
+ end
+
+ it "returns a string representation without the class name for structs nested in anonymous modules" do
+ m = Module.new
+ m.class_eval <<~DOC
+ Foo = Data.define(:a)
+ DOC
+
+ m::Foo.new("").inspect.should == '#<data a="">'
+ end
+
+ it "does not call #name method" do
+ struct = DataSpecs::MeasureWithOverriddenName.new(42, "km")
+ struct.inspect.should == '#<data DataSpecs::MeasureWithOverriddenName amount=42, unit="km">'
+ end
+
+ it "does not call #name method when struct is anonymous" do
+ klass = Class.new(DataSpecs::Measure) do
+ def self.name
+ "A"
+ end
+ end
+ struct = klass.new(42, "km")
+ struct.inspect.should == '#<data amount=42, unit="km">'
+ end
+
+ context "recursive structure" do
+ it "returns string representation with recursive attribute replaced with ..." do
+ a = DataSpecs::Measure.allocate
+ a.send(:initialize, amount: 42, unit: a)
+
+ a.inspect.should == "#<data DataSpecs::Measure amount=42, unit=#<data DataSpecs::Measure:...>>"
+ end
+
+ it "returns string representation with recursive attribute replaced with ... when an anonymous class" do
+ klass = Class.new(DataSpecs::Measure)
+ a = klass.allocate
+ a.send(:initialize, amount: 42, unit: a)
+
+ a.inspect.should =~ /#<data amount=42, unit=#<data #<Class:0x.+?>:\.\.\.>>/
+ end
+ end
+end
diff --git a/spec/ruby/core/data/members_spec.rb b/spec/ruby/core/data/members_spec.rb
new file mode 100644
index 0000000000..457a90a0d6
--- /dev/null
+++ b/spec/ruby/core/data/members_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#members" do
+ it "returns an array of attribute names" do
+ measure = DataSpecs::Measure.new(amount: 42, unit: 'km')
+ measure.members.should == [:amount, :unit]
+ end
+end
+
+describe "DataClass#members" do
+ it "returns an array of attribute names" do
+ DataSpecs::Measure.members.should == [:amount, :unit]
+ end
+
+ context "class inheriting Data" do
+ it "isn't available in a subclass" do
+ DataSpecs::DataSubclass.should_not.respond_to?(:members)
+ end
+ end
+end
diff --git a/spec/ruby/core/data/to_h_spec.rb b/spec/ruby/core/data/to_h_spec.rb
new file mode 100644
index 0000000000..41925cf3b2
--- /dev/null
+++ b/spec/ruby/core/data/to_h_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#to_h" do
+ it "transforms the data object into a hash" do
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+ data.to_h.should == { amount: 42, unit: 'km' }
+ end
+
+ context "with block" do
+ it "transforms [key, value] pairs returned by the block into a hash" do
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+ data.to_h { |key, value| [value, key] }.should == { 42 => :amount, 'km' => :unit }
+ end
+
+ it "passes to a block each pair's key and value as separate arguments" do
+ ScratchPad.record []
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+ data.to_h { |k, v| ScratchPad << [k, v]; [k, v] }
+ ScratchPad.recorded.sort.should == [[:amount, 42], [:unit, 'km']]
+
+ ScratchPad.record []
+ data.to_h { |*args| ScratchPad << args; [args[0], args[1]] }
+ ScratchPad.recorded.sort.should == [[:amount, 42], [:unit, 'km']]
+ end
+
+ it "raises ArgumentError if block returns longer or shorter array" do
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+ -> do
+ data.to_h { |k, v| [k.to_s, v*v, 1] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+
+ -> do
+ data.to_h { |k, v| [k] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+ end
+
+ it "raises TypeError if block returns something other than Array" do
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+ -> do
+ data.to_h { |k, v| "not-array" }
+ end.should.raise(TypeError, /wrong element type String/)
+ end
+
+ it "coerces returned pair to Array with #to_ary" do
+ x = mock('x')
+ x.stub!(:to_ary).and_return([:b, 'b'])
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+
+ data.to_h { |k| x }.should == { :b => 'b' }
+ end
+
+ it "does not coerce returned pair to Array with #to_a" do
+ x = mock('x')
+ x.stub!(:to_a).and_return([:b, 'b'])
+ data = DataSpecs::Measure.new(amount: 42, unit: 'km')
+
+ -> do
+ data.to_h { |k| x }
+ end.should.raise(TypeError, /wrong element type MockObject/)
+ end
+ end
+end
diff --git a/spec/ruby/core/data/to_s_spec.rb b/spec/ruby/core/data/to_s_spec.rb
new file mode 100644
index 0000000000..a552e4659b
--- /dev/null
+++ b/spec/ruby/core/data/to_s_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#to_s" do
+ it "is an alias of Data#inspect" do
+ a = DataSpecs::Measure.new(42, "km")
+ a.method(:to_s).should == a.method(:inspect)
+ end
+end
diff --git a/spec/ruby/core/data/with_spec.rb b/spec/ruby/core/data/with_spec.rb
new file mode 100644
index 0000000000..b74df185c7
--- /dev/null
+++ b/spec/ruby/core/data/with_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Data#with" do
+ it "returns self if given no arguments" do
+ data = DataSpecs::Measure.new(amount: 42, unit: "km")
+ data = data.with.should.equal?(data)
+ end
+
+ it "accepts keyword arguments" do
+ data = DataSpecs::Measure.new(amount: 42, unit: "km")
+ data = data.with(amount: 4, unit: "m")
+
+ data.amount.should == 4
+ data.unit.should == "m"
+ end
+
+ it "accepts String keyword arguments" do
+ data = DataSpecs::Measure.new(amount: 42, unit: "km")
+ data = data.with("amount" => 4, "unit" => "m")
+
+ data.amount.should == 4
+ data.unit.should == "m"
+ end
+
+ it "raises ArgumentError if no keyword arguments are given" do
+ data = DataSpecs::Measure.new(amount: 42, unit: "km")
+
+ -> {
+ data.with(4, "m")
+ }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 0)")
+ end
+
+ it "does not depend on the Data.new method" do
+ subclass = Class.new(DataSpecs::Measure)
+ data = subclass.new(amount: 42, unit: "km")
+
+ def subclass.new(*)
+ raise "Data.new is called"
+ end
+
+ data_copy = data.with(unit: "m")
+ data_copy.amount.should == 42
+ data_copy.unit.should == "m"
+ end
+
+ it "calls #initialize" do
+ data = DataSpecs::DataWithOverriddenInitialize.new(42, "m")
+ ScratchPad.clear
+
+ data.with(amount: 0)
+
+ ScratchPad.recorded.should == [:initialize, [], {amount: 0, unit: "m"}]
+ end
+end
diff --git a/spec/ruby/core/dir/chdir_spec.rb b/spec/ruby/core/dir/chdir_spec.rb
new file mode 100644
index 0000000000..2dc598e2a9
--- /dev/null
+++ b/spec/ruby/core/dir/chdir_spec.rb
@@ -0,0 +1,218 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.chdir" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @original = Dir.pwd
+ end
+
+ after :each do
+ Dir.chdir(@original)
+ end
+
+ it "defaults to $HOME with no arguments" do
+ skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME'])
+
+ Dir.chdir
+ current_dir = Dir.pwd
+
+ Dir.chdir(ENV['HOME'])
+ home = Dir.pwd
+ current_dir.should == home
+ end
+
+ it "changes to the specified directory" do
+ Dir.chdir DirSpecs.mock_dir
+ Dir.pwd.should == DirSpecs.mock_dir
+ end
+
+ it "returns 0 when successfully changing directory" do
+ Dir.chdir(@original).should == 0
+ end
+
+ it "calls #to_str on the argument if it's not a String" do
+ obj = mock('path')
+ obj.should_receive(:to_str).and_return(Dir.pwd)
+ Dir.chdir(obj)
+ end
+
+ it "calls #to_str on the argument if it's not a String and a block is given" do
+ obj = mock('path')
+ obj.should_receive(:to_str).and_return(Dir.pwd)
+ Dir.chdir(obj) { }
+ end
+
+ it "calls #to_path on the argument if it's not a String" do
+ obj = mock('path')
+ obj.should_receive(:to_path).and_return(Dir.pwd)
+ Dir.chdir(obj)
+ end
+
+ it "prefers #to_path over #to_str" do
+ obj = Class.new do
+ def to_path; Dir.pwd; end
+ def to_str; DirSpecs.mock_dir; end
+ end
+ Dir.chdir(obj.new)
+ Dir.pwd.should == @original
+ end
+
+ it "returns the value of the block when a block is given" do
+ Dir.chdir(@original) { :block_value }.should == :block_value
+ end
+
+ it "defaults to the home directory when given a block but no argument" do
+ skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME'])
+
+ # Windows will return a path with forward slashes for ENV["HOME"] so we have
+ # to compare the route representations returned by Dir.chdir.
+ current_dir = ""
+ Dir.chdir { current_dir = Dir.pwd }
+
+ Dir.chdir(ENV['HOME'])
+ home = Dir.pwd
+ current_dir.should == home
+ end
+
+ it "changes to the specified directory for the duration of the block" do
+ ar = Dir.chdir(DirSpecs.mock_dir) { |dir| [dir, Dir.pwd] }
+ ar.should == [DirSpecs.mock_dir, DirSpecs.mock_dir]
+
+ Dir.pwd.should == @original
+ end
+
+ it "raises an Errno::ENOENT if the directory does not exist" do
+ -> { Dir.chdir DirSpecs.nonexistent }.should.raise(Errno::ENOENT)
+ -> { Dir.chdir(DirSpecs.nonexistent) { } }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises an Errno::ENOENT if the original directory no longer exists" do
+ dir1 = tmp('testdir1')
+ dir2 = tmp('testdir2')
+ Dir.should_not.exist?(dir1)
+ Dir.should_not.exist?(dir2)
+ Dir.mkdir dir1
+ Dir.mkdir dir2
+ begin
+ -> {
+ Dir.chdir dir1 do
+ Dir.chdir(dir2) { Dir.unlink dir1 }
+ end
+ }.should.raise(Errno::ENOENT)
+ ensure
+ Dir.unlink dir1 if Dir.exist?(dir1)
+ Dir.unlink dir2 if Dir.exist?(dir2)
+ end
+ end
+
+ it "always returns to the original directory when given a block" do
+ begin
+ Dir.chdir(DirSpecs.mock_dir) do
+ raise StandardError, "something bad happened"
+ end
+ rescue StandardError
+ end
+
+ Dir.pwd.should == @original
+ end
+end
+
+describe "Dir#chdir" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @original = Dir.pwd
+ end
+
+ after :each do
+ Dir.chdir(@original)
+ end
+
+ it "changes the current working directory to self" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ dir.chdir
+ Dir.pwd.should == DirSpecs.mock_dir
+ ensure
+ dir.close
+ end
+
+ it "changes the current working directory to self for duration of the block when a block is given" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ pwd_in_block = nil
+
+ dir.chdir { pwd_in_block = Dir.pwd }
+
+ pwd_in_block.should == DirSpecs.mock_dir
+ Dir.pwd.should == @original
+ ensure
+ dir.close
+ end
+
+ it "returns 0 when successfully changing directory" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ dir.chdir.should == 0
+ ensure
+ dir.close
+ end
+
+ it "returns the value of the block when a block is given" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ dir.chdir { :block_value }.should == :block_value
+ ensure
+ dir.close
+ end
+
+ platform_is_not :windows do
+ it "does not raise an Errno::ENOENT if the original directory no longer exists" do
+ dir_name1 = tmp('testdir1')
+ dir_name2 = tmp('testdir2')
+ Dir.should_not.exist?(dir_name1)
+ Dir.should_not.exist?(dir_name2)
+ Dir.mkdir dir_name1
+ Dir.mkdir dir_name2
+
+ dir2 = Dir.new(dir_name2)
+
+ begin
+ Dir.chdir(dir_name1) do
+ dir2.chdir { Dir.unlink dir_name1 }
+ end
+ Dir.pwd.should == @original
+ ensure
+ Dir.unlink dir_name1 if Dir.exist?(dir_name1)
+ Dir.unlink dir_name2 if Dir.exist?(dir_name2)
+ end
+ ensure
+ dir2.close
+ end
+ end
+
+ it "always returns to the original directory when given a block" do
+ dir = Dir.new(DirSpecs.mock_dir)
+
+ begin
+ dir.chdir do
+ raise StandardError, "something bad happened"
+ end
+ rescue StandardError
+ end
+
+ Dir.pwd.should == @original
+ ensure
+ dir.close
+ end
+end
diff --git a/spec/ruby/core/dir/children_spec.rb b/spec/ruby/core/dir/children_spec.rb
new file mode 100644
index 0000000000..6e6da1dd44
--- /dev/null
+++ b/spec/ruby/core/dir/children_spec.rb
@@ -0,0 +1,147 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.children" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "returns an Array of filenames in an existing directory including dotfiles" do
+ a = Dir.children(DirSpecs.mock_dir).sort
+
+ a.should == DirSpecs.expected_paths - %w[. ..]
+
+ a = Dir.children("#{DirSpecs.mock_dir}/deeply/nested").sort
+ a.should == %w|.dotfile.ext directory|
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir)
+ Dir.children(p)
+ end
+
+ it "accepts an options Hash" do
+ a = Dir.children("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").sort
+ a.should == %w|.dotfile.ext directory|
+ end
+
+ it "returns children encoded with the filesystem encoding by default" do
+ # This spec depends on the locale not being US-ASCII because if it is, the
+ # children that are not ascii_only? will be BINARY encoded.
+ children = Dir.children(File.join(DirSpecs.mock_dir, 'special')).sort
+ encoding = Encoding.find("filesystem")
+ encoding = Encoding::BINARY if encoding == Encoding::US_ASCII
+ platform_is_not :windows do
+ children.should.include?("ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding))
+ end
+ children.first.encoding.should.equal?(Encoding.find("filesystem"))
+ end
+
+ it "returns children encoded with the specified encoding" do
+ dir = File.join(DirSpecs.mock_dir, 'special')
+ children = Dir.children(dir, encoding: "euc-jp").sort
+ children.first.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns children transcoded to the default internal encoding" do
+ Encoding.default_internal = Encoding::EUC_KR
+ children = Dir.children(File.join(DirSpecs.mock_dir, 'special')).sort
+ children.first.encoding.should.equal?(Encoding::EUC_KR)
+ end
+
+ it "raises a SystemCallError if called with a nonexistent directory" do
+ -> { Dir.children DirSpecs.nonexistent }.should.raise(SystemCallError)
+ end
+end
+
+describe "Dir#children" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ @dir.close if @dir
+ end
+
+ it "returns an Array of filenames in an existing directory including dotfiles" do
+ @dir = Dir.new(DirSpecs.mock_dir)
+ a = @dir.children.sort
+ @dir.close
+
+ a.should == DirSpecs.expected_paths - %w[. ..]
+
+ @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested")
+ a = @dir.children.sort
+ a.should == %w|.dotfile.ext directory|
+ end
+
+ it "accepts an encoding keyword for the encoding of the entries" do
+ @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8")
+ dirs = @dir.to_a.sort
+ dirs.each { |d| d.encoding.should == Encoding::UTF_8 }
+ end
+
+ it "returns children encoded with the filesystem encoding by default" do
+ # This spec depends on the locale not being US-ASCII because if it is, the
+ # children that are not ascii_only? will be BINARY encoded.
+ @dir = Dir.new(File.join(DirSpecs.mock_dir, 'special'))
+ children = @dir.children.sort
+ encoding = Encoding.find("filesystem")
+ encoding = Encoding::BINARY if encoding == Encoding::US_ASCII
+ platform_is_not :windows do
+ children.should.include?("ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding))
+ end
+ children.first.encoding.should.equal?(Encoding.find("filesystem"))
+ end
+
+ it "returns children encoded with the specified encoding" do
+ path = File.join(DirSpecs.mock_dir, 'special')
+ @dir = Dir.new(path, encoding: "euc-jp")
+ children = @dir.children.sort
+ children.first.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns children transcoded to the default internal encoding" do
+ Encoding.default_internal = Encoding::EUC_KR
+ @dir = Dir.new(File.join(DirSpecs.mock_dir, 'special'))
+ children = @dir.children.sort
+ children.first.encoding.should.equal?(Encoding::EUC_KR)
+ end
+
+ it "returns the same result when called repeatedly" do
+ @dir = Dir.open DirSpecs.mock_dir
+
+ a = []
+ @dir.each {|dir| a << dir}
+
+ b = []
+ @dir.each {|dir| b << dir}
+
+ a.sort.should == b.sort
+ a.sort.should == DirSpecs.expected_paths
+ end
+end
diff --git a/spec/ruby/core/dir/chroot_spec.rb b/spec/ruby/core/dir/chroot_spec.rb
new file mode 100644
index 0000000000..79ad9759b0
--- /dev/null
+++ b/spec/ruby/core/dir/chroot_spec.rb
@@ -0,0 +1,47 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/chroot'
+
+platform_is_not :windows do
+ as_superuser do
+ describe "Dir.chroot as root" do
+ it_behaves_like :dir_chroot_as_root, :chroot
+ end
+ end
+
+ platform_is_not :cygwin, :android do
+ as_user do
+ describe "Dir.chroot as regular user" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "raises an Errno::EPERM exception if the directory exists" do
+ -> { Dir.chroot('.') }.should.raise(Errno::EPERM)
+ end
+
+ it "raises a SystemCallError if the directory doesn't exist" do
+ -> { Dir.chroot('xgwhwhsjai2222jg') }.should.raise(SystemCallError)
+ end
+
+ it "calls #to_path on non-String argument" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return('.')
+ -> { Dir.chroot(p) }.should.raise(Errno::EPERM)
+ end
+ end
+ end
+ end
+
+ platform_is :cygwin do
+ as_user do
+ describe "Dir.chroot as regular user" do
+ it_behaves_like :dir_chroot_as_root, :chroot
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/close_spec.rb b/spec/ruby/core/dir/close_spec.rb
new file mode 100644
index 0000000000..9902d98934
--- /dev/null
+++ b/spec/ruby/core/dir/close_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+describe "Dir#close" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "does not raise an IOError even if the Dir instance is closed" do
+ dir = Dir.open DirSpecs.mock_dir
+ dir.close.should == nil
+ dir.close.should == nil
+
+ platform_is_not :windows do
+ -> { dir.fileno }.should.raise(IOError, /closed directory/)
+ end
+ end
+
+ it "returns nil" do
+ dir = Dir.open DirSpecs.mock_dir
+ dir.close.should == nil
+ end
+
+ ruby_version_is ''...'3.4' do
+ platform_is_not :windows do
+ it "does not raise an error even if the file descriptor is closed with another Dir instance" do
+ dir = Dir.open DirSpecs.mock_dir
+ dir_new = Dir.for_fd(dir.fileno)
+
+ dir.close
+ dir_new.close
+
+ -> { dir.fileno }.should.raise(IOError, /closed directory/)
+ -> { dir_new.fileno }.should.raise(IOError, /closed directory/)
+ end
+ end
+ end
+
+ ruby_version_is '3.4' do
+ platform_is_not :windows do
+ it "raises an error if the file descriptor is closed with another Dir instance" do
+ dir = Dir.open DirSpecs.mock_dir
+ dir_new = Dir.for_fd(dir.fileno)
+ dir.close
+
+ -> { dir_new.close }.should.raise(Errno::EBADF, 'Bad file descriptor - closedir')
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/delete_spec.rb b/spec/ruby/core/dir/delete_spec.rb
new file mode 100644
index 0000000000..a0020788ca
--- /dev/null
+++ b/spec/ruby/core/dir/delete_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/delete'
+
+describe "Dir.delete" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it_behaves_like :dir_delete, :delete
+end
diff --git a/spec/ruby/core/dir/dir_spec.rb b/spec/ruby/core/dir/dir_spec.rb
new file mode 100644
index 0000000000..7d55ea26d4
--- /dev/null
+++ b/spec/ruby/core/dir/dir_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Dir" do
+ it "includes Enumerable" do
+ Dir.include?(Enumerable).should == true
+ end
+end
diff --git a/spec/ruby/core/dir/each_child_spec.rb b/spec/ruby/core/dir/each_child_spec.rb
new file mode 100644
index 0000000000..4d6575df39
--- /dev/null
+++ b/spec/ruby/core/dir/each_child_spec.rb
@@ -0,0 +1,119 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.each_child" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "accepts an encoding keyword for the encoding of the entries" do
+ dirs = Dir.each_child("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort
+ dirs.each {|dir| dir.encoding.should == Encoding::UTF_8}
+ end
+
+ it "yields all names in an existing directory to the provided block" do
+ a, b = [], []
+
+ Dir.each_child(DirSpecs.mock_dir) {|f| a << f}
+ Dir.each_child("#{DirSpecs.mock_dir}/deeply/nested") {|f| b << f}
+
+ a.sort.should == DirSpecs.expected_paths - %w[. ..]
+ b.sort.should == %w|.dotfile.ext directory|
+ end
+
+ it "returns nil when successful" do
+ Dir.each_child(DirSpecs.mock_dir) {|f| f}.should == nil
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir)
+ Dir.each_child(p).to_a
+ end
+
+ it "raises a SystemCallError if passed a nonexistent directory" do
+ -> { Dir.each_child(DirSpecs.nonexistent) {} }.should.raise(SystemCallError)
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ Dir.each_child(DirSpecs.mock_dir).should.instance_of?(Enumerator)
+ Dir.each_child(DirSpecs.mock_dir).to_a.sort.should == DirSpecs.expected_paths - %w[. ..]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ Dir.each_child(DirSpecs.mock_dir).size.should == nil
+ end
+ end
+ end
+ end
+end
+
+describe "Dir#each_child" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ after :each do
+ @dir.close if @dir
+ end
+
+ it "yields all names in an existing directory to the provided block" do
+ a, b = [], []
+ @dir = Dir.new(DirSpecs.mock_dir)
+ @dir2 = Dir.new("#{DirSpecs.mock_dir}/deeply/nested")
+
+ @dir.each_child { |f| a << f }
+ @dir2.each_child { |f| b << f }
+ @dir2.close
+
+ a.sort.should == DirSpecs.expected_paths - %w|. ..|
+ b.sort.should == %w|.dotfile.ext directory|
+ end
+
+ it "returns self when successful" do
+ @dir = Dir.new(DirSpecs.mock_dir)
+ @dir.each_child { |f| f }.should == @dir
+ end
+
+ it "returns the same result when called repeatedly" do
+ @dir = Dir.open DirSpecs.mock_dir
+
+ a = []
+ @dir.each {|dir| a << dir}
+
+ b = []
+ @dir.each {|dir| b << dir}
+
+ a.sort.should == b.sort
+ a.sort.should == DirSpecs.expected_paths
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ @dir = Dir.new(DirSpecs.mock_dir)
+
+ @dir.each_child.should.instance_of?(Enumerator)
+ @dir.each_child.to_a.sort.should == DirSpecs.expected_paths - %w|. ..|
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @dir = Dir.new(DirSpecs.mock_dir)
+ @dir.each_child.size.should == nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/each_spec.rb b/spec/ruby/core/dir/each_spec.rb
new file mode 100644
index 0000000000..e997e340b1
--- /dev/null
+++ b/spec/ruby/core/dir/each_spec.rb
@@ -0,0 +1,75 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/closed'
+
+describe "Dir#each" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @dir = Dir.open DirSpecs.mock_dir
+ end
+
+ after :each do
+ @dir.close
+ end
+
+ it "yields each directory entry in succession" do
+ a = []
+ @dir.each {|dir| a << dir}
+
+ a.sort.should == DirSpecs.expected_paths
+ end
+
+ it "returns the directory which remains open" do
+ # an FS does not necessarily impose order
+ ls = Dir.entries(DirSpecs.mock_dir)
+ @dir.each {}.should == @dir
+ @dir.read.should == nil
+ @dir.rewind
+ ls.should.include?(@dir.read)
+ end
+
+ it "returns the same result when called repeatedly" do
+ a = []
+ @dir.each {|dir| a << dir}
+
+ b = []
+ @dir.each {|dir| b << dir}
+
+ a.sort.should == b.sort
+ a.sort.should == DirSpecs.expected_paths
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ @dir.each.should.instance_of?(Enumerator)
+ @dir.each.to_a.sort.should == DirSpecs.expected_paths
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @dir.each.size.should == nil
+ end
+ end
+ end
+ end
+end
+
+describe "Dir#each" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it_behaves_like :dir_closed, :each
+end
diff --git a/spec/ruby/core/dir/element_reference_spec.rb b/spec/ruby/core/dir/element_reference_spec.rb
new file mode 100644
index 0000000000..092114bed4
--- /dev/null
+++ b/spec/ruby/core/dir/element_reference_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/glob'
+
+describe "Dir.[]" do
+ it_behaves_like :dir_glob, :[]
+end
+
+describe "Dir.[]" do
+ it_behaves_like :dir_glob_recursive, :[]
+end
+
+describe "Dir.[]" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ @cwd = Dir.pwd
+ Dir.chdir DirSpecs.mock_dir
+ end
+
+ after :all do
+ Dir.chdir @cwd
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "calls #to_path to convert multiple patterns" do
+ pat1 = mock('file_one.ext')
+ pat1.should_receive(:to_path).and_return('file_one.ext')
+ pat2 = mock('file_two.ext')
+ pat2.should_receive(:to_path).and_return('file_two.ext')
+
+ Dir[pat1, pat2].should == %w[file_one.ext file_two.ext]
+ end
+end
diff --git a/spec/ruby/core/dir/empty_spec.rb b/spec/ruby/core/dir/empty_spec.rb
new file mode 100644
index 0000000000..3b6b2bac85
--- /dev/null
+++ b/spec/ruby/core/dir/empty_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Dir.empty?" do
+ before :all do
+ @empty_dir = tmp("empty_dir")
+ mkdir_p @empty_dir
+ end
+
+ after :all do
+ rm_r @empty_dir
+ end
+
+ it "returns true for empty directories" do
+ result = Dir.empty? @empty_dir
+ result.should == true
+ end
+
+ it "returns false for non-empty directories" do
+ result = Dir.empty? __dir__
+ result.should == false
+ end
+
+ it "returns false for a non-directory" do
+ result = Dir.empty? __FILE__
+ result.should == false
+ end
+
+ it "raises ENOENT for nonexistent directories" do
+ -> { Dir.empty? tmp("nonexistent") }.should.raise(Errno::ENOENT)
+ end
+end
diff --git a/spec/ruby/core/dir/entries_spec.rb b/spec/ruby/core/dir/entries_spec.rb
new file mode 100644
index 0000000000..f3ca49b26d
--- /dev/null
+++ b/spec/ruby/core/dir/entries_spec.rb
@@ -0,0 +1,70 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.entries" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "returns an Array of filenames in an existing directory including dotfiles" do
+ a = Dir.entries(DirSpecs.mock_dir).sort
+
+ a.should == DirSpecs.expected_paths
+
+ a = Dir.entries("#{DirSpecs.mock_dir}/deeply/nested").sort
+ a.should == %w|. .. .dotfile.ext directory|
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir)
+ Dir.entries(p)
+ end
+
+ it "accepts an encoding keyword for the encoding of the entries" do
+ dirs = Dir.entries("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort
+ dirs.each {|dir| dir.encoding.should == Encoding::UTF_8}
+ end
+
+ it "returns entries encoded with the filesystem encoding by default" do
+ # This spec depends on the locale not being US-ASCII because if it is, the
+ # entries that are not ascii_only? will be BINARY encoded.
+ entries = Dir.entries(File.join(DirSpecs.mock_dir, 'special')).sort
+ encoding = Encoding.find("filesystem")
+ encoding = Encoding::BINARY if encoding == Encoding::US_ASCII
+ platform_is_not :windows do
+ entries.should.include?("ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding))
+ end
+ entries.first.encoding.should.equal?(Encoding.find("filesystem"))
+ end
+
+ it "returns entries encoded with the specified encoding" do
+ dir = File.join(DirSpecs.mock_dir, 'special')
+ entries = Dir.entries(dir, encoding: "euc-jp").sort
+ entries.first.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns entries transcoded to the default internal encoding" do
+ Encoding.default_internal = Encoding::EUC_KR
+ entries = Dir.entries(File.join(DirSpecs.mock_dir, 'special')).sort
+ entries.first.encoding.should.equal?(Encoding::EUC_KR)
+ end
+
+ it "raises a SystemCallError if called with a nonexistent directory" do
+ -> { Dir.entries DirSpecs.nonexistent }.should.raise(SystemCallError)
+ end
+end
diff --git a/spec/ruby/core/dir/exist_spec.rb b/spec/ruby/core/dir/exist_spec.rb
new file mode 100644
index 0000000000..05ad67dd03
--- /dev/null
+++ b/spec/ruby/core/dir/exist_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.exist?" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "returns true if the given directory exists" do
+ Dir.exist?(__dir__).should == true
+ end
+
+ it "returns true for '.'" do
+ Dir.exist?('.').should == true
+ end
+
+ it "returns true for '..'" do
+ Dir.exist?('..').should == true
+ end
+
+ it "understands non-ASCII paths" do
+ subdir = File.join(tmp("\u{9876}\u{665}"))
+ Dir.exist?(subdir).should == false
+ Dir.mkdir(subdir)
+ Dir.exist?(subdir).should == true
+ Dir.rmdir(subdir)
+ end
+
+ it "understands relative paths" do
+ Dir.exist?(__dir__ + '/../').should == true
+ end
+
+ it "returns false if the given directory doesn't exist" do
+ Dir.exist?('y26dg27n2nwjs8a/').should == false
+ end
+
+ it "doesn't require the name to have a trailing slash" do
+ dir = __dir__
+ dir.sub!(/\/$/,'')
+ Dir.exist?(dir).should == true
+ end
+
+ it "doesn't expand paths" do
+ skip "$HOME not valid directory" unless ENV['HOME'] && File.directory?(ENV['HOME'])
+ Dir.exist?(File.expand_path('~')).should == true
+ Dir.exist?('~').should == false
+ end
+
+ it "returns false if the argument exists but is a file" do
+ File.should.exist?(__FILE__)
+ Dir.exist?(__FILE__).should == false
+ end
+
+ it "doesn't set $! when file doesn't exist" do
+ Dir.exist?("/path/to/non/existent/dir")
+ $!.should == nil
+ end
+
+ it "calls #to_path on non String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(__dir__)
+ Dir.exist?(p)
+ end
+end
+
+describe "Dir.exists?" do
+ it "has been removed" do
+ Dir.should_not.respond_to?(:exists?)
+ end
+end
diff --git a/spec/ruby/core/dir/fchdir_spec.rb b/spec/ruby/core/dir/fchdir_spec.rb
new file mode 100644
index 0000000000..bd1a92b05e
--- /dev/null
+++ b/spec/ruby/core/dir/fchdir_spec.rb
@@ -0,0 +1,71 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+platform_is_not :windows do
+ describe "Dir.fchdir" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @original = Dir.pwd
+ end
+
+ after :each do
+ Dir.chdir(@original)
+ end
+
+ it "changes the current working directory to the directory specified by the integer file descriptor" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ Dir.fchdir dir.fileno
+ Dir.pwd.should == DirSpecs.mock_dir
+ ensure
+ dir.close
+ end
+
+ it "returns 0 when successfully changing directory" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ Dir.fchdir(dir.fileno).should == 0
+ ensure
+ dir.close
+ end
+
+ it "returns the value of the block when a block is given" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ Dir.fchdir(dir.fileno) { :block_value }.should == :block_value
+ ensure
+ dir.close
+ end
+
+ it "changes to the specified directory for the duration of the block" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ Dir.fchdir(dir.fileno) { Dir.pwd }.should == DirSpecs.mock_dir
+ Dir.pwd.should == @original
+ ensure
+ dir.close
+ end
+
+ it "raises a SystemCallError if the file descriptor given is not valid" do
+ -> { Dir.fchdir(-1) }.should.raise(SystemCallError, "Bad file descriptor - fchdir")
+ -> { Dir.fchdir(-1) { } }.should.raise(SystemCallError, "Bad file descriptor - fchdir")
+ end
+
+ it "raises a SystemCallError if the file descriptor given is not for a directory" do
+ -> { Dir.fchdir $stdout.fileno }.should.raise(SystemCallError, /(Not a directory|Invalid argument) - fchdir/)
+ -> { Dir.fchdir($stdout.fileno) { } }.should.raise(SystemCallError, /(Not a directory|Invalid argument) - fchdir/)
+ end
+ end
+end
+
+platform_is :windows do
+ describe "Dir.fchdir" do
+ it "raises NotImplementedError" do
+ -> { Dir.fchdir 1 }.should.raise(NotImplementedError)
+ -> { Dir.fchdir(1) { } }.should.raise(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/fileno_spec.rb b/spec/ruby/core/dir/fileno_spec.rb
new file mode 100644
index 0000000000..3b563eb18f
--- /dev/null
+++ b/spec/ruby/core/dir/fileno_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+has_dir_fileno = begin
+ dir = Dir.new('.')
+ dir.fileno
+ true
+rescue NotImplementedError
+ false
+rescue Exception
+ true
+ensure
+ dir.close
+end
+
+describe "Dir#fileno" do
+ before :each do
+ @name = tmp("fileno")
+ mkdir_p @name
+ @dir = Dir.new(@name)
+ end
+
+ after :each do
+ @dir.close
+ rm_r @name
+ end
+
+ if has_dir_fileno
+ it "returns the file descriptor of the dir" do
+ @dir.fileno.should.is_a?(Integer)
+ end
+ else
+ it "raises an error when not implemented on the platform" do
+ -> { @dir.fileno }.should.raise(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/fixtures/common.rb b/spec/ruby/core/dir/fixtures/common.rb
new file mode 100644
index 0000000000..cfec91f68f
--- /dev/null
+++ b/spec/ruby/core/dir/fixtures/common.rb
@@ -0,0 +1,203 @@
+# encoding: utf-8
+
+module DirSpecs
+ def self.mock_dir(dirs = ['dir_specs_mock'])
+ @mock_dir ||= tmp("")
+ File.join @mock_dir, dirs
+ end
+
+ def self.nonexistent
+ name = File.join mock_dir, "nonexistent00"
+ name = name.next while File.exist? name
+ name
+ end
+
+ # TODO: make these relative to the mock_dir
+ def self.clear_dirs
+ [ 'nonexisting',
+ 'default_perms',
+ 'reduced',
+ 'always_returns_0',
+ '???',
+ [0xe9].pack('U')
+ ].each do |dir|
+ begin
+ Dir.rmdir mock_dir(dir)
+ rescue
+ end
+ end
+ end
+
+ # The names of the fixture directories and files used by
+ # various Dir specs.
+ def self.mock_dir_files
+ unless @mock_dir_files
+ @mock_dir_files = %w[
+ .dotfile
+ .dotsubdir/.dotfile
+ .dotsubdir/nondotfile
+ nested/.dotsubir/.dotfile
+ nested/.dotsubir/nondotfile
+
+ deeply/.dotfile
+ deeply/nested/.dotfile.ext
+ deeply/nested/directory/structure/.ext
+ deeply/nested/directory/structure/bar
+ deeply/nested/directory/structure/baz
+ deeply/nested/directory/structure/file_one
+ deeply/nested/directory/structure/file_one.ext
+ deeply/nested/directory/structure/foo
+ deeply/nondotfile
+
+ file_one.ext
+ file_two.ext
+
+ dir_filename_ordering
+ dir/filename_ordering
+
+ nondotfile
+
+ subdir_one/.dotfile
+ subdir_one/nondotfile
+ subdir_two/nondotfile
+ subdir_two/nondotfile.ext
+
+ brace/a
+ brace/a.js
+ brace/a.erb
+ brace/a.js.rjs
+ brace/a.html.erb
+
+ special/+
+
+ special/^
+ special/$
+
+ special/(
+ special/)
+ special/[
+ special/]
+ special/{
+ special/}
+
+ special/test{1}/file[1]
+ special/{}/special
+ special/test\ +()[]{}/hello_world.erb
+ ]
+
+ platform_is_not :windows do
+ @mock_dir_files += %w[
+ special/*
+ special/?
+
+ special/|
+
+ special/ã“ã‚“ã«ã¡ã¯.txt
+ special/\a
+ ]
+ @mock_dir_files << "special/_\u{1f60e}.erb"
+ end
+ end
+
+ @mock_dir_files
+ end
+
+ def self.mock_dir_links
+ unless @mock_dir_links
+ @mock_dir_links = []
+ platform_is_not :windows do
+ @mock_dir_links += [
+ ['special/ln', 'subdir_one']
+ ]
+ end
+ end
+ @mock_dir_links
+ end
+
+ def self.create_mock_dirs
+ delete_mock_dirs
+ mock_dir_files.each do |name|
+ file = File.join mock_dir, name
+ mkdir_p File.dirname(file)
+ touch file
+ end
+ mock_dir_links.each do |link, target|
+ full_link = File.join mock_dir, link
+ full_target = File.join mock_dir, target
+
+ File.symlink full_target, full_link
+ end
+ end
+
+ def self.delete_mock_dirs
+ begin
+ rm_r mock_dir
+ rescue Errno::ENOTEMPTY => e
+ puts Dir["#{mock_dir}/**/*"]
+ raise e
+ end
+ end
+
+ def self.mock_rmdir(*dirs)
+ mock_dir ['rmdir_dirs'].concat(dirs)
+ end
+
+ def self.rmdir_dirs(create = true)
+ dirs = %w[
+ empty
+ nonempty
+ nonempty/child
+ noperm
+ noperm/child
+ ]
+
+ base_dir = mock_dir ['rmdir_dirs']
+
+ dirs.reverse_each do |d|
+ dir = File.join base_dir, d
+ if File.exist? dir
+ File.chmod 0777, dir
+ rm_r dir
+ end
+ end
+ rm_r base_dir
+
+ if create
+ dirs.each do |d|
+ dir = File.join base_dir, d
+ unless File.exist? dir
+ mkdir_p dir
+ File.chmod 0777, dir
+ end
+ end
+ end
+ end
+
+ def self.expected_paths_with_type
+ [
+ [".", :directory],
+ ["..", :directory],
+ [".dotfile", :file],
+ [".dotsubdir", :directory],
+ ["brace", :directory],
+ ["deeply", :directory],
+ ["dir", :directory],
+ ["dir_filename_ordering", :file],
+ ["file_one.ext", :file],
+ ["file_two.ext", :file],
+ ["nested", :directory],
+ ["nondotfile", :file],
+ ["special", :directory],
+ ["subdir_one", :directory],
+ ["subdir_two", :directory],
+ ]
+ end
+
+ def self.expected_paths
+ expected_paths_with_type.map(&:first)
+ end
+
+ def self.expected_glob_paths
+ expected_paths - ['..']
+ end
+end
diff --git a/spec/ruby/core/dir/for_fd_spec.rb b/spec/ruby/core/dir/for_fd_spec.rb
new file mode 100644
index 0000000000..bbc75e0f8f
--- /dev/null
+++ b/spec/ruby/core/dir/for_fd_spec.rb
@@ -0,0 +1,77 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+quarantine! do # leads to "Errno::EBADF: Bad file descriptor - closedir" in DirSpecs.delete_mock_dirs
+platform_is_not :windows do
+ describe "Dir.for_fd" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @original = Dir.pwd
+ end
+
+ after :each do
+ Dir.chdir(@original)
+ end
+
+ it "returns a new Dir object representing the directory specified by the given integer directory file descriptor" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ dir_new = Dir.for_fd(dir.fileno)
+
+ dir_new.should.instance_of?(Dir)
+ dir_new.children.should == dir.children
+ dir_new.fileno.should == dir.fileno
+ ensure
+ dir.close
+ end
+
+ it "returns a new Dir object without associated path" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ dir_new = Dir.for_fd(dir.fileno)
+
+ dir_new.path.should == nil
+ ensure
+ dir.close
+ end
+
+ it "calls #to_int to convert a value to an Integer" do
+ dir = Dir.new(DirSpecs.mock_dir)
+ obj = mock("fd")
+ obj.should_receive(:to_int).and_return(dir.fileno)
+
+ dir_new = Dir.for_fd(obj)
+ dir_new.fileno.should == dir.fileno
+ ensure
+ dir.close
+ end
+
+ it "raises TypeError when value cannot be converted to Integer" do
+ -> {
+ Dir.for_fd(nil)
+ }.should raise_consistent_error(TypeError, "no implicit conversion of nil into Integer")
+ end
+
+ it "raises a SystemCallError if the file descriptor given is not valid" do
+ -> { Dir.for_fd(-1) }.should.raise(SystemCallError, "Bad file descriptor - fdopendir")
+ end
+
+ it "raises a SystemCallError if the file descriptor given is not for a directory" do
+ -> { Dir.for_fd $stdout.fileno }.should.raise(SystemCallError, "Not a directory - fdopendir")
+ end
+ end
+end
+
+platform_is :windows do
+ describe "Dir.for_fd" do
+ it "raises NotImplementedError" do
+ -> { Dir.for_fd 1 }.should.raise(NotImplementedError)
+ end
+ end
+end
+end
diff --git a/spec/ruby/core/dir/foreach_spec.rb b/spec/ruby/core/dir/foreach_spec.rb
new file mode 100644
index 0000000000..2a2265a029
--- /dev/null
+++ b/spec/ruby/core/dir/foreach_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.foreach" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "yields all names in an existing directory to the provided block" do
+ a, b = [], []
+
+ Dir.foreach(DirSpecs.mock_dir) {|f| a << f}
+ Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested") {|f| b << f}
+
+ a.sort.should == DirSpecs.expected_paths
+ b.sort.should == %w|. .. .dotfile.ext directory|
+ end
+
+ it "returns nil when successful" do
+ Dir.foreach(DirSpecs.mock_dir) {|f| f}.should == nil
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir)
+ Dir.foreach(p).to_a
+ end
+
+ it "raises a SystemCallError if passed a nonexistent directory" do
+ -> { Dir.foreach(DirSpecs.nonexistent) {} }.should.raise(SystemCallError)
+ end
+
+ it "returns an Enumerator if no block given" do
+ Dir.foreach(DirSpecs.mock_dir).should.instance_of?(Enumerator)
+ Dir.foreach(DirSpecs.mock_dir).to_a.sort.should == DirSpecs.expected_paths
+ end
+
+ it "accepts an encoding keyword for the encoding of the entries" do
+ dirs = Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").to_a.sort
+ dirs.each { |dir| dir.encoding.should == Encoding::UTF_8 }
+
+ dirs = Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: Encoding::ISO_8859_1).to_a.sort
+ dirs.each { |dir| dir.encoding.should == Encoding::ISO_8859_1 }
+
+ Dir.foreach("#{DirSpecs.mock_dir}/deeply/nested", encoding: Encoding::ISO_8859_1) do |f|
+ f.encoding.should == Encoding::ISO_8859_1
+ end
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ Dir.foreach(DirSpecs.mock_dir).should.instance_of?(Enumerator)
+ Dir.foreach(DirSpecs.mock_dir).to_a.sort.should == DirSpecs.expected_paths
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ Dir.foreach(DirSpecs.mock_dir).size.should == nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/getwd_spec.rb b/spec/ruby/core/dir/getwd_spec.rb
new file mode 100644
index 0000000000..138481821f
--- /dev/null
+++ b/spec/ruby/core/dir/getwd_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Dir.getwd" do
+ it "is an alias of Dir.pwd" do
+ Dir.method(:getwd).should == Dir.method(:pwd)
+ end
+end
diff --git a/spec/ruby/core/dir/glob_spec.rb b/spec/ruby/core/dir/glob_spec.rb
new file mode 100644
index 0000000000..9e81feb15f
--- /dev/null
+++ b/spec/ruby/core/dir/glob_spec.rb
@@ -0,0 +1,362 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/glob'
+
+describe "Dir.glob" do
+ it_behaves_like :dir_glob, :glob
+end
+
+describe "Dir.glob" do
+ it_behaves_like :dir_glob_recursive, :glob
+end
+
+describe "Dir.glob" do
+ before :each do
+ DirSpecs.create_mock_dirs
+
+ @cwd = Dir.pwd
+ Dir.chdir DirSpecs.mock_dir
+ end
+
+ after :each do
+ Dir.chdir @cwd
+
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "can take an array of patterns" do
+ Dir.glob(["file_o*", "file_t*"]).should ==
+ %w!file_one.ext file_two.ext!
+ end
+
+ it 'returns matching file paths when supplied :base keyword argument' do
+ dir = tmp('dir_glob_base')
+ file_1 = "#{dir}/lib/bloop.rb"
+ file_2 = "#{dir}/lib/soup.rb"
+ file_3 = "#{dir}/lib/mismatched_file_type.txt"
+ file_4 = "#{dir}/mismatched_directory.rb"
+
+ touch file_1
+ touch file_2
+ touch file_3
+ touch file_4
+
+ Dir.glob('**/*.rb', base: "#{dir}/lib").sort.should == ["bloop.rb", "soup.rb"].sort
+ ensure
+ rm_r dir
+ end
+
+ it "calls #to_path to convert multiple patterns" do
+ pat1 = mock('file_one.ext')
+ pat1.should_receive(:to_path).and_return('file_one.ext')
+ pat2 = mock('file_two.ext')
+ pat2.should_receive(:to_path).and_return('file_two.ext')
+
+ Dir.glob([pat1, pat2]).should == %w[file_one.ext file_two.ext]
+ end
+
+ it "matches both dot and non-dotfiles with '*' and option File::FNM_DOTMATCH" do
+ Dir.glob('*', File::FNM_DOTMATCH).sort.should == DirSpecs.expected_glob_paths
+ end
+
+ it "matches files with any beginning with '*<non-special characters>' and option File::FNM_DOTMATCH" do
+ Dir.glob('*file', File::FNM_DOTMATCH).sort.should == %w|.dotfile nondotfile|.sort
+ end
+
+ it "matches any files in the current directory with '**' and option File::FNM_DOTMATCH" do
+ Dir.glob('**', File::FNM_DOTMATCH).sort.should == DirSpecs.expected_glob_paths
+ end
+
+ it "recursively matches any subdirectories except './' or '../' with '**/' from the current directory and option File::FNM_DOTMATCH" do
+ expected = %w[
+ .dotsubdir/
+ brace/
+ deeply/
+ deeply/nested/
+ deeply/nested/directory/
+ deeply/nested/directory/structure/
+ dir/
+ nested/
+ nested/.dotsubir/
+ special/
+ special/test\ +()[]{}/
+ special/test{1}/
+ special/{}/
+ subdir_one/
+ subdir_two/
+ ]
+
+ Dir.glob('**/', File::FNM_DOTMATCH).sort.should == expected
+ end
+
+ it "recursively matches files and directories in nested dot subdirectory except . with 'nested/**/*' from the current directory and option File::FNM_DOTMATCH" do
+ expected = %w[
+ nested/.
+ nested/.dotsubir
+ nested/.dotsubir/.dotfile
+ nested/.dotsubir/nondotfile
+ ]
+
+ Dir.glob('nested/**/*', File::FNM_DOTMATCH).sort.should == expected.sort
+ end
+
+ # This is a separate case to check **/ coming after a constant
+ # directory as well.
+ it "recursively matches any subdirectories except './' or '../' with '**/' and option File::FNM_DOTMATCH" do
+ expected = %w[
+ ./
+ ./.dotsubdir/
+ ./brace/
+ ./deeply/
+ ./deeply/nested/
+ ./deeply/nested/directory/
+ ./deeply/nested/directory/structure/
+ ./dir/
+ ./nested/
+ ./nested/.dotsubir/
+ ./special/
+ ./special/test\ +()[]{}/
+ ./special/test{1}/
+ ./special/{}/
+ ./subdir_one/
+ ./subdir_two/
+ ]
+
+ Dir.glob('./**/', File::FNM_DOTMATCH).sort.should == expected
+ end
+
+ it "matches a list of paths by concatenating their individual results" do
+ expected = %w[
+ deeply/
+ deeply/nested/
+ deeply/nested/directory/
+ deeply/nested/directory/structure/
+ subdir_two/nondotfile
+ subdir_two/nondotfile.ext
+ ]
+
+ Dir.glob('{deeply/**/,subdir_two/*}').sort.should == expected
+ end
+
+ it "preserves multiple /s before a **" do
+ expected = %w[
+ deeply//nested/directory/structure
+ ]
+
+ Dir.glob('{deeply//**/structure}').sort.should == expected
+ end
+
+ it "accepts a block and yields it with each elements" do
+ ary = []
+ ret = Dir.glob(["file_o*", "file_t*"]) { |t| ary << t }
+ ret.should == nil
+ ary.should == %w!file_one.ext file_two.ext!
+ end
+
+ it "ignores non-dirs when traversing recursively" do
+ touch "spec"
+ Dir.glob("spec/**/*.rb").should == []
+ end
+
+ it "matches nothing when given an empty list of paths" do
+ Dir.glob('{}').should == []
+ end
+
+ it "handles infinite directory wildcards" do
+ Dir.glob('**/**/**').should_not.empty?
+ end
+
+ it "handles **/** with base keyword argument" do
+ Dir.glob('**/**', base: "dir").should == ["filename_ordering"]
+
+ expected = %w[
+ nested
+ nested/directory
+ nested/directory/structure
+ nested/directory/structure/bar
+ nested/directory/structure/baz
+ nested/directory/structure/file_one
+ nested/directory/structure/file_one.ext
+ nested/directory/structure/foo
+ nondotfile
+ ].sort
+
+ Dir.glob('**/**', base: "deeply").sort.should == expected
+ end
+
+ it "handles **/ with base keyword argument" do
+ expected = %w[
+ /
+ directory/
+ directory/structure/
+ ]
+ Dir.glob('**/', base: "deeply/nested").sort.should == expected
+ end
+
+ it "handles **/nondotfile with base keyword argument" do
+ expected = %w[
+ deeply/nondotfile
+ nondotfile
+ subdir_one/nondotfile
+ subdir_two/nondotfile
+ ]
+ Dir.glob('**/nondotfile', base: ".").sort.should == expected
+ end
+
+ it "handles **/nondotfile with base keyword argument and FNM_DOTMATCH" do
+ expected = %w[
+ .dotsubdir/nondotfile
+ deeply/nondotfile
+ nested/.dotsubir/nondotfile
+ nondotfile
+ subdir_one/nondotfile
+ subdir_two/nondotfile
+ ]
+ Dir.glob('**/nondotfile', File::FNM_DOTMATCH, base: ".").sort.should == expected
+ end
+
+ it "handles **/.dotfile with base keyword argument" do
+ expected = %w[
+ .dotfile
+ deeply/.dotfile
+ subdir_one/.dotfile
+ ]
+ Dir.glob('**/.dotfile', base: ".").sort.should == expected
+ end
+
+ it "handles **/.dotfile with base keyword argument and FNM_DOTMATCH" do
+ expected = %w[
+ .dotfile
+ .dotsubdir/.dotfile
+ deeply/.dotfile
+ nested/.dotsubir/.dotfile
+ subdir_one/.dotfile
+ ]
+ Dir.glob('**/.dotfile', File::FNM_DOTMATCH, base: ".").sort.should == expected
+ end
+
+ it "handles **/.* with base keyword argument" do
+ expected = %w[
+ .dotfile.ext
+ directory/structure/.ext
+ ].sort
+
+ Dir.glob('**/.*', base: "deeply/nested").sort.should == expected
+ end
+
+ it "handles **/.* with base keyword argument and FNM_DOTMATCH" do
+ expected = %w[
+ .
+ .dotfile.ext
+ directory/structure/.ext
+ ].sort
+
+ Dir.glob('**/.*', File::FNM_DOTMATCH, base: "deeply/nested").sort.should == expected
+ end
+
+ it "handles **/** with base keyword argument and FNM_DOTMATCH" do
+ expected = %w[
+ .
+ .dotfile.ext
+ directory
+ directory/structure
+ directory/structure/.ext
+ directory/structure/bar
+ directory/structure/baz
+ directory/structure/file_one
+ directory/structure/file_one.ext
+ directory/structure/foo
+ ].sort
+
+ Dir.glob('**/**', File::FNM_DOTMATCH, base: "deeply/nested").sort.should == expected
+ end
+
+ it "handles **/*pattern* with base keyword argument and FNM_DOTMATCH" do
+ expected = %w[
+ .dotfile.ext
+ directory/structure/file_one
+ directory/structure/file_one.ext
+ ]
+
+ Dir.glob('**/*file*', File::FNM_DOTMATCH, base: "deeply/nested").sort.should == expected
+ end
+
+ it "handles **/glob with base keyword argument and FNM_EXTGLOB" do
+ expected = %w[
+ directory/structure/bar
+ directory/structure/file_one
+ directory/structure/file_one.ext
+ ]
+
+ Dir.glob('**/*{file,bar}*', File::FNM_EXTGLOB, base: "deeply/nested").sort.should == expected
+ end
+
+ it "handles simple filename patterns" do
+ Dir.glob('.dotfile').should == ['.dotfile']
+ end
+
+ it "handles simple directory patterns" do
+ Dir.glob('.dotsubdir/').should == ['.dotsubdir/']
+ end
+
+ it "handles simple directory patterns applied to non-directories" do
+ Dir.glob('nondotfile/').should == []
+ end
+
+ platform_is_not(:windows) do
+ it "matches the literal character '\\' with option File::FNM_NOESCAPE" do
+ Dir.mkdir 'foo?bar'
+
+ begin
+ Dir.glob('foo?bar', File::FNM_NOESCAPE).should == %w|foo?bar|
+ Dir.glob('foo\?bar', File::FNM_NOESCAPE).should == []
+ ensure
+ Dir.rmdir 'foo?bar'
+ end
+
+ Dir.mkdir 'foo\?bar'
+
+ begin
+ Dir.glob('foo\?bar', File::FNM_NOESCAPE).should == %w|foo\\?bar|
+ ensure
+ Dir.rmdir 'foo\?bar'
+ end
+ end
+
+ it "returns nil for directories current user has no permission to read" do
+ Dir.mkdir('no_permission')
+ File.chmod(0, 'no_permission')
+
+ begin
+ Dir.glob('no_permission/*').should == []
+ ensure
+ Dir.rmdir('no_permission')
+ end
+ end
+
+ it "will follow symlinks when processing a `*/` pattern." do
+ expected = ['special/ln/nondotfile']
+ Dir.glob('special/*/nondotfile').should == expected
+ end
+
+ it "will not follow symlinks when recursively traversing directories" do
+ expected = %w[
+ deeply/nondotfile
+ nondotfile
+ subdir_one/nondotfile
+ subdir_two/nondotfile
+ ]
+ Dir.glob('**/nondotfile').sort.should == expected
+ end
+
+ it "will follow symlinks when testing directory after recursive directory in pattern" do
+ expected = %w[
+ deeply/nondotfile
+ special/ln/nondotfile
+ subdir_one/nondotfile
+ subdir_two/nondotfile
+ ]
+ Dir.glob('**/*/nondotfile').sort.should == expected
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/home_spec.rb b/spec/ruby/core/dir/home_spec.rb
new file mode 100644
index 0000000000..f0b20e0687
--- /dev/null
+++ b/spec/ruby/core/dir/home_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.home" do
+ before :each do
+ @home = ENV['HOME']
+ ENV['HOME'] = "/rubyspec_home"
+ end
+
+ after :each do
+ ENV['HOME'] = @home
+ end
+
+ describe "when called without arguments" do
+ it "returns the current user's home directory, reading $HOME first" do
+ Dir.home.should == "/rubyspec_home"
+ end
+
+ it "returns a non-frozen string" do
+ Dir.home.should_not.frozen?
+ end
+
+ it "returns a string with the filesystem encoding" do
+ Dir.home.encoding.should == Encoding.find("filesystem")
+ end
+
+ platform_is_not :windows do
+ it "works even if HOME is unset" do
+ ENV.delete('HOME')
+ Dir.home.should.start_with?('/')
+ Dir.home.encoding.should == Encoding.find("filesystem")
+ end
+ end
+
+ platform_is :windows do
+ it "returns the home directory with forward slashs and as UTF-8" do
+ ENV['HOME'] = "C:\\rubyspäc\\home"
+ home = Dir.home
+ home.should == "C:/rubyspäc/home"
+ home.encoding.should == Encoding::UTF_8
+ end
+
+ it "retrieves the directory from HOME, USERPROFILE, HOMEDRIVE/HOMEPATH and the WinAPI in that order" do
+ old_dirs = [ENV.delete('HOME'), ENV.delete('USERPROFILE'), ENV.delete('HOMEDRIVE'), ENV.delete('HOMEPATH')]
+
+ Dir.home.should == old_dirs[1].gsub("\\", "/")
+ ENV['HOMEDRIVE'] = "C:"
+ ENV['HOMEPATH'] = "\\rubyspec\\home1"
+ Dir.home.should == "C:/rubyspec/home1"
+ ENV['USERPROFILE'] = "C:\\rubyspec\\home2"
+ Dir.home.should == "C:/rubyspec/home2"
+ ENV['HOME'] = "C:\\rubyspec\\home3"
+ Dir.home.should == "C:/rubyspec/home3"
+ ensure
+ ENV['HOME'], ENV['USERPROFILE'], ENV['HOMEDRIVE'], ENV['HOMEPATH'] = *old_dirs
+ end
+ end
+ end
+
+ describe "when called with the current user name" do
+ platform_is_not :windows, :android, :wasi do
+ it "returns the named user's home directory, from the user database" do
+ Dir.home(ENV['USER']).should == `echo ~#{ENV['USER']}`.chomp
+ end
+ end
+
+ it "returns a non-frozen string" do
+ Dir.home(ENV['USER']).should_not.frozen?
+ end
+
+ it "returns a string with the filesystem encoding" do
+ Dir.home(ENV['USER']).encoding.should == Encoding.find("filesystem")
+ end
+ end
+
+ it "raises an ArgumentError if the named user doesn't exist" do
+ -> { Dir.home('geuw2n288dh2k') }.should.raise(ArgumentError)
+ end
+
+ describe "when called with a nil user name" do
+ it "returns the current user's home directory, reading $HOME first" do
+ Dir.home(nil).should == "/rubyspec_home"
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/initialize_spec.rb b/spec/ruby/core/dir/initialize_spec.rb
new file mode 100644
index 0000000000..547b7dc18e
--- /dev/null
+++ b/spec/ruby/core/dir/initialize_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir#initialize" do
+ before :each do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :each do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.stub!(:to_path).and_return(DirSpecs.mock_dir)
+ dir = Dir.new(p)
+ begin
+ dir.path.should == DirSpecs.mock_dir
+ ensure
+ dir.close
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/inspect_spec.rb b/spec/ruby/core/dir/inspect_spec.rb
new file mode 100644
index 0000000000..eabaa54ce0
--- /dev/null
+++ b/spec/ruby/core/dir/inspect_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir#inspect" do
+ before :each do
+ @dir = Dir.new(Dir.getwd)
+ end
+
+ after :each do
+ @dir.close
+ end
+
+ it "returns a String" do
+ @dir.inspect.should.instance_of?(String)
+ end
+
+ it "includes the class name" do
+ @dir.inspect.should =~ /Dir/
+ end
+
+ it "includes the directory name" do
+ @dir.inspect.should.include?(Dir.getwd)
+ end
+end
diff --git a/spec/ruby/core/dir/mkdir_spec.rb b/spec/ruby/core/dir/mkdir_spec.rb
new file mode 100644
index 0000000000..37513e417a
--- /dev/null
+++ b/spec/ruby/core/dir/mkdir_spec.rb
@@ -0,0 +1,107 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.mkdir" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "creates the named directory with the given permissions" do
+ DirSpecs.clear_dirs
+
+ nonexisting = DirSpecs.mock_dir('nonexisting')
+ default_perms = DirSpecs.mock_dir('default_perms')
+ reduced = DirSpecs.mock_dir('reduced')
+ begin
+ File.should_not.exist?(nonexisting)
+ Dir.mkdir nonexisting
+ File.should.exist?(nonexisting)
+ platform_is_not :windows do
+ Dir.mkdir default_perms
+ a = File.stat(default_perms).mode
+ Dir.mkdir reduced, (a - 1)
+ File.stat(reduced).mode.should_not == a
+ end
+ platform_is :windows do
+ Dir.mkdir default_perms, 0666
+ a = File.stat(default_perms).mode
+ Dir.mkdir reduced, 0444
+ File.stat(reduced).mode.should_not == a
+ end
+
+ always_returns_0 = DirSpecs.mock_dir('always_returns_0')
+ Dir.mkdir(always_returns_0).should == 0
+ platform_is_not(:windows) do
+ File.chmod(0777, nonexisting, default_perms, reduced, always_returns_0)
+ end
+ platform_is_not(:windows) do
+ File.chmod(0644, nonexisting, default_perms, reduced, always_returns_0)
+ end
+ ensure
+ DirSpecs.clear_dirs
+ end
+ end
+
+ it "calls #to_path on non-String path arguments" do
+ DirSpecs.clear_dirs
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir('nonexisting'))
+ Dir.mkdir(p)
+ DirSpecs.clear_dirs
+ end
+
+ it "calls #to_int on non-Integer permissions argument" do
+ DirSpecs.clear_dirs
+ path = DirSpecs.mock_dir('nonexisting')
+ permissions = mock('permissions')
+ permissions.should_receive(:to_int).and_return(0666)
+ Dir.mkdir(path, permissions)
+ DirSpecs.clear_dirs
+ end
+
+ it "raises TypeError if non-Integer permissions argument does not have #to_int method" do
+ path = DirSpecs.mock_dir('nonexisting')
+ permissions = Object.new
+
+ -> { Dir.mkdir(path, permissions) }.should.raise(TypeError, 'no implicit conversion of Object into Integer')
+ end
+
+ it "raises a SystemCallError if any of the directories in the path before the last does not exist" do
+ -> { Dir.mkdir "#{DirSpecs.nonexistent}/subdir" }.should.raise(SystemCallError)
+ end
+
+ it "raises Errno::EEXIST if the specified directory already exists" do
+ -> { Dir.mkdir("#{DirSpecs.mock_dir}/dir") }.should.raise(Errno::EEXIST)
+ end
+
+ it "raises Errno::EEXIST if the argument points to the existing file" do
+ -> { Dir.mkdir("#{DirSpecs.mock_dir}/file_one.ext") }.should.raise(Errno::EEXIST)
+ end
+end
+
+# The permissions flag are not supported on Windows as stated in documentation:
+# The permissions may be modified by the value of File.umask, and are ignored on NT.
+platform_is_not :windows do
+ as_user do
+ describe "Dir.mkdir" do
+ before :each do
+ @dir = tmp "noperms"
+ end
+
+ after :each do
+ File.chmod 0777, @dir
+ rm_r @dir
+ end
+
+ it "raises a SystemCallError when lacking adequate permissions in the parent dir" do
+ Dir.mkdir @dir, 0000
+
+ -> { Dir.mkdir "#{@dir}/subdir" }.should.raise(SystemCallError)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/open_spec.rb b/spec/ruby/core/dir/open_spec.rb
new file mode 100644
index 0000000000..be01638fbc
--- /dev/null
+++ b/spec/ruby/core/dir/open_spec.rb
@@ -0,0 +1,84 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.open" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "returns a Dir instance representing the specified directory" do
+ dir = Dir.open(DirSpecs.mock_dir)
+ dir.should.is_a?(Dir)
+ dir.close
+ end
+
+ it "raises a SystemCallError if the directory does not exist" do
+ -> do
+ Dir.open(DirSpecs.nonexistent)
+ end.should.raise(SystemCallError)
+ end
+
+ it "may take a block which is yielded to with the Dir instance" do
+ Dir.open(DirSpecs.mock_dir) {|dir| dir.should.is_a?(Dir)}
+ end
+
+ it "returns the value of the block if a block is given" do
+ Dir.open(DirSpecs.mock_dir) {|dir| :value }.should == :value
+ end
+
+ it "closes the Dir instance when the block exits if given a block" do
+ closed_dir = Dir.open(DirSpecs.mock_dir) { |dir| dir }
+ -> { closed_dir.read }.should.raise(IOError)
+ end
+
+ it "closes the Dir instance when the block exits the block even due to an exception" do
+ closed_dir = nil
+
+ -> do
+ Dir.open(DirSpecs.mock_dir) do |dir|
+ closed_dir = dir
+ raise "dir specs"
+ end
+ end.should.raise(RuntimeError, "dir specs")
+
+ -> { closed_dir.read }.should.raise(IOError)
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir)
+ Dir.open(p) { true }
+ end
+
+ it "accepts an options Hash" do
+ dir = Dir.open(DirSpecs.mock_dir, encoding: "utf-8") {|d| d }
+ dir.should.is_a?(Dir)
+ end
+
+ it "calls #to_hash to convert the options object" do
+ options = mock("dir_open")
+ options.should_receive(:to_hash).and_return({ encoding: Encoding::UTF_8 })
+
+ dir = Dir.open(DirSpecs.mock_dir, **options) {|d| d }
+ dir.should.is_a?(Dir)
+ end
+
+ it "ignores the :encoding option if it is nil" do
+ dir = Dir.open(DirSpecs.mock_dir, encoding: nil) {|d| d }
+ dir.should.is_a?(Dir)
+ end
+
+ platform_is_not :windows do
+ it 'sets the close-on-exec flag for the directory file descriptor' do
+ Dir.open(DirSpecs.mock_dir) do |dir|
+ io = IO.for_fd(dir.fileno)
+ io.autoclose = false
+ io.should.close_on_exec?
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/path_spec.rb b/spec/ruby/core/dir/path_spec.rb
new file mode 100644
index 0000000000..02ddd2cd42
--- /dev/null
+++ b/spec/ruby/core/dir/path_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir#path" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "returns the path that was supplied to .new or .open" do
+ dir = Dir.open DirSpecs.mock_dir
+ begin
+ dir.path.should == DirSpecs.mock_dir
+ ensure
+ dir.close rescue nil
+ end
+ end
+
+ it "returns the path even when called on a closed Dir instance" do
+ dir = Dir.open DirSpecs.mock_dir
+ dir.close
+ dir.path.should == DirSpecs.mock_dir
+ end
+
+ it "returns a String with the same encoding as the argument to .open" do
+ path = DirSpecs.mock_dir.force_encoding Encoding::IBM866
+ dir = Dir.open path
+ begin
+ dir.path.encoding.should.equal?(Encoding::IBM866)
+ ensure
+ dir.close
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/pos_spec.rb b/spec/ruby/core/dir/pos_spec.rb
new file mode 100644
index 0000000000..f8ca06d778
--- /dev/null
+++ b/spec/ruby/core/dir/pos_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/pos'
+
+describe "Dir#pos" do
+ it "is an alias of Dir#tell" do
+ Dir.instance_method(:pos).should == Dir.instance_method(:tell)
+ end
+end
+
+describe "Dir#pos=" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it_behaves_like :dir_pos_set, :pos=
+end
diff --git a/spec/ruby/core/dir/pwd_spec.rb b/spec/ruby/core/dir/pwd_spec.rb
new file mode 100644
index 0000000000..70208b03d6
--- /dev/null
+++ b/spec/ruby/core/dir/pwd_spec.rb
@@ -0,0 +1,80 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Dir.pwd" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @fs_encoding = Encoding.find('filesystem')
+ end
+
+ it "returns the current working directory" do
+ pwd = Dir.pwd
+
+ File.directory?(pwd).should == true
+
+ # On ubuntu gutsy, for example, /bin/pwd does not
+ # understand -P. With just `pwd -P`, /bin/pwd is run.
+
+ # The following uses inode rather than file names to account for
+ # case insensitive file systems like default OS/X file systems
+ platform_is_not :windows do
+ File.stat(pwd).ino.should == File.stat(`/bin/sh -c "pwd -P"`.chomp).ino
+ end
+ platform_is :windows do
+ File.stat(pwd).ino.should == File.stat(File.expand_path(`cd`.chomp)).ino
+ end
+ end
+
+ it "returns an absolute path" do
+ pwd = Dir.pwd
+ pwd.should == File.expand_path(pwd)
+ end
+
+ it "returns an absolute path even when chdir to a relative path" do
+ Dir.chdir(".") do
+ pwd = Dir.pwd
+ File.directory?(pwd).should == true
+ pwd.should == File.expand_path(pwd)
+ end
+ end
+
+ it "returns a String with the filesystem encoding" do
+ enc = Dir.pwd.encoding
+ if @fs_encoding == Encoding::US_ASCII
+ [Encoding::US_ASCII, Encoding::BINARY].should.include?(enc)
+ else
+ enc.should.equal?(@fs_encoding)
+ end
+ end
+end
+
+describe "Dir.pwd" do
+ before :each do
+ @name = tmp("ã‚").force_encoding('binary')
+ @fs_encoding = Encoding.find('filesystem')
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ platform_is_not :windows do
+ it "correctly handles dirs with unicode characters in them" do
+ Dir.mkdir @name
+ Dir.chdir @name do
+ if @fs_encoding == Encoding::UTF_8
+ Dir.pwd.encoding.should == Encoding::UTF_8
+ end
+ Dir.pwd.force_encoding('binary').should == @name
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/read_spec.rb b/spec/ruby/core/dir/read_spec.rb
new file mode 100644
index 0000000000..3f842457f4
--- /dev/null
+++ b/spec/ruby/core/dir/read_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/closed'
+
+describe "Dir#read" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "returns the file name in the current seek position" do
+ # an FS does not necessarily impose order
+ ls = Dir.entries DirSpecs.mock_dir
+ dir = Dir.open DirSpecs.mock_dir
+ ls.should.include?(dir.read)
+ dir.close
+ end
+
+ it "returns nil when there are no more entries" do
+ dir = Dir.open DirSpecs.mock_dir
+ DirSpecs.expected_paths.size.times do
+ dir.read.should_not == nil
+ end
+ dir.read.should == nil
+ dir.close
+ end
+
+ it "returns each entry successively" do
+ dir = Dir.open DirSpecs.mock_dir
+ entries = []
+ while entry = dir.read
+ entries << entry
+ end
+ dir.close
+
+ entries.sort.should == DirSpecs.expected_paths
+ end
+
+ platform_is_not :windows do
+ it "returns all directory entries even when encoding conversion will fail" do
+ dir = Dir.open(File.join(DirSpecs.mock_dir, 'special'))
+ utf8_entries = []
+ begin
+ while entry = dir.read
+ utf8_entries << entry
+ end
+ ensure
+ dir.close
+ end
+ old_internal_encoding = Encoding::default_internal
+ old_external_encoding = Encoding::default_external
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::SHIFT_JIS
+ shift_jis_entries = []
+ begin
+ Dir.open(File.join(DirSpecs.mock_dir, 'special')) do |d|
+ -> {
+ while entry = d.read
+ shift_jis_entries << entry
+ end
+ }.should_not.raise
+ end
+ ensure
+ Encoding.default_internal = old_internal_encoding
+ Encoding.default_external = old_external_encoding
+ end
+ shift_jis_entries.size.should == utf8_entries.size
+ shift_jis_entries.filter { |f| f.encoding == Encoding::SHIFT_JIS }.size.should == 1
+ end
+ end
+
+ it_behaves_like :dir_closed, :read
+end
diff --git a/spec/ruby/core/dir/rewind_spec.rb b/spec/ruby/core/dir/rewind_spec.rb
new file mode 100644
index 0000000000..220d7f5372
--- /dev/null
+++ b/spec/ruby/core/dir/rewind_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/closed'
+
+describe "Dir#rewind" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @dir = Dir.open DirSpecs.mock_dir
+ end
+
+ after :each do
+ @dir.close
+ end
+
+ it "resets the next read to start from the first entry" do
+ a = @dir.read
+ b = @dir.read
+ a.should_not == b
+ @dir.rewind
+ c = @dir.read
+ c.should == a
+ end
+
+ it "returns the Dir instance" do
+ @dir.rewind.should == @dir
+ end
+
+ it_behaves_like :dir_closed, :rewind
+end
diff --git a/spec/ruby/core/dir/rmdir_spec.rb b/spec/ruby/core/dir/rmdir_spec.rb
new file mode 100644
index 0000000000..08cd1a5bc6
--- /dev/null
+++ b/spec/ruby/core/dir/rmdir_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/delete'
+
+describe "Dir.rmdir" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it_behaves_like :dir_delete, :rmdir
+end
diff --git a/spec/ruby/core/dir/scan_spec.rb b/spec/ruby/core/dir/scan_spec.rb
new file mode 100644
index 0000000000..3aa071337b
--- /dev/null
+++ b/spec/ruby/core/dir/scan_spec.rb
@@ -0,0 +1,224 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative '../file/fixtures/file_types'
+
+ruby_version_is "4.1" do
+ describe "Dir.scan" do
+ before :all do
+ FileSpecs.configure_types
+ end
+
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "returns an Array of filename and type pairs in an existing directory including dotfiles" do
+ a = Dir.scan(DirSpecs.mock_dir).sort
+
+ a.should == DirSpecs.expected_paths_with_type - [[".", :directory], ["..", :directory]]
+
+ a = Dir.scan("#{DirSpecs.mock_dir}/deeply/nested").sort
+ a.should == [[".dotfile.ext", :file], ["directory", :directory]]
+ end
+
+ it "yields filename and type in an existing directory including dotfiles" do
+ a = []
+ Dir.scan(DirSpecs.mock_dir) do |n, t|
+ a << [n, t]
+ end
+ a.sort!
+ a.should == DirSpecs.expected_paths_with_type - [[".", :directory], ["..", :directory]]
+
+ a = []
+ Dir.scan("#{DirSpecs.mock_dir}/deeply/nested") do |n, t|
+ a << [n, t]
+ end
+ a.sort!
+ a.should == [[".dotfile.ext", :file], ["directory", :directory]]
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_dir)
+ Dir.scan(p)
+ end
+
+ it "accepts an options Hash" do
+ a = Dir.scan("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8").sort
+ a.should == [[".dotfile.ext", :file], ["directory", :directory]]
+ end
+
+ it "returns children names encoded with the filesystem encoding by default" do
+ # This spec depends on the locale not being US-ASCII because if it is, the
+ # children that are not ascii_only? will be BINARY encoded.
+ children = Dir.scan(File.join(DirSpecs.mock_dir, 'special')).sort
+ encoding = Encoding.find("filesystem")
+ encoding = Encoding::BINARY if encoding == Encoding::US_ASCII
+ platform_is_not :windows do
+ children.should.include?(["ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding), :file])
+ end
+ children.first.first.encoding.should.equal?(Encoding.find("filesystem"))
+ end
+
+ it "returns children names encoded with the specified encoding" do
+ dir = File.join(DirSpecs.mock_dir, 'special')
+ children = Dir.scan(dir, encoding: "euc-jp").sort
+ children.first.first.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns children names transcoded to the default internal encoding" do
+ Encoding.default_internal = Encoding::EUC_KR
+ children = Dir.scan(File.join(DirSpecs.mock_dir, 'special')).sort
+ children.first.first.encoding.should.equal?(Encoding::EUC_KR)
+ end
+
+ it "raises a SystemCallError if called with a nonexistent directory" do
+ -> { Dir.scan DirSpecs.nonexistent }.should.raise(SystemCallError)
+ end
+
+ it "handles symlink" do
+ FileSpecs.symlink do |path|
+ Dir.scan(File.dirname(path)).map(&:last).should.include?(:link)
+ end
+ end
+
+ platform_is_not :windows do
+ it "handles socket" do
+ FileSpecs.socket do |path|
+ Dir.scan(File.dirname(path)).map(&:last).should.include?(:socket)
+ end
+ end
+
+ it "handles FIFO" do
+ FileSpecs.fifo do |path|
+ Dir.scan(File.dirname(path)).map(&:last).should.include?(:fifo)
+ end
+ end
+
+ it "handles character devices" do
+ FileSpecs.character_device do |path|
+ Dir.scan(File.dirname(path)).map(&:last).should.include?(:characterSpecial)
+ end
+ end
+ end
+
+ platform_is_not :freebsd, :windows do
+ with_block_device do
+ it "handles block devices" do
+ FileSpecs.block_device do |path|
+ Dir.scan(File.dirname(path)).map(&:last).should.include?(:blockSpecial)
+ end
+ end
+ end
+ end
+ end
+
+ describe "Dir#scan" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ @dir.close if @dir
+ end
+
+ it "returns an Array of filenames in an existing directory including dotfiles" do
+ @dir = Dir.new(DirSpecs.mock_dir)
+ a = @dir.scan.sort
+ @dir.close
+
+ a.should == DirSpecs.expected_paths_with_type - [[".", :directory], ["..", :directory]]
+
+ @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested")
+ a = @dir.scan.sort
+ a.should == [[".dotfile.ext", :file], ["directory", :directory]]
+ end
+
+ it "yields filename and type in an existing directory including dotfiles" do
+ @dir = Dir.new(DirSpecs.mock_dir)
+ a = []
+ @dir.scan do |n, t|
+ a << [n, t]
+ end
+ a.sort!
+ a.should == DirSpecs.expected_paths_with_type - [[".", :directory], ["..", :directory]]
+
+ @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested")
+ a = []
+ @dir.scan do |n, t|
+ a << [n, t]
+ end
+ a.sort!
+ a.should == [[".dotfile.ext", :file], ["directory", :directory]]
+ end
+
+ it "accepts an encoding keyword for the encoding of the entries" do
+ @dir = Dir.new("#{DirSpecs.mock_dir}/deeply/nested", encoding: "utf-8")
+ dirs = @dir.to_a.sort
+ dirs.each { |d| d.encoding.should == Encoding::UTF_8 }
+ end
+
+ it "returns children names encoded with the filesystem encoding by default" do
+ # This spec depends on the locale not being US-ASCII because if it is, the
+ # children that are not ascii_only? will be BINARY encoded.
+ @dir = Dir.new(File.join(DirSpecs.mock_dir, 'special'))
+ children = @dir.scan.sort
+ encoding = Encoding.find("filesystem")
+ encoding = Encoding::BINARY if encoding == Encoding::US_ASCII
+ platform_is_not :windows do
+ children.should.include?(["ã“ã‚“ã«ã¡ã¯.txt".dup.force_encoding(encoding), :file])
+ end
+ children.first.first.encoding.should.equal?(Encoding.find("filesystem"))
+ end
+
+ it "returns children names encoded with the specified encoding" do
+ path = File.join(DirSpecs.mock_dir, 'special')
+ @dir = Dir.new(path, encoding: "euc-jp")
+ children = @dir.children.sort
+ children.first.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns children names transcoded to the default internal encoding" do
+ Encoding.default_internal = Encoding::EUC_KR
+ @dir = Dir.new(File.join(DirSpecs.mock_dir, 'special'))
+ children = @dir.scan.sort
+ children.first.first.encoding.should.equal?(Encoding::EUC_KR)
+ end
+
+ it "returns the same result when called repeatedly" do
+ @dir = Dir.open DirSpecs.mock_dir
+
+ a = []
+ @dir.each {|dir| a << dir}
+
+ b = []
+ @dir.each {|dir| b << dir}
+
+ a.sort.should == b.sort
+ a.sort.should == DirSpecs.expected_paths
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/seek_spec.rb b/spec/ruby/core/dir/seek_spec.rb
new file mode 100644
index 0000000000..ed409897cd
--- /dev/null
+++ b/spec/ruby/core/dir/seek_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/pos'
+
+describe "Dir#seek" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "returns the Dir instance" do
+ @dir.seek(@dir.pos).should == @dir
+ end
+
+ it_behaves_like :dir_pos_set, :seek
+end
diff --git a/spec/ruby/core/dir/shared/chroot.rb b/spec/ruby/core/dir/shared/chroot.rb
new file mode 100644
index 0000000000..e4e6103799
--- /dev/null
+++ b/spec/ruby/core/dir/shared/chroot.rb
@@ -0,0 +1,44 @@
+describe :dir_chroot_as_root, shared: true do
+ before :all do
+ DirSpecs.create_mock_dirs
+
+ @real_root = "../" * (__dir__.count('/') - 1)
+ @ref_dir = File.join("/", File.basename(Dir["/*"].first))
+ end
+
+ after :all do
+ until File.exist?(@ref_dir)
+ Dir.send(@method, "../") or break
+ end
+
+ DirSpecs.delete_mock_dirs
+ end
+
+ # Pending until https://github.com/ruby/ruby/runs/8075149420 is fixed
+ compilations_ci = ENV["GITHUB_WORKFLOW"] == "Compilations"
+
+ it "can be used to change the process' root directory" do
+ -> { Dir.send(@method, __dir__) }.should_not.raise
+ File.should.exist?("/#{File.basename(__FILE__)}")
+ end unless compilations_ci
+
+ it "returns 0 if successful" do
+ Dir.send(@method, '/').should == 0
+ end
+
+ it "raises an Errno::ENOENT exception if the directory doesn't exist" do
+ -> { Dir.send(@method, 'xgwhwhsjai2222jg') }.should.raise(Errno::ENOENT)
+ end
+
+ it "can be escaped from with ../" do
+ Dir.send(@method, @real_root)
+ File.should.exist?(@ref_dir)
+ File.should_not.exist?("/#{File.basename(__FILE__)}")
+ end unless compilations_ci
+
+ it "calls #to_path on non-String argument" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(@real_root)
+ Dir.send(@method, p)
+ end
+end
diff --git a/spec/ruby/core/dir/shared/closed.rb b/spec/ruby/core/dir/shared/closed.rb
new file mode 100644
index 0000000000..c868fd6e6d
--- /dev/null
+++ b/spec/ruby/core/dir/shared/closed.rb
@@ -0,0 +1,9 @@
+describe :dir_closed, shared: true do
+ it "raises an IOError when called on a closed Dir instance" do
+ -> {
+ dir = Dir.open DirSpecs.mock_dir
+ dir.close
+ dir.send(@method) {}
+ }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/dir/shared/delete.rb b/spec/ruby/core/dir/shared/delete.rb
new file mode 100644
index 0000000000..ba013e8615
--- /dev/null
+++ b/spec/ruby/core/dir/shared/delete.rb
@@ -0,0 +1,53 @@
+describe :dir_delete, shared: true do
+ before :each do
+ DirSpecs.rmdir_dirs true
+ end
+
+ after :each do
+ DirSpecs.rmdir_dirs false
+ end
+
+ it "removes empty directories" do
+ Dir.send(@method, DirSpecs.mock_rmdir("empty")).should == 0
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(DirSpecs.mock_rmdir("empty"))
+ Dir.send(@method, p)
+ end
+
+ it "raises an Errno::ENOTEMPTY when trying to remove a nonempty directory" do
+ -> do
+ Dir.send @method, DirSpecs.mock_rmdir("nonempty")
+ end.should.raise(Errno::ENOTEMPTY)
+ end
+
+ it "raises an Errno::ENOENT when trying to remove a non-existing directory" do
+ -> do
+ Dir.send @method, DirSpecs.nonexistent
+ end.should.raise(Errno::ENOENT)
+ end
+
+ it "raises an Errno::ENOTDIR when trying to remove a non-directory" do
+ file = DirSpecs.mock_rmdir("nonempty/regular")
+ touch(file)
+ -> do
+ Dir.send @method, file
+ end.should.raise(Errno::ENOTDIR)
+ end
+
+ # this won't work on Windows, since chmod(0000) does not remove all permissions
+ platform_is_not :windows do
+ as_user do
+ it "raises an Errno::EACCES if lacking adequate permissions to remove the directory" do
+ parent = DirSpecs.mock_rmdir("noperm")
+ child = DirSpecs.mock_rmdir("noperm", "child")
+ File.chmod(0000, parent)
+ -> do
+ Dir.send @method, child
+ end.should.raise(Errno::EACCES)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/shared/glob.rb b/spec/ruby/core/dir/shared/glob.rb
new file mode 100644
index 0000000000..86aa105259
--- /dev/null
+++ b/spec/ruby/core/dir/shared/glob.rb
@@ -0,0 +1,441 @@
+# -*- encoding: utf-8 -*-
+describe :dir_glob, shared: true do
+ before :all do
+ DirSpecs.create_mock_dirs
+ @cwd = Dir.pwd
+ Dir.chdir DirSpecs.mock_dir
+ end
+
+ after :all do
+ Dir.chdir @cwd
+ DirSpecs.delete_mock_dirs
+ end
+
+ it "raises an Encoding::CompatibilityError if the argument encoding is not compatible with US-ASCII" do
+ pattern = "files*".dup.force_encoding Encoding::UTF_16BE
+ -> { Dir.send(@method, pattern) }.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "calls #to_path to convert a pattern" do
+ obj = mock('file_one.ext')
+ obj.should_receive(:to_path).and_return('file_one.ext')
+
+ Dir.send(@method, obj).should == %w[file_one.ext]
+ end
+
+ it "raises an ArgumentError if the string contains \\0" do
+ -> {Dir.send(@method, "file_o*\0file_t*")}.should.raise ArgumentError, /nul-separated/
+ end
+
+ it "result is sorted by default" do
+ result = Dir.send(@method, '*')
+ result.should == result.sort
+ end
+
+ it "result is sorted with sort: true" do
+ result = Dir.send(@method, '*', sort: true)
+ result.should == result.sort
+ end
+
+ it "sort: false returns same files" do
+ result = Dir.send(@method,'*', sort: false)
+ result.sort.should == Dir.send(@method, '*').sort
+ end
+
+ it "raises an ArgumentError if sort: is not true or false" do
+ -> { Dir.send(@method, '*', sort: 0) }.should.raise ArgumentError, /expected true or false/
+ -> { Dir.send(@method, '*', sort: nil) }.should.raise ArgumentError, /expected true or false/
+ -> { Dir.send(@method, '*', sort: 'false') }.should.raise ArgumentError, /expected true or false/
+ end
+
+ it "matches non-dotfiles with '*'" do
+ expected = %w[
+ brace
+ deeply
+ dir
+ dir_filename_ordering
+ file_one.ext
+ file_two.ext
+ nested
+ nondotfile
+ special
+ subdir_one
+ subdir_two
+ ]
+
+ Dir.send(@method,'*').sort.should == expected
+ end
+
+ it "returns empty array when empty pattern provided" do
+ Dir.send(@method, '').should == []
+ end
+
+ it "matches regexp special +" do
+ Dir.send(@method, 'special/+').should == ['special/+']
+ end
+
+ it "matches directories with special characters when escaped" do
+ Dir.send(@method, 'special/\{}/special').should == ["special/{}/special"]
+ end
+
+ platform_is_not :windows do
+ it "matches regexp special *" do
+ Dir.send(@method, 'special/\*').should == ['special/*']
+ end
+
+ it "matches regexp special ?" do
+ Dir.send(@method, 'special/\?').should == ['special/?']
+ end
+
+ it "matches regexp special |" do
+ Dir.send(@method, 'special/|').should == ['special/|']
+ end
+
+ it "matches files with backslashes in their name" do
+ Dir.glob('special/\\\\{a,b}').should == ['special/\a']
+ end
+
+ it "matches directory with special characters in their name in complex patterns" do
+ Dir.glob("special/test +()\\[\\]\\{\\}/hello_world{.{en},}{.{html},}{+{phone},}{.{erb},}").should == ['special/test +()[]{}/hello_world.erb']
+ end
+ end
+
+ it "matches regexp special ^" do
+ Dir.send(@method, 'special/^').should == ['special/^']
+ end
+
+ it "matches regexp special $" do
+ Dir.send(@method, 'special/$').should == ['special/$']
+ end
+
+ it "matches regexp special (" do
+ Dir.send(@method, 'special/(').should == ['special/(']
+ end
+
+ it "matches regexp special )" do
+ Dir.send(@method, 'special/)').should == ['special/)']
+ end
+
+ it "matches regexp special [" do
+ Dir.send(@method, 'special/\[').should == ['special/[']
+ end
+
+ it "matches regexp special ]" do
+ Dir.send(@method, 'special/]').should == ['special/]']
+ end
+
+ it "matches regexp special {" do
+ Dir.send(@method, 'special/\{').should == ['special/{']
+ end
+
+ it "matches regexp special }" do
+ Dir.send(@method, 'special/\}').should == ['special/}']
+ end
+
+ it "matches paths with glob patterns" do
+ Dir.send(@method, 'special/test\{1\}/*').should == ['special/test{1}/file[1]']
+ end
+
+ it "matches dotfiles except .. with '.*'" do
+ Dir.send(@method, '.*').sort.should == %w|. .dotfile .dotsubdir|.sort
+ end
+
+ it "matches non-dotfiles with '*<non-special characters>'" do
+ Dir.send(@method, '*file').sort.should == %w|nondotfile|.sort
+ end
+
+ it "matches dotfiles with '.*<non-special characters>'" do
+ Dir.send(@method, '.*file').sort.should == %w|.dotfile|.sort
+ end
+
+ it "matches files with any ending with '<non-special characters>*'" do
+ Dir.send(@method, 'file*').sort.should == %w|file_one.ext file_two.ext|.sort
+ end
+
+ it "matches files with any middle with '<non-special characters>*<non-special characters>'" do
+ Dir.send(@method, 'sub*_one').sort.should == %w|subdir_one|.sort
+ end
+
+ it "handles directories with globs" do
+ Dir.send(@method, 'sub*/*').sort.should == %w!subdir_one/nondotfile subdir_two/nondotfile subdir_two/nondotfile.ext!
+ end
+
+ it "matches files with multiple '*' special characters" do
+ Dir.send(@method, '*fi*e*').sort.should == %w|dir_filename_ordering nondotfile file_one.ext file_two.ext|.sort
+ end
+
+ it "matches non-dotfiles in the current directory with '**'" do
+ expected = %w[
+ brace
+ deeply
+ dir
+ dir_filename_ordering
+ file_one.ext
+ file_two.ext
+ nested
+ nondotfile
+ special
+ subdir_one
+ subdir_two
+ ]
+
+ Dir.send(@method, '**').sort.should == expected
+ end
+
+ it "matches dotfiles in the current directory except .. with '.**'" do
+ Dir.send(@method, '.**').sort.should == %w|. .dotsubdir .dotfile|.sort
+ end
+
+ it "recursively matches any nondot subdirectories with '**/'" do
+ expected = %w[
+ brace/
+ deeply/
+ deeply/nested/
+ deeply/nested/directory/
+ deeply/nested/directory/structure/
+ dir/
+ nested/
+ special/
+ special/test\ +()[]{}/
+ special/test{1}/
+ special/{}/
+ subdir_one/
+ subdir_two/
+ ]
+
+ Dir.send(@method, '**/').sort.should == expected
+ end
+
+ it "recursively matches any subdirectories except './' or '../' with '**/' from the base directory if that is specified" do
+ expected = %w[
+ nested/directory
+ ]
+
+ Dir.send(@method, '**/*ory', base: 'deeply').sort.should == expected
+ end
+
+ it "recursively matches any subdirectories including ./ with '.**/'" do
+ Dir.chdir("#{DirSpecs.mock_dir}/subdir_one") do
+ Dir.send(@method, '.**/').should == ['./']
+ end
+ end
+
+ it "matches a single character except leading '.' with '?'" do
+ Dir.send(@method, '?ubdir_one').sort.should == %w|subdir_one|.sort
+ end
+
+ it "accepts multiple '?' characters in a pattern" do
+ Dir.send(@method, 'subdir_???').sort.should == %w|subdir_one subdir_two|.sort
+ end
+
+ it "matches any characters in a set with '[<characters>]'" do
+ Dir.send(@method, '[stfu]ubdir_one').sort.should == %w|subdir_one|.sort
+ end
+
+ it "matches any characters in a range with '[<character>-<character>]'" do
+ Dir.send(@method, '[a-zA-Z]ubdir_one').sort.should == %w|subdir_one|.sort
+ end
+
+ it "matches any characters except those in a set with '[^<characters>]'" do
+ Dir.send(@method, '[^wtf]ubdir_one').sort.should == %w|subdir_one|.sort
+ end
+
+ it "matches any characters except those in a range with '[^<character>-<character]'" do
+ Dir.send(@method, '[^0-9]ubdir_one').sort.should == %w|subdir_one|.sort
+ end
+
+ it "matches any one of the strings in a set with '{<string>,<other>,...}'" do
+ Dir.send(@method, 'subdir_{one,two,three}').sort.should == %w|subdir_one subdir_two|.sort
+ end
+
+ it "matches a set '{<string>,<other>,...}' which also uses a glob" do
+ Dir.send(@method, 'sub*_{one,two,three}').sort.should == %w|subdir_one subdir_two|.sort
+ end
+
+ it "accepts string sets with empty strings with {<string>,,<other>}" do
+ a = Dir.send(@method, 'deeply/nested/directory/structure/file_one{.ext,}').sort
+ a.should == %w|deeply/nested/directory/structure/file_one.ext
+ deeply/nested/directory/structure/file_one|.sort
+ end
+
+ it "matches dot or non-dotfiles with '{,.}*'" do
+ Dir.send(@method, '{,.}*').sort.should == DirSpecs.expected_glob_paths
+ end
+
+ it "respects the order of {} expressions, expanding left most first" do
+ files = Dir.send(@method, "brace/a{.js,.html}{.erb,.rjs}")
+ files.should == %w!brace/a.js.rjs brace/a.html.erb!
+ end
+
+ it "respects the optional nested {} expressions" do
+ files = Dir.send(@method, "brace/a{.{js,html},}{.{erb,rjs},}")
+ files.should == %w!brace/a.js.rjs brace/a.js brace/a.html.erb brace/a.erb brace/a!
+ end
+
+ it "matches special characters by escaping with a backslash with '\\<character>'" do
+ Dir.mkdir 'foo^bar'
+
+ begin
+ Dir.send(@method, 'foo?bar').should == %w|foo^bar|
+ Dir.send(@method, 'foo\?bar').should == []
+ Dir.send(@method, 'nond\otfile').should == %w|nondotfile|
+ ensure
+ Dir.rmdir 'foo^bar'
+ end
+ end
+
+ it "recursively matches directories with '**/<characters>'" do
+ Dir.send(@method, '**/*fil?{,.}*').uniq.sort.should ==
+ %w[deeply/nested/directory/structure/file_one
+ deeply/nested/directory/structure/file_one.ext
+ deeply/nondotfile
+
+ dir/filename_ordering
+ dir_filename_ordering
+
+ file_one.ext
+ file_two.ext
+
+ nondotfile
+
+ special/test{1}/file[1]
+
+ subdir_one/nondotfile
+ subdir_two/nondotfile
+ subdir_two/nondotfile.ext]
+ end
+
+ it "ignores matching through directories that doesn't exist" do
+ Dir.send(@method, "deeply/notthere/blah*/whatever").should == []
+ end
+
+ it "ignores matching only directories under an nonexistent path" do
+ Dir.send(@method, "deeply/notthere/blah/").should == []
+ end
+
+ platform_is_not :windows do
+ it "matches UTF-8 paths" do
+ Dir.send(@method, "special/ã“ã‚“ã«ã¡ã¯{,.txt}").should == ["special/ã“ã‚“ã«ã¡ã¯.txt"]
+ end
+ end
+
+ context ":base option passed" do
+ before :each do
+ @mock_dir = File.expand_path tmp('dir_glob_mock')
+
+ %w[
+ a/b/x
+ a/b/c/y
+ a/b/c/d/z
+ ].each do |path|
+ file = File.join @mock_dir, path
+ mkdir_p File.dirname(file)
+ touch file
+ end
+ end
+
+ after :each do
+ rm_r @mock_dir
+ end
+
+ it "matches entries only from within the specified directory" do
+ path = File.join(@mock_dir, "a/b/c")
+ Dir.send(@method, "*", base: path).sort.should == %w( d y )
+ end
+
+ it "accepts both relative and absolute paths" do
+ require 'pathname'
+
+ path_abs = File.join(@mock_dir, "a/b/c")
+ path_rel = Pathname.new(path_abs).relative_path_from(Pathname.new(Dir.pwd))
+
+ result_abs = Dir.send(@method, "*", base: path_abs).sort
+ result_rel = Dir.send(@method, "*", base: path_rel).sort
+
+ result_abs.should == %w( d y )
+ result_rel.should == %w( d y )
+ end
+
+ it "returns [] if specified path does not exist" do
+ path = File.join(@mock_dir, "fake-name")
+ File.should_not.exist?(path)
+
+ Dir.send(@method, "*", base: path).should == []
+ end
+
+ it "returns [] if specified path is a file" do
+ path = File.join(@mock_dir, "a/b/x")
+ File.should.exist?(path)
+
+ Dir.send(@method, "*", base: path).should == []
+ end
+
+ it "raises TypeError when cannot convert value to string" do
+ -> {
+ Dir.send(@method, "*", base: [])
+ }.should.raise(TypeError)
+ end
+
+ it "handles '' as current directory path" do
+ Dir.chdir @mock_dir do
+ Dir.send(@method, "*", base: "").should == %w( a )
+ end
+ end
+
+ it "handles nil as current directory path" do
+ Dir.chdir @mock_dir do
+ Dir.send(@method, "*", base: nil).should == %w( a )
+ end
+ end
+ end
+end
+
+describe :dir_glob_recursive, shared: true do
+ before :each do
+ @cwd = Dir.pwd
+ @mock_dir = File.expand_path tmp('dir_glob_mock')
+
+ %w[
+ a/x/b/y/e
+ a/x/b/y/b/z/e
+ ].each do |path|
+ file = File.join @mock_dir, path
+ mkdir_p File.dirname(file)
+ touch file
+ end
+
+ Dir.chdir @mock_dir
+ end
+
+ after :each do
+ Dir.chdir @cwd
+ rm_r @mock_dir
+ end
+
+ it "matches multiple recursives" do
+ expected = %w[
+ a/x/b/y/b/z/e
+ a/x/b/y/e
+ ]
+
+ Dir.send(@method, 'a/**/b/**/e').uniq.sort.should == expected
+ end
+
+ platform_is_not :windows do
+ it "ignores symlinks" do
+ file = File.join @mock_dir, 'b/z/e'
+ link = File.join @mock_dir, 'a/y'
+
+ mkdir_p File.dirname(file)
+ touch file
+ File.symlink(File.dirname(file), link)
+
+ expected = %w[
+ a/x/b/y/b/z/e
+ a/x/b/y/e
+ ]
+
+ Dir.send(@method, 'a/**/e').uniq.sort.should == expected
+ end
+ end
+end
diff --git a/spec/ruby/core/dir/shared/pos.rb b/spec/ruby/core/dir/shared/pos.rb
new file mode 100644
index 0000000000..741de8918c
--- /dev/null
+++ b/spec/ruby/core/dir/shared/pos.rb
@@ -0,0 +1,24 @@
+describe :dir_pos_set, shared: true do
+ before :each do
+ @dir = Dir.open DirSpecs.mock_dir
+ end
+
+ after :each do
+ @dir.close
+ end
+
+ # NOTE: #seek/#pos= to a position not returned by #tell/#pos is undefined
+ # and should not be spec'd.
+
+ it "moves the read position to a previously obtained position" do
+ pos = @dir.pos
+ a = @dir.read
+ b = @dir.read
+ @dir.send @method, pos
+ c = @dir.read
+
+ a.should_not == b
+ b.should_not == c
+ c.should == a
+ end
+end
diff --git a/spec/ruby/core/dir/tell_spec.rb b/spec/ruby/core/dir/tell_spec.rb
new file mode 100644
index 0000000000..dcbb40438f
--- /dev/null
+++ b/spec/ruby/core/dir/tell_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/closed'
+require_relative 'shared/pos'
+
+describe "Dir#tell" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it_behaves_like :dir_closed, :tell
+
+ before :each do
+ @dir = Dir.open DirSpecs.mock_dir
+ end
+
+ after :each do
+ @dir.close rescue nil
+ end
+
+ it "returns an Integer representing the current position in the directory" do
+ @dir.tell.should.is_a?(Integer)
+ @dir.tell.should.is_a?(Integer)
+ @dir.tell.should.is_a?(Integer)
+ end
+
+ it "returns a different Integer if moved from previous position" do
+ a = @dir.tell
+ @dir.read
+ b = @dir.tell
+
+ a.should.is_a?(Integer)
+ b.should.is_a?(Integer)
+
+ a.should_not == b
+ end
+end
diff --git a/spec/ruby/core/dir/to_path_spec.rb b/spec/ruby/core/dir/to_path_spec.rb
new file mode 100644
index 0000000000..2ed533e757
--- /dev/null
+++ b/spec/ruby/core/dir/to_path_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Dir#to_path" do
+ it "is an alias of Dir#path" do
+ Dir.instance_method(:to_path).should == Dir.instance_method(:path)
+ end
+end
diff --git a/spec/ruby/core/dir/unlink_spec.rb b/spec/ruby/core/dir/unlink_spec.rb
new file mode 100644
index 0000000000..79027e020c
--- /dev/null
+++ b/spec/ruby/core/dir/unlink_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/delete'
+
+describe "Dir.unlink" do
+ before :all do
+ DirSpecs.create_mock_dirs
+ end
+
+ after :all do
+ DirSpecs.delete_mock_dirs
+ end
+
+ it_behaves_like :dir_delete, :unlink
+end
diff --git a/spec/ruby/core/encoding/_dump_spec.rb b/spec/ruby/core/encoding/_dump_spec.rb
new file mode 100644
index 0000000000..623fe88ec9
--- /dev/null
+++ b/spec/ruby/core/encoding/_dump_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Encoding#_dump" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/encoding/_load_spec.rb b/spec/ruby/core/encoding/_load_spec.rb
new file mode 100644
index 0000000000..608098d34b
--- /dev/null
+++ b/spec/ruby/core/encoding/_load_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Encoding._load" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/encoding/aliases_spec.rb b/spec/ruby/core/encoding/aliases_spec.rb
new file mode 100644
index 0000000000..12c6c6cf85
--- /dev/null
+++ b/spec/ruby/core/encoding/aliases_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.aliases" do
+ it "returns a Hash" do
+ Encoding.aliases.should.instance_of?(Hash)
+ end
+
+ it "has Strings as keys" do
+ Encoding.aliases.keys.each do |key|
+ key.should.instance_of?(String)
+ end
+ end
+
+ it "has Strings as values" do
+ Encoding.aliases.values.each do |value|
+ value.should.instance_of?(String)
+ end
+ end
+
+ it "has alias names as its keys" do
+ Encoding.aliases.key?('BINARY').should == true
+ Encoding.aliases.key?('ASCII').should == true
+ end
+
+ it "has the names of the aliased encoding as its values" do
+ Encoding.aliases['BINARY'].should == 'ASCII-8BIT'
+ Encoding.aliases['ASCII'].should == 'US-ASCII'
+ end
+
+ it "has an 'external' key with the external default encoding as its value" do
+ Encoding.aliases['external'].should == Encoding.default_external.name
+ end
+
+ it "has a 'locale' key and its value equals the name of the encoding found by the locale charmap" do
+ Encoding.aliases['locale'].should == Encoding.find(Encoding.locale_charmap).name
+ end
+
+ it "only contains valid aliased encodings" do
+ Encoding.aliases.each do |aliased, original|
+ Encoding.find(aliased).should == Encoding.find(original)
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/ascii_compatible_spec.rb b/spec/ruby/core/encoding/ascii_compatible_spec.rb
new file mode 100644
index 0000000000..04fc159bb8
--- /dev/null
+++ b/spec/ruby/core/encoding/ascii_compatible_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+
+describe "Encoding#ascii_compatible?" do
+ it "returns true if self represents an ASCII-compatible encoding" do
+ Encoding::UTF_8.ascii_compatible?.should == true
+ end
+
+ it "returns false if self does not represent an ASCII-compatible encoding" do
+ Encoding::UTF_16LE.ascii_compatible?.should == false
+ end
+
+ it "returns false for UTF_16 and UTF_32" do
+ Encoding::UTF_16.should_not.ascii_compatible?
+ Encoding::UTF_32.should_not.ascii_compatible?
+ end
+
+ it "is always false for dummy encodings" do
+ Encoding.list.select(&:dummy?).each do |encoding|
+ encoding.should_not.ascii_compatible?
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/compatible_spec.rb b/spec/ruby/core/encoding/compatible_spec.rb
new file mode 100644
index 0000000000..0d620e5bf3
--- /dev/null
+++ b/spec/ruby/core/encoding/compatible_spec.rb
@@ -0,0 +1,772 @@
+# encoding: binary
+
+require_relative '../../spec_helper'
+
+# TODO: add IO
+
+describe "Encoding.compatible? String, String" do
+ describe "when the first's Encoding is valid US-ASCII" do
+ before :each do
+ @str = "abc".dup.force_encoding Encoding::US_ASCII
+ end
+
+ it "returns US-ASCII when the second's is US-ASCII" do
+ Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::US_ASCII
+ end
+
+ it "returns US-ASCII if the second String is BINARY and ASCII only" do
+ Encoding.compatible?(@str, "\x7f").should == Encoding::US_ASCII
+ end
+
+ it "returns BINARY if the second String is BINARY but not ASCII only" do
+ Encoding.compatible?(@str, "\xff").should == Encoding::BINARY
+ end
+
+ it "returns US-ASCII if the second String is UTF-8 and ASCII only" do
+ Encoding.compatible?(@str, "\x7f".encode("utf-8")).should == Encoding::US_ASCII
+ end
+
+ it "returns UTF-8 if the second String is UTF-8 but not ASCII only" do
+ Encoding.compatible?(@str, "\u3042".encode("utf-8")).should == Encoding::UTF_8
+ end
+ end
+
+ describe "when the first's Encoding is ASCII compatible and ASCII only" do
+ it "returns the first's Encoding if the second is ASCII compatible and ASCII only" do
+ [ [Encoding, "abc".dup.force_encoding("UTF-8"), "123".dup.force_encoding("Shift_JIS"), Encoding::UTF_8],
+ [Encoding, "123".dup.force_encoding("Shift_JIS"), "abc".dup.force_encoding("UTF-8"), Encoding::Shift_JIS]
+ ].should be_computed_by(:compatible?)
+ end
+
+ it "returns the first's Encoding if the second is ASCII compatible and ASCII only" do
+ [ [Encoding, "abc".dup.force_encoding("BINARY"), "123".dup.force_encoding("US-ASCII"), Encoding::BINARY],
+ [Encoding, "123".dup.force_encoding("US-ASCII"), "abc".dup.force_encoding("BINARY"), Encoding::US_ASCII]
+ ].should be_computed_by(:compatible?)
+ end
+
+ it "returns the second's Encoding if the second is ASCII compatible but not ASCII only" do
+ [ [Encoding, "abc".dup.force_encoding("UTF-8"), "\xff".dup.force_encoding("Shift_JIS"), Encoding::Shift_JIS],
+ [Encoding, "123".dup.force_encoding("Shift_JIS"), "\xff".dup.force_encoding("UTF-8"), Encoding::UTF_8],
+ [Encoding, "abc".dup.force_encoding("BINARY"), "\xff".dup.force_encoding("US-ASCII"), Encoding::US_ASCII],
+ [Encoding, "123".dup.force_encoding("US-ASCII"), "\xff".dup.force_encoding("BINARY"), Encoding::BINARY],
+ ].should be_computed_by(:compatible?)
+ end
+
+ it "returns nil if the second's Encoding is not ASCII compatible" do
+ a = "abc".dup.force_encoding("UTF-8")
+ b = "1234".dup.force_encoding("UTF-16LE")
+ Encoding.compatible?(a, b).should == nil
+ end
+ end
+
+ describe "when the first's Encoding is ASCII compatible but not ASCII only" do
+ it "returns the first's Encoding if the second's is valid US-ASCII" do
+ Encoding.compatible?("\xff", "def".encode("us-ascii")).should == Encoding::BINARY
+ end
+
+ it "returns the first's Encoding if the second's is UTF-8 and ASCII only" do
+ Encoding.compatible?("\xff", "\u{7f}".encode("utf-8")).should == Encoding::BINARY
+ end
+
+ it "returns nil if the second encoding is ASCII compatible but neither String's encoding is ASCII only" do
+ Encoding.compatible?("\xff", "\u3042".encode("utf-8")).should == nil
+ end
+ end
+
+ describe "when the first's Encoding is not ASCII compatible" do
+ before :each do
+ @str = "abc".dup.force_encoding Encoding::UTF_7
+ end
+
+ it "returns nil when the second String is US-ASCII" do
+ Encoding.compatible?(@str, "def".encode("us-ascii")).should == nil
+ end
+
+ it "returns nil when the second String is BINARY and ASCII only" do
+ Encoding.compatible?(@str, "\x7f").should == nil
+ end
+
+ it "returns nil when the second String is BINARY but not ASCII only" do
+ Encoding.compatible?(@str, "\xff").should == nil
+ end
+
+ it "returns the Encoding when the second's Encoding is not ASCII compatible but the same as the first's Encoding" do
+ encoding = Encoding.compatible?(@str, "def".dup.force_encoding("utf-7"))
+ encoding.should == Encoding::UTF_7
+ end
+ end
+
+ describe "when the first's Encoding is invalid" do
+ before :each do
+ @str = "\xff".dup.force_encoding Encoding::UTF_8
+ end
+
+ it "returns the first's Encoding when the second's Encoding is US-ASCII" do
+ Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::UTF_8
+ end
+
+ it "returns the first's Encoding when the second String is ASCII only" do
+ Encoding.compatible?(@str, "\x7f").should == Encoding::UTF_8
+ end
+
+ it "returns nil when the second's Encoding is BINARY but not ASCII only" do
+ Encoding.compatible?(@str, "\xff").should == nil
+ end
+
+ it "returns nil when the second's Encoding is invalid and ASCII only" do
+ Encoding.compatible?(@str, "\x7f\x7f".dup.force_encoding("utf-16be")).should == nil
+ end
+
+ it "returns nil when the second's Encoding is invalid and not ASCII only" do
+ Encoding.compatible?(@str, "\xff\xff".dup.force_encoding("utf-16be")).should == nil
+ end
+
+ it "returns the Encoding when the second's Encoding is invalid but the same as the first" do
+ Encoding.compatible?(@str, @str).should == Encoding::UTF_8
+ end
+ end
+
+ describe "when the first String is empty and the second is not" do
+ describe "and the first's Encoding is ASCII compatible" do
+ before :each do
+ @str = "".dup.force_encoding("utf-8")
+ end
+
+ it "returns the first's encoding when the second String is ASCII only" do
+ Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::UTF_8
+ end
+
+ it "returns the second's encoding when the second String is not ASCII only" do
+ Encoding.compatible?(@str, "def".encode("utf-32le")).should == Encoding::UTF_32LE
+ end
+ end
+
+ describe "when the first's Encoding is not ASCII compatible" do
+ before :each do
+ @str = "".dup.force_encoding Encoding::UTF_7
+ end
+
+ it "returns the second string's encoding" do
+ Encoding.compatible?(@str, "def".encode("us-ascii")).should == Encoding::US_ASCII
+ end
+ end
+ end
+
+ describe "when the second String is empty" do
+ before :each do
+ @str = "abc".dup.force_encoding("utf-7")
+ end
+
+ it "returns the first Encoding" do
+ Encoding.compatible?(@str, "").should == Encoding::UTF_7
+ end
+ end
+
+ # Encoding negotiation depends on whether encodings are ASCII-compatible, empty
+ # and contain only ASCII characters (that take 7 bits). Check US-ASCII, UTF-8 and
+ # BINARY encodings (as most common) as well as an ASCII-compatible, a non-ASCII-compatible and a dummy
+ # encodings in all possible combinations.
+ describe "compatibility matrix" do
+
+# Use the following script to regenerate the matrix:
+#
+# ```
+# # encoding: binary
+#
+# ENCODINGS = [
+# "US-ASCII",
+# "UTF-8",
+# "ASCII-8BIT",
+# "ISO-8859-1", # ASCII-compatible
+# "UTF-16BE", # non-ASCII-compatible
+# "ISO-2022-JP" # dummy
+# ]
+#
+# TYPES = [:empty, :"7bits", :non7bits]
+#
+# VALUES = {
+# empty: "",
+# :"7bits" => "\x01\x01",
+# non7bits: "\x01\x81"
+# }
+#
+# ENCODINGS.product(TYPES, ENCODINGS, TYPES).each do |encoding1, type1, encoding2, type2|
+# value1 = VALUES[type1].dup.force_encoding(encoding1)
+# value2 = VALUES[type2].dup.force_encoding(encoding2)
+#
+# result_encoding = Encoding.compatible?(value1, value2)
+#
+# puts "[#{encoding1.inspect}, #{value1.inspect}, #{encoding2.inspect}, #{value2.inspect}, #{result_encoding&.name.inspect}],"
+# end
+# ```
+
+ matrix = [
+ ["US-ASCII", "", "US-ASCII", "", "US-ASCII"],
+ ["US-ASCII", "", "US-ASCII", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["US-ASCII", "", "UTF-8", "", "US-ASCII"],
+ ["US-ASCII", "", "UTF-8", "\u0001\u0001", "US-ASCII"],
+ ["US-ASCII", "", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["US-ASCII", "", "ASCII-8BIT", "", "US-ASCII"],
+ ["US-ASCII", "", "ASCII-8BIT", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["US-ASCII", "", "ISO-8859-1", "", "US-ASCII"],
+ ["US-ASCII", "", "ISO-8859-1", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["US-ASCII", "", "UTF-16BE", "", "US-ASCII"],
+ ["US-ASCII", "", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["US-ASCII", "", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["US-ASCII", "", "ISO-2022-JP", "", "US-ASCII"],
+ ["US-ASCII", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["US-ASCII", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["US-ASCII", "\x01\x01", "US-ASCII", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "US-ASCII", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "UTF-8", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "UTF-8", "\u0001\u0001", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["US-ASCII", "\x01\x01", "ASCII-8BIT", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "ASCII-8BIT", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["US-ASCII", "\x01\x01", "ISO-8859-1", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "ISO-8859-1", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["US-ASCII", "\x01\x01", "UTF-16BE", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "UTF-16BE", "\u0101", nil],
+ ["US-ASCII", "\x01\x01", "UTF-16BE", "\u0181", nil],
+ ["US-ASCII", "\x01\x01", "ISO-2022-JP", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x01", "ISO-2022-JP", "\x01\x01", nil],
+ ["US-ASCII", "\x01\x01", "ISO-2022-JP", "\x01\x81", nil],
+ ["US-ASCII", "\x01\x81", "US-ASCII", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "US-ASCII", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "UTF-8", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "UTF-8", "\u0001\u0001", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "UTF-8", "\u0001\x81", nil],
+ ["US-ASCII", "\x01\x81", "ASCII-8BIT", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "ASCII-8BIT", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "ASCII-8BIT", "\x01\x81", nil],
+ ["US-ASCII", "\x01\x81", "ISO-8859-1", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "ISO-8859-1", "\x01\x01", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "ISO-8859-1", "\x01\x81", nil],
+ ["US-ASCII", "\x01\x81", "UTF-16BE", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "UTF-16BE", "\u0101", nil],
+ ["US-ASCII", "\x01\x81", "UTF-16BE", "\u0181", nil],
+ ["US-ASCII", "\x01\x81", "ISO-2022-JP", "", "US-ASCII"],
+ ["US-ASCII", "\x01\x81", "ISO-2022-JP", "\x01\x01", nil],
+ ["US-ASCII", "\x01\x81", "ISO-2022-JP", "\x01\x81", nil],
+ ["UTF-8", "", "US-ASCII", "", "UTF-8"],
+ ["UTF-8", "", "US-ASCII", "\x01\x01", "UTF-8"],
+ ["UTF-8", "", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["UTF-8", "", "UTF-8", "", "UTF-8"],
+ ["UTF-8", "", "UTF-8", "\u0001\u0001", "UTF-8"],
+ ["UTF-8", "", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["UTF-8", "", "ASCII-8BIT", "", "UTF-8"],
+ ["UTF-8", "", "ASCII-8BIT", "\x01\x01", "UTF-8"],
+ ["UTF-8", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["UTF-8", "", "ISO-8859-1", "", "UTF-8"],
+ ["UTF-8", "", "ISO-8859-1", "\x01\x01", "UTF-8"],
+ ["UTF-8", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["UTF-8", "", "UTF-16BE", "", "UTF-8"],
+ ["UTF-8", "", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["UTF-8", "", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["UTF-8", "", "ISO-2022-JP", "", "UTF-8"],
+ ["UTF-8", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["UTF-8", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["UTF-8", "\u0001\u0001", "US-ASCII", "", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "US-ASCII", "\x01\x01", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["UTF-8", "\u0001\u0001", "UTF-8", "", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "UTF-8", "\u0001\u0001", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "ASCII-8BIT", "", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "ASCII-8BIT", "\x01\x01", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["UTF-8", "\u0001\u0001", "ISO-8859-1", "", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "ISO-8859-1", "\x01\x01", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["UTF-8", "\u0001\u0001", "UTF-16BE", "", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "UTF-16BE", "\u0101", nil],
+ ["UTF-8", "\u0001\u0001", "UTF-16BE", "\u0181", nil],
+ ["UTF-8", "\u0001\u0001", "ISO-2022-JP", "", "UTF-8"],
+ ["UTF-8", "\u0001\u0001", "ISO-2022-JP", "\x01\x01", nil],
+ ["UTF-8", "\u0001\u0001", "ISO-2022-JP", "\x01\x81", nil],
+ ["UTF-8", "\u0001\x81", "US-ASCII", "", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "US-ASCII", "\x01\x01", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "US-ASCII", "\x01\x81", nil],
+ ["UTF-8", "\u0001\x81", "UTF-8", "", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "UTF-8", "\u0001\u0001", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "ASCII-8BIT", "", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "ASCII-8BIT", "\x01\x01", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "ASCII-8BIT", "\x01\x81", nil],
+ ["UTF-8", "\u0001\x81", "ISO-8859-1", "", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "ISO-8859-1", "\x01\x01", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "ISO-8859-1", "\x01\x81", nil],
+ ["UTF-8", "\u0001\x81", "UTF-16BE", "", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "UTF-16BE", "\u0101", nil],
+ ["UTF-8", "\u0001\x81", "UTF-16BE", "\u0181", nil],
+ ["UTF-8", "\u0001\x81", "ISO-2022-JP", "", "UTF-8"],
+ ["UTF-8", "\u0001\x81", "ISO-2022-JP", "\x01\x01", nil],
+ ["UTF-8", "\u0001\x81", "ISO-2022-JP", "\x01\x81", nil],
+ ["ASCII-8BIT", "", "US-ASCII", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "US-ASCII", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["ASCII-8BIT", "", "UTF-8", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "UTF-8", "\u0001\u0001", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["ASCII-8BIT", "", "ASCII-8BIT", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "ISO-8859-1", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "ISO-8859-1", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["ASCII-8BIT", "", "UTF-16BE", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["ASCII-8BIT", "", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["ASCII-8BIT", "", "ISO-2022-JP", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["ASCII-8BIT", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["ASCII-8BIT", "\x01\x01", "US-ASCII", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "US-ASCII", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["ASCII-8BIT", "\x01\x01", "UTF-8", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "UTF-8", "\u0001\u0001", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["ASCII-8BIT", "\x01\x01", "ASCII-8BIT", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "ISO-8859-1", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "ISO-8859-1", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["ASCII-8BIT", "\x01\x01", "UTF-16BE", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "UTF-16BE", "\u0101", nil],
+ ["ASCII-8BIT", "\x01\x01", "UTF-16BE", "\u0181", nil],
+ ["ASCII-8BIT", "\x01\x01", "ISO-2022-JP", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x01", "ISO-2022-JP", "\x01\x01", nil],
+ ["ASCII-8BIT", "\x01\x01", "ISO-2022-JP", "\x01\x81", nil],
+ ["ASCII-8BIT", "\x01\x81", "US-ASCII", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "US-ASCII", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "US-ASCII", "\x01\x81", nil],
+ ["ASCII-8BIT", "\x01\x81", "UTF-8", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "UTF-8", "\u0001\u0001", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "UTF-8", "\u0001\x81", nil],
+ ["ASCII-8BIT", "\x01\x81", "ASCII-8BIT", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "ISO-8859-1", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "ISO-8859-1", "\x01\x01", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "ISO-8859-1", "\x01\x81", nil],
+ ["ASCII-8BIT", "\x01\x81", "UTF-16BE", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "UTF-16BE", "\u0101", nil],
+ ["ASCII-8BIT", "\x01\x81", "UTF-16BE", "\u0181", nil],
+ ["ASCII-8BIT", "\x01\x81", "ISO-2022-JP", "", "ASCII-8BIT"],
+ ["ASCII-8BIT", "\x01\x81", "ISO-2022-JP", "\x01\x01", nil],
+ ["ASCII-8BIT", "\x01\x81", "ISO-2022-JP", "\x01\x81", nil],
+ ["ISO-8859-1", "", "US-ASCII", "", "ISO-8859-1"],
+ ["ISO-8859-1", "", "US-ASCII", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["ISO-8859-1", "", "UTF-8", "", "ISO-8859-1"],
+ ["ISO-8859-1", "", "UTF-8", "\u0001\u0001", "ISO-8859-1"],
+ ["ISO-8859-1", "", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["ISO-8859-1", "", "ASCII-8BIT", "", "ISO-8859-1"],
+ ["ISO-8859-1", "", "ASCII-8BIT", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["ISO-8859-1", "", "ISO-8859-1", "", "ISO-8859-1"],
+ ["ISO-8859-1", "", "ISO-8859-1", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["ISO-8859-1", "", "UTF-16BE", "", "ISO-8859-1"],
+ ["ISO-8859-1", "", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["ISO-8859-1", "", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["ISO-8859-1", "", "ISO-2022-JP", "", "ISO-8859-1"],
+ ["ISO-8859-1", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["ISO-8859-1", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["ISO-8859-1", "\x01\x01", "US-ASCII", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "US-ASCII", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["ISO-8859-1", "\x01\x01", "UTF-8", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "UTF-8", "\u0001\u0001", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["ISO-8859-1", "\x01\x01", "ASCII-8BIT", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "ASCII-8BIT", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["ISO-8859-1", "\x01\x01", "ISO-8859-1", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "ISO-8859-1", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "UTF-16BE", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "UTF-16BE", "\u0101", nil],
+ ["ISO-8859-1", "\x01\x01", "UTF-16BE", "\u0181", nil],
+ ["ISO-8859-1", "\x01\x01", "ISO-2022-JP", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x01", "ISO-2022-JP", "\x01\x01", nil],
+ ["ISO-8859-1", "\x01\x01", "ISO-2022-JP", "\x01\x81", nil],
+ ["ISO-8859-1", "\x01\x81", "US-ASCII", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "US-ASCII", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "US-ASCII", "\x01\x81", nil],
+ ["ISO-8859-1", "\x01\x81", "UTF-8", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "UTF-8", "\u0001\u0001", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "UTF-8", "\u0001\x81", nil],
+ ["ISO-8859-1", "\x01\x81", "ASCII-8BIT", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "ASCII-8BIT", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "ASCII-8BIT", "\x01\x81", nil],
+ ["ISO-8859-1", "\x01\x81", "ISO-8859-1", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "ISO-8859-1", "\x01\x01", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "UTF-16BE", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "UTF-16BE", "\u0101", nil],
+ ["ISO-8859-1", "\x01\x81", "UTF-16BE", "\u0181", nil],
+ ["ISO-8859-1", "\x01\x81", "ISO-2022-JP", "", "ISO-8859-1"],
+ ["ISO-8859-1", "\x01\x81", "ISO-2022-JP", "\x01\x01", nil],
+ ["ISO-8859-1", "\x01\x81", "ISO-2022-JP", "\x01\x81", nil],
+ ["UTF-16BE", "", "US-ASCII", "", "UTF-16BE"],
+ ["UTF-16BE", "", "US-ASCII", "\x01\x01", "US-ASCII"],
+ ["UTF-16BE", "", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["UTF-16BE", "", "UTF-8", "", "UTF-16BE"],
+ ["UTF-16BE", "", "UTF-8", "\u0001\u0001", "UTF-8"],
+ ["UTF-16BE", "", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["UTF-16BE", "", "ASCII-8BIT", "", "UTF-16BE"],
+ ["UTF-16BE", "", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"],
+ ["UTF-16BE", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["UTF-16BE", "", "ISO-8859-1", "", "UTF-16BE"],
+ ["UTF-16BE", "", "ISO-8859-1", "\x01\x01", "ISO-8859-1"],
+ ["UTF-16BE", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["UTF-16BE", "", "UTF-16BE", "", "UTF-16BE"],
+ ["UTF-16BE", "", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["UTF-16BE", "", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["UTF-16BE", "", "ISO-2022-JP", "", "UTF-16BE"],
+ ["UTF-16BE", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["UTF-16BE", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["UTF-16BE", "\u0101", "US-ASCII", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "US-ASCII", "\x01\x01", nil],
+ ["UTF-16BE", "\u0101", "US-ASCII", "\x01\x81", nil],
+ ["UTF-16BE", "\u0101", "UTF-8", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "UTF-8", "\u0001\u0001", nil],
+ ["UTF-16BE", "\u0101", "UTF-8", "\u0001\x81", nil],
+ ["UTF-16BE", "\u0101", "ASCII-8BIT", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "ASCII-8BIT", "\x01\x01", nil],
+ ["UTF-16BE", "\u0101", "ASCII-8BIT", "\x01\x81", nil],
+ ["UTF-16BE", "\u0101", "ISO-8859-1", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "ISO-8859-1", "\x01\x01", nil],
+ ["UTF-16BE", "\u0101", "ISO-8859-1", "\x01\x81", nil],
+ ["UTF-16BE", "\u0101", "UTF-16BE", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "ISO-2022-JP", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0101", "ISO-2022-JP", "\x01\x01", nil],
+ ["UTF-16BE", "\u0101", "ISO-2022-JP", "\x01\x81", nil],
+ ["UTF-16BE", "\u0181", "US-ASCII", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "US-ASCII", "\x01\x01", nil],
+ ["UTF-16BE", "\u0181", "US-ASCII", "\x01\x81", nil],
+ ["UTF-16BE", "\u0181", "UTF-8", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "UTF-8", "\u0001\u0001", nil],
+ ["UTF-16BE", "\u0181", "UTF-8", "\u0001\x81", nil],
+ ["UTF-16BE", "\u0181", "ASCII-8BIT", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "ASCII-8BIT", "\x01\x01", nil],
+ ["UTF-16BE", "\u0181", "ASCII-8BIT", "\x01\x81", nil],
+ ["UTF-16BE", "\u0181", "ISO-8859-1", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "ISO-8859-1", "\x01\x01", nil],
+ ["UTF-16BE", "\u0181", "ISO-8859-1", "\x01\x81", nil],
+ ["UTF-16BE", "\u0181", "UTF-16BE", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "ISO-2022-JP", "", "UTF-16BE"],
+ ["UTF-16BE", "\u0181", "ISO-2022-JP", "\x01\x01", nil],
+ ["UTF-16BE", "\u0181", "ISO-2022-JP", "\x01\x81", nil],
+ ["ISO-2022-JP", "", "US-ASCII", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "US-ASCII", "\x01\x01", "US-ASCII"],
+ ["ISO-2022-JP", "", "US-ASCII", "\x01\x81", "US-ASCII"],
+ ["ISO-2022-JP", "", "UTF-8", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "UTF-8", "\u0001\u0001", "UTF-8"],
+ ["ISO-2022-JP", "", "UTF-8", "\u0001\x81", "UTF-8"],
+ ["ISO-2022-JP", "", "ASCII-8BIT", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "ASCII-8BIT", "\x01\x01", "ASCII-8BIT"],
+ ["ISO-2022-JP", "", "ASCII-8BIT", "\x01\x81", "ASCII-8BIT"],
+ ["ISO-2022-JP", "", "ISO-8859-1", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "ISO-8859-1", "\x01\x01", "ISO-8859-1"],
+ ["ISO-2022-JP", "", "ISO-8859-1", "\x01\x81", "ISO-8859-1"],
+ ["ISO-2022-JP", "", "UTF-16BE", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "UTF-16BE", "\u0101", "UTF-16BE"],
+ ["ISO-2022-JP", "", "UTF-16BE", "\u0181", "UTF-16BE"],
+ ["ISO-2022-JP", "", "ISO-2022-JP", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["ISO-2022-JP", "", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "US-ASCII", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "US-ASCII", "\x01\x01", nil],
+ ["ISO-2022-JP", "\x01\x01", "US-ASCII", "\x01\x81", nil],
+ ["ISO-2022-JP", "\x01\x01", "UTF-8", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "UTF-8", "\u0001\u0001", nil],
+ ["ISO-2022-JP", "\x01\x01", "UTF-8", "\u0001\x81", nil],
+ ["ISO-2022-JP", "\x01\x01", "ASCII-8BIT", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "ASCII-8BIT", "\x01\x01", nil],
+ ["ISO-2022-JP", "\x01\x01", "ASCII-8BIT", "\x01\x81", nil],
+ ["ISO-2022-JP", "\x01\x01", "ISO-8859-1", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "ISO-8859-1", "\x01\x01", nil],
+ ["ISO-2022-JP", "\x01\x01", "ISO-8859-1", "\x01\x81", nil],
+ ["ISO-2022-JP", "\x01\x01", "UTF-16BE", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "UTF-16BE", "\u0101", nil],
+ ["ISO-2022-JP", "\x01\x01", "UTF-16BE", "\u0181", nil],
+ ["ISO-2022-JP", "\x01\x01", "ISO-2022-JP", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x01", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "US-ASCII", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "US-ASCII", "\x01\x01", nil],
+ ["ISO-2022-JP", "\x01\x81", "US-ASCII", "\x01\x81", nil],
+ ["ISO-2022-JP", "\x01\x81", "UTF-8", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "UTF-8", "\u0001\u0001", nil],
+ ["ISO-2022-JP", "\x01\x81", "UTF-8", "\u0001\x81", nil],
+ ["ISO-2022-JP", "\x01\x81", "ASCII-8BIT", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "ASCII-8BIT", "\x01\x01", nil],
+ ["ISO-2022-JP", "\x01\x81", "ASCII-8BIT", "\x01\x81", nil],
+ ["ISO-2022-JP", "\x01\x81", "ISO-8859-1", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "ISO-8859-1", "\x01\x01", nil],
+ ["ISO-2022-JP", "\x01\x81", "ISO-8859-1", "\x01\x81", nil],
+ ["ISO-2022-JP", "\x01\x81", "UTF-16BE", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "UTF-16BE", "\u0101", nil],
+ ["ISO-2022-JP", "\x01\x81", "UTF-16BE", "\u0181", nil],
+ ["ISO-2022-JP", "\x01\x81", "ISO-2022-JP", "", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "ISO-2022-JP", "\x01\x01", "ISO-2022-JP"],
+ ["ISO-2022-JP", "\x01\x81", "ISO-2022-JP", "\x01\x81", "ISO-2022-JP"],
+ ]
+
+ matrix.each do |encoding1, value1, encoding2, value2, compatible_encoding|
+ it "returns #{compatible_encoding} for #{value1.inspect} in #{encoding1} and #{value2.inspect} in #{encoding2}" do
+ actual_encoding = Encoding.compatible?(value1.dup.force_encoding(encoding1), value2.dup.force_encoding(encoding2))
+ actual_encoding&.name.should == compatible_encoding
+ end
+ end
+ end
+end
+
+describe "Encoding.compatible? String, Regexp" do
+ it "returns US-ASCII if both are US-ASCII" do
+ str = "abc".dup.force_encoding("us-ascii")
+ Encoding.compatible?(str, /abc/).should == Encoding::US_ASCII
+ end
+
+ it "returns the String's Encoding if it is not US-ASCII but both are ASCII only" do
+ [ [Encoding, "abc", Encoding::BINARY],
+ [Encoding, "abc".encode("utf-8"), Encoding::UTF_8],
+ [Encoding, "abc".encode("euc-jp"), Encoding::EUC_JP],
+ [Encoding, "abc".encode("shift_jis"), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, /abc/)
+ end
+
+ it "returns the String's Encoding if the String is not ASCII only" do
+ [ [Encoding, "\xff", Encoding::BINARY],
+ [Encoding, "\u3042".encode("utf-8"), Encoding::UTF_8],
+ [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp"), Encoding::EUC_JP],
+ [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, /abc/)
+ end
+
+ it "returns the Regexp's Encoding if the String is ASCII only and the Regexp is not" do
+ r = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp"))
+ Encoding.compatible?("hello".dup.force_encoding("utf-8"), r).should == Encoding::EUC_JP
+ end
+end
+
+describe "Encoding.compatible? String, Symbol" do
+ it "returns US-ASCII if both are ASCII only" do
+ str = "abc".dup.force_encoding("us-ascii")
+ Encoding.compatible?(str, :abc).should == Encoding::US_ASCII
+ end
+
+ it "returns the String's Encoding if it is not US-ASCII but both are ASCII only" do
+ [ [Encoding, "abc", Encoding::BINARY],
+ [Encoding, "abc".encode("utf-8"), Encoding::UTF_8],
+ [Encoding, "abc".encode("euc-jp"), Encoding::EUC_JP],
+ [Encoding, "abc".encode("shift_jis"), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, :abc)
+ end
+
+ it "returns the String's Encoding if the String is not ASCII only" do
+ [ [Encoding, "\xff", Encoding::BINARY],
+ [Encoding, "\u3042".encode("utf-8"), Encoding::UTF_8],
+ [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp"), Encoding::EUC_JP],
+ [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, :abc)
+ end
+end
+
+describe "Encoding.compatible? String, Encoding" do
+ it "returns nil if the String's encoding is not ASCII compatible" do
+ Encoding.compatible?("abc".encode("utf-32le"), Encoding::US_ASCII).should == nil
+ end
+
+ it "returns nil if the Encoding is not ASCII compatible" do
+ Encoding.compatible?("abc".encode("us-ascii"), Encoding::UTF_32LE).should == nil
+ end
+
+ it "returns the String's encoding if the Encoding is US-ASCII" do
+ [ [Encoding, "\xff", Encoding::BINARY],
+ [Encoding, "\u3042".encode("utf-8"), Encoding::UTF_8],
+ [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp"), Encoding::EUC_JP],
+ [Encoding, "\x82\xa0".dup.force_encoding("shift_jis"), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, Encoding::US_ASCII)
+ end
+
+ it "returns the Encoding if the String's encoding is ASCII compatible and the String is ASCII only" do
+ str = "abc".encode("utf-8")
+
+ Encoding.compatible?(str, Encoding::BINARY).should == Encoding::BINARY
+ Encoding.compatible?(str, Encoding::UTF_8).should == Encoding::UTF_8
+ Encoding.compatible?(str, Encoding::EUC_JP).should == Encoding::EUC_JP
+ Encoding.compatible?(str, Encoding::Shift_JIS).should == Encoding::Shift_JIS
+ end
+
+ it "returns nil if the String's encoding is ASCII compatible but the string is not ASCII only" do
+ Encoding.compatible?("\u3042".encode("utf-8"), Encoding::BINARY).should == nil
+ end
+end
+
+describe "Encoding.compatible? Regexp, String" do
+ it "returns US-ASCII if both are US-ASCII" do
+ str = "abc".dup.force_encoding("us-ascii")
+ Encoding.compatible?(/abc/, str).should == Encoding::US_ASCII
+ end
+
+ it "returns the String's Encoding when the String is ASCII only with a different encoding" do
+ r = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp"))
+ Encoding.compatible?(r, "hello".dup.force_encoding("utf-8")).should == Encoding::UTF_8
+ end
+
+ it "returns the Regexp's Encoding if the String has the same non-ASCII encoding" do
+ r = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp"))
+ Encoding.compatible?(r, "hello".dup.force_encoding("euc-jp")).should == Encoding::EUC_JP
+ end
+end
+
+describe "Encoding.compatible? Regexp, Regexp" do
+ it "returns US-ASCII if both are US-ASCII" do
+ Encoding.compatible?(/abc/, /def/).should == Encoding::US_ASCII
+ end
+
+ it "returns the first's Encoding if it is not US-ASCII and not ASCII only" do
+ [ [Encoding, Regexp.new("\xff"), Encoding::BINARY],
+ [Encoding, Regexp.new("\u3042".encode("utf-8")), Encoding::UTF_8],
+ [Encoding, Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")), Encoding::EUC_JP],
+ [Encoding, Regexp.new("\x82\xa0".dup.force_encoding("shift_jis")), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, /abc/)
+ end
+end
+
+describe "Encoding.compatible? Regexp, Symbol" do
+ it "returns US-ASCII if both are US-ASCII" do
+ Encoding.compatible?(/abc/, :def).should == Encoding::US_ASCII
+ end
+
+ it "returns the first's Encoding if it is not US-ASCII and not ASCII only" do
+ [ [Encoding, Regexp.new("\xff"), Encoding::BINARY],
+ [Encoding, Regexp.new("\u3042".encode("utf-8")), Encoding::UTF_8],
+ [Encoding, Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp")), Encoding::EUC_JP],
+ [Encoding, Regexp.new("\x82\xa0".dup.force_encoding("shift_jis")), Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, /abc/)
+ end
+end
+
+describe "Encoding.compatible? Symbol, String" do
+ it "returns US-ASCII if both are ASCII only" do
+ str = "abc".dup.force_encoding("us-ascii")
+ Encoding.compatible?(str, :abc).should == Encoding::US_ASCII
+ end
+end
+
+describe "Encoding.compatible? Symbol, Regexp" do
+ it "returns US-ASCII if both are US-ASCII" do
+ Encoding.compatible?(:abc, /def/).should == Encoding::US_ASCII
+ end
+
+ it "returns the Regexp's Encoding if it is not US-ASCII and not ASCII only" do
+ a = Regexp.new("\xff")
+ b = Regexp.new("\u3042".encode("utf-8"))
+ c = Regexp.new("\xa4\xa2".dup.force_encoding("euc-jp"))
+ d = Regexp.new("\x82\xa0".dup.force_encoding("shift_jis"))
+
+ [ [Encoding, :abc, a, Encoding::BINARY],
+ [Encoding, :abc, b, Encoding::UTF_8],
+ [Encoding, :abc, c, Encoding::EUC_JP],
+ [Encoding, :abc, d, Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?)
+ end
+end
+
+describe "Encoding.compatible? Symbol, Symbol" do
+ it "returns US-ASCII if both are US-ASCII" do
+ Encoding.compatible?(:abc, :def).should == Encoding::US_ASCII
+ end
+
+ it "returns the first's Encoding if it is not ASCII only" do
+ [ [Encoding, "\xff".to_sym, Encoding::BINARY],
+ [Encoding, "\u3042".encode("utf-8").to_sym, Encoding::UTF_8],
+ [Encoding, "\xa4\xa2".dup.force_encoding("euc-jp").to_sym, Encoding::EUC_JP],
+ [Encoding, "\x82\xa0".dup.force_encoding("shift_jis").to_sym, Encoding::Shift_JIS],
+ ].should be_computed_by(:compatible?, :abc)
+ end
+end
+
+describe "Encoding.compatible? Encoding, Encoding" do
+ it "returns nil if one of the encodings is a dummy encoding" do
+ [ [Encoding, Encoding::UTF_7, Encoding::US_ASCII, nil],
+ [Encoding, Encoding::US_ASCII, Encoding::UTF_7, nil],
+ [Encoding, Encoding::EUC_JP, Encoding::UTF_7, nil],
+ [Encoding, Encoding::UTF_7, Encoding::EUC_JP, nil],
+ [Encoding, Encoding::UTF_7, Encoding::BINARY, nil],
+ [Encoding, Encoding::BINARY, Encoding::UTF_7, nil],
+ ].should be_computed_by(:compatible?)
+ end
+
+ it "returns nil if one of the encodings is not US-ASCII" do
+ [ [Encoding, Encoding::UTF_8, Encoding::BINARY, nil],
+ [Encoding, Encoding::BINARY, Encoding::UTF_8, nil],
+ [Encoding, Encoding::BINARY, Encoding::EUC_JP, nil],
+ [Encoding, Encoding::Shift_JIS, Encoding::EUC_JP, nil],
+ ].should be_computed_by(:compatible?)
+ end
+
+ it "returns the first if the second is US-ASCII" do
+ [ [Encoding, Encoding::UTF_8, Encoding::US_ASCII, Encoding::UTF_8],
+ [Encoding, Encoding::EUC_JP, Encoding::US_ASCII, Encoding::EUC_JP],
+ [Encoding, Encoding::Shift_JIS, Encoding::US_ASCII, Encoding::Shift_JIS],
+ [Encoding, Encoding::BINARY, Encoding::US_ASCII, Encoding::BINARY],
+ ].should be_computed_by(:compatible?)
+ end
+
+ it "returns the Encoding if both are the same" do
+ [ [Encoding, Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8],
+ [Encoding, Encoding::US_ASCII, Encoding::US_ASCII, Encoding::US_ASCII],
+ [Encoding, Encoding::BINARY, Encoding::BINARY, Encoding::BINARY],
+ [Encoding, Encoding::UTF_7, Encoding::UTF_7, Encoding::UTF_7],
+ ].should be_computed_by(:compatible?)
+ end
+end
+
+describe "Encoding.compatible? Object, Object" do
+ it "returns nil for Object, String" do
+ Encoding.compatible?(Object.new, "abc").should == nil
+ end
+
+ it "returns nil for Object, Regexp" do
+ Encoding.compatible?(Object.new, /./).should == nil
+ end
+
+ it "returns nil for Object, Symbol" do
+ Encoding.compatible?(Object.new, :sym).should == nil
+ end
+
+ it "returns nil for String, Object" do
+ Encoding.compatible?("abc", Object.new).should == nil
+ end
+
+ it "returns nil for Regexp, Object" do
+ Encoding.compatible?(/./, Object.new).should == nil
+ end
+
+ it "returns nil for Symbol, Object" do
+ Encoding.compatible?(:sym, Object.new).should == nil
+ end
+end
+
+describe "Encoding.compatible? nil, nil" do
+ it "returns nil" do
+ Encoding.compatible?(nil, nil).should == nil
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb b/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb
new file mode 100644
index 0000000000..07c7a88356
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/asciicompat_encoding_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter.asciicompat_encoding" do
+ it "accepts an encoding name as a String argument" do
+ -> { Encoding::Converter.asciicompat_encoding('UTF-8') }.
+ should_not raise_error
+ end
+
+ it "coerces non-String/Encoding objects with #to_str" do
+ str = mock('string')
+ str.should_receive(:to_str).at_least(1).times.and_return('string')
+ Encoding::Converter.asciicompat_encoding(str)
+ end
+
+ it "accepts an Encoding object as an argument" do
+ Encoding::Converter.
+ asciicompat_encoding(Encoding.find("ISO-2022-JP")).
+ should == Encoding::Converter.asciicompat_encoding("ISO-2022-JP")
+ end
+
+ it "returns a corresponding ASCII compatible encoding for ASCII-incompatible encodings" do
+ Encoding::Converter.asciicompat_encoding('UTF-16BE').should == Encoding::UTF_8
+ Encoding::Converter.asciicompat_encoding("ISO-2022-JP").should == Encoding.find("stateless-ISO-2022-JP")
+ end
+
+ it "returns nil when the given encoding is ASCII compatible" do
+ Encoding::Converter.asciicompat_encoding('ASCII').should == nil
+ Encoding::Converter.asciicompat_encoding('UTF-8').should == nil
+ end
+
+ it "handles encoding names who resolve to nil encodings" do
+ internal = Encoding.default_internal
+ Encoding.default_internal = nil
+ Encoding::Converter.asciicompat_encoding('internal').should == nil
+ Encoding.default_internal = internal
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/constants_spec.rb b/spec/ruby/core/encoding/converter/constants_spec.rb
new file mode 100644
index 0000000000..e2d21b5429
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/constants_spec.rb
@@ -0,0 +1,131 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter::INVALID_MASK" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:INVALID_MASK, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::INVALID_MASK.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::INVALID_REPLACE" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:INVALID_REPLACE, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::INVALID_REPLACE.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::UNDEF_MASK" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:UNDEF_MASK, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::UNDEF_MASK.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::UNDEF_REPLACE" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:UNDEF_REPLACE, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::UNDEF_REPLACE.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::UNDEF_HEX_CHARREF" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:UNDEF_HEX_CHARREF, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::UNDEF_HEX_CHARREF.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::PARTIAL_INPUT" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:PARTIAL_INPUT, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::PARTIAL_INPUT.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::AFTER_OUTPUT" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:AFTER_OUTPUT, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::AFTER_OUTPUT.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::UNIVERSAL_NEWLINE_DECORATOR" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:UNIVERSAL_NEWLINE_DECORATOR, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::UNIVERSAL_NEWLINE_DECORATOR.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::CRLF_NEWLINE_DECORATOR" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:CRLF_NEWLINE_DECORATOR, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::CRLF_NEWLINE_DECORATOR.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::CR_NEWLINE_DECORATOR" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:CR_NEWLINE_DECORATOR, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::CR_NEWLINE_DECORATOR.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::XML_TEXT_DECORATOR" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:XML_TEXT_DECORATOR, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::XML_TEXT_DECORATOR.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::XML_ATTR_CONTENT_DECORATOR" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:XML_ATTR_CONTENT_DECORATOR, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::XML_ATTR_CONTENT_DECORATOR.should.instance_of?(Integer)
+ end
+end
+
+describe "Encoding::Converter::XML_ATTR_QUOTE_DECORATOR" do
+ it "exists" do
+ Encoding::Converter.should.const_defined?(:XML_ATTR_QUOTE_DECORATOR, false)
+ end
+
+ it "has an Integer value" do
+ Encoding::Converter::XML_ATTR_QUOTE_DECORATOR.should.instance_of?(Integer)
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/convert_spec.rb b/spec/ruby/core/encoding/converter/convert_spec.rb
new file mode 100644
index 0000000000..c95e88a491
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/convert_spec.rb
@@ -0,0 +1,45 @@
+# encoding: binary
+# frozen_string_literal: true
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#convert" do
+ it "returns a String" do
+ ec = Encoding::Converter.new('ascii', 'utf-8')
+ ec.convert('glark').should.instance_of?(String)
+ end
+
+ it "sets the encoding of the result to the target encoding" do
+ ec = Encoding::Converter.new('ascii', 'utf-8')
+ str = 'glark'.dup.force_encoding('ascii')
+ ec.convert(str).encoding.should == Encoding::UTF_8
+ end
+
+ it "transcodes the given String to the target encoding" do
+ ec = Encoding::Converter.new("utf-8", "euc-jp")
+ ec.convert("\u3042".dup.force_encoding('UTF-8')).should == \
+ "\xA4\xA2".dup.force_encoding('EUC-JP')
+ end
+
+ it "allows Strings of different encodings to the source encoding" do
+ ec = Encoding::Converter.new('ascii', 'utf-8')
+ str = 'glark'.dup.force_encoding('SJIS')
+ ec.convert(str).encoding.should == Encoding::UTF_8
+ end
+
+ it "reuses the given encoding pair if called multiple times" do
+ ec = Encoding::Converter.new('ascii', 'SJIS')
+ ec.convert('a'.dup.force_encoding('ASCII')).should == 'a'.dup.force_encoding('SJIS')
+ ec.convert('b'.dup.force_encoding('ASCII')).should == 'b'.dup.force_encoding('SJIS')
+ end
+
+ it "raises UndefinedConversionError if the String contains characters invalid for the target encoding" do
+ ec = Encoding::Converter.new('UTF-8', Encoding.find('macCyrillic'))
+ -> { ec.convert("\u{6543}".dup.force_encoding('UTF-8')) }.should.raise(Encoding::UndefinedConversionError)
+ end
+
+ it "raises an ArgumentError if called on a finished stream" do
+ ec = Encoding::Converter.new('UTF-8', Encoding.find('macCyrillic'))
+ ec.finish
+ -> { ec.convert("\u{65}") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/convpath_spec.rb b/spec/ruby/core/encoding/converter/convpath_spec.rb
new file mode 100644
index 0000000000..23f1e5dc33
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/convpath_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#convpath" do
+ it "returns an Array with a single element if there is a direct converter" do
+ cp = Encoding::Converter.new('ASCII', 'UTF-8').convpath
+ cp.should == [[Encoding::US_ASCII, Encoding::UTF_8]]
+ end
+
+ it "returns multiple encoding pairs when direct conversion is impossible" do
+ cp = Encoding::Converter.new('ascii','Big5').convpath
+ cp.should == [
+ [Encoding::US_ASCII, Encoding::UTF_8],
+ [Encoding::UTF_8, Encoding::Big5]
+ ]
+ end
+
+ it "indicates if crlf_newline conversion would occur" do
+ ec = Encoding::Converter.new("ISo-8859-1", "EUC-JP", crlf_newline: true)
+ ec.convpath.last.should == "crlf_newline"
+
+ ec = Encoding::Converter.new("ASCII", "UTF-8", crlf_newline: false)
+ ec.convpath.last.should_not == "crlf_newline"
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/destination_encoding_spec.rb b/spec/ruby/core/encoding/converter/destination_encoding_spec.rb
new file mode 100644
index 0000000000..481a857909
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/destination_encoding_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#destination_encoding" do
+ it "returns the destination encoding as an Encoding object" do
+ ec = Encoding::Converter.new('ASCII','Big5')
+ ec.destination_encoding.should == Encoding::BIG5
+
+ ec = Encoding::Converter.new('SJIS','EUC-JP')
+ ec.destination_encoding.should == Encoding::EUC_JP
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/finish_spec.rb b/spec/ruby/core/encoding/converter/finish_spec.rb
new file mode 100644
index 0000000000..e13b7415f8
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/finish_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#finish" do
+ before :each do
+ @ec = Encoding::Converter.new("utf-8", "iso-2022-jp")
+ end
+
+ it "returns a String" do
+ @ec.convert('foo')
+ @ec.finish.should.instance_of?(String)
+ end
+
+ it "returns an empty String if there is nothing more to convert" do
+ @ec.convert("glark")
+ @ec.finish.should == ""
+ end
+
+ it "returns the last part of the converted String if it hasn't already" do
+ @ec.convert("\u{9999}").should == "\e$B9a".dup.force_encoding('iso-2022-jp')
+ @ec.finish.should == "\e(B".dup.force_encoding('iso-2022-jp')
+ end
+
+ it "returns a String in the destination encoding" do
+ @ec.convert("glark")
+ @ec.finish.encoding.should == Encoding::ISO2022_JP
+ end
+
+ it "returns an empty String if self was not given anything to convert" do
+ @ec.finish.should == ""
+ end
+
+ it "returns an empty String on subsequent invocations" do
+ @ec.finish.should == ""
+ @ec.finish.should == ""
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/insert_output_spec.rb b/spec/ruby/core/encoding/converter/insert_output_spec.rb
new file mode 100644
index 0000000000..1346adde1e
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/insert_output_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#insert_output" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/encoding/converter/inspect_spec.rb b/spec/ruby/core/encoding/converter/inspect_spec.rb
new file mode 100644
index 0000000000..3170ee451f
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/inspect_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#inspect" do
+ it "includes the source and destination encodings in the return value" do
+ source = Encoding::UTF_8
+ destination = Encoding::UTF_16LE
+
+ output = "#<Encoding::Converter: #{source.name} to #{destination.name}>"
+
+ x = Encoding::Converter.new(source, destination)
+ x.inspect.should == output
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/last_error_spec.rb b/spec/ruby/core/encoding/converter/last_error_spec.rb
new file mode 100644
index 0000000000..3984a628f5
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/last_error_spec.rb
@@ -0,0 +1,91 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#last_error" do
+ it "returns nil when the no conversion has been attempted" do
+ ec = Encoding::Converter.new('ascii','utf-8')
+ ec.last_error.should == nil
+ end
+
+ it "returns nil when the last conversion did not produce an error" do
+ ec = Encoding::Converter.new('ascii','utf-8')
+ ec.convert('a'.dup.force_encoding('ascii'))
+ ec.last_error.should == nil
+ end
+
+ it "returns nil when #primitive_convert last returned :destination_buffer_full" do
+ ec = Encoding::Converter.new("utf-8", "iso-2022-jp")
+ ec.primitive_convert(+"\u{9999}", +"", 0, 0, partial_input: false) \
+ .should == :destination_buffer_full
+ ec.last_error.should == nil
+ end
+
+ it "returns nil when #primitive_convert last returned :finished" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert("glark".dup.force_encoding('utf-8'), +"").should == :finished
+ ec.last_error.should == nil
+ end
+
+ it "returns nil if the last conversion succeeded but the penultimate failed" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert(+"\xf1abcd", +"").should == :invalid_byte_sequence
+ ec.primitive_convert("glark".dup.force_encoding('utf-8'), +"").should == :finished
+ ec.last_error.should == nil
+ end
+
+ it "returns an Encoding::InvalidByteSequenceError when #primitive_convert last returned :invalid_byte_sequence" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert(+"\xf1abcd", +"").should == :invalid_byte_sequence
+ ec.last_error.should.instance_of?(Encoding::InvalidByteSequenceError)
+ end
+
+ it "returns an Encoding::UndefinedConversionError when #primitive_convert last returned :undefined_conversion" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert(+"\u{9876}", +"").should == :undefined_conversion
+ ec.last_error.should.instance_of?(Encoding::UndefinedConversionError)
+ end
+
+ it "returns an Encoding::InvalidByteSequenceError when #primitive_convert last returned :incomplete_input" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ ec.primitive_convert(+"\xa4", +"", nil, 10).should == :incomplete_input
+ ec.last_error.should.instance_of?(Encoding::InvalidByteSequenceError)
+ end
+
+ it "returns an Encoding::InvalidByteSequenceError when the last call to #convert produced one" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ exception = nil
+ -> {
+ ec.convert("\xf1abcd")
+ }.should.raise(Encoding::InvalidByteSequenceError) { |e|
+ exception = e
+ }
+ ec.last_error.should.instance_of?(Encoding::InvalidByteSequenceError)
+ ec.last_error.message.should == exception.message
+ end
+
+ it "returns an Encoding::UndefinedConversionError when the last call to #convert produced one" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ exception = nil
+ -> {
+ ec.convert("\u{9899}")
+ }.should.raise(Encoding::UndefinedConversionError) { |e|
+ exception = e
+ }
+ ec.last_error.should.instance_of?(Encoding::UndefinedConversionError)
+ ec.last_error.message.should == exception.message
+ ec.last_error.message.should.include? "from UTF-8 to ISO-8859-1"
+ end
+
+ it "returns the last error of #convert with a message showing the transcoding path" do
+ ec = Encoding::Converter.new("iso-8859-1", "Big5")
+ exception = nil
+ -> {
+ ec.convert("\xE9") # é in ISO-8859-1
+ }.should.raise(Encoding::UndefinedConversionError) { |e|
+ exception = e
+ }
+ ec.last_error.should.instance_of?(Encoding::UndefinedConversionError)
+ ec.last_error.message.should == exception.message
+ ec.last_error.message.should.include? "from ISO-8859-1 to UTF-8 to Big5"
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/new_spec.rb b/spec/ruby/core/encoding/converter/new_spec.rb
new file mode 100644
index 0000000000..bbdfb48bce
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/new_spec.rb
@@ -0,0 +1,119 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter.new" do
+ it "accepts a String for the source encoding" do
+ conv = Encoding::Converter.new("us-ascii", "utf-8")
+ conv.source_encoding.should == Encoding::US_ASCII
+ end
+
+ it "accepts a String for the destination encoding" do
+ conv = Encoding::Converter.new("us-ascii", "utf-8")
+ conv.destination_encoding.should == Encoding::UTF_8
+ end
+
+ it "accepts an Encoding object for the source encoding" do
+ conv = Encoding::Converter.new(Encoding::US_ASCII, "utf-8")
+ conv.source_encoding.should == Encoding::US_ASCII
+ end
+
+ it "accepts an Encoding object for the destination encoding" do
+ conv = Encoding::Converter.new("us-ascii", Encoding::UTF_8)
+ conv.destination_encoding.should == Encoding::UTF_8
+ end
+
+ it "raises an Encoding::ConverterNotFoundError if both encodings are the same" do
+ -> do
+ Encoding::Converter.new "utf-8", "utf-8"
+ end.should.raise(Encoding::ConverterNotFoundError)
+ end
+
+ it "calls #to_str to convert the source encoding argument to an encoding name" do
+ enc = mock("us-ascii")
+ enc.should_receive(:to_str).and_return("us-ascii")
+ conv = Encoding::Converter.new(enc, "utf-8")
+ conv.source_encoding.should == Encoding::US_ASCII
+ end
+
+ it "calls #to_str to convert the destination encoding argument to an encoding name" do
+ enc = mock("utf-8")
+ enc.should_receive(:to_str).and_return("utf-8")
+ conv = Encoding::Converter.new("us-ascii", enc)
+ conv.destination_encoding.should == Encoding::UTF_8
+ end
+
+ it "sets replacement from the options Hash" do
+ conv = Encoding::Converter.new("us-ascii", "utf-8", replace: "fubar")
+ conv.replacement.should == "fubar"
+ end
+
+ it "calls #to_hash to convert the options argument to a Hash if not an Integer" do
+ opts = mock("encoding converter options")
+ opts.should_receive(:to_hash).and_return({ replace: "fubar" })
+ conv = Encoding::Converter.new("us-ascii", "utf-8", **opts)
+ conv.replacement.should == "fubar"
+ end
+
+ it "calls #to_str to convert the replacement object to a String" do
+ obj = mock("encoding converter replacement")
+ obj.should_receive(:to_str).and_return("fubar")
+ conv = Encoding::Converter.new("us-ascii", "utf-8", replace: obj)
+ conv.replacement.should == "fubar"
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ obj = mock("encoding converter replacement")
+ obj.should_receive(:to_str).and_return(1)
+
+ -> do
+ Encoding::Converter.new("us-ascii", "utf-8", replace: obj)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed true for the replacement object" do
+ -> do
+ Encoding::Converter.new("us-ascii", "utf-8", replace: true)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed false for the replacement object" do
+ -> do
+ Encoding::Converter.new("us-ascii", "utf-8", replace: false)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Integer for the replacement object" do
+ -> do
+ Encoding::Converter.new("us-ascii", "utf-8", replace: 1)
+ end.should.raise(TypeError)
+ end
+
+ it "accepts an empty String for the replacement object" do
+ conv = Encoding::Converter.new("us-ascii", "utf-8", replace: "")
+ conv.replacement.should == ""
+ end
+
+ describe "when passed nil for the replacement object" do
+ describe "when the destination encoding is not UTF-8" do
+ it "sets the replacement String to '?'" do
+ conv = Encoding::Converter.new("us-ascii", "binary", replace: nil)
+ conv.replacement.should == "?"
+ end
+
+ it "sets the replacement String encoding to US-ASCII" do
+ conv = Encoding::Converter.new("us-ascii", "binary", replace: nil)
+ conv.replacement.encoding.should == Encoding::US_ASCII
+ end
+
+ it "sets the replacement String to '\\uFFFD'" do
+ conv = Encoding::Converter.new("us-ascii", "utf-8", replace: nil)
+ conv.replacement.should == "\u{fffd}".dup.force_encoding("utf-8")
+ end
+
+ it "sets the replacement String encoding to UTF-8" do
+ conv = Encoding::Converter.new("us-ascii", "utf-8", replace: nil)
+ conv.replacement.encoding.should == Encoding::UTF_8
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/primitive_convert_spec.rb b/spec/ruby/core/encoding/converter/primitive_convert_spec.rb
new file mode 100644
index 0000000000..ab9ce6a992
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/primitive_convert_spec.rb
@@ -0,0 +1,216 @@
+# encoding: binary
+# frozen_string_literal: false
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#primitive_convert" do
+ before :each do
+ @ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ end
+
+ it "accepts a nil source buffer" do
+ -> { @ec.primitive_convert(nil,"") }.should_not.raise
+ end
+
+ it "accepts a String as the source buffer" do
+ -> { @ec.primitive_convert("","") }.should_not.raise
+ end
+
+ it "raises FrozenError when the destination buffer is a frozen String" do
+ -> { @ec.primitive_convert("", "".freeze) }.should.raise(FrozenError)
+ end
+
+ it "accepts nil for the destination byte offset" do
+ -> { @ec.primitive_convert("","", nil) }.should_not.raise
+ end
+
+ it "accepts an integer for the destination byte offset" do
+ -> { @ec.primitive_convert("","a", 1) }.should_not.raise
+ end
+
+ it "calls #to_int to convert the destination byte offset" do
+ offset = mock("encoding primitive_convert destination byte offset")
+ offset.should_receive(:to_int).and_return(2)
+ @ec.primitive_convert("abc", result = " ", offset).should == :finished
+ result.should == " abc"
+ end
+
+ it "raises an ArgumentError if the destination byte offset is greater than the bytesize of the destination buffer" do
+ -> { @ec.primitive_convert("","am", 0) }.should_not.raise
+ -> { @ec.primitive_convert("","am", 1) }.should_not.raise
+ -> { @ec.primitive_convert("","am", 2) }.should_not.raise
+ -> { @ec.primitive_convert("","am", 3) }.should.raise(ArgumentError)
+ end
+
+ it "uses the destination byte offset to determine where to write the result in the destination buffer" do
+ dest = "aa"
+ @ec.primitive_convert("b",dest, nil, 0)
+ dest.should == "aa"
+
+ @ec.primitive_convert("b",dest, nil, 1)
+ dest.should == "aab"
+
+ @ec.primitive_convert("b",dest, nil, 2)
+ dest.should == "aabbb"
+ end
+
+ it "accepts nil for the destination bytesize" do
+ -> { @ec.primitive_convert("","", nil, nil) }.should_not.raise
+ end
+
+ it "accepts an integer for the destination bytesize" do
+ -> { @ec.primitive_convert("","", nil, 0) }.should_not.raise
+ end
+
+ it "allows a destination bytesize value greater than the bytesize of the source buffer" do
+ -> { @ec.primitive_convert("am","", nil, 3) }.should_not.raise
+ end
+
+ it "allows a destination bytesize value less than the bytesize of the source buffer" do
+ -> { @ec.primitive_convert("am","", nil, 1) }.should_not.raise
+ end
+
+ it "calls #to_int to convert the destination byte size" do
+ size = mock("encoding primitive_convert destination byte size")
+ size.should_receive(:to_int).and_return(2)
+ @ec.primitive_convert("abc", result = " ", 0, size).should == :destination_buffer_full
+ result.should == "ab"
+ end
+
+ it "uses destination bytesize as the maximum bytesize of the destination buffer" do
+ dest = ""
+ @ec.primitive_convert("glark", dest, nil, 1)
+ dest.bytesize.should == 1
+ end
+
+ it "allows a destination buffer of unlimited size if destination bytesize is nil" do
+ source = "glark".force_encoding('utf-8')
+ dest = ""
+ @ec.primitive_convert("glark", dest, nil, nil)
+ dest.bytesize.should == source.bytesize
+ end
+
+ it "accepts an options hash" do
+ @ec.primitive_convert("","",nil,nil, after_output: true).should == :finished
+ end
+
+ it "sets the destination buffer's encoding to the destination encoding if the conversion succeeded" do
+ dest = "".force_encoding('utf-8')
+ dest.encoding.should == Encoding::UTF_8
+ @ec.primitive_convert("\u{98}",dest).should == :finished
+ dest.encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "sets the destination buffer's encoding to the destination encoding if the conversion failed" do
+ dest = "".force_encoding('utf-8')
+ dest.encoding.should == Encoding::UTF_8
+ @ec.primitive_convert("\u{9878}",dest).should == :undefined_conversion
+ dest.encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "removes the undefined part from the source buffer when returning :undefined_conversion" do
+ dest = "".force_encoding('utf-8')
+ s = "\u{9878}abcd"
+ @ec.primitive_convert(s, dest).should == :undefined_conversion
+
+ s.should == "abcd"
+ end
+
+ it "returns :incomplete_input when source buffer ends unexpectedly and :partial_input isn't specified" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ ec.primitive_convert("\xa4", "", nil, nil, partial_input: false).should == :incomplete_input
+ end
+
+ it "clears the source buffer when returning :incomplete_input" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ s = "\xa4"
+ ec.primitive_convert(s, "").should == :incomplete_input
+
+ s.should == ""
+ end
+
+ it "returns :source_buffer_empty when source buffer ends unexpectedly and :partial_input is true" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ ec.primitive_convert("\xa4", "", nil, nil, partial_input: true).should == :source_buffer_empty
+ end
+
+ it "clears the source buffer when returning :source_buffer_empty" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ s = "\xa4"
+ ec.primitive_convert(s, "", nil, nil, partial_input: true).should == :source_buffer_empty
+
+ s.should == ""
+ end
+
+ it "returns :undefined_conversion when a character in the source buffer is not representable in the output encoding" do
+ @ec.primitive_convert("\u{9876}","").should == :undefined_conversion
+ end
+
+ it "returns :invalid_byte_sequence when an invalid byte sequence was found in the source buffer" do
+ @ec.primitive_convert("\xf1abcd","").should == :invalid_byte_sequence
+ end
+
+ it "removes consumed and erroneous bytes from the source buffer when returning :invalid_byte_sequence" do
+ ec = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC)
+ s = "\xC3\xA1\x80\x80\xC3\xA1".force_encoding("utf-8")
+ dest = "".force_encoding("utf-8")
+ ec.primitive_convert(s, dest)
+
+ s.should == "\x80\xC3\xA1".force_encoding("utf-8")
+ end
+
+ it "returns :finished when the conversion succeeded" do
+ @ec.primitive_convert("glark".force_encoding('utf-8'),"").should == :finished
+ end
+
+ it "clears the source buffer when returning :finished" do
+ s = "glark".force_encoding('utf-8')
+ @ec.primitive_convert(s, "").should == :finished
+
+ s.should == ""
+ end
+
+ it "returns :destination_buffer_full when the destination buffer is too small" do
+ ec = Encoding::Converter.new("utf-8", "iso-2022-jp")
+ source = "\u{9999}"
+ destination_bytesize = source.bytesize - 1
+ ec.primitive_convert(source, "", 0, destination_bytesize) \
+ .should == :destination_buffer_full
+ source.should == ""
+ end
+
+ it "clears the source buffer when returning :destination_buffer_full" do
+ ec = Encoding::Converter.new("utf-8", "iso-2022-jp")
+ s = "\u{9999}"
+ destination_bytesize = s.bytesize - 1
+ ec.primitive_convert(s, "", 0, destination_bytesize).should == :destination_buffer_full
+
+ s.should == ""
+ end
+
+ it "keeps removing invalid bytes from the source buffer" do
+ ec = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC)
+ s = "\x80\x80\x80"
+ dest = "".force_encoding(Encoding::UTF_8_MAC)
+
+ ec.primitive_convert(s, dest)
+ s.should == "\x80\x80"
+ ec.primitive_convert(s, dest)
+ s.should == "\x80"
+ ec.primitive_convert(s, dest)
+ s.should == ""
+ end
+
+ it "reuses read-again bytes after the first error" do
+ s = "\xf1abcd"
+ dest = ""
+
+ @ec.primitive_convert(s, dest).should == :invalid_byte_sequence
+ s.should == "bcd"
+ @ec.primitive_errinfo[4].should == "a"
+
+ @ec.primitive_convert(s, dest).should == :finished
+ s.should == ""
+
+ dest.should == "abcd"
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb b/spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb
new file mode 100644
index 0000000000..580e2e37e1
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/primitive_errinfo_spec.rb
@@ -0,0 +1,69 @@
+# encoding: binary
+# frozen_string_literal: false
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#primitive_errinfo" do
+ it "returns [:source_buffer_empty,nil,nil,nil,nil] when no conversion has been attempted" do
+ ec = Encoding::Converter.new('ascii','utf-8')
+ ec.primitive_errinfo.should == [:source_buffer_empty, nil, nil, nil, nil]
+ end
+
+ it "returns [:finished,nil,nil,nil,nil] when #primitive_convert last returned :finished" do
+ ec = Encoding::Converter.new('ascii','utf-8')
+ ec.primitive_convert("a","").should == :finished
+ ec.primitive_errinfo.should == [:finished, nil, nil, nil, nil]
+ end
+
+ it "returns [:source_buffer_empty,nil,nil,nil, nil] when #convert last succeeded" do
+ ec = Encoding::Converter.new('ascii','utf-8')
+ ec.convert("a".force_encoding('ascii')).should == "a".force_encoding('utf-8')
+ ec.primitive_errinfo.should == [:source_buffer_empty, nil, nil, nil, nil]
+ end
+
+ it "returns [:destination_buffer_full,nil,nil,nil,nil] when #primitive_convert last returned :destination_buffer_full" do
+ ec = Encoding::Converter.new("utf-8", "iso-2022-jp")
+ ec.primitive_convert("\u{9999}", "", 0, 0, partial_input: false).should == :destination_buffer_full
+ ec.primitive_errinfo.should == [:destination_buffer_full, nil, nil, nil, nil]
+ end
+
+ it "returns the status of the last primitive conversion, even if it was successful and the previous one wasn't" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert("\xf1abcd","").should == :invalid_byte_sequence
+ ec.primitive_convert("glark".force_encoding('utf-8'),"").should == :finished
+ ec.primitive_errinfo.should == [:finished, nil, nil, nil, nil]
+ end
+
+ it "returns the state, source encoding, target encoding, and the erroneous bytes when #primitive_convert last returned :undefined_conversion" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert("\u{9876}","").should == :undefined_conversion
+ ec.primitive_errinfo.should ==
+ [:undefined_conversion, "UTF-8", "ISO-8859-1", "\xE9\xA1\xB6", ""]
+ end
+
+ it "returns the state, source encoding, target encoding, and erroneous bytes when #primitive_convert last returned :incomplete_input" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ ec.primitive_convert("\xa4", "", nil, 10).should == :incomplete_input
+ ec.primitive_errinfo.should == [:incomplete_input, "EUC-JP", "UTF-8", "\xA4", ""]
+ end
+
+ it "returns the state, source encoding, target encoding, erroneous bytes, and the read-again bytes when #primitive_convert last returned :invalid_byte_sequence" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ ec.primitive_convert("\xf1abcd","").should == :invalid_byte_sequence
+ ec.primitive_errinfo.should ==
+ [:invalid_byte_sequence, "UTF-8", "ISO-8859-1", "\xF1", "a"]
+ end
+
+ it "returns the state, source encoding, target encoding, erroneous bytes, and the read-again bytes when #convert last raised InvalidByteSequenceError" do
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ -> { ec.convert("\xf1abcd") }.should.raise(Encoding::InvalidByteSequenceError)
+ ec.primitive_errinfo.should ==
+ [:invalid_byte_sequence, "UTF-8", "ISO-8859-1", "\xF1", "a"]
+ end
+
+ it "returns the state, source encoding, target encoding, erroneous bytes, and the read-again bytes when #finish last raised InvalidByteSequenceError" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ ec.convert("\xa4")
+ -> { ec.finish }.should.raise(Encoding::InvalidByteSequenceError)
+ ec.primitive_errinfo.should == [:incomplete_input, "EUC-JP", "UTF-8", "\xA4", ""]
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/putback_spec.rb b/spec/ruby/core/encoding/converter/putback_spec.rb
new file mode 100644
index 0000000000..a85cec5145
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/putback_spec.rb
@@ -0,0 +1,56 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#putback" do
+ before :each do
+ @ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ @ret = @ec.primitive_convert(@src=+"abc\xa1def", @dst=+"", nil, 10)
+ end
+
+ it "returns a String" do
+ @ec.putback.should.instance_of?(String)
+ end
+
+ it "returns a String in the source encoding" do
+ @ec.putback.encoding.should == Encoding::EUC_JP
+ end
+
+ it "returns the bytes buffered due to an :invalid_byte_sequence error" do
+ @ret.should == :invalid_byte_sequence
+ @ec.putback.should == 'd'
+ @ec.primitive_errinfo.last.should == 'd'
+ end
+
+ it "allows conversion to be resumed after an :invalid_byte_sequence" do
+ @src = @ec.putback + @src
+ @ret = @ec.primitive_convert(@src, @dst, nil, 10)
+ @ret.should == :finished
+ @dst.should == "abcdef"
+ @src.should == ""
+ end
+
+ it "returns an empty String when there are no more bytes to put back" do
+ @ec.putback
+ @ec.putback.should == ""
+ end
+
+ it "returns the problematic bytes for UTF-16LE" do
+ ec = Encoding::Converter.new("utf-16le", "iso-8859-1")
+ src = +"\x00\xd8\x61\x00"
+ dst = +""
+ ec.primitive_convert(src, dst).should == :invalid_byte_sequence
+ ec.primitive_errinfo.should == [:invalid_byte_sequence, "UTF-16LE", "UTF-8", "\x00\xD8", "a\x00"]
+ ec.putback.should == "a\x00".dup.force_encoding("utf-16le")
+ ec.putback.should == ""
+ end
+
+ it "accepts an integer argument corresponding to the number of bytes to be put back" do
+ ec = Encoding::Converter.new("utf-16le", "iso-8859-1")
+ src = +"\x00\xd8\x61\x00"
+ dst = +""
+ ec.primitive_convert(src, dst).should == :invalid_byte_sequence
+ ec.primitive_errinfo.should == [:invalid_byte_sequence, "UTF-16LE", "UTF-8", "\x00\xD8", "a\x00"]
+ ec.putback(2).should == "a\x00".dup.force_encoding("utf-16le")
+ ec.putback.should == ""
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/replacement_spec.rb b/spec/ruby/core/encoding/converter/replacement_spec.rb
new file mode 100644
index 0000000000..c25ec36517
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/replacement_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#replacement" do
+ it "returns '?' in US-ASCII when the destination encoding is not UTF-8" do
+ ec = Encoding::Converter.new("utf-8", "us-ascii")
+ ec.replacement.should == "?"
+ ec.replacement.encoding.should == Encoding::US_ASCII
+
+ ec = Encoding::Converter.new("utf-8", "sjis")
+ ec.replacement.should == "?"
+ ec.replacement.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns \\uFFFD when the destination encoding is UTF-8" do
+ ec = Encoding::Converter.new("us-ascii", "utf-8")
+ ec.replacement.should == "\u{fffd}".dup.force_encoding('utf-8')
+ ec.replacement.encoding.should == Encoding::UTF_8
+ end
+end
+
+describe "Encoding::Converter#replacement=" do
+ it "accepts a String argument" do
+ ec = Encoding::Converter.new("utf-8", "us-ascii")
+ ec.replacement = "!"
+ ec.replacement.should == "!"
+ end
+
+ it "accepts a String argument of arbitrary length" do
+ ec = Encoding::Converter.new("utf-8", "us-ascii")
+ ec.replacement = "?!?" * 9999
+ ec.replacement.should == "?!?" * 9999
+ end
+
+ it "raises a TypeError if assigned a non-String argument" do
+ ec = Encoding::Converter.new("utf-8", "us-ascii")
+ -> { ec.replacement = nil }.should.raise(TypeError)
+ end
+
+ it "sets #replacement" do
+ ec = Encoding::Converter.new("us-ascii", "utf-8")
+ ec.replacement.should == "\u{fffd}".dup.force_encoding('utf-8')
+ ec.replacement = '?'.encode('utf-8')
+ ec.replacement.should == '?'.dup.force_encoding('utf-8')
+ end
+
+ it "raises an UndefinedConversionError is the argument cannot be converted into the destination encoding" do
+ ec = Encoding::Converter.new("sjis", "ascii")
+ utf8_q = "\u{986}".dup.force_encoding('utf-8')
+ ec.primitive_convert(utf8_q.dup, +"").should == :undefined_conversion
+ -> { ec.replacement = utf8_q }.should.raise(Encoding::UndefinedConversionError)
+ end
+
+ it "does not change the replacement character if the argument cannot be converted into the destination encoding" do
+ ec = Encoding::Converter.new("sjis", "ascii")
+ utf8_q = "\u{986}".dup.force_encoding('utf-8')
+ ec.primitive_convert(utf8_q.dup, +"").should == :undefined_conversion
+ -> { ec.replacement = utf8_q }.should.raise(Encoding::UndefinedConversionError)
+ ec.replacement.should == "?".dup.force_encoding('us-ascii')
+ end
+
+ it "uses the replacement character" do
+ ec = Encoding::Converter.new("utf-8", "us-ascii", :invalid => :replace, :undef => :replace)
+ ec.replacement = "!"
+ dest = +""
+ status = ec.primitive_convert(+"中文123", dest)
+
+ status.should == :finished
+ dest.should == "!!123"
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/search_convpath_spec.rb b/spec/ruby/core/encoding/converter/search_convpath_spec.rb
new file mode 100644
index 0000000000..cac44765f8
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/search_convpath_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter.search_convpath" do
+ it "returns an Array with a single element if there is a direct converter" do
+ cp = Encoding::Converter.search_convpath('ASCII', 'UTF-8')
+ cp.should == [[Encoding::US_ASCII, Encoding::UTF_8]]
+ end
+
+ it "returns multiple encoding pairs when direct conversion is impossible" do
+ cp = Encoding::Converter.search_convpath('ascii','Big5')
+ cp.should == [
+ [Encoding::US_ASCII, Encoding::UTF_8],
+ [Encoding::UTF_8, Encoding::Big5]
+ ]
+ end
+
+ it "indicates if crlf_newline conversion would occur" do
+ cp = Encoding::Converter.search_convpath("ISO-8859-1", "EUC-JP", crlf_newline: true)
+ cp.last.should == "crlf_newline"
+
+ cp = Encoding::Converter.search_convpath("ASCII", "UTF-8", crlf_newline: false)
+ cp.last.should_not == "crlf_newline"
+ end
+
+ it "raises an Encoding::ConverterNotFoundError if no conversion path exists" do
+ -> do
+ Encoding::Converter.search_convpath(Encoding::BINARY, Encoding::Emacs_Mule)
+ end.should.raise(Encoding::ConverterNotFoundError)
+ end
+end
diff --git a/spec/ruby/core/encoding/converter/source_encoding_spec.rb b/spec/ruby/core/encoding/converter/source_encoding_spec.rb
new file mode 100644
index 0000000000..6196f717bd
--- /dev/null
+++ b/spec/ruby/core/encoding/converter/source_encoding_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+
+describe "Encoding::Converter#source_encoding" do
+ it "returns the source encoding as an Encoding object" do
+ ec = Encoding::Converter.new('ASCII','Big5')
+ ec.source_encoding.should == Encoding::US_ASCII
+
+ ec = Encoding::Converter.new('Shift_JIS','EUC-JP')
+ ec.source_encoding.should == Encoding::SHIFT_JIS
+ end
+end
diff --git a/spec/ruby/core/encoding/default_external_spec.rb b/spec/ruby/core/encoding/default_external_spec.rb
new file mode 100644
index 0000000000..2a2bd7f6ae
--- /dev/null
+++ b/spec/ruby/core/encoding/default_external_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.default_external" do
+ before :each do
+ @original_encoding = Encoding.default_external
+ end
+
+ after :each do
+ Encoding.default_external = @original_encoding
+ end
+
+ it "returns an Encoding object" do
+ Encoding.default_external.should.instance_of?(Encoding)
+ end
+
+ it "returns the default external encoding" do
+ Encoding.default_external = Encoding::SHIFT_JIS
+ Encoding.default_external.should == Encoding::SHIFT_JIS
+ end
+
+ platform_is :windows do
+ it 'is UTF-8 by default on Windows' do
+ Encoding.default_external.should == Encoding::UTF_8
+ end
+ end
+end
+
+describe "Encoding.default_external=" do
+ before :each do
+ @original_encoding = Encoding.default_external
+ end
+
+ after :each do
+ Encoding.default_external = @original_encoding
+ end
+
+ it "sets the default external encoding" do
+ Encoding.default_external = Encoding::SHIFT_JIS
+ Encoding.default_external.should == Encoding::SHIFT_JIS
+ Encoding.find('external').should == Encoding::SHIFT_JIS
+ end
+
+ platform_is_not :windows do
+ it "also sets the filesystem encoding" do
+ Encoding.default_external = Encoding::SHIFT_JIS
+ Encoding.find('filesystem').should == Encoding::SHIFT_JIS
+ end
+ end
+
+ it "can accept a name of an encoding as a String" do
+ Encoding.default_external = 'Shift_JIS'
+ Encoding.default_external.should == Encoding::SHIFT_JIS
+ end
+
+ it "calls #to_s on arguments that are neither Strings nor Encodings" do
+ string = mock('string')
+ string.should_receive(:to_str).at_least(1).and_return('US-ASCII')
+ Encoding.default_external = string
+ Encoding.default_external.should == Encoding::ASCII
+ end
+
+ it "raises a TypeError unless the argument is an Encoding or convertible to a String" do
+ -> { Encoding.default_external = [] }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if the argument is nil" do
+ -> { Encoding.default_external = nil }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/encoding/default_internal_spec.rb b/spec/ruby/core/encoding/default_internal_spec.rb
new file mode 100644
index 0000000000..38aef9dce9
--- /dev/null
+++ b/spec/ruby/core/encoding/default_internal_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.default_internal" do
+ before :each do
+ @original_encoding = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @original_encoding
+ end
+
+ it "is nil by default" do
+ Encoding.default_internal.should == nil
+ end
+
+ it "returns an Encoding object if a default internal encoding is set" do
+ Encoding.default_internal = Encoding::ASCII
+ Encoding.default_internal.should.instance_of?(Encoding)
+ end
+
+ it "returns nil if no default internal encoding is set" do
+ Encoding.default_internal = nil
+ Encoding.default_internal.should == nil
+ end
+
+ it "returns the default internal encoding" do
+ Encoding.default_internal = Encoding::BINARY
+ Encoding.default_internal.should == Encoding::BINARY
+ end
+end
+
+describe "Encoding.default_internal=" do
+ before :each do
+ @original_encoding = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @original_encoding
+ end
+
+ it "sets the default internal encoding" do
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ Encoding.default_internal.should == Encoding::SHIFT_JIS
+ end
+
+ it "can accept a name of an encoding as a String" do
+ Encoding.default_internal = 'Shift_JIS'
+ Encoding.default_internal.should == Encoding::SHIFT_JIS
+ end
+
+ it "calls #to_str to convert an object to a String" do
+ obj = mock('string')
+ obj.should_receive(:to_str).at_least(1).times.and_return('ascii')
+
+ Encoding.default_internal = obj
+ Encoding.default_internal.should == Encoding::ASCII
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ obj = mock('string')
+ obj.should_receive(:to_str).at_least(1).times.and_return(1)
+
+ -> { Encoding.default_internal = obj }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed an object not providing #to_str" do
+ -> { Encoding.default_internal = mock("encoding") }.should.raise(TypeError)
+ end
+
+ it "accepts an argument of nil to unset the default internal encoding" do
+ Encoding.default_internal = nil
+ Encoding.default_internal.should == nil
+ end
+end
diff --git a/spec/ruby/core/encoding/dummy_spec.rb b/spec/ruby/core/encoding/dummy_spec.rb
new file mode 100644
index 0000000000..05530a8186
--- /dev/null
+++ b/spec/ruby/core/encoding/dummy_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+describe "Encoding#dummy?" do
+ it "returns false for proper encodings" do
+ Encoding::UTF_8.dummy?.should == false
+ Encoding::ASCII.dummy?.should == false
+ end
+
+ it "returns true for dummy encodings" do
+ Encoding::ISO_2022_JP.dummy?.should == true
+ Encoding::CP50221.dummy?.should == true
+ Encoding::UTF_7.dummy?.should == true
+ end
+
+ it "returns true for UTF_16 and UTF_32" do
+ Encoding::UTF_16.should.dummy?
+ Encoding::UTF_32.should.dummy?
+ end
+
+ it "implies not #ascii_compatible?" do
+ Encoding.list.select(&:dummy?).each do |encoding|
+ encoding.should_not.ascii_compatible?
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/find_spec.rb b/spec/ruby/core/encoding/find_spec.rb
new file mode 100644
index 0000000000..c5356560eb
--- /dev/null
+++ b/spec/ruby/core/encoding/find_spec.rb
@@ -0,0 +1,82 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.find" do
+ before :all do
+ @encodings = Encoding.aliases.to_a.flatten.uniq
+ end
+
+ it "returns the corresponding Encoding object if given a valid encoding name" do
+ @encodings.each do |enc|
+ Encoding.find(enc).should.instance_of?(Encoding)
+ end
+ end
+
+ it "returns the corresponding Encoding object if given a valid alias name" do
+ Encoding.aliases.keys.each do |enc_alias|
+ Encoding.find(enc_alias).should.instance_of?(Encoding)
+ end
+ end
+
+ it "raises a TypeError if passed a Symbol" do
+ -> { Encoding.find(:"utf-8") }.should.raise(TypeError)
+ end
+
+ it "returns the passed Encoding object" do
+ Encoding.find(Encoding::UTF_8).should == Encoding::UTF_8
+ end
+
+ it "accepts encoding names as Strings" do
+ Encoding.list.each do |enc|
+ Encoding.find(enc.name).should == enc
+ end
+ end
+
+ it "accepts any object as encoding name, if it responds to #to_str" do
+ obj = Class.new do
+ attr_writer :encoding_name
+ def to_str; @encoding_name; end
+ end.new
+
+ Encoding.list.each do |enc|
+ obj.encoding_name = enc.name
+ Encoding.find(obj).should == enc
+ end
+ end
+
+ it "is case insensitive" do
+ @encodings.each do |enc|
+ Encoding.find(enc.upcase).should == Encoding.find(enc)
+ end
+ end
+
+ it "raises an ArgumentError if the given encoding does not exist" do
+ -> { Encoding.find('dh2dh278d') }.should.raise(ArgumentError, 'unknown encoding name - dh2dh278d')
+ end
+
+ # Not sure how to do a better test, since locale depends on weird platform-specific stuff
+ it "supports the 'locale' encoding alias" do
+ enc = Encoding.find('locale')
+ enc.should_not == nil
+ end
+
+ it "returns default external encoding for the 'external' encoding alias" do
+ enc = Encoding.find('external')
+ enc.should == Encoding.default_external
+ end
+
+ it "returns default internal encoding for the 'internal' encoding alias" do
+ enc = Encoding.find('internal')
+ enc.should == Encoding.default_internal
+ end
+
+ platform_is_not :windows do
+ it "uses default external encoding for the 'filesystem' encoding alias" do
+ enc = Encoding.find('filesystem')
+ enc.should == Encoding.default_external
+ end
+ end
+
+ platform_is :windows do
+ it "needs to be reviewed for spec completeness"
+ end
+end
diff --git a/spec/ruby/core/encoding/fixtures/classes.rb b/spec/ruby/core/encoding/fixtures/classes.rb
new file mode 100644
index 0000000000..943865e8d8
--- /dev/null
+++ b/spec/ruby/core/encoding/fixtures/classes.rb
@@ -0,0 +1,49 @@
+# encoding: binary
+module EncodingSpecs
+ class UndefinedConversionError
+ def self.exception
+ ec = Encoding::Converter.new('utf-8','ascii')
+ begin
+ ec.convert("\u{8765}")
+ rescue Encoding::UndefinedConversionError => e
+ e
+ end
+ end
+ end
+
+ class UndefinedConversionErrorIndirect
+ def self.exception
+ ec = Encoding::Converter.new("ISO-8859-1", "EUC-JP")
+ begin
+ ec.convert("\xA0")
+ rescue Encoding::UndefinedConversionError => e
+ e
+ end
+ end
+ end
+
+ class InvalidByteSequenceError
+ def self.exception
+ ec = Encoding::Converter.new("utf-8", "iso-8859-1")
+ begin
+ ec.convert("\xf1abcd")
+ rescue Encoding::InvalidByteSequenceError => e
+ # Return the exception object and the primitive_errinfo Array
+ [e, ec.primitive_errinfo]
+ end
+ end
+ end
+
+ class InvalidByteSequenceErrorIndirect
+ def self.exception
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ begin
+ ec.convert("abc\xA1\xFFdef")
+ rescue Encoding::InvalidByteSequenceError => e
+ # Return the exception object and the discarded bytes reported by
+ # #primitive_errinfo
+ [e, ec.primitive_errinfo]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/inspect_spec.rb b/spec/ruby/core/encoding/inspect_spec.rb
new file mode 100644
index 0000000000..ab7f8cf9fc
--- /dev/null
+++ b/spec/ruby/core/encoding/inspect_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Encoding#inspect" do
+ it "returns a String" do
+ Encoding::UTF_8.inspect.should.instance_of?(String)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns #<Encoding:name> for a non-dummy encoding named 'name'" do
+ Encoding.list.to_a.reject {|e| e.dummy? }.each do |enc|
+ enc.inspect.should =~ /#<Encoding:#{enc.name}>/
+ end
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "returns #<Encoding:name> for a non-dummy encoding named 'name'" do
+ Encoding.list.to_a.reject {|e| e.dummy? }.each do |enc|
+ if enc.name == "ASCII-8BIT"
+ enc.inspect.should == "#<Encoding:BINARY (ASCII-8BIT)>"
+ else
+ enc.inspect.should =~ /#<Encoding:#{enc.name}>/
+ end
+ end
+ end
+ end
+
+ it "returns #<Encoding:name (dummy)> for a dummy encoding named 'name'" do
+ Encoding.list.to_a.select {|e| e.dummy? }.each do |enc|
+ enc.inspect.should =~ /#<Encoding:#{enc.name} \(dummy\)>/
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb
new file mode 100644
index 0000000000..7d3cc77c0b
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_name_spec.rb
@@ -0,0 +1,19 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::InvalidByteSequenceError#destination_encoding_name" do
+ before :each do
+ @exception, = EncodingSpecs::InvalidByteSequenceError.exception
+ @exception2, = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception
+ end
+
+ it "returns a String" do
+ @exception.destination_encoding_name.should.instance_of?(String)
+ @exception2.destination_encoding_name.should.instance_of?(String)
+ end
+
+ it "is equal to the destination encoding name of the object that raised it" do
+ @exception.destination_encoding_name.should == "ISO-8859-1"
+ @exception2.destination_encoding_name.should == "UTF-8"
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb
new file mode 100644
index 0000000000..264c409b1c
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/destination_encoding_spec.rb
@@ -0,0 +1,19 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::InvalidByteSequenceError#destination_encoding" do
+ before :each do
+ @exception, = EncodingSpecs::InvalidByteSequenceError.exception
+ @exception2, = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception
+ end
+
+ it "returns an Encoding object" do
+ @exception.destination_encoding.should.instance_of?(Encoding)
+ @exception2.destination_encoding.should.instance_of?(Encoding)
+ end
+
+ it "is equal to the destination encoding of the object that raised it" do
+ @exception.destination_encoding.should == Encoding::ISO_8859_1
+ @exception2.destination_encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb
new file mode 100644
index 0000000000..40a9a35caf
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/error_bytes_spec.rb
@@ -0,0 +1,31 @@
+# encoding: binary
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::InvalidByteSequenceError#error_bytes" do
+ before :each do
+ @exception, @errinfo = EncodingSpecs::InvalidByteSequenceError.exception
+ @exception2, @errinfo2 = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception
+ end
+
+ it "returns a String" do
+ @exception.error_bytes.should.instance_of?(String)
+ @exception2.error_bytes.should.instance_of?(String)
+ end
+
+ it "returns the bytes that caused the exception" do
+ @exception.error_bytes.size.should == 1
+ @exception.error_bytes.should == "\xF1"
+ @exception.error_bytes.should == @errinfo[-2]
+
+ @exception2.error_bytes.size.should == 1
+ @exception2.error_bytes.should == "\xA1"
+ @exception2.error_bytes.should == @errinfo2[-2]
+ end
+
+ it "uses BINARY as the encoding" do
+ @exception.error_bytes.encoding.should == Encoding::BINARY
+
+ @exception2.error_bytes.encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb
new file mode 100644
index 0000000000..143db7b6da
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/incomplete_input_spec.rb
@@ -0,0 +1,28 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+
+describe "Encoding::InvalidByteSequenceError#incomplete_input?" do
+ it "returns nil by default" do
+ Encoding::InvalidByteSequenceError.new.incomplete_input?.should == nil
+ end
+
+ it "returns true if #primitive_convert returned :incomplete_input for the same data" do
+ ec = Encoding::Converter.new("EUC-JP", "ISO-8859-1")
+ ec.primitive_convert(+"\xA1", +'').should == :incomplete_input
+ begin
+ ec.convert("\xA1")
+ rescue Encoding::InvalidByteSequenceError => e
+ e.incomplete_input?.should == true
+ end
+ end
+
+ it "returns false if #primitive_convert returned :invalid_byte_sequence for the same data" do
+ ec = Encoding::Converter.new("ascii", "utf-8")
+ ec.primitive_convert(+"\xfffffffff", +'').should == :invalid_byte_sequence
+ begin
+ ec.convert("\xfffffffff")
+ rescue Encoding::InvalidByteSequenceError => e
+ e.incomplete_input?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb
new file mode 100644
index 0000000000..e4fc81aac6
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/readagain_bytes_spec.rb
@@ -0,0 +1,31 @@
+# encoding: binary
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::InvalidByteSequenceError#readagain_bytes" do
+ before :each do
+ @exception, @errinfo = EncodingSpecs::InvalidByteSequenceError.exception
+ @exception2, @errinfo2 = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception
+ end
+
+ it "returns a String" do
+ @exception.readagain_bytes.should.instance_of?(String)
+ @exception2.readagain_bytes.should.instance_of?(String)
+ end
+
+ it "returns the bytes to be read again" do
+ @exception.readagain_bytes.size.should == 1
+ @exception.readagain_bytes.should == "a".dup.force_encoding('binary')
+ @exception.readagain_bytes.should == @errinfo[-1]
+
+ @exception2.readagain_bytes.size.should == 1
+ @exception2.readagain_bytes.should == "\xFF".dup.force_encoding('binary')
+ @exception2.readagain_bytes.should == @errinfo2[-1]
+ end
+
+ it "uses BINARY as the encoding" do
+ @exception.readagain_bytes.encoding.should == Encoding::BINARY
+
+ @exception2.readagain_bytes.encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb
new file mode 100644
index 0000000000..b00e1ad4e8
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_name_spec.rb
@@ -0,0 +1,29 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::UndefinedConversionError#source_encoding_name" do
+ before :each do
+ @exception, = EncodingSpecs::UndefinedConversionError.exception
+ @exception2, = EncodingSpecs::UndefinedConversionErrorIndirect.exception
+ end
+
+ it "returns a String" do
+ @exception.source_encoding_name.should.instance_of?(String)
+ end
+
+ it "is equal to the source encoding name of the object that raised it" do
+ @exception.source_encoding_name.should == "UTF-8"
+ end
+
+ # The source encoding specified in the Encoding::Converter constructor may
+ # differ from the source encoding returned here. What seems to happen is
+ # that when transcoding along a path with multiple pairs of encodings, the
+ # last one encountered when the error occurred is returned. So in this
+ # case, the conversion path is ISO-8859-1 -> UTF-8 -> EUC-JP. The
+ # conversion from ISO-8859-1 -> UTF-8 succeeded, but the conversion from
+ # UTF-8 to EUC-JP failed. IOW, it failed when the source encoding was
+ # UTF-8, so UTF-8 is regarded as the source encoding.
+ it "is equal to the source encoding at the stage of the conversion path where the error occurred" do
+ @exception2.source_encoding_name.should == 'UTF-8'
+ end
+end
diff --git a/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb
new file mode 100644
index 0000000000..32ad25dbb5
--- /dev/null
+++ b/spec/ruby/core/encoding/invalid_byte_sequence_error/source_encoding_spec.rb
@@ -0,0 +1,34 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::InvalidByteSequenceError#source_encoding" do
+ before :each do
+ @exception, = EncodingSpecs::InvalidByteSequenceError.exception
+ @exception2, = EncodingSpecs::InvalidByteSequenceErrorIndirect.exception
+ end
+
+ it "returns an Encoding object" do
+ @exception.source_encoding.should.instance_of?(Encoding)
+ @exception2.source_encoding.should.instance_of?(Encoding)
+ end
+
+ it "is equal to the source encoding of the object that raised it" do
+ @exception.source_encoding.should == Encoding::UTF_8
+ end
+
+ # The source encoding specified in the Encoding::Converter constructor may
+ # differ from the source encoding returned here. What seems to happen is
+ # that when transcoding along a path with multiple pairs of encodings, the
+ # last one encountered when the error occurred is returned. So in this
+ # case, the conversion path is EUC-JP -> UTF-8 -> ISO-8859-1. The
+ # conversions failed with the first pair of encodings (i.e. transcoding
+ # from EUC-JP to UTF-8, so UTF-8 is regarded as the source encoding; if
+ # the error had occurred when converting from UTF-8 to ISO-8859-1, UTF-8
+ # would have been the source encoding.
+
+ # FIXME: Derive example where the failure occurs at the UTF-8 ->
+ # ISO-8859-1 case so as to better illustrate the issue
+ it "is equal to the source encoding at the stage of the conversion path where the error occurred" do
+ @exception2.source_encoding.should == Encoding::EUC_JP
+ end
+end
diff --git a/spec/ruby/core/encoding/list_spec.rb b/spec/ruby/core/encoding/list_spec.rb
new file mode 100644
index 0000000000..9fe336c608
--- /dev/null
+++ b/spec/ruby/core/encoding/list_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.list" do
+ it "returns an Array" do
+ Encoding.list.should.instance_of?(Array)
+ end
+
+ it "returns an Array of Encoding objects" do
+ Encoding.list.each do |enc|
+ enc.should.instance_of?(Encoding)
+ end
+ end
+
+ it "returns each encoding only once" do
+ orig = Encoding.list.map { |e| e.name }
+ orig.should == orig.uniq
+ end
+
+ it "includes the default external encoding" do
+ Encoding.list.include?(Encoding.default_external).should == true
+ end
+
+ it "does not include any alias names" do
+ Encoding.aliases.keys.each do |enc_alias|
+ Encoding.list.include?(enc_alias).should == false
+ end
+ end
+
+ it "includes all aliased encodings" do
+ Encoding.aliases.values.each do |enc_alias|
+ Encoding.list.include?(Encoding.find(enc_alias)).should == true
+ end
+ end
+
+ it "includes dummy encodings" do
+ Encoding.list.select { |e| e.dummy? }.should_not == []
+ end
+
+ it 'includes UTF-8 encoding' do
+ Encoding.list.should.include?(Encoding::UTF_8)
+ end
+
+ it 'includes CESU-8 encoding' do
+ Encoding.list.should.include?(Encoding::CESU_8)
+ end
+
+ # TODO: Find example that illustrates this
+ it "updates the list when #find is used to load a new encoding"
+end
diff --git a/spec/ruby/core/encoding/locale_charmap_spec.rb b/spec/ruby/core/encoding/locale_charmap_spec.rb
new file mode 100644
index 0000000000..0d77bf227b
--- /dev/null
+++ b/spec/ruby/core/encoding/locale_charmap_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.locale_charmap" do
+ it "returns a String" do
+ Encoding.locale_charmap.should.instance_of?(String)
+ end
+
+ describe "when setting LC_ALL=C" do
+ before :each do
+ @old_lc_all = ENV['LC_ALL']
+ end
+
+ after :each do
+ ENV['LC_ALL'] = @old_lc_all
+ end
+
+ # FIXME: Get this working on Windows
+ platform_is :linux do
+ platform_is_not :android do
+ it "returns a value based on the LC_ALL environment variable" do
+ ENV['LC_ALL'] = 'C'
+ ruby_exe("print Encoding.locale_charmap").should == 'ANSI_X3.4-1968'
+ end
+ end
+ end
+
+ platform_is :freebsd, :openbsd, :darwin do
+ it "returns a value based on the LC_ALL environment variable" do
+ ENV['LC_ALL'] = 'C'
+ ruby_exe("print Encoding.locale_charmap").should == 'US-ASCII'
+ end
+ end
+
+ platform_is :netbsd do
+ it "returns a value based on the LC_ALL environment variable" do
+ ENV['LC_ALL'] = 'C'
+ ruby_exe("print Encoding.locale_charmap").should == '646'
+ end
+ end
+
+ platform_is :android do
+ it "always returns UTF-8" do
+ ENV['LC_ALL'] = 'C'
+ ruby_exe("print Encoding.locale_charmap").should == 'UTF-8'
+ end
+ end
+
+ platform_is :bsd, :darwin, :linux do
+ it "is unaffected by assigning to ENV['LC_ALL'] in the same process" do
+ old_charmap = Encoding.locale_charmap
+ ENV['LC_ALL'] = 'C'
+ Encoding.locale_charmap.should == old_charmap
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/name_list_spec.rb b/spec/ruby/core/encoding/name_list_spec.rb
new file mode 100644
index 0000000000..1ba8d383bc
--- /dev/null
+++ b/spec/ruby/core/encoding/name_list_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Encoding.name_list" do
+ it "returns an Array" do
+ Encoding.name_list.should.instance_of?(Array)
+ end
+
+ it "returns encoding names as Strings" do
+ Encoding.name_list.each {|e| e.should.instance_of?(String) }
+ end
+
+ it "includes all aliases" do
+ Encoding.aliases.keys.each do |enc_alias|
+ Encoding.name_list.include?(enc_alias).should == true
+ end
+ end
+
+ it "includes all non-dummy encodings" do
+ Encoding.list.each do |enc|
+ Encoding.name_list.include?(enc.name).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/name_spec.rb b/spec/ruby/core/encoding/name_spec.rb
new file mode 100644
index 0000000000..1d625c9379
--- /dev/null
+++ b/spec/ruby/core/encoding/name_spec.rb
@@ -0,0 +1,15 @@
+require_relative "../../spec_helper"
+
+describe "Encoding#name" do
+ it "returns a String" do
+ Encoding.list.each do |e|
+ e.name.should.instance_of?(String)
+ end
+ end
+
+ it "uniquely identifies an encoding" do
+ Encoding.list.each do |e|
+ e.should == Encoding.find(e.name)
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/names_spec.rb b/spec/ruby/core/encoding/names_spec.rb
new file mode 100644
index 0000000000..e6bcbf474a
--- /dev/null
+++ b/spec/ruby/core/encoding/names_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "Encoding#names" do
+ it "returns an Array" do
+ Encoding.name_list.each do |name|
+ e = Encoding.find(name) or next
+ e.names.should.instance_of?(Array)
+ end
+ end
+
+ it "returns names as Strings" do
+ Encoding.name_list.each do |name|
+ e = Encoding.find(name) or next
+ e.names.each do |this_name|
+ this_name.should.instance_of?(String)
+ end
+ end
+ end
+
+ it "returns #name as the first value" do
+ Encoding.name_list.each do |name|
+ e = Encoding.find(name) or next
+ e.names.first.should == e.name
+ end
+ end
+
+ it "includes any aliases the encoding has" do
+ Encoding.name_list.each do |name|
+ e = Encoding.find(name) or next
+ aliases = Encoding.aliases.select{|a,n| n == name}.keys
+ names = e.names
+ aliases.each {|a| names.include?(a).should == true}
+ end
+ end
+end
diff --git a/spec/ruby/core/encoding/replicate_spec.rb b/spec/ruby/core/encoding/replicate_spec.rb
new file mode 100644
index 0000000000..9fe0ba8747
--- /dev/null
+++ b/spec/ruby/core/encoding/replicate_spec.rb
@@ -0,0 +1,8 @@
+# encoding: binary
+require_relative '../../spec_helper'
+
+describe "Encoding#replicate" do
+ it "has been removed" do
+ Encoding::US_ASCII.should_not.respond_to?(:replicate, true)
+ end
+end
diff --git a/spec/ruby/core/encoding/to_s_spec.rb b/spec/ruby/core/encoding/to_s_spec.rb
new file mode 100644
index 0000000000..ee5e3b4abe
--- /dev/null
+++ b/spec/ruby/core/encoding/to_s_spec.rb
@@ -0,0 +1,7 @@
+require_relative "../../spec_helper"
+
+describe "Encoding#to_s" do
+ it "is an alias of Encoding#name" do
+ Encoding.instance_method(:to_s).should == Encoding.instance_method(:name)
+ end
+end
diff --git a/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb
new file mode 100644
index 0000000000..bc36695ca7
--- /dev/null
+++ b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_name_spec.rb
@@ -0,0 +1,16 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::UndefinedConversionError#destination_encoding_name" do
+ before :each do
+ @exception = EncodingSpecs::UndefinedConversionError.exception
+ end
+
+ it "returns a String" do
+ @exception.destination_encoding_name.should.instance_of?(String)
+ end
+
+ it "is equal to the destination encoding name of the object that raised it" do
+ @exception.destination_encoding_name.should == "US-ASCII"
+ end
+end
diff --git a/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb
new file mode 100644
index 0000000000..c0fcf8de58
--- /dev/null
+++ b/spec/ruby/core/encoding/undefined_conversion_error/destination_encoding_spec.rb
@@ -0,0 +1,16 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::UndefinedConversionError#destination_encoding" do
+ before :each do
+ @exception = EncodingSpecs::UndefinedConversionError.exception
+ end
+
+ it "returns an Encoding object" do
+ @exception.destination_encoding.should.instance_of?(Encoding)
+ end
+
+ it "is equal to the destination encoding of the object that raised it" do
+ @exception.destination_encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb
new file mode 100644
index 0000000000..333acf5ee6
--- /dev/null
+++ b/spec/ruby/core/encoding/undefined_conversion_error/error_char_spec.rb
@@ -0,0 +1,28 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::UndefinedConversionError#error_char" do
+ before :each do
+ @exception = EncodingSpecs::UndefinedConversionError.exception
+ @exception2 = EncodingSpecs::UndefinedConversionErrorIndirect.exception
+ end
+
+ it "returns a String" do
+ @exception.error_char.should.instance_of?(String)
+ @exception2.error_char.should.instance_of?(String)
+ end
+
+ it "returns the one-character String that caused the exception" do
+ @exception.error_char.size.should == 1
+ @exception.error_char.should == "\u{8765}"
+
+ @exception2.error_char.size.should == 1
+ @exception2.error_char.should == "\u{A0}"
+ end
+
+ it "uses the source encoding" do
+ @exception.error_char.encoding.should == @exception.source_encoding
+
+ @exception2.error_char.encoding.should == @exception2.source_encoding
+ end
+end
diff --git a/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb
new file mode 100644
index 0000000000..4932a25ed7
--- /dev/null
+++ b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_name_spec.rb
@@ -0,0 +1,29 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::UndefinedConversionError#source_encoding_name" do
+ before :each do
+ @exception = EncodingSpecs::UndefinedConversionError.exception
+ @exception2 = EncodingSpecs::UndefinedConversionErrorIndirect.exception
+ end
+
+ it "returns a String" do
+ @exception.source_encoding_name.should.instance_of?(String)
+ end
+
+ it "is equal to the source encoding name of the object that raised it" do
+ @exception.source_encoding_name.should == "UTF-8"
+ end
+
+ # The source encoding specified in the Encoding::Converter constructor may
+ # differ from the source encoding returned here. What seems to happen is
+ # that when transcoding along a path with multiple pairs of encodings, the
+ # last one encountered when the error occurred is returned. So in this
+ # case, the conversion path is ISO-8859-1 -> UTF-8 -> EUC-JP. The
+ # conversion from ISO-8859-1 -> UTF-8 succeeded, but the conversion from
+ # UTF-8 to EUC-JP failed. IOW, it failed when the source encoding was
+ # UTF-8, so UTF-8 is regarded as the source encoding.
+ it "is equal to the source encoding at the stage of the conversion path where the error occurred" do
+ @exception2.source_encoding_name.should == 'UTF-8'
+ end
+end
diff --git a/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb
new file mode 100644
index 0000000000..cf12020ad2
--- /dev/null
+++ b/spec/ruby/core/encoding/undefined_conversion_error/source_encoding_spec.rb
@@ -0,0 +1,30 @@
+require_relative "../../../spec_helper"
+require_relative '../fixtures/classes'
+
+describe "Encoding::UndefinedConversionError#source_encoding" do
+ before :each do
+ @exception = EncodingSpecs::UndefinedConversionError.exception
+ @exception2 = EncodingSpecs::UndefinedConversionErrorIndirect.exception
+ end
+
+ it "returns an Encoding object" do
+ @exception.source_encoding.should.instance_of?(Encoding)
+ @exception2.source_encoding.should.instance_of?(Encoding)
+ end
+
+ it "is equal to the source encoding of the object that raised it" do
+ @exception.source_encoding.should == Encoding::UTF_8
+ end
+
+ # The source encoding specified in the Encoding::Converter constructor may
+ # differ from the source encoding returned here. What seems to happen is
+ # that when transcoding along a path with multiple pairs of encodings, the
+ # last one encountered when the error occurred is returned. So in this
+ # case, the conversion path is ISO-8859-1 -> UTF-8 -> EUC-JP. The
+ # conversion from ISO-8859-1 -> UTF-8 succeeded, but the conversion from
+ # UTF-8 to EUC-JP failed. IOW, it failed when the source encoding was
+ # UTF-8, so UTF-8 is regarded as the source encoding.
+ it "is equal to the source encoding at the stage of the conversion path where the error occurred" do
+ @exception2.source_encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/enumerable/all_spec.rb b/spec/ruby/core/enumerable/all_spec.rb
new file mode 100644
index 0000000000..cbdd63f86a
--- /dev/null
+++ b/spec/ruby/core/enumerable/all_spec.rb
@@ -0,0 +1,187 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#all?" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new
+ @empty = EnumerableSpecs::Empty.new()
+ @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1)
+ @enum2 = EnumerableSpecs::Numerous.new(nil, false, true)
+ end
+
+ it "always returns true on empty enumeration" do
+ @empty.should.all?
+ @empty.all? { nil }.should == true
+
+ [].should.all?
+ [].all? { false }.should == true
+
+ {}.should.all?
+ {}.all? { nil }.should == true
+ end
+
+ it "raises an ArgumentError when more than 1 argument is provided" do
+ -> { @enum.all?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { [].all?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { {}.all?(1, 2, 3) }.should.raise(ArgumentError)
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.all?
+ }.should.raise(RuntimeError)
+
+ -> {
+ EnumerableSpecs::ThrowingEach.new.all? { false }
+ }.should.raise(RuntimeError)
+ end
+
+ describe "with no block" do
+ it "returns true if no elements are false or nil" do
+ @enum.should.all?
+ @enum1.should.all?
+ @enum2.should_not.all?
+
+ EnumerableSpecs::Numerous.new('a','b','c').should.all?
+ EnumerableSpecs::Numerous.new(0, "x", true).should.all?
+ end
+
+ it "returns false if there are false or nil elements" do
+ EnumerableSpecs::Numerous.new(false).should_not.all?
+ EnumerableSpecs::Numerous.new(false, false).should_not.all?
+
+ EnumerableSpecs::Numerous.new(nil).should_not.all?
+ EnumerableSpecs::Numerous.new(nil, nil).should_not.all?
+
+ EnumerableSpecs::Numerous.new(1, nil, 2).should_not.all?
+ EnumerableSpecs::Numerous.new(0, "x", false, true).should_not.all?
+ @enum2.should_not.all?
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMultiWithFalse.new
+ multi.all?.should == true
+ end
+ end
+
+ describe "with block" do
+ it "returns true if the block never returns false or nil" do
+ @enum.all? { true }.should == true
+ @enum1.all?{ |o| o < 5 }.should == true
+ @enum1.all?{ |o| 5 }.should == true
+ end
+
+ it "returns false if the block ever returns false or nil" do
+ @enum.all? { false }.should == false
+ @enum.all? { nil }.should == false
+ @enum1.all?{ |o| o > 2 }.should == false
+
+ EnumerableSpecs::Numerous.new.all? { |i| i > 5 }.should == false
+ EnumerableSpecs::Numerous.new.all? { |i| i == 3 ? nil : true }.should == false
+ end
+
+ it "stops iterating once the return value is determined" do
+ yielded = []
+ EnumerableSpecs::Numerous.new(:one, :two, :three).all? do |e|
+ yielded << e
+ false
+ end.should == false
+ yielded.should == [:one]
+
+ yielded = []
+ EnumerableSpecs::Numerous.new(true, true, false, true).all? do |e|
+ yielded << e
+ e
+ end.should == false
+ yielded.should == [true, true, false]
+
+ yielded = []
+ EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).all? do |e|
+ yielded << e
+ e
+ end.should == true
+ yielded.should == [1, 2, 3, 4, 5]
+ end
+
+ it "does not hide exceptions out of the block" do
+ -> {
+ @enum.all? { raise "from block" }
+ }.should.raise(RuntimeError)
+ end
+
+ it "gathers initial args as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.all? { |e| yielded << e }.should == true
+ yielded.should == [1, 3, 6]
+ end
+
+ it "yields multiple arguments when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.all? { |*args| yielded << args }.should == true
+ yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+ end
+
+ describe 'when given a pattern argument' do
+ it "calls `===` on the pattern the return value" do
+ pattern = EnumerableSpecs::Pattern.new { |x| x >= 0 }
+ @enum1.all?(pattern).should == false
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+ end
+
+ it "always returns true on empty enumeration" do
+ @empty.all?(Integer).should == true
+ [].all?(Integer).should == true
+ {}.all?(NilClass).should == true
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.all?(Integer)
+ }.should.raise(RuntimeError)
+ end
+
+ it "returns true if the pattern never returns false or nil" do
+ pattern = EnumerableSpecs::Pattern.new { |x| 42 }
+ @enum.all?(pattern).should == true
+
+ [1, 42, 3].all?(pattern).should == true
+
+ pattern = EnumerableSpecs::Pattern.new { |x| Array === x }
+ {a: 1, b: 2}.all?(pattern).should == true
+ end
+
+ it "returns false if the pattern ever returns false or nil" do
+ pattern = EnumerableSpecs::Pattern.new { |x| x >= 0 }
+ @enum1.all?(pattern).should == false
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+
+ [1, 2, 3, -1].all?(pattern).should == false
+
+ pattern = EnumerableSpecs::Pattern.new { |x| x[1] >= 0 }
+ {a: 1, b: -1}.all?(pattern).should == false
+ end
+
+ it "does not hide exceptions out of pattern#===" do
+ pattern = EnumerableSpecs::Pattern.new { raise "from pattern" }
+ -> {
+ @enum.all?(pattern)
+ }.should.raise(RuntimeError)
+ end
+
+ it "calls the pattern with gathered array when yielded with multiple arguments" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ pattern = EnumerableSpecs::Pattern.new { true }
+ multi.all?(pattern).should == true
+ pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]]
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).all?(String) { true }.should == false
+ }.should complain(/given block not used/)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/any_spec.rb b/spec/ruby/core/enumerable/any_spec.rb
new file mode 100644
index 0000000000..4405d4740a
--- /dev/null
+++ b/spec/ruby/core/enumerable/any_spec.rb
@@ -0,0 +1,200 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#any?" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new
+ @empty = EnumerableSpecs::Empty.new
+ @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1)
+ @enum2 = EnumerableSpecs::Numerous.new(nil, false, true)
+ end
+
+ it "always returns false on empty enumeration" do
+ @empty.should_not.any?
+ @empty.any? { nil }.should == false
+
+ [].should_not.any?
+ [].any? { false }.should == false
+
+ {}.should_not.any?
+ {}.any? { nil }.should == false
+ end
+
+ it "raises an ArgumentError when more than 1 argument is provided" do
+ -> { @enum.any?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { [].any?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { {}.any?(1, 2, 3) }.should.raise(ArgumentError)
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.any?
+ }.should.raise(RuntimeError)
+
+ -> {
+ EnumerableSpecs::ThrowingEach.new.any? { false }
+ }.should.raise(RuntimeError)
+ end
+
+ describe "with no block" do
+ it "returns true if any element is not false or nil" do
+ @enum.should.any?
+ @enum1.should.any?
+ @enum2.should.any?
+ EnumerableSpecs::Numerous.new(true).should.any?
+ EnumerableSpecs::Numerous.new('a','b','c').should.any?
+ EnumerableSpecs::Numerous.new('a','b','c', nil).should.any?
+ EnumerableSpecs::Numerous.new(1, nil, 2).should.any?
+ EnumerableSpecs::Numerous.new(1, false).should.any?
+ EnumerableSpecs::Numerous.new(false, nil, 1, false).should.any?
+ EnumerableSpecs::Numerous.new(false, 0, nil).should.any?
+ end
+
+ it "returns false if all elements are false or nil" do
+ EnumerableSpecs::Numerous.new(false).should_not.any?
+ EnumerableSpecs::Numerous.new(false, false).should_not.any?
+ EnumerableSpecs::Numerous.new(nil).should_not.any?
+ EnumerableSpecs::Numerous.new(nil, nil).should_not.any?
+ EnumerableSpecs::Numerous.new(nil, false, nil).should_not.any?
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMultiWithFalse.new
+ multi.any?.should == true
+ end
+ end
+
+ describe "with block" do
+ it "returns true if the block ever returns other than false or nil" do
+ @enum.any? { true }.should == true
+ @enum.any? { 0 }.should == true
+ @enum.any? { 1 }.should == true
+
+ @enum1.any? { Object.new }.should == true
+ @enum1.any?{ |o| o < 1 }.should == true
+ @enum1.any?{ |o| 5 }.should == true
+
+ @enum2.any? { |i| i == nil }.should == true
+ end
+
+ it "returns false if the block never returns other than false or nil" do
+ @enum.any? { false }.should == false
+ @enum.any? { nil }.should == false
+
+ @enum1.any?{ |o| o < -10 }.should == false
+ @enum1.any?{ |o| nil }.should == false
+
+ @enum2.any? { |i| i == :stuff }.should == false
+ end
+
+ it "stops iterating once the return value is determined" do
+ yielded = []
+ EnumerableSpecs::Numerous.new(:one, :two, :three).any? do |e|
+ yielded << e
+ false
+ end.should == false
+ yielded.should == [:one, :two, :three]
+
+ yielded = []
+ EnumerableSpecs::Numerous.new(true, true, false, true).any? do |e|
+ yielded << e
+ e
+ end.should == true
+ yielded.should == [true]
+
+ yielded = []
+ EnumerableSpecs::Numerous.new(false, nil, false, true, false).any? do |e|
+ yielded << e
+ e
+ end.should == true
+ yielded.should == [false, nil, false, true]
+
+ yielded = []
+ EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).any? do |e|
+ yielded << e
+ e
+ end.should == true
+ yielded.should == [1]
+ end
+
+ it "does not hide exceptions out of the block" do
+ -> {
+ @enum.any? { raise "from block" }
+ }.should.raise(RuntimeError)
+ end
+
+ it "gathers initial args as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.any? { |e| yielded << e; false }.should == false
+ yielded.should == [1, 3, 6]
+ end
+
+ it "yields multiple arguments when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.any? { |*args| yielded << args; false }.should == false
+ yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+ end
+
+ describe 'when given a pattern argument' do
+ it "calls `===` on the pattern the return value" do
+ pattern = EnumerableSpecs::Pattern.new { |x| x == 2 }
+ @enum1.any?(pattern).should == true
+ pattern.yielded.should == [[0], [1], [2]]
+ end
+
+ it "always returns false on empty enumeration" do
+ @empty.any?(Integer).should == false
+ [].any?(Integer).should == false
+ {}.any?(NilClass).should == false
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.any?(Integer)
+ }.should.raise(RuntimeError)
+ end
+
+ it "returns true if the pattern ever returns a truthy value" do
+ @enum2.any?(NilClass).should == true
+ pattern = EnumerableSpecs::Pattern.new { |x| 42 }
+ @enum.any?(pattern).should == true
+
+ [1, 42, 3].any?(pattern).should == true
+
+ pattern = EnumerableSpecs::Pattern.new { |x| x == [:b, 2] }
+ {a: 1, b: 2}.any?(pattern).should == true
+ end
+
+ it "returns false if the block never returns other than false or nil" do
+ pattern = EnumerableSpecs::Pattern.new { |x| nil }
+ @enum1.any?(pattern).should == false
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+
+ [1, 2, 3].any?(pattern).should == false
+ {a: 1}.any?(pattern).should == false
+ end
+
+ it "does not hide exceptions out of pattern#===" do
+ pattern = EnumerableSpecs::Pattern.new { raise "from pattern" }
+ -> {
+ @enum.any?(pattern)
+ }.should.raise(RuntimeError)
+ end
+
+ it "calls the pattern with gathered array when yielded with multiple arguments" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ pattern = EnumerableSpecs::Pattern.new { false }
+ multi.any?(pattern).should == false
+ pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]]
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).any?(String) { true }.should == false
+ }.should complain(/given block not used/)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/chain_spec.rb b/spec/ruby/core/enumerable/chain_spec.rb
new file mode 100644
index 0000000000..a0597e46a1
--- /dev/null
+++ b/spec/ruby/core/enumerable/chain_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#chain" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns a chain of self and provided enumerables" do
+ one = EnumerableSpecs::Numerous.new(1)
+ two = EnumerableSpecs::Numerous.new(2, 3)
+ three = EnumerableSpecs::Numerous.new(4, 5, 6)
+
+ chain = one.chain(two, three)
+
+ chain.each { |item| ScratchPad << item }
+ ScratchPad.recorded.should == [1, 2, 3, 4, 5, 6]
+ end
+
+ it "returns an Enumerator::Chain if given a block" do
+ EnumerableSpecs::Numerous.new.chain.should.instance_of?(Enumerator::Chain)
+ end
+end
diff --git a/spec/ruby/core/enumerable/chunk_spec.rb b/spec/ruby/core/enumerable/chunk_spec.rb
new file mode 100644
index 0000000000..7c9b31c991
--- /dev/null
+++ b/spec/ruby/core/enumerable/chunk_spec.rb
@@ -0,0 +1,77 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#chunk" do
+ before do
+ ScratchPad.record []
+ end
+
+ it "returns an Enumerator if called without a block" do
+ chunk = EnumerableSpecs::Numerous.new(1, 2, 3, 1, 2).chunk
+ chunk.should.instance_of?(Enumerator)
+ result = chunk.with_index {|elt, i| elt - i }.to_a
+ result.should == [[1, [1, 2, 3]], [-2, [1, 2]]]
+ end
+
+ it "returns an Enumerator if given a block" do
+ EnumerableSpecs::Numerous.new.chunk {}.should.instance_of?(Enumerator)
+ end
+
+ it "yields the current element and the current chunk to the block" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3)
+ e.chunk { |x| ScratchPad << x }.to_a
+ ScratchPad.recorded.should == [1, 2, 3]
+ end
+
+ it "returns elements of the Enumerable in an Array of Arrays, [v, ary], where 'ary' contains the consecutive elements for which the block returned the value 'v'" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 3, 2, 1)
+ result = e.chunk { |x| x < 3 && 1 || 0 }.to_a
+ result.should == [[1, [1, 2]], [0, [3]], [1, [2]], [0, [3]], [1, [2, 1]]]
+ end
+
+ it "returns a partitioned Array of values" do
+ e = EnumerableSpecs::Numerous.new(1,2,3)
+ e.chunk { |x| x > 2 }.map(&:last).should == [[1, 2], [3]]
+ end
+
+ it "returns elements for which the block returns :_alone in separate Arrays" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 1)
+ result = e.chunk { |x| x < 2 && :_alone }.to_a
+ result.should == [[:_alone, [1]], [false, [2, 3, 2]], [:_alone, [1]]]
+ end
+
+ it "yields Arrays as a single argument to a rest argument" do
+ e = EnumerableSpecs::Numerous.new([1, 2])
+ result = e.chunk { |*x| x.should == [[1,2]] }.to_a
+ end
+
+ it "does not return elements for which the block returns :_separator" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3, 3, 2, 1)
+ result = e.chunk { |x| x == 2 ? :_separator : 1 }.to_a
+ result.should == [[1, [1]], [1, [3, 3]], [1, [1]]]
+ end
+
+ it "does not return elements for which the block returns nil" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 1)
+ result = e.chunk { |x| x == 2 ? nil : 1 }.to_a
+ result.should == [[1, [1]], [1, [3]], [1, [1]]]
+ end
+
+ it "raises a RuntimeError if the block returns a Symbol starting with an underscore other than :_alone or :_separator" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3, 2, 1)
+ -> { e.chunk { |x| :_arbitrary }.to_a }.should.raise(RuntimeError)
+ end
+
+ it "does not accept arguments" do
+ e = EnumerableSpecs::Numerous.new(1, 2, 3)
+ -> {
+ e.chunk(1) {}
+ }.should.raise(ArgumentError)
+ end
+
+ it 'returned Enumerator size returns nil' do
+ e = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 2, 1)
+ enum = e.chunk { |x| true }
+ enum.size.should == nil
+ end
+end
diff --git a/spec/ruby/core/enumerable/chunk_while_spec.rb b/spec/ruby/core/enumerable/chunk_while_spec.rb
new file mode 100644
index 0000000000..286f717442
--- /dev/null
+++ b/spec/ruby/core/enumerable/chunk_while_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#chunk_while" do
+ before :each do
+ ary = [10, 9, 7, 6, 4, 3, 2, 1]
+ @enum = EnumerableSpecs::Numerous.new(*ary)
+ @result = @enum.chunk_while { |i, j| i - 1 == j }
+ @enum_length = ary.length
+ end
+
+ context "when given a block" do
+ it "returns an enumerator" do
+ @result.should.instance_of?(Enumerator)
+ end
+
+ it "splits chunks between adjacent elements i and j where the block returns false" do
+ @result.to_a.should == [[10, 9], [7, 6], [4, 3, 2, 1]]
+ end
+
+ it "calls the block for length of the receiver enumerable minus one times" do
+ times_called = 0
+ @enum.chunk_while do |i, j|
+ times_called += 1
+ i - 1 == j
+ end.to_a
+ times_called.should == (@enum_length - 1)
+ end
+ end
+
+ context "when not given a block" do
+ it "raises an ArgumentError" do
+ -> { @enum.chunk_while }.should.raise(ArgumentError)
+ end
+ end
+
+ context "on a single-element array" do
+ it "ignores the block and returns an enumerator that yields [element]" do
+ [1].chunk_while {|x| x.even?}.to_a.should == [[1]]
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/collect_concat_spec.rb b/spec/ruby/core/enumerable/collect_concat_spec.rb
new file mode 100644
index 0000000000..59317cfe34
--- /dev/null
+++ b/spec/ruby/core/enumerable/collect_concat_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/collect_concat'
+
+describe "Enumerable#collect_concat" do
+ it_behaves_like :enumerable_collect_concat, :collect_concat
+end
diff --git a/spec/ruby/core/enumerable/collect_spec.rb b/spec/ruby/core/enumerable/collect_spec.rb
new file mode 100644
index 0000000000..cfa2895cce
--- /dev/null
+++ b/spec/ruby/core/enumerable/collect_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/collect'
+
+describe "Enumerable#collect" do
+ it_behaves_like :enumerable_collect, :collect
+end
diff --git a/spec/ruby/core/enumerable/compact_spec.rb b/spec/ruby/core/enumerable/compact_spec.rb
new file mode 100644
index 0000000000..1895430c4e
--- /dev/null
+++ b/spec/ruby/core/enumerable/compact_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#compact" do
+ it 'returns array without nil elements' do
+ arr = EnumerableSpecs::Numerous.new(nil, 1, 2, nil, true)
+ arr.compact.should == [1, 2, true]
+ end
+end
diff --git a/spec/ruby/core/enumerable/count_spec.rb b/spec/ruby/core/enumerable/count_spec.rb
new file mode 100644
index 0000000000..50a1c8e1a4
--- /dev/null
+++ b/spec/ruby/core/enumerable/count_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#count" do
+ before :each do
+ @elements = [1, 2, 4, 2]
+ @numerous = EnumerableSpecs::Numerous.new(*@elements)
+ end
+
+ describe "when no argument or a block" do
+ it "returns size" do
+ @numerous.count.should == 4
+ end
+
+ describe "with a custom size method" do
+ before :each do
+ class << @numerous
+ def size
+ :any_object
+ end
+ end
+ end
+
+ it "ignores the custom size method" do
+ @numerous.count.should == 4
+ end
+ end
+ end
+
+ it "counts nils if given nil as an argument" do
+ EnumerableSpecs::Numerous.new(nil, nil, nil, false).count(nil).should == 3
+ end
+
+ it "accepts an argument for comparison using ==" do
+ @numerous.count(2).should == 2
+ end
+
+ it "uses a block for comparison" do
+ @numerous.count{|x| x%2==0 }.should == 3
+ end
+
+ it "ignores the block when given an argument" do
+ -> {
+ @numerous.count(4){|x| x%2==0 }.should == 1
+ }.should complain(/given block not used/)
+ end
+
+ describe "when each yields multiple values" do
+ it "gathers initial args as elements" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.count {|e| e == 1 }.should == 1
+ end
+
+ it "accepts an argument for comparison using ==" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.count([1, 2]).should == 1
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/cycle_spec.rb b/spec/ruby/core/enumerable/cycle_spec.rb
new file mode 100644
index 0000000000..1fb3cc3d41
--- /dev/null
+++ b/spec/ruby/core/enumerable/cycle_spec.rb
@@ -0,0 +1,104 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorized'
+
+describe "Enumerable#cycle" do
+ describe "passed no argument or nil" do
+ it "loops indefinitely" do
+ [[],[nil]].each do |args|
+ bomb = 10
+ EnumerableSpecs::Numerous.new.cycle(*args) do
+ bomb -= 1
+ break 42 if bomb <= 0
+ end.should == 42
+ bomb.should == 0
+ end
+ end
+
+ it "returns nil if there are no elements" do
+ out = EnumerableSpecs::Empty.new.cycle { break :nope }
+ out.should == nil
+ end
+
+ it "yields successive elements of the array repeatedly" do
+ b = []
+ EnumerableSpecs::Numerous.new(1,2,3).cycle do |elem|
+ b << elem
+ break if b.size == 7
+ end
+ b.should == [1,2,3,1,2,3,1]
+ end
+
+ it "calls each at most once" do
+ enum = EnumerableSpecs::EachCounter.new(1, 2)
+ enum.cycle.first(6).should == [1,2,1,2,1,2]
+ enum.times_called.should == 1
+ end
+
+ it "yields only when necessary" do
+ enum = EnumerableSpecs::EachCounter.new(10, 20, 30)
+ enum.cycle { |x| break if x == 20}
+ enum.times_yielded.should == 2
+ end
+ end
+
+ describe "passed a number n as an argument" do
+ it "returns nil and does nothing for non positive n" do
+ EnumerableSpecs::ThrowingEach.new.cycle(0) {}.should == nil
+ EnumerableSpecs::NoEach.new.cycle(-22) {}.should == nil
+ end
+
+ it "calls each at most once" do
+ enum = EnumerableSpecs::EachCounter.new(1, 2)
+ enum.cycle(3).to_a.should == [1,2,1,2,1,2]
+ enum.times_called.should == 1
+ end
+
+ it "yields only when necessary" do
+ enum = EnumerableSpecs::EachCounter.new(10, 20, 30)
+ enum.cycle(3) { |x| break if x == 20}
+ enum.times_yielded.should == 2
+ end
+
+ it "tries to convert n to an Integer using #to_int" do
+ enum = EnumerableSpecs::Numerous.new(3, 2, 1)
+ enum.cycle(2.3).to_a.should == [3, 2, 1, 3, 2, 1]
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ enum.cycle(obj).to_a.should == [3, 2, 1, 3, 2, 1]
+ end
+
+ it "raises a TypeError when the passed n cannot be coerced to Integer" do
+ enum = EnumerableSpecs::Numerous.new
+ ->{ enum.cycle("cat"){} }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if more arguments are passed" do
+ enum = EnumerableSpecs::Numerous.new
+ ->{ enum.cycle(1, 2) {} }.should.raise(ArgumentError)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.cycle(2).to_a.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9], [1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+ end
+
+ describe "Enumerable with size" do
+ before :all do
+ @object = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4)
+ @empty_object = EnumerableSpecs::EmptyWithSize.new
+ end
+ it_should_behave_like :enumeratorized_with_cycle_size
+ end
+
+ describe "Enumerable with no size" do
+ before :all do
+ @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4)
+ @method = :cycle
+ end
+ it_should_behave_like :enumeratorized_with_unknown_size
+ end
+
+end
diff --git a/spec/ruby/core/enumerable/detect_spec.rb b/spec/ruby/core/enumerable/detect_spec.rb
new file mode 100644
index 0000000000..6959aadc44
--- /dev/null
+++ b/spec/ruby/core/enumerable/detect_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/find'
+
+describe "Enumerable#detect" do
+ it_behaves_like :enumerable_find, :detect
+end
diff --git a/spec/ruby/core/enumerable/drop_spec.rb b/spec/ruby/core/enumerable/drop_spec.rb
new file mode 100644
index 0000000000..8d95f464b3
--- /dev/null
+++ b/spec/ruby/core/enumerable/drop_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#drop" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(3, 2, 1, :go)
+ end
+
+ it "requires exactly one argument" do
+ ->{ @enum.drop{} }.should.raise(ArgumentError)
+ ->{ @enum.drop(1, 2){} }.should.raise(ArgumentError)
+ end
+
+ describe "passed a number n as an argument" do
+ it "raises ArgumentError if n < 0" do
+ ->{ @enum.drop(-1) }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert n to an Integer using #to_int" do
+ @enum.drop(2.3).should == [1, :go]
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+ @enum.drop(obj).should == [1, :go]
+ end
+
+ it "returns [] for empty enumerables" do
+ EnumerableSpecs::Empty.new.drop(0).should == []
+ EnumerableSpecs::Empty.new.drop(2).should == []
+ end
+
+ it "returns [] if dropping all" do
+ @enum.drop(5).should == []
+ EnumerableSpecs::Numerous.new(3, 2, 1, :go).drop(4).should == []
+ end
+
+ it "raises a TypeError when the passed n cannot be coerced to Integer" do
+ ->{ @enum.drop("hat") }.should.raise(TypeError)
+ ->{ @enum.drop(nil) }.should.raise(TypeError)
+ end
+
+ end
+end
diff --git a/spec/ruby/core/enumerable/drop_while_spec.rb b/spec/ruby/core/enumerable/drop_while_spec.rb
new file mode 100644
index 0000000000..4b4fdf2d4f
--- /dev/null
+++ b/spec/ruby/core/enumerable/drop_while_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#drop_while" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(3, 2, 1, :go)
+ end
+
+ it "returns an Enumerator if no block given" do
+ @enum.drop_while.should.instance_of?(Enumerator)
+ end
+
+ it "returns no/all elements for {true/false} block" do
+ @enum.drop_while{true}.should == []
+ @enum.drop_while{false}.should == @enum.to_a
+ end
+
+ it "accepts returns other than true/false" do
+ @enum.drop_while{1}.should == []
+ @enum.drop_while{nil}.should == @enum.to_a
+ end
+
+ it "passes elements to the block until the first false" do
+ a = []
+ @enum.drop_while{|obj| (a << obj).size < 3}.should == [1, :go]
+ a.should == [3, 2, 1]
+ end
+
+ it "will only go through what's needed" do
+ enum = EnumerableSpecs::EachCounter.new(1,2,3,4)
+ enum.drop_while { |x|
+ break 42 if x == 3
+ true
+ }.should == 42
+ enum.times_yielded.should == 3
+ end
+
+ it "doesn't return self when it could" do
+ a = [1,2,3]
+ a.drop_while{false}.should_not.equal?(a)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.drop_while {|e| e != [6, 7, 8, 9] }.should == [[6, 7, 8, 9]]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_unknown_size, :drop_while
+end
diff --git a/spec/ruby/core/enumerable/each_cons_spec.rb b/spec/ruby/core/enumerable/each_cons_spec.rb
new file mode 100644
index 0000000000..c5e299fd00
--- /dev/null
+++ b/spec/ruby/core/enumerable/each_cons_spec.rb
@@ -0,0 +1,103 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorized'
+
+describe "Enumerable#each_cons" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(4,3,2,1)
+ @in_threes = [[4,3,2],[3,2,1]]
+ end
+
+ it "passes element groups to the block" do
+ acc = []
+ @enum.each_cons(3){|g| acc << g}
+ acc.should == @in_threes
+ end
+
+ it "raises an ArgumentError if there is not a single parameter > 0" do
+ ->{ @enum.each_cons(0){} }.should.raise(ArgumentError)
+ ->{ @enum.each_cons(-2){} }.should.raise(ArgumentError)
+ ->{ @enum.each_cons{} }.should.raise(ArgumentError)
+ ->{ @enum.each_cons(2,2){} }.should.raise(ArgumentError)
+ ->{ @enum.each_cons(0) }.should.raise(ArgumentError)
+ ->{ @enum.each_cons(-2) }.should.raise(ArgumentError)
+ ->{ @enum.each_cons }.should.raise(ArgumentError)
+ ->{ @enum.each_cons(2,2) }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert n to an Integer using #to_int" do
+ acc = []
+ @enum.each_cons(3.3){|g| acc << g}
+ acc.should == @in_threes
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(3)
+ @enum.each_cons(obj){|g| break g.length}.should == 3
+ end
+
+ it "works when n is >= full length" do
+ full = @enum.to_a
+ acc = []
+ @enum.each_cons(full.length){|g| acc << g}
+ acc.should == [full]
+ acc = []
+ @enum.each_cons(full.length+1){|g| acc << g}
+ acc.should == []
+ end
+
+ it "yields only as much as needed" do
+ cnt = EnumerableSpecs::EachCounter.new(1, 2, :stop, "I said stop!", :got_it)
+ cnt.each_cons(2) {|g| break 42 if g[-1] == :stop }.should == 42
+ cnt.times_yielded.should == 3
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.each_cons(2).to_a.should == [[[1, 2], [3, 4, 5]], [[3, 4, 5], [6, 7, 8, 9]]]
+ end
+
+ it "returns self when a block is given" do
+ @enum.each_cons(3){}.should == @enum
+ end
+
+ describe "when no block is given" do
+ it "returns an enumerator" do
+ e = @enum.each_cons(3)
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == @in_threes
+ end
+
+ describe "Enumerable with size" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns enum size - each_cons argument + 1" do
+ enum = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ enum.each_cons(10).size.should == 1
+ enum.each_cons(9).size.should == 2
+ enum.each_cons(3).size.should == 8
+ enum.each_cons(2).size.should == 9
+ enum.each_cons(1).size.should == 10
+ end
+
+ it "returns 0 when the argument is larger than self" do
+ enum = EnumerableSpecs::NumerousWithSize.new(1, 2, 3)
+ enum.each_cons(20).size.should == 0
+ end
+
+ it "returns 0 when the enum is empty" do
+ enum = EnumerableSpecs::EmptyWithSize.new
+ enum.each_cons(10).size.should == 0
+ end
+ end
+ end
+ end
+
+ describe "Enumerable with no size" do
+ before :all do
+ @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4)
+ @method = [:each_cons, 8]
+ end
+ it_should_behave_like :enumeratorized_with_unknown_size
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/each_entry_spec.rb b/spec/ruby/core/enumerable/each_entry_spec.rb
new file mode 100644
index 0000000000..9dc89ec28e
--- /dev/null
+++ b/spec/ruby/core/enumerable/each_entry_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#each_entry" do
+ before :each do
+ ScratchPad.record []
+ @enum = EnumerableSpecs::YieldsMixed.new
+ @entries = [1, [2], [3,4], [5,6,7], [8,9], nil, []]
+ end
+
+ it "yields multiple arguments as an array" do
+ acc = []
+ @enum.each_entry {|g| acc << g}.should.equal?(@enum)
+ acc.should == @entries
+ end
+
+ it "returns an enumerator if no block" do
+ e = @enum.each_entry
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == @entries
+ end
+
+ it "passes through the values yielded by #each_with_index" do
+ [:a, :b].each_with_index.each_entry { |x, i| ScratchPad << [x, i] }
+ ScratchPad.recorded.should == [[:a, 0], [:b, 1]]
+ end
+
+ it "raises an ArgumentError when extra arguments" do
+ -> { @enum.each_entry("one").to_a }.should.raise(ArgumentError)
+ -> { @enum.each_entry("one"){}.to_a }.should.raise(ArgumentError)
+ end
+
+ it "passes extra arguments to #each" do
+ enum = EnumerableSpecs::EachCounter.new(1, 2)
+ enum.each_entry(:foo, "bar").to_a.should == [1,2]
+ enum.arguments_passed.should == [:foo, "bar"]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :each_entry
+end
diff --git a/spec/ruby/core/enumerable/each_slice_spec.rb b/spec/ruby/core/enumerable/each_slice_spec.rb
new file mode 100644
index 0000000000..d05abad1e9
--- /dev/null
+++ b/spec/ruby/core/enumerable/each_slice_spec.rb
@@ -0,0 +1,105 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumeratorized'
+
+describe "Enumerable#each_slice" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(7,6,5,4,3,2,1)
+ @sliced = [[7,6,5],[4,3,2],[1]]
+ end
+
+ it "passes element groups to the block" do
+ acc = []
+ @enum.each_slice(3){|g| acc << g}
+ acc.should == @sliced
+ end
+
+ it "raises an ArgumentError if there is not a single parameter > 0" do
+ ->{ @enum.each_slice(0){} }.should.raise(ArgumentError)
+ ->{ @enum.each_slice(-2){} }.should.raise(ArgumentError)
+ ->{ @enum.each_slice{} }.should.raise(ArgumentError)
+ ->{ @enum.each_slice(2,2){} }.should.raise(ArgumentError)
+ ->{ @enum.each_slice(0) }.should.raise(ArgumentError)
+ ->{ @enum.each_slice(-2) }.should.raise(ArgumentError)
+ ->{ @enum.each_slice }.should.raise(ArgumentError)
+ ->{ @enum.each_slice(2,2) }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert n to an Integer using #to_int" do
+ acc = []
+ @enum.each_slice(3.3){|g| acc << g}
+ acc.should == @sliced
+
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(3)
+ @enum.each_slice(obj){|g| break g.length}.should == 3
+ end
+
+ it "works when n is >= full length" do
+ full = @enum.to_a
+ acc = []
+ @enum.each_slice(full.length){|g| acc << g}
+ acc.should == [full]
+ acc = []
+ @enum.each_slice(full.length+1){|g| acc << g}
+ acc.should == [full]
+ end
+
+ it "yields only as much as needed" do
+ cnt = EnumerableSpecs::EachCounter.new(1, 2, :stop, "I said stop!", :got_it)
+ cnt.each_slice(2) {|g| break 42 if g[0] == :stop }.should == 42
+ cnt.times_yielded.should == 4
+ end
+
+ it "returns an enumerator if no block" do
+ e = @enum.each_slice(3)
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == @sliced
+ end
+
+ it "returns self when a block is given" do
+ @enum.each_slice(3){}.should == @enum
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.each_slice(2).to_a.should == [[[1, 2], [3, 4, 5]], [[6, 7, 8, 9]]]
+ end
+
+ describe "when no block is given" do
+ it "returns an enumerator" do
+ e = @enum.each_slice(3)
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == @sliced
+ end
+
+ describe "Enumerable with size" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns the ceil of Enumerable size divided by the argument value" do
+ enum = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ enum.each_slice(10).size.should == 1
+ enum.each_slice(9).size.should == 2
+ enum.each_slice(3).size.should == 4
+ enum.each_slice(2).size.should == 5
+ enum.each_slice(1).size.should == 10
+ end
+
+ it "returns 0 when the Enumerable is empty" do
+ enum = EnumerableSpecs::EmptyWithSize.new
+ enum.each_slice(10).size.should == 0
+ end
+ end
+ end
+ end
+
+ describe "Enumerable with no size" do
+ before :all do
+ @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4)
+ @method = [:each_slice, 8]
+ end
+ it_should_behave_like :enumeratorized_with_unknown_size
+ end
+ end
+
+end
diff --git a/spec/ruby/core/enumerable/each_with_index_spec.rb b/spec/ruby/core/enumerable/each_with_index_spec.rb
new file mode 100644
index 0000000000..fcb2f82f84
--- /dev/null
+++ b/spec/ruby/core/enumerable/each_with_index_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#each_with_index" do
+
+ before :each do
+ @b = EnumerableSpecs::Numerous.new(2, 5, 3, 6, 1, 4)
+ end
+
+ it "passes each element and its index to block" do
+ @a = []
+ @b.each_with_index { |o, i| @a << [o, i] }
+ @a.should == [[2, 0], [5, 1], [3, 2], [6, 3], [1, 4], [4, 5]]
+ end
+
+ it "provides each element to the block" do
+ acc = []
+ obj = EnumerableSpecs::EachDefiner.new()
+ res = obj.each_with_index {|a,i| acc << [a,i]}
+ acc.should == []
+ obj.should == res
+ end
+
+ it "provides each element to the block and its index" do
+ acc = []
+ res = @b.each_with_index {|a,i| acc << [a,i]}
+ [[2, 0], [5, 1], [3, 2], [6, 3], [1, 4], [4, 5]].should == acc
+ res.should.eql?(@b)
+ end
+
+ it "binds splat arguments properly" do
+ acc = []
+ res = @b.each_with_index { |*b| c,d = b; acc << c; acc << d }
+ [2, 0, 5, 1, 3, 2, 6, 3, 1, 4, 4, 5].should == acc
+ res.should.eql?(@b)
+ end
+
+ it "returns an enumerator if no block" do
+ e = @b.each_with_index
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == [[2, 0], [5, 1], [3, 2], [6, 3], [1, 4], [4, 5]]
+ end
+
+ it "passes extra parameters to each" do
+ count = EnumerableSpecs::EachCounter.new(:apple)
+ e = count.each_with_index(:foo, :bar)
+ e.to_a.should == [[:apple, 0]]
+ count.arguments_passed.should == [:foo, :bar]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :each_with_index
+end
diff --git a/spec/ruby/core/enumerable/each_with_object_spec.rb b/spec/ruby/core/enumerable/each_with_object_spec.rb
new file mode 100644
index 0000000000..1760d3b267
--- /dev/null
+++ b/spec/ruby/core/enumerable/each_with_object_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#each_with_object" do
+ before :each do
+ @values = [2, 5, 3, 6, 1, 4]
+ @enum = EnumerableSpecs::Numerous.new(*@values)
+ @initial = "memo"
+ end
+
+ it "passes each element and its argument to the block" do
+ acc = []
+ @enum.each_with_object(@initial) do |elem, obj|
+ obj.should.equal?(@initial)
+ obj = 42
+ acc << elem
+ end.should.equal?(@initial)
+ acc.should == @values
+ end
+
+ it "returns an enumerator if no block" do
+ acc = []
+ e = @enum.each_with_object(@initial)
+ e.each do |elem, obj|
+ obj.should.equal?(@initial)
+ obj = 42
+ acc << elem
+ end.should.equal?(@initial)
+ acc.should == @values
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ array = []
+ multi.each_with_object(array) { |elem, obj| obj << elem }
+ array.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, [:each_with_object, []]
+end
diff --git a/spec/ruby/core/enumerable/entries_spec.rb b/spec/ruby/core/enumerable/entries_spec.rb
new file mode 100644
index 0000000000..2de4fc756a
--- /dev/null
+++ b/spec/ruby/core/enumerable/entries_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/entries'
+
+describe "Enumerable#entries" do
+ it_behaves_like :enumerable_entries, :entries
+end
diff --git a/spec/ruby/core/enumerable/filter_map_spec.rb b/spec/ruby/core/enumerable/filter_map_spec.rb
new file mode 100644
index 0000000000..1ed131a960
--- /dev/null
+++ b/spec/ruby/core/enumerable/filter_map_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Enumerable#filter_map' do
+ before :each do
+ @numerous = EnumerableSpecs::Numerous.new(*(1..8).to_a)
+ end
+
+ it 'returns an empty array if there are no elements' do
+ EnumerableSpecs::Empty.new.filter_map { true }.should == []
+ end
+
+ it 'returns an array with truthy results of passing each element to block' do
+ @numerous.filter_map { |i| i * 2 if i.even? }.should == [4, 8, 12, 16]
+ @numerous.filter_map { |i| i * 2 }.should == [2, 4, 6, 8, 10, 12, 14, 16]
+ @numerous.filter_map { 0 }.should == [0, 0, 0, 0, 0, 0, 0, 0]
+ @numerous.filter_map { false }.should == []
+ @numerous.filter_map { nil }.should == []
+ end
+
+ it 'returns an enumerator when no block given' do
+ @numerous.filter_map.should.instance_of?(Enumerator)
+ end
+end
diff --git a/spec/ruby/core/enumerable/filter_spec.rb b/spec/ruby/core/enumerable/filter_spec.rb
new file mode 100644
index 0000000000..1c3a7e9ff5
--- /dev/null
+++ b/spec/ruby/core/enumerable/filter_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/find_all'
+
+describe "Enumerable#filter" do
+ it_behaves_like :enumerable_find_all, :filter
+end
diff --git a/spec/ruby/core/enumerable/find_all_spec.rb b/spec/ruby/core/enumerable/find_all_spec.rb
new file mode 100644
index 0000000000..9cd635f247
--- /dev/null
+++ b/spec/ruby/core/enumerable/find_all_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/find_all'
+
+describe "Enumerable#find_all" do
+ it_behaves_like :enumerable_find_all, :find_all
+end
diff --git a/spec/ruby/core/enumerable/find_index_spec.rb b/spec/ruby/core/enumerable/find_index_spec.rb
new file mode 100644
index 0000000000..2e714367ba
--- /dev/null
+++ b/spec/ruby/core/enumerable/find_index_spec.rb
@@ -0,0 +1,89 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#find_index" do
+ before :each do
+ @elements = [2, 4, 6, 8, 10]
+ @numerous = EnumerableSpecs::Numerous.new(*@elements)
+ @yieldsmixed = EnumerableSpecs::YieldsMixed2.new
+ end
+
+ it "passes each entry in enum to block while block when block is false" do
+ visited_elements = []
+ @numerous.find_index do |element|
+ visited_elements << element
+ false
+ end
+ visited_elements.should == @elements
+ end
+
+ it "returns nil when the block is false" do
+ @numerous.find_index {|e| false }.should == nil
+ end
+
+ it "returns the first index for which the block is not false" do
+ @elements.each_with_index do |element, index|
+ @numerous.find_index {|e| e > element - 1 }.should == index
+ end
+ end
+
+ it "returns the first index found" do
+ repeated = [10, 11, 11, 13, 11, 13, 10, 10, 13, 11]
+ numerous_repeat = EnumerableSpecs::Numerous.new(*repeated)
+ repeated.each do |element|
+ numerous_repeat.find_index(element).should == element - 10
+ end
+ end
+
+ it "returns nil when the element not found" do
+ @numerous.find_index(-1).should == nil
+ end
+
+ it "ignores the block if an argument is given" do
+ -> {
+ @numerous.find_index(-1) {|e| true }.should == nil
+ }.should complain(/given block not used/)
+ end
+
+ it "returns an Enumerator if no block given" do
+ @numerous.find_index.should.instance_of?(Enumerator)
+ end
+
+ it "uses #== for testing equality" do
+ [2].to_enum.find_index(2.0).should == 0
+ [2.0].to_enum.find_index(2).should == 0
+ end
+
+ describe "without block" do
+ it "gathers whole arrays as elements when each yields multiple" do
+ @yieldsmixed.find_index([0, 1, 2]).should == 3
+ end
+ end
+
+ describe "with block" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ describe "given a single yield parameter" do
+ it "passes first element to the parameter" do
+ @yieldsmixed.find_index {|a| ScratchPad << a; false }
+ ScratchPad.recorded.should == EnumerableSpecs::YieldsMixed2.first_yields
+ end
+ end
+
+ describe "given a greedy yield parameter" do
+ it "passes a gathered array to the parameter" do
+ @yieldsmixed.find_index {|*args| ScratchPad << args; false }
+ ScratchPad.recorded.should == EnumerableSpecs::YieldsMixed2.greedy_yields
+ end
+ end
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_unknown_size, :find_index
+end
diff --git a/spec/ruby/core/enumerable/find_spec.rb b/spec/ruby/core/enumerable/find_spec.rb
new file mode 100644
index 0000000000..5ddebc05f8
--- /dev/null
+++ b/spec/ruby/core/enumerable/find_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/find'
+
+describe "Enumerable#find" do
+ it_behaves_like :enumerable_find, :find
+end
diff --git a/spec/ruby/core/enumerable/first_spec.rb b/spec/ruby/core/enumerable/first_spec.rb
new file mode 100644
index 0000000000..592dff1ebc
--- /dev/null
+++ b/spec/ruby/core/enumerable/first_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/take'
+
+describe "Enumerable#first" do
+ it "returns the first element" do
+ EnumerableSpecs::Numerous.new.first.should == 2
+ EnumerableSpecs::Empty.new.first.should == nil
+ end
+
+ it "returns nil if self is empty" do
+ EnumerableSpecs::Empty.new.first.should == nil
+ end
+
+ it 'returns a gathered array from yield parameters' do
+ EnumerableSpecs::YieldsMulti.new.to_enum.first.should == [1, 2]
+ EnumerableSpecs::YieldsMixed2.new.to_enum.first.should == nil
+ end
+
+ it "raises a RangeError when passed a Bignum" do
+ enum = EnumerableSpecs::Empty.new
+ -> { enum.first(bignum_value) }.should.raise(RangeError)
+ end
+
+ describe "when passed an argument" do
+ it_behaves_like :enumerable_take, :first
+ end
+end
diff --git a/spec/ruby/core/enumerable/fixtures/classes.rb b/spec/ruby/core/enumerable/fixtures/classes.rb
new file mode 100644
index 0000000000..b5feafcfb7
--- /dev/null
+++ b/spec/ruby/core/enumerable/fixtures/classes.rb
@@ -0,0 +1,350 @@
+module EnumerableSpecs
+
+ class Numerous
+ include Enumerable
+ def initialize(*list)
+ @list = list.empty? ? [2, 5, 3, 6, 1, 4] : list
+ end
+
+ def each
+ @list.each { |i| yield i }
+ end
+ end
+
+ class NumerousWithSize < Numerous
+ def size
+ @list.size
+ end
+ end
+
+ class EachCounter < Numerous
+ attr_reader :times_called, :times_yielded, :arguments_passed
+ def initialize(*list)
+ super(*list)
+ @times_yielded = @times_called = 0
+ end
+
+ def each(*arg)
+ @times_called += 1
+ @times_yielded = 0
+ @arguments_passed = arg
+ @list.each do |i|
+ @times_yielded +=1
+ yield i
+ end
+ end
+ end
+
+ class Empty
+ include Enumerable
+ def each
+ self
+ end
+ end
+
+ class EmptyWithSize
+ include Enumerable
+ def each
+ self
+ end
+ def size
+ 0
+ end
+ end
+
+ class ThrowingEach
+ include Enumerable
+ def each
+ raise "from each"
+ end
+ end
+
+ class NoEach
+ include Enumerable
+ end
+
+ # (Legacy form rubycon)
+ class EachDefiner
+
+ include Enumerable
+
+ attr_reader :arr
+
+ def initialize(*arr)
+ @arr = arr
+ end
+
+ def each
+ i = 0
+ loop do
+ break if i == @arr.size
+ yield @arr[i]
+ i += 1
+ end
+ end
+
+ end
+
+ class SortByDummy
+ def initialize(s)
+ @s = s
+ end
+
+ def s
+ @s
+ end
+ end
+
+ class ComparesByVowelCount
+
+ attr_accessor :value, :vowels
+
+ def self.wrap(*args)
+ args.map {|element| ComparesByVowelCount.new(element)}
+ end
+
+ def initialize(string)
+ self.value = string
+ self.vowels = string.gsub(/[^aeiou]/, '').size
+ end
+
+ def <=>(other)
+ self.vowels <=> other.vowels
+ end
+
+ end
+
+ class InvalidComparable
+ def <=>(other)
+ "Not Valid"
+ end
+ end
+
+ class ArrayConvertible
+ attr_accessor :called
+ def initialize(*values)
+ @values = values
+ end
+
+ def to_a
+ self.called = :to_a
+ @values
+ end
+
+ def to_ary
+ self.called = :to_ary
+ @values
+ end
+ end
+
+ class EnumConvertible
+ attr_accessor :called
+ attr_accessor :sym
+ def initialize(delegate)
+ @delegate = delegate
+ end
+
+ def to_enum(sym)
+ self.called = :to_enum
+ self.sym = sym
+ @delegate.to_enum(sym)
+ end
+
+ def respond_to_missing?(*args)
+ @delegate.respond_to?(*args)
+ end
+ end
+
+ class Equals
+ def initialize(obj)
+ @obj = obj
+ end
+ def ==(other)
+ @obj == other
+ end
+ end
+
+ class YieldsMulti
+ include Enumerable
+ def each
+ yield 1,2
+ yield 3,4,5
+ yield 6,7,8,9
+ end
+ end
+
+ class YieldsMultiWithFalse
+ include Enumerable
+ def each
+ yield false,2
+ yield false,4,5
+ yield false,7,8,9
+ end
+ end
+
+ class YieldsMultiWithSingleTrue
+ include Enumerable
+ def each
+ yield false,2
+ yield true,4,5
+ yield false,7,8,9
+ end
+ end
+
+ class YieldsMixed
+ include Enumerable
+ def each
+ yield 1
+ yield [2]
+ yield 3,4
+ yield 5,6,7
+ yield [8,9]
+ yield nil
+ yield []
+ end
+ end
+
+ class YieldsMixed2
+ include Enumerable
+
+ def self.first_yields
+ [nil, 0, 0, 0, 0, nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]]
+ end
+
+ def self.gathered_yields
+ [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]]
+ end
+
+ def self.gathered_yields_with_args(arg, *args)
+ [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, arg, args, [], [0], [0, 1], [0, 1, 2]]
+ end
+
+ def self.greedy_yields
+ [[], [0], [0, 1], [0, 1, 2], [0, 1, 2], [nil], [:default_arg], [[]], [[]], [[0]], [[0, 1]], [[0, 1, 2]]]
+ end
+
+ def each(arg=:default_arg, *args)
+ yield
+ yield 0
+ yield 0, 1
+ yield 0, 1, 2
+ yield(*[0, 1, 2])
+ yield nil
+ yield arg
+ yield args
+ yield []
+ yield [0]
+ yield [0, 1]
+ yield [0, 1, 2]
+ end
+ end
+
+ class ReverseComparable
+ include Comparable
+ def initialize(num)
+ @num = num
+ end
+
+ attr_accessor :num
+
+ # Reverse comparison
+ def <=>(other)
+ other.num <=> @num
+ end
+ end
+
+ class ComparableWithInteger
+ include Comparable
+ def initialize(num)
+ @num = num
+ end
+
+ def <=>(fixnum)
+ @num <=> fixnum
+ end
+ end
+
+ class Uncomparable
+ def <=>(obj)
+ nil
+ end
+ end
+
+ class Undupable
+ attr_reader :initialize_called, :initialize_dup_called
+ def dup
+ raise "Can't, sorry"
+ end
+
+ def clone
+ raise "Can't, either, sorry"
+ end
+
+ def initialize
+ @initialize_dup = true
+ end
+
+ def initialize_dup(arg)
+ @initialize_dup_called = true
+ end
+ end
+
+ class Freezy
+ include Enumerable
+
+ def each
+ yield 1
+ yield 2
+ end
+
+ def to_a
+ super.freeze
+ end
+ end
+
+ class MapReturnsEnumerable
+ include Enumerable
+
+ class EnumerableMapping
+ include Enumerable
+
+ def initialize(items, block)
+ @items = items
+ @block = block
+ end
+
+ def each
+ @items.each do |i|
+ yield @block.call(i)
+ end
+ end
+ end
+
+ def each
+ yield 1
+ yield 2
+ yield 3
+ end
+
+ def map(&block)
+ EnumerableMapping.new(self, block)
+ end
+ end
+
+ class Pattern
+ attr_reader :yielded
+
+ def initialize(&block)
+ @block = block
+ @yielded = []
+ end
+
+ def ===(*args)
+ @yielded << args
+ @block.call(*args)
+ end
+ end
+
+ class SetSubclass < Set
+ end
+end # EnumerableSpecs utility classes
diff --git a/spec/ruby/core/enumerable/flat_map_spec.rb b/spec/ruby/core/enumerable/flat_map_spec.rb
new file mode 100644
index 0000000000..bd07eab6c5
--- /dev/null
+++ b/spec/ruby/core/enumerable/flat_map_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/collect_concat'
+
+describe "Enumerable#flat_map" do
+ it_behaves_like :enumerable_collect_concat, :flat_map
+end
diff --git a/spec/ruby/core/enumerable/grep_spec.rb b/spec/ruby/core/enumerable/grep_spec.rb
new file mode 100644
index 0000000000..965e183766
--- /dev/null
+++ b/spec/ruby/core/enumerable/grep_spec.rb
@@ -0,0 +1,87 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#grep" do
+ before :each do
+ @a = EnumerableSpecs::EachDefiner.new( 2, 4, 6, 8, 10)
+ end
+
+ it "grep without a block should return an array of all elements === pattern" do
+ class EnumerableSpecGrep; def ===(obj); obj == '2'; end; end
+
+ EnumerableSpecs::Numerous.new('2', 'a', 'nil', '3', false).grep(EnumerableSpecGrep.new).should == ['2']
+ end
+
+ it "grep with a block should return an array of elements === pattern passed through block" do
+ class EnumerableSpecGrep2; def ===(obj); /^ca/ =~ obj; end; end
+
+ EnumerableSpecs::Numerous.new("cat", "coat", "car", "cadr", "cost").grep(EnumerableSpecGrep2.new) { |i| i.upcase }.should == ["CAT", "CAR", "CADR"]
+ end
+
+ it "grep the enumerable (rubycon legacy)" do
+ EnumerableSpecs::EachDefiner.new().grep(1).should == []
+ @a.grep(3..7).should == [4,6]
+ @a.grep(3..7) {|a| a+1}.should == [5,7]
+ end
+
+ it "can use $~ in the block when used with a Regexp" do
+ ary = ["aba", "aba"]
+ ary.grep(/a(b)a/) { $1 }.should == ["b", "b"]
+ end
+
+ it "sets $~ in the block" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].grep(/b/) { |e|
+ e.should == "abc"
+ $&.should == "b"
+ }
+
+ # Set by the failed match of "def"
+ $~.should == nil
+ end
+
+ it "does not set $~ when given no block" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].grep(/b/).should == ["abc"]
+ $&.should == "z"
+ end
+
+ it "does not modify Regexp.last_match without block" do
+ "z" =~ /z/ # Reset last match
+ ["abc", "def"].grep(/b/).should == ["abc"]
+ Regexp.last_match[0].should == "z"
+ end
+
+ it "correctly handles non-string elements" do
+ 'set last match' =~ /set last (.*)/
+ [:a, 'b', 'z', :c, 42, nil].grep(/[a-d]/).should == [:a, 'b', :c]
+ $1.should == 'match'
+
+ o = Object.new
+ def o.to_str
+ 'hello'
+ end
+ [o].grep(/ll/).first.should.equal?(o)
+ end
+
+ describe "with a block" do
+ before :each do
+ @numerous = EnumerableSpecs::Numerous.new(*(0..9).to_a)
+ def (@odd_matcher = BasicObject.new).===(obj)
+ obj.odd?
+ end
+ end
+
+ it "returns an Array of matched elements that mapped by the block" do
+ @numerous.grep(@odd_matcher) { |n| n * 2 }.should == [2, 6, 10, 14, 18]
+ end
+
+ it "calls the block with gathered array when yielded with multiple arguments" do
+ EnumerableSpecs::YieldsMixed2.new.grep(Object){ |e| e }.should == EnumerableSpecs::YieldsMixed2.gathered_yields
+ end
+
+ it "raises an ArgumentError when not given a pattern" do
+ -> { @numerous.grep { |e| e } }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/grep_v_spec.rb b/spec/ruby/core/enumerable/grep_v_spec.rb
new file mode 100644
index 0000000000..ee99a77ac1
--- /dev/null
+++ b/spec/ruby/core/enumerable/grep_v_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#grep_v" do
+ before :each do
+ @numerous = EnumerableSpecs::Numerous.new(*(0..9).to_a)
+ def (@odd_matcher = BasicObject.new).===(obj)
+ obj.odd?
+ end
+ end
+
+ it "sets $~ in the block" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].grep_v(/e/) { |e|
+ e.should == "abc"
+ $~.should == nil
+ }
+
+ # Set by the match of "def"
+ $&.should == "e"
+ end
+
+ it "does not set $~ when given no block" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].grep_v(/e/).should == ["abc"]
+ $&.should == "z"
+ end
+
+ it "does not modify Regexp.last_match without block" do
+ "z" =~ /z/ # Reset last match
+ ["abc", "def"].grep_v(/e/).should == ["abc"]
+ Regexp.last_match[0].should == "z"
+ end
+
+ it "correctly handles non-string elements" do
+ 'set last match' =~ /set last (.*)/
+ [:a, 'b', 'z', :c, 42, nil].grep_v(/[a-d]/).should == ['z', 42, nil]
+ $1.should == 'match'
+
+ o = Object.new
+ def o.to_str
+ 'hello'
+ end
+ [o].grep_v(/mm/).first.should.equal?(o)
+ end
+
+ describe "without block" do
+ it "returns an Array of matched elements" do
+ @numerous.grep_v(@odd_matcher).should == [0, 2, 4, 6, 8]
+ end
+
+ it "compares pattern with gathered array when yielded with multiple arguments" do
+ (unmatcher = Object.new).stub!(:===).and_return(false)
+ EnumerableSpecs::YieldsMixed2.new.grep_v(unmatcher).should == EnumerableSpecs::YieldsMixed2.gathered_yields
+ end
+
+ it "raises an ArgumentError when not given a pattern" do
+ -> { @numerous.grep_v }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "with block" do
+ it "returns an Array of matched elements that mapped by the block" do
+ @numerous.grep_v(@odd_matcher) { |n| n * 2 }.should == [0, 4, 8, 12, 16]
+ end
+
+ it "calls the block with gathered array when yielded with multiple arguments" do
+ (unmatcher = Object.new).stub!(:===).and_return(false)
+ EnumerableSpecs::YieldsMixed2.new.grep_v(unmatcher){ |e| e }.should == EnumerableSpecs::YieldsMixed2.gathered_yields
+ end
+
+ it "raises an ArgumentError when not given a pattern" do
+ -> { @numerous.grep_v { |e| e } }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/group_by_spec.rb b/spec/ruby/core/enumerable/group_by_spec.rb
new file mode 100644
index 0000000000..904e5d6c68
--- /dev/null
+++ b/spec/ruby/core/enumerable/group_by_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#group_by" do
+ it "returns a hash with values grouped according to the block" do
+ e = EnumerableSpecs::Numerous.new("foo", "bar", "baz")
+ h = e.group_by { |word| word[0..0].to_sym }
+ h.should == { f: ["foo"], b: ["bar", "baz"]}
+ end
+
+ it "returns an empty hash for empty enumerables" do
+ EnumerableSpecs::Empty.new.group_by { |x| x}.should == {}
+ end
+
+ it "returns a hash without default_proc" do
+ e = EnumerableSpecs::Numerous.new("foo", "bar", "baz")
+ h = e.group_by { |word| word[0..0].to_sym }
+ h[:some].should == nil
+ h.default_proc.should == nil
+ h.default.should == nil
+ end
+
+ it "returns an Enumerator if called without a block" do
+ EnumerableSpecs::Numerous.new.group_by.should.instance_of?(Enumerator)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ e = EnumerableSpecs::YieldsMulti.new
+ h = e.group_by { |i| i }
+ h.should == { [1, 2] => [[1, 2]],
+ [6, 7, 8, 9] => [[6, 7, 8, 9]],
+ [3, 4, 5] => [[3, 4, 5]] }
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :group_by
+end
diff --git a/spec/ruby/core/enumerable/include_spec.rb b/spec/ruby/core/enumerable/include_spec.rb
new file mode 100644
index 0000000000..dab1b04451
--- /dev/null
+++ b/spec/ruby/core/enumerable/include_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/include'
+
+describe "Enumerable#include?" do
+ it_behaves_like :enumerable_include, :include?
+end
diff --git a/spec/ruby/core/enumerable/inject_spec.rb b/spec/ruby/core/enumerable/inject_spec.rb
new file mode 100644
index 0000000000..e1fe216144
--- /dev/null
+++ b/spec/ruby/core/enumerable/inject_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/inject'
+
+describe "Enumerable#inject" do
+ it_behaves_like :enumerable_inject, :inject
+end
diff --git a/spec/ruby/core/enumerable/lazy_spec.rb b/spec/ruby/core/enumerable/lazy_spec.rb
new file mode 100644
index 0000000000..935e574067
--- /dev/null
+++ b/spec/ruby/core/enumerable/lazy_spec.rb
@@ -0,0 +1,10 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#lazy" do
+ it "returns an instance of Enumerator::Lazy" do
+ EnumerableSpecs::Numerous.new.lazy.should.instance_of?(Enumerator::Lazy)
+ end
+end
diff --git a/spec/ruby/core/enumerable/map_spec.rb b/spec/ruby/core/enumerable/map_spec.rb
new file mode 100644
index 0000000000..98a70781cf
--- /dev/null
+++ b/spec/ruby/core/enumerable/map_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/collect'
+
+describe "Enumerable#map" do
+ it_behaves_like :enumerable_collect, :map
+end
diff --git a/spec/ruby/core/enumerable/max_by_spec.rb b/spec/ruby/core/enumerable/max_by_spec.rb
new file mode 100644
index 0000000000..f67b5d15ea
--- /dev/null
+++ b/spec/ruby/core/enumerable/max_by_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#max_by" do
+ it "returns an enumerator if no block" do
+ EnumerableSpecs::Numerous.new(42).max_by.should.instance_of?(Enumerator)
+ end
+
+ it "returns nil if #each yields no objects" do
+ EnumerableSpecs::Empty.new.max_by {|o| o.nonesuch }.should == nil
+ end
+
+ it "returns the object for whom the value returned by block is the largest" do
+ EnumerableSpecs::Numerous.new(*%w[1 2 3]).max_by {|obj| obj.to_i }.should == '3'
+ EnumerableSpecs::Numerous.new(*%w[three five]).max_by {|obj| obj.length }.should == 'three'
+ end
+
+ it "returns the object that appears first in #each in case of a tie" do
+ a, b, c = '1', '2', '2'
+ EnumerableSpecs::Numerous.new(a, b, c).max_by {|obj| obj.to_i }.should.equal?(b)
+ end
+
+ it "uses max.<=>(current) to determine order" do
+ a, b, c = (1..3).map{|n| EnumerableSpecs::ReverseComparable.new(n)}
+
+ # Just using self here to avoid additional complexity
+ EnumerableSpecs::Numerous.new(a, b, c).max_by {|obj| obj }.should == a
+ end
+
+ it "is able to return the maximum for enums that contain nils" do
+ enum = EnumerableSpecs::Numerous.new(nil, nil, true)
+ enum.max_by {|o| o.nil? ? 0 : 1 }.should == true
+ enum.max_by {|o| o.nil? ? 1 : 0 }.should == nil
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.max_by {|e| e.size}.should == [6, 7, 8, 9]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :max_by
+
+ context "when called with an argument n" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(101, 55, 1, 20, 33, 500, 60)
+ end
+
+ context "without a block" do
+ it "returns an enumerator" do
+ @enum.max_by(2).should.instance_of?(Enumerator)
+ end
+ end
+
+ context "with a block" do
+ it "returns an array containing the maximum n elements based on the block's value" do
+ result = @enum.max_by(3) { |i| i.to_s }
+ result.should == [60, 55, 500]
+ end
+
+ context "on a enumerable of length x where x < n" do
+ it "returns an array containing the maximum n elements of length n" do
+ result = @enum.max_by(500) { |i| i.to_s }
+ result.length.should == 7
+ end
+ end
+
+ context "when n is negative" do
+ it "raises an ArgumentError" do
+ -> { @enum.max_by(-1) { |i| i.to_s } }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ context "when n is nil" do
+ it "returns the maximum element" do
+ @enum.max_by(nil) { |i| i.to_s }.should == 60
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/max_spec.rb b/spec/ruby/core/enumerable/max_spec.rb
new file mode 100644
index 0000000000..d92700258b
--- /dev/null
+++ b/spec/ruby/core/enumerable/max_spec.rb
@@ -0,0 +1,119 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#max" do
+ before :each do
+ @e_strs = EnumerableSpecs::EachDefiner.new("333", "22", "666666", "1", "55555", "1010101010")
+ @e_ints = EnumerableSpecs::EachDefiner.new( 333, 22, 666666, 55555, 1010101010)
+ end
+
+ it "returns the maximum element" do
+ EnumerableSpecs::Numerous.new.max.should == 6
+ end
+
+ it "returns the maximum element (basics cases)" do
+ EnumerableSpecs::EachDefiner.new(55).max.should == 55
+
+ EnumerableSpecs::EachDefiner.new(11,99).max.should == 99
+ EnumerableSpecs::EachDefiner.new(99,11).max.should == 99
+ EnumerableSpecs::EachDefiner.new(2, 33, 4, 11).max.should == 33
+
+ EnumerableSpecs::EachDefiner.new(1,2,3,4,5).max.should == 5
+ EnumerableSpecs::EachDefiner.new(5,4,3,2,1).max.should == 5
+ EnumerableSpecs::EachDefiner.new(1,4,3,5,2).max.should == 5
+ EnumerableSpecs::EachDefiner.new(5,5,5,5,5).max.should == 5
+
+ EnumerableSpecs::EachDefiner.new("aa","tt").max.should == "tt"
+ EnumerableSpecs::EachDefiner.new("tt","aa").max.should == "tt"
+ EnumerableSpecs::EachDefiner.new("2","33","4","11").max.should == "4"
+
+ @e_strs.max.should == "666666"
+ @e_ints.max.should == 1010101010
+ end
+
+ it "returns nil for an empty Enumerable" do
+ EnumerableSpecs::EachDefiner.new.max.should == nil
+ end
+
+ it "raises a NoMethodError for elements without #<=>" do
+ -> do
+ EnumerableSpecs::EachDefiner.new(BasicObject.new, BasicObject.new).max
+ end.should.raise(NoMethodError)
+ end
+
+ it "raises an ArgumentError for incomparable elements" do
+ -> do
+ EnumerableSpecs::EachDefiner.new(11,"22").max
+ end.should.raise(ArgumentError)
+ -> do
+ EnumerableSpecs::EachDefiner.new(11,12,22,33).max{|a, b| nil}
+ end.should.raise(ArgumentError)
+ end
+
+ context "when passed a block" do
+ it "returns the maximum element" do
+ EnumerableSpecs::EachDefiner.new("2","33","4","11").max {|a,b| a <=> b }.should == "4"
+ EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).max {|a,b| a <=> b }.should == 33
+
+ EnumerableSpecs::EachDefiner.new("2","33","4","11").max {|a,b| b <=> a }.should == "11"
+ EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).max {|a,b| b <=> a }.should == 2
+
+ @e_strs.max {|a,b| a.length <=> b.length }.should == "1010101010"
+
+ @e_strs.max {|a,b| a <=> b }.should == "666666"
+ @e_strs.max {|a,b| a.to_i <=> b.to_i }.should == "1010101010"
+
+ @e_ints.max {|a,b| a <=> b }.should == 1010101010
+ @e_ints.max {|a,b| a.to_s <=> b.to_s }.should == 666666
+ end
+ end
+
+ it "returns the maximum for enumerables that contain nils" do
+ arr = EnumerableSpecs::Numerous.new(nil, nil, true)
+ arr.max { |a, b|
+ x = a.nil? ? 1 : a ? 0 : -1
+ y = b.nil? ? 1 : b ? 0 : -1
+ x <=> y
+ }.should == nil
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.max.should == [6, 7, 8, 9]
+ end
+
+ context "when called with an argument n" do
+ context "without a block" do
+ it "returns an array containing the maximum n elements" do
+ result = @e_ints.max(2)
+ result.should == [1010101010, 666666]
+ end
+ end
+
+ context "with a block" do
+ it "returns an array containing the maximum n elements" do
+ result = @e_ints.max(2) { |a, b| a * 2 <=> b * 2 }
+ result.should == [1010101010, 666666]
+ end
+ end
+
+ context "on a enumerable of length x where x < n" do
+ it "returns an array containing the maximum n elements of length x" do
+ result = @e_ints.max(500)
+ result.length.should == 5
+ end
+ end
+
+ context "that is negative" do
+ it "raises an ArgumentError" do
+ -> { @e_ints.max(-1) }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ context "that is nil" do
+ it "returns the maximum element" do
+ @e_ints.max(nil).should == 1010101010
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/member_spec.rb b/spec/ruby/core/enumerable/member_spec.rb
new file mode 100644
index 0000000000..1fe3cebd28
--- /dev/null
+++ b/spec/ruby/core/enumerable/member_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/include'
+
+describe "Enumerable#member?" do
+ it_behaves_like :enumerable_include, :member?
+end
diff --git a/spec/ruby/core/enumerable/min_by_spec.rb b/spec/ruby/core/enumerable/min_by_spec.rb
new file mode 100644
index 0000000000..4f949e2130
--- /dev/null
+++ b/spec/ruby/core/enumerable/min_by_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#min_by" do
+ it "returns an enumerator if no block" do
+ EnumerableSpecs::Numerous.new(42).min_by.should.instance_of?(Enumerator)
+ end
+
+ it "returns nil if #each yields no objects" do
+ EnumerableSpecs::Empty.new.min_by {|o| o.nonesuch }.should == nil
+ end
+
+ it "returns the object for whom the value returned by block is the smallest" do
+ EnumerableSpecs::Numerous.new(*%w[3 2 1]).min_by {|obj| obj.to_i }.should == '1'
+ EnumerableSpecs::Numerous.new(*%w[five three]).min_by {|obj| obj.length }.should == 'five'
+ end
+
+ it "returns the object that appears first in #each in case of a tie" do
+ a, b, c = '2', '1', '1'
+ EnumerableSpecs::Numerous.new(a, b, c).min_by {|obj| obj.to_i }.should.equal?(b)
+ end
+
+ it "uses min.<=>(current) to determine order" do
+ a, b, c = (1..3).map{|n| EnumerableSpecs::ReverseComparable.new(n)}
+
+ # Just using self here to avoid additional complexity
+ EnumerableSpecs::Numerous.new(a, b, c).min_by {|obj| obj }.should == c
+ end
+
+ it "is able to return the minimum for enums that contain nils" do
+ enum = EnumerableSpecs::Numerous.new(nil, nil, true)
+ enum.min_by {|o| o.nil? ? 0 : 1 }.should == nil
+ enum.min_by {|o| o.nil? ? 1 : 0 }.should == true
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.min_by {|e| e.size}.should == [1, 2]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :min_by
+
+ context "when called with an argument n" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(101, 55, 1, 20, 33, 500, 60)
+ end
+
+ context "without a block" do
+ it "returns an enumerator" do
+ @enum.min_by(2).should.instance_of?(Enumerator)
+ end
+ end
+
+ context "with a block" do
+ it "returns an array containing the minimum n elements based on the block's value" do
+ result = @enum.min_by(3) { |i| i.to_s }
+ result.should == [1, 101, 20]
+ end
+
+ context "on a enumerable of length x where x < n" do
+ it "returns an array containing the minimum n elements of length n" do
+ result = @enum.min_by(500) { |i| i.to_s }
+ result.length.should == 7
+ end
+ end
+
+ context "when n is negative" do
+ it "raises an ArgumentError" do
+ -> { @enum.min_by(-1) { |i| i.to_s } }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ context "when n is nil" do
+ it "returns the minimum element" do
+ @enum.min_by(nil) { |i| i.to_s }.should == 1
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/min_spec.rb b/spec/ruby/core/enumerable/min_spec.rb
new file mode 100644
index 0000000000..f05d59c2c9
--- /dev/null
+++ b/spec/ruby/core/enumerable/min_spec.rb
@@ -0,0 +1,123 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#min" do
+ before :each do
+ @e_strs = EnumerableSpecs::EachDefiner.new("333", "22", "666666", "1", "55555", "1010101010")
+ @e_ints = EnumerableSpecs::EachDefiner.new( 333, 22, 666666, 55555, 1010101010)
+ end
+
+ it "min should return the minimum element" do
+ EnumerableSpecs::Numerous.new.min.should == 1
+ end
+
+ it "returns the minimum (basic cases)" do
+ EnumerableSpecs::EachDefiner.new(55).min.should == 55
+
+ EnumerableSpecs::EachDefiner.new(11,99).min.should == 11
+ EnumerableSpecs::EachDefiner.new(99,11).min.should == 11
+ EnumerableSpecs::EachDefiner.new(2, 33, 4, 11).min.should == 2
+
+ EnumerableSpecs::EachDefiner.new(1,2,3,4,5).min.should == 1
+ EnumerableSpecs::EachDefiner.new(5,4,3,2,1).min.should == 1
+ EnumerableSpecs::EachDefiner.new(4,1,3,5,2).min.should == 1
+ EnumerableSpecs::EachDefiner.new(5,5,5,5,5).min.should == 5
+
+ EnumerableSpecs::EachDefiner.new("aa","tt").min.should == "aa"
+ EnumerableSpecs::EachDefiner.new("tt","aa").min.should == "aa"
+ EnumerableSpecs::EachDefiner.new("2","33","4","11").min.should == "11"
+
+ @e_strs.min.should == "1"
+ @e_ints.min.should == 22
+ end
+
+ it "returns nil for an empty Enumerable" do
+ EnumerableSpecs::EachDefiner.new.min.should == nil
+ end
+
+ it "raises a NoMethodError for elements without #<=>" do
+ -> do
+ EnumerableSpecs::EachDefiner.new(BasicObject.new, BasicObject.new).min
+ end.should.raise(NoMethodError)
+ end
+
+ it "raises an ArgumentError for incomparable elements" do
+ -> do
+ EnumerableSpecs::EachDefiner.new(11,"22").min
+ end.should.raise(ArgumentError)
+ -> do
+ EnumerableSpecs::EachDefiner.new(11,12,22,33).min{|a, b| nil}
+ end.should.raise(ArgumentError)
+ end
+
+ it "returns the minimum when using a block rule" do
+ EnumerableSpecs::EachDefiner.new("2","33","4","11").min {|a,b| a <=> b }.should == "11"
+ EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).min {|a,b| a <=> b }.should == 2
+
+ EnumerableSpecs::EachDefiner.new("2","33","4","11").min {|a,b| b <=> a }.should == "4"
+ EnumerableSpecs::EachDefiner.new( 2 , 33 , 4 , 11 ).min {|a,b| b <=> a }.should == 33
+
+ EnumerableSpecs::EachDefiner.new( 1, 2, 3, 4 ).min {|a,b| 15 }.should == 1
+
+ EnumerableSpecs::EachDefiner.new(11,12,22,33).min{|a, b| 2 }.should == 11
+ @i = -2
+ EnumerableSpecs::EachDefiner.new(11,12,22,33).min{|a, b| @i += 1 }.should == 12
+
+ @e_strs.min {|a,b| a.length <=> b.length }.should == "1"
+
+ @e_strs.min {|a,b| a <=> b }.should == "1"
+ @e_strs.min {|a,b| a.to_i <=> b.to_i }.should == "1"
+
+ @e_ints.min {|a,b| a <=> b }.should == 22
+ @e_ints.min {|a,b| a.to_s <=> b.to_s }.should == 1010101010
+ end
+
+ it "returns the minimum for enumerables that contain nils" do
+ arr = EnumerableSpecs::Numerous.new(nil, nil, true)
+ arr.min { |a, b|
+ x = a.nil? ? -1 : a ? 0 : 1
+ y = b.nil? ? -1 : b ? 0 : 1
+ x <=> y
+ }.should == nil
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.min.should == [1, 2]
+ end
+
+ context "when called with an argument n" do
+ context "without a block" do
+ it "returns an array containing the minimum n elements" do
+ result = @e_ints.min(2)
+ result.should == [22, 333]
+ end
+ end
+
+ context "with a block" do
+ it "returns an array containing the minimum n elements" do
+ result = @e_ints.min(2) { |a, b| a * 2 <=> b * 2 }
+ result.should == [22, 333]
+ end
+ end
+
+ context "on a enumerable of length x where x < n" do
+ it "returns an array containing the minimum n elements of length x" do
+ result = @e_ints.min(500)
+ result.length.should == 5
+ end
+ end
+
+ context "that is negative" do
+ it "raises an ArgumentError" do
+ -> { @e_ints.min(-1) }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ context "that is nil" do
+ it "returns the minimum element" do
+ @e_ints.min(nil).should == 22
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/minmax_by_spec.rb b/spec/ruby/core/enumerable/minmax_by_spec.rb
new file mode 100644
index 0000000000..30c88cfcfb
--- /dev/null
+++ b/spec/ruby/core/enumerable/minmax_by_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#minmax_by" do
+ it "returns an enumerator if no block" do
+ EnumerableSpecs::Numerous.new(42).minmax_by.should.instance_of?(Enumerator)
+ end
+
+ it "returns nil if #each yields no objects" do
+ EnumerableSpecs::Empty.new.minmax_by {|o| o.nonesuch }.should == [nil, nil]
+ end
+
+ it "returns the object for whom the value returned by block is the largest" do
+ EnumerableSpecs::Numerous.new(*%w[1 2 3]).minmax_by {|obj| obj.to_i }.should == ['1', '3']
+ EnumerableSpecs::Numerous.new(*%w[three five]).minmax_by {|obj| obj.length }.should == ['five', 'three']
+ end
+
+ it "returns the object that appears first in #each in case of a tie" do
+ a, b, c, d = '1', '1', '2', '2'
+ mm = EnumerableSpecs::Numerous.new(a, b, c, d).minmax_by {|obj| obj.to_i }
+ mm[0].should.equal?(a)
+ mm[1].should.equal?(c)
+ end
+
+ it "uses min/max.<=>(current) to determine order" do
+ a, b, c = (1..3).map{|n| EnumerableSpecs::ReverseComparable.new(n)}
+
+ # Just using self here to avoid additional complexity
+ EnumerableSpecs::Numerous.new(a, b, c).minmax_by {|obj| obj }.should == [c, a]
+ end
+
+ it "is able to return the maximum for enums that contain nils" do
+ enum = EnumerableSpecs::Numerous.new(nil, nil, true)
+ enum.minmax_by {|o| o.nil? ? 0 : 1 }.should == [nil, true]
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.minmax_by {|e| e.size}.should == [[1, 2], [6, 7, 8, 9]]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :minmax_by
+end
diff --git a/spec/ruby/core/enumerable/minmax_spec.rb b/spec/ruby/core/enumerable/minmax_spec.rb
new file mode 100644
index 0000000000..f5f17ef079
--- /dev/null
+++ b/spec/ruby/core/enumerable/minmax_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/enumerable/minmax'
+
+describe "Enumerable#minmax" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(6, 4, 5, 10, 8)
+ @empty_enum = EnumerableSpecs::Empty.new
+ @incomparable_enum = EnumerableSpecs::Numerous.new(BasicObject.new, BasicObject.new)
+ @incompatible_enum = EnumerableSpecs::Numerous.new(11,"22")
+ @strs = EnumerableSpecs::Numerous.new("333", "2", "60", "55555", "1010", "111")
+ end
+
+ it_behaves_like :enumerable_minmax, :minmax
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.minmax.should == [[1, 2], [6, 7, 8, 9]]
+ end
+end
diff --git a/spec/ruby/core/enumerable/none_spec.rb b/spec/ruby/core/enumerable/none_spec.rb
new file mode 100644
index 0000000000..d9ee0b441e
--- /dev/null
+++ b/spec/ruby/core/enumerable/none_spec.rb
@@ -0,0 +1,153 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#none?" do
+ before :each do
+ @empty = EnumerableSpecs::Empty.new
+ @enum = EnumerableSpecs::Numerous.new
+ @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1)
+ @enum2 = EnumerableSpecs::Numerous.new(nil, false, true)
+ end
+
+ it "always returns true on empty enumeration" do
+ @empty.should.none?
+ @empty.none? { true }.should == true
+ end
+
+ it "raises an ArgumentError when more than 1 argument is provided" do
+ -> { @enum.none?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { [].none?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { {}.none?(1, 2, 3) }.should.raise(ArgumentError)
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.none?
+ }.should.raise(RuntimeError)
+
+ -> {
+ EnumerableSpecs::ThrowingEach.new.none? { false }
+ }.should.raise(RuntimeError)
+ end
+
+ describe "with no block" do
+ it "returns true if none of the elements in self are true" do
+ e = EnumerableSpecs::Numerous.new(false, nil, false)
+ e.none?.should == true
+ end
+
+ it "returns false if at least one of the elements in self are true" do
+ e = EnumerableSpecs::Numerous.new(false, nil, true, false)
+ e.none?.should == false
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMultiWithFalse.new
+ multi.none?.should == false
+ end
+ end
+
+ describe "with a block" do
+ before :each do
+ @e = EnumerableSpecs::Numerous.new(1,1,2,3,4)
+ end
+
+ it "passes each element to the block in turn until it returns true" do
+ acc = []
+ @e.none? {|e| acc << e; false }
+ acc.should == [1,1,2,3,4]
+ end
+
+ it "stops passing elements to the block when it returns true" do
+ acc = []
+ @e.none? {|e| acc << e; e == 3 ? true : false }
+ acc.should == [1,1,2,3]
+ end
+
+ it "returns true if the block never returns true" do
+ @e.none? {|e| false }.should == true
+ end
+
+ it "returns false if the block ever returns true" do
+ @e.none? {|e| e == 3 ? true : false }.should == false
+ end
+
+ it "does not hide exceptions out of the block" do
+ -> {
+ @enum.none? { raise "from block" }
+ }.should.raise(RuntimeError)
+ end
+
+ it "gathers initial args as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.none? { |e| yielded << e; false }
+ yielded.should == [1, 3, 6]
+ end
+
+ it "yields multiple arguments when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.none? { |*args| yielded << args; false }
+ yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+ end
+
+ describe 'when given a pattern argument' do
+ it "calls `===` on the pattern the return value" do
+ pattern = EnumerableSpecs::Pattern.new { |x| x == 3 }
+ @enum1.none?(pattern).should == true
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+ end
+
+ it "always returns true on empty enumeration" do
+ @empty.none?(Integer).should == true
+ [].none?(Integer).should == true
+ {}.none?(NilClass).should == true
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.none?(Integer)
+ }.should.raise(RuntimeError)
+ end
+
+ it "returns true if the pattern never returns a truthy value" do
+ @enum2.none?(Integer).should == true
+ pattern = EnumerableSpecs::Pattern.new { |x| nil }
+ @enum.none?(pattern).should == true
+
+ [1, 42, 3].none?(pattern).should == true
+ {a: 1, b: 2}.none?(pattern).should == true
+ end
+
+ it "returns false if the pattern ever returns other than false or nil" do
+ pattern = EnumerableSpecs::Pattern.new { |x| x < 0 }
+ @enum1.none?(pattern).should == false
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+
+ [1, 2, 3, -1].none?(pattern).should == false
+ {a: 1}.none?(Array).should == false
+ end
+
+ it "does not hide exceptions out of pattern#===" do
+ pattern = EnumerableSpecs::Pattern.new { raise "from pattern" }
+ -> {
+ @enum.none?(pattern)
+ }.should.raise(RuntimeError)
+ end
+
+ it "calls the pattern with gathered array when yielded with multiple arguments" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ pattern = EnumerableSpecs::Pattern.new { false }
+ multi.none?(pattern).should == true
+ pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]]
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ EnumerableSpecs::Numerous.new(1, 2, 3, 4, 5).none?(String) { true }.should == true
+ }.should complain(/given block not used/)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/one_spec.rb b/spec/ruby/core/enumerable/one_spec.rb
new file mode 100644
index 0000000000..cf966d4a9b
--- /dev/null
+++ b/spec/ruby/core/enumerable/one_spec.rb
@@ -0,0 +1,154 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#one?" do
+ before :each do
+ @empty = EnumerableSpecs::Empty.new
+ @enum = EnumerableSpecs::Numerous.new
+ @enum1 = EnumerableSpecs::Numerous.new(0, 1, 2, -1)
+ @enum2 = EnumerableSpecs::Numerous.new(nil, false, true)
+ end
+
+ it "always returns false on empty enumeration" do
+ @empty.should_not.one?
+ @empty.one? { true }.should == false
+ end
+
+ it "raises an ArgumentError when more than 1 argument is provided" do
+ -> { @enum.one?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { [].one?(1, 2, 3) }.should.raise(ArgumentError)
+ -> { {}.one?(1, 2, 3) }.should.raise(ArgumentError)
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.one?
+ }.should.raise(RuntimeError)
+
+ -> {
+ EnumerableSpecs::ThrowingEach.new.one? { false }
+ }.should.raise(RuntimeError)
+ end
+
+ describe "with no block" do
+ it "returns true if only one element evaluates to true" do
+ [false, nil, true].one?.should == true
+ end
+
+ it "returns false if two elements evaluate to true" do
+ [false, :value, nil, true].one?.should == false
+ end
+
+ it "returns false if all elements evaluate to false" do
+ [false, nil, false].one?.should == false
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMultiWithSingleTrue.new
+ multi.one?.should == false
+ end
+ end
+
+ describe "with a block" do
+ it "returns true if block returns true once" do
+ [:a, :b, :c].one? { |s| s == :a }.should == true
+ end
+
+ it "returns false if the block returns true more than once" do
+ [:a, :b, :c].one? { |s| s == :a || s == :b }.should == false
+ end
+
+ it "returns false if the block only returns false" do
+ [:a, :b, :c].one? { |s| s == :d }.should == false
+ end
+
+ it "does not hide exceptions out of the block" do
+ -> {
+ @enum.one? { raise "from block" }
+ }.should.raise(RuntimeError)
+ end
+
+ it "gathers initial args as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.one? { |e| yielded << e; false }.should == false
+ yielded.should == [1, 3, 6]
+ end
+
+ it "yields multiple arguments when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.one? { |*args| yielded << args; false }.should == false
+ yielded.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+ end
+
+ describe 'when given a pattern argument' do
+ it "calls `===` on the pattern the return value" do
+ pattern = EnumerableSpecs::Pattern.new { |x| x == 1 }
+ @enum1.one?(pattern).should == true
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+ end
+
+ it "always returns false on empty enumeration" do
+ @empty.one?(Integer).should == false
+ [].one?(Integer).should == false
+ {}.one?(NilClass).should == false
+ end
+
+ it "does not hide exceptions out of #each" do
+ -> {
+ EnumerableSpecs::ThrowingEach.new.one?(Integer)
+ }.should.raise(RuntimeError)
+ end
+
+ it "returns true if the pattern returns a truthy value only once" do
+ @enum2.one?(NilClass).should == true
+ pattern = EnumerableSpecs::Pattern.new { |x| x == 2 }
+ @enum1.one?(pattern).should == true
+
+ [1, 2, 42, 3].one?(pattern).should == true
+
+ pattern = EnumerableSpecs::Pattern.new { |x| x == [:b, 2] }
+ {a: 1, b: 2}.one?(pattern).should == true
+ end
+
+ it "returns false if the pattern returns a truthy value more than once" do
+ pattern = EnumerableSpecs::Pattern.new { |x| !x }
+ @enum2.one?(pattern).should == false
+ pattern.yielded.should == [[nil], [false]]
+
+ [1, 2, 3].one?(Integer).should == false
+ {a: 1, b: 2}.one?(Array).should == false
+ end
+
+ it "returns false if the pattern never returns a truthy value" do
+ pattern = EnumerableSpecs::Pattern.new { |x| nil }
+ @enum1.one?(pattern).should == false
+ pattern.yielded.should == [[0], [1], [2], [-1]]
+
+ [1, 2, 3].one?(pattern).should == false
+ {a: 1}.one?(pattern).should == false
+ end
+
+ it "does not hide exceptions out of pattern#===" do
+ pattern = EnumerableSpecs::Pattern.new { raise "from pattern" }
+ -> {
+ @enum.one?(pattern)
+ }.should.raise(RuntimeError)
+ end
+
+ it "calls the pattern with gathered array when yielded with multiple arguments" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ pattern = EnumerableSpecs::Pattern.new { false }
+ multi.one?(pattern).should == false
+ pattern.yielded.should == [[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]]
+ end
+
+ it "ignores the block if there is an argument" do
+ -> {
+ EnumerableSpecs::Numerous.new(1, 2, 3, 4, "5").one?(String) { false }.should == true
+ }.should complain(/given block not used/)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/partition_spec.rb b/spec/ruby/core/enumerable/partition_spec.rb
new file mode 100644
index 0000000000..8272ee189e
--- /dev/null
+++ b/spec/ruby/core/enumerable/partition_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#partition" do
+ it "returns two arrays, the first containing elements for which the block is true, the second containing the rest" do
+ EnumerableSpecs::Numerous.new.partition { |i| i % 2 == 0 }.should == [[2, 6, 4], [5, 3, 1]]
+ end
+
+ it "returns an Enumerator if called without a block" do
+ EnumerableSpecs::Numerous.new.partition.should.instance_of?(Enumerator)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.partition {|e| e == [3, 4, 5] }.should == [[[3, 4, 5]], [[1, 2], [6, 7, 8, 9]]]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :partition
+end
diff --git a/spec/ruby/core/enumerable/reduce_spec.rb b/spec/ruby/core/enumerable/reduce_spec.rb
new file mode 100644
index 0000000000..bc8691c1b0
--- /dev/null
+++ b/spec/ruby/core/enumerable/reduce_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/inject'
+
+describe "Enumerable#reduce" do
+ it_behaves_like :enumerable_inject, :reduce
+end
diff --git a/spec/ruby/core/enumerable/reject_spec.rb b/spec/ruby/core/enumerable/reject_spec.rb
new file mode 100644
index 0000000000..31e89f5b0e
--- /dev/null
+++ b/spec/ruby/core/enumerable/reject_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#reject" do
+ it "returns an array of the elements for which block is false" do
+ EnumerableSpecs::Numerous.new.reject { |i| i > 3 }.should == [2, 3, 1]
+ entries = (1..10).to_a
+ numerous = EnumerableSpecs::Numerous.new(*entries)
+ numerous.reject {|i| i % 2 == 0 }.should == [1,3,5,7,9]
+ numerous.reject {|i| true }.should == []
+ numerous.reject {|i| false }.should == entries
+ end
+
+ it "returns an Enumerator if called without a block" do
+ EnumerableSpecs::Numerous.new.reject.should.instance_of?(Enumerator)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.reject {|e| e == [3, 4, 5] }.should == [[1, 2], [6, 7, 8, 9]]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :reject
+end
diff --git a/spec/ruby/core/enumerable/reverse_each_spec.rb b/spec/ruby/core/enumerable/reverse_each_spec.rb
new file mode 100644
index 0000000000..4753956724
--- /dev/null
+++ b/spec/ruby/core/enumerable/reverse_each_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#reverse_each" do
+ it "traverses enum in reverse order and pass each element to block" do
+ a=[]
+ EnumerableSpecs::Numerous.new.reverse_each { |i| a << i }
+ a.should == [4, 1, 6, 3, 5, 2]
+ end
+
+ it "returns an Enumerator if no block given" do
+ enum = EnumerableSpecs::Numerous.new.reverse_each
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == [4, 1, 6, 3, 5, 2]
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ yielded = []
+ multi.reverse_each {|e| yielded << e }
+ yielded.should == [[6, 7, 8, 9], [3, 4, 5], [1, 2]]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :reverse_each
+end
diff --git a/spec/ruby/core/enumerable/select_spec.rb b/spec/ruby/core/enumerable/select_spec.rb
new file mode 100644
index 0000000000..7fc61926f9
--- /dev/null
+++ b/spec/ruby/core/enumerable/select_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/find_all'
+
+describe "Enumerable#select" do
+ it_behaves_like :enumerable_find_all, :select
+end
diff --git a/spec/ruby/core/enumerable/shared/collect.rb b/spec/ruby/core/enumerable/shared/collect.rb
new file mode 100644
index 0000000000..4696d32454
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/collect.rb
@@ -0,0 +1,107 @@
+require_relative 'enumerable_enumeratorized'
+
+describe :enumerable_collect, shared: true do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns a new array with the results of passing each element to block" do
+ entries = [0, 1, 3, 4, 5, 6]
+ numerous = EnumerableSpecs::Numerous.new(*entries)
+ numerous.send(@method) { |i| i % 2 }.should == [0, 1, 1, 0, 1, 0]
+ numerous.send(@method) { |i| i }.should == entries
+ end
+
+ it "passes through the values yielded by #each_with_index" do
+ [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i]; nil }
+ ScratchPad.recorded.should == [[:a, 0], [:b, 1]]
+ end
+
+ it "gathers initial args as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.send(@method) {|e| e}.should == [1,3,6]
+ end
+
+ it "only yields increasing values for a Range" do
+ (1..0).send(@method) { |x| x }.should == []
+ (1..1).send(@method) { |x| x }.should == [1]
+ (1..2).send(@method) { |x| x }.should == [1, 2]
+ end
+
+ it "returns an enumerator when no block given" do
+ enum = EnumerableSpecs::Numerous.new.send(@method)
+ enum.should.instance_of?(Enumerator)
+ enum.each { |i| -i }.should == [-2, -5, -3, -6, -1, -4]
+ end
+
+ it "reports the same arity as the given block" do
+ entries = [0, 1, 3, 4, 5, 6]
+ numerous = EnumerableSpecs::Numerous.new(*entries)
+
+ def numerous.each(&block)
+ ScratchPad << block.arity
+ super
+ end
+
+ numerous.send(@method) { |a, b| a % 2 }.should == [0, 1, 1, 0, 1, 0]
+ ScratchPad.recorded.should == [2]
+ ScratchPad.clear
+ ScratchPad.record []
+ numerous.send(@method) { |i| i }.should == entries
+ ScratchPad.recorded.should == [1]
+ end
+
+ it "yields an Array of 2 elements for a Hash when block arity is 1" do
+ c = Class.new do
+ def register(a)
+ ScratchPad << a
+ end
+ end
+ m = c.new.method(:register)
+
+ ScratchPad.record []
+ { 1 => 'a', 2 => 'b' }.map(&m)
+ ScratchPad.recorded.should == [[1, 'a'], [2, 'b']]
+ end
+
+ it "yields 2 arguments for a Hash when block arity is 2" do
+ c = Class.new do
+ def register(a, b)
+ ScratchPad << [a, b]
+ end
+ end
+ m = c.new.method(:register)
+
+ ScratchPad.record []
+ { 1 => 'a', 2 => 'b' }.map(&m)
+ ScratchPad.recorded.should == [[1, 'a'], [2, 'b']]
+ end
+
+ it "raises an error for a Hash when an arity enforcing block of arity >2 is passed in" do
+ c = Class.new do
+ def register(a, b, c)
+ end
+ end
+ m = c.new.method(:register)
+
+ -> do
+ { 1 => 'a', 2 => 'b' }.map(&m)
+ end.should.raise(ArgumentError)
+ end
+
+ it "calls the each method on sub-classes" do
+ c = Class.new(Hash) do
+ def each
+ ScratchPad << 'in each'
+ super
+ end
+ end
+ h = c.new
+ h[1] = 'a'
+ ScratchPad.record []
+ h.send(@method) { |k,v| v }
+ ScratchPad.recorded.should == ['in each']
+ end
+
+ it_should_behave_like :enumerable_enumeratorized_with_origin_size
+end
diff --git a/spec/ruby/core/enumerable/shared/collect_concat.rb b/spec/ruby/core/enumerable/shared/collect_concat.rb
new file mode 100644
index 0000000000..1694e3fdce
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/collect_concat.rb
@@ -0,0 +1,54 @@
+require_relative 'enumerable_enumeratorized'
+
+describe :enumerable_collect_concat, shared: true do
+ it "yields elements to the block and flattens one level" do
+ numerous = EnumerableSpecs::Numerous.new(1, [2, 3], [4, [5, 6]], {foo: :bar})
+ numerous.send(@method) { |i| i }.should == [1, 2, 3, 4, [5, 6], {foo: :bar}]
+ end
+
+ it "appends non-Array elements that do not define #to_ary" do
+ obj = mock("to_ary undefined")
+
+ numerous = EnumerableSpecs::Numerous.new(1, obj, 2)
+ numerous.send(@method) { |i| i }.should == [1, obj, 2]
+ end
+
+ it "concatenates the result of calling #to_ary if it returns an Array" do
+ obj = mock("to_ary defined")
+ obj.should_receive(:to_ary).and_return([:a, :b])
+
+ numerous = EnumerableSpecs::Numerous.new(1, obj, 2)
+ numerous.send(@method) { |i| i }.should == [1, :a, :b, 2]
+ end
+
+ it "does not call #to_a" do
+ obj = mock("to_ary undefined")
+ obj.should_not_receive(:to_a)
+
+ numerous = EnumerableSpecs::Numerous.new(1, obj, 2)
+ numerous.send(@method) { |i| i }.should == [1, obj, 2]
+ end
+
+ it "appends an element that defines #to_ary that returns nil" do
+ obj = mock("to_ary defined")
+ obj.should_receive(:to_ary).and_return(nil)
+
+ numerous = EnumerableSpecs::Numerous.new(1, obj, 2)
+ numerous.send(@method) { |i| i }.should == [1, obj, 2]
+ end
+
+ it "raises a TypeError if an element defining #to_ary does not return an Array or nil" do
+ obj = mock("to_ary defined")
+ obj.should_receive(:to_ary).and_return("array")
+
+ -> { [1, obj, 3].send(@method) { |i| i } }.should.raise(TypeError)
+ end
+
+ it "returns an enumerator when no block given" do
+ enum = EnumerableSpecs::Numerous.new(1, 2).send(@method)
+ enum.should.instance_of?(Enumerator)
+ enum.each{ |i| [i] * i }.should == [1, 2, 2]
+ end
+
+ it_should_behave_like :enumerable_enumeratorized_with_origin_size
+end
diff --git a/spec/ruby/core/enumerable/shared/entries.rb b/spec/ruby/core/enumerable/shared/entries.rb
new file mode 100644
index 0000000000..e32eb23d2a
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/entries.rb
@@ -0,0 +1,16 @@
+describe :enumerable_entries, shared: true do
+ it "returns an array containing the elements" do
+ numerous = EnumerableSpecs::Numerous.new(1, nil, 'a', 2, false, true)
+ numerous.send(@method).should == [1, nil, "a", 2, false, true]
+ end
+
+ it "passes through the values yielded by #each_with_index" do
+ [:a, :b].each_with_index.send(@method).should == [[:a, 0], [:b, 1]]
+ end
+
+ it "passes arguments to each" do
+ count = EnumerableSpecs::EachCounter.new(1, 2, 3)
+ count.send(@method, :hello, "world").should == [1, 2, 3]
+ count.arguments_passed.should == [:hello, "world"]
+ end
+end
diff --git a/spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb b/spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb
new file mode 100644
index 0000000000..e2bbe18eda
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/enumerable_enumeratorized.rb
@@ -0,0 +1,33 @@
+require_relative 'enumeratorized'
+
+describe :enumerable_enumeratorized_with_unknown_size, shared: true do
+ describe "Enumerable with size" do
+ before :all do
+ @object = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4)
+ end
+ it_should_behave_like :enumeratorized_with_unknown_size
+ end
+
+ describe "Enumerable with no size" do
+ before :all do
+ @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4)
+ end
+ it_should_behave_like :enumeratorized_with_unknown_size
+ end
+end
+
+describe :enumerable_enumeratorized_with_origin_size, shared: true do
+ describe "Enumerable with size" do
+ before :all do
+ @object = EnumerableSpecs::NumerousWithSize.new(1, 2, 3, 4)
+ end
+ it_should_behave_like :enumeratorized_with_origin_size
+ end
+
+ describe "Enumerable with no size" do
+ before :all do
+ @object = EnumerableSpecs::Numerous.new(1, 2, 3, 4)
+ end
+ it_should_behave_like :enumeratorized_with_unknown_size
+ end
+end
diff --git a/spec/ruby/core/enumerable/shared/enumeratorized.rb b/spec/ruby/core/enumerable/shared/enumeratorized.rb
new file mode 100644
index 0000000000..05d27b5783
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/enumeratorized.rb
@@ -0,0 +1,42 @@
+describe :enumeratorized_with_unknown_size, shared: true do
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ it "size returns nil" do
+ @object.send(*@method).size.should == nil
+ end
+ end
+ end
+end
+
+describe :enumeratorized_with_origin_size, shared: true do
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ it "size returns the enumerable size" do
+ @object.send(*@method).size.should == @object.size
+ end
+ end
+ end
+end
+
+describe :enumeratorized_with_cycle_size, shared: true do
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should be the result of multiplying the enumerable size by the argument passed" do
+ @object.cycle(2).size.should == @object.size * 2
+ @object.cycle(7).size.should == @object.size * 7
+ @object.cycle(0).size.should == 0
+ @empty_object.cycle(2).size.should == 0
+ end
+
+ it "should be zero when the argument passed is 0 or less" do
+ @object.cycle(-1).size.should == 0
+ end
+
+ it "should be Float::INFINITY when no argument is passed" do
+ @object.cycle.size.should == Float::INFINITY
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/shared/find.rb b/spec/ruby/core/enumerable/shared/find.rb
new file mode 100644
index 0000000000..cdff640415
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/find.rb
@@ -0,0 +1,77 @@
+require_relative 'enumerable_enumeratorized'
+
+describe :enumerable_find, shared: true do
+ # #detect and #find are aliases, so we only need one function
+ before :each do
+ ScratchPad.record []
+ @elements = [2, 4, 6, 8, 10]
+ @numerous = EnumerableSpecs::Numerous.new(*@elements)
+ @empty = []
+ end
+
+ it "passes each entry in enum to block while block when block is false" do
+ visited_elements = []
+ @numerous.send(@method) do |element|
+ visited_elements << element
+ false
+ end
+ visited_elements.should == @elements
+ end
+
+ it "returns nil when the block is false and there is no ifnone proc given" do
+ @numerous.send(@method) {|e| false }.should == nil
+ end
+
+ it "returns the first element for which the block is not false" do
+ @elements.each do |element|
+ @numerous.send(@method) {|e| e > element - 1 }.should == element
+ end
+ end
+
+ it "returns the value of the ifnone proc if the block is false" do
+ fail_proc = -> { "cheeseburgers" }
+ @numerous.send(@method, fail_proc) {|e| false }.should == "cheeseburgers"
+ end
+
+ it "doesn't call the ifnone proc if an element is found" do
+ fail_proc = -> { raise "This shouldn't have been called" }
+ @numerous.send(@method, fail_proc) {|e| e == @elements.first }.should == 2
+ end
+
+ it "calls the ifnone proc only once when the block is false" do
+ times = 0
+ fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" }
+ @numerous.send(@method, fail_proc) {|e| false }.should == "cheeseburgers"
+ end
+
+ it "calls the ifnone proc when there are no elements" do
+ fail_proc = -> { "yay" }
+ @empty.send(@method, fail_proc) {|e| true}.should == "yay"
+ end
+
+ it "ignores the ifnone argument when nil" do
+ @numerous.send(@method, nil) {|e| false }.should == nil
+ end
+
+ it "passes through the values yielded by #each_with_index" do
+ [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i]; nil }
+ ScratchPad.recorded.should == [[:a, 0], [:b, 1]]
+ end
+
+ it "returns an enumerator when no block given" do
+ @numerous.send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "passes the ifnone proc to the enumerator" do
+ times = 0
+ fail_proc = -> { times += 1; raise if times > 1; "cheeseburgers" }
+ @numerous.send(@method, fail_proc).each {|e| false }.should == "cheeseburgers"
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.send(@method) {|e| e == [1, 2] }.should == [1, 2]
+ end
+
+ it_should_behave_like :enumerable_enumeratorized_with_unknown_size
+end
diff --git a/spec/ruby/core/enumerable/shared/find_all.rb b/spec/ruby/core/enumerable/shared/find_all.rb
new file mode 100644
index 0000000000..27f01de6e0
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/find_all.rb
@@ -0,0 +1,31 @@
+require_relative 'enumerable_enumeratorized'
+
+describe :enumerable_find_all, shared: true do
+ before :each do
+ ScratchPad.record []
+ @elements = (1..10).to_a
+ @numerous = EnumerableSpecs::Numerous.new(*@elements)
+ end
+
+ it "returns all elements for which the block is not false" do
+ @numerous.send(@method) {|i| i % 3 == 0 }.should == [3, 6, 9]
+ @numerous.send(@method) {|i| true }.should == @elements
+ @numerous.send(@method) {|i| false }.should == []
+ end
+
+ it "returns an enumerator when no block given" do
+ @numerous.send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "passes through the values yielded by #each_with_index" do
+ [:a, :b].each_with_index.send(@method) { |x, i| ScratchPad << [x, i] }
+ ScratchPad.recorded.should == [[:a, 0], [:b, 1]]
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.send(@method) {|e| e == [3, 4, 5] }.should == [[3, 4, 5]]
+ end
+
+ it_should_behave_like :enumerable_enumeratorized_with_origin_size
+end
diff --git a/spec/ruby/core/enumerable/shared/include.rb b/spec/ruby/core/enumerable/shared/include.rb
new file mode 100644
index 0000000000..ea250f032b
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/include.rb
@@ -0,0 +1,34 @@
+describe :enumerable_include, shared: true do
+ it "returns true if any element == argument for numbers" do
+ class EnumerableSpecIncludeP; def ==(obj) obj == 5; end; end
+
+ elements = (0..5).to_a
+ EnumerableSpecs::Numerous.new(*elements).send(@method,5).should == true
+ EnumerableSpecs::Numerous.new(*elements).send(@method,10).should == false
+ EnumerableSpecs::Numerous.new(*elements).send(@method,EnumerableSpecIncludeP.new).should == true
+ end
+
+ it "returns true if any element == argument for other objects" do
+ class EnumerableSpecIncludeP11; def ==(obj); obj == '11'; end; end
+
+ elements = ('0'..'5').to_a + [EnumerableSpecIncludeP11.new]
+ EnumerableSpecs::Numerous.new(*elements).send(@method,'5').should == true
+ EnumerableSpecs::Numerous.new(*elements).send(@method,'10').should == false
+ EnumerableSpecs::Numerous.new(*elements).send(@method,EnumerableSpecIncludeP11.new).should == true
+ EnumerableSpecs::Numerous.new(*elements).send(@method,'11').should == true
+ end
+
+
+ it "returns true if any member of enum equals obj when == compare different classes (legacy rubycon)" do
+ # equality is tested with ==
+ EnumerableSpecs::Numerous.new(2,4,6,8,10).send(@method, 2.0).should == true
+ EnumerableSpecs::Numerous.new(2,4,[6,8],10).send(@method, [6, 8]).should == true
+ EnumerableSpecs::Numerous.new(2,4,[6,8],10).send(@method, [6.0, 8.0]).should == true
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.send(@method, [1,2]).should == true
+ end
+
+end
diff --git a/spec/ruby/core/enumerable/shared/inject.rb b/spec/ruby/core/enumerable/shared/inject.rb
new file mode 100644
index 0000000000..7da4f0ca99
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/inject.rb
@@ -0,0 +1,142 @@
+require_relative '../../array/shared/iterable_and_tolerating_size_increasing'
+
+describe :enumerable_inject, shared: true do
+ it "with argument takes a block with an accumulator (with argument as initial value) and the current element. Value of block becomes new accumulator" do
+ a = []
+ EnumerableSpecs::Numerous.new.send(@method, 0) { |memo, i| a << [memo, i]; i }
+ a.should == [[0, 2], [2, 5], [5, 3], [3, 6], [6, 1], [1, 4]]
+ EnumerableSpecs::EachDefiner.new(true, true, true).send(@method, nil) {|result, i| i && result}.should == nil
+ end
+
+ it "produces an array of the accumulator and the argument when given a block with a *arg" do
+ a = []
+ [1,2].send(@method, 0) {|*args| a << args; args[0] + args[1]}
+ a.should == [[0, 1], [1, 2]]
+ end
+
+ it "can take two argument" do
+ EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-).should == 4
+ EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, "-").should == 4
+
+ [1, 2, 3].send(@method, 10, :-).should == 4
+ [1, 2, 3].send(@method, 10, "-").should == 4
+ end
+
+ it "converts non-Symbol method name argument to String with #to_str if two arguments" do
+ name = Object.new
+ def name.to_str; "-"; end
+
+ EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, name).should == 4
+ [1, 2, 3].send(@method, 10, name).should == 4
+ end
+
+ it "raises TypeError when the second argument is not Symbol or String and it cannot be converted to String if two arguments" do
+ -> { EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/)
+ -> { [1, 2, 3].send(@method, 10, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/)
+ end
+
+ it "ignores the block if two arguments" do
+ -> {
+ EnumerableSpecs::Numerous.new(1, 2, 3).send(@method, 10, :-) { raise "we never get here"}.should == 4
+ }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true)
+
+ -> {
+ [1, 2, 3].send(@method, 10, :-) { raise "we never get here"}.should == 4
+ }.should complain(/#{__FILE__}:#{__LINE__-1}: warning: given block not used/, verbose: true)
+ end
+
+ it "does not warn when given a Symbol with $VERBOSE true" do
+ -> {
+ [1, 2].send(@method, 0, :+)
+ [1, 2].send(@method, :+)
+ EnumerableSpecs::Numerous.new(1, 2).send(@method, 0, :+)
+ EnumerableSpecs::Numerous.new(1, 2).send(@method, :+)
+ }.should_not complain(verbose: true)
+ end
+
+ it "can take a symbol argument" do
+ EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, :-).should == 4
+ [10, 1, 2, 3].send(@method, :-).should == 4
+ end
+
+ it "can take a String argument" do
+ EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, "-").should == 4
+ [10, 1, 2, 3].send(@method, "-").should == 4
+ end
+
+ it "converts non-Symbol method name argument to String with #to_str" do
+ name = Object.new
+ def name.to_str; "-"; end
+
+ EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, name).should == 4
+ [10, 1, 2, 3].send(@method, name).should == 4
+ end
+
+ it "raises TypeError when passed not Symbol or String method name argument and it cannot be converted to String" do
+ -> { EnumerableSpecs::Numerous.new(10, 1, 2, 3).send(@method, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/)
+ -> { [10, 1, 2, 3].send(@method, Object.new) }.should.raise(TypeError, /is not a symbol nor a string/)
+ end
+
+ it "without argument takes a block with an accumulator (with first element as initial value) and the current element. Value of block becomes new accumulator" do
+ a = []
+ EnumerableSpecs::Numerous.new.send(@method) { |memo, i| a << [memo, i]; i }
+ a.should == [[2, 5], [5, 3], [3, 6], [6, 1], [1, 4]]
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.send(@method, []) {|acc, e| acc << e }.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+
+ it "with inject arguments(legacy rubycon)" do
+ # with inject argument
+ EnumerableSpecs::EachDefiner.new().send(@method, 1) {|acc,x| 999 }.should == 1
+ EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| 999 }.should == 999
+ EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| acc }.should == 1
+ EnumerableSpecs::EachDefiner.new(2).send(@method, 1) {|acc,x| x }.should == 2
+
+ EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method, 100) {|acc,x| acc + x }.should == 110
+ EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method, 100) {|acc,x| acc * x }.should == 2400
+
+ EnumerableSpecs::EachDefiner.new('a','b','c').send(@method, "z") {|result, i| i+result}.should == "cbaz"
+ end
+
+ it "without inject arguments(legacy rubycon)" do
+ # no inject argument
+ EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| 999 }.should == 2
+ EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| acc }.should == 2
+ EnumerableSpecs::EachDefiner.new(2).send(@method) {|acc,x| x }.should == 2
+
+ EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method) {|acc,x| acc + x }.should == 10
+ EnumerableSpecs::EachDefiner.new(1,2,3,4).send(@method) {|acc,x| acc * x }.should == 24
+
+ EnumerableSpecs::EachDefiner.new('a','b','c').send(@method) {|result, i| i+result}.should == "cba"
+ EnumerableSpecs::EachDefiner.new(3, 4, 5).send(@method) {|result, i| result*i}.should == 60
+ EnumerableSpecs::EachDefiner.new([1], 2, 'a','b').send(@method){|r,i| r<<i}.should == [1, 2, 'a', 'b']
+ end
+
+ it "returns nil when fails(legacy rubycon)" do
+ EnumerableSpecs::EachDefiner.new().send(@method) {|acc,x| 999 }.should == nil
+ end
+
+ it "tolerates increasing a collection size during iterating Array" do
+ array = [:a, :b, :c]
+ ScratchPad.record []
+ i = 0
+
+ array.send(@method, nil) do |_, e|
+ ScratchPad << e
+ array << i if i < 100
+ i += 1
+ end
+
+ actual = ScratchPad.recorded
+ expected = [:a, :b, :c] + (0..99).to_a
+ actual.sort_by(&:to_s).should == expected.sort_by(&:to_s)
+ end
+
+ it "raises an ArgumentError when no parameters or block is given" do
+ -> { [1,2].send(@method) }.should.raise(ArgumentError)
+ -> { {one: 1, two: 2}.send(@method) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/enumerable/shared/take.rb b/spec/ruby/core/enumerable/shared/take.rb
new file mode 100644
index 0000000000..a6da06325f
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/take.rb
@@ -0,0 +1,63 @@
+describe :enumerable_take, shared: true do
+ before :each do
+ @values = [4,3,2,1,0,-1]
+ @enum = EnumerableSpecs::Numerous.new(*@values)
+ end
+
+ it "returns the first count elements if given a count" do
+ @enum.send(@method, 2).should == [4, 3]
+ @enum.send(@method, 4).should == [4, 3, 2, 1] # See redmine #1686 !
+ end
+
+ it "returns an empty array when passed count on an empty array" do
+ empty = EnumerableSpecs::Empty.new
+ empty.send(@method, 0).should == []
+ empty.send(@method, 1).should == []
+ empty.send(@method, 2).should == []
+ end
+
+ it "returns an empty array when passed count == 0" do
+ @enum.send(@method, 0).should == []
+ end
+
+ it "returns an array containing the first element when passed count == 1" do
+ @enum.send(@method, 1).should == [4]
+ end
+
+ it "raises an ArgumentError when count is negative" do
+ -> { @enum.send(@method, -1) }.should.raise(ArgumentError)
+ end
+
+ it "returns the entire array when count > length" do
+ @enum.send(@method, 100).should == @values
+ @enum.send(@method, 8).should == @values # See redmine #1686 !
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(3).at_most(:twice) # called twice, no apparent reason. See redmine #1554
+ @enum.send(@method, obj).should == [4, 3, 2]
+ end
+
+ it "raises a TypeError if the passed argument is not numeric" do
+ -> { @enum.send(@method, nil) }.should.raise(TypeError)
+ -> { @enum.send(@method, "a") }.should.raise(TypeError)
+
+ obj = mock("nonnumeric")
+ -> { @enum.send(@method, obj) }.should.raise(TypeError)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.send(@method, 1).should == [[1, 2]]
+ end
+
+ it "consumes only what is needed" do
+ thrower = EnumerableSpecs::ThrowingEach.new
+ thrower.send(@method, 0).should == []
+ counter = EnumerableSpecs::EachCounter.new(1,2,3,4)
+ counter.send(@method, 2).should == [1,2]
+ counter.times_called.should == 1
+ counter.times_yielded.should == 2
+ end
+end
diff --git a/spec/ruby/core/enumerable/shared/value_packing.rb b/spec/ruby/core/enumerable/shared/value_packing.rb
new file mode 100644
index 0000000000..ff77f45cdf
--- /dev/null
+++ b/spec/ruby/core/enumerable/shared/value_packing.rb
@@ -0,0 +1,26 @@
+# This is the behavior of rb_enum_values_pack() in CRuby
+describe :enumerable_value_packing, shared: true do
+ # @take must be set to a Proc that returns the take-result whose #each
+ # yields packed values (e.g. -> e { e.take(1) } or -> e { e.lazy.take(1) }).
+
+ it "yields a single nil for a zero-argument source yield" do
+ e = Enumerator.new { |y| y.yield }
+ args = nil
+ @take.call(e).each { |*a| args = a }
+ args.should == [nil]
+ end
+
+ it "yields the value for a single-argument source yield" do
+ e = Enumerator.new { |y| y.yield :v }
+ args = nil
+ @take.call(e).each { |*a| args = a }
+ args.should == [:v]
+ end
+
+ it "yields a packed Array for a multi-argument source yield" do
+ e = Enumerator.new { |y| y.yield 1, 2 }
+ args = nil
+ @take.call(e).each { |*a| args = a }
+ args.should == [[1, 2]]
+ end
+end
diff --git a/spec/ruby/core/enumerable/slice_after_spec.rb b/spec/ruby/core/enumerable/slice_after_spec.rb
new file mode 100644
index 0000000000..1ef37195e5
--- /dev/null
+++ b/spec/ruby/core/enumerable/slice_after_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#slice_after" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(7, 6, 5, 4, 3, 2, 1)
+ end
+
+ describe "when given an argument and no block" do
+ it "calls === on the argument to determine when to yield" do
+ arg = mock("filter")
+ arg.should_receive(:===).and_return(false, true, false, false, false, true, false)
+ e = @enum.slice_after(arg)
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == [[7, 6], [5, 4, 3, 2], [1]]
+ end
+
+ it "doesn't yield an empty array if the filter matches the first entry or the last entry" do
+ arg = mock("filter")
+ arg.should_receive(:===).and_return(true).exactly(7)
+ e = @enum.slice_after(arg)
+ e.to_a.should == [[7], [6], [5], [4], [3], [2], [1]]
+ end
+
+ it "uses standard boolean as a test" do
+ arg = mock("filter")
+ arg.should_receive(:===).and_return(false, :foo, nil, false, false, 42, false)
+ e = @enum.slice_after(arg)
+ e.to_a.should == [[7, 6], [5, 4, 3, 2], [1]]
+ end
+ end
+
+ describe "when given a block" do
+ describe "and no argument" do
+ it "calls the block to determine when to yield" do
+ e = @enum.slice_after{ |i| i == 6 || i == 2 }
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == [[7, 6], [5, 4, 3, 2], [1]]
+ end
+ end
+
+ describe "and an argument" do
+ it "raises an ArgumentError" do
+ -> { @enum.slice_after(42) { |i| i == 6 } }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ it "raises an ArgumentError when given an incorrect number of arguments" do
+ -> { @enum.slice_after("one", "two") }.should.raise(ArgumentError)
+ -> { @enum.slice_after }.should.raise(ArgumentError)
+ end
+end
+
+describe "when an iterator method yields more than one value" do
+ it "processes all yielded values" do
+ enum = EnumerableSpecs::YieldsMulti.new
+ result = enum.slice_after { |i| i == [3, 4, 5] }.to_a
+ result.should == [[[1, 2], [3, 4, 5]], [[6, 7, 8, 9]]]
+ end
+end
diff --git a/spec/ruby/core/enumerable/slice_before_spec.rb b/spec/ruby/core/enumerable/slice_before_spec.rb
new file mode 100644
index 0000000000..7eb4410a25
--- /dev/null
+++ b/spec/ruby/core/enumerable/slice_before_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#slice_before" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(7,6,5,4,3,2,1)
+ end
+
+ describe "when given an argument and no block" do
+ it "calls === on the argument to determine when to yield" do
+ arg = mock "filter"
+ arg.should_receive(:===).and_return(false, true, false, false, false, true, false)
+ e = @enum.slice_before(arg)
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == [[7], [6, 5, 4, 3], [2, 1]]
+ end
+
+ it "doesn't yield an empty array if the filter matches the first entry or the last entry" do
+ arg = mock "filter"
+ arg.should_receive(:===).and_return(true).exactly(7)
+ e = @enum.slice_before(arg)
+ e.to_a.should == [[7], [6], [5], [4], [3], [2], [1]]
+ end
+
+ it "uses standard boolean as a test" do
+ arg = mock "filter"
+ arg.should_receive(:===).and_return(false, :foo, nil, false, false, 42, false)
+ e = @enum.slice_before(arg)
+ e.to_a.should == [[7], [6, 5, 4, 3], [2, 1]]
+ end
+ end
+
+ describe "when given a block" do
+ describe "and no argument" do
+ it "calls the block to determine when to yield" do
+ e = @enum.slice_before{|i| i == 6 || i == 2}
+ e.should.instance_of?(Enumerator)
+ e.to_a.should == [[7], [6, 5, 4, 3], [2, 1]]
+ end
+ end
+
+ it "does not accept arguments" do
+ -> {
+ @enum.slice_before(1) {}
+ }.should.raise(ArgumentError)
+ end
+ end
+
+ it "raises an ArgumentError when given an incorrect number of arguments" do
+ -> { @enum.slice_before("one", "two") }.should.raise(ArgumentError)
+ -> { @enum.slice_before }.should.raise(ArgumentError)
+ end
+
+ describe "when an iterator method yields more than one value" do
+ it "processes all yielded values" do
+ enum = EnumerableSpecs::YieldsMulti.new
+ result = enum.slice_before { |i| i == [3, 4, 5] }.to_a
+ result.should == [[[1, 2]], [[3, 4, 5], [6, 7, 8, 9]]]
+ end
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_unknown_size, [:slice_before, 3]
+end
diff --git a/spec/ruby/core/enumerable/slice_when_spec.rb b/spec/ruby/core/enumerable/slice_when_spec.rb
new file mode 100644
index 0000000000..fe1ecd31e2
--- /dev/null
+++ b/spec/ruby/core/enumerable/slice_when_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#slice_when" do
+ before :each do
+ ary = [10, 9, 7, 6, 4, 3, 2, 1]
+ @enum = EnumerableSpecs::Numerous.new(*ary)
+ @result = @enum.slice_when { |i, j| i - 1 != j }
+ @enum_length = ary.length
+ end
+
+ context "when given a block" do
+ it "returns an enumerator" do
+ @result.should.instance_of?(Enumerator)
+ end
+
+ it "splits chunks between adjacent elements i and j where the block returns true" do
+ @result.to_a.should == [[10, 9], [7, 6], [4, 3, 2, 1]]
+ end
+
+ it "calls the block for length of the receiver enumerable minus one times" do
+ times_called = 0
+ @enum.slice_when do |i, j|
+ times_called += 1
+ i - 1 != j
+ end.to_a
+ times_called.should == (@enum_length - 1)
+ end
+
+ it "doesn't yield an empty array if the block matches the first or the last time" do
+ @enum.slice_when { true }.to_a.should == [[10], [9], [7], [6], [4], [3], [2], [1]]
+ end
+
+ it "doesn't yield an empty array on a small enumerable" do
+ EnumerableSpecs::Empty.new.slice_when { raise }.to_a.should == []
+ EnumerableSpecs::Numerous.new(42).slice_when { raise }.to_a.should == [[42]]
+ end
+ end
+
+ context "when not given a block" do
+ it "raises an ArgumentError" do
+ -> { @enum.slice_when }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "when an iterator method yields more than one value" do
+ it "processes all yielded values" do
+ def foo
+ yield 1, 2
+ end
+ to_enum(:foo).slice_when { true }.to_a.should == [[[1, 2]]]
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/sort_by_spec.rb b/spec/ruby/core/enumerable/sort_by_spec.rb
new file mode 100644
index 0000000000..62cf38ce3e
--- /dev/null
+++ b/spec/ruby/core/enumerable/sort_by_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#sort_by" do
+ it "returns an array of elements ordered by the result of block" do
+ a = EnumerableSpecs::Numerous.new("once", "upon", "a", "time")
+ a.sort_by { |i| i[0] }.should == ["a", "once", "time", "upon"]
+ end
+
+ it "sorts the object by the given attribute" do
+ a = EnumerableSpecs::SortByDummy.new("fooo")
+ b = EnumerableSpecs::SortByDummy.new("bar")
+
+ ar = [a, b].sort_by { |d| d.s }
+ ar.should == [b, a]
+ end
+
+ it "returns an Enumerator when a block is not supplied" do
+ a = EnumerableSpecs::Numerous.new("a","b")
+ a.sort_by.should.instance_of?(Enumerator)
+ a.to_a.should == ["a", "b"]
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.sort_by {|e| e.size}.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+
+ it "returns an array of elements when a block is supplied and #map returns an enumerable" do
+ b = EnumerableSpecs::MapReturnsEnumerable.new
+ b.sort_by{ |x| -x }.should == [3, 2, 1]
+ end
+
+ it "calls #each to iterate over the elements to be sorted" do
+ b = EnumerableSpecs::Numerous.new( 1, 2, 3 )
+ b.should_receive(:each).once.and_yield(1).and_yield(2).and_yield(3)
+ b.should_not_receive :map
+ b.sort_by { |x| -x }.should == [3, 2, 1]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_origin_size, :sort_by
+end
diff --git a/spec/ruby/core/enumerable/sort_spec.rb b/spec/ruby/core/enumerable/sort_spec.rb
new file mode 100644
index 0000000000..427b1cd8f1
--- /dev/null
+++ b/spec/ruby/core/enumerable/sort_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#sort" do
+ it "sorts by the natural order as defined by <=>" do
+ EnumerableSpecs::Numerous.new.sort.should == [1, 2, 3, 4, 5, 6]
+ sorted = EnumerableSpecs::ComparesByVowelCount.wrap("a" * 1, "a" * 2, "a"*3, "a"*4, "a"*5)
+ EnumerableSpecs::Numerous.new(sorted[2],sorted[0],sorted[1],sorted[3],sorted[4]).sort.should == sorted
+ end
+
+ it "yields elements to the provided block" do
+ EnumerableSpecs::Numerous.new.sort { |a, b| b <=> a }.should == [6, 5, 4, 3, 2, 1]
+ EnumerableSpecs::Numerous.new(2,0,1,3,4).sort { |n, m| -(n <=> m) }.should == [4,3,2,1,0]
+ end
+
+ it "raises a NoMethodError if elements do not define <=>" do
+ -> do
+ EnumerableSpecs::Numerous.new(BasicObject.new, BasicObject.new, BasicObject.new).sort
+ end.should.raise(NoMethodError)
+ end
+
+ it "sorts enumerables that contain nils" do
+ arr = EnumerableSpecs::Numerous.new(nil, true, nil, false, nil, true, nil, false, nil)
+ arr.sort { |a, b|
+ x = a ? -1 : a.nil? ? 0 : 1
+ y = b ? -1 : b.nil? ? 0 : 1
+ x <=> y
+ }.should == [true, true, nil, nil, nil, nil, nil, false, false]
+ end
+
+ it "compare values returned by block with 0" do
+ EnumerableSpecs::Numerous.new.sort { |n, m| -(n+m) * (n <=> m) }.should == [6, 5, 4, 3, 2, 1]
+ EnumerableSpecs::Numerous.new.sort { |n, m|
+ EnumerableSpecs::ComparableWithInteger.new(-(n+m) * (n <=> m))
+ }.should == [6, 5, 4, 3, 2, 1]
+ -> {
+ EnumerableSpecs::Numerous.new.sort { |n, m| (n <=> m).to_s }
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises an error if objects can't be compared" do
+ a=EnumerableSpecs::Numerous.new(EnumerableSpecs::Uncomparable.new, EnumerableSpecs::Uncomparable.new)
+ -> {a.sort}.should.raise(ArgumentError)
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.sort {|a, b| a.first <=> b.first}.should == [[1, 2], [3, 4, 5], [6, 7, 8, 9]]
+ end
+
+ it "doesn't raise an error if #to_a returns a frozen Array" do
+ EnumerableSpecs::Freezy.new.sort.should == [1,2]
+ end
+end
diff --git a/spec/ruby/core/enumerable/sum_spec.rb b/spec/ruby/core/enumerable/sum_spec.rb
new file mode 100644
index 0000000000..2eb74db6ac
--- /dev/null
+++ b/spec/ruby/core/enumerable/sum_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Enumerable#sum' do
+ before :each do
+ @enum = Object.new.to_enum
+ class << @enum
+ def each
+ yield 0
+ yield(-1)
+ yield 2
+ yield 2/3r
+ end
+ end
+ end
+
+ it 'returns amount of the elements with taking an argument as the initial value' do
+ @enum.sum(10).should == 35/3r
+ end
+
+ it 'gives 0 as a default argument' do
+ @enum.sum.should == 5/3r
+ end
+
+ context 'with a block' do
+ it 'transforms the elements' do
+ @enum.sum { |element| element * 2 }.should == 10/3r
+ end
+
+ it 'does not destructure array elements' do
+ class << @enum
+ def each
+ yield [1,2]
+ yield [3]
+ end
+ end
+
+ @enum.sum(&:last).should == 5
+ end
+ end
+
+ # https://bugs.ruby-lang.org/issues/12217
+ # https://github.com/ruby/ruby/blob/master/doc/ChangeLog/ChangeLog-2.4.0#L6208-L6214
+ it "uses Kahan's compensated summation algorithm for precise sum of float numbers" do
+ floats = [2.7800000000000002, 5.0, 2.5, 4.44, 3.89, 3.89, 4.44, 7.78, 5.0, 2.7800000000000002, 5.0, 2.5].to_enum
+ naive_sum = floats.reduce { |sum, e| sum + e }
+ naive_sum.should == 50.00000000000001
+ floats.sum.should == 50.0
+ end
+end
diff --git a/spec/ruby/core/enumerable/take_spec.rb b/spec/ruby/core/enumerable/take_spec.rb
new file mode 100644
index 0000000000..ca439b750d
--- /dev/null
+++ b/spec/ruby/core/enumerable/take_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/take'
+require_relative 'shared/value_packing'
+
+describe "Enumerable#take" do
+ it "requires an argument" do
+ ->{ EnumerableSpecs::Numerous.new.take}.should.raise(ArgumentError)
+ end
+
+ describe "when passed an argument" do
+ it_behaves_like :enumerable_take, :take
+ end
+
+ describe "value packing of source yields" do
+ before :each do
+ @take = -> e { e.take(1) }
+ end
+ it_behaves_like :enumerable_value_packing, nil
+ end
+end
diff --git a/spec/ruby/core/enumerable/take_while_spec.rb b/spec/ruby/core/enumerable/take_while_spec.rb
new file mode 100644
index 0000000000..918bfc897d
--- /dev/null
+++ b/spec/ruby/core/enumerable/take_while_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/enumerable_enumeratorized'
+
+describe "Enumerable#take_while" do
+ before :each do
+ @enum = EnumerableSpecs::Numerous.new(3, 2, 1, :go)
+ end
+
+ it "returns an Enumerator if no block given" do
+ @enum.take_while.should.instance_of?(Enumerator)
+ end
+
+ it "returns no/all elements for {true/false} block" do
+ @enum.take_while{true}.should == @enum.to_a
+ @enum.take_while{false}.should == []
+ end
+
+ it "accepts returns other than true/false" do
+ @enum.take_while{1}.should == @enum.to_a
+ @enum.take_while{nil}.should == []
+ end
+
+ it "passes elements to the block until the first false" do
+ a = []
+ @enum.take_while{|obj| (a << obj).size < 3}.should == [3, 2]
+ a.should == [3, 2, 1]
+ end
+
+ it "will only go through what's needed" do
+ enum = EnumerableSpecs::EachCounter.new(4, 3, 2, 1, :stop)
+ enum.take_while { |x|
+ break 42 if x == 3
+ true
+ }.should == 42
+ enum.times_yielded.should == 2
+ end
+
+ it "doesn't return self when it could" do
+ a = [1,2,3]
+ a.take_while{true}.should_not.equal?(a)
+ end
+
+ it "calls the block with initial args when yielded with multiple arguments" do
+ yields = []
+ EnumerableSpecs::YieldsMixed.new.take_while{ |v| yields << v }
+ yields.should == [1, [2], 3, 5, [8, 9], nil, []]
+ end
+
+ it_behaves_like :enumerable_enumeratorized_with_unknown_size, :take_while
+end
diff --git a/spec/ruby/core/enumerable/tally_spec.rb b/spec/ruby/core/enumerable/tally_spec.rb
new file mode 100644
index 0000000000..deef741407
--- /dev/null
+++ b/spec/ruby/core/enumerable/tally_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#tally" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns a hash with counts according to the value" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally.should == { 'foo' => 2, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "returns a hash without default" do
+ hash = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz').tally
+ hash.default_proc.should == nil
+ hash.default.should == nil
+ end
+
+ it "returns an empty hash for empty enumerables" do
+ EnumerableSpecs::Empty.new.tally.should == {}
+ end
+
+ it "counts values as gathered array when yielded with multiple arguments" do
+ EnumerableSpecs::YieldsMixed2.new.tally.should == EnumerableSpecs::YieldsMixed2.gathered_yields.group_by(&:itself).transform_values(&:size)
+ end
+
+ it "does not call given block" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally { |v| ScratchPad << v }
+ ScratchPad.recorded.should == []
+ end
+end
+
+describe "Enumerable#tally with a hash" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns a hash with counts according to the value" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally({ 'foo' => 1 }).should == { 'foo' => 3, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "returns the given hash" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ hash = { 'foo' => 1 }
+ enum.tally(hash).should.equal?(hash)
+ end
+
+ it "calls #to_hash to convert argument to Hash implicitly if passed not a Hash" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ object = Object.new
+ def object.to_hash; { 'foo' => 1 }; end
+ enum.tally(object).should == { 'foo' => 3, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "raises a FrozenError and does not update the given hash when the hash is frozen" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ hash = { 'foo' => 1 }.freeze
+ -> { enum.tally(hash) }.should.raise(FrozenError)
+ hash.should == { 'foo' => 1 }
+ end
+
+ it "raises a FrozenError even if enumerable is empty" do
+ enum = EnumerableSpecs::Numerous.new()
+ hash = { 'foo' => 1 }.freeze
+ -> { enum.tally(hash) }.should.raise(FrozenError)
+ end
+
+ it "does not call given block" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally({ 'foo' => 1 }) { |v| ScratchPad << v }
+ ScratchPad.recorded.should == []
+ end
+
+ it "ignores the default value" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally(Hash.new(100)).should == { 'foo' => 2, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "ignores the default proc" do
+ enum = EnumerableSpecs::Numerous.new('foo', 'bar', 'foo', 'baz')
+ enum.tally(Hash.new {100}).should == { 'foo' => 2, 'bar' => 1, 'baz' => 1}
+ end
+
+ it "needs the values counting each elements to be an integer" do
+ enum = EnumerableSpecs::Numerous.new('foo')
+ -> { enum.tally({ 'foo' => 'bar' }) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/enumerable/to_a_spec.rb b/spec/ruby/core/enumerable/to_a_spec.rb
new file mode 100644
index 0000000000..723f922574
--- /dev/null
+++ b/spec/ruby/core/enumerable/to_a_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/entries'
+
+describe "Enumerable#to_a" do
+ it_behaves_like :enumerable_entries, :to_a
+end
diff --git a/spec/ruby/core/enumerable/to_h_spec.rb b/spec/ruby/core/enumerable/to_h_spec.rb
new file mode 100644
index 0000000000..38847eccfb
--- /dev/null
+++ b/spec/ruby/core/enumerable/to_h_spec.rb
@@ -0,0 +1,96 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#to_h" do
+ it "converts empty enumerable to empty hash" do
+ enum = EnumerableSpecs::EachDefiner.new
+ enum.to_h.should == {}
+ end
+
+ it "converts yielded [key, value] pairs to a hash" do
+ enum = EnumerableSpecs::EachDefiner.new([:a, 1], [:b, 2])
+ enum.to_h.should == { a: 1, b: 2 }
+ end
+
+ it "uses the last value of a duplicated key" do
+ enum = EnumerableSpecs::EachDefiner.new([:a, 1], [:b, 2], [:a, 3])
+ enum.to_h.should == { a: 3, b: 2 }
+ end
+
+ it "calls #to_ary on contents" do
+ pair = mock('to_ary')
+ pair.should_receive(:to_ary).and_return([:b, 2])
+ enum = EnumerableSpecs::EachDefiner.new([:a, 1], pair)
+ enum.to_h.should == { a: 1, b: 2 }
+ end
+
+ it "forwards arguments to #each" do
+ enum = Object.new
+ def enum.each(*args)
+ yield(*args)
+ yield([:b, 2])
+ end
+ enum.extend Enumerable
+ enum.to_h(:a, 1).should == { a: 1, b: 2 }
+ end
+
+ it "raises TypeError if an element is not an array" do
+ enum = EnumerableSpecs::EachDefiner.new(:x)
+ -> { enum.to_h }.should.raise(TypeError)
+ end
+
+ it "raises ArgumentError if an element is not a [key, value] pair" do
+ enum = EnumerableSpecs::EachDefiner.new([:x])
+ -> { enum.to_h }.should.raise(ArgumentError)
+ end
+
+ context "with block" do
+ before do
+ @enum = EnumerableSpecs::EachDefiner.new(:a, :b)
+ end
+
+ it "converts [key, value] pairs returned by the block to a hash" do
+ @enum.to_h { |k| [k, k.to_s] }.should == { a: 'a', b: 'b' }
+ end
+
+ it "passes to a block each element as a single argument" do
+ enum_of_arrays = EnumerableSpecs::EachDefiner.new([:a, 1], [:b, 2])
+
+ ScratchPad.record []
+ enum_of_arrays.to_h { |*args| ScratchPad << args; [args[0], args[1]] }
+ ScratchPad.recorded.sort.should == [[[:a, 1]], [[:b, 2]]]
+ end
+
+ it "raises ArgumentError if block returns longer or shorter array" do
+ -> do
+ @enum.to_h { |k| [k, k.to_s, 1] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+
+ -> do
+ @enum.to_h { |k| [k] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+ end
+
+ it "raises TypeError if block returns something other than Array" do
+ -> do
+ @enum.to_h { |k| "not-array" }
+ end.should.raise(TypeError, /wrong element type String/)
+ end
+
+ it "coerces returned pair to Array with #to_ary" do
+ x = mock('x')
+ x.stub!(:to_ary).and_return([:b, 'b'])
+
+ @enum.to_h { |k| x }.should == { :b => 'b' }
+ end
+
+ it "does not coerce returned pair to Array with #to_a" do
+ x = mock('x')
+ x.stub!(:to_a).and_return([:b, 'b'])
+
+ -> do
+ @enum.to_h { |k| x }
+ end.should.raise(TypeError, /wrong element type MockObject/)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/to_set_spec.rb b/spec/ruby/core/enumerable/to_set_spec.rb
new file mode 100644
index 0000000000..7b04c72bce
--- /dev/null
+++ b/spec/ruby/core/enumerable/to_set_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#to_set" do
+ it "returns a new Set created from self" do
+ [1, 2, 3].to_set.should == Set[1, 2, 3]
+ {a: 1, b: 2}.to_set.should == Set[[:b, 2], [:a, 1]]
+ end
+
+ it "passes down passed blocks" do
+ [1, 2, 3].to_set { |x| x * x }.should == Set[1, 4, 9]
+ end
+
+ ruby_version_is "4.0"..."4.1" do
+ it "instantiates an object of provided as the first argument set class" do
+ set = nil
+ proc{set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass)}.should complain(/Enumerable#to_set/)
+ set.should.is_a?(EnumerableSpecs::SetSubclass)
+ set.to_a.sort.should == [1, 2, 3]
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "instantiates an object of provided as the first argument set class" do
+ set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass)
+ set.should.is_a?(EnumerableSpecs::SetSubclass)
+ set.to_a.sort.should == [1, 2, 3]
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/uniq_spec.rb b/spec/ruby/core/enumerable/uniq_spec.rb
new file mode 100644
index 0000000000..a1ed44796f
--- /dev/null
+++ b/spec/ruby/core/enumerable/uniq_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Enumerable#uniq' do
+ it 'returns an array that contains only unique elements' do
+ [0, 1, 2, 3].to_enum.uniq { |n| n.even? }.should == [0, 1]
+ end
+
+ it "uses eql? semantics" do
+ [1.0, 1].to_enum.uniq.should == [1.0, 1]
+ end
+
+ it "compares elements first with hash" do
+ x = mock('0')
+ x.should_receive(:hash).at_least(1).and_return(0)
+ y = mock('0')
+ y.should_receive(:hash).at_least(1).and_return(0)
+
+ [x, y].to_enum.uniq.should == [x, y]
+ end
+
+ it "does not compare elements with different hash codes via eql?" do
+ x = mock('0')
+ x.should_not_receive(:eql?)
+ y = mock('1')
+ y.should_not_receive(:eql?)
+
+ x.should_receive(:hash).at_least(1).and_return(0)
+ y.should_receive(:hash).at_least(1).and_return(1)
+
+ [x, y].to_enum.uniq.should == [x, y]
+ end
+
+ it "compares elements with matching hash codes with #eql?" do
+ a = Array.new(2) do
+ obj = mock('0')
+ obj.should_receive(:hash).at_least(1).and_return(0)
+
+ def obj.eql?(o)
+ false
+ end
+
+ obj
+ end
+
+ a.uniq.should == a
+
+ a = Array.new(2) do
+ obj = mock('0')
+ obj.should_receive(:hash).at_least(1).and_return(0)
+
+ def obj.eql?(o)
+ true
+ end
+
+ obj
+ end
+
+ a.to_enum.uniq.size.should == 1
+ end
+
+ context 'when yielded with multiple arguments' do
+ before :each do
+ @enum = Object.new.to_enum
+ class << @enum
+ def each
+ yield 0, 'foo'
+ yield 1, 'FOO'
+ yield 2, 'bar'
+ end
+ end
+ end
+
+ it 'returns all yield arguments as an array' do
+ @enum.uniq { |_, label| label.downcase }.should == [[0, 'foo'], [2, 'bar']]
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerable/zip_spec.rb b/spec/ruby/core/enumerable/zip_spec.rb
new file mode 100644
index 0000000000..c5f9a3e4d4
--- /dev/null
+++ b/spec/ruby/core/enumerable/zip_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerable#zip" do
+
+ it "combines each element of the receiver with the element of the same index in arrays given as arguments" do
+ EnumerableSpecs::Numerous.new(1,2,3).zip([4,5,6],[7,8,9]).should == [[1,4,7],[2,5,8],[3,6,9]]
+ EnumerableSpecs::Numerous.new(1,2,3).zip.should == [[1],[2],[3]]
+ end
+
+ it "passes each element of the result array to a block and return nil if a block is given" do
+ expected = [[1,4,7],[2,5,8],[3,6,9]]
+ EnumerableSpecs::Numerous.new(1,2,3).zip([4,5,6],[7,8,9]) do |result_component|
+ result_component.should == expected.shift
+ end.should == nil
+ expected.size.should == 0
+ end
+
+ it "fills resulting array with nils if an argument array is too short" do
+ EnumerableSpecs::Numerous.new(1,2,3).zip([4,5,6], [7,8]).should == [[1,4,7],[2,5,8],[3,6,nil]]
+ end
+
+ it "converts arguments to arrays using #to_ary" do
+ convertible = EnumerableSpecs::ArrayConvertible.new(4,5,6)
+ EnumerableSpecs::Numerous.new(1,2,3).zip(convertible).should == [[1,4],[2,5],[3,6]]
+ convertible.called.should == :to_ary
+ end
+
+ it "converts arguments to enums using #to_enum" do
+ convertible = EnumerableSpecs::EnumConvertible.new(4..6)
+ EnumerableSpecs::Numerous.new(1,2,3).zip(convertible).should == [[1,4],[2,5],[3,6]]
+ convertible.called.should == :to_enum
+ convertible.sym.should == :each
+ end
+
+ it "gathers whole arrays as elements when each yields multiple" do
+ multi = EnumerableSpecs::YieldsMulti.new
+ multi.zip(multi).should == [[[1, 2], [1, 2]], [[3, 4, 5], [3, 4, 5]], [[6, 7, 8, 9], [6, 7, 8, 9]]]
+ end
+
+ it "raises TypeError when some argument isn't Array and doesn't respond to #to_ary and #to_enum" do
+ -> { EnumerableSpecs::Numerous.new(1,2,3).zip(Object.new) }.should.raise(TypeError, "wrong argument type Object (must respond to :each)")
+ -> { EnumerableSpecs::Numerous.new(1,2,3).zip(1) }.should.raise(TypeError, "wrong argument type Integer (must respond to :each)")
+ -> { EnumerableSpecs::Numerous.new(1,2,3).zip(true) }.should.raise(TypeError, "wrong argument type TrueClass (must respond to :each)")
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb
new file mode 100644
index 0000000000..bd243fa0b5
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/begin_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#begin" do
+ it "returns the begin of the sequence" do
+ 1.step(10).begin.should == 1
+ (1..10).step.begin.should == 1
+ (1...10).step.begin.should == 1
+ end
+
+ context "with beginless" do
+ it "returns nil as begin of the sequence" do
+ (..10).step(1).begin.should == nil
+ (...10).step(1).begin.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb
new file mode 100644
index 0000000000..0a83019d49
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/each_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#each" do
+ before :each do
+ ScratchPad.record []
+ @seq = 1.step(10, 4)
+ end
+
+ it "calls given block on each item of the sequence" do
+ @seq.each { |item| ScratchPad << item }
+ ScratchPad.recorded.should == [1, 5, 9]
+ end
+
+ it "returns self" do
+ @seq.each { |item| }.should.equal?(@seq)
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb
new file mode 100644
index 0000000000..05429cac3e
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/end_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#end" do
+ it "returns the end of the sequence" do
+ 1.step(10).end.should == 10
+ (1..10).step.end.should == 10
+ (1...10).step(17).end.should == 10
+ end
+
+ context "with endless" do
+ it "returns nil as end of the sequence" do
+ (1..).step(1).end.should == nil
+ (1...).step(1).end.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb
new file mode 100644
index 0000000000..77eed02d8b
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/eq_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#==" do
+ it "returns true if begin, end, step and exclude_end? are equal" do
+ 1.step(10).should == 1.step(10)
+ 1.step(10, 5).should == 1.step(10, 5)
+
+ (1..10).step.should == (1..10).step
+ (1...10).step(8).should == (1...10).step(8)
+
+ # both have exclude_end? == false
+ (1..10).step(100).should == 1.step(10, 100)
+
+ ((1..10).step == (1..11).step).should == false
+ ((1..10).step == (1...10).step).should == false
+ ((1..10).step == (1..10).step(2)).should == false
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb
new file mode 100644
index 0000000000..021fe7d90f
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/exclude_end_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#exclude_end?" do
+ context "when created using Numeric#step" do
+ it "always returns false" do
+ 1.step(10).should_not.exclude_end?
+ 10.step(1).should_not.exclude_end?
+ end
+ end
+
+ context "when created using Range#step" do
+ it "mirrors range.exclude_end?" do
+ (1...10).step.should.exclude_end?
+ (1..10).step.should_not.exclude_end?
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb
new file mode 100644
index 0000000000..ccd02be020
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/first_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#first" do
+ it "returns the first element of the sequence" do
+ 1.step(10).first.should == 1
+ (1..10).step.first.should == 1
+ (1...10).step.first.should == 1
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb
new file mode 100644
index 0000000000..a18c554fb3
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/hash_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#hash" do
+ it "is based on begin, end, step and exclude_end?" do
+ 1.step(10).hash.should.instance_of?(Integer)
+
+ 1.step(10).hash.should == 1.step(10).hash
+ 1.step(10, 5).hash.should == 1.step(10, 5).hash
+
+ (1..10).step.hash.should == (1..10).step.hash
+ (1...10).step(8).hash.should == (1...10).step(8).hash
+
+ # both have exclude_end? == false
+ (1..10).step(100).hash.should == 1.step(10, 100).hash
+
+ ((1..10).step.hash == (1..11).step.hash).should == false
+ ((1..10).step.hash == (1...10).step.hash).should == false
+ ((1..10).step.hash == (1..10).step(2).hash).should == false
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb
new file mode 100644
index 0000000000..b73b49d272
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/inspect_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#inspect" do
+ context 'when Numeric#step is used' do
+ it "returns '(begin.step(end{, step}))'" do
+ 1.step(10).inspect.should == "(1.step(10))"
+ 1.step(10, 3).inspect.should == "(1.step(10, 3))"
+ end
+ end
+
+ context 'when Range#step is used' do
+ it "returns '((range).step{(step)})'" do
+ (1..10).step.inspect.should == "((1..10).step)"
+ (1..10).step(3).inspect.should == "((1..10).step(3))"
+
+ (1...10).step.inspect.should == "((1...10).step)"
+ (1...10).step(3).inspect.should == "((1...10).step(3))"
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb
new file mode 100644
index 0000000000..31f982b7c4
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/last_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#last" do
+ it "returns the last element of the sequence" do
+ 1.step(10).last.should == 10
+ (1..10).step.last.should == 10
+ (1...10).step(4).last.should == 9
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb
new file mode 100644
index 0000000000..1bd2f7f0f7
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/new_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence.new" do
+ it "is not defined" do
+ -> {
+ Enumerator::ArithmeticSequence.new
+ }.should.raise(NoMethodError)
+ end
+end
+
+describe "Enumerator::ArithmeticSequence.allocate" do
+ it "is not defined" do
+ -> {
+ Enumerator::ArithmeticSequence.allocate
+ }.should.raise(TypeError, 'allocator undefined for Enumerator::ArithmeticSequence')
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb
new file mode 100644
index 0000000000..7e03edd961
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/size_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#size" do
+ context "for finite sequence" do
+ it "returns the number of elements in this arithmetic sequence" do
+ 1.step(10).size.should == 10
+ (1...10).step.size.should == 9
+ end
+ end
+
+ context "for infinite sequence" do
+ it "returns Infinity" do
+ 1.step(Float::INFINITY).size.should == Float::INFINITY
+ (1..Float::INFINITY).step.size.should == Float::INFINITY
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb b/spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb
new file mode 100644
index 0000000000..c1f2d9173f
--- /dev/null
+++ b/spec/ruby/core/enumerator/arithmetic_sequence/step_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::ArithmeticSequence#step" do
+ it "returns the original value given to step method" do
+ (1..10).step.step.should == 1
+ (1..10).step(3).step.should == 3
+
+ 1.step(10).step.should == 1
+ 1.step(10, 3).step.should == 3
+ end
+end
diff --git a/spec/ruby/core/enumerator/chain/each_spec.rb b/spec/ruby/core/enumerator/chain/each_spec.rb
new file mode 100644
index 0000000000..cc93cbac60
--- /dev/null
+++ b/spec/ruby/core/enumerator/chain/each_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../../spec_helper'
+require_relative '../../enumerable/fixtures/classes'
+
+describe "Enumerator::Chain#each" do
+ it "calls each on its constituents as needed" do
+ a = EnumerableSpecs::EachCounter.new(:a, :b)
+ b = EnumerableSpecs::EachCounter.new(:c, :d)
+
+ ScratchPad.record []
+ Enumerator::Chain.new(a, b).each do |elem|
+ ScratchPad << elem << b.times_yielded
+ end
+ ScratchPad.recorded.should == [:a, 0, :b, 0, :c, 1, :d, 2]
+ end
+end
diff --git a/spec/ruby/core/enumerator/chain/initialize_spec.rb b/spec/ruby/core/enumerator/chain/initialize_spec.rb
new file mode 100644
index 0000000000..1df1dec5f8
--- /dev/null
+++ b/spec/ruby/core/enumerator/chain/initialize_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Chain#initialize" do
+ before :each do
+ @uninitialized = Enumerator::Chain.allocate
+ end
+
+ it "is a private method" do
+ Enumerator::Chain.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "returns self" do
+ @uninitialized.send(:initialize).should.equal?(@uninitialized)
+ end
+
+ it "accepts many arguments" do
+ @uninitialized.send(:initialize, 0..1, 2..3, 4..5).should.equal?(@uninitialized)
+ end
+
+ it "accepts arguments that are not Enumerable nor responding to :each" do
+ @uninitialized.send(:initialize, Object.new).should.equal?(@uninitialized)
+ end
+
+ describe "on frozen instance" do
+ it "raises a FrozenError" do
+ -> {
+ @uninitialized.freeze.send(:initialize)
+ }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/chain/inspect_spec.rb b/spec/ruby/core/enumerator/chain/inspect_spec.rb
new file mode 100644
index 0000000000..9b5a442b75
--- /dev/null
+++ b/spec/ruby/core/enumerator/chain/inspect_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Chain#inspect" do
+ it "shows a representation of the Enumerator" do
+ Enumerator::Chain.new.inspect.should == "#<Enumerator::Chain: []>"
+ Enumerator::Chain.new(1..2, 3..4).inspect.should == "#<Enumerator::Chain: [1..2, 3..4]>"
+ end
+
+ it "calls inspect on its chain elements" do
+ obj = mock('inspect')
+ obj.should_receive(:inspect).and_return('some desc')
+ Enumerator::Chain.new(obj).inspect.should == "#<Enumerator::Chain: [some desc]>"
+ end
+
+ it "returns a not initialized representation if #initialized is not called yet" do
+ Enumerator::Chain.allocate.inspect.should == "#<Enumerator::Chain: uninitialized>"
+ end
+end
diff --git a/spec/ruby/core/enumerator/chain/rewind_spec.rb b/spec/ruby/core/enumerator/chain/rewind_spec.rb
new file mode 100644
index 0000000000..4525b82f7b
--- /dev/null
+++ b/spec/ruby/core/enumerator/chain/rewind_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Chain#rewind" do
+ before(:each) do
+ @obj = mock('obj')
+ @obj.should_receive(:each).any_number_of_times.and_yield
+ @second = mock('obj')
+ @second.should_receive(:each).any_number_of_times.and_yield
+ @enum = Enumerator::Chain.new(@obj, @second)
+ end
+
+ it "returns self" do
+ @enum.rewind.should.equal? @enum
+ end
+
+ it "does nothing if receiver has not been iterated" do
+ @obj.should_not_receive(:rewind)
+ @obj.respond_to?(:rewind).should == true # sanity check
+ @enum.rewind
+ end
+
+ it "does nothing on objects that don't respond_to rewind" do
+ @obj.respond_to?(:rewind).should == false # sanity check
+ @enum.each {}
+ @enum.rewind
+ end
+
+ it "calls_rewind its objects" do
+ @obj.should_receive(:rewind)
+ @enum.each {}
+ @enum.rewind
+ end
+
+ it "calls_rewind in reverse order" do
+ @obj.should_not_receive(:rewind)
+ @second.should_receive(:rewind).and_raise(RuntimeError)
+ @enum.each {}
+ -> { @enum.rewind }.should.raise(RuntimeError)
+ end
+
+ it "calls rewind only for objects that have actually been iterated on" do
+ @obj = mock('obj')
+ @obj.should_receive(:each).any_number_of_times.and_raise(RuntimeError)
+ @enum = Enumerator::Chain.new(@obj, @second)
+
+ @obj.should_receive(:rewind)
+ @second.should_not_receive(:rewind)
+ -> { @enum.each {} }.should.raise(RuntimeError)
+ @enum.rewind
+ end
+end
diff --git a/spec/ruby/core/enumerator/chain/size_spec.rb b/spec/ruby/core/enumerator/chain/size_spec.rb
new file mode 100644
index 0000000000..d85b88ee8b
--- /dev/null
+++ b/spec/ruby/core/enumerator/chain/size_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../../spec_helper'
+require_relative '../../enumerable/fixtures/classes'
+
+describe "Enumerator::Chain#size" do
+ it "returns the sum of the sizes of the elements" do
+ a = mock('size')
+ a.should_receive(:size).and_return(40)
+ Enumerator::Chain.new(a, [:a, :b]).size.should == 42
+ end
+
+ it "returns nil or Infinity for the first element of such a size" do
+ [nil, Float::INFINITY].each do |special|
+ a = mock('size')
+ a.should_receive(:size).and_return(40)
+ b = mock('special')
+ b.should_receive(:size).and_return(special)
+ c = mock('not called')
+ c.should_not_receive(:size)
+ Enumerator::Chain.new(a, b, c).size.should == special
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/each_spec.rb b/spec/ruby/core/enumerator/each_spec.rb
new file mode 100644
index 0000000000..03be53fe05
--- /dev/null
+++ b/spec/ruby/core/enumerator/each_spec.rb
@@ -0,0 +1,104 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#each" do
+ before :each do
+ object_each_with_arguments = Object.new
+ def object_each_with_arguments.each_with_arguments(arg, *args)
+ yield arg, *args
+ :method_returned
+ end
+
+ @enum_with_arguments = object_each_with_arguments.to_enum(:each_with_arguments, :arg0, :arg1, :arg2)
+
+ @enum_with_yielder = Enumerator.new { |y| y.yield :ok }
+ end
+
+ it "yields each element of self to the given block" do
+ acc = []
+ [1, 2, 3].to_enum.each { |e| acc << e }
+ acc.should == [1, 2, 3]
+ end
+
+ it "calls #each on the object given in the constructor by default" do
+ each = mock('each')
+ each.should_receive(:each)
+ each.to_enum.each { |e| e }
+ end
+
+ it "calls #each on the underlying object until it's exhausted" do
+ each = mock('each')
+ each.should_receive(:each).and_yield(1).and_yield(2).and_yield(3)
+ acc = []
+ each.to_enum.each { |e| acc << e }
+ acc.should == [1, 2, 3]
+ end
+
+ it "calls the method given in the constructor instead of #each" do
+ each = mock('peach')
+ each.should_receive(:peach)
+ each.to_enum(:peach).each { |e| e }
+ end
+
+ it "calls the method given in the constructor until it's exhausted" do
+ each = mock('peach')
+ each.should_receive(:peach).and_yield(1).and_yield(2).and_yield(3)
+ acc = []
+ each.to_enum(:peach).each { |e| acc << e }
+ acc.should == [1, 2, 3]
+ end
+
+ it "raises a NoMethodError if the object doesn't respond to #each" do
+ enum = Object.new.to_enum
+ -> do
+ enum.each { |e| e }
+ end.should.raise(NoMethodError)
+ end
+
+ it "returns self if not given arguments and not given a block" do
+ @enum_with_arguments.each.should.equal?(@enum_with_arguments)
+
+ @enum_with_yielder.each.should.equal?(@enum_with_yielder)
+ end
+
+ it "returns the same value from receiver.each if block is given" do
+ @enum_with_arguments.each {}.should.equal?(:method_returned)
+ end
+
+ it "passes given arguments at initialized to receiver.each" do
+ @enum_with_arguments.each.to_a.should == [[:arg0, :arg1, :arg2]]
+ end
+
+ it "requires multiple arguments" do
+ Enumerator.instance_method(:each).arity.should < 0
+ end
+
+ it "appends given arguments to receiver.each" do
+ @enum_with_arguments.each(:each0, :each1).to_a.should == [[:arg0, :arg1, :arg2, :each0, :each1]]
+ @enum_with_arguments.each(:each2, :each3).to_a.should == [[:arg0, :arg1, :arg2, :each2, :each3]]
+ end
+
+ it "returns the same value from receiver.each if block and arguments are given" do
+ @enum_with_arguments.each(:each1, :each2) {}.should.equal?(:method_returned)
+ end
+
+ it "returns new Enumerator if given arguments but not given a block" do
+ ret = @enum_with_arguments.each 1
+ ret.should.instance_of?(Enumerator)
+ ret.should_not.equal?(@enum_with_arguments)
+ end
+
+ it "does not destructure yielded array values when chaining each.map" do
+ result = [[[1]]].each.map { |a, b| [a, b] }
+ result.should == [[[1], nil]]
+ end
+
+ it "preserves array values yielded from the enumerator" do
+ result = [[1, 2]].each.map { |a| a }
+ result.should == [[1, 2]]
+ end
+
+ it "allows destructuring to occur in the block, not the enumerator" do
+ result = [[1, 2]].each.map { |a, b| a }
+ result.should == [1]
+ end
+end
diff --git a/spec/ruby/core/enumerator/each_with_index_spec.rb b/spec/ruby/core/enumerator/each_with_index_spec.rb
new file mode 100644
index 0000000000..0630b7045e
--- /dev/null
+++ b/spec/ruby/core/enumerator/each_with_index_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'shared/with_index'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Enumerator#each_with_index" do
+ it_behaves_like :enum_with_index, :each_with_index
+ it_behaves_like :enumeratorized_with_origin_size, :each_with_index, [1,2,3].select
+
+ it "returns a new Enumerator when no block is given" do
+ enum1 = [1,2,3].select
+ enum2 = enum1.each_with_index
+ enum2.should.instance_of?(Enumerator)
+ enum1.should_not == enum2
+ end
+
+ it "raises an ArgumentError if passed extra arguments" do
+ -> do
+ [1].to_enum.each_with_index(:glark)
+ end.should.raise(ArgumentError)
+ end
+
+ it "passes on the given block's return value" do
+ arr = [1,2,3]
+ arr.delete_if.each_with_index { |a,b| false }
+ arr.should == [1,2,3]
+ end
+
+ it "returns the iterator's return value" do
+ [1,2,3].select.each_with_index { |a,b| false }.should == []
+ [1,2,3].select.each_with_index { |a,b| true }.should == [1,2,3]
+ end
+
+ it "returns the correct value if chained with itself" do
+ [:a].each_with_index.each_with_index.to_a.should == [[[:a,0],0]]
+ end
+end
diff --git a/spec/ruby/core/enumerator/each_with_object_spec.rb b/spec/ruby/core/enumerator/each_with_object_spec.rb
new file mode 100644
index 0000000000..84a45ae89d
--- /dev/null
+++ b/spec/ruby/core/enumerator/each_with_object_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/with_object'
+
+describe "Enumerator#each_with_object" do
+ it_behaves_like :enum_with_object, :each_with_object
+end
diff --git a/spec/ruby/core/enumerator/enum_for_spec.rb b/spec/ruby/core/enumerator/enum_for_spec.rb
new file mode 100644
index 0000000000..fbdf98545a
--- /dev/null
+++ b/spec/ruby/core/enumerator/enum_for_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/enum_for'
+
+describe "Enumerator#enum_for" do
+ it_behaves_like :enum_for, :enum_for
+end
diff --git a/spec/ruby/core/enumerator/enumerator_spec.rb b/spec/ruby/core/enumerator/enumerator_spec.rb
new file mode 100644
index 0000000000..7a263336cb
--- /dev/null
+++ b/spec/ruby/core/enumerator/enumerator_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator" do
+ it "includes Enumerable" do
+ Enumerator.include?(Enumerable).should == true
+ end
+end
diff --git a/spec/ruby/core/enumerator/feed_spec.rb b/spec/ruby/core/enumerator/feed_spec.rb
new file mode 100644
index 0000000000..781947a8c7
--- /dev/null
+++ b/spec/ruby/core/enumerator/feed_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Enumerator#feed" do
+ before :each do
+ ScratchPad.record []
+ @enum = EnumeratorSpecs::Feed.new.to_enum(:each)
+ end
+
+ it "sets the future return value of yield if called before advancing the iterator" do
+ @enum.feed :a
+ @enum.next
+ @enum.next
+ @enum.next
+ ScratchPad.recorded.should == [:a, nil]
+ end
+
+ it "causes yield to return the value if called during iteration" do
+ @enum.next
+ @enum.feed :a
+ @enum.next
+ @enum.next
+ ScratchPad.recorded.should == [:a, nil]
+ end
+
+ it "can be called for each iteration" do
+ @enum.next
+ @enum.feed :a
+ @enum.next
+ @enum.feed :b
+ @enum.next
+ ScratchPad.recorded.should == [:a, :b]
+ end
+
+ it "returns nil" do
+ @enum.feed(:a).should == nil
+ end
+
+ it "raises a TypeError if called more than once without advancing the enumerator" do
+ @enum.feed :a
+ @enum.next
+ -> { @enum.feed :b }.should.raise(TypeError)
+ end
+
+ it "sets the return value of Yielder#yield" do
+ enum = Enumerator.new { |y| ScratchPad << y.yield }
+ enum.next
+ enum.feed :a
+ -> { enum.next }.should.raise(StopIteration)
+ ScratchPad.recorded.should == [:a]
+ end
+end
diff --git a/spec/ruby/core/enumerator/first_spec.rb b/spec/ruby/core/enumerator/first_spec.rb
new file mode 100644
index 0000000000..458080bb31
--- /dev/null
+++ b/spec/ruby/core/enumerator/first_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#first" do
+ it "returns arrays correctly when calling #first (2376)" do
+ Enumerator.new {|y| y << [42] }.first.should == [42]
+ end
+end
diff --git a/spec/ruby/core/enumerator/fixtures/classes.rb b/spec/ruby/core/enumerator/fixtures/classes.rb
new file mode 100644
index 0000000000..6f285b8efa
--- /dev/null
+++ b/spec/ruby/core/enumerator/fixtures/classes.rb
@@ -0,0 +1,15 @@
+module EnumSpecs
+ class Numerous
+
+ include Enumerable
+
+ def initialize(*list)
+ @list = list.empty? ? [2, 5, 3, 6, 1, 4] : list
+ end
+
+ def each
+ @list.each { |i| yield i }
+ end
+ end
+
+end
diff --git a/spec/ruby/core/enumerator/fixtures/common.rb b/spec/ruby/core/enumerator/fixtures/common.rb
new file mode 100644
index 0000000000..e332e3195b
--- /dev/null
+++ b/spec/ruby/core/enumerator/fixtures/common.rb
@@ -0,0 +1,9 @@
+module EnumeratorSpecs
+ class Feed
+ def each
+ ScratchPad << yield
+ ScratchPad << yield
+ ScratchPad << yield
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/initialize_spec.rb b/spec/ruby/core/enumerator/initialize_spec.rb
new file mode 100644
index 0000000000..9929494b5a
--- /dev/null
+++ b/spec/ruby/core/enumerator/initialize_spec.rb
@@ -0,0 +1,57 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../spec_helper'
+
+describe "Enumerator#initialize" do
+ before :each do
+ @uninitialized = Enumerator.allocate
+ end
+
+ it "is a private method" do
+ Enumerator.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "returns self when given a block" do
+ @uninitialized.send(:initialize) {}.should.equal?(@uninitialized)
+ end
+
+ # Maybe spec should be broken up?
+ it "accepts a block" do
+ @uninitialized.send(:initialize) do |yielder|
+ r = yielder.yield 3
+ yielder << r << 2 << 1
+ end
+ @uninitialized.should.instance_of?(Enumerator)
+ r = []
+ @uninitialized.each{|x| r << x; x * 2}
+ r.should == [3, 6, 2, 1]
+ end
+
+ it "sets size to nil if size is not given" do
+ @uninitialized.send(:initialize) {}.size.should == nil
+ end
+
+ it "sets size to nil if the given size is nil" do
+ @uninitialized.send(:initialize, nil) {}.size.should == nil
+ end
+
+ it "sets size to the given size if the given size is Float::INFINITY" do
+ @uninitialized.send(:initialize, Float::INFINITY) {}.size.should.equal?(Float::INFINITY)
+ end
+
+ it "sets size to the given size if the given size is an Integer" do
+ @uninitialized.send(:initialize, 100) {}.size.should == 100
+ end
+
+ it "sets size to the given size if the given size is a Proc" do
+ @uninitialized.send(:initialize, -> { 200 }) {}.size.should == 200
+ end
+
+ describe "on frozen instance" do
+ it "raises a FrozenError" do
+ -> {
+ @uninitialized.freeze.send(:initialize) {}
+ }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/inspect_spec.rb b/spec/ruby/core/enumerator/inspect_spec.rb
new file mode 100644
index 0000000000..7e97864246
--- /dev/null
+++ b/spec/ruby/core/enumerator/inspect_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#inspect" do
+ describe "shows a representation of the Enumerator" do
+ it "including receiver and method" do
+ (1..3).each.inspect.should == "#<Enumerator: 1..3:each>"
+ end
+
+ it "including receiver and method and arguments" do
+ (1..3).each_slice(2).inspect.should == "#<Enumerator: 1..3:each_slice(2)>"
+ end
+
+ it "including the nested Enumerator" do
+ (1..3).each.each_slice(2).inspect.should == "#<Enumerator: #<Enumerator: 1..3:each>:each_slice(2)>"
+ end
+ end
+
+ it "returns a not initialized representation if #initialized is not called yet" do
+ Enumerator.allocate.inspect.should == "#<Enumerator: uninitialized>"
+ Enumerator::Lazy.allocate.inspect.should == "#<Enumerator::Lazy: uninitialized>"
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/chunk_spec.rb b/spec/ruby/core/enumerator/lazy/chunk_spec.rb
new file mode 100644
index 0000000000..d0179d32b6
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/chunk_spec.rb
@@ -0,0 +1,67 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#chunk" do
+
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.chunk {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.chunk { |v| v }.size.should == nil
+ end
+
+ it "returns an Enumerator if called without a block" do
+ chunk = @yieldsmixed.chunk
+ chunk.should.instance_of?(Enumerator::Lazy)
+
+ res = chunk.each { |v| true }.force
+ res.should == [[true, EnumeratorLazySpecs::YieldsMixed.gathered_yields]]
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ first_two = (0..Float::INFINITY).lazy.chunk { |n| n.even? }.first(2)
+ first_two.should == [[true, [0]], [false, [1]]]
+ end
+ end
+
+ it "calls the block with gathered values when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.chunk { |v| yields << v; true }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(20).chunk { |v| v }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ remains_lazy = (0..Float::INFINITY).lazy.chunk { |n| n }
+ remains_lazy.chunk { |n| n }.first(2).size.should == 2
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.chunk { |n| n.even? }.first(100).should ==
+ s.first(100).chunk { |n| n.even? }.to_a
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/chunk_while_spec.rb b/spec/ruby/core/enumerator/lazy/chunk_while_spec.rb
new file mode 100644
index 0000000000..edba8e1363
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/chunk_while_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy#chunk_while" do
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.chunk_while { |a, b| false }.first(100).should ==
+ s.first(100).chunk_while { |a, b| false }.to_a
+ end
+
+ it "should return a lazy enumerator" do
+ s = 0..Float::INFINITY
+ s.lazy.chunk_while { |a, b| false }.should.is_a?(Enumerator::Lazy)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb b/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb
new file mode 100644
index 0000000000..8765bb2190
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/collect_concat_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/collect_concat'
+
+describe "Enumerator::Lazy#collect_concat" do
+ it_behaves_like :enumerator_lazy_collect_concat, :collect_concat
+end
diff --git a/spec/ruby/core/enumerator/lazy/collect_spec.rb b/spec/ruby/core/enumerator/lazy/collect_spec.rb
new file mode 100644
index 0000000000..14b79ce16d
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/collect_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/collect'
+
+describe "Enumerator::Lazy#collect" do
+ it_behaves_like :enumerator_lazy_collect, :collect
+end
diff --git a/spec/ruby/core/enumerator/lazy/compact_spec.rb b/spec/ruby/core/enumerator/lazy/compact_spec.rb
new file mode 100644
index 0000000000..7305e1c9c4
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/compact_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#compact" do
+ it 'returns array without nil elements' do
+ arr = [1, nil, 3, false, 5].to_enum.lazy.compact
+ arr.should.instance_of?(Enumerator::Lazy)
+ arr.force.should == [1, 3, false, 5]
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.compact.size.should == nil
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/drop_spec.rb b/spec/ruby/core/enumerator/lazy/drop_spec.rb
new file mode 100644
index 0000000000..95ac7e9ecc
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/drop_spec.rb
@@ -0,0 +1,58 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#drop" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.drop(1)
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets difference of given count with old size to new size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.drop(20).size.should == 80
+ Enumerator::Lazy.new(Object.new, 100) {}.drop(200).size.should == 0
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.drop(2).first(2).should == [2, 3]
+
+ @eventsmixed.drop(0).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ describe "on a nested Lazy" do
+ it "sets difference of given count with old size to new size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.drop(20).drop(50).size.should == 30
+ Enumerator::Lazy.new(Object.new, 100) {}.drop(50).drop(20).size.should == 30
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.drop(2).drop(2).first(2).should == [4, 5]
+
+ @eventsmixed.drop(0).drop(0).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.drop(100).first(100).should ==
+ s.first(200).drop(100)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/drop_while_spec.rb b/spec/ruby/core/enumerator/lazy/drop_while_spec.rb
new file mode 100644
index 0000000000..65f3007dec
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/drop_while_spec.rb
@@ -0,0 +1,66 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#drop_while" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.drop_while {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.drop_while { |v| v }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.drop_while { |n| n < 5 }.first(2).should == [5, 6]
+
+ @eventsmixed.drop_while { false }.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with initial values when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.drop_while { |v| yields << v; true }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields
+ end
+
+ it "raises an ArgumentError when not given a block" do
+ -> { @yieldsmixed.drop_while }.should.raise(ArgumentError)
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(20).drop_while { |v| v }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.drop_while { |n| n < 5 }.drop_while { |n| n.odd? }.first(2).should == [6, 7]
+
+ @eventsmixed.drop_while { false }.drop_while { false }.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.drop_while { |n| n < 100 }.first(100).should ==
+ s.first(200).drop_while { |n| n < 100 }
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/eager_spec.rb b/spec/ruby/core/enumerator/lazy/eager_spec.rb
new file mode 100644
index 0000000000..592da4fd8c
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/eager_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy#eager" do
+ it "returns a non-lazy Enumerator converted from the lazy enumerator" do
+ enum = [1, 2, 3].lazy
+
+ enum.class.should == Enumerator::Lazy
+ enum.eager.class.should == Enumerator
+ end
+
+ it "does not enumerate an enumerator" do
+ ScratchPad.record []
+
+ sequence = [1, 2, 3]
+ enum_lazy = Enumerator::Lazy.new(sequence) do |yielder, value|
+ yielder << value
+ ScratchPad << value
+ end
+
+ ScratchPad.recorded.should == []
+ enum = enum_lazy.eager
+ ScratchPad.recorded.should == []
+
+ enum.map { |i| i }.should == [1, 2, 3]
+ ScratchPad.recorded.should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/enum_for_spec.rb b/spec/ruby/core/enumerator/lazy/enum_for_spec.rb
new file mode 100644
index 0000000000..7e7783f6f1
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/enum_for_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/to_enum'
+
+describe "Enumerator::Lazy#enum_for" do
+ it_behaves_like :enumerator_lazy_to_enum, :enum_for
+end
diff --git a/spec/ruby/core/enumerator/lazy/filter_map_spec.rb b/spec/ruby/core/enumerator/lazy/filter_map_spec.rb
new file mode 100644
index 0000000000..99dd59cebe
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/filter_map_spec.rb
@@ -0,0 +1,14 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#filter_map" do
+ it "maps only truthy results" do
+ (1..Float::INFINITY).lazy.filter_map { |i| i if i.odd? }.first(4).should == [1, 3, 5, 7]
+ end
+
+ it "does not map false results" do
+ (1..Float::INFINITY).lazy.filter_map { |i| i.odd? ? i : false }.first(4).should == [1, 3, 5, 7]
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/filter_spec.rb b/spec/ruby/core/enumerator/lazy/filter_spec.rb
new file mode 100644
index 0000000000..43128241e0
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/filter_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/select'
+
+describe "Enumerator::Lazy#filter" do
+ it_behaves_like :enumerator_lazy_select, :filter
+end
diff --git a/spec/ruby/core/enumerator/lazy/find_all_spec.rb b/spec/ruby/core/enumerator/lazy/find_all_spec.rb
new file mode 100644
index 0000000000..8b05c53803
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/find_all_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/select'
+
+describe "Enumerator::Lazy#find_all" do
+ it_behaves_like :enumerator_lazy_select, :find_all
+end
diff --git a/spec/ruby/core/enumerator/lazy/fixtures/classes.rb b/spec/ruby/core/enumerator/lazy/fixtures/classes.rb
new file mode 100644
index 0000000000..e35592ba1c
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/fixtures/classes.rb
@@ -0,0 +1,54 @@
+# -*- encoding: us-ascii -*-
+
+module EnumeratorLazySpecs
+ class SpecificError < Exception; end
+
+ class YieldsMixed
+ def self.initial_yields
+ [nil, 0, 0, 0, 0, nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]]
+ end
+
+ def self.gathered_yields
+ [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, :default_arg, [], [], [0], [0, 1], [0, 1, 2]]
+ end
+
+ def self.gathered_non_array_yields
+ [nil, 0, nil, :default_arg]
+ end
+
+ def self.gathered_yields_with_args(arg, *args)
+ [nil, 0, [0, 1], [0, 1, 2], [0, 1, 2], nil, arg, args, [], [0], [0, 1], [0, 1, 2]]
+ end
+
+ def each(arg=:default_arg, *args)
+ yield
+ yield 0
+ yield 0, 1
+ yield 0, 1, 2
+ yield(*[0, 1, 2])
+ yield nil
+ yield arg
+ yield args
+ yield []
+ yield [0]
+ yield [0, 1]
+ yield [0, 1, 2]
+ end
+ end
+
+ class EventsMixed
+ def each
+ ScratchPad << :before_yield
+
+ yield 0
+
+ ScratchPad << :after_yield
+
+ raise SpecificError
+
+ ScratchPad << :after_error
+
+ :should_not_reach_here
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/flat_map_spec.rb b/spec/ruby/core/enumerator/lazy/flat_map_spec.rb
new file mode 100644
index 0000000000..5dcaa8bfa1
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/flat_map_spec.rb
@@ -0,0 +1,16 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/collect_concat'
+
+describe "Enumerator::Lazy#flat_map" do
+ it_behaves_like :enumerator_lazy_collect_concat, :flat_map
+
+ it "properly unwraps nested yields" do
+ s = Enumerator.new do |y| loop do y << [1, 2] end end
+
+ expected = s.take(3).flat_map { |x| x }.to_a
+ actual = s.lazy.take(3).flat_map{ |x| x }.force
+ actual.should == expected
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/force_spec.rb b/spec/ruby/core/enumerator/lazy/force_spec.rb
new file mode 100644
index 0000000000..a7fa029135
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/force_spec.rb
@@ -0,0 +1,36 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#force" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "passes given arguments to receiver.each" do
+ @yieldsmixed.force(:arg1, :arg2, :arg3).should ==
+ EnumeratorLazySpecs::YieldsMixed.gathered_yields_with_args(:arg1, :arg2, :arg3)
+ end
+
+ describe "on a nested Lazy" do
+ it "calls all block and returns an Array" do
+ (0..Float::INFINITY).lazy.map(&:succ).take(2).force.should == [1, 2]
+
+ @eventsmixed.take(1).map(&:succ).force.should == [1]
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.take(100).force.should ==
+ s.take(100)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/grep_spec.rb b/spec/ruby/core/enumerator/lazy/grep_spec.rb
new file mode 100644
index 0000000000..383f80a918
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/grep_spec.rb
@@ -0,0 +1,121 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#grep" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "requires an argument" do
+ Enumerator::Lazy.instance_method(:grep).arity.should == 1
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.grep(Object) {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+
+ ret = @yieldsmixed.grep(Object)
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.grep(Object) {}.size.should == nil
+ Enumerator::Lazy.new(Object.new, 100) {}.grep(Object).size.should == nil
+ end
+
+ it "sets $~ in the block" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].lazy.grep(/b/) { |e|
+ e.should == "abc"
+ $&.should == "b"
+ }.force
+
+ # Set by the failed match of "def"
+ $~.should == nil
+ end
+
+ it "sets $~ in the next block with each" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].lazy.grep(/b/).each { |e|
+ e.should == "abc"
+ $&.should == "b"
+ }
+
+ # Set by the failed match of "def"
+ $~.should == nil
+ end
+
+ it "sets $~ in the next block with map" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].lazy.grep(/b/).map { |e|
+ e.should == "abc"
+ $&.should == "b"
+ }.force
+
+ # Set by the failed match of "def"
+ $~.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times when not given a block" do
+ (0..Float::INFINITY).lazy.grep(Integer).first(3).should == [0, 1, 2]
+
+ @eventsmixed.grep(BasicObject).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "stops after specified times when given a block" do
+ (0..Float::INFINITY).lazy.grep(Integer, &:succ).first(3).should == [1, 2, 3]
+
+ @eventsmixed.grep(BasicObject) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with a gathered array when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.grep(BasicObject) { |v| yields << v }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields
+
+ @yieldsmixed.grep(BasicObject).force.should == yields
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.grep(Object) {}.size.should == nil
+ Enumerator::Lazy.new(Object.new, 100) {}.grep(Object).size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times when not given a block" do
+ (0..Float::INFINITY).lazy.grep(Integer).grep(Object).first(3).should == [0, 1, 2]
+
+ @eventsmixed.grep(BasicObject).grep(Object).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "stops after specified times when given a block" do
+ (0..Float::INFINITY).lazy.grep(Integer) { |n| n > 3 ? n : false }.grep(Integer) { |n| n.even? ? n : false }.first(3).should == [4, false, 6]
+
+ @eventsmixed.grep(BasicObject) {}.grep(Object) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.grep(Numeric).first(100).should ==
+ s.first(100).grep(Numeric)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/grep_v_spec.rb b/spec/ruby/core/enumerator/lazy/grep_v_spec.rb
new file mode 100644
index 0000000000..19c917f254
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/grep_v_spec.rb
@@ -0,0 +1,123 @@
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#grep_v" do
+ before(:each) do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after(:each) do
+ ScratchPad.clear
+ end
+
+ it "requires an argument" do
+ Enumerator::Lazy.instance_method(:grep_v).arity.should == 1
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.grep_v(Object) {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+
+ ret = @yieldsmixed.grep_v(Object)
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object) {}.size.should == nil
+ Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object).size.should == nil
+ end
+
+ it "sets $~ in the block" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].lazy.grep_v(/e/) { |e|
+ e.should == "abc"
+ $~.should == nil
+ }.force
+
+ # Set by the match of "def"
+ $&.should == "e"
+ end
+
+ it "sets $~ in the next block with each" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].lazy.grep_v(/e/).each { |e|
+ e.should == "abc"
+ $~.should == nil
+ }
+
+ # Set by the match of "def"
+ $&.should == "e"
+ end
+
+ it "sets $~ in the next block with map" do
+ "z" =~ /z/ # Reset $~
+ ["abc", "def"].lazy.grep_v(/e/).map { |e|
+ e.should == "abc"
+ $~.should == nil
+ }.force
+
+ # Set by the match of "def"
+ $&.should == "e"
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times when not given a block" do
+ (0..Float::INFINITY).lazy.grep_v(3..5).first(3).should == [0, 1, 2]
+
+ @eventsmixed.grep_v(Symbol).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "stops after specified times when given a block" do
+ (0..Float::INFINITY).lazy.grep_v(4..8, &:succ).first(3).should == [1, 2, 3]
+
+ @eventsmixed.grep_v(Symbol) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with a gathered array when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.grep_v(Array) { |v| yields << v }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_non_array_yields
+
+ @yieldsmixed.grep_v(Array).force.should == yields
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object).grep_v(Object) {}.size.should == nil
+ Enumerator::Lazy.new(Object.new, 100) {}.grep_v(Object).grep_v(Object).size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times when not given a block" do
+ (0..Float::INFINITY).lazy.grep_v(3..5).grep_v(6..10).first(3).should == [0, 1, 2]
+
+ @eventsmixed.grep_v(Symbol).grep_v(String).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "stops after specified times when given a block" do
+ (0..Float::INFINITY).lazy
+ .grep_v(1..2) { |n| n > 3 ? n : false }
+ .grep_v(false) { |n| n.even? ? n : false }
+ .first(3)
+ .should == [4, false, 6]
+
+ @eventsmixed.grep_v(Symbol) {}.grep_v(String) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.grep_v(String).first(100).should ==
+ s.first(100).grep_v(String)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/initialize_spec.rb b/spec/ruby/core/enumerator/lazy/initialize_spec.rb
new file mode 100644
index 0000000000..47765d32c3
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/initialize_spec.rb
@@ -0,0 +1,63 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy#initialize" do
+ before :each do
+ @receiver = receiver = Object.new
+
+ def receiver.each
+ yield 0
+ yield 1
+ yield 2
+ end
+
+ @uninitialized = Enumerator::Lazy.allocate
+ end
+
+ it "is a private method" do
+ Enumerator::Lazy.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "returns self" do
+ @uninitialized.send(:initialize, @receiver) {}.should.equal?(@uninitialized)
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ @uninitialized.send(:initialize, @receiver) do |yielder, *values|
+ yielder.<<(*values)
+ end.first(2).should == [0, 1]
+ end
+ end
+
+ it "sets #size to nil if not given a size" do
+ @uninitialized.send(:initialize, @receiver) {}.size.should == nil
+ end
+
+ it "sets #size to nil if given size is nil" do
+ @uninitialized.send(:initialize, @receiver, nil) {}.size.should == nil
+ end
+
+ it "sets given size to own size if the given size is Float::INFINITY" do
+ @uninitialized.send(:initialize, @receiver, Float::INFINITY) {}.size.should.equal?(Float::INFINITY)
+ end
+
+ it "sets given size to own size if the given size is an Integer" do
+ @uninitialized.send(:initialize, @receiver, 100) {}.size.should == 100
+ end
+
+ it "sets given size to own size if the given size is a Proc" do
+ @uninitialized.send(:initialize, @receiver, -> { 200 }) {}.size.should == 200
+ end
+
+ it "raises an ArgumentError when block is not given" do
+ -> { @uninitialized.send :initialize, @receiver }.should.raise(ArgumentError)
+ end
+
+ describe "on frozen instance" do
+ it "raises a FrozenError" do
+ -> { @uninitialized.freeze.send(:initialize, @receiver) {} }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/lazy_spec.rb b/spec/ruby/core/enumerator/lazy/lazy_spec.rb
new file mode 100644
index 0000000000..12107383d6
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/lazy_spec.rb
@@ -0,0 +1,27 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy" do
+ it "is a subclass of Enumerator" do
+ Enumerator::Lazy.superclass.should.equal?(Enumerator)
+ end
+
+ it "defines lazy versions of a whitelist of Enumerator methods" do
+ lazy_methods = Set[
+ :chunk, :chunk_while, :collect, :collect_concat, :compact, :drop, :drop_while, :enum_for,
+ :find_all, :flat_map, :force, :grep, :grep_v, :lazy, :map, :reject,
+ :select, :slice_after, :slice_before, :slice_when, :take, :take_while,
+ :to_enum, :uniq, :zip
+ ]
+
+ Enumerator::Lazy.instance_methods(false).to_set.should >= lazy_methods
+ end
+end
+
+describe "Enumerator::Lazy#lazy" do
+ it "returns self" do
+ lazy = (1..3).to_enum.lazy
+ lazy.lazy.should.equal?(lazy)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/map_spec.rb b/spec/ruby/core/enumerator/lazy/map_spec.rb
new file mode 100644
index 0000000000..5cb998f5f7
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/map_spec.rb
@@ -0,0 +1,12 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/collect'
+
+describe "Enumerator::Lazy#map" do
+ it_behaves_like :enumerator_lazy_collect, :map
+
+ it "doesn't unwrap Arrays" do
+ Enumerator.new {|y| y.yield([1])}.lazy.to_a.should == [[1]]
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/reject_spec.rb b/spec/ruby/core/enumerator/lazy/reject_spec.rb
new file mode 100644
index 0000000000..374d4df14e
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/reject_spec.rb
@@ -0,0 +1,78 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#reject" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.reject {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.reject {}.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.reject(&:even?).first(3).should == [1, 3, 5]
+
+ @eventsmixed.reject { false }.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "lets exceptions raised in the block go through" do
+ lazy = 10.times.lazy.map do |i|
+ raise "foo"
+ end
+
+ lazy = lazy.reject(&:nil?)
+
+ -> {
+ lazy.first
+ }.should.raise(RuntimeError, "foo")
+ end
+
+ it "calls the block with a gathered array when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.reject { |v| yields << v }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields
+ end
+
+ it "raises an ArgumentError when not given a block" do
+ -> { @yieldsmixed.reject }.should.raise(ArgumentError)
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(20).reject {}.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.reject { |n| n < 4 }.reject(&:even?).first(3).should == [5, 7, 9]
+
+ @eventsmixed.reject { false }.reject { false }.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.reject { |n| false }.first(100).should ==
+ s.first(100).reject { |n| false }
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/select_spec.rb b/spec/ruby/core/enumerator/lazy/select_spec.rb
new file mode 100644
index 0000000000..3773d8f0a8
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/select_spec.rb
@@ -0,0 +1,47 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/select'
+
+describe "Enumerator::Lazy#select" do
+ it_behaves_like :enumerator_lazy_select, :select
+
+ it "doesn't pre-evaluate the next element" do
+ eval_count = 0
+ enum = %w[Text1 Text2 Text3].lazy.select do
+ eval_count += 1
+ true
+ end
+
+ eval_count.should == 0
+ enum.next
+ eval_count.should == 1
+ end
+
+ it "doesn't over-evaluate when peeked" do
+ eval_count = 0
+ enum = %w[Text1 Text2 Text3].lazy.select do
+ eval_count += 1
+ true
+ end
+
+ eval_count.should == 0
+ enum.peek
+ enum.peek
+ eval_count.should == 1
+ end
+
+ it "doesn't re-evaluate after peek" do
+ eval_count = 0
+ enum = %w[Text1 Text2 Text3].lazy.select do
+ eval_count += 1
+ true
+ end
+
+ eval_count.should == 0
+ enum.peek
+ eval_count.should == 1
+ enum.next
+ eval_count.should == 1
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/shared/collect.rb b/spec/ruby/core/enumerator/lazy/shared/collect.rb
new file mode 100644
index 0000000000..0ed04c8e02
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/shared/collect.rb
@@ -0,0 +1,62 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :enumerator_lazy_collect, shared: true do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.send(@method) {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "keeps size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.send(@method) {}.size.should == 100
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.send(@method, &:succ).first(3).should == [1, 2, 3]
+
+ @eventsmixed.send(@method) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with initial values when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.send(@method) { |v| yields << v }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields
+ end
+
+ describe "on a nested Lazy" do
+ it "keeps size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.send(@method) {}.send(@method) {}.size.should == 100
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.send(@method, &:succ).send(@method, &:succ).first(3).should == [2, 3, 4]
+
+ @eventsmixed.send(@method) {}.send(@method) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.send(@method) { |n| n }.first(100).should ==
+ s.first(100).send(@method) { |n| n }.to_a
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb b/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb
new file mode 100644
index 0000000000..685e6d0594
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/shared/collect_concat.rb
@@ -0,0 +1,78 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :enumerator_lazy_collect_concat, shared: true do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.send(@method) {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { true }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s }.first(6).should == %w[0 10 20 30 40 50]
+
+ @eventsmixed.send(@method) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "flattens elements when the given block returned an array or responding to .each and .force" do
+ (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.chars }.first(6).should == %w[0 1 0 2 0 3]
+ (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should == true
+ (0..Float::INFINITY).lazy.send(@method) { |n| (n * 10).to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3]
+ end
+ end
+
+ it "calls the block with initial values when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.send(@method) { |v| yields << v }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields
+ end
+
+ it "raises an ArgumentError when not given a block" do
+ -> { @yieldsmixed.send(@method) }.should.raise(ArgumentError)
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.send(@method) {}.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s }.first(6).should == %w[0 10 20 30 40 50]
+
+ @eventsmixed.send(@method) {}.send(@method) {}.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "flattens elements when the given block returned an array or responding to .each and .force" do
+ (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.chars }.first(6).should == %w[0 1 0 2 0 3]
+ (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.each_char }.first(6).all? { |o| o.instance_of? Enumerator }.should == true
+ (0..Float::INFINITY).lazy.map {|n| n * 10 }.send(@method) { |n| n.to_s.each_char.lazy }.first(6).should == %w[0 1 0 2 0 3]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.send(@method) { |n| [-n, +n] }.first(200).should ==
+ s.first(100).send(@method) { |n| [-n, +n] }.to_a
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/shared/select.rb b/spec/ruby/core/enumerator/lazy/shared/select.rb
new file mode 100644
index 0000000000..2d15176620
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/shared/select.rb
@@ -0,0 +1,66 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :enumerator_lazy_select, shared: true do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.send(@method) {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { true }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.send(@method, &:even?).first(3).should == [0, 2, 4]
+
+ @eventsmixed.send(@method) { true }.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with a gathered array when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.send(@method) { |v| yields << v }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.gathered_yields
+ end
+
+ it "raises an ArgumentError when not given a block" do
+ -> { @yieldsmixed.send(@method) }.should.raise(ArgumentError)
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(50) {}.send(@method) { true }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.send(@method) { |n| n > 5 }.send(@method, &:even?).first(3).should == [6, 8, 10]
+
+ @eventsmixed.send(@method) { true }.send(@method) { true }.first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.send(@method) { |n| true }.first(100).should ==
+ s.first(100).send(@method) { |n| true }
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/shared/to_enum.rb b/spec/ruby/core/enumerator/lazy/shared/to_enum.rb
new file mode 100644
index 0000000000..75b9aefe8c
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/shared/to_enum.rb
@@ -0,0 +1,55 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../../spec_helper'
+
+describe :enumerator_lazy_to_enum, shared: true do
+ before :each do
+ @infinite = (0..Float::INFINITY).lazy
+ end
+
+ it "requires multiple arguments" do
+ Enumerator::Lazy.instance_method(@method).arity.should < 0
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @infinite.send @method
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@infinite)
+ end
+
+ it "sets #size to nil when not given a block" do
+ Enumerator::Lazy.new(Object.new, 100) {}.send(@method).size.should == nil
+ end
+
+ it "sets given block to size when given a block" do
+ Enumerator::Lazy.new(Object.new, 100) {}.send(@method) { 30 }.size.should == 30
+ end
+
+ it "generates a lazy enumerator from the given name" do
+ @infinite.send(@method, :with_index, 10).first(3).should == [[0, 10], [1, 11], [2, 12]]
+ end
+
+ it "passes given arguments to wrapped method" do
+ @infinite.send(@method, :each_slice, 2).map { |assoc| assoc.first * assoc.last }.first(4).should == [0, 6, 20, 42]
+ end
+
+ it "used by some parent's methods though returning Lazy" do
+ { each_with_index: [],
+ with_index: [],
+ cycle: [1],
+ each_with_object: [Object.new],
+ with_object: [Object.new],
+ each_slice: [2],
+ each_entry: [],
+ each_cons: [2]
+ }.each_pair do |method, args|
+ @infinite.send(method, *args).should.instance_of?(Enumerator::Lazy)
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.send(@method, :with_index).first(100).should ==
+ s.first(100).to_enum.send(@method, :with_index).to_a
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/slice_after_spec.rb b/spec/ruby/core/enumerator/lazy/slice_after_spec.rb
new file mode 100644
index 0000000000..f8cd97178f
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/slice_after_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy#slice_after" do
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.slice_after { |n| true }.first(100).should ==
+ s.first(100).slice_after { |n| true }.to_a
+ end
+
+ it "should return a lazy enumerator" do
+ s = 0..Float::INFINITY
+ s.lazy.slice_after { |n| true }.should.is_a?(Enumerator::Lazy)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/slice_before_spec.rb b/spec/ruby/core/enumerator/lazy/slice_before_spec.rb
new file mode 100644
index 0000000000..192e853343
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/slice_before_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy#slice_before" do
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.slice_before { |n| true }.first(100).should ==
+ s.first(100).slice_before { |n| true }.to_a
+ end
+
+ it "should return a lazy enumerator" do
+ s = 0..Float::INFINITY
+ s.lazy.slice_before { |n| true }.should.is_a?(Enumerator::Lazy)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/slice_when_spec.rb b/spec/ruby/core/enumerator/lazy/slice_when_spec.rb
new file mode 100644
index 0000000000..fc9d5f5069
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/slice_when_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Lazy#slice_when" do
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.slice_when { |a, b| true }.first(100).should ==
+ s.first(100).slice_when { |a, b| true }.to_a
+ end
+
+ it "should return a lazy enumerator" do
+ s = 0..Float::INFINITY
+ s.lazy.slice_when { |a, b| true }.should.is_a?(Enumerator::Lazy)
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/take_spec.rb b/spec/ruby/core/enumerator/lazy/take_spec.rb
new file mode 100644
index 0000000000..2dd5b939e2
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/take_spec.rb
@@ -0,0 +1,74 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../enumerable/shared/value_packing'
+
+describe "Enumerator::Lazy#take" do
+ describe "value packing of source yields (matches Enumerable#take)" do
+ before :each do
+ @take = -> e { e.lazy.take(1) }
+ end
+ it_behaves_like :enumerable_value_packing, nil
+ end
+
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.take(1)
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets given count to size if the given count is less than old size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(20).size.should == 20
+ Enumerator::Lazy.new(Object.new, 100) {}.take(200).size.should == 100
+ end
+
+ it "sets given count to size if the old size is Infinity" do
+ loop.lazy.take(20).size.should == 20
+ end
+
+ describe "when the returned lazy enumerator is evaluated by .force" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.take(2).force.should == [0, 1]
+
+ @eventsmixed.take(1).force
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "stops without iterations if the given argument is 0" do
+ @eventsmixed.take(0).force
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "on a nested Lazy" do
+ it "sets given count to size if the given count is less than old size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(20).take(50).size.should == 20
+ Enumerator::Lazy.new(Object.new, 100) {}.take(50).take(20).size.should == 20
+ end
+
+ describe "when the returned lazy enumerator is evaluated by .force" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.map(&:succ).take(2).force.should == [1, 2]
+
+ @eventsmixed.take(10).take(1).force
+ ScratchPad.recorded.should == [:before_yield]
+ end
+
+ it "stops without iterations if the given argument is 0" do
+ @eventsmixed.take(10).take(0).force
+ ScratchPad.recorded.should == []
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/take_while_spec.rb b/spec/ruby/core/enumerator/lazy/take_while_spec.rb
new file mode 100644
index 0000000000..c369712c56
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/take_while_spec.rb
@@ -0,0 +1,60 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#take_while" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.take_while {}
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take_while { true }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by .force" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.take_while { |n| n < 3 }.force.should == [0, 1, 2]
+
+ @eventsmixed.take_while { false }.force
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with initial values when yield with multiple arguments" do
+ yields = []
+ @yieldsmixed.take_while { |v| yields << v; true }.force
+ yields.should == EnumeratorLazySpecs::YieldsMixed.initial_yields
+ end
+
+ it "raises an ArgumentError when not given a block" do
+ -> { @yieldsmixed.take_while }.should.raise(ArgumentError)
+ end
+
+ describe "on a nested Lazy" do
+ it "sets #size to nil" do
+ Enumerator::Lazy.new(Object.new, 100) {}.take(20).take_while { true }.size.should == nil
+ end
+
+ describe "when the returned lazy enumerator is evaluated by .force" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.take_while { |n| n < 3 }.take_while(&:even?).force.should == [0]
+
+ @eventsmixed.take_while { true }.take_while { false }.force
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/to_enum_spec.rb b/spec/ruby/core/enumerator/lazy/to_enum_spec.rb
new file mode 100644
index 0000000000..210e5294b7
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/to_enum_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'shared/to_enum'
+
+describe "Enumerator::Lazy#to_enum" do
+ it_behaves_like :enumerator_lazy_to_enum, :to_enum
+end
diff --git a/spec/ruby/core/enumerator/lazy/uniq_spec.rb b/spec/ruby/core/enumerator/lazy/uniq_spec.rb
new file mode 100644
index 0000000000..d30ed8df2f
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/uniq_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Enumerator::Lazy#uniq' do
+ context 'without block' do
+ before :each do
+ @lazy = [0, 1, 0, 1].to_enum.lazy.uniq
+ end
+
+ it 'returns a lazy enumerator' do
+ @lazy.should.instance_of?(Enumerator::Lazy)
+ @lazy.force.should == [0, 1]
+ end
+
+ it 'return same value after rewind' do
+ @lazy.force.should == [0, 1]
+ @lazy.force.should == [0, 1]
+ end
+
+ it 'sets the size to nil' do
+ @lazy.size.should == nil
+ end
+ end
+
+ context 'when yielded with an argument' do
+ before :each do
+ @lazy = [0, 1, 2, 3].to_enum.lazy.uniq(&:even?)
+ end
+
+ it 'returns a lazy enumerator' do
+ @lazy.should.instance_of?(Enumerator::Lazy)
+ @lazy.force.should == [0, 1]
+ end
+
+ it 'return same value after rewind' do
+ @lazy.force.should == [0, 1]
+ @lazy.force.should == [0, 1]
+ end
+
+ it 'sets the size to nil' do
+ @lazy.size.should == nil
+ end
+ end
+
+ context 'when yielded with multiple arguments' do
+ before :each do
+ enum = Object.new.to_enum
+ class << enum
+ def each
+ yield 0, 'foo'
+ yield 1, 'FOO'
+ yield 2, 'bar'
+ end
+ end
+ @lazy = enum.lazy
+ end
+
+ it 'return same value after rewind' do
+ enum = @lazy.uniq { |_, label| label.downcase }
+ enum.force.should == [[0, 'foo'], [2, 'bar']]
+ enum.force.should == [[0, 'foo'], [2, 'bar']]
+ end
+
+ it 'returns all yield arguments as an array' do
+ @lazy.uniq { |_, label| label.downcase }.force.should == [[0, 'foo'], [2, 'bar']]
+ end
+ end
+
+ it "works with an infinite enumerable" do
+ s = 0..Float::INFINITY
+ s.lazy.uniq.first(100).should ==
+ s.first(100).uniq
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/with_index_spec.rb b/spec/ruby/core/enumerator/lazy/with_index_spec.rb
new file mode 100644
index 0000000000..2e983fd3b1
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/with_index_spec.rb
@@ -0,0 +1,36 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#with_index" do
+ it "enumerates with an index" do
+ (0..Float::INFINITY).lazy.with_index.map { |i, idx| [i, idx] }.first(3).should == [[0, 0], [1, 1], [2, 2]]
+ end
+
+ it "enumerates with an index starting at a given offset" do
+ (0..Float::INFINITY).lazy.with_index(3).map { |i, idx| [i, idx] }.first(3).should == [[0, 3], [1, 4], [2, 5]]
+ end
+
+ it "enumerates with an index starting at 0 when offset is nil" do
+ (0..Float::INFINITY).lazy.with_index(nil).map { |i, idx| [i, idx] }.first(3).should == [[0, 0], [1, 1], [2, 2]]
+ end
+
+ it "raises TypeError when offset does not convert to Integer" do
+ -> { (0..Float::INFINITY).lazy.with_index(false).map { |i, idx| i }.first(3) }.should.raise(TypeError)
+ end
+
+ it "enumerates with a given block" do
+ result = []
+ (0..Float::INFINITY).lazy.with_index { |i, idx| result << [i * 2, idx] }.first(3)
+ result.should == [[0,0],[2,1],[4,2]]
+ end
+
+ it "resets after a new call to each" do
+ enum = (0..2).lazy.with_index.map { |i, idx| [i, idx] }
+ result = []
+ enum.each { |i, idx| result << [i, idx] }
+ enum.each { |i, idx| result << [i, idx] }
+ result.should == [[0,0], [1,1], [2,2], [0,0], [1,1], [2,2]]
+ end
+end
diff --git a/spec/ruby/core/enumerator/lazy/zip_spec.rb b/spec/ruby/core/enumerator/lazy/zip_spec.rb
new file mode 100644
index 0000000000..9f612542d7
--- /dev/null
+++ b/spec/ruby/core/enumerator/lazy/zip_spec.rb
@@ -0,0 +1,86 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Enumerator::Lazy#zip" do
+ before :each do
+ @yieldsmixed = EnumeratorLazySpecs::YieldsMixed.new.to_enum.lazy
+ @eventsmixed = EnumeratorLazySpecs::EventsMixed.new.to_enum.lazy
+ ScratchPad.record []
+ end
+
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "returns a new instance of Enumerator::Lazy" do
+ ret = @yieldsmixed.zip []
+ ret.should.instance_of?(Enumerator::Lazy)
+ ret.should_not.equal?(@yieldsmixed)
+ end
+
+ it "keeps size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.zip([], []).size.should == 100
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.zip([4, 5], [8]).first(2).should == [[0, 4, 8], [1, 5, nil]]
+
+ @eventsmixed.zip([0, 1]).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+
+ it "calls the block with a gathered array when yield with multiple arguments" do
+ yields = @yieldsmixed.zip(EnumeratorLazySpecs::YieldsMixed.new.to_enum).force
+ yields.should == [EnumeratorLazySpecs::YieldsMixed.gathered_yields,
+ EnumeratorLazySpecs::YieldsMixed.gathered_yields].transpose
+ end
+
+ it "returns a Lazy when no arguments given" do
+ @yieldsmixed.zip.should.instance_of?(Enumerator::Lazy)
+ end
+
+ it "raises a TypeError if arguments contain non-list object" do
+ -> { @yieldsmixed.zip [], Object.new, [] }.should.raise(TypeError)
+ end
+
+ describe "on a nested Lazy" do
+ it "keeps size" do
+ Enumerator::Lazy.new(Object.new, 100) {}.map {}.zip([], []).size.should == 100
+ end
+
+ it "behaves as Enumerable#zip when given a block" do
+ lazy_yields = []
+ lazy_ret = @yieldsmixed.zip(EnumeratorLazySpecs::YieldsMixed.new.to_enum) { |lists| lazy_yields << lists }
+ enum_yields = []
+ enum_ret = EnumeratorLazySpecs::YieldsMixed.new.to_enum.zip(EnumeratorLazySpecs::YieldsMixed.new.to_enum) { |lists| enum_yields << lists }
+
+ lazy_yields.should == enum_yields
+ lazy_ret.should == enum_ret
+ end
+
+ describe "when the returned lazy enumerator is evaluated by Enumerable#first" do
+ it "stops after specified times" do
+ (0..Float::INFINITY).lazy.map(&:succ).zip([4, 5], [8]).first(2).should == [[1, 4, 8], [2, 5, nil]]
+
+ @eventsmixed.zip([0, 1]).zip([0, 1]).first(1)
+ ScratchPad.recorded.should == [:before_yield]
+ end
+ end
+ end
+
+ it "works with an infinite enumerable and an array" do
+ s = 0..Float::INFINITY
+ s.lazy.zip(0..1000).first(100).should ==
+ s.first(100).zip(0..100)
+ end
+
+ it "works with two infinite enumerables" do
+ s = 0..Float::INFINITY
+ s.lazy.zip(s).first(100).should ==
+ s.first(100).zip(s)
+ end
+end
diff --git a/spec/ruby/core/enumerator/new_spec.rb b/spec/ruby/core/enumerator/new_spec.rb
new file mode 100644
index 0000000000..eb6c13759e
--- /dev/null
+++ b/spec/ruby/core/enumerator/new_spec.rb
@@ -0,0 +1,115 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator.new" do
+ context "no block given" do
+ it "raises" do
+ -> { Enumerator.new(1, :upto, 3) }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when passed a block" do
+ it "defines iteration with block, yielder argument and calling << method" do
+ enum = Enumerator.new do |yielder|
+ a = 1
+
+ loop do
+ yielder << a
+ a = a + 1
+ end
+ end
+
+ enum.take(3).should == [1, 2, 3]
+ end
+
+ it "defines iteration with block, yielder argument and calling yield method" do
+ enum = Enumerator.new do |yielder|
+ a = 1
+
+ loop do
+ yielder.yield(a)
+ a = a + 1
+ end
+ end
+
+ enum.take(3).should == [1, 2, 3]
+ end
+
+ it "defines iteration with block, yielder argument and treating it as a proc" do
+ enum = Enumerator.new do |yielder|
+ "a\nb\nc".each_line(&yielder)
+ end
+
+ enum.to_a.should == ["a\n", "b\n", "c"]
+ end
+
+ describe '#yield' do
+ it 'accepts a single argument' do
+ Enumerator.new { |y| y.yield(1) }.to_a.should == [1]
+ Enumerator.new { |y| y.yield(1) }.first.should == 1
+ end
+
+ it 'accepts multiple arguments' do
+ Enumerator.new { |y| y.yield(1, 2) }.to_a.should == [[1, 2]]
+ Enumerator.new { |y| y.yield(1, 2) }.first.should == [1, 2]
+ end
+
+ it "doesn't double-wrap arrays" do
+ Enumerator.new { |y| y.yield([1]) }.to_a.should == [[1]]
+ Enumerator.new { |y| y.yield([1]) }.first.should == [1]
+
+ Enumerator.new { |y| y.yield([1, 2]) }.to_a.should == [[1, 2]]
+ Enumerator.new { |y| y.yield([1, 2]) }.first.should == [1, 2]
+ end
+
+ it 'returns nil' do
+ ScratchPad.record []
+ Enumerator.new do |y|
+ ScratchPad << y.yield(1)
+ end.to_a
+
+ ScratchPad.recorded.should == [nil]
+ end
+
+ it 'accepts keyword arguments and treats them as a positional hash' do
+ Enumerator.new { |y| y.yield(foo: 42) }.to_a.should == [{ foo: 42 }]
+ Enumerator.new { |y| y.yield(foo: 42) }.first.should == { foo: 42 }
+
+ Enumerator.new { |y| y.yield(123, foo: 42) }.to_a.should == [[123, { foo: 42 }]]
+ Enumerator.new { |y| y.yield(123, foo: 42) }.first.should == [123, { foo: 42 }]
+ end
+ end
+
+ describe '#<<' do
+ it 'accepts a single argument' do
+ Enumerator.new { |y| y.<<(1) }.to_a.should == [1]
+ Enumerator.new { |y| y.<<(1) }.first.should == 1
+ end
+
+ it "doesn't double-wrap arrays" do
+ Enumerator.new { |y| y.<<([1]) }.to_a.should == [[1]]
+ Enumerator.new { |y| y.<<([1]) }.first.should == [1]
+
+ Enumerator.new { |y| y.<<([1, 2]) }.to_a.should == [[1, 2]]
+ Enumerator.new { |y| y.<<([1, 2]) }.first.should == [1, 2]
+ end
+
+ it 'accepts keyword arguments and treats them as a positional hash' do
+ Enumerator.new { |y| y.<<(foo: 42) }.to_a.should == [{ foo: 42 }]
+ Enumerator.new { |y| y.<<(foo: 42) }.first.should == { foo: 42 }
+ end
+
+ it 'can be chained' do
+ enum = Enumerator.new do |y|
+ y << 1 << 2
+ end
+ enum.to_a.should == [1, 2]
+ end
+
+ it 'raises ArgumentError when given more than one argument' do
+ -> {
+ Enumerator.new { |y| y.<<(1, 2) }.to_a
+ }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 1)")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/next_spec.rb b/spec/ruby/core/enumerator/next_spec.rb
new file mode 100644
index 0000000000..77e79185a9
--- /dev/null
+++ b/spec/ruby/core/enumerator/next_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#next" do
+ before :each do
+ @enum = 1.upto(3)
+ end
+
+ it "returns the next element of the enumeration" do
+ @enum.next.should == 1
+ @enum.next.should == 2
+ @enum.next.should == 3
+ end
+
+ it "raises a StopIteration exception at the end of the stream" do
+ 3.times { @enum.next }
+ -> { @enum.next }.should.raise(StopIteration)
+ end
+
+ it "cannot be called again until the enumerator is rewound" do
+ 3.times { @enum.next }
+ -> { @enum.next }.should.raise(StopIteration)
+ -> { @enum.next }.should.raise(StopIteration)
+ -> { @enum.next }.should.raise(StopIteration)
+ @enum.rewind
+ @enum.next.should == 1
+ end
+
+ it "restarts the enumerator if an exception terminated a previous iteration" do
+ exception = StandardError.new
+ enum = Enumerator.new do
+ raise exception
+ end
+
+ result = 2.times.map { enum.next rescue $! }
+
+ result.should == [exception, exception]
+ end
+end
diff --git a/spec/ruby/core/enumerator/next_values_spec.rb b/spec/ruby/core/enumerator/next_values_spec.rb
new file mode 100644
index 0000000000..63e024e2b4
--- /dev/null
+++ b/spec/ruby/core/enumerator/next_values_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#next_values" do
+ before :each do
+ o = Object.new
+ def o.each
+ yield :a
+ yield :b1, :b2
+ yield :c
+ yield :d1, :d2
+ yield :e1, :e2, :e3
+ yield nil
+ yield
+ yield [:f1, :f2]
+ end
+
+ @e = o.to_enum
+ end
+
+ it "returns the next element in self" do
+ @e.next_values.should == [:a]
+ end
+
+ it "advances the position of the current element" do
+ @e.next.should == :a
+ @e.next_values.should == [:b1, :b2]
+ @e.next.should == :c
+ end
+
+ it "advances the position of the enumerator each time when called multiple times" do
+ 2.times { @e.next_values }
+ @e.next_values.should == [:c]
+ @e.next.should == [:d1, :d2]
+ end
+
+ it "works in concert with #rewind" do
+ 2.times { @e.next }
+ @e.rewind
+ @e.next_values.should == [:a]
+ end
+
+ it "returns an array with only nil if yield is called with nil" do
+ 5.times { @e.next }
+ @e.next_values.should == [nil]
+ end
+
+ it "returns an empty array if yield is called without arguments" do
+ 6.times { @e.next }
+ @e.next_values.should == []
+ end
+
+ it "returns an array of array if yield is called with an array" do
+ 7.times { @e.next }
+ @e.next_values.should == [[:f1, :f2]]
+ end
+
+ it "raises StopIteration if called on a finished enumerator" do
+ 8.times { @e.next }
+ -> { @e.next_values }.should.raise(StopIteration)
+ end
+end
diff --git a/spec/ruby/core/enumerator/peek_spec.rb b/spec/ruby/core/enumerator/peek_spec.rb
new file mode 100644
index 0000000000..096fd2b10c
--- /dev/null
+++ b/spec/ruby/core/enumerator/peek_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#peek" do
+ before :each do
+ @e = (1..5).to_a.to_enum
+ end
+
+ it "returns the next element in self" do
+ @e.peek.should == 1
+ end
+
+ it "does not advance the position of the current element" do
+ @e.next.should == 1
+ @e.peek.should == 2
+ @e.next.should == 2
+ end
+
+ it "can be called repeatedly without advancing the position of the current element" do
+ @e.peek
+ @e.peek
+ @e.peek.should == 1
+ @e.next.should == 1
+ end
+
+ it "works in concert with #rewind" do
+ @e.next
+ @e.next
+ @e.rewind
+ @e.peek.should == 1
+ end
+
+ it "raises StopIteration if called on a finished enumerator" do
+ 5.times { @e.next }
+ -> { @e.peek }.should.raise(StopIteration)
+ end
+end
diff --git a/spec/ruby/core/enumerator/peek_values_spec.rb b/spec/ruby/core/enumerator/peek_values_spec.rb
new file mode 100644
index 0000000000..63f7988bcd
--- /dev/null
+++ b/spec/ruby/core/enumerator/peek_values_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#peek_values" do
+ before :each do
+ o = Object.new
+ def o.each
+ yield :a
+ yield :b1, :b2
+ yield :c
+ yield :d1, :d2
+ yield :e1, :e2, :e3
+ yield nil
+ yield
+ yield [:f1, :f2]
+ end
+
+ @e = o.to_enum
+ end
+
+ it "returns the next element in self" do
+ @e.peek_values.should == [:a]
+ end
+
+ it "does not advance the position of the current element" do
+ @e.next.should == :a
+ @e.peek_values.should == [:b1, :b2]
+ @e.next.should == [:b1, :b2]
+ end
+
+ it "can be called repeatedly without advancing the position of the current element" do
+ @e.peek_values
+ @e.peek_values
+ @e.peek_values.should == [:a]
+ @e.next.should == :a
+ end
+
+ it "works in concert with #rewind" do
+ @e.next
+ @e.next
+ @e.rewind
+ @e.peek_values.should == [:a]
+ end
+
+ it "returns an array with only nil if yield is called with nil" do
+ 5.times { @e.next }
+ @e.peek_values.should == [nil]
+ end
+
+ it "returns an empty array if yield is called without arguments" do
+ 6.times { @e.next }
+ @e.peek_values.should == []
+ end
+
+ it "returns an array of array if yield is called with an array" do
+ 7.times { @e.next }
+ @e.peek_values.should == [[:f1, :f2]]
+ end
+
+ it "raises StopIteration if called on a finished enumerator" do
+ 8.times { @e.next }
+ -> { @e.peek_values }.should.raise(StopIteration)
+ end
+end
diff --git a/spec/ruby/core/enumerator/plus_spec.rb b/spec/ruby/core/enumerator/plus_spec.rb
new file mode 100644
index 0000000000..d6c0fa93ac
--- /dev/null
+++ b/spec/ruby/core/enumerator/plus_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#+" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns a chain of self and provided enumerators" do
+ one = Enumerator.new { |y| y << 1 }
+ two = Enumerator.new { |y| y << 2 }
+ three = Enumerator.new { |y| y << 3 }
+
+ chain = one + two + three
+
+ chain.should.instance_of?(Enumerator::Chain)
+ chain.each { |item| ScratchPad << item }
+ ScratchPad.recorded.should == [1, 2, 3]
+ end
+
+ it "calls #each on each argument" do
+ enum = Enumerator.new { |y| y << "one" }
+
+ obj1 = mock("obj1")
+ obj1.should_receive(:each).once.and_yield("two")
+
+ obj2 = mock("obj2")
+ obj2.should_receive(:each).once.and_yield("three")
+
+ chain = enum + obj1 + obj2
+ chain.each { |item| ScratchPad << item }
+ ScratchPad.recorded.should == ["one", "two", "three"]
+ end
+end
diff --git a/spec/ruby/core/enumerator/produce_spec.rb b/spec/ruby/core/enumerator/produce_spec.rb
new file mode 100644
index 0000000000..eb1b09294e
--- /dev/null
+++ b/spec/ruby/core/enumerator/produce_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator.produce" do
+ it "creates an infinite enumerator" do
+ enum = Enumerator.produce(0) { |prev| prev + 1 }
+
+ enum.size.should == Float::INFINITY
+ enum.take(5).should == [0, 1, 2, 3, 4]
+ end
+
+ it "terminates iteration when block raises StopIteration exception" do
+ enum = Enumerator.produce(0) do | prev|
+ raise StopIteration if prev >= 2
+ prev + 1
+ end
+
+ enum.to_a.should == [0, 1, 2]
+ end
+
+ context "when initial value skipped" do
+ it "uses nil instead" do
+ ScratchPad.record []
+ enum = Enumerator.produce { |prev| ScratchPad << prev; (prev || 0) + 1 }
+
+ enum.take(3).should == [1, 2, 3]
+ ScratchPad.recorded.should == [nil, 1, 2]
+ end
+
+ it "starts enumerable from result of first block call" do
+ array = "a\nb\nc\nd".lines
+ lines = Enumerator.produce { array.shift }.take_while { |s| s }
+
+ lines.should == ["a\n", "b\n", "c\n", "d"]
+ end
+ end
+
+ it "raises ArgumentError when no block is given" do
+ -> { Enumerator.produce }.should.raise(ArgumentError, "no block given")
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "accepts keyword arguments as the initial value" do
+ enum = Enumerator.produce(a: 1, b: 1) {}
+ enum.take(1).should == [{a: 1, b: 1}]
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "raises ArgumentError for unknown keyword arguments" do
+ -> { Enumerator.produce(a: 1, b: 1) {} }.should.raise(ArgumentError, /unknown keywords/)
+ end
+ end
+
+ ruby_version_is "4.0" do
+ context "with size keyword argument" do
+ it "sets the size of the enumerator" do
+ enum = Enumerator.produce(0, size: 10) { |n| n + 1 }
+
+ enum.size.should == 10
+ enum.take(5).should == [0, 1, 2, 3, 4]
+ end
+
+ it "accepts a callable" do
+ enum = Enumerator.produce(0, size: -> { 5 * 5 }) { |n| n + 1 }
+
+ enum.size.should == 25
+ enum.take(5).should == [0, 1, 2, 3, 4]
+ end
+
+ it "accepts nil" do
+ enum = Enumerator.produce(0, size: nil) { |n| n + 1 }
+
+ enum.size.should == nil
+ enum.take(5).should == [0, 1, 2, 3, 4]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/product/each_spec.rb b/spec/ruby/core/enumerator/product/each_spec.rb
new file mode 100644
index 0000000000..a5dced4db1
--- /dev/null
+++ b/spec/ruby/core/enumerator/product/each_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../../spec_helper'
+require_relative '../../enumerable/shared/enumeratorized'
+
+describe "Enumerator::Product#each" do
+ it_behaves_like :enumeratorized_with_origin_size, :each, Enumerator::Product.new([1, 2], [:a, :b])
+
+ it "yields each element of Cartesian product of enumerators" do
+ enum = Enumerator::Product.new([1, 2], [:a, :b])
+ acc = []
+ enum.each { |e| acc << e }
+ acc.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "calls #each_entry method on enumerators" do
+ object1 = Object.new
+ def object1.each_entry
+ yield 1
+ yield 2
+ end
+
+ object2 = Object.new
+ def object2.each_entry
+ yield :a
+ yield :b
+ end
+
+ enum = Enumerator::Product.new(object1, object2)
+ acc = []
+ enum.each { |e| acc << e }
+ acc.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "raises a NoMethodError if the object doesn't respond to #each_entry" do
+ -> {
+ Enumerator::Product.new(Object.new).each {}
+ }.should.raise(NoMethodError, /undefined method [`']each_entry' for/)
+ end
+
+ it "returns enumerator if not given a block" do
+ enum = Enumerator::Product.new([1, 2], [:a, :b])
+ enum.each.should.kind_of?(Enumerator)
+
+ enum = Enumerator::Product.new([1, 2], [:a, :b])
+ enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "returns self if given a block" do
+ enum = Enumerator::Product.new([1, 2], [:a, :b])
+ enum.each {}.should.equal?(enum)
+ end
+
+ it "doesn't accept arguments" do
+ Enumerator::Product.instance_method(:each).arity.should == 0
+ end
+
+ it "yields each element to a block that takes multiple arguments" do
+ enum = Enumerator::Product.new([1, 2], [:a, :b])
+
+ acc = []
+ enum.each { |x, y| acc << x }
+ acc.should == [1, 1, 2, 2]
+
+ acc = []
+ enum.each { |x, y| acc << y }
+ acc.should == [:a, :b, :a, :b]
+
+ acc = []
+ enum.each { |x, y, z| acc << z }
+ acc.should == [nil, nil, nil, nil]
+ end
+
+ it "yields no element when any enumerable is empty" do
+ enum = Enumerator::Product.new([], [1])
+
+ acc = []
+ enum.each { |x| acc << x }
+ acc.should == []
+
+ enum = Enumerator::Product.new([1], [])
+
+ acc = []
+ enum.each { |x| acc << x }
+ acc.should == []
+ end
+end
diff --git a/spec/ruby/core/enumerator/product/initialize_copy_spec.rb b/spec/ruby/core/enumerator/product/initialize_copy_spec.rb
new file mode 100644
index 0000000000..b5d2b345a9
--- /dev/null
+++ b/spec/ruby/core/enumerator/product/initialize_copy_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Product#initialize_copy" do
+ it "replaces content of the receiver with content of the other object" do
+ enum = Enumerator::Product.new([true, false])
+ enum2 = Enumerator::Product.new([1, 2], [:a, :b])
+
+ enum.send(:initialize_copy, enum2)
+ enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "returns self" do
+ enum = Enumerator::Product.new([true, false])
+ enum2 = Enumerator::Product.new([1, 2], [:a, :b])
+
+ enum.send(:initialize_copy, enum2).should.equal?(enum)
+ end
+
+ it "is a private method" do
+ Enumerator::Product.private_instance_methods(false).should.include?(:initialize_copy)
+ end
+
+ it "does nothing if the argument is the same as the receiver" do
+ enum = Enumerator::Product.new(1..2)
+ enum.send(:initialize_copy, enum).should.equal?(enum)
+
+ enum.freeze
+ enum.send(:initialize_copy, enum).should.equal?(enum)
+ end
+
+ it "raises FrozenError if the receiver is frozen" do
+ enum = Enumerator::Product.new(1..2)
+ enum2 = Enumerator::Product.new(3..4)
+
+ -> { enum.freeze.send(:initialize_copy, enum2) }.should.raise(FrozenError)
+ end
+
+ it "raises TypeError if the objects are of different class" do
+ enum = Enumerator::Product.new(1..2)
+ enum2 = Class.new(Enumerator::Product).new(3..4)
+
+ -> { enum.send(:initialize_copy, enum2) }.should.raise(TypeError, 'initialize_copy should take same class object')
+ -> { enum2.send(:initialize_copy, enum) }.should.raise(TypeError, 'initialize_copy should take same class object')
+ end
+
+ it "raises ArgumentError if the argument is not initialized yet" do
+ enum = Enumerator::Product.new(1..2)
+ enum2 = Enumerator::Product.allocate
+
+ -> { enum.send(:initialize_copy, enum2) }.should.raise(ArgumentError, 'uninitialized product')
+ end
+end
diff --git a/spec/ruby/core/enumerator/product/initialize_spec.rb b/spec/ruby/core/enumerator/product/initialize_spec.rb
new file mode 100644
index 0000000000..8814f9d3c7
--- /dev/null
+++ b/spec/ruby/core/enumerator/product/initialize_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Product#initialize" do
+ before :each do
+ @uninitialized = Enumerator::Product.allocate
+ end
+
+ it "is a private method" do
+ Enumerator::Product.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "returns self" do
+ @uninitialized.send(:initialize).should.equal?(@uninitialized)
+ end
+
+ it "accepts many arguments" do
+ @uninitialized.send(:initialize, 0..1, 2..3, 4..5).should.equal?(@uninitialized)
+ end
+
+ it "accepts arguments that are not Enumerable nor responding to :each_entry" do
+ @uninitialized.send(:initialize, Object.new).should.equal?(@uninitialized)
+ end
+
+ describe "on frozen instance" do
+ it "raises a FrozenError" do
+ -> {
+ @uninitialized.freeze.send(:initialize, 0..1)
+ }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/product/inspect_spec.rb b/spec/ruby/core/enumerator/product/inspect_spec.rb
new file mode 100644
index 0000000000..e0d7441f26
--- /dev/null
+++ b/spec/ruby/core/enumerator/product/inspect_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Product#inspect" do
+ it "returns a String including enumerators" do
+ enum = Enumerator::Product.new([1, 2], [:a, :b])
+ enum.inspect.should == "#<Enumerator::Product: [[1, 2], [:a, :b]]>"
+ end
+
+ it "represents a recursive element with '[...]'" do
+ enum = [1, 2]
+ enum_recursive = Enumerator::Product.new(enum)
+
+ enum << enum_recursive
+ enum_recursive.inspect.should == "#<Enumerator::Product: [[1, 2, #<Enumerator::Product: ...>]]>"
+ end
+
+ it "returns a not initialized representation if #initialized is not called yet" do
+ Enumerator::Product.allocate.inspect.should == "#<Enumerator::Product: uninitialized>"
+ end
+end
diff --git a/spec/ruby/core/enumerator/product/rewind_spec.rb b/spec/ruby/core/enumerator/product/rewind_spec.rb
new file mode 100644
index 0000000000..2beffaf5c1
--- /dev/null
+++ b/spec/ruby/core/enumerator/product/rewind_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Product#rewind" do
+ before :each do
+ @enum = Enumerator::Product.new([1, 2].each.to_enum, [:a, :b].each.to_enum)
+ end
+
+ it "resets the enumerator to its initial state" do
+ @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ @enum.rewind
+ @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "returns self" do
+ @enum.rewind.should.equal? @enum
+ end
+
+ it "has no effect on a new enumerator" do
+ @enum.rewind
+ @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "has no effect if called multiple, consecutive times" do
+ @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ @enum.rewind
+ @enum.rewind
+ @enum.each.to_a.should == [[1, :a], [1, :b], [2, :a], [2, :b]]
+ end
+
+ it "calls the enclosed object's rewind method if one exists" do
+ obj = mock('rewinder')
+ enum = Enumerator::Product.new(obj.to_enum)
+
+ obj.should_receive(:rewind)
+ enum.rewind
+ end
+
+ it "does nothing if the object doesn't have a #rewind method" do
+ obj = mock('rewinder')
+ enum = Enumerator::Product.new(obj.to_enum)
+
+ enum.rewind.should == enum
+ end
+
+ it "calls a rewind method on each enumerable in direct order" do
+ ScratchPad.record []
+
+ object1 = Object.new
+ def object1.rewind; ScratchPad << :object1; end
+
+ object2 = Object.new
+ def object2.rewind; ScratchPad << :object2; end
+
+ object3 = Object.new
+ def object3.rewind; ScratchPad << :object3; end
+
+ enum = Enumerator::Product.new(object1, object2, object3)
+ enum.rewind
+
+ ScratchPad.recorded.should == [:object1, :object2, :object3]
+ end
+end
diff --git a/spec/ruby/core/enumerator/product/size_spec.rb b/spec/ruby/core/enumerator/product/size_spec.rb
new file mode 100644
index 0000000000..0ba427af9a
--- /dev/null
+++ b/spec/ruby/core/enumerator/product/size_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerator::Product#size" do
+ it "returns the total size of the enumerator product calculated by multiplying the sizes of enumerables in the product" do
+ product = Enumerator::Product.new(1..2, 1..3, 1..4)
+ product.size.should == 24 # 2 * 3 * 4
+ end
+
+ it "returns nil if any enumerable reports its size as nil" do
+ enum = Object.new
+ def enum.size; nil; end
+
+ product = Enumerator::Product.new(1..2, enum)
+ product.size.should == nil
+ end
+
+ it "returns Float::INFINITY if any enumerable reports its size as Float::INFINITY" do
+ enum = Object.new
+ def enum.size; Float::INFINITY; end
+
+ product = Enumerator::Product.new(1..2, enum)
+ product.size.should == Float::INFINITY
+ end
+
+ it "returns nil if any enumerable reports its size as Float::NAN" do
+ enum = Object.new
+ def enum.size; Float::NAN; end
+
+ product = Enumerator::Product.new(1..2, enum)
+ product.size.should == nil
+ end
+
+ it "returns nil if any enumerable doesn't respond to #size" do
+ enum = Object.new
+ product = Enumerator::Product.new(1..2, enum)
+ product.size.should == nil
+ end
+
+ it "returns nil if any enumerable reports a not-convertible to Integer" do
+ enum = Object.new
+ def enum.size; :symbol; end
+
+ product = Enumerator::Product.new(1..2, enum)
+ product.size.should == nil
+ end
+
+ it "returns nil if any enumerable reports a non-Integer but convertible to Integer size" do
+ enum = Object.new
+ def enum.size; 1.0; end
+
+ product = Enumerator::Product.new(1..2, enum)
+ product.size.should == nil
+ end
+
+ ruby_version_is "3.4" do
+ it "returns zero when any enumerable reports zero" do
+ enum = Enumerator::Product.new(1...1, ["A", "B"])
+ enum.size.should == 0
+
+ enum = Enumerator::Product.new(["A", "B"], 1...1)
+ enum.size.should == 0
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/product_spec.rb b/spec/ruby/core/enumerator/product_spec.rb
new file mode 100644
index 0000000000..0f37ef76e7
--- /dev/null
+++ b/spec/ruby/core/enumerator/product_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator.product" do
+ it "returns a Cartesian product of enumerators" do
+ enum = Enumerator.product(1..2, ["A", "B"])
+ enum.to_a.should == [[1, "A"], [1, "B"], [2, "A"], [2, "B"]]
+ end
+
+ it "accepts a list of enumerators of any length" do
+ enum = Enumerator.product(1..2)
+ enum.to_a.should == [[1], [2]]
+
+ enum = Enumerator.product(1..2, ["A"])
+ enum.to_a.should == [[1, "A"], [2, "A"]]
+
+ enum = Enumerator.product(1..2, ["A"], ["B"])
+ enum.to_a.should == [[1, "A", "B"], [2, "A", "B"]]
+
+ enum = Enumerator.product(2..3, ["A"], ["B"], ["C"])
+ enum.to_a.should == [[2, "A", "B", "C"], [3, "A", "B", "C"]]
+ end
+
+ it "returns an enumerator with an empty array when no arguments passed" do
+ enum = Enumerator.product
+ enum.to_a.should == [[]]
+ end
+
+ it "returns an instance of Enumerator::Product" do
+ enum = Enumerator.product
+ enum.class.should == Enumerator::Product
+ end
+
+ it "accepts infinite enumerators and returns infinite enumerator" do
+ enum = Enumerator.product(1.., ["A", "B"])
+ enum.take(5).should == [[1, "A"], [1, "B"], [2, "A"], [2, "B"], [3, "A"]]
+ enum.size.should == Float::INFINITY
+ end
+
+ it "accepts a block" do
+ elems = []
+ enum = Enumerator.product(1..2, ["X", "Y"]) { elems << _1 }
+
+ elems.should == [[1, "X"], [1, "Y"], [2, "X"], [2, "Y"]]
+ end
+
+ it "returns nil when a block passed" do
+ Enumerator.product(1..2) {}.should == nil
+ end
+
+ # https://bugs.ruby-lang.org/issues/19829
+ it "reject keyword arguments" do
+ -> {
+ Enumerator.product(1..3, foo: 1, bar: 2)
+ }.should.raise(ArgumentError, "unknown keywords: :foo, :bar")
+ end
+
+ it "calls only #each_entry method on arguments" do
+ object = Object.new
+ def object.each_entry
+ yield 1
+ yield 2
+ end
+
+ enum = Enumerator.product(object, ["A", "B"])
+ enum.to_a.should == [[1, "A"], [1, "B"], [2, "A"], [2, "B"]]
+ end
+
+ it "raises NoMethodError when argument doesn't respond to #each_entry" do
+ -> {
+ Enumerator.product(Object.new).to_a
+ }.should.raise(NoMethodError, /undefined method [`']each_entry' for/)
+ end
+
+ it "calls #each_entry lazily" do
+ Enumerator.product(Object.new).should.is_a?(Enumerator)
+ end
+
+ it "iterates through consuming enumerator elements only once" do
+ a = [1, 2, 3]
+ i = 0
+
+ enum = Enumerator.new do |y|
+ while i < a.size
+ y << a[i]
+ i += 1
+ end
+ end
+
+ Enumerator.product(['a', 'b'], enum).to_a.should == [["a", 1], ["a", 2], ["a", 3]]
+ end
+end
diff --git a/spec/ruby/core/enumerator/rewind_spec.rb b/spec/ruby/core/enumerator/rewind_spec.rb
new file mode 100644
index 0000000000..6ba0edf174
--- /dev/null
+++ b/spec/ruby/core/enumerator/rewind_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Enumerator#rewind" do
+ before :each do
+ @enum = 1.upto(3)
+ end
+
+ it "resets the enumerator to its initial state" do
+ @enum.next.should == 1
+ @enum.next.should == 2
+ @enum.rewind
+ @enum.next.should == 1
+ end
+
+ it "returns self" do
+ @enum.rewind.should.equal? @enum
+ end
+
+ it "has no effect on a new enumerator" do
+ @enum.rewind
+ @enum.next.should == 1
+ end
+
+ it "has no effect if called multiple, consecutive times" do
+ @enum.next.should == 1
+ @enum.rewind
+ @enum.rewind
+ @enum.next.should == 1
+ end
+
+ it "works with peek to reset the position" do
+ @enum.next
+ @enum.next
+ @enum.rewind
+ @enum.next
+ @enum.peek.should == 2
+ end
+
+ it "calls the enclosed object's rewind method if one exists" do
+ obj = mock('rewinder')
+ enum = obj.to_enum
+ obj.should_receive(:each).at_most(1)
+ obj.should_receive(:rewind)
+ enum.rewind
+ end
+
+ it "does nothing if the object doesn't have a #rewind method" do
+ obj = mock('rewinder')
+ enum = obj.to_enum
+ obj.should_receive(:each).at_most(1)
+ enum.rewind.should == enum
+ end
+end
+
+describe "Enumerator#rewind" do
+ before :each do
+ ScratchPad.record []
+ @enum = EnumeratorSpecs::Feed.new.to_enum(:each)
+ end
+
+ it "clears a pending #feed value" do
+ @enum.next
+ @enum.feed :a
+ @enum.rewind
+ @enum.next
+ @enum.next
+ ScratchPad.recorded.should == [nil]
+ end
+end
diff --git a/spec/ruby/core/enumerator/shared/enum_for.rb b/spec/ruby/core/enumerator/shared/enum_for.rb
new file mode 100644
index 0000000000..4388103ecf
--- /dev/null
+++ b/spec/ruby/core/enumerator/shared/enum_for.rb
@@ -0,0 +1,57 @@
+describe :enum_for, shared: true do
+ it "is defined in Kernel" do
+ Kernel.method_defined?(@method).should == true
+ end
+
+ it "returns a new enumerator" do
+ "abc".send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "defaults the first argument to :each" do
+ enum = [1,2].send(@method)
+ enum.map { |v| v }.should == [1,2].each { |v| v }
+ end
+
+ it "sets regexp matches in the caller" do
+ "wawa".send(@method, :scan, /./).map {|o| $& }.should == ["w", "a", "w", "a"]
+ a = []
+ "wawa".send(@method, :scan, /./).each {|o| a << $& }
+ a.should == ["w", "a", "w", "a"]
+ end
+
+ it "exposes multi-arg yields as an array" do
+ o = Object.new
+ def o.each
+ yield :a
+ yield :b1, :b2
+ yield [:c]
+ yield :d1, :d2
+ yield :e1, :e2, :e3
+ end
+
+ enum = o.send(@method)
+ enum.next.should == :a
+ enum.next.should == [:b1, :b2]
+ enum.next.should == [:c]
+ enum.next.should == [:d1, :d2]
+ enum.next.should == [:e1, :e2, :e3]
+ end
+
+ it "uses the passed block's value to calculate the size of the enumerator" do
+ Object.new.enum_for { 100 }.size.should == 100
+ end
+
+ it "defers the evaluation of the passed block until #size is called" do
+ ScratchPad.record []
+
+ enum = Object.new.enum_for do
+ ScratchPad << :called
+ 100
+ end
+
+ ScratchPad.recorded.should.empty?
+
+ enum.size
+ ScratchPad.recorded.should == [:called]
+ end
+end
diff --git a/spec/ruby/core/enumerator/shared/with_index.rb b/spec/ruby/core/enumerator/shared/with_index.rb
new file mode 100644
index 0000000000..0992397e95
--- /dev/null
+++ b/spec/ruby/core/enumerator/shared/with_index.rb
@@ -0,0 +1,33 @@
+require_relative '../../../spec_helper'
+
+describe :enum_with_index, shared: true do
+
+ require_relative '../fixtures/classes'
+
+ before :each do
+ @origin = [1, 2, 3, 4]
+ @enum = @origin.to_enum
+ end
+
+ it "passes each element and its index to block" do
+ a = []
+ @enum.send(@method) { |o, i| a << [o, i] }
+ a.should == [[1, 0], [2, 1], [3, 2], [4, 3]]
+ end
+
+ it "returns the object being enumerated when given a block" do
+ @enum.send(@method) { |o, i| :glark }.should.equal?(@origin)
+ end
+
+ it "binds splat arguments properly" do
+ acc = []
+ @enum.send(@method) { |*b| c,d = b; acc << c; acc << d }
+ [1, 0, 2, 1, 3, 2, 4, 3].should == acc
+ end
+
+ it "returns an enumerator if no block is supplied" do
+ ewi = @enum.send(@method)
+ ewi.should.instance_of?(Enumerator)
+ ewi.to_a.should == [[1, 0], [2, 1], [3, 2], [4, 3]]
+ end
+end
diff --git a/spec/ruby/core/enumerator/shared/with_object.rb b/spec/ruby/core/enumerator/shared/with_object.rb
new file mode 100644
index 0000000000..50d4f24eb3
--- /dev/null
+++ b/spec/ruby/core/enumerator/shared/with_object.rb
@@ -0,0 +1,42 @@
+require_relative '../../../spec_helper'
+
+describe :enum_with_object, shared: true do
+ before :each do
+ @enum = [:a, :b].to_enum
+ @memo = ''
+ @block_params = @enum.send(@method, @memo).to_a
+ end
+
+ it "receives an argument" do
+ @enum.method(@method).arity.should == 1
+ end
+
+ context "with block" do
+ it "returns the given object" do
+ ret = @enum.send(@method, @memo) do |elm, memo|
+ # nothing
+ end
+ ret.should.equal?(@memo)
+ end
+
+ context "the block parameter" do
+ it "passes each element to first parameter" do
+ @block_params[0][0].should.equal?(:a)
+ @block_params[1][0].should.equal?(:b)
+ end
+
+ it "passes the given object to last parameter" do
+ @block_params[0][1].should.equal?(@memo)
+ @block_params[1][1].should.equal?(@memo)
+ end
+ end
+ end
+
+ context "without block" do
+ it "returns new Enumerator" do
+ ret = @enum.send(@method, @memo)
+ ret.should.instance_of?(Enumerator)
+ ret.should_not.equal?(@enum)
+ end
+ end
+end
diff --git a/spec/ruby/core/enumerator/size_spec.rb b/spec/ruby/core/enumerator/size_spec.rb
new file mode 100644
index 0000000000..4b2beffbbe
--- /dev/null
+++ b/spec/ruby/core/enumerator/size_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Enumerator#size" do
+ it "returns same value if set size is an Integer" do
+ Enumerator.new(100) {}.size.should == 100
+ end
+
+ it "returns nil if set size is nil" do
+ Enumerator.new(nil) {}.size.should == nil
+ end
+
+ it "returns returning value from size.call if set size is a Proc" do
+ base_size = 100
+ enum = Enumerator.new(-> { base_size + 1 }) {}
+ base_size = 200
+ enum.size.should == 201
+ base_size = 300
+ enum.size.should == 301
+ end
+
+ it "returns the result from size.call if the size respond to call" do
+ obj = mock('call')
+ obj.should_receive(:call).and_return(42)
+ Enumerator.new(obj) {}.size.should == 42
+ end
+end
diff --git a/spec/ruby/core/enumerator/to_enum_spec.rb b/spec/ruby/core/enumerator/to_enum_spec.rb
new file mode 100644
index 0000000000..7fb73d0c3c
--- /dev/null
+++ b/spec/ruby/core/enumerator/to_enum_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/enum_for'
+
+describe "Enumerator#to_enum" do
+ it_behaves_like :enum_for, :to_enum
+end
diff --git a/spec/ruby/core/enumerator/with_index_spec.rb b/spec/ruby/core/enumerator/with_index_spec.rb
new file mode 100644
index 0000000000..ca90fd18f7
--- /dev/null
+++ b/spec/ruby/core/enumerator/with_index_spec.rb
@@ -0,0 +1,89 @@
+require_relative '../../spec_helper'
+require_relative 'shared/with_index'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Enumerator#with_index" do
+ it_behaves_like :enum_with_index, :with_index
+ it_behaves_like :enumeratorized_with_origin_size, :with_index, [1,2,3].select
+
+ it "returns a new Enumerator when no block is given" do
+ enum1 = [1,2,3].select
+ enum2 = enum1.with_index
+ enum2.should.instance_of?(Enumerator)
+ enum1.should_not === enum2
+ end
+
+ it "accepts an optional argument when given a block" do
+ -> do
+ @enum.with_index(1) { |f| f}
+ end.should_not.raise(ArgumentError)
+ end
+
+ it "accepts an optional argument when not given a block" do
+ -> do
+ @enum.with_index(1)
+ end.should_not.raise(ArgumentError)
+ end
+
+ it "numbers indices from the given index when given an offset but no block" do
+ @enum.with_index(1).to_a.should == [[1,1], [2,2], [3,3], [4,4]]
+ end
+
+ it "numbers indices from the given index when given an offset and block" do
+ acc = []
+ @enum.with_index(1) {|e,i| acc << [e,i] }
+ acc.should == [[1,1], [2,2], [3,3], [4,4]]
+ end
+
+ it "raises a TypeError when the argument cannot be converted to numeric" do
+ -> do
+ @enum.with_index('1') {|*i| i}
+ end.should.raise(TypeError)
+ end
+
+ it "converts non-numeric arguments to Integer via #to_int" do
+ (o = mock('1')).should_receive(:to_int).and_return(1)
+ @enum.with_index(o).to_a.should == [[1,1], [2,2], [3,3], [4,4]]
+ end
+
+ it "coerces the given numeric argument to an Integer" do
+ @enum.with_index(1.678).to_a.should == [[1,1], [2,2], [3,3], [4,4]]
+
+ res = []
+ @enum.with_index(1.001) { |*x| res << x}
+ res.should == [[1,1], [2,2], [3,3], [4,4]]
+ end
+
+ it "treats nil argument as no argument" do
+ @enum.with_index(nil).to_a.should == [[1,0], [2,1], [3,2], [4,3]]
+
+ res = []
+ @enum.with_index(nil) { |*x| res << x}
+ res.should == [[1,0], [2,1], [3,2], [4,3]]
+ end
+
+ it "accepts negative argument" do
+ @enum.with_index(-1).to_a.should == [[1,-1], [2,0], [3,1], [4,2]]
+
+ res = []
+ @enum.with_index(-1) { |*x| res << x}
+ res.should == [[1,-1], [2,0], [3,1], [4,2]]
+ end
+
+ it "passes on the given block's return value" do
+ arr = [1,2,3]
+ arr.delete_if.with_index { |a,b| false }
+ arr.should == [1,2,3]
+
+ arr.delete_if.with_index { |a,b| true }
+ arr.should == []
+ end
+
+ it "returns the iterator's return value" do
+ @enum.select.with_index { |a,b| false }.should == []
+ end
+
+ it "returns the correct value if chained with itself" do
+ [:a].each.with_index.with_index.to_a.should == [[[:a,0],0]]
+ end
+end
diff --git a/spec/ruby/core/enumerator/with_object_spec.rb b/spec/ruby/core/enumerator/with_object_spec.rb
new file mode 100644
index 0000000000..58031fd765
--- /dev/null
+++ b/spec/ruby/core/enumerator/with_object_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/with_object'
+
+describe "Enumerator#with_object" do
+ it_behaves_like :enum_with_object, :with_object
+end
diff --git a/spec/ruby/core/env/assoc_spec.rb b/spec/ruby/core/env/assoc_spec.rb
new file mode 100644
index 0000000000..b81be7ddf2
--- /dev/null
+++ b/spec/ruby/core/env/assoc_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "ENV.assoc" do
+ before :each do
+ @foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @foo
+ end
+
+ it "returns an array of the key and value of the environment variable with the given key" do
+ ENV["foo"] = "bar"
+ ENV.assoc("foo").should == ["foo", "bar"]
+ end
+
+ it "returns nil if no environment variable with the given key exists" do
+ ENV.assoc("foo").should == nil
+ end
+
+ it "returns the key element coerced with #to_str" do
+ ENV["foo"] = "bar"
+ k = mock('key')
+ k.should_receive(:to_str).and_return("foo")
+ ENV.assoc(k).should == ["foo", "bar"]
+ end
+
+ it "raises TypeError if the argument is not a String and does not respond to #to_str" do
+ -> { ENV.assoc(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+end
diff --git a/spec/ruby/core/env/clear_spec.rb b/spec/ruby/core/env/clear_spec.rb
new file mode 100644
index 0000000000..c0d20193ad
--- /dev/null
+++ b/spec/ruby/core/env/clear_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "ENV.clear" do
+ it "deletes all environment variables" do
+ orig = ENV.to_hash
+ begin
+ ENV.clear.should.equal?(ENV)
+
+ # This used 'env' the helper before. That shells out to 'env' which
+ # itself sets up certain environment variables before it runs, because
+ # the shell sets them up before it runs any command.
+ #
+ # Thusly, you can ONLY test this by asking through ENV itself.
+ ENV.size.should == 0
+ ensure
+ ENV.replace orig
+ end
+ end
+
+end
diff --git a/spec/ruby/core/env/clone_spec.rb b/spec/ruby/core/env/clone_spec.rb
new file mode 100644
index 0000000000..bb3c7ff2f8
--- /dev/null
+++ b/spec/ruby/core/env/clone_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "ENV#clone" do
+ it "raises ArgumentError when keyword argument 'freeze' is neither nil nor boolean" do
+ -> {
+ ENV.clone(freeze: 1)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when keyword argument is not 'freeze'" do
+ -> {
+ ENV.clone(foo: nil)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises TypeError" do
+ -> {
+ ENV.clone
+ }.should.raise(TypeError, /Cannot clone ENV, use ENV.to_h to get a copy of ENV as a hash/)
+ end
+end
diff --git a/spec/ruby/core/env/delete_if_spec.rb b/spec/ruby/core/env/delete_if_spec.rb
new file mode 100644
index 0000000000..4b05064eba
--- /dev/null
+++ b/spec/ruby/core/env/delete_if_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "ENV.delete_if" do
+ before :each do
+ @foo = ENV["foo"]
+ @bar = ENV["bar"]
+
+ ENV["foo"] = "0"
+ ENV["bar"] = "1"
+ end
+
+ after :each do
+ ENV["foo"] = @foo
+ ENV["bar"] = @bar
+ end
+
+ it "deletes pairs if the block returns true" do
+ ENV.delete_if { |k, v| ["foo", "bar"].include?(k) }
+ ENV["foo"].should == nil
+ ENV["bar"].should == nil
+ end
+
+ it "returns ENV when block given" do
+ ENV.delete_if { |k, v| ["foo", "bar"].include?(k) }.should.equal?(ENV)
+ end
+
+ it "returns ENV even if nothing deleted" do
+ ENV.delete_if { false }.should.equal?(ENV)
+ end
+
+ it "returns an Enumerator if no block given" do
+ ENV.delete_if.should.instance_of?(Enumerator)
+ end
+
+ it "deletes pairs through enumerator" do
+ enum = ENV.delete_if
+ enum.each { |k, v| ["foo", "bar"].include?(k) }
+ ENV["foo"].should == nil
+ ENV["bar"].should == nil
+ end
+
+ it "returns ENV from enumerator" do
+ enum = ENV.delete_if
+ enum.each { |k, v| ["foo", "bar"].include?(k) }.should.equal?(ENV)
+ end
+
+ it "returns ENV from enumerator even if nothing deleted" do
+ enum = ENV.delete_if
+ enum.each { false }.should.equal?(ENV)
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :delete_if, ENV
+end
diff --git a/spec/ruby/core/env/delete_spec.rb b/spec/ruby/core/env/delete_spec.rb
new file mode 100644
index 0000000000..db6d07b057
--- /dev/null
+++ b/spec/ruby/core/env/delete_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+
+describe "ENV.delete" do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "removes the variable from the environment" do
+ ENV["foo"] = "bar"
+ ENV.delete("foo")
+ ENV["foo"].should == nil
+ end
+
+ it "returns the previous value" do
+ ENV["foo"] = "bar"
+ ENV.delete("foo").should == "bar"
+ end
+
+ it "returns nil if the named environment variable does not exist and no block given" do
+ ENV.delete("foo")
+ ENV.delete("foo").should == nil
+ end
+
+ it "yields the name to the given block if the named environment variable does not exist" do
+ ENV.delete("foo")
+ ENV.delete("foo") { |name| ScratchPad.record name }
+ ScratchPad.recorded.should == "foo"
+ end
+
+ it "returns the result of given block if the named environment variable does not exist" do
+ ENV.delete("foo")
+ ENV.delete("foo") { |name| "bar" }.should == "bar"
+ end
+
+ it "does not evaluate the block if the environment variable exists" do
+ ENV["foo"] = "bar"
+ ENV.delete("foo") { |name| fail "Should not happen" }
+ ENV["foo"].should == nil
+ end
+
+ it "removes the variable coerced with #to_str" do
+ ENV["foo"] = "bar"
+ k = mock('key')
+ k.should_receive(:to_str).and_return("foo")
+ ENV.delete(k)
+ ENV["foo"].should == nil
+ end
+
+ it "raises TypeError if the argument is not a String and does not respond to #to_str" do
+ -> { ENV.delete(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+end
diff --git a/spec/ruby/core/env/dup_spec.rb b/spec/ruby/core/env/dup_spec.rb
new file mode 100644
index 0000000000..d8cfac891d
--- /dev/null
+++ b/spec/ruby/core/env/dup_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "ENV#dup" do
+ it "raises TypeError" do
+ -> {
+ ENV.dup
+ }.should.raise(TypeError, /Cannot dup ENV, use ENV.to_h to get a copy of ENV as a hash/)
+ end
+end
diff --git a/spec/ruby/core/env/each_key_spec.rb b/spec/ruby/core/env/each_key_spec.rb
new file mode 100644
index 0000000000..ad2cb560a0
--- /dev/null
+++ b/spec/ruby/core/env/each_key_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "ENV.each_key" do
+
+ it "returns each key" do
+ e = []
+ orig = ENV.to_hash
+ begin
+ ENV.clear
+ ENV["1"] = "3"
+ ENV["2"] = "4"
+ ENV.each_key { |k| e << k }.should.equal?(ENV)
+ e.should.include?("1")
+ e.should.include?("2")
+ ensure
+ ENV.replace orig
+ end
+ end
+
+ it "returns an Enumerator if called without a block" do
+ enum = ENV.each_key
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == ENV.keys
+ end
+
+ it "returns keys in the locale encoding" do
+ ENV.each_key do |key|
+ key.encoding.should == Encoding.find('locale')
+ end
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :each_key, ENV
+end
diff --git a/spec/ruby/core/env/each_pair_spec.rb b/spec/ruby/core/env/each_pair_spec.rb
new file mode 100644
index 0000000000..2d7ed5faa0
--- /dev/null
+++ b/spec/ruby/core/env/each_pair_spec.rb
@@ -0,0 +1,6 @@
+require_relative 'spec_helper'
+require_relative 'shared/each'
+
+describe "ENV.each_pair" do
+ it_behaves_like :env_each, :each_pair
+end
diff --git a/spec/ruby/core/env/each_spec.rb b/spec/ruby/core/env/each_spec.rb
new file mode 100644
index 0000000000..d1e06f55b6
--- /dev/null
+++ b/spec/ruby/core/env/each_spec.rb
@@ -0,0 +1,6 @@
+require_relative 'spec_helper'
+require_relative 'shared/each'
+
+describe "ENV.each" do
+ it_behaves_like :env_each, :each
+end
diff --git a/spec/ruby/core/env/each_value_spec.rb b/spec/ruby/core/env/each_value_spec.rb
new file mode 100644
index 0000000000..6f65e923e6
--- /dev/null
+++ b/spec/ruby/core/env/each_value_spec.rb
@@ -0,0 +1,34 @@
+require_relative 'spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "ENV.each_value" do
+
+ it "returns each value" do
+ e = []
+ orig = ENV.to_hash
+ begin
+ ENV.clear
+ ENV["1"] = "3"
+ ENV["2"] = "4"
+ ENV.each_value { |v| e << v }.should.equal?(ENV)
+ e.should.include?("3")
+ e.should.include?("4")
+ ensure
+ ENV.replace orig
+ end
+ end
+
+ it "returns an Enumerator if called without a block" do
+ enum = ENV.each_value
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == ENV.values
+ end
+
+ it "uses the locale encoding" do
+ ENV.each_value do |value|
+ value.should.be_locale_env
+ end
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :each_value, ENV
+end
diff --git a/spec/ruby/core/env/element_reference_spec.rb b/spec/ruby/core/env/element_reference_spec.rb
new file mode 100644
index 0000000000..e3ac979418
--- /dev/null
+++ b/spec/ruby/core/env/element_reference_spec.rb
@@ -0,0 +1,76 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "ENV.[]" do
+ before :each do
+ @variable = "returns_only_frozen_values"
+ end
+
+ after :each do
+ ENV.delete @variable
+ end
+
+ it "returns nil if the variable isn't found" do
+ ENV["this_var_is_never_set"].should == nil
+ end
+
+ it "returns only frozen values" do
+ ENV[@variable] = "a non-frozen string"
+ ENV[@variable].should.frozen?
+ end
+
+ it "coerces a non-string name with #to_str" do
+ ENV[@variable] = "bar"
+ k = mock('key')
+ k.should_receive(:to_str).and_return(@variable)
+ ENV[k].should == "bar"
+ end
+
+ it "raises TypeError if the argument is not a String and does not respond to #to_str" do
+ -> { ENV[Object.new] }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ platform_is :windows do
+ it "looks up values case-insensitively" do
+ ENV[@variable] = "bar"
+ ENV[@variable.upcase].should == "bar"
+ end
+ end
+end
+
+describe "ENV.[]" do
+ before :each do
+ @variable = "env_element_reference_encoding_specs"
+
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::BINARY
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+
+ ENV.delete @variable
+ end
+
+ it "uses the locale encoding if Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+
+ locale = ENVSpecs.encoding
+ locale = Encoding::BINARY if locale == Encoding::US_ASCII
+ ENV[@variable] = "\xC3\xB8"
+ ENV[@variable].encoding.should == locale
+ end
+
+ it "transcodes from the locale encoding to Encoding.default_internal if set" do
+ # We cannot reliably know the locale encoding, so we merely check that
+ # the result string has the expected encoding.
+ ENV[@variable] = ""
+ Encoding.default_internal = Encoding::IBM437
+
+ ENV[@variable].encoding.should.equal?(Encoding::IBM437)
+ end
+end
diff --git a/spec/ruby/core/env/element_set_spec.rb b/spec/ruby/core/env/element_set_spec.rb
new file mode 100644
index 0000000000..26dfee1ade
--- /dev/null
+++ b/spec/ruby/core/env/element_set_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/store'
+
+describe "ENV.[]=" do
+ it_behaves_like :env_store, :[]=
+end
diff --git a/spec/ruby/core/env/empty_spec.rb b/spec/ruby/core/env/empty_spec.rb
new file mode 100644
index 0000000000..afeb406a9e
--- /dev/null
+++ b/spec/ruby/core/env/empty_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "ENV.empty?" do
+
+ it "returns true if the Environment is empty" do
+ if ENV.keys.size > 0
+ ENV.should_not.empty?
+ end
+ orig = ENV.to_hash
+ begin
+ ENV.clear
+ ENV.should.empty?
+ ensure
+ ENV.replace orig
+ end
+ end
+
+ it "returns false if not empty" do
+ if ENV.keys.size > 0
+ ENV.should_not.empty?
+ end
+ end
+end
diff --git a/spec/ruby/core/env/except_spec.rb b/spec/ruby/core/env/except_spec.rb
new file mode 100644
index 0000000000..fb8f3b7536
--- /dev/null
+++ b/spec/ruby/core/env/except_spec.rb
@@ -0,0 +1,34 @@
+require_relative 'spec_helper'
+require_relative 'shared/to_hash'
+
+describe "ENV.except" do
+ before do
+ @orig_hash = ENV.to_hash
+ end
+
+ after do
+ ENV.replace @orig_hash
+ end
+
+ # Testing the method without arguments is covered via
+ it_behaves_like :env_to_hash, :except
+
+ it "returns a hash without the requested subset" do
+ ENV.clear
+
+ ENV['one'] = '1'
+ ENV['two'] = '2'
+ ENV['three'] = '3'
+
+ ENV.except('one', 'three').should == { 'two' => '2' }
+ end
+
+ it "ignores keys not present in the original hash" do
+ ENV.clear
+
+ ENV['one'] = '1'
+ ENV['two'] = '2'
+
+ ENV.except('one', 'three').should == { 'two' => '2' }
+ end
+end
diff --git a/spec/ruby/core/env/fetch_spec.rb b/spec/ruby/core/env/fetch_spec.rb
new file mode 100644
index 0000000000..a2ec79c62b
--- /dev/null
+++ b/spec/ruby/core/env/fetch_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/hash/key_error'
+require_relative 'fixtures/common'
+
+describe "ENV.fetch" do
+ before :each do
+ @foo_saved = ENV.delete("foo")
+ end
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "returns a value" do
+ ENV["foo"] = "bar"
+ ENV.fetch("foo").should == "bar"
+ end
+
+ it "raises a TypeError if the key is not a String" do
+ -> { ENV.fetch Object.new }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ context "when the key is not found" do
+ it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, ENV
+
+ it "formats the object with #inspect in the KeyError message" do
+ -> {
+ ENV.fetch('foo')
+ }.should.raise(KeyError, 'key not found: "foo"')
+ end
+ end
+
+ it "provides the given default parameter" do
+ ENV.fetch("foo", "default").should == "default"
+ end
+
+ it "does not insist that the default be a String" do
+ ENV.fetch("foo", :default).should == :default
+ end
+
+ it "provides a default value from a block" do
+ ENV.fetch("foo") { |k| "wanted #{k}" }.should == "wanted foo"
+ end
+
+ it "does not insist that the block return a String" do
+ ENV.fetch("foo") { |k| k.to_sym }.should == :foo
+ end
+
+ it "warns on block and default parameter given" do
+ -> do
+ ENV.fetch("foo", "default") { "bar" }.should == "bar"
+ end.should complain(/block supersedes default value argument/)
+ end
+
+ it "does not evaluate the block when key found" do
+ ENV["foo"] = "bar"
+ ENV.fetch("foo") { fail "should not get here"}.should == "bar"
+ end
+
+ it "uses the locale encoding" do
+ ENV["foo"] = "bar"
+ ENV.fetch("foo").encoding.should == ENVSpecs.encoding
+ end
+end
diff --git a/spec/ruby/core/env/fetch_values_spec.rb b/spec/ruby/core/env/fetch_values_spec.rb
new file mode 100644
index 0000000000..302cde2fd1
--- /dev/null
+++ b/spec/ruby/core/env/fetch_values_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+ruby_version_is "4.1" do
+ describe "ENV.fetch_values" do
+ before :each do
+ @saved_foo = ENV["foo"]
+ @saved_bar = ENV["bar"]
+ ENV.delete("foo")
+ ENV.delete("bar")
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ ENV["bar"] = @saved_bar
+ end
+
+ it "returns an array of the values corresponding to the given keys" do
+ ENV["foo"] = "oof"
+ ENV["bar"] = "rab"
+ ENV.fetch_values("bar", "foo").should == ["rab", "oof"]
+ end
+
+ it "returns the default value from block" do
+ ENV["foo"] = "oof"
+ ENV.fetch_values("bar") { |key| "`#{key}' is not found" }.should == ["`bar' is not found"]
+ ENV.fetch_values("bar", "foo") { |key| "`#{key}' is not found" }.should == ["`bar' is not found", "oof"]
+ end
+
+ it "returns an empty array if no keys specified" do
+ ENV.fetch_values.should == []
+ end
+
+ it "raises KeyError when there is no matching key" do
+ ENV["foo"] = "oof"
+ ENV["bar"] = "rab"
+ -> {
+ ENV.fetch_values("bar", "y", "foo", "z")
+ }.should.raise(KeyError, 'key not found: "y"')
+ end
+
+ it "uses the locale encoding" do
+ ENV.fetch_values(ENV.keys.first).first.encoding.should == ENVSpecs.encoding
+ end
+
+ it "raises TypeError when a key is not coercible to String" do
+ ENV["foo"] = "oof"
+ -> { ENV.fetch_values("foo", Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+ end
+end
diff --git a/spec/ruby/core/env/filter_spec.rb b/spec/ruby/core/env/filter_spec.rb
new file mode 100644
index 0000000000..52f8b79a0b
--- /dev/null
+++ b/spec/ruby/core/env/filter_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+require_relative 'shared/select'
+
+describe "ENV.filter!" do
+ it_behaves_like :env_select!, :filter!
+ it_behaves_like :enumeratorized_with_origin_size, :filter!, ENV
+end
+
+describe "ENV.filter" do
+ it_behaves_like :env_select, :filter
+ it_behaves_like :enumeratorized_with_origin_size, :filter, ENV
+end
diff --git a/spec/ruby/core/env/fixtures/common.rb b/spec/ruby/core/env/fixtures/common.rb
new file mode 100644
index 0000000000..8d5057614d
--- /dev/null
+++ b/spec/ruby/core/env/fixtures/common.rb
@@ -0,0 +1,9 @@
+module ENVSpecs
+ def self.encoding
+ locale = Encoding.find('locale')
+ if ruby_version_is '3' and platform_is :windows
+ locale = Encoding::UTF_8
+ end
+ locale
+ end
+end
diff --git a/spec/ruby/core/env/has_key_spec.rb b/spec/ruby/core/env/has_key_spec.rb
new file mode 100644
index 0000000000..798668105d
--- /dev/null
+++ b/spec/ruby/core/env/has_key_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "ENV.has_key?" do
+ it_behaves_like :env_include, :has_key?
+end
diff --git a/spec/ruby/core/env/has_value_spec.rb b/spec/ruby/core/env/has_value_spec.rb
new file mode 100644
index 0000000000..a2bf3eb877
--- /dev/null
+++ b/spec/ruby/core/env/has_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/value'
+
+describe "ENV.has_value?" do
+ it_behaves_like :env_value, :has_value?
+end
diff --git a/spec/ruby/core/env/include_spec.rb b/spec/ruby/core/env/include_spec.rb
new file mode 100644
index 0000000000..3975f095ac
--- /dev/null
+++ b/spec/ruby/core/env/include_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "ENV.include?" do
+ it_behaves_like :env_include, :include?
+end
diff --git a/spec/ruby/core/env/inspect_spec.rb b/spec/ruby/core/env/inspect_spec.rb
new file mode 100644
index 0000000000..7dd92b120f
--- /dev/null
+++ b/spec/ruby/core/env/inspect_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "ENV.inspect" do
+
+ it "returns a String that looks like a Hash with real data" do
+ ENV["foo"] = "bar"
+ ENV.inspect.should =~ /\{.*"foo" *=> *"bar".*\}/
+ ENV.delete "foo"
+ end
+
+end
diff --git a/spec/ruby/core/env/invert_spec.rb b/spec/ruby/core/env/invert_spec.rb
new file mode 100644
index 0000000000..c095374d95
--- /dev/null
+++ b/spec/ruby/core/env/invert_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "ENV.invert" do
+ before :each do
+ ENV["foo"] = "bar"
+ end
+
+ after :each do
+ ENV.delete "foo"
+ end
+
+ it "returns a hash with ENV.keys as the values and vice versa" do
+ ENV.invert["bar"].should == "foo"
+ ENV["foo"].should == "bar"
+ end
+end
diff --git a/spec/ruby/core/env/keep_if_spec.rb b/spec/ruby/core/env/keep_if_spec.rb
new file mode 100644
index 0000000000..f24981e216
--- /dev/null
+++ b/spec/ruby/core/env/keep_if_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "ENV.keep_if" do
+ before :each do
+ @foo = ENV["foo"]
+ @bar = ENV["bar"]
+
+ ENV["foo"] = "0"
+ ENV["bar"] = "1"
+ end
+
+ after :each do
+ ENV["foo"] = @foo
+ ENV["bar"] = @bar
+ end
+
+ it "deletes pairs if the block returns false" do
+ ENV.keep_if { |k, v| !["foo", "bar"].include?(k) }
+ ENV["foo"].should == nil
+ ENV["bar"].should == nil
+ end
+
+ it "returns ENV when block given" do
+ ENV.keep_if { |k, v| !["foo", "bar"].include?(k) }.should.equal?(ENV)
+ end
+
+ it "returns ENV even if nothing deleted" do
+ ENV.keep_if { true }.should.equal?(ENV)
+ end
+
+ it "returns an Enumerator if no block given" do
+ ENV.keep_if.should.instance_of?(Enumerator)
+ end
+
+ it "deletes pairs through enumerator" do
+ enum = ENV.keep_if
+ enum.each { |k, v| !["foo", "bar"].include?(k) }
+ ENV["foo"].should == nil
+ ENV["bar"].should == nil
+ end
+
+ it "returns ENV from enumerator" do
+ enum = ENV.keep_if
+ enum.each { |k, v| !["foo", "bar"].include?(k) }.should.equal?(ENV)
+ end
+
+ it "returns ENV from enumerator even if nothing deleted" do
+ enum = ENV.keep_if
+ enum.each { true }.should.equal?(ENV)
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :keep_if, ENV
+end
diff --git a/spec/ruby/core/env/key_spec.rb b/spec/ruby/core/env/key_spec.rb
new file mode 100644
index 0000000000..677cf35216
--- /dev/null
+++ b/spec/ruby/core/env/key_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "ENV.key?" do
+ it_behaves_like :env_include, :key?
+end
+
+describe "ENV.key" do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "returns the index associated with the passed value" do
+ ENV["foo"] = "bar"
+ ENV.key("bar").should == "foo"
+ end
+
+ it "returns nil if the passed value is not found" do
+ ENV.delete("foo")
+ ENV.key("foo").should == nil
+ end
+
+ it "coerces the key element with #to_str" do
+ ENV["foo"] = "bar"
+ k = mock('key')
+ k.should_receive(:to_str).and_return("bar")
+ ENV.key(k).should == "foo"
+ end
+
+ it "raises TypeError if the argument is not a String and does not respond to #to_str" do
+ -> {
+ ENV.key(Object.new)
+ }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+end
diff --git a/spec/ruby/core/env/keys_spec.rb b/spec/ruby/core/env/keys_spec.rb
new file mode 100644
index 0000000000..b074a8f7c7
--- /dev/null
+++ b/spec/ruby/core/env/keys_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "ENV.keys" do
+
+ it "returns an array of the keys" do
+ ENV.keys.should == ENV.to_hash.keys
+ end
+
+ it "returns the keys in the locale encoding" do
+ ENV.keys.each do |key|
+ key.encoding.should == Encoding.find('locale')
+ end
+ end
+end
diff --git a/spec/ruby/core/env/length_spec.rb b/spec/ruby/core/env/length_spec.rb
new file mode 100644
index 0000000000..c6f9062892
--- /dev/null
+++ b/spec/ruby/core/env/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "ENV.length" do
+ it_behaves_like :env_length, :length
+end
diff --git a/spec/ruby/core/env/member_spec.rb b/spec/ruby/core/env/member_spec.rb
new file mode 100644
index 0000000000..9119022ae5
--- /dev/null
+++ b/spec/ruby/core/env/member_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "ENV.member?" do
+ it_behaves_like :env_include, :member?
+end
diff --git a/spec/ruby/core/env/merge_spec.rb b/spec/ruby/core/env/merge_spec.rb
new file mode 100644
index 0000000000..f10662cf79
--- /dev/null
+++ b/spec/ruby/core/env/merge_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/update'
+
+describe "ENV.merge!" do
+ it_behaves_like :env_update, :merge!
+end
diff --git a/spec/ruby/core/env/rassoc_spec.rb b/spec/ruby/core/env/rassoc_spec.rb
new file mode 100644
index 0000000000..e4ac5ce5e2
--- /dev/null
+++ b/spec/ruby/core/env/rassoc_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+
+describe "ENV.rassoc" do
+ before :each do
+ @foo = ENV["foo"]
+ @baz = ENV["baz"]
+ end
+
+ after :each do
+ ENV["foo"] = @foo
+ ENV["baz"] = @baz
+ end
+
+ it "returns an array of the key and value of the environment variable with the given value" do
+ ENV["foo"] = "bar"
+ ENV.rassoc("bar").should == ["foo", "bar"]
+ end
+
+ it "returns a single array even if there are multiple such environment variables" do
+ ENV["foo"] = "bar"
+ ENV["baz"] = "bar"
+ [
+ ["foo", "bar"],
+ ["baz", "bar"],
+ ].should.include?(ENV.rassoc("bar"))
+ end
+
+ it "returns nil if no environment variable with the given value exists" do
+ ENV.rassoc("bar").should == nil
+ end
+
+ it "returns the value element coerced with #to_str" do
+ ENV["foo"] = "bar"
+ v = mock('value')
+ v.should_receive(:to_str).and_return("bar")
+ ENV.rassoc(v).should == ["foo", "bar"]
+ end
+
+ it "returns nil if the argument is not a String and does not respond to #to_str" do
+ ENV.rassoc(Object.new).should == nil
+ end
+end
diff --git a/spec/ruby/core/env/rehash_spec.rb b/spec/ruby/core/env/rehash_spec.rb
new file mode 100644
index 0000000000..3782e4b727
--- /dev/null
+++ b/spec/ruby/core/env/rehash_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ENV.rehash" do
+ it "returns nil" do
+ ENV.rehash.should == nil
+ end
+end
diff --git a/spec/ruby/core/env/reject_spec.rb b/spec/ruby/core/env/reject_spec.rb
new file mode 100644
index 0000000000..4df864493d
--- /dev/null
+++ b/spec/ruby/core/env/reject_spec.rb
@@ -0,0 +1,101 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "ENV.reject!" do
+ before :each do
+ @foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @foo
+ end
+
+ it "rejects entries based on key" do
+ ENV["foo"] = "bar"
+ ENV.reject! { |k, v| k == "foo" }
+ ENV["foo"].should == nil
+ end
+
+ it "rejects entries based on value" do
+ ENV["foo"] = "bar"
+ ENV.reject! { |k, v| v == "bar" }
+ ENV["foo"].should == nil
+ end
+
+ it "returns itself or nil" do
+ ENV.reject! { false }.should == nil
+ ENV["foo"] = "bar"
+ ENV.reject! { |k, v| k == "foo" }.should.equal?(ENV)
+ ENV["foo"].should == nil
+ end
+
+ it "returns an Enumerator if called without a block" do
+ ENV["foo"] = "bar"
+ enum = ENV.reject!
+ enum.should.instance_of?(Enumerator)
+ enum.each { |k, v| k == "foo" }.should.equal?(ENV)
+ ENV["foo"].should == nil
+ end
+
+ it "doesn't raise if empty" do
+ orig = ENV.to_hash
+ begin
+ ENV.clear
+ -> { ENV.reject! }.should_not.raise(LocalJumpError)
+ ensure
+ ENV.replace orig
+ end
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :reject!, ENV
+end
+
+describe "ENV.reject" do
+ before :each do
+ @foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @foo
+ end
+
+ it "rejects entries based on key" do
+ ENV["foo"] = "bar"
+ e = ENV.reject { |k, v| k == "foo" }
+ e["foo"].should == nil
+ ENV["foo"].should == "bar"
+ ENV["foo"] = nil
+ end
+
+ it "rejects entries based on value" do
+ ENV["foo"] = "bar"
+ e = ENV.reject { |k, v| v == "bar" }
+ e["foo"].should == nil
+ ENV["foo"].should == "bar"
+ ENV["foo"] = nil
+ end
+
+ it "returns a Hash" do
+ ENV.reject { false }.should.is_a?(Hash)
+ end
+
+ it "returns an Enumerator if called without a block" do
+ ENV["foo"] = "bar"
+ enum = ENV.reject
+ enum.should.instance_of?(Enumerator)
+ enum.each { |k, v| k == "foo"}
+ ENV["foo"] = nil
+ end
+
+ it "doesn't raise if empty" do
+ orig = ENV.to_hash
+ begin
+ ENV.clear
+ -> { ENV.reject }.should_not.raise(LocalJumpError)
+ ensure
+ ENV.replace orig
+ end
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :reject, ENV
+end
diff --git a/spec/ruby/core/env/replace_spec.rb b/spec/ruby/core/env/replace_spec.rb
new file mode 100644
index 0000000000..27eb3e45dd
--- /dev/null
+++ b/spec/ruby/core/env/replace_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+
+describe "ENV.replace" do
+ before :each do
+ @orig = ENV.to_hash
+ ENV.delete("foo")
+ end
+
+ after :each do
+ ENV.replace(@orig)
+ end
+
+ it "replaces ENV with a Hash" do
+ ENV.replace("foo" => "0", "bar" => "1").should.equal?(ENV)
+ ENV.size.should == 2
+ ENV["foo"].should == "0"
+ ENV["bar"].should == "1"
+ end
+
+ it "raises TypeError if the argument is not a Hash" do
+ -> { ENV.replace(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into Hash")
+ ENV.to_hash.should == @orig
+ end
+
+ it "raises TypeError if a key is not a String" do
+ -> { ENV.replace(Object.new => "0") }.should.raise(TypeError, "no implicit conversion of Object into String")
+ ENV.to_hash.should == @orig
+ end
+
+ it "raises TypeError if a value is not a String" do
+ -> { ENV.replace("foo" => Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ ENV.to_hash.should == @orig
+ end
+
+ it "raises Errno::EINVAL when the key contains the '=' character" do
+ -> { ENV.replace("foo=" =>"bar") }.should.raise(Errno::EINVAL)
+ end
+
+ it "raises Errno::EINVAL when the key is an empty string" do
+ -> { ENV.replace("" => "bar") }.should.raise(Errno::EINVAL)
+ end
+
+ it "does not accept good data preceding an error" do
+ -> { ENV.replace("foo" => "1", Object.new => Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "does not accept good data following an error" do
+ -> { ENV.replace(Object.new => Object.new, "foo" => "0") }.should.raise(TypeError, "no implicit conversion of Object into String")
+ ENV.to_hash.should == @orig
+ end
+end
diff --git a/spec/ruby/core/env/select_spec.rb b/spec/ruby/core/env/select_spec.rb
new file mode 100644
index 0000000000..c3a76f4434
--- /dev/null
+++ b/spec/ruby/core/env/select_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+require_relative 'shared/select'
+
+describe "ENV.select!" do
+ it_behaves_like :env_select!, :select!
+ it_behaves_like :enumeratorized_with_origin_size, :select!, ENV
+end
+
+describe "ENV.select" do
+ it_behaves_like :env_select, :select
+ it_behaves_like :enumeratorized_with_origin_size, :select, ENV
+end
diff --git a/spec/ruby/core/env/shared/each.rb b/spec/ruby/core/env/shared/each.rb
new file mode 100644
index 0000000000..0661ca924c
--- /dev/null
+++ b/spec/ruby/core/env/shared/each.rb
@@ -0,0 +1,65 @@
+require_relative '../../enumerable/shared/enumeratorized'
+
+describe :env_each, shared: true do
+ it "returns each pair" do
+ orig = ENV.to_hash
+ e = []
+ begin
+ ENV.clear
+ ENV["foo"] = "bar"
+ ENV["baz"] = "boo"
+ ENV.send(@method) { |k, v| e << [k, v] }.should.equal?(ENV)
+ e.should.include?(["foo", "bar"])
+ e.should.include?(["baz", "boo"])
+ ensure
+ ENV.replace orig
+ end
+ end
+
+ it "returns an Enumerator if called without a block" do
+ enum = ENV.send(@method)
+ enum.should.instance_of?(Enumerator)
+ enum.each do |name, value|
+ ENV[name].should == value
+ end
+ end
+
+ before :all do
+ @object = ENV
+ end
+ it_should_behave_like :enumeratorized_with_origin_size
+
+ describe "with encoding" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::BINARY
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "uses the locale encoding when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+
+ ENV.send(@method) do |key, value|
+ key.should.be_locale_env
+ value.should.be_locale_env
+ end
+ end
+
+ it "transcodes from the locale encoding to Encoding.default_internal if set" do
+ Encoding.default_internal = internal = Encoding::IBM437
+
+ ENV.send(@method) do |key, value|
+ key.encoding.should.equal?(internal)
+ if value.ascii_only?
+ value.encoding.should.equal?(internal)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/env/shared/include.rb b/spec/ruby/core/env/shared/include.rb
new file mode 100644
index 0000000000..ceca02e3eb
--- /dev/null
+++ b/spec/ruby/core/env/shared/include.rb
@@ -0,0 +1,30 @@
+describe :env_include, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "returns true if ENV has the key" do
+ ENV["foo"] = "bar"
+ ENV.send(@method, "foo").should == true
+ end
+
+ it "returns false if ENV doesn't include the key" do
+ ENV.delete("foo")
+ ENV.send(@method, "foo").should == false
+ end
+
+ it "coerces the key with #to_str" do
+ ENV["foo"] = "bar"
+ k = mock('key')
+ k.should_receive(:to_str).and_return("foo")
+ ENV.send(@method, k).should == true
+ end
+
+ it "raises TypeError if the argument is not a String and does not respond to #to_str" do
+ -> { ENV.send(@method, Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+end
diff --git a/spec/ruby/core/env/shared/length.rb b/spec/ruby/core/env/shared/length.rb
new file mode 100644
index 0000000000..6d788a3f4a
--- /dev/null
+++ b/spec/ruby/core/env/shared/length.rb
@@ -0,0 +1,13 @@
+describe :env_length, shared: true do
+ it "returns the number of ENV entries" do
+ orig = ENV.to_hash
+ begin
+ ENV.clear
+ ENV["foo"] = "bar"
+ ENV["baz"] = "boo"
+ ENV.send(@method).should == 2
+ ensure
+ ENV.replace orig
+ end
+ end
+end
diff --git a/spec/ruby/core/env/shared/select.rb b/spec/ruby/core/env/shared/select.rb
new file mode 100644
index 0000000000..8ec648a637
--- /dev/null
+++ b/spec/ruby/core/env/shared/select.rb
@@ -0,0 +1,61 @@
+describe :env_select, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "returns a Hash of names and values for which block return true" do
+ ENV["foo"] = "bar"
+ (ENV.send(@method) { |k, v| k == "foo" }).should == { "foo" => "bar" }
+ end
+
+ it "returns an Enumerator when no block is given" do
+ enum = ENV.send(@method)
+ enum.should.instance_of?(Enumerator)
+ end
+
+ it "selects via the enumerator" do
+ enum = ENV.send(@method)
+ ENV["foo"] = "bar"
+ enum.each { |k, v| k == "foo" }.should == { "foo" => "bar"}
+ end
+end
+
+describe :env_select!, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "removes environment variables for which the block returns true" do
+ ENV["foo"] = "bar"
+ ENV.send(@method) { |k, v| k != "foo" }
+ ENV["foo"].should == nil
+ end
+
+ it "returns self if any changes were made" do
+ ENV["foo"] = "bar"
+ (ENV.send(@method) { |k, v| k != "foo" }).should == ENV
+ end
+
+ it "returns nil if no changes were made" do
+ (ENV.send(@method) { true }).should == nil
+ end
+
+ it "returns an Enumerator if called without a block" do
+ ENV.send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "selects via the enumerator" do
+ enum = ENV.send(@method)
+ ENV["foo"] = "bar"
+ enum.each { |k, v| k != "foo" }
+ ENV["foo"].should == nil
+ end
+end
diff --git a/spec/ruby/core/env/shared/store.rb b/spec/ruby/core/env/shared/store.rb
new file mode 100644
index 0000000000..388208a8ac
--- /dev/null
+++ b/spec/ruby/core/env/shared/store.rb
@@ -0,0 +1,60 @@
+describe :env_store, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "sets the environment variable to the given value" do
+ ENV.send(@method, "foo", "bar")
+ ENV["foo"].should == "bar"
+ end
+
+ it "returns the value" do
+ value = "bar"
+ ENV.send(@method, "foo", value).should.equal?(value)
+ end
+
+ it "deletes the environment variable when the value is nil" do
+ ENV["foo"] = "bar"
+ ENV.send(@method, "foo", nil)
+ ENV.key?("foo").should == false
+ end
+
+ it "coerces the key argument with #to_str" do
+ k = mock("key")
+ k.should_receive(:to_str).and_return("foo")
+ ENV.send(@method, k, "bar")
+ ENV["foo"].should == "bar"
+ end
+
+ it "coerces the value argument with #to_str" do
+ v = mock("value")
+ v.should_receive(:to_str).and_return("bar")
+ ENV.send(@method, "foo", v)
+ ENV["foo"].should == "bar"
+ end
+
+ it "raises TypeError when the key is not coercible to String" do
+ -> { ENV.send(@method, Object.new, "bar") }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises TypeError when the value is not coercible to String" do
+ -> { ENV.send(@method, "foo", Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises Errno::EINVAL when the key contains the '=' character" do
+ -> { ENV.send(@method, "foo=", "bar") }.should.raise(Errno::EINVAL)
+ end
+
+ it "raises Errno::EINVAL when the key is an empty string" do
+ -> { ENV.send(@method, "", "bar") }.should.raise(Errno::EINVAL)
+ end
+
+ it "does nothing when the key is not a valid environment variable key and the value is nil" do
+ ENV.send(@method, "foo=", nil)
+ ENV.key?("foo=").should == false
+ end
+end
diff --git a/spec/ruby/core/env/shared/to_hash.rb b/spec/ruby/core/env/shared/to_hash.rb
new file mode 100644
index 0000000000..7868690c38
--- /dev/null
+++ b/spec/ruby/core/env/shared/to_hash.rb
@@ -0,0 +1,33 @@
+describe :env_to_hash, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"]= @saved_foo
+ end
+
+ it "returns the ENV as a hash" do
+ ENV["foo"] = "bar"
+ h = ENV.send(@method)
+ h.should.instance_of?(Hash)
+ h["foo"].should == "bar"
+ end
+
+ it "uses the locale encoding for keys" do
+ ENV.send(@method).keys.each {|k| k.should.be_locale_env }
+ end
+
+ it "uses the locale encoding for values" do
+ ENV.send(@method).values.each {|k| k.should.be_locale_env }
+ end
+
+ it "duplicates the ENV when converting to a Hash" do
+ h = ENV.send(@method)
+ h.should_not.equal? ENV
+ h.size.should == ENV.size
+ h.each_pair do |k, v|
+ ENV[k].should == v
+ end
+ end
+end
diff --git a/spec/ruby/core/env/shared/update.rb b/spec/ruby/core/env/shared/update.rb
new file mode 100644
index 0000000000..112cc2505d
--- /dev/null
+++ b/spec/ruby/core/env/shared/update.rb
@@ -0,0 +1,104 @@
+describe :env_update, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ @saved_bar = ENV["bar"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ ENV["bar"] = @saved_bar
+ end
+
+ it "adds the parameter hash to ENV, returning ENV" do
+ ENV.send(@method, "foo" => "0", "bar" => "1").should.equal?(ENV)
+ ENV["foo"].should == "0"
+ ENV["bar"].should == "1"
+ end
+
+ it "adds the multiple parameter hashes to ENV, returning ENV" do
+ ENV.send(@method, {"foo" => "multi1"}, {"bar" => "multi2"}).should.equal?(ENV)
+ ENV["foo"].should == "multi1"
+ ENV["bar"].should == "multi2"
+ end
+
+ it "returns ENV when no block given" do
+ ENV.send(@method, {"foo" => "0", "bar" => "1"}).should.equal?(ENV)
+ end
+
+ it "yields key, the old value and the new value when replacing an entry" do
+ ENV.send @method, {"foo" => "0", "bar" => "3"}
+ a = []
+ ENV.send @method, {"foo" => "1", "bar" => "4"} do |key, old, new|
+ a << [key, old, new]
+ new
+ end
+ a[0].should == ["foo", "0", "1"]
+ a[1].should == ["bar", "3", "4"]
+ end
+
+ it "yields key, the old value and the new value when replacing an entry" do
+ ENV.send @method, {"foo" => "0", "bar" => "3"}
+ ENV.send @method, {"foo" => "1", "bar" => "4"} do |key, old, new|
+ (new.to_i + 1).to_s
+ end
+ ENV["foo"].should == "2"
+ ENV["bar"].should == "5"
+ end
+
+ # BUG: https://bugs.ruby-lang.org/issues/16192
+ it "does not evaluate the block when the name is new" do
+ ENV.delete("bar")
+ ENV.send @method, {"foo" => "0"}
+ ENV.send(@method, "bar" => "1") { |key, old, new| fail "Should not get here" }
+ ENV["bar"].should == "1"
+ end
+
+ # BUG: https://bugs.ruby-lang.org/issues/16192
+ it "does not use the block's return value as the value when the name is new" do
+ ENV.delete("bar")
+ ENV.send @method, {"foo" => "0"}
+ ENV.send(@method, "bar" => "1") { |key, old, new| "Should not use this value" }
+ ENV["foo"].should == "0"
+ ENV["bar"].should == "1"
+ end
+
+ it "returns ENV when block given" do
+ ENV.send(@method, {"foo" => "0", "bar" => "1"}){}.should.equal?(ENV)
+ end
+
+ it "raises TypeError when a name is not coercible to String" do
+ -> { ENV.send @method, Object.new => "0" }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises TypeError when a value is not coercible to String" do
+ -> { ENV.send @method, "foo" => Object.new }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises Errno::EINVAL when a name contains the '=' character" do
+ -> { ENV.send(@method, "foo=" => "bar") }.should.raise(Errno::EINVAL)
+ end
+
+ it "raises Errno::EINVAL when a name is an empty string" do
+ -> { ENV.send(@method, "" => "bar") }.should.raise(Errno::EINVAL)
+ end
+
+ it "updates good data preceding an error" do
+ ENV["foo"] = "0"
+ begin
+ ENV.send @method, {"foo" => "2", Object.new => "1"}
+ rescue TypeError
+ ensure
+ ENV["foo"].should == "2"
+ end
+ end
+
+ it "does not update good data following an error" do
+ ENV["foo"] = "0"
+ begin
+ ENV.send @method, {Object.new => "1", "foo" => "2"}
+ rescue TypeError
+ ensure
+ ENV["foo"].should == "0"
+ end
+ end
+end
diff --git a/spec/ruby/core/env/shared/value.rb b/spec/ruby/core/env/shared/value.rb
new file mode 100644
index 0000000000..c2b5025465
--- /dev/null
+++ b/spec/ruby/core/env/shared/value.rb
@@ -0,0 +1,29 @@
+describe :env_value, shared: true do
+ before :each do
+ @saved_foo = ENV["foo"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ end
+
+ it "returns true if ENV has the value" do
+ ENV["foo"] = "bar"
+ ENV.send(@method, "bar").should == true
+ end
+
+ it "returns false if ENV doesn't have the value" do
+ ENV.send(@method, "foo").should == false
+ end
+
+ it "coerces the value element with #to_str" do
+ ENV["foo"] = "bar"
+ v = mock('value')
+ v.should_receive(:to_str).and_return("bar")
+ ENV.send(@method, v).should == true
+ end
+
+ it "returns nil if the argument is not a String and does not respond to #to_str" do
+ ENV.send(@method, Object.new).should == nil
+ end
+end
diff --git a/spec/ruby/core/env/shift_spec.rb b/spec/ruby/core/env/shift_spec.rb
new file mode 100644
index 0000000000..52bd0f9139
--- /dev/null
+++ b/spec/ruby/core/env/shift_spec.rb
@@ -0,0 +1,47 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "ENV.shift" do
+ before :each do
+ @orig = ENV.to_hash
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::BINARY
+ ENV.replace({"FOO"=>"BAR"})
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ ENV.replace @orig
+ end
+
+ it "returns a pair and deletes it" do
+ ENV.should.has_key?("FOO")
+ pair = ENV.shift
+ pair.should == ["FOO", "BAR"]
+ ENV.should_not.has_key?("FOO")
+ end
+
+ it "returns nil if ENV.empty?" do
+ ENV.clear
+ ENV.shift.should == nil
+ end
+
+ it "uses the locale encoding if Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+
+ pair = ENV.shift
+ pair.first.encoding.should.equal?(ENVSpecs.encoding)
+ pair.last.encoding.should.equal?(ENVSpecs.encoding)
+ end
+
+ it "transcodes from the locale encoding to Encoding.default_internal if set" do
+ Encoding.default_internal = Encoding::IBM437
+
+ pair = ENV.shift
+ pair.first.encoding.should.equal?(Encoding::IBM437)
+ pair.last.encoding.should.equal?(Encoding::IBM437)
+ end
+end
diff --git a/spec/ruby/core/env/size_spec.rb b/spec/ruby/core/env/size_spec.rb
new file mode 100644
index 0000000000..7c8072481e
--- /dev/null
+++ b/spec/ruby/core/env/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "ENV.size" do
+ it_behaves_like :env_length, :size
+end
diff --git a/spec/ruby/core/env/slice_spec.rb b/spec/ruby/core/env/slice_spec.rb
new file mode 100644
index 0000000000..4c0416547d
--- /dev/null
+++ b/spec/ruby/core/env/slice_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "ENV.slice" do
+ before :each do
+ @saved_foo = ENV["foo"]
+ @saved_bar = ENV["bar"]
+ ENV["foo"] = "0"
+ ENV["bar"] = "1"
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ ENV["bar"] = @saved_bar
+ end
+
+ it "returns a hash of the given environment variable names and their values" do
+ ENV.slice("foo", "bar").should == {"foo" => "0", "bar" => "1"}
+ end
+
+ it "ignores each String that is not an environment variable name" do
+ ENV.slice("foo", "boo", "bar").should == {"foo" => "0", "bar" => "1"}
+ end
+
+ it "returns the values for the keys coerced with #to_str, but keeps the original objects as result keys" do
+ foo = mock('key 1')
+ foo.should_receive(:to_str).and_return("foo")
+ boo = mock('key 2')
+ boo.should_receive(:to_str).and_return("boo")
+ bar = mock('key 3')
+ bar.should_receive(:to_str).and_return("bar")
+ ENV.slice(foo, boo, bar).should == {foo => "0", bar => "1"}
+ end
+
+ it "raises TypeError if any argument is not a String and does not respond to #to_str" do
+ -> { ENV.slice(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+end
diff --git a/spec/ruby/core/env/spec_helper.rb b/spec/ruby/core/env/spec_helper.rb
new file mode 100644
index 0000000000..470ffa58bc
--- /dev/null
+++ b/spec/ruby/core/env/spec_helper.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+locale_env_matcher = Class.new do
+ def initialize(name = 'locale')
+ encoding = Encoding.find(name)
+ @encodings = (encoding = Encoding::US_ASCII) ?
+ [encoding, Encoding::ASCII_8BIT] : [encoding]
+ end
+
+ def matches?(actual)
+ @actual = actual = actual.encoding
+ @encodings.include?(actual)
+ end
+
+ def failure_message
+ ["Expected #{@actual} to be #{@encodings.join(' or ')}"]
+ end
+
+ def negative_failure_message
+ ["Expected #{@actual} not to be #{@encodings.join(' or ')}"]
+ end
+end
+
+String.__send__(:define_method, :be_locale_env) do |expected = 'locale'|
+ locale_env_matcher.new(expected)
+end
diff --git a/spec/ruby/core/env/store_spec.rb b/spec/ruby/core/env/store_spec.rb
new file mode 100644
index 0000000000..b4700e0a96
--- /dev/null
+++ b/spec/ruby/core/env/store_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/store'
+
+describe "ENV.store" do
+ it_behaves_like :env_store, :store
+end
diff --git a/spec/ruby/core/env/to_a_spec.rb b/spec/ruby/core/env/to_a_spec.rb
new file mode 100644
index 0000000000..2b1649281f
--- /dev/null
+++ b/spec/ruby/core/env/to_a_spec.rb
@@ -0,0 +1,21 @@
+require_relative 'spec_helper'
+
+describe "ENV.to_a" do
+
+ it "returns the ENV as an array" do
+ a = ENV.to_a
+ a.is_a?(Array).should == true
+ a.size.should == ENV.size
+ a.each { |k,v| ENV[k].should == v }
+
+ a.first.should.is_a?(Array)
+ a.first.size.should == 2
+ end
+
+ it "returns the entries in the locale encoding" do
+ ENV.to_a.each do |key, value|
+ key.should.be_locale_env
+ value.should.be_locale_env
+ end
+ end
+end
diff --git a/spec/ruby/core/env/to_h_spec.rb b/spec/ruby/core/env/to_h_spec.rb
new file mode 100644
index 0000000000..7e0fef2120
--- /dev/null
+++ b/spec/ruby/core/env/to_h_spec.rb
@@ -0,0 +1,70 @@
+require_relative 'spec_helper'
+require_relative 'shared/to_hash'
+
+describe "ENV.to_h" do
+ it_behaves_like :env_to_hash, :to_h
+
+ context "with block" do
+ before do
+ @orig_hash = ENV.to_hash
+ end
+
+ after do
+ ENV.replace @orig_hash
+ end
+
+ it "converts [key, value] pairs returned by the block to a hash" do
+ ENV.replace("a" => "b", "c" => "d")
+ ENV.to_h { |k, v| [k, v.upcase] }.should == { 'a' => "B", 'c' => "D" }
+ end
+
+ it "passes to a block each pair's key and value as separate arguments" do
+ ENV.replace("a" => "b", "c" => "d")
+
+ ScratchPad.record []
+ ENV.to_h { |k, v| ScratchPad << [k, v]; [k, v] }
+ ScratchPad.recorded.sort.should == [["a", "b"], ["c", "d"]]
+
+ ScratchPad.record []
+ ENV.to_h { |*args| ScratchPad << args; [args[0], args[1]] }
+ ScratchPad.recorded.sort.should == [["a", "b"], ["c", "d"]]
+ end
+
+ it "does not require the array elements to be strings" do
+ ENV.replace("a" => "b", "c" => "d")
+ ENV.to_h { |k, v| [k.to_sym, v.to_sym] }.should == { :a => :b, :c => :d }
+ end
+
+ it "raises ArgumentError if block returns longer or shorter array" do
+ -> do
+ ENV.to_h { |k, v| [k, v.upcase, 1] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+
+ -> do
+ ENV.to_h { |k, v| [k] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+ end
+
+ it "raises TypeError if block returns something other than Array" do
+ -> do
+ ENV.to_h { |k, v| "not-array" }
+ end.should.raise(TypeError, /wrong element type String/)
+ end
+
+ it "coerces returned pair to Array with #to_ary" do
+ x = mock('x')
+ x.stub!(:to_ary).and_return([:b, 'b'])
+
+ ENV.to_h { |k| x }.should == { :b => 'b' }
+ end
+
+ it "does not coerce returned pair to Array with #to_a" do
+ x = mock('x')
+ x.stub!(:to_a).and_return([:b, 'b'])
+
+ -> do
+ ENV.to_h { |k| x }
+ end.should.raise(TypeError, /wrong element type MockObject/)
+ end
+ end
+end
diff --git a/spec/ruby/core/env/to_hash_spec.rb b/spec/ruby/core/env/to_hash_spec.rb
new file mode 100644
index 0000000000..306572c353
--- /dev/null
+++ b/spec/ruby/core/env/to_hash_spec.rb
@@ -0,0 +1,6 @@
+require_relative 'spec_helper'
+require_relative 'shared/to_hash'
+
+describe "ENV.to_hash" do
+ it_behaves_like :env_to_hash, :to_hash
+end
diff --git a/spec/ruby/core/env/to_s_spec.rb b/spec/ruby/core/env/to_s_spec.rb
new file mode 100644
index 0000000000..0bd92cf217
--- /dev/null
+++ b/spec/ruby/core/env/to_s_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "ENV.to_s" do
+ it "returns \"ENV\"" do
+ ENV.to_s.should == "ENV"
+ end
+end
diff --git a/spec/ruby/core/env/update_spec.rb b/spec/ruby/core/env/update_spec.rb
new file mode 100644
index 0000000000..95a8a2eb49
--- /dev/null
+++ b/spec/ruby/core/env/update_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/update'
+
+describe "ENV.update" do
+ it_behaves_like :env_update, :update
+end
diff --git a/spec/ruby/core/env/value_spec.rb b/spec/ruby/core/env/value_spec.rb
new file mode 100644
index 0000000000..906e86ab39
--- /dev/null
+++ b/spec/ruby/core/env/value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/value'
+
+describe "ENV.value?" do
+ it_behaves_like :env_value, :value?
+end
diff --git a/spec/ruby/core/env/values_at_spec.rb b/spec/ruby/core/env/values_at_spec.rb
new file mode 100644
index 0000000000..4a733f4ed5
--- /dev/null
+++ b/spec/ruby/core/env/values_at_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "ENV.values_at" do
+ before :each do
+ @saved_foo = ENV["foo"]
+ @saved_bar = ENV["bar"]
+ end
+
+ after :each do
+ ENV["foo"] = @saved_foo
+ ENV["bar"] = @saved_bar
+ end
+
+ it "returns an array of the values corresponding to the given keys" do
+ ENV["foo"] = "oof"
+ ENV["bar"] = "rab"
+ ENV.values_at("bar", "foo").should == ["rab", "oof"]
+ end
+
+ it "returns an empty array if no keys specified" do
+ ENV.values_at.should == []
+ end
+
+ it "returns nil for each key that is not a name" do
+ ENV["foo"] = "oof"
+ ENV["bar"] = "rab"
+ ENV.values_at("x", "bar", "y", "foo", "z").should == [nil, "rab", nil, "oof", nil]
+ end
+
+ it "uses the locale encoding" do
+ ENV.values_at(ENV.keys.first).first.encoding.should == ENVSpecs.encoding
+ end
+
+ it "raises TypeError when a key is not coercible to String" do
+ -> { ENV.values_at("foo", Object.new) }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+end
diff --git a/spec/ruby/core/env/values_spec.rb b/spec/ruby/core/env/values_spec.rb
new file mode 100644
index 0000000000..71bc877d31
--- /dev/null
+++ b/spec/ruby/core/env/values_spec.rb
@@ -0,0 +1,14 @@
+require_relative 'spec_helper'
+
+describe "ENV.values" do
+
+ it "returns an array of the values" do
+ ENV.values.should == ENV.to_hash.values
+ end
+
+ it "uses the locale encoding" do
+ ENV.values.each do |value|
+ value.should.be_locale_env
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/backtrace_locations_spec.rb b/spec/ruby/core/exception/backtrace_locations_spec.rb
new file mode 100644
index 0000000000..62eab8a0f3
--- /dev/null
+++ b/spec/ruby/core/exception/backtrace_locations_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#backtrace_locations" do
+ before :each do
+ @backtrace = ExceptionSpecs::Backtrace.backtrace_locations
+ end
+
+ it "returns nil if no backtrace was set" do
+ Exception.new.backtrace_locations.should == nil
+ end
+
+ it "returns an Array" do
+ @backtrace.should.instance_of?(Array)
+ end
+
+ it "sets each element to a Thread::Backtrace::Location" do
+ @backtrace.each {|l| l.should.instance_of?(Thread::Backtrace::Location)}
+ end
+
+ it "produces a backtrace for an exception captured using $!" do
+ exception = begin
+ raise
+ rescue RuntimeError
+ $!
+ end
+
+ exception.backtrace_locations.first.path.should =~ /backtrace_locations_spec/
+ end
+
+ it "returns an Array that can be updated" do
+ begin
+ raise
+ rescue RuntimeError => e
+ e.backtrace_locations.unshift "backtrace first"
+ e.backtrace_locations[0].should == "backtrace first"
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/backtrace_spec.rb b/spec/ruby/core/exception/backtrace_spec.rb
new file mode 100644
index 0000000000..a4a8272030
--- /dev/null
+++ b/spec/ruby/core/exception/backtrace_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#backtrace" do
+ before :each do
+ @backtrace = ExceptionSpecs::Backtrace.backtrace
+ end
+
+ it "returns nil if no backtrace was set" do
+ Exception.new.backtrace.should == nil
+ end
+
+ it "returns an Array" do
+ @backtrace.should.instance_of?(Array)
+ end
+
+ it "sets each element to a String" do
+ @backtrace.each {|l| l.should.instance_of?(String)}
+ end
+
+ it "includes the filename of the location where self raised in the first element" do
+ @backtrace.first.should =~ /common\.rb/
+ end
+
+ it "includes the line number of the location where self raised in the first element" do
+ @backtrace.first.should =~ /:7:in /
+ end
+
+ it "includes the name of the method from where self raised in the first element" do
+ @backtrace.first.should =~ /in [`'](?:ExceptionSpecs::Backtrace\.)?backtrace'/
+ end
+
+ it "includes the filename of the location immediately prior to where self raised in the second element" do
+ @backtrace[1].should =~ /backtrace_spec\.rb/
+ end
+
+ it "includes the line number of the location immediately prior to where self raised in the second element" do
+ @backtrace[1].should =~ /:6(:in )?/
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "contains lines of the same format for each prior position in the stack" do
+ @backtrace[2..-1].each do |line|
+ # This regexp is deliberately imprecise to account for the need to abstract out
+ # the paths of the included mspec files and the desire to avoid specifying in any
+ # detail what the in `...' portion looks like.
+ line.should =~ /^.+:\d+:in `[^`]+'$/
+ end
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "contains lines of the same format for each prior position in the stack" do
+ @backtrace[2..-1].each do |line|
+ # This regexp is deliberately imprecise to account for the need to abstract out
+ # the paths of the included mspec files and the desire to avoid specifying in any
+ # detail what the in '...' portion looks like.
+ line.should =~ /^.+:\d+:in '[^`]+'$/
+ end
+ end
+ end
+
+ it "captures the backtrace for an exception into $!" do
+ exception = begin
+ raise
+ rescue RuntimeError
+ $!
+ end
+
+ exception.backtrace.first.should =~ /backtrace_spec/
+ end
+
+ it "captures the backtrace for an exception into $@" do
+ backtrace = begin
+ raise
+ rescue RuntimeError
+ $@
+ end
+
+ backtrace.first.should =~ /backtrace_spec/
+ end
+
+ it "returns an Array that can be updated" do
+ begin
+ raise
+ rescue RuntimeError => e
+ e.backtrace.unshift "backtrace first"
+ e.backtrace[0].should == "backtrace first"
+ end
+ end
+
+ it "returns the same array after duping" do
+ begin
+ raise
+ rescue RuntimeError => err
+ bt = err.backtrace
+ err.dup.backtrace.should.equal?(bt)
+
+ new_bt = ['hi']
+ err.set_backtrace new_bt
+
+ err.backtrace.should == new_bt
+ err.dup.backtrace.should.equal?(new_bt)
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/case_compare_spec.rb b/spec/ruby/core/exception/case_compare_spec.rb
new file mode 100644
index 0000000000..5fd11ae741
--- /dev/null
+++ b/spec/ruby/core/exception/case_compare_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "SystemCallError.===" do
+ before :all do
+ @example_errno_class = Errno::EINVAL
+ @example_errno = @example_errno_class::Errno
+ end
+
+ it "returns true for an instance of the same class" do
+ Errno::EINVAL.should === Errno::EINVAL.new
+ end
+
+ it "returns true if errnos same" do
+ e = SystemCallError.new('foo', @example_errno)
+ @example_errno_class.===(e).should == true
+ end
+
+ it "returns false if errnos different" do
+ e = SystemCallError.new('foo', @example_errno + 1)
+ @example_errno_class.===(e).should == false
+ end
+
+ it "returns false if arg is not kind of SystemCallError" do
+ e = Object.new
+ @example_errno_class.===(e).should == false
+ end
+
+ it "returns true if receiver is generic and arg is kind of SystemCallError" do
+ e = SystemCallError.new('foo', @example_errno)
+ SystemCallError.===(e).should == true
+ end
+
+ it "returns false if receiver is generic and arg is not kind of SystemCallError" do
+ e = Object.new
+ SystemCallError.===(e).should == false
+ end
+end
diff --git a/spec/ruby/core/exception/cause_spec.rb b/spec/ruby/core/exception/cause_spec.rb
new file mode 100644
index 0000000000..cfc15bdda3
--- /dev/null
+++ b/spec/ruby/core/exception/cause_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+
+describe "Exception#cause" do
+ it "returns the active exception when an exception is raised" do
+ begin
+ raise Exception, "the cause"
+ rescue Exception => cause
+ -> {
+ raise RuntimeError, "the consequence"
+ }.should.raise(RuntimeError, "the consequence", cause:)
+ end
+ end
+
+ it "is set for user errors caused by internal errors" do
+ begin
+ 1 / 0
+ rescue => cause
+ -> { raise "foo" }.should.raise(RuntimeError, cause:)
+ end
+ end
+
+ it "is set for internal errors caused by user errors" do
+ cause = RuntimeError.new "cause"
+ begin
+ raise cause
+ rescue
+ -> { 1 / 0 }.should.raise(ZeroDivisionError, cause:)
+ end
+ end
+
+ it "is not set to the exception itself when it is re-raised" do
+ begin
+ raise RuntimeError
+ rescue RuntimeError => e
+ -> { raise e }.should.raise(RuntimeError, cause: nil)
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/detailed_message_spec.rb b/spec/ruby/core/exception/detailed_message_spec.rb
new file mode 100644
index 0000000000..9df164a1cf
--- /dev/null
+++ b/spec/ruby/core/exception/detailed_message_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#detailed_message" do
+ it "returns decorated message" do
+ RuntimeError.new("new error").detailed_message.should == "new error (RuntimeError)"
+ end
+
+ it "is called by #full_message to allow message customization" do
+ exception = Exception.new("new error")
+ def exception.detailed_message(**)
+ "<prefix>#{message}<suffix>"
+ end
+ exception.full_message(highlight: false).should.include? "<prefix>new error<suffix>"
+ end
+
+ it "returns just a message if exception class is anonymous" do
+ Class.new(RuntimeError).new("message").detailed_message.should == "message"
+ end
+
+ it "returns 'unhandled exception' for an instance of RuntimeError with empty message" do
+ RuntimeError.new("").detailed_message.should == "unhandled exception"
+ end
+
+ it "returns just class name for an instance other than RuntimeError with empty message" do
+ DetailedMessageSpec::C.new("").detailed_message.should == "DetailedMessageSpec::C"
+ StandardError.new("").detailed_message.should == "StandardError"
+ end
+
+ it "returns a generated class name for an instance of RuntimeError anonymous subclass with empty message" do
+ klass = Class.new(RuntimeError)
+ klass.new("").detailed_message.should =~ /\A#<Class:0x\h+>\z/
+ end
+
+ it "accepts highlight keyword argument and adds escape control sequences" do
+ RuntimeError.new("new error").detailed_message(highlight: true).should == "\e[1mnew error (\e[1;4mRuntimeError\e[m\e[1m)\e[m"
+ end
+
+ it "accepts highlight keyword argument and adds escape control sequences for an instance of RuntimeError with empty message" do
+ RuntimeError.new("").detailed_message(highlight: true).should == "\e[1;4munhandled exception\e[m"
+ end
+
+ it "accepts highlight keyword argument and adds escape control sequences for an instance other than RuntimeError with empty message" do
+ StandardError.new("").detailed_message(highlight: true).should == "\e[1;4mStandardError\e[m"
+ end
+
+ it "allows and ignores other keyword arguments" do
+ RuntimeError.new("new error").detailed_message(foo: true).should == "new error (RuntimeError)"
+ end
+end
diff --git a/spec/ruby/core/exception/dup_spec.rb b/spec/ruby/core/exception/dup_spec.rb
new file mode 100644
index 0000000000..b53ad79bf3
--- /dev/null
+++ b/spec/ruby/core/exception/dup_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#dup" do
+ before :each do
+ @obj = ExceptionSpecs::InitializeException.new("my exception")
+ end
+
+ it "calls #initialize_copy on the new instance" do
+ dup = @obj.dup
+ ScratchPad.recorded.should_not == @obj.object_id
+ ScratchPad.recorded.should == dup.object_id
+ end
+
+ it "copies instance variables" do
+ dup = @obj.dup
+ dup.ivar.should == 1
+ end
+
+ it "does not copy singleton methods" do
+ def @obj.special() :the_one end
+ dup = @obj.dup
+ -> { dup.special }.should.raise(NameError)
+ end
+
+ it "does not copy modules included in the singleton class" do
+ class << @obj
+ include ExceptionSpecs::ExceptionModule
+ end
+
+ dup = @obj.dup
+ -> { dup.repr }.should.raise(NameError)
+ end
+
+ it "does not copy constants defined in the singleton class" do
+ class << @obj
+ CLONE = :clone
+ end
+
+ dup = @obj.dup
+ -> { class << dup; CLONE; end }.should.raise(NameError)
+ end
+
+ it "does copy the message" do
+ @obj.dup.message.should == @obj.message
+ end
+
+ it "does copy the backtrace" do
+ begin
+ # Explicitly raise so a backtrace is associated with the exception.
+ # It's tempting to call `set_backtrace` instead, but that complicates
+ # the test because it might affect other state (e.g., instance variables)
+ # on some implementations.
+ raise ExceptionSpecs::InitializeException.new("my exception")
+ rescue => e
+ @obj = e
+ end
+
+ @obj.dup.backtrace.should == @obj.backtrace
+ end
+
+ it "does copy the cause" do
+ begin
+ raise StandardError
+ rescue StandardError => cause
+ begin
+ raise RuntimeError
+ rescue RuntimeError => e
+ e.cause.should.equal?(cause)
+ e.dup.cause.should.equal?(cause)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/equal_value_spec.rb b/spec/ruby/core/exception/equal_value_spec.rb
new file mode 100644
index 0000000000..b76b3bcd4a
--- /dev/null
+++ b/spec/ruby/core/exception/equal_value_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#==" do
+ it "returns true if both exceptions are the same object" do
+ e = ArgumentError.new
+ e.should == e
+ end
+
+ it "returns true if one exception is the dup'd copy of the other" do
+ e = ArgumentError.new
+ e.should == e.dup
+ end
+
+ it "returns true if both exceptions have the same class, no message, and no backtrace" do
+ RuntimeError.new.should == RuntimeError.new
+ end
+
+ it "returns true if both exceptions have the same class, the same message, and no backtrace" do
+ TypeError.new("message").should == TypeError.new("message")
+ end
+
+ it "returns true if both exceptions have the same class, the same message, and the same backtrace" do
+ one = TypeError.new("message")
+ one.set_backtrace [__dir__]
+ two = TypeError.new("message")
+ two.set_backtrace [__dir__]
+ one.should == two
+ end
+
+ it "returns false if the two exceptions inherit from Exception but have different classes" do
+ one = RuntimeError.new("message")
+ one.set_backtrace [__dir__]
+ one.should.is_a?(Exception)
+ two = TypeError.new("message")
+ two.set_backtrace [__dir__]
+ two.should.is_a?(Exception)
+ one.should_not == two
+ end
+
+ it "returns true if the two objects subclass Exception and have the same message and backtrace" do
+ one = ExceptionSpecs::UnExceptional.new
+ two = ExceptionSpecs::UnExceptional.new
+ one.message.should == two.message
+ two.backtrace.should == two.backtrace
+ one.should == two
+ end
+
+ it "returns false if the argument is not an Exception" do
+ ArgumentError.new.should_not == String.new
+ end
+
+ it "returns false if the two exceptions differ only in their backtrace" do
+ one = RuntimeError.new("message")
+ one.set_backtrace [__dir__]
+ two = RuntimeError.new("message")
+ two.set_backtrace nil
+ one.should_not == two
+ end
+
+ it "returns false if the two exceptions differ only in their message" do
+ one = RuntimeError.new("message")
+ one.set_backtrace [__dir__]
+ two = RuntimeError.new("message2")
+ two.set_backtrace [__dir__]
+ one.should_not == two
+ end
+end
diff --git a/spec/ruby/core/exception/errno_spec.rb b/spec/ruby/core/exception/errno_spec.rb
new file mode 100644
index 0000000000..36beae9976
--- /dev/null
+++ b/spec/ruby/core/exception/errno_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Errno::EINVAL.new" do
+ it "can be called with no arguments" do
+ exc = Errno::EINVAL.new
+ exc.should.instance_of?(Errno::EINVAL)
+ exc.errno.should == Errno::EINVAL::Errno
+ exc.message.should == "Invalid argument"
+ end
+
+ it "accepts an optional custom message" do
+ exc = Errno::EINVAL.new('custom message')
+ exc.should.instance_of?(Errno::EINVAL)
+ exc.errno.should == Errno::EINVAL::Errno
+ exc.message.should == "Invalid argument - custom message"
+ end
+
+ it "accepts an optional custom message and location" do
+ exc = Errno::EINVAL.new('custom message', 'location')
+ exc.should.instance_of?(Errno::EINVAL)
+ exc.errno.should == Errno::EINVAL::Errno
+ exc.message.should == "Invalid argument @ location - custom message"
+ end
+end
+
+describe "Errno::EMFILE" do
+ it "can be subclassed" do
+ ExceptionSpecs::EMFILESub = Class.new(Errno::EMFILE)
+ exc = ExceptionSpecs::EMFILESub.new
+ exc.should.instance_of?(ExceptionSpecs::EMFILESub)
+ ensure
+ ExceptionSpecs.send(:remove_const, :EMFILESub)
+ end
+end
+
+describe "Errno::EAGAIN" do
+ # From http://jira.codehaus.org/browse/JRUBY-4747
+ it "is the same class as Errno::EWOULDBLOCK if they represent the same errno value" do
+ if Errno::EAGAIN::Errno == Errno::EWOULDBLOCK::Errno
+ Errno::EAGAIN.should == Errno::EWOULDBLOCK
+ else
+ Errno::EAGAIN.should_not == Errno::EWOULDBLOCK
+ end
+ end
+end
+
+describe "Errno::ENOTSUP" do
+ it "is defined" do
+ Errno.should.const_defined?(:ENOTSUP, false)
+ end
+
+ it "is the same class as Errno::EOPNOTSUPP if they represent the same errno value" do
+ if Errno::ENOTSUP::Errno == Errno::EOPNOTSUPP::Errno
+ Errno::ENOTSUP.should == Errno::EOPNOTSUPP
+ else
+ Errno::ENOTSUP.should_not == Errno::EOPNOTSUPP
+ end
+ end
+end
+
+describe "Errno::ENOENT" do
+ it "lets subclasses inherit the default error message" do
+ c = Class.new(Errno::ENOENT)
+ raise c, "custom message"
+ rescue => e
+ e.message.should == "No such file or directory - custom message"
+ end
+end
diff --git a/spec/ruby/core/exception/exception_spec.rb b/spec/ruby/core/exception/exception_spec.rb
new file mode 100644
index 0000000000..f5424cdabd
--- /dev/null
+++ b/spec/ruby/core/exception/exception_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/new'
+
+describe "Exception.exception" do
+ it_behaves_like :exception_new, :exception
+end
+
+describe "Exception#exception" do
+ it "returns self when passed no argument" do
+ e = RuntimeError.new
+ e.should == e.exception
+ end
+
+ it "returns self when passed self as an argument" do
+ e = RuntimeError.new
+ e.should == e.exception(e)
+ end
+
+ it "returns an exception of the same class as self with the message given as argument" do
+ e = RuntimeError.new
+ e2 = e.exception("message")
+ e2.should.instance_of?(RuntimeError)
+ e2.message.should == "message"
+ end
+
+ it "when raised will be rescued as the new exception" do
+ begin
+ begin
+ raised_first = StandardError.new('first')
+ raise raised_first
+ rescue => caught_first
+ raised_second = raised_first.exception('second')
+ raise raised_second
+ end
+ rescue => caught_second
+ end
+
+ raised_first.should == caught_first
+ raised_second.should == caught_second
+ end
+
+ it "captures an exception into $!" do
+ exception = begin
+ raise
+ rescue RuntimeError
+ $!
+ end
+
+ exception.class.should == RuntimeError
+ exception.message.should == ""
+ exception.backtrace.first.should =~ /exception_spec/
+ end
+
+ class CustomArgumentError < StandardError
+ attr_reader :val
+ def initialize(val)
+ @val = val
+ end
+ end
+
+ it "returns an exception of the same class as self with the message given as argument, but without reinitializing" do
+ e = CustomArgumentError.new(:boom)
+ e2 = e.exception("message")
+ e2.should.instance_of?(CustomArgumentError)
+ e2.val.should == :boom
+ e2.message.should == "message"
+ end
+end
diff --git a/spec/ruby/core/exception/exit_value_spec.rb b/spec/ruby/core/exception/exit_value_spec.rb
new file mode 100644
index 0000000000..bb6cff1831
--- /dev/null
+++ b/spec/ruby/core/exception/exit_value_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "LocalJumpError#exit_value" do
+ def get_me_a_return
+ Proc.new { return 42 }
+ end
+
+ it "returns the value given to return" do
+ -> { get_me_a_return.call }.should.raise(LocalJumpError) { |e|
+ e.exit_value.should == 42
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/fixtures/common.rb b/spec/ruby/core/exception/fixtures/common.rb
new file mode 100644
index 0000000000..3d8a3c3430
--- /dev/null
+++ b/spec/ruby/core/exception/fixtures/common.rb
@@ -0,0 +1,102 @@
+module ExceptionSpecs
+ class Exceptional < Exception; end
+
+ class Backtrace
+ def self.backtrace
+ begin
+ raise # If you move this line, update backtrace_spec.rb
+ rescue RuntimeError => e
+ e.backtrace
+ end
+ end
+
+ def self.backtrace_locations
+ begin
+ raise
+ rescue RuntimeError => e
+ e.backtrace_locations
+ end
+ end
+ end
+
+ class UnExceptional < Exception
+ def backtrace
+ nil
+ end
+ def message
+ nil
+ end
+ end
+
+ class ConstructorException < Exception
+
+ def initialize
+ end
+
+ end
+
+ class OverrideToS < RuntimeError
+ def to_s
+ "this is from #to_s"
+ end
+ end
+
+ class EmptyToS < RuntimeError
+ def to_s
+ ""
+ end
+ end
+
+ class InitializeException < StandardError
+ attr_reader :ivar
+
+ def initialize(message = nil)
+ super
+ @ivar = 1
+ end
+
+ def initialize_copy(other)
+ super
+ ScratchPad.record object_id
+ end
+ end
+
+ module ExceptionModule
+ def repr
+ 1
+ end
+ end
+end
+
+module NoMethodErrorSpecs
+ class NoMethodErrorA; end
+
+ class NoMethodErrorB; end
+
+ class NoMethodErrorC;
+ protected
+ def a_protected_method;end
+ private
+ def a_private_method; end
+ end
+
+ class NoMethodErrorD; end
+
+ class InstanceException < Exception
+ end
+
+ class AClass; end
+ module AModule; end
+end
+
+class NameErrorSpecs
+ class ReceiverClass
+ def call_undefined_class_variable
+ @@doesnt_exist
+ end
+ end
+end
+
+module DetailedMessageSpec
+ C = Class.new(RuntimeError)
+end
diff --git a/spec/ruby/core/exception/fixtures/syntax_error.rb b/spec/ruby/core/exception/fixtures/syntax_error.rb
new file mode 100644
index 0000000000..ccec62f7a1
--- /dev/null
+++ b/spec/ruby/core/exception/fixtures/syntax_error.rb
@@ -0,0 +1,3 @@
+# rubocop:disable Lint/Syntax
+1+1=2
+# rubocop:enable Lint/Syntax
diff --git a/spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb b/spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb
new file mode 100644
index 0000000000..c109ec6247
--- /dev/null
+++ b/spec/ruby/core/exception/fixtures/thread_fiber_ensure.rb
@@ -0,0 +1,22 @@
+ready = false
+t = Thread.new do
+ f = Fiber.new do
+ begin
+ Fiber.yield
+ ensure
+ STDERR.puts "suspended fiber ensure"
+ end
+ end
+ f.resume
+
+ begin
+ ready = true
+ sleep
+ ensure
+ STDERR.puts "current fiber ensure"
+ end
+end
+
+Thread.pass until ready && t.stop?
+
+# let the program end, it's the same as #exit or an exception for this behavior
diff --git a/spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb b/spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb
new file mode 100644
index 0000000000..3364ed06d0
--- /dev/null
+++ b/spec/ruby/core/exception/fixtures/thread_fiber_ensure_non_root_fiber.rb
@@ -0,0 +1,25 @@
+ready = false
+t = Thread.new do
+ f = Fiber.new do
+ begin
+ Fiber.yield
+ ensure
+ STDERR.puts "suspended fiber ensure"
+ end
+ end
+ f.resume
+
+ f2 = Fiber.new do
+ begin
+ ready = true
+ sleep
+ ensure
+ STDERR.puts "current fiber ensure"
+ end
+ end
+ f2.resume
+end
+
+Thread.pass until ready && t.stop?
+
+# let the program end, it's the same as #exit or an exception for this behavior
diff --git a/spec/ruby/core/exception/frozen_error_spec.rb b/spec/ruby/core/exception/frozen_error_spec.rb
new file mode 100644
index 0000000000..a28f524b54
--- /dev/null
+++ b/spec/ruby/core/exception/frozen_error_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+
+describe "FrozenError.new" do
+ it "should take optional receiver argument" do
+ o = Object.new
+ FrozenError.new("msg", receiver: o).receiver.should.equal?(o)
+ end
+end
+
+describe "FrozenError#receiver" do
+ it "should return frozen object that modification was attempted on" do
+ o = Object.new.freeze
+ begin
+ def o.x; end
+ rescue => e
+ e.should.is_a?(FrozenError)
+ e.receiver.should.equal?(o)
+ else
+ raise
+ end
+ end
+end
+
+describe "FrozenError#message" do
+ it "includes a receiver" do
+ object = Object.new
+ object.freeze
+
+ msg_class = ruby_version_is("4.0") ? "Object" : "object"
+
+ -> {
+ def object.x; end
+ }.should.raise(FrozenError, "can't modify frozen #{msg_class}: #{object}")
+
+ object = [].freeze
+ -> { object << nil }.should.raise(FrozenError, "can't modify frozen Array: []")
+ end
+end
+
+describe "Modifying a frozen object" do
+ context "#inspect is redefined and modifies the object" do
+ it "returns ... instead of String representation of object" do
+ object = Object.new
+ def object.inspect; @a = 1 end
+ def object.modify; @a = 2 end
+
+ object.freeze
+
+ # CRuby's message contains multiple whitespaces before '...'.
+ # So handle both multiple and single whitespace.
+ -> { object.modify }.should.raise(FrozenError, /can't modify frozen .*?: \s*.../)
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/full_message_spec.rb b/spec/ruby/core/exception/full_message_spec.rb
new file mode 100644
index 0000000000..5a5e0a2b3a
--- /dev/null
+++ b/spec/ruby/core/exception/full_message_spec.rb
@@ -0,0 +1,226 @@
+require_relative '../../spec_helper'
+
+describe "Exception#full_message" do
+ it "returns formatted string of exception using the same format that is used to print an uncaught exceptions to stderr" do
+ e = RuntimeError.new("Some runtime error")
+ e.set_backtrace(["a.rb:1", "b.rb:2"])
+
+ full_message = e.full_message
+ full_message.should.include? "RuntimeError"
+ full_message.should.include? "Some runtime error"
+ full_message.should.include? "a.rb:1"
+ full_message.should.include? "b.rb:2"
+ end
+
+ it "supports :highlight option and adds escape sequences to highlight some strings" do
+ e = RuntimeError.new("Some runtime error")
+
+ full_message = e.full_message(highlight: true, order: :top).lines
+ full_message[0].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n"
+
+ full_message = e.full_message(highlight: true, order: :bottom).lines
+ full_message[0].should == "\e[1mTraceback\e[m (most recent call last):\n"
+ full_message[-1].should.end_with? "\e[1mSome runtime error (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n"
+
+ full_message = e.full_message(highlight: false, order: :top).lines
+ full_message[0].should.end_with? "Some runtime error (RuntimeError)\n"
+
+ full_message = e.full_message(highlight: false, order: :bottom).lines
+ full_message[0].should == "Traceback (most recent call last):\n"
+ full_message[-1].should.end_with? "Some runtime error (RuntimeError)\n"
+ end
+
+ it "supports :order option and places the error message and the backtrace at the top or the bottom" do
+ e = RuntimeError.new("Some runtime error")
+ e.set_backtrace(["a.rb:1", "b.rb:2"])
+
+ e.full_message(order: :top, highlight: false).should =~ /a.rb:1.*b.rb:2/m
+ e.full_message(order: :bottom, highlight: false).should =~ /b.rb:2.*a.rb:1/m
+ end
+
+ it "shows the caller if the exception has no backtrace" do
+ e = RuntimeError.new("Some runtime error")
+ e.backtrace.should == nil
+ full_message = e.full_message(highlight: false, order: :top).lines
+ full_message[0].should.start_with?("#{__FILE__}:#{__LINE__-1}:in ")
+ full_message[0].should.end_with?("': Some runtime error (RuntimeError)\n")
+ end
+
+ describe "includes details about whether an exception was handled" do
+ describe "RuntimeError" do
+ it "should report as unhandled if message is empty" do
+ err = RuntimeError.new("")
+
+ err.full_message.should =~ /unhandled exception/
+ err.full_message(highlight: true).should =~ /unhandled exception/
+ err.full_message(highlight: false).should =~ /unhandled exception/
+ end
+
+ it "should not report as unhandled if the message is not empty" do
+ err = RuntimeError.new("non-empty")
+
+ err.full_message.should !~ /unhandled exception/
+ err.full_message(highlight: true).should !~ /unhandled exception/
+ err.full_message(highlight: false).should !~ /unhandled exception/
+ end
+
+ it "should not report as unhandled if the message is nil" do
+ err = RuntimeError.new(nil)
+
+ err.full_message.should !~ /unhandled exception/
+ err.full_message(highlight: true).should !~ /unhandled exception/
+ err.full_message(highlight: false).should !~ /unhandled exception/
+ end
+
+ it "should not report as unhandled if the message is not specified" do
+ err = RuntimeError.new()
+
+ err.full_message.should !~ /unhandled exception/
+ err.full_message(highlight: true).should !~ /unhandled exception/
+ err.full_message(highlight: false).should !~ /unhandled exception/
+ end
+
+ it "adds escape sequences to highlight some strings if the message is not specified and :highlight option is specified" do
+ e = RuntimeError.new("")
+
+ full_message = e.full_message(highlight: true, order: :top).lines
+ full_message[0].should.end_with? "\e[1;4munhandled exception\e[m\n"
+
+ full_message = e.full_message(highlight: true, order: :bottom).lines
+ full_message[0].should == "\e[1mTraceback\e[m (most recent call last):\n"
+ full_message[-1].should.end_with? "\e[1;4munhandled exception\e[m\n"
+
+ full_message = e.full_message(highlight: false, order: :top).lines
+ full_message[0].should.end_with? "unhandled exception\n"
+
+ full_message = e.full_message(highlight: false, order: :bottom).lines
+ full_message[0].should == "Traceback (most recent call last):\n"
+ full_message[-1].should.end_with? "unhandled exception\n"
+ end
+ end
+
+ describe "generic Error" do
+ it "should not report as unhandled in any event" do
+ StandardError.new("").full_message.should !~ /unhandled exception/
+ StandardError.new("non-empty").full_message.should !~ /unhandled exception/
+ end
+ end
+ end
+
+ it "shows the exception class at the end of the first line of the message when the message contains multiple lines" do
+ begin
+ line = __LINE__; raise "first line\nsecond line"
+ rescue => e
+ full_message = e.full_message(highlight: false, order: :top).lines
+ full_message[0].should.start_with?("#{__FILE__}:#{line}:in ")
+ full_message[0].should.end_with?(": first line (RuntimeError)\n")
+ full_message[1].should == "second line\n"
+ end
+ end
+
+ it "highlights the entire message when the message contains multiple lines" do
+ begin
+ line = __LINE__; raise "first line\nsecond line\nthird line"
+ rescue => e
+ full_message = e.full_message(highlight: true, order: :top).lines
+ full_message[0].should.start_with?("#{__FILE__}:#{line}:in ")
+ full_message[0].should.end_with?(": \e[1mfirst line (\e[1;4mRuntimeError\e[m\e[1m)\e[m\n")
+ full_message[1].should == "\e[1msecond line\e[m\n"
+ full_message[2].should == "\e[1mthird line\e[m\n"
+ end
+ end
+
+ it "contains cause of exception" do
+ begin
+ begin
+ raise 'the cause'
+ rescue
+ raise 'main exception'
+ end
+ rescue => e
+ exception = e
+ end
+
+ exception.full_message.should.include? "main exception"
+ exception.full_message.should.include? "the cause"
+ end
+
+ it 'contains all the chain of exceptions' do
+ begin
+ begin
+ begin
+ raise 'origin exception'
+ rescue
+ raise 'intermediate exception'
+ end
+ rescue
+ raise 'last exception'
+ end
+ rescue => e
+ exception = e
+ end
+
+ exception.full_message.should.include? "last exception"
+ exception.full_message.should.include? "intermediate exception"
+ exception.full_message.should.include? "origin exception"
+ end
+
+ it "relies on #detailed_message" do
+ e = RuntimeError.new("new error")
+ e.define_singleton_method(:detailed_message) { |**| "DETAILED MESSAGE" }
+
+ e.full_message.lines.first.should =~ /DETAILED MESSAGE/
+ end
+
+ it "passes all its own keyword arguments (with :highlight default value and without :order default value) to #detailed_message" do
+ e = RuntimeError.new("new error")
+ options_passed = nil
+ e.define_singleton_method(:detailed_message) do |**options|
+ options_passed = options
+ "DETAILED MESSAGE"
+ end
+
+ e.full_message(foo: "bar")
+ options_passed.should == { foo: "bar", highlight: Exception.to_tty? }
+ end
+
+ it "converts #detailed_message returned value to String if it isn't a String" do
+ message = Object.new
+ def message.to_str; "DETAILED MESSAGE"; end
+
+ e = RuntimeError.new("new error")
+ e.define_singleton_method(:detailed_message) { |**| message }
+
+ e.full_message.lines.first.should =~ /DETAILED MESSAGE/
+ end
+
+ it "uses class name if #detailed_message returns nil" do
+ e = RuntimeError.new("new error")
+ e.define_singleton_method(:detailed_message) { |**| nil }
+
+ e.full_message(highlight: false).lines.first.should =~ /RuntimeError/
+ e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/
+ end
+
+ it "uses class name if exception object doesn't respond to #detailed_message" do
+ e = RuntimeError.new("new error")
+ class << e
+ undef :detailed_message
+ end
+
+ e.full_message(highlight: false).lines.first.should =~ /RuntimeError/
+ e.full_message(highlight: true).lines.first.should =~ /#{Regexp.escape("\e[1;4mRuntimeError\e[m")}/
+ end
+
+ it "allows cause with empty backtrace" do
+ begin
+ raise RuntimeError.new("Some runtime error"), cause: RuntimeError.new("Some other runtime error")
+ rescue => e
+ end
+
+ full_message = e.full_message
+ full_message.should.include? "RuntimeError"
+ full_message.should.include? "Some runtime error"
+ full_message.should.include? "Some other runtime error"
+ end
+end
diff --git a/spec/ruby/core/exception/hierarchy_spec.rb b/spec/ruby/core/exception/hierarchy_spec.rb
new file mode 100644
index 0000000000..6514eb1994
--- /dev/null
+++ b/spec/ruby/core/exception/hierarchy_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+
+describe "Exception" do
+ it "has the right class hierarchy" do
+ hierarchy = {
+ Exception => {
+ NoMemoryError => nil,
+ ScriptError => {
+ LoadError => nil,
+ NotImplementedError => nil,
+ SyntaxError => nil,
+ },
+ SecurityError => nil,
+ SignalException => {
+ Interrupt => nil,
+ },
+ StandardError => {
+ ArgumentError => {
+ UncaughtThrowError => nil,
+ },
+ EncodingError => nil,
+ FiberError => nil,
+ IOError => {
+ EOFError => nil,
+ },
+ IndexError => {
+ KeyError => nil,
+ StopIteration => {
+ ClosedQueueError => nil,
+ },
+ },
+ LocalJumpError => nil,
+ NameError => {
+ NoMethodError => nil,
+ },
+ RangeError => {
+ FloatDomainError => nil,
+ },
+ RegexpError => nil,
+ RuntimeError => {
+ FrozenError => nil,
+ },
+ SystemCallError => nil,
+ ThreadError => nil,
+ TypeError => nil,
+ ZeroDivisionError => nil,
+ },
+ SystemExit => nil,
+ SystemStackError => nil,
+ },
+ }
+
+ traverse = -> parent_class, parent_subclass_hash {
+ parent_subclass_hash.each do |child_class, child_subclass_hash|
+ child_class.class.should == Class
+ child_class.superclass.should == parent_class
+ traverse.call(child_class, child_subclass_hash) if child_subclass_hash
+ end
+ }
+ traverse.call(Object, hierarchy)
+ end
+end
diff --git a/spec/ruby/core/exception/inspect_spec.rb b/spec/ruby/core/exception/inspect_spec.rb
new file mode 100644
index 0000000000..6f380a36c7
--- /dev/null
+++ b/spec/ruby/core/exception/inspect_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#inspect" do
+ it "returns '#<Exception: Exception>' when no message given" do
+ Exception.new.inspect.should == "#<Exception: Exception>"
+ end
+
+ it "keeps message encoding" do
+ Exception.new('å').inspect.should == "#<Exception: å>"
+ end
+
+ it "includes #to_s when the result is non-empty" do
+ ExceptionSpecs::OverrideToS.new.inspect.should == "#<ExceptionSpecs::OverrideToS: this is from #to_s>"
+ end
+
+ it "returns the class name when #to_s returns an empty string" do
+ ExceptionSpecs::EmptyToS.new.inspect.should == "ExceptionSpecs::EmptyToS"
+ end
+
+ it "returns the derived class name with a subclassed Exception" do
+ ExceptionSpecs::UnExceptional.new.inspect.should == "#<ExceptionSpecs::UnExceptional: ExceptionSpecs::UnExceptional>"
+ end
+end
diff --git a/spec/ruby/core/exception/interrupt_spec.rb b/spec/ruby/core/exception/interrupt_spec.rb
new file mode 100644
index 0000000000..90d261e470
--- /dev/null
+++ b/spec/ruby/core/exception/interrupt_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "Interrupt.new" do
+ it "returns an instance of interrupt with no message given" do
+ e = Interrupt.new
+ e.signo.should == Signal.list["INT"]
+ e.signm.should == "Interrupt"
+ end
+
+ it "takes an optional message argument" do
+ e = Interrupt.new("message")
+ e.signo.should == Signal.list["INT"]
+ e.signm.should == "message"
+ end
+end
+
+describe "rescuing Interrupt" do
+ before do
+ @original_sigint_proc = Signal.trap(:INT, :SIG_DFL)
+ end
+
+ after do
+ Signal.trap(:INT, @original_sigint_proc)
+ end
+
+ it "raises an Interrupt when sent a signal SIGINT" do
+ begin
+ Process.kill :INT, Process.pid
+ sleep
+ rescue Interrupt => e
+ e.signo.should == Signal.list["INT"]
+ ["", "Interrupt"].should.include?(e.message)
+ end
+ end
+end
+
+describe "Interrupt" do
+ # This spec is basically the same as above,
+ # but it does not rely on Signal.trap(:INT, :SIG_DFL) which can be tricky
+ it "is raised on the main Thread by the default SIGINT handler" do
+ out = ruby_exe(<<-'RUBY', args: "2>&1")
+ begin
+ Process.kill :INT, Process.pid
+ sleep
+ rescue Interrupt => e
+ puts "Interrupt: #{e.signo}"
+ end
+ RUBY
+ out.should == "Interrupt: #{Signal.list["INT"]}\n"
+ end
+
+ platform_is_not :windows do
+ it "shows the backtrace and has a signaled exit status" do
+ err = IO.popen([*ruby_exe, '-e', 'Process.kill :INT, Process.pid; sleep'], err: [:child, :out], &:read)
+ $?.termsig.should == Signal.list.fetch('INT')
+ err.should.include? ': Interrupt'
+ err.should =~ /from -e:1:in [`']<main>'/
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/io_error_spec.rb b/spec/ruby/core/exception/io_error_spec.rb
new file mode 100644
index 0000000000..940d5be876
--- /dev/null
+++ b/spec/ruby/core/exception/io_error_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+
+describe "IO::EAGAINWaitReadable" do
+ it "combines Errno::EAGAIN and IO::WaitReadable" do
+ IO::EAGAINWaitReadable.superclass.should == Errno::EAGAIN
+ IO::EAGAINWaitReadable.ancestors.should.include? IO::WaitReadable
+ end
+
+ it "is the same as IO::EWOULDBLOCKWaitReadable if Errno::EAGAIN is the same as Errno::EWOULDBLOCK" do
+ if Errno::EAGAIN.equal? Errno::EWOULDBLOCK
+ IO::EAGAINWaitReadable.should.equal? IO::EWOULDBLOCKWaitReadable
+ else
+ IO::EAGAINWaitReadable.should_not.equal? IO::EWOULDBLOCKWaitReadable
+ end
+ end
+end
+
+describe "IO::EWOULDBLOCKWaitReadable" do
+ it "combines Errno::EWOULDBLOCK and IO::WaitReadable" do
+ IO::EWOULDBLOCKWaitReadable.superclass.should == Errno::EWOULDBLOCK
+ IO::EAGAINWaitReadable.ancestors.should.include? IO::WaitReadable
+ end
+end
+
+describe "IO::EAGAINWaitWritable" do
+ it "combines Errno::EAGAIN and IO::WaitWritable" do
+ IO::EAGAINWaitWritable.superclass.should == Errno::EAGAIN
+ IO::EAGAINWaitWritable.ancestors.should.include? IO::WaitWritable
+ end
+
+ it "is the same as IO::EWOULDBLOCKWaitWritable if Errno::EAGAIN is the same as Errno::EWOULDBLOCK" do
+ if Errno::EAGAIN.equal? Errno::EWOULDBLOCK
+ IO::EAGAINWaitWritable.should.equal? IO::EWOULDBLOCKWaitWritable
+ else
+ IO::EAGAINWaitWritable.should_not.equal? IO::EWOULDBLOCKWaitWritable
+ end
+ end
+end
+
+describe "IO::EWOULDBLOCKWaitWritable" do
+ it "combines Errno::EWOULDBLOCK and IO::WaitWritable" do
+ IO::EWOULDBLOCKWaitWritable.superclass.should == Errno::EWOULDBLOCK
+ IO::EAGAINWaitWritable.ancestors.should.include? IO::WaitWritable
+ end
+end
diff --git a/spec/ruby/core/exception/key_error_spec.rb b/spec/ruby/core/exception/key_error_spec.rb
new file mode 100644
index 0000000000..c5e2b1efbc
--- /dev/null
+++ b/spec/ruby/core/exception/key_error_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "KeyError" do
+ it "accepts :receiver and :key options" do
+ receiver = mock("receiver")
+ key = mock("key")
+
+ error = KeyError.new(receiver: receiver, key: key)
+
+ error.receiver.should == receiver
+ error.key.should == key
+
+ error = KeyError.new("message", receiver: receiver, key: key)
+
+ error.message.should == "message"
+ error.receiver.should == receiver
+ error.key.should == key
+ end
+end
diff --git a/spec/ruby/core/exception/load_error_spec.rb b/spec/ruby/core/exception/load_error_spec.rb
new file mode 100644
index 0000000000..0056403e58
--- /dev/null
+++ b/spec/ruby/core/exception/load_error_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "LoadError#path" do
+ before :each do
+ @le = LoadError.new
+ end
+
+ it "is nil when constructed directly" do
+ @le.path.should == nil
+ end
+end
+
+describe "LoadError raised by load or require" do
+ it "provides the failing path in its #path attribute" do
+ begin
+ require 'file_that_does_not_exist'
+ rescue LoadError => le
+ le.path.should == 'file_that_does_not_exist'
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/message_spec.rb b/spec/ruby/core/exception/message_spec.rb
new file mode 100644
index 0000000000..8d7476126e
--- /dev/null
+++ b/spec/ruby/core/exception/message_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#message" do
+ it "returns the class name if there is no message" do
+ Exception.new.message.should == "Exception"
+ end
+
+ it "returns the message passed to #initialize" do
+ Exception.new("Ouch!").message.should == "Ouch!"
+ end
+
+ it "calls #to_s on self" do
+ exc = ExceptionSpecs::OverrideToS.new("you won't see this")
+ exc.message.should == "this is from #to_s"
+ end
+
+ context "when #backtrace is redefined" do
+ it "returns the Exception message" do
+ e = Exception.new
+ e.message.should == 'Exception'
+
+ def e.backtrace; []; end
+ e.message.should == 'Exception'
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/name_error_spec.rb b/spec/ruby/core/exception/name_error_spec.rb
new file mode 100644
index 0000000000..ddd51a92e5
--- /dev/null
+++ b/spec/ruby/core/exception/name_error_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+
+describe "NameError.new" do
+ it "should take optional name argument" do
+ NameError.new("msg","name").name.should == "name"
+ end
+
+ it "accepts a :receiver keyword argument" do
+ receiver = mock("receiver")
+
+ error = NameError.new("msg", :name, receiver: receiver)
+
+ error.receiver.should == receiver
+ error.name.should == :name
+ end
+end
+
+describe "NameError#dup" do
+ it "copies the name and receiver" do
+ begin
+ foo
+ rescue NameError => ne
+ name_error_dup = ne.dup
+ name_error_dup.name.should == :foo
+ name_error_dup.receiver.should == self
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/name_spec.rb b/spec/ruby/core/exception/name_spec.rb
new file mode 100644
index 0000000000..6e0e99d194
--- /dev/null
+++ b/spec/ruby/core/exception/name_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "NameError#name" do
+ it "returns a method name as a symbol" do
+ -> {
+ doesnt_exist
+ }.should.raise(NameError) {|e| e.name.should == :doesnt_exist }
+ end
+
+ it "returns a constant name as a symbol" do
+ -> {
+ DoesntExist
+ }.should.raise(NameError) {|e| e.name.should == :DoesntExist }
+ end
+
+ it "returns a constant name without namespace as a symbol" do
+ -> {
+ Object::DoesntExist
+ }.should.raise(NameError) {|e| e.name.should == :DoesntExist }
+ end
+
+ it "returns a class variable name as a symbol" do
+ -> {
+ eval("class singleton_class::A; @@doesnt_exist end", binding, __FILE__, __LINE__)
+ }.should.raise(NameError) { |e| e.name.should == :@@doesnt_exist }
+ end
+
+ it "returns the first argument passed to the method when a NameError is raised from #instance_variable_get" do
+ invalid_ivar_name = "invalid_ivar_name"
+
+ -> {
+ Object.new.instance_variable_get(invalid_ivar_name)
+ }.should.raise(NameError) {|e| e.name.should.equal?(invalid_ivar_name) }
+ end
+
+ it "returns the first argument passed to the method when a NameError is raised from #class_variable_get" do
+ invalid_cvar_name = "invalid_cvar_name"
+
+ -> {
+ Object.class_variable_get(invalid_cvar_name)
+ }.should.raise(NameError) {|e| e.name.should.equal?(invalid_cvar_name) }
+ end
+end
diff --git a/spec/ruby/core/exception/new_spec.rb b/spec/ruby/core/exception/new_spec.rb
new file mode 100644
index 0000000000..100dbb0a24
--- /dev/null
+++ b/spec/ruby/core/exception/new_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/new'
+
+describe "Exception.new" do
+ it_behaves_like :exception_new, :new
+end
diff --git a/spec/ruby/core/exception/no_method_error_spec.rb b/spec/ruby/core/exception/no_method_error_spec.rb
new file mode 100644
index 0000000000..9f92104082
--- /dev/null
+++ b/spec/ruby/core/exception/no_method_error_spec.rb
@@ -0,0 +1,224 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "NoMethodError.new" do
+ it "allows passing method args" do
+ NoMethodError.new("msg", "name", ["args"]).args.should == ["args"]
+ end
+
+ it "does not require a name" do
+ NoMethodError.new("msg").message.should == "msg"
+ end
+
+ it "accepts a :receiver keyword argument" do
+ receiver = mock("receiver")
+
+ error = NoMethodError.new("msg", :name, receiver: receiver)
+
+ error.receiver.should == receiver
+ error.name.should == :name
+ end
+end
+
+describe "NoMethodError#args" do
+ it "returns an empty array if the caller method had no arguments" do
+ begin
+ NoMethodErrorSpecs::NoMethodErrorB.new.foo
+ rescue Exception => e
+ e.args.should == []
+ end
+ end
+
+ it "returns an array with the same elements as passed to the method" do
+ begin
+ a = NoMethodErrorSpecs::NoMethodErrorA.new
+ NoMethodErrorSpecs::NoMethodErrorB.new.foo(1,a)
+ rescue Exception => e
+ e.args.should == [1,a]
+ e.args[1].should.equal? a
+ end
+ end
+end
+
+describe "NoMethodError#message" do
+ it "for an undefined method match /undefined method/" do
+ begin
+ NoMethodErrorSpecs::NoMethodErrorD.new.foo
+ rescue Exception => e
+ e.should.is_a?(NoMethodError)
+ end
+ end
+
+ it "for an protected method match /protected method/" do
+ begin
+ NoMethodErrorSpecs::NoMethodErrorC.new.a_protected_method
+ rescue Exception => e
+ e.should.is_a?(NoMethodError)
+ end
+ end
+
+ it "for private method match /private method/" do
+ begin
+ NoMethodErrorSpecs::NoMethodErrorC.new.a_private_method
+ rescue Exception => e
+ e.should.is_a?(NoMethodError)
+ e.message.lines[0].should =~ /private method [`']a_private_method' called for /
+ end
+ end
+
+ it "uses a literal name when receiver is nil" do
+ begin
+ nil.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for nil\Z/
+ end
+ end
+
+ it "uses a literal name when receiver is true" do
+ begin
+ true.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for true\Z/
+ end
+ end
+
+ it "uses a literal name when receiver is false" do
+ begin
+ false.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for false\Z/
+ end
+ end
+
+ it "uses #name when receiver is a class" do
+ klass = Class.new { def self.name; "MyClass"; end }
+
+ begin
+ klass.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for class MyClass\Z/
+ end
+ end
+
+ it "uses class' string representation when receiver is an anonymous class" do
+ klass = Class.new
+
+ begin
+ klass.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for class #<Class:0x\h+>\Z/
+ end
+ end
+
+ it "uses class' string representation when receiver is a singleton class" do
+ obj = Object.new
+ singleton_class = obj.singleton_class
+
+ begin
+ singleton_class.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for class #<Class:#<Object:0x\h+>>\Z/
+ end
+ end
+
+ it "uses #name when receiver is a module" do
+ mod = Module.new { def self.name; "MyModule"; end }
+
+ begin
+ mod.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for module MyModule\Z/
+ end
+ end
+
+ it "uses module's string representation when receiver is an anonymous module" do
+ m = Module.new
+
+ begin
+ m.foo
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for module #<Module:0x\h+>\Z/
+ end
+ end
+
+ it "uses class #name when receiver is an ordinary object" do
+ klass = Class.new { def self.name; "MyClass"; end }
+ instance = klass.new
+
+ begin
+ instance.bar
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']bar' for an instance of MyClass\Z/
+ end
+ end
+
+ it "uses class string representation when receiver is an instance of anonymous class" do
+ klass = Class.new
+ instance = klass.new
+
+ begin
+ instance.bar
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']bar' for an instance of #<Class:0x\h+>\Z/
+ end
+ end
+
+ it "uses class name when receiver has a singleton class" do
+ instance = NoMethodErrorSpecs::NoMethodErrorA.new
+ def instance.foo; end
+
+ begin
+ instance.bar
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']bar' for #<NoMethodErrorSpecs::NoMethodErrorA:0x\h+>\Z/
+ end
+ end
+
+ it "does not call #inspect when calling Exception#message" do
+ ScratchPad.record []
+ test_class = Class.new do
+ def inspect
+ ScratchPad << :inspect_called
+ "<inspect>"
+ end
+ end
+ instance = test_class.new
+
+ begin
+ instance.bar
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']bar' for an instance of #<Class:0x\h+>\Z/
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ it "does not truncate long class names" do
+ class_name = 'ExceptionSpecs::A' + 'a'*100
+
+ begin
+ eval <<~RUBY
+ class #{class_name}
+ end
+
+ obj = #{class_name}.new
+ obj.foo
+ RUBY
+ rescue NoMethodError => error
+ error.message.should =~ /\Aundefined method [`']foo' for an instance of #{class_name}\Z/
+ end
+ end
+end
+
+describe "NoMethodError#dup" do
+ it "copies the name, arguments and receiver" do
+ begin
+ receiver = Object.new
+ receiver.foo(:one, :two)
+ rescue NoMethodError => nme
+ no_method_error_dup = nme.dup
+ no_method_error_dup.name.should == :foo
+ no_method_error_dup.receiver.should == receiver
+ no_method_error_dup.args.should == [:one, :two]
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/reason_spec.rb b/spec/ruby/core/exception/reason_spec.rb
new file mode 100644
index 0000000000..d7022768b6
--- /dev/null
+++ b/spec/ruby/core/exception/reason_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "LocalJumpError#reason" do
+ def get_me_a_return
+ Proc.new { return 42 }
+ end
+
+ it "returns 'return' for a return" do
+ -> { get_me_a_return.call }.should.raise(LocalJumpError) { |e|
+ e.reason.should == :return
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/receiver_spec.rb b/spec/ruby/core/exception/receiver_spec.rb
new file mode 100644
index 0000000000..6ecf640ad8
--- /dev/null
+++ b/spec/ruby/core/exception/receiver_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "NameError#receiver" do
+ class ::ReceiverClass
+ def call_undefined_class_variable; @@doesnt_exist end
+ end
+
+ it "returns the object that raised the exception" do
+ receiver = Object.new
+
+ -> {
+ receiver.doesnt_exist
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(receiver) }
+ end
+
+ it "returns the Object class when an undefined constant is called without namespace" do
+ -> {
+ DoesntExist
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(Object) }
+ end
+
+ it "returns a class when an undefined constant is called" do
+ -> {
+ NameErrorSpecs::ReceiverClass::DoesntExist
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(NameErrorSpecs::ReceiverClass) }
+ end
+
+ it "returns the Object class when an undefined class variable is called" do
+ -> {
+ eval("class singleton_class::A; @@doesnt_exist end", binding, __FILE__, __LINE__)
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(singleton_class::A) }
+ end
+
+ it "returns a class when an undefined class variable is called in a subclass' namespace" do
+ -> {
+ NameErrorSpecs::ReceiverClass.new.call_undefined_class_variable
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(NameErrorSpecs::ReceiverClass) }
+ end
+
+ it "returns the receiver when raised from #instance_variable_get" do
+ receiver = Object.new
+
+ -> {
+ receiver.instance_variable_get("invalid_ivar_name")
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(receiver) }
+ end
+
+ it "returns the receiver when raised from #class_variable_get" do
+ -> {
+ Object.class_variable_get("invalid_cvar_name")
+ }.should.raise(NameError) {|e| e.receiver.should.equal?(Object) }
+ end
+
+ it "raises an ArgumentError when the receiver is none" do
+ -> { NameError.new.receiver }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/exception/result_spec.rb b/spec/ruby/core/exception/result_spec.rb
new file mode 100644
index 0000000000..451ff43af5
--- /dev/null
+++ b/spec/ruby/core/exception/result_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "StopIteration#result" do
+ before :each do
+ obj = Object.new
+ def obj.each
+ yield :yield_returned_1
+ yield :yield_returned_2
+ :method_returned
+ end
+ @enum = obj.to_enum
+ end
+
+ it "returns the method-returned-object from an Enumerator" do
+ @enum.next
+ @enum.next
+ -> { @enum.next }.should.raise(StopIteration) { |error|
+ error.result.should.equal?(:method_returned)
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/set_backtrace_spec.rb b/spec/ruby/core/exception/set_backtrace_spec.rb
new file mode 100644
index 0000000000..2cd93326ec
--- /dev/null
+++ b/spec/ruby/core/exception/set_backtrace_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/set_backtrace'
+
+describe "Exception#set_backtrace" do
+ it "allows the user to set the backtrace from a rescued exception" do
+ bt = ExceptionSpecs::Backtrace.backtrace
+ err = RuntimeError.new
+ err.backtrace.should == nil
+ err.backtrace_locations.should == nil
+
+ err.set_backtrace bt
+
+ err.backtrace.should == bt
+ err.backtrace_locations.should == nil
+ end
+
+ it_behaves_like :exception_set_backtrace, -> backtrace {
+ err = RuntimeError.new
+ err.set_backtrace(backtrace)
+ err
+ }
+end
diff --git a/spec/ruby/core/exception/shared/new.rb b/spec/ruby/core/exception/shared/new.rb
new file mode 100644
index 0000000000..048fd14dd2
--- /dev/null
+++ b/spec/ruby/core/exception/shared/new.rb
@@ -0,0 +1,18 @@
+describe :exception_new, shared: true do
+ it "creates a new instance of Exception" do
+ Exception.send(@method).class.ancestors.should.include?(Exception)
+ end
+
+ it "sets the message of the Exception when passes a message" do
+ Exception.send(@method, "I'm broken.").message.should == "I'm broken."
+ end
+
+ it "returns 'Exception' for message when no message given" do
+ Exception.send(@method).message.should == "Exception"
+ end
+
+ it "returns the exception when it has a custom constructor" do
+ ExceptionSpecs::ConstructorException.send(@method).should.is_a?(ExceptionSpecs::ConstructorException)
+ end
+
+end
diff --git a/spec/ruby/core/exception/shared/set_backtrace.rb b/spec/ruby/core/exception/shared/set_backtrace.rb
new file mode 100644
index 0000000000..934bf3dc5f
--- /dev/null
+++ b/spec/ruby/core/exception/shared/set_backtrace.rb
@@ -0,0 +1,64 @@
+require_relative '../fixtures/common'
+
+describe :exception_set_backtrace, shared: true do
+ it "accepts an Array of Strings" do
+ err = @method.call(["unhappy"])
+ err.backtrace.should == ["unhappy"]
+ end
+
+ it "allows the user to set the backtrace from a rescued exception" do
+ bt = ExceptionSpecs::Backtrace.backtrace
+ err = @method.call(bt)
+ err.backtrace.should == bt
+ end
+
+ ruby_version_is "3.4" do
+ it "allows the user to set backtrace locations from a rescued exception" do
+ bt_locations = ExceptionSpecs::Backtrace.backtrace_locations
+ err = @method.call(bt_locations)
+ err.backtrace_locations.size.should == bt_locations.size
+ err.backtrace_locations.each_with_index do |loc, index|
+ other_loc = bt_locations[index]
+
+ loc.path.should == other_loc.path
+ loc.label.should == other_loc.label
+ loc.base_label.should == other_loc.base_label
+ loc.lineno.should == other_loc.lineno
+ loc.absolute_path.should == other_loc.absolute_path
+ loc.to_s.should == other_loc.to_s
+ end
+ err.backtrace.size.should == err.backtrace_locations.size
+ end
+ end
+
+ it "accepts an empty Array" do
+ err = @method.call([])
+ err.backtrace.should == []
+ end
+
+ it "accepts a String" do
+ err = @method.call("unhappy")
+ err.backtrace.should == ["unhappy"]
+ end
+
+ it "accepts nil" do
+ err = @method.call(nil)
+ err.backtrace.should == nil
+ end
+
+ it "raises a TypeError when passed a Symbol" do
+ -> { @method.call(:unhappy) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the Array contains a Symbol" do
+ -> { @method.call(["String", :unhappy]) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the array contains nil" do
+ -> { @method.call(["String", nil]) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the argument is a nested array" do
+ -> { @method.call(["String", ["String"]]) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/exception/signal_exception_spec.rb b/spec/ruby/core/exception/signal_exception_spec.rb
new file mode 100644
index 0000000000..010181bc55
--- /dev/null
+++ b/spec/ruby/core/exception/signal_exception_spec.rb
@@ -0,0 +1,123 @@
+require_relative '../../spec_helper'
+
+describe "SignalException.new" do
+ it "takes a signal number as the first argument" do
+ exc = SignalException.new(Signal.list["INT"])
+ exc.signo.should == Signal.list["INT"]
+ exc.signm.should == "SIGINT"
+ exc.message.should == "SIGINT"
+ end
+
+ it "raises an exception with an invalid signal number" do
+ -> { SignalException.new(100000) }.should.raise(ArgumentError)
+ end
+
+ it "takes a signal name without SIG prefix as the first argument" do
+ exc = SignalException.new("INT")
+ exc.signo.should == Signal.list["INT"]
+ exc.signm.should == "SIGINT"
+ exc.message.should == "SIGINT"
+ end
+
+ it "takes a signal name with SIG prefix as the first argument" do
+ exc = SignalException.new("SIGINT")
+ exc.signo.should == Signal.list["INT"]
+ exc.signm.should == "SIGINT"
+ exc.message.should == "SIGINT"
+ end
+
+ it "raises an exception with an invalid signal name" do
+ -> { SignalException.new("NONEXISTENT") }.should.raise(ArgumentError)
+ end
+
+ it "raises an exception with an invalid first argument type" do
+ -> { SignalException.new(Object.new) }.should.raise(ArgumentError)
+ end
+
+ it "takes a signal symbol without SIG prefix as the first argument" do
+ exc = SignalException.new(:INT)
+ exc.signo.should == Signal.list["INT"]
+ exc.signm.should == "SIGINT"
+ exc.message.should == "SIGINT"
+ end
+
+ it "takes a signal symbol with SIG prefix as the first argument" do
+ exc = SignalException.new(:SIGINT)
+ exc.signo.should == Signal.list["INT"]
+ exc.signm.should == "SIGINT"
+ exc.message.should == "SIGINT"
+ end
+
+ it "raises an exception with an invalid signal name" do
+ -> { SignalException.new(:NONEXISTENT) }.should.raise(ArgumentError)
+ end
+
+ it "takes an optional message argument with a signal number" do
+ exc = SignalException.new(Signal.list["INT"], "name")
+ exc.signo.should == Signal.list["INT"]
+ exc.signm.should == "name"
+ exc.message.should == "name"
+ end
+
+ it "raises an exception for an optional argument with a signal name" do
+ -> { SignalException.new("INT","name") }.should.raise(ArgumentError)
+ end
+end
+
+describe "rescuing SignalException" do
+ it "raises a SignalException when sent a signal" do
+ begin
+ Process.kill :TERM, Process.pid
+ sleep
+ rescue SignalException => e
+ e.signo.should == Signal.list["TERM"]
+ e.signm.should == "SIGTERM"
+ e.message.should == "SIGTERM"
+ end
+ end
+end
+
+describe "SignalException" do
+ it "can be rescued" do
+ ruby_exe(<<-RUBY)
+ begin
+ raise SignalException, 'SIGKILL'
+ rescue SignalException
+ exit(0)
+ end
+ exit(1)
+ RUBY
+
+ $?.exitstatus.should == 0
+ end
+
+ platform_is_not :windows do
+ it "runs after at_exit" do
+ output = ruby_exe(<<-RUBY, exit_status: :SIGKILL)
+ at_exit do
+ puts "hello"
+ $stdout.flush
+ end
+
+ raise SignalException, 'SIGKILL'
+ RUBY
+
+ $?.termsig.should == Signal.list.fetch("KILL")
+ output.should == "hello\n"
+ end
+
+ it "cannot be trapped with Signal.trap" do
+ ruby_exe(<<-RUBY, exit_status: :SIGPROF)
+ Signal.trap("PROF") {}
+ raise(SignalException, "PROF")
+ RUBY
+
+ $?.termsig.should == Signal.list.fetch("PROF")
+ end
+
+ it "self-signals for USR1" do
+ ruby_exe("raise(SignalException, 'USR1')", exit_status: :SIGUSR1)
+ $?.termsig.should == Signal.list.fetch('USR1')
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/signm_spec.rb b/spec/ruby/core/exception/signm_spec.rb
new file mode 100644
index 0000000000..cabcc7ad58
--- /dev/null
+++ b/spec/ruby/core/exception/signm_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "SignalException#signm" do
+ it "returns the signal name" do
+ -> { Process.kill(:TERM, Process.pid) }.should.raise(SignalException) { |e|
+ e.signm.should == 'SIGTERM'
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/signo_spec.rb b/spec/ruby/core/exception/signo_spec.rb
new file mode 100644
index 0000000000..46e79a8daf
--- /dev/null
+++ b/spec/ruby/core/exception/signo_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "SignalException#signo" do
+ it "returns the signal number" do
+ -> { Process.kill(:TERM, Process.pid) }.should.raise(SignalException) { |e|
+ e.signo.should == Signal.list['TERM']
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/standard_error_spec.rb b/spec/ruby/core/exception/standard_error_spec.rb
new file mode 100644
index 0000000000..b05d247f67
--- /dev/null
+++ b/spec/ruby/core/exception/standard_error_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "StandardError" do
+ it "rescues StandardError" do
+ begin
+ raise StandardError
+ rescue => exception
+ exception.class.should == StandardError
+ end
+ end
+
+ it "rescues subclass of StandardError" do
+ begin
+ raise RuntimeError
+ rescue => exception
+ exception.class.should == RuntimeError
+ end
+ end
+
+ it "does not rescue superclass of StandardError" do
+ -> { begin; raise Exception; rescue; end }.should.raise(Exception)
+ end
+end
diff --git a/spec/ruby/core/exception/status_spec.rb b/spec/ruby/core/exception/status_spec.rb
new file mode 100644
index 0000000000..7369b0815d
--- /dev/null
+++ b/spec/ruby/core/exception/status_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "SystemExit#status" do
+ it "returns the exit status" do
+ -> { exit 42 }.should.raise(SystemExit) { |e|
+ e.status.should == 42
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/success_spec.rb b/spec/ruby/core/exception/success_spec.rb
new file mode 100644
index 0000000000..5ab8f94454
--- /dev/null
+++ b/spec/ruby/core/exception/success_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "SystemExit#success?" do
+ it "returns true if the process exited successfully" do
+ -> { exit 0 }.should.raise(SystemExit) { |e|
+ e.should.success?
+ }
+ end
+
+ it "returns false if the process exited unsuccessfully" do
+ -> { exit(-1) }.should.raise(SystemExit) { |e|
+ e.should_not.success?
+ }
+ end
+end
diff --git a/spec/ruby/core/exception/syntax_error_spec.rb b/spec/ruby/core/exception/syntax_error_spec.rb
new file mode 100644
index 0000000000..66eb5649aa
--- /dev/null
+++ b/spec/ruby/core/exception/syntax_error_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+describe "SyntaxError#path" do
+ it "returns the file path provided to eval" do
+ filename = "speccing.rb"
+
+ -> {
+ eval("if true", TOPLEVEL_BINDING, filename)
+ }.should.raise(SyntaxError) { |e|
+ e.path.should == filename
+ }
+ end
+
+ it "returns the file path that raised an exception" do
+ expected_path = fixture(__FILE__, "syntax_error.rb")
+
+ -> {
+ require_relative "fixtures/syntax_error"
+ }.should.raise(SyntaxError) { |e| e.path.should == expected_path }
+ end
+
+ it "returns nil when constructed directly" do
+ SyntaxError.new.path.should == nil
+ end
+end
diff --git a/spec/ruby/core/exception/system_call_error_spec.rb b/spec/ruby/core/exception/system_call_error_spec.rb
new file mode 100644
index 0000000000..da01c5b6b0
--- /dev/null
+++ b/spec/ruby/core/exception/system_call_error_spec.rb
@@ -0,0 +1,163 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "SystemCallError" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "can be subclassed" do
+ ExceptionSpecs::SCESub = Class.new(SystemCallError) do
+ def initialize
+ ScratchPad.record :initialize
+ end
+ end
+
+ exc = ExceptionSpecs::SCESub.new
+ ScratchPad.recorded.should.equal?(:initialize)
+ exc.should.instance_of?(ExceptionSpecs::SCESub)
+ ensure
+ ExceptionSpecs.send(:remove_const, :SCESub)
+ end
+end
+
+describe "SystemCallError.new" do
+ before :all do
+ @example_errno = Errno::EINVAL::Errno
+ @example_errno_class = Errno::EINVAL
+ @last_known_errno = Errno.constants.size - 1
+ @unknown_errno = Errno.constants.size
+ @some_human_readable = /[[:graph:]]+/
+ end
+
+ it "requires at least one argument" do
+ -> { SystemCallError.new }.should.raise(ArgumentError)
+ end
+
+ it "accepts single Integer argument as errno" do
+ SystemCallError.new(-2**24).errno.should == -2**24
+ SystemCallError.new(-1).errno.should == -1
+ SystemCallError.new(0).errno.should == 0
+ SystemCallError.new(@last_known_errno).errno.should == @last_known_errno
+ SystemCallError.new(@unknown_errno).errno.should == @unknown_errno
+ SystemCallError.new(2**24).errno.should == 2**24
+ end
+
+ it "constructs a SystemCallError for an unknown error number" do
+ SystemCallError.new(-2**24).should.instance_of?(SystemCallError)
+ SystemCallError.new(-1).should.instance_of?(SystemCallError)
+ SystemCallError.new(@unknown_errno).should.instance_of?(SystemCallError)
+ SystemCallError.new(2**24).should.instance_of?(SystemCallError)
+ end
+
+ it "constructs the appropriate Errno class" do
+ e = SystemCallError.new(@example_errno)
+ e.should.is_a?(SystemCallError)
+ e.should.instance_of?(@example_errno_class)
+ end
+
+ it "sets an error message corresponding to an appropriate Errno class" do
+ e = SystemCallError.new(@example_errno)
+ e.message.should == 'Invalid argument'
+ end
+
+ it "accepts an optional custom message preceding the errno" do
+ exc = SystemCallError.new("custom message", @example_errno)
+ exc.should.instance_of?(@example_errno_class)
+ exc.errno.should == @example_errno
+ exc.message.should == 'Invalid argument - custom message'
+ end
+
+ it "accepts an optional third argument specifying the location" do
+ exc = SystemCallError.new("custom message", @example_errno, "location")
+ exc.should.instance_of?(@example_errno_class)
+ exc.errno.should == @example_errno
+ exc.message.should == 'Invalid argument @ location - custom message'
+ end
+
+ it "coerces location if it is not a String" do
+ e = SystemCallError.new('foo', 1, :not_a_string)
+ e.message.should =~ /@ not_a_string - foo/
+ end
+
+ it "returns an arity of -1 for the initialize method" do
+ SystemCallError.instance_method(:initialize).arity.should == -1
+ end
+
+ it "converts to Integer if errno is a Float" do
+ SystemCallError.new('foo', 2.0).should == SystemCallError.new('foo', 2)
+ SystemCallError.new('foo', 2.9).should == SystemCallError.new('foo', 2)
+ end
+
+ it "treats nil errno as unknown error value" do
+ SystemCallError.new(nil).should.instance_of?(SystemCallError)
+ end
+
+ it "treats nil custom message as if it is not passed at all" do
+ exc = SystemCallError.new(nil, @example_errno)
+ exc.message.should == 'Invalid argument'
+ end
+
+ it "sets an 'unknown error' message when an unknown error number" do
+ SystemCallError.new(-1).message.should =~ @some_human_readable
+ end
+
+ it "adds a custom error message to an 'unknown error' message when an unknown error number and a custom message specified" do
+ SystemCallError.new("custom message", -1).message.should =~ /#{@some_human_readable}.* - custom message/
+ end
+
+ it "converts to Integer if errno is a Complex convertible to Integer" do
+ SystemCallError.new('foo', Complex(2.9, 0)).should == SystemCallError.new('foo', 2)
+ end
+
+ it "raises TypeError if message is not a String" do
+ -> { SystemCallError.new(:foo, 1) }.should.raise(TypeError, /no implicit conversion of Symbol into String/)
+ end
+
+ it "raises TypeError if errno is not an Integer" do
+ -> { SystemCallError.new('foo', 'bar') }.should.raise(TypeError, /no implicit conversion of String into Integer/)
+ end
+
+ it "raises RangeError if errno is a Complex not convertible to Integer" do
+ -> { SystemCallError.new('foo', Complex(2.9, 1)) }.should.raise(RangeError, /can't convert/)
+ end
+end
+
+describe "SystemCallError#errno" do
+ it "returns nil when no errno given" do
+ SystemCallError.new("message").errno.should == nil
+ end
+
+ it "returns the errno given as optional argument to new" do
+ SystemCallError.new("message", -2**20).errno.should == -2**20
+ SystemCallError.new("message", -1).errno.should == -1
+ SystemCallError.new("message", 0).errno.should == 0
+ SystemCallError.new("message", 1).errno.should == 1
+ SystemCallError.new("message", 42).errno.should == 42
+ SystemCallError.new("message", 2**20).errno.should == 2**20
+ end
+end
+
+describe "SystemCallError#message" do
+ it "returns the default message when no message is given" do
+ SystemCallError.new(2**28).message.should =~ @some_human_readable
+ end
+
+ it "returns the message given as an argument to new" do
+ SystemCallError.new("message", 1).message.should =~ /message/
+ SystemCallError.new("XXX").message.should =~ /XXX/
+ end
+end
+
+describe "SystemCallError#dup" do
+ it "copies the errno" do
+ dup_sce = SystemCallError.new("message", 42).dup
+ dup_sce.errno.should == 42
+ end
+end
+
+describe "SystemCallError#backtrace" do
+ it "is nil if not raised" do
+ SystemCallError.new("message", 42).backtrace.should == nil
+ end
+end
diff --git a/spec/ruby/core/exception/system_exit_spec.rb b/spec/ruby/core/exception/system_exit_spec.rb
new file mode 100644
index 0000000000..d899844c4e
--- /dev/null
+++ b/spec/ruby/core/exception/system_exit_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../../spec_helper'
+
+describe "SystemExit" do
+ describe "#initialize" do
+ it "accepts a status and message" do
+ exc = SystemExit.new(42, "message")
+ exc.status.should == 42
+ exc.message.should == "message"
+
+ exc = SystemExit.new(true, "message")
+ exc.status.should == 0
+ exc.message.should == "message"
+
+ exc = SystemExit.new(false, "message")
+ exc.status.should == 1
+ exc.message.should == "message"
+ end
+
+ it "accepts a status only" do
+ exc = SystemExit.new(42)
+ exc.status.should == 42
+ exc.message.should == "SystemExit"
+
+ exc = SystemExit.new(true)
+ exc.status.should == 0
+ exc.message.should == "SystemExit"
+
+ exc = SystemExit.new(false)
+ exc.status.should == 1
+ exc.message.should == "SystemExit"
+ end
+
+ it "accepts a message only" do
+ exc = SystemExit.new("message")
+ exc.status.should == 0
+ exc.message.should == "message"
+ end
+
+ it "accepts no arguments" do
+ exc = SystemExit.new
+ exc.status.should == 0
+ exc.message.should == "SystemExit"
+ end
+ end
+
+ it "sets the exit status and exits silently when raised" do
+ code = 'raise SystemExit.new(7)'
+ result = ruby_exe(code, args: "2>&1", exit_status: 7)
+ result.should == ""
+ $?.exitstatus.should == 7
+ end
+
+ it "sets the exit status and exits silently when raised when subclassed" do
+ code = 'class CustomExit < SystemExit; end; raise CustomExit.new(8)'
+ result = ruby_exe(code, args: "2>&1", exit_status: 8)
+ result.should == ""
+ $?.exitstatus.should == 8
+ end
+end
diff --git a/spec/ruby/core/exception/to_s_spec.rb b/spec/ruby/core/exception/to_s_spec.rb
new file mode 100644
index 0000000000..65c0d73a98
--- /dev/null
+++ b/spec/ruby/core/exception/to_s_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Exception#to_s" do
+ it "returns the self's name if no message is set" do
+ Exception.new.to_s.should == 'Exception'
+ ExceptionSpecs::Exceptional.new.to_s.should == 'ExceptionSpecs::Exceptional'
+ end
+
+ it "returns self's message if set" do
+ ExceptionSpecs::Exceptional.new('!!').to_s.should == '!!'
+ end
+
+ it "calls #to_s on the message" do
+ message = mock("message")
+ message.should_receive(:to_s).and_return("message")
+ ExceptionSpecs::Exceptional.new(message).to_s.should == "message"
+ end
+end
+
+describe "NameError#to_s" do
+ it "raises its own message for an undefined variable" do
+ begin
+ puts not_defined
+ rescue => exception
+ exception.message.should =~ /undefined local variable or method [`']not_defined'/
+ end
+ end
+
+ it "raises its own message for an undefined constant" do
+ begin
+ puts NotDefined
+ rescue => exception
+ exception.message.should =~ /uninitialized constant NotDefined/
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/top_level_spec.rb b/spec/ruby/core/exception/top_level_spec.rb
new file mode 100644
index 0000000000..cc961d06d5
--- /dev/null
+++ b/spec/ruby/core/exception/top_level_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../../spec_helper'
+
+describe "An Exception reaching the top level" do
+ it "is printed on STDERR" do
+ ruby_exe('raise "foo"', args: "2>&1", exit_status: 1).should =~ /in [`']<main>': foo \(RuntimeError\)/
+ end
+
+ it "the Exception#cause is printed to STDERR with backtraces" do
+ code = <<-RUBY
+ def raise_cause
+ raise "the cause" # 2
+ end
+ def raise_wrapped
+ raise "wrapped" # 5
+ end
+ begin
+ raise_cause # 8
+ rescue
+ raise_wrapped # 10
+ end
+ RUBY
+ lines = ruby_exe(code, args: "2>&1", exit_status: 1).lines
+
+ lines.map! { |l| l.chomp[/:(\d+:in.+)/, 1] }
+ lines[0].should =~ /\A5:in [`'](?:Object#)?raise_wrapped': wrapped \(RuntimeError\)\z/
+ if lines[1].include? 'rescue in'
+ # CRuby < 3.4 has an extra 'rescue in' backtrace entry
+ lines[1].should =~ /\A10:in [`']rescue in <main>'\z/
+ lines.delete_at 1
+ lines[1].should =~ /\A7:in [`']<main>'\z/
+ else
+ lines[1].should =~ /\A10:in [`']<main>'\z/
+ end
+ lines[2].should =~ /\A2:in [`'](?:Object#)?raise_cause': the cause \(RuntimeError\)\z/
+ lines[3].should =~ /\A8:in [`']<main>'\z/
+ lines.size.should == 4
+ end
+
+ describe "with a custom backtrace" do
+ it "is printed on STDERR" do
+ code = <<-RUBY
+ raise RuntimeError, "foo", [
+ "/dir/foo.rb:10:in `raising'",
+ "/dir/bar.rb:20:in `caller'",
+ ]
+ RUBY
+ ruby_exe(code, args: "2>&1", exit_status: 1).should == <<-EOS
+/dir/foo.rb:10:in `raising': foo (RuntimeError)
+\tfrom /dir/bar.rb:20:in `caller'
+ EOS
+ end
+ end
+
+ describe "kills all threads and fibers, ensure clauses are only run for threads current fibers, not for suspended fibers" do
+ it "with ensure on the root fiber" do
+ file = fixture(__FILE__, "thread_fiber_ensure.rb")
+ ruby_exe(file, args: "2>&1", exit_status: 0).should == "current fiber ensure\n"
+ end
+
+ it "with ensure on non-root fiber" do
+ file = fixture(__FILE__, "thread_fiber_ensure_non_root_fiber.rb")
+ ruby_exe(file, args: "2>&1", exit_status: 0).should == "current fiber ensure\n"
+ end
+ end
+end
diff --git a/spec/ruby/core/exception/uncaught_throw_error_spec.rb b/spec/ruby/core/exception/uncaught_throw_error_spec.rb
new file mode 100644
index 0000000000..9267df6670
--- /dev/null
+++ b/spec/ruby/core/exception/uncaught_throw_error_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "UncaughtThrowError#tag" do
+ it "returns the object thrown" do
+ begin
+ throw :abc
+
+ rescue UncaughtThrowError => e
+ e.tag.should == :abc
+ end
+ end
+end
diff --git a/spec/ruby/core/false/and_spec.rb b/spec/ruby/core/false/and_spec.rb
new file mode 100644
index 0000000000..0b02ae62c5
--- /dev/null
+++ b/spec/ruby/core/false/and_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#&" do
+ it "returns false" do
+ (false & false).should == false
+ (false & true).should == false
+ (false & nil).should == false
+ (false & "").should == false
+ (false & mock('x')).should == false
+ end
+end
diff --git a/spec/ruby/core/false/case_compare_spec.rb b/spec/ruby/core/false/case_compare_spec.rb
new file mode 100644
index 0000000000..0bd0ab44ae
--- /dev/null
+++ b/spec/ruby/core/false/case_compare_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#===" do
+ it "returns true for false" do
+ (false === false).should == true
+ end
+
+ it "returns false for non-false object" do
+ (false === 0).should == false
+ (false === "").should == false
+ (false === Object).should == false
+ (false === nil).should == false
+ end
+end
diff --git a/spec/ruby/core/false/dup_spec.rb b/spec/ruby/core/false/dup_spec.rb
new file mode 100644
index 0000000000..b0eb85529e
--- /dev/null
+++ b/spec/ruby/core/false/dup_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#dup" do
+ it "returns self" do
+ false.dup.should.equal?(false)
+ end
+end
diff --git a/spec/ruby/core/false/falseclass_spec.rb b/spec/ruby/core/false/falseclass_spec.rb
new file mode 100644
index 0000000000..8dfe5ae891
--- /dev/null
+++ b/spec/ruby/core/false/falseclass_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass" do
+ it ".allocate raises a TypeError" do
+ -> do
+ FalseClass.allocate
+ end.should.raise(TypeError)
+ end
+
+ it ".new is undefined" do
+ -> do
+ FalseClass.new
+ end.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/false/inspect_spec.rb b/spec/ruby/core/false/inspect_spec.rb
new file mode 100644
index 0000000000..4cbb55d434
--- /dev/null
+++ b/spec/ruby/core/false/inspect_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#inspect" do
+ it "returns the string 'false'" do
+ false.inspect.should == "false"
+ end
+end
diff --git a/spec/ruby/core/false/or_spec.rb b/spec/ruby/core/false/or_spec.rb
new file mode 100644
index 0000000000..f3ee1a3439
--- /dev/null
+++ b/spec/ruby/core/false/or_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#|" do
+ it "returns false if other is nil or false, otherwise true" do
+ (false | false).should == false
+ (false | true).should == true
+ (false | nil).should == false
+ (false | "").should == true
+ (false | mock('x')).should == true
+ end
+end
diff --git a/spec/ruby/core/false/singleton_method_spec.rb b/spec/ruby/core/false/singleton_method_spec.rb
new file mode 100644
index 0000000000..72aee8609a
--- /dev/null
+++ b/spec/ruby/core/false/singleton_method_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#singleton_method" do
+ it "raises regardless of whether FalseClass defines the method" do
+ -> { false.singleton_method(:foo) }.should.raise(NameError)
+ begin
+ def (false).foo; end
+ -> { false.singleton_method(:foo) }.should.raise(NameError)
+ ensure
+ FalseClass.send(:remove_method, :foo)
+ end
+ end
+end
diff --git a/spec/ruby/core/false/to_s_spec.rb b/spec/ruby/core/false/to_s_spec.rb
new file mode 100644
index 0000000000..9e24be26a2
--- /dev/null
+++ b/spec/ruby/core/false/to_s_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#to_s" do
+ it "returns the string 'false'" do
+ false.to_s.should == "false"
+ end
+
+ it "returns a frozen string" do
+ false.to_s.should.frozen?
+ end
+
+ it "always returns the same string" do
+ false.to_s.should.equal?(false.to_s)
+ end
+end
diff --git a/spec/ruby/core/false/xor_spec.rb b/spec/ruby/core/false/xor_spec.rb
new file mode 100644
index 0000000000..1b87b9f412
--- /dev/null
+++ b/spec/ruby/core/false/xor_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "FalseClass#^" do
+ it "returns false if other is nil or false, otherwise true" do
+ (false ^ false).should == false
+ (false ^ true).should == true
+ (false ^ nil).should == false
+ (false ^ "").should == true
+ (false ^ mock('x')).should == true
+ end
+end
diff --git a/spec/ruby/core/fiber/alive_spec.rb b/spec/ruby/core/fiber/alive_spec.rb
new file mode 100644
index 0000000000..6fb1229d95
--- /dev/null
+++ b/spec/ruby/core/fiber/alive_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+
+describe "Fiber#alive?" do
+ it "returns true for a Fiber that hasn't had #resume called" do
+ fiber = Fiber.new { true }
+ fiber.alive?.should == true
+ end
+
+ # FIXME: Better description?
+ it "returns true for a Fiber that's yielded to the caller" do
+ fiber = Fiber.new { Fiber.yield }
+ fiber.resume
+ fiber.alive?.should == true
+ end
+
+ it "returns true when called from its Fiber" do
+ fiber = Fiber.new { fiber.alive?.should == true }
+ fiber.resume
+ end
+
+ it "doesn't invoke the block associated with the Fiber" do
+ offthehook = mock('do not call')
+ offthehook.should_not_receive(:ring)
+ fiber = Fiber.new { offthehook.ring }
+ fiber.alive?
+ end
+
+ it "returns false for a Fiber that's dead" do
+ fiber = Fiber.new { true }
+ fiber.resume
+ -> { fiber.resume }.should.raise(FiberError)
+ fiber.alive?.should == false
+ end
+
+ it "always returns false for a dead Fiber" do
+ fiber = Fiber.new { true }
+ fiber.resume
+ -> { fiber.resume }.should.raise(FiberError)
+ fiber.alive?.should == false
+ -> { fiber.resume }.should.raise(FiberError)
+ fiber.alive?.should == false
+ fiber.alive?.should == false
+ end
+end
diff --git a/spec/ruby/core/fiber/blocking_spec.rb b/spec/ruby/core/fiber/blocking_spec.rb
new file mode 100644
index 0000000000..d5caf81fbe
--- /dev/null
+++ b/spec/ruby/core/fiber/blocking_spec.rb
@@ -0,0 +1,73 @@
+require_relative '../../spec_helper'
+require_relative 'shared/blocking'
+
+describe "Fiber.blocking?" do
+ it_behaves_like :non_blocking_fiber, -> { Fiber.blocking? }
+
+ context "when fiber is blocking" do
+ context "root Fiber of the main thread" do
+ it "returns 1 for blocking: true" do
+ fiber = Fiber.new(blocking: true) { Fiber.blocking? }
+ blocking = fiber.resume
+
+ blocking.should == 1
+ end
+ end
+
+ context "root Fiber of a new thread" do
+ it "returns 1 for blocking: true" do
+ thread = Thread.new do
+ fiber = Fiber.new(blocking: true) { Fiber.blocking? }
+ blocking = fiber.resume
+
+ blocking.should == 1
+ end
+
+ thread.join
+ end
+ end
+ end
+end
+
+describe "Fiber#blocking?" do
+ it_behaves_like :non_blocking_fiber, -> { Fiber.current.blocking? }
+
+ context "when fiber is blocking" do
+ context "root Fiber of the main thread" do
+ it "returns true for blocking: true" do
+ fiber = Fiber.new(blocking: true) { Fiber.current.blocking? }
+ blocking = fiber.resume
+
+ blocking.should == true
+ end
+ end
+
+ context "root Fiber of a new thread" do
+ it "returns true for blocking: true" do
+ thread = Thread.new do
+ fiber = Fiber.new(blocking: true) { Fiber.current.blocking? }
+ blocking = fiber.resume
+
+ blocking.should == true
+ end
+
+ thread.join
+ end
+ end
+ end
+end
+
+describe "Fiber.blocking" do
+ context "when fiber is non-blocking" do
+ it "can become blocking" do
+ fiber = Fiber.new(blocking: false) do
+ Fiber.blocking do |f|
+ f.blocking? ? :blocking : :non_blocking
+ end
+ end
+
+ blocking = fiber.resume
+ blocking.should == :blocking
+ end
+ end
+end
diff --git a/spec/ruby/core/fiber/current_spec.rb b/spec/ruby/core/fiber/current_spec.rb
new file mode 100644
index 0000000000..cc5c9117b6
--- /dev/null
+++ b/spec/ruby/core/fiber/current_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+
+describe "Fiber.current" do
+ it "returns the root Fiber when called outside of a Fiber" do
+ root = Fiber.current
+ root.should.instance_of?(Fiber)
+ # We can always transfer to the root Fiber; it will never die
+ 5.times do
+ root.transfer.should == nil
+ root.alive?.should == true
+ end
+ end
+
+ it "returns the current Fiber when called from a Fiber" do
+ fiber = Fiber.new do
+ this = Fiber.current
+ this.should.instance_of?(Fiber)
+ this.should == fiber
+ this.alive?.should == true
+ end
+ fiber.resume
+ end
+
+ it "returns the current Fiber when called from a Fiber that transferred to another" do
+ states = []
+ fiber = Fiber.new do
+ states << :fiber
+ this = Fiber.current
+ this.should.instance_of?(Fiber)
+ this.should == fiber
+ this.alive?.should == true
+ end
+
+ fiber2 = Fiber.new do
+ states << :fiber2
+ fiber.transfer
+ flunk
+ end
+
+ fiber3 = Fiber.new do
+ states << :fiber3
+ fiber2.transfer
+ states << :fiber3_terminated
+ end
+
+ fiber3.resume
+
+ states.should == [:fiber3, :fiber2, :fiber, :fiber3_terminated]
+ end
+end
diff --git a/spec/ruby/core/fiber/fixtures/classes.rb b/spec/ruby/core/fiber/fixtures/classes.rb
new file mode 100644
index 0000000000..6b0e0fbc42
--- /dev/null
+++ b/spec/ruby/core/fiber/fixtures/classes.rb
@@ -0,0 +1,22 @@
+module FiberSpecs
+
+ class NewFiberToRaise
+ def self.raise(*args, **kwargs, &block)
+ fiber = Fiber.new do
+ if block_given?
+ block.call do
+ Fiber.yield
+ end
+ else
+ Fiber.yield
+ end
+ end
+
+ fiber.resume
+
+ fiber.raise(*args, **kwargs)
+ end
+ end
+
+ class CustomError < StandardError; end
+end
diff --git a/spec/ruby/core/fiber/fixtures/scheduler.rb b/spec/ruby/core/fiber/fixtures/scheduler.rb
new file mode 100644
index 0000000000..16bd2f6b44
--- /dev/null
+++ b/spec/ruby/core/fiber/fixtures/scheduler.rb
@@ -0,0 +1,35 @@
+module FiberSpecs
+
+ class LoggingScheduler
+ attr_reader :events
+ def initialize
+ @events = []
+ end
+
+ def block(*args)
+ @events << { event: :block, fiber: Fiber.current, args: args }
+ Fiber.yield
+ end
+
+ def io_wait(*args)
+ @events << { event: :io_wait, fiber: Fiber.current, args: args }
+ Fiber.yield
+ end
+
+ def kernel_sleep(*args)
+ @events << { event: :kernel_sleep, fiber: Fiber.current, args: args }
+ Fiber.yield
+ end
+
+ def unblock(*args)
+ @events << { event: :unblock, fiber: Fiber.current, args: args }
+ Fiber.yield
+ end
+
+ def fiber_interrupt(*args)
+ @events << { event: :fiber_interrupt, fiber: Fiber.current, args: args }
+ Fiber.yield
+ end
+ end
+
+end
diff --git a/spec/ruby/core/fiber/inspect_spec.rb b/spec/ruby/core/fiber/inspect_spec.rb
new file mode 100644
index 0000000000..fcfef20716
--- /dev/null
+++ b/spec/ruby/core/fiber/inspect_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "Fiber#inspect" do
+ describe "status" do
+ it "is resumed for the root Fiber of a Thread" do
+ inspected = Thread.new { Fiber.current.inspect }.value
+ inspected.should =~ /\A#<Fiber:0x\h+ .*\(resumed\)>\z/
+ end
+
+ it "is created for a Fiber which did not run yet" do
+ inspected = Fiber.new {}.inspect
+ inspected.should =~ /\A#<Fiber:0x\h+ .+ \(created\)>\z/
+ end
+
+ it "is resumed for a Fiber which was resumed" do
+ inspected = Fiber.new { Fiber.current.inspect }.resume
+ inspected.should =~ /\A#<Fiber:0x\h+ .+ \(resumed\)>\z/
+ end
+
+ it "is resumed for a Fiber which was transferred" do
+ inspected = Fiber.new { Fiber.current.inspect }.transfer
+ inspected.should =~ /\A#<Fiber:0x\h+ .+ \(resumed\)>\z/
+ end
+
+ it "is suspended for a Fiber which was resumed and yielded" do
+ inspected = Fiber.new { Fiber.yield }.tap(&:resume).inspect
+ inspected.should =~ /\A#<Fiber:0x\h+ .+ \(suspended\)>\z/
+ end
+
+ it "is terminated for a Fiber which has terminated" do
+ inspected = Fiber.new {}.tap(&:resume).inspect
+ inspected.should =~ /\A#<Fiber:0x\h+ .+ \(terminated\)>\z/
+ end
+ end
+end
diff --git a/spec/ruby/core/fiber/kill_spec.rb b/spec/ruby/core/fiber/kill_spec.rb
new file mode 100644
index 0000000000..abf23ff176
--- /dev/null
+++ b/spec/ruby/core/fiber/kill_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/kernel/raise'
+
+describe "Fiber#kill" do
+ it "kills a non-resumed fiber" do
+ fiber = Fiber.new{}
+
+ fiber.alive?.should == true
+
+ fiber.kill
+ fiber.alive?.should == false
+ end
+
+ it "kills a resumed fiber" do
+ fiber = Fiber.new{while true; Fiber.yield; end}
+ fiber.resume
+
+ fiber.alive?.should == true
+
+ fiber.kill
+ fiber.alive?.should == false
+ end
+
+ it "can kill itself" do
+ fiber = Fiber.new do
+ Fiber.current.kill
+ end
+
+ fiber.alive?.should == true
+
+ fiber.resume
+ fiber.alive?.should == false
+ end
+
+ it "kills a resumed fiber from a child" do
+ parent = Fiber.new do
+ child = Fiber.new do
+ parent.kill
+ parent.alive?.should == true
+ end
+
+ child.resume
+ end
+
+ parent.resume
+ parent.alive?.should == false
+ end
+
+ it "executes the ensure block" do
+ ensure_executed = false
+
+ fiber = Fiber.new do
+ while true; Fiber.yield; end
+ ensure
+ ensure_executed = true
+ end
+
+ fiber.resume
+ fiber.kill
+ ensure_executed.should == true
+ end
+
+ it "does not execute rescue block" do
+ rescue_executed = false
+
+ fiber = Fiber.new do
+ while true; Fiber.yield; end
+ rescue Exception
+ rescue_executed = true
+ end
+
+ fiber.resume
+ fiber.kill
+ rescue_executed.should == false
+ end
+
+ it "repeatedly kills a fiber" do
+ fiber = Fiber.new do
+ while true; Fiber.yield; end
+ ensure
+ while true; Fiber.yield; end
+ end
+
+ fiber.kill
+ fiber.alive?.should == false
+ end
+end
diff --git a/spec/ruby/core/fiber/new_spec.rb b/spec/ruby/core/fiber/new_spec.rb
new file mode 100644
index 0000000000..d31167496d
--- /dev/null
+++ b/spec/ruby/core/fiber/new_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "Fiber.new" do
+ it "creates a fiber from the given block" do
+ fiber = Fiber.new {}
+ fiber.resume
+ fiber.should.instance_of?(Fiber)
+ end
+
+ it "creates a fiber from a subclass" do
+ class MyFiber < Fiber
+ end
+ fiber = MyFiber.new {}
+ fiber.resume
+ fiber.should.instance_of?(MyFiber)
+ end
+
+ it "raises an ArgumentError if called without a block" do
+ -> { Fiber.new }.should.raise(ArgumentError)
+ end
+
+ it "does not invoke the block" do
+ invoked = false
+ fiber = Fiber.new { invoked = true }
+ invoked.should == false
+ fiber.resume
+ end
+
+ it "closes over lexical environments" do
+ o = Object.new
+ def o.f
+ a = 1
+ f = Fiber.new { a = 2 }
+ f.resume
+ a
+ end
+ o.f.should == 2
+ end
+end
diff --git a/spec/ruby/core/fiber/raise_spec.rb b/spec/ruby/core/fiber/raise_spec.rb
new file mode 100644
index 0000000000..107e5bd4ce
--- /dev/null
+++ b/spec/ruby/core/fiber/raise_spec.rb
@@ -0,0 +1,141 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/kernel/raise'
+
+describe "Fiber#raise" do
+ it "is a public method" do
+ Fiber.public_instance_methods.should.include?(:raise)
+ end
+
+ it_behaves_like :kernel_raise, :raise, FiberSpecs::NewFiberToRaise
+ it_behaves_like :kernel_raise_across_contexts, :raise, FiberSpecs::NewFiberToRaise
+ ruby_version_is "4.0" do
+ it_behaves_like :kernel_raise_with_cause, :raise, FiberSpecs::NewFiberToRaise
+ end
+
+ it 'raises RuntimeError by default' do
+ -> { FiberSpecs::NewFiberToRaise.raise }.should.raise(RuntimeError)
+ end
+
+ it "raises FiberError if Fiber is not born" do
+ fiber = Fiber.new { true }
+ -> { fiber.raise }.should.raise(FiberError, "cannot raise exception on unborn fiber")
+ end
+
+ it "raises FiberError if Fiber is dead" do
+ fiber = Fiber.new { true }
+ fiber.resume
+ -> { fiber.raise }.should.raise(FiberError, /dead fiber called|attempt to resume a terminated fiber/)
+ end
+
+ it 'accepts error class' do
+ -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError }.should.raise(FiberSpecs::CustomError)
+ end
+
+ it 'accepts error message' do
+ -> { FiberSpecs::NewFiberToRaise.raise "error message" }.should.raise(RuntimeError, "error message")
+ end
+
+ it 'does not accept array of backtrace information only' do
+ -> { FiberSpecs::NewFiberToRaise.raise ['foo'] }.should.raise(TypeError)
+ end
+
+ it 'does not accept integer' do
+ -> { FiberSpecs::NewFiberToRaise.raise 100 }.should.raise(TypeError)
+ end
+
+ it 'accepts error class with error message' do
+ -> { FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error' }.should.raise(FiberSpecs::CustomError, 'test error')
+ end
+
+ it 'accepts error class with error message and backtrace information' do
+ -> {
+ FiberSpecs::NewFiberToRaise.raise FiberSpecs::CustomError, 'test error', ['foo', 'boo']
+ }.should.raise(FiberSpecs::CustomError) { |e|
+ e.message.should == 'test error'
+ e.backtrace.should == ['foo', 'boo']
+ }
+ end
+
+ it 'does not accept only error message and backtrace information' do
+ -> { FiberSpecs::NewFiberToRaise.raise 'test error', ['foo', 'boo'] }.should.raise(TypeError)
+ end
+
+ it "raises a FiberError if invoked from a different Thread" do
+ fiber = Fiber.new { Fiber.yield }
+ fiber.resume
+ Thread.new do
+ -> {
+ fiber.raise
+ }.should.raise(FiberError, "fiber called across threads")
+ end.join
+ end
+
+ it "kills Fiber" do
+ fiber = Fiber.new { Fiber.yield :first; :second }
+ fiber.resume
+ -> { fiber.raise }.should.raise
+ -> { fiber.resume }.should.raise(FiberError, /dead fiber called|attempt to resume a terminated fiber/)
+ end
+
+ it "returns to calling fiber after raise" do
+ fiber_one = Fiber.new do
+ Fiber.yield :yield_one
+ :unreachable
+ end
+
+ fiber_two = Fiber.new do
+ results = []
+ results << fiber_one.resume
+ begin
+ fiber_one.raise
+ rescue
+ results << :rescued
+ end
+ results
+ end
+
+ fiber_two.resume.should == [:yield_one, :rescued]
+ end
+
+ ruby_version_is "3.4" do
+ it "raises on the resumed fiber" do
+ root_fiber = Fiber.current
+ f1 = Fiber.new { root_fiber.transfer }
+ f2 = Fiber.new { f1.resume }
+ f2.transfer
+
+ -> do
+ f2.raise(RuntimeError, "Expected error")
+ end.should.raise(RuntimeError, "Expected error")
+ end
+
+ it "raises on itself" do
+ -> do
+ Fiber.current.raise(RuntimeError, "Expected error")
+ end.should.raise(RuntimeError, "Expected error")
+ end
+
+ it "should raise on parent fiber" do
+ f2 = nil
+ f1 = Fiber.new do
+ # This is equivalent to Kernel#raise:
+ f2.raise(RuntimeError, "Expected error")
+ end
+ f2 = Fiber.new do
+ f1.resume
+ end
+
+ -> do
+ f2.resume
+ end.should.raise(RuntimeError, "Expected error")
+ end
+ end
+
+ it "transfers and raises on a transferring fiber" do
+ root = Fiber.current
+ fiber = Fiber.new { root.transfer }
+ fiber.transfer
+ -> { fiber.raise "msg" }.should.raise(RuntimeError, "msg")
+ end
+end
diff --git a/spec/ruby/core/fiber/resume_spec.rb b/spec/ruby/core/fiber/resume_spec.rb
new file mode 100644
index 0000000000..e183cc10d9
--- /dev/null
+++ b/spec/ruby/core/fiber/resume_spec.rb
@@ -0,0 +1,83 @@
+require_relative '../../spec_helper'
+require_relative 'shared/resume'
+
+describe "Fiber#resume" do
+ it_behaves_like :fiber_resume, :resume
+end
+
+describe "Fiber#resume" do
+ it "runs until Fiber.yield" do
+ obj = mock('obj')
+ obj.should_not_receive(:do)
+ fiber = Fiber.new { 1 + 2; Fiber.yield; obj.do }
+ fiber.resume
+ end
+
+ it "resumes from the last call to Fiber.yield on subsequent invocations" do
+ fiber = Fiber.new { Fiber.yield :first; :second }
+ fiber.resume.should == :first
+ fiber.resume.should == :second
+ end
+
+ it "sets the block parameters to its arguments on the first invocation" do
+ first = mock('first')
+ first.should_receive(:arg).with(:first).twice
+
+ fiber = Fiber.new { |arg| first.arg arg; Fiber.yield; first.arg arg; }
+ fiber.resume :first
+ fiber.resume :second
+ end
+
+ it "raises a FiberError if the Fiber tries to resume itself" do
+ fiber = Fiber.new { fiber.resume }
+ -> { fiber.resume }.should.raise(FiberError, /current fiber/)
+ end
+
+ it "returns control to the calling Fiber if called from one" do
+ fiber1 = Fiber.new { :fiber1 }
+ fiber2 = Fiber.new { fiber1.resume; :fiber2 }
+ fiber2.resume.should == :fiber2
+ end
+
+ # Redmine #595
+ it "executes the ensure clause" do
+ code = <<-RUBY
+ f = Fiber.new do
+ begin
+ Fiber.yield
+ ensure
+ puts "ensure executed"
+ end
+ end
+
+ # The apparent issue is that when Fiber.yield executes, control
+ # "leaves" the "ensure block" and so the ensure clause should run. But
+ # control really does NOT leave the ensure block when Fiber.yield
+ # executes. It merely pauses there. To require ensure to run when a
+ # Fiber is suspended then makes ensure-in-a-Fiber-context different
+ # than ensure-in-a-Thread-context and this would be very confusing.
+ f.resume
+
+ # When we execute the second #resume call, the ensure block DOES exit,
+ # the ensure clause runs.
+ f.resume
+
+ exit 0
+ RUBY
+
+ ruby_exe(code).should == "ensure executed\n"
+ end
+
+ it "can work with Fiber#transfer" do
+ fiber1 = Fiber.new { true }
+ fiber2 = Fiber.new { fiber1.transfer; Fiber.yield 10 ; Fiber.yield 20; raise }
+ fiber2.resume.should == 10
+ fiber2.resume.should == 20
+ end
+
+ it "raises a FiberError if the Fiber attempts to resume a resuming fiber" do
+ root_fiber = Fiber.current
+ fiber1 = Fiber.new { root_fiber.resume }
+ -> { fiber1.resume }.should.raise(FiberError, /attempt to resume a resuming fiber/)
+ end
+end
diff --git a/spec/ruby/core/fiber/scheduler_spec.rb b/spec/ruby/core/fiber/scheduler_spec.rb
new file mode 100644
index 0000000000..15a03c1479
--- /dev/null
+++ b/spec/ruby/core/fiber/scheduler_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/scheduler'
+
+require "fiber"
+
+describe "Fiber.scheduler" do
+ it_behaves_like :scheduler, :scheduler
+end
diff --git a/spec/ruby/core/fiber/set_scheduler_spec.rb b/spec/ruby/core/fiber/set_scheduler_spec.rb
new file mode 100644
index 0000000000..82f6acbe86
--- /dev/null
+++ b/spec/ruby/core/fiber/set_scheduler_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/scheduler'
+
+require "fiber"
+
+describe "Fiber.scheduler" do
+ it_behaves_like :scheduler, :set_scheduler
+end
diff --git a/spec/ruby/core/fiber/shared/blocking.rb b/spec/ruby/core/fiber/shared/blocking.rb
new file mode 100644
index 0000000000..21707e1ea7
--- /dev/null
+++ b/spec/ruby/core/fiber/shared/blocking.rb
@@ -0,0 +1,41 @@
+describe :non_blocking_fiber, shared: true do
+ context "root Fiber of the main thread" do
+ it "returns false" do
+ fiber = Fiber.new { @method.call }
+ blocking = fiber.resume
+
+ blocking.should == false
+ end
+
+ it "returns false for blocking: false" do
+ fiber = Fiber.new(blocking: false) { @method.call }
+ blocking = fiber.resume
+
+ blocking.should == false
+ end
+ end
+
+ context "root Fiber of a new thread" do
+ it "returns false" do
+ thread = Thread.new do
+ fiber = Fiber.new { @method.call }
+ blocking = fiber.resume
+
+ blocking.should == false
+ end
+
+ thread.join
+ end
+
+ it "returns false for blocking: false" do
+ thread = Thread.new do
+ fiber = Fiber.new(blocking: false) { @method.call }
+ blocking = fiber.resume
+
+ blocking.should == false
+ end
+
+ thread.join
+ end
+ end
+end
diff --git a/spec/ruby/core/fiber/shared/resume.rb b/spec/ruby/core/fiber/shared/resume.rb
new file mode 100644
index 0000000000..ff4bb72c73
--- /dev/null
+++ b/spec/ruby/core/fiber/shared/resume.rb
@@ -0,0 +1,58 @@
+describe :fiber_resume, shared: true do
+ it "can be invoked from the root Fiber" do
+ fiber = Fiber.new { :fiber }
+ fiber.send(@method).should == :fiber
+ end
+
+ it "raises a FiberError if invoked from a different Thread" do
+ fiber = Fiber.new { 42 }
+ Thread.new do
+ -> {
+ fiber.send(@method)
+ }.should.raise(FiberError)
+ end.join
+
+ # Check the Fiber can still be used
+ fiber.send(@method).should == 42
+ end
+
+ it "passes control to the beginning of the block on first invocation" do
+ invoked = false
+ fiber = Fiber.new { invoked = true }
+ fiber.send(@method)
+ invoked.should == true
+ end
+
+ it "returns the last value encountered on first invocation" do
+ fiber = Fiber.new { 1+1; true }
+ fiber.send(@method).should == true
+ end
+
+ it "runs until the end of the block" do
+ obj = mock('obj')
+ obj.should_receive(:do).once
+ fiber = Fiber.new { 1 + 2; a = "glark"; obj.do }
+ fiber.send(@method)
+ end
+
+ it "accepts any number of arguments" do
+ fiber = Fiber.new { |a| }
+ -> { fiber.send(@method, *(1..10).to_a) }.should_not.raise
+ end
+
+ it "raises a FiberError if the Fiber is dead" do
+ fiber = Fiber.new { true }
+ fiber.send(@method)
+ -> { fiber.send(@method) }.should.raise(FiberError)
+ end
+
+ it "raises a LocalJumpError if the block includes a return statement" do
+ fiber = Fiber.new { return; }
+ -> { fiber.send(@method) }.should.raise(LocalJumpError)
+ end
+
+ it "raises a LocalJumpError if the block includes a break statement" do
+ fiber = Fiber.new { break; }
+ -> { fiber.send(@method) }.should.raise(LocalJumpError)
+ end
+end
diff --git a/spec/ruby/core/fiber/shared/scheduler.rb b/spec/ruby/core/fiber/shared/scheduler.rb
new file mode 100644
index 0000000000..04bdded53a
--- /dev/null
+++ b/spec/ruby/core/fiber/shared/scheduler.rb
@@ -0,0 +1,51 @@
+describe :scheduler, shared: true do
+ it "validates the scheduler for required methods" do
+ required_methods = [:block, :unblock, :kernel_sleep, :io_wait]
+ required_methods.each do |missing_method|
+ scheduler = Object.new
+ required_methods.difference([missing_method]).each do |method|
+ scheduler.define_singleton_method(method) {}
+ end
+ -> {
+ suppress_warning { Fiber.set_scheduler(scheduler) }
+ }.should.raise(ArgumentError, /Scheduler must implement ##{missing_method}/)
+ end
+ end
+
+ it "can set and get the scheduler" do
+ required_methods = [:block, :unblock, :kernel_sleep, :io_wait]
+ scheduler = Object.new
+ required_methods.each do |method|
+ scheduler.define_singleton_method(method) {}
+ end
+ suppress_warning { Fiber.set_scheduler(scheduler) }
+ Fiber.scheduler.should == scheduler
+ end
+
+ it "returns the scheduler after setting it" do
+ required_methods = [:block, :unblock, :kernel_sleep, :io_wait]
+ scheduler = Object.new
+ required_methods.each do |method|
+ scheduler.define_singleton_method(method) {}
+ end
+ result = suppress_warning { Fiber.set_scheduler(scheduler) }
+ result.should == scheduler
+ end
+
+ it "can remove the scheduler" do
+ required_methods = [:block, :unblock, :kernel_sleep, :io_wait]
+ scheduler = Object.new
+ required_methods.each do |method|
+ scheduler.define_singleton_method(method) {}
+ end
+ suppress_warning { Fiber.set_scheduler(scheduler) }
+ Fiber.set_scheduler(nil)
+ Fiber.scheduler.should == nil
+ end
+
+ it "can assign a nil scheduler multiple times" do
+ Fiber.set_scheduler(nil)
+ Fiber.set_scheduler(nil)
+ Fiber.scheduler.should == nil
+ end
+end
diff --git a/spec/ruby/core/fiber/storage_spec.rb b/spec/ruby/core/fiber/storage_spec.rb
new file mode 100644
index 0000000000..a3f6bf9cad
--- /dev/null
+++ b/spec/ruby/core/fiber/storage_spec.rb
@@ -0,0 +1,177 @@
+require_relative '../../spec_helper'
+
+describe "Fiber.new(storage:)" do
+ it "creates a Fiber with the given storage" do
+ storage = {life: 42}
+ fiber = Fiber.new(storage: storage) { Fiber.current.storage }
+ fiber.resume.should == storage
+ end
+
+ it "creates a fiber with lazily initialized storage" do
+ Fiber.new(storage: nil) { Fiber[:x] = 10; Fiber.current.storage }.resume.should == {x: 10}
+ end
+
+ it "creates a fiber by inheriting the storage of the parent fiber" do
+ fiber = Fiber.new(storage: {life: 42}) do
+ Fiber.new { Fiber.current.storage }.resume
+ end
+ fiber.resume.should == {life: 42}
+ end
+
+ it "cannot create a fiber with non-hash storage" do
+ -> { Fiber.new(storage: 42) {} }.should.raise(TypeError)
+ end
+
+ it "cannot create a fiber with a frozen hash as storage" do
+ -> { Fiber.new(storage: {life: 43}.freeze) {} }.should.raise(FrozenError)
+ end
+
+ it "cannot create a fiber with a storage hash with non-symbol keys" do
+ -> { Fiber.new(storage: {life: 43, Object.new => 44}) {} }.should.raise(TypeError)
+ end
+end
+
+describe "Fiber#storage" do
+ it "cannot be accessed from a different fiber" do
+ f = Fiber.new(storage: {life: 42}) { nil }
+ -> {
+ f.storage
+ }.should.raise(ArgumentError, /Fiber storage can only be accessed from the Fiber it belongs to/)
+ end
+end
+
+describe "Fiber#storage=" do
+ it "can clear the storage of the fiber" do
+ fiber = Fiber.new(storage: {life: 42}) do
+ Fiber.current.storage = nil
+ Fiber[:x] = 10
+ Fiber.current.storage
+ end
+ fiber.resume.should == {x: 10}
+ end
+
+ it "can set the storage of the fiber" do
+ fiber = Fiber.new(storage: {life: 42}) do
+ Fiber.current.storage = {life: 43}
+ Fiber.current.storage
+ end
+ fiber.resume.should == {life: 43}
+ end
+
+ it "can't set the storage of the fiber to non-hash" do
+ -> { Fiber.current.storage = 42 }.should.raise(TypeError)
+ end
+
+ it "can't set the storage of the fiber to a frozen hash" do
+ -> { Fiber.current.storage = {life: 43}.freeze }.should.raise(FrozenError)
+ end
+
+ it "can't set the storage of the fiber to a hash with non-symbol keys" do
+ -> { Fiber.current.storage = {life: 43, Object.new => 44} }.should.raise(TypeError)
+ end
+end
+
+describe "Fiber.[]" do
+ it "returns the value of the given key in the storage of the current fiber" do
+ Fiber.new(storage: {life: 42}) { Fiber[:life] }.resume.should == 42
+ end
+
+ it "returns nil if the key is not present in the storage of the current fiber" do
+ Fiber.new(storage: {life: 42}) { Fiber[:death] }.resume.should == nil
+ end
+
+ it "returns nil if the current fiber has no storage" do
+ Fiber.new { Fiber[:life] }.resume.should == nil
+ end
+
+ it "can use dynamically defined keys" do
+ key = :"#{self.class.name}#.#{self.object_id}"
+ Fiber.new { Fiber[key] = 42; Fiber[key] }.resume.should == 42
+ end
+
+ it "can't use invalid keys" do
+ invalid_keys = [Object.new, 12]
+ invalid_keys.each do |key|
+ -> { Fiber[key] }.should.raise(TypeError)
+ end
+ end
+
+ ruby_bug "#20978", ""..."3.4" do
+ it "can use keys as strings" do
+ key = Object.new
+ def key.to_str; "Foo"; end
+ Fiber.new { Fiber[key] = 42; Fiber["Foo"] }.resume.should == 42
+ end
+
+ it "converts a String key into a Symbol" do
+ Fiber.new { Fiber["key"] = 42; Fiber[:key] }.resume.should == 42
+ Fiber.new { Fiber[:key] = 42; Fiber["key"] }.resume.should == 42
+ end
+
+ it "can use any object that responds to #to_str as a key" do
+ key = mock("key")
+ key.should_receive(:to_str).twice.and_return("key")
+ Fiber.new { Fiber[key] = 42; Fiber[key] }.resume.should == 42
+ end
+ end
+
+ it "does not call #to_sym on the key" do
+ key = mock("key")
+ key.should_not_receive(:to_sym)
+ -> { Fiber[key] }.should.raise(TypeError)
+ end
+
+ it "can access the storage of the parent fiber" do
+ f = Fiber.new(storage: {life: 42}) do
+ Fiber.new { Fiber[:life] }.resume
+ end
+ f.resume.should == 42
+ end
+
+ it "can't access the storage of the fiber with non-symbol keys" do
+ -> { Fiber[Object.new] }.should.raise(TypeError)
+ end
+end
+
+describe "Fiber.[]=" do
+ it "sets the value of the given key in the storage of the current fiber" do
+ Fiber.new(storage: {life: 42}) { Fiber[:life] = 43; Fiber[:life] }.resume.should == 43
+ end
+
+ it "sets the value of the given key in the storage of the current fiber" do
+ Fiber.new(storage: {life: 42}) { Fiber[:death] = 43; Fiber[:death] }.resume.should == 43
+ end
+
+ it "sets the value of the given key in the storage of the current fiber" do
+ Fiber.new { Fiber[:life] = 43; Fiber[:life] }.resume.should == 43
+ end
+
+ it "does not overwrite the storage of the parent fiber" do
+ f = Fiber.new(storage: {life: 42}) do
+ Fiber.yield Fiber.new { Fiber[:life] = 43; Fiber[:life] }.resume
+ Fiber[:life]
+ end
+ f.resume.should == 43 # Value of the inner fiber
+ f.resume.should == 42 # Value of the outer fiber
+ end
+
+ it "can't access the storage of the fiber with non-symbol keys" do
+ -> { Fiber[Object.new] = 44 }.should.raise(TypeError)
+ end
+
+ it "deletes the fiber storage key when assigning nil" do
+ Fiber.new(storage: {life: 42}) {
+ Fiber[:life] = nil
+ Fiber.current.storage
+ }.resume.should == {}
+ end
+end
+
+describe "Thread.new" do
+ it "creates a thread with the storage of the current fiber" do
+ fiber = Fiber.new(storage: {life: 42}) do
+ Thread.new { Fiber.current.storage }.value
+ end
+ fiber.resume.should == {life: 42}
+ end
+end
diff --git a/spec/ruby/core/fiber/transfer_spec.rb b/spec/ruby/core/fiber/transfer_spec.rb
new file mode 100644
index 0000000000..d8737aeeb3
--- /dev/null
+++ b/spec/ruby/core/fiber/transfer_spec.rb
@@ -0,0 +1,84 @@
+require_relative '../../spec_helper'
+require_relative 'shared/resume'
+
+describe "Fiber#transfer" do
+ it_behaves_like :fiber_resume, :transfer
+end
+
+describe "Fiber#transfer" do
+ it "transfers control from one Fiber to another when called from a Fiber" do
+ fiber1 = Fiber.new { :fiber1 }
+ fiber2 = Fiber.new { fiber1.transfer; :fiber2 }
+ fiber2.resume.should == :fiber2
+ end
+
+ it "returns to the root Fiber when finished" do
+ f1 = Fiber.new { :fiber_1 }
+ f2 = Fiber.new { f1.transfer; :fiber_2 }
+
+ f2.transfer.should == :fiber_1
+ f2.transfer.should == :fiber_2
+ end
+
+ it "can be invoked from the same Fiber it transfers control to" do
+ states = []
+ fiber = Fiber.new { states << :start; fiber.transfer; states << :end }
+ fiber.transfer
+ states.should == [:start, :end]
+
+ states = []
+ fiber = Fiber.new { states << :start; fiber.transfer; states << :end }
+ fiber.resume
+ states.should == [:start, :end]
+ end
+
+ it "can not transfer control to a Fiber that has suspended by Fiber.yield" do
+ states = []
+ fiber1 = Fiber.new { states << :fiber1 }
+ fiber2 = Fiber.new { states << :fiber2_start; Fiber.yield fiber1.transfer; states << :fiber2_end}
+ fiber2.resume.should == [:fiber2_start, :fiber1]
+ -> { fiber2.transfer }.should.raise(FiberError)
+ end
+
+ it "raises a FiberError when transferring to a Fiber which resumes itself" do
+ fiber = Fiber.new { fiber.resume }
+ -> { fiber.transfer }.should.raise(FiberError)
+ end
+
+ it "works if Fibers in different Threads each transfer to a Fiber in the same Thread" do
+ # This catches a bug where Fibers are running on a thread-pool
+ # and Fibers from a different Ruby Thread reuse the same native thread.
+ # Caching the Ruby Thread based on the native thread is not correct in that case,
+ # and the check for "fiber called across threads" in Fiber#transfer
+ # might be incorrect based on that.
+ 2.times do
+ Thread.new do
+ io_fiber = Fiber.new do |calling_fiber|
+ calling_fiber.transfer
+ end
+ io_fiber.transfer(Fiber.current)
+ value = Object.new
+ io_fiber.transfer(value).should.equal? value
+ end.join
+ end
+ end
+
+ it "transfers control between a non-main thread's root fiber to a child fiber and back again" do
+ states = []
+ thread = Thread.new do
+ f1 = Fiber.new do |f0|
+ states << 0
+ value2 = f0.transfer(1)
+ states << value2
+ 3
+ end
+
+ value1 = f1.transfer(Fiber.current)
+ states << value1
+ value3 = f1.transfer(2)
+ states << value3
+ end
+ thread.join
+ states.should == [0, 1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/fiber/yield_spec.rb b/spec/ruby/core/fiber/yield_spec.rb
new file mode 100644
index 0000000000..12ec6ebcef
--- /dev/null
+++ b/spec/ruby/core/fiber/yield_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+
+describe "Fiber.yield" do
+ it "passes control to the Fiber's caller" do
+ step = 0
+ fiber = Fiber.new { step = 1; Fiber.yield; step = 2; Fiber.yield; step = 3 }
+ fiber.resume
+ step.should == 1
+ fiber.resume
+ step.should == 2
+ end
+
+ it "returns its arguments to the caller" do
+ fiber = Fiber.new { true; Fiber.yield :glark; true }
+ fiber.resume.should == :glark
+ fiber.resume
+ end
+
+ it "returns nil to the caller if given no arguments" do
+ fiber = Fiber.new { true; Fiber.yield; true }
+ fiber.resume.should == nil
+ fiber.resume
+ end
+
+ it "returns to the Fiber the value of the #resume call that invoked it" do
+ fiber = Fiber.new { Fiber.yield.should == :caller }
+ fiber.resume
+ fiber.resume :caller
+ end
+
+ it "does not propagate or reraise a rescued exception" do
+ fiber = Fiber.new do
+ begin
+ raise "an error in a Fiber"
+ rescue
+ Fiber.yield :first
+ end
+
+ :second
+ end
+
+ fiber.resume.should == :first
+ fiber.resume.should == :second
+ end
+
+ it "raises a FiberError if called from the root Fiber" do
+ ->{ Fiber.yield }.should.raise(FiberError)
+ end
+end
diff --git a/spec/ruby/core/file/absolute_path_spec.rb b/spec/ruby/core/file/absolute_path_spec.rb
new file mode 100644
index 0000000000..fc12985a75
--- /dev/null
+++ b/spec/ruby/core/file/absolute_path_spec.rb
@@ -0,0 +1,94 @@
+require_relative '../../spec_helper'
+
+describe "File.absolute_path?" do
+ before :each do
+ @abs = File.expand_path(__FILE__)
+ end
+
+ it "returns true if it's an absolute pathname" do
+ File.absolute_path?(@abs).should == true
+ end
+
+ it "returns false if it's a relative path" do
+ File.absolute_path?(File.basename(__FILE__)).should == false
+ end
+
+ it "returns false if it's a tricky relative path" do
+ File.absolute_path?("C:foo\\bar").should == false
+ end
+
+ it "does not expand '~' to a home directory." do
+ File.absolute_path?('~').should == false
+ end
+
+ it "does not expand '~user' to a home directory." do
+ path = File.dirname(@abs)
+ Dir.chdir(path) do
+ File.absolute_path?('~user').should == false
+ end
+ end
+
+ it "calls #to_path on its argument" do
+ mock = mock_to_path(File.expand_path(__FILE__))
+
+ File.absolute_path?(mock).should == true
+ end
+
+ platform_is_not :windows do
+ it "takes into consideration the platform's root" do
+ File.absolute_path?("C:\\foo\\bar").should == false
+ File.absolute_path?("C:/foo/bar").should == false
+ File.absolute_path?("/foo/bar\\baz").should == true
+ end
+ end
+
+ platform_is :windows do
+ it "takes into consideration the platform path separator(s)" do
+ File.absolute_path?("C:\\foo\\bar").should == true
+ File.absolute_path?("C:/foo/bar").should == true
+ File.absolute_path?("/foo/bar\\baz").should == false
+ end
+ end
+end
+
+describe "File.absolute_path" do
+ before :each do
+ @abs = File.expand_path(__FILE__)
+ end
+
+ it "returns the argument if it's an absolute pathname" do
+ File.absolute_path(@abs).should == @abs
+ end
+
+ it "resolves paths relative to the current working directory" do
+ path = File.dirname(@abs)
+ Dir.chdir(path) do
+ File.absolute_path('hello.txt').should == File.join(Dir.pwd, 'hello.txt')
+ end
+ end
+
+ it "does not expand '~' to a home directory." do
+ File.absolute_path('~').should_not == File.expand_path('~')
+ end
+
+ platform_is_not :windows do
+ it "does not expand '~' when given dir argument" do
+ File.absolute_path('~', '/').should == '/~'
+ end
+ end
+
+ it "does not expand '~user' to a home directory." do
+ path = File.dirname(@abs)
+ Dir.chdir(path) do
+ File.absolute_path('~user').should == File.join(Dir.pwd, '~user')
+ end
+ end
+
+ it "accepts a second argument of a directory from which to resolve the path" do
+ File.absolute_path(__FILE__, __dir__).should == @abs
+ end
+
+ it "calls #to_path on its argument" do
+ File.absolute_path(mock_to_path(@abs)).should == @abs
+ end
+end
diff --git a/spec/ruby/core/file/atime_spec.rb b/spec/ruby/core/file/atime_spec.rb
new file mode 100644
index 0000000000..5c6c110eec
--- /dev/null
+++ b/spec/ruby/core/file/atime_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "File.atime" do
+ before :each do
+ @file = tmp('test.txt')
+ touch @file
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the last access time for the named file as a Time object" do
+ File.atime(@file)
+ File.atime(@file).should.is_a?(Time)
+ end
+
+ platform_is :linux, :windows do
+ unless ENV.key?('TRAVIS') # https://bugs.ruby-lang.org/issues/17926
+ ## NOTE also that some Linux systems disable atime (e.g. via mount params) for better filesystem speed.
+ it "returns the last access time for the named file with microseconds" do
+ supports_subseconds = Integer(`stat -c%x '#{__FILE__}'`[/\.(\d{1,6})/, 1], 10)
+ if supports_subseconds != 0
+ expected_time = Time.at(Time.now.to_i + 0.123456)
+ File.utime expected_time, 0, @file
+ File.atime(@file).usec.should == expected_time.usec
+ else
+ File.atime(__FILE__).usec.should == 0
+ end
+ rescue Errno::ENOENT => e
+ # Native Windows don't have stat command.
+ skip e.message
+ end
+ end
+ end
+
+ it "raises an Errno::ENOENT exception if the file is not found" do
+ -> { File.atime('a_fake_file') }.should.raise(Errno::ENOENT)
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.atime(mock_to_path(@file))
+ end
+end
+
+describe "File#atime" do
+ before :each do
+ @name = File.expand_path(__FILE__)
+ @file = File.open(@name)
+ end
+
+ after :each do
+ @file.close rescue nil
+ end
+
+ it "returns the last access time to self" do
+ @file.atime
+ @file.atime.should.is_a?(Time)
+ end
+end
diff --git a/spec/ruby/core/file/basename_spec.rb b/spec/ruby/core/file/basename_spec.rb
new file mode 100644
index 0000000000..77afe5c22f
--- /dev/null
+++ b/spec/ruby/core/file/basename_spec.rb
@@ -0,0 +1,205 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+# TODO: Fix these
+describe "File.basename" do
+ it "returns the basename of a path (basic cases)" do
+ File.basename("/Some/path/to/test.txt").should == "test.txt"
+ File.basename(File.join("/tmp")).should == "tmp"
+ File.basename(File.join(*%w( g f d s a b))).should == "b"
+ File.basename("/tmp", ".*").should == "tmp"
+ File.basename("/tmp", ".c").should == "tmp"
+ File.basename("/tmp.c", ".c").should == "tmp"
+ File.basename("/tmp.c", ".*").should == "tmp"
+ File.basename("/tmp.c", ".?").should == "tmp.c"
+ File.basename("/tmp.cpp", ".*").should == "tmp"
+ File.basename("/tmp.cpp", ".???").should == "tmp.cpp"
+ File.basename("/tmp.o", ".c").should == "tmp.o"
+ File.basename(File.join("/tmp/")).should == "tmp"
+ File.basename("/").should == "/"
+ File.basename("//").should == "/"
+ File.basename("dir///base", ".*").should == "base"
+ File.basename("dir///base", ".c").should == "base"
+ File.basename("dir///base.c", ".c").should == "base"
+ File.basename("dir///base.c", ".*").should == "base"
+ File.basename("dir///base.o", ".c").should == "base.o"
+ File.basename("dir///base///").should == "base"
+ File.basename("dir//base/", ".*").should == "base"
+ File.basename("dir//base/", ".c").should == "base"
+ File.basename("dir//base.c/", ".c").should == "base"
+ File.basename("dir//base.c/", ".*").should == "base"
+ end
+
+ it "returns the last component of the filename" do
+ File.basename('a').should == 'a'
+ File.basename('/a').should == 'a'
+ File.basename('/a/b').should == 'b'
+ File.basename('/ab/ba/bag').should == 'bag'
+ File.basename('/ab/ba/bag.txt').should == 'bag.txt'
+ File.basename('/').should == '/'
+ File.basename('/foo/bar/baz.rb', '.rb').should == 'baz'
+ File.basename('baz.rb', 'z.rb').should == 'ba'
+ end
+
+ it "returns an string" do
+ File.basename("foo").should.is_a?(String)
+ end
+
+ it "returns the basename for unix format" do
+ File.basename("/foo/bar").should == "bar"
+ File.basename("/foo/bar.txt").should == "bar.txt"
+ File.basename("bar.c").should == "bar.c"
+ File.basename("/bar").should == "bar"
+ File.basename("/bar/").should == "bar"
+
+ # Considered UNC paths on Windows
+ platform_is :windows do
+ File.basename("baz//foo").should =="foo"
+ File.basename("//foo/bar/baz").should == "baz"
+ end
+ end
+
+ it "returns the basename for edge cases" do
+ File.basename("").should == ""
+ File.basename(".").should == "."
+ File.basename("..").should == ".."
+ platform_is_not :windows do
+ File.basename("//foo/").should == "foo"
+ File.basename("//foo//").should == "foo"
+ end
+ File.basename("foo/").should == "foo"
+ end
+
+ it "ignores a trailing directory separator" do
+ File.basename("foo.rb/", '.rb').should == "foo"
+ File.basename("bar.rb///", '.*').should == "bar"
+ end
+
+ it "returns the basename for unix suffix" do
+ File.basename("bar.c", ".c").should == "bar"
+ File.basename("bar.txt", ".txt").should == "bar"
+ File.basename("/bar.txt", ".txt").should == "bar"
+ File.basename("/foo/bar.txt", ".txt").should == "bar"
+ File.basename("bar.txt", ".exe").should == "bar.txt"
+ File.basename("bar.txt.exe", ".exe").should == "bar.txt"
+ File.basename("bar.txt.exe", ".txt").should == "bar.txt.exe"
+ File.basename("bar.txt", ".*").should == "bar"
+ File.basename("bar.txt.exe", ".*").should == "bar.txt"
+ File.basename("bar.txt.exe", ".txt.exe").should == "bar"
+ end
+
+ platform_is_not :windows do
+ it "takes into consideration the platform path separator(s)" do
+ File.basename("C:\\foo\\bar").should == "C:\\foo\\bar"
+ File.basename("C:/foo/bar").should == "bar"
+ File.basename("/foo/bar\\baz").should == "bar\\baz"
+ end
+ end
+
+ platform_is :windows do
+ it "takes into consideration the platform path separator(s)" do
+ File.basename("C:\\foo\\bar").should == "bar"
+ File.basename("C:/foo/bar").should == "bar"
+ File.basename("/foo/bar\\baz").should == "baz"
+ end
+ end
+
+ it "raises a TypeError if the arguments are not String types" do
+ -> { File.basename(nil) }.should.raise(TypeError)
+ -> { File.basename(1) }.should.raise(TypeError)
+ -> { File.basename("bar.txt", 1) }.should.raise(TypeError)
+ -> { File.basename(true) }.should.raise(TypeError)
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.basename(mock_to_path("foo.txt"))
+ end
+
+ it "raises an ArgumentError if passed more than two arguments" do
+ -> { File.basename('bar.txt', '.txt', '.txt') }.should.raise(ArgumentError)
+ end
+
+ # specific to MS Windows
+ platform_is :windows do
+ it "returns the basename for windows" do
+ File.basename("C:\\foo\\bar\\baz.txt").should == "baz.txt"
+ File.basename("C:\\foo\\bar").should == "bar"
+ File.basename("C:\\foo\\bar\\").should == "bar"
+ File.basename("C:\\foo").should == "foo"
+ File.basename("C:\\").should == "\\"
+ end
+
+ it "returns basename windows unc" do
+ File.basename("\\\\foo\\bar\\baz.txt").should == "baz.txt"
+ File.basename("\\\\foo\\bar\\baz").should =="baz"
+ end
+
+ it "returns basename windows forward slash" do
+ File.basename("C:/").should == "/"
+ File.basename("C:/foo").should == "foo"
+ File.basename("C:/foo/bar").should == "bar"
+ File.basename("C:/foo/bar/").should == "bar"
+ File.basename("C:/foo/bar//").should == "bar"
+ end
+
+ it "returns basename with windows suffix" do
+ File.basename("c:\\bar.txt", ".txt").should == "bar"
+ File.basename("c:\\foo\\bar.txt", ".txt").should == "bar"
+ File.basename("c:\\bar.txt", ".exe").should == "bar.txt"
+ File.basename("c:\\bar.txt.exe", ".exe").should == "bar.txt"
+ File.basename("c:\\bar.txt.exe", ".txt").should == "bar.txt.exe"
+ File.basename("c:\\bar.txt", ".*").should == "bar"
+ File.basename("c:\\bar.txt.exe", ".*").should == "bar.txt"
+ end
+
+ it "handles Shift JIS 0x5C (\\) as second byte of a multi-byte sequence" do
+ # dir\fileソname.txt
+ path = "dir\\file\x83\x5cname.txt".b.force_encoding(Encoding::SHIFT_JIS)
+ path.valid_encoding?.should == true
+ File.basename(path).should == "file\x83\x5cname.txt".b.force_encoding(Encoding::SHIFT_JIS)
+ end
+ end
+
+ it "rejects strings encoded with non ASCII-compatible encodings" do
+ Encoding.list.reject(&:ascii_compatible?).reject(&:dummy?).each do |enc|
+ path = "/foo/bar".encode(enc)
+
+ -> {
+ File.basename(path)
+ }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ it "works with all ASCII-compatible encodings" do
+ Encoding.list.select(&:ascii_compatible?).each do |enc|
+ File.basename("/foo/bar".encode(enc)).should == "bar".encode(enc)
+ end
+ end
+
+ it "returns the extension for a multibyte filename" do
+ File.basename('/path/ОфиÑ.m4a').should == "ОфиÑ.m4a"
+ end
+
+ it "returns the basename with the same encoding as the original" do
+ basename = File.basename('C:/Users/Scuby Pagrubý'.encode(Encoding::Windows_1250))
+ basename.should == 'Scuby Pagrubý'.encode(Encoding::Windows_1250)
+ basename.encoding.should == Encoding::Windows_1250
+ end
+
+ it "returns a new unfrozen String" do
+ exts = [nil, '.rb', '.*', '.txt']
+ ['foo.rb','//', '/test/', 'test'].each do |example|
+ exts.each do |ext|
+ original = example.freeze
+ result = if ext
+ File.basename(original, ext)
+ else
+ File.basename(original)
+ end
+ result.should_not.equal?(original)
+ result.frozen?.should == false
+ end
+ end
+ end
+
+end
diff --git a/spec/ruby/core/file/birthtime_spec.rb b/spec/ruby/core/file/birthtime_spec.rb
new file mode 100644
index 0000000000..039fd7572c
--- /dev/null
+++ b/spec/ruby/core/file/birthtime_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+
+platform_is :windows, :darwin, :freebsd, :netbsd, :linux do
+ not_implemented_messages = [
+ "birthtime() function is unimplemented", # unsupported OS/version
+ "birthtime is unimplemented", # unsupported filesystem
+ ]
+
+ describe "File.birthtime" do
+ before :each do
+ @file = __FILE__
+ end
+
+ after :each do
+ @file = nil
+ end
+
+ it "returns the birth time for the named file as a Time object" do
+ File.birthtime(@file)
+ File.birthtime(@file).should.is_a?(Time)
+ rescue NotImplementedError => e
+ e.message.should.start_with?(*not_implemented_messages)
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.birthtime(@file) # Avoid to failure of mock object with old Kernel and glibc
+ File.birthtime(mock_to_path(@file))
+ rescue NotImplementedError => e
+ e.message.should.start_with?(*not_implemented_messages)
+ end
+
+ it "raises an Errno::ENOENT exception if the file is not found" do
+ -> { File.birthtime('bogus') }.should.raise(Errno::ENOENT)
+ rescue NotImplementedError => e
+ e.message.should.start_with?(*not_implemented_messages)
+ end
+ end
+
+ describe "File#birthtime" do
+ before :each do
+ @file = File.open(__FILE__)
+ end
+
+ after :each do
+ @file.close
+ @file = nil
+ end
+
+ it "returns the birth time for self" do
+ @file.birthtime
+ @file.birthtime.should.is_a?(Time)
+ rescue NotImplementedError => e
+ e.message.should.start_with?(*not_implemented_messages)
+ end
+ end
+end
diff --git a/spec/ruby/core/file/blockdev_spec.rb b/spec/ruby/core/file/blockdev_spec.rb
new file mode 100644
index 0000000000..9ba9afc251
--- /dev/null
+++ b/spec/ruby/core/file/blockdev_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/blockdev'
+
+describe "File.blockdev?" do
+ it_behaves_like :file_blockdev, :blockdev?, File
+end
diff --git a/spec/ruby/core/file/chardev_spec.rb b/spec/ruby/core/file/chardev_spec.rb
new file mode 100644
index 0000000000..1fc932ee4e
--- /dev/null
+++ b/spec/ruby/core/file/chardev_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/chardev'
+
+describe "File.chardev?" do
+ it_behaves_like :file_chardev, :chardev?, File
+end
diff --git a/spec/ruby/core/file/chmod_spec.rb b/spec/ruby/core/file/chmod_spec.rb
new file mode 100644
index 0000000000..e0fd10ceb1
--- /dev/null
+++ b/spec/ruby/core/file/chmod_spec.rb
@@ -0,0 +1,185 @@
+require_relative '../../spec_helper'
+
+describe "File#chmod" do
+ before :each do
+ @filename = tmp('i_exist.exe')
+ @file = File.open(@filename, 'w')
+ end
+
+ after :each do
+ @file.close
+ rm_r @filename
+ end
+
+ it "returns 0 if successful" do
+ @file.chmod(0755).should == 0
+ end
+
+ it "raises RangeError with too large values" do
+ -> { @file.chmod(2**64) }.should.raise(RangeError)
+ -> { @file.chmod(-2**63 - 1) }.should.raise(RangeError)
+ end
+
+ it "invokes to_int on non-integer argument" do
+ mode = File.stat(@filename).mode
+ (obj = mock('mode')).should_receive(:to_int).and_return(mode)
+ @file.chmod(obj)
+ File.stat(@filename).mode.should == mode
+ end
+
+ platform_is :windows do
+ it "with '0444' makes file readable and executable but not writable" do
+ @file.chmod(0444)
+ File.readable?(@filename).should == true
+ File.writable?(@filename).should == false
+ File.executable?(@filename).should == true
+ end
+
+ it "with '0644' makes file readable and writable and also executable" do
+ @file.chmod(0644)
+ File.readable?(@filename).should == true
+ File.writable?(@filename).should == true
+ File.executable?(@filename).should == true
+ end
+ end
+
+ platform_is_not :windows do
+ as_user do
+ it "with '0222' makes file writable but not readable or executable" do
+ @file.chmod(0222)
+ File.readable?(@filename).should == false
+ File.writable?(@filename).should == true
+ File.executable?(@filename).should == false
+ end
+
+ it "with '0444' makes file readable but not writable or executable" do
+ @file.chmod(0444)
+ File.readable?(@filename).should == true
+ File.writable?(@filename).should == false
+ File.executable?(@filename).should == false
+ end
+
+ it "with '0666' makes file readable and writable but not executable" do
+ @file.chmod(0666)
+ File.readable?(@filename).should == true
+ File.writable?(@filename).should == true
+ File.executable?(@filename).should == false
+ end
+
+ it "with '0111' makes file executable but not readable or writable" do
+ @file.chmod(0111)
+ File.readable?(@filename).should == false
+ File.writable?(@filename).should == false
+ File.executable?(@filename).should == true
+ end
+
+ it "modifies the permission bits of the files specified" do
+ @file.chmod(0755)
+ File.stat(@filename).mode.should == 33261
+ end
+ end
+ end
+end
+
+describe "File.chmod" do
+ before :each do
+ @file = tmp('i_exist.exe')
+ touch @file
+ @count = File.chmod(0755, @file)
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the number of files modified" do
+ @count.should == 1
+ end
+
+ it "raises RangeError with too large values" do
+ -> { File.chmod(2**64, @file) }.should.raise(RangeError)
+ -> { File.chmod(-2**63 - 1, @file) }.should.raise(RangeError)
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.chmod(0, mock_to_path(@file))
+ end
+
+ it "throws a TypeError if the given path is not coercible into a string" do
+ -> { File.chmod(0, []) }.should.raise(TypeError)
+ end
+
+ it "raises an error for a non existent path" do
+ -> {
+ File.chmod(0644, "#{@file}.not.existing")
+ }.should.raise(Errno::ENOENT)
+ end
+
+ it "invokes to_int on non-integer argument" do
+ mode = File.stat(@file).mode
+ (obj = mock('mode')).should_receive(:to_int).and_return(mode)
+ File.chmod(obj, @file)
+ File.stat(@file).mode.should == mode
+ end
+
+ it "invokes to_str on non-string file names" do
+ mode = File.stat(@file).mode
+ (obj = mock('path')).should_receive(:to_str).and_return(@file)
+ File.chmod(mode, obj)
+ File.stat(@file).mode.should == mode
+ end
+
+ platform_is :windows do
+ it "with '0444' makes file readable and executable but not writable" do
+ File.chmod(0444, @file)
+ File.readable?(@file).should == true
+ File.writable?(@file).should == false
+ File.executable?(@file).should == true
+ end
+
+ it "with '0644' makes file readable and writable and also executable" do
+ File.chmod(0644, @file)
+ File.readable?(@file).should == true
+ File.writable?(@file).should == true
+ File.executable?(@file).should == true
+ end
+ end
+
+ platform_is_not :windows do
+ as_user do
+ it "with '0222' makes file writable but not readable or executable" do
+ File.chmod(0222, @file)
+ File.readable?(@file).should == false
+ File.writable?(@file).should == true
+ File.executable?(@file).should == false
+ end
+
+ it "with '0444' makes file readable but not writable or executable" do
+ File.chmod(0444, @file)
+ File.readable?(@file).should == true
+ File.writable?(@file).should == false
+ File.executable?(@file).should == false
+ end
+ end
+
+ it "with '0666' makes file readable and writable but not executable" do
+ File.chmod(0666, @file)
+ File.readable?(@file).should == true
+ File.writable?(@file).should == true
+ File.executable?(@file).should == false
+ end
+
+ as_user do
+ it "with '0111' makes file executable but not readable or writable" do
+ File.chmod(0111, @file)
+ File.readable?(@file).should == false
+ File.writable?(@file).should == false
+ File.executable?(@file).should == true
+ end
+ end
+
+ it "modifies the permission bits of the files specified" do
+ File.stat(@file).mode.should == 33261
+ end
+ end
+end
diff --git a/spec/ruby/core/file/chown_spec.rb b/spec/ruby/core/file/chown_spec.rb
new file mode 100644
index 0000000000..3353aafc70
--- /dev/null
+++ b/spec/ruby/core/file/chown_spec.rb
@@ -0,0 +1,144 @@
+require_relative '../../spec_helper'
+
+describe "File.chown" do
+ before :each do
+ @fname = tmp('file_chown_test')
+ touch @fname
+ end
+
+ after :each do
+ rm_r @fname
+ end
+
+ as_superuser do
+ platform_is :windows do
+ it "does not modify the owner id of the file" do
+ File.chown 0, nil, @fname
+ File.stat(@fname).uid.should == 0
+ File.chown 501, nil, @fname
+ File.stat(@fname).uid.should == 0
+ end
+
+ it "does not modify the group id of the file" do
+ File.chown nil, 0, @fname
+ File.stat(@fname).gid.should == 0
+ File.chown nil, 501, @fname
+ File.stat(@fname).gid.should == 0
+ end
+ end
+
+ platform_is_not :windows do
+ it "changes the owner id of the file" do
+ File.chown 501, nil, @fname
+ File.stat(@fname).uid.should == 501
+ File.chown 0, nil, @fname
+ File.stat(@fname).uid.should == 0
+ end
+
+ it "changes the group id of the file" do
+ File.chown nil, 501, @fname
+ File.stat(@fname).gid.should == 501
+ File.chown nil, 0, @fname
+ File.stat(@fname).uid.should == 0
+ end
+
+ it "does not modify the owner id of the file if passed nil or -1" do
+ File.chown 501, nil, @fname
+ File.chown nil, nil, @fname
+ File.stat(@fname).uid.should == 501
+ File.chown nil, -1, @fname
+ File.stat(@fname).uid.should == 501
+ end
+
+ it "does not modify the group id of the file if passed nil or -1" do
+ File.chown nil, 501, @fname
+ File.chown nil, nil, @fname
+ File.stat(@fname).gid.should == 501
+ File.chown nil, -1, @fname
+ File.stat(@fname).gid.should == 501
+ end
+ end
+ end
+
+ it "returns the number of files processed" do
+ File.chown(nil, nil, @fname, @fname).should == 2
+ end
+
+ platform_is_not :windows do
+ it "raises an error for a non existent path" do
+ -> {
+ File.chown(nil, nil, "#{@fname}_not_existing")
+ }.should.raise(Errno::ENOENT)
+ end
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.chown(nil, nil, mock_to_path(@fname)).should == 1
+ end
+end
+
+describe "File#chown" do
+ before :each do
+ @fname = tmp('file_chown_test')
+ @file = File.open(@fname, 'w')
+ end
+
+ after :each do
+ @file.close unless @file.closed?
+ rm_r @fname
+ end
+
+ as_superuser do
+ platform_is :windows do
+ it "does not modify the owner id of the file" do
+ @file.chown 0, nil
+ @file.stat.uid.should == 0
+ @file.chown 501, nil
+ @file.stat.uid.should == 0
+ end
+
+ it "does not modify the group id of the file" do
+ @file.chown nil, 0
+ @file.stat.gid.should == 0
+ @file.chown nil, 501
+ @file.stat.gid.should == 0
+ end
+ end
+
+ platform_is_not :windows do
+ it "changes the owner id of the file" do
+ @file.chown 501, nil
+ @file.stat.uid.should == 501
+ @file.chown 0, nil
+ @file.stat.uid.should == 0
+ end
+
+ it "changes the group id of the file" do
+ @file.chown nil, 501
+ @file.stat.gid.should == 501
+ @file.chown nil, 0
+ @file.stat.uid.should == 0
+ end
+
+ it "does not modify the owner id of the file if passed nil or -1" do
+ @file.chown 501, nil
+ @file.chown nil, nil
+ @file.stat.uid.should == 501
+ @file.chown nil, -1
+ @file.stat.uid.should == 501
+ end
+
+ it "does not modify the group id of the file if passed nil or -1" do
+ @file.chown nil, 501
+ @file.chown nil, nil
+ @file.stat.gid.should == 501
+ @file.chown nil, -1
+ @file.stat.gid.should == 501
+ end
+ end
+ end
+
+ it "returns 0" do
+ @file.chown(nil, nil).should == 0
+ end
+end
diff --git a/spec/ruby/core/file/constants/constants_spec.rb b/spec/ruby/core/file/constants/constants_spec.rb
new file mode 100644
index 0000000000..9d9c1c3b25
--- /dev/null
+++ b/spec/ruby/core/file/constants/constants_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../../spec_helper'
+
+["APPEND", "CREAT", "EXCL", "FNM_CASEFOLD",
+ "FNM_DOTMATCH", "FNM_EXTGLOB", "FNM_NOESCAPE", "FNM_PATHNAME",
+ "FNM_SYSCASE", "LOCK_EX", "LOCK_NB", "LOCK_SH",
+ "LOCK_UN", "NONBLOCK", "RDONLY",
+ "RDWR", "TRUNC", "WRONLY", "SHARE_DELETE"].each do |const|
+ describe "File::Constants::#{const}" do
+ it "is defined" do
+ File::Constants.const_defined?(const).should == true
+ end
+ end
+end
+
+platform_is :windows do
+ describe "File::Constants::BINARY" do
+ it "is defined" do
+ File::Constants.const_defined?(:BINARY).should == true
+ end
+ end
+end
+
+platform_is_not :windows do
+ ["NOCTTY", "SYNC"].each do |const|
+ describe "File::Constants::#{const}" do
+ it "is defined" do
+ File::Constants.const_defined?(const).should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/constants_spec.rb b/spec/ruby/core/file/constants_spec.rb
new file mode 100644
index 0000000000..5f058a7f40
--- /dev/null
+++ b/spec/ruby/core/file/constants_spec.rb
@@ -0,0 +1,141 @@
+require_relative '../../spec_helper'
+
+# TODO: migrate these to constants/constants_spec.rb
+
+describe "File::Constants" do
+ it "matches mode constants" do
+ File::FNM_NOESCAPE.should_not == nil
+ File::FNM_PATHNAME.should_not == nil
+ File::FNM_DOTMATCH.should_not == nil
+ File::FNM_CASEFOLD.should_not == nil
+ File::FNM_SYSCASE.should_not == nil
+
+ platform_is :windows do #|| VMS
+ File::FNM_SYSCASE.should == 8
+ end
+ end
+
+ # Only these constants are not inherited from the IO class
+ it "the separator constant" do
+ File::SEPARATOR.should_not == nil
+ File::Separator.should_not == nil
+ File::PATH_SEPARATOR.should_not == nil
+ File::SEPARATOR.should == "/"
+
+ platform_is :windows do #|| VMS
+ File::ALT_SEPARATOR.should_not == nil
+ File::PATH_SEPARATOR.should == ";"
+ end
+
+ platform_is_not :windows do
+ File::ALT_SEPARATOR.should == nil
+ File::PATH_SEPARATOR.should == ":"
+ end
+ end
+
+ it "the open mode constants" do
+ File::APPEND.should_not == nil
+ File::CREAT.should_not == nil
+ File::EXCL.should_not == nil
+ File::NONBLOCK.should_not == nil
+ File::RDONLY.should_not == nil
+ File::RDWR.should_not == nil
+ File::TRUNC.should_not == nil
+ File::WRONLY.should_not == nil
+
+ platform_is_not :windows do # Not sure about VMS here
+ File::NOCTTY.should_not == nil
+ end
+ end
+
+ it "lock mode constants" do
+ File::LOCK_EX.should_not == nil
+ File::LOCK_NB.should_not == nil
+ File::LOCK_SH.should_not == nil
+ File::LOCK_UN.should_not == nil
+ end
+end
+
+describe "File::Constants" do
+ # These mode and permission bits are platform dependent
+ it "File::RDONLY" do
+ defined?(File::RDONLY).should == "constant"
+ end
+
+ it "File::WRONLY" do
+ defined?(File::WRONLY).should == "constant"
+ end
+
+ it "File::CREAT" do
+ defined?(File::CREAT).should == "constant"
+ end
+
+ it "File::RDWR" do
+ defined?(File::RDWR).should == "constant"
+ end
+
+ it "File::APPEND" do
+ defined?(File::APPEND).should == "constant"
+ end
+
+ it "File::TRUNC" do
+ defined?(File::TRUNC).should == "constant"
+ end
+
+ platform_is_not :windows do # Not sure about VMS here
+ it "File::NOCTTY" do
+ defined?(File::NOCTTY).should == "constant"
+ end
+ end
+
+ it "File::NONBLOCK" do
+ defined?(File::NONBLOCK).should == "constant"
+ end
+
+ it "File::LOCK_EX" do
+ defined?(File::LOCK_EX).should == "constant"
+ end
+
+ it "File::LOCK_NB" do
+ defined?(File::LOCK_NB).should == "constant"
+ end
+
+ it "File::LOCK_SH" do
+ defined?(File::LOCK_SH).should == "constant"
+ end
+
+ it "File::LOCK_UN" do
+ defined?(File::LOCK_UN).should == "constant"
+ end
+
+ it "File::SEPARATOR" do
+ defined?(File::SEPARATOR).should == "constant"
+ end
+ it "File::Separator" do
+ defined?(File::Separator).should == "constant"
+ end
+
+ it "File::PATH_SEPARATOR" do
+ defined?(File::PATH_SEPARATOR).should == "constant"
+ end
+
+ it "File::SEPARATOR" do
+ defined?(File::SEPARATOR).should == "constant"
+ File::SEPARATOR.should == "/"
+ end
+
+ platform_is :windows do #|| VMS
+ it "File::ALT_SEPARATOR" do
+ defined?(File::ALT_SEPARATOR).should == "constant"
+ File::PATH_SEPARATOR.should == ";"
+ end
+ end
+
+ platform_is_not :windows do
+ it "File::PATH_SEPARATOR" do
+ defined?(File::PATH_SEPARATOR).should == "constant"
+ File::PATH_SEPARATOR.should == ":"
+ end
+ end
+
+end
diff --git a/spec/ruby/core/file/ctime_spec.rb b/spec/ruby/core/file/ctime_spec.rb
new file mode 100644
index 0000000000..cf37d1f4ee
--- /dev/null
+++ b/spec/ruby/core/file/ctime_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+
+describe "File.ctime" do
+ before :each do
+ @file = __FILE__
+ end
+
+ after :each do
+ @file = nil
+ end
+
+ it "returns the change time for the named file (the time at which directory information about the file was changed, not the file itself)." do
+ File.ctime(@file)
+ File.ctime(@file).should.is_a?(Time)
+ end
+
+ platform_is :linux, :windows do
+ it "returns the change time for the named file (the time at which directory information about the file was changed, not the file itself) with microseconds." do
+ supports_subseconds = Integer(`stat -c%z '#{__FILE__}'`[/\.(\d{1,6})/, 1], 10)
+ if supports_subseconds != 0
+ File.ctime(__FILE__).usec.should > 0
+ else
+ File.ctime(__FILE__).usec.should == 0
+ end
+ rescue Errno::ENOENT => e
+ # Windows don't have stat command.
+ skip e.message
+ end
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.ctime(mock_to_path(@file))
+ end
+
+ it "raises an Errno::ENOENT exception if the file is not found" do
+ -> { File.ctime('bogus') }.should.raise(Errno::ENOENT)
+ end
+end
+
+describe "File#ctime" do
+ before :each do
+ @file = File.open(__FILE__)
+ end
+
+ after :each do
+ @file.close
+ @file = nil
+ end
+
+ it "returns the change time for the named file (the time at which directory information about the file was changed, not the file itself)." do
+ @file.ctime
+ @file.ctime.should.is_a?(Time)
+ end
+end
diff --git a/spec/ruby/core/file/delete_spec.rb b/spec/ruby/core/file/delete_spec.rb
new file mode 100644
index 0000000000..4098499942
--- /dev/null
+++ b/spec/ruby/core/file/delete_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/unlink'
+
+describe "File.delete" do
+ it_behaves_like :file_unlink, :delete
+end
diff --git a/spec/ruby/core/file/directory_spec.rb b/spec/ruby/core/file/directory_spec.rb
new file mode 100644
index 0000000000..8014a7a03d
--- /dev/null
+++ b/spec/ruby/core/file/directory_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/directory'
+
+describe "File.directory?" do
+ it_behaves_like :file_directory, :directory?, File
+end
+
+describe "File.directory?" do
+ it_behaves_like :file_directory_io, :directory?, File
+end
diff --git a/spec/ruby/core/file/dirname_spec.rb b/spec/ruby/core/file/dirname_spec.rb
new file mode 100644
index 0000000000..855148a684
--- /dev/null
+++ b/spec/ruby/core/file/dirname_spec.rb
@@ -0,0 +1,170 @@
+require_relative '../../spec_helper'
+
+describe "File.dirname" do
+ it "returns all the components of filename except the last one" do
+ File.dirname('/home/jason').should == '/home'
+ File.dirname('/home/jason/poot.txt').should == '/home/jason'
+ File.dirname('poot.txt').should == '.'
+ File.dirname('/holy///schnikies//w00t.bin').should == '/holy///schnikies'
+ File.dirname('').should == '.'
+ File.dirname('/').should == '/'
+ File.dirname('/foo/foo').should == '/foo'
+ end
+
+ context "when level is passed" do
+ it "returns all the components of filename except the last parts by the level" do
+ File.dirname('/home/jason', 2).should == '/'
+ File.dirname('/home/jason/poot.txt', 2).should == '/home'
+ end
+
+ it "returns the same String if the level is 0" do
+ File.dirname('poot.txt', 0).should == 'poot.txt'
+ File.dirname('/', 0).should == '/'
+ end
+
+ it "raises ArgumentError if the level is negative" do
+ -> {
+ File.dirname('/home/jason', -1)
+ }.should.raise(ArgumentError, "negative level: -1")
+ end
+
+ it "returns '/' when level exceeds the number of segments in the path" do
+ File.dirname("/home/jason", 100).should == '/'
+ end
+
+ it "calls #to_int if passed not numeric value" do
+ object = Object.new
+ def object.to_int; 2; end
+
+ File.dirname("/a/b/c/d", object).should == '/a/b'
+ end
+ end
+
+ it "returns a String" do
+ File.dirname("foo").should.is_a?(String)
+ end
+
+ it "does not modify its argument" do
+ x = "/usr/bin"
+ File.dirname(x)
+ x.should == "/usr/bin"
+ end
+
+ it "ignores a trailing /" do
+ File.dirname("/foo/bar/").should == "/foo"
+ end
+
+ it "returns the return all the components of filename except the last one (unix format)" do
+ File.dirname("foo").should =="."
+ File.dirname("/foo").should =="/"
+ File.dirname("/foo/bar").should =="/foo"
+ File.dirname("/foo/bar.txt").should =="/foo"
+ File.dirname("/foo/bar/baz").should =="/foo/bar"
+ end
+
+ it "returns all the components of filename except the last one (edge cases on all platforms)" do
+ File.dirname("").should == "."
+ File.dirname(".").should == "."
+ File.dirname("./").should == "."
+ File.dirname("./b/./").should == "./b"
+ File.dirname("..").should == "."
+ File.dirname("../").should == "."
+ File.dirname("/").should == "/"
+ File.dirname("/.").should == "/"
+ File.dirname("/foo/").should == "/"
+ File.dirname("/foo/.").should == "/foo"
+ File.dirname("/foo/./").should == "/foo"
+ File.dirname("/foo/../.").should == "/foo/.."
+ File.dirname("foo/../").should == "foo"
+ end
+
+ it "rejects strings encoded with non ASCII-compatible encodings" do
+ Encoding.list.reject(&:ascii_compatible?).reject(&:dummy?).each do |enc|
+ path = "/foo/bar".encode(enc)
+ -> {
+ File.dirname(path)
+ }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ it "works with all ASCII-compatible encodings" do
+ Encoding.list.select(&:ascii_compatible?).each do |enc|
+ File.dirname("/foo/bar".encode(enc)).should == "/foo".encode(enc)
+ end
+ end
+
+ it "handles Shift JIS 0x5C (\\) as second byte of a multi-byte sequence" do
+ # dir/fileソname.txt
+ path = "dir/file\x83\x5cname.txt".b.force_encoding(Encoding::SHIFT_JIS)
+ path.valid_encoding?.should == true
+ File.dirname(path).should == "dir"
+ end
+
+ platform_is_not :windows do
+ it "ignores repeated leading / (edge cases on non-windows)" do
+ File.dirname("/////foo/bar/").should == "/foo"
+ end
+
+ it "returns all the components of filename except the last one (edge cases on non-windows)" do
+ File.dirname('/////').should == '/'
+ File.dirname("//foo//").should == "/"
+ File.dirname('foo\bar').should == '.'
+ File.dirname('/foo\bar').should == '/'
+ File.dirname('foo/bar\baz').should == 'foo'
+ end
+ end
+
+ platform_is :windows do
+ it "returns all the components of filename except the last one (edge cases on windows)" do
+ File.dirname("//foo").should == "//foo"
+ File.dirname("//foo//").should == "//foo"
+ File.dirname('/////').should == '//'
+ end
+
+ it "handles Shift JIS 0x5C (\\) as second byte of a multi-byte sequence (windows)" do
+ # dir\fileソname.txt
+ path = "dir\\file\x83\x5cname.txt".b.force_encoding(Encoding::SHIFT_JIS)
+ path.valid_encoding?.should == true
+ File.dirname(path).should == "dir"
+ end
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.dirname(mock_to_path("/")).should == "/"
+ end
+
+ it "raises a TypeError if not passed a String type" do
+ -> { File.dirname(nil) }.should.raise(TypeError)
+ -> { File.dirname(0) }.should.raise(TypeError)
+ -> { File.dirname(true) }.should.raise(TypeError)
+ -> { File.dirname(false) }.should.raise(TypeError)
+ end
+
+ # Windows specific tests
+ platform_is :windows do
+ it "returns the return all the components of filename except the last one (Windows format)" do
+ File.dirname("C:\\foo\\bar\\baz.txt").should =="C:\\foo\\bar"
+ File.dirname("C:\\foo\\bar").should =="C:\\foo"
+ File.dirname("C:\\foo\\bar\\").should == "C:\\foo"
+ File.dirname("C:\\foo").should == "C:\\"
+ File.dirname("C:\\").should =="C:\\"
+ end
+
+ it "returns the return all the components of filename except the last one (windows unc)" do
+ File.dirname("\\\\foo\\bar\\baz.txt").should == "\\\\foo\\bar"
+ File.dirname("\\\\foo\\bar\\baz").should == "\\\\foo\\bar"
+ File.dirname("\\\\foo").should =="\\\\foo"
+ File.dirname("\\\\foo\\bar").should =="\\\\foo\\bar"
+ File.dirname("\\\\\\foo\\bar").should =="\\\\foo\\bar"
+ File.dirname("\\\\\\foo").should =="\\\\foo"
+ end
+
+ it "returns the return all the components of filename except the last one (forward_slash)" do
+ File.dirname("C:/").should == "C:/"
+ File.dirname("C:/foo").should == "C:/"
+ File.dirname("C:/foo/bar").should == "C:/foo"
+ File.dirname("C:/foo/bar/").should == "C:/foo"
+ File.dirname("C:/foo/bar//").should == "C:/foo"
+ end
+ end
+end
diff --git a/spec/ruby/core/file/empty_spec.rb b/spec/ruby/core/file/empty_spec.rb
new file mode 100644
index 0000000000..e8c9941676
--- /dev/null
+++ b/spec/ruby/core/file/empty_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/zero'
+
+describe "File.empty?" do
+ it_behaves_like :file_zero, :empty?, File
+ it_behaves_like :file_zero_missing, :empty?, File
+end
diff --git a/spec/ruby/core/file/executable_real_spec.rb b/spec/ruby/core/file/executable_real_spec.rb
new file mode 100644
index 0000000000..0cb848b201
--- /dev/null
+++ b/spec/ruby/core/file/executable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/executable_real'
+
+describe "File.executable_real?" do
+ it_behaves_like :file_executable_real, :executable_real?, File
+ it_behaves_like :file_executable_real_missing, :executable_real?, File
+end
diff --git a/spec/ruby/core/file/executable_spec.rb b/spec/ruby/core/file/executable_spec.rb
new file mode 100644
index 0000000000..1dbb3b233d
--- /dev/null
+++ b/spec/ruby/core/file/executable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/executable'
+
+describe "File.executable?" do
+ it_behaves_like :file_executable, :executable?, File
+ it_behaves_like :file_executable_missing, :executable?, File
+end
diff --git a/spec/ruby/core/file/exist_spec.rb b/spec/ruby/core/file/exist_spec.rb
new file mode 100644
index 0000000000..b5600e5b07
--- /dev/null
+++ b/spec/ruby/core/file/exist_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/exist'
+
+describe "File.exist?" do
+ it_behaves_like :file_exist, :exist?, File
+end
+
+describe "File.exists?" do
+ it "has been removed" do
+ File.should_not.respond_to?(:exists?)
+ end
+end
diff --git a/spec/ruby/core/file/expand_path_spec.rb b/spec/ruby/core/file/expand_path_spec.rb
new file mode 100644
index 0000000000..160494f145
--- /dev/null
+++ b/spec/ruby/core/file/expand_path_spec.rb
@@ -0,0 +1,265 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require 'etc'
+
+describe "File.expand_path" do
+ before :each do
+ platform_is :windows do
+ @base = `cd`.chomp.tr '\\', '/'
+ @tmpdir = "c:/tmp"
+ @rootdir = "c:/"
+ end
+
+ platform_is_not :windows do
+ @base = Dir.pwd
+ @tmpdir = "/tmp"
+ @rootdir = "/"
+ end
+ end
+
+ before :each do
+ @external = Encoding.default_external
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ end
+
+ it "converts a pathname to an absolute pathname" do
+ File.expand_path('').should == @base
+ File.expand_path('a').should == File.join(@base, 'a')
+ File.expand_path('a', nil).should == File.join(@base, 'a')
+ end
+
+ it "converts a pathname to an absolute pathname, Ruby-Talk:18512" do
+ # See Ruby-Talk:18512
+ File.expand_path('.a').should == File.join(@base, '.a')
+ File.expand_path('..a').should == File.join(@base, '..a')
+ File.expand_path('a../b').should == File.join(@base, 'a../b')
+ end
+
+ platform_is_not :windows do
+ it "keeps trailing dots on absolute pathname" do
+ # See Ruby-Talk:18512
+ File.expand_path('a.').should == File.join(@base, 'a.')
+ File.expand_path('a..').should == File.join(@base, 'a..')
+ end
+ end
+
+ it "converts a pathname to an absolute pathname, using a complete path" do
+ File.expand_path("", "#{@tmpdir}").should == "#{@tmpdir}"
+ File.expand_path("a", "#{@tmpdir}").should =="#{@tmpdir}/a"
+ File.expand_path("../a", "#{@tmpdir}/xxx").should == "#{@tmpdir}/a"
+ File.expand_path(".", "#{@rootdir}").should == "#{@rootdir}"
+ end
+
+ platform_is_not :windows do
+ before do
+ @var_home = ENV['HOME'].chomp('/')
+ @db_home = Dir.home(ENV['USER'])
+ end
+
+ # FIXME: these are insane!
+ it "expand path with" do
+ File.expand_path("../../bin", "/tmp/x").should == "/bin"
+ File.expand_path("../../bin", "/tmp").should == "/bin"
+ File.expand_path("../../bin", "/").should == "/bin"
+ File.expand_path("../bin", "tmp/x").should == File.join(@base, 'tmp', 'bin')
+ File.expand_path("../bin", "x/../tmp").should == File.join(@base, 'bin')
+ end
+
+ it "expand_path for common unix path gives a full path" do
+ File.expand_path('/tmp/').should =='/tmp'
+ File.expand_path('/tmp/../../../tmp').should == '/tmp'
+ File.expand_path('').should == Dir.pwd
+ File.expand_path('./////').should == Dir.pwd
+ File.expand_path('.').should == Dir.pwd
+ File.expand_path(Dir.pwd).should == Dir.pwd
+ File.expand_path('~/').should == @var_home
+ File.expand_path('~/..badfilename').should == "#{@var_home}/..badfilename"
+ File.expand_path('~/a','~/b').should == "#{@var_home}/a"
+ File.expand_path('..').should == File.dirname(Dir.pwd)
+ end
+
+ it "does not replace multiple '/' at the beginning of the path" do
+ File.expand_path('////some/path').should == "////some/path"
+ end
+
+ it "replaces multiple '/' with a single '/'" do
+ File.expand_path('/some////path').should == "/some/path"
+ end
+
+ it "raises an ArgumentError if the path is not valid" do
+ -> { File.expand_path("~a_not_existing_user") }.should.raise(ArgumentError)
+ end
+
+ it "expands ~ENV['USER'] to the user's home directory" do
+ File.expand_path("~#{ENV['USER']}").should == @db_home
+ end
+
+ it "expands ~ENV['USER']/a to a in the user's home directory" do
+ File.expand_path("~#{ENV['USER']}/a").should == "#{@db_home}/a"
+ end
+
+ it "does not expand ~ENV['USER'] when it's not at the start" do
+ File.expand_path("/~#{ENV['USER']}/a").should == "/~#{ENV['USER']}/a"
+ end
+
+ it "expands ../foo with ~/dir as base dir to /path/to/user/home/foo" do
+ File.expand_path('../foo', '~/dir').should == "#{@var_home}/foo"
+ end
+ end
+
+ it "accepts objects that have a #to_path method" do
+ File.expand_path(mock_to_path("a"), mock_to_path("#{@tmpdir}"))
+ end
+
+ it "raises a TypeError if not passed a String type" do
+ -> { File.expand_path(1) }.should.raise(TypeError)
+ -> { File.expand_path(nil) }.should.raise(TypeError)
+ -> { File.expand_path(true) }.should.raise(TypeError)
+ end
+
+ platform_is_not :windows do
+ it "expands /./dir to /dir" do
+ File.expand_path("/./dir").should == "/dir"
+ end
+ end
+
+ platform_is :windows do
+ it "expands C:/./dir to C:/dir" do
+ File.expand_path("C:/./dir").should == "C:/dir"
+ end
+ end
+
+ it "returns a String in the same encoding as the argument" do
+ Encoding.default_external = Encoding::SHIFT_JIS
+
+ path = "./a".dup.force_encoding Encoding::CP1251
+ File.expand_path(path).encoding.should.equal?(Encoding::CP1251)
+
+ weird_path = [222, 173, 190, 175].pack('C*')
+ File.expand_path(weird_path).encoding.should.equal?(Encoding::BINARY)
+ end
+
+ platform_is_not :windows do
+ it "expands a path when the default external encoding is BINARY" do
+ Encoding.default_external = Encoding::BINARY
+ path_8bit = [222, 173, 190, 175].pack('C*')
+ File.expand_path( path_8bit, @rootdir).should == "#{@rootdir}" + path_8bit
+ end
+ end
+
+ it "expands a path with multi-byte characters" do
+ File.expand_path("Ångström").should == "#{@base}/Ångström"
+ end
+
+ platform_is_not :windows do
+ it "raises an Encoding::CompatibilityError if the external encoding is not compatible" do
+ Encoding.default_external = Encoding::UTF_16BE
+ -> { File.expand_path("./a") }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ it "does not modify the string argument" do
+ str = "./a/b/../c"
+ File.expand_path(str, @base).should == "#{@base}/a/c"
+ str.should == "./a/b/../c"
+ end
+
+ it "does not modify a HOME string argument" do
+ str = "~/a"
+ File.expand_path(str).should == "#{Dir.home}/a"
+ str.should == "~/a"
+ end
+
+ it "returns a String when passed a String subclass" do
+ str = FileSpecs::SubString.new "./a/b/../c"
+ path = File.expand_path(str, @base)
+ path.should == "#{@base}/a/c"
+ path.should.instance_of?(String)
+ end
+end
+
+platform_is_not :windows do
+ describe "File.expand_path when HOME is set" do
+ before :each do
+ @home = ENV["HOME"]
+ ENV["HOME"] = "/rubyspec_home"
+ end
+
+ after :each do
+ ENV["HOME"] = @home
+ end
+
+ it "converts a pathname to an absolute pathname, using ~ (home) as base" do
+ home = "/rubyspec_home"
+ File.expand_path('~').should == home
+ File.expand_path('~', '/tmp/gumby/ddd').should == home
+ File.expand_path('~/a', '/tmp/gumby/ddd').should == File.join(home, 'a')
+ end
+
+ it "does not return a frozen string" do
+ home = "/rubyspec_home"
+ File.expand_path('~').should_not.frozen?
+ File.expand_path('~', '/tmp/gumby/ddd').should_not.frozen?
+ File.expand_path('~/a', '/tmp/gumby/ddd').should_not.frozen?
+ end
+ end
+
+
+ describe "File.expand_path when HOME is not set" do
+ before :each do
+ @home = ENV["HOME"]
+ end
+
+ after :each do
+ ENV["HOME"] = @home
+ end
+
+ guard -> {
+ # We need to check if getlogin(3) returns non-NULL,
+ # as MRI only checks getlogin(3) for expanding '~' if $HOME is not set.
+ user = ENV.delete("USER")
+ begin
+ Etc.getlogin != nil
+ rescue
+ false
+ ensure
+ ENV["USER"] = user
+ end
+ } do
+ it "uses the user database when passed '~' if HOME is nil" do
+ ENV.delete "HOME"
+ File.directory?(File.expand_path("~")).should == true
+ end
+
+ it "uses the user database when passed '~/' if HOME is nil" do
+ ENV.delete "HOME"
+ File.directory?(File.expand_path("~/")).should == true
+ end
+ end
+
+ it "raises an ArgumentError when passed '~' if HOME == ''" do
+ ENV["HOME"] = ""
+ -> { File.expand_path("~") }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "File.expand_path with a non-absolute HOME" do
+ before :each do
+ @home = ENV["HOME"]
+ end
+
+ after :each do
+ ENV["HOME"] = @home
+ end
+
+ it "raises an ArgumentError" do
+ ENV["HOME"] = "non-absolute"
+ -> { File.expand_path("~") }.should.raise(ArgumentError, 'non-absolute home')
+ end
+ end
+end
diff --git a/spec/ruby/core/file/extname_spec.rb b/spec/ruby/core/file/extname_spec.rb
new file mode 100644
index 0000000000..995d0ea31a
--- /dev/null
+++ b/spec/ruby/core/file/extname_spec.rb
@@ -0,0 +1,76 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "File.extname" do
+ it "returns the extension (the portion of file name in path after the period)" do
+ File.extname("foo.rb").should == ".rb"
+ File.extname("/foo/bar.rb").should == ".rb"
+ File.extname("/foo.rb/bar.c").should == ".c"
+ File.extname("bar").should == ""
+ File.extname(".bashrc").should == ""
+ File.extname("/foo.bar/baz").should == ""
+ File.extname(".app.conf").should == ".conf"
+ end
+
+ it "returns unfrozen strings" do
+ File.extname("foo.rb").frozen?.should == false
+ File.extname("/foo/bar.rb").frozen?.should == false
+ File.extname("/foo.rb/bar.c").frozen?.should == false
+ File.extname("bar").frozen?.should == false
+ File.extname(".bashrc").frozen?.should == false
+ File.extname("/foo.bar/baz").frozen?.should == false
+ File.extname(".app.conf").frozen?.should == false
+ end
+
+ it "returns the extension for edge cases" do
+ File.extname("").should == ""
+ File.extname(".").should == ""
+ File.extname("/").should == ""
+ File.extname("/.").should == ""
+ File.extname("..").should == ""
+ File.extname("...").should == ""
+ File.extname("....").should == ""
+ end
+
+ describe "for a filename ending with a dot" do
+ platform_is :windows do
+ it "returns ''" do
+ File.extname(".foo.").should == ""
+ File.extname("foo.").should == ""
+ end
+ end
+
+ platform_is_not :windows do
+ it "returns '.'" do
+ File.extname(".foo.").should == "."
+ File.extname("foo.").should == "."
+ end
+ end
+ end
+
+ it "returns only the last extension of a file with several dots" do
+ File.extname("a.b.c.d.e").should == ".e"
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.extname(mock_to_path("a.b.c.d.e")).should == ".e"
+ end
+
+ it "raises a TypeError if not passed a String type" do
+ -> { File.extname(nil) }.should.raise(TypeError)
+ -> { File.extname(0) }.should.raise(TypeError)
+ -> { File.extname(true) }.should.raise(TypeError)
+ -> { File.extname(false) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if not passed one argument" do
+ -> { File.extname }.should.raise(ArgumentError)
+ -> { File.extname("foo.bar", "foo.baz") }.should.raise(ArgumentError)
+ end
+
+
+ it "returns the extension for a multibyte filename" do
+ File.extname('ИмÑ.m4a').should == ".m4a"
+ end
+
+end
diff --git a/spec/ruby/core/file/file_spec.rb b/spec/ruby/core/file/file_spec.rb
new file mode 100644
index 0000000000..8a9dfd5fe2
--- /dev/null
+++ b/spec/ruby/core/file/file_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/file'
+
+describe "File" do
+ it "includes Enumerable" do
+ File.include?(Enumerable).should == true
+ end
+
+ it "includes File::Constants" do
+ File.include?(File::Constants).should == true
+ end
+end
+
+describe "File.file?" do
+ it_behaves_like :file_file, :file?, File
+end
diff --git a/spec/ruby/core/file/fixtures/common.rb b/spec/ruby/core/file/fixtures/common.rb
new file mode 100644
index 0000000000..50721388ad
--- /dev/null
+++ b/spec/ruby/core/file/fixtures/common.rb
@@ -0,0 +1,22 @@
+module FileSpecs
+ class SubString < String; end
+
+ def self.make_closer(obj, exc=nil)
+ ScratchPad << :file_opened
+
+ class << obj
+ attr_accessor :close_exception
+
+ alias_method :original_close, :close
+
+ def close
+ original_close
+ ScratchPad << :file_closed
+
+ raise @close_exception if @close_exception
+ end
+ end
+
+ obj.close_exception = exc
+ end
+end
diff --git a/spec/ruby/core/file/fixtures/do_not_remove b/spec/ruby/core/file/fixtures/do_not_remove
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/ruby/core/file/fixtures/do_not_remove
@@ -0,0 +1 @@
+
diff --git a/spec/ruby/core/file/fixtures/file_types.rb b/spec/ruby/core/file/fixtures/file_types.rb
new file mode 100644
index 0000000000..109bcfe42e
--- /dev/null
+++ b/spec/ruby/core/file/fixtures/file_types.rb
@@ -0,0 +1,66 @@
+module FileSpecs
+ def self.configure_types
+ return if @configured
+
+ @file = tmp("test.txt")
+ @dir = Dir.pwd
+ @fifo = tmp("test_fifo")
+ @link = tmp("test_link")
+
+ platform_is_not :windows do
+ @block = `find /dev /devices -type b 2>/dev/null`.split("\n").first
+ @char = `{ tty || find /dev /devices -type c; } 2>/dev/null`.split("\n").last
+ end
+
+ @configured = true
+ end
+
+ def self.normal_file
+ touch(@file)
+ yield @file
+ ensure
+ rm_r @file
+ end
+
+ def self.directory
+ yield @dir
+ end
+
+ def self.fifo
+ File.mkfifo(@fifo)
+ yield @fifo
+ ensure
+ rm_r @fifo
+ end
+
+ def self.block_device
+ raise "Could not find a block device" unless @block
+ yield @block
+ end
+
+ def self.character_device
+ raise "Could not find a character device" unless @char
+ yield @char
+ end
+
+ def self.symlink
+ touch(@file)
+ File.symlink(@file, @link)
+ yield @link
+ ensure
+ rm_r @file, @link
+ end
+
+ def self.socket
+ require_relative '../../../library/socket/fixtures/classes.rb'
+
+ name = SocketSpecs.socket_path
+ socket = UNIXServer.new name
+ begin
+ yield name
+ ensure
+ socket.close
+ rm_r name
+ end
+ end
+end
diff --git a/spec/ruby/core/file/flock_spec.rb b/spec/ruby/core/file/flock_spec.rb
new file mode 100644
index 0000000000..23ddf89ed8
--- /dev/null
+++ b/spec/ruby/core/file/flock_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+
+describe "File#flock" do
+ before :each do
+ ScratchPad.record []
+
+ @name = tmp("flock_test")
+ touch(@name)
+
+ @file = File.open @name, "w+"
+ end
+
+ after :each do
+ @file.flock File::LOCK_UN
+ @file.close
+
+ rm_r @name
+ end
+
+ it "exclusively locks a file" do
+ @file.flock(File::LOCK_EX).should == 0
+ @file.flock(File::LOCK_UN).should == 0
+ end
+
+ it "non-exclusively locks a file" do
+ @file.flock(File::LOCK_SH).should == 0
+ @file.flock(File::LOCK_UN).should == 0
+ end
+
+ it "returns false if trying to lock an exclusively locked file" do
+ @file.flock File::LOCK_EX
+
+ ruby_exe(<<-END_OF_CODE).should == "false"
+ File.open('#{@name}', "w") do |f2|
+ print f2.flock(File::LOCK_EX | File::LOCK_NB).to_s
+ end
+ END_OF_CODE
+ end
+
+ it "blocks if trying to lock an exclusively locked file" do
+ @file.flock File::LOCK_EX
+
+ out = ruby_exe(<<-END_OF_CODE)
+ running = false
+
+ t = Thread.new do
+ File.open('#{@name}', "w") do |f2|
+ puts "before"
+ running = true
+ f2.flock(File::LOCK_EX)
+ puts "after"
+ end
+ end
+
+ Thread.pass until running
+ Thread.pass while t.status and t.status != "sleep"
+ sleep 0.1
+
+ t.kill
+ t.join
+ END_OF_CODE
+
+ out.should == "before\n"
+ end
+
+ it "returns 0 if trying to lock a non-exclusively locked file" do
+ @file.flock File::LOCK_SH
+
+ File.open(@name, "r") do |f2|
+ f2.flock(File::LOCK_SH | File::LOCK_NB).should == 0
+ f2.flock(File::LOCK_UN).should == 0
+ end
+ end
+end
diff --git a/spec/ruby/core/file/fnmatch_spec.rb b/spec/ruby/core/file/fnmatch_spec.rb
new file mode 100644
index 0000000000..a1b7fa12b3
--- /dev/null
+++ b/spec/ruby/core/file/fnmatch_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/fnmatch'
+
+describe "File.fnmatch" do
+ it_behaves_like :file_fnmatch, :fnmatch
+end
+
+describe "File.fnmatch?" do
+ it_behaves_like :file_fnmatch, :fnmatch?
+end
diff --git a/spec/ruby/core/file/ftype_spec.rb b/spec/ruby/core/file/ftype_spec.rb
new file mode 100644
index 0000000000..ab9f76b79b
--- /dev/null
+++ b/spec/ruby/core/file/ftype_spec.rb
@@ -0,0 +1,82 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/file_types'
+
+describe "File.ftype" do
+ before :all do
+ FileSpecs.configure_types
+ end
+
+ it "raises ArgumentError if not given exactly one filename" do
+ -> { File.ftype }.should.raise(ArgumentError)
+ -> { File.ftype('blah', 'bleh') }.should.raise(ArgumentError)
+ end
+
+ it "raises Errno::ENOENT if the file is not valid" do
+ -> {
+ File.ftype("/#{$$}#{Time.now.to_f}")
+ }.should.raise(Errno::ENOENT)
+ end
+
+ it "returns a String" do
+ FileSpecs.normal_file do |file|
+ File.ftype(file).should.is_a?(String)
+ end
+ end
+
+ it "returns 'file' when the file is a file" do
+ FileSpecs.normal_file do |file|
+ File.ftype(file).should == 'file'
+ end
+ end
+
+ it "returns 'directory' when the file is a dir" do
+ FileSpecs.directory do |dir|
+ File.ftype(dir).should == 'directory'
+ end
+ end
+
+ it "uses to_path to convert arguments" do
+ FileSpecs.normal_file do |file|
+ obj = mock('path')
+ obj.should_receive(:to_path).and_return(file)
+ File.ftype(obj).should == 'file'
+ end
+ end
+
+ # Both FreeBSD and Windows does not have block devices
+ platform_is_not :freebsd, :windows do
+ with_block_device do
+ it "returns 'blockSpecial' when the file is a block" do
+ FileSpecs.block_device do |block|
+ File.ftype(block).should == 'blockSpecial'
+ end
+ end
+ end
+ end
+
+ platform_is_not :windows do
+ it "returns 'characterSpecial' when the file is a char" do
+ FileSpecs.character_device do |char|
+ File.ftype(char).should == 'characterSpecial'
+ end
+ end
+
+ it "returns 'link' when the file is a link" do
+ FileSpecs.symlink do |link|
+ File.ftype(link).should == 'link'
+ end
+ end
+
+ it "returns fifo when the file is a fifo" do
+ FileSpecs.fifo do |fifo|
+ File.ftype(fifo).should == 'fifo'
+ end
+ end
+
+ it "returns 'socket' when the file is a socket" do
+ FileSpecs.socket do |socket|
+ File.ftype(socket).should == 'socket'
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/grpowned_spec.rb b/spec/ruby/core/file/grpowned_spec.rb
new file mode 100644
index 0000000000..8ddac5237c
--- /dev/null
+++ b/spec/ruby/core/file/grpowned_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/grpowned'
+
+describe "File.grpowned?" do
+ it_behaves_like :file_grpowned, :grpowned?, File
+
+ it "returns false if file the does not exist" do
+ File.grpowned?("i_am_a_bogus_file").should == false
+ end
+end
diff --git a/spec/ruby/core/file/identical_spec.rb b/spec/ruby/core/file/identical_spec.rb
new file mode 100644
index 0000000000..bbeaef24d2
--- /dev/null
+++ b/spec/ruby/core/file/identical_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/identical'
+
+describe "File.identical?" do
+ it_behaves_like :file_identical, :identical?, File
+end
diff --git a/spec/ruby/core/file/initialize_spec.rb b/spec/ruby/core/file/initialize_spec.rb
new file mode 100644
index 0000000000..9a76a95260
--- /dev/null
+++ b/spec/ruby/core/file/initialize_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "File#initialize" do
+ after :each do
+ @io.close if @io
+ end
+
+ it "accepts encoding options in mode parameter" do
+ @io = File.new(__FILE__, 'r:UTF-8:iso-8859-1')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "accepts encoding options as a hash parameter" do
+ @io = File.new(__FILE__, 'r', encoding: 'UTF-8:iso-8859-1')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == 'ISO-8859-1'
+ end
+end
diff --git a/spec/ruby/core/file/inspect_spec.rb b/spec/ruby/core/file/inspect_spec.rb
new file mode 100644
index 0000000000..fe87429e8d
--- /dev/null
+++ b/spec/ruby/core/file/inspect_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "File#inspect" do
+ before :each do
+ @name = tmp("file_inspect.txt")
+ @file = File.open @name, "w"
+ end
+
+ after :each do
+ @file.close unless @file.closed?
+ rm_r @name
+ end
+
+ it "returns a String" do
+ @file.inspect.should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/file/join_spec.rb b/spec/ruby/core/file/join_spec.rb
new file mode 100644
index 0000000000..0f0911ea31
--- /dev/null
+++ b/spec/ruby/core/file/join_spec.rb
@@ -0,0 +1,148 @@
+require_relative '../../spec_helper'
+
+describe "File.join" do
+ # see [ruby-core:46804] for the 4 following rules
+ it "changes only boundaries separators" do
+ File.join("file/\\/usr/", "/bin").should == "file/\\/usr/bin"
+ File.join("file://usr", "bin").should == "file://usr/bin"
+ end
+
+ it "respects the given separator if only one part has a boundary separator" do
+ File.join("usr/", "bin").should == "usr/bin"
+ File.join("usr", "/bin").should == "usr/bin"
+ File.join("usr//", "bin").should == "usr//bin"
+ File.join("usr", "//bin").should == "usr//bin"
+ end
+
+ it "joins parts using File::SEPARATOR if there are no boundary separators" do
+ File.join("usr", "bin").should == "usr/bin"
+ end
+
+ it "prefers the separator of the right part if both parts have separators" do
+ File.join("usr/", "//bin").should == "usr//bin"
+ File.join("usr//", "/bin").should == "usr/bin"
+ end
+
+ platform_is :windows do
+ it "respects given separator if only one part has a boundary separator" do
+ File.join("C:\\", 'windows').should == "C:\\windows"
+ File.join("C:", "\\windows").should == "C:\\windows"
+ File.join("\\\\", "usr").should == "\\\\usr"
+ end
+
+ it "prefers the separator of the right part if both parts have separators" do
+ File.join("C:/", "\\windows").should == "C:\\windows"
+ File.join("C:\\", "/windows").should == "C:/windows"
+ end
+ end
+
+ platform_is_not :windows do
+ it "does not treat \\ as a separator on non-Windows" do
+ File.join("usr\\", 'bin').should == "usr\\/bin"
+ File.join("usr", "\\bin").should == "usr/\\bin"
+ File.join("usr/", "\\bin").should == "usr/\\bin"
+ File.join("usr\\", "/bin").should == "usr\\/bin"
+ end
+ end
+
+ it "returns an empty string when given no arguments" do
+ File.join.should == ""
+ end
+
+ it "returns a duplicate string when given a single argument" do
+ str = "usr"
+ File.join(str).should == str
+ File.join(str).should_not.equal?(str)
+ end
+
+ it "supports any number of arguments" do
+ File.join("a", "b", "c", "d").should == "a/b/c/d"
+ end
+
+ it "flattens nested arrays" do
+ File.join(["a", "b", "c"]).should == "a/b/c"
+ File.join(["a", ["b", ["c"]]]).should == "a/b/c"
+ end
+
+ it "inserts the separator in between empty strings and arrays" do
+ File.join("").should == ""
+ File.join("", "").should == "/"
+ File.join(["", ""]).should == "/"
+ File.join("a", "").should == "a/"
+ File.join("", "a").should == "/a"
+
+ File.join([]).should == ""
+ File.join([], []).should == "/"
+ File.join([[], []]).should == "/"
+ File.join("a", []).should == "a/"
+ File.join([], "a").should == "/a"
+ end
+
+ it "handles leading parts edge cases" do
+ File.join("/bin") .should == "/bin"
+ File.join("", "bin") .should == "/bin"
+ File.join("/", "bin") .should == "/bin"
+ File.join("/", "/bin").should == "/bin"
+ end
+
+ it "handles trailing parts edge cases" do
+ File.join("bin", "") .should == "bin/"
+ File.join("bin/") .should == "bin/"
+ File.join("bin/", "") .should == "bin/"
+ File.join("bin", "/") .should == "bin/"
+ File.join("bin/", "/").should == "bin/"
+ end
+
+ it "handles middle parts edge cases" do
+ File.join("usr", "", "bin") .should == "usr/bin"
+ File.join("usr/", "", "bin") .should == "usr/bin"
+ File.join("usr", "", "/bin").should == "usr/bin"
+ File.join("usr/", "", "/bin").should == "usr/bin"
+ end
+
+ # TODO: See MRI svn r23306. Add patchlevel when there is a release.
+ it "raises an ArgumentError if passed a recursive array" do
+ a = ["a"]
+ a << a
+ -> { File.join a }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError exception when args are nil" do
+ -> { File.join nil }.should.raise(TypeError)
+ end
+
+ it "calls #to_str" do
+ -> { File.join(mock('x')) }.should.raise(TypeError)
+
+ bin = mock("bin")
+ bin.should_receive(:to_str).exactly(:twice).and_return("bin")
+ File.join(bin).should == "bin"
+ File.join("usr", bin).should == "usr/bin"
+ end
+
+ it "doesn't mutate the object when calling #to_str" do
+ usr = mock("usr")
+ str = "usr"
+ usr.should_receive(:to_str).and_return(str)
+ File.join(usr, "bin").should == "usr/bin"
+ str.should == "usr"
+ end
+
+ it "calls #to_path" do
+ -> { File.join(mock('x')) }.should.raise(TypeError)
+
+ bin = mock("bin")
+ bin.should_receive(:to_path).exactly(:twice).and_return("bin")
+ File.join(bin).should == "bin"
+ File.join("usr", bin).should == "usr/bin"
+ end
+
+ it "raises errors for null bytes" do
+ -> { File.join("\x00x", "metadata.gz") }.should.raise(ArgumentError) { |e|
+ e.message.should == 'string contains null byte'
+ }
+ -> { File.join("metadata.gz", "\x00x") }.should.raise(ArgumentError) { |e|
+ e.message.should == 'string contains null byte'
+ }
+ end
+end
diff --git a/spec/ruby/core/file/lchmod_spec.rb b/spec/ruby/core/file/lchmod_spec.rb
new file mode 100644
index 0000000000..3c44374983
--- /dev/null
+++ b/spec/ruby/core/file/lchmod_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "File.lchmod" do
+ platform_is_not :linux, :windows, :openbsd, :aix do
+ before :each do
+ @fname = tmp('file_chmod_test')
+ @lname = @fname + '.lnk'
+
+ touch(@fname) { |f| f.write "rubinius" }
+
+ rm_r @lname
+ File.symlink @fname, @lname
+ end
+
+ after :each do
+ rm_r @lname, @fname
+ end
+
+ it "changes the file mode of the link and not of the file" do
+ File.chmod(0222, @lname).should == 1
+ File.lchmod(0755, @lname).should == 1
+
+ File.lstat(@lname).should.executable?
+ File.lstat(@lname).should.readable?
+ File.lstat(@lname).should.writable?
+
+ File.stat(@lname).should_not.executable?
+ File.stat(@lname).should_not.readable?
+ File.stat(@lname).should.writable?
+ end
+ end
+end
diff --git a/spec/ruby/core/file/lchown_spec.rb b/spec/ruby/core/file/lchown_spec.rb
new file mode 100644
index 0000000000..8d95d287ba
--- /dev/null
+++ b/spec/ruby/core/file/lchown_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../../spec_helper'
+
+as_superuser do
+ describe "File.lchown" do
+ platform_is_not :windows do
+ before :each do
+ @fname = tmp('file_chown_test')
+ @lname = @fname + '.lnk'
+
+ touch(@fname) { |f| f.chown 501, 501 }
+
+ rm_r @lname
+ File.symlink @fname, @lname
+ end
+
+ after :each do
+ rm_r @lname, @fname
+ end
+
+ it "changes the owner id of the file" do
+ File.lchown 502, nil, @lname
+ File.stat(@fname).uid.should == 501
+ File.lstat(@lname).uid.should == 502
+ File.lchown 0, nil, @lname
+ File.stat(@fname).uid.should == 501
+ File.lstat(@lname).uid.should == 0
+ end
+
+ it "changes the group id of the file" do
+ File.lchown nil, 502, @lname
+ File.stat(@fname).gid.should == 501
+ File.lstat(@lname).gid.should == 502
+ File.lchown nil, 0, @lname
+ File.stat(@fname).uid.should == 501
+ File.lstat(@lname).uid.should == 0
+ end
+
+ it "does not modify the owner id of the file if passed nil or -1" do
+ File.lchown 502, nil, @lname
+ File.lchown nil, nil, @lname
+ File.lstat(@lname).uid.should == 502
+ File.lchown nil, -1, @lname
+ File.lstat(@lname).uid.should == 502
+ end
+
+ it "does not modify the group id of the file if passed nil or -1" do
+ File.lchown nil, 502, @lname
+ File.lchown nil, nil, @lname
+ File.lstat(@lname).gid.should == 502
+ File.lchown nil, -1, @lname
+ File.lstat(@lname).gid.should == 502
+ end
+
+ it "returns the number of files processed" do
+ File.lchown(nil, nil, @lname, @lname).should == 2
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/link_spec.rb b/spec/ruby/core/file/link_spec.rb
new file mode 100644
index 0000000000..768ee4b0fa
--- /dev/null
+++ b/spec/ruby/core/file/link_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "File.link" do
+ before :each do
+ @file = tmp("file_link.txt")
+ @link = tmp("file_link.lnk")
+
+ rm_r @link
+ touch @file
+ end
+
+ after :each do
+ rm_r @link, @file
+ end
+
+ platform_is_not :windows, :android do
+ it "link a file with another" do
+ File.link(@file, @link).should == 0
+ File.should.exist?(@link)
+ File.identical?(@file, @link).should == true
+ end
+
+ it "raises an Errno::EEXIST if the target already exists" do
+ File.link(@file, @link)
+ -> { File.link(@file, @link) }.should.raise(Errno::EEXIST)
+ end
+
+ it "raises an ArgumentError if not passed two arguments" do
+ -> { File.link }.should.raise(ArgumentError)
+ -> { File.link(@file) }.should.raise(ArgumentError)
+ -> { File.link(@file, @link, @file) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if not passed String types" do
+ -> { File.link(@file, nil) }.should.raise(TypeError)
+ -> { File.link(@file, 1) }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/file/lstat_spec.rb b/spec/ruby/core/file/lstat_spec.rb
new file mode 100644
index 0000000000..a5ea9d15a5
--- /dev/null
+++ b/spec/ruby/core/file/lstat_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'shared/stat'
+
+describe "File.lstat" do
+ it_behaves_like :file_stat, :lstat
+end
+
+describe "File.lstat" do
+
+ before :each do
+ @file = tmp('i_exist')
+ @link = tmp('i_am_a_symlink')
+ touch(@file) { |f| f.write 'rubinius' }
+ File.symlink(@file, @link)
+ end
+
+ after :each do
+ rm_r @link, @file
+ end
+
+ platform_is_not :windows do
+ it "returns a File::Stat object with symlink properties for a symlink" do
+ st = File.lstat(@link)
+
+ st.should.symlink?
+ st.should_not.file?
+ end
+ end
+end
+
+describe "File#lstat" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/file/lutime_spec.rb b/spec/ruby/core/file/lutime_spec.rb
new file mode 100644
index 0000000000..0f6df42ea3
--- /dev/null
+++ b/spec/ruby/core/file/lutime_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'shared/update_time'
+
+platform_is_not :windows do
+ describe "File.lutime" do
+ it_behaves_like :update_time, :lutime
+ end
+
+ describe "File.lutime" do
+ before :each do
+ @atime = Time.utc(2000)
+ @mtime = Time.utc(2001)
+ @file = tmp("specs_lutime_file")
+ @symlink = tmp("specs_lutime_symlink")
+ touch @file
+ File.symlink(@file, @symlink)
+ end
+
+ after :each do
+ rm_r @file, @symlink
+ end
+
+ it "sets the access and modification time for a regular file" do
+ File.lutime(@atime, @mtime, @file)
+ stat = File.stat(@file)
+ stat.atime.should == @atime
+ stat.mtime.should === @mtime
+ end
+
+ it "sets the access and modification time for a symlink" do
+ original = File.stat(@file)
+
+ File.lutime(@atime, @mtime, @symlink)
+ stat = File.lstat(@symlink)
+ stat.atime.should == @atime
+ stat.mtime.should === @mtime
+
+ file = File.stat(@file)
+ file.atime.should == original.atime
+ file.mtime.should == original.mtime
+ end
+ end
+end
diff --git a/spec/ruby/core/file/mkfifo_spec.rb b/spec/ruby/core/file/mkfifo_spec.rb
new file mode 100644
index 0000000000..ce4a67fe31
--- /dev/null
+++ b/spec/ruby/core/file/mkfifo_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+
+describe "File.mkfifo" do
+ platform_is_not :windows do
+ before do
+ @path = tmp('fifo')
+ end
+
+ after do
+ rm_r(@path)
+ end
+
+ context "when path passed responds to :to_path" do
+ it "creates a FIFO file at the path specified" do
+ File.mkfifo(@path)
+ File.ftype(@path).should == "fifo"
+ end
+ end
+
+ context "when path passed is not a String value" do
+ it "raises a TypeError" do
+ -> { File.mkfifo(:"/tmp/fifo") }.should.raise(TypeError)
+ end
+ end
+
+ context "when path does not exist" do
+ it "raises an Errno::ENOENT exception" do
+ -> { File.mkfifo("/bogus/path") }.should.raise(Errno::ENOENT)
+ end
+ end
+
+ it "creates a FIFO file at the passed path" do
+ File.mkfifo(@path.to_s)
+ File.ftype(@path).should == "fifo"
+ end
+
+ it "creates a FIFO file with passed mode & ~umask" do
+ File.mkfifo(@path, 0755)
+ File.stat(@path).mode.should == 010755 & ~File.umask
+ end
+
+ it "creates a FIFO file with a default mode of 0666 & ~umask" do
+ File.mkfifo(@path)
+ File.stat(@path).mode.should == 010666 & ~File.umask
+ end
+
+ it "returns 0 after creating the FIFO file" do
+ File.mkfifo(@path).should == 0
+ end
+ end
+end
diff --git a/spec/ruby/core/file/mtime_spec.rb b/spec/ruby/core/file/mtime_spec.rb
new file mode 100644
index 0000000000..d83725e25d
--- /dev/null
+++ b/spec/ruby/core/file/mtime_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+
+describe "File.mtime" do
+ before :each do
+ @filename = tmp('i_exist')
+ touch(@filename) { @mtime = Time.now }
+ end
+
+ after :each do
+ rm_r @filename
+ end
+
+ it "returns the modification Time of the file" do
+ File.mtime(@filename).should.is_a?(Time)
+ File.mtime(@filename).should be_close(@mtime, TIME_TOLERANCE)
+ end
+
+ platform_is :linux, :windows do
+ unless ENV.key?('TRAVIS') # https://bugs.ruby-lang.org/issues/17926
+ it "returns the modification Time of the file with microseconds" do
+ supports_subseconds = Integer(`stat -c%y '#{__FILE__}'`[/\.(\d{1,6})/, 1], 10)
+ if supports_subseconds != 0
+ expected_time = Time.at(Time.now.to_i + 0.123456)
+ File.utime 0, expected_time, @filename
+ File.mtime(@filename).usec.should == expected_time.usec
+ else
+ File.mtime(__FILE__).usec.should == 0
+ end
+ rescue Errno::ENOENT => e
+ # Windows don't have stat command.
+ skip e.message
+ end
+ end
+ end
+
+ it "raises an Errno::ENOENT exception if the file is not found" do
+ -> { File.mtime('bogus') }.should.raise(Errno::ENOENT)
+ end
+end
+
+describe "File#mtime" do
+ before :each do
+ @filename = tmp('i_exist')
+ @f = File.open(@filename, 'w')
+ end
+
+ after :each do
+ @f.close
+ rm_r @filename
+ end
+
+ it "returns the modification Time of the file" do
+ @f.mtime.should.is_a?(Time)
+ end
+
+end
diff --git a/spec/ruby/core/file/new_spec.rb b/spec/ruby/core/file/new_spec.rb
new file mode 100644
index 0000000000..4cd2cb5dcb
--- /dev/null
+++ b/spec/ruby/core/file/new_spec.rb
@@ -0,0 +1,223 @@
+require_relative '../../spec_helper'
+require_relative 'shared/open'
+
+describe "File.new" do
+ before :each do
+ @file = tmp('test.txt')
+ @fh = nil
+ @flags = File::CREAT | File::TRUNC | File::WRONLY
+ touch @file
+ end
+
+ after :each do
+ @fh.close if @fh
+ rm_r @file
+ end
+
+ it "returns a new File with mode string" do
+ @fh = File.new(@file, 'w')
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new File with mode num" do
+ @fh = File.new(@file, @flags)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new File with modus num and permissions" do
+ rm_r @file
+ File.umask(0011)
+ @fh = File.new(@file, @flags, 0755)
+ @fh.should.is_a?(File)
+ platform_is_not :windows do
+ File.stat(@file).mode.to_s(8).should == "100744"
+ end
+ File.should.exist?(@file)
+ end
+
+ it "creates the file and returns writable descriptor when called with 'w' mode and r-o permissions" do
+ # it should be possible to write to such a file via returned descriptor,
+ # even though the file permissions are r-r-r.
+
+ rm_r @file
+ begin
+ f = File.new(@file, "w", 0444)
+ -> { f.puts("test") }.should_not.raise(IOError)
+ ensure
+ f.close
+ end
+ File.should.exist?(@file)
+ File.read(@file).should == "test\n"
+ end
+
+ platform_is_not :windows do
+ it "opens the existing file, does not change permissions even when they are specified" do
+ File.chmod(0644, @file) # r-w perms
+ orig_perms = File.stat(@file).mode & 0777
+ begin
+ f = File.new(@file, "w", 0444) # r-o perms, but they should be ignored
+ f.puts("test")
+ ensure
+ f.close
+ end
+ perms = File.stat(@file).mode & 0777
+ perms.should == orig_perms
+
+ # it should be still possible to read from the file
+ File.read(@file).should == "test\n"
+ end
+ end
+
+ it "returns a new File with modus fd" do
+ @fh = File.new(@file)
+ fh_copy = File.new(@fh.fileno)
+ fh_copy.autoclose = false
+ fh_copy.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new read-only File when mode is not specified" do
+ @fh = File.new(@file)
+
+ -> { @fh.puts("test") }.should.raise(IOError)
+ @fh.read.should == ""
+ File.should.exist?(@file)
+ end
+
+ it "returns a new read-only File when mode is not specified but flags option is present" do
+ @fh = File.new(@file, flags: File::CREAT)
+
+ -> { @fh.puts("test") }.should.raise(IOError)
+ @fh.read.should == ""
+ File.should.exist?(@file)
+ end
+
+ it "creates a new file when use File::EXCL mode" do
+ @fh = File.new(@file, File::EXCL)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "raises an Errno::EEXIST if the file exists when create a new file with File::CREAT|File::EXCL" do
+ -> { @fh = File.new(@file, File::CREAT|File::EXCL) }.should.raise(Errno::EEXIST)
+ end
+
+ it "creates a new file when use File::WRONLY|File::APPEND mode" do
+ @fh = File.new(@file, File::WRONLY|File::APPEND)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new File when use File::APPEND mode" do
+ @fh = File.new(@file, File::APPEND)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new File when use File::RDONLY|File::APPEND mode" do
+ @fh = File.new(@file, File::RDONLY|File::APPEND)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new File when use File::RDONLY|File::WRONLY mode" do
+ @fh = File.new(@file, File::RDONLY|File::WRONLY)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "creates a new file when use File::WRONLY|File::TRUNC mode" do
+ @fh = File.new(@file, File::WRONLY|File::TRUNC)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "returns a new read-only File when use File::RDONLY|File::CREAT mode" do
+ @fh = File.new(@file, File::RDONLY|File::CREAT)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+
+ # it's read-only
+ -> { @fh.puts("test") }.should.raise(IOError)
+ @fh.read.should == ""
+ end
+
+ it "returns a new read-only File when use File::CREAT mode" do
+ @fh = File.new(@file, File::CREAT)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+
+ # it's read-only
+ -> { @fh.puts("test") }.should.raise(IOError)
+ @fh.read.should == ""
+ end
+
+ it "coerces filename using to_str" do
+ name = mock("file")
+ name.should_receive(:to_str).and_return(@file)
+ @fh = File.new(name, "w")
+ File.should.exist?(@file)
+ end
+
+ it "coerces filename using #to_path" do
+ name = mock("file")
+ name.should_receive(:to_path).and_return(@file)
+ @fh = File.new(name, "w")
+ File.should.exist?(@file)
+ end
+
+ it "accepts options as a keyword argument" do
+ @fh = File.new(@file, 'w', 0755, flags: @flags)
+ @fh.should.is_a?(File)
+ @fh.close
+
+ -> {
+ @fh = File.new(@file, 'w', 0755, {flags: @flags})
+ }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)")
+ end
+
+ it "bitwise-ORs mode and flags option" do
+ -> {
+ @fh = File.new(@file, 'w', flags: File::EXCL)
+ }.should.raise(Errno::EEXIST, /File exists/)
+
+ -> {
+ @fh = File.new(@file, mode: 'w', flags: File::EXCL)
+ }.should.raise(Errno::EEXIST, /File exists/)
+ end
+
+ it "does not use the given block and warns to use File::open" do
+ -> {
+ @fh = File.new(@file) { raise }
+ }.should complain(/warning: File::new\(\) does not take block; use File::open\(\) instead/)
+ end
+
+ it "raises a TypeError if the first parameter can't be coerced to a string" do
+ -> { File.new(true) }.should.raise(TypeError)
+ -> { File.new(false) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the first parameter is nil" do
+ -> { File.new(nil) }.should.raise(TypeError)
+ end
+
+ it "raises an Errno::EBADF if the first parameter is an invalid file descriptor" do
+ -> { File.new(-1) }.should.raise(Errno::EBADF)
+ end
+
+ platform_is_not :windows do
+ it "can't alter mode or permissions when opening a file" do
+ @fh = File.new(@file)
+ -> {
+ f = File.new(@fh.fileno, @flags)
+ f.autoclose = false
+ }.should.raise(Errno::EINVAL)
+ end
+ end
+
+ platform_is_not :windows do
+ it_behaves_like :open_directory, :new
+ end
+end
diff --git a/spec/ruby/core/file/null_spec.rb b/spec/ruby/core/file/null_spec.rb
new file mode 100644
index 0000000000..355b72b799
--- /dev/null
+++ b/spec/ruby/core/file/null_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "File::NULL" do
+ platform_is :windows do
+ it "returns NUL as a string" do
+ File::NULL.should == 'NUL'
+ end
+ end
+
+ platform_is_not :windows do
+ it "returns /dev/null as a string" do
+ File::NULL.should == '/dev/null'
+ end
+ end
+end
diff --git a/spec/ruby/core/file/open_spec.rb b/spec/ruby/core/file/open_spec.rb
new file mode 100644
index 0000000000..7318c31636
--- /dev/null
+++ b/spec/ruby/core/file/open_spec.rb
@@ -0,0 +1,713 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/open'
+
+describe "File.open" do
+ before :all do
+ @file = tmp("file_open.txt")
+ @unicode_path = tmp("ã“ã‚“ã«ã¡ã¯.txt")
+ @nonexistent = tmp("fake.txt")
+ rm_r @file, @nonexistent
+ end
+
+ before :each do
+ ScratchPad.record []
+
+ @fh = @fd = nil
+ @flags = File::CREAT | File::TRUNC | File::WRONLY
+ touch @file
+ end
+
+ after :each do
+ @fh.close if @fh and not @fh.closed?
+ rm_r @file, @unicode_path, @nonexistent
+ end
+
+ describe "with a block" do
+ it "does not raise error when file is closed inside the block" do
+ @fh = File.open(@file) { |fh| fh.close; fh }
+ @fh.should.closed?
+ end
+
+ it "invokes close on an opened file when exiting the block" do
+ File.open(@file, 'r') { |f| FileSpecs.make_closer f }
+
+ ScratchPad.recorded.should == [:file_opened, :file_closed]
+ end
+
+ it "propagates non-StandardErrors produced by close" do
+ -> {
+ File.open(@file, 'r') { |f| FileSpecs.make_closer f, Exception }
+ }.should.raise(Exception)
+
+ ScratchPad.recorded.should == [:file_opened, :file_closed]
+ end
+
+ it "propagates StandardErrors produced by close" do
+ -> {
+ File.open(@file, 'r') { |f| FileSpecs.make_closer f, StandardError }
+ }.should.raise(StandardError)
+
+ ScratchPad.recorded.should == [:file_opened, :file_closed]
+ end
+
+ it "does not propagate IOError with 'closed stream' message produced by close" do
+ File.open(@file, 'r') { |f| FileSpecs.make_closer f, IOError.new('closed stream') }
+
+ ScratchPad.recorded.should == [:file_opened, :file_closed]
+ end
+ end
+
+ it "opens the file (basic case)" do
+ @fh = File.open(@file)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens the file with unicode characters" do
+ @fh = File.open(@unicode_path, "w")
+ @fh.should.is_a?(File)
+ File.should.exist?(@unicode_path)
+ end
+
+ it "opens a file when called with a block" do
+ File.open(@file) { |fh| }
+ File.should.exist?(@file)
+ end
+
+ it "opens with mode string" do
+ @fh = File.open(@file, 'w')
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file with mode string and block" do
+ File.open(@file, 'w') { |fh| }
+ File.should.exist?(@file)
+ end
+
+ it "opens a file with mode num" do
+ @fh = File.open(@file, @flags)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file with mode num and block" do
+ File.open(@file, 'w') { |fh| }
+ File.should.exist?(@file)
+ end
+
+ it "opens a file with mode and permission as nil" do
+ @fh = File.open(@file, nil, nil)
+ @fh.should.is_a?(File)
+ end
+
+ # For this test we delete the file first to reset the perms
+ it "opens the file when passed mode, num and permissions" do
+ rm_r @file
+ File.umask(0011)
+ @fh = File.open(@file, @flags, 0755)
+ @fh.should.is_a?(File)
+ platform_is_not :windows do
+ @fh.lstat.mode.to_s(8).should == "100744"
+ end
+ File.should.exist?(@file)
+ end
+
+ # For this test we delete the file first to reset the perms
+ it "opens the file when passed mode, num, permissions and block" do
+ rm_r @file
+ File.umask(0022)
+ File.open(@file, "w", 0755){ |fh| }
+ platform_is_not :windows do
+ File.stat(@file).mode.to_s(8).should == "100755"
+ end
+ File.should.exist?(@file)
+ end
+
+ it "creates the file and returns writable descriptor when called with 'w' mode and r-o permissions" do
+ # it should be possible to write to such a file via returned descriptor,
+ # even though the file permissions are r-r-r.
+
+ File.open(@file, "w", 0444) { |f| f.write("test") }
+ File.read(@file).should == "test"
+ end
+
+ platform_is_not :windows do
+ it "opens the existing file, does not change permissions even when they are specified" do
+ File.chmod(0664, @file)
+ orig_perms = File.stat(@file).mode.to_s(8)
+ File.open(@file, "w", 0444) { |f| f.write("test") }
+
+ File.stat(@file).mode.to_s(8).should == orig_perms
+ File.read(@file).should == "test"
+ end
+ end
+
+ platform_is_not :windows do
+ as_user do
+ it "creates a new write-only file when invoked with 'w' and '0222'" do
+ rm_r @file
+ File.open(@file, 'w', 0222) {}
+ File.readable?(@file).should == false
+ File.writable?(@file).should == true
+ end
+ end
+ end
+
+ it "opens the file when call with fd" do
+ @fh = File.open(@file)
+ fh_copy = File.open(@fh.fileno)
+ fh_copy.autoclose = false
+ fh_copy.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file that no exists when use File::WRONLY mode" do
+ -> { File.open(@nonexistent, File::WRONLY) }.should.raise(Errno::ENOENT)
+ end
+
+ it "opens a file that no exists when use File::RDONLY mode" do
+ -> { File.open(@nonexistent, File::RDONLY) }.should.raise(Errno::ENOENT)
+ end
+
+ it "opens a file that no exists when use 'r' mode" do
+ -> { File.open(@nonexistent, 'r') }.should.raise(Errno::ENOENT)
+ end
+
+ it "opens a file that no exists when use File::EXCL mode" do
+ -> { File.open(@nonexistent, File::EXCL) }.should.raise(Errno::ENOENT)
+ end
+
+ it "opens a file that no exists when use File::NONBLOCK mode" do
+ -> { File.open(@nonexistent, File::NONBLOCK) }.should.raise(Errno::ENOENT)
+ end
+
+ platform_is_not :openbsd, :windows do
+ it "opens a file that no exists when use File::TRUNC mode" do
+ -> { File.open(@nonexistent, File::TRUNC) }.should.raise(Errno::ENOENT)
+ end
+ end
+
+ platform_is :openbsd, :windows do
+ it "does not open a file that does no exists when using File::TRUNC mode" do
+ -> { File.open(@nonexistent, File::TRUNC) }.should.raise(Errno::EINVAL)
+ end
+ end
+
+ platform_is_not :windows do
+ it "opens a file that no exists when use File::NOCTTY mode" do
+ -> { File.open(@nonexistent, File::NOCTTY) }.should.raise(Errno::ENOENT)
+ end
+ end
+
+ it "opens a file that no exists when use File::CREAT mode" do
+ @fh = File.open(@nonexistent, File::CREAT) { |f| f }
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file that no exists when use 'a' mode" do
+ @fh = File.open(@nonexistent, 'a') { |f| f }
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file that no exists when use 'w' mode" do
+ @fh = File.open(@nonexistent, 'w') { |f| f }
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ # Check the grants associated to the different open modes combinations.
+ it "raises an ArgumentError exception when call with an unknown mode" do
+ -> { File.open(@file, "q") }.should.raise(ArgumentError)
+ end
+
+ it "can read in a block when call open with RDONLY mode" do
+ File.open(@file, File::RDONLY) do |f|
+ f.gets.should == nil
+ end
+ end
+
+ it "can read in a block when call open with 'r' mode" do
+ File.open(@file, "r") do |f|
+ f.gets.should == nil
+ end
+ end
+
+ it "raises an IO exception when write in a block opened with RDONLY mode" do
+ File.open(@file, File::RDONLY) do |f|
+ -> { f.puts "writing ..." }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IO exception when write in a block opened with 'r' mode" do
+ File.open(@file, "r") do |f|
+ -> { f.puts "writing ..." }.should.raise(IOError)
+ end
+ end
+
+ it "can't write in a block when call open with File::WRONLY||File::RDONLY mode" do
+ File.open(@file, File::WRONLY|File::RDONLY ) do |f|
+ f.puts("writing").should == nil
+ end
+ end
+
+ it "can't read in a block when call open with File::WRONLY||File::RDONLY mode" do
+ -> {
+ File.open(@file, File::WRONLY|File::RDONLY ) do |f|
+ f.gets.should == nil
+ end
+ }.should.raise(IOError)
+ end
+
+ it "can write in a block when call open with WRONLY mode" do
+ File.open(@file, File::WRONLY) do |f|
+ f.puts("writing").should == nil
+ end
+ end
+
+ it "can write in a block when call open with 'w' mode" do
+ File.open(@file, "w") do |f|
+ f.puts("writing").should == nil
+ end
+ end
+
+ it "raises an IOError when read in a block opened with WRONLY mode" do
+ File.open(@file, File::WRONLY) do |f|
+ -> { f.gets }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IOError when read in a block opened with 'w' mode" do
+ File.open(@file, "w") do |f|
+ -> { f.gets }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IOError when read in a block opened with 'a' mode" do
+ File.open(@file, "a") do |f|
+ -> { f.gets }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IOError when read in a block opened with 'a' mode" do
+ File.open(@file, "a") do |f|
+ f.puts("writing").should == nil
+ -> { f.gets }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IOError when read in a block opened with 'a' mode" do
+ File.open(@file, File::WRONLY|File::APPEND ) do |f|
+ -> { f.gets }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IOError when read in a block opened with File::WRONLY|File::APPEND mode" do
+ File.open(@file, File::WRONLY|File::APPEND ) do |f|
+ f.puts("writing").should == nil
+ -> { f.gets }.should.raise(IOError)
+ end
+ end
+
+ it "raises an IOError when write in a block opened with File::RDONLY|File::APPEND mode" do
+ -> {
+ File.open(@file, File::RDONLY|File::APPEND ) do |f|
+ f.puts("writing")
+ end
+ }.should.raise(IOError)
+ end
+
+ it "can read and write in a block when call open with RDWR mode" do
+ File.open(@file, File::RDWR) do |f|
+ f.gets.should == nil
+ f.puts("writing").should == nil
+ f.rewind
+ f.gets.should == "writing\n"
+ end
+ end
+
+ it "can't read in a block when call open with File::EXCL mode" do
+ -> {
+ File.open(@file, File::EXCL) do |f|
+ f.puts("writing").should == nil
+ end
+ }.should.raise(IOError)
+ end
+
+ it "can read in a block when call open with File::EXCL mode" do
+ File.open(@file, File::EXCL) do |f|
+ f.gets.should == nil
+ end
+ end
+
+ it "can read and write in a block when call open with File::RDWR|File::EXCL mode" do
+ File.open(@file, File::RDWR|File::EXCL) do |f|
+ f.gets.should == nil
+ f.puts("writing").should == nil
+ f.rewind
+ f.gets.should == "writing\n"
+ end
+ end
+
+ it "raises an Errno::EEXIST if the file exists when open with File::CREAT|File::EXCL" do
+ -> {
+ File.open(@file, File::CREAT|File::EXCL) do |f|
+ f.puts("writing")
+ end
+ }.should.raise(Errno::EEXIST)
+ end
+
+ it "creates a new file when use File::WRONLY|File::APPEND mode" do
+ @fh = File.open(@file, File::WRONLY|File::APPEND)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file when use File::WRONLY|File::APPEND mode" do
+ File.open(@file, File::WRONLY) do |f|
+ f.puts("hello file")
+ end
+ File.open(@file, File::RDWR|File::APPEND) do |f|
+ f.puts("bye file")
+ f.rewind
+ f.gets.should == "hello file\n"
+ f.gets.should == "bye file\n"
+ f.gets.should == nil
+ end
+ end
+
+ it "raises an IOError if the file exists when open with File::RDONLY|File::APPEND" do
+ -> {
+ File.open(@file, File::RDONLY|File::APPEND) do |f|
+ f.puts("writing").should == nil
+ end
+ }.should.raise(IOError)
+ end
+
+ platform_is_not :openbsd, :windows do
+ it "truncates the file when passed File::TRUNC mode" do
+ File.open(@file, File::RDWR) { |f| f.puts "hello file" }
+ @fh = File.open(@file, File::TRUNC)
+ @fh.gets.should == nil
+ end
+
+ it "can't read in a block when call open with File::TRUNC mode" do
+ File.open(@file, File::TRUNC) do |f|
+ f.gets.should == nil
+ end
+ end
+ end
+
+ it "opens a file when use File::WRONLY|File::TRUNC mode" do
+ fh1 = File.open(@file, "w")
+ begin
+ @fh = File.open(@file, File::WRONLY|File::TRUNC)
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ ensure
+ fh1.close
+ end
+ end
+
+ platform_is_not :openbsd, :windows do
+ it "can't write in a block when call open with File::TRUNC mode" do
+ -> {
+ File.open(@file, File::TRUNC) do |f|
+ f.puts("writing")
+ end
+ }.should.raise(IOError)
+ end
+
+ it "raises an Errno::EEXIST if the file exists when open with File::RDONLY|File::TRUNC" do
+ -> {
+ File.open(@file, File::RDONLY|File::TRUNC) do |f|
+ f.puts("writing").should == nil
+ end
+ }.should.raise(IOError)
+ end
+ end
+
+ platform_is :openbsd, :windows do
+ it "can't write in a block when call open with File::TRUNC mode" do
+ -> {
+ File.open(@file, File::TRUNC) do |f|
+ f.puts("writing")
+ end
+ }.should.raise(Errno::EINVAL)
+ end
+
+ it "raises an Errno::EEXIST if the file exists when open with File::RDONLY|File::TRUNC" do
+ -> {
+ File.open(@file, File::RDONLY|File::TRUNC) do |f|
+ f.puts("writing").should == nil
+ end
+ }.should.raise(Errno::EINVAL)
+ end
+ end
+
+ platform_is_not :windows do
+ as_user do
+ it "raises an Errno::EACCES when opening non-permitted file" do
+ @fh = File.open(@file, "w")
+ @fh.chmod(000)
+ -> { fh1 = File.open(@file); fh1.close }.should.raise(Errno::EACCES)
+ end
+ end
+ end
+
+ as_user do
+ it "raises an Errno::EACCES when opening read-only file" do
+ @fh = File.open(@file, "w")
+ @fh.chmod(0444)
+ -> { File.open(@file, "w") }.should.raise(Errno::EACCES)
+ end
+ end
+
+ it "opens a file for binary read" do
+ @fh = File.open(@file, "rb")
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file for binary write" do
+ @fh = File.open(@file, "wb")
+ @fh.should.is_a?(File)
+ File.should.exist?(@file)
+ end
+
+ it "opens a file for read-write and truncate the file" do
+ File.open(@file, "w") { |f| f.puts "testing" }
+ File.size(@file).should > 0
+ File.open(@file, "w+") do |f|
+ f.pos.should == 0
+ f.should.eof?
+ end
+ File.size(@file).should == 0
+ end
+
+ it "opens a file for binary read-write starting at the beginning of the file" do
+ File.open(@file, "w") { |f| f.puts "testing" }
+ File.size(@file).should > 0
+ File.open(@file, "rb+") do |f|
+ f.binmode?.should == true
+ f.external_encoding.should == Encoding::ASCII_8BIT
+ f.pos.should == 0
+ f.should_not.eof?
+ end
+ File.open(@file, "r+b") do |f|
+ f.binmode?.should == true
+ f.external_encoding.should == Encoding::ASCII_8BIT
+ f.pos.should == 0
+ f.should_not.eof?
+ end
+ end
+
+ it "opens a file for binary read-write and truncate the file" do
+ File.open(@file, "w") { |f| f.puts "testing" }
+ File.size(@file).should > 0
+ File.open(@file, "wb+") do |f|
+ f.pos.should == 0
+ f.should.eof?
+ end
+ File.size(@file).should == 0
+ end
+
+ platform_is :linux do
+ guard -> { defined?(File::TMPFILE) } do
+ it "creates an unnamed temporary file with File::TMPFILE" do
+ dir = tmp("tmpfilespec")
+ mkdir_p dir
+ begin
+ Dir["#{dir}/*"].should == []
+ File.open(dir, "r+", flags: File::TMPFILE) do |io|
+ io.write("ruby")
+ io.flush
+ io.rewind
+ io.read.should == "ruby"
+ Dir["#{dir}/*"].should == []
+ end
+ rescue Errno::EOPNOTSUPP
+ skip "no support from the filesystem"
+ rescue Errno::EINVAL, Errno::EISDIR
+ skip "presumably bug in glibc"
+ ensure
+ rm_r dir
+ end
+ end
+ end
+ end
+
+ it "raises a TypeError if passed a filename that is not a String or Integer type" do
+ -> { File.open(true) }.should.raise(TypeError)
+ -> { File.open(false) }.should.raise(TypeError)
+ -> { File.open(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a SystemCallError if passed an invalid Integer type" do
+ -> { File.open(-1) }.should.raise(SystemCallError)
+ end
+
+ it "raises an ArgumentError if passed the wrong number of arguments" do
+ -> { File.open(@file, File::CREAT, 0755, 'test') }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if passed an invalid string for mode" do
+ -> { File.open(@file, 'fake') }.should.raise(ArgumentError)
+ end
+
+ it "defaults external_encoding to BINARY for binary modes" do
+ File.open(@file, 'rb') {|f| f.external_encoding.should == Encoding::BINARY}
+ File.open(@file, 'wb+') {|f| f.external_encoding.should == Encoding::BINARY}
+ end
+
+ it "accepts options as a keyword argument" do
+ @fh = File.open(@file, 'w', 0755, flags: File::CREAT)
+ @fh.should.instance_of?(File)
+
+ -> {
+ File.open(@file, 'w', 0755, {flags: File::CREAT})
+ }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)")
+ end
+
+ it "uses the second argument as an options Hash" do
+ @fh = File.open(@file, mode: "r")
+ @fh.should.instance_of?(File)
+ end
+
+ it "calls #to_hash to convert the second argument to a Hash" do
+ options = mock("file open options")
+ options.should_receive(:to_hash).and_return({ mode: "r" })
+
+ @fh = File.open(@file, **options)
+ end
+
+ it "accepts extra flags as a keyword argument and combine with a string mode" do
+ -> {
+ File.open(@file, "w", flags: File::EXCL) { }
+ }.should.raise(Errno::EEXIST)
+
+ -> {
+ File.open(@file, mode: "w", flags: File::EXCL) { }
+ }.should.raise(Errno::EEXIST)
+ end
+
+ it "accepts extra flags as a keyword argument and combine with an integer mode" do
+ -> {
+ File.open(@file, File::WRONLY | File::CREAT, flags: File::EXCL) { }
+ }.should.raise(Errno::EEXIST)
+ end
+
+ platform_is_not :windows do
+ describe "on a FIFO" do
+ before :each do
+ @fifo = tmp("File_open_fifo")
+ File.mkfifo(@fifo)
+ end
+
+ after :each do
+ rm_r @fifo
+ end
+
+ it "opens it as a normal file" do
+ file_w, file_r, read_bytes, written_length = nil
+
+ # open in threads, due to blocking open and writes
+ writer = Thread.new do
+ file_w = File.open(@fifo, 'w')
+ written_length = file_w.syswrite('hello')
+ end
+ reader = Thread.new do
+ file_r = File.open(@fifo, 'r')
+ read_bytes = file_r.sysread(5)
+ end
+
+ begin
+ writer.join
+ reader.join
+
+ written_length.should == 5
+ read_bytes.should == 'hello'
+ ensure
+ file_w.close if file_w
+ file_r.close if file_r
+ end
+ end
+ end
+ end
+
+ it "raises ArgumentError if mixing :newline and binary mode" do
+ -> {
+ File.open(@file, "rb", newline: :universal) {}
+ }.should.raise(ArgumentError, "newline decorator with binary mode")
+ end
+
+ context "'x' flag" do
+ before :each do
+ @xfile = tmp("x-flag")
+ rm_r @xfile
+ end
+
+ after :each do
+ rm_r @xfile
+ end
+
+ it "does nothing if the file doesn't exist" do
+ File.open(@xfile, "wx") { |f| f.write("content") }
+ File.read(@xfile).should == "content"
+ end
+
+ it "throws a Errno::EEXIST error if the file exists" do
+ touch @xfile
+ -> { File.open(@xfile, "wx") }.should.raise(Errno::EEXIST)
+ end
+
+ it "can't be used with 'r' and 'a' flags" do
+ -> { File.open(@xfile, "rx") }.should.raise(ArgumentError, 'invalid access mode rx')
+ -> { File.open(@xfile, "ax") }.should.raise(ArgumentError, 'invalid access mode ax')
+ end
+ end
+end
+
+describe "File.open when passed a file descriptor" do
+ before do
+ @content = "File#open when passed a file descriptor"
+ @name = tmp("file_open_with_fd.txt")
+ @fd = new_fd @name, "w:utf-8"
+ @file = nil
+ end
+
+ after do
+ @file.close if @file and not @file.closed?
+ rm_r @name
+ end
+
+ it "opens a file" do
+ @file = File.open(@fd, "w")
+ @file.should.instance_of?(File)
+ @file.fileno.should.equal?(@fd)
+ @file.write @content
+ @file.flush
+ File.read(@name).should == @content
+ end
+
+ it "opens a file when passed a block" do
+ @file = File.open(@fd, "w") do |f|
+ f.should.instance_of?(File)
+ f.fileno.should.equal?(@fd)
+ f.write @content
+ f
+ end
+ File.read(@name).should == @content
+ end
+end
+
+platform_is_not :windows do
+ describe "File.open" do
+ it_behaves_like :open_directory, :open
+ end
+end
diff --git a/spec/ruby/core/file/owned_spec.rb b/spec/ruby/core/file/owned_spec.rb
new file mode 100644
index 0000000000..06d6796da9
--- /dev/null
+++ b/spec/ruby/core/file/owned_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/owned'
+
+describe "File.owned?" do
+ it_behaves_like :file_owned, :owned?, File
+end
+
+describe "File.owned?" do
+ before :each do
+ @filename = tmp("i_exist")
+ touch(@filename)
+ end
+
+ after :each do
+ rm_r @filename
+ end
+
+ it "returns false if file does not exist" do
+ File.owned?("I_am_a_bogus_file").should == false
+ end
+
+ it "returns true if the file exist and is owned by the user" do
+ File.owned?(@filename).should == true
+ end
+
+ platform_is_not :windows do
+ as_user do
+ it "returns false when the file is not owned by the user" do
+ system_file = '/etc/passwd'
+ File.owned?(system_file).should == false
+ end
+ end
+ end
+
+end
diff --git a/spec/ruby/core/file/path_spec.rb b/spec/ruby/core/file/path_spec.rb
new file mode 100644
index 0000000000..ce78dbeede
--- /dev/null
+++ b/spec/ruby/core/file/path_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative 'shared/path'
+
+describe "File#path" do
+ it_behaves_like :file_path, :path
+end
+
+describe "File.path" do
+ before :each do
+ @name = tmp("file_path")
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "returns the string argument without any change" do
+ File.path("abc").should == "abc"
+ File.path("./abc").should == "./abc"
+ File.path("../abc").should == "../abc"
+ File.path("/./a/../bc").should == "/./a/../bc"
+ end
+
+ it "returns path for File argument" do
+ File.open(@name, "w") do |f|
+ File.path(f).should == @name
+ end
+ end
+
+ it "returns path for Pathname argument" do
+ require "pathname"
+ File.path(Pathname.new(@name)).should == @name
+ end
+
+ it "calls #to_path for non-string argument and returns result" do
+ path = mock("path")
+ path.should_receive(:to_path).and_return("abc")
+ File.path(path).should == "abc"
+ end
+
+ it "raises TypeError when #to_path result is not a string" do
+ path = mock("path")
+ path.should_receive(:to_path).and_return(nil)
+ -> { File.path(path) }.should.raise TypeError
+
+ path = mock("path")
+ path.should_receive(:to_path).and_return(42)
+ -> { File.path(path) }.should.raise TypeError
+ end
+
+ it "raises ArgumentError for string argument contains NUL character" do
+ -> { File.path("\0") }.should.raise ArgumentError
+ -> { File.path("a\0") }.should.raise ArgumentError
+ -> { File.path("a\0c") }.should.raise ArgumentError
+ end
+
+ it "raises ArgumentError when #to_path result contains NUL character" do
+ path = mock("path")
+ path.should_receive(:to_path).and_return("\0")
+ -> { File.path(path) }.should.raise ArgumentError
+
+ path = mock("path")
+ path.should_receive(:to_path).and_return("a\0")
+ -> { File.path(path) }.should.raise ArgumentError
+
+ path = mock("path")
+ path.should_receive(:to_path).and_return("a\0c")
+ -> { File.path(path) }.should.raise ArgumentError
+ end
+
+ it "raises Encoding::CompatibilityError for ASCII-incompatible string argument" do
+ path = "abc".encode(Encoding::UTF_32BE)
+ -> { File.path(path) }.should.raise Encoding::CompatibilityError
+ end
+
+ it "raises Encoding::CompatibilityError when #to_path result is ASCII-incompatible" do
+ path = mock("path")
+ path.should_receive(:to_path).and_return("abc".encode(Encoding::UTF_32BE))
+ -> { File.path(path) }.should.raise Encoding::CompatibilityError
+ end
+end
diff --git a/spec/ruby/core/file/pipe_spec.rb b/spec/ruby/core/file/pipe_spec.rb
new file mode 100644
index 0000000000..01d72dbe85
--- /dev/null
+++ b/spec/ruby/core/file/pipe_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/pipe'
+
+describe "File.pipe?" do
+ it_behaves_like :file_pipe, :pipe?, File
+end
+
+describe "File.pipe?" do
+ it "returns false if file does not exist" do
+ File.pipe?("I_am_a_bogus_file").should == false
+ end
+
+ it "returns false if the file is not a pipe" do
+ filename = tmp("i_exist")
+ touch(filename)
+
+ File.pipe?(filename).should == false
+
+ rm_r filename
+ end
+
+ platform_is_not :windows do
+ it "returns true if the file is a pipe" do
+ filename = tmp("i_am_a_pipe")
+ File.mkfifo(filename)
+
+ File.pipe?(filename).should == true
+
+ rm_r filename
+ end
+ end
+end
diff --git a/spec/ruby/core/file/printf_spec.rb b/spec/ruby/core/file/printf_spec.rb
new file mode 100644
index 0000000000..2530419fc7
--- /dev/null
+++ b/spec/ruby/core/file/printf_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative '../kernel/shared/sprintf'
+
+describe "File#printf" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ begin
+ @filename = tmp("printf.txt")
+
+ File.open(@filename, "w", encoding: "utf-8") do |f|
+ f.printf(format, *args)
+ end
+
+ File.read(@filename, encoding: "utf-8")
+ ensure
+ rm_r @filename
+ end
+ }
+end
diff --git a/spec/ruby/core/file/read_spec.rb b/spec/ruby/core/file/read_spec.rb
new file mode 100644
index 0000000000..67a3325cbd
--- /dev/null
+++ b/spec/ruby/core/file/read_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/read'
+
+describe "File.read" do
+ it_behaves_like :file_read_directory, :read, File
+end
diff --git a/spec/ruby/core/file/readable_real_spec.rb b/spec/ruby/core/file/readable_real_spec.rb
new file mode 100644
index 0000000000..524466cd96
--- /dev/null
+++ b/spec/ruby/core/file/readable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/readable_real'
+
+describe "File.readable_real?" do
+ it_behaves_like :file_readable_real, :readable_real?, File
+ it_behaves_like :file_readable_real_missing, :readable_real?, File
+end
diff --git a/spec/ruby/core/file/readable_spec.rb b/spec/ruby/core/file/readable_spec.rb
new file mode 100644
index 0000000000..ed75a23f39
--- /dev/null
+++ b/spec/ruby/core/file/readable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/readable'
+
+describe "File.readable?" do
+ it_behaves_like :file_readable, :readable?, File
+ it_behaves_like :file_readable_missing, :readable?, File
+end
diff --git a/spec/ruby/core/file/readlink_spec.rb b/spec/ruby/core/file/readlink_spec.rb
new file mode 100644
index 0000000000..568692b9b6
--- /dev/null
+++ b/spec/ruby/core/file/readlink_spec.rb
@@ -0,0 +1,86 @@
+require_relative '../../spec_helper'
+
+describe "File.readlink" do
+ # symlink/readlink are not supported on Windows
+ platform_is_not :windows do
+ describe "with absolute paths" do
+ before :each do
+ @file = tmp('file_readlink.txt')
+ @link = tmp('file_readlink.lnk')
+
+ File.symlink(@file, @link)
+ end
+
+ after :each do
+ rm_r @file, @link
+ end
+
+ it "returns the name of the file referenced by the given link" do
+ touch @file
+ File.readlink(@link).should == @file
+ end
+
+ it "returns the name of the file referenced by the given link when the file does not exist" do
+ File.readlink(@link).should == @file
+ end
+
+ it "raises an Errno::ENOENT if there is no such file" do
+ # TODO: missing_file
+ -> { File.readlink("/this/surely/does/not/exist") }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises an Errno::EINVAL if called with a normal file" do
+ touch @file
+ -> { File.readlink(@file) }.should.raise(Errno::EINVAL)
+ end
+ end
+
+ describe "with paths containing unicode characters" do
+ before :each do
+ @file = tmp('tàrget.txt')
+ @link = tmp('lïnk.lnk')
+ File.symlink(@file, @link)
+ end
+
+ after :each do
+ rm_r @file, @link
+ end
+
+ it "returns the name of the file referenced by the given link" do
+ touch @file
+ result = File.readlink(@link)
+ result.encoding.should.equal? Encoding.find('filesystem')
+ result.should == @file.dup.force_encoding(Encoding.find('filesystem'))
+ end
+ end
+
+ describe "when changing the working directory" do
+ before :each do
+ @cwd = Dir.pwd
+ @tmpdir = tmp("/readlink")
+ Dir.mkdir @tmpdir
+ Dir.chdir @tmpdir
+
+ @link = 'readlink_link'
+ @file = 'readlink_file'
+
+ File.symlink(@file, @link)
+ end
+
+ after :each do
+ rm_r @file, @link
+ Dir.chdir @cwd
+ Dir.rmdir @tmpdir
+ end
+
+ it "returns the name of the file referenced by the given link" do
+ touch @file
+ File.readlink(@link).should == @file
+ end
+
+ it "returns the name of the file referenced by the given link when the file does not exist" do
+ File.readlink(@link).should == @file
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/realdirpath_spec.rb b/spec/ruby/core/file/realdirpath_spec.rb
new file mode 100644
index 0000000000..ecf1e0c6d9
--- /dev/null
+++ b/spec/ruby/core/file/realdirpath_spec.rb
@@ -0,0 +1,104 @@
+require_relative '../../spec_helper'
+
+platform_is_not :windows do
+ describe "File.realdirpath" do
+ before :each do
+ @real_dir = tmp('dir_realdirpath_real')
+ @fake_dir = tmp('dir_realdirpath_fake')
+ @link_dir = tmp('dir_realdirpath_link')
+
+ mkdir_p @real_dir
+ File.symlink(@real_dir, @link_dir)
+
+ @file = File.join(@real_dir, 'file')
+ @link = File.join(@link_dir, 'link')
+
+ touch @file
+ File.symlink(@file, @link)
+
+ @fake_file_in_real_dir = File.join(@real_dir, 'fake_file_in_real_dir')
+ @fake_file_in_fake_dir = File.join(@fake_dir, 'fake_file_in_fake_dir')
+ @fake_link_to_real_dir = File.join(@link_dir, 'fake_link_to_real_dir')
+ @fake_link_to_fake_dir = File.join(@link_dir, 'fake_link_to_fake_dir')
+
+ File.symlink(@fake_file_in_real_dir, @fake_link_to_real_dir)
+ File.symlink(@fake_file_in_fake_dir, @fake_link_to_fake_dir)
+
+ @dir_for_relative_link = File.join(@real_dir, 'dir1')
+ mkdir_p @dir_for_relative_link
+
+ @relative_path_to_file = File.join('..', 'file')
+ @relative_symlink = File.join(@dir_for_relative_link, 'link')
+ File.symlink(@relative_path_to_file, @relative_symlink)
+ end
+
+ after :each do
+ rm_r @file, @link, @fake_link_to_real_dir, @fake_link_to_fake_dir, @real_dir, @link_dir
+ end
+
+ it "returns '/' when passed '/'" do
+ File.realdirpath('/').should == '/'
+ end
+
+ it "returns the real (absolute) pathname not containing symlinks" do
+ File.realdirpath(@link).should == @file
+ end
+
+ it "uses base directory for interpreting relative pathname" do
+ File.realdirpath(File.basename(@link), @link_dir).should == @file
+ end
+
+ it "uses current directory for interpreting relative pathname" do
+ Dir.chdir @link_dir do
+ File.realdirpath(File.basename(@link)).should == @file
+ end
+ end
+
+ it "uses link directory for expanding relative links" do
+ File.realdirpath(@relative_symlink).should == @file
+ end
+
+ it "raises an Errno::ELOOP if the symlink points to itself" do
+ File.unlink @link
+ File.symlink(@link, @link)
+ -> { File.realdirpath(@link) }.should.raise(Errno::ELOOP)
+ end
+
+ it "returns the real (absolute) pathname if the file is absent" do
+ File.realdirpath(@fake_file_in_real_dir).should == @fake_file_in_real_dir
+ end
+
+ it "raises Errno::ENOENT if the directory is absent" do
+ -> { File.realdirpath(@fake_file_in_fake_dir) }.should.raise(Errno::ENOENT)
+ end
+
+ it "returns the real (absolute) pathname if the symlink points to an absent file" do
+ File.realdirpath(@fake_link_to_real_dir).should == @fake_file_in_real_dir
+ end
+
+ it "raises Errno::ENOENT if the symlink points to an absent directory" do
+ -> { File.realdirpath(@fake_link_to_fake_dir) }.should.raise(Errno::ENOENT)
+ end
+ end
+end
+
+platform_is :windows do
+ describe "File.realdirpath" do
+ before :each do
+ @file = tmp("realdirpath")
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the same path" do
+ touch @file
+ File.realdirpath(@file).should == @file
+ end
+
+ it "returns the same path even if the last component does not exist" do
+ File.realdirpath(@file).should == @file
+ end
+ end
+end
diff --git a/spec/ruby/core/file/realpath_spec.rb b/spec/ruby/core/file/realpath_spec.rb
new file mode 100644
index 0000000000..ccb981eff1
--- /dev/null
+++ b/spec/ruby/core/file/realpath_spec.rb
@@ -0,0 +1,98 @@
+require_relative '../../spec_helper'
+
+platform_is_not :windows do
+ describe "File.realpath" do
+ before :each do
+ @real_dir = tmp('dir_realpath_real')
+ @link_dir = tmp('dir_realpath_link')
+
+ mkdir_p @real_dir
+ File.symlink(@real_dir, @link_dir)
+
+ @file = File.join(@real_dir, 'file')
+ @link = File.join(@link_dir, 'link')
+
+ touch @file
+ File.symlink(@file, @link)
+
+ @fake_file = File.join(@real_dir, 'fake_file')
+ @fake_link = File.join(@link_dir, 'fake_link')
+
+ File.symlink(@fake_file, @fake_link)
+
+ @dir_for_relative_link = File.join(@real_dir, 'dir1')
+ mkdir_p @dir_for_relative_link
+
+ @relative_path_to_file = File.join('..', 'file')
+ @relative_symlink = File.join(@dir_for_relative_link, 'link')
+ File.symlink(@relative_path_to_file, @relative_symlink)
+ end
+
+ after :each do
+ rm_r @file, @link, @fake_link, @real_dir, @link_dir
+ end
+
+ it "returns '/' when passed '/'" do
+ File.realpath('/').should == '/'
+ end
+
+ it "returns the real (absolute) pathname not containing symlinks" do
+ File.realpath(@link).should == @file
+ end
+
+ it "uses base directory for interpreting relative pathname" do
+ File.realpath(File.basename(@link), @link_dir).should == @file
+ end
+
+ it "uses current directory for interpreting relative pathname" do
+ Dir.chdir @link_dir do
+ File.realpath(File.basename(@link)).should == @file
+ end
+ end
+
+ it "uses link directory for expanding relative links" do
+ File.realpath(@relative_symlink).should == @file
+ end
+
+ it "removes the file element when going one level up" do
+ File.realpath('../', @file).should == @real_dir
+ end
+
+ it "raises an Errno::ELOOP if the symlink points to itself" do
+ File.unlink @link
+ File.symlink(@link, @link)
+ -> { File.realpath(@link) }.should.raise(Errno::ELOOP)
+ end
+
+ it "raises Errno::ENOENT if the file is absent" do
+ -> { File.realpath(@fake_file) }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises Errno::ENOENT if the symlink points to an absent file" do
+ -> { File.realpath(@fake_link) }.should.raise(Errno::ENOENT)
+ end
+
+ it "converts the argument with #to_path" do
+ path = mock("path")
+ path.should_receive(:to_path).and_return(__FILE__)
+ File.realpath(path).should == File.realpath(__FILE__ )
+ end
+ end
+end
+
+platform_is :windows do
+ describe "File.realpath" do
+ before :each do
+ @file = tmp("realpath")
+ touch @file
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the same path" do
+ File.realpath(@file).should == @file
+ end
+ end
+end
diff --git a/spec/ruby/core/file/rename_spec.rb b/spec/ruby/core/file/rename_spec.rb
new file mode 100644
index 0000000000..70ea669a68
--- /dev/null
+++ b/spec/ruby/core/file/rename_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "File.rename" do
+ before :each do
+ @old = tmp("file_rename.txt")
+ @new = tmp("file_rename.new")
+
+ rm_r @new
+ touch(@old) { |f| f.puts "hello" }
+ end
+
+ after :each do
+ rm_r @old, @new
+ end
+
+ it "renames a file" do
+ File.should.exist?(@old)
+ File.should_not.exist?(@new)
+ File.rename(@old, @new)
+ File.should_not.exist?(@old)
+ File.should.exist?(@new)
+ end
+
+ it "raises an Errno::ENOENT if the source does not exist" do
+ rm_r @old
+ -> { File.rename(@old, @new) }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises an ArgumentError if not passed two arguments" do
+ -> { File.rename }.should.raise(ArgumentError)
+ -> { File.rename(@file) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if not passed String types" do
+ -> { File.rename(1, 2) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/file/reopen_spec.rb b/spec/ruby/core/file/reopen_spec.rb
new file mode 100644
index 0000000000..858d424c67
--- /dev/null
+++ b/spec/ruby/core/file/reopen_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "File#reopen" do
+ before :each do
+ @name_a = tmp("file_reopen_a.txt")
+ @name_b = tmp("file_reopen_b.txt")
+ @content_a = "File#reopen a"
+ @content_b = "File#reopen b"
+
+ touch(@name_a) { |f| f.write @content_a }
+ touch(@name_b) { |f| f.write @content_b }
+
+ @file = nil
+ end
+
+ after :each do
+ @file.close if @file and not @file.closed?
+ rm_r @name_a, @name_b
+ end
+
+ it "resets the stream to a new file path" do
+ file = File.new @name_a, "r"
+ file.read.should == @content_a
+ @file = file.reopen(@name_b, "r")
+ @file.read.should == @content_b
+ end
+
+ it "calls #to_path to convert an Object" do
+ @file = File.new(@name_a).reopen(mock_to_path(@name_b), "r")
+ @file.read.should == @content_b
+ end
+end
diff --git a/spec/ruby/core/file/setgid_spec.rb b/spec/ruby/core/file/setgid_spec.rb
new file mode 100644
index 0000000000..f5df5390f5
--- /dev/null
+++ b/spec/ruby/core/file/setgid_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/setgid'
+
+describe "File.setgid?" do
+ it_behaves_like :file_setgid, :setgid?, File
+end
+
+describe "File.setgid?" do
+ before :each do
+ @name = tmp('test.txt')
+ touch @name
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "returns false if the file was just made" do
+ File.setgid?(@name).should == false
+ end
+
+ it "returns false if the file does not exist" do
+ rm_r @name # delete it prematurely, just for this part
+ File.setgid?(@name).should == false
+ end
+
+ as_superuser do
+ platform_is_not :windows do
+ it "returns true when the gid bit is set" do
+ system "chmod g+s #{@name}"
+
+ File.setgid?(@name).should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/setuid_spec.rb b/spec/ruby/core/file/setuid_spec.rb
new file mode 100644
index 0000000000..9e5e86df61
--- /dev/null
+++ b/spec/ruby/core/file/setuid_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/setuid'
+
+describe "File.setuid?" do
+ it_behaves_like :file_setuid, :setuid?, File
+end
+
+describe "File.setuid?" do
+ before :each do
+ @name = tmp('test.txt')
+ touch @name
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "returns false if the file was just made" do
+ File.setuid?(@name).should == false
+ end
+
+ it "returns false if the file does not exist" do
+ rm_r @name # delete it prematurely, just for this part
+ File.setuid?(@name).should == false
+ end
+
+ platform_is_not :windows do
+ it "returns true when the gid bit is set" do
+ system "chmod u+s #{@name}"
+
+ File.setuid?(@name).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/file/shared/fnmatch.rb b/spec/ruby/core/file/shared/fnmatch.rb
new file mode 100644
index 0000000000..b9140d027d
--- /dev/null
+++ b/spec/ruby/core/file/shared/fnmatch.rb
@@ -0,0 +1,294 @@
+describe :file_fnmatch, shared: true do
+ it "matches entire strings" do
+ File.send(@method, 'cat', 'cat').should == true
+ end
+
+ it "does not match partial strings" do
+ File.send(@method, 'cat', 'category').should == false
+ end
+
+ it "does not support { } patterns by default" do
+ File.send(@method, 'c{at,ub}s', 'cats').should == false
+ File.send(@method, 'c{at,ub}s', 'c{at,ub}s').should == true
+ end
+
+ it "supports some { } patterns when File::FNM_EXTGLOB is passed" do
+ File.send(@method, "{a,b}", "a", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{a,b}", "b", File::FNM_EXTGLOB).should == true
+ File.send(@method, "c{at,ub}s", "cats", File::FNM_EXTGLOB).should == true
+ File.send(@method, "c{at,ub}s", "cubs", File::FNM_EXTGLOB).should == true
+ File.send(@method, "-c{at,ub}s-", "-cats-", File::FNM_EXTGLOB).should == true
+ File.send(@method, "-c{at,ub}s-", "-cubs-", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{a,b,c}{d,e,f}{g,h}", "adg", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{a,b,c}{d,e,f}{g,h}", "bdg", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{a,b,c}{d,e,f}{g,h}", "ceh", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{aa,bb,cc,dd}", "aa", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{aa,bb,cc,dd}", "bb", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{aa,bb,cc,dd}", "cc", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{aa,bb,cc,dd}", "dd", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{1,5{a,b{c,d}}}", "1", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{1,5{a,b{c,d}}}", "5a", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{1,5{a,b{c,d}}}", "5bc", File::FNM_EXTGLOB).should == true
+ File.send(@method, "{1,5{a,b{c,d}}}", "5bd", File::FNM_EXTGLOB).should == true
+ File.send(@method, "\\\\{a\\,b,b\\}c}", "\\a,b", File::FNM_EXTGLOB).should == true
+ File.send(@method, "\\\\{a\\,b,b\\}c}", "\\b}c", File::FNM_EXTGLOB).should == true
+ end
+
+ it "doesn't support some { } patterns even when File::FNM_EXTGLOB is passed" do
+ File.send(@method, "a{0..3}b", "a0b", File::FNM_EXTGLOB).should == false
+ File.send(@method, "a{0..3}b", "a1b", File::FNM_EXTGLOB).should == false
+ File.send(@method, "a{0..3}b", "a2b", File::FNM_EXTGLOB).should == false
+ File.send(@method, "a{0..3}b", "a3b", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{0..12}", "0", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{0..12}", "6", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{0..12}", "12", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{3..-2}", "3", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{3..-2}", "0", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{3..-2}", "-2", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{a..g}", "a", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{a..g}", "d", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{a..g}", "g", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{g..a}", "a", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{g..a}", "d", File::FNM_EXTGLOB).should == false
+ File.send(@method, "{g..a}", "g", File::FNM_EXTGLOB).should == false
+ File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false
+ File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: ,", File::FNM_EXTGLOB).should == false
+ File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: }", File::FNM_EXTGLOB).should == false
+ File.send(@method, "escaping: {{,\\,,\\},\\{}", "escaping: {", File::FNM_EXTGLOB).should == false
+ end
+
+ it "doesn't match an extra } when File::FNM_EXTGLOB is passed" do
+ File.send(@method, 'c{at,ub}}s', 'cats', File::FNM_EXTGLOB).should == false
+ end
+
+ it "matches when both FNM_EXTGLOB and FNM_PATHNAME are passed" do
+ File.send(@method, "?.md", "a.md", File::FNM_EXTGLOB | File::FNM_PATHNAME).should == true
+ end
+
+ it "matches a single character for each ? character" do
+ File.send(@method, 'c?t', 'cat').should == true
+ File.send(@method, 'c??t', 'cat').should == false
+ end
+
+ it "matches zero or more characters for each * character" do
+ File.send(@method, 'c*', 'cats').should == true
+ File.send(@method, 'c*t', 'c/a/b/t').should == true
+ end
+
+ it "does not match unterminated range of characters" do
+ File.send(@method, 'abc[de', 'abcd').should == false
+ end
+
+ it "does not match unterminated range of characters as a literal" do
+ File.send(@method, 'abc[de', 'abc[de').should == false
+ end
+
+ it "matches ranges of characters using bracket expression (e.g. [a-z])" do
+ File.send(@method, 'ca[a-z]', 'cat').should == true
+ end
+
+ it "matches ranges of characters using bracket expression, taking case into account" do
+ File.send(@method, '[a-z]', 'D').should == false
+ File.send(@method, '[^a-z]', 'D').should == true
+ File.send(@method, '[A-Z]', 'd').should == false
+ File.send(@method, '[^A-Z]', 'd').should == true
+ File.send(@method, '[a-z]', 'D', File::FNM_CASEFOLD).should == true
+ end
+
+ it "does not match characters outside of the range of the bracket expression" do
+ File.send(@method, 'ca[x-z]', 'cat').should == false
+ File.send(@method, '/ca[s][s-t]/rul[a-b]/[z]he/[x-Z]orld', '/cats/rule/the/World').should == false
+ end
+
+ it "matches ranges of characters using exclusive bracket expression (e.g. [^t] or [!t])" do
+ File.send(@method, 'ca[^t]', 'cat').should == false
+ File.send(@method, 'ca[^t]', 'cas').should == true
+ File.send(@method, 'ca[!t]', 'cat').should == false
+ end
+
+ it "matches characters with a case sensitive comparison" do
+ File.send(@method, 'cat', 'CAT').should == false
+ end
+
+ it "matches characters with case insensitive comparison when flags includes FNM_CASEFOLD" do
+ File.send(@method, 'cat', 'CAT', File::FNM_CASEFOLD).should == true
+ end
+
+ platform_is_not :windows do
+ it "doesn't match case sensitive characters on platforms with case sensitive paths, when flags include FNM_SYSCASE" do
+ File.send(@method, 'cat', 'CAT', File::FNM_SYSCASE).should == false
+ end
+ end
+
+ platform_is :windows do
+ it "matches case sensitive characters on platforms with case insensitive paths, when flags include FNM_SYSCASE" do
+ File.send(@method, 'cat', 'CAT', File::FNM_SYSCASE).should == true
+ end
+ end
+
+ it "matches wildcard with characters when flags includes FNM_PATHNAME" do
+ File.send(@method, '*a', 'aa', File::FNM_PATHNAME).should == true
+ File.send(@method, 'a*', 'aa', File::FNM_PATHNAME).should == true
+ File.send(@method, 'a*', 'aaa', File::FNM_PATHNAME).should == true
+ File.send(@method, '*a', 'aaa', File::FNM_PATHNAME).should == true
+ end
+
+ it "does not match '/' characters with ? or * when flags includes FNM_PATHNAME" do
+ File.send(@method, '?', '/', File::FNM_PATHNAME).should == false
+ File.send(@method, '*', '/', File::FNM_PATHNAME).should == false
+ end
+
+ it "does not match '/' characters inside bracket expressions when flags includes FNM_PATHNAME" do
+ File.send(@method, '[/]', '/', File::FNM_PATHNAME).should == false
+ end
+
+ it "matches literal ? or * in path when pattern includes \\? or \\*" do
+ File.send(@method, '\?', '?').should == true
+ File.send(@method, '\?', 'a').should == false
+
+ File.send(@method, '\*', '*').should == true
+ File.send(@method, '\*', 'a').should == false
+ end
+
+ it "matches literal character (e.g. 'a') in path when pattern includes escaped character (e.g. \\a)" do
+ File.send(@method, '\a', 'a').should == true
+ File.send(@method, 'this\b', 'thisb').should == true
+ end
+
+ it "matches '\\' characters in path when flags includes FNM_NOESACPE" do
+ File.send(@method, '\a', '\a', File::FNM_NOESCAPE).should == true
+ File.send(@method, '\a', 'a', File::FNM_NOESCAPE).should == false
+ File.send(@method, '\[foo\]\[bar\]', '[foo][bar]', File::FNM_NOESCAPE).should == false
+ end
+
+ it "escapes special characters inside bracket expression" do
+ File.send(@method, '[\?]', '?').should == true
+ File.send(@method, '[\*]', '*').should == true
+ end
+
+ it "does not match leading periods in filenames with wildcards by default" do
+ File.should_not.send(@method, '*', '.profile')
+ File.should.send(@method, '*', 'home/.profile')
+ File.should.send(@method, '*/*', 'home/.profile')
+ File.should_not.send(@method, '*/*', 'dave/.profile', File::FNM_PATHNAME)
+ end
+
+ it "matches patterns with leading periods to dotfiles" do
+ File.send(@method, '.*', '.profile').should == true
+ File.send(@method, '.*', '.profile', File::FNM_PATHNAME).should == true
+ File.send(@method, ".*file", "nondotfile").should == false
+ File.send(@method, ".*file", "nondotfile", File::FNM_PATHNAME).should == false
+ end
+
+ it "does not match directories with leading periods by default with FNM_PATHNAME" do
+ File.send(@method, '.*', '.directory/nondotfile', File::FNM_PATHNAME).should == false
+ File.send(@method, '.*', '.directory/.profile', File::FNM_PATHNAME).should == false
+ File.send(@method, '.*', 'foo/.directory/nondotfile', File::FNM_PATHNAME).should == false
+ File.send(@method, '.*', 'foo/.directory/.profile', File::FNM_PATHNAME).should == false
+ File.send(@method, '**/.dotfile', '.dotsubdir/.dotfile', File::FNM_PATHNAME).should == false
+ end
+
+ it "matches leading periods in filenames when flags includes FNM_DOTMATCH" do
+ File.send(@method, '*', '.profile', File::FNM_DOTMATCH).should == true
+ File.send(@method, '*', 'home/.profile', File::FNM_DOTMATCH).should == true
+ end
+
+ it "matches multiple directories with ** and *" do
+ files = '**/*.rb'
+ File.send(@method, files, 'main.rb').should == false
+ File.send(@method, files, './main.rb').should == false
+ File.send(@method, files, 'lib/song.rb').should == true
+ File.send(@method, '**.rb', 'main.rb').should == true
+ File.send(@method, '**.rb', './main.rb').should == false
+ File.send(@method, '**.rb', 'lib/song.rb').should == true
+ File.send(@method, '*', 'dave/.profile').should == true
+ end
+
+ it "matches multiple directories with ** when flags includes File::FNM_PATHNAME" do
+ files = '**/*.rb'
+ flags = File::FNM_PATHNAME
+
+ File.send(@method, files, 'main.rb', flags).should == true
+ File.send(@method, files, 'one/two/three/main.rb', flags).should == true
+ File.send(@method, files, './main.rb', flags).should == false
+
+ flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
+
+ File.send(@method, files, './main.rb', flags).should == true
+ File.send(@method, files, 'one/two/.main.rb', flags).should == true
+
+ File.send(@method, "**/best/*", 'lib/my/best/song.rb').should == true
+ end
+
+ it "returns false if '/' in pattern do not match '/' in path when flags includes FNM_PATHNAME" do
+ pattern = '*/*'
+ File.send(@method, pattern, 'dave/.profile', File::FNM_PATHNAME).should == false
+
+ pattern = '**/foo'
+ File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME).should == false
+ end
+
+ it "returns true if '/' in pattern match '/' in path when flags includes FNM_PATHNAME" do
+ pattern = '*/*'
+ File.send(@method, pattern, 'dave/.profile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+
+ pattern = '**/foo'
+ File.send(@method, pattern, 'a/b/c/foo', File::FNM_PATHNAME).should == true
+ File.send(@method, pattern, '/a/b/c/foo', File::FNM_PATHNAME).should == true
+ File.send(@method, pattern, 'c:/a/b/c/foo', File::FNM_PATHNAME).should == true
+ File.send(@method, pattern, 'a/.b/c/foo', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ end
+
+ it "has special handling for ./ when using * and FNM_PATHNAME" do
+ File.send(@method, './*', '.', File::FNM_PATHNAME).should == false
+ File.send(@method, './*', './', File::FNM_PATHNAME).should == true
+ File.send(@method, './*/', './', File::FNM_PATHNAME).should == false
+ File.send(@method, './**', './', File::FNM_PATHNAME).should == true
+ File.send(@method, './**/', './', File::FNM_PATHNAME).should == true
+ File.send(@method, './*', '.', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false
+ File.send(@method, './*', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ File.send(@method, './*/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false
+ File.send(@method, './**', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ File.send(@method, './**/', './', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ end
+
+ it "matches **/* with FNM_PATHNAME to recurse directories" do
+ File.send(@method, 'nested/**/*', 'nested/subdir', File::FNM_PATHNAME).should == true
+ File.send(@method, 'nested/**/*', 'nested/subdir/file', File::FNM_PATHNAME).should == true
+ File.send(@method, 'nested/**/*', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ File.send(@method, 'nested/**/*', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ end
+
+ it "matches ** with FNM_PATHNAME only in current directory" do
+ File.send(@method, 'nested/**', 'nested/subdir', File::FNM_PATHNAME).should == true
+ File.send(@method, 'nested/**', 'nested/subdir/file', File::FNM_PATHNAME).should == false
+ File.send(@method, 'nested/**', 'nested/.dotsubdir', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == true
+ File.send(@method, 'nested/**', 'nested/.dotsubir/.dotfile', File::FNM_PATHNAME | File::FNM_DOTMATCH).should == false
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.send(@method, '\*', mock_to_path('a')).should == false
+ end
+
+ it "raises a TypeError if the first and second arguments are not string-like" do
+ -> { File.send(@method, nil, nil, 0, 0) }.should.raise(ArgumentError)
+ -> { File.send(@method, 1, 'some/thing') }.should.raise(TypeError)
+ -> { File.send(@method, 'some/thing', 1) }.should.raise(TypeError)
+ -> { File.send(@method, 1, 1) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the third argument is not an Integer" do
+ -> { File.send(@method, "*/place", "path/to/file", "flags") }.should.raise(TypeError)
+ -> { File.send(@method, "*/place", "path/to/file", nil) }.should.raise(TypeError)
+ end
+
+ it "does not raise a TypeError if the third argument can be coerced to an Integer" do
+ flags = mock("flags")
+ flags.should_receive(:to_int).and_return(10)
+ -> { File.send(@method, "*/place", "path/to/file", flags) }.should_not.raise
+ end
+
+ it "matches multibyte characters" do
+ File.fnmatch("*/ä/ø/ñ", "a/ä/ø/ñ").should == true
+ end
+end
diff --git a/spec/ruby/core/file/shared/open.rb b/spec/ruby/core/file/shared/open.rb
new file mode 100644
index 0000000000..67149235ca
--- /dev/null
+++ b/spec/ruby/core/file/shared/open.rb
@@ -0,0 +1,12 @@
+require_relative '../../dir/fixtures/common'
+
+describe :open_directory, shared: true do
+ it "opens directories" do
+ file = File.send(@method, tmp(""))
+ begin
+ file.should.is_a?(File)
+ ensure
+ file.close
+ end
+ end
+end
diff --git a/spec/ruby/core/file/shared/path.rb b/spec/ruby/core/file/shared/path.rb
new file mode 100644
index 0000000000..6c6f7d4234
--- /dev/null
+++ b/spec/ruby/core/file/shared/path.rb
@@ -0,0 +1,82 @@
+describe :file_path, shared: true do
+ before :each do
+ @path = tmp("file_to_path")
+ @name = File.basename(@path)
+ touch @path
+ end
+
+ after :each do
+ @file.close if @file and !@file.closed?
+ rm_r @path
+ end
+
+ it "returns a String" do
+ @file = File.new @path
+ @file.send(@method).should.instance_of?(String)
+ end
+
+ it "returns a different String on every call" do
+ @file = File.new @path
+ path1 = @file.send(@method)
+ path2 = @file.send(@method)
+ path1.should == path2
+ path1.should_not.equal?(path2)
+ end
+
+ it "returns a mutable String" do
+ @file = File.new @path.dup.freeze
+ path = @file.send(@method)
+ path.should == @path
+ path.should_not.frozen?
+ path << "test"
+ @file.send(@method).should == @path
+ end
+
+ it "calls to_str on argument and returns exact value" do
+ path = mock('path')
+ path.should_receive(:to_str).and_return(@path)
+ @file = File.new path
+ @file.send(@method).should == @path
+ end
+
+ it "does not normalise the path it returns" do
+ Dir.chdir(tmp("")) do
+ unorm = "./#{@name}"
+ @file = File.new unorm
+ @file.send(@method).should == unorm
+ end
+ end
+
+ it "does not canonicalize the path it returns" do
+ dir = File.basename tmp("")
+ path = "#{tmp("")}../#{dir}/#{@name}"
+ @file = File.new path
+ @file.send(@method).should == path
+ end
+
+ it "does not absolute-ise the path it returns" do
+ Dir.chdir(tmp("")) do
+ @file = File.new @name
+ @file.send(@method).should == @name
+ end
+ end
+
+ it "preserves the encoding of the path" do
+ path = @path.force_encoding("euc-jp")
+ @file = File.new path
+ @file.send(@method).encoding.should == Encoding.find("euc-jp")
+ end
+
+ platform_is :linux do
+ guard -> { defined?(File::TMPFILE) } do
+ before :each do
+ @dir = tmp("tmpfilespec")
+ mkdir_p @dir
+ end
+
+ after :each do
+ rm_r @dir
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/shared/read.rb b/spec/ruby/core/file/shared/read.rb
new file mode 100644
index 0000000000..f60800bb2f
--- /dev/null
+++ b/spec/ruby/core/file/shared/read.rb
@@ -0,0 +1,15 @@
+require_relative '../../dir/fixtures/common'
+
+describe :file_read_directory, shared: true do
+ platform_is :darwin, :linux, :freebsd, :openbsd, :windows do
+ it "raises an Errno::EISDIR when passed a path that is a directory" do
+ -> { @object.send(@method, ".") }.should.raise(Errno::EISDIR)
+ end
+ end
+
+ platform_is :netbsd do
+ it "does not raises any exception when passed a path that is a directory" do
+ -> { @object.send(@method, ".") }.should_not.raise
+ end
+ end
+end
diff --git a/spec/ruby/core/file/shared/stat.rb b/spec/ruby/core/file/shared/stat.rb
new file mode 100644
index 0000000000..879a7f11ff
--- /dev/null
+++ b/spec/ruby/core/file/shared/stat.rb
@@ -0,0 +1,32 @@
+describe :file_stat, shared: true do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file)
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns a File::Stat object if the given file exists" do
+ st = File.send(@method, @file)
+ st.should.instance_of?(File::Stat)
+ end
+
+ it "returns a File::Stat object when called on an instance of File" do
+ File.open(@file) do |f|
+ st = f.send(@method)
+ st.should.instance_of?(File::Stat)
+ end
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.send(@method, mock_to_path(@file))
+ end
+
+ it "raises an Errno::ENOENT if the file does not exist" do
+ -> {
+ File.send(@method, "fake_file")
+ }.should.raise(Errno::ENOENT)
+ end
+end
diff --git a/spec/ruby/core/file/shared/unlink.rb b/spec/ruby/core/file/shared/unlink.rb
new file mode 100644
index 0000000000..0032907ba2
--- /dev/null
+++ b/spec/ruby/core/file/shared/unlink.rb
@@ -0,0 +1,61 @@
+describe :file_unlink, shared: true do
+ before :each do
+ @file1 = tmp('test.txt')
+ @file2 = tmp('test2.txt')
+
+ touch @file1
+ touch @file2
+ end
+
+ after :each do
+ File.send(@method, @file1) if File.exist?(@file1)
+ File.send(@method, @file2) if File.exist?(@file2)
+
+ @file1 = nil
+ @file2 = nil
+ end
+
+ it "returns 0 when called without arguments" do
+ File.send(@method).should == 0
+ end
+
+ it "deletes a single file" do
+ File.send(@method, @file1).should == 1
+ File.should_not.exist?(@file1)
+ end
+
+ it "deletes multiple files" do
+ File.send(@method, @file1, @file2).should == 2
+ File.should_not.exist?(@file1)
+ File.should_not.exist?(@file2)
+ end
+
+ it "raises a TypeError if not passed a String type" do
+ -> { File.send(@method, 1) }.should.raise(TypeError)
+ end
+
+ it "raises an Errno::ENOENT when the given file doesn't exist" do
+ -> { File.send(@method, 'bogus') }.should.raise(Errno::ENOENT)
+ end
+
+ it "coerces a given parameter into a string if possible" do
+ mock = mock("to_str")
+ mock.should_receive(:to_str).and_return(@file1)
+ File.send(@method, mock).should == 1
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.send(@method, mock_to_path(@file1)).should == 1
+ end
+
+ platform_is :windows do
+ it "allows deleting an open file with File::SHARE_DELETE" do
+ path = tmp("share_delete.txt")
+ File.open(path, mode: File::CREAT | File::WRONLY | File::BINARY | File::SHARE_DELETE) do |f|
+ File.should.exist?(path)
+ File.send(@method, path)
+ end
+ File.should_not.exist?(path)
+ end
+ end
+end
diff --git a/spec/ruby/core/file/shared/update_time.rb b/spec/ruby/core/file/shared/update_time.rb
new file mode 100644
index 0000000000..3fe7266a00
--- /dev/null
+++ b/spec/ruby/core/file/shared/update_time.rb
@@ -0,0 +1,105 @@
+describe :update_time, shared: true do
+ before :all do
+ @time_is_float = platform_is :windows
+ end
+
+ before :each do
+ @atime = Time.now
+ @mtime = Time.now
+ @file1 = tmp("specs_file_utime1")
+ @file2 = tmp("specs_file_utime2")
+ touch @file1
+ touch @file2
+ end
+
+ after :each do
+ rm_r @file1, @file2
+ end
+
+ it "sets the access and modification time of each file" do
+ File.send(@method, @atime, @mtime, @file1, @file2)
+
+ if @time_is_float
+ File.atime(@file1).should be_close(@atime, 0.0001)
+ File.mtime(@file1).should be_close(@mtime, 0.0001)
+ File.atime(@file2).should be_close(@atime, 0.0001)
+ File.mtime(@file2).should be_close(@mtime, 0.0001)
+ else
+ File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
+ File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
+ File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
+ File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
+ end
+ end
+
+ it "uses the current times if two nil values are passed" do
+ tn = Time.now
+ File.send(@method, nil, nil, @file1, @file2)
+
+ if @time_is_float
+ File.atime(@file1).should be_close(tn, 0.050)
+ File.mtime(@file1).should be_close(tn, 0.050)
+ File.atime(@file2).should be_close(tn, 0.050)
+ File.mtime(@file2).should be_close(tn, 0.050)
+ else
+ File.atime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
+ File.mtime(@file1).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
+ File.atime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
+ File.mtime(@file2).to_i.should be_close(Time.now.to_i, TIME_TOLERANCE)
+ end
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.send(@method, @atime, @mtime, mock_to_path(@file1), mock_to_path(@file2))
+ end
+
+ it "accepts numeric atime and mtime arguments" do
+ if @time_is_float
+ File.send(@method, @atime.to_f, @mtime.to_f, @file1, @file2)
+
+ File.atime(@file1).should be_close(@atime, 0.0001)
+ File.mtime(@file1).should be_close(@mtime, 0.0001)
+ File.atime(@file2).should be_close(@atime, 0.0001)
+ File.mtime(@file2).should be_close(@mtime, 0.0001)
+ else
+ File.send(@method, @atime.to_i, @mtime.to_i, @file1, @file2)
+
+ File.atime(@file1).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
+ File.mtime(@file1).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
+ File.atime(@file2).to_i.should be_close(@atime.to_i, TIME_TOLERANCE)
+ File.mtime(@file2).to_i.should be_close(@mtime.to_i, TIME_TOLERANCE)
+ end
+ end
+
+ it "may set nanosecond precision" do
+ t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r)
+ File.send(@method, t, t, @file1)
+
+ File.atime(@file1).nsec.should.between?(0, 123500000)
+ File.mtime(@file1).nsec.should.between?(0, 123500000)
+ end
+
+ it "returns the number of filenames in the arguments" do
+ File.send(@method, @atime.to_f, @mtime.to_f, @file1, @file2).should == 2
+ end
+
+ platform_is :linux do
+ platform_is pointer_size: 64 do
+ it "allows Time instances in the far future to set mtime and atime (but some filesystems limit it up to 2446-05-10 or 2038-01-19 or 2486-07-02)" do
+ # https://ext4.wiki.kernel.org/index.php/Ext4_Disk_Layout#Inode_Timestamps
+ # "Therefore, timestamps should not overflow until May 2446."
+ # https://lwn.net/Articles/804382/
+ # "On-disk timestamps hitting the y2038 limit..."
+ # The problem seems to be being improved, but currently it actually fails on XFS on RHEL8
+ # https://rubyci.org/logs/rubyci.s3.amazonaws.com/rhel8/ruby-master/log/20201112T123004Z.fail.html.gz
+ # Amazon Linux 2023 returns 2486-07-02 in this example
+ # http://rubyci.s3.amazonaws.com/amazon2023/ruby-master/log/20230322T063004Z.fail.html.gz
+ time = Time.at(1<<44)
+ File.send(@method, time, time, @file1)
+
+ [559444, 2486, 2446, 2038].should.include? File.atime(@file1).year
+ [559444, 2486, 2446, 2038].should.include? File.mtime(@file1).year
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/size_spec.rb b/spec/ruby/core/file/size_spec.rb
new file mode 100644
index 0000000000..784c18c26e
--- /dev/null
+++ b/spec/ruby/core/file/size_spec.rb
@@ -0,0 +1,119 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/size'
+
+describe "File.size?" do
+ it_behaves_like :file_size, :size?, File
+end
+
+describe "File.size?" do
+ it_behaves_like :file_size_to_io, :size?, File
+end
+
+describe "File.size?" do
+ it_behaves_like :file_size_nil_when_missing, :size?, File
+end
+
+describe "File.size?" do
+ it_behaves_like :file_size_nil_when_empty, :size?, File
+end
+
+describe "File.size?" do
+ it_behaves_like :file_size_with_file_argument, :size?, File
+end
+
+describe "File.size" do
+ it_behaves_like :file_size, :size, File
+end
+
+describe "File.size" do
+ it_behaves_like :file_size_to_io, :size, File
+end
+
+describe "File.size" do
+ it_behaves_like :file_size_raise_when_missing, :size, File
+end
+
+describe "File.size" do
+ it_behaves_like :file_size_0_when_empty, :size, File
+end
+
+describe "File.size" do
+ it_behaves_like :file_size_with_file_argument, :size, File
+end
+
+describe "File#size" do
+
+ before :each do
+ @name = tmp('i_exist')
+ touch(@name) { |f| f.write 'rubinius' }
+ @file = File.new @name
+ @file_org = @file
+ end
+
+ after :each do
+ @file_org.close unless @file_org.closed?
+ rm_r @name
+ end
+
+ it "is an instance method" do
+ @file.respond_to?(:size).should == true
+ end
+
+ it "returns the file's size as an Integer" do
+ @file.size.should.instance_of?(Integer)
+ end
+
+ it "returns the file's size in bytes" do
+ @file.size.should == 8
+ end
+
+ platform_is_not :windows do # impossible to remove opened file on Windows
+ it "returns the cached size of the file if subsequently deleted" do
+ rm_r @file.path
+ @file.size.should == 8
+ end
+ end
+
+ it "returns the file's current size even if modified" do
+ File.open(@file.path,'a') {|f| f.write '!'}
+ @file.size.should == 9
+ end
+
+ it "raises an IOError on a closed file" do
+ @file.close
+ -> { @file.size }.should.raise(IOError)
+ end
+
+ platform_is_not :windows do
+ it "follows symlinks if necessary" do
+ ln_file = tmp('i_exist_ln')
+ rm_r ln_file
+
+ begin
+ File.symlink(@file.path, ln_file).should == 0
+ file = File.new(ln_file)
+ file.size.should == 8
+ ensure
+ file.close if file && !file.closed?
+ rm_r ln_file
+ end
+ end
+ end
+end
+
+describe "File#size for an empty file" do
+ before :each do
+ @name = tmp('empty')
+ touch(@name)
+ @file = File.new @name
+ end
+
+ after :each do
+ @file.close unless @file.closed?
+ rm_r @name
+ end
+
+ it "returns 0" do
+ @file.size.should == 0
+ end
+end
diff --git a/spec/ruby/core/file/socket_spec.rb b/spec/ruby/core/file/socket_spec.rb
new file mode 100644
index 0000000000..d3f4eb013a
--- /dev/null
+++ b/spec/ruby/core/file/socket_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/socket'
+
+describe "File.socket?" do
+ it_behaves_like :file_socket, :socket?, File
+
+ it "returns false if file does not exist" do
+ File.socket?("I_am_a_bogus_file").should == false
+ end
+end
diff --git a/spec/ruby/core/file/split_spec.rb b/spec/ruby/core/file/split_spec.rb
new file mode 100644
index 0000000000..e989a6b86e
--- /dev/null
+++ b/spec/ruby/core/file/split_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+
+describe "File.split" do
+ before :each do
+ @backslash_ext = "C:\\foo\\bar\\baz.rb"
+ @backslash = "C:\\foo\\bar\\baz"
+ end
+
+ it "splits the string at the last '/' when the last component does not have an extension" do
+ File.split("/foo/bar/baz").should == ["/foo/bar", "baz"]
+ File.split("C:/foo/bar/baz").should == ["C:/foo/bar", "baz"]
+ end
+
+ it "splits the string at the last '/' when the last component has an extension" do
+ File.split("/foo/bar/baz.rb").should == ["/foo/bar", "baz.rb"]
+ File.split("C:/foo/bar/baz.rb").should == ["C:/foo/bar", "baz.rb"]
+ end
+
+ it "splits an empty string into a '.' and an empty string" do
+ File.split("").should == [".", ""]
+ end
+
+ platform_is_not :windows do
+ it "collapses multiple '/' characters and strips trailing ones" do
+ File.split("//foo////").should == ["/", "foo"]
+ end
+ end
+
+ platform_is_not :windows do
+ it "does not split a string that contains '\\'" do
+ File.split(@backslash).should == [".", "C:\\foo\\bar\\baz"]
+ File.split(@backslash_ext).should == [".", "C:\\foo\\bar\\baz.rb"]
+ end
+ end
+
+ platform_is :windows do
+ it "splits the string at the last '\\' when the last component does not have an extension" do
+ File.split(@backslash).should == ["C:\\foo\\bar", "baz"]
+ end
+
+ it "splits the string at the last '\\' when the last component has an extension" do
+ File.split(@backslash_ext).should == ["C:\\foo\\bar", "baz.rb"]
+ end
+ end
+
+ it "raises an ArgumentError when not passed a single argument" do
+ -> { File.split }.should.raise(ArgumentError)
+ -> { File.split('string', 'another string') }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if the argument is not a String type" do
+ -> { File.split(1) }.should.raise(TypeError)
+ end
+
+ it "coerces the argument with to_str if it is not a String type" do
+ obj = mock("str")
+ obj.should_receive(:to_str).and_return("/one/two/three")
+ File.split(obj).should == ["/one/two", "three"]
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.split(mock_to_path("")).should == [".", ""]
+ end
+end
diff --git a/spec/ruby/core/file/stat/atime_spec.rb b/spec/ruby/core/file/stat/atime_spec.rb
new file mode 100644
index 0000000000..2fecaed300
--- /dev/null
+++ b/spec/ruby/core/file/stat/atime_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#atime" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the atime of a File::Stat object" do
+ st = File.stat(@file)
+ st.atime.should.is_a?(Time)
+ st.atime.should <= Time.now
+ end
+end
diff --git a/spec/ruby/core/file/stat/birthtime_spec.rb b/spec/ruby/core/file/stat/birthtime_spec.rb
new file mode 100644
index 0000000000..728f635397
--- /dev/null
+++ b/spec/ruby/core/file/stat/birthtime_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../../spec_helper'
+
+platform_is(:windows, :darwin, :freebsd, :netbsd,
+ *ruby_version_is("4.0") { :linux },
+ ) do
+ not_implemented_messages = [
+ "birthtime() function is unimplemented", # unsupported OS/version
+ "birthtime is unimplemented", # unsupported filesystem
+ ]
+
+ describe "File::Stat#birthtime" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the birthtime of a File::Stat object" do
+ st = File.stat(@file)
+ st.birthtime.should.is_a?(Time)
+ st.birthtime.should <= Time.now
+ rescue NotImplementedError => e
+ e.message.should.start_with?(*not_implemented_messages)
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/blksize_spec.rb b/spec/ruby/core/file/stat/blksize_spec.rb
new file mode 100644
index 0000000000..4d85b05e4d
--- /dev/null
+++ b/spec/ruby/core/file/stat/blksize_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#blksize" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ platform_is_not :windows do
+ it "returns the blksize of a File::Stat object" do
+ st = File.stat(@file)
+ st.blksize.is_a?(Integer).should == true
+ st.blksize.should > 0
+ end
+ end
+
+ platform_is :windows do
+ it "returns nil" do
+ st = File.stat(@file)
+ st.blksize.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/blockdev_spec.rb b/spec/ruby/core/file/stat/blockdev_spec.rb
new file mode 100644
index 0000000000..f986c18125
--- /dev/null
+++ b/spec/ruby/core/file/stat/blockdev_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/blockdev'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#blockdev?" do
+ it_behaves_like :file_blockdev, :blockdev?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/blocks_spec.rb b/spec/ruby/core/file/stat/blocks_spec.rb
new file mode 100644
index 0000000000..5e0efc8bc2
--- /dev/null
+++ b/spec/ruby/core/file/stat/blocks_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#blocks" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ platform_is_not :windows do
+ it "returns a non-negative integer" do
+ st = File.stat(@file)
+ st.blocks.is_a?(Integer).should == true
+ st.blocks.should >= 0
+ end
+ end
+
+ platform_is :windows do
+ it "returns nil" do
+ st = File.stat(@file)
+ st.blocks.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/chardev_spec.rb b/spec/ruby/core/file/stat/chardev_spec.rb
new file mode 100644
index 0000000000..622fb2052d
--- /dev/null
+++ b/spec/ruby/core/file/stat/chardev_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/chardev'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#chardev?" do
+ it_behaves_like :file_chardev, :chardev?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/comparison_spec.rb b/spec/ruby/core/file/stat/comparison_spec.rb
new file mode 100644
index 0000000000..faa3b6bf62
--- /dev/null
+++ b/spec/ruby/core/file/stat/comparison_spec.rb
@@ -0,0 +1,66 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#<=>" do
+ before :each do
+ @name1 = tmp("i_exist")
+ @name2 = tmp("i_exist_too")
+ touch @name1
+ touch @name2
+ end
+
+ after :each do
+ rm_r @name1, @name2
+ end
+
+ it "is able to compare files by the same modification times" do
+ now = Time.now - 1 # 1 second ago to avoid NFS cache issue
+ File.utime(now, now, @name1)
+ File.utime(now, now, @name2)
+
+ File.open(@name1) { |file1|
+ File.open(@name2) { |file2|
+ (file1.stat <=> file2.stat).should == 0
+ }
+ }
+ end
+
+ it "is able to compare files by different modification times" do
+ now = Time.now
+ File.utime(now, now + 100, @name2)
+
+ File.open(@name1) { |file1|
+ File.open(@name2) { |file2|
+ (file1.stat <=> file2.stat).should == -1
+ }
+ }
+
+ File.utime(now, now - 100, @name2)
+
+ File.open(@name1) { |file1|
+ File.open(@name2) { |file2|
+ (file1.stat <=> file2.stat).should == 1
+ }
+ }
+ end
+
+ # TODO: Fix
+ it "includes Comparable and #== shows mtime equality between two File::Stat objects" do
+ File.open(@name1) { |file1|
+ File.open(@name2) { |file2|
+ (file1.stat == file1.stat).should == true
+ (file2.stat == file2.stat).should == true
+ }
+ }
+
+ now = Time.now
+ File.utime(now, now + 100, @name2)
+
+ File.open(@name1) { |file1|
+ File.open(@name2) { |file2|
+ (file1.stat == file2.stat).should == false
+ (file1.stat == file1.stat).should == true
+ (file2.stat == file2.stat).should == true
+ }
+ }
+ end
+end
diff --git a/spec/ruby/core/file/stat/ctime_spec.rb b/spec/ruby/core/file/stat/ctime_spec.rb
new file mode 100644
index 0000000000..dbf43a7453
--- /dev/null
+++ b/spec/ruby/core/file/stat/ctime_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#ctime" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the ctime of a File::Stat object" do
+ st = File.stat(@file)
+ st.ctime.should.is_a?(Time)
+ st.ctime.should <= Time.now
+ end
+end
diff --git a/spec/ruby/core/file/stat/dev_major_spec.rb b/spec/ruby/core/file/stat/dev_major_spec.rb
new file mode 100644
index 0000000000..a199eaaa11
--- /dev/null
+++ b/spec/ruby/core/file/stat/dev_major_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#dev_major" do
+ before :each do
+ @name = tmp("file.txt")
+ touch(@name)
+ end
+ after :each do
+ rm_r @name
+ end
+
+ platform_is_not :windows do
+ it "returns the major part of File::Stat#dev" do
+ File.stat(@name).dev_major.should.is_a?(Integer)
+ end
+ end
+
+ platform_is :windows do
+ it "returns nil" do
+ File.stat(@name).dev_major.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/dev_minor_spec.rb b/spec/ruby/core/file/stat/dev_minor_spec.rb
new file mode 100644
index 0000000000..8ce94778ca
--- /dev/null
+++ b/spec/ruby/core/file/stat/dev_minor_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#dev_minor" do
+ before :each do
+ @name = tmp("file.txt")
+ touch(@name)
+ end
+ after :each do
+ rm_r @name
+ end
+
+ platform_is_not :windows do
+ it "returns the minor part of File::Stat#dev" do
+ File.stat(@name).dev_minor.should.is_a?(Integer)
+ end
+ end
+
+ platform_is :windows do
+ it "returns nil" do
+ File.stat(@name).dev_minor.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/dev_spec.rb b/spec/ruby/core/file/stat/dev_spec.rb
new file mode 100644
index 0000000000..cd5e3d175e
--- /dev/null
+++ b/spec/ruby/core/file/stat/dev_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#dev" do
+ before :each do
+ @name = tmp("file.txt")
+ touch(@name)
+ end
+ after :each do
+ rm_r @name
+ end
+
+ it "returns the number of the device on which the file exists" do
+ File.stat(@name).dev.should.is_a?(Integer)
+ end
+end
diff --git a/spec/ruby/core/file/stat/directory_spec.rb b/spec/ruby/core/file/stat/directory_spec.rb
new file mode 100644
index 0000000000..c03610388b
--- /dev/null
+++ b/spec/ruby/core/file/stat/directory_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/directory'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#directory?" do
+ it_behaves_like :file_directory, :directory?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/executable_real_spec.rb b/spec/ruby/core/file/stat/executable_real_spec.rb
new file mode 100644
index 0000000000..23bffe89c5
--- /dev/null
+++ b/spec/ruby/core/file/stat/executable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/executable_real'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#executable_real?" do
+ it_behaves_like :file_executable_real, :executable_real?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/executable_spec.rb b/spec/ruby/core/file/stat/executable_spec.rb
new file mode 100644
index 0000000000..422975d14b
--- /dev/null
+++ b/spec/ruby/core/file/stat/executable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/executable'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#executable?" do
+ it_behaves_like :file_executable, :executable?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/file_spec.rb b/spec/ruby/core/file/stat/file_spec.rb
new file mode 100644
index 0000000000..d141536b4b
--- /dev/null
+++ b/spec/ruby/core/file/stat/file_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/file'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#file?" do
+ it_behaves_like :file_file, :file?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/fixtures/classes.rb b/spec/ruby/core/file/stat/fixtures/classes.rb
new file mode 100644
index 0000000000..4fe9a2a30f
--- /dev/null
+++ b/spec/ruby/core/file/stat/fixtures/classes.rb
@@ -0,0 +1,5 @@
+class FileStat
+ def self.method_missing(meth, file)
+ File.lstat(file).send(meth)
+ end
+end
diff --git a/spec/ruby/core/file/stat/ftype_spec.rb b/spec/ruby/core/file/stat/ftype_spec.rb
new file mode 100644
index 0000000000..df2e3ada1e
--- /dev/null
+++ b/spec/ruby/core/file/stat/ftype_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/file_types'
+
+describe "File::Stat#ftype" do
+ before :all do
+ FileSpecs.configure_types
+ end
+
+ it "returns a String" do
+ FileSpecs.normal_file do |file|
+ File.lstat(file).ftype.should.is_a?(String)
+ end
+ end
+
+ it "returns 'file' when the file is a file" do
+ FileSpecs.normal_file do |file|
+ File.lstat(file).ftype.should == 'file'
+ end
+ end
+
+ it "returns 'directory' when the file is a dir" do
+ FileSpecs.directory do |dir|
+ File.lstat(dir).ftype.should == 'directory'
+ end
+ end
+
+ platform_is_not :windows do
+ it "returns 'characterSpecial' when the file is a char" do
+ FileSpecs.character_device do |char|
+ File.lstat(char).ftype.should == 'characterSpecial'
+ end
+ end
+ end
+
+ platform_is_not :freebsd do # FreeBSD does not have block devices
+ with_block_device do
+ it "returns 'blockSpecial' when the file is a block" do
+ FileSpecs.block_device do |block|
+ File.lstat(block).ftype.should == 'blockSpecial'
+ end
+ end
+ end
+ end
+
+ platform_is_not :windows do
+ it "returns 'link' when the file is a link" do
+ FileSpecs.symlink do |link|
+ File.lstat(link).ftype.should == 'link'
+ end
+ end
+
+ it "returns fifo when the file is a fifo" do
+ FileSpecs.fifo do |fifo|
+ File.lstat(fifo).ftype.should == 'fifo'
+ end
+ end
+
+ it "returns 'socket' when the file is a socket" do
+ FileSpecs.socket do |socket|
+ File.lstat(socket).ftype.should == 'socket'
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/gid_spec.rb b/spec/ruby/core/file/stat/gid_spec.rb
new file mode 100644
index 0000000000..3bba65bc82
--- /dev/null
+++ b/spec/ruby/core/file/stat/gid_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#gid" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ File.chown(nil, Process.gid, @file)
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the group owner attribute of a File::Stat object" do
+ st = File.stat(@file)
+ st.gid.is_a?(Integer).should == true
+ st.gid.should == Process.gid
+ end
+end
diff --git a/spec/ruby/core/file/stat/grpowned_spec.rb b/spec/ruby/core/file/stat/grpowned_spec.rb
new file mode 100644
index 0000000000..e7278e229b
--- /dev/null
+++ b/spec/ruby/core/file/stat/grpowned_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/grpowned'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#grpowned?" do
+ it_behaves_like :file_grpowned, :grpowned?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/ino_spec.rb b/spec/ruby/core/file/stat/ino_spec.rb
new file mode 100644
index 0000000000..c09b6448c7
--- /dev/null
+++ b/spec/ruby/core/file/stat/ino_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#ino" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ platform_is_not :windows do
+ it "returns the ino of a File::Stat object" do
+ st = File.stat(@file)
+ st.ino.should.is_a?(Integer)
+ st.ino.should > 0
+ end
+ end
+
+ platform_is :windows do
+ it "returns BY_HANDLE_FILE_INFORMATION.nFileIndexHigh/Low of a File::Stat object" do
+ st = File.stat(@file)
+ st.ino.should.is_a?(Integer)
+ st.ino.should > 0
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/inspect_spec.rb b/spec/ruby/core/file/stat/inspect_spec.rb
new file mode 100644
index 0000000000..1613b427d0
--- /dev/null
+++ b/spec/ruby/core/file/stat/inspect_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#inspect" do
+
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "produces a nicely formatted description of a File::Stat object" do
+ st = File.stat(@file)
+ expected = "#<File::Stat dev=0x#{st.dev.to_s(16)}, ino=#{st.ino}, mode=#{sprintf("%07o", st.mode)}, nlink=#{st.nlink}"
+ expected << ", uid=#{st.uid}, gid=#{st.gid}, rdev=0x#{st.rdev.to_s(16)}, size=#{st.size}, blksize=#{st.blksize.inspect}"
+ expected << ", blocks=#{st.blocks.inspect}, atime=#{st.atime.inspect}, mtime=#{st.mtime.inspect}, ctime=#{st.ctime.inspect}"
+ platform_is :netbsd, :freebsd, :darwin do
+ # Windows has File.birthtime but it's not here since already shown by ctime.
+ expected << ", birthtime=#{st.birthtime.inspect}"
+ end
+ expected << ">"
+ st.inspect.should == expected
+ end
+end
diff --git a/spec/ruby/core/file/stat/mode_spec.rb b/spec/ruby/core/file/stat/mode_spec.rb
new file mode 100644
index 0000000000..c85fb85a58
--- /dev/null
+++ b/spec/ruby/core/file/stat/mode_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#mode" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ File.chmod(0644, @file)
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the mode of a File::Stat object" do
+ st = File.stat(@file)
+ st.mode.is_a?(Integer).should == true
+ (st.mode & 0777).should == 0644
+ end
+end
diff --git a/spec/ruby/core/file/stat/mtime_spec.rb b/spec/ruby/core/file/stat/mtime_spec.rb
new file mode 100644
index 0000000000..7844491212
--- /dev/null
+++ b/spec/ruby/core/file/stat/mtime_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#mtime" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the mtime of a File::Stat object" do
+ st = File.stat(@file)
+ st.mtime.should.is_a?(Time)
+ st.mtime.should <= Time.now
+ end
+end
diff --git a/spec/ruby/core/file/stat/new_spec.rb b/spec/ruby/core/file/stat/new_spec.rb
new file mode 100644
index 0000000000..b8c3600028
--- /dev/null
+++ b/spec/ruby/core/file/stat/new_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#initialize" do
+
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ File.chmod(0755, @file)
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "raises an exception if the file doesn't exist" do
+ -> {
+ File::Stat.new(tmp("i_am_a_dummy_file_that_doesnt_exist"))
+ }.should.raise(Errno::ENOENT)
+ end
+
+ it "creates a File::Stat object for the given file" do
+ st = File::Stat.new(@file)
+ st.should.is_a?(File::Stat)
+ st.ftype.should == 'file'
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return @file
+ File::Stat.new p
+ end
+end
diff --git a/spec/ruby/core/file/stat/nlink_spec.rb b/spec/ruby/core/file/stat/nlink_spec.rb
new file mode 100644
index 0000000000..7143923cfc
--- /dev/null
+++ b/spec/ruby/core/file/stat/nlink_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#nlink" do
+ before :each do
+ @file = tmp("stat_nlink")
+ @link = @file + ".lnk"
+ touch @file
+ end
+
+ after :each do
+ rm_r @link, @file
+ end
+
+ platform_is_not :windows, :android do
+ it "returns the number of links to a file" do
+ File::Stat.new(@file).nlink.should == 1
+ File.link(@file, @link)
+ File::Stat.new(@file).nlink.should == 2
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/owned_spec.rb b/spec/ruby/core/file/stat/owned_spec.rb
new file mode 100644
index 0000000000..a23ad850c5
--- /dev/null
+++ b/spec/ruby/core/file/stat/owned_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/owned'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#owned?" do
+ it_behaves_like :file_owned, :owned?, FileStat
+end
+
+describe "File::Stat#owned?" do
+ before :each do
+ @file = tmp("i_exist")
+ touch(@file)
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns true if the file is owned by the user" do
+ st = File.stat(@file)
+ st.should.owned?
+ end
+
+ platform_is_not :windows, :android do
+ as_user do
+ it "returns false if the file is not owned by the user" do
+ system_file = '/etc/passwd'
+ st = File.stat(system_file)
+ st.should_not.owned?
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/pipe_spec.rb b/spec/ruby/core/file/stat/pipe_spec.rb
new file mode 100644
index 0000000000..692dfbf42a
--- /dev/null
+++ b/spec/ruby/core/file/stat/pipe_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/pipe'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#pipe?" do
+ it_behaves_like :file_pipe, :pipe?, FileStat
+end
+
+describe "File::Stat#pipe?" do
+ it "returns false if the file is not a pipe" do
+ filename = tmp("i_exist")
+ touch(filename)
+
+ st = File.stat(filename)
+ st.should_not.pipe?
+
+ rm_r filename
+ end
+
+ platform_is_not :windows do
+ it "returns true if the file is a pipe" do
+ filename = tmp("i_am_a_pipe")
+ File.mkfifo(filename)
+
+ st = File.stat(filename)
+ st.should.pipe?
+
+ rm_r filename
+ end
+ end
+
+end
diff --git a/spec/ruby/core/file/stat/rdev_major_spec.rb b/spec/ruby/core/file/stat/rdev_major_spec.rb
new file mode 100644
index 0000000000..e1b44fc2d0
--- /dev/null
+++ b/spec/ruby/core/file/stat/rdev_major_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#rdev_major" do
+ before :each do
+ @name = tmp("file.txt")
+ touch(@name)
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ platform_is_not :windows do
+ it "returns the major part of File::Stat#rdev" do
+ File.stat(@name).rdev_major.should.is_a?(Integer)
+ end
+ end
+
+ platform_is :windows do
+ it "returns nil" do
+ File.stat(@name).rdev_major.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/rdev_minor_spec.rb b/spec/ruby/core/file/stat/rdev_minor_spec.rb
new file mode 100644
index 0000000000..8af3b9f587
--- /dev/null
+++ b/spec/ruby/core/file/stat/rdev_minor_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#rdev_minor" do
+ before :each do
+ @name = tmp("file.txt")
+ touch(@name)
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ platform_is_not :windows do
+ it "returns the minor part of File::Stat#rdev" do
+ File.stat(@name).rdev_minor.should.is_a?(Integer)
+ end
+ end
+
+ platform_is :windows do
+ it "returns nil" do
+ File.stat(@name).rdev_minor.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/file/stat/rdev_spec.rb b/spec/ruby/core/file/stat/rdev_spec.rb
new file mode 100644
index 0000000000..7e4252fcc6
--- /dev/null
+++ b/spec/ruby/core/file/stat/rdev_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#rdev" do
+ before :each do
+ @name = tmp("file.txt")
+ touch(@name)
+ end
+ after :each do
+ rm_r @name
+ end
+
+ it "returns the number of the device this file represents which the file exists" do
+ File.stat(@name).rdev.should.is_a?(Integer)
+ end
+end
diff --git a/spec/ruby/core/file/stat/readable_real_spec.rb b/spec/ruby/core/file/stat/readable_real_spec.rb
new file mode 100644
index 0000000000..f138fd7b00
--- /dev/null
+++ b/spec/ruby/core/file/stat/readable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/readable_real'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#readable_real?" do
+ it_behaves_like :file_readable_real, :readable_real?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/readable_spec.rb b/spec/ruby/core/file/stat/readable_spec.rb
new file mode 100644
index 0000000000..e99e48feed
--- /dev/null
+++ b/spec/ruby/core/file/stat/readable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/readable'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#readable?" do
+ it_behaves_like :file_readable, :readable?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/setgid_spec.rb b/spec/ruby/core/file/stat/setgid_spec.rb
new file mode 100644
index 0000000000..c0748ede57
--- /dev/null
+++ b/spec/ruby/core/file/stat/setgid_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/setgid'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#setgid?" do
+ it_behaves_like :file_setgid, :setgid?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/setuid_spec.rb b/spec/ruby/core/file/stat/setuid_spec.rb
new file mode 100644
index 0000000000..6408120fc4
--- /dev/null
+++ b/spec/ruby/core/file/stat/setuid_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/setuid'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#setuid?" do
+ it_behaves_like :file_setuid, :setuid?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/size_spec.rb b/spec/ruby/core/file/stat/size_spec.rb
new file mode 100644
index 0000000000..4b4f57f8c8
--- /dev/null
+++ b/spec/ruby/core/file/stat/size_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/size'
+require_relative 'fixtures/classes'
+
+describe "File::Stat.size?" do
+ it_behaves_like :file_size, :size?, FileStat
+ it_behaves_like :file_size_nil_when_empty, :size?, FileStat
+end
+
+describe "File::Stat.size" do
+ it_behaves_like :file_size, :size, FileStat
+ it_behaves_like :file_size_0_when_empty, :size, FileStat
+end
+
+describe "File::Stat#size" do
+ it "needs to be reviewed for spec completeness"
+end
+
+describe "File::Stat#size?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/file/stat/socket_spec.rb b/spec/ruby/core/file/stat/socket_spec.rb
new file mode 100644
index 0000000000..09740be110
--- /dev/null
+++ b/spec/ruby/core/file/stat/socket_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/socket'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#socket?" do
+ it_behaves_like :file_socket, :socket?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/sticky_spec.rb b/spec/ruby/core/file/stat/sticky_spec.rb
new file mode 100644
index 0000000000..7083e644e9
--- /dev/null
+++ b/spec/ruby/core/file/stat/sticky_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/sticky'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#sticky?" do
+ it_behaves_like :file_sticky, :sticky?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/symlink_spec.rb b/spec/ruby/core/file/stat/symlink_spec.rb
new file mode 100644
index 0000000000..0def832a4c
--- /dev/null
+++ b/spec/ruby/core/file/stat/symlink_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/symlink'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#symlink?" do
+ it_behaves_like :file_symlink, :symlink?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/uid_spec.rb b/spec/ruby/core/file/stat/uid_spec.rb
new file mode 100644
index 0000000000..b97147db21
--- /dev/null
+++ b/spec/ruby/core/file/stat/uid_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe "File::Stat#uid" do
+ before :each do
+ @file = tmp('i_exist')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @file
+ end
+
+ it "returns the owner attribute of a File::Stat object" do
+ st = File.stat(@file)
+ st.uid.is_a?(Integer).should == true
+ st.uid.should == Process.uid
+ end
+end
diff --git a/spec/ruby/core/file/stat/world_readable_spec.rb b/spec/ruby/core/file/stat/world_readable_spec.rb
new file mode 100644
index 0000000000..d94a02205e
--- /dev/null
+++ b/spec/ruby/core/file/stat/world_readable_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/world_readable'
+require_relative 'fixtures/classes'
+
+describe "File::Stat.world_readable?" do
+ it_behaves_like :file_world_readable, :world_readable?, FileStat
+end
+
+describe "File::Stat#world_readable?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/file/stat/world_writable_spec.rb b/spec/ruby/core/file/stat/world_writable_spec.rb
new file mode 100644
index 0000000000..8100008344
--- /dev/null
+++ b/spec/ruby/core/file/stat/world_writable_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/world_writable'
+require_relative 'fixtures/classes'
+
+describe "File::Stat.world_writable?" do
+ it_behaves_like :file_world_writable, :world_writable?, FileStat
+end
+
+describe "File::Stat#world_writable?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/file/stat/writable_real_spec.rb b/spec/ruby/core/file/stat/writable_real_spec.rb
new file mode 100644
index 0000000000..4c9e78eb70
--- /dev/null
+++ b/spec/ruby/core/file/stat/writable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/writable_real'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#writable_real?" do
+ it_behaves_like :file_writable_real, :writable_real?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/writable_spec.rb b/spec/ruby/core/file/stat/writable_spec.rb
new file mode 100644
index 0000000000..551268751f
--- /dev/null
+++ b/spec/ruby/core/file/stat/writable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/writable'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#writable?" do
+ it_behaves_like :file_writable, :writable?, FileStat
+end
diff --git a/spec/ruby/core/file/stat/zero_spec.rb b/spec/ruby/core/file/stat/zero_spec.rb
new file mode 100644
index 0000000000..74facac66a
--- /dev/null
+++ b/spec/ruby/core/file/stat/zero_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+require_relative '../../../shared/file/zero'
+require_relative 'fixtures/classes'
+
+describe "File::Stat#zero?" do
+ it_behaves_like :file_zero, :zero?, FileStat
+end
diff --git a/spec/ruby/core/file/stat_spec.rb b/spec/ruby/core/file/stat_spec.rb
new file mode 100644
index 0000000000..d5238b6331
--- /dev/null
+++ b/spec/ruby/core/file/stat_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'shared/stat'
+
+describe "File.stat" do
+ it_behaves_like :file_stat, :stat
+end
+
+platform_is_not :windows do
+ describe "File.stat" do
+ before :each do
+ @file = tmp('i_exist')
+ @link = tmp('i_am_a_symlink')
+ touch(@file) { |f| f.write "rubinius" }
+ end
+
+ after :each do
+ rm_r @link, @file
+ end
+
+ it "returns information for a file that has been deleted but is still open" do
+ File.open(@file) do |f|
+ rm_r @file
+
+ st = f.stat
+
+ st.should.file?
+ st.should_not.zero?
+ st.size.should == 8
+ st.size?.should == 8
+ st.blksize.should >= 0
+ st.atime.should.is_a?(Time)
+ st.ctime.should.is_a?(Time)
+ st.mtime.should.is_a?(Time)
+ end
+ end
+
+ it "returns a File::Stat object with file properties for a symlink" do
+ File.symlink(@file, @link)
+ st = File.stat(@link)
+
+ st.should.file?
+ st.should_not.symlink?
+ end
+
+ it "returns an error when given missing non-ASCII path" do
+ missing_path = "/missingfilepath\xE3E4".b
+ -> {
+ File.stat(missing_path)
+ }.should.raise(SystemCallError) { |e|
+ [Errno::ENOENT, Errno::EILSEQ].should.include?(e.class)
+ e.message.should.include?(missing_path)
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/file/sticky_spec.rb b/spec/ruby/core/file/sticky_spec.rb
new file mode 100644
index 0000000000..5f7b2d93eb
--- /dev/null
+++ b/spec/ruby/core/file/sticky_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/sticky'
+
+describe "File.sticky?" do
+ it_behaves_like :file_sticky, :sticky?, File
+ it_behaves_like :file_sticky_missing, :sticky?, File
+end
+
+describe "File.sticky?" do
+ platform_is_not :windows do
+ it "returns false if file does not exist" do
+ File.sticky?("I_am_a_bogus_file").should == false
+ end
+
+ it "returns false if the file has not sticky bit set" do
+ filename = tmp("i_exist")
+ touch(filename)
+
+ File.sticky?(filename).should == false
+
+ rm_r filename
+ end
+ end
+
+ platform_is :linux, :darwin do
+ it "returns true if the file has sticky bit set" do
+ filename = tmp("i_exist")
+ touch(filename)
+ system "chmod +t #{filename}"
+
+ File.sticky?(filename).should == true
+
+ rm_r filename
+ end
+ end
+
+ platform_is :bsd do
+ # FreeBSD and NetBSD can't set sticky bit to a normal file
+ it "cannot set sticky bit to a normal file" do
+ filename = tmp("i_exist")
+ touch(filename)
+ stat = File.stat(filename)
+ mode = stat.mode
+ raise_error(Errno::EFTYPE){File.chmod(mode|01000, filename)}
+ File.sticky?(filename).should == false
+
+ rm_r filename
+ end
+ end
+end
diff --git a/spec/ruby/core/file/symlink_spec.rb b/spec/ruby/core/file/symlink_spec.rb
new file mode 100644
index 0000000000..4ceeb28c84
--- /dev/null
+++ b/spec/ruby/core/file/symlink_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/symlink'
+
+describe "File.symlink" do
+ before :each do
+ @file = tmp("file_symlink.txt")
+ @link = tmp("file_symlink.lnk")
+
+ rm_r @link
+ touch @file
+ end
+
+ after :each do
+ rm_r @link, @file
+ end
+
+ platform_is_not :windows do
+ it "creates a symlink between a source and target file" do
+ File.symlink(@file, @link).should == 0
+ File.identical?(@file, @link).should == true
+ end
+
+ it "creates a symbolic link" do
+ File.symlink(@file, @link)
+ File.symlink?(@link).should == true
+ end
+
+ it "accepts args that have #to_path methods" do
+ File.symlink(mock_to_path(@file), mock_to_path(@link))
+ File.symlink?(@link).should == true
+ end
+
+ it "raises an Errno::EEXIST if the target already exists" do
+ File.symlink(@file, @link)
+ -> { File.symlink(@file, @link) }.should.raise(Errno::EEXIST)
+ end
+
+ it "raises an ArgumentError if not called with two arguments" do
+ -> { File.symlink }.should.raise(ArgumentError)
+ -> { File.symlink(@file) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if not called with String types" do
+ -> { File.symlink(@file, nil) }.should.raise(TypeError)
+ -> { File.symlink(@file, 1) }.should.raise(TypeError)
+ -> { File.symlink(1, 1) }.should.raise(TypeError)
+ end
+ end
+end
+
+describe "File.symlink?" do
+ it_behaves_like :file_symlink, :symlink?, File
+end
+
+describe "File.symlink?" do
+ it_behaves_like :file_symlink_nonexistent, :symlink?, File
+end
diff --git a/spec/ruby/core/file/to_path_spec.rb b/spec/ruby/core/file/to_path_spec.rb
new file mode 100644
index 0000000000..6d168a065c
--- /dev/null
+++ b/spec/ruby/core/file/to_path_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/path'
+
+describe "File#to_path" do
+ it_behaves_like :file_path, :to_path
+end
diff --git a/spec/ruby/core/file/truncate_spec.rb b/spec/ruby/core/file/truncate_spec.rb
new file mode 100644
index 0000000000..5f37f34155
--- /dev/null
+++ b/spec/ruby/core/file/truncate_spec.rb
@@ -0,0 +1,177 @@
+require_relative '../../spec_helper'
+
+describe "File.truncate" do
+ before :each do
+ @name = tmp("test.txt")
+ touch(@name) { |f| f.write("1234567890") }
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "truncates a file" do
+ File.size(@name).should == 10
+
+ File.truncate(@name, 5)
+ File.size(@name).should == 5
+
+ File.open(@name, "r") do |f|
+ f.read(99).should == "12345"
+ f.should.eof?
+ end
+ end
+
+ it "truncate a file size to 0" do
+ File.truncate(@name, 0).should == 0
+ IO.read(@name).should == ""
+ end
+
+ it "truncate a file size to 5" do
+ File.size(@name).should == 10
+ File.truncate(@name, 5)
+ File.size(@name).should == 5
+ IO.read(@name).should == "12345"
+ end
+
+ it "truncates to a larger file size than the original file" do
+ File.truncate(@name, 12)
+ File.size(@name).should == 12
+ IO.read(@name).should == "1234567890\000\000"
+ end
+
+ it "truncates to the same size as the original file" do
+ File.truncate(@name, File.size(@name))
+ File.size(@name).should == 10
+ IO.read(@name).should == "1234567890"
+ end
+
+ it "raises an Errno::ENOENT if the file does not exist" do
+ # TODO: missing_file
+ not_existing_file = tmp("file-does-not-exist-for-sure.txt")
+
+ # make sure it doesn't exist for real
+ rm_r not_existing_file
+
+ begin
+ -> { File.truncate(not_existing_file, 5) }.should.raise(Errno::ENOENT)
+ ensure
+ rm_r not_existing_file
+ end
+ end
+
+ it "raises an ArgumentError if not passed two arguments" do
+ -> { File.truncate }.should.raise(ArgumentError)
+ -> { File.truncate(@name) }.should.raise(ArgumentError)
+ end
+
+ platform_is_not :netbsd, :openbsd do
+ it "raises an Errno::EINVAL if the length argument is not valid" do
+ -> { File.truncate(@name, -1) }.should.raise(Errno::EINVAL) # May fail
+ end
+ end
+
+ it "raises a TypeError if not passed a String type for the first argument" do
+ -> { File.truncate(1, 1) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if not passed an Integer type for the second argument" do
+ -> { File.truncate(@name, nil) }.should.raise(TypeError)
+ end
+
+ it "accepts an object that has a #to_path method" do
+ File.truncate(mock_to_path(@name), 0).should == 0
+ end
+end
+
+
+describe "File#truncate" do
+ before :each do
+ @name = tmp("test.txt")
+ @file = File.open @name, 'w'
+ @file.write "1234567890"
+ @file.flush
+ end
+
+ after :each do
+ @file.close unless @file.closed?
+ rm_r @name
+ end
+
+ it "does not move the file write pointer to the specified byte offset" do
+ @file.truncate(3)
+ @file.write "abc"
+ @file.close
+ File.read(@name).should == "123\x00\x00\x00\x00\x00\x00\x00abc"
+ end
+
+ it "does not move the file read pointer to the specified byte offset" do
+ File.open(@name, "r+") do |f|
+ f.read(1).should == "1"
+ f.truncate(0)
+ f.read(1).should == nil
+ end
+ end
+
+ it "truncates a file" do
+ File.size(@name).should == 10
+
+ @file.truncate(5)
+ File.size(@name).should == 5
+ File.open(@name, "r") do |f|
+ f.read(99).should == "12345"
+ f.should.eof?
+ end
+ end
+
+ it "truncates a file size to 0" do
+ @file.truncate(0).should == 0
+ IO.read(@name).should == ""
+ end
+
+ it "truncates a file size to 5" do
+ File.size(@name).should == 10
+ @file.truncate(5)
+ File.size(@name).should == 5
+ IO.read(@name).should == "12345"
+ end
+
+ it "truncates a file to a larger size than the original file" do
+ @file.truncate(12)
+ File.size(@name).should == 12
+ IO.read(@name).should == "1234567890\000\000"
+ end
+
+ it "truncates a file to the same size as the original file" do
+ @file.truncate(File.size(@name))
+ File.size(@name).should == 10
+ IO.read(@name).should == "1234567890"
+ end
+
+ it "raises an ArgumentError if not passed one argument" do
+ -> { @file.truncate }.should.raise(ArgumentError)
+ -> { @file.truncate(1) }.should_not.raise(ArgumentError)
+ end
+
+ platform_is_not :netbsd do
+ it "raises an Errno::EINVAL if the length argument is not valid" do
+ -> { @file.truncate(-1) }.should.raise(Errno::EINVAL) # May fail
+ end
+ end
+
+ it "raises an IOError if file is closed" do
+ @file.close
+ @file.should.closed?
+ -> { @file.truncate(42) }.should.raise(IOError)
+ end
+
+ it "raises an IOError if file is not opened for writing" do
+ File.open(@name, 'r') do |file|
+ -> { file.truncate(42) }.should.raise(IOError)
+ end
+ end
+
+ it "raises a TypeError if not passed an Integer type for the for the argument" do
+ -> { @file.truncate(nil) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/file/umask_spec.rb b/spec/ruby/core/file/umask_spec.rb
new file mode 100644
index 0000000000..fea8cf7633
--- /dev/null
+++ b/spec/ruby/core/file/umask_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+
+describe "File.umask" do
+ before :each do
+ @orig_umask = File.umask
+ @file = tmp('test.txt')
+ touch @file
+ end
+
+ after :each do
+ rm_r @file
+ File.umask(@orig_umask)
+ end
+
+ it "returns an Integer" do
+ File.umask.should.is_a?(Integer)
+ end
+
+ platform_is_not :windows do
+ it "returns the current umask value for the process" do
+ File.umask(022)
+ File.umask(006).should == 022
+ File.umask.should == 006
+ end
+
+ it "invokes to_int on non-integer argument" do
+ (obj = mock(022)).should_receive(:to_int).any_number_of_times.and_return(022)
+ File.umask(obj)
+ File.umask(obj).should == 022
+ end
+ end
+
+ platform_is :windows do
+ it "returns the current umask value for this process (basic)" do
+ File.umask.should == 0
+ File.umask(022).should == 0
+ File.umask(044).should == 0
+ end
+
+ # The value used here is the value of _S_IWRITE.
+ it "returns the current umask value for this process" do
+ File.umask(0000200)
+ File.umask.should == 0000200
+ File.umask(0006)
+ File.umask.should == 0
+ end
+ end
+
+ it "raises RangeError with too large values" do
+ -> { File.umask(2**64) }.should.raise(RangeError)
+ -> { File.umask(-2**63 - 1) }.should.raise(RangeError)
+ end
+
+ it "raises ArgumentError when more than one argument is provided" do
+ -> { File.umask(022, 022) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/file/unlink_spec.rb b/spec/ruby/core/file/unlink_spec.rb
new file mode 100644
index 0000000000..28872d55ed
--- /dev/null
+++ b/spec/ruby/core/file/unlink_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/unlink'
+
+describe "File.unlink" do
+ it_behaves_like :file_unlink, :unlink
+end
diff --git a/spec/ruby/core/file/utime_spec.rb b/spec/ruby/core/file/utime_spec.rb
new file mode 100644
index 0000000000..d87626be50
--- /dev/null
+++ b/spec/ruby/core/file/utime_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/update_time'
+
+describe "File.utime" do
+ it_behaves_like :update_time, :utime
+end
diff --git a/spec/ruby/core/file/world_readable_spec.rb b/spec/ruby/core/file/world_readable_spec.rb
new file mode 100644
index 0000000000..0f5e0b13d7
--- /dev/null
+++ b/spec/ruby/core/file/world_readable_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/world_readable'
+
+describe "File.world_readable?" do
+ it_behaves_like :file_world_readable, :world_readable?, File
+
+ it "returns nil if the file does not exist" do
+ file = rand.to_s + $$.to_s
+ File.should_not.exist?(file)
+ File.world_readable?(file).should == nil
+ end
+end
diff --git a/spec/ruby/core/file/world_writable_spec.rb b/spec/ruby/core/file/world_writable_spec.rb
new file mode 100644
index 0000000000..46ba6832f9
--- /dev/null
+++ b/spec/ruby/core/file/world_writable_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/world_writable'
+
+describe "File.world_writable?" do
+ it_behaves_like :file_world_writable, :world_writable?, File
+
+ it "returns nil if the file does not exist" do
+ file = rand.to_s + $$.to_s
+ File.should_not.exist?(file)
+ File.world_writable?(file).should == nil
+ end
+end
diff --git a/spec/ruby/core/file/writable_real_spec.rb b/spec/ruby/core/file/writable_real_spec.rb
new file mode 100644
index 0000000000..bea4c4c262
--- /dev/null
+++ b/spec/ruby/core/file/writable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/writable_real'
+
+describe "File.writable_real?" do
+ it_behaves_like :file_writable_real, :writable_real?, File
+ it_behaves_like :file_writable_real_missing, :writable_real?, File
+end
diff --git a/spec/ruby/core/file/writable_spec.rb b/spec/ruby/core/file/writable_spec.rb
new file mode 100644
index 0000000000..519837b0d1
--- /dev/null
+++ b/spec/ruby/core/file/writable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/writable'
+
+describe "File.writable?" do
+ it_behaves_like :file_writable, :writable?, File
+ it_behaves_like :file_writable_missing, :writable?, File
+end
diff --git a/spec/ruby/core/file/zero_spec.rb b/spec/ruby/core/file/zero_spec.rb
new file mode 100644
index 0000000000..01c7505ef2
--- /dev/null
+++ b/spec/ruby/core/file/zero_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/zero'
+
+describe "File.zero?" do
+ it_behaves_like :file_zero, :zero?, File
+ it_behaves_like :file_zero_missing, :zero?, File
+end
diff --git a/spec/ruby/core/filetest/blockdev_spec.rb b/spec/ruby/core/filetest/blockdev_spec.rb
new file mode 100644
index 0000000000..4f32991c4a
--- /dev/null
+++ b/spec/ruby/core/filetest/blockdev_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/blockdev'
+
+describe "FileTest.blockdev?" do
+ it_behaves_like :file_blockdev, :blockdev?, FileTest
+end
diff --git a/spec/ruby/core/filetest/chardev_spec.rb b/spec/ruby/core/filetest/chardev_spec.rb
new file mode 100644
index 0000000000..59c48bb2d5
--- /dev/null
+++ b/spec/ruby/core/filetest/chardev_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/chardev'
+
+describe "FileTest.chardev?" do
+ it_behaves_like :file_chardev, :chardev?, FileTest
+end
diff --git a/spec/ruby/core/filetest/directory_spec.rb b/spec/ruby/core/filetest/directory_spec.rb
new file mode 100644
index 0000000000..8f9d0e3901
--- /dev/null
+++ b/spec/ruby/core/filetest/directory_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/directory'
+
+describe "FileTest.directory?" do
+ it_behaves_like :file_directory, :directory?, FileTest
+end
+
+describe "FileTest.directory?" do
+ it_behaves_like :file_directory_io, :directory?, FileTest
+end
diff --git a/spec/ruby/core/filetest/executable_real_spec.rb b/spec/ruby/core/filetest/executable_real_spec.rb
new file mode 100644
index 0000000000..da65245785
--- /dev/null
+++ b/spec/ruby/core/filetest/executable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/executable_real'
+
+describe "FileTest.executable_real?" do
+ it_behaves_like :file_executable_real, :executable_real?, FileTest
+ it_behaves_like :file_executable_real_missing, :executable_real?, FileTest
+end
diff --git a/spec/ruby/core/filetest/executable_spec.rb b/spec/ruby/core/filetest/executable_spec.rb
new file mode 100644
index 0000000000..03056669f6
--- /dev/null
+++ b/spec/ruby/core/filetest/executable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/executable'
+
+describe "FileTest.executable?" do
+ it_behaves_like :file_executable, :executable?, FileTest
+ it_behaves_like :file_executable_missing, :executable?, FileTest
+end
diff --git a/spec/ruby/core/filetest/exist_spec.rb b/spec/ruby/core/filetest/exist_spec.rb
new file mode 100644
index 0000000000..612ffa9fcb
--- /dev/null
+++ b/spec/ruby/core/filetest/exist_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/exist'
+
+describe "FileTest.exist?" do
+ it_behaves_like :file_exist, :exist?, FileTest
+end
+
+describe "FileTest.exists?" do
+ it "has been removed" do
+ FileTest.should_not.respond_to?(:exists?)
+ end
+end
diff --git a/spec/ruby/core/filetest/file_spec.rb b/spec/ruby/core/filetest/file_spec.rb
new file mode 100644
index 0000000000..0c0cb82f96
--- /dev/null
+++ b/spec/ruby/core/filetest/file_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/file'
+
+describe "File.file?" do
+ it_behaves_like :file_file, :file?, File
+end
+
+describe "FileTest.file?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/filetest/grpowned_spec.rb b/spec/ruby/core/filetest/grpowned_spec.rb
new file mode 100644
index 0000000000..d6bd6abd71
--- /dev/null
+++ b/spec/ruby/core/filetest/grpowned_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/grpowned'
+
+describe "FileTest.grpowned?" do
+ it_behaves_like :file_grpowned, :grpowned?, FileTest
+
+ it "returns false if the file doesn't exist" do
+ FileTest.grpowned?("xxx-tmp-doesnt_exist-blah").should == false
+ end
+end
diff --git a/spec/ruby/core/filetest/identical_spec.rb b/spec/ruby/core/filetest/identical_spec.rb
new file mode 100644
index 0000000000..b00c5b75e8
--- /dev/null
+++ b/spec/ruby/core/filetest/identical_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/identical'
+
+describe "FileTest.identical?" do
+ it_behaves_like :file_identical, :identical?, FileTest
+end
diff --git a/spec/ruby/core/filetest/owned_spec.rb b/spec/ruby/core/filetest/owned_spec.rb
new file mode 100644
index 0000000000..b26165f98d
--- /dev/null
+++ b/spec/ruby/core/filetest/owned_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/owned'
+
+describe "FileTest.owned?" do
+ it_behaves_like :file_owned, :owned?, FileTest
+end
diff --git a/spec/ruby/core/filetest/pipe_spec.rb b/spec/ruby/core/filetest/pipe_spec.rb
new file mode 100644
index 0000000000..8ce67568fb
--- /dev/null
+++ b/spec/ruby/core/filetest/pipe_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/pipe'
+
+describe "FileTest.pipe?" do
+ it_behaves_like :file_pipe, :pipe?, FileTest
+end
diff --git a/spec/ruby/core/filetest/readable_real_spec.rb b/spec/ruby/core/filetest/readable_real_spec.rb
new file mode 100644
index 0000000000..82c62fe8f0
--- /dev/null
+++ b/spec/ruby/core/filetest/readable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/readable_real'
+
+describe "FileTest.readable_real?" do
+ it_behaves_like :file_readable_real, :readable_real?, FileTest
+ it_behaves_like :file_readable_real_missing, :readable_real?, FileTest
+end
diff --git a/spec/ruby/core/filetest/readable_spec.rb b/spec/ruby/core/filetest/readable_spec.rb
new file mode 100644
index 0000000000..039ca56ca3
--- /dev/null
+++ b/spec/ruby/core/filetest/readable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/readable'
+
+describe "FileTest.readable?" do
+ it_behaves_like :file_readable, :readable?, FileTest
+ it_behaves_like :file_readable_missing, :readable?, FileTest
+end
diff --git a/spec/ruby/core/filetest/setgid_spec.rb b/spec/ruby/core/filetest/setgid_spec.rb
new file mode 100644
index 0000000000..c83ffccb2a
--- /dev/null
+++ b/spec/ruby/core/filetest/setgid_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/setgid'
+
+describe "FileTest.setgid?" do
+ it_behaves_like :file_setgid, :setgid?, FileTest
+end
diff --git a/spec/ruby/core/filetest/setuid_spec.rb b/spec/ruby/core/filetest/setuid_spec.rb
new file mode 100644
index 0000000000..1c1fd2efe4
--- /dev/null
+++ b/spec/ruby/core/filetest/setuid_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/setuid'
+
+describe "FileTest.setuid?" do
+ it_behaves_like :file_setuid, :setuid?, FileTest
+end
diff --git a/spec/ruby/core/filetest/size_spec.rb b/spec/ruby/core/filetest/size_spec.rb
new file mode 100644
index 0000000000..dc3ddb127f
--- /dev/null
+++ b/spec/ruby/core/filetest/size_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/size'
+
+describe "FileTest.size?" do
+ it_behaves_like :file_size, :size?, FileTest
+end
+
+describe "FileTest.size?" do
+ it_behaves_like :file_size_nil_when_missing, :size?, FileTest
+end
+
+describe "FileTest.size?" do
+ it_behaves_like :file_size_nil_when_empty, :size?, FileTest
+end
+
+describe "FileTest.size?" do
+ it_behaves_like :file_size_with_file_argument, :size?, FileTest
+end
+
+describe "FileTest.size" do
+ it_behaves_like :file_size, :size, FileTest
+end
+
+describe "FileTest.size" do
+ it_behaves_like :file_size_raise_when_missing, :size, FileTest
+end
+
+describe "FileTest.size" do
+ it_behaves_like :file_size_0_when_empty, :size, FileTest
+end
+
+describe "FileTest.size" do
+ it_behaves_like :file_size_with_file_argument, :size, FileTest
+end
diff --git a/spec/ruby/core/filetest/socket_spec.rb b/spec/ruby/core/filetest/socket_spec.rb
new file mode 100644
index 0000000000..f274be6318
--- /dev/null
+++ b/spec/ruby/core/filetest/socket_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/socket'
+
+describe "FileTest.socket?" do
+ it_behaves_like :file_socket, :socket?, FileTest
+
+ it "returns false if file does not exist" do
+ FileTest.socket?("I_am_a_bogus_file").should == false
+ end
+end
diff --git a/spec/ruby/core/filetest/sticky_spec.rb b/spec/ruby/core/filetest/sticky_spec.rb
new file mode 100644
index 0000000000..8b776b6672
--- /dev/null
+++ b/spec/ruby/core/filetest/sticky_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/sticky'
+
+describe "FileTest.sticky?" do
+ it_behaves_like :file_sticky, :sticky?, FileTest
+ it_behaves_like :file_sticky_missing, :sticky?, FileTest
+end
diff --git a/spec/ruby/core/filetest/symlink_spec.rb b/spec/ruby/core/filetest/symlink_spec.rb
new file mode 100644
index 0000000000..41c924dc1a
--- /dev/null
+++ b/spec/ruby/core/filetest/symlink_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/symlink'
+
+describe "FileTest.symlink?" do
+ it_behaves_like :file_symlink, :symlink?, FileTest
+end
+
+describe "FileTest.symlink?" do
+ it_behaves_like :file_symlink_nonexistent, :symlink?, File
+end
diff --git a/spec/ruby/core/filetest/world_readable_spec.rb b/spec/ruby/core/filetest/world_readable_spec.rb
new file mode 100644
index 0000000000..72abdd9e03
--- /dev/null
+++ b/spec/ruby/core/filetest/world_readable_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "FileTest.world_readable?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/filetest/world_writable_spec.rb b/spec/ruby/core/filetest/world_writable_spec.rb
new file mode 100644
index 0000000000..533f698fd3
--- /dev/null
+++ b/spec/ruby/core/filetest/world_writable_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "FileTest.world_writable?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/filetest/writable_real_spec.rb b/spec/ruby/core/filetest/writable_real_spec.rb
new file mode 100644
index 0000000000..64abe4cd3f
--- /dev/null
+++ b/spec/ruby/core/filetest/writable_real_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/writable_real'
+
+describe "FileTest.writable_real?" do
+ it_behaves_like :file_writable_real, :writable_real?, FileTest
+ it_behaves_like :file_writable_real_missing, :writable_real?, FileTest
+end
diff --git a/spec/ruby/core/filetest/writable_spec.rb b/spec/ruby/core/filetest/writable_spec.rb
new file mode 100644
index 0000000000..e921a5887b
--- /dev/null
+++ b/spec/ruby/core/filetest/writable_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/writable'
+
+describe "FileTest.writable?" do
+ it_behaves_like :file_writable, :writable?, FileTest
+ it_behaves_like :file_writable_missing, :writable?, FileTest
+end
diff --git a/spec/ruby/core/filetest/zero_spec.rb b/spec/ruby/core/filetest/zero_spec.rb
new file mode 100644
index 0000000000..92cab67f1b
--- /dev/null
+++ b/spec/ruby/core/filetest/zero_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/file/zero'
+
+describe "FileTest.zero?" do
+ it_behaves_like :file_zero, :zero?, FileTest
+ it_behaves_like :file_zero_missing, :zero?, FileTest
+end
diff --git a/spec/ruby/core/float/abs_spec.rb b/spec/ruby/core/float/abs_spec.rb
new file mode 100644
index 0000000000..a08601926d
--- /dev/null
+++ b/spec/ruby/core/float/abs_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/abs'
+
+describe "Float#abs" do
+ it_behaves_like :float_abs, :abs
+end
diff --git a/spec/ruby/core/float/angle_spec.rb b/spec/ruby/core/float/angle_spec.rb
new file mode 100644
index 0000000000..c07249aa97
--- /dev/null
+++ b/spec/ruby/core/float/angle_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arg'
+
+describe "Float#angle" do
+ it_behaves_like :float_arg, :angle
+end
diff --git a/spec/ruby/core/float/arg_spec.rb b/spec/ruby/core/float/arg_spec.rb
new file mode 100644
index 0000000000..d3a50668f8
--- /dev/null
+++ b/spec/ruby/core/float/arg_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arg'
+
+describe "Float#arg" do
+ it_behaves_like :float_arg, :arg
+end
diff --git a/spec/ruby/core/float/case_compare_spec.rb b/spec/ruby/core/float/case_compare_spec.rb
new file mode 100644
index 0000000000..b902fbea18
--- /dev/null
+++ b/spec/ruby/core/float/case_compare_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal'
+
+describe "Float#===" do
+ it_behaves_like :float_equal, :===
+end
diff --git a/spec/ruby/core/float/ceil_spec.rb b/spec/ruby/core/float/ceil_spec.rb
new file mode 100644
index 0000000000..efd1e6feb2
--- /dev/null
+++ b/spec/ruby/core/float/ceil_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+require_relative '../integer/shared/integer_ceil_precision'
+
+describe "Float#ceil" do
+ context "with values equal to integers" do
+ it_behaves_like :integer_ceil_precision, :Float
+ end
+
+ it "returns the smallest Integer greater than or equal to self" do
+ -1.2.ceil.should.eql?( -1)
+ -1.0.ceil.should.eql?( -1)
+ 0.0.ceil.should.eql?( 0 )
+ 1.3.ceil.should.eql?( 2 )
+ 3.0.ceil.should.eql?( 3 )
+ -9223372036854775808.1.ceil.should.eql?(-9223372036854775808)
+ +9223372036854775808.1.ceil.should.eql?(+9223372036854775808)
+ end
+
+ it "returns the smallest number greater than or equal to self with an optionally given precision" do
+ 2.1679.ceil(0).should.eql?(3)
+ 214.94.ceil(-1).should.eql?(220)
+ 7.0.ceil(1).should.eql?(7.0)
+ 200.0.ceil(-2).should.eql?(200)
+ -1.234.ceil(2).should.eql?(-1.23)
+ 5.123812.ceil(4).should.eql?(5.1239)
+ 10.00001.ceil(5).should.eql?(10.00001)
+ end
+end
diff --git a/spec/ruby/core/float/coerce_spec.rb b/spec/ruby/core/float/coerce_spec.rb
new file mode 100644
index 0000000000..baa831dcf6
--- /dev/null
+++ b/spec/ruby/core/float/coerce_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+
+describe "Float#coerce" do
+ it "returns [other, self] both as Floats" do
+ 1.2.coerce(1).should == [1.0, 1.2]
+ 5.28.coerce(1.0).should == [1.0, 5.28]
+ 1.0.coerce(1).should == [1.0, 1.0]
+ 1.0.coerce("2.5").should == [2.5, 1.0]
+ 1.0.coerce(3.14).should == [3.14, 1.0]
+
+ a, b = -0.0.coerce(bignum_value)
+ a.should be_close(18446744073709551616.0, TOLERANCE)
+ b.should be_close(-0.0, TOLERANCE)
+ a, b = 1.0.coerce(bignum_value)
+ a.should be_close(18446744073709551616.0, TOLERANCE)
+ b.should be_close(1.0, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/float/comparison_spec.rb b/spec/ruby/core/float/comparison_spec.rb
new file mode 100644
index 0000000000..e9adf2fd6a
--- /dev/null
+++ b/spec/ruby/core/float/comparison_spec.rb
@@ -0,0 +1,113 @@
+require_relative '../../spec_helper'
+
+describe "Float#<=>" do
+ it "returns -1, 0, 1 when self is less than, equal, or greater than other" do
+ (1.5 <=> 5).should == -1
+ (2.45 <=> 2.45).should == 0
+ ((bignum_value*1.1) <=> bignum_value).should == 1
+ end
+
+ it "returns nil if one side is NaN" do
+ [1.0, 42, bignum_value].each { |n|
+ (nan_value <=> n).should == nil
+ (n <=> nan_value).should == nil
+ }
+ end
+
+ it "handles positive infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (infinity_value <=> n).should == 1
+ (n <=> infinity_value).should == -1
+ }
+ end
+
+ it "handles negative infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (-infinity_value <=> n).should == -1
+ (n <=> -infinity_value).should == 1
+ }
+ end
+
+ it "returns nil when the given argument is not a Float" do
+ (1.0 <=> "1").should == nil
+ (1.0 <=> "1".freeze).should == nil
+ (1.0 <=> :one).should == nil
+ (1.0 <=> true).should == nil
+ end
+
+ it "compares using #coerce when argument is not a Float" do
+ klass = Class.new do
+ attr_reader :call_count
+ def coerce(other)
+ @call_count ||= 0
+ @call_count += 1
+ [other, 42.0]
+ end
+ end
+
+ coercible = klass.new
+ (2.33 <=> coercible).should == -1
+ (42.0 <=> coercible).should == 0
+ (43.0 <=> coercible).should == 1
+ coercible.call_count.should == 3
+ end
+
+ it "raises TypeError when #coerce misbehaves" do
+ klass = Class.new do
+ def coerce(other)
+ :incorrect
+ end
+ end
+
+ bad_coercible = klass.new
+ -> {
+ 4.2 <=> bad_coercible
+ }.should.raise(TypeError, "coerce must return [x, y]")
+ end
+
+ it "returns the correct result when one side is infinite" do
+ (infinity_value <=> Float::MAX.to_i*2).should == 1
+ (-Float::MAX.to_i*2 <=> infinity_value).should == -1
+ (-infinity_value <=> -Float::MAX.to_i*2).should == -1
+ (-Float::MAX.to_i*2 <=> -infinity_value).should == 1
+ end
+
+ it "returns 0 when self is Infinity and other is infinite?=1" do
+ obj = Object.new
+ def obj.infinite?
+ 1
+ end
+ (infinity_value <=> obj).should == 0
+ end
+
+ it "returns 1 when self is Infinity and other is infinite?=-1" do
+ obj = Object.new
+ def obj.infinite?
+ -1
+ end
+ (infinity_value <=> obj).should == 1
+ end
+
+ it "returns 1 when self is Infinity and other is infinite?=nil (which means finite)" do
+ obj = Object.new
+ def obj.infinite?
+ nil
+ end
+ (infinity_value <=> obj).should == 1
+ end
+
+ it "returns 0 for -0.0 and 0.0" do
+ (-0.0 <=> 0.0).should == 0
+ (0.0 <=> -0.0).should == 0
+ end
+
+ it "returns 0 for -0.0 and 0" do
+ (-0.0 <=> 0).should == 0
+ (0 <=> -0.0).should == 0
+ end
+
+ it "returns 0 for 0.0 and 0" do
+ (0.0 <=> 0).should == 0
+ (0 <=> 0.0).should == 0
+ end
+end
diff --git a/spec/ruby/core/float/constants_spec.rb b/spec/ruby/core/float/constants_spec.rb
new file mode 100644
index 0000000000..1b71ee8adf
--- /dev/null
+++ b/spec/ruby/core/float/constants_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+
+describe "Float constant" do
+ it "DIG is 15" do
+ Float::DIG.should == 15
+ end
+
+ it "EPSILON is 2.220446049250313e-16" do
+ Float::EPSILON.should == 2.0 ** -52
+ Float::EPSILON.should == 2.220446049250313e-16
+ end
+
+ it "MANT_DIG is 53" do
+ Float::MANT_DIG.should == 53
+ end
+
+ it "MAX_10_EXP is 308" do
+ Float::MAX_10_EXP.should == 308
+ end
+
+ it "MIN_10_EXP is -308" do
+ Float::MIN_10_EXP.should == -307
+ end
+
+ it "MAX_EXP is 1024" do
+ Float::MAX_EXP.should == 1024
+ end
+
+ it "MIN_EXP is -1021" do
+ Float::MIN_EXP.should == -1021
+ end
+
+ it "MAX is 1.7976931348623157e+308" do
+ # See https://en.wikipedia.org/wiki/Double-precision_floating-point_format#Double-precision_examples
+ Float::MAX.should == (1 + (1 - (2 ** -52))) * (2.0 ** 1023)
+ Float::MAX.should == 1.7976931348623157e+308
+ end
+
+ it "MIN is 2.2250738585072014e-308" do
+ Float::MIN.should == (2.0 ** -1022)
+ Float::MIN.should == 2.2250738585072014e-308
+ end
+
+ it "RADIX is 2" do
+ Float::RADIX.should == 2
+ end
+
+ it "INFINITY is the positive infinity" do
+ Float::INFINITY.infinite?.should == 1
+ end
+
+ it "NAN is 'not a number'" do
+ Float::NAN.nan?.should == true
+ end
+end
diff --git a/spec/ruby/core/float/denominator_spec.rb b/spec/ruby/core/float/denominator_spec.rb
new file mode 100644
index 0000000000..85beaa98cd
--- /dev/null
+++ b/spec/ruby/core/float/denominator_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Float#denominator" do
+ before :each do
+ @numbers = [
+ 0.0,
+ 29871.22736282,
+ 7772222663.0,
+ 1.4592,
+ ].map {|n| [0-n, n]}.flatten
+ end
+
+ it "returns an Integer" do
+ @numbers.each do |number|
+ number.denominator.should.is_a?(Integer)
+ end
+ end
+
+ it "converts self to a Rational and returns the denominator" do
+ @numbers.each do |number|
+ number.denominator.should == Rational(number).denominator
+ end
+ end
+
+ it "returns 1 for NaN and Infinity" do
+ nan_value.denominator.should == 1
+ infinity_value.denominator.should == 1
+ end
+end
diff --git a/spec/ruby/core/float/divide_spec.rb b/spec/ruby/core/float/divide_spec.rb
new file mode 100644
index 0000000000..68a2c550a7
--- /dev/null
+++ b/spec/ruby/core/float/divide_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/coerce'
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Float#/" do
+ it_behaves_like :float_arithmetic_exception_in_coerce, :/
+
+ it "returns self divided by other" do
+ (5.75 / -2).should be_close(-2.875,TOLERANCE)
+ (451.0 / 9.3).should be_close(48.494623655914,TOLERANCE)
+ (91.1 / -0xffffffff).should be_close(-2.12108716418061e-08, TOLERANCE)
+ end
+
+ it "properly coerces objects" do
+ (5.0 / FloatSpecs::CanCoerce.new(5)).should be_close(0, TOLERANCE)
+ end
+
+ it "returns +Infinity when dividing non-zero by zero of the same sign" do
+ (1.0 / 0.0).should.infinite? == 1
+ (-1.0 / -0.0).should.infinite? == 1
+ end
+
+ it "returns -Infinity when dividing non-zero by zero of opposite sign" do
+ (-1.0 / 0.0).should.infinite? == -1
+ (1.0 / -0.0).should.infinite? == -1
+ end
+
+ it "returns NaN when dividing zero by zero" do
+ (0.0 / 0.0).should.nan?
+ (-0.0 / 0.0).should.nan?
+ (0.0 / -0.0).should.nan?
+ (-0.0 / -0.0).should.nan?
+ end
+
+ it "raises a TypeError when given a non-Numeric" do
+ -> { 13.0 / "10" }.should.raise(TypeError)
+ -> { 13.0 / :symbol }.should.raise(TypeError)
+ end
+
+ it "divides correctly by Rational numbers" do
+ (1.2345678901234567 / Rational(1, 10000000000000000000)).should == 1.2345678901234567e+19
+ end
+end
diff --git a/spec/ruby/core/float/divmod_spec.rb b/spec/ruby/core/float/divmod_spec.rb
new file mode 100644
index 0000000000..7ed6cd3487
--- /dev/null
+++ b/spec/ruby/core/float/divmod_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "Float#divmod" do
+ it "returns an [quotient, modulus] from dividing self by other" do
+ values = 3.14.divmod(2)
+ values[0].should.eql?(1)
+ values[1].should be_close(1.14, TOLERANCE)
+ values = 2.8284.divmod(3.1415)
+ values[0].should.eql?(0)
+ values[1].should be_close(2.8284, TOLERANCE)
+ values = -1.0.divmod(bignum_value)
+ values[0].should.eql?(-1)
+ values[1].should be_close(18446744073709551616.0, TOLERANCE)
+ values = -1.0.divmod(1)
+ values[0].should.eql?(-1)
+ values[1].should.eql?(0.0)
+ end
+
+ # Behaviour established as correct in r23953
+ it "raises a FloatDomainError if self is NaN" do
+ -> { nan_value.divmod(1) }.should.raise(FloatDomainError)
+ end
+
+ # Behaviour established as correct in r23953
+ it "raises a FloatDomainError if other is NaN" do
+ -> { 1.0.divmod(nan_value) }.should.raise(FloatDomainError)
+ end
+
+ # Behaviour established as correct in r23953
+ it "raises a FloatDomainError if self is Infinity" do
+ -> { infinity_value.divmod(1) }.should.raise(FloatDomainError)
+ end
+
+ it "raises a ZeroDivisionError if other is zero" do
+ -> { 1.0.divmod(0) }.should.raise(ZeroDivisionError)
+ -> { 1.0.divmod(0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ # redmine #5276"
+ it "returns the correct [quotient, modulus] even for large quotient" do
+ 0.59.divmod(7.761021455128987e-11).first.should.eql?(7602092113)
+ end
+end
diff --git a/spec/ruby/core/float/dup_spec.rb b/spec/ruby/core/float/dup_spec.rb
new file mode 100644
index 0000000000..b474e21325
--- /dev/null
+++ b/spec/ruby/core/float/dup_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "Float#dup" do
+ it "returns self" do
+ float = 2.4
+ float.dup.should.equal?(float)
+ end
+end
diff --git a/spec/ruby/core/float/eql_spec.rb b/spec/ruby/core/float/eql_spec.rb
new file mode 100644
index 0000000000..cf1ad8416f
--- /dev/null
+++ b/spec/ruby/core/float/eql_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Float#eql?" do
+ it "returns true if other is a Float equal to self" do
+ 0.0.eql?(0.0).should == true
+ end
+
+ it "returns false if other is a Float not equal to self" do
+ 1.0.eql?(1.1).should == false
+ end
+
+ it "returns false if other is not a Float" do
+ 1.0.eql?(1).should == false
+ 1.0.eql?(:one).should == false
+ end
+end
diff --git a/spec/ruby/core/float/equal_value_spec.rb b/spec/ruby/core/float/equal_value_spec.rb
new file mode 100644
index 0000000000..03eea5108e
--- /dev/null
+++ b/spec/ruby/core/float/equal_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal'
+
+describe "Float#==" do
+ it_behaves_like :float_equal, :==
+end
diff --git a/spec/ruby/core/float/exponent_spec.rb b/spec/ruby/core/float/exponent_spec.rb
new file mode 100644
index 0000000000..a4c03469a7
--- /dev/null
+++ b/spec/ruby/core/float/exponent_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "Float#**" do
+ it "returns self raise to the other power" do
+ (2.3 ** 3).should be_close(12.167,TOLERANCE)
+ (5.2 ** -1).should be_close(0.192307692307692,TOLERANCE)
+ (9.5 ** 0.5).should be_close(3.08220700148449, TOLERANCE)
+ (9.5 ** 0xffffffff).to_s.should == 'Infinity'
+ end
+
+ it "returns a complex number when negative and raised to a fractional power" do
+ ((-8.0) ** (1.0/3)) .should be_close(Complex(1, 1.73205), TOLERANCE)
+ ((-8.0) ** Rational(1,3)).should be_close(Complex(1, 1.73205), TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/float/fdiv_spec.rb b/spec/ruby/core/float/fdiv_spec.rb
new file mode 100644
index 0000000000..be25ee283b
--- /dev/null
+++ b/spec/ruby/core/float/fdiv_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/quo'
+
+describe "Float#fdiv" do
+ it_behaves_like :float_quo, :fdiv
+end
diff --git a/spec/ruby/core/float/finite_spec.rb b/spec/ruby/core/float/finite_spec.rb
new file mode 100644
index 0000000000..d839b30e32
--- /dev/null
+++ b/spec/ruby/core/float/finite_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Float#finite?" do
+ it "returns true for finite values" do
+ 3.14159.should.finite?
+ end
+
+ it "returns false for positive infinity" do
+ infinity_value.should_not.finite?
+ end
+
+ it "returns false for negative infinity" do
+ (-infinity_value).should_not.finite?
+ end
+
+ it "returns false for NaN" do
+ nan_value.should_not.finite?
+ end
+end
diff --git a/spec/ruby/core/float/fixtures/classes.rb b/spec/ruby/core/float/fixtures/classes.rb
new file mode 100644
index 0000000000..2d80184e7d
--- /dev/null
+++ b/spec/ruby/core/float/fixtures/classes.rb
@@ -0,0 +1,4 @@
+module FloatSpecs
+ class CoerceError < StandardError
+ end
+end
diff --git a/spec/ruby/core/float/fixtures/coerce.rb b/spec/ruby/core/float/fixtures/coerce.rb
new file mode 100644
index 0000000000..2cf155be95
--- /dev/null
+++ b/spec/ruby/core/float/fixtures/coerce.rb
@@ -0,0 +1,15 @@
+module FloatSpecs
+ class CanCoerce
+ def initialize(a)
+ @a = a
+ end
+
+ def coerce(b)
+ [self.class.new(b), @a]
+ end
+
+ def /(b)
+ @a.to_i % b.to_i
+ end
+ end
+end
diff --git a/spec/ruby/core/float/float_spec.rb b/spec/ruby/core/float/float_spec.rb
new file mode 100644
index 0000000000..46b2eff372
--- /dev/null
+++ b/spec/ruby/core/float/float_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Float" do
+ it "includes Comparable" do
+ Float.include?(Comparable).should == true
+ end
+
+ it ".allocate raises a TypeError" do
+ -> do
+ Float.allocate
+ end.should.raise(TypeError)
+ end
+
+ it ".new is undefined" do
+ -> do
+ Float.new
+ end.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/float/floor_spec.rb b/spec/ruby/core/float/floor_spec.rb
new file mode 100644
index 0000000000..f77300eb04
--- /dev/null
+++ b/spec/ruby/core/float/floor_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+require_relative '../integer/shared/integer_floor_precision'
+
+describe "Float#floor" do
+ context "with values equal to integers" do
+ it_behaves_like :integer_floor_precision, :Float
+ end
+
+ it "returns the largest Integer less than or equal to self" do
+ -1.2.floor.should.eql?( -2)
+ -1.0.floor.should.eql?( -1)
+ 0.0.floor.should.eql?( 0 )
+ 1.0.floor.should.eql?( 1 )
+ 5.9.floor.should.eql?( 5 )
+ -9223372036854775808.1.floor.should.eql?(-9223372036854775808)
+ +9223372036854775808.1.floor.should.eql?(+9223372036854775808)
+ end
+
+ it "returns the largest number less than or equal to self with an optionally given precision" do
+ 2.1679.floor(0).should.eql?(2)
+ 214.94.floor(-1).should.eql?(210)
+ 7.0.floor(1).should.eql?(7.0)
+ 200.0.floor(-2).should.eql?(200)
+ -1.234.floor(2).should.eql?(-1.24)
+ 5.123812.floor(4).should.eql?(5.1238)
+ 10.00001.floor(5).should.eql?(10.00001)
+ end
+end
diff --git a/spec/ruby/core/float/gt_spec.rb b/spec/ruby/core/float/gt_spec.rb
new file mode 100644
index 0000000000..5194796b46
--- /dev/null
+++ b/spec/ruby/core/float/gt_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_exception_in_coerce'
+
+describe "Float#>" do
+ it_behaves_like :float_comparison_exception_in_coerce, :>
+
+ it "returns true if self is greater than other" do
+ (1.5 > 1).should == true
+ (2.5 > 3).should == false
+ (45.91 > bignum_value).should == false
+ end
+
+ it "raises an ArgumentError when given a non-Numeric" do
+ -> { 5.0 > "4" }.should.raise(ArgumentError)
+ -> { 5.0 > mock('x') }.should.raise(ArgumentError)
+ end
+
+ it "returns false if one side is NaN" do
+ [1.0, 42, bignum_value].each { |n|
+ (nan_value > n).should == false
+ (n > nan_value).should == false
+ }
+ end
+
+ it "handles positive infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (infinity_value > n).should == true
+ (n > infinity_value).should == false
+ }
+ end
+
+ it "handles negative infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (-infinity_value > n).should == false
+ (n > -infinity_value).should == true
+ }
+ end
+end
diff --git a/spec/ruby/core/float/gte_spec.rb b/spec/ruby/core/float/gte_spec.rb
new file mode 100644
index 0000000000..4a62725d53
--- /dev/null
+++ b/spec/ruby/core/float/gte_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_exception_in_coerce'
+
+describe "Float#>=" do
+ it_behaves_like :float_comparison_exception_in_coerce, :>=
+
+ it "returns true if self is greater than or equal to other" do
+ (5.2 >= 5.2).should == true
+ (9.71 >= 1).should == true
+ (5.55382 >= 0xfabdafbafcab).should == false
+ end
+
+ it "raises an ArgumentError when given a non-Numeric" do
+ -> { 5.0 >= "4" }.should.raise(ArgumentError)
+ -> { 5.0 >= mock('x') }.should.raise(ArgumentError)
+ end
+
+ it "returns false if one side is NaN" do
+ [1.0, 42, bignum_value].each { |n|
+ (nan_value >= n).should == false
+ (n >= nan_value).should == false
+ }
+ end
+
+ it "handles positive infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (infinity_value >= n).should == true
+ (n >= infinity_value).should == false
+ }
+ end
+
+ it "handles negative infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (-infinity_value >= n).should == false
+ (n >= -infinity_value).should == true
+ }
+ end
+end
diff --git a/spec/ruby/core/float/hash_spec.rb b/spec/ruby/core/float/hash_spec.rb
new file mode 100644
index 0000000000..5f77e3b4a1
--- /dev/null
+++ b/spec/ruby/core/float/hash_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Float#hash" do
+ it "is provided" do
+ 0.0.respond_to?(:hash).should == true
+ end
+
+ it "is stable" do
+ 1.0.hash.should == 1.0.hash
+ end
+end
diff --git a/spec/ruby/core/float/infinite_spec.rb b/spec/ruby/core/float/infinite_spec.rb
new file mode 100644
index 0000000000..901c2738aa
--- /dev/null
+++ b/spec/ruby/core/float/infinite_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Float#infinite?" do
+ it "returns nil for finite values" do
+ 1.0.infinite?.should == nil
+ end
+
+ it "returns 1 for positive infinity" do
+ infinity_value.infinite?.should == 1
+ end
+
+ it "returns -1 for negative infinity" do
+ (-infinity_value).infinite?.should == -1
+ end
+
+ it "returns nil for NaN" do
+ nan_value.infinite?.should == nil
+ end
+end
diff --git a/spec/ruby/core/float/inspect_spec.rb b/spec/ruby/core/float/inspect_spec.rb
new file mode 100644
index 0000000000..4be1927d84
--- /dev/null
+++ b/spec/ruby/core/float/inspect_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+
+describe "Float#inspect" do
+ it_behaves_like :float_to_s, :inspect
+end
diff --git a/spec/ruby/core/float/lt_spec.rb b/spec/ruby/core/float/lt_spec.rb
new file mode 100644
index 0000000000..0f0e1752bd
--- /dev/null
+++ b/spec/ruby/core/float/lt_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_exception_in_coerce'
+
+describe "Float#<" do
+ it_behaves_like :float_comparison_exception_in_coerce, :<
+
+ it "returns true if self is less than other" do
+ (71.3 < 91.8).should == true
+ (192.6 < -500).should == false
+ (-0.12 < 0x4fffffff).should == true
+ end
+
+ it "raises an ArgumentError when given a non-Numeric" do
+ -> { 5.0 < "4" }.should.raise(ArgumentError)
+ -> { 5.0 < mock('x') }.should.raise(ArgumentError)
+ end
+
+ it "returns false if one side is NaN" do
+ [1.0, 42, bignum_value].each { |n|
+ (nan_value < n).should == false
+ (n < nan_value).should == false
+ }
+ end
+
+ it "handles positive infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (infinity_value < n).should == false
+ (n < infinity_value).should == true
+ }
+ end
+
+ it "handles negative infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (-infinity_value < n).should == true
+ (n < -infinity_value).should == false
+ }
+ end
+end
diff --git a/spec/ruby/core/float/lte_spec.rb b/spec/ruby/core/float/lte_spec.rb
new file mode 100644
index 0000000000..afb64ade09
--- /dev/null
+++ b/spec/ruby/core/float/lte_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_exception_in_coerce'
+
+describe "Float#<=" do
+ it_behaves_like :float_comparison_exception_in_coerce, :>=
+
+ it "returns true if self is less than or equal to other" do
+ (2.0 <= 3.14159).should == true
+ (-2.7183 <= -24).should == false
+ (0.0 <= 0.0).should == true
+ (9_235.9 <= bignum_value).should == true
+ end
+
+ it "raises an ArgumentError when given a non-Numeric" do
+ -> { 5.0 <= "4" }.should.raise(ArgumentError)
+ -> { 5.0 <= mock('x') }.should.raise(ArgumentError)
+ end
+
+ it "returns false if one side is NaN" do
+ [1.0, 42, bignum_value].each { |n|
+ (nan_value <= n).should == false
+ (n <= nan_value).should == false
+ }
+ end
+
+ it "handles positive infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (infinity_value <= n).should == false
+ (n <= infinity_value).should == true
+ }
+ end
+
+ it "handles negative infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (-infinity_value <= n).should == true
+ (n <= -infinity_value).should == false
+ }
+ end
+end
diff --git a/spec/ruby/core/float/magnitude_spec.rb b/spec/ruby/core/float/magnitude_spec.rb
new file mode 100644
index 0000000000..7cdd8ef28a
--- /dev/null
+++ b/spec/ruby/core/float/magnitude_spec.rb
@@ -0,0 +1,6 @@
+require_relative "../../spec_helper"
+require_relative 'shared/abs'
+
+describe "Float#magnitude" do
+ it_behaves_like :float_abs, :magnitude
+end
diff --git a/spec/ruby/core/float/minus_spec.rb b/spec/ruby/core/float/minus_spec.rb
new file mode 100644
index 0000000000..a4281a397b
--- /dev/null
+++ b/spec/ruby/core/float/minus_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Float#-" do
+ it_behaves_like :float_arithmetic_exception_in_coerce, :-
+
+ it "returns self minus other" do
+ (9_237_212.5280 - 5_280).should be_close(9231932.528, TOLERANCE)
+ (2_560_496.1691 - bignum_value).should be_close(-18446744073706991616.0, TOLERANCE)
+ (5.5 - 5.5).should be_close(0.0,TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/float/modulo_spec.rb b/spec/ruby/core/float/modulo_spec.rb
new file mode 100644
index 0000000000..8ae80a0b05
--- /dev/null
+++ b/spec/ruby/core/float/modulo_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/modulo'
+
+describe "Float#%" do
+ it_behaves_like :float_modulo, :%
+end
+
+describe "Float#modulo" do
+ it_behaves_like :float_modulo, :modulo
+end
diff --git a/spec/ruby/core/float/multiply_spec.rb b/spec/ruby/core/float/multiply_spec.rb
new file mode 100644
index 0000000000..edaaba7e61
--- /dev/null
+++ b/spec/ruby/core/float/multiply_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Float#*" do
+ it_behaves_like :float_arithmetic_exception_in_coerce, :*
+
+ it "returns self multiplied by other" do
+ (4923.98221 * 2).should be_close(9847.96442, TOLERANCE)
+ (6712.5 * 0.25).should be_close(1678.125, TOLERANCE)
+ (256.4096 * bignum_value).should be_close(4729922269242236862464.0, TOLERANCE)
+ end
+
+ it "raises a TypeError when given a non-Numeric" do
+ -> { 13.0 * "10" }.should.raise(TypeError)
+ -> { 13.0 * :symbol }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/float/nan_spec.rb b/spec/ruby/core/float/nan_spec.rb
new file mode 100644
index 0000000000..c1043ef21b
--- /dev/null
+++ b/spec/ruby/core/float/nan_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Float#nan?" do
+ it "returns true if self is not a valid IEEE floating-point number" do
+ 0.0.should_not.nan?
+ -1.5.should_not.nan?
+ nan_value.should.nan?
+ end
+end
diff --git a/spec/ruby/core/float/negative_spec.rb b/spec/ruby/core/float/negative_spec.rb
new file mode 100644
index 0000000000..484e636adb
--- /dev/null
+++ b/spec/ruby/core/float/negative_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Float#negative?" do
+ describe "on positive numbers" do
+ it "returns false" do
+ 0.1.negative?.should == false
+ end
+ end
+
+ describe "on zero" do
+ it "returns false" do
+ 0.0.negative?.should == false
+ end
+ end
+
+ describe "on negative zero" do
+ it "returns false" do
+ -0.0.negative?.should == false
+ end
+ end
+
+ describe "on negative numbers" do
+ it "returns true" do
+ -0.1.negative?.should == true
+ end
+ end
+
+ describe "on NaN" do
+ it "returns false" do
+ nan_value.negative?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/float/next_float_spec.rb b/spec/ruby/core/float/next_float_spec.rb
new file mode 100644
index 0000000000..59892be343
--- /dev/null
+++ b/spec/ruby/core/float/next_float_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+
+describe "Float#next_float" do
+ it "returns a float the smallest possible step greater than the receiver" do
+ barely_positive = 0.0.next_float
+ barely_positive.should == 0.0.next_float
+
+ barely_positive.should > 0.0
+ barely_positive.should < barely_positive.next_float
+
+ midpoint = barely_positive / 2
+ [0.0, barely_positive].should.include? midpoint
+ end
+
+ it "returns Float::INFINITY for Float::INFINITY" do
+ Float::INFINITY.next_float.should == Float::INFINITY
+ end
+
+ it "steps directly between MAX and INFINITY" do
+ (-Float::INFINITY).next_float.should == -Float::MAX
+ Float::MAX.next_float.should == Float::INFINITY
+ end
+
+ it "steps directly between 1.0 and 1.0 + EPSILON" do
+ 1.0.next_float.should == 1.0 + Float::EPSILON
+ end
+
+ it "steps directly between -1.0 and -1.0 + EPSILON/2" do
+ (-1.0).next_float.should == -1.0 + Float::EPSILON/2
+ end
+
+ it "reverses the effect of prev_float for all Floats except INFINITY and +0.0" do
+ num = -rand
+ num.prev_float.next_float.should == num
+ end
+
+ it "returns negative zero when stepping upward from just below zero" do
+ x = (-0.0).prev_float.next_float
+ (1/x).should == -Float::INFINITY
+ end
+
+ it "gives the same result for -0.0 as for +0.0" do
+ (-0.0).next_float.should == (0.0).next_float
+ end
+
+ it "returns NAN if NAN was the receiver" do
+ Float::NAN.next_float.should.nan?
+ end
+end
diff --git a/spec/ruby/core/float/numerator_spec.rb b/spec/ruby/core/float/numerator_spec.rb
new file mode 100644
index 0000000000..53b32fdd3d
--- /dev/null
+++ b/spec/ruby/core/float/numerator_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "Float#numerator" do
+ before :all do
+ @numbers = [
+ 29871.2722891,
+ 999.1**99.928888,
+ -72628191273.22,
+ 29282.2827,
+ -2927.00091,
+ 12.0,
+ Float::MAX,
+ ]
+ end
+
+ it "converts self to a Rational object then returns its numerator" do
+ @numbers.each do |number|
+ number.infinite?.should == nil
+ number.numerator.should == Rational(number).numerator
+ end
+ end
+
+ it "returns 0 for 0.0" do
+ 0.0.numerator.should == 0
+ end
+
+ it "returns NaN for NaN" do
+ nan_value.numerator.nan?.should == true
+ end
+
+ it "returns Infinity for Infinity" do
+ infinity_value.numerator.infinite?.should == 1
+ end
+
+ it "returns -Infinity for -Infinity" do
+ (-infinity_value).numerator.infinite?.should == -1
+ end
+
+end
diff --git a/spec/ruby/core/float/phase_spec.rb b/spec/ruby/core/float/phase_spec.rb
new file mode 100644
index 0000000000..2aa84024b4
--- /dev/null
+++ b/spec/ruby/core/float/phase_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arg'
+
+describe "Float#phase" do
+ it_behaves_like :float_arg, :phase
+end
diff --git a/spec/ruby/core/float/plus_spec.rb b/spec/ruby/core/float/plus_spec.rb
new file mode 100644
index 0000000000..e3e19d7f39
--- /dev/null
+++ b/spec/ruby/core/float/plus_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Float#+" do
+ it_behaves_like :float_arithmetic_exception_in_coerce, :+
+
+ it "returns self plus other" do
+ (491.213 + 2).should be_close(493.213, TOLERANCE)
+ (9.99 + bignum_value).should be_close(18446744073709551616.0, TOLERANCE)
+ (1001.99 + 5.219).should be_close(1007.209, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/float/positive_spec.rb b/spec/ruby/core/float/positive_spec.rb
new file mode 100644
index 0000000000..aa87c03151
--- /dev/null
+++ b/spec/ruby/core/float/positive_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Float#positive?" do
+ describe "on positive numbers" do
+ it "returns true" do
+ 0.1.positive?.should == true
+ end
+ end
+
+ describe "on zero" do
+ it "returns false" do
+ 0.0.positive?.should == false
+ end
+ end
+
+ describe "on negative zero" do
+ it "returns false" do
+ -0.0.positive?.should == false
+ end
+ end
+
+ describe "on negative numbers" do
+ it "returns false" do
+ -0.1.positive?.should == false
+ end
+ end
+
+ describe "on NaN" do
+ it "returns false" do
+ nan_value.positive?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/float/prev_float_spec.rb b/spec/ruby/core/float/prev_float_spec.rb
new file mode 100644
index 0000000000..2d1f136254
--- /dev/null
+++ b/spec/ruby/core/float/prev_float_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+
+describe "Float#prev_float" do
+ it "returns a float the smallest possible step smaller than the receiver" do
+ barely_negative = 0.0.prev_float
+ barely_negative.should == 0.0.prev_float
+
+ barely_negative.should < 0.0
+ barely_negative.should > barely_negative.prev_float
+
+ midpoint = barely_negative / 2
+ [0.0, barely_negative].should.include? midpoint
+ end
+
+ it "returns -Float::INFINITY for -Float::INFINITY" do
+ (-Float::INFINITY).prev_float.should == -Float::INFINITY
+ end
+
+ it "steps directly between MAX and INFINITY" do
+ Float::INFINITY.prev_float.should == Float::MAX
+ (-Float::MAX).prev_float.should == -Float::INFINITY
+ end
+
+ it "steps directly between 1.0 and 1.0 - EPSILON/2" do
+ 1.0.prev_float.should == 1.0 - Float::EPSILON/2
+ end
+
+ it "steps directly between -1.0 and -1.0 - EPSILON" do
+ (-1.0).prev_float.should == -1.0 - Float::EPSILON
+ end
+
+ it "reverses the effect of next_float for all Floats except -INFINITY and -0.0" do
+ num = rand
+ num.next_float.prev_float.should == num
+ end
+
+ it "returns positive zero when stepping downward from just above zero" do
+ x = 0.0.next_float.prev_float
+ (1/x).should == Float::INFINITY
+ end
+
+ it "gives the same result for -0.0 as for +0.0" do
+ (0.0).prev_float.should == (-0.0).prev_float
+ end
+
+ it "returns NAN if NAN was the receiver" do
+ Float::NAN.prev_float.should.nan?
+ end
+end
diff --git a/spec/ruby/core/float/quo_spec.rb b/spec/ruby/core/float/quo_spec.rb
new file mode 100644
index 0000000000..b5c64f9d87
--- /dev/null
+++ b/spec/ruby/core/float/quo_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/quo'
+
+describe "Float#quo" do
+ it_behaves_like :float_quo, :quo
+end
diff --git a/spec/ruby/core/float/rationalize_spec.rb b/spec/ruby/core/float/rationalize_spec.rb
new file mode 100644
index 0000000000..fa8fb5c841
--- /dev/null
+++ b/spec/ruby/core/float/rationalize_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "Float#rationalize" do
+ it "returns self as a simplified Rational with no argument" do
+ (3382729202.92822).rationalize.should == Rational(4806858197361, 1421)
+ end
+
+ # FIXME: These specs need reviewing by somebody familiar with the
+ # algorithm used by #rationalize
+ it "simplifies self to the degree specified by a Rational argument" do
+ f = 0.3
+ f.rationalize(Rational(1,10)).should == Rational(1,3)
+ f.rationalize(Rational(-1,10)).should == Rational(1,3)
+
+ f = -f
+ f.rationalize(Rational(1,10)).should == Rational(-1,3)
+ f.rationalize(Rational(-1,10)).should == Rational(-1,3)
+
+ end
+
+ it "simplifies self to the degree specified by a Float argument" do
+ f = 0.3
+ f.rationalize(0.05).should == Rational(1,3)
+ f.rationalize(0.001).should == Rational(3, 10)
+
+ f = -f
+ f.rationalize(0.05).should == Rational(-1,3)
+ f.rationalize(0.001).should == Rational(-3,10)
+ end
+
+ it "raises a FloatDomainError for Infinity" do
+ -> {infinity_value.rationalize}.should.raise(FloatDomainError)
+ end
+
+ it "raises a FloatDomainError for NaN" do
+ -> { nan_value.rationalize }.should.raise(FloatDomainError)
+ end
+
+ it "raises ArgumentError when passed more than one argument" do
+ -> { 0.3.rationalize(0.1, 0.1) }.should.raise(ArgumentError)
+ -> { 0.3.rationalize(0.1, 0.1, 2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/float/round_spec.rb b/spec/ruby/core/float/round_spec.rb
new file mode 100644
index 0000000000..63c1d5689c
--- /dev/null
+++ b/spec/ruby/core/float/round_spec.rb
@@ -0,0 +1,208 @@
+require_relative '../../spec_helper'
+
+describe "Float#round" do
+ it "returns the nearest Integer" do
+ 5.5.round.should == 6
+ 0.4.round.should == 0
+ 0.6.round.should == 1
+ -1.4.round.should == -1
+ -2.8.round.should == -3
+ 0.0.round.should == 0
+ end
+
+ it "returns the nearest Integer for Float near the limit" do
+ 0.49999999999999994.round.should == 0
+ -0.49999999999999994.round.should == 0
+ end
+
+ it "raises FloatDomainError for exceptional values" do
+ -> { (+infinity_value).round }.should.raise(FloatDomainError)
+ -> { (-infinity_value).round }.should.raise(FloatDomainError)
+ -> { nan_value.round }.should.raise(FloatDomainError)
+ end
+
+ it "rounds self to an optionally given precision" do
+ 5.5.round(0).should.eql?(6)
+ 5.7.round(1).should.eql?(5.7)
+ 1.2345678.round(2).should == 1.23
+ 123456.78.round(-2).should.eql?(123500) # rounded up
+ -123456.78.round(-2).should.eql?(-123500)
+ 12.345678.round(3.999).should == 12.346
+ end
+
+ it "correctly rounds exact floats with a numerous digits in a fraction part" do
+ 0.8241000000000004.round(10).should == 0.8241
+ 0.8241000000000002.round(10).should == 0.8241
+ end
+
+ it "returns zero when passed a negative argument with magnitude greater than magnitude of the whole number portion of the Float" do
+ 0.8346268.round(-1).should.eql?(0)
+ end
+
+ it "raises a TypeError when its argument can not be converted to an Integer" do
+ -> { 1.0.round("4") }.should.raise(TypeError)
+ -> { 1.0.round(nil) }.should.raise(TypeError)
+ end
+
+ it "raises FloatDomainError for exceptional values when passed a non-positive precision" do
+ -> { Float::INFINITY.round( 0) }.should.raise(FloatDomainError)
+ -> { Float::INFINITY.round(-2) }.should.raise(FloatDomainError)
+ -> { (-Float::INFINITY).round( 0) }.should.raise(FloatDomainError)
+ -> { (-Float::INFINITY).round(-2) }.should.raise(FloatDomainError)
+ end
+
+ it "raises RangeError for NAN when passed a non-positive precision" do
+ -> { Float::NAN.round(0) }.should.raise(RangeError)
+ -> { Float::NAN.round(-2) }.should.raise(RangeError)
+ end
+
+ it "returns self for exceptional values when passed a non-negative precision" do
+ Float::INFINITY.round(2).should == Float::INFINITY
+ (-Float::INFINITY).round(2).should == -Float::INFINITY
+ Float::NAN.round(2).should.nan?
+ end
+
+ # redmine:5227
+ it "works for corner cases" do
+ 42.0.round(308).should.eql?(42.0)
+ 1.0e307.round(2).should.eql?(1.0e307)
+ 120.0.round(-1).should.eql?(120)
+ end
+
+ # redmine:5271
+ it "returns rounded values for big argument" do
+ 0.42.round(2.0**30).should == 0.42
+ end
+
+ it "returns rounded values for not so big argument" do
+ 0.42.round(2.0**23).should == 0.42
+ end
+
+ it "returns big values rounded to nearest" do
+ +2.5e20.round(-20).should.eql?( +3 * 10 ** 20 )
+ -2.5e20.round(-20).should.eql?( -3 * 10 ** 20 )
+ end
+
+ # redmine #5272
+ it "returns rounded values for big values" do
+ +2.4e20.round(-20).should.eql?( +2 * 10 ** 20 )
+ -2.4e20.round(-20).should.eql?( -2 * 10 ** 20 )
+ +2.5e200.round(-200).should.eql?( +3 * 10 ** 200 )
+ +2.4e200.round(-200).should.eql?( +2 * 10 ** 200 )
+ -2.5e200.round(-200).should.eql?( -3 * 10 ** 200 )
+ -2.4e200.round(-200).should.eql?( -2 * 10 ** 200 )
+ end
+
+ it "returns different rounded values depending on the half option" do
+ 2.5.round(half: nil).should.eql?(3)
+ 2.5.round(half: :up).should.eql?(3)
+ 2.5.round(half: :down).should.eql?(2)
+ 2.5.round(half: :even).should.eql?(2)
+ 3.5.round(half: nil).should.eql?(4)
+ 3.5.round(half: :up).should.eql?(4)
+ 3.5.round(half: :down).should.eql?(3)
+ 3.5.round(half: :even).should.eql?(4)
+ (-2.5).round(half: nil).should.eql?(-3)
+ (-2.5).round(half: :up).should.eql?(-3)
+ (-2.5).round(half: :down).should.eql?(-2)
+ (-2.5).round(half: :even).should.eql?(-2)
+ end
+
+ it "rounds self to an optionally given precision with a half option" do
+ 5.55.round(1, half: nil).should.eql?(5.6)
+ 5.55.round(1, half: :up).should.eql?(5.6)
+ 5.55.round(1, half: :down).should.eql?(5.5)
+ 5.55.round(1, half: :even).should.eql?(5.6)
+ -5.55.round(1, half: nil).should.eql?(-5.6)
+ -5.55.round(1, half: :up).should.eql?(-5.6)
+ -5.55.round(1, half: :down).should.eql?(-5.5)
+ -5.55.round(1, half: :even).should.eql?(-5.6)
+ end
+
+ it "preserves cases where neighbouring floating pointer number increase the decimal places" do
+ 4.8100000000000005.round(5, half: nil).should.eql?(4.81)
+ 4.8100000000000005.round(5, half: :up).should.eql?(4.81)
+ 4.8100000000000005.round(5, half: :down).should.eql?(4.81)
+ 4.8100000000000005.round(5, half: :even).should.eql?(4.81)
+ -4.8100000000000005.round(5, half: nil).should.eql?(-4.81)
+ -4.8100000000000005.round(5, half: :up).should.eql?(-4.81)
+ -4.8100000000000005.round(5, half: :down).should.eql?(-4.81)
+ -4.8100000000000005.round(5, half: :even).should.eql?(-4.81)
+ 4.81.round(5, half: nil).should.eql?(4.81)
+ 4.81.round(5, half: :up).should.eql?(4.81)
+ 4.81.round(5, half: :down).should.eql?(4.81)
+ 4.81.round(5, half: :even).should.eql?(4.81)
+ -4.81.round(5, half: nil).should.eql?(-4.81)
+ -4.81.round(5, half: :up).should.eql?(-4.81)
+ -4.81.round(5, half: :down).should.eql?(-4.81)
+ -4.81.round(5, half: :even).should.eql?(-4.81)
+ 4.809999999999999.round(5, half: nil).should.eql?(4.81)
+ 4.809999999999999.round(5, half: :up).should.eql?(4.81)
+ 4.809999999999999.round(5, half: :down).should.eql?(4.81)
+ 4.809999999999999.round(5, half: :even).should.eql?(4.81)
+ -4.809999999999999.round(5, half: nil).should.eql?(-4.81)
+ -4.809999999999999.round(5, half: :up).should.eql?(-4.81)
+ -4.809999999999999.round(5, half: :down).should.eql?(-4.81)
+ -4.809999999999999.round(5, half: :even).should.eql?(-4.81)
+ end
+
+ # These numbers are neighbouring floating point numbers round a
+ # precise value. They test that the rounding modes work correctly
+ # round that value and precision is not lost which might cause
+ # incorrect results.
+ it "does not lose precision during the rounding process" do
+ 767573.1875850001.round(5, half: nil).should.eql?(767573.18759)
+ 767573.1875850001.round(5, half: :up).should.eql?(767573.18759)
+ 767573.1875850001.round(5, half: :down).should.eql?(767573.18759)
+ 767573.1875850001.round(5, half: :even).should.eql?(767573.18759)
+ -767573.1875850001.round(5, half: nil).should.eql?(-767573.18759)
+ -767573.1875850001.round(5, half: :up).should.eql?(-767573.18759)
+ -767573.1875850001.round(5, half: :down).should.eql?(-767573.18759)
+ -767573.1875850001.round(5, half: :even).should.eql?(-767573.18759)
+ 767573.187585.round(5, half: nil).should.eql?(767573.18759)
+ 767573.187585.round(5, half: :up).should.eql?(767573.18759)
+ 767573.187585.round(5, half: :down).should.eql?(767573.18758)
+ 767573.187585.round(5, half: :even).should.eql?(767573.18758)
+ -767573.187585.round(5, half: nil).should.eql?(-767573.18759)
+ -767573.187585.round(5, half: :up).should.eql?(-767573.18759)
+ -767573.187585.round(5, half: :down).should.eql?(-767573.18758)
+ -767573.187585.round(5, half: :even).should.eql?(-767573.18758)
+ 767573.1875849998.round(5, half: nil).should.eql?(767573.18758)
+ 767573.1875849998.round(5, half: :up).should.eql?(767573.18758)
+ 767573.1875849998.round(5, half: :down).should.eql?(767573.18758)
+ 767573.1875849998.round(5, half: :even).should.eql?(767573.18758)
+ -767573.1875849998.round(5, half: nil).should.eql?(-767573.18758)
+ -767573.1875849998.round(5, half: :up).should.eql?(-767573.18758)
+ -767573.1875849998.round(5, half: :down).should.eql?(-767573.18758)
+ -767573.1875849998.round(5, half: :even).should.eql?(-767573.18758)
+ end
+
+ it "raises FloatDomainError for exceptional values with a half option" do
+ -> { (+infinity_value).round(half: :up) }.should.raise(FloatDomainError)
+ -> { (-infinity_value).round(half: :down) }.should.raise(FloatDomainError)
+ -> { nan_value.round(half: :even) }.should.raise(FloatDomainError)
+ end
+
+ it "raise for a non-existent round mode" do
+ -> { 14.2.round(half: :nonsense) }.should.raise(ArgumentError, "invalid rounding mode: nonsense")
+ end
+
+ describe "when 0.0 is given" do
+ it "returns self for positive ndigits" do
+ (0.0).round(5).inspect.should == "0.0"
+ (-0.0).round(1).inspect.should == "-0.0"
+ end
+
+ it "returns 0 for 0 or undefined ndigits" do
+ (0.0).round.should == 0
+ (-0.0).round(0).should == 0
+ (0.0).round(half: :up).should == 0
+ end
+
+ it "returns 0 for negative ndigits" do
+ (0.0).round(-1).should == 0
+ (-0.0).round(-1).should == 0
+ (0.0).round(-1, half: :up).should == 0
+ end
+ end
+end
diff --git a/spec/ruby/core/float/shared/abs.rb b/spec/ruby/core/float/shared/abs.rb
new file mode 100644
index 0000000000..ab21480e24
--- /dev/null
+++ b/spec/ruby/core/float/shared/abs.rb
@@ -0,0 +1,21 @@
+require_relative '../../../spec_helper'
+
+describe :float_abs, shared: true do
+ it "returns the absolute value" do
+ -99.1.send(@method).should be_close(99.1, TOLERANCE)
+ 4.5.send(@method).should be_close(4.5, TOLERANCE)
+ 0.0.send(@method).should be_close(0.0, TOLERANCE)
+ end
+
+ it "returns 0.0 if -0.0" do
+ (-0.0).send(@method).should be_positive_zero
+ end
+
+ it "returns Infinity if -Infinity" do
+ (-infinity_value).send(@method).infinite?.should == 1
+ end
+
+ it "returns NaN if NaN" do
+ nan_value.send(@method).nan?.should == true
+ end
+end
diff --git a/spec/ruby/core/float/shared/arg.rb b/spec/ruby/core/float/shared/arg.rb
new file mode 100644
index 0000000000..de0024313d
--- /dev/null
+++ b/spec/ruby/core/float/shared/arg.rb
@@ -0,0 +1,36 @@
+describe :float_arg, shared: true do
+ it "returns NaN if NaN" do
+ f = nan_value
+ f.send(@method).nan?.should == true
+ end
+
+ it "returns self if NaN" do
+ f = nan_value
+ f.send(@method).should.equal?(f)
+ end
+
+ it "returns 0 if positive" do
+ 1.0.send(@method).should == 0
+ end
+
+ it "returns 0 if +0.0" do
+ 0.0.send(@method).should == 0
+ end
+
+ it "returns 0 if +Infinity" do
+ infinity_value.send(@method).should == 0
+ end
+
+ it "returns Pi if negative" do
+ (-1.0).send(@method).should == Math::PI
+ end
+
+ # This was established in r23960
+ it "returns Pi if -0.0" do
+ (-0.0).send(@method).should == Math::PI
+ end
+
+ it "returns Pi if -Infinity" do
+ (-infinity_value).send(@method).should == Math::PI
+ end
+end
diff --git a/spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb b/spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb
new file mode 100644
index 0000000000..bd3bf9019f
--- /dev/null
+++ b/spec/ruby/core/float/shared/arithmetic_exception_in_coerce.rb
@@ -0,0 +1,11 @@
+require_relative '../fixtures/classes'
+
+describe :float_arithmetic_exception_in_coerce, shared: true do
+ it "does not rescue exception raised in other#coerce" do
+ b = mock("numeric with failed #coerce")
+ b.should_receive(:coerce).and_raise(FloatSpecs::CoerceError)
+
+ # e.g. 1.0 > b
+ -> { 1.0.send(@method, b) }.should.raise(FloatSpecs::CoerceError)
+ end
+end
diff --git a/spec/ruby/core/float/shared/comparison_exception_in_coerce.rb b/spec/ruby/core/float/shared/comparison_exception_in_coerce.rb
new file mode 100644
index 0000000000..eec5d8daf9
--- /dev/null
+++ b/spec/ruby/core/float/shared/comparison_exception_in_coerce.rb
@@ -0,0 +1,11 @@
+require_relative '../fixtures/classes'
+
+describe :float_comparison_exception_in_coerce, shared: true do
+ it "does not rescue exception raised in other#coerce" do
+ b = mock("numeric with failed #coerce")
+ b.should_receive(:coerce).and_raise(FloatSpecs::CoerceError)
+
+ # e.g. 1.0 > b
+ -> { 1.0.send(@method, b) }.should.raise(FloatSpecs::CoerceError)
+ end
+end
diff --git a/spec/ruby/core/float/shared/equal.rb b/spec/ruby/core/float/shared/equal.rb
new file mode 100644
index 0000000000..4d524e1cf2
--- /dev/null
+++ b/spec/ruby/core/float/shared/equal.rb
@@ -0,0 +1,38 @@
+describe :float_equal, shared: true do
+ it "returns true if self has the same value as other" do
+ 1.0.send(@method, 1).should == true
+ 2.71828.send(@method, 1.428).should == false
+ -4.2.send(@method, 4.2).should == false
+ end
+
+ it "calls 'other == self' if coercion fails" do
+ x = mock('other')
+ def x.==(other)
+ 2.0 == other
+ end
+
+ 1.0.send(@method, x).should == false
+ 2.0.send(@method, x).should == true
+ end
+
+ it "returns false if one side is NaN" do
+ [1.0, 42, bignum_value].each { |n|
+ (nan_value.send(@method, n)).should == false
+ (n.send(@method, nan_value)).should == false
+ }
+ end
+
+ it "handles positive infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ (infinity_value.send(@method, n)).should == false
+ (n.send(@method, infinity_value)).should == false
+ }
+ end
+
+ it "handles negative infinity" do
+ [1.0, 42, bignum_value].each { |n|
+ ((-infinity_value).send(@method, n)).should == false
+ (n.send(@method, -infinity_value)).should == false
+ }
+ end
+end
diff --git a/spec/ruby/core/float/shared/modulo.rb b/spec/ruby/core/float/shared/modulo.rb
new file mode 100644
index 0000000000..1efee1476d
--- /dev/null
+++ b/spec/ruby/core/float/shared/modulo.rb
@@ -0,0 +1,48 @@
+describe :float_modulo, shared: true do
+ it "returns self modulo other" do
+ 6543.21.send(@method, 137).should be_close(104.21, TOLERANCE)
+ 5667.19.send(@method, bignum_value).should be_close(5667.19, TOLERANCE)
+ 6543.21.send(@method, 137.24).should be_close(92.9299999999996, TOLERANCE)
+
+ -1.0.send(@method, 1).should == 0
+ end
+
+ it "returns self when modulus is +Infinity" do
+ 4.2.send(@method, Float::INFINITY).should == 4.2
+ end
+
+ it "returns -Infinity when modulus is -Infinity" do
+ 4.2.send(@method, -Float::INFINITY).should == -Float::INFINITY
+ end
+
+ it "returns NaN when called on NaN or Infinities" do
+ Float::NAN.send(@method, 42).should.nan?
+ Float::INFINITY.send(@method, 42).should.nan?
+ (-Float::INFINITY).send(@method, 42).should.nan?
+ end
+
+ it "returns NaN when modulus is NaN" do
+ 4.2.send(@method, Float::NAN).should.nan?
+ end
+
+ it "returns -0.0 when called on -0.0 with a non zero modulus" do
+ r = (-0.0).send(@method, 42)
+ r.should == 0
+ (1/r).should < 0
+
+ r = (-0.0).send(@method, Float::INFINITY)
+ r.should == 0
+ (1/r).should < 0
+ end
+
+ it "tries to coerce the modulus" do
+ obj = mock("modulus")
+ obj.should_receive(:coerce).with(1.25).and_return([1.25, 0.5])
+ (1.25 % obj).should == 0.25
+ end
+
+ it "raises a ZeroDivisionError if other is zero" do
+ -> { 1.0.send(@method, 0) }.should.raise(ZeroDivisionError)
+ -> { 1.0.send(@method, 0.0) }.should.raise(ZeroDivisionError)
+ end
+end
diff --git a/spec/ruby/core/float/shared/quo.rb b/spec/ruby/core/float/shared/quo.rb
new file mode 100644
index 0000000000..930187aaf7
--- /dev/null
+++ b/spec/ruby/core/float/shared/quo.rb
@@ -0,0 +1,59 @@
+describe :float_quo, shared: true do
+ it "performs floating-point division between self and an Integer" do
+ 8.9.send(@method, 7).should == 1.2714285714285716
+ end
+
+ it "performs floating-point division between self and an Integer" do
+ 8.9.send(@method, 9999999999999**9).should == 8.900000000008011e-117
+ end
+
+ it "performs floating-point division between self and a Float" do
+ 2827.22.send(@method, 872.111111).should == 3.2418116961704433
+ end
+
+ it "returns NaN when the argument is NaN" do
+ -1819.999999.send(@method, nan_value).nan?.should == true
+ 11109.1981271.send(@method, nan_value).nan?.should == true
+ end
+
+ it "returns Infinity when the argument is 0.0" do
+ 2827.22.send(@method, 0.0).infinite?.should == 1
+ end
+
+ it "returns -Infinity when the argument is 0.0 and self is negative" do
+ -48229.282.send(@method, 0.0).infinite?.should == -1
+ end
+
+ it "returns Infinity when the argument is 0" do
+ 2827.22.send(@method, 0).infinite?.should == 1
+ end
+
+ it "returns -Infinity when the argument is 0 and self is negative" do
+ -48229.282.send(@method, 0).infinite?.should == -1
+ end
+
+ it "returns 0.0 when the argument is Infinity" do
+ 47292.2821.send(@method, infinity_value).should == 0.0
+ end
+
+ it "returns -0.0 when the argument is -Infinity" do
+ 1.9999918.send(@method, -infinity_value).should == -0.0
+ end
+
+ it "performs floating-point division between self and a Rational" do
+ 74620.09.send(@method, Rational(2,3)).should == 111930.135
+ end
+
+ it "performs floating-point division between self and a Complex" do
+ 74620.09.send(@method, Complex(8,2)).should == Complex(
+ 8778.834117647059, -2194.7085294117646)
+ end
+
+ it "raises a TypeError when argument isn't numeric" do
+ -> { 27292.2.send(@method, mock('non-numeric')) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when passed multiple arguments" do
+ -> { 272.221.send(@method, 6,0.2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/float/shared/to_i.rb b/spec/ruby/core/float/shared/to_i.rb
new file mode 100644
index 0000000000..1e6f941467
--- /dev/null
+++ b/spec/ruby/core/float/shared/to_i.rb
@@ -0,0 +1,14 @@
+describe :float_to_i, shared: true do
+ it "returns self truncated to an Integer" do
+ 899.2.send(@method).should.eql?(899)
+ -1.122256e-45.send(@method).should.eql?(0)
+ 5_213_451.9201.send(@method).should.eql?(5213451)
+ 1.233450999123389e+12.send(@method).should.eql?(1233450999123)
+ -9223372036854775808.1.send(@method).should.eql?(-9223372036854775808)
+ 9223372036854775808.1.send(@method).should.eql?(9223372036854775808)
+ end
+
+ it "raises a FloatDomainError for NaN" do
+ -> { nan_value.send(@method) }.should.raise(FloatDomainError)
+ end
+end
diff --git a/spec/ruby/core/float/shared/to_s.rb b/spec/ruby/core/float/shared/to_s.rb
new file mode 100644
index 0000000000..81ffdd9e81
--- /dev/null
+++ b/spec/ruby/core/float/shared/to_s.rb
@@ -0,0 +1,308 @@
+describe :float_to_s, shared: true do
+ it "returns 'NaN' for NaN" do
+ nan_value().send(@method).should == 'NaN'
+ end
+
+ it "returns 'Infinity' for positive infinity" do
+ infinity_value().send(@method).should == 'Infinity'
+ end
+
+ it "returns '-Infinity' for negative infinity" do
+ (-infinity_value()).send(@method).should == '-Infinity'
+ end
+
+ it "returns '0.0' for 0.0" do
+ 0.0.send(@method).should == "0.0"
+ end
+
+ platform_is_not :openbsd do
+ it "emits '-' for -0.0" do
+ -0.0.send(@method).should == "-0.0"
+ end
+ end
+
+ it "emits a '-' for negative values" do
+ -3.14.send(@method).should == "-3.14"
+ end
+
+ it "emits a trailing '.0' for a whole number" do
+ 50.0.send(@method).should == "50.0"
+ end
+
+ it "emits a trailing '.0' for the mantissa in e format" do
+ 1.0e20.send(@method).should == "1.0e+20"
+ end
+
+ it "uses non-e format for a positive value with fractional part having 5 significant figures" do
+ 0.0001.send(@method).should == "0.0001"
+ end
+
+ it "uses non-e format for a negative value with fractional part having 5 significant figures" do
+ -0.0001.send(@method).should == "-0.0001"
+ end
+
+ it "uses e format for a positive value with fractional part having 6 significant figures" do
+ 0.00001.send(@method).should == "1.0e-05"
+ end
+
+ it "uses e format for a negative value with fractional part having 6 significant figures" do
+ -0.00001.send(@method).should == "-1.0e-05"
+ end
+
+ it "uses non-e format for a positive value with whole part having 15 significant figures" do
+ 10000000000000.0.send(@method).should == "10000000000000.0"
+ end
+
+ it "uses non-e format for a negative value with whole part having 15 significant figures" do
+ -10000000000000.0.send(@method).should == "-10000000000000.0"
+ end
+
+ it "uses non-e format for a positive value with whole part having 16 significant figures" do
+ 100000000000000.0.send(@method).should == "100000000000000.0"
+ end
+
+ it "uses non-e format for a negative value with whole part having 16 significant figures" do
+ -100000000000000.0.send(@method).should == "-100000000000000.0"
+ end
+
+ it "uses e format for a positive value with whole part having 18 significant figures" do
+ 10000000000000000.0.send(@method).should == "1.0e+16"
+ end
+
+ it "uses e format for a negative value with whole part having 18 significant figures" do
+ -10000000000000000.0.send(@method).should == "-1.0e+16"
+ end
+
+ it "uses e format for a positive value with whole part having 17 significant figures" do
+ 1000000000000000.0.send(@method).should == "1.0e+15"
+ end
+
+ it "uses e format for a negative value with whole part having 17 significant figures" do
+ -1000000000000000.0.send(@method).should == "-1.0e+15"
+ end
+
+ # #3273
+ it "outputs the minimal, unique form necessary to recreate the value" do
+ value = 0.21611564636388508
+ string = "0.21611564636388508"
+
+ value.send(@method).should == string
+ string.to_f.should == value
+ end
+
+ it "outputs the minimal, unique form to represent the value" do
+ 0.56.send(@method).should == "0.56"
+ end
+
+ describe "matches" do
+ it "random examples in all ranges" do
+ # 50.times do
+ # bytes = (0...8).map { rand(256) }
+ # string = bytes.pack('C8')
+ # float = string.unpack('D').first
+ # puts "#{'%.20g' % float}.send(@method).should == #{float.send(@method).inspect}"
+ # end
+
+ 2.5540217314354050325e+163.send(@method).should == "2.554021731435405e+163"
+ 2.5492588360356597544e-172.send(@method).should == "2.5492588360356598e-172"
+ 1.742770260934704852e-82.send(@method).should == "1.7427702609347049e-82"
+ 6.2108093676180883209e-104.send(@method).should == "6.210809367618088e-104"
+ -3.3448803488331067402e-143.send(@method).should == "-3.3448803488331067e-143"
+ -2.2740074343500832557e-168.send(@method).should == "-2.2740074343500833e-168"
+ 7.0587971678048535732e+191.send(@method).should == "7.058797167804854e+191"
+ -284438.88327586348169.send(@method).should == "-284438.8832758635"
+ 3.953272468476091301e+105.send(@method).should == "3.9532724684760913e+105"
+ -3.6361359552959847853e+100.send(@method).should == "-3.636135955295985e+100"
+ -1.3222325865575206185e-31.send(@method).should == "-1.3222325865575206e-31"
+ 1.1440138916932761366e+130.send(@method).should == "1.1440138916932761e+130"
+ 4.8750891560387561157e-286.send(@method).should == "4.875089156038756e-286"
+ 5.6101113356591453525e-257.send(@method).should == "5.610111335659145e-257"
+ -3.829644279545809575e-100.send(@method).should == "-3.8296442795458096e-100"
+ 1.5342839401396406117e-194.send(@method).should == "1.5342839401396406e-194"
+ 2.2284972755169921402e-144.send(@method).should == "2.228497275516992e-144"
+ 2.1825655917065601737e-61.send(@method).should == "2.1825655917065602e-61"
+ -2.6672271363524338322e-62.send(@method).should == "-2.667227136352434e-62"
+ -1.9257995160119059415e+21.send(@method).should == "-1.925799516011906e+21"
+ -8.9096732962887121718e-198.send(@method).should == "-8.909673296288712e-198"
+ 2.0202075376548644959e-90.send(@method).should == "2.0202075376548645e-90"
+ -7.7341602581786258961e-266.send(@method).should == "-7.734160258178626e-266"
+ 3.5134482598733635046e+98.send(@method).should == "3.5134482598733635e+98"
+ -2.124411722371029134e+154.send(@method).should == "-2.124411722371029e+154"
+ -4.573908787355718687e+110.send(@method).should == "-4.573908787355719e+110"
+ -1.9344425934170969879e-232.send(@method).should == "-1.934442593417097e-232"
+ -1.3274227399979271095e+171.send(@method).should == "-1.3274227399979271e+171"
+ 9.3495270482104442383e-283.send(@method).should == "9.349527048210444e-283"
+ -4.2046059371986483233e+307.send(@method).should == "-4.2046059371986483e+307"
+ 3.6133547278583543004e-117.send(@method).should == "3.613354727858354e-117"
+ 4.9247416523566613499e-08.send(@method).should == "4.9247416523566613e-08"
+ 1.6936145488250064007e-71.send(@method).should == "1.6936145488250064e-71"
+ 2.4455483206829433098e+96.send(@method).should == "2.4455483206829433e+96"
+ 7.9797449851436455384e+124.send(@method).should == "7.979744985143646e+124"
+ -1.3873689634457876774e-129.send(@method).should == "-1.3873689634457877e-129"
+ 3.9761102037533483075e+284.send(@method).should == "3.976110203753348e+284"
+ -4.2819791952139402486e-303.send(@method).should == "-4.28197919521394e-303"
+ -5.7981017546689831298e-116.send(@method).should == "-5.798101754668983e-116"
+ -3.953266497860534199e-28.send(@method).should == "-3.953266497860534e-28"
+ -2.0659852720290440959e-243.send(@method).should == "-2.065985272029044e-243"
+ 8.9670488995878688018e-05.send(@method).should == "8.967048899587869e-05"
+ -1.2317943708113061768e-98.send(@method).should == "-1.2317943708113062e-98"
+ -3.8930768307633080463e+248.send(@method).should == "-3.893076830763308e+248"
+ 6.5854032671803925627e-239.send(@method).should == "6.5854032671803926e-239"
+ 4.6257022188980878952e+177.send(@method).should == "4.625702218898088e+177"
+ -1.9397155125507235603e-187.send(@method).should == "-1.9397155125507236e-187"
+ 8.5752156951245705056e+117.send(@method).should == "8.57521569512457e+117"
+ -2.4784875958162501671e-132.send(@method).should == "-2.4784875958162502e-132"
+ -4.4125691841230058457e-203.send(@method).should == "-4.412569184123006e-203"
+ end
+
+ it "random examples in human ranges" do
+ # 50.times do
+ # formatted = ''
+ # rand(1..3).times do
+ # formatted << rand(10).to_s
+ # end
+ # formatted << '.'
+ # rand(1..9).times do
+ # formatted << rand(10).to_s
+ # end
+ # float = formatted.to_f
+ # puts "#{'%.20f' % float}.send(@method).should == #{float.send(@method).inspect}"
+ # end
+
+ 5.17869899999999994122.send(@method).should == "5.178699"
+ 905.62695729999995819526.send(@method).should == "905.6269573"
+ 62.75999999999999801048.send(@method).should == "62.76"
+ 6.93856795800000014651.send(@method).should == "6.938567958"
+ 4.95999999999999996447.send(@method).should == "4.96"
+ 32.77993899999999882766.send(@method).should == "32.779939"
+ 544.12756779999995160324.send(@method).should == "544.1275678"
+ 66.25801119999999855281.send(@method).should == "66.2580112"
+ 7.90000000000000035527.send(@method).should == "7.9"
+ 5.93100000000000004974.send(@method).should == "5.931"
+ 5.21229313600000043749.send(@method).should == "5.212293136"
+ 503.44173809000000119340.send(@method).should == "503.44173809"
+ 79.26000000000000511591.send(@method).should == "79.26"
+ 8.51524999999999998579.send(@method).should == "8.51525"
+ 174.00000000000000000000.send(@method).should == "174.0"
+ 50.39580000000000126192.send(@method).should == "50.3958"
+ 35.28999999999999914735.send(@method).should == "35.29"
+ 5.43136675399999990788.send(@method).should == "5.431366754"
+ 654.07680000000004838512.send(@method).should == "654.0768"
+ 6.07423700000000010846.send(@method).should == "6.074237"
+ 102.25779799999999397642.send(@method).should == "102.257798"
+ 5.08129999999999970584.send(@method).should == "5.0813"
+ 6.00000000000000000000.send(@method).should == "6.0"
+ 8.30000000000000071054.send(@method).should == "8.3"
+ 32.68345999999999662577.send(@method).should == "32.68346"
+ 581.11170000000004165486.send(@method).should == "581.1117"
+ 76.31342999999999676675.send(@method).should == "76.31343"
+ 438.30826000000001840817.send(@method).should == "438.30826"
+ 482.06631994000002805478.send(@method).should == "482.06631994"
+ 55.92721026899999969828.send(@method).should == "55.927210269"
+ 4.00000000000000000000.send(@method).should == "4.0"
+ 55.86693999999999959982.send(@method).should == "55.86694"
+ 787.98299999999994724931.send(@method).should == "787.983"
+ 5.73810511000000023074.send(@method).should == "5.73810511"
+ 74.51926810000000500622.send(@method).should == "74.5192681"
+ 892.89999999999997726263.send(@method).should == "892.9"
+ 68.27299999999999613465.send(@method).should == "68.273"
+ 904.10000000000002273737.send(@method).should == "904.1"
+ 5.23200000000000020606.send(@method).should == "5.232"
+ 4.09628000000000014325.send(@method).should == "4.09628"
+ 46.05152633699999853434.send(@method).should == "46.051526337"
+ 142.12884990599999923688.send(@method).should == "142.128849906"
+ 3.83057023500000015659.send(@method).should == "3.830570235"
+ 11.81684594699999912848.send(@method).should == "11.816845947"
+ 80.50000000000000000000.send(@method).should == "80.5"
+ 382.18215010000000120272.send(@method).should == "382.1821501"
+ 55.38444606899999911320.send(@method).should == "55.384446069"
+ 5.78000000000000024869.send(@method).should == "5.78"
+ 2.88244999999999995666.send(@method).should == "2.88245"
+ 43.27709999999999723741.send(@method).should == "43.2771"
+ end
+
+ it "random values from divisions" do
+ (1.0 / 7).send(@method).should == "0.14285714285714285"
+
+ # 50.times do
+ # a = rand(10)
+ # b = rand(10)
+ # c = rand(10)
+ # d = rand(10)
+ # expression = "#{a}.#{b} / #{c}.#{d}"
+ # puts " (#{expression}).send(@method).should == #{eval(expression).send(@method).inspect}"
+ # end
+
+ (1.1 / 7.1).send(@method).should == "0.15492957746478875"
+ (6.5 / 8.8).send(@method).should == "0.7386363636363635"
+ (4.8 / 4.3).send(@method).should == "1.1162790697674418"
+ (4.0 / 1.9).send(@method).should == "2.1052631578947367"
+ (9.1 / 0.8).send(@method).should == "11.374999999999998"
+ (5.3 / 7.5).send(@method).should == "0.7066666666666667"
+ (2.8 / 1.8).send(@method).should == "1.5555555555555554"
+ (2.1 / 2.5).send(@method).should == "0.8400000000000001"
+ (3.5 / 6.0).send(@method).should == "0.5833333333333334"
+ (4.6 / 0.3).send(@method).should == "15.333333333333332"
+ (0.6 / 2.4).send(@method).should == "0.25"
+ (1.3 / 9.1).send(@method).should == "0.14285714285714288"
+ (0.3 / 5.0).send(@method).should == "0.06"
+ (5.0 / 4.2).send(@method).should == "1.1904761904761905"
+ (3.0 / 2.0).send(@method).should == "1.5"
+ (6.3 / 2.0).send(@method).should == "3.15"
+ (5.4 / 6.0).send(@method).should == "0.9"
+ (9.6 / 8.1).send(@method).should == "1.1851851851851851"
+ (8.7 / 1.6).send(@method).should == "5.437499999999999"
+ (1.9 / 7.8).send(@method).should == "0.24358974358974358"
+ (0.5 / 2.1).send(@method).should == "0.23809523809523808"
+ (9.3 / 5.8).send(@method).should == "1.6034482758620692"
+ (2.7 / 8.0).send(@method).should == "0.3375"
+ (9.7 / 7.8).send(@method).should == "1.2435897435897436"
+ (8.1 / 2.4).send(@method).should == "3.375"
+ (7.7 / 2.7).send(@method).should == "2.8518518518518516"
+ (7.9 / 1.7).send(@method).should == "4.647058823529412"
+ (6.5 / 8.2).send(@method).should == "0.7926829268292683"
+ (7.8 / 9.6).send(@method).should == "0.8125"
+ (2.2 / 4.6).send(@method).should == "0.47826086956521746"
+ (0.0 / 1.0).send(@method).should == "0.0"
+ (8.3 / 2.9).send(@method).should == "2.8620689655172415"
+ (3.1 / 6.1).send(@method).should == "0.5081967213114754"
+ (2.8 / 7.8).send(@method).should == "0.358974358974359"
+ (8.0 / 0.1).send(@method).should == "80.0"
+ (1.7 / 6.4).send(@method).should == "0.265625"
+ (1.8 / 5.4).send(@method).should == "0.3333333333333333"
+ (8.0 / 5.8).send(@method).should == "1.3793103448275863"
+ (5.2 / 4.1).send(@method).should == "1.2682926829268295"
+ (9.8 / 5.8).send(@method).should == "1.6896551724137934"
+ (5.4 / 9.5).send(@method).should == "0.5684210526315789"
+ (8.4 / 4.9).send(@method).should == "1.7142857142857142"
+ (1.7 / 3.5).send(@method).should == "0.4857142857142857"
+ (1.2 / 5.1).send(@method).should == "0.23529411764705882"
+ (1.4 / 2.0).send(@method).should == "0.7"
+ (4.8 / 8.0).send(@method).should == "0.6"
+ (9.0 / 2.5).send(@method).should == "3.6"
+ (0.2 / 0.6).send(@method).should == "0.33333333333333337"
+ (7.8 / 5.2).send(@method).should == "1.5"
+ (9.5 / 5.5).send(@method).should == "1.7272727272727273"
+ end
+ end
+
+ describe 'encoding' do
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ 1.23.send(@method).encoding.should.equal?(Encoding::US_ASCII)
+ end
+
+ it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do
+ Encoding.default_internal = Encoding::IBM437
+ 5.47.send(@method).encoding.should.equal?(Encoding::US_ASCII)
+ end
+ end
+end
diff --git a/spec/ruby/core/float/to_f_spec.rb b/spec/ruby/core/float/to_f_spec.rb
new file mode 100644
index 0000000000..6677556cd9
--- /dev/null
+++ b/spec/ruby/core/float/to_f_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Float#to_f" do
+ it "returns self" do
+ -500.3.to_f.should == -500.3
+ 267.51.to_f.should == 267.51
+ 1.1412.to_f.should == 1.1412
+ end
+end
diff --git a/spec/ruby/core/float/to_i_spec.rb b/spec/ruby/core/float/to_i_spec.rb
new file mode 100644
index 0000000000..91d84c5fa3
--- /dev/null
+++ b/spec/ruby/core/float/to_i_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+
+describe "Float#to_i" do
+ it_behaves_like :float_to_i, :to_i
+end
diff --git a/spec/ruby/core/float/to_int_spec.rb b/spec/ruby/core/float/to_int_spec.rb
new file mode 100644
index 0000000000..084a58b431
--- /dev/null
+++ b/spec/ruby/core/float/to_int_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+
+describe "Float#to_int" do
+ it_behaves_like :float_to_i, :to_int
+end
diff --git a/spec/ruby/core/float/to_r_spec.rb b/spec/ruby/core/float/to_r_spec.rb
new file mode 100644
index 0000000000..907ff08f27
--- /dev/null
+++ b/spec/ruby/core/float/to_r_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Float#to_r" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/float/to_s_spec.rb b/spec/ruby/core/float/to_s_spec.rb
new file mode 100644
index 0000000000..6727a883f8
--- /dev/null
+++ b/spec/ruby/core/float/to_s_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+
+describe "Float#to_s" do
+ it_behaves_like :float_to_s, :to_s
+end
diff --git a/spec/ruby/core/float/truncate_spec.rb b/spec/ruby/core/float/truncate_spec.rb
new file mode 100644
index 0000000000..1750e3fdbc
--- /dev/null
+++ b/spec/ruby/core/float/truncate_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+
+describe "Float#truncate" do
+ it_behaves_like :float_to_i, :truncate
+
+ it "returns self truncated to an optionally given precision" do
+ 2.1679.truncate(0).should.eql?(2)
+ 7.1.truncate(1).should.eql?(7.1)
+ 214.94.truncate(-1).should.eql?(210)
+ -1.234.truncate(2).should.eql?(-1.23)
+ 5.123812.truncate(4).should.eql?(5.1238)
+ end
+end
diff --git a/spec/ruby/core/float/uminus_spec.rb b/spec/ruby/core/float/uminus_spec.rb
new file mode 100644
index 0000000000..57bae0fb4b
--- /dev/null
+++ b/spec/ruby/core/float/uminus_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+
+describe "Float#-@" do
+ it "negates self" do
+ (2.221.send(:-@)).should be_close(-2.221, TOLERANCE)
+ -2.01.should be_close(-2.01,TOLERANCE)
+ -2_455_999_221.5512.should be_close(-2455999221.5512, TOLERANCE)
+ (--5.5).should be_close(5.5, TOLERANCE)
+ -8.551.send(:-@).should be_close(8.551, TOLERANCE)
+ end
+
+ it "negates self at Float boundaries" do
+ Float::MAX.send(:-@).should be_close(0.0 - Float::MAX, TOLERANCE)
+ Float::MIN.send(:-@).should be_close(0.0 - Float::MIN, TOLERANCE)
+ end
+
+ it "returns negative infinity for positive infinity" do
+ infinity_value.send(:-@).infinite?.should == -1
+ end
+
+ it "returns positive infinity for negative infinity" do
+ (-infinity_value).send(:-@).infinite?.should == 1
+ end
+
+ it "returns NaN for NaN" do
+ nan_value.send(:-@).should.nan?
+ end
+end
diff --git a/spec/ruby/core/float/uplus_spec.rb b/spec/ruby/core/float/uplus_spec.rb
new file mode 100644
index 0000000000..b979b2717a
--- /dev/null
+++ b/spec/ruby/core/float/uplus_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Float#+@" do
+ it "returns the same value with same sign (twos complement)" do
+ 34.56.send(:+@).should == 34.56
+ -34.56.send(:+@).should == -34.56
+ 0.0.send(:+@).should.eql?(0.0)
+ end
+end
diff --git a/spec/ruby/core/float/zero_spec.rb b/spec/ruby/core/float/zero_spec.rb
new file mode 100644
index 0000000000..1f3de27793
--- /dev/null
+++ b/spec/ruby/core/float/zero_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Float#zero?" do
+ it "returns true if self is 0.0" do
+ 0.0.should.zero?
+ 1.0.should_not.zero?
+ -1.0.should_not.zero?
+ end
+end
diff --git a/spec/ruby/core/gc/auto_compact_spec.rb b/spec/ruby/core/gc/auto_compact_spec.rb
new file mode 100644
index 0000000000..33ad1cb56c
--- /dev/null
+++ b/spec/ruby/core/gc/auto_compact_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "GC.auto_compact" do
+ it "can set and get a boolean value" do
+ begin
+ GC.auto_compact = GC.auto_compact
+ rescue NotImplementedError # platform does not support autocompact
+ skip
+ end
+
+ original = GC.auto_compact
+ begin
+ GC.auto_compact = !original
+ rescue NotImplementedError # platform does not support autocompact
+ skip
+ end
+
+ begin
+ GC.auto_compact.should == !original
+ ensure
+ GC.auto_compact = original
+ end
+ end
+end
diff --git a/spec/ruby/core/gc/config_spec.rb b/spec/ruby/core/gc/config_spec.rb
new file mode 100644
index 0000000000..57160f122c
--- /dev/null
+++ b/spec/ruby/core/gc/config_spec.rb
@@ -0,0 +1,97 @@
+require_relative '../../spec_helper'
+
+ruby_version_is "3.4" do
+ describe "GC.config" do
+ context "without arguments" do
+ it "returns a hash of current settings" do
+ GC.config.should.is_a?(Hash)
+ end
+
+ it "includes the name of currently loaded GC implementation as a global key" do
+ GC.config.should.include?(:implementation)
+ GC.config[:implementation].should.is_a?(String)
+ end
+ end
+
+ context "with a hash of options" do
+ it "allows to set GC implementation's options, returning the new config" do
+ config = GC.config({})
+ # Try to find a boolean setting to reliably test changing it.
+ key, _value = config.find { |_k, v| v == true }
+ skip unless key
+
+ GC.config(key => false).should == config.merge(key => false)
+ GC.config[key].should == false
+ GC.config(key => true).should == config
+ GC.config[key].should == true
+ ensure
+ GC.config(config.except(:implementation))
+ end
+
+ it "does not change settings that aren't present in the hash" do
+ previous = GC.config
+ GC.config({})
+ GC.config.should == previous
+ end
+
+ it "ignores unknown keys" do
+ previous = GC.config
+ GC.config(foo: "bar")
+ GC.config.should == previous
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "returns the same as GC.config but without the :implementation key" do
+ previous = GC.config
+ GC.config({}).should == previous.except(:implementation)
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "returns the same as GC.config, including the :implementation key" do
+ previous = GC.config
+ GC.config({}).should == previous
+ end
+ end
+
+ it "raises an ArgumentError if options include global keys" do
+ -> { GC.config(implementation: "default") }.should.raise(ArgumentError, 'Attempting to set read-only key "Implementation"')
+ end
+ end
+
+ context "with a non-hash argument" do
+ it "returns current settings if argument is nil" do
+ GC.config(nil).should == GC.config
+ end
+
+ it "raises ArgumentError for all other arguments" do
+ -> { GC.config([]) }.should.raise(ArgumentError)
+ -> { GC.config("default") }.should.raise(ArgumentError)
+ -> { GC.config(1) }.should.raise(ArgumentError)
+ end
+ end
+
+ guard -> { PlatformGuard.standard? && GC.config[:implementation] == "default" } do
+ context "with default GC implementation on MRI" do
+ before do
+ @default_config = GC.config({})
+ end
+
+ after do
+ GC.config(@default_config.except(:implementation))
+ end
+
+ it "includes :rgengc_allow_full_mark option, true by default" do
+ GC.config.should.include?(:rgengc_allow_full_mark)
+ GC.config[:rgengc_allow_full_mark].should == true
+ end
+
+ it "allows to set :rgengc_allow_full_mark" do
+ # This key maps truthy and falsey values to true and false.
+ GC.config(rgengc_allow_full_mark: nil).should == @default_config.merge(rgengc_allow_full_mark: false)
+ GC.config(rgengc_allow_full_mark: 1.23).should == @default_config.merge(rgengc_allow_full_mark: true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/gc/count_spec.rb b/spec/ruby/core/gc/count_spec.rb
new file mode 100644
index 0000000000..fe20c4ed2b
--- /dev/null
+++ b/spec/ruby/core/gc/count_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "GC.count" do
+ it "returns an integer" do
+ GC.count.should.is_a?(Integer)
+ end
+
+ it "increases as collections are run" do
+ count_before = GC.count
+ i = 0
+ while GC.count <= count_before and i < 10
+ GC.start
+ i += 1
+ end
+ GC.count.should > count_before
+ end
+end
diff --git a/spec/ruby/core/gc/disable_spec.rb b/spec/ruby/core/gc/disable_spec.rb
new file mode 100644
index 0000000000..f89a9d2768
--- /dev/null
+++ b/spec/ruby/core/gc/disable_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+
+describe "GC.disable" do
+ after :each do
+ GC.enable
+ end
+
+ it "returns true if and only if the garbage collection was previously disabled" do
+ GC.enable
+ GC.disable.should == false
+ GC.disable.should == true
+ GC.disable.should == true
+ GC.enable
+ GC.disable.should == false
+ GC.disable.should == true
+ end
+
+end
diff --git a/spec/ruby/core/gc/enable_spec.rb b/spec/ruby/core/gc/enable_spec.rb
new file mode 100644
index 0000000000..ca4488547a
--- /dev/null
+++ b/spec/ruby/core/gc/enable_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "GC.enable" do
+
+ it "returns true if and only if the garbage collection was already disabled" do
+ GC.enable
+ GC.enable.should == false
+ GC.disable
+ GC.enable.should == true
+ GC.enable.should == false
+ end
+
+end
diff --git a/spec/ruby/core/gc/garbage_collect_spec.rb b/spec/ruby/core/gc/garbage_collect_spec.rb
new file mode 100644
index 0000000000..f67f0486c8
--- /dev/null
+++ b/spec/ruby/core/gc/garbage_collect_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "GC#garbage_collect" do
+
+ before :each do
+ @obj = Object.new
+ @obj.extend(GC)
+ end
+
+ it "always returns nil" do
+ @obj.garbage_collect.should == nil
+ @obj.garbage_collect.should == nil
+ end
+
+end
diff --git a/spec/ruby/core/gc/measure_total_time_spec.rb b/spec/ruby/core/gc/measure_total_time_spec.rb
new file mode 100644
index 0000000000..f5377c18fd
--- /dev/null
+++ b/spec/ruby/core/gc/measure_total_time_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "GC.measure_total_time" do
+ before :each do
+ @default = GC.measure_total_time
+ end
+
+ after :each do
+ GC.measure_total_time = @default
+ end
+
+ it "can set and get a boolean value" do
+ original = GC.measure_total_time
+ GC.measure_total_time = !original
+ GC.measure_total_time.should == !original
+ end
+end
diff --git a/spec/ruby/core/gc/profiler/clear_spec.rb b/spec/ruby/core/gc/profiler/clear_spec.rb
new file mode 100644
index 0000000000..95c3d4ed65
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/clear_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.clear" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/gc/profiler/disable_spec.rb b/spec/ruby/core/gc/profiler/disable_spec.rb
new file mode 100644
index 0000000000..74089693eb
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/disable_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.disable" do
+ before do
+ @status = GC::Profiler.enabled?
+ end
+
+ after do
+ @status ? GC::Profiler.enable : GC::Profiler.disable
+ end
+
+ it "disables the profiler" do
+ GC::Profiler.disable
+ GC::Profiler.should_not.enabled?
+ end
+end
diff --git a/spec/ruby/core/gc/profiler/enable_spec.rb b/spec/ruby/core/gc/profiler/enable_spec.rb
new file mode 100644
index 0000000000..313ca3d949
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/enable_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.enable" do
+
+ before do
+ @status = GC::Profiler.enabled?
+ end
+
+ after do
+ @status ? GC::Profiler.enable : GC::Profiler.disable
+ end
+
+ it "enables the profiler" do
+ GC::Profiler.enable
+ GC::Profiler.should.enabled?
+ end
+end
diff --git a/spec/ruby/core/gc/profiler/enabled_spec.rb b/spec/ruby/core/gc/profiler/enabled_spec.rb
new file mode 100644
index 0000000000..cfcd0951f0
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/enabled_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.enabled?" do
+ before do
+ @status = GC::Profiler.enabled?
+ end
+
+ after do
+ @status ? GC::Profiler.enable : GC::Profiler.disable
+ end
+
+ it "reports as enabled when enabled" do
+ GC::Profiler.enable
+ GC::Profiler.enabled?.should == true
+ end
+
+ it "reports as disabled when disabled" do
+ GC::Profiler.disable
+ GC::Profiler.enabled?.should == false
+ end
+end
diff --git a/spec/ruby/core/gc/profiler/report_spec.rb b/spec/ruby/core/gc/profiler/report_spec.rb
new file mode 100644
index 0000000000..732b1d0f51
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/report_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.report" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/gc/profiler/result_spec.rb b/spec/ruby/core/gc/profiler/result_spec.rb
new file mode 100644
index 0000000000..e888f975dc
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/result_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.result" do
+ it "returns a string" do
+ GC::Profiler.result.should.is_a?(String)
+ end
+end
diff --git a/spec/ruby/core/gc/profiler/total_time_spec.rb b/spec/ruby/core/gc/profiler/total_time_spec.rb
new file mode 100644
index 0000000000..a351e922af
--- /dev/null
+++ b/spec/ruby/core/gc/profiler/total_time_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+
+describe "GC::Profiler.total_time" do
+ it "returns an float" do
+ GC::Profiler.total_time.should.is_a?(Float)
+ end
+end
diff --git a/spec/ruby/core/gc/start_spec.rb b/spec/ruby/core/gc/start_spec.rb
new file mode 100644
index 0000000000..c941058969
--- /dev/null
+++ b/spec/ruby/core/gc/start_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "GC.start" do
+ it "always returns nil" do
+ GC.start.should == nil
+ GC.start.should == nil
+ end
+
+ it "accepts keyword arguments" do
+ GC.start(full_mark: true, immediate_sweep: true).should == nil
+ end
+end
diff --git a/spec/ruby/core/gc/stat_spec.rb b/spec/ruby/core/gc/stat_spec.rb
new file mode 100644
index 0000000000..6e4800b3e3
--- /dev/null
+++ b/spec/ruby/core/gc/stat_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+
+describe "GC.stat" do
+ it "returns hash of values" do
+ stat = GC.stat
+ stat.should.is_a?(Hash)
+ stat.keys.should.include?(:count)
+ end
+
+ it "updates the given hash values" do
+ hash = { count: "hello", __other__: "world" }
+ stat = GC.stat(hash)
+
+ stat.should.is_a?(Hash)
+ stat.should.equal? hash
+ stat[:count].should.is_a?(Integer)
+ stat[:__other__].should == "world"
+ end
+
+ it "the values are all Integer since rb_gc_stat() returns size_t" do
+ GC.stat.values.each { |value| value.should.is_a?(Integer) }
+ end
+
+ it "can return a single value" do
+ GC.stat(:count).should.is_a?(Integer)
+ end
+
+ it "increases count after GC is run" do
+ count = GC.stat(:count)
+ GC.start
+ GC.stat(:count).should > count
+ end
+
+ it "increases major_gc_count after GC is run" do
+ count = GC.stat(:major_gc_count)
+ GC.start
+ GC.stat(:major_gc_count).should > count
+ end
+
+ it "provides some number for count" do
+ GC.stat(:count).should.is_a?(Integer)
+ GC.stat[:count].should.is_a?(Integer)
+ end
+
+ it "provides some number for heap_free_slots" do
+ GC.stat(:heap_free_slots).should.is_a?(Integer)
+ GC.stat[:heap_free_slots].should.is_a?(Integer)
+ end
+
+ it "provides some number for total_allocated_objects" do
+ GC.stat(:total_allocated_objects).should.is_a?(Integer)
+ GC.stat[:total_allocated_objects].should.is_a?(Integer)
+ end
+
+ it "raises an error if argument is not nil, a symbol, or a hash" do
+ -> { GC.stat(7) }.should.raise(TypeError, "non-hash or symbol given")
+ end
+
+ it "raises an error if an unknown key is given" do
+ -> { GC.stat(:foo) }.should.raise(ArgumentError, "unknown key: foo")
+ end
+end
diff --git a/spec/ruby/core/gc/stress_spec.rb b/spec/ruby/core/gc/stress_spec.rb
new file mode 100644
index 0000000000..227a795e7f
--- /dev/null
+++ b/spec/ruby/core/gc/stress_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+
+describe "GC.stress" do
+ after :each do
+ # make sure that we never leave these tests in stress enabled GC!
+ GC.stress = false
+ end
+
+ it "returns current status of GC stress mode" do
+ GC.stress.should == false
+ GC.stress = true
+ GC.stress.should == true
+ GC.stress = false
+ GC.stress.should == false
+ end
+end
+
+describe "GC.stress=" do
+ after :each do
+ GC.stress = false
+ end
+
+ it "sets the stress mode" do
+ GC.stress = true
+ GC.stress.should == true
+ end
+end
diff --git a/spec/ruby/core/gc/total_time_spec.rb b/spec/ruby/core/gc/total_time_spec.rb
new file mode 100644
index 0000000000..f10e0ad742
--- /dev/null
+++ b/spec/ruby/core/gc/total_time_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "GC.total_time" do
+ it "returns an Integer" do
+ GC.total_time.should.is_a?(Integer)
+ end
+
+ it "increases as collections are run" do
+ time_before = GC.total_time
+ GC.start
+ GC.total_time.should >= time_before
+ end
+end
diff --git a/spec/ruby/core/hash/allocate_spec.rb b/spec/ruby/core/hash/allocate_spec.rb
new file mode 100644
index 0000000000..a92accc161
--- /dev/null
+++ b/spec/ruby/core/hash/allocate_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "Hash.allocate" do
+ it "returns an instance of Hash" do
+ hsh = Hash.allocate
+ hsh.should.instance_of?(Hash)
+ end
+
+ it "returns a fully-formed instance of Hash" do
+ hsh = Hash.allocate
+ hsh.size.should == 0
+ hsh[:a] = 1
+ hsh.should == { a: 1 }
+ end
+end
diff --git a/spec/ruby/core/hash/any_spec.rb b/spec/ruby/core/hash/any_spec.rb
new file mode 100644
index 0000000000..c26dfabde6
--- /dev/null
+++ b/spec/ruby/core/hash/any_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+
+describe "Hash#any?" do
+ describe 'with no block given' do
+ it "checks if there are any members of a Hash" do
+ empty_hash = {}
+ empty_hash.should_not.any?
+
+ hash_with_members = { 'key' => 'value' }
+ hash_with_members.should.any?
+ end
+ end
+
+ describe 'with a block given' do
+ it 'is false if the hash is empty' do
+ empty_hash = {}
+ empty_hash.any? {|k,v| 1 == 1 }.should == false
+ end
+
+ it 'is true if the block returns true for any member of the hash' do
+ hash_with_members = { 'a' => false, 'b' => false, 'c' => true, 'd' => false }
+ hash_with_members.any? {|k,v| v == true}.should == true
+ end
+
+ it 'is false if the block returns false for all members of the hash' do
+ hash_with_members = { 'a' => false, 'b' => false, 'c' => true, 'd' => false }
+ hash_with_members.any? {|k,v| v == 42}.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/assoc_spec.rb b/spec/ruby/core/hash/assoc_spec.rb
new file mode 100644
index 0000000000..51af51a20f
--- /dev/null
+++ b/spec/ruby/core/hash/assoc_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+
+describe "Hash#assoc" do
+ before :each do
+ @h = {apple: :green, orange: :orange, grape: :green, banana: :yellow}
+ end
+
+ it "returns an Array if the argument is == to a key of the Hash" do
+ @h.assoc(:apple).should.instance_of?(Array)
+ end
+
+ it "returns a 2-element Array if the argument is == to a key of the Hash" do
+ @h.assoc(:grape).size.should == 2
+ end
+
+ it "sets the first element of the Array to the located key" do
+ @h.assoc(:banana).first.should == :banana
+ end
+
+ it "sets the last element of the Array to the value of the located key" do
+ @h.assoc(:banana).last.should == :yellow
+ end
+
+ it "only returns the first matching key-value pair for identity hashes" do
+ # Avoid literal String keys since string literals can be frozen and interned e.g. with --enable-frozen-string-literal
+ h = {}.compare_by_identity
+ k1 = 'pear'.dup
+ h[k1] = :red
+ k2 = 'pear'.dup
+ h[k2] = :green
+ h.size.should == 2
+ h.keys.grep(/pear/).size.should == 2
+ h.assoc('pear').should == ['pear', :red]
+ end
+
+ it "uses #== to compare the argument to the keys" do
+ @h[1.0] = :value
+ 1.should == 1.0
+ @h.assoc(1).should == [1.0, :value]
+ end
+
+ it "returns nil if the argument is not a key of the Hash" do
+ @h.assoc(:green).should == nil
+ end
+
+ it "returns nil if the argument is not a key of the Hash even when there is a default" do
+ Hash.new(42).merge!( foo: :bar ).assoc(42).should == nil
+ Hash.new{|h, k| h[k] = 42}.merge!( foo: :bar ).assoc(42).should == nil
+ end
+end
diff --git a/spec/ruby/core/hash/clear_spec.rb b/spec/ruby/core/hash/clear_spec.rb
new file mode 100644
index 0000000000..beff925a63
--- /dev/null
+++ b/spec/ruby/core/hash/clear_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#clear" do
+ it "removes all key, value pairs" do
+ h = { 1 => 2, 3 => 4 }
+ h.clear.should.equal?(h)
+ h.should == {}
+ end
+
+ it "does not remove default values" do
+ h = Hash.new(5)
+ h.clear
+ h.default.should == 5
+
+ h = { "a" => 100, "b" => 200 }
+ h.default = "Go fish"
+ h.clear
+ h["z"].should == "Go fish"
+ end
+
+ it "does not remove default procs" do
+ h = Hash.new { 5 }
+ h.clear
+ h.default_proc.should_not == nil
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.clear }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.clear }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/clone_spec.rb b/spec/ruby/core/hash/clone_spec.rb
new file mode 100644
index 0000000000..d135a84453
--- /dev/null
+++ b/spec/ruby/core/hash/clone_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "Hash#clone" do
+ it "copies instance variable but not the objects they refer to" do
+ hash = { 'key' => 'value' }
+
+ clone = hash.clone
+
+ clone.should == hash
+ clone.should_not.equal? hash
+ end
+end
diff --git a/spec/ruby/core/hash/compact_spec.rb b/spec/ruby/core/hash/compact_spec.rb
new file mode 100644
index 0000000000..de83dabed3
--- /dev/null
+++ b/spec/ruby/core/hash/compact_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#compact" do
+ before :each do
+ @hash = { truthy: true, false: false, nil: nil, nil => true }
+ @initial_pairs = @hash.dup
+ @compact = { truthy: true, false: false, nil => true }
+ end
+
+ it "returns new object that rejects pair has nil value" do
+ ret = @hash.compact
+ ret.should_not.equal?(@hash)
+ ret.should == @compact
+ end
+
+ it "keeps own pairs" do
+ @hash.compact
+ @hash.should == @initial_pairs
+ end
+
+ it "retains the default value" do
+ hash = Hash.new(1)
+ hash.compact.default.should == 1
+ hash[:a] = 1
+ hash.compact.default.should == 1
+ end
+
+ it "retains the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ hash = Hash.new(&pr)
+ hash.compact.default_proc.should == pr
+ hash[:a] = 1
+ hash.compact.default_proc.should == pr
+ end
+
+ it "retains compare_by_identity flag" do
+ hash = {}.compare_by_identity
+ hash.compact.compare_by_identity?.should == true
+ hash[:a] = 1
+ hash.compact.compare_by_identity?.should == true
+ end
+end
+
+describe "Hash#compact!" do
+ before :each do
+ @hash = { truthy: true, false: false, nil: nil, nil => true }
+ @initial_pairs = @hash.dup
+ @compact = { truthy: true, false: false, nil => true }
+ end
+
+ it "returns self" do
+ @hash.compact!.should.equal?(@hash)
+ end
+
+ it "rejects own pair has nil value" do
+ @hash.compact!
+ @hash.should == @compact
+ end
+
+ context "when each pair does not have nil value" do
+ before :each do
+ @hash.compact!
+ end
+
+ it "returns nil" do
+ @hash.compact!.should == nil
+ end
+ end
+
+ describe "on frozen instance" do
+ before :each do
+ @hash.freeze
+ end
+
+ it "keeps pairs and raises a FrozenError" do
+ ->{ @hash.compact! }.should.raise(FrozenError)
+ @hash.should == @initial_pairs
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/compare_by_identity_spec.rb b/spec/ruby/core/hash/compare_by_identity_spec.rb
new file mode 100644
index 0000000000..1abf9d5666
--- /dev/null
+++ b/spec/ruby/core/hash/compare_by_identity_spec.rb
@@ -0,0 +1,147 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#compare_by_identity" do
+ before :each do
+ @h = {}
+ @idh = {}.compare_by_identity
+ end
+
+ it "causes future comparisons on the receiver to be made by identity" do
+ @h[[1]] = :a
+ @h[[1]].should == :a
+ @h.compare_by_identity
+ @h[[1].dup].should == nil
+ end
+
+ it "rehashes internally so that old keys can be looked up" do
+ h = {}
+ (1..10).each { |k| h[k] = k }
+ o = Object.new
+ def o.hash; 123; end
+ h[o] = 1
+ h.compare_by_identity
+ h[o].should == 1
+ end
+
+ it "returns self" do
+ h = {}
+ h[:foo] = :bar
+ h.compare_by_identity.should.equal? h
+ end
+
+ it "has no effect on an already compare_by_identity hash" do
+ @idh[:foo] = :bar
+ @idh.compare_by_identity.should.equal? @idh
+ @idh.should.compare_by_identity?
+ @idh[:foo].should == :bar
+ end
+
+ it "uses the semantics of BasicObject#equal? to determine key identity" do
+ [1].should_not.equal?([1])
+ @idh[[1]] = :c
+ @idh[[1]] = :d
+ :bar.should.equal?(:bar)
+ @idh[:bar] = :e
+ @idh[:bar] = :f
+ @idh.values.should == [:c, :d, :f]
+ end
+
+ it "uses #equal? semantics, but doesn't actually call #equal? to determine identity" do
+ obj = mock('equal')
+ obj.should_not_receive(:equal?)
+ @idh[:foo] = :glark
+ @idh[obj] = :a
+ @idh[obj].should == :a
+ end
+
+ it "does not call #hash on keys" do
+ key = HashSpecs::ByIdentityKey.new
+ @idh[key] = 1
+ @idh[key].should == 1
+ end
+
+ it "regards #dup'd objects as having different identities" do
+ key = ['foo']
+ @idh[key.dup] = :str
+ @idh[key].should == nil
+ end
+
+ it "regards #clone'd objects as having different identities" do
+ key = ['foo']
+ @idh[key.clone] = :str
+ @idh[key].should == nil
+ end
+
+ it "regards references to the same object as having the same identity" do
+ o = Object.new
+ @h[o] = :o
+ @h[:a] = :a
+ @h[o].should == :o
+ end
+
+ it "raises a FrozenError on frozen hashes" do
+ @h = @h.freeze
+ -> { @h.compare_by_identity }.should.raise(FrozenError)
+ end
+
+ # Behaviour confirmed in https://bugs.ruby-lang.org/issues/1871
+ it "persists over #dups" do
+ @idh['foo'.dup] = :bar
+ @idh['foo'.dup] = :glark
+ @idh.dup.should == @idh
+ @idh.dup.size.should == @idh.size
+ @idh.dup.should.compare_by_identity?
+ end
+
+ it "persists over #clones" do
+ @idh['foo'.dup] = :bar
+ @idh['foo'.dup] = :glark
+ @idh.clone.should == @idh
+ @idh.clone.size.should == @idh.size
+ @idh.dup.should.compare_by_identity?
+ end
+
+ it "does not copy string keys" do
+ foo = 'foo'
+ @idh[foo] = true
+ @idh[foo] = true
+ @idh.size.should == 1
+ @idh.keys.first.should.equal? foo
+ end
+
+ # Check `#[]=` call with a String literal.
+ # Don't use `#+` because with `#+` it's no longer a String literal.
+ #
+ # See https://bugs.ruby-lang.org/issues/12855
+ it "gives different identity for string literals" do
+ eval <<~RUBY
+ # frozen_string_literal: false
+ @idh['foo'] = 1
+ @idh['foo'] = 2
+ RUBY
+ @idh.values.should == [1, 2]
+ @idh.size.should == 2
+ end
+end
+
+describe "Hash#compare_by_identity?" do
+ it "returns false by default" do
+ h = {}
+ h.compare_by_identity?.should == false
+ end
+
+ it "returns true once #compare_by_identity has been invoked on self" do
+ h = {}
+ h.compare_by_identity
+ h.compare_by_identity?.should == true
+ end
+
+ it "returns true when called multiple times on the same ident hash" do
+ h = {}
+ h.compare_by_identity
+ h.compare_by_identity?.should == true
+ h.compare_by_identity?.should == true
+ h.compare_by_identity?.should == true
+ end
+end
diff --git a/spec/ruby/core/hash/constructor_spec.rb b/spec/ruby/core/hash/constructor_spec.rb
new file mode 100644
index 0000000000..c16f373160
--- /dev/null
+++ b/spec/ruby/core/hash/constructor_spec.rb
@@ -0,0 +1,127 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash.[]" do
+ describe "passed zero arguments" do
+ it "returns an empty hash" do
+ Hash[].should == {}
+ end
+ end
+
+ it "creates a Hash; values can be provided as the argument list" do
+ Hash[:a, 1, :b, 2].should == { a: 1, b: 2 }
+ Hash[].should == {}
+ Hash[:a, 1, :b, { c: 2 }].should == { a: 1, b: { c: 2 } }
+ end
+
+ it "creates a Hash; values can be provided as one single hash" do
+ Hash[a: 1, b: 2].should == { a: 1, b: 2 }
+ Hash[{1 => 2, 3 => 4}].should == {1 => 2, 3 => 4}
+ Hash[{}].should == {}
+ end
+
+ describe "passed an array" do
+ it "treats elements that are 2 element arrays as key and value" do
+ Hash[[[:a, :b], [:c, :d]]].should == { a: :b, c: :d }
+ end
+
+ it "treats elements that are 1 element arrays as keys with value nil" do
+ Hash[[[:a]]].should == { a: nil }
+ end
+ end
+
+ # #1000 #1385
+ it "creates a Hash; values can be provided as a list of value-pairs in an array" do
+ Hash[[[:a, 1], [:b, 2]]].should == { a: 1, b: 2 }
+ end
+
+ it "coerces a single argument which responds to #to_ary" do
+ ary = mock('to_ary')
+ ary.should_receive(:to_ary).and_return([[:a, :b]])
+
+ Hash[ary].should == { a: :b }
+ end
+
+ it "raises for elements that are not arrays" do
+ -> {
+ Hash[[:a]]
+ }.should.raise(ArgumentError, "wrong element type Symbol at 0 (expected array)")
+ -> {
+ Hash[[nil]]
+ }.should.raise(ArgumentError, "wrong element type nil at 0 (expected array)")
+ end
+
+ it "raises an ArgumentError for arrays of more than 2 elements" do
+ ->{
+ Hash[[[:a, :b, :c]]]
+ }.should.raise(ArgumentError, "invalid number of elements (3 for 1..2)")
+ end
+
+ it "raises an ArgumentError when passed a list of value-invalid-pairs in an array" do
+ -> {
+ Hash[[[:a, 1], [:b], 42, [:d, 2], [:e, 2, 3], []]]
+ }.should.raise(ArgumentError, "wrong element type Integer at 2 (expected array)")
+ end
+
+ describe "passed a single argument which responds to #to_hash" do
+ it "coerces it and returns a copy" do
+ h = { a: :b, c: :d }
+ to_hash = mock('to_hash')
+ to_hash.should_receive(:to_hash).and_return(h)
+
+ result = Hash[to_hash]
+ result.should == h
+ result.should_not.equal?(h)
+ end
+ end
+
+ it "raises an ArgumentError when passed an odd number of arguments" do
+ -> { Hash[1, 2, 3] }.should.raise(ArgumentError)
+ -> { Hash[1, 2, { 3 => 4 }] }.should.raise(ArgumentError)
+ end
+
+ it "calls to_hash" do
+ obj = mock('x')
+ def obj.to_hash() { 1 => 2, 3 => 4 } end
+ Hash[obj].should == { 1 => 2, 3 => 4 }
+ end
+
+ it "returns an instance of a subclass when passed an Array" do
+ HashSpecs::MyHash[1,2,3,4].should.instance_of?(HashSpecs::MyHash)
+ end
+
+ it "returns instances of subclasses" do
+ HashSpecs::MyHash[].should.instance_of?(HashSpecs::MyHash)
+ end
+
+ it "returns an instance of the class it's called on" do
+ Hash[HashSpecs::MyHash[1, 2]].class.should == Hash
+ HashSpecs::MyHash[Hash[1, 2]].should.instance_of?(HashSpecs::MyHash)
+ end
+
+ it "does not call #initialize on the subclass instance" do
+ HashSpecs::MyInitializerHash[Hash[1, 2]].should.instance_of?(HashSpecs::MyInitializerHash)
+ end
+
+ it "does not retain the default value" do
+ hash = Hash.new(1)
+ Hash[hash].default.should == nil
+ hash[:a] = 1
+ Hash[hash].default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ hash = Hash.new { |h, k| h[k] = [] }
+ Hash[hash].default_proc.should == nil
+ hash[:a] = 1
+ Hash[hash].default_proc.should == nil
+ end
+
+ it "does not retain compare_by_identity flag" do
+ hash = { a: 1 }.compare_by_identity
+ Hash[hash].compare_by_identity?.should == false
+
+ hash = {}.compare_by_identity
+ Hash[hash].compare_by_identity?.should == false
+ end
+end
diff --git a/spec/ruby/core/hash/deconstruct_keys_spec.rb b/spec/ruby/core/hash/deconstruct_keys_spec.rb
new file mode 100644
index 0000000000..f6ff3a2818
--- /dev/null
+++ b/spec/ruby/core/hash/deconstruct_keys_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Hash#deconstruct_keys" do
+ it "returns self" do
+ hash = {a: 1, b: 2}
+
+ hash.deconstruct_keys([:a, :b]).should.equal? hash
+ end
+
+ it "requires one argument" do
+ -> {
+ {a: 1}.deconstruct_keys
+ }.should.raise(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/)
+ end
+
+ it "ignores argument" do
+ hash = {a: 1, b: 2}
+
+ hash.deconstruct_keys([:a]).should == {a: 1, b: 2}
+ hash.deconstruct_keys(0 ).should == {a: 1, b: 2}
+ hash.deconstruct_keys('' ).should == {a: 1, b: 2}
+ end
+end
diff --git a/spec/ruby/core/hash/default_proc_spec.rb b/spec/ruby/core/hash/default_proc_spec.rb
new file mode 100644
index 0000000000..4830e02acd
--- /dev/null
+++ b/spec/ruby/core/hash/default_proc_spec.rb
@@ -0,0 +1,80 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#default_proc" do
+ it "returns the block passed to Hash.new" do
+ h = Hash.new { 'Paris' }
+ p = h.default_proc
+ p.call(1).should == 'Paris'
+ end
+
+ it "returns nil if no block was passed to proc" do
+ {}.default_proc.should == nil
+ end
+end
+
+describe "Hash#default_proc=" do
+ it "replaces the block passed to Hash.new" do
+ h = Hash.new { 'Paris' }
+ h.default_proc = Proc.new { 'Montreal' }
+ p = h.default_proc
+ p.call(1).should == 'Montreal'
+ end
+
+ it "uses :to_proc on its argument" do
+ h = Hash.new { 'Paris' }
+ obj = mock('to_proc')
+ obj.should_receive(:to_proc).and_return(Proc.new { 'Montreal' })
+ (h.default_proc = obj).should.equal?(obj)
+ h[:cool_city].should == 'Montreal'
+ end
+
+ it "overrides the static default" do
+ h = Hash.new(42)
+ h.default_proc = Proc.new { 6 }
+ h.default.should == nil
+ h.default_proc.call.should == 6
+ end
+
+ it "raises an error if passed stuff not convertible to procs" do
+ ->{{}.default_proc = 42}.should.raise(TypeError)
+ end
+
+ it "returns the passed Proc" do
+ new_proc = Proc.new {}
+ ({}.default_proc = new_proc).should.equal?(new_proc)
+ end
+
+ it "clears the default proc if passed nil" do
+ h = Hash.new { 'Paris' }
+ h.default_proc = nil
+ h.default_proc.should == nil
+ h[:city].should == nil
+ end
+
+ it "returns nil if passed nil" do
+ ({}.default_proc = nil).should == nil
+ end
+
+ it "accepts a lambda with an arity of 2" do
+ h = {}
+ -> do
+ h.default_proc = -> a, b { }
+ end.should_not.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a lambda with an arity other than 2" do
+ h = {}
+ -> do
+ h.default_proc = -> a { }
+ end.should.raise(TypeError)
+ -> do
+ h.default_proc = -> a, b, c { }
+ end.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ -> { {}.freeze.default_proc = Proc.new {} }.should.raise(FrozenError)
+ -> { {}.freeze.default_proc = nil }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/default_spec.rb b/spec/ruby/core/hash/default_spec.rb
new file mode 100644
index 0000000000..17ee032e2b
--- /dev/null
+++ b/spec/ruby/core/hash/default_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#default" do
+ it "returns the default value" do
+ h = Hash.new(5)
+ h.default.should == 5
+ h.default(4).should == 5
+ {}.default.should == nil
+ {}.default(4).should == nil
+ end
+
+ it "uses the default proc to compute a default value, passing given key" do
+ h = Hash.new { |*args| args }
+ h.default(nil).should == [h, nil]
+ h.default(5).should == [h, 5]
+ end
+
+ it "calls default proc with nil arg if passed a default proc but no arg" do
+ h = Hash.new { |*args| args }
+ h.default.should == nil
+ end
+end
+
+describe "Hash#default=" do
+ it "sets the default value" do
+ h = {}
+ h.default = 99
+ h.default.should == 99
+ end
+
+ it "unsets the default proc" do
+ [99, nil, -> { 6 }].each do |default|
+ h = Hash.new { 5 }
+ h.default_proc.should_not == nil
+ h.default = default
+ h.default.should == default
+ h.default_proc.should == nil
+ end
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.default = nil }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.default = nil }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/delete_if_spec.rb b/spec/ruby/core/hash/delete_if_spec.rb
new file mode 100644
index 0000000000..3090633d88
--- /dev/null
+++ b/spec/ruby/core/hash/delete_if_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iteration'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Hash#delete_if" do
+ it "yields two arguments: key and value" do
+ all_args = []
+ { 1 => 2, 3 => 4 }.delete_if { |*args| all_args << args }
+ all_args.sort.should == [[1, 2], [3, 4]]
+ end
+
+ it "removes every entry for which block is true and returns self" do
+ h = { a: 1, b: 2, c: 3, d: 4 }
+ h.delete_if { |k,v| v % 2 == 1 }.should.equal?(h)
+ h.should == { b: 2, d: 4 }
+ end
+
+ it "removes all entries if the block is true" do
+ h = { a: 1, b: 2, c: 3 }
+ h.delete_if { |k,v| true }.should.equal?(h)
+ h.should == {}
+ end
+
+ it "processes entries with the same order as each()" do
+ h = { a: 1, b: 2, c: 3, d: 4 }
+
+ each_pairs = []
+ delete_pairs = []
+
+ h.each_pair { |k,v| each_pairs << [k, v] }
+ h.delete_if { |k,v| delete_pairs << [k,v] }
+
+ each_pairs.should == delete_pairs
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.delete_if { false } }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.delete_if { true } }.should.raise(FrozenError)
+ end
+
+ it_behaves_like :hash_iteration_no_block, :delete_if
+ it_behaves_like :enumeratorized_with_origin_size, :delete_if, { 1 => 2, 3 => 4, 5 => 6 }
+end
diff --git a/spec/ruby/core/hash/delete_spec.rb b/spec/ruby/core/hash/delete_spec.rb
new file mode 100644
index 0000000000..33ff01bf36
--- /dev/null
+++ b/spec/ruby/core/hash/delete_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#delete" do
+ it "removes the entry and returns the deleted value" do
+ h = { a: 5, b: 2 }
+ h.delete(:b).should == 2
+ h.should == { a: 5 }
+ end
+
+ it "calls supplied block if the key is not found" do
+ { a: 1, b: 10, c: 100 }.delete(:d) { 5 }.should == 5
+ Hash.new(:default).delete(:d) { 5 }.should == 5
+ Hash.new { :default }.delete(:d) { 5 }.should == 5
+ end
+
+ it "returns nil if the key is not found when no block is given" do
+ { a: 1, b: 10, c: 100 }.delete(:d).should == nil
+ Hash.new(:default).delete(:d).should == nil
+ Hash.new { :default }.delete(:d).should == nil
+ end
+
+ # MRI explicitly implements this behavior
+ it "allows removing a key while iterating" do
+ h = { a: 1, b: 2 }
+ visited = []
+ h.each_pair { |k, v|
+ visited << k
+ h.delete(k)
+ }
+ visited.should == [:a, :b]
+ h.should == {}
+ end
+
+ it "allows removing a key while iterating for big hashes" do
+ h = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10,
+ k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20,
+ u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 }
+ visited = []
+ h.each_pair { |k, v|
+ visited << k
+ h.delete(k)
+ }
+ visited.should == [:a, :b, :c, :d, :e, :f, :g, :h, :i, :j, :k, :l, :m,
+ :n, :o, :p, :q, :r, :s, :t, :u, :v, :w, :x, :y, :z]
+ h.should == {}
+ end
+
+ it "accepts keys with private #hash method" do
+ key = HashSpecs::KeyWithPrivateHash.new
+ { key => 5 }.delete(key).should == 5
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.delete("foo") }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.delete("foo") }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/dig_spec.rb b/spec/ruby/core/hash/dig_spec.rb
new file mode 100644
index 0000000000..94ac94e850
--- /dev/null
+++ b/spec/ruby/core/hash/dig_spec.rb
@@ -0,0 +1,66 @@
+require_relative '../../spec_helper'
+
+describe "Hash#dig" do
+
+ it "returns #[] with one arg" do
+ h = { 0 => false, a: 1 }
+ h.dig(:a).should == 1
+ h.dig(0).should == false
+ h.dig(1).should == nil
+ end
+
+ it "returns the nested value specified by the sequence of keys" do
+ h = { foo: { bar: { baz: 1 } } }
+ h.dig(:foo, :bar, :baz).should == 1
+ h.dig(:foo, :bar, :nope).should == nil
+ h.dig(:foo, :baz).should == nil
+ h.dig(:bar, :baz, :foo).should == nil
+ end
+
+ it "returns the nested value specified if the sequence includes an index" do
+ h = { foo: [1, 2, 3] }
+ h.dig(:foo, 2).should == 3
+ end
+
+ it "returns nil if any intermediate step is nil" do
+ h = { foo: { bar: { baz: 1 } } }
+ h.dig(:foo, :zot, :xyz).should == nil
+ end
+
+ it "raises an ArgumentError if no arguments provided" do
+ -> { { the: 'borg' }.dig() }.should.raise(ArgumentError)
+ end
+
+ it "handles type-mixed deep digging" do
+ h = {}
+ h[:foo] = [ { bar: [ 1 ] }, [ obj = Object.new, 'str' ] ]
+ def obj.dig(*args); [ 42 ] end
+
+ h.dig(:foo, 0, :bar).should == [ 1 ]
+ h.dig(:foo, 0, :bar, 0).should == 1
+ h.dig(:foo, 1, 1).should == 'str'
+ # MRI does not recurse values returned from `obj.dig`
+ h.dig(:foo, 1, 0, 0).should == [ 42 ]
+ h.dig(:foo, 1, 0, 0, 10).should == [ 42 ]
+ end
+
+ it "raises TypeError if an intermediate element does not respond to #dig" do
+ h = {}
+ h[:foo] = [ { bar: [ 1 ] }, [ nil, 'str' ] ]
+ -> { h.dig(:foo, 0, :bar, 0, 0) }.should.raise(TypeError)
+ -> { h.dig(:foo, 1, 1, 0) }.should.raise(TypeError)
+ end
+
+ it "calls #dig on the result of #[] with the remaining arguments" do
+ h = { foo: { bar: { baz: 42 } } }
+ h[:foo].should_receive(:dig).with(:bar, :baz).and_return(42)
+ h.dig(:foo, :bar, :baz).should == 42
+ end
+
+ it "respects Hash's default" do
+ default = {bar: 42}
+ h = Hash.new(default)
+ h.dig(:foo).should.equal? default
+ h.dig(:foo, :bar).should == 42
+ end
+end
diff --git a/spec/ruby/core/hash/each_key_spec.rb b/spec/ruby/core/hash/each_key_spec.rb
new file mode 100644
index 0000000000..5b2e9deb63
--- /dev/null
+++ b/spec/ruby/core/hash/each_key_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iteration'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Hash#each_key" do
+ it "calls block once for each key, passing key" do
+ r = {}
+ h = { 1 => -1, 2 => -2, 3 => -3, 4 => -4 }
+ h.each_key { |k| r[k] = k }.should.equal?(h)
+ r.should == { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
+ end
+
+ it "processes keys in the same order as keys()" do
+ keys = []
+ h = { 1 => -1, 2 => -2, 3 => -3, 4 => -4 }
+ h.each_key { |k| keys << k }
+ keys.should == h.keys
+ end
+
+ it_behaves_like :hash_iteration_no_block, :each_key
+ it_behaves_like :enumeratorized_with_origin_size, :each_key, { 1 => 2, 3 => 4, 5 => 6 }
+end
diff --git a/spec/ruby/core/hash/each_pair_spec.rb b/spec/ruby/core/hash/each_pair_spec.rb
new file mode 100644
index 0000000000..01810c130c
--- /dev/null
+++ b/spec/ruby/core/hash/each_pair_spec.rb
@@ -0,0 +1,111 @@
+require_relative '../../spec_helper'
+require_relative 'shared/iteration'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Hash#each_pair" do
+ it_behaves_like :hash_iteration_no_block, :each_pair
+ it_behaves_like :enumeratorized_with_origin_size, :each_pair, { 1 => 2, 3 => 4, 5 => 6 }
+
+ # This is inconsistent with below, MRI checks the block arity in rb_hash_each_pair()
+ it "yields a [[key, value]] Array for each pair to a block expecting |*args|" do
+ all_args = []
+ { 1 => 2, 3 => 4 }.each_pair { |*args| all_args << args }
+ all_args.sort.should == [[[1, 2]], [[3, 4]]]
+ end
+
+ it "yields the key and value of each pair to a block expecting |key, value|" do
+ r = {}
+ h = { a: 1, b: 2, c: 3, d: 5 }
+ h.each_pair { |k,v| r[k.to_s] = v.to_s }.should.equal?(h)
+ r.should == { "a" => "1", "b" => "2", "c" => "3", "d" => "5" }
+ end
+
+ it "yields the key only to a block expecting |key,|" do
+ ary = []
+ h = { "a" => 1, "b" => 2, "c" => 3 }
+ h.each_pair { |k,| ary << k }
+ ary.sort.should == ["a", "b", "c"]
+ end
+
+ it "always yields an Array of 2 elements, even when given a callable of arity 2" do
+ obj = Object.new
+ def obj.foo(key, value)
+ end
+
+ -> {
+ { "a" => 1 }.each_pair(&obj.method(:foo))
+ }.should.raise(ArgumentError)
+
+ -> {
+ { "a" => 1 }.each_pair(&-> key, value { })
+ }.should.raise(ArgumentError)
+ end
+
+ it "yields an Array of 2 elements when given a callable of arity 1" do
+ obj = Object.new
+ def obj.foo(key_value)
+ ScratchPad << key_value
+ end
+
+ ScratchPad.record([])
+ { "a" => 1 }.each_pair(&obj.method(:foo))
+ ScratchPad.recorded.should == [["a", 1]]
+ end
+
+ it "raises an error for a Hash when an arity enforcing callable of arity >2 is passed in" do
+ obj = Object.new
+ def obj.foo(key, value, extra)
+ end
+
+ -> {
+ { "a" => 1 }.each_pair(&obj.method(:foo))
+ }.should.raise(ArgumentError)
+ end
+
+ it "uses the same order as keys() and values()" do
+ h = { a: 1, b: 2, c: 3, d: 5 }
+ keys = []
+ values = []
+
+ h.each_pair do |k, v|
+ keys << k
+ values << v
+ end
+
+ keys.should == h.keys
+ values.should == h.values
+ end
+
+ # Confirming the argument-splatting works from child class for both k, v and [k, v]
+ it "properly expands (or not) child class's 'each'-yielded args" do
+ cls1 = Class.new(Hash) do
+ attr_accessor :k_v
+ def each
+ super do |k, v|
+ @k_v = [k, v]
+ yield k, v
+ end
+ end
+ end
+
+ cls2 = Class.new(Hash) do
+ attr_accessor :k_v
+ def each
+ super do |k, v|
+ @k_v = [k, v]
+ yield([k, v])
+ end
+ end
+ end
+
+ obj1 = cls1.new
+ obj1['a'] = 'b'
+ obj1.map {|k, v| [k, v]}.should == [['a', 'b']]
+ obj1.k_v.should == ['a', 'b']
+
+ obj2 = cls2.new
+ obj2['a'] = 'b'
+ obj2.map {|k, v| [k, v]}.should == [['a', 'b']]
+ obj2.k_v.should == ['a', 'b']
+ end
+end
diff --git a/spec/ruby/core/hash/each_spec.rb b/spec/ruby/core/hash/each_spec.rb
new file mode 100644
index 0000000000..1644b63216
--- /dev/null
+++ b/spec/ruby/core/hash/each_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#each" do
+ it "is an alias of Hash#each_pair" do
+ Hash.instance_method(:each).should == Hash.instance_method(:each_pair)
+ end
+end
diff --git a/spec/ruby/core/hash/each_value_spec.rb b/spec/ruby/core/hash/each_value_spec.rb
new file mode 100644
index 0000000000..2df95a8789
--- /dev/null
+++ b/spec/ruby/core/hash/each_value_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iteration'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Hash#each_value" do
+ it "calls block once for each key, passing value" do
+ r = []
+ h = { a: -5, b: -3, c: -2, d: -1, e: -1 }
+ h.each_value { |v| r << v }.should.equal?(h)
+ r.sort.should == [-5, -3, -2, -1, -1]
+ end
+
+ it "processes values in the same order as values()" do
+ values = []
+ h = { a: -5, b: -3, c: -2, d: -1, e: -1 }
+ h.each_value { |v| values << v }
+ values.should == h.values
+ end
+
+ it_behaves_like :hash_iteration_no_block, :each_value
+ it_behaves_like :enumeratorized_with_origin_size, :each_value, { 1 => 2, 3 => 4, 5 => 6 }
+end
diff --git a/spec/ruby/core/hash/element_reference_spec.rb b/spec/ruby/core/hash/element_reference_spec.rb
new file mode 100644
index 0000000000..3d074b346d
--- /dev/null
+++ b/spec/ruby/core/hash/element_reference_spec.rb
@@ -0,0 +1,134 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#[]" do
+ it "returns the value for key" do
+ obj = mock('x')
+ h = { 1 => 2, 3 => 4, "foo" => "bar", obj => obj, [] => "baz" }
+ h[1].should == 2
+ h[3].should == 4
+ h["foo"].should == "bar"
+ h[obj].should == obj
+ h[[]].should == "baz"
+ end
+
+ it "returns nil as default value" do
+ { 0 => 0 }[5].should == nil
+ end
+
+ it "returns the default (immediate) value for missing keys" do
+ h = Hash.new(5)
+ h[:a].should == 5
+ h[:a] = 0
+ h[:a].should == 0
+ h[:b].should == 5
+ end
+
+ it "calls subclass implementations of default" do
+ h = HashSpecs::DefaultHash.new
+ h[:nothing].should == 100
+ end
+
+ it "does not create copies of the immediate default value" do
+ str = +"foo"
+ h = Hash.new(str)
+ a = h[:a]
+ b = h[:b]
+ a << "bar"
+
+ a.should.equal?(b)
+ a.should == "foobar"
+ b.should == "foobar"
+ end
+
+ it "returns the default (dynamic) value for missing keys" do
+ h = Hash.new { |hsh, k| k.kind_of?(Numeric) ? hsh[k] = k + 2 : hsh[k] = k }
+ h[1].should == 3
+ h['this'].should == 'this'
+ h.should == { 1 => 3, 'this' => 'this' }
+
+ i = 0
+ h = Hash.new { |hsh, key| i += 1 }
+ h[:foo].should == 1
+ h[:foo].should == 2
+ h[:bar].should == 3
+ end
+
+ it "does not return default values for keys with nil values" do
+ h = Hash.new(5)
+ h[:a] = nil
+ h[:a].should == nil
+
+ h = Hash.new { 5 }
+ h[:a] = nil
+ h[:a].should == nil
+ end
+
+ it "compares keys with eql? semantics" do
+ { 1.0 => "x" }[1].should == nil
+ { 1.0 => "x" }[1.0].should == "x"
+ { 1 => "x" }[1.0].should == nil
+ { 1 => "x" }[1].should == "x"
+ end
+
+ it "compares key via hash" do
+ x = mock('0')
+ x.should_receive(:hash).and_return(0)
+
+ h = {}
+ # 1.9 only calls #hash if the hash had at least one entry beforehand.
+ h[:foo] = :bar
+ h[x].should == nil
+ end
+
+ it "does not compare keys with different #hash values via #eql?" do
+ x = mock('x')
+ x.should_not_receive(:eql?)
+ x.stub!(:hash).and_return(0)
+
+ y = mock('y')
+ y.should_not_receive(:eql?)
+ y.stub!(:hash).and_return(1)
+
+ { y => 1 }[x].should == nil
+ end
+
+ it "compares keys with the same #hash value via #eql?" do
+ x = mock('x')
+ x.should_receive(:eql?).and_return(true)
+ x.stub!(:hash).and_return(42)
+
+ y = mock('y')
+ y.should_not_receive(:eql?)
+ y.stub!(:hash).and_return(42)
+
+ { y => 1 }[x].should == 1
+ end
+
+ it "finds a value via an identical key even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.should_receive(:hash).at_least(1).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ { x => :x }[x].should == :x
+ end
+
+ it "supports keys with private #hash method" do
+ key = HashSpecs::KeyWithPrivateHash.new
+ { key => 42 }[key].should == 42
+ end
+
+ it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do
+ code = <<-EOC
+ load '#{fixture __FILE__, "name.rb"}'
+ hash = { true => 42, false => 42, 1 => 42, 2.0 => 42, "hello" => 42, :ok => 42 }
+ [true, false, 1, 2.0, "hello", :ok].each do |value|
+ raise "incorrect value" unless hash[value] == 42
+ end
+ puts "Ok."
+ EOC
+ result = ruby_exe(code, args: "2>&1")
+ result.should == "Ok.\n"
+ end
+
+end
diff --git a/spec/ruby/core/hash/element_set_spec.rb b/spec/ruby/core/hash/element_set_spec.rb
new file mode 100644
index 0000000000..bb805477ca
--- /dev/null
+++ b/spec/ruby/core/hash/element_set_spec.rb
@@ -0,0 +1,121 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#[]=" do
+ it "associates the key with the value" do
+ h = { a: 1 }
+ h[:b] = 2
+ h.should == { b:2, a:1 }
+ end
+
+ it "returns the assigned value" do
+ h = {}
+ h.send(:[]=, 1, 2).should == 2
+ end
+
+ it "duplicates string keys using dup semantics" do
+ # dup doesn't copy singleton methods
+ key = +"foo"
+ def key.reverse() "bar" end
+ h = {}
+ h[key] = 0
+ h.keys[0].reverse.should == "oof"
+ end
+
+ it "stores unequal keys that hash to the same value" do
+ h = {}
+ k1 = ["x"]
+ k2 = ["y"]
+ # So they end up in the same bucket
+ k1.should_receive(:hash).and_return(0)
+ k2.should_receive(:hash).and_return(0)
+
+ h[k1] = 1
+ h[k2] = 2
+ h.size.should == 2
+ end
+
+ it "accepts keys with private #hash method" do
+ key = HashSpecs::KeyWithPrivateHash.new
+ h = {}
+ h[key] = "foo"
+ h[key].should == "foo"
+ end
+
+ it " accepts keys with an Integer hash" do
+ o = mock(hash: 1 << 100)
+ h = {}
+ h[o] = 1
+ h[o].should == 1
+ end
+
+ it "duplicates and freezes string keys" do
+ key = +"foo"
+ h = {}
+ h[key] = 0
+ key << "bar"
+
+ h.should == { "foo" => 0 }
+ h.keys[0].should.frozen?
+ end
+
+ it "doesn't duplicate and freeze already frozen string keys" do
+ key = "foo".freeze
+ h = {}
+ h[key] = 0
+ h.keys[0].should.equal?(key)
+ end
+
+ it "keeps the existing key in the hash if there is a matching one" do
+ h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 }
+ key1 = HashSpecs::ByValueKey.new(13)
+ key2 = HashSpecs::ByValueKey.new(13)
+ h[key1] = 41
+ key_in_hash = h.keys.last
+ key_in_hash.should.equal?(key1)
+ h[key2] = 42
+ last_key = h.keys.last
+ last_key.should.equal?(key_in_hash)
+ last_key.should_not.equal?(key2)
+ end
+
+ it "keeps the existing String key in the hash if there is a matching one" do
+ h = { "a" => 1, "b" => 2, "c" => 3, "d" => 4 }
+ key1 = "foo".dup
+ key2 = "foo".dup
+ key1.should_not.equal?(key2)
+ h[key1] = 41
+ frozen_key = h.keys.last
+ frozen_key.should_not.equal?(key1)
+ h[key2] = 42
+ h.keys.last.should.equal?(frozen_key)
+ h.keys.last.should_not.equal?(key2)
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash[1] = 2 }.should.raise(FrozenError)
+ end
+
+ it "does not raise an exception if changing the value of an existing key during iteration" do
+ hash = {1 => 2, 3 => 4, 5 => 6}
+ hash.each { hash[1] = :foo }
+ hash.should == {1 => :foo, 3 => 4, 5 => 6}
+ end
+
+ it "does not dispatch to hash for Boolean, Integer, Float, String, or Symbol" do
+ code = <<-EOC
+ load '#{fixture __FILE__, "name.rb"}'
+ hash = {}
+ [true, false, 1, 2.0, "hello", :ok].each do |value|
+ hash[value] = 42
+ raise "incorrect value" unless hash[value] == 42
+ hash[value] = 43
+ raise "incorrect value" unless hash[value] == 43
+ end
+ puts "OK"
+ puts hash.size
+ EOC
+ result = ruby_exe(code, args: "2>&1")
+ result.should == "OK\n6\n"
+ end
+end
diff --git a/spec/ruby/core/hash/empty_spec.rb b/spec/ruby/core/hash/empty_spec.rb
new file mode 100644
index 0000000000..881e1cc34b
--- /dev/null
+++ b/spec/ruby/core/hash/empty_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#empty?" do
+ it "returns true if the hash has no entries" do
+ {}.should.empty?
+ { 1 => 1 }.should_not.empty?
+ end
+
+ it "returns true if the hash has no entries and has a default value" do
+ Hash.new(5).should.empty?
+ Hash.new { 5 }.should.empty?
+ Hash.new { |hsh, k| hsh[k] = k }.should.empty?
+ end
+end
diff --git a/spec/ruby/core/hash/eql_spec.rb b/spec/ruby/core/hash/eql_spec.rb
new file mode 100644
index 0000000000..9013e12ffd
--- /dev/null
+++ b/spec/ruby/core/hash/eql_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/eql'
+
+describe "Hash#eql?" do
+ it_behaves_like :hash_eql, :eql?
+ it_behaves_like :hash_eql_additional, :eql?
+ it_behaves_like :hash_eql_additional_more, :eql?
+end
diff --git a/spec/ruby/core/hash/equal_value_spec.rb b/spec/ruby/core/hash/equal_value_spec.rb
new file mode 100644
index 0000000000..ab14f81489
--- /dev/null
+++ b/spec/ruby/core/hash/equal_value_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/eql'
+
+describe "Hash#==" do
+ it_behaves_like :hash_eql, :==
+ it_behaves_like :hash_eql_additional, :==
+ it_behaves_like :hash_eql_additional_more, :==
+
+ it "compares values with == semantics" do
+ l_val = mock("left")
+ r_val = mock("right")
+
+ l_val.should_receive(:==).with(r_val).and_return(true)
+
+ ({ 1 => l_val } == { 1 => r_val }).should == true
+ end
+end
diff --git a/spec/ruby/core/hash/except_spec.rb b/spec/ruby/core/hash/except_spec.rb
new file mode 100644
index 0000000000..77a020e9b7
--- /dev/null
+++ b/spec/ruby/core/hash/except_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+
+describe "Hash#except" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3 }
+ end
+
+ it "returns a new duplicate hash without arguments" do
+ ret = @hash.except
+ ret.should_not.equal?(@hash)
+ ret.should == @hash
+ end
+
+ it "returns a hash without the requested subset" do
+ @hash.except(:c, :a).should == { b: 2 }
+ end
+
+ it "ignores keys not present in the original hash" do
+ @hash.except(:a, :chunky_bacon).should == { b: 2, c: 3 }
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.except(:a).default.should == nil
+ h[:a] = 1
+ h.except(:a).default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.except(:a).default_proc.should == nil
+ h[:a] = 1
+ h.except(:a).default_proc.should == nil
+ end
+
+ it "retains compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.except(:a)
+ h2.compare_by_identity?.should == true
+ end
+end
diff --git a/spec/ruby/core/hash/fetch_spec.rb b/spec/ruby/core/hash/fetch_spec.rb
new file mode 100644
index 0000000000..f9b80b6c49
--- /dev/null
+++ b/spec/ruby/core/hash/fetch_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/hash/key_error'
+
+describe "Hash#fetch" do
+ context "when the key is not found" do
+ it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new({ a: 5 })
+ it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, {}
+ it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new { 5 }
+ it_behaves_like :key_error, -> obj, key { obj.fetch(key) }, Hash.new(5)
+
+ it "formats the object with #inspect in the KeyError message" do
+ -> {
+ {}.fetch('foo')
+ }.should.raise(KeyError, 'key not found: "foo"')
+ end
+ end
+
+ it "returns the value for key" do
+ { a: 1, b: -1 }.fetch(:b).should == -1
+ end
+
+ it "returns default if key is not found when passed a default" do
+ {}.fetch(:a, nil).should == nil
+ {}.fetch(:a, 'not here!').should == "not here!"
+ { a: nil }.fetch(:a, 'not here!').should == nil
+ end
+
+ it "returns value of block if key is not found when passed a block" do
+ {}.fetch('a') { |k| k + '!' }.should == "a!"
+ end
+
+ it "gives precedence to the default block over the default argument when passed both" do
+ -> {
+ @result = {}.fetch(9, :foo) { |i| i * i }
+ }.should complain(/block supersedes default value argument/)
+ @result.should == 81
+ end
+
+ it "raises an ArgumentError when not passed one or two arguments" do
+ -> { {}.fetch() }.should.raise(ArgumentError)
+ -> { {}.fetch(1, 2, 3) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/hash/fetch_values_spec.rb b/spec/ruby/core/hash/fetch_values_spec.rb
new file mode 100644
index 0000000000..0cd48565af
--- /dev/null
+++ b/spec/ruby/core/hash/fetch_values_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/hash/key_error'
+
+describe "Hash#fetch_values" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3 }
+ end
+
+ describe "with matched keys" do
+ it "returns the values for keys" do
+ @hash.fetch_values(:a).should == [1]
+ @hash.fetch_values(:a, :c).should == [1, 3]
+ end
+
+ it "returns the values for keys ordered in the order of the requested keys" do
+ @hash.fetch_values(:c, :a).should == [3, 1]
+ end
+ end
+
+ describe "with unmatched keys" do
+ it_behaves_like :key_error, -> obj, key { obj.fetch_values(key) }, Hash.new({ a: 5 })
+
+ it "returns the default value from block" do
+ @hash.fetch_values(:z) { |key| "`#{key}' is not found" }.should == ["`z' is not found"]
+ @hash.fetch_values(:a, :z) { |key| "`#{key}' is not found" }.should == [1, "`z' is not found"]
+ end
+ end
+
+ describe "without keys" do
+ it "returns an empty Array" do
+ @hash.fetch_values.should == []
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/filter_spec.rb b/spec/ruby/core/hash/filter_spec.rb
new file mode 100644
index 0000000000..625a7feb90
--- /dev/null
+++ b/spec/ruby/core/hash/filter_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Hash#filter" do
+ it "is an alias of Hash#select" do
+ Hash.instance_method(:filter).should == Hash.instance_method(:select)
+ end
+end
+
+describe "Hash#filter!" do
+ it "is an alias of Hash#select!" do
+ Hash.instance_method(:filter!).should == Hash.instance_method(:select!)
+ end
+end
diff --git a/spec/ruby/core/hash/fixtures/classes.rb b/spec/ruby/core/hash/fixtures/classes.rb
new file mode 100644
index 0000000000..ae907aaff6
--- /dev/null
+++ b/spec/ruby/core/hash/fixtures/classes.rb
@@ -0,0 +1,75 @@
+module HashSpecs
+ class MyHash < Hash; end
+
+ class MyInitializerHash < Hash
+
+ def initialize
+ raise "Constructor called"
+ end
+
+ end
+
+ class NewHash < Hash
+ def initialize(*args)
+ args.each_with_index do |val, index|
+ self[index] = val
+ end
+ end
+ end
+
+ class SubHashSettingInInitialize < Hash
+ def initialize(*args, &block)
+ self[:foo] = :bar
+ super(*args, &block)
+ end
+ end
+
+ class DefaultHash < Hash
+ def default(key)
+ 100
+ end
+ end
+
+ class ToHashHash < Hash
+ def to_hash
+ { "to_hash" => "was", "called!" => "duh." }
+ end
+ end
+
+ class KeyWithPrivateHash
+ private :hash
+ end
+
+ class ByIdentityKey
+ def hash
+ fail("#hash should not be called on compare_by_identity Hash")
+ end
+ end
+
+ class ByValueKey
+ attr_reader :n
+ def initialize(n)
+ @n = n
+ end
+
+ def hash
+ n
+ end
+
+ def eql? other
+ ByValueKey === other and @n == other.n
+ end
+ end
+
+ def self.empty_frozen_hash
+ @empty ||= {}
+ @empty.freeze
+ @empty
+ end
+
+ def self.frozen_hash
+ @hash ||= { 1 => 2, 3 => 4 }
+ @hash.freeze
+ @hash
+ end
+end
diff --git a/spec/ruby/core/hash/fixtures/name.rb b/spec/ruby/core/hash/fixtures/name.rb
new file mode 100644
index 0000000000..b203bf6ae4
--- /dev/null
+++ b/spec/ruby/core/hash/fixtures/name.rb
@@ -0,0 +1,30 @@
+class TrueClass
+ def hash
+ raise "TrueClass#hash should not be called"
+ end
+end
+class FalseClass
+ def hash
+ raise "FalseClass#hash should not be called"
+ end
+end
+class Integer
+ def hash
+ raise "Integer#hash should not be called"
+ end
+end
+class Float
+ def hash
+ raise "Float#hash should not be called"
+ end
+end
+class String
+ def hash
+ raise "String#hash should not be called"
+ end
+end
+class Symbol
+ def hash
+ raise "Symbol#hash should not be called"
+ end
+end
diff --git a/spec/ruby/core/hash/flatten_spec.rb b/spec/ruby/core/hash/flatten_spec.rb
new file mode 100644
index 0000000000..18e9a7b56a
--- /dev/null
+++ b/spec/ruby/core/hash/flatten_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+
+describe "Hash#flatten" do
+
+ before :each do
+ @h = {plato: :greek,
+ witgenstein: [:austrian, :british],
+ russell: :welsh}
+ end
+
+ it "returns an Array" do
+ {}.flatten.should.instance_of?(Array)
+ end
+
+ it "returns an empty Array for an empty Hash" do
+ {}.flatten.should == []
+ end
+
+ it "sets each even index of the Array to a key of the Hash" do
+ a = @h.flatten
+ a[0].should == :plato
+ a[2].should == :witgenstein
+ a[4].should == :russell
+ end
+
+ it "sets each odd index of the Array to the value corresponding to the previous element" do
+ a = @h.flatten
+ a[1].should == :greek
+ a[3].should == [:austrian, :british]
+ a[5].should == :welsh
+ end
+
+ it "does not recursively flatten Array values when called without arguments" do
+ a = @h.flatten
+ a[3].should == [:austrian, :british]
+ end
+
+ it "does not recursively flatten Hash values when called without arguments" do
+ @h[:russell] = {born: :wales, influenced_by: :mill }
+ a = @h.flatten
+ a[5].should_not == {born: :wales, influenced_by: :mill }.flatten
+ end
+
+ it "recursively flattens Array values when called with an argument >= 2" do
+ a = @h.flatten(2)
+ a[3].should == :austrian
+ a[4].should == :british
+ end
+
+ it "recursively flattens Array values to the given depth" do
+ @h[:russell] = [[:born, :wales], [:influenced_by, :mill]]
+ a = @h.flatten(2)
+ a[6].should == [:born, :wales]
+ a[7].should == [:influenced_by, :mill]
+ end
+
+ it "raises a TypeError if given a non-Integer argument" do
+ -> do
+ @h.flatten(Object.new)
+ end.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/hash/gt_spec.rb b/spec/ruby/core/hash/gt_spec.rb
new file mode 100644
index 0000000000..69d23b7d29
--- /dev/null
+++ b/spec/ruby/core/hash/gt_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison'
+require_relative 'shared/greater_than'
+
+describe "Hash#>" do
+ it_behaves_like :hash_comparison, :>
+ it_behaves_like :hash_greater_than, :>
+
+ it "returns false if both hashes are identical" do
+ h = { a: 1, b: 2 }
+ (h > h).should == false
+ end
+end
+
+describe "Hash#>" do
+ before :each do
+ @hash = {a:1, b:2}
+ @bigger = {a:1, b:2, c:3}
+ @unrelated = {c:3, d:4}
+ @similar = {a:2, b:3}
+ end
+
+ it "returns false when receiver size is smaller than argument" do
+ (@hash > @bigger).should == false
+ (@unrelated > @bigger).should == false
+ end
+
+ it "returns false when receiver size is the same as argument" do
+ (@hash > @hash).should == false
+ (@hash > @unrelated).should == false
+ (@unrelated > @hash).should == false
+ end
+
+ it "returns true when argument is a subset of receiver" do
+ (@bigger > @hash).should == true
+ end
+
+ it "returns false when keys match but values don't" do
+ (@hash > @similar).should == false
+ (@similar > @hash).should == false
+ end
+end
diff --git a/spec/ruby/core/hash/gte_spec.rb b/spec/ruby/core/hash/gte_spec.rb
new file mode 100644
index 0000000000..be5060ea4f
--- /dev/null
+++ b/spec/ruby/core/hash/gte_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison'
+require_relative 'shared/greater_than'
+
+describe "Hash#>=" do
+ it_behaves_like :hash_comparison, :>=
+ it_behaves_like :hash_greater_than, :>=
+
+ it "returns true if both hashes are identical" do
+ h = { a: 1, b: 2 }
+ (h >= h).should == true
+ end
+end
+
+describe "Hash#>=" do
+ before :each do
+ @hash = {a:1, b:2}
+ @bigger = {a:1, b:2, c:3}
+ @unrelated = {c:3, d:4}
+ @similar = {a:2, b:3}
+ end
+
+ it "returns false when receiver size is smaller than argument" do
+ (@hash >= @bigger).should == false
+ (@unrelated >= @bigger).should == false
+ end
+
+ it "returns false when argument is not a subset or not equals to receiver" do
+ (@hash >= @unrelated).should == false
+ (@unrelated >= @hash).should == false
+ end
+
+ it "returns true when argument is a subset of receiver or equals to receiver" do
+ (@bigger >= @hash).should == true
+ (@hash >= @hash).should == true
+ end
+
+ it "returns false when keys match but values don't" do
+ (@hash >= @similar).should == false
+ (@similar >= @hash).should == false
+ end
+end
diff --git a/spec/ruby/core/hash/has_key_spec.rb b/spec/ruby/core/hash/has_key_spec.rb
new file mode 100644
index 0000000000..9a6244c6f6
--- /dev/null
+++ b/spec/ruby/core/hash/has_key_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#has_key?" do
+ it "is an alias of Hash#include?" do
+ Hash.instance_method(:has_key?).should == Hash.instance_method(:include?)
+ end
+end
diff --git a/spec/ruby/core/hash/has_value_spec.rb b/spec/ruby/core/hash/has_value_spec.rb
new file mode 100644
index 0000000000..d40e52ebfd
--- /dev/null
+++ b/spec/ruby/core/hash/has_value_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Hash#has_value?" do
+ it "returns true if the value exists in the hash" do
+ { a: :b }.has_value?(:a).should == false
+ { 1 => 2 }.has_value?(2).should == true
+ h = Hash.new(5)
+ h.has_value?(5).should == false
+ h = Hash.new { 5 }
+ h.has_value?(5).should == false
+ end
+
+ it "uses == semantics for comparing values" do
+ { 5 => 2.0 }.has_value?(2).should == true
+ end
+end
diff --git a/spec/ruby/core/hash/hash_spec.rb b/spec/ruby/core/hash/hash_spec.rb
new file mode 100644
index 0000000000..cd67f7a652
--- /dev/null
+++ b/spec/ruby/core/hash/hash_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+
+describe "Hash" do
+ it "includes Enumerable" do
+ Hash.include?(Enumerable).should == true
+ end
+end
+
+describe "Hash#hash" do
+ it "returns a value which doesn't depend on the hash order" do
+ { 0=>2, 11=>1 }.hash.should == { 11=>1, 0=>2 }.hash
+ end
+
+ it "returns a value in which element values do not cancel each other out" do
+ { a: 2, b: 2 }.hash.should_not == { a: 7, b: 7 }.hash
+ end
+
+ it "returns a value in which element keys and values do not cancel each other out" do
+ { :a => :a }.hash.should_not == { :b => :b }.hash
+ end
+
+ it "generates a hash for recursive hash structures" do
+ h = {}
+ h[:a] = h
+ (h.hash == h[:a].hash).should == true
+ end
+
+ it "returns the same hash for recursive hashes" do
+ h = {} ; h[:x] = h
+ h.hash.should == {x: h}.hash
+ h.hash.should == {x: {x: h}}.hash
+ # This is because h.eql?(x: h)
+ # Remember that if two objects are eql?
+ # then the need to have the same hash.
+ # Check the Hash#eql? specs!
+ end
+
+ it "returns the same hash for recursive hashes through arrays" do
+ h = {} ; rec = [h] ; h[:x] = rec
+ h.hash.should == {x: rec}.hash
+ h.hash.should == {x: [h]}.hash
+ # Like above, because h.eql?(x: [h])
+ end
+
+ it "allows omitting values" do
+ a = 1
+ b = 2
+
+ {a:, b:}.should == { a: 1, b: 2 }
+ end
+end
diff --git a/spec/ruby/core/hash/include_spec.rb b/spec/ruby/core/hash/include_spec.rb
new file mode 100644
index 0000000000..1c32e9fb23
--- /dev/null
+++ b/spec/ruby/core/hash/include_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+
+describe "Hash#include?" do
+ it "returns true if argument is a key" do
+ h = { a: 1, b: 2, c: 3, 4 => 0 }
+ h.include?(:a).should == true
+ h.include?(:b).should == true
+ h.include?(2).should == false
+ h.include?(4).should == true
+
+ not_supported_on :opal do
+ h.include?('b').should == false
+ h.include?(4.0).should == false
+ end
+ end
+
+ it "returns true if the key's matching value was nil" do
+ { xyz: nil }.include?(:xyz).should == true
+ end
+
+ it "returns true if the key's matching value was false" do
+ { xyz: false }.include?(:xyz).should == true
+ end
+
+ it "returns true if the key is nil" do
+ { nil => 'b' }.include?(nil).should == true
+ { nil => nil }.include?(nil).should == true
+ end
+
+ it "compares keys with the same #hash value via #eql?" do
+ x = mock('x')
+ x.stub!(:hash).and_return(42)
+
+ y = mock('y')
+ y.stub!(:hash).and_return(42)
+ y.should_receive(:eql?).and_return(false)
+
+ { x => nil }.include?(y).should == false
+ end
+end
diff --git a/spec/ruby/core/hash/initialize_spec.rb b/spec/ruby/core/hash/initialize_spec.rb
new file mode 100644
index 0000000000..cdba598813
--- /dev/null
+++ b/spec/ruby/core/hash/initialize_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#initialize" do
+ it "is private" do
+ Hash.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "can be used to reset default_proc" do
+ h = { "foo" => 1, "bar" => 2 }
+ h.default_proc.should == nil
+ h.send(:initialize) { |_, k| k * 2 }
+ h.default_proc.should_not == nil
+ h["a"].should == "aa"
+ end
+
+ it "can be used to reset the default value" do
+ h = {}
+ h.default = 42
+ h.default.should == 42
+ h.send(:initialize, 1)
+ h.default.should == 1
+ h.send(:initialize)
+ h.default.should == nil
+ end
+
+ it "receives the arguments passed to Hash#new" do
+ HashSpecs::NewHash.new(:one, :two)[0].should == :one
+ HashSpecs::NewHash.new(:one, :two)[1].should == :two
+ end
+
+ it "does not change the storage, only the default value or proc" do
+ h = HashSpecs::SubHashSettingInInitialize.new
+ h.to_a.should == [[:foo, :bar]]
+
+ h = HashSpecs::SubHashSettingInInitialize.new(:default)
+ h.to_a.should == [[:foo, :bar]]
+
+ h = HashSpecs::SubHashSettingInInitialize.new { :default_block }
+ h.to_a.should == [[:foo, :bar]]
+ end
+
+ it "returns self" do
+ h = Hash.new
+ h.send(:initialize).should.equal?(h)
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ block = -> { HashSpecs.frozen_hash.instance_eval { initialize() }}
+ block.should.raise(FrozenError)
+
+ block = -> { HashSpecs.frozen_hash.instance_eval { initialize(nil) } }
+ block.should.raise(FrozenError)
+
+ block = -> { HashSpecs.frozen_hash.instance_eval { initialize(5) } }
+ block.should.raise(FrozenError)
+
+ block = -> { HashSpecs.frozen_hash.instance_eval { initialize { 5 } } }
+ block.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/inspect_spec.rb b/spec/ruby/core/hash/inspect_spec.rb
new file mode 100644
index 0000000000..359b13360f
--- /dev/null
+++ b/spec/ruby/core/hash/inspect_spec.rb
@@ -0,0 +1,123 @@
+require_relative '../../spec_helper'
+
+describe "Hash#inspect" do
+ it "returns a string representation with same order as each()" do
+ h = { a: [1, 2], b: -2, d: -6, nil => nil }
+ expected = ruby_version_is("3.4") ? "{a: [1, 2], b: -2, d: -6, nil => nil}" : "{:a=>[1, 2], :b=>-2, :d=>-6, nil=>nil}"
+ h.inspect.should == expected
+ end
+
+ it "calls #inspect on keys and values" do
+ key = mock('key')
+ val = mock('val')
+ key.should_receive(:inspect).and_return('key')
+ val.should_receive(:inspect).and_return('val')
+ expected = ruby_version_is("3.4") ? "{key => val}" : "{key=>val}"
+ { key => val }.inspect.should == expected
+ end
+
+ it "does not call #to_s on a String returned from #inspect" do
+ str = +"abc"
+ str.should_not_receive(:to_s)
+ expected = ruby_version_is("3.4") ? '{a: "abc"}' : '{:a=>"abc"}'
+ { a: str }.inspect.should == expected
+ end
+
+ it "calls #to_s on the object returned from #inspect if the Object isn't a String" do
+ obj = mock("Hash#inspect/to_s calls #to_s")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_receive(:to_s).and_return("abc")
+ expected = ruby_version_is("3.4") ? "{a: abc}" : "{:a=>abc}"
+ { a: obj }.inspect.should == expected
+ end
+
+ it "does not call #to_str on the object returned from #inspect when it is not a String" do
+ obj = mock("Hash#inspect/to_s does not call #to_str")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_not_receive(:to_str)
+ expected_pattern = ruby_version_is("3.4") ? /^\{a: #<MockObject:0x[0-9a-f]+>\}$/ : /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/
+ { a: obj }.inspect.should =~ expected_pattern
+ end
+
+ it "does not call #to_str on the object returned from #to_s when it is not a String" do
+ obj = mock("Hash#inspect/to_s does not call #to_str on #to_s result")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_receive(:to_s).and_return(obj)
+ obj.should_not_receive(:to_str)
+ expected_pattern = ruby_version_is("3.4") ? /^\{a: #<MockObject:0x[0-9a-f]+>\}$/ : /^\{:a=>#<MockObject:0x[0-9a-f]+>\}$/
+ { a: obj }.inspect.should =~ expected_pattern
+ end
+
+ it "does not swallow exceptions raised by #to_s" do
+ obj = mock("Hash#inspect/to_s does not swallow #to_s exceptions")
+ obj.should_receive(:inspect).and_return(obj)
+ obj.should_receive(:to_s).and_raise(Exception)
+
+ -> { { a: obj }.inspect }.should.raise(Exception)
+ end
+
+ it "handles hashes with recursive values" do
+ x = {}
+ x[0] = x
+ expected = ruby_version_is("3.4") ? '{0 => {...}}' : '{0=>{...}}'
+ x.inspect.should == expected
+
+ x = {}
+ y = {}
+ x[0] = y
+ y[1] = x
+ expected_x = ruby_version_is("3.4") ? '{0 => {1 => {...}}}' : '{0=>{1=>{...}}}'
+ expected_y = ruby_version_is("3.4") ? '{1 => {0 => {...}}}' : '{1=>{0=>{...}}}'
+ x.inspect.should == expected_x
+ y.inspect.should == expected_y
+ end
+
+ it "does not raise if inspected result is not default external encoding" do
+ utf_16be = mock("utf_16be")
+ utf_16be.should_receive(:inspect).and_return(%<"utf_16be \u3042">.encode(Encoding::UTF_16BE))
+ expected = ruby_version_is("3.4") ? '{a: "utf_16be \u3042"}' : '{:a=>"utf_16be \u3042"}'
+ {a: utf_16be}.inspect.should == expected
+ end
+
+ it "works for keys and values whose #inspect return a frozen String" do
+ expected = ruby_version_is("3.4") ? "{true => false}" : "{true=>false}"
+ { true => false }.inspect.should == expected
+ end
+
+ ruby_version_is "3.4" do
+ it "adds quotes to symbol keys that are not valid symbol literals" do
+ { "needs-quotes": 1 }.inspect.should == '{"needs-quotes": 1}'
+ end
+
+ it "can be evaled" do
+ no_quote = '{a: 1, a!: 1, a?: 1}'
+ eval(no_quote).inspect.should == no_quote
+ [
+ '{"": 1}',
+ '{"0": 1, "!": 1, "%": 1, "&": 1, "*": 1, "+": 1, "-": 1, "/": 1, "<": 1, ">": 1, "^": 1, "`": 1, "|": 1, "~": 1}',
+ '{"@a": 1, "$a": 1, "+@": 1, "a=": 1, "[]": 1}',
+ '{"a\"b": 1, "@@a": 1, "<=>": 1, "===": 1, "[]=": 1}',
+ ].each do |quote|
+ eval(quote).inspect.should == quote
+ end
+ end
+
+ it "can be evaled when Encoding.default_external is changed" do
+ external = Encoding.default_external
+
+ Encoding.default_external = Encoding::ASCII
+ utf8_ascii_hash = '{"\\u3042": 1}'
+ eval(utf8_ascii_hash).inspect.should == utf8_ascii_hash
+
+ Encoding.default_external = Encoding::UTF_8
+ utf8_hash = "{\u3042: 1}"
+ eval(utf8_hash).inspect.should == utf8_hash
+
+ Encoding.default_external = Encoding::Windows_31J
+ sjis_hash = "{\x87]: 1}".dup.force_encoding('sjis')
+ eval(sjis_hash).inspect.should == sjis_hash
+ ensure
+ Encoding.default_external = external
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/invert_spec.rb b/spec/ruby/core/hash/invert_spec.rb
new file mode 100644
index 0000000000..ea441751f2
--- /dev/null
+++ b/spec/ruby/core/hash/invert_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#invert" do
+ it "returns a new hash where keys are values and vice versa" do
+ { 1 => 'a', 2 => 'b', 3 => 'c' }.invert.should ==
+ { 'a' => 1, 'b' => 2, 'c' => 3 }
+ end
+
+ it "handles collisions by overriding with the key coming later in keys()" do
+ h = { a: 1, b: 1 }
+ override_key = h.keys.last
+ h.invert[1].should == override_key
+ end
+
+ it "compares new keys with eql? semantics" do
+ h = { a: 1.0, b: 1 }
+ i = h.invert
+ i[1.0].should == :a
+ i[1].should == :b
+ end
+
+ it "does not return subclass instances for subclasses" do
+ HashSpecs::MyHash[1 => 2, 3 => 4].invert.class.should == Hash
+ HashSpecs::MyHash[].invert.class.should == Hash
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.invert.default.should == nil
+ h[:a] = 1
+ h.invert.default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.invert.default_proc.should == nil
+ h[:a] = 1
+ h.invert.default_proc.should == nil
+ end
+
+ it "does not retain compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.invert
+ h2.compare_by_identity?.should == false
+ end
+end
diff --git a/spec/ruby/core/hash/keep_if_spec.rb b/spec/ruby/core/hash/keep_if_spec.rb
new file mode 100644
index 0000000000..c975efc5f8
--- /dev/null
+++ b/spec/ruby/core/hash/keep_if_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iteration'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Hash#keep_if" do
+ it "yields two arguments: key and value" do
+ all_args = []
+ { 1 => 2, 3 => 4 }.keep_if { |*args| all_args << args }
+ all_args.should == [[1, 2], [3, 4]]
+ end
+
+ it "keeps every entry for which block is true and returns self" do
+ h = { a: 1, b: 2, c: 3, d: 4 }
+ h.keep_if { |k,v| v % 2 == 0 }.should.equal?(h)
+ h.should == { b: 2, d: 4 }
+ end
+
+ it "removes all entries if the block is false" do
+ h = { a: 1, b: 2, c: 3 }
+ h.keep_if { |k,v| false }.should.equal?(h)
+ h.should == {}
+ end
+
+ it "returns self even if unmodified" do
+ h = { 1 => 2, 3 => 4 }
+ h.keep_if { true }.should.equal?(h)
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.keep_if { true } }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.keep_if { false } }.should.raise(FrozenError)
+ end
+
+ it_behaves_like :hash_iteration_no_block, :keep_if
+ it_behaves_like :enumeratorized_with_origin_size, :keep_if, { 1 => 2, 3 => 4, 5 => 6 }
+end
diff --git a/spec/ruby/core/hash/key_spec.rb b/spec/ruby/core/hash/key_spec.rb
new file mode 100644
index 0000000000..c2d7049aed
--- /dev/null
+++ b/spec/ruby/core/hash/key_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "Hash#key?" do
+ it "is an alias of Hash#include?" do
+ Hash.instance_method(:key?).should == Hash.instance_method(:include?)
+ end
+end
+
+describe "Hash#key" do
+ it "returns the corresponding key for value" do
+ { 2 => 'a', 1 => 'b' }.key('b').should == 1
+ end
+
+ it "returns nil if the value is not found" do
+ { a: -1, b: 3.14, c: 2.718 }.key(1).should == nil
+ end
+
+ it "doesn't return default value if the value is not found" do
+ Hash.new(5).key(5).should == nil
+ end
+
+ it "compares values using ==" do
+ { 1 => 0 }.key(0.0).should == 1
+ { 1 => 0.0 }.key(0).should == 1
+
+ needle = mock('needle')
+ inhash = mock('inhash')
+ inhash.should_receive(:==).with(needle).and_return(true)
+
+ { 1 => inhash }.key(needle).should == 1
+ end
+end
diff --git a/spec/ruby/core/hash/keys_spec.rb b/spec/ruby/core/hash/keys_spec.rb
new file mode 100644
index 0000000000..789c413bee
--- /dev/null
+++ b/spec/ruby/core/hash/keys_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#keys" do
+
+ it "returns an array with the keys in the order they were inserted" do
+ {}.keys.should == []
+ {}.keys.should.is_a?(Array)
+ Hash.new(5).keys.should == []
+ Hash.new { 5 }.keys.should == []
+ { 1 => 2, 4 => 8, 2 => 4 }.keys.should == [1, 4, 2]
+ { 1 => 2, 2 => 4, 4 => 8 }.keys.should.is_a?(Array)
+ { nil => nil }.keys.should == [nil]
+ end
+
+ it "uses the same order as #values" do
+ h = { 1 => "1", 2 => "2", 3 => "3", 4 => "4" }
+
+ h.size.times do |i|
+ h[h.keys[i]].should == h.values[i]
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/length_spec.rb b/spec/ruby/core/hash/length_spec.rb
new file mode 100644
index 0000000000..325973306f
--- /dev/null
+++ b/spec/ruby/core/hash/length_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#length" do
+ it "is an alias of Hash#size" do
+ Hash.instance_method(:size).should == Hash.instance_method(:length)
+ end
+end
diff --git a/spec/ruby/core/hash/lt_spec.rb b/spec/ruby/core/hash/lt_spec.rb
new file mode 100644
index 0000000000..4ca326d077
--- /dev/null
+++ b/spec/ruby/core/hash/lt_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison'
+require_relative 'shared/less_than'
+
+describe "Hash#<" do
+ it_behaves_like :hash_comparison, :<
+ it_behaves_like :hash_less_than, :<
+
+ it "returns false if both hashes are identical" do
+ h = { a: 1, b: 2 }
+ (h < h).should == false
+ end
+end
+
+describe "Hash#<" do
+ before :each do
+ @hash = {a:1, b:2}
+ @bigger = {a:1, b:2, c:3}
+ @unrelated = {c:3, d:4}
+ @similar = {a:2, b:3}
+ end
+
+ it "returns false when receiver size is larger than argument" do
+ (@bigger < @hash).should == false
+ (@bigger < @unrelated).should == false
+ end
+
+ it "returns false when receiver size is the same as argument" do
+ (@hash < @hash).should == false
+ (@hash < @unrelated).should == false
+ (@unrelated < @hash).should == false
+ end
+
+ it "returns true when receiver is a subset of argument" do
+ (@hash < @bigger).should == true
+ end
+
+ it "returns false when keys match but values don't" do
+ (@hash < @similar).should == false
+ (@similar < @hash).should == false
+ end
+end
diff --git a/spec/ruby/core/hash/lte_spec.rb b/spec/ruby/core/hash/lte_spec.rb
new file mode 100644
index 0000000000..29e60273d1
--- /dev/null
+++ b/spec/ruby/core/hash/lte_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison'
+require_relative 'shared/less_than'
+
+describe "Hash#<=" do
+ it_behaves_like :hash_comparison, :<=
+ it_behaves_like :hash_less_than, :<=
+
+ it "returns true if both hashes are identical" do
+ h = { a: 1, b: 2 }
+ (h <= h).should == true
+ end
+end
+
+describe "Hash#<=" do
+ before :each do
+ @hash = {a:1, b:2}
+ @bigger = {a:1, b:2, c:3}
+ @unrelated = {c:3, d:4}
+ @similar = {a:2, b:3}
+ end
+
+ it "returns false when receiver size is larger than argument" do
+ (@bigger <= @hash).should == false
+ (@bigger <= @unrelated).should == false
+ end
+
+ it "returns false when receiver size is the same as argument" do
+ (@hash <= @unrelated).should == false
+ (@unrelated <= @hash).should == false
+ end
+
+ it "returns true when receiver is a subset of argument or equals to argument" do
+ (@hash <= @bigger).should == true
+ (@hash <= @hash).should == true
+ end
+
+ it "returns false when keys match but values don't" do
+ (@hash <= @similar).should == false
+ (@similar <= @hash).should == false
+ end
+end
diff --git a/spec/ruby/core/hash/member_spec.rb b/spec/ruby/core/hash/member_spec.rb
new file mode 100644
index 0000000000..e7309c3f7d
--- /dev/null
+++ b/spec/ruby/core/hash/member_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#member?" do
+ it "is an alias of Hash#include?" do
+ Hash.instance_method(:member?).should == Hash.instance_method(:include?)
+ end
+end
diff --git a/spec/ruby/core/hash/merge_spec.rb b/spec/ruby/core/hash/merge_spec.rb
new file mode 100644
index 0000000000..9e566fcee9
--- /dev/null
+++ b/spec/ruby/core/hash/merge_spec.rb
@@ -0,0 +1,123 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#merge" do
+ it "returns a new hash by combining self with the contents of other" do
+ h = { 1 => :a, 2 => :b, 3 => :c }.merge(a: 1, c: 2)
+ h.should == { c: 2, 1 => :a, 2 => :b, a: 1, 3 => :c }
+
+ hash = { a: 1, b: 2 }
+ {}.merge(hash).should == hash
+ hash.merge({}).should == hash
+
+ h = { 1 => :a, 2 => :b, 3 => :c }.merge(1 => :b)
+ h.should == { 1 => :b, 2 => :b, 3 => :c }
+
+ h = { 1 => :a, 2 => :b }.merge(1 => :b, 3 => :c)
+ h.should == { 1 => :b, 2 => :b, 3 => :c }
+ end
+
+ it "sets any duplicate key to the value of block if passed a block" do
+ h1 = { a: 2, b: 1, d: 5 }
+ h2 = { a: -2, b: 4, c: -3 }
+ r = h1.merge(h2) { |k,x,y| nil }
+ r.should == { a: nil, b: nil, c: -3, d: 5 }
+
+ r = h1.merge(h2) { |k,x,y| "#{k}:#{x+2*y}" }
+ r.should == { a: "a:-2", b: "b:9", c: -3, d: 5 }
+
+ -> {
+ h1.merge(h2) { |k, x, y| raise(IndexError) }
+ }.should.raise(IndexError)
+
+ r = h1.merge(h1) { |k,x,y| :x }
+ r.should == { a: :x, b: :x, d: :x }
+ end
+
+ it "tries to convert the passed argument to a hash using #to_hash" do
+ obj = mock('{1=>2}')
+ obj.should_receive(:to_hash).and_return({ 1 => 2 })
+ { 3 => 4 }.merge(obj).should == { 1 => 2, 3 => 4 }
+ end
+
+ it "does not call to_hash on hash subclasses" do
+ { 3 => 4 }.merge(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 }
+ end
+
+ it "returns subclass instance for subclasses" do
+ HashSpecs::MyHash[1 => 2, 3 => 4].merge({ 1 => 2 }).should.instance_of?(HashSpecs::MyHash)
+ HashSpecs::MyHash[].merge({ 1 => 2 }).should.instance_of?(HashSpecs::MyHash)
+
+ { 1 => 2, 3 => 4 }.merge(HashSpecs::MyHash[1 => 2]).class.should == Hash
+ {}.merge(HashSpecs::MyHash[1 => 2]).class.should == Hash
+ end
+
+ it "processes entries with same order as each()" do
+ h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] }
+ merge_pairs = []
+ each_pairs = []
+ h.each_pair { |k, v| each_pairs << [k, v] }
+ h.merge(h) { |k, v1, v2| merge_pairs << [k, v1] }
+ merge_pairs.should == each_pairs
+ end
+
+ it "preserves the order of merged elements" do
+ h1 = { 1 => 2, 3 => 4, 5 => 6 }
+ h2 = { 1 => 7 }
+ merge_pairs = []
+ h1.merge(h2).each_pair { |k, v| merge_pairs << [k, v] }
+ merge_pairs.should == [[1,7], [3, 4], [5, 6]]
+ end
+
+ it "preserves the order of merged elements for large hashes" do
+ h1 = {}
+ h2 = {}
+ merge_pairs = []
+ expected_pairs = []
+ (1..100).each { |x| h1[x] = x; h2[101 - x] = x; expected_pairs << [x, 101 - x] }
+ h1.merge(h2).each_pair { |k, v| merge_pairs << [k, v] }
+ merge_pairs.should == expected_pairs
+ end
+
+ it "accepts multiple hashes" do
+ result = { a: 1 }.merge({ b: 2 }, { c: 3 }, { d: 4 })
+ result.should == { a: 1, b: 2, c: 3, d: 4 }
+ end
+
+ it "accepts zero arguments and returns a copy of self" do
+ hash = { a: 1 }
+ merged = hash.merge
+
+ merged.should.eql?(hash)
+ merged.should_not.equal?(hash)
+ end
+
+ it "retains the default value" do
+ h = Hash.new(1)
+ h.merge(b: 1, d: 2).default.should == 1
+ end
+
+ it "retains the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.merge(b: 1, d: 2).default_proc.should == pr
+ end
+
+ it "retains compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.merge(b: 1, d: 2)
+ h2.compare_by_identity?.should == true
+ end
+
+ it "ignores compare_by_identity flag of an argument" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = { b: 1, d: 2 }.merge(h)
+ h2.compare_by_identity?.should == false
+ end
+end
+
+describe "Hash#merge!" do
+ it "is an alias of Hash#update" do
+ Hash.instance_method(:merge!).should == Hash.instance_method(:update)
+ end
+end
diff --git a/spec/ruby/core/hash/new_spec.rb b/spec/ruby/core/hash/new_spec.rb
new file mode 100644
index 0000000000..207fc2931f
--- /dev/null
+++ b/spec/ruby/core/hash/new_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Actually these are specs of Hash#initialize, there is no Hash.new it's just Class.new.
+describe "Hash.new" do
+ it "creates an empty Hash if passed no arguments" do
+ Hash.new.should == {}
+ Hash.new.size.should == 0
+ end
+
+ it "creates a new Hash with default object if passed a default argument" do
+ Hash.new(5).default.should == 5
+ Hash.new({}).default.should == {}
+ end
+
+ it "does not create a copy of the default argument" do
+ str = "foo"
+ Hash.new(str).default.should.equal?(str)
+ end
+
+ it "creates a Hash with a default_proc if passed a block" do
+ Hash.new.default_proc.should == nil
+
+ h = Hash.new { |x| "Answer to #{x}" }
+ h.default_proc.call(5).should == "Answer to 5"
+ h.default_proc.call("x").should == "Answer to x"
+ end
+
+ it "raises an ArgumentError if more than one argument is passed" do
+ -> { Hash.new(5,6) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if passed both default argument and default block" do
+ -> { Hash.new(5) { 0 } }.should.raise(ArgumentError)
+ -> { Hash.new(nil) { 0 } }.should.raise(ArgumentError)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "emits a deprecation warning if keyword arguments are passed" do
+ -> { Hash.new(unknown: true) }.should complain(
+ Regexp.new(Regexp.escape("Calling Hash.new with keyword arguments is deprecated and will be removed in Ruby 3.4; use Hash.new({ key: value }) instead"))
+ )
+
+ -> { Hash.new(1, unknown: true) }.should.raise(ArgumentError)
+ -> { Hash.new(unknown: true) { 0 } }.should.raise(ArgumentError)
+
+ Hash.new({ unknown: true }).default.should == { unknown: true }
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "accepts a capacity: argument" do
+ Hash.new(5, capacity: 42).default.should == 5
+ Hash.new(capacity: 42).default.should == nil
+ (Hash.new(capacity: 42) { 1 }).default_proc.should_not == nil
+ end
+
+ it "ignores negative capacity" do
+ -> { Hash.new(capacity: -42) }.should_not.raise
+ end
+
+ it "raises an error if unknown keyword arguments are passed" do
+ -> { Hash.new(unknown: true) }.should.raise(ArgumentError)
+ -> { Hash.new(1, unknown: true) }.should.raise(ArgumentError)
+ -> { Hash.new(unknown: true) { 0 } }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/rassoc_spec.rb b/spec/ruby/core/hash/rassoc_spec.rb
new file mode 100644
index 0000000000..73379f6867
--- /dev/null
+++ b/spec/ruby/core/hash/rassoc_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+
+describe "Hash#rassoc" do
+ before :each do
+ @h = {apple: :green, orange: :orange, grape: :green, banana: :yellow}
+ end
+
+ it "returns an Array if the argument is a value of the Hash" do
+ @h.rassoc(:green).should.instance_of?(Array)
+ end
+
+ it "returns a 2-element Array if the argument is a value of the Hash" do
+ @h.rassoc(:orange).size.should == 2
+ end
+
+ it "sets the first element of the Array to the key of the located value" do
+ @h.rassoc(:yellow).first.should == :banana
+ end
+
+ it "sets the last element of the Array to the located value" do
+ @h.rassoc(:yellow).last.should == :yellow
+ end
+
+ it "only returns the first matching key-value pair" do
+ @h.rassoc(:green).should == [:apple, :green]
+ end
+
+ it "uses #== to compare the argument to the values" do
+ @h[:key] = 1.0
+ 1.should == 1.0
+ @h.rassoc(1).should.eql? [:key, 1.0]
+ end
+
+ it "returns nil if the argument is not a value of the Hash" do
+ @h.rassoc(:banana).should == nil
+ end
+
+ it "returns nil if the argument is not a value of the Hash even when there is a default" do
+ Hash.new(42).merge!( foo: :bar ).rassoc(42).should == nil
+ Hash.new{|h, k| h[k] = 42}.merge!( foo: :bar ).rassoc(42).should == nil
+ end
+end
diff --git a/spec/ruby/core/hash/rehash_spec.rb b/spec/ruby/core/hash/rehash_spec.rb
new file mode 100644
index 0000000000..fcd5a037bd
--- /dev/null
+++ b/spec/ruby/core/hash/rehash_spec.rb
@@ -0,0 +1,114 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#rehash" do
+ it "reorganizes the Hash by recomputing all key hash codes" do
+ k1 = Object.new
+ k2 = Object.new
+ def k1.hash; 0; end
+ def k2.hash; 1; end
+
+ h = {}
+ h[k1] = :v1
+ h[k2] = :v2
+
+ def k1.hash; 1; end
+
+ # The key should no longer be found as the #hash changed.
+ # Hash values 0 and 1 should not conflict, even with 1-bit stored hash.
+ h.key?(k1).should == false
+
+ h.keys.include?(k1).should == true
+
+ h.rehash.should.equal?(h)
+ h.key?(k1).should == true
+ h[k1].should == :v1
+ end
+
+ it "calls #hash for each key" do
+ k1 = mock('k1')
+ k2 = mock('k2')
+ v1 = mock('v1')
+ v2 = mock('v2')
+
+ v1.should_not_receive(:hash)
+ v2.should_not_receive(:hash)
+
+ h = { k1 => v1, k2 => v2 }
+
+ k1.should_receive(:hash).twice.and_return(0)
+ k2.should_receive(:hash).twice.and_return(0)
+
+ h.rehash
+ h[k1].should == v1
+ h[k2].should == v2
+ end
+
+ it "removes duplicate keys" do
+ a = [1,2]
+ b = [1]
+
+ h = {}
+ h[a] = true
+ h[b] = true
+ b << 2
+ h.size.should == 2
+ h.keys.should == [a, b]
+ h.rehash
+ h.size.should == 1
+ h.keys.should == [a]
+ end
+
+ it "removes duplicate keys for large hashes" do
+ a = [1,2]
+ b = [1]
+
+ h = {}
+ h[a] = true
+ h[b] = true
+ 100.times { |n| h[n] = true }
+ b << 2
+ h.size.should == 102
+ h.keys.should.include? a
+ h.keys.should.include? b
+ h.rehash
+ h.size.should == 101
+ h.keys.should.include? a
+ h.keys.should_not.include? [1]
+ end
+
+ it "iterates keys in insertion order" do
+ key = Class.new do
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def hash
+ 123
+ end
+ end
+
+ a, b, c, d = key.new('a'), key.new('b'), key.new('c'), key.new('d')
+ h = { a => 1, b => 2, c => 3, d => 4 }
+ h.size.should == 4
+
+ key.class_exec do
+ def eql?(other)
+ true
+ end
+ end
+
+ h.rehash
+ h.size.should == 1
+ k, v = h.first
+ k.name.should == 'a'
+ v.should == 4
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.rehash }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.rehash }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/reject_spec.rb b/spec/ruby/core/hash/reject_spec.rb
new file mode 100644
index 0000000000..aa1a074897
--- /dev/null
+++ b/spec/ruby/core/hash/reject_spec.rb
@@ -0,0 +1,116 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/iteration'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Hash#reject" do
+ it "returns a new hash removing keys for which the block yields true" do
+ h = { 1=>false, 2=>true, 3=>false, 4=>true }
+ h.reject { |k,v| v }.keys.sort.should == [1,3]
+ end
+
+ it "is equivalent to hsh.dup.delete_if" do
+ h = { a: 'a', b: 'b', c: 'd' }
+ h.reject { |k,v| k == 'd' }.should == (h.dup.delete_if { |k, v| k == 'd' })
+
+ all_args_reject = []
+ all_args_delete_if = []
+ h = { 1 => 2, 3 => 4 }
+ h.reject { |*args| all_args_reject << args }
+ h.delete_if { |*args| all_args_delete_if << args }
+ all_args_reject.should == all_args_delete_if
+
+ h = { 1 => 2 }
+ # dup doesn't copy singleton methods
+ def h.to_a() end
+ h.reject { false }.to_a.should == [[1, 2]]
+ end
+
+ context "with extra state" do
+ it "returns Hash instance for subclasses" do
+ HashSpecs::MyHash[1 => 2, 3 => 4].reject { false }.should.is_a?(Hash)
+ HashSpecs::MyHash[1 => 2, 3 => 4].reject { true }.should.is_a?(Hash)
+ end
+ end
+
+ it "processes entries with the same order as reject!" do
+ h = { a: 1, b: 2, c: 3, d: 4 }
+
+ reject_pairs = []
+ reject_bang_pairs = []
+ h.dup.reject { |*pair| reject_pairs << pair }
+ h.reject! { |*pair| reject_bang_pairs << pair }
+
+ reject_pairs.should == reject_bang_pairs
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.reject { false }.default.should == nil
+ h[:a] = 1
+ h.reject { false }.default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.reject { false }.default_proc.should == nil
+ h[:a] = 1
+ h.reject { false }.default_proc.should == nil
+ end
+
+ it "retains compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.reject { |k, _| k == :a }
+ h2.compare_by_identity?.should == true
+ end
+
+ it_behaves_like :hash_iteration_no_block, :reject
+ it_behaves_like :enumeratorized_with_origin_size, :reject, { 1 => 2, 3 => 4, 5 => 6 }
+end
+
+describe "Hash#reject!" do
+ it "removes keys from self for which the block yields true" do
+ hsh = {}
+ (1 .. 10).each { |k| hsh[k] = (k % 2 == 0) }
+ hsh.reject! { |k,v| v }
+ hsh.keys.sort.should == [1,3,5,7,9]
+ end
+
+ it "removes all entries if the block is true" do
+ h = { a: 1, b: 2, c: 3 }
+ h.reject! { |k,v| true }.should.equal?(h)
+ h.should == {}
+ end
+
+ it "is equivalent to delete_if if changes are made" do
+ hsh = { a: 1 }
+ hsh.reject! { |k,v| v < 2 }.should == hsh.dup.delete_if { |k, v| v < 2 }
+ end
+
+ it "returns nil if no changes were made" do
+ { a: 1 }.reject! { |k,v| v > 1 }.should == nil
+ end
+
+ it "processes entries with the same order as delete_if" do
+ h = { a: 1, b: 2, c: 3, d: 4 }
+
+ reject_bang_pairs = []
+ delete_if_pairs = []
+ h.dup.reject! { |*pair| reject_bang_pairs << pair }
+ h.dup.delete_if { |*pair| delete_if_pairs << pair }
+
+ reject_bang_pairs.should == delete_if_pairs
+ end
+
+ it "raises a FrozenError if called on a frozen instance that is modified" do
+ -> { HashSpecs.empty_frozen_hash.reject! { true } }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError if called on a frozen instance that would not be modified" do
+ -> { HashSpecs.frozen_hash.reject! { false } }.should.raise(FrozenError)
+ end
+
+ it_behaves_like :hash_iteration_no_block, :reject!
+ it_behaves_like :enumeratorized_with_origin_size, :reject!, { 1 => 2, 3 => 4, 5 => 6 }
+end
diff --git a/spec/ruby/core/hash/replace_spec.rb b/spec/ruby/core/hash/replace_spec.rb
new file mode 100644
index 0000000000..4dacbf9779
--- /dev/null
+++ b/spec/ruby/core/hash/replace_spec.rb
@@ -0,0 +1,79 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#replace" do
+ it "replaces the contents of self with other" do
+ h = { a: 1, b: 2 }
+ h.replace(c: -1, d: -2).should.equal?(h)
+ h.should == { c: -1, d: -2 }
+ end
+
+ it "tries to convert the passed argument to a hash using #to_hash" do
+ obj = mock('{1=>2,3=>4}')
+ obj.should_receive(:to_hash).and_return({ 1 => 2, 3 => 4 })
+
+ h = {}
+ h.replace(obj)
+ h.should == { 1 => 2, 3 => 4 }
+ end
+
+ it "calls to_hash on hash subclasses" do
+ h = {}
+ h.replace(HashSpecs::ToHashHash[1 => 2])
+ h.should == { 1 => 2 }
+ end
+
+ it "does not retain the default value" do
+ hash = Hash.new(1)
+ hash.replace(b: 2).default.should == nil
+ end
+
+ it "transfers the default value of an argument" do
+ hash = Hash.new(1)
+ { a: 1 }.replace(hash).default.should == 1
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ hash = Hash.new(&pr)
+ hash.replace(b: 2).default_proc.should == nil
+ end
+
+ it "transfers the default_proc of an argument" do
+ pr = proc { |h, k| h[k] = [] }
+ hash = Hash.new(&pr)
+ { a: 1 }.replace(hash).default_proc.should == pr
+ end
+
+ it "does not call the default_proc of an argument" do
+ hash_a = Hash.new { |h, k| k * 5 }
+ hash_b = Hash.new(-> { raise "Should not invoke lambda" })
+ hash_a.replace(hash_b)
+ hash_a.default.should == hash_b.default
+ end
+
+ it "transfers compare_by_identity flag of an argument" do
+ h = { a: 1, c: 3 }
+ h2 = { b: 2, d: 4 }.compare_by_identity
+ h.replace(h2)
+ h.compare_by_identity?.should == true
+ end
+
+ it "does not retain compare_by_identity flag" do
+ h = { a: 1, c: 3 }.compare_by_identity
+ h.replace(b: 2, d: 4)
+ h.compare_by_identity?.should == false
+ end
+
+ it "raises a FrozenError if called on a frozen instance that would not be modified" do
+ -> do
+ HashSpecs.frozen_hash.replace(HashSpecs.frozen_hash)
+ end.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError if called on a frozen instance that is modified" do
+ -> do
+ HashSpecs.frozen_hash.replace(HashSpecs.empty_frozen_hash)
+ end.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb
new file mode 100644
index 0000000000..609a53f81f
--- /dev/null
+++ b/spec/ruby/core/hash/ruby2_keywords_hash_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash.ruby2_keywords_hash?" do
+ it "returns false if the Hash is not a keywords Hash" do
+ Hash.ruby2_keywords_hash?({}).should == false
+ end
+
+ it "returns true if the Hash is a keywords Hash marked by Module#ruby2_keywords" do
+ obj = Class.new {
+ ruby2_keywords def m(*args)
+ args.last
+ end
+ }.new
+ Hash.ruby2_keywords_hash?(obj.m(a: 1)).should == true
+ end
+
+ it "raises TypeError for non-Hash" do
+ -> { Hash.ruby2_keywords_hash?(nil) }.should.raise(TypeError)
+ end
+end
+
+describe "Hash.ruby2_keywords_hash" do
+ it "returns a copy of a Hash and marks the copy as a keywords Hash" do
+ h = {a: 1}.freeze
+ kw = Hash.ruby2_keywords_hash(h)
+ Hash.ruby2_keywords_hash?(h).should == false
+ Hash.ruby2_keywords_hash?(kw).should == true
+ kw.should == h
+ end
+
+ it "returns an instance of the subclass if called on an instance of a subclass of Hash" do
+ h = HashSpecs::MyHash.new
+ h[:a] = 1
+ kw = Hash.ruby2_keywords_hash(h)
+ kw.class.should == HashSpecs::MyHash
+ Hash.ruby2_keywords_hash?(h).should == false
+ Hash.ruby2_keywords_hash?(kw).should == true
+ kw.should == h
+ end
+
+ it "copies instance variables" do
+ h = {a: 1}
+ h.instance_variable_set(:@foo, 42)
+ kw = Hash.ruby2_keywords_hash(h)
+ kw.instance_variable_get(:@foo).should == 42
+ end
+
+ it "copies the hash internals" do
+ h = {a: 1}
+ kw = Hash.ruby2_keywords_hash(h)
+ h[:a] = 2
+ kw[:a].should == 1
+ end
+
+ it "raises TypeError for non-Hash" do
+ -> { Hash.ruby2_keywords_hash(nil) }.should.raise(TypeError)
+ end
+
+ it "retains the default value" do
+ hash = Hash.new(1)
+ Hash.ruby2_keywords_hash(hash).default.should == 1
+ hash[:a] = 1
+ Hash.ruby2_keywords_hash(hash).default.should == 1
+ end
+
+ it "retains the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ hash = Hash.new(&pr)
+ Hash.ruby2_keywords_hash(hash).default_proc.should == pr
+ hash[:a] = 1
+ Hash.ruby2_keywords_hash(hash).default_proc.should == pr
+ end
+
+ it "retains compare_by_identity_flag" do
+ hash = {}.compare_by_identity
+ Hash.ruby2_keywords_hash(hash).compare_by_identity?.should == true
+ hash[:a] = 1
+ Hash.ruby2_keywords_hash(hash).compare_by_identity?.should == true
+ end
+end
diff --git a/spec/ruby/core/hash/select_spec.rb b/spec/ruby/core/hash/select_spec.rb
new file mode 100644
index 0000000000..60b2ce67c1
--- /dev/null
+++ b/spec/ruby/core/hash/select_spec.rb
@@ -0,0 +1,112 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+require_relative 'fixtures/classes'
+require_relative 'shared/iteration'
+
+describe "Hash#select" do
+ before :each do
+ @hsh = { 1 => 2, 3 => 4, 5 => 6 }
+ @empty = {}
+ end
+
+ it "yields two arguments: key and value" do
+ all_args = []
+ { 1 => 2, 3 => 4 }.select { |*args| all_args << args }
+ all_args.sort.should == [[1, 2], [3, 4]]
+ end
+
+ it "returns a Hash of entries for which block is true" do
+ a_pairs = { 'a' => 9, 'c' => 4, 'b' => 5, 'd' => 2 }.select { |k,v| v % 2 == 0 }
+ a_pairs.should.instance_of?(Hash)
+ a_pairs.sort.should == [['c', 4], ['d', 2]]
+ end
+
+ it "processes entries with the same order as reject" do
+ h = { a: 9, c: 4, b: 5, d: 2 }
+
+ select_pairs = []
+ reject_pairs = []
+ h.dup.select{ |*pair| select_pairs << pair }
+ h.reject { |*pair| reject_pairs << pair }
+
+ select_pairs.should == reject_pairs
+ end
+
+ it "returns an Enumerator when called on a non-empty hash without a block" do
+ @hsh.select.should.instance_of?(Enumerator)
+ end
+
+ it "returns an Enumerator when called on an empty hash without a block" do
+ @empty.select.should.instance_of?(Enumerator)
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.select { true }.default.should == nil
+ h[:a] = 1
+ h.select { true }.default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.select { true }.default_proc.should == nil
+ h[:a] = 1
+ h.select { true }.default_proc.should == nil
+ end
+
+ it "retains compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.select { |k, _| k == :a }
+ h2.compare_by_identity?.should == true
+ end
+
+ it_behaves_like :hash_iteration_no_block, :select
+
+ before :each do
+ @object = { 1 => 2, 3 => 4, 5 => 6 }
+ end
+ it_behaves_like :enumeratorized_with_origin_size, :select
+end
+
+describe "Hash#select!" do
+ before :each do
+ @hsh = { 1 => 2, 3 => 4, 5 => 6 }
+ @empty = {}
+ end
+
+ it "is equivalent to keep_if if changes are made" do
+ h = { a: 2 }
+ h.select! { |k,v| v <= 1 }.should.equal? h
+
+ h = { 1 => 2, 3 => 4 }
+ all_args_select = []
+ h.dup.select! { |*args| all_args_select << args }
+ all_args_select.should == [[1, 2], [3, 4]]
+ end
+
+ it "removes all entries if the block is false" do
+ h = { a: 1, b: 2, c: 3 }
+ h.select! { |k,v| false }.should.equal?(h)
+ h.should == {}
+ end
+
+ it "returns nil if no changes were made" do
+ { a: 1 }.select! { |k,v| v <= 1 }.should == nil
+ end
+
+ it "raises a FrozenError if called on an empty frozen instance" do
+ -> { HashSpecs.empty_frozen_hash.select! { false } }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError if called on a frozen instance that would not be modified" do
+ -> { HashSpecs.frozen_hash.select! { true } }.should.raise(FrozenError)
+ end
+
+ it_behaves_like :hash_iteration_no_block, :select!
+
+ before :each do
+ @object = { 1 => 2, 3 => 4, 5 => 6 }
+ end
+ it_behaves_like :enumeratorized_with_origin_size, :select!
+end
diff --git a/spec/ruby/core/hash/shared/comparison.rb b/spec/ruby/core/hash/shared/comparison.rb
new file mode 100644
index 0000000000..2b62837232
--- /dev/null
+++ b/spec/ruby/core/hash/shared/comparison.rb
@@ -0,0 +1,15 @@
+describe :hash_comparison, shared: true do
+ it "raises a TypeError if the right operand is not a hash" do
+ -> { { a: 1 }.send(@method, 1) }.should.raise(TypeError)
+ -> { { a: 1 }.send(@method, nil) }.should.raise(TypeError)
+ -> { { a: 1 }.send(@method, []) }.should.raise(TypeError)
+ end
+
+ it "returns false if both hashes have the same keys but different values" do
+ h1 = { a: 1 }
+ h2 = { a: 2 }
+
+ h1.send(@method, h2).should == false
+ h2.send(@method, h1).should == false
+ end
+end
diff --git a/spec/ruby/core/hash/shared/eql.rb b/spec/ruby/core/hash/shared/eql.rb
new file mode 100644
index 0000000000..512e1ad016
--- /dev/null
+++ b/spec/ruby/core/hash/shared/eql.rb
@@ -0,0 +1,204 @@
+describe :hash_eql, shared: true do
+ it "does not compare values when keys don't match" do
+ value = mock('x')
+ value.should_not_receive(:==)
+ value.should_not_receive(:eql?)
+ { 1 => value }.send(@method, { 2 => value }).should == false
+ end
+
+ it "returns false when the numbers of keys differ without comparing any elements" do
+ obj = mock('x')
+ h = { obj => obj }
+
+ obj.should_not_receive(:==)
+ obj.should_not_receive(:eql?)
+
+ {}.send(@method, h).should == false
+ h.send(@method, {}).should == false
+ end
+
+ it "first compares keys via hash" do
+ x = mock('x')
+ x.should_receive(:hash).any_number_of_times.and_return(0)
+ y = mock('y')
+ y.should_receive(:hash).any_number_of_times.and_return(0)
+
+ { x => 1 }.send(@method, { y => 1 }).should == false
+ end
+
+ it "does not compare keys with different hash codes via eql?" do
+ x = mock('x')
+ y = mock('y')
+ x.should_not_receive(:eql?)
+ y.should_not_receive(:eql?)
+
+ x.should_receive(:hash).any_number_of_times.and_return(0)
+ y.should_receive(:hash).any_number_of_times.and_return(1)
+
+ { x => 1 }.send(@method, { y => 1 }).should == false
+ end
+
+ it "computes equality for recursive hashes" do
+ h = {}
+ h[:a] = h
+ h.send(@method, h[:a]).should == true
+ (h == h[:a]).should == true
+ end
+
+ it "doesn't call to_hash on objects" do
+ mock_hash = mock("fake hash")
+ def mock_hash.to_hash() {} end
+ {}.send(@method, mock_hash).should == false
+ end
+
+ it "computes equality for complex recursive hashes" do
+ a, b = {}, {}
+ a.merge! self: a, other: b
+ b.merge! self: b, other: a
+ a.send(@method, b).should == true # they both have the same structure!
+
+ c = {}
+ c.merge! other: c, self: c
+ c.send(@method, a).should == true # subtle, but they both have the same structure!
+ a[:delta] = c[:delta] = a
+ c.send(@method, a).should == false # not quite the same structure, as a[:other][:delta] = nil
+ c[:delta] = 42
+ c.send(@method, a).should == false
+ a[:delta] = 42
+ c.send(@method, a).should == false
+ b[:delta] = 42
+ c.send(@method, a).should == true
+ end
+
+ it "computes equality for recursive hashes & arrays" do
+ x, y, z = [], [], []
+ a, b, c = {foo: x, bar: 42}, {foo: y, bar: 42}, {foo: z, bar: 42}
+ x << a
+ y << c
+ z << b
+ b.send(@method, c).should == true # they clearly have the same structure!
+ y.send(@method, z).should == true
+ a.send(@method, b).should == true # subtle, but they both have the same structure!
+ x.send(@method, y).should == true
+ y << x
+ y.send(@method, z).should == false
+ z << x
+ y.send(@method, z).should == true
+
+ a[:foo], a[:bar] = a[:bar], a[:foo]
+ a.send(@method, b).should == false
+ b[:bar] = b[:foo]
+ b.send(@method, c).should == false
+ end
+end
+
+describe :hash_eql_additional, shared: true do
+ it "compares values when keys match" do
+ x = mock('x')
+ y = mock('y')
+ def x.==(o) false end
+ def y.==(o) false end
+ def x.eql?(o) false end
+ def y.eql?(o) false end
+ { 1 => x }.send(@method, { 1 => y }).should == false
+
+ x = mock('x')
+ y = mock('y')
+ def x.==(o) true end
+ def y.==(o) true end
+ def x.eql?(o) true end
+ def y.eql?(o) true end
+ { 1 => x }.send(@method, { 1 => y }).should == true
+ end
+
+ it "compares keys with eql? semantics" do
+ { 1.0 => "x" }.send(@method, { 1.0 => "x" }).should == true
+ { 1.0 => "x" }.send(@method, { 1.0 => "x" }).should == true
+ { 1 => "x" }.send(@method, { 1.0 => "x" }).should == false
+ { 1.0 => "x" }.send(@method, { 1 => "x" }).should == false
+ end
+
+ it "returns true if and only if other Hash has the same number of keys and each key-value pair matches" do
+ a = { a: 5 }
+ b = {}
+ a.send(@method, b).should == false
+
+ b[:a] = 5
+ a.send(@method, b).should == true
+
+ not_supported_on :opal do
+ c = { "a" => 5 }
+ a.send(@method, c).should == false
+ end
+
+ c = { "A" => 5 }
+ a.send(@method, c).should == false
+
+ c = { a: 6 }
+ a.send(@method, c).should == false
+ end
+
+ it "does not call to_hash on hash subclasses" do
+ { 5 => 6 }.send(@method, HashSpecs::ToHashHash[5 => 6]).should == true
+ end
+
+ it "ignores hash class differences" do
+ h = { 1 => 2, 3 => 4 }
+ HashSpecs::MyHash[h].send(@method, h).should == true
+ HashSpecs::MyHash[h].send(@method, HashSpecs::MyHash[h]).should == true
+ h.send(@method, HashSpecs::MyHash[h]).should == true
+ end
+
+ # Why isn't this true of eql? too ?
+ it "compares keys with matching hash codes via eql?" do
+ a = Array.new(2) do
+ obj = mock('0')
+ obj.should_receive(:hash).at_least(1).and_return(0)
+
+ def obj.eql?(o)
+ return true if self.equal?(o)
+ false
+ end
+
+ obj
+ end
+
+ { a[0] => 1 }.send(@method, { a[1] => 1 }).should == false
+
+ a = Array.new(2) do
+ obj = mock('0')
+ obj.should_receive(:hash).at_least(1).and_return(0)
+
+ def obj.eql?(o)
+ true
+ end
+
+ obj
+ end
+
+ { a[0] => 1 }.send(@method, { a[1] => 1 }).should == true
+ end
+
+ it "compares the values in self to values in other hash" do
+ l_val = mock("left")
+ r_val = mock("right")
+
+ l_val.should_receive(:eql?).with(r_val).and_return(true)
+
+ { 1 => l_val }.eql?({ 1 => r_val }).should == true
+ end
+end
+
+describe :hash_eql_additional_more, shared: true do
+ it "returns true if other Hash has the same number of keys and each key-value pair matches, even though the default-value are not same" do
+ Hash.new(5).send(@method, Hash.new(1)).should == true
+ Hash.new {|h, k| 1}.send(@method, Hash.new {}).should == true
+ Hash.new {|h, k| 1}.send(@method, Hash.new(2)).should == true
+
+ d = Hash.new {|h, k| 1}
+ e = Hash.new {}
+ d[1] = 2
+ e[1] = 2
+ d.send(@method, e).should == true
+ end
+end
diff --git a/spec/ruby/core/hash/shared/greater_than.rb b/spec/ruby/core/hash/shared/greater_than.rb
new file mode 100644
index 0000000000..dfe0b80250
--- /dev/null
+++ b/spec/ruby/core/hash/shared/greater_than.rb
@@ -0,0 +1,23 @@
+describe :hash_greater_than, shared: true do
+ before do
+ @h1 = { a: 1, b: 2, c: 3 }
+ @h2 = { a: 1, b: 2 }
+ end
+
+ it "returns true if the other hash is a subset of self" do
+ @h1.send(@method, @h2).should == true
+ end
+
+ it "returns false if the other hash is not a subset of self" do
+ @h2.send(@method, @h1).should == false
+ end
+
+ it "converts the right operand to a hash before comparing" do
+ o = Object.new
+ def o.to_hash
+ { a: 1, b: 2 }
+ end
+
+ @h1.send(@method, o).should == true
+ end
+end
diff --git a/spec/ruby/core/hash/shared/iteration.rb b/spec/ruby/core/hash/shared/iteration.rb
new file mode 100644
index 0000000000..322e4b9a2f
--- /dev/null
+++ b/spec/ruby/core/hash/shared/iteration.rb
@@ -0,0 +1,19 @@
+describe :hash_iteration_no_block, shared: true do
+ before :each do
+ @hsh = { 1 => 2, 3 => 4, 5 => 6 }
+ @empty = {}
+ end
+
+ it "returns an Enumerator if called on a non-empty hash without a block" do
+ @hsh.send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "returns an Enumerator if called on an empty hash without a block" do
+ @empty.send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "returns an Enumerator if called on a frozen instance" do
+ @hsh.freeze
+ @hsh.send(@method).should.instance_of?(Enumerator)
+ end
+end
diff --git a/spec/ruby/core/hash/shared/less_than.rb b/spec/ruby/core/hash/shared/less_than.rb
new file mode 100644
index 0000000000..071b3e97bb
--- /dev/null
+++ b/spec/ruby/core/hash/shared/less_than.rb
@@ -0,0 +1,23 @@
+describe :hash_less_than, shared: true do
+ before do
+ @h1 = { a: 1, b: 2 }
+ @h2 = { a: 1, b: 2, c: 3 }
+ end
+
+ it "returns true if self is a subset of the other hash" do
+ @h1.send(@method, @h2).should == true
+ end
+
+ it "returns false if self is not a subset of the other hash" do
+ @h2.send(@method, @h1).should == false
+ end
+
+ it "converts the right operand to a hash before comparing" do
+ o = Object.new
+ def o.to_hash
+ { a: 1, b: 2, c: 3 }
+ end
+
+ @h1.send(@method, o).should == true
+ end
+end
diff --git a/spec/ruby/core/hash/shift_spec.rb b/spec/ruby/core/hash/shift_spec.rb
new file mode 100644
index 0000000000..6095d2e55f
--- /dev/null
+++ b/spec/ruby/core/hash/shift_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#shift" do
+ it "removes a pair from hash and return it" do
+ h = { a: 1, b: 2, "c" => 3, nil => 4, [] => 5 }
+ h2 = h.dup
+
+ h.size.times do |i|
+ r = h.shift
+ r.should.is_a?(Array)
+ h2[r.first].should == r.last
+ h.size.should == h2.size - i - 1
+ end
+
+ h.should == {}
+ end
+
+ # MRI explicitly implements this behavior
+ it "allows shifting entries while iterating" do
+ h = { a: 1, b: 2, c: 3 }
+ visited = []
+ shifted = []
+ h.each_pair { |k,v|
+ visited << k
+ shifted << h.shift
+ }
+ visited.should == [:a, :b, :c]
+ shifted.should == [[:a, 1], [:b, 2], [:c, 3]]
+ h.should == {}
+ end
+
+ it "returns nil if the Hash is empty" do
+ h = {}
+ def h.default(key)
+ raise
+ end
+ h.shift.should == nil
+ end
+
+ it "returns nil from an empty hash" do
+ {}.shift.should == nil
+ end
+
+ it "returns nil for empty hashes with defaults and default procs" do
+ Hash.new(5).shift.should == nil
+ h = Hash.new { |*args| args }
+ h.shift.should == nil
+ end
+
+ it "preserves Hash invariants when removing the last item" do
+ h = { :a => 1, :b => 2 }
+ h.shift.should == [:a, 1]
+ h.shift.should == [:b, 2]
+ h[:c] = 3
+ h.should == {:c => 3}
+ end
+
+ it "raises a FrozenError if called on a frozen instance" do
+ -> { HashSpecs.frozen_hash.shift }.should.raise(FrozenError)
+ -> { HashSpecs.empty_frozen_hash.shift }.should.raise(FrozenError)
+ end
+
+ it "works when the hash is at capacity" do
+ # We try a wide range of sizes in hopes that this will cover all implementations' base Hash size.
+ results = []
+ 1.upto(100) do |n|
+ h = {}
+ n.times do |i|
+ h[i] = i
+ end
+ h.shift
+ results << h.size
+ end
+
+ results.should == 0.upto(99).to_a
+ end
+end
diff --git a/spec/ruby/core/hash/size_spec.rb b/spec/ruby/core/hash/size_spec.rb
new file mode 100644
index 0000000000..5e5008a5dc
--- /dev/null
+++ b/spec/ruby/core/hash/size_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "Hash#size" do
+ it "returns the number of entries" do
+ { a: 1, b: 'c' }.size.should == 2
+ h = { a: 1, b: 2 }
+ h[:a] = 2
+ h.size.should == 2
+ { a: 1, b: 1, c: 1 }.size.should == 3
+ {}.size.should == 0
+ Hash.new(5).size.should == 0
+ Hash.new { 5 }.size.should == 0
+ end
+end
diff --git a/spec/ruby/core/hash/slice_spec.rb b/spec/ruby/core/hash/slice_spec.rb
new file mode 100644
index 0000000000..fd93254517
--- /dev/null
+++ b/spec/ruby/core/hash/slice_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+
+describe "Hash#slice" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3 }
+ end
+
+ it "returns a new empty hash without arguments" do
+ ret = @hash.slice
+ ret.should_not.equal?(@hash)
+ ret.should.instance_of?(Hash)
+ ret.should == {}
+ end
+
+ it "returns the requested subset" do
+ @hash.slice(:c, :a).should == { c: 3, a: 1 }
+ end
+
+ it "returns a hash ordered in the order of the requested keys" do
+ @hash.slice(:c, :a).keys.should == [:c, :a]
+ end
+
+ it "returns only the keys of the original hash" do
+ @hash.slice(:a, :chunky_bacon).should == { a: 1 }
+ end
+
+ it "returns a Hash instance, even on subclasses" do
+ klass = Class.new(Hash)
+ h = klass.new
+ h[:bar] = 12
+ h[:foo] = 42
+ r = h.slice(:foo)
+ r.should == {foo: 42}
+ r.class.should == Hash
+ end
+
+ it "uses the regular Hash#[] method, even on subclasses that override it" do
+ ScratchPad.record []
+ klass = Class.new(Hash) do
+ def [](value)
+ ScratchPad << :used_subclassed_operator
+ super
+ end
+ end
+
+ h = klass.new
+ h[:bar] = 12
+ h[:foo] = 42
+ h.slice(:foo)
+
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.slice(:a).default.should == nil
+ h[:a] = 1
+ h.slice(:a).default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.slice(:a).default_proc.should == nil
+ h[:a] = 1
+ h.slice(:a).default_proc.should == nil
+ end
+
+ it "retains compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.slice(:a)
+ h2.compare_by_identity?.should == true
+ end
+end
diff --git a/spec/ruby/core/hash/sort_spec.rb b/spec/ruby/core/hash/sort_spec.rb
new file mode 100644
index 0000000000..26058c845e
--- /dev/null
+++ b/spec/ruby/core/hash/sort_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#sort" do
+ it "converts self to a nested array of [key, value] arrays and sort with Array#sort" do
+ { 'a' => 'b', '1' => '2', 'b' => 'a' }.sort.should ==
+ [["1", "2"], ["a", "b"], ["b", "a"]]
+ end
+
+ it "works when some of the keys are themselves arrays" do
+ { [1,2] => 5, [1,1] => 5 }.sort.should == [[[1,1],5], [[1,2],5]]
+ end
+
+ it "uses block to sort array if passed a block" do
+ { 1 => 2, 2 => 9, 3 => 4 }.sort { |a,b| b <=> a }.should == [[3, 4], [2, 9], [1, 2]]
+ end
+end
diff --git a/spec/ruby/core/hash/store_spec.rb b/spec/ruby/core/hash/store_spec.rb
new file mode 100644
index 0000000000..7017d8ba2b
--- /dev/null
+++ b/spec/ruby/core/hash/store_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#store" do
+ it "is an alias of Hash#[]=" do
+ Hash.instance_method(:store).should == Hash.instance_method(:[]=)
+ end
+end
diff --git a/spec/ruby/core/hash/to_a_spec.rb b/spec/ruby/core/hash/to_a_spec.rb
new file mode 100644
index 0000000000..8c638db6c3
--- /dev/null
+++ b/spec/ruby/core/hash/to_a_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#to_a" do
+ it "returns a list of [key, value] pairs with same order as each()" do
+ h = { a: 1, 1 => :a, 3 => :b, b: 5 }
+ pairs = []
+
+ h.each_pair do |key, value|
+ pairs << [key, value]
+ end
+
+ h.to_a.should.is_a?(Array)
+ h.to_a.should == pairs
+ end
+
+ it "is called for Enumerable#entries" do
+ h = { a: 1, 1 => :a, 3 => :b, b: 5 }
+ pairs = []
+
+ h.each_pair do |key, value|
+ pairs << [key, value]
+ end
+
+ ent = h.entries
+ ent.should.is_a?(Array)
+ ent.should == pairs
+ end
+end
diff --git a/spec/ruby/core/hash/to_h_spec.rb b/spec/ruby/core/hash/to_h_spec.rb
new file mode 100644
index 0000000000..5d5a280dac
--- /dev/null
+++ b/spec/ruby/core/hash/to_h_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#to_h" do
+ it "returns self for Hash instances" do
+ h = {}
+ h.to_h.should.equal?(h)
+ end
+
+ describe "when called on a subclass of Hash" do
+ before :each do
+ @h = HashSpecs::MyHash.new
+ @h[:foo] = :bar
+ end
+
+ it "returns a new Hash instance" do
+ @h.to_h.should.instance_of?(Hash)
+ @h.to_h.should == @h
+ @h[:foo].should == :bar
+ end
+
+ it "retains the default" do
+ @h.default = 42
+ @h.to_h.default.should == 42
+ @h[:hello].should == 42
+ end
+
+ it "retains the default_proc" do
+ @h.default_proc = prc = Proc.new{ |h, k| h[k] = 2 * k }
+ @h.to_h.default_proc.should == prc
+ @h[42].should == 84
+ end
+
+ it "retains compare_by_identity flag" do
+ @h.compare_by_identity
+ @h.to_h.compare_by_identity?.should == true
+ end
+ end
+
+ context "with block" do
+ it "converts [key, value] pairs returned by the block to a hash" do
+ { a: 1, b: 2 }.to_h { |k, v| [k.to_s, v*v]}.should == { "a" => 1, "b" => 4 }
+ end
+
+ it "passes to a block each pair's key and value as separate arguments" do
+ ScratchPad.record []
+ { a: 1, b: 2 }.to_h { |k, v| ScratchPad << [k, v]; [k, v] }
+ ScratchPad.recorded.sort.should == [[:a, 1], [:b, 2]]
+
+ ScratchPad.record []
+ { a: 1, b: 2 }.to_h { |*args| ScratchPad << args; [args[0], args[1]] }
+ ScratchPad.recorded.sort.should == [[:a, 1], [:b, 2]]
+ end
+
+ it "raises ArgumentError if block returns longer or shorter array" do
+ -> do
+ { a: 1, b: 2 }.to_h { |k, v| [k.to_s, v*v, 1] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+
+ -> do
+ { a: 1, b: 2 }.to_h { |k, v| [k] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+ end
+
+ it "raises TypeError if block returns something other than Array" do
+ -> do
+ { a: 1, b: 2 }.to_h { |k, v| "not-array" }
+ end.should.raise(TypeError, /wrong element type String/)
+ end
+
+ it "coerces returned pair to Array with #to_ary" do
+ x = mock('x')
+ x.stub!(:to_ary).and_return([:b, 'b'])
+
+ { a: 1 }.to_h { |k| x }.should == { :b => 'b' }
+ end
+
+ it "does not coerce returned pair to Array with #to_a" do
+ x = mock('x')
+ x.stub!(:to_a).and_return([:b, 'b'])
+
+ -> do
+ { a: 1 }.to_h { |k| x }
+ end.should.raise(TypeError, /wrong element type MockObject/)
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h2 = h.to_h { |k, v| [k.to_s, v*v]}
+ h2.default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h2 = h.to_h { |k, v| [k.to_s, v*v]}
+ h2.default_proc.should == nil
+ end
+
+ it "does not retain compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.to_h { |k, v| [k.to_s, v*v]}
+ h2.compare_by_identity?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/to_hash_spec.rb b/spec/ruby/core/hash/to_hash_spec.rb
new file mode 100644
index 0000000000..f5622b3d9c
--- /dev/null
+++ b/spec/ruby/core/hash/to_hash_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#to_hash" do
+ it "returns self for Hash instances" do
+ h = {}
+ h.to_hash.should.equal?(h)
+ end
+
+ it "returns self for instances of subclasses of Hash" do
+ h = HashSpecs::MyHash.new
+ h.to_hash.should.equal?(h)
+ end
+end
diff --git a/spec/ruby/core/hash/to_proc_spec.rb b/spec/ruby/core/hash/to_proc_spec.rb
new file mode 100644
index 0000000000..bc4756600d
--- /dev/null
+++ b/spec/ruby/core/hash/to_proc_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#to_proc" do
+ before :each do
+ @key = Object.new
+ @value = Object.new
+ @hash = { @key => @value }
+ @default = Object.new
+ @unstored = Object.new
+ end
+
+ it "returns an instance of Proc" do
+ @hash.to_proc.should.instance_of? Proc
+ end
+
+ describe "the returned proc" do
+ before :each do
+ @proc = @hash.to_proc
+ end
+
+ it "is a lambda" do
+ @proc.should.lambda?
+ end
+
+ it "has an arity of 1" do
+ @proc.arity.should == 1
+ end
+
+ it "raises ArgumentError if not passed exactly one argument" do
+ -> {
+ @proc.call
+ }.should.raise(ArgumentError)
+
+ -> {
+ @proc.call 1, 2
+ }.should.raise(ArgumentError)
+ end
+
+ context "with a stored key" do
+ it "returns the paired value" do
+ @proc.call(@key).should.equal?(@value)
+ end
+ end
+
+ context "passed as a block" do
+ it "retrieves the hash's values" do
+ [@key].map(&@proc)[0].should.equal?(@value)
+ end
+
+ context "to instance_exec" do
+ it "always retrieves the original hash's values" do
+ hash = {foo: 1, bar: 2}
+ proc = hash.to_proc
+
+ hash.instance_exec(:foo, &proc).should == 1
+
+ hash2 = {quux: 1}
+ hash2.instance_exec(:foo, &proc).should == 1
+ end
+ end
+ end
+
+ context "with no stored key" do
+ it "returns nil" do
+ @proc.call(@unstored).should == nil
+ end
+
+ context "when the hash has a default value" do
+ before :each do
+ @hash.default = @default
+ end
+
+ it "returns the default value" do
+ @proc.call(@unstored).should.equal?(@default)
+ end
+ end
+
+ context "when the hash has a default proc" do
+ it "returns an evaluated value from the default proc" do
+ @hash.default_proc = -> hash, called_with { [hash.keys, called_with] }
+ @proc.call(@unstored).should == [[@key], @unstored]
+ end
+ end
+ end
+
+ it "raises an ArgumentError when calling #call on the Proc with no arguments" do
+ -> { @hash.to_proc.call }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/to_s_spec.rb b/spec/ruby/core/hash/to_s_spec.rb
new file mode 100644
index 0000000000..2915db6ef8
--- /dev/null
+++ b/spec/ruby/core/hash/to_s_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#to_s" do
+ it "is an alias of Hash#inspect" do
+ Hash.instance_method(:to_s).should == Hash.instance_method(:inspect)
+ end
+end
diff --git a/spec/ruby/core/hash/transform_keys_spec.rb b/spec/ruby/core/hash/transform_keys_spec.rb
new file mode 100644
index 0000000000..d37a2b8616
--- /dev/null
+++ b/spec/ruby/core/hash/transform_keys_spec.rb
@@ -0,0 +1,150 @@
+require_relative '../../spec_helper'
+
+describe "Hash#transform_keys" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3 }
+ end
+
+ it "returns new hash" do
+ ret = @hash.transform_keys(&:succ)
+ ret.should_not.equal?(@hash)
+ ret.should.instance_of?(Hash)
+ end
+
+ it "sets the result as transformed keys with the given block" do
+ @hash.transform_keys(&:succ).should == { b: 1, c: 2, d: 3 }
+ end
+
+ it "keeps last pair if new keys conflict" do
+ @hash.transform_keys { |_| :a }.should == { a: 3 }
+ end
+
+ it "makes both hashes to share values" do
+ value = [1, 2, 3]
+ new_hash = { a: value }.transform_keys(&:upcase)
+ new_hash[:A].should.equal?(value)
+ end
+
+ context "when no block is given" do
+ it "returns a sized Enumerator" do
+ enumerator = @hash.transform_keys
+ enumerator.should.instance_of?(Enumerator)
+ enumerator.size.should == @hash.size
+ enumerator.each(&:succ).should == { b: 1, c: 2, d: 3 }
+ end
+ end
+
+ it "returns a Hash instance, even on subclasses" do
+ klass = Class.new(Hash)
+ h = klass.new
+ h[:foo] = 42
+ r = h.transform_keys{|v| :"x#{v}"}
+ r.keys.should == [:xfoo]
+ r.class.should == Hash
+ end
+
+ it "allows a hash argument" do
+ @hash.transform_keys({ a: :A, b: :B, c: :C }).should == { A: 1, B: 2, C: 3 }
+ end
+
+ it "allows a partial transformation of keys when using a hash argument" do
+ @hash.transform_keys({ a: :A, c: :C }).should == { A: 1, b: 2, C: 3 }
+ end
+
+ it "allows a combination of hash and block argument" do
+ @hash.transform_keys({ a: :A }, &:to_s).should == { A: 1, 'b' => 2, 'c' => 3 }
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.transform_keys(&:succ).default.should == nil
+ h[:a] = 1
+ h.transform_keys(&:succ).default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.transform_values(&:succ).default_proc.should == nil
+ h[:a] = 1
+ h.transform_values(&:succ).default_proc.should == nil
+ end
+
+ it "does not retain compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.transform_keys(&:succ)
+ h2.compare_by_identity?.should == false
+ end
+end
+
+describe "Hash#transform_keys!" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3, d: 4 }
+ @initial_pairs = @hash.dup
+ end
+
+ it "returns self" do
+ @hash.transform_keys!(&:succ).should.equal?(@hash)
+ end
+
+ it "updates self as transformed values with the given block" do
+ @hash.transform_keys!(&:to_s)
+ @hash.should == { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 }
+ end
+
+ it "prevents conflicts between new keys and old ones" do
+ @hash.transform_keys!(&:succ)
+ @hash.should == { b: 1, c: 2, d: 3, e: 4 }
+ end
+
+ it "returns the processed keys and non evaluated keys if we break from the block" do
+ @hash.transform_keys! do |v|
+ break if v == :c
+ v.succ
+ end
+ @hash.should == { b: 1, c: 2, d: 4 }
+ end
+
+ it "keeps later pair if new keys conflict" do
+ @hash.transform_keys! { |_| :a }.should == { a: 4 }
+ end
+
+ context "when no block is given" do
+ it "returns a sized Enumerator" do
+ enumerator = @hash.transform_keys!
+ enumerator.should.instance_of?(Enumerator)
+ enumerator.size.should == @hash.size
+ enumerator.each(&:upcase).should == { A: 1, B: 2, C: 3, D: 4 }
+ end
+ end
+
+ it "allows a hash argument" do
+ @hash.transform_keys!({ a: :A, b: :B, c: :C, d: :D })
+ @hash.should == { A: 1, B: 2, C: 3, D: 4 }
+ end
+
+ describe "on frozen instance" do
+ before :each do
+ @hash.freeze
+ end
+
+ it "raises a FrozenError on an empty hash" do
+ ->{ {}.freeze.transform_keys!(&:upcase) }.should.raise(FrozenError)
+ end
+
+ it "keeps pairs and raises a FrozenError" do
+ ->{ @hash.transform_keys!(&:upcase) }.should.raise(FrozenError)
+ @hash.should == @initial_pairs
+ end
+
+ it "raises a FrozenError on hash argument" do
+ ->{ @hash.transform_keys!({ a: :A, b: :B, c: :C }) }.should.raise(FrozenError)
+ end
+
+ context "when no block is given" do
+ it "does not raise an exception" do
+ @hash.transform_keys!.should.instance_of?(Enumerator)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/transform_values_spec.rb b/spec/ruby/core/hash/transform_values_spec.rb
new file mode 100644
index 0000000000..b19543a9f4
--- /dev/null
+++ b/spec/ruby/core/hash/transform_values_spec.rb
@@ -0,0 +1,118 @@
+require_relative '../../spec_helper'
+
+describe "Hash#transform_values" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3 }
+ end
+
+ it "returns new hash" do
+ ret = @hash.transform_values(&:succ)
+ ret.should_not.equal?(@hash)
+ ret.should.instance_of?(Hash)
+ end
+
+ it "sets the result as transformed values with the given block" do
+ @hash.transform_values(&:succ).should == { a: 2, b: 3, c: 4 }
+ end
+
+ it "makes both hashes to share keys" do
+ key = [1, 2, 3]
+ new_hash = { key => 1 }.transform_values(&:succ)
+ new_hash[key].should == 2
+ new_hash.keys[0].should.equal?(key)
+ end
+
+ context "when no block is given" do
+ it "returns a sized Enumerator" do
+ enumerator = @hash.transform_values
+ enumerator.should.instance_of?(Enumerator)
+ enumerator.size.should == @hash.size
+ enumerator.each(&:succ).should == { a: 2, b: 3, c: 4 }
+ end
+ end
+
+ it "returns a Hash instance, even on subclasses" do
+ klass = Class.new(Hash)
+ h = klass.new
+ h[:foo] = 42
+ r = h.transform_values{|v| 2 * v}
+ r[:foo].should == 84
+ r.class.should == Hash
+ end
+
+ it "does not retain the default value" do
+ h = Hash.new(1)
+ h.transform_values(&:succ).default.should == nil
+ h[:a] = 1
+ h.transform_values(&:succ).default.should == nil
+ end
+
+ it "does not retain the default_proc" do
+ pr = proc { |h, k| h[k] = [] }
+ h = Hash.new(&pr)
+ h.transform_values(&:succ).default_proc.should == nil
+ h[:a] = 1
+ h.transform_values(&:succ).default_proc.should == nil
+ end
+
+ it "retains compare_by_identity flag" do
+ h = { a: 9, c: 4 }.compare_by_identity
+ h2 = h.transform_values(&:succ)
+ h2.compare_by_identity?.should == true
+ end
+end
+
+describe "Hash#transform_values!" do
+ before :each do
+ @hash = { a: 1, b: 2, c: 3 }
+ @initial_pairs = @hash.dup
+ end
+
+ it "returns self" do
+ @hash.transform_values!(&:succ).should.equal?(@hash)
+ end
+
+ it "updates self as transformed values with the given block" do
+ @hash.transform_values!(&:succ)
+ @hash.should == { a: 2, b: 3, c: 4 }
+ end
+
+ it "partially modifies the contents if we broke from the block" do
+ @hash.transform_values! do |v|
+ break if v == 3
+ 100 + v
+ end
+ @hash.should == { a: 101, b: 102, c: 3}
+ end
+
+ context "when no block is given" do
+ it "returns a sized Enumerator" do
+ enumerator = @hash.transform_values!
+ enumerator.should.instance_of?(Enumerator)
+ enumerator.size.should == @hash.size
+ enumerator.each(&:succ)
+ @hash.should == { a: 2, b: 3, c: 4 }
+ end
+ end
+
+ describe "on frozen instance" do
+ before :each do
+ @hash.freeze
+ end
+
+ it "raises a FrozenError on an empty hash" do
+ ->{ {}.freeze.transform_values!(&:succ) }.should.raise(FrozenError)
+ end
+
+ it "keeps pairs and raises a FrozenError" do
+ ->{ @hash.transform_values!(&:succ) }.should.raise(FrozenError)
+ @hash.should == @initial_pairs
+ end
+
+ context "when no block is given" do
+ it "does not raise an exception" do
+ @hash.transform_values!.should.instance_of?(Enumerator)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/hash/try_convert_spec.rb b/spec/ruby/core/hash/try_convert_spec.rb
new file mode 100644
index 0000000000..c321183356
--- /dev/null
+++ b/spec/ruby/core/hash/try_convert_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash.try_convert" do
+ it "returns the argument if it's a Hash" do
+ x = Hash.new
+ Hash.try_convert(x).should.equal?(x)
+ end
+
+ it "returns the argument if it's a kind of Hash" do
+ x = HashSpecs::MyHash.new
+ Hash.try_convert(x).should.equal?(x)
+ end
+
+ it "returns nil when the argument does not respond to #to_hash" do
+ Hash.try_convert(Object.new).should == nil
+ end
+
+ it "sends #to_hash to the argument and returns the result if it's nil" do
+ obj = mock("to_hash")
+ obj.should_receive(:to_hash).and_return(nil)
+ Hash.try_convert(obj).should == nil
+ end
+
+ it "sends #to_hash to the argument and returns the result if it's a Hash" do
+ x = Hash.new
+ obj = mock("to_hash")
+ obj.should_receive(:to_hash).and_return(x)
+ Hash.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_hash to the argument and returns the result if it's a kind of Hash" do
+ x = HashSpecs::MyHash.new
+ obj = mock("to_hash")
+ obj.should_receive(:to_hash).and_return(x)
+ Hash.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_hash to the argument and raises TypeError if it's not a kind of Hash" do
+ obj = mock("to_hash")
+ obj.should_receive(:to_hash).and_return(Object.new)
+ -> { Hash.try_convert obj }.should raise_consistent_error(TypeError, "can't convert MockObject into Hash (MockObject#to_hash gives Object)")
+ end
+
+ it "does not rescue exceptions raised by #to_hash" do
+ obj = mock("to_hash")
+ obj.should_receive(:to_hash).and_raise(RuntimeError)
+ -> { Hash.try_convert obj }.should.raise(RuntimeError)
+ end
+end
diff --git a/spec/ruby/core/hash/update_spec.rb b/spec/ruby/core/hash/update_spec.rb
new file mode 100644
index 0000000000..f3a3e6b4db
--- /dev/null
+++ b/spec/ruby/core/hash/update_spec.rb
@@ -0,0 +1,79 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#update" do
+ it "adds the entries from other, overwriting duplicate keys. Returns self" do
+ h = { _1: 'a', _2: '3' }
+ h.update(_1: '9', _9: 2).should.equal?(h)
+ h.should == { _1: "9", _2: "3", _9: 2 }
+ end
+
+ it "sets any duplicate key to the value of block if passed a block" do
+ h1 = { a: 2, b: -1 }
+ h2 = { a: -2, c: 1 }
+ h1.update(h2) { |k,x,y| 3.14 }.should.equal?(h1)
+ h1.should == { c: 1, b: -1, a: 3.14 }
+
+ h1.update(h1) { nil }
+ h1.should == { a: nil, b: nil, c: nil }
+ end
+
+ it "tries to convert the passed argument to a hash using #to_hash" do
+ obj = mock('{1=>2}')
+ obj.should_receive(:to_hash).and_return({ 1 => 2 })
+ { 3 => 4 }.update(obj).should == { 1 => 2, 3 => 4 }
+ end
+
+ it "does not call to_hash on hash subclasses" do
+ { 3 => 4 }.update(HashSpecs::ToHashHash[1 => 2]).should == { 1 => 2, 3 => 4 }
+ end
+
+ it "processes entries with same order as merge()" do
+ h = { 1 => 2, 3 => 4, 5 => 6, "x" => nil, nil => 5, [] => [] }
+ merge_bang_pairs = []
+ merge_pairs = []
+ h.merge(h) { |*arg| merge_pairs << arg }
+ h.update(h) { |*arg| merge_bang_pairs << arg }
+ merge_bang_pairs.should == merge_pairs
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> do
+ HashSpecs.frozen_hash.update(1 => 2)
+ end.should.raise(FrozenError)
+ end
+
+ it "checks frozen status before coercing an object with #to_hash" do
+ obj = mock("to_hash frozen")
+ # This is necessary because mock cleanup code cannot run on the frozen
+ # object.
+ def obj.to_hash() raise Exception, "should not receive #to_hash" end
+ obj.freeze
+
+ -> { HashSpecs.frozen_hash.update(obj) }.should.raise(FrozenError)
+ end
+
+ # see redmine #1571
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> do
+ HashSpecs.frozen_hash.update(HashSpecs.empty_frozen_hash)
+ end.should.raise(FrozenError)
+ end
+
+ it "does not raise an exception if changing the value of an existing key during iteration" do
+ hash = {1 => 2, 3 => 4, 5 => 6}
+ hash2 = {1 => :foo, 3 => :bar}
+ hash.each { hash.update(hash2) }
+ hash.should == {1 => :foo, 3 => :bar, 5 => 6}
+ end
+
+ it "accepts multiple hashes" do
+ result = { a: 1 }.update({ b: 2 }, { c: 3 }, { d: 4 })
+ result.should == { a: 1, b: 2, c: 3, d: 4 }
+ end
+
+ it "accepts zero arguments" do
+ hash = { a: 1 }
+ hash.update.should.eql?(hash)
+ end
+end
diff --git a/spec/ruby/core/hash/value_spec.rb b/spec/ruby/core/hash/value_spec.rb
new file mode 100644
index 0000000000..9cfbe576d2
--- /dev/null
+++ b/spec/ruby/core/hash/value_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Hash#value?" do
+ it "is an alias of Hash#has_value?" do
+ Hash.instance_method(:value?).should == Hash.instance_method(:has_value?)
+ end
+end
diff --git a/spec/ruby/core/hash/values_at_spec.rb b/spec/ruby/core/hash/values_at_spec.rb
new file mode 100644
index 0000000000..78dcd8df6a
--- /dev/null
+++ b/spec/ruby/core/hash/values_at_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Hash#values_at" do
+ it "returns an array of values for the given keys" do
+ h = { a: 9, b: 'a', c: -10, d: nil }
+ h.values_at.should.is_a?(Array)
+ h.values_at.should == []
+ h.values_at(:a, :d, :b).should.is_a?(Array)
+ h.values_at(:a, :d, :b).should == [9, nil, 'a']
+ end
+end
diff --git a/spec/ruby/core/hash/values_spec.rb b/spec/ruby/core/hash/values_spec.rb
new file mode 100644
index 0000000000..1fe5f48ad6
--- /dev/null
+++ b/spec/ruby/core/hash/values_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Hash#values" do
+ it "returns an array of values" do
+ h = { 1 => :a, 'a' => :a, 'the' => 'lang' }
+ h.values.should.is_a?(Array)
+ h.values.sort {|a, b| a.to_s <=> b.to_s}.should == [:a, :a, 'lang']
+ end
+end
diff --git a/spec/ruby/core/integer/abs_spec.rb b/spec/ruby/core/integer/abs_spec.rb
new file mode 100644
index 0000000000..c40356db12
--- /dev/null
+++ b/spec/ruby/core/integer/abs_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/abs'
+
+describe "Integer#abs" do
+ it_behaves_like :integer_abs, :abs
+end
diff --git a/spec/ruby/core/integer/allbits_spec.rb b/spec/ruby/core/integer/allbits_spec.rb
new file mode 100644
index 0000000000..6023cc32bf
--- /dev/null
+++ b/spec/ruby/core/integer/allbits_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "Integer#allbits?" do
+ it "returns true if and only if all the bits of the argument are set in the receiver" do
+ 42.allbits?(42).should == true
+ 0b1010_1010.allbits?(0b1000_0010).should == true
+ 0b1010_1010.allbits?(0b1000_0001).should == false
+ 0b1000_0010.allbits?(0b1010_1010).should == false
+ (0b1010_1010 | bignum_value).allbits?(0b1000_0010 | bignum_value).should == true
+ (0b1010_1010 | bignum_value).allbits?(0b1000_0001 | bignum_value).should == false
+ (0b1000_0010 | bignum_value).allbits?(0b1010_1010 | bignum_value).should == false
+ end
+
+ it "handles negative values using two's complement notation" do
+ (~0b1).allbits?(42).should == true
+ (-42).allbits?(-42).should == true
+ (~0b1010_1010).allbits?(~0b1110_1011).should == true
+ (~0b1010_1010).allbits?(~0b1000_0010).should == false
+ (~(0b1010_1010 | bignum_value)).allbits?(~(0b1110_1011 | bignum_value)).should == true
+ (~(0b1010_1010 | bignum_value)).allbits?(~(0b1000_0010 | bignum_value)).should == false
+ end
+
+ it "coerces the rhs using to_int" do
+ obj = mock("the int 0b10")
+ obj.should_receive(:to_int).and_return(0b10)
+ 0b110.allbits?(obj).should == true
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:coerce).any_number_of_times.and_return([42,10])
+ 13.allbits?(obj)
+ }.should.raise(TypeError)
+ -> { 13.allbits?("10") }.should.raise(TypeError)
+ -> { 13.allbits?(:symbol) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/anybits_spec.rb b/spec/ruby/core/integer/anybits_spec.rb
new file mode 100644
index 0000000000..1ea47b76dc
--- /dev/null
+++ b/spec/ruby/core/integer/anybits_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Integer#anybits?" do
+ it "returns true if and only if all the bits of the argument are set in the receiver" do
+ 42.anybits?(42).should == true
+ 0b1010_1010.anybits?(0b1000_0010).should == true
+ 0b1010_1010.anybits?(0b1000_0001).should == true
+ 0b1000_0010.anybits?(0b0010_1100).should == false
+ different_bignum = (2 * bignum_value) & (~bignum_value)
+ (0b1010_1010 | different_bignum).anybits?(0b1000_0010 | bignum_value).should == true
+ (0b1010_1010 | different_bignum).anybits?(0b0010_1100 | bignum_value).should == true
+ (0b1000_0010 | different_bignum).anybits?(0b0010_1100 | bignum_value).should == false
+ end
+
+ it "handles negative values using two's complement notation" do
+ (~42).anybits?(42).should == false
+ (-42).anybits?(-42).should == true
+ (~0b100).anybits?(~0b1).should == true
+ (~(0b100 | bignum_value)).anybits?(~(0b1 | bignum_value)).should == true
+ end
+
+ it "coerces the rhs using to_int" do
+ obj = mock("the int 0b10")
+ obj.should_receive(:to_int).and_return(0b10)
+ 0b110.anybits?(obj).should == true
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:coerce).any_number_of_times.and_return([42,10])
+ 13.anybits?(obj)
+ }.should.raise(TypeError)
+ -> { 13.anybits?("10") }.should.raise(TypeError)
+ -> { 13.anybits?(:symbol) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/bit_and_spec.rb b/spec/ruby/core/integer/bit_and_spec.rb
new file mode 100644
index 0000000000..4098ad7c7b
--- /dev/null
+++ b/spec/ruby/core/integer/bit_and_spec.rb
@@ -0,0 +1,97 @@
+require_relative '../../spec_helper'
+
+describe "Integer#&" do
+ context "fixnum" do
+ it "returns self bitwise AND other" do
+ (256 & 16).should == 0
+ (2010 & 5).should == 0
+ (65535 & 1).should == 1
+ (0xffff & bignum_value + 0xffff_ffff).should == 65535
+ end
+
+ it "returns self bitwise AND other when one operand is negative" do
+ ((1 << 33) & -1).should == (1 << 33)
+ (-1 & (1 << 33)).should == (1 << 33)
+
+ ((-(1<<33)-1) & 5).should == 5
+ (5 & (-(1<<33)-1)).should == 5
+ end
+
+ it "returns self bitwise AND other when both operands are negative" do
+ (-5 & -1).should == -5
+ (-3 & -4).should == -4
+ (-12 & -13).should == -16
+ (-13 & -12).should == -16
+ end
+
+ it "returns self bitwise AND a bignum" do
+ (-1 & 2**64).should == 18446744073709551616
+ end
+
+ it "coerces the rhs and calls #coerce" do
+ obj = mock("fixnum bit and")
+ obj.should_receive(:coerce).with(6).and_return([6, 3])
+ (6 & obj).should == 2
+ end
+
+ it "raises a TypeError when passed a Float" do
+ -> { (3 & 3.4) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError and does not call #to_int when defined on an object" do
+ obj = mock("fixnum bit and")
+ obj.should_not_receive(:to_int)
+
+ -> { 3 & obj }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(5)
+ end
+
+ it "returns self bitwise AND other" do
+ @bignum = bignum_value(5)
+ (@bignum & 3).should == 1
+ (@bignum & 52).should == 4
+ (@bignum & bignum_value(9921)).should == 18446744073709551617
+
+ ((2*bignum_value) & 1).should == 0
+ ((2*bignum_value) & (2*bignum_value)).should == 36893488147419103232
+ end
+
+ it "returns self bitwise AND other when one operand is negative" do
+ ((2*bignum_value) & -1).should == (2*bignum_value)
+ ((4*bignum_value) & -1).should == (4*bignum_value)
+ (@bignum & -0xffffffffffffff5).should == 18446744073709551617
+ (@bignum & -@bignum).should == 1
+ (@bignum & -0x8000000000000000).should == 18446744073709551616
+ end
+
+ it "returns self bitwise AND other when both operands are negative" do
+ (-@bignum & -0x4000000000000005).should == -23058430092136939525
+ (-@bignum & -@bignum).should == -18446744073709551621
+ (-@bignum & -0x4000000000000000).should == -23058430092136939520
+ end
+
+ it "returns self bitwise AND other when both are negative and a multiple in bitsize of Fixnum::MIN" do
+ val = - ((1 << 93) - 1)
+ (val & val).should == val
+
+ val = - ((1 << 126) - 1)
+ (val & val).should == val
+ end
+
+ it "raises a TypeError when passed a Float" do
+ -> { (@bignum & 3.4) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError and does not call #to_int when defined on an object" do
+ obj = mock("bignum bit and")
+ obj.should_not_receive(:to_int)
+
+ -> { @bignum & obj }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/bit_length_spec.rb b/spec/ruby/core/integer/bit_length_spec.rb
new file mode 100644
index 0000000000..827007b7e0
--- /dev/null
+++ b/spec/ruby/core/integer/bit_length_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+
+describe "Integer#bit_length" do
+ context "fixnum" do
+ it "returns the position of the leftmost bit of a positive number" do
+ 0.bit_length.should == 0
+ 1.bit_length.should == 1
+ 2.bit_length.should == 2
+ 3.bit_length.should == 2
+ 4.bit_length.should == 3
+ n = fixnum_max.bit_length
+ fixnum_max[n].should == 0
+ fixnum_max[n - 1].should == 1
+
+ 0.bit_length.should == 0
+ 1.bit_length.should == 1
+ 0xff.bit_length.should == 8
+ 0x100.bit_length.should == 9
+ (2**12 - 1).bit_length.should == 12
+ (2**12).bit_length.should == 13
+ (2**12 + 1).bit_length.should == 13
+ end
+
+ it "returns the position of the leftmost 0 bit of a negative number" do
+ -1.bit_length.should == 0
+ -2.bit_length.should == 1
+ -3.bit_length.should == 2
+ -4.bit_length.should == 2
+ -5.bit_length.should == 3
+ n = fixnum_min.bit_length
+ fixnum_min[n].should == 1
+ fixnum_min[n - 1].should == 0
+
+ (-2**12 - 1).bit_length.should == 13
+ (-2**12).bit_length.should == 12
+ (-2**12 + 1).bit_length.should == 12
+ -0x101.bit_length.should == 9
+ -0x100.bit_length.should == 8
+ -0xff.bit_length.should == 8
+ -2.bit_length.should == 1
+ -1.bit_length.should == 0
+ end
+ end
+
+ context "bignum" do
+ it "returns the position of the leftmost bit of a positive number" do
+ (2**1000-1).bit_length.should == 1000
+ (2**1000).bit_length.should == 1001
+ (2**1000+1).bit_length.should == 1001
+
+ (2**10000-1).bit_length.should == 10000
+ (2**10000).bit_length.should == 10001
+ (2**10000+1).bit_length.should == 10001
+
+ (1 << 100).bit_length.should == 101
+ (1 << 100).succ.bit_length.should == 101
+ (1 << 100).pred.bit_length.should == 100
+ (1 << 10000).bit_length.should == 10001
+ end
+
+ it "returns the position of the leftmost 0 bit of a negative number" do
+ (-2**10000-1).bit_length.should == 10001
+ (-2**10000).bit_length.should == 10000
+ (-2**10000+1).bit_length.should == 10000
+
+ (-2**1000-1).bit_length.should == 1001
+ (-2**1000).bit_length.should == 1000
+ (-2**1000+1).bit_length.should == 1000
+
+ ((-1 << 100)-1).bit_length.should == 101
+ ((-1 << 100)-1).succ.bit_length.should == 100
+ ((-1 << 100)-1).pred.bit_length.should == 101
+ ((-1 << 10000)-1).bit_length.should == 10001
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/bit_or_spec.rb b/spec/ruby/core/integer/bit_or_spec.rb
new file mode 100644
index 0000000000..c380cd729e
--- /dev/null
+++ b/spec/ruby/core/integer/bit_or_spec.rb
@@ -0,0 +1,89 @@
+require_relative '../../spec_helper'
+
+describe "Integer#|" do
+ context "fixnum" do
+ it "returns self bitwise OR other" do
+ (1 | 0).should == 1
+ (5 | 4).should == 5
+ (5 | 6).should == 7
+ (248 | 4096).should == 4344
+ (0xffff | bignum_value + 0xf0f0).should == 0x1_0000_0000_0000_ffff
+ end
+
+ it "returns self bitwise OR other when one operand is negative" do
+ ((1 << 33) | -1).should == -1
+ (-1 | (1 << 33)).should == -1
+
+ ((-(1<<33)-1) | 5).should == -8589934593
+ (5 | (-(1<<33)-1)).should == -8589934593
+ end
+
+ it "returns self bitwise OR other when both operands are negative" do
+ (-5 | -1).should == -1
+ (-3 | -4).should == -3
+ (-12 | -13).should == -9
+ (-13 | -12).should == -9
+ end
+
+ it "returns self bitwise OR a bignum" do
+ (-1 | 2**64).should == -1
+ end
+
+ it "coerces the rhs and calls #coerce" do
+ obj = mock("fixnum bit or")
+ obj.should_receive(:coerce).with(6).and_return([6, 3])
+ (6 | obj).should == 7
+ end
+
+ it "raises a TypeError when passed a Float" do
+ -> { (3 | 3.4) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError and does not call #to_int when defined on an object" do
+ obj = mock("integer bit or")
+ obj.should_not_receive(:to_int)
+
+ -> { 3 | obj }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(11)
+ end
+
+ it "returns self bitwise OR other" do
+ (@bignum | 2).should == 18446744073709551627
+ (@bignum | 9).should == 18446744073709551627
+ (@bignum | bignum_value).should == 18446744073709551627
+ end
+
+ it "returns self bitwise OR other when one operand is negative" do
+ (@bignum | -0x40000000000000000).should == -55340232221128654837
+ (@bignum | -@bignum).should == -1
+ (@bignum | -0x8000000000000000).should == -9223372036854775797
+ end
+
+ it "returns self bitwise OR other when both operands are negative" do
+ (-@bignum | -0x4000000000000005).should == -1
+ (-@bignum | -@bignum).should == -18446744073709551627
+ (-@bignum | -0x4000000000000000).should == -11
+ end
+
+ it "raises a TypeError when passed a Float" do
+ not_supported_on :opal do
+ -> {
+ bignum_value | bignum_value(0xffff).to_f
+ }.should.raise(TypeError)
+ end
+ -> { @bignum | 9.9 }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError and does not call #to_int when defined on an object" do
+ obj = mock("bignum bit or")
+ obj.should_not_receive(:to_int)
+
+ -> { @bignum | obj }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/bit_xor_spec.rb b/spec/ruby/core/integer/bit_xor_spec.rb
new file mode 100644
index 0000000000..c0cf8405f7
--- /dev/null
+++ b/spec/ruby/core/integer/bit_xor_spec.rb
@@ -0,0 +1,93 @@
+require_relative '../../spec_helper'
+
+describe "Integer#^" do
+ context "fixnum" do
+ it "returns self bitwise EXCLUSIVE OR other" do
+ (3 ^ 5).should == 6
+ (-2 ^ -255).should == 255
+ (5 ^ bignum_value + 0xffff_ffff).should == 0x1_0000_0000_ffff_fffa
+ end
+
+ it "returns self bitwise XOR other when one operand is negative" do
+ ((1 << 33) ^ -1).should == -8589934593
+ (-1 ^ (1 << 33)).should == -8589934593
+
+ ((-(1<<33)-1) ^ 5).should == -8589934598
+ (5 ^ (-(1<<33)-1)).should == -8589934598
+ end
+
+ it "returns self bitwise XOR other when both operands are negative" do
+ (-5 ^ -1).should == 4
+ (-3 ^ -4).should == 1
+ (-12 ^ -13).should == 7
+ (-13 ^ -12).should == 7
+ end
+
+ it "returns self bitwise EXCLUSIVE OR a bignum" do
+ (-1 ^ 2**64).should == -18446744073709551617
+ end
+
+ it "coerces the rhs and calls #coerce" do
+ obj = mock("fixnum bit xor")
+ obj.should_receive(:coerce).with(6).and_return([6, 3])
+ (6 ^ obj).should == 5
+ end
+
+ it "raises a TypeError when passed a Float" do
+ -> { (3 ^ 3.4) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError and does not call #to_int when defined on an object" do
+ obj = mock("integer bit xor")
+ obj.should_not_receive(:to_int)
+
+ -> { 3 ^ obj }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(18)
+ end
+
+ it "returns self bitwise EXCLUSIVE OR other" do
+ (@bignum ^ 2).should == 18446744073709551632
+ (@bignum ^ @bignum).should == 0
+ (@bignum ^ 14).should == 18446744073709551644
+ end
+
+ it "returns self bitwise EXCLUSIVE OR other when one operand is negative" do
+ (@bignum ^ -0x40000000000000000).should == -55340232221128654830
+ (@bignum ^ -@bignum).should == -4
+ (@bignum ^ -0x8000000000000000).should == -27670116110564327406
+ end
+
+ it "returns self bitwise EXCLUSIVE OR other when both operands are negative" do
+ (-@bignum ^ -0x40000000000000000).should == 55340232221128654830
+ (-@bignum ^ -@bignum).should == 0
+ (-@bignum ^ -0x4000000000000000).should == 23058430092136939502
+ end
+
+ it "returns self bitwise EXCLUSIVE OR other when all bits are 1 and other value is negative" do
+ (9903520314283042199192993791 ^ -1).should == -9903520314283042199192993792
+ (784637716923335095479473677900958302012794430558004314111 ^ -1).should ==
+ -784637716923335095479473677900958302012794430558004314112
+ end
+
+ it "raises a TypeError when passed a Float" do
+ not_supported_on :opal do
+ -> {
+ bignum_value ^ bignum_value(0xffff).to_f
+ }.should.raise(TypeError)
+ end
+ -> { @bignum ^ 14.5 }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError and does not call #to_int when defined on an object" do
+ obj = mock("bignum bit xor")
+ obj.should_not_receive(:to_int)
+
+ -> { @bignum ^ obj }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/case_compare_spec.rb b/spec/ruby/core/integer/case_compare_spec.rb
new file mode 100644
index 0000000000..e5dde2c64a
--- /dev/null
+++ b/spec/ruby/core/integer/case_compare_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal'
+
+describe "Integer#===" do
+ it_behaves_like :integer_equal, :===
+end
diff --git a/spec/ruby/core/integer/ceil_spec.rb b/spec/ruby/core/integer/ceil_spec.rb
new file mode 100644
index 0000000000..395be58fbd
--- /dev/null
+++ b/spec/ruby/core/integer/ceil_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+require_relative 'shared/integer_rounding'
+require_relative 'shared/integer_ceil_precision'
+
+describe "Integer#ceil" do
+ it_behaves_like :integer_to_i, :ceil
+ it_behaves_like :integer_rounding_positive_precision, :ceil
+
+ context "with precision" do
+ it_behaves_like :integer_ceil_precision, :Integer
+ end
+end
diff --git a/spec/ruby/core/integer/ceildiv_spec.rb b/spec/ruby/core/integer/ceildiv_spec.rb
new file mode 100644
index 0000000000..91f63832f8
--- /dev/null
+++ b/spec/ruby/core/integer/ceildiv_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "Integer#ceildiv" do
+ it "returns a quotient of division which is rounded up to the nearest integer" do
+ 0.ceildiv(3).should.eql?(0)
+ 1.ceildiv(3).should.eql?(1)
+ 3.ceildiv(3).should.eql?(1)
+ 4.ceildiv(3).should.eql?(2)
+
+ 4.ceildiv(-3).should.eql?(-1)
+ -4.ceildiv(3).should.eql?(-1)
+ -4.ceildiv(-3).should.eql?(2)
+
+ 3.ceildiv(1.2).should.eql?(3)
+ 3.ceildiv(6/5r).should.eql?(3)
+
+ (10**100-11).ceildiv(10**99-1).should.eql?(10)
+ (10**100-9).ceildiv(10**99-1).should.eql?(11)
+ end
+end
diff --git a/spec/ruby/core/integer/chr_spec.rb b/spec/ruby/core/integer/chr_spec.rb
new file mode 100644
index 0000000000..72784e1833
--- /dev/null
+++ b/spec/ruby/core/integer/chr_spec.rb
@@ -0,0 +1,257 @@
+require_relative '../../spec_helper'
+
+describe "Integer#chr without argument" do
+ it "returns a String" do
+ 17.chr.should.instance_of?(String)
+ end
+
+ it "returns a new String for each call" do
+ 82.chr.should_not.equal?(82.chr)
+ end
+
+ it "raises a RangeError is self is less than 0" do
+ -> { -1.chr }.should.raise(RangeError, /-1 out of char range/)
+ -> { (-bignum_value).chr }.should.raise(RangeError, /bignum out of char range/)
+ end
+
+ it "raises a RangeError if self is too large" do
+ -> { 2206368128.chr(Encoding::UTF_8) }.should.raise(RangeError, /2206368128 out of char range/)
+ end
+
+ describe "when Encoding.default_internal is nil" do
+ describe "and self is between 0 and 127 (inclusive)" do
+ it "returns a US-ASCII String" do
+ (0..127).each do |c|
+ c.chr.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+ it "returns a String encoding self interpreted as a US-ASCII codepoint" do
+ (0..127).each do |c|
+ c.chr.bytes.to_a.should == [c]
+ end
+ end
+ end
+
+ describe "and self is between 128 and 255 (inclusive)" do
+ it "returns a binary String" do
+ (128..255).each do |c|
+ c.chr.encoding.should == Encoding::BINARY
+ end
+ end
+
+ it "returns a String containing self interpreted as a byte" do
+ (128..255).each do |c|
+ c.chr.bytes.to_a.should == [c]
+ end
+ end
+ end
+
+ it "raises a RangeError is self is greater than 255" do
+ -> { 256.chr }.should.raise(RangeError, /256 out of char range/)
+ -> { bignum_value.chr }.should.raise(RangeError, /bignum out of char range/)
+ end
+ end
+
+ describe "when Encoding.default_internal is not nil" do
+ before do
+ @default_internal = Encoding.default_internal
+ end
+
+ after do
+ Encoding.default_internal = @default_internal
+ end
+
+ describe "and self is between 0 and 127 (inclusive)" do
+ it "returns a US-ASCII String" do
+ (0..127).each do |c|
+ Encoding.default_internal = Encoding::UTF_8
+ c.chr.encoding.should == Encoding::US_ASCII
+
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ c.chr.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+ it "returns a String encoding self interpreted as a US-ASCII codepoint" do
+ (0..127).each do |c|
+ Encoding.default_internal = Encoding::UTF_8
+ c.chr.bytes.to_a.should == [c]
+
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ c.chr.bytes.to_a.should == [c]
+ end
+ end
+ end
+
+ describe "and self is between 128 and 255 (inclusive)" do
+ it "returns a binary String" do
+ (128..255).each do |c|
+ Encoding.default_internal = Encoding::UTF_8
+ c.chr.encoding.should == Encoding::BINARY
+
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ c.chr.encoding.should == Encoding::BINARY
+ end
+ end
+
+ it "returns a String containing self interpreted as a byte" do
+ (128..255).each do |c|
+ Encoding.default_internal = Encoding::UTF_8
+ c.chr.bytes.to_a.should == [c]
+
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ c.chr.bytes.to_a.should == [c]
+ end
+ end
+ end
+
+ describe "and self is greater than 255" do
+ it "returns a String with the default internal encoding" do
+ Encoding.default_internal = Encoding::UTF_8
+ 0x0100.chr.encoding.should == Encoding::UTF_8
+ 0x3000.chr.encoding.should == Encoding::UTF_8
+
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ 0x8140.chr.encoding.should == Encoding::SHIFT_JIS
+ 0xFC4B.chr.encoding.should == Encoding::SHIFT_JIS
+ end
+
+ it "returns a String encoding self interpreted as a codepoint in the default internal encoding" do
+ Encoding.default_internal = Encoding::UTF_8
+ 0x0100.chr.bytes.to_a.should == [0xC4, 0x80]
+ 0x3000.chr.bytes.to_a.should == [0xE3, 0x80, 0x80]
+
+ Encoding.default_internal = Encoding::SHIFT_JIS
+ 0x8140.chr.bytes.to_a.should == [0x81, 0x40] # Smallest assigned CP932 codepoint greater than 255
+ 0xFC4B.chr.bytes.to_a.should == [0xFC, 0x4B] # Largest assigned CP932 codepoint
+ end
+
+ # #5864
+ it "raises RangeError if self is invalid as a codepoint in the default internal encoding" do
+ [ [0x0100, "US-ASCII"],
+ [0x0100, "BINARY"],
+ [0x0100, "EUC-JP"],
+ [0xA1A0, "EUC-JP"],
+ [0x0100, "ISO-8859-9"],
+ [620, "TIS-620"]
+ ].each do |integer, encoding_name|
+ Encoding.default_internal = Encoding.find(encoding_name)
+ -> { integer.chr }.should.raise(RangeError, /(invalid codepoint|out of char range)/)
+ end
+ end
+ end
+ end
+end
+
+describe "Integer#chr with an encoding argument" do
+ it "returns a String" do
+ 900.chr(Encoding::UTF_8).should.instance_of?(String)
+ end
+
+ it "returns a new String for each call" do
+ 8287.chr(Encoding::UTF_8).should_not.equal?(8287.chr(Encoding::UTF_8))
+ end
+
+ it "accepts a String as an argument" do
+ -> { 0xA4A2.chr('euc-jp') }.should_not.raise
+ end
+
+ it "converts a String to an Encoding as Encoding.find does" do
+ ['utf-8', 'UTF-8', 'Utf-8'].each do |encoding|
+ 7894.chr(encoding).encoding.should == Encoding::UTF_8
+ end
+ end
+
+ # http://redmine.ruby-lang.org/issues/4869
+ it "raises a RangeError is self is less than 0" do
+ -> { -1.chr(Encoding::UTF_8) }.should.raise(RangeError, /-1 out of char range/)
+ -> { (-bignum_value).chr(Encoding::EUC_JP) }.should.raise(RangeError, /bignum out of char range/)
+ end
+
+ it "raises a RangeError if self is too large" do
+ -> { 2206368128.chr(Encoding::UTF_8) }.should.raise(RangeError, /2206368128 out of char range/)
+ end
+
+ it "returns a String with the specified encoding" do
+ 0x0000.chr(Encoding::US_ASCII).encoding.should == Encoding::US_ASCII
+ 0x007F.chr(Encoding::US_ASCII).encoding.should == Encoding::US_ASCII
+
+ 0x0000.chr(Encoding::BINARY).encoding.should == Encoding::BINARY
+ 0x007F.chr(Encoding::BINARY).encoding.should == Encoding::BINARY
+ 0x0080.chr(Encoding::BINARY).encoding.should == Encoding::BINARY
+ 0x00FF.chr(Encoding::BINARY).encoding.should == Encoding::BINARY
+
+ 0x0000.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+ 0x007F.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+ 0x0080.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+ 0x00FF.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+ 0x0100.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+ 0x3000.chr(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+
+ 0x0000.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ 0x007F.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ 0x00A1.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ 0x00DF.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ 0x8140.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ 0xFC4B.chr(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ end
+
+ it "returns a String encoding self interpreted as a codepoint in the specified encoding" do
+ 0x0000.chr(Encoding::US_ASCII).bytes.to_a.should == [0x00]
+ 0x007F.chr(Encoding::US_ASCII).bytes.to_a.should == [0x7F]
+
+ 0x0000.chr(Encoding::BINARY).bytes.to_a.should == [0x00]
+ 0x007F.chr(Encoding::BINARY).bytes.to_a.should == [0x7F]
+ 0x0080.chr(Encoding::BINARY).bytes.to_a.should == [0x80]
+ 0x00FF.chr(Encoding::BINARY).bytes.to_a.should == [0xFF]
+
+ 0x0000.chr(Encoding::UTF_8).bytes.to_a.should == [0x00]
+ 0x007F.chr(Encoding::UTF_8).bytes.to_a.should == [0x7F]
+ 0x0080.chr(Encoding::UTF_8).bytes.to_a.should == [0xC2, 0x80]
+ 0x00FF.chr(Encoding::UTF_8).bytes.to_a.should == [0xC3, 0xBF]
+ 0x0100.chr(Encoding::UTF_8).bytes.to_a.should == [0xC4, 0x80]
+ 0x3000.chr(Encoding::UTF_8).bytes.to_a.should == [0xE3, 0x80, 0x80]
+
+ 0x0000.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0x00]
+ 0x007F.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0x7F]
+ 0x00A1.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0xA1]
+ 0x00DF.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0xDF]
+ 0x8140.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0x81, 0x40] # Smallest assigned CP932 codepoint greater than 255
+ 0xFC4B.chr(Encoding::SHIFT_JIS).bytes.to_a.should == [0xFC, 0x4B] # Largest assigned CP932 codepoint
+ end
+
+ # #5864
+ it "raises RangeError if self is invalid as a codepoint in the specified encoding" do
+ -> { 0x80.chr("US-ASCII") }.should.raise(RangeError)
+ -> { 0x0100.chr("BINARY") }.should.raise(RangeError)
+ -> { 0x0100.chr("EUC-JP") }.should.raise(RangeError)
+ -> { 0xA1A0.chr("EUC-JP") }.should.raise(RangeError)
+ -> { 0xA1.chr("EUC-JP") }.should.raise(RangeError)
+ -> { 0x80.chr("SHIFT_JIS") }.should.raise(RangeError)
+ -> { 0xE0.chr("SHIFT_JIS") }.should.raise(RangeError)
+ -> { 0x0100.chr("ISO-8859-9") }.should.raise(RangeError)
+ -> { 620.chr("TIS-620") }.should.raise(RangeError)
+ # UTF-16 surrogate range
+ -> { 0xD800.chr("UTF-8") }.should.raise(RangeError)
+ -> { 0xDBFF.chr("UTF-8") }.should.raise(RangeError)
+ -> { 0xDC00.chr("UTF-8") }.should.raise(RangeError)
+ -> { 0xDFFF.chr("UTF-8") }.should.raise(RangeError)
+ # UTF-16 surrogate range
+ -> { 0xD800.chr("UTF-16") }.should.raise(RangeError)
+ -> { 0xDBFF.chr("UTF-16") }.should.raise(RangeError)
+ -> { 0xDC00.chr("UTF-16") }.should.raise(RangeError)
+ -> { 0xDFFF.chr("UTF-16") }.should.raise(RangeError)
+ end
+
+ it 'returns a String encoding self interpreted as a codepoint in the CESU-8 encoding' do
+ # see more details here https://en.wikipedia.org/wiki/CESU-8
+ # code points from U+0000 to U+FFFF is encoded in the same way as in UTF-8
+ 0x0045.chr(Encoding::CESU_8).bytes.should == 0x0045.chr(Encoding::UTF_8).bytes
+
+ # code points in range from U+10000 to U+10FFFF is CESU-8 data containing a 6-byte surrogate pair,
+ # which decodes to a 4-byte UTF-8 string
+ 0x10400.chr(Encoding::CESU_8).bytes.should != 0x10400.chr(Encoding::UTF_8).bytes
+ 0x10400.chr(Encoding::CESU_8).bytes.to_a.should == [0xED, 0xA0, 0x81, 0xED, 0xB0, 0x80]
+ end
+end
diff --git a/spec/ruby/core/integer/coerce_spec.rb b/spec/ruby/core/integer/coerce_spec.rb
new file mode 100644
index 0000000000..c0e642da03
--- /dev/null
+++ b/spec/ruby/core/integer/coerce_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+
+describe "Integer#coerce" do
+ context "fixnum" do
+ describe "when given a Fixnum" do
+ it "returns an array containing two Fixnums" do
+ 1.coerce(2).should == [2, 1]
+ 1.coerce(2).map { |i| i.class }.should == [Integer, Integer]
+ end
+ end
+
+ describe "when given a String" do
+ it "raises an ArgumentError when trying to coerce with a non-number String" do
+ -> { 1.coerce(":)") }.should.raise(ArgumentError)
+ end
+
+ it "returns an array containing two Floats" do
+ 1.coerce("2").should == [2.0, 1.0]
+ 1.coerce("-2").should == [-2.0, 1.0]
+ end
+ end
+
+ it "raises a TypeError when trying to coerce with nil" do
+ -> { 1.coerce(nil) }.should.raise(TypeError)
+ end
+
+ it "tries to convert the given Object into a Float by using #to_f" do
+ (obj = mock('1.0')).should_receive(:to_f).and_return(1.0)
+ 2.coerce(obj).should == [1.0, 2.0]
+
+ (obj = mock('0')).should_receive(:to_f).and_return('0')
+ -> { 2.coerce(obj).should == [1.0, 2.0] }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Object that does not respond to #to_f" do
+ -> { 1.coerce(mock('x')) }.should.raise(TypeError)
+ -> { 1.coerce(1..4) }.should.raise(TypeError)
+ -> { 1.coerce(:test) }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ it "coerces other to a Bignum and returns [other, self] when passed a Fixnum" do
+ a = bignum_value
+ ary = a.coerce(2)
+
+ ary[0].should.is_a?(Integer)
+ ary[1].should.is_a?(Integer)
+ ary.should == [2, a]
+ end
+
+ it "returns [other, self] when passed a Bignum" do
+ a = bignum_value
+ b = bignum_value
+ ary = a.coerce(b)
+
+ ary[0].should.is_a?(Integer)
+ ary[1].should.is_a?(Integer)
+ ary.should == [b, a]
+ end
+
+ it "raises a TypeError when not passed a Fixnum or Bignum" do
+ a = bignum_value
+
+ -> { a.coerce(nil) }.should.raise(TypeError)
+ -> { a.coerce(mock('str')) }.should.raise(TypeError)
+ -> { a.coerce(1..4) }.should.raise(TypeError)
+ -> { a.coerce(:test) }.should.raise(TypeError)
+ end
+
+ it "coerces both values to Floats and returns [other, self] when passed a Float" do
+ a = bignum_value
+ a.coerce(1.2).should == [1.2, a.to_f]
+ end
+
+ it "coerces both values to Floats and returns [other, self] when passed a String" do
+ a = bignum_value
+ a.coerce("123").should == [123.0, a.to_f]
+ end
+
+ it "calls #to_f to coerce other to a Float" do
+ b = mock("bignum value")
+ b.should_receive(:to_f).and_return(1.2)
+
+ a = bignum_value
+ ary = a.coerce(b)
+
+ ary.should == [1.2, a.to_f]
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/comparison_spec.rb b/spec/ruby/core/integer/comparison_spec.rb
new file mode 100644
index 0000000000..cc5cbe2506
--- /dev/null
+++ b/spec/ruby/core/integer/comparison_spec.rb
@@ -0,0 +1,185 @@
+require_relative '../../spec_helper'
+
+describe "Integer#<=>" do
+ context "fixnum" do
+ it "returns -1 when self is less than the given argument" do
+ (-3 <=> -1).should == -1
+ (-5 <=> 10).should == -1
+ (-5 <=> -4.5).should == -1
+ end
+
+ it "returns 0 when self is equal to the given argument" do
+ (0 <=> 0).should == 0
+ (954 <=> 954).should == 0
+ (954 <=> 954.0).should == 0
+ end
+
+ it "returns 1 when self is greater than the given argument" do
+ (496 <=> 5).should == 1
+ (200 <=> 100).should == 1
+ (51 <=> 50.5).should == 1
+ end
+
+ it "returns nil when the given argument is not an Integer" do
+ (3 <=> mock('x')).should == nil
+ (3 <=> 'test').should == nil
+ end
+ end
+
+ context "bignum" do
+ describe "with a Fixnum" do
+ it "returns -1 when other is larger" do
+ (-bignum_value <=> 2).should == -1
+ end
+
+ it "returns 1 when other is smaller" do
+ (bignum_value <=> 2).should == 1
+ end
+ end
+
+ describe "with a Bignum" do
+ describe "when other is negative" do
+ it "returns -1 when self is negative and other is larger" do
+ (-bignum_value(42) <=> -bignum_value).should == -1
+ end
+
+ it "returns 0 when other is equal" do
+ (-bignum_value <=> -bignum_value).should == 0
+ end
+
+ it "returns 1 when self is negative and other is smaller" do
+ (-bignum_value <=> -bignum_value(94)).should == 1
+ end
+
+ it "returns 1 when self is positive" do
+ (bignum_value <=> -bignum_value).should == 1
+ end
+ end
+
+ describe "when other is positive" do
+ it "returns -1 when self is negative" do
+ (-bignum_value <=> bignum_value).should == -1
+ end
+
+ it "returns -1 when self is positive and other is larger" do
+ (bignum_value <=> bignum_value(38)).should == -1
+ end
+
+ it "returns 0 when other is equal" do
+ (bignum_value <=> bignum_value).should == 0
+ end
+
+ it "returns 1 when other is smaller" do
+ (bignum_value(56) <=> bignum_value).should == 1
+ end
+ end
+ end
+
+ describe "with a Float" do
+ describe "when other is negative" do
+ it "returns -1 when self is negative and other is larger" do
+ (-bignum_value(0xffff) <=> -bignum_value.to_f).should == -1
+ end
+
+ it "returns 0 when other is equal" do
+ (-bignum_value <=> -bignum_value.to_f).should == 0
+ end
+
+ it "returns 1 when self is negative and other is smaller" do
+ (-bignum_value <=> -bignum_value(0xffef).to_f).should == 1
+ end
+
+ it "returns 1 when self is positive" do
+ (bignum_value <=> -bignum_value.to_f).should == 1
+ end
+ end
+
+ describe "when other is positive" do
+ it "returns -1 when self is negative" do
+ (-bignum_value <=> bignum_value.to_f).should == -1
+ end
+
+ it "returns -1 when self is positive and other is larger" do
+ (bignum_value <=> bignum_value(0xfffe).to_f).should == -1
+ end
+
+ it "returns 0 when other is equal" do
+ (bignum_value <=> bignum_value.to_f).should == 0
+ end
+
+ it "returns 1 when other is smaller" do
+ (bignum_value(0xfeff) <=> bignum_value.to_f).should == 1
+ end
+ end
+ end
+
+ describe "with an Object" do
+ before :each do
+ @big = bignum_value
+ @num = mock("value for Integer#<=>")
+ end
+
+ it "calls #coerce on other" do
+ @num.should_receive(:coerce).with(@big).and_return([@big.to_f, 2.5])
+ @big <=> @num
+ end
+
+ it "lets the exception go through if #coerce raises an exception" do
+ @num.should_receive(:coerce).with(@big).and_raise(RuntimeError.new("my error"))
+ -> {
+ @big <=> @num
+ }.should.raise(RuntimeError, "my error")
+ end
+
+ it "raises an exception if #coerce raises a non-StandardError exception" do
+ @num.should_receive(:coerce).with(@big).and_raise(Exception)
+ -> { @big <=> @num }.should.raise(Exception)
+ end
+
+ it "returns nil if #coerce does not return an Array" do
+ @num.should_receive(:coerce).with(@big).and_return(nil)
+ (@big <=> @num).should == nil
+ end
+
+ it "returns -1 if the coerced value is larger" do
+ @num.should_receive(:coerce).with(@big).and_return([@big, bignum_value(10)])
+ (@big <=> @num).should == -1
+ end
+
+ it "returns 0 if the coerced value is equal" do
+ @num.should_receive(:coerce).with(@big).and_return([@big, bignum_value])
+ (@big <=> @num).should == 0
+ end
+
+ it "returns 1 if the coerced value is smaller" do
+ @num.should_receive(:coerce).with(@big).and_return([@big, 22])
+ (@big <=> @num).should == 1
+ end
+ end
+
+ describe "with a Float" do
+ it "does not lose precision for values that don't fit in a double" do
+ (bignum_value(1) <=> bignum_value.to_f).should == 1
+ (bignum_value <=> bignum_value.to_f).should == 0
+ ((bignum_value - 1) <=> bignum_value.to_f).should == -1
+ end
+ end
+
+ # The tests below are taken from matz's revision 23730 for Ruby trunk
+ it "returns 1 when self is Infinity and other is a Bignum" do
+ (infinity_value <=> Float::MAX.to_i*2).should == 1
+ end
+
+ it "returns -1 when self is negative and other is Infinity" do
+ (-Float::MAX.to_i*2 <=> infinity_value).should == -1
+ end
+
+ it "returns 1 when self is negative and other is -Infinity" do
+ (-Float::MAX.to_i*2 <=> -infinity_value).should == 1
+ end
+
+ it "returns -1 when self is -Infinity and other is negative" do
+ (-infinity_value <=> -Float::MAX.to_i*2).should == -1
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/complement_spec.rb b/spec/ruby/core/integer/complement_spec.rb
new file mode 100644
index 0000000000..baf5e6c457
--- /dev/null
+++ b/spec/ruby/core/integer/complement_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "Integer#~" do
+ context "fixnum" do
+ it "returns self with each bit flipped" do
+ (~0).should == -1
+ (~1221).should == -1222
+ (~-2).should == 1
+ (~-599).should == 598
+ end
+ end
+
+ context "bignum" do
+ it "returns self with each bit flipped" do
+ (~bignum_value(48)).should == -18446744073709551665
+ (~(-bignum_value(21))).should == 18446744073709551636
+ (~bignum_value(1)).should == -18446744073709551618
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/constants_spec.rb b/spec/ruby/core/integer/constants_spec.rb
new file mode 100644
index 0000000000..937806c72f
--- /dev/null
+++ b/spec/ruby/core/integer/constants_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Fixnum" do
+ it "is no longer defined" do
+ Object.should_not.const_defined?(:Fixnum)
+ end
+end
+
+describe "Bignum" do
+ it "is no longer defined" do
+ Object.should_not.const_defined?(:Bignum)
+ end
+end
diff --git a/spec/ruby/core/integer/denominator_spec.rb b/spec/ruby/core/integer/denominator_spec.rb
new file mode 100644
index 0000000000..c1477d0757
--- /dev/null
+++ b/spec/ruby/core/integer/denominator_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "Integer#denominator" do
+ # The Numeric child classes override this method, so their behaviour is
+ # specified in the appropriate place
+ before :each do
+ @numbers = [
+ 20, # Integer
+ -2709, # Negative Integer
+ 99999999**99, # Bignum
+ -99999**621, # Negative BigNum
+ 0,
+ 1
+ ]
+ end
+
+ it "returns 1" do
+ @numbers.each {|number| number.denominator.should == 1}
+ end
+end
diff --git a/spec/ruby/core/integer/digits_spec.rb b/spec/ruby/core/integer/digits_spec.rb
new file mode 100644
index 0000000000..c4ebf2cd88
--- /dev/null
+++ b/spec/ruby/core/integer/digits_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Integer#digits" do
+ it "returns an array of place values in base-10 by default" do
+ 12345.digits.should == [5,4,3,2,1]
+ end
+
+ it "returns digits by place value of a given radix" do
+ 12345.digits(7).should == [4,6,6,0,5]
+ end
+
+ it "converts the radix with #to_int" do
+ 12345.digits(mock_int(2)).should == [1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1]
+ end
+
+ it "returns [0] when called on 0, regardless of base" do
+ 0.digits.should == [0]
+ 0.digits(7).should == [0]
+ end
+
+ it "raises ArgumentError when calling with a radix less than 2" do
+ -> { 12345.digits(1) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when calling with a negative radix" do
+ -> { 12345.digits(-2) }.should.raise(ArgumentError)
+ end
+
+ it "raises Math::DomainError when calling digits on a negative number" do
+ -> { -12345.digits(7) }.should.raise(Math::DomainError)
+ end
+
+ it "returns integer values > 9 when base is above 10" do
+ 1234.digits(16).should == [2, 13, 4]
+ end
+
+ it "can be used with base > 37" do
+ 1234.digits(100).should == [34, 12]
+ 980099.digits(100).should == [99, 0, 98]
+ end
+end
diff --git a/spec/ruby/core/integer/div_spec.rb b/spec/ruby/core/integer/div_spec.rb
new file mode 100644
index 0000000000..220ca4a73e
--- /dev/null
+++ b/spec/ruby/core/integer/div_spec.rb
@@ -0,0 +1,154 @@
+require_relative '../../spec_helper'
+
+describe "Integer#div" do
+ context "fixnum" do
+ it "returns self divided by the given argument as an Integer" do
+ 2.div(2).should == 1
+ 1.div(2).should == 0
+ 5.div(2).should == 2
+ end
+
+ it "rounds towards -inf" do
+ 8192.div(10).should == 819
+ 8192.div(-10).should == -820
+ (-8192).div(10).should == -820
+ (-8192).div(-10).should == 819
+ end
+
+ it "means (x / y).floor" do
+ 5.div(2).should == (5 / 2).floor
+ 5.div(2.0).should == (5 / 2.0).floor
+ 5.div(-2).should == (5 / -2).floor
+
+ 5.div(100).should == (5 / 100).floor
+ 5.div(100.0).should == (5 / 100.0).floor
+ 5.div(-100).should == (5 / -100).floor
+ end
+
+ it "calls #coerce and #div if argument responds to #coerce" do
+ x = mock("x")
+ y = mock("y")
+ result = mock("result")
+
+ y.should_receive(:coerce).and_return([x, y])
+ x.should_receive(:div).with(y).and_return(result)
+
+ 10.div(y).should == result
+ end
+
+ it "coerces self and the given argument to Floats and returns self divided by other as Integer" do
+ 1.div(0.2).should == 5
+ 1.div(0.16).should == 6
+ 1.div(0.169).should == 5
+ -1.div(50.4).should == -1
+ 1.div(bignum_value).should == 0
+ 1.div(Rational(1, 5)).should == 5
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0 and a Float" do
+ -> { 0.div(0.0) }.should.raise(ZeroDivisionError)
+ -> { 10.div(0.0) }.should.raise(ZeroDivisionError)
+ -> { -10.div(0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0 and not a Float" do
+ -> { 13.div(0) }.should.raise(ZeroDivisionError)
+ -> { 13.div(-0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a TypeError when given a non-numeric argument" do
+ -> { 13.div(mock('10')) }.should.raise(TypeError)
+ -> { 5.div("2") }.should.raise(TypeError)
+ -> { 5.div(:"2") }.should.raise(TypeError)
+ -> { 5.div([]) }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(88)
+ end
+
+ it "returns self divided by other" do
+ @bignum.div(4).should == 4611686018427387926
+ @bignum.div(Rational(4, 1)).should == 4611686018427387926
+ @bignum.div(bignum_value(2)).should == 1
+
+ (-(10**50)).div(-(10**40 + 1)).should == 9999999999
+ (10**50).div(10**40 + 1).should == 9999999999
+
+ (-10**50).div(10**40 + 1).should == -10000000000
+ (10**50).div(-(10**40 + 1)).should == -10000000000
+ end
+
+ it "handles fixnum_min / -1" do
+ (fixnum_min / -1).should == -fixnum_min
+ (fixnum_min / -1).should > 0
+
+ int_min = -2147483648
+ (int_min / -1).should == 2147483648
+ end
+
+ it "calls #coerce and #div if argument responds to #coerce" do
+ x = mock("x")
+ y = mock("y")
+ result = mock("result")
+
+ y.should_receive(:coerce).and_return([x, y])
+ x.should_receive(:div).with(y).and_return(result)
+
+ @bignum.div(y).should == result
+ end
+
+ it "means (x / y).floor" do
+ @bignum.div(2).should == (@bignum / 2).floor
+ @bignum.div(-2).should == (@bignum / -2).floor
+
+ @bignum.div(@bignum+1).should == (@bignum / (@bignum+1)).floor
+ @bignum.div(-(@bignum+1)).should == (@bignum / -(@bignum+1)).floor
+
+ @bignum.div(2.0).should == (@bignum / 2.0).floor
+ @bignum.div(100.0).should == (@bignum / 100.0).floor
+ end
+
+ it "looses precision if passed Float argument" do
+ @bignum.div(1).should_not == @bignum.div(1.0)
+ @bignum.div(4).should_not == @bignum.div(4.0)
+ @bignum.div(21).should_not == @bignum.div(21.0)
+ end
+
+ it "raises a TypeError when given a non-numeric" do
+ -> { @bignum.div(mock("10")) }.should.raise(TypeError)
+ -> { @bignum.div("2") }.should.raise(TypeError)
+ -> { @bignum.div(:symbol) }.should.raise(TypeError)
+ end
+
+ it "returns a result of integer division of self by a float argument" do
+ @bignum.div(4294967295.5).should.eql?(4294967296)
+ not_supported_on :opal do
+ @bignum.div(4294967295.0).should.eql?(4294967297)
+ @bignum.div(bignum_value(88).to_f).should.eql?(1)
+ @bignum.div((-bignum_value(88)).to_f).should.eql?(-1)
+ end
+ end
+
+ # #5490
+ it "raises ZeroDivisionError if the argument is 0 and is a Float" do
+ -> { @bignum.div(0.0) }.should.raise(ZeroDivisionError)
+ -> { @bignum.div(-0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises ZeroDivisionError if the argument is 0 and is not a Float" do
+ -> { @bignum.div(0) }.should.raise(ZeroDivisionError)
+ -> { @bignum.div(-0) }.should.raise(ZeroDivisionError)
+ end
+ end
+
+ context "rational" do
+ it "returns self divided by the given argument as an Integer" do
+ 2.div(6/5r).should == 1
+ 1.div(6/5r).should == 0
+ 5.div(6/5r).should == 4
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/divide_spec.rb b/spec/ruby/core/integer/divide_spec.rb
new file mode 100644
index 0000000000..e75432fd83
--- /dev/null
+++ b/spec/ruby/core/integer/divide_spec.rb
@@ -0,0 +1,126 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_coerce'
+
+describe "Integer#/" do
+ it_behaves_like :integer_arithmetic_coerce_not_rescue, :/
+
+ context "fixnum" do
+ it "returns self divided by the given argument" do
+ (2 / 2).should == 1
+ (3 / 2).should == 1
+ end
+
+ it "supports dividing negative numbers" do
+ (-1 / 10).should == -1
+ (-1 / 10**10).should == -1
+ (-1 / 10**20).should == -1
+ end
+
+ it "preservers sign correctly" do
+ (4 / 3).should == 1
+ (4 / -3).should == -2
+ (-4 / 3).should == -2
+ (-4 / -3).should == 1
+ (0 / -3).should == 0
+ (0 / 3).should == 0
+ end
+
+ it "returns result the same class as the argument" do
+ (3 / 2).should == 1
+ (3 / 2.0).should == 1.5
+ (3 / Rational(2, 1)).should == Rational(3, 2)
+ end
+
+ it "raises a ZeroDivisionError if the given argument is zero and not a Float" do
+ -> { 1 / 0 }.should.raise(ZeroDivisionError)
+ end
+
+ it "does NOT raise ZeroDivisionError if the given argument is zero and is a Float" do
+ (1 / 0.0).to_s.should == 'Infinity'
+ (-1 / 0.0).to_s.should == '-Infinity'
+ end
+
+ it "coerces fixnum and return self divided by other" do
+ (-1 / 50.4).should be_close(-0.0198412698412698, TOLERANCE)
+ (1 / bignum_value).should == 0
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> { 13 / mock('10') }.should.raise(TypeError)
+ -> { 13 / "10" }.should.raise(TypeError)
+ -> { 13 / :symbol }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(88)
+ end
+
+ it "returns self divided by other" do
+ (@bignum / 4).should == 4611686018427387926
+
+ (@bignum / bignum_value(2)).should == 1
+
+ (-(10**50) / -(10**40 + 1)).should == 9999999999
+ ((10**50) / (10**40 + 1)).should == 9999999999
+
+ ((-10**50) / (10**40 + 1)).should == -10000000000
+ ((10**50) / -(10**40 + 1)).should == -10000000000
+ end
+
+ it "preservers sign correctly" do
+ (4 / bignum_value).should == 0
+ (4 / -bignum_value).should == -1
+ (-4 / bignum_value).should == -1
+ (-4 / -bignum_value).should == 0
+ (0 / bignum_value).should == 0
+ (0 / -bignum_value).should == 0
+ end
+
+ it "returns self divided by Float" do
+ not_supported_on :opal do
+ (bignum_value(88) / 4294967295.0).should be_close(4294967297.0, TOLERANCE)
+ end
+ (bignum_value(88) / 4294967295.5).should be_close(4294967296.5, TOLERANCE)
+ end
+
+ it "returns result the same class as the argument" do
+ (@bignum / 4).should == 4611686018427387926
+ (@bignum / 4.0).should be_close(4611686018427387926, TOLERANCE)
+ (@bignum / Rational(4, 1)).should == Rational(4611686018427387926, 1)
+ end
+
+ it "does NOT raise ZeroDivisionError if other is zero and is a Float" do
+ (bignum_value / 0.0).to_s.should == 'Infinity'
+ (bignum_value / -0.0).to_s.should == '-Infinity'
+ end
+
+ it "raises a ZeroDivisionError if other is zero and not a Float" do
+ -> { @bignum / 0 }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a TypeError when given a non-numeric" do
+ -> { @bignum / mock('10') }.should.raise(TypeError)
+ -> { @bignum / "2" }.should.raise(TypeError)
+ -> { @bignum / :symbol }.should.raise(TypeError)
+ end
+ end
+
+ it "coerces the RHS and calls #coerce" do
+ obj = mock("integer plus")
+ obj.should_receive(:coerce).with(6).and_return([6, 3])
+ (6 / obj).should == 2
+ end
+
+ it "coerces the RHS and calls #coerce even if it's private" do
+ obj = Object.new
+ class << obj
+ private def coerce(n)
+ [n, 3]
+ end
+ end
+
+ (6 / obj).should == 2
+ end
+end
diff --git a/spec/ruby/core/integer/divmod_spec.rb b/spec/ruby/core/integer/divmod_spec.rb
new file mode 100644
index 0000000000..db470c5731
--- /dev/null
+++ b/spec/ruby/core/integer/divmod_spec.rb
@@ -0,0 +1,117 @@
+require_relative '../../spec_helper'
+
+describe "Integer#divmod" do
+ context "fixnum" do
+ it "returns an Array containing quotient and modulus obtained from dividing self by the given argument" do
+ 13.divmod(4).should == [3, 1]
+ 4.divmod(13).should == [0, 4]
+
+ 13.divmod(4.0).should == [3, 1]
+ 4.divmod(13.0).should == [0, 4]
+
+ 1.divmod(2.0).should == [0, 1.0]
+ 200.divmod(bignum_value).should == [0, 200]
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0" do
+ -> { 13.divmod(0) }.should.raise(ZeroDivisionError)
+ -> { 0.divmod(0) }.should.raise(ZeroDivisionError)
+ -> { -10.divmod(0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0 and a Float" do
+ -> { 0.divmod(0.0) }.should.raise(ZeroDivisionError)
+ -> { 10.divmod(0.0) }.should.raise(ZeroDivisionError)
+ -> { -10.divmod(0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10)
+ 13.divmod(obj)
+ }.should.raise(TypeError)
+ -> { 13.divmod("10") }.should.raise(TypeError)
+ -> { 13.divmod(:symbol) }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(55)
+ end
+
+ # Based on MRI's test/test_integer.rb (test_divmod),
+ # MRI maintains the following property:
+ # if q, r = a.divmod(b) ==>
+ # assert(0 < b ? (0 <= r && r < b) : (b < r && r <= 0))
+ # So, r is always between 0 and b.
+ it "returns an Array containing quotient and modulus obtained from dividing self by the given argument" do
+ @bignum.divmod(4).should == [4611686018427387917, 3]
+ @bignum.divmod(13).should == [1418980313362273205, 6]
+
+ @bignum.divmod(4.5).should == [4099276460824344576, 2.5]
+
+ not_supported_on :opal do
+ @bignum.divmod(4.0).should == [4611686018427387904, 0.0]
+ @bignum.divmod(13.0).should == [1418980313362273280, 3.0]
+
+ @bignum.divmod(2.0).should == [9223372036854775808, 0.0]
+ end
+
+ @bignum.divmod(bignum_value).should == [1, 55]
+
+ (-(10**50)).divmod(-(10**40 + 1)).should == [9999999999, -9999999999999999999999999999990000000001]
+ (10**50).divmod(10**40 + 1).should == [9999999999, 9999999999999999999999999999990000000001]
+
+ (-10**50).divmod(10**40 + 1).should == [-10000000000, 10000000000]
+ (10**50).divmod(-(10**40 + 1)).should == [-10000000000, -10000000000]
+ end
+
+ describe "with q = floor(x/y), a = q*b + r," do
+ it "returns [q,r] when a < 0, b > 0 and |a| < b" do
+ a = -@bignum + 1
+ b = @bignum
+ a.divmod(b).should == [-1, 1]
+ end
+
+ it "returns [q,r] when a > 0, b < 0 and a > |b|" do
+ b = -@bignum + 1
+ a = @bignum
+ a.divmod(b).should == [-2, -@bignum + 2]
+ end
+
+ it "returns [q,r] when a > 0, b < 0 and a < |b|" do
+ a = @bignum - 1
+ b = -@bignum
+ a.divmod(b).should == [-1, -1]
+ end
+
+ it "returns [q,r] when a < 0, b < 0 and |a| < |b|" do
+ a = -@bignum + 1
+ b = -@bignum
+ a.divmod(b).should == [0, -@bignum + 1]
+ end
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0" do
+ -> { @bignum.divmod(0) }.should.raise(ZeroDivisionError)
+ -> { (-@bignum).divmod(0) }.should.raise(ZeroDivisionError)
+ end
+
+ # Behaviour established as correct in r23953
+ it "raises a FloatDomainError if other is NaN" do
+ -> { @bignum.divmod(nan_value) }.should.raise(FloatDomainError)
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0 and a Float" do
+ -> { @bignum.divmod(0.0) }.should.raise(ZeroDivisionError)
+ -> { (-@bignum).divmod(0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a TypeError when the given argument is not an Integer" do
+ -> { @bignum.divmod(mock('10')) }.should.raise(TypeError)
+ -> { @bignum.divmod("10") }.should.raise(TypeError)
+ -> { @bignum.divmod(:symbol) }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/downto_spec.rb b/spec/ruby/core/integer/downto_spec.rb
new file mode 100644
index 0000000000..a244d3df55
--- /dev/null
+++ b/spec/ruby/core/integer/downto_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+
+describe "Integer#downto [stop] when self and stop are Integers" do
+ it "does not yield when stop is greater than self" do
+ result = []
+ 5.downto(6) { |x| result << x }
+ result.should == []
+ end
+
+ it "yields once when stop equals self" do
+ result = []
+ 5.downto(5) { |x| result << x }
+ result.should == [5]
+ end
+
+ it "yields while decreasing self until it is less than stop" do
+ result = []
+ 5.downto(2) { |x| result << x }
+ result.should == [5, 4, 3, 2]
+ end
+
+ it "yields while decreasing self until it less than ceil for a Float endpoint" do
+ result = []
+ 9.downto(1.3) {|i| result << i}
+ 3.downto(-1.3) {|i| result << i}
+ result.should == [9, 8, 7, 6, 5, 4, 3, 2, 3, 2, 1, 0, -1]
+ end
+
+ it "raises an ArgumentError for invalid endpoints" do
+ -> {1.downto("A") {|x| p x } }.should.raise(ArgumentError)
+ -> {1.downto(nil) {|x| p x } }.should.raise(ArgumentError)
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ result = []
+
+ enum = 5.downto(2)
+ enum.each { |i| result << i }
+
+ result.should == [5, 4, 3, 2]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "raises an ArgumentError for invalid endpoints" do
+ enum = 1.downto("A")
+ -> { enum.size }.should.raise(ArgumentError)
+ enum = 1.downto(nil)
+ -> { enum.size }.should.raise(ArgumentError)
+ end
+
+ it "returns self - stop + 1" do
+ 10.downto(5).size.should == 6
+ 10.downto(1).size.should == 10
+ 10.downto(0).size.should == 11
+ 0.downto(0).size.should == 1
+ -3.downto(-5).size.should == 3
+ end
+
+ it "returns 0 when stop > self" do
+ 4.downto(5).size.should == 0
+ -5.downto(0).size.should == 0
+ -5.downto(-3).size.should == 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/dup_spec.rb b/spec/ruby/core/integer/dup_spec.rb
new file mode 100644
index 0000000000..3e5739ad37
--- /dev/null
+++ b/spec/ruby/core/integer/dup_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Integer#dup" do
+ it "returns self for small integers" do
+ integer = 1_000
+ integer.dup.should.equal?(integer)
+ end
+
+ it "returns self for large integers" do
+ integer = 4_611_686_018_427_387_905
+ integer.dup.should.equal?(integer)
+ end
+end
diff --git a/spec/ruby/core/integer/element_reference_spec.rb b/spec/ruby/core/integer/element_reference_spec.rb
new file mode 100644
index 0000000000..c69ec2315d
--- /dev/null
+++ b/spec/ruby/core/integer/element_reference_spec.rb
@@ -0,0 +1,188 @@
+require_relative '../../spec_helper'
+
+describe "Integer#[]" do
+ context "fixnum" do
+ it "behaves like (n >> b) & 1" do
+ 0b101[1].should == 0
+ 0b101[2].should == 1
+ end
+
+ it "returns 1 if the nth bit is set" do
+ 15[1].should == 1
+ end
+
+ it "returns 1 if the nth bit is set (in two's-complement representation)" do
+ (-1)[1].should == 1
+ end
+
+ it "returns 0 if the nth bit is not set" do
+ 8[2].should == 0
+ end
+
+ it "returns 0 if the nth bit is not set (in two's-complement representation)" do
+ (-2)[0].should == 0
+ end
+
+ it "returns 0 if the nth bit is greater than the most significant bit" do
+ 2[3].should == 0
+ end
+
+ it "returns 1 if self is negative and the nth bit is greater than the most significant bit" do
+ (-1)[3].should == 1
+ end
+
+ it "returns 0 when passed a negative argument" do
+ 3[-1].should == 0
+ (-1)[-1].should == 0
+ end
+
+ it "calls #to_int to convert the argument to an Integer and returns 1 if the nth bit is set" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+
+ 2[obj].should == 1
+ end
+
+ it "calls #to_int to convert the argument to an Integer and returns 0 if the nth bit is set" do
+ obj = mock('0')
+ obj.should_receive(:to_int).and_return(0)
+
+ 2[obj].should == 0
+ end
+
+ it "accepts a Float argument and returns 0 if the bit at the truncated value is not set" do
+ 13[1.3].should == 0
+ end
+
+ it "accepts a Float argument and returns 1 if the bit at the truncated value is set" do
+ 13[2.1].should == 1
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { 3["3"] }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when #to_int does not return an Integer" do
+ obj = mock('asdf')
+ obj.should_receive(:to_int).and_return("asdf")
+ -> { 3[obj] }.should.raise(TypeError)
+ end
+
+ it "calls #to_int to coerce a String to a Bignum and returns 0" do
+ obj = mock('bignum value')
+ obj.should_receive(:to_int).and_return(bignum_value)
+
+ 3[obj].should == 0
+ end
+
+ it "returns 0 when passed a Float in the range of a Bignum" do
+ 3[bignum_value.to_f].should == 0
+ end
+
+ context "when index and length passed" do
+ it "returns specified number of bits from specified position" do
+ 0b101001101[2, 4].should == 0b0011
+ 0b101001101[2, 5].should == 0b10011
+ 0b101001101[2, 7].should == 0b1010011
+ end
+
+ it "ensures n[i, len] equals to (n >> i) & ((1 << len) - 1)" do
+ n = 0b101001101; i = 2; len = 4
+ n[i, len].should == (n >> i) & ((1 << len) - 1)
+ end
+
+ it "moves start position to the most significant bits when negative index passed" do
+ 0b000001[-1, 4].should == 0b10
+ 0b000001[-2, 4].should == 0b100
+ 0b000001[-3, 4].should == 0b1000
+ end
+
+ it "ignores negative length" do
+ 0b101001101[1, -1].should == 0b10100110
+ 0b101001101[2, -1].should == 0b1010011
+ 0b101001101[3, -1].should == 0b101001
+
+ 0b101001101[3, -5].should == 0b101001
+ 0b101001101[3, -15].should == 0b101001
+ 0b101001101[3, -125].should == 0b101001
+ end
+ end
+
+ context "when range passed" do
+ it "returns bits specified by range" do
+ 0b101001101[2..5].should == 0b0011
+ 0b101001101[2..6].should == 0b10011
+ 0b101001101[2..8].should == 0b1010011
+ end
+
+ it "ensures n[i..j] equals to (n >> i) & ((1 << (j - i + 1)) - 1)" do
+ n = 0b101001101; i = 2; j = 5
+ n[i..j].should == (n >> i) & ((1 << (j - i + 1)) - 1)
+ end
+
+ it "ensures n[i..] equals to (n >> i)" do
+ eval("0b101001101[3..]").should == 0b101001101 >> 3
+ end
+
+ it "moves lower boundary to the most significant bits when negative value passed" do
+ 0b000001[-1, 4].should == 0b10
+ 0b000001[-2, 4].should == 0b100
+ 0b000001[-3, 4].should == 0b1000
+ end
+
+ it "ignores upper boundary smaller than lower boundary" do
+ 0b101001101[4..1].should == 0b10100
+ 0b101001101[4..2].should == 0b10100
+ 0b101001101[-4..-5].should == 0b1010011010000
+ end
+
+ it "raises FloatDomainError if any boundary is infinity" do
+ -> { 0x0001[3..Float::INFINITY] }.should.raise(FloatDomainError, /Infinity/)
+ -> { 0x0001[-Float::INFINITY..3] }.should.raise(FloatDomainError, /-Infinity/)
+ end
+
+ context "when passed (..i)" do
+ it "returns 0 if all i bits equal 0" do
+ eval("0b10000[..1]").should == 0
+ eval("0b10000[..2]").should == 0
+ eval("0b10000[..3]").should == 0
+ end
+
+ it "raises ArgumentError if any of i bit equals 1" do
+ -> {
+ eval("0b111110[..3]")
+ }.should.raise(ArgumentError, /The beginless range for Integer#\[\] results in infinity/)
+ end
+ end
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(4996)
+ end
+
+ it "returns the nth bit in the binary representation of self" do
+ @bignum[2].should == 1
+ @bignum[9.2].should == 1
+ @bignum[21].should == 0
+ @bignum[0xffffffff].should == 0
+ @bignum[-0xffffffff].should == 0
+ end
+
+ it "tries to convert the given argument to an Integer using #to_int" do
+ @bignum[1.3].should == @bignum[1]
+
+ (obj = mock('2')).should_receive(:to_int).at_least(1).and_return(2)
+ @bignum[obj].should == 1
+ end
+
+ it "raises a TypeError when the given argument can't be converted to Integer" do
+ obj = mock('asdf')
+ -> { @bignum[obj] }.should.raise(TypeError)
+
+ obj.should_receive(:to_int).and_return("asdf")
+ -> { @bignum[obj] }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/eql_spec.rb b/spec/ruby/core/integer/eql_spec.rb
new file mode 100644
index 0000000000..9c80173206
--- /dev/null
+++ b/spec/ruby/core/integer/eql_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+describe "Integer#eql?" do
+ context "bignum" do
+ it "returns true for the same value" do
+ bignum_value.eql?(bignum_value).should == true
+ end
+
+ it "returns false for a different Integer value" do
+ bignum_value.eql?(bignum_value(1)).should == false
+ end
+
+ it "returns false for a Float with the same numeric value" do
+ bignum_value.eql?(bignum_value.to_f).should == false
+ end
+
+ it "returns false for a Rational with the same numeric value" do
+ bignum_value.eql?(Rational(bignum_value)).should == false
+ end
+
+ it "returns false for a Fixnum-range Integer" do
+ bignum_value.eql?(42).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/equal_value_spec.rb b/spec/ruby/core/integer/equal_value_spec.rb
new file mode 100644
index 0000000000..67a73713af
--- /dev/null
+++ b/spec/ruby/core/integer/equal_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal'
+
+describe "Integer#==" do
+ it_behaves_like :integer_equal, :==
+end
diff --git a/spec/ruby/core/integer/even_spec.rb b/spec/ruby/core/integer/even_spec.rb
new file mode 100644
index 0000000000..578979320e
--- /dev/null
+++ b/spec/ruby/core/integer/even_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+
+describe "Integer#even?" do
+ context "fixnum" do
+ it "returns true for a Fixnum when it is an even number" do
+ (-2).even?.should == true
+ (-1).even?.should == false
+
+ 0.even?.should == true
+ 1.even?.should == false
+ 2.even?.should == true
+ end
+
+ it "returns true for a Bignum when it is an even number" do
+ bignum_value(0).even?.should == true
+ bignum_value(1).even?.should == false
+
+ (-bignum_value(0)).even?.should == true
+ (-bignum_value(1)).even?.should == false
+ end
+ end
+
+ context "bignum" do
+ it "returns true if self is even and positive" do
+ (10000**10).even?.should == true
+ end
+
+ it "returns true if self is even and negative" do
+ (-10000**10).even?.should == true
+ end
+
+ it "returns false if self is odd and positive" do
+ (9879**976).even?.should == false
+ end
+
+ it "returns false if self is odd and negative" do
+ (-9879**976).even?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/exponent_spec.rb b/spec/ruby/core/integer/exponent_spec.rb
new file mode 100644
index 0000000000..c610661aff
--- /dev/null
+++ b/spec/ruby/core/integer/exponent_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/exponent'
+
+describe "Integer#**" do
+ it_behaves_like :integer_exponent, :**
+end
diff --git a/spec/ruby/core/integer/fdiv_spec.rb b/spec/ruby/core/integer/fdiv_spec.rb
new file mode 100644
index 0000000000..7854d66dec
--- /dev/null
+++ b/spec/ruby/core/integer/fdiv_spec.rb
@@ -0,0 +1,100 @@
+require_relative '../../spec_helper'
+
+describe "Integer#fdiv" do
+ it "performs floating-point division between self and a fixnum" do
+ 8.fdiv(7).should be_close(1.14285714285714, TOLERANCE)
+ end
+
+ it "performs floating-point division between self and a bignum" do
+ 8.fdiv(bignum_value).should be_close(8.673617379884035e-19, TOLERANCE)
+ end
+
+ it "performs floating-point division between self bignum and a bignum" do
+ num = 1000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146009
+ den = 2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+ num.fdiv(den).should == 500.0
+ end
+
+ it "rounds to the correct value for bignums" do
+ den = 9 * 10**342
+
+ num = 1 * 10**344
+ num.fdiv(den).should == 11.11111111111111
+
+ num = 1 * 10**343
+ num.fdiv(den).should == 1.1111111111111112
+
+ num = 1 * 10**342
+ num.fdiv(den).should == 0.1111111111111111
+
+ num = 2 * 10**342
+ num.fdiv(den).should == 0.2222222222222222
+
+ num = 3 * 10**342
+ num.fdiv(den).should == 0.3333333333333333
+
+ num = 4 * 10**342
+ num.fdiv(den).should == 0.4444444444444444
+
+ num = 5 * 10**342
+ num.fdiv(den).should == 0.5555555555555556
+
+ num = 6 * 10**342
+ num.fdiv(den).should == 0.6666666666666666
+
+ num = 7 * 10**342
+ num.fdiv(den).should == 0.7777777777777778
+
+ num = 8 * 10**342
+ num.fdiv(den).should == 0.8888888888888888
+
+ num = 9 * 10**342
+ num.fdiv(den).should == 1.0
+
+ num = -5 * 10**342
+ num.fdiv(den).should == -0.5555555555555556
+ end
+
+ it "rounds to the correct float for bignum denominators" do
+ 1.fdiv(10**324).should == 0.0
+ 1.fdiv(10**323).should == 1.0e-323
+ end
+
+ it "performs floating-point division between self and a Float" do
+ 8.fdiv(9.0).should be_close(0.888888888888889, TOLERANCE)
+ end
+
+ it "returns NaN when the argument is NaN" do
+ -1.fdiv(nan_value).nan?.should == true
+ 1.fdiv(nan_value).nan?.should == true
+ end
+
+ it "returns Infinity when the argument is 0" do
+ 1.fdiv(0).infinite?.should == 1
+ end
+
+ it "returns -Infinity when the argument is 0 and self is negative" do
+ -1.fdiv(0).infinite?.should == -1
+ end
+
+ it "returns Infinity when the argument is 0.0" do
+ 1.fdiv(0.0).infinite?.should == 1
+ end
+
+ it "returns -Infinity when the argument is 0.0 and self is negative" do
+ -1.fdiv(0.0).infinite?.should == -1
+ end
+
+ it "raises a TypeError when argument isn't numeric" do
+ -> { 1.fdiv(mock('non-numeric')) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when passed multiple arguments" do
+ -> { 1.fdiv(6,0.2) }.should.raise(ArgumentError)
+ end
+
+ it "follows the coercion protocol" do
+ (obj = mock('10')).should_receive(:coerce).with(1).and_return([1, 10])
+ 1.fdiv(obj).should == 0.1
+ end
+end
diff --git a/spec/ruby/core/integer/fixtures/classes.rb b/spec/ruby/core/integer/fixtures/classes.rb
new file mode 100644
index 0000000000..65948efb9f
--- /dev/null
+++ b/spec/ruby/core/integer/fixtures/classes.rb
@@ -0,0 +1,14 @@
+module IntegerSpecs
+ class CoerceError < StandardError
+ end
+
+ class CoercibleNumeric
+ def initialize(v) @v = v end
+ def coerce(other) [self.class.new(other), self] end
+ def >(other) @v.to_i > other.to_i end
+ def >=(other) @v.to_i >= other.to_i end
+ def <(other) @v.to_i < other.to_i end
+ def <=(other) @v.to_i <= other.to_i end
+ def to_i() @v.to_i end
+ end
+end
diff --git a/spec/ruby/core/integer/floor_spec.rb b/spec/ruby/core/integer/floor_spec.rb
new file mode 100644
index 0000000000..8fb84d58cb
--- /dev/null
+++ b/spec/ruby/core/integer/floor_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+require_relative 'shared/integer_rounding'
+require_relative 'shared/integer_floor_precision'
+
+describe "Integer#floor" do
+ it_behaves_like :integer_to_i, :floor
+ it_behaves_like :integer_rounding_positive_precision, :floor
+
+ context "with precision" do
+ it_behaves_like :integer_floor_precision, :Integer
+ end
+end
diff --git a/spec/ruby/core/integer/gcd_spec.rb b/spec/ruby/core/integer/gcd_spec.rb
new file mode 100644
index 0000000000..1fff785927
--- /dev/null
+++ b/spec/ruby/core/integer/gcd_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+
+describe "Integer#gcd" do
+ it "returns self if equal to the argument" do
+ 1.gcd(1).should == 1
+ 398.gcd(398).should == 398
+ end
+
+ it "returns an Integer" do
+ 36.gcd(6).should.is_a?(Integer)
+ 4.gcd(20981).should.is_a?(Integer)
+ end
+
+ it "returns the greatest common divisor of self and argument" do
+ 10.gcd(5).should == 5
+ 200.gcd(20).should == 20
+ end
+
+ it "returns a positive integer even if self is negative" do
+ -12.gcd(6).should == 6
+ -100.gcd(100).should == 100
+ end
+
+ it "returns a positive integer even if the argument is negative" do
+ 12.gcd(-6).should == 6
+ 100.gcd(-100).should == 100
+ end
+
+ it "returns a positive integer even if both self and argument are negative" do
+ -12.gcd(-6).should == 6
+ -100.gcd(-100).should == 100
+ end
+
+ it "accepts a Bignum argument" do
+ bignum = 9999**99
+ bignum.should.is_a?(Integer)
+ 99.gcd(bignum).should == 99
+ end
+
+ it "works if self is a Bignum" do
+ bignum = 9999**99
+ bignum.should.is_a?(Integer)
+ bignum.gcd(99).should == 99
+ end
+
+ it "doesn't cause an integer overflow" do
+ [2 ** (1.size * 8 - 2), 0x8000000000000000].each do |max|
+ [max - 1, max, max + 1].each do |num|
+ num.gcd(num).should == num
+ (-num).gcd(num).should == num
+ (-num).gcd(-num).should == num
+ num.gcd(-num).should == num
+ end
+ end
+ end
+
+ it "raises an ArgumentError if not given an argument" do
+ -> { 12.gcd }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if given more than one argument" do
+ -> { 12.gcd(30, 20) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError unless the argument is an Integer" do
+ -> { 39.gcd(3.8) }.should.raise(TypeError)
+ -> { 45872.gcd([]) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/gcdlcm_spec.rb b/spec/ruby/core/integer/gcdlcm_spec.rb
new file mode 100644
index 0000000000..419bf9f275
--- /dev/null
+++ b/spec/ruby/core/integer/gcdlcm_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+
+describe "Integer#gcdlcm" do
+ it "returns [self, self] if self is equal to the argument" do
+ 1.gcdlcm(1).should == [1, 1]
+ 398.gcdlcm(398).should == [398, 398]
+ end
+
+ it "returns an Array" do
+ 36.gcdlcm(6).should.is_a?(Array)
+ 4.gcdlcm(20981).should.is_a?(Array)
+ end
+
+ it "returns a two-element Array" do
+ 36.gcdlcm(876).size.should == 2
+ 29.gcdlcm(17).size.should == 2
+ end
+
+ it "returns the greatest common divisor of self and argument as the first element" do
+ 10.gcdlcm(5)[0].should == 10.gcd(5)
+ 200.gcdlcm(20)[0].should == 200.gcd(20)
+ end
+
+ it "returns the least common multiple of self and argument as the last element" do
+ 10.gcdlcm(5)[1].should == 10.lcm(5)
+ 200.gcdlcm(20)[1].should == 200.lcm(20)
+ end
+
+ it "accepts a Bignum argument" do
+ bignum = 91999**99
+ bignum.should.is_a?(Integer)
+ 99.gcdlcm(bignum).should == [99.gcd(bignum), 99.lcm(bignum)]
+ end
+
+ it "works if self is a Bignum" do
+ bignum = 9999**89
+ bignum.should.is_a?(Integer)
+ bignum.gcdlcm(99).should == [bignum.gcd(99), bignum.lcm(99)]
+ end
+
+ it "raises an ArgumentError if not given an argument" do
+ -> { 12.gcdlcm }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if given more than one argument" do
+ -> { 12.gcdlcm(30, 20) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError unless the argument is an Integer" do
+ -> { 39.gcdlcm(3.8) }.should.raise(TypeError)
+ -> { 45872.gcdlcm([]) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/gt_spec.rb b/spec/ruby/core/integer/gt_spec.rb
new file mode 100644
index 0000000000..75436144cf
--- /dev/null
+++ b/spec/ruby/core/integer/gt_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_coerce'
+
+describe "Integer#>" do
+ it_behaves_like :integer_comparison_coerce_not_rescue, :>
+
+ context "fixnum" do
+ it "returns true if self is greater than the given argument" do
+ (13 > 2).should == true
+ (-500 > -600).should == true
+
+ (1 > 5).should == false
+ (5 > 5).should == false
+
+ (900 > bignum_value).should == false
+ (5 > 4.999).should == true
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { 5 > "4" }.should.raise(ArgumentError)
+ -> { 5 > mock('x') }.should.raise(ArgumentError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(732)
+ end
+
+ it "returns true if self is greater than the given argument" do
+ (@bignum > (@bignum - 1)).should == true
+ (@bignum > 14.6).should == true
+ (@bignum > 10).should == true
+
+ (@bignum > (@bignum + 500)).should == false
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { @bignum > "4" }.should.raise(ArgumentError)
+ -> { @bignum > mock('str') }.should.raise(ArgumentError)
+ end
+
+ it "dispatches the correct operator after coercion" do
+ (bignum_value > IntegerSpecs::CoercibleNumeric.new(1)).should == true
+ (bignum_value > IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/gte_spec.rb b/spec/ruby/core/integer/gte_spec.rb
new file mode 100644
index 0000000000..e5cb892efd
--- /dev/null
+++ b/spec/ruby/core/integer/gte_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_coerce'
+
+describe "Integer#>=" do
+ it_behaves_like :integer_comparison_coerce_not_rescue, :>=
+
+ context "fixnum" do
+ it "returns true if self is greater than or equal to the given argument" do
+ (13 >= 2).should == true
+ (-500 >= -600).should == true
+
+ (1 >= 5).should == false
+ (2 >= 2).should == true
+ (5 >= 5).should == true
+
+ (900 >= bignum_value).should == false
+ (5 >= 4.999).should == true
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { 5 >= "4" }.should.raise(ArgumentError)
+ -> { 5 >= mock('x') }.should.raise(ArgumentError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(14)
+ end
+
+ it "returns true if self is greater than or equal to other" do
+ (@bignum >= @bignum).should == true
+ (@bignum >= (@bignum + 2)).should == false
+ (@bignum >= 5664.2).should == true
+ (@bignum >= 4).should == true
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { @bignum >= "4" }.should.raise(ArgumentError)
+ -> { @bignum >= mock('str') }.should.raise(ArgumentError)
+ end
+
+ it "dispatches the correct operator after coercion" do
+ (bignum_value >= IntegerSpecs::CoercibleNumeric.new(1)).should == true
+ (bignum_value >= IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/integer_spec.rb b/spec/ruby/core/integer/integer_spec.rb
new file mode 100644
index 0000000000..19a962d548
--- /dev/null
+++ b/spec/ruby/core/integer/integer_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "Integer" do
+ it "includes Comparable" do
+ Integer.include?(Comparable).should == true
+ end
+
+ it "is the class of both small and large integers" do
+ 42.class.should.equal?(Integer)
+ bignum_value.class.should.equal?(Integer)
+ end
+end
+
+describe "Integer#integer?" do
+ it "returns true for Integers" do
+ 0.should.integer?
+ -1.should.integer?
+ bignum_value.should.integer?
+ end
+end
diff --git a/spec/ruby/core/integer/lcm_spec.rb b/spec/ruby/core/integer/lcm_spec.rb
new file mode 100644
index 0000000000..6659247d1e
--- /dev/null
+++ b/spec/ruby/core/integer/lcm_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+
+describe "Integer#lcm" do
+ it "returns self if equal to the argument" do
+ 1.lcm(1).should == 1
+ 398.lcm(398).should == 398
+ end
+
+ it "returns an Integer" do
+ 36.lcm(6).should.is_a?(Integer)
+ 4.lcm(20981).should.is_a?(Integer)
+ end
+
+ it "returns the least common multiple of self and argument" do
+ 200.lcm(2001).should == 400200
+ 99.lcm(90).should == 990
+ end
+
+ it "returns a positive integer even if self is negative" do
+ -12.lcm(6).should == 12
+ -100.lcm(100).should == 100
+ end
+
+ it "returns a positive integer even if the argument is negative" do
+ 12.lcm(-6).should == 12
+ 100.lcm(-100).should == 100
+ end
+
+ it "returns a positive integer even if both self and argument are negative" do
+ -12.lcm(-6).should == 12
+ -100.lcm(-100).should == 100
+ end
+
+ it "accepts a Bignum argument" do
+ bignum = 9999**99
+ bignum.should.is_a?(Integer)
+ 99.lcm(bignum).should == bignum
+ end
+
+ it "works if self is a Bignum" do
+ bignum = 9999**99
+ bignum.should.is_a?(Integer)
+ bignum.lcm(99).should == bignum
+ end
+
+ it "raises an ArgumentError if not given an argument" do
+ -> { 12.lcm }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if given more than one argument" do
+ -> { 12.lcm(30, 20) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError unless the argument is an Integer" do
+ -> { 39.lcm(3.8) }.should.raise(TypeError)
+ -> { 45872.lcm([]) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/left_shift_spec.rb b/spec/ruby/core/integer/left_shift_spec.rb
new file mode 100644
index 0000000000..7eedb91228
--- /dev/null
+++ b/spec/ruby/core/integer/left_shift_spec.rb
@@ -0,0 +1,211 @@
+require_relative '../../spec_helper'
+
+describe "Integer#<< (with n << m)" do
+ context "fixnum" do
+ it "returns n shifted left m bits when n > 0, m > 0" do
+ (1 << 1).should == 2
+ end
+
+ it "returns n shifted left m bits when n < 0, m > 0" do
+ (-1 << 1).should == -2
+ (-7 << 1).should == -14
+ (-42 << 2).should == -168
+ end
+
+ it "returns n shifted right m bits when n > 0, m < 0" do
+ (2 << -1).should == 1
+ end
+
+ it "returns n shifted right m bits when n < 0, m < 0" do
+ (-2 << -1).should == -1
+ end
+
+ it "returns 0 when n == 0" do
+ (0 << 1).should == 0
+ end
+
+ it "returns n when n > 0, m == 0" do
+ (1 << 0).should == 1
+ end
+
+ it "returns n when n < 0, m == 0" do
+ (-1 << 0).should == -1
+ end
+
+ it "returns 0 when n > 0, m < 0 and n < 2**-m" do
+ (3 << -2).should == 0
+ (7 << -3).should == 0
+ (127 << -7).should == 0
+
+ # To make sure the exponent is not truncated
+ (7 << -32).should == 0
+ (7 << -64).should == 0
+ end
+
+ it "returns -1 when n < 0, m < 0 and n > -(2**-m)" do
+ (-3 << -2).should == -1
+ (-7 << -3).should == -1
+ (-127 << -7).should == -1
+
+ # To make sure the exponent is not truncated
+ (-7 << -32).should == -1
+ (-7 << -64).should == -1
+ end
+
+ it "returns 0 when m < 0 and m is a Bignum" do
+ (3 << -bignum_value).should == 0
+ end
+
+ it "returns a Bignum == fixnum_max * 2 when fixnum_max << 1 and n > 0" do
+ result = fixnum_max << 1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_max * 2
+ end
+
+ it "returns a Bignum == fixnum_min * 2 when fixnum_min << 1 and n < 0" do
+ result = fixnum_min << 1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_min * 2
+ end
+
+ it "calls #to_int to convert the argument to an Integer" do
+ obj = mock("4")
+ obj.should_receive(:to_int).and_return(4)
+ (3 << obj).should == 48
+
+ obj = mock("to_int_neg_bignum")
+ obj.should_receive(:to_int).and_return(-bignum_value)
+ (3 << obj).should == 0
+ end
+
+ it "raises a TypeError when #to_int does not return an Integer" do
+ obj = mock("a string")
+ obj.should_receive(:to_int).and_return("asdf")
+
+ -> { 3 << obj }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { 3 << nil }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { 3 << "4" }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value * 8 # 2 ** 67
+ end
+
+ it "returns n shifted left m bits when n > 0, m > 0" do
+ (@bignum << 4).should == 2361183241434822606848
+ end
+
+ it "returns n shifted left m bits when n < 0, m > 0" do
+ (-@bignum << 9).should == -75557863725914323419136
+ end
+
+ it "returns n shifted right m bits when n > 0, m < 0" do
+ (@bignum << -1).should == 73786976294838206464
+ end
+
+ it "returns n shifted right m bits when n < 0, m < 0" do
+ (-@bignum << -2).should == -36893488147419103232
+ end
+
+ it "returns n when n > 0, m == 0" do
+ (@bignum << 0).should == @bignum
+ end
+
+ it "returns n when n < 0, m == 0" do
+ (-@bignum << 0).should == -@bignum
+ end
+
+ it "returns 0 when m < 0 and m == p where 2**p > n >= 2**(p-1)" do
+ (@bignum << -68).should == 0
+ end
+
+ it "returns a Fixnum == fixnum_max when (fixnum_max * 2) << -1 and n > 0" do
+ result = (fixnum_max * 2) << -1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_max
+ end
+
+ it "returns a Fixnum == fixnum_min when (fixnum_min * 2) << -1 and n < 0" do
+ result = (fixnum_min * 2) << -1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_min
+ end
+
+ it "calls #to_int to convert the argument to an Integer" do
+ obj = mock("4")
+ obj.should_receive(:to_int).and_return(4)
+
+ (@bignum << obj).should == 2361183241434822606848
+ end
+
+ it "raises a TypeError when #to_int does not return an Integer" do
+ obj = mock("a string")
+ obj.should_receive(:to_int).and_return("asdf")
+
+ -> { @bignum << obj }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { @bignum << nil }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { @bignum << "4" }.should.raise(TypeError)
+ end
+ end
+
+ context "when m is a bignum or larger than int" do
+ it "returns -1 when m < 0 and n < 0" do
+ (-1 << -bignum_value).should == -1
+ (-1 << -(2**40)).should == -1
+
+ (-bignum_value << -bignum_value).should == -1
+ (-bignum_value << -(2**40)).should == -1
+ end
+
+ it "returns 0 when m < 0 and n >= 0" do
+ (0 << -bignum_value).should == 0
+ (1 << -bignum_value).should == 0
+ (bignum_value << -bignum_value).should == 0
+
+ (0 << -(2**40)).should == 0
+ (1 << -(2**40)).should == 0
+ (bignum_value << -(2**40)).should == 0
+ end
+
+ it "returns 0 when m > 0 long and n == 0" do
+ (0 << (2**40)).should == 0
+ end
+
+ it "returns 0 when m > 0 bignum and n == 0" do
+ (0 << bignum_value).should == 0
+ end
+
+ it "raises RangeError when m > 0 and n != 0" do
+ # https://bugs.ruby-lang.org/issues/18518#note-9
+ limit = RUBY_ENGINE == 'ruby' ? 2**67 : 2**32
+
+ coerce_long = mock("long")
+ coerce_long.stub!(:to_int).and_return(limit)
+ coerce_bignum = mock("bignum")
+ coerce_bignum.stub!(:to_int).and_return(bignum_value)
+ exps = [limit, coerce_long]
+ exps << bignum_value << coerce_bignum if bignum_value >= limit
+
+ exps.each { |exp|
+ -> { (1 << exp) }.should.raise(RangeError, 'shift width too big')
+ -> { (-1 << exp) }.should.raise(RangeError, 'shift width too big')
+ -> { (bignum_value << exp) }.should.raise(RangeError, 'shift width too big')
+ -> { (-bignum_value << exp) }.should.raise(RangeError, 'shift width too big')
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/lt_spec.rb b/spec/ruby/core/integer/lt_spec.rb
new file mode 100644
index 0000000000..dd1391d093
--- /dev/null
+++ b/spec/ruby/core/integer/lt_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_coerce'
+
+describe "Integer#<" do
+ it_behaves_like :integer_comparison_coerce_not_rescue, :<
+
+ context "fixnum" do
+ it "returns true if self is less than the given argument" do
+ (2 < 13).should == true
+ (-600 < -500).should == true
+
+ (5 < 1).should == false
+ (5 < 5).should == false
+
+ (900 < bignum_value).should == true
+ (5 < 4.999).should == false
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { 5 < "4" }.should.raise(ArgumentError)
+ -> { 5 < mock('x') }.should.raise(ArgumentError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(32)
+ end
+
+ it "returns true if self is less than the given argument" do
+ (@bignum < @bignum + 1).should == true
+ (-@bignum < -(@bignum - 1)).should == true
+
+ (@bignum < 1).should == false
+ (@bignum < 5).should == false
+
+ (@bignum < 4.999).should == false
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { @bignum < "4" }.should.raise(ArgumentError)
+ -> { @bignum < mock('str') }.should.raise(ArgumentError)
+ end
+
+ it "dispatches the correct operator after coercion" do
+ (bignum_value < IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == true
+ (bignum_value < IntegerSpecs::CoercibleNumeric.new(1)).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/lte_spec.rb b/spec/ruby/core/integer/lte_spec.rb
new file mode 100644
index 0000000000..9ef1c9f604
--- /dev/null
+++ b/spec/ruby/core/integer/lte_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+require_relative 'shared/comparison_coerce'
+
+describe "Integer#<=" do
+ it_behaves_like :integer_comparison_coerce_not_rescue, :<=
+
+ context "fixnum" do
+ it "returns true if self is less than or equal to other" do
+ (2 <= 13).should == true
+ (-600 <= -500).should == true
+
+ (5 <= 1).should == false
+ (5 <= 5).should == true
+ (-2 <= -2).should == true
+
+ (900 <= bignum_value).should == true
+ (5 <= 4.999).should == false
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { 5 <= "4" }.should.raise(ArgumentError)
+ -> { 5 <= mock('x') }.should.raise(ArgumentError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(39)
+ end
+
+ it "returns true if self is less than or equal to other" do
+ (@bignum <= @bignum).should == true
+ (-@bignum <= -(@bignum - 1)).should == true
+
+ (@bignum <= 4.999).should == false
+ end
+
+ it "returns false if compares with near float" do
+ (@bignum <= (@bignum + 0.0)).should == false
+ (@bignum <= (@bignum + 0.5)).should == false
+ end
+
+ it "returns true for bignums compare to a bigger float" do
+ (18446744073709551616 <= 1.8446744073709552e+19).should == true
+ (@bignum <= (@bignum + 9999.0)).should == true
+ end
+
+ it "raises an ArgumentError when given a non-Integer" do
+ -> { @bignum <= "4" }.should.raise(ArgumentError)
+ -> { @bignum <= mock('str') }.should.raise(ArgumentError)
+ end
+
+ it "dispatches the correct operator after coercion" do
+ (bignum_value <= IntegerSpecs::CoercibleNumeric.new(bignum_value * 2)).should == true
+ (bignum_value <= IntegerSpecs::CoercibleNumeric.new(1)).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/magnitude_spec.rb b/spec/ruby/core/integer/magnitude_spec.rb
new file mode 100644
index 0000000000..48cf1a8534
--- /dev/null
+++ b/spec/ruby/core/integer/magnitude_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/abs'
+
+describe "Integer#magnitude" do
+ it_behaves_like :integer_abs, :magnitude
+end
diff --git a/spec/ruby/core/integer/minus_spec.rb b/spec/ruby/core/integer/minus_spec.rb
new file mode 100644
index 0000000000..5fd3a81a72
--- /dev/null
+++ b/spec/ruby/core/integer/minus_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_coerce'
+
+describe "Integer#-" do
+ it_behaves_like :integer_arithmetic_coerce_not_rescue, :-
+
+ context "fixnum" do
+ it "returns self minus the given Integer" do
+ (5 - 10).should == -5
+ (9237212 - 5_280).should == 9231932
+
+ (781 - 0.5).should == 780.5
+ (2_560_496 - bignum_value).should == -18446744073706991120
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10)
+ 13 - obj
+ }.should.raise(TypeError)
+ -> { 13 - "10" }.should.raise(TypeError)
+ -> { 13 - :symbol }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(314)
+ end
+
+ it "returns self minus the given Integer" do
+ (@bignum - 9).should == 18446744073709551921
+ (@bignum - 12.57).should be_close(18446744073709551917.43, TOLERANCE)
+ (@bignum - bignum_value(42)).should == 272
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> { @bignum - mock('10') }.should.raise(TypeError)
+ -> { @bignum - "10" }.should.raise(TypeError)
+ -> { @bignum - :symbol }.should.raise(TypeError)
+ end
+ end
+
+ it "coerces the RHS and calls #coerce" do
+ obj = mock("integer plus")
+ obj.should_receive(:coerce).with(5).and_return([5, 10])
+ (5 - obj).should == -5
+ end
+
+ it "coerces the RHS and calls #coerce even if it's private" do
+ obj = Object.new
+ class << obj
+ private def coerce(n)
+ [n, 10]
+ end
+ end
+
+ (5 - obj).should == -5
+ end
+end
diff --git a/spec/ruby/core/integer/modulo_spec.rb b/spec/ruby/core/integer/modulo_spec.rb
new file mode 100644
index 0000000000..e263338e38
--- /dev/null
+++ b/spec/ruby/core/integer/modulo_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/modulo'
+
+describe "Integer#%" do
+ it_behaves_like :integer_modulo, :%
+end
+
+describe "Integer#modulo" do
+ it_behaves_like :integer_modulo, :modulo
+end
diff --git a/spec/ruby/core/integer/multiply_spec.rb b/spec/ruby/core/integer/multiply_spec.rb
new file mode 100644
index 0000000000..7f30eebaa5
--- /dev/null
+++ b/spec/ruby/core/integer/multiply_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_coerce'
+
+describe "Integer#*" do
+ it_behaves_like :integer_arithmetic_coerce_not_rescue, :*
+
+ context "fixnum" do
+ it "returns self multiplied by the given Integer" do
+ (4923 * 2).should == 9846
+ (1342177 * 800).should == 1073741600
+ (65536 * 65536).should == 4294967296
+
+ (256 * bignum_value).should == 4722366482869645213696
+ (6712 * 0.25).should == 1678.0
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10)
+ 13 * obj
+ }.should.raise(TypeError)
+ -> { 13 * "10" }.should.raise(TypeError)
+ -> { 13 * :symbol }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(772)
+ end
+
+ it "returns self multiplied by the given Integer" do
+ (@bignum * (1/bignum_value(0xffff).to_f)).should be_close(1.0, TOLERANCE)
+ (@bignum * (1/bignum_value(0xffff).to_f)).should be_close(1.0, TOLERANCE)
+ (@bignum * 10).should == 184467440737095523880
+ (@bignum * (@bignum - 40)).should == 340282366920938491207277694290934407024
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> { @bignum * mock('10') }.should.raise(TypeError)
+ -> { @bignum * "10" }.should.raise(TypeError)
+ -> { @bignum * :symbol }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/next_spec.rb b/spec/ruby/core/integer/next_spec.rb
new file mode 100644
index 0000000000..63c4e67893
--- /dev/null
+++ b/spec/ruby/core/integer/next_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/next'
+
+describe "Integer#next" do
+ it_behaves_like :integer_next, :next
+end
diff --git a/spec/ruby/core/integer/nobits_spec.rb b/spec/ruby/core/integer/nobits_spec.rb
new file mode 100644
index 0000000000..f1a3aca9d5
--- /dev/null
+++ b/spec/ruby/core/integer/nobits_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Integer#nobits?" do
+ it "returns true if and only if all no bits of the argument are set in the receiver" do
+ 42.nobits?(42).should == false
+ 0b1010_1010.nobits?(0b1000_0010).should == false
+ 0b1010_1010.nobits?(0b1000_0001).should == false
+ 0b0100_0101.nobits?(0b1010_1010).should == true
+ different_bignum = (2 * bignum_value) & (~bignum_value)
+ (0b1010_1010 | different_bignum).nobits?(0b1000_0010 | bignum_value).should == false
+ (0b1010_1010 | different_bignum).nobits?(0b1000_0001 | bignum_value).should == false
+ (0b0100_0101 | different_bignum).nobits?(0b1010_1010 | bignum_value).should == true
+ end
+
+ it "handles negative values using two's complement notation" do
+ (~0b1101).nobits?(0b1101).should == true
+ (-42).nobits?(-42).should == false
+ (~0b1101).nobits?(~0b10).should == false
+ (~(0b1101 | bignum_value)).nobits?(~(0b10 | bignum_value)).should == false
+ end
+
+ it "coerces the rhs using to_int" do
+ obj = mock("the int 0b10")
+ obj.should_receive(:to_int).and_return(0b10)
+ 0b110.nobits?(obj).should == false
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:coerce).any_number_of_times.and_return([42,10])
+ 13.nobits?(obj)
+ }.should.raise(TypeError)
+ -> { 13.nobits?("10") }.should.raise(TypeError)
+ -> { 13.nobits?(:symbol) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/numerator_spec.rb b/spec/ruby/core/integer/numerator_spec.rb
new file mode 100644
index 0000000000..f4149d770e
--- /dev/null
+++ b/spec/ruby/core/integer/numerator_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+
+describe "Integer#numerator" do
+ before :all do
+ @numbers = [
+ 0,
+ 29871,
+ 99999999999999**99,
+ 72628191273,
+ ].map{|n| [-n, n]}.flatten
+ end
+
+ it "returns self" do
+ @numbers.each do |number|
+ number.numerator.should == number
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/odd_spec.rb b/spec/ruby/core/integer/odd_spec.rb
new file mode 100644
index 0000000000..f9e50ec790
--- /dev/null
+++ b/spec/ruby/core/integer/odd_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+
+describe "Integer#odd?" do
+ context "fixnum" do
+ it "returns true when self is an odd number" do
+ (-2).odd?.should == false
+ (-1).odd?.should == true
+
+ 0.odd?.should == false
+ 1.odd?.should == true
+ 2.odd?.should == false
+
+ bignum_value(0).odd?.should == false
+ bignum_value(1).odd?.should == true
+
+ (-bignum_value(0)).odd?.should == false
+ (-bignum_value(1)).odd?.should == true
+ end
+ end
+
+ context "bignum" do
+ it "returns true if self is odd and positive" do
+ (987279**19).odd?.should == true
+ end
+
+ it "returns true if self is odd and negative" do
+ (-9873389**97).odd?.should == true
+ end
+
+ it "returns false if self is even and positive" do
+ (10000000**10).odd?.should == false
+ end
+
+ it "returns false if self is even and negative" do
+ (-1000000**100).odd?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/ord_spec.rb b/spec/ruby/core/integer/ord_spec.rb
new file mode 100644
index 0000000000..8b7dfe460d
--- /dev/null
+++ b/spec/ruby/core/integer/ord_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Integer#ord" do
+ it "returns self" do
+ 20.ord.should.eql?(20)
+ 40.ord.should.eql?(40)
+
+ 0.ord.should.eql?(0)
+ (-10).ord.should.eql?(-10)
+
+ ?a.ord.should.eql?(97)
+ ?Z.ord.should.eql?(90)
+
+ bignum_value.ord.should.eql?(bignum_value)
+ (-bignum_value).ord.should.eql?(-bignum_value)
+ end
+end
diff --git a/spec/ruby/core/integer/plus_spec.rb b/spec/ruby/core/integer/plus_spec.rb
new file mode 100644
index 0000000000..b684377bd5
--- /dev/null
+++ b/spec/ruby/core/integer/plus_spec.rb
@@ -0,0 +1,75 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_coerce'
+
+describe "Integer#+" do
+ it_behaves_like :integer_arithmetic_coerce_not_rescue, :+
+
+ context "fixnum" do
+ it "returns self plus the given Integer" do
+ (491 + 2).should == 493
+ (90210 + 10).should == 90220
+
+ (9 + bignum_value).should == 18446744073709551625
+ (1001 + 5.219).should == 1006.219
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10)
+ 13 + obj
+ }.should.raise(TypeError)
+ -> { 13 + "10" }.should.raise(TypeError)
+ -> { 13 + :symbol }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(76)
+ end
+
+ it "returns self plus the given Integer" do
+ (@bignum + 4).should == 18446744073709551696
+ (@bignum + 4.2).should be_close(18446744073709551696.2, TOLERANCE)
+ (@bignum + bignum_value(3)).should == 36893488147419103311
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> { @bignum + mock('10') }.should.raise(TypeError)
+ -> { @bignum + "10" }.should.raise(TypeError)
+ -> { @bignum + :symbol}.should.raise(TypeError)
+ end
+ end
+
+ it "can be redefined" do
+ code = <<~RUBY
+ class Integer
+ alias_method :old_plus, :+
+ def +(other)
+ self - other
+ end
+ end
+ result = 1 + 2
+ Integer.alias_method :+, :old_plus
+ print result
+ RUBY
+ ruby_exe(code).should == "-1"
+ end
+
+ it "coerces the RHS and calls #coerce" do
+ obj = mock("integer plus")
+ obj.should_receive(:coerce).with(6).and_return([6, 3])
+ (6 + obj).should == 9
+ end
+
+ it "coerces the RHS and calls #coerce even if it's private" do
+ obj = Object.new
+ class << obj
+ private def coerce(n)
+ [n, 3]
+ end
+ end
+
+ (6 + obj).should == 9
+ end
+end
diff --git a/spec/ruby/core/integer/pow_spec.rb b/spec/ruby/core/integer/pow_spec.rb
new file mode 100644
index 0000000000..12a3839c40
--- /dev/null
+++ b/spec/ruby/core/integer/pow_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/exponent'
+
+describe "Integer#pow" do
+ context "one argument is passed" do
+ it_behaves_like :integer_exponent, :pow
+ end
+
+ context "two arguments are passed" do
+ it "returns modulo of self raised to the given power" do
+ 2.pow(5, 12).should == 8
+ 2.pow(6, 13).should == 12
+ 2.pow(7, 14).should == 2
+ 2.pow(8, 15).should == 1
+ end
+
+ it "works well with bignums" do
+ 2.pow(61, 5843009213693951).should.eql? 3697379018277258
+ 2.pow(62, 5843009213693952).should.eql? 1551748822859776
+ 2.pow(63, 5843009213693953).should.eql? 3103497645717974
+ 2.pow(64, 5843009213693954).should.eql? 363986077738838
+ end
+
+ it "handles sign like #divmod does" do
+ 2.pow(5, 12).should == 8
+ 2.pow(5, -12).should == -4
+ -2.pow(5, 12).should == 4
+ -2.pow(5, -12).should == -8
+ end
+
+ it "ensures all arguments are integers" do
+ -> { 2.pow(5, 12.0) }.should.raise(TypeError, /2nd argument not allowed unless all arguments are integers/)
+ -> { 2.pow(5, Rational(12, 1)) }.should.raise(TypeError, /2nd argument not allowed unless all arguments are integers/)
+ end
+
+ it "raises TypeError for non-numeric value" do
+ -> { 2.pow(5, "12") }.should.raise(TypeError)
+ -> { 2.pow(5, []) }.should.raise(TypeError)
+ -> { 2.pow(5, nil) }.should.raise(TypeError)
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0" do
+ -> { 2.pow(5, 0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a RangeError when the first argument is negative and the second argument is present" do
+ -> { 2.pow(-5, 1) }.should.raise(RangeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/pred_spec.rb b/spec/ruby/core/integer/pred_spec.rb
new file mode 100644
index 0000000000..fce536b5a2
--- /dev/null
+++ b/spec/ruby/core/integer/pred_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Integer#pred" do
+ it "returns the Integer equal to self - 1" do
+ 0.pred.should.eql?(-1)
+ -1.pred.should.eql?(-2)
+ bignum_value.pred.should.eql?(bignum_value(-1))
+ fixnum_min.pred.should.eql?(fixnum_min - 1)
+ 20.pred.should.eql?(19)
+ end
+end
diff --git a/spec/ruby/core/integer/rationalize_spec.rb b/spec/ruby/core/integer/rationalize_spec.rb
new file mode 100644
index 0000000000..272eca6911
--- /dev/null
+++ b/spec/ruby/core/integer/rationalize_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "Integer#rationalize" do
+ before :all do
+ @numbers = [
+ 0,
+ 29871,
+ 99999999999999**99,
+ -72628191273,
+ ]
+ end
+
+ it "returns a Rational object" do
+ @numbers.each do |number|
+ number.rationalize.should.instance_of?(Rational)
+ end
+ end
+
+ it "uses self as the numerator" do
+ @numbers.each do |number|
+ number.rationalize.numerator.should == number
+ end
+ end
+
+ it "uses 1 as the denominator" do
+ @numbers.each do |number|
+ number.rationalize.denominator.should == 1
+ end
+ end
+
+ it "ignores a single argument" do
+ 1.rationalize(0.1).should == Rational(1,1)
+ end
+
+ it "raises ArgumentError when passed more than one argument" do
+ -> { 1.rationalize(0.1, 0.1) }.should.raise(ArgumentError)
+ -> { 1.rationalize(0.1, 0.1, 2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/integer/remainder_spec.rb b/spec/ruby/core/integer/remainder_spec.rb
new file mode 100644
index 0000000000..deb81fb2a5
--- /dev/null
+++ b/spec/ruby/core/integer/remainder_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+
+describe "Integer#remainder" do
+ context "fixnum" do
+ it "returns the remainder of dividing self by other" do
+ 5.remainder(3).should == 2
+ 5.remainder(3.0).should == 2.0
+ 5.remainder(Rational(3, 1)).should == Rational(2, 1)
+ end
+
+ it "means x-y*(x/y).truncate" do
+ 5.remainder(3).should == 2
+ 5.remainder(3.3).should be_close(1.7, TOLERANCE)
+ 5.remainder(3.7).should be_close(1.3, TOLERANCE)
+ end
+
+ it "keeps sign of self" do
+ 5.remainder( 3).should == 2
+ 5.remainder(-3).should == 2
+ -5.remainder( 3).should == -2
+ -5.remainder(-3).should == -2
+ end
+
+ it "raises TypeError if passed non-numeric argument" do
+ -> { 5.remainder("3") }.should.raise(TypeError)
+ -> { 5.remainder(:"3") }.should.raise(TypeError)
+ -> { 5.remainder([]) }.should.raise(TypeError)
+ -> { 5.remainder(nil) }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ it "returns the remainder of dividing self by other" do
+ a = bignum_value(79)
+ a.remainder(2).should == 1
+ a.remainder(97.345).should be_close(93.1349992295444, TOLERANCE)
+ a.remainder(bignum_value).should == 79
+ end
+
+ it "raises a ZeroDivisionError if other is zero and not a Float" do
+ -> { bignum_value(66).remainder(0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "does raises ZeroDivisionError if other is zero and a Float" do
+ a = bignum_value(7)
+ b = bignum_value(32)
+ -> { a.remainder(0.0) }.should.raise(ZeroDivisionError)
+ -> { b.remainder(-0.0) }.should.raise(ZeroDivisionError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/right_shift_spec.rb b/spec/ruby/core/integer/right_shift_spec.rb
new file mode 100644
index 0000000000..4281d3b7ab
--- /dev/null
+++ b/spec/ruby/core/integer/right_shift_spec.rb
@@ -0,0 +1,233 @@
+require_relative '../../spec_helper'
+
+describe "Integer#>> (with n >> m)" do
+ context "fixnum" do
+ it "returns n shifted right m bits when n > 0, m > 0" do
+ (2 >> 1).should == 1
+ end
+
+ it "returns n shifted right m bits when n < 0, m > 0" do
+ (-2 >> 1).should == -1
+ (-7 >> 1).should == -4
+ (-42 >> 2).should == -11
+ end
+
+ it "returns n shifted left m bits when n > 0, m < 0" do
+ (1 >> -1).should == 2
+ end
+
+ it "returns n shifted left m bits when n < 0, m < 0" do
+ (-1 >> -1).should == -2
+ end
+
+ it "returns 0 when n == 0" do
+ (0 >> 1).should == 0
+ end
+
+ it "returns n when n > 0, m == 0" do
+ (1 >> 0).should == 1
+ end
+
+ it "returns n when n < 0, m == 0" do
+ (-1 >> 0).should == -1
+ end
+
+ it "returns 0 when n > 0, m > 0 and n < 2**m" do
+ (3 >> 2).should == 0
+ (7 >> 3).should == 0
+ (127 >> 7).should == 0
+
+ # To make sure the exponent is not truncated
+ (7 >> 32).should == 0
+ (7 >> 64).should == 0
+ end
+
+ it "returns -1 when n < 0, m > 0 and n > -(2**m)" do
+ (-3 >> 2).should == -1
+ (-7 >> 3).should == -1
+ (-127 >> 7).should == -1
+
+ # To make sure the exponent is not truncated
+ (-7 >> 32).should == -1
+ (-7 >> 64).should == -1
+ end
+
+ it "returns a Bignum == fixnum_max * 2 when fixnum_max >> -1 and n > 0" do
+ result = fixnum_max >> -1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_max * 2
+ end
+
+ it "returns a Bignum == fixnum_min * 2 when fixnum_min >> -1 and n < 0" do
+ result = fixnum_min >> -1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_min * 2
+ end
+
+ it "calls #to_int to convert the argument to an Integer" do
+ obj = mock("2")
+ obj.should_receive(:to_int).and_return(2)
+ (8 >> obj).should == 2
+
+ obj = mock("to_int_bignum")
+ obj.should_receive(:to_int).and_return(bignum_value)
+ (8 >> obj).should == 0
+ end
+
+ it "raises a TypeError when #to_int does not return an Integer" do
+ obj = mock("a string")
+ obj.should_receive(:to_int).and_return("asdf")
+
+ -> { 3 >> obj }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { 3 >> nil }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { 3 >> "4" }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value * 8 # 2 ** 67
+ end
+
+ it "returns n shifted right m bits when n > 0, m > 0" do
+ (@bignum >> 1).should == 73786976294838206464
+ end
+
+ it "returns n shifted right m bits when n < 0, m > 0" do
+ (-@bignum >> 2).should == -36893488147419103232
+ end
+
+ it "respects twos complement signed shifting" do
+ # This explicit left hand value is important because it is the
+ # exact bit pattern that matters, so it's important it's right
+ # here to show the significance.
+ #
+
+ (-42949672980000000000000 >> 14).should == -2621440001220703125
+ (-42949672980000000000001 >> 14).should == -2621440001220703126
+ # Note the off by one -------------------- ^^^^^^^^^^^^^^^^^^^^
+ # This is because even though we discard the lowest bit, in twos
+ # complement it would influence the bits to the left of it.
+
+ (-42949672980000000000000 >> 15).should == -1310720000610351563
+ (-42949672980000000000001 >> 15).should == -1310720000610351563
+
+ (-0xfffffffffffffffff >> 32).should == -68719476736
+ end
+
+ it "respects twos complement signed shifting for very large values" do
+ giant = 42949672980000000000000000000000000000000000000000000000000000000000000000000000000000000000
+ neg = -giant
+
+ (giant >> 84).should == 2220446050284288846538547929770901490087453566957265138626098632812
+ (neg >> 84).should == -2220446050284288846538547929770901490087453566957265138626098632813
+ end
+
+ it "returns n shifted left m bits when n > 0, m < 0" do
+ (@bignum >> -2).should == 590295810358705651712
+ end
+
+ it "returns n shifted left m bits when n < 0, m < 0" do
+ (-@bignum >> -3).should == -1180591620717411303424
+ end
+
+ it "returns n when n > 0, m == 0" do
+ (@bignum >> 0).should == @bignum
+ end
+
+ it "returns n when n < 0, m == 0" do
+ (-@bignum >> 0).should == -@bignum
+ end
+
+ it "returns 0 when m > 0 and m == p where 2**p > n >= 2**(p-1)" do
+ (@bignum >> 68).should == 0
+ end
+
+ it "returns a Fixnum == fixnum_max when (fixnum_max * 2) >> 1 and n > 0" do
+ result = (fixnum_max * 2) >> 1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_max
+ end
+
+ it "returns a Fixnum == fixnum_min when (fixnum_min * 2) >> 1 and n < 0" do
+ result = (fixnum_min * 2) >> 1
+ result.should.instance_of?(Integer)
+ result.should == fixnum_min
+ end
+
+ it "calls #to_int to convert the argument to an Integer" do
+ obj = mock("2")
+ obj.should_receive(:to_int).and_return(2)
+
+ (@bignum >> obj).should == 36893488147419103232
+ end
+
+ it "raises a TypeError when #to_int does not return an Integer" do
+ obj = mock("a string")
+ obj.should_receive(:to_int).and_return("asdf")
+
+ -> { @bignum >> obj }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { @bignum >> nil }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { @bignum >> "4" }.should.raise(TypeError)
+ end
+ end
+
+ context "when m is a bignum or larger than int" do
+ it "returns -1 when m > 0 and n < 0" do
+ (-1 >> bignum_value).should == -1
+ (-1 >> (2**40)).should == -1
+
+ (-bignum_value >> bignum_value).should == -1
+ (-bignum_value >> (2**40)).should == -1
+ end
+
+ it "returns 0 when m > 0 and n >= 0" do
+ (0 >> bignum_value).should == 0
+ (1 >> bignum_value).should == 0
+ (bignum_value >> bignum_value).should == 0
+
+ (0 >> (2**40)).should == 0
+ (1 >> (2**40)).should == 0
+ (bignum_value >> (2**40)).should == 0
+ end
+
+ it "returns 0 when m < 0 long and n == 0" do
+ (0 >> -(2**40)).should == 0
+ end
+
+ it "returns 0 when m < 0 bignum and n == 0" do
+ (0 >> -bignum_value).should == 0
+ end
+
+ it "raises RangeError when m < 0 and n != 0" do
+ # https://bugs.ruby-lang.org/issues/18518#note-9
+ limit = RUBY_ENGINE == 'ruby' ? 2**67 : 2**32
+
+ coerce_long = mock("long")
+ coerce_long.stub!(:to_int).and_return(-limit)
+ coerce_bignum = mock("bignum")
+ coerce_bignum.stub!(:to_int).and_return(-bignum_value)
+ exps = [-limit, coerce_long]
+ exps << -bignum_value << coerce_bignum if bignum_value >= limit
+
+ exps.each { |exp|
+ -> { (1 >> exp) }.should.raise(RangeError, 'shift width too big')
+ -> { (-1 >> exp) }.should.raise(RangeError, 'shift width too big')
+ -> { (bignum_value >> exp) }.should.raise(RangeError, 'shift width too big')
+ -> { (-bignum_value >> exp) }.should.raise(RangeError, 'shift width too big')
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/round_spec.rb b/spec/ruby/core/integer/round_spec.rb
new file mode 100644
index 0000000000..a3f11e7a76
--- /dev/null
+++ b/spec/ruby/core/integer/round_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+require_relative 'shared/integer_rounding'
+
+describe "Integer#round" do
+ it_behaves_like :integer_to_i, :round
+ it_behaves_like :integer_rounding_positive_precision, :round
+
+ # redmine:5228
+ it "returns itself rounded if passed a negative value" do
+ +249.round(-2).should.eql?(+200)
+ -249.round(-2).should.eql?(-200)
+ (+25 * 10**70 - 1).round(-71).should.eql?(+20 * 10**70)
+ (-25 * 10**70 + 1).round(-71).should.eql?(-20 * 10**70)
+ end
+
+ it "returns itself rounded to nearest if passed a negative value" do
+ +250.round(-2).should.eql?(+300)
+ -250.round(-2).should.eql?(-300)
+ (+25 * 10**70).round(-71).should.eql?(+30 * 10**70)
+ (-25 * 10**70).round(-71).should.eql?(-30 * 10**70)
+ end
+
+ it "raises a RangeError when passed a big negative value" do
+ -> { 42.round(min_long - 1) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError when passed Float::INFINITY" do
+ -> { 42.round(Float::INFINITY) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError when passed a beyond signed int" do
+ -> { 42.round(1<<31) }.should.raise(RangeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { 42.round("4") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when its argument cannot be converted to an Integer" do
+ -> { 42.round(nil) }.should.raise(TypeError)
+ end
+
+ it "calls #to_int on the argument to convert it to an Integer" do
+ obj = mock("Object")
+ obj.should_receive(:to_int).and_return(0)
+ 42.round(obj)
+ end
+
+ it "raises a TypeError when #to_int does not return an Integer" do
+ obj = mock("Object")
+ obj.stub!(:to_int).and_return([])
+ -> { 42.round(obj) }.should.raise(TypeError)
+ end
+
+ it "returns different rounded values depending on the half option" do
+ 25.round(-1, half: :up).should.eql?(30)
+ 25.round(-1, half: :down).should.eql?(20)
+ 25.round(-1, half: :even).should.eql?(20)
+ 25.round(-1, half: nil).should.eql?(30)
+ 35.round(-1, half: :up).should.eql?(40)
+ 35.round(-1, half: :down).should.eql?(30)
+ 35.round(-1, half: :even).should.eql?(40)
+ 35.round(-1, half: nil).should.eql?(40)
+ (-25).round(-1, half: :up).should.eql?(-30)
+ (-25).round(-1, half: :down).should.eql?(-20)
+ (-25).round(-1, half: :even).should.eql?(-20)
+ (-25).round(-1, half: nil).should.eql?(-30)
+ end
+
+ it "returns itself if passed a positive precision and the half option" do
+ 35.round(1, half: :up).should.eql?(35)
+ 35.round(1, half: :down).should.eql?(35)
+ 35.round(1, half: :even).should.eql?(35)
+ end
+
+ it "raises ArgumentError for an unknown rounding mode" do
+ -> { 42.round(-1, half: :foo) }.should.raise(ArgumentError, /invalid rounding mode: foo/)
+ -> { 42.round(1, half: :foo) }.should.raise(ArgumentError, /invalid rounding mode: foo/)
+ end
+end
diff --git a/spec/ruby/core/integer/shared/abs.rb b/spec/ruby/core/integer/shared/abs.rb
new file mode 100644
index 0000000000..43844c9065
--- /dev/null
+++ b/spec/ruby/core/integer/shared/abs.rb
@@ -0,0 +1,18 @@
+describe :integer_abs, shared: true do
+ context "fixnum" do
+ it "returns self's absolute fixnum value" do
+ { 0 => [0, -0, +0], 2 => [2, -2, +2], 100 => [100, -100, +100] }.each do |key, values|
+ values.each do |value|
+ value.send(@method).should == key
+ end
+ end
+ end
+ end
+
+ context "bignum" do
+ it "returns the absolute bignum value" do
+ bignum_value(39).send(@method).should == 18446744073709551655
+ (-bignum_value(18)).send(@method).should == 18446744073709551634
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/arithmetic_coerce.rb b/spec/ruby/core/integer/shared/arithmetic_coerce.rb
new file mode 100644
index 0000000000..561b18fe52
--- /dev/null
+++ b/spec/ruby/core/integer/shared/arithmetic_coerce.rb
@@ -0,0 +1,11 @@
+require_relative '../fixtures/classes'
+
+describe :integer_arithmetic_coerce_not_rescue, shared: true do
+ it "does not rescue exception raised in other#coerce" do
+ b = mock("numeric with failed #coerce")
+ b.should_receive(:coerce).and_raise(IntegerSpecs::CoerceError)
+
+ # e.g. 1 + b
+ -> { 1.send(@method, b) }.should.raise(IntegerSpecs::CoerceError)
+ end
+end
diff --git a/spec/ruby/core/integer/shared/comparison_coerce.rb b/spec/ruby/core/integer/shared/comparison_coerce.rb
new file mode 100644
index 0000000000..4bb7404183
--- /dev/null
+++ b/spec/ruby/core/integer/shared/comparison_coerce.rb
@@ -0,0 +1,11 @@
+require_relative '../fixtures/classes'
+
+describe :integer_comparison_coerce_not_rescue, shared: true do
+ it "does not rescue exception raised in other#coerce" do
+ b = mock("numeric with failed #coerce")
+ b.should_receive(:coerce).and_raise(IntegerSpecs::CoerceError)
+
+ # e.g. 1 > b
+ -> { 1.send(@method, b) }.should.raise(IntegerSpecs::CoerceError)
+ end
+end
diff --git a/spec/ruby/core/integer/shared/equal.rb b/spec/ruby/core/integer/shared/equal.rb
new file mode 100644
index 0000000000..c621ba3f81
--- /dev/null
+++ b/spec/ruby/core/integer/shared/equal.rb
@@ -0,0 +1,63 @@
+describe :integer_equal, shared: true do
+ context "fixnum" do
+ it "returns true if self has the same value as other" do
+ 1.send(@method, 1).should == true
+ 9.send(@method, 5).should == false
+
+ # Actually, these call Float#==, Integer#== etc.
+ 9.send(@method, 9.0).should == true
+ 9.send(@method, 9.01).should == false
+
+ 10.send(@method, bignum_value).should == false
+ end
+
+ it "calls 'other == self' if the given argument is not an Integer" do
+ 1.send(@method, '*').should == false
+
+ obj = mock('one other')
+ obj.should_receive(:==).any_number_of_times.and_return(false)
+ 1.send(@method, obj).should == false
+
+ obj = mock('another')
+ obj.should_receive(:==).any_number_of_times.and_return(true)
+ 2.send(@method, obj).should == true
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value
+ end
+
+ it "returns true if self has the same value as the given argument" do
+ @bignum.send(@method, @bignum).should == true
+ @bignum.send(@method, @bignum.to_f).should == true
+
+ @bignum.send(@method, @bignum + 1).should == false
+ (@bignum + 1).send(@method, @bignum).should == false
+
+ @bignum.send(@method, 9).should == false
+ @bignum.send(@method, 9.01).should == false
+
+ @bignum.send(@method, bignum_value(10)).should == false
+ end
+
+ it "calls 'other == self' if the given argument is not an Integer" do
+ obj = mock('not integer')
+ obj.should_receive(:==).and_return(true)
+ @bignum.send(@method, obj).should == true
+ end
+
+ it "returns the result of 'other == self' as a boolean" do
+ obj = mock('not integer')
+ obj.should_receive(:==).exactly(2).times.and_return("woot", nil)
+ @bignum.send(@method, obj).should == true
+ @bignum.send(@method, obj).should == false
+ end
+
+ it "does not lose precision when comparing with a Float" do
+ (bignum_value(1).send(@method, bignum_value.to_f)).should == false
+ (bignum_value.send(@method, bignum_value.to_f)).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/exponent.rb b/spec/ruby/core/integer/shared/exponent.rb
new file mode 100644
index 0000000000..0be13859f9
--- /dev/null
+++ b/spec/ruby/core/integer/shared/exponent.rb
@@ -0,0 +1,162 @@
+describe :integer_exponent, shared: true do
+ context "fixnum" do
+ it "returns self raised to the given power" do
+ 2.send(@method, 0).should.eql? 1
+ 2.send(@method, 1).should.eql? 2
+ 2.send(@method, 2).should.eql? 4
+
+ 9.send(@method, 0.5).should.eql? 3.0
+ 9.send(@method, Rational(1, 2)).should.eql? 3.0
+ 5.send(@method, -1).to_f.to_s.should == '0.2'
+
+ 2.send(@method, 40).should.eql? 1099511627776
+ end
+
+ it "overflows the answer to a bignum transparently" do
+ 2.send(@method, 29).should.eql? 536870912
+ 2.send(@method, 30).should.eql? 1073741824
+ 2.send(@method, 31).should.eql? 2147483648
+ 2.send(@method, 32).should.eql? 4294967296
+
+ 2.send(@method, 61).should.eql? 2305843009213693952
+ 2.send(@method, 62).should.eql? 4611686018427387904
+ 2.send(@method, 63).should.eql? 9223372036854775808
+ 2.send(@method, 64).should.eql? 18446744073709551616
+ 8.send(@method, 23).should.eql? 590295810358705651712
+ end
+
+ it "raises negative numbers to the given power" do
+ (-2).send(@method, 29).should.eql?(-536870912)
+ (-2).send(@method, 30).should.eql?(1073741824)
+ (-2).send(@method, 31).should.eql?(-2147483648)
+ (-2).send(@method, 32).should.eql?(4294967296)
+ (-2).send(@method, 33).should.eql?(-8589934592)
+
+ (-2).send(@method, 61).should.eql?(-2305843009213693952)
+ (-2).send(@method, 62).should.eql?(4611686018427387904)
+ (-2).send(@method, 63).should.eql?(-9223372036854775808)
+ (-2).send(@method, 64).should.eql?(18446744073709551616)
+ (-2).send(@method, 65).should.eql?(-36893488147419103232)
+ end
+
+ it "can raise 1 to a bignum safely" do
+ 1.send(@method, 4611686018427387904).should.eql? 1
+ end
+
+ it "can raise -1 to a bignum safely" do
+ (-1).send(@method, 4611686018427387904).should.eql?(1)
+ (-1).send(@method, 4611686018427387905).should.eql?(-1)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns Float::INFINITY when the number is too big" do
+ -> {
+ 2.send(@method, 427387904).should == Float::INFINITY
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "returns an Integer for results larger than the old 32MB limit" do
+ # 2 ** 40000000 requires 40000001 bits
+ # This exceeds the old 32MB limit but is within the new 16GB limit
+ result = 2.send(@method, 40000000)
+ result.should.is_a?(Integer)
+ result.bit_length.should == 40000001
+ end
+
+ it "raises an ArgumentError when the result size exceeds the limit" do
+ -> { 100000000.send(@method, 1000000000) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "raises a ZeroDivisionError for 0 ** -1" do
+ -> { 0.send(@method, -1) }.should.raise(ZeroDivisionError)
+ -> { 0.send(@method, Rational(-1, 1)) }.should.raise(ZeroDivisionError)
+ end
+
+ it "returns Float::INFINITY for 0 ** -1.0" do
+ 0.send(@method, -1.0).should == Float::INFINITY
+ end
+
+ it "raises a TypeError when given a non-numeric power" do
+ -> { 13.send(@method, "10") }.should.raise(TypeError)
+ -> { 13.send(@method, :symbol) }.should.raise(TypeError)
+ -> { 13.send(@method, nil) }.should.raise(TypeError)
+ end
+
+ it "coerces power and calls #**" do
+ num_2 = mock("2")
+ num_13 = mock("13")
+ num_2.should_receive(:coerce).with(13).and_return([num_13, num_2])
+ num_13.should_receive(:**).with(num_2).and_return(169)
+
+ 13.send(@method, num_2).should == 169
+ end
+
+ it "returns Float when power is Float" do
+ 2.send(@method, 2.0).should == 4.0
+ end
+
+ it "returns Rational when power is Rational" do
+ 2.send(@method, Rational(2, 1)).should == Rational(4, 1)
+ end
+
+ it "returns a complex number when negative and raised to a fractional power" do
+ (-8).send(@method, 1.0/3) .should be_close(Complex(1, 1.73205), TOLERANCE)
+ (-8).send(@method, Rational(1, 3)).should be_close(Complex(1, 1.73205), TOLERANCE)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(47)
+ end
+
+ it "returns self raised to other power" do
+ (@bignum.send(@method, 4)).should == 115792089237316196603666111261383895964500887398800252671052694326794607293761
+ (@bignum.send(@method, 1.3)).should be_close(11109528802438156839288832.0, TOLERANCE)
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> { @bignum.send(@method, mock('10')) }.should.raise(TypeError)
+ -> { @bignum.send(@method, "10") }.should.raise(TypeError)
+ -> { @bignum.send(@method, :symbol) }.should.raise(TypeError)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "switch to a Float when the values is too big" do
+ flt = nil
+ -> {
+ flt = @bignum.send(@method, @bignum)
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ flt.should.is_a?(Float)
+ flt.infinite?.should == 1
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "returns an Integer for large Bignum results exceeding the old limit" do
+ # (2 ** 70) ** 500000 requires 35000001 bits
+ # This exceeds the old 32MB limit but is within the new 16GB limit
+ bignum_base = 2 ** 70
+ result = bignum_base.send(@method, 500000)
+ result.should.is_a?(Integer)
+ result.bit_length.should == 35000001
+ end
+
+ it "raises an ArgumentError when Bignum result exceeds the limit" do
+ # @bignum ** @bignum would require enormous memory
+ -> {
+ @bignum.send(@method, @bignum)
+ }.should.raise(ArgumentError)
+ end
+ end
+
+ it "returns a complex number when negative and raised to a fractional power" do
+ (-bignum_value).send(@method, (1.0/2)).should be_close(Complex(0.0, 4294967296.0), TOLERANCE)
+ (-@bignum).send(@method, (1.0/3)) .should be_close(Complex(1321122.9748145656, 2288252.1154253655), TOLERANCE)
+ (-@bignum).send(@method, Rational(1,3)).should be_close(Complex(1321122.9748145656, 2288252.1154253655), TOLERANCE)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/integer_ceil_precision.rb b/spec/ruby/core/integer/shared/integer_ceil_precision.rb
new file mode 100644
index 0000000000..b23c17937f
--- /dev/null
+++ b/spec/ruby/core/integer/shared/integer_ceil_precision.rb
@@ -0,0 +1,54 @@
+describe :integer_ceil_precision, shared: true do
+ context "precision is zero" do
+ it "returns Integer equal to self" do
+ send(@method, 0).ceil(0).should.eql?(0)
+ send(@method, 123).ceil(0).should.eql?(123)
+ send(@method, -123).ceil(0).should.eql?(-123)
+ end
+ end
+
+ context "precision is positive" do
+ it "returns self" do
+ send(@method, 0).ceil(1).should.eql?(send(@method, 0))
+ send(@method, 0).ceil(10).should.eql?(send(@method, 0))
+
+ send(@method, 123).ceil(10).should.eql?(send(@method, 123))
+ send(@method, -123).ceil(10).should.eql?(send(@method, -123))
+ end
+ end
+
+ context "precision is negative" do
+ it "always returns 0 when self is 0" do
+ send(@method, 0).ceil(-1).should.eql?(0)
+ send(@method, 0).ceil(-10).should.eql?(0)
+ end
+
+ it "returns Integer equal to self if there are already at least precision.abs trailing zeros" do
+ send(@method, 10).ceil(-1).should.eql?(10)
+ send(@method, 100).ceil(-1).should.eql?(100)
+ send(@method, 100).ceil(-2).should.eql?(100)
+ send(@method, -10).ceil(-1).should.eql?(-10)
+ send(@method, -100).ceil(-1).should.eql?(-100)
+ send(@method, -100).ceil(-2).should.eql?(-100)
+ end
+
+ it "returns smallest Integer greater than self with at least precision.abs trailing zeros" do
+ send(@method, 123).ceil(-1).should.eql?(130)
+ send(@method, 123).ceil(-2).should.eql?(200)
+ send(@method, 123).ceil(-3).should.eql?(1000)
+
+ send(@method, -123).ceil(-1).should.eql?(-120)
+ send(@method, -123).ceil(-2).should.eql?(-100)
+ send(@method, -123).ceil(-3).should.eql?(0)
+
+ send(@method, 100).ceil(-3).should.eql?(1000)
+ send(@method, -100).ceil(-3).should.eql?(0)
+ end
+
+ # Bug #20654
+ it "returns 10**precision.abs when precision.abs has more digits than self" do
+ send(@method, 123).ceil(-20).should.eql?(100000000000000000000)
+ send(@method, 123).ceil(-50).should.eql?(100000000000000000000000000000000000000000000000000)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/integer_floor_precision.rb b/spec/ruby/core/integer/shared/integer_floor_precision.rb
new file mode 100644
index 0000000000..6247907d4c
--- /dev/null
+++ b/spec/ruby/core/integer/shared/integer_floor_precision.rb
@@ -0,0 +1,42 @@
+describe :integer_floor_precision, shared: true do
+ context "precision is zero" do
+ it "returns integer self" do
+ send(@method, 0).floor(0).should.eql?(0)
+ send(@method, 123).floor(0).should.eql?(123)
+ send(@method, -123).floor(0).should.eql?(-123)
+ end
+ end
+
+ context "precision is positive" do
+ it "returns self" do
+ send(@method, 0).floor(1).should.eql?(send(@method, 0))
+ send(@method, 0).floor(10).should.eql?(send(@method, 0))
+
+ send(@method, 123).floor(10).should.eql?(send(@method, 123))
+ send(@method, -123).floor(10).should.eql?(send(@method, -123))
+ end
+ end
+
+ context "precision is negative" do
+ it "always returns 0 when self is 0" do
+ send(@method, 0).floor(-1).should.eql?(0)
+ send(@method, 0).floor(-10).should.eql?(0)
+ end
+
+ it "returns largest integer less than self with at least precision.abs trailing zeros" do
+ send(@method, 123).floor(-1).should.eql?(120)
+ send(@method, 123).floor(-2).should.eql?(100)
+ send(@method, 123).floor(-3).should.eql?(0)
+
+ send(@method, -123).floor(-1).should.eql?(-130)
+ send(@method, -123).floor(-2).should.eql?(-200)
+ send(@method, -123).floor(-3).should.eql?(-1000)
+ end
+
+ # Bug #20654
+ it "returns -(10**precision.abs) when self is negative and precision.abs is larger than the number digits of self" do
+ send(@method, -123).floor(-20).should.eql?(-100000000000000000000)
+ send(@method, -123).floor(-50).should.eql?(-100000000000000000000000000000000000000000000000000)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/integer_rounding.rb b/spec/ruby/core/integer/shared/integer_rounding.rb
new file mode 100644
index 0000000000..92f2a2327c
--- /dev/null
+++ b/spec/ruby/core/integer/shared/integer_rounding.rb
@@ -0,0 +1,19 @@
+describe :integer_rounding_positive_precision, shared: true do
+ it "returns self if not passed a precision" do
+ [2, -4, 10**70, -10**100].each do |v|
+ v.send(@method).should.eql?(v)
+ end
+ end
+
+ it "returns self if passed a precision of zero" do
+ [2, -4, 10**70, -10**100].each do |v|
+ v.send(@method, 0).should.eql?(v)
+ end
+ end
+
+ it "returns itself if passed a positive precision" do
+ [2, -4, 10**70, -10**100].each do |v|
+ v.send(@method, 42).should.eql?(v)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/modulo.rb b/spec/ruby/core/integer/shared/modulo.rb
new file mode 100644
index 0000000000..d0b5e26ed5
--- /dev/null
+++ b/spec/ruby/core/integer/shared/modulo.rb
@@ -0,0 +1,114 @@
+describe :integer_modulo, shared: true do
+ context "fixnum" do
+ it "returns the modulus obtained from dividing self by the given argument" do
+ # test all possible combinations:
+ # - integer/double/bignum argument
+ # - positive/negative argument
+ # - positive/negative self
+ # - self greater/smaller than argument
+
+ 13.send(@method, 4).should == 1
+ 4.send(@method, 13).should == 4
+
+ 13.send(@method, 4.0).should == 1
+ 4.send(@method, 13.0).should == 4
+
+ (-200).send(@method, 256).should == 56
+ (-1000).send(@method, 512).should == 24
+
+ (-200).send(@method, -256).should == -200
+ (-1000).send(@method, -512).should == -488
+
+ (200).send(@method, -256).should == -56
+ (1000).send(@method, -512).should == -24
+
+ 13.send(@method, -4.0).should == -3.0
+ 4.send(@method, -13.0).should == -9.0
+
+ -13.send(@method, -4.0).should == -1.0
+ -4.send(@method, -13.0).should == -4.0
+
+ -13.send(@method, 4.0).should == 3.0
+ -4.send(@method, 13.0).should == 9.0
+
+ 1.send(@method, 2.0).should == 1.0
+ 200.send(@method, bignum_value).should == 200
+
+ 4.send(@method, bignum_value(10)).should == 4
+ 4.send(@method, -bignum_value(10)).should == -18446744073709551622
+ -4.send(@method, bignum_value(10)).should == 18446744073709551622
+ -4.send(@method, -bignum_value(10)).should == -4
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0" do
+ -> { 13.send(@method, 0) }.should.raise(ZeroDivisionError)
+ -> { 0.send(@method, 0) }.should.raise(ZeroDivisionError)
+ -> { -10.send(@method, 0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0 and a Float" do
+ -> { 0.send(@method, 0.0) }.should.raise(ZeroDivisionError)
+ -> { 10.send(@method, 0.0) }.should.raise(ZeroDivisionError)
+ -> { -10.send(@method, 0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('10')).should_receive(:to_int).any_number_of_times.and_return(10)
+ 13.send(@method, obj)
+ }.should.raise(TypeError)
+ -> { 13.send(@method, "10") }.should.raise(TypeError)
+ -> { 13.send(@method, :symbol) }.should.raise(TypeError)
+ end
+ end
+
+ context "bignum" do
+ before :each do
+ @bignum = bignum_value(10)
+ end
+
+ it "returns the modulus obtained from dividing self by the given argument" do
+ # test all possible combinations:
+ # - integer/double/bignum argument
+ # - positive/negative argument
+ # - positive/negative self
+ # - self greater/smaller than argument
+
+ @bignum.send(@method, 5).should == 1
+ @bignum.send(@method, -5).should == -4
+ (-@bignum).send(@method, 5).should == 4
+ (-@bignum).send(@method, -5).should == -1
+
+ @bignum.send(@method, 2.22).should be_close(1.5603603603605034, TOLERANCE)
+ @bignum.send(@method, -2.22).should be_close(-0.6596396396394968, TOLERANCE)
+ (-@bignum).send(@method, 2.22).should be_close(0.6596396396394968, TOLERANCE)
+ (-@bignum).send(@method, -2.22).should be_close(-1.5603603603605034, TOLERANCE)
+
+ @bignum.send(@method, @bignum + 10).should == 18446744073709551626
+ @bignum.send(@method, -(@bignum + 10)).should == -10
+ (-@bignum).send(@method, @bignum + 10).should == 10
+ (-@bignum).send(@method, -(@bignum + 10)).should == -18446744073709551626
+
+ (@bignum + 10).send(@method, @bignum).should == 10
+ (@bignum + 10).send(@method, -@bignum).should == -18446744073709551616
+ (-(@bignum + 10)).send(@method, @bignum).should == 18446744073709551616
+ (-(@bignum + 10)).send(@method, -@bignum).should == -10
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0" do
+ -> { @bignum.send(@method, 0) }.should.raise(ZeroDivisionError)
+ -> { (-@bignum).send(@method, 0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a ZeroDivisionError when the given argument is 0 and a Float" do
+ -> { @bignum.send(@method, 0.0) }.should.raise(ZeroDivisionError)
+ -> { -@bignum.send(@method, 0.0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> { @bignum.send(@method, mock('10')) }.should.raise(TypeError)
+ -> { @bignum.send(@method, "10") }.should.raise(TypeError)
+ -> { @bignum.send(@method, :symbol) }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/shared/next.rb b/spec/ruby/core/integer/shared/next.rb
new file mode 100644
index 0000000000..85b83d6965
--- /dev/null
+++ b/spec/ruby/core/integer/shared/next.rb
@@ -0,0 +1,25 @@
+describe :integer_next, shared: true do
+ it "returns the next larger positive Fixnum" do
+ 2.send(@method).should == 3
+ end
+
+ it "returns the next larger negative Fixnum" do
+ (-2).send(@method).should == -1
+ end
+
+ it "returns the next larger positive Bignum" do
+ bignum_value.send(@method).should == bignum_value(1)
+ end
+
+ it "returns the next larger negative Bignum" do
+ (-bignum_value(1)).send(@method).should == -bignum_value
+ end
+
+ it "overflows a Fixnum to a Bignum" do
+ fixnum_max.send(@method).should == fixnum_max + 1
+ end
+
+ it "underflows a Bignum to a Fixnum" do
+ (fixnum_min - 1).send(@method).should == fixnum_min
+ end
+end
diff --git a/spec/ruby/core/integer/shared/to_i.rb b/spec/ruby/core/integer/shared/to_i.rb
new file mode 100644
index 0000000000..2b6a50484a
--- /dev/null
+++ b/spec/ruby/core/integer/shared/to_i.rb
@@ -0,0 +1,8 @@
+describe :integer_to_i, shared: true do
+ it "returns self" do
+ 10.send(@method).should.eql?(10)
+ (-15).send(@method).should.eql?(-15)
+ bignum_value.send(@method).should.eql?(bignum_value)
+ (-bignum_value).send(@method).should.eql?(-bignum_value)
+ end
+end
diff --git a/spec/ruby/core/integer/size_spec.rb b/spec/ruby/core/integer/size_spec.rb
new file mode 100644
index 0000000000..725e9eb062
--- /dev/null
+++ b/spec/ruby/core/integer/size_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+
+describe "Integer#size" do
+ platform_is c_long_size: 32 do
+ it "returns the number of bytes in the machine representation of self" do
+ -1.size.should == 4
+ 0.size.should == 4
+ 4091.size.should == 4
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ it "returns the number of bytes in the machine representation of self" do
+ -1.size.should == 8
+ 0.size.should == 8
+ 4091.size.should == 8
+ end
+ end
+
+ context "bignum" do
+ it "returns the number of bytes required to hold the unsigned bignum data" do
+ # that is, n such that 256 * n <= val.abs < 256 * (n+1)
+ (256**7).size.should == 8
+ (256**8).size.should == 9
+ (256**9).size.should == 10
+ (256**10).size.should == 11
+ (256**10-1).size.should == 10
+ (256**11).size.should == 12
+ (256**12).size.should == 13
+ (256**20-1).size.should == 20
+ (256**40-1).size.should == 40
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/sqrt_spec.rb b/spec/ruby/core/integer/sqrt_spec.rb
new file mode 100644
index 0000000000..bf90ea747e
--- /dev/null
+++ b/spec/ruby/core/integer/sqrt_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Integer.sqrt" do
+ it "returns an integer" do
+ Integer.sqrt(10).should.is_a?(Integer)
+ end
+
+ it "returns the integer square root of the argument" do
+ Integer.sqrt(0).should == 0
+ Integer.sqrt(1).should == 1
+ Integer.sqrt(24).should == 4
+ Integer.sqrt(25).should == 5
+ Integer.sqrt(10**400).should == 10**200
+ end
+
+ it "raises a Math::DomainError if the argument is negative" do
+ -> { Integer.sqrt(-4) }.should.raise(Math::DomainError)
+ end
+
+ it "accepts any argument that can be coerced to Integer" do
+ Integer.sqrt(10.0).should == 3
+ end
+
+ it "converts the argument with #to_int" do
+ Integer.sqrt(mock_int(10)).should == 3
+ end
+
+ it "raises a TypeError if the argument cannot be coerced to Integer" do
+ -> { Integer.sqrt("test") }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/integer/succ_spec.rb b/spec/ruby/core/integer/succ_spec.rb
new file mode 100644
index 0000000000..9ae9a14fe7
--- /dev/null
+++ b/spec/ruby/core/integer/succ_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/next'
+
+describe "Integer#succ" do
+ it_behaves_like :integer_next, :succ
+end
diff --git a/spec/ruby/core/integer/times_spec.rb b/spec/ruby/core/integer/times_spec.rb
new file mode 100644
index 0000000000..356340592b
--- /dev/null
+++ b/spec/ruby/core/integer/times_spec.rb
@@ -0,0 +1,79 @@
+require_relative '../../spec_helper'
+
+describe "Integer#times" do
+ it "returns self" do
+ 5.times {}.should == 5
+ 9.times {}.should == 9
+ 9.times { |n| n - 2 }.should == 9
+ end
+
+ it "yields each value from 0 to self - 1" do
+ a = []
+ 9.times { |i| a << i }
+ -2.times { |i| a << i }
+ a.should == [0, 1, 2, 3, 4, 5, 6, 7, 8]
+ end
+
+ it "skips the current iteration when encountering 'next'" do
+ a = []
+ 3.times do |i|
+ next if i == 1
+ a << i
+ end
+ a.should == [0, 2]
+ end
+
+ it "skips all iterations when encountering 'break'" do
+ a = []
+ 5.times do |i|
+ break if i == 3
+ a << i
+ end
+ a.should == [0, 1, 2]
+ end
+
+ it "skips all iterations when encountering break with an argument and returns that argument" do
+ 9.times { break 2 }.should == 2
+ end
+
+ it "executes a nested while loop containing a break expression" do
+ a = [false]
+ b = 1.times do |i|
+ while true
+ a.shift or break
+ end
+ end
+ a.should == []
+ b.should == 1
+ end
+
+ it "executes a nested #times" do
+ a = 0
+ b = 3.times do |i|
+ 2.times { a += 1 }
+ end
+ a.should == 6
+ b.should == 3
+ end
+
+ it "returns an Enumerator" do
+ result = []
+
+ enum = 3.times
+ enum.each { |i| result << i }
+
+ result.should == [0, 1, 2]
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns self" do
+ 5.times.size.should == 5
+ 10.times.size.should == 10
+ 0.times.size.should == 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/to_f_spec.rb b/spec/ruby/core/integer/to_f_spec.rb
new file mode 100644
index 0000000000..0681236095
--- /dev/null
+++ b/spec/ruby/core/integer/to_f_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Integer#to_f" do
+ context "fixnum" do
+ it "returns self converted to a Float" do
+ 0.to_f.should == 0.0
+ -500.to_f.should == -500.0
+ 9_641_278.to_f.should == 9641278.0
+ end
+ end
+
+ context "bignum" do
+ it "returns self converted to a Float" do
+ bignum_value(0x4000_0aa0_0bb0_0000).to_f.should.eql?(23_058_441_774_644_068_352.0)
+ bignum_value(0x8000_0000_0000_0ccc).to_f.should.eql?(27_670_116_110_564_330_700.0)
+ (-bignum_value(99)).to_f.should.eql?(-18_446_744_073_709_551_715.0)
+ end
+
+ it "converts number close to Float::MAX without exceeding MAX or producing NaN" do
+ (10**308).to_f.should == 10.0 ** 308
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/to_i_spec.rb b/spec/ruby/core/integer/to_i_spec.rb
new file mode 100644
index 0000000000..1a8e477b3c
--- /dev/null
+++ b/spec/ruby/core/integer/to_i_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+
+describe "Integer#to_i" do
+ it_behaves_like :integer_to_i, :to_i
+end
diff --git a/spec/ruby/core/integer/to_int_spec.rb b/spec/ruby/core/integer/to_int_spec.rb
new file mode 100644
index 0000000000..4b7eccad3a
--- /dev/null
+++ b/spec/ruby/core/integer/to_int_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+
+describe "Integer#to_int" do
+ it_behaves_like :integer_to_i, :to_int
+end
diff --git a/spec/ruby/core/integer/to_r_spec.rb b/spec/ruby/core/integer/to_r_spec.rb
new file mode 100644
index 0000000000..2649c7c78d
--- /dev/null
+++ b/spec/ruby/core/integer/to_r_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Integer#to_r" do
+ it "returns a Rational object" do
+ 309.to_r.should.instance_of?(Rational)
+ end
+
+ it "constructs a rational number with self as the numerator" do
+ 34.to_r.numerator.should == 34
+ end
+
+ it "constructs a rational number with 1 as the denominator" do
+ 298.to_r.denominator.should == 1
+ end
+
+ it "works even if self is a Bignum" do
+ bignum = 99999**999
+ bignum.should.instance_of?(Integer)
+ bignum.to_r.should == Rational(bignum, 1)
+ end
+
+ it "raises an ArgumentError if given any arguments" do
+ -> { 287.to_r(2) }.should.raise(ArgumentError)
+ -> { 9102826.to_r(309, [], 71) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/integer/to_s_spec.rb b/spec/ruby/core/integer/to_s_spec.rb
new file mode 100644
index 0000000000..4970a2a12f
--- /dev/null
+++ b/spec/ruby/core/integer/to_s_spec.rb
@@ -0,0 +1,95 @@
+require_relative '../../spec_helper'
+
+describe "Integer#to_s" do
+ context "fixnum" do
+ context "when given a base" do
+ it "returns self converted to a String in the given base" do
+ 12345.to_s(2).should == "11000000111001"
+ 12345.to_s(8).should == "30071"
+ 12345.to_s(10).should == "12345"
+ 12345.to_s(16).should == "3039"
+ 95.to_s(16).should == "5f"
+ 12345.to_s(36).should == "9ix"
+ end
+
+ it "raises an ArgumentError if the base is less than 2 or higher than 36" do
+ -> { 123.to_s(-1) }.should.raise(ArgumentError)
+ -> { 123.to_s(0) }.should.raise(ArgumentError)
+ -> { 123.to_s(1) }.should.raise(ArgumentError)
+ -> { 123.to_s(37) }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when no base given" do
+ it "returns self converted to a String using base 10" do
+ 255.to_s.should == '255'
+ 3.to_s.should == '3'
+ 0.to_s.should == '0'
+ -9002.to_s.should == '-9002'
+ end
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ 1.to_s.encoding.should.equal?(Encoding::US_ASCII)
+ end
+
+ it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do
+ Encoding.default_internal = Encoding::IBM437
+ 1.to_s.encoding.should.equal?(Encoding::US_ASCII)
+ end
+ end
+
+ context "bignum" do
+ describe "when given a base" do
+ it "returns self converted to a String using the given base" do
+ a = 2**64
+ a.to_s(2).should == "10000000000000000000000000000000000000000000000000000000000000000"
+ a.to_s(8).should == "2000000000000000000000"
+ a.to_s(16).should == "10000000000000000"
+ a.to_s(32).should == "g000000000000"
+ end
+
+ it "raises an ArgumentError if the base is less than 2 or higher than 36" do
+ -> { 123.to_s(-1) }.should.raise(ArgumentError)
+ -> { 123.to_s(0) }.should.raise(ArgumentError)
+ -> { 123.to_s(1) }.should.raise(ArgumentError)
+ -> { 123.to_s(37) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "when given no base" do
+ it "returns self converted to a String using base 10" do
+ bignum_value(9).to_s.should == "18446744073709551625"
+ bignum_value.to_s.should == "18446744073709551616"
+ (-bignum_value(675)).to_s.should == "-18446744073709552291"
+ end
+ end
+
+ before :each do
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "returns a String in US-ASCII encoding when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ bignum_value.to_s.encoding.should.equal?(Encoding::US_ASCII)
+ end
+
+ it "returns a String in US-ASCII encoding when Encoding.default_internal is not nil" do
+ Encoding.default_internal = Encoding::IBM437
+ bignum_value.to_s.encoding.should.equal?(Encoding::US_ASCII)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/truncate_spec.rb b/spec/ruby/core/integer/truncate_spec.rb
new file mode 100644
index 0000000000..b95c183cf3
--- /dev/null
+++ b/spec/ruby/core/integer/truncate_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_i'
+require_relative 'shared/integer_rounding'
+
+describe "Integer#truncate" do
+ it_behaves_like :integer_to_i, :truncate
+ it_behaves_like :integer_rounding_positive_precision, :truncate
+
+ context "precision argument specified as part of the truncate method is negative" do
+ it "returns an integer with at least precision.abs trailing zeros" do
+ 1832.truncate(-1).should.eql?(1830)
+ 1832.truncate(-2).should.eql?(1800)
+ 1832.truncate(-3).should.eql?(1000)
+ -1832.truncate(-1).should.eql?(-1830)
+ -1832.truncate(-2).should.eql?(-1800)
+ -1832.truncate(-3).should.eql?(-1000)
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/try_convert_spec.rb b/spec/ruby/core/integer/try_convert_spec.rb
new file mode 100644
index 0000000000..8c9b4276b9
--- /dev/null
+++ b/spec/ruby/core/integer/try_convert_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Integer.try_convert" do
+ it "returns the argument if it's an Integer" do
+ x = 42
+ Integer.try_convert(x).should.equal?(x)
+ end
+
+ it "returns nil when the argument does not respond to #to_int" do
+ Integer.try_convert(Object.new).should == nil
+ end
+
+ it "sends #to_int to the argument and returns the result if it's nil" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return(nil)
+ Integer.try_convert(obj).should == nil
+ end
+
+ it "sends #to_int to the argument and returns the result if it's an Integer" do
+ x = 234
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return(x)
+ Integer.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_int to the argument and raises TypeError if it's not a kind of Integer" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return(Object.new)
+ -> {
+ Integer.try_convert obj
+ }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_int gives Object)")
+ end
+
+ it "responds with a different error message when it raises a TypeError, depending on the type of the non-Integer object :to_int returns" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return("A String")
+ -> {
+ Integer.try_convert obj
+ }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_int gives String)")
+ end
+
+ it "does not rescue exceptions raised by #to_int" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_raise(RuntimeError)
+ -> { Integer.try_convert obj }.should.raise(RuntimeError)
+ end
+end
diff --git a/spec/ruby/core/integer/uminus_spec.rb b/spec/ruby/core/integer/uminus_spec.rb
new file mode 100644
index 0000000000..7a9cfe89bf
--- /dev/null
+++ b/spec/ruby/core/integer/uminus_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+
+describe "Integer#-@" do
+ context "fixnum" do
+ it "returns self as a negative value" do
+ 2.send(:-@).should == -2
+ -2.should == -2
+ -268435455.should == -268435455
+ (--5).should == 5
+ -8.send(:-@).should == 8
+ end
+
+ it "negates self at Fixnum/Bignum boundaries" do
+ (-fixnum_max).should == (0 - fixnum_max)
+ (-fixnum_max).should < 0
+ (-fixnum_min).should == (0 - fixnum_min)
+ (-fixnum_min).should > 0
+ end
+ end
+
+ context "bignum" do
+ it "returns self as a negative value" do
+ bignum_value.send(:-@).should == -18446744073709551616
+ (-bignum_value).send(:-@).should == 18446744073709551616
+
+ bignum_value(921).send(:-@).should == -18446744073709552537
+ (-bignum_value(921).send(:-@)).should == 18446744073709552537
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/upto_spec.rb b/spec/ruby/core/integer/upto_spec.rb
new file mode 100644
index 0000000000..ba63dcc9ea
--- /dev/null
+++ b/spec/ruby/core/integer/upto_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+
+describe "Integer#upto [stop] when self and stop are Integers" do
+ it "does not yield when stop is less than self" do
+ result = []
+ 5.upto(4) { |x| result << x }
+ result.should == []
+ end
+
+ it "yields once when stop equals self" do
+ result = []
+ 5.upto(5) { |x| result << x }
+ result.should == [5]
+ end
+
+ it "yields while increasing self until it is less than stop" do
+ result = []
+ 2.upto(5) { |x| result << x }
+ result.should == [2, 3, 4, 5]
+ end
+
+ it "yields while increasing self until it is greater than floor of a Float endpoint" do
+ result = []
+ 9.upto(13.3) {|i| result << i}
+ -5.upto(-1.3) {|i| result << i}
+ result.should == [9,10,11,12,13,-5,-4,-3,-2]
+ end
+
+ it "raises an ArgumentError for non-numeric endpoints" do
+ -> { 1.upto("A") {|x| p x} }.should.raise(ArgumentError)
+ -> { 1.upto(nil) {|x| p x} }.should.raise(ArgumentError)
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ result = []
+
+ enum = 2.upto(5)
+ enum.each { |i| result << i }
+
+ result.should == [2, 3, 4, 5]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "raises an ArgumentError for non-numeric endpoints" do
+ enum = 1.upto("A")
+ -> { enum.size }.should.raise(ArgumentError)
+ enum = 1.upto(nil)
+ -> { enum.size }.should.raise(ArgumentError)
+ end
+
+ it "returns stop - self + 1" do
+ 5.upto(10).size.should == 6
+ 1.upto(10).size.should == 10
+ 0.upto(10).size.should == 11
+ 0.upto(0).size.should == 1
+ -5.upto(-3).size.should == 3
+ end
+
+ it "returns 0 when stop < self" do
+ 5.upto(4).size.should == 0
+ 0.upto(-5).size.should == 0
+ -3.upto(-5).size.should == 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/integer/zero_spec.rb b/spec/ruby/core/integer/zero_spec.rb
new file mode 100644
index 0000000000..bd362c4181
--- /dev/null
+++ b/spec/ruby/core/integer/zero_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Integer#zero?" do
+ it "returns true if self is 0" do
+ 0.should.zero?
+ 1.should_not.zero?
+ -1.should_not.zero?
+ end
+
+ it "Integer#zero? overrides Numeric#zero?" do
+ 42.method(:zero?).owner.should == Integer
+ end
+end
diff --git a/spec/ruby/core/io/advise_spec.rb b/spec/ruby/core/io/advise_spec.rb
new file mode 100644
index 0000000000..b77ed53ad4
--- /dev/null
+++ b/spec/ruby/core/io/advise_spec.rb
@@ -0,0 +1,86 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#advise" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "raises a TypeError if advise is not a Symbol" do
+ -> {
+ @io.advise("normal")
+ }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if offset cannot be coerced to an Integer" do
+ -> {
+ @io.advise(:normal, "wat")
+ }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if len cannot be coerced to an Integer" do
+ -> {
+ @io.advise(:normal, 0, "wat")
+ }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError if offset is too big" do
+ -> {
+ @io.advise(:normal, 10 ** 32)
+ }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if len is too big" do
+ -> {
+ @io.advise(:normal, 0, 10 ** 32)
+ }.should.raise(RangeError)
+ end
+
+ it "raises a NotImplementedError if advise is not recognized" do
+ ->{
+ @io.advise(:foo)
+ }.should.raise(NotImplementedError)
+ end
+
+ it "supports the normal advice type" do
+ @io.advise(:normal).should == nil
+ end
+
+ it "supports the sequential advice type" do
+ @io.advise(:sequential).should == nil
+ end
+
+ it "supports the random advice type" do
+ @io.advise(:random).should == nil
+ end
+
+ it "supports the dontneed advice type" do
+ @io.advise(:dontneed).should == nil
+ end
+
+ it "supports the noreuse advice type" do
+ @io.advise(:noreuse).should == nil
+ end
+
+ platform_is_not :linux do
+ it "supports the willneed advice type" do
+ @io.advise(:willneed).should == nil
+ end
+ end
+
+ guard -> { platform_is :linux and kernel_version_is '3.6' } do # [ruby-core:65355] tmpfs is not supported
+ it "supports the willneed advice type" do
+ @io.advise(:willneed).should == nil
+ end
+ end
+
+ it "raises an IOError if the stream is closed" do
+ @io.close
+ -> { @io.advise(:normal) }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/autoclose_spec.rb b/spec/ruby/core/io/autoclose_spec.rb
new file mode 100644
index 0000000000..596f3d6934
--- /dev/null
+++ b/spec/ruby/core/io/autoclose_spec.rb
@@ -0,0 +1,77 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#autoclose?" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.autoclose = true unless @io.closed?
+ @io.close unless @io.closed?
+ end
+
+ it "is set to true by default" do
+ @io.should.autoclose?
+ end
+
+ it "cannot be queried on a closed IO object" do
+ @io.close
+ -> { @io.autoclose? }.should.raise(IOError, /closed stream/)
+ end
+end
+
+describe "IO#autoclose=" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.autoclose = true unless @io.closed?
+ @io.close unless @io.closed?
+ end
+
+ it "can be set to true" do
+ @io.autoclose = false
+ @io.autoclose = true
+ @io.should.autoclose?
+ end
+
+ it "can be set to false" do
+ @io.autoclose = true
+ @io.autoclose = false
+ @io.should_not.autoclose?
+ end
+
+ it "can be set to any truthy value" do
+ @io.autoclose = false
+ @io.autoclose = 42
+ @io.should.autoclose?
+
+ @io.autoclose = false
+ @io.autoclose = Object.new
+ @io.should.autoclose?
+ end
+
+ it "can be set to any falsy value" do
+ @io.autoclose = true
+ @io.autoclose = nil
+ @io.should_not.autoclose?
+ end
+
+ it "can be set multiple times" do
+ @io.autoclose = true
+ @io.should.autoclose?
+
+ @io.autoclose = false
+ @io.should_not.autoclose?
+
+ @io.autoclose = true
+ @io.should.autoclose?
+ end
+
+ it "cannot be set on a closed IO object" do
+ @io.close
+ -> { @io.autoclose = false }.should.raise(IOError, /closed stream/)
+ end
+end
diff --git a/spec/ruby/core/io/binmode_spec.rb b/spec/ruby/core/io/binmode_spec.rb
new file mode 100644
index 0000000000..8117229c91
--- /dev/null
+++ b/spec/ruby/core/io/binmode_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#binmode" do
+ before :each do
+ @name = tmp("io_binmode.txt")
+ end
+
+ after :each do
+ @io.close if @io and !@io.closed?
+ rm_r @name
+ end
+
+ it "returns self" do
+ @io = new_io(@name)
+ @io.binmode.should.equal?(@io)
+ end
+
+ it "raises an IOError on closed stream" do
+ -> { IOSpecs.closed_io.binmode }.should.raise(IOError)
+ end
+
+ it "sets external encoding to binary" do
+ @io = new_io(@name, "w:utf-8")
+ @io.binmode
+ @io.external_encoding.should == Encoding::BINARY
+ end
+
+ it "sets internal encoding to nil" do
+ @io = new_io(@name, "w:utf-8:ISO-8859-1")
+ @io.binmode
+ @io.internal_encoding.should == nil
+ end
+end
+
+describe "IO#binmode?" do
+ before :each do
+ @filename = tmp("IO_binmode_file")
+ @file = File.open(@filename, "w")
+ @duped = nil
+ end
+
+ after :each do
+ @duped.close if @duped
+ @file.close
+ rm_r @filename
+ end
+
+ it "is true after a call to IO#binmode" do
+ @file.binmode?.should == false
+ @file.binmode
+ @file.binmode?.should == true
+ end
+
+ it "propagates to dup'ed IO objects" do
+ @file.binmode
+ @duped = @file.dup
+ @duped.binmode?.should == @file.binmode?
+ end
+
+ it "raises an IOError on closed stream" do
+ -> { IOSpecs.closed_io.binmode? }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/binread_spec.rb b/spec/ruby/core/io/binread_spec.rb
new file mode 100644
index 0000000000..3023c7f177
--- /dev/null
+++ b/spec/ruby/core/io/binread_spec.rb
@@ -0,0 +1,57 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO.binread" do
+ before :each do
+ @internal = Encoding.default_internal
+
+ @fname = tmp('io_read.txt')
+ @contents = "1234567890"
+ touch(@fname) { |f| f.write @contents }
+ end
+
+ after :each do
+ rm_r @fname
+ Encoding.default_internal = @internal
+ end
+
+ it "reads the contents of a file" do
+ IO.binread(@fname).should == @contents
+ end
+
+ it "reads the contents of a file up to a certain size when specified" do
+ IO.binread(@fname, 5).should == @contents.slice(0..4)
+ end
+
+ it "reads the contents of a file from an offset of a specific size when specified" do
+ IO.binread(@fname, 5, 3).should == @contents.slice(3, 5)
+ end
+
+ it "returns a String in BINARY encoding" do
+ IO.binread(@fname).encoding.should == Encoding::BINARY
+ end
+
+ it "returns a String in BINARY encoding regardless of Encoding.default_internal" do
+ Encoding.default_internal = Encoding::EUC_JP
+ IO.binread(@fname).encoding.should == Encoding::BINARY
+ end
+
+ it "raises an ArgumentError when not passed a valid length" do
+ -> { IO.binread @fname, -1 }.should.raise(ArgumentError)
+ end
+
+ it "raises an Errno::EINVAL when not passed a valid offset" do
+ -> { IO.binread @fname, 0, -1 }.should.raise(Errno::EINVAL)
+ end
+
+ ruby_version_is ""..."4.0" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.binread(cmd)
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
+end
diff --git a/spec/ruby/core/io/binwrite_spec.rb b/spec/ruby/core/io/binwrite_spec.rb
new file mode 100644
index 0000000000..8ebc86a52e
--- /dev/null
+++ b/spec/ruby/core/io/binwrite_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/binwrite'
+
+describe "IO.binwrite" do
+ it_behaves_like :io_binwrite, :binwrite
+end
diff --git a/spec/ruby/core/io/buffer/and_spec.rb b/spec/ruby/core/io/buffer/and_spec.rb
new file mode 100644
index 0000000000..3cea163b0e
--- /dev/null
+++ b/spec/ruby/core/io/buffer/and_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../../spec_helper'
+
+describe :io_buffer_and, shared: true do
+ it "applies the argument buffer as an AND bit mask across the whole buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer.send(@method, mask)
+ result.get_string.should == "\x30\x02\x30\x04\x30".b
+ result.free
+ end
+ end
+ end
+
+ it "ignores extra parts of mask if it is longer than source buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F\x00\x00\x00\xFF\xFF") do |mask|
+ result = buffer.send(@method, mask)
+ result.get_string.should == "\x30\x02\x00\x00\x00".b
+ result.free
+ end
+ end
+ end
+
+ it "raises TypeError if mask is not an IO::Buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ -> { buffer.send(@method, "\xF8\x8F") }.should.raise(TypeError, "wrong argument type String (expected IO::Buffer)")
+ -> { buffer.send(@method, 0xF8) }.should.raise(TypeError, "wrong argument type Integer (expected IO::Buffer)")
+ -> { buffer.send(@method, nil) }.should.raise(TypeError, "wrong argument type nil (expected IO::Buffer)")
+ end
+ end
+end
+
+describe "IO::Buffer#&" do
+ it_behaves_like :io_buffer_and, :&
+
+ it "creates a new internal buffer of the same size" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer & mask
+ result.should_not.equal? buffer
+ result.should.internal?
+ result.size.should == buffer.size
+ result.free
+ buffer.get_string.should == "12345".b
+ end
+ end
+ end
+end
+
+describe "IO::Buffer#and!" do
+ it_behaves_like :io_buffer_and, :and!
+
+ it "modifies the buffer in place" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer.and!(mask)
+ result.should.equal? buffer
+ result.should.external?
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/bit_count_spec.rb b/spec/ruby/core/io/buffer/bit_count_spec.rb
new file mode 100644
index 0000000000..62f56f5b98
--- /dev/null
+++ b/spec/ruby/core/io/buffer/bit_count_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../../spec_helper'
+
+ruby_version_is "4.1" do
+ describe "IO::Buffer#bit_count" do
+ it "counts all set bits in the whole buffer" do
+ IO::Buffer.for(+"\xFF\x00\x0F") do |buffer|
+ buffer.bit_count.should == 12
+ end
+ end
+
+ it "returns 0 for a buffer of all zero bytes" do
+ IO::Buffer.for(+"\x00\x00\x00") do |buffer|
+ buffer.bit_count.should == 0
+ end
+ end
+
+ it "returns 8 * size for a buffer of all 0xFF bytes" do
+ IO::Buffer.for(+"\xFF" * 9) do |buffer|
+ buffer.bit_count.should == 72
+ end
+ end
+
+ it "returns 0 for an empty buffer" do
+ IO::Buffer.new(0).bit_count.should == 0
+ end
+
+ it "accepts an offset to start counting from (length defaults to remaining bytes)" do
+ IO::Buffer.for(+"\xFF\x00\x0F") do |buffer|
+ buffer.bit_count(0).should == 12 # offset=0 => entire buffer
+ buffer.bit_count(1).should == 4 # offset=1 => 0x00 + 0x0F
+ buffer.bit_count(2).should == 4 # offset=2 => 0x0F only
+ end
+ end
+
+ it "accepts an offset and length to restrict the counted region" do
+ IO::Buffer.for(+"\xFF\x00\x0F") do |buffer|
+ buffer.bit_count(0, 1).should == 8 # just 0xFF
+ buffer.bit_count(1, 1).should == 0 # just 0x00
+ buffer.bit_count(2, 1).should == 4 # just 0x0F
+ buffer.bit_count(1, 2).should == 4 # 0x00 + 0x0F
+ end
+ end
+
+ it "handles 8-byte aligned buffers efficiently" do
+ IO::Buffer.for(+"\xAA" * 8) do |buffer|
+ # 0xAA = 10101010 => 4 bits per byte => 32 total
+ buffer.bit_count.should == 32
+ end
+ end
+
+ it "raises ArgumentError when offset + length exceeds buffer size" do
+ IO::Buffer.for(+"\xFF") do |buffer|
+ -> { buffer.bit_count(0, 2) }.should.raise(ArgumentError)
+ -> { buffer.bit_count(1, 1) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "returns an Integer" do
+ IO::Buffer.for(+"\xFF") do |buffer|
+ buffer.bit_count.should.is_a?(Integer)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/empty_spec.rb b/spec/ruby/core/io/buffer/empty_spec.rb
new file mode 100644
index 0000000000..4cceb4dc0d
--- /dev/null
+++ b/spec/ruby/core/io/buffer/empty_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/null_and_empty'
+
+describe "IO::Buffer#empty?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it_behaves_like :io_buffer_null_and_empty, :empty?
+
+ it "is true for a 0-length String-backed buffer created with .for" do
+ @buffer = IO::Buffer.for("")
+ @buffer.empty?.should == true
+ end
+
+ it "is true for a 0-length String-backed buffer created with .string" do
+ IO::Buffer.string(0) do |buffer|
+ buffer.empty?.should == true
+ end
+ end
+
+ it "is true for a 0-length slice of a buffer with size > 0" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.slice(3, 0).empty?.should == true
+ end
+end
diff --git a/spec/ruby/core/io/buffer/external_spec.rb b/spec/ruby/core/io/buffer/external_spec.rb
new file mode 100644
index 0000000000..18b2ee0d06
--- /dev/null
+++ b/spec/ruby/core/io/buffer/external_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#external?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is true for a buffer with externally-managed memory" do
+ @buffer = IO::Buffer.for("string")
+ @buffer.external?.should == true
+ end
+
+ it "is false for a buffer with self-managed memory" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED)
+ @buffer.external?.should == false
+ end
+
+ it "is false for a null buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.external?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/for_spec.rb b/spec/ruby/core/io/buffer/for_spec.rb
new file mode 100644
index 0000000000..4c614f74b0
--- /dev/null
+++ b/spec/ruby/core/io/buffer/for_spec.rb
@@ -0,0 +1,95 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer.for" do
+ before :each do
+ @string = +"för striñg"
+ end
+
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ context "without a block" do
+ it "copies string's contents, creating a separate read-only buffer" do
+ @buffer = IO::Buffer.for(@string)
+
+ @buffer.size.should == @string.bytesize
+ @buffer.get_string.should == @string.b
+
+ @string[0] = "d"
+ @buffer.get_string(0, 1).should == "f".b
+
+ -> { @buffer.set_string("d") }.should.raise(IO::Buffer::AccessError, "Buffer is not writable!")
+ end
+
+ it "creates an external, read-only buffer" do
+ @buffer = IO::Buffer.for(@string)
+
+ @buffer.should_not.internal?
+ @buffer.should_not.mapped?
+ @buffer.should.external?
+
+ @buffer.should_not.empty?
+ @buffer.should_not.null?
+
+ @buffer.should_not.shared?
+ @buffer.should_not.private?
+ @buffer.should.readonly?
+
+ @buffer.should_not.locked?
+ @buffer.should.valid?
+ end
+ end
+
+ context "with a block" do
+ it "returns the last value in the block" do
+ value =
+ IO::Buffer.for(@string) do |buffer|
+ buffer.size * 3
+ end
+ value.should == @string.bytesize * 3
+ end
+
+ it "frees the buffer at the end of the block" do
+ IO::Buffer.for(@string) do |buffer|
+ @buffer = buffer
+ @buffer.should_not.null?
+ end
+ @buffer.should.null?
+ end
+
+ context "if string is not frozen" do
+ it "creates a modifiable string-backed buffer" do
+ IO::Buffer.for(@string) do |buffer|
+ buffer.size.should == @string.bytesize
+ buffer.get_string.should == @string.b
+
+ buffer.should_not.readonly?
+ buffer.should_not.locked?
+
+ buffer.set_string("ghost shell")
+ @string.should == "ghost shellg"
+ end
+ end
+
+ it "locks the original string to prevent modification" do
+ IO::Buffer.for(@string) do |_buffer|
+ -> { @string[0] = "t" }.should.raise(RuntimeError, "can't modify string; temporarily locked")
+ end
+ @string[1] = "u"
+ @string.should == "fur striñg"
+ end
+ end
+
+ context "if string is frozen" do
+ it "creates a read-only string-backed buffer" do
+ IO::Buffer.for(@string.freeze) do |buffer|
+ buffer.should.readonly?
+
+ -> { buffer.set_string("ghost shell") }.should.raise(IO::Buffer::AccessError, "Buffer is not writable!")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/free_spec.rb b/spec/ruby/core/io/buffer/free_spec.rb
new file mode 100644
index 0000000000..20fb7b901f
--- /dev/null
+++ b/spec/ruby/core/io/buffer/free_spec.rb
@@ -0,0 +1,102 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#free" do
+ context "with a buffer created with .new" do
+ it "frees internal memory and nullifies the buffer" do
+ buffer = IO::Buffer.new(4)
+ buffer.free
+ buffer.null?.should == true
+ end
+
+ it "frees mapped memory and nullifies the buffer" do
+ buffer = IO::Buffer.new(4, IO::Buffer::MAPPED)
+ buffer.free
+ buffer.null?.should == true
+ end
+ end
+
+ context "with a file-backed buffer created with .map" do
+ it "frees mapped memory and nullifies the buffer" do
+ File.open(__FILE__, "r") do |file|
+ buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)
+ buffer.free
+ buffer.null?.should == true
+ end
+ end
+ end
+
+ context "with a String-backed buffer created with .for" do
+ context "without a block" do
+ it "disassociates the buffer from the string and nullifies the buffer" do
+ string = +"test"
+ buffer = IO::Buffer.for(string)
+ # Read-only buffer, can't modify the string.
+ buffer.free
+ buffer.null?.should == true
+ end
+ end
+
+ context "with a block" do
+ it "disassociates the buffer from the string and nullifies the buffer" do
+ string = +"test"
+ IO::Buffer.for(string) do |buffer|
+ buffer.set_string("meat")
+ buffer.free
+ buffer.null?.should == true
+ end
+ string.should == "meat"
+ end
+ end
+ end
+
+ context "with a String-backed buffer created with .string" do
+ it "disassociates the buffer from the string and nullifies the buffer" do
+ string =
+ IO::Buffer.string(4) do |buffer|
+ buffer.set_string("meat")
+ buffer.free
+ buffer.null?.should == true
+ end
+ string.should == "meat"
+ end
+ end
+
+ it "can be called repeatedly without an error" do
+ buffer = IO::Buffer.new(4)
+ buffer.free
+ buffer.null?.should == true
+ buffer.free
+ buffer.null?.should == true
+ end
+
+ it "is disallowed while locked, raising IO::Buffer::LockedError" do
+ buffer = IO::Buffer.new(4)
+ buffer.locked do
+ -> { buffer.free }.should.raise(IO::Buffer::LockedError, "Buffer is locked!")
+ end
+ buffer.free
+ buffer.null?.should == true
+ end
+
+ context "with a slice of a buffer" do
+ it "nullifies the slice, not touching the buffer" do
+ buffer = IO::Buffer.new(4)
+ slice = buffer.slice(0, 2)
+
+ slice.free
+ slice.null?.should == true
+ buffer.null?.should == false
+
+ buffer.free
+ end
+
+ it "nullifies buffer, invalidating the slice" do
+ buffer = IO::Buffer.new(4)
+ slice = buffer.slice(0, 2)
+
+ buffer.free
+ slice.null?.should == false
+ slice.valid?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/initialize_spec.rb b/spec/ruby/core/io/buffer/initialize_spec.rb
new file mode 100644
index 0000000000..dba6fddc79
--- /dev/null
+++ b/spec/ruby/core/io/buffer/initialize_spec.rb
@@ -0,0 +1,119 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#initialize" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "creates a new zero-filled buffer with default size" do
+ @buffer = IO::Buffer.new
+ @buffer.size.should == IO::Buffer::DEFAULT_SIZE
+ @buffer.each(:U8).should.all? { |_offset, value| value.eql?(0) }
+ end
+
+ it "creates a buffer with default state" do
+ @buffer = IO::Buffer.new
+
+ @buffer.should_not.external?
+
+ @buffer.should_not.shared?
+ @buffer.should_not.private?
+ @buffer.should_not.readonly?
+
+ @buffer.should_not.empty?
+ @buffer.should_not.null?
+
+ @buffer.should_not.locked?
+ @buffer.should.valid?
+ end
+
+ context "with size argument" do
+ it "creates a new internal buffer if size is less than IO::Buffer::PAGE_SIZE" do
+ size = IO::Buffer::PAGE_SIZE - 1
+ @buffer = IO::Buffer.new(size)
+ @buffer.size.should == size
+ @buffer.should_not.empty?
+
+ @buffer.should.internal?
+ @buffer.should_not.mapped?
+ end
+
+ it "creates a new mapped buffer if size is greater than or equal to IO::Buffer::PAGE_SIZE" do
+ size = IO::Buffer::PAGE_SIZE
+ @buffer = IO::Buffer.new(size)
+ @buffer.size.should == size
+ @buffer.should_not.empty?
+
+ @buffer.should_not.internal?
+ @buffer.should.mapped?
+ end
+
+ it "creates a null buffer if size is 0" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.should.null?
+ @buffer.should.empty?
+ end
+
+ it "raises TypeError if size is not an Integer" do
+ -> { IO::Buffer.new(nil) }.should.raise(TypeError, "not an Integer")
+ -> { IO::Buffer.new(10.0) }.should.raise(TypeError, "not an Integer")
+ end
+
+ it "raises ArgumentError if size is negative" do
+ -> { IO::Buffer.new(-1) }.should.raise(ArgumentError, "Size can't be negative!")
+ end
+ end
+
+ context "with size and flags arguments" do
+ it "forces mapped buffer with IO::Buffer::MAPPED flag" do
+ @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE - 1, IO::Buffer::MAPPED)
+ @buffer.should.mapped?
+ @buffer.should_not.internal?
+ @buffer.should_not.empty?
+ end
+
+ it "forces internal buffer with IO::Buffer::INTERNAL flag" do
+ @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::INTERNAL)
+ @buffer.should.internal?
+ @buffer.should_not.mapped?
+ @buffer.should_not.empty?
+ end
+
+ it "allows extra flags" do
+ @buffer = IO::Buffer.new(10, IO::Buffer::INTERNAL | IO::Buffer::SHARED | IO::Buffer::READONLY)
+ @buffer.should.internal?
+ @buffer.should.shared?
+ @buffer.should.readonly?
+ end
+
+ it "ignores flags if size is 0" do
+ @buffer = IO::Buffer.new(0, 0xffff)
+ @buffer.should.null?
+ @buffer.should.empty?
+
+ @buffer.should_not.internal?
+ @buffer.should_not.mapped?
+ @buffer.should_not.external?
+
+ @buffer.should_not.shared?
+ @buffer.should_not.readonly?
+
+ @buffer.should_not.locked?
+ @buffer.should.valid?
+ end
+
+ it "raises IO::Buffer::AllocationError if neither IO::Buffer::MAPPED nor IO::Buffer::INTERNAL is given" do
+ -> { IO::Buffer.new(10, IO::Buffer::READONLY) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!")
+ -> { IO::Buffer.new(10, 0) }.should.raise(IO::Buffer::AllocationError, "Could not allocate buffer!")
+ end
+
+ it "raises ArgumentError if flags is negative" do
+ -> { IO::Buffer.new(10, -1) }.should.raise(ArgumentError, "Flags can't be negative!")
+ end
+
+ it "raises TypeError with non-Integer flags" do
+ -> { IO::Buffer.new(10, 0.0) }.should.raise(TypeError, "not an Integer")
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/internal_spec.rb b/spec/ruby/core/io/buffer/internal_spec.rb
new file mode 100644
index 0000000000..73e19eb85e
--- /dev/null
+++ b/spec/ruby/core/io/buffer/internal_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#internal?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is true for an internally-allocated buffer" do
+ @buffer = IO::Buffer.new(12)
+ @buffer.internal?.should == true
+ end
+
+ it "is false for an externally-allocated buffer" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED)
+ @buffer.internal?.should == false
+ end
+
+ it "is false for a null buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.internal?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/locked_spec.rb b/spec/ruby/core/io/buffer/locked_spec.rb
new file mode 100644
index 0000000000..249026aa8a
--- /dev/null
+++ b/spec/ruby/core/io/buffer/locked_spec.rb
@@ -0,0 +1,75 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#locked" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ context "when buffer is locked" do
+ it "allows reading and writing operations on the buffer" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.set_string("test")
+ @buffer.locked do
+ @buffer.get_string.should == "test"
+ @buffer.set_string("meat")
+ end
+ @buffer.get_string.should == "meat"
+ end
+
+ it "disallows operations changing buffer itself, raising IO::Buffer::LockedError" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.locked do
+ # Just an example, each method is responsible for checking the lock state.
+ -> { @buffer.resize(8) }.should.raise(IO::Buffer::LockedError)
+ end
+ end
+ end
+
+ it "disallows reentrant locking, raising IO::Buffer::LockedError" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.locked do
+ -> { @buffer.locked {} }.should.raise(IO::Buffer::LockedError, "Buffer already locked!")
+ end
+ end
+
+ it "does not propagate to buffer's slices" do
+ @buffer = IO::Buffer.new(4)
+ slice = @buffer.slice(0, 2)
+ @buffer.locked do
+ @buffer.locked?.should == true
+ slice.locked?.should == false
+ slice.locked { slice.locked?.should == true }
+ end
+ end
+
+ it "does not propagate backwards from buffer's slices" do
+ @buffer = IO::Buffer.new(4)
+ slice = @buffer.slice(0, 2)
+ slice.locked do
+ slice.locked?.should == true
+ @buffer.locked?.should == false
+ @buffer.locked { @buffer.locked?.should == true }
+ end
+ end
+end
+
+describe "IO::Buffer#locked?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is false by default" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.locked?.should == false
+ end
+
+ it "is true only inside of #locked block" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.locked do
+ @buffer.locked?.should == true
+ end
+ @buffer.locked?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/map_spec.rb b/spec/ruby/core/io/buffer/map_spec.rb
new file mode 100644
index 0000000000..4b28539ad8
--- /dev/null
+++ b/spec/ruby/core/io/buffer/map_spec.rb
@@ -0,0 +1,343 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer.map" do
+ before :all do
+ @tmp_files = []
+
+ @big_file_name = nil
+ @small_file_name = nil
+ end
+
+ after :all do
+ @tmp_files.each {|file| File.delete(file)}
+ end
+
+ def open_fixture
+ unless @small_file_name
+ @small_file_name = tmp("read_text.txt")
+ File.copy_stream(fixture(__dir__, "read_text.txt"), @small_file_name)
+ @tmp_files << @small_file_name
+ end
+ File.open(@small_file_name, "rb+")
+ end
+
+ def open_big_file_fixture
+ unless @big_file_name
+ @big_file_name = tmp("big_file")
+ # Usually 4 kibibytes + 16 bytes
+ File.write(@big_file_name, "12345678" * (IO::Buffer::PAGE_SIZE / 8 + 2))
+ @tmp_files << @big_file_name
+ end
+ File.open(@big_file_name, "rb+")
+ end
+
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ @file&.close
+ @file = nil
+ end
+
+ it "creates a new buffer mapped from a file" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file)
+
+ @buffer.size.should == 9
+ @buffer.get_string.should == "abcâdef\n".b
+ end
+
+ it "allows to close the file after creating buffer, retaining mapping" do
+ file = open_fixture
+ @buffer = IO::Buffer.map(file)
+ file.close
+
+ @buffer.get_string.should == "abcâdef\n".b
+ end
+
+ it "creates a mapped, external, shared buffer" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file)
+
+ @buffer.should_not.internal?
+ @buffer.should.mapped?
+ @buffer.should.external?
+
+ @buffer.should_not.empty?
+ @buffer.should_not.null?
+
+ @buffer.should.shared?
+ @buffer.should_not.private?
+ @buffer.should_not.readonly?
+
+ @buffer.should_not.locked?
+ @buffer.should.valid?
+ end
+
+ guard -> { Process.respond_to?(:fork) } do
+ it "is shareable across processes" do
+ file_name = tmp("shared_buffer")
+ @file = File.open(file_name, "w+")
+ @file << "I'm private"
+ @file.rewind
+ @buffer = IO::Buffer.map(@file)
+
+ IO.popen("-") do |child_pipe|
+ if child_pipe
+ # Synchronize on child's output.
+ child_pipe.readlines.first.chomp.should == @buffer.to_s
+ @buffer.get_string.should == "I'm shared!"
+
+ @file.read.should == "I'm shared!"
+ else
+ @buffer.set_string("I'm shared!")
+ puts @buffer
+ end
+ ensure
+ child_pipe&.close
+ end
+ ensure
+ File.unlink(file_name)
+ end
+ end
+
+ context "with an empty file" do
+ ruby_version_is "4.0" do
+ it "raises ArgumentError" do
+ file_name = tmp("empty.txt")
+ @file = File.open(file_name, "wb+")
+ @tmp_files << file_name
+ -> { IO::Buffer.map(@file) }.should.raise(ArgumentError, "Invalid negative or zero file size!")
+ end
+ end
+ end
+
+ context "with a file opened only for reading" do
+ it "raises a SystemCallError unless read-only" do
+ @file = File.open(fixture(__dir__, "read_text.txt"), "rb")
+ -> { IO::Buffer.map(@file) }.should.raise(SystemCallError)
+ end
+ end
+
+ context "with size argument" do
+ it "limits the buffer to the specified size in bytes, starting from the start of the file" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file, 4)
+
+ @buffer.size.should == 4
+ @buffer.get_string.should == "abc\xC3".b
+ end
+
+ it "maps the whole file if size is nil" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file, nil)
+
+ @buffer.size.should == 9
+ end
+
+ context "if size is 0" do
+ ruby_version_is "4.0" do
+ it "raises ArgumentError" do
+ @file = open_fixture
+ -> { IO::Buffer.map(@file, 0) }.should.raise(ArgumentError, "Size can't be zero!")
+ end
+ end
+ end
+
+ it "raises TypeError if size is not an Integer or nil" do
+ @file = open_fixture
+ -> { IO::Buffer.map(@file, "10") }.should.raise(TypeError, "not an Integer")
+ -> { IO::Buffer.map(@file, 10.0) }.should.raise(TypeError, "not an Integer")
+ end
+
+ it "raises ArgumentError if size is negative" do
+ @file = open_fixture
+ -> { IO::Buffer.map(@file, -1) }.should.raise(ArgumentError, "Size can't be negative!")
+ end
+
+ ruby_version_is ""..."4.0" do
+ # May or may not cause a crash on access.
+ it "is undefined behavior if size is larger than file size"
+ end
+
+ ruby_version_is "4.0" do
+ it "raises ArgumentError if size is larger than file size" do
+ @file = open_fixture
+ -> { IO::Buffer.map(@file, 8192) }.should.raise(ArgumentError, "Size can't be larger than file size!")
+ end
+ end
+ end
+
+ context "with size and offset arguments" do
+ # Neither Windows nor macOS have clear, stable behavior with non-zero offset.
+ # https://bugs.ruby-lang.org/issues/21700
+ platform_is :linux do
+ context "if offset is an allowed value for system call" do
+ it "maps the span specified by size starting from the offset" do
+ @file = open_big_file_fixture
+ @buffer = IO::Buffer.map(@file, 14, IO::Buffer::PAGE_SIZE)
+
+ @buffer.size.should == 14
+ @buffer.get_string(0, 14).should == "12345678123456"
+ end
+
+ context "if size is nil" do
+ ruby_version_is ""..."4.0" do
+ it "maps the rest of the file" do
+ @file = open_big_file_fixture
+ @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
+
+ @buffer.get_string(0, 1).should == "1"
+ end
+
+ it "incorrectly sets buffer's size to file's full size" do
+ @file = open_big_file_fixture
+ @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
+
+ @buffer.size.should == @file.size
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "maps the rest of the file" do
+ @file = open_big_file_fixture
+ @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
+
+ @buffer.get_string(0, 1).should == "1"
+ end
+
+ it "sets buffer's size to file's remaining size" do
+ @file = open_big_file_fixture
+ @buffer = IO::Buffer.map(@file, nil, IO::Buffer::PAGE_SIZE)
+
+ @buffer.size.should == (@file.size - IO::Buffer::PAGE_SIZE)
+ end
+ end
+ end
+ end
+ end
+
+ it "maps the file from the start if offset is 0" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file, 4, 0)
+
+ @buffer.size.should == 4
+ @buffer.get_string.should == "abc\xC3".b
+ end
+
+ ruby_version_is ""..."4.0" do
+ # May or may not cause a crash on access.
+ it "is undefined behavior if offset+size is larger than file size"
+ end
+
+ ruby_version_is "4.0" do
+ it "raises ArgumentError if offset+size is larger than file size" do
+ @file = open_big_file_fixture
+ -> { IO::Buffer.map(@file, 17, IO::Buffer::PAGE_SIZE) }.should.raise(ArgumentError, "Offset too large!")
+ ensure
+ # Windows requires the file to be closed before deletion.
+ @file.close unless @file.closed?
+ end
+ end
+
+ it "raises TypeError if offset is not convertible to Integer" do
+ @file = open_fixture
+ -> { IO::Buffer.map(@file, 4, "4096") }.should.raise(TypeError, /no implicit conversion/)
+ -> { IO::Buffer.map(@file, 4, nil) }.should.raise(TypeError, /no implicit conversion/)
+ end
+
+ ruby_version_is "4.0" do
+ it "raises ArgumentError if offset is negative" do
+ @file = open_fixture
+ -> { IO::Buffer.map(@file, 4, -1) }.should.raise(ArgumentError, "Offset can't be negative!")
+ end
+ end
+ end
+
+ context "with flags argument" do
+ context "when READONLY flag is specified" do
+ it "sets readonly flag on the buffer, allowing only reads" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY)
+
+ @buffer.should.readonly?
+
+ @buffer.get_string.should == "abc\xC3\xA2def\n".b
+ end
+
+ it "allows mapping read-only files" do
+ @file = File.open(fixture(__dir__, "read_text.txt"), "rb")
+ @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY)
+
+ @buffer.should.readonly?
+
+ @buffer.get_string.should == "abc\xC3\xA2def\n".b
+ end
+
+ it "causes IO::Buffer::AccessError on write" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::READONLY)
+
+ -> { @buffer.set_string("test") }.should.raise(IO::Buffer::AccessError, "Buffer is not writable!")
+ end
+ end
+
+ context "when PRIVATE is specified" do
+ it "sets private flag on the buffer, making it freely modifiable" do
+ @file = open_fixture
+ @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE)
+
+ @buffer.should.private?
+ @buffer.should_not.shared?
+ @buffer.should_not.external?
+
+ @buffer.get_string.should == "abc\xC3\xA2def\n".b
+ @buffer.set_string("test12345")
+ @buffer.get_string.should == "test12345".b
+
+ @file.read.should == "abcâdef\n".b
+ end
+
+ it "allows mapping read-only files and modifying the buffer" do
+ @file = File.open(fixture(__dir__, "read_text.txt"), "rb")
+ @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE)
+
+ @buffer.should.private?
+ @buffer.should_not.shared?
+ @buffer.should_not.external?
+
+ @buffer.get_string.should == "abc\xC3\xA2def\n".b
+ @buffer.set_string("test12345")
+ @buffer.get_string.should == "test12345".b
+
+ @file.read.should == "abcâdef\n".b
+ end
+
+ guard -> { Process.respond_to?(:fork) } do
+ it "is not shared across processes" do
+ file_name = tmp("shared_buffer")
+ @file = File.open(file_name, "w+")
+ @file << "I'm private"
+ @file.rewind
+ @buffer = IO::Buffer.map(@file, nil, 0, IO::Buffer::PRIVATE)
+
+ IO.popen("-") do |child_pipe|
+ if child_pipe
+ # Synchronize on child's output.
+ child_pipe.readlines.first.chomp.should == @buffer.to_s
+ @buffer.get_string.should == "I'm private"
+
+ @file.read.should == "I'm private"
+ else
+ @buffer.set_string("I'm shared!")
+ puts @buffer
+ end
+ ensure
+ child_pipe&.close
+ end
+ ensure
+ File.unlink(file_name)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/mapped_spec.rb b/spec/ruby/core/io/buffer/mapped_spec.rb
new file mode 100644
index 0000000000..0decb97704
--- /dev/null
+++ b/spec/ruby/core/io/buffer/mapped_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#mapped?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is true for a buffer with mapped memory" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::MAPPED)
+ @buffer.mapped?.should == true
+ end
+
+ it "is false for a buffer with non-mapped memory" do
+ @buffer = IO::Buffer.for("string")
+ @buffer.mapped?.should == false
+ end
+
+ it "is false for a null buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.mapped?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/not_spec.rb b/spec/ruby/core/io/buffer/not_spec.rb
new file mode 100644
index 0000000000..4737a30bde
--- /dev/null
+++ b/spec/ruby/core/io/buffer/not_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../../spec_helper'
+
+describe :io_buffer_not, shared: true do
+ it "inverts every bit of the buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ result = buffer.send(@method)
+ result.get_string.should == "\xCE\xCD\xCC\xCB\xCA".b
+ result.free
+ end
+ end
+end
+
+describe "IO::Buffer#~" do
+ it_behaves_like :io_buffer_not, :~
+
+ it "creates a new internal buffer of the same size" do
+ IO::Buffer.for(+"12345") do |buffer|
+ result = ~buffer
+ result.should_not.equal? buffer
+ result.should.internal?
+ result.size.should == buffer.size
+ result.free
+ end
+ end
+end
+
+describe "IO::Buffer#not!" do
+ it_behaves_like :io_buffer_not, :not!
+
+ it "modifies the buffer in place" do
+ IO::Buffer.for(+"12345") do |buffer|
+ result = buffer.not!
+ result.should.equal? buffer
+ result.should.external?
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/null_spec.rb b/spec/ruby/core/io/buffer/null_spec.rb
new file mode 100644
index 0000000000..380a98bde1
--- /dev/null
+++ b/spec/ruby/core/io/buffer/null_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/null_and_empty'
+
+describe "IO::Buffer#null?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it_behaves_like :io_buffer_null_and_empty, :null?
+
+ it "is false for a 0-length String-backed buffer created with .for" do
+ @buffer = IO::Buffer.for("")
+ @buffer.null?.should == false
+ end
+
+ it "is false for a 0-length String-backed buffer created with .string" do
+ IO::Buffer.string(0) do |buffer|
+ buffer.null?.should == false
+ end
+ end
+
+ it "is false for a 0-length slice of a buffer with size > 0" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.slice(3, 0).null?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/or_spec.rb b/spec/ruby/core/io/buffer/or_spec.rb
new file mode 100644
index 0000000000..3e627c216c
--- /dev/null
+++ b/spec/ruby/core/io/buffer/or_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../../spec_helper'
+
+describe :io_buffer_or, shared: true do
+ it "applies the argument buffer as an OR bit mask across the whole buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer.send(@method, mask)
+ result.get_string.should == "\xF9\xBF\xFB\xBF\xFD".b
+ result.free
+ end
+ end
+ end
+
+ it "ignores extra parts of mask if it is longer than source buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F\x00\x00\x00\xFF\xFF") do |mask|
+ result = buffer.send(@method, mask)
+ result.get_string.should == "\xF9\xBF345".b
+ result.free
+ end
+ end
+ end
+
+ it "raises TypeError if mask is not an IO::Buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ -> { buffer.send(@method, "\xF8\x8F") }.should.raise(TypeError, "wrong argument type String (expected IO::Buffer)")
+ -> { buffer.send(@method, 0xF8) }.should.raise(TypeError, "wrong argument type Integer (expected IO::Buffer)")
+ -> { buffer.send(@method, nil) }.should.raise(TypeError, "wrong argument type nil (expected IO::Buffer)")
+ end
+ end
+end
+
+describe "IO::Buffer#|" do
+ it_behaves_like :io_buffer_or, :|
+
+ it "creates a new internal buffer of the same size" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer | mask
+ result.should_not.equal? buffer
+ result.should.internal?
+ result.size.should == buffer.size
+ result.free
+ buffer.get_string.should == "12345".b
+ end
+ end
+ end
+end
+
+describe "IO::Buffer#or!" do
+ it_behaves_like :io_buffer_or, :or!
+
+ it "modifies the buffer in place" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer.or!(mask)
+ result.should.equal? buffer
+ result.should.external?
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/private_spec.rb b/spec/ruby/core/io/buffer/private_spec.rb
new file mode 100644
index 0000000000..6e6afee34c
--- /dev/null
+++ b/spec/ruby/core/io/buffer/private_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#private?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is true for a buffer created with PRIVATE flag" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::PRIVATE)
+ @buffer.private?.should == true
+ end
+
+ it "is false for a buffer created without PRIVATE flag" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL)
+ @buffer.private?.should == false
+ end
+
+ it "is false for a null buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.private?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/readonly_spec.rb b/spec/ruby/core/io/buffer/readonly_spec.rb
new file mode 100644
index 0000000000..4eefc9f29f
--- /dev/null
+++ b/spec/ruby/core/io/buffer/readonly_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#readonly?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is true for a buffer created with READONLY flag" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::READONLY)
+ @buffer.readonly?.should == true
+ end
+
+ it "is true for a buffer that is non-writable" do
+ @buffer = IO::Buffer.for("string")
+ @buffer.readonly?.should == true
+ end
+
+ it "is false for a modifiable buffer" do
+ @buffer = IO::Buffer.new(12)
+ @buffer.readonly?.should == false
+ end
+
+ it "is false for a null buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.readonly?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/resize_spec.rb b/spec/ruby/core/io/buffer/resize_spec.rb
new file mode 100644
index 0000000000..6e684475f3
--- /dev/null
+++ b/spec/ruby/core/io/buffer/resize_spec.rb
@@ -0,0 +1,151 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#resize" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ context "with a buffer created with .new" do
+ it "resizes internal buffer, preserving type" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.resize(IO::Buffer::PAGE_SIZE)
+ @buffer.size.should == IO::Buffer::PAGE_SIZE
+ @buffer.internal?.should == true
+ @buffer.mapped?.should == false
+ end
+
+ platform_is :linux do
+ it "resizes mapped buffer, preserving type" do
+ @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED)
+ @buffer.resize(4)
+ @buffer.size.should == 4
+ @buffer.internal?.should == false
+ @buffer.mapped?.should == true
+ end
+ end
+
+ platform_is_not :linux do
+ it "resizes mapped buffer, changing type to internal" do
+ @buffer = IO::Buffer.new(IO::Buffer::PAGE_SIZE, IO::Buffer::MAPPED)
+ @buffer.resize(4)
+ @buffer.size.should == 4
+ @buffer.internal?.should == true
+ @buffer.mapped?.should == false
+ end
+ end
+ end
+
+ context "with a file-backed buffer created with .map" do
+ it "disallows resizing shared buffer, raising IO::Buffer::AccessError" do
+ File.open(__FILE__, "r+") do |file|
+ @buffer = IO::Buffer.map(file)
+ -> { @buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!")
+ end
+ end
+
+ it "resizes private buffer, discarding excess contents" do
+ File.open(__FILE__, "r") do |file|
+ @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::PRIVATE)
+ @buffer.resize(10)
+ @buffer.size.should == 10
+ @buffer.get_string.should == "require_re"
+ @buffer.resize(12)
+ @buffer.size.should == 12
+ @buffer.get_string.should == "require_re\0\0"
+ end
+ end
+ end
+
+ context "with a String-backed buffer created with .for" do
+ context "without a block" do
+ it "disallows resizing, raising IO::Buffer::AccessError" do
+ @buffer = IO::Buffer.for(+"test")
+ -> { @buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!")
+ end
+ end
+
+ context "with a block" do
+ it "disallows resizing, raising IO::Buffer::AccessError" do
+ IO::Buffer.for(+'test') do |buffer|
+ -> { buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!")
+ end
+ end
+ end
+ end
+
+ context "with a String-backed buffer created with .string" do
+ it "disallows resizing, raising IO::Buffer::AccessError" do
+ IO::Buffer.string(4) do |buffer|
+ -> { buffer.resize(10) }.should.raise(IO::Buffer::AccessError, "Cannot resize external buffer!")
+ end
+ end
+ end
+
+ context "with a null buffer" do
+ it "allows resizing a 0-sized buffer, creating a regular buffer according to new size" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.resize(IO::Buffer::PAGE_SIZE)
+ @buffer.size.should == IO::Buffer::PAGE_SIZE
+ @buffer.internal?.should == false
+ @buffer.mapped?.should == true
+ end
+
+ it "allows resizing after a free, creating a regular buffer according to new size" do
+ @buffer = IO::Buffer.for("test")
+ @buffer.free
+ @buffer.resize(10)
+ @buffer.size.should == 10
+ @buffer.internal?.should == true
+ @buffer.mapped?.should == false
+ end
+ end
+
+ it "allows resizing to 0, freeing memory" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.resize(0)
+ @buffer.null?.should == true
+ end
+
+ it "can be called repeatedly" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.resize(10)
+ @buffer.resize(27)
+ @buffer.resize(1)
+ @buffer.size.should == 1
+ end
+
+ it "always clears extra memory" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.set_string("test")
+ # This should not cause a re-allocation, just a technical resizing,
+ # even with very aggressive memory allocation.
+ @buffer.resize(2)
+ @buffer.resize(4)
+ @buffer.get_string.should == "te\0\0"
+ end
+
+ it "is disallowed while locked, raising IO::Buffer::LockedError" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.locked do
+ -> { @buffer.resize(10) }.should.raise(IO::Buffer::LockedError, "Cannot resize locked buffer!")
+ end
+ end
+
+ it "raises ArgumentError if size is negative" do
+ @buffer = IO::Buffer.new(4)
+ -> { @buffer.resize(-1) }.should.raise(ArgumentError, "Size can't be negative!")
+ end
+
+ it "raises TypeError if size is not an Integer" do
+ @buffer = IO::Buffer.new(4)
+ -> { @buffer.resize(nil) }.should.raise(TypeError, "not an Integer")
+ -> { @buffer.resize(10.0) }.should.raise(TypeError, "not an Integer")
+ end
+
+ context "with a slice of a buffer" do
+ # Current behavior of slice resizing seems unintended (it's undocumented, too).
+ # It either creates a completely new buffer, or breaks the slice on size 0.
+ it "needs to be reviewed for spec completeness"
+ end
+end
diff --git a/spec/ruby/core/io/buffer/shared/null_and_empty.rb b/spec/ruby/core/io/buffer/shared/null_and_empty.rb
new file mode 100644
index 0000000000..f8abc5f0fc
--- /dev/null
+++ b/spec/ruby/core/io/buffer/shared/null_and_empty.rb
@@ -0,0 +1,57 @@
+describe :io_buffer_null_and_empty, shared: true do
+ it "is false for a buffer with size > 0" do
+ @buffer = IO::Buffer.new(1)
+ @buffer.send(@method).should == false
+ end
+
+ it "is false for a slice with length > 0" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.slice(1, 2).send(@method).should == false
+ end
+
+ it "is false for a file-mapped buffer" do
+ File.open(__FILE__, "rb") do |file|
+ @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)
+ @buffer.send(@method).should == false
+ end
+ end
+
+ it "is false for a non-empty String-backed buffer created with .for" do
+ @buffer = IO::Buffer.for("test")
+ @buffer.send(@method).should == false
+ end
+
+ it "is false for a non-empty String-backed buffer created with .string" do
+ IO::Buffer.string(4) do |buffer|
+ buffer.send(@method).should == false
+ end
+ end
+
+ it "is true for a 0-sized buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.send(@method).should == true
+ end
+
+ it "is true for a slice of a 0-sized buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.slice(0, 0).send(@method).should == true
+ end
+
+ it "is true for a freed buffer" do
+ @buffer = IO::Buffer.new(1)
+ @buffer.free
+ @buffer.send(@method).should == true
+ end
+
+ it "is true for a buffer resized to 0" do
+ @buffer = IO::Buffer.new(1)
+ @buffer.resize(0)
+ @buffer.send(@method).should == true
+ end
+
+ it "is true for a buffer whose memory was transferred" do
+ buffer = IO::Buffer.new(1)
+ @buffer = buffer.transfer
+ buffer.send(@method).should == true
+ end
+end
diff --git a/spec/ruby/core/io/buffer/shared_spec.rb b/spec/ruby/core/io/buffer/shared_spec.rb
new file mode 100644
index 0000000000..2cc93e6d08
--- /dev/null
+++ b/spec/ruby/core/io/buffer/shared_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#shared?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ it "is true for a buffer created with SHARED flag" do
+ @buffer = IO::Buffer.new(12, IO::Buffer::INTERNAL | IO::Buffer::SHARED)
+ @buffer.shared?.should == true
+ end
+
+ it "is true for a non-private buffer created with .map" do
+ path = fixture(__dir__, "read_text.txt")
+ file = File.open(path, "r+")
+ @buffer = IO::Buffer.map(file)
+ @buffer.shared?.should == true
+ ensure
+ @buffer.free
+ file.close
+ end
+
+ it "is false for an unshared buffer" do
+ @buffer = IO::Buffer.new(12)
+ @buffer.shared?.should == false
+ end
+
+ it "is false for a null buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.shared?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/buffer/string_spec.rb b/spec/ruby/core/io/buffer/string_spec.rb
new file mode 100644
index 0000000000..4c73ba5e1e
--- /dev/null
+++ b/spec/ruby/core/io/buffer/string_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer.string" do
+ it "creates a modifiable buffer for the duration of the block" do
+ IO::Buffer.string(7) do |buffer|
+ @buffer = buffer
+
+ buffer.size.should == 7
+ buffer.get_string.should == "\0\0\0\0\0\0\0".b
+
+ buffer.set_string("test")
+ buffer.get_string.should == "test\0\0\0"
+ end
+ @buffer.should.null?
+ end
+
+ it "returns contents of the buffer as a binary string" do
+ string =
+ IO::Buffer.string(7) do |buffer|
+ buffer.set_string("ä test")
+ end
+ string.should == "\xC3\xA4 test".b
+ end
+
+ it "creates an external buffer" do
+ IO::Buffer.string(8) do |buffer|
+ buffer.should_not.internal?
+ buffer.should_not.mapped?
+ buffer.should.external?
+
+ buffer.should_not.empty?
+ buffer.should_not.null?
+
+ buffer.should_not.shared?
+ buffer.should_not.private?
+ buffer.should_not.readonly?
+
+ buffer.should_not.locked?
+ buffer.should.valid?
+ end
+ end
+
+ it "returns an empty string if size is 0" do
+ string =
+ IO::Buffer.string(0) do |buffer|
+ buffer.size.should == 0
+ end
+ string.should == ""
+ end
+
+ it "raises ArgumentError if size is negative" do
+ -> { IO::Buffer.string(-1) {} }.should.raise(ArgumentError, "negative string size (or size too big)")
+ end
+
+ it "raises RangeError if size is too large" do
+ -> { IO::Buffer.string(2 ** 232) {} }.should.raise(RangeError, /\Abignum too big to convert into [`']long'\z/)
+ end
+
+ it "raises LocalJumpError if no block is given" do
+ -> { IO::Buffer.string(7) }.should.raise(LocalJumpError, "no block given")
+ end
+end
diff --git a/spec/ruby/core/io/buffer/transfer_spec.rb b/spec/ruby/core/io/buffer/transfer_spec.rb
new file mode 100644
index 0000000000..3bc08998dd
--- /dev/null
+++ b/spec/ruby/core/io/buffer/transfer_spec.rb
@@ -0,0 +1,117 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#transfer" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ context "with a buffer created with .new" do
+ it "transfers internal memory to a new buffer, nullifying the original" do
+ buffer = IO::Buffer.new(4)
+ info = buffer.to_s
+ @buffer = buffer.transfer
+ @buffer.to_s.should == info
+ buffer.null?.should == true
+ end
+
+ it "transfers mapped memory to a new buffer, nullifying the original" do
+ buffer = IO::Buffer.new(4, IO::Buffer::MAPPED)
+ info = buffer.to_s
+ @buffer = buffer.transfer
+ @buffer.to_s.should == info
+ buffer.null?.should == true
+ end
+ end
+
+ context "with a file-backed buffer created with .map" do
+ it "transfers mapped memory to a new buffer, nullifying the original" do
+ File.open(__FILE__, "r") do |file|
+ buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)
+ info = buffer.to_s
+ @buffer = buffer.transfer
+ @buffer.to_s.should == info
+ buffer.null?.should == true
+ end
+ end
+ end
+
+ context "with a String-backed buffer created with .for" do
+ context "without a block" do
+ it "transfers memory to a new buffer, nullifying the original" do
+ buffer = IO::Buffer.for("test")
+ info = buffer.to_s
+ @buffer = buffer.transfer
+ @buffer.to_s.should == info
+ buffer.null?.should == true
+ end
+ end
+
+ context "with a block" do
+ it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do
+ IO::Buffer.for(+"test") do |buffer|
+ info = buffer.to_s
+ @buffer = buffer.transfer
+ @buffer.to_s.should == info
+ buffer.null?.should == true
+ end
+ @buffer.null?.should == false
+ end
+ end
+ end
+
+ context "with a String-backed buffer created with .string" do
+ it "transfers memory to a new buffer, breaking the transaction by nullifying the original" do
+ IO::Buffer.string(4) do |buffer|
+ info = buffer.to_s
+ @buffer = buffer.transfer
+ @buffer.to_s.should == info
+ buffer.null?.should == true
+ end
+ @buffer.null?.should == false
+ end
+ end
+
+ it "allows multiple transfers" do
+ buffer_1 = IO::Buffer.new(4)
+ buffer_2 = buffer_1.transfer
+ @buffer = buffer_2.transfer
+ buffer_1.null?.should == true
+ buffer_2.null?.should == true
+ @buffer.null?.should == false
+ end
+
+ it "is disallowed while locked, raising IO::Buffer::LockedError" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.locked do
+ -> { @buffer.transfer }.should.raise(IO::Buffer::LockedError, "Cannot transfer ownership of locked buffer!")
+ end
+ end
+
+ context "with a slice of a buffer" do
+ it "transfers source to a new slice, not touching the buffer" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.set_string("test")
+ slice = @buffer.slice(0, 2)
+ slice.get_string.should == "te"
+
+ new_slice = slice.transfer
+ slice.null?.should == true
+ new_slice.null?.should == false
+ @buffer.null?.should == false
+
+ new_slice.set_string("ea")
+ @buffer.get_string.should == "east"
+ end
+
+ it "nullifies buffer, invalidating the slice" do
+ buffer = IO::Buffer.new(4)
+ slice = buffer.slice(0, 2)
+ @buffer = buffer.transfer
+
+ slice.null?.should == false
+ slice.valid?.should == false
+ -> { slice.get_string }.should.raise(IO::Buffer::InvalidatedError, "Buffer has been invalidated!")
+ end
+ end
+end
diff --git a/spec/ruby/core/io/buffer/valid_spec.rb b/spec/ruby/core/io/buffer/valid_spec.rb
new file mode 100644
index 0000000000..b84bdd0cfd
--- /dev/null
+++ b/spec/ruby/core/io/buffer/valid_spec.rb
@@ -0,0 +1,99 @@
+require_relative '../../../spec_helper'
+
+describe "IO::Buffer#valid?" do
+ after :each do
+ @buffer&.free
+ @buffer = nil
+ end
+
+ # Non-slices are always valid
+ context "with a non-slice buffer" do
+ it "is true for a regular buffer" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.valid?.should == true
+ end
+
+ it "is true for a 0-size buffer" do
+ @buffer = IO::Buffer.new(0)
+ @buffer.valid?.should == true
+ end
+
+ it "is true for a freed buffer" do
+ @buffer = IO::Buffer.new(4)
+ @buffer.free
+ @buffer.valid?.should == true
+ end
+
+ it "is true for a freed file-backed buffer" do
+ File.open(__FILE__, "r") do |file|
+ @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)
+ @buffer.valid?.should == true
+ @buffer.free
+ @buffer.valid?.should == true
+ end
+ end
+
+ it "is true for a freed string-backed buffer" do
+ @buffer = IO::Buffer.for("hello")
+ @buffer.valid?.should == true
+ @buffer.free
+ @buffer.valid?.should == true
+ end
+ end
+
+ # "A buffer becomes invalid if it is a slice of another buffer (or string)
+ # which has been freed or re-allocated at a different address."
+ context "with a slice" do
+ it "is true for a slice of a live buffer" do
+ @buffer = IO::Buffer.new(4)
+ slice = @buffer.slice(0, 2)
+ slice.valid?.should == true
+ end
+
+ context "when buffer is resized" do
+ it "is false when slice becomes outside the buffer" do
+ @buffer = IO::Buffer.new(4)
+ slice = @buffer.slice(2, 2)
+ @buffer.resize(3)
+ slice.valid?.should == false
+ end
+ end
+
+ it "is false for a slice of a transferred buffer" do
+ buffer = IO::Buffer.new(4)
+ slice = buffer.slice(0, 2)
+ @buffer = buffer.transfer
+ slice.valid?.should == false
+ end
+
+ it "is false for a slice of a freed buffer" do
+ @buffer = IO::Buffer.new(4)
+ slice = @buffer.slice(0, 2)
+ @buffer.free
+ slice.valid?.should == false
+ end
+
+ it "is false for a slice of a freed file-backed buffer" do
+ File.open(__FILE__, "r") do |file|
+ @buffer = IO::Buffer.map(file, nil, 0, IO::Buffer::READONLY)
+ slice = @buffer.slice(0, 2)
+ slice.valid?.should == true
+ @buffer.free
+ slice.valid?.should == false
+ end
+ end
+
+ it "is true for a slice of a freed string-backed buffer while string is alive" do
+ @buffer = IO::Buffer.for("alive")
+ slice = @buffer.slice(0, 2)
+ slice.valid?.should == true
+ @buffer.free
+ slice.valid?.should == true
+ end
+
+ # There probably should be a test with a garbage-collected string,
+ # but it's not clear how to force that.
+
+ it "needs to be reviewed for spec completeness"
+ end
+end
diff --git a/spec/ruby/core/io/buffer/xor_spec.rb b/spec/ruby/core/io/buffer/xor_spec.rb
new file mode 100644
index 0000000000..9287611f7f
--- /dev/null
+++ b/spec/ruby/core/io/buffer/xor_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../../spec_helper'
+
+describe :io_buffer_xor, shared: true do
+ it "applies the argument buffer as an XOR bit mask across the whole buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer.send(@method, mask)
+ result.get_string.should == "\xC9\xBD\xCB\xBB\xCD".b
+ result.free
+ end
+ end
+ end
+
+ it "ignores extra parts of mask if it is longer than source buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F\x00\x00\x00\xFF\xFF") do |mask|
+ result = buffer.send(@method, mask)
+ result.get_string.should == "\xC9\xBD345".b
+ result.free
+ end
+ end
+ end
+
+ it "raises TypeError if mask is not an IO::Buffer" do
+ IO::Buffer.for(+"12345") do |buffer|
+ -> { buffer.send(@method, "\xF8\x8F") }.should.raise(TypeError, "wrong argument type String (expected IO::Buffer)")
+ -> { buffer.send(@method, 0xF8) }.should.raise(TypeError, "wrong argument type Integer (expected IO::Buffer)")
+ -> { buffer.send(@method, nil) }.should.raise(TypeError, "wrong argument type nil (expected IO::Buffer)")
+ end
+ end
+end
+
+describe "IO::Buffer#^" do
+ it_behaves_like :io_buffer_xor, :^
+
+ it "creates a new internal buffer of the same size" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer ^ mask
+ result.should_not.equal? buffer
+ result.should.internal?
+ result.size.should == buffer.size
+ result.free
+ buffer.get_string.should == "12345".b
+ end
+ end
+ end
+end
+
+describe "IO::Buffer#xor!" do
+ it_behaves_like :io_buffer_xor, :xor!
+
+ it "modifies the buffer in place" do
+ IO::Buffer.for(+"12345") do |buffer|
+ IO::Buffer.for(+"\xF8\x8F") do |mask|
+ result = buffer.xor!(mask)
+ result.should.equal? buffer
+ result.should.external?
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/close_on_exec_spec.rb b/spec/ruby/core/io/close_on_exec_spec.rb
new file mode 100644
index 0000000000..28cdb967b9
--- /dev/null
+++ b/spec/ruby/core/io/close_on_exec_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+
+describe "IO#close_on_exec=" do
+ before :each do
+ @name = tmp('io_close_on_exec.txt')
+ @io = new_io @name
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ guard -> { platform_is_not :windows } do
+ it "sets the close-on-exec flag if true" do
+ @io.close_on_exec = true
+ @io.should.close_on_exec?
+ end
+
+ it "sets the close-on-exec flag if non-false" do
+ @io.close_on_exec = :true
+ @io.should.close_on_exec?
+ end
+
+ it "unsets the close-on-exec flag if false" do
+ @io.close_on_exec = true
+ @io.close_on_exec = false
+ @io.should_not.close_on_exec?
+ end
+
+ it "unsets the close-on-exec flag if nil" do
+ @io.close_on_exec = true
+ @io.close_on_exec = nil
+ @io.should_not.close_on_exec?
+ end
+
+ it "ensures the IO's file descriptor is closed in exec'ed processes" do
+ require 'fcntl'
+ @io.close_on_exec = true
+ (@io.fcntl(Fcntl::F_GETFD) & Fcntl::FD_CLOEXEC).should == Fcntl::FD_CLOEXEC
+ end
+
+ it "raises IOError if called on a closed IO" do
+ @io.close
+ -> { @io.close_on_exec = true }.should.raise(IOError)
+ end
+ end
+end
+
+describe "IO#close_on_exec?" do
+ before :each do
+ @name = tmp('io_is_close_on_exec.txt')
+ @io = new_io @name
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ guard -> { platform_is_not :windows } do
+ it "returns true by default" do
+ @io.should.close_on_exec?
+ end
+
+ it "returns true if set" do
+ @io.close_on_exec = true
+ @io.should.close_on_exec?
+ end
+
+ it "raises IOError if called on a closed IO" do
+ @io.close
+ -> { @io.close_on_exec? }.should.raise(IOError)
+ end
+ end
+end
diff --git a/spec/ruby/core/io/close_read_spec.rb b/spec/ruby/core/io/close_read_spec.rb
new file mode 100644
index 0000000000..c505289d72
--- /dev/null
+++ b/spec/ruby/core/io/close_read_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#close_read" do
+
+ before :each do
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ @io = IO.popen cmd, "r+"
+ @path = tmp('io.close.txt')
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @path
+ end
+
+ it "closes the read end of a duplex I/O stream" do
+ @io.close_read
+
+ -> { @io.read }.should.raise(IOError)
+ end
+
+ it "does nothing on subsequent invocations" do
+ @io.close_read
+
+ @io.close_read.should == nil
+ end
+
+ it "allows subsequent invocation of close" do
+ @io.close_read
+
+ -> { @io.close }.should_not.raise
+ end
+
+ it "raises an IOError if the stream is writable and not duplexed" do
+ io = File.open @path, 'w'
+
+ begin
+ -> { io.close_read }.should.raise(IOError)
+ ensure
+ io.close unless io.closed?
+ end
+ end
+
+ it "closes the stream if it is neither writable nor duplexed" do
+ io_close_path = @path
+ touch io_close_path
+
+ io = File.open io_close_path
+
+ io.close_read
+
+ io.should.closed?
+ end
+
+ it "does nothing on closed stream" do
+ @io.close
+
+ @io.close_read.should == nil
+ end
+end
diff --git a/spec/ruby/core/io/close_spec.rb b/spec/ruby/core/io/close_spec.rb
new file mode 100644
index 0000000000..afd84ba101
--- /dev/null
+++ b/spec/ruby/core/io/close_spec.rb
@@ -0,0 +1,118 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#close" do
+ before :each do
+ @name = tmp('io_close.txt')
+ @io = new_io @name
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "closes the stream" do
+ @io.close
+ @io.should.closed?
+ end
+
+ it "returns nil" do
+ @io.close.should == nil
+ end
+
+ it "raises an IOError reading from a closed IO" do
+ @io.close
+ -> { @io.read }.should.raise(IOError)
+ end
+
+ it "raises an IOError writing to a closed IO" do
+ @io.close
+ -> { @io.write "data" }.should.raise(IOError)
+ end
+
+ it 'does not close the stream if autoclose is false' do
+ other_io = IO.new(@io.fileno)
+ other_io.autoclose = false
+ other_io.close
+ -> { @io.write "data" }.should_not.raise(IOError)
+ end
+
+ it "does nothing if already closed" do
+ @io.close
+
+ @io.close.should == nil
+ end
+
+ it "does not call the #flush method but flushes the stream internally" do
+ @io.should_not_receive(:flush)
+ @io.close
+ @io.should.closed?
+ end
+
+ it 'raises an IOError with a clear message' do
+ matching_exception = nil
+
+ -> do
+ IOSpecs::THREAD_CLOSE_RETRIES.times do
+ read_io, write_io = IO.pipe
+ going_to_read = false
+
+ thread = Thread.new do
+ begin
+ going_to_read = true
+ read_io.read
+ rescue IOError => ioe
+ if ioe.message == IOSpecs::THREAD_CLOSE_ERROR_MESSAGE
+ matching_exception = ioe
+ end
+ # try again
+ end
+ end
+
+ # best attempt to ensure the thread is actually blocked on read
+ Thread.pass until going_to_read && thread.stop?
+ sleep(0.001)
+
+ read_io.close
+ thread.join
+ write_io.close
+
+ matching_exception&.tap {|ex| raise ex}
+ end
+ end.should.raise(IOError, IOSpecs::THREAD_CLOSE_ERROR_MESSAGE)
+ end
+end
+
+describe "IO#close on an IO.popen stream" do
+
+ it "clears #pid" do
+ io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r'
+
+ io.pid.should_not == 0
+
+ io.close
+
+ -> { io.pid }.should.raise(IOError)
+ end
+
+ it "sets $?" do
+ io = IO.popen ruby_cmd('exit 0'), 'r'
+ io.close
+
+ $?.exitstatus.should == 0
+
+ io = IO.popen ruby_cmd('exit 1'), 'r'
+ io.close
+
+ $?.exitstatus.should == 1
+ end
+
+ it "waits for the child to exit" do
+ io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r'
+ io.close
+
+ $?.exitstatus.should_not == 0
+ end
+
+end
diff --git a/spec/ruby/core/io/close_write_spec.rb b/spec/ruby/core/io/close_write_spec.rb
new file mode 100644
index 0000000000..60b41505c3
--- /dev/null
+++ b/spec/ruby/core/io/close_write_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#close_write" do
+ before :each do
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ @io = IO.popen cmd, 'r+'
+ @path = tmp('io.close.txt')
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @path
+ end
+
+ it "closes the write end of a duplex I/O stream" do
+ @io.close_write
+
+ -> { @io.write "attempt to write" }.should.raise(IOError)
+ end
+
+ it "does nothing on subsequent invocations" do
+ @io.close_write
+
+ @io.close_write.should == nil
+ end
+
+ it "allows subsequent invocation of close" do
+ @io.close_write
+
+ -> { @io.close }.should_not.raise
+ end
+
+ it "raises an IOError if the stream is readable and not duplexed" do
+ io = File.open @path, 'w+'
+
+ begin
+ -> { io.close_write }.should.raise(IOError)
+ ensure
+ io.close unless io.closed?
+ end
+ end
+
+ it "closes the stream if it is neither readable nor duplexed" do
+ io = File.open @path, 'w'
+
+ io.close_write
+
+ io.should.closed?
+ end
+
+ # Windows didn't have command like cat
+ platform_is_not :windows do
+ it "flushes and closes the write stream" do
+ @io.puts '12345'
+
+ @io.close_write
+
+ @io.read.should == "12345\n"
+ end
+ end
+
+ it "does nothing on closed stream" do
+ @io.close
+
+ @io.close_write.should == nil
+ end
+end
diff --git a/spec/ruby/core/io/closed_spec.rb b/spec/ruby/core/io/closed_spec.rb
new file mode 100644
index 0000000000..1f10858e28
--- /dev/null
+++ b/spec/ruby/core/io/closed_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#closed?" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close
+ end
+
+ it "returns true on closed stream" do
+ IOSpecs.closed_io.closed?.should == true
+ end
+
+ it "returns false on open stream" do
+ @io.closed?.should == false
+ end
+end
diff --git a/spec/ruby/core/io/constants_spec.rb b/spec/ruby/core/io/constants_spec.rb
new file mode 100644
index 0000000000..f9dccd08b9
--- /dev/null
+++ b/spec/ruby/core/io/constants_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "IO::SEEK_SET" do
+ it "is defined" do
+ IO.const_defined?(:SEEK_SET).should == true
+ end
+end
+
+describe "IO::SEEK_CUR" do
+ it "is defined" do
+ IO.const_defined?(:SEEK_CUR).should == true
+ end
+end
+
+describe "IO::SEEK_END" do
+ it "is defined" do
+ IO.const_defined?(:SEEK_END).should == true
+ end
+end
diff --git a/spec/ruby/core/io/copy_stream_spec.rb b/spec/ruby/core/io/copy_stream_spec.rb
new file mode 100644
index 0000000000..31383f9b0f
--- /dev/null
+++ b/spec/ruby/core/io/copy_stream_spec.rb
@@ -0,0 +1,351 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :io_copy_stream_to_file, shared: true do
+ it "copies the entire IO contents to the file" do
+ IO.copy_stream(@object.from, @to_name)
+ File.read(@to_name).should == @content
+ IO.copy_stream(@from_bigfile, @to_name)
+ File.read(@to_name).should == @content_bigfile
+ end
+
+ it "returns the number of bytes copied" do
+ IO.copy_stream(@object.from, @to_name).should == @size
+ IO.copy_stream(@from_bigfile, @to_name).should == @size_bigfile
+ end
+
+ it "copies only length bytes when specified" do
+ IO.copy_stream(@object.from, @to_name, 8).should == 8
+ File.read(@to_name).should == "Line one"
+ end
+
+ it "calls #to_path to convert on object to a file name" do
+ obj = mock("io_copy_stream_to")
+ obj.should_receive(:to_path).and_return(@to_name)
+
+ IO.copy_stream(@object.from, obj)
+ File.read(@to_name).should == @content
+ end
+
+ it "raises a TypeError if #to_path does not return a String" do
+ obj = mock("io_copy_stream_to")
+ obj.should_receive(:to_path).and_return(1)
+
+ -> { IO.copy_stream(@object.from, obj) }.should.raise(TypeError)
+ end
+end
+
+describe :io_copy_stream_to_file_with_offset, shared: true do
+ platform_is_not :windows do
+ it "copies only length bytes from the offset" do
+ IO.copy_stream(@object.from, @to_name, 8, 4).should == 8
+ File.read(@to_name).should == " one\n\nLi"
+ end
+ end
+end
+
+describe :io_copy_stream_to_io, shared: true do
+ it "copies the entire IO contents to the IO" do
+ IO.copy_stream(@object.from, @to_io)
+ File.read(@to_name).should == @content
+ IO.copy_stream(@from_bigfile, @to_io)
+ File.read(@to_name).should == (@content + @content_bigfile)
+ end
+
+ it "returns the number of bytes copied" do
+ IO.copy_stream(@object.from, @to_io).should == @size
+ IO.copy_stream(@from_bigfile, @to_io).should == @size_bigfile
+ end
+
+ it "starts writing at the destination IO's current position" do
+ @to_io.write("prelude ")
+ IO.copy_stream(@object.from, @to_io)
+ File.read(@to_name).should == ("prelude " + @content)
+ end
+
+ it "leaves the destination IO position at the last write" do
+ IO.copy_stream(@object.from, @to_io)
+ @to_io.pos.should == @size
+ end
+
+ it "raises an IOError if the destination IO is not open for writing" do
+ to_io = new_io __FILE__, "r"
+ begin
+ -> { IO.copy_stream @object.from, to_io }.should.raise(IOError)
+ ensure
+ to_io.close
+ end
+ end
+
+ it "does not close the destination IO" do
+ IO.copy_stream(@object.from, @to_io)
+ @to_io.closed?.should == false
+ end
+
+ it "copies only length bytes when specified" do
+ IO.copy_stream(@object.from, @to_io, 8).should == 8
+ File.read(@to_name).should == "Line one"
+ end
+end
+
+describe :io_copy_stream_to_io_with_offset, shared: true do
+ platform_is_not :windows do
+ it "copies only length bytes from the offset" do
+ IO.copy_stream(@object.from, @to_io, 8, 4).should == 8
+ File.read(@to_name).should == " one\n\nLi"
+ end
+ end
+end
+
+describe "IO.copy_stream" do
+ before :each do
+ @from_name = fixture __FILE__, "copy_stream.txt"
+ @to_name = tmp("io_copy_stream_io_name")
+
+ @content = IO.read(@from_name)
+ @size = @content.size
+
+ @from_bigfile = tmp("io_copy_stream_bigfile")
+ @content_bigfile = "A" * 17_000
+ touch(@from_bigfile){|f| f.print @content_bigfile }
+ @size_bigfile = @content_bigfile.size
+ end
+
+ after :each do
+ rm_r @to_name if @to_name
+ rm_r @from_bigfile
+ end
+
+ describe "from an IO" do
+ before :each do
+ @from_io = new_io @from_name, "rb"
+ IOSpecs::CopyStream.from = @from_io
+ end
+
+ after :each do
+ @from_io.close
+ end
+
+ it "raises an IOError if the source IO is not open for reading" do
+ @from_io.close
+ @from_io = new_io @from_bigfile, "a"
+ -> { IO.copy_stream @from_io, @to_name }.should.raise(IOError)
+ end
+
+ it "does not close the source IO" do
+ IO.copy_stream(@from_io, @to_name)
+ @from_io.closed?.should == false
+ end
+
+ platform_is_not :windows do
+ it "does not change the IO offset when an offset is specified" do
+ @from_io.pos = 10
+ IO.copy_stream(@from_io, @to_name, 8, 4)
+ @from_io.pos.should == 10
+ end
+ end
+
+ it "does change the IO offset when an offset is not specified" do
+ @from_io.pos = 10
+ IO.copy_stream(@from_io, @to_name)
+ @from_io.pos.should == 42
+ end
+
+ describe "to a file name" do
+ it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream
+ it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream
+ end
+
+ describe "to an IO" do
+ before :each do
+ @to_io = new_io @to_name, "wb"
+ end
+
+ after :each do
+ @to_io.close
+ end
+
+ it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
+ it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
+ end
+
+ describe "to a Tempfile" do
+ before :all do
+ require 'tempfile'
+ end
+
+ before :each do
+ @to_io = Tempfile.new("rubyspec_copy_stream", encoding: Encoding::BINARY, mode: File::RDONLY)
+ @to_name = @to_io.path
+ end
+
+ after :each do
+ @to_io.close!
+ @to_name = nil # do not rm_r it, already done by Tempfile#close!
+ end
+
+ it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
+ it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
+ end
+ end
+
+ describe "from a file name" do
+ before :each do
+ IOSpecs::CopyStream.from = @from_name
+ end
+
+ it "calls #to_path to convert on object to a file name" do
+ obj = mock("io_copy_stream_from")
+ obj.should_receive(:to_path).and_return(@from_name)
+
+ IO.copy_stream(obj, @to_name)
+ File.read(@to_name).should == @content
+ end
+
+ it "raises a TypeError if #to_path does not return a String" do
+ obj = mock("io_copy_stream_from")
+ obj.should_receive(:to_path).and_return(1)
+
+ -> { IO.copy_stream(obj, @to_name) }.should.raise(TypeError)
+ end
+
+ describe "to a file name" do
+ it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream
+ it_behaves_like :io_copy_stream_to_file_with_offset, nil, IOSpecs::CopyStream
+ end
+
+ describe "to an IO" do
+ before :each do
+ @to_io = new_io @to_name, "wb"
+ end
+
+ after :each do
+ @to_io.close
+ end
+
+ it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
+ it_behaves_like :io_copy_stream_to_io_with_offset, nil, IOSpecs::CopyStream
+ end
+ end
+
+ describe "from a pipe IO" do
+ before :each do
+ @from_io = IOSpecs.pipe_fixture(@content)
+ IOSpecs::CopyStream.from = @from_io
+ end
+
+ after :each do
+ @from_io.close
+ end
+
+ it "does not close the source IO" do
+ IO.copy_stream(@from_io, @to_name)
+ @from_io.closed?.should == false
+ end
+
+ platform_is_not :windows do
+ it "raises an error when an offset is specified" do
+ -> { IO.copy_stream(@from_io, @to_name, 8, 4) }.should.raise(Errno::ESPIPE)
+ end
+ end
+
+ describe "to a file name" do
+ it_behaves_like :io_copy_stream_to_file, nil, IOSpecs::CopyStream
+ end
+
+ describe "to an IO" do
+ before :each do
+ @to_io = new_io @to_name, "wb"
+ end
+
+ after :each do
+ @to_io.close
+ end
+
+ it_behaves_like :io_copy_stream_to_io, nil, IOSpecs::CopyStream
+ end
+ end
+
+ describe "with non-IO Objects" do
+ before do
+ @io = new_io @from_name, "rb"
+ end
+
+ after do
+ @io.close unless @io.closed?
+ end
+
+ it "calls #readpartial on the source Object if defined" do
+ from = IOSpecs::CopyStreamReadPartial.new @io
+
+ IO.copy_stream(from, @to_name)
+ File.read(@to_name).should == @content
+ end
+
+ it "calls #read on the source Object" do
+ from = IOSpecs::CopyStreamRead.new @io
+
+ IO.copy_stream(from, @to_name)
+ File.read(@to_name).should == @content
+ end
+
+ it "calls #write on the destination Object" do
+ to = mock("io_copy_stream_to_object")
+ to.should_receive(:write).with(@content).and_return(@content.size)
+
+ IO.copy_stream(@from_name, to)
+ end
+
+ it "does not call #pos on the source if no offset is given" do
+ @io.should_not_receive(:pos)
+ IO.copy_stream(@io, @to_name)
+ end
+
+ it "does not call #read on the source or #write on the destination if zero length is given" do
+ from = mock("io_copy_stream_to_object_zero_length_read")
+ to = mock("io_copy_stream_to_object_zero_length_write")
+ from.should_not_receive(:read)
+ to.should_not_receive(:write)
+ IO.copy_stream(from, to, 0)
+ end
+ end
+
+ describe "with a destination that does partial reads" do
+ before do
+ @from_out, @from_in = IO.pipe
+ @to_out, @to_in = IO.pipe
+ end
+
+ after do
+ [@from_out, @from_in, @to_out, @to_in].each {|io| io.close rescue nil}
+ end
+
+ it "calls #write repeatedly on the destination Object" do
+ @from_in.write "1234"
+ @from_in.close
+
+ th = Thread.new do
+ IO.copy_stream(@from_out, @to_in)
+ end
+
+ copied = ""
+ 4.times do
+ copied += @to_out.read(1)
+ end
+
+ th.join
+
+ copied.should == "1234"
+ end
+
+ end
+end
+
+describe "IO.copy_stream" do
+ it "does not use buffering when writing to STDOUT" do
+ IO.popen([*ruby_exe, fixture(__FILE__ , "copy_in_out.rb")], "r+") do |io|
+ io.write("bar")
+ io.read(3).should == "bar"
+ end
+ end
+end
diff --git a/spec/ruby/core/io/dup_spec.rb b/spec/ruby/core/io/dup_spec.rb
new file mode 100644
index 0000000000..db4e9fe641
--- /dev/null
+++ b/spec/ruby/core/io/dup_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#dup" do
+ before :each do
+ @file = tmp("spec_io_dup")
+ @f = File.open @file, 'w+'
+ @i = @f.dup
+
+ @f.sync = true
+ @i.sync = true
+ end
+
+ after :each do
+ @i.close if @i && !@i.closed?
+ @f.close if @f && !@f.closed?
+ rm_r @file
+ end
+
+ it "returns a new IO instance" do
+ @i.class.should == @f.class
+ end
+
+ it "sets a new descriptor on the returned object" do
+ @i.fileno.should_not == @f.fileno
+ end
+
+ quarantine! do # This does not appear to be consistent across platforms
+ it "shares the original stream between the two IOs" do
+ start = @f.pos
+ @i.pos.should == start
+
+ s = "Hello, wo.. wait, where am I?\n"
+ s2 = "<evil voice> Muhahahaa!"
+
+ @f.write s
+ @i.pos.should == @f.pos
+
+ @i.rewind
+ @i.gets.should == s
+
+ @i.rewind
+ @i.write s2
+
+ @f.rewind
+ @f.gets.should == "#{s2}\n"
+ end
+ end
+
+ it "allows closing the new IO without affecting the original" do
+ @i.close
+ -> { @f.gets }.should_not.raise(Exception)
+
+ @i.should.closed?
+ @f.should_not.closed?
+ end
+
+ it "allows closing the original IO without affecting the new one" do
+ @f.close
+ -> { @i.gets }.should_not.raise(Exception)
+
+ @i.should_not.closed?
+ @f.should.closed?
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.dup }.should.raise(IOError)
+ end
+
+ it "always sets the close-on-exec flag for the new IO object" do
+ @f.close_on_exec = true
+ dup = @f.dup
+ begin
+ dup.should.close_on_exec?
+ ensure
+ dup.close
+ end
+
+ @f.close_on_exec = false
+ dup = @f.dup
+ begin
+ dup.should.close_on_exec?
+ ensure
+ dup.close
+ end
+ end
+
+ it "always sets the autoclose flag for the new IO object" do
+ @f.autoclose = true
+ dup = @f.dup
+ begin
+ dup.should.autoclose?
+ ensure
+ dup.close
+ end
+
+ @f.autoclose = false
+ dup = @f.dup
+ begin
+ dup.should.autoclose?
+ ensure
+ dup.close
+ @f.autoclose = true
+ end
+ end
+end
diff --git a/spec/ruby/core/io/each_byte_spec.rb b/spec/ruby/core/io/each_byte_spec.rb
new file mode 100644
index 0000000000..fe299f0fba
--- /dev/null
+++ b/spec/ruby/core/io/each_byte_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#each_byte" do
+ before :each do
+ ScratchPad.record []
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.each_byte {} }.should.raise(IOError)
+ end
+
+ it "yields each byte" do
+ count = 0
+ @io.each_byte do |byte|
+ ScratchPad << byte
+ break if 4 < count += 1
+ end
+
+ ScratchPad.recorded.should == [86, 111, 105, 99, 105]
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ enum = @io.each_byte
+ enum.should.instance_of?(Enumerator)
+ enum.first(5).should == [86, 111, 105, 99, 105]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @io.each_byte.size.should == nil
+ end
+ end
+ end
+ end
+end
+
+describe "IO#each_byte" do
+ before :each do
+ @io = IOSpecs.io_fixture "empty.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "returns self on an empty stream" do
+ @io.each_byte { |b| }.should.equal?(@io)
+ end
+end
diff --git a/spec/ruby/core/io/each_char_spec.rb b/spec/ruby/core/io/each_char_spec.rb
new file mode 100644
index 0000000000..5d460a1e7c
--- /dev/null
+++ b/spec/ruby/core/io/each_char_spec.rb
@@ -0,0 +1,12 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/chars'
+
+describe "IO#each_char" do
+ it_behaves_like :io_chars, :each_char
+end
+
+describe "IO#each_char" do
+ it_behaves_like :io_chars_empty, :each_char
+end
diff --git a/spec/ruby/core/io/each_codepoint_spec.rb b/spec/ruby/core/io/each_codepoint_spec.rb
new file mode 100644
index 0000000000..26cc87fc0e
--- /dev/null
+++ b/spec/ruby/core/io/each_codepoint_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/codepoints'
+
+# See redmine #1667
+describe "IO#each_codepoint" do
+ it_behaves_like :io_codepoints, :each_codepoint
+end
+
+describe "IO#each_codepoint" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "calls the given block" do
+ r = []
+ @io.each_codepoint { |c| r << c }
+ r[24].should == 232
+ r.last.should == 10
+ end
+
+ it "returns self" do
+ @io.each_codepoint { |l| l }.should.equal?(@io)
+ end
+end
+
+describe "IO#each_codepoint" do
+ before :each do
+ @io = IOSpecs.io_fixture("incomplete.txt")
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "raises an exception at incomplete character before EOF when conversion takes place" do
+ -> { @io.each_codepoint {} }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/io/each_line_spec.rb b/spec/ruby/core/io/each_line_spec.rb
new file mode 100644
index 0000000000..58d26b325d
--- /dev/null
+++ b/spec/ruby/core/io/each_line_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/each'
+
+describe "IO#each_line" do
+ it_behaves_like :io_each, :each_line
+end
+
+describe "IO#each_line" do
+ it_behaves_like :io_each_default_separator, :each_line
+end
diff --git a/spec/ruby/core/io/each_spec.rb b/spec/ruby/core/io/each_spec.rb
new file mode 100644
index 0000000000..91ecbd19c8
--- /dev/null
+++ b/spec/ruby/core/io/each_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/each'
+
+describe "IO#each" do
+ it_behaves_like :io_each, :each
+end
+
+describe "IO#each" do
+ it_behaves_like :io_each_default_separator, :each
+end
diff --git a/spec/ruby/core/io/eof_spec.rb b/spec/ruby/core/io/eof_spec.rb
new file mode 100644
index 0000000000..c8955abde0
--- /dev/null
+++ b/spec/ruby/core/io/eof_spec.rb
@@ -0,0 +1,107 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#eof?" do
+ before :each do
+ @name = tmp("empty.txt")
+ touch @name
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "returns true on an empty stream that has just been opened" do
+ File.open(@name) { |empty| empty.should.eof? }
+ end
+
+ it "raises IOError on stream not opened for reading" do
+ -> do
+ File.open(@name, "w") { |f| f.eof? }
+ end.should.raise(IOError)
+ end
+end
+
+describe "IO#eof?" do
+ before :each do
+ @name = fixture __FILE__, "lines.txt"
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io && !@io.closed?
+ end
+
+ it "returns false when not at end of file" do
+ @io.read 1
+ @io.should_not.eof?
+ end
+
+ it "returns true after reading with read with no parameters" do
+ @io.read()
+ @io.should.eof?
+ end
+
+ it "returns true after reading with read" do
+ @io.read(File.size(@name))
+ @io.should.eof?
+ end
+
+ it "returns true after reading with sysread" do
+ @io.sysread(File.size(@name))
+ @io.should.eof?
+ end
+
+ it "returns true after reading with readlines" do
+ @io.readlines
+ @io.should.eof?
+ end
+
+ it "returns false on just opened non-empty stream" do
+ @io.should_not.eof?
+ end
+
+ it "does not consume the data from the stream" do
+ @io.should_not.eof?
+ @io.getc.should == 'V'
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.eof? }.should.raise(IOError)
+ end
+
+ it "raises IOError on stream closed for reading by close_read" do
+ @io.close_read
+ -> { @io.eof? }.should.raise(IOError)
+ end
+
+ it "returns true on one-byte stream after single-byte read" do
+ File.open(__dir__ + '/fixtures/one_byte.txt') { |one_byte|
+ one_byte.read(1)
+ one_byte.should.eof?
+ }
+ end
+end
+
+describe "IO#eof?" do
+ after :each do
+ @r.close if @r && !@r.closed?
+ @w.close if @w && !@w.closed?
+ end
+
+ it "returns true on receiving side of Pipe when writing side is closed" do
+ @r, @w = IO.pipe
+ @w.close
+ @r.should.eof?
+ end
+
+ it "returns false on receiving side of Pipe when writing side wrote some data" do
+ @r, @w = IO.pipe
+ @w.puts "hello"
+ @r.should_not.eof?
+ @w.close
+ @r.should_not.eof?
+ @r.read
+ @r.should.eof?
+ end
+end
diff --git a/spec/ruby/core/io/external_encoding_spec.rb b/spec/ruby/core/io/external_encoding_spec.rb
new file mode 100644
index 0000000000..72d246cc2b
--- /dev/null
+++ b/spec/ruby/core/io/external_encoding_spec.rb
@@ -0,0 +1,223 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :io_external_encoding_write, shared: true do
+ describe "when Encoding.default_internal is nil" do
+ before :each do
+ Encoding.default_internal = nil
+ end
+
+ it "returns nil" do
+ @io = new_io @name, @object
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should == nil
+ end
+
+ it "returns the external encoding specified when the instance was created" do
+ @io = new_io @name, "#{@object}:ibm866"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns the encoding set by #set_encoding" do
+ @io = new_io @name, "#{@object}:ibm866"
+ @io.set_encoding Encoding::EUC_JP, nil
+ @io.external_encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+
+ describe "when Encoding.default_external != Encoding.default_internal" do
+ before :each do
+ Encoding.default_external = Encoding::IBM437
+ Encoding.default_internal = Encoding::IBM866
+ end
+
+ it "returns the value of Encoding.default_external when the instance was created" do
+ @io = new_io @name, @object
+ Encoding.default_external = Encoding::UTF_8
+ @io.external_encoding.should.equal?(Encoding::IBM437)
+ end
+
+ it "returns the external encoding specified when the instance was created" do
+ @io = new_io @name, "#{@object}:ibm866"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns the encoding set by #set_encoding" do
+ @io = new_io @name, "#{@object}:ibm866"
+ @io.set_encoding Encoding::EUC_JP, nil
+ @io.external_encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+
+ describe "when Encoding.default_external == Encoding.default_internal" do
+ before :each do
+ Encoding.default_external = Encoding::IBM866
+ Encoding.default_internal = Encoding::IBM866
+ end
+
+ it "returns the value of Encoding.default_external when the instance was created" do
+ @io = new_io @name, @object
+ Encoding.default_external = Encoding::UTF_8
+ @io.external_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns the external encoding specified when the instance was created" do
+ @io = new_io @name, "#{@object}:ibm866"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns the encoding set by #set_encoding" do
+ @io = new_io @name, "#{@object}:ibm866"
+ @io.set_encoding Encoding::EUC_JP, nil
+ @io.external_encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+end
+
+describe "IO#external_encoding" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ @name = tmp("io_external_encoding")
+ touch(@name)
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+
+ @io.close if @io
+ rm_r @name
+ end
+
+ it "can be retrieved from a closed stream" do
+ io = IOSpecs.io_fixture("lines.txt", "r")
+ io.close
+ io.external_encoding.should.equal?(Encoding.default_external)
+ end
+
+ describe "with 'r' mode" do
+ describe "when Encoding.default_internal is nil" do
+ before :each do
+ Encoding.default_internal = nil
+ Encoding.default_external = Encoding::IBM866
+ end
+
+ it "returns Encoding.default_external if the external encoding is not set" do
+ @io = new_io @name, "r"
+ @io.external_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns Encoding.default_external when that encoding is changed after the instance is created" do
+ @io = new_io @name, "r"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::IBM437)
+ end
+
+ it "returns the external encoding specified when the instance was created" do
+ @io = new_io @name, "r:utf-8"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "returns the encoding set by #set_encoding" do
+ @io = new_io @name, "r:utf-8"
+ @io.set_encoding Encoding::EUC_JP, nil
+ @io.external_encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+
+ describe "when Encoding.default_external == Encoding.default_internal" do
+ before :each do
+ Encoding.default_external = Encoding::IBM866
+ Encoding.default_internal = Encoding::IBM866
+ end
+
+ it "returns the value of Encoding.default_external when the instance was created" do
+ @io = new_io @name, "r"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns the external encoding specified when the instance was created" do
+ @io = new_io @name, "r:utf-8"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "returns the encoding set by #set_encoding" do
+ @io = new_io @name, "r:utf-8"
+ @io.set_encoding Encoding::EUC_JP, nil
+ @io.external_encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+
+ describe "when Encoding.default_external != Encoding.default_internal" do
+ before :each do
+ Encoding.default_external = Encoding::IBM437
+ Encoding.default_internal = Encoding::IBM866
+ end
+
+
+ it "returns the external encoding specified when the instance was created" do
+ @io = new_io @name, "r:utf-8"
+ Encoding.default_external = Encoding::IBM437
+ @io.external_encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "returns the encoding set by #set_encoding" do
+ @io = new_io @name, "r:utf-8"
+ @io.set_encoding Encoding::EUC_JP, nil
+ @io.external_encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+ end
+
+ describe "with 'rb' mode" do
+ it "returns Encoding::BINARY" do
+ @io = new_io @name, "rb"
+ @io.external_encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "returns the external encoding specified by the mode argument" do
+ @io = new_io @name, "rb:ibm437"
+ @io.external_encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "with 'r+' mode" do
+ it_behaves_like :io_external_encoding_write, nil, "r+"
+ end
+
+ describe "with 'w' mode" do
+ it_behaves_like :io_external_encoding_write, nil, "w"
+ end
+
+ describe "with 'wb' mode" do
+ it "returns Encoding::BINARY" do
+ @io = new_io @name, "wb"
+ @io.external_encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "returns the external encoding specified by the mode argument" do
+ @io = new_io @name, "wb:ibm437"
+ @io.external_encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "with 'w+' mode" do
+ it_behaves_like :io_external_encoding_write, nil, "w+"
+ end
+
+ describe "with 'a' mode" do
+ it_behaves_like :io_external_encoding_write, nil, "a"
+ end
+
+ describe "with 'a+' mode" do
+ it_behaves_like :io_external_encoding_write, nil, "a+"
+ end
+end
diff --git a/spec/ruby/core/io/fcntl_spec.rb b/spec/ruby/core/io/fcntl_spec.rb
new file mode 100644
index 0000000000..be6d06c672
--- /dev/null
+++ b/spec/ruby/core/io/fcntl_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#fcntl" do
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.fcntl(5, 5) }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/fdatasync_spec.rb b/spec/ruby/core/io/fdatasync_spec.rb
new file mode 100644
index 0000000000..6242258ea0
--- /dev/null
+++ b/spec/ruby/core/io/fdatasync_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "IO#fdatasync" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/io/fileno_spec.rb b/spec/ruby/core/io/fileno_spec.rb
new file mode 100644
index 0000000000..22fd68d166
--- /dev/null
+++ b/spec/ruby/core/io/fileno_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#fileno" do
+ it "returns the numeric file descriptor of the given IO object" do
+ $stdout.fileno.should == 1
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.fileno }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt b/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt
new file mode 100644
index 0000000000..c7c42e9de4
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/bom_UTF-16BE.txt
Binary files differ
diff --git a/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt b/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt
new file mode 100644
index 0000000000..53642b6984
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/bom_UTF-16LE.txt
Binary files differ
diff --git a/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt b/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt
new file mode 100644
index 0000000000..c5efe6c278
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/bom_UTF-32BE.txt
Binary files differ
diff --git a/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt b/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt
new file mode 100644
index 0000000000..1168384393
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/bom_UTF-32LE.txt
Binary files differ
diff --git a/spec/ruby/core/io/fixtures/bom_UTF-8.txt b/spec/ruby/core/io/fixtures/bom_UTF-8.txt
new file mode 100644
index 0000000000..ca971bef61
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/bom_UTF-8.txt
@@ -0,0 +1 @@
+UTF-8
diff --git a/spec/ruby/core/io/fixtures/classes.rb b/spec/ruby/core/io/fixtures/classes.rb
new file mode 100644
index 0000000000..204a2a101b
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/classes.rb
@@ -0,0 +1,218 @@
+# -*- encoding: utf-8 -*-
+
+module IOSpecs
+ THREAD_CLOSE_RETRIES = 10
+ THREAD_CLOSE_ERROR_MESSAGE = 'stream closed in another thread'
+
+ class SubIO < IO
+ end
+
+ class SubIOWithRedefinedNew < IO
+ def self.new(...)
+ ScratchPad << :redefined_new_called
+ super
+ end
+
+ def initialize(...)
+ ScratchPad << :call_original_initialize
+ super
+ end
+ end
+
+ def self.collector
+ Proc.new { |x| ScratchPad << x }
+ end
+
+ def self.lines
+ [ "Voici la ligne une.\n",
+ "Qui \303\250 la linea due.\n",
+ "\n",
+ "\n",
+ "Aqu\303\255 est\303\241 la l\303\255nea tres.\n",
+ "Hier ist Zeile vier.\n",
+ "\n",
+ "Est\303\241 aqui a linha cinco.\n",
+ "Here is line six.\n" ]
+ end
+
+ def self.lines_without_newline_characters
+ [ "Voici la ligne une.",
+ "Qui \303\250 la linea due.",
+ "",
+ "",
+ "Aqu\303\255 est\303\241 la l\303\255nea tres.",
+ "Hier ist Zeile vier.",
+ "",
+ "Est\303\241 aqui a linha cinco.",
+ "Here is line six." ]
+ end
+
+ def self.lines_limit
+ [ "Voici la l",
+ "igne une.\n",
+ "Qui è la ",
+ "linea due.",
+ "\n",
+ "\n",
+ "\n",
+ "Aquí está",
+ " la línea",
+ " tres.\n",
+ "Hier ist Z",
+ "eile vier.",
+ "\n",
+ "\n",
+ "Está aqui",
+ " a linha c",
+ "inco.\n",
+ "Here is li",
+ "ne six.\n" ]
+ end
+
+ def self.lines_space_separator_limit
+ [ "Voici ",
+ "la ",
+ "ligne ",
+ "une.\nQui ",
+ "è ",
+ "la ",
+ "linea ",
+ "due.\n\n\nAqu",
+ "í ",
+ "está ",
+ "la ",
+ "línea ",
+ "tres.\nHier",
+ " ",
+ "ist ",
+ "Zeile ",
+ "vier.\n\nEst",
+ "á ",
+ "aqui ",
+ "a ",
+ "linha ",
+ "cinco.\nHer",
+ "e ",
+ "is ",
+ "line ",
+ "six.\n" ]
+ end
+
+ def self.lines_r_separator
+ [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tr",
+ "es.\nHier",
+ " ist Zeile vier",
+ ".\n\nEst\303\241 aqui a linha cinco.\nHer",
+ "e is line six.\n" ]
+ end
+
+ def self.lines_empty_separator
+ [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n",
+ "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\n",
+ "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ]
+ end
+
+ def self.lines_space_separator
+ [ "Voici ", "la ", "ligne ", "une.\nQui ",
+ "\303\250 ", "la ", "linea ", "due.\n\n\nAqu\303\255 ",
+ "est\303\241 ", "la ", "l\303\255nea ", "tres.\nHier ",
+ "ist ", "Zeile ", "vier.\n\nEst\303\241 ", "aqui ", "a ",
+ "linha ", "cinco.\nHere ", "is ", "line ", "six.\n" ]
+ end
+
+ def self.lines_space_separator_without_trailing_spaces
+ [ "Voici", "la", "ligne", "une.\nQui",
+ "\303\250", "la", "linea", "due.\n\n\nAqu\303\255",
+ "est\303\241", "la", "l\303\255nea", "tres.\nHier",
+ "ist", "Zeile", "vier.\n\nEst\303\241", "aqui", "a",
+ "linha", "cinco.\nHere", "is", "line", "six.\n" ]
+ end
+
+ def self.lines_arbitrary_separator
+ [ "Voici la ligne une.\nQui \303\250",
+ " la linea due.\n\n\nAqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\nEst\303\241 aqui a linha cinco.\nHere is line six.\n" ]
+ end
+
+ def self.paragraphs
+ [ "Voici la ligne une.\nQui \303\250 la linea due.\n\n",
+ "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.\n\n",
+ "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ]
+ end
+
+ def self.paragraphs_without_trailing_new_line_characters
+ [ "Voici la ligne une.\nQui \303\250 la linea due.",
+ "Aqu\303\255 est\303\241 la l\303\255nea tres.\nHier ist Zeile vier.",
+ "Est\303\241 aqui a linha cinco.\nHere is line six.\n" ]
+ end
+
+ # Creates an IO instance for an existing fixture file. The
+ # file should obviously not be deleted.
+ def self.io_fixture(name, mode = "r:utf-8")
+ path = fixture __FILE__, name
+ name = path if File.exist? path
+ new_io(name, mode)
+ end
+
+ # Returns a closed instance of IO that was opened to reference
+ # a fixture file (i.e. the IO instance was perfectly valid at
+ # one point but is now closed).
+ def self.closed_io
+ io = io_fixture "lines.txt"
+ io.close
+ io
+ end
+
+ # Creates a pipe-based IO fixture containing the specified
+ # contents
+ def self.pipe_fixture(content)
+ source, sink = IO.pipe
+ sink.write content
+ sink.close
+ source
+ end
+
+ # Defines +method+ on +obj+ using the provided +block+. This
+ # special helper is needed for e.g. IO.open specs to avoid
+ # mock methods preventing IO#close from running.
+ def self.io_mock(obj, method, &block)
+ obj.singleton_class.send(:define_method, method, &block)
+ end
+
+ module CopyStream
+ def self.from=(from)
+ @from = from
+ end
+
+ def self.from
+ @from
+ end
+ end
+
+ class CopyStreamRead
+ def initialize(io)
+ @io = io
+ end
+
+ def read(size, buf)
+ @io.read size, buf
+ end
+
+ def send(*args)
+ raise "send called"
+ end
+ end
+
+ class CopyStreamReadPartial
+ def initialize(io)
+ @io = io
+ end
+
+ def readpartial(size, buf)
+ @io.readpartial size, buf
+ end
+
+ def send(*args)
+ raise "send called"
+ end
+ end
+end
diff --git a/spec/ruby/core/io/fixtures/copy_in_out.rb b/spec/ruby/core/io/fixtures/copy_in_out.rb
new file mode 100644
index 0000000000..b9d4085a47
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/copy_in_out.rb
@@ -0,0 +1,2 @@
+STDOUT.sync = false
+IO.copy_stream(STDIN, STDOUT)
diff --git a/spec/ruby/core/io/fixtures/copy_stream.txt b/spec/ruby/core/io/fixtures/copy_stream.txt
new file mode 100644
index 0000000000..a2d827b351
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/copy_stream.txt
@@ -0,0 +1,6 @@
+Line one
+
+Line three
+Line four
+
+Line last
diff --git a/spec/ruby/core/io/fixtures/empty.txt b/spec/ruby/core/io/fixtures/empty.txt
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/empty.txt
diff --git a/spec/ruby/core/io/fixtures/incomplete.txt b/spec/ruby/core/io/fixtures/incomplete.txt
new file mode 100644
index 0000000000..23d432f642
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/incomplete.txt
@@ -0,0 +1 @@
+ðŸ \ No newline at end of file
diff --git a/spec/ruby/core/io/fixtures/lines.txt b/spec/ruby/core/io/fixtures/lines.txt
new file mode 100644
index 0000000000..0959997e7b
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/lines.txt
@@ -0,0 +1,9 @@
+Voici la ligne une.
+Qui è la linea due.
+
+
+Aquí está la línea tres.
+Hier ist Zeile vier.
+
+Está aqui a linha cinco.
+Here is line six.
diff --git a/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt b/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt
new file mode 100644
index 0000000000..7edc66b06a
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/no_bom_UTF-8.txt
@@ -0,0 +1 @@
+UTF-8
diff --git a/spec/ruby/core/io/fixtures/numbered_lines.txt b/spec/ruby/core/io/fixtures/numbered_lines.txt
new file mode 100644
index 0000000000..70e49a3d98
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/numbered_lines.txt
@@ -0,0 +1,5 @@
+Line 1: One
+Line 2: Two
+Line 3: Three
+Line 4: Four
+Line 5: Five
diff --git a/spec/ruby/core/io/fixtures/one_byte.txt b/spec/ruby/core/io/fixtures/one_byte.txt
new file mode 100644
index 0000000000..56a6051ca2
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/one_byte.txt
@@ -0,0 +1 @@
+1 \ No newline at end of file
diff --git a/spec/ruby/core/io/fixtures/read_binary.txt b/spec/ruby/core/io/fixtures/read_binary.txt
new file mode 100644
index 0000000000..fa036dca4b
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/read_binary.txt
@@ -0,0 +1 @@
+abcâdef
diff --git a/spec/ruby/core/io/fixtures/read_euc_jp.txt b/spec/ruby/core/io/fixtures/read_euc_jp.txt
new file mode 100644
index 0000000000..0e17cd717a
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/read_euc_jp.txt
@@ -0,0 +1 @@
+¤¢¤ê¤¬¤È¤¦
diff --git a/spec/ruby/core/io/fixtures/read_text.txt b/spec/ruby/core/io/fixtures/read_text.txt
new file mode 100644
index 0000000000..5a7a80f6e4
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/read_text.txt
@@ -0,0 +1 @@
+abcâdef
diff --git a/spec/ruby/core/io/fixtures/reopen_stdout.rb b/spec/ruby/core/io/fixtures/reopen_stdout.rb
new file mode 100644
index 0000000000..506ba072bd
--- /dev/null
+++ b/spec/ruby/core/io/fixtures/reopen_stdout.rb
@@ -0,0 +1,3 @@
+STDOUT.reopen ARGV[0]
+system "echo from system"
+exec "echo from exec"
diff --git a/spec/ruby/core/io/flush_spec.rb b/spec/ruby/core/io/flush_spec.rb
new file mode 100644
index 0000000000..4c3e8d12af
--- /dev/null
+++ b/spec/ruby/core/io/flush_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#flush" do
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.flush }.should.raise(IOError)
+ end
+
+ describe "on a pipe" do
+ before :each do
+ @r, @w = IO.pipe
+ end
+
+ after :each do
+ @r.close
+ begin
+ @w.close
+ rescue Errno::EPIPE
+ end
+ end
+
+ # [ruby-core:90895] RJIT worker may leave fd open in a forked child.
+ # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens,
+ # and the RJIT worker keeps the pipe open until the worker execve().
+ # TODO: consider acquiring GVL from RJIT worker.
+ guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do
+ it "raises Errno::EPIPE if sync=false and the read end is closed" do
+ @w.sync = false
+ @w.write "foo"
+ @r.close
+
+ -> { @w.flush }.should.raise(Errno::EPIPE, /Broken pipe/)
+ -> { @w.close }.should.raise(Errno::EPIPE, /Broken pipe/)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/for_fd_spec.rb b/spec/ruby/core/io/for_fd_spec.rb
new file mode 100644
index 0000000000..2d86361b73
--- /dev/null
+++ b/spec/ruby/core/io/for_fd_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/new'
+
+describe "IO.for_fd" do
+ it_behaves_like :io_new, :for_fd
+end
+
+describe "IO.for_fd" do
+ it_behaves_like :io_new_errors, :for_fd
+end
diff --git a/spec/ruby/core/io/foreach_spec.rb b/spec/ruby/core/io/foreach_spec.rb
new file mode 100644
index 0000000000..015988f9fb
--- /dev/null
+++ b/spec/ruby/core/io/foreach_spec.rb
@@ -0,0 +1,96 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/readlines'
+
+describe "IO.foreach" do
+ before :each do
+ @name = fixture __FILE__, "lines.txt"
+ @count = 0
+ ScratchPad.record []
+ end
+
+ it "updates $. with each yield" do
+ IO.foreach(@name) { $..should == @count += 1 }
+ end
+
+ ruby_version_is ""..."4.0" do
+ describe "when the filename starts with |" do
+ it "gets data from the standard out of the subprocess" do
+ cmd = "|sh -c 'echo hello;echo line2'"
+ platform_is :windows do
+ cmd = "|cmd.exe /C echo hello&echo line2"
+ end
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.foreach(cmd) { |l| ScratchPad << l }
+ end
+ ScratchPad.recorded.should == ["hello\n", "line2\n"]
+ end
+
+ platform_is_not :windows do
+ it "gets data from a fork when passed -" do
+ parent_pid = $$
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.foreach("|-") { |l| ScratchPad << l }
+ end
+
+ if $$ == parent_pid
+ ScratchPad.recorded.should == ["hello\n", "from a fork\n"]
+ else # child
+ puts "hello"
+ puts "from a fork"
+ exit!
+ end
+ end
+ end
+ end
+
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.foreach(cmd).to_a
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
+end
+
+describe "IO.foreach" do
+ before :each do
+ @external = Encoding.default_external
+ Encoding.default_external = Encoding::UTF_8
+
+ @name = fixture __FILE__, "lines.txt"
+ ScratchPad.record []
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ end
+
+ it "sets $_ to nil" do
+ $_ = "test"
+ IO.foreach(@name) { }
+ $_.should == nil
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ IO.foreach(@name).should.instance_of?(Enumerator)
+ IO.foreach(@name).to_a.should == IOSpecs.lines
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ IO.foreach(@name).size.should == nil
+ end
+ end
+ end
+ end
+
+ it_behaves_like :io_readlines, :foreach, IOSpecs.collector
+ it_behaves_like :io_readlines_options_19, :foreach, IOSpecs.collector
+end
diff --git a/spec/ruby/core/io/fsync_spec.rb b/spec/ruby/core/io/fsync_spec.rb
new file mode 100644
index 0000000000..0317cbc805
--- /dev/null
+++ b/spec/ruby/core/io/fsync_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#fsync" do
+ before :each do
+ @name = tmp("io_fsync.txt")
+ ScratchPad.clear
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "raises an IOError on closed stream" do
+ -> { IOSpecs.closed_io.fsync }.should.raise(IOError)
+ end
+
+ it "writes the buffered data to permanent storage" do
+ File.open(@name, "w") do |f|
+ f.write "one hit wonder"
+ f.fsync.should == 0
+ end
+ end
+end
diff --git a/spec/ruby/core/io/getbyte_spec.rb b/spec/ruby/core/io/getbyte_spec.rb
new file mode 100644
index 0000000000..668d81519c
--- /dev/null
+++ b/spec/ruby/core/io/getbyte_spec.rb
@@ -0,0 +1,58 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#getbyte" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "returns the next byte from the stream" do
+ @io.readline.should == "Voici la ligne une.\n"
+ letters = @io.getbyte, @io.getbyte, @io.getbyte, @io.getbyte, @io.getbyte
+ letters.should == [81, 117, 105, 32, 195]
+ end
+
+ it "returns nil when invoked at the end of the stream" do
+ @io.read
+ @io.getbyte.should == nil
+ end
+
+ it "raises an IOError on closed stream" do
+ -> { IOSpecs.closed_io.getbyte }.should.raise(IOError)
+ end
+end
+
+describe "IO#getbyte" do
+ before :each do
+ @io = IOSpecs.io_fixture "empty.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "returns nil on empty stream" do
+ @io.getbyte.should == nil
+ end
+end
+
+describe "IO#getbyte" do
+ before :each do
+ @name = tmp("io_getbyte.txt")
+ @io = new_io(@name, 'w')
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name if @name
+ end
+
+ it "raises an IOError if the stream is not readable" do
+ -> { @io.getbyte }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/getc_spec.rb b/spec/ruby/core/io/getc_spec.rb
new file mode 100644
index 0000000000..3be86203c0
--- /dev/null
+++ b/spec/ruby/core/io/getc_spec.rb
@@ -0,0 +1,42 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#getc" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "returns the next character from the stream" do
+ @io.readline.should == "Voici la ligne une.\n"
+ letters = @io.getc, @io.getc, @io.getc, @io.getc, @io.getc
+ letters.should == ["Q", "u", "i", " ", "è"]
+ end
+
+ it "returns nil when invoked at the end of the stream" do
+ @io.read
+ @io.getc.should == nil
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.getc }.should.raise(IOError)
+ end
+end
+
+describe "IO#getc" do
+ before :each do
+ @io = IOSpecs.io_fixture "empty.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "returns nil on empty stream" do
+ @io.getc.should == nil
+ end
+end
diff --git a/spec/ruby/core/io/gets_spec.rb b/spec/ruby/core/io/gets_spec.rb
new file mode 100644
index 0000000000..ce3ee73b94
--- /dev/null
+++ b/spec/ruby/core/io/gets_spec.rb
@@ -0,0 +1,348 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/gets_ascii'
+
+describe "IO#gets" do
+ it_behaves_like :io_gets_ascii, :gets
+end
+
+describe "IO#gets" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ @count = 0
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "assigns the returned line to $_" do
+ IOSpecs.lines.each do |line|
+ @io.gets
+ $_.should == line
+ end
+ end
+
+ it "sets $_ to nil after the last line has been read" do
+ while @io.gets
+ end
+ $_.should == nil
+ end
+
+ it "returns nil if called at the end of the stream" do
+ IOSpecs.lines.length.times { @io.gets }
+ @io.gets.should == nil
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.gets }.should.raise(IOError)
+ end
+
+ describe "with no separator" do
+ it "returns the next line of string that is separated by $/" do
+ IOSpecs.lines.each { |line| line.should == @io.gets }
+ end
+
+ it "updates lineno with each invocation" do
+ while @io.gets
+ @io.lineno.should == @count += 1
+ end
+ end
+
+ it "updates $. with each invocation" do
+ while @io.gets
+ $..should == @count += 1
+ end
+ end
+ end
+
+ describe "with nil separator" do
+ it "returns the entire contents" do
+ @io.gets(nil).should == IOSpecs.lines.join("")
+ end
+
+ it "updates lineno with each invocation" do
+ while @io.gets(nil)
+ @io.lineno.should == @count += 1
+ end
+ end
+
+ it "updates $. with each invocation" do
+ while @io.gets(nil)
+ $..should == @count += 1
+ end
+ end
+ end
+
+ describe "with an empty String separator" do
+ # Two successive newlines in the input separate paragraphs.
+ # When there are more than two successive newlines, only two are kept.
+ it "returns the next paragraph" do
+ @io.gets("").should == IOSpecs.lines[0,3].join("")
+ @io.gets("").should == IOSpecs.lines[4,3].join("")
+ @io.gets("").should == IOSpecs.lines[7,2].join("")
+ end
+
+ it "reads until the beginning of the next paragraph" do
+ # There are three newlines between the first and second paragraph
+ @io.gets("")
+ @io.gets.should == IOSpecs.lines[4]
+ end
+
+ it "updates lineno with each invocation" do
+ while @io.gets("")
+ @io.lineno.should == @count += 1
+ end
+ end
+
+ it "updates $. with each invocation" do
+ while @io.gets("")
+ $..should == @count += 1
+ end
+ end
+ end
+
+ describe "with an arbitrary String separator" do
+ it "reads up to and including the separator" do
+ @io.gets("la linea").should == "Voici la ligne une.\nQui \303\250 la linea"
+ end
+
+ it "updates lineno with each invocation" do
+ while (@io.gets("la"))
+ @io.lineno.should == @count += 1
+ end
+ end
+
+ it "updates $. with each invocation" do
+ while @io.gets("la")
+ $..should == @count += 1
+ end
+ end
+
+ describe "that consists of multiple bytes" do
+ platform_is_not :windows do
+ it "should match the separator even if the buffer is filled over successive reads" do
+ IO.pipe do |read, write|
+
+ # Write part of the string with the separator split between two write calls. We want
+ # the read to intertwine such that when the read starts the full data isn't yet
+ # available in the buffer.
+ write.write("Aquí está la línea tres\r\n")
+
+ t = Thread.new do
+ # Continue reading until the separator is encountered or the pipe is closed.
+ read.gets("\r\n\r\n")
+ end
+
+ # Write the other half of the separator, which should cause the `gets` call to now
+ # match. Explicitly close the pipe for good measure so a bug in `gets` doesn't block forever.
+ Thread.pass until t.stop?
+
+ write.write("\r\nelse\r\n\r\n")
+ write.close
+
+ t.value.bytes.should == "Aquí está la línea tres\r\n\r\n".bytes
+ read.read(8).bytes.should == "else\r\n\r\n".bytes
+ end
+ end
+ end
+ end
+ end
+
+ describe "when passed chomp" do
+ it "returns the first line without a trailing newline character" do
+ @io.gets(chomp: true).should == IOSpecs.lines_without_newline_characters[0]
+ end
+
+ it "raises exception when options passed as Hash" do
+ -> { @io.gets({ chomp: true }) }.should.raise(TypeError)
+
+ -> {
+ @io.gets("\n", 1, { chomp: true })
+ }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
+ end
+end
+
+describe "IO#gets" do
+ before :each do
+ @name = tmp("io_gets")
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "raises an IOError if the stream is opened for append only" do
+ -> { File.open(@name, "a:utf-8") { |f| f.gets } }.should.raise(IOError)
+ end
+
+ it "raises an IOError if the stream is opened for writing only" do
+ -> { File.open(@name, "w:utf-8") { |f| f.gets } }.should.raise(IOError)
+ end
+end
+
+describe "IO#gets" do
+ before :each do
+ @name = tmp("io_gets")
+ touch(@name) { |f| f.write "one\n\ntwo\n\nthree\nfour\n" }
+ @io = new_io @name, "r:utf-8"
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+ end
+
+ it "calls #to_int to convert a single object argument to an Integer limit" do
+ obj = mock("io gets limit")
+ obj.should_receive(:to_int).and_return(6)
+
+ @io.gets(obj).should == "one\n"
+ end
+
+ it "calls #to_int to convert the second object argument to an Integer limit" do
+ obj = mock("io gets limit")
+ obj.should_receive(:to_int).and_return(2)
+
+ @io.gets(nil, obj).should == "on"
+ end
+
+ it "calls #to_str to convert the first argument to a String when passed a limit" do
+ obj = mock("io gets separator")
+ obj.should_receive(:to_str).and_return($/)
+
+ @io.gets(obj, 5).should == "one\n"
+ end
+
+ it "reads to the default separator when passed a single argument greater than the number of bytes to the separator" do
+ @io.gets(6).should == "one\n"
+ end
+
+ it "reads limit bytes when passed a single argument less than the number of bytes to the default separator" do
+ @io.gets(3).should == "one"
+ end
+
+ it "reads limit bytes when passed nil and a limit" do
+ @io.gets(nil, 6).should == "one\n\nt"
+ end
+
+ it "reads all bytes when the limit is higher than the available bytes" do
+ @io.gets(nil, 100).should == "one\n\ntwo\n\nthree\nfour\n"
+ end
+
+ it "reads until the next paragraph when passed '' and a limit greater than the next paragraph" do
+ @io.gets("", 6).should == "one\n\n"
+ end
+
+ it "reads limit bytes when passed '' and a limit less than the next paragraph" do
+ @io.gets("", 3).should == "one"
+ end
+
+ it "reads all bytes when pass a separator and reading more than all bytes" do
+ @io.gets("\t", 100).should == "one\n\ntwo\n\nthree\nfour\n"
+ end
+
+ it "returns empty string when 0 passed as a limit" do
+ @io.gets(0).should == ""
+ @io.gets(nil, 0).should == ""
+ @io.gets("", 0).should == ""
+ end
+
+ it "does not accept limit that doesn't fit in a C off_t" do
+ -> { @io.gets(2**128) }.should.raise(RangeError)
+ end
+end
+
+describe "IO#gets" do
+ before :each do
+ @name = tmp("io_gets")
+ # create data "æœæ—¥" + "\xE3\x81" * 100 to avoid utf-8 conflicts
+ data = "æœæ—¥" + ([227,129].pack('C*') * 100).force_encoding('utf-8')
+ touch(@name) { |f| f.write data }
+ @io = new_io @name, "r:utf-8"
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+ end
+
+ it "reads limit bytes and extra bytes when limit is reached not at character boundary" do
+ [@io.gets(1), @io.gets(1)].should == ["æœ", "æ—¥"]
+ end
+
+ it "read limit bytes and extra bytes with maximum of 16" do
+ # create str "æœæ—¥\xE3" + "\x81\xE3" * 8 to avoid utf-8 conflicts
+ str = "æœæ—¥" + ([227] + [129,227] * 8).pack('C*').force_encoding('utf-8')
+ @io.gets(7).should == str
+ end
+end
+
+describe "IO#gets" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::UTF_8
+ Encoding.default_internal = nil
+
+ @name = tmp("io_gets")
+ touch(@name) { |f| f.write "line" }
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "uses the default external encoding" do
+ @io = new_io @name, 'r'
+ @io.gets.encoding.should == Encoding::UTF_8
+ end
+
+ it "uses the IO object's external encoding, when set" do
+ @io = new_io @name, 'r'
+ @io.set_encoding Encoding::US_ASCII
+ @io.gets.encoding.should == Encoding::US_ASCII
+ end
+
+ it "transcodes into the default internal encoding" do
+ Encoding.default_internal = Encoding::US_ASCII
+ @io = new_io @name, 'r'
+ @io.gets.encoding.should == Encoding::US_ASCII
+ end
+
+ it "transcodes into the IO object's internal encoding, when set" do
+ Encoding.default_internal = Encoding::US_ASCII
+ @io = new_io @name, 'r'
+ @io.set_encoding Encoding::UTF_8, Encoding::UTF_16
+ @io.gets.encoding.should == Encoding::UTF_16
+ end
+
+ it "overwrites the default external encoding with the IO object's own external encoding" do
+ Encoding.default_external = Encoding::BINARY
+ Encoding.default_internal = Encoding::UTF_8
+ @io = new_io @name, 'r'
+ @io.set_encoding Encoding::IBM866
+ @io.gets.encoding.should == Encoding::UTF_8
+ end
+
+ it "ignores the internal encoding if the default external encoding is BINARY" do
+ Encoding.default_external = Encoding::BINARY
+ Encoding.default_internal = Encoding::UTF_8
+ @io = new_io @name, 'r'
+ @io.gets.encoding.should == Encoding::BINARY
+ end
+
+ it "ignores the internal encoding if the IO object's external encoding is BINARY" do
+ Encoding.default_external = Encoding::BINARY
+ Encoding.default_internal = Encoding::UTF_8
+ @io = new_io @name, 'r'
+ @io.set_encoding Encoding::BINARY, Encoding::UTF_8
+ @io.gets.encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/io/initialize_spec.rb b/spec/ruby/core/io/initialize_spec.rb
new file mode 100644
index 0000000000..3425e5ac37
--- /dev/null
+++ b/spec/ruby/core/io/initialize_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#initialize" do
+ before :each do
+ @name = tmp("io_initialize.txt")
+ @io = IO.new(new_fd(@name))
+ @fd = @io.fileno
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+ end
+
+ it "reassociates the IO instance with the new descriptor when passed an Integer" do
+ fd = new_fd @name, "r:utf-8"
+ @io.send :initialize, fd, 'r'
+ @io.fileno.should == fd
+ end
+
+ it "calls #to_int to coerce the object passed as an fd" do
+ obj = mock('fileno')
+ fd = new_fd @name, "r:utf-8"
+ obj.should_receive(:to_int).and_return(fd)
+ @io.send :initialize, obj, 'r'
+ @io.fileno.should == fd
+ end
+
+ it "accepts options as keyword arguments" do
+ fd = new_fd @name, "w:utf-8"
+
+ @io.send(:initialize, fd, "w", flags: File::CREAT)
+ @io.fileno.should == fd
+
+ -> {
+ @io.send(:initialize, fd, "w", {flags: File::CREAT})
+ }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 1..2)")
+ end
+
+ it "raises a TypeError when passed an IO" do
+ -> { @io.send :initialize, STDOUT, 'w' }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { @io.send :initialize, nil, 'w' }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { @io.send :initialize, "4", 'w' }.should.raise(TypeError)
+ end
+
+ it "raises IOError on closed stream" do
+ -> { @io.send :initialize, IOSpecs.closed_io.fileno }.should.raise(IOError)
+ end
+
+ it "raises an Errno::EBADF when given an invalid file descriptor" do
+ -> { @io.send :initialize, -1, 'w' }.should.raise(Errno::EBADF)
+ end
+end
diff --git a/spec/ruby/core/io/inspect_spec.rb b/spec/ruby/core/io/inspect_spec.rb
new file mode 100644
index 0000000000..37dc459f22
--- /dev/null
+++ b/spec/ruby/core/io/inspect_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "IO#inspect" do
+ after :each do
+ @r.close if @r && !@r.closed?
+ @w.close if @w && !@w.closed?
+ end
+
+ it "contains the file descriptor number" do
+ @r, @w = IO.pipe
+ @r.inspect.should.include?("fd #{@r.fileno}")
+ end
+
+ it "contains \"(closed)\" if the stream is closed" do
+ @r, @w = IO.pipe
+ @r.close
+ @r.inspect.should.include?("(closed)")
+ end
+
+ it "reports IO as its Method object's owner" do
+ IO.instance_method(:inspect).owner.should == IO
+ end
+end
diff --git a/spec/ruby/core/io/internal_encoding_spec.rb b/spec/ruby/core/io/internal_encoding_spec.rb
new file mode 100644
index 0000000000..9963a93f33
--- /dev/null
+++ b/spec/ruby/core/io/internal_encoding_spec.rb
@@ -0,0 +1,145 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :io_internal_encoding, shared: true do
+ describe "when Encoding.default_internal is not set" do
+ before :each do
+ Encoding.default_internal = nil
+ end
+
+ it "returns nil if the internal encoding is not set" do
+ @io = new_io @name, @object
+ @io.internal_encoding.should == nil
+ end
+
+ it "returns nil if Encoding.default_internal is changed after the instance is created" do
+ @io = new_io @name, @object
+ Encoding.default_internal = Encoding::IBM437
+ @io.internal_encoding.should == nil
+ end
+
+ it "returns the value set when the instance was created" do
+ @io = new_io @name, "#{@object}:utf-8:euc-jp"
+ Encoding.default_internal = Encoding::IBM437
+ @io.internal_encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns the value set by #set_encoding" do
+ @io = new_io @name, @object
+ @io.set_encoding(Encoding::US_ASCII, Encoding::IBM437)
+ @io.internal_encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "when Encoding.default_internal == Encoding.default_external" do
+ before :each do
+ Encoding.default_external = Encoding::IBM866
+ Encoding.default_internal = Encoding::IBM866
+ end
+
+ it "returns nil" do
+ @io = new_io @name, @object
+ @io.internal_encoding.should == nil
+ end
+
+ it "returns nil regardless of Encoding.default_internal changes" do
+ @io = new_io @name, @object
+ Encoding.default_internal = Encoding::IBM437
+ @io.internal_encoding.should == nil
+ end
+ end
+
+ describe "when Encoding.default_internal != Encoding.default_external" do
+ before :each do
+ Encoding.default_external = Encoding::IBM437
+ Encoding.default_internal = Encoding::IBM866
+ end
+
+ it "returns the value of Encoding.default_internal when the instance was created if the internal encoding is not set" do
+ @io = new_io @name, @object
+ @io.internal_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "does not change when Encoding.default_internal is changed" do
+ @io = new_io @name, @object
+ Encoding.default_internal = Encoding::IBM437
+ @io.internal_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "returns the internal encoding set when the instance was created" do
+ @io = new_io @name, "#{@object}:utf-8:euc-jp"
+ @io.internal_encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "does not change when set and Encoding.default_internal is changed" do
+ @io = new_io @name, "#{@object}:utf-8:euc-jp"
+ Encoding.default_internal = Encoding::IBM437
+ @io.internal_encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "returns the value set by #set_encoding" do
+ @io = new_io @name, @object
+ @io.set_encoding(Encoding::US_ASCII, Encoding::IBM437)
+ @io.internal_encoding.should.equal?(Encoding::IBM437)
+ end
+
+ it "returns nil when Encoding.default_external is BINARY and the internal encoding is not set" do
+ Encoding.default_external = Encoding::BINARY
+ @io = new_io @name, @object
+ @io.internal_encoding.should == nil
+ end
+
+ it "returns nil when the external encoding is BINARY and the internal encoding is not set" do
+ @io = new_io @name, "#{@object}:binary"
+ @io.internal_encoding.should == nil
+ end
+ end
+end
+
+describe "IO#internal_encoding" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ @name = tmp("io_internal_encoding")
+ touch(@name)
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "can be retrieved from a closed stream" do
+ io = IOSpecs.io_fixture("lines.txt", "r")
+ io.close
+ io.internal_encoding.should.equal?(Encoding.default_internal)
+ end
+
+ describe "with 'r' mode" do
+ it_behaves_like :io_internal_encoding, nil, "r"
+ end
+
+ describe "with 'r+' mode" do
+ it_behaves_like :io_internal_encoding, nil, "r+"
+ end
+
+ describe "with 'w' mode" do
+ it_behaves_like :io_internal_encoding, nil, "w"
+ end
+
+ describe "with 'w+' mode" do
+ it_behaves_like :io_internal_encoding, nil, "w+"
+ end
+
+ describe "with 'a' mode" do
+ it_behaves_like :io_internal_encoding, nil, "a"
+ end
+
+ describe "with 'a+' mode" do
+ it_behaves_like :io_internal_encoding, nil, "a+"
+ end
+end
diff --git a/spec/ruby/core/io/io_spec.rb b/spec/ruby/core/io/io_spec.rb
new file mode 100644
index 0000000000..0feb1a8774
--- /dev/null
+++ b/spec/ruby/core/io/io_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "IO" do
+ it "includes File::Constants" do
+ IO.include?(File::Constants).should == true
+ end
+
+ it "includes Enumerable" do
+ IO.include?(Enumerable).should == true
+ end
+end
diff --git a/spec/ruby/core/io/ioctl_spec.rb b/spec/ruby/core/io/ioctl_spec.rb
new file mode 100644
index 0000000000..15a5d6ec22
--- /dev/null
+++ b/spec/ruby/core/io/ioctl_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#ioctl" do
+ platform_is_not :windows do
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.ioctl(5, 5) }.should.raise(IOError)
+ end
+ end
+
+ platform_is :linux do
+ guard -> { RUBY_PLATFORM.include?("86") } do # x86 / x86_64
+ it "resizes an empty String to match the output size" do
+ File.open(__FILE__, 'r') do |f|
+ buffer = +''
+ # FIONREAD in /usr/include/asm-generic/ioctls.h
+ f.ioctl 0x541B, buffer
+ buffer.unpack('I').first.should.is_a?(Integer)
+ end
+ end
+ end
+
+ it "raises a system call error when ioctl fails" do
+ File.open(__FILE__, 'r') do |f|
+ -> {
+ # TIOCGWINSZ in /usr/include/asm-generic/ioctls.h
+ f.ioctl 0x5413, nil
+ }.should.raise(SystemCallError)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/isatty_spec.rb b/spec/ruby/core/io/isatty_spec.rb
new file mode 100644
index 0000000000..3b6c69b190
--- /dev/null
+++ b/spec/ruby/core/io/isatty_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/tty'
+
+describe "IO#isatty" do
+ it_behaves_like :io_tty, :isatty
+end
diff --git a/spec/ruby/core/io/lineno_spec.rb b/spec/ruby/core/io/lineno_spec.rb
new file mode 100644
index 0000000000..93b505652a
--- /dev/null
+++ b/spec/ruby/core/io/lineno_spec.rb
@@ -0,0 +1,138 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#lineno" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "raises an IOError on a closed stream" do
+ -> { IOSpecs.closed_io.lineno }.should.raise(IOError)
+ end
+
+ it "raises an IOError on a write-only stream" do
+ name = tmp("io_lineno.txt")
+ begin
+ File.open(name, 'w') do |f|
+ -> { f.lineno }.should.raise(IOError)
+ end
+ ensure
+ rm_r name
+ end
+ end
+
+ it "raises an IOError on a duplexed stream with the read side closed" do
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ IO.popen(cmd, 'r+') do |p|
+ p.close_read
+ -> { p.lineno }.should.raise(IOError)
+ end
+ end
+
+ it "returns the current line number" do
+ @io.lineno.should == 0
+
+ count = 0
+ while @io.gets
+ @io.lineno.should == count += 1
+ end
+
+ @io.rewind
+ @io.lineno.should == 0
+ end
+end
+
+describe "IO#lineno=" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "raises an IOError on a closed stream" do
+ -> { IOSpecs.closed_io.lineno = 5 }.should.raise(IOError)
+ end
+
+ it "raises an IOError on a write-only stream" do
+ name = tmp("io_lineno.txt")
+ begin
+ File.open(name, 'w') do |f|
+ -> { f.lineno = 0 }.should.raise(IOError)
+ end
+ ensure
+ rm_r name
+ end
+ end
+
+ it "raises an IOError on a duplexed stream with the read side closed" do
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ IO.popen(cmd, 'r+') do |p|
+ p.close_read
+ -> { p.lineno = 0 }.should.raise(IOError)
+ end
+ end
+
+ it "calls #to_int on a non-numeric argument" do
+ obj = mock('123')
+ obj.should_receive(:to_int).and_return(123)
+
+ @io.lineno = obj
+ @io.lineno.should == 123
+ end
+
+ it "truncates a Float argument" do
+ @io.lineno = 1.5
+ @io.lineno.should == 1
+
+ @io.lineno = 92233.72036854775808
+ @io.lineno.should == 92233
+ end
+
+ it "raises TypeError if cannot convert argument to Integer implicitly" do
+ -> { @io.lineno = "1" }.should.raise(TypeError, 'no implicit conversion of String into Integer')
+ -> { @io.lineno = nil }.should raise_consistent_error(TypeError, 'no implicit conversion of nil into Integer')
+ end
+
+ it "does not accept Integers that don't fit in a C int" do
+ -> { @io.lineno = 2**32 }.should.raise(RangeError)
+ end
+
+ it "sets the current line number to the given value" do
+ @io.lineno = count = 500
+
+ while @io.gets
+ @io.lineno.should == count += 1
+ end
+
+ @io.rewind
+ @io.lineno.should == 0
+ end
+
+ it "does not change $." do
+ original_line = $.
+ numbers = [-2**30, -2**16, -2**8, -100, -10, -1, 0, 1, 10, 2**8, 2**16, 2**30]
+ numbers.each do |num|
+ @io.lineno = num
+ @io.lineno.should == num
+ $..should == original_line
+ end
+ end
+
+ it "does not change $. until next read" do
+ $. = 0
+ $..should == 0
+
+ @io.lineno = count = 500
+ $..should == 0
+
+ while @io.gets
+ $..should == count += 1
+ end
+ end
+end
diff --git a/spec/ruby/core/io/new_spec.rb b/spec/ruby/core/io/new_spec.rb
new file mode 100644
index 0000000000..979ac0efcb
--- /dev/null
+++ b/spec/ruby/core/io/new_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'shared/new'
+
+# NOTE: should be synchronized with library/stringio/initialize_spec.rb
+
+describe "IO.new" do
+ it_behaves_like :io_new, :new
+
+ it "does not use the given block and warns to use IO::open" do
+ -> {
+ @io = IO.send(@method, @fd) { raise }
+ }.should complain(/warning: IO::new\(\) does not take block; use IO::open\(\) instead/)
+ end
+end
+
+describe "IO.new" do
+ it_behaves_like :io_new_errors, :new
+end
diff --git a/spec/ruby/core/io/nonblock_spec.rb b/spec/ruby/core/io/nonblock_spec.rb
new file mode 100644
index 0000000000..99dc0cafd0
--- /dev/null
+++ b/spec/ruby/core/io/nonblock_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+
+platform_is_not :windows do
+ describe "IO#nonblock?" do
+ before :all do
+ require 'io/nonblock'
+ end
+
+ it "returns false for a file by default" do
+ File.open(__FILE__) do |f|
+ f.nonblock?.should == false
+ end
+ end
+
+ it "returns true for pipe by default" do
+ r, w = IO.pipe
+ begin
+ r.nonblock?.should == true
+ w.nonblock?.should == true
+ ensure
+ r.close
+ w.close
+ end
+ end
+
+ it "returns true for socket by default" do
+ require 'socket'
+ TCPServer.open(0) do |socket|
+ socket.nonblock?.should == true
+ end
+ end
+ end
+
+ describe "IO#nonblock=" do
+ before :all do
+ require 'io/nonblock'
+ end
+
+ it "changes the IO to non-blocking mode" do
+ File.open(__FILE__) do |f|
+ f.nonblock = true
+ f.nonblock?.should == true
+ f.nonblock = false
+ f.nonblock?.should == false
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/open_spec.rb b/spec/ruby/core/io/open_spec.rb
new file mode 100644
index 0000000000..ff22d14685
--- /dev/null
+++ b/spec/ruby/core/io/open_spec.rb
@@ -0,0 +1,99 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/new'
+
+describe "IO.open" do
+ it_behaves_like :io_new, :open
+end
+
+describe "IO.open" do
+ it_behaves_like :io_new_errors, :open
+end
+
+# These specs use a special mock helper to avoid mock
+# methods from preventing IO#close from running and
+# which would prevent the file referenced by @fd from
+# being deleted on Windows.
+
+describe "IO.open" do
+ before :each do
+ @name = tmp("io_open.txt")
+ @fd = new_fd @name
+ ScratchPad.clear
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "calls #close after yielding to the block" do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ ScratchPad.record :called
+ end
+ io.closed?.should == false
+ end
+ ScratchPad.recorded.should == :called
+ end
+
+ it "propagate an exception in the block after calling #close" do
+ -> do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ ScratchPad.record :called
+ end
+ raise Exception
+ end
+ end.should.raise(Exception)
+ ScratchPad.recorded.should == :called
+ end
+
+ it "propagates an exception raised by #close that is not a StandardError" do
+ -> do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ ScratchPad.record :called
+ raise Exception
+ end
+ end
+ end.should.raise(Exception)
+ ScratchPad.recorded.should == :called
+ end
+
+ it "propagates an exception raised by #close that is a StandardError" do
+ -> do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ ScratchPad.record :called
+ raise StandardError
+ end
+ end
+ end.should.raise(StandardError)
+ ScratchPad.recorded.should == :called
+ end
+
+ it "does not propagate an IOError with 'closed stream' message raised by #close" do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ ScratchPad.record :called
+ raise IOError, 'closed stream'
+ end
+ end
+ ScratchPad.recorded.should == :called
+ end
+
+ it "does not set last error when an IOError with 'closed stream' raised by #close" do
+ IO.open(@fd, "w") do |io|
+ IOSpecs.io_mock(io, :close) do
+ super()
+ raise IOError, 'closed stream'
+ end
+ end
+ $!.should == nil
+ end
+end
diff --git a/spec/ruby/core/io/output_spec.rb b/spec/ruby/core/io/output_spec.rb
new file mode 100644
index 0000000000..0decf8c95b
--- /dev/null
+++ b/spec/ruby/core/io/output_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#<<" do
+ it "writes an object to the IO stream" do
+ -> {
+ $stderr << "Oh noes, an error!"
+ }.should output_to_fd("Oh noes, an error!", $stderr)
+ end
+
+ it "calls #to_s on the object to print it" do
+ -> {
+ $stderr << 1337
+ }.should output_to_fd("1337", $stderr)
+ end
+
+ it "raises an error if the stream is closed" do
+ io = IOSpecs.closed_io
+ -> { io << "test" }.should.raise(IOError)
+ end
+
+ it "returns self" do
+ -> {
+ ($stderr << "to_stderr").should == $stderr
+ }.should output(nil, "to_stderr")
+ end
+end
diff --git a/spec/ruby/core/io/path_spec.rb b/spec/ruby/core/io/path_spec.rb
new file mode 100644
index 0000000000..798adb2163
--- /dev/null
+++ b/spec/ruby/core/io/path_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "IO#path" do
+ it "returns the path of the file associated with the IO object" do
+ path = tmp("io_path.txt")
+ File.open(path, "w") do |file|
+ IO.new(file.fileno, path: file.path, autoclose: false).path.should == file.path
+ end
+ ensure
+ File.unlink(path)
+ end
+end
diff --git a/spec/ruby/core/io/pid_spec.rb b/spec/ruby/core/io/pid_spec.rb
new file mode 100644
index 0000000000..04956887ff
--- /dev/null
+++ b/spec/ruby/core/io/pid_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#pid" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "returns nil for IO not associated with a process" do
+ @io.pid.should == nil
+ end
+end
+
+describe "IO#pid" do
+ before :each do
+ @io = IO.popen ruby_cmd('STDIN.read'), "r+"
+ end
+
+ after :each do
+ @io.close if @io && !@io.closed?
+ end
+
+ it "returns the ID of a process associated with stream" do
+ @io.pid.should_not == nil
+ end
+
+ it "raises an IOError on closed stream" do
+ @io.close
+ -> { @io.pid }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/pipe_spec.rb b/spec/ruby/core/io/pipe_spec.rb
new file mode 100644
index 0000000000..9f1b01a5cd
--- /dev/null
+++ b/spec/ruby/core/io/pipe_spec.rb
@@ -0,0 +1,225 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO.pipe" do
+ after :each do
+ @r.close if @r && !@r.closed?
+ @w.close if @w && !@w.closed?
+ end
+
+ it "creates a two-ended pipe" do
+ @r, @w = IO.pipe
+ @w.puts "test_create_pipe\\n"
+ @w.close
+ @r.read(16).should == "test_create_pipe"
+ end
+
+ it "returns two IO objects" do
+ @r, @w = IO.pipe
+ @r.should.is_a?(IO)
+ @w.should.is_a?(IO)
+ end
+
+ it "returns instances of a subclass when called on a subclass" do
+ @r, @w = IOSpecs::SubIO.pipe
+ @r.should.instance_of?(IOSpecs::SubIO)
+ @w.should.instance_of?(IOSpecs::SubIO)
+ end
+
+ it "does not use IO.new method to create pipes and allows its overriding" do
+ ScratchPad.record []
+
+ # so redefined .new is not called, but original #initialize is
+ @r, @w = IOSpecs::SubIOWithRedefinedNew.pipe
+ ScratchPad.recorded.should == [:call_original_initialize, :call_original_initialize] # called 2 times - for each pipe (r and w)
+
+ @r.should.instance_of?(IOSpecs::SubIOWithRedefinedNew)
+ @w.should.instance_of?(IOSpecs::SubIOWithRedefinedNew)
+ end
+end
+
+describe "IO.pipe" do
+ describe "passed a block" do
+ it "yields two IO objects" do
+ IO.pipe do |r, w|
+ r.should.is_a?(IO)
+ w.should.is_a?(IO)
+ end
+ end
+
+ it "returns the result of the block" do
+ IO.pipe { |r, w| :result }.should == :result
+ end
+
+ it "closes both IO objects" do
+ r, w = IO.pipe do |_r, _w|
+ [_r, _w]
+ end
+ r.should.closed?
+ w.should.closed?
+ end
+
+ it "closes both IO objects when the block raises" do
+ r = w = nil
+ -> do
+ IO.pipe do |_r, _w|
+ r = _r
+ w = _w
+ raise RuntimeError
+ end
+ end.should.raise(RuntimeError)
+ r.should.closed?
+ w.should.closed?
+ end
+
+ it "allows IO objects to be closed within the block" do
+ r, w = IO.pipe do |_r, _w|
+ _r.close
+ _w.close
+ [_r, _w]
+ end
+ r.should.closed?
+ w.should.closed?
+ end
+ end
+end
+
+describe "IO.pipe" do
+ before :each do
+ @default_external = Encoding.default_external
+ @default_internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_external = @default_external
+ Encoding.default_internal = @default_internal
+ end
+
+ it "sets the external encoding of the read end to the default when passed no arguments" do
+ Encoding.default_external = Encoding::ISO_8859_1
+
+ IO.pipe do |r, w|
+ r.external_encoding.should == Encoding::ISO_8859_1
+ r.internal_encoding.should == nil
+ end
+ end
+
+ it "sets the internal encoding of the read end to the default when passed no arguments" do
+ Encoding.default_external = Encoding::ISO_8859_1
+ Encoding.default_internal = Encoding::UTF_8
+
+ IO.pipe do |r, w|
+ r.external_encoding.should == Encoding::ISO_8859_1
+ r.internal_encoding.should == Encoding::UTF_8
+ end
+ end
+
+ it "sets the internal encoding to nil if the same as the external" do
+ Encoding.default_external = Encoding::UTF_8
+ Encoding.default_internal = Encoding::UTF_8
+
+ IO.pipe do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == nil
+ end
+ end
+
+ it "sets the external encoding of the read end when passed an Encoding argument" do
+ IO.pipe(Encoding::UTF_8) do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == nil
+ end
+ end
+
+ it "sets the external and internal encodings of the read end when passed two Encoding arguments" do
+ IO.pipe(Encoding::UTF_8, Encoding::UTF_16BE) do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::UTF_16BE
+ end
+ end
+
+ it "sets the external encoding of the read end when passed the name of an Encoding" do
+ IO.pipe("UTF-8") do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == nil
+ end
+ end
+
+ it "accepts 'bom|' prefix for external encoding" do
+ IO.pipe("BOM|UTF-8") do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == nil
+ end
+ end
+
+ it "sets the external and internal encodings specified as a String and separated with a colon" do
+ IO.pipe("UTF-8:ISO-8859-1") do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::ISO_8859_1
+ end
+ end
+
+ it "accepts 'bom|' prefix for external encoding when specifying 'external:internal'" do
+ IO.pipe("BOM|UTF-8:ISO-8859-1") do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::ISO_8859_1
+ end
+ end
+
+ it "sets the external and internal encoding when passed two String arguments" do
+ IO.pipe("UTF-8", "UTF-16BE") do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::UTF_16BE
+ end
+ end
+
+ it "accepts an options Hash with one String encoding argument" do
+ IO.pipe("BOM|UTF-8:ISO-8859-1", invalid: :replace) do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::ISO_8859_1
+ end
+ end
+
+ it "accepts an options Hash with two String encoding arguments" do
+ IO.pipe("UTF-8", "ISO-8859-1", invalid: :replace) do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::ISO_8859_1
+ end
+ end
+
+ it "calls #to_hash to convert an options argument" do
+ options = mock("io pipe encoding options")
+ options.should_receive(:to_hash).and_return({ invalid: :replace })
+ IO.pipe("UTF-8", "ISO-8859-1", **options) { |r, w| }
+ end
+
+ it "calls #to_str to convert the first argument to a String" do
+ obj = mock("io_pipe_encoding")
+ obj.should_receive(:to_str).and_return("UTF-8:UTF-16BE")
+ IO.pipe(obj) do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::UTF_16BE
+ end
+ end
+
+ it "calls #to_str to convert the second argument to a String" do
+ obj = mock("io_pipe_encoding")
+ obj.should_receive(:to_str).at_least(1).times.and_return("UTF-16BE")
+ IO.pipe(Encoding::UTF_8, obj) do |r, w|
+ r.external_encoding.should == Encoding::UTF_8
+ r.internal_encoding.should == Encoding::UTF_16BE
+ end
+ end
+
+ it "sets no external encoding for the write end" do
+ IO.pipe(Encoding::UTF_8) do |r, w|
+ w.external_encoding.should == nil
+ end
+ end
+
+ it "sets no internal encoding for the write end" do
+ IO.pipe(Encoding::UTF_8) do |r, w|
+ w.external_encoding.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/io/popen_spec.rb b/spec/ruby/core/io/popen_spec.rb
new file mode 100644
index 0000000000..b5747bf255
--- /dev/null
+++ b/spec/ruby/core/io/popen_spec.rb
@@ -0,0 +1,287 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../process/fixtures/common'
+
+describe "IO.popen" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :each do
+ @fname = tmp("IO_popen_spec")
+ @io = nil
+ @var = "$FOO"
+ platform_is :windows do
+ @var = "%FOO%"
+ end
+ end
+
+ after :each do
+ @io.close if @io and !@io.closed?
+ rm_r @fname
+ end
+
+ it "returns an open IO" do
+ @io = IO.popen(ruby_cmd('exit'), "r")
+ @io.closed?.should == false
+ end
+
+ it "reads a read-only pipe" do
+ @io = IO.popen('echo foo', "r")
+ @io.read.should == "foo\n"
+ end
+
+ it "raises IOError when writing a read-only pipe" do
+ @io = IO.popen('echo foo', "r")
+ -> { @io.write('bar') }.should.raise(IOError)
+ @io.read.should == "foo\n"
+ end
+
+ it "sees an infinitely looping subprocess exit when read pipe is closed" do
+ io = IO.popen ruby_cmd('r = loop{puts "y"; 0} rescue 1; exit r'), 'r'
+ io.close
+
+ $?.exitstatus.should_not == 0
+ end
+
+ it "writes to a write-only pipe" do
+ @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)', args: "> #{@fname}"), "w")
+ @io.write("bar")
+ @io.close
+
+ File.read(@fname).should == "bar"
+ end
+
+ it "raises IOError when reading a write-only pipe" do
+ @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)'), "w")
+ -> { @io.read }.should.raise(IOError)
+ end
+
+ it "reads and writes a read/write pipe" do
+ @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)'), "r+")
+ @io.write("bar")
+ @io.read(3).should == "bar"
+ end
+
+ it "waits for the child to finish" do
+ @io = IO.popen(ruby_cmd('IO.copy_stream(STDIN,STDOUT)', args: "> #{@fname}"), "w")
+ @io.write("bar")
+ @io.close
+
+ $?.exitstatus.should == 0
+
+ File.read(@fname).should == "bar"
+ end
+
+ it "does not throw an exception if child exited and has been waited for" do
+ @io = IO.popen([*ruby_exe, '-e', 'sleep'])
+ pid = @io.pid
+ Process.kill "KILL", pid
+ @io.close
+ platform_is_not :windows do
+ $?.should.signaled?
+ end
+ platform_is :windows do
+ $?.should.exited?
+ end
+ end
+
+ it "returns an instance of a subclass when called on a subclass" do
+ @io = IOSpecs::SubIO.popen(ruby_cmd('exit'), "r")
+ @io.should.instance_of?(IOSpecs::SubIO)
+ end
+
+ it "coerces mode argument with #to_str" do
+ mode = mock("mode")
+ mode.should_receive(:to_str).and_return("r")
+ @io = IO.popen(ruby_cmd('exit 0'), mode)
+ end
+
+ it "accepts a path using the chdir: keyword argument" do
+ path = File.dirname(@fname)
+
+ @io = IO.popen(ruby_cmd("puts Dir.pwd"), "r", chdir: path)
+ @io.read.chomp.should == path
+ end
+
+ it "accepts a path using the chdir: keyword argument and a coercible path" do
+ path = File.dirname(@fname)
+ object = mock("path")
+ object.should_receive(:to_path).and_return(path)
+
+ @io = IO.popen(ruby_cmd("puts Dir.pwd"), "r", chdir: object)
+ @io.read.chomp.should == path
+ end
+
+ describe "with a block" do
+ it "yields an open IO to the block" do
+ IO.popen(ruby_cmd('exit'), "r") do |io|
+ io.closed?.should == false
+ end
+ end
+
+ it "yields an instance of a subclass when called on a subclass" do
+ IOSpecs::SubIO.popen(ruby_cmd('exit'), "r") do |io|
+ io.should.instance_of?(IOSpecs::SubIO)
+ end
+ end
+
+ it "closes the IO after yielding" do
+ io = IO.popen(ruby_cmd('exit'), "r") { |_io| _io }
+ io.closed?.should == true
+ end
+
+ it "allows the IO to be closed inside the block" do
+ io = IO.popen(ruby_cmd('exit'), 'r') { |_io| _io.close; _io }
+ io.closed?.should == true
+ end
+
+ it "returns the value of the block" do
+ IO.popen(ruby_cmd('exit'), "r") { :hello }.should == :hello
+ end
+ end
+
+ platform_is_not :windows do
+ it "starts returns a forked process if the command is -" do
+ io = IO.popen("-")
+
+ if io # parent
+ begin
+ io.gets.should == "hello from child\n"
+ ensure
+ io.close
+ end
+ else # child
+ puts "hello from child"
+ exit!
+ end
+ end
+ end
+
+ it "has the given external encoding" do
+ @io = IO.popen(ruby_cmd('exit'), external_encoding: Encoding::EUC_JP)
+ @io.external_encoding.should == Encoding::EUC_JP
+ end
+
+ it "has the given internal encoding" do
+ @io = IO.popen(ruby_cmd('exit'), internal_encoding: Encoding::EUC_JP)
+ @io.internal_encoding.should == Encoding::EUC_JP
+ end
+
+ it "sets the internal encoding to nil if it's the same as the external encoding" do
+ @io = IO.popen(ruby_cmd('exit'), external_encoding: Encoding::EUC_JP,
+ internal_encoding: Encoding::EUC_JP)
+ @io.internal_encoding.should == nil
+ end
+
+ context "with a leading ENV Hash" do
+ it "accepts a single String command" do
+ IO.popen({"FOO" => "bar"}, "echo #{@var}") do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts a single String command, and an IO mode" do
+ IO.popen({"FOO" => "bar"}, "echo #{@var}", "r") do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts a single String command with a trailing Hash of Process.exec options" do
+ IO.popen({"FOO" => "bar"}, ruby_cmd('STDERR.puts ENV["FOO"]'),
+ err: [:child, :out]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts a single String command with a trailing Hash of Process.exec options, and an IO mode" do
+ IO.popen({"FOO" => "bar"}, ruby_cmd('STDERR.puts ENV["FOO"]'), "r",
+ err: [:child, :out]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts an Array of command and arguments" do
+ exe, *args = ruby_exe
+ IO.popen({"FOO" => "bar"}, [[exe, "specfu"], *args, "-e", "puts ENV['FOO']"]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts an Array of command and arguments, and an IO mode" do
+ exe, *args = ruby_exe
+ IO.popen({"FOO" => "bar"}, [[exe, "specfu"], *args, "-e", "puts ENV['FOO']"], "r") do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts an Array command with a separate trailing Hash of Process.exec options" do
+ IO.popen({"FOO" => "bar"}, [*ruby_exe, "-e", "STDERR.puts ENV['FOO']"],
+ err: [:child, :out]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts an Array command with a separate trailing Hash of Process.exec options, and an IO mode" do
+ IO.popen({"FOO" => "bar"}, [*ruby_exe, "-e", "STDERR.puts ENV['FOO']"],
+ "r", err: [:child, :out]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+ end
+
+ context "with a leading Array argument" do
+ it "uses the Array as command plus args for the child process" do
+ IO.popen([*ruby_exe, "-e", "puts 'hello'"]) do |io|
+ io.read.should == "hello\n"
+ end
+ end
+
+ it "accepts a leading ENV Hash" do
+ IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "puts ENV['FOO']"]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts a trailing Hash of Process.exec options" do
+ IO.popen([*ruby_exe, "does_not_exist", {err: [:child, :out]}]) do |io|
+ io.read.should =~ /LoadError/
+ end
+ end
+
+ it "accepts an IO mode argument following the Array" do
+ IO.popen([*ruby_exe, "does_not_exist", {err: [:child, :out]}], "r") do |io|
+ io.read.should =~ /LoadError/
+ end
+ end
+
+ it "accepts [env, command, arg1, arg2, ..., exec options]" do
+ IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]",
+ err: [:child, :out]]) do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts '[env, command, arg1, arg2, ..., exec options], mode'" do
+ IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]",
+ err: [:child, :out]], "r") do |io|
+ io.read.should == "bar\n"
+ end
+ end
+
+ it "accepts '[env, command, arg1, arg2, ..., exec options], mode, IO options'" do
+ IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]",
+ err: [:child, :out]], "r",
+ internal_encoding: Encoding::EUC_JP) do |io|
+ io.read.should == "bar\n"
+ io.internal_encoding.should == Encoding::EUC_JP
+ end
+ end
+
+ it "accepts '[env, command, arg1, arg2, ...], mode, IO + exec options'" do
+ IO.popen([{"FOO" => "bar"}, *ruby_exe, "-e", "STDERR.puts ENV[:FOO.to_s]"], "r",
+ err: [:child, :out], internal_encoding: Encoding::EUC_JP) do |io|
+ io.read.should == "bar\n"
+ io.internal_encoding.should == Encoding::EUC_JP
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/pos_spec.rb b/spec/ruby/core/io/pos_spec.rb
new file mode 100644
index 0000000000..e6cda2643d
--- /dev/null
+++ b/spec/ruby/core/io/pos_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/pos'
+
+describe "IO#pos" do
+ it_behaves_like :io_pos, :pos
+end
+
+describe "IO#pos=" do
+ it_behaves_like :io_set_pos, :pos=
+end
diff --git a/spec/ruby/core/io/pread_spec.rb b/spec/ruby/core/io/pread_spec.rb
new file mode 100644
index 0000000000..cfb8dc4c68
--- /dev/null
+++ b/spec/ruby/core/io/pread_spec.rb
@@ -0,0 +1,138 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "IO#pread" do
+ before :each do
+ @fname = tmp("io_pread.txt")
+ @contents = "1234567890"
+ touch(@fname) { |f| f.write @contents }
+ @file = File.open(@fname, "r+")
+ end
+
+ after :each do
+ @file.close
+ rm_r @fname
+ end
+
+ it "accepts a length, and an offset" do
+ @file.pread(4, 0).should == "1234"
+ @file.pread(3, 4).should == "567"
+ end
+
+ it "accepts a length, an offset, and an output buffer" do
+ buffer = +"foo"
+ @file.pread(3, 4, buffer).should.equal?(buffer)
+ buffer.should == "567"
+ end
+
+ it "shrinks the buffer in case of less bytes read" do
+ buffer = +"foo"
+ @file.pread(1, 0, buffer)
+ buffer.should == "1"
+ end
+
+ it "grows the buffer in case of more bytes read" do
+ buffer = +"foo"
+ @file.pread(5, 0, buffer)
+ buffer.should == "12345"
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @file.pread(10, 0, buffer)
+
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "does not advance the file pointer" do
+ @file.pread(4, 0).should == "1234"
+ @file.read.should == "1234567890"
+ end
+
+ it "ignores the current offset" do
+ @file.pos = 3
+ @file.pread(4, 0).should == "1234"
+ end
+
+ it "returns an empty string for maxlen = 0" do
+ @file.pread(0, 4).should == ""
+ end
+
+ it "returns a buffer for maxlen = 0 when buffer specified" do
+ buffer = +"foo"
+ @file.pread(0, 4, buffer).should.equal?(buffer)
+ buffer.should == "foo"
+ end
+
+ it "ignores the offset for maxlen = 0, even if it is out of file bounds" do
+ @file.pread(0, 400).should == ""
+ end
+
+ it "does not reset the buffer when reading with maxlen = 0" do
+ buffer = +"foo"
+ @file.pread(0, 4, buffer)
+ buffer.should == "foo"
+
+ @file.pread(0, 400, buffer)
+ buffer.should == "foo"
+ end
+
+ it "converts maxlen to Integer using #to_int" do
+ maxlen = mock('maxlen')
+ maxlen.should_receive(:to_int).and_return(4)
+ @file.pread(maxlen, 0).should == "1234"
+ end
+
+ it "converts offset to Integer using #to_int" do
+ offset = mock('offset')
+ offset.should_receive(:to_int).and_return(0)
+ @file.pread(4, offset).should == "1234"
+ end
+
+ it "converts a buffer to String using to_str" do
+ buffer = mock('buffer')
+ buffer.should_receive(:to_str).at_least(1).and_return(+"foo")
+ @file.pread(4, 0, buffer)
+ buffer.should_not.is_a?(String)
+ buffer.to_str.should == "1234"
+ end
+
+ it "raises TypeError if maxlen is not an Integer and cannot be coerced into Integer" do
+ maxlen = Object.new
+ -> { @file.pread(maxlen, 0) }.should.raise(TypeError, 'no implicit conversion of Object into Integer')
+ end
+
+ it "raises TypeError if offset is not an Integer and cannot be coerced into Integer" do
+ offset = Object.new
+ -> { @file.pread(4, offset) }.should.raise(TypeError, 'no implicit conversion of Object into Integer')
+ end
+
+ it "raises ArgumentError for negative values of maxlen" do
+ -> { @file.pread(-4, 0) }.should.raise(ArgumentError, 'negative string size (or size too big)')
+ end
+
+ it "raised Errno::EINVAL for negative values of offset" do
+ -> { @file.pread(4, -1) }.should.raise(Errno::EINVAL, /Invalid argument/)
+ end
+
+ it "raises TypeError if the buffer is not a String and cannot be coerced into String" do
+ buffer = Object.new
+ -> { @file.pread(4, 0, buffer) }.should.raise(TypeError, 'no implicit conversion of Object into String')
+ end
+
+ it "raises EOFError if end-of-file is reached" do
+ -> { @file.pread(1, 10) }.should.raise(EOFError)
+ end
+
+ it "raises IOError when file is not open in read mode" do
+ File.open(@fname, "w") do |file|
+ -> { file.pread(1, 1) }.should.raise(IOError)
+ end
+ end
+
+ it "raises IOError when file is closed" do
+ file = File.open(@fname, "r+")
+ file.close
+ -> { file.pread(1, 1) }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/print_spec.rb b/spec/ruby/core/io/print_spec.rb
new file mode 100644
index 0000000000..065cb3b8cb
--- /dev/null
+++ b/spec/ruby/core/io/print_spec.rb
@@ -0,0 +1,66 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#print" do
+ before :each do
+ @old_record_separator = $\
+ @old_field_separator = $,
+ suppress_warning {
+ $\ = '->'
+ $, = '^^'
+ }
+ @name = tmp("io_print")
+ end
+
+ after :each do
+ suppress_warning {
+ $\ = @old_record_separator
+ $, = @old_field_separator
+ }
+ rm_r @name
+ end
+
+ it "returns nil" do
+ touch(@name) { |f| f.print.should == nil }
+ end
+
+ it "writes $_.to_s followed by $\\ (if any) to the stream if no arguments given" do
+ o = mock('o')
+ o.should_receive(:to_s).and_return("mockmockmock")
+ $_ = o
+
+ touch(@name) { |f| f.print }
+ IO.read(@name).should == "mockmockmock#{$\}"
+
+ # Set $_ to something known
+ string = File.open(__FILE__) {|f| f.gets }
+
+ touch(@name) { |f| f.print }
+ IO.read(@name).should == "#{string}#{$\}"
+ end
+
+ it "calls obj.to_s and not obj.to_str then writes the record separator" do
+ o = mock('o')
+ o.should_not_receive(:to_str)
+ o.should_receive(:to_s).and_return("hello")
+
+ touch(@name) { |f| f.print(o) }
+
+ IO.read(@name).should == "hello#{$\}"
+ end
+
+ it "writes each obj.to_s to the stream separated by $, (if any) and appends $\\ (if any) given multiple objects" do
+ o, o2 = Object.new, Object.new
+ def o.to_s(); 'o'; end
+ def o2.to_s(); 'o2'; end
+
+ suppress_warning {
+ touch(@name) { |f| f.print(o, o2) }
+ }
+ IO.read(@name).should == "#{o.to_s}#{$,}#{o2.to_s}#{$\}"
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.print("stuff") }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/printf_spec.rb b/spec/ruby/core/io/printf_spec.rb
new file mode 100644
index 0000000000..d5519bdaa3
--- /dev/null
+++ b/spec/ruby/core/io/printf_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#printf" do
+ before :each do
+ @name = tmp("io_printf.txt")
+ @io = new_io @name
+ @io.sync = true
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+ end
+
+ it "calls #to_str to convert the format object to a String" do
+ obj = mock("printf format")
+ obj.should_receive(:to_str).and_return("%s")
+
+ @io.printf obj, "printf"
+ File.read(@name).should == "printf"
+ end
+
+ it "writes the #sprintf formatted string" do
+ @io.printf "%d %s", 5, "cookies"
+ File.read(@name).should == "5 cookies"
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.printf("stuff") }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/putc_spec.rb b/spec/ruby/core/io/putc_spec.rb
new file mode 100644
index 0000000000..73473ad821
--- /dev/null
+++ b/spec/ruby/core/io/putc_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/io/putc'
+
+describe "IO#putc" do
+ before :each do
+ @name = tmp("io_putc.txt")
+ @io_object = @io = new_io(@name)
+ end
+
+ it_behaves_like :io_putc, :putc
+end
diff --git a/spec/ruby/core/io/puts_spec.rb b/spec/ruby/core/io/puts_spec.rb
new file mode 100644
index 0000000000..df526ea56a
--- /dev/null
+++ b/spec/ruby/core/io/puts_spec.rb
@@ -0,0 +1,139 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#puts" do
+ before :each do
+ @before_separator = $/
+ @name = tmp("io_puts.txt")
+ @io = new_io @name
+ ScratchPad.record(+"")
+ def @io.write(str)
+ ScratchPad << str
+ end
+ end
+
+ after :each do
+ ScratchPad.clear
+ @io.close if @io
+ rm_r @name
+ suppress_warning {$/ = @before_separator}
+ end
+
+ it "writes just a newline when given no args" do
+ @io.puts.should == nil
+ ScratchPad.recorded.should == "\n"
+ end
+
+ it "writes just a newline when given just a newline" do
+ -> { $stdout.puts "\n" }.should output_to_fd("\n", $stdout)
+ end
+
+ it "writes empty string with a newline when given nil as an arg" do
+ @io.puts(nil).should == nil
+ ScratchPad.recorded.should == "\n"
+ end
+
+ it "writes empty string with a newline when given nil as multiple args" do
+ @io.puts(nil, nil).should == nil
+ ScratchPad.recorded.should == "\n\n"
+ end
+
+ it "calls :to_ary before writing non-string objects, regardless of it being implemented in the receiver" do
+ object = mock('hola')
+ object.should_receive(:method_missing).with(:to_ary)
+ object.should_receive(:to_s).and_return("#<Object:0x...>")
+
+ @io.puts(object).should == nil
+ ScratchPad.recorded.should == "#<Object:0x...>\n"
+ end
+
+ it "calls :to_ary before writing non-string objects" do
+ object = mock('hola')
+ object.should_receive(:to_ary).and_return(["hola"])
+
+ @io.puts(object).should == nil
+ ScratchPad.recorded.should == "hola\n"
+ end
+
+ it "calls :to_s before writing non-string objects that don't respond to :to_ary" do
+ object = mock('hola')
+ object.should_receive(:to_s).and_return("hola")
+
+ @io.puts(object).should == nil
+ ScratchPad.recorded.should == "hola\n"
+ end
+
+ it "returns general object info if :to_s does not return a string" do
+ object = mock('hola')
+ object.should_receive(:to_s).and_return(false)
+
+ @io.puts(object).should == nil
+ ScratchPad.recorded.should == object.inspect.split(" ")[0] + ">\n"
+ end
+
+ it "writes each arg if given several" do
+ @io.puts(1, "two", 3).should == nil
+ ScratchPad.recorded.should == "1\ntwo\n3\n"
+ end
+
+ it "flattens a nested array before writing it" do
+ @io.puts([1, 2, [3]]).should == nil
+ ScratchPad.recorded.should == "1\n2\n3\n"
+ end
+
+ it "writes nothing for an empty array" do
+ x = []
+ @io.should_not_receive(:write)
+ @io.puts(x).should == nil
+ end
+
+ it "writes [...] for a recursive array arg" do
+ x = []
+ x << 2 << x
+ @io.puts(x).should == nil
+ ScratchPad.recorded.should == "2\n[...]\n"
+ end
+
+ it "writes a newline after objects that do not end in newlines" do
+ @io.puts(5).should == nil
+ ScratchPad.recorded.should == "5\n"
+ end
+
+ it "does not write a newline after objects that end in newlines" do
+ @io.puts("5\n").should == nil
+ ScratchPad.recorded.should == "5\n"
+ end
+
+ it "ignores the $/ separator global" do
+ suppress_warning {$/ = ":"}
+ @io.puts(5).should == nil
+ ScratchPad.recorded.should == "5\n"
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.puts("stuff") }.should.raise(IOError)
+ end
+
+ it "writes crlf when IO is opened with newline: :crlf" do
+ File.open(@name, 'wt', newline: :crlf) do |file|
+ file.puts
+ end
+ File.binread(@name).should == "\r\n"
+ end
+
+ it "writes cr when IO is opened with newline: :cr" do
+ File.open(@name, 'wt', newline: :cr) do |file|
+ file.puts
+ end
+ File.binread(@name).should == "\r"
+ end
+
+ platform_is_not :windows do # https://bugs.ruby-lang.org/issues/12436
+ it "writes lf when IO is opened with newline: :lf" do
+ File.open(@name, 'wt', newline: :lf) do |file|
+ file.puts
+ end
+ File.binread(@name).should == "\n"
+ end
+ end
+end
diff --git a/spec/ruby/core/io/pwrite_spec.rb b/spec/ruby/core/io/pwrite_spec.rb
new file mode 100644
index 0000000000..c318d551bc
--- /dev/null
+++ b/spec/ruby/core/io/pwrite_spec.rb
@@ -0,0 +1,67 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "IO#pwrite" do
+ before :each do
+ @fname = tmp("io_pwrite.txt")
+ @file = File.open(@fname, "w+")
+ end
+
+ after :each do
+ @file.close
+ rm_r @fname
+ end
+
+ it "returns the number of bytes written" do
+ @file.pwrite("foo", 0).should == 3
+ end
+
+ it "accepts a string and an offset" do
+ @file.pwrite("foo", 2)
+ @file.pread(3, 2).should == "foo"
+ end
+
+ it "does not advance the pointer in the file" do
+ @file.pwrite("bar", 3)
+ @file.write("foo")
+ @file.pread(6, 0).should == "foobar"
+ end
+
+ it "calls #to_s on the object to be written" do
+ object = mock("to_s")
+ object.should_receive(:to_s).and_return("foo")
+ @file.pwrite(object, 0)
+ @file.pread(3, 0).should == "foo"
+ end
+
+ it "calls #to_int on the offset" do
+ offset = mock("to_int")
+ offset.should_receive(:to_int).and_return(2)
+ @file.pwrite("foo", offset)
+ @file.pread(3, 2).should == "foo"
+ end
+
+ it "raises IOError when file is not open in write mode" do
+ File.open(@fname, "r") do |file|
+ -> { file.pwrite("foo", 1) }.should.raise(IOError, "not opened for writing")
+ end
+ end
+
+ it "raises IOError when file is closed" do
+ file = File.open(@fname, "w+")
+ file.close
+ -> { file.pwrite("foo", 1) }.should.raise(IOError, "closed stream")
+ end
+
+ it "raises a NoMethodError if object does not respond to #to_s" do
+ -> {
+ @file.pwrite(BasicObject.new, 0)
+ }.should.raise(NoMethodError, /undefined method [`']to_s'/)
+ end
+
+ it "raises a TypeError if the offset cannot be converted to an Integer" do
+ -> {
+ @file.pwrite("foo", Object.new)
+ }.should.raise(TypeError, "no implicit conversion of Object into Integer")
+ end
+end
diff --git a/spec/ruby/core/io/read_nonblock_spec.rb b/spec/ruby/core/io/read_nonblock_spec.rb
new file mode 100644
index 0000000000..511cf03263
--- /dev/null
+++ b/spec/ruby/core/io/read_nonblock_spec.rb
@@ -0,0 +1,148 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#read_nonblock" do
+ before :each do
+ @read, @write = IO.pipe
+ end
+
+ after :each do
+ @read.close if @read && !@read.closed?
+ @write.close if @write && !@write.closed?
+ end
+
+ it "raises an exception extending IO::WaitReadable when there is no data" do
+ -> { @read.read_nonblock(5) }.should.raise(IO::WaitReadable) { |e|
+ platform_is_not :windows do
+ e.should.is_a?(Errno::EAGAIN)
+ end
+ platform_is :windows do
+ e.should.is_a?(Errno::EWOULDBLOCK)
+ end
+ }
+ end
+
+ context "when exception option is set to false" do
+ context "when there is no data" do
+ it "returns :wait_readable" do
+ @read.read_nonblock(5, exception: false).should == :wait_readable
+ end
+ end
+
+ context "when the end is reached" do
+ it "returns nil" do
+ @write << "hello"
+ @write.close
+
+ @read.read_nonblock(5)
+
+ @read.read_nonblock(5, exception: false).should == nil
+ end
+ end
+ end
+
+ platform_is_not :windows do
+ it 'sets the IO in nonblock mode' do
+ require 'io/nonblock'
+ @write.write "abc"
+ @read.read_nonblock(1).should == "a"
+ @read.should.nonblock?
+ end
+ end
+
+ it "returns at most the number of bytes requested" do
+ @write << "hello"
+ @read.read_nonblock(4).should == "hell"
+ end
+
+ it "reads after ungetc with data in the buffer" do
+ @write.write("foobar")
+ @read.set_encoding(
+ 'utf-8', universal_newline: false
+ )
+ c = @read.getc
+ @read.ungetc(c)
+ @read.read_nonblock(3).should == "foo"
+ @read.read_nonblock(3).should == "bar"
+ end
+
+ it "raises an exception after ungetc with data in the buffer and character conversion enabled" do
+ @write.write("foobar")
+ @read.set_encoding(
+ 'utf-8', universal_newline: true
+ )
+ c = @read.getc
+ @read.ungetc(c)
+ -> { @read.read_nonblock(3).should == "foo" }.should.raise(IOError)
+ end
+
+ it "returns less data if that is all that is available" do
+ @write << "hello"
+ @read.read_nonblock(10).should == "hello"
+ end
+
+ it "allows for reading 0 bytes before any write" do
+ @read.read_nonblock(0).should == ""
+ end
+
+ it "allows for reading 0 bytes after a write" do
+ @write.write "1"
+ @read.read_nonblock(0).should == ""
+ @read.read_nonblock(1).should == "1"
+ end
+
+ it "raises ArgumentError when length is less than 0" do
+ -> { @read.read_nonblock(-1) }.should.raise(ArgumentError)
+ end
+
+ it "reads into the passed buffer" do
+ buffer = +""
+ @write.write("1")
+ @read.read_nonblock(1, buffer)
+ buffer.should == "1"
+ end
+
+ it "returns the passed buffer" do
+ buffer = +""
+ @write.write("1")
+ output = @read.read_nonblock(1, buffer)
+ output.should.equal?(buffer)
+ end
+
+ it "discards the existing buffer content upon successful read" do
+ buffer = +"existing content"
+ @write.write("hello world")
+ @write.close
+ @read.read_nonblock(11, buffer)
+ buffer.should == "hello world"
+ end
+
+ it "discards the existing buffer content upon error" do
+ buffer = +"existing content"
+ @write.close
+ -> { @read.read_nonblock(1, buffer) }.should.raise(EOFError)
+ buffer.should.empty?
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.read_nonblock(5) }.should.raise(IOError)
+ end
+
+ it "raises EOFError when the end is reached" do
+ @write << "hello"
+ @write.close
+
+ @read.read_nonblock(5)
+
+ -> { @read.read_nonblock(5) }.should.raise(EOFError)
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @write.write("abc")
+ @write.close
+ @read.read_nonblock(10, buffer)
+
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
+end
diff --git a/spec/ruby/core/io/read_spec.rb b/spec/ruby/core/io/read_spec.rb
new file mode 100644
index 0000000000..dd787c9b60
--- /dev/null
+++ b/spec/ruby/core/io/read_spec.rb
@@ -0,0 +1,735 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO.read" do
+ before :each do
+ @fname = tmp("io_read.txt")
+ @contents = "1234567890"
+ touch(@fname) { |f| f.write @contents }
+ end
+
+ after :each do
+ rm_r @fname
+ end
+
+ it "reads the contents of a file" do
+ IO.read(@fname).should == @contents
+ end
+
+ it "calls #to_path on non-String arguments" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return(@fname)
+ IO.read(p)
+ end
+
+ # https://bugs.ruby-lang.org/issues/19354
+ it "accepts options as keyword arguments" do
+ IO.read(@fname, 3, 0, mode: "r+").should == @contents[0, 3]
+
+ -> {
+ IO.read(@fname, 3, 0, {mode: "r+"})
+ }.should.raise(ArgumentError, /wrong number of arguments/)
+ end
+
+ it "accepts an empty options Hash" do
+ IO.read(@fname, **{}).should == @contents
+ end
+
+ it "accepts a length, and empty options Hash" do
+ IO.read(@fname, 3, **{}).should == @contents[0, 3]
+ end
+
+ it "accepts a length, offset, and empty options Hash" do
+ IO.read(@fname, 3, 0, **{}).should == @contents[0, 3]
+ end
+
+ it "raises an IOError if the options Hash specifies write mode" do
+ -> { IO.read(@fname, 3, 0, mode: "w") }.should.raise(IOError)
+ end
+
+ it "raises an IOError if the options Hash specifies append only mode" do
+ -> { IO.read(@fname, mode: "a") }.should.raise(IOError)
+ end
+
+ it "reads the file if the options Hash includes read mode" do
+ IO.read(@fname, mode: "r").should == @contents
+ end
+
+ it "reads the file if the options Hash includes read/write mode" do
+ IO.read(@fname, mode: "r+").should == @contents
+ end
+
+ it "reads the file if the options Hash includes read/write append mode" do
+ IO.read(@fname, mode: "a+").should == @contents
+ end
+
+ platform_is_not :windows do
+ end
+
+ it "disregards other options if :open_args is given" do
+ string = IO.read(@fname,mode: "w", encoding: Encoding::UTF_32LE, open_args: ["r", encoding: Encoding::UTF_8])
+ string.encoding.should == Encoding::UTF_8
+ end
+
+ it "doesn't require mode to be specified in :open_args" do
+ string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII}])
+ string.encoding.should == Encoding::US_ASCII
+ end
+
+ it "doesn't require mode to be specified in :open_args even if flags option passed" do
+ string = IO.read(@fname, nil, 0, open_args: [{encoding: Encoding::US_ASCII, flags: File::CREAT}])
+ string.encoding.should == Encoding::US_ASCII
+ end
+
+ it "treats second nil argument as no length limit" do
+ IO.read(@fname, nil).should == @contents
+ IO.read(@fname, nil, 5).should == IO.read(@fname, @contents.length, 5)
+ end
+
+ it "treats third nil argument as 0" do
+ IO.read(@fname, nil, nil).should == @contents
+ IO.read(@fname, 5, nil).should == IO.read(@fname, 5, 0)
+ end
+
+ it "reads the contents of a file up to a certain size when specified" do
+ IO.read(@fname, 5).should == @contents.slice(0..4)
+ end
+
+ it "reads the contents of a file from an offset of a specific size when specified" do
+ IO.read(@fname, 5, 3).should == @contents.slice(3, 5)
+ end
+
+ it "returns nil at end-of-file when length is passed" do
+ IO.read(@fname, 1, 10).should == nil
+ end
+
+ it "returns an empty string when reading zero bytes" do
+ IO.read(@fname, 0).should == ''
+ end
+
+ it "returns a String in BINARY when passed a size" do
+ IO.read(@fname, 1).encoding.should == Encoding::BINARY
+ IO.read(@fname, 0).encoding.should == Encoding::BINARY
+ end
+
+ it "raises an Errno::ENOENT when the requested file does not exist" do
+ rm_r @fname
+ -> { IO.read @fname }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises a TypeError when not passed a String type" do
+ -> { IO.read nil }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when not passed a valid length" do
+ -> { IO.read @fname, -1 }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when not passed a valid offset" do
+ -> { IO.read @fname, 0, -1 }.should.raise(ArgumentError)
+ -> { IO.read @fname, -1, -1 }.should.raise(ArgumentError)
+ end
+
+ it "uses the external encoding specified via the :external_encoding option" do
+ str = IO.read(@fname, external_encoding: Encoding::ISO_8859_1)
+ str.encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "uses the external encoding specified via the :encoding option" do
+ str = IO.read(@fname, encoding: Encoding::ISO_8859_1)
+ str.encoding.should == Encoding::ISO_8859_1
+ end
+
+ platform_is :windows do
+ it "reads the file in text mode" do
+ # 0x1A is CTRL+Z and is EOF in Windows text mode.
+ File.binwrite(@fname, "\x1Abbb")
+ IO.read(@fname).should.empty?
+ end
+ end
+end
+
+ruby_version_is ""..."4.0" do
+ describe "IO.read from a pipe" do
+ it "runs the rest as a subprocess and returns the standard output" do
+ cmd = "|sh -c 'echo hello'"
+ platform_is :windows do
+ cmd = "|cmd.exe /C echo hello"
+ end
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read(cmd).should == "hello\n"
+ end
+ end
+
+ platform_is_not :windows do
+ it "opens a pipe to a fork if the rest is -" do
+ str = nil
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ str = IO.read("|-")
+ end
+
+ if str # parent
+ str.should == "hello from child\n"
+ else #child
+ puts "hello from child"
+ exit!
+ end
+ end
+ end
+
+ it "reads only the specified number of bytes requested" do
+ cmd = "|sh -c 'echo hello'"
+ platform_is :windows do
+ cmd = "|cmd.exe /C echo hello"
+ end
+
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read(cmd, 1).should == "h"
+ end
+ end
+
+ platform_is_not :windows do
+ it "raises Errno::ESPIPE if passed an offset" do
+ -> {
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read("|sh -c 'echo hello'", 1, 1)
+ end
+ }.should.raise(Errno::ESPIPE)
+ end
+ end
+
+ quarantine! do # The process tried to write to a nonexistent pipe.
+ platform_is :windows do
+ # TODO: It should raise Errno::ESPIPE on Windows as well
+ # once https://bugs.ruby-lang.org/issues/12230 is fixed.
+ it "raises Errno::EINVAL if passed an offset" do
+ -> {
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ IO.read("|cmd.exe /C echo hello", 1, 1)
+ end
+ }.should.raise(Errno::EINVAL)
+ end
+ end
+ end
+
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation" do
+ cmd = "|echo ok"
+ -> {
+ IO.read(cmd)
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
+end
+
+describe "IO.read on an empty file" do
+ before :each do
+ @fname = tmp("io_read_empty.txt")
+ touch(@fname)
+ end
+
+ after :each do
+ rm_r @fname
+ end
+
+ it "returns nil when length is passed" do
+ IO.read(@fname, 1).should == nil
+ end
+
+ it "returns an empty string when no length is passed" do
+ IO.read(@fname).should == ""
+ end
+end
+
+describe "IO#read" do
+
+ before :each do
+ @fname = tmp("io_read.txt")
+ @contents = "1234567890"
+ touch(@fname) { |f| f.write @contents }
+
+ @io = open @fname, "r+"
+ end
+
+ after :each do
+ @io.close
+ rm_r @fname
+ end
+
+ it "can be read from consecutively" do
+ @io.read(1).should == '1'
+ @io.read(2).should == '23'
+ @io.read(3).should == '456'
+ @io.read(4).should == '7890'
+ end
+
+ it "treats first nil argument as no length limit" do
+ @io.read(nil).should == @contents
+ end
+
+ it "raises an ArgumentError when not passed a valid length" do
+ -> { @io.read(-1) }.should.raise(ArgumentError)
+ end
+
+ it "clears the output buffer if there is nothing to read" do
+ @io.pos = 10
+
+ buf = +'non-empty string'
+
+ @io.read(10, buf).should == nil
+
+ buf.should == ''
+
+ buf = +'non-empty string'
+
+ @io.read(nil, buf).should == ""
+
+ buf.should == ''
+
+ buf = +'non-empty string'
+
+ @io.read(0, buf).should == ""
+
+ buf.should == ''
+ end
+
+ it "raise FrozenError if the output buffer is frozen" do
+ @io.read
+ -> { @io.read(0, 'frozen-string'.freeze) }.should.raise(FrozenError)
+ -> { @io.read(1, 'frozen-string'.freeze) }.should.raise(FrozenError)
+ -> { @io.read(nil, 'frozen-string'.freeze) }.should.raise(FrozenError)
+ end
+
+ it "raise FrozenError if the output buffer is frozen (2)" do
+ @io.read
+ -> { @io.read(1, ''.freeze) }.should.raise(FrozenError)
+ end
+
+ it "consumes zero bytes when reading zero bytes" do
+ @io.read(0).should == ''
+ @io.pos.should == 0
+
+ @io.getc.should == '1'
+ end
+
+ it "is at end-of-file when everything has been read" do
+ @io.read
+ @io.should.eof?
+ end
+
+ it "reads the contents of a file" do
+ @io.read.should == @contents
+ end
+
+ it "places the specified number of bytes in the buffer" do
+ buf = +""
+ @io.read 5, buf
+
+ buf.should == "12345"
+ end
+
+ it "expands the buffer when too small" do
+ buf = +"ABCDE"
+ @io.read nil, buf
+
+ buf.should == @contents
+ end
+
+ it "overwrites the buffer" do
+ buf = +"ABCDEFGHIJ"
+ @io.read nil, buf
+
+ buf.should == @contents
+ end
+
+ it "truncates the buffer when too big" do
+ buf = +"ABCDEFGHIJKLMNO"
+ @io.read nil, buf
+ buf.should == @contents
+
+ @io.rewind
+
+ buf = +"ABCDEFGHIJKLMNO"
+ @io.read 5, buf
+ buf.should == @contents[0..4]
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @io.read(10, buffer)
+
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
+
+ # https://bugs.ruby-lang.org/issues/20416
+ it "does not preserve the encoding of the given buffer when max length is not provided" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @io.read(nil, buffer)
+
+ buffer.encoding.should_not == Encoding::ISO_8859_1
+ end
+
+ it "returns the given buffer" do
+ buf = +""
+
+ @io.read(nil, buf).should.equal? buf
+ end
+
+ it "returns the given buffer when there is nothing to read" do
+ buf = +""
+
+ @io.read
+ @io.read(nil, buf).should.equal? buf
+ end
+
+ it "coerces the second argument to string and uses it as a buffer" do
+ buf = +"ABCDE"
+ obj = mock("buff")
+ obj.should_receive(:to_str).any_number_of_times.and_return(buf)
+
+ @io.read(15, obj).should_not.equal? obj
+ buf.should == @contents
+ end
+
+ it "returns an empty string at end-of-file" do
+ @io.read
+ @io.read.should == ''
+ end
+
+ it "reads the contents of a file when more bytes are specified" do
+ @io.read(@contents.length + 1).should == @contents
+ end
+
+ it "returns an empty string at end-of-file" do
+ @io.read
+ @io.read.should == ''
+ end
+
+ it "returns an empty string when the current pos is bigger than the content size" do
+ @io.pos = 1000
+ @io.read.should == ''
+ end
+
+ it "returns nil at end-of-file with a length" do
+ @io.read
+ @io.read(1).should == nil
+ end
+
+ it "with length argument returns nil when the current pos is bigger than the content size" do
+ @io.pos = 1000
+ @io.read(1).should == nil
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.read }.should.raise(IOError)
+ end
+
+ it "raises ArgumentError when length is less than 0" do
+ -> { @io.read(-1) }.should.raise(ArgumentError)
+ end
+
+ platform_is_not :windows do
+ it "raises IOError when stream is closed by another thread" do
+ r, w = IO.pipe
+ t = Thread.new do
+ begin
+ r.read(1)
+ rescue => e
+ e
+ end
+ end
+
+ Thread.pass until t.stop?
+ r.close
+ t.join
+ t.value.should.is_a?(IOError)
+ w.close
+ end
+ end
+end
+
+platform_is :windows do
+ describe "IO#read on Windows" do
+ before :each do
+ @fname = tmp("io_read.txt")
+ touch(@fname, "wb") { |f| f.write "a\r\nb\r\nc" }
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @fname
+ end
+
+ it "normalizes line endings in text mode" do
+ @io = new_io(@fname, "r")
+ @io.read.should == "a\nb\nc"
+ end
+
+ it "does not normalize line endings in binary mode" do
+ @io = new_io(@fname, "rb")
+ @io.read.should == "a\r\nb\r\nc"
+ end
+ end
+end
+
+describe "IO#read" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ it "ignores unicode encoding" do
+ @io.readline.should == "Voici la ligne une.\n"
+ # read "Qui è"
+ @io.read(5).should == "Qui " + [195].pack('C*')
+ end
+end
+
+describe "IO#read in binary mode" do
+ before :each do
+ @internal = Encoding.default_internal
+ @name = fixture __FILE__, "read_binary.txt"
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "does not transcode file contents when Encoding.default_internal is set" do
+ Encoding.default_internal = "utf-8"
+
+ result = File.open(@name, "rb") { |f| f.read }.chomp
+
+ result.encoding.should == Encoding::BINARY
+ xE2 = [226].pack('C*')
+ result.should == ("abc" + xE2 + "def").force_encoding(Encoding::BINARY)
+ end
+end
+
+describe "IO#read in text mode" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+ @name = fixture __FILE__, "read_text.txt"
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "reads data according to the internal encoding" do
+ Encoding.default_internal = "utf-8"
+ Encoding.default_external = "utf-8"
+
+ result = File.open(@name, "rt") { |f| f.read }.chomp
+
+ result.encoding.should == Encoding::UTF_8
+ result.should == "abcâdef"
+ end
+end
+
+describe "IO.read with BOM" do
+ it "reads a file without a bom" do
+ name = fixture __FILE__, "no_bom_UTF-8.txt"
+ result = File.read(name, mode: "rb:BOM|utf-8")
+ result.force_encoding("binary").should == "UTF-8\n"
+ end
+
+ it "reads a file with a utf-8 bom" do
+ name = fixture __FILE__, "bom_UTF-8.txt"
+ result = File.read(name, mode: "rb:BOM|utf-16le")
+ result.force_encoding("binary").should == "UTF-8\n"
+ end
+
+ it "reads a file with a utf-16le bom" do
+ name = fixture __FILE__, "bom_UTF-16LE.txt"
+ result = File.read(name, mode: "rb:BOM|utf-8")
+ result.force_encoding("binary").should == "U\x00T\x00F\x00-\x001\x006\x00L\x00E\x00\n\x00"
+ end
+
+ it "reads a file with a utf-16be bom" do
+ name = fixture __FILE__, "bom_UTF-16BE.txt"
+ result = File.read(name, mode: "rb:BOM|utf-8")
+ result.force_encoding("binary").should == "\x00U\x00T\x00F\x00-\x001\x006\x00B\x00E\x00\n"
+ end
+
+ it "reads a file with a utf-32le bom" do
+ name = fixture __FILE__, "bom_UTF-32LE.txt"
+ result = File.read(name, mode: "rb:BOM|utf-8")
+ result.force_encoding("binary").should == "U\x00\x00\x00T\x00\x00\x00F\x00\x00\x00-\x00\x00\x003\x00\x00\x002\x00\x00\x00L\x00\x00\x00E\x00\x00\x00\n\x00\x00\x00"
+ end
+
+ it "reads a file with a utf-32be bom" do
+ name = fixture __FILE__, "bom_UTF-32BE.txt"
+ result = File.read(name, mode: "rb:BOM|utf-8")
+ result.force_encoding("binary").should == "\x00\x00\x00U\x00\x00\x00T\x00\x00\x00F\x00\x00\x00-\x00\x00\x003\x00\x00\x002\x00\x00\x00B\x00\x00\x00E\x00\x00\x00\n"
+ end
+end
+
+describe :io_read_internal_encoding, shared: true do
+ it "returns a transcoded String" do
+ @io.read.should == "ã‚りãŒã¨ã†\n"
+ end
+
+ it "sets the String encoding to the internal encoding" do
+ @io.read.encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ describe "when passed nil for limit" do
+ it "sets the buffer to a transcoded String" do
+ result = @io.read(nil, buf = +"")
+ buf.should.equal?(result)
+ buf.should == "ã‚りãŒã¨ã†\n"
+ end
+
+ it "sets the buffer's encoding to the internal encoding" do
+ buf = "".dup.force_encoding Encoding::ISO_8859_1
+ @io.read(nil, buf)
+ buf.encoding.should.equal?(Encoding::UTF_8)
+ end
+ end
+end
+
+describe :io_read_size_internal_encoding, shared: true do
+ it "reads bytes when passed a size" do
+ @io.read(2).should == [164, 162].pack('C*').force_encoding(Encoding::BINARY)
+ end
+
+ it "returns a String in BINARY when passed a size" do
+ @io.read(4).encoding.should.equal?(Encoding::BINARY)
+ @io.read(0).encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "does not change the buffer's encoding when passed a limit" do
+ buf = "".dup.force_encoding Encoding::ISO_8859_1
+ @io.read(4, buf)
+ buf.should == [164, 162, 164, 234].pack('C*').force_encoding(Encoding::ISO_8859_1)
+ buf.encoding.should.equal?(Encoding::ISO_8859_1)
+ end
+
+ it "truncates the buffer but does not change the buffer's encoding when no data remains" do
+ buf = "abc".dup.force_encoding Encoding::ISO_8859_1
+ @io.read
+
+ @io.read(1, buf).should == nil
+ buf.size.should == 0
+ buf.encoding.should.equal?(Encoding::ISO_8859_1)
+ end
+end
+
+describe "IO#read" do
+ describe "when IO#external_encoding and IO#internal_encoding are nil" do
+ before :each do
+ @name = tmp("io_read.txt")
+ touch(@name) { |f| f.write "\x00\x01\x02" }
+ @io = new_io @name, "r+"
+ end
+
+ after :each do
+ @io.close if @io
+ rm_r @name
+ end
+
+ it "sets the String encoding to Encoding.default_external" do
+ @io.read.encoding.should.equal?(Encoding.default_external)
+ end
+ end
+
+ describe "with internal encoding" do
+ after :each do
+ @io.close if @io
+ end
+
+ describe "not specified" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp"
+ end
+
+ it "does not transcode the String" do
+ @io.read.should == ("ã‚りãŒã¨ã†\n").encode(Encoding::EUC_JP)
+ end
+
+ it "sets the String encoding to the external encoding" do
+ @io.read.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it_behaves_like :io_read_size_internal_encoding, nil
+ end
+
+ describe "specified by open mode" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8"
+ end
+
+ it_behaves_like :io_read_internal_encoding, nil
+ it_behaves_like :io_read_size_internal_encoding, nil
+ end
+
+ describe "specified by mode: option" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8"
+ end
+
+ it_behaves_like :io_read_internal_encoding, nil
+ it_behaves_like :io_read_size_internal_encoding, nil
+ end
+
+ describe "specified by internal_encoding: option" do
+ before :each do
+ options = { mode: "r",
+ internal_encoding: "utf-8",
+ external_encoding: "euc-jp" }
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", options
+ end
+
+ it_behaves_like :io_read_internal_encoding, nil
+ it_behaves_like :io_read_size_internal_encoding, nil
+ end
+
+ describe "specified by encoding: option" do
+ before :each do
+ options = { mode: "r", encoding: "euc-jp:utf-8" }
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", options
+ end
+
+ it_behaves_like :io_read_internal_encoding, nil
+ it_behaves_like :io_read_size_internal_encoding, nil
+ end
+ end
+end
+
+describe "IO#read with large data" do
+ before :each do
+ # TODO: what is the significance of this mystery math?
+ @data_size = 8096 * 2 + 1024
+ @data = "*" * @data_size
+
+ @fname = tmp("io_read.txt")
+ touch(@fname) { |f| f.write @data }
+ end
+
+ after :each do
+ rm_r @fname
+ end
+
+ it "reads all the data at once" do
+ File.open(@fname, 'r') { |io| ScratchPad.record io.read }
+
+ ScratchPad.recorded.size.should == @data_size
+ ScratchPad.recorded.should == @data
+ end
+
+ it "reads only the requested number of bytes" do
+ read_size = @data_size / 2
+ File.open(@fname, 'r') { |io| ScratchPad.record io.read(read_size) }
+
+ ScratchPad.recorded.size.should == read_size
+ ScratchPad.recorded.should == @data[0, read_size]
+ end
+end
diff --git a/spec/ruby/core/io/readbyte_spec.rb b/spec/ruby/core/io/readbyte_spec.rb
new file mode 100644
index 0000000000..07da1da919
--- /dev/null
+++ b/spec/ruby/core/io/readbyte_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "IO#readbyte" do
+ before :each do
+ @io = File.open(__FILE__, 'r')
+ end
+
+ after :each do
+ @io.close
+ end
+
+ it "reads one byte from the stream" do
+ byte = @io.readbyte
+ byte.should == ?r.getbyte(0)
+ @io.pos.should == 1
+ end
+
+ it "raises EOFError on EOF" do
+ @io.seek(999999)
+ -> do
+ @io.readbyte
+ end.should.raise EOFError
+ end
+end
diff --git a/spec/ruby/core/io/readchar_spec.rb b/spec/ruby/core/io/readchar_spec.rb
new file mode 100644
index 0000000000..29d14880ff
--- /dev/null
+++ b/spec/ruby/core/io/readchar_spec.rb
@@ -0,0 +1,110 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :io_readchar_internal_encoding, shared: true do
+ it "returns a transcoded String" do
+ @io.readchar.should == "ã‚"
+ end
+
+ it "sets the String encoding to the internal encoding" do
+ @io.readchar.encoding.should.equal?(Encoding::UTF_8)
+ end
+end
+
+describe "IO#readchar" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "returns the next string from the stream" do
+ @io.readchar.should == 'V'
+ @io.readchar.should == 'o'
+ @io.readchar.should == 'i'
+ # read the rest of line
+ @io.readline.should == "ci la ligne une.\n"
+ @io.readchar.should == 'Q'
+ end
+
+ it "raises an EOFError when invoked at the end of the stream" do
+ @io.read
+ -> { @io.readchar }.should.raise(EOFError)
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.readchar }.should.raise(IOError)
+ end
+end
+
+describe "IO#readchar with internal encoding" do
+ after :each do
+ @io.close if @io
+ end
+
+ describe "not specified" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp"
+ end
+
+ it "does not transcode the String" do
+ @io.readchar.should == ("ã‚").encode(Encoding::EUC_JP)
+ end
+
+ it "sets the String encoding to the external encoding" do
+ @io.readchar.encoding.should.equal?(Encoding::EUC_JP)
+ end
+ end
+
+ describe "specified by open mode" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", "r:euc-jp:utf-8"
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+
+ describe "specified by mode: option" do
+ before :each do
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", mode: "r:euc-jp:utf-8"
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+
+ describe "specified by internal_encoding: option" do
+ before :each do
+ options = { mode: "r",
+ internal_encoding: "utf-8",
+ external_encoding: "euc-jp" }
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", options
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+
+ describe "specified by encoding: option" do
+ before :each do
+ options = { mode: "r", encoding: "euc-jp:utf-8" }
+ @io = IOSpecs.io_fixture "read_euc_jp.txt", options
+ end
+
+ it_behaves_like :io_readchar_internal_encoding, nil
+ end
+end
+
+describe "IO#readchar" do
+ before :each do
+ @io = IOSpecs.io_fixture "empty.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "raises EOFError on empty stream" do
+ -> { @io.readchar }.should.raise(EOFError)
+ end
+end
diff --git a/spec/ruby/core/io/readline_spec.rb b/spec/ruby/core/io/readline_spec.rb
new file mode 100644
index 0000000000..009687710a
--- /dev/null
+++ b/spec/ruby/core/io/readline_spec.rb
@@ -0,0 +1,84 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#readline" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "returns the next line on the stream" do
+ @io.readline.should == "Voici la ligne une.\n"
+ @io.readline.should == "Qui è la linea due.\n"
+ end
+
+ it "goes back to first position after a rewind" do
+ @io.readline.should == "Voici la ligne une.\n"
+ @io.rewind
+ @io.readline.should == "Voici la ligne une.\n"
+ end
+
+ it "returns characters after the position set by #seek" do
+ @io.seek(1)
+ @io.readline.should == "oici la ligne une.\n"
+ end
+
+ it "raises EOFError on end of stream" do
+ IOSpecs.lines.length.times { @io.readline }
+ -> { @io.readline }.should.raise(EOFError)
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.readline }.should.raise(IOError)
+ end
+
+ it "assigns the returned line to $_" do
+ IOSpecs.lines.each do |line|
+ @io.readline
+ $_.should == line
+ end
+ end
+
+ describe "when passed limit" do
+ it "reads limit bytes" do
+ @io.readline(3).should == "Voi"
+ end
+
+ it "returns an empty string when passed 0 as a limit" do
+ @io.readline(0).should == ""
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { @io.readline(2**128) }.should.raise(RangeError)
+ end
+ end
+
+ describe "when passed separator and limit" do
+ it "reads limit bytes till the separator" do
+ # Voici la ligne une.\
+ @io.readline(" ", 4).should == "Voic"
+ @io.readline(" ", 4).should == "i "
+ @io.readline(" ", 4).should == "la "
+ @io.readline(" ", 4).should == "lign"
+ @io.readline(" ", 4).should == "e "
+ end
+ end
+
+ describe "when passed chomp" do
+ it "returns the first line without a trailing newline character" do
+ @io.readline(chomp: true).should == IOSpecs.lines_without_newline_characters[0]
+ end
+
+ it "raises exception when options passed as Hash" do
+ -> { @io.readline({ chomp: true }) }.should.raise(TypeError)
+
+ -> {
+ @io.readline("\n", 1, { chomp: true })
+ }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
+ end
+end
diff --git a/spec/ruby/core/io/readlines_spec.rb b/spec/ruby/core/io/readlines_spec.rb
new file mode 100644
index 0000000000..640e253200
--- /dev/null
+++ b/spec/ruby/core/io/readlines_spec.rb
@@ -0,0 +1,257 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/readlines'
+
+describe "IO#readlines" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ @orig_extenc = Encoding.default_external
+ Encoding.default_external = Encoding::UTF_8
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ Encoding.default_external = @orig_extenc
+ end
+
+ it "raises an IOError if the stream is closed" do
+ @io.close
+ -> { @io.readlines }.should.raise(IOError)
+ end
+
+ describe "when passed no arguments" do
+ before :each do
+ suppress_warning {@sep, $/ = $/, " "}
+ end
+
+ after :each do
+ suppress_warning {$/ = @sep}
+ end
+
+ it "returns an Array containing lines based on $/" do
+ @io.readlines.should == IOSpecs.lines_space_separator
+ end
+ end
+
+ describe "when passed no arguments" do
+ it "updates self's position" do
+ @io.readlines
+ @io.pos.should.eql?(137)
+ end
+
+ it "updates self's lineno based on the number of lines read" do
+ @io.readlines
+ @io.lineno.should.eql?(9)
+ end
+
+ it "does not change $_" do
+ $_ = "test"
+ @io.readlines
+ $_.should == "test"
+ end
+
+ it "returns an empty Array when self is at the end" do
+ @io.readlines.should == IOSpecs.lines
+ @io.readlines.should == []
+ end
+ end
+
+ describe "when passed nil" do
+ it "returns the remaining content as one line starting at the current position" do
+ @io.readlines(nil).should == [IOSpecs.lines.join]
+ end
+ end
+
+ describe "when passed an empty String" do
+ it "returns an Array containing all paragraphs" do
+ @io.readlines("").should == IOSpecs.paragraphs
+ end
+ end
+
+ describe "when passed a separator" do
+ it "returns an Array containing lines based on the separator" do
+ @io.readlines("r").should == IOSpecs.lines_r_separator
+ end
+
+ it "returns an empty Array when self is at the end" do
+ @io.readlines
+ @io.readlines("r").should == []
+ end
+
+ it "updates self's lineno based on the number of lines read" do
+ @io.readlines("r")
+ @io.lineno.should.eql?(5)
+ end
+
+ it "updates self's position based on the number of characters read" do
+ @io.readlines("r")
+ @io.pos.should.eql?(137)
+ end
+
+ it "does not change $_" do
+ $_ = "test"
+ @io.readlines("r")
+ $_.should == "test"
+ end
+
+ it "tries to convert the passed separator to a String using #to_str" do
+ obj = mock('to_str')
+ obj.stub!(:to_str).and_return("r")
+ @io.readlines(obj).should == IOSpecs.lines_r_separator
+ end
+ end
+
+ describe "when passed limit" do
+ it "raises ArgumentError when passed 0 as a limit" do
+ -> { @io.readlines(0) }.should.raise(ArgumentError)
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { @io.readlines(2**128) }.should.raise(RangeError)
+ end
+ end
+
+ describe "when passed chomp" do
+ it "returns the first line without a trailing newline character" do
+ @io.readlines(chomp: true).should == IOSpecs.lines_without_newline_characters
+ end
+
+ it "raises exception when options passed as Hash" do
+ -> { @io.readlines({ chomp: true }) }.should.raise(TypeError)
+
+ -> {
+ @io.readlines("\n", 1, { chomp: true })
+ }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
+ end
+
+ describe "when passed arbitrary keyword argument" do
+ it "tolerates it" do
+ @io.readlines(chomp: true, foo: :bar).should == IOSpecs.lines_without_newline_characters
+ end
+ end
+end
+
+describe "IO#readlines" do
+ before :each do
+ @name = tmp("io_readlines")
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "raises an IOError if the stream is opened for append only" do
+ -> do
+ File.open(@name, "a:utf-8") { |f| f.readlines }
+ end.should.raise(IOError)
+ end
+
+ it "raises an IOError if the stream is opened for write only" do
+ -> do
+ File.open(@name, "w:utf-8") { |f| f.readlines }
+ end.should.raise(IOError)
+ end
+end
+
+describe "IO.readlines" do
+ before :each do
+ @external = Encoding.default_external
+ Encoding.default_external = Encoding::UTF_8
+
+ @name = fixture __FILE__, "lines.txt"
+ ScratchPad.record []
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ end
+
+ it "does not change $_" do
+ $_ = "test"
+ IO.readlines(@name)
+ $_.should == "test"
+ end
+
+ ruby_version_is ""..."4.0" do
+ describe "when passed a string that starts with a |" do
+ it "gets data from the standard out of the subprocess" do
+ cmd = "|sh -c 'echo hello;echo line2'"
+ platform_is :windows do
+ cmd = "|cmd.exe /C echo hello&echo line2"
+ end
+
+ lines = nil
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ lines = IO.readlines(cmd)
+ end
+ lines.should == ["hello\n", "line2\n"]
+ end
+
+ platform_is_not :windows do
+ it "gets data from a fork when passed -" do
+ lines = nil
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ lines = IO.readlines("|-")
+ end
+
+ if lines # parent
+ lines.should == ["hello\n", "from a fork\n"]
+ else
+ puts "hello"
+ puts "from a fork"
+ exit!
+ end
+ end
+ end
+ end
+
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ IO.readlines(cmd)
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
+
+ it_behaves_like :io_readlines, :readlines
+ it_behaves_like :io_readlines_options_19, :readlines
+end
+
+describe "IO.readlines" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+ @name = fixture __FILE__, "lines.txt"
+ @dollar_slash = $/
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ suppress_warning {$/ = @dollar_slash}
+ end
+
+ it "encodes lines using the default external encoding" do
+ Encoding.default_external = Encoding::UTF_8
+ lines = IO.readlines(@name)
+ lines.all? { |s| s.encoding == Encoding::UTF_8 }.should == true
+ end
+
+ it "encodes lines using the default internal encoding, when set" do
+ Encoding.default_external = Encoding::UTF_8
+ Encoding.default_internal = Encoding::UTF_16
+ suppress_warning {$/ = $/.encode Encoding::UTF_16}
+ lines = IO.readlines(@name)
+ lines.all? { |s| s.encoding == Encoding::UTF_16 }.should == true
+ end
+
+ it "ignores the default internal encoding if the external encoding is BINARY" do
+ Encoding.default_external = Encoding::BINARY
+ Encoding.default_internal = Encoding::UTF_8
+ lines = IO.readlines(@name)
+ lines.all? { |s| s.encoding == Encoding::BINARY }.should == true
+ end
+end
diff --git a/spec/ruby/core/io/readpartial_spec.rb b/spec/ruby/core/io/readpartial_spec.rb
new file mode 100644
index 0000000000..d3f5545c8f
--- /dev/null
+++ b/spec/ruby/core/io/readpartial_spec.rb
@@ -0,0 +1,115 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#readpartial" do
+ before :each do
+ @rd, @wr = IO.pipe
+ @rd.binmode
+ @wr.binmode
+ end
+
+ after :each do
+ @rd.close unless @rd.closed?
+ @wr.close unless @wr.closed?
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.readpartial(10) }.should.raise(IOError)
+
+ @rd.close
+ -> { @rd.readpartial(10) }.should.raise(IOError)
+ end
+
+ it "reads at most the specified number of bytes" do
+ @wr.write("foobar")
+
+ # buffered read
+ @rd.read(1).should == 'f'
+ # return only specified number, not the whole buffer
+ @rd.readpartial(1).should == "o"
+ end
+
+ it "reads after ungetc with data in the buffer" do
+ @wr.write("foobar")
+ c = @rd.getc
+ @rd.ungetc(c)
+ @rd.readpartial(3).should == "foo"
+ @rd.readpartial(3).should == "bar"
+ end
+
+ it "reads after ungetc with multibyte characters in the buffer" do
+ @wr.write("∂φ/∂x = gaîté")
+ c = @rd.getc
+ @rd.ungetc(c)
+ @rd.readpartial(3).should == "\xE2\x88\x82"
+ @rd.readpartial(3).should == "\xCF\x86/"
+ end
+
+ it "reads after ungetc without data in the buffer" do
+ @wr.write("f")
+ c = @rd.getc
+ @rd.ungetc(c)
+ @rd.readpartial(2).should == "f"
+
+ # now, also check that the ungot char is cleared and
+ # not returned again
+ @wr.write("b")
+ @rd.readpartial(2).should == "b"
+ end
+
+ it "discards the existing buffer content upon successful read" do
+ buffer = +"existing content"
+ @wr.write("hello world")
+ @wr.close
+ @rd.readpartial(11, buffer).should.equal?(buffer)
+ buffer.should == "hello world"
+ end
+
+ it "raises EOFError on EOF" do
+ @wr.write("abc")
+ @wr.close
+ @rd.readpartial(10).should == 'abc'
+ -> { @rd.readpartial(10) }.should.raise(EOFError)
+ end
+
+ it "discards the existing buffer content upon error" do
+ buffer = +'hello'
+ @wr.close
+ -> { @rd.readpartial(1, buffer) }.should.raise(EOFError)
+ buffer.should.empty?
+ end
+
+ it "raises IOError if the stream is closed" do
+ @wr.close
+ -> { @rd.readpartial(1) }.should.raise(IOError)
+ end
+
+ it "raises ArgumentError if the negative argument is provided" do
+ -> { @rd.readpartial(-1) }.should.raise(ArgumentError)
+ end
+
+ it "immediately returns an empty string if the length argument is 0" do
+ @rd.readpartial(0).should == ""
+ end
+
+ it "raises IOError if the stream is closed and the length argument is 0" do
+ @rd.close
+ -> { @rd.readpartial(0) }.should.raise(IOError, "closed stream")
+ end
+
+ it "clears and returns the given buffer if the length argument is 0" do
+ buffer = +"existing content"
+ @rd.readpartial(0, buffer).should == buffer
+ buffer.should == ""
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ @wr.write("abc")
+ @wr.close
+ @rd.readpartial(10, buffer)
+
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
+end
diff --git a/spec/ruby/core/io/reopen_spec.rb b/spec/ruby/core/io/reopen_spec.rb
new file mode 100644
index 0000000000..3b972d8978
--- /dev/null
+++ b/spec/ruby/core/io/reopen_spec.rb
@@ -0,0 +1,313 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+require 'fcntl'
+
+describe "IO#reopen" do
+ before :each do
+ @name = tmp("io_reopen.txt")
+ @other_name = tmp("io_reopen_other.txt")
+
+ @io = new_io @name
+ @other_io = File.open @other_name, "w"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ @other_io.close unless @other_io.closed?
+ rm_r @name, @other_name
+ end
+
+ it "calls #to_io to convert an object" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_return(@other_io)
+ @io.reopen obj
+ end
+
+ it "changes the class of the instance to the class of the object returned by #to_io" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_return(@other_io)
+ @io.reopen(obj).should.instance_of?(File)
+ end
+
+ it "raises an IOError if the object returned by #to_io is closed" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_return(IOSpecs.closed_io)
+ -> { @io.reopen obj }.should.raise(IOError)
+ end
+
+ it "raises a TypeError if #to_io does not return an IO instance" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_return("something else")
+ -> { @io.reopen obj }.should.raise(TypeError)
+ end
+
+ it "raises an IOError when called on a closed stream with an object" do
+ @io.close
+ obj = mock("io")
+ obj.should_not_receive(:to_io)
+ -> { @io.reopen(STDOUT) }.should.raise(IOError)
+ end
+
+ it "raises an IOError if the IO argument is closed" do
+ -> { @io.reopen(IOSpecs.closed_io) }.should.raise(IOError)
+ end
+
+ it "raises an IOError when called on a closed stream with an IO" do
+ @io.close
+ -> { @io.reopen(STDOUT) }.should.raise(IOError)
+ end
+end
+
+describe "IO#reopen with a String" do
+ before :each do
+ @name = fixture __FILE__, "numbered_lines.txt"
+ @other_name = tmp("io_reopen.txt")
+ touch @other_name
+ @io = IOSpecs.io_fixture "lines.txt"
+
+ @tmp_file = tmp("reopen")
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @other_name, @tmp_file
+ end
+
+ it "does not raise an exception when called on a closed stream with a path" do
+ @io.close
+ @io.reopen @name, "r"
+ @io.closed?.should == false
+ @io.gets.should == "Line 1: One\n"
+ end
+
+ it "returns self" do
+ @io.reopen(@name).should.equal?(@io)
+ end
+
+ it "positions a newly created instance at the beginning of the new stream" do
+ @io.reopen(@name)
+ @io.gets.should == "Line 1: One\n"
+ end
+
+ it "positions an instance that has been read from at the beginning of the new stream" do
+ @io.gets
+ @io.reopen(@name)
+ @io.gets.should == "Line 1: One\n"
+ end
+
+ platform_is_not :windows do
+ it "passes all mode flags through" do
+ @io.reopen(@tmp_file, "ab")
+ (@io.fcntl(Fcntl::F_GETFL) & File::APPEND).should == File::APPEND
+ end
+ end
+
+ platform_is_not :windows do
+ # TODO Should this work on Windows?
+ it "affects exec/system/fork performed after it" do
+ ruby_exe fixture(__FILE__, "reopen_stdout.rb"), args: @tmp_file
+ File.read(@tmp_file).should == "from system\nfrom exec\n"
+ end
+ end
+
+ it "calls #to_path on non-String arguments" do
+ obj = mock('path')
+ obj.should_receive(:to_path).and_return(@other_name)
+ @io.reopen(obj)
+ end
+end
+
+describe "IO#reopen with a String" do
+ before :each do
+ @name = tmp("io_reopen.txt")
+ @other_name = tmp("io_reopen_other.txt")
+ @other_io = nil
+
+ rm_r @other_name
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ @other_io.close if @other_io and not @other_io.closed?
+ rm_r @name, @other_name
+ end
+
+ it "opens a path after writing to the original file descriptor" do
+ @io = new_io @name, "w"
+
+ @io.print "original data"
+ @io.reopen @other_name
+ @io.print "new data"
+ @io.flush
+
+ File.read(@name).should == "original data"
+ File.read(@other_name).should == "new data"
+ end
+
+ it "always resets the close-on-exec flag to true on non-STDIO objects" do
+ @io = new_io @name, "w"
+
+ @io.close_on_exec = true
+ @io.reopen @other_name
+ @io.should.close_on_exec?
+
+ @io.close_on_exec = false
+ @io.reopen @other_name
+ @io.should.close_on_exec?
+ end
+
+ it "creates the file if it doesn't exist if the IO is opened in write mode" do
+ @io = new_io @name, "w"
+
+ @io.reopen(@other_name)
+ File.should.exist?(@other_name)
+ end
+
+ it "creates the file if it doesn't exist if the IO is opened in write mode" do
+ @io = new_io @name, "a"
+
+ @io.reopen(@other_name)
+ File.should.exist?(@other_name)
+ end
+end
+
+describe "IO#reopen with a String" do
+ before :each do
+ @name = tmp("io_reopen.txt")
+ @other_name = tmp("io_reopen_other.txt")
+
+ touch @name
+ rm_r @other_name
+ end
+
+ after :each do
+ @io.close
+ rm_r @name, @other_name
+ end
+
+ it "raises an Errno::ENOENT if the file does not exist and the IO is not opened in write mode" do
+ @io = new_io @name, "r"
+ -> { @io.reopen(@other_name) }.should.raise(Errno::ENOENT)
+ end
+end
+
+describe "IO#reopen with an IO at EOF" do
+ before :each do
+ @name = tmp("io_reopen.txt")
+ touch(@name) { |f| f.puts "a line" }
+ @other_name = tmp("io_reopen_other.txt")
+ touch(@other_name) do |f|
+ f.puts "Line 1"
+ f.puts "Line 2"
+ end
+
+ @io = new_io @name, "r"
+ @other_io = new_io @other_name, "r"
+ @io.read
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ @other_io.close unless @other_io.closed?
+ rm_r @name, @other_name
+ end
+
+ it "resets the EOF status to false" do
+ @io.eof?.should == true
+ @io.reopen @other_io
+ @io.eof?.should == false
+ end
+end
+
+describe "IO#reopen with an IO" do
+ before :each do
+ @name = tmp("io_reopen.txt")
+ @other_name = tmp("io_reopen_other.txt")
+ touch(@other_name) do |f|
+ f.puts "Line 1"
+ f.puts "Line 2"
+ end
+
+ @io = new_io @name
+ @other_io = IO.new(new_fd(@other_name, "r"), "r")
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ @other_io.close unless @other_io.closed?
+ rm_r @name, @other_name
+ end
+
+ it "does not call #to_io" do
+ # Why do we not use #should_not_receive(:to_io) here? Because
+ # MRI actually changes the class of @io in the call to #reopen
+ # but does not preserve the existing singleton class of @io.
+ def @io.to_io; flunk; end
+ @io.reopen(@other_io).should.instance_of?(IO)
+ end
+
+ it "does not change the object_id" do
+ obj_id = @io.object_id
+ @io.reopen @other_io
+ @io.object_id.should == obj_id
+ end
+
+ it "reads from the beginning if the other IO has not been read from" do
+ @io.reopen @other_io
+ @io.gets.should == "Line 1\n"
+ end
+
+ it "reads from the current position of the other IO's stream" do
+ @other_io.gets.should == "Line 1\n"
+ @io.reopen @other_io
+ @io.gets.should == "Line 2\n"
+ end
+end
+
+describe "IO#reopen with an IO" do
+ before :each do
+ @name = tmp("io_reopen.txt")
+ @other_name = tmp("io_reopen_other.txt")
+
+ @io = new_io @name
+ @other_io = File.open @other_name, "w"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ @other_io.close unless @other_io.closed?
+ rm_r @name, @other_name
+ end
+
+ it "associates the IO instance with the other IO's stream" do
+ File.read(@other_name).should == ""
+ @io.reopen @other_io
+ @io.print "io data"
+ @io.flush
+ File.read(@name).should == ""
+ File.read(@other_name).should == "io data"
+ end
+
+ it "always resets the close-on-exec flag to true on non-STDIO objects" do
+ @other_io.close_on_exec = true
+ @io.close_on_exec = true
+ @io.reopen @other_io
+ @io.should.close_on_exec?
+
+ @other_io.close_on_exec = false
+ @io.close_on_exec = false
+ @io.reopen @other_io
+ @io.should.close_on_exec?
+ end
+
+ it "may change the class of the instance" do
+ @io.reopen @other_io
+ @io.should.instance_of?(File)
+ end
+
+ it "sets path equals to the other IO's path if other IO is File" do
+ @io.reopen @other_io
+ @io.path.should == @other_io.path
+ end
+end
diff --git a/spec/ruby/core/io/rewind_spec.rb b/spec/ruby/core/io/rewind_spec.rb
new file mode 100644
index 0000000000..43834ef307
--- /dev/null
+++ b/spec/ruby/core/io/rewind_spec.rb
@@ -0,0 +1,53 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#rewind" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "positions the instance to the beginning of input" do
+ @io.readline.should == "Voici la ligne une.\n"
+ @io.readline.should == "Qui è la linea due.\n"
+ @io.rewind
+ @io.readline.should == "Voici la ligne une.\n"
+ end
+
+ it "positions the instance to the beginning of output for write-only IO" do
+ name = tmp("io_rewind_spec")
+ io = File.open(name, "w")
+ io.write("Voici la ligne une.\n")
+ io.rewind
+ io.pos.should == 0
+ ensure
+ io.close
+ rm_r name
+ end
+
+ it "positions the instance to the beginning of input and clears EOF" do
+ value = @io.read
+ @io.rewind
+ @io.should_not.eof?
+ value.should == @io.read
+ end
+
+ it "sets lineno to 0" do
+ @io.readline.should == "Voici la ligne une.\n"
+ @io.lineno.should == 1
+ @io.rewind
+ @io.lineno.should == 0
+ end
+
+ it "returns 0" do
+ @io.rewind.should == 0
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.rewind }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/seek_spec.rb b/spec/ruby/core/io/seek_spec.rb
new file mode 100644
index 0000000000..d26629fb89
--- /dev/null
+++ b/spec/ruby/core/io/seek_spec.rb
@@ -0,0 +1,79 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/pos'
+
+describe "IO#seek" do
+ it_behaves_like :io_set_pos, :seek
+end
+
+describe "IO#seek" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "moves the read position relative to the current position with SEEK_CUR" do
+ -> { @io.seek(-1) }.should.raise(Errno::EINVAL)
+ @io.seek(10, IO::SEEK_CUR)
+ @io.readline.should == "igne une.\n"
+ @io.seek(-5, IO::SEEK_CUR)
+ @io.readline.should == "une.\n"
+ end
+
+ it "moves the read position relative to the start with SEEK_SET" do
+ @io.seek(1)
+ @io.pos.should == 1
+ @io.rewind
+ @io.seek(43, IO::SEEK_SET)
+ @io.readline.should == "Aquí está la línea tres.\n"
+ @io.seek(5, IO::SEEK_SET)
+ @io.readline.should == " la ligne une.\n"
+ end
+
+ it "moves the read position relative to the end with SEEK_END" do
+ @io.seek(0, IO::SEEK_END)
+ @io.tell.should == 137
+ @io.seek(-25, IO::SEEK_END)
+ @io.readline.should == "cinco.\n"
+ end
+
+ it "moves the read position and clears EOF with SEEK_SET" do
+ value = @io.read
+ @io.seek(0, IO::SEEK_SET)
+ @io.should_not.eof?
+ value.should == @io.read
+ end
+
+ it "moves the read position and clears EOF with SEEK_CUR" do
+ value = @io.read
+ @io.seek(-1, IO::SEEK_CUR)
+ @io.should_not.eof?
+ value[-1].should == @io.read[0]
+ end
+
+ it "moves the read position and clears EOF with SEEK_END" do
+ value = @io.read
+ @io.seek(-1, IO::SEEK_END)
+ @io.should_not.eof?
+ value[-1].should == @io.read[0]
+ end
+
+ platform_is :darwin do
+ it "supports seek offsets greater than 2^32" do
+ begin
+ zero = File.open('/dev/zero')
+ offset = 2**33
+ zero.seek(offset, File::SEEK_SET)
+ pos = zero.pos
+
+ pos.should == offset
+ ensure
+ zero.close rescue nil
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/select_spec.rb b/spec/ruby/core/io/select_spec.rb
new file mode 100644
index 0000000000..0a43fc6f5f
--- /dev/null
+++ b/spec/ruby/core/io/select_spec.rb
@@ -0,0 +1,190 @@
+require_relative '../../spec_helper'
+
+describe "IO.select" do
+ before :each do
+ @rd, @wr = IO.pipe
+ end
+
+ after :each do
+ @rd.close unless @rd.closed?
+ @wr.close unless @wr.closed?
+ end
+
+ it "blocks for duration of timeout and returns nil if there are no objects ready for I/O" do
+ IO.select([@rd], nil, nil, 0.001).should == nil
+ end
+
+ it "returns immediately all objects that are ready for I/O when timeout is 0" do
+ @wr.syswrite("be ready")
+ IO.pipe do |_, wr|
+ result = IO.select [@rd], [wr], nil, 0
+ unless result
+ # On some platforms (e.g., Windows), pipe readiness may not be immediate
+ result = IO.select [@rd], [wr], nil, 2
+ end
+ result.should == [[@rd], [wr], []]
+ end
+ end
+
+ it "returns nil after timeout if there are no objects ready for I/O" do
+ result = IO.select [@rd], nil, nil, 0
+ result.should == nil
+ end
+
+ it "returns supplied objects when they are ready for I/O" do
+ main = Thread.current
+ t = Thread.new {
+ Thread.pass until main.status == "sleep"
+ @wr.write "be ready"
+ }
+ result = IO.select [@rd], nil, nil, nil
+ result.should == [[@rd], [], []]
+ t.join
+ end
+
+ it "leaves out IO objects for which there is no I/O ready" do
+ @wr.write "be ready"
+ platform_is :aix do
+ # In AIX, when a pipe is readable, select(2) returns the write side
+ # of the pipe as "readable", even though you cannot actually read
+ # anything from the write side.
+ result = IO.select [@wr, @rd], nil, nil, nil
+ result.should == [[@wr, @rd], [], []]
+ end
+ platform_is_not :aix do
+ # Order matters here. We want to see that @wr doesn't expand the size
+ # of the returned array, so it must be 1st.
+ result = IO.select [@wr, @rd], nil, nil, nil
+ result.should == [[@rd], [], []]
+ end
+ end
+
+ it "returns supplied objects correctly when monitoring the same object in different arrays" do
+ filename = tmp("IO_select_pipe_file")
+ io = File.open(filename, 'w+')
+ result = IO.select [io], [io], nil, 0
+ result.should == [[io], [io], []]
+ io.close
+ rm_r filename
+ end
+
+ it "returns the pipe read end in read set if the pipe write end is closed concurrently" do
+ main = Thread.current
+ t = Thread.new {
+ Thread.pass until main.stop?
+ @wr.close
+ }
+ IO.select([@rd]).should == [[@rd], [], []]
+ ensure
+ t.join
+ end
+
+ it "invokes to_io on supplied objects that are not IO and returns the supplied objects" do
+ # make some data available
+ @wr.write("foobar")
+
+ obj = mock("read_io")
+ obj.should_receive(:to_io).at_least(1).and_return(@rd)
+ IO.select([obj]).should == [[obj], [], []]
+
+ IO.pipe do |_, wr|
+ obj = mock("write_io")
+ obj.should_receive(:to_io).at_least(1).and_return(wr)
+ IO.select(nil, [obj]).should == [[], [obj], []]
+ end
+ end
+
+ it "raises TypeError if supplied objects are not IO" do
+ -> { IO.select([Object.new]) }.should.raise(TypeError)
+ -> { IO.select(nil, [Object.new]) }.should.raise(TypeError)
+
+ obj = mock("io")
+ obj.should_receive(:to_io).any_number_of_times.and_return(nil)
+
+ -> { IO.select([obj]) }.should.raise(TypeError)
+ -> { IO.select(nil, [obj]) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the specified timeout value is not Numeric" do
+ -> { IO.select([@rd], nil, nil, Object.new) }.should.raise(TypeError)
+ end
+
+ it "raises TypeError if the first three arguments are not Arrays" do
+ -> { IO.select(Object.new)}.should.raise(TypeError)
+ -> { IO.select(nil, Object.new)}.should.raise(TypeError)
+ -> { IO.select(nil, nil, Object.new)}.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when passed a negative timeout" do
+ -> { IO.select(nil, nil, nil, -5)}.should.raise(ArgumentError, "time interval must not be negative")
+ end
+
+ ruby_version_is "4.0" do
+ it "raises an ArgumentError when passed negative infinity as timeout" do
+ -> { IO.select(nil, nil, nil, -Float::INFINITY)}.should.raise(ArgumentError, "time interval must not be negative")
+ end
+ end
+
+ it "raises an RangeError when passed NaN as timeout" do
+ -> { IO.select(nil, nil, nil, Float::NAN)}.should.raise(RangeError, "NaN out of Time range")
+ end
+
+ describe "returns the available descriptors when the file descriptor" do
+ it "is in both read and error arrays" do
+ @wr.write("foobar")
+ result = IO.select([@rd], nil, [@rd])
+ result.should == [[@rd], [], []]
+ end
+
+ it "is in both write and error arrays" do
+ result = IO.select(nil, [@wr], [@wr])
+ result.should == [[], [@wr], []]
+ end
+
+ it "is in both read and write arrays" do
+ filename = tmp("IO_select_read_write_file")
+ w = File.open(filename, 'w+')
+ begin
+ IO.select([w], [w], []).should == [[w], [w], []]
+ ensure
+ w.close
+ rm_r filename
+ end
+
+ IO.select([@wr], [@wr], []).should == [[], [@wr], []]
+
+ @wr.write("foobar")
+ # CRuby on macOS returns [[@rd], [@rd], []], weird but we accept it here, probably only for pipe read-end
+ [
+ [[@rd], [], []],
+ [[@rd], [@rd], []]
+ ].should.include? IO.select([@rd], [@rd], [])
+ end
+ end
+end
+
+describe "IO.select with infinite timeout" do
+ describe :io_select_infinite_timeout, shared: true do
+ it "sleeps forever and sets the thread status to 'sleep'" do
+ t = Thread.new do
+ IO.select(nil, nil, nil, @method)
+ end
+
+ Thread.pass while t.status && t.status != "sleep"
+ t.join unless t.status
+ t.status.should == "sleep"
+ t.kill
+ t.join
+ end
+ end
+
+ describe "IO.select when passed nil for timeout" do
+ it_behaves_like :io_select_infinite_timeout, nil
+ end
+
+ ruby_version_is "4.0" do
+ describe "IO.select when passed Float::INFINITY for timeout" do
+ it_behaves_like :io_select_infinite_timeout, Float::INFINITY
+ end
+ end
+end
diff --git a/spec/ruby/core/io/set_encoding_by_bom_spec.rb b/spec/ruby/core/io/set_encoding_by_bom_spec.rb
new file mode 100644
index 0000000000..5436879f11
--- /dev/null
+++ b/spec/ruby/core/io/set_encoding_by_bom_spec.rb
@@ -0,0 +1,262 @@
+require_relative '../../spec_helper'
+
+describe "IO#set_encoding_by_bom" do
+ before :each do
+ @name = tmp('io_set_encoding_by_bom.txt')
+ touch(@name)
+ @io = new_io(@name, 'rb')
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "returns nil if not readable" do
+ not_readable_io = new_io(@name, 'wb')
+
+ not_readable_io.set_encoding_by_bom.should == nil
+ not_readable_io.external_encoding.should == Encoding::ASCII_8BIT
+ ensure
+ not_readable_io.close
+ end
+
+ it "returns the result encoding if found BOM UTF-8 sequence" do
+ File.binwrite(@name, "\u{FEFF}")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_8
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\u{FEFF}abc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_8
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_16LE sequence" do
+ File.binwrite(@name, "\xFF\xFE")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\xFF\xFEabc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_16BE sequence" do
+ File.binwrite(@name, "\xFE\xFF")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16BE
+ @io.external_encoding.should == Encoding::UTF_16BE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\xFE\xFFabcd")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16BE
+ @io.external_encoding.should == Encoding::UTF_16BE
+ @io.read.b.should == "abcd".b
+ end
+
+ it "returns the result encoding if found BOM UTF_32LE sequence" do
+ File.binwrite(@name, "\xFF\xFE\x00\x00")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32LE
+ @io.external_encoding.should == Encoding::UTF_32LE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\xFF\xFE\x00\x00abc")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32LE
+ @io.external_encoding.should == Encoding::UTF_32LE
+ @io.read.b.should == "abc".b
+ end
+
+ it "returns the result encoding if found BOM UTF_32BE sequence" do
+ File.binwrite(@name, "\x00\x00\xFE\xFF")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32BE
+ @io.external_encoding.should == Encoding::UTF_32BE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.binwrite(@name, "\x00\x00\xFE\xFFabcd")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_32BE
+ @io.external_encoding.should == Encoding::UTF_32BE
+ @io.read.b.should == "abcd".b
+ end
+
+ it "returns nil if io is empty" do
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ end
+
+ it "returns nil if UTF-8 BOM sequence is incomplete" do
+ File.write(@name, "\xEF")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEF".b
+ @io.rewind
+
+ File.write(@name, "\xEFa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEFa".b
+ @io.rewind
+
+ File.write(@name, "\xEF\xBB")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEF\xBB".b
+ @io.rewind
+
+ File.write(@name, "\xEF\xBBa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xEF\xBBa".b
+ end
+
+ it "returns nil if UTF-16BE BOM sequence is incomplete" do
+ File.write(@name, "\xFE")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFE".b
+ @io.rewind
+
+ File.write(@name, "\xFEa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFEa".b
+ end
+
+ it "returns nil if UTF-16LE/UTF-32LE BOM sequence is incomplete" do
+ File.write(@name, "\xFF")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFF".b
+ @io.rewind
+
+ File.write(@name, "\xFFa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\xFFa".b
+ end
+
+ it "returns UTF-16LE if UTF-32LE BOM sequence is incomplete" do
+ File.write(@name, "\xFF\xFE")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.write(@name, "\xFF\xFE\x00")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "\x00".b
+ @io.rewind
+ @io.set_encoding(Encoding::ASCII_8BIT)
+
+ File.write(@name, "\xFF\xFE\x00a")
+
+ @io.set_encoding_by_bom.should == Encoding::UTF_16LE
+ @io.external_encoding.should == Encoding::UTF_16LE
+ @io.read.b.should == "\x00a".b
+ end
+
+ it "returns nil if UTF-32BE BOM sequence is incomplete" do
+ File.write(@name, "\x00")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00".b
+ @io.rewind
+
+ File.write(@name, "\x00a")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00a".b
+ @io.rewind
+
+ File.write(@name, "\x00\x00")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00".b
+ @io.rewind
+
+ File.write(@name, "\x00\x00a")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00a".b
+ @io.rewind
+
+ File.write(@name, "\x00\x00\xFE")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00\xFE".b
+ @io.rewind
+
+ File.write(@name, "\x00\x00\xFEa")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read.b.should == "\x00\x00\xFEa".b
+ end
+
+ it "returns nil if found BOM sequence not provided" do
+ File.write(@name, "abc")
+
+ @io.set_encoding_by_bom.should == nil
+ @io.external_encoding.should == Encoding::ASCII_8BIT
+ @io.read(3).should == "abc".b
+ end
+
+ it 'returns exception if io not in binary mode' do
+ not_binary_io = new_io(@name, 'r')
+
+ -> { not_binary_io.set_encoding_by_bom }.should.raise(ArgumentError, 'ASCII incompatible encoding needs binmode')
+ ensure
+ not_binary_io.close
+ end
+
+ it 'returns exception if encoding already set' do
+ @io.set_encoding("utf-8")
+
+ -> { @io.set_encoding_by_bom }.should.raise(ArgumentError, 'encoding is set to UTF-8 already')
+ end
+
+ it 'returns exception if encoding conversion is already set' do
+ @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE)
+
+ -> { @io.set_encoding_by_bom }.should.raise(ArgumentError, 'encoding conversion is set')
+ end
+end
diff --git a/spec/ruby/core/io/set_encoding_spec.rb b/spec/ruby/core/io/set_encoding_spec.rb
new file mode 100644
index 0000000000..237251de5b
--- /dev/null
+++ b/spec/ruby/core/io/set_encoding_spec.rb
@@ -0,0 +1,238 @@
+require_relative '../../spec_helper'
+
+describe :io_set_encoding_write, shared: true do
+ it "sets the encodings to nil when they were set previously" do
+ @io = new_io @name, "#{@object}:ibm437:ibm866"
+ @io.set_encoding nil, nil
+
+ @io.external_encoding.should == nil
+ @io.internal_encoding.should == nil
+ end
+
+ it "sets the encodings to nil when the IO is built with no explicit encoding" do
+ @io = new_io @name, @object
+
+ # Checking our assumptions first
+ @io.external_encoding.should == nil
+ @io.internal_encoding.should == nil
+
+ @io.set_encoding nil, nil
+
+ @io.external_encoding.should == nil
+ @io.internal_encoding.should == nil
+ end
+
+ it "prevents the encodings from changing when Encoding defaults are changed" do
+ @io = new_io @name, "#{@object}:utf-8:us-ascii"
+ @io.set_encoding nil, nil
+
+ Encoding.default_external = Encoding::IBM437
+ Encoding.default_internal = Encoding::IBM866
+
+ @io.external_encoding.should == nil
+ @io.internal_encoding.should == nil
+ end
+
+ it "sets the encodings to the current Encoding defaults" do
+ @io = new_io @name, @object
+
+ Encoding.default_external = Encoding::IBM437
+ Encoding.default_internal = Encoding::IBM866
+
+ @io.set_encoding nil, nil
+
+ @io.external_encoding.should == Encoding::IBM437
+ @io.internal_encoding.should == Encoding::IBM866
+ end
+end
+
+describe "IO#set_encoding when passed nil, nil" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ # The defaults
+ Encoding.default_external = Encoding::UTF_8
+ Encoding.default_internal = nil
+
+ @name = tmp('io_set_encoding.txt')
+ touch(@name)
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+
+ @io.close if @io and not @io.closed?
+ rm_r @name
+ end
+
+ describe "with 'r' mode" do
+ it "sets the encodings to the current Encoding defaults" do
+ @io = new_io @name, "r"
+
+ Encoding.default_external = Encoding::IBM437
+ Encoding.default_internal = Encoding::IBM866
+
+ @io.set_encoding nil, nil
+ @io.external_encoding.should.equal?(Encoding::IBM437)
+ @io.internal_encoding.should.equal?(Encoding::IBM866)
+ end
+
+ it "prevents the #internal_encoding from changing when Encoding.default_internal is changed" do
+ @io = new_io @name, "r"
+ @io.set_encoding nil, nil
+
+ Encoding.default_internal = Encoding::IBM437
+
+ @io.internal_encoding.should == nil
+ end
+
+ it "allows the #external_encoding to change when Encoding.default_external is changed" do
+ @io = new_io @name, "r"
+ @io.set_encoding nil, nil
+
+ Encoding.default_external = Encoding::IBM437
+
+ @io.external_encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "with 'rb' mode" do
+ it "returns Encoding.default_external" do
+ @io = new_io @name, "rb"
+ @io.external_encoding.should.equal?(Encoding::BINARY)
+
+ @io.set_encoding nil, nil
+ @io.external_encoding.should.equal?(Encoding.default_external)
+ end
+ end
+
+ describe "with 'r+' mode" do
+ it_behaves_like :io_set_encoding_write, nil, "r+"
+ end
+
+ describe "with 'w' mode" do
+ it_behaves_like :io_set_encoding_write, nil, "w"
+ end
+
+ describe "with 'w+' mode" do
+ it_behaves_like :io_set_encoding_write, nil, "w+"
+ end
+
+ describe "with 'a' mode" do
+ it_behaves_like :io_set_encoding_write, nil, "a"
+ end
+
+ describe "with 'a+' mode" do
+ it_behaves_like :io_set_encoding_write, nil, "a+"
+ end
+
+ describe "with standard IOs" do
+ it "correctly resets them" do
+ STDOUT.external_encoding.should == nil
+ STDOUT.internal_encoding.should == nil
+
+ begin
+ STDOUT.set_encoding(Encoding::US_ASCII, Encoding::ISO_8859_1)
+ ensure
+ STDOUT.set_encoding(nil, nil)
+ end
+
+ STDOUT.external_encoding.should == nil
+ STDOUT.internal_encoding.should == nil
+ end
+ end
+end
+
+describe "IO#set_encoding" do
+ before :each do
+ @name = tmp('io_set_encoding.txt')
+ touch(@name)
+ @io = new_io @name
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "returns self" do
+ @io.set_encoding(Encoding::UTF_8).should.equal?(@io)
+ end
+
+ it "sets the external encoding when passed an Encoding argument" do
+ @io.set_encoding(Encoding::UTF_8)
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == nil
+ end
+
+ it "sets the external and internal encoding when passed two Encoding arguments" do
+ @io.set_encoding(Encoding::UTF_8, Encoding::UTF_16BE)
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == Encoding::UTF_16BE
+ end
+
+ it "sets the external encoding when passed the name of an Encoding" do
+ @io.set_encoding("utf-8")
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == nil
+ end
+
+ it "ignores the internal encoding if the same as external when passed Encoding objects" do
+ @io.set_encoding(Encoding::UTF_8, Encoding::UTF_8)
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == nil
+ end
+
+ it "ignores the internal encoding if the same as external when passed encoding names separated by ':'" do
+ @io.set_encoding("utf-8:utf-8")
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == nil
+ end
+
+ it "sets the external and internal encoding when passed the names of Encodings separated by ':'" do
+ @io.set_encoding("utf-8:utf-16be")
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == Encoding::UTF_16BE
+ end
+
+ it "sets the external and internal encoding when passed two String arguments" do
+ @io.set_encoding("utf-8", "utf-16be")
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == Encoding::UTF_16BE
+ end
+
+ it "calls #to_str to convert an abject to a String" do
+ obj = mock("io_set_encoding")
+ obj.should_receive(:to_str).and_return("utf-8:utf-16be")
+ @io.set_encoding(obj)
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == Encoding::UTF_16BE
+ end
+
+ it "calls #to_str to convert the second argument to a String" do
+ obj = mock("io_set_encoding")
+ obj.should_receive(:to_str).at_least(1).times.and_return("utf-16be")
+ @io.set_encoding(Encoding::UTF_8, obj)
+ @io.external_encoding.should == Encoding::UTF_8
+ @io.internal_encoding.should == Encoding::UTF_16BE
+ end
+
+ it "saves encoding options passed as a hash in the last argument" do
+ File.write(@name, "\xff")
+ io = File.open(@name)
+ io.set_encoding(Encoding::EUC_JP, Encoding::SHIFT_JIS, invalid: :replace, replace: ".")
+ io.read.should == "."
+ ensure
+ io.close
+ end
+
+ it "raises ArgumentError when no arguments are given" do
+ -> { @io.set_encoding() }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when too many arguments are given" do
+ -> { @io.set_encoding(1, 2, 3) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/io/shared/binwrite.rb b/spec/ruby/core/io/shared/binwrite.rb
new file mode 100644
index 0000000000..64793b1936
--- /dev/null
+++ b/spec/ruby/core/io/shared/binwrite.rb
@@ -0,0 +1,91 @@
+require_relative '../fixtures/classes'
+
+describe :io_binwrite, shared: true do
+ before :each do
+ @filename = tmp("IO_binwrite_file") + $$.to_s
+ File.open(@filename, "w") do |file|
+ file << "012345678901234567890123456789"
+ end
+ end
+
+ after :each do
+ rm_r @filename
+ end
+
+ it "coerces the argument to a string using to_s" do
+ (obj = mock('test')).should_receive(:to_s).and_return('a string')
+ IO.send(@method, @filename, obj)
+ end
+
+ it "returns the number of bytes written" do
+ IO.send(@method, @filename, "abcde").should == 5
+ end
+
+ it "accepts options as a keyword argument" do
+ IO.send(@method, @filename, "hi", 0, flags: File::CREAT).should == 2
+
+ -> {
+ IO.send(@method, @filename, "hi", 0, {flags: File::CREAT})
+ }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 2..3)")
+ end
+
+ it "creates a file if missing" do
+ fn = @filename + "xxx"
+ begin
+ File.should_not.exist?(fn)
+ IO.send(@method, fn, "test")
+ File.should.exist?(fn)
+ ensure
+ rm_r fn
+ end
+ end
+
+ it "creates file if missing even if offset given" do
+ fn = @filename + "xxx"
+ begin
+ File.should_not.exist?(fn)
+ IO.send(@method, fn, "test", 0)
+ File.should.exist?(fn)
+ ensure
+ rm_r fn
+ end
+ end
+
+ it "truncates the file and writes the given string" do
+ IO.send(@method, @filename, "hello, world!")
+ File.read(@filename).should == "hello, world!"
+ end
+
+ it "doesn't truncate the file and writes the given string if an offset is given" do
+ IO.send(@method, @filename, "hello, world!", 0)
+ File.read(@filename).should == "hello, world!34567890123456789"
+ IO.send(@method, @filename, "hello, world!", 20)
+ File.read(@filename).should == "hello, world!3456789hello, world!"
+ end
+
+ it "doesn't truncate and writes at the given offset after passing empty opts" do
+ IO.send(@method, @filename, "hello world!", 1, **{})
+ File.read(@filename).should == "0hello world!34567890123456789"
+ end
+
+ it "accepts a :mode option" do
+ IO.send(@method, @filename, "hello, world!", mode: 'a')
+ File.read(@filename).should == "012345678901234567890123456789hello, world!"
+ IO.send(@method, @filename, "foo", 2, mode: 'w')
+ File.read(@filename).should == "\0\0foo"
+ end
+
+ it "accepts a :flags option without :mode one" do
+ IO.send(@method, @filename, "hello, world!", flags: File::CREAT)
+ File.read(@filename).should == "hello, world!"
+ end
+
+ it "raises an error if readonly mode is specified" do
+ -> { IO.send(@method, @filename, "abcde", mode: "r") }.should.raise(IOError)
+ end
+
+ it "truncates if empty :opts provided and offset skipped" do
+ IO.send(@method, @filename, "hello, world!", **{})
+ File.read(@filename).should == "hello, world!"
+ end
+end
diff --git a/spec/ruby/core/io/shared/chars.rb b/spec/ruby/core/io/shared/chars.rb
new file mode 100644
index 0000000000..efd4d5ee10
--- /dev/null
+++ b/spec/ruby/core/io/shared/chars.rb
@@ -0,0 +1,73 @@
+# -*- encoding: utf-8 -*-
+describe :io_chars, shared: true do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ ScratchPad.record []
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "yields each character" do
+ @io.readline.should == "Voici la ligne une.\n"
+
+ count = 0
+ @io.send(@method) do |c|
+ ScratchPad << c
+ break if 4 < count += 1
+ end
+
+ ScratchPad.recorded.should == ["Q", "u", "i", " ", "è"]
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ enum = @io.send(@method)
+ enum.should.instance_of?(Enumerator)
+ enum.first(5).should == ["V", "o", "i", "c", "i"]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @io.send(@method).size.should == nil
+ end
+ end
+ end
+ end
+
+ it "returns itself" do
+ @io.send(@method) { |c| }.should.equal?(@io)
+ end
+
+ it "returns an enumerator for a closed stream" do
+ IOSpecs.closed_io.send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "raises an IOError when an enumerator created on a closed stream is accessed" do
+ -> { IOSpecs.closed_io.send(@method).first }.should.raise(IOError)
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.send(@method) {} }.should.raise(IOError)
+ end
+end
+
+describe :io_chars_empty, shared: true do
+ before :each do
+ @name = tmp("io_each_char")
+ @io = new_io @name, "w+:utf-8"
+ ScratchPad.record []
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "does not yield any characters on an empty stream" do
+ @io.send(@method) { |c| ScratchPad << c }
+ ScratchPad.recorded.should == []
+ end
+end
diff --git a/spec/ruby/core/io/shared/codepoints.rb b/spec/ruby/core/io/shared/codepoints.rb
new file mode 100644
index 0000000000..21c756986f
--- /dev/null
+++ b/spec/ruby/core/io/shared/codepoints.rb
@@ -0,0 +1,54 @@
+# -*- encoding: utf-8 -*-
+require_relative '../fixtures/classes'
+
+describe :io_codepoints, shared: true do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ @enum = @io.send(@method)
+ end
+
+ after :each do
+ @io.close
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ @enum.should.instance_of?(Enumerator)
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @enum.size.should == nil
+ end
+ end
+ end
+ end
+
+ it "yields each codepoint" do
+ @enum.first(25).should == [
+ 86, 111, 105, 99, 105, 32, 108, 97, 32, 108, 105, 103, 110,
+ 101, 32, 117, 110, 101, 46, 10, 81, 117, 105, 32, 232
+ ]
+ end
+
+ it "yields each codepoint starting from the current position" do
+ @io.pos = 130
+ @enum.to_a.should == [101, 32, 115, 105, 120, 46, 10]
+ end
+
+ it "raises an error if reading invalid sequence" do
+ @io.pos = 60 # inside of a multibyte sequence
+ -> { @enum.first }.should.raise(ArgumentError)
+ end
+
+ it "does not change $_" do
+ $_ = "test"
+ @enum.to_a
+ $_.should == "test"
+ end
+
+ it "raises an IOError when self is not readable" do
+ -> { IOSpecs.closed_io.send(@method).to_a }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/shared/each.rb b/spec/ruby/core/io/shared/each.rb
new file mode 100644
index 0000000000..ae60c3506a
--- /dev/null
+++ b/spec/ruby/core/io/shared/each.rb
@@ -0,0 +1,251 @@
+# -*- encoding: utf-8 -*-
+require_relative '../fixtures/classes'
+
+describe :io_each, shared: true do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ ScratchPad.record []
+ end
+
+ after :each do
+ @io.close if @io
+ end
+
+ describe "with no separator" do
+ it "yields each line to the passed block" do
+ @io.send(@method) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines
+ end
+
+ it "yields each line starting from the current position" do
+ @io.pos = 41
+ @io.send(@method) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines[2..-1]
+ end
+
+ it "returns self" do
+ @io.send(@method) { |l| l }.should.equal?(@io)
+ end
+
+ it "does not change $_" do
+ $_ = "test"
+ @io.send(@method) { |s| s }
+ $_.should == "test"
+ end
+
+ it "raises an IOError when self is not readable" do
+ -> { IOSpecs.closed_io.send(@method) {} }.should.raise(IOError)
+ end
+
+ it "makes line count accessible via lineno" do
+ @io.send(@method) { ScratchPad << @io.lineno }
+ ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ]
+ end
+
+ it "makes line count accessible via $." do
+ @io.send(@method) { ScratchPad << $. }
+ ScratchPad.recorded.should == [ 1,2,3,4,5,6,7,8,9 ]
+ end
+
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ enum = @io.send(@method)
+ enum.should.instance_of?(Enumerator)
+
+ enum.each { |l| ScratchPad << l }
+ ScratchPad.recorded.should == IOSpecs.lines
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @io.send(@method).size.should == nil
+ end
+ end
+ end
+ end
+ end
+
+ describe "with limit" do
+ describe "when limit is 0" do
+ it "raises an ArgumentError" do
+ # must pass block so Enumerator is evaluated and raises
+ -> { @io.send(@method, 0){} }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { @io.send(@method, 2**128){} }.should.raise(RangeError)
+ end
+ end
+
+ describe "when passed a String containing one space as a separator" do
+ it "uses the passed argument as the line separator" do
+ @io.send(@method, " ") { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines_space_separator
+ end
+
+ it "does not change $_" do
+ $_ = "test"
+ @io.send(@method, " ") { |s| }
+ $_.should == "test"
+ end
+
+ it "tries to convert the passed separator to a String using #to_str" do
+ obj = mock("to_str")
+ obj.stub!(:to_str).and_return(" ")
+
+ @io.send(@method, obj) { |l| ScratchPad << l }
+ ScratchPad.recorded.should == IOSpecs.lines_space_separator
+ end
+ end
+
+ describe "when passed nil as a separator" do
+ it "yields self's content starting from the current position when the passed separator is nil" do
+ @io.pos = 100
+ @io.send(@method, nil) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"]
+ end
+ end
+
+ describe "when passed an empty String as a separator" do
+ it "yields each paragraph" do
+ @io.send(@method, "") { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs
+ end
+
+ it "discards leading newlines" do
+ @io.readline
+ @io.readline
+ @io.send(@method, "") { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1]
+ end
+ end
+
+ describe "with both separator and limit" do
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ enum = @io.send(@method, nil, 1024)
+ enum.should.instance_of?(Enumerator)
+
+ enum.each { |l| ScratchPad << l }
+ ScratchPad.recorded.should == [IOSpecs.lines.join]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ @io.send(@method, nil, 1024).size.should == nil
+ end
+ end
+ end
+ end
+
+ describe "when a block is given" do
+ it "accepts an empty block" do
+ @io.send(@method, nil, 1024) {}.should.equal?(@io)
+ end
+
+ describe "when passed nil as a separator" do
+ it "yields self's content starting from the current position when the passed separator is nil" do
+ @io.pos = 100
+ @io.send(@method, nil, 1024) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"]
+ end
+ end
+
+ describe "when passed an empty String as a separator" do
+ it "yields each paragraph" do
+ @io.send(@method, "", 1024) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs
+ end
+
+ it "discards leading newlines" do
+ @io.readline
+ @io.readline
+ @io.send(@method, "", 1024) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs[1..-1]
+ end
+ end
+ end
+ end
+
+ describe "when passed chomp" do
+ it "yields each line without trailing newline characters to the passed block" do
+ @io.send(@method, chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines_without_newline_characters
+ end
+
+ it "raises exception when options passed as Hash" do
+ -> {
+ @io.send(@method, { chomp: true }) { |s| }
+ }.should.raise(TypeError)
+
+ -> {
+ @io.send(@method, "\n", 1, { chomp: true }) { |s| }
+ }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 0..2)")
+ end
+ end
+
+ describe "when passed chomp and a separator" do
+ it "yields each line without separator to the passed block" do
+ @io.send(@method, " ", chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines_space_separator_without_trailing_spaces
+ end
+ end
+
+ describe "when passed chomp and empty line as a separator" do
+ it "yields each paragraph without trailing new line characters" do
+ @io.send(@method, "", 1024, chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.paragraphs_without_trailing_new_line_characters
+ end
+ end
+
+ describe "when passed chomp and nil as a separator" do
+ it "yields self's content" do
+ @io.pos = 100
+ @io.send(@method, nil, chomp: true) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == ["qui a linha cinco.\nHere is line six.\n"]
+ end
+ end
+
+ describe "when passed chomp, nil as a separator, and a limit" do
+ it "yields each line of limit size without truncating trailing new line character" do
+ # 43 - is a size of the 1st paragraph in the file
+ @io.send(@method, nil, 43, chomp: true) { |s| ScratchPad << s }
+
+ ScratchPad.recorded.should == [
+ "Voici la ligne une.\nQui è la linea due.\n\n\n",
+ "Aquí está la línea tres.\n" + "Hier ist Zeile ",
+ "vier.\n\nEstá aqui a linha cinco.\nHere is li",
+ "ne six.\n"
+ ]
+ end
+ end
+
+ describe "when passed too many arguments" do
+ it "raises ArgumentError" do
+ -> {
+ @io.send(@method, "", 1, "excess argument", chomp: true) {}
+ }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe :io_each_default_separator, shared: true do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ ScratchPad.record []
+ suppress_warning {@sep, $/ = $/, " "}
+ end
+
+ after :each do
+ @io.close if @io
+ suppress_warning {$/ = @sep}
+ end
+
+ it "uses $/ as the default line separator" do
+ @io.send(@method) { |s| ScratchPad << s }
+ ScratchPad.recorded.should == IOSpecs.lines_space_separator
+ end
+end
diff --git a/spec/ruby/core/io/shared/gets_ascii.rb b/spec/ruby/core/io/shared/gets_ascii.rb
new file mode 100644
index 0000000000..2bd5470d99
--- /dev/null
+++ b/spec/ruby/core/io/shared/gets_ascii.rb
@@ -0,0 +1,19 @@
+# encoding: binary
+describe :io_gets_ascii, shared: true do
+ describe "with ASCII separator" do
+ before :each do
+ @name = tmp("gets_specs.txt")
+ touch(@name, "wb") { |f| f.print "this is a test\xFFtesty\ntestier" }
+
+ File.open(@name, "rb") { |f| @data = f.send(@method, "\xFF") }
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "returns the separator's character representation" do
+ @data.should == "this is a test\xFF"
+ end
+ end
+end
diff --git a/spec/ruby/core/io/shared/new.rb b/spec/ruby/core/io/shared/new.rb
new file mode 100644
index 0000000000..6f318ddee5
--- /dev/null
+++ b/spec/ruby/core/io/shared/new.rb
@@ -0,0 +1,413 @@
+require_relative '../fixtures/classes'
+
+# NOTE: should be synchronized with library/stringio/initialize_spec.rb
+
+# This group of specs may ONLY contain specs that do successfully create
+# an IO instance from the file descriptor returned by #new_fd helper.
+describe :io_new, shared: true do
+ before :each do
+ @name = tmp("io_new.txt")
+ @fd = new_fd @name
+ @io = nil
+ end
+
+ after :each do
+ if @io
+ @io.close
+ elsif @fd
+ IO.new(@fd, "w").close
+ end
+ rm_r @name
+ end
+
+ it "creates an IO instance from an Integer argument" do
+ @io = IO.send(@method, @fd, "w")
+ @io.should.instance_of?(IO)
+ end
+
+ it "creates an IO instance when STDOUT is closed" do
+ suppress_warning do
+ stdout = STDOUT
+ stdout_file = tmp("stdout.txt")
+
+ begin
+ @io = IO.send(@method, @fd, "w")
+ @io.should.instance_of?(IO)
+ ensure
+ STDOUT = stdout
+ rm_r stdout_file
+ end
+ end
+ end
+
+ it "creates an IO instance when STDERR is closed" do
+ suppress_warning do
+ stderr = STDERR
+ stderr_file = tmp("stderr.txt")
+ STDERR = new_io stderr_file
+ STDERR.close
+
+ begin
+ @io = IO.send(@method, @fd, "w")
+ @io.should.instance_of?(IO)
+ ensure
+ STDERR = stderr
+ rm_r stderr_file
+ end
+ end
+ end
+
+ it "calls #to_int on an object to convert to an Integer" do
+ obj = mock("file descriptor")
+ obj.should_receive(:to_int).and_return(@fd)
+ @io = IO.send(@method, obj, "w")
+ @io.should.instance_of?(IO)
+ end
+
+ it "accepts options as keyword arguments" do
+ @io = IO.send(@method, @fd, "w", flags: File::CREAT)
+ @io.write("foo").should == 3
+
+ -> {
+ IO.send(@method, @fd, "w", {flags: File::CREAT})
+ }.should.raise(ArgumentError, "wrong number of arguments (given 3, expected 1..2)")
+ end
+
+ it "accepts a :mode option" do
+ @io = IO.send(@method, @fd, mode: "w")
+ @io.write("foo").should == 3
+ end
+
+ it "accepts a mode argument set to nil with a valid :mode option" do
+ @io = IO.send(@method, @fd, nil, mode: "w")
+ @io.write("foo").should == 3
+ end
+
+ it "accepts a mode argument with a :mode option set to nil" do
+ @io = IO.send(@method, @fd, "w", mode: nil)
+ @io.write("foo").should == 3
+ end
+
+ it "uses the external encoding specified in the mode argument" do
+ @io = IO.send(@method, @fd, 'w:utf-8')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ end
+
+ it "uses the external and the internal encoding specified in the mode argument" do
+ @io = IO.send(@method, @fd, 'w:utf-8:ISO-8859-1')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "uses the external encoding specified via the :external_encoding option" do
+ @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ end
+
+ it "uses the internal encoding specified via the :internal_encoding option" do
+ @io = IO.send(@method, @fd, 'w', internal_encoding: 'ibm866')
+ @io.internal_encoding.to_s.should == 'IBM866'
+ end
+
+ it "uses the colon-separated encodings specified via the :encoding option" do
+ @io = IO.send(@method, @fd, 'w', encoding: 'utf-8:ISO-8859-1')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "uses the :encoding option as the external encoding when only one is given" do
+ @io = IO.send(@method, @fd, 'w', encoding: 'ISO-8859-1')
+ @io.external_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "uses the :encoding options as the external encoding when it's an Encoding object" do
+ @io = IO.send(@method, @fd, 'w', encoding: Encoding::ISO_8859_1)
+ @io.external_encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "ignores the :encoding option when the :external_encoding option is present" do
+ -> {
+ @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', encoding: 'iso-8859-1:iso-8859-1')
+ }.should complain(/Ignoring encoding parameter/)
+ @io.external_encoding.to_s.should == 'UTF-8'
+ end
+
+ it "ignores the :encoding option when the :internal_encoding option is present" do
+ -> {
+ @io = IO.send(@method, @fd, 'w', internal_encoding: 'ibm866', encoding: 'iso-8859-1:iso-8859-1')
+ }.should complain(/Ignoring encoding parameter/)
+ @io.internal_encoding.to_s.should == 'IBM866'
+ end
+
+ it "uses the encoding specified via the :mode option hash" do
+ @io = IO.send(@method, @fd, mode: 'w:utf-8:ISO-8859-1')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "ignores the :internal_encoding option when the same as the external encoding" do
+ @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', internal_encoding: 'utf-8')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == ''
+ end
+
+ it "sets internal encoding to nil when passed '-'" do
+ @io = IO.send(@method, @fd, 'w', external_encoding: 'utf-8', internal_encoding: '-')
+ @io.external_encoding.to_s.should == 'UTF-8'
+ @io.internal_encoding.to_s.should == ''
+ end
+
+ it "sets binmode from mode string" do
+ @io = IO.send(@method, @fd, 'wb')
+ @io.should.binmode?
+ end
+
+ it "does not set binmode without being asked" do
+ @io = IO.send(@method, @fd, 'w')
+ @io.should_not.binmode?
+ end
+
+ it "sets binmode from :binmode option" do
+ @io = IO.send(@method, @fd, 'w', binmode: true)
+ @io.should.binmode?
+ end
+
+ it "does not set binmode from false :binmode" do
+ @io = IO.send(@method, @fd, 'w', binmode: false)
+ @io.should_not.binmode?
+ end
+
+ it "sets external encoding to binary with binmode in mode string" do
+ @io = IO.send(@method, @fd, 'wb')
+ @io.external_encoding.should == Encoding::BINARY
+ end
+
+ # #5917
+ it "sets external encoding to binary with :binmode option" do
+ @io = IO.send(@method, @fd, 'w', binmode: true)
+ @io.external_encoding.should == Encoding::BINARY
+ end
+
+ it "does not use binary encoding when mode encoding is specified" do
+ @io = IO.send(@method, @fd, 'wb:iso-8859-1')
+ @io.external_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "does not use binary encoding when :encoding option is specified" do
+ @io = IO.send(@method, @fd, 'wb', encoding: "iso-8859-1")
+ @io.external_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "does not use binary encoding when :external_encoding option is specified" do
+ @io = IO.send(@method, @fd, 'wb', external_encoding: "iso-8859-1")
+ @io.external_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "does not use binary encoding when :internal_encoding option is specified" do
+ @io = IO.send(@method, @fd, 'wb', internal_encoding: "ibm866")
+ @io.internal_encoding.to_s.should == 'IBM866'
+ end
+
+ it "does not use binary encoding when mode encoding is specified along with binmode: true option" do
+ @io = IO.send(@method, @fd, 'w:iso-8859-1', binmode: true)
+ @io.external_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "does not use textmode argument when mode encoding is specified" do
+ @io = IO.send(@method, @fd, 'w:ascii-8bit', textmode: true)
+ @io.external_encoding.to_s.should == 'ASCII-8BIT'
+ end
+
+ it "does not use binmode argument when external encoding is specified via the :external_encoding option" do
+ @io = IO.send(@method, @fd, 'w', binmode: true, external_encoding: 'iso-8859-1')
+ @io.external_encoding.to_s.should == 'ISO-8859-1'
+ end
+
+ it "does not use textmode argument when external encoding is specified via the :external_encoding option" do
+ @io = IO.send(@method, @fd, 'w', textmode: true, external_encoding: 'ascii-8bit')
+ @io.external_encoding.to_s.should == 'ASCII-8BIT'
+ end
+
+ it "raises ArgumentError for nil options" do
+ -> {
+ IO.send(@method, @fd, 'w', nil)
+ }.should.raise(ArgumentError)
+ end
+
+ it "coerces mode with #to_str" do
+ mode = mock("mode")
+ mode.should_receive(:to_str).and_return('w')
+ @io = IO.send(@method, @fd, mode)
+ end
+
+ it "coerces mode with #to_int" do
+ mode = mock("mode")
+ mode.should_receive(:to_int).and_return(File::WRONLY)
+ @io = IO.send(@method, @fd, mode)
+ end
+
+ it "coerces mode with #to_str when passed in options" do
+ mode = mock("mode")
+ mode.should_receive(:to_str).and_return('w')
+ @io = IO.send(@method, @fd, mode: mode)
+ end
+
+ it "coerces mode with #to_int when passed in options" do
+ mode = mock("mode")
+ mode.should_receive(:to_int).and_return(File::WRONLY)
+ @io = IO.send(@method, @fd, mode: mode)
+ end
+
+ it "coerces :encoding option with #to_str" do
+ encoding = mock("encoding")
+ encoding.should_receive(:to_str).and_return('utf-8')
+ @io = IO.send(@method, @fd, 'w', encoding: encoding)
+ end
+
+ it "coerces :external_encoding option with #to_str" do
+ encoding = mock("encoding")
+ encoding.should_receive(:to_str).and_return('utf-8')
+ @io = IO.send(@method, @fd, 'w', external_encoding: encoding)
+ end
+
+ it "coerces :internal_encoding option with #to_str" do
+ encoding = mock("encoding")
+ encoding.should_receive(:to_str).at_least(:once).and_return('utf-8')
+ @io = IO.send(@method, @fd, 'w', internal_encoding: encoding)
+ end
+
+ it "coerces options as third argument with #to_hash" do
+ options = mock("options")
+ options.should_receive(:to_hash).and_return({})
+ @io = IO.send(@method, @fd, 'w', **options)
+ end
+
+ it "coerces options as second argument with #to_hash" do
+ options = mock("options")
+ options.should_receive(:to_hash).and_return({})
+ @io = IO.send(@method, @fd, **options)
+ end
+
+ it "accepts an :autoclose option" do
+ @io = IO.send(@method, @fd, 'w', autoclose: false)
+ @io.should_not.autoclose?
+ @io.autoclose = true
+ end
+
+ it "accepts any truthy option :autoclose" do
+ @io = IO.send(@method, @fd, 'w', autoclose: 42)
+ @io.should.autoclose?
+ end
+end
+
+# This group of specs may ONLY contain specs that do not actually create
+# an IO instance from the file descriptor returned by #new_fd helper.
+describe :io_new_errors, shared: true do
+ before :each do
+ @name = tmp("io_new.txt")
+ @fd = new_fd @name
+ end
+
+ after :each do
+ IO.new(@fd, "w").close if @fd
+ rm_r @name
+ end
+
+ it "raises an Errno::EBADF if the file descriptor is not valid" do
+ -> { IO.send(@method, -1, "w") }.should.raise(Errno::EBADF)
+ end
+
+ it "raises an IOError if passed a closed stream" do
+ -> { IO.send(@method, IOSpecs.closed_io.fileno, 'w') }.should.raise(IOError)
+ end
+
+ platform_is_not :windows do
+ it "raises an Errno::EINVAL if the new mode is not compatible with the descriptor's current mode" do
+ -> { IO.send(@method, @fd, "r") }.should.raise(Errno::EINVAL)
+ end
+ end
+
+ it "raises ArgumentError if passed an empty mode string" do
+ -> { IO.send(@method, @fd, "") }.should.raise(ArgumentError)
+ end
+
+ it "raises an error if passed modes two ways" do
+ -> {
+ IO.send(@method, @fd, "w", mode: "w")
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises an error if passed encodings two ways" do
+ -> {
+ @io = IO.send(@method, @fd, 'w:ISO-8859-1', encoding: 'ISO-8859-1')
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, 'w:ISO-8859-1', external_encoding: 'ISO-8859-1')
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, 'w:ISO-8859-1', internal_encoding: 'ISO-8859-1')
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, 'w:ISO-8859-1:UTF-8', internal_encoding: 'ISO-8859-1')
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises an error if passed matching binary/text mode two ways" do
+ -> {
+ @io = IO.send(@method, @fd, "wb", binmode: true)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, "wt", textmode: true)
+ }.should.raise(ArgumentError)
+
+ -> {
+ @io = IO.send(@method, @fd, "wb", textmode: false)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, "wt", binmode: false)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises an error if passed conflicting binary/text mode two ways" do
+ -> {
+ @io = IO.send(@method, @fd, "wb", binmode: false)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, "wt", textmode: false)
+ }.should.raise(ArgumentError)
+
+ -> {
+ @io = IO.send(@method, @fd, "wb", textmode: true)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, "wt", binmode: true)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises an error when trying to set both binmode and textmode" do
+ -> {
+ @io = IO.send(@method, @fd, "w", textmode: true, binmode: true)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, File::Constants::WRONLY, textmode: true, binmode: true)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if not passed a hash or nil for options" do
+ -> {
+ @io = IO.send(@method, @fd, 'w', false)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, false, false)
+ }.should.raise(ArgumentError)
+ -> {
+ @io = IO.send(@method, @fd, nil, false)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if passed a hash for mode and nil for options" do
+ -> {
+ @io = IO.send(@method, @fd, {mode: 'w'}, nil)
+ }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/io/shared/pos.rb b/spec/ruby/core/io/shared/pos.rb
new file mode 100644
index 0000000000..f4d0405863
--- /dev/null
+++ b/spec/ruby/core/io/shared/pos.rb
@@ -0,0 +1,78 @@
+describe :io_pos, shared: true do
+ before :each do
+ @fname = tmp('test.txt')
+ File.open(@fname, 'w') { |f| f.write "123" }
+ end
+
+ after :each do
+ rm_r @fname
+ end
+
+ it "gets the offset" do
+ File.open @fname do |f|
+ f.send(@method).should == 0
+ f.read 1
+ f.send(@method).should == 1
+ f.read 2
+ f.send(@method).should == 3
+ end
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.send(@method) }.should.raise(IOError)
+ end
+
+ it "resets #eof?" do
+ open @fname do |io|
+ io.read 1
+ io.read 1
+ io.send(@method)
+ io.should_not.eof?
+ end
+ end
+end
+
+describe :io_set_pos, shared: true do
+ before :each do
+ @fname = tmp('test.txt')
+ File.open(@fname, 'w') { |f| f.write "123" }
+ end
+
+ after :each do
+ rm_r @fname
+ end
+
+ it "sets the offset" do
+ File.open @fname do |f|
+ val1 = f.read 1
+ f.send @method, 0
+ f.read(1).should == val1
+ end
+ end
+
+ it "converts arguments to Integers" do
+ File.open @fname do |io|
+ o = mock("o")
+ o.should_receive(:to_int).and_return(1)
+
+ io.send @method, o
+ io.pos.should == 1
+ end
+ end
+
+ it "raises TypeError when cannot convert implicitly argument to Integer" do
+ File.open @fname do |io|
+ -> { io.send @method, Object.new }.should.raise(TypeError, "no implicit conversion of Object into Integer")
+ end
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ File.open @fname do |io|
+ -> { io.send @method, 2**128 }.should.raise(RangeError)
+ end
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.send @method, 0 }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/shared/readlines.rb b/spec/ruby/core/io/shared/readlines.rb
new file mode 100644
index 0000000000..f54fccc2e3
--- /dev/null
+++ b/spec/ruby/core/io/shared/readlines.rb
@@ -0,0 +1,257 @@
+describe :io_readlines, shared: true do
+ it "raises TypeError if the first parameter is nil" do
+ -> { IO.send(@method, nil, &@object) }.should.raise(TypeError)
+ end
+
+ it "raises an Errno::ENOENT if the file does not exist" do
+ name = tmp("nonexistent.txt")
+ -> { IO.send(@method, name, &@object) }.should.raise(Errno::ENOENT)
+ end
+
+ it "yields a single string with entire content when the separator is nil" do
+ result = IO.send(@method, @name, nil, &@object)
+ (result ? result : ScratchPad.recorded).should == [IO.read(@name)]
+ end
+
+ it "yields a sequence of paragraphs when the separator is an empty string" do
+ result = IO.send(@method, @name, "", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_empty_separator
+ end
+
+ it "yields a sequence of lines without trailing newline characters when chomp is passed" do
+ result = IO.send(@method, @name, chomp: true, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_without_newline_characters
+ end
+end
+
+describe :io_readlines_options_19, shared: true do
+ before :each do
+ @filename = tmp("io readlines options")
+ end
+
+ after :each do
+ rm_r @filename
+ end
+
+ describe "when passed name" do
+ it "calls #to_path to convert the name" do
+ name = mock("io name to_path")
+ name.should_receive(:to_path).and_return(@name)
+ IO.send(@method, name, &@object)
+ end
+
+ it "defaults to $/ as the separator" do
+ result = IO.send(@method, @name, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines
+ end
+ end
+
+ describe "when passed name, object" do
+ it "calls #to_str to convert the object to a separator" do
+ sep = mock("io readlines separator")
+ sep.should_receive(:to_str).at_least(1).and_return(" ")
+ result = IO.send(@method, @name, sep, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator
+ end
+
+ describe "when the object is an Integer" do
+ before :each do
+ @sep = $/
+ end
+
+ after :each do
+ suppress_warning {$/ = @sep}
+ end
+
+ it "defaults to $/ as the separator" do
+ suppress_warning {$/ = " "}
+ result = IO.send(@method, @name, 10, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "uses the object as a limit if it is an Integer" do
+ result = IO.send(@method, @name, 10, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_limit
+ end
+
+ it "ignores the object as a limit if it is negative" do
+ result = IO.send(@method, @name, -2, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines
+ end
+
+ it "does not accept Integers that don't fit in a C off_t" do
+ -> { IO.send(@method, @name, 2**128, &@object) }.should.raise(RangeError)
+ end
+
+ describe "when passed limit" do
+ it "raises ArgumentError when passed 0 as a limit" do
+ -> { IO.send(@method, @name, 0, &@object) }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ describe "when the object is a String" do
+ it "uses the value as the separator" do
+ result = IO.send(@method, @name, " ", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator
+ end
+
+ it "accepts non-ASCII data as separator" do
+ result = IO.send(@method, @name, "\303\250".dup.force_encoding("utf-8"), &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_arbitrary_separator
+ end
+ end
+
+ describe "when the object is an options Hash" do
+ it "raises TypeError exception" do
+ -> {
+ IO.send(@method, @name, { chomp: true }, &@object)
+ }.should.raise(TypeError)
+ end
+ end
+
+ describe "when the object is neither Integer nor String" do
+ it "raises TypeError exception" do
+ obj = mock("not io readlines limit")
+
+ -> {
+ IO.send(@method, @name, obj, &@object)
+ }.should.raise(TypeError)
+ end
+ end
+ end
+
+ describe "when passed name, keyword arguments" do
+ it "uses the keyword arguments as options" do
+ result = IO.send(@method, @name, mode: "r", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines
+ end
+ end
+
+ describe "when passed name, object, object" do
+ describe "when the first object is a String" do
+ it "uses the second object as a limit if it is an Integer" do
+ result = IO.send(@method, @name, " ", 10, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "calls #to_int to convert the second object" do
+ limit = mock("io readlines limit")
+ limit.should_receive(:to_int).at_least(1).and_return(10)
+ result = IO.send(@method, @name, " ", limit, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+ end
+
+ describe "when the first object is not a String or Integer" do
+ it "calls #to_str to convert the object to a String" do
+ sep = mock("io readlines separator")
+ sep.should_receive(:to_str).at_least(1).and_return(" ")
+ result = IO.send(@method, @name, sep, 10, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "uses the second object as a limit if it is an Integer" do
+ result = IO.send(@method, @name, " ", 10, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "calls #to_int to convert the second object" do
+ limit = mock("io readlines limit")
+ limit.should_receive(:to_int).at_least(1).and_return(10)
+ result = IO.send(@method, @name, " ", limit, &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+ end
+
+ describe "when the second object is neither Integer nor String" do
+ it "raises TypeError exception" do
+ obj = mock("not io readlines limit")
+
+ -> {
+ IO.send(@method, @name, " ", obj, &@object)
+ }.should.raise(TypeError)
+ end
+ end
+
+ describe "when the second object is an options Hash" do
+ it "raises TypeError exception" do
+ -> {
+ IO.send(@method, @name, "", { chomp: true }, &@object)
+ }.should.raise(TypeError)
+ end
+ end
+ end
+
+ describe "when passed name, object, keyword arguments" do
+ describe "when the first object is an Integer" do
+ it "uses the keyword arguments as options" do
+ -> do
+ IO.send(@method, @filename, 10, mode: "w", &@object)
+ end.should.raise(IOError)
+ end
+ end
+
+ describe "when the first object is a String" do
+ it "uses the keyword arguments as options" do
+ -> do
+ IO.send(@method, @filename, " ", mode: "w", &@object)
+ end.should.raise(IOError)
+ end
+ end
+
+ describe "when the first object is not a String or Integer" do
+ it "uses the keyword arguments as options" do
+ sep = mock("io readlines separator")
+ sep.should_receive(:to_str).at_least(1).and_return(" ")
+
+ -> do
+ IO.send(@method, @filename, sep, mode: "w", &@object)
+ end.should.raise(IOError)
+ end
+ end
+ end
+
+ describe "when passed name, separator, limit, keyword arguments" do
+ it "calls #to_path to convert the name object" do
+ name = mock("io name to_path")
+ name.should_receive(:to_path).and_return(@name)
+ result = IO.send(@method, name, " ", 10, mode: "r", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "calls #to_str to convert the separator object" do
+ sep = mock("io readlines separator")
+ sep.should_receive(:to_str).at_least(1).and_return(" ")
+ result = IO.send(@method, @name, sep, 10, mode: "r", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "calls #to_int to convert the limit argument" do
+ limit = mock("io readlines limit")
+ limit.should_receive(:to_int).at_least(1).and_return(10)
+ result = IO.send(@method, @name, " ", limit, mode: "r", &@object)
+ (result ? result : ScratchPad.recorded).should == IOSpecs.lines_space_separator_limit
+ end
+
+ it "uses the keyword arguments as options" do
+ -> do
+ IO.send(@method, @filename, " ", 10, mode: "w", &@object)
+ end.should.raise(IOError)
+ end
+
+ describe "when passed chomp, nil as a separator, and a limit" do
+ it "yields each line of limit size without truncating trailing new line character" do
+ # 43 - is a size of the 1st paragraph in the file
+ result = IO.send(@method, @name, nil, 43, chomp: true, &@object)
+
+ (result ? result : ScratchPad.recorded).should == [
+ "Voici la ligne une.\nQui è la linea due.\n\n\n",
+ "Aquí está la línea tres.\n" + "Hier ist Zeile ",
+ "vier.\n\nEstá aqui a linha cinco.\nHere is li",
+ "ne six.\n"
+ ]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/io/shared/tty.rb b/spec/ruby/core/io/shared/tty.rb
new file mode 100644
index 0000000000..1dc0e95739
--- /dev/null
+++ b/spec/ruby/core/io/shared/tty.rb
@@ -0,0 +1,24 @@
+require_relative '../fixtures/classes'
+
+describe :io_tty, shared: true do
+ platform_is_not :windows do
+ it "returns true if this stream is a terminal device (TTY)" do
+ begin
+ # check to enabled tty
+ File.open('/dev/tty') {}
+ rescue Errno::ENXIO
+ skip "workaround for not configured environment like OS X"
+ else
+ File.open('/dev/tty') { |f| f.send(@method) }.should == true
+ end
+ end
+ end
+
+ it "returns false if this stream is not a terminal device (TTY)" do
+ File.open(__FILE__) { |f| f.send(@method) }.should == false
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.send @method }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/shared/write.rb b/spec/ruby/core/io/shared/write.rb
new file mode 100644
index 0000000000..5de9fe4335
--- /dev/null
+++ b/spec/ruby/core/io/shared/write.rb
@@ -0,0 +1,154 @@
+# encoding: utf-8
+require_relative '../fixtures/classes'
+
+describe :io_write, shared: true do
+ before :each do
+ @filename = tmp("IO_syswrite_file") + $$.to_s
+ File.open(@filename, "w") do |file|
+ file.send(@method, "012345678901234567890123456789")
+ end
+ @file = File.open(@filename, "r+")
+ @readonly_file = File.open(@filename)
+ end
+
+ after :each do
+ @readonly_file.close if @readonly_file
+ @file.close if @file
+ rm_r @filename
+ end
+
+ it "coerces the argument to a string using to_s" do
+ (obj = mock('test')).should_receive(:to_s).and_return('a string')
+ @file.send(@method, obj)
+ end
+
+ it "checks if the file is writable if writing more than zero bytes" do
+ -> { @readonly_file.send(@method, "abcde") }.should.raise(IOError)
+ end
+
+ it "returns the number of bytes written" do
+ written = @file.send(@method, "abcde")
+ written.should == 5
+ end
+
+ it "invokes to_s on non-String argument" do
+ data = "abcdefgh9876"
+ (obj = mock(data)).should_receive(:to_s).and_return(data)
+ @file.send(@method, obj)
+ @file.seek(0)
+ @file.read(data.size).should == data
+ end
+
+ it "writes all of the string's bytes without buffering if mode is sync" do
+ @file.sync = true
+ written = @file.send(@method, "abcde")
+ written.should == 5
+ File.open(@filename) do |file|
+ file.read(10).should == "abcde56789"
+ end
+ end
+
+ it "does not warn if called after IO#read" do
+ @file.read(5)
+ -> { @file.send(@method, "fghij") }.should_not complain
+ end
+
+ it "writes to the current position after IO#read" do
+ @file.read(5)
+ @file.send(@method, "abcd")
+ @file.rewind
+ @file.read.should == "01234abcd901234567890123456789"
+ end
+
+ it "advances the file position by the count of given bytes" do
+ @file.send(@method, "abcde")
+ @file.read(10).should == "5678901234"
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.send(@method, "hello") }.should.raise(IOError)
+ end
+
+ describe "on a pipe" do
+ before :each do
+ @r, @w = IO.pipe
+ end
+
+ after :each do
+ @r.close
+ @w.close
+ end
+
+ it "writes the given String to the pipe" do
+ @w.send(@method, "foo")
+ @w.close
+ @r.read.should == "foo"
+ end
+
+ # [ruby-core:90895] RJIT worker may leave fd open in a forked child.
+ # For instance, RJIT creates a worker before @r.close with fork(), @r.close happens,
+ # and the RJIT worker keeps the pipe open until the worker execve().
+ # TODO: consider acquiring GVL from RJIT worker.
+ guard_not -> { defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? } do
+ it "raises Errno::EPIPE if the read end is closed and does not die from SIGPIPE" do
+ @r.close
+ -> { @w.send(@method, "foo") }.should.raise(Errno::EPIPE, /Broken pipe/)
+ end
+ end
+ end
+end
+
+describe :io_write_transcode, shared: true do
+ before :each do
+ @transcode_filename = tmp("io_write_transcode")
+ end
+
+ after :each do
+ rm_r @transcode_filename
+ end
+
+ it "transcodes the given string when the external encoding is set and neither is BINARY" do
+ utf8_str = "hello"
+
+ File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
+ file.external_encoding.should == Encoding::UTF_16BE
+ file.send(@method, utf8_str)
+ end
+
+ result = File.binread(@transcode_filename)
+ expected = [0, 104, 0, 101, 0, 108, 0, 108, 0, 111] # UTF-16BE bytes for "hello"
+
+ result.bytes.should == expected
+ end
+
+ it "transcodes the given string when the external encoding is set and the string encoding is BINARY" do
+ str = "été".b
+
+ File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
+ file.external_encoding.should == Encoding::UTF_16BE
+ -> { file.send(@method, str) }.should.raise(Encoding::UndefinedConversionError)
+ end
+ end
+end
+
+describe :io_write_no_transcode, shared: true do
+ before :each do
+ @transcode_filename = tmp("io_write_no_transcode")
+ end
+
+ after :each do
+ rm_r @transcode_filename
+ end
+
+ it "does not transcode the given string even when the external encoding is set" do
+ utf8_str = "hello"
+
+ File.open(@transcode_filename, "w", external_encoding: Encoding::UTF_16BE) do |file|
+ file.external_encoding.should == Encoding::UTF_16BE
+ file.send(@method, utf8_str)
+ end
+
+ result = File.binread(@transcode_filename)
+ result.bytes.should == utf8_str.bytes
+ end
+end
diff --git a/spec/ruby/core/io/stat_spec.rb b/spec/ruby/core/io/stat_spec.rb
new file mode 100644
index 0000000000..f9fc232ee0
--- /dev/null
+++ b/spec/ruby/core/io/stat_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#stat" do
+ before :each do
+ cmd = platform_is(:windows) ? 'rem' : 'cat'
+ @io = IO.popen cmd, "r+"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.stat }.should.raise(IOError)
+ end
+
+ it "returns a File::Stat object for the stream" do
+ STDOUT.stat.should.instance_of?(File::Stat)
+ end
+
+ it "can stat pipes" do
+ @io.stat.should.instance_of?(File::Stat)
+ end
+end
diff --git a/spec/ruby/core/io/sync_spec.rb b/spec/ruby/core/io/sync_spec.rb
new file mode 100644
index 0000000000..b537db335b
--- /dev/null
+++ b/spec/ruby/core/io/sync_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#sync=" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "sets the sync mode to true or false" do
+ @io.sync = true
+ @io.sync.should == true
+ @io.sync = false
+ @io.sync.should == false
+ end
+
+ it "accepts non-boolean arguments" do
+ @io.sync = 10
+ @io.sync.should == true
+ @io.sync = nil
+ @io.sync.should == false
+ @io.sync = Object.new
+ @io.sync.should == true
+ end
+
+ it "raises an IOError on closed stream" do
+ -> { IOSpecs.closed_io.sync = true }.should.raise(IOError)
+ end
+end
+
+describe "IO#sync" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "returns the current sync mode" do
+ @io.sync.should == false
+ end
+
+ it "raises an IOError on closed stream" do
+ -> { IOSpecs.closed_io.sync }.should.raise(IOError)
+ end
+end
+
+describe "IO#sync" do
+ it "is false by default for STDIN" do
+ STDIN.sync.should == false
+ end
+
+ it "is false by default for STDOUT" do
+ STDOUT.sync.should == false
+ end
+
+ it "is true by default for STDERR" do
+ STDERR.sync.should == true
+ end
+end
diff --git a/spec/ruby/core/io/sysopen_spec.rb b/spec/ruby/core/io/sysopen_spec.rb
new file mode 100644
index 0000000000..325d51ed23
--- /dev/null
+++ b/spec/ruby/core/io/sysopen_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+
+describe "IO.sysopen" do
+ before :each do
+ @filename = tmp("rubinius-spec-io-sysopen-#{$$}.txt")
+ @fd = nil
+ end
+
+ after :each do
+ IO.for_fd(@fd).close if @fd
+ rm_r @filename
+ end
+
+ it "returns the file descriptor for a given path" do
+ @fd = IO.sysopen(@filename, "w")
+ @fd.should.is_a?(Integer)
+ @fd.should_not.equal?(0)
+ end
+
+ # opening a directory is not supported on Windows
+ platform_is_not :windows do
+ it "works on directories" do
+ @fd = IO.sysopen(tmp("")) # /tmp
+ @fd.should.is_a?(Integer)
+ @fd.should_not.equal?(0)
+ end
+ end
+
+ it "calls #to_path to convert an object to a path" do
+ path = mock('sysopen to_path')
+ path.should_receive(:to_path).and_return(@filename)
+ @fd = IO.sysopen(path, 'w')
+ end
+
+ it "accepts a mode as second argument" do
+ -> { @fd = IO.sysopen(@filename, "w") }.should_not.raise
+ @fd.should_not.equal?(0)
+ end
+
+ it "accepts permissions as third argument" do
+ @fd = IO.sysopen(@filename, "w", 777)
+ @fd.should_not.equal?(0)
+ end
+
+ it "accepts mode & permission that are nil" do
+ touch @filename # create the file
+ @fd = IO.sysopen(@filename, nil, nil)
+ @fd.should_not.equal?(0)
+ end
+end
diff --git a/spec/ruby/core/io/sysread_spec.rb b/spec/ruby/core/io/sysread_spec.rb
new file mode 100644
index 0000000000..2d58db2250
--- /dev/null
+++ b/spec/ruby/core/io/sysread_spec.rb
@@ -0,0 +1,137 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#sysread on a file" do
+ before :each do
+ @file_name = tmp("IO_sysread_file") + $$.to_s
+ File.open(@file_name, "w") do |f|
+ # write some stuff
+ f.write("012345678901234567890123456789\nabcdef")
+ end
+ @file = File.open(@file_name, "r+")
+ end
+
+ after :each do
+ @file.close
+ rm_r @file_name
+ end
+
+ it "reads the specified number of bytes from the file" do
+ @file.sysread(15).should == "012345678901234"
+ end
+
+ it "reads the specified number of bytes from the file to the buffer" do
+ buf = +"" # empty buffer
+ @file.sysread(15, buf).should == buf
+ buf.should == "012345678901234"
+
+ @file.rewind
+
+ buf = +"ABCDE" # small buffer
+ @file.sysread(15, buf).should == buf
+ buf.should == "012345678901234"
+
+ @file.rewind
+
+ buf = +"ABCDE" * 5 # large buffer
+ @file.sysread(15, buf).should == buf
+ buf.should == "012345678901234"
+ end
+
+ it "coerces the second argument to string and uses it as a buffer" do
+ buf = +"ABCDE"
+ (obj = mock("buff")).should_receive(:to_str).any_number_of_times.and_return(buf)
+ @file.sysread(15, obj).should == buf
+ buf.should == "012345678901234"
+ end
+
+ it "advances the position of the file by the specified number of bytes" do
+ @file.sysread(15)
+ @file.sysread(5).should == "56789"
+ end
+
+ it "raises an error when called after buffered reads" do
+ @file.readline
+ -> { @file.sysread(5) }.should.raise(IOError)
+ end
+
+ it "reads normally even when called immediately after a buffered IO#read" do
+ @file.read(15)
+ @file.sysread(5).should == "56789"
+ end
+
+ it "does not raise error if called after IO#read followed by IO#write" do
+ @file.read(5)
+ @file.write("abcde")
+ -> { @file.sysread(5) }.should_not.raise(IOError)
+ end
+
+ it "does not raise error if called after IO#read followed by IO#syswrite" do
+ @file.read(5)
+ @file.syswrite("abcde")
+ -> { @file.sysread(5) }.should_not.raise(IOError)
+ end
+
+ it "reads updated content after the flushed buffered IO#write" do
+ @file.write("abcde")
+ @file.flush
+ @file.sysread(5).should == "56789"
+ File.open(@file_name) do |f|
+ f.sysread(10).should == "abcde56789"
+ end
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.sysread(5) }.should.raise(IOError)
+ end
+
+ it "immediately returns an empty string if the length argument is 0" do
+ @file.sysread(0).should == ""
+ end
+
+ it "immediately returns the given buffer if the length argument is 0" do
+ buffer = +"existing content"
+ @file.sysread(0, buffer).should == buffer
+ buffer.should == "existing content"
+ end
+
+ it "discards the existing buffer content upon successful read" do
+ buffer = +"existing content"
+ @file.sysread(11, buffer).should.equal?(buffer)
+ buffer.should == "01234567890"
+ end
+
+ it "discards the existing buffer content upon error" do
+ buffer = +"existing content"
+ @file.seek(0, :END)
+ -> { @file.sysread(1, buffer) }.should.raise(EOFError)
+ buffer.should.empty?
+ end
+
+ it "preserves the encoding of the given buffer" do
+ buffer = ''.encode(Encoding::ISO_8859_1)
+ string = @file.sysread(10, buffer)
+
+ buffer.encoding.should == Encoding::ISO_8859_1
+ end
+end
+
+describe "IO#sysread" do
+ before do
+ @read, @write = IO.pipe
+ end
+
+ after do
+ @read.close
+ @write.close
+ end
+
+ it "returns a smaller string if less than size bytes are available" do
+ @write.syswrite "ab"
+ @read.sysread(3).should == "ab"
+ end
+
+ it "raises ArgumentError when length is less than 0" do
+ -> { @read.sysread(-1) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/io/sysseek_spec.rb b/spec/ruby/core/io/sysseek_spec.rb
new file mode 100644
index 0000000000..2384895dc5
--- /dev/null
+++ b/spec/ruby/core/io/sysseek_spec.rb
@@ -0,0 +1,49 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/pos'
+
+describe "IO#sysseek" do
+ it_behaves_like :io_set_pos, :sysseek
+end
+
+describe "IO#sysseek" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "moves the read position relative to the current position with SEEK_CUR" do
+ @io.sysseek(10, IO::SEEK_CUR)
+ @io.readline.should == "igne une.\n"
+ end
+
+ it "raises an error when called after buffered reads" do
+ @io.readline
+ -> { @io.sysseek(-5, IO::SEEK_CUR) }.should.raise(IOError)
+ end
+
+ it "seeks normally even when called immediately after a buffered IO#read" do
+ @io.read(15)
+ @io.sysseek(-5, IO::SEEK_CUR).should == 10
+ end
+
+ it "moves the read position relative to the start with SEEK_SET" do
+ @io.sysseek(43, IO::SEEK_SET)
+ @io.readline.should == "Aquí está la línea tres.\n"
+ end
+
+ it "moves the read position relative to the end with SEEK_END" do
+ @io.sysseek(1, IO::SEEK_END)
+
+ # this is the safest way of checking the EOF when
+ # sys-* methods are invoked
+ -> { @io.sysread(1) }.should.raise(EOFError)
+
+ @io.sysseek(-25, IO::SEEK_END)
+ @io.sysread(7).should == "cinco.\n"
+ end
+end
diff --git a/spec/ruby/core/io/syswrite_spec.rb b/spec/ruby/core/io/syswrite_spec.rb
new file mode 100644
index 0000000000..8bf61a27c3
--- /dev/null
+++ b/spec/ruby/core/io/syswrite_spec.rb
@@ -0,0 +1,82 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/write'
+
+describe "IO#syswrite on a file" do
+ before :each do
+ @filename = tmp("IO_syswrite_file") + $$.to_s
+ File.open(@filename, "w") do |file|
+ file.syswrite("012345678901234567890123456789")
+ end
+ @file = File.open(@filename, "r+")
+ @readonly_file = File.open(@filename)
+ end
+
+ after :each do
+ @file.close
+ @readonly_file.close
+ rm_r @filename
+ end
+
+ it "writes all of the string's bytes but does not buffer them" do
+ written = @file.syswrite("abcde")
+ written.should == 5
+ File.open(@filename) do |file|
+ file.sysread(10).should == "abcde56789"
+ file.seek(0)
+ @file.fsync
+ file.sysread(10).should == "abcde56789"
+ end
+ end
+
+ it "does not modify the passed argument" do
+ File.open(@filename, "w") do |f|
+ f.set_encoding(Encoding::IBM437)
+ # A character whose codepoint differs between UTF-8 and IBM437
+ f.syswrite("Æ’".freeze)
+ end
+
+ File.binread(@filename).bytes.should == [198, 146]
+ end
+
+ it "warns if called immediately after a buffered IO#write" do
+ @file.write("abcde")
+ -> { @file.syswrite("fghij") }.should complain(/syswrite/)
+ end
+
+ it "does not warn if called after IO#write with intervening IO#sysread" do
+ @file.syswrite("abcde")
+ @file.sysread(5)
+ -> { @file.syswrite("fghij") }.should_not complain
+ end
+
+ it "writes to the actual file position when called after buffered IO#read" do
+ @file.read(5)
+ @file.syswrite("abcde")
+ File.open(@filename) do |file|
+ file.sysread(10).should == "01234abcde"
+ end
+ end
+end
+
+describe "IO#syswrite on a pipe" do
+ it "returns the written bytes if the fd is in nonblock mode and write would block" do
+ require 'io/nonblock'
+ r, w = IO.pipe
+ begin
+ w.nonblock = true
+ larger_than_pipe_capacity = 2 * 1024 * 1024
+ written = w.syswrite("a"*larger_than_pipe_capacity)
+ written.should > 0
+ written.should < larger_than_pipe_capacity
+ ensure
+ w.close
+ r.close
+ end
+ end
+end
+
+describe "IO#syswrite" do
+ it_behaves_like :io_write, :syswrite
+ it_behaves_like :io_write_no_transcode, :syswrite
+end
diff --git a/spec/ruby/core/io/tell_spec.rb b/spec/ruby/core/io/tell_spec.rb
new file mode 100644
index 0000000000..0d6c6b02d3
--- /dev/null
+++ b/spec/ruby/core/io/tell_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/pos'
+
+describe "IO#tell" do
+ it_behaves_like :io_pos, :tell
+end
diff --git a/spec/ruby/core/io/to_i_spec.rb b/spec/ruby/core/io/to_i_spec.rb
new file mode 100644
index 0000000000..1d0cf2a1f6
--- /dev/null
+++ b/spec/ruby/core/io/to_i_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#to_i" do
+ it "returns the numeric file descriptor of the given IO object" do
+ $stdout.to_i.should == 1
+ end
+
+ it "raises IOError on closed stream" do
+ -> { IOSpecs.closed_io.to_i }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/to_io_spec.rb b/spec/ruby/core/io/to_io_spec.rb
new file mode 100644
index 0000000000..0b1809dffa
--- /dev/null
+++ b/spec/ruby/core/io/to_io_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#to_io" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ end
+
+ it "returns self for open stream" do
+ @io.to_io.should.equal?(@io)
+ end
+
+ it "returns self for closed stream" do
+ io = IOSpecs.closed_io
+ io.to_io.should.equal?(io)
+ end
+end
diff --git a/spec/ruby/core/io/try_convert_spec.rb b/spec/ruby/core/io/try_convert_spec.rb
new file mode 100644
index 0000000000..7600b01b75
--- /dev/null
+++ b/spec/ruby/core/io/try_convert_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO.try_convert" do
+ before :each do
+ @name = tmp("io_try_convert.txt")
+ @io = new_io @name
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "returns the passed IO object" do
+ IO.try_convert(@io).should.equal?(@io)
+ end
+
+ it "does not call #to_io on an IO instance" do
+ @io.should_not_receive(:to_io)
+ IO.try_convert(@io)
+ end
+
+ it "calls #to_io to coerce an object" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_return(@io)
+ IO.try_convert(obj).should.equal?(@io)
+ end
+
+ it "returns nil when the passed object does not respond to #to_io" do
+ IO.try_convert(mock("io")).should == nil
+ end
+
+ it "return nil when BasicObject is passed" do
+ IO.try_convert(BasicObject.new).should == nil
+ end
+
+ it "raises a TypeError if the object does not return an IO from #to_io" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_return("io")
+ -> { IO.try_convert(obj) }.should raise_consistent_error(TypeError, "can't convert MockObject into IO (MockObject#to_io gives String)")
+ end
+
+ it "propagates an exception raised by #to_io" do
+ obj = mock("io")
+ obj.should_receive(:to_io).and_raise(TypeError.new)
+ ->{ IO.try_convert(obj) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/io/tty_spec.rb b/spec/ruby/core/io/tty_spec.rb
new file mode 100644
index 0000000000..3b76c6d2b8
--- /dev/null
+++ b/spec/ruby/core/io/tty_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/tty'
+
+describe "IO#tty?" do
+ it_behaves_like :io_tty, :tty?
+end
diff --git a/spec/ruby/core/io/ungetbyte_spec.rb b/spec/ruby/core/io/ungetbyte_spec.rb
new file mode 100644
index 0000000000..e0615cd76c
--- /dev/null
+++ b/spec/ruby/core/io/ungetbyte_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+
+describe "IO#ungetbyte" do
+ before :each do
+ @name = tmp("io_ungetbyte")
+ touch(@name) { |f| f.write "a" }
+ @io = new_io @name, "r"
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "does nothing when passed nil" do
+ @io.ungetbyte(nil).should == nil
+ @io.getbyte.should == 97
+ end
+
+ it "puts back each byte in a String argument" do
+ @io.ungetbyte("cat").should == nil
+ @io.getbyte.should == 99
+ @io.getbyte.should == 97
+ @io.getbyte.should == 116
+ @io.getbyte.should == 97
+ end
+
+ it "calls #to_str to convert the argument" do
+ str = mock("io ungetbyte")
+ str.should_receive(:to_str).and_return("dog")
+
+ @io.ungetbyte(str).should == nil
+ @io.getbyte.should == 100
+ @io.getbyte.should == 111
+ @io.getbyte.should == 103
+ @io.getbyte.should == 97
+ end
+
+ it "never raises RangeError" do
+ for i in [4095, 0x4f7574206f6620636861722072616e67ff] do
+ @io.ungetbyte(i).should == nil
+ @io.getbyte.should == 255
+ end
+ end
+
+ it "raises IOError on stream not opened for reading" do
+ -> { STDOUT.ungetbyte(42) }.should.raise(IOError, "not opened for reading")
+ end
+
+ it "raises an IOError if the IO is closed" do
+ @io.close
+ -> { @io.ungetbyte(42) }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/ungetc_spec.rb b/spec/ruby/core/io/ungetc_spec.rb
new file mode 100644
index 0000000000..4a9e67f126
--- /dev/null
+++ b/spec/ruby/core/io/ungetc_spec.rb
@@ -0,0 +1,138 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "IO#ungetc" do
+ before :each do
+ @io = IOSpecs.io_fixture "lines.txt"
+
+ @empty = tmp('empty.txt')
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @empty
+ end
+
+ it "pushes back one character onto stream" do
+ @io.getc.should == ?V
+ @io.ungetc(86)
+ @io.getc.should == ?V
+
+ @io.ungetc(10)
+ @io.getc.should == ?\n
+
+ @io.getc.should == ?o
+ @io.getc.should == ?i
+ # read the rest of line
+ @io.readline.should == "ci la ligne une.\n"
+ @io.getc.should == ?Q
+ @io.ungetc(99)
+ @io.getc.should == ?c
+ end
+
+ it "interprets the codepoint in the external encoding" do
+ @io.set_encoding(Encoding::UTF_8)
+ @io.ungetc(233)
+ c = @io.getc
+ c.encoding.should == Encoding::UTF_8
+ c.should == "é"
+ c.bytes.should == [195, 169]
+
+ @io.set_encoding(Encoding::IBM437)
+ @io.ungetc(130)
+ c = @io.getc
+ c.encoding.should == Encoding::IBM437
+ c.bytes.should == [130]
+ c.encode(Encoding::UTF_8).should == "é"
+ end
+
+ it "pushes back one character when invoked at the end of the stream" do
+ # read entire content
+ @io.read
+ @io.ungetc(100)
+ @io.getc.should == ?d
+ end
+
+ it "pushes back one character when invoked at the start of the stream" do
+ @io.read(0)
+ @io.ungetc(100)
+ @io.getc.should == ?d
+ end
+
+ it "pushes back one character when invoked on empty stream" do
+ touch(@empty)
+
+ File.open(@empty) { |empty|
+ empty.getc().should == nil
+ empty.ungetc(10)
+ empty.getc.should == ?\n
+ }
+ end
+
+ it "affects EOF state" do
+ touch(@empty)
+
+ File.open(@empty) { |empty|
+ empty.should.eof?
+ empty.getc.should == nil
+ empty.ungetc(100)
+ empty.should_not.eof?
+ }
+ end
+
+ it "adjusts the stream position" do
+ @io.pos.should == 0
+
+ # read one char
+ c = @io.getc
+ @io.pos.should == 1
+ @io.ungetc(c)
+ @io.pos.should == 0
+
+ # read all
+ @io.read
+ pos = @io.pos
+ @io.ungetc(98)
+ @io.pos.should == pos - 1
+ end
+
+ it "makes subsequent unbuffered operations to raise IOError" do
+ @io.getc
+ @io.ungetc(100)
+ -> { @io.sysread(1) }.should.raise(IOError)
+ end
+
+ it "raises TypeError if passed nil" do
+ @io.getc.should == ?V
+ proc{@io.ungetc(nil)}.should.raise(TypeError)
+ end
+
+ it "puts one or more characters back in the stream" do
+ @io.gets
+ @io.ungetc("Aquí ").should == nil
+ @io.gets.chomp.should == "Aquí Qui è la linea due."
+ end
+
+ it "calls #to_str to convert the argument if it is not an Integer" do
+ chars = mock("io ungetc")
+ chars.should_receive(:to_str).and_return("Aquí ")
+
+ @io.ungetc(chars).should == nil
+ @io.gets.chomp.should == "Aquí Voici la ligne une."
+ end
+
+ it "returns nil when invoked on stream that was not yet read" do
+ @io.ungetc(100).should == nil
+ end
+
+ it "raises IOError on stream not opened for reading" do
+ -> { STDOUT.ungetc(100) }.should.raise(IOError, "not opened for reading")
+ end
+
+ it "raises IOError on closed stream" do
+ @io.getc
+ @io.close
+ -> { @io.ungetc(100) }.should.raise(IOError)
+ end
+end
diff --git a/spec/ruby/core/io/write_nonblock_spec.rb b/spec/ruby/core/io/write_nonblock_spec.rb
new file mode 100644
index 0000000000..a6bd43c058
--- /dev/null
+++ b/spec/ruby/core/io/write_nonblock_spec.rb
@@ -0,0 +1,96 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/write'
+
+# See https://bugs.ruby-lang.org/issues/5954#note-5
+platform_is_not :windows do
+ describe "IO#write_nonblock on a file" do
+ before :each do
+ @filename = tmp("IO_syswrite_file") + $$.to_s
+ File.open(@filename, "w") do |file|
+ file.write_nonblock("012345678901234567890123456789")
+ end
+ @file = File.open(@filename, "r+")
+ @readonly_file = File.open(@filename)
+ end
+
+ after :each do
+ @file.close if @file
+ @readonly_file.close if @readonly_file
+ rm_r @filename
+ end
+
+ it "writes all of the string's bytes but does not buffer them" do
+ written = @file.write_nonblock("abcde")
+ written.should == 5
+ File.open(@filename) do |file|
+ file.sysread(10).should == "abcde56789"
+ file.seek(0)
+ @file.fsync
+ file.sysread(10).should == "abcde56789"
+ end
+ end
+
+ it "does not modify the passed argument" do
+ File.open(@filename, "w") do |f|
+ f.set_encoding(Encoding::IBM437)
+ # A character whose codepoint differs between UTF-8 and IBM437
+ f.write_nonblock("Æ’".freeze)
+ end
+
+ File.binread(@filename).bytes.should == [198, 146]
+ end
+
+ it "checks if the file is writable if writing zero bytes" do
+ -> {
+ @readonly_file.write_nonblock("")
+ }.should.raise(IOError)
+ end
+ end
+
+ describe "IO#write_nonblock" do
+ it_behaves_like :io_write, :write_nonblock
+ it_behaves_like :io_write_no_transcode, :write_nonblock
+ end
+end
+
+describe 'IO#write_nonblock' do
+ before do
+ @read, @write = IO.pipe
+ end
+
+ after do
+ @read.close
+ @write.close
+ end
+
+ it "raises an exception extending IO::WaitWritable when the write would block" do
+ -> {
+ loop { @write.write_nonblock('a' * 10_000) }
+ }.should.raise(IO::WaitWritable) { |e|
+ platform_is_not :windows do
+ e.should.is_a?(Errno::EAGAIN)
+ end
+ platform_is :windows do
+ e.should.is_a?(Errno::EWOULDBLOCK)
+ end
+ }
+ end
+
+ context "when exception option is set to false" do
+ it "returns :wait_writable when the operation would block" do
+ loop {
+ break if @write.write_nonblock("a" * 10_000, exception: false) == :wait_writable
+ }
+ @write.write_nonblock("a" * 10_000, exception: false).should == :wait_writable
+ end
+ end
+
+ platform_is_not :windows do
+ it 'sets the IO in nonblock mode' do
+ require 'io/nonblock'
+ @write.write_nonblock('a')
+ @write.should.nonblock?
+ end
+ end
+end
diff --git a/spec/ruby/core/io/write_spec.rb b/spec/ruby/core/io/write_spec.rb
new file mode 100644
index 0000000000..1a745ba012
--- /dev/null
+++ b/spec/ruby/core/io/write_spec.rb
@@ -0,0 +1,304 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/write'
+require_relative 'shared/binwrite'
+
+describe "IO#write on a file" do
+ before :each do
+ @filename = tmp("IO_syswrite_file") + $$.to_s
+ File.open(@filename, "w") do |file|
+ file.write("012345678901234567890123456789")
+ end
+ @file = File.open(@filename, "r+")
+ @readonly_file = File.open(@filename)
+ end
+
+ after :each do
+ @file.close
+ @readonly_file.close
+ rm_r @filename
+ end
+
+ it "does not check if the file is writable if writing zero bytes" do
+ -> { @readonly_file.write("") }.should_not.raise
+ end
+
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+
+ Encoding.default_external = Encoding::UTF_8
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it "returns a length of 0 when writing a blank string" do
+ @file.write('').should == 0
+ end
+
+ it "returns a length of 0 when writing blank strings" do
+ @file.write('', '', '').should == 0
+ end
+
+ it "returns a length of 0 when passed no arguments" do
+ @file.write().should == 0
+ end
+
+ it "returns the number of bytes written" do
+ @file.write("hellø").should == 6
+ end
+
+ it "does not modify the passed argument" do
+ File.open(@filename, "w") do |f|
+ f.set_encoding(Encoding::IBM437)
+ # A character whose codepoint differs between UTF-8 and IBM437
+ f.write("Æ’".freeze)
+ end
+
+ File.binread(@filename).bytes.should == [159]
+ end
+
+ it "does not modify arguments when passed multiple arguments and external encoding not set" do
+ a, b = "a".freeze, "b".freeze
+
+ File.open(@filename, "w") do |f|
+ f.write(a, b)
+ end
+
+ File.binread(@filename).bytes.should == [97, 98]
+ a.encoding.should == Encoding::UTF_8
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "uses the encoding from the given option for non-ascii encoding" do
+ File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file|
+ file.write("hi").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
+ end
+
+ it "uses the encoding from the given option for non-ascii encoding even if in binary mode" do
+ File.open(@filename, "w", encoding: Encoding::UTF_32LE, binmode: true) do |file|
+ file.should.binmode?
+ file.write("hi").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
+
+ File.open(@filename, "wb", encoding: Encoding::UTF_32LE) do |file|
+ file.should.binmode?
+ file.write("hi").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
+ end
+
+ it "uses the encoding from the given option for non-ascii encoding when multiple arguments passes" do
+ File.open(@filename, "w", encoding: Encoding::UTF_32LE) do |file|
+ file.write("h", "i").should == 8
+ end
+ File.binread(@filename).should == "h\u0000\u0000\u0000i\u0000\u0000\u0000"
+ end
+
+ it "ignores the 'bom|' prefix" do
+ File.open(@filename, "w", encoding: 'bom|utf-8') do |file|
+ file.write("hi")
+ end
+ File.binread(@filename).should == "hi"
+ end
+
+ it "raises a invalid byte sequence error if invalid bytes are being written" do
+ # pack "\xFEhi" to avoid utf-8 conflict
+ xFEhi = ([254].pack('C*') + 'hi').force_encoding('utf-8')
+ File.open(@filename, "w", encoding: Encoding::US_ASCII) do |file|
+ -> { file.write(xFEhi) }.should.raise(Encoding::InvalidByteSequenceError)
+ end
+ end
+
+ it "writes binary data if no encoding is given" do
+ File.open(@filename, "w") do |file|
+ file.write('Hëllö'.encode('ISO-8859-1'))
+ end
+ ë = ([235].pack('U')).encode('ISO-8859-1')
+ ö = ([246].pack('U')).encode('ISO-8859-1')
+ res = "H#{ë}ll#{ö}"
+ File.binread(@filename).should == res.force_encoding(Encoding::BINARY)
+ end
+
+ platform_is_not :windows do
+ it "writes binary data if no encoding is given and multiple arguments passed" do
+ File.open(@filename, "w") do |file|
+ file.write("\x87".b, "Ä…") # 0x87 isn't a valid UTF-8 binary representation of a character
+ end
+ File.binread(@filename).bytes.should == [0x87, 0xC4, 0x85]
+
+ File.open(@filename, "w") do |file|
+ file.write("\x61".encode("utf-32le"), "Ä…")
+ end
+ File.binread(@filename).bytes.should == [0x61, 0x00, 0x00, 0x00, 0xC4, 0x85]
+ end
+ end
+end
+
+describe "IO.write" do
+ it_behaves_like :io_binwrite, :write
+
+ it "uses an :open_args option" do
+ IO.write(@filename, 'hi', open_args: ["w", nil, {encoding: Encoding::UTF_32LE}]).should == 8
+ end
+
+ it "disregards other options if :open_args is given" do
+ IO.write(@filename, 'hi', 2, mode: "r", encoding: Encoding::UTF_32LE, open_args: ["w"]).should == 2
+ File.read(@filename).should == "\0\0hi"
+ end
+
+ it "requires mode to be specified in :open_args" do
+ -> {
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true}])
+ }.should.raise(IOError, "not opened for writing")
+
+ IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true}]).should == 8
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, mode: "w"}]).should == 8
+ end
+
+ it "requires mode to be specified in :open_args even if flags option passed" do
+ -> {
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}])
+ }.should.raise(IOError, "not opened for writing")
+
+ IO.write(@filename, 'hi', open_args: ["w", {encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT}]).should == 8
+ IO.write(@filename, 'hi', open_args: [{encoding: Encoding::UTF_32LE, binmode: true, flags: File::CREAT, mode: "w"}]).should == 8
+ end
+
+ it "uses the given encoding and returns the number of bytes written" do
+ IO.write(@filename, 'hi', mode: "w", encoding: Encoding::UTF_32LE).should == 8
+ end
+
+ it "raises ArgumentError if encoding is specified in mode parameter and is given as :encoding option" do
+ -> {
+ IO.write(@filename, 'hi', mode: "w:UTF-16LE:UTF-16BE", encoding: Encoding::UTF_32LE)
+ }.should.raise(ArgumentError, "encoding specified twice")
+
+ -> {
+ IO.write(@filename, 'hi', mode: "w:UTF-16BE", encoding: Encoding::UTF_32LE)
+ }.should.raise(ArgumentError, "encoding specified twice")
+ end
+
+ it "writes the file with the permissions in the :perm parameter" do
+ rm_r @filename
+ IO.write(@filename, 'write :perm spec', mode: "w", perm: 0o755).should == 16
+ (File.stat(@filename).mode & 0o777) == 0o755
+ end
+
+ it "writes binary data if no encoding is given" do
+ IO.write(@filename, 'Hëllö'.encode('ISO-8859-1'))
+ xEB = [235].pack('C*')
+ xF6 = [246].pack('C*')
+ File.binread(@filename).should == ("H" + xEB + "ll" + xF6).force_encoding(Encoding::BINARY)
+ end
+
+ platform_is_not :windows do
+ describe "on a FIFO" do
+ before :each do
+ @fifo = tmp("File_open_fifo")
+ File.mkfifo(@fifo)
+ end
+
+ after :each do
+ rm_r @fifo
+ end
+
+ # rb_cloexec_open() is currently missing a retry on EINTR.
+ # @ioquatix is looking into fixing it. Quarantined until it's done.
+ quarantine! do
+ it "writes correctly" do
+ thr = Thread.new do
+ IO.read(@fifo)
+ end
+ begin
+ string = "hi"
+ IO.write(@fifo, string).should == string.length
+ ensure
+ thr.join
+ end
+ end
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ -> {
+ -> {
+ IO.write("|cat", "xxx")
+ }.should output_to_fd("xxx")
+ }.should complain(/IO process creation with a leading '\|'/)
+ end
+ end
+ end
+end
+
+describe "IO#write" do
+ it_behaves_like :io_write, :write
+ it_behaves_like :io_write_transcode, :write
+
+ it "accepts multiple arguments" do
+ IO.pipe do |r, w|
+ w.write("foo", "bar")
+ w.close
+
+ r.read.should == "foobar"
+ end
+ end
+end
+
+platform_is :windows do
+ describe "IO#write on Windows" do
+ before :each do
+ @fname = tmp("io_write.txt")
+ end
+
+ after :each do
+ rm_r @fname
+ @io.close if @io and !@io.closed?
+ end
+
+ it "normalizes line endings in text mode" do
+ @io = new_io(@fname, "wt")
+ @io.write "a\nb\nc"
+ @io.close
+ File.binread(@fname).should == "a\r\nb\r\nc"
+ end
+
+ it "does not normalize line endings in binary mode" do
+ @io = new_io(@fname, "wb")
+ @io.write "a\r\nb\r\nc"
+ @io.close
+ File.binread(@fname).should == "a\r\nb\r\nc"
+ end
+ end
+end
+
+describe "IO#write on STDOUT" do
+ # https://bugs.ruby-lang.org/issues/14413
+ platform_is_not :windows do
+ it "raises SignalException SIGPIPE if the stream is closed instead of Errno::EPIPE like other IOs" do
+ stderr_file = tmp("stderr")
+ begin
+ IO.popen([*ruby_exe, "-e", "loop { puts :ok }"], "r", err: stderr_file) do |io|
+ io.gets.should == "ok\n"
+ io.close
+ end
+ status = $?
+ status.should_not.success?
+ status.should.signaled?
+ Signal.signame(status.termsig).should == 'PIPE'
+ File.read(stderr_file).should.empty?
+ ensure
+ rm_r stderr_file
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/Array_spec.rb b/spec/ruby/core/kernel/Array_spec.rb
new file mode 100644
index 0000000000..063faf7097
--- /dev/null
+++ b/spec/ruby/core/kernel/Array_spec.rb
@@ -0,0 +1,97 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel" do
+ it "has private instance method Array()" do
+ Kernel.private_instance_methods(false).should.include?(:Array)
+ end
+end
+
+describe :kernel_Array, shared: true do
+ before :each do
+ @array = [1, 2, 3]
+ end
+
+ it "does not call #to_ary on an Array" do
+ @array.should_not_receive(:to_ary)
+ @object.send(@method, @array).should == @array
+ end
+
+ it "calls #to_ary to convert the argument to an Array" do
+ obj = mock("Array([1,2,3])")
+ obj.should_receive(:to_ary).and_return(@array)
+ obj.should_not_receive(:to_a)
+
+ @object.send(@method, obj).should == @array
+ end
+
+ it "does not call #to_a on an Array" do
+ @array.should_not_receive(:to_a)
+ @object.send(@method, @array).should == @array
+ end
+
+ it "calls #to_a if the argument does not respond to #to_ary" do
+ obj = mock("Array([1,2,3])")
+ obj.should_receive(:to_a).and_return(@array)
+
+ @object.send(@method, obj).should == @array
+ end
+
+ it "calls #to_a if #to_ary returns nil" do
+ obj = mock("Array([1,2,3])")
+ obj.should_receive(:to_ary).and_return(nil)
+ obj.should_receive(:to_a).and_return(@array)
+
+ @object.send(@method, obj).should == @array
+ end
+
+ it "returns an Array containing the argument if #to_a returns nil" do
+ obj = mock("Array([1,2,3])")
+ obj.should_receive(:to_a).and_return(nil)
+
+ @object.send(@method, obj).should == [obj]
+ end
+
+ it "calls #to_ary first, even if it's private" do
+ obj = KernelSpecs::PrivateToAry.new
+
+ @object.send(@method, obj).should == [1, 2]
+ end
+
+ it "calls #to_a if #to_ary is not defined, even if it's private" do
+ obj = KernelSpecs::PrivateToA.new
+
+ @object.send(@method, obj).should == [3, 4]
+ end
+
+ it "returns an Array containing the argument if it responds to neither #to_ary nor #to_a" do
+ obj = mock("Array(x)")
+ @object.send(@method, obj).should == [obj]
+ end
+
+ it "returns an empty Array when passed nil" do
+ @object.send(@method, nil).should == []
+ end
+
+ it "raises a TypeError if #to_ary does not return an Array" do
+ obj = mock("Array() string")
+ obj.should_receive(:to_ary).and_return("string")
+
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_a does not return an Array" do
+ obj = mock("Array() string")
+ obj.should_receive(:to_a).and_return("string")
+
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+end
+
+describe "Kernel.Array" do
+ it_behaves_like :kernel_Array, :Array_method, KernelSpecs
+end
+
+describe "Kernel#Array" do
+ it_behaves_like :kernel_Array, :Array_function, KernelSpecs
+end
diff --git a/spec/ruby/core/kernel/Complex_spec.rb b/spec/ruby/core/kernel/Complex_spec.rb
new file mode 100644
index 0000000000..92ce183cc8
--- /dev/null
+++ b/spec/ruby/core/kernel/Complex_spec.rb
@@ -0,0 +1,276 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/complex'
+require_relative 'fixtures/Complex'
+
+describe "Kernel.Complex()" do
+ describe "when passed [Complex, Complex]" do
+ it "returns a new Complex number based on the two given numbers" do
+ Complex(Complex(3, 4), Complex(5, 6)).should == Complex(3 - 6, 4 + 5)
+ Complex(Complex(1.5, 2), Complex(-5, 6.3)).should == Complex(1.5 - 6.3, 2 - 5)
+ end
+ end
+
+ describe "when passed [Complex]" do
+ it "returns the passed Complex number" do
+ Complex(Complex(1, 2)).should == Complex(1, 2)
+ Complex(Complex(-3.4, bignum_value)).should == Complex(-3.4, bignum_value)
+ end
+ end
+
+ describe "when passed [Integer, Integer]" do
+ it "returns a new Complex number" do
+ Complex(1, 2).should.instance_of?(Complex)
+ Complex(1, 2).real.should == 1
+ Complex(1, 2).imag.should == 2
+
+ Complex(-3, -5).should.instance_of?(Complex)
+ Complex(-3, -5).real.should == -3
+ Complex(-3, -5).imag.should == -5
+
+ Complex(3.5, -4.5).should.instance_of?(Complex)
+ Complex(3.5, -4.5).real.should == 3.5
+ Complex(3.5, -4.5).imag.should == -4.5
+
+ Complex(bignum_value, 30).should.instance_of?(Complex)
+ Complex(bignum_value, 30).real.should == bignum_value
+ Complex(bignum_value, 30).imag.should == 30
+ end
+ end
+
+ describe "when passed [Integer/Float]" do
+ it "returns a new Complex number with 0 as the imaginary component" do
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ Complex(1).should.instance_of?(Complex)
+ Complex(1).imag.should == 0
+ Complex(1).real.should == 1
+
+ Complex(-3).should.instance_of?(Complex)
+ Complex(-3).imag.should == 0
+ Complex(-3).real.should == -3
+
+ Complex(-4.5).should.instance_of?(Complex)
+ Complex(-4.5).imag.should == 0
+ Complex(-4.5).real.should == -4.5
+
+ Complex(bignum_value).should.instance_of?(Complex)
+ Complex(bignum_value).imag.should == 0
+ Complex(bignum_value).real.should == bignum_value
+ end
+ end
+ end
+
+ describe "when passed [String]" do
+ it_behaves_like :kernel_complex, :Complex_method, KernelSpecs
+
+ context "invalid argument" do
+ it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do
+ -> {
+ Complex("79+4i".encode("UTF-16"))
+ }.should.raise(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16")
+ end
+
+ it "raises ArgumentError for unrecognised Strings" do
+ -> {
+ Complex("ruby")
+ }.should.raise(ArgumentError, 'invalid value for convert(): "ruby"')
+ end
+
+ it "raises ArgumentError for trailing garbage" do
+ -> {
+ Complex("79+4iruby")
+ }.should.raise(ArgumentError, 'invalid value for convert(): "79+4iruby"')
+ end
+
+ it "does not understand Float::INFINITY" do
+ -> {
+ Complex("Infinity")
+ }.should.raise(ArgumentError, 'invalid value for convert(): "Infinity"')
+
+ -> {
+ Complex("-Infinity")
+ }.should.raise(ArgumentError, 'invalid value for convert(): "-Infinity"')
+ end
+
+ it "does not understand Float::NAN" do
+ -> {
+ Complex("NaN")
+ }.should.raise(ArgumentError, 'invalid value for convert(): "NaN"')
+ end
+
+ it "does not understand a sequence of _" do
+ -> {
+ Complex("7__9+4__0i")
+ }.should.raise(ArgumentError, 'invalid value for convert(): "7__9+4__0i"')
+ end
+
+ it "does not allow null-byte" do
+ -> {
+ Complex("1-2i\0")
+ }.should.raise(ArgumentError, "string contains null byte")
+ end
+ end
+
+ context "invalid argument and exception: false passed" do
+ it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do
+ -> {
+ Complex("79+4i".encode("UTF-16"), exception: false)
+ }.should.raise(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16")
+ end
+
+ it "returns nil for unrecognised Strings" do
+ Complex("ruby", exception: false).should == nil
+ end
+
+ it "returns nil when trailing garbage" do
+ Complex("79+4iruby", exception: false).should == nil
+ end
+
+ it "returns nil for Float::INFINITY" do
+ Complex("Infinity", exception: false).should == nil
+ Complex("-Infinity", exception: false).should == nil
+ end
+
+ it "returns nil for Float::NAN" do
+ Complex("NaN", exception: false).should == nil
+ end
+
+ it "returns nil when there is a sequence of _" do
+ Complex("7__9+4__0i", exception: false).should == nil
+ end
+
+ it "returns nil when String contains null-byte" do
+ Complex("1-2i\0", exception: false).should == nil
+ end
+ end
+ end
+
+ describe "when passes [String, String]" do
+ it "needs to be reviewed for spec completeness"
+ end
+
+ describe "when passed an Object which responds to #to_c" do
+ it "returns the passed argument" do
+ obj = Object.new; def obj.to_c; 1i end
+ Complex(obj).should == Complex(0, 1)
+ end
+ end
+
+ describe "when passed a Numeric which responds to #real? with false" do
+ it "returns the passed argument" do
+ n = mock_numeric("unreal")
+ n.should_receive(:real?).any_number_of_times.and_return(false)
+ Complex(n).should.equal?(n)
+ end
+ end
+
+ describe "when passed a Numeric which responds to #real? with true" do
+ it "returns a Complex with the passed argument as the real component and 0 as the imaginary component" do
+ n = mock_numeric("real")
+ n.should_receive(:real?).any_number_of_times.and_return(true)
+ result = Complex(n)
+ result.real.should.equal?(n)
+ result.imag.should.equal?(0)
+ end
+ end
+
+ describe "when passed Numerics n1 and n2 and at least one responds to #real? with false" do
+ [[false, false], [false, true], [true, false]].each do |r1, r2|
+ it "returns n1 + n2 * Complex(0, 1)" do
+ n1 = mock_numeric("n1")
+ n2 = mock_numeric("n2")
+ n3 = mock_numeric("n3")
+ n4 = mock_numeric("n4")
+ n1.should_receive(:real?).any_number_of_times.and_return(r1)
+ n2.should_receive(:real?).any_number_of_times.and_return(r2)
+ n2.should_receive(:*).with(Complex(0, 1)).and_return(n3)
+ n1.should_receive(:+).with(n3).and_return(n4)
+ Complex(n1, n2).should.equal?(n4)
+ end
+ end
+ end
+
+ describe "when passed two Numerics and both respond to #real? with true" do
+ it "returns a Complex with the passed arguments as real and imaginary components respectively" do
+ n1 = mock_numeric("n1")
+ n2 = mock_numeric("n2")
+ n1.should_receive(:real?).any_number_of_times.and_return(true)
+ n2.should_receive(:real?).any_number_of_times.and_return(true)
+ result = Complex(n1, n2)
+ result.real.should.equal?(n1)
+ result.imag.should.equal?(n2)
+ end
+ end
+
+ describe "when passed a single non-Numeric" do
+ it "coerces the passed argument using #to_c" do
+ n = mock("n")
+ c = Complex(0, 0)
+ n.should_receive(:to_c).and_return(c)
+ Complex(n).should.equal?(c)
+ end
+ end
+
+ describe "when passed a non-Numeric second argument" do
+ it "raises TypeError" do
+ -> { Complex(:sym, :sym) }.should.raise(TypeError)
+ -> { Complex(0, :sym) }.should.raise(TypeError)
+ end
+ end
+
+ describe "when passed nil" do
+ it "raises TypeError" do
+ -> { Complex(nil) }.should.raise(TypeError, "can't convert nil into Complex")
+ -> { Complex(0, nil) }.should.raise(TypeError, "can't convert nil into Complex")
+ -> { Complex(nil, 0) }.should.raise(TypeError, "can't convert nil into Complex")
+ end
+ end
+
+ describe "when passed exception: false" do
+ describe "and [Numeric]" do
+ it "returns a complex number" do
+ Complex("123", exception: false).should == Complex(123)
+ end
+ end
+
+ describe "and [non-Numeric]" do
+ it "swallows an error" do
+ Complex(:sym, exception: false).should == nil
+ end
+ end
+
+ describe "and [non-Numeric, Numeric] argument" do
+ it "throws a TypeError" do
+ -> { Complex(:sym, 0, exception: false) }.should.raise(TypeError, "not a real")
+ end
+ end
+
+ describe "and [anything, non-Numeric] argument" do
+ it "swallows an error" do
+ Complex("a", :sym, exception: false).should == nil
+ Complex(:sym, :sym, exception: false).should == nil
+ Complex(0, :sym, exception: false).should == nil
+ end
+ end
+
+ describe "and non-numeric String arguments" do
+ it "swallows an error" do
+ Complex("a", "b", exception: false).should == nil
+ Complex("a", 0, exception: false).should == nil
+ Complex(0, "b", exception: false).should == nil
+ end
+ end
+
+ describe "and nil arguments" do
+ it "swallows an error" do
+ Complex(nil, exception: false).should == nil
+ Complex(0, nil, exception: false).should == nil
+ Complex(nil, 0, exception: false).should == nil
+ end
+ end
+ end
+
+ it "freezes its result" do
+ Complex(1).frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/kernel/Float_spec.rb b/spec/ruby/core/kernel/Float_spec.rb
new file mode 100644
index 0000000000..f5566067ba
--- /dev/null
+++ b/spec/ruby/core/kernel/Float_spec.rb
@@ -0,0 +1,413 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_float, shared: true do
+ it "returns the identical Float for numeric Floats" do
+ float = 1.12
+ float2 = @object.send(:Float, float)
+ float2.should == float
+ float2.should.equal? float
+ end
+
+ it "returns a Float for Fixnums" do
+ @object.send(:Float, 1).should == 1.0
+ end
+
+ it "returns a Float for Complex with only a real part" do
+ @object.send(:Float, Complex(1)).should == 1.0
+ end
+
+ it "returns a Float for Bignums" do
+ @object.send(:Float, 1000000000000).should == 1000000000000.0
+ end
+
+ it "raises an ArgumentError for nil" do
+ -> { @object.send(:Float, nil) }.should.raise(TypeError)
+ end
+
+ it "returns the identical NaN for NaN" do
+ nan = nan_value
+ nan.nan?.should == true
+ nan2 = @object.send(:Float, nan)
+ nan2.nan?.should == true
+ nan2.should.equal?(nan)
+ end
+
+ it "returns the same Infinity for Infinity" do
+ infinity = infinity_value
+ infinity2 = @object.send(:Float, infinity)
+ infinity2.should == infinity_value
+ infinity.should.equal?(infinity2)
+ end
+
+ it "converts Strings to floats without calling #to_f" do
+ string = +"10"
+ string.should_not_receive(:to_f)
+ @object.send(:Float, string).should == 10.0
+ end
+
+ it "converts Strings with decimal points into Floats" do
+ @object.send(:Float, "10.0").should == 10.0
+ end
+
+ it "raises an ArgumentError for a String of word characters" do
+ -> { @object.send(:Float, "float") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String with string in error message" do
+ -> { @object.send(:Float, "foo") }.should.raise(ArgumentError) { |e|
+ e.message.should == 'invalid value for Float(): "foo"'
+ }
+ end
+
+ it "raises an ArgumentError if there are two decimal points in the String" do
+ -> { @object.send(:Float, "10.0.0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String of numbers followed by word characters" do
+ -> { @object.send(:Float, "10D") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String of word characters followed by numbers" do
+ -> { @object.send(:Float, "D10") }.should.raise(ArgumentError)
+ end
+
+ it "is strict about the string form even across newlines" do
+ -> { @object.send(:Float, "not a number\n10") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "10\nnot a number") }.should.raise(ArgumentError)
+ end
+
+ it "converts String subclasses to floats without calling #to_f" do
+ my_string = Class.new(String) do
+ def to_f() 1.2 end
+ end
+
+ @object.send(:Float, my_string.new("10")).should == 10.0
+ end
+
+ it "returns a positive Float if the string is prefixed with +" do
+ @object.send(:Float, "+10").should == 10.0
+ @object.send(:Float, " +10").should == 10.0
+ end
+
+ it "returns a negative Float if the string is prefixed with +" do
+ @object.send(:Float, "-10").should == -10.0
+ @object.send(:Float, " -10").should == -10.0
+ end
+
+ it "raises an ArgumentError if a + or - is embedded in a String" do
+ -> { @object.send(:Float, "1+1") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "1-1") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if a String has a trailing + or -" do
+ -> { @object.send(:Float, "11+") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "11-") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String with a leading _" do
+ -> { @object.send(:Float, "_1") }.should.raise(ArgumentError)
+ end
+
+ it "returns a value for a String with an embedded _" do
+ @object.send(:Float, "1_000").should == 1000.0
+ end
+
+ it "raises an ArgumentError for a String with a trailing _" do
+ -> { @object.send(:Float, "10_") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String of \\0" do
+ -> { @object.send(:Float, "\0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String with a leading \\0" do
+ -> { @object.send(:Float, "\01") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String with an embedded \\0" do
+ -> { @object.send(:Float, "1\01") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String with a trailing \\0" do
+ -> { @object.send(:Float, "1\0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String that is just an empty space" do
+ -> { @object.send(:Float, " ") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a String that with an embedded space" do
+ -> { @object.send(:Float, "1 2") }.should.raise(ArgumentError)
+ end
+
+ it "returns a value for a String with a leading space" do
+ @object.send(:Float, " 1").should == 1.0
+ end
+
+ it "returns a value for a String with a trailing space" do
+ @object.send(:Float, "1 ").should == 1.0
+ end
+
+ it "returns a value for a String with any leading whitespace" do
+ @object.send(:Float, "\t\n1").should == 1.0
+ end
+
+ it "returns a value for a String with any trailing whitespace" do
+ @object.send(:Float, "1\t\n").should == 1.0
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "raises ArgumentError if a fractional part is missing" do
+ -> { @object.send(:Float, "1.") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "+1.") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "-1.") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "1.e+0") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "1.e-2") }.should.raise(ArgumentError)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "allows String representation without a fractional part" do
+ @object.send(:Float, "1.").should == 1.0
+ @object.send(:Float, "+1.").should == 1.0
+ @object.send(:Float, "-1.").should == -1.0
+ @object.send(:Float, "1.e+0").should == 1.0
+ @object.send(:Float, "1.e-2").should be_close(0.01, TOLERANCE)
+ end
+ end
+
+ %w(e E).each do |e|
+ it "raises an ArgumentError if #{e} is the trailing character" do
+ -> { @object.send(:Float, "2#{e}") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if #{e} is the leading character" do
+ -> { @object.send(:Float, "#{e}2") }.should.raise(ArgumentError)
+ end
+
+ it "returns Infinity for '2#{e}1000'" do
+ @object.send(:Float, "2#{e}1000").should == Float::INFINITY
+ end
+
+ it "returns 0 for '2#{e}-1000'" do
+ @object.send(:Float, "2#{e}-1000").should == 0
+ end
+
+ it "allows embedded _ in a number on either side of the #{e}" do
+ @object.send(:Float, "2_0#{e}100").should == 20e100
+ @object.send(:Float, "20#{e}1_00").should == 20e100
+ @object.send(:Float, "2_0#{e}1_00").should == 20e100
+ end
+
+ it "raises an exception if a space is embedded on either side of the '#{e}'" do
+ -> { @object.send(:Float, "2 0#{e}100") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "20#{e}1 00") }.should.raise(ArgumentError)
+ end
+
+ it "raises an exception if there's a leading _ on either side of the '#{e}'" do
+ -> { @object.send(:Float, "_20#{e}100") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "20#{e}_100") }.should.raise(ArgumentError)
+ end
+
+ it "raises an exception if there's a trailing _ on either side of the '#{e}'" do
+ -> { @object.send(:Float, "20_#{e}100") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "20#{e}100_") }.should.raise(ArgumentError)
+ end
+
+ it "allows decimal points on the left side of the '#{e}'" do
+ @object.send(:Float, "2.0#{e}2").should == 2e2
+ end
+
+ it "raises an ArgumentError if there's a decimal point on the right side of the '#{e}'" do
+ -> { @object.send(:Float, "20#{e}2.0") }.should.raise(ArgumentError)
+ end
+ end
+
+ context "for hexadecimal literals" do
+ it "interprets the 0x prefix as hexadecimal" do
+ @object.send(:Float, "0x10").should == 16.0
+ @object.send(:Float, "0x0F").should == 15.0
+ @object.send(:Float, "0x0f").should == 15.0
+ end
+
+ it "interprets negative hex value" do
+ @object.send(:Float, "-0x10").should == -16.0
+ end
+
+ it "accepts embedded _ if the number does not contain a-f" do
+ @object.send(:Float, "0x1_0").should == 16.0
+ end
+
+ ruby_version_is ""..."3.4.3" do
+ it "does not accept embedded _ if the number contains a-f" do
+ -> { @object.send(:Float, "0x1_0a") }.should.raise(ArgumentError)
+ @object.send(:Float, "0x1_0a", exception: false).should == nil
+ end
+ end
+
+ ruby_version_is "3.4.3" do
+ it "accepts embedded _ if the number contains a-f" do
+ @object.send(:Float, "0x1_0a").should == 0x10a.to_f
+ end
+ end
+
+ it "does not accept _ before, after or inside the 0x prefix" do
+ -> { @object.send(:Float, "_0x10") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "0_x10") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "0x_10") }.should.raise(ArgumentError)
+ @object.send(:Float, "_0x10", exception: false).should == nil
+ @object.send(:Float, "0_x10", exception: false).should == nil
+ @object.send(:Float, "0x_10", exception: false).should == nil
+ end
+
+ it "parses negative hexadecimal string as negative float" do
+ @object.send(:Float, "-0x7b").should == -123.0
+ end
+
+ ruby_version_is "3.4" do
+ it "accepts a fractional part" do
+ @object.send(:Float, "0x0.8").should == 0.5
+ end
+ end
+
+ describe "with binary exponent" do
+ %w(p P).each do |p|
+ it "interprets the fractional part (on the left side of '#{p}') in hexadecimal" do
+ @object.send(:Float, "0x10#{p}0").should == 16.0
+ end
+
+ it "interprets the exponent (on the right of '#{p}') in decimal" do
+ @object.send(:Float, "0x1#{p}10").should == 1024.0
+ end
+
+ it "raises an ArgumentError if #{p} is the trailing character" do
+ -> { @object.send(:Float, "0x1#{p}") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if #{p} is the leading character" do
+ -> { @object.send(:Float, "0x#{p}1") }.should.raise(ArgumentError)
+ end
+
+ it "returns Infinity for '0x1#{p}10000'" do
+ @object.send(:Float, "0x1#{p}10000").should == Float::INFINITY
+ end
+
+ it "returns 0 for '0x1#{p}-10000'" do
+ @object.send(:Float, "0x1#{p}-10000").should == 0
+ end
+
+ it "allows embedded _ in a number on either side of the #{p}" do
+ @object.send(:Float, "0x1_0#{p}10").should == 16384.0
+ @object.send(:Float, "0x10#{p}1_0").should == 16384.0
+ @object.send(:Float, "0x1_0#{p}1_0").should == 16384.0
+ end
+
+ it "raises an exception if a space is embedded on either side of the '#{p}'" do
+ -> { @object.send(:Float, "0x1 0#{p}10") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "0x10#{p}1 0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an exception if there's a leading _ on either side of the '#{p}'" do
+ -> { @object.send(:Float, "0x_10#{p}10") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "0x10#{p}_10") }.should.raise(ArgumentError)
+ end
+
+ it "raises an exception if there's a trailing _ on either side of the '#{p}'" do
+ -> { @object.send(:Float, "0x10_#{p}10") }.should.raise(ArgumentError)
+ -> { @object.send(:Float, "0x10#{p}10_") }.should.raise(ArgumentError)
+ end
+
+ it "allows hexadecimal points on the left side of the '#{p}'" do
+ @object.send(:Float, "0x1.8#{p}0").should == 1.5
+ end
+
+ it "raises an ArgumentError if there's a decimal point on the right side of the '#{p}'" do
+ -> { @object.send(:Float, "0x1#{p}1.0") }.should.raise(ArgumentError)
+ end
+ end
+ end
+ end
+
+ it "returns a Float that can be a parameter to #Float again" do
+ float = @object.send(:Float, "10")
+ @object.send(:Float, float).should == 10.0
+ end
+
+ it "otherwise, converts the given argument to a Float by calling #to_f" do
+ (obj = mock('1.2')).should_receive(:to_f).once.and_return(1.2)
+ obj.should_not_receive(:to_i)
+ @object.send(:Float, obj).should == 1.2
+ end
+
+ it "returns the identical NaN if to_f is called and it returns NaN" do
+ nan = nan_value
+ (nan_to_f = mock('NaN')).should_receive(:to_f).once.and_return(nan)
+ nan2 = @object.send(:Float, nan_to_f)
+ nan2.nan?.should == true
+ nan2.should.equal?(nan)
+ end
+
+ it "returns the identical Infinity if #to_f is called and it returns Infinity" do
+ infinity = infinity_value
+ (infinity_to_f = mock('Infinity')).should_receive(:to_f).once.and_return(infinity)
+ infinity2 = @object.send(:Float, infinity_to_f)
+ infinity2.should.equal?(infinity)
+ end
+
+ it "raises a TypeError if #to_f is not provided" do
+ -> { @object.send(:Float, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_f returns a String" do
+ (obj = mock('ha!')).should_receive(:to_f).once.and_return('ha!')
+ -> { @object.send(:Float, obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_f returns an Integer" do
+ (obj = mock('123')).should_receive(:to_f).once.and_return(123)
+ -> { @object.send(:Float, obj) }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError when passed a Complex argument" do
+ c = Complex(2, 3)
+ -> { @object.send(:Float, c) }.should.raise(RangeError)
+ end
+
+ describe "when passed exception: false" do
+ describe "and valid input" do
+ it "returns a Float number" do
+ @object.send(:Float, 1, exception: false).should == 1.0
+ @object.send(:Float, "1", exception: false).should == 1.0
+ @object.send(:Float, "1.23", exception: false).should == 1.23
+ end
+ end
+
+ describe "and invalid input" do
+ it "swallows an error" do
+ @object.send(:Float, "abc", exception: false).should == nil
+ @object.send(:Float, :sym, exception: false).should == nil
+ end
+ end
+
+ describe "and nil" do
+ it "swallows it" do
+ @object.send(:Float, nil, exception: false).should == nil
+ end
+ end
+ end
+end
+
+describe "Kernel.Float" do
+ it_behaves_like :kernel_float, :Float, Kernel
+end
+
+describe "Kernel#Float" do
+ it_behaves_like :kernel_float, :Float, Object.new
+end
+
+describe "Kernel#Float" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:Float)
+ end
+end
diff --git a/spec/ruby/core/kernel/Hash_spec.rb b/spec/ruby/core/kernel/Hash_spec.rb
new file mode 100644
index 0000000000..ee16f56a90
--- /dev/null
+++ b/spec/ruby/core/kernel/Hash_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#hash" do
+ it "is provided" do
+ 1.respond_to?(:hash).should == true
+ end
+
+ it "is stable" do
+ 1.hash.should == 1.hash
+ end
+end
+
+describe "Kernel" do
+ it "has private instance method Hash()" do
+ Kernel.private_instance_methods(false).should.include?(:Hash)
+ end
+end
+
+describe :kernel_Hash, shared: true do
+ before :each do
+ @hash = { a: 1}
+ end
+
+ it "converts nil to a Hash" do
+ @object.send(@method, nil).should == {}
+ end
+
+ it "converts an empty array to a Hash" do
+ @object.send(@method, []).should == {}
+ end
+
+ it "does not call #to_hash on an Hash" do
+ @hash.should_not_receive(:to_hash)
+ @object.send(@method, @hash).should == @hash
+ end
+
+ it "calls #to_hash to convert the argument to an Hash" do
+ obj = mock("Hash(a: 1)")
+ obj.should_receive(:to_hash).and_return(@hash)
+
+ @object.send(@method, obj).should == @hash
+ end
+
+ it "raises a TypeError if it doesn't respond to #to_hash" do
+ -> { @object.send(@method, mock("")) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_hash does not return an Hash" do
+ obj = mock("Hash() string")
+ obj.should_receive(:to_hash).and_return("string")
+
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+end
+
+describe "Kernel.Hash" do
+ it_behaves_like :kernel_Hash, :Hash_method, KernelSpecs
+end
+
+describe "Kernel#Hash" do
+ it_behaves_like :kernel_Hash, :Hash_function, KernelSpecs
+end
diff --git a/spec/ruby/core/kernel/Integer_spec.rb b/spec/ruby/core/kernel/Integer_spec.rb
new file mode 100644
index 0000000000..978fd8ef08
--- /dev/null
+++ b/spec/ruby/core/kernel/Integer_spec.rb
@@ -0,0 +1,845 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_integer, shared: true do
+ it "returns a Bignum for a Bignum" do
+ Integer(2e100).should == 2e100
+ end
+
+ it "returns a Fixnum for a Fixnum" do
+ Integer(100).should == 100
+ end
+
+ it "raises a TypeError when to_int returns not-an-Integer object and to_i returns nil" do
+ obj = mock("object")
+ obj.should_receive(:to_int).and_return("1")
+ obj.should_receive(:to_i).and_return(nil)
+ -> {
+ Integer(obj)
+ }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_i gives nil)")
+ end
+
+ it "return a result of to_i when to_int does not return an Integer" do
+ obj = mock("object")
+ obj.should_receive(:to_int).and_return("1")
+ obj.should_receive(:to_i).and_return(42)
+ Integer(obj).should == 42
+ end
+
+ it "returns a result of to_str" do
+ obj = mock("obj")
+ obj.should_receive(:to_str).and_return("1")
+
+ Integer(obj).should == 1
+ end
+
+ it "returns a result of to_int when both to_int and to_str are defined" do
+ obj = mock("obj")
+ obj.should_receive(:to_int).and_return(1)
+ obj.should_not_receive(:to_str)
+
+ Integer(obj).should == 1
+ end
+
+ it "returns a result of to_str when both to_str and to_i are defined" do
+ obj = mock("obj")
+ obj.should_receive(:to_str).and_return("1")
+ obj.should_not_receive(:to_i)
+
+ Integer(obj).should == 1
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { Integer(nil) }.should.raise(TypeError, "can't convert nil into Integer")
+ end
+
+ it "returns an Integer object" do
+ Integer(2).should.instance_of?(Integer)
+ Integer(9**99).should.instance_of?(Integer)
+ end
+
+ it "truncates Floats" do
+ Integer(3.14).should == 3
+ Integer(90.8).should == 90
+ end
+
+ it "calls to_i on Rationals" do
+ Integer(Rational(8,3)).should == 2
+ Integer(3.quo(2)).should == 1
+ end
+
+ it "returns the value of to_int if the result is a Fixnum" do
+ obj = mock("object")
+ obj.should_receive(:to_int).and_return(1)
+ obj.should_not_receive(:to_i)
+ Integer(obj).should == 1
+ end
+
+ it "returns the value of to_int if the result is a Bignum" do
+ obj = mock("object")
+ obj.should_receive(:to_int).and_return(2 * 10**100)
+ obj.should_not_receive(:to_i)
+ Integer(obj).should == 2 * 10**100
+ end
+
+ it "calls to_i on an object whose to_int returns nil" do
+ obj = mock("object")
+ obj.should_receive(:to_int).and_return(nil)
+ obj.should_receive(:to_i).and_return(1)
+ Integer(obj).should == 1
+ end
+
+ it "raises a TypeError if to_i returns a value that is not an Integer" do
+ obj = mock("object")
+ obj.should_receive(:to_i).and_return("1")
+ -> {
+ Integer(obj)
+ }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_i gives String)")
+ end
+
+ it "raises a TypeError if no to_int or to_i methods exist" do
+ obj = mock("object")
+ -> {
+ Integer(obj)
+ }.should.raise(TypeError, "can't convert MockObject into Integer")
+ end
+
+ it "raises a TypeError if to_int returns nil and no to_i exists" do
+ obj = mock("object")
+ obj.should_receive(:to_i).and_return(nil)
+ -> {
+ Integer(obj)
+ }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_i gives nil)")
+ end
+
+ it "raises a FloatDomainError when passed NaN" do
+ -> { Integer(nan_value) }.should.raise(FloatDomainError)
+ end
+
+ it "raises a FloatDomainError when passed Infinity" do
+ -> { Integer(infinity_value) }.should.raise(FloatDomainError)
+ end
+
+ describe "when passed exception: false" do
+ describe "and to_i returns a value that is not an Integer" do
+ it "swallows an error" do
+ obj = mock("object")
+ obj.should_receive(:to_i).and_return("1")
+ Integer(obj, exception: false).should == nil
+ end
+ end
+
+ describe "and no to_int or to_i methods exist" do
+ it "swallows an error" do
+ obj = mock("object")
+ Integer(obj, exception: false).should == nil
+ end
+ end
+
+ describe "and to_int returns nil and no to_i exists" do
+ it "swallows an error" do
+ obj = mock("object")
+ obj.should_receive(:to_i).and_return(nil)
+ Integer(obj, exception: false).should == nil
+ end
+ end
+
+ describe "and passed NaN" do
+ it "swallows an error" do
+ Integer(nan_value, exception: false).should == nil
+ end
+ end
+
+ describe "and passed Infinity" do
+ it "swallows an error" do
+ Integer(infinity_value, exception: false).should == nil
+ end
+ end
+
+ describe "and passed nil" do
+ it "swallows an error" do
+ Integer(nil, exception: false).should == nil
+ end
+ end
+
+ describe "and passed a String that contains numbers" do
+ it "normally parses it and returns an Integer" do
+ Integer("42", exception: false).should == 42
+ end
+ end
+
+ describe "and passed a String that can't be converted to an Integer" do
+ it "swallows an error" do
+ Integer("abc", exception: false).should == nil
+ end
+ end
+ end
+end
+
+describe :kernel_integer_string, shared: true do
+ it "raises an ArgumentError if the String is a null byte" do
+ -> { Integer("\0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the String starts with a null byte" do
+ -> { Integer("\01") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the String ends with a null byte" do
+ -> { Integer("1\0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the String contains a null byte" do
+ -> { Integer("1\01") }.should.raise(ArgumentError)
+ end
+
+ it "ignores leading whitespace" do
+ Integer(" 1").should == 1
+ Integer(" 1").should == 1
+ Integer("\t\n1").should == 1
+ end
+
+ it "ignores trailing whitespace" do
+ Integer("1 ").should == 1
+ Integer("1 ").should == 1
+ Integer("1\t\n").should == 1
+ end
+
+ it "raises an ArgumentError if there are leading _s" do
+ -> { Integer("_1") }.should.raise(ArgumentError)
+ -> { Integer("___1") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are trailing _s" do
+ -> { Integer("1_") }.should.raise(ArgumentError)
+ -> { Integer("1___") }.should.raise(ArgumentError)
+ end
+
+ it "ignores an embedded _" do
+ Integer("1_1").should == 11
+ end
+
+ it "raises an ArgumentError if there are multiple embedded _s" do
+ -> { Integer("1__1") }.should.raise(ArgumentError)
+ -> { Integer("1___1") }.should.raise(ArgumentError)
+ end
+
+ it "ignores a single leading +" do
+ Integer("+1").should == 1
+ end
+
+ it "raises an ArgumentError if there is a space between the + and number" do
+ -> { Integer("+ 1") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are multiple leading +s" do
+ -> { Integer("++1") }.should.raise(ArgumentError)
+ -> { Integer("+++1") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are trailing +s" do
+ -> { Integer("1+") }.should.raise(ArgumentError)
+ -> { Integer("1+++") }.should.raise(ArgumentError)
+ end
+
+ it "makes the number negative if there's a leading -" do
+ Integer("-1").should == -1
+ end
+
+ it "raises an ArgumentError if there are multiple leading -s" do
+ -> { Integer("--1") }.should.raise(ArgumentError)
+ -> { Integer("---1") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are trailing -s" do
+ -> { Integer("1-") }.should.raise(ArgumentError)
+ -> { Integer("1---") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there is a period" do
+ -> { Integer("0.0") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for an empty String" do
+ -> { Integer("") }.should.raise(ArgumentError)
+ end
+
+ describe "when passed exception: false" do
+ describe "and multiple leading -s" do
+ it "swallows an error" do
+ Integer("---1", exception: false).should == nil
+ end
+ end
+
+ describe "and multiple trailing -s" do
+ it "swallows an error" do
+ Integer("1---", exception: false).should == nil
+ end
+ end
+
+ describe "and an argument that contains a period" do
+ it "swallows an error" do
+ Integer("0.0", exception: false).should == nil
+ end
+ end
+
+ describe "and an empty string" do
+ it "swallows an error" do
+ Integer("", exception: false).should == nil
+ end
+ end
+ end
+
+ it "parses the value as 0 if the string consists of a single zero character" do
+ Integer("0").should == 0
+ end
+
+ %w(x X).each do |x|
+ it "parses the value as a hex number if there's a leading 0#{x}" do
+ Integer("0#{x}1").should == 0x1
+ Integer("0#{x}dd").should == 0xdd
+ end
+
+ it "is a positive hex number if there's a leading +0#{x}" do
+ Integer("+0#{x}1").should == 0x1
+ Integer("+0#{x}dd").should == 0xdd
+ end
+
+ it "is a negative hex number if there's a leading -0#{x}" do
+ Integer("-0#{x}1").should == -0x1
+ Integer("-0#{x}dd").should == -0xdd
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as hex" do
+ -> { Integer("0#{x}g") }.should.raise(ArgumentError)
+ end
+ end
+
+ %w(b B).each do |b|
+ it "parses the value as a binary number if there's a leading 0#{b}" do
+ Integer("0#{b}1").should == 0b1
+ Integer("0#{b}10").should == 0b10
+ end
+
+ it "is a positive binary number if there's a leading +0#{b}" do
+ Integer("+0#{b}1").should == 0b1
+ Integer("+0#{b}10").should == 0b10
+ end
+
+ it "is a negative binary number if there's a leading -0#{b}" do
+ Integer("-0#{b}1").should == -0b1
+ Integer("-0#{b}10").should == -0b10
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as binary" do
+ -> { Integer("0#{b}2") }.should.raise(ArgumentError)
+ end
+ end
+
+ ["o", "O", ""].each do |o|
+ it "parses the value as an octal number if there's a leading 0#{o}" do
+ Integer("0#{o}1").should == 0O1
+ Integer("0#{o}10").should == 0O10
+ end
+
+ it "is a positive octal number if there's a leading +0#{o}" do
+ Integer("+0#{o}1").should == 0O1
+ Integer("+0#{o}10").should == 0O10
+ end
+
+ it "is a negative octal number if there's a leading -0#{o}" do
+ Integer("-0#{o}1").should == -0O1
+ Integer("-0#{o}10").should == -0O10
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as octal" do
+ -> { Integer("0#{o}9") }.should.raise(ArgumentError)
+ end
+ end
+
+ %w(D d).each do |d|
+ it "parses the value as a decimal number if there's a leading 0#{d}" do
+ Integer("0#{d}1").should == 1
+ Integer("0#{d}10").should == 10
+ end
+
+ it "is a positive decimal number if there's a leading +0#{d}" do
+ Integer("+0#{d}1").should == 1
+ Integer("+0#{d}10").should == 10
+ end
+
+ it "is a negative decimal number if there's a leading -0#{d}" do
+ Integer("-0#{d}1").should == -1
+ Integer("-0#{d}10").should == -10
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as decimal" do
+ -> { Integer("0#{d}a") }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe :kernel_integer_string_base, shared: true do
+ it "raises an ArgumentError if the String is a null byte" do
+ -> { Integer("\0", 2) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the String starts with a null byte" do
+ -> { Integer("\01", 3) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the String ends with a null byte" do
+ -> { Integer("1\0", 4) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the String contains a null byte" do
+ -> { Integer("1\01", 5) }.should.raise(ArgumentError)
+ end
+
+ it "ignores leading whitespace" do
+ Integer(" 16", 16).should == 22
+ Integer(" 16", 16).should == 22
+ Integer("\t\n16", 16).should == 22
+ end
+
+ it "ignores trailing whitespace" do
+ Integer("16 ", 16).should == 22
+ Integer("16 ", 16).should == 22
+ Integer("16\t\n", 16).should == 22
+ end
+
+ it "raises an ArgumentError if there are leading _s" do
+ -> { Integer("_1", 7) }.should.raise(ArgumentError)
+ -> { Integer("___1", 7) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are trailing _s" do
+ -> { Integer("1_", 12) }.should.raise(ArgumentError)
+ -> { Integer("1___", 12) }.should.raise(ArgumentError)
+ end
+
+ it "ignores an embedded _" do
+ Integer("1_1", 4).should == 5
+ end
+
+ it "raises an ArgumentError if there are multiple embedded _s" do
+ -> { Integer("1__1", 4) }.should.raise(ArgumentError)
+ -> { Integer("1___1", 4) }.should.raise(ArgumentError)
+ end
+
+ it "ignores a single leading +" do
+ Integer("+10", 3).should == 3
+ end
+
+ it "raises an ArgumentError if there is a space between the + and number" do
+ -> { Integer("+ 1", 3) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are multiple leading +s" do
+ -> { Integer("++1", 3) }.should.raise(ArgumentError)
+ -> { Integer("+++1", 3) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are trailing +s" do
+ -> { Integer("1+", 3) }.should.raise(ArgumentError)
+ -> { Integer("1+++", 12) }.should.raise(ArgumentError)
+ end
+
+ it "makes the number negative if there's a leading -" do
+ Integer("-19", 20).should == -29
+ end
+
+ it "raises an ArgumentError if there are multiple leading -s" do
+ -> { Integer("--1", 9) }.should.raise(ArgumentError)
+ -> { Integer("---1", 9) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there are trailing -s" do
+ -> { Integer("1-", 12) }.should.raise(ArgumentError)
+ -> { Integer("1---", 12) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if there is a period" do
+ -> { Integer("0.0", 3) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for an empty String" do
+ -> { Integer("", 12) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a base of 1" do
+ -> { Integer("1", 1) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError for a base of 37" do
+ -> { Integer("1", 37) }.should.raise(ArgumentError)
+ end
+
+ it "accepts wholly lowercase alphabetic strings for bases > 10" do
+ Integer('ab',12).should == 131
+ Integer('af',20).should == 215
+ Integer('ghj',30).should == 14929
+ end
+
+ it "accepts wholly uppercase alphabetic strings for bases > 10" do
+ Integer('AB',12).should == 131
+ Integer('AF',20).should == 215
+ Integer('GHJ',30).should == 14929
+ end
+
+ it "accepts mixed-case alphabetic strings for bases > 10" do
+ Integer('Ab',12).should == 131
+ Integer('aF',20).should == 215
+ Integer('GhJ',30).should == 14929
+ end
+
+ it "accepts alphanumeric strings for bases > 10" do
+ Integer('a3e',19).should == 3681
+ Integer('12q',31).should == 1049
+ Integer('c00o',29).should == 292692
+ end
+
+ it "raises an ArgumentError for letters invalid in the given base" do
+ -> { Integer('z',19) }.should.raise(ArgumentError)
+ -> { Integer('c00o',2) }.should.raise(ArgumentError)
+ end
+
+ %w(x X).each do |x|
+ it "parses the value as a hex number if there's a leading 0#{x} and a base of 16" do
+ Integer("0#{x}10", 16).should == 16
+ Integer("0#{x}dd", 16).should == 221
+ end
+
+ it "is a positive hex number if there's a leading +0#{x} and base of 16" do
+ Integer("+0#{x}1", 16).should == 0x1
+ Integer("+0#{x}dd", 16).should == 0xdd
+ end
+
+ it "is a negative hex number if there's a leading -0#{x} and a base of 16" do
+ Integer("-0#{x}1", 16).should == -0x1
+ Integer("-0#{x}dd", 16).should == -0xdd
+ end
+
+ 2.upto(15) do |base|
+ it "raises an ArgumentError if the number begins with 0#{x} and the base is #{base}" do
+ -> { Integer("0#{x}1", base) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as hex and the base is 16" do
+ -> { Integer("0#{x}g", 16) }.should.raise(ArgumentError)
+ end
+ end
+
+ %w(b B).each do |b|
+ it "parses the value as a binary number if there's a leading 0#{b} and the base is 2" do
+ Integer("0#{b}1", 2).should == 0b1
+ Integer("0#{b}10", 2).should == 0b10
+ end
+
+ it "is a positive binary number if there's a leading +0#{b} and a base of 2" do
+ Integer("+0#{b}1", 2).should == 0b1
+ Integer("+0#{b}10", 2).should == 0b10
+ end
+
+ it "is a negative binary number if there's a leading -0#{b} and a base of 2" do
+ Integer("-0#{b}1", 2).should == -0b1
+ Integer("-0#{b}10", 2).should == -0b10
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as binary and the base is 2" do
+ -> { Integer("0#{b}2", 2) }.should.raise(ArgumentError)
+ end
+ end
+
+ ["o", "O"].each do |o|
+ it "parses the value as an octal number if there's a leading 0#{o} and a base of 8" do
+ Integer("0#{o}1", 8).should == 0O1
+ Integer("0#{o}10", 8).should == 0O10
+ end
+
+ it "is a positive octal number if there's a leading +0#{o} and a base of 8" do
+ Integer("+0#{o}1", 8).should == 0O1
+ Integer("+0#{o}10", 8).should == 0O10
+ end
+
+ it "is a negative octal number if there's a leading -0#{o} and a base of 8" do
+ Integer("-0#{o}1", 8).should == -0O1
+ Integer("-0#{o}10", 8).should == -0O10
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as octal and the base is 8" do
+ -> { Integer("0#{o}9", 8) }.should.raise(ArgumentError)
+ end
+
+ 2.upto(7) do |base|
+ it "raises an ArgumentError if the number begins with 0#{o} and the base is #{base}" do
+ -> { Integer("0#{o}1", base) }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ %w(D d).each do |d|
+ it "parses the value as a decimal number if there's a leading 0#{d} and a base of 10" do
+ Integer("0#{d}1", 10).should == 1
+ Integer("0#{d}10",10).should == 10
+ end
+
+ it "is a positive decimal number if there's a leading +0#{d} and a base of 10" do
+ Integer("+0#{d}1", 10).should == 1
+ Integer("+0#{d}10", 10).should == 10
+ end
+
+ it "is a negative decimal number if there's a leading -0#{d} and a base of 10" do
+ Integer("-0#{d}1", 10).should == -1
+ Integer("-0#{d}10", 10).should == -10
+ end
+
+ it "raises an ArgumentError if the number cannot be parsed as decimal and the base is 10" do
+ -> { Integer("0#{d}a", 10) }.should.raise(ArgumentError)
+ end
+
+ 2.upto(9) do |base|
+ it "raises an ArgumentError if the number begins with 0#{d} and the base is #{base}" do
+ -> { Integer("0#{d}1", base) }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ it "raises an ArgumentError if a base is given for a non-String value" do
+ -> { Integer(98, 15) }.should.raise(ArgumentError, "base specified for non string value")
+ end
+
+ it "tries to convert the base to an integer using to_int" do
+ obj = mock('8')
+ obj.should_receive(:to_int).and_return(8)
+
+ Integer("777", obj).should == 0777
+ end
+
+ it "raises a TypeError if it is not an integer and does not respond to #to_i" do
+ -> {
+ Integer("777", "8")
+ }.should.raise(TypeError, "no implicit conversion of String into Integer")
+ end
+
+ describe "when passed exception: false" do
+ describe "and valid argument" do
+ it "returns an Integer number" do
+ Integer("100", 10, exception: false).should == 100
+ Integer("100", 2, exception: false).should == 4
+ end
+ end
+
+ describe "and invalid argument" do
+ it "swallows an error" do
+ Integer("999", 2, exception: false).should == nil
+ Integer("abc", 10, exception: false).should == nil
+ end
+ end
+ end
+end
+
+describe :kernel_Integer, shared: true do
+ it "raises an ArgumentError when the String contains digits out of range of radix 2" do
+ str = "23456789abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 2) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 3" do
+ str = "3456789abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 3) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 4" do
+ str = "456789abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 4) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 5" do
+ str = "56789abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 5) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 6" do
+ str = "6789abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 6) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 7" do
+ str = "789abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 7) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 8" do
+ str = "89abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 8) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 9" do
+ str = "9abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 9) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 10" do
+ str = "abcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 10) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 11" do
+ str = "bcdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 11) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 12" do
+ str = "cdefghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 12) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 13" do
+ str = "defghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 13) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 14" do
+ str = "efghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 14) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 15" do
+ str = "fghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 15) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 16" do
+ str = "ghijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 16) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 17" do
+ str = "hijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 17) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 18" do
+ str = "ijklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 18) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 19" do
+ str = "jklmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 19) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 20" do
+ str = "klmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 20) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 21" do
+ str = "lmnopqrstuvwxyz"
+ -> { @object.send(@method, str, 21) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 22" do
+ str = "mnopqrstuvwxyz"
+ -> { @object.send(@method, str, 22) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 23" do
+ str = "nopqrstuvwxyz"
+ -> { @object.send(@method, str, 23) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 24" do
+ str = "opqrstuvwxyz"
+ -> { @object.send(@method, str, 24) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 25" do
+ str = "pqrstuvwxyz"
+ -> { @object.send(@method, str, 25) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 26" do
+ str = "qrstuvwxyz"
+ -> { @object.send(@method, str, 26) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 27" do
+ str = "rstuvwxyz"
+ -> { @object.send(@method, str, 27) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 28" do
+ str = "stuvwxyz"
+ -> { @object.send(@method, str, 28) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 29" do
+ str = "tuvwxyz"
+ -> { @object.send(@method, str, 29) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 30" do
+ str = "uvwxyz"
+ -> { @object.send(@method, str, 30) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 31" do
+ str = "vwxyz"
+ -> { @object.send(@method, str, 31) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 32" do
+ str = "wxyz"
+ -> { @object.send(@method, str, 32) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 33" do
+ str = "xyz"
+ -> { @object.send(@method, str, 33) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 34" do
+ str = "yz"
+ -> { @object.send(@method, str, 34) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 35" do
+ str = "z"
+ -> { @object.send(@method, str, 35) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the String contains digits out of range of radix 36" do
+ -> { @object.send(@method, "{", 36) }.should.raise(ArgumentError)
+ end
+end
+
+describe "Kernel.Integer" do
+ it_behaves_like :kernel_Integer, :Integer_method, KernelSpecs
+
+ # TODO: fix these specs
+ it_behaves_like :kernel_integer, :Integer, Kernel
+ it_behaves_like :kernel_integer_string, :Integer
+
+ it_behaves_like :kernel_integer_string_base, :Integer
+
+ it "is a public method" do
+ Kernel.Integer(10).should == 10
+ end
+end
+
+describe "Kernel#Integer" do
+ it_behaves_like :kernel_Integer, :Integer_function, KernelSpecs
+
+ # TODO: fix these specs
+ it_behaves_like :kernel_integer, :Integer, Object.new
+ it_behaves_like :kernel_integer_string, :Integer
+
+ it_behaves_like :kernel_integer_string_base, :Integer
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:Integer)
+ end
+end
diff --git a/spec/ruby/core/kernel/Rational_spec.rb b/spec/ruby/core/kernel/Rational_spec.rb
new file mode 100644
index 0000000000..b13ab4cf55
--- /dev/null
+++ b/spec/ruby/core/kernel/Rational_spec.rb
@@ -0,0 +1,236 @@
+require_relative '../../spec_helper'
+require_relative '../rational/fixtures/rational'
+
+describe "Kernel.Rational" do
+ describe "passed Integer" do
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ it "returns a new Rational number with 1 as the denominator" do
+ Rational(1).should.eql?(Rational(1, 1))
+ Rational(-3).should.eql?(Rational(-3, 1))
+ Rational(bignum_value).should.eql?(Rational(bignum_value, 1))
+ end
+ end
+ end
+
+ describe "passed two integers" do
+ it "returns a new Rational number" do
+ rat = Rational(1, 2)
+ rat.numerator.should == 1
+ rat.denominator.should == 2
+ rat.should.instance_of?(Rational)
+
+ rat = Rational(-3, -5)
+ rat.numerator.should == 3
+ rat.denominator.should == 5
+ rat.should.instance_of?(Rational)
+
+ rat = Rational(bignum_value, 3)
+ rat.numerator.should == bignum_value
+ rat.denominator.should == 3
+ rat.should.instance_of?(Rational)
+ end
+
+ it "reduces the Rational" do
+ rat = Rational(2, 4)
+ rat.numerator.should == 1
+ rat.denominator.should == 2
+
+ rat = Rational(3, 9)
+ rat.numerator.should == 1
+ rat.denominator.should == 3
+ end
+ end
+
+ describe "when passed a String" do
+ it "converts the String to a Rational using the same method as String#to_r" do
+ r = Rational(13, 25)
+ s_r = ".52".to_r
+ r_s = Rational(".52")
+
+ r_s.should == r
+ r_s.should == s_r
+ end
+
+ it "scales the Rational value of the first argument by the Rational value of the second" do
+ Rational(".52", ".6").should == Rational(13, 15)
+ Rational(".52", "1.6").should == Rational(13, 40)
+ end
+
+ it "does not use the same method as Float#to_r" do
+ r = Rational(3, 5)
+ f_r = 0.6.to_r
+ r_s = Rational("0.6")
+
+ r_s.should == r
+ r_s.should_not == f_r
+ end
+ end
+
+ describe "when passed a Numeric" do
+ it "calls #to_r to convert the first argument to a Rational" do
+ num = RationalSpecs::SubNumeric.new(2)
+
+ Rational(num).should == Rational(2)
+ end
+ end
+
+ describe "when passed a Complex" do
+ context "[Complex]" do
+ it "returns a Rational from the real part if the imaginary part is 0" do
+ Rational(Complex(1, 0)).should == Rational(1)
+ end
+
+ it "raises a RangeError if the imaginary part is not 0" do
+ -> { Rational(Complex(1, 2)) }.should.raise(RangeError, "can't convert 1+2i into Rational")
+ end
+ end
+
+ context "[Numeric, Complex]" do
+ it "uses the real part if the imaginary part is 0" do
+ Rational(1, Complex(2, 0)).should == Rational(1, 2)
+ end
+
+ it "divides a numerator by the Complex denominator if the imaginary part is not 0" do
+ Rational(1, Complex(2, 1)).should == Complex(2/5r, -1/5r)
+ end
+ end
+ end
+
+ context "when passed neither a Numeric nor a String" do
+ it "converts to Rational with #to_r method" do
+ obj = Object.new
+ def obj.to_r; 1/2r; end
+
+ Rational(obj).should == 1/2r
+ end
+
+ it "tries to convert to Integer with #to_int method if it does not respond to #to_r" do
+ obj = Object.new
+ def obj.to_int; 1; end
+
+ Rational(obj).should == 1r
+ end
+
+ it "raises TypeError if it neither responds to #to_r nor #to_int method" do
+ -> { Rational([]) }.should.raise(TypeError, "can't convert Array into Rational")
+ -> { Rational({}) }.should.raise(TypeError, "can't convert Hash into Rational")
+ -> { Rational(nil) }.should.raise(TypeError, "can't convert nil into Rational")
+ end
+
+ it "swallows exception raised in #to_int method" do
+ object = Object.new
+ def object.to_int() raise NoMethodError; end
+
+ -> { Rational(object) }.should.raise(TypeError)
+ -> { Rational(object, 1) }.should.raise(TypeError)
+ -> { Rational(1, object) }.should.raise(TypeError)
+ end
+
+ it "raises TypeError if #to_r does not return Rational" do
+ obj = Object.new
+ def obj.to_r; []; end
+
+ -> { Rational(obj) }.should raise_consistent_error(TypeError, "can't convert Object into Rational (Object#to_r gives Array)")
+ end
+ end
+
+ it "raises a ZeroDivisionError if the second argument is 0" do
+ -> { Rational(1, 0) }.should.raise(ZeroDivisionError, "divided by 0")
+ -> { Rational(1, 0.0) }.should.raise(ZeroDivisionError, "divided by 0")
+ end
+
+ it "raises a TypeError if the first argument is nil" do
+ -> { Rational(nil) }.should.raise(TypeError, "can't convert nil into Rational")
+ end
+
+ it "raises a TypeError if the second argument is nil" do
+ -> { Rational(1, nil) }.should.raise(TypeError, "can't convert nil into Rational")
+ end
+
+ it "raises a TypeError if the first argument is a Symbol" do
+ -> { Rational(:sym) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the second argument is a Symbol" do
+ -> { Rational(1, :sym) }.should.raise(TypeError)
+ end
+
+ describe "when passed exception: false" do
+ describe "and [non-Numeric]" do
+ it "swallows an error" do
+ Rational(:sym, exception: false).should == nil
+ Rational("abc", exception: false).should == nil
+ end
+
+ it "swallows an exception raised in #to_r" do
+ obj = Object.new
+ def obj.to_r; raise; end
+ Rational(obj, exception: false).should == nil
+ end
+
+ it "swallows an exception raised in #to_int" do
+ obj = Object.new
+ def obj.to_int; raise; end
+ Rational(obj, exception: false).should == nil
+ end
+ end
+
+ describe "and [non-Numeric, Numeric]" do
+ it "swallows an error" do
+ Rational(:sym, 1, exception: false).should == nil
+ Rational("abc", 1, exception: false).should == nil
+ end
+
+ it "swallows an exception raised in #to_r" do
+ obj = Object.new
+ def obj.to_r; raise; end
+ Rational(obj, 1, exception: false).should == nil
+ end
+
+ it "swallows an exception raised in #to_int" do
+ obj = Object.new
+ def obj.to_int; raise; end
+ Rational(obj, 1, exception: false).should == nil
+ end
+ end
+
+ describe "and [anything, non-Numeric]" do
+ it "swallows an error" do
+ Rational(:sym, :sym, exception: false).should == nil
+ Rational("abc", :sym, exception: false).should == nil
+ end
+
+ it "swallows an exception raised in #to_r" do
+ obj = Object.new
+ def obj.to_r; raise; end
+ Rational(obj, obj, exception: false).should == nil
+ end
+
+ it "swallows an exception raised in #to_int" do
+ obj = Object.new
+ def obj.to_int; raise; end
+ Rational(obj, obj, exception: false).should == nil
+ end
+ end
+
+ describe "and non-Numeric String arguments" do
+ it "swallows an error" do
+ Rational("a", "b", exception: false).should == nil
+ Rational("a", 0, exception: false).should == nil
+ Rational(0, "b", exception: false).should == nil
+ end
+ end
+
+ describe "and nil arguments" do
+ it "swallows an error" do
+ Rational(nil, exception: false).should == nil
+ Rational(nil, nil, exception: false).should == nil
+ end
+ end
+ end
+
+ it "freezes its result" do
+ Rational(1).frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/kernel/String_spec.rb b/spec/ruby/core/kernel/String_spec.rb
new file mode 100644
index 0000000000..a9b0758ad7
--- /dev/null
+++ b/spec/ruby/core/kernel/String_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_String, shared: true do
+ it "converts nil to a String" do
+ @object.send(@method, nil).should == ""
+ end
+
+ it "converts a Float to a String" do
+ @object.send(@method, 1.12).should == "1.12"
+ end
+
+ it "converts a boolean to a String" do
+ @object.send(@method, false).should == "false"
+ @object.send(@method, true).should == "true"
+ end
+
+ it "converts a constant to a String" do
+ @object.send(@method, Object).should == "Object"
+ end
+
+ it "calls #to_s to convert an arbitrary object to a String" do
+ obj = mock('test')
+ obj.should_receive(:to_s).and_return("test")
+
+ @object.send(@method, obj).should == "test"
+ end
+
+ it "raises a TypeError if #to_s does not exist" do
+ obj = mock('to_s')
+ class << obj
+ undef_method :to_s
+ end
+
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+
+ # #5158
+ it "raises a TypeError if respond_to? returns false for #to_s" do
+ obj = mock("to_s")
+ class << obj
+ def respond_to?(meth, include_private=false)
+ meth == :to_s ? false : super
+ end
+ end
+
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_s is not defined, even though #respond_to?(:to_s) returns true" do
+ # cannot use a mock because of how RSpec affects #method_missing
+ obj = Object.new
+ class << obj
+ undef_method :to_s
+ def respond_to?(meth, include_private=false)
+ meth == :to_s ? true : super
+ end
+ end
+
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+
+ it "calls #to_s if #respond_to?(:to_s) returns true" do
+ obj = mock('to_s')
+ class << obj
+ undef_method :to_s
+ def method_missing(meth, *args)
+ meth == :to_s ? "test" : super
+ end
+ end
+
+ @object.send(@method, obj).should == "test"
+ end
+
+ it "raises a TypeError if #to_s does not return a String" do
+ (obj = mock('123')).should_receive(:to_s).and_return(123)
+ -> { @object.send(@method, obj) }.should.raise(TypeError)
+ end
+
+ it "returns the same object if it is already a String" do
+ string = +"Hello"
+ string.should_not_receive(:to_s)
+ string2 = @object.send(@method, string)
+ string.should.equal?(string2)
+ end
+
+ it "returns the same object if it is an instance of a String subclass" do
+ subklass = Class.new(String)
+ string = subklass.new("Hello")
+ string.should_not_receive(:to_s)
+ string2 = @object.send(@method, string)
+ string.should.equal?(string2)
+ end
+end
+
+describe "Kernel.String" do
+ it_behaves_like :kernel_String, :String, Kernel
+end
+
+describe "Kernel#String" do
+ it_behaves_like :kernel_String, :String, Object.new
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:String)
+ end
+end
diff --git a/spec/ruby/core/kernel/__callee___spec.rb b/spec/ruby/core/kernel/__callee___spec.rb
new file mode 100644
index 0000000000..3059ea8b57
--- /dev/null
+++ b/spec/ruby/core/kernel/__callee___spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/__callee__'
+
+describe "Kernel.__callee__" do
+ it "returns the current method, even when aliased" do
+ KernelSpecs::CalleeTest.new.f.should == :f
+ end
+
+ it "returns the aliased name when aliased method" do
+ KernelSpecs::CalleeTest.new.g.should == :g
+ end
+
+ it "returns the caller from blocks too" do
+ KernelSpecs::CalleeTest.new.in_block.should == [:in_block, :in_block]
+ end
+
+ it "returns the caller from define_method too" do
+ KernelSpecs::CalleeTest.new.dm.should == :dm
+ end
+
+ it "returns the caller from block inside define_method too" do
+ KernelSpecs::CalleeTest.new.dm_block.should == [:dm_block, :dm_block]
+ end
+
+ it "returns method name even from send" do
+ KernelSpecs::CalleeTest.new.from_send.should == :from_send
+ end
+
+ it "returns method name even from eval" do
+ KernelSpecs::CalleeTest.new.from_eval.should == :from_eval
+ end
+
+ it "returns nil from inside a class body" do
+ KernelSpecs::CalleeTest.new.from_class_body.should == nil
+ end
+
+ it "returns nil when not called from a method" do
+ __callee__.should == nil
+ end
+
+ it "returns the caller from a define_method called from the same class" do
+ c = Class.new do
+ define_method(:f) { 1.times{ break __callee__ } }
+ def g; f end
+ end
+ c.new.g.should == :f
+ end
+end
diff --git a/spec/ruby/core/kernel/__dir___spec.rb b/spec/ruby/core/kernel/__dir___spec.rb
new file mode 100644
index 0000000000..242adbf48b
--- /dev/null
+++ b/spec/ruby/core/kernel/__dir___spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#__dir__" do
+ it "returns the real name of the directory containing the currently-executing file" do
+ __dir__.should == File.realpath(File.dirname(__FILE__))
+ end
+
+ it "returns the expanded path of the directory when used in the main script" do
+ fixtures_dir = File.dirname(fixture(__FILE__, '__dir__.rb'))
+ Dir.chdir(fixtures_dir) do
+ ruby_exe("__dir__.rb").should == "__dir__.rb\n#{fixtures_dir}\n"
+ end
+ end
+
+ context "when used in eval with a given filename" do
+ it "returns File.dirname(filename)" do
+ eval("__dir__", nil, "foo.rb").should == "."
+ eval("__dir__", nil, "foo/bar.rb").should == "foo"
+ end
+ end
+
+ context "when used in eval with top level binding" do
+ it "returns nil" do
+ eval("__dir__", binding).should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/__method___spec.rb b/spec/ruby/core/kernel/__method___spec.rb
new file mode 100644
index 0000000000..578d25640d
--- /dev/null
+++ b/spec/ruby/core/kernel/__method___spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/__method__'
+
+describe "Kernel.__method__" do
+ it "returns the current method, even when aliased" do
+ KernelSpecs::MethodTest.new.f.should == :f
+ end
+
+ it "returns the original name when aliased method" do
+ KernelSpecs::MethodTest.new.g.should == :f
+ end
+
+ it "returns the caller from blocks too" do
+ KernelSpecs::MethodTest.new.in_block.should == [:in_block, :in_block]
+ end
+
+ it "returns the caller from define_method too" do
+ KernelSpecs::MethodTest.new.dm.should == :dm
+ end
+
+ it "returns the caller from block inside define_method too" do
+ KernelSpecs::MethodTest.new.dm_block.should == [:dm_block, :dm_block]
+ end
+
+ it "returns method name even from send" do
+ KernelSpecs::MethodTest.new.from_send.should == :from_send
+ end
+
+ it "returns method name even from eval" do
+ KernelSpecs::MethodTest.new.from_eval.should == :from_eval
+ end
+
+ it "returns nil from inside a class body" do
+ KernelSpecs::MethodTest.new.from_class_body.should == nil
+ end
+
+ it "returns nil when not called from a method" do
+ __method__.should == nil
+ end
+end
diff --git a/spec/ruby/core/kernel/abort_spec.rb b/spec/ruby/core/kernel/abort_spec.rb
new file mode 100644
index 0000000000..b75138182d
--- /dev/null
+++ b/spec/ruby/core/kernel/abort_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/process/abort'
+
+describe "Kernel#abort" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:abort)
+ end
+
+ it_behaves_like :process_abort, :abort, KernelSpecs::Method.new
+end
+
+describe "Kernel.abort" do
+ it_behaves_like :process_abort, :abort, Kernel
+end
diff --git a/spec/ruby/core/kernel/at_exit_spec.rb b/spec/ruby/core/kernel/at_exit_spec.rb
new file mode 100644
index 0000000000..fdb0eaf34c
--- /dev/null
+++ b/spec/ruby/core/kernel/at_exit_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/kernel/at_exit'
+
+describe "Kernel.at_exit" do
+ it_behaves_like :kernel_at_exit, :at_exit
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:at_exit)
+ end
+
+ it "raises ArgumentError if called without a block" do
+ -> { at_exit }.should.raise(ArgumentError, "called without a block")
+ end
+end
+
+describe "Kernel#at_exit" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/autoload_relative_spec.rb b/spec/ruby/core/kernel/autoload_relative_spec.rb
new file mode 100644
index 0000000000..7d03da8a40
--- /dev/null
+++ b/spec/ruby/core/kernel/autoload_relative_spec.rb
@@ -0,0 +1,114 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Specs for Kernel#autoload_relative
+
+ruby_version_is "4.1" do
+ describe "Kernel#autoload_relative" do
+ before :each do
+ @loaded_features = $".dup
+ end
+
+ after :each do
+ $".replace @loaded_features
+ # Clean up constants defined by these tests
+ [:KSAutoloadRelativeA, :KSAutoloadRelativeB, :KSAutoloadRelativeC,
+ :KSAutoloadRelativeE, :KSAutoloadRelativeF, :KSAutoloadRelativeG,
+ :KSAutoloadRelativeH, :KSAutoloadRelativeI].each do |const|
+ KernelSpecs.send(:remove_const, const) if KernelSpecs.const_defined?(const, false)
+ end
+ [:KSAutoloadRelativeD, :NestedTest].each do |const|
+ Object.send(:remove_const, const) if Object.const_defined?(const, false)
+ end
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:autoload_relative)
+ end
+
+ it "registers a file to load relative to the current file" do
+ KernelSpecs.autoload_relative :KSAutoloadRelativeA, "fixtures/autoload_relative_b.rb"
+ path = KernelSpecs.autoload?(:KSAutoloadRelativeA)
+ path.should_not == nil
+ path.should.end_with?("autoload_relative_b.rb")
+ File.exist?(path).should == true
+ end
+
+ it "loads the file when the constant is accessed" do
+ KernelSpecs.autoload_relative :KSAutoloadRelativeB, "fixtures/autoload_relative_b.rb"
+ KernelSpecs::KSAutoloadRelativeB.loaded.should == :ksautoload_b
+ end
+
+ it "sets the autoload constant in the constant table" do
+ KernelSpecs.autoload_relative :KSAutoloadRelativeC, "fixtures/autoload_relative_b.rb"
+ KernelSpecs.should.const_defined?(:KSAutoloadRelativeC, false)
+ end
+
+ it "can autoload in instance_eval with a file context" do
+ result = Object.new.instance_eval(<<-CODE, __FILE__, __LINE__)
+ autoload_relative :KSAutoloadRelativeD, "fixtures/autoload_relative_d.rb"
+ KSAutoloadRelativeD.loaded
+ CODE
+ result.should == :ksautoload_d
+ end
+
+ it "raises LoadError if called from eval without file context" do
+ -> {
+ eval('autoload_relative :Foo, "foo.rb"')
+ }.should.raise(LoadError, /autoload_relative called without file context/)
+ end
+
+ it "accepts both string and symbol for constant name" do
+ KernelSpecs.autoload_relative :KSAutoloadRelativeE, "fixtures/autoload_relative_b.rb"
+ KernelSpecs.autoload_relative "KSAutoloadRelativeF", "fixtures/autoload_relative_b.rb"
+
+ KernelSpecs.should.const_defined?(:KSAutoloadRelativeE, false)
+ KernelSpecs.should.const_defined?(:KSAutoloadRelativeF, false)
+ end
+
+ it "returns nil" do
+ KernelSpecs.autoload_relative(:KSAutoloadRelativeG, "fixtures/autoload_relative_b.rb").should == nil
+ end
+
+ it "resolves nested directory paths correctly" do
+ -> {
+ autoload_relative :NestedTest, "../kernel/fixtures/autoload_relative_b.rb"
+ autoload?(:NestedTest)
+ }.should_not.raise
+ end
+
+ it "resolves paths starting with ./" do
+ KernelSpecs.autoload_relative :KSAutoloadRelativeH, "./fixtures/autoload_relative_b.rb"
+ path = KernelSpecs.autoload?(:KSAutoloadRelativeH)
+ path.should_not == nil
+ path.should.end_with?("autoload_relative_b.rb")
+ end
+
+ it "ignores $LOAD_PATH and uses only relative path resolution" do
+ original_load_path = $LOAD_PATH.dup
+ $LOAD_PATH.clear
+ begin
+ KernelSpecs.autoload_relative :KSAutoloadRelativeI, "fixtures/autoload_relative_b.rb"
+ path = KernelSpecs.autoload?(:KSAutoloadRelativeI)
+ path.should_not == nil
+ # Should still resolve even with empty $LOAD_PATH
+ File.exist?(path).should == true
+ ensure
+ $LOAD_PATH.replace(original_load_path)
+ end
+ end
+
+ describe "when Object is frozen" do
+ it "raises a FrozenError before defining the constant" do
+ ruby_exe(<<-RUBY).should.include?("FrozenError")
+ Object.freeze
+ begin
+ autoload_relative :Foo, "autoload_b.rb"
+ rescue => e
+ puts e.class
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/autoload_spec.rb b/spec/ruby/core/kernel/autoload_spec.rb
new file mode 100644
index 0000000000..46783734c4
--- /dev/null
+++ b/spec/ruby/core/kernel/autoload_spec.rb
@@ -0,0 +1,178 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# These specs only illustrate the basic autoload cases
+# and where toplevel autoload behaves differently from
+# Module#autoload. See those specs for more examples.
+
+autoload :KSAutoloadA, "autoload_a.rb"
+autoload :KSAutoloadB, fixture(__FILE__, "autoload_b.rb")
+define_autoload_KSAutoloadCallsRequire = -> {
+ autoload :KSAutoloadCallsRequire, "main_autoload_not_exist.rb"
+}
+
+def check_autoload(const)
+ autoload? const
+end
+
+describe "Kernel#autoload" do
+ before :each do
+ @loaded_features = $".dup
+ end
+
+ after :each do
+ $".replace @loaded_features
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:autoload)
+ end
+
+ it "registers a file to load the first time the named constant is accessed" do
+ Object.autoload?(:KSAutoloadA).should == "autoload_a.rb"
+ end
+
+ it "registers a file to load the first time the named constant is accessed" do
+ check_autoload(:KSAutoloadA).should == "autoload_a.rb"
+ end
+
+ it "sets the autoload constant in Object's constant table" do
+ Object.should.const_defined?(:KSAutoloadA, false)
+ end
+
+ it "loads the file when the constant is accessed" do
+ KSAutoloadB.loaded.should == :ksautoload_b
+ end
+
+ it "calls main.require(path) to load the file" do
+ define_autoload_KSAutoloadCallsRequire.call
+ main = TOPLEVEL_BINDING.eval("self")
+ main.should_receive(:require).with("main_autoload_not_exist.rb")
+ # The constant won't be defined since require is mocked to do nothing
+ -> { KSAutoloadCallsRequire }.should.raise(NameError)
+ end
+
+ it "can autoload in instance_eval" do
+ Object.new.instance_eval do
+ autoload :KSAutoloadD, fixture(__FILE__, "autoload_d.rb")
+ KSAutoloadD.loaded.should == :ksautoload_d
+ end
+ end
+
+ describe "inside a Class.new method body" do
+ # NOTE: this spec is being discussed in https://github.com/ruby/spec/pull/839
+ it "should define on the new anonymous class" do
+ cls = Class.new do
+ def go
+ autoload :Object, 'bogus'
+ autoload? :Object
+ end
+ end
+
+ cls.new.go.should == 'bogus'
+ cls.autoload?(:Object).should == 'bogus'
+ end
+ end
+
+ describe "when Object is frozen" do
+ it "raises a FrozenError before defining the constant" do
+ ruby_exe(fixture(__FILE__, "autoload_frozen.rb")).should == "FrozenError - nil"
+ end
+ end
+
+ describe "when called from included module's method" do
+ before :all do
+ @path = fixture(__FILE__, "autoload_from_included_module.rb")
+ KernelSpecs::AutoloadMethodIncluder.new.setup_autoload(@path)
+ end
+
+ it "setups the autoload on the included module" do
+ KernelSpecs::AutoloadMethod.autoload?(:AutoloadFromIncludedModule).should == @path
+ end
+
+ it "the autoload is reachable from the class too" do
+ KernelSpecs::AutoloadMethodIncluder.autoload?(:AutoloadFromIncludedModule).should == @path
+ end
+
+ it "the autoload relative to the included module works" do
+ KernelSpecs::AutoloadMethod::AutoloadFromIncludedModule.loaded.should == :autoload_from_included_module
+ end
+ end
+end
+
+describe "Kernel#autoload?" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:autoload?)
+ end
+
+ it "returns the name of the file that will be autoloaded" do
+ check_autoload(:KSAutoloadA).should == "autoload_a.rb"
+ end
+
+ it "returns nil if no file has been registered for a constant" do
+ check_autoload(:Manualload).should == nil
+ end
+end
+
+Kernel.autoload :KSAutoloadBB, "no_autoload.rb"
+
+describe "Kernel.autoload" do
+ before :all do
+ @non_existent = fixture __FILE__, "no_autoload.rb"
+ end
+
+ before :each do
+ @loaded_features = $".dup
+
+ ScratchPad.clear
+ end
+
+ after :each do
+ $".replace @loaded_features
+ end
+
+ it "registers a file to load the first time the toplevel constant is accessed" do
+ Kernel.autoload :KSAutoloadAA, @non_existent
+ Kernel.autoload?(:KSAutoloadAA).should == @non_existent
+ end
+
+ it "sets the autoload constant in Object's constant table" do
+ Object.should.const_defined?(:KSAutoloadBB, false)
+ end
+
+ it "calls #to_path on non-String filenames" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return @non_existent
+ Kernel.autoload :KSAutoloadAA, p
+ end
+
+ describe "when called from included module's method" do
+ before :all do
+ @path = fixture(__FILE__, "autoload_from_included_module2.rb")
+ KernelSpecs::AutoloadMethodIncluder2.new.setup_autoload(@path)
+ end
+
+ it "setups the autoload on the included module" do
+ KernelSpecs::AutoloadMethod2.autoload?(:AutoloadFromIncludedModule2).should == @path
+ end
+
+ it "the autoload is reachable from the class too" do
+ KernelSpecs::AutoloadMethodIncluder2.autoload?(:AutoloadFromIncludedModule2).should == @path
+ end
+
+ it "the autoload relative to the included module works" do
+ KernelSpecs::AutoloadMethod2::AutoloadFromIncludedModule2.loaded.should == :autoload_from_included_module2
+ end
+ end
+end
+
+describe "Kernel.autoload?" do
+ it "returns the name of the file that will be autoloaded" do
+ Kernel.autoload :KSAutoload, "autoload.rb"
+ Kernel.autoload?(:KSAutoload).should == "autoload.rb"
+ end
+
+ it "returns nil if no file has been registered for a constant" do
+ Kernel.autoload?(:Manualload).should == nil
+ end
+end
diff --git a/spec/ruby/core/kernel/backtick_spec.rb b/spec/ruby/core/kernel/backtick_spec.rb
new file mode 100644
index 0000000000..42e0f975f5
--- /dev/null
+++ b/spec/ruby/core/kernel/backtick_spec.rb
@@ -0,0 +1,84 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#`" do
+ before :each do
+ @original_external = Encoding.default_external
+ end
+
+ after :each do
+ Encoding.default_external = @original_external
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:`)
+ end
+
+ it "returns the standard output of the executed sub-process" do
+ ip = 'world'
+ `echo disc #{ip}`.should == "disc world\n"
+ end
+
+ it "lets the standard error stream pass through to the inherited stderr" do
+ cmd = ruby_cmd('STDERR.print "error stream"')
+ -> {
+ `#{cmd}`.should == ""
+ }.should output_to_fd("error stream", STDERR)
+ end
+
+ it "produces a String in the default external encoding" do
+ Encoding.default_external = Encoding::SHIFT_JIS
+ `echo disc`.encoding.should.equal?(Encoding::SHIFT_JIS)
+ end
+
+ it "raises an Errno::ENOENT if the command is not executable" do
+ -> { `nonexistent_command` }.should.raise(Errno::ENOENT)
+ end
+
+ platform_is_not :windows do
+ it "handles invalid UTF-8 bytes in command" do
+ `echo "testing\xC2 a non UTF-8 string"`.b.should == "testing\xC2 a non UTF-8 string\n".b
+ end
+
+ it "sets $? to the exit status of the executed sub-process" do
+ ip = 'world'
+ `echo disc #{ip}`
+ $?.should.is_a?(Process::Status)
+ $?.should_not.stopped?
+ $?.should.exited?
+ $?.exitstatus.should == 0
+ $?.should.success?
+ `echo disc #{ip}; exit 99`
+ $?.should.is_a?(Process::Status)
+ $?.should_not.stopped?
+ $?.should.exited?
+ $?.exitstatus.should == 99
+ $?.should_not.success?
+ end
+ end
+
+ platform_is :windows do
+ it "sets $? to the exit status of the executed sub-process" do
+ ip = 'world'
+ `echo disc #{ip}`
+ $?.should.is_a?(Process::Status)
+ $?.should_not.stopped?
+ $?.should.exited?
+ $?.exitstatus.should == 0
+ $?.should.success?
+ `echo disc #{ip}& exit 99`
+ $?.should.is_a?(Process::Status)
+ $?.should_not.stopped?
+ $?.should.exited?
+ $?.exitstatus.should == 99
+ $?.should_not.success?
+ end
+ end
+end
+
+describe "Kernel.`" do
+ it "tries to convert the given argument to String using #to_str" do
+ (obj = mock('echo test')).should_receive(:to_str).and_return("echo test")
+ Kernel.`(obj).should == "test\n" #` fix vim syntax highlighting
+ end
+end
diff --git a/spec/ruby/core/kernel/binding_spec.rb b/spec/ruby/core/kernel/binding_spec.rb
new file mode 100644
index 0000000000..6f27b26f37
--- /dev/null
+++ b/spec/ruby/core/kernel/binding_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel.binding" do
+ it "returns a binding for the caller" do
+ Kernel.binding.eval("self").should == self
+ end
+end
+
+describe "Kernel#binding" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:binding)
+ end
+
+ before :each do
+ @b1 = KernelSpecs::Binding.new(99).get_binding
+ ScratchPad.clear
+ end
+
+ it "returns a Binding object" do
+ @b1.kind_of?(Binding).should == true
+ end
+
+ it "encapsulates the execution context properly" do
+ eval("@secret", @b1).should == 100
+ eval("a", @b1).should == true
+ eval("b", @b1).should == true
+ eval("@@super_secret", @b1).should == "password"
+
+ eval("square(2)", @b1).should == 4
+ eval("self.square(2)", @b1).should == 4
+
+ eval("a = false", @b1)
+ eval("a", @b1).should == false
+ end
+
+ it "raises a NameError on undefined variable" do
+ -> { eval("a_fake_variable", @b1) }.should.raise(NameError)
+ end
+
+ it "uses the closure's self as self in the binding" do
+ m = mock(:whatever)
+ eval('self', m.send(:binding)).should == self
+ end
+
+ it "uses the class as self in a Class.new block" do
+ m = mock(:whatever)
+ cls = Class.new { ScratchPad.record eval('self', m.send(:binding)) }
+ ScratchPad.recorded.should == cls
+ end
+end
diff --git a/spec/ruby/core/kernel/block_given_spec.rb b/spec/ruby/core/kernel/block_given_spec.rb
new file mode 100644
index 0000000000..20bb90ae96
--- /dev/null
+++ b/spec/ruby/core/kernel/block_given_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_block_given, shared: true do
+ it "returns true if and only if a block is supplied" do
+ @object.accept_block {}.should == true
+ @object.accept_block_as_argument {}.should == true
+ @object.accept_block_inside_block {}.should == true
+ @object.accept_block_as_argument_inside_block {}.should == true
+
+ @object.accept_block.should == false
+ @object.accept_block_as_argument.should == false
+ @object.accept_block_inside_block.should == false
+ @object.accept_block_as_argument_inside_block.should == false
+ end
+
+ # Clarify: Based on http://www.ruby-forum.com/topic/137822 it appears
+ # that Matz wanted this to be true in 1.9.
+ it "returns false when a method defined by define_method is called with a block" do
+ @object.defined_block {}.should == false
+ @object.defined_block_inside_block {}.should == false
+ end
+end
+
+describe "Kernel#block_given?" do
+ it_behaves_like :kernel_block_given, :block_given?, KernelSpecs::BlockGiven
+
+ it "returns false outside of a method" do
+ block_given?.should == false
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:block_given?)
+ end
+end
+
+describe "Kernel.block_given?" do
+ it_behaves_like :kernel_block_given, :block_given?, KernelSpecs::KernelBlockGiven
+end
+
+describe "self.send(:block_given?)" do
+ it_behaves_like :kernel_block_given, :block_given?, KernelSpecs::SelfBlockGiven
+end
diff --git a/spec/ruby/core/kernel/caller_locations_spec.rb b/spec/ruby/core/kernel/caller_locations_spec.rb
new file mode 100644
index 0000000000..04cf806ef4
--- /dev/null
+++ b/spec/ruby/core/kernel/caller_locations_spec.rb
@@ -0,0 +1,113 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/caller_locations'
+
+describe 'Kernel#caller_locations' do
+ it 'is a private method' do
+ Kernel.private_instance_methods(false).should.include?(:caller_locations)
+ end
+
+ it 'returns an Array of caller locations' do
+ KernelSpecs::CallerLocationsTest.locations.should_not.empty?
+ end
+
+ it 'returns an Array of caller locations using a custom offset' do
+ locations = KernelSpecs::CallerLocationsTest.locations(2)
+
+ locations[0].absolute_path.should.end_with?('mspec.rb')
+ end
+
+ it 'returns an Array of caller locations using a custom limit' do
+ locations = KernelSpecs::CallerLocationsTest.locations(1, 1)
+
+ locations.length.should == 1
+ end
+
+ it "can be called with a range" do
+ locations1 = caller_locations(0)
+ locations2 = caller_locations(2..4)
+ locations1[2..4].map(&:to_s).should == locations2.map(&:to_s)
+ end
+
+ it "works with endless ranges" do
+ locations1 = caller_locations(0)
+ locations2 = caller_locations(eval("(2..)"))
+ locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s)
+ end
+
+ it "works with beginless ranges" do
+ locations1 = caller_locations(0)
+ locations2 = caller_locations((...5))
+ locations2.map(&:to_s)[eval("(2..)")].should == locations1[(...5)].map(&:to_s)[eval("(2..)")]
+ end
+
+ it "can be called with a range whose end is negative" do
+ locations1 = caller_locations(0)
+ locations2 = caller_locations(2..-1)
+ locations3 = caller_locations(2..-2)
+ locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s)
+ locations1[2..-2].map(&:to_s).should == locations3.map(&:to_s)
+ end
+
+ it "must return nil if omitting more locations than available" do
+ caller_locations(100).should == nil
+ caller_locations(100..-1).should == nil
+ end
+
+ it "must return [] if omitting exactly the number of locations available" do
+ omit = caller_locations(0).length
+ caller_locations(omit).should == []
+ end
+
+ it 'returns the locations as Thread::Backtrace::Location instances' do
+ locations = KernelSpecs::CallerLocationsTest.locations
+
+ locations.each do |location|
+ location.kind_of?(Thread::Backtrace::Location).should == true
+ end
+ end
+
+ it "must return the same locations when called with 1..-1 and when called with no arguments" do
+ caller_locations.map(&:to_s).should == caller_locations(1..-1).map(&:to_s)
+ end
+
+ guard -> { Kernel.instance_method(:tap).source_location } do
+ ruby_version_is ""..."3.4" do
+ it "includes core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ loc = nil
+ tap { loc = caller_locations(1, 1)[0] }
+ loc.label.should == "tap"
+ loc.path.should.start_with? "<internal:"
+ end
+ end
+
+ ruby_version_is "3.4"..."4.0" do
+ it "includes core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ loc = nil
+ tap { loc = caller_locations(1, 1)[0] }
+ loc.label.should == "Kernel#tap"
+ loc.path.should.start_with? "<internal:"
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "does not include core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ loc = nil
+ tap { loc = caller_locations(1, 1)[0] }
+ loc.label.should == "Kernel#tap"
+ # CRuby hides the file which defines the method: https://bugs.ruby-lang.org/issues/20968
+ unless loc.path == __FILE__
+ loc.path.should.start_with? "<internal:"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/caller_spec.rb b/spec/ruby/core/kernel/caller_spec.rb
new file mode 100644
index 0000000000..225f6fc458
--- /dev/null
+++ b/spec/ruby/core/kernel/caller_spec.rb
@@ -0,0 +1,121 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/caller'
+
+describe 'Kernel#caller' do
+ it 'is a private method' do
+ Kernel.private_instance_methods(false).should.include?(:caller)
+ end
+
+ it 'returns an Array of caller locations' do
+ KernelSpecs::CallerTest.locations.should_not.empty?
+ end
+
+ it 'returns an Array of caller locations using a custom offset' do
+ locations = KernelSpecs::CallerTest.locations(2)
+
+ locations[0].should =~ %r{runner/mspec.rb}
+ end
+
+ it 'returns an Array of caller locations using a custom limit' do
+ locations = KernelSpecs::CallerTest.locations(1, 1)
+
+ locations.length.should == 1
+ end
+
+ it 'returns an Array of caller locations using a range' do
+ locations = KernelSpecs::CallerTest.locations(1..1)
+
+ locations.length.should == 1
+ end
+
+ it 'returns the locations as String instances' do
+ locations = KernelSpecs::CallerTest.locations
+ line = __LINE__ - 1
+
+ locations[0].should.include?("#{__FILE__}:#{line}:in")
+ end
+
+ it "returns an Array with the block given to #at_exit at the base of the stack" do
+ path = fixture(__FILE__, "caller_at_exit.rb")
+ lines = ruby_exe(path).lines
+ lines.size.should == 2
+ lines[0].should =~ /\A#{path}:6:in [`'](?:Object#)?foo'\n\z/
+ lines[1].should =~ /\A#{path}:2:in [`']block in <main>'\n\z/
+ end
+
+ it "can be called with a range" do
+ locations1 = caller(0)
+ locations2 = caller(2..4)
+ locations1[2..4].should == locations2
+ end
+
+ it "works with endless ranges" do
+ locations1 = KernelSpecs::CallerTest.locations(0)
+ locations2 = KernelSpecs::CallerTest.locations(eval("(2..)"))
+ locations2.should == locations1[2..-1]
+ end
+
+ it "works with beginless ranges" do
+ locations1 = KernelSpecs::CallerTest.locations(0)
+ locations2 = KernelSpecs::CallerTest.locations((..5))
+ locations2[eval("(2..)")].should == locations1[(..5)][eval("(2..)")]
+ end
+
+ it "can be called with a range whose end is negative" do
+ locations1 = caller(0)
+ locations2 = caller(2..-1)
+ locations3 = caller(2..-2)
+ locations1[2..-1].should == locations2
+ locations1[2..-2].should == locations3
+ end
+
+ it "must return nil if omitting more locations than available" do
+ caller(100).should == nil
+ caller(100..-1).should == nil
+ end
+
+ it "must return [] if omitting exactly the number of locations available" do
+ omit = caller(0).length
+ caller(omit).should == []
+ end
+
+ it "must return the same locations when called with 1..-1 and when called with no arguments" do
+ caller.should == caller(1..-1)
+ end
+
+ guard -> { Kernel.instance_method(:tap).source_location } do
+ ruby_version_is ""..."3.4" do
+ it "includes core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ loc = nil
+ tap { loc = caller(1, 1)[0] }
+ loc.should =~ /\A<internal:.*in `tap'\z/
+ end
+ end
+
+ ruby_version_is "3.4"..."4.0" do
+ it "includes core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ loc = nil
+ tap { loc = caller(1, 1)[0] }
+ loc.should =~ /\A<internal:.*in 'Kernel#tap'\z/
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "does not include core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ loc = nil
+ tap { loc = caller(1, 1)[0] }
+ # CRuby hides the file which defines the method: https://bugs.ruby-lang.org/issues/20968
+ loc.should =~ /\A(<internal:|#{__FILE__}:).*in 'Kernel#tap'\z/
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/case_compare_spec.rb b/spec/ruby/core/kernel/case_compare_spec.rb
new file mode 100644
index 0000000000..c74bbf63b9
--- /dev/null
+++ b/spec/ruby/core/kernel/case_compare_spec.rb
@@ -0,0 +1,135 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+
+module Specs
+ module Kernel
+
+ class HasNone
+ end
+
+ class HasOpEqual
+ def ==(other)
+ other.kind_of? HasOpEqual
+ end
+ end
+
+ class HasEqual
+ def equal?(other)
+ false
+ end
+ end
+
+ class HasOppoOpEqual
+ def ==(other)
+ false
+ end
+
+ def equal?(other)
+ false
+ end
+ end
+ end
+end
+
+
+describe "Kernel#=== for a class with default #== and #equal?" do
+ before :each do
+ @o1 = Specs::Kernel::HasNone.new
+ @o2 = @o1.dup
+ end
+
+ it "returns true if other object has same object id" do
+ @o1.object_id.should == @o1.object_id
+ (@o1 === @o1).should == true
+ end
+
+ it "returns false if other object does not have same object id" do
+ @o1.object_id.should_not == @o2.object_id
+ (@o1 === @o2).should == false
+ end
+end
+
+describe "Kernel#=== for a class with #== overridden to consider other object's class" do
+ before :each do
+ @o = Object.new
+ @o1 = Specs::Kernel::HasOpEqual.new
+ @o2 = @o1.dup
+ end
+
+ it "returns true if #== returns true even if #equal? is false" do
+ @o1.should_not.equal?(@o2)
+ (@o1 == @o2).should == true
+ (@o1 === @o2).should == true
+ end
+
+ it "returns true if #equal? returns true" do
+ @o1.should.equal?(@o1)
+ (@o1 === @o1).should == true
+ end
+
+ it "returns false if neither #== nor #equal? returns true" do
+ @o1.should_not.equal?(@o)
+ (@o1 == @o).should == false
+ (@o1 === @o).should == false
+ end
+end
+
+describe "Kernel#=== for a class with #equal? overridden to always be false" do
+ before :each do
+ @o = Object.new
+ @o1 = Specs::Kernel::HasEqual.new
+ @o2 = @o1.dup
+ end
+
+ it "returns true if #== returns true even if #equal? is false" do
+ @o1.should_not.equal?(@o1)
+ (@o1 == @o1).should == true
+ (@o1 === @o1).should == true
+ end
+
+ it "returns false if neither #== nor #equal? returns true" do
+ @o1.should_not.equal?(@o)
+ (@o1 == @o).should == false
+ (@o1 === @o).should == false
+ end
+end
+
+describe "Kernel#=== for a class with #== and #equal? overridden to always be false" do
+ before :each do
+ @o = Object.new
+ @o1 = Specs::Kernel::HasOppoOpEqual.new
+ @o2 = @o1.dup
+ end
+
+ it "returns true if the object id is the same even if both #== and #equal? return false" do
+ @o1.object_id.should == @o1.object_id
+
+ @o1.should_not.equal?(@o1)
+ (@o1 == @o1).should == false
+
+ (@o1 === @o1).should == true
+ end
+
+ it "returns false if the object id is not the same and both #== and #equal? return false" do
+ @o1.object_id.should_not == @o2.object_id
+
+ @o1.should_not.equal?(@o2)
+ (@o1 == @o2).should == false
+
+ (@o1 === @o2).should == false
+ end
+end
+
+describe "Kernel#=== does not call #object_id nor #equal?" do
+ before :each do
+ @o1 = Object.new
+ @o1.should_not_receive(:object_id)
+ @o1.should_not_receive(:equal?)
+ end
+
+ it "but still returns true for #== or #=== on the same object" do
+ (@o1 == @o1).should == true
+ (@o1 === @o1).should == true
+ end
+end
diff --git a/spec/ruby/core/kernel/catch_spec.rb b/spec/ruby/core/kernel/catch_spec.rb
new file mode 100644
index 0000000000..9f02303678
--- /dev/null
+++ b/spec/ruby/core/kernel/catch_spec.rb
@@ -0,0 +1,127 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel.catch" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "executes its block and catches a thrown value matching its argument" do
+ catch :thrown_key do
+ ScratchPad.record :catch_block
+ throw :thrown_key
+ ScratchPad.record :throw_failed
+ end
+ ScratchPad.recorded.should == :catch_block
+ end
+
+ it "returns the second value passed to throw" do
+ catch(:thrown_key) { throw :thrown_key, :catch_value }.should == :catch_value
+ end
+
+ it "returns the last expression evaluated if throw was not called" do
+ catch(:thrown_key) { 1; :catch_block }.should == :catch_block
+ end
+
+ it "passes the given symbol to its block" do
+ catch :thrown_key do |tag|
+ ScratchPad.record tag
+ end
+ ScratchPad.recorded.should == :thrown_key
+ end
+
+ it "raises an ArgumentError if a Symbol is thrown for a String catch value" do
+ -> { catch("exit") { throw :exit } }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if a String with different identity is thrown" do
+ -> { catch("exit".dup) { throw "exit".dup } }.should.raise(ArgumentError)
+ end
+
+ it "catches a Symbol when thrown a matching Symbol" do
+ catch :thrown_key do
+ ScratchPad.record :catch_block
+ throw :thrown_key
+ end
+ ScratchPad.recorded.should == :catch_block
+ end
+
+ it "catches a String when thrown a String with the same identity" do
+ key = "thrown_key"
+ catch key do
+ ScratchPad.record :catch_block
+ throw key
+ end
+ ScratchPad.recorded.should == :catch_block
+ end
+
+ it "accepts an object as an argument" do
+ catch(Object.new) { :catch_block }.should == :catch_block
+ end
+
+ it "yields an object when called without arguments" do
+ catch { |tag| tag }.should.instance_of?(Object)
+ end
+
+ it "can be used even in a method different from where throw is called" do
+ class CatchSpecs
+ def self.throwing_method
+ throw :blah, :thrown_value
+ end
+ def self.catching_method
+ catch :blah do
+ throwing_method
+ end
+ end
+ end
+ CatchSpecs.catching_method.should == :thrown_value
+ end
+
+ describe "when nested" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "catches across invocation boundaries" do
+ catch :one do
+ ScratchPad << 1
+ catch :two do
+ ScratchPad << 2
+ catch :three do
+ ScratchPad << 3
+ throw :one
+ ScratchPad << 4
+ end
+ ScratchPad << 5
+ end
+ ScratchPad << 6
+ end
+
+ ScratchPad.recorded.should == [1, 2, 3]
+ end
+
+ it "catches in the nested invocation with the same key object" do
+ catch :thrown_key do
+ ScratchPad << 1
+ catch :thrown_key do
+ ScratchPad << 2
+ throw :thrown_key
+ ScratchPad << 3
+ end
+ ScratchPad << 4
+ end
+
+ ScratchPad.recorded.should == [1, 2, 4]
+ end
+ end
+
+ it "raises LocalJumpError if no block is given" do
+ -> { catch :blah }.should.raise(LocalJumpError)
+ end
+end
+
+describe "Kernel#catch" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:catch)
+ end
+end
diff --git a/spec/ruby/core/kernel/chomp_spec.rb b/spec/ruby/core/kernel/chomp_spec.rb
new file mode 100644
index 0000000000..434bf652f9
--- /dev/null
+++ b/spec/ruby/core/kernel/chomp_spec.rb
@@ -0,0 +1,65 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_chomp, shared: true do
+ it "removes the final newline of $_" do
+ KernelSpecs.chomp("abc\n", @method).should == "abc"
+ end
+
+ it "removes the final carriage return of $_" do
+ KernelSpecs.chomp("abc\r", @method).should == "abc"
+ end
+
+ it "removes the final carriage return, newline of $_" do
+ KernelSpecs.chomp("abc\r\n", @method).should == "abc"
+ end
+
+ it "removes only the final newline of $_" do
+ KernelSpecs.chomp("abc\n\n", @method).should == "abc\n"
+ end
+
+ it "removes the value of $/ from the end of $_" do
+ KernelSpecs.chomp("abcde", @method, "cde").should == "ab"
+ end
+end
+
+describe :kernel_chomp_private, shared: true do
+ it "is a private method" do
+ KernelSpecs.has_private_method(@method).should == true
+ end
+end
+
+describe "Kernel.chomp" do
+ it_behaves_like :kernel_chomp, "Kernel.chomp"
+end
+
+describe "Kernel#chomp" do
+ it_behaves_like :kernel_chomp, "chomp"
+
+ it_behaves_like :kernel_chomp_private, :chomp
+end
+
+describe :kernel_chomp_encoded, shared: true do
+ before :each do
+ @external = Encoding.default_external
+ Encoding.default_external = Encoding::UTF_8
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ end
+
+ it "removes the final carriage return, newline from a multi-byte $_" do
+ script = fixture __FILE__, "#{@method}.rb"
+ KernelSpecs.run_with_dash_n(script).should == "ã‚れ"
+ end
+end
+
+describe "Kernel.chomp" do
+ it_behaves_like :kernel_chomp_encoded, "chomp"
+end
+
+describe "Kernel#chomp" do
+ it_behaves_like :kernel_chomp_encoded, "chomp_f"
+end
diff --git a/spec/ruby/core/kernel/chop_spec.rb b/spec/ruby/core/kernel/chop_spec.rb
new file mode 100644
index 0000000000..bbf3c3f724
--- /dev/null
+++ b/spec/ruby/core/kernel/chop_spec.rb
@@ -0,0 +1,53 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_chop, shared: true do
+ it "removes the final character of $_" do
+ KernelSpecs.chop("abc", @method).should == "ab"
+ end
+
+ it "removes the final carriage return, newline of $_" do
+ KernelSpecs.chop("abc\r\n", @method).should == "abc"
+ end
+end
+
+describe :kernel_chop_private, shared: true do
+ it "is a private method" do
+ KernelSpecs.has_private_method(@method).should == true
+ end
+end
+
+describe "Kernel.chop" do
+ it_behaves_like :kernel_chop, "Kernel.chop"
+end
+
+describe "Kernel#chop" do
+ it_behaves_like :kernel_chop_private, :chop
+
+ it_behaves_like :kernel_chop, "chop"
+end
+
+describe :kernel_chop_encoded, shared: true do
+ before :each do
+ @external = Encoding.default_external
+ Encoding.default_external = Encoding::UTF_8
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ end
+
+ it "removes the final multi-byte character from $_" do
+ script = fixture __FILE__, "#{@method}.rb"
+ KernelSpecs.run_with_dash_n(script).should == "ã‚"
+ end
+end
+
+describe "Kernel.chop" do
+ it_behaves_like :kernel_chop_encoded, "chop"
+end
+
+describe "Kernel#chop" do
+ it_behaves_like :kernel_chop_encoded, "chop_f"
+end
diff --git a/spec/ruby/core/kernel/class_spec.rb b/spec/ruby/core/kernel/class_spec.rb
new file mode 100644
index 0000000000..0a7f774d14
--- /dev/null
+++ b/spec/ruby/core/kernel/class_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#class" do
+ it "returns the class of the object" do
+ Object.new.class.should.equal?(Object)
+
+ 1.class.should.equal?(Integer)
+ 3.14.class.should.equal?(Float)
+ :hello.class.should.equal?(Symbol)
+ "hello".class.should.equal?(String)
+ [1, 2].class.should.equal?(Array)
+ { 1 => 2 }.class.should.equal?(Hash)
+ end
+
+ it "returns Class for a class" do
+ BasicObject.class.should.equal?(Class)
+ String.class.should.equal?(Class)
+ end
+
+ it "returns the first non-singleton class" do
+ a = +"hello"
+ def a.my_singleton_method; end
+ a.class.should.equal?(String)
+ end
+end
diff --git a/spec/ruby/core/kernel/clone_spec.rb b/spec/ruby/core/kernel/clone_spec.rb
new file mode 100644
index 0000000000..4ddb23d6e6
--- /dev/null
+++ b/spec/ruby/core/kernel/clone_spec.rb
@@ -0,0 +1,177 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/dup_clone'
+
+describe "Kernel#clone" do
+ it_behaves_like :kernel_dup_clone, :clone
+
+ before :each do
+ ScratchPad.clear
+ @obj = KernelSpecs::Duplicate.new 1, :a
+ end
+
+ it "calls #initialize_copy on the new instance" do
+ clone = @obj.clone
+ ScratchPad.recorded.should_not == @obj.object_id
+ ScratchPad.recorded.should == clone.object_id
+ end
+
+ it "uses the internal allocator and does not call #allocate" do
+ klass = Class.new
+ instance = klass.new
+
+ def klass.allocate
+ raise "allocate should not be called"
+ end
+
+ clone = instance.clone
+ clone.class.should.equal? klass
+ end
+
+ describe "with no arguments" do
+ it "copies frozen state from the original" do
+ o2 = @obj.clone
+ o2.should_not.frozen?
+
+ @obj.freeze
+ o3 = @obj.clone
+ o3.should.frozen?
+ end
+
+ it 'copies frozen?' do
+ o = ''.freeze.clone
+ o.frozen?.should == true
+ end
+ end
+
+ describe "with freeze: nil" do
+ it "copies frozen state from the original, like #clone without arguments" do
+ o2 = @obj.clone(freeze: nil)
+ o2.should_not.frozen?
+
+ @obj.freeze
+ o3 = @obj.clone(freeze: nil)
+ o3.should.frozen?
+ end
+
+ it "copies frozen?" do
+ o = "".freeze.clone(freeze: nil)
+ o.frozen?.should == true
+ end
+ end
+
+ describe "with freeze: true" do
+ it 'makes a frozen copy if the original is frozen' do
+ @obj.freeze
+ @obj.clone(freeze: true).should.frozen?
+ end
+
+ it 'freezes the copy even if the original was not frozen' do
+ @obj.clone(freeze: true).should.frozen?
+ end
+
+ it "calls #initialize_clone with kwargs freeze: true" do
+ obj = KernelSpecs::CloneFreeze.new
+ obj.clone(freeze: true)
+ ScratchPad.recorded.should == [obj, { freeze: true }]
+ end
+
+ it "calls #initialize_clone with kwargs freeze: true even if #initialize_clone only takes a single argument" do
+ obj = KernelSpecs::Clone.new
+ -> { obj.clone(freeze: true) }.should.raise(ArgumentError, 'wrong number of arguments (given 2, expected 1)')
+ end
+ end
+
+ describe "with freeze: false" do
+ it 'does not freeze the copy if the original is frozen' do
+ @obj.freeze
+ @obj.clone(freeze: false).should_not.frozen?
+ end
+
+ it 'does not freeze the copy if the original is not frozen' do
+ @obj.clone(freeze: false).should_not.frozen?
+ end
+
+ it "calls #initialize_clone with kwargs freeze: false" do
+ obj = KernelSpecs::CloneFreeze.new
+ obj.clone(freeze: false)
+ ScratchPad.recorded.should == [obj, { freeze: false }]
+ end
+
+ it "calls #initialize_clone with kwargs freeze: false even if #initialize_clone only takes a single argument" do
+ obj = KernelSpecs::Clone.new
+ -> { obj.clone(freeze: false) }.should.raise(ArgumentError, 'wrong number of arguments (given 2, expected 1)')
+ end
+ end
+
+ describe "with freeze: anything else" do
+ it 'raises ArgumentError when passed not true/false/nil' do
+ -> { @obj.clone(freeze: 1) }.should.raise(ArgumentError, /unexpected value for freeze: Integer/)
+ -> { @obj.clone(freeze: "") }.should.raise(ArgumentError, /unexpected value for freeze: String/)
+ -> { @obj.clone(freeze: Object.new) }.should.raise(ArgumentError, /unexpected value for freeze: Object/)
+ end
+ end
+
+ it "copies instance variables" do
+ clone = @obj.clone
+ clone.one.should == 1
+ clone.two.should == :a
+ end
+
+ it "copies singleton methods" do
+ def @obj.special() :the_one end
+ clone = @obj.clone
+ clone.special.should == :the_one
+ end
+
+ it "copies modules included in the singleton class" do
+ class << @obj
+ include KernelSpecs::DuplicateM
+ end
+
+ clone = @obj.clone
+ clone.repr.should == "KernelSpecs::Duplicate"
+ end
+
+ it "copies constants defined in the singleton class" do
+ class << @obj
+ CLONE = :clone
+ end
+
+ clone = @obj.clone
+ class << clone
+ CLONE.should == :clone
+ end
+ end
+
+ it "replaces a singleton object's metaclass with a new copy with the same superclass" do
+ cls = Class.new do
+ def bar
+ ['a']
+ end
+ end
+
+ object = cls.new
+ object.define_singleton_method(:bar) do
+ ['b', *super()]
+ end
+ object.bar.should == ['b', 'a']
+
+ cloned = object.clone
+
+ cloned.singleton_methods.should == [:bar]
+
+ # bar should replace previous one
+ cloned.define_singleton_method(:bar) do
+ ['c', *super()]
+ end
+ cloned.bar.should == ['c', 'a']
+
+ # bar should be removed and call through to superclass
+ cloned.singleton_class.class_eval do
+ remove_method :bar
+ end
+
+ cloned.bar.should == ['a']
+ end
+end
diff --git a/spec/ruby/core/kernel/comparison_spec.rb b/spec/ruby/core/kernel/comparison_spec.rb
new file mode 100644
index 0000000000..48f17e1172
--- /dev/null
+++ b/spec/ruby/core/kernel/comparison_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#<=>" do
+ it "returns 0 if self" do
+ obj = Object.new
+ obj.<=>(obj).should == 0
+ end
+
+ it "returns 0 if self is == to the argument" do
+ obj = mock('has ==')
+ obj.should_receive(:==).and_return(true)
+ obj.<=>(Object.new).should == 0
+ end
+
+ it "returns nil if self is eql? but not == to the argument" do
+ obj = mock('has eql?')
+ obj.should_not_receive(:eql?)
+ obj.<=>(Object.new).should == nil
+ end
+
+ it "returns nil if self.==(arg) returns nil" do
+ obj = mock('wrong ==')
+ obj.should_receive(:==).and_return(nil)
+ obj.<=>(Object.new).should == nil
+ end
+
+ it "returns nil if self is not == to the argument" do
+ obj = Object.new
+ obj.<=>(3.14).should == nil
+ end
+end
diff --git a/spec/ruby/core/kernel/define_singleton_method_spec.rb b/spec/ruby/core/kernel/define_singleton_method_spec.rb
new file mode 100644
index 0000000000..ec48581db8
--- /dev/null
+++ b/spec/ruby/core/kernel/define_singleton_method_spec.rb
@@ -0,0 +1,120 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#define_singleton_method" do
+ describe "when given an UnboundMethod" do
+ class DefineSingletonMethodSpecClass
+ MY_CONST = 42
+ define_singleton_method(:another_test_method, self.method(:constants))
+ end
+
+ it "correctly calls the new method" do
+ klass = DefineSingletonMethodSpecClass
+ klass.another_test_method.should == klass.constants
+ end
+
+ it "adds the new method to the methods list" do
+ DefineSingletonMethodSpecClass.should.respond_to?(:another_test_method)
+ end
+
+ it "defines any Child class method from any Parent's class methods" do
+ um = KernelSpecs::Parent.method(:parent_class_method).unbind
+ KernelSpecs::Child.send :define_singleton_method, :child_class_method, um
+ KernelSpecs::Child.child_class_method.should == :foo
+ ->{KernelSpecs::Parent.child_class_method}.should.raise(NoMethodError)
+ end
+
+ it "will raise when attempting to define an object's singleton method from another object's singleton method" do
+ other = KernelSpecs::Parent.new
+ p = KernelSpecs::Parent.new
+ class << p
+ def singleton_method
+ :single
+ end
+ end
+ um = p.method(:singleton_method).unbind
+ ->{ other.send :define_singleton_method, :other_singleton_method, um }.should.raise(TypeError)
+ end
+
+ end
+
+ it "defines a new method with the given name and the given block as body in self" do
+ class DefineSingletonMethodSpecClass
+ define_singleton_method(:block_test1) { self }
+ define_singleton_method(:block_test2, &-> { self })
+ end
+
+ o = DefineSingletonMethodSpecClass
+ o.block_test1.should == o
+ o.block_test2.should == o
+ end
+
+ it "raises a TypeError when the given method is no Method/Proc" do
+ -> {
+ Class.new { define_singleton_method(:test, "self") }
+ }.should.raise(TypeError)
+
+ -> {
+ Class.new { define_singleton_method(:test, 1234) }
+ }.should.raise(TypeError)
+ end
+
+ it "defines a new singleton method for objects" do
+ obj = Object.new
+ obj.define_singleton_method(:test) { "world!" }
+ obj.test.should == "world!"
+ -> {
+ Object.new.test
+ }.should.raise(NoMethodError)
+ end
+
+ it "maintains the Proc's scope" do
+ class DefineMethodByProcClass
+ in_scope = true
+ method_proc = proc { in_scope }
+
+ define_singleton_method(:proc_test, &method_proc)
+ end
+
+ DefineMethodByProcClass.proc_test.should == true
+ end
+
+ it "raises an ArgumentError when no block is given" do
+ obj = Object.new
+ -> {
+ obj.define_singleton_method(:test)
+ }.should.raise(ArgumentError)
+ end
+
+ it "does not use the caller block when no block is given" do
+ o = Object.new
+ def o.define(name)
+ define_singleton_method(name)
+ end
+
+ -> {
+ o.define(:foo) { raise "not used" }
+ }.should.raise(ArgumentError)
+ end
+
+ it "always defines the method with public visibility" do
+ cls = Class.new
+ def cls.define(name, &block)
+ private
+ define_singleton_method(name, &block)
+ end
+
+ -> {
+ suppress_warning do
+ cls.define(:foo) { :ok }
+ end
+ cls.foo.should == :ok
+ }.should_not.raise(NoMethodError)
+ end
+
+ it "cannot define a singleton method with a frozen singleton class" do
+ o = Object.new
+ o.freeze
+ -> { o.define_singleton_method(:foo) { 1 } }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/kernel/display_spec.rb b/spec/ruby/core/kernel/display_spec.rb
new file mode 100644
index 0000000000..9d429a9fac
--- /dev/null
+++ b/spec/ruby/core/kernel/display_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#display" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/dup_spec.rb b/spec/ruby/core/kernel/dup_spec.rb
new file mode 100644
index 0000000000..99de5e3732
--- /dev/null
+++ b/spec/ruby/core/kernel/dup_spec.rb
@@ -0,0 +1,67 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/dup_clone'
+
+describe "Kernel#dup" do
+ it_behaves_like :kernel_dup_clone, :dup
+
+ before :each do
+ ScratchPad.clear
+ @obj = KernelSpecs::Duplicate.new 1, :a
+ end
+
+ it "calls #initialize_copy on the new instance" do
+ dup = @obj.dup
+ ScratchPad.recorded.should_not == @obj.object_id
+ ScratchPad.recorded.should == dup.object_id
+ end
+
+ it "uses the internal allocator and does not call #allocate" do
+ klass = Class.new
+ instance = klass.new
+
+ def klass.allocate
+ raise "allocate should not be called"
+ end
+
+ dup = instance.dup
+ dup.class.should.equal? klass
+ end
+
+ it "does not copy frozen state from the original" do
+ @obj.freeze
+ dup = @obj.dup
+
+ dup.should_not.frozen?
+ end
+
+ it "copies instance variables" do
+ dup = @obj.dup
+ dup.one.should == 1
+ dup.two.should == :a
+ end
+
+ it "does not copy singleton methods" do
+ def @obj.special() :the_one end
+ dup = @obj.dup
+ -> { dup.special }.should.raise(NameError)
+ end
+
+ it "does not copy modules included in the singleton class" do
+ class << @obj
+ include KernelSpecs::DuplicateM
+ end
+
+ dup = @obj.dup
+ -> { dup.repr }.should.raise(NameError)
+ end
+
+ it "does not copy constants defined in the singleton class" do
+ class << @obj
+ CLONE = :clone
+ end
+
+ dup = @obj.dup
+ -> { class << dup; CLONE; end }.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/kernel/enum_for_spec.rb b/spec/ruby/core/kernel/enum_for_spec.rb
new file mode 100644
index 0000000000..0092e20468
--- /dev/null
+++ b/spec/ruby/core/kernel/enum_for_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#enum_for" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/eql_spec.rb b/spec/ruby/core/kernel/eql_spec.rb
new file mode 100644
index 0000000000..b3289090e5
--- /dev/null
+++ b/spec/ruby/core/kernel/eql_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/equal'
+
+describe "Kernel#eql?" do
+ it "is a public instance method" do
+ Kernel.public_instance_methods(false).should.include?(:eql?)
+ end
+
+ it_behaves_like :object_equal, :eql?
+end
diff --git a/spec/ruby/core/kernel/equal_value_spec.rb b/spec/ruby/core/kernel/equal_value_spec.rb
new file mode 100644
index 0000000000..2be151263d
--- /dev/null
+++ b/spec/ruby/core/kernel/equal_value_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#==" do
+ it "returns true only if obj and other are the same object" do
+ o1 = mock('o1')
+ o2 = mock('o2')
+ (o1 == o1).should == true
+ (o2 == o2).should == true
+ (o1 == o2).should == false
+ (nil == nil).should == true
+ (o1 == nil).should == false
+ (nil == o2).should == false
+ end
+end
diff --git a/spec/ruby/core/kernel/eval_spec.rb b/spec/ruby/core/kernel/eval_spec.rb
new file mode 100644
index 0000000000..f9ca47866e
--- /dev/null
+++ b/spec/ruby/core/kernel/eval_spec.rb
@@ -0,0 +1,552 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+EvalSpecs::A.new.c
+
+describe "Kernel#eval" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:eval)
+ end
+
+ it "is a module function" do
+ Kernel.respond_to?(:eval).should == true
+ end
+
+ it "evaluates the code within" do
+ eval("2 + 3").should == 5
+ end
+
+ it "coerces an object to string" do
+ eval(EvalSpecs::CoercedObject.new).should == 5
+ end
+
+ it "evaluates within the scope of the eval" do
+ EvalSpecs::A::B.name.should == "EvalSpecs::A::B"
+ end
+
+ it "evaluates such that constants are scoped to the class of the eval" do
+ EvalSpecs::A::C.name.should == "EvalSpecs::A::C"
+ end
+
+ it "finds a local in an enclosing scope" do
+ a = 1
+ eval("a").should == 1
+ end
+
+ it "updates a local in an enclosing scope" do
+ a = 1
+ eval("a = 2")
+ a.should == 2
+ end
+
+ it "updates a local in a surrounding block scope" do
+ EvalSpecs.new.f do
+ a = 1
+ eval("a = 2")
+ a.should == 2
+ end
+ end
+
+ it "updates a local in a scope above a surrounding block scope" do
+ a = 1
+ EvalSpecs.new.f do
+ eval("a = 2")
+ a.should == 2
+ end
+ a.should == 2
+ end
+
+ it "updates a local in a scope above when modified in a nested block scope" do
+ a = 1
+ es = EvalSpecs.new
+ eval("es.f { es.f { a = 2 } }")
+ a.should == 2
+ end
+
+ it "finds locals in a nested eval" do
+ eval('test = 10; eval("test")').should == 10
+ end
+
+ it "does not share locals across eval scopes" do
+ code = fixture __FILE__, "eval_locals.rb"
+ ruby_exe(code).chomp.should == "NameError"
+ end
+
+ it "doesn't accept a Proc object as a binding" do
+ x = 1
+ bind = proc {}
+
+ -> { eval("x", bind) }.should.raise(TypeError)
+ end
+
+ it "does not make Proc locals visible to evaluated code" do
+ bind = proc { inner = 4 }
+ -> { eval("inner", bind.binding) }.should.raise(NameError)
+ end
+
+ # REWRITE ME: This obscures the real behavior of where locals are stored
+ # in eval bindings.
+ it "allows a binding to be captured inside an eval" do
+ outer_binding = binding
+ level1 = eval("binding", outer_binding)
+ level2 = eval("binding", level1)
+
+ eval("x = 2", outer_binding)
+ eval("y = 3", level1)
+
+ eval("w=1", outer_binding)
+ eval("w", outer_binding).should == 1
+ eval("w=1", level1).should == 1
+ eval("w", level1).should == 1
+ eval("w=1", level2).should == 1
+ eval("w", level2).should == 1
+
+ eval("x", outer_binding).should == 2
+ eval("x=2", level1)
+ eval("x", level1).should == 2
+ eval("x=2", level2)
+ eval("x", level2).should == 2
+
+ eval("y=3", outer_binding)
+ eval("y", outer_binding).should == 3
+ eval("y=3", level1)
+ eval("y", level1).should == 3
+ eval("y=3", level2)
+ eval("y", level2).should == 3
+ end
+
+ it "uses the same scope for local variables when given the same binding" do
+ outer_binding = binding
+
+ eval("if false; a = 1; end", outer_binding)
+ eval("a", outer_binding).should == nil
+ end
+
+ it "allows creating a new class in a binding" do
+ bind = proc {}
+ eval("class EvalBindingProcA; end; EvalBindingProcA.name", bind.binding).should =~ /EvalBindingProcA$/
+ end
+
+ it "allows creating a new class in a binding created by #eval" do
+ bind = eval "binding"
+ eval("class EvalBindingA; end; EvalBindingA.name", bind).should =~ /EvalBindingA$/
+ end
+
+ it "includes file and line information in syntax error" do
+ expected = 'speccing.rb'
+ -> {
+ eval('if true', TOPLEVEL_BINDING, expected)
+ }.should.raise(SyntaxError) { |e|
+ e.message.should =~ /#{expected}:1:.+/
+ }
+ end
+
+ it "evaluates string with given filename and negative linenumber" do
+ expected_file = 'speccing.rb'
+ -> {
+ eval('if true', TOPLEVEL_BINDING, expected_file, -100)
+ }.should.raise(SyntaxError) { |e|
+ e.message.should =~ /#{expected_file}:-100:.+/
+ }
+ end
+
+ it "sets constants at the toplevel from inside a block" do
+ # The class Object bit is needed to workaround some mspec oddness
+ class Object
+ [1].each { eval "Const = 1"}
+ Const.should == 1
+ remove_const :Const
+ end
+ end
+
+ context "parameter forwarding" do
+ it "allows anonymous rest parameter forwarding" do
+ object = Object.new
+ def object.foo(a, b, c)
+ [a, b, c]
+ end
+ def object.bar(*)
+ eval "foo(*)"
+ end
+
+ object.bar(1, 2, 3).should == [1, 2, 3]
+ end
+
+ it "allows anonymous keyword parameters forwarding" do
+ object = Object.new
+ def object.foo(a:, b:, c:)
+ [a, b, c]
+ end
+ def object.bar(**)
+ eval "foo(**)"
+ end
+
+ object.bar(a: 1, b: 2, c: 3).should == [1, 2, 3]
+ end
+
+ it "allows anonymous block parameter forwarding" do
+ object = Object.new
+ def object.foo(&block)
+ block.call
+ end
+ def object.bar(&)
+ eval "foo(&)"
+ end
+
+ object.bar { :foobar }.should == :foobar
+ end
+
+ it "allows ... forwarding" do
+ object = Object.new
+ def object.foo(a, b:, &block)
+ [a, b, block.call]
+ end
+ def object.bar(...)
+ eval "foo(...)"
+ end
+
+ object.bar(1, b: 2) { 3 }.should == [1, 2, 3]
+ end
+
+ it "allows parameter forwarding to super" do
+ m = Module.new do
+ def foo(a, b:, &block)
+ [a, b, block.call]
+ end
+ end
+
+ c = Class.new do
+ include m
+
+ def foo(a, b:, &block)
+ eval "super"
+ end
+ end
+
+ object = c.new
+ object.foo(1, b: 2) { 3 }.should == [1, 2, 3]
+ end
+ end
+
+ it "uses (eval at __FILE__:__LINE__) if none is provided" do
+ eval("__FILE__").should == "(eval at #{__FILE__}:#{__LINE__})"
+ eval("__FILE__", binding).should == "(eval at #{__FILE__}:#{__LINE__})"
+ eval("__FILE__", binding, "success").should == "success"
+ eval("eval '__FILE__', binding").should == "(eval at (eval at #{__FILE__}:#{__LINE__}):1)"
+ eval("eval '__FILE__', binding", binding).should == "(eval at (eval at #{__FILE__}:#{__LINE__}):1)"
+ eval("eval '__FILE__', binding", binding, 'success').should == "(eval at success:1)"
+ eval("eval '__FILE__', binding, 'success'", binding).should == 'success'
+ end
+
+ it 'uses (eval at __FILE__:__LINE__) for __FILE__ and 1 for __LINE__ with a binding argument' do
+ eval("[__FILE__, __LINE__]", binding).should == ["(eval at #{__FILE__}:#{__LINE__})", 1]
+ end
+ # Found via Rubinius bug github:#149
+ it "does not alter the value of __FILE__ in the binding" do
+ first_time = EvalSpecs.call_eval
+ second_time = EvalSpecs.call_eval
+
+ # This bug is seen by calling the method twice and comparing the values
+ # of __FILE__ each time. If the bug is present, calling eval will set the
+ # value of __FILE__ to the eval's "filename" argument.
+
+ second_time.should_not == "(eval)"
+ first_time.should == second_time
+ end
+
+ it "can be aliased" do
+ alias aliased_eval eval
+ x = 2
+ aliased_eval('x += 40')
+ x.should == 42
+ end
+
+ # See http://jira.codehaus.org/browse/JRUBY-5163
+ it "uses the receiver as self inside the eval" do
+ eval("self").should.equal?(self)
+ Kernel.eval("self").should.equal?(Kernel)
+ end
+
+ it "does not pass the block to the method being eval'ed" do
+ -> {
+ eval('KernelSpecs::EvalTest.call_yield') { "content" }
+ }.should.raise(LocalJumpError)
+ end
+
+ it "returns from the scope calling #eval when evaluating 'return'" do
+ -> { eval("return :eval") }.call.should == :eval
+ end
+
+ it "returns from the method calling #eval when evaluating 'return'" do
+ def eval_return(n)
+ eval("return n*2")
+ end
+ -> { eval_return(3) }.call.should == 6
+ end
+
+ it "returns from the method calling #eval when evaluating 'return' in BEGIN" do
+ def eval_return(n)
+ eval("BEGIN {return n*3}")
+ end
+ -> { eval_return(4) }.call.should == 12
+ end
+
+ it "unwinds through a Proc-style closure and returns from a lambda-style closure in the closure chain" do
+ code = fixture __FILE__, "eval_return_with_lambda.rb"
+ ruby_exe(code).chomp.should == "a,b,c,eval,f"
+ end
+
+ it "raises a LocalJumpError if there is no lambda-style closure in the chain" do
+ code = fixture __FILE__, "eval_return_without_lambda.rb"
+ ruby_exe(code).chomp.should == "a,b,c,e,LocalJumpError,f"
+ end
+
+ it "can be called with Method#call" do
+ method(:eval).call("2 * 3").should == 6
+ end
+
+ it "has the correct default definee when called through Method#call" do
+ class EvalSpecs
+ method(:eval).call("def eval_spec_method_call; end")
+ EvalSpecs.should.method_defined?(:eval_spec_method_call, false)
+ end
+ end
+
+ it "makes flip-flop operator work correctly" do
+ ScratchPad.record []
+
+ eval "10.times { |i| ScratchPad << i if (i == 4)...(i == 4) }"
+ ScratchPad.recorded.should == [4, 5, 6, 7, 8, 9]
+
+ ScratchPad.clear
+ end
+
+ it "returns nil if given an empty string" do
+ eval("").should == nil
+ end
+
+ context "with shebang" do
+ it "ignores shebang with ruby interpreter" do
+ pid = eval(<<~CODE.b)
+ #!/usr/bin/env ruby
+ Process.pid
+ CODE
+
+ pid.should == Process.pid
+ end
+
+ it "ignores shebang with non-ruby interpreter" do
+ pid = eval(<<~CODE.b)
+ #!/usr/bin/env puma
+ Process.pid
+ CODE
+
+ pid.should == Process.pid
+ end
+ end
+
+ # See language/magic_comment_spec.rb for more magic comments specs
+ describe "with a magic encoding comment" do
+ it "uses the magic comment encoding for the encoding of literal strings" do
+ code = "# encoding: UTF-8\n'é'.encoding".b
+ code.encoding.should == Encoding::BINARY
+ eval(code).should == Encoding::UTF_8
+ end
+
+ it "uses the magic comment encoding for parsing constants" do
+ code = <<CODE.b
+# encoding: UTF-8
+class EvalSpecs
+ VÏ€ = 3.14
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€")
+ EvalSpecs::VÏ€.should == 3.14
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€)
+ end
+
+ it "allows an emacs-style magic comment encoding" do
+ code = <<CODE.b
+# -*- encoding: UTF-8 -*-
+class EvalSpecs
+VÏ€emacs = 3.14
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€emacs")
+ EvalSpecs::VÏ€emacs.should == 3.14
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€emacs)
+ end
+
+ it "allows spaces before the magic encoding comment" do
+ code = <<CODE.b
+\t \t # encoding: UTF-8
+class EvalSpecs
+ VÏ€spaces = 3.14
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€spaces")
+ EvalSpecs::VÏ€spaces.should == 3.14
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€spaces)
+ end
+
+ it "allows a shebang line before the magic encoding comment" do
+ code = <<CODE.b
+#!/usr/bin/env ruby
+# encoding: UTF-8
+class EvalSpecs
+ VÏ€shebang = 3.14
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€shebang")
+ EvalSpecs::VÏ€shebang.should == 3.14
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€shebang)
+ end
+
+ it "allows a shebang line and some spaces before the magic encoding comment" do
+ code = <<CODE.b
+#!/usr/bin/env ruby
+ # encoding: UTF-8
+class EvalSpecs
+ VÏ€shebang_spaces = 3.14
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€shebang_spaces")
+ EvalSpecs::VÏ€shebang_spaces.should == 3.14
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€shebang_spaces)
+ end
+
+ it "allows a magic encoding comment and a subsequent frozen_string_literal magic comment" do
+ frozen_string_default = "test".frozen?
+
+ code = <<CODE.b
+# encoding: UTF-8
+# frozen_string_literal: #{!frozen_string_default}
+class EvalSpecs
+ VÏ€string = "frozen"
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€string")
+ EvalSpecs::VÏ€string.should == "frozen"
+ EvalSpecs::VÏ€string.encoding.should == Encoding::UTF_8
+ EvalSpecs::VÏ€string.frozen?.should == !frozen_string_default
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€string)
+ end
+
+ it "allows a magic encoding comment and a frozen_string_literal magic comment on the same line in emacs style" do
+ code = <<CODE.b
+# -*- encoding: UTF-8; frozen_string_literal: true -*-
+class EvalSpecs
+VÏ€same_line = "frozen"
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should.include?(:"VÏ€same_line")
+ EvalSpecs::VÏ€same_line.should == "frozen"
+ EvalSpecs::VÏ€same_line.encoding.should == Encoding::UTF_8
+ EvalSpecs::VÏ€same_line.frozen?.should == true
+ ensure
+ EvalSpecs.send(:remove_const, :VÏ€same_line)
+ end
+
+ it "ignores the magic encoding comment if it is after a frozen_string_literal magic comment" do
+ frozen_string_default = "test".frozen?
+ code = <<CODE.b
+# frozen_string_literal: #{!frozen_string_default}
+# encoding: UTF-8
+class EvalSpecs
+ VÏ€frozen_first = "frozen"
+end
+CODE
+ code.encoding.should == Encoding::BINARY
+ eval(code)
+ EvalSpecs.constants(false).should_not.include?(:"VÏ€frozen_first")
+ binary_constant = "VÏ€frozen_first".b.to_sym
+ EvalSpecs.constants(false).should.include?(binary_constant)
+ value = EvalSpecs.const_get(binary_constant)
+ value.should == "frozen"
+ value.encoding.should == Encoding::BINARY
+ value.frozen?.should == !frozen_string_default
+ ensure
+ EvalSpecs.send(:remove_const, binary_constant)
+ end
+
+ it "ignores the frozen_string_literal magic comment if it appears after a token and warns if $VERBOSE is true" do
+ frozen_string_default = "test".frozen?
+ code = <<CODE
+some_token_before_magic_comment = :anything
+# frozen_string_literal: #{!frozen_string_default}
+class EvalSpecs
+ VÏ€string_not_frozen = "not frozen"
+end
+CODE
+ -> { eval(code) }.should complain(/warning: [`']frozen_string_literal' is ignored after any tokens/, verbose: true)
+ EvalSpecs::VÏ€string_not_frozen.frozen?.should == frozen_string_default
+ EvalSpecs.send :remove_const, :VÏ€string_not_frozen
+
+ -> { eval(code) }.should_not complain(verbose: false)
+ EvalSpecs::VÏ€string_not_frozen.frozen?.should == frozen_string_default
+ EvalSpecs.send :remove_const, :VÏ€string_not_frozen
+ end
+ end
+
+ describe 'with refinements' do
+ it "activates refinements from the eval scope" do
+ refinery = Module.new do
+ refine EvalSpecs::A do
+ def foo
+ "bar"
+ end
+ end
+ end
+
+ result = nil
+
+ Module.new do
+ using refinery
+
+ result = eval "EvalSpecs::A.new.foo"
+ end
+
+ result.should == "bar"
+ end
+
+ it "activates refinements from the binding" do
+ refinery = Module.new do
+ refine EvalSpecs::A do
+ def foo
+ "bar"
+ end
+ end
+ end
+
+ b = nil
+ m = Module.new do
+ using refinery
+ b = binding
+ end
+
+ result = eval "EvalSpecs::A.new.foo", b
+
+ result.should == "bar"
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/exec_spec.rb b/spec/ruby/core/kernel/exec_spec.rb
new file mode 100644
index 0000000000..fd8485791a
--- /dev/null
+++ b/spec/ruby/core/kernel/exec_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#exec" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:exec)
+ end
+
+ it "runs the specified command, replacing current process" do
+ ruby_exe('exec "echo hello"; puts "fail"').should == "hello\n"
+ end
+end
+
+describe "Kernel.exec" do
+ it "runs the specified command, replacing current process" do
+ ruby_exe('Kernel.exec "echo hello"; puts "fail"').should == "hello\n"
+ end
+end
diff --git a/spec/ruby/core/kernel/exit_spec.rb b/spec/ruby/core/kernel/exit_spec.rb
new file mode 100644
index 0000000000..864ff84449
--- /dev/null
+++ b/spec/ruby/core/kernel/exit_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/process/exit'
+
+describe "Kernel#exit" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:exit)
+ end
+
+ it_behaves_like :process_exit, :exit, KernelSpecs::Method.new
+end
+
+describe "Kernel.exit" do
+ it_behaves_like :process_exit, :exit, Kernel
+end
+
+describe "Kernel#exit!" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:exit!)
+ end
+
+ it_behaves_like :process_exit!, :exit!, "self"
+end
+
+describe "Kernel.exit!" do
+ it_behaves_like :process_exit!, :exit!, "Kernel"
+end
diff --git a/spec/ruby/core/kernel/extend_spec.rb b/spec/ruby/core/kernel/extend_spec.rb
new file mode 100644
index 0000000000..a344c7b5ca
--- /dev/null
+++ b/spec/ruby/core/kernel/extend_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+module KernelSpecs::M
+ def self.extend_object(o)
+ ScratchPad << "extend_object"
+ super
+ end
+
+ def self.extended(o)
+ ScratchPad << "extended"
+ super
+ end
+
+ def self.append_features(o)
+ ScratchPad << "append_features"
+ super
+ end
+end
+
+describe "Kernel#extend" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "requires multiple arguments" do
+ Object.new.method(:extend).arity.should < 0
+ end
+
+ it "calls extend_object on argument" do
+ o = mock('o')
+ o.extend KernelSpecs::M
+ ScratchPad.recorded.include?("extend_object").should == true
+ end
+
+ it "does not calls append_features on arguments metaclass" do
+ o = mock('o')
+ o.extend KernelSpecs::M
+ ScratchPad.recorded.include?("append_features").should == false
+ end
+
+ it "calls extended on argument" do
+ o = mock('o')
+ o.extend KernelSpecs::M
+ ScratchPad.recorded.include?("extended").should == true
+ end
+
+ it "makes the class a kind_of? the argument" do
+ c = Class.new do
+ extend KernelSpecs::M
+ end
+ (c.kind_of? KernelSpecs::M).should == true
+ end
+
+ it "raises an ArgumentError when no arguments given" do
+ -> { Object.new.extend }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError when the argument is not a Module" do
+ o = mock('o')
+ klass = Class.new
+ -> { o.extend(klass) }.should.raise(TypeError)
+ end
+
+ describe "on frozen instance" do
+ before :each do
+ @frozen = Object.new.freeze
+ @module = KernelSpecs::M
+ end
+
+ it "raises an ArgumentError when no arguments given" do
+ -> { @frozen.extend }.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError" do
+ -> { @frozen.extend @module }.should.raise(FrozenError)
+ end
+ end
+
+ it "updated class methods of a module when it extends self and includes another module" do
+ a = Module.new do
+ extend self
+ end
+ b = Module.new do
+ def foo; :foo; end
+ end
+
+ a.include b
+ a.foo.should == :foo
+ end
+end
diff --git a/spec/ruby/core/kernel/fail_spec.rb b/spec/ruby/core/kernel/fail_spec.rb
new file mode 100644
index 0000000000..ac379b67d5
--- /dev/null
+++ b/spec/ruby/core/kernel/fail_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#fail" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:fail)
+ end
+
+ it "raises a RuntimeError" do
+ -> { fail }.should.raise(RuntimeError)
+ end
+
+ it "accepts an Object with an exception method returning an Exception" do
+ obj = Object.new
+ def obj.exception(msg)
+ StandardError.new msg
+ end
+ -> { fail obj, "..." }.should.raise(StandardError, "...")
+ end
+
+ it "instantiates the specified exception class" do
+ error_class = Class.new(RuntimeError)
+ -> { fail error_class }.should.raise(error_class)
+ end
+
+ it "uses the specified message" do
+ -> {
+ begin
+ fail "the duck is not irish."
+ rescue => e
+ e.message.should == "the duck is not irish."
+ raise
+ else
+ raise Exception
+ end
+ }.should.raise(RuntimeError)
+ end
+end
+
+describe "Kernel.fail" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/fixtures/Complex.rb b/spec/ruby/core/kernel/fixtures/Complex.rb
new file mode 100644
index 0000000000..bf14d55ad5
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/Complex.rb
@@ -0,0 +1,5 @@
+module KernelSpecs
+ def self.Complex_method(string)
+ Complex(string)
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/__callee__.rb b/spec/ruby/core/kernel/fixtures/__callee__.rb
new file mode 100644
index 0000000000..7138dbc5aa
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/__callee__.rb
@@ -0,0 +1,34 @@
+module KernelSpecs
+ class CalleeTest
+ def f
+ __callee__
+ end
+
+ alias_method :g, :f
+
+ def in_block
+ (1..2).map { __callee__ }
+ end
+
+ define_method(:dm) do
+ __callee__
+ end
+
+ define_method(:dm_block) do
+ (1..2).map { __callee__ }
+ end
+
+ def from_send
+ send "__callee__"
+ end
+
+ def from_eval
+ eval "__callee__"
+ end
+
+ @@method = __callee__
+ def from_class_body
+ @@method
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/__dir__.rb b/spec/ruby/core/kernel/fixtures/__dir__.rb
new file mode 100644
index 0000000000..bf9a15e3c8
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/__dir__.rb
@@ -0,0 +1,2 @@
+puts __FILE__
+puts __dir__
diff --git a/spec/ruby/core/kernel/fixtures/__method__.rb b/spec/ruby/core/kernel/fixtures/__method__.rb
new file mode 100644
index 0000000000..9300366b37
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/__method__.rb
@@ -0,0 +1,34 @@
+module KernelSpecs
+ class MethodTest
+ def f
+ __method__
+ end
+
+ alias_method :g, :f
+
+ def in_block
+ (1..2).map { __method__ }
+ end
+
+ define_method(:dm) do
+ __method__
+ end
+
+ define_method(:dm_block) do
+ (1..2).map { __method__ }
+ end
+
+ def from_send
+ send "__method__"
+ end
+
+ def from_eval
+ eval "__method__"
+ end
+
+ @@method = __method__
+ def from_class_body
+ @@method
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_b.rb b/spec/ruby/core/kernel/fixtures/autoload_b.rb
new file mode 100644
index 0000000000..e8be221ec7
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_b.rb
@@ -0,0 +1,5 @@
+module KSAutoloadB
+ def self.loaded
+ :ksautoload_b
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_d.rb b/spec/ruby/core/kernel/fixtures/autoload_d.rb
new file mode 100644
index 0000000000..552cb5e82c
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_d.rb
@@ -0,0 +1,5 @@
+module KSAutoloadD
+ def self.loaded
+ :ksautoload_d
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb b/spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb
new file mode 100644
index 0000000000..f5bedc6992
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_from_included_module.rb
@@ -0,0 +1,9 @@
+module KernelSpecs
+ module AutoloadMethod
+ module AutoloadFromIncludedModule
+ def self.loaded
+ :autoload_from_included_module
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb b/spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb
new file mode 100644
index 0000000000..f4f1cfbf7c
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_from_included_module2.rb
@@ -0,0 +1,9 @@
+module KernelSpecs
+ module AutoloadMethod2
+ module AutoloadFromIncludedModule2
+ def self.loaded
+ :autoload_from_included_module2
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_frozen.rb b/spec/ruby/core/kernel/fixtures/autoload_frozen.rb
new file mode 100644
index 0000000000..e9dc42912b
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_frozen.rb
@@ -0,0 +1,7 @@
+Object.freeze
+
+begin
+ autoload :ANY_CONSTANT, "no_autoload.rb"
+rescue Exception => e
+ print e.class, " - ", defined?(ANY_CONSTANT).inspect
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb b/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb
new file mode 100644
index 0000000000..6de6f5091d
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_relative_b.rb
@@ -0,0 +1,7 @@
+module KernelSpecs
+ module KSAutoloadRelativeB
+ def self.loaded
+ :ksautoload_b
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb b/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb
new file mode 100644
index 0000000000..5b6b5e1fa2
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/autoload_relative_d.rb
@@ -0,0 +1,5 @@
+module KSAutoloadRelativeD
+ def self.loaded
+ :ksautoload_d
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/caller.rb b/spec/ruby/core/kernel/fixtures/caller.rb
new file mode 100644
index 0000000000..ae3e13e9c9
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/caller.rb
@@ -0,0 +1,7 @@
+module KernelSpecs
+ class CallerTest
+ def self.locations(*args)
+ caller(*args)
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/caller_at_exit.rb b/spec/ruby/core/kernel/fixtures/caller_at_exit.rb
new file mode 100644
index 0000000000..ca9d808597
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/caller_at_exit.rb
@@ -0,0 +1,7 @@
+at_exit {
+ foo
+}
+
+def foo
+ puts caller(0)
+end
diff --git a/spec/ruby/core/kernel/fixtures/caller_locations.rb b/spec/ruby/core/kernel/fixtures/caller_locations.rb
new file mode 100644
index 0000000000..cc4e04d38f
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/caller_locations.rb
@@ -0,0 +1,7 @@
+module KernelSpecs
+ class CallerLocationsTest
+ def self.locations(*args)
+ caller_locations(*args)
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/chomp.rb b/spec/ruby/core/kernel/fixtures/chomp.rb
new file mode 100644
index 0000000000..f08dbadce5
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/chomp.rb
@@ -0,0 +1,4 @@
+# -*- encoding: utf-8 -*-
+
+$_ = "ã‚れ\r\n"
+print Kernel.chomp
diff --git a/spec/ruby/core/kernel/fixtures/chomp_f.rb b/spec/ruby/core/kernel/fixtures/chomp_f.rb
new file mode 100644
index 0000000000..3c22cb21e7
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/chomp_f.rb
@@ -0,0 +1,4 @@
+# -*- encoding: utf-8 -*-
+
+$_ = "ã‚れ\r\n"
+print chomp
diff --git a/spec/ruby/core/kernel/fixtures/chop.rb b/spec/ruby/core/kernel/fixtures/chop.rb
new file mode 100644
index 0000000000..dfd0626723
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/chop.rb
@@ -0,0 +1,4 @@
+# -*- encoding: utf-8 -*-
+
+$_ = "ã‚れ"
+print Kernel.chop
diff --git a/spec/ruby/core/kernel/fixtures/chop_f.rb b/spec/ruby/core/kernel/fixtures/chop_f.rb
new file mode 100644
index 0000000000..4ec89eb9ec
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/chop_f.rb
@@ -0,0 +1,4 @@
+# -*- encoding: utf-8 -*-
+
+$_ = "ã‚れ"
+print chop
diff --git a/spec/ruby/core/kernel/fixtures/classes.rb b/spec/ruby/core/kernel/fixtures/classes.rb
new file mode 100644
index 0000000000..ad82f3cef5
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/classes.rb
@@ -0,0 +1,577 @@
+module KernelSpecs
+ def self.Array_function(arg)
+ Array(arg)
+ end
+
+ def self.Array_method(arg)
+ Kernel.Array(arg)
+ end
+
+ def self.Hash_function(arg)
+ Hash(arg)
+ end
+
+ def self.Hash_method(arg)
+ Kernel.Hash(arg)
+ end
+
+ def self.Integer_function(arg)
+ Integer(arg)
+ end
+
+ def self.Integer_method(arg)
+ Kernel.Integer(arg)
+ end
+
+ def self.putc_function(arg)
+ putc arg
+ end
+
+ def self.putc_method(arg)
+ Kernel.putc arg
+ end
+
+ def self.has_private_method(name)
+ IO.popen([*ruby_exe, "-n", "-e", "print Kernel.private_method_defined?(#{name.inspect})"], "r+") do |io|
+ io.puts
+ io.close_write
+ io.read
+ end == "true"
+ end
+
+ def self.chop(str, method)
+ IO.popen([*ruby_exe, "-n", "-e", "$_ = #{str.inspect}; #{method}; print $_"], "r+") do |io|
+ io.puts
+ io.close_write
+ io.read
+ end
+ end
+
+ def self.chomp(str, method, sep="\n")
+ code = "$_ = #{str.inspect}; $/ = #{sep.inspect}; #{method}; print $_"
+ IO.popen([*ruby_exe, "-W0", "-n", "-e", code], "r+") do |io|
+ io.puts
+ io.close_write
+ io.read
+ end
+ end
+
+ def self.run_with_dash_n(file)
+ IO.popen([*ruby_exe, "-n", file], "r+") do |io|
+ io.puts
+ io.close_write
+ io.read
+ end
+ end
+
+ # kind_of?, is_a?, instance_of?
+ module SomeOtherModule; end
+ module AncestorModule; end
+ module MyModule; end
+ module MyPrependedModule; end
+ module MyExtensionModule; end
+
+ class AncestorClass < String
+ include AncestorModule
+ end
+
+ class InstanceClass < AncestorClass
+ include MyModule
+ end
+
+ class KindaClass < AncestorClass
+ include MyModule
+ prepend MyPrependedModule
+
+ def initialize
+ self.extend MyExtensionModule
+ end
+ end
+
+ class Method
+ public :abort, :exit, :exit!, :fork, :system
+ end
+
+ class Methods
+
+ module MetaclassMethods
+ def peekaboo
+ end
+
+ protected
+
+ def nopeeking
+ end
+
+ private
+
+ def shoo
+ end
+ end
+
+ def self.ichi; end
+ def ni; end
+ class << self
+ def san; end
+ end
+
+ private
+
+ def self.shi; end
+ def juu_shi; end
+
+ class << self
+ def roku; end
+
+ private
+
+ def shichi; end
+ end
+
+ protected
+
+ def self.hachi; end
+ def ku; end
+
+ class << self
+ def juu; end
+
+ protected
+
+ def juu_ichi; end
+ end
+
+ public
+
+ def self.juu_ni; end
+ def juu_san; end
+ end
+
+ class PrivateSup
+ def public_in_sub
+ end
+
+ private :public_in_sub
+ end
+
+ class PublicSub < PrivateSup
+ def public_in_sub
+ end
+ end
+
+ class A
+ # There is Kernel#public_method, so we don't want this one to clash
+ def pub_method; :public_method; end
+
+ def undefed_method; :undefed_method; end
+ undef_method :undefed_method
+
+ protected
+ def protected_method; :protected_method; end
+
+ private
+ def private_method; :private_method; end
+
+ public
+ define_method(:defined_method) { :defined }
+ end
+
+ class B < A
+ alias aliased_pub_method pub_method
+ end
+
+ class BasicA < BasicObject
+ define_method(:respond_to?, ::Kernel.instance_method(:respond_to?))
+
+ def pub_method; :public_method; end
+
+ def undefed_method; :undefed_method; end
+ undef_method :undefed_method
+
+ protected
+ def protected_method; :protected_method; end
+
+ private
+ def private_method; :private_method; end
+ end
+
+ class MissingA < A
+ undef :respond_to_missing?
+ end
+
+ class VisibilityChange
+ class << self
+ private :new
+ end
+ end
+
+ class Binding
+ @@super_secret = "password"
+
+ def initialize(n)
+ @secret = n
+ end
+
+ def square(n)
+ n * n
+ end
+
+ def get_binding
+ a = true
+ @bind = binding
+
+ # Add/Change stuff
+ b = true
+ @secret += 1
+
+ @bind
+ end
+ end
+
+
+ module BlockGiven
+ def self.accept_block
+ block_given?
+ end
+
+ def self.accept_block_as_argument(&block)
+ block_given?
+ end
+
+ def self.accept_block_inside_block()
+ yield_self {
+ block_given?
+ }
+ end
+
+ def self.accept_block_as_argument_inside_block(&block)
+ yield_self {
+ block_given?
+ }
+ end
+
+ class << self
+ define_method(:defined_block) do
+ block_given?
+ end
+
+ define_method(:defined_block_inside_block) do
+ yield_self {
+ block_given?
+ }
+ end
+ end
+ end
+
+ module SelfBlockGiven
+ def self.accept_block
+ self.send(:block_given?)
+ end
+
+ def self.accept_block_as_argument(&block)
+ self.send(:block_given?)
+ end
+
+ def self.accept_block_inside_block
+ yield_self {
+ self.send(:block_given?)
+ }
+ end
+
+ def self.accept_block_as_argument_inside_block(&block)
+ yield_self {
+ self.send(:block_given?)
+ }
+ end
+
+ class << self
+ define_method(:defined_block) do
+ self.send(:block_given?)
+ end
+
+ define_method(:defined_block_inside_block) do
+ yield_self {
+ self.send(:block_given?)
+ }
+ end
+ end
+ end
+
+ module KernelBlockGiven
+ def self.accept_block
+ Kernel.block_given?
+ end
+
+ def self.accept_block_as_argument(&block)
+ Kernel.block_given?
+ end
+
+ def self.accept_block_inside_block
+ yield_self {
+ Kernel.block_given?
+ }
+ end
+
+ def self.accept_block_as_argument_inside_block(&block)
+ yield_self {
+ Kernel.block_given?
+ }
+ end
+
+ class << self
+ define_method(:defined_block) do
+ Kernel.block_given?
+ end
+
+ define_method(:defined_block_inside_block) do
+ yield_self {
+ Kernel.block_given?
+ }
+ end
+ end
+ end
+
+ class EvalTest
+ def self.eval_yield_with_binding
+ eval("yield", binding)
+ end
+ def self.call_yield
+ yield
+ end
+ end
+
+ module DuplicateM
+ def repr
+ self.class.name.to_s
+ end
+ end
+
+ class Duplicate
+ attr_accessor :one, :two
+
+ def initialize(one, two)
+ @one = one
+ @two = two
+ end
+
+ def initialize_copy(other, **kw)
+ ScratchPad.record object_id
+ end
+
+ # define to support calling #clone with optional :freeze keyword argument
+ def initialize_clone(other, **kw)
+ super(other) # to call #initialize_copy
+ end
+ end
+
+ class Clone
+ def initialize_clone(other)
+ ScratchPad.record other
+ end
+ end
+
+ class CloneFreeze
+ def initialize_clone(other, **kwargs)
+ ScratchPad.record([other, kwargs])
+ end
+ end
+
+ class Dup
+ def initialize_dup(other)
+ ScratchPad.record other.object_id
+ end
+ end
+
+ module ParentMixin
+ def parent_mixin_method; end
+ end
+
+ class Parent
+ include ParentMixin
+ def parent_method; end
+ def another_parent_method; end
+ def self.parent_class_method; :foo; end
+ end
+
+ class Child < Parent
+ undef_method :parent_method
+ end
+
+ class Grandchild < Child
+ undef_method :parent_mixin_method
+ end
+
+ # for testing lambda
+ class Lambda
+ def outer
+ inner
+ end
+
+ def mp(&b); b; end
+
+ def inner
+ b = mp { return :good }
+
+ pr = -> x { x.call }
+
+ pr.call(b)
+
+ # We shouldn't be here, b should have unwinded through
+ return :bad
+ end
+ end
+
+ module LambdaSpecs
+ module ZSuper
+ def lambda
+ super
+ end
+ end
+
+ class ForwardBlockWithZSuper
+ prepend(ZSuper)
+ end
+
+ module Ampersand
+ def lambda(&block)
+ suppress_warning {super(&block)}
+ end
+ end
+
+ class SuperAmpersand
+ prepend(Ampersand)
+ end
+ end
+
+ class RespondViaMissing
+ def respond_to_missing?(method, priv=false)
+ case method
+ when :handled_publicly
+ true
+ when :handled_privately
+ priv
+ when :not_handled
+ false
+ else
+ raise "Typo in method name: #{method.inspect}"
+ end
+ end
+
+ def method_missing(method, *args)
+ raise "the method name should be a Symbol" unless Symbol === method
+ "Done #{method}(#{args})"
+ end
+ end
+
+ class InstanceVariable
+ def initialize
+ @greeting = "hello"
+ end
+ end
+
+ class PrivateToAry
+ private
+
+ def to_ary
+ [1, 2]
+ end
+
+ def to_a
+ [3, 4]
+ end
+ end
+
+ class PrivateToA
+ private
+
+ def to_a
+ [3, 4]
+ end
+ end
+
+ module AutoloadMethod
+ def setup_autoload(file)
+ autoload :AutoloadFromIncludedModule, file
+ end
+ end
+
+ class AutoloadMethodIncluder
+ include AutoloadMethod
+ end
+
+ module AutoloadMethod2
+ def setup_autoload(file)
+ Kernel.autoload :AutoloadFromIncludedModule2, file
+ end
+ end
+
+ class AutoloadMethodIncluder2
+ include AutoloadMethod2
+ end
+
+ class WarnInNestedCall
+ def f4(s = "", n)
+ f3(s, n)
+ end
+
+ def f3(s, n)
+ f2(s, n)
+ end
+
+ def f2(s, n)
+ f1(s, n)
+ end
+
+ def f1(s, n)
+ warn(s, uplevel: n)
+ end
+
+ def warn_call_lineno; method(:f1).source_location[1] + 1; end
+ def f1_call_lineno; method(:f2).source_location[1] + 1; end
+ def f2_call_lineno; method(:f3).source_location[1] + 1; end
+ def f3_call_lineno; method(:f4).source_location[1] + 1; end
+ end
+
+ CustomRangeInteger = Struct.new(:value) do
+ def to_int; value; end
+ def <=>(other); to_int <=> other.to_int; end
+ def -(other); self.class.new(to_int - other.to_int); end
+ def +(other); self.class.new(to_int + other.to_int); end
+ end
+
+ CustomRangeFloat = Struct.new(:value) do
+ def to_f; value; end
+ def <=>(other); to_f <=> other.to_f; end
+ def -(other); to_f - other.to_f; end
+ def +(other); self.class.new(to_f + other.to_f); end
+ end
+end
+
+class EvalSpecs
+ class A
+ eval "class B; end"
+ def c
+ eval "class C; end"
+ end
+ end
+
+ class CoercedObject
+ def to_str
+ '2 + 3'
+ end
+
+ def hash
+ nil
+ end
+ end
+
+ def f
+ yield
+ end
+
+ def self.call_eval
+ f = __FILE__
+ eval "true", binding, "(eval)", 1
+ return f
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/eval_locals.rb b/spec/ruby/core/kernel/fixtures/eval_locals.rb
new file mode 100644
index 0000000000..ca8b381806
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/eval_locals.rb
@@ -0,0 +1,6 @@
+begin
+ eval("a = 2")
+ eval("p a")
+rescue Object => e
+ puts e.class
+end
diff --git a/spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb b/spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb
new file mode 100644
index 0000000000..9e2d045bf3
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/eval_return_with_lambda.rb
@@ -0,0 +1,12 @@
+print "a,"
+x = -> do
+ print "b,"
+ Proc.new do
+ print "c,"
+ eval("return :eval")
+ print "d,"
+ end.call
+ print "e,"
+end.call
+print x, ","
+print "f"
diff --git a/spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb b/spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb
new file mode 100644
index 0000000000..fc8b7f1d4a
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/eval_return_without_lambda.rb
@@ -0,0 +1,14 @@
+print "a,"
+begin
+ print "b,"
+ x = Proc.new do
+ print "c,"
+ eval("return :eval")
+ print "d,"
+ end.call
+ print x, ","
+rescue LocalJumpError => e
+ print "e,"
+ print e.class, ","
+end
+print "f"
diff --git a/spec/ruby/core/kernel/fixtures/singleton_methods.rb b/spec/ruby/core/kernel/fixtures/singleton_methods.rb
new file mode 100644
index 0000000000..32ea0adf89
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/singleton_methods.rb
@@ -0,0 +1,13 @@
+module SingletonMethodsSpecs
+ module Prepended
+ def mspec_test_kernel_singleton_methods
+ end
+ public :mspec_test_kernel_singleton_methods
+ end
+
+ ::Module.prepend Prepended
+
+ module SelfExtending
+ extend self
+ end
+end
diff --git a/spec/ruby/core/kernel/fixtures/test.rb b/spec/ruby/core/kernel/fixtures/test.rb
new file mode 100644
index 0000000000..949948606f
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/test.rb
@@ -0,0 +1,362 @@
+def foo1
+end
+
+def foo2
+end
+
+def foo3
+end
+
+def foo4
+end
+
+def foo5
+end
+
+def foo6
+end
+
+def foo7
+end
+
+def foo8
+end
+
+def foo9
+end
+
+def foo10
+end
+
+def foo11
+end
+
+def foo12
+end
+
+def foo13
+end
+
+def foo14
+end
+
+def foo15
+end
+
+def foo16
+end
+
+def foo17
+end
+
+def foo18
+end
+
+def foo19
+end
+
+def foo20
+end
+
+def foo21
+end
+
+def foo22
+end
+
+def foo23
+end
+
+def foo24
+end
+
+def foo25
+end
+
+def foo26
+end
+
+def foo27
+end
+
+def foo28
+end
+
+def foo29
+end
+
+def foo30
+end
+
+def foo31
+end
+
+def foo32
+end
+
+def foo33
+end
+
+def foo34
+end
+
+def foo35
+end
+
+def foo36
+end
+
+def foo37
+end
+
+def foo38
+end
+
+def foo39
+end
+
+def foo40
+end
+
+def foo41
+end
+
+def foo42
+end
+
+def foo43
+end
+
+def foo44
+end
+
+def foo45
+end
+
+def foo46
+end
+
+def foo47
+end
+
+def foo48
+end
+
+def foo49
+end
+
+def foo50
+end
+
+def foo51
+end
+
+def foo52
+end
+
+def foo53
+end
+
+def foo54
+end
+
+def foo55
+end
+
+def foo56
+end
+
+def foo57
+end
+
+def foo58
+end
+
+def foo59
+end
+
+def foo60
+end
+
+def foo61
+end
+
+def foo62
+end
+
+def foo63
+end
+
+def foo64
+end
+
+def foo65
+end
+
+def foo66
+end
+
+def foo67
+end
+
+def foo68
+end
+
+def foo69
+end
+
+def foo70
+end
+
+def foo71
+end
+
+def foo72
+end
+
+def foo73
+end
+
+def foo74
+end
+
+def foo75
+end
+
+def foo76
+end
+
+def foo77
+end
+
+def foo78
+end
+
+def foo79
+end
+
+def foo80
+end
+
+def foo81
+end
+
+def foo82
+end
+
+def foo83
+end
+
+def foo84
+end
+
+def foo85
+end
+
+def foo86
+end
+
+def foo87
+end
+
+def foo88
+end
+
+def foo89
+end
+
+def foo90
+end
+
+def foo91
+end
+
+def foo92
+end
+
+def foo93
+end
+
+def foo94
+end
+
+def foo95
+end
+
+def foo96
+end
+
+def foo97
+end
+
+def foo98
+end
+
+def foo99
+end
+
+def foo100
+end
+
+def foo101
+end
+
+def foo102
+end
+
+def foo103
+end
+
+def foo104
+end
+
+def foo105
+end
+
+def foo106
+end
+
+def foo107
+end
+
+def foo108
+end
+
+def foo109
+end
+
+def foo110
+end
+
+def foo111
+end
+
+def foo112
+end
+
+def foo113
+end
+
+def foo114
+end
+
+def foo115
+end
+
+def foo116
+end
+
+def foo117
+end
+
+def foo118
+end
+
+def foo119
+end
+
+def foo120
+end
+
+def foo121
+end
diff --git a/spec/ruby/core/kernel/fixtures/warn_core_method.rb b/spec/ruby/core/kernel/fixtures/warn_core_method.rb
new file mode 100644
index 0000000000..fd82562404
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/warn_core_method.rb
@@ -0,0 +1,14 @@
+raise 'should be run without RubyGems' if defined?(Gem)
+
+public def deprecated(n=1)
+ # puts nil, caller(0), nil
+ warn "use X instead", uplevel: n
+end
+
+1.times do # to test with a non-empty stack above the reported locations
+ deprecated
+ tap(&:deprecated)
+ tap { deprecated(2) }
+ # eval sources with a <internal: file are also ignored
+ eval "tap(&:deprecated)", nil, "<internal:should-be-skipped-by-warn-uplevel>"
+end
diff --git a/spec/ruby/core/kernel/fixtures/warn_require.rb b/spec/ruby/core/kernel/fixtures/warn_require.rb
new file mode 100644
index 0000000000..c4b0733233
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/warn_require.rb
@@ -0,0 +1 @@
+warn 'warn-require-warning', uplevel: 1
diff --git a/spec/ruby/core/kernel/fixtures/warn_require_caller.rb b/spec/ruby/core/kernel/fixtures/warn_require_caller.rb
new file mode 100644
index 0000000000..35a0f969f9
--- /dev/null
+++ b/spec/ruby/core/kernel/fixtures/warn_require_caller.rb
@@ -0,0 +1,2 @@
+# Use a different line than just 1
+require "#{__dir__}/warn_require"
diff --git a/spec/ruby/core/kernel/fork_spec.rb b/spec/ruby/core/kernel/fork_spec.rb
new file mode 100644
index 0000000000..4a2c848202
--- /dev/null
+++ b/spec/ruby/core/kernel/fork_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/process/fork'
+
+describe "Kernel#fork" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:fork)
+ end
+
+ it_behaves_like :process_fork, :fork, KernelSpecs::Method.new
+end
+
+describe "Kernel.fork" do
+ it_behaves_like :process_fork, :fork, Kernel
+end
diff --git a/spec/ruby/core/kernel/format_spec.rb b/spec/ruby/core/kernel/format_spec.rb
new file mode 100644
index 0000000000..35c752b1ab
--- /dev/null
+++ b/spec/ruby/core/kernel/format_spec.rb
@@ -0,0 +1,47 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# NOTE: most specs are in sprintf_spec.rb, this is just an alias
+describe "Kernel#format" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:format)
+ end
+end
+
+describe "Kernel.format" do
+ it "is accessible as a module function" do
+ Kernel.format("%s", "hello").should == "hello"
+ end
+
+ describe "when $VERBOSE is true" do
+ it "warns if too many arguments are passed" do
+ code = <<~RUBY
+ $VERBOSE = true
+ format("test", 1)
+ RUBY
+
+ ruby_exe(code, args: "2>&1").should.include?("warning: too many arguments for format string")
+ end
+
+ it "does not warns if too many keyword arguments are passed" do
+ code = <<~RUBY
+ $VERBOSE = true
+ format("test %{test}", test: 1, unused: 2)
+ RUBY
+
+ ruby_exe(code, args: "2>&1").should_not.include?("warning")
+ end
+
+ ruby_bug "#20593", ""..."3.4" do
+ it "doesn't warns if keyword arguments are passed and none are used" do
+ code = <<~RUBY
+ $VERBOSE = true
+ format("test", test: 1)
+ format("test", {})
+ RUBY
+
+ ruby_exe(code, args: "2>&1").should_not.include?("warning")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/freeze_spec.rb b/spec/ruby/core/kernel/freeze_spec.rb
new file mode 100644
index 0000000000..079617dce4
--- /dev/null
+++ b/spec/ruby/core/kernel/freeze_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#freeze" do
+ it "prevents self from being further modified" do
+ o = mock('o')
+ o.frozen?.should == false
+ o.freeze
+ o.frozen?.should == true
+ end
+
+ it "returns self" do
+ o = Object.new
+ o.freeze.should.equal?(o)
+ end
+
+ describe "on integers" do
+ it "has no effect since they are already frozen" do
+ 1.frozen?.should == true
+ 1.freeze
+
+ bignum = bignum_value
+ bignum.frozen?.should == true
+ bignum.freeze
+ end
+ end
+
+ describe "on a Float" do
+ it "has no effect since it is already frozen" do
+ 1.2.frozen?.should == true
+ 1.2.freeze
+ end
+ end
+
+ describe "on a Symbol" do
+ it "has no effect since it is already frozen" do
+ :sym.frozen?.should == true
+ :sym.freeze
+ end
+ end
+
+ describe "on true, false and nil" do
+ it "has no effect since they are already frozen" do
+ nil.frozen?.should == true
+ true.frozen?.should == true
+ false.frozen?.should == true
+
+ nil.freeze
+ true.freeze
+ false.freeze
+ end
+ end
+
+ describe "on a Complex" do
+ it "has no effect since it is already frozen" do
+ c = Complex(1.3, 3.1)
+ c.frozen?.should == true
+ c.freeze
+ end
+ end
+
+ describe "on a Rational" do
+ it "has no effect since it is already frozen" do
+ r = Rational(1, 3)
+ r.frozen?.should == true
+ r.freeze
+ end
+ end
+
+ it "causes mutative calls to raise RuntimeError" do
+ o = Class.new do
+ def mutate; @foo = 1; end
+ end.new
+ o.freeze
+ -> {o.mutate}.should.raise(RuntimeError)
+ end
+
+ it "causes instance_variable_set to raise RuntimeError" do
+ o = Object.new
+ o.freeze
+ -> {o.instance_variable_set(:@foo, 1)}.should.raise(RuntimeError)
+ end
+
+ it "freezes an object's singleton class" do
+ o = Object.new
+ c = o.singleton_class
+ c.frozen?.should == false
+ o.freeze
+ c.frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/kernel/frozen_spec.rb b/spec/ruby/core/kernel/frozen_spec.rb
new file mode 100644
index 0000000000..8b8fad3de7
--- /dev/null
+++ b/spec/ruby/core/kernel/frozen_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#frozen?" do
+ it "returns true if self is frozen" do
+ o = mock('o')
+ p = mock('p')
+ p.freeze
+ o.should_not.frozen?
+ p.should.frozen?
+ end
+
+ describe "on true, false and nil" do
+ it "returns true" do
+ true.frozen?.should == true
+ false.frozen?.should == true
+ nil.frozen?.should == true
+ end
+ end
+
+ describe "on integers" do
+ before :each do
+ @fixnum = 1
+ @bignum = bignum_value
+ end
+
+ it "returns true" do
+ @fixnum.frozen?.should == true
+ @bignum.frozen?.should == true
+ end
+ end
+
+ describe "on a Float" do
+ before :each do
+ @float = 0.1
+ end
+
+ it "returns true" do
+ @float.frozen?.should == true
+ end
+ end
+
+ describe "on a Symbol" do
+ before :each do
+ @symbol = :symbol
+ end
+
+ it "returns true" do
+ @symbol.frozen?.should == true
+ end
+ end
+
+ describe "on a Complex" do
+ it "returns true" do
+ c = Complex(1.3, 3.1)
+ c.frozen?.should == true
+ end
+
+ it "literal returns true" do
+ c = eval "1.3i"
+ c.frozen?.should == true
+ end
+ end
+
+ describe "on a Rational" do
+ it "returns true" do
+ r = Rational(1, 3)
+ r.frozen?.should == true
+ end
+
+ it "literal returns true" do
+ r = eval "1/3r"
+ r.frozen?.should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/gets_spec.rb b/spec/ruby/core/kernel/gets_spec.rb
new file mode 100644
index 0000000000..1b2b36fbb9
--- /dev/null
+++ b/spec/ruby/core/kernel/gets_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#gets" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:gets)
+ end
+
+ it "calls ARGF.gets" do
+ ARGF.should_receive(:gets).and_return("spec")
+ gets.should == "spec"
+ end
+end
+
+describe "Kernel.gets" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/global_variables_spec.rb b/spec/ruby/core/kernel/global_variables_spec.rb
new file mode 100644
index 0000000000..d41bdff49a
--- /dev/null
+++ b/spec/ruby/core/kernel/global_variables_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel.global_variables" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:global_variables)
+ end
+
+ before :all do
+ @i = 0
+ end
+
+ it "finds subset starting with std" do
+ global_variables.grep(/std/).to_set.should >= Set[:$stderr, :$stdin, :$stdout]
+ a = global_variables.size
+ gvar_name = "$foolish_global_var#{@i += 1}"
+ global_variables.include?(gvar_name.to_sym).should == false
+ eval("#{gvar_name} = 1")
+ global_variables.size.should == a+1
+ global_variables.should.include?(gvar_name.to_sym)
+ end
+end
+
+describe "Kernel#global_variables" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/gsub_spec.rb b/spec/ruby/core/kernel/gsub_spec.rb
new file mode 100644
index 0000000000..e05349e2b5
--- /dev/null
+++ b/spec/ruby/core/kernel/gsub_spec.rb
@@ -0,0 +1,96 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# FIXME: These methods exist only when the -n or -p option is passed to
+# ruby, but we currently don't have a way of specifying that.
+ruby_version_is ""..."1.9" do
+ describe "Kernel#gsub" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:gsub)
+ end
+
+ it "raises a TypeError if $_ is not a String" do
+ -> {
+ $_ = 123
+ gsub(/./, "!")
+ }.should.raise(TypeError)
+ end
+
+ it "when matches sets $_ to a new string, leaving the former value unaltered" do
+ orig_value = $_ = "hello"
+ gsub("ello", "ola")
+ $_.should_not.equal?(orig_value)
+ $_.should == "hola"
+ orig_value.should == "hello"
+ end
+
+ it "returns a string with the same contents as $_ after the operation" do
+ $_ = "bye"
+ gsub("non-match", "?").should == "bye"
+
+ orig_value = $_ = "bye"
+ gsub(/$/, "!").should == "bye!"
+ orig_value.should == "bye"
+ end
+
+ it "accepts Regexps as patterns" do
+ $_ = "food"
+ gsub(/.$/, "l")
+ $_.should == "fool"
+ end
+
+ it "accepts Strings as patterns, treated literally" do
+ $_ = "hello, world."
+ gsub(".", "!")
+ $_.should == "hello, world!"
+ end
+
+ it "accepts objects which respond to #to_str as patterns and treats them as strings" do
+ $_ = "hello, world."
+ stringlike = mock(".")
+ stringlike.should_receive(:to_str).and_return(".")
+ gsub(stringlike, "!")
+ $_.should == "hello, world!"
+ end
+ end
+
+ describe "Kernel#gsub with a pattern and replacement" do
+ it "accepts strings for replacement" do
+ $_ = "hello"
+ gsub(/./, ".")
+ $_.should == "....."
+ end
+
+ it "accepts objects which respond to #to_str for replacement" do
+ o = mock("o")
+ o.should_receive(:to_str).and_return("o")
+ $_ = "ping"
+ gsub("i", o)
+ $_.should == "pong"
+ end
+
+ it "replaces \\1 sequences with the regexp's corresponding capture" do
+ $_ = "hello!"
+ gsub(/(.)(.)/, '\2\1')
+ $_.should == "ehll!o"
+ end
+ end
+
+ describe "Kernel#gsub with pattern and block" do
+ it "acts similarly to using $_.gsub" do
+ $_ = "olleh dlrow"
+ gsub(/(\w+)/){ $1.reverse }
+ $_.should == "hello world"
+ end
+ end
+
+ describe "Kernel#gsub!" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:gsub!)
+ end
+ end
+
+ describe "Kernel.gsub!" do
+ it "needs to be reviewed for spec completeness"
+ end
+end
diff --git a/spec/ruby/core/kernel/initialize_clone_spec.rb b/spec/ruby/core/kernel/initialize_clone_spec.rb
new file mode 100644
index 0000000000..0033eadffb
--- /dev/null
+++ b/spec/ruby/core/kernel/initialize_clone_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#initialize_clone" do
+ it "is a private instance method" do
+ Kernel.private_instance_methods(false).should.include?(:initialize_clone)
+ end
+
+ it "returns the receiver" do
+ a = Object.new
+ b = Object.new
+ a.send(:initialize_clone, b).should == a
+ end
+
+ it "calls #initialize_copy" do
+ a = Object.new
+ b = Object.new
+ a.should_receive(:initialize_copy).with(b)
+ a.send(:initialize_clone, b)
+ end
+
+ it "accepts a :freeze keyword argument for obj.clone(freeze: value)" do
+ a = Object.new
+ b = Object.new
+ a.send(:initialize_clone, b, freeze: true).should == a
+ end
+end
diff --git a/spec/ruby/core/kernel/initialize_copy_spec.rb b/spec/ruby/core/kernel/initialize_copy_spec.rb
new file mode 100644
index 0000000000..ebac0c14de
--- /dev/null
+++ b/spec/ruby/core/kernel/initialize_copy_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#initialize_copy" do
+ it "returns self" do
+ obj = Object.new
+ obj.send(:initialize_copy, obj).should.equal?(obj)
+ end
+
+ it "does nothing if the argument is the same as the receiver" do
+ obj = Object.new
+ obj.send(:initialize_copy, obj).should.equal?(obj)
+
+ obj = Object.new.freeze
+ obj.send(:initialize_copy, obj).should.equal?(obj)
+
+ 1.send(:initialize_copy, 1).should.equal?(1)
+ end
+
+ it "raises FrozenError if the receiver is frozen" do
+ -> { Object.new.freeze.send(:initialize_copy, Object.new) }.should.raise(FrozenError)
+ -> { 1.send(:initialize_copy, Object.new) }.should.raise(FrozenError)
+ end
+
+ it "raises TypeError if the objects are of different class" do
+ klass = Class.new
+ sub = Class.new(klass)
+ a = klass.new
+ b = sub.new
+ message = 'initialize_copy should take same class object'
+ -> { a.send(:initialize_copy, b) }.should.raise(TypeError, message)
+ -> { b.send(:initialize_copy, a) }.should.raise(TypeError, message)
+
+ -> { a.send(:initialize_copy, 1) }.should.raise(TypeError, message)
+ -> { a.send(:initialize_copy, 1.0) }.should.raise(TypeError, message)
+ end
+end
diff --git a/spec/ruby/core/kernel/initialize_dup_spec.rb b/spec/ruby/core/kernel/initialize_dup_spec.rb
new file mode 100644
index 0000000000..f144647eb8
--- /dev/null
+++ b/spec/ruby/core/kernel/initialize_dup_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#initialize_dup" do
+ it "is a private instance method" do
+ Kernel.private_instance_methods(false).should.include?(:initialize_dup)
+ end
+
+ it "returns the receiver" do
+ a = Object.new
+ b = Object.new
+ a.send(:initialize_dup, b).should == a
+ end
+
+ it "calls #initialize_copy" do
+ a = Object.new
+ b = Object.new
+ a.should_receive(:initialize_copy).with(b)
+ a.send(:initialize_dup, b)
+ end
+end
diff --git a/spec/ruby/core/kernel/inspect_spec.rb b/spec/ruby/core/kernel/inspect_spec.rb
new file mode 100644
index 0000000000..8fc3ab51cd
--- /dev/null
+++ b/spec/ruby/core/kernel/inspect_spec.rb
@@ -0,0 +1,103 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#inspect" do
+ it "returns a String" do
+ Object.new.inspect.should.instance_of?(String)
+ end
+
+ it "does not call #to_s if it is defined" do
+ # We must use a bare Object here
+ obj = Object.new
+ inspected = obj.inspect
+
+ obj.stub!(:to_s).and_return("to_s'd")
+
+ obj.inspect.should == inspected
+ end
+
+ it "returns a String with the object class and object_id encoded" do
+ obj = Object.new
+ obj.inspect.should =~ /^#<Object:0x[0-9a-f]+>$/
+ end
+
+ it "returns a String for an object without #class method" do
+ obj = Object.new
+ class << obj
+ undef_method :class
+ end
+ obj.inspect.should.is_a?(String)
+ end
+
+ ruby_version_is "4.0" do
+ it "calls #instance_variables_to_inspect private method to know which variables to display" do
+ obj = Object.new
+ obj.instance_eval do
+ @host = "localhost"
+ @user = "root"
+ @password = "hunter2"
+ end
+ obj.singleton_class.class_eval do
+ private def instance_variables_to_inspect = %i[@host @user @does_not_exist]
+ end
+
+ inspected = obj.inspect.sub(/^#<Object:0x[0-9a-f]+/, '#<Object:0x00')
+ inspected.should == '#<Object:0x00 @host="localhost", @user="root">'
+
+ obj = Object.new
+ obj.instance_eval do
+ @host = "localhost"
+ @user = "root"
+ @password = "hunter2"
+ end
+ obj.singleton_class.class_eval do
+ private def instance_variables_to_inspect = []
+ end
+
+ inspected = obj.inspect.sub(/^#<Object:0x[0-9a-f]+/, '#<Object:0x00')
+ inspected.should == "#<Object:0x00>"
+ end
+
+ it "displays all instance variables if #instance_variables_to_inspect returns nil" do
+ obj = Object.new
+ obj.instance_eval do
+ @host = "localhost"
+ @user = "root"
+ @password = "hunter2"
+ end
+ obj.singleton_class.class_eval do
+ private def instance_variables_to_inspect = nil
+ end
+
+ inspected = obj.inspect.sub(/^#<Object:0x[0-9a-f]+/, '#<Object:0x00')
+ inspected.should == %{#<Object:0x00 @host="localhost", @user="root", @password="hunter2">}
+ end
+
+ it "displays all instance variables if #instance_variables_to_inspect is not defined" do
+ obj = BasicObject.new
+ obj.instance_eval do
+ @host = "localhost"
+ @user = "root"
+ @password = "hunter2"
+ end
+ method_inspect = Kernel.instance_method(:inspect)
+
+ inspected = method_inspect.bind(obj).call.sub(/^#<BasicObject:0x[0-9a-f]+/, '#<BasicObject:0x00')
+ inspected.should == %{#<BasicObject:0x00 @host="localhost", @user="root", @password="hunter2">}
+ end
+
+ it "raises an error if #instance_variables_to_inspect returns an invalid value" do
+ obj = Object.new
+ obj.instance_eval do
+ @host = "localhost"
+ @user = "root"
+ @password = "hunter2"
+ end
+ obj.singleton_class.class_eval do
+ private def instance_variables_to_inspect = {}
+ end
+
+ ->{ obj.inspect }.should.raise(TypeError, "Expected #instance_variables_to_inspect to return an Array or nil, but it returned Hash")
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/instance_of_spec.rb b/spec/ruby/core/kernel/instance_of_spec.rb
new file mode 100644
index 0000000000..8d19974aa3
--- /dev/null
+++ b/spec/ruby/core/kernel/instance_of_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#instance_of?" do
+ before :each do
+ @o = KernelSpecs::InstanceClass.new
+ end
+
+ it "returns true if given class is object's class" do
+ @o.instance_of?(KernelSpecs::InstanceClass).should == true
+ [].instance_of?(Array).should == true
+ ''.instance_of?(String).should == true
+ end
+
+ it "returns false if given class is object's ancestor class" do
+ @o.instance_of?(KernelSpecs::AncestorClass).should == false
+ end
+
+ it "returns false if given class is not object's class nor object's ancestor class" do
+ @o.instance_of?(Array).should == false
+ end
+
+ it "returns false if given a Module that is included in object's class" do
+ @o.instance_of?(KernelSpecs::MyModule).should == false
+ end
+
+ it "returns false if given a Module that is included one of object's ancestors only" do
+ @o.instance_of?(KernelSpecs::AncestorModule).should == false
+ end
+
+ it "returns false if given a Module that is not included in object's class" do
+ @o.instance_of?(KernelSpecs::SomeOtherModule).should == false
+ end
+
+ it "raises a TypeError if given an object that is not a Class nor a Module" do
+ -> { @o.instance_of?(Object.new) }.should.raise(TypeError)
+ -> { @o.instance_of?('KernelSpecs::InstanceClass') }.should.raise(TypeError)
+ -> { @o.instance_of?(1) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/kernel/instance_variable_defined_spec.rb b/spec/ruby/core/kernel/instance_variable_defined_spec.rb
new file mode 100644
index 0000000000..512f811393
--- /dev/null
+++ b/spec/ruby/core/kernel/instance_variable_defined_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#instance_variable_defined?" do
+ before do
+ @instance = KernelSpecs::InstanceVariable.new
+ end
+
+ describe "when passed a String" do
+ it "returns false if the instance variable is not defined" do
+ @instance.instance_variable_defined?("@goodbye").should == false
+ end
+
+ it "returns true if the instance variable is defined" do
+ @instance.instance_variable_defined?("@greeting").should == true
+ end
+ end
+
+ describe "when passed a Symbol" do
+ it "returns false if the instance variable is not defined" do
+ @instance.instance_variable_defined?(:@goodbye).should == false
+ end
+
+ it "returns true if the instance variable is defined" do
+ @instance.instance_variable_defined?(:@greeting).should == true
+ end
+ end
+
+ it "raises a TypeError if passed an Object not defining #to_str" do
+ -> do
+ obj = mock("kernel instance_variable_defined?")
+ @instance.instance_variable_defined? obj
+ end.should.raise(TypeError)
+ end
+
+ it "returns false if the instance variable is not defined for different types" do
+ [nil, false, true, 1, 2.0, :test, "test"].each do |obj|
+ obj.instance_variable_defined?("@goodbye").should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/instance_variable_get_spec.rb b/spec/ruby/core/kernel/instance_variable_get_spec.rb
new file mode 100644
index 0000000000..8404c94192
--- /dev/null
+++ b/spec/ruby/core/kernel/instance_variable_get_spec.rb
@@ -0,0 +1,111 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#instance_variable_get" do
+ before :each do
+ @obj = Object.new
+ @obj.instance_variable_set("@test", :test)
+ end
+
+ it "tries to convert the passed argument to a String using #to_str" do
+ obj = mock("to_str")
+ obj.should_receive(:to_str).and_return("@test")
+ @obj.instance_variable_get(obj)
+ end
+
+ it "returns the value of the passed instance variable that is referred to by the conversion result" do
+ obj = mock("to_str")
+ obj.stub!(:to_str).and_return("@test")
+ @obj.instance_variable_get(obj).should == :test
+ end
+
+ it "returns nil when the referred instance variable does not exist" do
+ @obj.instance_variable_get(:@does_not_exist).should == nil
+ end
+
+ it "raises a TypeError when the passed argument does not respond to #to_str" do
+ -> { @obj.instance_variable_get(Object.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the passed argument can't be converted to a String" do
+ obj = mock("to_str")
+ obj.stub!(:to_str).and_return(123)
+ -> { @obj.instance_variable_get(obj) }.should.raise(TypeError)
+ end
+
+ it "raises a NameError when the conversion result does not start with an '@'" do
+ obj = mock("to_str")
+ obj.stub!(:to_str).and_return("test")
+ -> { @obj.instance_variable_get(obj) }.should.raise(NameError)
+ end
+
+ it "raises a NameError when passed just '@'" do
+ obj = mock("to_str")
+ obj.stub!(:to_str).and_return('@')
+ -> { @obj.instance_variable_get(obj) }.should.raise(NameError)
+ end
+end
+
+describe "Kernel#instance_variable_get when passed Symbol" do
+ before :each do
+ @obj = Object.new
+ @obj.instance_variable_set("@test", :test)
+ end
+
+ it "returns the value of the instance variable that is referred to by the passed Symbol" do
+ @obj.instance_variable_get(:@test).should == :test
+ end
+
+ it "raises a NameError when passed :@ as an instance variable name" do
+ -> { @obj.instance_variable_get(:"@") }.should.raise(NameError)
+ end
+
+ it "raises a NameError when the passed Symbol does not start with an '@'" do
+ -> { @obj.instance_variable_get(:test) }.should.raise(NameError)
+ end
+
+ it "raises a NameError when the passed Symbol is an invalid instance variable name" do
+ -> { @obj.instance_variable_get(:"@0") }.should.raise(NameError)
+ end
+
+ it "returns nil or raises for frozen objects" do
+ nil.instance_variable_get(:@foo).should == nil
+ -> { nil.instance_variable_get(:foo) }.should.raise(NameError)
+ :foo.instance_variable_get(:@foo).should == nil
+ end
+end
+
+describe "Kernel#instance_variable_get when passed String" do
+ before :each do
+ @obj = Object.new
+ @obj.instance_variable_set("@test", :test)
+ end
+
+ it "returns the value of the instance variable that is referred to by the passed String" do
+ @obj.instance_variable_get("@test").should == :test
+ end
+
+ it "raises a NameError when the passed String does not start with an '@'" do
+ -> { @obj.instance_variable_get("test") }.should.raise(NameError)
+ end
+
+ it "raises a NameError when the passed String is an invalid instance variable name" do
+ -> { @obj.instance_variable_get("@0") }.should.raise(NameError)
+ end
+
+ it "raises a NameError when passed '@' as an instance variable name" do
+ -> { @obj.instance_variable_get("@") }.should.raise(NameError)
+ end
+end
+
+describe "Kernel#instance_variable_get when passed Integer" do
+ before :each do
+ @obj = Object.new
+ @obj.instance_variable_set("@test", :test)
+ end
+
+ it "raises a TypeError" do
+ -> { @obj.instance_variable_get(10) }.should.raise(TypeError)
+ -> { @obj.instance_variable_get(-10) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/kernel/instance_variable_set_spec.rb b/spec/ruby/core/kernel/instance_variable_set_spec.rb
new file mode 100644
index 0000000000..bf165a824e
--- /dev/null
+++ b/spec/ruby/core/kernel/instance_variable_set_spec.rb
@@ -0,0 +1,105 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#instance_variable_set" do
+ it "sets the value of the specified instance variable" do
+ dog = Class.new do
+ def initialize(p1, p2)
+ @a, @b = p1, p2
+ end
+ end
+ dog.new('cat', 99).instance_variable_set(:@a, 'dog').should == "dog"
+ end
+
+ it "sets the value of the instance variable when no instance variables exist yet" do
+ no_variables = Class.new
+ no_variables.new.instance_variable_set(:@a, "new").should == "new"
+ end
+
+ it "raises a NameError exception if the argument is not of form '@x'" do
+ no_dog = Class.new
+ -> { no_dog.new.instance_variable_set(:c, "cat") }.should.raise(NameError)
+ end
+
+ it "raises a NameError exception if the argument is an invalid instance variable name" do
+ digit_dog = Class.new
+ -> { digit_dog.new.instance_variable_set(:"@0", "cat") }.should.raise(NameError)
+ end
+
+ it "raises a NameError when the argument is '@'" do
+ dog_at = Class.new
+ -> { dog_at.new.instance_variable_set(:"@", "cat") }.should.raise(NameError)
+ end
+
+ it "raises a TypeError if the instance variable name is an Integer" do
+ -> { "".instance_variable_set(1, 2) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the instance variable name is an object that does not respond to to_str" do
+ class KernelSpecs::A; end
+ -> { "".instance_variable_set(KernelSpecs::A.new, 3) }.should.raise(TypeError)
+ end
+
+ it "raises a NameError if the passed object, when coerced with to_str, does not start with @" do
+ class KernelSpecs::B
+ def to_str
+ ":c"
+ end
+ end
+ -> { "".instance_variable_set(KernelSpecs::B.new, 4) }.should.raise(NameError)
+ end
+
+ it "raises a NameError if pass an object that cannot be a symbol" do
+ -> { "".instance_variable_set(:c, 1) }.should.raise(NameError)
+ end
+
+ it "accepts as instance variable name any instance of a class that responds to to_str" do
+ class KernelSpecs::C
+ def initialize
+ @a = 1
+ end
+ def to_str
+ "@a"
+ end
+ end
+ KernelSpecs::C.new.instance_variable_set(KernelSpecs::C.new, 2).should == 2
+ end
+
+ describe "on frozen objects" do
+ before :each do
+ klass = Class.new do
+ attr_reader :ivar
+ def initialize
+ @ivar = :origin
+ end
+ end
+
+ @frozen = klass.new.freeze
+ end
+
+ it "keeps stored object after any exceptions" do
+ -> { @frozen.instance_variable_set(:@ivar, :replacement) }.should.raise(Exception)
+ @frozen.ivar.should.equal?(:origin)
+ end
+
+ it "raises a FrozenError when passed replacement is identical to stored object" do
+ -> { @frozen.instance_variable_set(:@ivar, :origin) }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError when passed replacement is different from stored object" do
+ -> { @frozen.instance_variable_set(:@ivar, :replacement) }.should.raise(FrozenError)
+ end
+
+ it "accepts unicode instance variable names" do
+ o = Object.new
+ o.instance_variable_set(:@💙, 42)
+ o.instance_variable_get(:@💙).should == 42
+ end
+
+ it "raises for frozen objects" do
+ -> { nil.instance_variable_set(:@foo, 42) }.should.raise(FrozenError)
+ -> { nil.instance_variable_set(:foo, 42) }.should.raise(NameError)
+ -> { :foo.instance_variable_set(:@foo, 42) }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/instance_variables_spec.rb b/spec/ruby/core/kernel/instance_variables_spec.rb
new file mode 100644
index 0000000000..75c1d8f752
--- /dev/null
+++ b/spec/ruby/core/kernel/instance_variables_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#instance_variables" do
+ describe "immediate values" do
+ it "returns an empty array if no instance variables are defined" do
+ [0, 0.5, true, false, nil].each do |value|
+ value.instance_variables.should == []
+ end
+ end
+
+ it "returns the correct array if an instance variable is added" do
+ a = 0
+ ->{ a.instance_variable_set("@test", 1) }.should.raise(RuntimeError)
+ end
+ end
+
+ describe "regular objects" do
+ it "returns an empty array if no instance variables are defined" do
+ Object.new.instance_variables.should == []
+ end
+
+ it "returns the correct array if an instance variable is added" do
+ a = Object.new
+ a.instance_variable_set("@test", 1)
+ a.instance_variables.should == [:@test]
+ end
+
+ it "returns the instances variables in the order declared" do
+ c = Class.new do
+ def initialize
+ @c = 1
+ @a = 2
+ @b = 3
+ end
+ end
+ c.new.instance_variables.should == [:@c, :@a, :@b]
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/is_a_spec.rb b/spec/ruby/core/kernel/is_a_spec.rb
new file mode 100644
index 0000000000..bd8c96529a
--- /dev/null
+++ b/spec/ruby/core/kernel/is_a_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/kind_of'
+
+describe "Kernel#is_a?" do
+ it_behaves_like :kernel_kind_of, :is_a?
+end
diff --git a/spec/ruby/core/kernel/itself_spec.rb b/spec/ruby/core/kernel/itself_spec.rb
new file mode 100644
index 0000000000..c1f01fdc5e
--- /dev/null
+++ b/spec/ruby/core/kernel/itself_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#itself" do
+ it "returns the receiver itself" do
+ foo = Object.new
+ foo.itself.should.equal? foo
+ end
+end
diff --git a/spec/ruby/core/kernel/kind_of_spec.rb b/spec/ruby/core/kernel/kind_of_spec.rb
new file mode 100644
index 0000000000..c988edccb5
--- /dev/null
+++ b/spec/ruby/core/kernel/kind_of_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/kind_of'
+
+describe "Kernel#kind_of?" do
+ it_behaves_like :kernel_kind_of, :kind_of?
+end
diff --git a/spec/ruby/core/kernel/lambda_spec.rb b/spec/ruby/core/kernel/lambda_spec.rb
new file mode 100644
index 0000000000..6ab89c2bbb
--- /dev/null
+++ b/spec/ruby/core/kernel/lambda_spec.rb
@@ -0,0 +1,110 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/lambda'
+
+# The functionality of lambdas is specified in core/proc
+
+describe "Kernel.lambda" do
+ it_behaves_like :kernel_lambda, :lambda
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:lambda)
+ end
+
+ it "creates a lambda-style Proc if given a literal block" do
+ l = lambda { 42 }
+ l.lambda?.should == true
+ end
+
+ it "creates a lambda-style Proc if given a literal block via #send" do
+ l = send(:lambda) { 42 }
+ l.lambda?.should == true
+ end
+
+ it "creates a lambda-style Proc if given a literal block via #__send__" do
+ l = __send__(:lambda) { 42 }
+ l.lambda?.should == true
+ end
+
+ it "checks the arity of the call when no args are specified" do
+ l = lambda { :called }
+ l.call.should == :called
+
+ lambda { l.call(1) }.should.raise(ArgumentError)
+ lambda { l.call(1, 2) }.should.raise(ArgumentError)
+ end
+
+ it "checks the arity when 1 arg is specified" do
+ l = lambda { |a| :called }
+ l.call(1).should == :called
+
+ lambda { l.call }.should.raise(ArgumentError)
+ lambda { l.call(1, 2) }.should.raise(ArgumentError)
+ end
+
+ it "does not check the arity when passing a Proc with &" do
+ l = lambda { || :called }
+ p = proc { || :called }
+
+ lambda { l.call(1) }.should.raise(ArgumentError)
+ p.call(1).should == :called
+ end
+
+ it "accepts 0 arguments when used with ||" do
+ lambda {
+ lambda { || }.call(1)
+ }.should.raise(ArgumentError)
+ end
+
+ it "strictly checks the arity when 0 or 2..inf args are specified" do
+ l = lambda { |a,b| }
+
+ lambda {
+ l.call
+ }.should.raise(ArgumentError)
+
+ lambda {
+ l.call(1)
+ }.should.raise(ArgumentError)
+
+ lambda {
+ l.call(1,2)
+ }.should_not.raise(ArgumentError)
+ end
+
+ it "returns from the lambda itself, not the creation site of the lambda" do
+ @reached_end_of_method = nil
+ def test
+ send(:lambda) { return }.call
+ @reached_end_of_method = true
+ end
+ test
+ @reached_end_of_method.should == true
+ end
+
+ it "allows long returns to flow through it" do
+ KernelSpecs::Lambda.new.outer.should == :good
+ end
+
+ it "treats the block as a Proc when lambda is re-defined" do
+ klass = Class.new do
+ def lambda (&block); block; end
+ def ret
+ lambda { return 1 }.call
+ 2
+ end
+ end
+ klass.new.lambda { 42 }.should.instance_of? Proc
+ klass.new.ret.should == 1
+ end
+
+ context "when called without a literal block" do
+ it "raises when proc isn't a lambda" do
+ -> { lambda(&proc{}) }.should.raise(ArgumentError, /the lambda method requires a literal block/)
+ end
+
+ it "doesn't warn when proc is lambda" do
+ -> { lambda(&lambda{}) }.should_not complain(verbose: true)
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/load_spec.rb b/spec/ruby/core/kernel/load_spec.rb
new file mode 100644
index 0000000000..890aab8c27
--- /dev/null
+++ b/spec/ruby/core/kernel/load_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/code_loading'
+require_relative 'shared/load'
+require_relative 'shared/require'
+
+describe "Kernel#load" do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:load)
+ end
+
+ it_behaves_like :kernel_require_basic, :load, CodeLoadingSpecs::Method.new
+end
+
+describe "Kernel#load" do
+ it_behaves_like :kernel_load, :load, CodeLoadingSpecs::Method.new
+end
+
+describe "Kernel.load" do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ it_behaves_like :kernel_require_basic, :load, Kernel
+end
+
+describe "Kernel.load" do
+ it_behaves_like :kernel_load, :load, Kernel
+end
diff --git a/spec/ruby/core/kernel/local_variables_spec.rb b/spec/ruby/core/kernel/local_variables_spec.rb
new file mode 100644
index 0000000000..40c343f7e4
--- /dev/null
+++ b/spec/ruby/core/kernel/local_variables_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#local_variables" do
+ after :each do
+ ScratchPad.clear
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:local_variables)
+ end
+
+ it "contains locals as they are added" do
+ a = 1
+ b = 2
+ local_variables.sort.should == [:a, :b]
+ end
+
+ it "is accessible from bindings" do
+ def local_var_foo
+ a = 1
+ b = 2
+ binding
+ end
+ foo_binding = local_var_foo()
+ res = eval("local_variables",foo_binding)
+ res.sort.should == [:a, :b]
+ end
+
+ it "is accessible in eval" do
+ eval "a=1; b=2; ScratchPad.record local_variables"
+ ScratchPad.recorded.sort.should == [:a, :b]
+ end
+
+ it "includes only unique variable names" do
+ def local_var_method
+ a = 1
+ 1.times do |;a|
+ return local_variables
+ end
+ end
+
+ local_var_method.should == [:a]
+ end
+end
diff --git a/spec/ruby/core/kernel/loop_spec.rb b/spec/ruby/core/kernel/loop_spec.rb
new file mode 100644
index 0000000000..c2976e5cc5
--- /dev/null
+++ b/spec/ruby/core/kernel/loop_spec.rb
@@ -0,0 +1,79 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel.loop" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:loop)
+ end
+
+ it "calls block until it is terminated by a break" do
+ i = 0
+ loop do
+ i += 1
+ break if i == 10
+ end
+
+ i.should == 10
+ end
+
+ it "returns value passed to break" do
+ loop do
+ break 123
+ end.should == 123
+ end
+
+ it "returns nil if no value passed to break" do
+ loop do
+ break
+ end.should == nil
+ end
+
+ it "returns an enumerator if no block given" do
+ enum = loop
+ enum.instance_of?(Enumerator).should == true
+ cnt = 0
+ enum.each do |*args|
+ raise "Args should be empty #{args.inspect}" unless args.empty?
+ cnt += 1
+ break cnt if cnt >= 42
+ end.should == 42
+ end
+
+ it "rescues StopIteration" do
+ loop do
+ raise StopIteration
+ end
+ 42.should == 42
+ end
+
+ it "rescues StopIteration's subclasses" do
+ finish = Class.new StopIteration
+ loop do
+ raise finish
+ end
+ 42.should == 42
+ end
+
+ it "does not rescue other errors" do
+ ->{ loop do raise StandardError end }.should.raise( StandardError )
+ end
+
+ it "returns StopIteration#result, the result value of a finished iterator" do
+ e = Enumerator.new { |y|
+ y << 1
+ y << 2
+ :stopped
+ }
+ loop { e.next }.should == :stopped
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "returns Float::INFINITY" do
+ loop.size.should == Float::INFINITY
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/match_spec.rb b/spec/ruby/core/kernel/match_spec.rb
new file mode 100644
index 0000000000..cd6330fe91
--- /dev/null
+++ b/spec/ruby/core/kernel/match_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#=~" do
+ it "is no longer defined" do
+ Object.new.should_not.respond_to?(:=~)
+ end
+end
diff --git a/spec/ruby/core/kernel/method_spec.rb b/spec/ruby/core/kernel/method_spec.rb
new file mode 100644
index 0000000000..9187b8c7e7
--- /dev/null
+++ b/spec/ruby/core/kernel/method_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+require_relative 'shared/method'
+require_relative 'fixtures/classes'
+
+describe "Kernel#method" do
+ it_behaves_like :kernel_method, :method
+
+ before :each do
+ @obj = KernelSpecs::A.new
+ end
+
+ it "can be called on a private method" do
+ @obj.send(:private_method).should == :private_method
+ @obj.method(:private_method).should.instance_of?(Method)
+ end
+
+ it "can be called on a protected method" do
+ @obj.send(:protected_method).should == :protected_method
+ @obj.method(:protected_method).should.instance_of?(Method)
+ end
+
+ it "will see an alias of the original method as == when in a derived class" do
+ obj = KernelSpecs::B.new
+ obj.method(:aliased_pub_method).should == obj.method(:pub_method)
+ end
+
+ it "can call methods created with define_method" do
+ m = @obj.method(:defined_method)
+ m.call.should == :defined
+ end
+
+ it "can be called even if we only respond_to_missing? method, true" do
+ m = KernelSpecs::RespondViaMissing.new.method(:handled_privately)
+ m.should.instance_of?(Method)
+ m.call(1, 2, 3).should == "Done handled_privately([1, 2, 3])"
+ end
+
+ it "can call a #method_missing accepting zero or one arguments" do
+ cls = Class.new do
+ def respond_to_missing?(name, *)
+ name == :foo or super
+ end
+ def method_missing
+ :no_args
+ end
+ end
+ m = cls.new.method(:foo)
+ -> { m.call }.should.raise(ArgumentError)
+
+ cls = Class.new do
+ def respond_to_missing?(name, *)
+ name == :bar or super
+ end
+ def method_missing(m)
+ m
+ end
+ end
+ m = cls.new.method(:bar)
+ m.call.should == :bar
+ end
+
+ describe "converts the given name to a String using #to_str" do
+ it "calls #to_str to convert the given name to a String" do
+ name = mock("method-name")
+ name.should_receive(:to_str).and_return("hash")
+ Object.method(name).should == Object.method(:hash)
+ end
+
+ it "raises a TypeError if the given name can't be converted to a String" do
+ -> { Object.method(nil) }.should.raise(TypeError)
+ -> { Object.method([]) }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do
+ name = mock("method-name")
+ name.should_receive(:to_str).and_raise(NoMethodError)
+ -> { Object.method(name) }.should.raise(NoMethodError)
+
+ name = mock("method-name")
+ name.should_receive(:to_str).and_raise(NoMethodError)
+ begin
+ raise RuntimeError.new
+ rescue => cause
+ -> { Object.method(name) }.should.raise(NoMethodError, cause:)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/methods_spec.rb b/spec/ruby/core/kernel/methods_spec.rb
new file mode 100644
index 0000000000..cfa22aa05a
--- /dev/null
+++ b/spec/ruby/core/kernel/methods_spec.rb
@@ -0,0 +1,101 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+describe "Kernel#methods" do
+ it "returns singleton methods defined by obj.meth" do
+ KernelSpecs::Methods.methods(false).should.include?(:ichi)
+ end
+
+ it "returns singleton methods defined in 'class << self'" do
+ KernelSpecs::Methods.methods(false).should.include?(:san)
+ end
+
+ it "returns private singleton methods defined by obj.meth" do
+ KernelSpecs::Methods.methods(false).should.include?(:shi)
+ end
+
+ it "returns singleton methods defined in 'class << self' when it follows 'private'" do
+ KernelSpecs::Methods.methods(false).should.include?(:roku)
+ end
+
+ it "does not return private singleton methods defined in 'class << self'" do
+ KernelSpecs::Methods.methods(false).should_not.include?(:shichi)
+ end
+
+ it "returns the publicly accessible methods of the object" do
+ meths = KernelSpecs::Methods.methods(false)
+ meths.to_set.should >= Set[:hachi, :ichi, :juu, :juu_ichi,
+ :juu_ni, :roku, :san, :shi]
+
+ KernelSpecs::Methods.new.methods(false).should == []
+ end
+
+ it "returns the publicly accessible methods in the object, its ancestors and mixed-in modules" do
+ meths = KernelSpecs::Methods.methods(false) & KernelSpecs::Methods.methods
+ meths.to_set.should >= Set[:hachi, :ichi, :juu, :juu_ichi,
+ :juu_ni, :roku, :san, :shi]
+
+ KernelSpecs::Methods.new.methods.to_set.should >= Set[:ku, :ni, :juu_san]
+ end
+
+ it "returns methods added to the metaclass through extend" do
+ meth = KernelSpecs::Methods.new
+ meth.methods.should_not.include?(:peekaboo)
+ meth.extend(KernelSpecs::Methods::MetaclassMethods)
+ meth.methods.should.include?(:peekaboo)
+ end
+
+ it "does not return undefined singleton methods defined by obj.meth" do
+ o = KernelSpecs::Child.new
+ def o.single; end
+ o.methods.should.include?(:single)
+
+ class << o; self; end.send :undef_method, :single
+ o.methods.should_not.include?(:single)
+ end
+
+ it "does not return superclass methods undefined in the object's class" do
+ KernelSpecs::Child.new.methods.should_not.include?(:parent_method)
+ end
+
+ it "does not return superclass methods undefined in a superclass" do
+ KernelSpecs::Grandchild.new.methods.should_not.include?(:parent_method)
+ end
+
+ it "does not return included module methods undefined in the object's class" do
+ KernelSpecs::Grandchild.new.methods.should_not.include?(:parent_mixin_method)
+ end
+end
+
+describe :kernel_methods_supers, shared: true do
+ before :all do
+ @ms = [:pro, :pub]
+ end
+
+ it "returns a unique list for an object extended by a module" do
+ m = ReflectSpecs.oed.methods(*@object)
+ m.select { |x| @ms.include? x }.sort.should == @ms
+ end
+
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.new.methods(*@object)
+ m.select { |x| @ms.include? x }.sort.should == @ms
+ end
+
+ it "returns a unique list for a subclass of a class that includes a module" do
+ m = ReflectSpecs::E.new.methods(*@object)
+ m.select { |x| @ms.include? x }.sort.should == @ms
+ end
+end
+
+describe "Kernel#methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :kernel_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :kernel_methods_supers, nil, true
+ end
+end
diff --git a/spec/ruby/core/kernel/nil_spec.rb b/spec/ruby/core/kernel/nil_spec.rb
new file mode 100644
index 0000000000..7418245f26
--- /dev/null
+++ b/spec/ruby/core/kernel/nil_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Kernel#nil?' do
+ it 'returns false' do
+ Object.should_not.nil?
+ Object.new.should_not.nil?
+ ''.should_not.nil?
+ [].should_not.nil?
+ {}.should_not.nil?
+ end
+end
diff --git a/spec/ruby/core/kernel/not_match_spec.rb b/spec/ruby/core/kernel/not_match_spec.rb
new file mode 100644
index 0000000000..4ed7ea7b42
--- /dev/null
+++ b/spec/ruby/core/kernel/not_match_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#!~" do
+ class KernelSpecs::NotMatch
+ def !~(obj)
+ :foo
+ end
+ end
+
+ it 'calls =~ internally and negates the result' do
+ obj = Object.new
+ obj.should_receive(:=~).and_return(true)
+ (obj !~ :foo).should == false
+ end
+
+ it "raises NoMethodError if self does not respond to #=~" do
+ -> { Object.new !~ :foo }.should.raise(NoMethodError)
+ end
+
+ it 'can be overridden in subclasses' do
+ obj = KernelSpecs::NotMatch.new
+ (obj !~ :bar).should == :foo
+ end
+end
diff --git a/spec/ruby/core/kernel/object_id_spec.rb b/spec/ruby/core/kernel/object_id_spec.rb
new file mode 100644
index 0000000000..ef9e80c831
--- /dev/null
+++ b/spec/ruby/core/kernel/object_id_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/object_id'
+
+describe "Kernel#object_id" do
+ it_behaves_like :object_id, :object_id, Object
+end
diff --git a/spec/ruby/core/kernel/open_spec.rb b/spec/ruby/core/kernel/open_spec.rb
new file mode 100644
index 0000000000..14a3c43cad
--- /dev/null
+++ b/spec/ruby/core/kernel/open_spec.rb
@@ -0,0 +1,192 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#open" do
+ before :each do
+ @name = tmp("kernel_open.txt")
+ @content = "This is a test"
+ touch(@name) { |f| f.write @content }
+ @file = nil
+ end
+
+ after :each do
+ @file.close if @file
+ rm_r @name
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:open)
+ end
+
+ it "opens a file when given a valid filename" do
+ @file = open(@name)
+ @file.should.is_a?(File)
+ end
+
+ it "opens a file when called with a block" do
+ open(@name, "r") { |f| f.gets }.should == @content
+ end
+
+ ruby_version_is ""..."4.0" do
+ platform_is_not :windows, :wasi do
+ it "opens an io when path starts with a pipe" do
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ @io = open("|date")
+ end
+ begin
+ @io.should.is_a?(IO)
+ @io.read
+ ensure
+ @io.close
+ end
+ end
+
+ it "opens an io when called with a block" do
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ @output = open("|date") { |f| f.read }
+ end
+ @output.should_not == ''
+ end
+
+ it "opens an io for writing" do
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ -> {
+ bytes = open("|cat", "w") { |io| io.write(".") }
+ bytes.should == 1
+ }.should output_to_fd(".")
+ end
+ end
+ end
+
+ platform_is :windows do
+ it "opens an io when path starts with a pipe" do
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ @io = open("|date /t")
+ end
+ begin
+ @io.should.is_a?(IO)
+ @io.read
+ ensure
+ @io.close
+ end
+ end
+
+ it "opens an io when called with a block" do
+ suppress_warning do # https://bugs.ruby-lang.org/issues/19630
+ @output = open("|date /t") { |f| f.read }
+ end
+ @output.should_not == ''
+ end
+ end
+
+ # https://bugs.ruby-lang.org/issues/19630
+ it "warns about deprecation given a path with a pipe" do
+ cmd = "|echo ok"
+ -> {
+ open(cmd) { |f| f.read }
+ }.should complain(/Kernel#open with a leading '\|'/)
+ end
+ end
+
+ it "raises an ArgumentError if not passed one argument" do
+ -> { open }.should.raise(ArgumentError)
+ end
+
+ it "accepts options as keyword arguments" do
+ @file = open(@name, "r", 0666, flags: File::CREAT)
+ @file.should.is_a?(File)
+
+ -> {
+ open(@name, "r", 0666, {flags: File::CREAT})
+ }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)")
+ end
+
+ describe "when given an object that responds to to_open" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "calls #to_path to convert the argument to a String before calling #to_str" do
+ obj = mock("open to_path")
+ obj.should_receive(:to_path).at_least(1).times.and_return(@name)
+ obj.should_not_receive(:to_str)
+
+ open(obj, "r") { |f| f.gets }.should == @content
+ end
+
+ it "calls #to_str to convert the argument to a String" do
+ obj = mock("open to_str")
+ obj.should_receive(:to_str).at_least(1).times.and_return(@name)
+
+ open(obj, "r") { |f| f.gets }.should == @content
+ end
+
+ it "calls #to_open on argument" do
+ obj = mock('fileish')
+ @file = File.open(@name)
+ obj.should_receive(:to_open).and_return(@file)
+ @file = open(obj)
+ @file.should.is_a?(File)
+ end
+
+ it "returns the value from #to_open" do
+ obj = mock('to_open')
+ obj.should_receive(:to_open).and_return(:value)
+
+ open(obj).should == :value
+ end
+
+ it "passes its arguments onto #to_open" do
+ obj = mock('to_open')
+ obj.should_receive(:to_open).with(1, 2, 3)
+ open(obj, 1, 2, 3)
+ end
+
+ it "passes keyword arguments onto #to_open as keyword arguments if to_open accepts them" do
+ obj = Object.new
+ def obj.to_open(*args, **kw)
+ ScratchPad << {args: args, kw: kw}
+ end
+
+ ScratchPad.record []
+ open(obj, 1, 2, 3, a: "b")
+ ScratchPad.recorded.should == [args: [1, 2, 3], kw: {a: "b"}]
+ end
+
+ it "passes the return value from #to_open to a block" do
+ obj = mock('to_open')
+ obj.should_receive(:to_open).and_return(:value)
+
+ open(obj) do |mock|
+ ScratchPad.record(mock)
+ end
+
+ ScratchPad.recorded.should == :value
+ end
+ end
+
+ it "raises a TypeError if passed a non-String that does not respond to #to_open" do
+ obj = mock('non-fileish')
+ -> { open(obj) }.should.raise(TypeError)
+ -> { open(nil) }.should.raise(TypeError)
+ -> { open(7) }.should.raise(TypeError)
+ end
+
+ it "accepts nil for mode and permission" do
+ open(@name, nil, nil) { |f| f.gets }.should == @content
+ end
+
+ it "is not redefined by open-uri" do
+ code = <<~RUBY
+ before = Kernel.instance_method(:open)
+ require 'open-uri'
+ after = Kernel.instance_method(:open)
+ p before == after
+ RUBY
+ ruby_exe(code, args: "2>&1").should == "true\n"
+ end
+end
+
+describe "Kernel.open" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/p_spec.rb b/spec/ruby/core/kernel/p_spec.rb
new file mode 100644
index 0000000000..f89716899a
--- /dev/null
+++ b/spec/ruby/core/kernel/p_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#p" do
+ before :all do
+ @rs_f, @rs_b, @rs_c = $/, $\, $,
+ end
+
+ after :each do
+ suppress_warning {
+ $/, $\, $, = @rs_f, @rs_b, @rs_c
+ }
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:p)
+ end
+
+ # TODO: fix
+ it "flushes output if receiver is a File" do
+ filename = tmp("Kernel_p_flush") + $$.to_s
+ begin
+ File.open(filename, "w") do |f|
+ begin
+ old_stdout = $stdout
+ $stdout = f
+ p("abcde")
+ ensure
+ $stdout = old_stdout
+ end
+
+ File.open(filename) do |f2|
+ f2.read(7).should == "\"abcde\""
+ end
+ end
+ ensure
+ rm_r filename
+ end
+ end
+
+ it "prints obj.inspect followed by system record separator for each argument given" do
+ o = mock("Inspector Gadget")
+ o.should_receive(:inspect).any_number_of_times.and_return "Next time, Gadget, NEXT TIME!"
+
+ -> { p(o) }.should output("Next time, Gadget, NEXT TIME!\n")
+ -> { p(*[o]) }.should output("Next time, Gadget, NEXT TIME!\n")
+ -> { p(*[o, o]) }.should output("Next time, Gadget, NEXT TIME!\nNext time, Gadget, NEXT TIME!\n")
+ -> { p([o])}.should output("[#{o.inspect}]\n")
+ end
+
+ it "is not affected by setting $\\, $/ or $," do
+ o = mock("Inspector Gadget")
+ o.should_receive(:inspect).any_number_of_times.and_return "Next time, Gadget, NEXT TIME!"
+
+ suppress_warning {
+ $, = " *helicopter sound*\n"
+ }
+ -> { p(o) }.should output_to_fd("Next time, Gadget, NEXT TIME!\n")
+
+ suppress_warning {
+ $\ = " *helicopter sound*\n"
+ }
+ -> { p(o) }.should output_to_fd("Next time, Gadget, NEXT TIME!\n")
+
+ suppress_warning {
+ $/ = " *helicopter sound*\n"
+ }
+ -> { p(o) }.should output_to_fd("Next time, Gadget, NEXT TIME!\n")
+ end
+
+ it "prints nothing if no argument is given" do
+ -> { p }.should output("")
+ end
+
+ it "prints nothing if called splatting an empty Array" do
+ -> { p(*[]) }.should output("")
+ end
+
+ # Not sure how to spec this, but wanted to note the behavior here
+ it "does not flush if receiver is not a TTY or a File"
+end
+
+describe "Kernel.p" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/pp_spec.rb b/spec/ruby/core/kernel/pp_spec.rb
new file mode 100644
index 0000000000..b5b1c98d38
--- /dev/null
+++ b/spec/ruby/core/kernel/pp_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#pp" do
+ it "lazily loads the 'pp' library and delegates the call to that library" do
+ # Run in child process to ensure 'pp' hasn't been loaded yet.
+ output = ruby_exe("pp [1, 2, 3]")
+ output.should == "[1, 2, 3]\n"
+ end
+end
diff --git a/spec/ruby/core/kernel/print_spec.rb b/spec/ruby/core/kernel/print_spec.rb
new file mode 100644
index 0000000000..5473d22f71
--- /dev/null
+++ b/spec/ruby/core/kernel/print_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#print" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:print)
+ end
+
+ it "delegates to $stdout" do
+ -> { print :arg }.should output("arg")
+ end
+
+ it "prints $_ when no arguments are given" do
+ orig_value = $_
+ $_ = 'foo'
+ -> { print }.should output("foo")
+ ensure
+ $_ = orig_value
+ end
+end
+
+describe "Kernel.print" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/printf_spec.rb b/spec/ruby/core/kernel/printf_spec.rb
new file mode 100644
index 0000000000..50939b3794
--- /dev/null
+++ b/spec/ruby/core/kernel/printf_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/sprintf'
+
+describe "Kernel#printf" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:printf)
+ end
+end
+
+describe "Kernel.printf" do
+ before :each do
+ @stdout = $stdout
+ @name = tmp("kernel_puts.txt")
+ $stdout = new_io @name
+ end
+
+ after :each do
+ $stdout.close
+ $stdout = @stdout
+ rm_r @name
+ end
+
+ it "writes to stdout when a string is the first argument" do
+ $stdout.should_receive(:write).with("string")
+ Kernel.printf("%s", "string")
+ end
+
+ it "calls write on the first argument when it is not a string" do
+ object = mock('io')
+ object.should_receive(:write).with("string")
+ Kernel.printf(object, "%s", "string")
+ end
+
+ it "calls #to_str to convert the format object to a String" do
+ object = mock('format string')
+ object.should_receive(:to_str).and_return("to_str: %i")
+ $stdout.should_receive(:write).with("to_str: 42")
+ Kernel.printf($stdout, object, 42)
+ end
+end
+
+describe "Kernel.printf" do
+ describe "formatting" do
+ before :each do
+ require "stringio"
+ end
+
+ context "io is specified" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ io = StringIO.new(+"")
+ Kernel.printf(io, format, *args)
+ io.string
+ }
+ end
+
+ context "io is not specified" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ stdout = $stdout
+ begin
+ $stdout = io = StringIO.new(+"")
+ Kernel.printf(format, *args)
+ io.string
+ ensure
+ $stdout = stdout
+ end
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/private_methods_spec.rb b/spec/ruby/core/kernel/private_methods_spec.rb
new file mode 100644
index 0000000000..b0e6d042a9
--- /dev/null
+++ b/spec/ruby/core/kernel/private_methods_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+describe "Kernel#private_methods" do
+ it "returns a list of the names of privately accessible methods in the object" do
+ m = KernelSpecs::Methods.private_methods(false)
+ m.should.include?(:shichi)
+ m = KernelSpecs::Methods.new.private_methods(false)
+ m.should.include?(:juu_shi)
+ end
+
+ it "returns a list of the names of privately accessible methods in the object and its ancestors and mixed-in modules" do
+ m = (KernelSpecs::Methods.private_methods(false) & KernelSpecs::Methods.private_methods)
+
+ m.should.include?(:shichi)
+ m = KernelSpecs::Methods.new.private_methods
+ m.should.include?(:juu_shi)
+ end
+
+ it "returns private methods mixed in to the metaclass" do
+ m = KernelSpecs::Methods.new
+ m.extend(KernelSpecs::Methods::MetaclassMethods)
+ m.private_methods.should.include?(:shoo)
+ end
+end
+
+describe :kernel_private_methods_supers, shared: true do
+ it "returns a unique list for an object extended by a module" do
+ m = ReflectSpecs.oed.private_methods(*@object)
+ m.select { |x| x == :pri }.sort.should == [:pri]
+ end
+
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.new.private_methods(*@object)
+ m.select { |x| x == :pri }.sort.should == [:pri]
+ end
+
+ it "returns a unique list for a subclass of a class that includes a module" do
+ m = ReflectSpecs::E.new.private_methods(*@object)
+ m.select { |x| x == :pri }.sort.should == [:pri]
+ end
+end
+
+describe :kernel_private_methods_with_falsy, shared: true do
+ it "returns a list of private methods in without its ancestors" do
+ ReflectSpecs::F.private_methods(@object).select{|m|/_pri\z/ =~ m}.sort.should == [:ds_pri, :fs_pri]
+ ReflectSpecs::F.new.private_methods(@object).should == [:f_pri]
+ end
+end
+
+describe "Kernel#private_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :kernel_private_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :kernel_private_methods_supers, nil, true
+ end
+
+ describe "when passed false" do
+ it_behaves_like :kernel_private_methods_with_falsy, nil, false
+ end
+
+ describe "when passed nil" do
+ it_behaves_like :kernel_private_methods_with_falsy, nil, nil
+ end
+end
diff --git a/spec/ruby/core/kernel/proc_spec.rb b/spec/ruby/core/kernel/proc_spec.rb
new file mode 100644
index 0000000000..1ba662177b
--- /dev/null
+++ b/spec/ruby/core/kernel/proc_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/lambda'
+
+# The functionality of Proc objects is specified in core/proc
+
+describe "Kernel.proc" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:proc)
+ end
+
+ it "creates a proc-style Proc if given a literal block" do
+ l = proc { 42 }
+ l.lambda?.should == false
+ end
+
+ it "returned the passed Proc if given an existing Proc" do
+ some_lambda = -> {}
+ some_lambda.lambda?.should == true
+ l = proc(&some_lambda)
+ l.should.equal?(some_lambda)
+ l.lambda?.should == true
+ end
+
+ it_behaves_like :kernel_lambda, :proc
+
+ it "returns from the creation site of the proc, not just the proc itself" do
+ @reached_end_of_method = nil
+ def test
+ proc { return }.call
+ @reached_end_of_method = true
+ end
+ test
+ @reached_end_of_method.should == nil
+ end
+end
+
+describe "Kernel#proc" do
+ def some_method
+ proc
+ end
+
+ it "raises an ArgumentError when passed no block" do
+ -> {
+ some_method { "hello" }
+ }.should.raise(ArgumentError, 'tried to create Proc object without a block')
+ end
+end
diff --git a/spec/ruby/core/kernel/protected_methods_spec.rb b/spec/ruby/core/kernel/protected_methods_spec.rb
new file mode 100644
index 0000000000..aecb9f5ffb
--- /dev/null
+++ b/spec/ruby/core/kernel/protected_methods_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+
+# The reason why having include() is to show the specification explicitly.
+# You should use have_protected_method() with the exception of this spec.
+describe "Kernel#protected_methods" do
+ it "returns a list of the names of protected methods accessible in the object" do
+ KernelSpecs::Methods.protected_methods(false).sort.should.include?(:juu_ichi)
+ KernelSpecs::Methods.new.protected_methods(false).should.include?(:ku)
+ end
+
+ it "returns a list of the names of protected methods accessible in the object and from its ancestors and mixed-in modules" do
+ l1 = KernelSpecs::Methods.protected_methods(false)
+ l2 = KernelSpecs::Methods.protected_methods
+ (l1 & l2).should.include?(:juu_ichi)
+ KernelSpecs::Methods.new.protected_methods.should.include?(:ku)
+ end
+
+ it "returns methods mixed in to the metaclass" do
+ m = KernelSpecs::Methods.new
+ m.extend(KernelSpecs::Methods::MetaclassMethods)
+ m.protected_methods.should.include?(:nopeeking)
+ end
+end
+
+describe :kernel_protected_methods_supers, shared: true do
+ it "returns a unique list for an object extended by a module" do
+ m = ReflectSpecs.oed.protected_methods(*@object)
+ m.select { |x| x == :pro }.sort.should == [:pro]
+ end
+
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.new.protected_methods(*@object)
+ m.select { |x| x == :pro }.sort.should == [:pro]
+ end
+
+ it "returns a unique list for a subclass of a class that includes a module" do
+ m = ReflectSpecs::E.new.protected_methods(*@object)
+ m.select { |x| x == :pro }.sort.should == [:pro]
+ end
+end
+
+describe :kernel_protected_methods_with_falsy, shared: true do
+ it "returns a list of protected methods in without its ancestors" do
+ ReflectSpecs::F.protected_methods(@object).select{|m|/_pro\z/ =~ m}.sort.should == [:ds_pro, :fs_pro]
+ ReflectSpecs::F.new.protected_methods(@object).should == [:f_pro]
+ end
+end
+
+describe "Kernel#protected_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :kernel_protected_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :kernel_protected_methods_supers, nil, true
+ end
+
+ describe "when passed false" do
+ it_behaves_like :kernel_protected_methods_with_falsy, nil, false
+ end
+
+ describe "when passed nil" do
+ it_behaves_like :kernel_protected_methods_with_falsy, nil, nil
+ end
+end
diff --git a/spec/ruby/core/kernel/public_method_spec.rb b/spec/ruby/core/kernel/public_method_spec.rb
new file mode 100644
index 0000000000..42b8f797d3
--- /dev/null
+++ b/spec/ruby/core/kernel/public_method_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/method'
+
+describe "Kernel#public_method" do
+ it_behaves_like :kernel_method, :public_method
+
+ before :each do
+ @obj = KernelSpecs::A.new
+ end
+
+ it "raises a NameError when called on a private method" do
+ @obj.send(:private_method).should == :private_method
+ -> do
+ @obj.public_method(:private_method)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError when called on a protected method" do
+ @obj.send(:protected_method).should == :protected_method
+ -> {
+ @obj.public_method(:protected_method)
+ }.should.raise(NameError)
+ end
+
+ it "raises a NameError if we only repond_to_missing? method, true" do
+ obj = KernelSpecs::RespondViaMissing.new
+ -> do
+ obj.public_method(:handled_privately)
+ end.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/kernel/public_methods_spec.rb b/spec/ruby/core/kernel/public_methods_spec.rb
new file mode 100644
index 0000000000..e334ac9a55
--- /dev/null
+++ b/spec/ruby/core/kernel/public_methods_spec.rb
@@ -0,0 +1,75 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+describe "Kernel#public_methods" do
+ it "returns a list of the names of publicly accessible methods in the object" do
+ KernelSpecs::Methods.public_methods(false).to_set.should >= Set[:hachi, :ichi, :juu, :juu_ni, :roku, :san, :shi]
+ KernelSpecs::Methods.new.public_methods(false).to_set.should >= Set[:juu_san, :ni]
+ end
+
+ it "returns a list of names without protected accessible methods in the object" do
+ KernelSpecs::Methods.public_methods(false).sort.should_not.include?(:juu_ichi)
+ KernelSpecs::Methods.new.public_methods(false).sort.should_not.include?(:ku)
+ end
+
+ it "returns a list of the names of publicly accessible methods in the object and its ancestors and mixed-in modules" do
+ (KernelSpecs::Methods.public_methods(false) & KernelSpecs::Methods.public_methods).to_set.should >= Set[
+ :hachi, :ichi, :juu, :juu_ni, :roku, :san, :shi]
+ m = KernelSpecs::Methods.new.public_methods
+ m.to_set.should >= Set[:ni, :juu_san]
+ end
+
+ it "returns methods mixed in to the metaclass" do
+ m = KernelSpecs::Methods.new
+ m.extend(KernelSpecs::Methods::MetaclassMethods)
+ m.public_methods.should.include?(:peekaboo)
+ end
+
+ it "returns public methods for immediates" do
+ 10.public_methods.should.include?(:divmod)
+ end
+end
+
+describe :kernel_public_methods_supers, shared: true do
+ it "returns a unique list for an object extended by a module" do
+ m = ReflectSpecs.oed.public_methods(*@object)
+ m.select { |x| x == :pub }.sort.should == [:pub]
+ end
+
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.new.public_methods(*@object)
+ m.select { |x| x == :pub }.sort.should == [:pub]
+ end
+
+ it "returns a unique list for a subclass of a class that includes a module" do
+ m = ReflectSpecs::E.new.public_methods(*@object)
+ m.select { |x| x == :pub }.sort.should == [:pub]
+ end
+end
+
+describe :kernel_public_methods_with_falsy, shared: true do
+ it "returns a list of public methods in without its ancestors" do
+ ReflectSpecs::F.public_methods(@object).select{|m|/_pub\z/ =~ m}.sort.should == [:ds_pub, :fs_pub]
+ ReflectSpecs::F.new.public_methods(@object).should == [:f_pub]
+ end
+end
+
+describe "Kernel#public_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :kernel_public_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :kernel_public_methods_supers, nil, true
+ end
+
+ describe "when passed false" do
+ it_behaves_like :kernel_public_methods_with_falsy, nil, false
+ end
+
+ describe "when passed nil" do
+ it_behaves_like :kernel_public_methods_with_falsy, nil, nil
+ end
+end
diff --git a/spec/ruby/core/kernel/public_send_spec.rb b/spec/ruby/core/kernel/public_send_spec.rb
new file mode 100644
index 0000000000..6a4a969c77
--- /dev/null
+++ b/spec/ruby/core/kernel/public_send_spec.rb
@@ -0,0 +1,116 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/basicobject/send'
+
+describe "Kernel#public_send" do
+ it "invokes the named public method" do
+ class KernelSpecs::Foo
+ def bar
+ 'done'
+ end
+ end
+ KernelSpecs::Foo.new.public_send(:bar).should == 'done'
+ end
+
+ it "invokes the named alias of a public method" do
+ class KernelSpecs::Foo
+ def bar
+ 'done'
+ end
+ alias :aka :bar
+ end
+ KernelSpecs::Foo.new.public_send(:aka).should == 'done'
+ end
+
+ it "raises a NoMethodError if the method is protected" do
+ class KernelSpecs::Foo
+ protected
+ def bar
+ 'done'
+ end
+ end
+ -> { KernelSpecs::Foo.new.public_send(:bar)}.should.raise(NoMethodError)
+ end
+
+ it "raises a NoMethodError if the named method is private" do
+ class KernelSpecs::Foo
+ private
+ def bar
+ 'done2'
+ end
+ end
+ -> {
+ KernelSpecs::Foo.new.public_send(:bar)
+ }.should.raise(NoMethodError)
+ end
+
+ context 'called from own public method' do
+ before do
+ class << @receiver = Object.new
+ def call_protected_method
+ public_send :protected_method
+ end
+
+ def call_private_method
+ public_send :private_method
+ end
+
+ protected
+
+ def protected_method
+ raise 'Should not called'
+ end
+
+ private
+
+ def private_method
+ raise 'Should not called'
+ end
+ end
+ end
+
+ it "raises a NoMethodError if the method is protected" do
+ -> { @receiver.call_protected_method }.should.raise(NoMethodError)
+ end
+
+ it "raises a NoMethodError if the method is private" do
+ -> { @receiver.call_private_method }.should.raise(NoMethodError)
+ end
+ end
+
+ it "raises a NoMethodError if the named method is an alias of a private method" do
+ class KernelSpecs::Foo
+ private
+ def bar
+ 'done2'
+ end
+ alias :aka :bar
+ end
+ -> {
+ KernelSpecs::Foo.new.public_send(:aka)
+ }.should.raise(NoMethodError)
+ end
+
+ it "raises a NoMethodError if the named method is an alias of a protected method" do
+ class KernelSpecs::Foo
+ protected
+ def bar
+ 'done2'
+ end
+ alias :aka :bar
+ end
+ -> {
+ KernelSpecs::Foo.new.public_send(:aka)
+ }.should.raise(NoMethodError)
+ end
+
+ it "includes `public_send` in the backtrace when passed not enough arguments" do
+ -> { public_send() }.should.raise(ArgumentError) { |e| e.backtrace[0].should =~ /[`'](?:Kernel#)?public_send'/ }
+ end
+
+ it "includes `public_send` in the backtrace when passed a single incorrect argument" do
+ -> { public_send(Object.new) }.should.raise(TypeError) { |e| e.backtrace[0].should =~ /[`'](?:Kernel#)?public_send'/ }
+ end
+
+ it_behaves_like :basicobject_send, :public_send
+end
diff --git a/spec/ruby/core/kernel/putc_spec.rb b/spec/ruby/core/kernel/putc_spec.rb
new file mode 100644
index 0000000000..e6a20a9af1
--- /dev/null
+++ b/spec/ruby/core/kernel/putc_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/io/putc'
+
+describe "Kernel#putc" do
+ it "is a private instance method" do
+ Kernel.private_instance_methods(false).should.include?(:putc)
+ end
+end
+
+describe "Kernel.putc" do
+ before :each do
+ @name = tmp("kernel_putc.txt")
+ @io = new_io @name
+ @io_object = @object
+ @stdout, $stdout = $stdout, @io
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it_behaves_like :io_putc, :putc_method, KernelSpecs
+end
+
+describe "Kernel#putc" do
+ before :each do
+ @name = tmp("kernel_putc.txt")
+ @io = new_io @name
+ @io_object = @object
+ @stdout, $stdout = $stdout, @io
+ end
+
+ after :each do
+ $stdout = @stdout
+ end
+
+ it_behaves_like :io_putc, :putc_function, KernelSpecs
+end
diff --git a/spec/ruby/core/kernel/puts_spec.rb b/spec/ruby/core/kernel/puts_spec.rb
new file mode 100644
index 0000000000..eed18fcd50
--- /dev/null
+++ b/spec/ruby/core/kernel/puts_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#puts" do
+ before :each do
+ @stdout = $stdout
+ @name = tmp("kernel_puts.txt")
+ $stdout = new_io @name
+ end
+
+ after :each do
+ $stdout.close
+ $stdout = @stdout
+ rm_r @name
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:puts)
+ end
+
+ it "delegates to $stdout.puts" do
+ $stdout.should_receive(:puts).with(:arg)
+ puts :arg
+ end
+end
+
+describe "Kernel.puts" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/raise_spec.rb b/spec/ruby/core/kernel/raise_spec.rb
new file mode 100644
index 0000000000..6162677e17
--- /dev/null
+++ b/spec/ruby/core/kernel/raise_spec.rb
@@ -0,0 +1,222 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/kernel/raise'
+
+describe "Kernel#raise" do
+ it "is a private method" do
+ Kernel.private_instance_methods.should.include?(:raise)
+ end
+
+ # Shared specs expect a public #raise method.
+ public_raiser = Object.new
+ class << public_raiser
+ public :raise
+ end
+ it_behaves_like :kernel_raise, :raise, public_raiser
+ it_behaves_like :kernel_raise_with_cause, :raise, public_raiser
+end
+
+describe "Kernel#raise with previously rescued exception" do
+ it "re-raises the previously rescued exception if no exception is specified" do
+ ScratchPad.record nil
+
+ -> do
+ begin
+ raise Exception, "outer"
+ ScratchPad.record :no_abort
+ rescue Exception
+ begin
+ raise StandardError, "inner"
+ rescue StandardError
+ end
+
+ raise
+ ScratchPad.record :no_reraise
+ end
+ end.should.raise(Exception, "outer")
+
+ ScratchPad.recorded.should == nil
+ end
+
+ it "re-raises a previously rescued exception without overwriting the cause" do
+ begin
+ begin
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ raise "Error 2"
+ end
+ rescue => e2
+ raise "Error 3"
+ end
+ rescue
+ e2.cause.should == e1
+ raise e2
+ end
+ rescue => e
+ e.cause.should == e1
+ end
+ end
+
+ it "re-raises a previously rescued exception with overwriting the cause when it's explicitly specified with :cause option" do
+ e4 = RuntimeError.new("Error 4")
+
+ begin
+ begin
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ raise "Error 2"
+ end
+ rescue => e2
+ raise "Error 3"
+ end
+ rescue
+ e2.cause.should == e1
+ raise e2, cause: e4
+ end
+ rescue => e
+ e.cause.should == e4
+ end
+ end
+
+ it "re-raises a previously rescued exception without overwriting the cause when it's explicitly specified with :cause option and has nil value" do
+ begin
+ begin
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ raise "Error 2"
+ end
+ rescue => e2
+ raise "Error 3"
+ end
+ rescue
+ e2.cause.should == e1
+ raise e2, cause: nil
+ end
+ rescue => e
+ e.cause.should == e1
+ end
+ end
+
+ it "re-raises a previously rescued exception without setting a cause implicitly" do
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ raise
+ end
+ rescue => e
+ e.should == e1
+ e.cause.should == nil
+ end
+ end
+
+ it "re-raises a previously rescued exception that has a cause without setting a cause implicitly" do
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ begin
+ raise "Error 2"
+ rescue => e2
+ raise
+ end
+ end
+ rescue => e
+ e.should == e2
+ e.cause.should == e1
+ end
+ end
+
+ it "re-raises a previously rescued exception that doesn't have a cause and isn't a cause of any other exception with setting a cause implicitly" do
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ begin
+ raise "Error 2"
+ rescue => e2
+ raise "Error 3"
+ end
+ end
+ rescue => e
+ e.message.should == "Error 3"
+ e.cause.should == e2
+ end
+ end
+
+ it "re-raises a previously rescued exception that doesn't have a cause and is a cause of other exception without setting a cause implicitly" do
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ begin
+ raise "Error 2"
+ rescue => e2
+ e1.cause.should == nil
+ e2.cause.should == e1
+ raise e1
+ end
+ end
+ rescue => e
+ e.should == e1
+ e.cause.should == nil
+ end
+ end
+
+ it "re-raises a previously rescued exception that doesn't have a cause and is a cause of other exception (that wasn't raised explicitly) without setting a cause implicitly" do
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ begin
+ foo # raises NameError
+ rescue => e2
+ e1.cause.should == nil
+ e2.cause.should == e1
+ raise e1
+ end
+ end
+ rescue => e
+ e.should == e1
+ e.cause.should == nil
+ end
+ end
+
+ it "re-raises a previously rescued exception that has a cause but isn't a cause of any other exception without setting a cause implicitly" do
+ begin
+ begin
+ raise "Error 1"
+ rescue => e1
+ begin
+ raise "Error 2"
+ rescue => e2
+ begin
+ raise "Error 3", cause: RuntimeError.new("Error 4")
+ rescue => e3
+ e2.cause.should == e1
+ e3.cause.should_not == e2
+ raise e2
+ end
+ end
+ end
+ rescue => e
+ e.should == e2
+ e.cause.should == e1
+ end
+ end
+end
+
+describe "Kernel.raise" do
+ it "is a public method" do
+ Kernel.singleton_class.should.public_method_defined?(:raise)
+ end
+
+ it_behaves_like :kernel_raise, :raise, Kernel
+ it_behaves_like :kernel_raise_with_cause, :raise, Kernel
+end
diff --git a/spec/ruby/core/kernel/rand_spec.rb b/spec/ruby/core/kernel/rand_spec.rb
new file mode 100644
index 0000000000..1bf5e225d9
--- /dev/null
+++ b/spec/ruby/core/kernel/rand_spec.rb
@@ -0,0 +1,197 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#rand" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:rand)
+ end
+
+ it "returns a float if no argument is passed" do
+ rand.should.is_a?(Float)
+ end
+
+ it "returns an integer for an integer argument" do
+ rand(77).should.is_a?(Integer)
+ end
+
+ it "returns an integer for a float argument greater than 1" do
+ rand(1.3).should.is_a?(Integer)
+ end
+
+ it "returns a float for an argument between -1 and 1" do
+ rand(-0.999).should.is_a?(Float)
+ rand(-0.01).should.is_a?(Float)
+ rand(0).should.is_a?(Float)
+ rand(0.01).should.is_a?(Float)
+ rand(0.999).should.is_a?(Float)
+ end
+
+ it "ignores the sign of the argument" do
+ [0, 1, 2, 3].should.include?(rand(-4))
+ end
+
+ it "never returns a value greater or equal to 1.0 with no arguments" do
+ 1000.times do
+ (0...1.0).should.include?(rand)
+ end
+ end
+
+ it "never returns a value greater or equal to any passed in max argument" do
+ 1000.times do
+ (0...100).to_a.should.include?(rand(100))
+ end
+ end
+
+ it "calls to_int on its argument" do
+ l = mock('limit')
+ l.should_receive(:to_int).and_return 7
+
+ rand l
+ end
+
+ context "given an exclusive range" do
+ it "returns an Integer between the two Integers" do
+ 1000.times do
+ x = rand(4...6)
+ x.should.is_a?(Integer)
+ (4...6).should.include?(x)
+ end
+ end
+
+ it "returns a Float between the given Integer and Float" do
+ 1000.times do
+ x = rand(4...6.5)
+ x.should.is_a?(Float)
+ (4...6.5).should.include?(x)
+ end
+ end
+
+ it "returns a Float between the given Float and Integer" do
+ 1000.times do
+ x = rand(3.5...6)
+ x.should.is_a?(Float)
+ (3.5...6).should.include?(x)
+ end
+ end
+
+ it "returns a Float between the two given Floats" do
+ 1000.times do
+ x = rand(3.5...6.5)
+ x.should.is_a?(Float)
+ (3.5...6.5).should.include?(x)
+ end
+ end
+ end
+
+ context "given an inclusive range" do
+ it "returns an Integer between the two Integers" do
+ 1000.times do
+ x = rand(4..6)
+ x.should.is_a?(Integer)
+ (4..6).should.include?(x)
+ end
+ end
+
+ it "returns a Float between the given Integer and Float" do
+ 1000.times do
+ x = rand(4..6.5)
+ x.should.is_a?(Float)
+ (4..6.5).should.include?(x)
+ end
+ end
+
+ it "returns a Float between the given Float and Integer" do
+ 1000.times do
+ x = rand(3.5..6)
+ x.should.is_a?(Float)
+ (3.5..6).should.include?(x)
+ end
+ end
+
+ it "returns a Float between the two given Floats" do
+ 1000.times do
+ x = rand(3.5..6.5)
+ x.should.is_a?(Float)
+ (3.5..6.5).should.include?(x)
+ end
+ end
+ end
+
+ context "given an inclusive range between 0 and 1" do
+ it "returns an Integer between the two Integers" do
+ x = rand(0..1)
+ x.should.is_a?(Integer)
+ (0..1).should.include?(x)
+ end
+
+ it "returns a Float if at least one side is Float" do
+ seed = 42
+ x1 = Random.new(seed).rand(0..1.0)
+ x2 = Random.new(seed).rand(0.0..1.0)
+ x3 = Random.new(seed).rand(0.0..1)
+
+ x3.should.is_a?(Float)
+ x1.should.eql?(x3)
+ x2.should.eql?(x3)
+
+ (0.0..1.0).should.include?(x3)
+ end
+ end
+
+ context "given an exclusive range between 0 and 1" do
+ it "returns zero as an Integer" do
+ x = rand(0...1)
+ x.should.is_a?(Integer)
+ x.should.eql?(0)
+ end
+
+ it "returns a Float if at least one side is Float" do
+ seed = 42
+ x1 = Random.new(seed).rand(0...1.0)
+ x2 = Random.new(seed).rand(0.0...1.0)
+ x3 = Random.new(seed).rand(0.0...1)
+
+ x3.should.is_a?(Float)
+ x1.should.eql?(x3)
+ x2.should.eql?(x3)
+
+ (0.0...1.0).should.include?(x3)
+ end
+ end
+
+ it "returns a numeric for an range argument where max is < 1" do
+ rand(0.25..0.75).should.is_a?(Numeric)
+ end
+
+ it "returns nil when range is backwards" do
+ rand(1..0).should == nil
+ end
+
+ it "returns the range start/end when Float range is 0" do
+ rand(1.0..1.0).should.eql?(1.0)
+ end
+
+ it "returns the range start/end when Integer range is 0" do
+ rand(42..42).should.eql?(42)
+ end
+
+ it "supports custom object types" do
+ rand(KernelSpecs::CustomRangeInteger.new(1)..KernelSpecs::CustomRangeInteger.new(42)).should.instance_of?(KernelSpecs::CustomRangeInteger)
+ rand(KernelSpecs::CustomRangeFloat.new(1.0)..KernelSpecs::CustomRangeFloat.new(42.0)).should.instance_of?(KernelSpecs::CustomRangeFloat)
+ rand(Time.now..Time.now).should.instance_of?(Time)
+ end
+
+ it "is random on boot" do
+ results = 2.times.map {
+ out = ruby_exe('p rand', options: '--disable-gems')
+ Float(out)
+ }
+ results.size.should == 2
+ # this is technically flaky, but very unlikely in a good distribution
+ results[0].should_not == results[1]
+ end
+end
+
+describe "Kernel.rand" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/readline_spec.rb b/spec/ruby/core/kernel/readline_spec.rb
new file mode 100644
index 0000000000..86899d78d2
--- /dev/null
+++ b/spec/ruby/core/kernel/readline_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#readline" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:readline)
+ end
+end
+
+describe "Kernel.readline" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/readlines_spec.rb b/spec/ruby/core/kernel/readlines_spec.rb
new file mode 100644
index 0000000000..c758014aab
--- /dev/null
+++ b/spec/ruby/core/kernel/readlines_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#readlines" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:readlines)
+ end
+end
+
+describe "Kernel.readlines" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/remove_instance_variable_spec.rb b/spec/ruby/core/kernel/remove_instance_variable_spec.rb
new file mode 100644
index 0000000000..114536064f
--- /dev/null
+++ b/spec/ruby/core/kernel/remove_instance_variable_spec.rb
@@ -0,0 +1,72 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_remove_instance_variable, shared: true do
+ it "returns the instance variable's value" do
+ value = @instance.send :remove_instance_variable, @object
+ value.should == "hello"
+ end
+
+ it "removes the instance variable" do
+ @instance.send :remove_instance_variable, @object
+ @instance.instance_variable_defined?(@object).should == false
+ end
+end
+
+describe "Kernel#remove_instance_variable" do
+ before do
+ @instance = KernelSpecs::InstanceVariable.new
+ end
+
+ it "is a public method" do
+ Kernel.public_instance_methods(false).should.include?(:remove_instance_variable)
+ end
+
+ it "raises a NameError if the instance variable is not defined" do
+ -> do
+ @instance.send :remove_instance_variable, :@unknown
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError if the argument is not a valid instance variable name" do
+ -> do
+ @instance.send :remove_instance_variable, :"@0"
+ end.should.raise(NameError)
+ end
+
+ it "raises a TypeError if passed an Object not defining #to_str" do
+ -> do
+ obj = mock("kernel remove_instance_variable")
+ @instance.send :remove_instance_variable, obj
+ end.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ o = Object.new
+ o.freeze
+ -> { o.remove_instance_variable(:@foo) }.should.raise(FrozenError)
+ -> { o.remove_instance_variable(:foo) }.should.raise(NameError)
+ end
+
+ it "raises for frozen objects" do
+ -> { nil.remove_instance_variable(:@foo) }.should.raise(FrozenError)
+ -> { nil.remove_instance_variable(:foo) }.should.raise(NameError)
+ -> { :foo.remove_instance_variable(:@foo) }.should.raise(FrozenError)
+ end
+
+ describe "when passed a String" do
+ it_behaves_like :kernel_remove_instance_variable, nil, "@greeting"
+ end
+
+ describe "when passed a Symbol" do
+ it_behaves_like :kernel_remove_instance_variable, nil, :@greeting
+ end
+
+ describe "when passed an Object" do
+ it "calls #to_str to convert the argument" do
+ name = mock("kernel remove_instance_variable")
+ name.should_receive(:to_str).and_return("@greeting")
+ @instance.send :remove_instance_variable, name
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/require_relative_spec.rb b/spec/ruby/core/kernel/require_relative_spec.rb
new file mode 100644
index 0000000000..332045b200
--- /dev/null
+++ b/spec/ruby/core/kernel/require_relative_spec.rb
@@ -0,0 +1,437 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/code_loading'
+
+describe "Kernel#require_relative with a relative path" do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ @dir = "../../fixtures/code"
+ @abs_dir = File.realpath(@dir, __dir__)
+ @path = "#{@dir}/load_fixture.rb"
+ @abs_path = File.realpath(@path, __dir__)
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ platform_is_not :windows do
+ describe "when file is a symlink" do
+ before :each do
+ @link = tmp("symlink.rb", false)
+ @real_path = "#{@abs_dir}/symlink/symlink1.rb"
+ File.symlink(@real_path, @link)
+ end
+
+ after :each do
+ rm_r @link
+ end
+
+ it "loads a path relative to current file" do
+ require_relative(@link).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+ end
+
+ it "loads a path relative to the current file" do
+ require_relative(@path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ describe "in an #instance_eval with a" do
+
+ it "synthetic file base name loads a file base name relative to the working directory" do
+ Dir.chdir @abs_dir do
+ Object.new.instance_eval("require_relative(#{File.basename(@path).inspect})", "foo.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "synthetic file path loads a relative path relative to the working directory plus the directory of the synthetic path" do
+ Dir.chdir @abs_dir do
+ Object.new.instance_eval("require_relative(File.join('..', #{File.basename(@path).inspect}))", "bar/foo.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ platform_is_not :windows do
+ it "synthetic relative file path with a Windows path separator specified loads a relative path relative to the working directory" do
+ Dir.chdir @abs_dir do
+ Object.new.instance_eval("require_relative(#{File.basename(@path).inspect})", "bar\\foo.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+
+ it "absolute file path loads a path relative to the absolute path" do
+ Object.new.instance_eval("require_relative(#{@path.inspect})", __FILE__).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "absolute file path loads a path relative to the root directory" do
+ root = @abs_path
+ until File.dirname(root) == root
+ root = File.dirname(root)
+ end
+ root_relative = @abs_path[root.size..-1]
+ Object.new.instance_eval("require_relative(#{root_relative.inspect})", "/").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ end
+
+ it "loads a file defining many methods" do
+ require_relative("#{@dir}/methods_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "raises a LoadError if the file does not exist" do
+ -> { require_relative("#{@dir}/nonexistent.rb") }.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+
+ it "raises a LoadError that includes the missing path" do
+ missing_path = "#{@dir}/nonexistent.rb"
+ expanded_missing_path = File.expand_path(missing_path, __dir__)
+ -> { require_relative(missing_path) }.should.raise(LoadError) { |e|
+ e.message.should.include?(expanded_missing_path)
+ e.path.should == expanded_missing_path
+ }
+ ScratchPad.recorded.should == []
+ end
+
+ it "raises a LoadError if basepath does not exist" do
+ -> { eval("require_relative('#{@dir}/nonexistent.rb')") }.should.raise(LoadError)
+ end
+
+ it "stores the missing path in a LoadError object" do
+ path = "#{@dir}/nonexistent.rb"
+
+ -> {
+ require_relative(path)
+ }.should(raise_error(LoadError) { |e|
+ e.path.should == File.expand_path(path, @abs_dir)
+ })
+ end
+
+ it "calls #to_str on non-String objects" do
+ name = mock("load_fixture.rb mock")
+ name.should_receive(:to_str).and_return(@path)
+ require_relative(name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "raises a TypeError if argument does not respond to #to_str" do
+ -> { require_relative(nil) }.should.raise(TypeError)
+ -> { require_relative(42) }.should.raise(TypeError)
+ -> {
+ require_relative([@path,@path])
+ }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that has #to_s but not #to_str" do
+ name = mock("load_fixture.rb mock")
+ name.stub!(:to_s).and_return(@path)
+ -> { require_relative(name) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ name = mock("#to_str returns nil")
+ name.should_receive(:to_str).at_least(1).times.and_return(nil)
+ -> { require_relative(name) }.should.raise(TypeError)
+ end
+
+ it "calls #to_path on non-String objects" do
+ name = mock("load_fixture.rb mock")
+ name.should_receive(:to_path).and_return(@path)
+ require_relative(name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "calls #to_str on non-String objects returned by #to_path" do
+ name = mock("load_fixture.rb mock")
+ to_path = mock("load_fixture_rb #to_path mock")
+ name.should_receive(:to_path).and_return(to_path)
+ to_path.should_receive(:to_str).and_return(@path)
+ require_relative(name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ describe "(file extensions)" do
+ it "loads a .rb extensioned file when passed a non-extensioned path" do
+ require_relative("#{@dir}/load_fixture").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.bundle"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dylib"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.so"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dll"
+ require_relative(@path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not load a C-extension file if a .rb extensioned file is already loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.rb"
+ require_relative("#{@dir}/load_fixture").should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "loads a .rb extensioned file when passed a non-.rb extensioned path" do
+ require_relative("#{@dir}/load_fixture.ext").should == true
+ ScratchPad.recorded.should == [:loaded]
+ $LOADED_FEATURES.should.include? "#{@abs_dir}/load_fixture.ext.rb"
+ end
+
+ it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.bundle"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dylib"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.so"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dll"
+ require_relative("#{@dir}/load_fixture.ext").should == true
+ ScratchPad.recorded.should == [:loaded]
+ $LOADED_FEATURES.should.include? "#{@abs_dir}/load_fixture.ext.rb"
+ end
+
+ it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.rb"
+ require_relative("#{@dir}/load_fixture.ext").should == false
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "($LOADED_FEATURES)" do
+ it "stores an absolute path" do
+ require_relative(@path).should == true
+ $LOADED_FEATURES.should.include?(@abs_path)
+ end
+
+ platform_is_not :windows, :wasi do
+ describe "with symlinks" do
+ before :each do
+ @symlink_to_code_dir = tmp("codesymlink")
+ File.symlink(CODE_LOADING_DIR, @symlink_to_code_dir)
+ @symlink_basename = File.basename(@symlink_to_code_dir)
+ @requiring_file = tmp("requiring")
+ end
+
+ after :each do
+ rm_r @symlink_to_code_dir, @requiring_file
+ end
+
+ it "does not canonicalize the path and stores a path with symlinks" do
+ symlink_path = "#{@symlink_basename}/load_fixture.rb"
+ absolute_path = "#{tmp("")}#{symlink_path}"
+ canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb"
+ touch(@requiring_file) { |f|
+ f.puts "require_relative #{symlink_path.inspect}"
+ }
+ load(@requiring_file)
+ ScratchPad.recorded.should == [:loaded]
+
+ features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') }
+ features.should.include?(absolute_path)
+ features.should_not.include?(canonical_path)
+ end
+
+ it "stores the same path that __FILE__ returns in the required file" do
+ symlink_path = "#{@symlink_basename}/load_fixture_and__FILE__.rb"
+ touch(@requiring_file) { |f|
+ f.puts "require_relative #{symlink_path.inspect}"
+ }
+ load(@requiring_file)
+ loaded_feature = $LOADED_FEATURES.last
+ ScratchPad.recorded.should == [loaded_feature]
+ end
+ end
+ end
+
+ it "does not store the path if the load fails" do
+ saved_loaded_features = $LOADED_FEATURES.dup
+ -> { require_relative("#{@dir}/raise_fixture.rb") }.should.raise(RuntimeError)
+ $LOADED_FEATURES.should == saved_loaded_features
+ end
+
+ it "does not load an absolute path that is already stored" do
+ $LOADED_FEATURES << @abs_path
+ require_relative(@path).should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "adds the suffix of the resolved filename" do
+ require_relative("#{@dir}/load_fixture").should == true
+ $LOADED_FEATURES.should.include?("#{@abs_dir}/load_fixture.rb")
+ end
+
+ it "loads a path for a file already loaded with a relative path" do
+ $LOAD_PATH << File.expand_path(@dir)
+ $LOADED_FEATURES << "load_fixture.rb" << "load_fixture"
+ require_relative(@path).should == true
+ $LOADED_FEATURES.should.include?(@abs_path)
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+end
+
+describe "Kernel#require_relative with an absolute path" do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ @dir = File.expand_path "../../fixtures/code", __dir__
+ @abs_dir = @dir
+ @path = File.join @dir, "load_fixture.rb"
+ @abs_path = @path
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ it "loads a path relative to the current file" do
+ require_relative(@path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a file defining many methods" do
+ require_relative("#{@dir}/methods_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "raises a LoadError if the file does not exist" do
+ -> { require_relative("#{@dir}/nonexistent.rb") }.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+
+ it "raises a LoadError if basepath does not exist" do
+ -> { eval("require_relative('#{@dir}/nonexistent.rb')") }.should.raise(LoadError)
+ end
+
+ it "stores the missing path in a LoadError object" do
+ path = "#{@dir}/nonexistent.rb"
+
+ -> {
+ require_relative(path)
+ }.should(raise_error(LoadError) { |e|
+ e.path.should == File.expand_path(path, @abs_dir)
+ })
+ end
+
+ it "calls #to_str on non-String objects" do
+ name = mock("load_fixture.rb mock")
+ name.should_receive(:to_str).and_return(@path)
+ require_relative(name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "raises a TypeError if argument does not respond to #to_str" do
+ -> { require_relative(nil) }.should.raise(TypeError)
+ -> { require_relative(42) }.should.raise(TypeError)
+ -> {
+ require_relative([@path,@path])
+ }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that has #to_s but not #to_str" do
+ name = mock("load_fixture.rb mock")
+ name.stub!(:to_s).and_return(@path)
+ -> { require_relative(name) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ name = mock("#to_str returns nil")
+ name.should_receive(:to_str).at_least(1).times.and_return(nil)
+ -> { require_relative(name) }.should.raise(TypeError)
+ end
+
+ it "calls #to_path on non-String objects" do
+ name = mock("load_fixture.rb mock")
+ name.should_receive(:to_path).and_return(@path)
+ require_relative(name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "calls #to_str on non-String objects returned by #to_path" do
+ name = mock("load_fixture.rb mock")
+ to_path = mock("load_fixture_rb #to_path mock")
+ name.should_receive(:to_path).and_return(to_path)
+ to_path.should_receive(:to_str).and_return(@path)
+ require_relative(name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ describe "(file extensions)" do
+ it "loads a .rb extensioned file when passed a non-extensioned path" do
+ require_relative("#{@dir}/load_fixture").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.bundle"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dylib"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.so"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.dll"
+ require_relative(@path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not load a C-extension file if a .rb extensioned file is already loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.rb"
+ require_relative("#{@dir}/load_fixture").should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "loads a .rb extensioned file when passed a non-.rb extensioned path" do
+ require_relative("#{@dir}/load_fixture.ext").should == true
+ ScratchPad.recorded.should == [:loaded]
+ $LOADED_FEATURES.should.include? "#{@abs_dir}/load_fixture.ext.rb"
+ end
+
+ it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.bundle"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dylib"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.so"
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.dll"
+ require_relative("#{@dir}/load_fixture.ext").should == true
+ ScratchPad.recorded.should == [:loaded]
+ $LOADED_FEATURES.should.include? "#{@abs_dir}/load_fixture.ext.rb"
+ end
+
+ it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do
+ $LOADED_FEATURES << "#{@abs_dir}/load_fixture.ext.rb"
+ require_relative("#{@dir}/load_fixture.ext").should == false
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "($LOAD_FEATURES)" do
+ it "stores an absolute path" do
+ require_relative(@path).should == true
+ $LOADED_FEATURES.should.include?(@abs_path)
+ end
+
+ it "does not store the path if the load fails" do
+ saved_loaded_features = $LOADED_FEATURES.dup
+ -> { require_relative("#{@dir}/raise_fixture.rb") }.should.raise(RuntimeError)
+ $LOADED_FEATURES.should == saved_loaded_features
+ end
+
+ it "does not load an absolute path that is already stored" do
+ $LOADED_FEATURES << @abs_path
+ require_relative(@path).should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "adds the suffix of the resolved filename" do
+ require_relative("#{@dir}/load_fixture").should == true
+ $LOADED_FEATURES.should.include?("#{@abs_dir}/load_fixture.rb")
+ end
+
+ it "loads a path for a file already loaded with a relative path" do
+ $LOAD_PATH << File.expand_path(@dir)
+ $LOADED_FEATURES << "load_fixture.rb" << "load_fixture"
+ require_relative(@path).should == true
+ $LOADED_FEATURES.should.include?(@abs_path)
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb
new file mode 100644
index 0000000000..62d954c2bf
--- /dev/null
+++ b/spec/ruby/core/kernel/require_spec.rb
@@ -0,0 +1,67 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/code_loading'
+require_relative 'shared/require'
+
+describe "Kernel#require" do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ # if this fails, update your rubygems
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:require)
+ end
+
+ it "provided features are already required" do
+ provided = %w[complex enumerator fiber rational thread ruby2_keywords]
+ ruby_version_is "4.0" do
+ provided += %w[set pathname]
+ end
+ ruby_version_is "4.1" do
+ provided += %w[monitor]
+ end
+
+ out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean')
+ features = out.lines.map(&:chomp)
+
+ # Ignore engine-specific internals
+ case RUBY_ENGINE
+ when "jruby"
+ features -= %w[java.rb jruby/util.rb]
+ when "ruby"
+ so = RbConfig::CONFIG['DLEXT']
+ features -= ["windows_1252.#{so}", "windows_31.#{so}"]
+ features.reject! { |feature| feature.end_with? "encdb.#{so}" }
+ features.reject! { |feature| feature.end_with? "transdb.#{so}" }
+ features.reject! { |feature| feature.include?('-fake') }
+ end
+
+ features_no_ext = features.map { |path| File.basename(path, '.*') }
+ features_no_ext.sort.should == provided.sort
+
+ requires = features
+ code = requires.map { |f| "puts require #{f.inspect}\n" }.join
+ required = ruby_exe(code, options: '--disable-gems')
+ required.should == "false\n" * requires.size
+ end
+
+ it_behaves_like :kernel_require_basic, :require, CodeLoadingSpecs::Method.new
+ it_behaves_like :kernel_require, :require, CodeLoadingSpecs::Method.new
+end
+
+describe "Kernel.require" do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ it_behaves_like :kernel_require_basic, :require, Kernel
+ it_behaves_like :kernel_require, :require, Kernel
+end
diff --git a/spec/ruby/core/kernel/respond_to_missing_spec.rb b/spec/ruby/core/kernel/respond_to_missing_spec.rb
new file mode 100644
index 0000000000..08c9184fb4
--- /dev/null
+++ b/spec/ruby/core/kernel/respond_to_missing_spec.rb
@@ -0,0 +1,100 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#respond_to_missing?" do
+ before :each do
+ @a = KernelSpecs::A.new
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:respond_to_missing?)
+ end
+
+ it "is only an instance method" do
+ Kernel.method(:respond_to_missing?).owner.should == Kernel
+ end
+
+ it "is not called when #respond_to? would return true" do
+ obj = mock('object')
+ obj.stub!(:glark)
+ obj.should_not_receive(:respond_to_missing?)
+ obj.respond_to?(:glark).should == true
+ end
+
+ it "is called with a 2nd argument of false when #respond_to? is" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, false)
+ obj.respond_to?(:undefined_method, false)
+ end
+
+ it "is called a 2nd argument of false when #respond_to? is called with only 1 argument" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, false)
+ obj.respond_to?(:undefined_method)
+ end
+
+ it "is called with true as the second argument when #respond_to? is" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, true)
+ obj.respond_to?(:undefined_method, true)
+ end
+
+ it "is called when #respond_to? would return false" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, false)
+ obj.respond_to?(:undefined_method)
+ end
+
+ it "causes #respond_to? to return true if called and not returning false" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, false).and_return(:glark)
+ obj.respond_to?(:undefined_method).should == true
+ end
+
+ it "causes #respond_to? to return false if called and returning false" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, false).and_return(false)
+ obj.respond_to?(:undefined_method).should == false
+ end
+
+ it "causes #respond_to? to return false if called and returning nil" do
+ obj = mock('object')
+ obj.should_receive(:respond_to_missing?).with(:undefined_method, false).and_return(nil)
+ obj.respond_to?(:undefined_method).should == false
+ end
+
+ it "isn't called when obj responds to the given public method" do
+ @a.should_not_receive(:respond_to_missing?)
+ @a.respond_to?(:pub_method).should == true
+ end
+
+ it "isn't called when obj responds to the given public method, include_private = true" do
+ @a.should_not_receive(:respond_to_missing?)
+ @a.respond_to?(:pub_method, true).should == true
+ end
+
+ it "is called when obj responds to the given protected method, include_private = false" do
+ @a.should_receive(:respond_to_missing?)
+ @a.respond_to?(:protected_method, false).should == false
+ end
+
+ it "isn't called when obj responds to the given protected method, include_private = true" do
+ @a.should_not_receive(:respond_to_missing?)
+ @a.respond_to?(:protected_method, true).should == true
+ end
+
+ it "is called when obj responds to the given private method, include_private = false" do
+ @a.should_receive(:respond_to_missing?).with(:private_method, false)
+ @a.respond_to?(:private_method)
+ end
+
+ it "isn't called when obj responds to the given private method, include_private = true" do
+ @a.should_not_receive(:respond_to_missing?)
+ @a.respond_to?(:private_method, true).should == true
+ end
+
+ it "is called for missing class methods" do
+ @a.class.should_receive(:respond_to_missing?).with(:oOoOoO, false)
+ @a.class.respond_to?(:oOoOoO)
+ end
+end
diff --git a/spec/ruby/core/kernel/respond_to_spec.rb b/spec/ruby/core/kernel/respond_to_spec.rb
new file mode 100644
index 0000000000..72ab4eebe1
--- /dev/null
+++ b/spec/ruby/core/kernel/respond_to_spec.rb
@@ -0,0 +1,99 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#respond_to?" do
+ before :each do
+ @a = KernelSpecs::A.new
+ end
+
+ it "is a public method" do
+ Kernel.public_instance_methods(false).should.include?(:respond_to?)
+ end
+
+ it "is only an instance method" do
+ Kernel.method(:respond_to?).owner.should == Kernel
+ end
+
+ it "returns false if the given method was undefined" do
+ @a.respond_to?(:undefed_method).should == false
+ @a.respond_to?("undefed_method").should == false
+ end
+
+ it "returns true if obj responds to the given public method" do
+ @a.respond_to?(:pub_method).should == true
+ @a.respond_to?("pub_method").should == true
+ end
+
+ it "throws a type error if argument can't be coerced into a Symbol" do
+ -> { @a.respond_to?(Object.new) }.should.raise(TypeError, /is not a symbol nor a string/)
+ end
+
+ it "returns false if obj responds to the given protected method" do
+ @a.respond_to?(:protected_method).should == false
+ @a.respond_to?("protected_method").should == false
+ end
+
+ it "returns false if obj responds to the given private method" do
+ @a.respond_to?(:private_method).should == false
+ @a.respond_to?("private_method").should == false
+ end
+
+ it "returns true if obj responds to the given protected method (include_private = true)" do
+ @a.respond_to?(:protected_method, true).should == true
+ @a.respond_to?("protected_method", true).should == true
+ end
+
+ it "returns false if obj responds to the given protected method (include_private = false)" do
+ @a.respond_to?(:protected_method, false).should == false
+ @a.respond_to?("protected_method", false).should == false
+ end
+
+ it "returns false even if obj responds to the given private method (include_private = false)" do
+ @a.respond_to?(:private_method, false).should == false
+ @a.respond_to?("private_method", false).should == false
+ end
+
+ it "returns true if obj responds to the given private method (include_private = true)" do
+ @a.respond_to?(:private_method, true).should == true
+ @a.respond_to?("private_method", true).should == true
+ end
+
+ it "does not change method visibility when finding private method" do
+ KernelSpecs::VisibilityChange.respond_to?(:new, false).should == false
+ KernelSpecs::VisibilityChange.respond_to?(:new, true).should == true
+ -> { KernelSpecs::VisibilityChange.new }.should.raise(NoMethodError)
+ end
+
+ it "indicates if an object responds to a particular message" do
+ class KernelSpecs::Foo; def bar; 'done'; end; end
+ KernelSpecs::Foo.new.respond_to?(:bar).should == true
+ KernelSpecs::Foo.new.respond_to?(:invalid_and_silly_method_name).should == false
+ end
+
+ context "if object does not have #respond_to_missing?" do
+ it "returns true if object responds to the given public method" do
+ KernelSpecs::BasicA.new.respond_to?(:pub_method).should == true
+ KernelSpecs::MissingA.new.respond_to?(:pub_method).should == true
+ end
+
+ it "returns false if object responds to the given protected method" do
+ KernelSpecs::BasicA.new.respond_to?(:protected_method).should == false
+ KernelSpecs::MissingA.new.respond_to?(:protected_method).should == false
+ end
+
+ it "returns false if object responds to the given private method" do
+ KernelSpecs::BasicA.new.respond_to?(:private_method).should == false
+ KernelSpecs::MissingA.new.respond_to?(:private_method).should == false
+ end
+
+ it "returns false if the given method was undefined" do
+ KernelSpecs::BasicA.new.respond_to?(:undefed_method).should == false
+ KernelSpecs::MissingA.new.respond_to?(:undefed_method).should == false
+ end
+
+ it "returns false if the given method never existed" do
+ KernelSpecs::BasicA.new.respond_to?(:invalid_and_silly_method_name).should == false
+ KernelSpecs::MissingA.new.respond_to?(:invalid_and_silly_method_name).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/select_spec.rb b/spec/ruby/core/kernel/select_spec.rb
new file mode 100644
index 0000000000..58c963c1a4
--- /dev/null
+++ b/spec/ruby/core/kernel/select_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#select" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:select)
+ end
+end
+
+describe "Kernel.select" do
+ it 'does not block when timeout is 0' do
+ IO.pipe do |read, write|
+ select([read], [], [], 0).should == nil
+ write.write 'data'
+ select([read], [], [], 0).should == [[read], [], []]
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/send_spec.rb b/spec/ruby/core/kernel/send_spec.rb
new file mode 100644
index 0000000000..9a4d261964
--- /dev/null
+++ b/spec/ruby/core/kernel/send_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/basicobject/send'
+
+describe "Kernel#send" do
+ it "invokes the named public method" do
+ class KernelSpecs::Foo
+ def bar
+ 'done'
+ end
+ end
+ KernelSpecs::Foo.new.send(:bar).should == 'done'
+ end
+
+ it "invokes the named alias of a public method" do
+ class KernelSpecs::Foo
+ def bar
+ 'done'
+ end
+ alias :aka :bar
+ end
+ KernelSpecs::Foo.new.send(:aka).should == 'done'
+ end
+
+ it "invokes the named protected method" do
+ class KernelSpecs::Foo
+ protected
+ def bar
+ 'done'
+ end
+ end
+ KernelSpecs::Foo.new.send(:bar).should == 'done'
+ end
+
+ it "invokes the named private method" do
+ class KernelSpecs::Foo
+ private
+ def bar
+ 'done2'
+ end
+ end
+ KernelSpecs::Foo.new.send(:bar).should == 'done2'
+ end
+
+ it "invokes the named alias of a private method" do
+ class KernelSpecs::Foo
+ private
+ def bar
+ 'done2'
+ end
+ alias :aka :bar
+ end
+ KernelSpecs::Foo.new.send(:aka).should == 'done2'
+ end
+
+ it "invokes the named alias of a protected method" do
+ class KernelSpecs::Foo
+ protected
+ def bar
+ 'done2'
+ end
+ alias :aka :bar
+ end
+ KernelSpecs::Foo.new.send(:aka).should == 'done2'
+ end
+
+ it_behaves_like :basicobject_send, :send
+end
diff --git a/spec/ruby/core/kernel/set_trace_func_spec.rb b/spec/ruby/core/kernel/set_trace_func_spec.rb
new file mode 100644
index 0000000000..5201812f2f
--- /dev/null
+++ b/spec/ruby/core/kernel/set_trace_func_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#set_trace_func" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:set_trace_func)
+ end
+end
+
+describe "Kernel.set_trace_func" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/shared/dup_clone.rb b/spec/ruby/core/kernel/shared/dup_clone.rb
new file mode 100644
index 0000000000..3fbf918283
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/dup_clone.rb
@@ -0,0 +1,91 @@
+class ObjectSpecDup
+ def initialize()
+ @obj = :original
+ end
+
+ attr_accessor :obj
+end
+
+class ObjectSpecDupInitCopy
+ def initialize()
+ @obj = :original
+ end
+
+ attr_accessor :obj, :original
+
+ def initialize_copy(original)
+ @obj = :init_copy
+ @original = original
+ end
+
+ private :initialize_copy
+end
+
+describe :kernel_dup_clone, shared: true do
+ it "returns a new object duplicated from the original" do
+ o = ObjectSpecDup.new
+ o2 = ObjectSpecDup.new
+
+ o.obj = 10
+
+ o3 = o.send(@method)
+
+ o3.obj.should == 10
+ o2.obj.should == :original
+ end
+
+ it "produces a shallow copy, contained objects are not recursively dupped" do
+ o = ObjectSpecDup.new
+ array = [1, 2]
+ o.obj = array
+
+ o2 = o.send(@method)
+ o2.obj.should.equal?(o.obj)
+ end
+
+ it "calls #initialize_copy on the NEW object if available, passing in original object" do
+ o = ObjectSpecDupInitCopy.new
+ o2 = o.send(@method)
+
+ o.obj.should == :original
+ o2.obj.should == :init_copy
+ o2.original.should.equal?(o)
+ end
+
+ it "does not preserve the object_id" do
+ o1 = ObjectSpecDupInitCopy.new
+ old_object_id = o1.object_id
+ o2 = o1.send(@method)
+ o2.object_id.should_not == old_object_id
+ end
+
+ it "returns nil for NilClass" do
+ nil.send(@method).should == nil
+ end
+
+ it "returns true for TrueClass" do
+ true.send(@method).should == true
+ end
+
+ it "returns false for FalseClass" do
+ false.send(@method).should == false
+ end
+
+ it "returns the same Integer for Integer" do
+ 1.send(@method).should == 1
+ end
+
+ it "returns the same Symbol for Symbol" do
+ :my_symbol.send(@method).should == :my_symbol
+ end
+
+ it "returns self for Complex" do
+ c = Complex(1.3, 3.1)
+ c.send(@method).should.equal? c
+ end
+
+ it "returns self for Rational" do
+ r = Rational(1, 3)
+ r.send(@method).should.equal? r
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/kind_of.rb b/spec/ruby/core/kernel/shared/kind_of.rb
new file mode 100644
index 0000000000..a5e0eab08f
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/kind_of.rb
@@ -0,0 +1,55 @@
+require_relative '../fixtures/classes'
+
+describe :kernel_kind_of, shared: true do
+ before :each do
+ @o = KernelSpecs::KindaClass.new
+ end
+
+ it "returns true if given class is the object's class" do
+ @o.send(@method, KernelSpecs::KindaClass).should == true
+ end
+
+ it "returns true if given class is an ancestor of the object's class" do
+ @o.send(@method, KernelSpecs::AncestorClass).should == true
+ @o.send(@method, String).should == true
+ @o.send(@method, Object).should == true
+ end
+
+ it "returns false if the given class is not object's class nor an ancestor" do
+ @o.send(@method, Array).should == false
+ end
+
+ it "returns true if given a Module that is included in object's class" do
+ @o.send(@method, KernelSpecs::MyModule).should == true
+ end
+
+ it "returns true if given a Module that is included one of object's ancestors only" do
+ @o.send(@method, KernelSpecs::AncestorModule).should == true
+ end
+
+ it "returns true if given a Module that object has been extended with" do
+ @o.send(@method, KernelSpecs::MyExtensionModule).should == true
+ end
+
+ it "returns true if given a Module that object has been prepended with" do
+ @o.send(@method, KernelSpecs::MyPrependedModule).should == true
+ end
+
+ it "returns false if given a Module not included nor prepended in object's class nor ancestors" do
+ @o.send(@method, KernelSpecs::SomeOtherModule).should == false
+ end
+
+ it "raises a TypeError if given an object that is not a Class nor a Module" do
+ -> { @o.send(@method, 1) }.should.raise(TypeError)
+ -> { @o.send(@method, 'KindaClass') }.should.raise(TypeError)
+ -> { @o.send(@method, :KindaClass) }.should.raise(TypeError)
+ -> { @o.send(@method, Object.new) }.should.raise(TypeError)
+ end
+
+ it "does not take into account `class` method overriding" do
+ def @o.class; Integer; end
+
+ @o.send(@method, Integer).should == false
+ @o.send(@method, KernelSpecs::KindaClass).should == true
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/lambda.rb b/spec/ruby/core/kernel/shared/lambda.rb
new file mode 100644
index 0000000000..83de06bb41
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/lambda.rb
@@ -0,0 +1,11 @@
+describe :kernel_lambda, shared: true do
+ it "returns a Proc object" do
+ send(@method) { true }.kind_of?(Proc).should == true
+ end
+
+ it "raises an ArgumentError when no block is given" do
+ suppress_warning do
+ -> { send(@method) }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/load.rb b/spec/ruby/core/kernel/shared/load.rb
new file mode 100644
index 0000000000..20d23a623e
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/load.rb
@@ -0,0 +1,215 @@
+main = self
+
+# The big difference is Kernel#load does not attempt to add an extension to the passed path, unlike Kernel#require
+describe :kernel_load, shared: true do
+ before :each do
+ CodeLoadingSpecs.spec_setup
+ @path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR
+ end
+
+ after :each do
+ CodeLoadingSpecs.spec_cleanup
+ end
+
+ describe "(path resolution)" do
+ # This behavior is specific to Kernel#load, it differs for Kernel#require
+ it "loads a non-extensioned file as a Ruby source file" do
+ path = File.expand_path "load_fixture", CODE_LOADING_DIR
+ @object.load(path).should == true
+ ScratchPad.recorded.should == [:no_ext]
+ end
+
+ it "loads a non .rb extensioned file as a Ruby source file" do
+ path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR
+ @object.load(path).should == true
+ ScratchPad.recorded.should == [:no_rb_ext]
+ end
+
+ it "loads from the current working directory" do
+ Dir.chdir CODE_LOADING_DIR do
+ @object.load("load_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+
+ # This behavior is specific to Kernel#load, it differs for Kernel#require
+ it "does not look for a c-extension file when passed a path without extension (when no .rb is present)" do
+ path = File.join CODE_LOADING_DIR, "a", "load_fixture"
+ -> { @object.send(@method, path) }.should.raise(LoadError)
+ end
+ end
+
+ it "loads a file that recursively requires itself" do
+ path = File.expand_path "recursive_require_fixture.rb", CODE_LOADING_DIR
+ -> {
+ @object.load(path).should == true
+ }.should complain(/circular require considered harmful/, verbose: true)
+ ScratchPad.recorded.should == [:loaded, :loaded]
+ end
+
+ it "loads a file that recursively loads itself" do
+ path = File.expand_path "recursive_load_fixture.rb", CODE_LOADING_DIR
+ @object.load(path).should == true
+ ScratchPad.recorded.should == [:loaded, :loaded]
+ end
+
+ it "loads a file each time the method is called" do
+ @object.load(@path).should == true
+ @object.load(@path).should == true
+ ScratchPad.recorded.should == [:loaded, :loaded]
+ end
+
+ it "loads a file even when the name appears in $LOADED_FEATURES" do
+ $LOADED_FEATURES << @path
+ @object.load(@path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a file that has been loaded by #require" do
+ @object.require(@path).should == true
+ @object.load(@path).should == true
+ ScratchPad.recorded.should == [:loaded, :loaded]
+ end
+
+ it "loads file even after $LOAD_PATH change" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ @object.load("load_fixture.rb").should == true
+ $LOAD_PATH.unshift CODE_LOADING_DIR + "/gem"
+ @object.load("load_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded, :loaded_gem]
+ end
+
+ it "does not cause #require with the same path to fail" do
+ @object.load(@path).should == true
+ @object.require(@path).should == true
+ ScratchPad.recorded.should == [:loaded, :loaded]
+ end
+
+ it "does not add the loaded path to $LOADED_FEATURES" do
+ saved_loaded_features = $LOADED_FEATURES.dup
+ @object.load(@path).should == true
+ $LOADED_FEATURES.should == saved_loaded_features
+ end
+
+ it "raises a LoadError if passed a non-extensioned path that does not exist but a .rb extensioned path does exist" do
+ path = File.expand_path "load_ext_fixture", CODE_LOADING_DIR
+ -> { @object.load(path) }.should.raise(LoadError)
+ end
+
+ describe "when passed true for 'wrap'" do
+ it "loads from an existing path" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ @object.load(path, true).should == true
+ end
+
+ it "sets the enclosing scope to an anonymous module" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ @object.load(path, true)
+
+ Object.const_defined?(:LoadSpecWrap).should == false
+
+ wrap_module = ScratchPad.recorded[1]
+ wrap_module.should.instance_of?(Module)
+ end
+
+ it "allows referencing outside namespaces" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ @object.load(path, true)
+
+ ScratchPad.recorded[0].should.equal?(String)
+ end
+
+ it "sets self as a copy of the top-level main" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ @object.load(path, true)
+
+ top_level = ScratchPad.recorded[2]
+ top_level.to_s.should == "main"
+ top_level.method(:to_s).owner.should == top_level.singleton_class
+ top_level.should_not.equal?(main)
+ top_level.should.instance_of?(Object)
+ end
+
+ it "includes modules included in main's singleton class in self's class" do
+ mod = Module.new
+ main.extend(mod)
+
+ main_ancestors = main.singleton_class.ancestors[1..-1]
+ main_ancestors.first.should == mod
+
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ @object.load(path, true)
+
+ top_level = ScratchPad.recorded[2]
+ top_level_ancestors = top_level.singleton_class.ancestors[-main_ancestors.size..-1]
+ top_level_ancestors.should == main_ancestors
+
+ wrap_module = ScratchPad.recorded[1]
+ top_level.singleton_class.ancestors.should == [top_level.singleton_class, wrap_module, *main_ancestors]
+ end
+
+ describe "with top-level methods" do
+ before :each do
+ path = File.expand_path "load_wrap_method_fixture.rb", CODE_LOADING_DIR
+ @object.load(path, true)
+ end
+
+ it "allows calling top-level methods" do
+ ScratchPad.recorded.last.should == :load_wrap_loaded
+ end
+
+ it "does not pollute the receiver" do
+ -> { @object.send(:top_level_method) }.should.raise(NameError)
+ end
+ end
+ end
+
+ describe "when passed a module for 'wrap'" do
+ it "sets the enclosing scope to the supplied module" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ mod = Module.new
+ @object.load(path, mod)
+
+ Object.const_defined?(:LoadSpecWrap).should == false
+ mod.const_defined?(:LoadSpecWrap).should == true
+
+ wrap_module = ScratchPad.recorded[1]
+ wrap_module.should == mod
+ end
+
+ it "makes constants and instance methods in the source file reachable with the supplied module" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ mod = Module.new
+ @object.load(path, mod)
+
+ mod::LOAD_WRAP_SPECS_TOP_LEVEL_CONSTANT.should == 1
+ obj = Object.new
+ obj.extend(mod)
+ obj.send(:load_wrap_specs_top_level_method).should == :load_wrap_specs_top_level_method
+ end
+
+ it "makes instance methods in the source file private" do
+ path = File.expand_path "load_wrap_fixture.rb", CODE_LOADING_DIR
+ mod = Module.new
+ @object.load(path, mod)
+
+ mod.private_instance_methods.include?(:load_wrap_specs_top_level_method).should == true
+ end
+ end
+
+ describe "(shell expansion)" do
+ before :each do
+ @env_home = ENV["HOME"]
+ ENV["HOME"] = CODE_LOADING_DIR
+ end
+
+ after :each do
+ ENV["HOME"] = @env_home
+ end
+
+ it "expands a tilde to the HOME environment variable as the path to load" do
+ @object.require("~/load_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/method.rb b/spec/ruby/core/kernel/shared/method.rb
new file mode 100644
index 0000000000..82abc287d1
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/method.rb
@@ -0,0 +1,56 @@
+require_relative '../../../spec_helper'
+
+describe :kernel_method, shared: true do
+ it "returns a method object for a valid method" do
+ class KernelSpecs::Foo; def bar; 'done'; end; end
+ m = KernelSpecs::Foo.new.send(@method, :bar)
+ m.should.instance_of? Method
+ m.call.should == 'done'
+ end
+
+ it "returns a method object for a valid singleton method" do
+ class KernelSpecs::Foo; def self.bar; 'class done'; end; end
+ m = KernelSpecs::Foo.send(@method, :bar)
+ m.should.instance_of? Method
+ m.call.should == 'class done'
+ end
+
+ it "returns a method object if respond_to_missing?(method) is true" do
+ m = KernelSpecs::RespondViaMissing.new.send(@method, :handled_publicly)
+ m.should.instance_of? Method
+ m.call(42).should == "Done handled_publicly([42])"
+ end
+
+ it "the returned method object if respond_to_missing?(method) calls #method_missing with a Symbol name" do
+ m = KernelSpecs::RespondViaMissing.new.send(@method, "handled_publicly")
+ m.should.instance_of? Method
+ m.call(42).should == "Done handled_publicly([42])"
+ end
+
+ it "raises a NameError for an invalid method name" do
+ class KernelSpecs::Foo; def bar; 'done'; end; end
+ -> {
+ KernelSpecs::Foo.new.send(@method, :invalid_and_silly_method_name)
+ }.should.raise(NameError)
+ end
+
+ it "raises a NameError for an invalid singleton method name" do
+ class KernelSpecs::Foo; def self.bar; 'done'; end; end
+ -> { KernelSpecs::Foo.send(@method, :baz) }.should.raise(NameError)
+ end
+
+ it "changes the method called for super on a target aliased method" do
+ c1 = Class.new do
+ def a; 'a'; end
+ def b; 'b'; end
+ end
+ c2 = Class.new(c1) do
+ def a; super; end
+ alias b a
+ end
+
+ c2.new.a.should == 'a'
+ c2.new.b.should == 'a'
+ c2.new.send(@method, :b).call.should == 'a'
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/require.rb b/spec/ruby/core/kernel/shared/require.rb
new file mode 100644
index 0000000000..6272b00665
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/require.rb
@@ -0,0 +1,846 @@
+describe :kernel_require_basic, shared: true do
+ describe "(path resolution)" do
+ it "loads an absolute path" do
+ path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR
+ @object.send(@method, path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a non-canonical absolute path" do
+ path = File.join CODE_LOADING_DIR, "..", "code", "load_fixture.rb"
+ @object.send(@method, path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a file defining many methods" do
+ path = File.expand_path "methods_fixture.rb", CODE_LOADING_DIR
+ @object.send(@method, path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "raises a LoadError if the file does not exist" do
+ path = File.expand_path "nonexistent.rb", CODE_LOADING_DIR
+ File.should_not.exist?(path)
+ -> { @object.send(@method, path) }.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+
+ # Can't make a file unreadable on these platforms
+ platform_is_not :windows, :cygwin do
+ as_user do
+ describe "with an unreadable file" do
+ before :each do
+ @path = tmp("unreadable_file.rb")
+ touch @path
+ File.chmod 0000, @path
+ end
+
+ after :each do
+ File.chmod 0666, @path
+ rm_r @path
+ end
+
+ it "raises a LoadError" do
+ File.should.exist?(@path)
+ -> { @object.send(@method, @path) }.should.raise(LoadError)
+ end
+ end
+ end
+ end
+
+ it "calls #to_str on non-String objects" do
+ path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR
+ name = mock("load_fixture.rb mock")
+ name.should_receive(:to_str).and_return(path)
+ @object.send(@method, name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> { @object.send(@method, nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> { @object.send(@method, 42) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Array" do
+ -> { @object.send(@method, []) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that does not provide #to_str" do
+ -> { @object.send(@method, mock("not a filename")) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that has #to_s but not #to_str" do
+ name = mock("load_fixture.rb mock")
+ name.stub!(:to_s).and_return("load_fixture.rb")
+ $LOAD_PATH << "."
+ Dir.chdir CODE_LOADING_DIR do
+ -> { @object.send(@method, name) }.should.raise(TypeError)
+ end
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ name = mock("#to_str returns nil")
+ name.should_receive(:to_str).at_least(1).times.and_return(nil)
+ -> { @object.send(@method, name) }.should.raise(TypeError)
+ end
+
+ it "calls #to_path on non-String objects" do
+ name = mock("load_fixture.rb mock")
+ name.stub!(:to_path).and_return("load_fixture.rb")
+ $LOAD_PATH << "."
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, name).should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "calls #to_path on a String" do
+ path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR
+ str = mock("load_fixture.rb mock")
+ str.should_receive(:to_path).and_return(path)
+ @object.send(@method, str).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "calls #to_str on non-String objects returned by #to_path" do
+ path = File.expand_path "load_fixture.rb", CODE_LOADING_DIR
+ name = mock("load_fixture.rb mock")
+ to_path = mock("load_fixture_rb #to_path mock")
+ name.should_receive(:to_path).and_return(to_path)
+ to_path.should_receive(:to_str).and_return(path)
+ @object.send(@method, name).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ # "http://redmine.ruby-lang.org/issues/show/2578"
+ it "loads a ./ relative path from the current working directory with empty $LOAD_PATH" do
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, "./load_fixture.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a ../ relative path from the current working directory with empty $LOAD_PATH" do
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, "../code/load_fixture.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a ./ relative path from the current working directory with non-empty $LOAD_PATH" do
+ $LOAD_PATH << "an_irrelevant_dir"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, "./load_fixture.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a ../ relative path from the current working directory with non-empty $LOAD_PATH" do
+ $LOAD_PATH << "an_irrelevant_dir"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, "../code/load_fixture.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a non-canonical path from the current working directory with non-empty $LOAD_PATH" do
+ $LOAD_PATH << "an_irrelevant_dir"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, "../code/../code/load_fixture.rb").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "resolves a filename against $LOAD_PATH entries" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ @object.send(@method, "load_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "accepts an Object with #to_path in $LOAD_PATH" do
+ obj = mock("to_path")
+ obj.should_receive(:to_path).at_least(:once).and_return(CODE_LOADING_DIR)
+ $LOAD_PATH << obj
+ @object.send(@method, "load_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not require file twice after $LOAD_PATH change" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ @object.require("load_fixture.rb").should == true
+ $LOAD_PATH.push CODE_LOADING_DIR + "/gem"
+ @object.require("load_fixture.rb").should == false
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not resolve a ./ relative path against $LOAD_PATH entries" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ -> do
+ @object.send(@method, "./load_fixture.rb")
+ end.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not resolve a ../ relative path against $LOAD_PATH entries" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ -> do
+ @object.send(@method, "../code/load_fixture.rb")
+ end.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+
+ it "resolves a non-canonical path against $LOAD_PATH entries" do
+ $LOAD_PATH << File.dirname(CODE_LOADING_DIR)
+ @object.send(@method, "code/../code/load_fixture.rb").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a path with duplicate path separators" do
+ $LOAD_PATH << "."
+ sep = File::Separator + File::Separator
+ path = ["..", "code", "load_fixture.rb"].join(sep)
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, path).should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+ end
+ end
+end
+
+describe :kernel_require, shared: true do
+ describe "(path resolution)" do
+ it "loads .rb file when passed absolute path without extension" do
+ path = File.expand_path "load_fixture", CODE_LOADING_DIR
+ @object.send(@method, path).should == true
+ # This should _not_ be [:no_ext]
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ platform_is :linux, :darwin do
+ it "loads c-extension file when passed absolute path without extension when no .rb is present" do
+ # the error message is specific to what dlerror() returns
+ path = File.join CODE_LOADING_DIR, "a", "load_fixture"
+ -> { @object.send(@method, path) }.should.raise(LoadError)
+ end
+ end
+
+ platform_is :darwin do
+ it "loads .bundle file when passed absolute path with .so" do
+ # the error message is specific to what dlerror() returns
+ path = File.join CODE_LOADING_DIR, "a", "load_fixture.so"
+ -> { @object.send(@method, path) }.should.raise(LoadError)
+ end
+ end
+
+ it "does not try an extra .rb if the path already ends in .rb" do
+ path = File.join CODE_LOADING_DIR, "d", "load_fixture.rb"
+ -> { @object.send(@method, path) }.should.raise(LoadError)
+ end
+
+ # For reference see [ruby-core:24155] in which matz confirms this feature is
+ # intentional for security reasons.
+ it "does not load a bare filename unless the current working directory is in $LOAD_PATH" do
+ Dir.chdir CODE_LOADING_DIR do
+ -> { @object.require("load_fixture.rb") }.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ it "does not load a relative path unless the current working directory is in $LOAD_PATH" do
+ Dir.chdir File.dirname(CODE_LOADING_DIR) do
+ -> do
+ @object.require("code/load_fixture.rb")
+ end.should.raise(LoadError)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ it "loads a file that recursively requires itself" do
+ path = File.expand_path "recursive_require_fixture.rb", CODE_LOADING_DIR
+ -> {
+ @object.require(path).should == true
+ }.should complain(/circular require considered harmful/, verbose: true)
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a file concurrently" do
+ path = File.expand_path "concurrent_require_fixture.rb", CODE_LOADING_DIR
+ ScratchPad.record(@object)
+ -> {
+ @object.require(path)
+ }.should_not complain(/circular require considered harmful/, verbose: true)
+ ScratchPad.recorded.join
+ end
+ end
+
+ describe "(non-extensioned path)" do
+ before :each do
+ a = File.expand_path "a", CODE_LOADING_DIR
+ b = File.expand_path "b", CODE_LOADING_DIR
+ $LOAD_PATH.replace [a, b]
+ end
+
+ it "loads a .rb extensioned file when a C-extension file exists on an earlier load path" do
+ @object.require("load_fixture").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not load a feature twice when $LOAD_PATH has been modified" do
+ $LOAD_PATH.replace [CODE_LOADING_DIR]
+ @object.require("load_fixture").should == true
+ $LOAD_PATH.replace [File.expand_path("b", CODE_LOADING_DIR), CODE_LOADING_DIR]
+ @object.require("load_fixture").should == false
+ end
+
+ it "stores the missing path in a LoadError object" do
+ path = "abcd1234"
+
+ -> {
+ @object.send(@method, path)
+ }.should.raise(LoadError) { |e|
+ e.path.should == path
+ }
+ end
+ end
+
+ describe "(file extensions)" do
+ it "loads a .rb extensioned file when passed a non-extensioned path" do
+ path = File.expand_path "load_fixture", CODE_LOADING_DIR
+ File.should.exist?(path)
+ @object.require(path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a .rb extensioned file when a C-extension file of the same name is loaded" do
+ $LOADED_FEATURES << File.expand_path("load_fixture.bundle", CODE_LOADING_DIR)
+ $LOADED_FEATURES << File.expand_path("load_fixture.dylib", CODE_LOADING_DIR)
+ $LOADED_FEATURES << File.expand_path("load_fixture.so", CODE_LOADING_DIR)
+ $LOADED_FEATURES << File.expand_path("load_fixture.dll", CODE_LOADING_DIR)
+ path = File.expand_path "load_fixture", CODE_LOADING_DIR
+ @object.require(path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not load a C-extension file if a .rb extensioned file is already loaded" do
+ $LOADED_FEATURES << File.expand_path("load_fixture.rb", CODE_LOADING_DIR)
+ path = File.expand_path "load_fixture", CODE_LOADING_DIR
+ @object.require(path).should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "loads a .rb extensioned file when passed a non-.rb extensioned path" do
+ path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR
+ File.should.exist?(path)
+ @object.require(path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a .rb extensioned file when a complex-extensioned C-extension file of the same name is loaded" do
+ $LOADED_FEATURES << File.expand_path("load_fixture.ext.bundle", CODE_LOADING_DIR)
+ $LOADED_FEATURES << File.expand_path("load_fixture.ext.dylib", CODE_LOADING_DIR)
+ $LOADED_FEATURES << File.expand_path("load_fixture.ext.so", CODE_LOADING_DIR)
+ $LOADED_FEATURES << File.expand_path("load_fixture.ext.dll", CODE_LOADING_DIR)
+ path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR
+ @object.require(path).should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "does not load a C-extension file if a complex-extensioned .rb file is already loaded" do
+ $LOADED_FEATURES << File.expand_path("load_fixture.ext.rb", CODE_LOADING_DIR)
+ path = File.expand_path "load_fixture.ext", CODE_LOADING_DIR
+ @object.require(path).should == false
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "($LOADED_FEATURES)" do
+ before :each do
+ @path = File.expand_path("load_fixture.rb", CODE_LOADING_DIR)
+ end
+
+ it "stores an absolute path" do
+ @object.require(@path).should == true
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ platform_is_not :windows do
+ describe "with symlinks" do
+ before :each do
+ @symlink_to_code_dir = tmp("codesymlink")
+ File.symlink(CODE_LOADING_DIR, @symlink_to_code_dir)
+
+ $LOAD_PATH.delete(CODE_LOADING_DIR)
+ $LOAD_PATH.unshift(@symlink_to_code_dir)
+ end
+
+ after :each do
+ rm_r @symlink_to_code_dir
+ end
+
+ it "does not canonicalize the path and stores a path with symlinks" do
+ symlink_path = "#{@symlink_to_code_dir}/load_fixture.rb"
+ canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb"
+ @object.require(symlink_path).should == true
+ ScratchPad.recorded.should == [:loaded]
+
+ features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') }
+ features.should.include?(symlink_path)
+ features.should_not.include?(canonical_path)
+ end
+
+ it "stores the same path that __FILE__ returns in the required file" do
+ symlink_path = "#{@symlink_to_code_dir}/load_fixture_and__FILE__.rb"
+ @object.require(symlink_path).should == true
+ loaded_feature = $LOADED_FEATURES.last
+ ScratchPad.recorded.should == [loaded_feature]
+ end
+
+ it "requires only once when a new matching file added to path" do
+ @object.require('load_fixture').should == true
+ ScratchPad.recorded.should == [:loaded]
+
+ symlink_to_code_dir_two = tmp("codesymlinktwo")
+ File.symlink("#{CODE_LOADING_DIR}/b", symlink_to_code_dir_two)
+ begin
+ $LOAD_PATH.unshift(symlink_to_code_dir_two)
+
+ @object.require('load_fixture').should == false
+ ensure
+ rm_r symlink_to_code_dir_two
+ end
+ end
+ end
+
+ describe "with symlinks in the required feature and $LOAD_PATH" do
+ before :each do
+ @dir = tmp("realdir")
+ mkdir_p @dir
+ @file = "#{@dir}/realfile.rb"
+ touch(@file) { |f| f.puts 'ScratchPad << __FILE__' }
+
+ @symlink_to_dir = tmp("symdir").freeze
+ File.symlink(@dir, @symlink_to_dir)
+ @symlink_to_file = "#{@dir}/symfile.rb"
+ File.symlink("realfile.rb", @symlink_to_file)
+ end
+
+ after :each do
+ rm_r @dir, @symlink_to_dir
+ end
+
+ it "canonicalizes the entry in $LOAD_PATH but not the filename passed to #require" do
+ $LOAD_PATH.unshift(@symlink_to_dir)
+ @object.require("symfile").should == true
+ loaded_feature = "#{@dir}/symfile.rb"
+ ScratchPad.recorded.should == [loaded_feature]
+ $".last.should == loaded_feature
+ $LOAD_PATH[0].should == @symlink_to_dir
+ end
+ end
+ end
+
+ it "does not store the path if the load fails" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ saved_loaded_features = $LOADED_FEATURES.dup
+ -> { @object.require("raise_fixture.rb") }.should.raise(RuntimeError)
+ $LOADED_FEATURES.should == saved_loaded_features
+ end
+
+ it "does not load an absolute path that is already stored" do
+ $LOADED_FEATURES << @path
+ @object.require(@path).should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not load a ./ relative path that is already stored" do
+ $LOADED_FEATURES << "./load_fixture.rb"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("./load_fixture.rb").should == false
+ end
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not load a ../ relative path that is already stored" do
+ $LOADED_FEATURES << "../load_fixture.rb"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("../load_fixture.rb").should == false
+ end
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not load a non-canonical path that is already stored" do
+ $LOADED_FEATURES << "code/../code/load_fixture.rb"
+ $LOAD_PATH << File.dirname(CODE_LOADING_DIR)
+ @object.require("code/../code/load_fixture.rb").should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "respects being replaced with a new array" do
+ prev = $LOADED_FEATURES.dup
+
+ @object.require(@path).should == true
+ $LOADED_FEATURES.should.include?(@path)
+
+ $LOADED_FEATURES.replace(prev)
+
+ $LOADED_FEATURES.should_not.include?(@path)
+ @object.require(@path).should == true
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "does not load twice the same file with and without extension" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ @object.require("load_fixture.rb").should == true
+ @object.require("load_fixture").should == false
+ end
+
+ describe "when a non-extensioned file is in $LOADED_FEATURES" do
+ before :each do
+ $LOADED_FEATURES << "load_fixture"
+ end
+
+ it "loads a .rb extensioned file when a non extensioned file is in $LOADED_FEATURES" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ @object.require("load_fixture").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "loads a .rb extensioned file from a subdirectory" do
+ $LOAD_PATH << File.dirname(CODE_LOADING_DIR)
+ @object.require("code/load_fixture").should == true
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "returns false if the file is not found" do
+ Dir.chdir File.dirname(CODE_LOADING_DIR) do
+ @object.require("load_fixture").should == false
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ it "returns false when passed a path and the file is not found" do
+ $LOADED_FEATURES << "code/load_fixture"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("code/load_fixture").should == false
+ ScratchPad.recorded.should == []
+ end
+ end
+ end
+
+ it "stores ../ relative paths as absolute paths" do
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("../code/load_fixture.rb").should == true
+ end
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "stores ./ relative paths as absolute paths" do
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("./load_fixture.rb").should == true
+ end
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "collapses duplicate path separators" do
+ $LOAD_PATH << "."
+ sep = File::Separator + File::Separator
+ path = ["..", "code", "load_fixture.rb"].join(sep)
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require(path).should == true
+ end
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "expands absolute paths containing .." do
+ path = File.join CODE_LOADING_DIR, "..", "code", "load_fixture.rb"
+ @object.require(path).should == true
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "adds the suffix of the resolved filename" do
+ $LOAD_PATH << CODE_LOADING_DIR
+ @object.require("load_fixture").should == true
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "does not load a non-canonical path for a file already loaded" do
+ $LOADED_FEATURES << @path
+ $LOAD_PATH << File.dirname(CODE_LOADING_DIR)
+ @object.require("code/../code/load_fixture.rb").should == false
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not load a ./ relative path for a file already loaded" do
+ $LOADED_FEATURES << @path
+ $LOAD_PATH << "an_irrelevant_dir"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("./load_fixture.rb").should == false
+ end
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not load a ../ relative path for a file already loaded" do
+ $LOADED_FEATURES << @path
+ $LOAD_PATH << "an_irrelevant_dir"
+ Dir.chdir CODE_LOADING_DIR do
+ @object.require("../code/load_fixture.rb").should == false
+ end
+ ScratchPad.recorded.should == []
+ end
+
+ it "unicode_normalize is part of core and not $LOADED_FEATURES" do
+ features = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems')
+ features.lines.each { |feature|
+ feature.should_not.include?("unicode_normalize")
+ }
+
+ -> { @object.require("unicode_normalize") }.should.raise(LoadError)
+ end
+
+ it "does not load a file earlier on the $LOAD_PATH when other similar features were already loaded" do
+ Dir.chdir CODE_LOADING_DIR do
+ @object.send(@method, "../code/load_fixture").should == true
+ end
+ ScratchPad.recorded.should == [:loaded]
+
+ $LOAD_PATH.unshift "#{CODE_LOADING_DIR}/b"
+ # This loads because the above load was not on the $LOAD_PATH
+ @object.send(@method, "load_fixture").should == true
+ ScratchPad.recorded.should == [:loaded, :loaded]
+
+ $LOAD_PATH.unshift "#{CODE_LOADING_DIR}/c"
+ # This does not load because the above load was on the $LOAD_PATH
+ @object.send(@method, "load_fixture").should == false
+ ScratchPad.recorded.should == [:loaded, :loaded]
+ end
+ end
+
+ describe "(shell expansion)" do
+ before :each do
+ @path = File.expand_path("load_fixture.rb", CODE_LOADING_DIR)
+ @env_home = ENV["HOME"]
+ ENV["HOME"] = CODE_LOADING_DIR
+ end
+
+ after :each do
+ ENV["HOME"] = @env_home
+ end
+
+ # "#3171"
+ it "performs tilde expansion on a .rb file before storing paths in $LOADED_FEATURES" do
+ @object.require("~/load_fixture.rb").should == true
+ $LOADED_FEATURES.should.include?(@path)
+ end
+
+ it "performs tilde expansion on a non-extensioned file before storing paths in $LOADED_FEATURES" do
+ @object.require("~/load_fixture").should == true
+ $LOADED_FEATURES.should.include?(@path)
+ end
+ end
+
+ describe "(concurrently)" do
+ before :each do
+ ScratchPad.record []
+ @path = File.expand_path "concurrent.rb", CODE_LOADING_DIR
+ @path2 = File.expand_path "concurrent2.rb", CODE_LOADING_DIR
+ @path3 = File.expand_path "concurrent3.rb", CODE_LOADING_DIR
+ end
+
+ after :each do
+ ScratchPad.clear
+ $LOADED_FEATURES.delete @path
+ $LOADED_FEATURES.delete @path2
+ $LOADED_FEATURES.delete @path3
+ end
+
+ # Quick note about these specs:
+ #
+ # The behavior we're spec'ing requires that t2 enter #require, see t1 is
+ # loading @path, grab a lock, and wait on it.
+ #
+ # We do make sure that t2 starts the require once t1 is in the middle
+ # of concurrent.rb, but we then need to get t2 to get far enough into #require
+ # to see t1's lock and try to lock it.
+ it "blocks a second thread from returning while the 1st is still requiring" do
+ fin = false
+
+ t1_res = nil
+ t2_res = nil
+
+ t2 = nil
+ t1 = Thread.new do
+ Thread.pass until t2
+ Thread.current[:wait_for] = t2
+ t1_res = @object.require(@path)
+ Thread.pass until fin
+ ScratchPad.recorded << :t1_post
+ end
+
+ t2 = Thread.new do
+ Thread.pass until t1[:in_concurrent_rb]
+ $VERBOSE, @verbose = nil, $VERBOSE
+ begin
+ t2_res = @object.require(@path)
+ ScratchPad.recorded << :t2_post
+ ensure
+ $VERBOSE = @verbose
+ fin = true
+ end
+ end
+
+ t1.join
+ t2.join
+
+ t1_res.should == true
+ t2_res.should == false
+
+ ScratchPad.recorded.should == [:con_pre, :con_post, :t2_post, :t1_post]
+ end
+
+ it "blocks based on the path" do
+ t1_res = nil
+ t2_res = nil
+
+ t2 = nil
+ t1 = Thread.new do
+ Thread.pass until t2
+ Thread.current[:concurrent_require_thread] = t2
+ t1_res = @object.require(@path2)
+ end
+
+ t2 = Thread.new do
+ Thread.pass until t1[:in_concurrent_rb2]
+ t2_res = @object.require(@path3)
+ end
+
+ t1.join
+ t2.join
+
+ t1_res.should == true
+ t2_res.should == true
+
+ ScratchPad.recorded.should == [:con2_pre, :con3, :con2_post]
+ end
+
+ it "allows a 2nd require if the 1st raised an exception" do
+ fin = false
+
+ t2_res = nil
+
+ t2 = nil
+ t1 = Thread.new do
+ Thread.pass until t2
+ Thread.current[:wait_for] = t2
+ Thread.current[:con_raise] = true
+
+ -> {
+ @object.require(@path)
+ }.should.raise(RuntimeError)
+
+ Thread.pass until fin
+ ScratchPad.recorded << :t1_post
+ end
+
+ t2 = Thread.new do
+ Thread.pass until t1[:in_concurrent_rb]
+ $VERBOSE, @verbose = nil, $VERBOSE
+ begin
+ t2_res = @object.require(@path)
+ ScratchPad.recorded << :t2_post
+ ensure
+ $VERBOSE = @verbose
+ fin = true
+ end
+ end
+
+ t1.join
+ t2.join
+
+ t2_res.should == true
+
+ ScratchPad.recorded.should == [:con_pre, :con_pre, :con_post, :t2_post, :t1_post]
+ end
+
+ # "redmine #5754"
+ it "blocks a 3rd require if the 1st raises an exception and the 2nd is still running" do
+ fin = false
+
+ t1_res = nil
+ t2_res = nil
+
+ raised = false
+
+ t2 = nil
+ t1 = Thread.new do
+ Thread.current[:con_raise] = true
+
+ -> {
+ @object.require(@path)
+ }.should.raise(RuntimeError)
+
+ raised = true
+
+ # This hits the bug. Because MRI removes its internal lock from a table
+ # when the exception is raised, this #require doesn't see that t2 is in
+ # the middle of requiring the file, so this #require runs when it should not.
+ Thread.pass until t2 && t2[:in_concurrent_rb]
+ t1_res = @object.require(@path)
+
+ Thread.pass until fin
+ ScratchPad.recorded << :t1_post
+ end
+
+ t2 = Thread.new do
+ Thread.pass until raised
+ Thread.current[:wait_for] = t1
+ begin
+ t2_res = @object.require(@path)
+ ScratchPad.recorded << :t2_post
+ ensure
+ fin = true
+ end
+ end
+
+ t1.join
+ t2.join
+
+ t1_res.should == false
+ t2_res.should == true
+
+ ScratchPad.recorded.should == [:con_pre, :con_pre, :con_post, :t2_post, :t1_post]
+ end
+ end
+
+ it "stores the missing path in a LoadError object" do
+ path = "abcd1234"
+
+ -> {
+ @object.send(@method, path)
+ }.should.raise(LoadError) { |e|
+ e.path.should == path
+ }
+ end
+
+ platform_is :linux, :darwin do
+ it "does not store the missing path in a LoadError object when c-extension file exists but loading fails and passed absolute path without extension" do
+ # the error message is specific to what dlerror() returns
+ path = File.join CODE_LOADING_DIR, "a", "load_fixture"
+ -> { @object.send(@method, path) }.should.raise(LoadError) { |e|
+ e.path.should == nil
+ }
+ end
+ end
+
+ platform_is :darwin do
+ it "does not store the missing path in a LoadError object when c-extension file exists but loading fails and passed absolute path with extension" do
+ # the error message is specific to what dlerror() returns
+ path = File.join CODE_LOADING_DIR, "a", "load_fixture.bundle"
+ -> { @object.send(@method, path) }.should.raise(LoadError) { |e|
+ e.path.should == nil
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/sprintf.rb b/spec/ruby/core/kernel/shared/sprintf.rb
new file mode 100644
index 0000000000..b40bd95f59
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/sprintf.rb
@@ -0,0 +1,1034 @@
+describe :kernel_sprintf, shared: true do
+ describe "integer formats" do
+ it "converts argument into Integer with to_int" do
+ obj = Object.new
+ def obj.to_i; 10; end
+ def obj.to_int; 10; end
+
+ obj.should_receive(:to_int).and_return(10)
+ @method.call("%b", obj).should == "1010"
+ end
+
+ it "converts argument into Integer with to_i if to_int isn't available" do
+ obj = Object.new
+ def obj.to_i; 10; end
+
+ obj.should_receive(:to_i).and_return(10)
+ @method.call("%b", obj).should == "1010"
+ end
+
+ it "converts String argument with Kernel#Integer" do
+ @method.call("%d", "0b1010").should == "10"
+ @method.call("%d", "112").should == "112"
+ @method.call("%d", "0127").should == "87"
+ @method.call("%d", "0xc4").should == "196"
+ @method.call("%d", "0").should == "0"
+ end
+
+ it "raises TypeError exception if cannot convert to Integer" do
+ -> {
+ @method.call("%b", Object.new)
+ }.should.raise(TypeError)
+ end
+
+ ["b", "B"].each do |f|
+ describe f do
+ it "converts argument as a binary number" do
+ @method.call("%#{f}", 10).should == "1010"
+ end
+
+ it "displays negative number as a two's complement prefixed with '..1'" do
+ @method.call("%#{f}", -10).should == "..1" + "0110"
+ end
+
+ it "collapse negative number representation if it equals 1" do
+ @method.call("%#{f}", -1).should_not == "..11"
+ @method.call("%#{f}", -1).should == "..1"
+ end
+ end
+ end
+
+ ["d", "i", "u"].each do |f|
+ describe f do
+ it "converts argument as a decimal number" do
+ @method.call("%#{f}", 112).should == "112"
+ @method.call("%#{f}", -112).should == "-112"
+ end
+
+ it "works well with large numbers" do
+ @method.call("%#{f}", 1234567890987654321).should == "1234567890987654321"
+ end
+ end
+ end
+
+ describe "o" do
+ it "converts argument as an octal number" do
+ @method.call("%o", 87).should == "127"
+ end
+
+ it "displays negative number as a two's complement prefixed with '..7'" do
+ @method.call("%o", -87).should == "..7" + "651"
+ end
+
+ it "collapse negative number representation if it equals 7" do
+ @method.call("%o", -1).should_not == "..77"
+ @method.call("%o", -1).should == "..7"
+ end
+ end
+
+ describe "x" do
+ it "converts argument as a hexadecimal number" do
+ @method.call("%x", 196).should == "c4"
+ end
+
+ it "displays negative number as a two's complement prefixed with '..f'" do
+ @method.call("%x", -196).should == "..f" + "3c"
+ end
+
+ it "collapse negative number representation if it equals f" do
+ @method.call("%x", -1).should_not == "..ff"
+ @method.call("%x", -1).should == "..f"
+ end
+ end
+
+ describe "X" do
+ it "converts argument as a hexadecimal number with uppercase letters" do
+ @method.call("%X", 196).should == "C4"
+ end
+
+ it "displays negative number as a two's complement prefixed with '..f'" do
+ @method.call("%X", -196).should == "..F" + "3C"
+ end
+
+ it "collapse negative number representation if it equals F" do
+ @method.call("%X", -1).should_not == "..FF"
+ @method.call("%X", -1).should == "..F"
+ end
+ end
+
+ %w[b B d i u o x X].each do |f|
+ describe f do
+ it "converts to the empty string if precision is 0 and value is 0" do
+ @method.call("%.#{f}", 0).should == ""
+ @method.call("%.0#{f}", 0).should == ""
+ end
+
+ it "pads the empty string if precision is 0 and value is 0" do
+ @method.call("%2.#{f}", 0).should == " "
+ @method.call("%2.0#{f}", 0).should == " "
+ end
+ end
+ end
+ end
+
+ describe "float formats" do
+ it "converts argument into Float" do
+ obj = mock("float")
+ obj.should_receive(:to_f).and_return(9.6)
+ @method.call("%f", obj).should == "9.600000"
+ end
+
+ it "raises TypeError exception if cannot convert to Float" do
+ -> {
+ @method.call("%f", Object.new)
+ }.should.raise(TypeError)
+ end
+
+ {"e" => "e", "E" => "E"}.each_pair do |f, exp|
+ describe f do
+ it "converts argument into exponential notation [-]d.dddddde[+-]dd" do
+ @method.call("%#{f}", 109.52).should == "1.095200#{exp}+02"
+ @method.call("%#{f}", -109.52).should == "-1.095200#{exp}+02"
+ @method.call("%#{f}", 0.10952).should == "1.095200#{exp}-01"
+ @method.call("%#{f}", -0.10952).should == "-1.095200#{exp}-01"
+ end
+
+ it "cuts excessive digits and keeps only 6 ones" do
+ @method.call("%#{f}", 1.123456789).should == "1.123457#{exp}+00"
+ end
+
+ it "rounds the last significant digit to the closest one" do
+ @method.call("%#{f}", 1.555555555).should == "1.555556#{exp}+00"
+ @method.call("%#{f}", -1.555555555).should == "-1.555556#{exp}+00"
+ @method.call("%#{f}", 1.444444444).should == "1.444444#{exp}+00"
+ end
+
+ it "displays Float::INFINITY as Inf" do
+ @method.call("%#{f}", Float::INFINITY).should == "Inf"
+ @method.call("%#{f}", -Float::INFINITY).should == "-Inf"
+ end
+
+ it "displays Float::NAN as NaN" do
+ @method.call("%#{f}", Float::NAN).should == "NaN"
+ @method.call("%#{f}", -Float::NAN).should == "NaN"
+ end
+ end
+ end
+
+ describe "f" do
+ it "converts floating point argument as [-]ddd.dddddd" do
+ @method.call("%f", 10.952).should == "10.952000"
+ @method.call("%f", -10.952).should == "-10.952000"
+ end
+
+ it "cuts excessive digits and keeps only 6 ones" do
+ @method.call("%f", 1.123456789).should == "1.123457"
+ end
+
+ it "rounds the last significant digit to the closest one" do
+ @method.call("%f", 1.555555555).should == "1.555556"
+ @method.call("%f", -1.555555555).should == "-1.555556"
+ @method.call("%f", 1.444444444).should == "1.444444"
+ end
+
+ it "displays Float::INFINITY as Inf" do
+ @method.call("%f", Float::INFINITY).should == "Inf"
+ @method.call("%f", -Float::INFINITY).should == "-Inf"
+ end
+
+ it "displays Float::NAN as NaN" do
+ @method.call("%f", Float::NAN).should == "NaN"
+ @method.call("%f", -Float::NAN).should == "NaN"
+ end
+ end
+
+ {"g" => "e", "G" => "E"}.each_pair do |f, exp|
+ describe f do
+ context "the exponent is less than -4" do
+ it "converts a floating point number using exponential form" do
+ @method.call("%#{f}", 0.0000123456).should == "1.23456#{exp}-05"
+ @method.call("%#{f}", -0.0000123456).should == "-1.23456#{exp}-05"
+
+ @method.call("%#{f}", 0.000000000123456).should == "1.23456#{exp}-10"
+ @method.call("%#{f}", -0.000000000123456).should == "-1.23456#{exp}-10"
+ end
+ end
+
+ context "the exponent is greater than or equal to the precision (6 by default)" do
+ it "converts a floating point number using exponential form" do
+ @method.call("%#{f}", 1234567).should == "1.23457#{exp}+06"
+ @method.call("%#{f}", 1234567890123).should == "1.23457#{exp}+12"
+ @method.call("%#{f}", -1234567).should == "-1.23457#{exp}+06"
+ end
+ end
+
+ context "otherwise" do
+ it "converts a floating point number in dd.dddd form" do
+ @method.call("%#{f}", 0.0001).should == "0.0001"
+ @method.call("%#{f}", -0.0001).should == "-0.0001"
+ @method.call("%#{f}", 123456).should == "123456"
+ @method.call("%#{f}", -123456).should == "-123456"
+ end
+
+ it "cuts excessive digits in fractional part and keeps only 4 ones" do
+ @method.call("%#{f}", 12.12341111).should == "12.1234"
+ @method.call("%#{f}", -12.12341111).should == "-12.1234"
+ end
+
+ it "rounds the last significant digit to the closest one in fractional part" do
+ @method.call("%#{f}", 1.555555555).should == "1.55556"
+ @method.call("%#{f}", -1.555555555).should == "-1.55556"
+ @method.call("%#{f}", 1.444444444).should == "1.44444"
+ end
+
+ it "cuts fraction part to have only 6 digits at all" do
+ @method.call("%#{f}", 1.1234567).should == "1.12346"
+ @method.call("%#{f}", 12.1234567).should == "12.1235"
+ @method.call("%#{f}", 123.1234567).should == "123.123"
+ @method.call("%#{f}", 1234.1234567).should == "1234.12"
+ @method.call("%#{f}", 12345.1234567).should == "12345.1"
+ @method.call("%#{f}", 123456.1234567).should == "123456"
+ end
+ end
+
+ it "displays Float::INFINITY as Inf" do
+ @method.call("%#{f}", Float::INFINITY).should == "Inf"
+ @method.call("%#{f}", -Float::INFINITY).should == "-Inf"
+ end
+
+ it "displays Float::NAN as NaN" do
+ @method.call("%#{f}", Float::NAN).should == "NaN"
+ @method.call("%#{f}", -Float::NAN).should == "NaN"
+ end
+ end
+ end
+
+ describe "a" do
+ it "converts floating point argument as [-]0xh.hhhhp[+-]dd" do
+ @method.call("%a", 196).should == "0x1.88p+7"
+ @method.call("%a", -196).should == "-0x1.88p+7"
+ @method.call("%a", 196.1).should == "0x1.8833333333333p+7"
+ @method.call("%a", 0.01).should == "0x1.47ae147ae147bp-7"
+ @method.call("%a", -0.01).should == "-0x1.47ae147ae147bp-7"
+ end
+
+ it "displays Float::INFINITY as Inf" do
+ @method.call("%a", Float::INFINITY).should == "Inf"
+ @method.call("%a", -Float::INFINITY).should == "-Inf"
+ end
+
+ it "displays Float::NAN as NaN" do
+ @method.call("%a", Float::NAN).should == "NaN"
+ @method.call("%a", -Float::NAN).should == "NaN"
+ end
+ end
+
+ describe "A" do
+ it "converts floating point argument as [-]0xh.hhhhp[+-]dd and use uppercase X and P" do
+ @method.call("%A", 196).should == "0X1.88P+7"
+ @method.call("%A", -196).should == "-0X1.88P+7"
+ @method.call("%A", 196.1).should == "0X1.8833333333333P+7"
+ @method.call("%A", 0.01).should == "0X1.47AE147AE147BP-7"
+ @method.call("%A", -0.01).should == "-0X1.47AE147AE147BP-7"
+ end
+
+ it "displays Float::INFINITY as Inf" do
+ @method.call("%A", Float::INFINITY).should == "Inf"
+ @method.call("%A", -Float::INFINITY).should == "-Inf"
+ end
+
+ it "displays Float::NAN as NaN" do
+ @method.call("%A", Float::NAN).should == "NaN"
+ @method.call("%A", -Float::NAN).should == "NaN"
+ end
+ end
+ end
+
+ describe "other formats" do
+ describe "c" do
+ it "displays character if argument is a numeric code of character" do
+ @method.call("%c", 97).should == "a"
+ end
+
+ it "displays character if argument is a single character string" do
+ @method.call("%c", "a").should == "a"
+ end
+
+ it "displays only the first character if argument is a string of several characters" do
+ @method.call("%c", "abc").should == "a"
+ end
+
+ it "displays only the first character if argument is a string of several multibyte characters" do
+ @method.call("%c", "ã‚ã„ã†ãˆãŠ").should == "ã‚"
+ end
+
+ it "displays no characters if argument is an empty string" do
+ @method.call("%c", "").should == ""
+ end
+
+ it "raises TypeError if argument is not String or Integer and cannot be converted to them" do
+ -> {
+ @method.call("%c", [])
+ }.should raise_consistent_error(TypeError, /no implicit conversion of Array into Integer/)
+ end
+
+ it "raises TypeError if argument is nil" do
+ -> {
+ @method.call("%c", nil)
+ }.should raise_consistent_error(TypeError, /no implicit conversion of nil into Integer/)
+ end
+
+ it "tries to convert argument to String with to_str" do
+ obj = BasicObject.new
+ def obj.to_str
+ "a"
+ end
+
+ @method.call("%c", obj).should == "a"
+ end
+
+ it "tries to convert argument to Integer with to_int" do
+ obj = BasicObject.new
+ def obj.to_int
+ 90
+ end
+
+ @method.call("%c", obj).should == "Z"
+ end
+
+ it "raises TypeError if converting to String with to_str returns non-String" do
+ obj = BasicObject.new
+ def obj.to_str
+ :foo
+ end
+
+ -> {
+ @method.call("%c", obj)
+ }.should raise_consistent_error(TypeError, /can't convert BasicObject into String/)
+ end
+
+ it "raises TypeError if converting to Integer with to_int returns non-Integer" do
+ obj = BasicObject.new
+ def obj.to_int
+ :foo
+ end
+
+ -> {
+ @method.call("%c", obj)
+ }.should raise_consistent_error(TypeError, /can't convert BasicObject into Integer/)
+ end
+ end
+
+ describe "p" do
+ it "displays argument.inspect value" do
+ obj = mock("object")
+ obj.should_receive(:inspect).and_return("<inspect-result>")
+ @method.call("%p", obj).should == "<inspect-result>"
+ end
+
+ it "substitutes 'nil' for nil" do
+ @method.call("%p", nil).should == "nil"
+ end
+ end
+
+ describe "s" do
+ it "substitute argument passes as a string" do
+ @method.call("%s", "abc").should == "abc"
+ end
+
+ it "substitutes '' for nil" do
+ @method.call("%s", nil).should == ""
+ end
+
+ it "converts argument to string with to_s" do
+ obj = mock("string")
+ obj.should_receive(:to_s).and_return("abc")
+ @method.call("%s", obj).should == "abc"
+ end
+
+ it "does not try to convert with to_str" do
+ obj = BasicObject.new
+ def obj.to_str
+ "abc"
+ end
+
+ -> {
+ @method.call("%s", obj)
+ }.should.raise(NoMethodError)
+ end
+
+ it "formats a partial substring without including omitted characters" do
+ long_string = "aabbccddhelloddccbbaa"
+ sub_string = long_string[8, 5]
+ sprintf("%.#{1 * 3}s", sub_string).should == "hel"
+ end
+
+ it "formats string with precision" do
+ Kernel.format("%.3s", "hello").should == "hel"
+ Kernel.format("%-3.3s", "hello").should == "hel"
+ end
+
+ it "formats string with width" do
+ @method.call("%6s", "abc").should == " abc"
+ @method.call("%6s", "abcdefg").should == "abcdefg"
+ end
+
+ it "formats string with width and precision" do
+ @method.call("%4.6s", "abc").should == " abc"
+ @method.call("%4.6s", "abcdefg").should == "abcdef"
+ end
+
+ it "formats nil with width" do
+ @method.call("%6s", nil).should == " "
+ end
+
+ it "formats nil with precision" do
+ @method.call("%.6s", nil).should == ""
+ end
+
+ it "formats nil with width and precision" do
+ @method.call("%4.6s", nil).should == " "
+ end
+
+ it "formats multibyte string with precision" do
+ Kernel.format("%.2s", "été").should == "ét"
+ end
+
+ it "preserves encoding of the format string" do
+ str = format('%s'.encode(Encoding::UTF_8), 'foobar')
+ str.encoding.should == Encoding::UTF_8
+
+ str = format('%s'.encode(Encoding::US_ASCII), 'foobar')
+ str.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+ describe "%" do
+ it "alone raises an ArgumentError" do
+ -> {
+ @method.call("%")
+ }.should.raise(ArgumentError)
+ end
+
+ it "is escaped by %" do
+ @method.call("%%").should == "%"
+ @method.call("%%d").should == "%d"
+ end
+ end
+ end
+
+ describe "flags" do
+ describe "space" do
+ context "applies to numeric formats bBdiouxXeEfgGaA" do
+ it "leaves a space at the start of non-negative numbers" do
+ @method.call("% b", 10).should == " 1010"
+ @method.call("% B", 10).should == " 1010"
+ @method.call("% d", 112).should == " 112"
+ @method.call("% i", 112).should == " 112"
+ @method.call("% o", 87).should == " 127"
+ @method.call("% u", 112).should == " 112"
+ @method.call("% x", 196).should == " c4"
+ @method.call("% X", 196).should == " C4"
+
+ @method.call("% e", 109.52).should == " 1.095200e+02"
+ @method.call("% E", 109.52).should == " 1.095200E+02"
+ @method.call("% f", 10.952).should == " 10.952000"
+ @method.call("% g", 12.1234).should == " 12.1234"
+ @method.call("% G", 12.1234).should == " 12.1234"
+ @method.call("% a", 196).should == " 0x1.88p+7"
+ @method.call("% A", 196).should == " 0X1.88P+7"
+ end
+
+ it "does not leave a space at the start of negative numbers" do
+ @method.call("% b", -10).should == "-1010"
+ @method.call("% B", -10).should == "-1010"
+ @method.call("% d", -112).should == "-112"
+ @method.call("% i", -112).should == "-112"
+ @method.call("% o", -87).should == "-127"
+ @method.call("% u", -112).should == "-112"
+ @method.call("% x", -196).should == "-c4"
+ @method.call("% X", -196).should == "-C4"
+
+ @method.call("% e", -109.52).should == "-1.095200e+02"
+ @method.call("% E", -109.52).should == "-1.095200E+02"
+ @method.call("% f", -10.952).should == "-10.952000"
+ @method.call("% g", -12.1234).should == "-12.1234"
+ @method.call("% G", -12.1234).should == "-12.1234"
+ @method.call("% a", -196).should == "-0x1.88p+7"
+ @method.call("% A", -196).should == "-0X1.88P+7"
+ end
+
+ it "prevents converting negative argument to two's complement form" do
+ @method.call("% b", -10).should == "-1010"
+ @method.call("% B", -10).should == "-1010"
+ @method.call("% o", -87).should == "-127"
+ @method.call("% x", -196).should == "-c4"
+ @method.call("% X", -196).should == "-C4"
+ end
+
+ it "treats several white spaces as one" do
+ @method.call("% b", 10).should == " 1010"
+ @method.call("% B", 10).should == " 1010"
+ @method.call("% d", 112).should == " 112"
+ @method.call("% i", 112).should == " 112"
+ @method.call("% o", 87).should == " 127"
+ @method.call("% u", 112).should == " 112"
+ @method.call("% x", 196).should == " c4"
+ @method.call("% X", 196).should == " C4"
+
+ @method.call("% e", 109.52).should == " 1.095200e+02"
+ @method.call("% E", 109.52).should == " 1.095200E+02"
+ @method.call("% f", 10.952).should == " 10.952000"
+ @method.call("% g", 12.1234).should == " 12.1234"
+ @method.call("% G", 12.1234).should == " 12.1234"
+ @method.call("% a", 196).should == " 0x1.88p+7"
+ @method.call("% A", 196).should == " 0X1.88P+7"
+ end
+ end
+ end
+
+ describe "(digit)$" do
+ it "specifies the absolute argument number for this field" do
+ @method.call("%2$b", 0, 10).should == "1010"
+ @method.call("%2$B", 0, 10).should == "1010"
+ @method.call("%2$d", 0, 112).should == "112"
+ @method.call("%2$i", 0, 112).should == "112"
+ @method.call("%2$o", 0, 87).should == "127"
+ @method.call("%2$u", 0, 112).should == "112"
+ @method.call("%2$x", 0, 196).should == "c4"
+ @method.call("%2$X", 0, 196).should == "C4"
+
+ @method.call("%2$e", 0, 109.52).should == "1.095200e+02"
+ @method.call("%2$E", 0, 109.52).should == "1.095200E+02"
+ @method.call("%2$f", 0, 10.952).should == "10.952000"
+ @method.call("%2$g", 0, 12.1234).should == "12.1234"
+ @method.call("%2$G", 0, 12.1234).should == "12.1234"
+ @method.call("%2$a", 0, 196).should == "0x1.88p+7"
+ @method.call("%2$A", 0, 196).should == "0X1.88P+7"
+
+ @method.call("%2$c", 1, 97).should == "a"
+ @method.call("%2$p", "a", []).should == "[]"
+ @method.call("%2$s", "-", "abc").should == "abc"
+ end
+
+ it "raises exception if argument number is bigger than actual arguments list" do
+ -> {
+ @method.call("%4$d", 1, 2, 3)
+ }.should.raise(ArgumentError)
+ end
+
+ it "ignores '-' sign" do
+ @method.call("%2$d", 1, 2, 3).should == "2"
+ @method.call("%-2$d", 1, 2, 3).should == "2"
+ end
+
+ it "raises ArgumentError exception when absolute and relative argument numbers are mixed" do
+ -> {
+ @method.call("%1$d %d", 1, 2)
+ }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "#" do
+ context "applies to format o" do
+ it "increases the precision until the first digit will be `0' if it is not formatted as complements" do
+ @method.call("%#o", 87).should == "0127"
+ end
+
+ it "does nothing for negative argument" do
+ @method.call("%#o", -87).should == "..7651"
+ end
+ end
+
+ context "applies to formats bBxX" do
+ it "prefixes the result with 0x, 0X, 0b and 0B respectively for non-zero argument" do
+ @method.call("%#b", 10).should == "0b1010"
+ @method.call("%#b", -10).should == "0b..10110"
+ @method.call("%#B", 10).should == "0B1010"
+ @method.call("%#B", -10).should == "0B..10110"
+
+ @method.call("%#x", 196).should == "0xc4"
+ @method.call("%#x", -196).should == "0x..f3c"
+ @method.call("%#X", 196).should == "0XC4"
+ @method.call("%#X", -196).should == "0X..F3C"
+ end
+
+ it "does nothing for zero argument" do
+ @method.call("%#b", 0).should == "0"
+ @method.call("%#B", 0).should == "0"
+
+ @method.call("%#x", 0).should == "0"
+ @method.call("%#X", 0).should == "0"
+ end
+
+ it "does nothing for zero argument when combined with zero precision" do
+ @method.call("%#.0b", 0).should == ""
+ @method.call("%#.0B", 0).should == ""
+
+ @method.call("%#.0x", 0).should == ""
+ @method.call("%#.0X", 0).should == ""
+ end
+ end
+
+ context "applies to format o" do
+ it "does nothing for zero argument" do
+ @method.call("%#o", 0).should == "0"
+ @method.call("%#.1o", 0).should == "0"
+ end
+
+ it "increases the precision if precision zero is requested with zero argument" do
+ @method.call("%#.0o", 0).should == "0"
+ end
+ end
+
+ context "applies to formats aAeEfgG" do
+ it "forces a decimal point to be added, even if no digits follow" do
+ @method.call("%#.0a", 16.25).should == "0x1.p+4"
+ @method.call("%#.0A", 16.25).should == "0X1.P+4"
+
+ @method.call("%#.0e", 100).should == "1.e+02"
+ @method.call("%#.0E", 100).should == "1.E+02"
+
+ @method.call("%#.0f", 123.4).should == "123."
+
+ @method.call("%#g", 123456).should == "123456."
+ @method.call("%#G", 123456).should == "123456."
+ end
+
+ it "changes format from dd.dddd to exponential form for gG" do
+ @method.call("%#.0g", 123.4).should_not == "123."
+ @method.call("%#.0g", 123.4).should == "1.e+02"
+ end
+ end
+
+ context "applies to gG" do
+ it "does not remove trailing zeros" do
+ @method.call("%#g", 123.4).should == "123.400"
+ @method.call("%#g", 123.4).should == "123.400"
+ end
+ end
+ end
+
+ describe "+" do
+ context "applies to numeric formats bBdiouxXaAeEfgG" do
+ it "adds a leading plus sign to non-negative numbers" do
+ @method.call("%+b", 10).should == "+1010"
+ @method.call("%+B", 10).should == "+1010"
+ @method.call("%+d", 112).should == "+112"
+ @method.call("%+i", 112).should == "+112"
+ @method.call("%+o", 87).should == "+127"
+ @method.call("%+u", 112).should == "+112"
+ @method.call("%+x", 196).should == "+c4"
+ @method.call("%+X", 196).should == "+C4"
+
+ @method.call("%+e", 109.52).should == "+1.095200e+02"
+ @method.call("%+E", 109.52).should == "+1.095200E+02"
+ @method.call("%+f", 10.952).should == "+10.952000"
+ @method.call("%+g", 12.1234).should == "+12.1234"
+ @method.call("%+G", 12.1234).should == "+12.1234"
+ @method.call("%+a", 196).should == "+0x1.88p+7"
+ @method.call("%+A", 196).should == "+0X1.88P+7"
+ end
+
+ it "does not use two's complement form for negative numbers for formats bBoxX" do
+ @method.call("%+b", -10).should == "-1010"
+ @method.call("%+B", -10).should == "-1010"
+ @method.call("%+o", -87).should == "-127"
+ @method.call("%+x", -196).should == "-c4"
+ @method.call("%+X", -196).should == "-C4"
+ end
+ end
+ end
+
+ describe "-" do
+ it "left-justifies the result of conversion if width is specified" do
+ @method.call("%-10b", 10).should == "1010 "
+ @method.call("%-10B", 10).should == "1010 "
+ @method.call("%-10d", 112).should == "112 "
+ @method.call("%-10i", 112).should == "112 "
+ @method.call("%-10o", 87).should == "127 "
+ @method.call("%-10u", 112).should == "112 "
+ @method.call("%-10x", 196).should == "c4 "
+ @method.call("%-10X", 196).should == "C4 "
+
+ @method.call("%-20e", 109.52).should == "1.095200e+02 "
+ @method.call("%-20E", 109.52).should == "1.095200E+02 "
+ @method.call("%-20f", 10.952).should == "10.952000 "
+ @method.call("%-20g", 12.1234).should == "12.1234 "
+ @method.call("%-20G", 12.1234).should == "12.1234 "
+ @method.call("%-20a", 196).should == "0x1.88p+7 "
+ @method.call("%-20A", 196).should == "0X1.88P+7 "
+
+ @method.call("%-10c", 97).should == "a "
+ @method.call("%-10p", []).should == "[] "
+ @method.call("%-10s", "abc").should == "abc "
+ end
+ end
+
+ describe "0 (zero)" do
+ context "applies to numeric formats bBdiouxXaAeEfgG and width is specified" do
+ it "pads with zeros, not spaces" do
+ @method.call("%010b", 10).should == "0000001010"
+ @method.call("%010B", 10).should == "0000001010"
+ @method.call("%010d", 112).should == "0000000112"
+ @method.call("%010i", 112).should == "0000000112"
+ @method.call("%010o", 87).should == "0000000127"
+ @method.call("%010u", 112).should == "0000000112"
+ @method.call("%010x", 196).should == "00000000c4"
+ @method.call("%010X", 196).should == "00000000C4"
+
+ @method.call("%020e", 109.52).should == "000000001.095200e+02"
+ @method.call("%020E", 109.52).should == "000000001.095200E+02"
+ @method.call("%020f", 10.952).should == "0000000000010.952000"
+ @method.call("%020g", 12.1234).should == "000000000000012.1234"
+ @method.call("%020G", 12.1234).should == "000000000000012.1234"
+ @method.call("%020a", 196).should == "0x000000000001.88p+7"
+ @method.call("%020A", 196).should == "0X000000000001.88P+7"
+ end
+
+ it "uses radix-1 when displays negative argument as a two's complement" do
+ @method.call("%010b", -10).should == "..11110110"
+ @method.call("%010B", -10).should == "..11110110"
+ @method.call("%010o", -87).should == "..77777651"
+ @method.call("%010x", -196).should == "..ffffff3c"
+ @method.call("%010X", -196).should == "..FFFFFF3C"
+ end
+ end
+ end
+
+ describe "*" do
+ it "uses the previous argument as the field width" do
+ @method.call("%*b", 10, 10).should == " 1010"
+ @method.call("%*B", 10, 10).should == " 1010"
+ @method.call("%*d", 10, 112).should == " 112"
+ @method.call("%*i", 10, 112).should == " 112"
+ @method.call("%*o", 10, 87).should == " 127"
+ @method.call("%*u", 10, 112).should == " 112"
+ @method.call("%*x", 10, 196).should == " c4"
+ @method.call("%*X", 10, 196).should == " C4"
+
+ @method.call("%*e", 20, 109.52).should == " 1.095200e+02"
+ @method.call("%*E", 20, 109.52).should == " 1.095200E+02"
+ @method.call("%*f", 20, 10.952).should == " 10.952000"
+ @method.call("%*g", 20, 12.1234).should == " 12.1234"
+ @method.call("%*G", 20, 12.1234).should == " 12.1234"
+ @method.call("%*a", 20, 196).should == " 0x1.88p+7"
+ @method.call("%*A", 20, 196).should == " 0X1.88P+7"
+
+ @method.call("%*c", 10, 97).should == " a"
+ @method.call("%*p", 10, []).should == " []"
+ @method.call("%*s", 10, "abc").should == " abc"
+ end
+
+ it "left-justifies the result if width is negative" do
+ @method.call("%*b", -10, 10).should == "1010 "
+ @method.call("%*B", -10, 10).should == "1010 "
+ @method.call("%*d", -10, 112).should == "112 "
+ @method.call("%*i", -10, 112).should == "112 "
+ @method.call("%*o", -10, 87).should == "127 "
+ @method.call("%*u", -10, 112).should == "112 "
+ @method.call("%*x", -10, 196).should == "c4 "
+ @method.call("%*X", -10, 196).should == "C4 "
+
+ @method.call("%*e", -20, 109.52).should == "1.095200e+02 "
+ @method.call("%*E", -20, 109.52).should == "1.095200E+02 "
+ @method.call("%*f", -20, 10.952).should == "10.952000 "
+ @method.call("%*g", -20, 12.1234).should == "12.1234 "
+ @method.call("%*G", -20, 12.1234).should == "12.1234 "
+ @method.call("%*a", -20, 196).should == "0x1.88p+7 "
+ @method.call("%*A", -20, 196).should == "0X1.88P+7 "
+
+ @method.call("%*c", -10, 97).should == "a "
+ @method.call("%*p", -10, []).should == "[] "
+ @method.call("%*s", -10, "abc").should == "abc "
+ end
+
+ it "uses the specified argument as the width if * is followed by a number and $" do
+ @method.call("%1$*2$b", 10, 10).should == " 1010"
+ @method.call("%1$*2$B", 10, 10).should == " 1010"
+ @method.call("%1$*2$d", 112, 10).should == " 112"
+ @method.call("%1$*2$i", 112, 10).should == " 112"
+ @method.call("%1$*2$o", 87, 10).should == " 127"
+ @method.call("%1$*2$u", 112, 10).should == " 112"
+ @method.call("%1$*2$x", 196, 10).should == " c4"
+ @method.call("%1$*2$X", 196, 10).should == " C4"
+
+ @method.call("%1$*2$e", 109.52, 20).should == " 1.095200e+02"
+ @method.call("%1$*2$E", 109.52, 20).should == " 1.095200E+02"
+ @method.call("%1$*2$f", 10.952, 20).should == " 10.952000"
+ @method.call("%1$*2$g", 12.1234, 20).should == " 12.1234"
+ @method.call("%1$*2$G", 12.1234, 20).should == " 12.1234"
+ @method.call("%1$*2$a", 196, 20).should == " 0x1.88p+7"
+ @method.call("%1$*2$A", 196, 20).should == " 0X1.88P+7"
+
+ @method.call("%1$*2$c", 97, 10).should == " a"
+ @method.call("%1$*2$p", [], 10).should == " []"
+ @method.call("%1$*2$s", "abc", 10).should == " abc"
+ end
+
+ it "left-justifies the result if specified with $ argument is negative" do
+ @method.call("%1$*2$b", 10, -10).should == "1010 "
+ @method.call("%1$*2$B", 10, -10).should == "1010 "
+ @method.call("%1$*2$d", 112, -10).should == "112 "
+ @method.call("%1$*2$i", 112, -10).should == "112 "
+ @method.call("%1$*2$o", 87, -10).should == "127 "
+ @method.call("%1$*2$u", 112, -10).should == "112 "
+ @method.call("%1$*2$x", 196, -10).should == "c4 "
+ @method.call("%1$*2$X", 196, -10).should == "C4 "
+
+ @method.call("%1$*2$e", 109.52, -20).should == "1.095200e+02 "
+ @method.call("%1$*2$E", 109.52, -20).should == "1.095200E+02 "
+ @method.call("%1$*2$f", 10.952, -20).should == "10.952000 "
+ @method.call("%1$*2$g", 12.1234, -20).should == "12.1234 "
+ @method.call("%1$*2$G", 12.1234, -20).should == "12.1234 "
+ @method.call("%1$*2$a", 196, -20).should == "0x1.88p+7 "
+ @method.call("%1$*2$A", 196, -20).should == "0X1.88P+7 "
+
+ @method.call("%1$*2$c", 97, -10).should == "a "
+ @method.call("%1$*2$p", [], -10).should == "[] "
+ @method.call("%1$*2$s", "abc", -10).should == "abc "
+ end
+
+ it "raises ArgumentError when is mixed with width" do
+ -> {
+ @method.call("%*10d", 10, 112)
+ }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ describe "width" do
+ it "specifies the minimum number of characters that will be written to the result" do
+ @method.call("%10b", 10).should == " 1010"
+ @method.call("%10B", 10).should == " 1010"
+ @method.call("%10d", 112).should == " 112"
+ @method.call("%10i", 112).should == " 112"
+ @method.call("%10o", 87).should == " 127"
+ @method.call("%10u", 112).should == " 112"
+ @method.call("%10x", 196).should == " c4"
+ @method.call("%10X", 196).should == " C4"
+
+ @method.call("%20e", 109.52).should == " 1.095200e+02"
+ @method.call("%20E", 109.52).should == " 1.095200E+02"
+ @method.call("%20f", 10.952).should == " 10.952000"
+ @method.call("%20g", 12.1234).should == " 12.1234"
+ @method.call("%20G", 12.1234).should == " 12.1234"
+ @method.call("%20a", 196).should == " 0x1.88p+7"
+ @method.call("%20A", 196).should == " 0X1.88P+7"
+
+ @method.call("%10c", 97).should == " a"
+ @method.call("%10p", []).should == " []"
+ @method.call("%10s", "abc").should == " abc"
+ end
+
+ it "is ignored if argument's actual length is greater" do
+ @method.call("%5d", 1234567890).should == "1234567890"
+ end
+ end
+
+ describe "precision" do
+ context "integer types" do
+ it "controls the number of decimal places displayed" do
+ @method.call("%.6b", 10).should == "001010"
+ @method.call("%.6B", 10).should == "001010"
+ @method.call("%.5d", 112).should == "00112"
+ @method.call("%.5i", 112).should == "00112"
+ @method.call("%.5o", 87).should == "00127"
+ @method.call("%.5u", 112).should == "00112"
+
+ @method.call("%.5x", 196).should == "000c4"
+ @method.call("%.5X", 196).should == "000C4"
+ end
+ end
+
+ context "float types" do
+ it "controls the number of decimal places displayed in fraction part" do
+ @method.call("%.10e", 109.52).should == "1.0952000000e+02"
+ @method.call("%.10E", 109.52).should == "1.0952000000E+02"
+ @method.call("%.10f", 10.952).should == "10.9520000000"
+ @method.call("%.10a", 196).should == "0x1.8800000000p+7"
+ @method.call("%.10A", 196).should == "0X1.8800000000P+7"
+ end
+
+ it "does not affect G format" do
+ @method.call("%.10g", 12.1234).should == "12.1234"
+ @method.call("%.10g", 123456789).should == "123456789"
+ end
+ end
+
+ context "string formats" do
+ it "determines the maximum number of characters to be copied from the string" do
+ @method.call("%.1p", [1]).should == "["
+ @method.call("%.2p", [1]).should == "[1"
+ @method.call("%.10p", [1]).should == "[1]"
+ @method.call("%.0p", [1]).should == ""
+
+ @method.call("%.1s", "abc").should == "a"
+ @method.call("%.2s", "abc").should == "ab"
+ @method.call("%.10s", "abc").should == "abc"
+ @method.call("%.0s", "abc").should == ""
+ end
+ end
+ end
+
+ describe "reference by name" do
+ describe "%<name>s style" do
+ it "uses value passed in a hash argument" do
+ @method.call("%<foo>d", foo: 123).should == "123"
+ end
+
+ it "supports flags, width, precision and type" do
+ @method.call("%+20.10<foo>f", foo: 10.952).should == " +10.9520000000"
+ end
+
+ it "allows to place name in any position" do
+ @method.call("%+15.5<foo>f", foo: 10.952).should == " +10.95200"
+ @method.call("%+15<foo>.5f", foo: 10.952).should == " +10.95200"
+ @method.call("%+<foo>15.5f", foo: 10.952).should == " +10.95200"
+ @method.call("%<foo>+15.5f", foo: 10.952).should == " +10.95200"
+ end
+
+ it "cannot be mixed with unnamed style" do
+ -> {
+ @method.call("%d %<foo>d", 1, foo: "123")
+ }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "%{name} style" do
+ it "uses value passed in a hash argument" do
+ @method.call("%{foo}", foo: 123).should == "123"
+ end
+
+ it "does not support type style" do
+ @method.call("%{foo}d", foo: 123).should == "123d"
+ end
+
+ it "supports flags, width and precision" do
+ @method.call("%-20.5{foo}", foo: "123456789").should == "12345 "
+ end
+
+ it "cannot be mixed with unnamed style" do
+ -> {
+ @method.call("%d %{foo}", 1, foo: "123")
+ }.should.raise(ArgumentError)
+ end
+
+ it "respects Hash#default when there is no set key" do
+ @method.call("%{foo}", Hash.new(123)).should == "123"
+ @method.call("%{foo}", Hash.new { 123 }).should == "123"
+ end
+
+ it "raises KeyError when Hash#default returns nil" do
+ -> {
+ @method.call("%{foo}", {})
+ }.should.raise(KeyError, 'key{foo} not found')
+
+ -> {
+ @method.call("%{foo}", Hash.new(nil))
+ }.should.raise(KeyError, 'key{foo} not found')
+
+ -> {
+ @method.call("%{foo}", Hash.new { nil })
+ }.should.raise(KeyError, 'key{foo} not found')
+ end
+
+ it "accepts a nil value for an existing key" do
+ @method.call("%{foo}", { foo: nil }).should == ""
+ end
+
+ it "converts value to String with to_s" do
+ obj = Object.new
+ def obj.to_s; end
+ def obj.to_str; end
+
+ obj.should_receive(:to_s).and_return("42")
+ obj.should_not_receive(:to_str)
+
+ @method.call("%{foo}", foo: obj).should == "42"
+ end
+ end
+ end
+
+ describe "faulty key" do
+ before :each do
+ @object = { foooo: 1 }
+ end
+
+ it "raises a KeyError" do
+ -> {
+ @method.call("%<foo>s", @object)
+ }.should.raise(KeyError)
+ end
+
+ it "sets the Hash as the receiver of KeyError" do
+ -> {
+ @method.call("%<foo>s", @object)
+ }.should.raise(KeyError) { |err|
+ err.receiver.should.equal?(@object)
+ }
+ end
+
+ it "sets the unmatched key as the key of KeyError" do
+ -> {
+ @method.call("%<foo>s", @object)
+ }.should.raise(KeyError) { |err|
+ err.key.to_s.should == 'foo'
+ }
+ end
+ end
+
+ it "does not raise error when passed more arguments than needed" do
+ sprintf("%s %d %c", "string", 2, "c", []).should == "string 2 c"
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/sprintf_encoding.rb b/spec/ruby/core/kernel/shared/sprintf_encoding.rb
new file mode 100644
index 0000000000..849c95cbb7
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/sprintf_encoding.rb
@@ -0,0 +1,67 @@
+# Keep encoding-related specs in a separate shared example to be able to skip them in IO/File/StringIO specs.
+# It's difficult to check result's encoding in the test after writing to a file/io buffer.
+describe :kernel_sprintf_encoding, shared: true do
+ it "can produce a string with valid encoding" do
+ string = @method.call("good day %{valid}", valid: "e")
+ string.encoding.should == Encoding::UTF_8
+ string.valid_encoding?.should == true
+ end
+
+ it "can produce a string with invalid encoding" do
+ string = @method.call("good day %{invalid}", invalid: "\x80")
+ string.encoding.should == Encoding::UTF_8
+ string.valid_encoding?.should == false
+ end
+
+ it "returns a String in the same encoding as the format String if compatible" do
+ string = "%s".dup.force_encoding(Encoding::KOI8_U)
+ result = @method.call(string, "dogs")
+ result.encoding.should.equal?(Encoding::KOI8_U)
+ end
+
+ it "returns a String in the argument's encoding if format encoding is more restrictive" do
+ string = "foo %s".dup.force_encoding(Encoding::US_ASCII)
+ argument = "b\303\274r".dup.force_encoding(Encoding::UTF_8)
+
+ result = @method.call(string, argument)
+ result.encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "raises Encoding::CompatibilityError if both encodings are ASCII compatible and there are not ASCII characters" do
+ string = "Ä %s".encode('windows-1252')
+ argument = "Ђ".encode('windows-1251')
+
+ -> {
+ @method.call(string, argument)
+ }.should.raise(Encoding::CompatibilityError)
+ end
+
+ describe "%c" do
+ it "supports Unicode characters" do
+ result = @method.call("%c", 1286)
+ result.should == "Ô†"
+ result.bytes.should == [212, 134]
+
+ result = @method.call("%c", "Ø´")
+ result.should == "Ø´"
+ result.bytes.should == [216, 180]
+ end
+
+ it "raises error when a codepoint isn't representable in an encoding of a format string" do
+ format = "%c".encode("ASCII")
+
+ -> {
+ @method.call(format, 1286)
+ }.should.raise(RangeError, /out of char range/)
+ end
+
+ it "uses the encoding of the format string to interpret codepoints" do
+ format = "%c".dup.force_encoding("euc-jp")
+ result = @method.call(format, 9415601)
+
+ result.encoding.should == Encoding::EUC_JP
+ result.should == "é".encode(Encoding::EUC_JP)
+ result.bytes.should == [143, 171, 177]
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/shared/then.rb b/spec/ruby/core/kernel/shared/then.rb
new file mode 100644
index 0000000000..c71393bf50
--- /dev/null
+++ b/spec/ruby/core/kernel/shared/then.rb
@@ -0,0 +1,20 @@
+describe :kernel_then, shared: true do
+ it "yields self" do
+ object = Object.new
+ object.send(@method) { |o| o.should.equal? object }
+ end
+
+ it "returns the block return value" do
+ object = Object.new
+ object.send(@method) { 42 }.should.equal? 42
+ end
+
+ it "returns a sized Enumerator when no block given" do
+ object = Object.new
+ enum = object.send(@method)
+ enum.should.instance_of? Enumerator
+ enum.size.should.equal? 1
+ enum.peek.should.equal? object
+ enum.first.should.equal? object
+ end
+end
diff --git a/spec/ruby/core/kernel/singleton_class_spec.rb b/spec/ruby/core/kernel/singleton_class_spec.rb
new file mode 100644
index 0000000000..7915272937
--- /dev/null
+++ b/spec/ruby/core/kernel/singleton_class_spec.rb
@@ -0,0 +1,74 @@
+# truffleruby_primitives: true
+require_relative '../../spec_helper'
+
+describe "Kernel#singleton_class" do
+ it "returns class extended from an object" do
+ x = Object.new
+ xs = class << x; self; end
+ xs.should == x.singleton_class
+ end
+
+ it "returns NilClass for nil" do
+ nil.singleton_class.should == NilClass
+ end
+
+ it "returns TrueClass for true" do
+ true.singleton_class.should == TrueClass
+ end
+
+ it "returns FalseClass for false" do
+ false.singleton_class.should == FalseClass
+ end
+
+ it "raises TypeError for Integer" do
+ -> { 123.singleton_class }.should.raise(TypeError, "can't define singleton")
+ end
+
+ it "raises TypeError for Float" do
+ -> { 3.14.singleton_class }.should.raise(TypeError, "can't define singleton")
+ end
+
+ it "raises TypeError for Symbol" do
+ -> { :foo.singleton_class }.should.raise(TypeError, "can't define singleton")
+ end
+
+ it "raises TypeError for a frozen deduplicated String" do
+ -> { (-"string").singleton_class }.should.raise(TypeError, "can't define singleton")
+ -> { a = -"string"; a.singleton_class }.should.raise(TypeError, "can't define singleton")
+ -> { a = "string"; (-a).singleton_class }.should.raise(TypeError, "can't define singleton")
+ end
+
+ it "returns a frozen singleton class if object is frozen" do
+ obj = Object.new
+ obj.freeze
+ obj.singleton_class.frozen?.should == true
+ end
+
+ context "for an IO object with a replaced singleton class" do
+ it "looks up singleton methods from the fresh singleton class after an object instance got a new one" do
+ proxy = -> io { io.foo }
+ if RUBY_ENGINE == 'truffleruby'
+ # We need an inline cache with only this object seen, the best way to do that is to use a Primitive
+ sclass = -> io { Primitive.singleton_class(io) }
+ else
+ sclass = -> io { io.singleton_class }
+ end
+
+ io = File.new(__FILE__)
+ io.define_singleton_method(:foo) { "old" }
+ sclass1 = sclass.call(io)
+ proxy.call(io).should == "old"
+
+ # IO#reopen is the only method which can replace an object's singleton class
+ io2 = File.new(__FILE__)
+ io.reopen(io2)
+ io.define_singleton_method(:foo) { "new" }
+ sclass2 = sclass.call(io)
+ sclass2.should_not.equal?(sclass1)
+ proxy.call(io).should == "new"
+ ensure
+ io2.close
+ io.close
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/singleton_method_spec.rb b/spec/ruby/core/kernel/singleton_method_spec.rb
new file mode 100644
index 0000000000..fe8e23eb02
--- /dev/null
+++ b/spec/ruby/core/kernel/singleton_method_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#singleton_method" do
+ it "finds a method defined on the singleton class" do
+ obj = Object.new
+ def obj.foo; end
+ obj.singleton_method(:foo).should.instance_of?(Method)
+ end
+
+ it "returns a Method which can be called" do
+ obj = Object.new
+ def obj.foo; 42; end
+ obj.singleton_method(:foo).call.should == 42
+ end
+
+ it "only looks at singleton methods and not at methods in the class" do
+ klass = Class.new do
+ def foo
+ 42
+ end
+ end
+ obj = klass.new
+ obj.foo.should == 42
+ -> {
+ obj.singleton_method(:foo)
+ }.should.raise(NameError) { |e|
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ it "raises a NameError if there is no such method" do
+ obj = Object.new
+ -> {
+ obj.singleton_method(:not_existing)
+ }.should.raise(NameError) { |e|
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ ruby_bug "#20620", ""..."3.4" do
+ it "finds a method defined in a module included in the singleton class" do
+ m = Module.new do
+ def foo
+ :foo
+ end
+ end
+
+ obj = Object.new
+ obj.singleton_class.include(m)
+
+ obj.singleton_method(:foo).should.instance_of?(Method)
+ obj.singleton_method(:foo).call.should == :foo
+ end
+
+ it "finds a method defined in a module prepended in the singleton class" do
+ m = Module.new do
+ def foo
+ :foo
+ end
+ end
+
+ obj = Object.new
+ obj.singleton_class.prepend(m)
+
+ obj.singleton_method(:foo).should.instance_of?(Method)
+ obj.singleton_method(:foo).call.should == :foo
+ end
+
+ it "finds a method defined in a module that an object is extended with" do
+ m = Module.new do
+ def foo
+ :foo
+ end
+ end
+
+ obj = Object.new
+ obj.extend(m)
+
+ obj.singleton_method(:foo).should.instance_of?(Method)
+ obj.singleton_method(:foo).call.should == :foo
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/singleton_methods_spec.rb b/spec/ruby/core/kernel/singleton_methods_spec.rb
new file mode 100644
index 0000000000..a7f6969519
--- /dev/null
+++ b/spec/ruby/core/kernel/singleton_methods_spec.rb
@@ -0,0 +1,199 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/reflection'
+require_relative 'fixtures/classes'
+
+describe :kernel_singleton_methods, shared: true do
+ it "returns an empty Array for an object with no singleton methods" do
+ ReflectSpecs.o.singleton_methods(*@object).should == []
+ end
+
+ it "returns the names of module methods for a module" do
+ ReflectSpecs::M.singleton_methods(*@object).to_set.should >= Set[:ms_pro, :ms_pub]
+ end
+
+ it "does not return private module methods for a module" do
+ ReflectSpecs::M.singleton_methods(*@object).should_not.include?(:ms_pri)
+ end
+
+ it "returns the names of class methods for a class" do
+ ReflectSpecs::A.singleton_methods(*@object).to_set.should >= Set[:as_pro, :as_pub]
+ end
+
+ it "does not return private class methods for a class" do
+ ReflectSpecs::A.singleton_methods(*@object).should_not.include?(:as_pri)
+ end
+
+ it "returns the names of singleton methods for an object" do
+ ReflectSpecs.os.singleton_methods(*@object).to_set.should >= Set[:os_pro, :os_pub]
+ end
+end
+
+describe :kernel_singleton_methods_modules, shared: true do
+ it "does not return any included methods for a module including a module" do
+ ReflectSpecs::N.singleton_methods(*@object).to_set.should >= Set[:ns_pro, :ns_pub]
+ end
+
+ it "does not return any included methods for a class including a module" do
+ ReflectSpecs::D.singleton_methods(*@object).to_set.should >= Set[:ds_pro, :ds_pub]
+ end
+
+ it "for a module does not return methods in a module prepended to Module itself" do
+ require_relative 'fixtures/singleton_methods'
+ mod = SingletonMethodsSpecs::SelfExtending
+ mod.method(:mspec_test_kernel_singleton_methods).owner.should == SingletonMethodsSpecs::Prepended
+
+ ancestors = mod.singleton_class.ancestors
+ ancestors[0...2].should == [ mod.singleton_class, mod ]
+ ancestors.should.include?(SingletonMethodsSpecs::Prepended)
+
+ # Do not search prepended modules of `Module`, as that's a non-singleton class
+ mod.singleton_methods.should == []
+ end
+end
+
+describe :kernel_singleton_methods_supers, shared: true do
+ it "returns the names of singleton methods for an object extended with a module" do
+ ReflectSpecs.oe.singleton_methods(*@object).to_set.should >= Set[:m_pro, :m_pub]
+ end
+
+ it "returns a unique list for an object extended with a module" do
+ m = ReflectSpecs.oed.singleton_methods(*@object)
+ r = m.select { |x| x == :pub or x == :pro }.sort
+ r.should == [:pro, :pub]
+ end
+
+ it "returns the names of singleton methods for an object extended with two modules" do
+ ReflectSpecs.oee.singleton_methods(*@object).to_set.should >= Set[:m_pro, :m_pub, :n_pro, :n_pub]
+ end
+
+ it "returns the names of singleton methods for an object extended with a module including a module" do
+ ReflectSpecs.oei.singleton_methods(*@object).to_set.should >= Set[:n_pro, :n_pub, :m_pro, :m_pub]
+ end
+
+ it "returns the names of inherited singleton methods for a subclass" do
+ ReflectSpecs::B.singleton_methods(*@object).to_set.should >= Set[:as_pro, :as_pub, :bs_pro, :bs_pub]
+ end
+
+ it "returns a unique list for a subclass" do
+ m = ReflectSpecs::B.singleton_methods(*@object)
+ r = m.select { |x| x == :pub or x == :pro }.sort
+ r.should == [:pro, :pub]
+ end
+
+ it "returns the names of inherited singleton methods for a subclass including a module" do
+ ReflectSpecs::C.singleton_methods(*@object).to_set.should >= Set[:as_pro, :as_pub, :cs_pro, :cs_pub]
+ end
+
+ it "returns a unique list for a subclass including a module" do
+ m = ReflectSpecs::C.singleton_methods(*@object)
+ r = m.select { |x| x == :pub or x == :pro }.sort
+ r.should == [:pro, :pub]
+ end
+
+ it "returns the names of inherited singleton methods for a subclass of a class including a module" do
+ ReflectSpecs::E.singleton_methods(*@object).to_set.should >= Set[:ds_pro, :ds_pub, :es_pro, :es_pub]
+ end
+
+ it "returns the names of inherited singleton methods for a subclass of a class that includes a module, where the subclass also includes a module" do
+ ReflectSpecs::F.singleton_methods(*@object).to_set.should >= Set[:ds_pro, :ds_pub, :fs_pro, :fs_pub]
+ end
+
+ it "returns the names of inherited singleton methods for a class extended with a module" do
+ ReflectSpecs::P.singleton_methods(*@object).to_set.should >= Set[:m_pro, :m_pub]
+ end
+end
+
+describe :kernel_singleton_methods_private_supers, shared: true do
+ it "does not return private singleton methods for an object extended with a module" do
+ ReflectSpecs.oe.singleton_methods(*@object).should_not.include?(:m_pri)
+ end
+
+ it "does not return private singleton methods for an object extended with two modules" do
+ ReflectSpecs.oee.singleton_methods(*@object).should_not.include?(:m_pri)
+ end
+
+ it "does not return private singleton methods for an object extended with a module including a module" do
+ ReflectSpecs.oei.singleton_methods(*@object).should_not.include?(:n_pri)
+ ReflectSpecs.oei.singleton_methods(*@object).should_not.include?(:m_pri)
+ end
+
+ it "does not return private singleton methods for a class extended with a module" do
+ ReflectSpecs::P.singleton_methods(*@object).should_not.include?(:m_pri)
+ end
+
+ it "does not return private inherited singleton methods for a module including a module" do
+ ReflectSpecs::N.singleton_methods(*@object).should_not.include?(:ns_pri)
+ end
+
+ it "does not return private inherited singleton methods for a class including a module" do
+ ReflectSpecs::D.singleton_methods(*@object).should_not.include?(:ds_pri)
+ end
+
+ it "does not return private inherited singleton methods for a subclass" do
+ ReflectSpecs::B.singleton_methods(*@object).should_not.include?(:as_pri)
+ ReflectSpecs::B.singleton_methods(*@object).should_not.include?(:bs_pri)
+ end
+
+ it "does not return private inherited singleton methods for a subclass including a module" do
+ ReflectSpecs::C.singleton_methods(*@object).should_not.include?(:as_pri)
+ ReflectSpecs::C.singleton_methods(*@object).should_not.include?(:cs_pri)
+ end
+
+ it "does not return private inherited singleton methods for a subclass of a class including a module" do
+ ReflectSpecs::E.singleton_methods(*@object).should_not.include?(:ds_pri)
+ ReflectSpecs::E.singleton_methods(*@object).should_not.include?(:es_pri)
+ end
+
+ it "does not return private inherited singleton methods for a subclass of a class that includes a module, where the subclass also includes a module" do
+ ReflectSpecs::F.singleton_methods(*@object).should_not.include?(:ds_pri)
+ ReflectSpecs::F.singleton_methods(*@object).should_not.include?(:fs_pri)
+ end
+end
+
+describe "Kernel#singleton_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :kernel_singleton_methods, nil, []
+ it_behaves_like :kernel_singleton_methods_supers, nil, []
+ it_behaves_like :kernel_singleton_methods_modules, nil, []
+ it_behaves_like :kernel_singleton_methods_private_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :kernel_singleton_methods, nil, true
+ it_behaves_like :kernel_singleton_methods_supers, nil, true
+ it_behaves_like :kernel_singleton_methods_modules, nil, true
+ it_behaves_like :kernel_singleton_methods_private_supers, nil, true
+ end
+
+ describe "when passed false" do
+ it_behaves_like :kernel_singleton_methods, nil, false
+ it_behaves_like :kernel_singleton_methods_modules, nil, false
+ it_behaves_like :kernel_singleton_methods_private_supers, nil, false
+
+ it "returns an empty Array for an object extended with a module" do
+ ReflectSpecs.oe.singleton_methods(false).should == []
+ end
+
+ it "returns an empty Array for an object extended with two modules" do
+ ReflectSpecs.oee.singleton_methods(false).should == []
+ end
+
+ it "returns an empty Array for an object extended with a module including a module" do
+ ReflectSpecs.oei.singleton_methods(false).should == []
+ end
+
+ it "returns the names of singleton methods of the subclass" do
+ ReflectSpecs::B.singleton_methods(false).to_set.should >= Set[:bs_pro, :bs_pub]
+ end
+
+ it "does not return names of inherited singleton methods for a subclass" do
+ ReflectSpecs::B.singleton_methods(false).should_not.include?(:as_pro)
+ ReflectSpecs::B.singleton_methods(false).should_not.include?(:as_pub)
+ end
+
+ it "does not return the names of inherited singleton methods for a class extended with a module" do
+ ReflectSpecs::P.singleton_methods(false).should_not.include?(:m_pro)
+ ReflectSpecs::P.singleton_methods(false).should_not.include?(:m_pub)
+ end
+ end
+end
diff --git a/spec/ruby/core/kernel/sleep_spec.rb b/spec/ruby/core/kernel/sleep_spec.rb
new file mode 100644
index 0000000000..61d8cc2380
--- /dev/null
+++ b/spec/ruby/core/kernel/sleep_spec.rb
@@ -0,0 +1,118 @@
+require_relative '../../spec_helper'
+require_relative '../fiber/fixtures/scheduler'
+
+describe "Kernel#sleep" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:sleep)
+ end
+
+ it "returns an Integer" do
+ sleep(0.001).should.is_a?(Integer)
+ end
+
+ it "accepts a Float" do
+ sleep(0.001).should >= 0
+ end
+
+ it "accepts an Integer" do
+ sleep(0).should >= 0
+ end
+
+ it "accepts a Rational" do
+ sleep(Rational(1, 999)).should >= 0
+ end
+
+ it "accepts any Object that responds to divmod" do
+ o = Object.new
+ def o.divmod(*); [0, 0.001]; end
+ sleep(o).should >= 0
+ end
+
+ it "raises an ArgumentError when passed a negative duration" do
+ -> { sleep(-0.1) }.should.raise(ArgumentError)
+ -> { sleep(-1) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { sleep('2') }.should.raise(TypeError)
+ end
+
+ it "pauses execution indefinitely if not given a duration" do
+ running = false
+ t = Thread.new do
+ running = true
+ sleep
+ 5
+ end
+
+ Thread.pass until running
+ Thread.pass while t.status and t.status != "sleep"
+
+ t.wakeup
+ t.value.should == 5
+ end
+
+ it "sleeps with nanosecond precision" do
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ 100.times do
+ sleep(0.0001)
+ end
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+
+ actual_duration = end_time - start_time
+ actual_duration.should > 0.01 # 100 * 0.0001 => 0.01
+ end
+
+ it "accepts a nil duration" do
+ running = false
+ t = Thread.new do
+ running = true
+ sleep(nil)
+ 5
+ end
+
+ Thread.pass until running
+ Thread.pass while t.status and t.status != "sleep"
+
+ t.wakeup
+ t.value.should == 5
+ end
+
+ context "Kernel.sleep with Fiber scheduler" do
+ before :each do
+ Fiber.set_scheduler(FiberSpecs::LoggingScheduler.new)
+ end
+
+ after :each do
+ Fiber.set_scheduler(nil)
+ end
+
+ it "calls the scheduler without arguments when no duration is given" do
+ sleeper = Fiber.new(blocking: false) do
+ sleep
+ end
+ sleeper.resume
+ Fiber.scheduler.events.should == [{ event: :kernel_sleep, fiber: sleeper, args: [] }]
+ end
+
+ it "calls the scheduler with the given duration" do
+ sleeper = Fiber.new(blocking: false) do
+ sleep(0.01)
+ end
+ sleeper.resume
+ Fiber.scheduler.events.should == [{ event: :kernel_sleep, fiber: sleeper, args: [0.01] }]
+ end
+
+ it "does not call the scheduler if the fiber is blocking" do
+ sleeper = Fiber.new(blocking: true) do
+ sleep(0.01)
+ end
+ sleeper.resume
+ Fiber.scheduler.events.should == []
+ end
+ end
+end
+
+describe "Kernel.sleep" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/spawn_spec.rb b/spec/ruby/core/kernel/spawn_spec.rb
new file mode 100644
index 0000000000..3432fc31da
--- /dev/null
+++ b/spec/ruby/core/kernel/spawn_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# These specs only run a basic usage of #spawn.
+# Process.spawn has more complete specs and they are not
+# run here as it is redundant and takes too long for little gain.
+describe "Kernel#spawn" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:spawn)
+ end
+
+ it "executes the given command" do
+ -> {
+ Process.wait spawn("echo spawn")
+ }.should output_to_fd("spawn\n")
+ end
+end
+
+describe "Kernel.spawn" do
+ it "executes the given command" do
+ -> {
+ Process.wait Kernel.spawn("echo spawn")
+ }.should output_to_fd("spawn\n")
+ end
+end
diff --git a/spec/ruby/core/kernel/sprintf_spec.rb b/spec/ruby/core/kernel/sprintf_spec.rb
new file mode 100644
index 0000000000..5a4a90ff7a
--- /dev/null
+++ b/spec/ruby/core/kernel/sprintf_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/sprintf'
+require_relative 'shared/sprintf_encoding'
+
+describe :kernel_sprintf_to_str, shared: true do
+ it "calls #to_str to convert the format object to a String" do
+ obj = mock('format string')
+ obj.should_receive(:to_str).and_return("to_str: %i")
+ @method.call(obj, 42).should == "to_str: 42"
+ end
+end
+
+describe "Kernel#sprintf" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ r = nil
+ -> {
+ r = sprintf(format, *args)
+ }.should_not complain(verbose: true)
+ r
+ }
+
+ it_behaves_like :kernel_sprintf_encoding, -> format, *args {
+ r = nil
+ -> {
+ r = sprintf(format, *args)
+ }.should_not complain(verbose: true)
+ r
+ }
+
+ it_behaves_like :kernel_sprintf_to_str, -> format, *args {
+ r = nil
+ -> {
+ r = sprintf(format, *args)
+ }.should_not complain(verbose: true)
+ r
+ }
+end
+
+describe "Kernel.sprintf" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ r = nil
+ -> {
+ r = Kernel.sprintf(format, *args)
+ }.should_not complain(verbose: true)
+ r
+ }
+
+ it_behaves_like :kernel_sprintf_encoding, -> format, *args {
+ r = nil
+ -> {
+ r = Kernel.sprintf(format, *args)
+ }.should_not complain(verbose: true)
+ r
+ }
+
+ it_behaves_like :kernel_sprintf_to_str, -> format, *args {
+ r = nil
+ -> {
+ r = Kernel.sprintf(format, *args)
+ }.should_not complain(verbose: true)
+ r
+ }
+end
diff --git a/spec/ruby/core/kernel/srand_spec.rb b/spec/ruby/core/kernel/srand_spec.rb
new file mode 100644
index 0000000000..cbc3a7f4b8
--- /dev/null
+++ b/spec/ruby/core/kernel/srand_spec.rb
@@ -0,0 +1,73 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#srand" do
+ before :each do
+ @seed = srand
+ end
+
+ after :each do
+ srand(@seed)
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:srand)
+ end
+
+ it "returns the previous seed value" do
+ srand(10)
+ srand(20).should == 10
+ end
+
+ it "returns the system-initialized seed value on the first call" do
+ ruby_exe('print srand(10)', options: '--disable-gems').should =~ /\A\d+\z/
+ end
+
+ it "seeds the RNG correctly and repeatably" do
+ srand(10)
+ x = rand
+ srand(10)
+ rand.should == x
+ end
+
+ it "defaults number to a random value" do
+ -> { srand }.should_not.raise
+ srand.should_not == 0
+ end
+
+ it "accepts and uses a seed of 0" do
+ srand(0)
+ srand.should == 0
+ end
+
+ it "accepts a negative seed" do
+ srand(-17)
+ srand.should == -17
+ end
+
+ it "accepts an Integer as a seed" do
+ srand(0x12345678901234567890)
+ srand.should == 0x12345678901234567890
+ end
+
+ it "calls #to_int on seed" do
+ srand(3.8)
+ srand.should == 3
+
+ s = mock('seed')
+ s.should_receive(:to_int).and_return 0
+ srand(s)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { srand(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { srand("7") }.should.raise(TypeError)
+ end
+end
+
+describe "Kernel.srand" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/sub_spec.rb b/spec/ruby/core/kernel/sub_spec.rb
new file mode 100644
index 0000000000..b175e371dc
--- /dev/null
+++ b/spec/ruby/core/kernel/sub_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# FIXME: These methods exist only when the -n or -p option is passed to
+# ruby, but we currently don't have a way of specifying that.
+ruby_version_is ""..."1.9" do
+ describe "Kernel#sub" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:sub)
+ end
+ end
+
+ describe "Kernel#sub!" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:sub!)
+ end
+ end
+
+ describe "Kernel.sub" do
+ it "needs to be reviewed for spec completeness"
+ end
+
+ describe "Kernel.sub!" do
+ it "needs to be reviewed for spec completeness"
+ end
+end
diff --git a/spec/ruby/core/kernel/syscall_spec.rb b/spec/ruby/core/kernel/syscall_spec.rb
new file mode 100644
index 0000000000..63943cdad5
--- /dev/null
+++ b/spec/ruby/core/kernel/syscall_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#syscall" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:syscall)
+ end
+end
+
+describe "Kernel.syscall" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/system_spec.rb b/spec/ruby/core/kernel/system_spec.rb
new file mode 100644
index 0000000000..b24956104a
--- /dev/null
+++ b/spec/ruby/core/kernel/system_spec.rb
@@ -0,0 +1,132 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :kernel_system, shared: true do
+ it "executes the specified command in a subprocess" do
+ -> { @object.system("echo a") }.should output_to_fd("a\n")
+
+ $?.should.instance_of? Process::Status
+ $?.should.success?
+ end
+
+ it "returns true when the command exits with a zero exit status" do
+ @object.system(ruby_cmd('exit 0')).should == true
+
+ $?.should.instance_of? Process::Status
+ $?.should.success?
+ $?.exitstatus.should == 0
+ end
+
+ it "returns false when the command exits with a non-zero exit status" do
+ @object.system(ruby_cmd('exit 1')).should == false
+
+ $?.should.instance_of? Process::Status
+ $?.should_not.success?
+ $?.exitstatus.should == 1
+ end
+
+ it "raises RuntimeError when `exception: true` is given and the command exits with a non-zero exit status" do
+ -> { @object.system(ruby_cmd('exit 1'), exception: true) }.should.raise(RuntimeError)
+ end
+
+ it "raises Errno::ENOENT when `exception: true` is given and the specified command does not exist" do
+ -> { @object.system('feature_14386', exception: true) }.should.raise(Errno::ENOENT)
+ end
+
+ it "returns nil when command execution fails" do
+ @object.system("sad").should == nil
+
+ $?.should.instance_of? Process::Status
+ $?.pid.should.is_a?(Integer)
+ $?.should_not.success?
+ end
+
+ it "does not write to stderr when command execution fails" do
+ -> { @object.system("sad") }.should output_to_fd("", STDERR)
+ end
+
+ platform_is_not :windows do
+ before :each do
+ @shell = ENV['SHELL']
+ end
+
+ after :each do
+ ENV['SHELL'] = @shell
+ end
+
+ it "executes with `sh` if the command contains shell characters" do
+ -> { @object.system("echo $0") }.should output_to_fd("sh\n")
+ end
+
+ it "ignores SHELL env var and always uses `sh`" do
+ ENV['SHELL'] = "/bin/fakeshell"
+ -> { @object.system("echo $0") }.should output_to_fd("sh\n")
+ end
+ end
+
+ platform_is_not :windows do
+ before :each do
+ require 'tmpdir'
+ @shell_command = File.join(Dir.mktmpdir, "noshebang.cmd")
+ File.write(@shell_command, %[echo "$PATH"\n], perm: 0o700)
+ end
+
+ after :each do
+ File.unlink(@shell_command)
+ Dir.rmdir(File.dirname(@shell_command))
+ end
+
+ it "executes with `sh` if the command is executable but not binary and there is no shebang" do
+ -> { @object.system(@shell_command) }.should output_to_fd(ENV['PATH'] + "\n")
+ end
+ end
+
+ before :each do
+ ENV['TEST_SH_EXPANSION'] = 'foo'
+ @shell_var = '$TEST_SH_EXPANSION'
+ platform_is :windows do
+ @shell_var = '%TEST_SH_EXPANSION%'
+ end
+ end
+
+ after :each do
+ ENV.delete('TEST_SH_EXPANSION')
+ end
+
+ it "expands shell variables when given a single string argument" do
+ -> { @object.system("echo #{@shell_var}") }.should output_to_fd("foo\n")
+ end
+
+ platform_is_not :windows do
+ it "does not expand shell variables when given multiples arguments" do
+ -> { @object.system("echo", @shell_var) }.should output_to_fd("#{@shell_var}\n")
+ end
+ end
+
+ platform_is :windows do
+ it "does expand shell variables when given multiples arguments" do
+ # See https://bugs.ruby-lang.org/issues/12231
+ -> { @object.system("echo", @shell_var) }.should output_to_fd("foo\n")
+ end
+ end
+
+ platform_is :windows do
+ it "runs commands starting with any number of @ using shell" do
+ `#{ruby_cmd("p system 'does_not_exist'")} 2>NUL`.chomp.should == "nil"
+ @object.system('@does_not_exist 2>NUL').should == false
+ @object.system("@@@#{ruby_cmd('exit 0')}").should == true
+ end
+ end
+end
+
+describe "Kernel#system" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:system)
+ end
+
+ it_behaves_like :kernel_system, :system, KernelSpecs::Method.new
+end
+
+describe "Kernel.system" do
+ it_behaves_like :kernel_system, :system, Kernel
+end
diff --git a/spec/ruby/core/kernel/taint_spec.rb b/spec/ruby/core/kernel/taint_spec.rb
new file mode 100644
index 0000000000..9a2efbaea0
--- /dev/null
+++ b/spec/ruby/core/kernel/taint_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#taint" do
+ it "has been removed" do
+ Object.new.should_not.respond_to?(:taint)
+ end
+end
diff --git a/spec/ruby/core/kernel/tainted_spec.rb b/spec/ruby/core/kernel/tainted_spec.rb
new file mode 100644
index 0000000000..837eb1dafb
--- /dev/null
+++ b/spec/ruby/core/kernel/tainted_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#tainted?" do
+ it "has been removed" do
+ Object.new.should_not.respond_to?(:tainted?)
+ end
+end
diff --git a/spec/ruby/core/kernel/tap_spec.rb b/spec/ruby/core/kernel/tap_spec.rb
new file mode 100644
index 0000000000..453b9b84d5
--- /dev/null
+++ b/spec/ruby/core/kernel/tap_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#tap" do
+ it "always yields self and returns self" do
+ a = KernelSpecs::A.new
+ a.tap{|o| o.should.equal?(a); 42}.should.equal?(a)
+ end
+
+ it "raises a LocalJumpError when no block given" do
+ -> { 3.tap }.should.raise(LocalJumpError)
+ end
+end
diff --git a/spec/ruby/core/kernel/test_spec.rb b/spec/ruby/core/kernel/test_spec.rb
new file mode 100644
index 0000000000..30717dfd98
--- /dev/null
+++ b/spec/ruby/core/kernel/test_spec.rb
@@ -0,0 +1,109 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#test" do
+ before :all do
+ @file = __dir__ + '/fixtures/classes.rb'
+ @dir = __dir__ + '/fixtures'
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:test)
+ end
+
+ it "returns true when passed ?f if the argument is a regular file" do
+ Kernel.test(?f, @file).should == true
+ end
+
+ it "returns true when passed ?e if the argument is a file" do
+ Kernel.test(?e, @file).should == true
+ end
+
+ it "returns true when passed ?d if the argument is a directory" do
+ Kernel.test(?d, @dir).should == true
+ end
+
+ platform_is_not :windows do
+ it "returns true when passed ?l if the argument is a symlink" do
+ link = tmp("file_symlink.lnk")
+ File.symlink(@file, link)
+ begin
+ Kernel.test(?l, link).should == true
+ ensure
+ rm_r link
+ end
+ end
+ end
+
+ it "returns true when passed ?r if the argument is readable by the effective uid" do
+ Kernel.test(?r, @file).should == true
+ end
+
+ it "returns true when passed ?R if the argument is readable by the real uid" do
+ Kernel.test(?R, @file).should == true
+ end
+
+ context "writable test" do
+ before do
+ @tmp_file = tmp("file.kernel.test")
+ touch(@tmp_file)
+ end
+
+ after do
+ rm_r @tmp_file
+ end
+
+ it "returns true when passed ?w if the argument is readable by the effective uid" do
+ Kernel.test(?w, @tmp_file).should == true
+ end
+
+ it "returns true when passed ?W if the argument is readable by the real uid" do
+ Kernel.test(?W, @tmp_file).should == true
+ end
+ end
+
+ context "time commands" do
+ before :each do
+ @tmp_file = File.new(tmp("file.kernel.test"), "w")
+ end
+
+ after :each do
+ @tmp_file.close
+ rm_r @tmp_file
+ end
+
+ it "returns the last access time for the provided file when passed ?A" do
+ Kernel.test(?A, @tmp_file).should == @tmp_file.atime
+ end
+
+ it "returns the time at which the file was created when passed ?C" do
+ Kernel.test(?C, @tmp_file).should == @tmp_file.ctime
+ end
+
+ it "returns the time at which the file was modified when passed ?M" do
+ Kernel.test(?M, @tmp_file).should == @tmp_file.mtime
+ end
+ end
+
+ it "calls #to_path on second argument when passed ?f and a filename" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return @file
+ Kernel.test(?f, p)
+ end
+
+ it "calls #to_path on second argument when passed ?e and a filename" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return @file
+ Kernel.test(?e, p)
+ end
+
+ it "calls #to_path on second argument when passed ?d and a directory" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return @dir
+ Kernel.test(?d, p)
+ end
+end
+
+describe "Kernel.test" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/then_spec.rb b/spec/ruby/core/kernel/then_spec.rb
new file mode 100644
index 0000000000..8109a2960a
--- /dev/null
+++ b/spec/ruby/core/kernel/then_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/then'
+
+describe "Kernel#then" do
+ it_behaves_like :kernel_then, :then
+end
diff --git a/spec/ruby/core/kernel/throw_spec.rb b/spec/ruby/core/kernel/throw_spec.rb
new file mode 100644
index 0000000000..1086126176
--- /dev/null
+++ b/spec/ruby/core/kernel/throw_spec.rb
@@ -0,0 +1,80 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel.throw" do
+ it "transfers control to the end of the active catch block waiting for symbol" do
+ catch(:blah) do
+ :value
+ throw :blah
+ fail("throw didn't transfer the control")
+ end.should == nil
+ end
+
+ it "transfers control to the innermost catch block waiting for the same symbol" do
+ one = two = three = 0
+ catch :duplicate do
+ catch :duplicate do
+ catch :duplicate do
+ one = 1
+ throw :duplicate
+ end
+ two = 2
+ throw :duplicate
+ end
+ three = 3
+ throw :duplicate
+ end
+ [one, two, three].should == [1, 2, 3]
+ end
+
+ it "sets the return value of the catch block to nil by default" do
+ res = catch :blah do
+ throw :blah
+ end
+ res.should == nil
+ end
+
+ it "sets the return value of the catch block to a value specified as second parameter" do
+ res = catch :blah do
+ throw :blah, :return_value
+ end
+ res.should == :return_value
+ end
+
+ it "raises an ArgumentError if there is no catch block for the symbol" do
+ -> { throw :blah }.should.raise(ArgumentError)
+ end
+
+ it "raises an UncaughtThrowError if there is no catch block for the symbol" do
+ -> { throw :blah }.should.raise(UncaughtThrowError)
+ end
+
+ it "raises ArgumentError if 3 or more arguments provided" do
+ -> {
+ catch :blah do
+ throw :blah, :return_value, 2
+ end
+ }.should.raise(ArgumentError)
+
+ -> {
+ catch :blah do
+ throw :blah, :return_value, 2, 3, 4, 5
+ end
+ }.should.raise(ArgumentError)
+ end
+
+ it "can throw an object" do
+ -> {
+ obj = Object.new
+ catch obj do
+ throw obj
+ end
+ }.should_not.raise(NameError)
+ end
+end
+
+describe "Kernel#throw" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:throw)
+ end
+end
diff --git a/spec/ruby/core/kernel/to_enum_spec.rb b/spec/ruby/core/kernel/to_enum_spec.rb
new file mode 100644
index 0000000000..9d9945450f
--- /dev/null
+++ b/spec/ruby/core/kernel/to_enum_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#to_enum" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/to_s_spec.rb b/spec/ruby/core/kernel/to_s_spec.rb
new file mode 100644
index 0000000000..ea4b00151e
--- /dev/null
+++ b/spec/ruby/core/kernel/to_s_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#to_s" do
+ it "returns a String containing the name of self's class" do
+ Object.new.to_s.should =~ /Object/
+ end
+end
diff --git a/spec/ruby/core/kernel/trace_var_spec.rb b/spec/ruby/core/kernel/trace_var_spec.rb
new file mode 100644
index 0000000000..150c3da266
--- /dev/null
+++ b/spec/ruby/core/kernel/trace_var_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#trace_var" do
+ before :each do
+ $Kernel_trace_var_global = nil
+ end
+
+ after :each do
+ untrace_var :$Kernel_trace_var_global
+
+ $Kernel_trace_var_global = nil
+ $Kernel_trace_var_extra = nil
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:trace_var)
+ end
+
+ it "hooks assignments to a global variable" do
+ captured = nil
+
+ trace_var :$Kernel_trace_var_global do |value|
+ captured = value
+ end
+
+ $Kernel_trace_var_global = 'foo'
+ captured.should == 'foo'
+ end
+
+ it "accepts a proc argument instead of a block" do
+ captured = nil
+
+ trace_var :$Kernel_trace_var_global, proc {|value| captured = value}
+
+ $Kernel_trace_var_global = 'foo'
+ captured.should == 'foo'
+ end
+
+ # String arguments should be evaluated in the context of the caller.
+ it "accepts a String argument instead of a Proc or block" do
+ trace_var :$Kernel_trace_var_global, '$Kernel_trace_var_extra = true'
+
+ $Kernel_trace_var_global = 'foo'
+
+ $Kernel_trace_var_extra.should == true
+ end
+
+ it "raises ArgumentError if no block or proc is provided" do
+ -> do
+ trace_var :$Kernel_trace_var_global
+ end.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/kernel/trap_spec.rb b/spec/ruby/core/kernel/trap_spec.rb
new file mode 100644
index 0000000000..8f44f3af4b
--- /dev/null
+++ b/spec/ruby/core/kernel/trap_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Kernel#trap" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:trap)
+ end
+
+ # Behaviour is specified for Signal.trap
+end
diff --git a/spec/ruby/core/kernel/trust_spec.rb b/spec/ruby/core/kernel/trust_spec.rb
new file mode 100644
index 0000000000..ef3fa9a3e1
--- /dev/null
+++ b/spec/ruby/core/kernel/trust_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#trust" do
+ it "has been removed" do
+ Object.new.should_not.respond_to?(:trust)
+ end
+end
diff --git a/spec/ruby/core/kernel/untaint_spec.rb b/spec/ruby/core/kernel/untaint_spec.rb
new file mode 100644
index 0000000000..47e8544bd4
--- /dev/null
+++ b/spec/ruby/core/kernel/untaint_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#untaint" do
+ it "has been removed" do
+ Object.new.should_not.respond_to?(:untaint)
+ end
+end
diff --git a/spec/ruby/core/kernel/untrace_var_spec.rb b/spec/ruby/core/kernel/untrace_var_spec.rb
new file mode 100644
index 0000000000..8b219801c8
--- /dev/null
+++ b/spec/ruby/core/kernel/untrace_var_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#untrace_var" do
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:untrace_var)
+ end
+end
+
+describe "Kernel.untrace_var" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/kernel/untrust_spec.rb b/spec/ruby/core/kernel/untrust_spec.rb
new file mode 100644
index 0000000000..8787ab3fc9
--- /dev/null
+++ b/spec/ruby/core/kernel/untrust_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#untrust" do
+ it "has been removed" do
+ Object.new.should_not.respond_to?(:untrust)
+ end
+end
diff --git a/spec/ruby/core/kernel/untrusted_spec.rb b/spec/ruby/core/kernel/untrusted_spec.rb
new file mode 100644
index 0000000000..29261be9c4
--- /dev/null
+++ b/spec/ruby/core/kernel/untrusted_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#untrusted?" do
+ it "has been removed" do
+ Object.new.should_not.respond_to?(:untrusted?)
+ end
+end
diff --git a/spec/ruby/core/kernel/warn_spec.rb b/spec/ruby/core/kernel/warn_spec.rb
new file mode 100644
index 0000000000..189129dd31
--- /dev/null
+++ b/spec/ruby/core/kernel/warn_spec.rb
@@ -0,0 +1,298 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Kernel#warn" do
+ before :each do
+ @before_verbose = $VERBOSE
+ @before_separator = $/
+ end
+
+ after :each do
+ $VERBOSE = nil
+ $/ = @before_separator
+ $VERBOSE = @before_verbose
+ end
+
+ it "is a private method" do
+ Kernel.private_instance_methods(false).should.include?(:warn)
+ end
+
+ it "accepts multiple arguments" do
+ Kernel.method(:warn).arity.should < 0
+ end
+
+ it "does not append line-end if last character is line-end" do
+ -> {
+ $VERBOSE = true
+ warn("this is some simple text with line-end\n")
+ }.should output(nil, "this is some simple text with line-end\n")
+ end
+
+ it "calls #write on $stderr if $VERBOSE is true" do
+ -> {
+ $VERBOSE = true
+ warn("this is some simple text")
+ }.should output(nil, "this is some simple text\n")
+ end
+
+ it "calls #write on $stderr if $VERBOSE is false" do
+ -> {
+ $VERBOSE = false
+ warn("this is some simple text")
+ }.should output(nil, "this is some simple text\n")
+ end
+
+ it "does not call #write on $stderr if $VERBOSE is nil" do
+ -> {
+ $VERBOSE = nil
+ warn("this is some simple text")
+ }.should output(nil, "")
+ end
+
+ it "writes each argument on a line when passed multiple arguments" do
+ -> {
+ $VERBOSE = true
+ warn("line 1", "line 2")
+ }.should output(nil, "line 1\nline 2\n")
+ end
+
+ it "writes each array element on a line when passes an array" do
+ -> {
+ $VERBOSE = true
+ warn(["line 1", "line 2"])
+ }.should output(nil, "line 1\nline 2\n")
+ end
+
+ it "does not write strings when passed no arguments" do
+ -> {
+ $VERBOSE = true
+ warn
+ }.should output("", "")
+ end
+
+ it "writes the default record separator and NOT $/ to $stderr after the warning message" do
+ -> {
+ $VERBOSE = true
+ $/ = 'rs'
+ warn("")
+ }.should output(nil, /\n/)
+ end
+
+ it "writes to_s representation if passed a non-string" do
+ obj = mock("obj")
+ obj.should_receive(:to_s).and_return("to_s called")
+ -> {
+ $VERBOSE = true
+ warn(obj)
+ }.should output(nil, "to_s called\n")
+ end
+
+ describe ":uplevel keyword argument" do
+ before :each do
+ $VERBOSE = true
+ end
+
+ it "prepends a message with specified line from the backtrace" do
+ w = KernelSpecs::WarnInNestedCall.new
+
+ -> { w.f4("foo", 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: foo|)
+ -> { w.f4("foo", 1) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f1_call_lineno}: warning: foo|)
+ -> { w.f4("foo", 2) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f2_call_lineno}: warning: foo|)
+ -> { w.f4("foo", 3) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f3_call_lineno}: warning: foo|)
+ end
+
+ # Test both explicitly without and with RubyGems as RubyGems overrides Kernel#warn
+ it "shows the caller of #require and not #require itself without RubyGems" do
+ file = fixture(__FILE__ , "warn_require_caller.rb")
+ ruby_exe(file, options: "--disable-gems", args: "2>&1").should == "#{file}:2: warning: warn-require-warning\n"
+ end
+
+ it "shows the caller of #require and not #require itself with RubyGems loaded" do
+ file = fixture(__FILE__ , "warn_require_caller.rb")
+ ruby_exe(file, options: "-rrubygems", args: "2>&1").should == "#{file}:2: warning: warn-require-warning\n"
+ end
+
+ it "doesn't show the caller when the uplevel is `nil`" do
+ w = KernelSpecs::WarnInNestedCall.new
+
+ -> { w.f4("foo", nil) }.should output(nil, "foo\n")
+ end
+
+ guard -> { Kernel.instance_method(:tap).source_location } do
+ it "skips <internal: core library methods defined in Ruby" do
+ file, line = Kernel.instance_method(:tap).source_location
+ file.should.start_with?('<internal:')
+
+ file = fixture(__FILE__ , "warn_core_method.rb")
+ n = 9
+ ruby_exe(file, options: "--disable-gems", args: "2>&1").lines.should == [
+ "#{file}:#{n+0}: warning: use X instead\n",
+ "#{file}:#{n+1}: warning: use X instead\n",
+ "#{file}:#{n+2}: warning: use X instead\n",
+ "#{file}:#{n+4}: warning: use X instead\n",
+ ]
+ end
+ end
+
+ it "accepts :category keyword with a symbol" do
+ -> {
+ $VERBOSE = true
+ warn("message", category: :deprecated)
+ }.should output(nil, "message\n")
+ end
+
+ it "accepts :category keyword with nil" do
+ -> {
+ $VERBOSE = true
+ warn("message", category: nil)
+ }.should output(nil, "message\n")
+ end
+
+ it "accepts :category keyword with object convertible to symbol" do
+ o = Object.new
+ def o.to_sym; :deprecated; end
+ -> {
+ $VERBOSE = true
+ warn("message", category: o)
+ }.should output(nil, "message\n")
+ end
+
+ it "raises if :category keyword is not nil and not convertible to symbol" do
+ -> {
+ $VERBOSE = true
+ warn("message", category: Object.new)
+ }.should.raise(TypeError)
+ end
+
+ it "converts first arg using to_s" do
+ w = KernelSpecs::WarnInNestedCall.new
+
+ -> { w.f4(false, 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: false|)
+ -> { w.f4(nil, 1) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f1_call_lineno}: warning: |)
+ obj = mock("obj")
+ obj.should_receive(:to_s).and_return("to_s called")
+ -> { w.f4(obj, 2) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.f2_call_lineno}: warning: to_s called|)
+ end
+
+ it "does not prepend caller information if the uplevel argument is too large" do
+ w = KernelSpecs::WarnInNestedCall.new
+ -> { w.f4("foo", 100) }.should output(nil, "warning: foo\n")
+ end
+
+ it "prepends even if a message is empty or nil" do
+ w = KernelSpecs::WarnInNestedCall.new
+
+ -> { w.f4("", 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: \n$|)
+ -> { w.f4(nil, 0) }.should output(nil, %r|core/kernel/fixtures/classes.rb:#{w.warn_call_lineno}: warning: \n$|)
+ end
+
+ it "converts value to Integer" do
+ w = KernelSpecs::WarnInNestedCall.new
+
+ -> { w.f4(0.1) }.should output(nil, %r|classes.rb:#{w.warn_call_lineno}:|)
+ -> { w.f4(Rational(1, 2)) }.should output(nil, %r|classes.rb:#{w.warn_call_lineno}:|)
+ end
+
+ it "raises ArgumentError if passed negative value" do
+ -> { warn "", uplevel: -2 }.should.raise(ArgumentError)
+ -> { warn "", uplevel: -100 }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if passed -1" do
+ -> { warn "", uplevel: -1 }.should.raise(ArgumentError)
+ end
+
+ it "raises TypeError if passed not Integer" do
+ -> { warn "", uplevel: "" }.should.raise(TypeError)
+ -> { warn "", uplevel: [] }.should.raise(TypeError)
+ -> { warn "", uplevel: {} }.should.raise(TypeError)
+ -> { warn "", uplevel: Object.new }.should.raise(TypeError)
+ end
+ end
+
+ it "treats empty hash as no keyword argument" do
+ h = {}
+ -> { warn(**h) }.should_not complain(verbose: true)
+ -> { warn('foo', **h) }.should complain("foo\n")
+ end
+
+ it "calls Warning.warn without keyword arguments if Warning.warn does not accept keyword arguments" do
+ verbose = $VERBOSE
+ $VERBOSE = false
+ class << Warning
+ alias_method :_warn, :warn
+ def warn(message)
+ ScratchPad.record(message)
+ end
+ end
+
+ begin
+ ScratchPad.clear
+ Kernel.warn("Chunky bacon!")
+ ScratchPad.recorded.should == "Chunky bacon!\n"
+
+ Kernel.warn("Deprecated bacon!", category: :deprecated)
+ ScratchPad.recorded.should == "Deprecated bacon!\n"
+ ensure
+ class << Warning
+ remove_method :warn
+ alias_method :warn, :_warn
+ remove_method :_warn
+ end
+ $VERBOSE = verbose
+ end
+ end
+
+ it "calls Warning.warn with category: nil if Warning.warn accepts keyword arguments" do
+ Warning.should_receive(:warn).with("Chunky bacon!\n", category: nil)
+ verbose = $VERBOSE
+ $VERBOSE = false
+ begin
+ Kernel.warn("Chunky bacon!")
+ ensure
+ $VERBOSE = verbose
+ end
+ end
+
+ it "calls Warning.warn with given category keyword converted to a symbol" do
+ Warning.should_receive(:warn).with("Chunky bacon!\n", category: :deprecated)
+ verbose = $VERBOSE
+ $VERBOSE = false
+ begin
+ Kernel.warn("Chunky bacon!", category: 'deprecated')
+ ensure
+ $VERBOSE = verbose
+ end
+ end
+
+ it "does not call Warning.warn if self is the Warning module" do
+ # RubyGems redefines Kernel#warn so we need to use a subprocess and disable RubyGems here
+ code = <<-RUBY
+ def Warning.warn(*args, **kwargs)
+ raise 'should not be called'
+ end
+ Kernel.instance_method(:warn).bind(Warning).call('Kernel#warn spec edge case')
+ RUBY
+ out = ruby_exe(code, args: "2>&1", options: "--disable-gems")
+ out.should == "Kernel#warn spec edge case\n"
+ $?.should.success?
+ end
+
+ it "avoids recursion if Warning#warn is redefined and calls super" do
+ # This works because of the spec above, which is the workaround for it.
+ # Note that redefining Warning#warn is a mistake which would naturally end in infinite recursion,
+ # Warning.extend Module.new { def warn } should be used instead.
+ # RubyGems redefines Kernel#warn so we need to use a subprocess and disable RubyGems here
+ code = <<-RUBY
+ module Warning
+ def warn(*args, **kwargs)
+ super
+ end
+ end
+ warn "avoid infinite recursion"
+ RUBY
+ out = ruby_exe(code, args: "2>&1", options: "--disable-gems")
+ out.should == "avoid infinite recursion\n"
+ $?.should.success?
+ end
+end
diff --git a/spec/ruby/core/kernel/yield_self_spec.rb b/spec/ruby/core/kernel/yield_self_spec.rb
new file mode 100644
index 0000000000..e311dcee47
--- /dev/null
+++ b/spec/ruby/core/kernel/yield_self_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/then'
+
+describe "Kernel#yield_self" do
+ it_behaves_like :kernel_then, :yield_self
+end
diff --git a/spec/ruby/core/main/define_method_spec.rb b/spec/ruby/core/main/define_method_spec.rb
new file mode 100644
index 0000000000..5279d078c3
--- /dev/null
+++ b/spec/ruby/core/main/define_method_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+script_binding = binding
+
+describe "main#define_method" do
+ before :each do
+ @code = 'define_method(:boom) { :bam }'
+ end
+
+ after :each do
+ Object.send :remove_method, :boom
+ end
+
+ it 'creates a public method in TOPLEVEL_BINDING' do
+ eval @code, TOPLEVEL_BINDING
+ Object.should.respond_to? :boom
+ end
+
+ it 'creates a public method in script binding' do
+ eval @code, script_binding
+ Object.should.respond_to? :boom
+ end
+
+ it 'returns the method name as symbol' do
+ eval(@code, TOPLEVEL_BINDING).should.equal? :boom
+ end
+end
diff --git a/spec/ruby/core/main/fixtures/classes.rb b/spec/ruby/core/main/fixtures/classes.rb
new file mode 100644
index 0000000000..757cee4e4a
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/classes.rb
@@ -0,0 +1,26 @@
+module MainSpecs
+ module Module
+ end
+
+ module WrapIncludeModule
+ end
+
+ DATA = {}
+end
+
+
+def main_public_method
+end
+public :main_public_method
+
+def main_public_method2
+end
+public :main_public_method2
+
+def main_private_method
+end
+private :main_private_method
+
+def main_private_method2
+end
+private :main_private_method2
diff --git a/spec/ruby/core/main/fixtures/string_refinement.rb b/spec/ruby/core/main/fixtures/string_refinement.rb
new file mode 100644
index 0000000000..2dc6de52ca
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/string_refinement.rb
@@ -0,0 +1,7 @@
+module StringRefinement
+ refine(String) do
+ def foo
+ 'foo'
+ end
+ end
+end
diff --git a/spec/ruby/core/main/fixtures/string_refinement_user.rb b/spec/ruby/core/main/fixtures/string_refinement_user.rb
new file mode 100644
index 0000000000..48620c325f
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/string_refinement_user.rb
@@ -0,0 +1,11 @@
+using StringRefinement
+
+module MainSpecs
+ DATA[:in_module] = 'hello'.foo
+
+ def self.call_foo(x)
+ x.foo
+ end
+end
+
+MainSpecs::DATA[:toplevel] = 'hello'.foo
diff --git a/spec/ruby/core/main/fixtures/using.rb b/spec/ruby/core/main/fixtures/using.rb
new file mode 100644
index 0000000000..30713ef309
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/using.rb
@@ -0,0 +1 @@
+using Module.new
diff --git a/spec/ruby/core/main/fixtures/using_in_main.rb b/spec/ruby/core/main/fixtures/using_in_main.rb
new file mode 100644
index 0000000000..a4a71c89cc
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/using_in_main.rb
@@ -0,0 +1,5 @@
+MAIN = self
+
+module X
+ MAIN.send(:using, Module.new)
+end
diff --git a/spec/ruby/core/main/fixtures/using_in_method.rb b/spec/ruby/core/main/fixtures/using_in_method.rb
new file mode 100644
index 0000000000..d9ea2e9ef0
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/using_in_method.rb
@@ -0,0 +1,5 @@
+def foo
+ using Module.new
+end
+
+foo
diff --git a/spec/ruby/core/main/fixtures/wrapped_include.rb b/spec/ruby/core/main/fixtures/wrapped_include.rb
new file mode 100644
index 0000000000..307c98b419
--- /dev/null
+++ b/spec/ruby/core/main/fixtures/wrapped_include.rb
@@ -0,0 +1 @@
+include MainSpecs::WrapIncludeModule
diff --git a/spec/ruby/core/main/include_spec.rb b/spec/ruby/core/main/include_spec.rb
new file mode 100644
index 0000000000..09f8d4d3b5
--- /dev/null
+++ b/spec/ruby/core/main/include_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "main#include" do
+ it "includes the given Module in Object" do
+ eval "include MainSpecs::Module", TOPLEVEL_BINDING
+ Object.ancestors.should.include?(MainSpecs::Module)
+ end
+
+ context "in a file loaded with wrapping" do
+ it "includes the given Module in the load wrapper" do
+ load(File.expand_path("../fixtures/wrapped_include.rb", __FILE__), true)
+ Object.ancestors.should_not.include?(MainSpecs::WrapIncludeModule)
+ end
+ end
+end
diff --git a/spec/ruby/core/main/private_spec.rb b/spec/ruby/core/main/private_spec.rb
new file mode 100644
index 0000000000..56a39ae3c1
--- /dev/null
+++ b/spec/ruby/core/main/private_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "main#private" do
+ after :each do
+ Object.send(:public, :main_public_method)
+ Object.send(:public, :main_public_method2)
+ end
+
+ context "when single argument is passed and it is not an array" do
+ it "sets the visibility of the given methods to private" do
+ eval "private :main_public_method", TOPLEVEL_BINDING
+ Object.private_methods(true).should.include?(:main_public_method)
+ end
+ end
+
+ context "when multiple arguments are passed" do
+ it "sets the visibility of the given methods to private" do
+ eval "private :main_public_method, :main_public_method2", TOPLEVEL_BINDING
+ Object.private_methods(true).should.include?(:main_public_method)
+ Object.private_methods(true).should.include?(:main_public_method2)
+ end
+ end
+
+ context "when single argument is passed and is an array" do
+ it "sets the visibility of the given methods to private" do
+ eval "private [:main_public_method, :main_public_method2]", TOPLEVEL_BINDING
+ Object.private_methods(true).should.include?(:main_public_method)
+ Object.private_methods(true).should.include?(:main_public_method2)
+ end
+ end
+
+ it "returns argument" do
+ eval("private :main_public_method", TOPLEVEL_BINDING).should.equal?(:main_public_method)
+ end
+
+ it "raises a NameError when at least one of given method names is undefined" do
+ -> do
+ eval "private :main_public_method, :main_undefined_method", TOPLEVEL_BINDING
+ end.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/main/public_spec.rb b/spec/ruby/core/main/public_spec.rb
new file mode 100644
index 0000000000..89368ebb0d
--- /dev/null
+++ b/spec/ruby/core/main/public_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "main#public" do
+ after :each do
+ Object.send(:private, :main_private_method)
+ Object.send(:private, :main_private_method2)
+ end
+
+ context "when single argument is passed and it is not an array" do
+ it "sets the visibility of the given methods to public" do
+ eval "public :main_private_method", TOPLEVEL_BINDING
+ Object.private_methods(true).should_not.include?(:main_private_method)
+ end
+ end
+
+ context "when multiple arguments are passed" do
+ it "sets the visibility of the given methods to public" do
+ eval "public :main_private_method, :main_private_method2", TOPLEVEL_BINDING
+ Object.private_methods(true).should_not.include?(:main_private_method)
+ Object.private_methods(true).should_not.include?(:main_private_method2)
+ end
+ end
+
+ context "when single argument is passed and is an array" do
+ it "sets the visibility of the given methods to public" do
+ eval "public [:main_private_method, :main_private_method2]", TOPLEVEL_BINDING
+ Object.private_methods(true).should_not.include?(:main_private_method)
+ Object.private_methods(true).should_not.include?(:main_private_method2)
+ end
+ end
+
+ it "returns argument" do
+ eval("public :main_private_method", TOPLEVEL_BINDING).should.equal?(:main_private_method)
+ end
+
+
+ it "raises a NameError when given an undefined name" do
+ -> do
+ eval "public :main_undefined_method", TOPLEVEL_BINDING
+ end.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/main/ruby2_keywords_spec.rb b/spec/ruby/core/main/ruby2_keywords_spec.rb
new file mode 100644
index 0000000000..d12c0ed4e4
--- /dev/null
+++ b/spec/ruby/core/main/ruby2_keywords_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "main.ruby2_keywords" do
+ it "is the same as Object.ruby2_keywords" do
+ main = TOPLEVEL_BINDING.receiver
+ main.private_methods(false).should.include?(:ruby2_keywords)
+ end
+end
diff --git a/spec/ruby/core/main/to_s_spec.rb b/spec/ruby/core/main/to_s_spec.rb
new file mode 100644
index 0000000000..642cfa4433
--- /dev/null
+++ b/spec/ruby/core/main/to_s_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "main#to_s" do
+ it "returns 'main'" do
+ eval('to_s', TOPLEVEL_BINDING).should == "main"
+ end
+end
diff --git a/spec/ruby/core/main/using_spec.rb b/spec/ruby/core/main/using_spec.rb
new file mode 100644
index 0000000000..314a6be416
--- /dev/null
+++ b/spec/ruby/core/main/using_spec.rb
@@ -0,0 +1,150 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "main.using" do
+ it "requires one Module argument" do
+ -> do
+ eval('using', TOPLEVEL_BINDING)
+ end.should.raise(ArgumentError)
+
+ -> do
+ eval('using "foo"', TOPLEVEL_BINDING)
+ end.should.raise(TypeError)
+ end
+
+ it "uses refinements from the given module only in the target file" do
+ require_relative 'fixtures/string_refinement'
+ load File.expand_path('../fixtures/string_refinement_user.rb', __FILE__)
+ MainSpecs::DATA[:in_module].should == 'foo'
+ MainSpecs::DATA[:toplevel].should == 'foo'
+ -> do
+ 'hello'.foo
+ end.should.raise(NoMethodError)
+ end
+
+ it "uses refinements from the given module for method calls in the target file" do
+ require_relative 'fixtures/string_refinement'
+ load File.expand_path('../fixtures/string_refinement_user.rb', __FILE__)
+ -> do
+ 'hello'.foo
+ end.should.raise(NoMethodError)
+ MainSpecs.call_foo('hello').should == 'foo'
+ end
+
+ it "uses refinements from the given module in the eval string" do
+ cls = MainSpecs::DATA[:cls] = Class.new {def foo; 'foo'; end}
+ MainSpecs::DATA[:mod] = Module.new do
+ refine(cls) do
+ def foo; 'bar'; end
+ end
+ end
+ eval(<<-EOS, TOPLEVEL_BINDING).should == 'bar'
+ using MainSpecs::DATA[:mod]
+ MainSpecs::DATA[:cls].new.foo
+ EOS
+ end
+
+ it "does not affect methods defined before it is called" do
+ cls = Class.new {def foo; 'foo'; end}
+ MainSpecs::DATA[:mod] = Module.new do
+ refine(cls) do
+ def foo; 'bar'; end
+ end
+ end
+ x = MainSpecs::DATA[:x] = Object.new
+ eval <<-EOS, TOPLEVEL_BINDING
+ x = MainSpecs::DATA[:x]
+ def x.before_using(obj)
+ obj.foo
+ end
+ using MainSpecs::DATA[:mod]
+ def x.after_using(obj)
+ obj.foo
+ end
+ EOS
+
+ obj = cls.new
+ x.before_using(obj).should == 'foo'
+ x.after_using(obj).should == 'bar'
+ end
+
+ it "propagates refinements added to existing modules after it is called" do
+ cls = Class.new {def foo; 'foo'; end}
+ mod = MainSpecs::DATA[:mod] = Module.new do
+ refine(cls) do
+ def foo; 'quux'; end
+ end
+ end
+ x = MainSpecs::DATA[:x] = Object.new
+ eval <<-EOS, TOPLEVEL_BINDING
+ using MainSpecs::DATA[:mod]
+ x = MainSpecs::DATA[:x]
+ def x.call_foo(obj)
+ obj.foo
+ end
+ def x.call_bar(obj)
+ obj.bar
+ end
+ EOS
+
+ obj = cls.new
+ x.call_foo(obj).should == 'quux'
+
+ mod.module_eval do
+ refine(cls) do
+ def bar; 'quux'; end
+ end
+ end
+
+ x.call_bar(obj).should == 'quux'
+ end
+
+ it "does not propagate refinements of new modules added after it is called" do
+ cls = Class.new {def foo; 'foo'; end}
+ cls2 = Class.new {def bar; 'bar'; end}
+ mod = MainSpecs::DATA[:mod] = Module.new do
+ refine(cls) do
+ def foo; 'quux'; end
+ end
+ end
+ x = MainSpecs::DATA[:x] = Object.new
+ eval <<-EOS, TOPLEVEL_BINDING
+ using MainSpecs::DATA[:mod]
+ x = MainSpecs::DATA[:x]
+ def x.call_foo(obj)
+ obj.foo
+ end
+ def x.call_bar(obj)
+ obj.bar
+ end
+ EOS
+
+ x.call_foo(cls.new).should == 'quux'
+
+ mod.module_eval do
+ refine(cls2) do
+ def bar; 'quux'; end
+ end
+ end
+
+ x.call_bar(cls2.new).should == 'bar'
+ end
+
+ it "raises error when called from method in wrapped script" do
+ -> do
+ load File.expand_path('../fixtures/using_in_method.rb', __FILE__), true
+ end.should.raise(RuntimeError)
+ end
+
+ it "raises error when called on toplevel from module" do
+ -> do
+ load File.expand_path('../fixtures/using_in_main.rb', __FILE__), true
+ end.should.raise(RuntimeError)
+ end
+
+ it "does not raise error when wrapped with module" do
+ -> do
+ load File.expand_path('../fixtures/using.rb', __FILE__), true
+ end.should_not.raise
+ end
+end
diff --git a/spec/ruby/core/marshal/dump_spec.rb b/spec/ruby/core/marshal/dump_spec.rb
new file mode 100644
index 0000000000..9bbb7809af
--- /dev/null
+++ b/spec/ruby/core/marshal/dump_spec.rb
@@ -0,0 +1,1082 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'fixtures/marshal_data'
+
+describe "Marshal.dump" do
+ it "dumps nil" do
+ Marshal.dump(nil).should == "\004\b0"
+ end
+
+ it "dumps true" do
+ Marshal.dump(true).should == "\004\bT"
+ end
+
+ it "dumps false" do
+ Marshal.dump(false).should == "\004\bF"
+ end
+
+ describe "with a Fixnum" do
+ it "dumps a Fixnum" do
+ [ [Marshal, 0, "\004\bi\000"],
+ [Marshal, 5, "\004\bi\n"],
+ [Marshal, 8, "\004\bi\r"],
+ [Marshal, 122, "\004\bi\177"],
+ [Marshal, 123, "\004\bi\001{"],
+ [Marshal, 1234, "\004\bi\002\322\004"],
+ [Marshal, -8, "\004\bi\363"],
+ [Marshal, -123, "\004\bi\200"],
+ [Marshal, -124, "\004\bi\377\204"],
+ [Marshal, -1234, "\004\bi\376.\373"],
+ [Marshal, -4516727, "\004\bi\375\211\024\273"],
+ [Marshal, 2**8, "\004\bi\002\000\001"],
+ [Marshal, 2**16, "\004\bi\003\000\000\001"],
+ [Marshal, 2**24, "\004\bi\004\000\000\000\001"],
+ [Marshal, -2**8, "\004\bi\377\000"],
+ [Marshal, -2**16, "\004\bi\376\000\000"],
+ [Marshal, -2**24, "\004\bi\375\000\000\000"],
+ ].should be_computed_by(:dump)
+ end
+
+ platform_is c_long_size: 64 do
+ it "dumps a positive Fixnum > 31 bits as a Bignum" do
+ Marshal.dump(2**31 + 1).should == "\x04\bl+\a\x01\x00\x00\x80"
+ end
+
+ it "dumps a negative Fixnum > 31 bits as a Bignum" do
+ Marshal.dump(-2**31 - 1).should == "\x04\bl-\a\x01\x00\x00\x80"
+ end
+ end
+
+ it "does not use object links for objects repeatedly dumped" do
+ Marshal.dump([0, 0]).should == "\x04\b[\ai\x00i\x00"
+ Marshal.dump([2**16, 2**16]).should == "\x04\b[\ai\x03\x00\x00\x01i\x03\x00\x00\x01"
+ end
+ end
+
+ describe "with a Symbol" do
+ it "dumps a Symbol" do
+ Marshal.dump(:symbol).should == "\004\b:\vsymbol"
+ end
+
+ it "dumps a big Symbol" do
+ Marshal.dump(('big' * 100).to_sym).should == "\004\b:\002,\001#{'big' * 100}"
+ end
+
+ it "dumps an encoded Symbol" do
+ s = "\u2192"
+ [ [Marshal, s.encode("utf-8").to_sym,
+ "\x04\bI:\b\xE2\x86\x92\x06:\x06ET"],
+ [Marshal, s.encode("utf-16").to_sym,
+ "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16"],
+ [Marshal, s.encode("utf-16le").to_sym,
+ "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE"],
+ [Marshal, s.encode("utf-16be").to_sym,
+ "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE"],
+ [Marshal, s.encode("euc-jp").to_sym,
+ "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP"],
+ [Marshal, s.encode("sjis").to_sym,
+ "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J"]
+ ].should be_computed_by(:dump)
+ end
+
+ it "dumps a binary encoded Symbol" do
+ s = "\u2192".dup.force_encoding("binary").to_sym
+ Marshal.dump(s).should == "\x04\b:\b\xE2\x86\x92"
+ end
+
+ it "dumps multiple Symbols sharing the same encoding" do
+ # Note that the encoding is a link for the second Symbol
+ symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET"
+ symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T"
+ value = [
+ "€a".dup.force_encoding(Encoding::UTF_8).to_sym,
+ "€b".dup.force_encoding(Encoding::UTF_8).to_sym
+ ]
+ Marshal.dump(value).should == "\x04\b[\a#{symbol1}#{symbol2}"
+
+ value = [*value, value[0]]
+ Marshal.dump(value).should == "\x04\b[\b#{symbol1}#{symbol2};\x00"
+ end
+
+ it "uses symbol links for objects repeatedly dumped" do
+ symbol = :foo
+ Marshal.dump([symbol, symbol]).should == "\x04\b[\a:\bfoo;\x00" # ;\x00 is a link to the symbol object
+ end
+ end
+
+ describe "with an object responding to #marshal_dump" do
+ it "dumps the object returned by #marshal_dump" do
+ Marshal.dump(UserMarshal.new).should == "\x04\bU:\x10UserMarshal:\tdata"
+ end
+
+ it "does not use Class#name" do
+ UserMarshal.should_not_receive(:name)
+ Marshal.dump(UserMarshal.new)
+ end
+
+ it "raises TypeError if an Object is an instance of an anonymous class" do
+ -> { Marshal.dump(Class.new(UserMarshal).new) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ obj = UserMarshal.new
+ Marshal.dump([obj, obj]).should == "\x04\b[\aU:\x10UserMarshal:\tdata@\x06" # @\x06 is a link to the object
+ end
+
+ it "adds instance variables of a dumped object after the object itself into the objects table" do
+ value = "<foo>"
+ obj = MarshalSpec::UserMarshalDumpWithIvar.new("string", value)
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\t, that means Integer 4)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bU:)MarshalSpec::UserMarshalDumpWithIvarI[\x06\"\vstring\x06:\t@foo\"\n<foo>@\x06@\t"
+ end
+ end
+
+ describe "with an object responding to #_dump" do
+ it "dumps the String returned by #_dump" do
+ Marshal.dump(UserDefined.new).should == "\004\bu:\020UserDefined\022\004\b[\a:\nstuff;\000"
+ end
+
+ it "dumps the String in non US-ASCII and non UTF-8 encoding" do
+ object = UserDefinedString.new("a".encode("windows-1251"))
+ Marshal.dump(object).should == "\x04\bIu:\x16UserDefinedString\x06a\x06:\rencoding\"\x11Windows-1251"
+ end
+
+ it "dumps the String in multibyte encoding" do
+ object = UserDefinedString.new("a".encode("utf-32le"))
+ Marshal.dump(object).should == "\x04\bIu:\x16UserDefinedString\ta\x00\x00\x00\x06:\rencoding\"\rUTF-32LE"
+ end
+
+ it "ignores overridden name method" do
+ obj = MarshalSpec::UserDefinedWithOverriddenName.new
+ Marshal.dump(obj).should == "\x04\bu:/MarshalSpec::UserDefinedWithOverriddenName\x12\x04\b[\a:\nstuff;\x00"
+ end
+
+ it "raises a TypeError if _dump returns a non-string" do
+ m = mock("marshaled")
+ m.should_receive(:_dump).and_return(0)
+ -> { Marshal.dump(m) }.should.raise(TypeError)
+ end
+
+ it "raises TypeError if an Object is an instance of an anonymous class" do
+ -> { Marshal.dump(Class.new(UserDefined).new) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+
+ it "favors marshal_dump over _dump" do
+ m = mock("marshaled")
+ m.should_receive(:marshal_dump).and_return(0)
+ m.should_not_receive(:_dump)
+ Marshal.dump(m)
+ end
+
+ it "indexes instance variables of a String returned by #_dump at first and then indexes the object itself" do
+ class MarshalSpec::M1::A
+ def _dump(level)
+ s = +"<dump>"
+ s.instance_variable_set(:@foo, "bar")
+ s
+ end
+ end
+
+ a = MarshalSpec::M1::A.new
+
+ # 0-based index of the object a = 2, that is encoded as \x07 and printed as "\a" character.
+ # Objects are serialized in the following order: Array, a, "bar".
+ # But they are indexed in different order: Array (index=0), "bar" (index=1), a (index=2)
+ # So the second occurenc of the object a is encoded as an index 2.
+ reference = "@\a"
+ Marshal.dump([a, a]).should == "\x04\b[\aIu:\x17MarshalSpec::M1::A\v<dump>\x06:\t@foo\"\bbar#{reference}"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ obj = UserDefined.new
+ Marshal.dump([obj, obj]).should == "\x04\b[\au:\x10UserDefined\x12\x04\b[\a:\nstuff;\x00@\x06" # @\x06 is a link to the object
+ end
+
+ it "adds instance variables of a dumped String before the object itself into the objects table" do
+ value = "<foo>"
+ obj = MarshalSpec::UserDefinedDumpWithIVars.new(+"string", value)
+
+ # expect a link to the object (@\a, that means Integer 2) is greater than a link
+ # to the instance variable value (@\x06, that means Integer 1)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:*MarshalSpec::UserDefinedDumpWithIVars\vstring\x06:\t@foo\"\n<foo>@\a@\x06"
+ end
+
+ describe "Core library classes with #_dump returning a String with instance variables" do
+ it "indexes instance variables and then a Time object itself" do
+ t = Time.utc(2022)
+ reference = "@\a"
+
+ Marshal.dump([t, t]).should == "\x04\b[\aIu:\tTime\r \x80\x1E\xC0\x00\x00\x00\x00\x06:\tzoneI\"\bUTC\x06:\x06EF#{reference}"
+ end
+ end
+ end
+
+ describe "with a Class" do
+ it "dumps a builtin Class" do
+ Marshal.dump(String).should == "\004\bc\vString"
+ end
+
+ it "dumps a user Class" do
+ Marshal.dump(UserDefined).should == "\x04\bc\x10UserDefined"
+ end
+
+ it "dumps a nested Class" do
+ Marshal.dump(UserDefined::Nested).should == "\004\bc\030UserDefined::Nested"
+ end
+
+ it "ignores overridden name method" do
+ Marshal.dump(MarshalSpec::ClassWithOverriddenName).should == "\x04\bc)MarshalSpec::ClassWithOverriddenName"
+ end
+
+ ruby_version_is "4.0" do
+ it "dumps a class with multibyte characters in name" do
+ source_object = eval("MarshalSpec::Multibyteãã‚ãƒã„Class".dup.force_encoding(Encoding::UTF_8))
+ Marshal.dump(source_object).should == "\x04\bIc,MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Class\x06:\x06ET"
+ Marshal.load(Marshal.dump(source_object)) == source_object
+ end
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ Marshal.dump([String, String]).should == "\x04\b[\ac\vString@\x06" # @\x06 is a link to the object
+ end
+
+ it "raises TypeError with an anonymous Class" do
+ -> { Marshal.dump(Class.new) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+
+ it "raises TypeError with a singleton Class" do
+ -> { Marshal.dump(class << self; self end) }.should.raise(TypeError)
+ end
+ end
+
+ describe "with a Module" do
+ it "dumps a builtin Module" do
+ Marshal.dump(Marshal).should == "\004\bm\fMarshal"
+ end
+
+ it "ignores overridden name method" do
+ Marshal.dump(MarshalSpec::ModuleWithOverriddenName).should == "\x04\bc*MarshalSpec::ModuleWithOverriddenName"
+ end
+
+ ruby_version_is "4.0" do
+ it "dumps a module with multibyte characters in name" do
+ source_object = eval("MarshalSpec::Multibyteã‘ã’ã“ã”Module".dup.force_encoding(Encoding::UTF_8))
+ Marshal.dump(source_object).should == "\x04\bIm-MarshalSpec::Multibyte\xE3\x81\x91\xE3\x81\x92\xE3\x81\x93\xE3\x81\x94Module\x06:\x06ET"
+ Marshal.load(Marshal.dump(source_object)) == source_object
+ end
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ Marshal.dump([Marshal, Marshal]).should == "\x04\b[\am\fMarshal@\x06" # @\x06 is a link to the object
+ end
+
+ it "raises TypeError with an anonymous Module" do
+ -> { Marshal.dump(Module.new) }.should.raise(TypeError, /can't dump anonymous module/)
+ end
+ end
+
+ describe "with a Float" do
+ it "dumps a Float" do
+ [ [Marshal, 0.0, "\004\bf\0060"],
+ [Marshal, -0.0, "\004\bf\a-0"],
+ [Marshal, 1.0, "\004\bf\0061"],
+ [Marshal, 123.4567, "\004\bf\r123.4567"],
+ [Marshal, -0.841, "\x04\bf\v-0.841"],
+ [Marshal, -9876.345, "\x04\bf\x0E-9876.345"],
+ [Marshal, infinity_value, "\004\bf\binf"],
+ [Marshal, -infinity_value, "\004\bf\t-inf"],
+ [Marshal, nan_value, "\004\bf\bnan"],
+ ].should be_computed_by(:dump)
+ end
+
+ it "may or may not use object links for objects repeatedly dumped" do
+ # it's an MRI implementation detail - on x86 architecture object links
+ # aren't used for Float values but on amd64 - object links are used
+
+ dump = Marshal.dump([0.0, 0.0])
+ ["\x04\b[\af\x060@\x06", "\x04\b[\af\x060f\x060"].should.include?(dump)
+
+ # if object links aren't used - entries in the objects table are still
+ # occupied by Float values
+ if dump == "\x04\b[\af\x060f\x060"
+ s = "string"
+ # an index of "string" ("@\b") in the object table equals 3 (`"\b".ord - 5`),
+ # so `0.0, 0,0` elements occupied indices 1 and 2
+ Marshal.dump([0.0, 0.0, s, s]).should == "\x04\b[\tf\x060f\x060\"\vstring@\b"
+ end
+ end
+ end
+
+ describe "with a Bignum" do
+ it "dumps a Bignum" do
+ [ [Marshal, -4611686018427387903, "\004\bl-\t\377\377\377\377\377\377\377?"],
+ [Marshal, -2361183241434822606847, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000"],
+ ].should be_computed_by(:dump)
+ end
+
+ it "dumps a Bignum" do
+ [ [Marshal, 2**64, "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"],
+ [Marshal, 2**90, "\004\bl+\v#{"\000" * 11}\004"],
+ [Marshal, -2**63, "\004\bl-\t\000\000\000\000\000\000\000\200"],
+ [Marshal, -2**64, "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"],
+ ].should be_computed_by(:dump)
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ n = 2**64
+ Marshal.dump([n, n]).should == "\x04\b[\al+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00@\x06" # @\x06 is a link to the object
+ end
+
+ it "increases the object links counter" do
+ obj = Object.new
+ object_1_link = "\x06" # representing of (0-based) index=1 (by adding 5 for small Integers)
+ object_2_link = "\x07" # representing of index=2
+
+ # objects: Array, Object, Object
+ Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@#{object_1_link}"
+
+ # objects: Array, Bignum, Object, Object
+ Marshal.dump([2**64, obj, obj]).should == "\x04\b[\bl+\n\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}"
+ Marshal.dump([2**48, obj, obj]).should == "\x04\b[\bl+\t\x00\x00\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}"
+ Marshal.dump([2**32, obj, obj]).should == "\x04\b[\bl+\b\x00\x00\x00\x00\x01\x00o:\vObject\x00@#{object_2_link}"
+ end
+ end
+
+ describe "with a Rational" do
+ it "dumps a Rational" do
+ Marshal.dump(Rational(2, 3)).should == "\x04\bU:\rRational[\ai\ai\b"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ r = Rational(2, 3)
+ Marshal.dump([r, r]).should == "\x04\b[\aU:\rRational[\ai\ai\b@\x06" # @\x06 is a link to the object
+ end
+ end
+
+ describe "with a Complex" do
+ it "dumps a Complex" do
+ Marshal.dump(Complex(2, 3)).should == "\x04\bU:\fComplex[\ai\ai\b"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ c = Complex(2, 3)
+ Marshal.dump([c, c]).should == "\x04\b[\aU:\fComplex[\ai\ai\b@\x06" # @\x06 is a link to the object
+ end
+ end
+
+ describe "with a Data" do
+ it "dumps a Data" do
+ Marshal.dump(MarshalSpec::DataSpec::Measure.new(100, 'km')).should == "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
+ end
+
+ it "dumps an extended Data" do
+ obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km")
+ Marshal.dump(obj).should == "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm"
+ end
+
+ it "ignores overridden name method" do
+ obj = MarshalSpec::DataSpec::MeasureWithOverriddenName.new(100, "km")
+ Marshal.dump(obj).should == "\x04\bS:5MarshalSpec::DataSpec::MeasureWithOverriddenName\a:\vamountii:\tunit\"\akm"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ d = MarshalSpec::DataSpec::Measure.new(100, 'km')
+ Marshal.dump([d, d]).should == "\x04\b[\aS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm@\x06" # @\x06 is a link to the object
+ end
+
+ it "raises TypeError with an anonymous Struct" do
+ -> { Marshal.dump(Data.define(:a).new(1)) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+ end
+
+ describe "with a String" do
+ it "dumps a blank String" do
+ Marshal.dump("".dup.force_encoding("binary")).should == "\004\b\"\000"
+ end
+
+ it "dumps a short String" do
+ Marshal.dump("short".dup.force_encoding("binary")).should == "\004\b\"\012short"
+ end
+
+ it "dumps a long String" do
+ Marshal.dump(("big" * 100).force_encoding("binary")).should == "\004\b\"\002,\001#{"big" * 100}"
+ end
+
+ it "dumps a String extended with a Module" do
+ Marshal.dump("".dup.extend(Meths).force_encoding("binary")).should == "\004\be:\nMeths\"\000"
+ end
+
+ it "dumps a String subclass" do
+ Marshal.dump(UserString.new.force_encoding("binary")).should == "\004\bC:\017UserString\"\000"
+ end
+
+ it "dumps a String subclass extended with a Module" do
+ Marshal.dump(UserString.new.extend(Meths).force_encoding("binary")).should == "\004\be:\nMethsC:\017UserString\"\000"
+ end
+
+ it "ignores overridden name method when dumps a String subclass" do
+ obj = MarshalSpec::StringWithOverriddenName.new
+ Marshal.dump(obj).should == "\x04\bC:*MarshalSpec::StringWithOverriddenName\"\x00"
+ end
+
+ it "dumps a String with instance variables" do
+ str = +""
+ str.instance_variable_set("@foo", "bar")
+ Marshal.dump(str.force_encoding("binary")).should == "\x04\bI\"\x00\x06:\t@foo\"\bbar"
+ end
+
+ it "dumps a US-ASCII String" do
+ str = "abc".dup.force_encoding("us-ascii")
+ Marshal.dump(str).should == "\x04\bI\"\babc\x06:\x06EF"
+ end
+
+ it "dumps a UTF-8 String" do
+ str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8")
+ Marshal.dump(str).should == "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET"
+ end
+
+ it "dumps a String in another encoding" do
+ str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le")
+ result = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE"
+ Marshal.dump(str).should == result
+ end
+
+ it "dumps multiple strings using symlinks for the :E (encoding) symbol" do
+ Marshal.dump(["".encode("us-ascii"), "".encode("utf-8")]).should == "\x04\b[\aI\"\x00\x06:\x06EFI\"\x00\x06;\x00T"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ s = "string"
+ Marshal.dump([s, s]).should == "\x04\b[\a\"\vstring@\x06" # @\x06 is a link to the object
+ end
+
+ it "adds instance variables after the object itself into the objects table" do
+ obj = +"string"
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\a, that means Integer 2)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bI\"\vstring\x06:\t@foo\"\n<foo>@\x06@\a"
+ end
+ end
+
+ describe "with a Regexp" do
+ it "dumps a Regexp" do
+ Marshal.dump(/\A.\Z/).should == "\x04\bI/\n\\A.\\Z\x00\x06:\x06EF"
+ end
+
+ it "dumps a Regexp with flags" do
+ Marshal.dump(//im).should == "\x04\bI/\x00\x05\x06:\x06EF"
+ end
+
+ it "dumps a Regexp subclass with instance variables" do
+ o = UserRegexp.new("")
+ o.instance_variable_set(:@ivar, :ivar)
+ Marshal.dump(o).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\a:\x06EF:\n@ivar:\tivar"
+ end
+
+ it "dumps an extended Regexp subclass" do
+ Marshal.dump(UserRegexp.new("").extend(Meths)).should == "\x04\bIe:\nMethsC:\x0FUserRegexp/\x00\x00\x06:\x06EF"
+ end
+
+ ruby_version_is ""..."4.1" do
+ it "dumps a Regexp with instance variables" do
+ o = Regexp.new("")
+ o.instance_variable_set(:@ivar, :ivar)
+ Marshal.dump(o).should == "\x04\bI/\x00\x00\a:\x06EF:\n@ivar:\tivar"
+ end
+
+ it "dumps an extended Regexp" do
+ Marshal.dump(Regexp.new("").extend(Meths)).should == "\x04\bIe:\nMeths/\x00\x00\x06:\x06EF"
+ end
+ end
+
+ it "dumps a Regexp subclass" do
+ Marshal.dump(UserRegexp.new("")).should == "\x04\bIC:\x0FUserRegexp/\x00\x00\x06:\x06EF"
+ end
+
+ it "dumps a binary Regexp" do
+ o = Regexp.new("".dup.force_encoding("binary"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\b/\x00\x10"
+ end
+
+ it "dumps an ascii-compatible Regexp" do
+ o = Regexp.new("a".encode("us-ascii"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\x06EF"
+
+ o = Regexp.new("a".encode("us-ascii"))
+ Marshal.dump(o).should == "\x04\bI/\x06a\x00\x06:\x06EF"
+
+ o = Regexp.new("a".encode("windows-1251"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\rencoding\"\x11Windows-1251"
+
+ o = Regexp.new("a".encode("windows-1251"))
+ Marshal.dump(o).should == "\x04\bI/\x06a\x00\x06:\x06EF"
+ end
+
+ it "dumps a UTF-8 Regexp" do
+ o = Regexp.new("".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\x00\x10\x06:\x06ET"
+
+ o = Regexp.new("a".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\x06a\x10\x06:\x06ET"
+
+ o = Regexp.new("\u3042".dup.force_encoding("utf-8"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\b\xE3\x81\x82\x10\x06:\x06ET"
+ end
+
+ it "dumps a Regexp in another encoding" do
+ o = Regexp.new("".dup.force_encoding("utf-16le"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\x00\x10\x06:\rencoding\"\rUTF-16LE"
+
+ o = Regexp.new("a".encode("utf-16le"), Regexp::FIXEDENCODING)
+ Marshal.dump(o).should == "\x04\bI/\aa\x00\x10\x06:\rencoding\"\rUTF-16LE"
+ end
+
+ it "ignores overridden name method when dumps a Regexp subclass" do
+ obj = MarshalSpec::RegexpWithOverriddenName.new("")
+ Marshal.dump(obj).should == "\x04\bIC:*MarshalSpec::RegexpWithOverriddenName/\x00\x00\x06:\x06EF"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ r = /\A.\Z/
+ Marshal.dump([r, r]).should == "\x04\b[\aI/\n\\A.\\Z\x00\x06:\x06EF@\x06" # @\x06 is a link to the object
+ end
+ end
+
+ describe "with an Array" do
+ it "dumps an empty Array" do
+ Marshal.dump([]).should == "\004\b[\000"
+ end
+
+ it "dumps a non-empty Array" do
+ Marshal.dump([:a, 1, 2]).should == "\004\b[\b:\006ai\006i\a"
+ end
+
+ it "dumps an Array subclass" do
+ Marshal.dump(UserArray.new).should == "\004\bC:\016UserArray[\000"
+ end
+
+ it "dumps a recursive Array" do
+ a = []
+ a << a
+ Marshal.dump(a).should == "\x04\b[\x06@\x00"
+ end
+
+ it "dumps an Array with instance variables" do
+ a = []
+ a.instance_variable_set(:@ivar, 1)
+ Marshal.dump(a).should == "\004\bI[\000\006:\n@ivari\006"
+ end
+
+ it "dumps an extended Array" do
+ Marshal.dump([].extend(Meths)).should == "\004\be:\nMeths[\000"
+ end
+
+ it "ignores overridden name method when dumps an Array subclass" do
+ obj = MarshalSpec::ArrayWithOverriddenName.new
+ Marshal.dump(obj).should == "\x04\bC:)MarshalSpec::ArrayWithOverriddenName[\x00"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ a = [1]
+ Marshal.dump([a, a]).should == "\x04\b[\a[\x06i\x06@\x06" # @\x06 is a link to the object
+ end
+
+ it "adds instance variables after the object itself into the objects table" do
+ obj = []
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\a, that means Integer 2)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bI[\x00\x06:\t@foo\"\n<foo>@\x06@\a"
+ end
+ end
+
+ describe "with a Hash" do
+ it "dumps a Hash" do
+ Marshal.dump({}).should == "\004\b{\000"
+ end
+
+ it "dumps a non-empty Hash" do
+ Marshal.dump({a: 1}).should == "\x04\b{\x06:\x06ai\x06"
+ end
+
+ it "dumps a Hash subclass" do
+ Marshal.dump(UserHash.new).should == "\004\bC:\rUserHash{\000"
+ end
+
+ it "dumps a Hash with a default value" do
+ Marshal.dump(Hash.new(1)).should == "\004\b}\000i\006"
+ end
+
+ it "dumps a Hash with compare_by_identity" do
+ h = {}
+ h.compare_by_identity
+
+ Marshal.dump(h).should == "\004\bC:\tHash{\x00"
+ end
+
+ it "dumps a Hash subclass with compare_by_identity" do
+ h = UserHash.new
+ h.compare_by_identity
+
+ Marshal.dump(h).should == "\x04\bC:\rUserHashC:\tHash{\x00"
+ end
+
+ it "raises a TypeError with hash having default proc" do
+ -> { Marshal.dump(Hash.new {}) }.should.raise(TypeError, "can't dump hash with default proc")
+ end
+
+ it "dumps a Hash with instance variables" do
+ a = {}
+ a.instance_variable_set(:@ivar, 1)
+ Marshal.dump(a).should == "\004\bI{\000\006:\n@ivari\006"
+ end
+
+ it "dumps an extended Hash" do
+ Marshal.dump({}.extend(Meths)).should == "\004\be:\nMeths{\000"
+ end
+
+ it "dumps an Hash subclass with a parameter to initialize" do
+ Marshal.dump(UserHashInitParams.new(1)).should == "\004\bIC:\027UserHashInitParams{\000\006:\a@ai\006"
+ end
+
+ it "ignores overridden name method when dumps a Hash subclass" do
+ obj = MarshalSpec::HashWithOverriddenName.new
+ Marshal.dump(obj).should == "\x04\bC:(MarshalSpec::HashWithOverriddenName{\x00"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ h = {a: 1}
+ Marshal.dump([h, h]).should == "\x04\b[\a{\x06:\x06ai\x06@\x06" # @\x06 is a link to the object
+ end
+
+ it "adds instance variables after the object itself into the objects table" do
+ obj = {}
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\a, that means Integer 2)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bI{\x00\x06:\t@foo\"\n<foo>@\x06@\a"
+ end
+ end
+
+ describe "with a Struct" do
+ it "dumps a Struct" do
+ Marshal.dump(Struct::Pyramid.new).should == "\004\bS:\024Struct::Pyramid\000"
+ end
+
+ it "dumps a Struct" do
+ Marshal.dump(Struct::Useful.new(1, 2)).should == "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"
+ end
+
+ it "dumps a Struct with instance variables" do
+ st = Struct.new("Thick").new
+ st.instance_variable_set(:@ivar, 1)
+ Marshal.dump(st).should == "\004\bIS:\022Struct::Thick\000\006:\n@ivari\006"
+ Struct.send(:remove_const, :Thick)
+ end
+
+ it "dumps an extended Struct" do
+ obj = Struct.new("Extended", :a, :b).new.extend(Meths)
+ Marshal.dump(obj).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0"
+
+ s = 'hi'
+ obj.a = [:a, s]
+ obj.b = [:Meths, s]
+ Marshal.dump(obj).should == "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a"
+ Struct.send(:remove_const, :Extended)
+ end
+
+ it "ignores overridden name method" do
+ obj = MarshalSpec::StructWithOverriddenName.new("member")
+ Marshal.dump(obj).should == "\x04\bS:*MarshalSpec::StructWithOverriddenName\x06:\x06a\"\vmember"
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ s = Struct::Pyramid.new
+ Marshal.dump([s, s]).should == "\x04\b[\aS:\x14Struct::Pyramid\x00@\x06" # @\x06 is a link to the object
+ end
+
+ it "raises TypeError with an anonymous Struct" do
+ -> { Marshal.dump(Struct.new(:a).new(1)) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+
+ it "adds instance variables after the object itself into the objects table" do
+ obj = Struct::Pyramid.new
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\a, that means Integer 2)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bIS:\x14Struct::Pyramid\x00\x06:\t@foo\"\n<foo>@\x06@\a"
+ end
+ end
+
+ describe "with an Object" do
+ it "dumps an Object" do
+ Marshal.dump(Object.new).should == "\004\bo:\x0BObject\x00"
+ end
+
+ it "dumps an extended Object" do
+ Marshal.dump(Object.new.extend(Meths)).should == "\004\be:\x0AMethso:\x0BObject\x00"
+ end
+
+ it "dumps an Object with an instance variable" do
+ obj = Object.new
+ obj.instance_variable_set(:@ivar, 1)
+ Marshal.dump(obj).should == "\004\bo:\vObject\006:\n@ivari\006"
+ end
+
+ it "dumps an Object with a non-US-ASCII instance variable" do
+ obj = Object.new
+ ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym
+ obj.instance_variable_set(ivar, 1)
+ Marshal.dump(obj).should == "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06"
+ end
+
+ it "dumps an Object that has had an instance variable added and removed as though it was never set" do
+ obj = Object.new
+ obj.instance_variable_set(:@ivar, 1)
+ obj.send(:remove_instance_variable, :@ivar)
+ Marshal.dump(obj).should == "\004\bo:\x0BObject\x00"
+ end
+
+ it "dumps an Object if it has a singleton class but no singleton methods and no singleton instance variables" do
+ obj = Object.new
+ obj.singleton_class
+ Marshal.dump(obj).should == "\004\bo:\x0BObject\x00"
+ end
+
+ it "ignores overridden name method" do
+ obj = MarshalSpec::ClassWithOverriddenName.new
+ Marshal.dump(obj).should == "\x04\bo:)MarshalSpec::ClassWithOverriddenName\x00"
+ end
+
+ it "raises TypeError if an Object has a singleton class and singleton methods" do
+ obj = Object.new
+ def obj.foo; end
+ -> {
+ Marshal.dump(obj)
+ }.should.raise(TypeError, "singleton can't be dumped")
+ end
+
+ it "raises TypeError if an Object has a singleton class and singleton instance variables" do
+ obj = Object.new
+ class << obj
+ @v = 1
+ end
+
+ -> {
+ Marshal.dump(obj)
+ }.should.raise(TypeError, "singleton can't be dumped")
+ end
+
+ it "raises TypeError if an Object is an instance of an anonymous class" do
+ anonymous_class = Class.new
+ obj = anonymous_class.new
+
+ -> { Marshal.dump(obj) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+
+ it "raises TypeError if an Object extends an anonymous module" do
+ anonymous_module = Module.new
+ obj = Object.new
+ obj.extend(anonymous_module)
+
+ -> { Marshal.dump(obj) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+
+ it "dumps a BasicObject subclass if it defines respond_to?" do
+ obj = MarshalSpec::BasicObjectSubWithRespondToFalse.new
+ Marshal.dump(obj).should == "\x04\bo:2MarshalSpec::BasicObjectSubWithRespondToFalse\x00"
+ end
+
+ it "dumps without marshaling any attached finalizer" do
+ obj = Object.new
+ finalizer = Object.new
+ def finalizer.noop(_)
+ end
+ ObjectSpace.define_finalizer(obj, finalizer.method(:noop))
+ Marshal.load(Marshal.dump(obj)).class.should == Object
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ obj = Object.new
+ Marshal.dump([obj, obj]).should == "\x04\b[\ao:\vObject\x00@\x06" # @\x06 is a link to the object
+ end
+
+ it "adds instance variables after the object itself into the objects table" do
+ obj = Object.new
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\a, that means Integer 2)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\vObject\x06:\t@foo\"\n<foo>@\x06@\a"
+ end
+ end
+
+ describe "with a Range" do
+ it "dumps a Range inclusive of end" do
+ dump = Marshal.dump(1..2)
+ dump.should == "\x04\bo:\nRange\b:\texclF:\nbegini\x06:\bendi\a"
+
+ load = Marshal.load(dump)
+ load.should == (1..2)
+ end
+
+ it "dumps a Range exclusive of end" do
+ dump = Marshal.dump(1...2)
+ dump.should == "\x04\bo:\nRange\b:\texclT:\nbegini\x06:\bendi\a"
+
+ load = Marshal.load(dump)
+ load.should == (1...2)
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ r = 1..2
+ Marshal.dump([r, r]).should == "\x04\b[\ao:\nRange\b:\texclF:\nbegini\x06:\bendi\a@\x06" # @\x06 is a link to the object
+ end
+
+ it "raises TypeError with an anonymous Range subclass" do
+ -> { Marshal.dump(Class.new(Range).new(1, 2)) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+ end
+
+ describe "with a Time" do
+ before :each do
+ @internal = Encoding.default_internal
+ Encoding.default_internal = Encoding::UTF_8
+
+ @utc = Time.utc(2012, 1, 1)
+ @utc_dump = @utc.send(:_dump)
+
+ with_timezone 'AST', 3 do
+ @t = Time.local(2012, 1, 1)
+ @fract = Time.local(2012, 1, 1, 1, 59, 56.2)
+ @t_dump = @t.send(:_dump)
+ @fract_dump = @fract.send(:_dump)
+ end
+ end
+
+ after :each do
+ Encoding.default_internal = @internal
+ end
+
+ it "dumps the zone and the offset" do
+ with_timezone 'AST', 3 do
+ dump = Marshal.dump(@t)
+ base = "\x04\bIu:\tTime\r#{@t_dump}\a"
+ offset = ":\voffseti\x020*"
+ zone = ":\tzoneI\"\bAST\x06:\x06EF" # Last is 'F' (US-ASCII)
+ [ "#{base}#{offset}#{zone}", "#{base}#{zone}#{offset}" ].should.include?(dump)
+ end
+ end
+
+ it "dumps the zone, but not the offset if zone is UTC" do
+ dump = Marshal.dump(@utc)
+ zone = ":\tzoneI\"\bUTC\x06:\x06EF" # Last is 'F' (US-ASCII)
+ dump.should == "\x04\bIu:\tTime\r#{@utc_dump}\x06#{zone}"
+ end
+
+ it "ignores overridden name method" do
+ obj = MarshalSpec::TimeWithOverriddenName.new
+ Marshal.dump(obj).should.include?("MarshalSpec::TimeWithOverriddenName")
+ end
+
+ ruby_version_is "4.0" do
+ it "dumps a Time subclass with multibyte characters in name" do
+ source_object = eval("MarshalSpec::Multibyteãã‚ãƒã„Time".dup.force_encoding(Encoding::UTF_8))
+ Marshal.dump(source_object).should == "\x04\bIc+MarshalSpec::Multibyte\xE3\x81\x81\xE3\x81\x82\xE3\x81\x83\xE3\x81\x84Time\x06:\x06ET"
+ Marshal.load(Marshal.dump(source_object)) == source_object
+ end
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ # order of the offset and zone instance variables is a subject to change
+ # and may be different on different CRuby versions
+ base = Regexp.quote("\x04\b[\aIu:\tTime\r\xF5\xEF\e\x80\x00\x00\x00\x00\a")
+ offset = Regexp.quote(":\voffseti\x020*:\tzoneI\"\bAST\x06:\x06EF")
+ zone = Regexp.quote(":\tzoneI\"\bAST\x06:\x06EF:\voffseti\x020*")
+ instance_variables = /#{offset}|#{zone}/
+ Marshal.dump([@t, @t]).should =~ /\A#{base}#{instance_variables}@\a\Z/ # @\a is a link to the object
+ end
+
+ it "adds instance variables before the object itself into the objects table" do
+ obj = @utc
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\b, that means Integer 3) is greater than a link
+ # to the instance variable value (@\x06, that means Integer 1)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bIu:\tTime\r \x00\x1C\xC0\x00\x00\x00\x00\a:\t@foo\"\n<foo>:\tzoneI\"\bUTC\x06:\x06EF@\b@\x06"
+ end
+
+ it "raises TypeError with an anonymous Time subclass" do
+ -> { Marshal.dump(Class.new(Time).now) }.should.raise(TypeError)
+ end
+ end
+
+ describe "with an Exception" do
+ it "dumps an empty Exception" do
+ Marshal.dump(Exception.new).should == "\x04\bo:\x0EException\a:\tmesg0:\abt0"
+ end
+
+ it "dumps the message for the exception" do
+ Marshal.dump(Exception.new("foo")).should == "\x04\bo:\x0EException\a:\tmesg\"\bfoo:\abt0"
+ end
+
+ it "contains the filename in the backtrace" do
+ obj = Exception.new("foo")
+ obj.set_backtrace(["foo/bar.rb:10"])
+ Marshal.dump(obj).should == "\x04\bo:\x0EException\a:\tmesg\"\bfoo:\abt[\x06\"\x12foo/bar.rb:10"
+ end
+
+ it "dumps instance variables if they exist" do
+ obj = Exception.new("foo")
+ obj.instance_variable_set(:@ivar, 1)
+ Marshal.dump(obj).should == "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\n@ivari\x06"
+ end
+
+ it "dumps the cause for the exception" do
+ exc = nil
+ begin
+ raise StandardError, "the cause"
+ rescue StandardError => cause
+ begin
+ raise RuntimeError, "the consequence"
+ rescue RuntimeError => e
+ e.cause.should.equal?(cause)
+ exc = e
+ end
+ end
+
+ reloaded = Marshal.load(Marshal.dump(exc))
+ reloaded.cause.should.instance_of?(StandardError)
+ reloaded.cause.message.should == "the cause"
+ end
+
+ # NoMethodError uses an exception formatter on TruffleRuby and computes a message lazily
+ it "dumps the message for the raised NoMethodError exception" do
+ begin
+ "".foo
+ rescue => e
+ end
+
+ Marshal.dump(e).should =~ /undefined method [`']foo' for ("":String|an instance of String)/
+ end
+
+ it "uses object links for objects repeatedly dumped" do
+ e = Exception.new
+ Marshal.dump([e, e]).should == "\x04\b[\ao:\x0EException\a:\tmesg0:\abt0@\x06" # @\x\a is a link to the object
+ end
+
+ it "adds instance variables after the object itself into the objects table" do
+ obj = Exception.new
+ value = "<foo>"
+ obj.instance_variable_set :@foo, value
+
+ # expect a link to the object (@\x06, that means Integer 1) is smaller than a link
+ # to the instance variable value (@\a, that means Integer 2)
+ Marshal.dump([obj, obj, value]).should == "\x04\b[\bo:\x0EException\b:\tmesg0:\abt0:\t@foo\"\n<foo>@\x06@\a"
+ end
+
+ it "raises TypeError if an Object is an instance of an anonymous class" do
+ anonymous_class = Class.new(Exception)
+ obj = anonymous_class.new
+
+ -> { Marshal.dump(obj) }.should.raise(TypeError, /can't dump anonymous class/)
+ end
+ end
+
+ it "dumps subsequent appearances of a symbol as a link" do
+ Marshal.dump([:a, :a]).should == "\004\b[\a:\006a;\000"
+ end
+
+ it "dumps subsequent appearances of an object as a link" do
+ o = Object.new
+ Marshal.dump([o, o]).should == "\004\b[\ao:\vObject\000@\006"
+ end
+
+ MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)|
+ it "#{description} returns a binary string" do
+ Marshal.dump(object).encoding.should == Encoding::BINARY
+ end
+ end
+
+ it "raises an ArgumentError when the recursion limit is exceeded" do
+ h = {'one' => {'two' => {'three' => 0}}}
+ -> { Marshal.dump(h, 3) }.should.raise(ArgumentError)
+ -> { Marshal.dump([h], 4) }.should.raise(ArgumentError)
+ -> { Marshal.dump([], 0) }.should.raise(ArgumentError)
+ -> { Marshal.dump([[[]]], 1) }.should.raise(ArgumentError)
+ end
+
+ it "ignores the recursion limit if the limit is negative" do
+ Marshal.dump([], -1).should == "\004\b[\000"
+ Marshal.dump([[]], -1).should == "\004\b[\006[\000"
+ Marshal.dump([[[]]], -1).should == "\004\b[\006[\006[\000"
+ end
+
+ describe "when passed an IO" do
+ it "writes the serialized data to the IO-Object" do
+ (obj = mock('test')).should_receive(:write).at_least(1)
+ Marshal.dump("test", obj)
+ end
+
+ it "returns the IO-Object" do
+ (obj = mock('test')).should_receive(:write).at_least(1)
+ Marshal.dump("test", obj).should == obj
+ end
+
+ it "raises an Error when the IO-Object does not respond to #write" do
+ obj = mock('test')
+ -> { Marshal.dump("test", obj) }.should.raise(TypeError)
+ end
+
+
+ it "calls binmode when it's defined" do
+ obj = mock('test')
+ obj.should_receive(:write).at_least(1)
+ obj.should_receive(:binmode).at_least(1)
+ Marshal.dump("test", obj)
+ end
+ end
+
+ describe "when passed a StringIO" do
+ it "should raise an error" do
+ require "stringio"
+ -> { Marshal.dump(StringIO.new) }.should.raise(TypeError)
+ end
+ end
+
+ it "raises a TypeError if marshalling a Method instance" do
+ -> { Marshal.dump(Marshal.method(:dump)) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if marshalling a Proc" do
+ -> { Marshal.dump(proc {}) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if dumping a IO/File instance" do
+ -> { Marshal.dump(STDIN) }.should.raise(TypeError)
+ -> { File.open(__FILE__) { |f| Marshal.dump(f) } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if dumping a MatchData instance" do
+ -> { Marshal.dump(/(.)/.match("foo")) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if dumping a Mutex instance" do
+ m = Mutex.new
+ -> { Marshal.dump(m) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/marshal/fixtures/classes.rb b/spec/ruby/core/marshal/fixtures/classes.rb
new file mode 100644
index 0000000000..7c81c64927
--- /dev/null
+++ b/spec/ruby/core/marshal/fixtures/classes.rb
@@ -0,0 +1,4 @@
+module MarshalSpec
+ # empty modules
+ module M1 end
+end
diff --git a/spec/ruby/core/marshal/fixtures/marshal_data.rb b/spec/ruby/core/marshal/fixtures/marshal_data.rb
new file mode 100644
index 0000000000..c16d9e4bb6
--- /dev/null
+++ b/spec/ruby/core/marshal/fixtures/marshal_data.rb
@@ -0,0 +1,567 @@
+# encoding: binary
+
+require_relative 'marshal_multibyte_data'
+
+class UserDefined
+ class Nested
+ def ==(other)
+ other.kind_of? self.class
+ end
+ end
+
+ attr_reader :a, :b
+
+ def initialize
+ @a = 'stuff'
+ @b = @a
+ end
+
+ def _dump(depth)
+ Marshal.dump [:stuff, :stuff]
+ end
+
+ def self._load(data)
+ a, b = Marshal.load data
+
+ obj = allocate
+ obj.instance_variable_set :@a, a
+ obj.instance_variable_set :@b, b
+
+ obj
+ end
+
+ def ==(other)
+ self.class === other and
+ @a == other.a and
+ @b == other.b
+ end
+end
+
+class UserDefinedWithIvar
+ attr_reader :a, :b, :c
+
+ def initialize
+ @a = +'stuff'
+ @a.instance_variable_set :@foo, :UserDefinedWithIvar
+ @b = 'more'
+ @c = @b
+ end
+
+ def _dump(depth)
+ Marshal.dump [:stuff, :more, :more]
+ end
+
+ def self._load(data)
+ a, b, c = Marshal.load data
+
+ obj = allocate
+ obj.instance_variable_set :@a, a
+ obj.instance_variable_set :@b, b
+ obj.instance_variable_set :@c, c
+
+ obj
+ end
+
+ def ==(other)
+ self.class === other and
+ @a == other.a and
+ @b == other.b and
+ @c == other.c and
+ @a.instance_variable_get(:@foo) == other.a.instance_variable_get(:@foo)
+ end
+end
+
+class UserDefinedImmediate
+ def _dump(depth)
+ ''
+ end
+
+ def self._load(data)
+ nil
+ end
+end
+
+class UserDefinedString
+ attr_reader :string
+
+ def initialize(string)
+ @string = string
+ end
+
+ def _dump(depth)
+ @string
+ end
+
+ def self._load(data)
+ new(data)
+ end
+end
+
+module MarshalSpec
+ class UserDefinedDumpWithIVars
+ attr_reader :string
+
+ def initialize(string, ivar_value)
+ @string = string
+ @string.instance_variable_set(:@foo, ivar_value)
+ end
+
+ def _dump(depth)
+ @string
+ end
+
+ def self._load(data)
+ new(data)
+ end
+ end
+end
+
+class UserPreviouslyDefinedWithInitializedIvar
+ attr_accessor :field1, :field2
+end
+
+class UserMarshal
+ attr_accessor :data
+
+ def initialize
+ @data = 'stuff'
+ end
+ def marshal_dump() :data end
+ def marshal_load(data) @data = data end
+ def ==(other) self.class === other and @data == other.data end
+end
+
+class UserMarshalWithClassName < UserMarshal
+ def self.name
+ "Never::A::Real::Class"
+ end
+end
+
+class UserMarshalWithIvar
+ attr_reader :data
+
+ def initialize
+ @data = 'my data'
+ end
+
+ def marshal_dump
+ [:data]
+ end
+
+ def marshal_load(o)
+ @data = o.first
+ end
+
+ def ==(other)
+ self.class === other and
+ @data = other.data
+ end
+end
+
+module MarshalSpec
+ class UserMarshalDumpWithIvar
+ attr_reader :data
+
+ def initialize(data, ivar_value)
+ @data = data
+ @ivar_value = ivar_value
+ end
+
+ def marshal_dump
+ obj = [data]
+ obj.instance_variable_set(:@foo, @ivar_value)
+ obj
+ end
+
+ def marshal_load(o)
+ @data = o[0]
+ end
+
+ def ==(other)
+ self.class === other and
+ @data = other.data
+ end
+ end
+end
+
+class UserArray < Array
+end
+
+class UserHash < Hash
+end
+
+class UserHashInitParams < Hash
+ def initialize(a)
+ @a = a
+ end
+end
+
+class UserObject
+end
+
+class UserRegexp < Regexp
+end
+
+class UserString < String
+end
+
+class UserCustomConstructorString < String
+ def initialize(arg1, arg2)
+ end
+end
+
+module Meths
+ def meths_method() end
+end
+
+module MethsMore
+ def meths_more_method() end
+end
+
+Struct.new "Pyramid"
+Struct.new "Useful", :a, :b
+
+module MarshalSpec
+ class StructWithUserInitialize < Struct.new(:a)
+ THREADLOCAL_KEY = :marshal_load_struct_args
+ def initialize(*args)
+ # using thread-local to avoid ivar marshaling
+ Thread.current[THREADLOCAL_KEY] = args
+ super(*args)
+ end
+ end
+
+ StructToDump = Struct.new(:a, :b)
+
+ class BasicObjectSubWithRespondToFalse < BasicObject
+ def respond_to?(method_name, include_all=false)
+ false
+ end
+ end
+
+ module ModuleToExtendBy
+ end
+
+ def self.random_data
+ randomizer = Random.new(42)
+ 1000.times{randomizer.rand} # Make sure we exhaust his first state of 624 random words
+ dump_data = File.binread(fixture(__FILE__, 'random.dump'))
+ [randomizer, dump_data]
+ rescue => e
+ ["Error when building Random marshal data #{e}", ""]
+ end
+
+ SwappedClass = nil
+ def self.set_swapped_class(cls)
+ remove_const(:SwappedClass)
+ const_set(:SwappedClass, cls)
+ end
+
+ def self.reset_swapped_class
+ set_swapped_class(nil)
+ end
+
+ class ClassWithOverriddenName
+ def self.name
+ "Foo"
+ end
+ end
+
+ class ModuleWithOverriddenName
+ def self.name
+ "Foo"
+ end
+ end
+
+ class TimeWithOverriddenName < Time
+ def self.name
+ "Foo"
+ end
+ end
+
+ class StructWithOverriddenName < Struct.new(:a)
+ def self.name
+ "Foo"
+ end
+ end
+
+ class UserDefinedWithOverriddenName < UserDefined
+ def self.name
+ "Foo"
+ end
+ end
+
+ class StringWithOverriddenName < String
+ def self.name
+ "Foo"
+ end
+ end
+
+ class ArrayWithOverriddenName < Array
+ def self.name
+ "Foo"
+ end
+ end
+
+ class HashWithOverriddenName < Hash
+ def self.name
+ "Foo"
+ end
+ end
+
+ class RegexpWithOverriddenName < Regexp
+ def self.name
+ "Foo"
+ end
+ end
+
+ class ObjectWithFreezeRaisingException < Object
+ def freeze
+ raise
+ end
+ end
+
+ class ObjectWithoutFreeze < Object
+ undef freeze
+ end
+
+ DATA = {
+ "nil" => [nil, "\004\b0"],
+ "1..2" => [(1..2),
+ "\004\bo:\nRange\b:\nbegini\006:\texclF:\bendi\a",
+ { begin: 1, end: 2, :exclude_end? => false }],
+ "1...2" => [(1...2),
+ "\004\bo:\nRange\b:\nbegini\006:\texclT:\bendi\a",
+ { begin: 1, end: 2, :exclude_end? => true }],
+ "'a'..'b'" => [('a'..'b'),
+ "\004\bo:\nRange\b:\nbegin\"\006a:\texclF:\bend\"\006b",
+ { begin: 'a', end: 'b', :exclude_end? => false }],
+ "Struct" => [Struct::Useful.new(1, 2),
+ "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"],
+ "Symbol" => [:symbol,
+ "\004\b:\vsymbol"],
+ "true" => [true,
+ "\004\bT"],
+ "false" => [false,
+ "\004\bF"],
+ "String empty" => ['',
+ "\004\b\"\000"],
+ "String small" => ['small',
+ "\004\b\"\012small"],
+ "String big" => ['big' * 100,
+ "\004\b\"\002,\001#{'big' * 100}"],
+ "String extended" => [''.dup.extend(Meths), # TODO: check for module on load
+ "\004\be:\nMeths\"\000"],
+ "String subclass" => [UserString.new,
+ "\004\bC:\017UserString\"\000"],
+ "String subclass extended" => [UserString.new.extend(Meths),
+ "\004\be:\nMethsC:\017UserString\"\000"],
+ "Symbol small" => [:big,
+ "\004\b:\010big"],
+ "Symbol big" => [('big' * 100).to_sym,
+ "\004\b:\002,\001#{'big' * 100}"],
+ "Integer -2**64" => [-2**64,
+ "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"],
+ "Integer -2**63" => [-2**63,
+ "\004\bl-\t\000\000\000\000\000\000\000\200"],
+ "Integer -2**24" => [-2**24,
+ "\004\bi\375\000\000\000"],
+ "Integer -4516727" => [-4516727,
+ "\004\bi\375\211\024\273"],
+ "Integer -2**16" => [-2**16,
+ "\004\bi\376\000\000"],
+ "Integer -2**8" => [-2**8,
+ "\004\bi\377\000"],
+ "Integer -123" => [-123,
+ "\004\bi\200"],
+ "Integer -124" => [-124, "\004\bi\377\204"],
+ "Integer 0" => [0,
+ "\004\bi\000"],
+ "Integer 5" => [5,
+ "\004\bi\n"],
+ "Integer 122" => [122, "\004\bi\177"],
+ "Integer 123" => [123, "\004\bi\001{"],
+ "Integer 2**8" => [2**8,
+ "\004\bi\002\000\001"],
+ "Integer 2**16" => [2**16,
+ "\004\bi\003\000\000\001"],
+ "Integer 2**24" => [2**24,
+ "\004\bi\004\000\000\000\001"],
+ "Integer 2**64" => [2**64,
+ "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"],
+ "Integer 2**90" => [2**90,
+ "\004\bl+\v#{"\000" * 11}\004"],
+ "Class String" => [String,
+ "\004\bc\vString"],
+ "Module Marshal" => [Marshal,
+ "\004\bm\fMarshal"],
+ "Module nested" => [UserDefined::Nested.new,
+ "\004\bo:\030UserDefined::Nested\000"],
+ "_dump object" => [UserDefinedWithIvar.new,
+ "\004\bu:\030UserDefinedWithIvar5\004\b[\bI\"\nstuff\006:\t@foo:\030UserDefinedWithIvar\"\tmore@\a"],
+ "_dump object extended" => [UserDefined.new.extend(Meths),
+ "\004\bu:\020UserDefined\022\004\b[\a\"\nstuff@\006"],
+ "marshal_dump object" => [UserMarshalWithIvar.new,
+ "\004\bU:\030UserMarshalWithIvar[\006\"\fmy data"],
+ "Regexp" => [/\A.\Z/,
+ "\004\b/\n\\A.\\Z\000"],
+ "Regexp subclass /i" => [UserRegexp.new('', Regexp::IGNORECASE),
+ "\004\bC:\017UserRegexp/\000\001"],
+ "Float 0.0" => [0.0,
+ "\004\bf\0060"],
+ "Float -0.0" => [-0.0,
+ "\004\bf\a-0"],
+ "Float Infinity" => [(1.0 / 0.0),
+ "\004\bf\binf"],
+ "Float -Infinity" => [(-1.0 / 0.0),
+ "\004\bf\t-inf"],
+ "Float 1.0" => [1.0,
+ "\004\bf\0061"],
+ "Float 8323434.342" => [8323434.342,
+ "\004\bf\0328323434.3420000002\000S\370"],
+ "Float 1.0799999999999912" => [1.0799999999999912,
+ "\004\bf\0321.0799999999999912\000\341 "],
+ "Hash" => [Hash.new,
+ "\004\b{\000"],
+ "Hash subclass" => [UserHash.new,
+ "\004\bC:\rUserHash{\000"],
+ "Array" => [Array.new,
+ "\004\b[\000"],
+ "Array subclass" => [UserArray.new,
+ "\004\bC:\016UserArray[\000"],
+ "Struct Pyramid" => [Struct::Pyramid.new,
+ "\004\bS:\024Struct::Pyramid\000"],
+ }
+ DATA_19 = {
+ "nil" => [nil, "\004\b0"],
+ "1..2" => [(1..2),
+ "\004\bo:\nRange\b:\nbegini\006:\texclF:\bendi\a",
+ { begin: 1, end: 2, :exclude_end? => false }],
+ "1...2" => [(1...2),
+ "\004\bo:\nRange\b:\nbegini\006:\texclT:\bendi\a",
+ { begin: 1, end: 2, :exclude_end? => true }],
+ "'a'..'b'" => [('a'..'b'),
+ "\004\bo:\nRange\b:\nbegin\"\006a:\texclF:\bend\"\006b",
+ { begin: 'a', end: 'b', :exclude_end? => false }],
+ "Struct" => [Struct::Useful.new(1, 2),
+ "\004\bS:\023Struct::Useful\a:\006ai\006:\006bi\a"],
+ "Symbol" => [:symbol,
+ "\004\b:\vsymbol"],
+ "true" => [true,
+ "\004\bT"],
+ "false" => [false,
+ "\004\bF"],
+ "String empty" => ['',
+ "\x04\bI\"\x00\x06:\x06EF"],
+ "String small" => ['small',
+ "\x04\bI\"\nsmall\x06:\x06EF"],
+ "String big" => ['big' * 100,
+ "\x04\bI\"\x02,\x01bigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbigbig\x06:\x06EF"],
+ "String extended" => [''.dup.extend(Meths), # TODO: check for module on load
+ "\x04\bIe:\nMeths\"\x00\x06:\x06EF"],
+ "String subclass" => [UserString.new,
+ "\004\bC:\017UserString\"\000"],
+ "String subclass extended" => [UserString.new.extend(Meths),
+ "\004\be:\nMethsC:\017UserString\"\000"],
+ "Symbol small" => [:big,
+ "\004\b:\010big"],
+ "Symbol big" => [('big' * 100).to_sym,
+ "\004\b:\002,\001#{'big' * 100}"],
+ "Integer -2**64" => [-2**64,
+ "\004\bl-\n\000\000\000\000\000\000\000\000\001\000"],
+ "Integer -2**63" => [-2**63,
+ "\004\bl-\t\000\000\000\000\000\000\000\200"],
+ "Integer -2**24" => [-2**24,
+ "\004\bi\375\000\000\000"],
+ "Integer -2**16" => [-2**16,
+ "\004\bi\376\000\000"],
+ "Integer -2**8" => [-2**8,
+ "\004\bi\377\000"],
+ "Integer -123" => [-123,
+ "\004\bi\200"],
+ "Integer 0" => [0,
+ "\004\bi\000"],
+ "Integer 5" => [5,
+ "\004\bi\n"],
+ "Integer 2**8" => [2**8,
+ "\004\bi\002\000\001"],
+ "Integer 2**16" => [2**16,
+ "\004\bi\003\000\000\001"],
+ "Integer 2**24" => [2**24,
+ "\004\bi\004\000\000\000\001"],
+ "Integer 2**64" => [2**64,
+ "\004\bl+\n\000\000\000\000\000\000\000\000\001\000"],
+ "Integer 2**90" => [2**90,
+ "\004\bl+\v#{"\000" * 11}\004"],
+ "Class String" => [String,
+ "\004\bc\vString"],
+ "Module Marshal" => [Marshal,
+ "\004\bm\fMarshal"],
+ "Module nested" => [UserDefined::Nested.new,
+ "\004\bo:\030UserDefined::Nested\000"],
+ "_dump object" => [UserDefinedWithIvar.new,
+ "\x04\bu:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a"],
+ "_dump object extended" => [UserDefined.new.extend(Meths),
+ "\x04\bu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06"],
+ "marshal_dump object" => [UserMarshalWithIvar.new,
+ "\x04\bU:\x18UserMarshalWithIvar[\x06I\"\fmy data\x06:\x06EF"],
+ "Regexp" => [/\A.\Z/,
+ "\x04\bI/\n\\A.\\Z\x00\x06:\x06EF"],
+ "Regexp subclass /i" => [UserRegexp.new('', Regexp::IGNORECASE),
+ "\x04\bIC:\x0FUserRegexp/\x00\x01\x06:\x06EF"],
+ "Float 0.0" => [0.0,
+ "\004\bf\0060"],
+ "Float -0.0" => [-0.0,
+ "\004\bf\a-0"],
+ "Float Infinity" => [(1.0 / 0.0),
+ "\004\bf\binf"],
+ "Float -Infinity" => [(-1.0 / 0.0),
+ "\004\bf\t-inf"],
+ "Float 1.0" => [1.0,
+ "\004\bf\0061"],
+ "Hash" => [Hash.new,
+ "\004\b{\000"],
+ "Hash subclass" => [UserHash.new,
+ "\004\bC:\rUserHash{\000"],
+ "Array" => [Array.new,
+ "\004\b[\000"],
+ "Array subclass" => [UserArray.new,
+ "\004\bC:\016UserArray[\000"],
+ "Struct Pyramid" => [Struct::Pyramid.new,
+ "\004\bS:\024Struct::Pyramid\000"],
+ "Random" => random_data,
+ }
+
+ module DataSpec
+ Measure = Data.define(:amount, :unit)
+ Empty = Data.define
+
+ MeasureExtended = Class.new(Measure)
+ MeasureExtended.extend(Enumerable)
+
+ class MeasureWithOverriddenName < Measure
+ def self.name
+ "Foo"
+ end
+ end
+ end
+end
+
+class ArraySub < Array
+ def initialize(*args)
+ super(args)
+ end
+end
+
+class ArraySubPush < Array
+ def << value
+ raise 'broken'
+ end
+ alias_method :push, :<<
+end
+
+class SameName
+end
+
+module NamespaceTest
+end
diff --git a/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb b/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb
new file mode 100644
index 0000000000..98a0d43392
--- /dev/null
+++ b/spec/ruby/core/marshal/fixtures/marshal_multibyte_data.rb
@@ -0,0 +1,12 @@
+# -*- encoding: utf-8 -*-
+
+module MarshalSpec
+ class Multibyteãã‚ãƒã„Class
+ end
+
+ module Multibyteã‘ã’ã“ã”Module
+ end
+
+ class Multibyteãã‚ãƒã„Time < Time
+ end
+end
diff --git a/spec/ruby/core/marshal/fixtures/random.dump b/spec/ruby/core/marshal/fixtures/random.dump
new file mode 100644
index 0000000000..0af56120aa
--- /dev/null
+++ b/spec/ruby/core/marshal/fixtures/random.dump
Binary files differ
diff --git a/spec/ruby/core/marshal/float_spec.rb b/spec/ruby/core/marshal/float_spec.rb
new file mode 100644
index 0000000000..5b2d68d6e1
--- /dev/null
+++ b/spec/ruby/core/marshal/float_spec.rb
@@ -0,0 +1,77 @@
+require_relative '../../spec_helper'
+
+describe "Marshal.dump with Float" do
+ it "represents NaN" do
+ Marshal.dump(nan_value).should == "\004\bf\bnan"
+ end
+
+ it "represents +Infinity" do
+ Marshal.dump(infinity_value).should == "\004\bf\binf"
+ end
+
+ it "represents -Infinity" do
+ Marshal.dump(-infinity_value).should == "\004\bf\t-inf"
+ end
+
+ it "represents zero" do
+ Marshal.dump(0.0).should == "\004\bf\0060"
+ end
+
+ it "represents a Float less than 1" do
+ Marshal.dump(0.666666667).should == "\x04\bf\x100.666666667"
+ end
+
+ it "represents a Float much less than 1" do
+ Marshal.dump(0.000000001234697).should == "\x04\bf\x101.234697e-9"
+ end
+
+ it "represents a Float greater than 1" do
+ Marshal.dump(42.666666667).should == "\x04\bf\x1142.666666667"
+ end
+
+ it "represents a Float much greater than 1" do
+ Marshal.dump(98743561239011.0).should == "\x04\bf\x1398743561239011"
+ end
+
+ it "represents a Float much greater than 1 with a very small fractional part" do
+ Marshal.dump(799346183459.0000000002999312541).should == "\x04\bf\x11799346183459"
+ end
+end
+
+describe "Marshal.load with Float" do
+ it "loads NaN" do
+ Marshal.load("\004\bf\bnan").should.nan?
+ end
+
+ it "loads +Infinity" do
+ Marshal.load("\004\bf\binf").should == infinity_value
+ end
+
+ it "loads -Infinity" do
+ Marshal.load("\004\bf\t-inf").should == -infinity_value
+ end
+
+ it "loads zero" do
+ Marshal.load("\004\bf\0060").should == 0.0
+ end
+
+ it "loads a Float less than 1" do
+ Marshal.load("\x04\bf\x100.666666667").should == 0.666666667
+ end
+
+ it "loads a Float much less than 1" do
+ Marshal.load("\x04\bf\x101.234697e-9").should == 0.000000001234697
+ end
+
+ it "loads a Float greater than 1" do
+ Marshal.load("\x04\bf\x1142.666666667").should == 42.666666667
+ end
+
+ it "loads a Float much greater than 1" do
+ Marshal.load("\x04\bf\x1398743561239011").should == 98743561239011.0
+ end
+
+ it "loads a Float much greater than 1 with a very small fractional part" do
+ Marshal.load("\x04\bf\x16793468359.0002999").should == 793468359.0002999
+ end
+end
diff --git a/spec/ruby/core/marshal/load_spec.rb b/spec/ruby/core/marshal/load_spec.rb
new file mode 100644
index 0000000000..a5bdfbf520
--- /dev/null
+++ b/spec/ruby/core/marshal/load_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/load'
+
+describe "Marshal.load" do
+ it_behaves_like :marshal_load, :load
+end
diff --git a/spec/ruby/core/marshal/major_version_spec.rb b/spec/ruby/core/marshal/major_version_spec.rb
new file mode 100644
index 0000000000..931f1f6c27
--- /dev/null
+++ b/spec/ruby/core/marshal/major_version_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Marshal::MAJOR_VERSION" do
+ it "is 4" do
+ Marshal::MAJOR_VERSION.should == 4
+ end
+end
diff --git a/spec/ruby/core/marshal/minor_version_spec.rb b/spec/ruby/core/marshal/minor_version_spec.rb
new file mode 100644
index 0000000000..f19210c4e1
--- /dev/null
+++ b/spec/ruby/core/marshal/minor_version_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Marshal::MINOR_VERSION" do
+ it "is 8" do
+ Marshal::MINOR_VERSION.should == 8
+ end
+end
diff --git a/spec/ruby/core/marshal/restore_spec.rb b/spec/ruby/core/marshal/restore_spec.rb
new file mode 100644
index 0000000000..7e75d7dea6
--- /dev/null
+++ b/spec/ruby/core/marshal/restore_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/load'
+
+describe "Marshal.restore" do
+ it_behaves_like :marshal_load, :restore
+end
diff --git a/spec/ruby/core/marshal/shared/load.rb b/spec/ruby/core/marshal/shared/load.rb
new file mode 100644
index 0000000000..02c8e7f0b1
--- /dev/null
+++ b/spec/ruby/core/marshal/shared/load.rb
@@ -0,0 +1,1291 @@
+# encoding: binary
+require_relative '../fixtures/marshal_data'
+
+describe :marshal_load, shared: true do
+ before :all do
+ @num_self_class = 1
+ end
+
+ it "raises an ArgumentError when the dumped data is truncated" do
+ obj = {first: 1, second: 2, third: 3}
+ -> { Marshal.send(@method, Marshal.dump(obj)[0, 5]) }.should.raise(ArgumentError, "marshal data too short")
+ end
+
+ it "raises an ArgumentError when the argument is empty String" do
+ -> { Marshal.send(@method, "") }.should.raise(ArgumentError, "marshal data too short")
+ end
+
+ it "raises an ArgumentError when the dumped class is missing" do
+ Object.send(:const_set, :KaBoom, Class.new)
+ kaboom = Marshal.dump(KaBoom.new)
+ Object.send(:remove_const, :KaBoom)
+
+ -> { Marshal.send(@method, kaboom) }.should.raise(ArgumentError)
+ end
+
+ describe "when called with freeze: true" do
+ it "returns frozen strings" do
+ string = Marshal.send(@method, Marshal.dump("foo"), freeze: true)
+ string.should == "foo"
+ string.should.frozen?
+
+ utf8_string = "foo".encode(Encoding::UTF_8)
+ string = Marshal.send(@method, Marshal.dump(utf8_string), freeze: true)
+ string.should == utf8_string
+ string.should.frozen?
+ end
+
+ it "returns frozen arrays" do
+ array = Marshal.send(@method, Marshal.dump([1, 2, 3]), freeze: true)
+ array.should == [1, 2, 3]
+ array.should.frozen?
+ end
+
+ it "returns frozen hashes" do
+ hash = Marshal.send(@method, Marshal.dump({foo: 42}), freeze: true)
+ hash.should == {foo: 42}
+ hash.should.frozen?
+ end
+
+ it "returns frozen regexps" do
+ regexp = Marshal.send(@method, Marshal.dump(/foo/), freeze: true)
+ regexp.should == /foo/
+ regexp.should.frozen?
+ end
+
+ it "returns frozen structs" do
+ struct = Marshal.send(@method, Marshal.dump(MarshalSpec::StructToDump.new(1, 2)), freeze: true)
+ struct.should == MarshalSpec::StructToDump.new(1, 2)
+ struct.should.frozen?
+ end
+
+ it "returns frozen objects" do
+ source_object = Object.new
+
+ object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+ object.should.frozen?
+ end
+
+ describe "deep freezing" do
+ it "returns hashes with frozen keys and values" do
+ key = Object.new
+ value = Object.new
+ source_object = {key => value}
+
+ hash = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+ hash.size.should == 1
+ hash.keys[0].should.frozen?
+ hash.values[0].should.frozen?
+ end
+
+ it "returns arrays with frozen elements" do
+ object = Object.new
+ source_object = [object]
+
+ array = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+ array.size.should == 1
+ array[0].should.frozen?
+ end
+
+ it "returns structs with frozen members" do
+ object1 = Object.new
+ object2 = Object.new
+ source_object = MarshalSpec::StructToDump.new(object1, object2)
+
+ struct = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+ struct.a.should.frozen?
+ struct.b.should.frozen?
+ end
+
+ it "returns objects with frozen instance variables" do
+ source_object = Object.new
+ instance_variable = Object.new
+ source_object.instance_variable_set(:@a, instance_variable)
+
+ object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+ object.instance_variable_get(:@a).should != nil
+ object.instance_variable_get(:@a).should.frozen?
+ end
+
+ it "deduplicates frozen strings" do
+ source_object = ["foo" + "bar", "foobar"]
+ object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+
+ object[0].should.equal?(object[1])
+ end
+ end
+
+ it "does not freeze modules" do
+ object = Marshal.send(@method, Marshal.dump(Kernel), freeze: true)
+ object.should_not.frozen?
+ Kernel.should_not.frozen?
+ end
+
+ it "does not freeze classes" do
+ object = Marshal.send(@method, Marshal.dump(Object), freeze: true)
+ object.should_not.frozen?
+ Object.should_not.frozen?
+ end
+
+ it "does freeze extended objects" do
+ object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", freeze: true)
+ object.should.frozen?
+ end
+
+ it "does freeze extended objects with instance variables" do
+ object = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x06:\n@ivarT", freeze: true)
+ object.should.frozen?
+ end
+
+ it "returns frozen object having #_dump method" do
+ object = Marshal.send(@method, Marshal.dump(UserDefined.new), freeze: true)
+ object.should.frozen?
+ end
+
+ it "returns frozen object responding to #marshal_dump and #marshal_load" do
+ object = Marshal.send(@method, Marshal.dump(UserMarshal.new), freeze: true)
+ object.should.frozen?
+ end
+
+ it "returns frozen object extended by a module" do
+ object = Object.new
+ object.extend(MarshalSpec::ModuleToExtendBy)
+
+ object = Marshal.send(@method, Marshal.dump(object), freeze: true)
+ object.should.frozen?
+ end
+
+ it "does not call freeze method" do
+ object = MarshalSpec::ObjectWithFreezeRaisingException.new
+ object = Marshal.send(@method, Marshal.dump(object), freeze: true)
+ object.should.frozen?
+ end
+
+ it "returns frozen object even if object does not respond to freeze method" do
+ object = MarshalSpec::ObjectWithoutFreeze.new
+ object = Marshal.send(@method, Marshal.dump(object), freeze: true)
+ object.should.frozen?
+ end
+
+ it "returns a frozen object when is an instance of String/Array/Regexp/Hash subclass and has instance variables" do
+ source_object = UserString.new
+ source_object.instance_variable_set(:@foo, "bar")
+
+ object = Marshal.send(@method, Marshal.dump(source_object), freeze: true)
+ object.should.frozen?
+ end
+
+ describe "when called with a proc" do
+ it "call the proc with frozen objects" do
+ arr = []
+ s = +'hi'
+ s.instance_variable_set(:@foo, 5)
+ st = Struct.new("Brittle", :a).new
+ st.instance_variable_set(:@clue, 'none')
+ st.a = 0.0
+ h = Hash.new('def')
+ h['nine'] = 9
+ a = [:a, :b, :c]
+ a.instance_variable_set(:@two, 2)
+ obj = [s, 10, s, s, st, a]
+ obj.instance_variable_set(:@zoo, 'ant')
+ proc = Proc.new { |o| arr << o; o}
+
+ Marshal.send(
+ @method,
+ "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F",
+ proc,
+ freeze: true,
+ )
+
+ arr.should == [
+ false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st,
+ :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]],
+ ]
+
+ arr.each do |v|
+ v.should.frozen?
+ end
+
+ Struct.send(:remove_const, :Brittle)
+ end
+
+ it "does not freeze the object returned by the proc" do
+ string = Marshal.send(@method, Marshal.dump("foo"), proc { |o| o.upcase }, freeze: true)
+ string.should == "FOO"
+ string.should_not.frozen?
+ end
+ end
+ end
+
+ describe "when called with a proc" do
+ it "call the proc with fully initialized strings" do
+ utf8_string = "foo".encode(Encoding::UTF_8)
+ Marshal.send(@method, Marshal.dump(utf8_string), proc { |arg|
+ if arg.is_a?(String)
+ arg.should == utf8_string
+ arg.encoding.should == Encoding::UTF_8
+ end
+ arg
+ })
+ end
+
+ it "no longer mutate the object after it was passed to the proc" do
+ string = Marshal.load(Marshal.dump("foo"), :freeze.to_proc)
+ string.should.frozen?
+ end
+
+ it "call the proc with extended objects" do
+ objs = []
+ obj = Marshal.load("\x04\be:\x0FEnumerableo:\vObject\x00", Proc.new { |o| objs << o; o })
+ objs.should == [obj]
+ end
+
+ it "returns the value of the proc" do
+ Marshal.send(@method, Marshal.dump([1,2]), proc { [3,4] }).should == [3,4]
+ end
+
+ it "calls the proc for recursively visited data" do
+ a = [1]
+ a << a
+ ret = []
+ Marshal.send(@method, Marshal.dump(a), proc { |arg| ret << arg.inspect; arg })
+ ret[0].should == 1.inspect
+ ret[1].should == a.inspect
+ ret.size.should == 2
+ end
+
+ it "loads an Array with proc" do
+ arr = []
+ s = +'hi'
+ s.instance_variable_set(:@foo, 5)
+ st = Struct.new("Brittle", :a).new
+ st.instance_variable_set(:@clue, 'none')
+ st.a = 0.0
+ h = Hash.new('def')
+ h['nine'] = 9
+ a = [:a, :b, :c]
+ a.instance_variable_set(:@two, 2)
+ obj = [s, 10, s, s, st, a]
+ obj.instance_variable_set(:@zoo, 'ant')
+ proc = Proc.new { |o| arr << o.dup; o}
+
+ Marshal.send(@method, "\x04\bI[\vI\"\ahi\a:\x06EF:\t@fooi\ni\x0F@\x06@\x06IS:\x14Struct::Brittle\x06:\x06af\x060\x06:\n@clueI\"\tnone\x06;\x00FI[\b;\b:\x06b:\x06c\x06:\t@twoi\a\x06:\t@zooI\"\bant\x06;\x00F", proc)
+
+ arr.should == [
+ false, 5, "hi", 10, "hi", "hi", 0.0, false, "none", st,
+ :b, :c, 2, a, false, "ant", ["hi", 10, "hi", "hi", st, [:a, :b, :c]],
+ ]
+ Struct.send(:remove_const, :Brittle)
+ end
+ end
+
+ describe "when called with nil for the proc argument" do
+ it "behaves as if no proc argument was passed" do
+ a = [1]
+ a << a
+ b = Marshal.send(@method, Marshal.dump(a), nil)
+ b.should == a
+ end
+ end
+
+ describe "when called on objects with custom _dump methods" do
+ it "does not set instance variables of an object with user-defined _dump/_load" do
+ # this string represents: <#UserPreviouslyDefinedWithInitializedIvar @field2=7 @field1=6>
+ dump_str = "\004\bu:-UserPreviouslyDefinedWithInitializedIvar\a:\f@field2i\f:\f@field1i\v"
+
+ UserPreviouslyDefinedWithInitializedIvar.should_receive(:_load).and_return(UserPreviouslyDefinedWithInitializedIvar.new)
+ marshaled_obj = Marshal.send(@method, dump_str)
+
+ marshaled_obj.should.instance_of?(UserPreviouslyDefinedWithInitializedIvar)
+ marshaled_obj.field1.should == nil
+ marshaled_obj.field2.should == nil
+ end
+
+ it "loads the String in non US-ASCII and non UTF-8 encoding" do
+ source_object = UserDefinedString.new("a".encode("windows-1251"))
+ object = Marshal.send(@method, Marshal.dump(source_object))
+ object.string.should == "a".encode("windows-1251")
+ end
+
+ it "loads the String in multibyte encoding" do
+ source_object = UserDefinedString.new("a".encode("utf-32le"))
+ object = Marshal.send(@method, Marshal.dump(source_object))
+ object.string.should == "a".encode("utf-32le")
+ end
+
+ describe "that returns an immediate value" do
+ it "loads an array containing an instance of the object, followed by multiple instances of another object" do
+ str = "string"
+
+ # this string represents: [<#UserDefinedImmediate A>, <#String "string">, <#String "string">]
+ marshaled_obj = Marshal.send(@method, "\004\b[\bu:\031UserDefinedImmediate\000\"\vstring@\a")
+
+ marshaled_obj.should == [nil, str, str]
+ end
+
+ it "loads any structure with multiple references to the same object, followed by multiple instances of another object" do
+ str = "string"
+
+ # this string represents: {a: <#UserDefinedImmediate A>, b: <#UserDefinedImmediate A>, c: <#String "string">, d: <#String "string">}
+ hash_dump = "\x04\b{\t:\x06aIu:\x19UserDefinedImmediate\x00\x06:\x06ET:\x06b@\x06:\x06cI\"\vstring\x06;\aT:\x06d@\a"
+
+ marshaled_obj = Marshal.send(@method, hash_dump)
+ marshaled_obj.should == {a: nil, b: nil, c: str, d: str}
+
+ # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate A>, <#String "string">, <#String "string">]
+ array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ET@\x06I\"\vstring\x06;\x06T@\a"
+
+ marshaled_obj = Marshal.send(@method, array_dump)
+ marshaled_obj.should == [nil, nil, str, str]
+ end
+
+ it "loads an array containing references to multiple instances of the object, followed by multiple instances of another object" do
+ str = "string"
+
+ # this string represents: [<#UserDefinedImmediate A>, <#UserDefinedImmediate B>, <#String "string">, <#String "string">]
+ array_dump = "\x04\b[\tIu:\x19UserDefinedImmediate\x00\x06:\x06ETIu;\x00\x00\x06;\x06TI\"\vstring\x06;\x06T@\b"
+
+ marshaled_obj = Marshal.send(@method, array_dump)
+ marshaled_obj.should == [nil, nil, str, str]
+ end
+ end
+ end
+
+ it "loads an array containing objects having _dump method, and with proc" do
+ arr = []
+ myproc = Proc.new { |o| arr << o.dup; o }
+ o1 = UserDefined.new;
+ o2 = UserDefinedWithIvar.new
+ obj = [o1, o2, o1, o2]
+
+ Marshal.send(@method, "\x04\b[\tu:\x10UserDefined\x18\x04\b[\aI\"\nstuff\x06:\x06EF@\x06u:\x18UserDefinedWithIvar>\x04\b[\bI\"\nstuff\a:\x06EF:\t@foo:\x18UserDefinedWithIvarI\"\tmore\x06;\x00F@\a@\x06@\a", myproc)
+
+ arr[0].should == o1
+ arr[1].should == o2
+ arr[2].should == obj
+ arr.size.should == 3
+ end
+
+ it "loads an array containing objects having marshal_dump method, and with proc" do
+ arr = []
+ proc = Proc.new { |o| arr << o.dup; o }
+ o1 = UserMarshal.new
+ o2 = UserMarshalWithIvar.new
+
+ Marshal.send(@method, "\004\b[\tU:\020UserMarshal\"\nstuffU:\030UserMarshalWithIvar[\006\"\fmy data@\006@\b", proc)
+
+ arr[0].should == 'stuff'
+ arr[1].should == o1
+ arr[2].should == 'my data'
+ arr[3].should == ['my data']
+ arr[4].should == o2
+ arr[5].should == [o1, o2, o1, o2]
+
+ arr.size.should == 6
+ end
+
+ it "assigns classes to nested subclasses of Array correctly" do
+ arr = ArraySub.new(ArraySub.new)
+ arr_dump = Marshal.dump(arr)
+ Marshal.send(@method, arr_dump).class.should == ArraySub
+ end
+
+ it "loads subclasses of Array with overridden << and push correctly" do
+ arr = ArraySubPush.new
+ arr[0] = '1'
+ arr_dump = Marshal.dump(arr)
+ Marshal.send(@method, arr_dump).should == arr
+ end
+
+ it "raises a TypeError with bad Marshal version" do
+ marshal_data = +'\xff\xff'
+ marshal_data[0] = (Marshal::MAJOR_VERSION).chr
+ marshal_data[1] = (Marshal::MINOR_VERSION + 1).chr
+
+ -> { Marshal.send(@method, marshal_data) }.should.raise(TypeError)
+
+ marshal_data = +'\xff\xff'
+ marshal_data[0] = (Marshal::MAJOR_VERSION - 1).chr
+ marshal_data[1] = (Marshal::MINOR_VERSION).chr
+
+ -> { Marshal.send(@method, marshal_data) }.should.raise(TypeError)
+ end
+
+ it "raises EOFError on loading an empty file" do
+ temp_file = tmp("marshal.rubyspec.tmp.#{Process.pid}")
+ file = File.new(temp_file, "w+")
+ begin
+ -> { Marshal.send(@method, file) }.should.raise(EOFError)
+ ensure
+ file.close
+ rm_r temp_file
+ end
+ end
+
+ # Note: Ruby 1.9 should be compatible with older marshal format
+ MarshalSpec::DATA.each do |description, (object, marshal, attributes)|
+ it "loads a #{description}" do
+ Marshal.send(@method, marshal).should == object
+ end
+ end
+
+ MarshalSpec::DATA_19.each do |description, (object, marshal, attributes)|
+ it "loads a #{description}" do
+ Marshal.send(@method, marshal).should == object
+ end
+ end
+
+ describe "for an Array" do
+ it "loads an array containing the same objects" do
+ s = 'oh'
+ b = 'hi'
+ r = //
+ d = [b, :no, s, :go]
+ c = String
+ f = 1.0
+
+ o1 = UserMarshalWithIvar.new; o2 = UserMarshal.new
+
+ obj = [:so, 'hello', 100, :so, :so, d, :so, o2, :so, :no, o2,
+ :go, c, nil, Struct::Pyramid.new, f, :go, :no, s, b, r,
+ :so, 'huh', o1, true, b, b, 99, r, b, s, :so, f, c, :no, o1, d]
+
+ Marshal.send(@method, "\004\b[*:\aso\"\nhelloii;\000;\000[\t\"\ahi:\ano\"\aoh:\ago;\000U:\020UserMarshal\"\nstuff;\000;\006@\n;\ac\vString0S:\024Struct::Pyramid\000f\0061;\a;\006@\t@\b/\000\000;\000\"\bhuhU:\030UserMarshalWithIvar[\006\"\fmy dataT@\b@\bih@\017@\b@\t;\000@\016@\f;\006@\021@\a").should ==
+ obj
+ end
+
+ it "loads an array having ivar" do
+ s = +'well'
+ s.instance_variable_set(:@foo, 10)
+ obj = ['5', s, 'hi'].extend(Meths, MethsMore)
+ obj.instance_variable_set(:@mix, s)
+ new_obj = Marshal.send(@method, "\004\bI[\b\"\0065I\"\twell\006:\t@fooi\017\"\ahi\006:\t@mix@\a")
+ new_obj.should == obj
+ new_obj.instance_variable_get(:@mix).should.equal? new_obj[1]
+ new_obj[1].instance_variable_get(:@foo).should == 10
+ end
+
+ it "loads an extended Array object containing a user-marshaled object" do
+ obj = [UserMarshal.new, UserMarshal.new].extend(Meths)
+ dump = "\x04\be:\nMeths[\ao:\x10UserMarshal\x06:\n@dataI\"\nstuff\x06:\x06ETo;\x06\x06;\aI\"\nstuff\x06;\bT"
+ new_obj = Marshal.send(@method, dump)
+
+ new_obj.should == obj
+ obj_ancestors = class << obj; ancestors[1..-1]; end
+ new_obj_ancestors = class << new_obj; ancestors[1..-1]; end
+ obj_ancestors.should == new_obj_ancestors
+ end
+ end
+
+ describe "for a Hash" do
+ it "loads an extended_user_hash with a parameter to initialize" do
+ obj = UserHashInitParams.new(:abc).extend(Meths)
+
+ new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\027UserHashInitParams{\000\006:\a@a:\babc")
+
+ new_obj.should == obj
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class].should == Meths
+ new_obj_metaclass_ancestors[@num_self_class+1].should == UserHashInitParams
+ end
+
+ it "loads an extended hash object containing a user-marshaled object" do
+ obj = {a: UserMarshal.new}.extend(Meths)
+
+ new_obj = Marshal.send(@method, "\004\be:\nMeths{\006:\006aU:\020UserMarshal\"\nstuff")
+
+ new_obj.should == obj
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class].should == Meths
+ new_obj_metaclass_ancestors[@num_self_class+1].should == Hash
+ end
+
+ it "preserves hash ivars when hash contains a string having ivar" do
+ s = +'string'
+ s.instance_variable_set :@string_ivar, 'string ivar'
+ h = { key: s }
+ h.instance_variable_set :@hash_ivar, 'hash ivar'
+
+ unmarshalled = Marshal.send(@method, Marshal.dump(h))
+ unmarshalled.instance_variable_get(:@hash_ivar).should == 'hash ivar'
+ unmarshalled[:key].instance_variable_get(:@string_ivar).should == 'string ivar'
+ end
+
+ it "preserves compare_by_identity behaviour" do
+ h = { a: 1 }
+ h.compare_by_identity
+ unmarshalled = Marshal.send(@method, Marshal.dump(h))
+ unmarshalled.should.compare_by_identity?
+
+ h = { a: 1 }
+ unmarshalled = Marshal.send(@method, Marshal.dump(h))
+ unmarshalled.should_not.compare_by_identity?
+ end
+
+ it "preserves compare_by_identity behaviour for a Hash subclass" do
+ h = UserHash.new({ a: 1 })
+ h.compare_by_identity
+ unmarshalled = Marshal.send(@method, Marshal.dump(h))
+ unmarshalled.should.compare_by_identity?
+
+ h = UserHash.new({ a: 1 })
+ unmarshalled = Marshal.send(@method, Marshal.dump(h))
+ unmarshalled.should_not.compare_by_identity?
+ end
+
+ it "allocates an instance of the proper class when Hash subclass with compare_by_identity behaviour" do
+ h = UserHash.new({ a: 1 })
+ h.compare_by_identity
+
+ unmarshalled = Marshal.send(@method, Marshal.dump(h))
+ unmarshalled.should.kind_of?(UserHash)
+ end
+ end
+
+ describe "for a Symbol" do
+ it "loads a Symbol" do
+ sym = Marshal.send(@method, "\004\b:\vsymbol")
+ sym.should == :symbol
+ sym.encoding.should == Encoding::US_ASCII
+ end
+
+ it "loads a big Symbol" do
+ sym = ('big' * 100).to_sym
+ Marshal.send(@method, "\004\b:\002,\001#{'big' * 100}").should == sym
+ end
+
+ it "loads an encoded Symbol" do
+ s = "\u2192"
+
+ sym = Marshal.send(@method, "\x04\bI:\b\xE2\x86\x92\x06:\x06ET")
+ sym.should == s.encode("utf-8").to_sym
+ sym.encoding.should == Encoding::UTF_8
+
+ sym = Marshal.send(@method, "\x04\bI:\t\xFE\xFF!\x92\x06:\rencoding\"\vUTF-16")
+ sym.should == s.encode("utf-16").to_sym
+ sym.encoding.should == Encoding::UTF_16
+
+ sym = Marshal.send(@method, "\x04\bI:\a\x92!\x06:\rencoding\"\rUTF-16LE")
+ sym.should == s.encode("utf-16le").to_sym
+ sym.encoding.should == Encoding::UTF_16LE
+
+ sym = Marshal.send(@method, "\x04\bI:\a!\x92\x06:\rencoding\"\rUTF-16BE")
+ sym.should == s.encode("utf-16be").to_sym
+ sym.encoding.should == Encoding::UTF_16BE
+
+ sym = Marshal.send(@method, "\x04\bI:\a\xA2\xAA\x06:\rencoding\"\vEUC-JP")
+ sym.should == s.encode("euc-jp").to_sym
+ sym.encoding.should == Encoding::EUC_JP
+
+ sym = Marshal.send(@method, "\x04\bI:\a\x81\xA8\x06:\rencoding\"\x10Windows-31J")
+ sym.should == s.encode("sjis").to_sym
+ sym.encoding.should == Encoding::SJIS
+ end
+
+ it "loads a binary encoded Symbol" do
+ s = "\u2192".dup.force_encoding("binary").to_sym
+ sym = Marshal.send(@method, "\x04\b:\b\xE2\x86\x92")
+ sym.should == s
+ sym.encoding.should == Encoding::BINARY
+ end
+
+ it "loads multiple Symbols sharing the same encoding" do
+ # Note that the encoding is a link for the second Symbol
+ symbol1 = "I:\t\xE2\x82\xACa\x06:\x06ET"
+ symbol2 = "I:\t\xE2\x82\xACb\x06;\x06T"
+ dump = "\x04\b[\a#{symbol1}#{symbol2}"
+ value = Marshal.send(@method, dump)
+ value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8]
+ expected = [
+ "€a".dup.force_encoding(Encoding::UTF_8).to_sym,
+ "€b".dup.force_encoding(Encoding::UTF_8).to_sym
+ ]
+ value.should == expected
+
+ value = Marshal.send(@method, "\x04\b[\b#{symbol1}#{symbol2};\x00")
+ value.map(&:encoding).should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8]
+ value.should == [*expected, expected[0]]
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before symbol characters end" do
+ Marshal.dump(:hello).should == "\x04\b:\nhello"
+
+ -> {
+ Marshal.send(@method, "\x04\b:\nhel")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for a String" do
+ it "loads a string having ivar with ref to self" do
+ obj = +'hi'
+ obj.instance_variable_set(:@self, obj)
+ Marshal.send(@method, "\004\bI\"\ahi\006:\n@self@\000").should == obj
+ end
+
+ it "loads a string through StringIO stream" do
+ require 'stringio'
+ obj = "This is a string which should be unmarshalled through StringIO stream!"
+ Marshal.send(@method, StringIO.new(Marshal.dump(obj))).should == obj
+ end
+
+ it "sets binmode if it is loading through StringIO stream" do
+ io = StringIO.new("\004\b:\vsymbol")
+ def io.binmode; raise "binmode"; end
+ -> { Marshal.load(io) }.should.raise(RuntimeError, "binmode")
+ end
+
+ it "loads a string with an ivar" do
+ str = Marshal.send(@method, "\x04\bI\"\x00\x06:\t@fooI\"\bbar\x06:\x06EF")
+ str.instance_variable_get("@foo").should == "bar"
+ end
+
+ it "loads a String subclass with custom constructor" do
+ str = Marshal.send(@method, "\x04\bC: UserCustomConstructorString\"\x00")
+ str.should.instance_of?(UserCustomConstructorString)
+ end
+
+ it "loads a US-ASCII String" do
+ str = "abc".dup.force_encoding("us-ascii")
+ data = "\x04\bI\"\babc\x06:\x06EF"
+ result = Marshal.send(@method, data)
+ result.should == str
+ result.encoding.should.equal?(Encoding::US_ASCII)
+ end
+
+ it "loads a UTF-8 String" do
+ str = "\x6d\xc3\xb6\x68\x72\x65".dup.force_encoding("utf-8")
+ data = "\x04\bI\"\vm\xC3\xB6hre\x06:\x06ET"
+ result = Marshal.send(@method, data)
+ result.should == str
+ result.encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "loads a String in another encoding" do
+ str = "\x6d\x00\xf6\x00\x68\x00\x72\x00\x65\x00".dup.force_encoding("utf-16le")
+ data = "\x04\bI\"\x0Fm\x00\xF6\x00h\x00r\x00e\x00\x06:\rencoding\"\rUTF-16LE"
+ result = Marshal.send(@method, data)
+ result.should == str
+ result.encoding.should.equal?(Encoding::UTF_16LE)
+ end
+
+ it "loads a String as BINARY if no encoding is specified at the end" do
+ str = "\xC3\xB8".dup.force_encoding("BINARY")
+ data = "\x04\b\"\a\xC3\xB8".dup.force_encoding("UTF-8")
+ result = Marshal.send(@method, data)
+ result.encoding.should == Encoding::BINARY
+ result.should == str
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before string characters end" do
+ Marshal.dump("hello").should == "\x04\b\"\nhello"
+
+ -> {
+ Marshal.send(@method, "\x04\b\"\nhel")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for a Struct" do
+ it "loads a extended_struct having fields with same objects" do
+ s = 'hi'
+ obj = Struct.new("Extended", :a, :b).new.extend(Meths)
+ dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a0:\006b0"
+ Marshal.send(@method, dump).should == obj
+
+ obj.a = [:a, s]
+ obj.b = [:Meths, s]
+ dump = "\004\be:\nMethsS:\025Struct::Extended\a:\006a[\a;\a\"\ahi:\006b[\a;\000@\a"
+ Marshal.send(@method, dump).should == obj
+ Struct.send(:remove_const, :Extended)
+ end
+
+ it "loads a struct having ivar" do
+ obj = Struct.new("Thick").new
+ obj.instance_variable_set(:@foo, 5)
+ reloaded = Marshal.send(@method, "\004\bIS:\022Struct::Thick\000\006:\t@fooi\n")
+ reloaded.should == obj
+ reloaded.instance_variable_get(:@foo).should == 5
+ Struct.send(:remove_const, :Thick)
+ end
+
+ it "loads a struct having fields" do
+ obj = Struct.new("Ure1", :a, :b).new
+ Marshal.send(@method, "\004\bS:\021Struct::Ure1\a:\006a0:\006b0").should == obj
+ Struct.send(:remove_const, :Ure1)
+ end
+
+ it "does not call initialize on the unmarshaled struct" do
+ threadlocal_key = MarshalSpec::StructWithUserInitialize::THREADLOCAL_KEY
+
+ s = MarshalSpec::StructWithUserInitialize.new('foo')
+ Thread.current[threadlocal_key].should == ['foo']
+ s.a.should == 'foo'
+
+ Thread.current[threadlocal_key] = nil
+
+ dumped = Marshal.dump(s)
+ loaded = Marshal.send(@method, dumped)
+
+ Thread.current[threadlocal_key].should == nil
+ loaded.a.should == 'foo'
+ end
+ end
+
+ describe "for a Data" do
+ it "loads a Data" do
+ obj = MarshalSpec::DataSpec::Measure.new(100, 'km')
+ dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
+ Marshal.dump(obj).should == dumped
+
+ Marshal.send(@method, dumped).should == obj
+ end
+
+ it "loads an extended Data" do
+ obj = MarshalSpec::DataSpec::MeasureExtended.new(100, "km")
+ dumped = "\x04\bS:+MarshalSpec::DataSpec::MeasureExtended\a:\vamountii:\tunit\"\akm"
+ Marshal.dump(obj).should == dumped
+
+ Marshal.send(@method, dumped).should == obj
+ end
+
+ it "returns a frozen object" do
+ obj = MarshalSpec::DataSpec::Measure.new(100, 'km')
+ dumped = "\x04\bS:#MarshalSpec::DataSpec::Measure\a:\vamountii:\tunit\"\akm"
+ Marshal.dump(obj).should == dumped
+
+ Marshal.send(@method, dumped).should.frozen?
+ end
+ end
+
+ describe "for an Exception" do
+ it "loads a marshalled exception with no message" do
+ obj = Exception.new
+ loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg0")
+ loaded.message.should == obj.message
+ loaded.backtrace.should == obj.backtrace
+ loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesg0:\abt0")
+ loaded.message.should == obj.message
+ loaded.backtrace.should == obj.backtrace
+ end
+
+ it "loads a marshalled exception with a message" do
+ obj = Exception.new("foo")
+ loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt0:\tmesg\"\bfoo")
+ loaded.message.should == obj.message
+ loaded.backtrace.should == obj.backtrace
+ loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt0")
+ loaded.message.should == obj.message
+ loaded.backtrace.should == obj.backtrace
+ end
+
+ it "loads a marshalled exception with a backtrace" do
+ obj = Exception.new("foo")
+ obj.set_backtrace(["foo/bar.rb:10"])
+ loaded = Marshal.send(@method, "\004\bo:\016Exception\a:\abt[\006\"\022foo/bar.rb:10:\tmesg\"\bfoo")
+ loaded.message.should == obj.message
+ loaded.backtrace.should == obj.backtrace
+ loaded = Marshal.send(@method, "\x04\bo:\x0EException\a:\tmesgI\"\bfoo\x06:\x06EF:\abt[\x06I\"\x12foo/bar.rb:10\x06;\aF")
+ loaded.message.should == obj.message
+ loaded.backtrace.should == obj.backtrace
+ end
+
+ it "loads an marshalled exception with ivars" do
+ s = 'hi'
+ arr = [:so, :so, s, s]
+ obj = Exception.new("foo")
+ obj.instance_variable_set :@arr, arr
+
+ loaded = Marshal.send(@method, "\x04\bo:\x0EException\b:\tmesg\"\bfoo:\abt0:\t@arr[\t:\aso;\t\"\ahi@\b")
+ new_arr = loaded.instance_variable_get :@arr
+
+ loaded.message.should == obj.message
+ new_arr.should == arr
+ end
+ end
+
+ describe "for an Object" do
+ it "loads an object" do
+ Marshal.send(@method, "\004\bo:\vObject\000").should.is_a?(Object)
+ end
+
+ it "loads an extended Object" do
+ obj = Object.new.extend(Meths)
+
+ new_obj = Marshal.send(@method, "\004\be:\nMethso:\vObject\000")
+
+ new_obj.class.should == obj.class
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class, 2].should == [Meths, Object]
+ end
+
+ it "loads an object having ivar" do
+ s = 'hi'
+ arr = [:so, :so, s, s]
+ obj = Object.new
+ obj.instance_variable_set :@str, arr
+
+ new_obj = Marshal.send(@method, "\004\bo:\vObject\006:\t@str[\t:\aso;\a\"\ahi@\a")
+ new_str = new_obj.instance_variable_get :@str
+
+ new_str.should == arr
+ end
+
+ it "loads an Object with a non-US-ASCII instance variable" do
+ ivar = "@é".dup.force_encoding(Encoding::UTF_8).to_sym
+ obj = Marshal.send(@method, "\x04\bo:\vObject\x06I:\b@\xC3\xA9\x06:\x06ETi\x06")
+ obj.instance_variables.should == [ivar]
+ obj.instance_variables[0].encoding.should == Encoding::UTF_8
+ obj.instance_variable_get(ivar).should == 1
+ end
+
+ it "raises ArgumentError if the object from an 'o' stream is not dumpable as 'o' type user class" do
+ -> do
+ Marshal.send(@method, "\x04\bo:\tFile\001\001:\001\005@path\"\x10/etc/passwd")
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before class name end" do
+ Marshal.dump(Object.new).should == "\x04\bo:\vObject\x00"
+
+ -> {
+ Marshal.send(@method, "\x04\bo:\vObj")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for an object responding to #marshal_dump and #marshal_load" do
+ it "loads a user-marshaled object" do
+ obj = UserMarshal.new
+ obj.data = :data
+ value = [obj, :data]
+ dump = Marshal.dump(value)
+ dump.should == "\x04\b[\aU:\x10UserMarshal:\tdata;\x06"
+ reloaded = Marshal.load(dump)
+ reloaded.should == value
+ end
+ end
+
+ describe "for a user object" do
+ it "loads a user-marshaled extended object" do
+ obj = UserMarshal.new.extend(Meths)
+
+ new_obj = Marshal.send(@method, "\004\bU:\020UserMarshal\"\nstuff")
+
+ new_obj.should == obj
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class].should == UserMarshal
+ end
+
+ it "loads a UserObject" do
+ Marshal.send(@method, "\004\bo:\017UserObject\000").should.is_a?(UserObject)
+ end
+
+ describe "that extends a core type other than Object or BasicObject" do
+ after :each do
+ MarshalSpec.reset_swapped_class
+ end
+
+ it "raises ArgumentError if the resulting class does not extend the same type" do
+ MarshalSpec.set_swapped_class(Class.new(Hash))
+ data = Marshal.dump(MarshalSpec::SwappedClass.new)
+
+ MarshalSpec.set_swapped_class(Class.new(Array))
+ -> { Marshal.send(@method, data) }.should.raise(ArgumentError)
+
+ MarshalSpec.set_swapped_class(Class.new)
+ -> { Marshal.send(@method, data) }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ describe "for a Regexp" do
+ ruby_version_is "4.1" do
+ it "raises FrozenError for an extended Regexp" do
+ -> {
+ Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000")
+ }.should.raise(FrozenError)
+ end
+
+ it "raises FrozenError when regexp has instance variables" do
+ -> {
+ Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/")
+ }.should.raise(FrozenError)
+ end
+ end
+
+ ruby_version_is ""..."4.1" do
+ it "loads an extended Regexp" do
+ obj = /[a-z]/.dup.extend(Meths, MethsMore)
+ new_obj = Marshal.send(@method, "\004\be:\nMethse:\016MethsMore/\n[a-z]\000")
+
+ new_obj.should == obj
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class, 3].should ==
+ [Meths, MethsMore, Regexp]
+ end
+
+ it "restore the regexp instance variables" do
+ obj = Regexp.new("hello")
+ obj.instance_variable_set(:@regexp_ivar, [42])
+
+ new_obj = Marshal.send(@method, "\x04\bI/\nhello\x00\a:\x06EF:\x11@regexp_ivar[\x06i/")
+ new_obj.instance_variables.should == [:@regexp_ivar]
+ new_obj.instance_variable_get(:@regexp_ivar).should == [42]
+ end
+ end
+
+ it "loads a Regexp subclass instance variables" do
+ obj = UserRegexp.new('abc')
+ obj.instance_variable_set(:@noise, 'much')
+
+ new_obj = Marshal.send(@method, Marshal.dump(obj))
+
+ new_obj.should == obj
+ new_obj.instance_variable_get(:@noise).should == 'much'
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class, 2].should ==
+ [UserRegexp, Regexp]
+ end
+
+ it "loads a Regexp subclass instance variables when it is extended with a module" do
+ obj = UserRegexp.new('').extend(Meths)
+ obj.instance_variable_set(:@noise, 'much')
+
+ new_obj = Marshal.send(@method, "\004\bIe:\nMethsC:\017UserRegexp/\000\000\006:\v@noise\"\tmuch")
+
+ new_obj.should == obj
+ new_obj.instance_variable_get(:@noise).should == 'much'
+ new_obj_metaclass_ancestors = class << new_obj; ancestors; end
+ new_obj_metaclass_ancestors[@num_self_class, 3].should ==
+ [Meths, UserRegexp, Regexp]
+ end
+
+ it "preserves Regexp encoding" do
+ source_object = Regexp.new("a".encode("utf-32le"))
+ regexp = Marshal.send(@method, Marshal.dump(source_object))
+
+ regexp.encoding.should == Encoding::UTF_32LE
+ regexp.source.should == "a".encode("utf-32le")
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before source string end" do
+ Marshal.dump(/hello world/).should == "\x04\bI/\x10hello world\x00\x06:\x06EF"
+
+ -> {
+ Marshal.send(@method, "\x04\bI/\x10hel")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for a Float" do
+ it "loads a Float NaN" do
+ obj = 0.0 / 0.0
+ Marshal.send(@method, "\004\bf\bnan").to_s.should == obj.to_s
+ end
+
+ it "loads a Float 1.3" do
+ Marshal.send(@method, "\004\bf\v1.3\000\314\315").should == 1.3
+ end
+
+ it "loads a Float -5.1867345e-22" do
+ obj = -5.1867345e-22
+ Marshal.send(@method, "\004\bf\037-5.1867345000000008e-22\000\203_").should be_close(obj, 1e-30)
+ end
+
+ it "loads a Float 1.1867345e+22" do
+ obj = 1.1867345e+22
+ Marshal.send(@method, "\004\bf\0361.1867344999999999e+22\000\344@").should == obj
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before float string representation end" do
+ Marshal.dump(1.3).should == "\x04\bf\b1.3"
+
+ -> {
+ Marshal.send(@method, "\004\bf\v1")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for an Integer" do
+ it "loads 0" do
+ Marshal.send(@method, "\004\bi\000").should == 0
+ Marshal.send(@method, "\004\bi\005").should == 0
+ end
+
+ it "loads an Integer 8" do
+ Marshal.send(@method, "\004\bi\r" ).should == 8
+ end
+
+ it "loads and Integer -8" do
+ Marshal.send(@method, "\004\bi\363" ).should == -8
+ end
+
+ it "loads an Integer 1234" do
+ Marshal.send(@method, "\004\bi\002\322\004").should == 1234
+ end
+
+ it "loads an Integer -1234" do
+ Marshal.send(@method, "\004\bi\376.\373").should == -1234
+ end
+
+ it "loads an Integer 4611686018427387903" do
+ Marshal.send(@method, "\004\bl+\t\377\377\377\377\377\377\377?").should == 4611686018427387903
+ end
+
+ it "loads an Integer -4611686018427387903" do
+ Marshal.send(@method, "\004\bl-\t\377\377\377\377\377\377\377?").should == -4611686018427387903
+ end
+
+ it "loads an Integer 2361183241434822606847" do
+ Marshal.send(@method, "\004\bl+\n\377\377\377\377\377\377\377\377\177\000").should == 2361183241434822606847
+ end
+
+ it "loads an Integer -2361183241434822606847" do
+ Marshal.send(@method, "\004\bl-\n\377\377\377\377\377\377\377\377\177\000").should == -2361183241434822606847
+ end
+
+ it "raises ArgumentError if the input is too short" do
+ ["\004\bi",
+ "\004\bi\001",
+ "\004\bi\002",
+ "\004\bi\002\0",
+ "\004\bi\003",
+ "\004\bi\003\0",
+ "\004\bi\003\0\0",
+ "\004\bi\004",
+ "\004\bi\004\0",
+ "\004\bi\004\0\0",
+ "\004\bi\004\0\0\0"].each do |invalid|
+ -> { Marshal.send(@method, invalid) }.should.raise(ArgumentError)
+ end
+ end
+
+ if 0.size == 8 # for platforms like x86_64
+ it "roundtrips 4611686018427387903 from dump/load correctly" do
+ Marshal.send(@method, Marshal.dump(4611686018427387903)).should == 4611686018427387903
+ end
+ end
+ end
+
+ describe "for a Rational" do
+ it "loads" do
+ r = Marshal.send(@method, Marshal.dump(Rational(1, 3)))
+ r.should == Rational(1, 3)
+ r.should.frozen?
+ end
+ end
+
+ describe "for a Complex" do
+ it "loads" do
+ c = Marshal.send(@method, Marshal.dump(Complex(4, 3)))
+ c.should == Complex(4, 3)
+ c.should.frozen?
+ end
+ end
+
+ describe "for a Bignum" do
+ platform_is c_long_size: 64 do
+ context "that is Bignum on 32-bit platforms but Fixnum on 64-bit" do
+ it "dumps a Fixnum" do
+ val = Marshal.send(@method, "\004\bl+\ab:wU")
+ val.should == 1433877090
+ val.class.should == Integer
+ end
+
+ it "dumps an array containing multiple references to the Bignum as an array of Fixnum" do
+ arr = Marshal.send(@method, "\004\b[\al+\a\223BwU@\006")
+ arr.should == [1433879187, 1433879187]
+ arr.each { |v| v.class.should == Integer }
+ end
+ end
+ end
+ end
+
+ describe "for a Time" do
+ it "loads" do
+ Marshal.send(@method, Marshal.dump(Time.at(1))).should == Time.at(1)
+ end
+
+ it "loads serialized instance variables" do
+ t = Time.new
+ t.instance_variable_set(:@foo, 'bar')
+
+ Marshal.send(@method, Marshal.dump(t)).instance_variable_get(:@foo).should == 'bar'
+ end
+
+ it "loads Time objects stored as links" do
+ t = Time.new
+
+ t1, t2 = Marshal.send(@method, Marshal.dump([t, t]))
+ t1.should.equal? t2
+ end
+
+ it "keeps the local zone" do
+ with_timezone 'AST', 3 do
+ t = Time.local(2012, 1, 1)
+ Marshal.send(@method, Marshal.dump(t)).zone.should == t.zone
+ end
+ end
+
+ it "keeps UTC zone" do
+ t = Time.now.utc
+ t2 = Marshal.send(@method, Marshal.dump(t))
+ t2.should.utc?
+ end
+
+ it "keeps the zone" do
+ t = nil
+
+ with_timezone 'AST', 4 do
+ t = Time.local(2012, 1, 1)
+ end
+
+ with_timezone 'EET', -2 do
+ Marshal.send(@method, Marshal.dump(t)).zone.should == 'AST'
+ end
+ end
+
+ it "keeps utc offset" do
+ t = Time.new(2007,11,1,15,25,0, "+09:00")
+ t2 = Marshal.send(@method, Marshal.dump(t))
+ t2.utc_offset.should == 32400
+ end
+
+ it "keeps nanoseconds" do
+ t = Time.now
+ Marshal.send(@method, Marshal.dump(t)).nsec.should == t.nsec
+ end
+
+ it "does not add any additional instance variable" do
+ t = Time.now
+ t2 = Marshal.send(@method, Marshal.dump(t))
+ t2.instance_variables.should.empty?
+ end
+ end
+
+ describe "for nil" do
+ it "loads" do
+ Marshal.send(@method, "\x04\b0").should == nil
+ end
+ end
+
+ describe "for true" do
+ it "loads" do
+ Marshal.send(@method, "\x04\bT").should == true
+ end
+ end
+
+ describe "for false" do
+ it "loads" do
+ Marshal.send(@method, "\x04\bF").should == false
+ end
+ end
+
+ describe "for a Class" do
+ it "loads" do
+ Marshal.send(@method, "\x04\bc\vString").should == String
+ end
+
+ it "raises ArgumentError if given the name of a non-Module" do
+ -> { Marshal.send(@method, "\x04\bc\vKernel") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if given a nonexistent class" do
+ -> { Marshal.send(@method, "\x04\bc\vStrung") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before class name end" do
+ Marshal.dump(String).should == "\x04\bc\vString"
+
+ -> {
+ Marshal.send(@method, "\x04\bc\vStr")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for a Module" do
+ it "loads a module" do
+ Marshal.send(@method, "\x04\bm\vKernel").should == Kernel
+ end
+
+ it "raises ArgumentError if given the name of a non-Class" do
+ -> { Marshal.send(@method, "\x04\bm\vString") }.should.raise(ArgumentError)
+ end
+
+ it "loads an old module" do
+ Marshal.send(@method, "\x04\bM\vKernel").should == Kernel
+ end
+
+ it "raises ArgumentError when end of byte sequence reached before module name end" do
+ Marshal.dump(Kernel).should == "\x04\bm\vKernel"
+
+ -> {
+ Marshal.send(@method, "\x04\bm\vKer")
+ }.should.raise(ArgumentError, "marshal data too short")
+ end
+ end
+
+ describe "for a wrapped C pointer" do
+ it "loads" do
+ class DumpableDir < Dir
+ def _dump_data
+ path
+ end
+ def _load_data path
+ initialize(path)
+ end
+ end
+
+ data = "\x04\bd:\x10DumpableDirI\"\x06.\x06:\x06ET"
+
+ dir = Marshal.send(@method, data)
+ begin
+ dir.path.should == '.'
+ ensure
+ dir.close
+ end
+ end
+
+ it "raises TypeError when the local class is missing _load_data" do
+ class UnloadableDumpableDir < Dir
+ def _dump_data
+ path
+ end
+ # no _load_data
+ end
+
+ data = "\x04\bd:\x1AUnloadableDumpableDirI\"\x06.\x06:\x06ET"
+
+ -> { Marshal.send(@method, data) }.should.raise(TypeError)
+ end
+
+ it "raises ArgumentError when the local class is a regular object" do
+ data = "\004\bd:\020UserDefined\0"
+
+ -> { Marshal.send(@method, data) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "when a class does not exist in the namespace" do
+ before :each do
+ NamespaceTest.send(:const_set, :SameName, Class.new)
+ @data = Marshal.dump(NamespaceTest::SameName.new)
+ NamespaceTest.send(:remove_const, :SameName)
+ end
+
+ it "raises an ArgumentError" do
+ message = "undefined class/module NamespaceTest::SameName"
+ -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, message)
+ end
+ end
+
+ it "raises an ArgumentError with full constant name when the dumped constant is missing" do
+ NamespaceTest.send(:const_set, :KaBoom, Class.new)
+ @data = Marshal.dump(NamespaceTest::KaBoom.new)
+ NamespaceTest.send(:remove_const, :KaBoom)
+
+ -> { Marshal.send(@method, @data) }.should.raise(ArgumentError, /NamespaceTest::KaBoom/)
+ end
+end
diff --git a/spec/ruby/core/matchdata/allocate_spec.rb b/spec/ruby/core/matchdata/allocate_spec.rb
new file mode 100644
index 0000000000..f41e2d5481
--- /dev/null
+++ b/spec/ruby/core/matchdata/allocate_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "MatchData.allocate" do
+ it "is undefined" do
+ # https://bugs.ruby-lang.org/issues/16294
+ -> { MatchData.allocate }.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/matchdata/begin_spec.rb b/spec/ruby/core/matchdata/begin_spec.rb
new file mode 100644
index 0000000000..b4be077ae4
--- /dev/null
+++ b/spec/ruby/core/matchdata/begin_spec.rb
@@ -0,0 +1,132 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+
+describe "MatchData#begin" do
+ context "when passed an integer argument" do
+ it "returns the character offset of the start of the nth element" do
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.begin(0).should == 1
+ match_data.begin(2).should == 2
+ end
+
+ it "returns nil when the nth match isn't found" do
+ match_data = /something is( not)? (right)/.match("something is right")
+ match_data.begin(1).should == nil
+ end
+
+ it "returns the character offset for multi-byte strings" do
+ match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.")
+ match_data.begin(0).should == 1
+ match_data.begin(2).should == 2
+ end
+
+ not_supported_on :opal do
+ it "returns the character offset for multi-byte strings with unicode regexp" do
+ match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.")
+ match_data.begin(0).should == 1
+ match_data.begin(2).should == 2
+ end
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.begin(obj).should == 2
+ end
+
+ it "raises IndexError if index is out of bounds" do
+ match_data = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ match_data.begin(-1)
+ }.should.raise(IndexError, "index -1 out of matches")
+
+ -> {
+ match_data.begin(3)
+ }.should.raise(IndexError, "index 3 out of matches")
+ end
+ end
+
+ context "when passed a String argument" do
+ it "return the character offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.begin("a").should == 1
+ match_data.begin("b").should == 3
+ end
+
+ it "returns the character offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.begin("a").should == 1
+ match_data.begin("b").should == 3
+ end
+
+ not_supported_on :opal do
+ it "returns the character offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.begin("a").should == 1
+ match_data.begin("b").should == 3
+ end
+ end
+
+ it "returns the character offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.begin("a").should == 3
+ end
+
+ it "returns the character offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.begin("æ").should == 1
+ end
+
+ it "raises IndexError if there is no group with the provided name" do
+ match_data = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ match_data.begin("y")
+ }.should.raise(IndexError, "undefined group name reference: y")
+ end
+ end
+
+ context "when passed a Symbol argument" do
+ it "return the character offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.begin(:a).should == 1
+ match_data.begin(:b).should == 3
+ end
+
+ it "returns the character offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.begin(:a).should == 1
+ match_data.begin(:b).should == 3
+ end
+
+ not_supported_on :opal do
+ it "returns the character offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.begin(:a).should == 1
+ match_data.begin(:b).should == 3
+ end
+ end
+
+ it "returns the character offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.begin(:a).should == 3
+ end
+
+ it "returns the character offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.begin(:æ).should == 1
+ end
+
+ it "raises IndexError if there is no group with the provided name" do
+ match_data = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ match_data.begin(:y)
+ }.should.raise(IndexError, "undefined group name reference: y")
+ end
+ end
+end
diff --git a/spec/ruby/core/matchdata/bytebegin_spec.rb b/spec/ruby/core/matchdata/bytebegin_spec.rb
new file mode 100644
index 0000000000..fa44ec3b41
--- /dev/null
+++ b/spec/ruby/core/matchdata/bytebegin_spec.rb
@@ -0,0 +1,132 @@
+require_relative '../../spec_helper'
+
+ruby_version_is "3.4" do
+ describe "MatchData#bytebegin" do
+ context "when passed an integer argument" do
+ it "returns the byte-based offset of the start of the nth element" do
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin(0).should == 1
+ match_data.bytebegin(2).should == 2
+ end
+
+ it "returns nil when the nth match isn't found" do
+ match_data = /something is( not)? (right)/.match("something is right")
+ match_data.bytebegin(1).should == nil
+ end
+
+ it "returns the byte-based offset for multi-byte strings" do
+ match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.")
+ match_data.bytebegin(0).should == 1
+ match_data.bytebegin(2).should == 3
+ end
+
+ not_supported_on :opal do
+ it "returns the byte-based offset for multi-byte strings with unicode regexp" do
+ match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.")
+ match_data.bytebegin(0).should == 1
+ match_data.bytebegin(2).should == 3
+ end
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin(obj).should == 2
+ end
+
+ it "raises IndexError if index is out of bounds" do
+ match_data = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ match_data.bytebegin(-1)
+ }.should.raise(IndexError, "index -1 out of matches")
+
+ -> {
+ match_data.bytebegin(3)
+ }.should.raise(IndexError, "index 3 out of matches")
+ end
+ end
+
+ context "when passed a String argument" do
+ it "return the byte-based offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin("a").should == 1
+ match_data.bytebegin("b").should == 3
+ end
+
+ it "returns the byte-based offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.bytebegin("a").should == 1
+ match_data.bytebegin("b").should == 4
+ end
+
+ not_supported_on :opal do
+ it "returns the byte-based offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.bytebegin("a").should == 1
+ match_data.bytebegin("b").should == 4
+ end
+ end
+
+ it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin("a").should == 3
+ end
+
+ it "returns the byte-based offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin("æ").should == 1
+ end
+
+ it "raises IndexError if there is no group with the provided name" do
+ match_data = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ match_data.bytebegin("y")
+ }.should.raise(IndexError, "undefined group name reference: y")
+ end
+ end
+
+ context "when passed a Symbol argument" do
+ it "return the byte-based offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin(:a).should == 1
+ match_data.bytebegin(:b).should == 3
+ end
+
+ it "returns the byte-based offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.bytebegin(:a).should == 1
+ match_data.bytebegin(:b).should == 4
+ end
+
+ not_supported_on :opal do
+ it "returns the byte-based offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.bytebegin(:a).should == 1
+ match_data.bytebegin(:b).should == 4
+ end
+ end
+
+ it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin(:a).should == 3
+ end
+
+ it "returns the byte-based offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.bytebegin(:æ).should == 1
+ end
+
+ it "raises IndexError if there is no group with the provided name" do
+ match_data = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ match_data.bytebegin(:y)
+ }.should.raise(IndexError, "undefined group name reference: y")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/matchdata/byteend_spec.rb b/spec/ruby/core/matchdata/byteend_spec.rb
new file mode 100644
index 0000000000..e77cbf8b0d
--- /dev/null
+++ b/spec/ruby/core/matchdata/byteend_spec.rb
@@ -0,0 +1,104 @@
+require_relative '../../spec_helper'
+
+ruby_version_is "3.4" do
+ describe "MatchData#byteend" do
+ context "when passed an integer argument" do
+ it "returns the byte-based offset of the end of the nth element" do
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.byteend(0).should == 7
+ match_data.byteend(2).should == 3
+ end
+
+ it "returns nil when the nth match isn't found" do
+ match_data = /something is( not)? (right)/.match("something is right")
+ match_data.byteend(1).should == nil
+ end
+
+ it "returns the byte-based offset for multi-byte strings" do
+ match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.")
+ match_data.byteend(0).should == 8
+ match_data.byteend(2).should == 4
+ end
+
+ not_supported_on :opal do
+ it "returns the byte-based offset for multi-byte strings with unicode regexp" do
+ match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.")
+ match_data.byteend(0).should == 8
+ match_data.byteend(2).should == 4
+ end
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.byteend(obj).should == 3
+ end
+ end
+
+ context "when passed a String argument" do
+ it "return the byte-based offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.byteend("a").should == 2
+ match_data.byteend("b").should == 6
+ end
+
+ it "returns the byte-based offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.byteend("a").should == 3
+ match_data.byteend("b").should == 7
+ end
+
+ not_supported_on :opal do
+ it "returns the byte-based offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.byteend("a").should == 3
+ match_data.byteend("b").should == 7
+ end
+ end
+
+ it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.byteend("a").should == 6
+ end
+
+ it "returns the byte-based offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.byteend("æ").should == 2
+ end
+ end
+
+ context "when passed a Symbol argument" do
+ it "return the byte-based offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.byteend(:a).should == 2
+ match_data.byteend(:b).should == 6
+ end
+
+ it "returns the byte-based offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.byteend(:a).should == 3
+ match_data.byteend(:b).should == 7
+ end
+
+ not_supported_on :opal do
+ it "returns the byte-based offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.byteend(:a).should == 3
+ match_data.byteend(:b).should == 7
+ end
+ end
+
+ it "returns the byte-based offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.byteend(:a).should == 6
+ end
+
+ it "returns the byte-based offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.byteend(:æ).should == 2
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/matchdata/byteoffset_spec.rb b/spec/ruby/core/matchdata/byteoffset_spec.rb
new file mode 100644
index 0000000000..062e84027c
--- /dev/null
+++ b/spec/ruby/core/matchdata/byteoffset_spec.rb
@@ -0,0 +1,93 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#byteoffset" do
+ it "returns beginning and ending byte-based offset of whole matched substring for 0 element" do
+ m = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ m.byteoffset(0).should == [1, 7]
+ end
+
+ it "returns beginning and ending byte-based offset of n-th match, all the subsequent elements are capturing groups" do
+ m = /(.)(.)(\d+)(\d)/.match("THX1138.")
+
+ m.byteoffset(2).should == [2, 3]
+ m.byteoffset(3).should == [3, 6]
+ m.byteoffset(4).should == [6, 7]
+ end
+
+ it "accepts String as a reference to a named capture" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.byteoffset("f").should == [0, 3]
+ m.byteoffset("b").should == [3, 6]
+ end
+
+ it "accepts Symbol as a reference to a named capture" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.byteoffset(:f).should == [0, 3]
+ m.byteoffset(:b).should == [3, 6]
+ end
+
+ it "returns [nil, nil] if a capturing group is optional and doesn't match" do
+ m = /(?<x>q..)?/.match("foobarbaz")
+
+ m.byteoffset("x").should == [nil, nil]
+ m.byteoffset(1).should == [nil, nil]
+ end
+
+ it "returns correct beginning and ending byte-based offset for multi-byte strings" do
+ m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044")
+
+ m.byteoffset(1).should == [3, 6]
+ m.byteoffset(3).should == [6, 9]
+ end
+
+ it "returns [nil, nil] if a capturing group is optional and doesn't match for multi-byte string" do
+ m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044")
+
+ m.byteoffset(2).should == [nil, nil]
+ end
+
+ it "converts argument into integer if is not String nor Symbol" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ obj = Object.new
+ def obj.to_int; 2; end
+
+ m.byteoffset(1r).should == [0, 3]
+ m.byteoffset(1.1).should == [0, 3]
+ m.byteoffset(obj).should == [3, 6]
+ end
+
+ it "raises IndexError if there is no group with the provided name" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.byteoffset("y")
+ }.should.raise(IndexError, "undefined group name reference: y")
+
+ -> {
+ m.byteoffset(:y)
+ }.should.raise(IndexError, "undefined group name reference: y")
+ end
+
+ it "raises IndexError if index is out of bounds" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.byteoffset(-1)
+ }.should.raise(IndexError, "index -1 out of matches")
+
+ -> {
+ m.byteoffset(3)
+ }.should.raise(IndexError, "index 3 out of matches")
+ end
+
+ it "raises TypeError if can't convert argument into Integer" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.byteoffset([])
+ }.should.raise(TypeError, "no implicit conversion of Array into Integer")
+ end
+end
diff --git a/spec/ruby/core/matchdata/captures_spec.rb b/spec/ruby/core/matchdata/captures_spec.rb
new file mode 100644
index 0000000000..f829a25481
--- /dev/null
+++ b/spec/ruby/core/matchdata/captures_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/captures'
+
+describe "MatchData#captures" do
+ it_behaves_like :matchdata_captures, :captures
+end
diff --git a/spec/ruby/core/matchdata/deconstruct_keys_spec.rb b/spec/ruby/core/matchdata/deconstruct_keys_spec.rb
new file mode 100644
index 0000000000..9126d20394
--- /dev/null
+++ b/spec/ruby/core/matchdata/deconstruct_keys_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#deconstruct_keys" do
+ it "returns whole hash for nil as an argument" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.deconstruct_keys(nil).should == { f: "foo", b: "bar" }
+ end
+
+ it "returns only specified keys" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.deconstruct_keys([:f]).should == { f: "foo" }
+ end
+
+ it "requires one argument" do
+ m = /l/.match("l")
+
+ -> {
+ m.deconstruct_keys
+ }.should.raise(ArgumentError, "wrong number of arguments (given 0, expected 1)")
+ end
+
+ it "it raises error when argument is neither nil nor array" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> { m.deconstruct_keys(1) }.should.raise(TypeError, "wrong argument type Integer (expected Array)")
+ -> { m.deconstruct_keys("asd") }.should.raise(TypeError, "wrong argument type String (expected Array)")
+ -> { m.deconstruct_keys(:x) }.should.raise(TypeError, "wrong argument type Symbol (expected Array)")
+ -> { m.deconstruct_keys({}) }.should.raise(TypeError, "wrong argument type Hash (expected Array)")
+ end
+
+ it "returns {} when passed []" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.deconstruct_keys([]).should == {}
+ end
+
+ it "does not accept non-Symbol keys" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.deconstruct_keys(['year', :foo])
+ }.should.raise(TypeError, "wrong argument type String (expected Symbol)")
+ end
+
+ it "process keys till the first non-existing one" do
+ m = /(?<f>foo)(?<b>bar)(?<c>baz)/.match("foobarbaz")
+
+ m.deconstruct_keys([:f, :a, :b]).should == { f: "foo" }
+ end
+
+ it "returns {} when there are no named captured groups at all" do
+ m = /foo.+/.match("foobar")
+
+ m.deconstruct_keys(nil).should == {}
+ end
+
+ it "returns {} when passed more keys than named captured groups" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+ m.deconstruct_keys([:f, :b, :c]).should == {}
+ end
+
+ it "includes non-participating captures as nil for nil argument" do
+ m = "hello".match(/(?<a>hello)(?<b>world)?/)
+ m.deconstruct_keys(nil).should == { a: "hello", b: nil }
+ end
+
+ it "includes non-participating captures as nil for explicit keys" do
+ m = "hello".match(/(?<a>hello)(?<b>world)?/)
+ m.deconstruct_keys([:a, :b]).should == { a: "hello", b: nil }
+ end
+
+ it "does not stop iterating at a non-participating capture" do
+ m = "hello!".match(/(?<a>hello)(?<b>world)?(?<c>!)/)
+ m.deconstruct_keys([:b, :c, :a]).should == { b: nil, c: "!", a: "hello" }
+ end
+end
diff --git a/spec/ruby/core/matchdata/deconstruct_spec.rb b/spec/ruby/core/matchdata/deconstruct_spec.rb
new file mode 100644
index 0000000000..c55095665d
--- /dev/null
+++ b/spec/ruby/core/matchdata/deconstruct_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/captures'
+
+describe "MatchData#deconstruct" do
+ it_behaves_like :matchdata_captures, :deconstruct
+end
diff --git a/spec/ruby/core/matchdata/dup_spec.rb b/spec/ruby/core/matchdata/dup_spec.rb
new file mode 100644
index 0000000000..70877f07eb
--- /dev/null
+++ b/spec/ruby/core/matchdata/dup_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#dup" do
+ it "duplicates the match data" do
+ original = /ll/.match("hello")
+ original.instance_variable_set(:@custom_ivar, 42)
+ duplicate = original.dup
+
+ duplicate.instance_variable_get(:@custom_ivar).should == 42
+ original.regexp.should == duplicate.regexp
+ original.string.should == duplicate.string
+ original.offset(0).should == duplicate.offset(0)
+ end
+end
diff --git a/spec/ruby/core/matchdata/element_reference_spec.rb b/spec/ruby/core/matchdata/element_reference_spec.rb
new file mode 100644
index 0000000000..5509371cd2
--- /dev/null
+++ b/spec/ruby/core/matchdata/element_reference_spec.rb
@@ -0,0 +1,124 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "MatchData#[]" do
+ it "acts as normal array indexing [index]" do
+ md = /(.)(.)(\d+)(\d)/.match("THX1138.")
+
+ md[0].should == 'HX1138'
+ md[1].should == 'H'
+ md[2].should == 'X'
+ md[-3].should == 'X'
+ md[10000].should == nil
+ md[-10000].should == nil
+ end
+
+ it "supports accessors [start, length]" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[1, 2].should == %w|H X|
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[-3, 2].should == %w|X 113|
+
+ # negative index is larger than the number of match values
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[-30, 2].should == nil
+
+ # positive index larger than number of match values
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[5, 2].should == []
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[6, 2].should == nil
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[30, 2].should == nil
+
+ # length argument larger than number of match values is capped to match value length
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3, 10].should == %w|113 8|
+
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3, 0].should == []
+
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3, -1].should == nil
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3, -30].should == nil
+ end
+
+ it "supports ranges [start..end]" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[1..3].should == %w|H X 113|
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3..10].should == %w|113 8|
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[-30..2].should == nil
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3..1].should == []
+ end
+
+ it "supports endless ranges [start..]" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[3..].should == %w|113 8|
+ end
+
+ it "supports beginningless ranges [..end]" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[..1].should == %w|HX1138 H|
+ end
+
+ it "supports beginningless endless ranges [nil..nil]" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.")[nil..nil].should == %w|HX1138 H X 113 8|
+ end
+
+ it "returns instances of String when given a String subclass" do
+ str = MatchDataSpecs::MyString.new("THX1138.")
+ /(.)(.)(\d+)(\d)/.match(str)[0..-1].each { |m| m.should.instance_of?(String) }
+ end
+end
+
+describe "MatchData#[Symbol]" do
+ it "returns the corresponding named match when given a Symbol" do
+ md = 'haystack'.match(/(?<t>t(?<a>ack))/)
+ md[:a].should == 'ack'
+ md[:t].should == 'tack'
+ end
+
+ it "returns the corresponding named match when given a String" do
+ md = 'haystack'.match(/(?<t>t(?<a>ack))/)
+ md['a'].should == 'ack'
+ md['t'].should == 'tack'
+ end
+
+ it "returns the matching version of multiple corresponding named match" do
+ regexp = /(?:
+ A(?<word>\w+)
+ |
+ B(?<word>\w+)
+ )/x
+ md_a = regexp.match("Afoo")
+ md_b = regexp.match("Bfoo")
+
+ md_a[:word].should == "foo"
+ md_b[:word].should == "foo"
+
+ md_a['word'].should == "foo"
+ md_b['word'].should == "foo"
+ end
+
+ it "returns the last match when multiple named matches exist with the same name" do
+ md = /(?<word>hay)(?<word>stack)/.match('haystack')
+ md[:word].should == "stack"
+ md['word'].should == "stack"
+ end
+
+ it "returns nil on non-matching named matches" do
+ regexp = /(?<foo>foo )?(?<bar>bar)/
+ full_match = regexp.match("foo bar")
+ partial_match = regexp.match("bar")
+
+ full_match[:foo].should == "foo "
+ partial_match[:foo].should == nil
+
+ full_match['foo'].should == "foo "
+ partial_match['foo'].should == nil
+ end
+
+ it "raises an IndexError if there is no named match corresponding to the Symbol" do
+ md = 'haystack'.match(/(?<t>t(?<a>ack))/)
+ -> { md[:baz] }.should.raise(IndexError, /baz/)
+ end
+
+ it "raises an IndexError if there is no named match corresponding to the String" do
+ md = 'haystack'.match(/(?<t>t(?<a>ack))/)
+ -> { md['baz'] }.should.raise(IndexError, /baz/)
+ end
+
+ it "returns matches in the String's encoding" do
+ rex = /(?<t>t(?<a>ack))/u
+ md = 'haystack'.dup.force_encoding('euc-jp').match(rex)
+ md[:t].encoding.should == Encoding::EUC_JP
+ end
+end
diff --git a/spec/ruby/core/matchdata/end_spec.rb b/spec/ruby/core/matchdata/end_spec.rb
new file mode 100644
index 0000000000..4fee24a763
--- /dev/null
+++ b/spec/ruby/core/matchdata/end_spec.rb
@@ -0,0 +1,104 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+
+describe "MatchData#end" do
+ context "when passed an integer argument" do
+ it "returns the character offset of the end of the nth element" do
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.end(0).should == 7
+ match_data.end(2).should == 3
+ end
+
+ it "returns nil when the nth match isn't found" do
+ match_data = /something is( not)? (right)/.match("something is right")
+ match_data.end(1).should == nil
+ end
+
+ it "returns the character offset for multi-byte strings" do
+ match_data = /(.)(.)(\d+)(\d)/.match("TñX1138.")
+ match_data.end(0).should == 7
+ match_data.end(2).should == 3
+ end
+
+ not_supported_on :opal do
+ it "returns the character offset for multi-byte strings with unicode regexp" do
+ match_data = /(.)(.)(\d+)(\d)/u.match("TñX1138.")
+ match_data.end(0).should == 7
+ match_data.end(2).should == 3
+ end
+ end
+
+ it "tries to convert the passed argument to an Integer using #to_int" do
+ obj = mock('to_int')
+ obj.should_receive(:to_int).and_return(2)
+
+ match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ match_data.end(obj).should == 3
+ end
+ end
+
+ context "when passed a String argument" do
+ it "return the character offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.end("a").should == 2
+ match_data.end("b").should == 6
+ end
+
+ it "returns the character offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.end("a").should == 2
+ match_data.end("b").should == 6
+ end
+
+ not_supported_on :opal do
+ it "returns the character offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.end("a").should == 2
+ match_data.end("b").should == 6
+ end
+ end
+
+ it "returns the character offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.end("a").should == 6
+ end
+
+ it "returns the character offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.end("æ").should == 2
+ end
+ end
+
+ context "when passed a Symbol argument" do
+ it "return the character offset of the start of the named capture" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.end(:a).should == 2
+ match_data.end(:b).should == 6
+ end
+
+ it "returns the character offset for multi byte strings" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/.match("TñX1138.")
+ match_data.end(:a).should == 2
+ match_data.end(:b).should == 6
+ end
+
+ not_supported_on :opal do
+ it "returns the character offset for multi byte strings with unicode regexp" do
+ match_data = /(?<a>.)(.)(?<b>\d+)(\d)/u.match("TñX1138.")
+ match_data.end(:a).should == 2
+ match_data.end(:b).should == 6
+ end
+ end
+
+ it "returns the character offset for the farthest match when multiple named captures use the same name" do
+ match_data = /(?<a>.)(.)(?<a>\d+)(\d)/.match("THX1138.")
+ match_data.end(:a).should == 6
+ end
+
+ it "returns the character offset for multi-byte names" do
+ match_data = /(?<æ>.)(.)(?<b>\d+)(\d)/.match("THX1138.")
+ match_data.end(:æ).should == 2
+ end
+ end
+end
diff --git a/spec/ruby/core/matchdata/eql_spec.rb b/spec/ruby/core/matchdata/eql_spec.rb
new file mode 100644
index 0000000000..1d9666ebe1
--- /dev/null
+++ b/spec/ruby/core/matchdata/eql_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+
+describe "MatchData#eql?" do
+ it_behaves_like :matchdata_eql, :eql?
+end
diff --git a/spec/ruby/core/matchdata/equal_value_spec.rb b/spec/ruby/core/matchdata/equal_value_spec.rb
new file mode 100644
index 0000000000..a58f1277e4
--- /dev/null
+++ b/spec/ruby/core/matchdata/equal_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+
+describe "MatchData#==" do
+ it_behaves_like :matchdata_eql, :==
+end
diff --git a/spec/ruby/core/matchdata/fixtures/classes.rb b/spec/ruby/core/matchdata/fixtures/classes.rb
new file mode 100644
index 0000000000..54795636e5
--- /dev/null
+++ b/spec/ruby/core/matchdata/fixtures/classes.rb
@@ -0,0 +1,3 @@
+module MatchDataSpecs
+ class MyString < String; end
+end
diff --git a/spec/ruby/core/matchdata/hash_spec.rb b/spec/ruby/core/matchdata/hash_spec.rb
new file mode 100644
index 0000000000..cef18fdd20
--- /dev/null
+++ b/spec/ruby/core/matchdata/hash_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#hash" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/matchdata/inspect_spec.rb b/spec/ruby/core/matchdata/inspect_spec.rb
new file mode 100644
index 0000000000..cacbe10c5d
--- /dev/null
+++ b/spec/ruby/core/matchdata/inspect_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#inspect" do
+ before :each do
+ @match_data = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ end
+
+ it "returns a String" do
+ @match_data.inspect.should.is_a?(String)
+ end
+
+ it "returns a human readable representation that contains entire matched string and the captures" do
+ # yeah, hardcoding the inspect output is not ideal, but in this case
+ # it makes perfect sense. See JRUBY-4558 for example.
+ @match_data.inspect.should == '#<MatchData "HX1138" 1:"H" 2:"X" 3:"113" 4:"8">'
+ end
+
+ it "returns a human readable representation of named captures" do
+ match_data = "abc def ghi".match(/(?<first>\w+)\s+(?<last>\w+)\s+(\w+)/)
+
+ match_data.inspect.should == '#<MatchData "abc def ghi" first:"abc" last:"def">'
+ end
+end
diff --git a/spec/ruby/core/matchdata/integer_at_spec.rb b/spec/ruby/core/matchdata/integer_at_spec.rb
new file mode 100644
index 0000000000..65f73a7bee
--- /dev/null
+++ b/spec/ruby/core/matchdata/integer_at_spec.rb
@@ -0,0 +1,38 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+
+ruby_version_is "4.1" do
+ describe "MatchData#integer_at" do
+ it "converts the corresponding match to an Integer and returns it when given an Integer" do
+ md = /(\d{4})(\d{2})(\d{2})/.match("20260308")
+ md.integer_at(0).should == 20260308
+ md.integer_at(1).should == 2026
+ md.integer_at(2).should == 3
+ end
+
+ it "returns nil on non-matching index matches" do
+ md = /\b(\d)?\b/.match("THX1138.")
+ md.integer_at(1).should == nil
+ end
+
+ it "returns nil on non-integer matches" do
+ md = /(\w)?/.match("THX1138.")
+ md.integer_at(1).should == nil
+ end
+
+ it "converts the match to an Integer in the given base" do
+ md = /\w+/.match("0c")
+ md.integer_at(0).should == 0
+ md.integer_at(0, 16).should == 12
+ end
+
+ it "converts the match to an Integer in the prefix when given base is zero" do
+ /\w+/.match("010").integer_at(0, 0).should == 010
+ /\w+/.match("0x10").integer_at(0, 0).should == 0x10
+ /\w+/.match("0d10").integer_at(0, 0).should == 0d10
+ /\w+/.match("0o10").integer_at(0, 0).should == 0o10
+ /\w+/.match("0b10").integer_at(0, 0).should == 0b10
+ end
+ end
+end
diff --git a/spec/ruby/core/matchdata/length_spec.rb b/spec/ruby/core/matchdata/length_spec.rb
new file mode 100644
index 0000000000..39df36df4b
--- /dev/null
+++ b/spec/ruby/core/matchdata/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "MatchData#length" do
+ it_behaves_like :matchdata_length, :length
+end
diff --git a/spec/ruby/core/matchdata/match_length_spec.rb b/spec/ruby/core/matchdata/match_length_spec.rb
new file mode 100644
index 0000000000..824f94a397
--- /dev/null
+++ b/spec/ruby/core/matchdata/match_length_spec.rb
@@ -0,0 +1,32 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+
+describe "MatchData#match_length" do
+ it "returns the length of the corresponding match when given an Integer" do
+ md = /(.)(.)(\d+)(\d)/.match("THX1138.")
+
+ md.match_length(0).should == 6
+ md.match_length(1).should == 1
+ md.match_length(2).should == 1
+ md.match_length(3).should == 3
+ md.match_length(4).should == 1
+ end
+
+ it "returns nil on non-matching index matches" do
+ md = /\d+(\w)?/.match("THX1138.")
+ md.match_length(1).should == nil
+ end
+
+ it "returns the length of the corresponding named match when given a Symbol" do
+ md = 'haystack'.match(/(?<t>t(?<a>ack))/)
+ md.match_length(:a).should == 3
+ md.match_length(:t).should == 4
+ end
+
+ it "returns nil on non-matching index matches" do
+ md = 'haystack'.match(/(?<t>t)(?<a>all)?/)
+ md.match_length(:t).should == 1
+ md.match_length(:a).should == nil
+ end
+end
diff --git a/spec/ruby/core/matchdata/match_spec.rb b/spec/ruby/core/matchdata/match_spec.rb
new file mode 100644
index 0000000000..a16914ea15
--- /dev/null
+++ b/spec/ruby/core/matchdata/match_spec.rb
@@ -0,0 +1,32 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+
+describe "MatchData#match" do
+ it "returns the corresponding match when given an Integer" do
+ md = /(.)(.)(\d+)(\d)/.match("THX1138.")
+
+ md.match(0).should == 'HX1138'
+ md.match(1).should == 'H'
+ md.match(2).should == 'X'
+ md.match(3).should == '113'
+ md.match(4).should == '8'
+ end
+
+ it "returns nil on non-matching index matches" do
+ md = /\d+(\w)?/.match("THX1138.")
+ md.match(1).should == nil
+ end
+
+ it "returns the corresponding named match when given a Symbol" do
+ md = 'haystack'.match(/(?<t>t(?<a>ack))/)
+ md.match(:a).should == 'ack'
+ md.match(:t).should == 'tack'
+ end
+
+ it "returns nil on non-matching index matches" do
+ md = 'haystack'.match(/(?<t>t)(?<a>all)?/)
+ md.match(:t).should == 't'
+ md.match(:a).should == nil
+ end
+end
diff --git a/spec/ruby/core/matchdata/named_captures_spec.rb b/spec/ruby/core/matchdata/named_captures_spec.rb
new file mode 100644
index 0000000000..10b1f884d6
--- /dev/null
+++ b/spec/ruby/core/matchdata/named_captures_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+describe 'MatchData#named_captures' do
+ it 'returns a Hash that has captured name and the matched string pairs' do
+ /(?<a>.)(?<b>.)?/.match('0').named_captures.should == { 'a' => '0', 'b' => nil }
+ end
+
+ it 'prefers later captures' do
+ /\A(?<a>.)(?<b>.)(?<b>.)(?<a>.)\z/.match('0123').named_captures.should == { 'a' => '3', 'b' => '2' }
+ end
+
+ it 'returns the latest matched capture, even if a later one that does not match exists' do
+ /\A(?<a>.)(?<b>.)(?<b>.)(?<a>.)?\z/.match('012').named_captures.should == { 'a' => '0', 'b' => '2' }
+ end
+
+ it 'returns a Hash with Symbol keys when symbolize_names is provided a true value' do
+ /(?<a>.)(?<b>.)?/.match('0').named_captures(symbolize_names: true).should == { a: '0', b: nil }
+ /(?<a>.)(?<b>.)?/.match('0').named_captures(symbolize_names: "truly").should == { a: '0', b: nil }
+ end
+
+ it 'returns a Hash with String keys when symbolize_names is provided a false value' do
+ /(?<a>.)(?<b>.)?/.match('02').named_captures(symbolize_names: false).should == { 'a' => '0', 'b' => '2' }
+ /(?<a>.)(?<b>.)?/.match('02').named_captures(symbolize_names: nil).should == { 'a' => '0', 'b' => '2' }
+ end
+end
diff --git a/spec/ruby/core/matchdata/names_spec.rb b/spec/ruby/core/matchdata/names_spec.rb
new file mode 100644
index 0000000000..dca15985b0
--- /dev/null
+++ b/spec/ruby/core/matchdata/names_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#names" do
+ it "returns an Array" do
+ md = 'haystack'.match(/(?<yellow>hay)/)
+ md.names.should.instance_of?(Array)
+ end
+
+ it "sets each element to a String" do
+ 'haystack'.match(/(?<yellow>hay)/).names.all? do |e|
+ e.should.instance_of?(String)
+ end
+ end
+
+ it "returns the names of the named capture groups" do
+ md = 'haystack'.match(/(?<yellow>hay).(?<pin>tack)/)
+ md.names.should == ['yellow', 'pin']
+ end
+
+ it "returns [] if there were no named captures" do
+ 'haystack'.match(/(hay).(tack)/).names.should == []
+ end
+
+ it "returns each name only once" do
+ md = 'haystack'.match(/(?<hay>hay)(?<dot>.)(?<hay>tack)/)
+ md.names.should == ['hay', 'dot']
+ end
+
+ it "equals Regexp#names" do
+ r = /(?<hay>hay)(?<dot>.)(?<hay>tack)/
+ 'haystack'.match(r).names.should == r.names
+ end
+end
diff --git a/spec/ruby/core/matchdata/offset_spec.rb b/spec/ruby/core/matchdata/offset_spec.rb
new file mode 100644
index 0000000000..5a923d6ce0
--- /dev/null
+++ b/spec/ruby/core/matchdata/offset_spec.rb
@@ -0,0 +1,102 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#offset" do
+ it "returns beginning and ending character offset of whole matched substring for 0 element" do
+ m = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ m.offset(0).should == [1, 7]
+ end
+
+ it "returns beginning and ending character offset of n-th match, all the subsequent elements are capturing groups" do
+ m = /(.)(.)(\d+)(\d)/.match("THX1138.")
+
+ m.offset(2).should == [2, 3]
+ m.offset(3).should == [3, 6]
+ m.offset(4).should == [6, 7]
+ end
+
+ it "accepts String as a reference to a named capture" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.offset("f").should == [0, 3]
+ m.offset("b").should == [3, 6]
+ end
+
+ it "accepts Symbol as a reference to a named capture" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ m.offset(:f).should == [0, 3]
+ m.offset(:b).should == [3, 6]
+ end
+
+ it "returns [nil, nil] if a capturing group is optional and doesn't match" do
+ m = /(?<x>q..)?/.match("foobarbaz")
+
+ m.offset("x").should == [nil, nil]
+ m.offset(1).should == [nil, nil]
+ end
+
+ it "returns correct beginning and ending character offset for multi-byte strings" do
+ m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044")
+
+ m.offset(1).should == [1, 2]
+ m.offset(3).should == [2, 3]
+ end
+
+ not_supported_on :opal do
+ it "returns correct character offset for multi-byte strings with unicode regexp" do
+ m = /\A\u3042(.)(.)?(.)\z/u.match("\u3042\u3043\u3044")
+
+ m.offset(1).should == [1, 2]
+ m.offset(3).should == [2, 3]
+ end
+ end
+
+ it "returns [nil, nil] if a capturing group is optional and doesn't match for multi-byte string" do
+ m = /\A\u3042(.)(.)?(.)\z/.match("\u3042\u3043\u3044")
+
+ m.offset(2).should == [nil, nil]
+ end
+
+ it "converts argument into integer if is not String nor Symbol" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ obj = Object.new
+ def obj.to_int; 2; end
+
+ m.offset(1r).should == [0, 3]
+ m.offset(1.1).should == [0, 3]
+ m.offset(obj).should == [3, 6]
+ end
+
+ it "raises IndexError if there is no group with the provided name" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.offset("y")
+ }.should.raise(IndexError, "undefined group name reference: y")
+
+ -> {
+ m.offset(:y)
+ }.should.raise(IndexError, "undefined group name reference: y")
+ end
+
+ it "raises IndexError if index is out of bounds" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.offset(-1)
+ }.should.raise(IndexError, "index -1 out of matches")
+
+ -> {
+ m.offset(3)
+ }.should.raise(IndexError, "index 3 out of matches")
+ end
+
+ it "raises TypeError if can't convert argument into Integer" do
+ m = /(?<f>foo)(?<b>bar)/.match("foobar")
+
+ -> {
+ m.offset([])
+ }.should.raise(TypeError, "no implicit conversion of Array into Integer")
+ end
+end
diff --git a/spec/ruby/core/matchdata/post_match_spec.rb b/spec/ruby/core/matchdata/post_match_spec.rb
new file mode 100644
index 0000000000..b50d637124
--- /dev/null
+++ b/spec/ruby/core/matchdata/post_match_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "MatchData#post_match" do
+ it "returns the string after the match equiv. special var $'" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").post_match.should == ': The Movie'
+ $'.should == ': The Movie'
+ end
+
+ it "sets the encoding to the encoding of the source String" do
+ str = "abc".dup.force_encoding Encoding::EUC_JP
+ str.match(/b/).post_match.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "sets an empty result to the encoding of the source String" do
+ str = "abc".dup.force_encoding Encoding::ISO_8859_1
+ str.match(/c/).post_match.encoding.should.equal?(Encoding::ISO_8859_1)
+ end
+
+ it "returns an instance of String when given a String subclass" do
+ str = MatchDataSpecs::MyString.new("THX1138: The Movie")
+ /(.)(.)(\d+)(\d)/.match(str).post_match.should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/matchdata/pre_match_spec.rb b/spec/ruby/core/matchdata/pre_match_spec.rb
new file mode 100644
index 0000000000..106612a4f7
--- /dev/null
+++ b/spec/ruby/core/matchdata/pre_match_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "MatchData#pre_match" do
+ it "returns the string before the match, equiv. special var $`" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").pre_match.should == 'T'
+ $`.should == 'T'
+ end
+
+ it "sets the encoding to the encoding of the source String" do
+ str = "abc".dup.force_encoding Encoding::EUC_JP
+ str.match(/b/).pre_match.encoding.should.equal?(Encoding::EUC_JP)
+ end
+
+ it "sets an empty result to the encoding of the source String" do
+ str = "abc".dup.force_encoding Encoding::ISO_8859_1
+ str.match(/a/).pre_match.encoding.should.equal?(Encoding::ISO_8859_1)
+ end
+
+ it "returns an instance of String when given a String subclass" do
+ str = MatchDataSpecs::MyString.new("THX1138: The Movie")
+ /(.)(.)(\d+)(\d)/.match(str).pre_match.should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/matchdata/regexp_spec.rb b/spec/ruby/core/matchdata/regexp_spec.rb
new file mode 100644
index 0000000000..7dcb0e62db
--- /dev/null
+++ b/spec/ruby/core/matchdata/regexp_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#regexp" do
+ it "returns a Regexp object" do
+ m = 'haystack'.match(/hay/)
+ m.regexp.should.instance_of?(Regexp)
+ end
+
+ it "returns the pattern used in the match" do
+ m = 'haystack'.match(/hay/)
+ m.regexp.should == /hay/
+ end
+
+ it "returns the same Regexp used to match" do
+ r = /hay/
+ m = 'haystack'.match(r)
+ m.regexp.object_id.should == r.object_id
+ end
+
+ it "returns a Regexp for the result of gsub(String)" do
+ 'he[[o'.gsub('[', ']')
+ $~.regexp.should == /\[/
+ end
+end
diff --git a/spec/ruby/core/matchdata/shared/captures.rb b/spec/ruby/core/matchdata/shared/captures.rb
new file mode 100644
index 0000000000..de5870f543
--- /dev/null
+++ b/spec/ruby/core/matchdata/shared/captures.rb
@@ -0,0 +1,13 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :matchdata_captures, shared: true do
+ it "returns an array of the match captures" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.").send(@method).should == ["H","X","113","8"]
+ end
+
+ it "returns instances of String when given a String subclass" do
+ str = MatchDataSpecs::MyString.new("THX1138: The Movie")
+ /(.)(.)(\d+)(\d)/.match(str).send(@method).each { |c| c.should.instance_of?(String) }
+ end
+end
diff --git a/spec/ruby/core/matchdata/shared/eql.rb b/spec/ruby/core/matchdata/shared/eql.rb
new file mode 100644
index 0000000000..e4bb8797b7
--- /dev/null
+++ b/spec/ruby/core/matchdata/shared/eql.rb
@@ -0,0 +1,26 @@
+require_relative '../../../spec_helper'
+
+describe :matchdata_eql, shared: true do
+ it "returns true if both operands have equal target strings, patterns, and match positions" do
+ a = 'haystack'.match(/hay/)
+ b = 'haystack'.match(/hay/)
+ a.send(@method, b).should == true
+ end
+
+ it "returns false if the operands have different target strings" do
+ a = 'hay'.match(/hay/)
+ b = 'haystack'.match(/hay/)
+ a.send(@method, b).should == false
+ end
+
+ it "returns false if the operands have different patterns" do
+ a = 'haystack'.match(/h.y/)
+ b = 'haystack'.match(/hay/)
+ a.send(@method, b).should == false
+ end
+
+ it "returns false if the argument is not a MatchData object" do
+ a = 'haystack'.match(/hay/)
+ a.send(@method, Object.new).should == false
+ end
+end
diff --git a/spec/ruby/core/matchdata/shared/length.rb b/spec/ruby/core/matchdata/shared/length.rb
new file mode 100644
index 0000000000..6312a7ed4c
--- /dev/null
+++ b/spec/ruby/core/matchdata/shared/length.rb
@@ -0,0 +1,5 @@
+describe :matchdata_length, shared: true do
+ it "length should return the number of elements in the match array" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.").send(@method).should == 5
+ end
+end
diff --git a/spec/ruby/core/matchdata/size_spec.rb b/spec/ruby/core/matchdata/size_spec.rb
new file mode 100644
index 0000000000..b4965db3b8
--- /dev/null
+++ b/spec/ruby/core/matchdata/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "MatchData#size" do
+ it_behaves_like :matchdata_length, :size
+end
diff --git a/spec/ruby/core/matchdata/string_spec.rb b/spec/ruby/core/matchdata/string_spec.rb
new file mode 100644
index 0000000000..50bbb5a64f
--- /dev/null
+++ b/spec/ruby/core/matchdata/string_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#string" do
+ it "returns a copy of the match string" do
+ str = /(.)(.)(\d+)(\d)/.match("THX1138.").string
+ str.should == "THX1138."
+ end
+
+ it "returns a frozen copy of the match string" do
+ str = /(.)(.)(\d+)(\d)/.match("THX1138.").string
+ str.should == "THX1138."
+ str.should.frozen?
+ end
+
+ it "returns the same frozen string for every call" do
+ md = /(.)(.)(\d+)(\d)/.match("THX1138.")
+ md.string.should.equal?(md.string)
+ end
+
+ it "returns a frozen copy of the matched string for gsub!(String)" do
+ s = +'he[[o'
+ s.gsub!('[', ']')
+ $~.string.should == 'he[[o'
+ $~.string.should.frozen?
+ end
+end
diff --git a/spec/ruby/core/matchdata/to_a_spec.rb b/spec/ruby/core/matchdata/to_a_spec.rb
new file mode 100644
index 0000000000..c77fc4ab37
--- /dev/null
+++ b/spec/ruby/core/matchdata/to_a_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "MatchData#to_a" do
+ it "returns an array of matches" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.").to_a.should == ["HX1138", "H", "X", "113", "8"]
+ end
+
+ it "returns instances of String when given a String subclass" do
+ str = MatchDataSpecs::MyString.new("THX1138.")
+ /(.)(.)(\d+)(\d)/.match(str)[0..-1].to_a.each { |m| m.should.instance_of?(String) }
+ end
+end
diff --git a/spec/ruby/core/matchdata/to_s_spec.rb b/spec/ruby/core/matchdata/to_s_spec.rb
new file mode 100644
index 0000000000..cbcc5e8a21
--- /dev/null
+++ b/spec/ruby/core/matchdata/to_s_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "MatchData#to_s" do
+ it "returns the entire matched string" do
+ /(.)(.)(\d+)(\d)/.match("THX1138.").to_s.should == "HX1138"
+ end
+
+ it "returns an instance of String when given a String subclass" do
+ str = MatchDataSpecs::MyString.new("THX1138.")
+ /(.)(.)(\d+)(\d)/.match(str).to_s.should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/matchdata/values_at_spec.rb b/spec/ruby/core/matchdata/values_at_spec.rb
new file mode 100644
index 0000000000..ba5b0e662c
--- /dev/null
+++ b/spec/ruby/core/matchdata/values_at_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+
+describe "MatchData#values_at" do
+ # Should be synchronized with core/array/values_at_spec.rb and core/struct/values_at_spec.rb
+ #
+ # /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").to_a # => ["HX1138", "H", "X", "113", "8"]
+
+ context "when passed a list of Integers" do
+ it "returns an array containing each value given by one of integers" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0, 2, -2).should == ["HX1138", "X", "113"]
+ end
+
+ it "returns nil value for any integer that is out of range" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(5).should == [nil]
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(-6).should == [nil]
+ end
+ end
+
+ context "when passed an integer Range" do
+ it "returns an array containing each value given by the elements of the range" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..2).should == ["HX1138", "H", "X"]
+ end
+
+ it "fills with nil values for range elements larger than the captured values number" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..5).should == ["HX1138", "H", "X", "113", "8", nil]
+ end
+
+ it "raises RangeError if any element of the range is negative and out of range" do
+ -> { /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(-6..3) }.should.raise(RangeError, "-6..3 out of range")
+ end
+
+ it "supports endless Range" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(0..).should == ["HX1138", "H", "X", "113", "8"]
+ end
+
+ it "supports beginningless Range" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(..2).should == ["HX1138", "H", "X"]
+ end
+
+ it "returns an empty Array when Range is empty" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(2..0).should == []
+ end
+ end
+
+ context "when passed names" do
+ it 'slices captures with the given names' do
+ /(?<a>.)(?<b>.)(?<c>.)/.match('012').values_at(:c, :a).should == ['2', '0']
+ end
+
+ it 'slices captures with the given String names' do
+ /(?<a>.)(?<b>.)(?<c>.)/.match('012').values_at('c', 'a').should == ['2', '0']
+ end
+ end
+
+ it "supports multiple integer Ranges" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(1..2, 2..3).should == ["H", "X", "X", "113"]
+ end
+
+ it "supports mixing integer Ranges and Integers" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(1..2, 4).should == ["H", "X", "8"]
+ end
+
+ it 'supports mixing of names and indices' do
+ /\A(?<a>.)(?<b>.)\z/.match('01').values_at(0, 1, 2, :a, :b).should == ['01', '0', '1', '0', '1']
+ end
+
+ it "returns a new empty Array if no arguments given" do
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at().should == []
+ end
+
+ it "fails when passed arguments of unsupported types" do
+ -> {
+ /(.)(.)(\d+)(\d)/.match("THX1138: The Movie").values_at(Object.new)
+ }.should.raise(TypeError, "no implicit conversion of Object into Integer")
+ end
+end
diff --git a/spec/ruby/core/math/acos_spec.rb b/spec/ruby/core/math/acos_spec.rb
new file mode 100644
index 0000000000..4649dc527c
--- /dev/null
+++ b/spec/ruby/core/math/acos_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# arccosine : (-1.0, 1.0) --> (0, PI)
+describe "Math.acos" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "returns a float" do
+ Math.acos(1).should.is_a?(Float )
+ end
+
+ it "returns the arccosine of the argument" do
+ Math.acos(1).should be_close(0.0, TOLERANCE)
+ Math.acos(0).should be_close(1.5707963267949, TOLERANCE)
+ Math.acos(-1).should be_close(Math::PI,TOLERANCE)
+ Math.acos(0.25).should be_close(1.31811607165282, TOLERANCE)
+ Math.acos(0.50).should be_close(1.0471975511966 , TOLERANCE)
+ Math.acos(0.75).should be_close(0.722734247813416, TOLERANCE)
+ end
+
+ it "raises an Math::DomainError if the argument is greater than 1.0" do
+ -> { Math.acos(1.0001) }.should.raise(Math::DomainError)
+ end
+
+ it "raises an Math::DomainError if the argument is less than -1.0" do
+ -> { Math.acos(-1.0001) }.should.raise(Math::DomainError)
+ end
+
+ it "raises a TypeError if the string argument cannot be coerced with Float()" do
+ -> { Math.acos("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.acos(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.acos(MathSpecs::UserClass.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.acos(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.acos(MathSpecs::Float.new(0.5)).should be_close(Math.acos(0.5), TOLERANCE)
+ end
+end
+
+describe "Math#acos" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:acos, 0).should be_close(1.5707963267949, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/acosh_spec.rb b/spec/ruby/core/math/acosh_spec.rb
new file mode 100644
index 0000000000..ccacda37e0
--- /dev/null
+++ b/spec/ruby/core/math/acosh_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.acosh" do
+ it "returns a float" do
+ Math.acosh(1.0).should.is_a?(Float)
+ end
+
+ it "returns the principle value of the inverse hyperbolic cosine of the argument" do
+ Math.acosh(14.2).should be_close(3.345146999647, TOLERANCE)
+ Math.acosh(1.0).should be_close(0.0, TOLERANCE)
+ end
+
+ it "raises Math::DomainError if the passed argument is less than -1.0 or greater than 1.0" do
+ -> { Math.acosh(1.0 - TOLERANCE) }.should.raise(Math::DomainError)
+ -> { Math.acosh(0) }.should.raise(Math::DomainError)
+ -> { Math.acosh(-1.0) }.should.raise(Math::DomainError)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.acosh("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.acosh(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.acosh(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.acosh(MathSpecs::Float.new).should == 0.0
+ end
+end
+
+describe "Math#acosh" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:acosh, 1.0).should be_close(0.0, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/asin_spec.rb b/spec/ruby/core/math/asin_spec.rb
new file mode 100644
index 0000000000..1386bccc06
--- /dev/null
+++ b/spec/ruby/core/math/asin_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# arcsine : (-1.0, 1.0) --> (-PI/2, PI/2)
+describe "Math.asin" do
+ it "returns a float" do
+ Math.asin(1).should.is_a?(Float)
+ end
+
+ it "returns the arcsine of the argument" do
+ Math.asin(1).should be_close(Math::PI/2, TOLERANCE)
+ Math.asin(0).should be_close(0.0, TOLERANCE)
+ Math.asin(-1).should be_close(-Math::PI/2, TOLERANCE)
+ Math.asin(0.25).should be_close(0.252680255142079, TOLERANCE)
+ Math.asin(0.50).should be_close(0.523598775598299, TOLERANCE)
+ Math.asin(0.75).should be_close(0.8480620789814816,TOLERANCE)
+ end
+
+ it "raises an Math::DomainError if the argument is greater than 1.0" do
+ -> { Math.asin(1.0001) }.should.raise( Math::DomainError)
+ end
+
+ it "raises an Math::DomainError if the argument is less than -1.0" do
+ -> { Math.asin(-1.0001) }.should.raise( Math::DomainError)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.asin("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.asin(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.asin(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.asin(MathSpecs::Float.new).should be_close(1.5707963267949, TOLERANCE)
+ end
+end
+
+describe "Math#asin" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:asin, 0.5).should be_close(0.523598775598299, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/asinh_spec.rb b/spec/ruby/core/math/asinh_spec.rb
new file mode 100644
index 0000000000..8aa019f05f
--- /dev/null
+++ b/spec/ruby/core/math/asinh_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.asinh" do
+ it "returns a float" do
+ Math.asinh(1.5).should.is_a?(Float)
+ end
+
+ it "returns the inverse hyperbolic sin of the argument" do
+ Math.asinh(1.5).should be_close(1.19476321728711, TOLERANCE)
+ Math.asinh(-2.97).should be_close(-1.8089166921397, TOLERANCE)
+ Math.asinh(0.0).should == 0.0
+ Math.asinh(-0.0).should == -0.0
+ Math.asinh(1.05367e-08).should be_close(1.05367e-08, TOLERANCE)
+ Math.asinh(-1.05367e-08).should be_close(-1.05367e-08, TOLERANCE)
+ # Default tolerance does not scale right for these...
+ #Math.asinh(94906265.62).should be_close(19.0615, TOLERANCE)
+ #Math.asinh(-94906265.62).should be_close(-19.0615, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.asinh("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.asinh(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.asinh(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.asinh(MathSpecs::Float.new).should be_close(0.881373587019543, TOLERANCE)
+ end
+end
+
+describe "Math#asinh" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:asinh, 19.275).should be_close(3.65262832292466, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/atan2_spec.rb b/spec/ruby/core/math/atan2_spec.rb
new file mode 100644
index 0000000000..1f1de506c5
--- /dev/null
+++ b/spec/ruby/core/math/atan2_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.atan2" do
+ it "returns a float" do
+ Math.atan2(1.2, 0.5).should.is_a?(Float)
+ end
+
+ it "returns the arc tangent of y, x" do
+ Math.atan2(4.2, 0.3).should be_close(1.49948886200961, TOLERANCE)
+ Math.atan2(0.0, 1.0).should be_close(0.0, TOLERANCE)
+ Math.atan2(-9.1, 3.2).should be_close(-1.23265379809025, TOLERANCE)
+ Math.atan2(7.22, -3.3).should be_close(1.99950888779256, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.atan2(1.0, "test") }.should.raise(TypeError)
+ -> { Math.atan2("test", 0.0) }.should.raise(TypeError)
+ -> { Math.atan2("test", "this") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.atan2(nil, 1.0) }.should.raise(TypeError)
+ -> { Math.atan2(-1.0, nil) }.should.raise(TypeError)
+ -> { Math.atan2(nil, nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.atan2(MathSpecs::Float.new, MathSpecs::Float.new).should be_close(0.785398163397448, TOLERANCE)
+ end
+
+ it "returns positive zero when passed 0.0, 0.0" do
+ Math.atan2(0.0, 0.0).should be_positive_zero
+ end
+
+ it "returns negative zero when passed -0.0, 0.0" do
+ Math.atan2(-0.0, 0.0).should be_negative_zero
+ end
+
+ it "returns Pi when passed 0.0, -0.0" do
+ Math.atan2(0.0, -0.0).should == Math::PI
+ end
+
+ it "returns -Pi when passed -0.0, -0.0" do
+ Math.atan2(-0.0, -0.0).should == -Math::PI
+ end
+
+end
+
+describe "Math#atan2" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:atan2, 1.1, 2.2).should be_close(0.463647609000806, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/atan_spec.rb b/spec/ruby/core/math/atan_spec.rb
new file mode 100644
index 0000000000..07d04cdf82
--- /dev/null
+++ b/spec/ruby/core/math/atan_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# arctangent : (-Inf, Inf) --> (-PI/2, PI/2)
+describe "Math.atan" do
+ it "returns a float" do
+ Math.atan(1).should.is_a?(Float)
+ end
+
+ it "returns the arctangent of the argument" do
+ Math.atan(1).should be_close(Math::PI/4, TOLERANCE)
+ Math.atan(0).should be_close(0.0, TOLERANCE)
+ Math.atan(-1).should be_close(-Math::PI/4, TOLERANCE)
+ Math.atan(0.25).should be_close(0.244978663126864, TOLERANCE)
+ Math.atan(0.50).should be_close(0.463647609000806, TOLERANCE)
+ Math.atan(0.75).should be_close(0.643501108793284, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.atan("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.atan(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.atan(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.atan(MathSpecs::Float.new).should be_close(0.785398163397448, TOLERANCE)
+ end
+end
+
+describe "Math#atan" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:atan, 3.1415).should be_close(1.2626187313511, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/atanh_spec.rb b/spec/ruby/core/math/atanh_spec.rb
new file mode 100644
index 0000000000..edcb8f2e52
--- /dev/null
+++ b/spec/ruby/core/math/atanh_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/atanh'
+
+describe "Math.atanh" do
+ it_behaves_like :math_atanh_base, :atanh, Math
+ it_behaves_like :math_atanh_no_complex, :atanh, Math
+end
+
+describe "Math#atanh" do
+ it_behaves_like :math_atanh_private, :atanh
+ it_behaves_like :math_atanh_base, :atanh, IncludesMath.new
+ it_behaves_like :math_atanh_no_complex, :atanh, IncludesMath.new
+end
diff --git a/spec/ruby/core/math/cbrt_spec.rb b/spec/ruby/core/math/cbrt_spec.rb
new file mode 100644
index 0000000000..4e2383043b
--- /dev/null
+++ b/spec/ruby/core/math/cbrt_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.cbrt" do
+ it "returns a float" do
+ Math.cbrt(1).should.instance_of?(Float)
+ end
+
+ it "returns the cubic root of the argument" do
+ Math.cbrt(1).should == 1.0
+ Math.cbrt(8.0).should == 2.0
+ Math.cbrt(-8.0).should == -2.0
+ Math.cbrt(3).should be_close(1.44224957030741, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.cbrt("foobar") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.cbrt(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.cbrt(MathSpecs::Float.new).should be_close(1.0, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/constants_spec.rb b/spec/ruby/core/math/constants_spec.rb
new file mode 100644
index 0000000000..b500b21a79
--- /dev/null
+++ b/spec/ruby/core/math/constants_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math::PI" do
+ it "approximates the value of pi" do
+ Math::PI.should be_close(3.14159_26535_89793_23846, TOLERANCE)
+ end
+
+ it "is accessible to a class that includes Math" do
+ IncludesMath::PI.should == Math::PI
+ end
+end
+
+describe "Math::E" do
+ it "approximates the value of Napier's constant" do
+ Math::E.should be_close(2.71828_18284_59045_23536, TOLERANCE)
+ end
+
+ it "is accessible to a class that includes Math" do
+ IncludesMath::E.should == Math::E
+ end
+end
diff --git a/spec/ruby/core/math/cos_spec.rb b/spec/ruby/core/math/cos_spec.rb
new file mode 100644
index 0000000000..e8602cde3c
--- /dev/null
+++ b/spec/ruby/core/math/cos_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# cosine : (-Inf, Inf) --> (-1.0, 1.0)
+describe "Math.cos" do
+ it "returns a float" do
+ Math.cos(Math::PI).should.is_a?(Float)
+ end
+
+ it "returns the cosine of the argument expressed in radians" do
+ Math.cos(Math::PI).should be_close(-1.0, TOLERANCE)
+ Math.cos(0).should be_close(1.0, TOLERANCE)
+ Math.cos(Math::PI/2).should be_close(0.0, TOLERANCE)
+ Math.cos(3*Math::PI/2).should be_close(0.0, TOLERANCE)
+ Math.cos(2*Math::PI).should be_close(1.0, TOLERANCE)
+ end
+
+ it "raises a TypeError unless the argument is Numeric and has #to_f" do
+ -> { Math.cos("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.cos(nan_value).nan?.should == true
+ end
+
+ describe "coerces its argument with #to_f" do
+ it "coerces its argument with #to_f" do
+ f = mock_numeric('8.2')
+ f.should_receive(:to_f).and_return(8.2)
+ Math.cos(f).should == Math.cos(8.2)
+ end
+
+ it "raises a TypeError if the given argument can't be converted to a Float" do
+ -> { Math.cos(nil) }.should.raise(TypeError)
+ -> { Math.cos(:abc) }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a Float" do
+ object = mock_numeric('mock-float')
+ object.should_receive(:to_f).and_raise(NoMethodError)
+ -> { Math.cos(object) }.should.raise(NoMethodError)
+ end
+ end
+end
+
+describe "Math#cos" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:cos, 3.1415).should be_close(-0.999999995707656, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/cosh_spec.rb b/spec/ruby/core/math/cosh_spec.rb
new file mode 100644
index 0000000000..2093d8a74a
--- /dev/null
+++ b/spec/ruby/core/math/cosh_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.cosh" do
+ it "returns a float" do
+ Math.cosh(1.0).should.is_a?(Float)
+ end
+
+ it "returns the hyperbolic cosine of the argument" do
+ Math.cosh(0.0).should == 1.0
+ Math.cosh(-0.0).should == 1.0
+ Math.cosh(1.5).should be_close(2.35240961524325, TOLERANCE)
+ Math.cosh(-2.99).should be_close(9.96798496414416, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.cosh("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.cosh(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.cosh(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.cosh(MathSpecs::Float.new).should be_close(1.54308063481524, TOLERANCE)
+ end
+end
+
+describe "Math#cosh" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:cos, 3.1415).should be_close(-0.999999995707656, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/erf_spec.rb b/spec/ruby/core/math/erf_spec.rb
new file mode 100644
index 0000000000..5384e73ae9
--- /dev/null
+++ b/spec/ruby/core/math/erf_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# erf method is the "error function" encountered in integrating the normal
+# distribution (which is a normalized form of the Gaussian function).
+describe "Math.erf" do
+ it "returns a float" do
+ Math.erf(1).should.is_a?(Float)
+ end
+
+ it "returns the error function of the argument" do
+ Math.erf(0).should be_close(0.0, TOLERANCE)
+ Math.erf(1).should be_close(0.842700792949715, TOLERANCE)
+ Math.erf(-1).should be_close(-0.842700792949715, TOLERANCE)
+ Math.erf(0.5).should be_close(0.520499877813047, TOLERANCE)
+ Math.erf(-0.5).should be_close(-0.520499877813047, TOLERANCE)
+ Math.erf(10000).should be_close(1.0, TOLERANCE)
+ Math.erf(-10000).should be_close(-1.0, TOLERANCE)
+ Math.erf(0.00000000000001).should be_close(0.0, TOLERANCE)
+ Math.erf(-0.00000000000001).should be_close(0.0, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.erf("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.erf(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.erf(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.erf(MathSpecs::Float.new).should be_close(0.842700792949715, TOLERANCE)
+ end
+end
+
+describe "Math#erf" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:erf, 3.1415).should be_close(0.999991118444483, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/erfc_spec.rb b/spec/ruby/core/math/erfc_spec.rb
new file mode 100644
index 0000000000..4e09a68d1e
--- /dev/null
+++ b/spec/ruby/core/math/erfc_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# erfc is the complementary error function
+describe "Math.erfc" do
+ it "returns a float" do
+ Math.erf(1).should.is_a?(Float)
+ end
+
+ it "returns the complementary error function of the argument" do
+ Math.erfc(0).should be_close(1.0, TOLERANCE)
+ Math.erfc(1).should be_close(0.157299207050285, TOLERANCE)
+ Math.erfc(-1).should be_close(1.84270079294971, TOLERANCE)
+ Math.erfc(0.5).should be_close(0.479500122186953, TOLERANCE)
+ Math.erfc(-0.5).should be_close(1.52049987781305, TOLERANCE)
+ Math.erfc(10000).should be_close(0.0, TOLERANCE)
+ Math.erfc(-10000).should be_close(2.0, TOLERANCE)
+ Math.erfc(0.00000000000001).should be_close(0.999999999999989, TOLERANCE)
+ Math.erfc(-0.00000000000001).should be_close(1.00000000000001, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.erfc("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.erfc(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.erfc(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.erfc(MathSpecs::Float.new).should be_close(0.157299207050285, TOLERANCE)
+ end
+end
+
+describe "Math#erfc" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:erf, 3.1415).should be_close(0.999991118444483, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/exp_spec.rb b/spec/ruby/core/math/exp_spec.rb
new file mode 100644
index 0000000000..3688482457
--- /dev/null
+++ b/spec/ruby/core/math/exp_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.exp" do
+ it "returns a float" do
+ Math.exp(1.0).should.is_a?(Float)
+ end
+
+ it "returns the base-e exponential of the argument" do
+ Math.exp(0.0).should == 1.0
+ Math.exp(-0.0).should == 1.0
+ Math.exp(-1.8).should be_close(0.165298888221587, TOLERANCE)
+ Math.exp(1.25).should be_close(3.49034295746184, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.exp("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.exp(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.exp(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.exp(MathSpecs::Float.new).should be_close(Math::E, TOLERANCE)
+ end
+end
+
+describe "Math#exp" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:exp, 23.1415).should be_close(11226018484.0012, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/expm1_spec.rb b/spec/ruby/core/math/expm1_spec.rb
new file mode 100644
index 0000000000..35f62b5dbd
--- /dev/null
+++ b/spec/ruby/core/math/expm1_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+ruby_version_is "4.0" do
+ describe "Math.expm1" do
+ it "calculates Math.exp(arg) - 1" do
+ Math.expm1(3).should == Math.exp(3) - 1
+ end
+
+ it "preserves precision that can be lost otherwise" do
+ Math.expm1(1.0e-16).should be_close(1.0e-16, TOLERANCE)
+ Math.expm1(1.0e-16).should != 0.0
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.expm1("test") }.should.raise(TypeError, "can't convert String into Float")
+ end
+
+ it "returns NaN given NaN" do
+ Math.expm1(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.expm1(nil) }.should.raise(TypeError, "can't convert nil into Float")
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.expm1(MathSpecs::Float.new).should be_close(Math::E - 1, TOLERANCE)
+ end
+ end
+
+ describe "Math#expm1" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:expm1, 23.1415).should be_close(11226018483.0012, TOLERANCE)
+ end
+ end
+end
diff --git a/spec/ruby/core/math/fixtures/classes.rb b/spec/ruby/core/math/fixtures/classes.rb
new file mode 100644
index 0000000000..6f2241e739
--- /dev/null
+++ b/spec/ruby/core/math/fixtures/classes.rb
@@ -0,0 +1,28 @@
+class IncludesMath
+ include Math
+end
+
+module MathSpecs
+ class Float < Numeric
+ def initialize(value=1.0)
+ @value = value
+ end
+
+ def to_f
+ @value
+ end
+ end
+
+ class Integer
+ def to_int
+ 2
+ end
+ end
+
+ class UserClass
+ end
+
+ class StringSubClass < String
+ end
+
+end
diff --git a/spec/ruby/core/math/fixtures/common.rb b/spec/ruby/core/math/fixtures/common.rb
new file mode 100644
index 0000000000..024732fa7a
--- /dev/null
+++ b/spec/ruby/core/math/fixtures/common.rb
@@ -0,0 +1,3 @@
+class IncludesMath
+ include Math
+end
diff --git a/spec/ruby/core/math/frexp_spec.rb b/spec/ruby/core/math/frexp_spec.rb
new file mode 100644
index 0000000000..6853e4672f
--- /dev/null
+++ b/spec/ruby/core/math/frexp_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.frexp" do
+ it "returns the normalized fraction and exponent" do
+ frac, exp = Math.frexp(102.83)
+ frac.should be_close(0.803359375, TOLERANCE)
+ exp.should == 7
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.frexp("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ frac, _exp = Math.frexp(nan_value)
+ frac.nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.frexp(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ frac, exp = Math.frexp(MathSpecs::Float.new)
+ frac.should be_close(0.5, TOLERANCE)
+ exp.should == 1
+ end
+end
+
+describe "Math#frexp" do
+ it "is accessible as a private instance method" do
+ frac, exp = IncludesMath.new.send(:frexp, 2.1415)
+ frac.should be_close(0.535375, TOLERANCE)
+ exp.should == 2
+ end
+end
diff --git a/spec/ruby/core/math/gamma_spec.rb b/spec/ruby/core/math/gamma_spec.rb
new file mode 100644
index 0000000000..9a0b14448a
--- /dev/null
+++ b/spec/ruby/core/math/gamma_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+
+describe "Math.gamma" do
+ it "returns +infinity given 0" do
+ Math.gamma(0).should == Float::INFINITY
+ end
+
+ platform_is_not :windows do
+ # https://bugs.ruby-lang.org/issues/12249
+ it "returns -infinity given -0.0" do
+ Math.gamma(-0.0).should == -Float::INFINITY
+ end
+ end
+
+ it "returns Math.sqrt(Math::PI) given 0.5" do
+ Math.gamma(0.5).should be_close(Math.sqrt(Math::PI), TOLERANCE)
+ end
+
+ # stop at n == 23 because 23! cannot be exactly represented by IEEE 754 double
+ it "returns exactly (n-1)! given n for n between 2 and 23" do
+ fact = 1
+ 2.upto(23) do |n|
+ fact *= (n - 1)
+ Math.gamma(n).should == fact
+ end
+ end
+
+ it "returns approximately (n-1)! given n for n between 24 and 30" do
+ fact2 = 1124000727777607680000 # 22!
+ 24.upto(30) do |n|
+ fact2 *= n - 1
+ # compare only the first 12 places, tolerate the rest
+ Math.gamma(n).should be_close(fact2, fact2.to_s[12..-1].to_i)
+ end
+ end
+
+ it "returns good numerical approximation for gamma(3.2)" do
+ Math.gamma(3.2).should be_close(2.423965, TOLERANCE)
+ end
+
+ it "returns good numerical approximation for gamma(-2.15)" do
+ Math.gamma(-2.15).should be_close(-2.999619, TOLERANCE)
+ end
+
+ it "returns good numerical approximation for gamma(0.00001)" do
+ Math.gamma(0.00001).should be_close(99999.422794, TOLERANCE)
+ end
+
+ it "returns good numerical approximation for gamma(-0.00001)" do
+ Math.gamma(-0.00001).should be_close(-100000.577225, TOLERANCE)
+ end
+
+ it "raises Math::DomainError given -1" do
+ -> { Math.gamma(-1) }.should.raise(Math::DomainError)
+ end
+
+ # See https://bugs.ruby-lang.org/issues/10642
+ it "returns +infinity given +infinity" do
+ Math.gamma(infinity_value).infinite?.should == 1
+ end
+
+ it "raises Math::DomainError given negative infinity" do
+ -> { Math.gamma(-Float::INFINITY) }.should.raise(Math::DomainError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.gamma(nan_value).nan?.should == true
+ end
+end
diff --git a/spec/ruby/core/math/hypot_spec.rb b/spec/ruby/core/math/hypot_spec.rb
new file mode 100644
index 0000000000..4ab5bd4e88
--- /dev/null
+++ b/spec/ruby/core/math/hypot_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.hypot" do
+ it "returns a float" do
+ Math.hypot(3, 4).should.is_a?(Float)
+ end
+
+ it "returns the length of the hypotenuse of a right triangle with legs given by the arguments" do
+ Math.hypot(0, 0).should be_close(0.0, TOLERANCE)
+ Math.hypot(2, 10).should be_close( 10.1980390271856, TOLERANCE)
+ Math.hypot(5000, 5000).should be_close(7071.06781186548, TOLERANCE)
+ Math.hypot(0.0001, 0.0002).should be_close(0.000223606797749979, TOLERANCE)
+ Math.hypot(-2, -10).should be_close(10.1980390271856, TOLERANCE)
+ Math.hypot(2, 10).should be_close(10.1980390271856, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.hypot("test", "this") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.hypot(nan_value, 0).nan?.should == true
+ Math.hypot(0, nan_value).nan?.should == true
+ Math.hypot(nan_value, nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.hypot(nil, nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.hypot(MathSpecs::Float.new, MathSpecs::Float.new).should be_close(1.4142135623731, TOLERANCE)
+ end
+end
+
+describe "Math#hypot" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:hypot, 2, 3.1415).should be_close(3.72411361937307, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/ldexp_spec.rb b/spec/ruby/core/math/ldexp_spec.rb
new file mode 100644
index 0000000000..1864b7455a
--- /dev/null
+++ b/spec/ruby/core/math/ldexp_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.ldexp" do
+ it "returns a float" do
+ Math.ldexp(1.0, 2).should.is_a?(Float)
+ end
+
+ it "returns the argument multiplied by 2**n" do
+ Math.ldexp(0.0, 0.0).should == 0.0
+ Math.ldexp(0.0, 1.0).should == 0.0
+ Math.ldexp(-1.25, 2).should be_close(-5.0, TOLERANCE)
+ Math.ldexp(2.1, -3).should be_close(0.2625, TOLERANCE)
+ Math.ldexp(5.7, 4).should be_close(91.2, TOLERANCE)
+ end
+
+ it "raises a TypeError if the first argument cannot be coerced with Float()" do
+ -> { Math.ldexp("test", 2) }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.ldexp(nan_value, 0).nan?.should == true
+ end
+
+ it "raises RangeError if NaN is given as the second arg" do
+ -> { Math.ldexp(0, nan_value) }.should.raise(RangeError)
+ end
+
+ it "raises a TypeError if the second argument cannot be coerced with Integer()" do
+ -> { Math.ldexp(3.2, "this") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the first argument is nil" do
+ -> { Math.ldexp(nil, 2) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the second argument is nil" do
+ -> { Math.ldexp(3.1, nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any first argument that can be coerced with Float()" do
+ Math.ldexp(MathSpecs::Float.new, 2).should be_close(4.0, TOLERANCE)
+ end
+
+ it "accepts any second argument that can be coerced with Integer()" do
+ Math.ldexp(3.23, MathSpecs::Integer.new).should be_close(12.92, TOLERANCE)
+ end
+
+ it "returns correct value that closes to the max value of double type" do
+ Math.ldexp(0.5122058490966879, 1024).should == 9.207889385574391e+307
+ Math.ldexp(0.9999999999999999, 1024).should == 1.7976931348623157e+308
+ Math.ldexp(0.99999999999999999, 1024).should == Float::INFINITY
+ end
+end
+
+describe "Math#ldexp" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:ldexp, 3.1415, 2).should be_close(12.566, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/lgamma_spec.rb b/spec/ruby/core/math/lgamma_spec.rb
new file mode 100644
index 0000000000..38f07d0bf8
--- /dev/null
+++ b/spec/ruby/core/math/lgamma_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+
+describe "Math.lgamma" do
+ it "returns [Infinity, 1] when passed 0" do
+ Math.lgamma(0).should == [infinity_value, 1]
+ end
+
+ it "returns [Infinity, ...] when passed -1" do
+ Math.lgamma(-1)[0].should == infinity_value
+ end
+
+ it "returns [Infinity, -1] when passed -0.0" do
+ Math.lgamma(-0.0).should == [infinity_value, -1]
+ end
+
+ it "returns [log(sqrt(PI)), 1] when passed 0.5" do
+ lg1 = Math.lgamma(0.5)
+ lg1[0].should be_close(Math.log(Math.sqrt(Math::PI)), TOLERANCE)
+ lg1[1].should == 1
+ end
+
+ it "returns [log(2/3*PI, 1] when passed 6.0" do
+ lg2 = Math.lgamma(6.0)
+ lg2[0].should be_close(Math.log(120.0), TOLERANCE)
+ lg2[1].should == 1
+ end
+
+ it "returns an approximate value when passed -0.5" do
+ lg1 = Math.lgamma(-0.5)
+ lg1[0].should be_close(1.2655121, TOLERANCE)
+ lg1[1].should == -1
+ end
+
+ it "returns an approximate value when passed -1.5" do
+ lg2 = Math.lgamma(-1.5)
+ lg2[0].should be_close(0.8600470, TOLERANCE)
+ lg2[1].should == 1
+ end
+
+ it "raises Math::DomainError when passed -Infinity" do
+ -> { Math.lgamma(-infinity_value) }.should.raise(Math::DomainError)
+ end
+
+ it "returns [Infinity, 1] when passed Infinity" do
+ Math.lgamma(infinity_value).should == [infinity_value, 1]
+ end
+
+ it "returns [NaN, ...] when passed NaN" do
+ Math.lgamma(nan_value)[0].should.nan?
+ end
+end
diff --git a/spec/ruby/core/math/log10_spec.rb b/spec/ruby/core/math/log10_spec.rb
new file mode 100644
index 0000000000..7576a67002
--- /dev/null
+++ b/spec/ruby/core/math/log10_spec.rb
@@ -0,0 +1,47 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# The common logarithm, having base 10
+describe "Math.log10" do
+ it "returns a float" do
+ Math.log10(1).should.is_a?(Float)
+ end
+
+ it "returns the base-10 logarithm of the argument" do
+ Math.log10(0.0001).should be_close(-4.0, TOLERANCE)
+ Math.log10(0.000000000001e-15).should be_close(-27.0, TOLERANCE)
+ Math.log10(1).should be_close(0.0, TOLERANCE)
+ Math.log10(10).should be_close(1.0, TOLERANCE)
+ Math.log10(10e15).should be_close(16.0, TOLERANCE)
+ end
+
+ it "raises an Math::DomainError if the argument is less than 0" do
+ -> { Math.log10(-1e-15) }.should.raise(Math::DomainError)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.log10("test") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a numerical argument as a string" do
+ -> { Math.log10("1.0") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.log10(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.log10(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.log10(MathSpecs::Float.new).should be_close(0.0, TOLERANCE)
+ end
+end
+
+describe "Math#log10" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:log10, 4.15).should be_close(0.618048096712093, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/log1p_spec.rb b/spec/ruby/core/math/log1p_spec.rb
new file mode 100644
index 0000000000..181b462ded
--- /dev/null
+++ b/spec/ruby/core/math/log1p_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+ruby_version_is "4.0" do
+ describe "Math.log1p" do
+ it "calculates Math.log(1 + arg)" do
+ Math.log1p(3).should == Math.log(1 + 3)
+ end
+
+ it "preserves precision that can be lost otherwise" do
+ Math.log1p(1e-16).should be_close(1.0e-16, TOLERANCE)
+ Math.log1p(1e-16).should != 0.0
+ end
+
+ it "raises an Math::DomainError if the argument is less than 1" do
+ -> { Math.log1p(-1-1e-15) }.should.raise(Math::DomainError, "Numerical argument is out of domain - log1p")
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.log1p("test") }.should.raise(TypeError, "can't convert String into Float")
+ end
+
+ it "raises a TypeError for numerical values passed as string" do
+ -> { Math.log1p("10") }.should.raise(TypeError, "can't convert String into Float")
+ end
+
+ it "does not accept a second argument for the base" do
+ -> { Math.log1p(9, 3) }.should.raise(ArgumentError, "wrong number of arguments (given 2, expected 1)")
+ end
+
+ it "returns NaN given NaN" do
+ Math.log1p(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.log1p(nil) }.should.raise(TypeError, "can't convert nil into Float")
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.log1p(MathSpecs::Float.new).should be_close(0.6931471805599453, TOLERANCE)
+ end
+ end
+
+ describe "Math#log1p" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:log1p, 4.21).should be_close(1.65057985576528, TOLERANCE)
+ end
+ end
+end
diff --git a/spec/ruby/core/math/log2_spec.rb b/spec/ruby/core/math/log2_spec.rb
new file mode 100644
index 0000000000..e38a8bb67f
--- /dev/null
+++ b/spec/ruby/core/math/log2_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.log2" do
+ it "returns a float" do
+ Math.log2(5.79).should be_close(2.53356334821451, TOLERANCE)
+ end
+
+ it "returns the natural logarithm of the argument" do
+ Math.log2(1.1).should be_close(0.137503523749935, TOLERANCE)
+ Math.log2(3.14).should be_close(1.6507645591169, TOLERANCE)
+ Math.log2((2**101+45677544234809571)).should be_close(101.00000000000003, TOLERANCE)
+
+ Math.log2((2**10001+45677544234809571)).should == 10001.0
+ Math.log2((2**301+45677544234809571)).should == 301.0
+ end
+
+ it "raises Math::DomainError if the argument is less than 0" do
+ -> { Math.log2(-1e-15) }.should.raise( Math::DomainError)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.log2("test") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a numerical argument as a string" do
+ -> { Math.log2("1.0") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.log2(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.log2(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.log2(MathSpecs::Float.new).should be_close(0.0, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/log_spec.rb b/spec/ruby/core/math/log_spec.rb
new file mode 100644
index 0000000000..7e0bc13bc6
--- /dev/null
+++ b/spec/ruby/core/math/log_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# The natural logarithm, having base Math::E
+describe "Math.log" do
+ it "returns a float" do
+ Math.log(1).should.is_a?(Float)
+ end
+
+ it "returns the natural logarithm of the argument" do
+ Math.log(0.0001).should be_close(-9.21034037197618, TOLERANCE)
+ Math.log(0.000000000001e-15).should be_close(-62.1697975108392, TOLERANCE)
+ Math.log(1).should be_close(0.0, TOLERANCE)
+ Math.log(10).should be_close( 2.30258509299405, TOLERANCE)
+ Math.log(10e15).should be_close(36.8413614879047, TOLERANCE)
+ end
+
+ it "raises an Math::DomainError if the argument is less than 0" do
+ -> { Math.log(-1e-15) }.should.raise(Math::DomainError)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.log("test") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError for numerical values passed as string" do
+ -> { Math.log("10") }.should.raise(TypeError)
+ end
+
+ it "accepts a second argument for the base" do
+ Math.log(9, 3).should be_close(2, TOLERANCE)
+ Math.log(8, 1.4142135623730951).should be_close(6, TOLERANCE)
+ end
+
+ it "raises a TypeError when the numerical base cannot be coerced to a float" do
+ -> { Math.log(10, "2") }.should.raise(TypeError)
+ -> { Math.log(10, nil) }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.log(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.log(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.log(MathSpecs::Float.new).should be_close(0.0, TOLERANCE)
+ end
+end
+
+describe "Math#log" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:log, 5.21).should be_close(1.65057985576528, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/shared/atanh.rb b/spec/ruby/core/math/shared/atanh.rb
new file mode 100644
index 0000000000..48a6bf836e
--- /dev/null
+++ b/spec/ruby/core/math/shared/atanh.rb
@@ -0,0 +1,44 @@
+describe :math_atanh_base, shared: true do
+ it "returns a float" do
+ @object.send(@method, 0.5).should.instance_of?(Float)
+ end
+
+ it "returns the inverse hyperbolic tangent of the argument" do
+ @object.send(@method, 0.0).should == 0.0
+ @object.send(@method, -0.0).should == -0.0
+ @object.send(@method, 0.5).should be_close(0.549306144334055, TOLERANCE)
+ @object.send(@method, -0.2).should be_close(-0.202732554054082, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { @object.send(@method, nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the argument is not a Numeric" do
+ -> { @object.send(@method, "test") }.should.raise(TypeError)
+ end
+
+ it "returns Infinity if x == 1.0" do
+ @object.send(@method, 1.0).should == Float::INFINITY
+ end
+
+ it "return -Infinity if x == -1.0" do
+ @object.send(@method, -1.0).should == -Float::INFINITY
+ end
+end
+
+describe :math_atanh_private, shared: true do
+ it "is a private instance method" do
+ Math.private_instance_methods(false).should.include?(@method)
+ end
+end
+
+describe :math_atanh_no_complex, shared: true do
+ it "raises a Math::DomainError for arguments greater than 1.0" do
+ -> { @object.send(@method, 1.0 + Float::EPSILON) }.should.raise(Math::DomainError)
+ end
+
+ it "raises a Math::DomainError for arguments less than -1.0" do
+ -> { @object.send(@method, -1.0 - Float::EPSILON) }.should.raise(Math::DomainError)
+ end
+end
diff --git a/spec/ruby/core/math/sin_spec.rb b/spec/ruby/core/math/sin_spec.rb
new file mode 100644
index 0000000000..a9479e3aec
--- /dev/null
+++ b/spec/ruby/core/math/sin_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# sine : (-Inf, Inf) --> (-1.0, 1.0)
+describe "Math.sin" do
+ it "returns a float" do
+ Math.sin(Math::PI).should.is_a?(Float)
+ end
+
+ it "returns the sine of the argument expressed in radians" do
+ Math.sin(Math::PI).should be_close(0.0, TOLERANCE)
+ Math.sin(0).should be_close(0.0, TOLERANCE)
+ Math.sin(Math::PI/2).should be_close(1.0, TOLERANCE)
+ Math.sin(3*Math::PI/2).should be_close(-1.0, TOLERANCE)
+ Math.sin(2*Math::PI).should be_close(0.0, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.sin("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.sin(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.sin(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.sin(MathSpecs::Float.new).should be_close(0.841470984807897, TOLERANCE)
+ end
+end
+
+describe "Math#sin" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:sin, 1.21).should be_close(0.935616001553386, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/sinh_spec.rb b/spec/ruby/core/math/sinh_spec.rb
new file mode 100644
index 0000000000..de0f06affa
--- /dev/null
+++ b/spec/ruby/core/math/sinh_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.sinh" do
+ it "returns a float" do
+ Math.sinh(1.2).should.is_a?(Float)
+ end
+
+ it "returns the hyperbolic sin of the argument" do
+ Math.sinh(0.0).should == 0.0
+ Math.sinh(-0.0).should == 0.0
+ Math.sinh(1.5).should be_close(2.12927945509482, TOLERANCE)
+ Math.sinh(-2.8).should be_close(-8.19191835423591, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.sinh("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.sinh(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.sinh(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.sinh(MathSpecs::Float.new).should be_close(1.1752011936438, TOLERANCE)
+ end
+end
+
+describe "Math#sinh" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:sinh, 1.99).should be_close(3.58941916843202, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/sqrt_spec.rb b/spec/ruby/core/math/sqrt_spec.rb
new file mode 100644
index 0000000000..545fa4d1c2
--- /dev/null
+++ b/spec/ruby/core/math/sqrt_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.sqrt" do
+ it "returns a float" do
+ Math.sqrt(1).should.is_a?(Float)
+ end
+
+ it "returns the square root of the argument" do
+ Math.sqrt(1).should == 1.0
+ Math.sqrt(4.0).should == 2.0
+ Math.sqrt(15241578780673814.441547445).should be_close(123456789.123457, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.sqrt("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.sqrt(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.sqrt(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.sqrt(MathSpecs::Float.new).should be_close(1.0, TOLERANCE)
+ end
+
+ it "raises a Math::DomainError when given a negative number" do
+ -> { Math.sqrt(-1) }.should.raise(Math::DomainError)
+ end
+end
+
+describe "Math#sqrt" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:sqrt, 2.23).should be_close(1.49331845230681, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/tan_spec.rb b/spec/ruby/core/math/tan_spec.rb
new file mode 100644
index 0000000000..c3e773f318
--- /dev/null
+++ b/spec/ruby/core/math/tan_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.tan" do
+ it "returns a float" do
+ Math.tan(1.35).should.is_a?(Float)
+ end
+
+ it "returns the tangent of the argument" do
+ Math.tan(0.0).should == 0.0
+ Math.tan(-0.0).should == -0.0
+ Math.tan(4.22).should be_close(1.86406937682395, TOLERANCE)
+ Math.tan(-9.65).should be_close(-0.229109052606441, TOLERANCE)
+ end
+
+ it "returns NaN if called with +-Infinity" do
+ Math.tan(infinity_value).should.nan?
+ Math.tan(-infinity_value).should.nan?
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.tan("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.tan(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.tan(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.tan(MathSpecs::Float.new).should be_close(1.5574077246549, TOLERANCE)
+ end
+end
+
+describe "Math#tan" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:tan, 1.0).should be_close(1.5574077246549, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/math/tanh_spec.rb b/spec/ruby/core/math/tanh_spec.rb
new file mode 100644
index 0000000000..74a938ffd8
--- /dev/null
+++ b/spec/ruby/core/math/tanh_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Math.tanh" do
+ it "returns a float" do
+ Math.tanh(0.5).should.is_a?(Float)
+ end
+
+ it "returns the hyperbolic tangent of the argument" do
+ Math.tanh(0.0).should == 0.0
+ Math.tanh(-0.0).should == -0.0
+ Math.tanh(infinity_value).should == 1.0
+ Math.tanh(-infinity_value).should == -1.0
+ Math.tanh(2.5).should be_close(0.98661429815143, TOLERANCE)
+ Math.tanh(-4.892).should be_close(-0.999887314427707, TOLERANCE)
+ end
+
+ it "raises a TypeError if the argument cannot be coerced with Float()" do
+ -> { Math.tanh("test") }.should.raise(TypeError)
+ end
+
+ it "returns NaN given NaN" do
+ Math.tanh(nan_value).nan?.should == true
+ end
+
+ it "raises a TypeError if the argument is nil" do
+ -> { Math.tanh(nil) }.should.raise(TypeError)
+ end
+
+ it "accepts any argument that can be coerced with Float()" do
+ Math.tanh(MathSpecs::Float.new).should be_close(0.761594155955765, TOLERANCE)
+ end
+end
+
+describe "Math#tanh" do
+ it "is accessible as a private instance method" do
+ IncludesMath.new.send(:tanh, 5.21).should be_close(0.99994034202065, TOLERANCE)
+ end
+end
diff --git a/spec/ruby/core/method/arity_spec.rb b/spec/ruby/core/method/arity_spec.rb
new file mode 100644
index 0000000000..4bb821735a
--- /dev/null
+++ b/spec/ruby/core/method/arity_spec.rb
@@ -0,0 +1,222 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#arity" do
+ SpecEvaluate.desc = "for method definition"
+
+ context "returns zero" do
+ evaluate <<-ruby do
+ def m() end
+ ruby
+
+ method(:m).arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ def n(&b) end
+ ruby
+
+ method(:n).arity.should == 0
+ end
+ end
+
+ context "returns positive values" do
+ evaluate <<-ruby do
+ def m(a) end
+ def n(a, b) end
+ def o(a, b, c) end
+ def p(a, b, c, d) end
+ ruby
+
+ method(:m).arity.should == 1
+ method(:n).arity.should == 2
+ method(:o).arity.should == 3
+ method(:p).arity.should == 4
+ end
+
+ evaluate <<-ruby do
+ def m(a:) end
+ def n(a:, b:) end
+ def o(a: 1, b:, c:, d: 2) end
+ ruby
+
+ method(:m).arity.should == 1
+ method(:n).arity.should == 1
+ method(:o).arity.should == 1
+ end
+
+ evaluate <<-ruby do
+ def m(a, b:) end
+ def n(a, b:, &l) end
+ ruby
+
+ method(:m).arity.should == 2
+ method(:n).arity.should == 2
+ end
+
+ evaluate <<-ruby do
+ def m(a, b, c:, d: 1) end
+ def n(a, b, c:, d: 1, **k, &l) end
+ ruby
+
+ method(:m).arity.should == 3
+ method(:n).arity.should == 3
+ end
+ end
+
+ context "returns negative values" do
+ evaluate <<-ruby do
+ def m(a=1) end
+ def n(a=1, b=2) end
+ ruby
+
+ method(:m).arity.should == -1
+ method(:n).arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ def m(a, b=1) end
+ def n(a, b, c=1, d=2) end
+ ruby
+
+ method(:m).arity.should == -2
+ method(:n).arity.should == -3
+ end
+
+ evaluate <<-ruby do
+ def m(a=1, *b) end
+ def n(a=1, b=2, *c) end
+ ruby
+
+ method(:m).arity.should == -1
+ method(:n).arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ def m(*) end
+ def n(*a) end
+ ruby
+
+ method(:m).arity.should == -1
+ method(:n).arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ def m(a, *) end
+ def n(a, *b) end
+ def o(a, b, *c) end
+ def p(a, b, c, *d) end
+ ruby
+
+ method(:m).arity.should == -2
+ method(:n).arity.should == -2
+ method(:o).arity.should == -3
+ method(:p).arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ def m(*a, b) end
+ def n(*a, b, c) end
+ def o(*a, b, c, d) end
+ ruby
+
+ method(:m).arity.should == -2
+ method(:n).arity.should == -3
+ method(:o).arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ def m(a, *b, c) end
+ def n(a, b, *c, d, e) end
+ ruby
+
+ method(:m).arity.should == -3
+ method(:n).arity.should == -5
+ end
+
+ evaluate <<-ruby do
+ def m(a, b=1, c=2, *d, e, f) end
+ def n(a, b, c=1, *d, e, f, g) end
+ ruby
+
+ method(:m).arity.should == -4
+ method(:n).arity.should == -6
+ end
+
+ evaluate <<-ruby do
+ def m(a: 1) end
+ def n(a: 1, b: 2) end
+ ruby
+
+ method(:m).arity.should == -1
+ method(:n).arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ def m(a=1, b: 2) end
+ def n(*a, b: 1) end
+ def o(a=1, b: 2) end
+ def p(a=1, *b, c: 2, &l) end
+ ruby
+
+ method(:m).arity.should == -1
+ method(:n).arity.should == -1
+ method(:o).arity.should == -1
+ method(:p).arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ def m(**k, &l) end
+ def n(*a, **k) end
+ def o(a: 1, b: 2, **k) end
+ ruby
+
+ method(:m).arity.should == -1
+ method(:n).arity.should == -1
+ method(:o).arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ def m(a=1, *b, c:, d: 2, **k, &l) end
+ ruby
+
+ method(:m).arity.should == -2
+ end
+
+ evaluate <<-ruby do
+ def m(a, b=1, *c, d, e:, f: 2, **k, &l) end
+ def n(a, b=1, *c, d:, e:, f: 2, **k, &l) end
+ def o(a=0, b=1, *c, d, e:, f: 2, **k, &l) end
+ def p(a=0, b=1, *c, d:, e:, f: 2, **k, &l) end
+ ruby
+
+ method(:m).arity.should == -4
+ method(:n).arity.should == -3
+ method(:o).arity.should == -3
+ method(:p).arity.should == -2
+ end
+ end
+
+ context "for a Method generated by respond_to_missing?" do
+ it "returns -1" do
+ obj = mock("method arity respond_to_missing")
+ obj.should_receive(:respond_to_missing?).and_return(true)
+
+ obj.method(:m).arity.should == -1
+ end
+ end
+
+ context "for a Method generated by attr_reader" do
+ it "return 0" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:reader).arity.should == 0
+ end
+ end
+
+ context "for a Method generated by attr_writer" do
+ it "returns 1" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:writer=).arity.should == 1
+ end
+ end
+end
diff --git a/spec/ruby/core/method/call_spec.rb b/spec/ruby/core/method/call_spec.rb
new file mode 100644
index 0000000000..6d997325fa
--- /dev/null
+++ b/spec/ruby/core/method/call_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/call'
+
+describe "Method#call" do
+ it_behaves_like :method_call, :call
+end
diff --git a/spec/ruby/core/method/case_compare_spec.rb b/spec/ruby/core/method/case_compare_spec.rb
new file mode 100644
index 0000000000..a78953e8ad
--- /dev/null
+++ b/spec/ruby/core/method/case_compare_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/call'
+
+describe "Method#===" do
+ it_behaves_like :method_call, :===
+end
diff --git a/spec/ruby/core/method/clone_spec.rb b/spec/ruby/core/method/clone_spec.rb
new file mode 100644
index 0000000000..b0eb5751a9
--- /dev/null
+++ b/spec/ruby/core/method/clone_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'shared/dup'
+
+describe "Method#clone" do
+ it_behaves_like :method_dup, :clone
+
+ it "preserves frozen status" do
+ method = Object.new.method(:method)
+ method.freeze
+ method.frozen?.should == true
+ method.clone.frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/method/compose_spec.rb b/spec/ruby/core/method/compose_spec.rb
new file mode 100644
index 0000000000..7506e33ea8
--- /dev/null
+++ b/spec/ruby/core/method/compose_spec.rb
@@ -0,0 +1,99 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../proc/shared/compose'
+
+describe "Method#<<" do
+ it "returns a Proc that is the composition of self and the passed Proc" do
+ succ = MethodSpecs::Composition.new.method(:succ)
+ upcase = proc { |s| s.upcase }
+
+ (succ << upcase).call('Ruby').should == "RUBZ"
+ end
+
+ it "calls passed Proc with arguments and then calls self with result" do
+ pow_2_proc = proc { |x| x * x }
+ double_proc = proc { |x| x + x }
+
+ pow_2_method = MethodSpecs::Composition.new.method(:pow_2)
+ double_method = MethodSpecs::Composition.new.method(:double)
+
+ (pow_2_method << double_proc).call(2).should == 16
+ (double_method << pow_2_proc).call(2).should == 8
+ end
+
+ it "accepts any callable object" do
+ inc = MethodSpecs::Composition.new.method(:inc)
+
+ double = Object.new
+ def double.call(n); n * 2; end
+
+ (inc << double).call(3).should == 7
+ end
+
+ it_behaves_like :proc_compose, :<<, -> { MethodSpecs::Composition.new.method(:upcase) }
+
+ describe "composition" do
+ it "is a lambda" do
+ pow_2 = MethodSpecs::Composition.new.method(:pow_2)
+ double = proc { |x| x + x }
+
+ (pow_2 << double).is_a?(Proc).should == true
+ (pow_2 << double).should_not.lambda?
+ end
+
+ it "may accept multiple arguments" do
+ inc = MethodSpecs::Composition.new.method(:inc)
+ mul = proc { |n, m| n * m }
+
+ (inc << mul).call(2, 3).should == 7
+ end
+ end
+end
+
+describe "Method#>>" do
+ it "returns a Proc that is the composition of self and the passed Proc" do
+ upcase = proc { |s| s.upcase }
+ succ = MethodSpecs::Composition.new.method(:succ)
+
+ (succ >> upcase).call('Ruby').should == "RUBZ"
+ end
+
+ it "calls passed Proc with arguments and then calls self with result" do
+ pow_2_proc = proc { |x| x * x }
+ double_proc = proc { |x| x + x }
+
+ pow_2_method = MethodSpecs::Composition.new.method(:pow_2)
+ double_method = MethodSpecs::Composition.new.method(:double)
+
+ (pow_2_method >> double_proc).call(2).should == 8
+ (double_method >> pow_2_proc).call(2).should == 16
+ end
+
+ it "accepts any callable object" do
+ inc = MethodSpecs::Composition.new.method(:inc)
+
+ double = Object.new
+ def double.call(n); n * 2; end
+
+ (inc >> double).call(3).should == 8
+ end
+
+ it_behaves_like :proc_compose, :>>, -> { MethodSpecs::Composition.new.method(:upcase) }
+
+ describe "composition" do
+ it "is a lambda" do
+ pow_2 = MethodSpecs::Composition.new.method(:pow_2)
+ double = proc { |x| x + x }
+
+ (pow_2 >> double).is_a?(Proc).should == true
+ (pow_2 >> double).should.lambda?
+ end
+
+ it "may accept multiple arguments" do
+ mul = MethodSpecs::Composition.new.method(:mul)
+ inc = proc { |n| n + 1 }
+
+ (mul >> inc).call(2, 3).should == 7
+ end
+ end
+end
diff --git a/spec/ruby/core/method/curry_spec.rb b/spec/ruby/core/method/curry_spec.rb
new file mode 100644
index 0000000000..79c5d7c662
--- /dev/null
+++ b/spec/ruby/core/method/curry_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#curry" do
+ it "returns a curried proc" do
+ x = Object.new
+ def x.foo(a,b,c); [a,b,c]; end
+
+ c = x.method(:foo).curry
+ c.should.is_a?(Proc)
+ c.call(1).call(2, 3).should == [1,2,3]
+ end
+
+ describe "with optional arity argument" do
+ before(:each) do
+ @obj = MethodSpecs::Methods.new
+ end
+
+ it "returns a curried proc when given correct arity" do
+ @obj.method(:one_req).curry(1).should.is_a?(Proc)
+ @obj.method(:zero_with_splat).curry(100).should.is_a?(Proc)
+ @obj.method(:two_req_with_splat).curry(2).should.is_a?(Proc)
+ end
+
+ it "raises ArgumentError when the method requires less arguments than the given arity" do
+ -> { @obj.method(:zero).curry(1) }.should.raise(ArgumentError)
+ -> { @obj.method(:one_req_one_opt).curry(3) }.should.raise(ArgumentError)
+ -> { @obj.method(:two_req_one_opt_with_block).curry(4) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError when the method requires more arguments than the given arity" do
+ -> { @obj.method(:two_req_with_splat).curry(1) }.should.raise(ArgumentError)
+ -> { @obj.method(:one_req).curry(0) }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/method/dup_spec.rb b/spec/ruby/core/method/dup_spec.rb
new file mode 100644
index 0000000000..e3e29d8a68
--- /dev/null
+++ b/spec/ruby/core/method/dup_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'shared/dup'
+
+describe "Method#dup" do
+ ruby_version_is "3.4" do
+ it_behaves_like :method_dup, :dup
+
+ it "resets frozen status" do
+ method = Object.new.method(:method)
+ method.freeze
+ method.frozen?.should == true
+ method.dup.frozen?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/method/element_reference_spec.rb b/spec/ruby/core/method/element_reference_spec.rb
new file mode 100644
index 0000000000..aa6c54d1cb
--- /dev/null
+++ b/spec/ruby/core/method/element_reference_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/call'
+
+describe "Method#[]" do
+ it_behaves_like :method_call, :[]
+end
diff --git a/spec/ruby/core/method/eql_spec.rb b/spec/ruby/core/method/eql_spec.rb
new file mode 100644
index 0000000000..b97c9e4db0
--- /dev/null
+++ b/spec/ruby/core/method/eql_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+
+describe "Method#eql?" do
+ it_behaves_like :method_equal, :eql?
+end
diff --git a/spec/ruby/core/method/equal_value_spec.rb b/spec/ruby/core/method/equal_value_spec.rb
new file mode 100644
index 0000000000..0431d0c5f6
--- /dev/null
+++ b/spec/ruby/core/method/equal_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+
+describe "Method#==" do
+ it_behaves_like :method_equal, :==
+end
diff --git a/spec/ruby/core/method/fixtures/classes.rb b/spec/ruby/core/method/fixtures/classes.rb
new file mode 100644
index 0000000000..41904df1d1
--- /dev/null
+++ b/spec/ruby/core/method/fixtures/classes.rb
@@ -0,0 +1,247 @@
+module MethodSpecs
+
+
+ class SourceLocation
+ def self.location # This needs to be on this line
+ :location # for the spec to pass
+ end
+
+ def self.redefined
+ :first
+ end
+
+ def self.redefined
+ :last
+ end
+
+ def original
+ end
+
+ alias :aka :original
+ end
+
+ class Methods
+ def foo
+ true
+ end
+
+ alias bar foo
+ alias baz bar
+ alias qux baz
+
+ def same_as_foo
+ true
+ end
+
+ def respond_to_missing? method, bool
+ [:handled_via_method_missing, :also_handled].include? method
+ end
+
+ def method_missing(method, *arguments)
+ if [:handled_via_method_missing, :also_handled].include? method
+ arguments
+ else
+ super
+ end
+ end
+
+ attr_accessor :attr
+
+ def zero; end
+ def one_req(a); end
+ def two_req(a, b); end
+
+ def one_req_named(a:); end
+
+ def zero_with_block(&blk); end
+ def one_req_with_block(a, &blk); end
+ def two_req_with_block(a, b, &blk); end
+
+ def one_opt(a=nil); end
+ def one_req_one_opt(a, b=nil); end
+ def one_req_two_opt(a, b=nil, c=nil); end
+ def two_req_one_opt(a, b, c=nil); end
+
+ def one_opt_named(a: nil); end
+
+ def one_opt_with_block(a=nil, &blk); end
+ def one_req_one_opt_with_block(a, b=nil, &blk); end
+ def one_req_two_opt_with_block(a, b=nil, c=nil, &blk); end
+ def two_req_one_opt_with_block(a, b, c=nil, &blk); end
+
+ def zero_with_splat(*a); end
+ def one_req_with_splat(a, *b); end
+ def two_req_with_splat(a, b, *c); end
+ def one_req_one_opt_with_splat(a, b=nil, *c); end
+ def two_req_one_opt_with_splat(a, b, c=nil, *d); end
+ def one_req_two_opt_with_splat(a, b=nil, c=nil, *d); end
+
+ def zero_with_double_splat(**a); end
+
+ def zero_with_splat_and_block(*a, &blk); end
+ def one_req_with_splat_and_block(a, *b, &blk); end
+ def two_req_with_splat_and_block(a, b, *c, &blk); end
+ def one_req_one_opt_with_splat_and_block(a, b=nil, *c, &blk); end
+ def two_req_one_opt_with_splat_and_block(a, b, c=nil, *d, &blk); end
+ def one_req_two_opt_with_splat_and_block(a, b=nil, c=nil, *d, &blk); end
+
+ def my_public_method; end
+ def my_protected_method; end
+ def my_private_method; end
+ protected :my_protected_method
+ private :my_private_method
+
+ define_method(:zero_defined_method, Proc.new {||})
+ define_method(:zero_with_splat_defined_method, Proc.new {|*x|})
+ define_method(:one_req_defined_method, Proc.new {|x|})
+ define_method(:two_req_defined_method, Proc.new {|x, y|})
+ define_method(:no_args_defined_method) {}
+ define_method(:two_grouped_defined_method) {|(_x1,_x2)|}
+
+ attr_reader :reader
+ attr_writer :writer
+ end
+
+ module MyMod
+ def bar; :bar; end
+ end
+
+ class MySuper
+ include MyMod
+ end
+
+ class MySub < MySuper; end
+
+ class A
+ def baz(a, b)
+ self.class
+ end
+ def overridden; end
+ end
+
+ class B < A
+ def overridden; end
+ end
+
+ module BetweenBAndC
+ def overridden; end
+ end
+
+ class C < B
+ include BetweenBAndC
+ def overridden; end
+ end
+
+ module OverrideAgain
+ def overridden; end
+ end
+
+ class D
+ def bar() 'done' end
+ end
+
+ class Eql
+
+ def same_body
+ 1 + 1
+ end
+
+ alias :same_body_alias :same_body
+
+ def same_body_with_args(arg)
+ 1 + 1
+ end
+
+ def different_body
+ 1 + 2
+ end
+
+ def same_body_two
+ 1 + 1
+ end
+
+ private
+ def same_body_private
+ 1 + 1
+ end
+ end
+
+ class Eql2
+
+ def same_body
+ 1 + 1
+ end
+
+ end
+
+ class ToProc
+ def method_called(a, b)
+ ScratchPad << [a, b]
+ end
+
+ def to_proc
+ method(:method_called).to_proc
+ end
+ end
+
+ class ToProcBeta
+ def method_called(a)
+ ScratchPad << a
+ a
+ end
+
+ def to_proc
+ method(:method_called).to_proc
+ end
+ end
+
+ class Composition
+ def upcase(s)
+ s.upcase
+ end
+
+ def succ(s)
+ s.succ
+ end
+
+ def pow_2(n)
+ n * n
+ end
+
+ def double(n)
+ n + n
+ end
+
+ def inc(n)
+ n + 1
+ end
+
+ def mul(n, m)
+ n * m
+ end
+ end
+
+ module InheritedMethods
+ module A
+ private
+ def derp(message)
+ 'A'
+ end
+ end
+
+ module B
+ private
+ def derp
+ 'B' + super('superclass')
+ end
+ end
+
+ class C
+ include A
+ include B
+
+ public :derp
+ alias_method :meow, :derp
+ end
+ end
+end
diff --git a/spec/ruby/core/method/hash_spec.rb b/spec/ruby/core/method/hash_spec.rb
new file mode 100644
index 0000000000..d6c8440acc
--- /dev/null
+++ b/spec/ruby/core/method/hash_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#hash" do
+ it "returns the same value for user methods that are eql?" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:foo).hash.should == obj.method(:bar).hash
+ end
+
+ # See also redmine #6048
+ it "returns the same value for builtin methods that are eql?" do
+ obj = [42]
+ obj.method(:to_s).hash.should == obj.method(:inspect).hash
+ end
+end
diff --git a/spec/ruby/core/method/inspect_spec.rb b/spec/ruby/core/method/inspect_spec.rb
new file mode 100644
index 0000000000..97ff2d8c11
--- /dev/null
+++ b/spec/ruby/core/method/inspect_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+require_relative 'shared/aliased_inspect'
+
+describe "Method#inspect" do
+ it_behaves_like :method_to_s, :inspect
+ it_behaves_like :method_to_s_aliased, :inspect, -> meth { meth }
+end
diff --git a/spec/ruby/core/method/name_spec.rb b/spec/ruby/core/method/name_spec.rb
new file mode 100644
index 0000000000..de390c6f52
--- /dev/null
+++ b/spec/ruby/core/method/name_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#name" do
+ it "returns the name of the method" do
+ "abc".method(:upcase).name.should == :upcase
+ end
+
+ it "returns the name even when aliased" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:foo).name.should == :foo
+ obj.method(:bar).name.should == :bar
+ obj.method(:bar).unbind.bind(obj).name.should == :bar
+ end
+
+ describe "for a Method generated by respond_to_missing?" do
+ it "returns the name passed to respond_to_missing?" do
+ @m = MethodSpecs::Methods.new
+ @m.method(:handled_via_method_missing).name.should == :handled_via_method_missing
+ end
+ end
+end
diff --git a/spec/ruby/core/method/original_name_spec.rb b/spec/ruby/core/method/original_name_spec.rb
new file mode 100644
index 0000000000..b92cf35154
--- /dev/null
+++ b/spec/ruby/core/method/original_name_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#original_name" do
+ it "returns the name of the method" do
+ "abc".method(:upcase).original_name.should == :upcase
+ end
+
+ it "returns the original name when aliased" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:foo).original_name.should == :foo
+ obj.method(:bar).original_name.should == :foo
+ obj.method(:bar).unbind.bind(obj).original_name.should == :foo
+ end
+
+ it "returns the original name even when aliased twice" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:foo).original_name.should == :foo
+ obj.method(:baz).original_name.should == :foo
+ obj.method(:baz).unbind.bind(obj).original_name.should == :foo
+ end
+
+ it "returns the original name even when aliased thrice" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:qux).original_name.should == :foo
+ obj.method(:qux).unbind.bind(obj).original_name.should == :foo
+ end
+
+ it "returns the source UnboundMethod's name (not the name given to define_method)" do
+ klass = Class.new { define_method(:my_inspect, ::Kernel.instance_method(:inspect)) }
+ klass.new.method(:my_inspect).original_name.should == :inspect
+ end
+
+ it "preserves the source method's name through define_method and alias" do
+ source = Class.new { def my_method; end }
+ klass = Class.new(source) do
+ define_method(:renamed, source.instance_method(:my_method))
+ alias aliased renamed
+ end
+ klass.new.method(:renamed).original_name.should == :my_method
+ klass.new.method(:aliased).original_name.should == :my_method
+ end
+
+ it "returns the source UnboundMethod's name for Kernel#is_a? and Kernel#kind_of?" do
+ klass = Class.new { define_method(:my_is_a?, ::Kernel.instance_method(:is_a?)) }
+ klass.new.method(:my_is_a?).original_name.should == :is_a?
+
+ klass = Class.new { define_method(:my_kind_of?, ::Kernel.instance_method(:kind_of?)) }
+ klass.new.method(:my_kind_of?).original_name.should == :kind_of?
+ end
+
+ it "preserves the source name when aliasing a define_method'd Kernel method" do
+ klass = Class.new do
+ define_method(:my_is_a?, ::Kernel.instance_method(:is_a?))
+ alias_method :renamed_is_a?, :my_is_a?
+ end
+ klass.new.method(:renamed_is_a?).original_name.should == :is_a?
+ end
+end
diff --git a/spec/ruby/core/method/owner_spec.rb b/spec/ruby/core/method/owner_spec.rb
new file mode 100644
index 0000000000..1cdc4edfa7
--- /dev/null
+++ b/spec/ruby/core/method/owner_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#owner" do
+ it "returns the owner of the method" do
+ "abc".method(:upcase).owner.should == String
+ end
+
+ it "returns the same owner when aliased in the same classes" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:foo).owner.should == MethodSpecs::Methods
+ obj.method(:bar).owner.should == MethodSpecs::Methods
+ end
+
+ it "returns the class/module it was defined in" do
+ MethodSpecs::C.new.method(:baz).owner.should == MethodSpecs::A
+ MethodSpecs::MySuper.new.method(:bar).owner.should == MethodSpecs::MyMod
+ end
+
+ describe "for a Method generated by respond_to_missing?" do
+ it "returns the owner of the method" do
+ @m = MethodSpecs::Methods.new
+ @m.method(:handled_via_method_missing).owner.should == MethodSpecs::Methods
+ end
+ end
+
+ it "returns the class on which public was called for a private method in ancestor" do
+ MethodSpecs::InheritedMethods::C.new.method(:derp).owner.should == MethodSpecs::InheritedMethods::C
+ end
+end
diff --git a/spec/ruby/core/method/parameters_spec.rb b/spec/ruby/core/method/parameters_spec.rb
new file mode 100644
index 0000000000..fd88e8dcb8
--- /dev/null
+++ b/spec/ruby/core/method/parameters_spec.rb
@@ -0,0 +1,317 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#parameters" do
+ class MethodSpecs::Methods
+ def one_key(a: 1); end
+ def one_keyrest(**a); end
+
+ def one_keyreq(a:); end
+ def one_nokey(**nil); end
+
+ def one_splat_one_req(*a,b); end
+ def one_splat_two_req(*a,b,c); end
+ def one_splat_one_req_with_block(*a,b,&blk); end
+
+ def one_opt_with_stabby(a=-> b { true }); end
+
+ def one_unnamed_splat(*); end
+ def one_unnamed_keyrest(**); end
+
+ def one_splat_one_block(*args, &block)
+ local_is_not_parameter = {}
+ end
+
+ ruby_version_is "4.1" do
+ eval <<-RUBY
+ def one_noblock(&nil); end
+ RUBY
+ end
+
+ def forward_parameters(...) end
+
+ def underscore_parameters(_, _, _ = 1, *_, _:, _: 2, **_, &_); end
+
+ define_method(:one_optional_defined_method) {|x = 1|}
+ end
+
+ it "returns an empty Array when the method expects no arguments" do
+ MethodSpecs::Methods.instance_method(:zero).parameters.should == []
+ end
+
+ it "returns [[:req,:name]] for a method expecting one required argument called 'name'" do
+ MethodSpecs::Methods.instance_method(:one_req).parameters.should == [[:req,:a]]
+ end
+
+ it "returns [[:req,:a],[:req,:b]] for a method expecting two required arguments called 'a' and 'b''" do
+ m = MethodSpecs::Methods.instance_method(:two_req)
+ m.parameters.should == [[:req,:a], [:req,:b]]
+ end
+
+ it "returns [[:block,:blk]] for a method expecting one block argument called 'a'" do
+ m = MethodSpecs::Methods.instance_method(:zero_with_block)
+ m.parameters.should == [[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:block,:b] for a method expecting a required argument ('a') and a block argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_with_block)
+ m.parameters.should == [[:req,:a], [:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:req,:b],[:block,:c] for a method expecting two required arguments ('a','b') and a block argument ('c')" do
+ m = MethodSpecs::Methods.instance_method(:two_req_with_block)
+ m.parameters.should == [[:req,:a], [:req,:b], [:block,:blk]]
+ end
+
+ it "returns [[:opt,:a]] for a method expecting one optional argument ('a')" do
+ m = MethodSpecs::Methods.instance_method(:one_opt)
+ m.parameters.should == [[:opt,:a]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b]] for a method expecting one required argument ('a') and one optional argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_one_opt)
+ m.parameters.should == [[:req,:a],[:opt,:b]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b]] for a method expecting one required argument ('a') and one optional argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_one_opt)
+ m.parameters.should == [[:req,:a],[:opt,:b]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b],[:opt,:c]] for a method expecting one required argument ('a') and two optional arguments ('b','c')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_two_opt)
+ m.parameters.should == [[:req,:a],[:opt,:b],[:opt,:c]]
+ end
+
+ it "returns [[:req,:a],[:req,:b],[:opt,:c]] for a method expecting two required arguments ('a','b') and one optional arguments ('c')" do
+ m = MethodSpecs::Methods.instance_method(:two_req_one_opt)
+ m.parameters.should == [[:req,:a],[:req,:b],[:opt,:c]]
+ end
+
+ it "returns [[:opt,:a],[:block,:b]] for a method expecting one required argument ('a') and one block argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:one_opt_with_block)
+ m.parameters.should == [[:opt,:a],[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b],[:block,:c]] for a method expecting one required argument ('a'), one optional argument ('b'), and a block ('c')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_one_opt_with_block)
+ m.parameters.should == [[:req,:a],[:opt,:b],[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b],[:opt,:c],[:block,:d]] for a method expecting one required argument ('a'), two optional arguments ('b','c'), and a block ('d')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_two_opt_with_block)
+ m.parameters.should == [[:req,:a],[:opt,:b],[:opt,:c],[:block,:blk]]
+ end
+
+ it "returns [[:rest,:a]] for a method expecting a single splat argument ('a')" do
+ m = MethodSpecs::Methods.instance_method(:zero_with_splat)
+ m.parameters.should == [[:rest,:a]]
+ end
+
+ it "returns [[:req,:a],[:rest,:b]] for a method expecting a splat argument ('a') and a required argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_with_splat)
+ m.parameters.should == [[:req,:a],[:rest,:b]]
+ end
+
+ it "returns [[:req,:a],[:req,:b],[:rest,:c]] for a method expecting two required arguments ('a','b') and a splat argument ('c')" do
+ m = MethodSpecs::Methods.instance_method(:two_req_with_splat)
+ m.parameters.should == [[:req,:a],[:req,:b],[:rest,:c]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b],[:rest,:c]] for a method expecting a required argument ('a','b'), an optional argument ('b'), and a splat argument ('c')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_one_opt_with_splat)
+ m.parameters.should == [[:req,:a],[:opt,:b],[:rest,:c]]
+ end
+
+ it "returns [[:req,:a],[:req,:b],[:opt,:b],[:rest,:d]] for a method expecting two required arguments ('a','b'), an optional argument ('c'), and a splat argument ('d')" do
+ m = MethodSpecs::Methods.instance_method(:two_req_one_opt_with_splat)
+ m.parameters.should == [[:req,:a],[:req,:b],[:opt,:c],[:rest,:d]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b],[:opt,:c],[:rest,:d]] for a method expecting a required argument ('a'), two optional arguments ('b','c'), and a splat argument ('d')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_two_opt_with_splat)
+ m.parameters.should == [[:req,:a],[:opt,:b],[:opt,:c],[:rest,:d]]
+ end
+
+ it "returns [[:rest,:a],[:block,:b]] for a method expecting a splat argument ('a') and a block argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:zero_with_splat_and_block)
+ m.parameters.should == [[:rest,:a],[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:rest,:b],[:block,:c]] for a method expecting a required argument ('a'), a splat argument ('b'), and a block ('c')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_with_splat_and_block)
+ m.parameters.should == [[:req,:a],[:rest,:b],[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:req,:b],[:rest,:c],[:block,:d]] for a method expecting two required arguments ('a','b'), a splat argument ('c'), and a block ('d')" do
+ m = MethodSpecs::Methods.instance_method(:two_req_with_splat_and_block)
+ m.parameters.should == [[:req,:a],[:req,:b],[:rest,:c],[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:opt,:b],[:rest,:c],[:block,:d]] for a method expecting a required argument ('a'), a splat argument ('c'), and a block ('d')" do
+ m = MethodSpecs::Methods.instance_method(:one_req_one_opt_with_splat_and_block)
+ m.parameters.should == [[:req,:a],[:opt,:b],[:rest,:c],[:block,:blk]]
+ end
+
+ it "returns [[:req,:a],[:req,:b],[:opt,:c],[:block,:d]] for a method expecting two required arguments ('a','b'), an optional argument ('c'), a splat argument ('d'), and a block ('e')" do
+ m = MethodSpecs::Methods.instance_method(:two_req_one_opt_with_splat_and_block)
+ m.parameters.should == [[:req,:a],[:req,:b],[:opt,:c],[:rest,:d],[:block,:blk]]
+ end
+
+ it "returns [[:rest,:a],[:req,:b]] for a method expecting a splat argument ('a') and a required argument ('b')" do
+ m = MethodSpecs::Methods.instance_method(:one_splat_one_req)
+ m.parameters.should == [[:rest,:a],[:req,:b]]
+ end
+
+ it "returns [[:rest,:a],[:req,:b],[:req,:c]] for a method expecting a splat argument ('a') and two required arguments ('b','c')" do
+ m = MethodSpecs::Methods.instance_method(:one_splat_two_req)
+ m.parameters.should == [[:rest,:a],[:req,:b],[:req,:c]]
+ end
+
+ it "returns [[:rest,:a],[:req,:b],[:block,:c]] for a method expecting a splat argument ('a'), a required argument ('b'), and a block ('c')" do
+ m = MethodSpecs::Methods.instance_method(:one_splat_one_req_with_block)
+ m.parameters.should == [[:rest,:a],[:req,:b],[:block,:blk]]
+ end
+
+ it "returns [[:key,:a]] for a method with a single optional keyword argument" do
+ m = MethodSpecs::Methods.instance_method(:one_key)
+ m.parameters.should == [[:key,:a]]
+ end
+
+ it "returns [[:keyrest,:a]] for a method with a keyword rest argument" do
+ m = MethodSpecs::Methods.instance_method(:one_keyrest)
+ m.parameters.should == [[:keyrest,:a]]
+ end
+
+ it "returns [[:keyreq,:a]] for a method with a single required keyword argument" do
+ m = MethodSpecs::Methods.instance_method(:one_keyreq)
+ m.parameters.should == [[:keyreq,:a]]
+ end
+
+ it "returns [[:nokey]] for a method with a single **nil parameter" do
+ m = MethodSpecs::Methods.instance_method(:one_nokey)
+ m.parameters.should == [[:nokey]]
+ end
+
+ ruby_version_is "4.1" do
+ it "returns [[:noblock]] for a method with a single &nil parameter" do
+ m = MethodSpecs::Methods.instance_method(:one_noblock)
+ m.parameters.should == [[:noblock]]
+ end
+ end
+
+ it "works with ->(){} as the value of an optional argument" do
+ m = MethodSpecs::Methods.instance_method(:one_opt_with_stabby)
+ m.parameters.should == [[:opt,:a]]
+ end
+
+ # define_method variants
+ it "returns [] for a define_method method with explicit no-args || specification" do
+ m = MethodSpecs::Methods.instance_method(:zero_defined_method)
+ m.parameters.should == []
+ end
+
+ it "returns [[:rest, :x]] for a define_method method with rest arg 'x' only" do
+ m = MethodSpecs::Methods.instance_method(:zero_with_splat_defined_method)
+ m.parameters.should == [[:rest, :x]]
+ end
+
+ it "returns [[:req, :x]] for a define_method method expecting one required argument 'x'" do
+ m = MethodSpecs::Methods.instance_method(:one_req_defined_method)
+ m.parameters.should == [[:req, :x]]
+ end
+
+ it "returns [[:req, :x], [:req, :y]] for a define_method method expecting two required arguments 'x' and 'y'" do
+ m = MethodSpecs::Methods.instance_method(:two_req_defined_method)
+ m.parameters.should == [[:req, :x], [:req, :y]]
+ end
+
+ it "returns [] for a define_method method with no args specification" do
+ m = MethodSpecs::Methods.instance_method(:no_args_defined_method)
+ m.parameters.should == []
+ end
+
+ it "returns [[:req]] for a define_method method with a grouping as its only argument" do
+ m = MethodSpecs::Methods.instance_method(:two_grouped_defined_method)
+ m.parameters.should == [[:req]]
+ end
+
+ it "returns [[:opt, :x]] for a define_method method with an optional argument 'x'" do
+ m = MethodSpecs::Methods.instance_method(:one_optional_defined_method)
+ m.parameters.should == [[:opt, :x]]
+ end
+
+ it "returns [[:rest]] for a Method generated by respond_to_missing?" do
+ m = MethodSpecs::Methods.new
+ m.method(:handled_via_method_missing).parameters.should == [[:rest]]
+ end
+
+ it "adds rest arg with name * for \"star\" argument" do
+ m = MethodSpecs::Methods.new
+ m.method(:one_unnamed_splat).parameters.should == [[:rest, :*]]
+ end
+
+ it "adds keyrest arg with ** as a name for \"double star\" argument" do
+ m = MethodSpecs::Methods.new
+ m.method(:one_unnamed_keyrest).parameters.should == [[:keyrest, :**]]
+ end
+
+ it "adds block arg with name & for anonymous block argument" do
+ object = Object.new
+ def object.foo(&)
+ end
+
+ object.method(:foo).parameters.should == [[:block, :&]]
+ end
+
+ it "returns [:rest, :*], [:keyrest, :**], [:block, :&] for forward parameters operator" do
+ m = MethodSpecs::Methods.new
+ m.method(:forward_parameters).parameters.should == [[:rest, :*], [:keyrest, :**], [:block, :&]]
+ end
+
+ it "returns the args and block for a splat and block argument" do
+ m = MethodSpecs::Methods.new
+ m.method(:one_splat_one_block).parameters.should == [[:rest, :args], [:block, :block]]
+ end
+
+ it "returns [] for a Method generated by attr_reader" do
+ m = MethodSpecs::Methods.new
+ m.method(:reader).parameters.should == []
+ end
+
+ it "return [[:req]] for a Method generated by attr_writer" do
+ m = MethodSpecs::Methods.new
+ m.method(:writer=).parameters.should == [[:req]]
+ end
+
+ it "returns all parameters defined with the name _ as _" do
+ m = MethodSpecs::Methods.instance_method(:underscore_parameters)
+ m.parameters.should == [
+ [:req, :_],
+ [:req, :_],
+ [:opt, :_],
+ [:rest, :_],
+ [:keyreq, :_],
+ [:key, :_],
+ [:keyrest, :_],
+ [:block, :_]
+ ]
+ end
+
+ it "returns [[:rest]] for core methods with variable-length argument lists" do
+ # delete! takes rest args
+ "foo".method(:delete!).parameters.should == [[:rest]]
+ end
+
+ it "returns [[:rest]] or [[:opt]] for core methods with optional arguments" do
+ # pop takes 1 optional argument
+ [
+ [[:rest]],
+ [[:opt]]
+ ].should.include?([].method(:pop).parameters)
+ end
+
+ it "returns [[:req]] for each parameter for core methods with fixed-length argument lists" do
+ "foo".method(:+).parameters.should == [[:req]]
+ end
+end
diff --git a/spec/ruby/core/method/private_spec.rb b/spec/ruby/core/method/private_spec.rb
new file mode 100644
index 0000000000..e708542b2e
--- /dev/null
+++ b/spec/ruby/core/method/private_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#private?" do
+ it "has been removed" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:my_private_method).should_not.respond_to?(:private?)
+ end
+end
diff --git a/spec/ruby/core/method/protected_spec.rb b/spec/ruby/core/method/protected_spec.rb
new file mode 100644
index 0000000000..f9e422ae3d
--- /dev/null
+++ b/spec/ruby/core/method/protected_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#protected?" do
+ it "has been removed" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:my_protected_method).should_not.respond_to?(:protected?)
+ end
+end
diff --git a/spec/ruby/core/method/public_spec.rb b/spec/ruby/core/method/public_spec.rb
new file mode 100644
index 0000000000..4cb23f4cf1
--- /dev/null
+++ b/spec/ruby/core/method/public_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#public?" do
+ it "has been removed" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:my_public_method).should_not.respond_to?(:public?)
+ end
+end
diff --git a/spec/ruby/core/method/receiver_spec.rb b/spec/ruby/core/method/receiver_spec.rb
new file mode 100644
index 0000000000..315a08d288
--- /dev/null
+++ b/spec/ruby/core/method/receiver_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#receiver" do
+ it "returns the receiver of the method" do
+ s = "abc"
+ s.method(:upcase).receiver.should.equal?(s)
+ end
+
+ it "returns the right receiver even when aliased" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:foo).receiver.should.equal?(obj)
+ obj.method(:bar).receiver.should.equal?(obj)
+ end
+
+ describe "for a Method generated by respond_to_missing?" do
+ it "returns the receiver of the method" do
+ m = MethodSpecs::Methods.new
+ m.method(:handled_via_method_missing).receiver.should.equal?(m)
+ end
+ end
+end
diff --git a/spec/ruby/core/method/shared/aliased_inspect.rb b/spec/ruby/core/method/shared/aliased_inspect.rb
new file mode 100644
index 0000000000..2a622c2f97
--- /dev/null
+++ b/spec/ruby/core/method/shared/aliased_inspect.rb
@@ -0,0 +1,31 @@
+describe :method_to_s_aliased, shared: true do
+ # @object converts a bound Method to either a Method (identity) or an
+ # UnboundMethod (-> meth { meth.unbind }), so these expectations cover both
+ # Method#to_s/#inspect and UnboundMethod#to_s/#inspect.
+
+ it "shows the original name in parentheses for an aliased method" do
+ klass = Class.new do
+ def original_method; end
+ alias_method :renamed_method, :original_method
+ end
+ @object.call(klass.new.method(:renamed_method)).send(@method).should.include? '#renamed_method(original_method)'
+ end
+
+ it "shows the source UnboundMethod's name in parentheses for a define_method'd method" do
+ klass = Class.new { define_method(:renamed_is_a?, ::Kernel.instance_method(:is_a?)) }
+ @object.call(klass.new.method(:renamed_is_a?)).send(@method).should.include? '#renamed_is_a?(is_a?)'
+ end
+
+ it "does not annotate a directly looked-up Kernel method with a shared internal name" do
+ @object.call(Object.new.method(:is_a?)).send(@method).should_not.include? '(kind_of?)'
+ @object.call(Object.new.method(:kind_of?)).send(@method).should_not.include? '(is_a?)'
+ end
+
+ it "shows the source name when aliasing a define_method'd Kernel method" do
+ klass = Class.new do
+ define_method(:my_is_a?, ::Kernel.instance_method(:is_a?))
+ alias_method :renamed_is_a?, :my_is_a?
+ end
+ @object.call(klass.new.method(:renamed_is_a?)).send(@method).should.include? '#renamed_is_a?(is_a?)'
+ end
+end
diff --git a/spec/ruby/core/method/shared/call.rb b/spec/ruby/core/method/shared/call.rb
new file mode 100644
index 0000000000..41ee2b06cb
--- /dev/null
+++ b/spec/ruby/core/method/shared/call.rb
@@ -0,0 +1,51 @@
+describe :method_call, shared: true do
+ it "invokes the method with the specified arguments, returning the method's return value" do
+ m = 12.method("+")
+ m.send(@method, 3).should == 15
+ m.send(@method, 20).should == 32
+
+ m = MethodSpecs::Methods.new.method(:attr=)
+ m.send(@method, 42).should == 42
+ end
+
+ it "raises an ArgumentError when given incorrect number of arguments" do
+ -> {
+ MethodSpecs::Methods.new.method(:two_req).send(@method, 1, 2, 3)
+ }.should.raise(ArgumentError)
+ -> {
+ MethodSpecs::Methods.new.method(:two_req).send(@method, 1)
+ }.should.raise(ArgumentError)
+ end
+
+ describe "for a Method generated by respond_to_missing?" do
+ it "invokes method_missing with the specified arguments and returns the result" do
+ @m = MethodSpecs::Methods.new
+ meth = @m.method(:handled_via_method_missing)
+ meth.send(@method, :argument).should == [:argument]
+ end
+
+ it "invokes method_missing with the method name and the specified arguments" do
+ @m = MethodSpecs::Methods.new
+ meth = @m.method(:handled_via_method_missing)
+
+ @m.should_receive(:method_missing).with(:handled_via_method_missing, :argument)
+ meth.send(@method, :argument)
+ end
+
+ it "invokes method_missing dynamically" do
+ @m = MethodSpecs::Methods.new
+ meth = @m.method(:handled_via_method_missing)
+
+ def @m.method_missing(*); :changed; end
+ meth.send(@method, :argument).should == :changed
+ end
+
+ it "does not call the original method name even if it now exists" do
+ @m = MethodSpecs::Methods.new
+ meth = @m.method(:handled_via_method_missing)
+
+ def @m.handled_via_method_missing(*); :not_called; end
+ meth.send(@method, :argument).should == [:argument]
+ end
+ end
+end
diff --git a/spec/ruby/core/method/shared/dup.rb b/spec/ruby/core/method/shared/dup.rb
new file mode 100644
index 0000000000..eee790890a
--- /dev/null
+++ b/spec/ruby/core/method/shared/dup.rb
@@ -0,0 +1,32 @@
+describe :method_dup, shared: true do
+ it "returns a copy of self" do
+ a = Object.new.method(:method)
+ b = a.send(@method)
+
+ a.should == b
+ a.should_not.equal?(b)
+ end
+
+ ruby_version_is "3.4" do
+ it "copies instance variables" do
+ method = Object.new.method(:method)
+ method.instance_variable_set(:@ivar, 1)
+ cl = method.send(@method)
+ cl.instance_variables.should == [:@ivar]
+ end
+
+ it "copies the finalizer" do
+ code = <<-'RUBY'
+ obj = Object.new.method(:method)
+
+ ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" })
+
+ obj.clone
+
+ exit 0
+ RUBY
+
+ ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"]
+ end
+ end
+end
diff --git a/spec/ruby/core/method/shared/eql.rb b/spec/ruby/core/method/shared/eql.rb
new file mode 100644
index 0000000000..3c340202b7
--- /dev/null
+++ b/spec/ruby/core/method/shared/eql.rb
@@ -0,0 +1,94 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :method_equal, shared: true do
+ before :each do
+ @m = MethodSpecs::Methods.new
+ @m_foo = @m.method(:foo)
+ @m2 = MethodSpecs::Methods.new
+ @a = MethodSpecs::A.new
+ end
+
+ it "returns true if methods are the same" do
+ m2 = @m.method(:foo)
+
+ @m_foo.send(@method, @m_foo).should == true
+ @m_foo.send(@method, m2).should == true
+ end
+
+ it "returns true on aliased methods" do
+ m_bar = @m.method(:bar)
+
+ m_bar.send(@method, @m_foo).should == true
+ end
+
+ it "returns true if the two core methods are aliases" do
+ s = "hello"
+ a = s.method(:size)
+ b = s.method(:length)
+ a.send(@method, b).should == true
+ end
+
+ it "returns false on a method which is neither aliased nor the same method" do
+ m2 = @m.method(:zero)
+
+ @m_foo.send(@method, m2).should == false
+ end
+
+ it "returns false for a method which is not bound to the same object" do
+ m2_foo = @m2.method(:foo)
+ a_baz = @a.method(:baz)
+
+ @m_foo.send(@method, m2_foo).should == false
+ @m_foo.send(@method, a_baz).should == false
+ end
+
+ it "returns false if the two methods are bound to the same object but were defined independently" do
+ m2 = @m.method(:same_as_foo)
+ @m_foo.send(@method, m2).should == false
+ end
+
+ it "returns true if a method was defined using the other one" do
+ MethodSpecs::Methods.send :define_method, :defined_foo, MethodSpecs::Methods.instance_method(:foo)
+ m2 = @m.method(:defined_foo)
+ @m_foo.send(@method, m2).should == true
+ end
+
+ it "returns false if comparing a method defined via define_method and def" do
+ defn = @m.method(:zero)
+ defined = @m.method(:zero_defined_method)
+
+ defn.send(@method, defined).should == false
+ defined.send(@method, defn).should == false
+ end
+
+ describe 'missing methods' do
+ it "returns true for the same method missing" do
+ miss1 = @m.method(:handled_via_method_missing)
+ miss1bis = @m.method(:handled_via_method_missing)
+ miss2 = @m.method(:also_handled)
+
+ miss1.send(@method, miss1bis).should == true
+ miss1.send(@method, miss2).should == false
+ end
+
+ it 'calls respond_to_missing? with true to include private methods' do
+ @m.should_receive(:respond_to_missing?).with(:some_missing_method, true).and_return(true)
+ @m.method(:some_missing_method)
+ end
+ end
+
+ it "returns false if the two methods are bound to different objects, have the same names, and identical bodies" do
+ a = MethodSpecs::Eql.instance_method(:same_body)
+ b = MethodSpecs::Eql2.instance_method(:same_body)
+ a.send(@method, b).should == false
+ end
+
+ it "returns false if the argument is not a Method object" do
+ String.instance_method(:size).send(@method, 7).should == false
+ end
+
+ it "returns false if the argument is an unbound version of self" do
+ method(:load).send(@method, method(:load).unbind).should == false
+ end
+end
diff --git a/spec/ruby/core/method/shared/to_s.rb b/spec/ruby/core/method/shared/to_s.rb
new file mode 100644
index 0000000000..bfb58e6896
--- /dev/null
+++ b/spec/ruby/core/method/shared/to_s.rb
@@ -0,0 +1,81 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :method_to_s, shared: true do
+ before :each do
+ @m = MethodSpecs::MySub.new.method :bar
+ @string = @m.send(@method)
+ end
+
+ it "returns a String" do
+ @m.send(@method).should.is_a?(String)
+ end
+
+ it "returns a String for methods defined with attr_accessor" do
+ m = MethodSpecs::Methods.new.method :attr
+ m.send(@method).should.is_a?(String)
+ end
+
+ it "returns a String containing 'Method'" do
+ @string.should =~ /\bMethod\b/
+ end
+
+ it "returns a String containing the method name" do
+ @string.should =~ /\#bar/
+ end
+
+ it "returns a String containing method arguments" do
+ obj = MethodSpecs::Methods.new
+ obj.method(:zero).send(@method).should.include?("()")
+ obj.method(:one_req).send(@method).should.include?("(a)")
+ obj.method(:one_req_named).send(@method).should.include?("(a:)")
+ obj.method(:zero_with_block).send(@method).should.include?("(&blk)")
+ obj.method(:one_opt).send(@method).should.include?("(a=...)")
+ obj.method(:one_opt_named).send(@method).should.include?("(a: ...)")
+ obj.method(:zero_with_splat).send(@method).should.include?("(*a)")
+ obj.method(:zero_with_double_splat).send(@method).should.include?("(**a)")
+ obj.method(:one_req_one_opt_with_splat_and_block).send(@method).should.include?("(a, b=..., *c, &blk)")
+ end
+
+ it "returns a String containing the Module the method is defined in" do
+ @string.should =~ /MethodSpecs::MyMod/
+ end
+
+ it "returns a String containing the Module the method is referenced from" do
+ @string.should =~ /MethodSpecs::MySub/
+ end
+
+ it "returns a String including all details" do
+ @string.should.start_with? "#<Method: MethodSpecs::MySub(MethodSpecs::MyMod)#bar"
+ end
+
+ it "does not show the defining module if it is the same as the receiver class" do
+ MethodSpecs::A.new.method(:baz).send(@method).should.start_with? "#<Method: MethodSpecs::A#baz"
+ end
+
+ it "returns a String containing the Module containing the method if object has a singleton class but method is not defined in the singleton class" do
+ obj = MethodSpecs::MySub.new
+ obj.singleton_class
+ @m = obj.method(:bar)
+ @string = @m.send(@method)
+ @string.should.start_with? "#<Method: MethodSpecs::MySub(MethodSpecs::MyMod)#bar"
+
+ c = MethodSpecs::MySub.dup
+ m = Module.new{def bar; end}
+ c.extend(m)
+ @string = c.method(:bar).send(@method)
+ @string.should.start_with? "#<Method: #<Class:#{c.inspect}>(#{m.inspect})#bar"
+ end
+
+ it "returns a String containing the singleton class if method is defined in the singleton class" do
+ obj = MethodSpecs::MySub.new
+ def obj.bar; end
+ @m = obj.method(:bar)
+ @string = @m.send(@method).sub(/0x\h+/, '0xXXXXXX')
+ @string.should.start_with? "#<Method: #<MethodSpecs::MySub:0xXXXXXX>.bar"
+ end
+
+ it "shows the metaclass and the owner for a Module instance method retrieved from a class" do
+ String.method(:include).inspect.should.start_with?("#<Method: #<Class:String>(Module)#include")
+ end
+end
diff --git a/spec/ruby/core/method/source_location_spec.rb b/spec/ruby/core/method/source_location_spec.rb
new file mode 100644
index 0000000000..22fcb98c74
--- /dev/null
+++ b/spec/ruby/core/method/source_location_spec.rb
@@ -0,0 +1,120 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#source_location" do
+ before :each do
+ @method = MethodSpecs::SourceLocation.method(:location)
+ end
+
+ it "returns an Array" do
+ @method.source_location.should.instance_of?(Array)
+ end
+
+ it "sets the first value to the path of the file in which the method was defined" do
+ file = @method.source_location.first
+ file.should.instance_of?(String)
+ file.should == File.realpath('fixtures/classes.rb', __dir__)
+ end
+
+ it "sets the last value to an Integer representing the line on which the method was defined" do
+ line = @method.source_location.last
+ line.should.instance_of?(Integer)
+ line.should == 5
+ end
+
+ it "returns the last place the method was defined" do
+ MethodSpecs::SourceLocation.method(:redefined).source_location.last.should == 13
+ end
+
+ it "returns the location of the original method even if it was aliased" do
+ MethodSpecs::SourceLocation.new.method(:aka).source_location.last.should == 17
+ end
+
+ it "works for methods defined with a block" do
+ line = nil
+ klass = Class.new do
+ line = __LINE__ + 1
+ define_method(:f) { }
+ end
+
+ method = klass.new.method(:f)
+ method.source_location[0].should =~ /#{__FILE__}/
+ method.source_location[1].should == line
+ end
+
+ it "works for methods defined with a Method" do
+ line = nil
+ klass = Class.new do
+ line = __LINE__ + 1
+ def f
+ end
+ define_method :g, new.method(:f)
+ end
+
+ method = klass.new.method(:g)
+ method.source_location[0].should =~ /#{__FILE__}/
+ method.source_location[1].should == line
+ end
+
+ it "works for methods defined with an UnboundMethod" do
+ line = nil
+ klass = Class.new do
+ line = __LINE__ + 1
+ def f
+ end
+ define_method :g, instance_method(:f)
+ end
+
+ method = klass.new.method(:g)
+ method.source_location[0].should =~ /#{__FILE__}/
+ method.source_location[1].should == line
+ end
+
+ it "works for methods whose visibility has been overridden in a subclass" do
+ line = nil
+ superclass = Class.new do
+ line = __LINE__ + 1
+ def f
+ end
+ end
+ subclass = Class.new(superclass) do
+ private :f
+ end
+
+ method = subclass.new.method(:f)
+ method.source_location[0].should =~ /#{__FILE__}/
+ method.source_location[1].should == line
+ end
+
+ it "works for core methods where it returns nil or <internal:" do
+ loc = method(:__id__).source_location
+ if loc == nil
+ loc.should == nil
+ else
+ loc[0].should.start_with?('<internal:')
+ loc[1].should.is_a?(Integer)
+ end
+
+ loc = method(:tap).source_location
+ if loc == nil
+ loc.should == nil
+ else
+ loc[0].should.start_with?('<internal:')
+ loc[1].should.is_a?(Integer)
+ end
+ end
+
+ it "works for eval with a given line" do
+ c = Class.new do
+ eval('def self.m; end', nil, "foo", 100)
+ end
+ c.method(:m).source_location.should == ["foo", 100]
+ end
+
+ describe "for a Method generated by respond_to_missing?" do
+ it "returns nil" do
+ m = MethodSpecs::Methods.new
+ m.method(:handled_via_method_missing).source_location.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/method/super_method_spec.rb b/spec/ruby/core/method/super_method_spec.rb
new file mode 100644
index 0000000000..c63a7aaa0f
--- /dev/null
+++ b/spec/ruby/core/method/super_method_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#super_method" do
+ it "returns the method that would be called by super in the method" do
+ obj = MethodSpecs::C.new
+ obj.extend MethodSpecs::OverrideAgain
+ meth = obj.method(:overridden)
+
+ s_meth = meth.super_method
+ s_meth.owner.should == MethodSpecs::C
+ s_meth.receiver.should == obj
+ s_meth.name.should == :overridden
+
+ ss_meth = meth.super_method.super_method
+ ss_meth.owner.should == MethodSpecs::BetweenBAndC
+ ss_meth.receiver.should == obj
+ ss_meth.name.should == :overridden
+
+ sss_meth = meth.super_method.super_method.super_method
+ sss_meth.owner.should == MethodSpecs::B
+ sss_meth.receiver.should == obj
+ sss_meth.name.should == :overridden
+ end
+
+ it "returns nil when there's no super method in the parent" do
+ method = Object.new.method(:method)
+ method.super_method.should == nil
+ end
+
+ it "returns nil when the parent's method is removed" do
+ klass = Class.new do
+ def overridden; end
+ end
+ sub = Class.new(klass) do
+ def overridden; end
+ end
+ object = sub.new
+ method = object.method(:overridden)
+
+ klass.class_eval { undef :overridden }
+
+ method.super_method.should == nil
+ end
+
+ # https://github.com/jruby/jruby/issues/7240
+ context "after changing an inherited methods visibility" do
+ it "calls the proper super method" do
+ MethodSpecs::InheritedMethods::C.new.derp.should == 'BA'
+ end
+
+ it "returns the expected super_method" do
+ method = MethodSpecs::InheritedMethods::C.new.method(:derp)
+ method.super_method.owner.should == MethodSpecs::InheritedMethods::A
+ end
+ end
+
+ context "after aliasing an inherited method" do
+ it "returns the expected super_method" do
+ method = MethodSpecs::InheritedMethods::C.new.method(:meow)
+ method.super_method.owner.should == MethodSpecs::InheritedMethods::A
+ end
+ end
+end
diff --git a/spec/ruby/core/method/to_proc_spec.rb b/spec/ruby/core/method/to_proc_spec.rb
new file mode 100644
index 0000000000..4993cce239
--- /dev/null
+++ b/spec/ruby/core/method/to_proc_spec.rb
@@ -0,0 +1,104 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#to_proc" do
+ before :each do
+ ScratchPad.record []
+
+ @m = MethodSpecs::Methods.new
+ @meth = @m.method(:foo)
+ end
+
+ it "returns a Proc object corresponding to the method" do
+ @meth.to_proc.kind_of?(Proc).should == true
+ end
+
+ it "returns a Proc which does not depends on the value of self" do
+ 3.instance_exec(4, &5.method(:+)).should == 9
+ end
+
+
+ it "returns a Proc object with the correct arity" do
+ # This may seem redundant but this bug has cropped up in jruby, mri and yarv.
+ # http://jira.codehaus.org/browse/JRUBY-124
+ [ :zero, :one_req, :two_req,
+ :zero_with_block, :one_req_with_block, :two_req_with_block,
+ :one_opt, :one_req_one_opt, :one_req_two_opt, :two_req_one_opt,
+ :one_opt_with_block, :one_req_one_opt_with_block, :one_req_two_opt_with_block, :two_req_one_opt_with_block,
+ :zero_with_splat, :one_req_with_splat, :two_req_with_splat,
+ :one_req_one_opt_with_splat, :one_req_two_opt_with_splat, :two_req_one_opt_with_splat,
+ :zero_with_splat_and_block, :one_req_with_splat_and_block, :two_req_with_splat_and_block,
+ :one_req_one_opt_with_splat_and_block, :one_req_two_opt_with_splat_and_block, :two_req_one_opt_with_splat_and_block
+ ].each do |m|
+ @m.method(m).to_proc.arity.should == @m.method(m).arity
+ end
+ end
+
+ it "returns a proc that can be used by define_method" do
+ x = +'test'
+ to_s = class << x
+ define_method :foo, method(:to_s).to_proc
+ to_s
+ end
+
+ x.foo.should == to_s
+ end
+
+ it "returns a proc that can be yielded to" do
+ x = Object.new
+ def x.foo(*a); a; end
+ def x.bar; yield; end
+ def x.baz(*a); yield(*a); end
+
+ m = x.method :foo
+ x.bar(&m).should == []
+ x.baz(1,2,3,&m).should == [1,2,3]
+ end
+
+ it "returns a proc whose binding has the same receiver as the method" do
+ @meth.receiver.should == @meth.to_proc.binding.receiver
+ end
+
+ # #5926
+ it "returns a proc that can receive a block" do
+ x = Object.new
+ def x.foo; yield 'bar'; end
+
+ m = x.method :foo
+ result = nil
+ m.to_proc.call {|val| result = val}
+ result.should == 'bar'
+ end
+
+ it "can be called directly and not unwrap arguments like a block" do
+ obj = MethodSpecs::ToProcBeta.new
+ obj.to_proc.call([1]).should == [1]
+ end
+
+ it "should correct handle arguments (unwrap)" do
+ obj = MethodSpecs::ToProcBeta.new
+
+ array = [[1]]
+ array.each(&obj)
+ ScratchPad.recorded.should == [[1]]
+ end
+
+ it "executes method with whole array (one argument)" do
+ obj = MethodSpecs::ToProcBeta.new
+
+ array = [[1, 2]]
+ array.each(&obj)
+ ScratchPad.recorded.should == [[1, 2]]
+ end
+
+ it "returns a proc that properly invokes module methods with super" do
+ m1 = Module.new { def foo(ary); ary << :m1; end; }
+ m2 = Module.new { def foo(ary = []); super(ary); ary << :m2; end; }
+ c2 = Class.new do
+ include m1
+ include m2
+ end
+
+ c2.new.method(:foo).to_proc.call.should == %i[m1 m2]
+ end
+end
diff --git a/spec/ruby/core/method/to_s_spec.rb b/spec/ruby/core/method/to_s_spec.rb
new file mode 100644
index 0000000000..ba0b4fa3c6
--- /dev/null
+++ b/spec/ruby/core/method/to_s_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+require_relative 'shared/aliased_inspect'
+
+describe "Method#to_s" do
+ it_behaves_like :method_to_s, :to_s
+ it_behaves_like :method_to_s_aliased, :to_s, -> meth { meth }
+end
diff --git a/spec/ruby/core/method/unbind_spec.rb b/spec/ruby/core/method/unbind_spec.rb
new file mode 100644
index 0000000000..994c3a8879
--- /dev/null
+++ b/spec/ruby/core/method/unbind_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Method#unbind" do
+ before :each do
+ @normal = MethodSpecs::Methods.new
+ @normal_m = @normal.method :foo
+ @normal_um = @normal_m.unbind
+ @pop_um = MethodSpecs::MySub.new.method(:bar).unbind
+ @string = @pop_um.inspect.sub(/0x\w+/, '0xXXXXXX')
+ end
+
+ it "returns an UnboundMethod" do
+ @normal_um.should.is_a?(UnboundMethod)
+ end
+
+ describe "#inspect" do
+ it "returns a String containing 'UnboundMethod'" do
+ @string.should =~ /\bUnboundMethod\b/
+ end
+
+ it "returns a String containing the method name" do
+ @string.should =~ /\#bar/
+ end
+
+ it "returns a String containing the Module the method is defined in" do
+ @string.should =~ /MethodSpecs::MyMod/
+ end
+
+ it "returns a String containing the Module the method is referenced from" do
+ @string.should =~ /MethodSpecs::MyMod/
+ end
+ end
+
+ it "keeps the origin singleton class if there is one" do
+ obj = Object.new
+ def obj.foo
+ end
+ obj.method(:foo).unbind.inspect.should.start_with?("#<UnboundMethod: #{obj.singleton_class}#foo")
+ end
+
+ specify "rebinding UnboundMethod to Method's obj produces exactly equivalent Methods" do
+ @normal_um.bind(@normal).should == @normal_m
+ @normal_m.should == @normal_um.bind(@normal)
+ end
+end
diff --git a/spec/ruby/core/module/alias_method_spec.rb b/spec/ruby/core/module/alias_method_spec.rb
new file mode 100644
index 0000000000..852879cc8a
--- /dev/null
+++ b/spec/ruby/core/module/alias_method_spec.rb
@@ -0,0 +1,171 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#alias_method" do
+ before :each do
+ @class = Class.new(ModuleSpecs::Aliasing)
+ @object = @class.new
+ end
+
+ it "makes a copy of the method" do
+ @class.make_alias :uno, :public_one
+ @class.make_alias :double, :public_two
+ @object.uno.should == @object.public_one
+ @object.double(12).should == @object.public_two(12)
+ end
+
+ it "creates methods that are == to each other" do
+ @class.make_alias :uno, :public_one
+ @object.method(:uno).should == @object.method(:public_one)
+ end
+
+ it "preserves the arguments information of the original methods" do
+ @class.make_alias :uno, :public_one
+ @class.make_alias :double, :public_two
+ @class.instance_method(:uno).parameters.should == @class.instance_method(:public_one).parameters
+ @class.instance_method(:double).parameters.should == @class.instance_method(:public_two).parameters
+ end
+
+ it "retains method visibility" do
+ @class.make_alias :private_ichi, :private_one
+ -> { @object.private_one }.should.raise(NameError)
+ -> { @object.private_ichi }.should.raise(NameError)
+ @class.make_alias :public_ichi, :public_one
+ @object.public_ichi.should == @object.public_one
+ @class.make_alias :protected_ichi, :protected_one
+ -> { @object.protected_ichi }.should.raise(NameError)
+ end
+
+ it "handles aliasing a stub that changes visibility" do
+ @class.__send__ :public, :private_one
+ @class.make_alias :was_private_one, :private_one
+ @object.was_private_one.should == 1
+ end
+
+ it "handles aliasing a method only present in a refinement" do
+ c = @class
+ Module.new do
+ refine c do
+ def uno_refined_method
+ end
+ alias_method :double_refined_method, :uno_refined_method
+ instance_method(:uno_refined_method).should == instance_method(:double_refined_method)
+ end
+ end
+ end
+
+ it "fails if origin method not found" do
+ -> { @class.make_alias :ni, :san }.should.raise(NameError) { |e|
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ it "raises FrozenError if frozen" do
+ @class.freeze
+ -> { @class.make_alias :uno, :public_one }.should.raise(FrozenError)
+ end
+
+ it "converts the names using #to_str" do
+ @class.make_alias "un", "public_one"
+ @class.make_alias :deux, "public_one"
+ @class.make_alias "trois", :public_one
+ @class.make_alias :quatre, :public_one
+ name = mock('cinq')
+ name.should_receive(:to_str).any_number_of_times.and_return("cinq")
+ @class.make_alias name, "public_one"
+ @class.make_alias "cinq", name
+ end
+
+ it "raises a TypeError when the given name can't be converted using to_str" do
+ -> { @class.make_alias mock('x'), :public_one }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given name raises a NoMethodError during type coercion using to_str" do
+ obj = mock("mock-name")
+ obj.should_receive(:to_str).and_raise(NoMethodError)
+ -> { @class.make_alias obj, :public_one }.should.raise(NoMethodError)
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:alias_method)
+ end
+
+ describe "returned value" do
+ it "returns symbol of the defined method name" do
+ @class.send(:alias_method, :checking_return_value, :public_one).should.equal?(:checking_return_value)
+ @class.send(:alias_method, 'checking_return_value', :public_one).should.equal?(:checking_return_value)
+ end
+ end
+
+ it "works in module" do
+ ModuleSpecs::Allonym.new.publish.should == :report
+ end
+
+ it "works on private module methods in a module that has been reopened" do
+ ModuleSpecs::ReopeningModule.foo.should == true
+ -> { ModuleSpecs::ReopeningModule.foo2 }.should_not.raise(NoMethodError)
+ end
+
+ it "accesses a method defined on Object from Kernel" do
+ Kernel.public_instance_methods(true).should_not.include?(:module_specs_public_method_on_object)
+
+ Kernel.public_instance_methods(false).should.include?(:module_specs_alias_on_kernel)
+ Object.public_instance_methods(true).should.include?(:module_specs_alias_on_kernel)
+ end
+
+ it "can call a method with super aliased twice" do
+ ModuleSpecs::AliasingSuper::Target.new.super_call(1).should == 1
+ end
+
+ it "preserves original super call after alias redefine" do
+ ModuleSpecs::AliasingSuper::RedefineAfterAlias.new.alias_super_call(1).should == 1
+ end
+
+ describe "aliasing special methods" do
+ before :all do
+ @class = ModuleSpecs::Aliasing
+ @subclass = ModuleSpecs::AliasingSubclass
+ end
+
+ it "keeps initialize private when aliasing" do
+ @class.make_alias(:initialize, :public_one)
+ @class.private_instance_methods.include?(:initialize).should == true
+
+ @subclass.make_alias(:initialize, :public_one)
+ @subclass.private_instance_methods.include?(:initialize).should == true
+ end
+
+ it "keeps initialize_copy private when aliasing" do
+ @class.make_alias(:initialize_copy, :public_one)
+ @class.private_instance_methods.include?(:initialize_copy).should == true
+
+ @subclass.make_alias(:initialize_copy, :public_one)
+ @subclass.private_instance_methods.include?(:initialize_copy).should == true
+ end
+
+ it "keeps initialize_clone private when aliasing" do
+ @class.make_alias(:initialize_clone, :public_one)
+ @class.private_instance_methods.include?(:initialize_clone).should == true
+
+ @subclass.make_alias(:initialize_clone, :public_one)
+ @subclass.private_instance_methods.include?(:initialize_clone).should == true
+ end
+
+ it "keeps initialize_dup private when aliasing" do
+ @class.make_alias(:initialize_dup, :public_one)
+ @class.private_instance_methods.include?(:initialize_dup).should == true
+
+ @subclass.make_alias(:initialize_dup, :public_one)
+ @subclass.private_instance_methods.include?(:initialize_dup).should == true
+ end
+
+ it "keeps respond_to_missing? private when aliasing" do
+ @class.make_alias(:respond_to_missing?, :public_one)
+ @class.private_instance_methods.include?(:respond_to_missing?).should == true
+
+ @subclass.make_alias(:respond_to_missing?, :public_one)
+ @subclass.private_instance_methods.include?(:respond_to_missing?).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/module/ancestors_spec.rb b/spec/ruby/core/module/ancestors_spec.rb
new file mode 100644
index 0000000000..f85884a4f3
--- /dev/null
+++ b/spec/ruby/core/module/ancestors_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#ancestors" do
+ it "returns a list of modules included in self (including self)" do
+ BasicObject.ancestors.should == [BasicObject]
+ ModuleSpecs.ancestors.should == [ModuleSpecs]
+ ModuleSpecs::Basic.ancestors.should == [ModuleSpecs::Basic]
+ ModuleSpecs::Super.ancestors.should == [ModuleSpecs::Super, ModuleSpecs::Basic]
+ if defined?(Ruby::Box) && Ruby::Box.enabled?
+ ModuleSpecs.without_test_modules(ModuleSpecs::Parent.ancestors).should ==
+ [ModuleSpecs::Parent, Object, Ruby::Box::Loader, Kernel, BasicObject]
+ ModuleSpecs.without_test_modules(ModuleSpecs::Child.ancestors).should ==
+ [ModuleSpecs::Child, ModuleSpecs::Super, ModuleSpecs::Basic, ModuleSpecs::Parent, Object, Ruby::Box::Loader, Kernel, BasicObject]
+ else
+ ModuleSpecs.without_test_modules(ModuleSpecs::Parent.ancestors).should ==
+ [ModuleSpecs::Parent, Object, Kernel, BasicObject]
+ ModuleSpecs.without_test_modules(ModuleSpecs::Child.ancestors).should ==
+ [ModuleSpecs::Child, ModuleSpecs::Super, ModuleSpecs::Basic, ModuleSpecs::Parent, Object, Kernel, BasicObject]
+ end
+ end
+
+ it "returns only modules and classes" do
+ class << ModuleSpecs::Child; self; end.ancestors.to_set.should >= Set[ModuleSpecs::Internal, Class, Module, Object, Kernel]
+ end
+
+ it "has 1 entry per module or class" do
+ ModuleSpecs::Parent.ancestors.should == ModuleSpecs::Parent.ancestors.uniq
+ end
+
+ it "returns a module that is included later into a nested module as well" do
+ m1 = Module.new
+ m2 = Module.new
+ m3 = Module.new do
+ include m2
+ end
+ m2.include m1 # should be after m3 includes m2
+
+ m3.ancestors.should == [m3, m2, m1]
+ end
+
+ describe "when called on a singleton class" do
+ it "includes the singleton classes of ancestors" do
+ parent = Class.new
+ child = Class.new(parent)
+ schild = child.singleton_class
+
+ schild.ancestors.to_set.should >= Set[schild,
+ parent.singleton_class,
+ Object.singleton_class,
+ BasicObject.singleton_class,
+ Class,
+ Module,
+ Object,
+ Kernel,
+ BasicObject]
+
+ end
+
+ describe 'for a standalone module' do
+ it 'does not include Class' do
+ s_mod = ModuleSpecs.singleton_class
+ s_mod.ancestors.should_not.include?(Class)
+ end
+
+ it 'does not include other singleton classes' do
+ s_standalone_mod = ModuleSpecs.singleton_class
+ s_module = Module.singleton_class
+ s_object = Object.singleton_class
+ s_basic_object = BasicObject.singleton_class
+
+ (s_standalone_mod.ancestors & [s_module, s_object, s_basic_object]).should.empty?
+ end
+
+ it 'includes its own singleton class' do
+ s_mod = ModuleSpecs.singleton_class
+
+ s_mod.ancestors.should.include?(s_mod)
+ end
+
+ it 'includes standard chain' do
+ s_mod = ModuleSpecs.singleton_class
+
+ s_mod.ancestors.to_set.should >= Set[Module, Object, Kernel, BasicObject]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/module/append_features_spec.rb b/spec/ruby/core/module/append_features_spec.rb
new file mode 100644
index 0000000000..4d2207330d
--- /dev/null
+++ b/spec/ruby/core/module/append_features_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#append_features" do
+ it "is a private method" do
+ Module.private_instance_methods(false).should.include?(:append_features)
+ end
+
+ describe "on Class" do
+ it "is undefined" do
+ Class.private_instance_methods(true).should_not.include?(:append_features)
+ end
+
+ it "raises a TypeError if calling after rebinded to Class" do
+ -> {
+ Module.instance_method(:append_features).bind(Class.new).call Module.new
+ }.should.raise(TypeError)
+ end
+ end
+
+ it "gets called when self is included in another module/class" do
+ begin
+ m = Module.new do
+ def self.append_features(mod)
+ $appended_to = mod
+ end
+ end
+
+ c = Class.new do
+ include m
+ end
+
+ $appended_to.should == c
+ ensure
+ $appended_to = nil
+ end
+ end
+
+ it "raises an ArgumentError on a cyclic include" do
+ -> {
+ ModuleSpecs::CyclicAppendA.send(:append_features, ModuleSpecs::CyclicAppendA)
+ }.should.raise(ArgumentError)
+
+ -> {
+ ModuleSpecs::CyclicAppendB.send(:append_features, ModuleSpecs::CyclicAppendA)
+ }.should.raise(ArgumentError)
+
+ end
+
+ describe "when other is frozen" do
+ before :each do
+ @receiver = Module.new
+ @other = Module.new.freeze
+ end
+
+ it "raises a FrozenError before appending self" do
+ -> { @receiver.send(:append_features, @other) }.should.raise(FrozenError)
+ @other.ancestors.should_not.include?(@receiver)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/attr_accessor_spec.rb b/spec/ruby/core/module/attr_accessor_spec.rb
new file mode 100644
index 0000000000..a608760cf2
--- /dev/null
+++ b/spec/ruby/core/module/attr_accessor_spec.rb
@@ -0,0 +1,112 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/attr_added'
+
+describe "Module#attr_accessor" do
+ it "creates a getter and setter for each given attribute name" do
+ c = Class.new do
+ attr_accessor :a, "b"
+ end
+
+ o = c.new
+
+ ['a','b'].each do |x|
+ o.respond_to?(x).should == true
+ o.respond_to?("#{x}=").should == true
+ end
+
+ o.a = "a"
+ o.a.should == "a"
+
+ o.b = "b"
+ o.b.should == "b"
+ o.a = o.b = nil
+
+ o.send(:a=,"a")
+ o.send(:a).should == "a"
+
+ o.send(:b=, "b")
+ o.send(:b).should == "b"
+ end
+
+ it "not allows creating an attr_accessor on an immediate class" do
+ class TrueClass
+ attr_accessor :spec_attr_accessor
+ end
+
+ -> { true.spec_attr_accessor = "a" }.should.raise(FrozenError)
+ end
+
+ it "raises FrozenError if the receiver if frozen" do
+ c = Class.new do
+ attr_accessor :foo
+ end
+ obj = c.new
+ obj.foo = 1
+ obj.foo.should == 1
+
+ obj.freeze
+ -> { obj.foo = 42 }.should.raise(FrozenError)
+ obj.foo.should == 1
+ end
+
+ it "converts non string/symbol names to strings using to_str" do
+ (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test")
+ c = Class.new do
+ attr_accessor o
+ end
+
+ c.new.respond_to?("test").should == true
+ c.new.respond_to?("test=").should == true
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ o = mock('o')
+ -> { Class.new { attr_accessor o } }.should.raise(TypeError)
+ (o = mock('123')).should_receive(:to_str).and_return(123)
+ -> { Class.new { attr_accessor o } }.should.raise(TypeError)
+ end
+
+ it "applies current visibility to methods created" do
+ c = Class.new do
+ protected
+ attr_accessor :foo
+ end
+
+ -> { c.new.foo }.should.raise(NoMethodError)
+ -> { c.new.foo=1 }.should.raise(NoMethodError)
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:attr_accessor)
+ end
+
+ it "returns an array of defined method names as symbols" do
+ Class.new do
+ (attr_accessor :foo, 'bar').should == [:foo, :foo=, :bar, :bar=]
+ end
+ end
+
+ describe "on immediates" do
+ before :each do
+ class Integer
+ attr_accessor :foobar
+ end
+ end
+
+ after :each do
+ if Integer.method_defined?(:foobar)
+ Integer.send(:remove_method, :foobar)
+ end
+ if Integer.method_defined?(:foobar=)
+ Integer.send(:remove_method, :foobar=)
+ end
+ end
+
+ it "can read through the accessor" do
+ 1.foobar.should == nil
+ end
+ end
+
+ it_behaves_like :module_attr_added, :attr_accessor
+end
diff --git a/spec/ruby/core/module/attr_reader_spec.rb b/spec/ruby/core/module/attr_reader_spec.rb
new file mode 100644
index 0000000000..2b4ca2100e
--- /dev/null
+++ b/spec/ruby/core/module/attr_reader_spec.rb
@@ -0,0 +1,73 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/attr_added'
+
+describe "Module#attr_reader" do
+ it "creates a getter for each given attribute name" do
+ c = Class.new do
+ attr_reader :a, "b"
+
+ def initialize
+ @a = "test"
+ @b = "test2"
+ end
+ end
+
+ o = c.new
+ %w{a b}.each do |x|
+ o.respond_to?(x).should == true
+ o.respond_to?("#{x}=").should == false
+ end
+
+ o.a.should == "test"
+ o.b.should == "test2"
+ o.send(:a).should == "test"
+ o.send(:b).should == "test2"
+ end
+
+ it "not allows for adding an attr_reader to an immediate" do
+ class TrueClass
+ attr_reader :spec_attr_reader
+ end
+
+ -> { true.instance_variable_set("@spec_attr_reader", "a") }.should.raise(RuntimeError)
+ end
+
+ it "converts non string/symbol names to strings using to_str" do
+ (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test")
+ c = Class.new do
+ attr_reader o
+ end
+
+ c.new.respond_to?("test").should == true
+ c.new.respond_to?("test=").should == false
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ o = mock('o')
+ -> { Class.new { attr_reader o } }.should.raise(TypeError)
+ (o = mock('123')).should_receive(:to_str).and_return(123)
+ -> { Class.new { attr_reader o } }.should.raise(TypeError)
+ end
+
+ it "applies current visibility to methods created" do
+ c = Class.new do
+ protected
+ attr_reader :foo
+ end
+
+ -> { c.new.foo }.should.raise(NoMethodError)
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:attr_reader)
+ end
+
+ it "returns an array of defined method names as symbols" do
+ Class.new do
+ (attr_reader :foo, 'bar').should == [:foo, :bar]
+ end
+ end
+
+ it_behaves_like :module_attr_added, :attr_reader
+end
diff --git a/spec/ruby/core/module/attr_spec.rb b/spec/ruby/core/module/attr_spec.rb
new file mode 100644
index 0000000000..d696864955
--- /dev/null
+++ b/spec/ruby/core/module/attr_spec.rb
@@ -0,0 +1,159 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/attr_added'
+
+describe "Module#attr" do
+ before :each do
+ $VERBOSE, @verbose = false, $VERBOSE
+ end
+
+ after :each do
+ $VERBOSE = @verbose
+ end
+
+ it "creates a getter for the given attribute name" do
+ c = Class.new do
+ attr :attr
+ attr "attr3"
+
+ def initialize
+ @attr, @attr2, @attr3 = "test", "test2", "test3"
+ end
+ end
+
+ o = c.new
+
+ %w{attr attr3}.each do |a|
+ o.respond_to?(a).should == true
+ o.respond_to?("#{a}=").should == false
+ end
+
+ o.attr.should == "test"
+ o.attr3.should == "test3"
+ o.send(:attr).should == "test"
+ o.send(:attr3).should == "test3"
+ end
+
+ it "creates a setter for the given attribute name if writable is true" do
+ c = Class.new do
+ attr :attr, true
+ attr "attr3", true
+
+ def initialize
+ @attr, @attr2, @attr3 = "test", "test2", "test3"
+ end
+ end
+
+ o = c.new
+
+ %w{attr attr3}.each do |a|
+ o.respond_to?(a).should == true
+ o.respond_to?("#{a}=").should == true
+ end
+
+ o.attr = "test updated"
+ o.attr3 = "test3 updated"
+ end
+
+ it "creates a getter and setter for the given attribute name if called with and without writable is true" do
+ c = Class.new do
+ attr :attr, true
+ attr :attr
+
+ attr "attr3", true
+ attr "attr3"
+
+ def initialize
+ @attr, @attr2, @attr3 = "test", "test2", "test3"
+ end
+ end
+
+ o = c.new
+
+ %w{attr attr3}.each do |a|
+ o.respond_to?(a).should == true
+ o.respond_to?("#{a}=").should == true
+ end
+
+ o.attr.should == "test"
+ o.attr = "test updated"
+ o.attr.should == "test updated"
+
+ o.attr3.should == "test3"
+ o.attr3 = "test3 updated"
+ o.attr3.should == "test3 updated"
+ end
+
+ it "applies current visibility to methods created" do
+ c = Class.new do
+ protected
+ attr :foo, true
+ end
+
+ -> { c.new.foo }.should.raise(NoMethodError)
+ -> { c.new.foo=1 }.should.raise(NoMethodError)
+ end
+
+ it "creates a getter but no setter for all given attribute names" do
+ c = Class.new do
+ attr :attr, "attr2", "attr3"
+
+ def initialize
+ @attr, @attr2, @attr3 = "test", "test2", "test3"
+ end
+ end
+
+ o = c.new
+
+ %w{attr attr2 attr3}.each do |a|
+ o.respond_to?(a).should == true
+ o.respond_to?("#{a}=").should == false
+ end
+
+ o.attr.should == "test"
+ o.attr2.should == "test2"
+ o.attr3.should == "test3"
+ end
+
+ it "applies current visibility to methods created" do
+ c = Class.new do
+ protected
+ attr :foo, :bar
+ end
+
+ -> { c.new.foo }.should.raise(NoMethodError)
+ -> { c.new.bar }.should.raise(NoMethodError)
+ end
+
+ it "converts non string/symbol names to strings using to_str" do
+ (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test")
+ Class.new { attr o }.new.respond_to?("test").should == true
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ o = mock('o')
+ -> { Class.new { attr o } }.should.raise(TypeError)
+ (o = mock('123')).should_receive(:to_str).and_return(123)
+ -> { Class.new { attr o } }.should.raise(TypeError)
+ end
+
+ it "with a boolean argument emits a warning when $VERBOSE is true" do
+ -> {
+ Class.new { attr :foo, true }
+ }.should complain(/boolean argument is obsoleted/, verbose: true)
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:attr)
+ end
+
+ it "returns an array of defined method names as symbols" do
+ Class.new do
+ (attr :foo, 'bar').should == [:foo, :bar]
+ (attr :baz, false).should == [:baz]
+ (attr :qux, true).should == [:qux, :qux=]
+ end
+ end
+
+ it_behaves_like :module_attr_added, :attr
+end
diff --git a/spec/ruby/core/module/attr_writer_spec.rb b/spec/ruby/core/module/attr_writer_spec.rb
new file mode 100644
index 0000000000..6089b52495
--- /dev/null
+++ b/spec/ruby/core/module/attr_writer_spec.rb
@@ -0,0 +1,83 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/attr_added'
+
+describe "Module#attr_writer" do
+ it "creates a setter for each given attribute name" do
+ c = Class.new do
+ attr_writer :test1, "test2"
+ end
+ o = c.new
+
+ o.respond_to?("test1").should == false
+ o.respond_to?("test2").should == false
+
+ o.respond_to?("test1=").should == true
+ o.test1 = "test_1"
+ o.instance_variable_get(:@test1).should == "test_1"
+
+ o.respond_to?("test2=").should == true
+ o.test2 = "test_2"
+ o.instance_variable_get(:@test2).should == "test_2"
+ o.send(:test1=,"test_1 updated")
+ o.instance_variable_get(:@test1).should == "test_1 updated"
+ o.send(:test2=,"test_2 updated")
+ o.instance_variable_get(:@test2).should == "test_2 updated"
+ end
+
+ it "not allows for adding an attr_writer to an immediate" do
+ class TrueClass
+ attr_writer :spec_attr_writer
+ end
+
+ -> { true.spec_attr_writer = "a" }.should.raise(FrozenError)
+ end
+
+ it "raises FrozenError if the receiver if frozen" do
+ c = Class.new do
+ attr_writer :foo
+ end
+ obj = c.new
+ obj.freeze
+
+ -> { obj.foo = 42 }.should.raise(FrozenError)
+ end
+
+ it "converts non string/symbol names to strings using to_str" do
+ (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test")
+ c = Class.new do
+ attr_writer o
+ end
+
+ c.new.respond_to?("test").should == false
+ c.new.respond_to?("test=").should == true
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ o = mock('test1')
+ -> { Class.new { attr_writer o } }.should.raise(TypeError)
+ (o = mock('123')).should_receive(:to_str).and_return(123)
+ -> { Class.new { attr_writer o } }.should.raise(TypeError)
+ end
+
+ it "applies current visibility to methods created" do
+ c = Class.new do
+ protected
+ attr_writer :foo
+ end
+
+ -> { c.new.foo=1 }.should.raise(NoMethodError)
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:attr_writer)
+ end
+
+ it "returns an array of defined method names as symbols" do
+ Class.new do
+ (attr_writer :foo, 'bar').should == [:foo=, :bar=]
+ end
+ end
+
+ it_behaves_like :module_attr_added, :attr_writer
+end
diff --git a/spec/ruby/core/module/autoload_relative_spec.rb b/spec/ruby/core/module/autoload_relative_spec.rb
new file mode 100644
index 0000000000..2e7d85496b
--- /dev/null
+++ b/spec/ruby/core/module/autoload_relative_spec.rb
@@ -0,0 +1,128 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Specs for Module#autoload_relative
+module ModuleSpecs
+ module AutoloadRelative
+ # Will be used for testing
+ end
+end
+
+ruby_version_is "4.1" do
+ describe "Module#autoload_relative" do
+ before :each do
+ @loaded_features = $".dup
+ end
+
+ after :each do
+ $".replace @loaded_features
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:autoload_relative)
+ end
+
+ it "registers a file to load relative to the current file the first time the named constant is accessed" do
+ ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeA, "fixtures/autoload_relative_a.rb"
+ path = ModuleSpecs::Autoload.autoload?(:AutoloadRelativeA)
+ path.should_not == nil
+ path.should.end_with?("autoload_relative_a.rb")
+ File.exist?(path).should == true
+ end
+
+ it "loads the registered file when the constant is accessed" do
+ ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeB, "fixtures/autoload_relative_a.rb"
+ ModuleSpecs::Autoload::AutoloadRelativeB.should.is_a?(Module)
+ end
+
+ it "returns nil" do
+ ModuleSpecs::Autoload.autoload_relative(:AutoloadRelativeC, "fixtures/autoload_relative_a.rb").should == nil
+ end
+
+ it "registers a file to load the first time the named constant is accessed" do
+ module ModuleSpecs::Autoload::AutoloadRelativeTest
+ autoload_relative :D, "fixtures/autoload_relative_a.rb"
+ end
+ path = ModuleSpecs::Autoload::AutoloadRelativeTest.autoload?(:D)
+ path.should_not == nil
+ path.should.end_with?("autoload_relative_a.rb")
+ end
+
+ it "sets the autoload constant in the constants table" do
+ ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeTableTest, "fixtures/autoload_relative_a.rb"
+ ModuleSpecs::Autoload.should.const_defined?(:AutoloadRelativeTableTest, false)
+ end
+
+ it "calls #to_path on non-String filenames" do
+ name = mock("autoload_relative mock")
+ name.should_receive(:to_path).and_return("fixtures/autoload_relative_a.rb")
+ ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeToPath, name
+ ModuleSpecs::Autoload.autoload?(:AutoloadRelativeToPath).should_not == nil
+ end
+
+ it "calls #to_str on non-String filenames" do
+ name = mock("autoload_relative mock")
+ name.should_receive(:to_str).and_return("fixtures/autoload_relative_a.rb")
+ ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeToStr, name
+ ModuleSpecs::Autoload.autoload?(:AutoloadRelativeToStr).should_not == nil
+ end
+
+ it "raises a TypeError if the filename argument is not a String or pathname" do
+ -> {
+ ModuleSpecs::Autoload.autoload_relative :AutoloadRelativeTypError, nil
+ }.should.raise(TypeError)
+ end
+
+ it "raises a NameError if the constant name is not valid" do
+ -> {
+ ModuleSpecs::Autoload.autoload_relative :invalid_name, "fixtures/autoload_relative_a.rb"
+ }.should.raise(NameError)
+ end
+
+ it "raises an ArgumentError if the constant name starts with a lowercase letter" do
+ -> {
+ ModuleSpecs::Autoload.autoload_relative :autoload, "fixtures/autoload_relative_a.rb"
+ }.should.raise(NameError)
+ end
+
+ it "raises LoadError if called from eval without file context" do
+ -> {
+ ModuleSpecs::Autoload.module_eval('autoload_relative :EvalTest, "fixtures/autoload_relative_a.rb"')
+ }.should.raise(LoadError, /autoload_relative called without file context/)
+ end
+
+ it "can autoload in instance_eval with a file context" do
+ path = nil
+ ModuleSpecs::Autoload.instance_eval(<<-CODE, __FILE__, __LINE__)
+ autoload_relative :InstanceEvalTest, "fixtures/autoload_relative_a.rb"
+ path = autoload?(:InstanceEvalTest)
+ CODE
+ path.should_not == nil
+ path.should.end_with?("autoload_relative_a.rb")
+ end
+
+ it "resolves paths relative to the file where it's called" do
+ # Using fixtures/autoload_relative_a.rb which exists
+ ModuleSpecs::Autoload.autoload_relative :RelativePathTest, "fixtures/autoload_relative_a.rb"
+ path = ModuleSpecs::Autoload.autoload?(:RelativePathTest)
+ path.should.include?("fixtures")
+ path.should.end_with?("autoload_relative_a.rb")
+ end
+
+ it "can load nested directory paths" do
+ ModuleSpecs::Autoload.autoload_relative :NestedPath, "fixtures/autoload_relative_a.rb"
+ path = ModuleSpecs::Autoload.autoload?(:NestedPath)
+ path.should_not == nil
+ File.exist?(path).should == true
+ end
+
+ describe "interoperability with autoload?" do
+ it "returns the absolute path with autoload?" do
+ ModuleSpecs::Autoload.autoload_relative :QueryTest, "fixtures/autoload_relative_a.rb"
+ path = ModuleSpecs::Autoload.autoload?(:QueryTest)
+ # Should be an absolute path
+ Pathname.new(path).absolute?.should == true
+ end
+ end
+end
+end
diff --git a/spec/ruby/core/module/autoload_spec.rb b/spec/ruby/core/module/autoload_spec.rb
new file mode 100644
index 0000000000..057237a92f
--- /dev/null
+++ b/spec/ruby/core/module/autoload_spec.rb
@@ -0,0 +1,1028 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/code_loading'
+require_relative 'fixtures/classes'
+
+describe "Module#autoload?" do
+ it "returns the name of the file that will be autoloaded" do
+ ModuleSpecs::Autoload.autoload :Autoload, "autoload.rb"
+ ModuleSpecs::Autoload.autoload?(:Autoload).should == "autoload.rb"
+ end
+
+ it "returns nil if no file has been registered for a constant" do
+ ModuleSpecs::Autoload.autoload?(:Manualload).should == nil
+ end
+
+ it "returns the name of the file that will be autoloaded if an ancestor defined that autoload" do
+ ModuleSpecs::Autoload::Parent.autoload :AnotherAutoload, "another_autoload.rb"
+ ModuleSpecs::Autoload::Child.autoload?(:AnotherAutoload).should == "another_autoload.rb"
+ end
+
+ it "returns nil if an ancestor defined that autoload but recursion is disabled" do
+ ModuleSpecs::Autoload::Parent.autoload :InheritedAutoload, "inherited_autoload.rb"
+ ModuleSpecs::Autoload::Child.autoload?(:InheritedAutoload, false).should == nil
+ end
+
+ it "returns the name of the file that will be loaded if recursion is disabled but the autoload is defined on the class itself" do
+ ModuleSpecs::Autoload::Child.autoload :ChildAutoload, "child_autoload.rb"
+ ModuleSpecs::Autoload::Child.autoload?(:ChildAutoload, false).should == "child_autoload.rb"
+ end
+end
+
+describe "Module#autoload" do
+ before :all do
+ @non_existent = fixture __FILE__, "no_autoload.rb"
+ CodeLoadingSpecs.preload_rubygems
+ end
+
+ before :each do
+ @loaded_features = $".dup
+
+ ScratchPad.clear
+ @remove = []
+ end
+
+ after :each do
+ $".replace @loaded_features
+ @remove.each { |const|
+ ModuleSpecs::Autoload.send :remove_const, const
+ }
+ end
+
+ it "registers a file to load the first time the named constant is accessed" do
+ ModuleSpecs::Autoload.autoload :A, @non_existent
+ ModuleSpecs::Autoload.autoload?(:A).should == @non_existent
+ end
+
+ it "sets the autoload constant in the constants table" do
+ ModuleSpecs::Autoload.autoload :B, @non_existent
+ ModuleSpecs::Autoload.should.const_defined?(:B, false)
+ end
+
+ it "can be overridden with a second autoload on the same constant" do
+ ModuleSpecs::Autoload.autoload :Overridden, @non_existent
+ @remove << :Overridden
+ ModuleSpecs::Autoload.autoload?(:Overridden).should == @non_existent
+
+ path = fixture(__FILE__, "autoload_overridden.rb")
+ ModuleSpecs::Autoload.autoload :Overridden, path
+ ModuleSpecs::Autoload.autoload?(:Overridden).should == path
+
+ ModuleSpecs::Autoload::Overridden.should == :overridden
+ end
+
+ it "loads the registered constant when it is accessed" do
+ ModuleSpecs::Autoload.should_not.const_defined?(:X)
+ ModuleSpecs::Autoload.autoload :X, fixture(__FILE__, "autoload_x.rb")
+ @remove << :X
+ ModuleSpecs::Autoload::X.should == :x
+ end
+
+ it "loads the registered constant into a dynamically created class" do
+ cls = Class.new { autoload :C, fixture(__FILE__, "autoload_c.rb") }
+ ModuleSpecs::Autoload::DynClass = cls
+ @remove << :DynClass
+
+ ScratchPad.recorded.should == nil
+ ModuleSpecs::Autoload::DynClass::C.new.loaded.should == :dynclass_c
+ ScratchPad.recorded.should == :loaded
+ end
+
+ it "loads the registered constant into a dynamically created module" do
+ mod = Module.new { autoload :D, fixture(__FILE__, "autoload_d.rb") }
+ ModuleSpecs::Autoload::DynModule = mod
+ @remove << :DynModule
+
+ ScratchPad.recorded.should == nil
+ ModuleSpecs::Autoload::DynModule::D.new.loaded.should == :dynmodule_d
+ ScratchPad.recorded.should == :loaded
+ end
+
+ it "loads the registered constant when it is opened as a class" do
+ ModuleSpecs::Autoload.autoload :E, fixture(__FILE__, "autoload_e.rb")
+ class ModuleSpecs::Autoload::E
+ end
+ ModuleSpecs::Autoload::E.new.loaded.should == :autoload_e
+ end
+
+ it "loads the registered constant when it is opened as a module" do
+ ModuleSpecs::Autoload.autoload :F, fixture(__FILE__, "autoload_f.rb")
+ module ModuleSpecs::Autoload::F
+ end
+ ModuleSpecs::Autoload::F.loaded.should == :autoload_f
+ end
+
+ it "loads the registered constant when it is inherited from" do
+ ModuleSpecs::Autoload.autoload :G, fixture(__FILE__, "autoload_g.rb")
+ class ModuleSpecs::Autoload::Gsub < ModuleSpecs::Autoload::G
+ end
+ ModuleSpecs::Autoload::Gsub.new.loaded.should == :autoload_g
+ end
+
+ it "loads the registered constant when it is included" do
+ ModuleSpecs::Autoload.autoload :H, fixture(__FILE__, "autoload_h.rb")
+ class ModuleSpecs::Autoload::HClass
+ include ModuleSpecs::Autoload::H
+ end
+ ModuleSpecs::Autoload::HClass.new.loaded.should == :autoload_h
+ end
+
+ it "does not load the file when the constant is already set" do
+ ModuleSpecs::Autoload.autoload :I, fixture(__FILE__, "autoload_i.rb")
+ @remove << :I
+ ModuleSpecs::Autoload.const_set :I, 3
+ ModuleSpecs::Autoload::I.should == 3
+ ScratchPad.recorded.should == nil
+ end
+
+ it "loads a file with .rb extension when passed the name without the extension" do
+ ModuleSpecs::Autoload.autoload :J, fixture(__FILE__, "autoload_j")
+ ModuleSpecs::Autoload::J.should == :autoload_j
+ end
+
+ it "calls main.require(path) to load the file" do
+ ModuleSpecs::Autoload.autoload :ModuleAutoloadCallsRequire, "module_autoload_not_exist.rb"
+ main = TOPLEVEL_BINDING.eval("self")
+ main.should_receive(:require).with("module_autoload_not_exist.rb")
+ # The constant won't be defined since require is mocked to do nothing
+ -> { ModuleSpecs::Autoload::ModuleAutoloadCallsRequire }.should.raise(NameError)
+ end
+
+ it "does not load the file if the file is manually required" do
+ filename = fixture(__FILE__, "autoload_k.rb")
+ ModuleSpecs::Autoload.autoload :KHash, filename
+ @remove << :KHash
+
+ require filename
+ ScratchPad.recorded.should == :loaded
+ ScratchPad.clear
+
+ ModuleSpecs::Autoload::KHash.should.is_a?(Class)
+ ModuleSpecs::Autoload::KHash::K.should == :autoload_k
+ ScratchPad.recorded.should == nil
+ end
+
+ it "ignores the autoload request if the file is already loaded" do
+ filename = fixture(__FILE__, "autoload_s.rb")
+
+ require filename
+
+ ScratchPad.recorded.should == :loaded
+ ScratchPad.clear
+
+ ModuleSpecs::Autoload.autoload :S, filename
+ @remove << :S
+ ModuleSpecs::Autoload.autoload?(:S).should == nil
+ end
+
+ it "retains the autoload even if the request to require fails" do
+ filename = fixture(__FILE__, "a_path_that_should_not_exist.rb")
+
+ ModuleSpecs::Autoload.autoload :NotThere, filename
+ ModuleSpecs::Autoload.autoload?(:NotThere).should == filename
+
+ -> {
+ require filename
+ }.should.raise(LoadError)
+
+ ModuleSpecs::Autoload.autoload?(:NotThere).should == filename
+ end
+
+ it "allows multiple autoload constants for a single file" do
+ filename = fixture(__FILE__, "autoload_lm.rb")
+ ModuleSpecs::Autoload.autoload :L, filename
+ ModuleSpecs::Autoload.autoload :M, filename
+ ModuleSpecs::Autoload::L.should == :autoload_l
+ ModuleSpecs::Autoload::M.should == :autoload_m
+ end
+
+ it "runs for an exception condition class and doesn't trample the exception" do
+ filename = fixture(__FILE__, "autoload_ex1.rb")
+ ModuleSpecs::Autoload.autoload :EX1, filename
+ ModuleSpecs::Autoload.use_ex1.should == :good
+ end
+
+ it "considers an autoload constant as loaded when autoload is called for/from the current file" do
+ filename = fixture(__FILE__, "autoload_during_require_current_file.rb")
+ require filename
+
+ ScratchPad.recorded.should == nil
+ end
+
+ describe "interacting with defined?" do
+ it "does not load the file when referring to the constant in defined?" do
+ module ModuleSpecs::Autoload::Dog
+ autoload :R, fixture(__FILE__, "autoload_exception.rb")
+ end
+
+ defined?(ModuleSpecs::Autoload::Dog::R).should == "constant"
+ ScratchPad.recorded.should == nil
+
+ ModuleSpecs::Autoload::Dog.should.const_defined?(:R, false)
+ end
+
+ it "loads an autoloaded parent when referencing a nested constant" do
+ module ModuleSpecs::Autoload
+ autoload :GoodParent, fixture(__FILE__, "autoload_nested.rb")
+ end
+ @remove << :GoodParent
+
+ defined?(ModuleSpecs::Autoload::GoodParent::Nested).should == 'constant'
+ ScratchPad.recorded.should == :loaded
+ end
+
+ it "returns nil when it fails to load an autoloaded parent when referencing a nested constant" do
+ module ModuleSpecs::Autoload
+ autoload :BadParent, fixture(__FILE__, "autoload_exception.rb")
+ end
+
+ defined?(ModuleSpecs::Autoload::BadParent::Nested).should == nil
+ ScratchPad.recorded.should == :exception
+ end
+ end
+
+ describe "the autoload is triggered when the same file is required directly" do
+ before :each do
+ module ModuleSpecs::Autoload
+ autoload :RequiredDirectly, fixture(__FILE__, "autoload_required_directly.rb")
+ end
+ @remove << :RequiredDirectly
+ @path = fixture(__FILE__, "autoload_required_directly.rb")
+ @check = -> {
+ [
+ defined?(ModuleSpecs::Autoload::RequiredDirectly),
+ ModuleSpecs::Autoload.autoload?(:RequiredDirectly)
+ ]
+ }
+ ScratchPad.record @check
+ end
+
+ it "with a full path" do
+ @check.call.should == ["constant", @path]
+ require @path
+ ScratchPad.recorded.should == [nil, nil]
+ @check.call.should == ["constant", nil]
+ end
+
+ it "with a relative path" do
+ @check.call.should == ["constant", @path]
+ $:.push File.dirname(@path)
+ begin
+ require "autoload_required_directly.rb"
+ ensure
+ $:.pop
+ end
+ ScratchPad.recorded.should == [nil, nil]
+ @check.call.should == ["constant", nil]
+ end
+
+ it "in a nested require" do
+ nested = fixture(__FILE__, "autoload_required_directly_nested.rb")
+ nested_require = -> {
+ result = nil
+ ScratchPad.record -> {
+ result = @check.call
+ }
+ require nested
+ result
+ }
+ ScratchPad.record nested_require
+
+ @check.call.should == ["constant", @path]
+ require @path
+ ScratchPad.recorded.should == [nil, nil]
+ @check.call.should == ["constant", nil]
+ end
+
+ it "does not raise an error if the autoload constant was not defined" do
+ module ModuleSpecs::Autoload
+ autoload :RequiredDirectlyNoConstant, fixture(__FILE__, "autoload_required_directly_no_constant.rb")
+ end
+ @path = fixture(__FILE__, "autoload_required_directly_no_constant.rb")
+ @remove << :RequiredDirectlyNoConstant
+ @check = -> {
+ [
+ defined?(ModuleSpecs::Autoload::RequiredDirectlyNoConstant),
+ ModuleSpecs::Autoload.constants(false).include?(:RequiredDirectlyNoConstant),
+ ModuleSpecs::Autoload.const_defined?(:RequiredDirectlyNoConstant),
+ ModuleSpecs::Autoload.autoload?(:RequiredDirectlyNoConstant)
+ ]
+ }
+ ScratchPad.record @check
+ @check.call.should == ["constant", true, true, @path]
+ $:.push File.dirname(@path)
+ begin
+ require "autoload_required_directly_no_constant.rb"
+ ensure
+ $:.pop
+ end
+ ScratchPad.recorded.should == [nil, true, false, nil]
+ @check.call.should == [nil, true, false, nil]
+ end
+ end
+
+ describe "after the autoload is triggered by require" do
+ before :each do
+ @path = tmp("autoload.rb")
+ end
+
+ after :each do
+ rm_r @path
+ end
+
+ it "the mapping feature to autoload is removed, and a new autoload with the same path is considered" do
+ ModuleSpecs::Autoload.autoload :RequireMapping1, @path
+ touch(@path) { |f| f.puts "ModuleSpecs::Autoload::RequireMapping1 = 1" }
+ ModuleSpecs::Autoload::RequireMapping1.should == 1
+
+ $LOADED_FEATURES.delete(@path)
+ ModuleSpecs::Autoload.autoload :RequireMapping2, @path[0...-3]
+ @remove << :RequireMapping2
+ touch(@path) { |f| f.puts "ModuleSpecs::Autoload::RequireMapping2 = 2" }
+ ModuleSpecs::Autoload::RequireMapping2.should == 2
+ end
+ end
+
+ def check_before_during_thread_after(const, &check)
+ before = check.call
+ to_autoload_thread, from_autoload_thread = Queue.new, Queue.new
+ ScratchPad.record -> {
+ from_autoload_thread.push check.call
+ to_autoload_thread.pop
+ }
+ t = Thread.new {
+ in_loading_thread = from_autoload_thread.pop
+ in_other_thread = check.call
+ to_autoload_thread.push :done
+ [in_loading_thread, in_other_thread]
+ }
+ in_loading_thread, in_other_thread = nil
+ begin
+ ModuleSpecs::Autoload.const_get(const)
+ ensure
+ in_loading_thread, in_other_thread = t.value
+ end
+ after = check.call
+ [before, in_loading_thread, in_other_thread, after]
+ end
+
+ describe "during the autoload before the constant is assigned" do
+ before :each do
+ @path = fixture(__FILE__, "autoload_during_autoload.rb")
+ ModuleSpecs::Autoload.autoload :DuringAutoload, @path
+ @remove << :DuringAutoload
+ raise unless ModuleSpecs::Autoload.autoload?(:DuringAutoload) == @path
+ end
+
+ it "returns nil in autoload thread and 'constant' otherwise for defined?" do
+ results = check_before_during_thread_after(:DuringAutoload) {
+ defined?(ModuleSpecs::Autoload::DuringAutoload)
+ }
+ results.should == ['constant', nil, 'constant', 'constant']
+ end
+
+ it "keeps the constant in Module#constants" do
+ results = check_before_during_thread_after(:DuringAutoload) {
+ ModuleSpecs::Autoload.constants(false).include?(:DuringAutoload)
+ }
+ results.should == [true, true, true, true]
+ end
+
+ it "returns false in autoload thread and true otherwise for Module#const_defined?" do
+ results = check_before_during_thread_after(:DuringAutoload) {
+ ModuleSpecs::Autoload.const_defined?(:DuringAutoload, false)
+ }
+ results.should == [true, false, true, true]
+ end
+
+ it "returns nil in autoload thread and returns the path in other threads for Module#autoload?" do
+ results = check_before_during_thread_after(:DuringAutoload) {
+ ModuleSpecs::Autoload.autoload?(:DuringAutoload)
+ }
+ results.should == [@path, nil, @path, nil]
+ end
+ end
+
+ describe "during the autoload after the constant is assigned" do
+ before :each do
+ @path = fixture(__FILE__, "autoload_during_autoload_after_define.rb")
+ ModuleSpecs::Autoload.autoload :DuringAutoloadAfterDefine, @path
+ @autoload_location = [__FILE__, __LINE__ - 1]
+ @const_location = [@path, 2]
+ @remove << :DuringAutoloadAfterDefine
+ raise unless ModuleSpecs::Autoload.autoload?(:DuringAutoloadAfterDefine) == @path
+ end
+
+ it "returns 'constant' in both threads" do
+ results = check_before_during_thread_after(:DuringAutoloadAfterDefine) {
+ defined?(ModuleSpecs::Autoload::DuringAutoloadAfterDefine)
+ }
+ results.should == ['constant', 'constant', 'constant', 'constant']
+ end
+
+ it "Module#constants include the autoloaded in both threads" do
+ results = check_before_during_thread_after(:DuringAutoloadAfterDefine) {
+ ModuleSpecs::Autoload.constants(false).include?(:DuringAutoloadAfterDefine)
+ }
+ results.should == [true, true, true, true]
+ end
+
+ it "Module#const_defined? returns true in both threads" do
+ results = check_before_during_thread_after(:DuringAutoloadAfterDefine) {
+ ModuleSpecs::Autoload.const_defined?(:DuringAutoloadAfterDefine, false)
+ }
+ results.should == [true, true, true, true]
+ end
+
+ it "returns nil in autoload thread and returns the path in other threads for Module#autoload?" do
+ results = check_before_during_thread_after(:DuringAutoloadAfterDefine) {
+ ModuleSpecs::Autoload.autoload?(:DuringAutoloadAfterDefine)
+ }
+ results.should == [@path, nil, @path, nil]
+ end
+
+ ruby_bug("#20188", ""..."3.4") do
+ it "returns the real constant location in autoload thread and returns the autoload location in other threads for Module#const_source_location" do
+ results = check_before_during_thread_after(:DuringAutoloadAfterDefine) {
+ ModuleSpecs::Autoload.const_source_location(:DuringAutoloadAfterDefine)
+ }
+ results.should == [@autoload_location, @const_location, @autoload_location, @const_location]
+ end
+ end
+ end
+
+ it "does not remove the constant from Module#constants if load fails and keeps it as an autoload" do
+ ModuleSpecs::Autoload.autoload :Fail, @non_existent
+
+ ModuleSpecs::Autoload.const_defined?(:Fail).should == true
+ ModuleSpecs::Autoload.should.const_defined?(:Fail, false)
+ ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent
+
+ -> { ModuleSpecs::Autoload::Fail }.should.raise(LoadError)
+
+ ModuleSpecs::Autoload.should.const_defined?(:Fail, false)
+ ModuleSpecs::Autoload.const_defined?(:Fail).should == true
+ ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent
+
+ -> { ModuleSpecs::Autoload::Fail }.should.raise(LoadError)
+ end
+
+ it "does not remove the constant from Module#constants if load raises a RuntimeError and keeps it as an autoload" do
+ path = fixture(__FILE__, "autoload_raise.rb")
+ ScratchPad.record []
+ ModuleSpecs::Autoload.autoload :Raise, path
+
+ ModuleSpecs::Autoload.const_defined?(:Raise).should == true
+ ModuleSpecs::Autoload.should.const_defined?(:Raise, false)
+ ModuleSpecs::Autoload.autoload?(:Raise).should == path
+
+ -> { ModuleSpecs::Autoload::Raise }.should.raise(RuntimeError)
+ ScratchPad.recorded.should == [:raise]
+
+ ModuleSpecs::Autoload.should.const_defined?(:Raise, false)
+ ModuleSpecs::Autoload.const_defined?(:Raise).should == true
+ ModuleSpecs::Autoload.autoload?(:Raise).should == path
+
+ -> { ModuleSpecs::Autoload::Raise }.should.raise(RuntimeError)
+ ScratchPad.recorded.should == [:raise, :raise]
+ end
+
+ it "removes the constant from Module#constants if the loaded file does not define it" do
+ path = fixture(__FILE__, "autoload_o.rb")
+ ScratchPad.record []
+ ModuleSpecs::Autoload.autoload :O, path
+
+ ModuleSpecs::Autoload.const_defined?(:O).should == true
+ ModuleSpecs::Autoload.should.const_defined?(:O, false)
+ ModuleSpecs::Autoload.autoload?(:O).should == path
+
+ -> { ModuleSpecs::Autoload::O }.should.raise(NameError)
+
+ ModuleSpecs::Autoload.const_defined?(:O).should == false
+ ModuleSpecs::Autoload.should_not.const_defined?(:O)
+ ModuleSpecs::Autoload.autoload?(:O).should == nil
+ -> { ModuleSpecs::Autoload.const_get(:O) }.should.raise(NameError)
+ end
+
+ it "does not try to load the file again if the loaded file did not define the constant" do
+ path = fixture(__FILE__, "autoload_o.rb")
+ ScratchPad.record []
+ ModuleSpecs::Autoload.autoload :NotDefinedByFile, path
+
+ -> { ModuleSpecs::Autoload::NotDefinedByFile }.should.raise(NameError)
+ ScratchPad.recorded.should == [:loaded]
+ -> { ModuleSpecs::Autoload::NotDefinedByFile }.should.raise(NameError)
+ ScratchPad.recorded.should == [:loaded]
+
+ Thread.new {
+ -> { ModuleSpecs::Autoload::NotDefinedByFile }.should.raise(NameError)
+ }.join
+ ScratchPad.recorded.should == [:loaded]
+ end
+
+ it "returns 'constant' on referring the constant with defined?()" do
+ module ModuleSpecs::Autoload::Q
+ autoload :R, fixture(__FILE__, "autoload.rb")
+ defined?(R).should == 'constant'
+ end
+ ModuleSpecs::Autoload::Q.should.const_defined?(:R, false)
+ end
+
+ it "does not load the file when removing an autoload constant" do
+ module ModuleSpecs::Autoload::Q
+ autoload :R, fixture(__FILE__, "autoload.rb")
+ remove_const :R
+ end
+ ModuleSpecs::Autoload::Q.should_not.const_defined?(:R)
+ end
+
+ it "does not load the file when accessing the constants table of the module" do
+ ModuleSpecs::Autoload.autoload :P, @non_existent
+ ModuleSpecs::Autoload.const_defined?(:P).should == true
+ ModuleSpecs::Autoload.const_defined?("P").should == true
+ end
+
+ it "loads the file when opening a module that is the autoloaded constant" do
+ module ModuleSpecs::Autoload::U
+ autoload :V, fixture(__FILE__, "autoload_v.rb")
+
+ class V
+ X = get_value
+ end
+ end
+ @remove << :U
+
+ ModuleSpecs::Autoload::U::V::X.should == :autoload_uvx
+ end
+
+ it "loads the file that defines subclass XX::CS_CONST_AUTOLOAD < CS_CONST_AUTOLOAD and CS_CONST_AUTOLOAD is a top level constant" do
+ module ModuleSpecs::Autoload::XX
+ autoload :CS_CONST_AUTOLOAD, fixture(__FILE__, "autoload_subclass.rb")
+ end
+
+ ModuleSpecs::Autoload::XX::CS_CONST_AUTOLOAD.superclass.should == CS_CONST_AUTOLOAD
+ end
+
+ describe "after autoloading searches for the constant like the original lookup" do
+ it "in lexical scopes if both declared and defined in parent" do
+ module ModuleSpecs::Autoload
+ ScratchPad.record -> {
+ DeclaredAndDefinedInParent = :declared_and_defined_in_parent
+ }
+ autoload :DeclaredAndDefinedInParent, fixture(__FILE__, "autoload_callback.rb")
+ class LexicalScope
+ DeclaredAndDefinedInParent.should == :declared_and_defined_in_parent
+
+ # The constant is really in Autoload, not Autoload::LexicalScope
+ self.should_not.const_defined?(:DeclaredAndDefinedInParent)
+ -> { const_get(:DeclaredAndDefinedInParent) }.should.raise(NameError)
+ end
+ DeclaredAndDefinedInParent.should == :declared_and_defined_in_parent
+ end
+ end
+
+ it "in lexical scopes if declared in parent and defined in current" do
+ module ModuleSpecs::Autoload
+ ScratchPad.record -> {
+ class LexicalScope
+ DeclaredInParentDefinedInCurrent = :declared_in_parent_defined_in_current
+ end
+ }
+ autoload :DeclaredInParentDefinedInCurrent, fixture(__FILE__, "autoload_callback.rb")
+
+ class LexicalScope
+ DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current
+ LexicalScope::DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current
+ end
+
+ # Basically, the parent autoload constant remains in a "undefined" state
+ self.autoload?(:DeclaredInParentDefinedInCurrent).should == nil
+ const_defined?(:DeclaredInParentDefinedInCurrent).should == false
+ -> { DeclaredInParentDefinedInCurrent }.should.raise(NameError)
+
+ ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent)
+ end
+ end
+
+ it "warns once in verbose mode if the constant was defined in a parent scope" do
+ ScratchPad.record -> {
+ ModuleSpecs::DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent
+ }
+
+ module ModuleSpecs
+ module Autoload
+ autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb")
+ self.autoload?(:DeclaredInCurrentDefinedInParent).should == fixture(__FILE__, "autoload_callback.rb")
+ const_defined?(:DeclaredInCurrentDefinedInParent).should == true
+
+ -> {
+ DeclaredInCurrentDefinedInParent
+ }.should complain(
+ /Expected .*autoload_callback.rb to define ModuleSpecs::Autoload::DeclaredInCurrentDefinedInParent but it didn't/,
+ verbose: true,
+ )
+
+ -> {
+ DeclaredInCurrentDefinedInParent
+ }.should_not complain(/.*/, verbose: true)
+ self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
+ const_defined?(:DeclaredInCurrentDefinedInParent).should == false
+ ModuleSpecs.const_defined?(:DeclaredInCurrentDefinedInParent).should == true
+ end
+ end
+ end
+
+ it "looks up in parent scope after failed autoload" do
+ @remove << :DeclaredInCurrentDefinedInParent
+ module ModuleSpecs::Autoload
+ ScratchPad.record -> {
+ DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent
+ }
+
+ class LexicalScope
+ autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb")
+ -> { DeclaredInCurrentDefinedInParent }.should_not.raise(NameError)
+ # Basically, the autoload constant remains in a "undefined" state
+ self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil
+ const_defined?(:DeclaredInCurrentDefinedInParent).should == false
+ -> { const_get(:DeclaredInCurrentDefinedInParent) }.should.raise(NameError)
+ end
+
+ DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent
+ end
+ end
+
+ it "in the included modules" do
+ @remove << :DefinedInIncludedModule
+ module ModuleSpecs::Autoload
+ ScratchPad.record -> {
+ module DefinedInIncludedModule
+ Incl = :defined_in_included_module
+ end
+ include DefinedInIncludedModule
+ }
+ autoload :Incl, fixture(__FILE__, "autoload_callback.rb")
+ Incl.should == :defined_in_included_module
+ end
+ end
+
+ it "in the included modules of the superclass" do
+ @remove << :DefinedInSuperclassIncludedModule
+ module ModuleSpecs::Autoload
+ class LookupAfterAutoloadSuper
+ end
+ class LookupAfterAutoloadChild < LookupAfterAutoloadSuper
+ end
+
+ ScratchPad.record -> {
+ module DefinedInSuperclassIncludedModule
+ InclS = :defined_in_superclass_included_module
+ end
+ LookupAfterAutoloadSuper.include DefinedInSuperclassIncludedModule
+ }
+
+ class LookupAfterAutoloadChild
+ autoload :InclS, fixture(__FILE__, "autoload_callback.rb")
+ InclS.should == :defined_in_superclass_included_module
+ end
+ end
+ end
+
+ it "in the prepended modules" do
+ @remove << :DefinedInPrependedModule
+ module ModuleSpecs::Autoload
+ ScratchPad.record -> {
+ module DefinedInPrependedModule
+ Prep = :defined_in_prepended_module
+ end
+ include DefinedInPrependedModule
+ }
+ autoload :Prep, fixture(__FILE__, "autoload_callback.rb")
+ Prep.should == :defined_in_prepended_module
+ end
+ end
+
+ it "in a meta class scope" do
+ module ModuleSpecs::Autoload
+ ScratchPad.record -> {
+ class MetaScope
+ end
+ }
+ autoload :MetaScope, fixture(__FILE__, "autoload_callback.rb")
+ class << self
+ def r
+ MetaScope.new
+ end
+ end
+ end
+ ModuleSpecs::Autoload.r.should.is_a?(ModuleSpecs::Autoload::MetaScope)
+ end
+ end
+
+ it "should trigger the autoload when using `private_constant`" do
+ @remove << :DynClass
+ module ModuleSpecs::Autoload
+ autoload :DynClass, fixture(__FILE__, "autoload_c.rb")
+ private_constant :DynClass
+
+ ScratchPad.recorded.should == nil
+
+ DynClass::C.new.loaded.should == :dynclass_c
+ ScratchPad.recorded.should == :loaded
+ end
+
+ -> { ModuleSpecs::Autoload::DynClass }.should.raise(NameError, /private constant/)
+ end
+
+ # [ruby-core:19127] [ruby-core:29941]
+ it "does NOT raise a NameError when the autoload file did not define the constant and a module is opened with the same name" do
+ module ModuleSpecs::Autoload
+ class W
+ autoload :Y, fixture(__FILE__, "autoload_w.rb")
+
+ class Y
+ end
+ end
+ end
+ @remove << :W
+
+ ModuleSpecs::Autoload::W::Y.should.is_a?(Class)
+ ScratchPad.recorded.should == :loaded
+ end
+
+ it "does not call #require a second time and does not warn if already loading the same feature with #require" do
+ main = TOPLEVEL_BINDING.eval("self")
+ main.should_not_receive(:require)
+
+ module ModuleSpecs::Autoload
+ autoload :AutoloadDuringRequire, fixture(__FILE__, "autoload_during_require.rb")
+ end
+
+ -> {
+ Kernel.require fixture(__FILE__, "autoload_during_require.rb")
+ }.should_not complain(verbose: true)
+ ModuleSpecs::Autoload::AutoloadDuringRequire.should.is_a?(Class)
+ end
+
+ it "does not call #require a second time and does not warn if feature sets and trigger autoload on itself" do
+ main = TOPLEVEL_BINDING.eval("self")
+ main.should_not_receive(:require)
+
+ -> {
+ Kernel.require fixture(__FILE__, "autoload_self_during_require.rb")
+ }.should_not complain(verbose: true)
+ ModuleSpecs::Autoload::AutoloadSelfDuringRequire.should.is_a?(Class)
+ end
+
+ it "handles multiple autoloads in the same file" do
+ $LOAD_PATH.unshift(File.expand_path('../fixtures/multi', __FILE__))
+ begin
+ require 'foo/bar_baz'
+ ModuleSpecs::Autoload::Foo::Bar.should.is_a?(Class)
+ ModuleSpecs::Autoload::Foo::Baz.should.is_a?(Class)
+ ensure
+ $LOAD_PATH.shift
+ end
+ end
+
+ it "calls #to_path on non-string filenames" do
+ p = mock('path')
+ p.should_receive(:to_path).and_return @non_existent
+ ModuleSpecs.autoload :A, p
+ end
+
+ it "raises an ArgumentError when an empty filename is given" do
+ -> { ModuleSpecs.autoload :A, "" }.should.raise(ArgumentError)
+ end
+
+ it "raises a NameError when the constant name starts with a lower case letter" do
+ -> { ModuleSpecs.autoload "a", @non_existent }.should.raise(NameError)
+ end
+
+ it "raises a NameError when the constant name starts with a number" do
+ -> { ModuleSpecs.autoload "1two", @non_existent }.should.raise(NameError)
+ end
+
+ it "raises a NameError when the constant name has a space in it" do
+ -> { ModuleSpecs.autoload "a name", @non_existent }.should.raise(NameError)
+ end
+
+ it "shares the autoload request across dup'ed copies of modules" do
+ require fixture(__FILE__, "autoload_s.rb")
+ @remove << :S
+ filename = fixture(__FILE__, "autoload_t.rb")
+ mod1 = Module.new { autoload :T, filename }
+ -> {
+ ModuleSpecs::Autoload::S = mod1
+ }.should complain(/already initialized constant/)
+ mod2 = mod1.dup
+
+ mod1.autoload?(:T).should == filename
+ mod2.autoload?(:T).should == filename
+
+ mod1::T.should == :autoload_t
+ -> { mod2::T }.should.raise(NameError)
+ end
+
+ it "raises a TypeError if opening a class with a different superclass than the class defined in the autoload file" do
+ ModuleSpecs::Autoload.autoload :Z, fixture(__FILE__, "autoload_z.rb")
+ class ModuleSpecs::Autoload::ZZ
+ end
+
+ -> do
+ class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::ZZ
+ end
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if not passed a String or object responding to #to_path for the filename" do
+ name = mock("autoload_name.rb")
+
+ -> { ModuleSpecs::Autoload.autoload :Str, name }.should.raise(TypeError)
+ end
+
+ it "calls #to_path on non-String filename arguments" do
+ name = mock("autoload_name.rb")
+ name.should_receive(:to_path).and_return("autoload_name.rb")
+
+ -> { ModuleSpecs::Autoload.autoload :Str, name }.should_not.raise
+ end
+
+ describe "on a frozen module" do
+ it "raises a FrozenError before setting the name" do
+ frozen_module = Module.new.freeze
+ -> { frozen_module.autoload :Foo, @non_existent }.should.raise(FrozenError)
+ frozen_module.should_not.const_defined?(:Foo)
+ end
+ end
+
+ describe "when changing $LOAD_PATH" do
+ before do
+ $LOAD_PATH.unshift(File.expand_path('../fixtures/path1', __FILE__))
+ end
+
+ after do
+ $LOAD_PATH.shift
+ $LOAD_PATH.shift
+ end
+
+ it "does not reload a file due to a different load path" do
+ ModuleSpecs::Autoload.autoload :LoadPath, "load_path"
+ ModuleSpecs::Autoload::LoadPath.loaded.should == :autoload_load_path
+ end
+ end
+
+ describe "(concurrently)" do
+ it "blocks a second thread while a first is doing the autoload" do
+ ModuleSpecs::Autoload.autoload :Concur, fixture(__FILE__, "autoload_concur.rb")
+ @remove << :Concur
+
+ start = false
+
+ ScratchPad.record []
+
+ t1_val = nil
+ t2_val = nil
+
+ fin = false
+
+ t1 = Thread.new do
+ Thread.pass until start
+ t1_val = ModuleSpecs::Autoload::Concur
+ ScratchPad.recorded << :t1_post
+ fin = true
+ end
+
+ t2_exc = nil
+
+ t2 = Thread.new do
+ Thread.pass until t1 and t1[:in_autoload_rb]
+ begin
+ t2_val = ModuleSpecs::Autoload::Concur
+ rescue Exception => e
+ t2_exc = e
+ else
+ Thread.pass until fin
+ ScratchPad.recorded << :t2_post
+ end
+ end
+
+ start = true
+
+ t1.join
+ t2.join
+
+ ScratchPad.recorded.should == [:con_pre, :con_post, :t1_post, :t2_post]
+
+ t1_val.should == 1
+ t2_val.should == t1_val
+
+ t2_exc.should == nil
+ end
+
+ # https://bugs.ruby-lang.org/issues/10892
+ it "blocks others threads while doing an autoload" do
+ file_path = fixture(__FILE__, "repeated_concurrent_autoload.rb")
+ autoload_path = file_path.sub(/\.rb\Z/, '')
+ mod_count = 30
+ thread_count = 16
+
+ mod_names = []
+ mod_count.times do |i|
+ mod_name = :"Mod#{i}"
+ Object.autoload mod_name, autoload_path
+ mod_names << mod_name
+ end
+
+ barrier = ModuleSpecs::CyclicBarrier.new thread_count
+ ScratchPad.record ModuleSpecs::ThreadSafeCounter.new
+
+ threads = (1..thread_count).map do
+ Thread.new do
+ mod_names.each do |mod_name|
+ break false unless barrier.enabled?
+
+ was_last_one_in = barrier.await # wait for all threads to finish the iteration
+ # clean up so we can autoload the same file again
+ $LOADED_FEATURES.delete(file_path) if was_last_one_in && $LOADED_FEATURES.include?(file_path)
+ barrier.await # get ready for race
+
+ begin
+ Object.const_get(mod_name).foo
+ rescue NameError, NoMethodError # rubocop:disable Lint/ShadowedException
+ barrier.disable!
+ break false
+ end
+ end
+ end
+ end
+
+ # check that no thread got a NameError or NoMethodError because of partially loaded module
+ threads.all? {|t| t.value}.should == true
+
+ # check that the autoloaded file was evaled exactly once
+ ScratchPad.recorded.get.should == mod_count
+
+ mod_names.each do |mod_name|
+ Object.send(:remove_const, mod_name)
+ end
+ ensure
+ threads.each(&:join) if threads
+ end
+
+ it "raises a NameError in each thread if the constant is not set" do
+ file = fixture(__FILE__, "autoload_never_set.rb")
+ start = false
+
+ threads = Array.new(10) do
+ Thread.new do
+ Thread.pass until start
+ begin
+ ModuleSpecs::Autoload.autoload :NeverSetConstant, file
+ Thread.pass
+ ModuleSpecs::Autoload::NeverSetConstant
+ rescue NameError => e
+ e
+ ensure
+ Thread.pass
+ end
+ end
+ end
+
+ start = true
+ threads.each { |t|
+ t.value.should.instance_of?(NameError)
+ }
+ end
+
+ it "raises a LoadError in each thread if the file does not exist" do
+ file = fixture(__FILE__, "autoload_does_not_exist.rb")
+ start = false
+
+ threads = Array.new(10) do
+ Thread.new do
+ Thread.pass until start
+ begin
+ ModuleSpecs::Autoload.autoload :FileDoesNotExist, file
+ Thread.pass
+ ModuleSpecs::Autoload::FileDoesNotExist
+ rescue LoadError => e
+ e
+ ensure
+ Thread.pass
+ end
+ end
+ end
+
+ start = true
+ threads.each { |t|
+ t.value.should.instance_of?(LoadError)
+ }
+ end
+ end
+
+ it "loads the registered constant even if the constant was already loaded by another thread" do
+ Thread.new {
+ ModuleSpecs::Autoload::FromThread::D.foo
+ }.value.should == :foo
+ end
+end
diff --git a/spec/ruby/core/module/case_compare_spec.rb b/spec/ruby/core/module/case_compare_spec.rb
new file mode 100644
index 0000000000..49ac359f6f
--- /dev/null
+++ b/spec/ruby/core/module/case_compare_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#===" do
+ it "returns true when the given Object is an instance of self or of self's descendants" do
+ (ModuleSpecs::Child === ModuleSpecs::Child.new).should == true
+ (ModuleSpecs::Parent === ModuleSpecs::Parent.new).should == true
+
+ (ModuleSpecs::Parent === ModuleSpecs::Child.new).should == true
+ (Object === ModuleSpecs::Child.new).should == true
+
+ (ModuleSpecs::Child === String.new).should == false
+ (ModuleSpecs::Child === mock('x')).should == false
+ end
+
+ it "returns true when the given Object's class includes self or when the given Object is extended by self" do
+ (ModuleSpecs::Basic === ModuleSpecs::Child.new).should == true
+ (ModuleSpecs::Super === ModuleSpecs::Child.new).should == true
+ (ModuleSpecs::Basic === mock('x').extend(ModuleSpecs::Super)).should == true
+ (ModuleSpecs::Super === mock('y').extend(ModuleSpecs::Super)).should == true
+
+ (ModuleSpecs::Basic === ModuleSpecs::Parent.new).should == false
+ (ModuleSpecs::Super === ModuleSpecs::Parent.new).should == false
+ (ModuleSpecs::Basic === mock('z')).should == false
+ (ModuleSpecs::Super === mock('a')).should == false
+ end
+
+ it "does not let a module singleton class interfere when its on the RHS" do
+ (Class === ModuleSpecs::CaseCompareOnSingleton).should == false
+ end
+end
diff --git a/spec/ruby/core/module/class_eval_spec.rb b/spec/ruby/core/module/class_eval_spec.rb
new file mode 100644
index 0000000000..c6665d5aff
--- /dev/null
+++ b/spec/ruby/core/module/class_eval_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/class_eval'
+
+describe "Module#class_eval" do
+ it_behaves_like :module_class_eval, :class_eval
+end
diff --git a/spec/ruby/core/module/class_exec_spec.rb b/spec/ruby/core/module/class_exec_spec.rb
new file mode 100644
index 0000000000..4acd0169ad
--- /dev/null
+++ b/spec/ruby/core/module/class_exec_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/class_exec'
+
+describe "Module#class_exec" do
+ it_behaves_like :module_class_exec, :class_exec
+end
diff --git a/spec/ruby/core/module/class_variable_defined_spec.rb b/spec/ruby/core/module/class_variable_defined_spec.rb
new file mode 100644
index 0000000000..dedce9589a
--- /dev/null
+++ b/spec/ruby/core/module/class_variable_defined_spec.rb
@@ -0,0 +1,72 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#class_variable_defined?" do
+ it "returns true if a class variable with the given name is defined in self" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ c.class_variable_defined?(:@@class_var).should == true
+ c.class_variable_defined?("@@class_var").should == true
+ c.class_variable_defined?(:@@no_class_var).should == false
+ c.class_variable_defined?("@@no_class_var").should == false
+ ModuleSpecs::CVars.class_variable_defined?("@@cls").should == true
+ end
+
+ it "returns true if a class variable with the given name is defined in the metaclass" do
+ ModuleSpecs::CVars.class_variable_defined?("@@meta").should == true
+ end
+
+ it "returns true if the class variable is defined in a metaclass" do
+ obj = mock("metaclass class variable")
+ meta = obj.singleton_class
+ meta.send :class_variable_set, :@@var, 1
+ meta.send(:class_variable_defined?, :@@var).should == true
+ end
+
+ it "returns false if the class variable is not defined in a metaclass" do
+ obj = mock("metaclass class variable")
+ meta = obj.singleton_class
+ meta.class_variable_defined?(:@@var).should == false
+ end
+
+ it "returns true if a class variables with the given name is defined in an included module" do
+ c = Class.new { include ModuleSpecs::MVars }
+ c.class_variable_defined?("@@mvar").should == true
+ end
+
+ it "returns false if a class variables with the given name is defined in an extended module" do
+ c = Class.new
+ c.extend ModuleSpecs::MVars
+ c.class_variable_defined?("@@mvar").should == false
+ end
+
+ it "raises a NameError when the given name is not allowed" do
+ c = Class.new
+
+ -> {
+ c.class_variable_defined?(:invalid_name)
+ }.should.raise(NameError)
+
+ -> {
+ c.class_variable_defined?("@invalid_name")
+ }.should.raise(NameError)
+ end
+
+ it "converts a non string/symbol name to string using to_str" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ (o = mock('@@class_var')).should_receive(:to_str).and_return("@@class_var")
+ c.class_variable_defined?(o).should == true
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ o = mock('123')
+ -> {
+ c.class_variable_defined?(o)
+ }.should.raise(TypeError)
+
+ o.should_receive(:to_str).and_return(123)
+ -> {
+ c.class_variable_defined?(o)
+ }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/class_variable_get_spec.rb b/spec/ruby/core/module/class_variable_get_spec.rb
new file mode 100644
index 0000000000..13f06cb94a
--- /dev/null
+++ b/spec/ruby/core/module/class_variable_get_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#class_variable_get" do
+ it "returns the value of the class variable with the given name" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ c.send(:class_variable_get, :@@class_var).should == "test"
+ c.send(:class_variable_get, "@@class_var").should == "test"
+ end
+
+ it "returns the value of a class variable with the given name defined in an included module" do
+ c = Class.new { include ModuleSpecs::MVars }
+ c.send(:class_variable_get, "@@mvar").should == :mvar
+ end
+
+ it "raises a NameError for a class variable named '@@'" do
+ c = Class.new
+ -> { c.send(:class_variable_get, "@@") }.should.raise(NameError)
+ -> { c.send(:class_variable_get, :"@@") }.should.raise(NameError)
+ end
+
+ it "raises a NameError for a class variables with the given name defined in an extended module" do
+ c = Class.new
+ c.extend ModuleSpecs::MVars
+ -> {
+ c.send(:class_variable_get, "@@mvar")
+ }.should.raise(NameError)
+ end
+
+ it "returns class variables defined in the class body and accessed in the metaclass" do
+ ModuleSpecs::CVars.cls.should == :class
+ end
+
+ it "returns class variables defined in the metaclass and accessed by class methods" do
+ ModuleSpecs::CVars.meta.should == :metainfo
+ end
+
+ it "returns class variables defined in the metaclass and accessed by instance methods" do
+ ModuleSpecs::CVars.new.meta.should == :metainfo
+ end
+
+ it "returns a class variable defined in a metaclass" do
+ obj = mock("metaclass class variable")
+ meta = obj.singleton_class
+ meta.send :class_variable_set, :@@var, :cvar_value
+ meta.send(:class_variable_get, :@@var).should == :cvar_value
+ end
+
+ it "raises a NameError when an uninitialized class variable is accessed" do
+ c = Class.new
+ [:@@no_class_var, "@@no_class_var"].each do |cvar|
+ -> { c.send(:class_variable_get, cvar) }.should.raise(NameError)
+ end
+ end
+
+ it "raises a NameError when the given name is not allowed" do
+ c = Class.new
+
+ -> { c.send(:class_variable_get, :invalid_name) }.should.raise(NameError)
+ -> { c.send(:class_variable_get, "@invalid_name") }.should.raise(NameError)
+ end
+
+ it "converts a non string/symbol name to string using to_str" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ (o = mock('@@class_var')).should_receive(:to_str).and_return("@@class_var")
+ c.send(:class_variable_get, o).should == "test"
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ o = mock('123')
+ -> { c.send(:class_variable_get, o) }.should.raise(TypeError)
+ o.should_receive(:to_str).and_return(123)
+ -> { c.send(:class_variable_get, o) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/class_variable_set_spec.rb b/spec/ruby/core/module/class_variable_set_spec.rb
new file mode 100644
index 0000000000..a3d759767b
--- /dev/null
+++ b/spec/ruby/core/module/class_variable_set_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#class_variable_set" do
+ it "sets the class variable with the given name to the given value" do
+ c = Class.new
+
+ c.send(:class_variable_set, :@@test, "test")
+ c.send(:class_variable_set, "@@test3", "test3")
+
+ c.send(:class_variable_get, :@@test).should == "test"
+ c.send(:class_variable_get, :@@test3).should == "test3"
+ end
+
+ it "sets a class variable on a metaclass" do
+ obj = mock("metaclass class variable")
+ meta = obj.singleton_class
+ meta.send(:class_variable_set, :@@var, :cvar_value).should == :cvar_value
+ meta.send(:class_variable_get, :@@var).should == :cvar_value
+ end
+
+ it "sets the value of a class variable with the given name defined in an included module" do
+ c = Class.new { include ModuleSpecs::MVars.dup }
+ c.send(:class_variable_set, "@@mvar", :new_mvar).should == :new_mvar
+ c.send(:class_variable_get, "@@mvar").should == :new_mvar
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ -> {
+ Class.new.freeze.send(:class_variable_set, :@@test, "test")
+ }.should.raise(FrozenError)
+ -> {
+ Module.new.freeze.send(:class_variable_set, :@@test, "test")
+ }.should.raise(FrozenError)
+ end
+
+ it "raises a NameError when the given name is not allowed" do
+ c = Class.new
+
+ -> {
+ c.send(:class_variable_set, :invalid_name, "test")
+ }.should.raise(NameError)
+ -> {
+ c.send(:class_variable_set, "@invalid_name", "test")
+ }.should.raise(NameError)
+ end
+
+ it "converts a non string/symbol name to string using to_str" do
+ (o = mock('@@class_var')).should_receive(:to_str).and_return("@@class_var")
+ c = Class.new
+ c.send(:class_variable_set, o, "test")
+ c.send(:class_variable_get, :@@class_var).should == "test"
+ end
+
+ it "raises a TypeError when the given names can't be converted to strings using to_str" do
+ c = Class.new { class_variable_set :@@class_var, "test" }
+ o = mock('123')
+ -> { c.send(:class_variable_set, o, "test") }.should.raise(TypeError)
+ o.should_receive(:to_str).and_return(123)
+ -> { c.send(:class_variable_set, o, "test") }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/class_variables_spec.rb b/spec/ruby/core/module/class_variables_spec.rb
new file mode 100644
index 0000000000..9529df48ae
--- /dev/null
+++ b/spec/ruby/core/module/class_variables_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#class_variables" do
+ it "returns an Array with the names of class variables of self" do
+ ModuleSpecs::ClassVars::A.class_variables.should.include?(:@@a_cvar)
+ ModuleSpecs::ClassVars::M.class_variables.should.include?(:@@m_cvar)
+ end
+
+ it "returns an Array of Symbols of class variable names defined in a metaclass" do
+ obj = mock("metaclass class variable")
+ meta = obj.singleton_class
+ meta.send :class_variable_set, :@@var, :cvar_value
+ meta.class_variables.should == [:@@var]
+ end
+
+ it "returns an Array with names of class variables defined in metaclasses" do
+ ModuleSpecs::CVars.class_variables.to_set.should >= Set[:@@cls, :@@meta]
+ end
+
+ it "does not return class variables defined in extended modules" do
+ c = Class.new
+ c.extend ModuleSpecs::MVars
+ c.class_variables.should_not.include?(:@@mvar)
+ end
+
+ it "returns the correct class variables when inherit is given" do
+ ModuleSpecs::SubCVars.class_variables(false).should == [:@@sub]
+ ModuleSpecs::SubCVars.new.singleton_class.class_variables(false).should == []
+
+ ModuleSpecs::SubCVars.class_variables(true).should == [:@@sub, :@@cls, :@@meta]
+ ModuleSpecs::SubCVars.new.singleton_class.class_variables(true).should == [:@@sub, :@@cls, :@@meta]
+ end
+end
diff --git a/spec/ruby/core/module/comparison_spec.rb b/spec/ruby/core/module/comparison_spec.rb
new file mode 100644
index 0000000000..86ee5db22a
--- /dev/null
+++ b/spec/ruby/core/module/comparison_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#<=>" do
+ it "returns -1 if self is a subclass of or includes the given module" do
+ (ModuleSpecs::Child <=> ModuleSpecs::Parent).should == -1
+ (ModuleSpecs::Child <=> ModuleSpecs::Basic).should == -1
+ (ModuleSpecs::Child <=> ModuleSpecs::Super).should == -1
+ (ModuleSpecs::Super <=> ModuleSpecs::Basic).should == -1
+ end
+
+ it "returns 0 if self is the same as the given module" do
+ (ModuleSpecs::Child <=> ModuleSpecs::Child).should == 0
+ (ModuleSpecs::Parent <=> ModuleSpecs::Parent).should == 0
+ (ModuleSpecs::Basic <=> ModuleSpecs::Basic).should == 0
+ (ModuleSpecs::Super <=> ModuleSpecs::Super).should == 0
+ end
+
+ it "returns +1 if self is a superclass of or included by the given module" do
+ (ModuleSpecs::Parent <=> ModuleSpecs::Child).should == +1
+ (ModuleSpecs::Basic <=> ModuleSpecs::Child).should == +1
+ (ModuleSpecs::Super <=> ModuleSpecs::Child).should == +1
+ (ModuleSpecs::Basic <=> ModuleSpecs::Super).should == +1
+ end
+
+ it "returns nil if self and the given module are not related" do
+ (ModuleSpecs::Parent <=> ModuleSpecs::Basic).should == nil
+ (ModuleSpecs::Parent <=> ModuleSpecs::Super).should == nil
+ (ModuleSpecs::Basic <=> ModuleSpecs::Parent).should == nil
+ (ModuleSpecs::Super <=> ModuleSpecs::Parent).should == nil
+ end
+
+ it "returns nil if the argument is not a class/module" do
+ (ModuleSpecs::Parent <=> mock('x')).should == nil
+ end
+end
diff --git a/spec/ruby/core/module/const_added_spec.rb b/spec/ruby/core/module/const_added_spec.rb
new file mode 100644
index 0000000000..b60af7a2e8
--- /dev/null
+++ b/spec/ruby/core/module/const_added_spec.rb
@@ -0,0 +1,238 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'fixtures/const_added'
+
+describe "Module#const_added" do
+ it "is a private instance method" do
+ Module.private_instance_methods(false).should.include?(:const_added)
+ end
+
+ it "returns nil in the default implementation" do
+ Module.new do
+ const_added(:TEST).should == nil
+ end
+ end
+
+ it "for a class defined with the `class` keyword, const_added runs before inherited" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(_)
+ ScratchPad << :const_added
+ end
+ end
+
+ parent = Class.new do
+ def self.inherited(_)
+ ScratchPad << :inherited
+ end
+ end
+
+ class mod::C < parent; end
+
+ ScratchPad.recorded.should == [:const_added, :inherited]
+ end
+
+ it "the superclass of a class assigned to a constant is set before const_added is called" do
+ ScratchPad.record []
+
+ parent = Class.new do
+ def self.const_added(name)
+ ScratchPad << name
+ ScratchPad << const_get(name).superclass
+ end
+ end
+
+ class parent::C < parent; end
+
+ ScratchPad.recorded.should == [:C, parent]
+ end
+
+ it "is called when a new constant is assigned on self" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+ end
+
+ mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ TEST = 1
+ RUBY
+
+ ScratchPad.recorded.should == [:TEST]
+ end
+
+ it "is called when a new constant is assigned on self through const_set" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+ end
+
+ mod.const_set(:TEST, 1)
+
+ ScratchPad.recorded.should == [:TEST]
+ end
+
+ it "is called when a new module is defined under self" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+ end
+
+ mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ module SubModule
+ end
+
+ module SubModule
+ end
+ RUBY
+
+ ScratchPad.recorded.should == [:SubModule]
+ end
+
+ it "is called when a new module is defined under a named module (assigned to a constant)" do
+ ScratchPad.record []
+
+ ModuleSpecs::ConstAddedSpecs::NamedModule = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+
+ module self::A
+ def self.const_added(name)
+ ScratchPad << name
+ end
+
+ module self::B
+ end
+ end
+ end
+
+ ScratchPad.recorded.should == [:A, :B]
+ ModuleSpecs::ConstAddedSpecs.send :remove_const, :NamedModule
+ end
+
+ it "is called when a new class is defined under self" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+ end
+
+ mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ class SubClass
+ end
+
+ class SubClass
+ end
+ RUBY
+
+ ScratchPad.recorded.should == [:SubClass]
+ end
+
+ it "is called when a new class is defined under a named module (assigned to a constant)" do
+ ScratchPad.record []
+
+ ModuleSpecs::ConstAddedSpecs::NamedModuleB = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+
+ class self::A
+ def self.const_added(name)
+ ScratchPad << name
+ end
+
+ class self::B
+ end
+ end
+ end
+
+ ScratchPad.recorded.should == [:A, :B]
+ ModuleSpecs::ConstAddedSpecs.send :remove_const, :NamedModuleB
+ end
+
+ it "is called when an autoload is defined" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad << name
+ end
+ end
+
+ mod.autoload :Autoload, "foo"
+ ScratchPad.recorded.should == [:Autoload]
+ end
+
+ it "is called with a precise caller location with the line of definition" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ location = caller_locations(1, 1)[0]
+ ScratchPad << location.lineno
+ end
+ end
+
+ line = __LINE__
+ mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ TEST = 1
+
+ module SubModule
+ end
+
+ class SubClass
+ end
+ RUBY
+
+ mod.const_set(:CONST_SET, 1)
+
+ ScratchPad.recorded.should == [line + 2, line + 4, line + 7, line + 11]
+ end
+
+ it "is called when the constant is already assigned a value" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad.record const_get(name)
+ end
+ end
+
+ mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ TEST = 123
+ RUBY
+
+ ScratchPad.recorded.should == 123
+ end
+
+ it "records re-definition of existing constants" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.const_added(name)
+ ScratchPad << const_get(name)
+ end
+ end
+
+ -> {
+ mod.module_eval(<<-RUBY, __FILE__, __LINE__ + 1)
+ TEST = 123
+ TEST = 456
+ RUBY
+ }.should complain(/warning: already initialized constant .+::TEST/)
+
+ ScratchPad.recorded.should == [123, 456]
+ end
+end
diff --git a/spec/ruby/core/module/const_defined_spec.rb b/spec/ruby/core/module/const_defined_spec.rb
new file mode 100644
index 0000000000..9d973c2b2b
--- /dev/null
+++ b/spec/ruby/core/module/const_defined_spec.rb
@@ -0,0 +1,169 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+require_relative 'fixtures/constant_unicode'
+
+describe "Module#const_defined?" do
+ it "returns true if the given Symbol names a constant defined in the receiver" do
+ ConstantSpecs.const_defined?(:CS_CONST2).should == true
+ ConstantSpecs.const_defined?(:ModuleA).should == true
+ ConstantSpecs.const_defined?(:ClassA).should == true
+ ConstantSpecs::ContainerA.const_defined?(:ChildA).should == true
+ end
+
+ it "returns true if the constant is defined in the receiver's superclass" do
+ # CS_CONST4 is defined in the superclass of ChildA
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4).should == true
+ end
+
+ it "returns true if the constant is defined in a mixed-in module of the receiver's parent" do
+ # CS_CONST10 is defined in a module included by ChildA
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST10).should == true
+ end
+
+ it "returns true if the constant is defined in a mixed-in module (with prepends) of the receiver" do
+ # CS_CONST11 is defined in the module included by ContainerPrepend
+ ConstantSpecs::ContainerPrepend.const_defined?(:CS_CONST11).should == true
+ end
+
+ it "returns true if the constant is defined in Object and the receiver is a module" do
+ # CS_CONST1 is defined in Object
+ ConstantSpecs::ModuleA.const_defined?(:CS_CONST1).should == true
+ end
+
+ it "returns true if the constant is defined in Object and the receiver is a class that has Object among its ancestors" do
+ # CS_CONST1 is defined in Object
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST1).should == true
+ end
+
+ it "returns false if the constant is defined in the receiver's superclass and the inherit flag is false" do
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, false).should == false
+ end
+
+ it "returns true if the constant is defined in the receiver's superclass and the inherit flag is true" do
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, true).should == true
+ end
+
+ it "coerces the inherit flag to a boolean" do
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, nil).should == false
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4, :true).should == true
+ end
+
+ it "returns true if the given String names a constant defined in the receiver" do
+ ConstantSpecs.const_defined?("CS_CONST2").should == true
+ ConstantSpecs.const_defined?("ModuleA").should == true
+ ConstantSpecs.const_defined?("ClassA").should == true
+ ConstantSpecs::ContainerA.const_defined?("ChildA").should == true
+ end
+
+ it "returns true when passed a constant name with unicode characters" do
+ ConstantUnicodeSpecs.const_defined?("CS_CONSTλ").should == true
+ end
+
+ it "returns true when passed a constant name with EUC-JP characters" do
+ str = "CS_CONSTλ".encode("euc-jp")
+ ConstantSpecs.const_set str, 1
+ ConstantSpecs.const_defined?(str).should == true
+ ensure
+ ConstantSpecs.send(:remove_const, str)
+ end
+
+ it "returns false if the constant is not defined in the receiver, its superclass, or any included modules" do
+ # The following constant isn't defined at all.
+ ConstantSpecs::ContainerA::ChildA.const_defined?(:CS_CONST4726).should == false
+ # DETACHED_CONSTANT is defined in ConstantSpecs::Detached, which isn't
+ # included by or inherited from ParentA
+ ConstantSpecs::ParentA.const_defined?(:DETACHED_CONSTANT).should == false
+ end
+
+ it "does not call #const_missing if the constant is not defined in the receiver" do
+ ConstantSpecs::ClassA.should_not_receive(:const_missing)
+ ConstantSpecs::ClassA.const_defined?(:CS_CONSTX).should == false
+ end
+
+ describe "converts the given name to a String using #to_str" do
+ it "calls #to_str to convert the given name to a String" do
+ name = mock("ClassA")
+ name.should_receive(:to_str).and_return("ClassA")
+ ConstantSpecs.const_defined?(name).should == true
+ end
+
+ it "raises a TypeError if the given name can't be converted to a String" do
+ -> { ConstantSpecs.const_defined?(nil) }.should.raise(TypeError)
+ -> { ConstantSpecs.const_defined?([]) }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do
+ name = mock("classA")
+ name.should_receive(:to_str).and_raise(NoMethodError)
+ -> { ConstantSpecs.const_defined?(name) }.should.raise(NoMethodError)
+ end
+ end
+
+ it "special cases Object and checks it's included Modules" do
+ Object.const_defined?(:CS_CONST10).should == true
+ end
+
+ it "returns true for toplevel constant when the name begins with '::'" do
+ ConstantSpecs.const_defined?("::Array").should == true
+ end
+
+ it "returns true when passed a scoped constant name" do
+ ConstantSpecs.const_defined?("ClassC::CS_CONST1").should == true
+ end
+
+ it "returns true when passed a scoped constant name for a constant in the inheritance hierarchy and the inherited flag is default" do
+ ConstantSpecs::ClassD.const_defined?("ClassE::CS_CONST2").should == true
+ end
+
+ it "returns true when passed a scoped constant name for a constant in the inheritance hierarchy and the inherited flag is true" do
+ ConstantSpecs::ClassD.const_defined?("ClassE::CS_CONST2", true).should == true
+ end
+
+ it "returns false when passed a scoped constant name for a constant in the inheritance hierarchy and the inherited flag is false" do
+ ConstantSpecs::ClassD.const_defined?("ClassE::CS_CONST2", false).should == false
+ end
+
+ it "returns false when the name begins with '::' and the toplevel constant does not exist" do
+ ConstantSpecs.const_defined?("::Name").should == false
+ end
+
+ it "raises a NameError if the name does not start with a capital letter" do
+ -> { ConstantSpecs.const_defined? "name" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with '_'" do
+ -> { ConstantSpecs.const_defined? "__CONSTX__" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with '@'" do
+ -> { ConstantSpecs.const_defined? "@Name" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with '!'" do
+ -> { ConstantSpecs.const_defined? "!Name" }.should.raise(NameError)
+ end
+
+ it "returns true or false for the nested name" do
+ ConstantSpecs.const_defined?("NotExist::Name").should == false
+ ConstantSpecs.const_defined?("::Name").should == false
+ ConstantSpecs.const_defined?("::Object").should == true
+ ConstantSpecs.const_defined?("ClassA::CS_CONST10").should == true
+ ConstantSpecs.const_defined?("ClassA::CS_CONST10_").should == false
+ end
+
+ it "raises a NameError if the name contains non-alphabetic characters except '_'" do
+ ConstantSpecs.const_defined?("CS_CONSTX").should == false
+ -> { ConstantSpecs.const_defined? "Name=" }.should.raise(NameError)
+ -> { ConstantSpecs.const_defined? "Name?" }.should.raise(NameError)
+ end
+
+ it "raises a TypeError if conversion to a String by calling #to_str fails" do
+ name = mock('123')
+ -> { ConstantSpecs.const_defined? name }.should.raise(TypeError)
+
+ name.should_receive(:to_str).and_return(123)
+ -> { ConstantSpecs.const_defined? name }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/const_get_spec.rb b/spec/ruby/core/module/const_get_spec.rb
new file mode 100644
index 0000000000..68b5594faa
--- /dev/null
+++ b/spec/ruby/core/module/const_get_spec.rb
@@ -0,0 +1,273 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+require_relative 'fixtures/constants_autoload'
+
+describe "Module#const_get" do
+ it "accepts a String or Symbol name" do
+ Object.const_get(:CS_CONST1).should == :const1
+ Object.const_get("CS_CONST1").should == :const1
+ end
+
+ it "raises a NameError if no constant is defined in the search path" do
+ -> { ConstantSpecs.const_get :CS_CONSTX }.should.raise(NameError)
+ end
+
+ it "raises a NameError with the not found constant symbol" do
+ error_inspection = -> e { e.name.should == :CS_CONSTX }
+ -> { ConstantSpecs.const_get :CS_CONSTX }.should.raise(NameError, &error_inspection)
+ end
+
+ it "raises a NameError if the name does not start with a capital letter" do
+ -> { ConstantSpecs.const_get "name" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with a non-alphabetic character" do
+ -> { ConstantSpecs.const_get "__CONSTX__" }.should.raise(NameError)
+ -> { ConstantSpecs.const_get "@CS_CONST1" }.should.raise(NameError)
+ -> { ConstantSpecs.const_get "!CS_CONST1" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name contains non-alphabetic characters except '_'" do
+ Object.const_get("CS_CONST1").should == :const1
+ -> { ConstantSpecs.const_get "CS_CONST1=" }.should.raise(NameError)
+ -> { ConstantSpecs.const_get "CS_CONST1?" }.should.raise(NameError)
+ end
+
+ it "calls #to_str to convert the given name to a String" do
+ name = mock("ClassA")
+ name.should_receive(:to_str).and_return("ClassA")
+ ConstantSpecs.const_get(name).should == ConstantSpecs::ClassA
+ end
+
+ it "raises a TypeError if conversion to a String by calling #to_str fails" do
+ name = mock('123')
+ -> { ConstantSpecs.const_get(name) }.should.raise(TypeError)
+
+ name.should_receive(:to_str).and_return(123)
+ -> { ConstantSpecs.const_get(name) }.should.raise(TypeError)
+ end
+
+ it "calls #const_missing on the receiver if unable to locate the constant" do
+ ConstantSpecs::ContainerA.should_receive(:const_missing).with(:CS_CONSTX)
+ ConstantSpecs::ContainerA.const_get(:CS_CONSTX)
+ end
+
+ it "does not search the singleton class of a Class or Module" do
+ -> do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST14)
+ end.should.raise(NameError)
+ -> { ConstantSpecs.const_get(:CS_CONST14) }.should.raise(NameError)
+ end
+
+ it "does not search the containing scope" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST20).should == :const20_2
+ -> do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST5)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError if the constant is defined in the receiver's superclass and the inherit flag is false" do
+ -> do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST4, false)
+ end.should.raise(NameError)
+ end
+
+ it "searches into the receiver superclasses if the inherit flag is true" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST4, true).should == :const4
+ end
+
+ it "raises a NameError when the receiver is a Module, the constant is defined at toplevel and the inherit flag is false" do
+ -> do
+ ConstantSpecs::ModuleA.const_get(:CS_CONST1, false)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError when the receiver is a Class, the constant is defined at toplevel and the inherit flag is false" do
+ -> do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST1, false)
+ end.should.raise(NameError)
+ end
+
+ it "coerces the inherit flag to a boolean" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST4, :true).should == :const4
+
+ -> do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST1, nil)
+ end.should.raise(NameError)
+ end
+
+ it "accepts a toplevel scope qualifier" do
+ ConstantSpecs.const_get("::CS_CONST1").should == :const1
+ end
+
+ it "accepts a toplevel scope qualifier when inherit is false" do
+ ConstantSpecs.const_get("::CS_CONST1", false).should == :const1
+ -> { ConstantSpecs.const_get("CS_CONST1", false) }.should.raise(NameError)
+ end
+
+ it "returns a constant whose module is defined the toplevel" do
+ ConstantSpecs.const_get("ConstantSpecsTwo::Foo").should == :cs_two_foo
+ ConstantSpecsThree.const_get("ConstantSpecsTwo::Foo").should == :cs_three_foo
+ end
+
+ it "accepts a scoped constant name" do
+ ConstantSpecs.const_get("ClassA::CS_CONST10").should == :const10_10
+ end
+
+ it "raises a NameError if the name includes two successive scope separators" do
+ -> { ConstantSpecs.const_get("ClassA::::CS_CONST10") }.should.raise(NameError)
+ end
+
+ it "raises a NameError if only '::' is passed" do
+ -> { ConstantSpecs.const_get("::") }.should.raise(NameError)
+ end
+
+ it "raises a NameError if a Symbol has a toplevel scope qualifier" do
+ -> { ConstantSpecs.const_get(:'::CS_CONST1') }.should.raise(NameError)
+ end
+
+ it "raises a NameError if a Symbol is a scoped constant name" do
+ -> { ConstantSpecs.const_get(:'ClassA::CS_CONST10') }.should.raise(NameError)
+ end
+
+ it "does read private constants" do
+ ConstantSpecs.const_get(:CS_PRIVATE).should == :cs_private
+ end
+
+ it 'does autoload a constant' do
+ Object.const_get('CSAutoloadA').name.should == 'CSAutoloadA'
+ end
+
+ it 'does autoload a constant with a toplevel scope qualifier' do
+ Object.const_get('::CSAutoloadB').name.should == 'CSAutoloadB'
+ end
+
+ it 'does autoload a module and resolve a constant within' do
+ Object.const_get('CSAutoloadC::CONST').should == 7
+ end
+
+ it 'does autoload a non-toplevel module' do
+ Object.const_get('CSAutoloadD::InnerModule').name.should == 'CSAutoloadD::InnerModule'
+ end
+
+ it "raises a NameError when the nested constant does not exist on the module but exists in Object" do
+ -> { Object.const_get('ConstantSpecs::CS_CONST1') }.should.raise(NameError)
+ end
+
+ describe "with statically assigned constants" do
+ it "searches the immediate class or module first" do
+ ConstantSpecs::ClassA.const_get(:CS_CONST10).should == :const10_10
+ ConstantSpecs::ModuleA.const_get(:CS_CONST10).should == :const10_1
+ ConstantSpecs::ParentA.const_get(:CS_CONST10).should == :const10_5
+ ConstantSpecs::ContainerA.const_get(:CS_CONST10).should == :const10_2
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST10).should == :const10_3
+ end
+
+ it "searches a module included in the immediate class before the superclass" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST15).should == :const15_1
+ end
+
+ it "searches the superclass before a module included in the superclass" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST11).should == :const11_1
+ end
+
+ it "searches a module included in the superclass" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST12).should == :const12_1
+ end
+
+ it "searches the superclass chain" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST13).should == :const13
+ end
+
+ it "returns a toplevel constant when the receiver is a Class" do
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST1).should == :const1
+ end
+
+ it "returns a toplevel constant when the receiver is a Module" do
+ ConstantSpecs.const_get(:CS_CONST1).should == :const1
+ ConstantSpecs::ModuleA.const_get(:CS_CONST1).should == :const1
+ end
+ end
+
+ describe "with dynamically assigned constants" do
+ it "searches the immediate class or module first" do
+ ConstantSpecs::ClassA::CS_CONST301 = :const301_1
+ ConstantSpecs::ClassA.const_get(:CS_CONST301).should == :const301_1
+
+ ConstantSpecs::ModuleA::CS_CONST301 = :const301_2
+ ConstantSpecs::ModuleA.const_get(:CS_CONST301).should == :const301_2
+
+ ConstantSpecs::ParentA::CS_CONST301 = :const301_3
+ ConstantSpecs::ParentA.const_get(:CS_CONST301).should == :const301_3
+
+ ConstantSpecs::ContainerA::ChildA::CS_CONST301 = :const301_5
+ ConstantSpecs::ContainerA::ChildA.const_get(:CS_CONST301).should == :const301_5
+ ensure
+ ConstantSpecs::ClassA.send(:remove_const, :CS_CONST301)
+ ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST301)
+ ConstantSpecs::ParentA.send(:remove_const, :CS_CONST301)
+ ConstantSpecs::ContainerA::ChildA.send(:remove_const, :CS_CONST301)
+ end
+
+ it "searches a module included in the immediate class before the superclass" do
+ ConstantSpecs::ParentB::CS_CONST302 = :const302_1
+ ConstantSpecs::ModuleF::CS_CONST302 = :const302_2
+ ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST302).should == :const302_2
+ ensure
+ ConstantSpecs::ParentB.send(:remove_const, :CS_CONST302)
+ ConstantSpecs::ModuleF.send(:remove_const, :CS_CONST302)
+ end
+
+ it "searches the superclass before a module included in the superclass" do
+ ConstantSpecs::ModuleE::CS_CONST303 = :const303_1
+ ConstantSpecs::ParentB::CS_CONST303 = :const303_2
+ ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST303).should == :const303_2
+ ensure
+ ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST303)
+ ConstantSpecs::ParentB.send(:remove_const, :CS_CONST303)
+ end
+
+ it "searches a module included in the superclass" do
+ ConstantSpecs::ModuleA::CS_CONST304 = :const304_1
+ ConstantSpecs::ModuleE::CS_CONST304 = :const304_2
+ ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST304).should == :const304_2
+ ensure
+ ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST304)
+ ConstantSpecs::ModuleE.send(:remove_const, :CS_CONST304)
+ end
+
+ it "searches the superclass chain" do
+ ConstantSpecs::ModuleA::CS_CONST305 = :const305
+ ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST305).should == :const305
+ ensure
+ ConstantSpecs::ModuleA.send(:remove_const, :CS_CONST305)
+ end
+
+ it "returns a toplevel constant when the receiver is a Class" do
+ Object::CS_CONST306 = :const306
+ ConstantSpecs::ContainerB::ChildB.const_get(:CS_CONST306).should == :const306
+ ensure
+ Object.send(:remove_const, :CS_CONST306)
+ end
+
+ it "returns a toplevel constant when the receiver is a Module" do
+ Object::CS_CONST308 = :const308
+ ConstantSpecs.const_get(:CS_CONST308).should == :const308
+ ConstantSpecs::ModuleA.const_get(:CS_CONST308).should == :const308
+ ensure
+ Object.send(:remove_const, :CS_CONST308)
+ end
+
+ it "returns the updated value of a constant" do
+ ConstantSpecs::ClassB::CS_CONST309 = :const309_1
+ ConstantSpecs::ClassB.const_get(:CS_CONST309).should == :const309_1
+
+ -> {
+ ConstantSpecs::ClassB::CS_CONST309 = :const309_2
+ }.should complain(/already initialized constant/)
+ ConstantSpecs::ClassB.const_get(:CS_CONST309).should == :const309_2
+ ensure
+ ConstantSpecs::ClassB.send(:remove_const, :CS_CONST309)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/const_missing_spec.rb b/spec/ruby/core/module/const_missing_spec.rb
new file mode 100644
index 0000000000..80a2caccab
--- /dev/null
+++ b/spec/ruby/core/module/const_missing_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+
+describe "Module#const_missing" do
+ it "is called when an undefined constant is referenced via literal form" do
+ ConstantSpecs::ClassA::CS_CONSTX.should == :CS_CONSTX
+ end
+
+ it "is called when an undefined constant is referenced via #const_get" do
+ ConstantSpecs::ClassA.const_get(:CS_CONSTX).should == :CS_CONSTX
+ end
+
+ it "raises NameError and includes the name of the value that wasn't found" do
+ -> {
+ ConstantSpecs.const_missing("HelloMissing")
+ }.should.raise(NameError, /ConstantSpecs::HelloMissing/)
+ end
+
+ it "raises NameError and does not include toplevel Object" do
+ begin
+ Object.const_missing("HelloMissing")
+ rescue NameError => e
+ e.message.should_not =~ / Object::/
+ end
+ end
+
+ it "is called regardless of visibility" do
+ klass = Class.new do
+ def self.const_missing(name)
+ "Found:#{name}"
+ end
+ private_class_method :const_missing
+ end
+ klass::Hello.should == 'Found:Hello'
+ end
+end
diff --git a/spec/ruby/core/module/const_set_spec.rb b/spec/ruby/core/module/const_set_spec.rb
new file mode 100644
index 0000000000..aa3c6bbcfc
--- /dev/null
+++ b/spec/ruby/core/module/const_set_spec.rb
@@ -0,0 +1,145 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+
+describe "Module#const_set" do
+ it "sets the constant specified by a String or Symbol to the given value" do
+ ConstantSpecs.const_set :CS_CONST401, :const401
+ ConstantSpecs::CS_CONST401.should == :const401
+
+ ConstantSpecs.const_set "CS_CONST402", :const402
+ ConstantSpecs.const_get(:CS_CONST402).should == :const402
+ ensure
+ ConstantSpecs.send(:remove_const, :CS_CONST401)
+ ConstantSpecs.send(:remove_const, :CS_CONST402)
+ end
+
+ it "returns the value set" do
+ ConstantSpecs.const_set(:CS_CONST403, :const403).should == :const403
+ ensure
+ ConstantSpecs.send(:remove_const, :CS_CONST403)
+ end
+
+ it "sets the name of an anonymous module" do
+ m = Module.new
+ ConstantSpecs.const_set(:CS_CONST1000, m)
+ m.name.should == "ConstantSpecs::CS_CONST1000"
+ ensure
+ ConstantSpecs.send(:remove_const, :CS_CONST1000)
+ end
+
+ it "sets the name of a module scoped by an anonymous module" do
+ a, b = Module.new, Module.new
+ a.const_set :B, b
+ b.name.should.end_with? '::B'
+ end
+
+ it "sets the name of contained modules when assigning a toplevel anonymous module" do
+ a, b, c, d = Module.new, Module.new, Module.new, Module.new
+ a::B = b
+ a::B::C = c
+ a::B::C::E = c
+ a::D = d
+
+ Object.const_set :ModuleSpecs_CS3, a
+ a.name.should == "ModuleSpecs_CS3"
+ b.name.should == "ModuleSpecs_CS3::B"
+ c.name.should == "ModuleSpecs_CS3::B::C"
+ d.name.should == "ModuleSpecs_CS3::D"
+ ensure
+ Object.send(:remove_const, :ModuleSpecs_CS3)
+ end
+
+ it "raises a NameError if the name does not start with a capital letter" do
+ -> { ConstantSpecs.const_set "name", 1 }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with a non-alphabetic character" do
+ -> { ConstantSpecs.const_set "__CONSTX__", 1 }.should.raise(NameError)
+ -> { ConstantSpecs.const_set "@Name", 1 }.should.raise(NameError)
+ -> { ConstantSpecs.const_set "!Name", 1 }.should.raise(NameError)
+ -> { ConstantSpecs.const_set "::Name", 1 }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name contains non-alphabetic characters except '_'" do
+ ConstantSpecs.const_set("CS_CONST404", :const404).should == :const404
+ -> { ConstantSpecs.const_set "Name=", 1 }.should.raise(NameError)
+ -> { ConstantSpecs.const_set "Name?", 1 }.should.raise(NameError)
+ ensure
+ ConstantSpecs.send(:remove_const, :CS_CONST404)
+ end
+
+ it "calls #to_str to convert the given name to a String" do
+ name = mock("CS_CONST405")
+ name.should_receive(:to_str).and_return("CS_CONST405")
+ ConstantSpecs.const_set(name, :const405).should == :const405
+ ConstantSpecs::CS_CONST405.should == :const405
+ ensure
+ ConstantSpecs.send(:remove_const, :CS_CONST405)
+ end
+
+ it "raises a TypeError if conversion to a String by calling #to_str fails" do
+ name = mock('123')
+ -> { ConstantSpecs.const_set name, 1 }.should.raise(TypeError)
+
+ name.should_receive(:to_str).and_return(123)
+ -> { ConstantSpecs.const_set name, 1 }.should.raise(TypeError)
+ end
+
+ describe "when overwriting an existing constant" do
+ it "warns if the previous value was a normal value" do
+ mod = Module.new
+ mod.const_set :Foo, 42
+ -> {
+ mod.const_set :Foo, 1
+ }.should complain(/already initialized constant/)
+ mod.const_get(:Foo).should == 1
+ end
+
+ it "does not warn if the previous value was an autoload" do
+ mod = Module.new
+ mod.autoload :Foo, "not-existing"
+ -> {
+ mod.const_set :Foo, 1
+ }.should_not complain
+ mod.const_get(:Foo).should == 1
+ end
+
+ it "does not warn after a failed autoload" do
+ path = fixture(__FILE__, "autoload_o.rb")
+ ScratchPad.record []
+ mod = Module.new
+
+ mod.autoload :Foo, path
+ -> { mod::Foo }.should.raise(NameError)
+
+ mod.const_defined?(:Foo).should == false
+ mod.autoload?(:Foo).should == nil
+
+ -> {
+ mod.const_set :Foo, 1
+ }.should_not complain
+ mod.const_get(:Foo).should == 1
+ end
+
+ it "does not warn if the new value is an autoload" do
+ mod = Module.new
+ mod.const_set :Foo, 42
+ -> {
+ mod.autoload :Foo, "not-existing"
+ }.should_not complain
+ mod.const_get(:Foo).should == 42
+ end
+ end
+
+ describe "on a frozen module" do
+ before :each do
+ @frozen = Module.new.freeze
+ @name = :Foo
+ end
+
+ it "raises a FrozenError before setting the name" do
+ -> { @frozen.const_set @name, nil }.should.raise(FrozenError)
+ @frozen.should_not.const_defined?(@name)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/const_source_location_spec.rb b/spec/ruby/core/module/const_source_location_spec.rb
new file mode 100644
index 0000000000..e6cef727e2
--- /dev/null
+++ b/spec/ruby/core/module/const_source_location_spec.rb
@@ -0,0 +1,281 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+
+describe "Module#const_source_location" do
+ before do
+ @constants_fixture_path = File.expand_path('../../fixtures/constants.rb', __dir__)
+ end
+
+ describe "with dynamically assigned constants" do
+ it "searches a path in the immediate class or module first" do
+ ConstantSpecs::ClassA::CSL_CONST301 = :const301_1
+ ConstantSpecs::ClassA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1]
+
+ ConstantSpecs::ModuleA::CSL_CONST301 = :const301_2
+ ConstantSpecs::ModuleA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1]
+
+ ConstantSpecs::ParentA::CSL_CONST301 = :const301_3
+ ConstantSpecs::ParentA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1]
+
+ ConstantSpecs::ContainerA::ChildA::CSL_CONST301 = :const301_5
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CSL_CONST301).should == [__FILE__, __LINE__ - 1]
+ ensure
+ ConstantSpecs::ClassA.send(:remove_const, :CSL_CONST301)
+ ConstantSpecs::ModuleA.send(:remove_const, :CSL_CONST301)
+ ConstantSpecs::ParentA.send(:remove_const, :CSL_CONST301)
+ ConstantSpecs::ContainerA::ChildA.send(:remove_const, :CSL_CONST301)
+ end
+
+ it "searches a path in a module included in the immediate class before the superclass" do
+ ConstantSpecs::ParentB::CSL_CONST302 = :const302_1
+ ConstantSpecs::ModuleF::CSL_CONST302 = :const302_2
+ ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST302).should == [__FILE__, __LINE__ - 1]
+ ensure
+ ConstantSpecs::ParentB.send(:remove_const, :CSL_CONST302)
+ ConstantSpecs::ModuleF.send(:remove_const, :CSL_CONST302)
+ end
+
+ it "searches a path in the superclass before a module included in the superclass" do
+ ConstantSpecs::ModuleE::CSL_CONST303 = :const303_1
+ ConstantSpecs::ParentB::CSL_CONST303 = :const303_2
+ ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST303).should == [__FILE__, __LINE__ - 1]
+ ensure
+ ConstantSpecs::ModuleE.send(:remove_const, :CSL_CONST303)
+ ConstantSpecs::ParentB.send(:remove_const, :CSL_CONST303)
+ end
+
+ it "searches a path in a module included in the superclass" do
+ ConstantSpecs::ModuleA::CSL_CONST304 = :const304_1
+ ConstantSpecs::ModuleE::CSL_CONST304 = :const304_2
+ ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST304).should == [__FILE__, __LINE__ - 1]
+ ensure
+ ConstantSpecs::ModuleA.send(:remove_const, :CSL_CONST304)
+ ConstantSpecs::ModuleE.send(:remove_const, :CSL_CONST304)
+ end
+
+ it "searches a path in the superclass chain" do
+ ConstantSpecs::ModuleA::CSL_CONST305 = :const305
+ ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST305).should == [__FILE__, __LINE__ - 1]
+ ensure
+ ConstantSpecs::ModuleA.send(:remove_const, :CSL_CONST305)
+ end
+
+ it "returns path to a toplevel constant when the receiver is a Class" do
+ Object::CSL_CONST306 = :const306
+ ConstantSpecs::ContainerB::ChildB.const_source_location(:CSL_CONST306).should == [__FILE__, __LINE__ - 1]
+ ensure
+ Object.send(:remove_const, :CSL_CONST306)
+ end
+
+ it "returns path to a toplevel constant when the receiver is a Module" do
+ Object::CSL_CONST308 = :const308
+ ConstantSpecs.const_source_location(:CSL_CONST308).should == [__FILE__, __LINE__ - 1]
+ ConstantSpecs::ModuleA.const_source_location(:CSL_CONST308).should == [__FILE__, __LINE__ - 2]
+ ensure
+ Object.send(:remove_const, :CSL_CONST308)
+ end
+
+ it "returns path to the updated value of a constant" do
+ ConstantSpecs::ClassB::CSL_CONST309 = :const309_1
+ ConstantSpecs::ClassB.const_source_location(:CSL_CONST309).should == [__FILE__, __LINE__ - 1]
+
+ -> {
+ ConstantSpecs::ClassB::CSL_CONST309 = :const309_2
+ }.should complain(/already initialized constant/)
+ ConstantSpecs::ClassB.const_source_location(:CSL_CONST309).should == [__FILE__, __LINE__ - 2]
+ ensure
+ ConstantSpecs::ClassB.send(:remove_const, :CSL_CONST309)
+ end
+ end
+
+ describe "with statically assigned constants" do
+ it "works for the module and class keywords" do
+ ConstantSpecs.const_source_location(:ModuleB).should == [@constants_fixture_path, ConstantSpecs::ModuleB::LINE]
+ ConstantSpecs.const_source_location(:ClassA).should == [@constants_fixture_path, ConstantSpecs::ClassA::LINE]
+ end
+
+ it "searches location path the immediate class or module first" do
+ ConstantSpecs::ClassA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ClassA::CS_CONST10_LINE]
+ ConstantSpecs::ModuleA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ModuleA::CS_CONST10_LINE]
+ ConstantSpecs::ParentA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST10_LINE]
+ ConstantSpecs::ContainerA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ContainerA::CS_CONST10_LINE]
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, ConstantSpecs::ContainerA::ChildA::CS_CONST10_LINE]
+ end
+
+ it "searches location path a module included in the immediate class before the superclass" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST15).should == [@constants_fixture_path, ConstantSpecs::ModuleC::CS_CONST15_LINE]
+ end
+
+ it "searches location path the superclass before a module included in the superclass" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST11).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST11_LINE]
+ end
+
+ it "searches location path a module included in the superclass" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST12).should == [@constants_fixture_path, ConstantSpecs::ModuleB::CS_CONST12_LINE]
+ end
+
+ it "searches location path the superclass chain" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST13).should == [@constants_fixture_path, ConstantSpecs::ModuleA::CS_CONST13_LINE]
+ end
+
+ it "returns location path a toplevel constant when the receiver is a Class" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE]
+ end
+
+ it "returns location path a toplevel constant when the receiver is a Module" do
+ ConstantSpecs.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE]
+ ConstantSpecs::ModuleA.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE]
+ end
+ end
+
+ it "return empty path if constant defined in C code" do
+ Object.const_source_location(:String).should == []
+ end
+
+ it "accepts a String or Symbol name" do
+ Object.const_source_location(:CS_CONST1).should == [@constants_fixture_path, CS_CONST1_LINE]
+ Object.const_source_location("CS_CONST1").should == [@constants_fixture_path, CS_CONST1_LINE]
+ end
+
+ it "returns nil if no constant is defined in the search path" do
+ ConstantSpecs.const_source_location(:CS_CONSTX).should == nil
+ end
+
+ it "raises a NameError if the name does not start with a capital letter" do
+ -> { ConstantSpecs.const_source_location "name" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with a non-alphabetic character" do
+ -> { ConstantSpecs.const_source_location "__CONSTX__" }.should.raise(NameError)
+ -> { ConstantSpecs.const_source_location "@CS_CONST1" }.should.raise(NameError)
+ -> { ConstantSpecs.const_source_location "!CS_CONST1" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name contains non-alphabetic characters except '_'" do
+ Object.const_source_location("CS_CONST1").should == [@constants_fixture_path, CS_CONST1_LINE]
+ -> { ConstantSpecs.const_source_location "CS_CONST1=" }.should.raise(NameError)
+ -> { ConstantSpecs.const_source_location "CS_CONST1?" }.should.raise(NameError)
+ end
+
+ it "calls #to_str to convert the given name to a String" do
+ name = mock("ClassA")
+ name.should_receive(:to_str).and_return("ClassA")
+ ConstantSpecs.const_source_location(name).should == [@constants_fixture_path, ConstantSpecs::ClassA::LINE]
+ end
+
+ it "raises a TypeError if conversion to a String by calling #to_str fails" do
+ name = mock('123')
+ -> { ConstantSpecs.const_source_location(name) }.should.raise(TypeError)
+
+ name.should_receive(:to_str).and_return(123)
+ -> { ConstantSpecs.const_source_location(name) }.should.raise(TypeError)
+ end
+
+ it "does not search the singleton class of a Class or Module" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST14).should == nil
+ ConstantSpecs.const_source_location(:CS_CONST14).should == nil
+ end
+
+ it "does not search the containing scope" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST20).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST20_LINE]
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST5) == nil
+ end
+
+ it "returns nil if the constant is defined in the receiver's superclass and the inherit flag is false" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST4, false).should == nil
+ end
+
+ it "searches into the receiver superclasses if the inherit flag is true" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST4, true).should == [@constants_fixture_path, ConstantSpecs::ParentA::CS_CONST4_LINE]
+ end
+
+ it "returns nil when the receiver is a Module, the constant is defined at toplevel and the inherit flag is false" do
+ ConstantSpecs::ModuleA.const_source_location(:CS_CONST1, false).should == nil
+ end
+
+ it "returns nil when the receiver is a Class, the constant is defined at toplevel and the inherit flag is false" do
+ ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST1, false).should == nil
+ end
+
+ it "accepts a toplevel scope qualifier" do
+ ConstantSpecs.const_source_location("::CS_CONST1").should == [@constants_fixture_path, CS_CONST1_LINE]
+ end
+
+ it "accepts a scoped constant name" do
+ ConstantSpecs.const_source_location("ClassA::CS_CONST10").should == [@constants_fixture_path, ConstantSpecs::ClassA::CS_CONST10_LINE]
+ end
+
+ it "returns updated location from const_set" do
+ mod = Module.new
+ const_line = __LINE__ + 1
+ mod.const_set :Foo, 1
+ mod.const_source_location(:Foo).should == [__FILE__, const_line]
+ end
+
+ it "raises a NameError if the name includes two successive scope separators" do
+ -> { ConstantSpecs.const_source_location("ClassA::::CS_CONST10") }.should.raise(NameError)
+ end
+
+ it "raises a NameError if only '::' is passed" do
+ -> { ConstantSpecs.const_source_location("::") }.should.raise(NameError)
+ end
+
+ it "raises a NameError if a Symbol has a toplevel scope qualifier" do
+ -> { ConstantSpecs.const_source_location(:'::CS_CONST1') }.should.raise(NameError)
+ end
+
+ it "raises a NameError if a Symbol is a scoped constant name" do
+ -> { ConstantSpecs.const_source_location(:'ClassA::CS_CONST10') }.should.raise(NameError)
+ end
+
+ it "does search private constants path" do
+ ConstantSpecs.const_source_location(:CS_PRIVATE).should == [@constants_fixture_path, ConstantSpecs::CS_PRIVATE_LINE]
+ end
+
+ it "works for eval with a given line" do
+ c = Class.new do
+ eval('self::C = 1', nil, "foo", 100)
+ end
+ c.const_source_location(:C).should == ["foo", 100]
+ end
+
+ context 'autoload' do
+ before :all do
+ ConstantSpecs.autoload :CSL_CONST1, "#{__dir__}/notexisting.rb"
+ @line = __LINE__ - 1
+ end
+
+ before :each do
+ @loaded_features = $".dup
+ end
+
+ after :each do
+ $".replace @loaded_features
+ end
+
+ it 'returns the autoload location while not resolved' do
+ ConstantSpecs.const_source_location('CSL_CONST1').should == [__FILE__, @line]
+ end
+
+ it 'returns where the constant was resolved when resolved' do
+ file = fixture(__FILE__, 'autoload_location.rb')
+ ConstantSpecs.autoload :CONST_LOCATION, file
+ line = ConstantSpecs::CONST_LOCATION
+ ConstantSpecs.const_source_location('CONST_LOCATION').should == [file, line]
+ end
+
+ ruby_bug("#20188", ""..."3.4") do
+ it 'returns the real constant location as soon as it is defined' do
+ file = fixture(__FILE__, 'autoload_const_source_location.rb')
+ ConstantSpecs.autoload :ConstSource, file
+ autoload_location = [__FILE__, __LINE__ - 1]
+
+ ConstantSpecs.const_source_location(:ConstSource).should == autoload_location
+ ConstantSpecs::ConstSource::LOCATION.should == ConstantSpecs.const_source_location(:ConstSource)
+ ConstantSpecs::BEFORE_DEFINE_LOCATION.should == autoload_location
+ ConstantSpecs.send :remove_const, :ConstSource
+ ConstantSpecs.send :remove_const, :BEFORE_DEFINE_LOCATION
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/module/constants_spec.rb b/spec/ruby/core/module/constants_spec.rb
new file mode 100644
index 0000000000..f2f12761e3
--- /dev/null
+++ b/spec/ruby/core/module/constants_spec.rb
@@ -0,0 +1,98 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+require_relative 'fixtures/classes'
+
+describe "Module.constants" do
+ it "returns an array of the names of all toplevel constants" do
+ count = Module.constants.size
+ module ConstantSpecsAdded
+ end
+ Module.constants.size.should == count + 1
+ Object.send(:remove_const, :ConstantSpecsAdded)
+ end
+
+ it "returns an array of Symbol names" do
+ # This in NOT an exhaustive list
+ Module.constants.to_set.should >= Set[
+ :Array, :Class, :Comparable, :Dir,
+ :Enumerable, :ENV, :Exception, :FalseClass,
+ :File, :Float, :Hash, :Integer, :IO,
+ :Kernel, :Math, :Method, :Module, :NilClass,
+ :Numeric, :Object, :Range, :Regexp, :String,
+ :Symbol, :Thread, :Time, :TrueClass]
+ end
+
+ it "returns Module's constants when given a parameter" do
+ direct = Module.constants(false)
+ indirect = Module.constants(true)
+ module ConstantSpecsIncludedModule
+ MODULE_CONSTANTS_SPECS_INDIRECT = :foo
+ end
+
+ class Module
+ MODULE_CONSTANTS_SPECS_DIRECT = :bar
+ include ConstantSpecsIncludedModule
+ end
+ (Module.constants(false) - direct).should == [:MODULE_CONSTANTS_SPECS_DIRECT]
+ (Module.constants(true) - indirect).sort.should == [:MODULE_CONSTANTS_SPECS_DIRECT, :MODULE_CONSTANTS_SPECS_INDIRECT]
+
+ Module.send(:remove_const, :MODULE_CONSTANTS_SPECS_DIRECT)
+ ConstantSpecsIncludedModule.send(:remove_const, :MODULE_CONSTANTS_SPECS_INDIRECT)
+ end
+end
+
+describe "Module#constants" do
+ it "returns an array of Symbol names of all constants defined in the module and all included modules" do
+ ConstantSpecs::ContainerA.constants.sort.should == [
+ :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST24, :CS_CONST5, :ChildA
+ ]
+ end
+
+ it "returns all constants including inherited when passed true" do
+ ConstantSpecs::ContainerA.constants(true).sort.should == [
+ :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST24, :CS_CONST5, :ChildA
+ ]
+ end
+
+ it "returns all constants including inherited when passed some object" do
+ ConstantSpecs::ContainerA.constants(Object.new).sort.should == [
+ :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST24, :CS_CONST5, :ChildA
+ ]
+ end
+
+ it "doesn't returns inherited constants when passed false" do
+ ConstantSpecs::ContainerA.constants(false).sort.should == [
+ :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST5, :ChildA
+ ]
+ end
+
+ it "doesn't returns inherited constants when passed nil" do
+ ConstantSpecs::ContainerA.constants(nil).sort.should == [
+ :CS_CONST10, :CS_CONST10_LINE, :CS_CONST23, :CS_CONST5, :ChildA
+ ]
+ end
+
+ it "returns only public constants" do
+ ModuleSpecs::PrivConstModule.constants.should == [:PUBLIC_CONSTANT]
+ end
+
+ it "returns only constants starting with an uppercase letter" do
+ # e.g. fatal, IO::generic_readable and IO::generic_writable should not be returned by Module#constants
+ Object.constants.each { |c| c[0].should == c[0].upcase }
+ IO.constants.each { |c| c[0].should == c[0].upcase }
+ end
+end
+
+describe "Module#constants" do
+ before :each do
+ ConstantSpecs::ModuleM::CS_CONST251 = :const251
+ end
+
+ after :each do
+ ConstantSpecs::ModuleM.send(:remove_const, :CS_CONST251)
+ end
+
+ it "includes names of constants defined after a module is included" do
+ ConstantSpecs::ContainerA.constants.should.include?(:CS_CONST251)
+ end
+end
diff --git a/spec/ruby/core/module/define_method_spec.rb b/spec/ruby/core/module/define_method_spec.rb
new file mode 100644
index 0000000000..f838e2b85f
--- /dev/null
+++ b/spec/ruby/core/module/define_method_spec.rb
@@ -0,0 +1,846 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+class DefineMethodSpecClass
+end
+
+describe "passed { |a, b = 1| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |a, b = 1| return a, b }
+ end
+ end
+
+ it "raises an ArgumentError when passed zero arguments" do
+ -> { @klass.new.m }.should.raise(ArgumentError)
+ end
+
+ it "has a default value for b when passed one argument" do
+ @klass.new.m(1).should == [1, 1]
+ end
+
+ it "overrides the default argument when passed two arguments" do
+ @klass.new.m(1, 2).should == [1, 2]
+ end
+
+ it "raises an ArgumentError when passed three arguments" do
+ -> { @klass.new.m(1, 2, 3) }.should.raise(ArgumentError)
+ end
+end
+
+describe "Module#define_method when given an UnboundMethod" do
+ it "passes the given arguments to the new method" do
+ klass = Class.new do
+ def test_method(arg1, arg2)
+ [arg1, arg2]
+ end
+ define_method(:another_test_method, instance_method(:test_method))
+ end
+
+ klass.new.another_test_method(1, 2).should == [1, 2]
+ end
+
+ it "adds the new method to the methods list" do
+ klass = Class.new do
+ def test_method(arg1, arg2)
+ [arg1, arg2]
+ end
+ define_method(:another_test_method, instance_method(:test_method))
+ end
+ klass.new.should.respond_to?(:another_test_method)
+ end
+
+ describe "defining a method on a singleton class" do
+ before do
+ klass = Class.new
+ class << klass
+ def test_method
+ :foo
+ end
+ end
+ child = Class.new(klass)
+ sc = class << child; self; end
+ sc.send :define_method, :another_test_method, klass.method(:test_method).unbind
+
+ @class = child
+ end
+
+ it "doesn't raise TypeError when calling the method" do
+ @class.another_test_method.should == :foo
+ end
+ end
+
+ it "sets the new method's visibility to the current frame's visibility" do
+ foo = Class.new do
+ def ziggy
+ 'piggy'
+ end
+ private :ziggy
+
+ # make sure frame visibility is public
+ public
+
+ define_method :piggy, instance_method(:ziggy)
+ end
+
+ -> { foo.new.ziggy }.should.raise(NoMethodError)
+ foo.new.piggy.should == 'piggy'
+ end
+end
+
+describe "Module#define_method" do
+ describe "when the default definee is not the same as the module" do
+ it "sets the visibility of the method to public" do
+ klass = Class.new
+ class << klass
+ private
+ define_method(:meta) do
+ define_method(:foo) { :foo }
+ end
+ end
+
+ klass.send :meta
+ klass.new.foo.should == :foo
+ end
+ end
+end
+
+describe "Module#define_method when name is not a special private name" do
+ describe "given an UnboundMethod" do
+ describe "and called from the target module" do
+ it "sets the visibility of the method to the current visibility" do
+ klass = Class.new do
+ define_method(:bar, ModuleSpecs::EmptyFooMethod)
+ private
+ define_method(:baz, ModuleSpecs::EmptyFooMethod)
+ end
+
+ klass.public_instance_methods(false).should.include?(:bar)
+ klass.private_instance_methods(false).should.include?(:baz)
+ end
+ end
+
+ describe "and called from another module" do
+ it "sets the visibility of the method to public" do
+ klass = Class.new
+ Class.new do
+ klass.send(:define_method, :bar, ModuleSpecs::EmptyFooMethod)
+ private
+ klass.send(:define_method, :baz, ModuleSpecs::EmptyFooMethod)
+ end
+
+ klass.public_instance_methods(false).should.include?(:bar)
+ klass.public_instance_methods(false).should.include?(:baz)
+ end
+ end
+
+ it "sets the method owner for a dynamically added method with a different original owner" do
+ mixin_module = Module.new do
+ def bar; end
+ end
+
+ foo = Object.new
+ foo.singleton_class.define_method(:bar, mixin_module.instance_method(:bar))
+
+ foo.method(:bar).owner.should == foo.singleton_class
+ end
+ end
+
+ describe "passed a block" do
+ describe "and called from the target module" do
+ it "sets the visibility of the method to the current visibility" do
+ klass = Class.new do
+ define_method(:bar) {}
+ private
+ define_method(:baz) {}
+ end
+
+ klass.public_instance_methods(false).should.include?(:bar)
+ klass.private_instance_methods(false).should.include?(:baz)
+ end
+ end
+
+ describe "and called from another module" do
+ it "sets the visibility of the method to public" do
+ klass = Class.new
+ Class.new do
+ klass.send(:define_method, :bar) {}
+ private
+ klass.send(:define_method, :baz) {}
+ end
+
+ klass.public_instance_methods(false).should.include?(:bar)
+ klass.public_instance_methods(false).should.include?(:baz)
+ end
+ end
+ end
+end
+
+describe "Module#define_method when name is :initialize" do
+ describe "passed a block" do
+ it "sets visibility to private when method name is :initialize" do
+ klass = Class.new do
+ define_method(:initialize) { }
+ end
+ klass.private_instance_methods(false).should.include?(:initialize)
+ end
+ end
+
+ describe "given an UnboundMethod" do
+ it "sets the visibility to private when method is named :initialize" do
+ klass = Class.new do
+ def test_method
+ end
+ define_method(:initialize, instance_method(:test_method))
+ end
+ klass.private_instance_methods(false).should.include?(:initialize)
+ end
+ end
+end
+
+describe "Module#define_method" do
+ it "defines the given method as an instance method with the given name in self" do
+ class DefineMethodSpecClass
+ def test1
+ "test"
+ end
+ define_method(:another_test, instance_method(:test1))
+ end
+
+ o = DefineMethodSpecClass.new
+ o.test1.should == o.another_test
+ end
+
+ it "calls #method_added after the method is added to the Module" do
+ DefineMethodSpecClass.should_receive(:method_added).with(:test_ma)
+
+ class DefineMethodSpecClass
+ define_method(:test_ma) { true }
+ end
+ end
+
+ it "defines a new method with the given name and the given block as body in self" do
+ class DefineMethodSpecClass
+ define_method(:block_test1) { self }
+ define_method(:block_test2, &-> { self })
+ end
+
+ o = DefineMethodSpecClass.new
+ o.block_test1.should == o
+ o.block_test2.should == o
+ end
+
+ it "raises TypeError if name cannot converted to String" do
+ -> {
+ Class.new { define_method(1001, -> {}) }
+ }.should.raise(TypeError, /is not a symbol nor a string/)
+
+ -> {
+ Class.new { define_method([], -> {}) }
+ }.should.raise(TypeError, /is not a symbol nor a string/)
+ end
+
+ it "converts non-String name to String with #to_str" do
+ obj = Object.new
+ def obj.to_str() "foo" end
+
+ new_class = Class.new { define_method(obj, -> { :called }) }
+ new_class.new.foo.should == :called
+ end
+
+ it "raises TypeError when #to_str called on non-String name returns non-String value" do
+ obj = Object.new
+ def obj.to_str() [] end
+
+ -> {
+ Class.new { define_method(obj, -> {}) }
+ }.should raise_consistent_error(TypeError, /can't convert Object into String/)
+ end
+
+ it "raises a TypeError when the given method is no Method/Proc" do
+ -> {
+ Class.new { define_method(:test, "self") }
+ }.should.raise(TypeError, "wrong argument type String (expected Proc/Method/UnboundMethod)")
+
+ -> {
+ Class.new { define_method(:test, 1234) }
+ }.should.raise(TypeError, "wrong argument type Integer (expected Proc/Method/UnboundMethod)")
+
+ -> {
+ Class.new { define_method(:test, nil) }
+ }.should.raise(TypeError, "wrong argument type NilClass (expected Proc/Method/UnboundMethod)")
+ end
+
+ it "uses provided Method/Proc even if block is specified" do
+ new_class = Class.new do
+ define_method(:test, -> { :method_is_called }) do
+ :block_is_called
+ end
+ end
+
+ new_class.new.test.should == :method_is_called
+ end
+
+ it "raises an ArgumentError when no block is given" do
+ -> {
+ Class.new { define_method(:test) }
+ }.should.raise(ArgumentError)
+ end
+
+ it "does not use the caller block when no block is given" do
+ o = Object.new
+ def o.define(name)
+ self.class.class_eval do
+ define_method(name)
+ end
+ end
+
+ -> {
+ o.define(:foo) { raise "not used" }
+ }.should.raise(ArgumentError)
+ end
+
+ it "does not change the arity check style of the original proc" do
+ class DefineMethodSpecClass
+ prc = Proc.new { || true }
+ define_method("proc_style_test", &prc)
+ end
+
+ obj = DefineMethodSpecClass.new
+ -> { obj.proc_style_test :arg }.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError if frozen" do
+ -> {
+ Class.new { freeze; define_method(:foo) {} }
+ }.should.raise(FrozenError)
+ end
+
+ it "accepts a Method (still bound)" do
+ class DefineMethodSpecClass
+ attr_accessor :data
+ def inspect_data
+ "data is #{@data}"
+ end
+ end
+ o = DefineMethodSpecClass.new
+ o.data = :foo
+ m = o.method(:inspect_data)
+ m.should.instance_of?(Method)
+ klass = Class.new(DefineMethodSpecClass)
+ klass.send(:define_method,:other_inspect, m)
+ c = klass.new
+ c.data = :bar
+ c.other_inspect.should == "data is bar"
+ ->{o.other_inspect}.should.raise(NoMethodError)
+ end
+
+ it "raises a TypeError when a Method from a singleton class is defined on another class" do
+ c = Class.new do
+ class << self
+ def foo
+ end
+ end
+ end
+ m = c.method(:foo)
+
+ -> {
+ Class.new { define_method :bar, m }
+ }.should.raise(TypeError, /can't bind singleton method to a different class/)
+ end
+
+ it "raises a TypeError when a Method from one class is defined on an unrelated class" do
+ c = Class.new do
+ def foo
+ end
+ end
+ m = c.new.method(:foo)
+
+ -> {
+ Class.new { define_method :bar, m }
+ }.should.raise(TypeError)
+ end
+
+ it "accepts an UnboundMethod from an attr_accessor method" do
+ class DefineMethodSpecClass
+ attr_accessor :accessor_method
+ end
+
+ m = DefineMethodSpecClass.instance_method(:accessor_method)
+ o = DefineMethodSpecClass.new
+
+ DefineMethodSpecClass.send(:undef_method, :accessor_method)
+ -> { o.accessor_method }.should.raise(NoMethodError)
+
+ DefineMethodSpecClass.send(:define_method, :accessor_method, m)
+
+ o.accessor_method = :abc
+ o.accessor_method.should == :abc
+ end
+
+ it "accepts a proc from a method" do
+ class ProcFromMethod
+ attr_accessor :data
+ def cool_method
+ "data is #{@data}"
+ end
+ end
+
+ object1 = ProcFromMethod.new
+ object1.data = :foo
+
+ method_proc = object1.method(:cool_method).to_proc
+ klass = Class.new(ProcFromMethod)
+ klass.send(:define_method, :other_cool_method, &method_proc)
+
+ object2 = klass.new
+ object2.data = :bar
+ object2.other_cool_method.should == "data is foo"
+ end
+
+ it "accepts a proc from a Symbol" do
+ symbol_proc = :+.to_proc
+ klass = Class.new do
+ define_method :foo, &symbol_proc
+ end
+ klass.new.foo(1, 2).should == 3
+ end
+
+ it "maintains the Proc's scope" do
+ class DefineMethodByProcClass
+ in_scope = true
+ method_proc = proc { in_scope }
+
+ define_method(:proc_test, &method_proc)
+ end
+
+ o = DefineMethodByProcClass.new
+ o.proc_test.should == true
+ end
+
+ it "accepts a String method name" do
+ klass = Class.new do
+ define_method("string_test") do
+ "string_test result"
+ end
+ end
+
+ klass.new.string_test.should == "string_test result"
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:define_method)
+ end
+
+ it "returns its symbol" do
+ class DefineMethodSpecClass
+ method = define_method("return_test") { true }
+ method.should == :return_test
+ end
+ end
+
+ it "allows an UnboundMethod from a module to be defined on a class" do
+ klass = Class.new {
+ define_method :bar, ModuleSpecs::UnboundMethodTest.instance_method(:foo)
+ }
+ klass.new.should.respond_to?(:bar)
+ end
+
+ it "allows an UnboundMethod from a parent class to be defined on a child class" do
+ parent = Class.new { define_method(:foo) { :bar } }
+ child = Class.new(parent) {
+ define_method :baz, parent.instance_method(:foo)
+ }
+ child.new.should.respond_to?(:baz)
+ end
+
+ it "allows an UnboundMethod from a module to be defined on another unrelated module" do
+ mod = Module.new {
+ define_method :bar, ModuleSpecs::UnboundMethodTest.instance_method(:foo)
+ }
+ klass = Class.new { include mod }
+ klass.new.should.respond_to?(:bar)
+ end
+
+
+ it "allows an UnboundMethod of a Kernel method retrieved from Object to defined on a BasicObject subclass" do
+ klass = Class.new(BasicObject) do
+ define_method :instance_of?, ::Object.instance_method(:instance_of?)
+ end
+ klass.new.instance_of?(klass).should == true
+ end
+
+ it "raises a TypeError when an UnboundMethod from a child class is defined on a parent class" do
+ -> {
+ ParentClass = Class.new { define_method(:foo) { :bar } }
+ ChildClass = Class.new(ParentClass) { define_method(:foo) { :baz } }
+ ParentClass.send :define_method, :foo, ChildClass.instance_method(:foo)
+ }.should.raise(TypeError, /bind argument must be a subclass of ChildClass/)
+ ensure
+ Object.send(:remove_const, :ParentClass)
+ Object.send(:remove_const, :ChildClass)
+ end
+
+ it "raises a TypeError when an UnboundMethod from one class is defined on an unrelated class" do
+ -> {
+ DestinationClass = Class.new {
+ define_method :bar, ModuleSpecs::InstanceMeth.instance_method(:foo)
+ }
+ }.should.raise(TypeError, /bind argument must be a subclass of ModuleSpecs::InstanceMeth/)
+ end
+
+ it "raises a TypeError when an UnboundMethod from a singleton class is defined on another class" do
+ c = Class.new do
+ class << self
+ def foo
+ end
+ end
+ end
+ m = c.method(:foo).unbind
+
+ -> {
+ Class.new { define_method :bar, m }
+ }.should.raise(TypeError, /can't bind singleton method to a different class/)
+ end
+
+ it "defines a new method with public visibility when a Method passed and the class/module of the context isn't equal to the receiver of #define_method" do
+ c = Class.new do
+ private def foo
+ "public"
+ end
+ end
+
+ object = c.new
+ object.singleton_class.define_method(:bar, object.method(:foo))
+
+ object.bar.should == "public"
+ end
+
+ it "defines the new method according to the scope visibility when a Method passed and the class/module of the context is equal to the receiver of #define_method" do
+ c = Class.new do
+ def foo; end
+ end
+
+ object = c.new
+ object.singleton_class.class_eval do
+ private
+ define_method(:bar, c.new.method(:foo))
+ end
+
+ -> { object.bar }.should.raise(NoMethodError)
+ end
+end
+
+describe "Module#define_method" do
+ describe "passed { } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { :called }
+ end
+ end
+
+ it "returns the value computed by the block when passed zero arguments" do
+ @klass.new.m().should == :called
+ end
+
+ it "raises an ArgumentError when passed one argument" do
+ -> { @klass.new.m 1 }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed two arguments" do
+ -> { @klass.new.m 1, 2 }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "passed { || } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { || :called }
+ end
+ end
+
+ it "returns the value computed by the block when passed zero arguments" do
+ @klass.new.m().should == :called
+ end
+
+ it "raises an ArgumentError when passed one argument" do
+ -> { @klass.new.m 1 }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed two arguments" do
+ -> { @klass.new.m 1, 2 }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "passed { |a| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |a| a }
+ end
+ end
+
+ it "raises an ArgumentError when passed zero arguments" do
+ -> { @klass.new.m }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed zero arguments and a block" do
+ -> { @klass.new.m { :computed } }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed two arguments" do
+ -> { @klass.new.m 1, 2 }.should.raise(ArgumentError)
+ end
+
+ it "receives the value passed as the argument when passed one argument" do
+ @klass.new.m(1).should == 1
+ end
+ end
+
+ describe "passed { |a,| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |a,| a }
+ end
+ end
+
+ it "raises an ArgumentError when passed zero arguments" do
+ -> { @klass.new.m }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed zero arguments and a block" do
+ -> { @klass.new.m { :computed } }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed two arguments" do
+ -> { @klass.new.m 1, 2 }.should.raise(ArgumentError)
+ end
+
+ it "receives the value passed as the argument when passed one argument" do
+ @klass.new.m(1).should == 1
+ end
+
+ it "does not destructure the passed argument" do
+ @klass.new.m([1, 2]).should == [1, 2]
+ # for comparison:
+ proc { |a,| a }.call([1, 2]).should == 1
+ end
+ end
+
+ describe "passed { |*a| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |*a| a }
+ end
+ end
+
+ it "receives an empty array as the argument when passed zero arguments" do
+ @klass.new.m().should == []
+ end
+
+ it "receives the value in an array when passed one argument" do
+ @klass.new.m(1).should == [1]
+ end
+
+ it "receives the values in an array when passed two arguments" do
+ @klass.new.m(1, 2).should == [1, 2]
+ end
+ end
+
+ describe "passed { |a, *b| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |a, *b| return a, b }
+ end
+ end
+
+ it "raises an ArgumentError when passed zero arguments" do
+ -> { @klass.new.m }.should.raise(ArgumentError)
+ end
+
+ it "returns the value computed by the block when passed one argument" do
+ @klass.new.m(1).should == [1, []]
+ end
+
+ it "returns the value computed by the block when passed two arguments" do
+ @klass.new.m(1, 2).should == [1, [2]]
+ end
+
+ it "returns the value computed by the block when passed three arguments" do
+ @klass.new.m(1, 2, 3).should == [1, [2, 3]]
+ end
+ end
+
+ describe "passed { |a, b| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |a, b| return a, b }
+ end
+ end
+
+ it "returns the value computed by the block when passed two arguments" do
+ @klass.new.m(1, 2).should == [1, 2]
+ end
+
+ it "raises an ArgumentError when passed zero arguments" do
+ -> { @klass.new.m }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed one argument" do
+ -> { @klass.new.m 1 }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed one argument and a block" do
+ -> { @klass.new.m(1) { } }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed three arguments" do
+ -> { @klass.new.m 1, 2, 3 }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "passed { |a, b, *c| } creates a method that" do
+ before :each do
+ @klass = Class.new do
+ define_method(:m) { |a, b, *c| return a, b, c }
+ end
+ end
+
+ it "raises an ArgumentError when passed zero arguments" do
+ -> { @klass.new.m }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed one argument" do
+ -> { @klass.new.m 1 }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed one argument and a block" do
+ -> { @klass.new.m(1) { } }.should.raise(ArgumentError)
+ end
+
+ it "receives an empty array as the third argument when passed two arguments" do
+ @klass.new.m(1, 2).should == [1, 2, []]
+ end
+
+ it "receives the third argument in an array when passed three arguments" do
+ @klass.new.m(1, 2, 3).should == [1, 2, [3]]
+ end
+ end
+end
+
+describe "Module#define_method when passed a Method object" do
+ before :each do
+ @klass = Class.new do
+ def m(a, b, *c)
+ :m
+ end
+ end
+
+ @obj = @klass.new
+ m = @obj.method :m
+
+ @klass.class_exec do
+ define_method :n, m
+ end
+ end
+
+ it "defines a method with the same #arity as the original" do
+ @obj.method(:n).arity.should == @obj.method(:m).arity
+ end
+
+ it "defines a method with the same #parameters as the original" do
+ @obj.method(:n).parameters.should == @obj.method(:m).parameters
+ end
+end
+
+describe "Module#define_method when passed an UnboundMethod object" do
+ before :each do
+ @klass = Class.new do
+ def m(a, b, *c)
+ :m
+ end
+ end
+
+ @obj = @klass.new
+ m = @klass.instance_method :m
+
+ @klass.class_exec do
+ define_method :n, m
+ end
+ end
+
+ it "defines a method with the same #arity as the original" do
+ @obj.method(:n).arity.should == @obj.method(:m).arity
+ end
+
+ it "defines a method with the same #parameters as the original" do
+ @obj.method(:n).parameters.should == @obj.method(:m).parameters
+ end
+end
+
+describe "Module#define_method when passed a Proc object" do
+ describe "and a method is defined inside" do
+ it "defines the nested method in the default definee where the Proc was created" do
+ prc = nil
+ t = Class.new do
+ prc = -> {
+ def nested_method_in_proc_for_define_method
+ 42
+ end
+ }
+ end
+
+ c = Class.new do
+ define_method(:test, prc)
+ end
+
+ o = c.new
+ o.test
+ o.should_not.respond_to? :nested_method_in_proc_for_define_method
+
+ t.new.nested_method_in_proc_for_define_method.should == 42
+ end
+ end
+end
+
+describe "Module#define_method when passed a block" do
+ describe "behaves exactly like a lambda" do
+ it "for return" do
+ Class.new do
+ define_method(:foo) do
+ return 42
+ end
+ end.new.foo.should == 42
+ end
+
+ it "for break" do
+ Class.new do
+ define_method(:foo) do
+ break 42
+ end
+ end.new.foo.should == 42
+ end
+
+ it "for next" do
+ Class.new do
+ define_method(:foo) do
+ next 42
+ end
+ end.new.foo.should == 42
+ end
+
+ it "for redo" do
+ Class.new do
+ result = []
+ define_method(:foo) do
+ if result.empty?
+ result << :first
+ redo
+ else
+ result << :second
+ result
+ end
+ end
+ end.new.foo.should == [:first, :second]
+ end
+ end
+end
diff --git a/spec/ruby/core/module/define_singleton_method_spec.rb b/spec/ruby/core/module/define_singleton_method_spec.rb
new file mode 100644
index 0000000000..eb5cb89ed1
--- /dev/null
+++ b/spec/ruby/core/module/define_singleton_method_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "Module#define_singleton_method" do
+ it "defines the given method as an class method with the given name in self" do
+ klass = Module.new do
+ define_singleton_method :a do
+ 42
+ end
+ define_singleton_method(:b, -> x { 2*x })
+ end
+
+ klass.a.should == 42
+ klass.b(10).should == 20
+ end
+end
diff --git a/spec/ruby/core/module/deprecate_constant_spec.rb b/spec/ruby/core/module/deprecate_constant_spec.rb
new file mode 100644
index 0000000000..597379eb87
--- /dev/null
+++ b/spec/ruby/core/module/deprecate_constant_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../spec_helper'
+
+describe "Module#deprecate_constant" do
+ before :each do
+ @module = Module.new
+ @value = :value
+ @module::PUBLIC1 = @value
+ @module::PUBLIC2 = @value
+ @module::PRIVATE = @value
+ @module.private_constant :PRIVATE
+ @module.deprecate_constant :PRIVATE
+ end
+
+ describe "when accessing the deprecated module" do
+ it "passes the accessing" do
+ @module.deprecate_constant :PUBLIC1
+
+ value = nil
+ -> {
+ value = @module::PUBLIC1
+ }.should complain(/warning: constant .+::PUBLIC1 is deprecated/)
+ value.should.equal?(@value)
+
+ -> { @module::PRIVATE }.should.raise(NameError)
+ end
+
+ it "warns with a message" do
+ @module.deprecate_constant :PUBLIC1
+
+ -> { @module::PUBLIC1 }.should complain(/warning: constant .+::PUBLIC1 is deprecated/)
+ -> { @module.const_get :PRIVATE }.should complain(/warning: constant .+::PRIVATE is deprecated/)
+ end
+
+ it "does not warn if Warning[:deprecated] is false" do
+ @module.deprecate_constant :PUBLIC1
+
+ deprecated = Warning[:deprecated]
+ begin
+ Warning[:deprecated] = false
+ -> { @module::PUBLIC1 }.should_not complain
+ ensure
+ Warning[:deprecated] = deprecated
+ end
+ end
+ end
+
+ ruby_bug '#20900', ''...'3.4' do
+ describe "when removing the deprecated module" do
+ it "warns with a message" do
+ @module.deprecate_constant :PUBLIC1
+ -> { @module.module_eval {remove_const :PUBLIC1} }.should complain(/warning: constant .+::PUBLIC1 is deprecated/)
+ end
+ end
+ end
+
+ it "accepts multiple symbols and strings as constant names" do
+ @module.deprecate_constant "PUBLIC1", :PUBLIC2
+
+ -> { @module::PUBLIC1 }.should complain(/warning: constant .+::PUBLIC1 is deprecated/)
+ -> { @module::PUBLIC2 }.should complain(/warning: constant .+::PUBLIC2 is deprecated/)
+ end
+
+ it "returns self" do
+ @module.deprecate_constant(:PUBLIC1).should.equal?(@module)
+ end
+
+ it "raises a NameError when given an undefined name" do
+ -> { @module.deprecate_constant :UNDEFINED }.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/module/eql_spec.rb b/spec/ruby/core/module/eql_spec.rb
new file mode 100644
index 0000000000..76bb271d8d
--- /dev/null
+++ b/spec/ruby/core/module/eql_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/equal_value'
+
+describe "Module#eql?" do
+ it_behaves_like :module_equal, :eql?
+end
diff --git a/spec/ruby/core/module/equal_spec.rb b/spec/ruby/core/module/equal_spec.rb
new file mode 100644
index 0000000000..01ab06152d
--- /dev/null
+++ b/spec/ruby/core/module/equal_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/equal_value'
+
+describe "Module#equal?" do
+ it_behaves_like :module_equal, :equal?
+end
diff --git a/spec/ruby/core/module/equal_value_spec.rb b/spec/ruby/core/module/equal_value_spec.rb
new file mode 100644
index 0000000000..a7191cd755
--- /dev/null
+++ b/spec/ruby/core/module/equal_value_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/equal_value'
+
+describe "Module#==" do
+ it_behaves_like :module_equal, :==
+end
diff --git a/spec/ruby/core/module/extend_object_spec.rb b/spec/ruby/core/module/extend_object_spec.rb
new file mode 100644
index 0000000000..b428eb7924
--- /dev/null
+++ b/spec/ruby/core/module/extend_object_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#extend_object" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is a private method" do
+ Module.private_instance_methods(false).should.include?(:extend_object)
+ end
+
+ describe "on Class" do
+ it "is undefined" do
+ Class.private_instance_methods(true).should_not.include?(:extend_object)
+ end
+
+ it "raises a TypeError if calling after rebinded to Class" do
+ -> {
+ Module.instance_method(:extend_object).bind(Class.new).call Object.new
+ }.should.raise(TypeError)
+ end
+ end
+
+ it "is called when #extend is called on an object" do
+ ModuleSpecs::ExtendObject.should_receive(:extend_object)
+ obj = mock("extended object")
+ obj.extend ModuleSpecs::ExtendObject
+ end
+
+ it "extends the given object with its constants and methods by default" do
+ obj = mock("extended direct")
+ ModuleSpecs::ExtendObject.send :extend_object, obj
+
+ obj.test_method.should == "hello test"
+ obj.singleton_class.const_get(:C).should == :test
+ end
+
+ it "is called even when private" do
+ obj = mock("extended private")
+ obj.extend ModuleSpecs::ExtendObjectPrivate
+ ScratchPad.recorded.should == :extended
+ end
+
+ describe "when given a frozen object" do
+ before :each do
+ @receiver = Module.new
+ @object = Object.new.freeze
+ end
+
+ it "raises a RuntimeError before extending the object" do
+ -> { @receiver.send(:extend_object, @object) }.should.raise(RuntimeError)
+ @object.should_not.is_a?(@receiver)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/extended_spec.rb b/spec/ruby/core/module/extended_spec.rb
new file mode 100644
index 0000000000..a4ec5a4ba7
--- /dev/null
+++ b/spec/ruby/core/module/extended_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#extended" do
+ it "is called when an object gets extended with self" do
+ begin
+ m = Module.new do
+ def self.extended(o)
+ $extended_object = o
+ end
+ end
+
+ (o = mock('x')).extend(m)
+
+ $extended_object.should == o
+ ensure
+ $extended_object = nil
+ end
+ end
+
+ it "is called after Module#extend_object" do
+ begin
+ m = Module.new do
+ def self.extend_object(o)
+ $extended_object = nil
+ end
+
+ def self.extended(o)
+ $extended_object = o
+ end
+ end
+
+ (o = mock('x')).extend(m)
+
+ $extended_object.should == o
+ ensure
+ $extended_object = nil
+ end
+ end
+
+ it "is private in its default implementation" do
+ Module.new.private_methods.should.include?(:extended)
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload.rb b/spec/ruby/core/module/fixtures/autoload.rb
new file mode 100644
index 0000000000..5a77a2d9d4
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload.rb
@@ -0,0 +1 @@
+$m.const_set(:AAA, "test") unless $m.nil?
diff --git a/spec/ruby/core/module/fixtures/autoload_abc.rb b/spec/ruby/core/module/fixtures/autoload_abc.rb
new file mode 100644
index 0000000000..ffaec91cfe
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_abc.rb
@@ -0,0 +1,11 @@
+module ModuleSpecs::Autoload::FromThread
+ module A
+ class B
+ class C
+ def self.foo
+ :foo
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_c.rb b/spec/ruby/core/module/fixtures/autoload_c.rb
new file mode 100644
index 0000000000..ff2bcc548c
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_c.rb
@@ -0,0 +1,11 @@
+module ModuleSpecs::Autoload
+ class DynClass
+ class C
+ def loaded
+ :dynclass_c
+ end
+ end
+ end
+end
+
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_callback.rb b/spec/ruby/core/module/fixtures/autoload_callback.rb
new file mode 100644
index 0000000000..51d53eb580
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_callback.rb
@@ -0,0 +1,2 @@
+block = ScratchPad.recorded
+block.call
diff --git a/spec/ruby/core/module/fixtures/autoload_concur.rb b/spec/ruby/core/module/fixtures/autoload_concur.rb
new file mode 100644
index 0000000000..0585e36880
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_concur.rb
@@ -0,0 +1,9 @@
+ScratchPad.recorded << :con_pre
+Thread.current[:in_autoload_rb] = true
+sleep 0.1
+
+module ModuleSpecs::Autoload
+ Concur = 1
+end
+
+ScratchPad.recorded << :con_post
diff --git a/spec/ruby/core/module/fixtures/autoload_const_source_location.rb b/spec/ruby/core/module/fixtures/autoload_const_source_location.rb
new file mode 100644
index 0000000000..ee0e5a689f
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_const_source_location.rb
@@ -0,0 +1,6 @@
+module ConstantSpecs
+ BEFORE_DEFINE_LOCATION = const_source_location(:ConstSource)
+ module ConstSource
+ LOCATION = Object.const_source_location(name)
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_d.rb b/spec/ruby/core/module/fixtures/autoload_d.rb
new file mode 100644
index 0000000000..6f5eee741c
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_d.rb
@@ -0,0 +1,11 @@
+module ModuleSpecs::Autoload
+ module DynModule
+ class D
+ def loaded
+ :dynmodule_d
+ end
+ end
+ end
+end
+
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_during_autoload.rb b/spec/ruby/core/module/fixtures/autoload_during_autoload.rb
new file mode 100644
index 0000000000..5202bd8b23
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_during_autoload.rb
@@ -0,0 +1,7 @@
+block = ScratchPad.recorded
+ScratchPad.record(block.call)
+
+module ModuleSpecs::Autoload
+ class DuringAutoload
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb b/spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb
new file mode 100644
index 0000000000..a9d886dfd6
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_during_autoload_after_define.rb
@@ -0,0 +1,6 @@
+module ModuleSpecs::Autoload
+ class DuringAutoloadAfterDefine
+ block = ScratchPad.recorded
+ ScratchPad.record(block.call)
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_during_require.rb b/spec/ruby/core/module/fixtures/autoload_during_require.rb
new file mode 100644
index 0000000000..6fd81592e3
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_during_require.rb
@@ -0,0 +1,4 @@
+module ModuleSpecs::Autoload
+ class AutoloadDuringRequire
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb b/spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb
new file mode 100644
index 0000000000..5aa8595065
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_during_require_current_file.rb
@@ -0,0 +1,5 @@
+module ModuleSpecs::Autoload
+ autoload(:AutoloadCurrentFile, __FILE__)
+
+ ScratchPad.record autoload?(:AutoloadCurrentFile)
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_e.rb b/spec/ruby/core/module/fixtures/autoload_e.rb
new file mode 100644
index 0000000000..fb78c6cbd4
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_e.rb
@@ -0,0 +1,7 @@
+module ModuleSpecs::Autoload
+ class E
+ def loaded
+ :autoload_e
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_empty.rb b/spec/ruby/core/module/fixtures/autoload_empty.rb
new file mode 100644
index 0000000000..d7116f3049
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_empty.rb
@@ -0,0 +1 @@
+# This file is left empty on purpose
diff --git a/spec/ruby/core/module/fixtures/autoload_ex1.rb b/spec/ruby/core/module/fixtures/autoload_ex1.rb
new file mode 100644
index 0000000000..a90092389c
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_ex1.rb
@@ -0,0 +1,16 @@
+
+class ModuleSpecs::Autoload::EX1 < Exception
+ def self.trample1
+ 1.times { return }
+ end
+
+ def self.trample2
+ begin
+ raise "hello"
+ rescue
+ end
+ end
+
+ trample1
+ trample2
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_exception.rb b/spec/ruby/core/module/fixtures/autoload_exception.rb
new file mode 100644
index 0000000000..09acf9f537
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_exception.rb
@@ -0,0 +1,3 @@
+ScratchPad.record(:exception)
+
+raise 'intentional error to test failure conditions during autoloading'
diff --git a/spec/ruby/core/module/fixtures/autoload_f.rb b/spec/ruby/core/module/fixtures/autoload_f.rb
new file mode 100644
index 0000000000..54c2b05b7b
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_f.rb
@@ -0,0 +1,7 @@
+module ModuleSpecs::Autoload
+ module F
+ def self.loaded
+ :autoload_f
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_g.rb b/spec/ruby/core/module/fixtures/autoload_g.rb
new file mode 100644
index 0000000000..a1c4e429d9
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_g.rb
@@ -0,0 +1,7 @@
+module ModuleSpecs::Autoload
+ class G
+ def loaded
+ :autoload_g
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_h.rb b/spec/ruby/core/module/fixtures/autoload_h.rb
new file mode 100644
index 0000000000..53988c5382
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_h.rb
@@ -0,0 +1,7 @@
+module ModuleSpecs::Autoload
+ module H
+ def loaded
+ :autoload_h
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_i.rb b/spec/ruby/core/module/fixtures/autoload_i.rb
new file mode 100644
index 0000000000..f7f720516e
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_i.rb
@@ -0,0 +1,5 @@
+module ModuleSpecs::Autoload
+ I = :autoloaded
+end
+
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_j.rb b/spec/ruby/core/module/fixtures/autoload_j.rb
new file mode 100644
index 0000000000..da6d35d43d
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_j.rb
@@ -0,0 +1,3 @@
+module ModuleSpecs::Autoload
+ J = :autoload_j
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_k.rb b/spec/ruby/core/module/fixtures/autoload_k.rb
new file mode 100644
index 0000000000..431602bf80
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_k.rb
@@ -0,0 +1,7 @@
+module ModuleSpecs::Autoload
+ class KHash < Hash
+ K = :autoload_k
+ end
+end
+
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_lm.rb b/spec/ruby/core/module/fixtures/autoload_lm.rb
new file mode 100644
index 0000000000..d93d783cd0
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_lm.rb
@@ -0,0 +1,4 @@
+module ModuleSpecs::Autoload
+ L = :autoload_l
+ M = :autoload_m
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_location.rb b/spec/ruby/core/module/fixtures/autoload_location.rb
new file mode 100644
index 0000000000..318851b2df
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_location.rb
@@ -0,0 +1,3 @@
+module ConstantSpecs
+ CONST_LOCATION = __LINE__
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_nested.rb b/spec/ruby/core/module/fixtures/autoload_nested.rb
new file mode 100644
index 0000000000..073cec0dce
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_nested.rb
@@ -0,0 +1,8 @@
+module ModuleSpecs::Autoload
+ module GoodParent
+ class Nested
+ end
+ end
+end
+
+ScratchPad.record(:loaded)
diff --git a/spec/ruby/core/module/fixtures/autoload_never_set.rb b/spec/ruby/core/module/fixtures/autoload_never_set.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_never_set.rb
@@ -0,0 +1 @@
+
diff --git a/spec/ruby/core/module/fixtures/autoload_o.rb b/spec/ruby/core/module/fixtures/autoload_o.rb
new file mode 100644
index 0000000000..7d88f969b2
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_o.rb
@@ -0,0 +1,2 @@
+# does not define ModuleSpecs::Autoload::O
+ScratchPad << :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_overridden.rb b/spec/ruby/core/module/fixtures/autoload_overridden.rb
new file mode 100644
index 0000000000..7062bcfabc
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_overridden.rb
@@ -0,0 +1,3 @@
+module ModuleSpecs::Autoload
+ Overridden = :overridden
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_r.rb b/spec/ruby/core/module/fixtures/autoload_r.rb
new file mode 100644
index 0000000000..34209d20c3
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_r.rb
@@ -0,0 +1,4 @@
+module ModuleSpecs::Autoload
+ class R
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_raise.rb b/spec/ruby/core/module/fixtures/autoload_raise.rb
new file mode 100644
index 0000000000..f6051e3ba2
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_raise.rb
@@ -0,0 +1,2 @@
+ScratchPad << :raise
+raise "exception during autoload"
diff --git a/spec/ruby/core/module/fixtures/autoload_relative_a.rb b/spec/ruby/core/module/fixtures/autoload_relative_a.rb
new file mode 100644
index 0000000000..494181adc2
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_relative_a.rb
@@ -0,0 +1,9 @@
+module ModuleSpecs
+ module Autoload
+ class AutoloadRelativeA
+ end
+
+ class AutoloadRelativeB
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_required_directly.rb b/spec/ruby/core/module/fixtures/autoload_required_directly.rb
new file mode 100644
index 0000000000..bed60a71ec
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_required_directly.rb
@@ -0,0 +1,7 @@
+block = ScratchPad.recorded
+ScratchPad.record(block.call)
+
+module ModuleSpecs::Autoload
+ class RequiredDirectly
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb b/spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb
new file mode 100644
index 0000000000..a9f11c2188
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_required_directly_nested.rb
@@ -0,0 +1 @@
+ScratchPad.recorded.call
diff --git a/spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb b/spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb
new file mode 100644
index 0000000000..25e08c1129
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_required_directly_no_constant.rb
@@ -0,0 +1,2 @@
+block = ScratchPad.recorded
+ScratchPad.record(block.call)
diff --git a/spec/ruby/core/module/fixtures/autoload_s.rb b/spec/ruby/core/module/fixtures/autoload_s.rb
new file mode 100644
index 0000000000..f5d412ff18
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_s.rb
@@ -0,0 +1,5 @@
+module ModuleSpecs::Autoload
+ S = :autoload_s
+end
+
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_self_during_require.rb b/spec/ruby/core/module/fixtures/autoload_self_during_require.rb
new file mode 100644
index 0000000000..f4a514a807
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_self_during_require.rb
@@ -0,0 +1,5 @@
+module ModuleSpecs::Autoload
+ autoload :AutoloadSelfDuringRequire, __FILE__
+ class AutoloadSelfDuringRequire
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_subclass.rb b/spec/ruby/core/module/fixtures/autoload_subclass.rb
new file mode 100644
index 0000000000..8027fa3fcd
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_subclass.rb
@@ -0,0 +1,11 @@
+class CS_CONST_AUTOLOAD
+end
+
+module ModuleSpecs
+ module Autoload
+ module XX
+ class CS_CONST_AUTOLOAD < CS_CONST_AUTOLOAD
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_t.rb b/spec/ruby/core/module/fixtures/autoload_t.rb
new file mode 100644
index 0000000000..4962c97f4c
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_t.rb
@@ -0,0 +1,3 @@
+module ModuleSpecs::Autoload::S
+ T = :autoload_t
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_v.rb b/spec/ruby/core/module/fixtures/autoload_v.rb
new file mode 100644
index 0000000000..2aa8c44169
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_v.rb
@@ -0,0 +1,7 @@
+module ModuleSpecs::Autoload::U
+ class V
+ def self.get_value
+ :autoload_uvx
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_w.rb b/spec/ruby/core/module/fixtures/autoload_w.rb
new file mode 100644
index 0000000000..997273812e
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_w.rb
@@ -0,0 +1,2 @@
+# Fails to define ModuleSpecs::Autoload::W::Y
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_w2.rb b/spec/ruby/core/module/fixtures/autoload_w2.rb
new file mode 100644
index 0000000000..a8dbebf322
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_w2.rb
@@ -0,0 +1 @@
+ScratchPad.record :loaded
diff --git a/spec/ruby/core/module/fixtures/autoload_x.rb b/spec/ruby/core/module/fixtures/autoload_x.rb
new file mode 100644
index 0000000000..a6c5842609
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_x.rb
@@ -0,0 +1,3 @@
+module ModuleSpecs::Autoload
+ X = :x
+end
diff --git a/spec/ruby/core/module/fixtures/autoload_z.rb b/spec/ruby/core/module/fixtures/autoload_z.rb
new file mode 100644
index 0000000000..cce1c13f37
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/autoload_z.rb
@@ -0,0 +1,5 @@
+class ModuleSpecs::Autoload::YY
+end
+
+class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::YY
+end
diff --git a/spec/ruby/core/module/fixtures/classes.rb b/spec/ruby/core/module/fixtures/classes.rb
new file mode 100644
index 0000000000..964f64c593
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/classes.rb
@@ -0,0 +1,653 @@
+module ModuleSpecs
+ def self.without_test_modules(modules)
+ ignore = %w[MSpecRSpecAdapter PP::ObjectMixin MainSpecs::Module ConstantSpecs::ModuleA]
+ modules.reject { |k| ignore.include?(k.name) }
+ end
+
+ CONST = :plain_constant
+
+ class NamedClass
+ end
+
+ module PrivConstModule
+ PRIVATE_CONSTANT = 1
+ private_constant :PRIVATE_CONSTANT
+ PUBLIC_CONSTANT = 2
+ end
+
+ class Subclass < Module
+ end
+
+ class SubclassSpec
+ end
+
+ class RemoveClassVariable
+ end
+
+ module LookupModInMod
+ INCS = :ethereal
+ end
+
+ module LookupMod
+ include LookupModInMod
+
+ MODS = :rockers
+ end
+
+ class Lookup
+ include LookupMod
+ LOOKIE = :lookie
+ end
+
+ class LookupChild < Lookup
+ end
+
+ module ModuleWithPrepend
+ prepend LookupMod
+ end
+
+ class WithPrependedModule
+ include ModuleWithPrepend
+ end
+
+ class Parent
+ # For private_class_method spec
+ def self.private_method; end
+ private_class_method :private_method
+
+ def undefed_method() end
+ undef_method :undefed_method
+
+ def parent_method; end
+ def another_parent_method; end
+
+ # For public_class_method spec
+ private
+ def self.public_method; end
+ public_class_method :public_method
+
+ public
+ def public_parent() end
+
+ protected
+ def protected_parent() end
+
+ private
+ def private_parent() end
+ end
+
+ module Basic
+ def public_module() end
+
+ protected
+ def protected_module() end
+
+ private
+ def private_module() end
+ end
+
+ module Super
+ include Basic
+
+ def public_super_module() end
+
+ protected
+ def protected_super_module() end
+
+ private
+ def private_super_module() end
+
+ def super_included_method; end
+
+ class SuperChild
+ end
+ end
+
+ module Internal
+ end
+
+ class Child < Parent
+ include Super
+
+ class << self
+ include Internal
+ end
+ attr_accessor :accessor_method
+
+ def public_child() end
+
+ undef_method :parent_method
+ undef_method :another_parent_method
+
+ protected
+ def protected_child() end
+
+ private
+ def private_child() end
+ end
+
+ class Grandchild < Child
+ undef_method :super_included_method
+ end
+
+ class Child2 < Parent
+ attr_reader :foo
+ end
+
+ # Be careful touching the Counts* classes as there used for testing
+ # private_instance_methods, public_instance_methods, etc. So adding, removing
+ # a method will break those tests.
+ module CountsMixin
+ def public_3; end
+ public :public_3
+
+ def private_3; end
+ private :private_3
+
+ def protected_3; end
+ protected :protected_3
+ end
+
+ class CountsParent
+ include CountsMixin
+
+ def public_2; end
+
+ private
+ def private_2; end
+
+ protected
+ def protected_2; end
+ end
+
+ class CountsChild < CountsParent
+ def public_1; end
+
+ private
+ def private_1; end
+
+ protected
+ def protected_1; end
+ end
+
+ module AddConstant
+ end
+
+ module A
+ CONSTANT_A = :a
+ OVERRIDE = :a
+ def ma(); :a; end
+ def self.cma(); :a; end
+ end
+
+ module B
+ CONSTANT_B = :b
+ OVERRIDE = :b
+ include A
+ def mb(); :b; end
+ def self.cmb(); :b; end
+ end
+
+ class C
+ OVERRIDE = :c
+ include B
+ end
+
+ module Z
+ MODULE_SPEC_TOPLEVEL_CONSTANT = 1
+ end
+
+ module Alias
+ def report() :report end
+ alias publish report
+ end
+
+ class Allonym
+ include ModuleSpecs::Alias
+ end
+
+ class Aliasing
+ def self.make_alias(*a)
+ alias_method(*a)
+ end
+
+ def public_one; 1; end
+
+ def public_two(n); n * 2; end
+
+ private
+ def private_one; 1; end
+
+ protected
+ def protected_one; 1; end
+ end
+
+ class AliasingSubclass < Aliasing
+ end
+
+ module AliasingSuper
+
+ module Parent
+ def super_call(arg)
+ arg
+ end
+ end
+
+ module Child
+ include Parent
+ def super_call(arg)
+ super(arg)
+ end
+ end
+
+ class Target
+ include Child
+ alias_method :alias_super_call, :super_call
+ alias_method :super_call, :alias_super_call
+ end
+
+ class RedefineAfterAlias
+ include Parent
+
+ def super_call(arg)
+ super(arg)
+ end
+
+ alias_method :alias_super_call, :super_call
+
+ def super_call(arg)
+ :wrong
+ end
+ end
+ end
+
+
+ module ReopeningModule
+ def foo; true; end
+ module_function :foo
+ private :foo
+ end
+
+ # Yes, we want to re-open the module
+ module ReopeningModule
+ alias :foo2 :foo
+ module_function :foo2
+ end
+
+ module Nesting
+ @tests = {}
+ def self.[](name); @tests[name]; end
+ def self.[]=(name, val); @tests[name] = val; end
+ def self.meta; class << self; self; end; end
+
+ Nesting[:basic] = Module.nesting
+
+ module ::ModuleSpecs
+ Nesting[:open_first_level] = Module.nesting
+ end
+
+ class << self
+ Nesting[:open_meta] = Module.nesting
+ end
+
+ def self.called_from_module_method
+ Module.nesting
+ end
+
+ class NestedClass
+ Nesting[:nest_class] = Module.nesting
+
+ def self.called_from_class_method
+ Module.nesting
+ end
+
+ def called_from_inst_method
+ Module.nesting
+ end
+ end
+
+ end
+
+ Nesting[:first_level] = Module.nesting
+
+ module InstanceMethMod
+ def bar(); :bar; end
+ end
+
+ class InstanceMeth
+ include InstanceMethMod
+ def foo(); :foo; end
+ end
+
+ class InstanceMethChild < InstanceMeth
+ end
+
+ module ClassVars
+ class A
+ @@a_cvar = :a_cvar
+ end
+
+ module M
+ @@m_cvar = :m_cvar
+ end
+
+ class B < A
+ include M
+
+ @@b_cvar = :b_cvar
+ end
+ end
+
+ class CVars
+ @@cls = :class
+
+ # Singleton class lexical scopes are ignored for class variables
+ class << self
+ def cls
+ # This looks in the parent lexical scope, class CVars
+ @@cls
+ end
+ # This actually adds it to the parent lexical scope, class CVars
+ @@meta = :metainfo
+ end
+
+ def self.meta
+ @@meta
+ end
+
+ def meta
+ @@meta
+ end
+ end
+
+ class SubCVars < CVars
+ @@sub = :sub
+ end
+
+ module MVars
+ @@mvar = :mvar
+ end
+
+ class SubModule < Module
+ attr_reader :special
+ def initialize
+ @special = 10
+ end
+ end
+
+ module MA; end
+ module MB
+ include MA
+ end
+ module MC; end
+
+ class MultipleIncludes
+ include MB
+ end
+
+ # empty modules
+ module M1; end
+ module M2; end
+ module M3; end
+
+ module Autoload
+ def self.use_ex1
+ begin
+ begin
+ raise "test exception"
+ rescue ModuleSpecs::Autoload::EX1
+ end
+ rescue RuntimeError
+ return :good
+ end
+ end
+
+ class Parent
+ end
+
+ class Child < Parent
+ end
+
+ module FromThread
+ module A
+ autoload :B, fixture(__FILE__, "autoload_empty.rb")
+
+ class B
+ autoload :C, fixture(__FILE__, "autoload_abc.rb")
+
+ def self.foo
+ C.foo
+ end
+ end
+ end
+
+ class D < A::B; end
+ end
+ end
+
+ # This class isn't inherited from or included in anywhere.
+ # It exists to test the constant scoping rules.
+ class Detached
+ DETACHED_CONSTANT = :d
+ end
+
+ class ParentPrivateMethodRedef
+ private
+ def private_method_redefined
+ :before_redefinition
+ end
+ end
+
+ class ChildPrivateMethodMadePublic < ParentPrivateMethodRedef
+ public :private_method_redefined
+ end
+
+ class ParentPrivateMethodRedef
+ def private_method_redefined
+ :after_redefinition
+ end
+ end
+
+ module CyclicAppendA
+ end
+
+ module CyclicAppendB
+ include CyclicAppendA
+ end
+
+ module CyclicPrepend
+ end
+
+ module ExtendObject
+ C = :test
+ def test_method
+ "hello test"
+ end
+ end
+
+ module ExtendObjectPrivate
+ class << self
+ def extend_object(obj)
+ ScratchPad.record :extended
+ end
+ private :extend_object
+ end
+ end
+
+ class CyclicBarrier
+ def initialize(count = 1)
+ @count = count
+ @state = 0
+ @mutex = Mutex.new
+ @cond = ConditionVariable.new
+ end
+
+ def await
+ @mutex.synchronize do
+ @state += 1
+ if @state >= @count
+ @state = 0
+ @cond.broadcast
+ true
+ else
+ @cond.wait @mutex
+ false
+ end
+ end
+ end
+
+ def enabled?
+ @mutex.synchronize { @count != -1 }
+ end
+
+ def disable!
+ @mutex.synchronize do
+ @count = -1
+ @cond.broadcast
+ end
+ end
+ end
+
+ class ThreadSafeCounter
+ def initialize(value = 0)
+ @value = 0
+ @mutex = Mutex.new
+ end
+
+ def get
+ @mutex.synchronize { @value }
+ end
+
+ def increment_and_get
+ @mutex.synchronize do
+ prev_value = @value
+ @value += 1
+ prev_value
+ end
+ end
+ end
+
+ module ShadowingOuter
+ module M
+ SHADOW = 123
+ end
+
+ module N
+ SHADOW = 456
+ end
+ end
+
+ module UnboundMethodTest
+ def foo
+ 'bar'
+ end
+ end
+
+ module ClassEvalTest
+ def self.get_constant_from_scope
+ module_eval("Lookup")
+ end
+
+ def self.get_constant_from_scope_with_send(method)
+ send(method, "Lookup")
+ end
+ end
+
+ class RecordIncludedModules
+ def self.inherited(base)
+ ScratchPad.record base
+ end
+ end
+
+ module SingletonOnModuleCase
+ module Foo
+ class << Foo
+ def included(base)
+ base.included_called
+ super
+ end
+ end
+ end
+
+ class Bar
+ @included_called = false
+
+ class << self
+ def included_called
+ @included_called = true
+ end
+
+ def included_called?
+ @included_called
+ end
+ end
+ end
+ end
+
+ module CaseCompareOnSingleton
+ def self.===(*)
+ raise 'method contents are irrelevant to test'
+ end
+ end
+
+ m = Module.new do
+ def foo
+ end
+ private :foo
+ end
+ EmptyFooMethod = m.instance_method(:foo)
+
+ # for undefined_instance_methods spec
+ module UndefinedInstanceMethods
+ module Super
+ def super_included_method; end
+ end
+
+ class Parent
+ def undefed_method; end
+ undef_method :undefed_method
+
+ def parent_method; end
+ def another_parent_method; end
+ end
+
+ class Child < Parent
+ include Super
+
+ undef_method :parent_method
+ undef_method :another_parent_method
+ end
+
+ class Grandchild < Child
+ undef_method :super_included_method
+ end
+ end
+end
+
+class Object
+ def module_specs_public_method_on_object; end
+
+ def module_specs_private_method_on_object; end
+ private :module_specs_private_method_on_object
+
+ def module_specs_protected_method_on_object; end
+ protected :module_specs_private_method_on_object
+
+ def module_specs_private_method_on_object_for_kernel_public; end
+ private :module_specs_private_method_on_object_for_kernel_public
+
+ def module_specs_public_method_on_object_for_kernel_protected; end
+ def module_specs_public_method_on_object_for_kernel_private; end
+end
+
+module Kernel
+ def module_specs_public_method_on_kernel; end
+
+ alias_method :module_specs_alias_on_kernel, :module_specs_public_method_on_object
+
+ public :module_specs_private_method_on_object_for_kernel_public
+ protected :module_specs_public_method_on_object_for_kernel_protected
+ private :module_specs_public_method_on_object_for_kernel_private
+end
+
+ModuleSpecs::Nesting[:root_level] = Module.nesting
diff --git a/spec/ruby/core/module/fixtures/const_added.rb b/spec/ruby/core/module/fixtures/const_added.rb
new file mode 100644
index 0000000000..0f5baad65d
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/const_added.rb
@@ -0,0 +1,4 @@
+module ModuleSpecs
+ module ConstAddedSpecs
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/constant_unicode.rb b/spec/ruby/core/module/fixtures/constant_unicode.rb
new file mode 100644
index 0000000000..415911576d
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/constant_unicode.rb
@@ -0,0 +1,5 @@
+# encoding: utf-8
+
+module ConstantUnicodeSpecs
+ CS_CONSTλ = :const_unicode
+end
diff --git a/spec/ruby/core/module/fixtures/constants_autoload.rb b/spec/ruby/core/module/fixtures/constants_autoload.rb
new file mode 100644
index 0000000000..8e9aa8de0c
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/constants_autoload.rb
@@ -0,0 +1,6 @@
+autoload :CSAutoloadA, fixture(__FILE__, 'constants_autoload_a.rb')
+autoload :CSAutoloadB, fixture(__FILE__, 'constants_autoload_b.rb')
+autoload :CSAutoloadC, fixture(__FILE__, 'constants_autoload_c.rb')
+module CSAutoloadD
+ autoload :InnerModule, fixture(__FILE__, 'constants_autoload_d.rb')
+end
diff --git a/spec/ruby/core/module/fixtures/constants_autoload_a.rb b/spec/ruby/core/module/fixtures/constants_autoload_a.rb
new file mode 100644
index 0000000000..48d3b63681
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/constants_autoload_a.rb
@@ -0,0 +1,2 @@
+module CSAutoloadA
+end
diff --git a/spec/ruby/core/module/fixtures/constants_autoload_b.rb b/spec/ruby/core/module/fixtures/constants_autoload_b.rb
new file mode 100644
index 0000000000..29cd742d03
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/constants_autoload_b.rb
@@ -0,0 +1,2 @@
+module CSAutoloadB
+end
diff --git a/spec/ruby/core/module/fixtures/constants_autoload_c.rb b/spec/ruby/core/module/fixtures/constants_autoload_c.rb
new file mode 100644
index 0000000000..9d6a6bf4d7
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/constants_autoload_c.rb
@@ -0,0 +1,3 @@
+module CSAutoloadC
+ CONST = 7
+end
diff --git a/spec/ruby/core/module/fixtures/constants_autoload_d.rb b/spec/ruby/core/module/fixtures/constants_autoload_d.rb
new file mode 100644
index 0000000000..52d550bab0
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/constants_autoload_d.rb
@@ -0,0 +1,4 @@
+module CSAutoloadD
+ module InnerModule
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/module.rb b/spec/ruby/core/module/fixtures/module.rb
new file mode 100644
index 0000000000..34543ca2b4
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/module.rb
@@ -0,0 +1,8 @@
+module ModuleSpecs
+ module Anonymous
+ module Child
+ end
+
+ SameChild = Child
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/multi/foo.rb b/spec/ruby/core/module/fixtures/multi/foo.rb
new file mode 100644
index 0000000000..549996f08f
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/multi/foo.rb
@@ -0,0 +1,6 @@
+module ModuleSpecs::Autoload
+ module Foo
+ autoload :Bar, 'foo/bar_baz'
+ autoload :Baz, 'foo/bar_baz'
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb b/spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb
new file mode 100644
index 0000000000..53d3849e1f
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/multi/foo/bar_baz.rb
@@ -0,0 +1,11 @@
+require 'foo'
+
+module ModuleSpecs::Autoload
+ module Foo
+ class Bar
+ end
+
+ class Baz
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/name.rb b/spec/ruby/core/module/fixtures/name.rb
new file mode 100644
index 0000000000..25c74d3944
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/name.rb
@@ -0,0 +1,13 @@
+# -*- encoding: utf-8 -*-
+module ModuleSpecs
+ class NameEncoding
+ class Cß
+ end
+ def name
+ Cß.name
+ end
+ end
+
+ module NameSpecs
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/path1/load_path.rb b/spec/ruby/core/module/fixtures/path1/load_path.rb
new file mode 100644
index 0000000000..d4c45463dc
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/path1/load_path.rb
@@ -0,0 +1,9 @@
+$LOAD_PATH.unshift(File.expand_path('../../path2', __FILE__))
+
+module ModuleSpecs::Autoload
+ module LoadPath
+ def self.loaded
+ :autoload_load_path
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/path2/load_path.rb b/spec/ruby/core/module/fixtures/path2/load_path.rb
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/path2/load_path.rb
@@ -0,0 +1 @@
+
diff --git a/spec/ruby/core/module/fixtures/refine.rb b/spec/ruby/core/module/fixtures/refine.rb
new file mode 100644
index 0000000000..e8215aa640
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/refine.rb
@@ -0,0 +1,25 @@
+module ModuleSpecs
+ class ClassWithFoo
+ def foo; "foo" end
+ end
+
+ class ClassWithSuperFoo
+ def foo; [:C] end
+ end
+
+ module PrependedModule
+ def foo; "foo from prepended module"; end
+ end
+
+ module IncludedModule
+ def foo; "foo from included module"; end
+ end
+
+ def self.build_refined_class(for_super: false)
+ if for_super
+ Class.new(ClassWithSuperFoo)
+ else
+ Class.new(ClassWithFoo)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb b/spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb
new file mode 100644
index 0000000000..32b770e6cf
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/repeated_concurrent_autoload.rb
@@ -0,0 +1,8 @@
+prev_value = ScratchPad.recorded.increment_and_get
+eval <<-RUBY_EVAL
+ module Mod#{prev_value}
+ sleep(0.05)
+ def self.foo
+ end
+ end
+RUBY_EVAL
diff --git a/spec/ruby/core/module/fixtures/set_temporary_name.rb b/spec/ruby/core/module/fixtures/set_temporary_name.rb
new file mode 100644
index 0000000000..901b3b94d1
--- /dev/null
+++ b/spec/ruby/core/module/fixtures/set_temporary_name.rb
@@ -0,0 +1,4 @@
+module ModuleSpecs
+ module SetTemporaryNameSpec
+ end
+end
diff --git a/spec/ruby/core/module/freeze_spec.rb b/spec/ruby/core/module/freeze_spec.rb
new file mode 100644
index 0000000000..fd76141431
--- /dev/null
+++ b/spec/ruby/core/module/freeze_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#freeze" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/module/gt_spec.rb b/spec/ruby/core/module/gt_spec.rb
new file mode 100644
index 0000000000..04cdd90efb
--- /dev/null
+++ b/spec/ruby/core/module/gt_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#>" do
+ it "returns false if self is a subclass of or includes the given module" do
+ (ModuleSpecs::Child > ModuleSpecs::Parent).should == false
+ (ModuleSpecs::Child > ModuleSpecs::Basic).should == false
+ (ModuleSpecs::Child > ModuleSpecs::Super).should == false
+ (ModuleSpecs::Super > ModuleSpecs::Basic).should == false
+ end
+
+ it "returns true if self is a superclass of or included by the given module" do
+ (ModuleSpecs::Parent > ModuleSpecs::Child).should == true
+ (ModuleSpecs::Basic > ModuleSpecs::Child).should == true
+ (ModuleSpecs::Super > ModuleSpecs::Child).should == true
+ (ModuleSpecs::Basic > ModuleSpecs::Super).should == true
+ end
+
+ it "returns false if self is the same as the given module" do
+ (ModuleSpecs::Child > ModuleSpecs::Child).should == false
+ (ModuleSpecs::Parent > ModuleSpecs::Parent).should == false
+ (ModuleSpecs::Basic > ModuleSpecs::Basic).should == false
+ (ModuleSpecs::Super > ModuleSpecs::Super).should == false
+ end
+
+ it "returns nil if self is not related to the given module" do
+ (ModuleSpecs::Parent > ModuleSpecs::Basic).should == nil
+ (ModuleSpecs::Parent > ModuleSpecs::Super).should == nil
+ (ModuleSpecs::Basic > ModuleSpecs::Parent).should == nil
+ (ModuleSpecs::Super > ModuleSpecs::Parent).should == nil
+ end
+
+ it "raises a TypeError if the argument is not a class/module" do
+ -> { ModuleSpecs::Parent > mock('x') }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/gte_spec.rb b/spec/ruby/core/module/gte_spec.rb
new file mode 100644
index 0000000000..b19fc9fac3
--- /dev/null
+++ b/spec/ruby/core/module/gte_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#>=" do
+ it "returns true if self is a superclass of, the same as or included by given module" do
+ (ModuleSpecs::Parent >= ModuleSpecs::Child).should == true
+ (ModuleSpecs::Basic >= ModuleSpecs::Child).should == true
+ (ModuleSpecs::Super >= ModuleSpecs::Child).should == true
+ (ModuleSpecs::Basic >= ModuleSpecs::Super).should == true
+ (ModuleSpecs::Child >= ModuleSpecs::Child).should == true
+ (ModuleSpecs::Parent >= ModuleSpecs::Parent).should == true
+ (ModuleSpecs::Basic >= ModuleSpecs::Basic).should == true
+ (ModuleSpecs::Super >= ModuleSpecs::Super).should == true
+ end
+
+ it "returns nil if self is not related to the given module" do
+ (ModuleSpecs::Parent >= ModuleSpecs::Basic).should == nil
+ (ModuleSpecs::Parent >= ModuleSpecs::Super).should == nil
+ (ModuleSpecs::Basic >= ModuleSpecs::Parent).should == nil
+ (ModuleSpecs::Super >= ModuleSpecs::Parent).should == nil
+ end
+
+ it "returns false if self is a subclass of or includes the given module" do
+ (ModuleSpecs::Child >= ModuleSpecs::Parent).should == false
+ (ModuleSpecs::Child >= ModuleSpecs::Basic).should == false
+ (ModuleSpecs::Child >= ModuleSpecs::Super).should == false
+ (ModuleSpecs::Super >= ModuleSpecs::Basic).should == false
+ end
+
+ it "raises a TypeError if the argument is not a class/module" do
+ -> { ModuleSpecs::Parent >= mock('x') }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/include_spec.rb b/spec/ruby/core/module/include_spec.rb
new file mode 100644
index 0000000000..b6201550e4
--- /dev/null
+++ b/spec/ruby/core/module/include_spec.rb
@@ -0,0 +1,628 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#include" do
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:include)
+ end
+
+ it "calls #append_features(self) in reversed order on each module" do
+ $appended_modules = []
+
+ m = Module.new do
+ def self.append_features(mod)
+ $appended_modules << [ self, mod ]
+ end
+ end
+
+ m2 = Module.new do
+ def self.append_features(mod)
+ $appended_modules << [ self, mod ]
+ end
+ end
+
+ m3 = Module.new do
+ def self.append_features(mod)
+ $appended_modules << [ self, mod ]
+ end
+ end
+
+ c = Class.new { include(m, m2, m3) }
+
+ $appended_modules.should == [ [ m3, c], [ m2, c ], [ m, c ] ]
+ end
+
+ it "adds all ancestor modules when a previously included module is included again" do
+ ModuleSpecs::MultipleIncludes.ancestors.to_set.should >= Set[ModuleSpecs::MA, ModuleSpecs::MB]
+ ModuleSpecs::MB.include(ModuleSpecs::MC)
+ ModuleSpecs::MultipleIncludes.include(ModuleSpecs::MB)
+ ModuleSpecs::MultipleIncludes.ancestors.to_set.should >= Set[ModuleSpecs::MA, ModuleSpecs::MB, ModuleSpecs::MC]
+ end
+
+ it "raises a TypeError when the argument is not a Module" do
+ -> { ModuleSpecs::Basic.include(Class.new) }.should.raise(TypeError)
+ end
+
+ it "does not raise a TypeError when the argument is an instance of a subclass of Module" do
+ class ModuleSpecs::SubclassSpec::AClass
+ end
+ -> { ModuleSpecs::SubclassSpec::AClass.include(ModuleSpecs::Subclass.new) }.should_not.raise(TypeError)
+ ensure
+ ModuleSpecs::SubclassSpec.send(:remove_const, :AClass)
+ end
+
+ it "raises a TypeError when the argument is a refinement" do
+ refinement = nil
+
+ Module.new do
+ refine String do
+ refinement = self
+ end
+ end
+
+ -> { ModuleSpecs::Basic.include(refinement) }.should.raise(TypeError, "Cannot include refinement")
+ end
+
+ it "imports constants to modules and classes" do
+ ModuleSpecs::A.constants.should.include?(:CONSTANT_A)
+ ModuleSpecs::B.constants.to_set.should >= Set[:CONSTANT_A, :CONSTANT_B]
+ ModuleSpecs::C.constants.to_set.should >= Set[:CONSTANT_A, :CONSTANT_B]
+ end
+
+ it "shadows constants from ancestors" do
+ klass = Class.new
+ klass.include ModuleSpecs::ShadowingOuter::M
+ klass::SHADOW.should == 123
+ klass.include ModuleSpecs::ShadowingOuter::N
+ klass::SHADOW.should == 456
+ end
+
+ it "does not override existing constants in modules and classes" do
+ ModuleSpecs::A::OVERRIDE.should == :a
+ ModuleSpecs::B::OVERRIDE.should == :b
+ ModuleSpecs::C::OVERRIDE.should == :c
+ end
+
+ it "imports instance methods to modules and classes" do
+ ModuleSpecs::A.instance_methods.should.include?(:ma)
+ ModuleSpecs::B.instance_methods.to_set.should >= Set[:ma,:mb]
+ ModuleSpecs::C.instance_methods.to_set.should >= Set[:ma,:mb]
+ end
+
+ it "does not import methods to modules and classes" do
+ ModuleSpecs::A.methods.include?(:cma).should == true
+ ModuleSpecs::B.methods.include?(:cma).should == false
+ ModuleSpecs::B.methods.include?(:cmb).should == true
+ ModuleSpecs::C.methods.include?(:cma).should == false
+ ModuleSpecs::C.methods.include?(:cmb).should == false
+ end
+
+ it "attaches the module as the caller's immediate ancestor" do
+ module IncludeSpecsTop
+ def value; 5; end
+ end
+
+ module IncludeSpecsMiddle
+ include IncludeSpecsTop
+ def value; 6; end
+ end
+
+ class IncludeSpecsClass
+ include IncludeSpecsMiddle
+ end
+
+ IncludeSpecsClass.new.value.should == 6
+ end
+
+ it "doesn't include module if it is included in a super class" do
+ module ModuleSpecs::M1
+ module M; end
+ class A; include M; end
+ class B < A; include M; end
+
+ all = [A, B, M]
+
+ (B.ancestors.filter { |a| all.include?(a) }).should == [B, A, M]
+ end
+ end
+
+ it "recursively includes new mixins" do
+ module ModuleSpecs::M1
+ module U; end
+ module V; end
+ module W; end
+ module X; end
+ module Y; end
+ class A; include X; end;
+ class B < A; include U, V, W; end;
+
+ # update V
+ module V; include X, U, Y; end
+
+ # This code used to use Array#& and then compare 2 arrays, but
+ # the ordering from Array#& is undefined, as it uses Hash internally.
+ #
+ # Loop is more verbose, but more explicit in what we're testing.
+
+ anc = B.ancestors
+ [B, U, V, W, A, X].each do |i|
+ anc.include?(i).should == true
+ end
+
+ class B; include V; end
+
+ # the only new module is Y, it is added after U since it follows U in V mixin list:
+ anc = B.ancestors
+ [B, U, Y, V, W, A, X].each do |i|
+ anc.include?(i).should == true
+ end
+ end
+ end
+
+ it "preserves ancestor order" do
+ module ModuleSpecs::M2
+ module M1; end
+ module M2; end
+ module M3; include M2; end
+
+ module M2; include M1; end
+ module M3; include M2; end
+
+ M3.ancestors.should == [M3, M2, M1]
+
+ end
+ end
+
+ it "detects cyclic includes" do
+ -> {
+ module ModuleSpecs::M
+ include ModuleSpecs::M
+ end
+ }.should.raise(ArgumentError)
+ end
+
+ it "doesn't accept no-arguments" do
+ -> {
+ Module.new do
+ include
+ end
+ }.should.raise(ArgumentError)
+ end
+
+ it "returns the class it's included into" do
+ m = Module.new
+ r = nil
+ c = Class.new { r = include m }
+ r.should == c
+ end
+
+ it "ignores modules it has already included via module mutual inclusion" do
+ module ModuleSpecs::AlreadyInc
+ module M0
+ end
+
+ module M
+ include M0
+ end
+
+ class K
+ include M
+ include M
+ end
+
+ K.ancestors[0].should == K
+ K.ancestors[1].should == M
+ K.ancestors[2].should == M0
+ end
+ end
+
+ it "clears any caches" do
+ module ModuleSpecs::M3
+ module M1
+ def foo
+ :m1
+ end
+ end
+
+ module M2
+ def foo
+ :m2
+ end
+ end
+
+ class C
+ include M1
+
+ def get
+ foo
+ end
+ end
+
+ c = C.new
+ c.get.should == :m1
+
+ class C
+ include M2
+ end
+
+ c.get.should == :m2
+
+ remove_const :C
+ end
+ end
+
+ it "updates the method when an included module is updated" do
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+
+ m_module = Module.new
+
+ b_class = Class.new(a_class) do
+ include m_module
+ end
+
+ b = b_class.new
+
+ foo = -> { b.foo }
+
+ foo.call.should == 'a'
+
+ m_module.module_eval do
+ def foo
+ 'm'
+ end
+ end
+
+ foo.call.should == 'm'
+ end
+
+
+ it "updates the method when a module included after a call is later updated" do
+ m_module = Module.new
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+ b_class = Class.new(a_class)
+ b = b_class.new
+ foo = -> { b.foo }
+ foo.call.should == 'a'
+
+ b_class.include m_module
+ foo.call.should == 'a'
+
+ m_module.module_eval do
+ def foo
+ "m"
+ end
+ end
+ foo.call.should == 'm'
+ end
+
+ it "updates the method when a nested included module is updated" do
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+
+ n_module = Module.new
+
+ m_module = Module.new do
+ include n_module
+ end
+
+ b_class = Class.new(a_class) do
+ include m_module
+ end
+
+ b = b_class.new
+
+ foo = -> { b.foo }
+
+ foo.call.should == 'a'
+
+ n_module.module_eval do
+ def foo
+ 'n'
+ end
+ end
+
+ foo.call.should == 'n'
+ end
+
+ it "updates the method when a new module is included" do
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+
+ m_module = Module.new do
+ def foo
+ 'm'
+ end
+ end
+
+ b_class = Class.new(a_class)
+ b = b_class.new
+
+ foo = -> { b.foo }
+
+ foo.call.should == 'a'
+
+ b_class.class_eval do
+ include m_module
+ end
+
+ foo.call.should == 'm'
+ end
+
+ it "updates the method when a new module with nested module is included" do
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+
+ n_module = Module.new do
+ def foo
+ 'n'
+ end
+ end
+
+ m_module = Module.new do
+ include n_module
+ end
+
+ b_class = Class.new(a_class)
+ b = b_class.new
+
+ foo = -> { b.foo }
+
+ foo.call.should == 'a'
+
+ b_class.class_eval do
+ include m_module
+ end
+
+ foo.call.should == 'n'
+ end
+
+ it "updates the constant when an included module is updated" do
+ module ModuleSpecs::ConstUpdated
+ module A
+ FOO = 'a'
+ end
+
+ module M
+ end
+
+ module B
+ include A
+ include M
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+
+ M.const_set(:FOO, 'm')
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdated)
+ end
+
+ it "updates the constant when a module included after a call is later updated" do
+ module ModuleSpecs::ConstLaterUpdated
+ module A
+ FOO = 'a'
+ end
+
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+
+ module M
+ end
+ B.include M
+
+ B.foo.should == 'a'
+
+ M.const_set(:FOO, 'm')
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstLaterUpdated)
+ end
+
+ it "updates the constant when a module included in another module after a call is later updated" do
+ module ModuleSpecs::ConstModuleLaterUpdated
+ module A
+ FOO = 'a'
+ end
+
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+
+ module M
+ end
+ B.include M
+
+ B.foo.should == 'a'
+
+ M.const_set(:FOO, 'm')
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstModuleLaterUpdated)
+ end
+
+ it "updates the constant when a nested included module is updated" do
+ module ModuleSpecs::ConstUpdatedNestedIncludeUpdated
+ module A
+ FOO = 'a'
+ end
+
+ module N
+ end
+
+ module M
+ include N
+ end
+
+ module B
+ include A
+ include M
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+
+ N.const_set(:FOO, 'n')
+ B.foo.should == 'n'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedNestedIncludeUpdated)
+ end
+
+ it "updates the constant when a new module is included" do
+ module ModuleSpecs::ConstUpdatedNewInclude
+ module A
+ FOO = 'a'
+ end
+
+ module M
+ FOO = 'm'
+ end
+
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+
+ B.include(M)
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedNewInclude)
+ end
+
+ it "updates the constant when a new module with nested module is included" do
+ module ModuleSpecs::ConstUpdatedNestedIncluded
+ module A
+ FOO = 'a'
+ end
+
+ module N
+ FOO = 'n'
+ end
+
+ module M
+ include N
+ end
+
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+
+ B.include M
+ B.foo.should == 'n'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedNestedIncluded)
+ end
+
+ it "overrides a previous super method call" do
+ c1 = Class.new do
+ def foo
+ [:c1]
+ end
+ end
+ c2 = Class.new(c1) do
+ def foo
+ [:c2] + super
+ end
+ end
+ c2.new.foo.should == [:c2, :c1]
+ m = Module.new do
+ def foo
+ [:m1]
+ end
+ end
+ c2.include(m)
+ c2.new.foo.should == [:c2, :m1]
+ end
+
+ it "update a module when a nested module is updated and includes a module on its own" do
+ m1 = Module.new
+ m2 = Module.new do
+ def m2; [:m2]; end
+ end
+ m3 = Module.new do
+ def m3; [:m3]; end
+ end
+ m4 = Module.new do
+ def m4; [:m4]; end
+ end
+ c = Class.new
+
+ c.include(m1)
+ m1.include(m2)
+ m2.include(m3)
+ m3.include(m4)
+
+ c.new.m2.should == [:m2]
+ c.new.m3.should == [:m3]
+ c.new.m4.should == [:m4]
+ end
+end
+
+describe "Module#include?" do
+ it "returns true if the given module is included by self or one of it's ancestors" do
+ ModuleSpecs::Super.include?(ModuleSpecs::Basic).should == true
+ ModuleSpecs::Child.include?(ModuleSpecs::Basic).should == true
+ ModuleSpecs::Child.include?(ModuleSpecs::Super).should == true
+ ModuleSpecs::Child.include?(Kernel).should == true
+
+ ModuleSpecs::Parent.include?(ModuleSpecs::Basic).should == false
+ ModuleSpecs::Basic.include?(ModuleSpecs::Super).should == false
+ end
+
+ it "returns false if given module is equal to self" do
+ ModuleSpecs.include?(ModuleSpecs).should == false
+ end
+
+ it "raises a TypeError when no module was given" do
+ -> { ModuleSpecs::Child.include?("Test") }.should.raise(TypeError)
+ -> { ModuleSpecs::Child.include?(ModuleSpecs::Parent) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/included_modules_spec.rb b/spec/ruby/core/module/included_modules_spec.rb
new file mode 100644
index 0000000000..e22ee5e6cf
--- /dev/null
+++ b/spec/ruby/core/module/included_modules_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#included_modules" do
+ it "returns a list of modules included in self" do
+ ModuleSpecs.included_modules.should == []
+
+ ModuleSpecs::Child.included_modules.to_set.should >= Set[ModuleSpecs::Super, ModuleSpecs::Basic, Kernel]
+ ModuleSpecs::Parent.included_modules.should.include?(Kernel)
+ ModuleSpecs::Basic.included_modules.should == []
+ ModuleSpecs::Super.included_modules.should.include?(ModuleSpecs::Basic)
+ ModuleSpecs::WithPrependedModule.included_modules.should.include?(ModuleSpecs::ModuleWithPrepend)
+ end
+end
diff --git a/spec/ruby/core/module/included_spec.rb b/spec/ruby/core/module/included_spec.rb
new file mode 100644
index 0000000000..d8a6c3f839
--- /dev/null
+++ b/spec/ruby/core/module/included_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#included" do
+ it "is invoked when self is included in another module or class" do
+ begin
+ m = Module.new do
+ def self.included(o)
+ $included_by = o
+ end
+ end
+
+ c = Class.new { include m }
+
+ $included_by.should == c
+ ensure
+ $included_by = nil
+ end
+ end
+
+ it "allows extending self with the object into which it is being included" do
+ m = Module.new do
+ def self.included(o)
+ o.extend(self)
+ end
+
+ def test
+ :passed
+ end
+ end
+
+ c = Class.new{ include(m) }
+ c.test.should == :passed
+ end
+
+ it "is private in its default implementation" do
+ Module.private_instance_methods(false).should.include?(:included)
+ end
+
+ it "works with super using a singleton class" do
+ ModuleSpecs::SingletonOnModuleCase::Bar.include ModuleSpecs::SingletonOnModuleCase::Foo
+ ModuleSpecs::SingletonOnModuleCase::Bar.should.included_called?
+ end
+end
diff --git a/spec/ruby/core/module/initialize_copy_spec.rb b/spec/ruby/core/module/initialize_copy_spec.rb
new file mode 100644
index 0000000000..7ae48f85a9
--- /dev/null
+++ b/spec/ruby/core/module/initialize_copy_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+
+describe "Module#initialize_copy" do
+ it "should retain singleton methods when duped" do
+ mod = Module.new
+ def mod.hello
+ end
+ mod.dup.methods(false).should == [:hello]
+ end
+
+ # jruby/jruby#5245, https://bugs.ruby-lang.org/issues/3461
+ it "should produce a duped module with inspectable class methods" do
+ mod = Module.new
+ def mod.hello
+ end
+ mod.dup.method(:hello).inspect.should =~ /Module.*hello/
+ end
+end
diff --git a/spec/ruby/core/module/initialize_spec.rb b/spec/ruby/core/module/initialize_spec.rb
new file mode 100644
index 0000000000..99e41e4619
--- /dev/null
+++ b/spec/ruby/core/module/initialize_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#initialize" do
+ it "accepts a block" do
+ m = Module.new do
+ const_set :A, "A"
+ end
+ m.const_get("A").should == "A"
+ end
+
+ it "is called on subclasses" do
+ m = ModuleSpecs::SubModule.new
+ m.special.should == 10
+ m.methods.should_not == nil
+ m.constants.should_not == nil
+ end
+end
diff --git a/spec/ruby/core/module/instance_method_spec.rb b/spec/ruby/core/module/instance_method_spec.rb
new file mode 100644
index 0000000000..8615e352f6
--- /dev/null
+++ b/spec/ruby/core/module/instance_method_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#instance_method" do
+ before :all do
+ @parent_um = ModuleSpecs::InstanceMeth.instance_method(:foo)
+ @child_um = ModuleSpecs::InstanceMethChild.instance_method(:foo)
+ @mod_um = ModuleSpecs::InstanceMethChild.instance_method(:bar)
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:instance_method)
+ end
+
+ it "requires an argument" do
+ Module.new.method(:instance_method).arity.should == 1
+ end
+
+ it "returns an UnboundMethod corresponding to the given name" do
+ @parent_um.should.is_a?(UnboundMethod)
+ @parent_um.bind(ModuleSpecs::InstanceMeth.new).call.should == :foo
+ end
+
+ it "returns an UnboundMethod corresponding to the given name from a superclass" do
+ @child_um.should.is_a?(UnboundMethod)
+ @child_um.bind(ModuleSpecs::InstanceMethChild.new).call.should == :foo
+ end
+
+ it "returns an UnboundMethod corresponding to the given name from an included Module" do
+ @mod_um.should.is_a?(UnboundMethod)
+ @mod_um.bind(ModuleSpecs::InstanceMethChild.new).call.should == :bar
+ end
+
+ it "returns an UnboundMethod when given a protected method name" do
+ ModuleSpecs::Basic.instance_method(:protected_module).should.instance_of?(UnboundMethod)
+ end
+
+ it "returns an UnboundMethod when given a private method name" do
+ ModuleSpecs::Basic.instance_method(:private_module).should.instance_of?(UnboundMethod)
+ end
+
+ it "gives UnboundMethod method name, Module defined in and Module extracted from" do
+ @parent_um.inspect.should =~ /\bfoo\b/
+ @parent_um.inspect.should =~ /\bModuleSpecs::InstanceMeth\b/
+ @parent_um.inspect.should =~ /\bModuleSpecs::InstanceMeth\b/
+ @child_um.inspect.should =~ /\bfoo\b/
+ @child_um.inspect.should =~ /\bModuleSpecs::InstanceMeth\b/
+
+ @mod_um.inspect.should =~ /\bbar\b/
+ @mod_um.inspect.should =~ /\bModuleSpecs::InstanceMethMod\b/
+ end
+
+ it "raises a TypeError if the given name is not a String/Symbol" do
+ -> { Object.instance_method([]) }.should.raise(TypeError, /is not a symbol nor a string/)
+ -> { Object.instance_method(0) }.should.raise(TypeError, /is not a symbol nor a string/)
+ -> { Object.instance_method(nil) }.should.raise(TypeError, /is not a symbol nor a string/)
+ -> { Object.instance_method(mock('x')) }.should.raise(TypeError, /is not a symbol nor a string/)
+ end
+
+ it "accepts String name argument" do
+ method = ModuleSpecs::InstanceMeth.instance_method(:foo)
+ method.should.is_a?(UnboundMethod)
+ end
+
+ it "accepts Symbol name argument" do
+ method = ModuleSpecs::InstanceMeth.instance_method("foo")
+ method.should.is_a?(UnboundMethod)
+ end
+
+ it "converts non-String name by calling #to_str method" do
+ obj = Object.new
+ def obj.to_str() "foo" end
+
+ method = ModuleSpecs::InstanceMeth.instance_method(obj)
+ method.should.is_a?(UnboundMethod)
+ end
+
+ it "raises TypeError when passed non-String name and #to_str returns non-String value" do
+ obj = Object.new
+ def obj.to_str() [] end
+
+ -> { ModuleSpecs::InstanceMeth.instance_method(obj) }.should raise_consistent_error(TypeError, /can't convert Object into String/)
+ end
+
+ it "raises a NameError if the method has been undefined" do
+ child = Class.new(ModuleSpecs::InstanceMeth)
+ child.send :undef_method, :foo
+ um = ModuleSpecs::InstanceMeth.instance_method(:foo)
+ um.should == @parent_um
+ -> do
+ child.instance_method(:foo)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError if the method does not exist" do
+ -> { Object.instance_method(:missing) }.should.raise(NameError)
+ end
+
+ it "sets the NameError#name attribute to the name of the missing method" do
+ begin
+ Object.instance_method(:missing)
+ rescue NameError => e
+ e.name.should == :missing
+ end
+ end
+end
diff --git a/spec/ruby/core/module/instance_methods_spec.rb b/spec/ruby/core/module/instance_methods_spec.rb
new file mode 100644
index 0000000000..dbb6df8c34
--- /dev/null
+++ b/spec/ruby/core/module/instance_methods_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#instance_methods" do
+ it "does not return methods undefined in a superclass" do
+ methods = ModuleSpecs::Parent.instance_methods(false)
+ methods.should_not.include?(:undefed_method)
+ end
+
+ it "only includes module methods on an included module" do
+ methods = ModuleSpecs::Basic.instance_methods(false)
+ methods.should.include?(:public_module)
+ # Child is an including class
+ methods = ModuleSpecs::Child.instance_methods(false)
+ methods.should.include?(:public_child)
+ methods.should_not.include?(:public_module)
+ end
+
+ it "does not return methods undefined in a subclass" do
+ methods = ModuleSpecs::Grandchild.instance_methods
+ methods.should_not.include?(:parent_method)
+ methods.should_not.include?(:another_parent_method)
+ end
+
+ it "does not return methods undefined in the current class" do
+ class ModuleSpecs::Child
+ def undefed_child
+ end
+ end
+ ModuleSpecs::Child.send(:undef_method, :undefed_child)
+ methods = ModuleSpecs::Child.instance_methods
+ methods.should_not.include?(:undefed_method)
+ methods.should_not.include?(:undefed_child)
+ end
+
+ it "does not return methods from an included module that are undefined in the class" do
+ ModuleSpecs::Grandchild.instance_methods.should_not.include?(:super_included_method)
+ end
+
+ it "returns the public and protected methods of self if include_super is false" do
+ methods = ModuleSpecs::Parent.instance_methods(false)
+ methods.to_set.should >= Set[:protected_parent, :public_parent]
+
+ methods = ModuleSpecs::Child.instance_methods(false)
+ methods.to_set.should >= Set[:protected_child, :public_child]
+ end
+
+ it "returns the public and protected methods of self and it's ancestors" do
+ methods = ModuleSpecs::Basic.instance_methods
+ methods.to_set.should >= Set[:protected_module, :public_module]
+
+ methods = ModuleSpecs::Super.instance_methods
+ methods.to_set.should >= Set[:protected_module, :protected_super_module,
+ :public_module, :public_super_module]
+ end
+
+ it "makes a private Object instance method public in Kernel" do
+ methods = Kernel.instance_methods
+ methods.should.include?(:module_specs_private_method_on_object_for_kernel_public)
+ methods = Object.instance_methods
+ methods.should_not.include?(:module_specs_private_method_on_object_for_kernel_public)
+ end
+end
diff --git a/spec/ruby/core/module/lt_spec.rb b/spec/ruby/core/module/lt_spec.rb
new file mode 100644
index 0000000000..8567a24993
--- /dev/null
+++ b/spec/ruby/core/module/lt_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#<" do
+ it "returns true if self is a subclass of or includes the given module" do
+ (ModuleSpecs::Child < ModuleSpecs::Parent).should == true
+ (ModuleSpecs::Child < ModuleSpecs::Basic).should == true
+ (ModuleSpecs::Child < ModuleSpecs::Super).should == true
+ (ModuleSpecs::Super < ModuleSpecs::Basic).should == true
+ end
+
+ it "returns false if self is a superclass of or included by the given module" do
+ (ModuleSpecs::Parent < ModuleSpecs::Child).should == false
+ (ModuleSpecs::Basic < ModuleSpecs::Child).should == false
+ (ModuleSpecs::Super < ModuleSpecs::Child).should == false
+ (ModuleSpecs::Basic < ModuleSpecs::Super).should == false
+ end
+
+ it "returns false if self is the same as the given module" do
+ (ModuleSpecs::Child < ModuleSpecs::Child).should == false
+ (ModuleSpecs::Parent < ModuleSpecs::Parent).should == false
+ (ModuleSpecs::Basic < ModuleSpecs::Basic).should == false
+ (ModuleSpecs::Super < ModuleSpecs::Super).should == false
+ end
+
+ it "returns nil if self is not related to the given module" do
+ (ModuleSpecs::Parent < ModuleSpecs::Basic).should == nil
+ (ModuleSpecs::Parent < ModuleSpecs::Super).should == nil
+ (ModuleSpecs::Basic < ModuleSpecs::Parent).should == nil
+ (ModuleSpecs::Super < ModuleSpecs::Parent).should == nil
+ end
+
+ it "raises a TypeError if the argument is not a class/module" do
+ -> { ModuleSpecs::Parent < mock('x') }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/lte_spec.rb b/spec/ruby/core/module/lte_spec.rb
new file mode 100644
index 0000000000..c6aab94e9f
--- /dev/null
+++ b/spec/ruby/core/module/lte_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#<=" do
+ it "returns true if self is a subclass of, the same as or includes the given module" do
+ (ModuleSpecs::Child <= ModuleSpecs::Parent).should == true
+ (ModuleSpecs::Child <= ModuleSpecs::Basic).should == true
+ (ModuleSpecs::Child <= ModuleSpecs::Super).should == true
+ (ModuleSpecs::Super <= ModuleSpecs::Basic).should == true
+ (ModuleSpecs::Child <= ModuleSpecs::Child).should == true
+ (ModuleSpecs::Parent <= ModuleSpecs::Parent).should == true
+ (ModuleSpecs::Basic <= ModuleSpecs::Basic).should == true
+ (ModuleSpecs::Super <= ModuleSpecs::Super).should == true
+ end
+
+ it "returns nil if self is not related to the given module" do
+ (ModuleSpecs::Parent <= ModuleSpecs::Basic).should == nil
+ (ModuleSpecs::Parent <= ModuleSpecs::Super).should == nil
+ (ModuleSpecs::Basic <= ModuleSpecs::Parent).should == nil
+ (ModuleSpecs::Super <= ModuleSpecs::Parent).should == nil
+ end
+
+ it "returns false if self is a superclass of or is included by the given module" do
+ (ModuleSpecs::Parent <= ModuleSpecs::Child).should == false
+ (ModuleSpecs::Basic <= ModuleSpecs::Child).should == false
+ (ModuleSpecs::Super <= ModuleSpecs::Child).should == false
+ (ModuleSpecs::Basic <= ModuleSpecs::Super).should == false
+ end
+
+ it "raises a TypeError if the argument is not a class/module" do
+ -> { ModuleSpecs::Parent <= mock('x') }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/module/method_added_spec.rb b/spec/ruby/core/module/method_added_spec.rb
new file mode 100644
index 0000000000..996a14eb86
--- /dev/null
+++ b/spec/ruby/core/module/method_added_spec.rb
@@ -0,0 +1,146 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#method_added" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "is a private instance method" do
+ Module.private_instance_methods(false).should.include?(:method_added)
+ end
+
+ it "returns nil in the default implementation" do
+ Module.new do
+ method_added(:test).should == nil
+ end
+ end
+
+ it "is called when a new instance method is defined in self" do
+ Module.new do
+ def self.method_added(name)
+ ScratchPad << name
+ end
+
+ def test() end
+ def test2() end
+ def test() end
+ alias_method :aliased_test, :test
+ alias aliased_test2 test
+ end
+
+ ScratchPad.recorded.should == [:test, :test2, :test, :aliased_test, :aliased_test2]
+ end
+
+ it "is not called when a singleton method is added" do
+ # obj.singleton_method_added is called instead
+ klass = Class.new
+ def klass.method_added(name)
+ ScratchPad << name
+ end
+
+ obj = klass.new
+ def obj.new_singleton_method
+ end
+
+ ScratchPad.recorded.should == []
+ end
+
+ it "is not called when a method is undefined in self" do
+ m = Module.new do
+ def method_to_undef
+ end
+
+ def self.method_added(name)
+ fail("method_added called by undef_method")
+ end
+
+ undef_method :method_to_undef
+ end
+ m.should_not.respond_to?(:method_to_undef)
+ end
+
+ it "is not called when a method changes visibility" do
+ Module.new do
+ def public_method
+ end
+
+ def private_method
+ end
+
+ def self.method_added(name)
+ ScratchPad << name
+ end
+
+ public :public_method
+ private :public_method
+
+ private :private_method
+ public :private_method
+ end
+
+ ScratchPad.recorded.should == []
+ end
+
+ it "is called when using #private in a subclass" do
+ parent = Class.new do
+ def foo
+ end
+ end
+
+ Class.new(parent) do
+ def self.method_added(name)
+ ScratchPad << name
+ end
+
+ # Create an instance as that might initialize some method lookup caches, which is interesting to test
+ self.new.foo
+
+ private :foo
+ public :foo
+ end
+
+ ScratchPad.recorded.should == [:foo]
+ end
+
+ it "is not called when a method is copied via module_function, rather #singleton_method_added is called" do
+ Module.new do
+ def mod_function
+ end
+
+ def self.method_added(name)
+ ScratchPad << [:method_added, name]
+ end
+
+ def self.singleton_method_added(name)
+ ScratchPad << [:singleton_method_added, name]
+ end
+
+ ScratchPad.record []
+
+ module_function :mod_function
+ end
+
+ ScratchPad.recorded.should == [[:singleton_method_added, :mod_function]]
+ end
+
+ it "is called with a precise caller location with the line of the 'def'" do
+ line = nil
+
+ Module.new do
+ def self.method_added(name)
+ location = caller_locations(1, 1)[0]
+ ScratchPad << location.lineno
+ end
+
+ line = __LINE__
+ def first
+ end
+
+ def second
+ end
+ end
+
+ ScratchPad.recorded.should == [line + 1, line + 4]
+ end
+end
diff --git a/spec/ruby/core/module/method_defined_spec.rb b/spec/ruby/core/module/method_defined_spec.rb
new file mode 100644
index 0000000000..e6b4c7b817
--- /dev/null
+++ b/spec/ruby/core/module/method_defined_spec.rb
@@ -0,0 +1,98 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#method_defined?" do
+ it "returns true if a public or private method with the given name is defined in self, self's ancestors or one of self's included modules" do
+ # Defined in Child
+ ModuleSpecs::Child.method_defined?(:public_child).should == true
+ ModuleSpecs::Child.method_defined?("private_child").should == false
+ ModuleSpecs::Child.method_defined?(:accessor_method).should == true
+
+ # Defined in Parent
+ ModuleSpecs::Child.method_defined?("public_parent").should == true
+ ModuleSpecs::Child.method_defined?(:private_parent).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.method_defined?(:public_module).should == true
+ ModuleSpecs::Child.method_defined?(:protected_module).should == true
+ ModuleSpecs::Child.method_defined?(:private_module).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.method_defined?(:public_super_module).should == true
+ ModuleSpecs::Child.method_defined?(:protected_super_module).should == true
+ ModuleSpecs::Child.method_defined?(:private_super_module).should == false
+ end
+
+ # unlike alias_method, module_function, public, and friends,
+ it "does not search Object or Kernel when called on a module" do
+ m = Module.new
+
+ m.method_defined?(:module_specs_public_method_on_kernel).should == false
+ end
+
+ it "raises a TypeError when the given object is not a string/symbol" do
+ c = Class.new
+ o = mock('123')
+
+ -> { c.method_defined?(o) }.should.raise(TypeError)
+
+ o.should_receive(:to_str).and_return(123)
+ -> { c.method_defined?(o) }.should.raise(TypeError)
+ end
+
+ it "converts the given name to a string using to_str" do
+ c = Class.new { def test(); end }
+ (o = mock('test')).should_receive(:to_str).and_return("test")
+
+ c.method_defined?(o).should == true
+ end
+
+ # works as method_defined?(method_name)
+ describe "when passed true as a second optional argument" do
+ it "performs a lookup in ancestors" do
+ ModuleSpecs::Child.method_defined?(:public_child, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_child, true).should == true
+ ModuleSpecs::Child.method_defined?(:accessor_method, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_child, true).should == false
+
+ # Defined in Parent
+ ModuleSpecs::Child.method_defined?(:public_parent, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_parent, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_parent, true).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.method_defined?(:public_module, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_module, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_module, true).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.method_defined?(:public_super_module, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_super_module, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_super_module, true).should == false
+ end
+ end
+
+ describe "when passed false as a second optional argument" do
+ it "checks only the class itself" do
+ ModuleSpecs::Child.method_defined?(:public_child, false).should == true
+ ModuleSpecs::Child.method_defined?(:protected_child, false).should == true
+ ModuleSpecs::Child.method_defined?(:accessor_method, false).should == true
+ ModuleSpecs::Child.method_defined?(:private_child, false).should == false
+
+ # Defined in Parent
+ ModuleSpecs::Child.method_defined?(:public_parent, false).should == false
+ ModuleSpecs::Child.method_defined?(:protected_parent, false).should == false
+ ModuleSpecs::Child.method_defined?(:private_parent, false).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.method_defined?(:public_module, false).should == false
+ ModuleSpecs::Child.method_defined?(:protected_module, false).should == false
+ ModuleSpecs::Child.method_defined?(:private_module, false).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.method_defined?(:public_super_module, false).should == false
+ ModuleSpecs::Child.method_defined?(:protected_super_module, false).should == false
+ ModuleSpecs::Child.method_defined?(:private_super_module, false).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/module/method_removed_spec.rb b/spec/ruby/core/module/method_removed_spec.rb
new file mode 100644
index 0000000000..80e546406c
--- /dev/null
+++ b/spec/ruby/core/module/method_removed_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#method_removed" do
+ it "is a private instance method" do
+ Module.private_instance_methods(false).should.include?(:method_removed)
+ end
+
+ it "returns nil in the default implementation" do
+ Module.new do
+ method_removed(:test).should == nil
+ end
+ end
+
+ it "is called when a method is removed from self" do
+ begin
+ Module.new do
+ def self.method_removed(name)
+ $method_removed = name
+ end
+
+ def test
+ "test"
+ end
+ remove_method :test
+ end
+
+ $method_removed.should == :test
+ ensure
+ $method_removed = nil
+ end
+ end
+end
diff --git a/spec/ruby/core/module/method_undefined_spec.rb b/spec/ruby/core/module/method_undefined_spec.rb
new file mode 100644
index 0000000000..596c5c50e2
--- /dev/null
+++ b/spec/ruby/core/module/method_undefined_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#method_undefined" do
+ it "is a private instance method" do
+ Module.private_instance_methods(false).should.include?(:method_undefined)
+ end
+
+ it "returns nil in the default implementation" do
+ Module.new do
+ method_undefined(:test).should == nil
+ end
+ end
+
+ it "is called when a method is undefined from self" do
+ begin
+ Module.new do
+ def self.method_undefined(name)
+ $method_undefined = name
+ end
+
+ def test
+ "test"
+ end
+ undef_method :test
+ end
+
+ $method_undefined.should == :test
+ ensure
+ $method_undefined = nil
+ end
+ end
+end
diff --git a/spec/ruby/core/module/module_eval_spec.rb b/spec/ruby/core/module/module_eval_spec.rb
new file mode 100644
index 0000000000..e9e9fda28d
--- /dev/null
+++ b/spec/ruby/core/module/module_eval_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/class_eval'
+
+describe "Module#module_eval" do
+ it_behaves_like :module_class_eval, :module_eval
+end
diff --git a/spec/ruby/core/module/module_exec_spec.rb b/spec/ruby/core/module/module_exec_spec.rb
new file mode 100644
index 0000000000..47cdf7ef52
--- /dev/null
+++ b/spec/ruby/core/module/module_exec_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/class_exec'
+
+describe "Module#module_exec" do
+ it_behaves_like :module_class_exec, :module_exec
+end
diff --git a/spec/ruby/core/module/module_function_spec.rb b/spec/ruby/core/module/module_function_spec.rb
new file mode 100644
index 0000000000..41bd152608
--- /dev/null
+++ b/spec/ruby/core/module/module_function_spec.rb
@@ -0,0 +1,358 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#module_function" do
+ it "is a private method" do
+ Module.private_instance_methods(false).should.include?(:module_function)
+ end
+
+ describe "on Class" do
+ it "is undefined" do
+ Class.private_instance_methods(true).should_not.include?(:module_function)
+ end
+
+ it "raises a TypeError if calling after rebinded to Class" do
+ -> {
+ Module.instance_method(:module_function).bind(Class.new).call
+ }.should.raise(TypeError)
+
+ -> {
+ Module.instance_method(:module_function).bind(Class.new).call :foo
+ }.should.raise(TypeError)
+ end
+ end
+end
+
+describe "Module#module_function with specific method names" do
+ it "creates duplicates of the given instance methods on the Module object" do
+ m = Module.new do
+ def test() end
+ def test2() end
+ def test3() end
+
+ module_function :test, :test2
+ end
+
+ m.respond_to?(:test).should == true
+ m.respond_to?(:test2).should == true
+ m.respond_to?(:test3).should == false
+ end
+
+ it "returns argument or arguments if given" do
+ Module.new do
+ def foo; end
+ module_function(:foo).should.equal?(:foo)
+ module_function(:foo, :foo).should == [:foo, :foo]
+ end
+ end
+
+ it "creates an independent copy of the method, not a redirect" do
+ module Mixin
+ def test
+ "hello"
+ end
+ module_function :test
+ end
+
+ class BaseClass
+ include Mixin
+ def call_test
+ test
+ end
+ end
+
+ Mixin.test.should == "hello"
+ c = BaseClass.new
+ c.call_test.should == "hello"
+
+ module Mixin
+ def test
+ "goodbye"
+ end
+ end
+
+ Mixin.test.should == "hello"
+ c.call_test.should == "goodbye"
+ end
+
+ it "makes the instance methods private" do
+ m = Module.new do
+ def test() "hello" end
+ module_function :test
+ end
+
+ (o = mock('x')).extend(m)
+ o.respond_to?(:test).should == false
+ m.private_instance_methods(false).should.include?(:test)
+ o.send(:test).should == "hello"
+ -> { o.test }.should.raise(NoMethodError)
+ end
+
+ it "makes the new Module methods public" do
+ m = Module.new do
+ def test() "hello" end
+ module_function :test
+ end
+
+ m.public_methods.map {|me| me.to_s }.include?('test').should == true
+ end
+
+ it "tries to convert the given names to strings using to_str" do
+ (o = mock('test')).should_receive(:to_str).any_number_of_times.and_return("test")
+ (o2 = mock('test2')).should_receive(:to_str).any_number_of_times.and_return("test2")
+
+ m = Module.new do
+ def test() end
+ def test2() end
+ module_function o, o2
+ end
+
+ m.respond_to?(:test).should == true
+ m.respond_to?(:test2).should == true
+ end
+
+ it "raises a TypeError when the given names can't be converted to string using to_str" do
+ o = mock('123')
+
+ -> { Module.new { module_function(o) } }.should.raise(TypeError)
+
+ o.should_receive(:to_str).and_return(123)
+ -> { Module.new { module_function(o) } }.should.raise(TypeError)
+ end
+
+ it "can make accessible private methods" do # JRUBY-4214
+ m = Module.new do
+ module_function :require
+ end
+ m.respond_to?(:require).should == true
+ end
+
+ it "creates Module methods that super up the singleton class of the module" do
+ super_m = Module.new do
+ def foo
+ "super_m"
+ end
+ end
+
+ m = Module.new do
+ extend super_m
+ module_function
+ def foo
+ ["m", super]
+ end
+ end
+
+ m.foo.should == ["m", "super_m"]
+ end
+
+ context "methods created with define_method" do
+ context "passed a block" do
+ it "creates duplicates of the given instance methods" do
+ m = Module.new do
+ define_method :test1 do; end
+ module_function :test1
+ end
+
+ m.respond_to?(:test1).should == true
+ end
+ end
+
+ context "passed a method" do
+ it "creates duplicates of the given instance methods" do
+ module_with_method = Module.new do
+ def test1; end
+ end
+
+ c = Class.new do
+ extend module_with_method
+ end
+
+ m = Module.new do
+ define_method :test2, c.method(:test1)
+ module_function :test2
+ end
+
+ m.respond_to?(:test2).should == true
+ end
+ end
+
+ context "passed an unbound method" do
+ it "creates duplicates of the given instance methods" do
+ module_with_method = Module.new do
+ def test1; end
+ end
+
+ m = Module.new do
+ define_method :test2, module_with_method.instance_method(:test1)
+ module_function :test2
+ end
+
+ m.respond_to?(:test2).should == true
+ end
+ end
+ end
+end
+
+describe "Module#module_function as a toggle (no arguments) in a Module body" do
+ it "makes any subsequently defined methods module functions with the normal semantics" do
+ m = Module.new do
+ module_function
+ def test1() end
+ def test2() end
+ end
+
+ m.respond_to?(:test1).should == true
+ m.respond_to?(:test2).should == true
+ end
+
+ it "returns nil" do
+ Module.new do
+ module_function.should.equal?(nil)
+ end
+ end
+
+ it "stops creating module functions if the body encounters another toggle " \
+ "like public/protected/private without arguments" do
+ m = Module.new do
+ module_function
+ def test1() end
+ def test2() end
+ public
+ def test3() end
+ end
+
+ m.respond_to?(:test1).should == true
+ m.respond_to?(:test2).should == true
+ m.respond_to?(:test3).should == false
+ end
+
+ it "does not stop creating module functions if the body encounters " \
+ "public/protected/private WITH arguments" do
+ m = Module.new do
+ def foo() end
+ module_function
+ def test1() end
+ def test2() end
+ public :foo
+ def test3() end
+ end
+
+ m.respond_to?(:test1).should == true
+ m.respond_to?(:test2).should == true
+ m.respond_to?(:test3).should == true
+ end
+
+ it "does not affect module_evaled method definitions also if outside the eval itself" do
+ m = Module.new do
+ module_function
+ module_eval { def test1() end }
+ module_eval " def test2() end "
+ end
+
+ m.respond_to?(:test1).should == false
+ m.respond_to?(:test2).should == false
+ end
+
+ it "has no effect if inside a module_eval if the definitions are outside of it" do
+ m = Module.new do
+ module_eval { module_function }
+ def test1() end
+ def test2() end
+ end
+
+ m.respond_to?(:test1).should == false
+ m.respond_to?(:test2).should == false
+ end
+
+ it "functions normally if both toggle and definitions inside a module_eval" do
+ m = Module.new do
+ module_eval do
+ module_function
+ def test1() end
+ def test2() end
+ end
+ end
+
+ m.respond_to?(:test1).should == true
+ m.respond_to?(:test2).should == true
+ end
+
+ it "affects eval'ed method definitions also even when outside the eval itself" do
+ m = Module.new do
+ module_function
+ eval "def test1() end"
+ end
+
+ m.respond_to?(:test1).should == true
+ end
+
+ it "doesn't affect definitions when inside an eval even if the definitions are outside of it" do
+ m = Module.new do
+ eval "module_function"
+ def test1() end
+ end
+
+ m.respond_to?(:test1).should == false
+ end
+
+ it "functions normally if both toggle and definitions inside a eval" do
+ m = Module.new do
+ eval <<-CODE
+ module_function
+
+ def test1() end
+ def test2() end
+ CODE
+ end
+
+ m.respond_to?(:test1).should == true
+ m.respond_to?(:test2).should == true
+ end
+
+ context "methods are defined with define_method" do
+ context "passed a block" do
+ it "makes any subsequently defined methods module functions with the normal semantics" do
+ m = Module.new do
+ module_function
+ define_method :test1 do; end
+ end
+
+ m.respond_to?(:test1).should == true
+ end
+ end
+
+ context "passed a method" do
+ it "makes any subsequently defined methods module functions with the normal semantics" do
+ module_with_method = Module.new do
+ def test1; end
+ end
+
+ c = Class.new do
+ extend module_with_method
+ end
+
+ m = Module.new do
+ module_function
+ define_method :test2, c.method(:test1)
+ end
+
+ m.respond_to?(:test2).should == true
+ end
+ end
+
+ context "passed an unbound method" do
+ it "makes any subsequently defined methods module functions with the normal semantics" do
+ module_with_method = Module.new do
+ def test1; end
+ end
+
+ m = Module.new do
+ module_function
+ define_method :test2, module_with_method.instance_method(:test1)
+ end
+
+ m.respond_to?(:test2).should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/module/name_spec.rb b/spec/ruby/core/module/name_spec.rb
new file mode 100644
index 0000000000..332c08d782
--- /dev/null
+++ b/spec/ruby/core/module/name_spec.rb
@@ -0,0 +1,205 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/module'
+
+describe "Module#name" do
+ it "is nil for an anonymous module" do
+ Module.new.name.should == nil
+ end
+
+ it "is not nil when assigned to a constant in an anonymous module" do
+ m = Module.new
+ m::N = Module.new
+ m::N.name.should.end_with? '::N'
+ end
+
+ it "is not nil for a nested module created with the module keyword" do
+ m = Module.new
+ module m::N; end
+ m::N.name.should =~ /\A#<Module:0x[0-9a-f]+>::N\z/
+ end
+
+ it "returns nil for a singleton class" do
+ Module.new.singleton_class.name.should == nil
+ String.singleton_class.name.should == nil
+ Object.new.singleton_class.name.should == nil
+ end
+
+ it "changes when the module is reachable through a constant path" do
+ m = Module.new
+ module m::N; end
+ m::N.name.should =~ /\A#<Module:0x\h+>::N\z/
+ ModuleSpecs::Anonymous::WasAnnon = m::N
+ m::N.name.should == "ModuleSpecs::Anonymous::WasAnnon"
+ ensure
+ ModuleSpecs::Anonymous.send(:remove_const, :WasAnnon)
+ end
+
+ it "may be the repeated in different module objects" do
+ m = Module.new
+ n = Module.new
+
+ suppress_warning do
+ ModuleSpecs::Anonymous::SameName = m
+ ModuleSpecs::Anonymous::SameName = n
+ end
+
+ m.name.should == "ModuleSpecs::Anonymous::SameName"
+ n.name.should == "ModuleSpecs::Anonymous::SameName"
+ end
+
+ it "is set after it is removed from a constant" do
+ module ModuleSpecs
+ module ModuleToRemove
+ end
+
+ mod = ModuleToRemove
+ remove_const(:ModuleToRemove)
+ mod.name.should == "ModuleSpecs::ModuleToRemove"
+ end
+ end
+
+ it "is set after it is removed from a constant under an anonymous module" do
+ m = Module.new
+ module m::Child; end
+ child = m::Child
+ m.send(:remove_const, :Child)
+ child.name.should =~ /\A#<Module:0x\h+>::Child\z/
+ end
+
+ it "is set when opened with the module keyword" do
+ ModuleSpecs.name.should == "ModuleSpecs"
+ end
+
+ it "is set when a nested module is opened with the module keyword" do
+ ModuleSpecs::Anonymous.name.should == "ModuleSpecs::Anonymous"
+ end
+
+ it "is set when assigning to a constant (constant path matches outer module name)" do
+ m = Module.new
+ ModuleSpecs::Anonymous::A = m
+ m.name.should == "ModuleSpecs::Anonymous::A"
+ ensure
+ ModuleSpecs::Anonymous.send(:remove_const, :A)
+ end
+
+ it "is set when assigning to a constant (constant path does not match outer module name)" do
+ m = Module.new
+ ModuleSpecs::Anonymous::SameChild::A = m
+ m.name.should == "ModuleSpecs::Anonymous::Child::A"
+ ensure
+ ModuleSpecs::Anonymous::SameChild.send(:remove_const, :A)
+ end
+
+ it "is not modified when assigning to a new constant after it has been accessed" do
+ m = Module.new
+ ModuleSpecs::Anonymous::B = m
+ m.name.should == "ModuleSpecs::Anonymous::B"
+ ModuleSpecs::Anonymous::C = m
+ m.name.should == "ModuleSpecs::Anonymous::B"
+ ensure
+ ModuleSpecs::Anonymous.send(:remove_const, :B)
+ ModuleSpecs::Anonymous.send(:remove_const, :C)
+ end
+
+ it "is not modified when assigned to a different anonymous module" do
+ m = Module.new
+ module m::M; end
+ first_name = m::M.name.dup
+ module m::N; end
+ m::N::F = m::M
+ m::M.name.should == first_name
+ end
+
+ # http://bugs.ruby-lang.org/issues/6067
+ it "is set with a conditional assignment to a nested constant" do
+ eval("ModuleSpecs::Anonymous::F ||= Module.new")
+ ModuleSpecs::Anonymous::F.name.should == "ModuleSpecs::Anonymous::F"
+ end
+
+ it "is set with a conditional assignment to a constant" do
+ module ModuleSpecs::Anonymous
+ D ||= Module.new
+ end
+ ModuleSpecs::Anonymous::D.name.should == "ModuleSpecs::Anonymous::D"
+ end
+
+ # http://redmine.ruby-lang.org/issues/show/1833
+ it "preserves the encoding in which the class was defined" do
+ require fixture(__FILE__, "name")
+ ModuleSpecs::NameEncoding.new.name.encoding.should == Encoding::UTF_8
+ end
+
+ it "is set when the anonymous outer module name is set (module in one single constant)" do
+ m = Module.new
+ m::N = Module.new
+ ModuleSpecs::Anonymous::E = m
+ m::N.name.should == "ModuleSpecs::Anonymous::E::N"
+ ensure
+ ModuleSpecs::Anonymous.send(:remove_const, :E)
+ end
+
+ # https://bugs.ruby-lang.org/issues/19681
+ it "is set when the anonymous outer module name is set (module in several constants)" do
+ m = Module.new
+ m::N = Module.new
+ m::O = m::N
+ ModuleSpecs::Anonymous::StoredInMultiplePlaces = m
+ valid_names = [
+ "ModuleSpecs::Anonymous::StoredInMultiplePlaces::N",
+ "ModuleSpecs::Anonymous::StoredInMultiplePlaces::O"
+ ]
+ valid_names.should.include?(m::N.name) # You get one of the two, but you don't know which one.
+ ensure
+ ModuleSpecs::Anonymous.send(:remove_const, :StoredInMultiplePlaces)
+ end
+
+ it "is set in #const_added callback when a module defined in the top-level scope" do
+ ruby_exe(<<~RUBY, args: "2>&1").chomp.should == "TEST1\nTEST2"
+ class Module
+ def const_added(name)
+ puts const_get(name).name
+ end
+ end
+
+ # module with name
+ module TEST1
+ end
+
+ # anonymous module
+ TEST2 = Module.new
+ RUBY
+ end
+
+ it "is set in #const_added callback for a nested module when an outer module defined in the top-level scope" do
+ ScratchPad.record []
+
+ ModuleSpecs::NameSpecs::NamedModule = Module.new do
+ def self.const_added(name)
+ ScratchPad << const_get(name).name
+ end
+
+ module self::A
+ def self.const_added(name)
+ ScratchPad << const_get(name).name
+ end
+
+ module self::B
+ end
+ end
+ end
+
+ ScratchPad.recorded.should.one?(/#<Module.+>::A$/)
+ ScratchPad.recorded.should.one?(/#<Module.+>::A::B$/)
+ ModuleSpecs::NameSpecs.send :remove_const, :NamedModule
+ end
+
+ it "returns a frozen String" do
+ ModuleSpecs.name.should.frozen?
+ end
+
+ it "always returns the same String for a given Module" do
+ s1 = ModuleSpecs.name
+ s2 = ModuleSpecs.name
+ s1.should.equal?(s2)
+ end
+end
diff --git a/spec/ruby/core/module/nesting_spec.rb b/spec/ruby/core/module/nesting_spec.rb
new file mode 100644
index 0000000000..d0611b3efe
--- /dev/null
+++ b/spec/ruby/core/module/nesting_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module::Nesting" do
+
+ it "returns the list of Modules nested at the point of call" do
+ ModuleSpecs::Nesting[:root_level].should == []
+ ModuleSpecs::Nesting[:first_level].should == [ModuleSpecs]
+ ModuleSpecs::Nesting[:basic].should == [ModuleSpecs::Nesting, ModuleSpecs]
+ ModuleSpecs::Nesting[:open_first_level].should ==
+ [ModuleSpecs, ModuleSpecs::Nesting, ModuleSpecs]
+ ModuleSpecs::Nesting[:open_meta].should ==
+ [ModuleSpecs::Nesting.meta, ModuleSpecs::Nesting, ModuleSpecs]
+ ModuleSpecs::Nesting[:nest_class].should ==
+ [ModuleSpecs::Nesting::NestedClass, ModuleSpecs::Nesting, ModuleSpecs]
+ end
+
+ it "returns the nesting for module/class declaring the called method" do
+ ModuleSpecs::Nesting.called_from_module_method.should ==
+ [ModuleSpecs::Nesting, ModuleSpecs]
+ ModuleSpecs::Nesting::NestedClass.called_from_class_method.should ==
+ [ModuleSpecs::Nesting::NestedClass, ModuleSpecs::Nesting, ModuleSpecs]
+ ModuleSpecs::Nesting::NestedClass.new.called_from_inst_method.should ==
+ [ModuleSpecs::Nesting::NestedClass, ModuleSpecs::Nesting, ModuleSpecs]
+ end
+
+end
+
+describe "Module.nesting" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/module/new_spec.rb b/spec/ruby/core/module/new_spec.rb
new file mode 100644
index 0000000000..ec7a0cfa77
--- /dev/null
+++ b/spec/ruby/core/module/new_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module.new" do
+ it "creates a new anonymous Module" do
+ Module.new.is_a?(Module).should == true
+ end
+
+ it "creates a module without a name" do
+ Module.new.name.should == nil
+ end
+
+ it "creates a new Module and passes it to the provided block" do
+ test_mod = nil
+ m = Module.new do |mod|
+ mod.should_not == nil
+ self.should == mod
+ test_mod = mod
+ mod.is_a?(Module).should == true
+ Object.new # trying to return something
+ end
+ test_mod.should == m
+ end
+
+ it "evaluates a passed block in the context of the module" do
+ fred = Module.new do
+ def hello() "hello" end
+ def bye() "bye" end
+ end
+
+ (o = mock('x')).extend(fred)
+ o.hello.should == "hello"
+ o.bye.should == "bye"
+ end
+end
diff --git a/spec/ruby/core/module/prepend_features_spec.rb b/spec/ruby/core/module/prepend_features_spec.rb
new file mode 100644
index 0000000000..3a50f2c2a4
--- /dev/null
+++ b/spec/ruby/core/module/prepend_features_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#prepend_features" do
+ it "is a private method" do
+ Module.private_instance_methods(false).should.include?(:prepend_features)
+ end
+
+ it "gets called when self is included in another module/class" do
+ ScratchPad.record []
+
+ m = Module.new do
+ def self.prepend_features(mod)
+ ScratchPad << mod
+ end
+ end
+
+ c = Class.new do
+ prepend m
+ end
+
+ ScratchPad.recorded.should == [c]
+ end
+
+ it "raises an ArgumentError on a cyclic prepend" do
+ -> {
+ ModuleSpecs::CyclicPrepend.send(:prepend_features, ModuleSpecs::CyclicPrepend)
+ }.should.raise(ArgumentError)
+ end
+
+ it "clears caches of the given module" do
+ parent = Class.new do
+ def bar; :bar; end
+ end
+
+ child = Class.new(parent) do
+ def foo; :foo; end
+ def bar; super; end
+ end
+
+ mod = Module.new do
+ def foo; :fooo; end
+ end
+
+ child.new.foo
+ child.new.bar
+
+ child.prepend(mod)
+
+ child.new.bar.should == :bar
+ end
+
+ describe "on Class" do
+ it "is undefined" do
+ Class.private_instance_methods(true).should_not.include?(:prepend_features)
+ end
+
+ it "raises a TypeError if calling after rebinded to Class" do
+ -> {
+ Module.instance_method(:prepend_features).bind(Class.new).call Module.new
+ }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/prepend_spec.rb b/spec/ruby/core/module/prepend_spec.rb
new file mode 100644
index 0000000000..f7887e6d6a
--- /dev/null
+++ b/spec/ruby/core/module/prepend_spec.rb
@@ -0,0 +1,823 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#prepend" do
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:prepend)
+ end
+
+ it "does not affect the superclass" do
+ Class.new { prepend Module.new }.superclass.should == Object
+ end
+
+ it "calls #prepend_features(self) in reversed order on each module" do
+ ScratchPad.record []
+
+ m = Module.new do
+ def self.prepend_features(mod)
+ ScratchPad << [ self, mod ]
+ end
+ end
+
+ m2 = Module.new do
+ def self.prepend_features(mod)
+ ScratchPad << [ self, mod ]
+ end
+ end
+
+ m3 = Module.new do
+ def self.prepend_features(mod)
+ ScratchPad << [ self, mod ]
+ end
+ end
+
+ c = Class.new { prepend(m, m2, m3) }
+
+ ScratchPad.recorded.should == [ [ m3, c], [ m2, c ], [ m, c ] ]
+ end
+
+ it "updates the method when a module is prepended" do
+ m_module = Module.new do
+ def foo
+ "m"
+ end
+ end
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+ a_class.class_eval do
+ prepend m_module
+ end
+ foo.call.should == 'm'
+ end
+
+ it "updates the method when a prepended module is updated" do
+ m_module = Module.new
+ a_class = Class.new do
+ prepend m_module
+ def foo
+ 'a'
+ end
+ end
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+ m_module.module_eval do
+ def foo
+ "m"
+ end
+ end
+ foo.call.should == 'm'
+ end
+
+ it "updates the optimized method when a prepended module is updated" do
+ out = ruby_exe(<<~RUBY)
+ module M; end
+ class Integer
+ prepend M
+ end
+ l = -> { 1 + 2 }
+ p l.call
+ M.module_eval do
+ def +(o)
+ $called = true
+ super(o)
+ end
+ end
+ p l.call
+ p $called
+ RUBY
+ out.should == "3\n3\ntrue\n"
+ end
+
+ it "updates the method when there is a base included method and the prepended module overrides it" do
+ base_module = Module.new do
+ def foo
+ 'a'
+ end
+ end
+ a_class = Class.new do
+ include base_module
+ end
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+
+ m_module = Module.new do
+ def foo
+ "m"
+ end
+ end
+ a_class.prepend m_module
+ foo.call.should == 'm'
+ end
+
+ it "updates the method when there is a base included method and the prepended module is later updated" do
+ base_module = Module.new do
+ def foo
+ 'a'
+ end
+ end
+ a_class = Class.new do
+ include base_module
+ end
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+
+ m_module = Module.new
+ a_class.prepend m_module
+ foo.call.should == 'a'
+
+ m_module.module_eval do
+ def foo
+ "m"
+ end
+ end
+ foo.call.should == 'm'
+ end
+
+ it "updates the method when a module prepended after a call is later updated" do
+ m_module = Module.new
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+
+ a_class.prepend m_module
+ foo.call.should == 'a'
+
+ m_module.module_eval do
+ def foo
+ "m"
+ end
+ end
+ foo.call.should == 'm'
+ end
+
+ it "updates the method when a module is prepended after another and the method is defined later on that module" do
+ m_module = Module.new do
+ def foo
+ 'a'
+ end
+ end
+ a_class = Class.new
+ a_class.prepend m_module
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+
+ n_module = Module.new
+ a_class.prepend n_module
+ foo.call.should == 'a'
+
+ n_module.module_eval do
+ def foo
+ "n"
+ end
+ end
+ foo.call.should == 'n'
+ end
+
+ it "updates the method when a module is included in a prepended module and the method is defined later" do
+ a_class = Class.new
+ base_module = Module.new do
+ def foo
+ 'a'
+ end
+ end
+ a_class.prepend base_module
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+
+ m_module = Module.new
+ n_module = Module.new
+ m_module.include n_module
+ a_class.prepend m_module
+
+ n_module.module_eval do
+ def foo
+ "n"
+ end
+ end
+ foo.call.should == 'n'
+ end
+
+ it "updates the method when a new module with an included module is prepended" do
+ a_class = Class.new do
+ def foo
+ 'a'
+ end
+ end
+
+ n_module = Module.new do
+ def foo
+ 'n'
+ end
+ end
+
+ m_module = Module.new do
+ include n_module
+ end
+
+ a = a_class.new
+ foo = -> { a.foo }
+
+ foo.call.should == 'a'
+
+ a_class.class_eval do
+ prepend m_module
+ end
+
+ foo.call.should == 'n'
+ end
+
+ it "updates the constant when a module is prepended" do
+ module ModuleSpecs::ConstUpdatePrepended
+ module M
+ FOO = 'm'
+ end
+ module A
+ FOO = 'a'
+ end
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+
+ B.foo.should == 'a'
+ B.prepend M
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatePrepended)
+ end
+
+ it "updates the constant when a prepended module is updated" do
+ module ModuleSpecs::ConstPrependedUpdated
+ module M
+ end
+ module A
+ FOO = 'a'
+ end
+ module B
+ include A
+ prepend M
+ def self.foo
+ FOO
+ end
+ end
+ B.foo.should == 'a'
+ M.const_set(:FOO, 'm')
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstPrependedUpdated)
+ end
+
+ it "updates the constant when there is a base included constant and the prepended module overrides it" do
+ module ModuleSpecs::ConstIncludedPrependedOverride
+ module Base
+ FOO = 'a'
+ end
+ module A
+ include Base
+ def self.foo
+ FOO
+ end
+ end
+ A.foo.should == 'a'
+
+ module M
+ FOO = 'm'
+ end
+ A.prepend M
+ A.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstIncludedPrependedOverride)
+ end
+
+ it "updates the constant when there is a base included constant and the prepended module is later updated" do
+ module ModuleSpecs::ConstIncludedPrependedLaterUpdated
+ module Base
+ FOO = 'a'
+ end
+ module A
+ include Base
+ def self.foo
+ FOO
+ end
+ end
+ A.foo.should == 'a'
+
+ module M
+ end
+ A.prepend M
+ A.foo.should == 'a'
+
+ M.const_set(:FOO, 'm')
+ A.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstIncludedPrependedLaterUpdated)
+ end
+
+ it "updates the constant when a module prepended after a constant is later updated" do
+ module ModuleSpecs::ConstUpdatedPrependedAfterLaterUpdated
+ module M
+ end
+ module A
+ FOO = 'a'
+ end
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+ B.foo.should == 'a'
+
+ B.prepend M
+ B.foo.should == 'a'
+
+ M.const_set(:FOO, 'm')
+ B.foo.should == 'm'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedPrependedAfterLaterUpdated)
+ end
+
+ it "updates the constant when a module is prepended after another and the constant is defined later on that module" do
+ module ModuleSpecs::ConstUpdatedPrependedAfterConstDefined
+ module M
+ FOO = 'm'
+ end
+ module A
+ prepend M
+ def self.foo
+ FOO
+ end
+ end
+
+ A.foo.should == 'm'
+
+ module N
+ end
+ A.prepend N
+ A.foo.should == 'm'
+
+ N.const_set(:FOO, 'n')
+ A.foo.should == 'n'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedPrependedAfterConstDefined)
+ end
+
+ it "updates the constant when a module is included in a prepended module and the constant is defined later" do
+ module ModuleSpecs::ConstUpdatedIncludedInPrependedConstDefinedLater
+ module A
+ def self.foo
+ FOO
+ end
+ end
+ module Base
+ FOO = 'a'
+ end
+
+ A.prepend Base
+ A.foo.should == 'a'
+
+ module N
+ end
+ module M
+ include N
+ end
+
+ A.prepend M
+
+ N.const_set(:FOO, 'n')
+ A.foo.should == 'n'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedIncludedInPrependedConstDefinedLater)
+ end
+
+ it "updates the constant when a new module with an included module is prepended" do
+ module ModuleSpecs::ConstUpdatedNewModuleIncludedPrepended
+ module A
+ FOO = 'a'
+ end
+ module B
+ include A
+ def self.foo
+ FOO
+ end
+ end
+ module N
+ FOO = 'n'
+ end
+
+ module M
+ include N
+ end
+
+ B.foo.should == 'a'
+
+ B.prepend M
+ B.foo.should == 'n'
+ end
+ ensure
+ ModuleSpecs.send(:remove_const, :ConstUpdatedNewModuleIncludedPrepended)
+ end
+
+ it "raises a TypeError when the argument is not a Module" do
+ -> { ModuleSpecs::Basic.prepend(Class.new) }.should.raise(TypeError)
+ end
+
+ it "does not raise a TypeError when the argument is an instance of a subclass of Module" do
+ class ModuleSpecs::SubclassSpec::AClass
+ end
+ -> { ModuleSpecs::SubclassSpec::AClass.prepend(ModuleSpecs::Subclass.new) }.should_not.raise(TypeError)
+ ensure
+ ModuleSpecs::SubclassSpec.send(:remove_const, :AClass)
+ end
+
+ it "raises a TypeError when the argument is a refinement" do
+ refinement = nil
+
+ Module.new do
+ refine String do
+ refinement = self
+ end
+ end
+
+ -> { ModuleSpecs::Basic.prepend(refinement) }.should.raise(TypeError, "Cannot prepend refinement")
+ end
+
+ it "imports constants" do
+ m1 = Module.new
+ m1::MY_CONSTANT = 1
+ m2 = Module.new { prepend(m1) }
+ m2.constants.should.include?(:MY_CONSTANT)
+ end
+
+ it "imports instance methods" do
+ Module.new { prepend ModuleSpecs::A }.instance_methods.should.include?(:ma)
+ end
+
+ it "does not import methods to modules and classes" do
+ Module.new { prepend ModuleSpecs::A }.methods.should_not.include?(:ma)
+ end
+
+ it "allows wrapping methods" do
+ m = Module.new { def calc(x) super + 3 end }
+ c = Class.new { def calc(x) x*2 end }
+ c.prepend(m)
+ c.new.calc(1).should == 5
+ end
+
+ it "also prepends included modules" do
+ a = Module.new { def calc(x) x end }
+ b = Module.new { include a }
+ c = Class.new { prepend b }
+ c.new.calc(1).should == 1
+ end
+
+ it "prepends multiple modules in the right order" do
+ m1 = Module.new { def chain; super << :m1; end }
+ m2 = Module.new { def chain; super << :m2; end; prepend(m1) }
+ c = Class.new { def chain; [:c]; end; prepend(m2) }
+ c.new.chain.should == [:c, :m2, :m1]
+ end
+
+ it "includes prepended modules in ancestors" do
+ m = Module.new
+ Class.new { prepend(m) }.ancestors.should.include?(m)
+ end
+
+ it "reports the prepended module as the method owner" do
+ m = Module.new { def meth; end }
+ c = Class.new { def meth; end; prepend(m) }
+ c.new.method(:meth).owner.should == m
+ end
+
+ it "reports the prepended module as the unbound method owner" do
+ m = Module.new { def meth; end }
+ c = Class.new { def meth; end; prepend(m) }
+ c.instance_method(:meth).owner.should == m
+ c.public_instance_method(:meth).owner.should == m
+ end
+
+ it "causes the prepended module's method to be aliased by alias_method" do
+ m = Module.new { def meth; :m end }
+ c = Class.new { def meth; :c end; prepend(m); alias_method :alias, :meth }
+ c.new.alias.should == :m
+ end
+
+ it "reports the class for the owner of an aliased method on the class" do
+ m = Module.new
+ c = Class.new { prepend(m); def meth; :c end; alias_method :alias, :meth }
+ c.instance_method(:alias).owner.should == c
+ end
+
+ it "reports the class for the owner of a method aliased from the prepended module" do
+ m = Module.new { def meth; :m end }
+ c = Class.new { prepend(m); alias_method :alias, :meth }
+ c.instance_method(:alias).owner.should == c
+ end
+
+ it "sees an instance of a prepended class as kind of the prepended module" do
+ m = Module.new
+ c = Class.new { prepend(m) }
+ c.new.should.is_a?(m)
+ end
+
+ it "keeps the module in the chain when dupping the class" do
+ m = Module.new
+ c = Class.new { prepend(m) }
+ c.dup.new.should.is_a?(m)
+ end
+
+ it "uses only new module when dupping the module" do
+ m1 = Module.new { def calc(x) x end }
+ m2 = Module.new { prepend(m1) }
+ c1 = Class.new { prepend(m2) }
+ m2dup = m2.dup
+ m2dup.ancestors.should == [m1,m2dup]
+ c2 = Class.new { prepend(m2dup) }
+ c1.ancestors[0,3].should == [m1,m2,c1]
+ c1.new.should.is_a?(m1)
+ c2.ancestors[0,3].should == [m1,m2dup,c2]
+ c2.new.should.is_a?(m1)
+ end
+
+ it "depends on prepend_features to add the module" do
+ m = Module.new { def self.prepend_features(mod) end }
+ Class.new { prepend(m) }.ancestors.should_not.include?(m)
+ end
+
+ it "adds the module in the subclass chains" do
+ parent = Class.new { def chain; [:parent]; end }
+ child = Class.new(parent) { def chain; super << :child; end }
+ mod = Module.new { def chain; super << :mod; end }
+ parent.prepend(mod)
+ parent.ancestors[0,2].should == [mod, parent]
+ child.ancestors[0,3].should == [child, mod, parent]
+
+ parent.new.chain.should == [:parent, :mod]
+ child.new.chain.should == [:parent, :mod, :child]
+ end
+
+ it "inserts a later prepended module into the chain" do
+ m1 = Module.new { def chain; super << :m1; end }
+ m2 = Module.new { def chain; super << :m2; end }
+ c1 = Class.new { def chain; [:c1]; end; prepend m1 }
+ c2 = Class.new(c1) { def chain; super << :c2; end }
+ c2.new.chain.should == [:c1, :m1, :c2]
+ c1.prepend(m2)
+ c2.new.chain.should == [:c1, :m1, :m2, :c2]
+ end
+
+ it "works with subclasses" do
+ m = Module.new do
+ def chain
+ super << :module
+ end
+ end
+
+ c = Class.new do
+ prepend m
+ def chain
+ [:class]
+ end
+ end
+
+ s = Class.new(c) do
+ def chain
+ super << :subclass
+ end
+ end
+
+ s.new.chain.should == [:class, :module, :subclass]
+ end
+
+ it "throws a NoMethodError when there is no more superclass" do
+ m = Module.new do
+ def chain
+ super << :module
+ end
+ end
+
+ c = Class.new do
+ prepend m
+ def chain
+ super << :class
+ end
+ end
+ -> { c.new.chain }.should.raise(NoMethodError)
+ end
+
+ it "calls prepended after prepend_features" do
+ ScratchPad.record []
+
+ m = Module.new do
+ def self.prepend_features(klass)
+ ScratchPad << [:prepend_features, klass]
+ end
+ def self.prepended(klass)
+ ScratchPad << [:prepended, klass]
+ end
+ end
+
+ c = Class.new { prepend(m) }
+ ScratchPad.recorded.should == [[:prepend_features, c], [:prepended, c]]
+ end
+
+ it "prepends a module if it is included in a super class" do
+ module ModuleSpecs::M3
+ module M; end
+ class A; include M; end
+ class B < A; prepend M; end
+
+ all = [A, B, M]
+
+ (B.ancestors.filter { |a| all.include?(a) }).should == [M, B, A, M]
+ end
+ end
+
+ it "detects cyclic prepends" do
+ -> {
+ module ModuleSpecs::P
+ prepend ModuleSpecs::P
+ end
+ }.should.raise(ArgumentError)
+ end
+
+ it "doesn't accept no-arguments" do
+ -> {
+ Module.new do
+ prepend
+ end
+ }.should.raise(ArgumentError)
+ end
+
+ it "returns the class it's included into" do
+ m = Module.new
+ r = nil
+ c = Class.new { r = prepend m }
+ r.should == c
+ end
+
+ it "clears any caches" do
+ module ModuleSpecs::M3
+ module PM1
+ def foo
+ :m1
+ end
+ end
+
+ module PM2
+ def foo
+ :m2
+ end
+ end
+
+ klass = Class.new do
+ prepend PM1
+
+ def get
+ foo
+ end
+ end
+
+ o = klass.new
+ o.get.should == :m1
+
+ klass.class_eval do
+ prepend PM2
+ end
+
+ o.get.should == :m2
+ end
+ end
+
+ it "supports super when the module is prepended into a singleton class" do
+ ScratchPad.record []
+
+ mod = Module.new do
+ def self.inherited(base)
+ super
+ end
+ end
+
+ module_with_singleton_class_prepend = Module.new do
+ singleton_class.prepend(mod)
+ end
+
+ klass = Class.new(ModuleSpecs::RecordIncludedModules) do
+ include module_with_singleton_class_prepend
+ end
+
+ ScratchPad.recorded.should == klass
+ end
+
+ it "supports super when the module is prepended into a singleton class with a class super" do
+ ScratchPad.record []
+
+ base_class = Class.new(ModuleSpecs::RecordIncludedModules) do
+ def self.inherited(base)
+ super
+ end
+ end
+
+ prepended_module = Module.new
+ base_class.singleton_class.prepend(prepended_module)
+
+ child_class = Class.new(base_class)
+ ScratchPad.recorded.should == child_class
+ end
+
+ it "does not interfere with a define_method super in the original class" do
+ base_class = Class.new do
+ def foo(ary)
+ ary << 1
+ end
+ end
+
+ child_class = Class.new(base_class) do
+ define_method :foo do |ary|
+ ary << 2
+ super(ary)
+ end
+ end
+
+ prep_mod = Module.new do
+ def foo(ary)
+ ary << 3
+ super(ary)
+ end
+ end
+
+ child_class.prepend(prep_mod)
+
+ ary = []
+ child_class.new.foo(ary)
+ ary.should == [3, 2, 1]
+ end
+
+ it "does not prepend a second copy if the module already indirectly exists in the hierarchy" do
+ mod = Module.new do; end
+ submod = Module.new do; end
+ klass = Class.new do; end
+ klass.include(mod)
+ mod.prepend(submod)
+ klass.include(mod)
+
+ klass.ancestors.take(4).should == [klass, submod, mod, Object]
+ end
+
+ # https://bugs.ruby-lang.org/issues/17423
+ describe "when module already exists in ancestor chain" do
+ it "modifies the ancestor chain" do
+ m = Module.new do; end
+ a = Module.new do; end
+ b = Class.new do; end
+
+ b.include(a)
+ a.prepend(m)
+ b.ancestors.take(4).should == [b, m, a, Object]
+
+ b.prepend(m)
+ b.ancestors.take(5).should == [m, b, m, a, Object]
+ end
+ end
+
+ describe "called on a module" do
+ describe "included into a class"
+ it "does not obscure the module's methods from reflective access" do
+ mod = Module.new do
+ def foo; end
+ end
+ cls = Class.new do
+ include mod
+ end
+ pre = Module.new
+ mod.prepend pre
+
+ cls.instance_methods.should.include?(:foo)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/prepended_spec.rb b/spec/ruby/core/module/prepended_spec.rb
new file mode 100644
index 0000000000..ccd450d668
--- /dev/null
+++ b/spec/ruby/core/module/prepended_spec.rb
@@ -0,0 +1,25 @@
+# -*- encoding: us-ascii -*-
+
+require_relative '../../spec_helper'
+
+describe "Module#prepended" do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "is a private method" do
+ Module.private_instance_methods(false).should.include?(:prepended)
+ end
+
+ it "is invoked when self is prepended to another module or class" do
+ m = Module.new do
+ def self.prepended(o)
+ ScratchPad.record o
+ end
+ end
+
+ c = Class.new { prepend m }
+
+ ScratchPad.recorded.should == c
+ end
+end
diff --git a/spec/ruby/core/module/private_class_method_spec.rb b/spec/ruby/core/module/private_class_method_spec.rb
new file mode 100644
index 0000000000..7ce07f1600
--- /dev/null
+++ b/spec/ruby/core/module/private_class_method_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#private_class_method" do
+ before :each do
+ # This is not in classes.rb because after marking a class method private it
+ # will stay private.
+ class << ModuleSpecs::Parent
+ public
+ def private_method_1; end
+ def private_method_2; end
+ end
+ end
+
+ after :each do
+ class << ModuleSpecs::Parent
+ remove_method :private_method_1
+ remove_method :private_method_2
+ end
+ end
+
+ it "makes an existing class method private" do
+ ModuleSpecs::Parent.private_method_1.should == nil
+ ModuleSpecs::Parent.private_class_method :private_method_1
+ -> { ModuleSpecs::Parent.private_method_1 }.should.raise(NoMethodError)
+
+ # Technically above we're testing the Singleton classes, class method(right?).
+ # Try a "real" class method set private.
+ -> { ModuleSpecs::Parent.private_method }.should.raise(NoMethodError)
+ end
+
+ it "makes an existing class method private up the inheritance tree" do
+ ModuleSpecs::Child.public_class_method :private_method_1
+ ModuleSpecs::Child.private_method_1.should == nil
+ ModuleSpecs::Child.private_class_method :private_method_1
+
+ -> { ModuleSpecs::Child.private_method_1 }.should.raise(NoMethodError)
+ -> { ModuleSpecs::Child.private_method }.should.raise(NoMethodError)
+ end
+
+ it "accepts more than one method at a time" do
+ ModuleSpecs::Parent.private_method_1.should == nil
+ ModuleSpecs::Parent.private_method_2.should == nil
+
+ ModuleSpecs::Child.private_class_method :private_method_1, :private_method_2
+
+ -> { ModuleSpecs::Child.private_method_1 }.should.raise(NoMethodError)
+ -> { ModuleSpecs::Child.private_method_2 }.should.raise(NoMethodError)
+ end
+
+ it "raises a NameError if class method doesn't exist" do
+ -> do
+ ModuleSpecs.private_class_method :no_method_here
+ end.should.raise(NameError)
+ end
+
+ it "makes a class method private" do
+ c = Class.new do
+ def self.foo() "foo" end
+ private_class_method :foo
+ end
+ -> { c.foo }.should.raise(NoMethodError)
+ end
+
+ it "raises a NameError when the given name is not a method" do
+ -> do
+ Class.new do
+ private_class_method :foo
+ end
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError when the given name is an instance method" do
+ -> do
+ Class.new do
+ def foo() "foo" end
+ private_class_method :foo
+ end
+ end.should.raise(NameError)
+ end
+
+ context "when single argument is passed and is an array" do
+ it "sets the visibility of the given methods to private" do
+ c = Class.new do
+ def self.foo() "foo" end
+ private_class_method [:foo]
+ end
+ -> { c.foo }.should.raise(NoMethodError)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/private_constant_spec.rb b/spec/ruby/core/module/private_constant_spec.rb
new file mode 100644
index 0000000000..ca0df48c0b
--- /dev/null
+++ b/spec/ruby/core/module/private_constant_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "Module#private_constant" do
+ it "can only be passed constant names defined in the target (self) module" do
+ cls1 = Class.new
+ cls1.const_set :Foo, true
+ cls2 = Class.new(cls1)
+
+ -> do
+ cls2.send :private_constant, :Foo
+ end.should.raise(NameError)
+ end
+
+ it "accepts strings as constant names" do
+ cls = Class.new
+ cls.const_set :Foo, true
+ cls.send :private_constant, "Foo"
+
+ -> { cls::Foo }.should.raise(NameError)
+ end
+
+ it "accepts multiple names" do
+ mod = Module.new
+ mod.const_set :Foo, true
+ mod.const_set :Bar, true
+
+ mod.send :private_constant, :Foo, :Bar
+
+ -> {mod::Foo}.should.raise(NameError)
+ -> {mod::Bar}.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/module/private_instance_methods_spec.rb b/spec/ruby/core/module/private_instance_methods_spec.rb
new file mode 100644
index 0000000000..ae0f21d1dd
--- /dev/null
+++ b/spec/ruby/core/module/private_instance_methods_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+describe "Module#private_instance_methods" do
+ it "returns a list of private methods in module and its ancestors" do
+ ModuleSpecs::CountsMixin.private_instance_methods(false).should.include?(:private_3)
+
+ ModuleSpecs::CountsParent.private_instance_methods(false).should.include?(:private_2)
+ ModuleSpecs::CountsParent.private_instance_methods(true).should.include?(:private_3)
+
+ ModuleSpecs::CountsChild.private_instance_methods(false).should.include?(:private_1)
+ ModuleSpecs::CountsChild.private_instance_methods(true).should.include?(:private_2)
+ ModuleSpecs::CountsChild.private_instance_methods(true).should.include?(:private_3)
+ end
+
+ it "when passed false as a parameter, should return only methods defined in that module" do
+ ModuleSpecs::CountsMixin.private_instance_methods(false).should.include?(:private_3)
+ ModuleSpecs::CountsParent.private_instance_methods(false).should.include?(:private_2)
+ ModuleSpecs::CountsChild.private_instance_methods(false).should.include?(:private_1)
+ end
+
+ it "default list should be the same as passing true as an argument" do
+ ModuleSpecs::CountsMixin.private_instance_methods(true).should ==
+ ModuleSpecs::CountsMixin.private_instance_methods
+ ModuleSpecs::CountsParent.private_instance_methods(true).should ==
+ ModuleSpecs::CountsParent.private_instance_methods
+ ModuleSpecs::CountsChild.private_instance_methods(true).should ==
+ ModuleSpecs::CountsChild.private_instance_methods
+ end
+end
+
+describe :module_private_instance_methods_supers, shared: true do
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.private_instance_methods(*@object)
+ m.select { |x| x == :pri }.sort.should == [:pri]
+ end
+
+ it "returns a unique list for a subclass" do
+ m = ReflectSpecs::E.private_instance_methods(*@object)
+ m.select { |x| x == :pri }.sort.should == [:pri]
+ end
+end
+
+describe "Module#private_instance_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :module_private_instance_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :module_private_instance_methods_supers, nil, true
+ end
+end
diff --git a/spec/ruby/core/module/private_method_defined_spec.rb b/spec/ruby/core/module/private_method_defined_spec.rb
new file mode 100644
index 0000000000..f730decc0a
--- /dev/null
+++ b/spec/ruby/core/module/private_method_defined_spec.rb
@@ -0,0 +1,120 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#private_method_defined?" do
+ it "returns true if the named private method is defined by module or its ancestors" do
+ ModuleSpecs::CountsMixin.private_method_defined?("private_3").should == true
+
+ ModuleSpecs::CountsParent.private_method_defined?("private_3").should == true
+ ModuleSpecs::CountsParent.private_method_defined?("private_2").should == true
+
+ ModuleSpecs::CountsChild.private_method_defined?("private_3").should == true
+ ModuleSpecs::CountsChild.private_method_defined?("private_2").should == true
+ ModuleSpecs::CountsChild.private_method_defined?("private_1").should == true
+ end
+
+ it "returns false if method is not a private method" do
+ ModuleSpecs::CountsChild.private_method_defined?("public_3").should == false
+ ModuleSpecs::CountsChild.private_method_defined?("public_2").should == false
+ ModuleSpecs::CountsChild.private_method_defined?("public_1").should == false
+
+ ModuleSpecs::CountsChild.private_method_defined?("protected_3").should == false
+ ModuleSpecs::CountsChild.private_method_defined?("protected_2").should == false
+ ModuleSpecs::CountsChild.private_method_defined?("protected_1").should == false
+ end
+
+ it "returns false if the named method is not defined by the module or its ancestors" do
+ ModuleSpecs::CountsMixin.private_method_defined?(:private_10).should == false
+ end
+
+ it "accepts symbols for the method name" do
+ ModuleSpecs::CountsMixin.private_method_defined?(:private_3).should == true
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> do
+ ModuleSpecs::CountsMixin.private_method_defined?(1)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> do
+ ModuleSpecs::CountsMixin.private_method_defined?(nil)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed false" do
+ -> do
+ ModuleSpecs::CountsMixin.private_method_defined?(false)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that does not defined #to_str" do
+ -> do
+ ModuleSpecs::CountsMixin.private_method_defined?(mock('x'))
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that defines #to_sym" do
+ sym = mock('symbol')
+ def sym.to_sym() :private_3 end
+
+ -> do
+ ModuleSpecs::CountsMixin.private_method_defined?(sym)
+ end.should.raise(TypeError)
+ end
+
+ it "calls #to_str to convert an Object" do
+ str = mock('string')
+ def str.to_str() 'private_3' end
+ ModuleSpecs::CountsMixin.private_method_defined?(str).should == true
+ end
+
+ describe "when passed true as a second optional argument" do
+ it "performs a lookup in ancestors" do
+ ModuleSpecs::Child.private_method_defined?(:public_child, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_child, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:accessor_method, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_child, true).should == true
+
+ # Defined in Parent
+ ModuleSpecs::Child.private_method_defined?(:public_parent, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_parent, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_parent, true).should == true
+
+ # Defined in Module
+ ModuleSpecs::Child.private_method_defined?(:public_module, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_module, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_module, true).should == true
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.private_method_defined?(:public_super_module, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_super_module, true).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_super_module, true).should == true
+ end
+ end
+
+ describe "when passed false as a second optional argument" do
+ it "checks only the class itself" do
+ ModuleSpecs::Child.private_method_defined?(:public_child, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_child, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:accessor_method, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_child, false).should == true
+
+ # Defined in Parent
+ ModuleSpecs::Child.private_method_defined?(:public_parent, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_parent, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_parent, false).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.private_method_defined?(:public_module, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_module, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_module, false).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.private_method_defined?(:public_super_module, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:protected_super_module, false).should == false
+ ModuleSpecs::Child.private_method_defined?(:private_super_module, false).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/module/private_spec.rb b/spec/ruby/core/module/private_spec.rb
new file mode 100644
index 0000000000..e60fdb24cc
--- /dev/null
+++ b/spec/ruby/core/module/private_spec.rb
@@ -0,0 +1,95 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/set_visibility'
+
+describe "Module#private" do
+ it_behaves_like :set_visibility, :private
+
+ it "makes the target method uncallable from other types" do
+ obj = Object.new
+ class << obj
+ def foo; true; end
+ end
+
+ obj.foo.should == true
+
+ class << obj
+ private :foo
+ end
+
+ -> { obj.foo }.should.raise(NoMethodError)
+ end
+
+ it "makes a public Object instance method private in a new module" do
+ m = Module.new do
+ private :module_specs_public_method_on_object
+ end
+
+ m.private_instance_methods(false).should.include?(:module_specs_public_method_on_object)
+
+ # Ensure we did not change Object's method
+ Object.private_instance_methods(true).should_not.include?(:module_specs_public_method_on_object)
+ end
+
+ it "makes a public Object instance method private in Kernel" do
+ Kernel.private_instance_methods(false).should.include?(
+ :module_specs_public_method_on_object_for_kernel_private)
+ Object.private_instance_methods(true).should_not.include?(
+ :module_specs_public_method_on_object_for_kernel_private)
+ end
+
+ it "returns argument or arguments if given" do
+ (class << Object.new; self; end).class_eval do
+ def foo; end
+ private(:foo).should.equal?(:foo)
+ private([:foo, :foo]).should == [:foo, :foo]
+ private(:foo, :foo).should == [:foo, :foo]
+ private.should.equal?(nil)
+ end
+ end
+
+ it "raises a NameError when given an undefined name" do
+ -> do
+ Module.new.send(:private, :undefined)
+ end.should.raise(NameError)
+ end
+
+ it "only makes the method private in the class it is called on" do
+ base = Class.new do
+ def wrapped
+ 1
+ end
+ end
+
+ klass = Class.new(base) do
+ def wrapped
+ super + 1
+ end
+ private :wrapped
+ end
+
+ base.new.wrapped.should == 1
+ -> do
+ klass.new.wrapped
+ end.should.raise(NameError)
+ end
+
+ it "continues to allow a prepended module method to call super" do
+ wrapper = Module.new do
+ def wrapped
+ super + 1
+ end
+ end
+
+ klass = Class.new do
+ prepend wrapper
+
+ def wrapped
+ 1
+ end
+ private :wrapped
+ end
+
+ klass.new.wrapped.should == 2
+ end
+end
diff --git a/spec/ruby/core/module/protected_instance_methods_spec.rb b/spec/ruby/core/module/protected_instance_methods_spec.rb
new file mode 100644
index 0000000000..ea7ded030e
--- /dev/null
+++ b/spec/ruby/core/module/protected_instance_methods_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+describe "Module#protected_instance_methods" do
+ it "returns a list of protected methods in module and its ancestors" do
+ methods = ModuleSpecs::CountsMixin.protected_instance_methods
+ methods.should.include?(:protected_3)
+
+ methods = ModuleSpecs::CountsParent.protected_instance_methods
+ methods.should.include?(:protected_3)
+ methods.should.include?(:protected_2)
+
+ methods = ModuleSpecs::CountsChild.protected_instance_methods
+ methods.should.include?(:protected_3)
+ methods.should.include?(:protected_2)
+ methods.should.include?(:protected_1)
+ end
+
+ it "when passed false as a parameter, should return only methods defined in that module" do
+ ModuleSpecs::CountsMixin.protected_instance_methods(false).should == [:protected_3]
+ ModuleSpecs::CountsParent.protected_instance_methods(false).should == [:protected_2]
+ ModuleSpecs::CountsChild.protected_instance_methods(false).should == [:protected_1]
+ end
+
+ it "default list should be the same as passing true as an argument" do
+ ModuleSpecs::CountsMixin.protected_instance_methods(true).should ==
+ ModuleSpecs::CountsMixin.protected_instance_methods
+ ModuleSpecs::CountsParent.protected_instance_methods(true).should ==
+ ModuleSpecs::CountsParent.protected_instance_methods
+ ModuleSpecs::CountsChild.protected_instance_methods(true).should ==
+ ModuleSpecs::CountsChild.protected_instance_methods
+ end
+end
+
+describe :module_protected_instance_methods_supers, shared: true do
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.protected_instance_methods(*@object)
+ m.select { |x| x == :pro }.sort.should == [:pro]
+ end
+
+ it "returns a unique list for a subclass" do
+ m = ReflectSpecs::E.protected_instance_methods(*@object)
+ m.select { |x| x == :pro }.sort.should == [:pro]
+ end
+end
+
+describe "Module#protected_instance_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :module_protected_instance_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :module_protected_instance_methods_supers, nil, true
+ end
+end
diff --git a/spec/ruby/core/module/protected_method_defined_spec.rb b/spec/ruby/core/module/protected_method_defined_spec.rb
new file mode 100644
index 0000000000..78ba120c2f
--- /dev/null
+++ b/spec/ruby/core/module/protected_method_defined_spec.rb
@@ -0,0 +1,120 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#protected_method_defined?" do
+ it "returns true if the named protected method is defined by module or its ancestors" do
+ ModuleSpecs::CountsMixin.protected_method_defined?("protected_3").should == true
+
+ ModuleSpecs::CountsParent.protected_method_defined?("protected_3").should == true
+ ModuleSpecs::CountsParent.protected_method_defined?("protected_2").should == true
+
+ ModuleSpecs::CountsChild.protected_method_defined?("protected_3").should == true
+ ModuleSpecs::CountsChild.protected_method_defined?("protected_2").should == true
+ ModuleSpecs::CountsChild.protected_method_defined?("protected_1").should == true
+ end
+
+ it "returns false if method is not a protected method" do
+ ModuleSpecs::CountsChild.protected_method_defined?("public_3").should == false
+ ModuleSpecs::CountsChild.protected_method_defined?("public_2").should == false
+ ModuleSpecs::CountsChild.protected_method_defined?("public_1").should == false
+
+ ModuleSpecs::CountsChild.protected_method_defined?("private_3").should == false
+ ModuleSpecs::CountsChild.protected_method_defined?("private_2").should == false
+ ModuleSpecs::CountsChild.protected_method_defined?("private_1").should == false
+ end
+
+ it "returns false if the named method is not defined by the module or its ancestors" do
+ ModuleSpecs::CountsMixin.protected_method_defined?(:protected_10).should == false
+ end
+
+ it "accepts symbols for the method name" do
+ ModuleSpecs::CountsMixin.protected_method_defined?(:protected_3).should == true
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> do
+ ModuleSpecs::CountsMixin.protected_method_defined?(1)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> do
+ ModuleSpecs::CountsMixin.protected_method_defined?(nil)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed false" do
+ -> do
+ ModuleSpecs::CountsMixin.protected_method_defined?(false)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that does not defined #to_str" do
+ -> do
+ ModuleSpecs::CountsMixin.protected_method_defined?(mock('x'))
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that defines #to_sym" do
+ sym = mock('symbol')
+ def sym.to_sym() :protected_3 end
+
+ -> do
+ ModuleSpecs::CountsMixin.protected_method_defined?(sym)
+ end.should.raise(TypeError)
+ end
+
+ it "calls #to_str to convert an Object" do
+ str = mock('protected_3')
+ str.should_receive(:to_str).and_return("protected_3")
+ ModuleSpecs::CountsMixin.protected_method_defined?(str).should == true
+ end
+
+ describe "when passed true as a second optional argument" do
+ it "performs a lookup in ancestors" do
+ ModuleSpecs::Child.protected_method_defined?(:public_child, true).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_child, true).should == true
+ ModuleSpecs::Child.protected_method_defined?(:accessor_method, true).should == false
+ ModuleSpecs::Child.protected_method_defined?(:private_child, true).should == false
+
+ # Defined in Parent
+ ModuleSpecs::Child.protected_method_defined?(:public_parent, true).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_parent, true).should == true
+ ModuleSpecs::Child.protected_method_defined?(:private_parent, true).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.protected_method_defined?(:public_module, true).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_module, true).should == true
+ ModuleSpecs::Child.protected_method_defined?(:private_module, true).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.protected_method_defined?(:public_super_module, true).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_super_module, true).should == true
+ ModuleSpecs::Child.protected_method_defined?(:private_super_module, true).should == false
+ end
+ end
+
+ describe "when passed false as a second optional argument" do
+ it "checks only the class itself" do
+ ModuleSpecs::Child.protected_method_defined?(:public_child, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_child, false).should == true
+ ModuleSpecs::Child.protected_method_defined?(:accessor_method, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:private_child, false).should == false
+
+ # Defined in Parent
+ ModuleSpecs::Child.protected_method_defined?(:public_parent, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_parent, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:private_parent, false).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.protected_method_defined?(:public_module, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_module, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:private_module, false).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.protected_method_defined?(:public_super_module, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:protected_super_module, false).should == false
+ ModuleSpecs::Child.protected_method_defined?(:private_super_module, false).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/module/protected_spec.rb b/spec/ruby/core/module/protected_spec.rb
new file mode 100644
index 0000000000..3ef6c76fae
--- /dev/null
+++ b/spec/ruby/core/module/protected_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/set_visibility'
+
+describe "Module#protected" do
+ before :each do
+ class << ModuleSpecs::Parent
+ def protected_method_1; 5; end
+ end
+ end
+
+ it_behaves_like :set_visibility, :protected
+
+ it "makes an existing class method protected" do
+ ModuleSpecs::Parent.protected_method_1.should == 5
+
+ class << ModuleSpecs::Parent
+ protected :protected_method_1
+ end
+
+ -> { ModuleSpecs::Parent.protected_method_1 }.should.raise(NoMethodError)
+ end
+
+ it "makes a public Object instance method protected in a new module" do
+ m = Module.new do
+ protected :module_specs_public_method_on_object
+ end
+
+ m.protected_instance_methods(false).should.include?(:module_specs_public_method_on_object)
+
+ # Ensure we did not change Object's method
+ Object.protected_instance_methods(true).should_not.include?(:module_specs_public_method_on_object)
+ end
+
+ it "makes a public Object instance method protected in Kernel" do
+ Kernel.protected_instance_methods(false).should.include?(
+ :module_specs_public_method_on_object_for_kernel_protected)
+ Object.protected_instance_methods(true).should_not.include?(
+ :module_specs_public_method_on_object_for_kernel_protected)
+ end
+
+ it "returns argument or arguments if given" do
+ (class << Object.new; self; end).class_eval do
+ def foo; end
+ protected(:foo).should.equal?(:foo)
+ protected([:foo, :foo]).should == [:foo, :foo]
+ protected(:foo, :foo).should == [:foo, :foo]
+ protected.should.equal?(nil)
+ end
+ end
+
+ it "raises a NameError when given an undefined name" do
+ -> do
+ Module.new.send(:protected, :undefined)
+ end.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/module/public_class_method_spec.rb b/spec/ruby/core/module/public_class_method_spec.rb
new file mode 100644
index 0000000000..4aca4d4311
--- /dev/null
+++ b/spec/ruby/core/module/public_class_method_spec.rb
@@ -0,0 +1,94 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#public_class_method" do
+ before :each do
+ class << ModuleSpecs::Parent
+ private
+ def public_method_1; end
+ def public_method_2; end
+ end
+ end
+
+ after :each do
+ class << ModuleSpecs::Parent
+ remove_method :public_method_1
+ remove_method :public_method_2
+ end
+ end
+
+ it "makes an existing class method public" do
+ -> { ModuleSpecs::Parent.public_method_1 }.should.raise(NoMethodError)
+ ModuleSpecs::Parent.public_class_method :public_method_1
+ ModuleSpecs::Parent.public_method_1.should == nil
+
+ # Technically above we're testing the Singleton classes, class method(right?).
+ # Try a "real" class method set public.
+ ModuleSpecs::Parent.public_method.should == nil
+ end
+
+ it "makes an existing class method public up the inheritance tree" do
+ ModuleSpecs::Child.private_class_method :public_method_1
+ -> { ModuleSpecs::Child.public_method_1 }.should.raise(NoMethodError)
+ ModuleSpecs::Child.public_class_method :public_method_1
+
+ ModuleSpecs::Child.public_method_1.should == nil
+ ModuleSpecs::Child.public_method.should == nil
+ end
+
+ it "accepts more than one method at a time" do
+ -> { ModuleSpecs::Parent.public_method_1 }.should.raise(NameError)
+ -> { ModuleSpecs::Parent.public_method_2 }.should.raise(NameError)
+
+ ModuleSpecs::Child.public_class_method :public_method_1, :public_method_2
+
+ ModuleSpecs::Child.public_method_1.should == nil
+ ModuleSpecs::Child.public_method_2.should == nil
+ end
+
+ it "raises a NameError if class method doesn't exist" do
+ -> do
+ ModuleSpecs.public_class_method :no_method_here
+ end.should.raise(NameError)
+ end
+
+ it "makes a class method public" do
+ c = Class.new do
+ def self.foo() "foo" end
+ public_class_method :foo
+ end
+
+ c.foo.should == "foo"
+ end
+
+ it "raises a NameError when the given name is not a method" do
+ -> do
+ Class.new do
+ public_class_method :foo
+ end
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError when the given name is an instance method" do
+ -> do
+ Class.new do
+ def foo() "foo" end
+ public_class_method :foo
+ end
+ end.should.raise(NameError)
+ end
+
+ context "when single argument is passed and is an array" do
+ it "makes a class method public" do
+ c = Class.new do
+ class << self
+ private
+ def foo() "foo" end
+ end
+ public_class_method [:foo]
+ end
+
+ c.foo.should == "foo"
+ end
+ end
+end
diff --git a/spec/ruby/core/module/public_constant_spec.rb b/spec/ruby/core/module/public_constant_spec.rb
new file mode 100644
index 0000000000..87a051f125
--- /dev/null
+++ b/spec/ruby/core/module/public_constant_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+
+describe "Module#public_constant" do
+ it "can only be passed constant names defined in the target (self) module" do
+ cls1 = Class.new
+ cls1.const_set :Foo, true
+ cls2 = Class.new(cls1)
+
+ -> do
+ cls2.send :public_constant, :Foo
+ end.should.raise(NameError)
+ end
+
+ it "accepts strings as constant names" do
+ cls = Class.new
+ cls.const_set :Foo, true
+
+ cls.send :private_constant, :Foo
+ cls.send :public_constant, "Foo"
+
+ cls::Foo.should == true
+ end
+
+ # [ruby-list:48558]
+ it "accepts multiple names" do
+ mod = Module.new
+ mod.const_set :Foo, true
+ mod.const_set :Bar, true
+
+ mod.send :private_constant, :Foo
+ mod.send :private_constant, :Bar
+
+ mod.send :public_constant, :Foo, :Bar
+
+ mod::Foo.should == true
+ mod::Bar.should == true
+ end
+end
diff --git a/spec/ruby/core/module/public_instance_method_spec.rb b/spec/ruby/core/module/public_instance_method_spec.rb
new file mode 100644
index 0000000000..87c1bae599
--- /dev/null
+++ b/spec/ruby/core/module/public_instance_method_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#public_instance_method" do
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:public_instance_method)
+ end
+
+ it "requires an argument" do
+ Module.new.method(:public_instance_method).arity.should == 1
+ end
+
+ describe "when given a public method name" do
+ it "returns an UnboundMethod corresponding to the defined Module" do
+ ret = ModuleSpecs::Super.public_instance_method(:public_module)
+ ret.should.instance_of?(UnboundMethod)
+ ret.owner.should.equal?(ModuleSpecs::Basic)
+
+ ret = ModuleSpecs::Super.public_instance_method(:public_super_module)
+ ret.should.instance_of?(UnboundMethod)
+ ret.owner.should.equal?(ModuleSpecs::Super)
+ end
+
+ it "accepts if the name is a Symbol or String" do
+ ret = ModuleSpecs::Basic.public_instance_method(:public_module)
+ ModuleSpecs::Basic.public_instance_method("public_module").should == ret
+ end
+ end
+
+ it "raises a TypeError when given a name is not Symbol or String" do
+ -> { Module.new.public_instance_method(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a NameError when given a protected method name" do
+ -> do
+ ModuleSpecs::Basic.public_instance_method(:protected_module)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError if the method is private" do
+ -> do
+ ModuleSpecs::Basic.public_instance_method(:private_module)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError if the method has been undefined" do
+ -> do
+ ModuleSpecs::Parent.public_instance_method(:undefed_method)
+ end.should.raise(NameError)
+ end
+
+ it "raises a NameError if the method does not exist" do
+ -> do
+ Module.new.public_instance_method(:missing)
+ end.should.raise(NameError)
+ end
+
+ it "sets the NameError#name attribute to the name of the missing method" do
+ begin
+ Module.new.public_instance_method(:missing)
+ rescue NameError => e
+ e.name.should == :missing
+ end
+ end
+end
diff --git a/spec/ruby/core/module/public_instance_methods_spec.rb b/spec/ruby/core/module/public_instance_methods_spec.rb
new file mode 100644
index 0000000000..edea00d927
--- /dev/null
+++ b/spec/ruby/core/module/public_instance_methods_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../fixtures/reflection'
+
+# TODO: rewrite
+
+describe "Module#public_instance_methods" do
+ it "returns a list of public methods in module and its ancestors" do
+ methods = ModuleSpecs::CountsMixin.public_instance_methods
+ methods.should.include?(:public_3)
+
+ methods = ModuleSpecs::CountsParent.public_instance_methods
+ methods.should.include?(:public_3)
+ methods.should.include?(:public_2)
+
+ methods = ModuleSpecs::CountsChild.public_instance_methods
+ methods.should.include?(:public_3)
+ methods.should.include?(:public_2)
+ methods.should.include?(:public_1)
+
+ methods = ModuleSpecs::Child2.public_instance_methods
+ methods.should.include?(:foo)
+ end
+
+ it "when passed false as a parameter, should return only methods defined in that module" do
+ ModuleSpecs::CountsMixin.public_instance_methods(false).should == [:public_3]
+ ModuleSpecs::CountsParent.public_instance_methods(false).should == [:public_2]
+ ModuleSpecs::CountsChild.public_instance_methods(false).should == [:public_1]
+ end
+
+ it "default list should be the same as passing true as an argument" do
+ ModuleSpecs::CountsMixin.public_instance_methods(true).should ==
+ ModuleSpecs::CountsMixin.public_instance_methods
+ ModuleSpecs::CountsParent.public_instance_methods(true).should ==
+ ModuleSpecs::CountsParent.public_instance_methods
+ ModuleSpecs::CountsChild.public_instance_methods(true).should ==
+ ModuleSpecs::CountsChild.public_instance_methods
+ end
+end
+
+describe :module_public_instance_methods_supers, shared: true do
+ it "returns a unique list for a class including a module" do
+ m = ReflectSpecs::D.public_instance_methods(*@object)
+ m.select { |x| x == :pub }.sort.should == [:pub]
+ end
+
+ it "returns a unique list for a subclass" do
+ m = ReflectSpecs::E.public_instance_methods(*@object)
+ m.select { |x| x == :pub }.sort.should == [:pub]
+ end
+end
+
+describe "Module#public_instance_methods" do
+ describe "when not passed an argument" do
+ it_behaves_like :module_public_instance_methods_supers, nil, []
+ end
+
+ describe "when passed true" do
+ it_behaves_like :module_public_instance_methods_supers, nil, true
+ end
+end
diff --git a/spec/ruby/core/module/public_method_defined_spec.rb b/spec/ruby/core/module/public_method_defined_spec.rb
new file mode 100644
index 0000000000..70cd992358
--- /dev/null
+++ b/spec/ruby/core/module/public_method_defined_spec.rb
@@ -0,0 +1,72 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#public_method_defined?" do
+ it "returns true if the named public method is defined by module or its ancestors" do
+ ModuleSpecs::CountsMixin.public_method_defined?("public_3").should == true
+
+ ModuleSpecs::CountsParent.public_method_defined?("public_3").should == true
+ ModuleSpecs::CountsParent.public_method_defined?("public_2").should == true
+
+ ModuleSpecs::CountsChild.public_method_defined?("public_3").should == true
+ ModuleSpecs::CountsChild.public_method_defined?("public_2").should == true
+ ModuleSpecs::CountsChild.public_method_defined?("public_1").should == true
+ end
+
+ it "returns false if method is not a public method" do
+ ModuleSpecs::CountsChild.public_method_defined?("private_3").should == false
+ ModuleSpecs::CountsChild.public_method_defined?("private_2").should == false
+ ModuleSpecs::CountsChild.public_method_defined?("private_1").should == false
+
+ ModuleSpecs::CountsChild.public_method_defined?("protected_3").should == false
+ ModuleSpecs::CountsChild.public_method_defined?("protected_2").should == false
+ ModuleSpecs::CountsChild.public_method_defined?("protected_1").should == false
+ end
+
+ it "returns false if the named method is not defined by the module or its ancestors" do
+ ModuleSpecs::CountsMixin.public_method_defined?(:public_10).should == false
+ end
+
+ it "accepts symbols for the method name" do
+ ModuleSpecs::CountsMixin.public_method_defined?(:public_3).should == true
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> do
+ ModuleSpecs::CountsMixin.public_method_defined?(1)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> do
+ ModuleSpecs::CountsMixin.public_method_defined?(nil)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed false" do
+ -> do
+ ModuleSpecs::CountsMixin.public_method_defined?(false)
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that does not defined #to_str" do
+ -> do
+ ModuleSpecs::CountsMixin.public_method_defined?(mock('x'))
+ end.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an object that defines #to_sym" do
+ sym = mock('symbol')
+ def sym.to_sym() :public_3 end
+
+ -> do
+ ModuleSpecs::CountsMixin.public_method_defined?(sym)
+ end.should.raise(TypeError)
+ end
+
+ it "calls #to_str to convert an Object" do
+ str = mock('public_3')
+ def str.to_str() 'public_3' end
+ ModuleSpecs::CountsMixin.public_method_defined?(str).should == true
+ end
+end
diff --git a/spec/ruby/core/module/public_spec.rb b/spec/ruby/core/module/public_spec.rb
new file mode 100644
index 0000000000..f35c64143b
--- /dev/null
+++ b/spec/ruby/core/module/public_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/set_visibility'
+
+describe "Module#public" do
+ it_behaves_like :set_visibility, :public
+
+ it "on a superclass method calls the redefined method" do
+ ModuleSpecs::ChildPrivateMethodMadePublic.new.private_method_redefined.should == :after_redefinition
+ end
+
+ it "makes a private Object instance method public in a new module" do
+ m = Module.new do
+ public :module_specs_private_method_on_object
+ end
+
+ m.public_instance_methods(false).should.include?(:module_specs_private_method_on_object)
+
+ # Ensure we did not change Object's method
+ Object.public_instance_methods(true).should_not.include?(:module_specs_private_method_on_object)
+ end
+
+ it "makes a private Object instance method public in Kernel" do
+ Kernel.public_instance_methods(false).should.include?(
+ :module_specs_private_method_on_object_for_kernel_public)
+ Object.public_instance_methods(true).should_not.include?(
+ :module_specs_private_method_on_object_for_kernel_public)
+ end
+
+ it "returns argument or arguments if given" do
+ (class << Object.new; self; end).class_eval do
+ def foo; end
+ public(:foo).should.equal?(:foo)
+ public([:foo, :foo]).should == [:foo, :foo]
+ public(:foo, :foo).should == [:foo, :foo]
+ public.should.equal?(nil)
+ end
+ end
+
+ it "raises a NameError when given an undefined name" do
+ -> do
+ Module.new.send(:public, :undefined)
+ end.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/module/refine_spec.rb b/spec/ruby/core/module/refine_spec.rb
new file mode 100644
index 0000000000..d0fc7015f8
--- /dev/null
+++ b/spec/ruby/core/module/refine_spec.rb
@@ -0,0 +1,713 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/refine'
+
+describe "Module#refine" do
+ it "runs its block in an anonymous module" do
+ inner_self = nil
+ mod = Module.new do
+ refine String do
+ inner_self = self
+ end
+ end
+
+ mod.should_not == inner_self
+ inner_self.should.is_a?(Module)
+ inner_self.name.should == nil
+ end
+
+ it "uses the same anonymous module for future refines of the same class" do
+ selves = []
+ mod = Module.new do
+ refine String do
+ selves << self
+ end
+ end
+
+ mod.module_eval do
+ refine String do
+ selves << self
+ end
+ end
+
+ selves[0].should == selves[1]
+ end
+
+ it "adds methods defined in its block to the anonymous module's public instance methods" do
+ inner_self = nil
+ mod = Module.new do
+ refine String do
+ def blah
+ "blah"
+ end
+ inner_self = self
+ end
+ end
+
+ inner_self.public_instance_methods.should.include?(:blah)
+ end
+
+ it "returns created anonymous module" do
+ inner_self = nil
+ result = nil
+ mod = Module.new do
+ result = refine String do
+ inner_self = self
+ end
+ end
+
+ result.should == inner_self
+ end
+
+ it "raises ArgumentError if not passed an argument" do
+ -> do
+ Module.new do
+ refine {}
+ end
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises TypeError if not passed a class" do
+ -> do
+ Module.new do
+ refine("foo") {}
+ end
+ end.should.raise(TypeError, "wrong argument type String (expected Class or Module)")
+ end
+
+ it "accepts a module as argument" do
+ inner_self = nil
+ Module.new do
+ refine(Enumerable) do
+ def blah
+ end
+ inner_self = self
+ end
+ end
+
+ inner_self.public_instance_methods.should.include?(:blah)
+ end
+
+ it "applies refinements to the module" do
+ refinement = Module.new do
+ refine(Enumerable) do
+ def foo?
+ self.any? ? "yes" : "no"
+ end
+ end
+ end
+
+ foo = Class.new do
+ using refinement
+
+ def initialize(items)
+ @items = items
+ end
+
+ def result
+ @items.foo?
+ end
+ end
+
+ foo.new([]).result.should == "no"
+ foo.new([1]).result.should == "yes"
+ end
+
+ it "raises ArgumentError if not given a block" do
+ -> do
+ Module.new do
+ refine String
+ end
+ end.should.raise(ArgumentError)
+ end
+
+ it "applies refinements to calls in the refine block" do
+ result = nil
+ Module.new do
+ refine(String) do
+ def foo; "foo"; end
+ result = "hello".foo
+ end
+ end
+ result.should == "foo"
+ end
+
+ it "doesn't apply refinements outside the refine block" do
+ Module.new do
+ refine(String) {def foo; "foo"; end}
+ -> {
+ "hello".foo
+ }.should.raise(NoMethodError)
+ end
+ end
+
+ it "does not apply refinements to external scopes not using the module" do
+ Module.new do
+ refine(String) {def foo; 'foo'; end}
+ end
+
+ -> {"hello".foo}.should.raise(NoMethodError)
+ end
+
+ # When defining multiple refinements in the same module,
+ # inside a refine block all refinements from the same
+ # module are active when a refined method is called
+ it "makes available all refinements from the same module" do
+ refinement = Module.new do
+ refine Integer do
+ def to_json_format
+ to_s
+ end
+ end
+
+ refine Array do
+ def to_json_format
+ "[" + map { |i| i.to_json_format }.join(", ") + "]"
+ end
+ end
+
+ refine Hash do
+ def to_json_format
+ "{" + map { |k, v| k.to_s.dump + ": " + v.to_json_format }.join(", ") + "}"
+ end
+ end
+ end
+
+ result = nil
+
+ Module.new do
+ using refinement
+
+ result = [{1 => 2}, {3 => 4}].to_json_format
+ end
+
+ result.should == '[{"1": 2}, {"3": 4}]'
+ end
+
+ it "does not make available methods from another refinement module" do
+ refinery_integer = Module.new do
+ refine Integer do
+ def to_json_format
+ to_s
+ end
+ end
+ end
+
+ refinery_array = Module.new do
+ refine Array do
+ def to_json_format
+ "[" + map { |i| i.to_json_format }.join(",") + "]"
+ end
+ end
+ end
+
+ result = nil
+
+ -> {
+ Module.new do
+ using refinery_integer
+ using refinery_array
+
+ [1, 2].to_json_format
+ end
+ }.should.raise(NoMethodError)
+ end
+
+ # method lookup:
+ # * The prepended modules from the refinement for C
+ # * The refinement for C
+ # * The included modules from the refinement for C
+ # * The prepended modules of C
+ # * C
+ # * The included modules of C
+ describe "method lookup" do
+ it "looks in the object singleton class first" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+
+ obj = refined_class.new
+ class << obj
+ def foo; "foo from singleton class"; end
+ end
+ result = obj.foo
+ end
+
+ result.should == "foo from singleton class"
+ end
+
+ it "looks in later included modules of the refined module first" do
+ a = Module.new do
+ def foo
+ "foo from A"
+ end
+ end
+
+ include_me_later = Module.new do
+ def foo
+ "foo from IncludeMeLater"
+ end
+ end
+
+ c = Class.new do
+ include a
+ end
+
+ refinement = Module.new do
+ refine c do; end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ c.include include_me_later
+ result = c.new.foo
+ end
+
+ result.should == "foo from IncludeMeLater"
+ end
+
+ it "looks in the class then" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ refinement = Module.new do
+ refine(refined_class) { }
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = refined_class.new.foo
+ end
+
+ result.should == "foo"
+ end
+ end
+
+
+ # methods in a subclass have priority over refinements in a superclass
+ it "does not override methods in subclasses" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ subclass = Class.new(refined_class) do
+ def foo; "foo from subclass"; end
+ end
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = subclass.new.foo
+ end
+
+ result.should == "foo from subclass"
+ end
+
+ context "for methods accessed indirectly" do
+ it "is honored by Kernel#send" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = refined_class.new.send :foo
+ end
+
+ result.should == "foo from refinement"
+ end
+
+ it "is honored by BasicObject#__send__" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = refined_class.new.__send__ :foo
+ end
+
+ result.should == "foo from refinement"
+ end
+
+ it "is honored by Symbol#to_proc" do
+ refinement = Module.new do
+ refine Integer do
+ def to_s
+ "(#{super})"
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = [1, 2, 3].map(&:to_s)
+ end
+
+ result.should == ["(1)", "(2)", "(3)"]
+ end
+
+ it "is honored by Kernel#public_send" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = refined_class.new.public_send :foo
+ end
+
+ result.should == "foo from refinement"
+ end
+
+ it "is honored by string interpolation" do
+ refinement = Module.new do
+ refine Integer do
+ def to_s
+ "foo"
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = "#{1}"
+ end
+
+ result.should == "foo"
+ end
+
+ it "is honored by Kernel#binding" do
+ refinement = Module.new do
+ refine String do
+ def to_s
+ "hello from refinement"
+ end
+ end
+ end
+
+ klass = Class.new do
+ using refinement
+
+ def foo
+ "foo".to_s
+ end
+
+ def get_binding
+ binding
+ end
+ end
+
+ result = Kernel.eval("self.foo()", klass.new.get_binding)
+ result.should == "hello from refinement"
+ end
+
+ it "is honored by Kernel#method" do
+ klass = Class.new
+ refinement = Module.new do
+ refine klass do
+ def foo; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = klass.new.method(:foo).class
+ end
+
+ result.should == Method
+ end
+
+ it "is honored by Kernel#public_method" do
+ klass = Class.new
+ refinement = Module.new do
+ refine klass do
+ def foo; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = klass.new.public_method(:foo).class
+ end
+
+ result.should == Method
+ end
+
+ it "is honored by Kernel#instance_method" do
+ klass = Class.new
+ refinement = Module.new do
+ refine klass do
+ def foo; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = klass.instance_method(:foo).class
+ end
+
+ result.should == UnboundMethod
+ end
+
+ it "is honored by Kernel#respond_to?" do
+ klass = Class.new
+ refinement = Module.new do
+ refine klass do
+ def foo; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = klass.new.respond_to?(:foo)
+ end
+
+ result.should == true
+ end
+
+ it "is honored by &" do
+ refinement = Module.new do
+ refine String do
+ def to_proc(*args)
+ -> * { 'foo' }
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = ["hola"].map(&"upcase")
+ end
+
+ result.should == ['foo']
+ end
+ end
+
+ context "when super is called in a refinement" do
+ it "looks in the refined class" do
+ refined_class = ModuleSpecs.build_refined_class
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo
+ super
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = refined_class.new.foo
+ end
+
+ result.should == "foo"
+ end
+
+ # super in a method of a refinement invokes the method in the refined
+ # class even if there is another refinement which has been activated
+ # in the same context.
+ it "looks in the refined class first if called from refined method" do
+ refined_class = ModuleSpecs.build_refined_class(for_super: true)
+
+ refinement = Module.new do
+ refine refined_class do
+ def foo
+ [:R1]
+ end
+ end
+ end
+
+ refinement_with_super = Module.new do
+ refine refined_class do
+ def foo
+ [:R2] + super
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ using refinement_with_super
+ result = refined_class.new.foo
+ end
+
+ result.should == [:R2, :C]
+ end
+
+ it "looks only in the refined class even if there is another active refinement" do
+ refined_class = ModuleSpecs.build_refined_class(for_super: true)
+
+ refinement = Module.new do
+ refine refined_class do
+ def bar
+ "you cannot see me from super because I belong to another active R"
+ end
+ end
+ end
+
+ refinement_with_super = Module.new do
+ refine refined_class do
+ def bar
+ super
+ end
+ end
+ end
+
+
+ Module.new do
+ using refinement
+ using refinement_with_super
+ -> {
+ refined_class.new.bar
+ }.should.raise(NoMethodError)
+ end
+ end
+ end
+
+ it 'and alias aliases a method within a refinement module, but not outside it' do
+ Module.new do
+ using Module.new {
+ refine Array do
+ alias :orig_count :count
+ end
+ }
+ [1,2].orig_count.should == 2
+ end
+ -> { [1,2].orig_count }.should.raise(NoMethodError)
+ end
+
+ it 'and alias_method aliases a method within a refinement module, but not outside it' do
+ Module.new do
+ using Module.new {
+ refine Array do
+ alias_method :orig_count, :count
+ end
+ }
+ [1,2].orig_count.should == 2
+ end
+ -> { [1,2].orig_count }.should.raise(NoMethodError)
+ end
+
+ it "and instance_methods returns a list of methods including those of the refined module" do
+ methods = Array.instance_methods
+ methods_2 = []
+ Module.new do
+ refine Array do
+ methods_2 = instance_methods
+ end
+ end
+ methods.should == methods_2
+ end
+
+ # Refinements are inherited by module inclusion.
+ # That is, using activates all refinements in the ancestors of the specified module.
+ # Refinements in a descendant have priority over refinements in an ancestor.
+ context "module inclusion" do
+ it "activates all refinements from all ancestors" do
+ refinement_included = Module.new do
+ refine Integer do
+ def to_json_format
+ to_s
+ end
+ end
+ end
+
+ refinement = Module.new do
+ include refinement_included
+
+ refine Array do
+ def to_json_format
+ "[" + map { |i| i.to_s }.join(", ") + "]"
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = [5.to_json_format, [1, 2, 3].to_json_format]
+ end
+
+ result.should == ["5", "[1, 2, 3]"]
+ end
+
+ it "overrides methods of ancestors by methods in descendants" do
+ refinement_included = Module.new do
+ refine Integer do
+ def to_json_format
+ to_s
+ end
+ end
+ end
+
+ refinement = Module.new do
+ include refinement_included
+
+ refine Integer do
+ def to_json_format
+ "hello from refinement"
+ end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = 5.to_json_format
+ end
+
+ result.should == "hello from refinement"
+ end
+ end
+
+ it 'does not list methods defined only in refinement' do
+ refine_object = Module.new do
+ refine Object do
+ def refinement_only_method
+ end
+ end
+ end
+ klass = Class.new { instance_methods.should_not.include?(:refinement_only_method) }
+ instance = klass.new
+ instance.methods.should_not.include? :refinement_only_method
+ instance.respond_to?(:refinement_only_method).should == false
+ -> { instance.method :refinement_only_method }.should.raise(NameError)
+ end
+end
diff --git a/spec/ruby/core/module/refinements_spec.rb b/spec/ruby/core/module/refinements_spec.rb
new file mode 100644
index 0000000000..05658a8b0e
--- /dev/null
+++ b/spec/ruby/core/module/refinements_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "Module#refinements" do
+ it "returns refinements defined in a module" do
+ ScratchPad.record []
+
+ m = Module.new do
+ refine String do
+ ScratchPad << self
+ end
+
+ refine Array do
+ ScratchPad << self
+ end
+ end
+
+ m.refinements.sort_by(&:object_id).should == ScratchPad.recorded.sort_by(&:object_id)
+ end
+
+ it "does not return refinements defined in the included module" do
+ ScratchPad.record []
+
+ m1 = Module.new do
+ refine Integer do
+ nil
+ end
+ end
+
+ m2 = Module.new do
+ include m1
+
+ refine String do
+ ScratchPad << self
+ end
+ end
+
+ m2.refinements.should == ScratchPad.recorded
+ end
+
+ it "returns an empty array if no refinements defined in a module" do
+ Module.new.refinements.should == []
+ end
+end
diff --git a/spec/ruby/core/module/remove_class_variable_spec.rb b/spec/ruby/core/module/remove_class_variable_spec.rb
new file mode 100644
index 0000000000..fccb29f52d
--- /dev/null
+++ b/spec/ruby/core/module/remove_class_variable_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#remove_class_variable" do
+ it "removes class variable" do
+ m = ModuleSpecs::MVars.dup
+ m.send(:remove_class_variable, :@@mvar)
+ m.class_variable_defined?(:@@mvar).should == false
+ end
+
+ it "returns the value of removing class variable" do
+ m = ModuleSpecs::MVars.dup
+ m.send(:remove_class_variable, :@@mvar).should == :mvar
+ end
+
+ it "removes a class variable defined in a metaclass" do
+ obj = mock("metaclass class variable")
+ meta = obj.singleton_class
+ meta.send :class_variable_set, :@@var, 1
+ meta.send(:remove_class_variable, :@@var).should == 1
+ meta.class_variable_defined?(:@@var).should == false
+ end
+
+ it "raises a NameError when removing class variable declared in included module" do
+ c = ModuleSpecs::RemoveClassVariable.new { include ModuleSpecs::MVars.dup }
+ -> { c.send(:remove_class_variable, :@@mvar) }.should.raise(NameError)
+ end
+
+ it "raises a NameError when passed a symbol with one leading @" do
+ -> { ModuleSpecs::MVars.send(:remove_class_variable, :@mvar) }.should.raise(NameError)
+ end
+
+ it "raises a NameError when passed a symbol with no leading @" do
+ -> { ModuleSpecs::MVars.send(:remove_class_variable, :mvar) }.should.raise(NameError)
+ end
+
+ it "raises a NameError when an uninitialized class variable is given" do
+ -> { ModuleSpecs::MVars.send(:remove_class_variable, :@@nonexisting_class_variable) }.should.raise(NameError)
+ end
+
+ it "is public" do
+ Module.private_instance_methods(true).should_not.include?(:remove_class_variable)
+ end
+end
diff --git a/spec/ruby/core/module/remove_const_spec.rb b/spec/ruby/core/module/remove_const_spec.rb
new file mode 100644
index 0000000000..4e4b9fcb45
--- /dev/null
+++ b/spec/ruby/core/module/remove_const_spec.rb
@@ -0,0 +1,107 @@
+require_relative '../../spec_helper'
+require_relative '../../fixtures/constants'
+
+describe "Module#remove_const" do
+ it "removes the constant specified by a String or Symbol from the receiver's constant table" do
+ ConstantSpecs::ModuleM::CS_CONST252 = :const252
+ ConstantSpecs::ModuleM::CS_CONST252.should == :const252
+
+ ConstantSpecs::ModuleM.send :remove_const, :CS_CONST252
+ -> { ConstantSpecs::ModuleM::CS_CONST252 }.should.raise(NameError)
+
+ ConstantSpecs::ModuleM::CS_CONST253 = :const253
+ ConstantSpecs::ModuleM::CS_CONST253.should == :const253
+
+ ConstantSpecs::ModuleM.send :remove_const, "CS_CONST253"
+ -> { ConstantSpecs::ModuleM::CS_CONST253 }.should.raise(NameError)
+ end
+
+ it "returns the value of the removed constant" do
+ ConstantSpecs::ModuleM::CS_CONST254 = :const254
+ ConstantSpecs::ModuleM.send(:remove_const, :CS_CONST254).should == :const254
+ end
+
+ it "raises a NameError and does not call #const_missing if the constant is not defined" do
+ ConstantSpecs.should_not_receive(:const_missing)
+ -> { ConstantSpecs.send(:remove_const, :Nonexistent) }.should.raise(NameError)
+ end
+
+ it "raises a NameError and does not call #const_missing if the constant is not defined directly in the module" do
+ begin
+ ConstantSpecs::ModuleM::CS_CONST255 = :const255
+ ConstantSpecs::ContainerA::CS_CONST255.should == :const255
+ ConstantSpecs::ContainerA.should_not_receive(:const_missing)
+
+ -> do
+ ConstantSpecs::ContainerA.send :remove_const, :CS_CONST255
+ end.should.raise(NameError)
+ ensure
+ ConstantSpecs::ModuleM.send :remove_const, "CS_CONST255"
+ end
+ end
+
+ it "raises a NameError if the name does not start with a capital letter" do
+ -> { ConstantSpecs.send :remove_const, "name" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name starts with a non-alphabetic character" do
+ -> { ConstantSpecs.send :remove_const, "__CONSTX__" }.should.raise(NameError)
+ -> { ConstantSpecs.send :remove_const, "@Name" }.should.raise(NameError)
+ -> { ConstantSpecs.send :remove_const, "!Name" }.should.raise(NameError)
+ -> { ConstantSpecs.send :remove_const, "::Name" }.should.raise(NameError)
+ end
+
+ it "raises a NameError if the name contains non-alphabetic characters except '_'" do
+ ConstantSpecs::ModuleM::CS_CONST256 = :const256
+ ConstantSpecs::ModuleM.send :remove_const, "CS_CONST256"
+ -> { ConstantSpecs.send :remove_const, "Name=" }.should.raise(NameError)
+ -> { ConstantSpecs.send :remove_const, "Name?" }.should.raise(NameError)
+ end
+
+ it "calls #to_str to convert the given name to a String" do
+ ConstantSpecs::CS_CONST257 = :const257
+ name = mock("CS_CONST257")
+ name.should_receive(:to_str).and_return("CS_CONST257")
+ ConstantSpecs.send(:remove_const, name).should == :const257
+ end
+
+ it "raises a TypeError if conversion to a String by calling #to_str fails" do
+ name = mock('123')
+ -> { ConstantSpecs.send :remove_const, name }.should.raise(TypeError)
+
+ name.should_receive(:to_str).and_return(123)
+ -> { ConstantSpecs.send :remove_const, name }.should.raise(TypeError)
+ end
+
+ it "is a private method" do
+ Module.private_methods.should.include?(:remove_const)
+ end
+
+ it "returns nil when removing autoloaded constant" do
+ ConstantSpecs.autoload :AutoloadedConstant, 'a_file'
+ ConstantSpecs.send(:remove_const, :AutoloadedConstant).should == nil
+ end
+
+ it "updates the constant value" do
+ module ConstantSpecs::RemovedConstantUpdate
+ module M
+ FOO = 'm'
+ end
+
+ module A
+ include M
+ FOO = 'a'
+ def self.foo
+ FOO
+ end
+ end
+
+ A.foo.should == 'a'
+
+ A.send(:remove_const,:FOO)
+ A.foo.should == 'm'
+ end
+ ensure
+ ConstantSpecs.send(:remove_const, :RemovedConstantUpdate)
+ end
+end
diff --git a/spec/ruby/core/module/remove_method_spec.rb b/spec/ruby/core/module/remove_method_spec.rb
new file mode 100644
index 0000000000..39add01e36
--- /dev/null
+++ b/spec/ruby/core/module/remove_method_spec.rb
@@ -0,0 +1,131 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+module ModuleSpecs
+ class Parent
+ def method_to_remove; 1; end
+ end
+
+ class First
+ def method_to_remove; 1; end
+ end
+
+ class Second < First
+ def method_to_remove; 2; end
+ end
+end
+
+describe "Module#remove_method" do
+ before :each do
+ @module = Module.new { def method_to_remove; end }
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:remove_method)
+ end
+
+ it "removes the method from a class" do
+ klass = Class.new do
+ def method_to_remove; 1; end
+ end
+ x = klass.new
+ klass.send(:remove_method, :method_to_remove)
+ x.respond_to?(:method_to_remove).should == false
+ end
+
+ it "removes method from subclass, but not parent" do
+ child = Class.new(ModuleSpecs::Parent) do
+ def method_to_remove; 2; end
+ remove_method :method_to_remove
+ end
+ x = child.new
+ x.respond_to?(:method_to_remove).should == true
+ x.method_to_remove.should == 1
+ end
+
+ it "updates the method implementation" do
+ m_module = Module.new do
+ def foo
+ 'm'
+ end
+ end
+
+ a_class = Class.new do
+ include m_module
+
+ def foo
+ 'a'
+ end
+ end
+
+ a = a_class.new
+ foo = -> { a.foo }
+ foo.call.should == 'a'
+ a_class.remove_method(:foo)
+ foo.call.should == 'm'
+ end
+
+ it "removes multiple methods with 1 call" do
+ klass = Class.new do
+ def method_to_remove_1; 1; end
+ def method_to_remove_2; 2; end
+ remove_method :method_to_remove_1, :method_to_remove_2
+ end
+ x = klass.new
+ x.respond_to?(:method_to_remove_1).should == false
+ x.respond_to?(:method_to_remove_2).should == false
+ end
+
+ it "accepts multiple arguments" do
+ Module.instance_method(:remove_method).arity.should < 0
+ end
+
+ it "does not remove any instance methods when argument not given" do
+ before = @module.instance_methods(true) + @module.private_instance_methods(true)
+ @module.send :remove_method
+ after = @module.instance_methods(true) + @module.private_instance_methods(true)
+ before.sort.should == after.sort
+ end
+
+ it "returns self" do
+ @module.send(:remove_method, :method_to_remove).should.equal?(@module)
+ end
+
+ it "raises a NameError when attempting to remove method further up the inheritance tree" do
+ Class.new(ModuleSpecs::Second) do
+ -> {
+ remove_method :method_to_remove
+ }.should.raise(NameError)
+ end
+ end
+
+ it "raises a NameError when attempting to remove a missing method" do
+ Class.new(ModuleSpecs::Second) do
+ -> {
+ remove_method :blah
+ }.should.raise(NameError)
+ end
+ end
+
+ describe "on frozen instance" do
+ before :each do
+ @frozen = @module.dup.freeze
+ end
+
+ it "raises a FrozenError when passed a name" do
+ -> { @frozen.send :remove_method, :method_to_remove }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError when passed a missing name" do
+ -> { @frozen.send :remove_method, :not_exist }.should.raise(FrozenError)
+ end
+
+ it "raises a TypeError when passed a not name" do
+ -> { @frozen.send :remove_method, Object.new }.should.raise(TypeError)
+ end
+
+ it "does not raise exceptions when no arguments given" do
+ @frozen.send(:remove_method).should.equal?(@frozen)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/ruby2_keywords_spec.rb b/spec/ruby/core/module/ruby2_keywords_spec.rb
new file mode 100644
index 0000000000..e392642978
--- /dev/null
+++ b/spec/ruby/core/module/ruby2_keywords_spec.rb
@@ -0,0 +1,248 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#ruby2_keywords" do
+ class << self
+ ruby2_keywords def mark(*args)
+ args
+ end
+ end
+
+ it "marks the final hash argument as keyword hash" do
+ last = mark(1, 2, a: "a").last
+ Hash.ruby2_keywords_hash?(last).should == true
+ end
+
+ it "makes a copy of the hash and only marks the copy as keyword hash" do
+ obj = Object.new
+ obj.singleton_class.class_exec do
+ def regular(*args)
+ args.last
+ end
+ end
+
+ h = {a: 1}
+
+ last = mark(**h).last
+ Hash.ruby2_keywords_hash?(last).should == true
+ Hash.ruby2_keywords_hash?(h).should == false
+
+ last2 = mark(**last).last # last is already marked
+ Hash.ruby2_keywords_hash?(last2).should == true
+ Hash.ruby2_keywords_hash?(last).should == true
+ last2.should_not.equal?(last)
+ Hash.ruby2_keywords_hash?(h).should == false
+ end
+
+ it "makes a copy and unmark the Hash when calling a method taking (arg)" do
+ obj = Object.new
+ obj.singleton_class.class_exec do
+ def single(arg)
+ arg
+ end
+ end
+
+ h = { a: 1 }
+ args = mark(**h)
+ marked = args.last
+ Hash.ruby2_keywords_hash?(marked).should == true
+
+ after_usage = obj.single(*args)
+ after_usage.should == h
+ after_usage.should_not.equal?(h)
+ after_usage.should_not.equal?(marked)
+ Hash.ruby2_keywords_hash?(after_usage).should == false
+ Hash.ruby2_keywords_hash?(marked).should == true
+ end
+
+ it "makes a copy and unmark the Hash when calling a method taking (**kw)" do
+ obj = Object.new
+ obj.singleton_class.class_exec do
+ def kwargs(**kw)
+ kw
+ end
+ end
+
+ h = { a: 1 }
+ args = mark(**h)
+ marked = args.last
+ Hash.ruby2_keywords_hash?(marked).should == true
+
+ after_usage = obj.kwargs(*args)
+ after_usage.should == h
+ after_usage.should_not.equal?(h)
+ after_usage.should_not.equal?(marked)
+ Hash.ruby2_keywords_hash?(after_usage).should == false
+ Hash.ruby2_keywords_hash?(marked).should == true
+ end
+
+ it "makes a copy and unmark the Hash when calling a method taking (*args)" do
+ obj = Object.new
+ obj.singleton_class.class_exec do
+ def splat(*args)
+ args.last
+ end
+
+ def splat1(arg, *args)
+ args.last
+ end
+
+ def proc_call(*args)
+ -> *a { a.last }.call(*args)
+ end
+ end
+
+ h = { a: 1 }
+ args = mark(**h)
+ marked = args.last
+ Hash.ruby2_keywords_hash?(marked).should == true
+
+ after_usage = obj.splat(*args)
+ after_usage.should == h
+ after_usage.should_not.equal?(h)
+ after_usage.should_not.equal?(marked)
+ Hash.ruby2_keywords_hash?(after_usage).should == false
+ Hash.ruby2_keywords_hash?(marked).should == true
+
+ args = mark(1, **h)
+ marked = args.last
+ after_usage = obj.splat1(*args)
+ after_usage.should == h
+ after_usage.should_not.equal?(h)
+ after_usage.should_not.equal?(marked)
+ Hash.ruby2_keywords_hash?(after_usage).should == false
+ Hash.ruby2_keywords_hash?(marked).should == true
+
+ args = mark(**h)
+ marked = args.last
+ after_usage = obj.proc_call(*args)
+ after_usage.should == h
+ after_usage.should_not.equal?(h)
+ after_usage.should_not.equal?(marked)
+ Hash.ruby2_keywords_hash?(after_usage).should == false
+ Hash.ruby2_keywords_hash?(marked).should == true
+
+ args = mark(**h)
+ marked = args.last
+ after_usage = obj.send(:splat, *args)
+ after_usage.should == h
+ after_usage.should_not.equal?(h)
+ after_usage.should_not.equal?(marked)
+ Hash.ruby2_keywords_hash?(after_usage).should == false
+ Hash.ruby2_keywords_hash?(marked).should == true
+ end
+
+ it "applies to the underlying method and applies across aliasing" do
+ obj = Object.new
+
+ obj.singleton_class.class_exec do
+ def foo(*a) a.last end
+ alias_method :bar, :foo
+ ruby2_keywords :foo
+
+ def baz(*a) a.last end
+ ruby2_keywords :baz
+ alias_method :bob, :baz
+ end
+
+ last = obj.foo(1, 2, a: "a")
+ Hash.ruby2_keywords_hash?(last).should == true
+
+ last = obj.bar(1, 2, a: "a")
+ Hash.ruby2_keywords_hash?(last).should == true
+
+ last = obj.baz(1, 2, a: "a")
+ Hash.ruby2_keywords_hash?(last).should == true
+
+ last = obj.bob(1, 2, a: "a")
+ Hash.ruby2_keywords_hash?(last).should == true
+ end
+
+ it "returns nil" do
+ obj = Object.new
+
+ obj.singleton_class.class_exec do
+ def foo(*a) end
+
+ ruby2_keywords(:foo).should == nil
+ end
+ end
+
+ it "raises NameError when passed not existing method name" do
+ obj = Object.new
+
+ -> {
+ obj.singleton_class.class_exec do
+ ruby2_keywords :not_existing
+ end
+ }.should.raise(NameError, /undefined method [`']not_existing'/)
+ end
+
+ it "accepts String as well" do
+ obj = Object.new
+
+ obj.singleton_class.class_exec do
+ def foo(*a) a.last end
+ ruby2_keywords "foo"
+ end
+
+ last = obj.foo(1, 2, a: "a")
+ Hash.ruby2_keywords_hash?(last).should == true
+ end
+
+ it "raises TypeError when passed not Symbol or String" do
+ obj = Object.new
+
+ -> {
+ obj.singleton_class.class_exec do
+ ruby2_keywords Object.new
+ end
+ }.should.raise(TypeError, /is not a symbol nor a string/)
+ end
+
+ it "prints warning when a method does not accept argument splat" do
+ obj = Object.new
+ def obj.foo(a, b, c) end
+
+ -> {
+ obj.singleton_class.class_exec do
+ ruby2_keywords :foo
+ end
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+
+ it "prints warning when a method accepts keywords" do
+ obj = Object.new
+ def obj.foo(*a, b:) end
+
+ -> {
+ obj.singleton_class.class_exec do
+ ruby2_keywords :foo
+ end
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+
+ it "prints warning when a method accepts keyword splat" do
+ obj = Object.new
+ def obj.foo(*a, **b) end
+
+ -> {
+ obj.singleton_class.class_exec do
+ ruby2_keywords :foo
+ end
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+
+ ruby_version_is "4.0" do
+ it "prints warning when a method accepts post arguments" do
+ obj = Object.new
+ def obj.foo(*a, b) end
+
+ -> {
+ obj.singleton_class.class_exec do
+ ruby2_keywords :foo
+ end
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+ end
+end
diff --git a/spec/ruby/core/module/set_temporary_name_spec.rb b/spec/ruby/core/module/set_temporary_name_spec.rb
new file mode 100644
index 0000000000..7c159121fa
--- /dev/null
+++ b/spec/ruby/core/module/set_temporary_name_spec.rb
@@ -0,0 +1,145 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_temporary_name'
+
+describe "Module#set_temporary_name" do
+ it "can assign a temporary name" do
+ m = Module.new
+ m.name.should == nil
+
+ m.set_temporary_name("fake_name")
+ m.name.should == "fake_name"
+
+ m.set_temporary_name(nil)
+ m.name.should == nil
+ end
+
+ it "returns self" do
+ m = Module.new
+ m.set_temporary_name("fake_name").should.equal? m
+ end
+
+ it "can assign a temporary name which is not a valid constant path" do
+ m = Module.new
+
+ m.set_temporary_name("name")
+ m.name.should == "name"
+
+ m.set_temporary_name("Template['foo.rb']")
+ m.name.should == "Template['foo.rb']"
+
+ m.set_temporary_name("a::B")
+ m.name.should == "a::B"
+
+ m.set_temporary_name("A::b")
+ m.name.should == "A::b"
+
+ m.set_temporary_name("A::B::")
+ m.name.should == "A::B::"
+
+ m.set_temporary_name("A::::B")
+ m.name.should == "A::::B"
+
+ m.set_temporary_name("A=")
+ m.name.should == "A="
+ end
+
+ it "can't assign empty string as name" do
+ m = Module.new
+ -> { m.set_temporary_name("") }.should.raise(ArgumentError, "empty class/module name")
+ end
+
+ it "can't assign a constant name as a temporary name" do
+ m = Module.new
+ -> { m.set_temporary_name("Object") }.should.raise(ArgumentError, "the temporary name must not be a constant path to avoid confusion")
+ end
+
+ it "can't assign a constant path as a temporary name" do
+ m = Module.new
+ -> { m.set_temporary_name("A::B") }.should.raise(ArgumentError, "the temporary name must not be a constant path to avoid confusion")
+ -> { m.set_temporary_name("::A") }.should.raise(ArgumentError, "the temporary name must not be a constant path to avoid confusion")
+ -> { m.set_temporary_name("::A::B") }.should.raise(ArgumentError, "the temporary name must not be a constant path to avoid confusion")
+ end
+
+ it "can't assign name to permanent module" do
+ -> { Object.set_temporary_name("fake_name") }.should.raise(RuntimeError, "can't change permanent name")
+ end
+
+ it "can assign a temporary name to a module nested into an anonymous module" do
+ m = Module.new
+ module m::N; end
+ m::N.name.should =~ /\A#<Module:0x\h+>::N\z/
+
+ m::N.set_temporary_name("fake_name")
+ m::N.name.should == "fake_name"
+
+ m::N.set_temporary_name(nil)
+ m::N.name.should == nil
+ end
+
+ it "discards a temporary name when an outer anonymous module gets a permanent name" do
+ m = Module.new
+ module m::N; end
+
+ m::N.set_temporary_name("fake_name")
+ m::N.name.should == "fake_name"
+
+ ModuleSpecs::SetTemporaryNameSpec::M = m
+ m::N.name.should == "ModuleSpecs::SetTemporaryNameSpec::M::N"
+ ModuleSpecs::SetTemporaryNameSpec.send :remove_const, :M
+ end
+
+ it "can update the name when assigned to a constant" do
+ m = Module.new
+ m::N = Module.new
+ m::N.name.should =~ /\A#<Module:0x\h+>::N\z/
+ m::N.set_temporary_name(nil)
+
+ m::M = m::N
+ m::M.name.should =~ /\A#<Module:0x\h+>::M\z/m
+ end
+
+ it "can reassign a temporary name repeatedly" do
+ m = Module.new
+
+ m.set_temporary_name("fake_name")
+ m.name.should == "fake_name"
+
+ m.set_temporary_name("fake_name_2")
+ m.name.should == "fake_name_2"
+ end
+
+ ruby_bug "#21094", ""..."4.0" do
+ it "also updates a name of a nested module" do
+ m = Module.new
+ m::N = Module.new
+ m::N.name.should =~ /\A#<Module:0x\h+>::N\z/
+
+ m.set_temporary_name "m"
+ m::N.name.should == "m::N"
+
+ m.set_temporary_name nil
+ m::N.name.should == nil
+ end
+ end
+
+ it "keeps temporary name when assigned in an anonymous module" do
+ outer = Module.new
+ m = Module.new
+ m.set_temporary_name "m"
+ m.name.should == "m"
+ outer::M = m
+ m.name.should == "m"
+ m.inspect.should == "m"
+ end
+
+ it "keeps temporary name when assigned in an anonymous module and nested before" do
+ outer = Module.new
+ m = Module.new
+ outer::A = m
+ m.set_temporary_name "m"
+ m.name.should == "m"
+ outer::M = m
+ m.name.should == "m"
+ m.inspect.should == "m"
+ end
+end
diff --git a/spec/ruby/core/module/shared/attr_added.rb b/spec/ruby/core/module/shared/attr_added.rb
new file mode 100644
index 0000000000..ce832cdcff
--- /dev/null
+++ b/spec/ruby/core/module/shared/attr_added.rb
@@ -0,0 +1,34 @@
+describe :module_attr_added, shared: true do
+ it "calls method_added for normal classes" do
+ ScratchPad.record []
+
+ cls = Class.new do
+ class << self
+ def method_added(name)
+ ScratchPad.recorded << name
+ end
+ end
+ end
+
+ cls.send(@method, :foo)
+
+ ScratchPad.recorded.each {|name| name.to_s.should =~ /foo[=]?/}
+ end
+
+ it "calls singleton_method_added for singleton classes" do
+ ScratchPad.record []
+ cls = Class.new do
+ class << self
+ def singleton_method_added(name)
+ # called for this def so ignore it
+ return if name == :singleton_method_added
+ ScratchPad.recorded << name
+ end
+ end
+ end
+
+ cls.singleton_class.send(@method, :foo)
+
+ ScratchPad.recorded.each {|name| name.to_s.should =~ /foo[=]?/}
+ end
+end
diff --git a/spec/ruby/core/module/shared/class_eval.rb b/spec/ruby/core/module/shared/class_eval.rb
new file mode 100644
index 0000000000..ee2860449a
--- /dev/null
+++ b/spec/ruby/core/module/shared/class_eval.rb
@@ -0,0 +1,172 @@
+describe :module_class_eval, shared: true do
+ # TODO: This should probably be replaced with a "should behave like" that uses
+ # the many scoping/binding specs from kernel/eval_spec, since most of those
+ # behaviors are the same for instance_eval. See also module_eval/class_eval.
+
+ it "evaluates a given string in the context of self" do
+ ModuleSpecs.send(@method, "self").should == ModuleSpecs
+ ModuleSpecs.send(@method, "1 + 1").should == 2
+ end
+
+ it "does not add defined methods to other classes" do
+ FalseClass.send(@method) do
+ def foo
+ 'foo'
+ end
+ end
+ -> {42.foo}.should.raise(NoMethodError)
+ end
+
+ it "resolves constants in the caller scope" do
+ ModuleSpecs::ClassEvalTest.get_constant_from_scope.should == ModuleSpecs::Lookup
+ end
+
+ it "resolves constants in the caller scope ignoring send" do
+ ModuleSpecs::ClassEvalTest.get_constant_from_scope_with_send(@method).should == ModuleSpecs::Lookup
+ end
+
+ it "resolves constants in the receiver's scope" do
+ ModuleSpecs.send(@method, "Lookup").should == ModuleSpecs::Lookup
+ ModuleSpecs.send(@method, "Lookup::LOOKIE").should == ModuleSpecs::Lookup::LOOKIE
+ end
+
+ it "defines constants in the receiver's scope" do
+ ModuleSpecs.send(@method, "module NewEvaluatedModule;end")
+ ModuleSpecs.const_defined?(:NewEvaluatedModule, false).should == true
+ end
+
+ it "evaluates a given block in the context of self" do
+ ModuleSpecs.send(@method) { self }.should == ModuleSpecs
+ ModuleSpecs.send(@method) { 1 + 1 }.should == 2
+ end
+
+ it "passes the module as the first argument of the block" do
+ given = nil
+ ModuleSpecs.send(@method) do |block_parameter|
+ given = block_parameter
+ end
+ given.should.equal? ModuleSpecs
+ end
+
+ it "uses the optional filename and lineno parameters for error messages" do
+ ModuleSpecs.send(@method, "[__FILE__, __LINE__]", "test", 102).should == ["test", 102]
+ end
+
+ it "uses the caller location as default filename" do
+ ModuleSpecs.send(@method, "[__FILE__, __LINE__]").should == ["(eval at #{__FILE__}:#{__LINE__})", 1]
+ end
+
+ it "converts a non-string filename to a string using to_str" do
+ (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__)
+ ModuleSpecs.send(@method, "1+1", file)
+
+ (file = mock(__FILE__)).should_receive(:to_str).and_return(__FILE__)
+ ModuleSpecs.send(@method, "1+1", file, 15)
+ end
+
+ it "raises a TypeError when the given filename can't be converted to string using to_str" do
+ (file = mock('123')).should_receive(:to_str).and_return(123)
+ -> { ModuleSpecs.send(@method, "1+1", file) }.should raise_consistent_error(TypeError, /can't convert MockObject into String/)
+ end
+
+ it "converts non string eval-string to string using to_str" do
+ (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1")
+ ModuleSpecs.send(@method, o).should == 2
+
+ (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1")
+ ModuleSpecs.send(@method, o, "file.rb").should == 2
+
+ (o = mock('1 + 1')).should_receive(:to_str).and_return("1 + 1")
+ ModuleSpecs.send(@method, o, "file.rb", 15).should == 2
+ end
+
+ it "raises a TypeError when the given eval-string can't be converted to string using to_str" do
+ o = mock('x')
+ -> { ModuleSpecs.send(@method, o) }.should.raise(TypeError, "no implicit conversion of MockObject into String")
+
+ (o = mock('123')).should_receive(:to_str).and_return(123)
+ -> { ModuleSpecs.send(@method, o) }.should raise_consistent_error(TypeError, /can't convert MockObject into String/)
+ end
+
+ it "raises an ArgumentError when no arguments and no block are given" do
+ -> { ModuleSpecs.send(@method) }.should.raise(ArgumentError, "wrong number of arguments (given 0, expected 1..3)")
+ end
+
+ it "raises an ArgumentError when more than 3 arguments are given" do
+ -> {
+ ModuleSpecs.send(@method, "1 + 1", "some file", 0, "bogus")
+ }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 1..3)")
+ end
+
+ it "raises an ArgumentError when a block and normal arguments are given" do
+ -> {
+ ModuleSpecs.send(@method, "1 + 1") { 1 + 1 }
+ }.should.raise(ArgumentError, "wrong number of arguments (given 1, expected 0)")
+ end
+
+ # This case was found because Rubinius was caching the compiled
+ # version of the string and not duping the methods within the
+ # eval, causing the method addition to change the static scope
+ # of the shared CompiledCode.
+ it "adds methods respecting the lexical constant scope" do
+ code = "def self.attribute; C; end"
+
+ a = Class.new do
+ self::C = "A"
+ end
+
+ b = Class.new do
+ self::C = "B"
+ end
+
+ a.send @method, code
+ b.send @method, code
+
+ a.attribute.should == "A"
+ b.attribute.should == "B"
+ end
+
+ it "activates refinements from the eval scope" do
+ refinery = Module.new do
+ refine ModuleSpecs::NamedClass do
+ def foo
+ "bar"
+ end
+ end
+ end
+
+ mid = @method
+ result = nil
+
+ Class.new do
+ using refinery
+
+ result = send(mid, "ModuleSpecs::NamedClass.new.foo")
+ end
+
+ result.should == "bar"
+ end
+
+ it "activates refinements from the eval scope with block" do
+ refinery = Module.new do
+ refine ModuleSpecs::NamedClass do
+ def foo
+ "bar"
+ end
+ end
+ end
+
+ mid = @method
+ result = nil
+
+ Class.new do
+ using refinery
+
+ result = send(mid) do
+ ModuleSpecs::NamedClass.new.foo
+ end
+ end
+
+ result.should == "bar"
+ end
+end
diff --git a/spec/ruby/core/module/shared/class_exec.rb b/spec/ruby/core/module/shared/class_exec.rb
new file mode 100644
index 0000000000..e51af1966d
--- /dev/null
+++ b/spec/ruby/core/module/shared/class_exec.rb
@@ -0,0 +1,35 @@
+describe :module_class_exec, shared: true do
+ it "does not add defined methods to other classes" do
+ FalseClass.send(@method) do
+ def foo
+ 'foo'
+ end
+ end
+ -> {42.foo}.should.raise(NoMethodError)
+ end
+
+ it "defines method in the receiver's scope" do
+ ModuleSpecs::Subclass.send(@method) { def foo; end }
+ ModuleSpecs::Subclass.new.respond_to?(:foo).should == true
+ end
+
+ it "evaluates a given block in the context of self" do
+ ModuleSpecs::Subclass.send(@method) { self }.should == ModuleSpecs::Subclass
+ ModuleSpecs::Subclass.new.send(@method) { 1 + 1 }.should == 2
+ end
+
+ it "raises a LocalJumpError when no block is given" do
+ -> { ModuleSpecs::Subclass.send(@method) }.should.raise(LocalJumpError)
+ end
+
+ it "passes arguments to the block" do
+ a = ModuleSpecs::Subclass
+ a.send(@method, 1) { |b| b }.should.equal?(1)
+ end
+
+ describe "with optional argument" do
+ it "does not destructure a single array argument" do
+ ModuleSpecs::Subclass.send(@method, [1, 2, 3]) { |a = 99| a }.should == [1, 2, 3]
+ end
+ end
+end
diff --git a/spec/ruby/core/module/shared/equal_value.rb b/spec/ruby/core/module/shared/equal_value.rb
new file mode 100644
index 0000000000..f1227d873c
--- /dev/null
+++ b/spec/ruby/core/module/shared/equal_value.rb
@@ -0,0 +1,14 @@
+describe :module_equal, shared: true do
+ it "returns true if self and the given module are the same" do
+ ModuleSpecs.send(@method, ModuleSpecs).should == true
+ ModuleSpecs::Child.send(@method, ModuleSpecs::Child).should == true
+ ModuleSpecs::Parent.send(@method, ModuleSpecs::Parent).should == true
+ ModuleSpecs::Basic.send(@method, ModuleSpecs::Basic).should == true
+ ModuleSpecs::Super.send(@method, ModuleSpecs::Super).should == true
+
+ ModuleSpecs::Child.send(@method, ModuleSpecs).should == false
+ ModuleSpecs::Child.send(@method, ModuleSpecs::Parent).should == false
+ ModuleSpecs::Child.send(@method, ModuleSpecs::Basic).should == false
+ ModuleSpecs::Child.send(@method, ModuleSpecs::Super).should == false
+ end
+end
diff --git a/spec/ruby/core/module/shared/set_visibility.rb b/spec/ruby/core/module/shared/set_visibility.rb
new file mode 100644
index 0000000000..38cc2ad260
--- /dev/null
+++ b/spec/ruby/core/module/shared/set_visibility.rb
@@ -0,0 +1,184 @@
+# -*- encoding: us-ascii -*-
+
+describe :set_visibility, shared: true do
+ it "is a private method" do
+ Module.private_instance_methods(false).should.include?(@method)
+ end
+
+ describe "with argument" do
+ describe "one or more arguments" do
+ it "sets visibility of given method names" do
+ visibility = @method
+ old_visibility = [:protected, :private].find {|vis| vis != visibility }
+
+ mod = Module.new {
+ send old_visibility
+ def test1() end
+ def test2() end
+ send visibility, :test1, :test2
+ }
+ mod.send(:"#{visibility}_instance_methods", false).should.include?(:test1)
+ mod.send(:"#{visibility}_instance_methods", false).should.include?(:test2)
+ end
+ end
+
+ describe "array as a single argument" do
+ it "sets visibility of given method names" do
+ visibility = @method
+ old_visibility = [:protected, :private].find {|vis| vis != visibility }
+
+ mod = Module.new {
+ send old_visibility
+ def test1() end
+ def test2() end
+ send visibility, [:test1, :test2]
+ }
+ mod.send(:"#{visibility}_instance_methods", false).should.include?(:test1)
+ mod.send(:"#{visibility}_instance_methods", false).should.include?(:test2)
+ end
+ end
+
+ it "does not clone method from the ancestor when setting to the same visibility in a child" do
+ visibility = @method
+ parent = Module.new {
+ def test_method; end
+ send(visibility, :test_method)
+ }
+
+ child = Module.new {
+ include parent
+ send(visibility, :test_method)
+ }
+
+ child.send(:"#{visibility}_instance_methods", false).should_not.include?(:test_method)
+ end
+ end
+
+ describe "without arguments" do
+ it "sets visibility to following method definitions" do
+ visibility = @method
+ mod = Module.new {
+ send visibility
+
+ def test1() end
+ def test2() end
+ }
+
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test1)
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test2)
+ end
+
+ it "stops setting visibility if the body encounters other visibility setters without arguments" do
+ visibility = @method
+ new_visibility = nil
+ mod = Module.new {
+ send visibility
+ new_visibility = [:protected, :private].find {|vis| vis != visibility }
+ send new_visibility
+ def test1() end
+ }
+
+ mod.send(:"#{new_visibility}_instance_methods", false).should.include?(:test1)
+ end
+
+ it "continues setting visibility if the body encounters other visibility setters with arguments" do
+ visibility = @method
+ mod = Module.new {
+ send visibility
+ def test1() end
+ send([:protected, :private].find {|vis| vis != visibility }, :test1)
+ def test2() end
+ }
+
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test2)
+ end
+
+ it "does not affect module_evaled method definitions when itself is outside the eval" do
+ visibility = @method
+ mod = Module.new {
+ send visibility
+
+ module_eval { def test1() end }
+ module_eval " def test2() end "
+ }
+
+ mod.public_instance_methods(false).should.include?(:test1)
+ mod.public_instance_methods(false).should.include?(:test2)
+ end
+
+ it "does not affect outside method definitions when itself is inside a module_eval" do
+ visibility = @method
+ mod = Module.new {
+ module_eval { send visibility }
+
+ def test1() end
+ }
+
+ mod.public_instance_methods(false).should.include?(:test1)
+ end
+
+ it "affects normally if itself and method definitions are inside a module_eval" do
+ visibility = @method
+ mod = Module.new {
+ module_eval {
+ send visibility
+
+ def test1() end
+ }
+ }
+
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test1)
+ end
+
+ it "does not affect method definitions when itself is inside an eval and method definitions are outside" do
+ visibility = @method
+ initialized_visibility = [:public, :protected, :private].find {|sym| sym != visibility }
+ mod = Module.new {
+ send initialized_visibility
+ eval visibility.to_s
+
+ def test1() end
+ }
+
+ mod.send(:"#{initialized_visibility}_instance_methods", false).should.include?(:test1)
+ end
+
+ it "affects evaled method definitions when itself is outside the eval" do
+ visibility = @method
+ mod = Module.new {
+ send visibility
+
+ eval "def test1() end"
+ }
+
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test1)
+ end
+
+ it "affects normally if itself and following method definitions are inside a eval" do
+ visibility = @method
+ mod = Module.new {
+ eval <<-CODE
+ #{visibility}
+
+ def test1() end
+ CODE
+ }
+
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test1)
+ end
+
+ describe "within a closure" do
+ it "sets the visibility outside the closure" do
+ visibility = @method
+ mod = Module.new {
+ 1.times {
+ send visibility
+ }
+ def test1() end
+ }
+
+ mod.send(:"#{@method}_instance_methods", false).should.include?(:test1)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/module/singleton_class_spec.rb b/spec/ruby/core/module/singleton_class_spec.rb
new file mode 100644
index 0000000000..052755b73b
--- /dev/null
+++ b/spec/ruby/core/module/singleton_class_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+
+describe "Module#singleton_class?" do
+ it "returns true for singleton classes" do
+ xs = self.singleton_class
+ xs.should.singleton_class?
+ end
+
+ it "returns false for other classes" do
+ c = Class.new
+ c.should_not.singleton_class?
+ end
+
+ describe "with singleton values" do
+ it "returns false for nil's singleton class" do
+ NilClass.should_not.singleton_class?
+ end
+
+ it "returns false for true's singleton class" do
+ TrueClass.should_not.singleton_class?
+ end
+
+ it "returns false for false's singleton class" do
+ FalseClass.should_not.singleton_class?
+ end
+ end
+end
diff --git a/spec/ruby/core/module/to_s_spec.rb b/spec/ruby/core/module/to_s_spec.rb
new file mode 100644
index 0000000000..83c0ae0825
--- /dev/null
+++ b/spec/ruby/core/module/to_s_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#to_s" do
+ it 'returns the name of the module if it has a name' do
+ Enumerable.to_s.should == 'Enumerable'
+ String.to_s.should == 'String'
+ end
+
+ it "returns the full constant path leading to the module" do
+ ModuleSpecs::LookupMod.to_s.should == "ModuleSpecs::LookupMod"
+ end
+
+ it "works with an anonymous module" do
+ m = Module.new
+ m.to_s.should =~ /\A#<Module:0x\h+>\z/
+ end
+
+ it "works with an anonymous class" do
+ c = Class.new
+ c.to_s.should =~ /\A#<Class:0x\h+>\z/
+ end
+
+ it 'for the singleton class of an object of an anonymous class' do
+ klass = Class.new
+ obj = klass.new
+ sclass = obj.singleton_class
+ sclass.to_s.should == "#<Class:#{obj}>"
+ sclass.to_s.should =~ /\A#<Class:#<#{klass}:0x\h+>>\z/
+ sclass.to_s.should =~ /\A#<Class:#<#<Class:0x\h+>:0x\h+>>\z/
+ end
+
+ it 'for a singleton class of a module includes the module name' do
+ ModuleSpecs.singleton_class.to_s.should == '#<Class:ModuleSpecs>'
+ end
+
+ it 'for a metaclass includes the class name' do
+ ModuleSpecs::NamedClass.singleton_class.to_s.should == '#<Class:ModuleSpecs::NamedClass>'
+ end
+
+ it 'for objects includes class name and object ID' do
+ obj = ModuleSpecs::NamedClass.new
+ obj.singleton_class.to_s.should =~ /\A#<Class:#<ModuleSpecs::NamedClass:0x\h+>>\z/
+ end
+
+ it "always show the refinement name, even if the module is named" do
+ module ModuleSpecs::RefinementInspect
+ R = refine String do
+ end
+ end
+
+ ModuleSpecs::RefinementInspect::R.name.should == 'ModuleSpecs::RefinementInspect::R'
+ ModuleSpecs::RefinementInspect::R.to_s.should == '#<refinement:String@ModuleSpecs::RefinementInspect>'
+ ensure
+ ModuleSpecs.send(:remove_const, :RefinementInspect)
+ end
+
+ it 'does not call #inspect or #to_s for singleton classes' do
+ klass = Class.new
+ obj = klass.new
+ def obj.to_s
+ "to_s"
+ end
+ def obj.inspect
+ "inspect"
+ end
+ sclass = obj.singleton_class
+ sclass.to_s.should =~ /\A#<Class:#<#{Regexp.escape klass.to_s}:0x\h+>>\z/
+ end
+end
diff --git a/spec/ruby/core/module/undef_method_spec.rb b/spec/ruby/core/module/undef_method_spec.rb
new file mode 100644
index 0000000000..d77640cb7e
--- /dev/null
+++ b/spec/ruby/core/module/undef_method_spec.rb
@@ -0,0 +1,181 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+module ModuleSpecs
+ class Parent
+ def method_to_undef() 1 end
+ def another_method_to_undef() 1 end
+ end
+
+ class Ancestor
+ def method_to_undef() 1 end
+ def another_method_to_undef() 1 end
+ end
+end
+
+describe "Module#undef_method" do
+ before :each do
+ @module = Module.new { def method_to_undef; end }
+ end
+
+ it "is a public method" do
+ Module.public_instance_methods(false).should.include?(:undef_method)
+ end
+
+ it "requires multiple arguments" do
+ Module.instance_method(:undef_method).arity.should < 0
+ end
+
+ it "allows multiple methods to be removed at once" do
+ klass = Class.new do
+ def method_to_undef() 1 end
+ def another_method_to_undef() 1 end
+ end
+ x = klass.new
+ klass.send(:undef_method, :method_to_undef, :another_method_to_undef)
+
+ -> { x.method_to_undef }.should.raise(NoMethodError)
+ -> { x.another_method_to_undef }.should.raise(NoMethodError)
+ end
+
+ it "does not undef any instance methods when argument not given" do
+ before = @module.instance_methods(true) + @module.private_instance_methods(true)
+ @module.send :undef_method
+ after = @module.instance_methods(true) + @module.private_instance_methods(true)
+ before.sort.should == after.sort
+ end
+
+ it "returns self" do
+ @module.send(:undef_method, :method_to_undef).should.equal?(@module)
+ end
+
+ it "raises a NameError when passed a missing name for a module" do
+ -> { @module.send :undef_method, :not_exist }.should.raise(NameError, /undefined method [`']not_exist' for module [`']#{@module}'/) { |e|
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ it "raises a NameError when passed a missing name for a class" do
+ klass = Class.new
+ -> { klass.send :undef_method, :not_exist }.should.raise(NameError, /undefined method [`']not_exist' for class [`']#{klass}'/) { |e|
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ it "raises a NameError when passed a missing name for a singleton class" do
+ klass = Class.new
+ obj = klass.new
+ sclass = obj.singleton_class
+
+ -> { sclass.send :undef_method, :not_exist }.should.raise(NameError, /undefined method [`']not_exist' for class [`']#{sclass}'/) { |e|
+ e.message.should =~ /[`']#<Class:#<#<Class:/
+
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ it "raises a NameError when passed a missing name for a metaclass" do
+ klass = String.singleton_class
+ -> { klass.send :undef_method, :not_exist }.should.raise(NameError, /undefined method [`']not_exist' for class [`']String'/) { |e|
+ # a NameError and not a NoMethodError
+ e.class.should == NameError
+ }
+ end
+
+ describe "on frozen instance" do
+ before :each do
+ @frozen = @module.dup.freeze
+ end
+
+ it "raises a FrozenError when passed a name" do
+ -> { @frozen.send :undef_method, :method_to_undef }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError when passed a missing name" do
+ -> { @frozen.send :undef_method, :not_exist }.should.raise(FrozenError)
+ end
+
+ it "raises a TypeError when passed a not name" do
+ -> { @frozen.send :undef_method, Object.new }.should.raise(TypeError)
+ end
+
+ it "does not raise exceptions when no arguments given" do
+ @frozen.send(:undef_method).should.equal?(@frozen)
+ end
+ end
+end
+
+describe "Module#undef_method with symbol" do
+ it "removes a method defined in a class" do
+ klass = Class.new do
+ def method_to_undef() 1 end
+ def another_method_to_undef() 1 end
+ end
+ x = klass.new
+
+ x.method_to_undef.should == 1
+
+ klass.send :undef_method, :method_to_undef
+
+ -> { x.method_to_undef }.should.raise(NoMethodError)
+ end
+
+ it "removes a method defined in a super class" do
+ child_class = Class.new(ModuleSpecs::Parent)
+ child = child_class.new
+ child.method_to_undef.should == 1
+
+ child_class.send :undef_method, :method_to_undef
+
+ -> { child.method_to_undef }.should.raise(NoMethodError)
+ end
+
+ it "does not remove a method defined in a super class when removed from a subclass" do
+ descendant = Class.new(ModuleSpecs::Ancestor)
+ ancestor = ModuleSpecs::Ancestor.new
+ ancestor.method_to_undef.should == 1
+
+ descendant.send :undef_method, :method_to_undef
+
+ ancestor.method_to_undef.should == 1
+ end
+end
+
+describe "Module#undef_method with string" do
+ it "removes a method defined in a class" do
+ klass = Class.new do
+ def method_to_undef() 1 end
+ def another_method_to_undef() 1 end
+ end
+ x = klass.new
+
+ x.another_method_to_undef.should == 1
+
+ klass.send :undef_method, 'another_method_to_undef'
+
+ -> { x.another_method_to_undef }.should.raise(NoMethodError)
+ end
+
+ it "removes a method defined in a super class" do
+ child_class = Class.new(ModuleSpecs::Parent)
+ child = child_class.new
+ child.another_method_to_undef.should == 1
+
+ child_class.send :undef_method, 'another_method_to_undef'
+
+ -> { child.another_method_to_undef }.should.raise(NoMethodError)
+ end
+
+ it "does not remove a method defined in a super class when removed from a subclass" do
+ descendant = Class.new(ModuleSpecs::Ancestor)
+ ancestor = ModuleSpecs::Ancestor.new
+ ancestor.another_method_to_undef.should == 1
+
+ descendant.send :undef_method, 'another_method_to_undef'
+
+ ancestor.another_method_to_undef.should == 1
+ end
+end
diff --git a/spec/ruby/core/module/undefined_instance_methods_spec.rb b/spec/ruby/core/module/undefined_instance_methods_spec.rb
new file mode 100644
index 0000000000..9f731c6adf
--- /dev/null
+++ b/spec/ruby/core/module/undefined_instance_methods_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Module#undefined_instance_methods" do
+ it "returns methods undefined in the class" do
+ methods = ModuleSpecs::UndefinedInstanceMethods::Parent.undefined_instance_methods
+ methods.should == [:undefed_method]
+ end
+
+ it "returns inherited methods undefined in the class" do
+ methods = ModuleSpecs::UndefinedInstanceMethods::Child.undefined_instance_methods
+ methods.to_set.should >= Set[:parent_method, :another_parent_method]
+ end
+
+ it "returns methods from an included module that are undefined in the class" do
+ methods = ModuleSpecs::UndefinedInstanceMethods::Grandchild.undefined_instance_methods
+ methods.should.include?(:super_included_method)
+ end
+
+ it "does not returns ancestors undefined methods" do
+ methods = ModuleSpecs::UndefinedInstanceMethods::Grandchild.undefined_instance_methods
+ methods.should_not.include?(:parent_method)
+ methods.should_not.include?(:another_parent_method)
+ end
+end
diff --git a/spec/ruby/core/module/used_refinements_spec.rb b/spec/ruby/core/module/used_refinements_spec.rb
new file mode 100644
index 0000000000..40dd4a444e
--- /dev/null
+++ b/spec/ruby/core/module/used_refinements_spec.rb
@@ -0,0 +1,85 @@
+require_relative '../../spec_helper'
+
+describe "Module.used_refinements" do
+ it "returns list of all refinements imported in the current scope" do
+ refinement_int = nil
+ refinement_str = nil
+ ScratchPad.record []
+
+ m1 = Module.new do
+ refine Integer do
+ refinement_int = self
+ end
+ end
+
+ m2 = Module.new do
+ refine String do
+ refinement_str = self
+ end
+ end
+
+ Module.new do
+ using m1
+ using m2
+
+ Module.used_refinements.each { |r| ScratchPad << r }
+ end
+
+ ScratchPad.recorded.sort_by(&:object_id).should == [refinement_int, refinement_str].sort_by(&:object_id)
+ end
+
+ it "returns empty array if does not have any refinements imported" do
+ used_refinements = nil
+
+ Module.new do
+ used_refinements = Module.used_refinements
+ end
+
+ used_refinements.should == []
+ end
+
+ it "ignores refinements imported in a module that is included into the current one" do
+ used_refinements = nil
+
+ m1 = Module.new do
+ refine Integer do
+ nil
+ end
+ end
+
+ m2 = Module.new do
+ using m1
+ end
+
+ Module.new do
+ include m2
+
+ used_refinements = Module.used_refinements
+ end
+
+ used_refinements.should == []
+ end
+
+ it "returns refinements even not defined directly in a module refinements are imported from" do
+ used_refinements = nil
+ ScratchPad.record []
+
+ m1 = Module.new do
+ refine Integer do
+ ScratchPad << self
+ end
+ end
+
+ m2 = Module.new do
+ include m1
+ end
+
+ Module.new do
+ using m2
+
+ used_refinements = Module.used_refinements
+ end
+
+ used_refinements.should == ScratchPad.recorded
+ end
+end
diff --git a/spec/ruby/core/module/using_spec.rb b/spec/ruby/core/module/using_spec.rb
new file mode 100644
index 0000000000..cff0edef28
--- /dev/null
+++ b/spec/ruby/core/module/using_spec.rb
@@ -0,0 +1,377 @@
+require_relative '../../spec_helper'
+
+describe "Module#using" do
+ it "imports class refinements from module into the current class/module" do
+ refinement = Module.new do
+ refine Integer do
+ def foo; "foo"; end
+ end
+ end
+
+ result = nil
+ Module.new do
+ using refinement
+ result = 1.foo
+ end
+
+ result.should == "foo"
+ end
+
+ it "accepts module as argument" do
+ refinement = Module.new do
+ refine Integer do
+ def foo; "foo"; end
+ end
+ end
+
+ -> {
+ Module.new do
+ using refinement
+ end
+ }.should_not.raise
+ end
+
+ it "accepts module without refinements" do
+ mod = Module.new
+
+ -> {
+ Module.new do
+ using mod
+ end
+ }.should_not.raise
+ end
+
+ it "does not accept class" do
+ klass = Class.new
+
+ -> {
+ Module.new do
+ using klass
+ end
+ }.should.raise(TypeError)
+ end
+
+ it "raises TypeError if passed something other than module" do
+ -> {
+ Module.new do
+ using "foo"
+ end
+ }.should.raise(TypeError)
+ end
+
+ it "returns self" do
+ refinement = Module.new
+
+ result = nil
+ mod = Module.new do
+ result = using refinement
+ end
+
+ result.should.equal?(mod)
+ end
+
+ it "works in classes too" do
+ refinement = Module.new do
+ refine Integer do
+ def foo; "foo"; end
+ end
+ end
+
+ result = nil
+ Class.new do
+ using refinement
+ result = 1.foo
+ end
+
+ result.should == "foo"
+ end
+
+ it "raises error in method scope" do
+ mod = Module.new do
+ def self.foo
+ using Module.new {}
+ end
+ end
+
+ -> {
+ mod.foo
+ }.should.raise(RuntimeError, /Module#using is not permitted in methods/)
+ end
+
+ it "activates refinement even for existed objects" do
+ result = nil
+
+ Module.new do
+ klass = Class.new do
+ def foo; "foo"; end
+ end
+
+ refinement = Module.new do
+ refine klass do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ obj = klass.new
+ using refinement
+ result = obj.foo
+ end
+
+ result.should == "foo from refinement"
+ end
+
+ it "activates updates when refinement reopens later" do
+ result = nil
+
+ Module.new do
+ klass = Class.new do
+ def foo; "foo"; end
+ end
+
+ refinement = Module.new do
+ refine klass do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ using refinement
+
+ refinement.class_eval do
+ refine klass do
+ def foo; "foo from reopened refinement"; end
+ end
+ end
+
+ obj = klass.new
+ result = obj.foo
+ end
+
+ result.should == "foo from reopened refinement"
+ end
+
+ describe "scope of refinement" do
+ it "is active until the end of current class/module" do
+ ScratchPad.record []
+
+ Module.new do
+ Class.new do
+ using Module.new {
+ refine String do
+ def to_s; "hello from refinement"; end
+ end
+ }
+ ScratchPad << "1".to_s
+ end
+
+ ScratchPad << "1".to_s
+ end
+
+ ScratchPad.recorded.should == ["hello from refinement", "1"]
+ end
+
+ # Refinements are lexical in scope.
+ # Refinements are only active within a scope after the call to using.
+ # Any code before the using statement will not have the refinement activated.
+ it "is not active before the `using` call" do
+ ScratchPad.record []
+
+ Module.new do
+ Class.new do
+ ScratchPad << "1".to_s
+ using Module.new {
+ refine String do
+ def to_s; "hello from refinement"; end
+ end
+ }
+ ScratchPad << "1".to_s
+ end
+ end
+
+ ScratchPad.recorded.should == ["1", "hello from refinement"]
+ end
+
+ # If you call a method that is defined outside the current scope
+ # the refinement will be deactivated
+ it "is not active for code defined outside the current scope" do
+ result = nil
+
+ Module.new do
+ klass = Class.new do
+ def foo; "foo"; end
+ end
+
+ refinement = Module.new do
+ refine klass do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ def self.call_foo(c)
+ c.foo
+ end
+
+ using refinement
+
+ result = call_foo(klass.new)
+ end
+
+ result.should == "foo"
+ end
+
+ # If a method is defined in a scope where a refinement is active
+ # the refinement will be active when the method is called.
+ it "is active for method defined in a scope wherever it's called" do
+ klass = Class.new do
+ def foo; "foo"; end
+ end
+
+ mod = Module.new do
+ refinement = Module.new do
+ refine klass do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ using refinement
+
+ def self.call_foo(c)
+ c.foo
+ end
+ end
+
+ c = klass.new
+ mod.call_foo(c).should == "foo from refinement"
+ end
+
+ it "is active for module defined via Module.new {}" do
+ refinement = Module.new do
+ refine Integer do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+
+ Module.new do
+ using refinement
+
+ Module.new do
+ result = 1.foo
+ end
+ end
+
+ result.should == "foo from refinement"
+ end
+
+ it "is active for class defined via Class.new {}" do
+ refinement = Module.new do
+ refine Integer do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ result = nil
+
+ Module.new do
+ using refinement
+
+ Class.new do
+ result = 1.foo
+ end
+ end
+
+ result.should == "foo from refinement"
+ end
+
+ it "is active for block called via instance_exec" do
+ refinement = Module.new do
+ refine Integer do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ c = Class.new do
+ using refinement
+
+ def abc
+ block = -> {
+ 1.foo
+ }
+
+ self.instance_exec(&block)
+ end
+ end
+
+ c.new.abc.should == "foo from refinement"
+ end
+
+ it "is active for block called via instance_eval" do
+ refinement = Module.new do
+ refine String do
+ def foo; "foo from refinement"; end
+ end
+ end
+
+ c = Class.new do
+ using refinement
+
+ def initialize
+ @a = +"1703"
+
+ @a.instance_eval do
+ def abc
+ "#{self}: #{self.foo}"
+ end
+ end
+ end
+
+ def abc
+ @a.abc
+ end
+ end
+
+ c.new.abc.should == "1703: foo from refinement"
+ end
+
+ it "is not active if `using` call is not evaluated" do
+ result = nil
+
+ Module.new do
+ if false
+ using Module.new {
+ refine String do
+ def to_s; "hello from refinement"; end
+ end
+ }
+ end
+ result = "1".to_s
+ end
+
+ result.should == "1"
+ end
+
+ # The refinements in module are not activated automatically
+ # if the class is reopened later
+ it "is not active when class/module reopens" do
+ refinement = Module.new do
+ refine String do
+ def to_s
+ "hello from refinement"
+ end
+ end
+ end
+
+ result = []
+ klass = Class.new do
+ using refinement
+ result << "1".to_s
+ end
+
+ klass.class_eval do
+ result << "1".to_s
+ end
+
+ result.should == ["hello from refinement", "1"]
+ end
+ end
+end
diff --git a/spec/ruby/core/mutex/lock_spec.rb b/spec/ruby/core/mutex/lock_spec.rb
new file mode 100644
index 0000000000..4fee29091a
--- /dev/null
+++ b/spec/ruby/core/mutex/lock_spec.rb
@@ -0,0 +1,92 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#lock" do
+ it "returns self" do
+ m = Mutex.new
+ m.lock.should == m
+ m.unlock
+ end
+
+ it "blocks the caller if already locked" do
+ m = Mutex.new
+ m.lock
+ -> { m.lock }.should block_caller
+ end
+
+ it "does not block the caller if not locked" do
+ m = Mutex.new
+ -> { m.lock }.should_not block_caller
+ end
+
+ # Unable to find a specific ticket but behavior change may be
+ # related to this ML thread.
+ it "raises a deadlock ThreadError when used recursively" do
+ m = Mutex.new
+ m.lock
+ -> {
+ m.lock
+ }.should.raise(ThreadError, /deadlock/)
+ end
+
+ it "raises a deadlock ThreadError when multiple fibers from the same thread try to lock" do
+ m = Mutex.new
+
+ m.lock
+ f0 = Fiber.new do
+ m.lock
+ end
+ -> { f0.resume }.should.raise(ThreadError, /deadlock/)
+
+ m.unlock
+ f1 = Fiber.new do
+ m.lock
+ Fiber.yield
+ end
+ f2 = Fiber.new do
+ m.lock
+ end
+ f1.resume
+ -> { f2.resume }.should.raise(ThreadError, /deadlock/)
+ end
+
+ it "does not raise deadlock if a fiber's attempt to lock was interrupted" do
+ lock = Mutex.new
+ main = Thread.current
+
+ t2 = nil
+ t1 = Thread.new do
+ loop do
+ # interrupt fiber below looping on synchronize
+ sleep 0.01
+ t2.raise if t2
+ end
+ end
+
+ # loop ten times to try to handle the interrupt during synchronize
+ t2 = Thread.new do
+ 10.times do
+ Fiber.new do
+ begin
+ loop { lock.synchronize {} }
+ rescue RuntimeError
+ end
+ end.resume
+
+ Fiber.new do
+ -> do
+ lock.synchronize {}
+ end.should_not.raise(ThreadError)
+ end.resume
+ rescue RuntimeError
+ retry
+ end
+ end
+ t2.join
+ ensure
+ t1.kill rescue nil
+ t2.kill rescue nil
+
+ t1.join
+ t2.join
+ end
+end
diff --git a/spec/ruby/core/mutex/locked_spec.rb b/spec/ruby/core/mutex/locked_spec.rb
new file mode 100644
index 0000000000..1818cdb4f3
--- /dev/null
+++ b/spec/ruby/core/mutex/locked_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#locked?" do
+ it "returns true if locked" do
+ m = Mutex.new
+ m.lock
+ m.locked?.should == true
+ end
+
+ it "returns false if unlocked" do
+ m = Mutex.new
+ m.locked?.should == false
+ end
+
+ it "returns the status of the lock" do
+ m1 = Mutex.new
+ m2 = Mutex.new
+
+ m2.lock # hold th with only m1 locked
+ m1_locked = false
+
+ th = Thread.new do
+ m1.lock
+ m1_locked = true
+ m2.lock
+ end
+
+ Thread.pass until m1_locked
+
+ m1.locked?.should == true
+ m2.unlock # release th
+ th.join
+ # A Thread releases its locks upon termination
+ m1.locked?.should == false
+ end
+end
diff --git a/spec/ruby/core/mutex/owned_spec.rb b/spec/ruby/core/mutex/owned_spec.rb
new file mode 100644
index 0000000000..ea7d5faf1c
--- /dev/null
+++ b/spec/ruby/core/mutex/owned_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#owned?" do
+ describe "when unlocked" do
+ it "returns false" do
+ m = Mutex.new
+ m.owned?.should == false
+ end
+ end
+
+ describe "when locked by the current thread" do
+ it "returns true" do
+ m = Mutex.new
+ m.lock
+ m.owned?.should == true
+ end
+ end
+
+ describe "when locked by another thread" do
+ before :each do
+ @checked = false
+ end
+
+ after :each do
+ @checked = true
+ @th.join
+ end
+
+ it "returns false" do
+ m = Mutex.new
+ locked = false
+
+ @th = Thread.new do
+ m.lock
+ locked = true
+ Thread.pass until @checked
+ end
+
+ Thread.pass until locked
+ m.owned?.should == false
+ end
+ end
+
+ it "is held per Fiber" do
+ m = Mutex.new
+ m.lock
+
+ Fiber.new do
+ m.locked?.should == true
+ m.owned?.should == false
+ end.resume
+ end
+end
diff --git a/spec/ruby/core/mutex/sleep_spec.rb b/spec/ruby/core/mutex/sleep_spec.rb
new file mode 100644
index 0000000000..71b089d251
--- /dev/null
+++ b/spec/ruby/core/mutex/sleep_spec.rb
@@ -0,0 +1,111 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#sleep" do
+ describe "when not locked by the current thread" do
+ it "raises a ThreadError" do
+ m = Mutex.new
+ -> { m.sleep }.should.raise(ThreadError)
+ end
+
+ it "raises an ArgumentError if passed a negative duration" do
+ m = Mutex.new
+ -> { m.sleep(-0.1) }.should.raise(ArgumentError)
+ -> { m.sleep(-1) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "raises an ArgumentError if passed a negative duration" do
+ m = Mutex.new
+ m.lock
+ -> { m.sleep(-0.1) }.should.raise(ArgumentError)
+ -> { m.sleep(-1) }.should.raise(ArgumentError)
+ end
+
+ it "pauses execution for approximately the duration requested" do
+ m = Mutex.new
+ m.lock
+ duration = 0.001
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ m.sleep duration
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ (now - start).should >= 0
+ (now - start).should < (duration + TIME_TOLERANCE)
+ end
+
+ it "unlocks the mutex while sleeping" do
+ m = Mutex.new
+ locked = false
+ th = Thread.new { m.lock; locked = true; m.sleep }
+ Thread.pass until locked
+ Thread.pass until th.stop?
+ m.locked?.should == false
+ th.run
+ th.join
+ end
+
+ it "relocks the mutex when woken" do
+ m = Mutex.new
+ m.lock
+ m.sleep(0.001)
+ m.locked?.should == true
+ end
+
+ it "relocks the mutex when woken by an exception being raised" do
+ m = Mutex.new
+ locked = false
+ th = Thread.new do
+ m.lock
+ locked = true
+ begin
+ m.sleep
+ rescue Exception
+ m.locked?
+ end
+ end
+ Thread.pass until locked
+ Thread.pass until th.stop?
+ th.raise(Exception)
+ th.value.should == true
+ end
+
+ it "returns the rounded number of seconds asleep" do
+ m = Mutex.new
+ locked = false
+ th = Thread.start do
+ m.lock
+ locked = true
+ m.sleep
+ end
+ Thread.pass until locked
+ Thread.pass until th.stop?
+ th.wakeup
+ th.value.should.is_a?(Integer)
+ end
+
+ it "accepts nil as a sleep duration" do
+ m = Mutex.new
+ -> {
+ m.lock
+ m.sleep(nil)
+ }.should block_caller
+ end
+
+ it "wakes up when requesting sleep times near or equal to zero" do
+ times = []
+ val = 1
+
+ # power of two divisor so we eventually get near zero
+ loop do
+ val = val / 16.0
+ times << val
+ break if val == 0.0
+ end
+
+ m = Mutex.new
+ m.lock
+ times.each do |time|
+ # just testing that sleep completes
+ -> {m.sleep(time)}.should_not.raise
+ end
+ end
+end
diff --git a/spec/ruby/core/mutex/synchronize_spec.rb b/spec/ruby/core/mutex/synchronize_spec.rb
new file mode 100644
index 0000000000..823f29a634
--- /dev/null
+++ b/spec/ruby/core/mutex/synchronize_spec.rb
@@ -0,0 +1,66 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#synchronize" do
+ it "wraps the lock/unlock pair in an ensure" do
+ m1 = Mutex.new
+ m2 = Mutex.new
+ m2.lock
+ synchronized = false
+
+ th = Thread.new do
+ -> do
+ m1.synchronize do
+ synchronized = true
+ m2.lock
+ raise Exception
+ end
+ end.should.raise(Exception)
+ end
+
+ Thread.pass until synchronized
+
+ m1.locked?.should == true
+ m2.unlock
+ th.join
+ m1.locked?.should == false
+ end
+
+ it "blocks the caller if already locked" do
+ m = Mutex.new
+ m.lock
+ -> { m.synchronize { } }.should block_caller
+ end
+
+ it "does not block the caller if not locked" do
+ m = Mutex.new
+ -> { m.synchronize { } }.should_not block_caller
+ end
+
+ it "blocks the caller if another thread is also in the synchronize block" do
+ m = Mutex.new
+ q1 = Queue.new
+ q2 = Queue.new
+
+ t = Thread.new {
+ m.synchronize {
+ q1.push :ready
+ q2.pop
+ }
+ }
+
+ q1.pop.should == :ready
+
+ -> { m.synchronize { } }.should block_caller
+
+ q2.push :done
+ t.join
+ end
+
+ it "is not recursive" do
+ m = Mutex.new
+
+ m.synchronize do
+ -> { m.synchronize { } }.should.raise(ThreadError)
+ end
+ end
+end
diff --git a/spec/ruby/core/mutex/try_lock_spec.rb b/spec/ruby/core/mutex/try_lock_spec.rb
new file mode 100644
index 0000000000..1da0735d6a
--- /dev/null
+++ b/spec/ruby/core/mutex/try_lock_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#try_lock" do
+ describe "when unlocked" do
+ it "returns true" do
+ m = Mutex.new
+ m.try_lock.should == true
+ end
+
+ it "locks the mutex" do
+ m = Mutex.new
+ m.try_lock
+ m.locked?.should == true
+ end
+ end
+
+ describe "when locked by the current thread" do
+ it "returns false" do
+ m = Mutex.new
+ m.lock
+ m.try_lock.should == false
+ end
+ end
+
+ describe "when locked by another thread" do
+ it "returns false" do
+ m = Mutex.new
+ m.lock
+ Thread.new { m.try_lock }.value.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/mutex/unlock_spec.rb b/spec/ruby/core/mutex/unlock_spec.rb
new file mode 100644
index 0000000000..ed493cf84a
--- /dev/null
+++ b/spec/ruby/core/mutex/unlock_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../spec_helper'
+
+describe "Mutex#unlock" do
+ it "raises ThreadError unless Mutex is locked" do
+ mutex = Mutex.new
+ -> { mutex.unlock }.should.raise(ThreadError)
+ end
+
+ it "raises ThreadError unless thread owns Mutex" do
+ mutex = Mutex.new
+ wait = Mutex.new
+ wait.lock
+ th = Thread.new do
+ mutex.lock
+ wait.lock
+ end
+
+ # avoid race on mutex.lock
+ Thread.pass until mutex.locked?
+ Thread.pass until th.stop?
+
+ -> { mutex.unlock }.should.raise(ThreadError)
+
+ wait.unlock
+ th.join
+ end
+
+ it "raises ThreadError if previously locking thread is gone" do
+ mutex = Mutex.new
+ th = Thread.new do
+ mutex.lock
+ end
+
+ th.join
+
+ -> { mutex.unlock }.should.raise(ThreadError)
+ end
+end
diff --git a/spec/ruby/core/nil/and_spec.rb b/spec/ruby/core/nil/and_spec.rb
new file mode 100644
index 0000000000..cd25aff1de
--- /dev/null
+++ b/spec/ruby/core/nil/and_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#&" do
+ it "returns false" do
+ (nil & nil).should == false
+ (nil & true).should == false
+ (nil & false).should == false
+ (nil & "").should == false
+ (nil & mock('x')).should == false
+ end
+end
diff --git a/spec/ruby/core/nil/case_compare_spec.rb b/spec/ruby/core/nil/case_compare_spec.rb
new file mode 100644
index 0000000000..142560c6f5
--- /dev/null
+++ b/spec/ruby/core/nil/case_compare_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#===" do
+ it "returns true for nil" do
+ (nil === nil).should == true
+ end
+
+ it "returns false for non-nil object" do
+ (nil === 1).should == false
+ (nil === "").should == false
+ (nil === Object).should == false
+ end
+end
diff --git a/spec/ruby/core/nil/dup_spec.rb b/spec/ruby/core/nil/dup_spec.rb
new file mode 100644
index 0000000000..e0be9540a6
--- /dev/null
+++ b/spec/ruby/core/nil/dup_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#dup" do
+ it "returns self" do
+ nil.dup.should.equal?(nil)
+ end
+end
diff --git a/spec/ruby/core/nil/inspect_spec.rb b/spec/ruby/core/nil/inspect_spec.rb
new file mode 100644
index 0000000000..1babc3d062
--- /dev/null
+++ b/spec/ruby/core/nil/inspect_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#inspect" do
+ it "returns the string 'nil'" do
+ nil.inspect.should == "nil"
+ end
+end
diff --git a/spec/ruby/core/nil/match_spec.rb b/spec/ruby/core/nil/match_spec.rb
new file mode 100644
index 0000000000..27ebc53c3d
--- /dev/null
+++ b/spec/ruby/core/nil/match_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#=~" do
+ it "returns nil matching any object" do
+ o = nil
+
+ suppress_warning do
+ (o =~ /Object/).should == nil
+ (o =~ 'Object').should == nil
+ (o =~ Object).should == nil
+ (o =~ Object.new).should == nil
+ (o =~ nil).should == nil
+ (o =~ false).should == nil
+ (o =~ true).should == nil
+ end
+ end
+
+ it "should not warn" do
+ -> { nil =~ /a/ }.should_not complain(verbose: true)
+ end
+end
diff --git a/spec/ruby/core/nil/nil_spec.rb b/spec/ruby/core/nil/nil_spec.rb
new file mode 100644
index 0000000000..2cf97621c6
--- /dev/null
+++ b/spec/ruby/core/nil/nil_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#nil?" do
+ it "returns true" do
+ nil.should.nil?
+ end
+end
diff --git a/spec/ruby/core/nil/nilclass_spec.rb b/spec/ruby/core/nil/nilclass_spec.rb
new file mode 100644
index 0000000000..55c5d0eba7
--- /dev/null
+++ b/spec/ruby/core/nil/nilclass_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "NilClass" do
+ it ".allocate raises a TypeError" do
+ -> do
+ NilClass.allocate
+ end.should.raise(TypeError)
+ end
+
+ it ".new is undefined" do
+ -> do
+ NilClass.new
+ end.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/nil/or_spec.rb b/spec/ruby/core/nil/or_spec.rb
new file mode 100644
index 0000000000..473a833d71
--- /dev/null
+++ b/spec/ruby/core/nil/or_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#|" do
+ it "returns false if other is nil or false, otherwise true" do
+ (nil | nil).should == false
+ (nil | true).should == true
+ (nil | false).should == false
+ (nil | "").should == true
+ (nil | mock('x')).should == true
+ end
+end
diff --git a/spec/ruby/core/nil/rationalize_spec.rb b/spec/ruby/core/nil/rationalize_spec.rb
new file mode 100644
index 0000000000..69fb257a7f
--- /dev/null
+++ b/spec/ruby/core/nil/rationalize_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#rationalize" do
+ it "returns 0/1" do
+ nil.rationalize.should == Rational(0, 1)
+ end
+
+ it "ignores a single argument" do
+ nil.rationalize(0.1).should == Rational(0, 1)
+ end
+
+ it "raises ArgumentError when passed more than one argument" do
+ -> { nil.rationalize(0.1, 0.1) }.should.raise(ArgumentError)
+ -> { nil.rationalize(0.1, 0.1, 2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/nil/singleton_method_spec.rb b/spec/ruby/core/nil/singleton_method_spec.rb
new file mode 100644
index 0000000000..f121b42f81
--- /dev/null
+++ b/spec/ruby/core/nil/singleton_method_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#singleton_method" do
+ it "raises regardless of whether NilClass defines the method" do
+ -> { nil.singleton_method(:foo) }.should.raise(NameError)
+ begin
+ def (nil).foo; end
+ -> { nil.singleton_method(:foo) }.should.raise(NameError)
+ ensure
+ NilClass.send(:remove_method, :foo)
+ end
+ end
+end
diff --git a/spec/ruby/core/nil/to_a_spec.rb b/spec/ruby/core/nil/to_a_spec.rb
new file mode 100644
index 0000000000..b8b339e330
--- /dev/null
+++ b/spec/ruby/core/nil/to_a_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_a" do
+ it "returns an empty array" do
+ nil.to_a.should == []
+ end
+end
diff --git a/spec/ruby/core/nil/to_c_spec.rb b/spec/ruby/core/nil/to_c_spec.rb
new file mode 100644
index 0000000000..f449ddb6a3
--- /dev/null
+++ b/spec/ruby/core/nil/to_c_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_c" do
+ it "returns Complex(0, 0)" do
+ nil.to_c.should.eql?(Complex(0, 0))
+ end
+end
diff --git a/spec/ruby/core/nil/to_f_spec.rb b/spec/ruby/core/nil/to_f_spec.rb
new file mode 100644
index 0000000000..a5f24ba3bf
--- /dev/null
+++ b/spec/ruby/core/nil/to_f_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_f" do
+ it "returns 0.0" do
+ nil.to_f.should == 0.0
+ end
+
+ it "does not cause NilClass to be coerced to Float" do
+ (0.0 == nil).should == false
+ end
+end
diff --git a/spec/ruby/core/nil/to_h_spec.rb b/spec/ruby/core/nil/to_h_spec.rb
new file mode 100644
index 0000000000..5c8d5dc25a
--- /dev/null
+++ b/spec/ruby/core/nil/to_h_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_h" do
+ it "returns an empty hash" do
+ nil.to_h.should == {}
+ nil.to_h.default.should == nil
+ end
+end
diff --git a/spec/ruby/core/nil/to_i_spec.rb b/spec/ruby/core/nil/to_i_spec.rb
new file mode 100644
index 0000000000..d3d088e999
--- /dev/null
+++ b/spec/ruby/core/nil/to_i_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_i" do
+ it "returns 0" do
+ nil.to_i.should == 0
+ end
+
+ it "does not cause NilClass to be coerced to Integer" do
+ (0 == nil).should == false
+ end
+end
diff --git a/spec/ruby/core/nil/to_r_spec.rb b/spec/ruby/core/nil/to_r_spec.rb
new file mode 100644
index 0000000000..8be43baf00
--- /dev/null
+++ b/spec/ruby/core/nil/to_r_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_r" do
+ it "returns 0/1" do
+ nil.to_r.should == Rational(0, 1)
+ end
+end
diff --git a/spec/ruby/core/nil/to_s_spec.rb b/spec/ruby/core/nil/to_s_spec.rb
new file mode 100644
index 0000000000..016ba4165a
--- /dev/null
+++ b/spec/ruby/core/nil/to_s_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#to_s" do
+ it "returns the string ''" do
+ nil.to_s.should == ""
+ end
+
+ it "returns a frozen string" do
+ nil.to_s.should.frozen?
+ end
+
+ it "always returns the same string" do
+ nil.to_s.should.equal?(nil.to_s)
+ end
+end
diff --git a/spec/ruby/core/nil/xor_spec.rb b/spec/ruby/core/nil/xor_spec.rb
new file mode 100644
index 0000000000..b45da9d443
--- /dev/null
+++ b/spec/ruby/core/nil/xor_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "NilClass#^" do
+ it "returns false if other is nil or false, otherwise true" do
+ (nil ^ nil).should == false
+ (nil ^ true).should == true
+ (nil ^ false).should == false
+ (nil ^ "").should == true
+ (nil ^ mock('x')).should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/abs2_spec.rb b/spec/ruby/core/numeric/abs2_spec.rb
new file mode 100644
index 0000000000..12866f9c47
--- /dev/null
+++ b/spec/ruby/core/numeric/abs2_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#abs2" do
+ before :each do
+ @numbers = [
+ 0,
+ 0.0,
+ 1,
+ 20,
+ bignum_value,
+ 278202.292871,
+ 72829,
+ 3.333333333333,
+ 0.1,
+ infinity_value
+ ].map { |n| [-n, n] }.flatten
+ end
+
+ it "returns the square of the absolute value of self" do
+ @numbers.each do |number|
+ number.abs2.should.eql?(number.abs ** 2)
+ end
+ end
+
+ it "calls #* on self" do
+ number = mock_numeric('numeric')
+ number.should_receive(:*).and_return(:result)
+ number.abs2.should == :result
+ end
+
+ it "returns NaN when self is NaN" do
+ nan_value.abs2.nan?.should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/abs_spec.rb b/spec/ruby/core/numeric/abs_spec.rb
new file mode 100644
index 0000000000..8bec50e337
--- /dev/null
+++ b/spec/ruby/core/numeric/abs_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/abs'
+
+describe "Numeric#abs" do
+ it_behaves_like :numeric_abs, :abs
+end
diff --git a/spec/ruby/core/numeric/angle_spec.rb b/spec/ruby/core/numeric/angle_spec.rb
new file mode 100644
index 0000000000..bb38165777
--- /dev/null
+++ b/spec/ruby/core/numeric/angle_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arg'
+
+describe "Numeric#angle" do
+ it_behaves_like :numeric_arg, :angle
+end
diff --git a/spec/ruby/core/numeric/arg_spec.rb b/spec/ruby/core/numeric/arg_spec.rb
new file mode 100644
index 0000000000..ba3b57c687
--- /dev/null
+++ b/spec/ruby/core/numeric/arg_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arg'
+
+describe "Numeric#arg" do
+ it_behaves_like :numeric_arg, :arg
+end
diff --git a/spec/ruby/core/numeric/ceil_spec.rb b/spec/ruby/core/numeric/ceil_spec.rb
new file mode 100644
index 0000000000..00c856e79b
--- /dev/null
+++ b/spec/ruby/core/numeric/ceil_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#ceil" do
+ it "converts self to a Float (using #to_f) and returns the #ceil'ed result" do
+ o = mock_numeric("ceil")
+ o.should_receive(:to_f).and_return(1 + TOLERANCE)
+ o.ceil.should == 2
+
+ o2 = mock_numeric("ceil")
+ v = -1 - TOLERANCE
+ o2.should_receive(:to_f).and_return(v)
+ o2.ceil.should == -1
+ end
+end
diff --git a/spec/ruby/core/numeric/clone_spec.rb b/spec/ruby/core/numeric/clone_spec.rb
new file mode 100644
index 0000000000..1d06fdb050
--- /dev/null
+++ b/spec/ruby/core/numeric/clone_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#clone" do
+ it "returns self" do
+ value = 1
+ value.clone.should.equal?(value)
+
+ subclass = Class.new(Numeric)
+ value = subclass.new
+ value.clone.should.equal?(value)
+ end
+
+ it "does not change frozen status" do
+ 1.clone.frozen?.should == true
+ end
+
+ it "accepts optional keyword argument :freeze" do
+ value = 1
+ value.clone(freeze: true).should.equal?(value)
+ end
+
+ it "raises ArgumentError if passed freeze: false" do
+ -> { 1.clone(freeze: false) }.should.raise(ArgumentError, /can't unfreeze/)
+ end
+
+ it "does not change frozen status if passed freeze: nil" do
+ value = 1
+ value.clone(freeze: nil).should.equal?(value)
+ end
+end
diff --git a/spec/ruby/core/numeric/coerce_spec.rb b/spec/ruby/core/numeric/coerce_spec.rb
new file mode 100644
index 0000000000..9344d99ee6
--- /dev/null
+++ b/spec/ruby/core/numeric/coerce_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#coerce" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ @obj.should_receive(:to_f).any_number_of_times.and_return(10.5)
+ end
+
+ it "returns [other, self] if self and other are instances of the same class" do
+ a = NumericSpecs::Subclass.new
+ b = NumericSpecs::Subclass.new
+
+ a.coerce(b).should == [b, a]
+ end
+
+ # I (emp) think that this behavior is actually a bug in MRI. It's here as documentation
+ # of the behavior until we find out if it's a bug.
+ quarantine! do
+ it "considers the presence of a metaclass when checking the class of the objects" do
+ a = NumericSpecs::Subclass.new
+ b = NumericSpecs::Subclass.new
+
+ # inject a metaclass on a
+ class << a; true; end
+
+ # watch it explode
+ -> { a.coerce(b) }.should.raise(TypeError)
+ end
+ end
+
+ it "returns [other.to_f, self.to_f] if self and other are instances of different classes" do
+ @obj.coerce(2.5).should == [2.5, 10.5]
+ @obj.coerce(3).should == [3.0, 10.5]
+ @obj.coerce("4.4").should == [4.4, 10.5]
+ @obj.coerce(bignum_value).should == [bignum_value.to_f, 10.5]
+ end
+
+ it "raise TypeError if they are instances of different classes and other does not respond to #to_f" do
+ other = mock("numeric")
+ -> { @obj.coerce(other) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { @obj.coerce(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a boolean" do
+ -> { @obj.coerce(false) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a Symbol" do
+ -> { @obj.coerce(:symbol) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when passed a non-numeric String" do
+ -> { @obj.coerce("test") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/numeric/comparison_spec.rb b/spec/ruby/core/numeric/comparison_spec.rb
new file mode 100644
index 0000000000..b0a9390cc0
--- /dev/null
+++ b/spec/ruby/core/numeric/comparison_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#<=>" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns 0 if self equals other" do
+ (@obj <=> @obj).should == 0
+ end
+
+ it "returns nil if self does not equal other" do
+ (@obj <=> NumericSpecs::Subclass.new).should == nil
+ (@obj <=> 10).should == nil
+ (@obj <=> -3.5).should == nil
+ (@obj <=> bignum_value).should == nil
+ end
+
+ describe "with subclasses of Numeric" do
+ before :each do
+ @a = NumericSpecs::Comparison.new
+ @b = NumericSpecs::Comparison.new
+
+ ScratchPad.clear
+ end
+
+ it "is called when instances are compared with #<" do
+ (@a < @b).should == false
+ ScratchPad.recorded.should == :numeric_comparison
+ end
+
+ it "is called when instances are compared with #<=" do
+ (@a <= @b).should == false
+ ScratchPad.recorded.should == :numeric_comparison
+ end
+
+ it "is called when instances are compared with #>" do
+ (@a > @b).should == true
+ ScratchPad.recorded.should == :numeric_comparison
+ end
+
+ it "is called when instances are compared with #>=" do
+ (@a >= @b).should == true
+ ScratchPad.recorded.should == :numeric_comparison
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/conj_spec.rb b/spec/ruby/core/numeric/conj_spec.rb
new file mode 100644
index 0000000000..7d4777ca60
--- /dev/null
+++ b/spec/ruby/core/numeric/conj_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/conj'
+
+describe "Numeric#conj" do
+ it_behaves_like :numeric_conj, :conj
+end
diff --git a/spec/ruby/core/numeric/conjugate_spec.rb b/spec/ruby/core/numeric/conjugate_spec.rb
new file mode 100644
index 0000000000..99854766e7
--- /dev/null
+++ b/spec/ruby/core/numeric/conjugate_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/conj'
+
+describe "Numeric#conjugate" do
+ it_behaves_like :numeric_conj, :conjugate
+end
diff --git a/spec/ruby/core/numeric/denominator_spec.rb b/spec/ruby/core/numeric/denominator_spec.rb
new file mode 100644
index 0000000000..34729446a2
--- /dev/null
+++ b/spec/ruby/core/numeric/denominator_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#denominator" do
+ # The Numeric child classes override this method, so their behaviour is
+ # specified in the appropriate place
+ before :each do
+ @numbers = [
+ 20, # Integer
+ 99999999**99, # Bignum
+ ]
+ end
+
+ it "returns 1" do
+ @numbers.each {|number| number.denominator.should == 1}
+ end
+
+ it "works with Numeric subclasses" do
+ rational = mock_numeric('rational')
+ rational.should_receive(:denominator).and_return(:denominator)
+ numeric = mock_numeric('numeric')
+ numeric.should_receive(:to_r).and_return(rational)
+ numeric.denominator.should == :denominator
+ end
+end
diff --git a/spec/ruby/core/numeric/div_spec.rb b/spec/ruby/core/numeric/div_spec.rb
new file mode 100644
index 0000000000..a17961850c
--- /dev/null
+++ b/spec/ruby/core/numeric/div_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#div" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "calls self#/ with other, then returns the #floor'ed result" do
+ result = mock("Numeric#div result")
+ result.should_receive(:floor).and_return(12)
+ @obj.should_receive(:/).with(10).and_return(result)
+
+ @obj.div(10).should == 12
+ end
+
+ it "raises ZeroDivisionError for 0" do
+ -> { @obj.div(0) }.should.raise(ZeroDivisionError)
+ -> { @obj.div(0.0) }.should.raise(ZeroDivisionError)
+ -> { @obj.div(Complex(0,0)) }.should.raise(ZeroDivisionError)
+ end
+end
diff --git a/spec/ruby/core/numeric/divmod_spec.rb b/spec/ruby/core/numeric/divmod_spec.rb
new file mode 100644
index 0000000000..8d5259bbcd
--- /dev/null
+++ b/spec/ruby/core/numeric/divmod_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#divmod" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns [quotient, modulus], with quotient being obtained as in Numeric#div then #floor and modulus being obtained by calling self#- with quotient * other" do
+ @obj.should_receive(:/).twice.with(10).and_return(13 - TOLERANCE, 13 - TOLERANCE)
+ @obj.should_receive(:-).with(120).and_return(3)
+
+ @obj.divmod(10).should == [12, 3]
+ end
+end
diff --git a/spec/ruby/core/numeric/dup_spec.rb b/spec/ruby/core/numeric/dup_spec.rb
new file mode 100644
index 0000000000..c158c618de
--- /dev/null
+++ b/spec/ruby/core/numeric/dup_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#dup" do
+ it "returns self" do
+ value = 1
+ value.dup.should.equal?(value)
+
+ subclass = Class.new(Numeric)
+ value = subclass.new
+ value.dup.should.equal?(value)
+ end
+
+ it "does not change frozen status" do
+ 1.dup.frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/eql_spec.rb b/spec/ruby/core/numeric/eql_spec.rb
new file mode 100644
index 0000000000..80c58caef4
--- /dev/null
+++ b/spec/ruby/core/numeric/eql_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#eql?" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns false if self's and other's types don't match" do
+ @obj.should_not.eql?(1)
+ @obj.should_not.eql?(-1.5)
+ @obj.should_not.eql?(bignum_value)
+ @obj.should_not.eql?(:sym)
+ end
+
+ it "returns the result of calling self#== with other when self's and other's types match" do
+ other = NumericSpecs::Subclass.new
+ @obj.should_receive(:==).with(other).and_return("result", nil)
+ @obj.should.eql?(other)
+ @obj.should_not.eql?(other)
+ end
+end
diff --git a/spec/ruby/core/numeric/fdiv_spec.rb b/spec/ruby/core/numeric/fdiv_spec.rb
new file mode 100644
index 0000000000..2c22c6a86a
--- /dev/null
+++ b/spec/ruby/core/numeric/fdiv_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#fdiv" do
+ it "coerces self with #to_f" do
+ numeric = mock_numeric('numeric')
+ numeric.should_receive(:to_f).and_return(3.0)
+ numeric.fdiv(0.5).should == 6.0
+ end
+
+ it "coerces other with #to_f" do
+ numeric = mock_numeric('numeric')
+ numeric.should_receive(:to_f).and_return(3.0)
+ 6.fdiv(numeric).should == 2.0
+ end
+
+ it "performs floating-point division" do
+ 3.fdiv(2).should == 1.5
+ end
+
+ it "returns a Float" do
+ bignum_value.fdiv(Float::MAX).should.instance_of?(Float)
+ end
+
+ it "returns Infinity if other is 0" do
+ 8121.92821.fdiv(0).infinite?.should == 1
+ end
+
+ it "returns NaN if other is NaN" do
+ 3334.fdiv(nan_value).nan?.should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/finite_spec.rb b/spec/ruby/core/numeric/finite_spec.rb
new file mode 100644
index 0000000000..2c18c89466
--- /dev/null
+++ b/spec/ruby/core/numeric/finite_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#finite?" do
+ it "returns true by default" do
+ o = mock_numeric("finite")
+ o.finite?.should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/fixtures/classes.rb b/spec/ruby/core/numeric/fixtures/classes.rb
new file mode 100644
index 0000000000..1505584889
--- /dev/null
+++ b/spec/ruby/core/numeric/fixtures/classes.rb
@@ -0,0 +1,17 @@
+module NumericSpecs
+ class Comparison < Numeric
+ # This method is used because we cannot define
+ # singleton methods on subclasses of Numeric,
+ # which is needed for a.should_receive to work.
+ def <=>(other)
+ ScratchPad.record :numeric_comparison
+ 1
+ end
+ end
+
+ class Subclass < Numeric
+ # Allow methods to be mocked
+ def singleton_method_added(val)
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/floor_spec.rb b/spec/ruby/core/numeric/floor_spec.rb
new file mode 100644
index 0000000000..80a4868e4d
--- /dev/null
+++ b/spec/ruby/core/numeric/floor_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#floor" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "converts self to a Float (using #to_f) and returns the #floor'ed result" do
+ @obj.should_receive(:to_f).and_return(2 - TOLERANCE, TOLERANCE - 2)
+ @obj.floor.should == 1
+ @obj.floor.should == -2
+ end
+end
diff --git a/spec/ruby/core/numeric/i_spec.rb b/spec/ruby/core/numeric/i_spec.rb
new file mode 100644
index 0000000000..f5fb99dfd3
--- /dev/null
+++ b/spec/ruby/core/numeric/i_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#i" do
+ it "returns a Complex object" do
+ 34.i.should.instance_of?(Complex)
+ end
+
+ it "sets the real part to 0" do
+ 7342.i.real.should == 0
+ end
+
+ it "sets the imaginary part to self" do
+ 62.81.i.imag.should == 62.81
+ end
+end
diff --git a/spec/ruby/core/numeric/imag_spec.rb b/spec/ruby/core/numeric/imag_spec.rb
new file mode 100644
index 0000000000..b9e343cee9
--- /dev/null
+++ b/spec/ruby/core/numeric/imag_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/imag'
+
+describe "Numeric#imag" do
+ it_behaves_like :numeric_imag, :imag
+end
diff --git a/spec/ruby/core/numeric/imaginary_spec.rb b/spec/ruby/core/numeric/imaginary_spec.rb
new file mode 100644
index 0000000000..ec708cb505
--- /dev/null
+++ b/spec/ruby/core/numeric/imaginary_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/imag'
+
+describe "Numeric#imaginary" do
+ it_behaves_like :numeric_imag, :imaginary
+end
diff --git a/spec/ruby/core/numeric/infinite_spec.rb b/spec/ruby/core/numeric/infinite_spec.rb
new file mode 100644
index 0000000000..3ea7825c8c
--- /dev/null
+++ b/spec/ruby/core/numeric/infinite_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#infinite?" do
+ it "returns nil by default" do
+ o = mock_numeric("infinite")
+ o.infinite?.should == nil
+ end
+end
diff --git a/spec/ruby/core/numeric/integer_spec.rb b/spec/ruby/core/numeric/integer_spec.rb
new file mode 100644
index 0000000000..adbac4d7aa
--- /dev/null
+++ b/spec/ruby/core/numeric/integer_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#integer?" do
+ it "returns false" do
+ NumericSpecs::Subclass.new.should_not.integer?
+ end
+end
diff --git a/spec/ruby/core/numeric/magnitude_spec.rb b/spec/ruby/core/numeric/magnitude_spec.rb
new file mode 100644
index 0000000000..1371dff21f
--- /dev/null
+++ b/spec/ruby/core/numeric/magnitude_spec.rb
@@ -0,0 +1,6 @@
+require_relative "../../spec_helper"
+require_relative 'shared/abs'
+
+describe "Numeric#magnitude" do
+ it_behaves_like :numeric_abs, :magnitude
+end
diff --git a/spec/ruby/core/numeric/modulo_spec.rb b/spec/ruby/core/numeric/modulo_spec.rb
new file mode 100644
index 0000000000..e3dc7e56f3
--- /dev/null
+++ b/spec/ruby/core/numeric/modulo_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :numeric_modulo_19, shared: true do
+ it "returns self - other * self.div(other)" do
+ s = mock_numeric('self')
+ o = mock_numeric('other')
+ n3 = mock_numeric('n3')
+ n4 = mock_numeric('n4')
+ n5 = mock_numeric('n5')
+ s.should_receive(:div).with(o).and_return(n3)
+ o.should_receive(:*).with(n3).and_return(n4)
+ s.should_receive(:-).with(n4).and_return(n5)
+ s.send(@method, o).should == n5
+ end
+end
+
+describe "Numeric#modulo" do
+ it_behaves_like :numeric_modulo_19, :modulo
+end
+
+describe "Numeric#%" do
+ it_behaves_like :numeric_modulo_19, :%
+end
diff --git a/spec/ruby/core/numeric/negative_spec.rb b/spec/ruby/core/numeric/negative_spec.rb
new file mode 100644
index 0000000000..f2d8a847da
--- /dev/null
+++ b/spec/ruby/core/numeric/negative_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#negative?" do
+ describe "on positive numbers" do
+ it "returns false" do
+ 1.negative?.should == false
+ 0.1.negative?.should == false
+ end
+ end
+
+ describe "on zero" do
+ it "returns false" do
+ 0.negative?.should == false
+ 0.0.negative?.should == false
+ end
+ end
+
+ describe "on negative numbers" do
+ it "returns true" do
+ -1.negative?.should == true
+ -0.1.negative?.should == true
+ end
+ end
+end
+
+describe "Numeric#negative?" do
+ before(:each) do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns true if self is less than 0" do
+ @obj.should_receive(:<).with(0).and_return(true)
+ @obj.should.negative?
+ end
+
+ it "returns false if self is greater than 0" do
+ @obj.should_receive(:<).with(0).and_return(false)
+ @obj.should_not.negative?
+ end
+end
diff --git a/spec/ruby/core/numeric/nonzero_spec.rb b/spec/ruby/core/numeric/nonzero_spec.rb
new file mode 100644
index 0000000000..464ed4f4f8
--- /dev/null
+++ b/spec/ruby/core/numeric/nonzero_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#nonzero?" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns self if self#zero? is false" do
+ @obj.should_receive(:zero?).and_return(false)
+ @obj.nonzero?.should == @obj
+ end
+
+ it "returns nil if self#zero? is true" do
+ @obj.should_receive(:zero?).and_return(true)
+ @obj.nonzero?.should == nil
+ end
+end
diff --git a/spec/ruby/core/numeric/numerator_spec.rb b/spec/ruby/core/numeric/numerator_spec.rb
new file mode 100644
index 0000000000..668df8b797
--- /dev/null
+++ b/spec/ruby/core/numeric/numerator_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#numerator" do
+ before :all do
+ @numbers = [
+ 0,
+ 29871,
+ 99999999999999**99,
+ -72628191273,
+ 29282.2827,
+ -2927.00091,
+ 0.0,
+ 12.0,
+ Float::MAX,
+ ]
+ end
+
+ # This isn't entirely true, as NaN.numerator works, whereas
+ # Rational(NaN) raises an exception, but we test this in Float#numerator
+ it "converts self to a Rational object then returns its numerator" do
+ @numbers.each do |number|
+ number.numerator.should == Rational(number).numerator
+ end
+ end
+
+ it "works with Numeric subclasses" do
+ rational = mock_numeric('rational')
+ rational.should_receive(:numerator).and_return(:numerator)
+ numeric = mock_numeric('numeric')
+ numeric.should_receive(:to_r).and_return(rational)
+ numeric.numerator.should == :numerator
+ end
+end
diff --git a/spec/ruby/core/numeric/numeric_spec.rb b/spec/ruby/core/numeric/numeric_spec.rb
new file mode 100644
index 0000000000..2bcf2a1175
--- /dev/null
+++ b/spec/ruby/core/numeric/numeric_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Numeric" do
+ it "includes Comparable" do
+ Numeric.include?(Comparable).should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/phase_spec.rb b/spec/ruby/core/numeric/phase_spec.rb
new file mode 100644
index 0000000000..bc1995303f
--- /dev/null
+++ b/spec/ruby/core/numeric/phase_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arg'
+
+describe "Numeric#phase" do
+ it_behaves_like :numeric_arg, :phase
+end
diff --git a/spec/ruby/core/numeric/polar_spec.rb b/spec/ruby/core/numeric/polar_spec.rb
new file mode 100644
index 0000000000..0695d7afb2
--- /dev/null
+++ b/spec/ruby/core/numeric/polar_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#polar" do
+ before :each do
+ @pos_numbers = [
+ 1,
+ 3898172610**9,
+ 987.18273,
+ Float::MAX,
+ Rational(13,7),
+ infinity_value,
+ ]
+ @neg_numbers = @pos_numbers.map {|n| -n}
+ @numbers = @pos_numbers + @neg_numbers
+ @numbers.push(0, 0.0)
+ end
+
+ it "returns a two-element Array" do
+ @numbers.each do |number|
+ number.polar.should.instance_of?(Array)
+ number.polar.size.should == 2
+ end
+ end
+
+ it "sets the first value to the absolute value of self" do
+ @numbers.each do |number|
+ number.polar.first.should == number.abs
+ end
+ end
+
+ it "sets the last value to 0 if self is positive" do
+ (@numbers - @neg_numbers).each do |number|
+ number.should >= 0
+ number.polar.last.should == 0
+ end
+ end
+
+ it "sets the last value to Pi if self is negative" do
+ @neg_numbers.each do |number|
+ number.should < 0
+ number.polar.last.should == Math::PI
+ end
+ end
+
+ it "returns [NaN, NaN] if self is NaN" do
+ nan_value.polar.size.should == 2
+ nan_value.polar.first.nan?.should == true
+ nan_value.polar.last.nan?.should == true
+ end
+end
diff --git a/spec/ruby/core/numeric/positive_spec.rb b/spec/ruby/core/numeric/positive_spec.rb
new file mode 100644
index 0000000000..7c8d15cd9f
--- /dev/null
+++ b/spec/ruby/core/numeric/positive_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#positive?" do
+ describe "on positive numbers" do
+ it "returns true" do
+ 1.positive?.should == true
+ 0.1.positive?.should == true
+ end
+ end
+
+ describe "on zero" do
+ it "returns false" do
+ 0.positive?.should == false
+ 0.0.positive?.should == false
+ end
+ end
+
+ describe "on negative numbers" do
+ it "returns false" do
+ -1.positive?.should == false
+ -0.1.positive?.should == false
+ end
+ end
+end
+
+describe "Numeric#positive?" do
+ before(:each) do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns true if self is greater than 0" do
+ @obj.should_receive(:>).with(0).and_return(true)
+ @obj.should.positive?
+ end
+
+ it "returns false if self is less than 0" do
+ @obj.should_receive(:>).with(0).and_return(false)
+ @obj.should_not.positive?
+ end
+end
diff --git a/spec/ruby/core/numeric/quo_spec.rb b/spec/ruby/core/numeric/quo_spec.rb
new file mode 100644
index 0000000000..66ff019231
--- /dev/null
+++ b/spec/ruby/core/numeric/quo_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#quo" do
+ it "returns the result of self divided by the given Integer as a Rational" do
+ 5.quo(2).should.eql?(Rational(5,2))
+ end
+
+ it "returns the result of self divided by the given Float as a Float" do
+ 2.quo(2.5).should.eql?(0.8)
+ end
+
+ it "returns the result of self divided by the given Bignum as a Float" do
+ 45.quo(bignum_value).should be_close(1.04773789668636e-08, TOLERANCE)
+ end
+
+ it "raises a ZeroDivisionError when the given Integer is 0" do
+ -> { 0.quo(0) }.should.raise(ZeroDivisionError)
+ -> { 10.quo(0) }.should.raise(ZeroDivisionError)
+ -> { -10.quo(0) }.should.raise(ZeroDivisionError)
+ -> { bignum_value.quo(0) }.should.raise(ZeroDivisionError)
+ -> { (-bignum_value).quo(0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "calls #to_r to convert the object to a Rational" do
+ obj = NumericSpecs::Subclass.new
+ obj.should_receive(:to_r).and_return(Rational(1))
+
+ obj.quo(19).should == Rational(1, 19)
+ end
+
+ it "raises a TypeError of #to_r does not return a Rational" do
+ obj = NumericSpecs::Subclass.new
+ obj.should_receive(:to_r).and_return(1)
+
+ -> { obj.quo(19) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given a non-Integer" do
+ -> {
+ (obj = mock('x')).should_not_receive(:to_int)
+ 13.quo(obj)
+ }.should.raise(TypeError)
+ -> { 13.quo("10") }.should.raise(TypeError)
+ -> { 13.quo(:symbol) }.should.raise(TypeError)
+ end
+
+ it "returns the result of calling self#/ with other" do
+ obj = NumericSpecs::Subclass.new
+ obj.should_receive(:to_r).and_return(19.quo(20))
+
+ obj.quo(19).should == 1.quo(20)
+ end
+
+ it "raises a ZeroDivisionError if the given argument is zero and not a Float" do
+ -> { 1.quo(0) }.should.raise(ZeroDivisionError)
+ end
+
+ it "returns infinity if the given argument is zero and is a Float" do
+ (1.quo(0.0)).to_s.should == 'Infinity'
+ (-1.quo(0.0)).to_s.should == '-Infinity'
+ end
+end
diff --git a/spec/ruby/core/numeric/real_spec.rb b/spec/ruby/core/numeric/real_spec.rb
new file mode 100644
index 0000000000..09d482a691
--- /dev/null
+++ b/spec/ruby/core/numeric/real_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#real" do
+ before :each do
+ @numbers = [
+ 20, # Integer
+ 398.72, # Float
+ Rational(3, 4), # Rational
+ bignum_value, # Bignum
+ infinity_value,
+ nan_value
+ ].map{ |n| [n, -n] }.flatten
+ end
+
+ it "returns self" do
+ @numbers.each do |number|
+ if number.to_f.nan?
+ number.real.nan?.should == true
+ else
+ number.real.should == number
+ end
+ end
+ end
+
+ it "raises an ArgumentError if given any arguments" do
+ @numbers.each do |number|
+ -> { number.real(number) }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe "Numeric#real?" do
+ it "returns true" do
+ NumericSpecs::Subclass.new.should.real?
+ end
+end
diff --git a/spec/ruby/core/numeric/rect_spec.rb b/spec/ruby/core/numeric/rect_spec.rb
new file mode 100644
index 0000000000..79a144c5a4
--- /dev/null
+++ b/spec/ruby/core/numeric/rect_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/rect'
+
+describe "Numeric#rect" do
+ it_behaves_like :numeric_rect, :rect
+end
diff --git a/spec/ruby/core/numeric/rectangular_spec.rb b/spec/ruby/core/numeric/rectangular_spec.rb
new file mode 100644
index 0000000000..2c68985a16
--- /dev/null
+++ b/spec/ruby/core/numeric/rectangular_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/rect'
+
+describe "Numeric#rectangular" do
+ it_behaves_like :numeric_rect, :rectangular
+end
diff --git a/spec/ruby/core/numeric/remainder_spec.rb b/spec/ruby/core/numeric/remainder_spec.rb
new file mode 100644
index 0000000000..bdf7358b21
--- /dev/null
+++ b/spec/ruby/core/numeric/remainder_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#remainder" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ @result = mock("Numeric#% result")
+ @other = mock("Passed Object")
+ @other.should_receive(:coerce).with(@obj).and_return([@obj, @other])
+ end
+
+ it "returns the result of calling self#% with other if self is 0" do
+ @obj.should_receive(:%).with(@other).and_return(@result)
+ @result.should_receive(:==).with(0).and_return(true)
+
+ @obj.remainder(@other).should.equal?(@result)
+ end
+
+ it "returns the result of calling self#% with other if self and other are greater than 0" do
+ @obj.should_receive(:%).with(@other).and_return(@result)
+ @result.should_receive(:==).with(0).and_return(false)
+
+ @obj.should_receive(:<).with(0).and_return(false)
+
+ @obj.should_receive(:>).with(0).and_return(true)
+ @other.should_receive(:<).with(0).and_return(false)
+
+ @obj.remainder(@other).should.equal?(@result)
+ end
+
+ it "returns the result of calling self#% with other if self and other are less than 0" do
+ @obj.should_receive(:%).with(@other).and_return(@result)
+ @result.should_receive(:==).with(0).and_return(false)
+
+ @obj.should_receive(:<).with(0).and_return(true)
+ @other.should_receive(:>).with(0).and_return(false)
+
+ @obj.should_receive(:>).with(0).and_return(false)
+
+ @obj.remainder(@other).should.equal?(@result)
+ end
+
+ it "returns the result of calling self#% with other - other if self is greater than 0 and other is less than 0" do
+ @obj.should_receive(:%).with(@other).and_return(@result)
+ @result.should_receive(:==).with(0).and_return(false)
+
+ @obj.should_receive(:<).with(0).and_return(false)
+
+ @obj.should_receive(:>).with(0).and_return(true)
+ @other.should_receive(:<).with(0).and_return(true)
+
+ @result.should_receive(:-).with(@other).and_return(:result)
+
+ @obj.remainder(@other).should == :result
+ end
+
+ it "returns the result of calling self#% with other - other if self is less than 0 and other is greater than 0" do
+ @obj.should_receive(:%).with(@other).and_return(@result)
+ @result.should_receive(:==).with(0).and_return(false)
+
+ @obj.should_receive(:<).with(0).and_return(true)
+ @other.should_receive(:>).with(0).and_return(true)
+
+ @result.should_receive(:-).with(@other).and_return(:result)
+
+ @obj.remainder(@other).should == :result
+ end
+end
diff --git a/spec/ruby/core/numeric/round_spec.rb b/spec/ruby/core/numeric/round_spec.rb
new file mode 100644
index 0000000000..47c5837693
--- /dev/null
+++ b/spec/ruby/core/numeric/round_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#round" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "converts self to a Float (using #to_f) and returns the #round'ed result" do
+ @obj.should_receive(:to_f).and_return(2 - TOLERANCE, TOLERANCE - 2)
+ @obj.round.should == 2
+ @obj.round.should == -2
+ end
+end
diff --git a/spec/ruby/core/numeric/shared/abs.rb b/spec/ruby/core/numeric/shared/abs.rb
new file mode 100644
index 0000000000..c3dadccfd6
--- /dev/null
+++ b/spec/ruby/core/numeric/shared/abs.rb
@@ -0,0 +1,19 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :numeric_abs, shared: true do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns self when self is greater than 0" do
+ @obj.should_receive(:<).with(0).and_return(false)
+ @obj.send(@method).should == @obj
+ end
+
+ it "returns self\#@- when self is less than 0" do
+ @obj.should_receive(:<).with(0).and_return(true)
+ @obj.should_receive(:-@).and_return(:absolute_value)
+ @obj.send(@method).should == :absolute_value
+ end
+end
diff --git a/spec/ruby/core/numeric/shared/arg.rb b/spec/ruby/core/numeric/shared/arg.rb
new file mode 100644
index 0000000000..c8e7ad8333
--- /dev/null
+++ b/spec/ruby/core/numeric/shared/arg.rb
@@ -0,0 +1,38 @@
+require_relative '../../../spec_helper'
+
+describe :numeric_arg, shared: true do
+ before :each do
+ @numbers = [
+ 20,
+ Rational(3, 4),
+ bignum_value,
+ infinity_value
+ ]
+ end
+
+ it "returns 0 if positive" do
+ @numbers.each do |number|
+ number.send(@method).should == 0
+ end
+ end
+
+ it "returns Pi if negative" do
+ @numbers.each do |number|
+ (0-number).send(@method).should == Math::PI
+ end
+ end
+
+ describe "with a Numeric subclass" do
+ it "returns 0 if self#<(0) returns false" do
+ numeric = mock_numeric('positive')
+ numeric.should_receive(:<).with(0).and_return(false)
+ numeric.send(@method).should == 0
+ end
+
+ it "returns Pi if self#<(0) returns true" do
+ numeric = mock_numeric('positive')
+ numeric.should_receive(:<).with(0).and_return(true)
+ numeric.send(@method).should == Math::PI
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/shared/conj.rb b/spec/ruby/core/numeric/shared/conj.rb
new file mode 100644
index 0000000000..1a661fbbe8
--- /dev/null
+++ b/spec/ruby/core/numeric/shared/conj.rb
@@ -0,0 +1,20 @@
+require_relative '../../../spec_helper'
+
+describe :numeric_conj, shared: true do
+ before :each do
+ @numbers = [
+ 20, # Integer
+ 398.72, # Float
+ Rational(3, 4), # Rational
+ bignum_value,
+ infinity_value,
+ nan_value
+ ]
+ end
+
+ it "returns self" do
+ @numbers.each do |number|
+ number.send(@method).should.equal?(number)
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/shared/imag.rb b/spec/ruby/core/numeric/shared/imag.rb
new file mode 100644
index 0000000000..605a23d8c6
--- /dev/null
+++ b/spec/ruby/core/numeric/shared/imag.rb
@@ -0,0 +1,26 @@
+require_relative '../../../spec_helper'
+
+describe :numeric_imag, shared: true do
+ before :each do
+ @numbers = [
+ 20, # Integer
+ 398.72, # Float
+ Rational(3, 4), # Rational
+ bignum_value, # Bignum
+ infinity_value,
+ nan_value
+ ].map{|n| [n,-n]}.flatten
+ end
+
+ it "returns 0" do
+ @numbers.each do |number|
+ number.send(@method).should == 0
+ end
+ end
+
+ it "raises an ArgumentError if given any arguments" do
+ @numbers.each do |number|
+ -> { number.send(@method, number) }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/shared/rect.rb b/spec/ruby/core/numeric/shared/rect.rb
new file mode 100644
index 0000000000..32d03e45d2
--- /dev/null
+++ b/spec/ruby/core/numeric/shared/rect.rb
@@ -0,0 +1,48 @@
+require_relative '../../../spec_helper'
+
+describe :numeric_rect, shared: true do
+ before :each do
+ @numbers = [
+ 20, # Integer
+ 398.72, # Float
+ Rational(3, 4), # Rational
+ 99999999**99, # Bignum
+ infinity_value,
+ nan_value
+ ]
+ end
+
+ it "returns an Array" do
+ @numbers.each do |number|
+ number.send(@method).should.instance_of?(Array)
+ end
+ end
+
+ it "returns a two-element Array" do
+ @numbers.each do |number|
+ number.send(@method).size.should == 2
+ end
+ end
+
+ it "returns self as the first element" do
+ @numbers.each do |number|
+ if Float === number and number.nan?
+ number.send(@method).first.nan?.should == true
+ else
+ number.send(@method).first.should == number
+ end
+ end
+ end
+
+ it "returns 0 as the last element" do
+ @numbers.each do |number|
+ number.send(@method).last.should == 0
+ end
+ end
+
+ it "raises an ArgumentError if given any arguments" do
+ @numbers.each do |number|
+ -> { number.send(@method, number) }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/shared/step.rb b/spec/ruby/core/numeric/shared/step.rb
new file mode 100644
index 0000000000..66b8c1af0d
--- /dev/null
+++ b/spec/ruby/core/numeric/shared/step.rb
@@ -0,0 +1,410 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+# Describes Numeric#step shared specs between different argument styles.
+# To be able to do it, the @step ivar must contain a Proc that transforms
+# the step call arguments passed as positional arguments to the style of
+# arguments pretended to test.
+describe :numeric_step, shared: true do
+ before :each do
+ ScratchPad.record []
+ @prc = -> x { ScratchPad << x }
+ end
+
+ it "defaults to step = 1" do
+ @step.call(1, 5, &@prc)
+ ScratchPad.recorded.should.eql? [1, 2, 3, 4, 5]
+ end
+
+ it "defaults to an infinite limit with a step size of 1 for Integers" do
+ 1.step.first(5).should == [1, 2, 3, 4, 5]
+ end
+
+ it "defaults to an infinite limit with a step size of 1.0 for Floats" do
+ 1.0.step.first(5).should == [1.0, 2.0, 3.0, 4.0, 5.0]
+ end
+
+ describe "when self, stop and step are Integers" do
+ it "yields only Integers" do
+ @step.call(1, 5, 1) { |x| x.should.instance_of?(Integer) }
+ end
+
+ describe "with a positive step" do
+ it "yields while increasing self by step until stop is reached" do
+ @step.call(1, 5, 1, &@prc)
+ ScratchPad.recorded.should.eql? [1, 2, 3, 4, 5]
+ end
+
+ it "yields once when self equals stop" do
+ @step.call(1, 1, 1, &@prc)
+ ScratchPad.recorded.should.eql? [1]
+ end
+
+ it "does not yield when self is greater than stop" do
+ @step.call(2, 1, 1, &@prc)
+ ScratchPad.recorded.should.eql? []
+ end
+ end
+
+ describe "with a negative step" do
+ it "yields while decreasing self by step until stop is reached" do
+ @step.call(5, 1, -1, &@prc)
+ ScratchPad.recorded.should.eql? [5, 4, 3, 2, 1]
+ end
+
+ it "yields once when self equals stop" do
+ @step.call(5, 5, -1, &@prc)
+ ScratchPad.recorded.should.eql? [5]
+ end
+
+ it "does not yield when self is less than stop" do
+ @step.call(1, 5, -1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+ end
+
+ describe "when at least one of self, stop or step is a Float" do
+ it "yields Floats even if only self is a Float" do
+ @step.call(1.5, 5, 1) { |x| x.should.instance_of?(Float) }
+ end
+
+ it "yields Floats even if only stop is a Float" do
+ @step.call(1, 5.0, 1) { |x| x.should.instance_of?(Float) }
+ end
+
+ it "yields Floats even if only step is a Float" do
+ @step.call(1, 5, 1.0) { |x| x.should.instance_of?(Float) }
+ end
+
+ describe "with a positive step" do
+ it "yields while increasing self by step while < stop" do
+ @step.call(1.5, 5, 1, &@prc)
+ ScratchPad.recorded.should.eql? [1.5, 2.5, 3.5, 4.5]
+ end
+
+ it "yields once when self equals stop" do
+ @step.call(1.5, 1.5, 1, &@prc)
+ ScratchPad.recorded.should.eql? [1.5]
+ end
+
+ it "does not yield when self is greater than stop" do
+ @step.call(2.5, 1.5, 1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+
+ it "is careful about not yielding a value greater than limit" do
+ # As 9*1.3+1.0 == 12.700000000000001 > 12.7, we test:
+ @step.call(1.0, 12.7, 1.3, &@prc)
+ ScratchPad.recorded.should.eql? [1.0, 2.3, 3.6, 4.9, 6.2, 7.5, 8.8, 10.1, 11.4, 12.7]
+ end
+ end
+
+ describe "with a negative step" do
+ it "yields while decreasing self by step while self > stop" do
+ @step.call(5, 1.5, -1, &@prc)
+ ScratchPad.recorded.should.eql? [5.0, 4.0, 3.0, 2.0]
+ end
+
+ it "yields once when self equals stop" do
+ @step.call(1.5, 1.5, -1, &@prc)
+ ScratchPad.recorded.should.eql? [1.5]
+ end
+
+ it "does not yield when self is less than stop" do
+ @step.call(1, 5, -1.5, &@prc)
+ ScratchPad.recorded.should == []
+ end
+
+ it "is careful about not yielding a value smaller than limit" do
+ # As -9*1.3-1.0 == -12.700000000000001 < -12.7, we test:
+ @step.call(-1.0, -12.7, -1.3, &@prc)
+ ScratchPad.recorded.should.eql? [-1.0, -2.3, -3.6, -4.9, -6.2, -7.5, -8.8, -10.1, -11.4, -12.7]
+ end
+ end
+
+ describe "with a positive Infinity step" do
+ it "yields once if self < stop" do
+ @step.call(42, 100, infinity_value, &@prc)
+ ScratchPad.recorded.should.eql? [42.0]
+ end
+
+ it "yields once when stop is Infinity" do
+ @step.call(42, infinity_value, infinity_value, &@prc)
+ ScratchPad.recorded.should.eql? [42.0]
+ end
+
+ it "yields once when self equals stop" do
+ @step.call(42, 42, infinity_value, &@prc)
+ ScratchPad.recorded.should.eql? [42.0]
+ end
+
+ it "yields once when self and stop are Infinity" do
+ # @step.call(infinity_value, infinity_value, infinity_value, &@prc)
+ @step.call(infinity_value, infinity_value, infinity_value, &@prc)
+ ScratchPad.recorded.should == [infinity_value]
+ end
+
+ it "does not yield when self > stop" do
+ @step.call(100, 42, infinity_value, &@prc)
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not yield when stop is -Infinity" do
+ @step.call(42, -infinity_value, infinity_value, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "with a negative Infinity step" do
+ it "yields once if self > stop" do
+ @step.call(42, 6, -infinity_value, &@prc)
+ ScratchPad.recorded.should.eql? [42.0]
+ end
+
+ it "yields once if stop is -Infinity" do
+ @step.call(42, -infinity_value, -infinity_value, &@prc)
+ ScratchPad.recorded.should.eql? [42.0]
+ end
+
+ it "yields once when self equals stop" do
+ @step.call(42, 42, -infinity_value, &@prc)
+ ScratchPad.recorded.should.eql? [42.0]
+ end
+
+ it "yields once when self and stop are Infinity" do
+ @step.call(infinity_value, infinity_value, -infinity_value, &@prc)
+ ScratchPad.recorded.should == [infinity_value]
+ end
+
+ it "does not yield when self > stop" do
+ @step.call(42, 100, -infinity_value, &@prc)
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not yield when stop is Infinity" do
+ @step.call(42, infinity_value, -infinity_value, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "with a Infinity stop and a positive step" do
+ it "does not yield when self is infinity" do
+ @step.call(infinity_value, infinity_value, 1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "with a Infinity stop and a negative step" do
+ it "does not yield when self is negative infinity" do
+ @step.call(-infinity_value, infinity_value, -1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+
+ it "does not yield when self is positive infinity" do
+ @step.call(infinity_value, infinity_value, -1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "with a negative Infinity stop and a positive step" do
+ it "does not yield when self is negative infinity" do
+ @step.call(-infinity_value, -infinity_value, 1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ describe "with a negative Infinity stop and a negative step" do
+ it "does not yield when self is negative infinity" do
+ @step.call(-infinity_value, -infinity_value, -1, &@prc)
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ end
+
+ describe "when step is a String" do
+ describe "with self and stop as Integers" do
+ it "raises an ArgumentError when step is a numeric representation" do
+ -> { @step.call(1, 5, "1") {} }.should.raise(ArgumentError)
+ -> { @step.call(1, 5, "0.1") {} }.should.raise(ArgumentError)
+ -> { @step.call(1, 5, "1/3") {} }.should.raise(ArgumentError)
+ end
+ it "raises an ArgumentError with step as an alphanumeric string" do
+ -> { @step.call(1, 5, "foo") {} }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "with self and stop as Floats" do
+ it "raises an ArgumentError when step is a numeric representation" do
+ -> { @step.call(1.1, 5.1, "1") {} }.should.raise(ArgumentError)
+ -> { @step.call(1.1, 5.1, "0.1") {} }.should.raise(ArgumentError)
+ -> { @step.call(1.1, 5.1, "1/3") {} }.should.raise(ArgumentError)
+ end
+ it "raises an ArgumentError with step as an alphanumeric string" do
+ -> { @step.call(1.1, 5.1, "foo") {} }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ it "does not rescue ArgumentError exceptions" do
+ -> { @step.call(1, 2) { raise ArgumentError, "" }}.should.raise(ArgumentError)
+ end
+
+ it "does not rescue TypeError exceptions" do
+ -> { @step.call(1, 2) { raise TypeError, "" } }.should.raise(TypeError)
+ end
+
+ describe "when no block is given" do
+ step_enum_class = Enumerator::ArithmeticSequence
+
+ it "returns an #{step_enum_class} when not passed a block and self > stop" do
+ @step.call(1, 0, 2).should.instance_of?(step_enum_class)
+ end
+
+ it "returns an #{step_enum_class} when not passed a block and self < stop" do
+ @step.call(1, 2, 3).should.instance_of?(step_enum_class)
+ end
+
+ it "returns an #{step_enum_class} that uses the given step" do
+ @step.call(0, 5, 2).to_a.should.eql? [0, 2, 4]
+ end
+
+ describe "when step is a String" do
+ describe "with self and stop as Integers" do
+ it "returns an Enumerator" do
+ @step.call(1, 5, "foo").should.instance_of?(Enumerator)
+ end
+ end
+
+ describe "with self and stop as Floats" do
+ it "returns an Enumerator" do
+ @step.call(1.1, 5.1, "foo").should.instance_of?(Enumerator)
+ end
+ end
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ describe "when step is a String" do
+ describe "with self and stop as Integers" do
+ it "raises an ArgumentError when step is a numeric representation" do
+ -> { @step.call(1, 5, "1").size }.should.raise(ArgumentError)
+ -> { @step.call(1, 5, "0.1").size }.should.raise(ArgumentError)
+ -> { @step.call(1, 5, "1/3").size }.should.raise(ArgumentError)
+ end
+ it "raises an ArgumentError with step as an alphanumeric string" do
+ -> { @step.call(1, 5, "foo").size }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "with self and stop as Floats" do
+ it "raises an ArgumentError when step is a numeric representation" do
+ -> { @step.call(1.1, 5.1, "1").size }.should.raise(ArgumentError)
+ -> { @step.call(1.1, 5.1, "0.1").size }.should.raise(ArgumentError)
+ -> { @step.call(1.1, 5.1, "1/3").size }.should.raise(ArgumentError)
+ end
+ it "raises an ArgumentError with step as an alphanumeric string" do
+ -> { @step.call(1.1, 5.1, "foo").size }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ describe "when self, stop and step are Integers and step is positive" do
+ it "returns the difference between self and stop divided by the number of steps" do
+ @step.call(5, 10, 11).size.should == 1
+ @step.call(5, 10, 6).size.should == 1
+ @step.call(5, 10, 5).size.should == 2
+ @step.call(5, 10, 4).size.should == 2
+ @step.call(5, 10, 2).size.should == 3
+ @step.call(5, 10, 1).size.should == 6
+ @step.call(5, 10).size.should == 6
+ @step.call(10, 10, 1).size.should == 1
+ end
+
+ it "returns 0 if value > limit" do
+ @step.call(11, 10, 1).size.should == 0
+ end
+ end
+
+ describe "when self, stop and step are Integers and step is negative" do
+ it "returns the difference between self and stop divided by the number of steps" do
+ @step.call(10, 5, -11).size.should == 1
+ @step.call(10, 5, -6).size.should == 1
+ @step.call(10, 5, -5).size.should == 2
+ @step.call(10, 5, -4).size.should == 2
+ @step.call(10, 5, -2).size.should == 3
+ @step.call(10, 5, -1).size.should == 6
+ @step.call(10, 10, -1).size.should == 1
+ end
+
+ it "returns 0 if value < limit" do
+ @step.call(10, 11, -1).size.should == 0
+ end
+ end
+
+ describe "when self, stop or step is a Float" do
+ describe "and step is positive" do
+ it "returns the difference between self and stop divided by the number of steps" do
+ @step.call(5, 10, 11.0).size.should == 1
+ @step.call(5, 10, 6.0).size.should == 1
+ @step.call(5, 10, 5.0).size.should == 2
+ @step.call(5, 10, 4.0).size.should == 2
+ @step.call(5, 10, 2.0).size.should == 3
+ @step.call(5, 10, 0.5).size.should == 11
+ @step.call(5, 10, 1.0).size.should == 6
+ @step.call(5, 10.5).size.should == 6
+ @step.call(10, 10, 1.0).size.should == 1
+ end
+
+ it "returns 0 if value > limit" do
+ @step.call(10, 5.5).size.should == 0
+ @step.call(11, 10, 1.0).size.should == 0
+ @step.call(11, 10, 1.5).size.should == 0
+ @step.call(10, 5, infinity_value).size.should == 0
+ end
+
+ it "returns 1 if step is infinity_value" do
+ @step.call(5, 10, infinity_value).size.should == 1
+ end
+ end
+
+ describe "and step is negative" do
+ it "returns the difference between self and stop divided by the number of steps" do
+ @step.call(10, 5, -11.0).size.should == 1
+ @step.call(10, 5, -6.0).size.should == 1
+ @step.call(10, 5, -5.0).size.should == 2
+ @step.call(10, 5, -4.0).size.should == 2
+ @step.call(10, 5, -2.0).size.should == 3
+ @step.call(10, 5, -0.5).size.should == 11
+ @step.call(10, 5, -1.0).size.should == 6
+ @step.call(10, 10, -1.0).size.should == 1
+ end
+
+ it "returns 0 if value < limit" do
+ @step.call(10, 11, -1.0).size.should == 0
+ @step.call(10, 11, -1.5).size.should == 0
+ @step.call(5, 10, -infinity_value).size.should == 0
+ end
+
+ it "returns 1 if step is infinity_value" do
+ @step.call(10, 5, -infinity_value).size.should == 1
+ end
+ end
+ end
+
+ describe "when stop is not passed" do
+ it "returns infinity_value" do
+ @step.call(1).size.should == infinity_value
+ end
+ end
+
+ describe "when stop is nil" do
+ it "returns infinity_value" do
+ @step.call(1, nil, 5).size.should == infinity_value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/singleton_method_added_spec.rb b/spec/ruby/core/numeric/singleton_method_added_spec.rb
new file mode 100644
index 0000000000..327bb5662a
--- /dev/null
+++ b/spec/ruby/core/numeric/singleton_method_added_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#singleton_method_added" do
+ before :all do
+ class ::NumericSpecs::Subclass
+ # We want restore default Numeric behaviour for this particular test
+ remove_method :singleton_method_added
+ end
+ end
+
+ after :all do
+ class ::NumericSpecs::Subclass
+ # Allow mocking methods again
+ def singleton_method_added(val)
+ end
+ end
+ end
+
+ it "raises a TypeError when trying to define a singleton method on a Numeric" do
+ -> do
+ a = NumericSpecs::Subclass.new
+ def a.test; end
+ end.should.raise(TypeError)
+
+ -> do
+ a = 1
+ def a.test; end
+ end.should.raise(TypeError)
+
+ -> do
+ a = 1.5
+ def a.test; end
+ end.should.raise(TypeError)
+
+ -> do
+ a = bignum_value
+ def a.test; end
+ end.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/numeric/step_spec.rb b/spec/ruby/core/numeric/step_spec.rb
new file mode 100644
index 0000000000..6896009eb6
--- /dev/null
+++ b/spec/ruby/core/numeric/step_spec.rb
@@ -0,0 +1,121 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/step'
+
+describe "Numeric#step" do
+
+ describe 'with positional args' do
+ it "raises an ArgumentError when step is 0" do
+ -> { 1.step(5, 0) {} }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when step is 0.0" do
+ -> { 1.step(2, 0.0) {} }.should.raise(ArgumentError)
+ end
+
+ before :all do
+ # This lambda definition limits to return the arguments it receives.
+ # It's needed to test numeric_step behaviour with positional arguments.
+ @step = -> receiver, *args, &block { receiver.step(*args, &block) }
+ end
+ it_behaves_like :numeric_step, :step
+
+ describe "when no block is given" do
+ step_enum_class = Enumerator::ArithmeticSequence
+
+ describe "returned #{step_enum_class}" do
+ describe "size" do
+ it "defaults to an infinite size" do
+ enum = 1.step
+ enum.size.should == Float::INFINITY
+ end
+ end
+
+ describe "type" do
+ it "returns an instance of Enumerator::ArithmeticSequence" do
+ 1.step(10).class.should == Enumerator::ArithmeticSequence
+ end
+ end
+ end
+ end
+ end
+
+ describe 'with keyword arguments' do
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return infinity_value when limit is nil" do
+ 1.step(by: 42).size.should == infinity_value
+ end
+
+ it "should return infinity_value when ascending towards a limit of Float::INFINITY" do
+ 1.step(to: Float::INFINITY, by: 42).size.should == infinity_value
+ end
+
+ it "should return infinity_value when descending towards a limit of -Float::INFINITY" do
+ 1.step(to: -Float::INFINITY, by: -42).size.should == infinity_value
+ end
+
+ it "should return 1 when the both limit and step are Float::INFINITY" do
+ 1.step(to: Float::INFINITY, by: Float::INFINITY).size.should == 1
+ end
+
+ it "should return 1 when the both limit and step are -Float::INFINITY" do
+ 1.step(to: -Float::INFINITY, by: -Float::INFINITY).size.should == 1
+ end
+ end
+ end
+ end
+
+ before :all do
+ # This lambda transforms a positional step method args into keyword arguments.
+ # It's needed to test numeric_step behaviour with keyword arguments.
+ @step = -> receiver, *args, &block do
+ kw_args = { to: args[0] }
+ kw_args[:by] = args[1] if args.size == 2
+ receiver.step(**kw_args, &block)
+ end
+ end
+ it_behaves_like :numeric_step, :step
+ end
+
+ describe 'with mixed arguments' do
+ it " raises an ArgumentError when step is 0" do
+ -> { 1.step(5, by: 0) { break } }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when step is 0.0" do
+ -> { 1.step(2, by: 0.0) { break } }.should.raise(ArgumentError)
+ end
+
+ it "raises a ArgumentError when limit and to are defined" do
+ -> { 1.step(5, 1, to: 5) { break } }.should.raise(ArgumentError)
+ end
+
+ it "raises a ArgumentError when step and by are defined" do
+ -> { 1.step(5, 1, by: 5) { break } }.should.raise(ArgumentError)
+ end
+
+ describe "when no block is given" do
+ describe "returned Enumerator" do
+ describe "size" do
+ end
+ end
+ end
+
+ before :all do
+ # This lambda definition transforms a positional step method args into
+ # a mix of positional and keyword arguments.
+ # It's needed to test numeric_step behaviour with positional mixed with
+ # keyword arguments.
+ @step = -> receiver, *args, &block do
+ if args.size == 2
+ receiver.step(args[0], by: args[1], &block)
+ else
+ receiver.step(*args, &block)
+ end
+ end
+ end
+ it_behaves_like :numeric_step, :step
+ end
+end
diff --git a/spec/ruby/core/numeric/to_c_spec.rb b/spec/ruby/core/numeric/to_c_spec.rb
new file mode 100644
index 0000000000..70b7a9dd0c
--- /dev/null
+++ b/spec/ruby/core/numeric/to_c_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#to_c" do
+ before :all do
+ @numbers = [
+ 0,
+ 29871,
+ 99999999999999**99,
+ -72628191273,
+ Rational(2,3),
+ Rational(1.898),
+ Rational(-238),
+ 29282.2827,
+ -2927.00091,
+ 0.0,
+ 12.0,
+ Float::MAX,
+ infinity_value,
+ nan_value
+ ]
+ end
+
+ it "returns a Complex object" do
+ @numbers.each do |number|
+ number.to_c.should.instance_of?(Complex)
+ end
+ end
+
+ it "uses self as the real component" do
+ @numbers.each do |number|
+ real = number.to_c.real
+ if Float === number and number.nan?
+ real.nan?.should == true
+ else
+ real.should == number
+ end
+ end
+ end
+
+ it "uses 0 as the imaginary component" do
+ @numbers.each do |number|
+ number.to_c.imag.should == 0
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/to_int_spec.rb b/spec/ruby/core/numeric/to_int_spec.rb
new file mode 100644
index 0000000000..3cc39a6d40
--- /dev/null
+++ b/spec/ruby/core/numeric/to_int_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#to_int" do
+ it "returns self#to_i" do
+ obj = NumericSpecs::Subclass.new
+ obj.should_receive(:to_i).and_return(:result)
+ obj.to_int.should == :result
+ end
+end
diff --git a/spec/ruby/core/numeric/truncate_spec.rb b/spec/ruby/core/numeric/truncate_spec.rb
new file mode 100644
index 0000000000..f1592334c5
--- /dev/null
+++ b/spec/ruby/core/numeric/truncate_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#truncate" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "converts self to a Float (using #to_f) and returns the #truncate'd result" do
+ @obj.should_receive(:to_f).and_return(2.5555, -2.3333)
+ @obj.truncate.should == 2
+ @obj.truncate.should == -2
+ end
+end
diff --git a/spec/ruby/core/numeric/uminus_spec.rb b/spec/ruby/core/numeric/uminus_spec.rb
new file mode 100644
index 0000000000..39065fa392
--- /dev/null
+++ b/spec/ruby/core/numeric/uminus_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Numeric#-@" do
+ it "returns the same value with opposite sign (integers)" do
+ 0.send(:-@).should == 0
+ 100.send(:-@).should == -100
+ -100.send(:-@).should == 100
+ end
+
+ it "returns the same value with opposite sign (floats)" do
+ 34.56.send(:-@).should == -34.56
+ -34.56.send(:-@).should == 34.56
+ end
+
+ it "returns the same value with opposite sign (two complement)" do
+ 2147483648.send(:-@).should == -2147483648
+ -2147483648.send(:-@).should == 2147483648
+ 9223372036854775808.send(:-@).should == -9223372036854775808
+ -9223372036854775808.send(:-@).should == 9223372036854775808
+ end
+
+ describe "with a Numeric subclass" do
+ it "calls #coerce(0) on self, then subtracts the second element of the result from the first" do
+ ten = mock_numeric('10')
+ zero = mock_numeric('0')
+ ten.should_receive(:coerce).with(0).and_return([zero, ten])
+ zero.should_receive(:-).with(ten).and_return(-10)
+ ten.send(:-@).should == -10
+ end
+ end
+end
diff --git a/spec/ruby/core/numeric/uplus_spec.rb b/spec/ruby/core/numeric/uplus_spec.rb
new file mode 100644
index 0000000000..88cf5e037b
--- /dev/null
+++ b/spec/ruby/core/numeric/uplus_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#+@" do
+ it "returns self" do
+ obj = NumericSpecs::Subclass.new
+ obj.send(:+@).should == obj
+ end
+end
diff --git a/spec/ruby/core/numeric/zero_spec.rb b/spec/ruby/core/numeric/zero_spec.rb
new file mode 100644
index 0000000000..0fb7619bcd
--- /dev/null
+++ b/spec/ruby/core/numeric/zero_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Numeric#zero?" do
+ before :each do
+ @obj = NumericSpecs::Subclass.new
+ end
+
+ it "returns true if self is 0" do
+ @obj.should_receive(:==).with(0).and_return(true)
+ @obj.should.zero?
+ end
+
+ it "returns false if self is not 0" do
+ @obj.should_receive(:==).with(0).and_return(false)
+ @obj.should_not.zero?
+ end
+end
diff --git a/spec/ruby/core/objectspace/_id2ref_spec.rb b/spec/ruby/core/objectspace/_id2ref_spec.rb
new file mode 100644
index 0000000000..a9fd526b7d
--- /dev/null
+++ b/spec/ruby/core/objectspace/_id2ref_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../../spec_helper'
+
+ruby_version_is "4.0" do
+ describe "ObjectSpace._id2ref" do
+ it "is deprecated" do
+ id = nil.object_id
+ -> {
+ ObjectSpace._id2ref(id)
+ }.should complain(/warning: ObjectSpace\._id2ref is deprecated/)
+ end
+ end
+end
+
+ruby_version_is ""..."4.0" do
+ describe "ObjectSpace._id2ref" do
+ it "converts an object id to a reference to the object" do
+ s = "I am a string"
+ r = ObjectSpace._id2ref(s.object_id)
+ r.should == s
+ end
+
+ it "retrieves true by object_id" do
+ ObjectSpace._id2ref(true.object_id).should == true
+ end
+
+ it "retrieves false by object_id" do
+ ObjectSpace._id2ref(false.object_id).should == false
+ end
+
+ it "retrieves nil by object_id" do
+ ObjectSpace._id2ref(nil.object_id).should == nil
+ end
+
+ it "retrieves a small Integer by object_id" do
+ ObjectSpace._id2ref(1.object_id).should == 1
+ ObjectSpace._id2ref((-42).object_id).should == -42
+ end
+
+ it "retrieves a large Integer by object_id" do
+ obj = 1 << 88
+ ObjectSpace._id2ref(obj.object_id).should.equal?(obj)
+ end
+
+ it "retrieves a Symbol by object_id" do
+ ObjectSpace._id2ref(:sym.object_id).should.equal?(:sym)
+ end
+
+ it "retrieves a String by object_id" do
+ obj = "str"
+ ObjectSpace._id2ref(obj.object_id).should.equal?(obj)
+ end
+
+ it "retrieves a frozen literal String by object_id" do
+ ObjectSpace._id2ref("frozen string literal _id2ref".freeze.object_id).should.equal?("frozen string literal _id2ref".freeze)
+ end
+
+ it "retrieves an Encoding by object_id" do
+ ObjectSpace._id2ref(Encoding::UTF_8.object_id).should.equal?(Encoding::UTF_8)
+ end
+
+ it 'raises RangeError when an object could not be found' do
+ proc { ObjectSpace._id2ref(1 << 60) }.should.raise(RangeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/objectspace/count_objects_spec.rb b/spec/ruby/core/objectspace/count_objects_spec.rb
new file mode 100644
index 0000000000..e9831a3a42
--- /dev/null
+++ b/spec/ruby/core/objectspace/count_objects_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "ObjectSpace.count_objects" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/objectspace/define_finalizer_spec.rb b/spec/ruby/core/objectspace/define_finalizer_spec.rb
new file mode 100644
index 0000000000..5441cb4a21
--- /dev/null
+++ b/spec/ruby/core/objectspace/define_finalizer_spec.rb
@@ -0,0 +1,217 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Why do we not test that finalizers are run by the GC? The documentation
+# says that finalizers are never guaranteed to be run, so we can't
+# spec that they are. On some implementations of Ruby the finalizers may
+# run asynchronously, meaning that we can't predict when they'll run,
+# even if they were guaranteed to do so. Even on MRI finalizers can be
+# very unpredictable, due to conservative stack scanning and references
+# left in unused memory.
+
+describe "ObjectSpace.define_finalizer" do
+ it "raises an ArgumentError if the action does not respond to call" do
+ -> {
+ ObjectSpace.define_finalizer(Object.new, mock("ObjectSpace.define_finalizer no #call"))
+ }.should.raise(ArgumentError)
+ end
+
+ it "accepts an object and a proc" do
+ handler = -> id { id }
+ ObjectSpace.define_finalizer(Object.new, handler).should == [0, handler]
+ end
+
+ it "accepts an object and a bound method" do
+ handler = mock("callable")
+ def handler.finalize(id) end
+ finalize = handler.method(:finalize)
+ ObjectSpace.define_finalizer(Object.new, finalize).should == [0, finalize]
+ end
+
+ it "accepts an object and a callable" do
+ handler = mock("callable")
+ def handler.call(id) end
+ ObjectSpace.define_finalizer(Object.new, handler).should == [0, handler]
+ end
+
+ it "accepts an object and a block" do
+ handler = -> id { id }
+ ObjectSpace.define_finalizer(Object.new, &handler).should == [0, handler]
+ end
+
+ it "raises ArgumentError trying to define a finalizer on a non-reference" do
+ -> {
+ ObjectSpace.define_finalizer(:blah) { 1 }
+ }.should.raise(ArgumentError)
+ end
+
+ # see [ruby-core:24095]
+ it "calls finalizer on process termination" do
+ code = <<-RUBY
+ def scoped
+ Proc.new { puts "finalizer run" }
+ end
+ handler = scoped
+ obj = +"Test"
+ ObjectSpace.define_finalizer(obj, handler)
+ exit 0
+ RUBY
+
+ ruby_exe(code, :args => "2>&1").should.include?("finalizer run\n")
+ end
+
+ it "warns if the finalizer has the object as the receiver" do
+ code = <<-RUBY
+ class CapturesSelf
+ def initialize
+ ObjectSpace.define_finalizer(self, proc {
+ puts "finalizer run"
+ })
+ end
+ end
+ CapturesSelf.new
+ exit 0
+ RUBY
+
+ ruby_exe(code, :args => "2>&1").should.include?("warning: finalizer references object to be finalized\n")
+ end
+
+ it "warns if the finalizer is a method bound to the receiver" do
+ code = <<-RUBY
+ class CapturesSelf
+ def initialize
+ ObjectSpace.define_finalizer(self, method(:finalize))
+ end
+ def finalize(id)
+ puts "finalizer run"
+ end
+ end
+ CapturesSelf.new
+ exit 0
+ RUBY
+
+ ruby_exe(code, :args => "2>&1").should.include?("warning: finalizer references object to be finalized\n")
+ end
+
+ it "warns if the finalizer was a block in the receiver" do
+ code = <<-RUBY
+ class CapturesSelf
+ def initialize
+ ObjectSpace.define_finalizer(self) do
+ puts "finalizer run"
+ end
+ end
+ end
+ CapturesSelf.new
+ exit 0
+ RUBY
+
+ ruby_exe(code, :args => "2>&1").should.include?("warning: finalizer references object to be finalized\n")
+ end
+
+ it "calls a finalizer at exit even if it is self-referencing" do
+ code = <<-RUBY
+ obj = +"Test"
+ handler = Proc.new { puts "finalizer run" }
+ ObjectSpace.define_finalizer(obj, handler)
+ exit 0
+ RUBY
+
+ ruby_exe(code).should.include?("finalizer run\n")
+ end
+
+ it "calls a finalizer at exit even if it is indirectly self-referencing" do
+ code = <<-RUBY
+ class CapturesSelf
+ def initialize
+ ObjectSpace.define_finalizer(self, finalizer(self))
+ end
+ def finalizer(zelf)
+ proc do
+ puts "finalizer run"
+ end
+ end
+ end
+ CapturesSelf.new
+ exit 0
+ RUBY
+
+ ruby_exe(code, :args => "2>&1").should.include?("finalizer run\n")
+ end
+
+ it "calls a finalizer defined in a finalizer running at exit" do
+ code = <<-RUBY
+ obj = +"Test"
+ handler = Proc.new do
+ obj2 = +"Test"
+ handler2 = Proc.new { puts "finalizer 2 run" }
+ ObjectSpace.define_finalizer(obj2, handler2)
+ exit 0
+ end
+ ObjectSpace.define_finalizer(obj, handler)
+ exit 0
+ RUBY
+
+ ruby_exe(code, :args => "2>&1").should.include?("finalizer 2 run\n")
+ end
+
+ it "allows multiple finalizers with different 'callables' to be defined" do
+ code = <<-'RUBY'
+ obj = Object.new
+
+ ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized1\n" })
+ ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized2\n" })
+
+ exit 0
+ RUBY
+
+ ruby_exe(code).lines.sort.should == ["finalized1\n", "finalized2\n"]
+ end
+
+ it "defines same finalizer only once" do
+ code = <<~RUBY
+ obj = Object.new
+ p = proc { |id| print "ok" }
+ ObjectSpace.define_finalizer(obj, p.dup)
+ ObjectSpace.define_finalizer(obj, p.dup)
+ RUBY
+
+ ruby_exe(code).should == "ok"
+ end
+
+ it "returns the defined finalizer" do
+ obj = Object.new
+ p = proc { |id| }
+ p2 = p.dup
+
+ ret = ObjectSpace.define_finalizer(obj, p)
+ ret.should == [0, p]
+ ret[1].should.equal?(p)
+
+ ret = ObjectSpace.define_finalizer(obj, p2)
+ ret.should == [0, p]
+ ret[1].should.equal?(p)
+ end
+
+ describe "when $VERBOSE is not nil" do
+ it "warns if an exception is raised in finalizer" do
+ code = <<-RUBY
+ ObjectSpace.define_finalizer(Object.new) { raise "finalizing" }
+ RUBY
+
+ out = ruby_exe(code, args: "2>&1")
+ out.should.include?("warning: Exception in finalizer")
+ out.should.include?("finalizing")
+ end
+ end
+
+ describe "when $VERBOSE is nil" do
+ it "does not warn even if an exception is raised in finalizer" do
+ code = <<-RUBY
+ ObjectSpace.define_finalizer(Object.new) { raise "finalizing" }
+ RUBY
+
+ ruby_exe(code, args: "2>&1", options: "-W0").should == ""
+ end
+ end
+end
diff --git a/spec/ruby/core/objectspace/each_object_spec.rb b/spec/ruby/core/objectspace/each_object_spec.rb
new file mode 100644
index 0000000000..aee0fd629c
--- /dev/null
+++ b/spec/ruby/core/objectspace/each_object_spec.rb
@@ -0,0 +1,213 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "ObjectSpace.each_object" do
+ it "calls the block once for each living, non-immediate object in the Ruby process" do
+ klass = Class.new
+ new_obj = klass.new
+
+ yields = 0
+ count = ObjectSpace.each_object(klass) do |obj|
+ obj.should == new_obj
+ yields += 1
+ end
+ count.should == 1
+ yields.should == 1
+
+ # this is needed to prevent the new_obj from being GC'd too early
+ new_obj.should_not == nil
+ end
+
+ it "calls the block once for each class, module in the Ruby process" do
+ klass = Class.new
+ mod = Module.new
+
+ [klass, mod].each do |k|
+ yields = 0
+ got_it = false
+ count = ObjectSpace.each_object(k.class) do |obj|
+ got_it = true if obj == k
+ yields += 1
+ end
+ got_it.should == true
+ count.should == yields
+ end
+ end
+
+ it "returns an enumerator if not given a block" do
+ klass = Class.new
+ new_obj = klass.new
+
+ counter = ObjectSpace.each_object(klass)
+ counter.should.instance_of?(Enumerator)
+ counter.each{}.should == 1
+ # this is needed to prevent the new_obj from being GC'd too early
+ new_obj.should_not == nil
+ end
+
+ it "finds an object stored in a global variable" do
+ $object_space_global_variable = ObjectSpaceFixtures::ObjectToBeFound.new(:global)
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:global)
+ end
+
+ it "finds an object stored in a top-level constant" do
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:top_level_constant)
+ end
+
+ it "finds an object stored in a second-level constant" do
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:second_level_constant)
+ end
+
+ it "finds an object stored in a local variable" do
+ local = ObjectSpaceFixtures::ObjectToBeFound.new(:local)
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:local)
+ end
+
+ it "finds an object stored in a local variable captured in a block explicitly" do
+ proc = Proc.new {
+ local_in_block = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_block_explicit)
+ Proc.new { local_in_block }
+ }.call
+
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:local_in_block_explicit)
+ end
+
+ it "finds an object stored in a local variable captured in a block implicitly" do
+ proc = Proc.new {
+ local_in_block = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_block_implicit)
+ Proc.new { }
+ }.call
+
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:local_in_block_implicit)
+ end
+
+ it "finds an object stored in a local variable captured in by a method defined with a block" do
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:captured_by_define_method)
+ end
+
+ it "finds an object stored in a local variable captured in a Proc#binding" do
+ binding = Proc.new {
+ local_in_proc_binding = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_proc_binding)
+ Proc.new { }.binding
+ }.call
+
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:local_in_proc_binding)
+ end
+
+ it "finds an object stored in a local variable captured in a Kernel#binding" do
+ b = Proc.new {
+ local_in_kernel_binding = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_kernel_binding)
+ binding
+ }.call
+
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:local_in_kernel_binding)
+ end
+
+ it "finds an object stored in a local variable set in a binding manually" do
+ b = binding
+ b.eval("local = ObjectSpaceFixtures::ObjectToBeFound.new(:local_in_manual_binding)")
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:local_in_manual_binding)
+ end
+
+ it "finds an object stored in an array" do
+ array = [ObjectSpaceFixtures::ObjectToBeFound.new(:array)]
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:array)
+ end
+
+ it "finds an object stored in a hash key" do
+ hash = {ObjectSpaceFixtures::ObjectToBeFound.new(:hash_key) => :value}
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:hash_key)
+ end
+
+ it "finds an object stored in a hash value" do
+ hash = {a: ObjectSpaceFixtures::ObjectToBeFound.new(:hash_value)}
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:hash_value)
+ end
+
+ it "finds an object stored in an instance variable" do
+ local = ObjectSpaceFixtures::ObjectWithInstanceVariable.new
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:instance_variable)
+ end
+
+ it "finds an object stored in a thread local" do
+ thread = Thread.new {}
+ thread.thread_variable_set(:object_space_thread_local, ObjectSpaceFixtures::ObjectToBeFound.new(:thread_local))
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:thread_local)
+ thread.join
+ end
+
+ it "finds an object stored in a fiber local" do
+ Thread.current[:object_space_fiber_local] = ObjectSpaceFixtures::ObjectToBeFound.new(:fiber_local)
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:fiber_local)
+ end
+
+ it "finds an object captured in an at_exit handler" do
+ Proc.new {
+ local = ObjectSpaceFixtures::ObjectToBeFound.new(:at_exit)
+
+ at_exit do
+ local
+ end
+ }.call
+
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:at_exit)
+ end
+
+ it "finds an object captured in finalizer" do
+ alive = Object.new
+
+ Proc.new {
+ local = ObjectSpaceFixtures::ObjectToBeFound.new(:finalizer)
+
+ ObjectSpace.define_finalizer(alive, Proc.new {
+ local
+ })
+ }.call
+
+ ObjectSpaceFixtures.to_be_found_symbols.should.include?(:finalizer)
+
+ alive.should_not == nil
+ end
+
+ describe "on singleton classes" do
+ before :each do
+ @klass = Class.new
+ instance = @klass.new
+ @sclass = instance.singleton_class
+ @meta = @klass.singleton_class
+ end
+
+ it "does not walk hidden metaclasses" do
+ klass = Class.new.singleton_class
+ ancestors = ObjectSpace.each_object(Class).select { |c| klass.is_a? c }
+ hidden = ancestors.find { |h| h.inspect.include? klass.inspect }
+ hidden.should == nil
+ end
+
+ it "walks singleton classes" do
+ @sclass.should.is_a?(@meta)
+ ObjectSpace.each_object(@meta).to_a.should.include?(@sclass)
+ end
+ end
+
+ it "walks a class and its normal descendants when passed the class's singleton class" do
+ a = Class.new
+ b = Class.new(a)
+ c = Class.new(a)
+ d = Class.new(b)
+
+ c_instance = c.new
+ c_sclass = c_instance.singleton_class
+
+ expected = [ a, b, c, d ]
+
+ expected << c_sclass
+ c_sclass.should.is_a?(a.singleton_class)
+
+ b.extend Enumerable # included modules should not be walked
+
+ classes = ObjectSpace.each_object(a.singleton_class).to_a
+
+ classes.sort_by(&:object_id).should == expected.sort_by(&:object_id)
+ end
+end
diff --git a/spec/ruby/core/objectspace/fixtures/classes.rb b/spec/ruby/core/objectspace/fixtures/classes.rb
new file mode 100644
index 0000000000..612156c180
--- /dev/null
+++ b/spec/ruby/core/objectspace/fixtures/classes.rb
@@ -0,0 +1,64 @@
+module ObjectSpaceFixtures
+ def self.garbage
+ blah
+ end
+
+ def self.blah
+ o = "hello"
+ @garbage_objid = o.object_id
+ return o
+ end
+
+ @last_objid = nil
+
+ def self.last_objid
+ @last_objid
+ end
+
+ def self.garbage_objid
+ @garbage_objid
+ end
+
+ def self.make_finalizer
+ proc { |obj_id| @last_objid = obj_id }
+ end
+
+ def self.define_finalizer
+ handler = -> obj { ScratchPad.record :finalized }
+ ObjectSpace.define_finalizer "#{rand 5}", handler
+ end
+
+ def self.scoped(wr)
+ return Proc.new { wr.write "finalized"; wr.close }
+ end
+
+ class ObjectToBeFound
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+ end
+
+ class ObjectWithInstanceVariable
+ def initialize
+ @instance_variable = ObjectToBeFound.new(:instance_variable)
+ end
+ end
+
+ def self.to_be_found_symbols
+ ObjectSpace.each_object(ObjectToBeFound).map do |o|
+ o.name
+ end
+ end
+
+ o = ObjectToBeFound.new(:captured_by_define_method)
+ define_method :capturing_method do
+ o
+ end
+
+ SECOND_LEVEL_CONSTANT = ObjectToBeFound.new(:second_level_constant)
+
+end
+
+OBJECT_SPACE_TOP_LEVEL_CONSTANT = ObjectSpaceFixtures::ObjectToBeFound.new(:top_level_constant)
diff --git a/spec/ruby/core/objectspace/garbage_collect_spec.rb b/spec/ruby/core/objectspace/garbage_collect_spec.rb
new file mode 100644
index 0000000000..d2db22e0aa
--- /dev/null
+++ b/spec/ruby/core/objectspace/garbage_collect_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+
+describe "ObjectSpace.garbage_collect" do
+
+ it "can be invoked without any exceptions" do
+ -> { ObjectSpace.garbage_collect }.should_not.raise
+ end
+
+ it "accepts keyword arguments" do
+ ObjectSpace.garbage_collect(full_mark: true, immediate_sweep: true).should == nil
+ end
+
+ it "ignores the supplied block" do
+ -> { ObjectSpace.garbage_collect {} }.should_not.raise
+ end
+
+ it "always returns nil" do
+ ObjectSpace.garbage_collect.should == nil
+ ObjectSpace.garbage_collect.should == nil
+ end
+
+end
diff --git a/spec/ruby/core/objectspace/undefine_finalizer_spec.rb b/spec/ruby/core/objectspace/undefine_finalizer_spec.rb
new file mode 100644
index 0000000000..98ffc6a986
--- /dev/null
+++ b/spec/ruby/core/objectspace/undefine_finalizer_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "ObjectSpace.undefine_finalizer" do
+ it "removes finalizers for an object" do
+ code = <<~RUBY
+ obj = Object.new
+ ObjectSpace.define_finalizer(obj, proc { |id| puts "hello" })
+ ObjectSpace.undefine_finalizer(obj)
+ RUBY
+
+ ruby_exe(code).should.empty?
+ end
+
+ it "should not remove finalizers for a frozen object" do
+ code = <<~RUBY
+ obj = Object.new
+ ObjectSpace.define_finalizer(obj, proc { |id| print "ok" })
+ obj.freeze
+ begin
+ ObjectSpace.undefine_finalizer(obj)
+ rescue
+ end
+ RUBY
+
+ ruby_exe(code).should == "ok"
+ end
+
+ it "should raise when removing finalizers for a frozen object" do
+ obj = Object.new
+ obj.freeze
+ -> { ObjectSpace.undefine_finalizer(obj) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/clear_spec.rb b/spec/ruby/core/objectspace/weakkeymap/clear_spec.rb
new file mode 100644
index 0000000000..b1804ec9b0
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/clear_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakKeyMap#clear" do
+ it "removes all the entries" do
+ m = ObjectSpace::WeakKeyMap.new
+
+ key = Object.new
+ value = Object.new
+ m[key] = value
+
+ key2 = Object.new
+ value2 = Object.new
+ m[key2] = value2
+
+ m.clear
+
+ m.key?(key).should == false
+ m.key?(key2).should == false
+ end
+
+ it "returns self" do
+ m = ObjectSpace::WeakKeyMap.new
+ m.clear.should.equal?(m)
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/delete_spec.rb b/spec/ruby/core/objectspace/weakkeymap/delete_spec.rb
new file mode 100644
index 0000000000..ad32c2c75e
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/delete_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakKeyMap#delete" do
+ it "removes the entry and returns the deleted value" do
+ m = ObjectSpace::WeakKeyMap.new
+ key = Object.new
+ value = Object.new
+ m[key] = value
+
+ m.delete(key).should == value
+ m.key?(key).should == false
+ end
+
+ it "uses equality semantic" do
+ m = ObjectSpace::WeakKeyMap.new
+ key = "foo".upcase
+ value = Object.new
+ m[key] = value
+
+ m.delete("foo".upcase).should == value
+ m.key?(key).should == false
+ end
+
+ it "calls supplied block if the key is not found" do
+ key = Object.new
+ m = ObjectSpace::WeakKeyMap.new
+ return_value = m.delete(key) do |yielded_key|
+ yielded_key.should == key
+ 5
+ end
+ return_value.should == 5
+ end
+
+ it "returns nil if the key is not found when no block is given" do
+ m = ObjectSpace::WeakKeyMap.new
+ m.delete(Object.new).should == nil
+ end
+
+ it "returns nil when a key cannot be garbage collected" do
+ map = ObjectSpace::WeakKeyMap.new
+
+ map.delete(1).should == nil
+ map.delete(1.0).should == nil
+ map.delete(:a).should == nil
+ map.delete(true).should == nil
+ map.delete(false).should == nil
+ map.delete(nil).should == nil
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb b/spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb
new file mode 100644
index 0000000000..53eff79c40
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/element_reference_spec.rb
@@ -0,0 +1,105 @@
+require_relative '../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "ObjectSpace::WeakKeyMap#[]" do
+ it "is faithful to the map's content" do
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+ map[key1] = ref1
+ map[key1].should == ref1
+ map[key1] = ref1
+ map[key1].should == ref1
+ map[key2] = ref2
+ map[key1].should == ref1
+ map[key2].should == ref2
+ end
+
+ it "compares keys with #eql? semantics" do
+ map = ObjectSpace::WeakKeyMap.new
+ key = [1.0]
+ map[key] = "x"
+ map[[1]].should == nil
+ map[[1.0]].should == "x"
+ key.should == [1.0] # keep the key alive until here to keep the map entry
+
+ map = ObjectSpace::WeakKeyMap.new
+ key = [1]
+ map[key] = "x"
+ map[[1.0]].should == nil
+ map[[1]].should == "x"
+ key.should == [1] # keep the key alive until here to keep the map entry
+
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2 = %w[a a].map(&:upcase)
+ ref = "x"
+ map[key1] = ref
+ map[key2].should == ref
+ end
+
+ it "compares key via #hash first" do
+ x = mock('0')
+ x.should_receive(:hash).and_return(0)
+
+ map = ObjectSpace::WeakKeyMap.new
+ key = 'foo'
+ map[key] = :bar
+ map[x].should == nil
+ end
+
+ it "does not compare keys with different #hash values via #eql?" do
+ x = mock('x')
+ x.should_not_receive(:eql?)
+ x.stub!(:hash).and_return(0)
+
+ y = mock('y')
+ y.should_not_receive(:eql?)
+ y.stub!(:hash).and_return(1)
+
+ map = ObjectSpace::WeakKeyMap.new
+ map[y] = 1
+ map[x].should == nil
+ end
+
+ it "compares keys with the same #hash value via #eql?" do
+ x = mock('x')
+ x.should_receive(:eql?).and_return(true)
+ x.stub!(:hash).and_return(42)
+
+ y = mock('y')
+ y.should_not_receive(:eql?)
+ y.stub!(:hash).and_return(42)
+
+ map = ObjectSpace::WeakKeyMap.new
+ map[y] = 1
+ map[x].should == 1
+ end
+
+ it "finds a value via an identical key even when its #eql? isn't reflexive" do
+ x = mock('x')
+ x.should_receive(:hash).at_least(1).and_return(42)
+ x.stub!(:eql?).and_return(false) # Stubbed for clarity and latitude in implementation; not actually sent by MRI.
+
+ map = ObjectSpace::WeakKeyMap.new
+ map[x] = :x
+ map[x].should == :x
+ end
+
+ it "supports keys with private #hash method" do
+ key = WeakKeyMapSpecs::KeyWithPrivateHash.new
+ map = ObjectSpace::WeakKeyMap.new
+ map[key] = 42
+ map[key].should == 42
+ end
+
+ it "returns nil and does not raise error when a key cannot be garbage collected" do
+ map = ObjectSpace::WeakKeyMap.new
+
+ map[1].should == nil
+ map[1.0].should == nil
+ map[:a].should == nil
+ map[true].should == nil
+ map[false].should == nil
+ map[nil].should == nil
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb b/spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb
new file mode 100644
index 0000000000..cf59aebc6f
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/element_set_spec.rb
@@ -0,0 +1,80 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakKeyMap#[]=" do
+ def should_accept(map, key, value)
+ (map[key] = value).should == value
+ map.should.key?(key)
+ map[key].should == value
+ end
+
+ it "is correct" do
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+ should_accept(map, key1, ref1)
+ should_accept(map, key1, ref1)
+ should_accept(map, key2, ref2)
+ map[key1].should == ref1
+ end
+
+ it "requires the keys to implement #hash" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[BasicObject.new] = 1 }.should.raise(NoMethodError, /undefined method [`']hash' for an instance of BasicObject/)
+ end
+
+ it "accepts frozen keys or values" do
+ map = ObjectSpace::WeakKeyMap.new
+ x = Object.new
+ should_accept(map, x, true)
+ should_accept(map, x, false)
+ should_accept(map, x, 42)
+ should_accept(map, x, :foo)
+
+ y = Object.new.freeze
+ should_accept(map, x, y)
+ should_accept(map, y, x)
+ end
+
+ it "does not duplicate and freeze String keys (like Hash#[]= does)" do
+ map = ObjectSpace::WeakKeyMap.new
+ key = +"a"
+ map[key] = 1
+
+ map.getkey("a").should.equal? key
+ map.getkey("a").should_not.frozen?
+
+ key.should == "a" # keep the key alive until here to keep the map entry
+ end
+
+ context "a key cannot be garbage collected" do
+ it "raises ArgumentError when Integer is used as a key" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[1] = "x" }.should.raise(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/)
+ end
+
+ it "raises ArgumentError when Float is used as a key" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[1.0] = "x" }.should.raise(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/)
+ end
+
+ it "raises ArgumentError when Symbol is used as a key" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[:a] = "x" }.should.raise(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/)
+ end
+
+ it "raises ArgumentError when true is used as a key" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[true] = "x" }.should.raise(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/)
+ end
+
+ it "raises ArgumentError when false is used as a key" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[false] = "x" }.should.raise(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/)
+ end
+
+ it "raises ArgumentError when nil is used as a key" do
+ map = ObjectSpace::WeakKeyMap.new
+ -> { map[nil] = "x" }.should.raise(ArgumentError, /WeakKeyMap (keys )?must be garbage collectable/)
+ end
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb b/spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb
new file mode 100644
index 0000000000..0fd04551b5
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/fixtures/classes.rb
@@ -0,0 +1,5 @@
+module WeakKeyMapSpecs
+ class KeyWithPrivateHash
+ private :hash
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb b/spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb
new file mode 100644
index 0000000000..798a2e2cda
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/getkey_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakKeyMap#getkey" do
+ it "returns the existing equal key" do
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2 = %w[a a].map(&:upcase)
+
+ map[key1] = true
+ map.getkey(key2).should.equal?(key1)
+ map.getkey("X").should == nil
+
+ key1.should == "A" # keep the key alive until here to keep the map entry
+ key2.should == "A" # keep the key alive until here to keep the map entry
+ end
+
+ it "returns nil when a key cannot be garbage collected" do
+ map = ObjectSpace::WeakKeyMap.new
+
+ map.getkey(1).should == nil
+ map.getkey(1.0).should == nil
+ map.getkey(:a).should == nil
+ map.getkey(true).should == nil
+ map.getkey(false).should == nil
+ map.getkey(nil).should == nil
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb b/spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb
new file mode 100644
index 0000000000..b6bb469158
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/inspect_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakKeyMap#inspect" do
+ it "only displays size in output" do
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2, key3 = "foo", "bar", "bar"
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=0>\z/
+ map[key1] = 1
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=1>\z/
+ map[key2] = 2
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=2>\z/
+ map[key3] = 3
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakKeyMap:0x\h+ size=2>\z/
+
+ key1.should == "foo" # keep the key alive until here to keep the map entry
+ key2.should == "bar" # keep the key alive until here to keep the map entry
+ key3.should == "bar" # keep the key alive until here to keep the map entry
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakkeymap/key_spec.rb b/spec/ruby/core/objectspace/weakkeymap/key_spec.rb
new file mode 100644
index 0000000000..e0b6866671
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakkeymap/key_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakKeyMap#key?" do
+ it "recognizes keys in use" do
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+
+ map[key1] = ref1
+ map.key?(key1).should == true
+ map[key1] = ref1
+ map.key?(key1).should == true
+ map[key2] = ref2
+ map.key?(key2).should == true
+ end
+
+ it "matches using equality semantics" do
+ map = ObjectSpace::WeakKeyMap.new
+ key1, key2 = %w[a a].map(&:upcase)
+ ref = "x"
+ map[key1] = ref
+ map.key?(key2).should == true
+ end
+
+ it "reports true if the pair exists and the value is nil" do
+ map = ObjectSpace::WeakKeyMap.new
+ key = Object.new
+ map[key] = nil
+ map.key?(key).should == true
+ end
+
+ it "returns false when a key cannot be garbage collected" do
+ map = ObjectSpace::WeakKeyMap.new
+
+ map.key?(1).should == false
+ map.key?(1.0).should == false
+ map.key?(:a).should == false
+ map.key?(true).should == false
+ map.key?(false).should == false
+ map.key?(nil).should == false
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/delete_spec.rb b/spec/ruby/core/objectspace/weakmap/delete_spec.rb
new file mode 100644
index 0000000000..03beebbb83
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/delete_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakMap#delete" do
+ it "removes the entry and returns the deleted value" do
+ m = ObjectSpace::WeakMap.new
+ key = Object.new
+ value = Object.new
+ m[key] = value
+
+ m.delete(key).should == value
+ m.key?(key).should == false
+ end
+
+ it "calls supplied block if the key is not found" do
+ key = Object.new
+ m = ObjectSpace::WeakMap.new
+ return_value = m.delete(key) do |yielded_key|
+ yielded_key.should == key
+ 5
+ end
+ return_value.should == 5
+ end
+
+ it "returns nil if the key is not found when no block is given" do
+ m = ObjectSpace::WeakMap.new
+ m.delete(Object.new).should == nil
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/each_key_spec.rb b/spec/ruby/core/objectspace/weakmap/each_key_spec.rb
new file mode 100644
index 0000000000..df971deeb9
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/each_key_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/members'
+require_relative 'shared/each'
+
+describe "ObjectSpace::WeakMap#each_key" do
+ it_behaves_like :weakmap_members, -> map { a = []; map.each_key{ |k| a << k }; a }, %w[A B]
+end
+
+describe "ObjectSpace::WeakMap#each_key" do
+ it_behaves_like :weakmap_each, :each_key
+end
diff --git a/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb b/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb
new file mode 100644
index 0000000000..ea29edbd2f
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/each_pair_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/members'
+require_relative 'shared/each'
+
+describe "ObjectSpace::WeakMap#each_pair" do
+ it_behaves_like :weakmap_members, -> map { a = []; map.each_pair{ |k,v| a << "#{k}#{v}" }; a }, %w[Ax By]
+end
+
+describe "ObjectSpace::WeakMap#each_key" do
+ it_behaves_like :weakmap_each, :each_pair
+end
diff --git a/spec/ruby/core/objectspace/weakmap/each_spec.rb b/spec/ruby/core/objectspace/weakmap/each_spec.rb
new file mode 100644
index 0000000000..46fcb66a6f
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/each_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/members'
+require_relative 'shared/each'
+
+describe "ObjectSpace::WeakMap#each" do
+ it_behaves_like :weakmap_members, -> map { a = []; map.each{ |k,v| a << "#{k}#{v}" }; a }, %w[Ax By]
+end
+
+describe "ObjectSpace::WeakMap#each_key" do
+ it_behaves_like :weakmap_each, :each
+end
diff --git a/spec/ruby/core/objectspace/weakmap/each_value_spec.rb b/spec/ruby/core/objectspace/weakmap/each_value_spec.rb
new file mode 100644
index 0000000000..65a1a7f6fe
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/each_value_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/members'
+require_relative 'shared/each'
+
+describe "ObjectSpace::WeakMap#each_value" do
+ it_behaves_like :weakmap_members, -> map { a = []; map.each_value{ |k| a << k }; a }, %w[x y]
+end
+
+describe "ObjectSpace::WeakMap#each_key" do
+ it_behaves_like :weakmap_each, :each_value
+end
diff --git a/spec/ruby/core/objectspace/weakmap/element_reference_spec.rb b/spec/ruby/core/objectspace/weakmap/element_reference_spec.rb
new file mode 100644
index 0000000000..cb3174cbfa
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/element_reference_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakMap#[]" do
+ it "is faithful to the map's content" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+ map[key1] = ref1
+ map[key1].should == ref1
+ map[key1] = ref1
+ map[key1].should == ref1
+ map[key2] = ref2
+ map[key1].should == ref1
+ map[key2].should == ref2
+ end
+
+ it "matches using identity semantics" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a a].map(&:upcase)
+ ref = "x"
+ map[key1] = ref
+ map[key2].should == nil
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/element_set_spec.rb b/spec/ruby/core/objectspace/weakmap/element_set_spec.rb
new file mode 100644
index 0000000000..8588877158
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/element_set_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakMap#[]=" do
+ def should_accept(map, key, value)
+ (map[key] = value).should == value
+ map.should.key?(key)
+ map[key].should == value
+ end
+
+ it "is correct" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+ should_accept(map, key1, ref1)
+ should_accept(map, key1, ref1)
+ should_accept(map, key2, ref2)
+ map[key1].should == ref1
+ end
+
+ it "accepts primitive or frozen keys or values" do
+ map = ObjectSpace::WeakMap.new
+ x = Object.new
+ should_accept(map, true, x)
+ should_accept(map, false, x)
+ should_accept(map, nil, x)
+ should_accept(map, 42, x)
+ should_accept(map, :foo, x)
+
+ should_accept(map, x, true)
+ should_accept(map, x, false)
+ should_accept(map, x, 42)
+ should_accept(map, x, :foo)
+
+ y = Object.new.freeze
+ should_accept(map, x, y)
+ should_accept(map, y, x)
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/include_spec.rb b/spec/ruby/core/objectspace/weakmap/include_spec.rb
new file mode 100644
index 0000000000..54ca6b3030
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/include_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/include'
+
+describe "ObjectSpace::WeakMap#include?" do
+ it_behaves_like :weakmap_include?, :include?
+end
diff --git a/spec/ruby/core/objectspace/weakmap/inspect_spec.rb b/spec/ruby/core/objectspace/weakmap/inspect_spec.rb
new file mode 100644
index 0000000000..f064f6e3ea
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/inspect_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+
+describe "ObjectSpace::WeakMap#inspect" do
+ it "displays object pointers in output" do
+ map = ObjectSpace::WeakMap.new
+ # important to test with BasicObject (without Kernel) here to test edge cases
+ key1, key2 = [BasicObject.new, Object.new]
+ ref1, ref2 = [BasicObject.new, Object.new]
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakMap:0x\h+>\z/
+ map[key1] = ref1
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakMap:0x\h+: \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>>\z/
+ map[key1] = ref1
+ map.inspect.should =~ /\A\#<ObjectSpace::WeakMap:0x\h+: \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>>\z/
+ map[key2] = ref2
+
+ regexp1 = /\A\#<ObjectSpace::WeakMap:0x\h+: \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>, \#<Object:0x\h+> => \#<Object:0x\h+>>\z/
+ regexp2 = /\A\#<ObjectSpace::WeakMap:0x\h+: \#<Object:0x\h+> => \#<Object:0x\h+>, \#<BasicObject:0x\h+> => \#<BasicObject:0x\h+>>\z/
+ str = map.inspect
+ if str =~ regexp1
+ str.should =~ regexp1
+ else
+ str.should =~ regexp2
+ end
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/key_spec.rb b/spec/ruby/core/objectspace/weakmap/key_spec.rb
new file mode 100644
index 0000000000..999685ff95
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/key_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/include'
+
+describe "ObjectSpace::WeakMap#key?" do
+ it_behaves_like :weakmap_include?, :key?
+end
diff --git a/spec/ruby/core/objectspace/weakmap/keys_spec.rb b/spec/ruby/core/objectspace/weakmap/keys_spec.rb
new file mode 100644
index 0000000000..7b1494bdd7
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/keys_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/members'
+
+describe "ObjectSpace::WeakMap#keys" do
+ it_behaves_like :weakmap_members, -> map { map.keys }, %w[A B]
+end
diff --git a/spec/ruby/core/objectspace/weakmap/length_spec.rb b/spec/ruby/core/objectspace/weakmap/length_spec.rb
new file mode 100644
index 0000000000..3a935648b1
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/size'
+
+describe "ObjectSpace::WeakMap#length" do
+ it_behaves_like :weakmap_size, :length
+end
diff --git a/spec/ruby/core/objectspace/weakmap/member_spec.rb b/spec/ruby/core/objectspace/weakmap/member_spec.rb
new file mode 100644
index 0000000000..cefb190ce7
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/member_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/include'
+
+describe "ObjectSpace::WeakMap#member?" do
+ it_behaves_like :weakmap_include?, :member?
+end
diff --git a/spec/ruby/core/objectspace/weakmap/shared/each.rb b/spec/ruby/core/objectspace/weakmap/shared/each.rb
new file mode 100644
index 0000000000..771c416dde
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/shared/each.rb
@@ -0,0 +1,10 @@
+describe :weakmap_each, shared: true do
+ it "must take a block, except when empty" do
+ map = ObjectSpace::WeakMap.new
+ key = "a".upcase
+ ref = "x"
+ map.send(@method).should == map
+ map[key] = ref
+ -> { map.send(@method) }.should.raise(LocalJumpError)
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/shared/include.rb b/spec/ruby/core/objectspace/weakmap/shared/include.rb
new file mode 100644
index 0000000000..1770eeac8b
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/shared/include.rb
@@ -0,0 +1,30 @@
+describe :weakmap_include?, shared: true do
+ it "recognizes keys in use" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+
+ map[key1] = ref1
+ map.send(@method, key1).should == true
+ map[key1] = ref1
+ map.send(@method, key1).should == true
+ map[key2] = ref2
+ map.send(@method, key2).should == true
+ end
+
+ it "matches using identity semantics" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a a].map(&:upcase)
+ ref = "x"
+ map[key1] = ref
+ map.send(@method, key2).should == false
+ end
+
+ it "reports true if the pair exists and the value is nil" do
+ map = ObjectSpace::WeakMap.new
+ key = Object.new
+ map[key] = nil
+ map.size.should == 1
+ map.send(@method, key).should == true
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/shared/members.rb b/spec/ruby/core/objectspace/weakmap/shared/members.rb
new file mode 100644
index 0000000000..57226c8d7a
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/shared/members.rb
@@ -0,0 +1,14 @@
+describe :weakmap_members, shared: true do
+ it "is correct" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+ @method.call(map).should == []
+ map[key1] = ref1
+ @method.call(map).should == @object[0..0]
+ map[key1] = ref1
+ @method.call(map).should == @object[0..0]
+ map[key2] = ref2
+ @method.call(map).sort.should == @object
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/shared/size.rb b/spec/ruby/core/objectspace/weakmap/shared/size.rb
new file mode 100644
index 0000000000..1064f99d1b
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/shared/size.rb
@@ -0,0 +1,14 @@
+describe :weakmap_size, shared: true do
+ it "is correct" do
+ map = ObjectSpace::WeakMap.new
+ key1, key2 = %w[a b].map(&:upcase)
+ ref1, ref2 = %w[x y]
+ map.send(@method).should == 0
+ map[key1] = ref1
+ map.send(@method).should == 1
+ map[key1] = ref1
+ map.send(@method).should == 1
+ map[key2] = ref2
+ map.send(@method).should == 2
+ end
+end
diff --git a/spec/ruby/core/objectspace/weakmap/size_spec.rb b/spec/ruby/core/objectspace/weakmap/size_spec.rb
new file mode 100644
index 0000000000..1446abaa24
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/size'
+
+describe "ObjectSpace::WeakMap#size" do
+ it_behaves_like :weakmap_size, :size
+end
diff --git a/spec/ruby/core/objectspace/weakmap/values_spec.rb b/spec/ruby/core/objectspace/weakmap/values_spec.rb
new file mode 100644
index 0000000000..6f6f90d0ba
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap/values_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../../spec_helper'
+require_relative 'shared/members'
+
+describe "ObjectSpace::WeakMap#values" do
+ it_behaves_like :weakmap_members, -> map { map.values }, %w[x y]
+end
diff --git a/spec/ruby/core/objectspace/weakmap_spec.rb b/spec/ruby/core/objectspace/weakmap_spec.rb
new file mode 100644
index 0000000000..2f3f93c291
--- /dev/null
+++ b/spec/ruby/core/objectspace/weakmap_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "ObjectSpace::WeakMap" do
+
+ # Note that we can't really spec the most important aspect of this class: that entries get removed when the values
+ # become unreachable. This is because Ruby does not offer a way to reliable invoke GC (GC.start is not enough, neither
+ # on MRI or on alternative implementations).
+
+ it "includes Enumerable" do
+ ObjectSpace::WeakMap.include?(Enumerable).should == true
+ end
+end
diff --git a/spec/ruby/core/proc/allocate_spec.rb b/spec/ruby/core/proc/allocate_spec.rb
new file mode 100644
index 0000000000..96c4eb9fa8
--- /dev/null
+++ b/spec/ruby/core/proc/allocate_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Proc.allocate" do
+ it "raises a TypeError" do
+ -> {
+ Proc.allocate
+ }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/proc/arity_spec.rb b/spec/ruby/core/proc/arity_spec.rb
new file mode 100644
index 0000000000..5c7728cb30
--- /dev/null
+++ b/spec/ruby/core/proc/arity_spec.rb
@@ -0,0 +1,656 @@
+require_relative '../../spec_helper'
+
+describe "Proc#arity" do
+ SpecEvaluate.desc = "for definition"
+
+ context "for instances created with -> () { }" do
+ context "returns zero" do
+ evaluate <<-ruby do
+ @a = -> () {}
+ ruby
+
+ @a.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = -> (&b) {}
+ ruby
+
+ @a.arity.should == 0
+ end
+ end
+
+ context "returns positive values" do
+ evaluate <<-ruby do
+ @a = -> (a) { }
+ @b = -> (a, b) { }
+ @c = -> (a, b, c) { }
+ @d = -> (a, b, c, d) { }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 2
+ @c.arity.should == 3
+ @d.arity.should == 4
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a:) { }
+ @b = -> (a:, b:) { }
+ @c = -> (a: 1, b:, c:, d: 2) { }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 1
+ @c.arity.should == 1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, b:) { }
+ @b = -> (a, b:, &l) { }
+ ruby
+
+ @a.arity.should == 2
+ @b.arity.should == 2
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, b, c:, d: 1) { }
+ @b = -> (a, b, c:, d: 1, **k, &l) { }
+ ruby
+
+ @a.arity.should == 3
+ @b.arity.should == 3
+ end
+
+ evaluate <<-ruby do
+ @a = -> ((a, (*b, c))) { }
+ @b = -> (a, (*b, c), d, (*e), (*)) { }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 5
+ end
+ end
+
+ context "returns negative values" do
+ evaluate <<-ruby do
+ @a = -> (a=1) { }
+ @b = -> (a=1, b=2) { }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, b=1) { }
+ @b = -> (a, b, c=1, d=2) { }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -3
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a=1, *b) { }
+ @b = -> (a=1, b=2, *c) { }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (*) { }
+ @b = -> (*a) { }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, *) { }
+ @b = -> (a, *b) { }
+ @c = -> (a, b, *c) { }
+ @d = -> (a, b, c, *d) { }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -2
+ @c.arity.should == -3
+ @d.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = -> (*a, b) { }
+ @b = -> (*a, b, c) { }
+ @c = -> (*a, b, c, d) { }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -3
+ @c.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, *b, c) { }
+ @b = -> (a, b, *c, d, e) { }
+ ruby
+
+ @a.arity.should == -3
+ @b.arity.should == -5
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, b=1, c=2, *d, e, f) { }
+ @b = -> (a, b, c=1, *d, e, f, g) { }
+ ruby
+
+ @a.arity.should == -4
+ @b.arity.should == -6
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a: 1) { }
+ @b = -> (a: 1, b: 2) { }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a=1, b: 2) { }
+ @b = -> (*a, b: 1) { }
+ @c = -> (a=1, b: 2) { }
+ @d = -> (a=1, *b, c: 2, &l) { }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ @c.arity.should == -1
+ @d.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (**k, &l) { }
+ @b= -> (*a, **k) { }
+ @c = ->(a: 1, b: 2, **k) { }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ @c.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a=1, *b, c:, d: 2, **k, &l) { }
+ ruby
+
+ @a.arity.should == -2
+ end
+
+ evaluate <<-ruby do
+ @a = -> (a, b=1, *c, d, e:, f: 2, **k, &l) { }
+ @b = -> (a, b=1, *c, d:, e:, f: 2, **k, &l) { }
+ @c = -> (a=0, b=1, *c, d, e:, f: 2, **k, &l) { }
+ @d = -> (a=0, b=1, *c, d:, e:, f: 2, **k, &l) { }
+ ruby
+
+ @a.arity.should == -4
+ @b.arity.should == -3
+ @c.arity.should == -3
+ @d.arity.should == -2
+ end
+ end
+ end
+
+ context "for instances created with lambda { || }" do
+ context "returns zero" do
+ evaluate <<-ruby do
+ @a = lambda { }
+ @b = lambda { || }
+ ruby
+
+ @a.arity.should == 0
+ @b.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |&b| }
+ ruby
+
+ @a.arity.should == 0
+ end
+ end
+
+ context "returns positive values" do
+ evaluate <<-ruby do
+ @a = lambda { |a| }
+ @b = lambda { |a, b| }
+ @c = lambda { |a, b, c| }
+ @d = lambda { |a, b, c, d| }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 2
+ @c.arity.should == 3
+ @d.arity.should == 4
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a:| }
+ @b = lambda { |a:, b:| }
+ @c = lambda { |a: 1, b:, c:, d: 2| }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 1
+ @c.arity.should == 1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, b:| }
+ @b = lambda { |a, b:, &l| }
+ ruby
+
+ @a.arity.should == 2
+ @b.arity.should == 2
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, b, c:, d: 1| }
+ @b = lambda { |a, b, c:, d: 1, **k, &l| }
+ ruby
+
+ @a.arity.should == 3
+ @b.arity.should == 3
+ end
+
+ # implicit rest
+ evaluate <<-ruby do
+ @a = lambda { |a, | }
+ ruby
+
+ @a.arity.should == 1
+ end
+ end
+
+ context "returns negative values" do
+ evaluate <<-ruby do
+ @a = lambda { |a=1| }
+ @b = lambda { |a=1, b=2| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, b=1| }
+ @b = lambda { |a, b, c=1, d=2| }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -3
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a=1, *b| }
+ @b = lambda { |a=1, b=2, *c| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |*| }
+ @b = lambda { |*a| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, *| }
+ @b = lambda { |a, *b| }
+ @c = lambda { |a, b, *c| }
+ @d = lambda { |a, b, c, *d| }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -2
+ @c.arity.should == -3
+ @d.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |*a, b| }
+ @b = lambda { |*a, b, c| }
+ @c = lambda { |*a, b, c, d| }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -3
+ @c.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, *b, c| }
+ @b = lambda { |a, b, *c, d, e| }
+ ruby
+
+ @a.arity.should == -3
+ @b.arity.should == -5
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, b=1, c=2, *d, e, f| }
+ @b = lambda { |a, b, c=1, *d, e, f, g| }
+ ruby
+
+ @a.arity.should == -4
+ @b.arity.should == -6
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a: 1| }
+ @b = lambda { |a: 1, b: 2| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a=1, b: 2| }
+ @b = lambda { |*a, b: 1| }
+ @c = lambda { |a=1, b: 2| }
+ @d = lambda { |a=1, *b, c: 2, &l| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ @c.arity.should == -1
+ @d.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |**k, &l| }
+ @b = lambda { |*a, **k| }
+ @c = lambda { |a: 1, b: 2, **k| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ @c.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a=1, *b, c:, d: 2, **k, &l| }
+ ruby
+
+ @a.arity.should == -2
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |(a, (*b, c)), d=1| }
+ @b = lambda { |a, (*b, c), d, (*e), (*), **k| }
+ @c = lambda { |a, (b, c), *, d:, e: 2, **| }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -6
+ @c.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a, b=1, *c, d, e:, f: 2, **k, &l| }
+ @b = lambda { |a, b=1, *c, d:, e:, f: 2, **k, &l| }
+ @c = lambda { |a=0, b=1, *c, d, e:, f: 2, **k, &l| }
+ @d = lambda { |a=0, b=1, *c, d:, e:, f: 2, **k, &l| }
+ ruby
+
+ @a.arity.should == -4
+ @b.arity.should == -3
+ @c.arity.should == -3
+ @d.arity.should == -2
+ end
+ end
+ end
+
+ context "for instances created with proc { || }" do
+ context "returns zero" do
+ evaluate <<-ruby do
+ @a = proc { }
+ @b = proc { || }
+ ruby
+
+ @a.arity.should == 0
+ @b.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |&b| }
+ ruby
+
+ @a.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a=1| }
+ @b = proc { |a=1, b=2| }
+ ruby
+
+ @a.arity.should == 0
+ @b.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a: 1| }
+ @b = proc { |a: 1, b: 2| }
+ ruby
+
+ @a.arity.should == 0
+ @b.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |**k, &l| }
+ @b = proc { |a: 1, b: 2, **k| }
+ ruby
+
+ @a.arity.should == 0
+ @b.arity.should == 0
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a=1, b: 2| }
+ @b = proc { |a=1, b: 2| }
+ ruby
+
+ @a.arity.should == 0
+ @b.arity.should == 0
+ end
+ end
+
+ context "returns positive values" do
+ evaluate <<-ruby do
+ @a = proc { |a| }
+ @b = proc { |a, b| }
+ @c = proc { |a, b, c| }
+ @d = proc { |a, b, c, d| }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 2
+ @c.arity.should == 3
+ @d.arity.should == 4
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, b=1| }
+ @b = proc { |a, b, c=1, d=2| }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 2
+ end
+
+ evaluate <<-ruby do
+ @a = lambda { |a:| }
+ @b = lambda { |a:, b:| }
+ @c = lambda { |a: 1, b:, c:, d: 2| }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 1
+ @c.arity.should == 1
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, b:| }
+ @b = proc { |a, b:, &l| }
+ ruby
+
+ @a.arity.should == 2
+ @b.arity.should == 2
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, b, c:, d: 1| }
+ @b = proc { |a, b, c:, d: 1, **k, &l| }
+ ruby
+
+ @a.arity.should == 3
+ @b.arity.should == 3
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |(a, (*b, c)), d=1| }
+ @b = proc { |a, (*b, c), d, (*e), (*), **k| }
+ ruby
+
+ @a.arity.should == 1
+ @b.arity.should == 5
+ end
+
+ # implicit rest
+ evaluate <<-ruby do
+ @a = proc { |a, | }
+ ruby
+
+ @a.arity.should == 1
+ end
+ end
+
+ context "returns negative values" do
+ evaluate <<-ruby do
+ @a = proc { |a=1, *b| }
+ @b = proc { |a=1, b=2, *c| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |*| }
+ @b = proc { |*a| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, *| }
+ @b = proc { |a, *b| }
+ @c = proc { |a, b, *c| }
+ @d = proc { |a, b, c, *d| }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -2
+ @c.arity.should == -3
+ @d.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |*a, b| }
+ @b = proc { |*a, b, c| }
+ @c = proc { |*a, b, c, d| }
+ ruby
+
+ @a.arity.should == -2
+ @b.arity.should == -3
+ @c.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, *b, c| }
+ @b = proc { |a, b, *c, d, e| }
+ ruby
+
+ @a.arity.should == -3
+ @b.arity.should == -5
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, b=1, c=2, *d, e, f| }
+ @b = proc { |a, b, c=1, *d, e, f, g| }
+ ruby
+
+ @a.arity.should == -4
+ @b.arity.should == -6
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |*a, b: 1| }
+ @b = proc { |a=1, *b, c: 2, &l| }
+ ruby
+
+ @a.arity.should == -1
+ @b.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |*a, **k| }
+ ruby
+
+ @a.arity.should == -1
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a=1, *b, c:, d: 2, **k, &l| }
+ ruby
+
+ @a.arity.should == -2
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, (b, c), *, d:, e: 2, **| }
+ ruby
+
+ @a.arity.should == -4
+ end
+
+ evaluate <<-ruby do
+ @a = proc { |a, b=1, *c, d, e:, f: 2, **k, &l| }
+ @b = proc { |a, b=1, *c, d:, e:, f: 2, **k, &l| }
+ @c = proc { |a=0, b=1, *c, d, e:, f: 2, **k, &l| }
+ @d = proc { |a=0, b=1, *c, d:, e:, f: 2, **k, &l| }
+ ruby
+
+ @a.arity.should == -4
+ @b.arity.should == -3
+ @c.arity.should == -3
+ @d.arity.should == -2
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/binding_spec.rb b/spec/ruby/core/proc/binding_spec.rb
new file mode 100644
index 0000000000..d643cbf89c
--- /dev/null
+++ b/spec/ruby/core/proc/binding_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "Proc#binding" do
+ it "returns a Binding instance" do
+ [Proc.new{}, -> {}, proc {}].each { |p|
+ p.binding.should.is_a?(Binding)
+ }
+ end
+
+ it "returns the binding associated with self" do
+ obj = mock('binding')
+ def obj.test_binding(some, params)
+ -> {}
+ end
+
+ lambdas_binding = obj.test_binding(1, 2).binding
+
+ eval("some", lambdas_binding).should == 1
+ eval("params", lambdas_binding).should == 2
+ end
+end
diff --git a/spec/ruby/core/proc/block_pass_spec.rb b/spec/ruby/core/proc/block_pass_spec.rb
new file mode 100644
index 0000000000..82c08db8a7
--- /dev/null
+++ b/spec/ruby/core/proc/block_pass_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "Proc as a block pass argument" do
+ def revivify(&b)
+ b
+ end
+
+ it "remains the same object if re-vivified by the target method" do
+ p = Proc.new {}
+ p2 = revivify(&p)
+ p.should.equal? p2
+ p.should == p2
+ end
+
+ it "remains the same object if reconstructed with Proc.new" do
+ p = Proc.new {}
+ p2 = Proc.new(&p)
+ p.should.equal? p2
+ p.should == p2
+ end
+end
diff --git a/spec/ruby/core/proc/call_spec.rb b/spec/ruby/core/proc/call_spec.rb
new file mode 100644
index 0000000000..6ec2fc8682
--- /dev/null
+++ b/spec/ruby/core/proc/call_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'shared/call'
+require_relative 'shared/call_arguments'
+
+describe "Proc#call" do
+ it_behaves_like :proc_call, :call
+ it_behaves_like :proc_call_block_args, :call
+end
+
+describe "Proc#call on a Proc created with Proc.new" do
+ it_behaves_like :proc_call_on_proc_new, :call
+end
+
+describe "Proc#call on a Proc created with Kernel#lambda or Kernel#proc" do
+ it_behaves_like :proc_call_on_proc_or_lambda, :call
+end
diff --git a/spec/ruby/core/proc/case_compare_spec.rb b/spec/ruby/core/proc/case_compare_spec.rb
new file mode 100644
index 0000000000..f11513cdb9
--- /dev/null
+++ b/spec/ruby/core/proc/case_compare_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'shared/call'
+require_relative 'shared/call_arguments'
+
+describe "Proc#===" do
+ it_behaves_like :proc_call, :===
+ it_behaves_like :proc_call_block_args, :===
+end
+
+describe "Proc#=== on a Proc created with Proc.new" do
+ it_behaves_like :proc_call_on_proc_new, :===
+end
+
+describe "Proc#=== on a Proc created with Kernel#lambda or Kernel#proc" do
+ it_behaves_like :proc_call_on_proc_or_lambda, :===
+end
diff --git a/spec/ruby/core/proc/clone_spec.rb b/spec/ruby/core/proc/clone_spec.rb
new file mode 100644
index 0000000000..aee4873e09
--- /dev/null
+++ b/spec/ruby/core/proc/clone_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/dup'
+
+describe "Proc#clone" do
+ it_behaves_like :proc_dup, :clone
+
+ ruby_bug "cloning a frozen proc is broken on Ruby 3.3", ""..."3.4" do
+ it "preserves frozen status" do
+ proc = Proc.new { }
+ proc.freeze
+ proc.frozen?.should == true
+ proc.clone.frozen?.should == true
+ end
+ end
+
+ it "calls #initialize_clone on subclass" do
+ obj = ProcSpecs::MyProc2.new(:a, 2) { }
+ dup = obj.clone
+
+ dup.should_not.equal?(obj)
+ dup.class.should == ProcSpecs::MyProc2
+
+ dup.first.should == :a
+ dup.second.should == 2
+ dup.initializer.should == :clone
+ end
+end
diff --git a/spec/ruby/core/proc/compose_spec.rb b/spec/ruby/core/proc/compose_spec.rb
new file mode 100644
index 0000000000..9e9b57e06f
--- /dev/null
+++ b/spec/ruby/core/proc/compose_spec.rb
@@ -0,0 +1,142 @@
+require_relative '../../spec_helper'
+require_relative 'shared/compose'
+
+describe "Proc#<<" do
+ it "returns a Proc that is the composition of self and the passed Proc" do
+ upcase = proc { |s| s.upcase }
+ succ = proc { |s| s.succ }
+
+ (succ << upcase).call('Ruby').should == "RUBZ"
+ end
+
+ it "calls passed Proc with arguments and then calls self with result" do
+ f = proc { |x| x * x }
+ g = proc { |x| x + x }
+
+ (f << g).call(2).should == 16
+ (g << f).call(2).should == 8
+ end
+
+ it "accepts any callable object" do
+ inc = proc { |n| n + 1 }
+
+ double = Object.new
+ def double.call(n); n * 2; end
+
+ (inc << double).call(3).should == 7
+ end
+
+ it_behaves_like :proc_compose, :<<, -> { proc { |s| s.upcase } }
+
+ describe "composition" do
+ it "is a Proc" do
+ f = proc { |x| x * x }
+ g = proc { |x| x + x }
+
+ (f << g).is_a?(Proc).should == true
+ (f << g).should_not.lambda?
+ end
+
+ it "is a lambda when parameter is lambda" do
+ f = -> x { x * x }
+ g = proc { |x| x + x }
+ lambda_proc = -> x { x }
+
+ # lambda << proc
+ (f << g).is_a?(Proc).should == true
+ (f << g).should_not.lambda?
+
+ # lambda << lambda
+ (f << lambda_proc).is_a?(Proc).should == true
+ (f << lambda_proc).should.lambda?
+
+ # proc << lambda
+ (g << f).is_a?(Proc).should == true
+ (g << f).should.lambda?
+ end
+
+ it "may accept multiple arguments" do
+ inc = proc { |n| n + 1 }
+ mul = proc { |n, m| n * m }
+
+ (inc << mul).call(2, 3).should == 7
+ end
+
+ it "passes blocks to the second proc" do
+ ScratchPad.record []
+ one = proc { |&arg| arg.call :one if arg }
+ two = proc { |&arg| arg.call :two if arg }
+ (one << two).call { |x| ScratchPad << x }
+ ScratchPad.recorded.should == [:two]
+ end
+ end
+end
+
+describe "Proc#>>" do
+ it "returns a Proc that is the composition of self and the passed Proc" do
+ upcase = proc { |s| s.upcase }
+ succ = proc { |s| s.succ }
+
+ (succ >> upcase).call('Ruby').should == "RUBZ"
+ end
+
+ it "calls passed Proc with arguments and then calls self with result" do
+ f = proc { |x| x * x }
+ g = proc { |x| x + x }
+
+ (f >> g).call(2).should == 8
+ (g >> f).call(2).should == 16
+ end
+
+ it "accepts any callable object" do
+ inc = proc { |n| n + 1 }
+
+ double = Object.new
+ def double.call(n); n * 2; end
+
+ (inc >> double).call(3).should == 8
+ end
+
+ it_behaves_like :proc_compose, :>>, -> { proc { |s| s.upcase } }
+
+ describe "composition" do
+ it "is a Proc" do
+ f = proc { |x| x * x }
+ g = proc { |x| x + x }
+
+ (f >> g).is_a?(Proc).should == true
+ (f >> g).should_not.lambda?
+ end
+
+ it "is a Proc when other is lambda" do
+ f = proc { |x| x * x }
+ g = -> x { x + x }
+
+ (f >> g).is_a?(Proc).should == true
+ (f >> g).should_not.lambda?
+ end
+
+ it "is a lambda when self is lambda" do
+ f = -> x { x * x }
+ g = proc { |x| x + x }
+
+ (f >> g).is_a?(Proc).should == true
+ (f >> g).should.lambda?
+ end
+
+ it "may accept multiple arguments" do
+ inc = proc { |n| n + 1 }
+ mul = proc { |n, m| n * m }
+
+ (mul >> inc).call(2, 3).should == 7
+ end
+
+ it "passes blocks to the first proc" do
+ ScratchPad.record []
+ one = proc { |&arg| arg.call :one if arg }
+ two = proc { |&arg| arg.call :two if arg }
+ (one >> two).call { |x| ScratchPad << x }
+ ScratchPad.recorded.should == [:one]
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/curry_spec.rb b/spec/ruby/core/proc/curry_spec.rb
new file mode 100644
index 0000000000..de13983ca6
--- /dev/null
+++ b/spec/ruby/core/proc/curry_spec.rb
@@ -0,0 +1,179 @@
+require_relative '../../spec_helper'
+
+describe "Proc#curry" do
+ before :each do
+ @proc_add = Proc.new {|x,y,z| (x||0) + (y||0) + (z||0) }
+ @lambda_add = -> x, y, z { (x||0) + (y||0) + (z||0) }
+ end
+
+ it "returns a Proc when called on a proc" do
+ p = proc { true }
+ p.curry.should.instance_of?(Proc)
+ end
+
+ it "returns a Proc when called on a lambda" do
+ p = -> { true }
+ p.curry.should.instance_of?(Proc)
+ end
+
+ it "calls the curried proc with the arguments if sufficient arguments have been given" do
+ @proc_add.curry[1][2][3].should == 6
+ @lambda_add.curry[1][2][3].should == 6
+ end
+
+ it "returns a Proc that consumes the remainder of the arguments unless sufficient arguments have been given" do
+ proc2 = @proc_add.curry[1][2]
+ proc2.should.instance_of?(Proc)
+ proc2.call(3).should == 6
+
+ lambda2 = @lambda_add.curry[1][2]
+ lambda2.should.instance_of?(Proc)
+ lambda2.call(3).should == 6
+
+ @proc_add.curry.call(1,2,3).should == 6
+ @lambda_add.curry.call(1,2,3).should == 6
+ end
+
+ it "can be called multiple times on the same Proc" do
+ @proc_add.curry
+ -> { @proc_add.curry }.should_not.raise
+
+ @lambda_add.curry
+ -> { @lambda_add.curry }.should_not.raise
+ end
+
+ it "can be passed superfluous arguments if created from a proc" do
+ @proc_add.curry[1,2,3,4].should == 6
+
+ @proc_add.curry[1,2].curry[3,4,5,6].should == 6
+ end
+
+ it "raises an ArgumentError if passed superfluous arguments when created from a lambda" do
+ -> { @lambda_add.curry[1,2,3,4] }.should.raise(ArgumentError)
+ -> { @lambda_add.curry[1,2].curry[3,4,5,6] }.should.raise(ArgumentError)
+ end
+
+ it "returns Procs with arities of -1" do
+ @proc_add.curry.arity.should == -1
+ @lambda_add.curry.arity.should == -1
+ l = -> *a { }
+ l.curry.arity.should == -1
+ end
+
+ it "produces Procs that raise ArgumentError for #binding" do
+ -> do
+ @proc_add.curry.binding
+ end.should.raise(ArgumentError)
+ end
+
+ it "produces Procs that return [[:rest]] for #parameters" do
+ @proc_add.curry.parameters.should == [[:rest]]
+ end
+
+ it "produces Procs that return nil for #source_location" do
+ @proc_add.curry.source_location.should == nil
+ end
+
+ it "produces Procs that can be passed as the block for instance_exec" do
+ curried = @proc_add.curry.call(1, 2)
+
+ instance_exec(3, &curried).should == 6
+ end
+
+ it "combines arguments and calculates incoming arity accurately for successively currying" do
+ l = -> a, b, c { a+b+c }
+ l1 = l.curry.call(1)
+ # the l1 currying seems unnecessary, but it triggered the original issue
+ l2 = l1.curry.call(2)
+
+ l2.curry.call(3).should == 6
+ l1.curry.call(2,3).should == 6
+ end
+end
+
+describe "Proc#curry with arity argument" do
+ before :each do
+ @proc_add = proc { |x,y,z| (x||0) + (y||0) + (z||0) }
+ @lambda_add = -> x, y, z { (x||0) + (y||0) + (z||0) }
+ end
+
+ it "accepts an optional Integer argument for the arity" do
+ -> { @proc_add.curry(3) }.should_not.raise
+ -> { @lambda_add.curry(3) }.should_not.raise
+ end
+
+ it "returns a Proc when called on a proc" do
+ @proc_add.curry(3).should.instance_of?(Proc)
+ end
+
+ it "returns a Proc when called on a lambda" do
+ @lambda_add.curry(3).should.instance_of?(Proc)
+ end
+
+ # [ruby-core:24127]
+ it "retains the lambda-ness of the Proc on which its called" do
+ @lambda_add.curry(3).lambda?.should == true
+ @proc_add.curry(3).lambda?.should == false
+ end
+
+ it "raises an ArgumentError if called on a lambda that requires more than _arity_ arguments" do
+ -> { @lambda_add.curry(2) }.should.raise(ArgumentError)
+ -> { -> x, y, z, *more{}.curry(2) }.should.raise(ArgumentError)
+ end
+
+ it 'returns a Proc if called on a lambda that requires fewer than _arity_ arguments but may take more' do
+ -> a, b, c, d=nil, e=nil {}.curry(4).should.instance_of?(Proc)
+ -> a, b, c, d=nil, *e {}.curry(4).should.instance_of?(Proc)
+ -> a, b, c, *d {}.curry(4).should.instance_of?(Proc)
+ end
+
+ it "raises an ArgumentError if called on a lambda that requires fewer than _arity_ arguments" do
+ -> { @lambda_add.curry(4) }.should.raise(ArgumentError)
+ -> { -> { true }.curry(1) }.should.raise(ArgumentError)
+ -> { -> a, b=nil {}.curry(5) }.should.raise(ArgumentError)
+ -> { -> a, &b {}.curry(2) }.should.raise(ArgumentError)
+ -> { -> a, b=nil, &c {}.curry(3) }.should.raise(ArgumentError)
+ end
+
+ it "calls the curried proc with the arguments if _arity_ arguments have been given" do
+ @proc_add.curry(3)[1][2][3].should == 6
+ @lambda_add.curry(3)[1][2][3].should == 6
+ end
+
+ it "returns a Proc that consumes the remainder of the arguments when fewer than _arity_ arguments are given" do
+ proc2 = @proc_add.curry(3)[1][2]
+ proc2.should.instance_of?(Proc)
+ proc2.call(3).should == 6
+
+ lambda2 = @lambda_add.curry(3)[1][2]
+ lambda2.should.instance_of?(Proc)
+ lambda2.call(3).should == 6
+ end
+
+ it "can be specified multiple times on the same Proc" do
+ @proc_add.curry(2)
+ -> { @proc_add.curry(1) }.should_not.raise
+
+ @lambda_add.curry(3)
+ -> { @lambda_add.curry(3) }.should_not.raise
+ end
+
+ it "can be passed more than _arity_ arguments if created from a proc" do
+ @proc_add.curry(3)[1,2,3,4].should == 6
+
+ @proc_add.curry(3)[1,2].curry(3)[3,4,5,6].should == 6
+ end
+
+ it "raises an ArgumentError if passed more than _arity_ arguments when created from a lambda" do
+ -> { @lambda_add.curry(3)[1,2,3,4] }.should.raise(ArgumentError)
+ -> { @lambda_add.curry(3)[1,2].curry(3)[3,4,5,6] }.should.raise(ArgumentError)
+ end
+
+ it "returns Procs with arities of -1 regardless of the value of _arity_" do
+ @proc_add.curry(1).arity.should == -1
+ @proc_add.curry(2).arity.should == -1
+ @lambda_add.curry(3).arity.should == -1
+ l = -> *a { }
+ l.curry(3).arity.should == -1
+ end
+end
diff --git a/spec/ruby/core/proc/dup_spec.rb b/spec/ruby/core/proc/dup_spec.rb
new file mode 100644
index 0000000000..8604389422
--- /dev/null
+++ b/spec/ruby/core/proc/dup_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+require_relative 'shared/dup'
+
+describe "Proc#dup" do
+ it_behaves_like :proc_dup, :dup
+
+ it "resets frozen status" do
+ proc = Proc.new { }
+ proc.freeze
+ proc.frozen?.should == true
+ proc.dup.frozen?.should == false
+ end
+
+ it "calls #initialize_dup on subclass" do
+ obj = ProcSpecs::MyProc2.new(:a, 2) { }
+ dup = obj.dup
+
+ dup.should_not.equal?(obj)
+ dup.class.should == ProcSpecs::MyProc2
+
+ dup.first.should == :a
+ dup.second.should == 2
+ dup.initializer.should == :dup
+ end
+end
diff --git a/spec/ruby/core/proc/element_reference_spec.rb b/spec/ruby/core/proc/element_reference_spec.rb
new file mode 100644
index 0000000000..ea3a915a11
--- /dev/null
+++ b/spec/ruby/core/proc/element_reference_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'shared/call'
+require_relative 'shared/call_arguments'
+require_relative 'fixtures/proc_aref'
+require_relative 'fixtures/proc_aref_frozen'
+
+describe "Proc#[]" do
+ it_behaves_like :proc_call, :[]
+ it_behaves_like :proc_call_block_args, :[]
+end
+
+describe "Proc#call on a Proc created with Proc.new" do
+ it_behaves_like :proc_call_on_proc_new, :call
+end
+
+describe "Proc#call on a Proc created with Kernel#lambda or Kernel#proc" do
+ it_behaves_like :proc_call_on_proc_or_lambda, :call
+end
+
+describe "Proc#[] with frozen_string_literal: true/false" do
+ it "doesn't duplicate frozen strings" do
+ ProcArefSpecs.aref.frozen?.should == false
+ ProcArefSpecs.aref_freeze.frozen?.should == true
+ ProcArefFrozenSpecs.aref.frozen?.should == true
+ ProcArefFrozenSpecs.aref_freeze.frozen?.should == true
+ end
+end
diff --git a/spec/ruby/core/proc/eql_spec.rb b/spec/ruby/core/proc/eql_spec.rb
new file mode 100644
index 0000000000..ad8f6749fc
--- /dev/null
+++ b/spec/ruby/core/proc/eql_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal'
+
+describe "Proc#eql?" do
+ it_behaves_like :proc_equal, :eql?
+end
diff --git a/spec/ruby/core/proc/equal_value_spec.rb b/spec/ruby/core/proc/equal_value_spec.rb
new file mode 100644
index 0000000000..ec7f274732
--- /dev/null
+++ b/spec/ruby/core/proc/equal_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal'
+
+describe "Proc#==" do
+ it_behaves_like :proc_equal, :==
+end
diff --git a/spec/ruby/core/proc/fixtures/common.rb b/spec/ruby/core/proc/fixtures/common.rb
new file mode 100644
index 0000000000..dfe67d7ba8
--- /dev/null
+++ b/spec/ruby/core/proc/fixtures/common.rb
@@ -0,0 +1,72 @@
+module ProcSpecs
+ class ToAryAsNil
+ def to_ary
+ nil
+ end
+ end
+ def self.new_proc_in_method
+ Proc.new
+ end
+
+ def self.new_proc_from_amp(&block)
+ block
+ end
+
+ def self.proc_for_1
+ proc { 1 }
+ end
+
+ class ProcSubclass < Proc
+ end
+
+ def self.new_proc_subclass_in_method
+ ProcSubclass.new
+ end
+
+ class MyProc < Proc
+ end
+
+ class MyProc2 < Proc
+ def initialize(a, b)
+ @first = a
+ @second = b
+ end
+
+ attr_reader :first, :second, :initializer
+
+ def initialize_copy(other)
+ super
+ @initializer = :copy
+ @first = other.first
+ @second = other.second
+ end
+
+ def initialize_dup(other)
+ super
+ @initializer = :dup
+ @first = other.first
+ @second = other.second
+ end
+
+ def initialize_clone(other, **options)
+ super
+ @initializer = :clone
+ @first = other.first
+ @second = other.second
+ end
+ end
+
+ class Arity
+ def arity_check(&block)
+ pn = Proc.new(&block).arity
+ pr = proc(&block).arity
+ lm = lambda(&block).arity
+
+ if pn == pr and pr == lm
+ return pn
+ else
+ return :arity_check_failed
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/fixtures/proc_aref.rb b/spec/ruby/core/proc/fixtures/proc_aref.rb
new file mode 100644
index 0000000000..8ee355b14c
--- /dev/null
+++ b/spec/ruby/core/proc/fixtures/proc_aref.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: false
+module ProcArefSpecs
+ def self.aref
+ proc {|a| a }["sometext"]
+ end
+
+ def self.aref_freeze
+ proc {|a| a }["sometext".freeze]
+ end
+end
diff --git a/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb b/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb
new file mode 100644
index 0000000000..50a330ba4f
--- /dev/null
+++ b/spec/ruby/core/proc/fixtures/proc_aref_frozen.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+module ProcArefFrozenSpecs
+ def self.aref
+ proc {|a| a }["sometext"]
+ end
+
+ def self.aref_freeze
+ proc {|a| a }["sometext".freeze]
+ end
+end
diff --git a/spec/ruby/core/proc/fixtures/source_location.rb b/spec/ruby/core/proc/fixtures/source_location.rb
new file mode 100644
index 0000000000..5572094630
--- /dev/null
+++ b/spec/ruby/core/proc/fixtures/source_location.rb
@@ -0,0 +1,55 @@
+module ProcSpecs
+ class SourceLocation
+ def self.my_proc
+ proc { true }
+ end
+
+ def self.my_lambda
+ -> { true }
+ end
+
+ def self.my_proc_new
+ Proc.new { true }
+ end
+
+ def self.my_method
+ method(__method__).to_proc
+ end
+
+ def self.my_multiline_proc
+ proc do
+ 'a'.upcase
+ 1 + 22
+ end
+ end
+
+ def self.my_multiline_lambda
+ -> do
+ 'a'.upcase
+ 1 + 22
+ end
+ end
+
+ def self.my_multiline_proc_new
+ Proc.new do
+ 'a'.upcase
+ 1 + 22
+ end
+ end
+
+ def self.my_detached_proc
+ body = proc { true }
+ proc(&body)
+ end
+
+ def self.my_detached_lambda
+ body = -> { true }
+ suppress_warning {lambda(&body)}
+ end
+
+ def self.my_detached_proc_new
+ body = Proc.new { true }
+ Proc.new(&body)
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/hash_spec.rb b/spec/ruby/core/proc/hash_spec.rb
new file mode 100644
index 0000000000..adcb1ccb78
--- /dev/null
+++ b/spec/ruby/core/proc/hash_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Proc#hash" do
+ it "is provided" do
+ proc {}.respond_to?(:hash).should == true
+ -> {}.respond_to?(:hash).should == true
+ end
+
+ it "returns an Integer" do
+ proc { 1 + 489 }.hash.should.is_a?(Integer)
+ end
+
+ it "is stable" do
+ body = proc { :foo }
+ proc(&body).hash.should == proc(&body).hash
+ end
+end
diff --git a/spec/ruby/core/proc/inspect_spec.rb b/spec/ruby/core/proc/inspect_spec.rb
new file mode 100644
index 0000000000..f53d34116f
--- /dev/null
+++ b/spec/ruby/core/proc/inspect_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+
+describe "Proc#inspect" do
+ it_behaves_like :proc_to_s, :inspect
+end
diff --git a/spec/ruby/core/proc/lambda_spec.rb b/spec/ruby/core/proc/lambda_spec.rb
new file mode 100644
index 0000000000..1ff6147319
--- /dev/null
+++ b/spec/ruby/core/proc/lambda_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Proc#lambda?" do
+ it "returns true if the Proc was created from a block with the lambda keyword" do
+ -> {}.lambda?.should == true
+ end
+
+ it "returns false if the Proc was created from a block with the proc keyword" do
+ proc {}.lambda?.should == false
+ end
+
+ it "returns false if the Proc was created from a block with Proc.new" do
+ Proc.new {}.lambda?.should == false
+ end
+
+ it "is preserved when passing a Proc with & to the proc keyword" do
+ proc(&->{}).lambda?.should == true
+ proc(&proc{}).lambda?.should == false
+ end
+
+ it "is preserved when passing a Proc with & to Proc.new" do
+ Proc.new(&->{}).lambda?.should == true
+ Proc.new(&proc{}).lambda?.should == false
+ end
+
+ it "returns false if the Proc was created from a block with &" do
+ ProcSpecs.new_proc_from_amp{}.lambda?.should == false
+ end
+
+ it "is preserved when the Proc was passed using &" do
+ ProcSpecs.new_proc_from_amp(&->{}).lambda?.should == true
+ ProcSpecs.new_proc_from_amp(&proc{}).lambda?.should == false
+ ProcSpecs.new_proc_from_amp(&Proc.new{}).lambda?.should == false
+ end
+
+ it "returns true for a Method converted to a Proc" do
+ m = :foo.method(:to_s)
+ m.to_proc.lambda?.should == true
+ ProcSpecs.new_proc_from_amp(&m).lambda?.should == true
+ end
+
+ # [ruby-core:24127]
+ it "is preserved when a Proc is curried" do
+ ->{}.curry.lambda?.should == true
+ proc{}.curry.lambda?.should == false
+ Proc.new{}.curry.lambda?.should == false
+ end
+
+ it "is preserved when a curried Proc is called without enough arguments" do
+ -> x, y{}.curry.call(42).lambda?.should == true
+ proc{|x,y|}.curry.call(42).lambda?.should == false
+ Proc.new{|x,y|}.curry.call(42).lambda?.should == false
+ end
+end
diff --git a/spec/ruby/core/proc/new_spec.rb b/spec/ruby/core/proc/new_spec.rb
new file mode 100644
index 0000000000..f0b817f0cb
--- /dev/null
+++ b/spec/ruby/core/proc/new_spec.rb
@@ -0,0 +1,178 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Proc.new with an associated block" do
+ it "returns a proc that represents the block" do
+ Proc.new { }.call.should == nil
+ Proc.new { "hello" }.call.should == "hello"
+ end
+
+ describe "called on a subclass of Proc" do
+ before :each do
+ @subclass = Class.new(Proc) do
+ attr_reader :ok
+ def initialize
+ @ok = true
+ super
+ end
+ end
+ end
+
+ it "returns an instance of the subclass" do
+ proc = @subclass.new {"hello"}
+
+ proc.class.should == @subclass
+ proc.call.should == "hello"
+ proc.ok.should == true
+ end
+
+ # JRUBY-5026
+ describe "using a reified block parameter" do
+ it "returns an instance of the subclass" do
+ cls = Class.new do
+ def self.subclass=(subclass)
+ @subclass = subclass
+ end
+ def self.foo(&block)
+ @subclass.new(&block)
+ end
+ end
+ cls.subclass = @subclass
+ proc = cls.foo {"hello"}
+
+ proc.class.should == @subclass
+ proc.call.should == "hello"
+ proc.ok.should == true
+ end
+ end
+ end
+
+ # JRUBY-5261; Proc sets up the block during .new, not in #initialize
+ describe "called on a subclass of Proc that does not 'super' in 'initialize'" do
+ before :each do
+ @subclass = Class.new(Proc) do
+ attr_reader :ok
+ def initialize
+ @ok = true
+ end
+ end
+ end
+
+ it "still constructs a functional proc" do
+ proc = @subclass.new {'ok'}
+ proc.call.should == 'ok'
+ proc.ok.should == true
+ end
+ end
+
+ it "raises a LocalJumpError when context of the block no longer exists" do
+ def some_method
+ Proc.new { return }
+ end
+ res = some_method()
+
+ -> { res.call }.should.raise(LocalJumpError)
+ end
+
+ it "returns from within enclosing method when 'return' is used in the block" do
+ # we essentially verify that the created instance behaves like proc,
+ # not like lambda.
+ def some_method
+ Proc.new { return :proc_return_value }.call
+ :method_return_value
+ end
+ some_method.should == :proc_return_value
+ end
+
+ it "returns a subclass of Proc" do
+ obj = ProcSpecs::MyProc.new { }
+ obj.should.is_a?(ProcSpecs::MyProc)
+ end
+
+ it "calls initialize on the Proc object" do
+ obj = ProcSpecs::MyProc2.new(:a, 2) { }
+ obj.first.should == :a
+ obj.second.should == 2
+ end
+end
+
+describe "Proc.new with a block argument" do
+ it "returns the passed proc created from a block" do
+ passed_prc = Proc.new { "hello".size }
+ prc = Proc.new(&passed_prc)
+
+ prc.should.equal?(passed_prc)
+ prc.call.should == 5
+ end
+
+ it "returns the passed proc created from a method" do
+ method = "hello".method(:size)
+ passed_prc = Proc.new(&method)
+ prc = Proc.new(&passed_prc)
+
+ prc.should.equal?(passed_prc)
+ prc.call.should == 5
+ end
+
+ it "returns the passed proc created from a symbol" do
+ passed_prc = Proc.new(&:size)
+ prc = Proc.new(&passed_prc)
+
+ prc.should.equal?(passed_prc)
+ prc.call("hello").should == 5
+ end
+end
+
+describe "Proc.new with a block argument called indirectly from a subclass" do
+ it "returns the passed proc created from a block" do
+ passed_prc = ProcSpecs::MyProc.new { "hello".size }
+ passed_prc.class.should == ProcSpecs::MyProc
+ prc = ProcSpecs::MyProc.new(&passed_prc)
+
+ prc.should.equal?(passed_prc)
+ prc.call.should == 5
+ end
+
+ it "returns the passed proc created from a method" do
+ method = "hello".method(:size)
+ passed_prc = ProcSpecs::MyProc.new(&method)
+ passed_prc.class.should == ProcSpecs::MyProc
+ prc = ProcSpecs::MyProc.new(&passed_prc)
+
+ prc.should.equal?(passed_prc)
+ prc.call.should == 5
+ end
+
+ it "returns the passed proc created from a symbol" do
+ passed_prc = ProcSpecs::MyProc.new(&:size)
+ passed_prc.class.should == ProcSpecs::MyProc
+ prc = ProcSpecs::MyProc.new(&passed_prc)
+
+ prc.should.equal?(passed_prc)
+ prc.call("hello").should == 5
+ end
+end
+
+describe "Proc.new without a block" do
+ it "raises an ArgumentError" do
+ -> { Proc.new }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if invoked from within a method with no block" do
+ -> { ProcSpecs.new_proc_in_method }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if invoked on a subclass from within a method with no block" do
+ -> { ProcSpecs.new_proc_subclass_in_method }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed no block" do
+ def some_method
+ Proc.new
+ end
+
+ -> { ProcSpecs.new_proc_in_method { "hello" } }.should.raise(ArgumentError, 'tried to create Proc object without a block')
+ -> { ProcSpecs.new_proc_subclass_in_method { "hello" } }.should.raise(ArgumentError, 'tried to create Proc object without a block')
+ -> { some_method { "hello" } }.should.raise(ArgumentError, 'tried to create Proc object without a block')
+ end
+end
diff --git a/spec/ruby/core/proc/parameters_spec.rb b/spec/ruby/core/proc/parameters_spec.rb
new file mode 100644
index 0000000000..ac8c6e3560
--- /dev/null
+++ b/spec/ruby/core/proc/parameters_spec.rb
@@ -0,0 +1,183 @@
+require_relative '../../spec_helper'
+
+describe "Proc#parameters" do
+ it "returns an empty Array for a proc expecting no parameters" do
+ proc {}.parameters.should == []
+ end
+
+ it "returns an Array of Arrays for a proc expecting parameters" do
+ p = proc {|x| }
+ p.parameters.should.instance_of?(Array)
+ p.parameters.first.should.instance_of?(Array)
+ end
+
+ it "sets the first element of each sub-Array to :opt for optional arguments" do
+ proc {|x| }.parameters.first.first.should == :opt
+ proc {|y,*x| }.parameters.first.first.should == :opt
+ end
+
+ it "regards named parameters in procs as optional" do
+ proc {|x| }.parameters.first.first.should == :opt
+ end
+
+ it "sets the first element of each sub-Array to :req for required argument if lambda keyword used" do
+ proc {|x| }.parameters(lambda: true).first.first.should == :req
+ proc {|y,*x| }.parameters(lambda: true).first.first.should == :req
+ end
+
+ it "regards named parameters in procs as required if lambda keyword used" do
+ proc {|x| }.parameters(lambda: true).first.first.should == :req
+ end
+
+ it "regards named parameters in lambda as optional if lambda: false keyword used" do
+ -> x { }.parameters(lambda: false).first.first.should == :opt
+ end
+
+ it "regards named parameters in procs and lambdas as required if lambda keyword is truthy" do
+ proc {|x| }.parameters(lambda: 123).first.first.should == :req
+ -> x { }.parameters(lambda: 123).first.first.should == :req
+ end
+
+ it "ignores the lambda keyword if it is nil" do
+ proc {|x|}.parameters(lambda: nil).first.first.should == :opt
+ -> x { }.parameters(lambda: nil).first.first.should == :req
+ end
+
+ it "regards optional keyword parameters in procs as optional" do
+ proc {|x: :y| }.parameters.first.first.should == :key
+ end
+
+ it "regards parameters with default values as optional" do
+ -> x=1 { }.parameters.first.first.should == :opt
+ proc {|x=1| }.parameters.first.first.should == :opt
+ end
+
+ it "sets the first element of each sub-Array to :req for required arguments" do
+ -> x, y=[] { }.parameters.first.first.should == :req
+ -> y, *x { }.parameters.first.first.should == :req
+ end
+
+ it "regards named parameters in lambdas as required" do
+ -> x { }.parameters.first.first.should == :req
+ end
+
+ it "regards keyword parameters in lambdas as required" do
+ -> x: { }.parameters.first.first.should == :keyreq
+ end
+
+ it "sets the first element of each sub-Array to :rest for parameters prefixed with asterisks" do
+ -> *x { }.parameters.first.first.should == :rest
+ -> x, *y { }.parameters.last.first.should == :rest
+ proc {|*x| }.parameters.first.first.should == :rest
+ proc {|x,*y| }.parameters.last.first.should == :rest
+ end
+
+ it "sets the first element of each sub-Array to :keyrest for parameters prefixed with double asterisks" do
+ -> **x { }.parameters.first.first.should == :keyrest
+ -> x, **y { }.parameters.last.first.should == :keyrest
+ proc {|**x| }.parameters.first.first.should == :keyrest
+ proc {|x,**y| }.parameters.last.first.should == :keyrest
+ end
+
+ it "sets the first element of each sub-Array to :block for parameters prefixed with ampersands" do
+ -> &x { }.parameters.first.first.should == :block
+ -> x, &y { }.parameters.last.first.should == :block
+ proc {|&x| }.parameters.first.first.should == :block
+ proc {|x,&y| }.parameters.last.first.should == :block
+ end
+
+ it "sets the second element of each sub-Array to the name of the argument" do
+ -> x { }.parameters.first.last.should == :x
+ -> x=Math::PI { }.parameters.first.last.should == :x
+ -> an_argument, glark, &foo { }.parameters[1].last.should == :glark
+ -> *rest { }.parameters.first.last.should == :rest
+ -> &block { }.parameters.first.last.should == :block
+ proc {|x| }.parameters.first.last.should == :x
+ proc {|x=Math::PI| }.parameters.first.last.should == :x
+ proc {|an_argument, glark, &foo| }.parameters[1].last.should == :glark
+ proc {|*rest| }.parameters.first.last.should == :rest
+ proc {|&block| }.parameters.first.last.should == :block
+ end
+
+ it "ignores unnamed rest arguments" do
+ -> x {}.parameters.should == [[:req, :x]]
+ end
+
+ it "ignores implicit rest arguments" do
+ proc { |x, | }.parameters.should == [[:opt, :x]]
+ -> x { }.parameters.should == [[:req, :x]]
+ end
+
+ it "adds rest arg with name * for \"star\" argument" do
+ -> * {}.parameters.should == [[:rest, :*]]
+ end
+
+ it "adds keyrest arg with ** as a name for \"double star\" argument" do
+ -> ** {}.parameters.should == [[:keyrest, :**]]
+ end
+
+ it "adds block arg with name & for anonymous block argument" do
+ -> & {}.parameters.should == [[:block, :&]]
+ end
+
+ it "does not add locals as block options with a block and splat" do
+ -> *args, &blk do
+ local_is_not_parameter = {}
+ end.parameters.should == [[:rest, :args], [:block, :blk]]
+ proc do |*args, &blk|
+ local_is_not_parameter = {}
+ end.parameters.should == [[:rest, :args], [:block, :blk]]
+ end
+
+ it "returns all parameters defined with the name _ as _" do
+ proc = proc {|_, _, _ = 1, *_, _:, _: 2, **_, &_| }
+ proc.parameters.should == [
+ [:opt, :_],
+ [:opt, :_],
+ [:opt, :_],
+ [:rest, :_],
+ [:keyreq, :_],
+ [:key, :_],
+ [:keyrest, :_],
+ [:block, :_]
+ ]
+
+ lambda = -> _, _, _ = 1, *_, _:, _: 2, **_, &_ {}
+ lambda.parameters.should == [
+ [:req, :_],
+ [:req, :_],
+ [:opt, :_],
+ [:rest, :_],
+ [:keyreq, :_],
+ [:key, :_],
+ [:keyrest, :_],
+ [:block, :_]
+ ]
+ end
+
+ it "returns :nokey for **nil parameter" do
+ proc { |**nil| }.parameters.should == [[:nokey]]
+ end
+
+ ruby_version_is "3.4"..."4.0" do
+ it "handles the usage of `it` as a parameter" do
+ eval("proc { it }").parameters.should == [[:opt, nil]]
+ eval("lambda { it }").parameters.should == [[:req]]
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "handles the usage of `it` as a parameter" do
+ eval("proc { it }").parameters.should == [[:opt]]
+ eval("lambda { it }").parameters.should == [[:req]]
+ end
+ end
+
+ ruby_version_is "4.1" do
+ it "returns :noblock for &nil parameter" do
+ eval <<~RUBY
+ proc { |&nil| }.parameters.should == [[:noblock]]
+ RUBY
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/ruby2_keywords_spec.rb b/spec/ruby/core/proc/ruby2_keywords_spec.rb
new file mode 100644
index 0000000000..e06fad8693
--- /dev/null
+++ b/spec/ruby/core/proc/ruby2_keywords_spec.rb
@@ -0,0 +1,66 @@
+require_relative '../../spec_helper'
+
+describe "Proc#ruby2_keywords" do
+ it "marks the final hash argument as keyword hash" do
+ f = -> *a { a.last }
+ f.ruby2_keywords
+
+ last = f.call(1, 2, a: "a")
+ Hash.ruby2_keywords_hash?(last).should == true
+ end
+
+ it "applies to the underlying method and applies across duplication" do
+ f1 = -> *a { a.last }
+ f1.ruby2_keywords
+ f2 = f1.dup
+
+ Hash.ruby2_keywords_hash?(f1.call(1, 2, a: "a")).should == true
+ Hash.ruby2_keywords_hash?(f2.call(1, 2, a: "a")).should == true
+
+ f3 = -> *a { a.last }
+ f4 = f3.dup
+ f3.ruby2_keywords
+
+ Hash.ruby2_keywords_hash?(f3.call(1, 2, a: "a")).should == true
+ Hash.ruby2_keywords_hash?(f4.call(1, 2, a: "a")).should == true
+ end
+
+ it "returns self" do
+ f = -> *a { }
+ f.ruby2_keywords.should.equal? f
+ end
+
+ it "prints warning when a proc does not accept argument splat" do
+ f = -> a, b, c { }
+
+ -> {
+ f.ruby2_keywords
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+
+ it "prints warning when a proc accepts keywords" do
+ f = -> *a, b: { }
+
+ -> {
+ f.ruby2_keywords
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+
+ it "prints warning when a proc accepts keyword splat" do
+ f = -> *a, **b { }
+
+ -> {
+ f.ruby2_keywords
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+
+ ruby_version_is "4.0" do
+ it "prints warning when a proc accepts post arguments" do
+ f = -> *a, b { }
+
+ -> {
+ f.ruby2_keywords
+ }.should complain(/Skipping set of ruby2_keywords flag for/)
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/shared/call.rb b/spec/ruby/core/proc/shared/call.rb
new file mode 100644
index 0000000000..fae2331b68
--- /dev/null
+++ b/spec/ruby/core/proc/shared/call.rb
@@ -0,0 +1,99 @@
+require_relative '../fixtures/common'
+
+describe :proc_call, shared: true do
+ it "invokes self" do
+ Proc.new { "test!" }.send(@method).should == "test!"
+ -> { "test!" }.send(@method).should == "test!"
+ proc { "test!" }.send(@method).should == "test!"
+ end
+
+ it "sets self's parameters to the given values" do
+ Proc.new { |a, b| a + b }.send(@method, 1, 2).should == 3
+ Proc.new { |*args| args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4]
+ Proc.new { |_, *args| args }.send(@method, 1, 2, 3).should == [2, 3]
+
+ -> a, b { a + b }.send(@method, 1, 2).should == 3
+ -> *args { args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4]
+ -> _, *args { args }.send(@method, 1, 2, 3).should == [2, 3]
+
+ proc { |a, b| a + b }.send(@method, 1, 2).should == 3
+ proc { |*args| args }.send(@method, 1, 2, 3, 4).should == [1, 2, 3, 4]
+ proc { |_, *args| args }.send(@method, 1, 2, 3).should == [2, 3]
+ end
+end
+
+
+describe :proc_call_on_proc_new, shared: true do
+ it "replaces missing arguments with nil" do
+ Proc.new { |a, b| [a, b] }.send(@method).should == [nil, nil]
+ Proc.new { |a, b| [a, b] }.send(@method, 1).should == [1, nil]
+ end
+
+ it "silently ignores extra arguments" do
+ Proc.new { |a, b| a + b }.send(@method, 1, 2, 5).should == 3
+ end
+
+ it "auto-explodes a single Array argument" do
+ p = Proc.new { |a, b| [a, b] }
+ p.send(@method, 1, 2).should == [1, 2]
+ p.send(@method, [1, 2]).should == [1, 2]
+ p.send(@method, [1, 2, 3]).should == [1, 2]
+ p.send(@method, [1, 2, 3], 4).should == [[1, 2, 3], 4]
+ end
+end
+
+describe :proc_call_on_proc_or_lambda, shared: true do
+ it "ignores excess arguments when self is a proc" do
+ a = proc {|x| x}.send(@method, 1, 2)
+ a.should == 1
+
+ a = proc {|x| x}.send(@method, 1, 2, 3)
+ a.should == 1
+
+ a = proc {|x:| x}.send(@method, 2, x: 1)
+ a.should == 1
+ end
+
+ it "will call #to_ary on argument and return self if return is nil" do
+ argument = ProcSpecs::ToAryAsNil.new
+ result = proc { |x, _| x }.send(@method, argument)
+ result.should == argument
+ end
+
+ it "substitutes nil for missing arguments when self is a proc" do
+ proc {|x,y| [x,y]}.send(@method).should == [nil,nil]
+
+ a = proc {|x,y| [x, y]}.send(@method, 1)
+ a.should == [1,nil]
+ end
+
+ it "raises an ArgumentError on excess arguments when self is a lambda" do
+ -> {
+ -> x { x }.send(@method, 1, 2)
+ }.should.raise(ArgumentError)
+
+ -> {
+ -> x { x }.send(@method, 1, 2, 3)
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError on missing arguments when self is a lambda" do
+ -> {
+ -> x { x }.send(@method)
+ }.should.raise(ArgumentError)
+
+ -> {
+ -> x, y { [x,y] }.send(@method, 1)
+ }.should.raise(ArgumentError)
+ end
+
+ it "treats a single Array argument as a single argument when self is a lambda" do
+ -> a { a }.send(@method, [1, 2]).should == [1, 2]
+ -> a, b { [a, b] }.send(@method, [1, 2], 3).should == [[1,2], 3]
+ end
+
+ it "treats a single Array argument as a single argument when self is a proc" do
+ proc { |a| a }.send(@method, [1, 2]).should == [1, 2]
+ proc { |a, b| [a, b] }.send(@method, [1, 2], 3).should == [[1,2], 3]
+ end
+end
diff --git a/spec/ruby/core/proc/shared/call_arguments.rb b/spec/ruby/core/proc/shared/call_arguments.rb
new file mode 100644
index 0000000000..91ada3439e
--- /dev/null
+++ b/spec/ruby/core/proc/shared/call_arguments.rb
@@ -0,0 +1,29 @@
+describe :proc_call_block_args, shared: true do
+ it "can receive block arguments" do
+ Proc.new {|&b| b.send(@method)}.send(@method) {1 + 1}.should == 2
+ -> &b { b.send(@method)}.send(@method) {1 + 1}.should == 2
+ proc {|&b| b.send(@method)}.send(@method) {1 + 1}.should == 2
+ end
+
+ it "yields to the block given at declaration and not to the block argument" do
+ proc_creator = Object.new
+ def proc_creator.create
+ Proc.new do |&b|
+ yield
+ end
+ end
+ a_proc = proc_creator.create { 7 }
+ a_proc.send(@method) { 3 }.should == 7
+ end
+
+ it "can call its block argument declared with a block argument" do
+ proc_creator = Object.new
+ def proc_creator.create(method_name)
+ Proc.new do |&b|
+ yield + b.send(method_name)
+ end
+ end
+ a_proc = proc_creator.create(@method) { 7 }
+ a_proc.call { 3 }.should == 10
+ end
+end
diff --git a/spec/ruby/core/proc/shared/compose.rb b/spec/ruby/core/proc/shared/compose.rb
new file mode 100644
index 0000000000..c004cec7c9
--- /dev/null
+++ b/spec/ruby/core/proc/shared/compose.rb
@@ -0,0 +1,22 @@
+describe :proc_compose, shared: true do
+ it "raises TypeError if passed not callable object" do
+ lhs = @object.call
+ not_callable = Object.new
+
+ -> {
+ lhs.send(@method, not_callable)
+ }.should.raise(TypeError, "callable object is expected")
+
+ end
+
+ it "does not try to coerce argument with #to_proc" do
+ lhs = @object.call
+
+ succ = Object.new
+ def succ.to_proc(s); s.succ; end
+
+ -> {
+ lhs.send(@method, succ)
+ }.should.raise(TypeError, "callable object is expected")
+ end
+end
diff --git a/spec/ruby/core/proc/shared/dup.rb b/spec/ruby/core/proc/shared/dup.rb
new file mode 100644
index 0000000000..2821d2e00f
--- /dev/null
+++ b/spec/ruby/core/proc/shared/dup.rb
@@ -0,0 +1,39 @@
+describe :proc_dup, shared: true do
+ it "returns a copy of self" do
+ a = -> { "hello" }
+ b = a.send(@method)
+
+ a.should_not.equal?(b)
+
+ a.call.should == b.call
+ end
+
+ it "returns an instance of subclass" do
+ cl = Class.new(Proc)
+
+ cl.new{}.send(@method).class.should == cl
+ end
+
+ ruby_version_is "3.4" do
+ it "copies instance variables" do
+ proc = -> { "hello" }
+ proc.instance_variable_set(:@ivar, 1)
+ cl = proc.send(@method)
+ cl.instance_variables.should == [:@ivar]
+ end
+
+ it "copies the finalizer" do
+ code = <<-'RUBY'
+ obj = Proc.new { }
+
+ ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized\n" })
+
+ obj.clone
+
+ exit 0
+ RUBY
+
+ ruby_exe(code).lines.sort.should == ["finalized\n", "finalized\n"]
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/shared/equal.rb b/spec/ruby/core/proc/shared/equal.rb
new file mode 100644
index 0000000000..4f6f6c41be
--- /dev/null
+++ b/spec/ruby/core/proc/shared/equal.rb
@@ -0,0 +1,83 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/common'
+
+describe :proc_equal, shared: true do
+ it "is a public method" do
+ Proc.public_instance_methods(false).should.include?(@method)
+ end
+
+ it "returns true if self and other are the same object" do
+ p = proc { :foo }
+ p.send(@method, p).should == true
+
+ p = Proc.new { :foo }
+ p.send(@method, p).should == true
+
+ p = -> { :foo }
+ p.send(@method, p).should == true
+ end
+
+ it "returns true if other is a dup of the original" do
+ p = proc { :foo }
+ p.send(@method, p.dup).should == true
+
+ p = Proc.new { :foo }
+ p.send(@method, p.dup).should == true
+
+ p = -> { :foo }
+ p.send(@method, p.dup).should == true
+ end
+
+ # identical here means the same method invocation.
+ it "returns false when bodies are the same but capture env is not identical" do
+ a = ProcSpecs.proc_for_1
+ b = ProcSpecs.proc_for_1
+
+ a.send(@method, b).should == false
+ end
+
+ it "returns false if procs are distinct but have the same body and environment" do
+ p = proc { :foo }
+ p2 = proc { :foo }
+ p.send(@method, p2).should == false
+ end
+
+ it "returns false if lambdas are distinct but have same body and environment" do
+ x = -> { :foo }
+ x2 = -> { :foo }
+ x.send(@method, x2).should == false
+ end
+
+ it "returns false if using comparing lambda to proc, even with the same body and env" do
+ p = -> { :foo }
+ p2 = proc { :foo }
+ p.send(@method, p2).should == false
+
+ x = proc { :bar }
+ x2 = -> { :bar }
+ x.send(@method, x2).should == false
+ end
+
+ it "returns false if other is not a Proc" do
+ p = proc { :foo }
+ p.send(@method, []).should == false
+
+ p = Proc.new { :foo }
+ p.send(@method, Object.new).should == false
+
+ p = -> { :foo }
+ p.send(@method, :foo).should == false
+ end
+
+ it "returns false if self and other are both procs but have different bodies" do
+ p = proc { :bar }
+ p2 = proc { :foo }
+ p.send(@method, p2).should == false
+ end
+
+ it "returns false if self and other are both lambdas but have different bodies" do
+ p = -> { :foo }
+ p2 = -> { :bar }
+ p.send(@method, p2).should == false
+ end
+end
diff --git a/spec/ruby/core/proc/shared/to_s.rb b/spec/ruby/core/proc/shared/to_s.rb
new file mode 100644
index 0000000000..a52688a89f
--- /dev/null
+++ b/spec/ruby/core/proc/shared/to_s.rb
@@ -0,0 +1,60 @@
+describe :proc_to_s, shared: true do
+ describe "for a proc created with Proc.new" do
+ it "returns a description including file and line number" do
+ Proc.new { "hello" }.send(@method).should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ }>$/
+ end
+
+ it "has a binary encoding" do
+ Proc.new { "hello" }.send(@method).encoding.should == Encoding::BINARY
+ end
+ end
+
+ describe "for a proc created with lambda" do
+ it "returns a description including '(lambda)' and including file and line number" do
+ -> { "hello" }.send(@method).should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ } \(lambda\)>$/
+ end
+
+ it "has a binary encoding" do
+ -> { "hello" }.send(@method).encoding.should == Encoding::BINARY
+ end
+ end
+
+ describe "for a proc created with proc" do
+ it "returns a description including file and line number" do
+ proc { "hello" }.send(@method).should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ }>$/
+ end
+
+ it "has a binary encoding" do
+ proc { "hello" }.send(@method).encoding.should == Encoding::BINARY
+ end
+ end
+
+ describe "for a proc created with UnboundMethod#to_proc" do
+ it "returns a description including '(lambda)' and optionally including file and line number" do
+ def hello; end
+ s = method("hello").to_proc.send(@method)
+ if s.include? __FILE__
+ s.should =~ /^#<Proc:([^ ]*?) #{Regexp.escape __FILE__}:#{__LINE__ - 3} \(lambda\)>$/
+ else
+ s.should =~ /^#<Proc:([^ ]*?) \(lambda\)>$/
+ end
+ end
+
+ it "has a binary encoding" do
+ def hello; end
+ method("hello").to_proc.send(@method).encoding.should == Encoding::BINARY
+ end
+ end
+
+ describe "for a proc created with Symbol#to_proc" do
+ it "returns a description including '(&:symbol)'" do
+ proc = :foobar.to_proc
+ proc.send(@method).should.include?('(&:foobar)')
+ end
+
+ it "has a binary encoding" do
+ proc = :foobar.to_proc
+ proc.send(@method).encoding.should == Encoding::BINARY
+ end
+ end
+end
diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb
new file mode 100644
index 0000000000..d27ad0559e
--- /dev/null
+++ b/spec/ruby/core/proc/source_location_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/source_location'
+
+describe "Proc#source_location" do
+ before :each do
+ @proc = ProcSpecs::SourceLocation.my_proc
+ @lambda = ProcSpecs::SourceLocation.my_lambda
+ @proc_new = ProcSpecs::SourceLocation.my_proc_new
+ @method = ProcSpecs::SourceLocation.my_method
+ end
+
+ it "returns an Array" do
+ @proc.source_location.should.instance_of?(Array)
+ @proc_new.source_location.should.instance_of?(Array)
+ @lambda.source_location.should.instance_of?(Array)
+ @method.source_location.should.instance_of?(Array)
+ end
+
+ it "sets the first value to the path of the file in which the proc was defined" do
+ file = @proc.source_location.first
+ file.should.instance_of?(String)
+ file.should == File.realpath('fixtures/source_location.rb', __dir__)
+
+ file = @proc_new.source_location.first
+ file.should.instance_of?(String)
+ file.should == File.realpath('fixtures/source_location.rb', __dir__)
+
+ file = @lambda.source_location.first
+ file.should.instance_of?(String)
+ file.should == File.realpath('fixtures/source_location.rb', __dir__)
+
+ file = @method.source_location.first
+ file.should.instance_of?(String)
+ file.should == File.realpath('fixtures/source_location.rb', __dir__)
+ end
+
+ it "sets the last value to an Integer representing the line on which the proc was defined" do
+ line = @proc.source_location.last
+ line.should.instance_of?(Integer)
+ line.should == 4
+
+ line = @proc_new.source_location.last
+ line.should.instance_of?(Integer)
+ line.should == 12
+
+ line = @lambda.source_location.last
+ line.should.instance_of?(Integer)
+ line.should == 8
+
+ line = @method.source_location.last
+ line.should.instance_of?(Integer)
+ line.should == 15
+ end
+
+ it "works even if the proc was created on the same line" do
+ proc { true }.source_location.should == [__FILE__, __LINE__]
+ Proc.new { true }.source_location.should == [__FILE__, __LINE__]
+ -> { true }.source_location.should == [__FILE__, __LINE__]
+ end
+
+ it "returns the first line of a multi-line proc (i.e. the line containing 'proc do')" do
+ ProcSpecs::SourceLocation.my_multiline_proc.source_location.last.should == 20
+ ProcSpecs::SourceLocation.my_multiline_proc_new.source_location.last.should == 34
+ ProcSpecs::SourceLocation.my_multiline_lambda.source_location.last.should == 27
+ end
+
+ it "returns the location of the proc's body; not necessarily the proc itself" do
+ ProcSpecs::SourceLocation.my_detached_proc.source_location.last.should == 41
+ ProcSpecs::SourceLocation.my_detached_proc_new.source_location.last.should == 51
+ ProcSpecs::SourceLocation.my_detached_lambda.source_location.last.should == 46
+ end
+
+ it "returns the same value for a proc-ified method as the method reports" do
+ method = ProcSpecs::SourceLocation.method(:my_proc)
+ proc = method.to_proc
+
+ method.source_location.should == proc.source_location
+ end
+
+ it "returns nil for a core method that has been proc-ified" do
+ method = [].method(:<<)
+ proc = method.to_proc
+
+ proc.source_location.should == nil
+ end
+
+ it "works for eval with a given line" do
+ proc = eval('-> {}', nil, "foo", 100)
+ proc.source_location.should == ["foo", 100]
+ end
+end
diff --git a/spec/ruby/core/proc/to_proc_spec.rb b/spec/ruby/core/proc/to_proc_spec.rb
new file mode 100644
index 0000000000..7f35a4f19b
--- /dev/null
+++ b/spec/ruby/core/proc/to_proc_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Proc#to_proc" do
+ it "returns self" do
+ [Proc.new {}, -> {}, proc {}].each { |p|
+ p.to_proc.should.equal?(p)
+ }
+ end
+end
diff --git a/spec/ruby/core/proc/to_s_spec.rb b/spec/ruby/core/proc/to_s_spec.rb
new file mode 100644
index 0000000000..5e9c46b6b8
--- /dev/null
+++ b/spec/ruby/core/proc/to_s_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+
+describe "Proc#to_s" do
+ it_behaves_like :proc_to_s, :to_s
+end
diff --git a/spec/ruby/core/proc/yield_spec.rb b/spec/ruby/core/proc/yield_spec.rb
new file mode 100644
index 0000000000..365d5b04bd
--- /dev/null
+++ b/spec/ruby/core/proc/yield_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'shared/call'
+require_relative 'shared/call_arguments'
+
+describe "Proc#yield" do
+ it_behaves_like :proc_call, :yield
+ it_behaves_like :proc_call_block_args, :yield
+end
+
+describe "Proc#yield on a Proc created with Proc.new" do
+ it_behaves_like :proc_call_on_proc_new, :yield
+end
+
+describe "Proc#yield on a Proc created with Kernel#lambda or Kernel#proc" do
+ it_behaves_like :proc_call_on_proc_or_lambda, :yield
+end
diff --git a/spec/ruby/core/process/_fork_spec.rb b/spec/ruby/core/process/_fork_spec.rb
new file mode 100644
index 0000000000..620f243ce5
--- /dev/null
+++ b/spec/ruby/core/process/_fork_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "Process._fork" do
+ it "for #respond_to? returns the same as Process.respond_to?(:fork)" do
+ Process.respond_to?(:_fork).should == Process.respond_to?(:fork)
+ end
+
+ # Using respond_to? in a guard here is OK because the correct semantics
+ # are that _fork is implemented if and only if fork is (see above).
+ guard_not -> { Process.respond_to?(:fork) } do
+ it "raises a NotImplementedError when called" do
+ -> { Process._fork }.should.raise(NotImplementedError)
+ end
+ end
+
+ guard -> { Process.respond_to?(:fork) } do
+ it "is called by Process#fork" do
+ Process.should_receive(:_fork).once.and_return(42)
+
+ pid = Process.fork {}
+ pid.should.equal?(42)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/abort_spec.rb b/spec/ruby/core/process/abort_spec.rb
new file mode 100644
index 0000000000..1b6ad1da43
--- /dev/null
+++ b/spec/ruby/core/process/abort_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/process/abort'
+
+describe "Process.abort" do
+ it_behaves_like :process_abort, :abort, Process
+end
diff --git a/spec/ruby/core/process/argv0_spec.rb b/spec/ruby/core/process/argv0_spec.rb
new file mode 100644
index 0000000000..4c7a2c9ecb
--- /dev/null
+++ b/spec/ruby/core/process/argv0_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Process.argv0" do
+ it "returns a String" do
+ Process.argv0.should.is_a?(String)
+ end
+
+ it "is the path given as the main script and the same as __FILE__" do
+ script = "fixtures/argv0.rb"
+
+ Dir.chdir(__dir__) do
+ ruby_exe(script).should == "#{script}\n#{script}\nOK"
+ end
+ end
+
+ it "returns a frozen object" do
+ Process.argv0.should.frozen?
+ end
+
+ it "returns every time the same object" do
+ Process.argv0.should.equal?(Process.argv0)
+ end
+end
diff --git a/spec/ruby/core/process/clock_getres_spec.rb b/spec/ruby/core/process/clock_getres_spec.rb
new file mode 100644
index 0000000000..85aa2b25f1
--- /dev/null
+++ b/spec/ruby/core/process/clock_getres_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Process.clock_getres" do
+ # These are documented
+
+ it "with :GETTIMEOFDAY_BASED_CLOCK_REALTIME reports 1 microsecond" do
+ Process.clock_getres(:GETTIMEOFDAY_BASED_CLOCK_REALTIME, :nanosecond).should == 1_000
+ end
+
+ it "with :TIME_BASED_CLOCK_REALTIME reports 1 second" do
+ Process.clock_getres(:TIME_BASED_CLOCK_REALTIME, :nanosecond).should == 1_000_000_000
+ end
+
+ platform_is_not :windows do
+ it "with :GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID reports 1 microsecond" do
+ Process.clock_getres(:GETRUSAGE_BASED_CLOCK_PROCESS_CPUTIME_ID, :nanosecond).should == 1_000
+ end
+ end
+
+ # These are observed
+
+ platform_is :linux, :darwin, :windows do
+ it "with Process::CLOCK_REALTIME reports at least 10 millisecond" do
+ Process.clock_getres(Process::CLOCK_REALTIME, :nanosecond).should <= 10_000_000
+ end
+ end
+
+ platform_is :linux, :darwin, :windows do
+ it "with Process::CLOCK_MONOTONIC reports at least 10 millisecond" do
+ Process.clock_getres(Process::CLOCK_MONOTONIC, :nanosecond).should <= 10_000_000
+ end
+ end
+end
diff --git a/spec/ruby/core/process/clock_gettime_spec.rb b/spec/ruby/core/process/clock_gettime_spec.rb
new file mode 100644
index 0000000000..88158904e1
--- /dev/null
+++ b/spec/ruby/core/process/clock_gettime_spec.rb
@@ -0,0 +1,152 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/clocks'
+
+describe "Process.clock_gettime" do
+ ProcessSpecs.clock_constants.each do |name, value|
+ it "can be called with Process::#{name}" do
+ Process.clock_gettime(value).should.instance_of?(Float)
+ end
+ end
+
+ describe 'time units' do
+ it 'handles a fixed set of time units' do
+ [:nanosecond, :microsecond, :millisecond, :second].each do |unit|
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, unit).should.is_a?(Integer)
+ end
+
+ [:float_microsecond, :float_millisecond, :float_second].each do |unit|
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, unit).should.instance_of?(Float)
+ end
+ end
+
+ it 'raises an ArgumentError for an invalid time unit' do
+ -> { Process.clock_gettime(Process::CLOCK_MONOTONIC, :bad) }.should.raise(ArgumentError)
+ end
+
+ it 'defaults to :float_second' do
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
+
+ t1.should.instance_of?(Float)
+ t2.should.instance_of?(Float)
+ t2.should be_close(t1, TIME_TOLERANCE)
+ end
+
+ it 'uses the default time unit (:float_second) when passed nil' do
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC, nil)
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
+
+ t1.should.instance_of?(Float)
+ t2.should.instance_of?(Float)
+ t2.should be_close(t1, TIME_TOLERANCE)
+ end
+ end
+
+ describe "supports the platform clocks mentioned in the documentation" do
+ it "CLOCK_REALTIME" do
+ Process.clock_gettime(Process::CLOCK_REALTIME).should.instance_of?(Float)
+ end
+
+ it "CLOCK_MONOTONIC" do
+ Process.clock_gettime(Process::CLOCK_MONOTONIC).should.instance_of?(Float)
+ end
+
+ # These specs need macOS 10.12+ / darwin 16+
+ guard -> { platform_is_not(:darwin) or kernel_version_is '16' } do
+ platform_is :linux, :openbsd, :darwin do
+ it "CLOCK_PROCESS_CPUTIME_ID" do
+ Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID).should.instance_of?(Float)
+ end
+ end
+
+ platform_is :linux, :freebsd, :openbsd, :darwin do
+ it "CLOCK_THREAD_CPUTIME_ID" do
+ Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID).should.instance_of?(Float)
+ end
+ end
+
+ platform_is :linux, :darwin do
+ it "CLOCK_MONOTONIC_RAW" do
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW).should.instance_of?(Float)
+ end
+ end
+
+ platform_is :darwin do
+ it "CLOCK_MONOTONIC_RAW_APPROX" do
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_RAW_APPROX).should.instance_of?(Float)
+ end
+
+ it "CLOCK_UPTIME_RAW and CLOCK_UPTIME_RAW_APPROX" do
+ Process.clock_gettime(Process::CLOCK_UPTIME_RAW).should.instance_of?(Float)
+ Process.clock_gettime(Process::CLOCK_UPTIME_RAW_APPROX).should.instance_of?(Float)
+ end
+ end
+ end
+
+ platform_is :freebsd do
+ it "CLOCK_VIRTUAL" do
+ Process.clock_gettime(Process::CLOCK_VIRTUAL).should.instance_of?(Float)
+ end
+
+ it "CLOCK_PROF" do
+ Process.clock_gettime(Process::CLOCK_PROF).should.instance_of?(Float)
+ end
+ end
+
+ platform_is :freebsd, :openbsd do
+ it "CLOCK_UPTIME" do
+ Process.clock_gettime(Process::CLOCK_UPTIME).should.instance_of?(Float)
+ end
+ end
+
+ platform_is :freebsd do
+ it "CLOCK_REALTIME_FAST and CLOCK_REALTIME_PRECISE" do
+ Process.clock_gettime(Process::CLOCK_REALTIME_FAST).should.instance_of?(Float)
+ Process.clock_gettime(Process::CLOCK_REALTIME_PRECISE).should.instance_of?(Float)
+ end
+
+ it "CLOCK_MONOTONIC_FAST and CLOCK_MONOTONIC_PRECISE" do
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_FAST).should.instance_of?(Float)
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_PRECISE).should.instance_of?(Float)
+ end
+
+ it "CLOCK_UPTIME_FAST and CLOCK_UPTIME_PRECISE" do
+ Process.clock_gettime(Process::CLOCK_UPTIME_FAST).should.instance_of?(Float)
+ Process.clock_gettime(Process::CLOCK_UPTIME_PRECISE).should.instance_of?(Float)
+ end
+
+ it "CLOCK_SECOND" do
+ Process.clock_gettime(Process::CLOCK_SECOND).should.instance_of?(Float)
+ end
+ end
+
+ guard -> { platform_is :linux and kernel_version_is '2.6.32' } do
+ it "CLOCK_REALTIME_COARSE" do
+ Process.clock_gettime(Process::CLOCK_REALTIME_COARSE).should.instance_of?(Float)
+ end
+
+ it "CLOCK_MONOTONIC_COARSE" do
+ Process.clock_gettime(Process::CLOCK_MONOTONIC_COARSE).should.instance_of?(Float)
+ end
+ end
+
+ guard -> { platform_is :linux and kernel_version_is '2.6.39' } do
+ it "CLOCK_BOOTTIME" do
+ skip "No Process::CLOCK_BOOTTIME" unless defined?(Process::CLOCK_BOOTTIME)
+ Process.clock_gettime(Process::CLOCK_BOOTTIME).should.instance_of?(Float)
+ end
+ end
+
+ guard -> { platform_is "x86_64-linux" and kernel_version_is '3.0' } do
+ it "CLOCK_REALTIME_ALARM" do
+ skip "No Process::CLOCK_REALTIME_ALARM" unless defined?(Process::CLOCK_REALTIME_ALARM)
+ Process.clock_gettime(Process::CLOCK_REALTIME_ALARM).should.instance_of?(Float)
+ end
+
+ it "CLOCK_BOOTTIME_ALARM" do
+ skip "No Process::CLOCK_BOOTTIME_ALARM" unless defined?(Process::CLOCK_BOOTTIME_ALARM)
+ Process.clock_gettime(Process::CLOCK_BOOTTIME_ALARM).should.instance_of?(Float)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/constants_spec.rb b/spec/ruby/core/process/constants_spec.rb
new file mode 100644
index 0000000000..5a4c478e74
--- /dev/null
+++ b/spec/ruby/core/process/constants_spec.rb
@@ -0,0 +1,126 @@
+require_relative '../../spec_helper'
+
+describe "Process::Constants" do
+ platform_is :darwin, :netbsd, :freebsd do
+ describe "on BSD-like systems" do
+ %i[
+ WNOHANG
+ WUNTRACED
+ PRIO_PROCESS
+ PRIO_PGRP
+ PRIO_USER
+ RLIM_INFINITY
+ RLIMIT_CPU
+ RLIMIT_FSIZE
+ RLIMIT_DATA
+ RLIMIT_STACK
+ RLIMIT_CORE
+ RLIMIT_RSS
+ RLIMIT_MEMLOCK
+ RLIMIT_NPROC
+ RLIMIT_NOFILE
+ ].each do |const|
+ it "defines #{const}" do
+ Process.const_defined?(const).should == true
+ Process.const_get(const).should.instance_of?(Integer)
+ end
+ end
+ end
+ end
+
+ platform_is :darwin do
+ describe "on Darwin" do
+ %i[
+ RLIM_SAVED_MAX
+ RLIM_SAVED_CUR
+ RLIMIT_AS
+ ].each do |const|
+ it "defines #{const}" do
+ Process.const_defined?(const).should == true
+ Process.const_get(const).should.instance_of?(Integer)
+ end
+ end
+ end
+ end
+
+ platform_is :linux do
+ describe "on Linux" do
+ %i[
+ WNOHANG
+ WUNTRACED
+ PRIO_PROCESS
+ PRIO_PGRP
+ PRIO_USER
+ RLIMIT_CPU
+ RLIMIT_FSIZE
+ RLIMIT_DATA
+ RLIMIT_STACK
+ RLIMIT_CORE
+ RLIMIT_RSS
+ RLIMIT_NPROC
+ RLIMIT_NOFILE
+ RLIMIT_MEMLOCK
+ RLIMIT_AS
+ RLIM_INFINITY
+ RLIM_SAVED_MAX
+ RLIM_SAVED_CUR
+ ].each do |const|
+ it "defines #{const}" do
+ Process.const_defined?(const).should == true
+ Process.const_get(const).should.instance_of?(Integer)
+ end
+ end
+ end
+ end
+
+ platform_is :netbsd, :freebsd do
+ describe "on NetBSD and FreeBSD" do
+ %i[
+ RLIMIT_SBSIZE
+ RLIMIT_AS
+ ].each do |const|
+ it "defines #{const}" do
+ Process.const_defined?(const).should == true
+ Process.const_get(const).should.instance_of?(Integer)
+ end
+ end
+ end
+ end
+
+ platform_is :freebsd do
+ describe "on FreeBSD" do
+ %i[
+ RLIMIT_NPTS
+ ].each do |const|
+ it "defines #{const}" do
+ Process.const_defined?(const).should == true
+ Process.const_get(const).should.instance_of?(Integer)
+ end
+ end
+ end
+ end
+
+ platform_is :windows do
+ describe "on Windows" do
+ %i[
+ RLIMIT_CPU
+ RLIMIT_FSIZE
+ RLIMIT_DATA
+ RLIMIT_STACK
+ RLIMIT_CORE
+ RLIMIT_RSS
+ RLIMIT_NPROC
+ RLIMIT_NOFILE
+ RLIMIT_MEMLOCK
+ RLIMIT_AS
+ RLIM_INFINITY
+ RLIM_SAVED_MAX
+ RLIM_SAVED_CUR
+ ].each do |const|
+ it "does not define #{const}" do
+ Process.const_defined?(const).should == false
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/daemon_spec.rb b/spec/ruby/core/process/daemon_spec.rb
new file mode 100644
index 0000000000..9b7eba1411
--- /dev/null
+++ b/spec/ruby/core/process/daemon_spec.rb
@@ -0,0 +1,118 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+platform_is_not :windows do
+ # macOS 15 is not working this examples
+ return if /darwin/ =~ RUBY_PLATFORM && /15/ =~ `sw_vers -productVersion`
+
+ describe :process_daemon_keep_stdio_open_false, shared: true do
+ it "redirects stdout to /dev/null" do
+ @daemon.invoke("keep_stdio_open_false_stdout", @object).should == ""
+ end
+
+ it "redirects stderr to /dev/null" do
+ @daemon.invoke("keep_stdio_open_false_stderr", @object).should == ""
+ end
+
+ it "redirects stdin to /dev/null" do
+ @daemon.invoke("keep_stdio_open_false_stdin", @object).should == ""
+ end
+
+ it "does not close open files" do
+ @daemon.invoke("keep_stdio_open_files", @object).should == "false"
+ end
+ end
+
+ describe :process_daemon_keep_stdio_open_true, shared: true do
+ it "does not redirect stdout to /dev/null" do
+ @daemon.invoke("keep_stdio_open_true_stdout", @object).should == "writing to stdout"
+ end
+
+ it "does not redirect stderr to /dev/null" do
+ @daemon.invoke("keep_stdio_open_true_stderr", @object).should == "writing to stderr"
+ end
+
+ it "does not redirect stdin to /dev/null" do
+ @daemon.invoke("keep_stdio_open_true_stdin", @object).should == "reading from stdin"
+ end
+
+ it "does not close open files" do
+ @daemon.invoke("keep_stdio_open_files", @object).should == "false"
+ end
+ end
+
+ describe "Process.daemon" do
+ before :each do
+ @invoke_dir = Dir.pwd
+ @daemon = ProcessSpecs::Daemonizer.new
+ end
+
+ after :each do
+ rm_r @daemon.input, @daemon.data if @daemon
+ end
+
+ it "returns 0" do
+ @daemon.invoke("return_value").should == "0"
+ end
+
+ it "has a different PID after daemonizing" do
+ parent, daemon = @daemon.invoke("pid").split(":")
+ parent.should_not == daemon
+ end
+
+ it "has a different process group after daemonizing" do
+ parent, daemon = @daemon.invoke("process_group").split(":")
+ parent.should_not == daemon
+ end
+
+ it "does not run existing at_exit handlers when daemonizing" do
+ @daemon.invoke("daemonizing_at_exit").should == "not running at_exit"
+ end
+
+ it "runs at_exit handlers when the daemon exits" do
+ @daemon.invoke("daemon_at_exit").should == "running at_exit"
+ end
+
+ it "changes directory to the root directory if the first argument is not given" do
+ @daemon.invoke("stay_in_dir").should == "/"
+ end
+
+ it "changes directory to the root directory if the first argument is false" do
+ @daemon.invoke("stay_in_dir", [false]).should == "/"
+ end
+
+ it "changes directory to the root directory if the first argument is nil" do
+ @daemon.invoke("stay_in_dir", [nil]).should == "/"
+ end
+
+ it "does not change to the root directory if the first argument is true" do
+ @daemon.invoke("stay_in_dir", [true]).should == @invoke_dir
+ end
+
+ describe "when the second argument is not given" do
+ it_behaves_like :process_daemon_keep_stdio_open_false, nil, [false]
+ end
+
+ describe "when the second argument is false" do
+ it_behaves_like :process_daemon_keep_stdio_open_false, nil, [false, false]
+ end
+
+ describe "when the second argument is nil" do
+ it_behaves_like :process_daemon_keep_stdio_open_false, nil, [false, nil]
+ end
+
+ describe "when the second argument is true" do
+ it_behaves_like :process_daemon_keep_stdio_open_true, nil, [false, true]
+ end
+ end
+end
+
+platform_is :windows do
+ describe "Process.daemon" do
+ it "raises a NotImplementedError" do
+ -> {
+ Process.daemon
+ }.should.raise(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/detach_spec.rb b/spec/ruby/core/process/detach_spec.rb
new file mode 100644
index 0000000000..33c674394e
--- /dev/null
+++ b/spec/ruby/core/process/detach_spec.rb
@@ -0,0 +1,81 @@
+require_relative '../../spec_helper'
+
+describe "Process.detach" do
+ platform_is_not :windows do
+ it "returns a thread" do
+ pid = Process.fork { Process.exit! }
+ thr = Process.detach(pid)
+ thr.should.is_a?(Thread)
+ thr.join
+ end
+
+ it "produces the exit Process::Status as the thread value" do
+ pid = Process.fork { Process.exit! }
+ thr = Process.detach(pid)
+ thr.join
+
+ status = thr.value
+ status.should.is_a?(Process::Status)
+ status.pid.should == pid
+ end
+
+ platform_is_not :openbsd do
+ it "reaps the child process's status automatically" do
+ pid = Process.fork { Process.exit! }
+ Process.detach(pid).join
+ -> { Process.waitpid(pid) }.should.raise(Errno::ECHILD)
+ end
+ end
+
+ it "sets the :pid thread-local to the PID" do
+ pid = Process.fork { Process.exit! }
+ thr = Process.detach(pid)
+ thr.join
+
+ thr[:pid].should == pid
+ end
+
+ it "provides a #pid method on the returned thread which returns the PID" do
+ pid = Process.fork { Process.exit! }
+ thr = Process.detach(pid)
+ thr.join
+
+ thr.pid.should == pid
+ end
+
+ it "tolerates not existing child process pid" do
+ # Use a value that is close to the INT_MAX (pid usually is signed int).
+ # It should (at least) be greater than allowed pid limit value that depends on OS.
+ pid_not_existing = 2.pow(30)
+
+ # Check that there is no a child process with this hardcoded pid.
+ # Command `kill 0 pid`:
+ # - returns "1" if a process exists and
+ # - raises Errno::ESRCH otherwise
+ -> { Process.kill(0, pid_not_existing) }.should.raise(Errno::ESRCH)
+
+ thr = Process.detach(pid_not_existing)
+ thr.join
+
+ thr.should.is_a?(Thread)
+ end
+
+ it "calls #to_int to implicitly convert non-Integer pid to Integer" do
+ pid = MockObject.new('mock-enumerable')
+ pid.should_receive(:to_int).and_return(100500)
+
+ Process.detach(pid).join
+ end
+
+ it "raises TypeError when pid argument does not have #to_int method" do
+ -> { Process.detach(Object.new) }.should.raise(TypeError, "no implicit conversion of Object into Integer")
+ end
+
+ it "raises TypeError when #to_int returns non-Integer value" do
+ pid = MockObject.new('mock-enumerable')
+ pid.should_receive(:to_int).and_return(:symbol)
+
+ -> { Process.detach(pid) }.should raise_consistent_error(TypeError, "can't convert MockObject into Integer (MockObject#to_int gives Symbol)")
+ end
+ end
+end
diff --git a/spec/ruby/core/process/egid_spec.rb b/spec/ruby/core/process/egid_spec.rb
new file mode 100644
index 0000000000..69c86bebe2
--- /dev/null
+++ b/spec/ruby/core/process/egid_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+
+describe "Process.egid" do
+ it "returns the effective group ID for this process" do
+ Process.egid.should.is_a?(Integer)
+ end
+
+ it "also goes by Process::GID.eid" do
+ Process::GID.eid.should == Process.egid
+ end
+
+ it "also goes by Process::Sys.getegid" do
+ Process::Sys.getegid.should == Process.egid
+ end
+end
+
+describe "Process.egid=" do
+
+ platform_is_not :windows do
+ it "raises TypeError if not passed an Integer or String" do
+ -> { Process.egid = Object.new }.should.raise(TypeError)
+ end
+
+ it "sets the effective group id to its own gid if given the username corresponding to its own gid" do
+ raise unless Process.gid == Process.egid
+
+ require "etc"
+ group = Etc.getgrgid(Process.gid).name
+
+ Process.egid = group
+ Process.egid.should == Process.gid
+ end
+
+ as_user do
+ it "raises Errno::ERPERM if run by a non superuser trying to set the root group id" do
+ -> { Process.egid = 0 }.should.raise(Errno::EPERM)
+ end
+
+ platform_is :linux do
+ it "raises Errno::ERPERM if run by a non superuser trying to set the group id from group name" do
+ -> { Process.egid = "root" }.should.raise(Errno::EPERM)
+ end
+ end
+ end
+
+ as_superuser do
+ context "when ran by a superuser" do
+ it "sets the effective group id for the current process if run by a superuser" do
+ code = <<-RUBY
+ Process.egid = 1
+ puts Process.egid
+ RUBY
+ ruby_exe(code).should == "1\n"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/euid_spec.rb b/spec/ruby/core/process/euid_spec.rb
new file mode 100644
index 0000000000..da76f06e59
--- /dev/null
+++ b/spec/ruby/core/process/euid_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+
+describe "Process.euid" do
+ it "returns the effective user ID for this process" do
+ Process.euid.should.is_a?(Integer)
+ end
+
+ it "also goes by Process::UID.eid" do
+ Process::UID.eid.should == Process.euid
+ end
+
+ it "also goes by Process::Sys.geteuid" do
+ Process::Sys.geteuid.should == Process.euid
+ end
+end
+
+describe "Process.euid=" do
+
+ platform_is_not :windows do
+ it "raises TypeError if not passed an Integer" do
+ -> { Process.euid = Object.new }.should.raise(TypeError)
+ end
+
+ it "sets the effective user id to its own uid if given the username corresponding to its own uid" do
+ raise unless Process.uid == Process.euid
+
+ require "etc"
+ user = Etc.getpwuid(Process.uid).name
+
+ Process.euid = user
+ Process.euid.should == Process.uid
+ end
+
+ as_user do
+ it "raises Errno::ERPERM if run by a non superuser trying to set the superuser id" do
+ -> { Process.euid = 0 }.should.raise(Errno::EPERM)
+ end
+
+ it "raises Errno::ERPERM if run by a non superuser trying to set the superuser id from username" do
+ -> { Process.euid = "root" }.should.raise(Errno::EPERM)
+ end
+ end
+
+ as_superuser do
+ describe "if run by a superuser" do
+ it "sets the effective user id for the current process if run by a superuser" do
+ code = <<-RUBY
+ Process.euid = 1
+ puts Process.euid
+ RUBY
+ ruby_exe(code).should == "1\n"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/exec_spec.rb b/spec/ruby/core/process/exec_spec.rb
new file mode 100644
index 0000000000..a48d461b02
--- /dev/null
+++ b/spec/ruby/core/process/exec_spec.rb
@@ -0,0 +1,241 @@
+require_relative '../../spec_helper'
+
+describe "Process.exec" do
+ it "raises Errno::ENOENT for an empty string" do
+ -> { Process.exec "" }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises Errno::ENOENT for a command which does not exist" do
+ -> { Process.exec "bogus-noent-script.sh" }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises an ArgumentError if the command includes a null byte" do
+ -> { Process.exec "\000" }.should.raise(ArgumentError)
+ end
+
+ unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable
+ platform_is_not :windows do
+ it "raises Errno::EACCES when the file does not have execute permissions" do
+ -> { Process.exec __FILE__ }.should.raise(Errno::EACCES)
+ end
+ end
+
+ platform_is :windows do
+ it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do
+ -> { Process.exec __FILE__ }.should.raise(SystemCallError) { |e|
+ [Errno::EACCES, Errno::ENOEXEC].should.include?(e.class)
+ }
+ end
+ end
+ end
+
+ it "raises Errno::EACCES when passed a directory" do
+ -> { Process.exec __dir__ }.should.raise(Errno::EACCES)
+ end
+
+ it "runs the specified command, replacing current process" do
+ ruby_exe('Process.exec "echo hello"; puts "fail"').should == "hello\n"
+ end
+
+ it "sets the current directory when given the :chdir option" do
+ tmpdir = tmp("")[0..-2]
+ platform_is_not :windows do
+ ruby_exe("Process.exec(\"pwd\", chdir: #{tmpdir.inspect})").should == "#{tmpdir}\n"
+ end
+ platform_is :windows do
+ ruby_exe("Process.exec(\"cd\", chdir: #{tmpdir.inspect})").tr('\\', '/').should == "#{tmpdir}\n"
+ end
+ end
+
+ it "flushes STDOUT upon exit when it's not set to sync" do
+ ruby_exe("STDOUT.sync = false; STDOUT.write 'hello'").should == "hello"
+ end
+
+ it "flushes STDERR upon exit when it's not set to sync" do
+ ruby_exe("STDERR.sync = false; STDERR.write 'hello'", args: "2>&1").should == "hello"
+ end
+
+ describe "with a single argument" do
+ before :each do
+ @dir = tmp("exec_with_dir", false)
+ Dir.mkdir @dir
+
+ @name = "some_file"
+ @path = tmp("exec_with_dir/#{@name}", false)
+ touch @path
+ end
+
+ after :each do
+ rm_r @path
+ rm_r @dir
+ end
+
+ platform_is_not :windows do
+ it "subjects the specified command to shell expansion" do
+ result = Dir.chdir(@dir) do
+ ruby_exe('Process.exec "echo *"')
+ end
+ result.chomp.should == @name
+ end
+
+ it "creates an argument array with shell parsing semantics for whitespace" do
+ ruby_exe('Process.exec "echo a b c d"').should == "a b c d\n"
+ end
+ end
+
+ platform_is :windows do
+ # There is no shell expansion on Windows
+ it "does not subject the specified command to shell expansion on Windows" do
+ result = Dir.chdir(@dir) do
+ ruby_exe('Process.exec "echo *"')
+ end
+ result.should == "*\n"
+ end
+
+ it "does not create an argument array with shell parsing semantics for whitespace on Windows" do
+ ruby_exe('Process.exec "echo a b c d"').should == "a b c d\n"
+ end
+ end
+
+ end
+
+ describe "with multiple arguments" do
+ it "does not subject the arguments to shell expansion" do
+ cmd = '"echo", "*"'
+ platform_is :windows do
+ cmd = '"cmd.exe", "/C", "echo", "*"'
+ end
+ ruby_exe("Process.exec #{cmd}").should == "*\n"
+ end
+ end
+
+ describe "(environment variables)" do
+ before :each do
+ ENV["FOO"] = "FOO"
+ end
+
+ after :each do
+ ENV["FOO"] = nil
+ end
+
+ var = '$FOO'
+ platform_is :windows do
+ var = '%FOO%'
+ end
+
+ it "sets environment variables in the child environment" do
+ ruby_exe('Process.exec({"FOO" => "BAR"}, "echo ' + var + '")').should == "BAR\n"
+ end
+
+ it "unsets environment variables whose value is nil" do
+ platform_is_not :windows do
+ ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")').should == "\n"
+ end
+ platform_is :windows do
+ # On Windows, echo-ing a non-existent env var is treated as echo-ing any other string of text
+ ruby_exe('Process.exec({"FOO" => nil}, "echo ' + var + '")').should == var + "\n"
+ end
+ end
+
+ it "coerces environment argument using to_hash" do
+ ruby_exe('o = Object.new; def o.to_hash; {"FOO" => "BAR"}; end; Process.exec(o, "echo ' + var + '")').should == "BAR\n"
+ end
+
+ it "unsets other environment variables when given a true :unsetenv_others option" do
+ platform_is_not :windows do
+ ruby_exe('Process.exec("echo ' + var + '", unsetenv_others: true)').should == "\n"
+ end
+ platform_is :windows do
+ ruby_exe('Process.exec("' + ENV['COMSPEC'].gsub('\\', '\\\\\\') + ' /C echo ' + var + '", unsetenv_others: true)').should == var + "\n"
+ end
+ end
+ end
+
+ describe "with a command array" do
+ it "uses the first element as the command name and the second as the argv[0] value" do
+ platform_is_not :windows do
+ ruby_exe('Process.exec(["/bin/sh", "argv_zero"], "-c", "echo $0")').should == "argv_zero\n"
+ end
+ platform_is :windows do
+ ruby_exe('Process.exec(["cmd.exe", "/C"], "/C", "echo", "argv_zero")').should == "argv_zero\n"
+ end
+ end
+
+ it "coerces the argument using to_ary" do
+ platform_is_not :windows do
+ ruby_exe('o = Object.new; def o.to_ary; ["/bin/sh", "argv_zero"]; end; Process.exec(o, "-c", "echo $0")').should == "argv_zero\n"
+ end
+ platform_is :windows do
+ ruby_exe('o = Object.new; def o.to_ary; ["cmd.exe", "/C"]; end; Process.exec(o, "/C", "echo", "argv_zero")').should == "argv_zero\n"
+ end
+ end
+
+ it "raises an ArgumentError if the Array does not have exactly two elements" do
+ -> { Process.exec([]) }.should.raise(ArgumentError)
+ -> { Process.exec([:a]) }.should.raise(ArgumentError)
+ -> { Process.exec([:a, :b, :c]) }.should.raise(ArgumentError)
+ end
+ end
+
+ platform_is_not :windows do
+ describe "with an options Hash" do
+ describe "with Integer option keys" do
+ before :each do
+ @name = tmp("exec_fd_map.txt")
+ @child_fd_file = tmp("child_fd_file.txt")
+ end
+
+ after :each do
+ rm_r @name, @child_fd_file
+ end
+
+ it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do
+ map_fd_fixture = fixture __FILE__, "map_fd.rb"
+ cmd = <<-EOC
+ f = File.open(#{@name.inspect}, "w+")
+ File.open(#{__FILE__.inspect}, "r") do |io|
+ child_fd = io.fileno
+ File.open(#{@child_fd_file.inspect}, "w") { |io| io.print child_fd }
+ Process.exec "#{ruby_cmd(map_fd_fixture)} \#{child_fd}", { child_fd => f }
+ end
+ EOC
+
+ ruby_exe(cmd)
+ child_fd = IO.read(@child_fd_file).to_i
+ child_fd.to_i.should > STDERR.fileno
+
+ File.read(@name).should == "writing to fd: #{child_fd}"
+ end
+
+ it "lets the process after exec have specified file descriptor despite close_on_exec" do
+ map_fd_fixture = fixture __FILE__, "map_fd.rb"
+ cmd = <<-EOC
+ f = File.open(#{@name.inspect}, 'w+')
+ puts(f.fileno, f.close_on_exec?)
+ STDOUT.flush
+ Process.exec("#{ruby_cmd(map_fd_fixture)} \#{f.fileno}", f.fileno => f.fileno)
+ EOC
+
+ output = ruby_exe(cmd)
+ child_fd, close_on_exec = output.split
+
+ child_fd.to_i.should > STDERR.fileno
+ close_on_exec.should == 'true'
+ File.read(@name).should == "writing to fd: #{child_fd}"
+ end
+
+ it "sets close_on_exec to false on specified fd even when it fails" do
+ cmd = <<-EOC
+ f = File.open(#{__FILE__.inspect}, 'r')
+ puts(f.close_on_exec?)
+ Process.exec('/', f.fileno => f.fileno) rescue nil
+ puts(f.close_on_exec?)
+ EOC
+
+ output = ruby_exe(cmd)
+ output.split.should == ['true', 'false']
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/exit_spec.rb b/spec/ruby/core/process/exit_spec.rb
new file mode 100644
index 0000000000..4f7dc94407
--- /dev/null
+++ b/spec/ruby/core/process/exit_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/process/exit'
+
+describe "Process.exit" do
+ it_behaves_like :process_exit, :exit, Process
+end
+
+describe "Process.exit!" do
+ it_behaves_like :process_exit!, :exit!, "Process"
+end
diff --git a/spec/ruby/core/process/fixtures/argv0.rb b/spec/ruby/core/process/fixtures/argv0.rb
new file mode 100644
index 0000000000..847a3e903e
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/argv0.rb
@@ -0,0 +1,6 @@
+puts Process.argv0
+puts __FILE__
+
+if Process.argv0 == __FILE__
+ print "OK"
+end
diff --git a/spec/ruby/core/process/fixtures/clocks.rb b/spec/ruby/core/process/fixtures/clocks.rb
new file mode 100644
index 0000000000..5757e280be
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/clocks.rb
@@ -0,0 +1,18 @@
+module ProcessSpecs
+ def self.clock_constants
+ clocks = []
+
+ platform_is_not :windows do
+ clocks += Process.constants.select { |c| c.to_s.start_with?('CLOCK_') }
+
+ # These require CAP_WAKE_ALARM and are not documented in
+ # Process#clock_gettime. They return EINVAL if the permission
+ # is not granted.
+ clocks -= [:CLOCK_BOOTTIME_ALARM, :CLOCK_REALTIME_ALARM]
+ end
+
+ clocks.sort.map { |c|
+ [c, Process.const_get(c)]
+ }
+ end
+end
diff --git a/spec/ruby/core/process/fixtures/common.rb b/spec/ruby/core/process/fixtures/common.rb
new file mode 100644
index 0000000000..f49513d262
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/common.rb
@@ -0,0 +1,88 @@
+module ProcessSpecs
+ def self.use_system_ruby(context)
+ if defined?(MSpecScript::SYSTEM_RUBY)
+ context.send(:before, :all) do
+ @ruby = ::RUBY_EXE
+ suppress_warning {
+ Object.const_set(:RUBY_EXE, MSpecScript::SYSTEM_RUBY)
+ }
+ end
+
+ context.send(:after, :all) do
+ suppress_warning {
+ Object.const_set(:RUBY_EXE, @ruby)
+ }
+ end
+ end
+ end
+
+ class Daemonizer
+ attr_reader :input, :data
+
+ def initialize
+ # Fast feedback for implementations without Process.daemon
+ raise NotImplementedError, "Process.daemon is not implemented" unless Process.respond_to? :daemon
+
+ @script = fixture __FILE__, "daemon.rb"
+ @input = tmp("process_daemon_input_file")
+ @data = tmp("process_daemon_data_file")
+ @args = []
+ end
+
+ def wait_for_daemon
+ sleep 0.001 until File.exist?(@data) and File.size?(@data)
+ end
+
+ def invoke(behavior, arguments=[])
+ args = Marshal.dump(arguments).unpack("H*")
+ args << @input << @data << behavior
+
+ ruby_exe @script, args: args
+
+ wait_for_daemon
+
+ return unless File.exist? @data
+
+ File.open(@data, "rb") { |f| return f.read.chomp }
+ end
+ end
+
+ class Signalizer
+ attr_reader :pid_file, :pid
+
+ def initialize(scenario=nil)
+ platform_is :windows do
+ fail "not supported on windows"
+ end
+ @script = fixture __FILE__, "kill.rb"
+ @pid = nil
+ @pid_file = tmp("process_kill_signal_file")
+ rm_r @pid_file
+
+ @thread = Thread.new do
+ Thread.current.abort_on_exception = true
+ args = [@pid_file]
+ args << scenario if scenario
+ @result = ruby_exe @script, args: args
+ end
+ Thread.pass while @thread.status and !File.exist?(@pid_file)
+ while @thread.status && (@pid.nil? || @pid == 0)
+ @pid = IO.read(@pid_file).chomp.to_i
+ end
+ end
+
+ def wait_on_result
+ @thread.join
+ end
+
+ def cleanup
+ wait_on_result
+ rm_r pid_file
+ end
+
+ def result
+ wait_on_result
+ @result.chomp if @result
+ end
+ end
+end
diff --git a/spec/ruby/core/process/fixtures/daemon.rb b/spec/ruby/core/process/fixtures/daemon.rb
new file mode 100644
index 0000000000..772df2d09e
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/daemon.rb
@@ -0,0 +1,111 @@
+module ProcessSpecs
+ class Daemon
+ def initialize(argv)
+ args, @input, @data, @behavior = argv
+ @args = Marshal.load [args].pack("H*")
+ @no_at_exit = false
+ end
+
+ def run
+ send @behavior
+
+ # Exit without running any at_exit handlers
+ exit!(0) if @no_at_exit
+ end
+
+ def write(data)
+ File.open(@data, "wb") { |f| f.puts data }
+ end
+
+ def daemonizing_at_exit
+ at_exit do
+ write "running at_exit"
+ end
+
+ @no_at_exit = true
+ Process.daemon
+ write "not running at_exit"
+ end
+
+ def return_value
+ write Process.daemon.to_s
+ end
+
+ def pid
+ parent = Process.pid
+ Process.daemon
+ daemon = Process.pid
+ write "#{parent}:#{daemon}"
+ end
+
+ def process_group
+ parent = Process.getpgrp
+ Process.daemon
+ daemon = Process.getpgrp
+ write "#{parent}:#{daemon}"
+ end
+
+ def daemon_at_exit
+ at_exit do
+ write "running at_exit"
+ end
+
+ Process.daemon
+ end
+
+ def stay_in_dir
+ Process.daemon(*@args)
+ write Dir.pwd
+ end
+
+ def keep_stdio_open_false_stdout
+ Process.daemon(*@args)
+ $stdout.write "writing to stdout"
+ write ""
+ end
+
+ def keep_stdio_open_false_stderr
+ Process.daemon(*@args)
+ $stderr.write "writing to stderr"
+ write ""
+ end
+
+ def keep_stdio_open_false_stdin
+ Process.daemon(*@args)
+
+ # Reading from /dev/null will return right away. If STDIN were not
+ # /dev/null, reading would block and the spec would hang. This is not a
+ # perfect way to spec the behavior but it works.
+ write $stdin.read
+ end
+
+ def keep_stdio_open_true_stdout
+ $stdout.reopen @data
+ Process.daemon(*@args)
+ $stdout.write "writing to stdout"
+ end
+
+ def keep_stdio_open_true_stderr
+ $stderr.reopen @data
+ Process.daemon(*@args)
+ $stderr.write "writing to stderr"
+ end
+
+ def keep_stdio_open_true_stdin
+ File.open(@input, "w") { |f| f.puts "reading from stdin" }
+
+ $stdin.reopen @input, "r"
+ Process.daemon(*@args)
+ write $stdin.read
+ end
+
+ def keep_stdio_open_files
+ file = File.open @input, "w"
+
+ Process.daemon(*@args)
+ write file.closed?
+ end
+ end
+end
+
+ProcessSpecs::Daemon.new(ARGV).run
diff --git a/spec/ruby/core/process/fixtures/in.txt b/spec/ruby/core/process/fixtures/in.txt
new file mode 100644
index 0000000000..cf52303bdc
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/in.txt
@@ -0,0 +1 @@
+stdin
diff --git a/spec/ruby/core/process/fixtures/kill.rb b/spec/ruby/core/process/fixtures/kill.rb
new file mode 100644
index 0000000000..b922a043f1
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/kill.rb
@@ -0,0 +1,43 @@
+pid_file = ARGV.shift
+scenario = ARGV.shift
+
+# We must do this first otherwise there will be a race with the process that
+# creates this process and the TERM signal below could go to that process
+# instead, which will likely abort the specs process.
+Process.setsid if scenario
+
+mutex = Mutex.new
+
+Signal.trap(:TERM) do
+ if mutex.try_lock
+ STDOUT.puts "signaled"
+ STDOUT.flush
+ $signaled = true
+ end
+end
+
+File.open(pid_file, "wb") { |f| f.puts Process.pid }
+
+if scenario
+ # We are sending a signal to the process group
+ process = "Process.getpgrp"
+
+ case scenario
+ when "self"
+ signal = %["SIGTERM"]
+ process = "0"
+ when "group_numeric"
+ signal = %[-Signal.list["TERM"]]
+ when "group_short_string"
+ signal = %["-TERM"]
+ when "group_full_string"
+ signal = %["-SIGTERM"]
+ else
+ raise "unknown scenario: #{scenario.inspect}"
+ end
+
+ code = "Process.kill(#{signal}, #{process})"
+ system(ENV["RUBY_EXE"], *ENV["RUBY_FLAGS"].split(' '), "-e", code)
+end
+
+sleep 0.001 until mutex.locked? and $signaled
diff --git a/spec/ruby/core/process/fixtures/map_fd.rb b/spec/ruby/core/process/fixtures/map_fd.rb
new file mode 100644
index 0000000000..3ed887486b
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/map_fd.rb
@@ -0,0 +1,9 @@
+fd = ARGV.shift.to_i
+
+f = File.for_fd(fd)
+f.autoclose = false
+begin
+ f.write "writing to fd: #{fd}"
+ensure
+ f.close
+end
diff --git a/spec/ruby/core/process/fixtures/setpriority.rb b/spec/ruby/core/process/fixtures/setpriority.rb
new file mode 100644
index 0000000000..cf22d85b12
--- /dev/null
+++ b/spec/ruby/core/process/fixtures/setpriority.rb
@@ -0,0 +1,12 @@
+case ARGV[0]
+when "process"
+ which = Process::PRIO_PROCESS
+when "group"
+ Process.setpgrp
+ which = Process::PRIO_PGRP
+end
+
+priority = Process.getpriority(which, 0)
+p priority
+p Process.setpriority(which, 0, priority + 1)
+p Process.getpriority(which, 0)
diff --git a/spec/ruby/core/process/fork_spec.rb b/spec/ruby/core/process/fork_spec.rb
new file mode 100644
index 0000000000..a4f765247d
--- /dev/null
+++ b/spec/ruby/core/process/fork_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/process/fork'
+
+describe "Process.fork" do
+ it_behaves_like :process_fork, :fork, Process
+end
diff --git a/spec/ruby/core/process/getpgid_spec.rb b/spec/ruby/core/process/getpgid_spec.rb
new file mode 100644
index 0000000000..c1dd007b16
--- /dev/null
+++ b/spec/ruby/core/process/getpgid_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Process.getpgid" do
+ platform_is_not :windows do
+ it "coerces the argument to an Integer" do
+ Process.getpgid(mock_int(Process.pid)).should == Process.getpgrp
+ end
+
+ it "returns the process group ID for the given process id" do
+ Process.getpgid(Process.pid).should == Process.getpgrp
+ end
+
+ it "returns the process group ID for the calling process id when passed 0" do
+ Process.getpgid(0).should == Process.getpgrp
+ end
+ end
+end
diff --git a/spec/ruby/core/process/getpgrp_spec.rb b/spec/ruby/core/process/getpgrp_spec.rb
new file mode 100644
index 0000000000..e1d1c5f92d
--- /dev/null
+++ b/spec/ruby/core/process/getpgrp_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+# see setpgrp_spec.rb
+
+describe "Process.getpgrp" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/getpriority_spec.rb b/spec/ruby/core/process/getpriority_spec.rb
new file mode 100644
index 0000000000..53fe7bfe20
--- /dev/null
+++ b/spec/ruby/core/process/getpriority_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Process.getpriority" do
+ platform_is_not :windows do
+
+ it "coerces arguments to Integers" do
+ ret = Process.getpriority mock_int(Process::PRIO_PROCESS), mock_int(0)
+ ret.should.is_a?(Integer)
+ end
+
+ it "gets the scheduling priority for a specified process" do
+ Process.getpriority(Process::PRIO_PROCESS, 0).should.is_a?(Integer)
+ end
+
+ it "gets the scheduling priority for a specified process group" do
+ Process.getpriority(Process::PRIO_PGRP, 0).should.is_a?(Integer)
+ end
+
+ it "gets the scheduling priority for a specified user" do
+ Process.getpriority(Process::PRIO_USER, 0).should.is_a?(Integer)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/getrlimit_spec.rb b/spec/ruby/core/process/getrlimit_spec.rb
new file mode 100644
index 0000000000..d36d8c3335
--- /dev/null
+++ b/spec/ruby/core/process/getrlimit_spec.rb
@@ -0,0 +1,100 @@
+require_relative '../../spec_helper'
+
+platform_is :aix do
+ # In AIX, if getrlimit(2) is called multiple times with RLIMIT_DATA,
+ # the first call and the subsequent calls return slightly different
+ # values of rlim_cur, even if the process does nothing between
+ # the calls. This behavior causes some of the tests in this spec
+ # to fail, so call Process.getrlimit(:DATA) once and discard the result.
+ # Subsequent calls to Process.getrlimit(:DATA) should return
+ # a consistent value of rlim_cur.
+ Process.getrlimit(:DATA)
+end
+
+describe "Process.getrlimit" do
+ platform_is_not :windows do
+ it "returns a two-element Array of Integers" do
+ result = Process.getrlimit Process::RLIMIT_CORE
+ result.size.should == 2
+ result.first.should.is_a?(Integer)
+ result.last.should.is_a?(Integer)
+ end
+
+ context "when passed an Object" do
+ before do
+ @resource = Process::RLIMIT_CORE
+ end
+
+ it "calls #to_int to convert to an Integer" do
+ obj = mock("process getrlimit integer")
+ obj.should_receive(:to_int).and_return(@resource)
+
+ Process.getrlimit(obj).should == Process.getrlimit(@resource)
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ obj = mock("process getrlimit integer")
+ obj.should_receive(:to_int).and_return(nil)
+
+ -> { Process.getrlimit(obj) }.should.raise(TypeError)
+ end
+ end
+
+ context "when passed a Symbol" do
+ it "coerces the short name into the full RLIMIT_ prefixed name" do
+ Process.constants.grep(/\ARLIMIT_/) do |fullname|
+ short = fullname[/\ARLIMIT_(.+)/, 1]
+ Process.getrlimit(short.to_sym).should == Process.getrlimit(Process.const_get(fullname))
+ end
+ end
+
+ it "raises ArgumentError when passed an unknown resource" do
+ -> { Process.getrlimit(:FOO) }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when passed a String" do
+ it "coerces the short name into the full RLIMIT_ prefixed name" do
+ Process.constants.grep(/\ARLIMIT_/) do |fullname|
+ short = fullname[/\ARLIMIT_(.+)/, 1]
+ Process.getrlimit(short).should == Process.getrlimit(Process.const_get(fullname))
+ end
+ end
+
+ it "raises ArgumentError when passed an unknown resource" do
+ -> { Process.getrlimit("FOO") }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when passed on Object" do
+ before do
+ @resource = Process::RLIMIT_CORE
+ end
+
+ it "calls #to_str to convert to a String" do
+ obj = mock("process getrlimit string")
+ obj.should_receive(:to_str).and_return("CORE")
+ obj.should_not_receive(:to_int)
+
+ Process.getrlimit(obj).should == Process.getrlimit(@resource)
+ end
+
+ it "calls #to_int if #to_str does not return a String" do
+ obj = mock("process getrlimit string")
+ obj.should_receive(:to_str).and_return(nil)
+ obj.should_receive(:to_int).and_return(@resource)
+
+ Process.getrlimit(obj).should == Process.getrlimit(@resource)
+ end
+ end
+ end
+
+ platform_is :windows do
+ it "is not implemented" do
+ Process.respond_to?(:getrlimit).should == false
+ -> do
+ Process.getrlimit(nil)
+ end.should.raise NotImplementedError
+ end
+ end
+end
diff --git a/spec/ruby/core/process/gid/change_privilege_spec.rb b/spec/ruby/core/process/gid/change_privilege_spec.rb
new file mode 100644
index 0000000000..4ada277514
--- /dev/null
+++ b/spec/ruby/core/process/gid/change_privilege_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.change_privilege" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/eid_spec.rb b/spec/ruby/core/process/gid/eid_spec.rb
new file mode 100644
index 0000000000..3f2186bb6a
--- /dev/null
+++ b/spec/ruby/core/process/gid/eid_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.eid" do
+ it "needs to be reviewed for spec completeness"
+end
+
+describe "Process::GID.eid=" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/grant_privilege_spec.rb b/spec/ruby/core/process/gid/grant_privilege_spec.rb
new file mode 100644
index 0000000000..10da7f8a26
--- /dev/null
+++ b/spec/ruby/core/process/gid/grant_privilege_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.grant_privilege" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/re_exchange_spec.rb b/spec/ruby/core/process/gid/re_exchange_spec.rb
new file mode 100644
index 0000000000..9642c81c5b
--- /dev/null
+++ b/spec/ruby/core/process/gid/re_exchange_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.re_exchange" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/re_exchangeable_spec.rb b/spec/ruby/core/process/gid/re_exchangeable_spec.rb
new file mode 100644
index 0000000000..1c38f903d5
--- /dev/null
+++ b/spec/ruby/core/process/gid/re_exchangeable_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.re_exchangeable?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/rid_spec.rb b/spec/ruby/core/process/gid/rid_spec.rb
new file mode 100644
index 0000000000..ad66c94e72
--- /dev/null
+++ b/spec/ruby/core/process/gid/rid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.rid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/sid_available_spec.rb b/spec/ruby/core/process/gid/sid_available_spec.rb
new file mode 100644
index 0000000000..8d86b3e9d2
--- /dev/null
+++ b/spec/ruby/core/process/gid/sid_available_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.sid_available?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid/switch_spec.rb b/spec/ruby/core/process/gid/switch_spec.rb
new file mode 100644
index 0000000000..b162f1e782
--- /dev/null
+++ b/spec/ruby/core/process/gid/switch_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::GID.switch" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/gid_spec.rb b/spec/ruby/core/process/gid_spec.rb
new file mode 100644
index 0000000000..ca935ed520
--- /dev/null
+++ b/spec/ruby/core/process/gid_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+
+describe "Process.gid" do
+ platform_is_not :windows do
+ it "returns the correct gid for the user executing this process" do
+ current_gid_according_to_unix = `id -gr`.to_i
+ Process.gid.should == current_gid_according_to_unix
+ end
+ end
+
+ it "also goes by Process::GID.rid" do
+ Process::GID.rid.should == Process.gid
+ end
+
+ it "also goes by Process::Sys.getgid" do
+ Process::Sys.getgid.should == Process.gid
+ end
+end
+
+describe "Process.gid=" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/groups_spec.rb b/spec/ruby/core/process/groups_spec.rb
new file mode 100644
index 0000000000..fa916671a4
--- /dev/null
+++ b/spec/ruby/core/process/groups_spec.rb
@@ -0,0 +1,67 @@
+require_relative '../../spec_helper'
+
+describe "Process.groups" do
+ platform_is_not :windows do
+ it "gets an Array of the gids of groups in the supplemental group access list" do
+ groups = `id -G`.scan(/\d+/).map { |i| i.to_i }
+ # Include the standard `id` command output. On macOS, GNU
+ # coreutils `id` is limited to NGROUPS_MAX groups, because of
+ # the backward compatibility of getgroups(2).
+ (groups |= `/usr/bin/id -G`.scan(/\d+/).map { |i| i.to_i }) rescue nil
+ gid = Process.gid
+
+ expected = (groups.sort - [gid]).uniq.sort
+ actual = (Process.groups - [gid]).uniq.sort
+ actual.should == expected
+ end
+ end
+end
+
+describe "Process.groups=" do
+ platform_is_not :windows, :android do
+ as_superuser do
+ it "sets the list of gids of groups in the supplemental group access list" do
+ groups = Process.groups
+ Process.groups = []
+ Process.groups.should == []
+ Process.groups = groups
+ Process.groups.sort.should == groups.sort
+ end
+ end
+
+ as_user do
+ platform_is :aix do
+ it "sets the list of gids of groups in the supplemental group access list" do
+ # setgroups() is not part of the POSIX standard,
+ # so its behavior varies from OS to OS. AIX allows a non-root
+ # process to set the supplementary group IDs, as long as
+ # they are presently in its supplementary group IDs.
+ # The order of the following tests matters.
+ # After this process executes "Process.groups = []"
+ # it should no longer be able to set any supplementary
+ # group IDs, even if it originally belonged to them.
+ # It should only be able to set its primary group ID.
+ groups = Process.groups
+ Process.groups = groups
+ Process.groups.sort.should == groups.sort
+ Process.groups = []
+ Process.groups.should == []
+ Process.groups = [ Process.gid ]
+ Process.groups.should == [ Process.gid ]
+ supplementary = groups - [ Process.gid ]
+ if supplementary.length > 0
+ -> { Process.groups = supplementary }.should.raise(Errno::EPERM)
+ end
+ end
+ end
+
+ platform_is_not :aix do
+ it "raises Errno::EPERM" do
+ -> {
+ Process.groups = [0]
+ }.should.raise(Errno::EPERM)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/initgroups_spec.rb b/spec/ruby/core/process/initgroups_spec.rb
new file mode 100644
index 0000000000..d9f31936cb
--- /dev/null
+++ b/spec/ruby/core/process/initgroups_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+
+describe "Process.initgroups" do
+ platform_is_not :windows, :android do
+ as_user do
+ it "initializes the supplemental group access list" do
+ name = `id -un`.strip
+ groups = Process.groups
+ gid = groups.max.to_i + 1
+ augmented_groups = `id -G`.scan(/\d+/).map {|i| i.to_i} << gid
+ if Process.uid == 0
+ Process.groups = []
+ Process.initgroups(name, gid).sort.should == augmented_groups.sort
+ Process.groups.sort.should == augmented_groups.sort
+ Process.groups = groups
+ else
+ -> { Process.initgroups(name, gid) }.should.raise(Errno::EPERM)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/kill_spec.rb b/spec/ruby/core/process/kill_spec.rb
new file mode 100644
index 0000000000..885c2bf2b7
--- /dev/null
+++ b/spec/ruby/core/process/kill_spec.rb
@@ -0,0 +1,132 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Process.kill" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :each do
+ @pid = Process.pid
+ end
+
+ it "raises an ArgumentError for unknown signals" do
+ -> { Process.kill("FOO", @pid) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if passed a lowercase signal name" do
+ -> { Process.kill("term", @pid) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if signal is not an Integer or String" do
+ signal = mock("process kill signal")
+ signal.should_not_receive(:to_int)
+
+ -> { Process.kill(signal, @pid) }.should.raise(ArgumentError)
+ end
+
+ it "raises Errno::ESRCH if the process does not exist" do
+ pid = Process.spawn(*ruby_exe, "-e", "sleep 10")
+ Process.kill("SIGKILL", pid)
+ Process.wait(pid)
+ -> {
+ Process.kill("SIGKILL", pid)
+ }.should.raise(Errno::ESRCH)
+ end
+
+ it "checks for existence and permissions to signal a process, but does not actually signal it, when using signal 0" do
+ Process.kill(0, @pid).should == 1
+ end
+end
+
+platform_is_not :windows do
+ describe "Process.kill" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :each do
+ @sp = ProcessSpecs::Signalizer.new
+ end
+
+ after :each do
+ @sp.cleanup
+ end
+
+ it "accepts a Symbol as a signal name" do
+ Process.kill(:SIGTERM, @sp.pid)
+ @sp.result.should == "signaled"
+ end
+
+ it "accepts a String as signal name" do
+ Process.kill("SIGTERM", @sp.pid)
+ @sp.result.should == "signaled"
+ end
+
+ it "accepts a signal name without the 'SIG' prefix" do
+ Process.kill("TERM", @sp.pid)
+ @sp.result.should == "signaled"
+ end
+
+ it "accepts a signal name with the 'SIG' prefix" do
+ Process.kill("SIGTERM", @sp.pid)
+ @sp.result.should == "signaled"
+ end
+
+ it "accepts an Integer as a signal value" do
+ Process.kill(15, @sp.pid)
+ @sp.result.should == "signaled"
+ end
+
+ it "calls #to_int to coerce the pid to an Integer" do
+ Process.kill("SIGTERM", mock_int(@sp.pid))
+ @sp.result.should == "signaled"
+ end
+ end
+
+ describe "Process.kill" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :each do
+ @sp1 = ProcessSpecs::Signalizer.new
+ @sp2 = ProcessSpecs::Signalizer.new
+ end
+
+ after :each do
+ @sp1.cleanup
+ @sp2.cleanup
+ end
+
+ it "signals multiple processes" do
+ Process.kill("SIGTERM", @sp1.pid, @sp2.pid)
+ @sp1.result.should == "signaled"
+ @sp2.result.should == "signaled"
+ end
+
+ it "returns the number of processes signaled" do
+ Process.kill("SIGTERM", @sp1.pid, @sp2.pid).should == 2
+ end
+ end
+
+ describe "Process.kill" do
+ after :each do
+ @sp.cleanup if @sp
+ end
+
+ it "signals the process group if the PID is zero" do
+ @sp = ProcessSpecs::Signalizer.new "self"
+ @sp.result.should == "signaled"
+ end
+
+ it "signals the process group if the signal number is negative" do
+ @sp = ProcessSpecs::Signalizer.new "group_numeric"
+ @sp.result.should == "signaled"
+ end
+
+ it "signals the process group if the short signal name starts with a minus sign" do
+ @sp = ProcessSpecs::Signalizer.new "group_short_string"
+ @sp.result.should == "signaled"
+ end
+
+ it "signals the process group if the full signal name starts with a minus sign" do
+ @sp = ProcessSpecs::Signalizer.new "group_full_string"
+ @sp.result.should == "signaled"
+ end
+ end
+end
diff --git a/spec/ruby/core/process/last_status_spec.rb b/spec/ruby/core/process/last_status_spec.rb
new file mode 100644
index 0000000000..1bd8adf798
--- /dev/null
+++ b/spec/ruby/core/process/last_status_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+
+describe 'Process#last_status' do
+ it 'returns the status of the last executed child process in the current thread' do
+ pid = Process.wait Process.spawn("exit 0")
+ Process.last_status.pid.should == pid
+ end
+
+ it 'returns nil if no child process has been ever executed in the current thread' do
+ Thread.new do
+ Process.last_status.should == nil
+ end.join
+ end
+
+ it 'raises an ArgumentError if any arguments are provided' do
+ -> { Process.last_status(1) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/process/maxgroups_spec.rb b/spec/ruby/core/process/maxgroups_spec.rb
new file mode 100644
index 0000000000..895b384bd0
--- /dev/null
+++ b/spec/ruby/core/process/maxgroups_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+platform_is_not :windows do
+ describe "Process.maxgroups" do
+ it "returns the maximum number of gids allowed in the supplemental group access list" do
+ Process.maxgroups.should.is_a?(Integer)
+ end
+
+ it "sets the maximum number of gids allowed in the supplemental group access list" do
+ n = Process.maxgroups
+ begin
+ Process.maxgroups = n - 1
+ Process.maxgroups.should == n - 1
+ ensure
+ Process.maxgroups = n
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/pid_spec.rb b/spec/ruby/core/process/pid_spec.rb
new file mode 100644
index 0000000000..42b0b918b9
--- /dev/null
+++ b/spec/ruby/core/process/pid_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Process.pid" do
+ it "returns the process id of this process" do
+ pid = Process.pid
+ pid.should.is_a?(Integer)
+ Process.pid.should == pid
+ end
+end
diff --git a/spec/ruby/core/process/ppid_spec.rb b/spec/ruby/core/process/ppid_spec.rb
new file mode 100644
index 0000000000..47c32a8591
--- /dev/null
+++ b/spec/ruby/core/process/ppid_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Process.ppid" do
+ platform_is_not :windows do
+ it "returns the process id of the parent of this process" do
+ ruby_exe("puts Process.ppid").should == "#{Process.pid}\n"
+ end
+ end
+end
diff --git a/spec/ruby/core/process/set_proctitle_spec.rb b/spec/ruby/core/process/set_proctitle_spec.rb
new file mode 100644
index 0000000000..28a0fa6cee
--- /dev/null
+++ b/spec/ruby/core/process/set_proctitle_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+# Note that there's no way to get the current process title defined as a spec
+# somewhere. Process.setproctitle explicitly does not change `$0` so the only
+# way to get the process title is to shell out.
+describe 'Process.setproctitle' do
+ platform_is :linux, :darwin do
+ before :each do
+ @old_title = $0
+ end
+
+ after :each do
+ Process.setproctitle(@old_title)
+ end
+
+ it 'should set the process title' do
+ title = 'rubyspec-proctitle-test'
+
+ Process.setproctitle(title).should == title
+ `ps -ocommand= -p#{$$}`.should.include?(title)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/setpgid_spec.rb b/spec/ruby/core/process/setpgid_spec.rb
new file mode 100644
index 0000000000..be724e9007
--- /dev/null
+++ b/spec/ruby/core/process/setpgid_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Process.setpgid" do
+ platform_is_not :windows do
+ # Must use fork as setpgid(2) gives EACCESS after execve()
+ it "sets the process group id of the specified process" do
+ rd, wr = IO.pipe
+
+ pid = Process.fork do
+ wr.close
+ rd.read
+ rd.close
+ Process.exit!
+ end
+
+ rd.close
+
+ begin
+ Process.getpgid(pid).should == Process.getpgrp
+ Process.setpgid(mock_int(pid), mock_int(pid)).should == 0
+ Process.getpgid(pid).should == pid
+ ensure
+ wr.write ' '
+ wr.close
+ Process.wait pid
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/setpgrp_spec.rb b/spec/ruby/core/process/setpgrp_spec.rb
new file mode 100644
index 0000000000..800668008d
--- /dev/null
+++ b/spec/ruby/core/process/setpgrp_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+# TODO: put these in the right files.
+describe "Process.setpgrp and Process.getpgrp" do
+ platform_is_not :windows do
+ it "sets and gets the process group ID of the calling process" do
+ # there are two synchronization points here:
+ # One for the child to let the parent know that it has finished
+ # setting its process group;
+ # and another for the parent to let the child know that it's ok to die.
+ read1, write1 = IO.pipe
+ read2, write2 = IO.pipe
+ pid = Process.fork do
+ read1.close
+ write2.close
+ Process.setpgrp
+ write1 << Process.getpgrp
+ write1.close
+ read2.read(1)
+ read2.close
+ Process.exit!
+ end
+ write1.close
+ read2.close
+ pgid = read1.read # wait for child to change process groups
+ read1.close
+
+ begin
+ Process.getpgid(pid).should == pgid.to_i
+ ensure
+ write2 << "!"
+ write2.close
+ Process.wait pid
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/setpriority_spec.rb b/spec/ruby/core/process/setpriority_spec.rb
new file mode 100644
index 0000000000..4d60973429
--- /dev/null
+++ b/spec/ruby/core/process/setpriority_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "Process.setpriority" do
+ platform_is_not :windows do
+ it "sets the scheduling priority for a specified process" do
+ priority = Process.getpriority(Process::PRIO_PROCESS, 0)
+
+ out = ruby_exe(fixture(__FILE__, "setpriority.rb"), args: "process")
+ out = out.lines.map { |l| Integer(l) }
+ pr = out[0]
+ out.should == [pr, 0, pr+1]
+
+ Process.getpriority(Process::PRIO_PROCESS, 0).should == priority
+ end
+
+ # Darwin and FreeBSD don't seem to handle these at all, getting all out of
+ # whack with either permission errors or just the wrong value
+ platform_is_not :darwin, :freebsd do
+ it "sets the scheduling priority for a specified process group" do
+ priority = Process.getpriority(Process::PRIO_PGRP, 0)
+
+ out = ruby_exe(fixture(__FILE__, "setpriority.rb"), args: "group")
+ out = out.lines.map { |l| Integer(l) }
+ pr = out[0]
+ out.should == [pr, 0, pr+1]
+
+ Process.getpriority(Process::PRIO_PGRP, 0).should == priority
+ end
+ end
+
+ as_superuser do
+ guard -> {
+ prio = Process.getpriority(Process::PRIO_USER, 0)
+ # The nice value is a value in the range -20 to 19.
+ # This test tries to change the nice value to +-1, so it cannot run if prio == -20 || prio == 19.
+ if -20 < prio && prio < 19
+ begin
+ # Check if we can lower the nice value or not.
+ #
+ # We are not always able to do it even as a root.
+ # Docker container is not always able to do it depending upon the configuration,
+ # which cannot know from the container itself.
+ Process.setpriority(Process::PRIO_USER, 0, prio - 1)
+ Process.setpriority(Process::PRIO_USER, 0, prio)
+ true
+ rescue Errno::EACCES
+ false
+ end
+ end
+ } do
+ it "sets the scheduling priority for a specified user" do
+ prio = Process.getpriority(Process::PRIO_USER, 0)
+ Process.setpriority(Process::PRIO_USER, 0, prio + 1).should == 0
+ Process.getpriority(Process::PRIO_USER, 0).should == (prio + 1)
+ Process.setpriority(Process::PRIO_USER, 0, prio).should == 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/setrlimit_spec.rb b/spec/ruby/core/process/setrlimit_spec.rb
new file mode 100644
index 0000000000..f02ab46fca
--- /dev/null
+++ b/spec/ruby/core/process/setrlimit_spec.rb
@@ -0,0 +1,237 @@
+require_relative '../../spec_helper'
+
+describe "Process.setrlimit" do
+ platform_is_not :windows do
+ context "when passed an Object" do
+ before do
+ @resource = Process::RLIMIT_CORE
+ @limit, @max = Process.getrlimit @resource
+ end
+
+ it "calls #to_int to convert resource to an Integer" do
+ Process.setrlimit(mock_int(@resource), @limit, @max).should == nil
+ end
+
+ it "raises a TypeError if #to_int for resource does not return an Integer" do
+ obj = mock("process getrlimit integer")
+ obj.should_receive(:to_int).and_return(nil)
+
+ -> { Process.setrlimit(obj, @limit, @max) }.should.raise(TypeError)
+ end
+
+ it "calls #to_int to convert the soft limit to an Integer" do
+ Process.setrlimit(@resource, mock_int(@limit), @max).should == nil
+ end
+
+ it "raises a TypeError if #to_int for resource does not return an Integer" do
+ obj = mock("process getrlimit integer")
+ obj.should_receive(:to_int).and_return(nil)
+
+ -> { Process.setrlimit(@resource, obj, @max) }.should.raise(TypeError)
+ end
+
+ it "calls #to_int to convert the hard limit to an Integer" do
+ Process.setrlimit(@resource, @limit, mock_int(@max)).should == nil
+ end
+
+ it "raises a TypeError if #to_int for resource does not return an Integer" do
+ obj = mock("process getrlimit integer")
+ obj.should_receive(:to_int).and_return(nil)
+
+ -> { Process.setrlimit(@resource, @limit, obj) }.should.raise(TypeError)
+ end
+ end
+
+ context "when passed a Symbol" do
+ platform_is_not :openbsd do
+ it "coerces :AS into RLIMIT_AS" do
+ Process.setrlimit(:AS, *Process.getrlimit(Process::RLIMIT_AS)).should == nil
+ end
+ end
+
+ it "coerces :CORE into RLIMIT_CORE" do
+ Process.setrlimit(:CORE, *Process.getrlimit(Process::RLIMIT_CORE)).should == nil
+ end
+
+ it "coerces :CPU into RLIMIT_CPU" do
+ Process.setrlimit(:CPU, *Process.getrlimit(Process::RLIMIT_CPU)).should == nil
+ end
+
+ it "coerces :DATA into RLIMIT_DATA" do
+ Process.setrlimit(:DATA, *Process.getrlimit(Process::RLIMIT_DATA)).should == nil
+ end
+
+ it "coerces :FSIZE into RLIMIT_FSIZE" do
+ Process.setrlimit(:FSIZE, *Process.getrlimit(Process::RLIMIT_FSIZE)).should == nil
+ end
+
+ it "coerces :NOFILE into RLIMIT_NOFILE" do
+ Process.setrlimit(:NOFILE, *Process.getrlimit(Process::RLIMIT_NOFILE)).should == nil
+ end
+
+ it "coerces :STACK into RLIMIT_STACK" do
+ Process.setrlimit(:STACK, *Process.getrlimit(Process::RLIMIT_STACK)).should == nil
+ end
+
+ platform_is_not :aix do
+ it "coerces :MEMLOCK into RLIMIT_MEMLOCK" do
+ Process.setrlimit(:MEMLOCK, *Process.getrlimit(Process::RLIMIT_MEMLOCK)).should == nil
+ end
+ end
+
+ it "coerces :NPROC into RLIMIT_NPROC" do
+ Process.setrlimit(:NPROC, *Process.getrlimit(Process::RLIMIT_NPROC)).should == nil
+ end
+
+ it "coerces :RSS into RLIMIT_RSS" do
+ Process.setrlimit(:RSS, *Process.getrlimit(Process::RLIMIT_RSS)).should == nil
+ end
+
+ platform_is :netbsd, :freebsd do
+ it "coerces :SBSIZE into RLIMIT_SBSIZE" do
+ Process.setrlimit(:SBSIZE, *Process.getrlimit(Process::RLIMIT_SBSIZE)).should == nil
+ end
+ end
+
+ platform_is :linux do
+ it "coerces :RTPRIO into RLIMIT_RTPRIO" do
+ Process.setrlimit(:RTPRIO, *Process.getrlimit(Process::RLIMIT_RTPRIO)).should == nil
+ end
+
+ guard -> { defined?(Process::RLIMIT_RTTIME) } do
+ it "coerces :RTTIME into RLIMIT_RTTIME" do
+ Process.setrlimit(:RTTIME, *Process.getrlimit(Process::RLIMIT_RTTIME)).should == nil
+ end
+ end
+
+ it "coerces :SIGPENDING into RLIMIT_SIGPENDING" do
+ Process.setrlimit(:SIGPENDING, *Process.getrlimit(Process::RLIMIT_SIGPENDING)).should == nil
+ end
+
+ it "coerces :MSGQUEUE into RLIMIT_MSGQUEUE" do
+ Process.setrlimit(:MSGQUEUE, *Process.getrlimit(Process::RLIMIT_MSGQUEUE)).should == nil
+ end
+
+ it "coerces :NICE into RLIMIT_NICE" do
+ Process.setrlimit(:NICE, *Process.getrlimit(Process::RLIMIT_NICE)).should == nil
+ end
+ end
+
+ it "raises ArgumentError when passed an unknown resource" do
+ -> { Process.setrlimit(:FOO, 1, 1) }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when passed a String" do
+ platform_is_not :openbsd do
+ it "coerces 'AS' into RLIMIT_AS" do
+ Process.setrlimit("AS", *Process.getrlimit(Process::RLIMIT_AS)).should == nil
+ end
+ end
+
+ it "coerces 'CORE' into RLIMIT_CORE" do
+ Process.setrlimit("CORE", *Process.getrlimit(Process::RLIMIT_CORE)).should == nil
+ end
+
+ it "coerces 'CPU' into RLIMIT_CPU" do
+ Process.setrlimit("CPU", *Process.getrlimit(Process::RLIMIT_CPU)).should == nil
+ end
+
+ it "coerces 'DATA' into RLIMIT_DATA" do
+ Process.setrlimit("DATA", *Process.getrlimit(Process::RLIMIT_DATA)).should == nil
+ end
+
+ it "coerces 'FSIZE' into RLIMIT_FSIZE" do
+ Process.setrlimit("FSIZE", *Process.getrlimit(Process::RLIMIT_FSIZE)).should == nil
+ end
+
+ it "coerces 'NOFILE' into RLIMIT_NOFILE" do
+ Process.setrlimit("NOFILE", *Process.getrlimit(Process::RLIMIT_NOFILE)).should == nil
+ end
+
+ it "coerces 'STACK' into RLIMIT_STACK" do
+ Process.setrlimit("STACK", *Process.getrlimit(Process::RLIMIT_STACK)).should == nil
+ end
+
+ platform_is_not :aix do
+ it "coerces 'MEMLOCK' into RLIMIT_MEMLOCK" do
+ Process.setrlimit("MEMLOCK", *Process.getrlimit(Process::RLIMIT_MEMLOCK)).should == nil
+ end
+ end
+
+ it "coerces 'NPROC' into RLIMIT_NPROC" do
+ Process.setrlimit("NPROC", *Process.getrlimit(Process::RLIMIT_NPROC)).should == nil
+ end
+
+ it "coerces 'RSS' into RLIMIT_RSS" do
+ Process.setrlimit("RSS", *Process.getrlimit(Process::RLIMIT_RSS)).should == nil
+ end
+
+ platform_is :netbsd, :freebsd do
+ it "coerces 'SBSIZE' into RLIMIT_SBSIZE" do
+ Process.setrlimit("SBSIZE", *Process.getrlimit(Process::RLIMIT_SBSIZE)).should == nil
+ end
+ end
+
+ platform_is :linux do
+ it "coerces 'RTPRIO' into RLIMIT_RTPRIO" do
+ Process.setrlimit("RTPRIO", *Process.getrlimit(Process::RLIMIT_RTPRIO)).should == nil
+ end
+
+ guard -> { defined?(Process::RLIMIT_RTTIME) } do
+ it "coerces 'RTTIME' into RLIMIT_RTTIME" do
+ Process.setrlimit("RTTIME", *Process.getrlimit(Process::RLIMIT_RTTIME)).should == nil
+ end
+ end
+
+ it "coerces 'SIGPENDING' into RLIMIT_SIGPENDING" do
+ Process.setrlimit("SIGPENDING", *Process.getrlimit(Process::RLIMIT_SIGPENDING)).should == nil
+ end
+
+ it "coerces 'MSGQUEUE' into RLIMIT_MSGQUEUE" do
+ Process.setrlimit("MSGQUEUE", *Process.getrlimit(Process::RLIMIT_MSGQUEUE)).should == nil
+ end
+
+ it "coerces 'NICE' into RLIMIT_NICE" do
+ Process.setrlimit("NICE", *Process.getrlimit(Process::RLIMIT_NICE)).should == nil
+ end
+ end
+
+ it "raises ArgumentError when passed an unknown resource" do
+ -> { Process.setrlimit("FOO", 1, 1) }.should.raise(ArgumentError)
+ end
+ end
+
+ context "when passed on Object" do
+ before do
+ @resource = Process::RLIMIT_CORE
+ @limit, @max = Process.getrlimit @resource
+ end
+
+ it "calls #to_str to convert to a String" do
+ obj = mock("process getrlimit string")
+ obj.should_receive(:to_str).and_return("CORE")
+ obj.should_not_receive(:to_int)
+
+ Process.setrlimit(obj, @limit, @max).should == nil
+ end
+
+ it "calls #to_int if #to_str does not return a String" do
+ obj = mock("process getrlimit string")
+ obj.should_receive(:to_str).and_return(nil)
+ obj.should_receive(:to_int).and_return(@resource)
+
+ Process.setrlimit(obj, @limit, @max).should == nil
+ end
+ end
+ end
+
+ platform_is :windows do
+ it "is not implemented" do
+ Process.respond_to?(:setrlimit).should == false
+ -> do
+ Process.setrlimit(nil, nil)
+ end.should.raise NotImplementedError
+ end
+ end
+end
diff --git a/spec/ruby/core/process/setsid_spec.rb b/spec/ruby/core/process/setsid_spec.rb
new file mode 100644
index 0000000000..c83f912066
--- /dev/null
+++ b/spec/ruby/core/process/setsid_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Process.setsid" do
+ platform_is_not :windows do
+ it "establishes this process as a new session and process group leader" do
+ sid = Process.getsid
+
+ out = ruby_exe("p Process.getsid; p Process.setsid; p Process.getsid").lines
+ out[0].should == "#{sid}\n"
+ out[1].should == out[2]
+ out[2].should_not == "#{sid}\n"
+
+ sid.should == Process.getsid
+ end
+ end
+end
diff --git a/spec/ruby/core/process/spawn_spec.rb b/spec/ruby/core/process/spawn_spec.rb
new file mode 100644
index 0000000000..fb619dce42
--- /dev/null
+++ b/spec/ruby/core/process/spawn_spec.rb
@@ -0,0 +1,796 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+newline = "\n"
+platform_is :windows do
+ newline = "\r\n"
+end
+
+describe :process_spawn_does_not_close_std_streams, shared: true do
+ it "does not close STDIN" do
+ code = "puts STDIN.read"
+ cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})"
+ ruby_exe(cmd, args: "< #{fixture(__FILE__, "in.txt")} > #{@name}")
+ File.binread(@name).should == %[stdin#{newline}]
+ end
+
+ it "does not close STDOUT" do
+ code = "STDOUT.puts 'hello'"
+ cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})"
+ ruby_exe(cmd, args: "> #{@name}")
+ File.binread(@name).should == "hello#{newline}"
+ end
+
+ it "does not close STDERR" do
+ code = "STDERR.puts 'hello'"
+ cmd = "Process.wait Process.spawn(#{ruby_cmd(code).inspect}, #{@options.inspect})"
+ ruby_exe(cmd, args: "2> #{@name}")
+ File.binread(@name).should =~ /hello#{newline}/
+ end
+end
+
+describe "Process.spawn" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :each do
+ @name = tmp("process_spawn.txt")
+ @var = "$FOO"
+ platform_is :windows do
+ @var = "%FOO%"
+ end
+ end
+
+ after :each do
+ rm_r @name
+ end
+
+ it "executes the given command" do
+ -> { Process.wait Process.spawn("echo spawn") }.should output_to_fd("spawn\n")
+ end
+
+ it "returns the process ID of the new process as an Integer" do
+ pid = Process.spawn(*ruby_exe, "-e", "exit")
+ Process.wait pid
+ pid.should.instance_of?(Integer)
+ end
+
+ it "returns immediately" do
+ start = Time.now
+ pid = Process.spawn(*ruby_exe, "-e", "sleep 10")
+ (Time.now - start).should < 5
+ Process.kill :KILL, pid
+ Process.wait pid
+ end
+
+ # argv processing
+
+ describe "with a single argument" do
+ platform_is_not :windows do
+ it "subjects the specified command to shell expansion" do
+ -> { Process.wait Process.spawn("echo *") }.should_not output_to_fd("*\n")
+ end
+
+ it "creates an argument array with shell parsing semantics for whitespace" do
+ -> { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n")
+ end
+ end
+
+ platform_is :windows do
+ # There is no shell expansion on Windows
+ it "does not subject the specified command to shell expansion on Windows" do
+ -> { Process.wait Process.spawn("echo *") }.should output_to_fd("*\n")
+ end
+
+ it "does not create an argument array with shell parsing semantics for whitespace on Windows" do
+ -> { Process.wait Process.spawn("echo a b c d") }.should output_to_fd("a b c d\n")
+ end
+ end
+
+ it "calls #to_str to convert the argument to a String" do
+ o = mock("to_str")
+ o.should_receive(:to_str).and_return("echo foo")
+ -> { Process.wait Process.spawn(o) }.should output_to_fd("foo\n")
+ end
+
+ it "raises an ArgumentError if the command includes a null byte" do
+ -> { Process.spawn "\000" }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if the argument does not respond to #to_str" do
+ -> { Process.spawn :echo }.should.raise(TypeError)
+ end
+ end
+
+ describe "with multiple arguments" do
+ it "does not subject the arguments to shell expansion" do
+ -> { Process.wait Process.spawn("echo", "*") }.should output_to_fd("*\n")
+ end
+
+ it "preserves whitespace in passed arguments" do
+ out = "a b c d\n"
+ platform_is :windows do
+ # The echo command on Windows takes quotes literally
+ out = "\"a b c d\"\n"
+ end
+ -> { Process.wait Process.spawn("echo", "a b c d") }.should output_to_fd(out)
+ end
+
+ it "calls #to_str to convert the arguments to Strings" do
+ o = mock("to_str")
+ o.should_receive(:to_str).and_return("foo")
+ -> { Process.wait Process.spawn("echo", o) }.should output_to_fd("foo\n")
+ end
+
+ it "raises an ArgumentError if an argument includes a null byte" do
+ -> { Process.spawn "echo", "\000" }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if an argument does not respond to #to_str" do
+ -> { Process.spawn "echo", :foo }.should.raise(TypeError)
+ end
+ end
+
+ describe "with a command array" do
+ it "uses the first element as the command name and the second as the argv[0] value" do
+ platform_is_not :windows do
+ -> { Process.wait Process.spawn(["/bin/sh", "argv_zero"], "-c", "echo $0") }.should output_to_fd("argv_zero\n")
+ end
+ platform_is :windows do
+ -> { Process.wait Process.spawn(["cmd.exe", "/C"], "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n")
+ end
+ end
+
+ it "does not subject the arguments to shell expansion" do
+ -> { Process.wait Process.spawn(["echo", "echo"], "*") }.should output_to_fd("*\n")
+ end
+
+ it "preserves whitespace in passed arguments" do
+ out = "a b c d\n"
+ platform_is :windows do
+ # The echo command on Windows takes quotes literally
+ out = "\"a b c d\"\n"
+ end
+ -> { Process.wait Process.spawn(["echo", "echo"], "a b c d") }.should output_to_fd(out)
+ end
+
+ it "calls #to_ary to convert the argument to an Array" do
+ o = mock("to_ary")
+ platform_is_not :windows do
+ o.should_receive(:to_ary).and_return(["/bin/sh", "argv_zero"])
+ -> { Process.wait Process.spawn(o, "-c", "echo $0") }.should output_to_fd("argv_zero\n")
+ end
+ platform_is :windows do
+ o.should_receive(:to_ary).and_return(["cmd.exe", "/C"])
+ -> { Process.wait Process.spawn(o, "/C", "echo", "argv_zero") }.should output_to_fd("argv_zero\n")
+ end
+ end
+
+ it "calls #to_str to convert the first element to a String" do
+ o = mock("to_str")
+ o.should_receive(:to_str).and_return("echo")
+ -> { Process.wait Process.spawn([o, "echo"], "foo") }.should output_to_fd("foo\n")
+ end
+
+ it "calls #to_str to convert the second element to a String" do
+ o = mock("to_str")
+ o.should_receive(:to_str).and_return("echo")
+ -> { Process.wait Process.spawn(["echo", o], "foo") }.should output_to_fd("foo\n")
+ end
+
+ it "raises an ArgumentError if the Array does not have exactly two elements" do
+ -> { Process.spawn([]) }.should.raise(ArgumentError)
+ -> { Process.spawn([:a]) }.should.raise(ArgumentError)
+ -> { Process.spawn([:a, :b, :c]) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the Strings in the Array include a null byte" do
+ -> { Process.spawn ["\000", "echo"] }.should.raise(ArgumentError)
+ -> { Process.spawn ["echo", "\000"] }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if an element in the Array does not respond to #to_str" do
+ -> { Process.spawn ["echo", :echo] }.should.raise(TypeError)
+ -> { Process.spawn [:echo, "echo"] }.should.raise(TypeError)
+ end
+ end
+
+ # env handling
+
+ after :each do
+ ENV.delete("FOO")
+ end
+
+ it "sets environment variables in the child environment" do
+ Process.wait Process.spawn({"FOO" => "BAR"}, "echo #{@var}>#{@name}")
+ File.read(@name).should == "BAR\n"
+ end
+
+ it "unsets environment variables whose value is nil" do
+ ENV["FOO"] = "BAR"
+ -> do
+ Process.wait Process.spawn({"FOO" => nil}, ruby_cmd("p ENV['FOO']"))
+ end.should output_to_fd("nil\n")
+ end
+
+ platform_is_not :windows do
+ it "uses the passed env['PATH'] to search the executable" do
+ dir = tmp("spawn_path_dir")
+ mkdir_p dir
+ begin
+ exe = 'process-spawn-executable-in-path'
+ path = "#{dir}/#{exe}"
+ File.write(path, "#!/bin/sh\necho $1")
+ File.chmod(0755, path)
+
+ env = { "PATH" => "#{dir}#{File::PATH_SEPARATOR}#{ENV['PATH']}" }
+ Process.wait Process.spawn(env, exe, 'OK', out: @name)
+ $?.should.success?
+ File.read(@name).should == "OK\n"
+ ensure
+ rm_r dir
+ end
+ end
+ end
+
+ it "calls #to_hash to convert the environment" do
+ o = mock("to_hash")
+ o.should_receive(:to_hash).and_return({"FOO" => "BAR"})
+ Process.wait Process.spawn(o, "echo #{@var}>#{@name}")
+ File.read(@name).should == "BAR\n"
+ end
+
+ it "calls #to_str to convert the environment keys" do
+ o = mock("to_str")
+ o.should_receive(:to_str).and_return("FOO")
+ Process.wait Process.spawn({o => "BAR"}, "echo #{@var}>#{@name}")
+ File.read(@name).should == "BAR\n"
+ end
+
+ it "calls #to_str to convert the environment values" do
+ o = mock("to_str")
+ o.should_receive(:to_str).and_return("BAR")
+ Process.wait Process.spawn({"FOO" => o}, "echo #{@var}>#{@name}")
+ File.read(@name).should == "BAR\n"
+ end
+
+ it "raises an ArgumentError if an environment key includes an equals sign" do
+ -> do
+ Process.spawn({"FOO=" => "BAR"}, "echo #{@var}>#{@name}")
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if an environment key includes a null byte" do
+ -> do
+ Process.spawn({"\000" => "BAR"}, "echo #{@var}>#{@name}")
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if an environment value includes a null byte" do
+ -> do
+ Process.spawn({"FOO" => "\000"}, "echo #{@var}>#{@name}")
+ end.should.raise(ArgumentError)
+ end
+
+ # :unsetenv_others
+
+ before :each do
+ @minimal_env = {
+ "PATH" => ENV["PATH"],
+ "HOME" => ENV["HOME"]
+ }
+ @common_env_spawn_args = [@minimal_env, "echo #{@var}>#{@name}"]
+ end
+
+ platform_is_not :windows do
+ it "unsets other environment variables when given a true :unsetenv_others option" do
+ ENV["FOO"] = "BAR"
+ Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: true)
+ $?.success?.should == true
+ File.read(@name).should == "\n"
+ end
+ end
+
+ it "does not unset other environment variables when given a false :unsetenv_others option" do
+ ENV["FOO"] = "BAR"
+ Process.wait Process.spawn(*@common_env_spawn_args, unsetenv_others: false)
+ $?.success?.should == true
+ File.read(@name).should == "BAR\n"
+ end
+
+ platform_is_not :windows do
+ it "does not unset environment variables included in the environment hash" do
+ env = @minimal_env.merge({"FOO" => "BAR"})
+ Process.wait Process.spawn(env, "echo #{@var}>#{@name}", unsetenv_others: true)
+ $?.success?.should == true
+ File.read(@name).should == "BAR\n"
+ end
+ end
+
+ # :pgroup
+
+ platform_is_not :windows do
+ it "joins the current process group by default" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"))
+ end.should output_to_fd(Process.getpgid(Process.pid).to_s)
+ end
+
+ it "joins the current process if pgroup: false" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: false)
+ end.should output_to_fd(Process.getpgid(Process.pid).to_s)
+ end
+
+ it "joins the current process if pgroup: nil" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: nil)
+ end.should output_to_fd(Process.getpgid(Process.pid).to_s)
+ end
+
+ it "joins a new process group if pgroup: true" do
+ process = -> do
+ Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: true)
+ end
+
+ process.should_not output_to_fd(Process.getpgid(Process.pid).to_s)
+ process.should output_to_fd(/\d+/)
+ end
+
+ it "joins a new process group if pgroup: 0" do
+ process = -> do
+ Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: 0)
+ end
+
+ process.should_not output_to_fd(Process.getpgid(Process.pid).to_s)
+ process.should output_to_fd(/\d+/)
+ end
+
+ it "joins the specified process group if pgroup: pgid" do
+ pgid = Process.getpgid(Process.pid)
+ # The process group is not available on all platforms.
+ # See "man proc" - /proc/[pid]/stat - (5) pgrp
+ # In Travis aarch64 environment, the value is 0.
+ #
+ # $ cat /proc/[pid]/stat
+ # 19179 (ruby) S 19160 0 0 ...
+ unless pgid.zero?
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Process.getpgid(Process.pid)"), pgroup: pgid)
+ end.should output_to_fd(pgid.to_s)
+ else
+ skip "The process group is not available."
+ end
+ end
+
+ it "raises an ArgumentError if given a negative :pgroup option" do
+ -> { Process.spawn("echo", pgroup: -1) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError if given a symbol as :pgroup option" do
+ -> { Process.spawn("echo", pgroup: :true) }.should.raise(TypeError)
+ end
+ end
+
+ platform_is :windows do
+ it "raises an ArgumentError if given :pgroup option" do
+ -> { Process.spawn("echo", pgroup: false) }.should.raise(ArgumentError)
+ end
+ end
+
+ # :rlimit_core
+ # :rlimit_cpu
+ # :rlimit_data
+
+ # :chdir
+
+ it "uses the current working directory as its working directory" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Dir.pwd"))
+ end.should output_to_fd(Dir.pwd)
+ end
+
+ describe "when passed :chdir" do
+ before do
+ @dir = tmp("spawn_chdir", false)
+ Dir.mkdir @dir
+ end
+
+ after do
+ rm_r @dir
+ end
+
+ it "changes to the directory passed for :chdir" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: @dir)
+ end.should output_to_fd(@dir)
+ end
+
+ it "calls #to_path to convert the :chdir value" do
+ dir = mock("spawn_to_path")
+ dir.should_receive(:to_path).and_return(@dir)
+
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print Dir.pwd"), chdir: dir)
+ end.should output_to_fd(@dir)
+ end
+ end
+
+ # chdir
+
+ platform_is :linux do
+ describe "inside Dir.chdir" do
+ def child_pids(pid)
+ `pgrep -P #{pid}`.lines.map { |child| Integer(child) }
+ end
+
+ it "does not create extra process without chdir" do
+ pid = Process.spawn("sleep 10")
+ begin
+ child_pids(pid).size.should == 0
+ ensure
+ Process.kill("TERM", pid)
+ Process.wait(pid)
+ end
+ end
+
+ it "kills extra chdir processes" do
+ pid = nil
+ Dir.chdir("/") do
+ pid = Process.spawn("sleep 10")
+ end
+
+ children = child_pids(pid)
+ children.size.should <= 1
+
+ Process.kill("TERM", pid)
+ Process.wait(pid)
+
+ if children.size > 0
+ # wait a bit for children to die
+ sleep(1)
+
+ children.each do |child|
+ -> do
+ Process.kill("TERM", child)
+ end.should.raise(Errno::ESRCH)
+ end
+ end
+ end
+ end
+ end
+
+ # :umask
+
+ it "uses the current umask by default" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print File.umask"))
+ end.should output_to_fd(File.umask.to_s)
+ end
+
+ platform_is_not :windows do
+ it "sets the umask if given the :umask option" do
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print File.umask"), umask: 146)
+ end.should output_to_fd("146")
+ end
+ end
+
+ # redirection
+
+ it 'redirects to the wrapped IO using wrapped_io.to_io if out: wrapped_io' do
+ File.open(@name, 'w') do |file|
+ -> do
+ wrapped_io = mock('wrapped IO')
+ wrapped_io.should_receive(:to_io).and_return(file)
+ Process.wait Process.spawn('echo Hello World', out: wrapped_io)
+ end.should output_to_fd("Hello World\n", file)
+ end
+ end
+
+ it "redirects STDOUT to the given file descriptor if out: Integer" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn("echo glark", out: file.fileno)
+ end.should output_to_fd("glark\n", file)
+ end
+ end
+
+ it "redirects STDOUT to the given file if out: IO" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn("echo glark", out: file)
+ end.should output_to_fd("glark\n", file)
+ end
+ end
+
+ it "redirects STDOUT to the given file if out: String" do
+ Process.wait Process.spawn("echo glark", out: @name)
+ File.read(@name).should == "glark\n"
+ end
+
+ it "redirects STDOUT to the given file if out: [String name, String mode]" do
+ Process.wait Process.spawn("echo glark", out: [@name, 'w'])
+ File.read(@name).should == "glark\n"
+ end
+
+ it "redirects STDERR to the given file descriptor if err: Integer" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn("echo glark>&2", err: file.fileno)
+ end.should output_to_fd("glark\n", file)
+ end
+ end
+
+ it "redirects STDERR to the given file descriptor if err: IO" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn("echo glark>&2", err: file)
+ end.should output_to_fd("glark\n", file)
+ end
+ end
+
+ it "redirects STDERR to the given file if err: String" do
+ Process.wait Process.spawn("echo glark>&2", err: @name)
+ File.read(@name).should == "glark\n"
+ end
+
+ it "redirects STDERR to child STDOUT if :err => [:child, :out]" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn("echo glark>&2", :out => file, :err => [:child, :out])
+ end.should output_to_fd("glark\n", file)
+ end
+ end
+
+ it "redirects both STDERR and STDOUT to the given file descriptor" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"),
+ [:out, :err] => file.fileno)
+ end.should output_to_fd("glarkbang", file)
+ end
+ end
+
+ it "redirects both STDERR and STDOUT to the given IO" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"),
+ [:out, :err] => file)
+ end.should output_to_fd("glarkbang", file)
+ end
+ end
+
+ it "redirects both STDERR and STDOUT at the time to the given name" do
+ touch @name
+ Process.wait Process.spawn(ruby_cmd("print(:glark); STDOUT.flush; STDERR.print(:bang)"), [:out, :err] => @name)
+ File.read(@name).should == "glarkbang"
+ end
+
+ platform_is_not :windows, :android do
+ it "closes STDERR in the child if :err => :close" do
+ File.open(@name, 'w') do |file|
+ -> do
+ code = "begin; STDOUT.puts 'out'; STDERR.puts 'hello'; rescue => e; puts 'rescued'; end"
+ Process.wait Process.spawn(ruby_cmd(code), :out => file, :err => :close)
+ end.should output_to_fd("out\nrescued\n", file)
+ end
+ end
+ end
+
+ platform_is_not :windows do
+ it "redirects non-default file descriptor to itself" do
+ File.open(@name, 'w') do |file|
+ -> do
+ Process.wait Process.spawn(
+ ruby_cmd("f = IO.new(#{file.fileno}, 'w'); f.print(:bang); f.flush"), file.fileno => file.fileno)
+ end.should output_to_fd("bang", file)
+ end
+ end
+ end
+
+ it "redirects default file descriptor to itself" do
+ -> do
+ Process.wait Process.spawn(
+ ruby_cmd("f = IO.new(#{STDOUT.fileno}, 'w'); f.print(:bang); f.flush"), STDOUT.fileno => STDOUT.fileno)
+ end.should output_to_fd("bang", STDOUT)
+ end
+
+ # :close_others
+
+ platform_is_not :windows do
+ context "defaults :close_others to" do
+ it "false" do
+ IO.pipe do |r, w|
+ w.close_on_exec = false
+ code = "io = IO.new(#{w.fileno}); io.puts('inherited'); io.close"
+ pid = Process.spawn(ruby_cmd(code))
+ w.close
+ Process.wait(pid)
+ r.read.should == "inherited\n"
+ end
+ end
+ end
+
+ context "when passed close_others: true" do
+ before :each do
+ @options = { close_others: true }
+ end
+
+ it "closes file descriptors >= 3 in the child process even if fds are set close_on_exec=false" do
+ touch @name
+ IO.pipe do |r, w|
+ r.close_on_exec = false
+ w.close_on_exec = false
+
+ begin
+ pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options)
+ w.close
+ r.read(1).should == nil
+ ensure
+ rm_r @name
+ Process.wait(pid) if pid
+ end
+ end
+ end
+
+ it_should_behave_like :process_spawn_does_not_close_std_streams
+ end
+
+ context "when passed close_others: false" do
+ before :each do
+ @options = { close_others: false }
+ end
+
+ it "closes file descriptors >= 3 in the child process because they are set close_on_exec by default" do
+ touch @name
+ IO.pipe do |r, w|
+ begin
+ pid = Process.spawn(ruby_cmd("while File.exist? '#{@name}'; sleep 0.1; end"), @options)
+ w.close
+ r.read(1).should == nil
+ ensure
+ rm_r @name
+ Process.wait(pid) if pid
+ end
+ end
+ end
+
+ it "does not close file descriptors >= 3 in the child process if fds are set close_on_exec=false" do
+ IO.pipe do |r, w|
+ r.close_on_exec = false
+ w.close_on_exec = false
+
+ code = "fd = IO.for_fd(#{w.fileno}); fd.autoclose = false; fd.write 'abc'; fd.close"
+ pid = Process.spawn(ruby_cmd(code), @options)
+ begin
+ w.close
+ r.read.should == 'abc'
+ ensure
+ Process.wait(pid)
+ end
+ end
+ end
+
+ it_should_behave_like :process_spawn_does_not_close_std_streams
+ end
+ end
+
+ # error handling
+
+ it "raises an ArgumentError if passed no command arguments" do
+ -> { Process.spawn }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if passed env or options but no command arguments" do
+ -> { Process.spawn({}) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if passed env and options but no command arguments" do
+ -> { Process.spawn({}, {}) }.should.raise(ArgumentError)
+ end
+
+ it "raises an Errno::ENOENT for an empty string" do
+ -> { Process.spawn "" }.should.raise(Errno::ENOENT)
+ end
+
+ it "raises an Errno::ENOENT if the command does not exist" do
+ -> { Process.spawn "nonesuch" }.should.raise(Errno::ENOENT, "No such file or directory - nonesuch")
+ end
+
+ it "sets $? to exit status 127 when the command does not exist" do
+ Process.spawn("nonesuch") rescue nil
+ $?.exitstatus.should == 127
+ end
+
+ it "raises an Errno::ENOENT if the file does not exist" do
+ -> { Process.spawn "./nonesuch" }.should.raise(Errno::ENOENT, "No such file or directory - ./nonesuch")
+ end
+
+ it "sets $? to exit status 127 when the file does not exist" do
+ Process.spawn("./nonesuch") rescue nil
+ $?.exitstatus.should == 127
+ end
+
+ platform_is_not :windows do
+ it "raises an Errno::EACCES when the path is a directory" do
+ -> { Process.spawn "./" }.should.raise(Errno::EACCES, "Permission denied - ./")
+ end
+ end
+
+ unless File.executable?(__FILE__) # Some FS (e.g. vboxfs) locate all files executable
+ platform_is_not :windows do
+ it "raises an Errno::EACCES when the file does not have execute permissions" do
+ -> { Process.spawn __FILE__ }.should.raise(Errno::EACCES, "Permission denied - #{__FILE__}")
+ end
+
+ it "sets $? to exit status 127 when the file does not have execute permissions" do
+ Process.spawn(__FILE__) rescue nil
+ $?.exitstatus.should == 127
+ end
+
+ it "raises an Errno::ENOENT when a non-executable file is found in PATH" do
+ dir = tmp("spawn_path_non_executable_dir")
+ mkdir_p dir
+ begin
+ exe = 'process-spawn-non-executable-in-path'
+ File.write("#{dir}/#{exe}", "#!/bin/sh\necho hi")
+ File.chmod(0644, "#{dir}/#{exe}")
+ env = { "PATH" => "#{dir}#{File::PATH_SEPARATOR}#{ENV['PATH']}" }
+ -> { Process.spawn(env, exe) }.should.raise(Errno::ENOENT, "No such file or directory - #{exe}")
+ $?.exitstatus.should == 127
+ ensure
+ rm_r dir
+ end
+ end
+ end
+
+ platform_is :windows do
+ it "raises Errno::EACCES or Errno::ENOEXEC when the file is not an executable file" do
+ -> { Process.spawn __FILE__ }.should.raise(SystemCallError) { |e|
+ [Errno::EACCES, Errno::ENOEXEC].should.include?(e.class)
+ }
+ end
+ end
+ end
+
+ it "raises an Errno::EACCES or Errno::EISDIR when passed a directory" do
+ -> { Process.spawn __dir__ }.should.raise(SystemCallError) { |e|
+ [Errno::EACCES, Errno::EISDIR].should.include?(e.class)
+ }
+ end
+
+ it "raises an ArgumentError when passed a string key in options" do
+ -> { Process.spawn("echo", "chdir" => Dir.pwd) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when passed an unknown option key" do
+ -> { Process.spawn("echo", nonesuch: :foo) }.should.raise(ArgumentError)
+ end
+
+ platform_is_not :windows, :aix do
+ describe "with Integer option keys" do
+ before :each do
+ @name = tmp("spawn_fd_map.txt")
+ @io = new_io @name, "w+"
+ @io.sync = true
+ end
+
+ after :each do
+ @io.close unless @io.closed?
+ rm_r @name
+ end
+
+ it "maps the key to a file descriptor in the child that inherits the file descriptor from the parent specified by the value" do
+ File.open(__FILE__, "r") do |f|
+ child_fd = f.fileno
+ args = ruby_cmd(fixture(__FILE__, "map_fd.rb"), args: [child_fd.to_s])
+ pid = Process.spawn(*args, { child_fd => @io })
+ Process.waitpid pid
+ @io.rewind
+
+ @io.read.should == "writing to fd: #{child_fd}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/bit_and_spec.rb b/spec/ruby/core/process/status/bit_and_spec.rb
new file mode 100644
index 0000000000..a5b1123e90
--- /dev/null
+++ b/spec/ruby/core/process/status/bit_and_spec.rb
@@ -0,0 +1,38 @@
+require_relative '../../../spec_helper'
+
+ruby_version_is ""..."4.0" do
+
+ describe "Process::Status#&" do
+ it "returns a bitwise and of the integer status of an exited child" do
+ suppress_warning do
+ ruby_exe("exit(29)", exit_status: 29)
+ ($? & 0).should == 0
+ ($? & $?.to_i).should == $?.to_i
+
+ # Actual value is implementation specific
+ platform_is :linux do
+ # 29 == 0b11101
+ ($? & 0b1011100000000).should == 0b1010100000000
+ end
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "raises an ArgumentError if mask is negative" do
+ suppress_warning do
+ ruby_exe("exit(0)")
+ -> {
+ $? & -1
+ }.should.raise(ArgumentError, 'negative mask value: -1')
+ end
+ end
+
+ it "shows a deprecation warning" do
+ ruby_exe("exit(0)")
+ -> {
+ $? & 0
+ }.should complain(/warning: Process::Status#& is deprecated and will be removed .*use other Process::Status predicates instead/)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/coredump_spec.rb b/spec/ruby/core/process/status/coredump_spec.rb
new file mode 100644
index 0000000000..fbbaf926f7
--- /dev/null
+++ b/spec/ruby/core/process/status/coredump_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#coredump?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/status/equal_value_spec.rb b/spec/ruby/core/process/status/equal_value_spec.rb
new file mode 100644
index 0000000000..d8a2be26b8
--- /dev/null
+++ b/spec/ruby/core/process/status/equal_value_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#==" do
+ it "returns true when compared to the integer status of an exited child" do
+ ruby_exe("exit(29)", exit_status: 29)
+ $?.to_i.should == $?
+ $?.should == $?.to_i
+ end
+
+ it "returns true when compared to the integer status of a terminated child" do
+ ruby_exe("Process.kill(:KILL, $$); exit(29)", exit_status: platform_is(:windows) ? 0 : :SIGKILL)
+ $?.to_i.should == $?
+ $?.should == $?.to_i
+ end
+end
diff --git a/spec/ruby/core/process/status/exited_spec.rb b/spec/ruby/core/process/status/exited_spec.rb
new file mode 100644
index 0000000000..ad14b35000
--- /dev/null
+++ b/spec/ruby/core/process/status/exited_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#exited?" do
+ describe "for a child that exited normally" do
+ before :each do
+ ruby_exe("exit(0)")
+ end
+
+ it "returns true" do
+ $?.exited?.should == true
+ end
+ end
+
+
+ describe "for a terminated child" do
+ before :each do
+ ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL)
+ end
+
+ platform_is_not :windows do
+ it "returns false" do
+ $?.exited?.should == false
+ end
+ end
+
+ platform_is :windows do
+ it "always returns true" do
+ $?.exited?.should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/exitstatus_spec.rb b/spec/ruby/core/process/status/exitstatus_spec.rb
new file mode 100644
index 0000000000..5c86c2b3c8
--- /dev/null
+++ b/spec/ruby/core/process/status/exitstatus_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#exitstatus" do
+ before :each do
+ ruby_exe("exit(42)", exit_status: 42)
+ end
+
+ it "returns the process exit code" do
+ $?.exitstatus.should == 42
+ end
+
+ describe "for a child that raised SignalException" do
+ before :each do
+ ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL)
+ end
+
+ platform_is_not :windows do
+ # The exitstatus is not set in these cases. See the termsig_spec
+ # for info on where the signal number (SIGTERM) is available.
+ it "returns nil" do
+ $?.exitstatus.should == nil
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/inspect_spec.rb b/spec/ruby/core/process/status/inspect_spec.rb
new file mode 100644
index 0000000000..03f0479f2c
--- /dev/null
+++ b/spec/ruby/core/process/status/inspect_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#inspect" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/status/pid_spec.rb b/spec/ruby/core/process/status/pid_spec.rb
new file mode 100644
index 0000000000..9965fc3bdf
--- /dev/null
+++ b/spec/ruby/core/process/status/pid_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../../spec_helper'
+
+platform_is_not :windows do
+ describe "Process::Status#pid" do
+
+ before :each do
+ @pid = ruby_exe("print $$").to_i
+ end
+
+ it "returns the pid of the process" do
+ $?.pid.should == @pid
+ end
+
+ end
+end
diff --git a/spec/ruby/core/process/status/right_shift_spec.rb b/spec/ruby/core/process/status/right_shift_spec.rb
new file mode 100644
index 0000000000..5689526f54
--- /dev/null
+++ b/spec/ruby/core/process/status/right_shift_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../../spec_helper'
+
+ruby_version_is ""..."4.0" do
+
+ describe "Process::Status#>>" do
+ it "returns a right shift of the integer status of an exited child" do
+ suppress_warning do
+ ruby_exe("exit(29)", exit_status: 29)
+ ($? >> 0).should == $?.to_i
+ ($? >> 1).should == $?.to_i >> 1
+
+ # Actual value is implementation specific
+ platform_is :linux do
+ ($? >> 8).should == 29
+ end
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "raises an ArgumentError if shift value is negative" do
+ suppress_warning do
+ ruby_exe("exit(0)")
+ -> {
+ $? >> -1
+ }.should.raise(ArgumentError, 'negative shift value: -1')
+ end
+ end
+
+ it "shows a deprecation warning" do
+ ruby_exe("exit(0)")
+ -> {
+ $? >> 0
+ }.should complain(/warning: Process::Status#>> is deprecated and will be removed .*use other Process::Status attributes instead/)
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/signaled_spec.rb b/spec/ruby/core/process/status/signaled_spec.rb
new file mode 100644
index 0000000000..8cf409bb42
--- /dev/null
+++ b/spec/ruby/core/process/status/signaled_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#signaled?" do
+ describe "for a cleanly exited child" do
+ before :each do
+ ruby_exe("exit(0)")
+ end
+
+ it "returns false" do
+ $?.signaled?.should == false
+ end
+ end
+
+ describe "for a terminated child" do
+ before :each do
+ ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL)
+ end
+
+ platform_is_not :windows do
+ it "returns true" do
+ $?.signaled?.should == true
+ end
+ end
+
+ platform_is :windows do
+ it "always returns false" do
+ $?.signaled?.should == false
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/stopped_spec.rb b/spec/ruby/core/process/status/stopped_spec.rb
new file mode 100644
index 0000000000..bebd441d6f
--- /dev/null
+++ b/spec/ruby/core/process/status/stopped_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#stopped?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/status/stopsig_spec.rb b/spec/ruby/core/process/status/stopsig_spec.rb
new file mode 100644
index 0000000000..b2a7c5d9e2
--- /dev/null
+++ b/spec/ruby/core/process/status/stopsig_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#stopsig" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/status/success_spec.rb b/spec/ruby/core/process/status/success_spec.rb
new file mode 100644
index 0000000000..f61243c667
--- /dev/null
+++ b/spec/ruby/core/process/status/success_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#success?" do
+ describe "for a child that exited normally" do
+ before :each do
+ ruby_exe("exit(0)")
+ end
+
+ it "returns true" do
+ $?.success?.should == true
+ end
+ end
+
+ describe "for a child that exited with a non zero status" do
+ before :each do
+ ruby_exe("exit(42)", exit_status: 42)
+ end
+
+ it "returns false" do
+ $?.success?.should == false
+ end
+ end
+
+ describe "for a child that was terminated" do
+ before :each do
+ ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL)
+ end
+
+ platform_is_not :windows do
+ it "returns nil" do
+ $?.success?.should == nil
+ end
+ end
+
+ platform_is :windows do
+ it "always returns true" do
+ $?.success?.should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/termsig_spec.rb b/spec/ruby/core/process/status/termsig_spec.rb
new file mode 100644
index 0000000000..1d57724d12
--- /dev/null
+++ b/spec/ruby/core/process/status/termsig_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#termsig" do
+ describe "for a child that exited normally" do
+ before :each do
+ ruby_exe("exit(0)")
+ end
+
+ it "returns nil" do
+ $?.termsig.should == nil
+ end
+ end
+
+ describe "for a child that raised SignalException" do
+ before :each do
+ ruby_exe("raise SignalException, 'SIGTERM'", exit_status: :SIGTERM)
+ end
+
+ platform_is_not :windows do
+ it "returns the signal" do
+ $?.termsig.should == Signal.list["TERM"]
+ end
+ end
+ end
+
+ describe "for a child that was sent a signal" do
+ before :each do
+ ruby_exe("Process.kill(:KILL, $$); exit(42)", exit_status: platform_is(:windows) ? 0 : :SIGKILL)
+ end
+
+ platform_is_not :windows do
+ it "returns the signal" do
+ $?.termsig.should == Signal.list["KILL"]
+ end
+ end
+
+ platform_is :windows do
+ it "always returns nil" do
+ $?.termsig.should == nil
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/status/to_i_spec.rb b/spec/ruby/core/process/status/to_i_spec.rb
new file mode 100644
index 0000000000..0bfb883d23
--- /dev/null
+++ b/spec/ruby/core/process/status/to_i_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#to_i" do
+ it "returns an integer when the child exits" do
+ ruby_exe('exit 48', exit_status: 48)
+ $?.to_i.should.instance_of?(Integer)
+ end
+
+ it "returns an integer when the child is signaled" do
+ ruby_exe('raise SignalException, "TERM"', exit_status: platform_is(:windows) ? 3 : :SIGTERM)
+ $?.to_i.should.instance_of?(Integer)
+ end
+end
diff --git a/spec/ruby/core/process/status/to_int_spec.rb b/spec/ruby/core/process/status/to_int_spec.rb
new file mode 100644
index 0000000000..fb596c1bfb
--- /dev/null
+++ b/spec/ruby/core/process/status/to_int_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#to_int" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/status/to_s_spec.rb b/spec/ruby/core/process/status/to_s_spec.rb
new file mode 100644
index 0000000000..5c0d4cd30c
--- /dev/null
+++ b/spec/ruby/core/process/status/to_s_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Status#to_s" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/status/wait_spec.rb b/spec/ruby/core/process/status/wait_spec.rb
new file mode 100644
index 0000000000..18ecc14f6f
--- /dev/null
+++ b/spec/ruby/core/process/status/wait_spec.rb
@@ -0,0 +1,100 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/common'
+
+describe "Process::Status.wait" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :all do
+ begin
+ leaked = Process.waitall
+ # Ruby-space should not see PIDs used by rjit
+ raise "subprocesses leaked before wait specs: #{leaked}" unless leaked.empty?
+ rescue NotImplementedError
+ end
+ end
+
+ it "returns a status with pid -1 if there are no child processes" do
+ Process::Status.wait.pid.should == -1
+ end
+
+ platform_is_not :windows do
+ it "returns a status with its child pid" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ status = Process::Status.wait
+ status.should.instance_of?(Process::Status)
+ status.pid.should == pid
+ end
+
+ it "should not set $? to the Process::Status" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ status = Process::Status.wait
+ $?.should_not.equal?(status)
+ end
+
+ it "should not change the value of $?" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process.wait
+ status = $?
+ Process::Status.wait
+ status.should.equal?($?)
+ end
+
+ it "waits for any child process if no pid is given" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process::Status.wait.pid.should == pid
+ -> { Process.kill(0, pid) }.should.raise(Errno::ESRCH)
+ end
+
+ it "waits for a specific child if a pid is given" do
+ pid1 = Process.spawn(ruby_cmd('exit'))
+ pid2 = Process.spawn(ruby_cmd('exit'))
+ Process::Status.wait(pid2).pid.should == pid2
+ Process::Status.wait(pid1).pid.should == pid1
+ -> { Process.kill(0, pid1) }.should.raise(Errno::ESRCH)
+ -> { Process.kill(0, pid2) }.should.raise(Errno::ESRCH)
+ end
+
+ it "coerces the pid to an Integer" do
+ pid1 = Process.spawn(ruby_cmd('exit'))
+ Process::Status.wait(mock_int(pid1)).pid.should == pid1
+ -> { Process.kill(0, pid1) }.should.raise(Errno::ESRCH)
+ end
+
+ # This spec is probably system-dependent.
+ it "waits for a child whose process group ID is that of the calling process" do
+ pid1 = Process.spawn(ruby_cmd('exit'), pgroup: true)
+ pid2 = Process.spawn(ruby_cmd('exit'))
+
+ Process::Status.wait(0).pid.should == pid2
+ Process::Status.wait.pid.should == pid1
+ end
+
+ # This spec is probably system-dependent.
+ it "doesn't block if no child is available when WNOHANG is used" do
+ read, write = IO.pipe
+ pid = Process.fork do
+ read.close
+ Signal.trap("TERM") { Process.exit! }
+ write << 1
+ write.close
+ sleep
+ end
+
+ Process::Status.wait(pid, Process::WNOHANG).should == nil
+
+ # wait for the child to setup its TERM handler
+ write.close
+ read.read(1)
+ read.close
+
+ Process.kill("TERM", pid)
+ Process::Status.wait.pid.should == pid
+ end
+
+ it "always accepts flags=0" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process::Status.wait(-1, 0).pid.should == pid
+ -> { Process.kill(0, pid) }.should.raise(Errno::ESRCH)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/sys/getegid_spec.rb b/spec/ruby/core/process/sys/getegid_spec.rb
new file mode 100644
index 0000000000..5b8588f147
--- /dev/null
+++ b/spec/ruby/core/process/sys/getegid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.getegid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/geteuid_spec.rb b/spec/ruby/core/process/sys/geteuid_spec.rb
new file mode 100644
index 0000000000..0ce7fc5459
--- /dev/null
+++ b/spec/ruby/core/process/sys/geteuid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.geteuid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/getgid_spec.rb b/spec/ruby/core/process/sys/getgid_spec.rb
new file mode 100644
index 0000000000..05c0fd4c0b
--- /dev/null
+++ b/spec/ruby/core/process/sys/getgid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.getgid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/getuid_spec.rb b/spec/ruby/core/process/sys/getuid_spec.rb
new file mode 100644
index 0000000000..56bcef01cc
--- /dev/null
+++ b/spec/ruby/core/process/sys/getuid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.getuid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/issetugid_spec.rb b/spec/ruby/core/process/sys/issetugid_spec.rb
new file mode 100644
index 0000000000..4fc7dc3bc6
--- /dev/null
+++ b/spec/ruby/core/process/sys/issetugid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.issetugid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setegid_spec.rb b/spec/ruby/core/process/sys/setegid_spec.rb
new file mode 100644
index 0000000000..8f20330b94
--- /dev/null
+++ b/spec/ruby/core/process/sys/setegid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setegid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/seteuid_spec.rb b/spec/ruby/core/process/sys/seteuid_spec.rb
new file mode 100644
index 0000000000..57d3a4800d
--- /dev/null
+++ b/spec/ruby/core/process/sys/seteuid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.seteuid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setgid_spec.rb b/spec/ruby/core/process/sys/setgid_spec.rb
new file mode 100644
index 0000000000..cc712e0102
--- /dev/null
+++ b/spec/ruby/core/process/sys/setgid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setgid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setregid_spec.rb b/spec/ruby/core/process/sys/setregid_spec.rb
new file mode 100644
index 0000000000..57d491a707
--- /dev/null
+++ b/spec/ruby/core/process/sys/setregid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setregid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setresgid_spec.rb b/spec/ruby/core/process/sys/setresgid_spec.rb
new file mode 100644
index 0000000000..9be06612c1
--- /dev/null
+++ b/spec/ruby/core/process/sys/setresgid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setresgid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setresuid_spec.rb b/spec/ruby/core/process/sys/setresuid_spec.rb
new file mode 100644
index 0000000000..1092b349cb
--- /dev/null
+++ b/spec/ruby/core/process/sys/setresuid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setresuid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setreuid_spec.rb b/spec/ruby/core/process/sys/setreuid_spec.rb
new file mode 100644
index 0000000000..f3451c63c8
--- /dev/null
+++ b/spec/ruby/core/process/sys/setreuid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setreuid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setrgid_spec.rb b/spec/ruby/core/process/sys/setrgid_spec.rb
new file mode 100644
index 0000000000..27eea2ed86
--- /dev/null
+++ b/spec/ruby/core/process/sys/setrgid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setrgid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setruid_spec.rb b/spec/ruby/core/process/sys/setruid_spec.rb
new file mode 100644
index 0000000000..9dbd84bf68
--- /dev/null
+++ b/spec/ruby/core/process/sys/setruid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setruid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/sys/setuid_spec.rb b/spec/ruby/core/process/sys/setuid_spec.rb
new file mode 100644
index 0000000000..e06c3588a5
--- /dev/null
+++ b/spec/ruby/core/process/sys/setuid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Sys.setuid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/times_spec.rb b/spec/ruby/core/process/times_spec.rb
new file mode 100644
index 0000000000..a7ffbb79e5
--- /dev/null
+++ b/spec/ruby/core/process/times_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Process.times" do
+ it "returns a Process::Tms" do
+ Process.times.should.is_a?(Process::Tms)
+ end
+
+ # TODO: Intel C Compiler does not work this example
+ # http://rubyci.s3.amazonaws.com/icc-x64/ruby-master/log/20221013T030005Z.fail.html.gz
+ unless RbConfig::CONFIG['CC']&.include?("icx")
+ it "returns current cpu times" do
+ t = Process.times
+ user = t.utime
+
+ 1 until Process.times.utime > user
+ Process.times.utime.should > user
+ end
+ end
+end
diff --git a/spec/ruby/core/process/tms/cstime_spec.rb b/spec/ruby/core/process/tms/cstime_spec.rb
new file mode 100644
index 0000000000..9c2d9e8632
--- /dev/null
+++ b/spec/ruby/core/process/tms/cstime_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Tms#cstime" do
+ it "returns cstime attribute" do
+ cstime = Object.new
+ Process::Tms.new(nil, nil, nil, cstime).cstime.should == cstime
+ end
+end
+
+describe "Process::Tms#cstime=" do
+ it "assigns a value to the cstime attribute" do
+ cstime = Object.new
+ tms = Process::Tms.new
+ tms.cstime = cstime
+ tms.cstime.should == cstime
+ end
+end
diff --git a/spec/ruby/core/process/tms/cutime_spec.rb b/spec/ruby/core/process/tms/cutime_spec.rb
new file mode 100644
index 0000000000..0ac3ff1964
--- /dev/null
+++ b/spec/ruby/core/process/tms/cutime_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Tms#cutime" do
+ it "returns cutime attribute" do
+ cutime = Object.new
+ Process::Tms.new(nil, nil, cutime, nil).cutime.should == cutime
+ end
+end
+
+describe "Process::Tms#cutime=" do
+ it "assigns a value to the cutime attribute" do
+ cutime = Object.new
+ tms = Process::Tms.new
+ tms.cutime = cutime
+ tms.cutime.should == cutime
+ end
+end
diff --git a/spec/ruby/core/process/tms/stime_spec.rb b/spec/ruby/core/process/tms/stime_spec.rb
new file mode 100644
index 0000000000..1e8371475f
--- /dev/null
+++ b/spec/ruby/core/process/tms/stime_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Tms#stime" do
+ it "returns stime attribute" do
+ stime = Object.new
+ Process::Tms.new(nil, stime, nil, nil).stime.should == stime
+ end
+end
+
+describe "Process::Tms#stime=" do
+ it "assigns a value to the stime attribute" do
+ stime = Object.new
+ tms = Process::Tms.new
+ tms.stime = stime
+ tms.stime.should == stime
+ end
+end
diff --git a/spec/ruby/core/process/tms/utime_spec.rb b/spec/ruby/core/process/tms/utime_spec.rb
new file mode 100644
index 0000000000..403a31e2e6
--- /dev/null
+++ b/spec/ruby/core/process/tms/utime_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../../spec_helper'
+
+describe "Process::Tms#utime" do
+ it "returns utime attribute" do
+ utime = Object.new
+ Process::Tms.new(utime, nil, nil, nil).utime.should == utime
+ end
+end
+
+describe "Process::Tms#utime=" do
+ it "assigns a value to the ctime attribute" do
+ utime = Object.new
+ tms = Process::Tms.new
+ tms.utime = utime
+ tms.utime.should == utime
+ end
+end
diff --git a/spec/ruby/core/process/uid/change_privilege_spec.rb b/spec/ruby/core/process/uid/change_privilege_spec.rb
new file mode 100644
index 0000000000..e4b552dd94
--- /dev/null
+++ b/spec/ruby/core/process/uid/change_privilege_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.change_privilege" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/eid_spec.rb b/spec/ruby/core/process/uid/eid_spec.rb
new file mode 100644
index 0000000000..f0bb9ce762
--- /dev/null
+++ b/spec/ruby/core/process/uid/eid_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.eid" do
+ it "needs to be reviewed for spec completeness"
+end
+
+describe "Process::UID.eid=" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/grant_privilege_spec.rb b/spec/ruby/core/process/uid/grant_privilege_spec.rb
new file mode 100644
index 0000000000..2b8a5c9102
--- /dev/null
+++ b/spec/ruby/core/process/uid/grant_privilege_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.grant_privilege" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/re_exchange_spec.rb b/spec/ruby/core/process/uid/re_exchange_spec.rb
new file mode 100644
index 0000000000..c0f10f33c4
--- /dev/null
+++ b/spec/ruby/core/process/uid/re_exchange_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.re_exchange" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/re_exchangeable_spec.rb b/spec/ruby/core/process/uid/re_exchangeable_spec.rb
new file mode 100644
index 0000000000..8200d7ecb7
--- /dev/null
+++ b/spec/ruby/core/process/uid/re_exchangeable_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.re_exchangeable?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/rid_spec.rb b/spec/ruby/core/process/uid/rid_spec.rb
new file mode 100644
index 0000000000..e865cbfef6
--- /dev/null
+++ b/spec/ruby/core/process/uid/rid_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.rid" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/sid_available_spec.rb b/spec/ruby/core/process/uid/sid_available_spec.rb
new file mode 100644
index 0000000000..be7912eb68
--- /dev/null
+++ b/spec/ruby/core/process/uid/sid_available_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.sid_available?" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid/switch_spec.rb b/spec/ruby/core/process/uid/switch_spec.rb
new file mode 100644
index 0000000000..4191b97db4
--- /dev/null
+++ b/spec/ruby/core/process/uid/switch_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../../spec_helper'
+
+describe "Process::UID.switch" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/uid_spec.rb b/spec/ruby/core/process/uid_spec.rb
new file mode 100644
index 0000000000..1e218ef4fe
--- /dev/null
+++ b/spec/ruby/core/process/uid_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+
+describe "Process.uid" do
+ platform_is_not :windows do
+ it "returns the correct uid for the user executing this process" do
+ current_uid_according_to_unix = `id -ur`.to_i
+ Process.uid.should == current_uid_according_to_unix
+ end
+ end
+
+ it "also goes by Process::UID.rid" do
+ Process::UID.rid.should == Process.uid
+ end
+
+ it "also goes by Process::Sys.getuid" do
+ Process::Sys.getuid.should == Process.uid
+ end
+end
+
+describe "Process.uid=" do
+ platform_is_not :windows do
+ it "raises TypeError if not passed an Integer" do
+ -> { Process.uid = Object.new }.should.raise(TypeError)
+ end
+
+ as_user do
+ it "raises Errno::ERPERM if run by a non privileged user trying to set the superuser id" do
+ -> { (Process.uid = 0)}.should.raise(Errno::EPERM)
+ end
+
+ it "raises Errno::ERPERM if run by a non privileged user trying to set the superuser id from username" do
+ -> { Process.uid = "root" }.should.raise(Errno::EPERM)
+ end
+ end
+
+ as_superuser do
+ describe "if run by a superuser" do
+ it "sets the real user id for the current process" do
+ code = <<-RUBY
+ Process.uid = 1
+ puts Process.uid
+ RUBY
+ ruby_exe(code).should == "1\n"
+ end
+
+ it "sets the real user id if preceded by Process.euid=id" do
+ code = <<-RUBY
+ Process.euid = 1
+ Process.uid = 1
+ puts Process.uid
+ RUBY
+ ruby_exe(code).should == "1\n"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/process/wait2_spec.rb b/spec/ruby/core/process/wait2_spec.rb
new file mode 100644
index 0000000000..5c57dd40fb
--- /dev/null
+++ b/spec/ruby/core/process/wait2_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+
+describe "Process.wait2" do
+ before :all do
+ # HACK: this kludge is temporarily necessary because some
+ # misbehaving spec somewhere else does not clear processes
+ # Note: background processes are unavoidable with RJIT,
+ # but we shouldn't reap them from Ruby-space
+ begin
+ Process.wait(-1, Process::WNOHANG)
+ $stderr.puts "Leaked process before wait2 specs! Waiting for it"
+ leaked = Process.waitall
+ $stderr.puts "leaked before wait2 specs: #{leaked}" unless leaked.empty?
+ # Ruby-space should not see PIDs used by rjit
+ leaked.should.empty?
+ rescue Errno::ECHILD # No child processes
+ rescue NotImplementedError
+ end
+ end
+
+ platform_is_not :windows do
+ it "returns the pid and status of child process" do
+ pidf = Process.fork { Process.exit! 99 }
+ results = Process.wait2
+ results.size.should == 2
+ pidw, status = results
+ pidf.should == pidw
+ status.exitstatus.should == 99
+ end
+ end
+
+ it "raises a StandardError if no child processes exist" do
+ -> { Process.wait2 }.should.raise(Errno::ECHILD)
+ -> { Process.wait2 }.should.raise(StandardError)
+ end
+
+ it "returns nil if the child process is still running when given the WNOHANG flag" do
+ IO.popen(ruby_cmd('STDIN.getbyte'), "w") do |io|
+ pid, status = Process.wait2(io.pid, Process::WNOHANG)
+ pid.should == nil
+ status.should == nil
+ io.write('a')
+ end
+ end
+end
diff --git a/spec/ruby/core/process/wait_spec.rb b/spec/ruby/core/process/wait_spec.rb
new file mode 100644
index 0000000000..0b2e715f65
--- /dev/null
+++ b/spec/ruby/core/process/wait_spec.rb
@@ -0,0 +1,91 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/common'
+
+describe "Process.wait" do
+ ProcessSpecs.use_system_ruby(self)
+
+ before :all do
+ begin
+ leaked = Process.waitall
+ # Ruby-space should not see PIDs used by rjit
+ raise "subprocesses leaked before wait specs: #{leaked}" unless leaked.empty?
+ rescue NotImplementedError
+ end
+ end
+
+ it "raises an Errno::ECHILD if there are no child processes" do
+ -> { Process.wait }.should.raise(Errno::ECHILD)
+ end
+
+ platform_is_not :windows do
+ it "returns its child pid" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process.wait.should == pid
+ end
+
+ it "sets $? to a Process::Status" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process.wait
+ $?.should.is_a?(Process::Status)
+ $?.pid.should == pid
+ end
+
+ it "waits for any child process if no pid is given" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process.wait.should == pid
+ -> { Process.kill(0, pid) }.should.raise(Errno::ESRCH)
+ end
+
+ it "waits for a specific child if a pid is given" do
+ pid1 = Process.spawn(ruby_cmd('exit'))
+ pid2 = Process.spawn(ruby_cmd('exit'))
+ Process.wait(pid2).should == pid2
+ Process.wait(pid1).should == pid1
+ -> { Process.kill(0, pid1) }.should.raise(Errno::ESRCH)
+ -> { Process.kill(0, pid2) }.should.raise(Errno::ESRCH)
+ end
+
+ it "coerces the pid to an Integer" do
+ pid1 = Process.spawn(ruby_cmd('exit'))
+ Process.wait(mock_int(pid1)).should == pid1
+ -> { Process.kill(0, pid1) }.should.raise(Errno::ESRCH)
+ end
+
+ # This spec is probably system-dependent.
+ it "waits for a child whose process group ID is that of the calling process" do
+ pid1 = Process.spawn(ruby_cmd('exit'), pgroup: true)
+ pid2 = Process.spawn(ruby_cmd('exit'))
+
+ Process.wait(0).should == pid2
+ Process.wait.should == pid1
+ end
+
+ # This spec is probably system-dependent.
+ it "doesn't block if no child is available when WNOHANG is used" do
+ read, write = IO.pipe
+ pid = Process.fork do
+ read.close
+ Signal.trap("TERM") { Process.exit! }
+ write << 1
+ write.close
+ sleep
+ end
+
+ Process.wait(pid, Process::WNOHANG).should == nil
+
+ # wait for the child to setup its TERM handler
+ write.close
+ read.read(1)
+ read.close
+
+ Process.kill("TERM", pid)
+ Process.wait.should == pid
+ end
+
+ it "always accepts flags=0" do
+ pid = Process.spawn(ruby_cmd('exit'))
+ Process.wait(-1, 0).should == pid
+ -> { Process.kill(0, pid) }.should.raise(Errno::ESRCH)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/waitall_spec.rb b/spec/ruby/core/process/waitall_spec.rb
new file mode 100644
index 0000000000..a77873f553
--- /dev/null
+++ b/spec/ruby/core/process/waitall_spec.rb
@@ -0,0 +1,48 @@
+require_relative '../../spec_helper'
+
+describe "Process.waitall" do
+ before :all do
+ begin
+ Process.waitall
+ rescue NotImplementedError
+ end
+ end
+
+ it "returns an empty array when there are no children" do
+ Process.waitall.should == []
+ end
+
+ it "takes no arguments" do
+ -> { Process.waitall(0) }.should.raise(ArgumentError)
+ end
+
+ platform_is_not :windows do
+ it "waits for all children" do
+ pids = []
+ pids << Process.fork { Process.exit! 2 }
+ pids << Process.fork { Process.exit! 1 }
+ pids << Process.fork { Process.exit! 0 }
+ Process.waitall
+ pids.each { |pid|
+ -> { Process.kill(0, pid) }.should.raise(Errno::ESRCH)
+ }
+ end
+
+ it "returns an array of pid/status pairs" do
+ pids = []
+ pids << Process.fork { Process.exit! 2 }
+ pids << Process.fork { Process.exit! 1 }
+ pids << Process.fork { Process.exit! 0 }
+ a = Process.waitall
+ a.should.is_a?(Array)
+ a.size.should == 3
+ pids.each { |pid|
+ pid_status = a.assoc(pid)
+ pid_status.should.is_a?(Array)
+ pid_status.size.should == 2
+ pid_status.first.should == pid
+ pid_status.last.should.is_a?(Process::Status)
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/process/waitpid2_spec.rb b/spec/ruby/core/process/waitpid2_spec.rb
new file mode 100644
index 0000000000..45513af667
--- /dev/null
+++ b/spec/ruby/core/process/waitpid2_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Process.waitpid2" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/process/waitpid_spec.rb b/spec/ruby/core/process/waitpid_spec.rb
new file mode 100644
index 0000000000..a02147b663
--- /dev/null
+++ b/spec/ruby/core/process/waitpid_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "Process.waitpid" do
+ it "returns nil when the process has not yet completed and WNOHANG is specified" do
+ cmd = platform_is(:windows) ? "timeout" : "sleep"
+ pid = spawn("#{cmd} 5")
+ begin
+ Process.waitpid(pid, Process::WNOHANG).should == nil
+ Process.kill("KILL", pid)
+ ensure
+ Process.wait(pid)
+ end
+ end
+end
diff --git a/spec/ruby/core/process/warmup_spec.rb b/spec/ruby/core/process/warmup_spec.rb
new file mode 100644
index 0000000000..4530ae222c
--- /dev/null
+++ b/spec/ruby/core/process/warmup_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Process.warmup" do
+ # The behavior is entirely implementation specific.
+ # Other implementations are free to just make it a noop
+ it "is implemented" do
+ Process.warmup.should == true
+ end
+end
diff --git a/spec/ruby/core/queue/append_spec.rb b/spec/ruby/core/queue/append_spec.rb
new file mode 100644
index 0000000000..34165a7506
--- /dev/null
+++ b/spec/ruby/core/queue/append_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/enque'
+
+describe "Queue#<<" do
+ it_behaves_like :queue_enq, :<<, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/clear_spec.rb b/spec/ruby/core/queue/clear_spec.rb
new file mode 100644
index 0000000000..3245e4cb83
--- /dev/null
+++ b/spec/ruby/core/queue/clear_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/clear'
+
+describe "Queue#clear" do
+ it_behaves_like :queue_clear, :clear, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/close_spec.rb b/spec/ruby/core/queue/close_spec.rb
new file mode 100644
index 0000000000..c0d774cd74
--- /dev/null
+++ b/spec/ruby/core/queue/close_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/close'
+
+describe "Queue#close" do
+ it_behaves_like :queue_close, :close, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/closed_spec.rb b/spec/ruby/core/queue/closed_spec.rb
new file mode 100644
index 0000000000..10d552996d
--- /dev/null
+++ b/spec/ruby/core/queue/closed_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/closed'
+
+describe "Queue#closed?" do
+ it_behaves_like :queue_closed?, :closed?, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/deq_spec.rb b/spec/ruby/core/queue/deq_spec.rb
new file mode 100644
index 0000000000..a2784e6a63
--- /dev/null
+++ b/spec/ruby/core/queue/deq_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/deque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "Queue#deq" do
+ it_behaves_like :queue_deq, :deq, -> { Queue.new }
+end
+
+describe "Queue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.deq(timeout: v) }
+end
diff --git a/spec/ruby/core/queue/empty_spec.rb b/spec/ruby/core/queue/empty_spec.rb
new file mode 100644
index 0000000000..55ca777466
--- /dev/null
+++ b/spec/ruby/core/queue/empty_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/empty'
+
+describe "Queue#empty?" do
+ it_behaves_like :queue_empty?, :empty?, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/enq_spec.rb b/spec/ruby/core/queue/enq_spec.rb
new file mode 100644
index 0000000000..c69c496fbc
--- /dev/null
+++ b/spec/ruby/core/queue/enq_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/enque'
+
+describe "Queue#enq" do
+ it_behaves_like :queue_enq, :enq, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/freeze_spec.rb b/spec/ruby/core/queue/freeze_spec.rb
new file mode 100644
index 0000000000..ced2cc52dd
--- /dev/null
+++ b/spec/ruby/core/queue/freeze_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/freeze'
+
+describe "Queue#freeze" do
+ it_behaves_like :queue_freeze, :freeze, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/initialize_spec.rb b/spec/ruby/core/queue/initialize_spec.rb
new file mode 100644
index 0000000000..080e4d0abd
--- /dev/null
+++ b/spec/ruby/core/queue/initialize_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "Queue#initialize" do
+ it "can be passed no arguments for an empty Queue" do
+ q = Queue.new
+ q.size.should == 0
+ q.should.empty?
+ end
+
+ it "is a private method" do
+ Queue.private_instance_methods.include?(:initialize).should == true
+ end
+
+ it "adds all elements of the passed Enumerable to self" do
+ q = Queue.new([1, 2, 3])
+ q.size.should == 3
+ q.should_not.empty?
+ q.pop.should == 1
+ q.pop.should == 2
+ q.pop.should == 3
+ q.should.empty?
+ end
+
+ describe "converts the given argument to an Array using #to_a" do
+ it "uses #to_a on the provided Enumerable" do
+ enumerable = MockObject.new('mock-enumerable')
+ enumerable.should_receive(:to_a).and_return([1, 2, 3])
+ q = Queue.new(enumerable)
+ q.size.should == 3
+ q.should_not.empty?
+ q.pop.should == 1
+ q.pop.should == 2
+ q.pop.should == 3
+ q.should.empty?
+ end
+
+ it "raises a TypeError if the given argument can't be converted to an Array" do
+ -> { Queue.new(42) }.should.raise(TypeError)
+ -> { Queue.new(:abc) }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to an Array" do
+ enumerable = MockObject.new('mock-enumerable')
+ enumerable.should_receive(:to_a).and_raise(NoMethodError)
+ -> { Queue.new(enumerable) }.should.raise(NoMethodError)
+ end
+ end
+
+ it "raises TypeError if the provided Enumerable does not respond to #to_a" do
+ enumerable = MockObject.new('mock-enumerable')
+ -> { Queue.new(enumerable) }.should.raise(TypeError, "can't convert MockObject into Array")
+ end
+
+ it "raises TypeError if #to_a does not return Array" do
+ enumerable = MockObject.new('mock-enumerable')
+ enumerable.should_receive(:to_a).and_return("string")
+
+ -> { Queue.new(enumerable) }.should raise_consistent_error(TypeError, "can't convert MockObject into Array (MockObject#to_a gives String)")
+ end
+end
diff --git a/spec/ruby/core/queue/length_spec.rb b/spec/ruby/core/queue/length_spec.rb
new file mode 100644
index 0000000000..25399b2b76
--- /dev/null
+++ b/spec/ruby/core/queue/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/length'
+
+describe "Queue#length" do
+ it_behaves_like :queue_length, :length, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/num_waiting_spec.rb b/spec/ruby/core/queue/num_waiting_spec.rb
new file mode 100644
index 0000000000..edc0c37a82
--- /dev/null
+++ b/spec/ruby/core/queue/num_waiting_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/num_waiting'
+
+describe "Queue#num_waiting" do
+ it_behaves_like :queue_num_waiting, :num_waiting, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/pop_spec.rb b/spec/ruby/core/queue/pop_spec.rb
new file mode 100644
index 0000000000..3dff7db242
--- /dev/null
+++ b/spec/ruby/core/queue/pop_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/deque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "Queue#pop" do
+ it_behaves_like :queue_deq, :pop, -> { Queue.new }
+end
+
+describe "Queue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.pop(timeout: v) }
+end
diff --git a/spec/ruby/core/queue/push_spec.rb b/spec/ruby/core/queue/push_spec.rb
new file mode 100644
index 0000000000..e936f9d282
--- /dev/null
+++ b/spec/ruby/core/queue/push_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/enque'
+
+describe "Queue#push" do
+ it_behaves_like :queue_enq, :push, -> { Queue.new }
+end
diff --git a/spec/ruby/core/queue/shift_spec.rb b/spec/ruby/core/queue/shift_spec.rb
new file mode 100644
index 0000000000..c105da74b2
--- /dev/null
+++ b/spec/ruby/core/queue/shift_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/deque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "Queue#shift" do
+ it_behaves_like :queue_deq, :shift, -> { Queue.new }
+end
+
+describe "Queue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = Queue.new; q.push(1); q.shift(timeout: v) }
+end
diff --git a/spec/ruby/core/queue/size_spec.rb b/spec/ruby/core/queue/size_spec.rb
new file mode 100644
index 0000000000..f528dfe797
--- /dev/null
+++ b/spec/ruby/core/queue/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/length'
+
+describe "Queue#size" do
+ it_behaves_like :queue_length, :size, -> { Queue.new }
+end
diff --git a/spec/ruby/core/random/bytes_spec.rb b/spec/ruby/core/random/bytes_spec.rb
new file mode 100644
index 0000000000..c9be07cd3f
--- /dev/null
+++ b/spec/ruby/core/random/bytes_spec.rb
@@ -0,0 +1,29 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'shared/bytes'
+
+describe "Random#bytes" do
+ it_behaves_like :random_bytes, :bytes, Random.new
+
+ it "returns the same output for a given seed" do
+ Random.new(33).bytes(2).should == Random.new(33).bytes(2)
+ end
+
+ it "returns the same numeric output for a given seed across all implementations and platforms" do
+ rnd = Random.new(33)
+ rnd.bytes(2).should == "\x14\\"
+ rnd.bytes(1000) # skip some
+ rnd.bytes(2).should == "\xA1p"
+ end
+
+ it "returns the same numeric output for a given huge seed across all implementations and platforms" do
+ rnd = Random.new(2 ** (63 * 4))
+ rnd.bytes(2).should == "_\x91"
+ rnd.bytes(1000) # skip some
+ rnd.bytes(2).should == "\x17\x12"
+ end
+end
+
+describe "Random.bytes" do
+ it_behaves_like :random_bytes, :bytes, Random
+end
diff --git a/spec/ruby/core/random/default_spec.rb b/spec/ruby/core/random/default_spec.rb
new file mode 100644
index 0000000000..9e4845986d
--- /dev/null
+++ b/spec/ruby/core/random/default_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Random::DEFAULT" do
+ it "is no longer defined" do
+ Random.should_not.const_defined?(:DEFAULT)
+ end
+end
diff --git a/spec/ruby/core/random/equal_value_spec.rb b/spec/ruby/core/random/equal_value_spec.rb
new file mode 100644
index 0000000000..5f470d6a4c
--- /dev/null
+++ b/spec/ruby/core/random/equal_value_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "Random#==" do
+ it "returns true if the two objects have the same state" do
+ a = Random.new(42)
+ b = Random.new(42)
+ a.send(:state).should == b.send(:state)
+ a.should == b
+ end
+
+ it "returns false if the two objects have different state" do
+ a = Random.new
+ b = Random.new
+ a.send(:state).should_not == b.send(:state)
+ a.should_not == b
+ end
+
+ it "returns true if the two objects have the same seed" do
+ a = Random.new(42)
+ b = Random.new(42.5)
+ a.seed.should == b.seed
+ a.should == b
+ end
+
+ it "returns false if the two objects have a different seed" do
+ a = Random.new(42)
+ b = Random.new(41)
+ a.seed.should_not == b.seed
+ a.should_not == b
+ end
+
+ it "returns false if the other object is not a Random" do
+ a = Random.new(42)
+ a.should_not == 42
+ a.should_not == [a]
+ end
+end
diff --git a/spec/ruby/core/random/fixtures/classes.rb b/spec/ruby/core/random/fixtures/classes.rb
new file mode 100644
index 0000000000..9c8d113e24
--- /dev/null
+++ b/spec/ruby/core/random/fixtures/classes.rb
@@ -0,0 +1,15 @@
+module RandomSpecs
+ CustomRangeInteger = Struct.new(:value) do
+ def to_int; value; end
+ def <=>(other); to_int <=> other.to_int; end
+ def -(other); self.class.new(to_int - other.to_int); end
+ def +(other); self.class.new(to_int + other.to_int); end
+ end
+
+ CustomRangeFloat = Struct.new(:value) do
+ def to_f; value; end
+ def <=>(other); to_f <=> other.to_f; end
+ def -(other); to_f - other.to_f; end
+ def +(other); self.class.new(to_f + other.to_f); end
+ end
+end
diff --git a/spec/ruby/core/random/new_seed_spec.rb b/spec/ruby/core/random/new_seed_spec.rb
new file mode 100644
index 0000000000..b2741aaa31
--- /dev/null
+++ b/spec/ruby/core/random/new_seed_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "Random.new_seed" do
+ it "returns an Integer" do
+ Random.new_seed.should.instance_of?(Integer)
+ end
+
+ it "returns an arbitrary seed value each time" do
+ bigs = 200.times.map { Random.new_seed }
+ bigs.uniq.size.should == 200
+ end
+
+ it "is not affected by Kernel#srand" do
+ begin
+ srand 25
+ a = Random.new_seed
+ srand 25
+ b = Random.new_seed
+ a.should_not == b
+ ensure
+ srand Random.new_seed
+ end
+ end
+end
diff --git a/spec/ruby/core/random/new_spec.rb b/spec/ruby/core/random/new_spec.rb
new file mode 100644
index 0000000000..e20d487137
--- /dev/null
+++ b/spec/ruby/core/random/new_spec.rb
@@ -0,0 +1,38 @@
+require_relative "../../spec_helper"
+describe "Random.new" do
+ it "returns a new instance of Random" do
+ Random.new.should.instance_of?(Random)
+ end
+
+ it "uses a random seed value if none is supplied" do
+ Random.new.seed.should.instance_of?(Integer)
+ end
+
+ it "returns Random instances initialized with different seeds" do
+ first = Random.new
+ second = Random.new
+ (0..20).map { first.rand }.should_not == (0..20).map { second.rand }
+ end
+
+ it "accepts an Integer seed value as an argument" do
+ Random.new(2).seed.should == 2
+ end
+
+ it "accepts (and truncates) a Float seed value as an argument" do
+ Random.new(3.4).seed.should == 3
+ end
+
+ it "accepts (and converts to Integer) a Rational seed value as an argument" do
+ Random.new(Rational(20,2)).seed.should == 10
+ end
+
+ it "accepts (and converts to Integer) a Complex (without imaginary part) seed value as an argument" do
+ Random.new(Complex(20)).seed.should == 20
+ end
+
+ it "raises a RangeError if passed a Complex (with imaginary part) seed value as an argument" do
+ -> do
+ Random.new(Complex(20,2))
+ end.should.raise(RangeError)
+ end
+end
diff --git a/spec/ruby/core/random/rand_spec.rb b/spec/ruby/core/random/rand_spec.rb
new file mode 100644
index 0000000000..c882db6381
--- /dev/null
+++ b/spec/ruby/core/random/rand_spec.rb
@@ -0,0 +1,224 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/rand'
+
+describe "Random.rand" do
+ it_behaves_like :random_number, :rand, Random.new
+ it_behaves_like :random_number, :rand, Random
+
+ it "returns a Float >= 0 if no max argument is passed" do
+ floats = 200.times.map { Random.rand }
+ floats.min.should >= 0
+ end
+
+ it "returns a Float < 1 if no max argument is passed" do
+ floats = 200.times.map { Random.rand }
+ floats.max.should < 1
+ end
+
+ it "returns the same sequence for a given seed if no max argument is passed" do
+ Random.srand 33
+ floats_a = 20.times.map { Random.rand }
+ Random.srand 33
+ floats_b = 20.times.map { Random.rand }
+ floats_a.should == floats_b
+ end
+
+ it "returns an Integer >= 0 if an Integer argument is passed" do
+ ints = 200.times.map { Random.rand(34) }
+ ints.min.should >= 0
+ end
+
+ it "returns an Integer < the max argument if an Integer argument is passed" do
+ ints = 200.times.map { Random.rand(55) }
+ ints.max.should < 55
+ end
+
+ it "returns the same sequence for a given seed if an Integer argument is passed" do
+ Random.srand 33
+ floats_a = 20.times.map { Random.rand(90) }
+ Random.srand 33
+ floats_b = 20.times.map { Random.rand(90) }
+ floats_a.should == floats_b
+ end
+
+ it "coerces arguments to Integers with #to_int" do
+ obj = mock_numeric('int')
+ obj.should_receive(:to_int).and_return(99)
+ Random.rand(obj).should.is_a?(Integer)
+ end
+end
+
+describe "Random#rand with Fixnum" do
+ it "returns an Integer" do
+ Random.new.rand(20).should.instance_of?(Integer)
+ end
+
+ it "returns a Fixnum greater than or equal to 0" do
+ prng = Random.new
+ ints = 20.times.map { prng.rand(5) }
+ ints.min.should >= 0
+ end
+
+ it "returns a Fixnum less than the argument" do
+ prng = Random.new
+ ints = 20.times.map { prng.rand(5) }
+ ints.max.should <= 4
+ end
+
+ it "returns the same sequence for a given seed" do
+ prng = Random.new 33
+ a = 20.times.map { prng.rand(90) }
+ prng = Random.new 33
+ b = 20.times.map { prng.rand(90) }
+ a.should == b
+ end
+
+ it "eventually returns all possible values" do
+ prng = Random.new 33
+ 100.times.map{ prng.rand(10) }.uniq.sort.should == (0...10).to_a
+ end
+
+ it "raises an ArgumentError when the argument is 0" do
+ -> do
+ Random.new.rand(0)
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the argument is negative" do
+ -> do
+ Random.new.rand(-12)
+ end.should.raise(ArgumentError)
+ end
+end
+
+describe "Random#rand with Bignum" do
+ it "typically returns a Bignum" do
+ rnd = Random.new(1)
+ 10.times.map{ rnd.rand(bignum_value*2) }.max.should.instance_of?(Integer)
+ end
+
+ it "returns a Bignum greater than or equal to 0" do
+ prng = Random.new
+ bigs = 20.times.map { prng.rand(bignum_value) }
+ bigs.min.should >= 0
+ end
+
+ it "returns a Bignum less than the argument" do
+ prng = Random.new
+ bigs = 20.times.map { prng.rand(bignum_value) }
+ bigs.max.should < bignum_value
+ end
+
+ it "returns the same sequence for a given seed" do
+ prng = Random.new 33
+ a = 20.times.map { prng.rand(bignum_value) }
+ prng = Random.new 33
+ b = 20.times.map { prng.rand(bignum_value) }
+ a.should == b
+ end
+
+ it "raises an ArgumentError when the argument is negative" do
+ -> do
+ Random.new.rand(-bignum_value)
+ end.should.raise(ArgumentError)
+ end
+end
+
+describe "Random#rand with Float" do
+ it "returns a Float" do
+ Random.new.rand(20.43).should.instance_of?(Float)
+ end
+
+ it "returns a Float greater than or equal to 0.0" do
+ prng = Random.new
+ floats = 20.times.map { prng.rand(5.2) }
+ floats.min.should >= 0.0
+ end
+
+ it "returns a Float less than the argument" do
+ prng = Random.new
+ floats = 20.times.map { prng.rand(4.30) }
+ floats.max.should < 4.30
+ end
+
+ it "returns the same sequence for a given seed" do
+ prng = Random.new 33
+ a = 20.times.map { prng.rand(89.2928) }
+ prng = Random.new 33
+ b = 20.times.map { prng.rand(89.2928) }
+ a.should == b
+ end
+
+ it "raises an ArgumentError when the argument is negative" do
+ -> do
+ Random.new.rand(-1.234567)
+ end.should.raise(ArgumentError)
+ end
+end
+
+describe "Random#rand with Range" do
+ it "returns an element from the Range" do
+ Random.new.rand(20..43).should.instance_of?(Integer)
+ end
+
+ it "supports custom object types" do
+ rand(RandomSpecs::CustomRangeInteger.new(1)..RandomSpecs::CustomRangeInteger.new(42)).should.instance_of?(RandomSpecs::CustomRangeInteger)
+ rand(RandomSpecs::CustomRangeFloat.new(1.0)..RandomSpecs::CustomRangeFloat.new(42.0)).should.instance_of?(RandomSpecs::CustomRangeFloat)
+ rand(Time.now..Time.now).should.instance_of?(Time)
+ end
+
+ it "returns an object that is a member of the Range" do
+ prng = Random.new
+ r = 20..30
+ 20.times { r.member?(prng.rand(r)).should == true }
+ end
+
+ it "works with inclusive ranges" do
+ prng = Random.new 33
+ r = 3..5
+ 40.times.map { prng.rand(r) }.uniq.sort.should == [3,4,5]
+ end
+
+ it "works with exclusive ranges" do
+ prng = Random.new 33
+ r = 3...5
+ 20.times.map { prng.rand(r) }.uniq.sort.should == [3,4]
+ end
+
+ it "returns the same sequence for a given seed" do
+ prng = Random.new 33
+ a = 20.times.map { prng.rand(76890.028..800000.00) }
+ prng = Random.new 33
+ b = 20.times.map { prng.rand(76890.028..800000.00) }
+ a.should == b
+ end
+
+ it "eventually returns all possible values" do
+ prng = Random.new 33
+ 100.times.map{ prng.rand(10..20) }.uniq.sort.should == (10..20).to_a
+ 100.times.map{ prng.rand(10...20) }.uniq.sort.should == (10...20).to_a
+ end
+
+ it "considers Integers as Floats if one end point is a float" do
+ Random.new(42).rand(0.0..1).should.is_a?(Float)
+ Random.new(42).rand(0..1.0).should.is_a?(Float)
+ end
+
+ it "returns a float within a given float range" do
+ Random.new(42).rand(0.0...100.0).should == 37.454011884736246
+ Random.new(42).rand(-100.0...0.0).should == -62.545988115263754
+ end
+
+ it "raises an ArgumentError when the startpoint lacks #+ and #- methods" do
+ -> do
+ Random.new.rand(Object.new..67)
+ end.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the endpoint lacks #+ and #- methods" do
+ -> do
+ Random.new.rand(68..Object.new)
+ end.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/random/random_number_spec.rb b/spec/ruby/core/random/random_number_spec.rb
new file mode 100644
index 0000000000..bad81aeff6
--- /dev/null
+++ b/spec/ruby/core/random/random_number_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/rand'
+
+describe "Random.random_number" do
+ it_behaves_like :random_number, :random_number, Random.new
+
+ it_behaves_like :random_number, :random_number, Random
+end
diff --git a/spec/ruby/core/random/seed_spec.rb b/spec/ruby/core/random/seed_spec.rb
new file mode 100644
index 0000000000..6529b2c873
--- /dev/null
+++ b/spec/ruby/core/random/seed_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Random#seed" do
+ it "returns an Integer" do
+ Random.new.seed.should.is_a?(Integer)
+ end
+
+ it "returns an arbitrary seed if the constructor was called without arguments" do
+ Random.new.seed.should_not == Random.new.seed
+ end
+
+ it "returns the same generated seed when repeatedly called on the same object" do
+ prng = Random.new
+ prng.seed.should == prng.seed
+ end
+
+ it "returns the seed given in the constructor" do
+ prng = Random.new(36788)
+ prng.seed.should == prng.seed
+ prng.seed.should == 36788
+ end
+
+ it "returns the given seed coerced with #to_int" do
+ obj = mock_numeric('int')
+ obj.should_receive(:to_int).and_return(34)
+ prng = Random.new(obj)
+ prng.seed.should == 34
+ end
+end
diff --git a/spec/ruby/core/random/shared/bytes.rb b/spec/ruby/core/random/shared/bytes.rb
new file mode 100644
index 0000000000..86c64d5696
--- /dev/null
+++ b/spec/ruby/core/random/shared/bytes.rb
@@ -0,0 +1,17 @@
+describe :random_bytes, shared: true do
+ it "returns a String" do
+ @object.send(@method, 1).should.instance_of?(String)
+ end
+
+ it "returns a String of the length given as argument" do
+ @object.send(@method, 15).length.should == 15
+ end
+
+ it "returns a binary String" do
+ @object.send(@method, 15).encoding.should == Encoding::BINARY
+ end
+
+ it "returns a random binary String" do
+ @object.send(@method, 12).should_not == @object.send(@method, 12)
+ end
+end
diff --git a/spec/ruby/core/random/shared/rand.rb b/spec/ruby/core/random/shared/rand.rb
new file mode 100644
index 0000000000..655c75c9f1
--- /dev/null
+++ b/spec/ruby/core/random/shared/rand.rb
@@ -0,0 +1,9 @@
+describe :random_number, shared: true do
+ it "returns a Float if no max argument is passed" do
+ @object.send(@method).should.is_a?(Float)
+ end
+
+ it "returns an Integer if an Integer argument is passed" do
+ @object.send(@method, 20).should.is_a?(Integer)
+ end
+end
diff --git a/spec/ruby/core/random/srand_spec.rb b/spec/ruby/core/random/srand_spec.rb
new file mode 100644
index 0000000000..1ef8e32cfb
--- /dev/null
+++ b/spec/ruby/core/random/srand_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "Random.srand" do
+ it "returns an arbitrary seed if .srand wasn't called previously with an argument and no argument is supplied this time" do
+ Random.srand # Reset to random seed in case .srand was called previously
+ Random.srand.should_not == Random.srand
+ end
+
+ it "returns the previous argument to .srand if one was given and no argument is supplied" do
+ Random.srand 34
+ Random.srand.should == 34
+ end
+
+ it "returns an arbitrary seed if .srand wasn't called previously with an argument and 0 is supplied this time" do
+ Random.srand # Reset to random seed in case .srand was called previously
+ Random.srand(0).should_not == Random.srand(0)
+ end
+
+ it "returns the previous argument to .srand if one was given and 0 is supplied" do
+ Random.srand 34
+ Random.srand(0).should == 34
+ end
+
+ it "seeds Random.rand such that its return value is deterministic" do
+ Random.srand 176542
+ a = 20.times.map { Random.rand }
+ Random.srand 176542
+ b = 20.times.map { Random.rand }
+ a.should == b
+ end
+
+ it "seeds Kernel.rand such that its return value is deterministic" do
+ Random.srand 176542
+ a = 20.times.map { Kernel.rand }
+ Random.srand 176542
+ b = 20.times.map { Kernel.rand }
+ a.should == b
+ end
+end
diff --git a/spec/ruby/core/random/urandom_spec.rb b/spec/ruby/core/random/urandom_spec.rb
new file mode 100644
index 0000000000..94e9423a06
--- /dev/null
+++ b/spec/ruby/core/random/urandom_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+describe "Random.urandom" do
+ it "returns a String" do
+ Random.urandom(1).should.instance_of?(String)
+ end
+
+ it "returns a String of the length given as argument" do
+ Random.urandom(15).length.should == 15
+ end
+
+ it "raises an ArgumentError on a negative size" do
+ -> {
+ Random.urandom(-1)
+ }.should.raise(ArgumentError)
+ end
+
+ it "returns a binary String" do
+ Random.urandom(15).encoding.should == Encoding::BINARY
+ end
+
+ it "returns a random binary String" do
+ Random.urandom(12).should_not == Random.urandom(12)
+ end
+end
diff --git a/spec/ruby/core/range/begin_spec.rb b/spec/ruby/core/range/begin_spec.rb
new file mode 100644
index 0000000000..ab82b45b7e
--- /dev/null
+++ b/spec/ruby/core/range/begin_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/begin'
+
+describe "Range#begin" do
+ it_behaves_like :range_begin, :begin
+end
diff --git a/spec/ruby/core/range/bsearch_spec.rb b/spec/ruby/core/range/bsearch_spec.rb
new file mode 100644
index 0000000000..151a1798cf
--- /dev/null
+++ b/spec/ruby/core/range/bsearch_spec.rb
@@ -0,0 +1,466 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Range#bsearch" do
+ it "returns an Enumerator when not passed a block" do
+ (0..1).bsearch.should.instance_of?(Enumerator)
+ end
+
+ it_behaves_like :enumeratorized_with_unknown_size, :bsearch, (1..3)
+
+ it "raises a TypeError if the block returns an Object" do
+ -> { (0..1).bsearch { Object.new } }.should.raise(TypeError, "wrong argument type Object (must be numeric, true, false or nil)")
+ end
+
+ it "raises a TypeError if the block returns a String and boundaries are Integer values" do
+ -> { (0..1).bsearch { "1" } }.should.raise(TypeError, "wrong argument type String (must be numeric, true, false or nil)")
+ end
+
+ it "raises a TypeError if the block returns a String and boundaries are Float values" do
+ -> { (0.0..1.0).bsearch { "1" } }.should.raise(TypeError, "wrong argument type String (must be numeric, true, false or nil)")
+ end
+
+ it "raises a TypeError if the Range has Object values" do
+ value = mock("range bsearch")
+ r = Range.new value, value
+
+ -> { r.bsearch { true } }.should.raise(TypeError, "can't do binary search for MockObject")
+ end
+
+ it "raises a TypeError if the Range has String values" do
+ -> { ("a".."e").bsearch { true } }.should.raise(TypeError, "can't do binary search for String")
+ end
+
+ it "raises TypeError when non-Numeric begin/end and block not passed" do
+ -> { ("a".."e").bsearch }.should.raise(TypeError, "can't do binary search for String")
+ end
+
+ context "with Integer values" do
+ context "with a block returning true or false" do
+ it "returns nil if the block returns false for every element" do
+ (0...3).bsearch { |x| x > 3 }.should == nil
+ end
+
+ it "returns nil if the block returns nil for every element" do
+ (0..3).bsearch { |x| nil }.should == nil
+ end
+
+ it "returns minimum element if the block returns true for every element" do
+ (-2..4).bsearch { |x| x < 4 }.should == -2
+ end
+
+ it "returns the smallest element for which block returns true" do
+ (0..4).bsearch { |x| x >= 2 }.should == 2
+ (-1..4).bsearch { |x| x >= 1 }.should == 1
+ end
+
+ it "returns the last element if the block returns true for the last element" do
+ (0..4).bsearch { |x| x >= 4 }.should == 4
+ (0...4).bsearch { |x| x >= 3 }.should == 3
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns less than zero for every element" do
+ (0..3).bsearch { |x| x <=> 5 }.should == nil
+ end
+
+ it "returns nil if the block returns greater than zero for every element" do
+ (0..3).bsearch { |x| x <=> -1 }.should == nil
+
+ end
+
+ it "returns nil if the block never returns zero" do
+ (0..3).bsearch { |x| x < 2 ? 1 : -1 }.should == nil
+ end
+
+ it "accepts (+/-)Float::INFINITY from the block" do
+ (0..4).bsearch { |x| Float::INFINITY }.should == nil
+ (0..4).bsearch { |x| -Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = (0..4).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = (0..4).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ [1, 2].should.include?(result)
+ end
+ end
+
+ it "returns nil for empty ranges" do
+ (0...0).bsearch { true }.should == nil
+ (0...0).bsearch { false }.should == nil
+ (0...0).bsearch { 1 }.should == nil
+ (0...0).bsearch { 0 }.should == nil
+ (0...0).bsearch { -1 }.should == nil
+
+ (4..2).bsearch { true }.should == nil
+ (4..2).bsearch { 1 }.should == nil
+ (4..2).bsearch { 0 }.should == nil
+ (4..2).bsearch { -1 }.should == nil
+ end
+
+ it "returns enumerator when block not passed" do
+ (0...3).bsearch.kind_of?(Enumerator).should == true
+ end
+ end
+
+ context "with Float values" do
+ context "with a block returning true or false" do
+ it "returns nil if the block returns false for every element" do
+ (0.1...2.3).bsearch { |x| x > 3 }.should == nil
+ end
+
+ it "returns nil if the block returns nil for every element" do
+ (-0.0..2.3).bsearch { |x| nil }.should == nil
+ end
+
+ it "returns minimum element if the block returns true for every element" do
+ (-0.2..4.8).bsearch { |x| x < 5 }.should == -0.2
+ end
+
+ it "returns the smallest element for which block returns true" do
+ (0..4.2).bsearch { |x| x >= 2 }.should == 2
+ (-1.2..4.3).bsearch { |x| x >= 1 }.should == 1
+ end
+
+ it "returns a boundary element if appropriate" do
+ (1.0..3.0).bsearch { |x| x >= 3.0 }.should == 3.0
+ (1.0...3.0).bsearch { |x| x >= 3.0.prev_float }.should == 3.0.prev_float
+ (1.0..3.0).bsearch { |x| x >= 1.0 }.should == 1.0
+ (1.0...3.0).bsearch { |x| x >= 1.0 }.should == 1.0
+ end
+
+ it "works with infinity bounds" do
+ inf = Float::INFINITY
+ (0..inf).bsearch { |x| x == inf }.should == inf
+ (0...inf).bsearch { |x| x == inf }.should == nil
+ (-inf..0).bsearch { |x| x != -inf }.should == -Float::MAX
+ (-inf...0).bsearch { |x| x != -inf }.should == -Float::MAX
+ (inf..inf).bsearch { |x| true }.should == inf
+ (inf...inf).bsearch { |x| true }.should == nil
+ (-inf..-inf).bsearch { |x| true }.should == -inf
+ (-inf...-inf).bsearch { |x| true }.should == nil
+ (inf..0).bsearch { true }.should == nil
+ (inf...0).bsearch { true }.should == nil
+ (0..-inf).bsearch { true }.should == nil
+ (0...-inf).bsearch { true }.should == nil
+ (inf..-inf).bsearch { true }.should == nil
+ (inf...-inf).bsearch { true }.should == nil
+ (0..inf).bsearch { |x| x >= 3 }.should == 3.0
+ (0...inf).bsearch { |x| x >= 3 }.should == 3.0
+ (-inf..0).bsearch { |x| x >= -3 }.should == -3.0
+ (-inf...0).bsearch { |x| x >= -3 }.should == -3.0
+ (-inf..inf).bsearch { |x| x >= 3 }.should == 3.0
+ (-inf...inf).bsearch { |x| x >= 3 }.should == 3.0
+ (0..inf).bsearch { |x| x >= Float::MAX }.should == Float::MAX
+ (0...inf).bsearch { |x| x >= Float::MAX }.should == Float::MAX
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns less than zero for every element" do
+ (-2.0..3.2).bsearch { |x| x <=> 5 }.should == nil
+ end
+
+ it "returns nil if the block returns greater than zero for every element" do
+ (0.3..3.0).bsearch { |x| x <=> -1 }.should == nil
+ end
+
+ it "returns nil if the block never returns zero" do
+ (0.2..2.3).bsearch { |x| x < 2 ? 1 : -1 }.should == nil
+ end
+
+ it "accepts (+/-)Float::INFINITY from the block" do
+ (0.1..4.5).bsearch { |x| Float::INFINITY }.should == nil
+ (-5.0..4.0).bsearch { |x| -Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = (0.0..4.0).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = (0.1..4.9).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ result.should >= 1
+ result.should <= 3
+ end
+
+ it "returns an element at an index for which block returns 0 (small numbers)" do
+ result = (0.1..0.3).bsearch { |x| x < 0.1 ? 1 : x > 0.3 ? -1 : 0 }
+ result.should >= 0.1
+ result.should <= 0.3
+ end
+
+ it "returns a boundary element if appropriate" do
+ (1.0..3.0).bsearch { |x| 3.0 - x }.should == 3.0
+ (1.0...3.0).bsearch { |x| 3.0.prev_float - x }.should == 3.0.prev_float
+ (1.0..3.0).bsearch { |x| 1.0 - x }.should == 1.0
+ (1.0...3.0).bsearch { |x| 1.0 - x }.should == 1.0
+ end
+
+ it "works with infinity bounds" do
+ inf = Float::INFINITY
+ (0..inf).bsearch { |x| x == inf ? 0 : 1 }.should == inf
+ (0...inf).bsearch { |x| x == inf ? 0 : 1 }.should == nil
+ (-inf...0).bsearch { |x| x == -inf ? 0 : -1 }.should == -inf
+ (-inf..0).bsearch { |x| x == -inf ? 0 : -1 }.should == -inf
+ (inf..inf).bsearch { 0 }.should == inf
+ (inf...inf).bsearch { 0 }.should == nil
+ (-inf..-inf).bsearch { 0 }.should == -inf
+ (-inf...-inf).bsearch { 0 }.should == nil
+ (inf..0).bsearch { 0 }.should == nil
+ (inf...0).bsearch { 0 }.should == nil
+ (0..-inf).bsearch { 0 }.should == nil
+ (0...-inf).bsearch { 0 }.should == nil
+ (inf..-inf).bsearch { 0 }.should == nil
+ (inf...-inf).bsearch { 0 }.should == nil
+ (-inf..inf).bsearch { |x| 3 - x }.should == 3.0
+ (-inf...inf).bsearch { |x| 3 - x }.should == 3.0
+ (0...inf).bsearch { |x| x >= Float::MAX ? 0 : 1 }.should == Float::MAX
+ end
+ end
+
+ it "returns enumerator when block not passed" do
+ (0.1...2.3).bsearch.kind_of?(Enumerator).should == true
+ end
+ end
+
+ context "with endless ranges and Integer values" do
+ context "with a block returning true or false" do
+ it "returns minimum element if the block returns true for every element" do
+ eval("(-2..)").bsearch { |x| true }.should == -2
+ end
+
+ it "returns the smallest element for which block returns true" do
+ eval("(0..)").bsearch { |x| x >= 2 }.should == 2
+ eval("(-1..)").bsearch { |x| x >= 1 }.should == 1
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns less than zero for every element" do
+ eval("(0..)").bsearch { |x| -1 }.should == nil
+ end
+
+ it "returns nil if the block never returns zero" do
+ eval("(0..)").bsearch { |x| x > 5 ? -1 : 1 }.should == nil
+ end
+
+ it "accepts -Float::INFINITY from the block" do
+ eval("(0..)").bsearch { |x| -Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = eval("(0..)").bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = eval("(0..)").bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ [1, 2, 3].should.include?(result)
+ end
+ end
+
+ it "returns enumerator when block not passed" do
+ eval("(-2..)").bsearch.kind_of?(Enumerator).should == true
+ end
+ end
+
+ context "with endless ranges and Float values" do
+ context "with a block returning true or false" do
+ it "returns nil if the block returns false for every element" do
+ eval("(0.1..)").bsearch { |x| x < 0.0 }.should == nil
+ eval("(0.1...)").bsearch { |x| x < 0.0 }.should == nil
+ end
+
+ it "returns nil if the block returns nil for every element" do
+ eval("(-0.0..)").bsearch { |x| nil }.should == nil
+ eval("(-0.0...)").bsearch { |x| nil }.should == nil
+ end
+
+ it "returns minimum element if the block returns true for every element" do
+ eval("(-0.2..)").bsearch { |x| true }.should == -0.2
+ eval("(-0.2...)").bsearch { |x| true }.should == -0.2
+ end
+
+ it "returns the smallest element for which block returns true" do
+ eval("(0..)").bsearch { |x| x >= 2 }.should == 2
+ eval("(-1.2..)").bsearch { |x| x >= 1 }.should == 1
+ end
+
+ it "works with infinity bounds" do
+ inf = Float::INFINITY
+ eval("(inf..)").bsearch { |x| true }.should == inf
+ eval("(inf...)").bsearch { |x| true }.should == nil
+ eval("(-inf..)").bsearch { |x| true }.should == -inf
+ eval("(-inf...)").bsearch { |x| true }.should == -inf
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns less than zero for every element" do
+ eval("(-2.0..)").bsearch { |x| -1 }.should == nil
+ eval("(-2.0...)").bsearch { |x| -1 }.should == nil
+ end
+
+ it "returns nil if the block returns greater than zero for every element" do
+ eval("(0.3..)").bsearch { |x| 1 }.should == nil
+ eval("(0.3...)").bsearch { |x| 1 }.should == nil
+ end
+
+ it "returns nil if the block never returns zero" do
+ eval("(0.2..)").bsearch { |x| x < 2 ? 1 : -1 }.should == nil
+ end
+
+ it "accepts (+/-)Float::INFINITY from the block" do
+ eval("(0.1..)").bsearch { |x| Float::INFINITY }.should == nil
+ eval("(-5.0..)").bsearch { |x| -Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = eval("(0.0..)").bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = eval("(0.1..)").bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ result.should >= 1
+ result.should <= 3
+ end
+
+ it "works with infinity bounds" do
+ inf = Float::INFINITY
+ eval("(inf..)").bsearch { |x| 1 }.should == nil
+ eval("(inf...)").bsearch { |x| 1 }.should == nil
+ eval("(inf..)").bsearch { |x| x == inf ? 0 : 1 }.should == inf
+ eval("(inf...)").bsearch { |x| x == inf ? 0 : 1 }.should == nil
+ eval("(-inf..)").bsearch { |x| x == -inf ? 0 : -1 }.should == -inf
+ eval("(-inf...)").bsearch { |x| x == -inf ? 0 : -1 }.should == -inf
+ eval("(-inf..)").bsearch { |x| 3 - x }.should == 3
+ eval("(-inf...)").bsearch { |x| 3 - x }.should == 3
+ eval("(0.0...)").bsearch { 0 }.should != inf
+ end
+ end
+
+ it "returns enumerator when block not passed" do
+ eval("(0.1..)").bsearch.kind_of?(Enumerator).should == true
+ end
+ end
+
+ context "with beginless ranges and Integer values" do
+ context "with a block returning true or false" do
+ it "returns the smallest element for which block returns true" do
+ (..10).bsearch { |x| x >= 2 }.should == 2
+ (...-1).bsearch { |x| x >= -10 }.should == -10
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns greater than zero for every element" do
+ (..0).bsearch { |x| 1 }.should == nil
+ end
+
+ it "returns nil if the block never returns zero" do
+ (..0).bsearch { |x| x > 5 ? -1 : 1 }.should == nil
+ end
+
+ it "accepts Float::INFINITY from the block" do
+ (..0).bsearch { |x| Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = (..10).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = (...10).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ [1, 2, 3].should.include?(result)
+ end
+ end
+
+ it "returns enumerator when block not passed" do
+ (..10).bsearch.kind_of?(Enumerator).should == true
+ end
+ end
+
+ context "with beginless ranges and Float values" do
+ context "with a block returning true or false" do
+ it "returns nil if the block returns true for every element" do
+ (..-0.1).bsearch { |x| x > 0.0 }.should == nil
+ (...-0.1).bsearch { |x| x > 0.0 }.should == nil
+ end
+
+ it "returns nil if the block returns nil for every element" do
+ (..-0.1).bsearch { |x| nil }.should == nil
+ (...-0.1).bsearch { |x| nil }.should == nil
+ end
+
+ it "returns the smallest element for which block returns true" do
+ (..10).bsearch { |x| x >= 2 }.should == 2
+ (..10).bsearch { |x| x >= 1 }.should == 1
+ end
+
+ it "works with infinity bounds" do
+ inf = Float::INFINITY
+ (..inf).bsearch { |x| true }.should == -inf
+ (...inf).bsearch { |x| true }.should == -inf
+ (..-inf).bsearch { |x| true }.should == -inf
+ (...-inf).bsearch { |x| true }.should == nil
+ end
+ end
+
+ context "with a block returning negative, zero, positive numbers" do
+ it "returns nil if the block returns less than zero for every element" do
+ (..5.0).bsearch { |x| -1 }.should == nil
+ (...5.0).bsearch { |x| -1 }.should == nil
+ end
+
+ it "returns nil if the block returns greater than zero for every element" do
+ (..1.1).bsearch { |x| 1 }.should == nil
+ (...1.1).bsearch { |x| 1 }.should == nil
+ end
+
+ it "returns nil if the block never returns zero" do
+ (..6.3).bsearch { |x| x < 2 ? 1 : -1 }.should == nil
+ end
+
+ it "accepts (+/-)Float::INFINITY from the block" do
+ (..5.0).bsearch { |x| Float::INFINITY }.should == nil
+ (..7.0).bsearch { |x| -Float::INFINITY }.should == nil
+ end
+
+ it "returns an element at an index for which block returns 0.0" do
+ result = (..8.0).bsearch { |x| x < 2 ? 1.0 : x > 2 ? -1.0 : 0.0 }
+ result.should == 2
+ end
+
+ it "returns an element at an index for which block returns 0" do
+ result = (..8.0).bsearch { |x| x < 1 ? 1 : x > 3 ? -1 : 0 }
+ result.should >= 1
+ result.should <= 3
+ end
+
+ it "works with infinity bounds" do
+ inf = Float::INFINITY
+ (..-inf).bsearch { |x| 1 }.should == nil
+ (...-inf).bsearch { |x| 1 }.should == nil
+ (..inf).bsearch { |x| x == inf ? 0 : 1 }.should == inf
+ (...inf).bsearch { |x| x == inf ? 0 : 1 }.should == nil
+ (..-inf).bsearch { |x| x == -inf ? 0 : -1 }.should == -inf
+ (...-inf).bsearch { |x| x == -inf ? 0 : -1 }.should == nil
+ (..inf).bsearch { |x| 3 - x }.should == 3
+ (...inf).bsearch { |x| 3 - x }.should == 3
+ end
+ end
+
+ it "returns enumerator when block not passed" do
+ (..-0.1).bsearch.kind_of?(Enumerator).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/range/case_compare_spec.rb b/spec/ruby/core/range/case_compare_spec.rb
new file mode 100644
index 0000000000..7a76487d68
--- /dev/null
+++ b/spec/ruby/core/range/case_compare_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+require_relative 'shared/cover_and_include'
+require_relative 'shared/cover'
+
+describe "Range#===" do
+ it "returns the result of calling #cover? on self" do
+ range = RangeSpecs::WithoutSucc.new(0)..RangeSpecs::WithoutSucc.new(10)
+ (range === RangeSpecs::WithoutSucc.new(2)).should == true
+ end
+
+ it_behaves_like :range_cover_and_include, :===
+ it_behaves_like :range_cover, :===
+
+ it "returns true on any value if begin and end are both nil" do
+ (nil..nil).should === 1
+ end
+end
diff --git a/spec/ruby/core/range/clone_spec.rb b/spec/ruby/core/range/clone_spec.rb
new file mode 100644
index 0000000000..cf6ce74da0
--- /dev/null
+++ b/spec/ruby/core/range/clone_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Range#clone" do
+ it "duplicates the range" do
+ original = (1..3)
+ copy = original.clone
+ copy.begin.should == 1
+ copy.end.should == 3
+ copy.should_not.exclude_end?
+ copy.should_not.equal? original
+
+ original = ("a"..."z")
+ copy = original.clone
+ copy.begin.should == "a"
+ copy.end.should == "z"
+ copy.should.exclude_end?
+ copy.should_not.equal? original
+ end
+
+ it "maintains the frozen state" do
+ (1..2).clone.frozen?.should == (1..2).frozen?
+ (1..).clone.frozen?.should == (1..).frozen?
+ Range.new(1, 2).clone.frozen?.should == Range.new(1, 2).frozen?
+ Class.new(Range).new(1, 2).clone.frozen?.should == Class.new(Range).new(1, 2).frozen?
+ end
+end
diff --git a/spec/ruby/core/range/count_spec.rb b/spec/ruby/core/range/count_spec.rb
new file mode 100644
index 0000000000..24d4a9caf3
--- /dev/null
+++ b/spec/ruby/core/range/count_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+
+describe "Range#count" do
+ it "returns Infinity for beginless ranges without arguments or blocks" do
+ inf = Float::INFINITY
+ eval("('a'...)").count.should == inf
+ eval("(7..)").count.should == inf
+ (...'a').count.should == inf
+ (...nil).count.should == inf
+ (..10.0).count.should == inf
+ end
+end
diff --git a/spec/ruby/core/range/cover_spec.rb b/spec/ruby/core/range/cover_spec.rb
new file mode 100644
index 0000000000..eb8d5453bf
--- /dev/null
+++ b/spec/ruby/core/range/cover_spec.rb
@@ -0,0 +1,14 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'shared/cover_and_include'
+require_relative 'shared/cover'
+
+describe "Range#cover?" do
+ it_behaves_like :range_cover_and_include, :cover?
+ it_behaves_like :range_cover, :cover?
+ it_behaves_like :range_cover_subrange, :cover?
+
+ it "covers U+9995 in the range U+0999..U+9999" do
+ ("\u{999}".."\u{9999}").cover?("\u{9995}").should == true
+ end
+end
diff --git a/spec/ruby/core/range/dup_spec.rb b/spec/ruby/core/range/dup_spec.rb
new file mode 100644
index 0000000000..fab3c3f1b2
--- /dev/null
+++ b/spec/ruby/core/range/dup_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Range#dup" do
+ it "duplicates the range" do
+ original = (1..3)
+ copy = original.dup
+ copy.begin.should == 1
+ copy.end.should == 3
+ copy.should_not.exclude_end?
+ copy.should_not.equal?(original)
+
+ copy = ("a"..."z").dup
+ copy.begin.should == "a"
+ copy.end.should == "z"
+ copy.should.exclude_end?
+ end
+
+ it "creates an unfrozen range" do
+ (1..2).dup.should_not.frozen?
+ (1..).dup.should_not.frozen?
+ Range.new(1, 2).dup.should_not.frozen?
+ end
+end
diff --git a/spec/ruby/core/range/each_spec.rb b/spec/ruby/core/range/each_spec.rb
new file mode 100644
index 0000000000..b4389f864d
--- /dev/null
+++ b/spec/ruby/core/range/each_spec.rb
@@ -0,0 +1,101 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Range#each" do
+ it "passes each element to the given block by using #succ" do
+ a = []
+ (-5..5).each { |i| a << i }
+ a.should == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]
+
+ a = []
+ ('A'..'D').each { |i| a << i }
+ a.should == ['A','B','C','D']
+
+ a = []
+ ('A'...'D').each { |i| a << i }
+ a.should == ['A','B','C']
+
+ a = []
+ (0xfffd...0xffff).each { |i| a << i }
+ a.should == [0xfffd, 0xfffe]
+
+ y = mock('y')
+ x = mock('x')
+ x.should_receive(:<=>).with(y).any_number_of_times.and_return(-1)
+ x.should_receive(:<=>).with(x).any_number_of_times.and_return(0)
+ x.should_receive(:succ).any_number_of_times.and_return(y)
+ y.should_receive(:<=>).with(x).any_number_of_times.and_return(1)
+ y.should_receive(:<=>).with(y).any_number_of_times.and_return(0)
+
+ a = []
+ (x..y).each { |i| a << i }
+ a.should == [x, y]
+ end
+
+ it "works for non-ASCII ranges" do
+ a = []
+ ('Σ'..'Ω').each { |i| a << i }
+ a.should == ["Σ", "Τ", "Υ", "Φ", "Χ", "Ψ", "Ω"]
+ end
+
+ it "works with endless ranges" do
+ a = []
+ (-2..).each { |x| break if x > 2; a << x }
+ a.should == [-2, -1, 0, 1, 2]
+
+ a = []
+ (-2...).each { |x| break if x > 2; a << x }
+ a.should == [-2, -1, 0, 1, 2]
+ end
+
+ it "works with String endless ranges" do
+ a = []
+ ('A'..).each { |x| break if x > "D"; a << x }
+ a.should == ["A", "B", "C", "D"]
+
+ a = []
+ ('A'...).each { |x| break if x > "D"; a << x }
+ a.should == ["A", "B", "C", "D"]
+ end
+
+ it "raises a TypeError beginless ranges" do
+ -> { (..2).each { |x| x } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the first element does not respond to #succ" do
+ -> { (0.5..2.4).each { |i| i } }.should.raise(TypeError)
+
+ b = mock('x')
+ (a = mock('1')).should_receive(:<=>).with(b).and_return(1)
+
+ -> { (a..b).each { |i| i } }.should.raise(TypeError)
+ end
+
+ it "returns self" do
+ range = 1..10
+ range.each{}.should.equal?(range)
+ end
+
+ it "returns an enumerator when no block given" do
+ enum = (1..3).each
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == [1, 2, 3]
+ end
+
+ it "supports Time objects that respond to #succ" do
+ t = Time.utc(1970)
+ def t.succ; self + 1 end
+ t_succ = t.succ
+ def t_succ.succ; self + 1; end
+
+ (t..t_succ).to_a.should == [Time.utc(1970), Time.utc(1970, nil, nil, nil, nil, 1)]
+ (t...t_succ).to_a.should == [Time.utc(1970)]
+ end
+
+ it "passes each Symbol element by using #succ" do
+ (:aa..:ac).each.to_a.should == [:aa, :ab, :ac]
+ (:aa...:ac).each.to_a.should == [:aa, :ab]
+ end
+
+ it_behaves_like :enumeratorized_with_origin_size, :each, (1..3)
+end
diff --git a/spec/ruby/core/range/end_spec.rb b/spec/ruby/core/range/end_spec.rb
new file mode 100644
index 0000000000..9e5e6f7d43
--- /dev/null
+++ b/spec/ruby/core/range/end_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/end'
+
+describe "Range#end" do
+ it_behaves_like :range_end, :end
+end
diff --git a/spec/ruby/core/range/eql_spec.rb b/spec/ruby/core/range/eql_spec.rb
new file mode 100644
index 0000000000..cdc19c9331
--- /dev/null
+++ b/spec/ruby/core/range/eql_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal_value'
+
+describe "Range#eql?" do
+ it_behaves_like :range_eql, :eql?
+
+ it "returns false if the endpoints are not eql?" do
+ (0..1).should_not.eql?(0..1.0)
+ end
+end
diff --git a/spec/ruby/core/range/equal_value_spec.rb b/spec/ruby/core/range/equal_value_spec.rb
new file mode 100644
index 0000000000..83dcf5cec8
--- /dev/null
+++ b/spec/ruby/core/range/equal_value_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal_value'
+
+describe "Range#==" do
+ it_behaves_like :range_eql, :==
+
+ it "returns true if the endpoints are ==" do
+ (0..1).should == (0..1.0)
+ end
+
+ it "returns true if the endpoints are == for endless ranges" do
+ eval("(1.0..)").should == eval("(1.0..)")
+ end
+
+ it "returns true if the endpoints are == for beginless ranges" do
+ (...10).should == (...10)
+ end
+end
diff --git a/spec/ruby/core/range/exclude_end_spec.rb b/spec/ruby/core/range/exclude_end_spec.rb
new file mode 100644
index 0000000000..c4006fea78
--- /dev/null
+++ b/spec/ruby/core/range/exclude_end_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Range#exclude_end?" do
+ it "returns false if the range does not exclude the end value" do
+ (-2..2).should_not.exclude_end?
+ ('A'..'B').should_not.exclude_end?
+ (0.5..2.4).should_not.exclude_end?
+ (0xfffd..0xffff).should_not.exclude_end?
+ Range.new(0, 1).should_not.exclude_end?
+ end
+
+ it "returns true if the range excludes the end value" do
+ (0...5).should.exclude_end?
+ ('A'...'B').should.exclude_end?
+ (0.5...2.4).should.exclude_end?
+ (0xfffd...0xffff).should.exclude_end?
+ Range.new(0, 1, true).should.exclude_end?
+ end
+end
diff --git a/spec/ruby/core/range/first_spec.rb b/spec/ruby/core/range/first_spec.rb
new file mode 100644
index 0000000000..54bd73a4e8
--- /dev/null
+++ b/spec/ruby/core/range/first_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+require_relative 'shared/begin'
+
+describe "Range#first" do
+ it_behaves_like :range_begin, :first
+
+ it "returns the specified number of elements from the beginning" do
+ (0..2).first(2).should == [0, 1]
+ end
+
+ it "returns an empty array for an empty Range" do
+ (0...0).first(2).should == []
+ end
+
+ it "returns an empty array when passed zero" do
+ (0..2).first(0).should == []
+ end
+
+ it "returns all elements in the range when count exceeds the number of elements" do
+ (0..2).first(4).should == [0, 1, 2]
+ end
+
+ it "raises an ArgumentError when count is negative" do
+ -> { (0..2).first(-1) }.should.raise(ArgumentError)
+ end
+
+ it "calls #to_int to convert the argument" do
+ obj = mock_int(2)
+ (3..7).first(obj).should == [3, 4]
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return("1")
+ -> { (2..3).first(obj) }.should.raise(TypeError)
+ end
+
+ it "truncates the value when passed a Float" do
+ (2..9).first(2.8).should == [2, 3]
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { (2..3).first(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { (2..3).first("1") }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError when called on an beginless range" do
+ -> { (..1).first }.should.raise(RangeError)
+ end
+end
diff --git a/spec/ruby/core/range/fixtures/classes.rb b/spec/ruby/core/range/fixtures/classes.rb
new file mode 100644
index 0000000000..3a1df010b2
--- /dev/null
+++ b/spec/ruby/core/range/fixtures/classes.rb
@@ -0,0 +1,90 @@
+module RangeSpecs
+ class TenfoldSucc
+ include Comparable
+
+ attr_reader :n
+
+ def initialize(n)
+ @n = n
+ end
+
+ def <=>(other)
+ @n <=> other.n
+ end
+
+ def succ
+ self.class.new(@n * 10)
+ end
+ end
+
+ # Custom Range classes Xs and Ys
+ class Custom
+ include Comparable
+ attr_reader :length
+
+ def initialize(n)
+ @length = n
+ end
+
+ def eql?(other)
+ inspect.eql? other.inspect
+ end
+ alias :== :eql?
+
+ def inspect
+ 'custom'
+ end
+
+ def <=>(other)
+ @length <=> other.length
+ end
+ end
+
+ class WithoutSucc
+ include Comparable
+ attr_reader :n
+
+ def initialize(n)
+ @n = n
+ end
+
+ def eql?(other)
+ inspect.eql? other.inspect
+ end
+ alias :== :eql?
+
+ def inspect
+ "WithoutSucc(#{@n})"
+ end
+
+ def <=>(other)
+ @n <=> other.n
+ end
+ end
+
+ class Xs < Custom # represent a string of 'x's
+ def succ
+ Xs.new(@length + 1)
+ end
+
+ def inspect
+ 'x' * @length
+ end
+ end
+
+ class Ys < Custom # represent a string of 'y's
+ def succ
+ Ys.new(@length + 1)
+ end
+
+ def inspect
+ 'y' * @length
+ end
+ end
+
+ class MyRange < Range
+ end
+
+ class ComparisonError < RuntimeError
+ end
+end
diff --git a/spec/ruby/core/range/frozen_spec.rb b/spec/ruby/core/range/frozen_spec.rb
new file mode 100644
index 0000000000..8dab5e5339
--- /dev/null
+++ b/spec/ruby/core/range/frozen_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+
+# There is no Range#frozen? method but this feels like the best place for these specs
+describe "Range#frozen?" do
+ it "is true for literal ranges" do
+ (1..2).should.frozen?
+ (1..).should.frozen?
+ (..1).should.frozen?
+ end
+
+ it "is true for Range.new" do
+ Range.new(1, 2).should.frozen?
+ Range.new(1, nil).should.frozen?
+ Range.new(nil, 1).should.frozen?
+ end
+
+ it "is false for instances of a subclass of Range" do
+ sub_range = Class.new(Range).new(1, 2)
+ sub_range.should_not.frozen?
+ end
+
+ it "is false for Range.allocate" do
+ Range.allocate.should_not.frozen?
+ end
+end
diff --git a/spec/ruby/core/range/hash_spec.rb b/spec/ruby/core/range/hash_spec.rb
new file mode 100644
index 0000000000..087f5d6de8
--- /dev/null
+++ b/spec/ruby/core/range/hash_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "Range#hash" do
+ it "is provided" do
+ (0..1).respond_to?(:hash).should == true
+ ('A'..'Z').respond_to?(:hash).should == true
+ (0xfffd..0xffff).respond_to?(:hash).should == true
+ (0.5..2.4).respond_to?(:hash).should == true
+ end
+
+ it "generates the same hash values for Ranges with the same start, end and exclude_end? values" do
+ (0..1).hash.should == (0..1).hash
+ (0...10).hash.should == (0...10).hash
+ (0..10).hash.should_not == (0...10).hash
+ end
+
+ it "generates an Integer for the hash value" do
+ (0..0).hash.should.instance_of?(Integer)
+ (0..1).hash.should.instance_of?(Integer)
+ (0...10).hash.should.instance_of?(Integer)
+ (0..10).hash.should.instance_of?(Integer)
+ end
+
+end
diff --git a/spec/ruby/core/range/include_spec.rb b/spec/ruby/core/range/include_spec.rb
new file mode 100644
index 0000000000..66a049a90d
--- /dev/null
+++ b/spec/ruby/core/range/include_spec.rb
@@ -0,0 +1,14 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'shared/cover_and_include'
+require_relative 'shared/include'
+require_relative 'shared/cover'
+
+describe "Range#include?" do
+ it_behaves_like :range_cover_and_include, :include?
+ it_behaves_like :range_include, :include?
+
+ it "does not include U+9995 in the range U+0999..U+9999" do
+ ("\u{999}".."\u{9999}").include?("\u{9995}").should == false
+ end
+end
diff --git a/spec/ruby/core/range/initialize_spec.rb b/spec/ruby/core/range/initialize_spec.rb
new file mode 100644
index 0000000000..b1a0565ab2
--- /dev/null
+++ b/spec/ruby/core/range/initialize_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Range#initialize" do
+ before do
+ @range = Range.allocate
+ end
+
+ it "is private" do
+ Range.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "initializes correctly the Range object when given 2 arguments" do
+ -> { @range.send(:initialize, 0, 1) }.should_not.raise
+ end
+
+ it "initializes correctly the Range object when given 3 arguments" do
+ -> { @range.send(:initialize, 0, 1, true) }.should_not.raise
+ end
+
+ it "raises an ArgumentError if passed without or with only one argument" do
+ -> { @range.send(:initialize) }.should.raise(ArgumentError)
+ -> { @range.send(:initialize, 1) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if passed with four or more arguments" do
+ -> { @range.send(:initialize, 1, 3, 5, 7) }.should.raise(ArgumentError)
+ -> { @range.send(:initialize, 1, 3, 5, 7, 9) }.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError if called on an already initialized Range" do
+ -> { (0..1).send(:initialize, 1, 3) }.should.raise(FrozenError)
+ -> { (0..1).send(:initialize, 1, 3, true) }.should.raise(FrozenError)
+ end
+
+ it "raises an ArgumentError if arguments don't respond to <=>" do
+ o1 = Object.new
+ o2 = Object.new
+
+ -> { @range.send(:initialize, o1, o2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/range/inspect_spec.rb b/spec/ruby/core/range/inspect_spec.rb
new file mode 100644
index 0000000000..072de123b7
--- /dev/null
+++ b/spec/ruby/core/range/inspect_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Range#inspect" do
+ it "provides a printable form, using #inspect to convert the start and end objects" do
+ ('A'..'Z').inspect.should == '"A".."Z"'
+ ('A'...'Z').inspect.should == '"A"..."Z"'
+
+ (0..21).inspect.should == "0..21"
+ (-8..0).inspect.should == "-8..0"
+ (-411..959).inspect.should == "-411..959"
+ (0xfff..0xfffff).inspect.should == "4095..1048575"
+ (0.5..2.4).inspect.should == "0.5..2.4"
+ end
+
+ it "works for endless ranges" do
+ eval("(1..)").inspect.should == "1.."
+ eval("(0.1...)").inspect.should == "0.1..."
+ end
+
+ it "works for beginless ranges" do
+ (..1).inspect.should == "..1"
+ (...0.1).inspect.should == "...0.1"
+ end
+
+ it "works for nil ... nil ranges" do
+ (..nil).inspect.should == "nil..nil"
+ eval("(nil...)").inspect.should == "nil...nil"
+ end
+end
diff --git a/spec/ruby/core/range/last_spec.rb b/spec/ruby/core/range/last_spec.rb
new file mode 100644
index 0000000000..a7db7f85a7
--- /dev/null
+++ b/spec/ruby/core/range/last_spec.rb
@@ -0,0 +1,57 @@
+require_relative '../../spec_helper'
+require_relative 'shared/end'
+
+describe "Range#last" do
+ it_behaves_like :range_end, :last
+
+ it "returns the specified number of elements from the end" do
+ (1..5).last(3).should == [3, 4, 5]
+ end
+
+ it "returns the specified number if elements for single element inclusive range" do
+ (1..1).last(1).should == [1]
+ end
+
+ it "returns an empty array for an empty Range" do
+ (0...0).last(2).should == []
+ end
+
+ it "returns an empty array when passed zero" do
+ (0..2).last(0).should == []
+ end
+
+ it "returns all elements in the range when count exceeds the number of elements" do
+ (2..4).last(5).should == [2, 3, 4]
+ end
+
+ it "raises an ArgumentError when count is negative" do
+ -> { (0..2).last(-1) }.should.raise(ArgumentError)
+ end
+
+ it "calls #to_int to convert the argument" do
+ obj = mock_int(2)
+ (3..7).last(obj).should == [6, 7]
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ obj = mock("to_int")
+ obj.should_receive(:to_int).and_return("1")
+ -> { (2..3).last(obj) }.should.raise(TypeError)
+ end
+
+ it "truncates the value when passed a Float" do
+ (2..9).last(2.8).should == [8, 9]
+ end
+
+ it "raises a TypeError when passed nil" do
+ -> { (2..3).last(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed a String" do
+ -> { (2..3).last("1") }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError when called on an endless range" do
+ -> { eval("(1..)").last }.should.raise(RangeError)
+ end
+end
diff --git a/spec/ruby/core/range/max_spec.rb b/spec/ruby/core/range/max_spec.rb
new file mode 100644
index 0000000000..57714967ce
--- /dev/null
+++ b/spec/ruby/core/range/max_spec.rb
@@ -0,0 +1,115 @@
+require_relative '../../spec_helper'
+
+describe "Range#max" do
+ it "returns the maximum value in the range when called with no arguments" do
+ (1..10).max.should == 10
+ (1...10).max.should == 9
+ (0...2**64).max.should == 18446744073709551615
+ ('f'..'l').max.should == 'l'
+ ('a'...'f').max.should == 'e'
+ end
+
+ it "returns the maximum value in the Float range when called with no arguments" do
+ (303.20..908.1111).max.should == 908.1111
+ end
+
+ it "raises TypeError when called on an exclusive range and a non Integer value" do
+ -> { (303.20...908.1111).max }.should.raise(TypeError)
+ end
+
+ it "returns nil when the endpoint is less than the start point" do
+ (100..10).max.should == nil
+ ('z'..'l').max.should == nil
+ end
+
+ it "returns nil when the endpoint equals the start point and the range is exclusive" do
+ (5...5).max.should == nil
+ end
+
+ it "returns the endpoint when the endpoint equals the start point and the range is inclusive" do
+ (5..5).max.should.equal?(5)
+ end
+
+ it "returns nil when the endpoint is less than the start point in a Float range" do
+ (3003.20..908.1111).max.should == nil
+ end
+
+ it "returns end point when the range is Time..Time(included end point)" do
+ time_start = Time.now
+ time_end = Time.now + 1.0
+ (time_start..time_end).max.should.equal?(time_end)
+ end
+
+ it "raises TypeError when called on a Time...Time(excluded end point)" do
+ time_start = Time.now
+ time_end = Time.now + 1.0
+ -> { (time_start...time_end).max }.should.raise(TypeError)
+ end
+
+ it "raises RangeError when called on an endless range" do
+ -> { eval("(1..)").max }.should.raise(RangeError)
+ end
+
+ it "returns the end point for beginless ranges" do
+ (..1).max.should == 1
+ (..1.0).max.should == 1.0
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "raises for an exclusive beginless Integer range" do
+ -> {
+ (...1).max
+ }.should.raise(TypeError, 'cannot exclude end value with non Integer begin value')
+ end
+ end
+
+ ruby_version_is "4.0" do
+ it "returns the end point for exclusive beginless Integer ranges" do
+ (...1).max.should == 0
+ end
+ end
+
+ it "raises for an exclusive beginless non Integer range" do
+ -> {
+ (...1.0).max
+ }.should.raise(TypeError, 'cannot exclude non Integer end value')
+ end
+end
+
+describe "Range#max given a block" do
+ it "passes each pair of values in the range to the block" do
+ acc = []
+ (1..10).max {|a,b| acc << [a,b]; a }
+ acc.flatten!
+ (1..10).each do |value|
+ acc.include?(value).should == true
+ end
+ end
+
+ it "passes each pair of elements to the block in reversed order" do
+ acc = []
+ (1..5).max {|a,b| acc << [a,b]; a }
+ acc.should == [[2,1],[3,2], [4,3], [5, 4]]
+ end
+
+ it "calls #> and #< on the return value of the block" do
+ obj = mock('obj')
+ obj.should_receive(:>).exactly(2).times
+ obj.should_receive(:<).exactly(2).times
+ (1..3).max {|a,b| obj }
+ end
+
+ it "returns the element the block determines to be the maximum" do
+ (1..3).max {|a,b| -3 }.should == 1
+ end
+
+ it "returns nil when the endpoint is less than the start point" do
+ (100..10).max {|x,y| x <=> y}.should == nil
+ ('z'..'l').max {|x,y| x <=> y}.should == nil
+ (5...5).max {|x,y| x <=> y}.should == nil
+ end
+
+ it "raises RangeError when called with custom comparison method on an beginless range" do
+ -> { (..1).max {|a, b| a} }.should.raise(RangeError)
+ end
+end
diff --git a/spec/ruby/core/range/member_spec.rb b/spec/ruby/core/range/member_spec.rb
new file mode 100644
index 0000000000..78299ae9e5
--- /dev/null
+++ b/spec/ruby/core/range/member_spec.rb
@@ -0,0 +1,10 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'shared/cover_and_include'
+require_relative 'shared/include'
+require_relative 'shared/cover'
+
+describe "Range#member?" do
+ it_behaves_like :range_cover_and_include, :member?
+ it_behaves_like :range_include, :member?
+end
diff --git a/spec/ruby/core/range/min_spec.rb b/spec/ruby/core/range/min_spec.rb
new file mode 100644
index 0000000000..9c83d3deca
--- /dev/null
+++ b/spec/ruby/core/range/min_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+
+describe "Range#min" do
+ it "returns the minimum value in the range when called with no arguments" do
+ (1..10).min.should == 1
+ ('f'..'l').min.should == 'f'
+ end
+
+ it "returns the minimum value in the Float range when called with no arguments" do
+ (303.20..908.1111).min.should == 303.20
+ end
+
+ it "returns nil when the start point is greater than the endpoint" do
+ (100..10).min.should == nil
+ ('z'..'l').min.should == nil
+ end
+
+ it "returns nil when the endpoint equals the start point and the range is exclusive" do
+ (7...7).min.should == nil
+ end
+
+ it "returns the start point when the endpoint equals the start point and the range is inclusive" do
+ (7..7).min.should.equal?(7)
+ end
+
+ it "returns nil when the start point is greater than the endpoint in a Float range" do
+ (3003.20..908.1111).min.should == nil
+ end
+
+ it "returns start point when the range is Time..Time(included end point)" do
+ time_start = Time.now
+ time_end = Time.now + 1.0
+ (time_start..time_end).min.should.equal?(time_start)
+ end
+
+ it "returns start point when the range is Time...Time(excluded end point)" do
+ time_start = Time.now
+ time_end = Time.now + 1.0
+ (time_start...time_end).min.should.equal?(time_start)
+ end
+
+ it "returns the start point for endless ranges" do
+ eval("(1..)").min.should == 1
+ eval("(1.0...)").min.should == 1.0
+ end
+
+ it "raises RangeError when called on an beginless range" do
+ -> { (..1).min }.should.raise(RangeError)
+ end
+end
+
+describe "Range#min given a block" do
+ it "passes each pair of values in the range to the block" do
+ acc = []
+ (1..10).min {|a,b| acc << [a,b]; a }
+ acc.flatten!
+ (1..10).each do |value|
+ acc.include?(value).should == true
+ end
+ end
+
+ it "passes each pair of elements to the block where the first argument is the current element, and the last is the first element" do
+ acc = []
+ (1..5).min {|a,b| acc << [a,b]; a }
+ acc.should == [[2, 1], [3, 1], [4, 1], [5, 1]]
+ end
+
+ it "calls #> and #< on the return value of the block" do
+ obj = mock('obj')
+ obj.should_receive(:>).exactly(2).times
+ obj.should_receive(:<).exactly(2).times
+ (1..3).min {|a,b| obj }
+ end
+
+ it "returns the element the block determines to be the minimum" do
+ (1..3).min {|a,b| -3 }.should == 3
+ end
+
+ it "returns nil when the start point is greater than the endpoint" do
+ (100..10).min {|x,y| x <=> y}.should == nil
+ ('z'..'l').min {|x,y| x <=> y}.should == nil
+ (7...7).min {|x,y| x <=> y}.should == nil
+ end
+
+ it "raises RangeError when called with custom comparison method on an endless range" do
+ -> { eval("(1..)").min {|a, b| a} }.should.raise(RangeError)
+ end
+end
diff --git a/spec/ruby/core/range/minmax_spec.rb b/spec/ruby/core/range/minmax_spec.rb
new file mode 100644
index 0000000000..16c7626ea3
--- /dev/null
+++ b/spec/ruby/core/range/minmax_spec.rb
@@ -0,0 +1,130 @@
+require_relative '../../spec_helper'
+
+describe 'Range#minmax' do
+ before(:each) do
+ @x = mock('x')
+ @y = mock('y')
+
+ @x.should_receive(:<=>).with(@y).any_number_of_times.and_return(-1) # x < y
+ @x.should_receive(:<=>).with(@x).any_number_of_times.and_return(0) # x == x
+ @y.should_receive(:<=>).with(@x).any_number_of_times.and_return(1) # y > x
+ @y.should_receive(:<=>).with(@y).any_number_of_times.and_return(0) # y == y
+ end
+
+ describe 'on an inclusive range' do
+ it 'should raise RangeError on an endless range without iterating the range' do
+ @x.should_not_receive(:succ)
+
+ range = (@x..)
+
+ -> { range.minmax }.should.raise(RangeError, 'cannot get the maximum of endless range')
+ end
+
+ it 'raises RangeError or ArgumentError on a beginless range' do
+ range = (..@x)
+
+ -> { range.minmax }.should.raise(StandardError) { |e|
+ if RangeError === e
+ # error from #min
+ -> { raise e }.should.raise(RangeError, 'cannot get the minimum of beginless range')
+ else
+ # error from #max
+ -> { raise e }.should.raise(ArgumentError, 'comparison of NilClass with MockObject failed')
+ end
+ }
+ end
+
+ it 'should return beginning of range if beginning and end are equal without iterating the range' do
+ @x.should_not_receive(:succ)
+
+ (@x..@x).minmax.should == [@x, @x]
+ end
+
+ it 'should return nil pair if beginning is greater than end without iterating the range' do
+ @y.should_not_receive(:succ)
+
+ (@y..@x).minmax.should == [nil, nil]
+ end
+
+ it 'should return the minimum and maximum values for a non-numeric range without iterating the range' do
+ @x.should_not_receive(:succ)
+
+ (@x..@y).minmax.should == [@x, @y]
+ end
+
+ it 'should return the minimum and maximum values for a numeric range' do
+ (1..3).minmax.should == [1, 3]
+ end
+
+ it 'should return the minimum and maximum values for a numeric range without iterating the range' do
+ # We cannot set expectations on integers,
+ # so we "prevent" iteration by picking a value that would iterate until the spec times out.
+ range_end = Float::INFINITY
+
+ (1..range_end).minmax.should == [1, range_end]
+ end
+
+ it 'should return the minimum and maximum values according to the provided block by iterating the range' do
+ @x.should_receive(:succ).once.and_return(@y)
+
+ (@x..@y).minmax { |x, y| - (x <=> y) }.should == [@y, @x]
+ end
+ end
+
+ describe 'on an exclusive range' do
+ it 'should raise RangeError on an endless range' do
+ @x.should_not_receive(:succ)
+ range = (@x...)
+
+ -> { range.minmax }.should.raise(RangeError, 'cannot get the maximum of endless range')
+ end
+
+ it 'should raise RangeError on a beginless range' do
+ range = (...@x)
+
+ -> { range.minmax }.should.raise(RangeError,
+ /cannot get the maximum of beginless range with custom comparison method|cannot get the minimum of beginless range/)
+ end
+
+ it 'should return nil pair if beginning and end are equal without iterating the range' do
+ @x.should_not_receive(:succ)
+
+ (@x...@x).minmax.should == [nil, nil]
+ end
+
+ it 'should return nil pair if beginning is greater than end without iterating the range' do
+ @y.should_not_receive(:succ)
+
+ (@y...@x).minmax.should == [nil, nil]
+ end
+
+ it 'should return the minimum and maximum values for a non-numeric range by iterating the range' do
+ @x.should_receive(:succ).once.and_return(@y)
+
+ (@x...@y).minmax.should == [@x, @x]
+ end
+
+ it 'should return the minimum and maximum values for a numeric range' do
+ (1...3).minmax.should == [1, 2]
+ end
+
+ it 'should return the minimum and maximum values for a numeric range without iterating the range' do
+ # We cannot set expectations on integers,
+ # so we "prevent" iteration by picking a value that would iterate until the spec times out.
+ range_end = bignum_value
+
+ (1...range_end).minmax.should == [1, range_end - 1]
+ end
+
+ it 'raises TypeError if the end value is not an integer' do
+ range = (0...Float::INFINITY)
+ -> { range.minmax }.should.raise(TypeError, 'cannot exclude non Integer end value')
+ end
+
+ it 'should return the minimum and maximum values according to the provided block by iterating the range' do
+ @x.should_receive(:succ).once.and_return(@y)
+
+ (@x...@y).minmax { |x, y| - (x <=> y) }.should == [@x, @x]
+ end
+ end
+end
diff --git a/spec/ruby/core/range/new_spec.rb b/spec/ruby/core/range/new_spec.rb
new file mode 100644
index 0000000000..9a35f28c7e
--- /dev/null
+++ b/spec/ruby/core/range/new_spec.rb
@@ -0,0 +1,77 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Range.new" do
+ it "constructs a range using the given start and end" do
+ range = Range.new('a', 'c')
+ range.should == ('a'..'c')
+
+ range.first.should == 'a'
+ range.last.should == 'c'
+ end
+
+ it "includes the end object when the third parameter is omitted or false" do
+ Range.new('a', 'c').to_a.should == ['a', 'b', 'c']
+ Range.new(1, 3).to_a.should == [1, 2, 3]
+
+ Range.new('a', 'c', false).to_a.should == ['a', 'b', 'c']
+ Range.new(1, 3, false).to_a.should == [1, 2, 3]
+
+ Range.new('a', 'c', true).to_a.should == ['a', 'b']
+ Range.new(1, 3, 1).to_a.should == [1, 2]
+
+ Range.new(1, 3, mock('[1,2]')).to_a.should == [1, 2]
+ Range.new(1, 3, :test).to_a.should == [1, 2]
+ end
+
+ it "raises an ArgumentError when the given start and end can't be compared by using #<=>" do
+ -> { Range.new(1, mock('x')) }.should.raise(ArgumentError)
+ -> { Range.new(mock('x'), mock('y')) }.should.raise(ArgumentError)
+
+ b = mock('x')
+ (a = mock('nil')).should_receive(:<=>).with(b).and_return(nil)
+ -> { Range.new(a, b) }.should.raise(ArgumentError)
+ end
+
+ it "does not rescue exception raised in #<=> when compares the given start and end" do
+ b = mock('a')
+ a = mock('b')
+ a.should_receive(:<=>).with(b).and_raise(RangeSpecs::ComparisonError)
+
+ -> { Range.new(a, b) }.should.raise(RangeSpecs::ComparisonError)
+ end
+
+ describe "beginless/endless range" do
+ it "allows beginless left boundary" do
+ range = Range.new(nil, 1)
+ range.begin.should == nil
+ end
+
+ it "distinguishes ranges with included and excluded right boundary" do
+ range_exclude = Range.new(nil, 1, true)
+ range_include = Range.new(nil, 1, false)
+
+ range_exclude.should_not == range_include
+ end
+
+ it "allows endless right boundary" do
+ range = Range.new(1, nil)
+ range.end.should == nil
+ end
+
+ it "distinguishes ranges with included and excluded right boundary" do
+ range_exclude = Range.new(1, nil, true)
+ range_include = Range.new(1, nil, false)
+
+ range_exclude.should_not == range_include
+ end
+
+ it "creates a frozen range if the class is Range.class" do
+ Range.new(1, 2).should.frozen?
+ end
+
+ it "does not create a frozen range if the class is not Range.class" do
+ Class.new(Range).new(1, 2).should_not.frozen?
+ end
+ end
+end
diff --git a/spec/ruby/core/range/overlap_spec.rb b/spec/ruby/core/range/overlap_spec.rb
new file mode 100644
index 0000000000..201cd2b1ff
--- /dev/null
+++ b/spec/ruby/core/range/overlap_spec.rb
@@ -0,0 +1,87 @@
+require_relative '../../spec_helper'
+
+describe "Range#overlap?" do
+ it "returns true if other Range overlaps self" do
+ (0..2).overlap?(1..3).should == true
+ (1..3).overlap?(0..2).should == true
+ (0..2).overlap?(0..2).should == true
+ (0..3).overlap?(1..2).should == true
+ (1..2).overlap?(0..3).should == true
+
+ ('a'..'c').overlap?('b'..'d').should == true
+ end
+
+ it "returns false if other Range does not overlap self" do
+ (0..2).overlap?(3..4).should == false
+ (0..2).overlap?(-4..-1).should == false
+
+ ('a'..'c').overlap?('d'..'f').should == false
+ end
+
+ it "raises TypeError when called with non-Range argument" do
+ -> {
+ (0..2).overlap?(1)
+ }.should.raise(TypeError, "wrong argument type Integer (expected Range)")
+ end
+
+ it "returns true when beginningless and endless Ranges overlap" do
+ (0..2).overlap?(..3).should == true
+ (0..2).overlap?(..1).should == true
+ (0..2).overlap?(..0).should == true
+
+ (..3).overlap?(0..2).should == true
+ (..1).overlap?(0..2).should == true
+ (..0).overlap?(0..2).should == true
+
+ (0..2).overlap?(-1..).should == true
+ (0..2).overlap?(1..).should == true
+ (0..2).overlap?(2..).should == true
+
+ (-1..).overlap?(0..2).should == true
+ (1..).overlap?(0..2).should == true
+ (2..).overlap?(0..2).should == true
+
+ (0..).overlap?(2..).should == true
+ (..0).overlap?(..2).should == true
+ end
+
+ it "returns false when beginningless and endless Ranges do not overlap" do
+ (0..2).overlap?(..-1).should == false
+ (0..2).overlap?(3..).should == false
+
+ (..-1).overlap?(0..2).should == false
+ (3..).overlap?(0..2).should == false
+ end
+
+ it "returns false when Ranges are not compatible" do
+ (0..2).overlap?('a'..'d').should == false
+ end
+
+ it "return false when self is empty" do
+ (2..0).overlap?(1..3).should == false
+ (2...2).overlap?(1..3).should == false
+ (1...1).overlap?(1...1).should == false
+ (2..0).overlap?(2..0).should == false
+
+ ('c'..'a').overlap?('b'..'d').should == false
+ ('a'...'a').overlap?('b'..'d').should == false
+ ('b'...'b').overlap?('b'...'b').should == false
+ ('c'...'a').overlap?('c'...'a').should == false
+ end
+
+ it "return false when other Range is empty" do
+ (1..3).overlap?(2..0).should == false
+ (1..3).overlap?(2...2).should == false
+
+ ('b'..'d').overlap?('c'..'a').should == false
+ ('b'..'d').overlap?('c'...'c').should == false
+ end
+
+ it "takes into account exclusive end" do
+ (0...2).overlap?(2..4).should == false
+ (2..4).overlap?(0...2).should == false
+
+ ('a'...'c').overlap?('c'..'e').should == false
+ ('c'..'e').overlap?('a'...'c').should == false
+ end
+end
diff --git a/spec/ruby/core/range/percent_spec.rb b/spec/ruby/core/range/percent_spec.rb
new file mode 100644
index 0000000000..5ec6770ddb
--- /dev/null
+++ b/spec/ruby/core/range/percent_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Range#%" do
+ it "works as a Range#step" do
+ aseq = (1..10) % 2
+ aseq.class.should == Enumerator::ArithmeticSequence
+ aseq.begin.should == 1
+ aseq.end.should == 10
+ aseq.step.should == 2
+ aseq.to_a.should == [1, 3, 5, 7, 9]
+ end
+
+ it "produces an arithmetic sequence with a percent sign in #inspect" do
+ ((1..10) % 2).inspect.should == "((1..10).%(2))"
+ end
+end
diff --git a/spec/ruby/core/range/range_spec.rb b/spec/ruby/core/range/range_spec.rb
new file mode 100644
index 0000000000..8e9433f8c1
--- /dev/null
+++ b/spec/ruby/core/range/range_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Range" do
+ it "includes Enumerable" do
+ Range.include?(Enumerable).should == true
+ end
+end
diff --git a/spec/ruby/core/range/reverse_each_spec.rb b/spec/ruby/core/range/reverse_each_spec.rb
new file mode 100644
index 0000000000..49790e8b0a
--- /dev/null
+++ b/spec/ruby/core/range/reverse_each_spec.rb
@@ -0,0 +1,125 @@
+require_relative '../../spec_helper'
+
+describe "Range#reverse_each" do
+ it "traverses the Range in reverse order and passes each element to block" do
+ a = []
+ (1..3).reverse_each { |i| a << i }
+ a.should == [3, 2, 1]
+
+ a = []
+ (1...3).reverse_each { |i| a << i }
+ a.should == [2, 1]
+ end
+
+ it "returns self" do
+ r = (1..3)
+ r.reverse_each { |x| }.should.equal?(r)
+ end
+
+ it "returns an Enumerator if no block given" do
+ enum = (1..3).reverse_each
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == [3, 2, 1]
+ end
+
+ it "raises a TypeError for endless Ranges of Integers" do
+ -> {
+ (1..).reverse_each.take(3)
+ }.should.raise(TypeError, "can't iterate from NilClass")
+ end
+
+ it "raises a TypeError for endless Ranges of non-Integers" do
+ -> {
+ ("a"..).reverse_each.take(3)
+ }.should.raise(TypeError, "can't iterate from NilClass")
+ end
+
+ context "Integer boundaries" do
+ it "supports beginningless Ranges" do
+ (..5).reverse_each.take(3).should == [5, 4, 3]
+ end
+ end
+
+ context "non-Integer boundaries" do
+ it "uses #succ to iterate a Range of non-Integer elements" do
+ y = mock('y')
+ x = mock('x')
+
+ x.should_receive(:succ).any_number_of_times.and_return(y)
+ x.should_receive(:<=>).with(y).any_number_of_times.and_return(-1)
+ x.should_receive(:<=>).with(x).any_number_of_times.and_return(0)
+ y.should_receive(:<=>).with(x).any_number_of_times.and_return(1)
+ y.should_receive(:<=>).with(y).any_number_of_times.and_return(0)
+
+ a = []
+ (x..y).each { |i| a << i }
+ a.should == [x, y]
+ end
+
+ it "uses #succ to iterate a Range of Strings" do
+ a = []
+ ('A'..'D').reverse_each { |i| a << i }
+ a.should == ['D','C','B','A']
+ end
+
+ it "uses #succ to iterate a Range of Symbols" do
+ a = []
+ (:A..:D).reverse_each { |i| a << i }
+ a.should == [:D, :C, :B, :A]
+ end
+
+ it "raises a TypeError when `begin` value does not respond to #succ" do
+ -> { (Time.now..Time.now).reverse_each { |x| x } }.should.raise(TypeError, /can't iterate from Time/)
+ -> { (//..//).reverse_each { |x| x } }.should.raise(TypeError, /can't iterate from Regexp/)
+ -> { ([]..[]).reverse_each { |x| x } }.should.raise(TypeError, /can't iterate from Array/)
+ end
+
+ it "does not support beginningless Ranges" do
+ -> {
+ (..'a').reverse_each { |x| x }
+ }.should.raise(TypeError, /can't iterate from NilClass/)
+ end
+ end
+
+ context "when no block is given" do
+ describe "returned Enumerator size" do
+ it "returns the Range size when Range size is finite" do
+ (1..3).reverse_each.size.should == 3
+ (1...3).reverse_each.size.should == 2
+
+ (1..3.3).reverse_each.size.should == 3
+ (1...3.3).reverse_each.size.should == 3
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns a size when it is not iterable" do
+ (1.1..3).reverse_each.size.should == 2
+ (1.1..3.3).reverse_each.size.should == 3
+ (1.1..nil).reverse_each.size.should == Float::INFINITY
+ (nil..3.3).reverse_each.size.should == Float::INFINITY
+ (nil..nil).reverse_each.size.should == nil
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "raises TypeError when the range is not iterable" do
+ -> { (1.1..3).reverse_each.size }.should.raise(TypeError, /can't iterate from Integer/)
+ -> { (1.1..3.3).reverse_each.size }.should.raise(TypeError, /can't iterate from Float/)
+ -> { (1.1..nil).reverse_each.size }.should.raise(TypeError, /can't iterate from NilClass/)
+ -> { (nil..3.3).reverse_each.size }.should.raise(TypeError, /can't iterate from Float/)
+ -> { (nil..nil).reverse_each.size }.should.raise(TypeError, /can't iterate from NilClass/)
+ end
+ end
+
+ ruby_bug "#20936", "3.4"..."4.0" do
+ it "returns Infinity when Range size is infinite" do
+ (..3).reverse_each.size.should == Float::INFINITY
+ end
+ end
+
+ it "returns nil when Range size is unknown" do
+ ('a'..'z').reverse_each.size.should == nil
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/range/shared/begin.rb b/spec/ruby/core/range/shared/begin.rb
new file mode 100644
index 0000000000..f660e3faf9
--- /dev/null
+++ b/spec/ruby/core/range/shared/begin.rb
@@ -0,0 +1,10 @@
+describe :range_begin, shared: true do
+ it "returns the first element of self" do
+ (-1..1).send(@method).should == -1
+ (0..1).send(@method).should == 0
+ (0xffff...0xfffff).send(@method).should == 65535
+ ('Q'..'T').send(@method).should == 'Q'
+ ('Q'...'T').send(@method).should == 'Q'
+ (0.5..2.4).send(@method).should == 0.5
+ end
+end
diff --git a/spec/ruby/core/range/shared/cover.rb b/spec/ruby/core/range/shared/cover.rb
new file mode 100644
index 0000000000..189f3da4bf
--- /dev/null
+++ b/spec/ruby/core/range/shared/cover.rb
@@ -0,0 +1,193 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :range_cover, shared: true do
+ it "uses the range element's <=> to make the comparison" do
+ a = mock('a')
+ a.should_receive(:<=>).twice.and_return(-1,-1)
+ (a..'z').send(@method, 'b').should == true
+ end
+
+ it "uses a continuous inclusion test" do
+ ('a'..'f').send(@method, 'aa').should == true
+ ('a'..'f').send(@method, 'babe').should == true
+ ('a'..'f').send(@method, 'baby').should == true
+ ('a'..'f').send(@method, 'ga').should == false
+ (-10..-2).send(@method, -2.5).should == true
+ end
+
+ describe "on string elements" do
+ it "returns true if other is matched by element.succ" do
+ ('a'..'c').send(@method, 'b').should == true
+ ('a'...'c').send(@method, 'b').should == true
+ end
+
+ it "returns true if other is not matched by element.succ" do
+ ('a'..'c').send(@method, 'bc').should == true
+ ('a'...'c').send(@method, 'bc').should == true
+ end
+ end
+
+ describe "with weird succ" do
+ describe "when included end value" do
+ before :each do
+ @range = RangeSpecs::TenfoldSucc.new(1)..RangeSpecs::TenfoldSucc.new(99)
+ end
+
+ it "returns false if other is less than first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should == false
+ end
+
+ it "returns true if other is equal as first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should == true
+ end
+
+ it "returns true if other is matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should == true
+ end
+
+ it "returns true if other is not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should == true
+ end
+
+ it "returns true if other is equal as last element but not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should == true
+ end
+
+ it "returns false if other is greater than last element but matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should == false
+ end
+ end
+
+ describe "when excluded end value" do
+ before :each do
+ @range = RangeSpecs::TenfoldSucc.new(1)...RangeSpecs::TenfoldSucc.new(99)
+ end
+
+ it "returns false if other is less than first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should == false
+ end
+
+ it "returns true if other is equal as first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should == true
+ end
+
+ it "returns true if other is matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should == true
+ end
+
+ it "returns true if other is not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should == true
+ end
+
+ it "returns false if other is equal as last element but not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should == false
+ end
+
+ it "returns false if other is greater than last element but matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should == false
+ end
+ end
+ end
+end
+
+describe :range_cover_subrange, shared: true do
+ context "range argument" do
+ it "accepts range argument" do
+ (0..10).send(@method, (3..7)).should == true
+ (0..10).send(@method, (3..15)).should == false
+ (0..10).send(@method, (-2..7)).should == false
+
+ (1.1..7.9).send(@method, (2.5..6.5)).should == true
+ (1.1..7.9).send(@method, (2.5..8.5)).should == false
+ (1.1..7.9).send(@method, (0.5..6.5)).should == false
+
+ ('c'..'i').send(@method, ('d'..'f')).should == true
+ ('c'..'i').send(@method, ('d'..'z')).should == false
+ ('c'..'i').send(@method, ('a'..'f')).should == false
+
+ range_10_100 = RangeSpecs::TenfoldSucc.new(10)..RangeSpecs::TenfoldSucc.new(100)
+ range_20_90 = RangeSpecs::TenfoldSucc.new(20)..RangeSpecs::TenfoldSucc.new(90)
+ range_20_110 = RangeSpecs::TenfoldSucc.new(20)..RangeSpecs::TenfoldSucc.new(110)
+ range_0_90 = RangeSpecs::TenfoldSucc.new(0)..RangeSpecs::TenfoldSucc.new(90)
+
+ range_10_100.send(@method, range_20_90).should == true
+ range_10_100.send(@method, range_20_110).should == false
+ range_10_100.send(@method, range_0_90).should == false
+ end
+
+ it "supports boundaries of different comparable types" do
+ (0..10).send(@method, (3.1..7.9)).should == true
+ (0..10).send(@method, (3.1..15.9)).should == false
+ (0..10).send(@method, (-2.1..7.9)).should == false
+ end
+
+ it "returns false if types are not comparable" do
+ (0..10).send(@method, ('a'..'z')).should == false
+ (0..10).send(@method, (RangeSpecs::TenfoldSucc.new(0)..RangeSpecs::TenfoldSucc.new(100))).should == false
+ end
+
+ it "honors exclusion of right boundary (:exclude_end option)" do
+ # Integer
+ (0..10).send(@method, (0..10)).should == true
+ (0...10).send(@method, (0...10)).should == true
+
+ (0..10).send(@method, (0...10)).should == true
+ (0...10).send(@method, (0..10)).should == false
+
+ (0...11).send(@method, (0..10)).should == true
+ (0..10).send(@method, (0...11)).should == true
+
+ # Float
+ (0..10.1).send(@method, (0..10.1)).should == true
+ (0...10.1).send(@method, (0...10.1)).should == true
+
+ (0..10.1).send(@method, (0...10.1)).should == true
+ (0...10.1).send(@method, (0..10.1)).should == false
+
+ (0...11.1).send(@method, (0..10.1)).should == true
+ (0..10.1).send(@method, (0...11.1)).should == false
+ end
+ end
+
+ it "allows self to be a beginless range" do
+ (...10).send(@method, (3..7)).should == true
+ (...10).send(@method, (3..15)).should == false
+
+ (..7.9).send(@method, (2.5..6.5)).should == true
+ (..7.9).send(@method, (2.5..8.5)).should == false
+
+ (..'i').send(@method, ('d'..'f')).should == true
+ (..'i').send(@method, ('d'..'z')).should == false
+ end
+
+ it "allows self to be a endless range" do
+ eval("(0...)").send(@method, (3..7)).should == true
+ eval("(5...)").send(@method, (3..15)).should == false
+
+ eval("(1.1..)").send(@method, (2.5..6.5)).should == true
+ eval("(3.3..)").send(@method, (2.5..8.5)).should == false
+
+ eval("('a'..)").send(@method, ('d'..'f')).should == true
+ eval("('p'..)").send(@method, ('d'..'z')).should == false
+ end
+
+ it "accepts beginless range argument" do
+ (..10).send(@method, (...10)).should == true
+ (0..10).send(@method, (...10)).should == false
+
+ (1.1..7.9).send(@method, (...10.5)).should == false
+
+ ('c'..'i').send(@method, (..'i')).should == false
+ end
+
+ it "accepts endless range argument" do
+ eval("(0..)").send(@method, eval("(0...)")).should == true
+ (0..10).send(@method, eval("(0...)")).should == false
+
+ (1.1..7.9).send(@method, eval("(0.8...)")).should == false
+
+ ('c'..'i').send(@method, eval("('a'..)")).should == false
+ end
+end
diff --git a/spec/ruby/core/range/shared/cover_and_include.rb b/spec/ruby/core/range/shared/cover_and_include.rb
new file mode 100644
index 0000000000..97721a7307
--- /dev/null
+++ b/spec/ruby/core/range/shared/cover_and_include.rb
@@ -0,0 +1,86 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+
+describe :range_cover_and_include, shared: true do
+ it "returns true if other is an element of self" do
+ (0..5).send(@method, 2).should == true
+ (-5..5).send(@method, 0).should == true
+ (-1...1).send(@method, 10.5).should == false
+ (-10..-2).send(@method, -2.5).should == true
+ ('C'..'X').send(@method, 'M').should == true
+ ('C'..'X').send(@method, 'A').should == false
+ ('B'...'W').send(@method, 'W').should == false
+ ('B'...'W').send(@method, 'Q').should == true
+ (0xffff..0xfffff).send(@method, 0xffffd).should == true
+ (0xffff..0xfffff).send(@method, 0xfffd).should == false
+ (0.5..2.4).send(@method, 2).should == true
+ (0.5..2.4).send(@method, 2.5).should == false
+ (0.5..2.4).send(@method, 2.4).should == true
+ (0.5...2.4).send(@method, 2.4).should == false
+ end
+
+ it "returns true if other is an element of self for endless ranges" do
+ (1..).send(@method, 2.4).should == true
+ (0.5...).send(@method, 2.4).should == true
+ end
+
+ it "returns true if other is an element of self for beginless ranges" do
+ (..10).send(@method, 2.4).should == true
+ (...10.5).send(@method, 2.4).should == true
+ end
+
+ it "returns false if values are not comparable" do
+ (1..10).send(@method, nil).should == false
+ (1...10).send(@method, nil).should == false
+
+ (..10).send(@method, nil).should == false
+ (...10).send(@method, nil).should == false
+
+ (1..).send(@method, nil).should == false
+ (1...).send(@method, nil).should == false
+ end
+
+ it "compares values using <=>" do
+ rng = (1..5)
+ m = mock("int")
+ m.should_receive(:coerce).and_return([1, 2])
+ m.should_receive(:<=>).and_return(1)
+
+ rng.send(@method, m).should == false
+ end
+
+ it "raises an ArgumentError without exactly one argument" do
+ ->{ (1..2).send(@method) }.should.raise(ArgumentError)
+ ->{ (1..2).send(@method, 1, 2) }.should.raise(ArgumentError)
+ end
+
+ it "returns true if argument is equal to the first value of the range" do
+ (0..5).send(@method, 0).should == true
+ ('f'..'s').send(@method, 'f').should == true
+ end
+
+ it "returns true if argument is equal to the last value of the range" do
+ (0..5).send(@method, 5).should == true
+ (0...5).send(@method, 4).should == true
+ ('f'..'s').send(@method, 's').should == true
+ end
+
+ it "returns true if argument is less than the last value of the range and greater than the first value" do
+ (20..30).send(@method, 28).should == true
+ ('e'..'h').send(@method, 'g').should == true
+ end
+
+ it "returns true if argument is sole element in the range" do
+ (30..30).send(@method, 30).should == true
+ end
+
+ it "returns false if range is empty" do
+ (30...30).send(@method, 30).should == false
+ (30...30).send(@method, nil).should == false
+ end
+
+ it "returns false if the range does not contain the argument" do
+ ('A'..'C').send(@method, 20.9).should == false
+ ('A'...'C').send(@method, 'C').should == false
+ end
+end
diff --git a/spec/ruby/core/range/shared/end.rb b/spec/ruby/core/range/shared/end.rb
new file mode 100644
index 0000000000..b26394fe31
--- /dev/null
+++ b/spec/ruby/core/range/shared/end.rb
@@ -0,0 +1,10 @@
+describe :range_end, shared: true do
+ it "end returns the last element of self" do
+ (-1..1).send(@method).should == 1
+ (0..1).send(@method).should == 1
+ ("A".."Q").send(@method).should == "Q"
+ ("A"..."Q").send(@method).should == "Q"
+ (0xffff...0xfffff).send(@method).should == 1048575
+ (0.5..2.4).send(@method).should == 2.4
+ end
+end
diff --git a/spec/ruby/core/range/shared/equal_value.rb b/spec/ruby/core/range/shared/equal_value.rb
new file mode 100644
index 0000000000..363c6be558
--- /dev/null
+++ b/spec/ruby/core/range/shared/equal_value.rb
@@ -0,0 +1,51 @@
+require_relative '../fixtures/classes'
+
+describe :range_eql, shared: true do
+ it "returns true if other has same begin, end, and exclude_end? values" do
+ (0..2).send(@method, 0..2).should == true
+ ('G'..'M').send(@method,'G'..'M').should == true
+ (0.5..2.4).send(@method, 0.5..2.4).should == true
+ (5..10).send(@method, Range.new(5,10)).should == true
+ ('D'..'V').send(@method, Range.new('D','V')).should == true
+ (0.5..2.4).send(@method, Range.new(0.5, 2.4)).should == true
+ (0xffff..0xfffff).send(@method, 0xffff..0xfffff).should == true
+ (0xffff..0xfffff).send(@method, Range.new(0xffff,0xfffff)).should == true
+
+ a = RangeSpecs::Xs.new(3)..RangeSpecs::Xs.new(5)
+ b = Range.new(RangeSpecs::Xs.new(3), RangeSpecs::Xs.new(5))
+ a.send(@method, b).should == true
+ end
+
+ it "returns false if one of the attributes differs" do
+ ('Q'..'X').send(@method, 'A'..'C').should == false
+ ('Q'...'X').send(@method, 'Q'..'W').should == false
+ ('Q'..'X').send(@method, 'Q'...'X').should == false
+ (0.5..2.4).send(@method, 0.5...2.4).should == false
+ (1482..1911).send(@method, 1482...1911).should == false
+ (0xffff..0xfffff).send(@method, 0xffff...0xfffff).should == false
+
+ a = RangeSpecs::Xs.new(3)..RangeSpecs::Xs.new(5)
+ b = Range.new(RangeSpecs::Ys.new(3), RangeSpecs::Ys.new(5))
+ a.send(@method, b).should == false
+ end
+
+ it "returns false if other is not a Range" do
+ (1..10).send(@method, 1).should == false
+ (1..10).send(@method, 'a').should == false
+ (1..10).send(@method, mock('x')).should == false
+ end
+
+ it "returns true for subclasses of Range" do
+ Range.new(1, 2).send(@method, RangeSpecs::MyRange.new(1, 2)).should == true
+
+ a = Range.new(RangeSpecs::Xs.new(3), RangeSpecs::Xs.new(5))
+ b = RangeSpecs::MyRange.new(RangeSpecs::Xs.new(3), RangeSpecs::Xs.new(5))
+ a.send(@method, b).should == true
+ end
+
+ it "works for endless Ranges" do
+ eval("(1..)").send(@method, eval("(1..)")).should == true
+ eval("(0.5...)").send(@method, eval("(0.5...)")).should == true
+ eval("(1..)").send(@method, eval("(1...)")).should == false
+ end
+end
diff --git a/spec/ruby/core/range/shared/include.rb b/spec/ruby/core/range/shared/include.rb
new file mode 100644
index 0000000000..5f0db48008
--- /dev/null
+++ b/spec/ruby/core/range/shared/include.rb
@@ -0,0 +1,91 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :range_include, shared: true do
+ describe "on string elements" do
+ it "returns true if other is matched by element.succ" do
+ ('a'..'c').send(@method, 'b').should == true
+ ('a'...'c').send(@method, 'b').should == true
+ end
+
+ it "returns false if other is not matched by element.succ" do
+ ('a'..'c').send(@method, 'bc').should == false
+ ('a'...'c').send(@method, 'bc').should == false
+ end
+ end
+
+ describe "with weird succ" do
+ describe "when included end value" do
+ before :each do
+ @range = RangeSpecs::TenfoldSucc.new(1)..RangeSpecs::TenfoldSucc.new(99)
+ end
+
+ it "returns false if other is less than first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should == false
+ end
+
+ it "returns true if other is equal as first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should == true
+ end
+
+ it "returns true if other is matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should == true
+ end
+
+ it "returns false if other is not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should == false
+ end
+
+ it "returns false if other is equal as last element but not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should == false
+ end
+
+ it "returns false if other is greater than last element but matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should == false
+ end
+ end
+
+ describe "when excluded end value" do
+ before :each do
+ @range = RangeSpecs::TenfoldSucc.new(1)...RangeSpecs::TenfoldSucc.new(99)
+ end
+
+ it "returns false if other is less than first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(0)).should == false
+ end
+
+ it "returns true if other is equal as first element" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(1)).should == true
+ end
+
+ it "returns true if other is matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(10)).should == true
+ end
+
+ it "returns false if other is not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(2)).should == false
+ end
+
+ it "returns false if other is equal as last element but not matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(99)).should == false
+ end
+
+ it "returns false if other is greater than last element but matched by element.succ" do
+ @range.send(@method, RangeSpecs::TenfoldSucc.new(100)).should == false
+ end
+ end
+ end
+
+ describe "with Time endpoints" do
+ it "uses cover? logic" do
+ now = Time.now
+ range = (now..(now + 60))
+
+ range.include?(now).should == true
+ range.include?(now - 1).should == false
+ range.include?(now + 60).should == true
+ range.include?(now + 61).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/range/size_spec.rb b/spec/ruby/core/range/size_spec.rb
new file mode 100644
index 0000000000..3a8843b99d
--- /dev/null
+++ b/spec/ruby/core/range/size_spec.rb
@@ -0,0 +1,92 @@
+require_relative '../../spec_helper'
+
+describe "Range#size" do
+ it "returns the number of elements in the range" do
+ (1..16).size.should == 16
+ (1...16).size.should == 15
+ end
+
+ it "returns 0 if last is less than first" do
+ (16..0).size.should == 0
+ end
+
+ it 'returns Float::INFINITY for increasing, infinite ranges' do
+ (0..Float::INFINITY).size.should == Float::INFINITY
+ end
+
+ it 'returns Float::INFINITY for endless ranges if the start is numeric' do
+ eval("(1..)").size.should == Float::INFINITY
+ end
+
+ it 'returns nil for endless ranges if the start is not numeric' do
+ eval("('z'..)").size.should == nil
+ end
+
+ ruby_version_is ""..."3.4" do
+ it 'returns Float::INFINITY for all beginless ranges if the end is numeric' do
+ (..1).size.should == Float::INFINITY
+ (...0.5).size.should == Float::INFINITY
+ end
+
+ it 'returns nil for all beginless ranges if the end is not numeric' do
+ (...'o').size.should == nil
+ end
+
+ it 'returns nil if the start and the end is both nil' do
+ (nil..nil).size.should == nil
+ end
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns the number of elements in the range" do
+ (1.0..16.0).size.should == 16
+ (1.0...16.0).size.should == 15
+ (1.0..15.9).size.should == 15
+ (1.1..16.0).size.should == 15
+ (1.1..15.9).size.should == 15
+ end
+
+ it "returns 0 if last is less than first" do
+ (16.0..0.0).size.should == 0
+ (Float::INFINITY..0).size.should == 0
+ end
+
+ it 'returns Float::INFINITY for increasing, infinite ranges' do
+ (-Float::INFINITY..0).size.should == Float::INFINITY
+ (-Float::INFINITY..Float::INFINITY).size.should == Float::INFINITY
+ end
+
+ it 'returns Float::INFINITY for endless ranges if the start is numeric' do
+ eval("(0.5...)").size.should == Float::INFINITY
+ end
+
+ it 'returns nil for endless ranges if the start is not numeric' do
+ eval("([]...)").size.should == nil
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it 'raises TypeError if a range is not iterable' do
+ -> { (1.0..16.0).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (1.0...16.0).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (1.0..15.9).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (1.1..16.0).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (1.1..15.9).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (16.0..0.0).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (Float::INFINITY..0).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (-Float::INFINITY..0).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (-Float::INFINITY..Float::INFINITY).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (..1).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (...0.5).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (..nil).size }.should.raise(TypeError, /can't iterate from/)
+ -> { (...'o').size }.should.raise(TypeError, /can't iterate from/)
+ -> { eval("(0.5...)").size }.should.raise(TypeError, /can't iterate from/)
+ -> { eval("([]...)").size }.should.raise(TypeError, /can't iterate from/)
+ end
+ end
+
+ it "returns nil if first and last are not Numeric" do
+ (:a..:z).size.should == nil
+ ('a'..'z').size.should == nil
+ end
+end
diff --git a/spec/ruby/core/range/step_spec.rb b/spec/ruby/core/range/step_spec.rb
new file mode 100644
index 0000000000..faab95d88d
--- /dev/null
+++ b/spec/ruby/core/range/step_spec.rb
@@ -0,0 +1,725 @@
+require_relative '../../spec_helper'
+
+describe "Range#step" do
+ before :each do
+ ScratchPad.record []
+ end
+
+ it "returns self" do
+ r = 1..2
+ r.step { }.should.equal?(r)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "calls #to_int to coerce step to an Integer" do
+ obj = mock("Range#step")
+ obj.should_receive(:to_int).and_return(1)
+
+ (1..2).step(obj) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1, 2])
+ end
+
+ it "raises a TypeError if step does not respond to #to_int" do
+ obj = mock("Range#step non-integer")
+
+ -> { (1..2).step(obj) { } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ obj = mock("Range#step non-integer")
+ obj.should_receive(:to_int).and_return("1")
+
+ -> { (1..2).step(obj) { } }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the first element does not respond to #succ" do
+ obj = mock("Range#step non-comparable")
+ obj.should_receive(:<=>).with(obj).and_return(1)
+
+ -> { (obj..obj).step { |x| x } }.should.raise(TypeError)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "calls #coerce to coerce step to an Integer" do
+ obj = mock("Range#step")
+ obj.should_receive(:coerce).at_least(:once).and_return([1, 2])
+
+ (1..3).step(obj) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1, 3])
+ end
+
+ it "raises a TypeError if step does not respond to #coerce" do
+ obj = mock("Range#step non-coercible")
+
+ -> { (1..2).step(obj) { } }.should.raise(TypeError)
+ end
+ end
+
+ it "raises an ArgumentError if step is 0" do
+ -> { (-1..1).step(0) { |x| x } }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if step is 0.0" do
+ -> { (-1..1).step(0.0) { |x| x } }.should.raise(ArgumentError)
+ end
+
+ ruby_version_is "3.4" do
+ it "does not iterate if step is 0 for bounded non-numeric ranges" do
+ t = Time.utc(2023, 2, 24)
+ (t..t + 1).step(0) { |x| ScratchPad << x }
+ ScratchPad.recorded.should == []
+ end
+
+ it "raises an ArgumentError when iterating a beginless range" do
+ -> { (..10).step(1) { break } }.should.raise(ArgumentError,
+ "#step iteration for beginless ranges is meaningless")
+ end
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "raises an ArgumentError if step is negative" do
+ -> { (-1..1).step(-2) { |x| x } }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "with inclusive end" do
+ describe "and Integer values" do
+ it "yields Integer values incremented by 1 and less than or equal to end when not passed a step" do
+ (-2..2).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2, -1, 0, 1, 2])
+ end
+
+ it "yields Integer values incremented by an Integer step" do
+ (-5..5).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5, -3, -1, 1, 3, 5])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-2..2).step(1.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -0.5, 1.0])
+ end
+
+ ruby_version_is "3.4" do
+ it "does not iterate if step is negative for forward range" do
+ (-1..1).step(-1) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([])
+ end
+
+ it "iterates backward if step is negative for backward range" do
+ (1..-1).step(-1) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1, 0, -1])
+ end
+ end
+ end
+
+ describe "and Float values" do
+ it "yields Float values incremented by 1 and less than or equal to end when not passed a step" do
+ (-2.0..2.0).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0, 2.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5.0..5.0).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0, 5.0])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-1.0..1.0).step(0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5, 1.0])
+ end
+
+ it "returns Float values of 'step * n + begin <= end'" do
+ (1.0..6.4).step(1.8) { |x| ScratchPad << x }
+ (1.0..12.7).step(1.3) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1.0, 2.8, 4.6, 6.4, 1.0, 2.3, 3.6,
+ 4.9, 6.2, 7.5, 8.8, 10.1, 11.4, 12.7])
+ end
+
+ it "handles infinite values at either end" do
+ (-Float::INFINITY..0.0).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 }
+ ScratchPad.recorded.should.eql?([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY])
+
+ ScratchPad.record []
+ (0.0..Float::INFINITY).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 }
+ ScratchPad.recorded.should.eql?([0.0, 2.0, 4.0])
+ end
+
+ ruby_version_is "3.4" do
+ it "does not iterate if step is negative for forward range" do
+ (-1.0..1.0).step(-0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([])
+ end
+
+ it "iterates backward if step is negative for backward range" do
+ (1.0..-1.0).step(-0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1.0, 0.5, 0.0, -0.5, -1.0])
+ end
+ end
+ end
+
+ describe "and Integer, Float values" do
+ it "yields Float values incremented by 1 and less than or equal to end when not passed a step" do
+ (-2..2.0).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0, 2.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5..5.0).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0, 5.0])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-1..1.0).step(0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5, 1.0])
+ end
+ end
+
+ describe "and Float, Integer values" do
+ it "yields Float values incremented by 1 and less than or equal to end when not passed a step" do
+ (-2.0..2).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0, 2.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5.0..5).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0, 5.0])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-1.0..1).step(0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5, 1.0])
+ end
+ end
+
+ describe "and String values" do
+ it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
+ ("A".."E").step { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "B", "C", "D", "E"]
+ end
+
+ it "yields String values incremented by #succ called Integer step times" do
+ ("A".."G").step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "C", "E", "G"]
+ end
+
+ it "raises a TypeError when passed a Float step" do
+ -> { ("A".."G").step(2.0) { } }.should.raise(TypeError)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "calls #succ on begin and each element returned by #succ" do
+ obj = mock("Range#step String start")
+ obj.should_receive(:<=>).exactly(3).times.and_return(-1, -1, -1, 0)
+ obj.should_receive(:succ).exactly(2).times.and_return(obj)
+
+ (obj..obj).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should == [obj, obj, obj]
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "yields String values adjusted by step and less than or equal to end" do
+ ("A".."AAA").step("A") { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "AA", "AAA"]
+ end
+
+ it "raises a TypeError when passed an incompatible type step" do
+ -> { ("A".."G").step([]) { } }.should.raise(TypeError)
+ end
+
+ it "calls #+ on begin and each element returned by #+" do
+ start = mock("Range#step String start")
+ stop = mock("Range#step String stop")
+
+ mid1 = mock("Range#step String mid1")
+ mid2 = mock("Range#step String mid2")
+
+ step = mock("Range#step String step")
+
+ # Deciding on the direction of iteration
+ start.should_receive(:<=>).with(stop).at_least(:twice).and_return(-1)
+ # Deciding whether the step moves iteration in the right direction
+ start.should_receive(:<=>).with(mid1).and_return(-1)
+ # Iteration 1
+ start.should_receive(:+).at_least(:once).with(step).and_return(mid1)
+ # Iteration 2
+ mid1.should_receive(:<=>).with(stop).and_return(-1)
+ mid1.should_receive(:+).with(step).and_return(mid2)
+ # Iteration 3
+ mid2.should_receive(:<=>).with(stop).and_return(0)
+
+ (start..stop).step(step) { |x| ScratchPad << x }
+ ScratchPad.recorded.should == [start, mid1, mid2]
+ end
+
+ it "iterates backward if the step is decreasing values, and the range is backward" do
+ start = mock("Range#step String start")
+ stop = mock("Range#step String stop")
+
+ mid1 = mock("Range#step String mid1")
+ mid2 = mock("Range#step String mid2")
+
+ step = mock("Range#step String step")
+
+ # Deciding on the direction of iteration
+ start.should_receive(:<=>).with(stop).at_least(:twice).and_return(1)
+ # Deciding whether the step moves iteration in the right direction
+ start.should_receive(:<=>).with(mid1).and_return(1)
+ # Iteration 1
+ start.should_receive(:+).at_least(:once).with(step).and_return(mid1)
+ # Iteration 2
+ mid1.should_receive(:<=>).with(stop).and_return(1)
+ mid1.should_receive(:+).with(step).and_return(mid2)
+ # Iteration 3
+ mid2.should_receive(:<=>).with(stop).and_return(0)
+
+ (start..stop).step(step) { |x| ScratchPad << x }
+ ScratchPad.recorded.should == [start, mid1, mid2]
+ end
+
+ it "does no iteration of the direction of the range and of the step don't match" do
+ start = mock("Range#step String start")
+ stop = mock("Range#step String stop")
+
+ mid1 = mock("Range#step String mid1")
+ mid2 = mock("Range#step String mid2")
+
+ step = mock("Range#step String step")
+
+ # Deciding on the direction of iteration: stop > start
+ start.should_receive(:<=>).with(stop).at_least(:twice).and_return(1)
+ # Deciding whether the step moves iteration in the right direction
+ # start + step < start, the direction is opposite to the range's
+ start.should_receive(:+).with(step).and_return(mid1)
+ start.should_receive(:<=>).with(mid1).and_return(-1)
+
+ (start..stop).step(step) { |x| ScratchPad << x }
+ ScratchPad.recorded.should == []
+ end
+ end
+ end
+ end
+
+ describe "with exclusive end" do
+ describe "and Integer values" do
+ it "yields Integer values incremented by 1 and less than end when not passed a step" do
+ (-2...2).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2, -1, 0, 1])
+ end
+
+ it "yields Integer values incremented by an Integer step" do
+ (-5...5).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5, -3, -1, 1, 3])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-2...2).step(1.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -0.5, 1.0])
+ end
+ end
+
+ describe "and Float values" do
+ it "yields Float values incremented by 1 and less than end when not passed a step" do
+ (-2.0...2.0).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5.0...5.0).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-1.0...1.0).step(0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5])
+ end
+
+ it "returns Float values of 'step * n + begin < end'" do
+ (1.0...6.4).step(1.8) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1.0, 2.8, 4.6])
+ end
+
+ it "correctly handles values near the upper limit" do # https://bugs.ruby-lang.org/issues/16612
+ (1.0...55.6).step(18.2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1.0, 19.2, 37.4, 55.599999999999994])
+
+ (1.0...55.6).step(18.2).size.should == 4
+ end
+
+ it "handles infinite values at either end" do
+ (-Float::INFINITY...0.0).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 }
+ ScratchPad.recorded.should.eql?([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY])
+
+ ScratchPad.record []
+ (0.0...Float::INFINITY).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 }
+ ScratchPad.recorded.should.eql?([0.0, 2.0, 4.0])
+ end
+
+ ruby_version_is "3.4" do
+ it "iterates backward with exclusive end if step is negative" do
+ (1.0...-1.0).step(-0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([1.0, 0.5, 0.0, -0.5])
+ end
+ end
+ end
+
+ describe "and Integer, Float values" do
+ it "yields Float values incremented by 1 and less than end when not passed a step" do
+ (-2...2.0).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5...5.0).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0])
+ end
+
+ it "yields an Float and then Float values incremented by a Float step" do
+ (-1...1.0).step(0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5])
+ end
+ end
+
+ describe "and Float, Integer values" do
+ it "yields Float values incremented by 1 and less than end when not passed a step" do
+ (-2.0...2).step { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5.0...5).step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-1.0...1).step(0.5) { |x| ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5])
+ end
+ end
+
+ describe "and String values" do
+ ruby_version_is ""..."3.4" do
+ it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
+ ("A"..."E").step { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "B", "C", "D"]
+ end
+
+ it "yields String values incremented by #succ called Integer step times" do
+ ("A"..."G").step(2) { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "C", "E"]
+ end
+
+ it "raises a TypeError when passed a Float step" do
+ -> { ("A"..."G").step(2.0) { } }.should.raise(TypeError)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "yields String values adjusted by step and less than or equal to end" do
+ ("A"..."AAA").step("A") { |x| ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "AA"]
+ end
+
+ it "raises a TypeError when passed an incompatible type step" do
+ -> { ("A".."G").step([]) { } }.should.raise(TypeError)
+ end
+ end
+ end
+ end
+
+ describe "with an endless range" do
+ describe "and Integer values" do
+ it "yield Integer values incremented by 1 when not passed a step" do
+ (-2..).step { |x| break if x > 2; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2, -1, 0, 1, 2])
+
+ ScratchPad.record []
+ (-2...).step { |x| break if x > 2; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2, -1, 0, 1, 2])
+ end
+
+ it "yields Integer values incremented by an Integer step" do
+ (-5..).step(2) { |x| break if x > 3; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5, -3, -1, 1, 3])
+
+ ScratchPad.record []
+ (-5...).step(2) { |x| break if x > 3; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5, -3, -1, 1, 3])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-2..).step(1.5) { |x| break if x > 1.0; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -0.5, 1.0])
+
+ ScratchPad.record []
+ (-2..).step(1.5) { |x| break if x > 1.0; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -0.5, 1.0])
+ end
+ end
+
+ describe "and Float values" do
+ it "yields Float values incremented by 1 and less than end when not passed a step" do
+ (-2.0..).step { |x| break if x > 1.5; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0])
+
+ ScratchPad.record []
+ (-2.0...).step { |x| break if x > 1.5; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-2.0, -1.0, 0.0, 1.0])
+ end
+
+ it "yields Float values incremented by an Integer step" do
+ (-5.0..).step(2) { |x| break if x > 3.5; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0])
+
+ ScratchPad.record []
+ (-5.0...).step(2) { |x| break if x > 3.5; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-5.0, -3.0, -1.0, 1.0, 3.0])
+ end
+
+ it "yields Float values incremented by a Float step" do
+ (-1.0..).step(0.5) { |x| break if x > 0.6; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5])
+
+ ScratchPad.record []
+ (-1.0...).step(0.5) { |x| break if x > 0.6; ScratchPad << x }
+ ScratchPad.recorded.should.eql?([-1.0, -0.5, 0.0, 0.5])
+ end
+
+ it "computes each value independently to avoid accumulating floating-point errors" do
+ result = []
+ (0.0..).step(0.1) { |x| result << x; break if result.size == 20 }
+ expected = 20.times.map { |i| i * 0.1 + 0.0 }
+ result.should.eql?(expected)
+ end
+
+ it "handles infinite values at the start" do
+ (-Float::INFINITY..).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 }
+ ScratchPad.recorded.should.eql?([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY])
+
+ ScratchPad.record []
+ (-Float::INFINITY...).step(2) { |x| ScratchPad << x; break if ScratchPad.recorded.size == 3 }
+ ScratchPad.recorded.should.eql?([-Float::INFINITY, -Float::INFINITY, -Float::INFINITY])
+ end
+ end
+
+ describe "and String values" do
+ it "yields String values incremented by #succ and less than or equal to end when not passed a step" do
+ ('A'..).step { |x| break if x > "D"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "B", "C", "D"]
+
+ ScratchPad.record []
+ ('A'...).step { |x| break if x > "D"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "B", "C", "D"]
+ end
+
+ it "yields String values incremented by #succ called Integer step times" do
+ ('A'..).step(2) { |x| break if x > "F"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "C", "E"]
+
+ ScratchPad.record []
+ ('A'...).step(2) { |x| break if x > "F"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "C", "E"]
+ end
+
+ it "raises a TypeError when passed a Float step" do
+ -> { ('A'..).step(2.0) { } }.should.raise(TypeError)
+ -> { ('A'...).step(2.0) { } }.should.raise(TypeError)
+ end
+
+ ruby_version_is "3.4" do
+ it "yields String values adjusted by step" do
+ ('A'..).step("A") { |x| break if x > "AAA"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "AA", "AAA"]
+
+ ScratchPad.record []
+ ('A'...).step("A") { |x| break if x > "AAA"; ScratchPad << x }
+ ScratchPad.recorded.should == ["A", "AA", "AAA"]
+ end
+
+ it "raises a TypeError when passed an incompatible type step" do
+ -> { ('A'..).step([]) { } }.should.raise(TypeError)
+ -> { ('A'...).step([]) { } }.should.raise(TypeError)
+ end
+ end
+ end
+ end
+
+ describe "when no block is given" do
+ it "raises an ArgumentError if step is 0" do
+ -> { (-1..1).step(0) }.should.raise(ArgumentError)
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ ruby_version_is ""..."3.4" do
+ it "raises a TypeError if step does not respond to #to_int" do
+ obj = mock("Range#step non-integer")
+ -> { (1..2).step(obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ obj = mock("Range#step non-integer")
+ obj.should_receive(:to_int).and_return("1")
+ -> { (1..2).step(obj) }.should.raise(TypeError)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "does not raise if step is incompatible" do
+ obj = mock("Range#step non-integer")
+ -> { (1..2).step(obj) }.should_not.raise
+ end
+ end
+
+ it "returns the ceil of range size divided by the number of steps" do
+ (1..10).step(4).size.should == 3
+ (1..10).step(3).size.should == 4
+ (1..10).step(2).size.should == 5
+ (1..10).step(1).size.should == 10
+ (-5..5).step(2).size.should == 6
+ (1...10).step(4).size.should == 3
+ (1...10).step(3).size.should == 3
+ (1...10).step(2).size.should == 5
+ (1...10).step(1).size.should == 9
+ (-5...5).step(2).size.should == 5
+ end
+
+ it "returns the ceil of range size divided by the number of steps even if step is negative" do
+ (-1..1).step(-1).size.should == 0
+ (1..-1).step(-1).size.should == 3
+ end
+
+ it "returns the correct number of steps when one of the arguments is a float" do
+ (-1..1.0).step(0.5).size.should == 5
+ (-1.0...1.0).step(0.5).size.should == 4
+ end
+
+ it "returns the range size when there's no step_size" do
+ (-2..2).step.size.should == 5
+ (-2.0..2.0).step.size.should == 5
+ (-2..2.0).step.size.should == 5
+ (-2.0..2).step.size.should == 5
+ (1.0..6.4).step(1.8).size.should == 4
+ (1.0..12.7).step(1.3).size.should == 10
+ (-2...2).step.size.should == 4
+ (-2.0...2.0).step.size.should == 4
+ (-2...2.0).step.size.should == 4
+ (-2.0...2).step.size.should == 4
+ (1.0...6.4).step(1.8).size.should == 3
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns nil with begin and end are String" do
+ ("A".."E").step(2).size.should == nil
+ ("A"..."E").step(2).size.should == nil
+ ("A".."E").step.size.should == nil
+ ("A"..."E").step.size.should == nil
+ end
+
+ it "return nil and not raises a TypeError if the first element does not respond to #succ" do
+ obj = mock("Range#step non-comparable")
+ obj.should_receive(:<=>).with(obj).and_return(1)
+ enum = (obj..obj).step
+ -> { enum.size }.should_not.raise
+ enum.size.should == nil
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "returns nil with begin and end are String" do
+ ("A".."E").step("A").size.should == nil
+ ("A"..."E").step("A").size.should == nil
+ end
+
+ it "return nil and not raises a TypeError if the first element is not of compatible type" do
+ obj = mock("Range#step non-comparable")
+ obj.should_receive(:<=>).with(obj).and_return(1)
+ enum = (obj..obj).step(obj)
+ -> { enum.size }.should_not.raise
+ enum.size.should == nil
+ end
+ end
+ end
+
+ # We use .take below to ensure the enumerator works
+ # because that's an Enumerable method and so it uses the Enumerator behavior
+ # not just a method overridden in Enumerator::ArithmeticSequence.
+ describe "type" do
+ context "when both begin and end are numerics" do
+ it "returns an instance of Enumerator::ArithmeticSequence" do
+ (1..10).step.class.should == Enumerator::ArithmeticSequence
+ (1..10).step(3).take(4).should == [1, 4, 7, 10]
+ end
+ end
+
+ context "when begin is not defined and end is numeric" do
+ it "returns an instance of Enumerator::ArithmeticSequence" do
+ (..10).step.class.should == Enumerator::ArithmeticSequence
+ end
+ end
+
+ context "when range is endless" do
+ it "returns an instance of Enumerator::ArithmeticSequence when begin is numeric" do
+ (1..).step.class.should == Enumerator::ArithmeticSequence
+ (1..).step(2).take(3).should == [1, 3, 5]
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns an instance of Enumerator when begin is not numeric" do
+ ("a"..).step.class.should == Enumerator
+ ("a"..).step(2).take(3).should == %w[a c e]
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "returns an instance of Enumerator when begin is not numeric" do
+ ("a"..).step("a").class.should == Enumerator
+ ("a"..).step("a").take(3).should == %w[a aa aaa]
+ end
+ end
+ end
+
+ context "when range is beginless and endless" do
+ ruby_version_is ""..."3.4" do
+ it "returns an instance of Enumerator" do
+ Range.new(nil, nil).step.class.should == Enumerator
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError" do
+ -> { Range.new(nil, nil).step(1) }.should.raise(ArgumentError,
+ "#step for non-numeric beginless ranges is meaningless")
+ end
+ end
+ end
+
+ context "when range is beginless and finite" do
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError if step is non-numeric" do
+ -> { (..10).step("a") }.should.raise(ArgumentError,
+ "#step for non-numeric beginless ranges is meaningless")
+ end
+ end
+ end
+
+ context "when begin and end are not numerics" do
+ ruby_version_is ""..."3.4" do
+ it "returns an instance of Enumerator" do
+ ("a".."z").step.class.should == Enumerator
+ ("a".."z").step(3).take(4).should == %w[a d g j]
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "returns an instance of Enumerator" do
+ ("a".."z").step("a").class.should == Enumerator
+ ("a".."z").step("a").take(4).should == %w[a aa aaa aaaa]
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/range/to_a_spec.rb b/spec/ruby/core/range/to_a_spec.rb
new file mode 100644
index 0000000000..6221ae5f71
--- /dev/null
+++ b/spec/ruby/core/range/to_a_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "Range#to_a" do
+ it "converts self to an array" do
+ (-5..5).to_a.should == [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5]
+ ('A'..'D').to_a.should == ['A','B','C','D']
+ ('A'...'D').to_a.should == ['A','B','C']
+ (0xfffd...0xffff).to_a.should == [0xfffd,0xfffe]
+ -> { (0.5..2.4).to_a }.should.raise(TypeError)
+ end
+
+ it "returns empty array for descending-ordered" do
+ (5..-5).to_a.should == []
+ ('D'..'A').to_a.should == []
+ ('D'...'A').to_a.should == []
+ (0xffff...0xfffd).to_a.should == []
+ end
+
+ it "works with Ranges of 64-bit integers" do
+ large = 1 << 40
+ (large..large+1).to_a.should == [1099511627776, 1099511627777]
+ end
+
+ it "works with Ranges of Symbols" do
+ (:A..:z).to_a.size.should == 58
+ end
+
+ it "works for non-ASCII ranges" do
+ ('Σ'..'Ω').to_a.should == ["Σ", "Τ", "Υ", "Φ", "Χ", "Ψ", "Ω"]
+ end
+
+ it "throws an exception for endless ranges" do
+ -> { eval("(1..)").to_a }.should.raise(RangeError)
+ end
+
+ it "throws an exception for beginless ranges" do
+ -> { (..1).to_a }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/range/to_s_spec.rb b/spec/ruby/core/range/to_s_spec.rb
new file mode 100644
index 0000000000..460c330912
--- /dev/null
+++ b/spec/ruby/core/range/to_s_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "Range#to_s" do
+ it "provides a printable form of self" do
+ (0..21).to_s.should == "0..21"
+ (-8..0).to_s.should == "-8..0"
+ (-411..959).to_s.should == "-411..959"
+ ('A'..'Z').to_s.should == 'A..Z'
+ ('A'...'Z').to_s.should == 'A...Z'
+ (0xfff..0xfffff).to_s.should == "4095..1048575"
+ (0.5..2.4).to_s.should == "0.5..2.4"
+ end
+
+ it "can show endless ranges" do
+ eval("(1..)").to_s.should == "1.."
+ eval("(1.0...)").to_s.should == "1.0..."
+ end
+
+ it "can show beginless ranges" do
+ (..1).to_s.should == "..1"
+ (...1.0).to_s.should == "...1.0"
+ end
+end
diff --git a/spec/ruby/core/range/to_set_spec.rb b/spec/ruby/core/range/to_set_spec.rb
new file mode 100644
index 0000000000..ac81d2cc4b
--- /dev/null
+++ b/spec/ruby/core/range/to_set_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative '../enumerable/fixtures/classes'
+
+describe "Range#to_set" do
+ it "returns a new Set created from self" do
+ (1..4).to_set.should == Set[1, 2, 3, 4]
+ (1...4).to_set.should == Set[1, 2, 3]
+ end
+
+ it "passes down passed blocks" do
+ (1..3).to_set { |x| x * x }.should == Set[1, 4, 9]
+ end
+
+ it "raises a TypeError for a beginningless range" do
+ -> {
+ (..0).to_set
+ }.should.raise(TypeError, "can't iterate from NilClass")
+ end
+
+ ruby_version_is "4.0" do
+ it "raises a RangeError if the range is endless" do
+ -> { (1..).to_set }.should.raise(RangeError, "cannot convert endless range to a set")
+ -> { (1...).to_set }.should.raise(RangeError, "cannot convert endless range to a set")
+ end
+ end
+
+ context "given positional arguments" do
+ ruby_version_is ""..."4.0" do
+ it "instantiates an object of provided as the first argument set class" do
+ set = (1..3).to_set(EnumerableSpecs::SetSubclass)
+ set.should.is_a?(EnumerableSpecs::SetSubclass)
+ set.to_a.sort.should == [1, 2, 3]
+ end
+ end
+
+ ruby_version_is "4.0"..."4.1" do
+ it "instantiates an object of provided as the first argument set class and warns" do
+ -> {
+ set = (1..3).to_set(EnumerableSpecs::SetSubclass)
+ set.should.is_a?(EnumerableSpecs::SetSubclass)
+ set.to_a.sort.should == [1, 2, 3]
+ }.should complain(/warning: passing arguments to Enumerable#to_set is deprecated/)
+ end
+ end
+
+ ruby_version_is "4.1" do
+ it "does not accept any positional argument" do
+ -> {
+ (1..3).to_set(EnumerableSpecs::SetSubclass)
+ }.should.raise(ArgumentError, "wrong number of arguments (given 1, expected 0)")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/abs_spec.rb b/spec/ruby/core/rational/abs_spec.rb
new file mode 100644
index 0000000000..54099aa14d
--- /dev/null
+++ b/spec/ruby/core/rational/abs_spec.rb
@@ -0,0 +1,6 @@
+require_relative "../../spec_helper"
+require_relative 'shared/abs'
+
+describe "Rational#abs" do
+ it_behaves_like :rational_abs, :abs
+end
diff --git a/spec/ruby/core/rational/ceil_spec.rb b/spec/ruby/core/rational/ceil_spec.rb
new file mode 100644
index 0000000000..0464eab101
--- /dev/null
+++ b/spec/ruby/core/rational/ceil_spec.rb
@@ -0,0 +1,48 @@
+require_relative "../../spec_helper"
+require_relative "../integer/shared/integer_ceil_precision"
+
+describe "Rational#ceil" do
+ context "with values equal to integers" do
+ it_behaves_like :integer_ceil_precision, :Rational
+ end
+
+ before do
+ @rational = Rational(2200, 7)
+ end
+
+ describe "with no arguments (precision = 0)" do
+ it "returns the Integer value rounded toward positive infinity" do
+ @rational.ceil.should.eql? 315
+
+ Rational(1, 2).ceil.should.eql? 1
+ Rational(-1, 2).ceil.should.eql? 0
+ Rational(1, 1).ceil.should.eql? 1
+ end
+ end
+
+ describe "with a precision < 0" do
+ it "moves the rounding point n decimal places left, returning an Integer" do
+ @rational.ceil(-3).should.eql? 1000
+ @rational.ceil(-2).should.eql? 400
+ @rational.ceil(-1).should.eql? 320
+
+ Rational(100, 2).ceil(-1).should.eql? 50
+ Rational(100, 2).ceil(-2).should.eql? 100
+ Rational(-100, 2).ceil(-1).should.eql?(-50)
+ Rational(-100, 2).ceil(-2).should.eql?(0)
+ end
+ end
+
+ describe "with precision > 0" do
+ it "moves the rounding point n decimal places right, returning a Rational" do
+ @rational.ceil(1).should.eql? Rational(3143, 10)
+ @rational.ceil(2).should.eql? Rational(31429, 100)
+ @rational.ceil(3).should.eql? Rational(157143, 500)
+
+ Rational(100, 2).ceil(1).should.eql? Rational(50, 1)
+ Rational(100, 2).ceil(2).should.eql? Rational(50, 1)
+ Rational(-100, 2).ceil(1).should.eql? Rational(-50, 1)
+ Rational(-100, 2).ceil(2).should.eql? Rational(-50, 1)
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/comparison_spec.rb b/spec/ruby/core/rational/comparison_spec.rb
new file mode 100644
index 0000000000..482e904989
--- /dev/null
+++ b/spec/ruby/core/rational/comparison_spec.rb
@@ -0,0 +1,93 @@
+require_relative "../../spec_helper"
+require_relative 'fixtures/rational'
+
+describe "Rational#<=> when passed a Rational object" do
+ it "returns 1 when self is greater than the passed argument" do
+ (Rational(4, 4) <=> Rational(3, 4)).should.equal?(1)
+ (Rational(-3, 4) <=> Rational(-4, 4)).should.equal?(1)
+ end
+
+ it "returns 0 when self is equal to the passed argument" do
+ (Rational(4, 4) <=> Rational(4, 4)).should.equal?(0)
+ (Rational(-3, 4) <=> Rational(-3, 4)).should.equal?(0)
+ end
+
+ it "returns -1 when self is less than the passed argument" do
+ (Rational(3, 4) <=> Rational(4, 4)).should.equal?(-1)
+ (Rational(-4, 4) <=> Rational(-3, 4)).should.equal?(-1)
+ end
+end
+
+describe "Rational#<=> when passed an Integer object" do
+ it "returns 1 when self is greater than the passed argument" do
+ (Rational(4, 4) <=> 0).should.equal?(1)
+ (Rational(4, 4) <=> -10).should.equal?(1)
+ (Rational(-3, 4) <=> -1).should.equal?(1)
+ end
+
+ it "returns 0 when self is equal to the passed argument" do
+ (Rational(4, 4) <=> 1).should.equal?(0)
+ (Rational(-8, 4) <=> -2).should.equal?(0)
+ end
+
+ it "returns -1 when self is less than the passed argument" do
+ (Rational(3, 4) <=> 1).should.equal?(-1)
+ (Rational(-4, 4) <=> 0).should.equal?(-1)
+ end
+end
+
+describe "Rational#<=> when passed a Float object" do
+ it "returns 1 when self is greater than the passed argument" do
+ (Rational(4, 4) <=> 0.5).should.equal?(1)
+ (Rational(4, 4) <=> -1.5).should.equal?(1)
+ (Rational(-3, 4) <=> -0.8).should.equal?(1)
+ end
+
+ it "returns 0 when self is equal to the passed argument" do
+ (Rational(4, 4) <=> 1.0).should.equal?(0)
+ (Rational(-6, 4) <=> -1.5).should.equal?(0)
+ end
+
+ it "returns -1 when self is less than the passed argument" do
+ (Rational(3, 4) <=> 1.2).should.equal?(-1)
+ (Rational(-4, 4) <=> 0.5).should.equal?(-1)
+ end
+end
+
+describe "Rational#<=> when passed an Object that responds to #coerce" do
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational <=> obj
+ end
+
+ it "calls #<=> on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:<=>).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ (rational <=> obj).should == :result
+ end
+
+ it "does not rescue exception raised in other#coerce" do
+ b = mock("numeric with failed #coerce")
+ b.should_receive(:coerce).and_raise(RationalSpecs::CoerceError)
+
+ -> { Rational(3, 4) <=> b }.should.raise(RationalSpecs::CoerceError)
+ end
+end
+
+describe "Rational#<=> when passed a non-Numeric Object that doesn't respond to #coerce" do
+ it "returns nil" do
+ (Rational <=> mock("Object")).should == nil
+ end
+end
diff --git a/spec/ruby/core/rational/denominator_spec.rb b/spec/ruby/core/rational/denominator_spec.rb
new file mode 100644
index 0000000000..ba6e936d60
--- /dev/null
+++ b/spec/ruby/core/rational/denominator_spec.rb
@@ -0,0 +1,14 @@
+require_relative "../../spec_helper"
+
+describe "Rational#denominator" do
+ it "returns the denominator" do
+ Rational(3, 4).denominator.should.equal?(4)
+ Rational(3, -4).denominator.should.equal?(4)
+
+ Rational(1, bignum_value).denominator.should == bignum_value
+ end
+
+ it "returns 1 if no denominator was given" do
+ Rational(80).denominator.should == 1
+ end
+end
diff --git a/spec/ruby/core/rational/div_spec.rb b/spec/ruby/core/rational/div_spec.rb
new file mode 100644
index 0000000000..a679663543
--- /dev/null
+++ b/spec/ruby/core/rational/div_spec.rb
@@ -0,0 +1,54 @@
+require_relative "../../spec_helper"
+
+describe "Rational#div" do
+ it "returns an Integer" do
+ Rational(229, 21).div(82).should.is_a?(Integer)
+ end
+
+ it "raises an ArgumentError if passed more than one argument" do
+ -> { Rational(3, 4).div(2,3) }.should.raise(ArgumentError)
+ end
+
+ # See http://redmine.ruby-lang.org/issues/show/1648
+ it "raises a TypeError if passed a non-numeric argument" do
+ -> { Rational(3, 4).div([]) }.should.raise(TypeError)
+ end
+end
+
+describe "Rational#div passed a Rational" do
+ it "performs integer division and returns the result" do
+ Rational(2, 3).div(Rational(2, 3)).should == 1
+ Rational(-2, 9).div(Rational(-9, 2)).should == 0
+ end
+
+ it "raises a ZeroDivisionError when the argument has a numerator of 0" do
+ -> { Rational(3, 4).div(Rational(0, 3)) }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a ZeroDivisionError when the argument has a numerator of 0.0" do
+ -> { Rational(3, 4).div(Rational(0.0, 3)) }.should.raise(ZeroDivisionError)
+ end
+end
+
+describe "Rational#div passed an Integer" do
+ it "performs integer division and returns the result" do
+ Rational(2, 1).div(1).should == 2
+ Rational(25, 5).div(-50).should == -1
+ end
+
+ it "raises a ZeroDivisionError when the argument is 0" do
+ -> { Rational(3, 4).div(0) }.should.raise(ZeroDivisionError)
+ end
+end
+
+describe "Rational#div passed a Float" do
+ it "performs integer division and returns the result" do
+ Rational(2, 3).div(30.333).should == 0
+ Rational(2, 9).div(Rational(-8.6)).should == -1
+ Rational(3.12).div(0.5).should == 6
+ end
+
+ it "raises a ZeroDivisionError when the argument is 0.0" do
+ -> { Rational(3, 4).div(0.0) }.should.raise(ZeroDivisionError)
+ end
+end
diff --git a/spec/ruby/core/rational/divide_spec.rb b/spec/ruby/core/rational/divide_spec.rb
new file mode 100644
index 0000000000..c45d1fca2f
--- /dev/null
+++ b/spec/ruby/core/rational/divide_spec.rb
@@ -0,0 +1,74 @@
+require_relative "../../spec_helper"
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Rational#/" do
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational / obj
+ end
+
+ it "calls #/ on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:/).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ (rational / obj).should == :result
+ end
+
+ it_behaves_like :rational_arithmetic_exception_in_coerce, :/
+end
+
+describe "Rational#/ when passed an Integer" do
+ it "returns self divided by other as a Rational" do
+ (Rational(3, 4) / 2).should.eql?(Rational(3, 8))
+ (Rational(2, 4) / 2).should.eql?(Rational(1, 4))
+ (Rational(6, 7) / -2).should.eql?(Rational(-3, 7))
+ end
+
+ it "raises a ZeroDivisionError when passed 0" do
+ -> { Rational(3, 4) / 0 }.should.raise(ZeroDivisionError)
+ end
+end
+
+describe "Rational#/ when passed a Rational" do
+ it "returns self divided by other as a Rational" do
+ (Rational(3, 4) / Rational(3, 4)).should.eql?(Rational(1, 1))
+ (Rational(2, 4) / Rational(1, 4)).should.eql?(Rational(2, 1))
+
+ (Rational(2, 4) / 2).should == Rational(1, 4)
+ (Rational(6, 7) / -2).should == Rational(-3, 7)
+ end
+
+ it "raises a ZeroDivisionError when passed a Rational with a numerator of 0" do
+ -> { Rational(3, 4) / Rational(0, 1) }.should.raise(ZeroDivisionError)
+ end
+end
+
+describe "Rational#/ when passed a Float" do
+ it "returns self divided by other as a Float" do
+ (Rational(3, 4) / 0.75).should.eql?(1.0)
+ (Rational(3, 4) / 0.25).should.eql?(3.0)
+ (Rational(3, 4) / 0.3).should.eql?(2.5)
+
+ (Rational(-3, 4) / 0.3).should.eql?(-2.5)
+ (Rational(3, -4) / 0.3).should.eql?(-2.5)
+ (Rational(3, 4) / -0.3).should.eql?(-2.5)
+ end
+
+ it "returns infinity when passed 0" do
+ (Rational(3, 4) / 0.0).infinite?.should.eql?(1)
+ (Rational(-3, -4) / 0.0).infinite?.should.eql?(1)
+
+ (Rational(-3, 4) / 0.0).infinite?.should.eql?(-1)
+ (Rational(3, -4) / 0.0).infinite?.should.eql?(-1)
+ end
+end
diff --git a/spec/ruby/core/rational/divmod_spec.rb b/spec/ruby/core/rational/divmod_spec.rb
new file mode 100644
index 0000000000..68f8ecfd2d
--- /dev/null
+++ b/spec/ruby/core/rational/divmod_spec.rb
@@ -0,0 +1,42 @@
+require_relative "../../spec_helper"
+
+describe "Rational#divmod when passed a Rational" do
+ it "returns the quotient as Integer and the remainder as Rational" do
+ Rational(7, 4).divmod(Rational(1, 2)).should.eql?([3, Rational(1, 4)])
+ Rational(7, 4).divmod(Rational(-1, 2)).should.eql?([-4, Rational(-1, 4)])
+ Rational(0, 4).divmod(Rational(4, 3)).should.eql?([0, Rational(0, 1)])
+
+ Rational(bignum_value, 4).divmod(Rational(4, 3)).should.eql?([3458764513820540928, Rational(0, 1)])
+ end
+
+ it "raises a ZeroDivisionError when passed a Rational with a numerator of 0" do
+ -> { Rational(7, 4).divmod(Rational(0, 3)) }.should.raise(ZeroDivisionError)
+ end
+end
+
+describe "Rational#divmod when passed an Integer" do
+ it "returns the quotient as Integer and the remainder as Rational" do
+ Rational(7, 4).divmod(2).should.eql?([0, Rational(7, 4)])
+ Rational(7, 4).divmod(-2).should.eql?([-1, Rational(-1, 4)])
+
+ Rational(bignum_value, 4).divmod(3).should.eql?([1537228672809129301, Rational(1, 1)])
+ end
+
+ it "raises a ZeroDivisionError when passed 0" do
+ -> { Rational(7, 4).divmod(0) }.should.raise(ZeroDivisionError)
+ end
+end
+
+describe "Rational#divmod when passed a Float" do
+ it "returns the quotient as Integer and the remainder as Float" do
+ Rational(7, 4).divmod(0.5).should.eql?([3, 0.25])
+ end
+
+ it "returns the quotient as Integer and the remainder as Float" do
+ Rational(7, 4).divmod(-0.5).should.eql?([-4, -0.25])
+ end
+
+ it "raises a ZeroDivisionError when passed 0" do
+ -> { Rational(7, 4).divmod(0.0) }.should.raise(ZeroDivisionError)
+ end
+end
diff --git a/spec/ruby/core/rational/equal_value_spec.rb b/spec/ruby/core/rational/equal_value_spec.rb
new file mode 100644
index 0000000000..1dd077ea41
--- /dev/null
+++ b/spec/ruby/core/rational/equal_value_spec.rb
@@ -0,0 +1,39 @@
+require_relative "../../spec_helper"
+
+describe "Rational#==" do
+ it "returns the result of calling #== with self on the passed argument" do
+ obj = mock("Object")
+ obj.should_receive(:==).and_return(:result)
+
+ (Rational(3, 4) == obj).should_not == false
+ end
+end
+
+describe "Rational#== when passed a Rational" do
+ it "returns true if self has the same numerator and denominator as the passed argument" do
+ (Rational(3, 4) == Rational(3, 4)).should == true
+ (Rational(-3, -4) == Rational(3, 4)).should == true
+ (Rational(-4, 5) == Rational(4, -5)).should == true
+
+ (Rational(bignum_value, 3) == Rational(bignum_value, 3)).should == true
+ (Rational(-bignum_value, 3) == Rational(bignum_value, -3)).should == true
+ end
+end
+
+describe "Rational#== when passed a Float" do
+ it "converts self to a Float and compares it with the passed argument" do
+ (Rational(3, 4) == 0.75).should == true
+ (Rational(4, 2) == 2.0).should == true
+ (Rational(-4, 2) == -2.0).should == true
+ (Rational(4, -2) == -2.0).should == true
+ end
+end
+
+describe "Rational#== when passed an Integer" do
+ it "returns true if self has the passed argument as numerator and a denominator of 1" do
+ # Rational(x, y) reduces x and y automatically
+ (Rational(4, 2) == 2).should == true
+ (Rational(-4, 2) == -2).should == true
+ (Rational(4, -2) == -2).should == true
+ end
+end
diff --git a/spec/ruby/core/rational/exponent_spec.rb b/spec/ruby/core/rational/exponent_spec.rb
new file mode 100644
index 0000000000..88b4a43796
--- /dev/null
+++ b/spec/ruby/core/rational/exponent_spec.rb
@@ -0,0 +1,236 @@
+require_relative "../../spec_helper"
+
+describe "Rational#**" do
+ describe "when passed Rational" do
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ it "returns Rational(1) if the exponent is Rational(0)" do
+ (Rational(0) ** Rational(0)).should.eql?(Rational(1))
+ (Rational(1) ** Rational(0)).should.eql?(Rational(1))
+ (Rational(3, 4) ** Rational(0)).should.eql?(Rational(1))
+ (Rational(-1) ** Rational(0)).should.eql?(Rational(1))
+ (Rational(-3, 4) ** Rational(0)).should.eql?(Rational(1))
+ (Rational(bignum_value) ** Rational(0)).should.eql?(Rational(1))
+ (Rational(-bignum_value) ** Rational(0)).should.eql?(Rational(1))
+ end
+
+ it "returns self raised to the argument as a Rational if the exponent's denominator is 1" do
+ (Rational(3, 4) ** Rational(1, 1)).should.eql?(Rational(3, 4))
+ (Rational(3, 4) ** Rational(2, 1)).should.eql?(Rational(9, 16))
+ (Rational(3, 4) ** Rational(-1, 1)).should.eql?(Rational(4, 3))
+ (Rational(3, 4) ** Rational(-2, 1)).should.eql?(Rational(16, 9))
+ end
+
+ it "returns self raised to the argument as a Float if the exponent's denominator is not 1" do
+ (Rational(3, 4) ** Rational(4, 3)).should be_close(0.681420222312052, TOLERANCE)
+ (Rational(3, 4) ** Rational(-4, 3)).should be_close(1.46752322173095, TOLERANCE)
+ (Rational(3, 4) ** Rational(4, -3)).should be_close(1.46752322173095, TOLERANCE)
+ end
+
+ it "returns a complex number when self is negative and the passed argument is not 0" do
+ (Rational(-3, 4) ** Rational(-4, 3)).should be_close(Complex(-0.7337616108654732, 1.2709123906625817), TOLERANCE)
+ end
+ end
+ end
+
+ describe "when passed Integer" do
+ it "returns the Rational value of self raised to the passed argument" do
+ (Rational(3, 4) ** 4).should == Rational(81, 256)
+ (Rational(3, 4) ** -4).should == Rational(256, 81)
+ (Rational(-3, 4) ** -4).should == Rational(256, 81)
+ (Rational(3, -4) ** -4).should == Rational(256, 81)
+
+ (Rational(bignum_value, 4) ** 4).should == Rational(452312848583266388373324160190187140051835877600158453279131187530910662656, 1)
+ (Rational(3, bignum_value) ** -4).should == Rational(115792089237316195423570985008687907853269984665640564039457584007913129639936, 81)
+ (Rational(-bignum_value, 4) ** -4).should == Rational(1, 452312848583266388373324160190187140051835877600158453279131187530910662656)
+ (Rational(3, -bignum_value) ** -4).should == Rational(115792089237316195423570985008687907853269984665640564039457584007913129639936, 81)
+ end
+
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ it "returns Rational(1, 1) when the passed argument is 0" do
+ (Rational(3, 4) ** 0).should.eql?(Rational(1, 1))
+ (Rational(-3, 4) ** 0).should.eql?(Rational(1, 1))
+ (Rational(3, -4) ** 0).should.eql?(Rational(1, 1))
+
+ (Rational(bignum_value, 4) ** 0).should.eql?(Rational(1, 1))
+ (Rational(3, -bignum_value) ** 0).should.eql?(Rational(1, 1))
+ end
+ end
+ end
+
+ describe "when passed Bignum" do
+ # #5713
+ it "returns Rational(0) when self is Rational(0) and the exponent is positive" do
+ (Rational(0) ** bignum_value).should.eql?(Rational(0))
+ end
+
+ it "raises ZeroDivisionError when self is Rational(0) and the exponent is negative" do
+ -> { Rational(0) ** -bignum_value }.should.raise(ZeroDivisionError)
+ end
+
+ it "returns Rational(1) when self is Rational(1)" do
+ (Rational(1) ** bignum_value).should.eql?(Rational(1))
+ (Rational(1) ** -bignum_value).should.eql?(Rational(1))
+ end
+
+ it "returns Rational(1) when self is Rational(-1) and the exponent is positive and even" do
+ (Rational(-1) ** bignum_value(0)).should.eql?(Rational(1))
+ (Rational(-1) ** bignum_value(2)).should.eql?(Rational(1))
+ end
+
+ it "returns Rational(-1) when self is Rational(-1) and the exponent is positive and odd" do
+ (Rational(-1) ** bignum_value(1)).should.eql?(Rational(-1))
+ (Rational(-1) ** bignum_value(3)).should.eql?(Rational(-1))
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "returns positive Infinity when self is > 1" do
+ -> {
+ (Rational(2) ** bignum_value).infinite?.should == 1
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ -> {
+ (Rational(fixnum_max) ** bignum_value).infinite?.should == 1
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ end
+
+ it "returns 0.0 when self is > 1 and the exponent is negative" do
+ -> {
+ (Rational(2) ** -bignum_value).should.eql?(0.0)
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ -> {
+ (Rational(fixnum_max) ** -bignum_value).should.eql?(0.0)
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError when self is > 1" do
+ -> {
+ (Rational(2) ** bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ -> {
+ (Rational(fixnum_max) ** bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ end
+
+ it "raises an ArgumentError when self is > 1 and the exponent is negative" do
+ -> {
+ (Rational(2) ** -bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ -> {
+ (Rational(fixnum_max) ** -bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ end
+
+ it "raises an ArgumentError when self is < -1" do
+ -> {
+ (Rational(-2) ** bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ -> {
+ (Rational(fixnum_min) ** bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ end
+
+ it "raises an ArgumentError when self is < -1 and the exponent is negative" do
+ -> {
+ (Rational(-2) ** -bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ -> {
+ (Rational(fixnum_min) ** -bignum_value)
+ }.should.raise(ArgumentError, "exponent is too large")
+ end
+ end
+
+ # Fails on linux due to pow() bugs in glibc: http://sources.redhat.com/bugzilla/show_bug.cgi?id=3866
+ platform_is_not :linux do
+ ruby_version_is ""..."3.4" do
+ it "returns positive Infinity when self < -1" do
+ -> {
+ (Rational(-2) ** bignum_value).infinite?.should == 1
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ -> {
+ (Rational(-2) ** (bignum_value + 1)).infinite?.should == 1
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ -> {
+ (Rational(fixnum_min) ** bignum_value).infinite?.should == 1
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ end
+
+ it "returns 0.0 when self is < -1 and the exponent is negative" do
+ -> {
+ (Rational(-2) ** -bignum_value).should.eql?(0.0)
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ -> {
+ (Rational(fixnum_min) ** -bignum_value).should.eql?(0.0)
+ }.should complain(/warning: in a\*\*b, b may be too big/)
+ end
+ end
+ end
+ end
+
+ describe "when passed Float" do
+ it "returns self converted to Float and raised to the passed argument" do
+ (Rational(3, 1) ** 3.0).should.eql?(27.0)
+ (Rational(3, 1) ** 1.5).should be_close(5.19615242270663, TOLERANCE)
+ (Rational(3, 1) ** -1.5).should be_close(0.192450089729875, TOLERANCE)
+ end
+
+ it "returns a complex number if self is negative and the passed argument is not 0" do
+ (Rational(-3, 2) ** 1.5).should be_close(Complex(0.0, -1.8371173070873836), TOLERANCE)
+ (Rational(3, -2) ** 1.5).should be_close(Complex(0.0, -1.8371173070873836), TOLERANCE)
+ (Rational(3, -2) ** -1.5).should be_close(Complex(0.0, 0.5443310539518174), TOLERANCE)
+ end
+
+ it "returns Complex(1.0) when the passed argument is 0.0" do
+ (Rational(3, 4) ** 0.0).should == Complex(1.0)
+ (Rational(-3, 4) ** 0.0).should == Complex(1.0)
+ (Rational(-3, 4) ** 0.0).should == Complex(1.0)
+ end
+ end
+
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational ** obj
+ end
+
+ it "calls #** on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:**).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ (rational ** obj).should == :result
+ end
+
+ it "raises ZeroDivisionError for Rational(0, 1) passed a negative Integer" do
+ [-1, -4, -9999].each do |exponent|
+ -> { Rational(0, 1) ** exponent }.should.raise(ZeroDivisionError, "divided by 0")
+ end
+ end
+
+ it "raises ZeroDivisionError for Rational(0, 1) passed a negative Rational with denominator 1" do
+ [Rational(-1, 1), Rational(-3, 1)].each do |exponent|
+ -> { Rational(0, 1) ** exponent }.should.raise(ZeroDivisionError, "divided by 0")
+ end
+ end
+
+ # #7513
+ it "raises ZeroDivisionError for Rational(0, 1) passed a negative Rational" do
+ -> { Rational(0, 1) ** Rational(-3, 2) }.should.raise(ZeroDivisionError, "divided by 0")
+ end
+
+ it "returns Infinity for Rational(0, 1) passed a negative Float" do
+ [-1.0, -3.0, -3.14].each do |exponent|
+ (Rational(0, 1) ** exponent).infinite?.should == 1
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/fdiv_spec.rb b/spec/ruby/core/rational/fdiv_spec.rb
new file mode 100644
index 0000000000..118d93dbe7
--- /dev/null
+++ b/spec/ruby/core/rational/fdiv_spec.rb
@@ -0,0 +1,5 @@
+require_relative "../../spec_helper"
+
+describe "Rational#fdiv" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/rational/fixtures/rational.rb b/spec/ruby/core/rational/fixtures/rational.rb
new file mode 100644
index 0000000000..844d7f9820
--- /dev/null
+++ b/spec/ruby/core/rational/fixtures/rational.rb
@@ -0,0 +1,14 @@
+module RationalSpecs
+ class SubNumeric < Numeric
+ def initialize(value)
+ @value = Rational(value)
+ end
+
+ def to_r
+ @value
+ end
+ end
+
+ class CoerceError < StandardError
+ end
+end
diff --git a/spec/ruby/core/rational/floor_spec.rb b/spec/ruby/core/rational/floor_spec.rb
new file mode 100644
index 0000000000..6d4cee79a9
--- /dev/null
+++ b/spec/ruby/core/rational/floor_spec.rb
@@ -0,0 +1,49 @@
+require_relative "../../spec_helper"
+require_relative "../integer/shared/integer_floor_precision"
+
+describe "Rational#floor" do
+ context "with values equal to integers" do
+ it_behaves_like :integer_floor_precision, :Rational
+ end
+
+ before do
+ @rational = Rational(2200, 7)
+ end
+
+ describe "with no arguments (precision = 0)" do
+
+ it "returns the Integer value rounded toward negative infinity" do
+ @rational.floor.should.eql? 314
+
+ Rational(1, 2).floor.should.eql? 0
+ Rational(-1, 2).floor.should.eql?(-1)
+ Rational(1, 1).floor.should.eql? 1
+ end
+ end
+
+ describe "with a precision < 0" do
+ it "moves the rounding point n decimal places left, returning an Integer" do
+ @rational.floor(-3).should.eql? 0
+ @rational.floor(-2).should.eql? 300
+ @rational.floor(-1).should.eql? 310
+
+ Rational(100, 2).floor(-1).should.eql? 50
+ Rational(100, 2).floor(-2).should.eql? 0
+ Rational(-100, 2).floor(-1).should.eql?(-50)
+ Rational(-100, 2).floor(-2).should.eql?(-100)
+ end
+ end
+
+ describe "with a precision > 0" do
+ it "moves the rounding point n decimal places right, returning a Rational" do
+ @rational.floor(1).should.eql? Rational(1571, 5)
+ @rational.floor(2).should.eql? Rational(7857, 25)
+ @rational.floor(3).should.eql? Rational(62857, 200)
+
+ Rational(100, 2).floor(1).should.eql? Rational(50, 1)
+ Rational(100, 2).floor(2).should.eql? Rational(50, 1)
+ Rational(-100, 2).floor(1).should.eql? Rational(-50, 1)
+ Rational(-100, 2).floor(2).should.eql? Rational(-50, 1)
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/hash_spec.rb b/spec/ruby/core/rational/hash_spec.rb
new file mode 100644
index 0000000000..528638056a
--- /dev/null
+++ b/spec/ruby/core/rational/hash_spec.rb
@@ -0,0 +1,9 @@
+require_relative "../../spec_helper"
+
+describe "Rational#hash" do
+ # BUG: Rational(2, 3).hash == Rational(3, 2).hash
+ it "is static" do
+ Rational(2, 3).hash.should == Rational(2, 3).hash
+ Rational(2, 4).hash.should_not == Rational(2, 3).hash
+ end
+end
diff --git a/spec/ruby/core/rational/inspect_spec.rb b/spec/ruby/core/rational/inspect_spec.rb
new file mode 100644
index 0000000000..edc5cffee9
--- /dev/null
+++ b/spec/ruby/core/rational/inspect_spec.rb
@@ -0,0 +1,14 @@
+require_relative "../../spec_helper"
+
+describe "Rational#inspect" do
+ it "returns a string representation of self" do
+ Rational(3, 4).inspect.should == "(3/4)"
+ Rational(-5, 8).inspect.should == "(-5/8)"
+ Rational(-1, -2).inspect.should == "(1/2)"
+
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ Rational(bignum_value, 1).inspect.should == "(#{bignum_value}/1)"
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/integer_spec.rb b/spec/ruby/core/rational/integer_spec.rb
new file mode 100644
index 0000000000..cd7fa97fcf
--- /dev/null
+++ b/spec/ruby/core/rational/integer_spec.rb
@@ -0,0 +1,13 @@
+require_relative "../../spec_helper"
+describe "Rational#integer?" do
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ it "returns false for a rational with a numerator and no denominator" do
+ Rational(20).integer?.should == false
+ end
+ end
+
+ it "returns false for a rational with a numerator and a denominator" do
+ Rational(20,3).integer?.should == false
+ end
+end
diff --git a/spec/ruby/core/rational/magnitude_spec.rb b/spec/ruby/core/rational/magnitude_spec.rb
new file mode 100644
index 0000000000..f5f667edb1
--- /dev/null
+++ b/spec/ruby/core/rational/magnitude_spec.rb
@@ -0,0 +1,6 @@
+require_relative "../../spec_helper"
+require_relative 'shared/abs'
+
+describe "Rational#abs" do
+ it_behaves_like :rational_abs, :magnitude
+end
diff --git a/spec/ruby/core/rational/marshal_dump_spec.rb b/spec/ruby/core/rational/marshal_dump_spec.rb
new file mode 100644
index 0000000000..06bf36f166
--- /dev/null
+++ b/spec/ruby/core/rational/marshal_dump_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Rational#marshal_dump" do
+ it "is a private method" do
+ Rational.private_instance_methods(false).should.include?(:marshal_dump)
+ end
+
+ it "dumps numerator and denominator" do
+ Rational(1, 2).send(:marshal_dump).should == [1, 2]
+ end
+end
diff --git a/spec/ruby/core/rational/minus_spec.rb b/spec/ruby/core/rational/minus_spec.rb
new file mode 100644
index 0000000000..4e10e118b9
--- /dev/null
+++ b/spec/ruby/core/rational/minus_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Rational#-" do
+ it_behaves_like :rational_arithmetic_exception_in_coerce, :-
+
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational - obj
+ end
+
+ it "calls #- on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:-).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ (rational - obj).should == :result
+ end
+end
+
+describe "Rational#- passed a Rational" do
+ it "returns the result of subtracting other from self as a Rational" do
+ (Rational(3, 4) - Rational(0, 1)).should.eql?(Rational(3, 4))
+ (Rational(3, 4) - Rational(1, 4)).should.eql?(Rational(1, 2))
+
+ (Rational(3, 4) - Rational(2, 1)).should.eql?(Rational(-5, 4))
+ end
+end
+
+describe "Rational#- passed a Float" do
+ it "returns the result of subtracting other from self as a Float" do
+ (Rational(3, 4) - 0.2).should.eql?(0.55)
+ (Rational(3, 4) - 2.5).should.eql?(-1.75)
+ end
+end
+
+describe "Rational#- passed an Integer" do
+ it "returns the result of subtracting other from self as a Rational" do
+ (Rational(3, 4) - 1).should.eql?(Rational(-1, 4))
+ (Rational(3, 4) - 2).should.eql?(Rational(-5, 4))
+ end
+end
diff --git a/spec/ruby/core/rational/modulo_spec.rb b/spec/ruby/core/rational/modulo_spec.rb
new file mode 100644
index 0000000000..6241077f68
--- /dev/null
+++ b/spec/ruby/core/rational/modulo_spec.rb
@@ -0,0 +1,43 @@
+require_relative "../../spec_helper"
+
+describe "Rational#%" do
+ it "returns the remainder when this value is divided by other" do
+ (Rational(2, 3) % Rational(2, 3)).should == Rational(0, 1)
+ (Rational(4, 3) % Rational(2, 3)).should == Rational(0, 1)
+ (Rational(2, -3) % Rational(-2, 3)).should == Rational(0, 1)
+ (Rational(0, -1) % -1).should == Rational(0, 1)
+
+ (Rational(7, 4) % Rational(1, 2)).should == Rational(1, 4)
+ (Rational(7, 4) % 1).should == Rational(3, 4)
+ (Rational(7, 4) % Rational(1, 7)).should == Rational(1, 28)
+
+ (Rational(3, 4) % -1).should == Rational(-1, 4)
+ (Rational(1, -5) % -1).should == Rational(-1, 5)
+ end
+
+ it "returns a Float value when the argument is Float" do
+ (Rational(7, 4) % 1.0).should.is_a?(Float)
+ (Rational(7, 4) % 1.0).should == 0.75
+ (Rational(7, 4) % 0.26).should be_close(0.19, 0.0001)
+ end
+
+ it "raises ZeroDivisionError on zero denominator" do
+ -> {
+ Rational(3, 5) % Rational(0, 1)
+ }.should.raise(ZeroDivisionError)
+
+ -> {
+ Rational(0, 1) % Rational(0, 1)
+ }.should.raise(ZeroDivisionError)
+
+ -> {
+ Rational(3, 5) % 0
+ }.should.raise(ZeroDivisionError)
+ end
+
+ it "raises a ZeroDivisionError when the argument is 0.0" do
+ -> {
+ Rational(3, 5) % 0.0
+ }.should.raise(ZeroDivisionError)
+ end
+end
diff --git a/spec/ruby/core/rational/multiply_spec.rb b/spec/ruby/core/rational/multiply_spec.rb
new file mode 100644
index 0000000000..3e059cbc1c
--- /dev/null
+++ b/spec/ruby/core/rational/multiply_spec.rb
@@ -0,0 +1,65 @@
+require_relative "../../spec_helper"
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Rational#*" do
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational * obj
+ end
+
+ it "calls #* on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:*).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ (rational * obj).should == :result
+ end
+
+ it_behaves_like :rational_arithmetic_exception_in_coerce, :*
+end
+
+describe "Rational#* passed a Rational" do
+ it "returns self divided by other as a Rational" do
+ (Rational(3, 4) * Rational(3, 4)).should.eql?(Rational(9, 16))
+ (Rational(2, 4) * Rational(1, 4)).should.eql?(Rational(1, 8))
+
+ (Rational(3, 4) * Rational(0, 1)).should.eql?(Rational(0, 4))
+ end
+end
+
+describe "Rational#* passed a Float" do
+ it "returns self divided by other as a Float" do
+ (Rational(3, 4) * 0.75).should.eql?(0.5625)
+ (Rational(3, 4) * 0.25).should.eql?(0.1875)
+ (Rational(3, 4) * 0.3).should be_close(0.225, TOLERANCE)
+
+ (Rational(-3, 4) * 0.3).should be_close(-0.225, TOLERANCE)
+ (Rational(3, -4) * 0.3).should be_close(-0.225, TOLERANCE)
+ (Rational(3, 4) * -0.3).should be_close(-0.225, TOLERANCE)
+
+ (Rational(3, 4) * 0.0).should.eql?(0.0)
+ (Rational(-3, -4) * 0.0).should.eql?(0.0)
+
+ (Rational(-3, 4) * 0.0).should.eql?(0.0)
+ (Rational(3, -4) * 0.0).should.eql?(0.0)
+ end
+end
+
+describe "Rational#* passed an Integer" do
+ it "returns self divided by other as a Rational" do
+ (Rational(3, 4) * 2).should.eql?(Rational(3, 2))
+ (Rational(2, 4) * 2).should.eql?(Rational(1, 1))
+ (Rational(6, 7) * -2).should.eql?(Rational(-12, 7))
+
+ (Rational(3, 4) * 0).should.eql?(Rational(0, 4))
+ end
+end
diff --git a/spec/ruby/core/rational/numerator_spec.rb b/spec/ruby/core/rational/numerator_spec.rb
new file mode 100644
index 0000000000..631bb4703c
--- /dev/null
+++ b/spec/ruby/core/rational/numerator_spec.rb
@@ -0,0 +1,10 @@
+require_relative "../../spec_helper"
+
+describe "Rational#numerator" do
+ it "returns the numerator" do
+ Rational(3, 4).numerator.should.equal?(3)
+ Rational(3, -4).numerator.should.equal?(-3)
+
+ Rational(bignum_value, 1).numerator.should == bignum_value
+ end
+end
diff --git a/spec/ruby/core/rational/plus_spec.rb b/spec/ruby/core/rational/plus_spec.rb
new file mode 100644
index 0000000000..92f830a105
--- /dev/null
+++ b/spec/ruby/core/rational/plus_spec.rb
@@ -0,0 +1,50 @@
+require_relative "../../spec_helper"
+require_relative 'shared/arithmetic_exception_in_coerce'
+
+describe "Rational#+" do
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational + obj
+ end
+
+ it "calls #+ on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:+).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ (rational + obj).should == :result
+ end
+
+ it_behaves_like :rational_arithmetic_exception_in_coerce, :+
+end
+
+describe "Rational#+ with a Rational" do
+ it "returns the result of subtracting other from self as a Rational" do
+ (Rational(3, 4) + Rational(0, 1)).should.eql?(Rational(3, 4))
+ (Rational(3, 4) + Rational(1, 4)).should.eql?(Rational(1, 1))
+
+ (Rational(3, 4) + Rational(2, 1)).should.eql?(Rational(11, 4))
+ end
+end
+describe "Rational#+ with a Float" do
+ it "returns the result of subtracting other from self as a Float" do
+ (Rational(3, 4) + 0.2).should.eql?(0.95)
+ (Rational(3, 4) + 2.5).should.eql?(3.25)
+ end
+end
+
+describe "Rational#+ with an Integer" do
+ it "returns the result of subtracting other from self as a Rational" do
+ (Rational(3, 4) + 1).should.eql?(Rational(7, 4))
+ (Rational(3, 4) + 2).should.eql?(Rational(11, 4))
+ end
+end
diff --git a/spec/ruby/core/rational/quo_spec.rb b/spec/ruby/core/rational/quo_spec.rb
new file mode 100644
index 0000000000..907898ad34
--- /dev/null
+++ b/spec/ruby/core/rational/quo_spec.rb
@@ -0,0 +1,25 @@
+require_relative "../../spec_helper"
+
+describe "Rational#quo" do
+ it "calls #coerce on the passed argument with self" do
+ rational = Rational(3, 4)
+ obj = mock("Object")
+ obj.should_receive(:coerce).with(rational).and_return([1, 2])
+
+ rational.quo(obj)
+ end
+
+ it "calls #/ on the coerced Rational with the coerced Object" do
+ rational = Rational(3, 4)
+
+ coerced_rational = mock("Coerced Rational")
+ coerced_rational.should_receive(:/).and_return(:result)
+
+ coerced_obj = mock("Coerced Object")
+
+ obj = mock("Object")
+ obj.should_receive(:coerce).and_return([coerced_rational, coerced_obj])
+
+ rational.quo(obj).should == :result
+ end
+end
diff --git a/spec/ruby/core/rational/rational_spec.rb b/spec/ruby/core/rational/rational_spec.rb
new file mode 100644
index 0000000000..278c4116a4
--- /dev/null
+++ b/spec/ruby/core/rational/rational_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Rational" do
+ it "includes Comparable" do
+ Rational.include?(Comparable).should == true
+ end
+
+ it "does not respond to new" do
+ -> { Rational.new(1) }.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/rational/rationalize_spec.rb b/spec/ruby/core/rational/rationalize_spec.rb
new file mode 100644
index 0000000000..9c86824ddd
--- /dev/null
+++ b/spec/ruby/core/rational/rationalize_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Rational#rationalize" do
+ it "returns self with no argument" do
+ Rational(12,3).rationalize.should == Rational(12,3)
+ Rational(-45,7).rationalize.should == Rational(-45,7)
+ end
+
+ # FIXME: These specs need reviewing by somebody familiar with the
+ # algorithm used by #rationalize
+ it "simplifies self to the degree specified by a Rational argument" do
+ r = Rational(5404319552844595,18014398509481984)
+ r.rationalize(Rational(1,10)).should == Rational(1,3)
+ r.rationalize(Rational(-1,10)).should == Rational(1,3)
+
+ r = Rational(-5404319552844595,18014398509481984)
+ r.rationalize(Rational(1,10)).should == Rational(-1,3)
+ r.rationalize(Rational(-1,10)).should == Rational(-1,3)
+
+ end
+
+ it "simplifies self to the degree specified by a Float argument" do
+ r = Rational(5404319552844595,18014398509481984)
+ r.rationalize(0.05).should == Rational(1,3)
+ r.rationalize(0.001).should == Rational(3, 10)
+
+ r = Rational(-5404319552844595,18014398509481984)
+ r.rationalize(0.05).should == Rational(-1,3)
+ r.rationalize(0.001).should == Rational(-3,10)
+ end
+
+ it "raises ArgumentError when passed more than one argument" do
+ -> { Rational(1,1).rationalize(0.1, 0.1) }.should.raise(ArgumentError)
+ -> { Rational(1,1).rationalize(0.1, 0.1, 2) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/rational/remainder_spec.rb b/spec/ruby/core/rational/remainder_spec.rb
new file mode 100644
index 0000000000..86ba4674e6
--- /dev/null
+++ b/spec/ruby/core/rational/remainder_spec.rb
@@ -0,0 +1,5 @@
+require_relative "../../spec_helper"
+
+describe "Rational#remainder" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/rational/round_spec.rb b/spec/ruby/core/rational/round_spec.rb
new file mode 100644
index 0000000000..dd6f66f408
--- /dev/null
+++ b/spec/ruby/core/rational/round_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+
+describe "Rational#round" do
+ before do
+ @rational = Rational(2200, 7)
+ end
+
+ describe "with no arguments (precision = 0)" do
+ it "returns an integer" do
+ @rational.round.should.is_a?(Integer)
+ Rational(0, 1).round(0).should.is_a?(Integer)
+ Rational(124, 1).round(0).should.is_a?(Integer)
+ end
+
+ it "returns the truncated value toward the nearest integer" do
+ @rational.round.should == 314
+ Rational(0, 1).round(0).should == 0
+ Rational(2, 1).round(0).should == 2
+ end
+
+ it "returns the rounded value toward the nearest integer" do
+ Rational(1, 2).round.should == 1
+ Rational(-1, 2).round.should == -1
+ Rational(3, 2).round.should == 2
+ Rational(-3, 2).round.should == -2
+ Rational(5, 2).round.should == 3
+ Rational(-5, 2).round.should == -3
+ end
+ end
+
+ describe "with a precision < 0" do
+ it "returns an integer" do
+ @rational.round(-2).should.is_a?(Integer)
+ @rational.round(-1).should.is_a?(Integer)
+ Rational(0, 1).round(-1).should.is_a?(Integer)
+ Rational(2, 1).round(-1).should.is_a?(Integer)
+ end
+
+ it "moves the truncation point n decimal places left" do
+ @rational.round(-3).should == 0
+ @rational.round(-2).should == 300
+ @rational.round(-1).should == 310
+ end
+ end
+
+ describe "with a precision > 0" do
+ it "returns a Rational" do
+ @rational.round(1).should.is_a?(Rational)
+ @rational.round(2).should.is_a?(Rational)
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ Rational(0, 1).round(1).should.is_a?(Rational)
+ Rational(2, 1).round(1).should.is_a?(Rational)
+ end
+ end
+
+ it "moves the truncation point n decimal places right" do
+ @rational.round(1).should == Rational(3143, 10)
+ @rational.round(2).should == Rational(31429, 100)
+ @rational.round(3).should == Rational(157143, 500)
+ Rational(0, 1).round(1).should == Rational(0, 1)
+ Rational(2, 1).round(1).should == Rational(2, 1)
+ end
+
+ it "doesn't alter the value if the precision is too great" do
+ Rational(3, 2).round(10).should == Rational(3, 2).round(20)
+ end
+
+ # #6605
+ it "doesn't fail when rounding to an absurdly large positive precision" do
+ Rational(3, 2).round(2_097_171).should == Rational(3, 2)
+ end
+ end
+
+ describe "with half option" do
+ it "returns an Integer when precision is not passed" do
+ Rational(10, 4).round(half: nil).should == 3
+ Rational(10, 4).round(half: :up).should == 3
+ Rational(10, 4).round(half: :down).should == 2
+ Rational(10, 4).round(half: :even).should == 2
+ Rational(-10, 4).round(half: nil).should == -3
+ Rational(-10, 4).round(half: :up).should == -3
+ Rational(-10, 4).round(half: :down).should == -2
+ Rational(-10, 4).round(half: :even).should == -2
+ end
+
+ it "returns a Rational when the precision is greater than 0" do
+ Rational(25, 100).round(1, half: nil).should == Rational(3, 10)
+ Rational(25, 100).round(1, half: :up).should == Rational(3, 10)
+ Rational(25, 100).round(1, half: :down).should == Rational(1, 5)
+ Rational(25, 100).round(1, half: :even).should == Rational(1, 5)
+ Rational(35, 100).round(1, half: nil).should == Rational(2, 5)
+ Rational(35, 100).round(1, half: :up).should == Rational(2, 5)
+ Rational(35, 100).round(1, half: :down).should == Rational(3, 10)
+ Rational(35, 100).round(1, half: :even).should == Rational(2, 5)
+ Rational(-25, 100).round(1, half: nil).should == Rational(-3, 10)
+ Rational(-25, 100).round(1, half: :up).should == Rational(-3, 10)
+ Rational(-25, 100).round(1, half: :down).should == Rational(-1, 5)
+ Rational(-25, 100).round(1, half: :even).should == Rational(-1, 5)
+ end
+
+ it "raise for a non-existent round mode" do
+ -> { Rational(10, 4).round(half: :nonsense) }.should.raise(ArgumentError, "invalid rounding mode: nonsense")
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/shared/abs.rb b/spec/ruby/core/rational/shared/abs.rb
new file mode 100644
index 0000000000..3d64bcc1a0
--- /dev/null
+++ b/spec/ruby/core/rational/shared/abs.rb
@@ -0,0 +1,11 @@
+require_relative '../../../spec_helper'
+
+describe :rational_abs, shared: true do
+ it "returns self's absolute value" do
+ Rational(3, 4).send(@method).should == Rational(3, 4)
+ Rational(-3, 4).send(@method).should == Rational(3, 4)
+ Rational(3, -4).send(@method).should == Rational(3, 4)
+
+ Rational(bignum_value, -bignum_value).send(@method).should == Rational(bignum_value, bignum_value)
+ end
+end
diff --git a/spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb b/spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb
new file mode 100644
index 0000000000..e15169c912
--- /dev/null
+++ b/spec/ruby/core/rational/shared/arithmetic_exception_in_coerce.rb
@@ -0,0 +1,11 @@
+require_relative '../fixtures/rational'
+
+describe :rational_arithmetic_exception_in_coerce, shared: true do
+ it "does not rescue exception raised in other#coerce" do
+ b = mock("numeric with failed #coerce")
+ b.should_receive(:coerce).and_raise(RationalSpecs::CoerceError)
+
+ # e.g. Rational(3, 4) + b
+ -> { Rational(3, 4).send(@method, b) }.should.raise(RationalSpecs::CoerceError)
+ end
+end
diff --git a/spec/ruby/core/rational/to_f_spec.rb b/spec/ruby/core/rational/to_f_spec.rb
new file mode 100644
index 0000000000..40e3f11c0c
--- /dev/null
+++ b/spec/ruby/core/rational/to_f_spec.rb
@@ -0,0 +1,16 @@
+require_relative "../../spec_helper"
+
+describe "Rational#to_f" do
+ it "returns self converted to a Float" do
+ Rational(3, 4).to_f.should.eql?(0.75)
+ Rational(3, -4).to_f.should.eql?(-0.75)
+ Rational(-1, 4).to_f.should.eql?(-0.25)
+ Rational(-1, -4).to_f.should.eql?(0.25)
+ end
+
+ it "converts to a Float for large numerator and denominator" do
+ num = 1000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146010000000000000000000000000000000000048148248609680896326399448564623182963452541226153892315137780403285956264146009
+ den = 2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+ Rational(num, den).to_f.should == 500.0
+ end
+end
diff --git a/spec/ruby/core/rational/to_i_spec.rb b/spec/ruby/core/rational/to_i_spec.rb
new file mode 100644
index 0000000000..e61b1b6c3b
--- /dev/null
+++ b/spec/ruby/core/rational/to_i_spec.rb
@@ -0,0 +1,12 @@
+require_relative "../../spec_helper"
+
+describe "Rational#to_i" do
+ it "converts self to an Integer by truncation" do
+ Rational(7, 4).to_i.should.eql?(1)
+ Rational(11, 4).to_i.should.eql?(2)
+ end
+
+ it "converts self to an Integer by truncation" do
+ Rational(-7, 4).to_i.should.eql?(-1)
+ end
+end
diff --git a/spec/ruby/core/rational/to_r_spec.rb b/spec/ruby/core/rational/to_r_spec.rb
new file mode 100644
index 0000000000..eb6097f595
--- /dev/null
+++ b/spec/ruby/core/rational/to_r_spec.rb
@@ -0,0 +1,26 @@
+require_relative "../../spec_helper"
+
+describe "Rational#to_r" do
+ it "returns self" do
+ a = Rational(3, 4)
+ a.to_r.should.equal?(a)
+
+ a = Rational(bignum_value, 4)
+ a.to_r.should.equal?(a)
+ end
+
+ it "raises TypeError trying to convert BasicObject" do
+ obj = BasicObject.new
+ -> { Rational(obj) }.should.raise(TypeError)
+ end
+
+ it "works when a BasicObject has to_r" do
+ obj = BasicObject.new; def obj.to_r; 1 / 2.to_r end
+ Rational(obj).should == Rational('1/2')
+ end
+
+ it "fails when a BasicObject's to_r does not return a Rational" do
+ obj = BasicObject.new; def obj.to_r; 1 end
+ -> { Rational(obj) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/rational/to_s_spec.rb b/spec/ruby/core/rational/to_s_spec.rb
new file mode 100644
index 0000000000..24e30778e5
--- /dev/null
+++ b/spec/ruby/core/rational/to_s_spec.rb
@@ -0,0 +1,14 @@
+require_relative "../../spec_helper"
+
+describe "Rational#to_s" do
+ it "returns a string representation of self" do
+ # Guard against the Mathn library
+ guard -> { !defined?(Math.rsqrt) } do
+ Rational(1, 1).to_s.should == "1/1"
+ Rational(2, 1).to_s.should == "2/1"
+ end
+ Rational(1, 2).to_s.should == "1/2"
+ Rational(-1, 3).to_s.should == "-1/3"
+ Rational(1, -3).to_s.should == "-1/3"
+ end
+end
diff --git a/spec/ruby/core/rational/truncate_spec.rb b/spec/ruby/core/rational/truncate_spec.rb
new file mode 100644
index 0000000000..3614431a7f
--- /dev/null
+++ b/spec/ruby/core/rational/truncate_spec.rb
@@ -0,0 +1,71 @@
+require_relative "../../spec_helper"
+
+describe "Rational#truncate" do
+ before do
+ @rational = Rational(2200, 7)
+ end
+
+ describe "with no arguments (precision = 0)" do
+ it "returns an integer" do
+ @rational.truncate.should.is_a?(Integer)
+ end
+
+ it "returns the truncated value toward 0" do
+ @rational.truncate.should == 314
+ Rational(1, 2).truncate.should == 0
+ Rational(-1, 2).truncate.should == 0
+ end
+ end
+
+ describe "with an explicit precision = 0" do
+ it "returns an integer" do
+ @rational.truncate(0).should.is_a?(Integer)
+ end
+
+ it "returns the truncated value toward 0" do
+ @rational.truncate(0).should == 314
+ Rational(1, 2).truncate(0).should == 0
+ Rational(-1, 2).truncate(0).should == 0
+ end
+ end
+
+ describe "with a precision < 0" do
+ it "returns an integer" do
+ @rational.truncate(-2).should.is_a?(Integer)
+ @rational.truncate(-1).should.is_a?(Integer)
+ end
+
+ it "moves the truncation point n decimal places left" do
+ @rational.truncate(-3).should == 0
+ @rational.truncate(-2).should == 300
+ @rational.truncate(-1).should == 310
+ end
+ end
+
+ describe "with a precision > 0" do
+ it "returns a Rational" do
+ @rational.truncate(1).should.is_a?(Rational)
+ @rational.truncate(2).should.is_a?(Rational)
+ end
+
+ it "moves the truncation point n decimal places right" do
+ @rational.truncate(1).should == Rational(1571, 5)
+ @rational.truncate(2).should == Rational(7857, 25)
+ @rational.truncate(3).should == Rational(62857, 200)
+ end
+ end
+
+ describe "with an invalid value for precision" do
+ it "raises a TypeError" do
+ -> { @rational.truncate(nil) }.should.raise(TypeError, "not an integer")
+ -> { @rational.truncate(1.0) }.should.raise(TypeError, "not an integer")
+ -> { @rational.truncate('') }.should.raise(TypeError, "not an integer")
+ end
+
+ it "does not call to_int on the argument" do
+ object = Object.new
+ object.should_not_receive(:to_int)
+ -> { @rational.truncate(object) }.should.raise(TypeError, "not an integer")
+ end
+ end
+end
diff --git a/spec/ruby/core/rational/zero_spec.rb b/spec/ruby/core/rational/zero_spec.rb
new file mode 100644
index 0000000000..2e4f783d5c
--- /dev/null
+++ b/spec/ruby/core/rational/zero_spec.rb
@@ -0,0 +1,14 @@
+require_relative "../../spec_helper"
+describe "Rational#zero?" do
+ it "returns true if the numerator is 0" do
+ Rational(0,26).zero?.should == true
+ end
+
+ it "returns true if the numerator is 0.0" do
+ Rational(0.0,26).zero?.should == true
+ end
+
+ it "returns false if the numerator isn't 0" do
+ Rational(26).zero?.should == false
+ end
+end
diff --git a/spec/ruby/core/refinement/append_features_spec.rb b/spec/ruby/core/refinement/append_features_spec.rb
new file mode 100644
index 0000000000..cffc9ed308
--- /dev/null
+++ b/spec/ruby/core/refinement/append_features_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Refinement#append_features" do
+ it "is not defined" do
+ Refinement.private_instance_methods(true).should_not.include?(:append_features)
+ end
+
+ it "is not called by Module#include" do
+ c = Class.new
+ Module.new do
+ refine c do
+ called = false
+ define_method(:append_features){called = true}
+ proc{c.include(self)}.should.raise(TypeError)
+ called.should == false
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/extend_object_spec.rb b/spec/ruby/core/refinement/extend_object_spec.rb
new file mode 100644
index 0000000000..f6fe2a60ea
--- /dev/null
+++ b/spec/ruby/core/refinement/extend_object_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+
+describe "Refinement#extend_object" do
+ it "is not defined" do
+ Refinement.private_instance_methods(true).should_not.include?(:extend_object)
+ end
+
+ it "is not called by Object#extend" do
+ c = Class.new
+ Module.new do
+ refine c do
+ called = false
+ define_method(:extend_object) { called = true }
+ -> {
+ c.extend(self)
+ }.should.raise(TypeError)
+ called.should == false
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/fixtures/classes.rb b/spec/ruby/core/refinement/fixtures/classes.rb
new file mode 100644
index 0000000000..94324db47c
--- /dev/null
+++ b/spec/ruby/core/refinement/fixtures/classes.rb
@@ -0,0 +1,10 @@
+module RefinementSpec
+
+ module ModuleWithAncestors
+ include Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/import_methods_spec.rb b/spec/ruby/core/refinement/import_methods_spec.rb
new file mode 100644
index 0000000000..bcb5e9b066
--- /dev/null
+++ b/spec/ruby/core/refinement/import_methods_spec.rb
@@ -0,0 +1,287 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Refinement#import_methods" do
+ context "when methods are defined in Ruby code" do
+ it "imports methods" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ Module.new do
+ refine String do
+ import_methods str_utils
+ "foo".indent(3).should == " foo"
+ end
+ end
+ end
+
+ it "throws an exception when argument is not a module" do
+ Module.new do
+ refine String do
+ -> {
+ import_methods Integer
+ }.should.raise(TypeError, "wrong argument type Class (expected Module)")
+ end
+ end
+ end
+
+ it "imports methods from multiple modules" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ str_utils_fancy = Module.new do
+ def indent_star(level)
+ "*" * level + self
+ end
+ end
+
+ Module.new do
+ refine String do
+ import_methods str_utils, str_utils_fancy
+ "foo".indent(3).should == " foo"
+ "foo".indent_star(3).should == "***foo"
+ end
+ end
+ end
+
+ it "imports a method defined in the last module if method with same name is defined in multiple modules" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ str_utils_fancy = Module.new do
+ def indent(level)
+ "*" * level + self
+ end
+ end
+
+ Module.new do
+ refine String do
+ import_methods str_utils, str_utils_fancy
+ "foo".indent(3).should == "***foo"
+ end
+ end
+ end
+
+ it "still imports methods of modules listed before a module that contains method not defined in Ruby" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ string_refined = Module.new do
+ refine String do
+ -> {
+ import_methods str_utils, Kernel
+ }.should.raise(ArgumentError)
+ end
+ end
+
+ Module.new do
+ using string_refined
+ "foo".indent(3).should == " foo"
+ end
+ end
+ end
+
+ it "warns if a module includes/prepends some other module" do
+ module1 = Module.new do
+ end
+
+ module2 = Module.new do
+ include module1
+ end
+
+ Module.new do
+ refine String do
+ -> {
+ import_methods module2
+ }.should complain(/warning: #<Module:\w*> has ancestors, but Refinement#import_methods doesn't import their methods/)
+ end
+ end
+
+ Module.new do
+ refine String do
+ -> {
+ import_methods RefinementSpec::ModuleWithAncestors
+ }.should complain(/warning: RefinementSpec::ModuleWithAncestors has ancestors, but Refinement#import_methods doesn't import their methods/)
+ end
+ end
+ end
+
+ it "doesn't import methods from included/prepended modules" do
+ Module.new do
+ refine String do
+ suppress_warning { import_methods RefinementSpec::ModuleWithAncestors }
+ end
+
+ using self
+ -> {
+ "foo".indent(3)
+ }.should.raise(NoMethodError, /undefined method [`']indent' for ("foo":String|an instance of String)/)
+ end
+ end
+
+ it "doesn't import any methods if one of the arguments is not a module" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ string_refined = Module.new do
+ refine String do
+ -> {
+ import_methods str_utils, Integer
+ }.should.raise(TypeError)
+ end
+ end
+
+ Module.new do
+ using string_refined
+ -> {
+ "foo".indent(3)
+ }.should.raise(NoMethodError)
+ end
+ end
+
+ it "imports methods from multiple modules so that methods see other's module's methods" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ str_utils_normal = Module.new do
+ def indent_normal(level)
+ self.indent(level)
+ end
+ end
+
+ Module.new do
+ refine String do
+ import_methods str_utils, str_utils_normal
+ end
+
+ using self
+ "foo".indent_normal(3).should == " foo"
+ end
+ end
+
+ it "imports methods from module so that methods can see each other" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+
+ def indent_with_dot(level)
+ self.indent(level) + "."
+ end
+ end
+
+ Module.new do
+ refine String do
+ import_methods str_utils
+ end
+
+ using self
+ "foo".indent_with_dot(3).should == " foo."
+ end
+ end
+
+ it "doesn't import module's class methods" do
+ str_utils = Module.new do
+ def self.indent(level)
+ " " * level + self
+ end
+ end
+
+ Module.new do
+ refine String do
+ import_methods str_utils
+ end
+
+ using self
+ -> {
+ String.indent(3)
+ }.should.raise(NoMethodError, /undefined method [`']indent' for (String:Class|class String)/)
+ end
+ end
+
+ it "imports module methods with super" do
+ class_to_refine = Class.new do
+ def foo(number)
+ 2 * number
+ end
+ end
+
+ extension = Module.new do
+ def foo(number)
+ super * 2
+ end
+ end
+
+ refinement = Module.new do
+ refine class_to_refine do
+ import_methods extension
+ end
+ end
+
+ Module.new do
+ using refinement
+ class_to_refine.new.foo(2).should == 8
+ end
+ end
+
+ it "correctly sets owner as the refinement module" do
+ str_utils = Module.new do
+ def indent(level)
+ " " * level + self
+ end
+ end
+
+ refinement = Module.new do
+ refine String do
+ import_methods str_utils
+ end
+ end
+
+ Module.new do
+ using refinement
+
+ String.instance_method(:indent).owner.should == refinement.refinements.first
+ end
+ end
+
+ context "when methods are not defined in Ruby code" do
+ it "raises ArgumentError" do
+ Module.new do
+ refine String do
+ -> {
+ import_methods Kernel
+ }.should.raise(ArgumentError)
+ end
+ end
+ end
+
+ it "raises ArgumentError when importing methods from C extension" do
+ require 'zlib'
+ Module.new do
+ refine String do
+ -> {
+ import_methods Zlib
+ }.should.raise(ArgumentError, /Can't import method which is not defined with Ruby code: Zlib#*/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/include_spec.rb b/spec/ruby/core/refinement/include_spec.rb
new file mode 100644
index 0000000000..55ac89ffb5
--- /dev/null
+++ b/spec/ruby/core/refinement/include_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Refinement#include" do
+ it "raises a TypeError" do
+ Module.new do
+ refine String do
+ -> {
+ include Module.new
+ }.should.raise(TypeError, "Refinement#include has been removed")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/prepend_features_spec.rb b/spec/ruby/core/refinement/prepend_features_spec.rb
new file mode 100644
index 0000000000..9dc5838f1f
--- /dev/null
+++ b/spec/ruby/core/refinement/prepend_features_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Refinement#prepend_features" do
+ it "is not defined" do
+ Refinement.private_instance_methods(true).should_not.include?(:prepend_features)
+ end
+
+ it "is not called by Module#prepend" do
+ c = Class.new
+ Module.new do
+ refine c do
+ called = false
+ define_method(:prepend_features){called = true}
+ proc{c.prepend(self)}.should.raise(TypeError)
+ called.should == false
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/prepend_spec.rb b/spec/ruby/core/refinement/prepend_spec.rb
new file mode 100644
index 0000000000..fd70dd6f97
--- /dev/null
+++ b/spec/ruby/core/refinement/prepend_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Refinement#prepend" do
+ it "raises a TypeError" do
+ Module.new do
+ refine String do
+ -> {
+ prepend Module.new
+ }.should.raise(TypeError, "Refinement#prepend has been removed")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/refined_class_spec.rb b/spec/ruby/core/refinement/refined_class_spec.rb
new file mode 100644
index 0000000000..b532d9a773
--- /dev/null
+++ b/spec/ruby/core/refinement/refined_class_spec.rb
@@ -0,0 +1,34 @@
+require_relative "../../spec_helper"
+require_relative 'shared/target'
+
+describe "Refinement#refined_class" do
+ ruby_version_is ""..."3.4" do
+ it "has been deprecated in favour of Refinement#target" do
+ refinement_int = nil
+
+ Module.new do
+ refine Integer do
+ refinement_int = self
+ end
+ end
+
+ -> {
+ refinement_int.refined_class
+ }.should complain(/warning: Refinement#refined_class is deprecated and will be removed in Ruby 3.4; use Refinement#target instead/)
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "has been removed" do
+ refinement_int = nil
+
+ Module.new do
+ refine Integer do
+ refinement_int = self
+ end
+ end
+
+ refinement_int.should_not.respond_to?(:refined_class)
+ end
+ end
+end
diff --git a/spec/ruby/core/refinement/shared/target.rb b/spec/ruby/core/refinement/shared/target.rb
new file mode 100644
index 0000000000..79557bea0b
--- /dev/null
+++ b/spec/ruby/core/refinement/shared/target.rb
@@ -0,0 +1,13 @@
+describe :refinement_target, shared: true do
+ it "returns the class refined by the receiver" do
+ refinement_int = nil
+
+ Module.new do
+ refine Integer do
+ refinement_int = self
+ end
+ end
+
+ refinement_int.send(@method).should == Integer
+ end
+end
diff --git a/spec/ruby/core/refinement/target_spec.rb b/spec/ruby/core/refinement/target_spec.rb
new file mode 100644
index 0000000000..8bd816aea6
--- /dev/null
+++ b/spec/ruby/core/refinement/target_spec.rb
@@ -0,0 +1,6 @@
+require_relative "../../spec_helper"
+require_relative 'shared/target'
+
+describe "Refinement#target" do
+ it_behaves_like :refinement_target, :target
+end
diff --git a/spec/ruby/core/regexp/case_compare_spec.rb b/spec/ruby/core/regexp/case_compare_spec.rb
new file mode 100644
index 0000000000..29aada70bc
--- /dev/null
+++ b/spec/ruby/core/regexp/case_compare_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#===" do
+ it "is true if there is a match" do
+ (/abc/ === "aabcc").should == true
+ end
+
+ it "is false if there is no match" do
+ (/abc/ === "xyz").should == false
+ end
+
+ it "returns true if it matches a Symbol" do
+ (/a/ === :a).should == true
+ end
+
+ it "returns false if it does not match a Symbol" do
+ (/a/ === :b).should == false
+ end
+
+ # mirroring https://github.com/ruby/ruby/blob/master/test/ruby/test_regexp.rb
+ it "returns false if the other value cannot be coerced to a string" do
+ (/abc/ === nil).should == false
+ (/abc/ === /abc/).should == false
+ end
+
+ it "uses #to_str on string-like objects" do
+ stringlike = Class.new do
+ def to_str
+ "abc"
+ end
+ end.new
+
+ (/abc/ === stringlike).should == true
+ end
+end
diff --git a/spec/ruby/core/regexp/casefold_spec.rb b/spec/ruby/core/regexp/casefold_spec.rb
new file mode 100644
index 0000000000..d36467a989
--- /dev/null
+++ b/spec/ruby/core/regexp/casefold_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#casefold?" do
+ it "returns the value of the case-insensitive flag" do
+ /abc/i.should.casefold?
+ /xyz/.should_not.casefold?
+ end
+end
diff --git a/spec/ruby/core/regexp/compile_spec.rb b/spec/ruby/core/regexp/compile_spec.rb
new file mode 100644
index 0000000000..887c8d77dc
--- /dev/null
+++ b/spec/ruby/core/regexp/compile_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'shared/new'
+
+describe "Regexp.compile" do
+ it_behaves_like :regexp_new, :compile
+end
+
+describe "Regexp.compile given a String" do
+ it_behaves_like :regexp_new_string, :compile
+ it_behaves_like :regexp_new_string_binary, :compile
+end
+
+describe "Regexp.compile given a Regexp" do
+ it_behaves_like :regexp_new_regexp, :compile
+end
+
+describe "Regexp.compile given a non-String/Regexp" do
+ it_behaves_like :regexp_new_non_string_or_regexp, :compile
+end
diff --git a/spec/ruby/core/regexp/encoding_spec.rb b/spec/ruby/core/regexp/encoding_spec.rb
new file mode 100644
index 0000000000..fb4fdba064
--- /dev/null
+++ b/spec/ruby/core/regexp/encoding_spec.rb
@@ -0,0 +1,62 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Regexp#encoding" do
+ it "returns an Encoding object" do
+ /glar/.encoding.should.instance_of?(Encoding)
+ end
+
+ it "defaults to US-ASCII if the Regexp contains only US-ASCII character" do
+ /ASCII/.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns US_ASCII if the 'n' modifier is supplied and only US-ASCII characters are present" do
+ /ASCII/n.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns BINARY if the 'n' modifier is supplied and non-US-ASCII characters are present" do
+ /\xc2\xa1/n.encoding.should == Encoding::BINARY
+ end
+
+ it "defaults to UTF-8 if \\u escapes appear" do
+ /\u{9879}/.encoding.should == Encoding::UTF_8
+ end
+
+ it "defaults to UTF-8 if a literal UTF-8 character appears" do
+ /Â¥/.encoding.should == Encoding::UTF_8
+ end
+
+ it "returns UTF-8 if the 'u' modifier is supplied" do
+ /ASCII/u.encoding.should == Encoding::UTF_8
+ end
+
+ it "returns Windows-31J if the 's' modifier is supplied" do
+ /ASCII/s.encoding.should == Encoding::Windows_31J
+ end
+
+ it "returns EUC_JP if the 'e' modifier is supplied" do
+ /ASCII/e.encoding.should == Encoding::EUC_JP
+ end
+
+ it "upgrades the encoding to that of an embedded String" do
+ str = "文字化ã‘".encode('euc-jp')
+ /#{str}/.encoding.should == Encoding::EUC_JP
+ end
+
+ it "ignores the encoding and uses US-ASCII if the string has only ASCII characters" do
+ str = "abc".encode('euc-jp')
+ str.encoding.should == Encoding::EUC_JP
+ /#{str}/.encoding.should == Encoding::US_ASCII
+ end
+
+ it "ignores the default_internal encoding" do
+ old_internal = Encoding.default_internal
+ Encoding.default_internal = Encoding::EUC_JP
+ /foo/.encoding.should_not == Encoding::EUC_JP
+ Encoding.default_internal = old_internal
+ end
+
+ it "allows otherwise invalid characters if NOENCODING is specified" do
+ Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE | Regexp::NOENCODING).encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/regexp/eql_spec.rb b/spec/ruby/core/regexp/eql_spec.rb
new file mode 100644
index 0000000000..bd5ae43eb2
--- /dev/null
+++ b/spec/ruby/core/regexp/eql_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal_value'
+
+describe "Regexp#eql?" do
+ it_behaves_like :regexp_eql, :eql?
+end
diff --git a/spec/ruby/core/regexp/equal_value_spec.rb b/spec/ruby/core/regexp/equal_value_spec.rb
new file mode 100644
index 0000000000..5455a30598
--- /dev/null
+++ b/spec/ruby/core/regexp/equal_value_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/equal_value'
+
+describe "Regexp#==" do
+ it_behaves_like :regexp_eql, :==
+end
diff --git a/spec/ruby/core/regexp/escape_spec.rb b/spec/ruby/core/regexp/escape_spec.rb
new file mode 100644
index 0000000000..6b06ab1cbc
--- /dev/null
+++ b/spec/ruby/core/regexp/escape_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/quote'
+
+describe "Regexp.escape" do
+ it_behaves_like :regexp_quote, :escape
+end
diff --git a/spec/ruby/core/regexp/fixed_encoding_spec.rb b/spec/ruby/core/regexp/fixed_encoding_spec.rb
new file mode 100644
index 0000000000..5d8b1c2860
--- /dev/null
+++ b/spec/ruby/core/regexp/fixed_encoding_spec.rb
@@ -0,0 +1,36 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Regexp#fixed_encoding?" do
+ it "returns false by default" do
+ /needle/.fixed_encoding?.should == false
+ end
+
+ it "returns false if the 'n' modifier was supplied to the Regexp" do
+ /needle/n.fixed_encoding?.should == false
+ end
+
+ it "returns true if the 'u' modifier was supplied to the Regexp" do
+ /needle/u.fixed_encoding?.should == true
+ end
+
+ it "returns true if the 's' modifier was supplied to the Regexp" do
+ /needle/s.fixed_encoding?.should == true
+ end
+
+ it "returns true if the 'e' modifier was supplied to the Regexp" do
+ /needle/e.fixed_encoding?.should == true
+ end
+
+ it "returns true if the Regexp contains a \\u escape" do
+ /needle \u{8768}/.fixed_encoding?.should == true
+ end
+
+ it "returns true if the Regexp contains a UTF-8 literal" do
+ /文字化ã‘/.fixed_encoding?.should == true
+ end
+
+ it "returns true if the Regexp was created with the Regexp::FIXEDENCODING option" do
+ Regexp.new("", Regexp::FIXEDENCODING).fixed_encoding?.should == true
+ end
+end
diff --git a/spec/ruby/core/regexp/hash_spec.rb b/spec/ruby/core/regexp/hash_spec.rb
new file mode 100644
index 0000000000..2d42e288e6
--- /dev/null
+++ b/spec/ruby/core/regexp/hash_spec.rb
@@ -0,0 +1,20 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#hash" do
+ it "is provided" do
+ Regexp.new('').respond_to?(:hash).should == true
+ end
+
+ it "is based on the text and options of Regexp" do
+ (/cat/.hash == /dog/.hash).should == false
+ (/dog/m.hash == /dog/m.hash).should == true
+ not_supported_on :opal do
+ (/cat/ix.hash == /cat/ixn.hash).should == true
+ (/cat/.hash == /cat/ix.hash).should == false
+ end
+ end
+
+ it "returns the same value for two Regexps differing only in the /n option" do
+ (//.hash == //n.hash).should == true
+ end
+end
diff --git a/spec/ruby/core/regexp/initialize_spec.rb b/spec/ruby/core/regexp/initialize_spec.rb
new file mode 100644
index 0000000000..1c0133acae
--- /dev/null
+++ b/spec/ruby/core/regexp/initialize_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#initialize" do
+ it "is a private method" do
+ Regexp.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "raises a FrozenError on a Regexp literal" do
+ -> { //.send(:initialize, "") }.should.raise(FrozenError)
+ end
+
+ ruby_version_is "4.1" do
+ it "raises a FrozenError on an initialized non-literal Regexp" do
+ regexp = Regexp.new("")
+ -> { regexp.send(:initialize, "") }.should.raise(FrozenError)
+ end
+ end
+
+ ruby_version_is ""..."4.1" do
+ it "raises a TypeError on an initialized non-literal Regexp" do
+ -> { Regexp.new("").send(:initialize, "") }.should.raise(TypeError)
+ end
+ end
+
+ it "raises a TypeError on an initialized non-literal Regexp subclass" do
+ r = Class.new(Regexp).new("")
+ -> { r.send(:initialize, "") }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/regexp/inspect_spec.rb b/spec/ruby/core/regexp/inspect_spec.rb
new file mode 100644
index 0000000000..f4e39234f5
--- /dev/null
+++ b/spec/ruby/core/regexp/inspect_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#inspect" do
+ it "returns a formatted string that would eval to the same regexp" do
+ not_supported_on :opal do
+ /ab+c/ix.inspect.should == "/ab+c/ix"
+ /a(.)+s/n.inspect.should =~ %r|/a(.)+s/n?| # Default 'n' may not appear
+ end
+ # 1.9 doesn't round-trip the encoding flags, such as 'u'. This is
+ # seemingly by design.
+ /a(.)+s/m.inspect.should == "/a(.)+s/m" # But a specified one does
+ end
+
+ it "returns options in the order 'mixn'" do
+ //nixm.inspect.should == "//mixn"
+ end
+
+ it "does not include the 'o' option" do
+ //o.inspect.should == "//"
+ end
+
+ it "does not include a character set code" do
+ //u.inspect.should == "//"
+ //s.inspect.should == "//"
+ //e.inspect.should == "//"
+ end
+
+ it "correctly escapes forward slashes /" do
+ Regexp.new("/foo/bar").inspect.should == "/\\/foo\\/bar/"
+ Regexp.new("/foo/bar[/]").inspect.should == "/\\/foo\\/bar[\\/]/"
+ end
+
+ it "doesn't over escape forward slashes" do
+ /\/foo\/bar/.inspect.should == '/\/foo\/bar/'
+ end
+
+ it "escapes 2 slashes in a row properly" do
+ Regexp.new("//").inspect.should == '/\/\//'
+ end
+
+ it "does not over escape" do
+ Regexp.new('\\\/').inspect.should == "/\\\\\\//"
+ end
+end
diff --git a/spec/ruby/core/regexp/last_match_spec.rb b/spec/ruby/core/regexp/last_match_spec.rb
new file mode 100644
index 0000000000..6c256cc1cf
--- /dev/null
+++ b/spec/ruby/core/regexp/last_match_spec.rb
@@ -0,0 +1,56 @@
+require_relative '../../spec_helper'
+
+describe "Regexp.last_match" do
+ it "returns MatchData instance when not passed arguments" do
+ /c(.)t/ =~ 'cat'
+
+ Regexp.last_match.should.is_a?(MatchData)
+ end
+
+ it "returns the nth field in this MatchData when passed an Integer" do
+ /c(.)t/ =~ 'cat'
+ Regexp.last_match(1).should == 'a'
+ end
+
+ it "returns nil when there is no match" do
+ /foo/ =~ "TEST123"
+ Regexp.last_match(:test).should == nil
+ Regexp.last_match(1).should == nil
+ Regexp.last_match(Object.new).should == nil
+ Regexp.last_match("test").should == nil
+ end
+
+ describe "when given a Symbol" do
+ it "returns a named capture" do
+ /(?<test>[A-Z]+.*)/ =~ "TEST123"
+ Regexp.last_match(:test).should == "TEST123"
+ end
+
+ it "raises an IndexError when given a missing name" do
+ /(?<test>[A-Z]+.*)/ =~ "TEST123"
+ -> { Regexp.last_match(:missing) }.should.raise(IndexError)
+ end
+ end
+
+ describe "when given a String" do
+ it "returns a named capture" do
+ /(?<test>[A-Z]+.*)/ =~ "TEST123"
+ Regexp.last_match("test").should == "TEST123"
+ end
+ end
+
+ describe "when given an Object" do
+ it "coerces argument to an index using #to_int" do
+ obj = mock("converted to int")
+ obj.should_receive(:to_int).and_return(1)
+ /(?<test>[A-Z]+.*)/ =~ "TEST123"
+ Regexp.last_match(obj).should == "TEST123"
+ end
+
+ it "raises a TypeError when unable to coerce" do
+ obj = Object.new
+ /(?<test>[A-Z]+.*)/ =~ "TEST123"
+ -> { Regexp.last_match(obj) }.should.raise(TypeError)
+ end
+ end
+end
diff --git a/spec/ruby/core/regexp/linear_time_spec.rb b/spec/ruby/core/regexp/linear_time_spec.rb
new file mode 100644
index 0000000000..f70021dfed
--- /dev/null
+++ b/spec/ruby/core/regexp/linear_time_spec.rb
@@ -0,0 +1,80 @@
+require_relative '../../spec_helper'
+
+describe "Regexp.linear_time?" do
+ it "returns true if matching can be done in linear time" do
+ Regexp.linear_time?(/a/).should == true
+ Regexp.linear_time?('a').should == true
+ end
+
+ it "returns true if matching can be done in linear time for a binary Regexp" do
+ Regexp.linear_time?(/[\x80-\xff]/n).should == true
+ end
+
+ it "return false if matching can't be done in linear time" do
+ Regexp.linear_time?(/(a)\1/).should == false
+ Regexp.linear_time?("(a)\\1").should == false
+ end
+
+ it "accepts flags for string argument" do
+ Regexp.linear_time?('a', Regexp::IGNORECASE).should == true
+ end
+
+ it "warns about flags being ignored for regexp arguments" do
+ -> {
+ Regexp.linear_time?(/a/, Regexp::IGNORECASE)
+ }.should complain(/warning: flags ignored/)
+ end
+
+ it "returns true for positive lookahead" do
+ Regexp.linear_time?(/a*(?:(?=a*)a)*b/).should == true
+ end
+
+ it "returns true for positive lookbehind" do
+ Regexp.linear_time?(/a*(?:(?<=a)a*)*b/).should == true
+ end
+
+ it "returns true for negative lookbehind" do
+ Regexp.linear_time?(/a*(?:(?<!a)a*)*b/).should == true
+ end
+
+ # There are two known ways to make Regexp linear:
+ # * Using a DFA (deterministic finite-state automaton) Regexp engine, which always matches in linear time (e.g. TruffleRuby with TRegex)
+ # * Caching position and state to avoid catastrophic backtracking (e.g. CRuby: https://bugs.ruby-lang.org/issues/19104)
+ #
+ # Both approach should be allowed and given that DFA Regexp engines
+ # are much faster there should be no specs preventing using them.
+ uses_regexp_caching = RUBY_ENGINE == 'ruby'
+ uses_dfa_regexp_engine = !uses_regexp_caching
+
+ # The following specs should not be relied upon,
+ # they are here only to illustrate differences between Regexp engines.
+ guard -> { uses_regexp_caching } do
+ it "returns true for negative lookahead" do
+ Regexp.linear_time?(/a*(?:(?!a*)a*)*b/).should == true
+ end
+
+ it "returns true for atomic groups" do
+ Regexp.linear_time?(/a*(?:(?>a)a*)*b/).should == true
+ end
+
+ it "returns true for possessive quantifiers" do
+ Regexp.linear_time?(/a*(?:(?:a)?+a*)*b/).should == true
+ end
+
+ it "returns true for positive lookbehind with capture group" do
+ Regexp.linear_time?(/.(?<=(a))/).should == true
+ end
+ end
+
+ # The following specs should not be relied upon,
+ # they are here only to illustrate differences between Regexp engines.
+ guard -> { uses_dfa_regexp_engine } do
+ it "returns true for non-recursive subexpression call" do
+ Regexp.linear_time?(/(?<a>a){0}\g<a>/).should == true
+ end
+
+ it "returns true for positive lookahead with capture group" do
+ Regexp.linear_time?(/x+(?=(a))/).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/regexp/match_spec.rb b/spec/ruby/core/regexp/match_spec.rb
new file mode 100644
index 0000000000..276cecc8e4
--- /dev/null
+++ b/spec/ruby/core/regexp/match_spec.rb
@@ -0,0 +1,146 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe :regexp_match, shared: true do
+ it "returns nil if there is no match" do
+ /xyz/.send(@method,"abxyc").should == nil
+ end
+
+ it "returns nil if the object is nil" do
+ /\w+/.send(@method, nil).should == nil
+ end
+end
+
+describe "Regexp#=~" do
+ it_behaves_like :regexp_match, :=~
+
+ it "returns the index of the first character of the matching region" do
+ (/(.)(.)(.)/ =~ "abc").should == 0
+ end
+
+ it "returns the index too, when argument is a Symbol" do
+ (/(.)(.)(.)/ =~ :abc).should == 0
+ end
+end
+
+describe "Regexp#match" do
+ it_behaves_like :regexp_match, :match
+
+ it "returns a MatchData object" do
+ /(.)(.)(.)/.match("abc").should.is_a?(MatchData)
+ end
+
+ it "returns a MatchData object, when argument is a Symbol" do
+ /(.)(.)(.)/.match(:abc).should.is_a?(MatchData)
+ end
+
+ it "raises a TypeError on an uninitialized Regexp" do
+ -> { Regexp.allocate.match('foo') }.should.raise(TypeError)
+ end
+
+ it "raises TypeError on an uninitialized Regexp" do
+ -> { Regexp.allocate.match('foo'.encode("UTF-16LE")) }.should.raise(TypeError)
+ end
+
+ describe "with [string, position]" do
+ describe "when given a positive position" do
+ it "matches the input at a given position" do
+ /(.).(.)/.match("01234", 1).captures.should == ["1", "3"]
+ end
+
+ it "uses the start as a character offset" do
+ /(.).(.)/.match("零一二三四", 1).captures.should == ["一", "三"]
+ end
+
+ it "raises an ArgumentError for an invalid encoding" do
+ x96 = ([150].pack('C')).force_encoding('utf-8')
+ -> { /(.).(.)/.match("Hello, #{x96} world!", 1) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "when given a negative position" do
+ it "matches the input at a given position" do
+ /(.).(.)/.match("01234", -4).captures.should == ["1", "3"]
+ end
+
+ it "uses the start as a character offset" do
+ /(.).(.)/.match("零一二三四", -4).captures.should == ["一", "三"]
+ end
+
+ it "raises an ArgumentError for an invalid encoding" do
+ x96 = ([150].pack('C')).force_encoding('utf-8')
+ -> { /(.).(.)/.match("Hello, #{x96} world!", -1) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "when passed a block" do
+ it "yields the MatchData" do
+ /./.match("abc") {|m| ScratchPad.record m }
+ ScratchPad.recorded.should.is_a?(MatchData)
+ end
+
+ it "returns the block result" do
+ /./.match("abc") { :result }.should == :result
+ end
+
+ it "does not yield if there is no match" do
+ ScratchPad.record []
+ /a/.match("b") {|m| ScratchPad << m }
+ ScratchPad.recorded.should == []
+ end
+ end
+ end
+
+ it "resets $~ if passed nil" do
+ # set $~
+ /./.match("a")
+ $~.should.is_a?(MatchData)
+
+ /1/.match(nil)
+ $~.should == nil
+ end
+
+ it "raises TypeError when the given argument cannot be coerced to String" do
+ f = 1
+ -> { /foo/.match(f)[0] }.should.raise(TypeError)
+ end
+
+ it "raises TypeError when the given argument is an Exception" do
+ f = Exception.new("foo")
+ -> { /foo/.match(f)[0] }.should.raise(TypeError)
+ end
+end
+
+describe "Regexp#match?" do
+ before :each do
+ # Resetting Regexp.last_match
+ /DONTMATCH/.match ''
+ end
+
+ context "when matches the given value" do
+ it "returns true but does not set Regexp.last_match" do
+ /string/i.match?('string').should == true
+ Regexp.last_match.should == nil
+ end
+ end
+
+ it "returns false when does not match the given value" do
+ /STRING/.match?('string').should == false
+ end
+
+ it "takes matching position as the 2nd argument" do
+ /str/i.match?('string', 0).should == true
+ /str/i.match?('string', 1).should == false
+ end
+
+ it "returns false when given nil" do
+ /./.match?(nil).should == false
+ end
+end
+
+describe "Regexp#~" do
+ it "matches against the contents of $_" do
+ $_ = "input data"
+ (~ /at/).should == 7
+ end
+end
diff --git a/spec/ruby/core/regexp/named_captures_spec.rb b/spec/ruby/core/regexp/named_captures_spec.rb
new file mode 100644
index 0000000000..4d3fdd23ab
--- /dev/null
+++ b/spec/ruby/core/regexp/named_captures_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#named_captures" do
+ it "returns a Hash" do
+ /foo/.named_captures.should.instance_of?(Hash)
+ end
+
+ it "returns an empty Hash when there are no capture groups" do
+ /foo/.named_captures.should == {}
+ end
+
+ it "sets the keys of the Hash to the names of the capture groups" do
+ rex = /this (?<is>is) [aA] (?<pat>pate?rn)/
+ rex.named_captures.keys.should == ['is','pat']
+ end
+
+ it "sets the values of the Hash to Arrays" do
+ rex = /this (?<is>is) [aA] (?<pat>pate?rn)/
+ rex.named_captures.values.each do |value|
+ value.should.instance_of?(Array)
+ end
+ end
+
+ it "sets each element of the Array to the corresponding group's index" do
+ rex = /this (?<is>is) [aA] (?<pat>pate?rn)/
+ rex.named_captures['is'].should == [1]
+ rex.named_captures['pat'].should == [2]
+ end
+
+ it "works with duplicate capture group names" do
+ rex = /this (?<is>is) [aA] (?<pat>pate?(?<is>rn))/
+ rex.named_captures['is'].should == [1,3]
+ rex.named_captures['pat'].should == [2]
+ end
+end
diff --git a/spec/ruby/core/regexp/names_spec.rb b/spec/ruby/core/regexp/names_spec.rb
new file mode 100644
index 0000000000..9013f41e20
--- /dev/null
+++ b/spec/ruby/core/regexp/names_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#names" do
+ it "returns an Array" do
+ /foo/.names.should.instance_of?(Array)
+ end
+
+ it "returns an empty Array if there are no named captures" do
+ /needle/.names.should == []
+ end
+
+ it "returns each named capture as a String" do
+ /n(?<cap>ee)d(?<ture>le)/.names.each do |name|
+ name.should.instance_of?(String)
+ end
+ end
+
+ it "returns all of the named captures" do
+ /n(?<cap>ee)d(?<ture>le)/.names.should == ['cap', 'ture']
+ end
+
+ it "works with nested named captures" do
+ /n(?<cap>eed(?<ture>le))/.names.should == ['cap', 'ture']
+ end
+
+ it "returns each capture name only once" do
+ /n(?<cap>ee)d(?<cap>le)/.names.should == ['cap']
+ end
+end
diff --git a/spec/ruby/core/regexp/new_spec.rb b/spec/ruby/core/regexp/new_spec.rb
new file mode 100644
index 0000000000..79210e9a23
--- /dev/null
+++ b/spec/ruby/core/regexp/new_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'shared/new'
+
+describe "Regexp.new" do
+ it_behaves_like :regexp_new, :new
+end
+
+describe "Regexp.new given a String" do
+ it_behaves_like :regexp_new_string, :new
+ it_behaves_like :regexp_new_string_binary, :new
+end
+
+describe "Regexp.new given a Regexp" do
+ it_behaves_like :regexp_new_regexp, :new
+end
+
+describe "Regexp.new given a non-String/Regexp" do
+ it_behaves_like :regexp_new_non_string_or_regexp, :new
+end
diff --git a/spec/ruby/core/regexp/options_spec.rb b/spec/ruby/core/regexp/options_spec.rb
new file mode 100644
index 0000000000..c3401cee6e
--- /dev/null
+++ b/spec/ruby/core/regexp/options_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#options" do
+ it "returns an Integer bitvector of regexp options for the Regexp object" do
+ /cat/.options.should.is_a?(Integer)
+ not_supported_on :opal do
+ /cat/ix.options.should.is_a?(Integer)
+ end
+ end
+
+ it "allows checking for presence of a certain option with bitwise &" do
+ (/cat/.options & Regexp::IGNORECASE).should == 0
+ (/cat/i.options & Regexp::IGNORECASE).should_not == 0
+ (/cat/.options & Regexp::MULTILINE).should == 0
+ (/cat/m.options & Regexp::MULTILINE).should_not == 0
+ not_supported_on :opal do
+ (/cat/.options & Regexp::EXTENDED).should == 0
+ (/cat/x.options & Regexp::EXTENDED).should_not == 0
+ (/cat/mx.options & Regexp::MULTILINE).should_not == 0
+ (/cat/mx.options & Regexp::EXTENDED).should_not == 0
+ (/cat/xi.options & Regexp::IGNORECASE).should_not == 0
+ (/cat/xi.options & Regexp::EXTENDED).should_not == 0
+ end
+ end
+
+ it "returns 0 for a Regexp literal without options" do
+ //.options.should == 0
+ /abc/.options.should == 0
+ end
+
+ it "raises a TypeError on an uninitialized Regexp" do
+ -> { Regexp.allocate.options }.should.raise(TypeError)
+ end
+
+ it "includes Regexp::FIXEDENCODING for a Regexp literal with the 'u' option" do
+ (//u.options & Regexp::FIXEDENCODING).should_not == 0
+ end
+
+ it "includes Regexp::FIXEDENCODING for a Regexp literal with the 'e' option" do
+ (//e.options & Regexp::FIXEDENCODING).should_not == 0
+ end
+
+ it "includes Regexp::FIXEDENCODING for a Regexp literal with the 's' option" do
+ (//s.options & Regexp::FIXEDENCODING).should_not == 0
+ end
+
+ it "does not include Regexp::FIXEDENCODING for a Regexp literal with the 'n' option" do
+ (//n.options & Regexp::FIXEDENCODING).should == 0
+ end
+
+ it "includes Regexp::NOENCODING for a Regexp literal with the 'n' option" do
+ (//n.options & Regexp::NOENCODING).should_not == 0
+ end
+end
diff --git a/spec/ruby/core/regexp/quote_spec.rb b/spec/ruby/core/regexp/quote_spec.rb
new file mode 100644
index 0000000000..370ab13e30
--- /dev/null
+++ b/spec/ruby/core/regexp/quote_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/quote'
+
+describe "Regexp.quote" do
+ it_behaves_like :regexp_quote, :quote
+end
diff --git a/spec/ruby/core/regexp/shared/equal_value.rb b/spec/ruby/core/regexp/shared/equal_value.rb
new file mode 100644
index 0000000000..803988de9e
--- /dev/null
+++ b/spec/ruby/core/regexp/shared/equal_value.rb
@@ -0,0 +1,31 @@
+describe :regexp_eql, shared: true do
+ it "is true if self and other have the same pattern" do
+ /abc/.send(@method, /abc/).should == true
+ /abc/.send(@method, /abd/).should == false
+ end
+
+ not_supported_on :opal do
+ it "is true if self and other have the same character set code" do
+ /abc/.send(@method, /abc/x).should == false
+ /abc/x.send(@method, /abc/x).should == true
+ /abc/u.send(@method, /abc/n).should == false
+ /abc/u.send(@method, /abc/u).should == true
+ /abc/n.send(@method, /abc/n).should == true
+ end
+ end
+
+ it "is true if other has the same #casefold? values" do
+ /abc/.send(@method, /abc/i).should == false
+ /abc/i.send(@method, /abc/i).should == true
+ end
+
+ not_supported_on :opal do
+ it "is true if self does not specify /n option and other does" do
+ //.send(@method, //n).should == true
+ end
+
+ it "is true if self specifies /n option and other does not" do
+ //n.send(@method, //).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/regexp/shared/new.rb b/spec/ruby/core/regexp/shared/new.rb
new file mode 100644
index 0000000000..affdaf855c
--- /dev/null
+++ b/spec/ruby/core/regexp/shared/new.rb
@@ -0,0 +1,321 @@
+# encoding: binary
+
+describe :regexp_new, shared: true do
+ it "requires one argument and creates a new regular expression object" do
+ Regexp.send(@method, '').is_a?(Regexp).should == true
+ end
+
+ ruby_version_is "4.1" do
+ it "is frozen" do
+ Regexp.send(@method, '').should.frozen?
+ end
+ end
+
+ it "works by default for subclasses with overridden #initialize" do
+ class RegexpSpecsSubclass < Regexp
+ def initialize(*args)
+ super
+ @args = args
+ end
+
+ attr_accessor :args
+ end
+
+ class RegexpSpecsSubclassTwo < Regexp; end
+
+ RegexpSpecsSubclass.send(@method, "hi").should.is_a?(RegexpSpecsSubclass)
+ RegexpSpecsSubclass.send(@method, "hi").args.first.should == "hi"
+
+ RegexpSpecsSubclassTwo.send(@method, "hi").should.is_a?(RegexpSpecsSubclassTwo)
+ end
+end
+
+describe :regexp_new_non_string_or_regexp, shared: true do
+ it "calls #to_str method for non-String/Regexp argument" do
+ obj = Object.new
+ def obj.to_str() "a" end
+
+ Regexp.send(@method, obj).should == /a/
+ end
+
+ it "raises TypeError if there is no #to_str method for non-String/Regexp argument" do
+ obj = Object.new
+ -> { Regexp.send(@method, obj) }.should.raise(TypeError, "no implicit conversion of Object into String")
+
+ -> { Regexp.send(@method, 1) }.should.raise(TypeError, "no implicit conversion of Integer into String")
+ -> { Regexp.send(@method, 1.0) }.should.raise(TypeError, "no implicit conversion of Float into String")
+ -> { Regexp.send(@method, :symbol) }.should.raise(TypeError, "no implicit conversion of Symbol into String")
+ -> { Regexp.send(@method, []) }.should.raise(TypeError, "no implicit conversion of Array into String")
+ end
+
+ it "raises TypeError if #to_str returns non-String value" do
+ obj = Object.new
+ def obj.to_str() [] end
+
+ -> { Regexp.send(@method, obj) }.should raise_consistent_error(TypeError, /can't convert Object into String/)
+ end
+end
+
+describe :regexp_new_string, shared: true do
+ it "uses the String argument as an unescaped literal to construct a Regexp object" do
+ Regexp.send(@method, "^hi{2,3}fo.o$").should == /^hi{2,3}fo.o$/
+ end
+
+ it "raises a RegexpError when passed an incorrect regexp" do
+ -> { Regexp.send(@method, "^[$", 0) }.should.raise(RegexpError, Regexp.new(Regexp.escape("premature end of char-class: /^[$/")))
+ end
+
+ it "does not set Regexp options if only given one argument" do
+ r = Regexp.send(@method, 'Hi')
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+ end
+
+ it "does not set Regexp options if second argument is nil or false" do
+ r = Regexp.send(@method, 'Hi', nil)
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+
+ r = Regexp.send(@method, 'Hi', false)
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+ end
+
+ it "sets options from second argument if it is true" do
+ r = Regexp.send(@method, 'Hi', true)
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+ end
+
+ it "sets options from second argument if it is one of the Integer option constants" do
+ r = Regexp.send(@method, 'Hi', Regexp::IGNORECASE)
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+
+ r = Regexp.send(@method, 'Hi', Regexp::MULTILINE)
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should_not == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+
+ not_supported_on :opal do
+ r = Regexp.send(@method, 'Hi', Regexp::EXTENDED)
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ (r.options & Regexp::EXTENDED).should_not == 1
+ end
+ end
+
+ it "accepts an Integer of two or more options ORed together as the second argument" do
+ r = Regexp.send(@method, 'Hi', Regexp::IGNORECASE | Regexp::EXTENDED)
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ (r.options & Regexp::EXTENDED).should_not == 0
+ end
+
+ it "does not try to convert the second argument to Integer with #to_int method call" do
+ ScratchPad.clear
+ obj = Object.new
+ def obj.to_int() ScratchPad.record(:called) end
+
+ -> {
+ Regexp.send(@method, "Hi", obj)
+ }.should complain(/expected true or false as ignorecase/, {verbose: true})
+
+ ScratchPad.recorded.should == nil
+ end
+
+ it "warns any non-Integer, non-nil, non-false second argument" do
+ r = nil
+ -> {
+ r = Regexp.send(@method, 'Hi', Object.new)
+ }.should complain(/expected true or false as ignorecase/, {verbose: true})
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+ end
+
+ it "accepts a String of supported flags as the second argument" do
+ r = Regexp.send(@method, 'Hi', 'i')
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+
+ r = Regexp.send(@method, 'Hi', 'imx')
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should_not == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should_not == 0
+ end
+
+ r = Regexp.send(@method, 'Hi', 'mimi')
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should_not == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+
+ r = Regexp.send(@method, 'Hi', '')
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+ end
+
+ it "raises an Argument error if the second argument contains unsupported chars" do
+ -> { Regexp.send(@method, 'Hi', 'e') }.should.raise(ArgumentError, "unknown regexp option: e")
+ -> { Regexp.send(@method, 'Hi', 'n') }.should.raise(ArgumentError, "unknown regexp option: n")
+ -> { Regexp.send(@method, 'Hi', 's') }.should.raise(ArgumentError, "unknown regexp option: s")
+ -> { Regexp.send(@method, 'Hi', 'u') }.should.raise(ArgumentError, "unknown regexp option: u")
+ -> { Regexp.send(@method, 'Hi', 'j') }.should.raise(ArgumentError, "unknown regexp option: j")
+ -> { Regexp.send(@method, 'Hi', 'mjx') }.should.raise(ArgumentError, /unknown regexp option: mjx\b/)
+ end
+
+ describe "with escaped characters" do
+ it "raises a Regexp error if there is a trailing backslash" do
+ -> { Regexp.send(@method, "\\") }.should.raise(RegexpError, Regexp.new(Regexp.escape("too short escape sequence: /\\/")))
+ end
+
+ it "does not raise a Regexp error if there is an escaped trailing backslash" do
+ -> { Regexp.send(@method, "\\\\") }.should_not.raise(RegexpError)
+ end
+
+ it "accepts a backspace followed by a non-special character" do
+ Regexp.send(@method, "\\N").should == /#{"\x5c"+"N"}/
+ end
+
+ it "raises a RegexpError if \\x is not followed by any hexadecimal digits" do
+ -> { Regexp.send(@method, "\\" + "xn") }.should.raise(RegexpError, Regexp.new(Regexp.escape("invalid hex escape: /\\xn/")))
+ end
+
+ it "raises a RegexpError if less than four digits are given for \\uHHHH" do
+ -> { Regexp.send(@method, "\\" + "u304") }.should.raise(RegexpError, Regexp.new(Regexp.escape("invalid Unicode escape: /\\u304/")))
+ end
+
+ it "raises a RegexpError if the \\u{} escape is empty" do
+ -> { Regexp.send(@method, "\\" + "u{}") }.should.raise(RegexpError, Regexp.new(Regexp.escape("invalid Unicode list: /\\u{}/")))
+ end
+
+ it "raises a RegexpError if the \\u{} escape contains non hexadecimal digits" do
+ -> { Regexp.send(@method, "\\" + "u{abcX}") }.should.raise(RegexpError, Regexp.new(Regexp.escape("invalid Unicode list: /\\u{abcX}/")))
+ end
+
+ it "raises a RegexpError if more than six hexadecimal digits are given" do
+ -> { Regexp.send(@method, "\\" + "u{0ffffff}") }.should.raise(RegexpError, Regexp.new(Regexp.escape("invalid Unicode range: /\\u{0ffffff}/")))
+ end
+
+ it "returns a Regexp with US-ASCII encoding if only 7-bit ASCII characters are present regardless of the input String's encoding" do
+ Regexp.send(@method, "abc").encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a Regexp with source String having US-ASCII encoding if only 7-bit ASCII characters are present regardless of the input String's encoding" do
+ Regexp.send(@method, "abc").source.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a Regexp with US-ASCII encoding if UTF-8 escape sequences using only 7-bit ASCII are present" do
+ Regexp.send(@method, "\u{61}").encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a Regexp with source String having US-ASCII encoding if UTF-8 escape sequences using only 7-bit ASCII are present" do
+ Regexp.send(@method, "\u{61}").source.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a Regexp with UTF-8 encoding if any UTF-8 escape sequences outside 7-bit ASCII are present" do
+ Regexp.send(@method, "\u{ff}").encoding.should == Encoding::UTF_8
+ end
+
+ it "returns a Regexp with source String having UTF-8 encoding if any UTF-8 escape sequences outside 7-bit ASCII are present" do
+ Regexp.send(@method, "\u{ff}").source.encoding.should == Encoding::UTF_8
+ end
+
+ it "returns a Regexp with the input String's encoding" do
+ str = "\x82\xa0".dup.force_encoding(Encoding::Shift_JIS)
+ Regexp.send(@method, str).encoding.should == Encoding::Shift_JIS
+ end
+
+ it "returns a Regexp with source String having the input String's encoding" do
+ str = "\x82\xa0".dup.force_encoding(Encoding::Shift_JIS)
+ Regexp.send(@method, str).source.encoding.should == Encoding::Shift_JIS
+ end
+ end
+end
+
+describe :regexp_new_string_binary, shared: true do
+ describe "with escaped characters" do
+ end
+end
+
+describe :regexp_new_regexp, shared: true do
+ it "uses the argument as a literal to construct a Regexp object" do
+ Regexp.send(@method, /^hi{2,3}fo.o$/).should == /^hi{2,3}fo.o$/
+ end
+
+ it "preserves any options given in the Regexp literal" do
+ (Regexp.send(@method, /Hi/i).options & Regexp::IGNORECASE).should_not == 0
+ (Regexp.send(@method, /Hi/m).options & Regexp::MULTILINE).should_not == 0
+ not_supported_on :opal do
+ (Regexp.send(@method, /Hi/x).options & Regexp::EXTENDED).should_not == 0
+ end
+
+ not_supported_on :opal do
+ r = Regexp.send @method, /Hi/imx
+ (r.options & Regexp::IGNORECASE).should_not == 0
+ (r.options & Regexp::MULTILINE).should_not == 0
+ (r.options & Regexp::EXTENDED).should_not == 0
+ end
+
+ r = Regexp.send @method, /Hi/
+ (r.options & Regexp::IGNORECASE).should == 0
+ (r.options & Regexp::MULTILINE).should == 0
+ not_supported_on :opal do
+ (r.options & Regexp::EXTENDED).should == 0
+ end
+ end
+
+ it "does not honour options given as additional arguments" do
+ r = nil
+ -> {
+ r = Regexp.send @method, /hi/, Regexp::IGNORECASE
+ }.should complain(/flags ignored/)
+ (r.options & Regexp::IGNORECASE).should == 0
+ end
+
+ not_supported_on :opal do
+ it "sets the encoding to UTF-8 if the Regexp literal has the 'u' option" do
+ Regexp.send(@method, /Hi/u).encoding.should == Encoding::UTF_8
+ end
+
+ it "sets the encoding to EUC-JP if the Regexp literal has the 'e' option" do
+ Regexp.send(@method, /Hi/e).encoding.should == Encoding::EUC_JP
+ end
+
+ it "sets the encoding to Windows-31J if the Regexp literal has the 's' option" do
+ Regexp.send(@method, /Hi/s).encoding.should == Encoding::Windows_31J
+ end
+
+ it "sets the encoding to US-ASCII if the Regexp literal has the 'n' option and the source String is ASCII only" do
+ Regexp.send(@method, /Hi/n).encoding.should == Encoding::US_ASCII
+ end
+ end
+end
diff --git a/spec/ruby/core/regexp/shared/quote.rb b/spec/ruby/core/regexp/shared/quote.rb
new file mode 100644
index 0000000000..083f12d78c
--- /dev/null
+++ b/spec/ruby/core/regexp/shared/quote.rb
@@ -0,0 +1,41 @@
+# encoding: binary
+
+describe :regexp_quote, shared: true do
+ it "escapes any characters with special meaning in a regular expression" do
+ Regexp.send(@method, '\*?{}.+^$[]()- ').should == '\\\\\*\?\{\}\.\+\^\$\[\]\(\)\-\\ '
+ Regexp.send(@method, "\*?{}.+^$[]()- ").should == '\\*\\?\\{\\}\\.\\+\\^\\$\\[\\]\\(\\)\\-\\ '
+ Regexp.send(@method, '\n\r\f\t').should == '\\\\n\\\\r\\\\f\\\\t'
+ Regexp.send(@method, "\n\r\f\t").should == '\\n\\r\\f\\t'
+ end
+
+ it "works with symbols" do
+ Regexp.send(@method, :symbol).should == 'symbol'
+ end
+
+ it "works with substrings" do
+ str = ".+[]()"[1...-1]
+ Regexp.send(@method, str).should == '\+\[\]\('
+ end
+
+ it "works for broken strings" do
+ Regexp.send(@method, "a.\x85b.".dup.force_encoding("US-ASCII")).should =="a\\.\x85b\\.".dup.force_encoding("US-ASCII")
+ Regexp.send(@method, "a.\x80".dup.force_encoding("UTF-8")).should == "a\\.\x80".dup.force_encoding("UTF-8")
+ end
+
+ it "sets the encoding of the result to US-ASCII if there are only US-ASCII characters present in the input String" do
+ str = "abc".dup.force_encoding("euc-jp")
+ Regexp.send(@method, str).encoding.should == Encoding::US_ASCII
+ end
+
+ it "sets the encoding of the result to the encoding of the String if any non-US-ASCII characters are present in an input String with valid encoding" do
+ str = "ã‚りãŒã¨ã†".dup.force_encoding("utf-8")
+ str.valid_encoding?.should == true
+ Regexp.send(@method, str).encoding.should == Encoding::UTF_8
+ end
+
+ it "sets the encoding of the result to BINARY if any non-US-ASCII characters are present in an input String with invalid encoding" do
+ str = "\xff".dup.force_encoding "us-ascii"
+ str.valid_encoding?.should == false
+ Regexp.send(@method, "\xff").encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/regexp/source_spec.rb b/spec/ruby/core/regexp/source_spec.rb
new file mode 100644
index 0000000000..4eebf280f0
--- /dev/null
+++ b/spec/ruby/core/regexp/source_spec.rb
@@ -0,0 +1,47 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Regexp#source" do
+ it "returns the original string of the pattern" do
+ not_supported_on :opal do
+ /ab+c/ix.source.should == "ab+c"
+ end
+ /x(.)xz/.source.should == "x(.)xz"
+ end
+
+ it "keeps escape sequences as is" do
+ /\x20\+/.source.should == '\x20\+'
+ end
+
+ describe "escaping" do
+ it "keeps escaping of metacharacter" do
+ /\$/.source.should == "\\$"
+ end
+
+ it "keeps escaping of metacharacter used as a terminator" do
+ %r+\++.source.should == "\\+"
+ end
+
+ it "removes escaping of non-metacharacter used as a terminator" do
+ %r@\@@.source.should == "@"
+ end
+
+ it "keeps escaping of non-metacharacter not used as a terminator" do
+ /\@/.source.should == "\\@"
+ end
+ end
+
+ not_supported_on :opal do
+ it "has US-ASCII encoding when created from an ASCII-only \\u{} literal" do
+ re = /[\u{20}-\u{7E}]/
+ re.source.encoding.should.equal?(Encoding::US_ASCII)
+ end
+ end
+
+ not_supported_on :opal do
+ it "has UTF-8 encoding when created from a non-ASCII-only \\u{} literal" do
+ re = /[\u{20}-\u{7EE}]/
+ re.source.encoding.should.equal?(Encoding::UTF_8)
+ end
+ end
+end
diff --git a/spec/ruby/core/regexp/timeout_spec.rb b/spec/ruby/core/regexp/timeout_spec.rb
new file mode 100644
index 0000000000..a1ec475ef3
--- /dev/null
+++ b/spec/ruby/core/regexp/timeout_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "Regexp.timeout" do
+ after :each do
+ Regexp.timeout = nil
+ end
+
+ it "returns global timeout" do
+ Regexp.timeout = 3
+ Regexp.timeout.should == 3
+ end
+
+ it "raises Regexp::TimeoutError after global timeout elapsed" do
+ Regexp.timeout = 0.001
+ Regexp.timeout.should == 0.001
+
+ -> {
+ # A typical ReDoS case
+ /^(a*)*$/ =~ "a" * 1000000 + "x"
+ }.should.raise(Regexp::TimeoutError, "regexp match timeout")
+ end
+
+ it "raises Regexp::TimeoutError after timeout keyword value elapsed" do
+ Regexp.timeout = 3 # This should be ignored
+ Regexp.timeout.should == 3
+
+ re = Regexp.new("^a*b?a*$", timeout: 0.001)
+
+ -> {
+ re =~ "a" * 1000000 + "x"
+ }.should.raise(Regexp::TimeoutError, "regexp match timeout")
+ end
+end
diff --git a/spec/ruby/core/regexp/to_s_spec.rb b/spec/ruby/core/regexp/to_s_spec.rb
new file mode 100644
index 0000000000..798eaee6c2
--- /dev/null
+++ b/spec/ruby/core/regexp/to_s_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+
+describe "Regexp#to_s" do
+ not_supported_on :opal do
+ it "displays options if included" do
+ /abc/mxi.to_s.should == "(?mix:abc)"
+ end
+ end
+
+ it "shows non-included options after a - sign" do
+ /abc/i.to_s.should == "(?i-mx:abc)"
+ end
+
+ it "shows all options as excluded if none are selected" do
+ /abc/.to_s.should == "(?-mix:abc)"
+ end
+
+ it "shows the pattern after the options" do
+ not_supported_on :opal do
+ /ab+c/mix.to_s.should == "(?mix:ab+c)"
+ end
+ /xyz/.to_s.should == "(?-mix:xyz)"
+ end
+
+ not_supported_on :opal do
+ it "displays groups with options" do
+ /(?ix:foo)(?m:bar)/.to_s.should == "(?-mix:(?ix:foo)(?m:bar))"
+ /(?ix:foo)bar/m.to_s.should == "(?m-ix:(?ix:foo)bar)"
+ end
+
+ it "displays single group with same options as main regex as the main regex" do
+ /(?i:nothing outside this group)/.to_s.should == "(?i-mx:nothing outside this group)"
+ end
+ end
+
+ not_supported_on :opal do
+ it "deals properly with uncaptured groups" do
+ /whatever(?:0d)/ix.to_s.should == "(?ix-m:whatever(?:0d))"
+ end
+ end
+
+ it "deals properly with the two types of lookahead groups" do
+ /(?=5)/.to_s.should == "(?-mix:(?=5))"
+ /(?!5)/.to_s.should == "(?-mix:(?!5))"
+ end
+
+ it "returns a string in (?xxx:yyy) notation" do
+ not_supported_on :opal do
+ /ab+c/ix.to_s.should == "(?ix-m:ab+c)"
+ /jis/s.to_s.should == "(?-mix:jis)"
+ /(?i:.)/.to_s.should == "(?i-mx:.)"
+ end
+ /(?:.)/.to_s.should == "(?-mix:.)"
+ end
+
+ not_supported_on :opal do
+ it "handles abusive option groups" do
+ /(?mmmmix-miiiix:)/.to_s.should == '(?-mix:)'
+ end
+ end
+
+end
diff --git a/spec/ruby/core/regexp/try_convert_spec.rb b/spec/ruby/core/regexp/try_convert_spec.rb
new file mode 100644
index 0000000000..da5e10adce
--- /dev/null
+++ b/spec/ruby/core/regexp/try_convert_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+
+describe "Regexp.try_convert" do
+ not_supported_on :opal do
+ it "returns the argument if given a Regexp" do
+ Regexp.try_convert(/foo/s).should == /foo/s
+ end
+ end
+
+ it "returns nil if given an argument that can't be converted to a Regexp" do
+ ['', 'glark', [], Object.new, :pat].each do |arg|
+ Regexp.try_convert(arg).should == nil
+ end
+ end
+
+ it "tries to coerce the argument by calling #to_regexp" do
+ rex = mock('regexp')
+ rex.should_receive(:to_regexp).and_return(/(p(a)t[e]rn)/)
+ Regexp.try_convert(rex).should == /(p(a)t[e]rn)/
+ end
+
+ it "raises a TypeError if the object does not return an Regexp from #to_regexp" do
+ obj = mock("regexp")
+ obj.should_receive(:to_regexp).and_return("string")
+ -> { Regexp.try_convert(obj) }.should raise_consistent_error(TypeError, "can't convert MockObject into Regexp (MockObject#to_regexp gives String)")
+ end
+end
diff --git a/spec/ruby/core/regexp/union_spec.rb b/spec/ruby/core/regexp/union_spec.rb
new file mode 100644
index 0000000000..c0a9d12fed
--- /dev/null
+++ b/spec/ruby/core/regexp/union_spec.rb
@@ -0,0 +1,182 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+
+describe "Regexp.union" do
+ it "returns /(?!)/ when passed no arguments" do
+ Regexp.union.should == /(?!)/
+ end
+
+ it "returns a regular expression that will match passed arguments" do
+ Regexp.union("penzance").should == /penzance/
+ Regexp.union("skiing", "sledding").should == /skiing|sledding/
+ not_supported_on :opal do
+ Regexp.union(/dogs/, /cats/i).should == /(?-mix:dogs)|(?i-mx:cats)/
+ end
+ end
+
+ it "quotes any string arguments" do
+ Regexp.union("n", ".").should == /n|\./
+ end
+
+ it "returns a Regexp with the encoding of an ASCII-incompatible String argument" do
+ Regexp.union("a".encode("UTF-16LE")).encoding.should == Encoding::UTF_16LE
+ end
+
+ it "returns a Regexp with the encoding of a String containing non-ASCII-compatible characters" do
+ Regexp.union("\u00A9".encode("ISO-8859-1")).encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "returns a Regexp with US-ASCII encoding if all arguments are ASCII-only" do
+ Regexp.union("a".encode("UTF-8"), "b".encode("SJIS")).encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a Regexp with the encoding of multiple non-conflicting ASCII-incompatible String arguments" do
+ Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-16LE")).encoding.should == Encoding::UTF_16LE
+ end
+
+ it "returns a Regexp with the encoding of multiple non-conflicting Strings containing non-ASCII-compatible characters" do
+ Regexp.union("\u00A9".encode("ISO-8859-1"), "\u00B0".encode("ISO-8859-1")).encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "returns a Regexp with the encoding of a String containing non-ASCII-compatible characters and another ASCII-only String" do
+ Regexp.union("\u00A9".encode("ISO-8859-1"), "a".encode("UTF-8")).encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "returns ASCII-8BIT if the regexp encodings are ASCII-8BIT and at least one has non-ASCII characters" do
+ us_ascii_implicit, us_ascii_explicit, binary = /abc/, /[\x00-\x7f]/n, /[\x80-\xBF]/n
+ us_ascii_implicit.encoding.should == Encoding::US_ASCII
+ us_ascii_explicit.encoding.should == Encoding::US_ASCII
+ binary.encoding.should == Encoding::BINARY
+
+ Regexp.union(us_ascii_implicit, us_ascii_explicit, binary).encoding.should == Encoding::BINARY
+ Regexp.union(us_ascii_implicit, binary, us_ascii_explicit).encoding.should == Encoding::BINARY
+ Regexp.union(us_ascii_explicit, us_ascii_implicit, binary).encoding.should == Encoding::BINARY
+ Regexp.union(us_ascii_explicit, binary, us_ascii_implicit).encoding.should == Encoding::BINARY
+ Regexp.union(binary, us_ascii_implicit, us_ascii_explicit).encoding.should == Encoding::BINARY
+ Regexp.union(binary, us_ascii_explicit, us_ascii_implicit).encoding.should == Encoding::BINARY
+ end
+
+ it "return US-ASCII if all patterns are ASCII-only" do
+ Regexp.union(/abc/e, /def/e).encoding.should == Encoding::US_ASCII
+ Regexp.union(/abc/n, /def/n).encoding.should == Encoding::US_ASCII
+ Regexp.union(/abc/s, /def/s).encoding.should == Encoding::US_ASCII
+ Regexp.union(/abc/u, /def/u).encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns a Regexp with UTF-8 if one part is UTF-8" do
+ Regexp.union(/probl[éeè]me/i, /help/i).encoding.should == Encoding::UTF_8
+ end
+
+ it "returns a Regexp if an array of string with special characters is passed" do
+ Regexp.union(["+","-"]).should == /\+|\-/
+ end
+
+ it "raises ArgumentError if the arguments include conflicting ASCII-incompatible Strings" do
+ -> {
+ Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-16BE"))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-16LE and UTF-16BE')
+ end
+
+ it "raises ArgumentError if the arguments include conflicting ASCII-incompatible Regexps" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-16LE")),
+ Regexp.new("b".encode("UTF-16BE")))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-16LE and UTF-16BE')
+ end
+
+ it "raises ArgumentError if the arguments include conflicting fixed encoding Regexps" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING),
+ Regexp.new("b".encode("US-ASCII"), Regexp::FIXEDENCODING))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-8 and US-ASCII')
+ end
+
+ it "raises ArgumentError if the arguments include a fixed encoding Regexp and a String containing non-ASCII-compatible characters in a different encoding" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING),
+ "\u00A9".encode("ISO-8859-1"))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-8 and ISO-8859-1')
+ end
+
+ it "raises ArgumentError if the arguments include a String containing non-ASCII-compatible characters and a fixed encoding Regexp in a different encoding" do
+ -> {
+ Regexp.union("\u00A9".encode("ISO-8859-1"),
+ Regexp.new("a".encode("UTF-8"), Regexp::FIXEDENCODING))
+ }.should.raise(ArgumentError, 'incompatible encodings: ISO-8859-1 and UTF-8')
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible String and an ASCII-only String" do
+ -> {
+ Regexp.union("a".encode("UTF-16LE"), "b".encode("UTF-8"))
+ }.should.raise(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/)
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and an ASCII-only String" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-16LE")), "b".encode("UTF-8"))
+ }.should.raise(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/)
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible String and an ASCII-only Regexp" do
+ -> {
+ Regexp.union("a".encode("UTF-16LE"), Regexp.new("b".encode("UTF-8")))
+ }.should.raise(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/)
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and an ASCII-only Regexp" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("b".encode("UTF-8")))
+ }.should.raise(ArgumentError, /ASCII incompatible encoding: UTF-16LE|incompatible encodings: UTF-16LE and US-ASCII/)
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible String and a String containing non-ASCII-compatible characters in a different encoding" do
+ -> {
+ Regexp.union("a".encode("UTF-16LE"), "\u00A9".encode("ISO-8859-1"))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1')
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and a String containing non-ASCII-compatible characters in a different encoding" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-16LE")), "\u00A9".encode("ISO-8859-1"))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1')
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible String and a Regexp containing non-ASCII-compatible characters in a different encoding" do
+ -> {
+ Regexp.union("a".encode("UTF-16LE"), Regexp.new("\u00A9".encode("ISO-8859-1")))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1')
+ end
+
+ it "raises ArgumentError if the arguments include an ASCII-incompatible Regexp and a Regexp containing non-ASCII-compatible characters in a different encoding" do
+ -> {
+ Regexp.union(Regexp.new("a".encode("UTF-16LE")), Regexp.new("\u00A9".encode("ISO-8859-1")))
+ }.should.raise(ArgumentError, 'incompatible encodings: UTF-16LE and ISO-8859-1')
+ end
+
+ it "uses to_str to convert arguments (if not Regexp)" do
+ obj = mock('pattern')
+ obj.should_receive(:to_str).and_return('foo')
+ Regexp.union(obj, "bar").should == /foo|bar/
+ end
+
+ it "uses to_regexp to convert argument" do
+ obj = mock('pattern')
+ obj.should_receive(:to_regexp).and_return(/foo/)
+ Regexp.union(obj).should == /foo/
+ end
+
+ it "accepts a Symbol as argument" do
+ Regexp.union(:foo).should == /foo/
+ end
+
+ it "accepts a single array of patterns as arguments" do
+ Regexp.union(["skiing", "sledding"]).should == /skiing|sledding/
+ not_supported_on :opal do
+ Regexp.union([/dogs/, /cats/i]).should == /(?-mix:dogs)|(?i-mx:cats)/
+ end
+ -> {
+ Regexp.union(["skiing", "sledding"], [/dogs/, /cats/i])
+ }.should.raise(TypeError, 'no implicit conversion of Array into String')
+ end
+end
diff --git a/spec/ruby/core/set/add_spec.rb b/spec/ruby/core/set/add_spec.rb
new file mode 100644
index 0000000000..1ce03b1eab
--- /dev/null
+++ b/spec/ruby/core/set/add_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative 'shared/add'
+
+describe "Set#add" do
+ it_behaves_like :set_add, :add
+end
+
+describe "Set#add?" do
+ before :each do
+ @set = Set.new
+ end
+
+ it "adds the passed Object to self" do
+ @set.add?("cat")
+ @set.should.include?("cat")
+ end
+
+ it "returns self when the Object has not yet been added to self" do
+ @set.add?("cat").should.equal?(@set)
+ end
+
+ it "returns nil when the Object has already been added to self" do
+ @set.add?("cat")
+ @set.add?("cat").should == nil
+ end
+
+ it "raises RuntimeError when called during iteration" do
+ set = Set[:a, :b, :c, :d, :e, :f]
+ set.each do |_m|
+ -> { set << 1 }.should.raise(RuntimeError, /iteration/)
+ end
+ set.should == Set[:a, :b, :c, :d, :e, :f]
+ end
+end
diff --git a/spec/ruby/core/set/append_spec.rb b/spec/ruby/core/set/append_spec.rb
new file mode 100644
index 0000000000..82d34d9130
--- /dev/null
+++ b/spec/ruby/core/set/append_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/add'
+
+describe "Set#<<" do
+ it_behaves_like :set_add, :<<
+end
diff --git a/spec/ruby/core/set/case_compare_spec.rb b/spec/ruby/core/set/case_compare_spec.rb
new file mode 100644
index 0000000000..3781b1b963
--- /dev/null
+++ b/spec/ruby/core/set/case_compare_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "Set#===" do
+ it_behaves_like :set_include, :===
+
+ it "is an alias for include?" do
+ set = Set.new
+ set.method(:===).should == set.method(:include?)
+ end
+end
diff --git a/spec/ruby/core/set/case_equality_spec.rb b/spec/ruby/core/set/case_equality_spec.rb
new file mode 100644
index 0000000000..19c1fb6b9c
--- /dev/null
+++ b/spec/ruby/core/set/case_equality_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "Set#===" do
+ it_behaves_like :set_include, :===
+end
diff --git a/spec/ruby/core/set/classify_spec.rb b/spec/ruby/core/set/classify_spec.rb
new file mode 100644
index 0000000000..a225ab7cbb
--- /dev/null
+++ b/spec/ruby/core/set/classify_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Set#classify" do
+ before :each do
+ @set = Set["one", "two", "three", "four"]
+ end
+
+ it "yields each Object in self" do
+ res = []
+ @set.classify { |x| res << x }
+ res.sort.should == ["one", "two", "three", "four"].sort
+ end
+
+ it "returns an Enumerator when passed no block" do
+ enum = @set.classify
+ enum.should.instance_of?(Enumerator)
+
+ classified = enum.each { |x| x.length }
+ classified.should == { 3 => Set["one", "two"], 4 => Set["four"], 5 => Set["three"] }
+ end
+
+ it "classifies the Objects in self based on the block's return value" do
+ classified = @set.classify { |x| x.length }
+ classified.should == { 3 => Set["one", "two"], 4 => Set["four"], 5 => Set["three"] }
+ end
+end
diff --git a/spec/ruby/core/set/clear_spec.rb b/spec/ruby/core/set/clear_spec.rb
new file mode 100644
index 0000000000..c61a0d78f2
--- /dev/null
+++ b/spec/ruby/core/set/clear_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Set#clear" do
+ before :each do
+ @set = Set["one", "two", "three", "four"]
+ end
+
+ it "removes all elements from self" do
+ @set.clear
+ @set.should.empty?
+ end
+
+ it "returns self" do
+ @set.clear.should.equal?(@set)
+ end
+end
diff --git a/spec/ruby/core/set/collect_spec.rb b/spec/ruby/core/set/collect_spec.rb
new file mode 100644
index 0000000000..d186f1a0d9
--- /dev/null
+++ b/spec/ruby/core/set/collect_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/collect'
+
+describe "Set#collect!" do
+ it_behaves_like :set_collect_bang, :collect!
+end
diff --git a/spec/ruby/core/set/compare_by_identity_spec.rb b/spec/ruby/core/set/compare_by_identity_spec.rb
new file mode 100644
index 0000000000..458e760da0
--- /dev/null
+++ b/spec/ruby/core/set/compare_by_identity_spec.rb
@@ -0,0 +1,153 @@
+require_relative '../../spec_helper'
+
+describe "Set#compare_by_identity" do
+ it "compares its members by identity" do
+ a = "a"
+ b1 = "b"
+ b2 = b1.dup
+
+ set = Set.new
+ set.compare_by_identity
+ set.merge([a, a, b1, b2])
+ set.to_a.sort.should == [a, b1, b2].sort
+ end
+
+ it "causes future comparisons on the receiver to be made by identity" do
+ elt = [1]
+ set = Set.new
+ set << elt
+ set.member?(elt.dup).should == true
+ set.compare_by_identity
+ set.member?(elt.dup).should == false
+ end
+
+ it "rehashes internally so that old members can be looked up" do
+ set = Set.new
+ (1..10).each { |k| set << k }
+ o = Object.new
+ def o.hash; 123; end
+ set << o
+ set.compare_by_identity
+ set.member?(o).should == true
+ end
+
+ it "returns self" do
+ set = Set.new
+ result = set.compare_by_identity
+ result.should.equal?(set)
+ end
+
+ it "is idempotent and has no effect on an already compare_by_identity set" do
+ set = Set.new.compare_by_identity
+ set << :foo
+ set.compare_by_identity.should.equal?(set)
+ set.should.compare_by_identity?
+ set.to_a.should == [:foo]
+ end
+
+ it "uses the semantics of BasicObject#equal? to determine members identity" do
+ :a.equal?(:a).should == true
+ Set.new.compare_by_identity.merge([:a, :a]).to_a.should == [:a]
+
+ ary1 = [1]
+ ary2 = [1]
+ ary1.equal?(ary2).should == false
+ Set.new.compare_by_identity.merge([ary1, ary2]).to_a.sort.should == [ary1, ary2].sort
+ end
+
+ it "uses #equal? semantics, but doesn't actually call #equal? to determine identity" do
+ set = Set.new.compare_by_identity
+ obj = mock("equal")
+ obj.should_not_receive(:equal?)
+ set << :foo
+ set << obj
+ set.to_a.should == [:foo, obj]
+ end
+
+ it "does not call #hash on members" do
+ elt = mock("element")
+ elt.should_not_receive(:hash)
+ set = Set.new.compare_by_identity
+ set << elt
+ set.member?(elt).should == true
+ end
+
+ it "regards #dup'd objects as having different identities" do
+ a1 = "a"
+ a2 = a1.dup
+
+ set = Set.new.compare_by_identity
+ set.merge([a1, a2])
+ set.to_a.sort.should == [a1, a2].sort
+ end
+
+ it "regards #clone'd objects as having different identities" do
+ a1 = "a"
+ a2 = a1.clone
+
+ set = Set.new.compare_by_identity
+ set.merge([a1, a2])
+ set.to_a.sort.should == [a1, a2].sort
+ end
+
+ ruby_version_is "4.0" do
+ it "raises a FrozenError on frozen sets" do
+ set = Set.new.freeze
+ -> {
+ set.compare_by_identity
+ }.should.raise(FrozenError, /can't modify frozen Set: (#<)?Set(\[|: {)[\]}]>?/)
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "raises a FrozenError on frozen sets" do
+ set = Set.new.freeze
+ -> {
+ set.compare_by_identity
+ }.should.raise(FrozenError, /frozen Hash/)
+ end
+ end
+
+ it "persists over #dups" do
+ set = Set.new.compare_by_identity
+ set << :a
+ set_dup = set.dup
+ set_dup.should == set
+ set_dup << :a
+ set_dup.to_a.should == [:a]
+ end
+
+ it "persists over #clones" do
+ set = Set.new.compare_by_identity
+ set << :a
+ set_clone = set.clone
+ set_clone.should == set
+ set_clone << :a
+ set_clone.to_a.should == [:a]
+ end
+
+ it "is not equal to set what does not compare by identity" do
+ Set.new([1, 2]).should == Set.new([1, 2])
+ Set.new([1, 2]).should_not == Set.new([1, 2]).compare_by_identity
+ end
+end
+
+describe "Set#compare_by_identity?" do
+ it "returns false by default" do
+ Set.new.should_not.compare_by_identity?
+ end
+
+ it "returns true once #compare_by_identity has been invoked on self" do
+ set = Set.new
+ set.compare_by_identity
+ set.should.compare_by_identity?
+ end
+
+ it "returns true when called multiple times on the same set" do
+ set = Set.new
+ set.compare_by_identity
+ set.should.compare_by_identity?
+ set.should.compare_by_identity?
+ set.should.compare_by_identity?
+ end
+end
diff --git a/spec/ruby/core/set/comparison_spec.rb b/spec/ruby/core/set/comparison_spec.rb
new file mode 100644
index 0000000000..eb18a198e5
--- /dev/null
+++ b/spec/ruby/core/set/comparison_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Set#<=>" do
+ it "returns 0 if the sets are equal" do
+ (Set[] <=> Set[]).should == 0
+ (Set[:a, :b, :c] <=> Set[:a, :b, :c]).should == 0
+ end
+
+ it "returns -1 if the set is a proper subset of the other set" do
+ (Set[] <=> Set[1]).should == -1
+ (Set[1, 2] <=> Set[1, 2, 3]).should == -1
+ end
+
+ it "returns +1 if the set is a proper superset of other set" do
+ (Set[1] <=> Set[]).should == +1
+ (Set[1, 2, 3] <=> Set[1, 2]).should == +1
+ end
+
+ it "returns nil if the set has unique elements" do
+ (Set[1, 2, 3] <=> Set[:a, :b, :c]).should == nil
+ end
+
+ it "returns nil when the argument is not set-like" do
+ (Set[] <=> false).should == nil
+ end
+end
diff --git a/spec/ruby/core/set/constructor_spec.rb b/spec/ruby/core/set/constructor_spec.rb
new file mode 100644
index 0000000000..11138f3a5b
--- /dev/null
+++ b/spec/ruby/core/set/constructor_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "Set[]" do
+ it "returns a new Set populated with the passed Objects" do
+ set = Set[1, 2, 3]
+
+ set.instance_of?(Set).should == true
+ set.size.should.eql?(3)
+
+ set.should.include?(1)
+ set.should.include?(2)
+ set.should.include?(3)
+ end
+end
diff --git a/spec/ruby/core/set/delete_if_spec.rb b/spec/ruby/core/set/delete_if_spec.rb
new file mode 100644
index 0000000000..b231dff50d
--- /dev/null
+++ b/spec/ruby/core/set/delete_if_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "Set#delete_if" do
+ before :each do
+ @set = Set["one", "two", "three"]
+ end
+
+ it "yields every element of self" do
+ ret = []
+ @set.delete_if { |x| ret << x }
+ ret.sort.should == ["one", "two", "three"].sort
+ end
+
+ it "deletes every element from self for which the passed block returns true" do
+ @set.delete_if { |x| x.size == 3 }
+ @set.size.should.eql?(1)
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+
+ it "returns self" do
+ @set.delete_if { |x| x }.should.equal?(@set)
+ end
+
+ it "returns an Enumerator when passed no block" do
+ enum = @set.delete_if
+ enum.should.instance_of?(Enumerator)
+
+ enum.each { |x| x.size == 3 }
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+end
diff --git a/spec/ruby/core/set/delete_spec.rb b/spec/ruby/core/set/delete_spec.rb
new file mode 100644
index 0000000000..cdc6dd7b36
--- /dev/null
+++ b/spec/ruby/core/set/delete_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Set#delete" do
+ before :each do
+ @set = Set["a", "b", "c"]
+ end
+
+ it "deletes the passed Object from self" do
+ @set.delete("a")
+ @set.should_not.include?("a")
+ end
+
+ it "returns self" do
+ @set.delete("a").should.equal?(@set)
+ @set.delete("x").should.equal?(@set)
+ end
+end
+
+describe "Set#delete?" do
+ before :each do
+ @set = Set["a", "b", "c"]
+ end
+
+ it "deletes the passed Object from self" do
+ @set.delete?("a")
+ @set.should_not.include?("a")
+ end
+
+ it "returns self when the passed Object is in self" do
+ @set.delete?("a").should.equal?(@set)
+ end
+
+ it "returns nil when the passed Object is not in self" do
+ @set.delete?("x").should == nil
+ end
+end
diff --git a/spec/ruby/core/set/difference_spec.rb b/spec/ruby/core/set/difference_spec.rb
new file mode 100644
index 0000000000..149f946592
--- /dev/null
+++ b/spec/ruby/core/set/difference_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/difference'
+
+describe "Set#difference" do
+ it_behaves_like :set_difference, :difference
+end
diff --git a/spec/ruby/core/set/disjoint_spec.rb b/spec/ruby/core/set/disjoint_spec.rb
new file mode 100644
index 0000000000..d415c21045
--- /dev/null
+++ b/spec/ruby/core/set/disjoint_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+
+describe "Set#disjoint?" do
+ it "returns false when two Sets have at least one element in common" do
+ Set[1, 2].disjoint?(Set[2, 3]).should == false
+ end
+
+ it "returns true when two Sets have no element in common" do
+ Set[1, 2].disjoint?(Set[3, 4]).should == true
+ end
+
+ context "when comparing to a Set-like object" do
+ it "returns false when a Set has at least one element in common with a Set-like object" do
+ Set[1, 2].disjoint?(SetSpecs::SetLike.new([2, 3])).should == false
+ end
+
+ it "returns true when a Set has no element in common with a Set-like object" do
+ Set[1, 2].disjoint?(SetSpecs::SetLike.new([3, 4])).should == true
+ end
+ end
+end
diff --git a/spec/ruby/core/set/divide_spec.rb b/spec/ruby/core/set/divide_spec.rb
new file mode 100644
index 0000000000..409a22df75
--- /dev/null
+++ b/spec/ruby/core/set/divide_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+
+describe "Set#divide" do
+ it "divides self into a set of subsets based on the blocks return values" do
+ set = Set["one", "two", "three", "four", "five"].divide { |x| x.length }
+ set.map { |x| x.to_a.sort }.sort.should == [["five", "four"], ["one", "two"], ["three"]]
+ end
+
+ it "yields each Object to the block" do
+ ret = []
+ Set["one", "two", "three", "four", "five"].divide { |x| ret << x }
+ ret.sort.should == ["five", "four", "one", "three", "two"]
+ end
+
+ it "returns an enumerator when not passed a block" do
+ ret = Set[1, 2, 3, 4].divide
+ ret.should.is_a?(Enumerator)
+ ret.each(&:even?).should == Set[Set[1, 3], Set[2, 4]]
+ end
+end
+
+describe "Set#divide when passed a block with an arity of 2" do
+ it "divides self into a set of subsets based on the blocks return values" do
+ set = Set[1, 3, 4, 6, 9, 10, 11].divide { |x, y| (x - y).abs == 1 }
+ set.map{ |x| x.to_a.sort }.sort.should == [[1], [3, 4], [6], [9, 10, 11]]
+ end
+
+ ruby_version_is "4.0" do
+ it "yields each two Object to the block" do
+ ret = []
+ Set[1, 2].divide { |x, y| ret << [x, y] }
+ ret.sort.should == [[1, 2], [2, 1]]
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "yields each two Object to the block" do
+ ret = []
+ Set[1, 2].divide { |x, y| ret << [x, y] }
+ ret.sort.should == [[1, 1], [1, 2], [2, 1], [2, 2]]
+ end
+ end
+
+ it "returns an enumerator when not passed a block" do
+ ret = Set[1, 2, 3, 4].divide
+ ret.should.is_a?(Enumerator)
+ ret.each { |a, b| (a + b).even? }.should == Set[Set[1, 3], Set[2, 4]]
+ end
+end
+
+describe "Set#divide when passed a block with an arity of > 2" do
+ it "only uses the first element if the arity > 2" do
+ set = Set["one", "two", "three", "four", "five"].divide do |x, y, z|
+ y.should == nil
+ z.should == nil
+ x.length
+ end
+ set.map { |x| x.to_a.sort }.sort.should == [["five", "four"], ["one", "two"], ["three"]]
+ end
+
+ it "only uses the first element if the arity = -1" do
+ set = Set["one", "two", "three", "four", "five"].divide do |*xs|
+ xs.size.should == 1
+ xs.first.length
+ end
+ set.map { |x| x.to_a.sort }.sort.should == [["five", "four"], ["one", "two"], ["three"]]
+ end
+end
diff --git a/spec/ruby/core/set/each_spec.rb b/spec/ruby/core/set/each_spec.rb
new file mode 100644
index 0000000000..bdafc99571
--- /dev/null
+++ b/spec/ruby/core/set/each_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "Set#each" do
+ before :each do
+ @set = Set[1, 2, 3]
+ end
+
+ it "yields each Object in self" do
+ ret = []
+ @set.each { |x| ret << x }
+ ret.sort.should == [1, 2, 3]
+ end
+
+ it "returns self" do
+ @set.each { |x| x }.should.equal?(@set)
+ end
+
+ it "returns an Enumerator when not passed a block" do
+ enum = @set.each
+ enum.should.instance_of?(Enumerator)
+
+ ret = []
+ enum.each { |x| ret << x }
+ ret.sort.should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/set/empty_spec.rb b/spec/ruby/core/set/empty_spec.rb
new file mode 100644
index 0000000000..c71f2ce18d
--- /dev/null
+++ b/spec/ruby/core/set/empty_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Set#empty?" do
+ it "returns true if self is empty" do
+ Set[].empty?.should == true
+ Set[1].empty?.should == false
+ Set[1,2,3].empty?.should == false
+ end
+end
diff --git a/spec/ruby/core/set/enumerable/to_set_spec.rb b/spec/ruby/core/set/enumerable/to_set_spec.rb
new file mode 100644
index 0000000000..f139e1c025
--- /dev/null
+++ b/spec/ruby/core/set/enumerable/to_set_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../../spec_helper'
+
+describe "Enumerable#to_set" do
+ it "returns a new Set created from self" do
+ [1, 2, 3].to_set.should == Set[1, 2, 3]
+ {a: 1, b: 2}.to_set.should == Set[[:b, 2], [:a, 1]]
+ end
+
+ it "passes down passed blocks" do
+ [1, 2, 3].to_set { |x| x * x }.should == Set[1, 4, 9]
+ end
+end
diff --git a/spec/ruby/core/set/eql_spec.rb b/spec/ruby/core/set/eql_spec.rb
new file mode 100644
index 0000000000..6862ed4eda
--- /dev/null
+++ b/spec/ruby/core/set/eql_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "Set#eql?" do
+ it "returns true when the passed argument is a Set and contains the same elements" do
+ Set[].should.eql?(Set[])
+ Set[1, 2, 3].should.eql?(Set[1, 2, 3])
+ Set[1, 2, 3].should.eql?(Set[3, 2, 1])
+ Set["a", :b, ?c].should.eql?(Set[?c, :b, "a"])
+
+ Set[1, 2, 3].should_not.eql?(Set[1.0, 2, 3])
+ Set[1, 2, 3].should_not.eql?(Set[2, 3])
+ Set[1, 2, 3].should_not.eql?(Set[])
+ end
+end
diff --git a/spec/ruby/core/set/equal_value_spec.rb b/spec/ruby/core/set/equal_value_spec.rb
new file mode 100644
index 0000000000..721a79a3f1
--- /dev/null
+++ b/spec/ruby/core/set/equal_value_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+
+describe "Set#==" do
+ it "returns true when the passed Object is a Set and self and the Object contain the same elements" do
+ Set[].should == Set[]
+ Set[1, 2, 3].should == Set[1, 2, 3]
+ Set["1", "2", "3"].should == Set["1", "2", "3"]
+
+ Set[1, 2, 3].should_not == Set[1.0, 2, 3]
+ Set[1, 2, 3].should_not == [1, 2, 3]
+ end
+
+ it "does not depend on the order of the elements" do
+ Set[1, 2, 3].should == Set[3, 2, 1]
+ Set[:a, "b", ?c].should == Set[?c, "b", :a]
+ end
+
+ it "does not depend on the order of nested Sets" do
+ Set[Set[1], Set[2], Set[3]].should == Set[Set[3], Set[2], Set[1]]
+
+ set1 = Set[Set["a", "b"], Set["c", "d"], Set["e", "f"]]
+ set2 = Set[Set["c", "d"], Set["a", "b"], Set["e", "f"]]
+ set1.should == set2
+ end
+
+ ruby_version_is ""..."4.0" do
+ context "when comparing to a Set-like object" do
+ it "returns true when a Set and a Set-like object contain the same elements" do
+ Set[1, 2, 3].should == SetSpecs::SetLike.new([1, 2, 3])
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/set/exclusion_spec.rb b/spec/ruby/core/set/exclusion_spec.rb
new file mode 100644
index 0000000000..52ee34fe78
--- /dev/null
+++ b/spec/ruby/core/set/exclusion_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Set#^" do
+ before :each do
+ @set = Set[1, 2, 3, 4]
+ end
+
+ it "returns a new Set containing elements that are not in both self and the passed Enumerable" do
+ (@set ^ Set[3, 4, 5]).should == Set[1, 2, 5]
+ (@set ^ [3, 4, 5]).should == Set[1, 2, 5]
+ end
+
+ it "raises an ArgumentError when passed a non-Enumerable" do
+ -> { @set ^ 3 }.should.raise(ArgumentError)
+ -> { @set ^ Object.new }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/filter_spec.rb b/spec/ruby/core/set/filter_spec.rb
new file mode 100644
index 0000000000..779254ad68
--- /dev/null
+++ b/spec/ruby/core/set/filter_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/select'
+
+describe "Set#filter!" do
+ it_behaves_like :set_select_bang, :filter!
+end
diff --git a/spec/ruby/core/set/fixtures/set_like.rb b/spec/ruby/core/set/fixtures/set_like.rb
new file mode 100644
index 0000000000..86dec2ed52
--- /dev/null
+++ b/spec/ruby/core/set/fixtures/set_like.rb
@@ -0,0 +1,30 @@
+
+module SetSpecs
+ # This class is used to test the interaction of "Set-like" objects with real Sets
+ #
+ # These "Set-like" objects reply to is_a?(Set) with true and thus real Set objects are able to transparently
+ # interoperate with them in a duck-typing manner.
+ class SetLike
+ include Enumerable
+
+ def is_a?(klass)
+ super || klass == ::Set
+ end
+
+ def initialize(entries)
+ @entries = entries
+ end
+
+ def each(&block)
+ @entries.each(&block)
+ end
+
+ def inspect
+ "#<#{self.class}: {#{map(&:inspect).join(", ")}}>"
+ end
+
+ def size
+ @entries.size
+ end
+ end
+end
diff --git a/spec/ruby/core/set/flatten_merge_spec.rb b/spec/ruby/core/set/flatten_merge_spec.rb
new file mode 100644
index 0000000000..3904d969ae
--- /dev/null
+++ b/spec/ruby/core/set/flatten_merge_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "Set#flatten_merge" do
+ ruby_version_is ""..."4.0" do
+ it "is protected" do
+ Set.protected_instance_methods(false).should.include?(:flatten_merge)
+ end
+
+ it "flattens the passed Set and merges it into self" do
+ set1 = Set[1, 2]
+ set2 = Set[3, 4, Set[5, 6]]
+
+ set1.send(:flatten_merge, set2).should == Set[1, 2, 3, 4, 5, 6]
+ end
+
+ it "raises an ArgumentError when trying to flatten a recursive Set" do
+ set1 = Set[1, 2, 3]
+ set2 = Set[5, 6, 7]
+ set2 << set2
+
+ -> { set1.send(:flatten_merge, set2) }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/set/flatten_spec.rb b/spec/ruby/core/set/flatten_spec.rb
new file mode 100644
index 0000000000..ca6323fac8
--- /dev/null
+++ b/spec/ruby/core/set/flatten_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+set_version = defined?(Set::VERSION) ? Set::VERSION : '1.0.0'
+
+describe "Set#flatten" do
+ it "returns a copy of self with each included Set flattened" do
+ set = Set[1, 2, Set[3, 4, Set[5, 6, Set[7, 8]]], 9, 10]
+ flattened_set = set.flatten
+
+ flattened_set.should_not.equal?(set)
+ flattened_set.should == Set[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ end
+
+ it "raises an ArgumentError when self is recursive" do
+ (set = Set[]) << set
+ -> { set.flatten }.should.raise(ArgumentError)
+ end
+
+ ruby_version_is ""..."4.0" do
+ context "when Set contains a Set-like object" do
+ it "returns a copy of self with each included Set-like object flattened" do
+ Set[SetSpecs::SetLike.new([1])].flatten.should == Set[1]
+ end
+ end
+ end
+end
+
+describe "Set#flatten!" do
+ it "flattens self" do
+ set = Set[1, 2, Set[3, 4, Set[5, 6, Set[7, 8]]], 9, 10]
+ set.flatten!
+ set.should == Set[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ end
+
+ it "returns self when self was modified" do
+ set = Set[1, 2, Set[3, 4]]
+ set.flatten!.should.equal?(set)
+ end
+
+ it "returns nil when self was not modified" do
+ set = Set[1, 2, 3, 4]
+ set.flatten!.should == nil
+ end
+
+ it "raises an ArgumentError when self is recursive" do
+ (set = Set[]) << set
+ -> { set.flatten! }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/hash_spec.rb b/spec/ruby/core/set/hash_spec.rb
new file mode 100644
index 0000000000..63a0aa66a5
--- /dev/null
+++ b/spec/ruby/core/set/hash_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Set#hash" do
+ it "is static" do
+ Set[].hash.should == Set[].hash
+ Set[1, 2, 3].hash.should == Set[1, 2, 3].hash
+ Set[:a, "b", ?c].hash.should == Set[?c, "b", :a].hash
+
+ Set[].hash.should_not == Set[1, 2, 3].hash
+ Set[1, 2, 3].hash.should_not == Set[:a, "b", ?c].hash
+ end
+
+ ruby_version_is ""..."4.0" do
+ # see https://github.com/jruby/jruby/issues/8393
+ it "is equal to nil.hash for an uninitialized Set" do
+ Set.allocate.hash.should == nil.hash
+ end
+ end
+end
diff --git a/spec/ruby/core/set/include_spec.rb b/spec/ruby/core/set/include_spec.rb
new file mode 100644
index 0000000000..dd33bbc3bd
--- /dev/null
+++ b/spec/ruby/core/set/include_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "Set#include?" do
+ it_behaves_like :set_include, :include?
+end
diff --git a/spec/ruby/core/set/initialize_clone_spec.rb b/spec/ruby/core/set/initialize_clone_spec.rb
new file mode 100644
index 0000000000..13abb7ee4e
--- /dev/null
+++ b/spec/ruby/core/set/initialize_clone_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+
+describe "Set#initialize_clone" do
+ # See https://bugs.ruby-lang.org/issues/14266
+ it "does not freeze the new Set when called from clone(freeze: false)" do
+ set1 = Set[1, 2]
+ set1.freeze
+ set2 = set1.clone(freeze: false)
+ set1.frozen?.should == true
+ set2.frozen?.should == false
+ set2.add 3
+ set1.should == Set[1, 2]
+ set2.should == Set[1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/set/initialize_spec.rb b/spec/ruby/core/set/initialize_spec.rb
new file mode 100644
index 0000000000..45538b38fb
--- /dev/null
+++ b/spec/ruby/core/set/initialize_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+
+describe "Set#initialize" do
+ it "is private" do
+ Set.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ it "adds all elements of the passed Enumerable to self" do
+ s = Set.new([1, 2, 3])
+ s.size.should.eql?(3)
+ s.should.include?(1)
+ s.should.include?(2)
+ s.should.include?(3)
+ end
+
+ it "uses #each_entry on the provided Enumerable" do
+ enumerable = MockObject.new('mock-enumerable')
+ enumerable.should_receive(:each_entry).and_yield(1).and_yield(2).and_yield(3)
+ s = Set.new(enumerable)
+ s.size.should.eql?(3)
+ s.should.include?(1)
+ s.should.include?(2)
+ s.should.include?(3)
+ end
+
+ it "uses #each on the provided Enumerable if it does not respond to #each_entry" do
+ enumerable = MockObject.new('mock-enumerable')
+ enumerable.should_receive(:each).and_yield(1).and_yield(2).and_yield(3)
+ s = Set.new(enumerable)
+ s.size.should.eql?(3)
+ s.should.include?(1)
+ s.should.include?(2)
+ s.should.include?(3)
+ end
+
+ it "raises if the provided Enumerable does not respond to #each_entry or #each" do
+ enumerable = MockObject.new('mock-enumerable')
+ -> { Set.new(enumerable) }.should.raise(ArgumentError, "value must be enumerable")
+ end
+
+ it "should initialize with empty array and set" do
+ s = Set.new([])
+ s.size.should.eql?(0)
+
+ s = Set.new({})
+ s.size.should.eql?(0)
+ end
+
+ it "preprocesses all elements by a passed block before adding to self" do
+ s = Set.new([1, 2, 3]) { |x| x * x }
+ s.size.should.eql?(3)
+ s.should.include?(1)
+ s.should.include?(4)
+ s.should.include?(9)
+ end
+
+ it "should initialize with empty array and block" do
+ s = Set.new([]) { |x| x * x }
+ s.size.should.eql?(0)
+ end
+
+ it "should initialize with empty set and block" do
+ s = Set.new(Set.new) { |x| x * x }
+ s.size.should.eql?(0)
+ end
+
+ it "should initialize with set" do
+ o = Set.new([1, 2])
+ s = Set.new(o)
+ s.size.should.eql?(2)
+ s.should.include?(1)
+ s.should.include?(2)
+ end
+
+ it "should initialize with set and block" do
+ o = Set.new([1, 2])
+ s = Set.new(o) { |e| e + 2 }
+ s.size.should.eql?(2)
+ s.should.include?(3)
+ s.should.include?(4)
+ end
+
+ it "should initialize with just block" do
+ s = Set.new { |x| x * x }
+ s.size.should.eql?(0)
+ s.should.eql?(Set.new)
+ end
+end
diff --git a/spec/ruby/core/set/inspect_spec.rb b/spec/ruby/core/set/inspect_spec.rb
new file mode 100644
index 0000000000..0dcce83eb6
--- /dev/null
+++ b/spec/ruby/core/set/inspect_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/inspect'
+
+describe "Set#inspect" do
+ it_behaves_like :set_inspect, :inspect
+end
diff --git a/spec/ruby/core/set/intersect_spec.rb b/spec/ruby/core/set/intersect_spec.rb
new file mode 100644
index 0000000000..d04a1af441
--- /dev/null
+++ b/spec/ruby/core/set/intersect_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+
+describe "Set#intersect?" do
+ it "returns true when two Sets have at least one element in common" do
+ Set[1, 2].intersect?(Set[2, 3]).should == true
+ end
+
+ it "returns false when two Sets have no element in common" do
+ Set[1, 2].intersect?(Set[3, 4]).should == false
+ end
+
+ context "when comparing to a Set-like object" do
+ it "returns true when a Set has at least one element in common with a Set-like object" do
+ Set[1, 2].intersect?(SetSpecs::SetLike.new([2, 3])).should == true
+ end
+
+ it "returns false when a Set has no element in common with a Set-like object" do
+ Set[1, 2].intersect?(SetSpecs::SetLike.new([3, 4])).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/set/intersection_spec.rb b/spec/ruby/core/set/intersection_spec.rb
new file mode 100644
index 0000000000..136b886775
--- /dev/null
+++ b/spec/ruby/core/set/intersection_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/intersection'
+
+describe "Set#intersection" do
+ it_behaves_like :set_intersection, :intersection
+end
+
+describe "Set#&" do
+ it_behaves_like :set_intersection, :&
+end
diff --git a/spec/ruby/core/set/join_spec.rb b/spec/ruby/core/set/join_spec.rb
new file mode 100644
index 0000000000..1c1e8a8af8
--- /dev/null
+++ b/spec/ruby/core/set/join_spec.rb
@@ -0,0 +1,30 @@
+require_relative '../../spec_helper'
+
+describe "Set#join" do
+ it "returns an empty string if the Set is empty" do
+ Set[].join.should == ''
+ end
+
+ it "returns a new string formed by joining elements after conversion" do
+ set = Set[:a, :b, :c]
+ set.join.should == "abc"
+ end
+
+ it "does not separate elements when the passed separator is nil" do
+ set = Set[:a, :b, :c]
+ set.join(nil).should == "abc"
+ end
+
+ it "returns a string formed by concatenating each element separated by the separator" do
+ set = Set[:a, :b, :c]
+ set.join(' | ').should == "a | b | c"
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "calls #to_a to convert the Set in to an Array" do
+ set = Set[:a, :b, :c]
+ set.should_receive(:to_a).and_return([:a, :b, :c])
+ set.join.should == "abc"
+ end
+ end
+end
diff --git a/spec/ruby/core/set/keep_if_spec.rb b/spec/ruby/core/set/keep_if_spec.rb
new file mode 100644
index 0000000000..7ca5d0cd43
--- /dev/null
+++ b/spec/ruby/core/set/keep_if_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+
+describe "Set#keep_if" do
+ before :each do
+ @set = Set["one", "two", "three"]
+ end
+
+ it "yields every element of self" do
+ ret = []
+ @set.keep_if { |x| ret << x }
+ ret.sort.should == ["one", "two", "three"].sort
+ end
+
+ it "keeps every element from self for which the passed block returns true" do
+ @set.keep_if { |x| x.size != 3 }
+ @set.size.should.eql?(1)
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+
+ it "returns self" do
+ @set.keep_if {}.should.equal?(@set)
+ end
+
+ it "returns an Enumerator when passed no block" do
+ enum = @set.keep_if
+ enum.should.instance_of?(Enumerator)
+
+ enum.each { |x| x.size != 3 }
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+end
diff --git a/spec/ruby/core/set/length_spec.rb b/spec/ruby/core/set/length_spec.rb
new file mode 100644
index 0000000000..6bb697b4ca
--- /dev/null
+++ b/spec/ruby/core/set/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "Set#length" do
+ it_behaves_like :set_length, :length
+end
diff --git a/spec/ruby/core/set/map_spec.rb b/spec/ruby/core/set/map_spec.rb
new file mode 100644
index 0000000000..996191b0a8
--- /dev/null
+++ b/spec/ruby/core/set/map_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/collect'
+
+describe "Set#map!" do
+ it_behaves_like :set_collect_bang, :map!
+end
diff --git a/spec/ruby/core/set/member_spec.rb b/spec/ruby/core/set/member_spec.rb
new file mode 100644
index 0000000000..5c82e8f826
--- /dev/null
+++ b/spec/ruby/core/set/member_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/include'
+
+describe "Set#member?" do
+ it_behaves_like :set_include, :member?
+end
diff --git a/spec/ruby/core/set/merge_spec.rb b/spec/ruby/core/set/merge_spec.rb
new file mode 100644
index 0000000000..a2c1a7e706
--- /dev/null
+++ b/spec/ruby/core/set/merge_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Set#merge" do
+ it "adds the elements of the passed Enumerable to self" do
+ Set[:a, :b].merge(Set[:b, :c, :d]).should == Set[:a, :b, :c, :d]
+ Set[1, 2].merge([3, 4]).should == Set[1, 2, 3, 4]
+ end
+
+ it "returns self" do
+ set = Set[1, 2]
+ set.merge([3, 4]).should.equal?(set)
+ end
+
+ it "raises an ArgumentError when passed a non-Enumerable" do
+ -> { Set[1, 2].merge(1) }.should.raise(ArgumentError)
+ -> { Set[1, 2].merge(Object.new) }.should.raise(ArgumentError)
+ end
+
+ it "raises RuntimeError when called during iteration" do
+ set = Set[:a, :b]
+ set.each do |_m|
+ -> { set.merge([1, 2]) }.should.raise(RuntimeError, /iteration/)
+ end
+ end
+
+ it "accepts multiple arguments" do
+ Set[:a, :b].merge(Set[:b, :c], [:d]).should == Set[:a, :b, :c, :d]
+ end
+end
diff --git a/spec/ruby/core/set/minus_spec.rb b/spec/ruby/core/set/minus_spec.rb
new file mode 100644
index 0000000000..72f98f985e
--- /dev/null
+++ b/spec/ruby/core/set/minus_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/difference'
+
+describe "Set#-" do
+ it_behaves_like :set_difference, :-
+end
diff --git a/spec/ruby/core/set/plus_spec.rb b/spec/ruby/core/set/plus_spec.rb
new file mode 100644
index 0000000000..7e44ff0b7e
--- /dev/null
+++ b/spec/ruby/core/set/plus_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/union'
+
+describe "Set#+" do
+ it_behaves_like :set_union, :+
+end
diff --git a/spec/ruby/core/set/pretty_print_cycle_spec.rb b/spec/ruby/core/set/pretty_print_cycle_spec.rb
new file mode 100644
index 0000000000..7e6017c112
--- /dev/null
+++ b/spec/ruby/core/set/pretty_print_cycle_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "Set#pretty_print_cycle" do
+ it "passes the 'pretty print' representation of a self-referencing Set to the pretty print writer" do
+ pp = mock("PrettyPrint")
+ ruby_version_is(""..."4.0") do
+ pp.should_receive(:text).with("#<Set: {...}>")
+ end
+ ruby_version_is("4.0") do
+ pp.should_receive(:text).with("Set[...]")
+ end
+ Set[1, 2, 3].pretty_print_cycle(pp)
+ end
+end
diff --git a/spec/ruby/core/set/proper_subset_spec.rb b/spec/ruby/core/set/proper_subset_spec.rb
new file mode 100644
index 0000000000..3fd27da131
--- /dev/null
+++ b/spec/ruby/core/set/proper_subset_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+set_version = defined?(Set::VERSION) ? Set::VERSION : '1.0.0'
+
+describe "Set#proper_subset?" do
+ before :each do
+ @set = Set[1, 2, 3, 4]
+ end
+
+ it "returns true if passed a Set that self is a proper subset of" do
+ Set[].proper_subset?(@set).should == true
+ Set[].proper_subset?(Set[1, 2, 3]).should == true
+ Set[].proper_subset?(Set["a", :b, ?c]).should == true
+
+ Set[1, 2, 3].proper_subset?(@set).should == true
+ Set[1, 3].proper_subset?(@set).should == true
+ Set[1, 2].proper_subset?(@set).should == true
+ Set[1].proper_subset?(@set).should == true
+
+ Set[5].proper_subset?(@set).should == false
+ Set[1, 5].proper_subset?(@set).should == false
+ Set[nil].proper_subset?(@set).should == false
+ Set["test"].proper_subset?(@set).should == false
+
+ @set.proper_subset?(@set).should == false
+ Set[].proper_subset?(Set[]).should == false
+ end
+
+ it "raises an ArgumentError when passed a non-Set" do
+ -> { Set[].proper_subset?([]) }.should.raise(ArgumentError)
+ -> { Set[].proper_subset?(1) }.should.raise(ArgumentError)
+ -> { Set[].proper_subset?("test") }.should.raise(ArgumentError)
+ -> { Set[].proper_subset?(Object.new) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/proper_superset_spec.rb b/spec/ruby/core/set/proper_superset_spec.rb
new file mode 100644
index 0000000000..e95c67ef0e
--- /dev/null
+++ b/spec/ruby/core/set/proper_superset_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+
+describe "Set#proper_superset?" do
+ before :each do
+ @set = Set[1, 2, 3, 4]
+ end
+
+ it "returns true if passed a Set that self is a proper superset of" do
+ @set.proper_superset?(Set[]).should == true
+ Set[1, 2, 3].proper_superset?(Set[]).should == true
+ Set["a", :b, ?c].proper_superset?(Set[]).should == true
+
+ @set.proper_superset?(Set[1, 2, 3]).should == true
+ @set.proper_superset?(Set[1, 3]).should == true
+ @set.proper_superset?(Set[1, 2]).should == true
+ @set.proper_superset?(Set[1]).should == true
+
+ @set.proper_superset?(Set[5]).should == false
+ @set.proper_superset?(Set[1, 5]).should == false
+ @set.proper_superset?(Set[nil]).should == false
+ @set.proper_superset?(Set["test"]).should == false
+
+ @set.proper_superset?(@set).should == false
+ Set[].proper_superset?(Set[]).should == false
+ end
+
+ it "raises an ArgumentError when passed a non-Set" do
+ -> { Set[].proper_superset?([]) }.should.raise(ArgumentError)
+ -> { Set[].proper_superset?(1) }.should.raise(ArgumentError)
+ -> { Set[].proper_superset?("test") }.should.raise(ArgumentError)
+ -> { Set[].proper_superset?(Object.new) }.should.raise(ArgumentError)
+ end
+
+ ruby_version_is ""..."4.0" do
+ context "when comparing to a Set-like object" do
+ it "returns true if passed a Set-like object that self is a proper superset of" do
+ Set[1, 2, 3, 4].proper_superset?(SetSpecs::SetLike.new([1, 2, 3])).should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/set/reject_spec.rb b/spec/ruby/core/set/reject_spec.rb
new file mode 100644
index 0000000000..b00a36812b
--- /dev/null
+++ b/spec/ruby/core/set/reject_spec.rb
@@ -0,0 +1,41 @@
+require_relative '../../spec_helper'
+
+describe "Set#reject!" do
+ before :each do
+ @set = Set["one", "two", "three"]
+ end
+
+ it "yields every element of self" do
+ ret = []
+ @set.reject! { |x| ret << x }
+ ret.sort.should == ["one", "two", "three"].sort
+ end
+
+ it "deletes every element from self for which the passed block returns true" do
+ @set.reject! { |x| x.size == 3 }
+ @set.size.should.eql?(1)
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+
+ it "returns self when self was modified" do
+ @set.reject! { |x| true }.should.equal?(@set)
+ end
+
+ it "returns nil when self was not modified" do
+ @set.reject! { |x| false }.should == nil
+ end
+
+ it "returns an Enumerator when passed no block" do
+ enum = @set.reject!
+ enum.should.instance_of?(Enumerator)
+
+ enum.each { |x| x.size == 3 }
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+end
diff --git a/spec/ruby/core/set/replace_spec.rb b/spec/ruby/core/set/replace_spec.rb
new file mode 100644
index 0000000000..2a51a024dc
--- /dev/null
+++ b/spec/ruby/core/set/replace_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "Set#replace" do
+ before :each do
+ @set = Set[:a, :b, :c]
+ end
+
+ it "replaces the contents with other and returns self" do
+ @set.replace(Set[1, 2, 3]).should == @set
+ @set.should == Set[1, 2, 3]
+ end
+
+ it "raises RuntimeError when called during iteration" do
+ set = Set[:a, :b, :c, :d, :e, :f]
+ set.each do |_m|
+ -> { set.replace(Set[1, 2, 3]) }.should.raise(RuntimeError, /iteration/)
+ end
+ set.should == Set[:a, :b, :c, :d, :e, :f]
+ end
+
+ it "accepts any enumerable as other" do
+ @set.replace([1, 2, 3]).should == Set[1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/set/select_spec.rb b/spec/ruby/core/set/select_spec.rb
new file mode 100644
index 0000000000..b458ffacaa
--- /dev/null
+++ b/spec/ruby/core/set/select_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/select'
+
+describe "Set#select!" do
+ it_behaves_like :set_select_bang, :select!
+end
diff --git a/spec/ruby/core/set/set_spec.rb b/spec/ruby/core/set/set_spec.rb
new file mode 100644
index 0000000000..fd1d2072e3
--- /dev/null
+++ b/spec/ruby/core/set/set_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+
+describe 'Set' do
+ it 'is available without explicit requiring' do
+ output = ruby_exe(<<~RUBY, options: '--disable-gems', args: '2>&1')
+ puts Set.new([1, 2, 3]).to_a.inspect
+ RUBY
+ output.chomp.should == "[1, 2, 3]"
+ end
+end
diff --git a/spec/ruby/core/set/shared/add.rb b/spec/ruby/core/set/shared/add.rb
new file mode 100644
index 0000000000..8d6d83434f
--- /dev/null
+++ b/spec/ruby/core/set/shared/add.rb
@@ -0,0 +1,14 @@
+describe :set_add, shared: true do
+ before :each do
+ @set = Set.new
+ end
+
+ it "adds the passed Object to self" do
+ @set.send(@method, "dog")
+ @set.should.include?("dog")
+ end
+
+ it "returns self" do
+ @set.send(@method, "dog").should.equal?(@set)
+ end
+end
diff --git a/spec/ruby/core/set/shared/collect.rb b/spec/ruby/core/set/shared/collect.rb
new file mode 100644
index 0000000000..ad5c5afa59
--- /dev/null
+++ b/spec/ruby/core/set/shared/collect.rb
@@ -0,0 +1,20 @@
+describe :set_collect_bang, shared: true do
+ before :each do
+ @set = Set[1, 2, 3, 4, 5]
+ end
+
+ it "yields each Object in self" do
+ res = []
+ @set.send(@method) { |x| res << x }
+ res.sort.should == [1, 2, 3, 4, 5].sort
+ end
+
+ it "returns self" do
+ @set.send(@method) { |x| x }.should.equal?(@set)
+ end
+
+ it "replaces self with the return values of the block" do
+ @set.send(@method) { |x| x * 2 }
+ @set.should == Set[2, 4, 6, 8, 10]
+ end
+end
diff --git a/spec/ruby/core/set/shared/difference.rb b/spec/ruby/core/set/shared/difference.rb
new file mode 100644
index 0000000000..8a17056a82
--- /dev/null
+++ b/spec/ruby/core/set/shared/difference.rb
@@ -0,0 +1,15 @@
+describe :set_difference, shared: true do
+ before :each do
+ @set = Set[:a, :b, :c]
+ end
+
+ it "returns a new Set containing self's elements excluding the elements in the passed Enumerable" do
+ @set.send(@method, Set[:a, :b]).should == Set[:c]
+ @set.send(@method, [:b, :c]).should == Set[:a]
+ end
+
+ it "raises an ArgumentError when passed a non-Enumerable" do
+ -> { @set.send(@method, 1) }.should.raise(ArgumentError)
+ -> { @set.send(@method, Object.new) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/shared/include.rb b/spec/ruby/core/set/shared/include.rb
new file mode 100644
index 0000000000..82755ccf59
--- /dev/null
+++ b/spec/ruby/core/set/shared/include.rb
@@ -0,0 +1,29 @@
+describe :set_include, shared: true do
+ it "returns true when self contains the passed Object" do
+ set = Set[:a, :b, :c]
+ set.send(@method, :a).should == true
+ set.send(@method, :e).should == false
+ end
+
+ describe "member equality" do
+ it "is checked using both #hash and #eql?" do
+ obj = Object.new
+ obj_another = Object.new
+
+ def obj.hash; 42 end
+ def obj_another.hash; 42 end
+ def obj_another.eql?(o) hash == o.hash end
+
+ set = Set["a", "b", "c", obj]
+ set.send(@method, obj_another).should == true
+ end
+
+ it "is not checked using #==" do
+ obj = Object.new
+ set = Set["a", "b", "c"]
+
+ obj.should_not_receive(:==)
+ set.send(@method, obj)
+ end
+ end
+end
diff --git a/spec/ruby/core/set/shared/inspect.rb b/spec/ruby/core/set/shared/inspect.rb
new file mode 100644
index 0000000000..31bd8accfd
--- /dev/null
+++ b/spec/ruby/core/set/shared/inspect.rb
@@ -0,0 +1,45 @@
+describe :set_inspect, shared: true do
+ it "returns a String representation of self" do
+ Set[].send(@method).should.is_a?(String)
+ Set[nil, false, true].send(@method).should.is_a?(String)
+ Set[1, 2, 3].send(@method).should.is_a?(String)
+ Set["1", "2", "3"].send(@method).should.is_a?(String)
+ Set[:a, "b", Set[?c]].send(@method).should.is_a?(String)
+ end
+
+ ruby_version_is "4.0" do
+ it "does include the elements of the set" do
+ Set["1"].send(@method).should == 'Set["1"]'
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "does include the elements of the set" do
+ Set["1"].send(@method).should == '#<Set: {"1"}>'
+ end
+ end
+
+ it "puts spaces between the elements" do
+ Set["1", "2"].send(@method).should.include?('", "')
+ end
+
+ ruby_version_is "4.0" do
+ it "correctly handles cyclic-references" do
+ set1 = Set[]
+ set2 = Set[set1]
+ set1 << set2
+ set1.send(@method).should.is_a?(String)
+ set1.send(@method).should.include?("Set[...]")
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "correctly handles cyclic-references" do
+ set1 = Set[]
+ set2 = Set[set1]
+ set1 << set2
+ set1.send(@method).should.is_a?(String)
+ set1.send(@method).should.include?("#<Set: {...}>")
+ end
+ end
+end
diff --git a/spec/ruby/core/set/shared/intersection.rb b/spec/ruby/core/set/shared/intersection.rb
new file mode 100644
index 0000000000..978a4924ef
--- /dev/null
+++ b/spec/ruby/core/set/shared/intersection.rb
@@ -0,0 +1,15 @@
+describe :set_intersection, shared: true do
+ before :each do
+ @set = Set[:a, :b, :c]
+ end
+
+ it "returns a new Set containing only elements shared by self and the passed Enumerable" do
+ @set.send(@method, Set[:b, :c, :d, :e]).should == Set[:b, :c]
+ @set.send(@method, [:b, :c, :d]).should == Set[:b, :c]
+ end
+
+ it "raises an ArgumentError when passed a non-Enumerable" do
+ -> { @set.send(@method, 1) }.should.raise(ArgumentError)
+ -> { @set.send(@method, Object.new) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/shared/length.rb b/spec/ruby/core/set/shared/length.rb
new file mode 100644
index 0000000000..a8fcee9f39
--- /dev/null
+++ b/spec/ruby/core/set/shared/length.rb
@@ -0,0 +1,6 @@
+describe :set_length, shared: true do
+ it "returns the number of elements in the set" do
+ set = Set[:a, :b, :c]
+ set.send(@method).should == 3
+ end
+end
diff --git a/spec/ruby/core/set/shared/select.rb b/spec/ruby/core/set/shared/select.rb
new file mode 100644
index 0000000000..0d4a53fffd
--- /dev/null
+++ b/spec/ruby/core/set/shared/select.rb
@@ -0,0 +1,41 @@
+require_relative '../../../spec_helper'
+
+describe :set_select_bang, shared: true do
+ before :each do
+ @set = Set["one", "two", "three"]
+ end
+
+ it "yields every element of self" do
+ ret = []
+ @set.send(@method) { |x| ret << x }
+ ret.sort.should == ["one", "two", "three"].sort
+ end
+
+ it "keeps every element from self for which the passed block returns true" do
+ @set.send(@method) { |x| x.size != 3 }
+ @set.size.should.eql?(1)
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+
+ it "returns self when self was modified" do
+ @set.send(@method) { false }.should.equal?(@set)
+ end
+
+ it "returns nil when self was not modified" do
+ @set.send(@method) { true }.should == nil
+ end
+
+ it "returns an Enumerator when passed no block" do
+ enum = @set.send(@method)
+ enum.should.instance_of?(Enumerator)
+
+ enum.each { |x| x.size != 3 }
+
+ @set.should_not.include?("one")
+ @set.should_not.include?("two")
+ @set.should.include?("three")
+ end
+end
diff --git a/spec/ruby/core/set/shared/union.rb b/spec/ruby/core/set/shared/union.rb
new file mode 100644
index 0000000000..dddf1716e5
--- /dev/null
+++ b/spec/ruby/core/set/shared/union.rb
@@ -0,0 +1,15 @@
+describe :set_union, shared: true do
+ before :each do
+ @set = Set[:a, :b, :c]
+ end
+
+ it "returns a new Set containing all elements of self and the passed Enumerable" do
+ @set.send(@method, Set[:b, :d, :e]).should == Set[:a, :b, :c, :d, :e]
+ @set.send(@method, [:b, :e]).should == Set[:a, :b, :c, :e]
+ end
+
+ it "raises an ArgumentError when passed a non-Enumerable" do
+ -> { @set.send(@method, 1) }.should.raise(ArgumentError)
+ -> { @set.send(@method, Object.new) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/size_spec.rb b/spec/ruby/core/set/size_spec.rb
new file mode 100644
index 0000000000..4ae22c5f0a
--- /dev/null
+++ b/spec/ruby/core/set/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "Set#size" do
+ it_behaves_like :set_length, :size
+end
diff --git a/spec/ruby/core/set/sortedset/sortedset_spec.rb b/spec/ruby/core/set/sortedset/sortedset_spec.rb
new file mode 100644
index 0000000000..c8f65f0851
--- /dev/null
+++ b/spec/ruby/core/set/sortedset/sortedset_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../../spec_helper'
+
+describe "SortedSet" do
+ ruby_version_is ""..."4.0" do
+ it "raises error including message that it has been extracted from the set stdlib" do
+ -> {
+ SortedSet
+ }.should.raise(RuntimeError) { |e|
+ e.message.should.include?("The `SortedSet` class has been extracted from the `set` library")
+ }
+ end
+ end
+end
diff --git a/spec/ruby/core/set/subset_spec.rb b/spec/ruby/core/set/subset_spec.rb
new file mode 100644
index 0000000000..81869d4993
--- /dev/null
+++ b/spec/ruby/core/set/subset_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+set_version = defined?(Set::VERSION) ? Set::VERSION : '1.0.0'
+
+describe "Set#subset?" do
+ before :each do
+ @set = Set[1, 2, 3, 4]
+ end
+
+ it "returns true if passed a Set that is equal to self or self is a subset of" do
+ @set.subset?(@set).should == true
+ Set[].subset?(Set[]).should == true
+
+ Set[].subset?(@set).should == true
+ Set[].subset?(Set[1, 2, 3]).should == true
+ Set[].subset?(Set["a", :b, ?c]).should == true
+
+ Set[1, 2, 3].subset?(@set).should == true
+ Set[1, 3].subset?(@set).should == true
+ Set[1, 2].subset?(@set).should == true
+ Set[1].subset?(@set).should == true
+
+ Set[5].subset?(@set).should == false
+ Set[1, 5].subset?(@set).should == false
+ Set[nil].subset?(@set).should == false
+ Set["test"].subset?(@set).should == false
+ end
+
+ it "raises an ArgumentError when passed a non-Set" do
+ -> { Set[].subset?([]) }.should.raise(ArgumentError)
+ -> { Set[].subset?(1) }.should.raise(ArgumentError)
+ -> { Set[].subset?("test") }.should.raise(ArgumentError)
+ -> { Set[].subset?(Object.new) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/set/subtract_spec.rb b/spec/ruby/core/set/subtract_spec.rb
new file mode 100644
index 0000000000..ae4bc73d41
--- /dev/null
+++ b/spec/ruby/core/set/subtract_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Set#subtract" do
+ before :each do
+ @set = Set[:a, :b, :c]
+ end
+
+ it "deletes any elements contained in other and returns self" do
+ @set.subtract(Set[:b, :c]).should == @set
+ @set.should == Set[:a]
+ end
+
+ it "accepts any enumerable as other" do
+ @set.subtract([:c]).should == Set[:a, :b]
+ end
+end
diff --git a/spec/ruby/core/set/superset_spec.rb b/spec/ruby/core/set/superset_spec.rb
new file mode 100644
index 0000000000..7e7db2b179
--- /dev/null
+++ b/spec/ruby/core/set/superset_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/set_like'
+
+describe "Set#superset?" do
+ before :each do
+ @set = Set[1, 2, 3, 4]
+ end
+
+ it "returns true if passed a Set that equals self or self is a proper superset of" do
+ @set.superset?(@set).should == true
+ Set[].superset?(Set[]).should == true
+
+ @set.superset?(Set[]).should == true
+ Set[1, 2, 3].superset?(Set[]).should == true
+ Set["a", :b, ?c].superset?(Set[]).should == true
+
+ @set.superset?(Set[1, 2, 3]).should == true
+ @set.superset?(Set[1, 3]).should == true
+ @set.superset?(Set[1, 2]).should == true
+ @set.superset?(Set[1]).should == true
+
+ @set.superset?(Set[5]).should == false
+ @set.superset?(Set[1, 5]).should == false
+ @set.superset?(Set[nil]).should == false
+ @set.superset?(Set["test"]).should == false
+ end
+
+ it "raises an ArgumentError when passed a non-Set" do
+ -> { Set[].superset?([]) }.should.raise(ArgumentError)
+ -> { Set[].superset?(1) }.should.raise(ArgumentError)
+ -> { Set[].superset?("test") }.should.raise(ArgumentError)
+ -> { Set[].superset?(Object.new) }.should.raise(ArgumentError)
+ end
+
+ ruby_version_is ""..."4.0" do
+ context "when comparing to a Set-like object" do
+ it "returns true if passed a Set-like object that self is a superset of" do
+ Set[1, 2, 3, 4].superset?(SetSpecs::SetLike.new([1, 2, 3])).should == true
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/set/to_a_spec.rb b/spec/ruby/core/set/to_a_spec.rb
new file mode 100644
index 0000000000..1e9800167a
--- /dev/null
+++ b/spec/ruby/core/set/to_a_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Set#to_a" do
+ it "returns an array containing elements of self" do
+ Set[1, 2, 3].to_a.sort.should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/set/to_s_spec.rb b/spec/ruby/core/set/to_s_spec.rb
new file mode 100644
index 0000000000..55b8bfd9b2
--- /dev/null
+++ b/spec/ruby/core/set/to_s_spec.rb
@@ -0,0 +1,11 @@
+require_relative "../../spec_helper"
+require_relative 'shared/inspect'
+
+describe "Set#to_s" do
+ it_behaves_like :set_inspect, :to_s
+
+ it "is an alias of inspect" do
+ set = Set.new
+ set.method(:to_s).should == set.method(:inspect)
+ end
+end
diff --git a/spec/ruby/core/set/union_spec.rb b/spec/ruby/core/set/union_spec.rb
new file mode 100644
index 0000000000..3e77022d4b
--- /dev/null
+++ b/spec/ruby/core/set/union_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/union'
+
+describe "Set#union" do
+ it_behaves_like :set_union, :union
+end
+
+describe "Set#|" do
+ it_behaves_like :set_union, :|
+end
diff --git a/spec/ruby/core/signal/fixtures/trap_all.rb b/spec/ruby/core/signal/fixtures/trap_all.rb
new file mode 100644
index 0000000000..afa00b498d
--- /dev/null
+++ b/spec/ruby/core/signal/fixtures/trap_all.rb
@@ -0,0 +1,15 @@
+cannot_be_trapped = %w[KILL STOP] # See man 2 signal
+
+(Signal.list.keys - cannot_be_trapped).each do |signal|
+ begin
+ Signal.trap(signal, -> {})
+ rescue ArgumentError => e
+ unless /can't trap reserved signal|Signal already used by VM or OS/ =~ e.message
+ raise e
+ end
+ else
+ Signal.trap(signal, "DEFAULT")
+ end
+end
+
+puts "OK"
diff --git a/spec/ruby/core/signal/list_spec.rb b/spec/ruby/core/signal/list_spec.rb
new file mode 100644
index 0000000000..56ad6828fe
--- /dev/null
+++ b/spec/ruby/core/signal/list_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+
+describe "Signal.list" do
+ RUBY_SIGNALS = %w{
+ EXIT
+ HUP
+ INT
+ QUIT
+ ILL
+ TRAP
+ IOT
+ ABRT
+ EMT
+ FPE
+ KILL
+ BUS
+ SEGV
+ SYS
+ PIPE
+ ALRM
+ TERM
+ URG
+ STOP
+ TSTP
+ CONT
+ CHLD
+ CLD
+ TTIN
+ TTOU
+ IO
+ XCPU
+ XFSZ
+ VTALRM
+ PROF
+ WINCH
+ USR1
+ USR2
+ LOST
+ MSG
+ PWR
+ POLL
+ DANGER
+ MIGRATE
+ PRE
+ GRANT
+ RETRACT
+ SOUND
+ INFO
+ }
+
+ it "doesn't contain other signals than the known list" do
+ (Signal.list.keys - RUBY_SIGNALS).should == []
+ end
+
+ if Signal.list["CHLD"]
+ it "redefines CLD with CHLD if defined" do
+ Signal.list["CLD"].should == Signal.list["CHLD"]
+ end
+ end
+
+ it "includes the EXIT key with a value of zero" do
+ Signal.list["EXIT"].should == 0
+ end
+
+ it "includes the KILL key with a value of nine" do
+ Signal.list["KILL"].should == 9
+ end
+end
diff --git a/spec/ruby/core/signal/signame_spec.rb b/spec/ruby/core/signal/signame_spec.rb
new file mode 100644
index 0000000000..82f040a6f9
--- /dev/null
+++ b/spec/ruby/core/signal/signame_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../spec_helper'
+
+describe "Signal.signame" do
+ it "takes a signal name with a well known signal number" do
+ Signal.signame(0).should == "EXIT"
+ end
+
+ it "returns nil if the argument is an invalid signal number" do
+ Signal.signame(-1).should == nil
+ end
+
+ it "calls #to_int on an object to convert to an Integer" do
+ obj = mock('signal')
+ obj.should_receive(:to_int).and_return(0)
+ Signal.signame(obj).should == "EXIT"
+ end
+
+ it "raises a TypeError when the passed argument can't be coerced to Integer" do
+ -> { Signal.signame("hello") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the passed argument responds to #to_int but does not return an Integer" do
+ obj = mock('signal')
+ obj.should_receive(:to_int).and_return('not an int')
+ -> { Signal.signame(obj) }.should.raise(TypeError)
+ end
+
+ platform_is_not :windows do
+ it "the original should take precedence over alias when looked up by number" do
+ Signal.signame(Signal.list["ABRT"]).should == "ABRT"
+ Signal.signame(Signal.list["CHLD"]).should == "CHLD"
+ end
+ end
+end
diff --git a/spec/ruby/core/signal/trap_spec.rb b/spec/ruby/core/signal/trap_spec.rb
new file mode 100644
index 0000000000..5d3105fee8
--- /dev/null
+++ b/spec/ruby/core/signal/trap_spec.rb
@@ -0,0 +1,320 @@
+require_relative '../../spec_helper'
+
+describe "Signal.trap" do
+ platform_is_not :windows do
+ before :each do
+ ScratchPad.clear
+ @proc = -> {}
+ @saved_trap = Signal.trap(:HUP, @proc)
+ @hup_number = Signal.list["HUP"]
+ end
+
+ after :each do
+ Signal.trap(:HUP, @saved_trap) if @saved_trap
+ end
+
+ it "returns the previous handler" do
+ Signal.trap(:HUP, @saved_trap).should.equal?(@proc)
+ end
+
+ it "accepts a block" do
+ done = false
+
+ Signal.trap(:HUP) do |signo|
+ signo.should == @hup_number
+ ScratchPad.record :block_trap
+ done = true
+ end
+
+ Process.kill :HUP, Process.pid
+ Thread.pass until done
+
+ ScratchPad.recorded.should == :block_trap
+ end
+
+ it "accepts a proc" do
+ done = false
+
+ handler = -> signo {
+ signo.should == @hup_number
+ ScratchPad.record :proc_trap
+ done = true
+ }
+
+ Signal.trap(:HUP, handler)
+
+ Process.kill :HUP, Process.pid
+ Thread.pass until done
+
+ ScratchPad.recorded.should == :proc_trap
+ end
+
+ it "accepts a method" do
+ done = false
+
+ handler_class = Class.new
+ hup_number = @hup_number
+
+ handler_class.define_method :handler_method do |signo|
+ signo.should == hup_number
+ ScratchPad.record :method_trap
+ done = true
+ end
+
+ handler_method = handler_class.new.method(:handler_method)
+
+ Signal.trap(:HUP, handler_method)
+
+ Process.kill :HUP, Process.pid
+ Thread.pass until done
+
+ ScratchPad.recorded.should == :method_trap
+ end
+
+ it "accepts anything you can call" do
+ done = false
+
+ callable = Object.new
+ hup_number = @hup_number
+
+ callable.singleton_class.define_method :call do |signo|
+ signo.should == hup_number
+ ScratchPad.record :callable_trap
+ done = true
+ end
+
+ Signal.trap(:HUP, callable)
+
+ Process.kill :HUP, Process.pid
+ Thread.pass until done
+
+ ScratchPad.recorded.should == :callable_trap
+ end
+
+ it "raises an exception for a non-callable at the point of use" do
+ not_callable = Object.new
+ Signal.trap(:HUP, not_callable)
+ -> {
+ Process.kill :HUP, Process.pid
+ loop { Thread.pass }
+ }.should.raise(NoMethodError)
+ end
+
+ it "accepts a non-callable that becomes callable when used" do
+ done = false
+
+ late_callable = Object.new
+ hup_number = @hup_number
+
+ Signal.trap(:HUP, late_callable)
+
+ late_callable.singleton_class.define_method :call do |signo|
+ signo.should == hup_number
+ ScratchPad.record :late_callable_trap
+ done = true
+ end
+
+ Process.kill :HUP, Process.pid
+ Thread.pass until done
+
+ ScratchPad.recorded.should == :late_callable_trap
+ end
+
+ it "is possible to create a new Thread when the handler runs" do
+ done = false
+
+ Signal.trap(:HUP) do
+ thr = Thread.new { }
+ thr.join
+ ScratchPad.record(thr.group == Thread.main.group)
+
+ done = true
+ end
+
+ Process.kill :HUP, Process.pid
+ Thread.pass until done
+
+ ScratchPad.recorded.should == true
+ end
+
+ it "registers an handler doing nothing with :IGNORE" do
+ Signal.trap :HUP, :IGNORE
+ Signal.trap(:HUP, @saved_trap).should == "IGNORE"
+ end
+
+ it "can register a new handler after :IGNORE" do
+ Signal.trap :HUP, :IGNORE
+
+ done = false
+ Signal.trap(:HUP) do
+ ScratchPad.record :block_trap
+ done = true
+ end
+
+ Process.kill(:HUP, Process.pid).should == 1
+ Thread.pass until done
+ ScratchPad.recorded.should == :block_trap
+ end
+
+ it "ignores the signal when passed nil" do
+ Signal.trap :HUP, nil
+ Signal.trap(:HUP, @saved_trap).should == nil
+ end
+
+ it "accepts :DEFAULT in place of a proc" do
+ Signal.trap :HUP, :DEFAULT
+ Signal.trap(:HUP, @saved_trap).should == "DEFAULT"
+ end
+
+ it "accepts :SIG_DFL in place of a proc" do
+ Signal.trap :HUP, :SIG_DFL
+ Signal.trap(:HUP, @saved_trap).should == "DEFAULT"
+ end
+
+ it "accepts :SIG_IGN in place of a proc" do
+ Signal.trap :HUP, :SIG_IGN
+ Signal.trap(:HUP, @saved_trap).should == "IGNORE"
+ end
+
+ it "accepts :IGNORE in place of a proc" do
+ Signal.trap :HUP, :IGNORE
+ Signal.trap(:HUP, @saved_trap).should == "IGNORE"
+ end
+
+ it "accepts 'SIG_DFL' in place of a proc" do
+ Signal.trap :HUP, "SIG_DFL"
+ Signal.trap(:HUP, @saved_trap).should == "DEFAULT"
+ end
+
+ it "accepts 'DEFAULT' in place of a proc" do
+ Signal.trap :HUP, "DEFAULT"
+ Signal.trap(:HUP, @saved_trap).should == "DEFAULT"
+ end
+
+ it "accepts 'SIG_IGN' in place of a proc" do
+ Signal.trap :HUP, "SIG_IGN"
+ Signal.trap(:HUP, @saved_trap).should == "IGNORE"
+ end
+
+ it "accepts 'IGNORE' in place of a proc" do
+ Signal.trap :HUP, "IGNORE"
+ Signal.trap(:HUP, @saved_trap).should == "IGNORE"
+ end
+
+ it "accepts long names as Strings" do
+ Signal.trap "SIGHUP", @proc
+ Signal.trap("SIGHUP", @saved_trap).should.equal?(@proc)
+ end
+
+ it "accepts short names as Strings" do
+ Signal.trap "HUP", @proc
+ Signal.trap("HUP", @saved_trap).should.equal?(@proc)
+ end
+
+ it "accepts long names as Symbols" do
+ Signal.trap :SIGHUP, @proc
+ Signal.trap(:SIGHUP, @saved_trap).should.equal?(@proc)
+ end
+
+ it "accepts short names as Symbols" do
+ Signal.trap :HUP, @proc
+ Signal.trap(:HUP, @saved_trap).should.equal?(@proc)
+ end
+
+ it "calls #to_str on an object to convert to a String" do
+ obj = mock("signal")
+ obj.should_receive(:to_str).exactly(2).times.and_return("HUP")
+ Signal.trap obj, @proc
+ Signal.trap(obj, @saved_trap).should.equal?(@proc)
+ end
+
+ it "accepts Integer values" do
+ hup = Signal.list["HUP"]
+ Signal.trap hup, @proc
+ Signal.trap(hup, @saved_trap).should.equal?(@proc)
+ end
+
+ it "does not call #to_int on an object to convert to an Integer" do
+ obj = mock("signal")
+ obj.should_not_receive(:to_int)
+ -> { Signal.trap obj, @proc }.should.raise(ArgumentError, /bad signal type/)
+ end
+
+ it "raises ArgumentError when passed unknown signal" do
+ -> { Signal.trap(300) { } }.should.raise(ArgumentError, "invalid signal number (300)")
+ -> { Signal.trap("USR10") { } }.should.raise(ArgumentError, /\Aunsupported signal [`']SIGUSR10'\z/)
+ -> { Signal.trap("SIGUSR10") { } }.should.raise(ArgumentError, /\Aunsupported signal [`']SIGUSR10'\z/)
+ end
+
+ it "raises ArgumentError when passed signal is not Integer, String or Symbol" do
+ -> { Signal.trap(nil) { } }.should.raise(ArgumentError, "bad signal type NilClass")
+ -> { Signal.trap(100.0) { } }.should.raise(ArgumentError, "bad signal type Float")
+ -> { Signal.trap(Rational(100)) { } }.should.raise(ArgumentError, "bad signal type Rational")
+ end
+
+ # See man 2 signal
+ %w[KILL STOP].each do |signal|
+ it "raises ArgumentError or Errno::EINVAL for SIG#{signal}" do
+ -> {
+ Signal.trap(signal, -> {})
+ }.should.raise(StandardError) { |e|
+ [ArgumentError, Errno::EINVAL].should.include?(e.class)
+ e.message.should =~ /Invalid argument|Signal already used by VM or OS/
+ }
+ end
+ end
+
+ %w[SEGV BUS ILL FPE VTALRM].each do |signal|
+ it "raises ArgumentError for SIG#{signal} which is reserved by Ruby" do
+ -> {
+ Signal.trap(signal, -> {})
+ }.should.raise(ArgumentError, "can't trap reserved signal: SIG#{signal}")
+ end
+ end
+
+ it "allows to register a handler for all known signals, except reserved signals for which it raises ArgumentError" do
+ out = ruby_exe(fixture(__FILE__, "trap_all.rb"), args: "2>&1")
+ out.should == "OK\n"
+ $?.exitstatus.should == 0
+ end
+
+ it "returns 'DEFAULT' for the initial SIGINT handler" do
+ ruby_exe("print Signal.trap(:INT) { abort }").should == 'DEFAULT'
+ end
+
+ it "returns SYSTEM_DEFAULT if passed DEFAULT and no handler was ever set" do
+ Signal.trap("PROF", "DEFAULT").should == "SYSTEM_DEFAULT"
+ end
+
+ it "accepts 'SYSTEM_DEFAULT' and uses the OS handler for SIGPIPE" do
+ code = <<-RUBY
+ p Signal.trap('PIPE', 'SYSTEM_DEFAULT')
+ r, w = IO.pipe
+ r.close
+ loop { w.write("a"*1024) }
+ RUBY
+ out = ruby_exe(code, exit_status: :SIGPIPE)
+ status = $?
+ out.should == "nil\n"
+ status.should.signaled?
+ end
+ end
+
+ describe "the special EXIT signal code" do
+ it "accepts the EXIT code" do
+ code = "Signal.trap(:EXIT, proc { print 1 })"
+ ruby_exe(code).should == "1"
+ end
+
+ it "runs the proc before at_exit handlers" do
+ code = "at_exit {print 1}; Signal.trap(:EXIT, proc {print 2}); at_exit {print 3}"
+ ruby_exe(code).should == "231"
+ end
+
+ it "can unset the handler" do
+ code = "Signal.trap(:EXIT, proc { print 1 }); Signal.trap(:EXIT, 'DEFAULT')"
+ ruby_exe(code).should == ""
+ end
+ end
+
+end
diff --git a/spec/ruby/core/sizedqueue/append_spec.rb b/spec/ruby/core/sizedqueue/append_spec.rb
new file mode 100644
index 0000000000..c52baa3802
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/append_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/enque'
+require_relative '../../shared/sizedqueue/enque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "SizedQueue#<<" do
+ it_behaves_like :queue_enq, :<<, -> { SizedQueue.new(10) }
+end
+
+describe "SizedQueue#<<" do
+ it_behaves_like :sizedqueue_enq, :<<, -> n { SizedQueue.new(n) }
+end
+
+describe "SizedQueue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.send(:<<, 1, timeout: v) }
+end
diff --git a/spec/ruby/core/sizedqueue/clear_spec.rb b/spec/ruby/core/sizedqueue/clear_spec.rb
new file mode 100644
index 0000000000..abae01c6c0
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/clear_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/clear'
+
+describe "SizedQueue#clear" do
+ it_behaves_like :queue_clear, :clear, -> { SizedQueue.new(10) }
+end
diff --git a/spec/ruby/core/sizedqueue/close_spec.rb b/spec/ruby/core/sizedqueue/close_spec.rb
new file mode 100644
index 0000000000..0e0af851cb
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/close_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/close'
+
+describe "SizedQueue#close" do
+ it_behaves_like :queue_close, :close, -> { SizedQueue.new(10) }
+end
diff --git a/spec/ruby/core/sizedqueue/closed_spec.rb b/spec/ruby/core/sizedqueue/closed_spec.rb
new file mode 100644
index 0000000000..4b90da1faa
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/closed_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/closed'
+
+describe "SizedQueue#closed?" do
+ it_behaves_like :queue_closed?, :closed?, -> { SizedQueue.new(10) }
+end
diff --git a/spec/ruby/core/sizedqueue/deq_spec.rb b/spec/ruby/core/sizedqueue/deq_spec.rb
new file mode 100644
index 0000000000..2aeb52f8a6
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/deq_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/deque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "SizedQueue#deq" do
+ it_behaves_like :queue_deq, :deq, -> { SizedQueue.new(10) }
+end
+
+describe "SizedQueue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.deq(timeout: v) }
+end
diff --git a/spec/ruby/core/sizedqueue/empty_spec.rb b/spec/ruby/core/sizedqueue/empty_spec.rb
new file mode 100644
index 0000000000..9b0d4ff013
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/empty_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/empty'
+
+describe "SizedQueue#empty?" do
+ it_behaves_like :queue_empty?, :empty?, -> { SizedQueue.new(10) }
+end
diff --git a/spec/ruby/core/sizedqueue/enq_spec.rb b/spec/ruby/core/sizedqueue/enq_spec.rb
new file mode 100644
index 0000000000..b955909475
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/enq_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/enque'
+require_relative '../../shared/sizedqueue/enque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "SizedQueue#enq" do
+ it_behaves_like :queue_enq, :enq, -> { SizedQueue.new(10) }
+end
+
+describe "SizedQueue#enq" do
+ it_behaves_like :sizedqueue_enq, :enq, -> n { SizedQueue.new(n) }
+end
+
+describe "SizedQueue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.enq(1, timeout: v) }
+end
diff --git a/spec/ruby/core/sizedqueue/freeze_spec.rb b/spec/ruby/core/sizedqueue/freeze_spec.rb
new file mode 100644
index 0000000000..98f01cae2f
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/freeze_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/freeze'
+
+describe "SizedQueue#freeze" do
+ it_behaves_like :queue_freeze, :freeze, -> { SizedQueue.new(1) }
+end
diff --git a/spec/ruby/core/sizedqueue/length_spec.rb b/spec/ruby/core/sizedqueue/length_spec.rb
new file mode 100644
index 0000000000..b93e7f8997
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/length'
+
+describe "SizedQueue#length" do
+ it_behaves_like :queue_length, :length, -> { SizedQueue.new(10) }
+end
diff --git a/spec/ruby/core/sizedqueue/max_spec.rb b/spec/ruby/core/sizedqueue/max_spec.rb
new file mode 100644
index 0000000000..cbabb5dc10
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/max_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/sizedqueue/max'
+
+describe "SizedQueue#max" do
+ it_behaves_like :sizedqueue_max, :max, -> n { SizedQueue.new(n) }
+end
+
+describe "SizedQueue#max=" do
+ it_behaves_like :sizedqueue_max=, :max=, -> n { SizedQueue.new(n) }
+end
diff --git a/spec/ruby/core/sizedqueue/new_spec.rb b/spec/ruby/core/sizedqueue/new_spec.rb
new file mode 100644
index 0000000000..d5704732c3
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/new_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/sizedqueue/new'
+
+describe "SizedQueue.new" do
+ it_behaves_like :sizedqueue_new, :new, -> *n { SizedQueue.new(*n) }
+end
diff --git a/spec/ruby/core/sizedqueue/num_waiting_spec.rb b/spec/ruby/core/sizedqueue/num_waiting_spec.rb
new file mode 100644
index 0000000000..9aef25e33d
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/num_waiting_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/sizedqueue/num_waiting'
+
+describe "SizedQueue#num_waiting" do
+ it_behaves_like :sizedqueue_num_waiting, :new, -> n { SizedQueue.new(n) }
+end
diff --git a/spec/ruby/core/sizedqueue/pop_spec.rb b/spec/ruby/core/sizedqueue/pop_spec.rb
new file mode 100644
index 0000000000..6338ddbaa0
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/pop_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/deque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "SizedQueue#pop" do
+ it_behaves_like :queue_deq, :pop, -> { SizedQueue.new(10) }
+end
+
+describe "SizedQueue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.pop(timeout: v) }
+end
diff --git a/spec/ruby/core/sizedqueue/push_spec.rb b/spec/ruby/core/sizedqueue/push_spec.rb
new file mode 100644
index 0000000000..9eaa6beca0
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/push_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/enque'
+require_relative '../../shared/sizedqueue/enque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "SizedQueue#push" do
+ it_behaves_like :queue_enq, :push, -> { SizedQueue.new(10) }
+end
+
+describe "SizedQueue#push" do
+ it_behaves_like :sizedqueue_enq, :push, -> n { SizedQueue.new(n) }
+end
+
+describe "SizedQueue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(1); q.push(1, timeout: v) }
+end
diff --git a/spec/ruby/core/sizedqueue/shift_spec.rb b/spec/ruby/core/sizedqueue/shift_spec.rb
new file mode 100644
index 0000000000..52974c1d99
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/shift_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/deque'
+require_relative '../../shared/types/rb_num2dbl_fails'
+
+describe "SizedQueue#shift" do
+ it_behaves_like :queue_deq, :shift, -> { SizedQueue.new(10) }
+end
+
+describe "SizedQueue operations with timeout" do
+ it_behaves_like :rb_num2dbl_fails, nil, -> v { q = SizedQueue.new(10); q.push(1); q.shift(timeout: v) }
+end
diff --git a/spec/ruby/core/sizedqueue/size_spec.rb b/spec/ruby/core/sizedqueue/size_spec.rb
new file mode 100644
index 0000000000..dfa76faabe
--- /dev/null
+++ b/spec/ruby/core/sizedqueue/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/queue/length'
+
+describe "SizedQueue#size" do
+ it_behaves_like :queue_length, :size, -> { SizedQueue.new(10) }
+end
diff --git a/spec/ruby/core/string/allocate_spec.rb b/spec/ruby/core/string/allocate_spec.rb
new file mode 100644
index 0000000000..00dadaf076
--- /dev/null
+++ b/spec/ruby/core/string/allocate_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "String.allocate" do
+ it "returns an instance of String" do
+ str = String.allocate
+ str.should.instance_of?(String)
+ end
+
+ it "returns a fully-formed String" do
+ str = String.allocate
+ str.size.should == 0
+ str << "more"
+ str.should == "more"
+ end
+
+ it "returns a binary String" do
+ String.allocate.encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/string/append_as_bytes_spec.rb b/spec/ruby/core/string/append_as_bytes_spec.rb
new file mode 100644
index 0000000000..feead64615
--- /dev/null
+++ b/spec/ruby/core/string/append_as_bytes_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "String#append_bytes" do
+ ruby_version_is "3.4" do
+ it "doesn't allow to mutate frozen strings" do
+ str = "hello".freeze
+ -> { str.append_as_bytes("\xE2\x82") }.should.raise(FrozenError)
+ end
+
+ it "allows creating broken strings in UTF8" do
+ str = +"hello"
+ str.append_as_bytes("\xE2\x82")
+ str.valid_encoding?.should == false
+
+ str.append_as_bytes("\xAC")
+ str.valid_encoding?.should == true
+ end
+
+ it "allows creating broken strings in UTF_32" do
+ str = "abc".encode(Encoding::UTF_32LE)
+ str.append_as_bytes("def")
+ str.encoding.should == Encoding::UTF_32LE
+ str.valid_encoding?.should == false
+ end
+
+ it "never changes the receiver encoding" do
+ str = "".b
+ str.append_as_bytes("€")
+ str.encoding.should == Encoding::BINARY
+ end
+
+ it "accepts variadic String or Integer arguments" do
+ str = "hello".b
+ str.append_as_bytes("\xE2\x82", 12, 43, "\xAC")
+ str.encoding.should == Encoding::BINARY
+ str.should == "hello\xE2\x82\f+\xAC".b
+ end
+
+ it "truncates integers to the least significant byte" do
+ str = +""
+ str.append_as_bytes(0x131, 0x232, 0x333, bignum_value, bignum_value(1))
+ str.bytes.should == [0x31, 0x32, 0x33, 0, 1]
+ end
+
+ it "wraps negative integers" do
+ str = "".b
+ str.append_as_bytes(-1, -bignum_value, -bignum_value(1))
+ str.bytes.should == [0xFF, 0, 0xFF]
+ end
+
+ it "only accepts strings or integers, and doesn't attempt to cast with #to_str or #to_int" do
+ to_str = mock("to_str")
+ to_str.should_not_receive(:to_str)
+ to_str.should_not_receive(:to_int)
+
+ str = +"hello"
+ -> { str.append_as_bytes(to_str) }.should.raise(TypeError, "wrong argument type MockObject (expected String or Integer)")
+ end
+ end
+end
diff --git a/spec/ruby/core/string/append_spec.rb b/spec/ruby/core/string/append_spec.rb
new file mode 100644
index 0000000000..e0f71b7c97
--- /dev/null
+++ b/spec/ruby/core/string/append_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/concat'
+
+describe "String#<<" do
+ it_behaves_like :string_concat, :<<
+ it_behaves_like :string_concat_encoding, :<<
+ it_behaves_like :string_concat_type_coercion, :<<
+
+ it "raises an ArgumentError when given the incorrect number of arguments" do
+ -> { "hello".send(:<<) }.should.raise(ArgumentError)
+ -> { "hello".send(:<<, "one", "two") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/ascii_only_spec.rb b/spec/ruby/core/string/ascii_only_spec.rb
new file mode 100644
index 0000000000..9af663beb8
--- /dev/null
+++ b/spec/ruby/core/string/ascii_only_spec.rb
@@ -0,0 +1,82 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#ascii_only?" do
+ describe "with ASCII only characters" do
+ it "returns true if the encoding is UTF-8" do
+ [ ["hello", true],
+ ["hello".encode('UTF-8'), true],
+ ["hello".dup.force_encoding('UTF-8'), true],
+ ].should be_computed_by(:ascii_only?)
+ end
+
+ it "returns true if the encoding is US-ASCII" do
+ "hello".dup.force_encoding(Encoding::US_ASCII).ascii_only?.should == true
+ "hello".encode(Encoding::US_ASCII).ascii_only?.should == true
+ end
+
+ it "returns true for all single-character UTF-8 Strings" do
+ 0.upto(127) do |n|
+ n.chr.ascii_only?.should == true
+ end
+ end
+ end
+
+ describe "with non-ASCII only characters" do
+ it "returns false if the encoding is BINARY" do
+ chr = 128.chr
+ chr.encoding.should == Encoding::BINARY
+ chr.ascii_only?.should == false
+ end
+
+ it "returns false if the String contains any non-ASCII characters" do
+ [ ["\u{6666}", false],
+ ["hello, \u{6666}", false],
+ ["\u{6666}".encode('UTF-8'), false],
+ ["\u{6666}".dup.force_encoding('UTF-8'), false],
+ ].should be_computed_by(:ascii_only?)
+ end
+
+ it "returns false if the encoding is US-ASCII" do
+ [ ["\u{6666}".dup.force_encoding(Encoding::US_ASCII), false],
+ ["hello, \u{6666}".dup.force_encoding(Encoding::US_ASCII), false],
+ ].should be_computed_by(:ascii_only?)
+ end
+ end
+
+ it "returns true for the empty String with an ASCII-compatible encoding" do
+ "".ascii_only?.should == true
+ "".encode('UTF-8').ascii_only?.should == true
+ end
+
+ it "returns false for the empty String with a non-ASCII-compatible encoding" do
+ "".dup.force_encoding('UTF-16LE').ascii_only?.should == false
+ "".encode('UTF-16BE').ascii_only?.should == false
+ end
+
+ it "returns false for a non-empty String with non-ASCII-compatible encoding" do
+ "\x78\x00".dup.force_encoding("UTF-16LE").ascii_only?.should == false
+ end
+
+ it "returns false when interpolating non ascii strings" do
+ base = "EU currency is".dup.force_encoding(Encoding::US_ASCII)
+ euro = "\u20AC"
+ interp = "#{base} #{euro}"
+ euro.ascii_only?.should == false
+ base.ascii_only?.should == true
+ interp.ascii_only?.should == false
+ end
+
+ it "returns false after appending non ASCII characters to an empty String" do
+ ("".dup << "λ").ascii_only?.should == false
+ end
+
+ it "returns false when concatenating an ASCII and non-ASCII String" do
+ "".dup.concat("λ").ascii_only?.should == false
+ end
+
+ it "returns false when replacing an ASCII String with a non-ASCII String" do
+ "".dup.replace("λ").ascii_only?.should == false
+ end
+end
diff --git a/spec/ruby/core/string/b_spec.rb b/spec/ruby/core/string/b_spec.rb
new file mode 100644
index 0000000000..d181447709
--- /dev/null
+++ b/spec/ruby/core/string/b_spec.rb
@@ -0,0 +1,16 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#b" do
+ it "returns a binary encoded string" do
+ "Hello".b.should == "Hello".force_encoding(Encoding::BINARY)
+ "ã“ã‚“ã¡ã«ã¯".b.should == "ã“ã‚“ã¡ã«ã¯".force_encoding(Encoding::BINARY)
+ end
+
+ it "returns new string without modifying self" do
+ str = "ã“ã‚“ã¡ã«ã¯"
+ str.b.should_not.equal?(str)
+ str.should == "ã“ã‚“ã¡ã«ã¯"
+ end
+end
diff --git a/spec/ruby/core/string/byteindex_spec.rb b/spec/ruby/core/string/byteindex_spec.rb
new file mode 100644
index 0000000000..f4c6408790
--- /dev/null
+++ b/spec/ruby/core/string/byteindex_spec.rb
@@ -0,0 +1,298 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/byte_index_common.rb'
+
+describe "String#byteindex" do
+ it "calls #to_str to convert the first argument" do
+ char = mock("string index char")
+ char.should_receive(:to_str).and_return("b")
+ "abc".byteindex(char).should == 1
+ end
+
+ it "calls #to_int to convert the second argument" do
+ offset = mock("string index offset")
+ offset.should_receive(:to_int).and_return(1)
+ "abc".byteindex("c", offset).should == 2
+ end
+
+ it "does not raise IndexError when byte offset is correct or on string boundary" do
+ "ã‚".byteindex("").should == 0
+ "ã‚".byteindex("", 0).should == 0
+ "ã‚".byteindex("", 3).should == 3
+ end
+
+ it_behaves_like :byte_index_common, :byteindex
+end
+
+describe "String#byteindex with String" do
+ it "behaves the same as String#byteindex(char) for one-character strings" do
+ "blablabla hello cruel world...!".split("").uniq.each do |str|
+ chr = str[0]
+ str.byteindex(str).should == str.byteindex(chr)
+
+ 0.upto(str.size + 1) do |start|
+ str.byteindex(str, start).should == str.byteindex(chr, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.byteindex(str, start).should == str.byteindex(chr, start)
+ end
+ end
+ end
+
+ it "returns the byteindex of the first occurrence of the given substring" do
+ "blablabla".byteindex("").should == 0
+ "blablabla".byteindex("b").should == 0
+ "blablabla".byteindex("bla").should == 0
+ "blablabla".byteindex("blabla").should == 0
+ "blablabla".byteindex("blablabla").should == 0
+
+ "blablabla".byteindex("l").should == 1
+ "blablabla".byteindex("la").should == 1
+ "blablabla".byteindex("labla").should == 1
+ "blablabla".byteindex("lablabla").should == 1
+
+ "blablabla".byteindex("a").should == 2
+ "blablabla".byteindex("abla").should == 2
+ "blablabla".byteindex("ablabla").should == 2
+ end
+
+ it "treats the offset as a byteindex" do
+ "aaaaa".byteindex("a", 0).should == 0
+ "aaaaa".byteindex("a", 2).should == 2
+ "aaaaa".byteindex("a", 4).should == 4
+ end
+
+ it "ignores string subclasses" do
+ "blablabla".byteindex(StringSpecs::MyString.new("bla")).should == 0
+ StringSpecs::MyString.new("blablabla").byteindex("bla").should == 0
+ StringSpecs::MyString.new("blablabla").byteindex(StringSpecs::MyString.new("bla")).should == 0
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".byteindex("bl", 0).should == 0
+ "blablabla".byteindex("bl", 1).should == 3
+ "blablabla".byteindex("bl", 2).should == 3
+ "blablabla".byteindex("bl", 3).should == 3
+
+ "blablabla".byteindex("bla", 0).should == 0
+ "blablabla".byteindex("bla", 1).should == 3
+ "blablabla".byteindex("bla", 2).should == 3
+ "blablabla".byteindex("bla", 3).should == 3
+
+ "blablabla".byteindex("blab", 0).should == 0
+ "blablabla".byteindex("blab", 1).should == 3
+ "blablabla".byteindex("blab", 2).should == 3
+ "blablabla".byteindex("blab", 3).should == 3
+
+ "blablabla".byteindex("la", 1).should == 1
+ "blablabla".byteindex("la", 2).should == 4
+ "blablabla".byteindex("la", 3).should == 4
+ "blablabla".byteindex("la", 4).should == 4
+
+ "blablabla".byteindex("lab", 1).should == 1
+ "blablabla".byteindex("lab", 2).should == 4
+ "blablabla".byteindex("lab", 3).should == 4
+ "blablabla".byteindex("lab", 4).should == 4
+
+ "blablabla".byteindex("ab", 2).should == 2
+ "blablabla".byteindex("ab", 3).should == 5
+ "blablabla".byteindex("ab", 4).should == 5
+ "blablabla".byteindex("ab", 5).should == 5
+
+ "blablabla".byteindex("", 0).should == 0
+ "blablabla".byteindex("", 1).should == 1
+ "blablabla".byteindex("", 2).should == 2
+ "blablabla".byteindex("", 7).should == 7
+ "blablabla".byteindex("", 8).should == 8
+ "blablabla".byteindex("", 9).should == 9
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.byteindex(needle, offset).should ==
+ str.byteindex(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".byteindex("B").should == nil
+ "blablabla".byteindex("z").should == nil
+ "blablabla".byteindex("BLA").should == nil
+ "blablabla".byteindex("blablablabla").should == nil
+ "blablabla".byteindex("", 10).should == nil
+
+ "hello".byteindex("he", 1).should == nil
+ "hello".byteindex("he", 2).should == nil
+ "I’ve got a multibyte character.\n".byteindex("\n\n").should == nil
+ end
+
+ it "returns the character byteindex of a multibyte character" do
+ "ã‚りãŒã¨ã†".byteindex("ãŒ").should == 6
+ end
+
+ it "returns the character byteindex after offset" do
+ "ã‚れã‚れ".byteindex("ã‚", 3).should == 6
+ "ã‚りãŒã¨ã†ã‚りãŒã¨ã†".byteindex("ãŒ", 9).should == 21
+ end
+
+ it "returns the character byteindex after a partial first match" do
+ "</</h".byteindex("</h").should == 2
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ char = "れ".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".byteindex(char)
+ end.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "handles a substring in a superset encoding" do
+ 'abc'.dup.force_encoding(Encoding::US_ASCII).byteindex('é').should == nil
+ end
+
+ it "handles a substring in a subset encoding" do
+ 'été'.byteindex('t'.dup.force_encoding(Encoding::US_ASCII)).should == 2
+ end
+end
+
+describe "String#byteindex with Regexp" do
+ it "behaves the same as String#byteindex(string) for escaped string regexps" do
+ ["blablabla", "hello cruel world...!"].each do |str|
+ ["", "b", "bla", "lab", "o c", "d."].each do |needle|
+ regexp = Regexp.new(Regexp.escape(needle))
+ str.byteindex(regexp).should == str.byteindex(needle)
+
+ 0.upto(str.size + 1) do |start|
+ str.byteindex(regexp, start).should == str.byteindex(needle, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.byteindex(regexp, start).should == str.byteindex(needle, start)
+ end
+ end
+ end
+ end
+
+ it "returns the byteindex of the first match of regexp" do
+ "blablabla".byteindex(/bla/).should == 0
+ "blablabla".byteindex(/BLA/i).should == 0
+
+ "blablabla".byteindex(/.{0}/).should == 0
+ "blablabla".byteindex(/.{6}/).should == 0
+ "blablabla".byteindex(/.{9}/).should == 0
+
+ "blablabla".byteindex(/.*/).should == 0
+ "blablabla".byteindex(/.+/).should == 0
+
+ "blablabla".byteindex(/lab|b/).should == 0
+
+ not_supported_on :opal do
+ "blablabla".byteindex(/\A/).should == 0
+ "blablabla".byteindex(/\Z/).should == 9
+ "blablabla".byteindex(/\z/).should == 9
+ "blablabla\n".byteindex(/\Z/).should == 9
+ "blablabla\n".byteindex(/\z/).should == 10
+ end
+
+ "blablabla".byteindex(/^/).should == 0
+ "\nblablabla".byteindex(/^/).should == 0
+ "b\nablabla".byteindex(/$/).should == 1
+ "bl\nablabla".byteindex(/$/).should == 2
+
+ "blablabla".byteindex(/.l./).should == 0
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".byteindex(/.{0}/, 5).should == 5
+ "blablabla".byteindex(/.{1}/, 5).should == 5
+ "blablabla".byteindex(/.{2}/, 5).should == 5
+ "blablabla".byteindex(/.{3}/, 5).should == 5
+ "blablabla".byteindex(/.{4}/, 5).should == 5
+
+ "blablabla".byteindex(/.{0}/, 3).should == 3
+ "blablabla".byteindex(/.{1}/, 3).should == 3
+ "blablabla".byteindex(/.{2}/, 3).should == 3
+ "blablabla".byteindex(/.{5}/, 3).should == 3
+ "blablabla".byteindex(/.{6}/, 3).should == 3
+
+ "blablabla".byteindex(/.l./, 0).should == 0
+ "blablabla".byteindex(/.l./, 1).should == 3
+ "blablabla".byteindex(/.l./, 2).should == 3
+ "blablabla".byteindex(/.l./, 3).should == 3
+
+ "xblaxbla".byteindex(/x./, 0).should == 0
+ "xblaxbla".byteindex(/x./, 1).should == 4
+ "xblaxbla".byteindex(/x./, 2).should == 4
+
+ not_supported_on :opal do
+ "blablabla\n".byteindex(/\Z/, 9).should == 9
+ end
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.byteindex(needle, offset).should ==
+ str.byteindex(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".byteindex(/BLA/).should == nil
+
+ "blablabla".byteindex(/.{10}/).should == nil
+ "blaxbla".byteindex(/.x/, 3).should == nil
+ "blaxbla".byteindex(/..x/, 2).should == nil
+ end
+
+ it "returns nil if the Regexp matches the empty string and the offset is out of range" do
+ "ruby".byteindex(//, 12).should == nil
+ end
+
+ it "supports \\G which matches at the given start offset" do
+ "helloYOU.".byteindex(/\GYOU/, 5).should == 5
+ "helloYOU.".byteindex(/\GYOU/).should == nil
+
+ re = /\G.+YOU/
+ # The # marks where \G will match.
+ [
+ ["#hi!YOUall.", 0],
+ ["h#i!YOUall.", 1],
+ ["hi#!YOUall.", 2],
+ ["hi!#YOUall.", nil]
+ ].each do |spec|
+
+ start = spec[0].byteindex("#")
+ str = spec[0].delete("#")
+
+ str.byteindex(re, start).should == spec[1]
+ end
+ end
+
+ it "converts start_offset to an integer via to_int" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ "RWOARW".byteindex(/R./, obj).should == 4
+ end
+
+ it "returns the character byteindex of a multibyte character" do
+ "ã‚りãŒã¨ã†".byteindex(/ãŒ/).should == 6
+ end
+
+ it "returns the character byteindex after offset" do
+ "ã‚れã‚れ".byteindex(/ã‚/, 3).should == 6
+ end
+
+ it "treats the offset as a byteindex" do
+ "ã‚れã‚ã‚れ".byteindex(/ã‚/, 6).should == 6
+ end
+end
diff --git a/spec/ruby/core/string/byterindex_spec.rb b/spec/ruby/core/string/byterindex_spec.rb
new file mode 100644
index 0000000000..569820463d
--- /dev/null
+++ b/spec/ruby/core/string/byterindex_spec.rb
@@ -0,0 +1,353 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/byte_index_common.rb'
+
+describe "String#byterindex with object" do
+ it "tries to convert obj to a string via to_str" do
+ obj = mock('lo')
+ def obj.to_str() "lo" end
+ "hello".byterindex(obj).should == "hello".byterindex("lo")
+
+ obj = mock('o')
+ def obj.respond_to?(arg, *) true end
+ def obj.method_missing(*args) "o" end
+ "hello".byterindex(obj).should == "hello".byterindex("o")
+ end
+
+ it "calls #to_int to convert the second argument" do
+ offset = mock("string index offset")
+ offset.should_receive(:to_int).and_return(3)
+ "abc".byterindex("c", offset).should == 2
+ end
+
+ it "does not raise IndexError when byte offset is correct or on string boundary" do
+ "ã‚".byterindex("", 0).should == 0
+ "ã‚".byterindex("", 3).should == 3
+ "ã‚".byterindex("").should == 3
+ end
+
+ it_behaves_like :byte_index_common, :byterindex
+end
+
+describe "String#byterindex with String" do
+ it "behaves the same as String#byterindex(char) for one-character strings" do
+ "blablabla hello cruel world...!".split("").uniq.each do |str|
+ chr = str[0]
+ str.byterindex(str).should == str.byterindex(chr)
+
+ 0.upto(str.size + 1) do |start|
+ str.byterindex(str, start).should == str.byterindex(chr, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.byterindex(str, start).should == str.byterindex(chr, start)
+ end
+ end
+ end
+
+ it "behaves the same as String#byterindex(?char) for one-character strings" do
+ "blablabla hello cruel world...!".split("").uniq.each do |str|
+ chr = str[0] =~ / / ? str[0] : eval("?#{str[0]}")
+ str.byterindex(str).should == str.byterindex(chr)
+
+ 0.upto(str.size + 1) do |start|
+ str.byterindex(str, start).should == str.byterindex(chr, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.byterindex(str, start).should == str.byterindex(chr, start)
+ end
+ end
+ end
+
+ it "returns the index of the last occurrence of the given substring" do
+ "blablabla".byterindex("").should == 9
+ "blablabla".byterindex("a").should == 8
+ "blablabla".byterindex("la").should == 7
+ "blablabla".byterindex("bla").should == 6
+ "blablabla".byterindex("abla").should == 5
+ "blablabla".byterindex("labla").should == 4
+ "blablabla".byterindex("blabla").should == 3
+ "blablabla".byterindex("ablabla").should == 2
+ "blablabla".byterindex("lablabla").should == 1
+ "blablabla".byterindex("blablabla").should == 0
+
+ "blablabla".byterindex("l").should == 7
+ "blablabla".byterindex("bl").should == 6
+ "blablabla".byterindex("abl").should == 5
+ "blablabla".byterindex("labl").should == 4
+ "blablabla".byterindex("blabl").should == 3
+ "blablabla".byterindex("ablabl").should == 2
+ "blablabla".byterindex("lablabl").should == 1
+ "blablabla".byterindex("blablabl").should == 0
+
+ "blablabla".byterindex("b").should == 6
+ "blablabla".byterindex("ab").should == 5
+ "blablabla".byterindex("lab").should == 4
+ "blablabla".byterindex("blab").should == 3
+ "blablabla".byterindex("ablab").should == 2
+ "blablabla".byterindex("lablab").should == 1
+ "blablabla".byterindex("blablab").should == 0
+ end
+
+ it "ignores string subclasses" do
+ "blablabla".byterindex(StringSpecs::MyString.new("bla")).should == 6
+ StringSpecs::MyString.new("blablabla").byterindex("bla").should == 6
+ StringSpecs::MyString.new("blablabla").byterindex(StringSpecs::MyString.new("bla")).should == 6
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".byterindex("bl", 0).should == 0
+ "blablabla".byterindex("bl", 1).should == 0
+ "blablabla".byterindex("bl", 2).should == 0
+ "blablabla".byterindex("bl", 3).should == 3
+
+ "blablabla".byterindex("bla", 0).should == 0
+ "blablabla".byterindex("bla", 1).should == 0
+ "blablabla".byterindex("bla", 2).should == 0
+ "blablabla".byterindex("bla", 3).should == 3
+
+ "blablabla".byterindex("blab", 0).should == 0
+ "blablabla".byterindex("blab", 1).should == 0
+ "blablabla".byterindex("blab", 2).should == 0
+ "blablabla".byterindex("blab", 3).should == 3
+ "blablabla".byterindex("blab", 6).should == 3
+ "blablablax".byterindex("blab", 6).should == 3
+
+ "blablabla".byterindex("la", 1).should == 1
+ "blablabla".byterindex("la", 2).should == 1
+ "blablabla".byterindex("la", 3).should == 1
+ "blablabla".byterindex("la", 4).should == 4
+
+ "blablabla".byterindex("lab", 1).should == 1
+ "blablabla".byterindex("lab", 2).should == 1
+ "blablabla".byterindex("lab", 3).should == 1
+ "blablabla".byterindex("lab", 4).should == 4
+
+ "blablabla".byterindex("ab", 2).should == 2
+ "blablabla".byterindex("ab", 3).should == 2
+ "blablabla".byterindex("ab", 4).should == 2
+ "blablabla".byterindex("ab", 5).should == 5
+
+ "blablabla".byterindex("", 0).should == 0
+ "blablabla".byterindex("", 1).should == 1
+ "blablabla".byterindex("", 2).should == 2
+ "blablabla".byterindex("", 7).should == 7
+ "blablabla".byterindex("", 8).should == 8
+ "blablabla".byterindex("", 9).should == 9
+ "blablabla".byterindex("", 10).should == 9
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.byterindex(needle, offset).should ==
+ str.byterindex(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".byterindex("B").should == nil
+ "blablabla".byterindex("z").should == nil
+ "blablabla".byterindex("BLA").should == nil
+ "blablabla".byterindex("blablablabla").should == nil
+
+ "hello".byterindex("lo", 0).should == nil
+ "hello".byterindex("lo", 1).should == nil
+ "hello".byterindex("lo", 2).should == nil
+
+ "hello".byterindex("llo", 0).should == nil
+ "hello".byterindex("llo", 1).should == nil
+
+ "hello".byterindex("el", 0).should == nil
+ "hello".byterindex("ello", 0).should == nil
+
+ "hello".byterindex("", -6).should == nil
+ "hello".byterindex("", -7).should == nil
+
+ "hello".byterindex("h", -6).should == nil
+ end
+
+ it "tries to convert start_offset to an integer via to_int" do
+ obj = mock('5')
+ def obj.to_int() 5 end
+ "str".byterindex("st", obj).should == 0
+
+ obj = mock('5')
+ def obj.respond_to?(arg, *) true end
+ def obj.method_missing(*args) 5 end
+ "str".byterindex("st", obj).should == 0
+ end
+
+ it "raises a TypeError when given offset is nil" do
+ -> { "str".byterindex("st", nil) }.should.raise(TypeError)
+ end
+
+ it "handles a substring in a superset encoding" do
+ 'abc'.dup.force_encoding(Encoding::US_ASCII).byterindex('é').should == nil
+ end
+
+ it "handles a substring in a subset encoding" do
+ 'été'.byterindex('t'.dup.force_encoding(Encoding::US_ASCII)).should == 2
+ end
+end
+
+describe "String#byterindex with Regexp" do
+ it "behaves the same as String#byterindex(string) for escaped string regexps" do
+ ["blablabla", "hello cruel world...!"].each do |str|
+ ["", "b", "bla", "lab", "o c", "d."].each do |needle|
+ regexp = Regexp.new(Regexp.escape(needle))
+ str.byterindex(regexp).should == str.byterindex(needle)
+
+ 0.upto(str.size + 1) do |start|
+ str.byterindex(regexp, start).should == str.byterindex(needle, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.byterindex(regexp, start).should == str.byterindex(needle, start)
+ end
+ end
+ end
+ end
+
+ it "returns the index of the first match from the end of string of regexp" do
+ "blablabla".byterindex(/bla/).should == 6
+ "blablabla".byterindex(/BLA/i).should == 6
+
+ "blablabla".byterindex(/.{0}/).should == 9
+ "blablabla".byterindex(/.{1}/).should == 8
+ "blablabla".byterindex(/.{2}/).should == 7
+ "blablabla".byterindex(/.{6}/).should == 3
+ "blablabla".byterindex(/.{9}/).should == 0
+
+ "blablabla".byterindex(/.*/).should == 9
+ "blablabla".byterindex(/.+/).should == 8
+
+ "blablabla".byterindex(/bla|a/).should == 8
+
+ not_supported_on :opal do
+ "blablabla".byterindex(/\A/).should == 0
+ "blablabla".byterindex(/\Z/).should == 9
+ "blablabla".byterindex(/\z/).should == 9
+ "blablabla\n".byterindex(/\Z/).should == 10
+ "blablabla\n".byterindex(/\z/).should == 10
+ end
+
+ "blablabla".byterindex(/^/).should == 0
+ not_supported_on :opal do
+ "\nblablabla".byterindex(/^/).should == 1
+ "b\nlablabla".byterindex(/^/).should == 2
+ end
+ "blablabla".byterindex(/$/).should == 9
+
+ "blablabla".byterindex(/.l./).should == 6
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".byterindex(/.{0}/, 5).should == 5
+ "blablabla".byterindex(/.{1}/, 5).should == 5
+ "blablabla".byterindex(/.{2}/, 5).should == 5
+ "blablabla".byterindex(/.{3}/, 5).should == 5
+ "blablabla".byterindex(/.{4}/, 5).should == 5
+
+ "blablabla".byterindex(/.{0}/, 3).should == 3
+ "blablabla".byterindex(/.{1}/, 3).should == 3
+ "blablabla".byterindex(/.{2}/, 3).should == 3
+ "blablabla".byterindex(/.{5}/, 3).should == 3
+ "blablabla".byterindex(/.{6}/, 3).should == 3
+
+ "blablabla".byterindex(/.l./, 0).should == 0
+ "blablabla".byterindex(/.l./, 1).should == 0
+ "blablabla".byterindex(/.l./, 2).should == 0
+ "blablabla".byterindex(/.l./, 3).should == 3
+
+ "blablablax".byterindex(/.x/, 10).should == 8
+ "blablablax".byterindex(/.x/, 9).should == 8
+ "blablablax".byterindex(/.x/, 8).should == 8
+
+ "blablablax".byterindex(/..x/, 10).should == 7
+ "blablablax".byterindex(/..x/, 9).should == 7
+ "blablablax".byterindex(/..x/, 8).should == 7
+ "blablablax".byterindex(/..x/, 7).should == 7
+
+ not_supported_on :opal do
+ "blablabla\n".byterindex(/\Z/, 9).should == 9
+ end
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.byterindex(needle, offset).should ==
+ str.byterindex(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".byterindex(/BLA/).should == nil
+ "blablabla".byterindex(/.{10}/).should == nil
+ "blablablax".byterindex(/.x/, 7).should == nil
+ "blablablax".byterindex(/..x/, 6).should == nil
+
+ not_supported_on :opal do
+ "blablabla".byterindex(/\Z/, 5).should == nil
+ "blablabla".byterindex(/\z/, 5).should == nil
+ "blablabla\n".byterindex(/\z/, 9).should == nil
+ end
+ end
+
+ not_supported_on :opal do
+ it "supports \\G which matches at the given start offset" do
+ "helloYOU.".byterindex(/YOU\G/, 8).should == 5
+ "helloYOU.".byterindex(/YOU\G/).should == nil
+
+ idx = "helloYOUall!".index("YOU")
+ re = /YOU.+\G.+/
+ # The # marks where \G will match.
+ [
+ ["helloYOU#all.", nil],
+ ["helloYOUa#ll.", idx],
+ ["helloYOUal#l.", idx],
+ ["helloYOUall#.", idx],
+ ["helloYOUall.#", nil]
+ ].each do |i|
+ start = i[0].index("#")
+ str = i[0].delete("#")
+
+ str.byterindex(re, start).should == i[1]
+ end
+ end
+ end
+
+ it "tries to convert start_offset to an integer" do
+ obj = mock('5')
+ def obj.to_int() 5 end
+ "str".byterindex(/../, obj).should == 1
+
+ obj = mock('5')
+ def obj.respond_to?(arg, *) true end
+ def obj.method_missing(*args); 5; end
+ "str".byterindex(/../, obj).should == 1
+ end
+
+ it "raises a TypeError when given offset is nil" do
+ -> { "str".byterindex(/../, nil) }.should.raise(TypeError)
+ end
+
+ it "returns the reverse byte index of a multibyte character" do
+ "ã‚りãŒã‚ŠãŒã¨ã†".byterindex("ãŒ").should == 12
+ "ã‚りãŒã‚ŠãŒã¨ã†".byterindex(/ãŒ/).should == 12
+ end
+
+ it "returns the character index before the finish" do
+ "ã‚りãŒã‚ŠãŒã¨ã†".byterindex("ãŒ", 9).should == 6
+ "ã‚りãŒã‚ŠãŒã¨ã†".byterindex(/ãŒ/, 9).should == 6
+ end
+end
diff --git a/spec/ruby/core/string/bytes_spec.rb b/spec/ruby/core/string/bytes_spec.rb
new file mode 100644
index 0000000000..e6019fb987
--- /dev/null
+++ b/spec/ruby/core/string/bytes_spec.rb
@@ -0,0 +1,55 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "String#bytes" do
+ before :each do
+ @utf8 = "æ±äº¬"
+ @ascii = 'Tokyo'
+ @utf8_ascii = @utf8 + @ascii
+ end
+
+ it "returns an Array when no block is given" do
+ @utf8.bytes.should.instance_of?(Array)
+ end
+
+ it "yields each byte to a block if one is given, returning self" do
+ bytes = []
+ @utf8.bytes {|b| bytes << b}.should == @utf8
+ bytes.should == @utf8.bytes.to_a
+ end
+
+ it "returns #bytesize bytes" do
+ @utf8_ascii.bytes.to_a.size.should == @utf8_ascii.bytesize
+ end
+
+ it "returns bytes as Integers" do
+ @ascii.bytes.to_a.each {|b| b.should.instance_of?(Integer)}
+ @utf8_ascii.bytes { |b| b.should.instance_of?(Integer) }
+ end
+
+ it "agrees with #unpack('C*')" do
+ @utf8_ascii.bytes.to_a.should == @utf8_ascii.unpack("C*")
+ end
+
+ it "yields/returns no bytes for the empty string" do
+ ''.bytes.to_a.should == []
+ end
+end
+
+describe "String#bytes" do
+ before :each do
+ @utf8 = "æ±äº¬"
+ @ascii = 'Tokyo'
+ @utf8_ascii = @utf8 + @ascii
+ end
+
+ it "agrees with #getbyte" do
+ @utf8_ascii.bytes.to_a.each_with_index do |byte,index|
+ byte.should == @utf8_ascii.getbyte(index)
+ end
+ end
+
+ it "is unaffected by #force_encoding" do
+ @utf8.dup.force_encoding('ASCII').bytes.to_a.should == @utf8.bytes.to_a
+ end
+end
diff --git a/spec/ruby/core/string/bytesize_spec.rb b/spec/ruby/core/string/bytesize_spec.rb
new file mode 100644
index 0000000000..2bbefc0820
--- /dev/null
+++ b/spec/ruby/core/string/bytesize_spec.rb
@@ -0,0 +1,33 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#bytesize" do
+ it "returns the length of self in bytes" do
+ "hello".bytesize.should == 5
+ " ".bytesize.should == 1
+ end
+
+ it "works with strings containing single UTF-8 characters" do
+ "\u{6666}".bytesize.should == 3
+ end
+
+ it "works with pseudo-ASCII strings containing single UTF-8 characters" do
+ "\u{6666}".dup.force_encoding('ASCII').bytesize.should == 3
+ end
+
+ it "works with strings containing UTF-8 characters" do
+ "c \u{6666}".dup.force_encoding('UTF-8').bytesize.should == 5
+ "c \u{6666}".bytesize.should == 5
+ end
+
+ it "works with pseudo-ASCII strings containing UTF-8 characters" do
+ "c \u{6666}".dup.force_encoding('ASCII').bytesize.should == 5
+ end
+
+ it "returns 0 for the empty string" do
+ "".bytesize.should == 0
+ "".dup.force_encoding('ASCII').bytesize.should == 0
+ "".dup.force_encoding('UTF-8').bytesize.should == 0
+ end
+end
diff --git a/spec/ruby/core/string/byteslice_spec.rb b/spec/ruby/core/string/byteslice_spec.rb
new file mode 100644
index 0000000000..4ad9e8d8f1
--- /dev/null
+++ b/spec/ruby/core/string/byteslice_spec.rb
@@ -0,0 +1,33 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/slice'
+
+describe "String#byteslice" do
+ it "needs to reviewed for spec completeness"
+
+ it_behaves_like :string_slice, :byteslice
+end
+
+describe "String#byteslice with index, length" do
+ it_behaves_like :string_slice_index_length, :byteslice
+end
+
+describe "String#byteslice with Range" do
+ it_behaves_like :string_slice_range, :byteslice
+end
+
+describe "String#byteslice on non ASCII strings" do
+ it "returns byteslice of unicode strings" do
+ "\u3042".byteslice(1).should == "\x81".dup.force_encoding("UTF-8")
+ "\u3042".byteslice(1, 2).should == "\x81\x82".dup.force_encoding("UTF-8")
+ "\u3042".byteslice(1..2).should == "\x81\x82".dup.force_encoding("UTF-8")
+ "\u3042".byteslice(-1).should == "\x82".dup.force_encoding("UTF-8")
+ end
+
+ it "returns a String in the same encoding as self" do
+ "ruby".encode("UTF-8").slice(0).encoding.should == Encoding::UTF_8
+ "ruby".encode("US-ASCII").slice(0).encoding.should == Encoding::US_ASCII
+ "ruby".encode("Windows-1251").slice(0).encoding.should == Encoding::Windows_1251
+ end
+end
diff --git a/spec/ruby/core/string/bytesplice_spec.rb b/spec/ruby/core/string/bytesplice_spec.rb
new file mode 100644
index 0000000000..3e5e6fe1ee
--- /dev/null
+++ b/spec/ruby/core/string/bytesplice_spec.rb
@@ -0,0 +1,290 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#bytesplice" do
+ it "raises IndexError when index is less than -bytesize" do
+ -> { "hello".bytesplice(-6, 0, "xxx") }.should.raise(IndexError, "index -6 out of string")
+ end
+
+ it "raises IndexError when index is greater than bytesize" do
+ -> { "hello".bytesplice(6, 0, "xxx") }.should.raise(IndexError, "index 6 out of string")
+ end
+
+ it "raises IndexError for negative length" do
+ -> { "abc".bytesplice(0, -2, "") }.should.raise(IndexError, "negative length -2")
+ end
+
+ it "replaces with integer indices" do
+ "hello".bytesplice(-5, 0, "xxx").should == "xxxhello"
+ "hello".bytesplice(0, 0, "xxx").should == "xxxhello"
+ "hello".bytesplice(0, 1, "xxx").should == "xxxello"
+ "hello".bytesplice(0, 5, "xxx").should == "xxx"
+ "hello".bytesplice(0, 6, "xxx").should == "xxx"
+ end
+
+ it "raises RangeError when range left boundary is less than -bytesize" do
+ -> { "hello".bytesplice(-6...-6, "xxx") }.should.raise(RangeError, "-6...-6 out of range")
+ end
+
+ it "replaces with ranges" do
+ "hello".bytesplice(-5...-5, "xxx").should == "xxxhello"
+ "hello".bytesplice(0...0, "xxx").should == "xxxhello"
+ "hello".bytesplice(0..0, "xxx").should == "xxxello"
+ "hello".bytesplice(0...1, "xxx").should == "xxxello"
+ "hello".bytesplice(0..1, "xxx").should == "xxxllo"
+ "hello".bytesplice(0..-1, "xxx").should == "xxx"
+ "hello".bytesplice(0...5, "xxx").should == "xxx"
+ "hello".bytesplice(0...6, "xxx").should == "xxx"
+ end
+
+ it "raises TypeError when integer index is provided without length argument" do
+ -> { "hello".bytesplice(0, "xxx") }.should.raise(TypeError, "wrong argument type Integer (expected Range)")
+ end
+
+ it "replaces on an empty string" do
+ "".bytesplice(0, 0, "").should == ""
+ "".bytesplice(0, 0, "xxx").should == "xxx"
+ end
+
+ it "mutates self" do
+ s = "hello"
+ s.bytesplice(2, 1, "xxx").should.equal?(s)
+ end
+
+ it "raises when string is frozen" do
+ s = "hello".freeze
+ -> { s.bytesplice(2, 1, "xxx") }.should.raise(FrozenError, "can't modify frozen String: \"hello\"")
+ end
+
+ it "raises IndexError when str_index is less than -bytesize" do
+ -> { "hello".bytesplice(2, 1, "HELLO", -6, 0) }.should.raise(IndexError, "index -6 out of string")
+ end
+
+ it "raises IndexError when str_index is greater than bytesize" do
+ -> { "hello".bytesplice(2, 1, "HELLO", 6, 0) }.should.raise(IndexError, "index 6 out of string")
+ end
+
+ it "raises IndexError for negative str length" do
+ -> { "abc".bytesplice(0, 1, "", 0, -2) }.should.raise(IndexError, "negative length -2")
+ end
+
+ it "replaces with integer str indices" do
+ "hello".bytesplice(1, 2, "HELLO", -5, 0).should == "hlo"
+ "hello".bytesplice(1, 2, "HELLO", 0, 0).should == "hlo"
+ "hello".bytesplice(1, 2, "HELLO", 0, 1).should == "hHlo"
+ "hello".bytesplice(1, 2, "HELLO", 0, 5).should == "hHELLOlo"
+ "hello".bytesplice(1, 2, "HELLO", 0, 6).should == "hHELLOlo"
+ end
+
+ it "raises RangeError when str range left boundary is less than -bytesize" do
+ -> { "hello".bytesplice(0..1, "HELLO", -6...-6) }.should.raise(RangeError, "-6...-6 out of range")
+ end
+
+ it "replaces with str ranges" do
+ "hello".bytesplice(1..2, "HELLO", -5...-5).should == "hlo"
+ "hello".bytesplice(1..2, "HELLO", 0...0).should == "hlo"
+ "hello".bytesplice(1..2, "HELLO", 0..0).should == "hHlo"
+ "hello".bytesplice(1..2, "HELLO", 0...1).should == "hHlo"
+ "hello".bytesplice(1..2, "HELLO", 0..1).should == "hHElo"
+ "hello".bytesplice(1..2, "HELLO", 0..-1).should == "hHELLOlo"
+ "hello".bytesplice(1..2, "HELLO", 0...5).should == "hHELLOlo"
+ "hello".bytesplice(1..2, "HELLO", 0...6).should == "hHELLOlo"
+ end
+
+ it "raises ArgumentError when integer str index is provided without str length argument" do
+ -> { "hello".bytesplice(0, 1, "xxx", 0) }.should.raise(ArgumentError, "wrong number of arguments (given 4, expected 2, 3, or 5)")
+ end
+
+ it "replaces on an empty string with str index/length" do
+ "".bytesplice(0, 0, "", 0, 0).should == ""
+ "".bytesplice(0, 0, "xxx", 0, 1).should == "x"
+ end
+
+ it "mutates self with substring and str index/length" do
+ s = "hello"
+ s.bytesplice(2, 1, "xxx", 1, 2).should.equal?(s)
+ s.should.eql?("hexxlo")
+ end
+
+ it "raises when string is frozen and str index/length" do
+ s = "hello".freeze
+ -> { s.bytesplice(2, 1, "xxx", 0, 1) }.should.raise(FrozenError, "can't modify frozen String: \"hello\"")
+ end
+
+ it "replaces on an empty string with str range" do
+ "".bytesplice(0..0, "", 0..0).should == ""
+ "".bytesplice(0..0, "xyz", 0..1).should == "xy"
+ end
+
+ it "mutates self with substring and str range" do
+ s = "hello"
+ s.bytesplice(2..2, "xyz", 1..2).should.equal?(s)
+ s.should.eql?("heyzlo")
+ end
+
+ it "raises when string is frozen and str range" do
+ s = "hello".freeze
+ -> { s.bytesplice(2..2, "yzx", 0..1) }.should.raise(FrozenError, "can't modify frozen String: \"hello\"")
+ end
+end
+
+describe "String#bytesplice with multibyte characters" do
+ it "raises IndexError when index is out of byte size boundary" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-16, 0, "xxx") }.should.raise(IndexError, "index -16 out of string")
+ end
+
+ it "raises IndexError when index is not on a codepoint boundary" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(1, 0, "xxx") }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ end
+
+ it "raises IndexError when length is not matching the codepoint boundary" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 1, "xxx") }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 2, "xxx") }.should.raise(IndexError, "offset 2 does not land on character boundary")
+ end
+
+ it "replaces with integer indices" do
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(-15, 0, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 0, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0, 3, "xxx").should == "xxxã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã¯ã¯").should == "ã“ã¯ã¯ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(15, 0, "xxx").should == "ã“ã‚“ã«ã¡ã¯xxx"
+ end
+
+ it "replaces with range" do
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(-15...-16, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0...0, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "xxx").should == "xxxã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0...3, "xxx").should == "xxxã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..5, "xxx").should == "xxxã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..-1, "xxx").should == "xxx"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0...15, "xxx").should == "xxx"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0...18, "xxx").should == "xxx"
+ end
+
+ it "treats negative length for range as 0" do
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0...-100, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3...-100, "xxx").should == "ã“xxxã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(-15...-100, "xxx").should == "xxxã“ã‚“ã«ã¡ã¯"
+ end
+
+ it "raises when ranges not match codepoint boundaries" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0..0, "x") }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(0..1, "x") }.should.raise(IndexError, "offset 2 does not land on character boundary")
+ # Begin is incorrect
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-4..-1, "x") }.should.raise(IndexError, "offset 11 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-5..-1, "x") }.should.raise(IndexError, "offset 10 does not land on character boundary")
+ # End is incorrect
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-3..-2, "x") }.should.raise(IndexError, "offset 14 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(-3..-3, "x") }.should.raise(IndexError, "offset 13 does not land on character boundary")
+ end
+
+ it "deals with a different encoded argument" do
+ s = "ã“ã‚“ã«ã¡ã¯"
+ s.encoding.should == Encoding::UTF_8
+ sub = "xxxxxx"
+ sub.force_encoding(Encoding::US_ASCII)
+
+ result = s.bytesplice(0, 3, sub)
+ result.should == "xxxxxxã‚“ã«ã¡ã¯"
+ result.encoding.should == Encoding::UTF_8
+
+ s = "xxxxxx"
+ s.force_encoding(Encoding::US_ASCII)
+ sub = "ã“ã‚“ã«ã¡ã¯"
+ sub.encoding.should == Encoding::UTF_8
+
+ result = s.bytesplice(0, 3, sub)
+ result.should == "ã“ã‚“ã«ã¡ã¯xxx"
+ result.encoding.should == Encoding::UTF_8
+ end
+
+ it "raises IndexError when str_index is out of byte size boundary" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", -16, 0) }.should.raise(IndexError, "index -16 out of string")
+ end
+
+ it "raises IndexError when str_index is not on a codepoint boundary" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 1, 0) }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ end
+
+ it "raises IndexError when str_length is not matching the codepoint boundary" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 1) }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 2) }.should.raise(IndexError, "offset 2 does not land on character boundary")
+ end
+
+ it "replaces with integer str indices" do
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", -15, 0).should == "ã“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 0).should == "ã“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 0, 3).should == "ã“ã“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã¯ã¯", 3, 3).should == "ã“ã¯ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(3, 3, "ã“ã‚“ã«ã¡ã¯", 15, 0).should == "ã“ã«ã¡ã¯"
+ end
+
+ it "replaces with str range" do
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", -15...-16).should == "ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...0).should == "ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3..5).should == "ã‚“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3...6).should == "ã‚“ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3..8).should == "ã‚“ã«ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0..-1).should == "ã“ã‚“ã«ã¡ã¯ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...15).should == "ã“ã‚“ã«ã¡ã¯ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...18).should == "ã“ã‚“ã«ã¡ã¯ã‚“ã«ã¡ã¯"
+ end
+
+ it "treats negative length for str range as 0" do
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 0...-100).should == "ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", 3...-100).should == "ã‚“ã«ã¡ã¯"
+ "ã“ã‚“ã«ã¡ã¯".bytesplice(0..2, "ã“ã‚“ã«ã¡ã¯", -15...-100).should == "ã‚“ã«ã¡ã¯"
+ end
+
+ it "raises when ranges not match codepoint boundaries in str" do
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“", 0..0) }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“", 0..1) }.should.raise(IndexError, "offset 2 does not land on character boundary")
+ # Begin is incorrect
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -4..-1) }.should.raise(IndexError, "offset 11 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -5..-1) }.should.raise(IndexError, "offset 10 does not land on character boundary")
+ # End is incorrect
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -3..-2) }.should.raise(IndexError, "offset 14 does not land on character boundary")
+ -> { "ã“ã‚“ã«ã¡ã¯".bytesplice(3...3, "ã“ã‚“ã«ã¡ã¯", -3..-3) }.should.raise(IndexError, "offset 13 does not land on character boundary")
+ end
+
+ it "deals with a different encoded argument with str index/length" do
+ s = "ã“ã‚“ã«ã¡ã¯"
+ s.encoding.should == Encoding::UTF_8
+ sub = "goodbye"
+ sub.force_encoding(Encoding::US_ASCII)
+
+ result = s.bytesplice(3, 3, sub, 0, 3)
+ result.should == "ã“gooã«ã¡ã¯"
+ result.encoding.should == Encoding::UTF_8
+
+ s = "hello"
+ s.force_encoding(Encoding::US_ASCII)
+ sub = "ã“ã‚“ã«ã¡ã¯"
+ sub.encoding.should == Encoding::UTF_8
+
+ result = s.bytesplice(1, 2, sub, 3, 3)
+ result.should == "hã‚“lo"
+ result.encoding.should == Encoding::UTF_8
+ end
+
+ it "deals with a different encoded argument with str range" do
+ s = "ã“ã‚“ã«ã¡ã¯"
+ s.encoding.should == Encoding::UTF_8
+ sub = "goodbye"
+ sub.force_encoding(Encoding::US_ASCII)
+
+ result = s.bytesplice(3..5, sub, 0..2)
+ result.should == "ã“gooã«ã¡ã¯"
+ result.encoding.should == Encoding::UTF_8
+
+ s = "hello"
+ s.force_encoding(Encoding::US_ASCII)
+ sub = "ã“ã‚“ã«ã¡ã¯"
+ sub.encoding.should == Encoding::UTF_8
+
+ result = s.bytesplice(1..2, sub, 3..5)
+ result.should == "hã‚“lo"
+ result.encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/string/capitalize_spec.rb b/spec/ruby/core/string/capitalize_spec.rb
new file mode 100644
index 0000000000..12b1675c2e
--- /dev/null
+++ b/spec/ruby/core/string/capitalize_spec.rb
@@ -0,0 +1,207 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#capitalize" do
+ it "returns a copy of self with the first character converted to uppercase and the remainder to lowercase" do
+ "".capitalize.should == ""
+ "h".capitalize.should == "H"
+ "H".capitalize.should == "H"
+ "hello".capitalize.should == "Hello"
+ "HELLO".capitalize.should == "Hello"
+ "123ABC".capitalize.should == "123abc"
+ "abcdef"[1...-1].capitalize.should == "Bcde"
+ end
+
+ describe "full Unicode case mapping" do
+ it "works for all of Unicode with no option" do
+ "äöÜ".capitalize.should == "Äöü"
+ end
+
+ it "only capitalizes the first resulting character when upcasing a character produces a multi-character sequence" do
+ "ß".capitalize.should == "Ss"
+ end
+
+ it "updates string metadata" do
+ capitalized = "ßeT".capitalize
+
+ capitalized.should == "Sset"
+ capitalized.size.should == 4
+ capitalized.bytesize.should == 4
+ capitalized.ascii_only?.should == true
+ end
+ end
+
+ describe "ASCII-only case mapping" do
+ it "does not capitalize non-ASCII characters" do
+ "ßet".capitalize(:ascii).should == "ßet"
+ end
+
+ it "handles non-ASCII substrings properly" do
+ "garçon"[1...-1].capitalize(:ascii).should == "Arço"
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Turkic languages" do
+ it "capitalizes ASCII characters according to Turkic semantics" do
+ "iSa".capitalize(:turkic).should == "İsa"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ "iSa".capitalize(:turkic, :lithuanian).should == "İsa"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "iSa".capitalize(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ "iß".capitalize(:lithuanian).should == "Iß"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ "iß".capitalize(:lithuanian, :turkic).should == "İß"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "iß".capitalize(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not allow the :fold option for upcasing" do
+ -> { "abc".capitalize(:fold) }.should.raise(ArgumentError)
+ end
+
+ it "does not allow invalid options" do
+ -> { "abc".capitalize(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("hello").capitalize.should.instance_of?(String)
+ StringSpecs::MyString.new("Hello").capitalize.should.instance_of?(String)
+ end
+
+ it "returns a String in the same encoding as self" do
+ "h".encode("US-ASCII").capitalize.encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#capitalize!" do
+ it "capitalizes self in place" do
+ a = +"hello"
+ a.capitalize!.should.equal?(a)
+ a.should == "Hello"
+ end
+
+ it "modifies self in place for non-ascii-compatible encodings" do
+ a = "heLLo".encode("utf-16le")
+ a.capitalize!
+ a.should == "Hello".encode("utf-16le")
+ end
+
+ describe "full Unicode case mapping" do
+ it "modifies self in place for all of Unicode with no option" do
+ a = +"äöÜ"
+ a.capitalize!
+ a.should == "Äöü"
+ end
+
+ it "only capitalizes the first resulting character when upcasing a character produces a multi-character sequence" do
+ a = +"ß"
+ a.capitalize!
+ a.should == "Ss"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "äöü".encode("utf-16le")
+ a.capitalize!
+ a.should == "Äöü".encode("utf-16le")
+ end
+
+ it "updates string metadata" do
+ capitalized = +"ßeT"
+ capitalized.capitalize!
+
+ capitalized.should == "Sset"
+ capitalized.size.should == 4
+ capitalized.bytesize.should == 4
+ capitalized.ascii_only?.should == true
+ end
+ end
+
+ describe "modifies self in place for ASCII-only case mapping" do
+ it "does not capitalize non-ASCII characters" do
+ a = +"ßet"
+ a.capitalize!(:ascii)
+ a.should == "ßet"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "aBc".encode("utf-16le")
+ a.capitalize!(:ascii)
+ a.should == "Abc".encode("utf-16le")
+ end
+ end
+
+ describe "modifies self in place for full Unicode case mapping adapted for Turkic languages" do
+ it "capitalizes ASCII characters according to Turkic semantics" do
+ a = +"iSa"
+ a.capitalize!(:turkic)
+ a.should == "İsa"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ a = +"iSa"
+ a.capitalize!(:turkic, :lithuanian)
+ a.should == "İsa"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "iSa"; a.capitalize!(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "modifies self in place for full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ a = +"iß"
+ a.capitalize!(:lithuanian)
+ a.should == "Iß"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ a = +"iß"
+ a.capitalize!(:lithuanian, :turkic)
+ a.should == "İß"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "iß"; a.capitalize!(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not allow the :fold option for upcasing" do
+ -> { a = "abc"; a.capitalize!(:fold) }.should.raise(ArgumentError)
+ end
+
+ it "does not allow invalid options" do
+ -> { a = "abc"; a.capitalize!(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns nil when no changes are made" do
+ a = +"Hello"
+ a.capitalize!.should == nil
+ a.should == "Hello"
+
+ (+"").capitalize!.should == nil
+ (+"H").capitalize!.should == nil
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ ["", "Hello", "hello"].each do |a|
+ a.freeze
+ -> { a.capitalize! }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/case_compare_spec.rb b/spec/ruby/core/string/case_compare_spec.rb
new file mode 100644
index 0000000000..b83d1adb91
--- /dev/null
+++ b/spec/ruby/core/string/case_compare_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+require_relative 'shared/equal_value'
+
+describe "String#===" do
+ it_behaves_like :string_eql_value, :===
+ it_behaves_like :string_equal_value, :===
+end
diff --git a/spec/ruby/core/string/casecmp_spec.rb b/spec/ruby/core/string/casecmp_spec.rb
new file mode 100644
index 0000000000..90577aaac0
--- /dev/null
+++ b/spec/ruby/core/string/casecmp_spec.rb
@@ -0,0 +1,204 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#casecmp independent of case" do
+ it "returns -1 when less than other" do
+ "a".casecmp("b").should == -1
+ "A".casecmp("b").should == -1
+ end
+
+ it "returns 0 when equal to other" do
+ "a".casecmp("a").should == 0
+ "A".casecmp("a").should == 0
+ end
+
+ it "returns 1 when greater than other" do
+ "b".casecmp("a").should == 1
+ "B".casecmp("a").should == 1
+ end
+
+ it "tries to convert other to string using to_str" do
+ other = mock('x')
+ other.should_receive(:to_str).and_return("abc")
+
+ "abc".casecmp(other).should == 0
+ end
+
+ it "returns nil if other can't be converted to a string" do
+ "abc".casecmp(mock('abc')).should == nil
+ end
+
+ it "returns nil if incompatible encodings" do
+ "ã‚れ".casecmp("れ".encode(Encoding::EUC_JP)).should == nil
+ end
+
+ describe "in UTF-8 mode" do
+ describe "for non-ASCII characters" do
+ before :each do
+ @upper_a_tilde = "Ã"
+ @lower_a_tilde = "ã"
+ @upper_a_umlaut = "Ä"
+ @lower_a_umlaut = "ä"
+ end
+
+ it "returns -1 when numerically less than other" do
+ @upper_a_tilde.casecmp(@lower_a_tilde).should == -1
+ @upper_a_tilde.casecmp(@upper_a_umlaut).should == -1
+ end
+
+ it "returns 0 when numerically equal to other" do
+ @upper_a_tilde.casecmp(@upper_a_tilde).should == 0
+ end
+
+ it "returns 1 when numerically greater than other" do
+ @lower_a_umlaut.casecmp(@upper_a_umlaut).should == 1
+ @lower_a_umlaut.casecmp(@lower_a_tilde).should == 1
+ end
+ end
+
+ describe "for ASCII characters" do
+ it "returns -1 when less than other" do
+ "a".casecmp("b").should == -1
+ "A".casecmp("b").should == -1
+ end
+
+ it "returns 0 when equal to other" do
+ "a".casecmp("a").should == 0
+ "A".casecmp("a").should == 0
+ end
+
+ it "returns 1 when greater than other" do
+ "b".casecmp("a").should == 1
+ "B".casecmp("a").should == 1
+ end
+ end
+ end
+
+ describe "for non-ASCII characters" do
+ before :each do
+ @upper_a_tilde = "\xc3"
+ @lower_a_tilde = "\xe3"
+ end
+
+ it "returns -1 when numerically less than other" do
+ @upper_a_tilde.casecmp(@lower_a_tilde).should == -1
+ end
+
+ it "returns 0 when equal to other" do
+ @upper_a_tilde.casecmp("\xc3").should == 0
+ end
+
+ it "returns 1 when numerically greater than other" do
+ @lower_a_tilde.casecmp(@upper_a_tilde).should == 1
+ end
+
+ it "does not case fold" do
+ "ß".casecmp("ss").should == 1
+ end
+ end
+
+ describe "when comparing a subclass instance" do
+ it "returns -1 when less than other" do
+ b = StringSpecs::MyString.new "b"
+ "a".casecmp(b).should == -1
+ "A".casecmp(b).should == -1
+ end
+
+ it "returns 0 when equal to other" do
+ a = StringSpecs::MyString.new "a"
+ "a".casecmp(a).should == 0
+ "A".casecmp(a).should == 0
+ end
+
+ it "returns 1 when greater than other" do
+ a = StringSpecs::MyString.new "a"
+ "b".casecmp(a).should == 1
+ "B".casecmp(a).should == 1
+ end
+ end
+
+ it "returns 0 for empty strings in different encodings" do
+ ''.b.casecmp('').should == 0
+ ''.b.casecmp(''.encode("UTF-32LE")).should == 0
+ end
+end
+
+describe 'String#casecmp? independent of case' do
+ it 'returns true when equal to other' do
+ 'abc'.casecmp?('abc').should == true
+ 'abc'.casecmp?('ABC').should == true
+ end
+
+ it 'returns false when not equal to other' do
+ 'abc'.casecmp?('DEF').should == false
+ 'abc'.casecmp?('def').should == false
+ end
+
+ it "tries to convert other to string using to_str" do
+ other = mock('x')
+ other.should_receive(:to_str).and_return("abc")
+
+ "abc".casecmp?(other).should == true
+ end
+
+ it "returns nil if incompatible encodings" do
+ "ã‚れ".casecmp?("れ".encode(Encoding::EUC_JP)).should == nil
+ end
+
+ describe 'for UNICODE characters' do
+ it 'returns true when downcase(:fold) on unicode' do
+ 'äöü'.casecmp?('ÄÖÜ').should == true
+ end
+ end
+
+ describe "when comparing a subclass instance" do
+ it 'returns true when equal to other' do
+ a = StringSpecs::MyString.new "a"
+ 'a'.casecmp?(a).should == true
+ 'A'.casecmp?(a).should == true
+ end
+
+ it 'returns false when not equal to other' do
+ b = StringSpecs::MyString.new "a"
+ 'b'.casecmp?(b).should == false
+ 'B'.casecmp?(b).should == false
+ end
+ end
+
+ describe "in UTF-8 mode" do
+ describe "for non-ASCII characters" do
+ before :each do
+ @upper_a_tilde = "Ã"
+ @lower_a_tilde = "ã"
+ @upper_a_umlaut = "Ä"
+ @lower_a_umlaut = "ä"
+ end
+
+ it "returns true when they are the same with normalized case" do
+ @upper_a_tilde.casecmp?(@lower_a_tilde).should == true
+ end
+
+ it "returns false when they are unrelated" do
+ @upper_a_tilde.casecmp?(@upper_a_umlaut).should == false
+ end
+
+ it "returns true when they have the same bytes" do
+ @upper_a_tilde.casecmp?(@upper_a_tilde).should == true
+ end
+ end
+ end
+
+ it "case folds" do
+ "ß".casecmp?("ss").should == true
+ end
+
+ it "returns nil if other can't be converted to a string" do
+ "abc".casecmp?(mock('abc')).should == nil
+ end
+
+ it "returns true for empty strings in different encodings" do
+ ''.b.should.casecmp?('')
+ ''.b.should.casecmp?(''.encode("UTF-32LE"))
+ end
+end
diff --git a/spec/ruby/core/string/center_spec.rb b/spec/ruby/core/string/center_spec.rb
new file mode 100644
index 0000000000..ac5b8a2ff3
--- /dev/null
+++ b/spec/ruby/core/string/center_spec.rb
@@ -0,0 +1,117 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#center with length, padding" do
+ it "returns a new string of specified length with self centered and padded with padstr" do
+ "one".center(9, '.').should == "...one..."
+ "hello".center(20, '123').should == "1231231hello12312312"
+ "middle".center(13, '-').should == "---middle----"
+
+ "".center(1, "abcd").should == "a"
+ "".center(2, "abcd").should == "aa"
+ "".center(3, "abcd").should == "aab"
+ "".center(4, "abcd").should == "abab"
+ "".center(6, "xy").should == "xyxxyx"
+ "".center(11, "12345").should == "12345123451"
+
+ "|".center(2, "abcd").should == "|a"
+ "|".center(3, "abcd").should == "a|a"
+ "|".center(4, "abcd").should == "a|ab"
+ "|".center(5, "abcd").should == "ab|ab"
+ "|".center(6, "xy").should == "xy|xyx"
+ "|".center(7, "xy").should == "xyx|xyx"
+ "|".center(11, "12345").should == "12345|12345"
+ "|".center(12, "12345").should == "12345|123451"
+
+ "||".center(3, "abcd").should == "||a"
+ "||".center(4, "abcd").should == "a||a"
+ "||".center(5, "abcd").should == "a||ab"
+ "||".center(6, "abcd").should == "ab||ab"
+ "||".center(8, "xy").should == "xyx||xyx"
+ "||".center(12, "12345").should == "12345||12345"
+ "||".center(13, "12345").should == "12345||123451"
+ end
+
+ it "pads with whitespace if no padstr is given" do
+ "two".center(5).should == " two "
+ "hello".center(20).should == " hello "
+ end
+
+ it "returns self if it's longer than or as long as the specified length" do
+ "".center(0).should == ""
+ "".center(-1).should == ""
+ "hello".center(4).should == "hello"
+ "hello".center(-1).should == "hello"
+ "this".center(3).should == "this"
+ "radiology".center(8, '-').should == "radiology"
+ end
+
+ it "calls #to_int to convert length to an integer" do
+ "_".center(3.8, "^").should == "^_^"
+
+ obj = mock('3')
+ obj.should_receive(:to_int).and_return(3)
+
+ "_".center(obj, "o").should == "o_o"
+ end
+
+ it "raises a TypeError when length can't be converted to an integer" do
+ -> { "hello".center("x") }.should.raise(TypeError)
+ -> { "hello".center("x", "y") }.should.raise(TypeError)
+ -> { "hello".center([]) }.should.raise(TypeError)
+ -> { "hello".center(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "calls #to_str to convert padstr to a String" do
+ padstr = mock('123')
+ padstr.should_receive(:to_str).and_return("123")
+
+ "hello".center(20, padstr).should == "1231231hello12312312"
+ end
+
+ it "raises a TypeError when padstr can't be converted to a string" do
+ -> { "hello".center(20, 100) }.should.raise(TypeError)
+ -> { "hello".center(20, []) }.should.raise(TypeError)
+ -> { "hello".center(20, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if padstr is empty" do
+ -> { "hello".center(10, "") }.should.raise(ArgumentError)
+ -> { "hello".center(0, "") }.should.raise(ArgumentError)
+ end
+
+ it "returns String instances when called on subclasses" do
+ StringSpecs::MyString.new("").center(10).should.instance_of?(String)
+ StringSpecs::MyString.new("foo").center(10).should.instance_of?(String)
+ StringSpecs::MyString.new("foo").center(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+
+ "".center(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+ "foo".center(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+ end
+
+ describe "with width" do
+ it "returns a String in the same encoding as the original" do
+ str = "abc".dup.force_encoding Encoding::IBM437
+ result = str.center 6
+ result.should == " abc "
+ result.encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "with width, pattern" do
+ it "returns a String in the compatible encoding" do
+ str = "abc".dup.force_encoding Encoding::IBM437
+ result = str.center 6, "ã‚"
+ result.should == "ã‚abcã‚ã‚"
+ result.encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ pat = "ã‚¢".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".center 5, pat
+ end.should.raise(Encoding::CompatibilityError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/chars_spec.rb b/spec/ruby/core/string/chars_spec.rb
new file mode 100644
index 0000000000..ee85430574
--- /dev/null
+++ b/spec/ruby/core/string/chars_spec.rb
@@ -0,0 +1,16 @@
+require_relative "../../spec_helper"
+require_relative 'shared/chars'
+
+describe "String#chars" do
+ it_behaves_like :string_chars, :chars
+
+ it "returns an array when no block given" do
+ "hello".chars.should == ['h', 'e', 'l', 'l', 'o']
+ end
+
+ it "returns Strings in the same encoding as self" do
+ "hello".encode("US-ASCII").chars.each do |c|
+ c.encoding.should == Encoding::US_ASCII
+ end
+ end
+end
diff --git a/spec/ruby/core/string/chilled_string_spec.rb b/spec/ruby/core/string/chilled_string_spec.rb
new file mode 100644
index 0000000000..6a5e846db2
--- /dev/null
+++ b/spec/ruby/core/string/chilled_string_spec.rb
@@ -0,0 +1,151 @@
+require_relative '../../spec_helper'
+
+describe "chilled String" do
+ guard -> { ruby_version_is "3.4" and !"test".equal?("test") } do
+ describe "chilled string literals" do
+
+ describe "#frozen?" do
+ it "returns false" do
+ "chilled".frozen?.should == false
+ end
+ end
+
+ describe "#-@" do
+ it "returns a different instance" do
+ input = "chilled"
+ interned = (-input)
+ interned.frozen?.should == true
+ interned.object_id.should_not == input.object_id
+ end
+ end
+
+ describe "#+@" do
+ it "returns a different instance" do
+ input = "chilled"
+ duped = (+input)
+ duped.frozen?.should == false
+ duped.object_id.should_not == input.object_id
+ end
+ end
+
+ describe "#clone" do
+ it "preserves chilled status" do
+ input = "chilled".clone
+ -> {
+ input << "-mutated"
+ }.should complain(/literal string will be frozen in the future/)
+ input.should == "chilled-mutated"
+ end
+ end
+
+ describe "mutation" do
+ it "emits a warning" do
+ input = "chilled"
+ -> {
+ input << "-mutated"
+ }.should complain(/literal string will be frozen in the future/)
+ input.should == "chilled-mutated"
+ end
+
+ it "emits a warning for concatenated strings" do
+ input = "still" "+chilled"
+ -> {
+ input << "-mutated"
+ }.should complain(/literal string will be frozen in the future/)
+ input.should == "still+chilled-mutated"
+ end
+
+ it "emits a warning on singleton_class creation" do
+ -> {
+ "chilled".singleton_class
+ }.should complain(/literal string will be frozen in the future/)
+ end
+
+ it "emits a warning on instance variable assignment" do
+ -> {
+ "chilled".instance_variable_set(:@ivar, 42)
+ }.should complain(/literal string will be frozen in the future/)
+ end
+
+ it "raises FrozenError after the string was explicitly frozen" do
+ input = "chilled"
+ input.freeze
+ -> {
+ -> {
+ input << "mutated"
+ }.should.raise(FrozenError)
+ }.should_not complain(/literal string will be frozen in the future/)
+ end
+ end
+ end
+
+ describe "chilled strings returned by Symbol#to_s" do
+
+ describe "#frozen?" do
+ it "returns false" do
+ :chilled.to_s.frozen?.should == false
+ end
+ end
+
+ describe "#-@" do
+ it "returns a different instance" do
+ input = :chilled.to_s
+ interned = (-input)
+ interned.frozen?.should == true
+ interned.object_id.should_not == input.object_id
+ end
+ end
+
+ describe "#+@" do
+ it "returns a different instance" do
+ input = :chilled.to_s
+ duped = (+input)
+ duped.frozen?.should == false
+ duped.object_id.should_not == input.object_id
+ end
+ end
+
+ describe "#clone" do
+ it "preserves chilled status" do
+ input = :chilled.to_s.clone
+ -> {
+ input << "-mutated"
+ }.should complain(/string returned by :chilled\.to_s will be frozen in the future/)
+ input.should == "chilled-mutated"
+ end
+ end
+
+ describe "mutation" do
+ it "emits a warning" do
+ input = :chilled.to_s
+ -> {
+ input << "-mutated"
+ }.should complain(/string returned by :chilled\.to_s will be frozen in the future/)
+ input.should == "chilled-mutated"
+ end
+
+ it "emits a warning on singleton_class creation" do
+ -> {
+ :chilled.to_s.singleton_class
+ }.should complain(/string returned by :chilled\.to_s will be frozen in the future/)
+ end
+
+ it "emits a warning on instance variable assignment" do
+ -> {
+ :chilled.to_s.instance_variable_set(:@ivar, 42)
+ }.should complain(/string returned by :chilled\.to_s will be frozen in the future/)
+ end
+
+ it "raises FrozenError after the string was explicitly frozen" do
+ input = :chilled.to_s
+ input.freeze
+ -> {
+ -> {
+ input << "mutated"
+ }.should.raise(FrozenError)
+ }.should_not complain(/string returned by :chilled\.to_s will be frozen in the future/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/chomp_spec.rb b/spec/ruby/core/string/chomp_spec.rb
new file mode 100644
index 0000000000..3a8550892f
--- /dev/null
+++ b/spec/ruby/core/string/chomp_spec.rb
@@ -0,0 +1,366 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#chomp" do
+ describe "when passed no argument" do
+ before do
+ # Ensure that $/ is set to the default value
+ @verbose, $VERBOSE = $VERBOSE, nil
+ @dollar_slash, $/ = $/, "\n"
+ end
+
+ after do
+ $/ = @dollar_slash
+ $VERBOSE = @verbose
+ end
+
+ it "does not modify a String with no trailing carriage return or newline" do
+ "abc".chomp.should == "abc"
+ end
+
+ it "returns a copy of the String when it is not modified" do
+ str = "abc"
+ str.chomp.should_not.equal?(str)
+ end
+
+ it "removes one trailing newline" do
+ "abc\n\n".chomp.should == "abc\n"
+ end
+
+ it "removes one trailing carriage return" do
+ "abc\r\r".chomp.should == "abc\r"
+ end
+
+ it "removes one trailing carriage return, newline pair" do
+ "abc\r\n\r\n".chomp.should == "abc\r\n"
+ end
+
+ it "returns an empty String when self is empty" do
+ "".chomp.should == ""
+ end
+
+ it "returns a String in the same encoding as self" do
+ "abc\n\n".encode("US-ASCII").chomp.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns String instances when called on a subclass" do
+ str = StringSpecs::MyString.new("hello\n").chomp
+ str.should.instance_of?(String)
+ end
+
+ it "removes trailing characters that match $/ when it has been assigned a value" do
+ $/ = "cdef"
+ "abcdef".chomp.should == "ab"
+ end
+
+ it "removes one trailing newline for string with invalid encoding" do
+ "\xa0\xa1\n".chomp.should == "\xa0\xa1"
+ end
+ end
+
+ describe "when passed nil" do
+ it "does not modify the String" do
+ "abc\r\n".chomp(nil).should == "abc\r\n"
+ end
+
+ it "returns a copy of the String" do
+ str = "abc"
+ str.chomp(nil).should_not.equal?(str)
+ end
+
+ it "returns an empty String when self is empty" do
+ "".chomp(nil).should == ""
+ end
+ end
+
+ describe "when passed ''" do
+ it "removes a final newline" do
+ "abc\n".chomp("").should == "abc"
+ end
+
+ it "removes a final carriage return, newline" do
+ "abc\r\n".chomp("").should == "abc"
+ end
+
+ it "does not remove a final carriage return" do
+ "abc\r".chomp("").should == "abc\r"
+ end
+
+ it "removes more than one trailing newlines" do
+ "abc\n\n\n".chomp("").should == "abc"
+ end
+
+ it "removes more than one trailing carriage return, newline pairs" do
+ "abc\r\n\r\n\r\n".chomp("").should == "abc"
+ end
+
+ it "returns an empty String when self is empty" do
+ "".chomp("").should == ""
+ end
+
+ it "removes one trailing newline for string with invalid encoding" do
+ "\xa0\xa1\n".chomp("").should == "\xa0\xa1"
+ end
+ end
+
+ describe "when passed '\\n'" do
+ it "removes one trailing newline" do
+ "abc\n\n".chomp("\n").should == "abc\n"
+ end
+
+ it "removes one trailing carriage return" do
+ "abc\r\r".chomp("\n").should == "abc\r"
+ end
+
+ it "removes one trailing carriage return, newline pair" do
+ "abc\r\n\r\n".chomp("\n").should == "abc\r\n"
+ end
+
+ it "returns an empty String when self is empty" do
+ "".chomp("\n").should == ""
+ end
+ end
+
+ describe "when passed an Object" do
+ it "calls #to_str to convert to a String" do
+ arg = mock("string chomp")
+ arg.should_receive(:to_str).and_return("bc")
+ "abc".chomp(arg).should == "a"
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ arg = mock("string chomp")
+ arg.should_receive(:to_str).and_return(1)
+ -> { "abc".chomp(arg) }.should.raise(TypeError)
+ end
+ end
+
+ describe "when passed a String" do
+ it "removes the trailing characters if they match the argument" do
+ "abcabc".chomp("abc").should == "abc"
+ end
+
+ it "does not modify the String if the argument does not match the trailing characters" do
+ "abc".chomp("def").should == "abc"
+ end
+
+ it "returns an empty String when self is empty" do
+ "".chomp("abc").should == ""
+ end
+
+ it "returns an empty String when the argument equals self" do
+ "abc".chomp("abc").should == ""
+ end
+ end
+end
+
+describe "String#chomp!" do
+ describe "when passed no argument" do
+ before do
+ # Ensure that $/ is set to the default value
+ @verbose, $VERBOSE = $VERBOSE, nil
+ @dollar_slash, $/ = $/, "\n"
+ end
+
+ after do
+ $/ = @dollar_slash
+ $VERBOSE = @verbose
+ end
+
+ it "modifies self" do
+ str = "abc\n"
+ str.chomp!.should.equal?(str)
+ end
+
+ it "returns nil if self is not modified" do
+ "abc".chomp!.should == nil
+ end
+
+ it "removes one trailing newline" do
+ "abc\n\n".chomp!.should == "abc\n"
+ end
+
+ it "removes one trailing carriage return" do
+ "abc\r\r".chomp!.should == "abc\r"
+ end
+
+ it "removes one trailing carriage return, newline pair" do
+ "abc\r\n\r\n".chomp!.should == "abc\r\n"
+ end
+
+ it "returns nil when self is empty" do
+ "".chomp!.should == nil
+ end
+
+ it "returns subclass instances when called on a subclass" do
+ str = StringSpecs::MyString.new("hello\n").chomp!
+ str.should.instance_of?(StringSpecs::MyString)
+ end
+
+ it "removes trailing characters that match $/ when it has been assigned a value" do
+ $/ = "cdef"
+ "abcdef".chomp!.should == "ab"
+ end
+ end
+
+ describe "when passed nil" do
+ it "returns nil" do
+ "abc\r\n".chomp!(nil).should == nil
+ end
+
+ it "returns nil when self is empty" do
+ "".chomp!(nil).should == nil
+ end
+ end
+
+ describe "when passed ''" do
+ it "removes a final newline" do
+ "abc\n".chomp!("").should == "abc"
+ end
+
+ it "removes a final carriage return, newline" do
+ "abc\r\n".chomp!("").should == "abc"
+ end
+
+ it "does not remove a final carriage return" do
+ "abc\r".chomp!("").should == nil
+ end
+
+ it "removes more than one trailing newlines" do
+ "abc\n\n\n".chomp!("").should == "abc"
+ end
+
+ it "removes more than one trailing carriage return, newline pairs" do
+ "abc\r\n\r\n\r\n".chomp!("").should == "abc"
+ end
+
+ it "returns nil when self is empty" do
+ "".chomp!("").should == nil
+ end
+ end
+
+ describe "when passed '\\n'" do
+ it "removes one trailing newline" do
+ "abc\n\n".chomp!("\n").should == "abc\n"
+ end
+
+ it "removes one trailing carriage return" do
+ "abc\r\r".chomp!("\n").should == "abc\r"
+ end
+
+ it "removes one trailing carriage return, newline pair" do
+ "abc\r\n\r\n".chomp!("\n").should == "abc\r\n"
+ end
+
+ it "returns nil when self is empty" do
+ "".chomp!("\n").should == nil
+ end
+ end
+
+ describe "when passed an Object" do
+ it "calls #to_str to convert to a String" do
+ arg = mock("string chomp")
+ arg.should_receive(:to_str).and_return("bc")
+ "abc".chomp!(arg).should == "a"
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ arg = mock("string chomp")
+ arg.should_receive(:to_str).and_return(1)
+ -> { "abc".chomp!(arg) }.should.raise(TypeError)
+ end
+ end
+
+ describe "when passed a String" do
+ it "removes the trailing characters if they match the argument" do
+ "abcabc".chomp!("abc").should == "abc"
+ end
+
+ it "returns nil if the argument does not match the trailing characters" do
+ "abc".chomp!("def").should == nil
+ end
+
+ it "returns nil when self is empty" do
+ "".chomp!("abc").should == nil
+ end
+ end
+
+ it "raises a FrozenError on a frozen instance when it is modified" do
+ a = "string\n\r"
+ a.freeze
+
+ -> { a.chomp! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen instance when it would not be modified" do
+ a = "string\n\r"
+ a.freeze
+ -> { a.chomp!(nil) }.should.raise(FrozenError)
+ -> { a.chomp!("x") }.should.raise(FrozenError)
+ end
+end
+
+describe "String#chomp" do
+ before :each do
+ @verbose, $VERBOSE = $VERBOSE, nil
+ @before_separator = $/
+ end
+
+ after :each do
+ $/ = @before_separator
+ $VERBOSE = @verbose
+ end
+
+ it "does not modify a multi-byte character" do
+ "ã‚れ".chomp.should == "ã‚れ"
+ end
+
+ it "removes the final carriage return, newline from a multibyte String" do
+ "ã‚れ\r\n".chomp.should == "ã‚れ"
+ end
+
+ it "removes the final carriage return, newline from a non-ASCII String" do
+ str = "abc\r\n".encode "utf-32be"
+ str.chomp.should == "abc".encode("utf-32be")
+ end
+
+ it "removes the final carriage return, newline from a non-ASCII String when the record separator is changed" do
+ $/ = "\n".encode("utf-8")
+ str = "abc\r\n".encode "utf-32be"
+ str.chomp.should == "abc".encode("utf-32be")
+ end
+end
+
+describe "String#chomp!" do
+ before :each do
+ @verbose, $VERBOSE = $VERBOSE, nil
+ @before_separator = $/
+ end
+
+ after :each do
+ $/ = @before_separator
+ $VERBOSE = @verbose
+ end
+
+ it "returns nil when the String is not modified" do
+ "ã‚れ".chomp!.should == nil
+ end
+
+ it "removes the final carriage return, newline from a multibyte String" do
+ "ã‚れ\r\n".chomp!.should == "ã‚れ"
+ end
+
+ it "removes the final carriage return, newline from a non-ASCII String" do
+ str = "abc\r\n".encode "utf-32be"
+ str.chomp!.should == "abc".encode("utf-32be")
+ end
+
+ it "removes the final carriage return, newline from a non-ASCII String when the record separator is changed" do
+ $/ = "\n".encode("utf-8")
+ str = "abc\r\n".encode "utf-32be"
+ str.chomp!.should == "abc".encode("utf-32be")
+ end
+end
diff --git a/spec/ruby/core/string/chop_spec.rb b/spec/ruby/core/string/chop_spec.rb
new file mode 100644
index 0000000000..2113da543b
--- /dev/null
+++ b/spec/ruby/core/string/chop_spec.rb
@@ -0,0 +1,119 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#chop" do
+ it "removes the final character" do
+ "abc".chop.should == "ab"
+ end
+
+ it "removes the final carriage return" do
+ "abc\r".chop.should == "abc"
+ end
+
+ it "removes the final newline" do
+ "abc\n".chop.should == "abc"
+ end
+
+ it "removes the final carriage return, newline" do
+ "abc\r\n".chop.should == "abc"
+ end
+
+ it "removes the carriage return, newline if they are the only characters" do
+ "\r\n".chop.should == ""
+ end
+
+ it "does not remove more than the final carriage return, newline" do
+ "abc\r\n\r\n".chop.should == "abc\r\n"
+ end
+
+ it "removes a multi-byte character" do
+ "ã‚れ".chop.should == "ã‚"
+ end
+
+ it "removes the final carriage return, newline from a multibyte String" do
+ "ã‚れ\r\n".chop.should == "ã‚れ"
+ end
+
+ it "removes the final carriage return, newline from a non-ASCII String" do
+ str = "abc\r\n".encode "utf-32be"
+ str.chop.should == "abc".encode("utf-32be")
+ end
+
+ it "returns an empty string when applied to an empty string" do
+ "".chop.should == ""
+ end
+
+ it "returns a new string when applied to an empty string" do
+ s = ""
+ s.chop.should_not.equal?(s)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("hello\n").chop.should.instance_of?(String)
+ end
+
+ it "returns a String in the same encoding as self" do
+ "abc\n\n".encode("US-ASCII").chop.encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#chop!" do
+ it "removes the final character" do
+ "abc".chop!.should == "ab"
+ end
+
+ it "removes the final carriage return" do
+ "abc\r".chop!.should == "abc"
+ end
+
+ it "removes the final newline" do
+ "abc\n".chop!.should == "abc"
+ end
+
+ it "removes the final carriage return, newline" do
+ "abc\r\n".chop!.should == "abc"
+ end
+
+ it "removes the carriage return, newline if they are the only characters" do
+ "\r\n".chop!.should == ""
+ end
+
+ it "does not remove more than the final carriage return, newline" do
+ "abc\r\n\r\n".chop!.should == "abc\r\n"
+ end
+
+ it "removes a multi-byte character" do
+ "ã‚れ".chop!.should == "ã‚"
+ end
+
+ it "removes the final carriage return, newline from a multibyte String" do
+ "ã‚れ\r\n".chop!.should == "ã‚れ"
+ end
+
+ it "removes the final carriage return, newline from a non-ASCII String" do
+ str = "abc\r\n".encode "utf-32be"
+ str.chop!.should == "abc".encode("utf-32be")
+ end
+
+ it "returns self if modifications were made" do
+ str = "hello"
+ str.chop!.should.equal?(str)
+ end
+
+ it "returns nil when called on an empty string" do
+ "".chop!.should == nil
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { "string\n\r".freeze.chop! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ a = ""
+ a.freeze
+ -> { a.chop! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/chr_spec.rb b/spec/ruby/core/string/chr_spec.rb
new file mode 100644
index 0000000000..26c7d51202
--- /dev/null
+++ b/spec/ruby/core/string/chr_spec.rb
@@ -0,0 +1,42 @@
+require_relative '../../spec_helper'
+
+describe "String#chr" do
+ it "returns a copy of self" do
+ s = 'e'
+ s.should_not.equal? s.chr
+ end
+
+ it "returns a String" do
+ 'glark'.chr.should.instance_of?(String)
+ end
+
+ it "returns an empty String if self is an empty String" do
+ "".chr.should == ""
+ end
+
+ it "returns a 1-character String" do
+ "glark".chr.size.should == 1
+ end
+
+ it "returns the character at the start of the String" do
+ "Goodbye, world".chr.should == "G"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "\x24".encode(Encoding::US_ASCII).chr.encoding.should == Encoding::US_ASCII
+ end
+
+ it "understands multi-byte characters" do
+ s = "\u{9879}"
+ s.bytesize.should == 3
+ s.chr.should == s
+ end
+
+ it "understands Strings that contain a mixture of character widths" do
+ three = "\u{8082}"
+ three.bytesize.should == 3
+ four = "\u{77082}"
+ four.bytesize.should == 4
+ "#{three}#{four}".chr.should == three
+ end
+end
diff --git a/spec/ruby/core/string/clear_spec.rb b/spec/ruby/core/string/clear_spec.rb
new file mode 100644
index 0000000000..d4688d3689
--- /dev/null
+++ b/spec/ruby/core/string/clear_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#clear" do
+ before :each do
+ @s = "Jolene"
+ end
+
+ it "sets self equal to the empty String" do
+ @s.clear
+ @s.should == ""
+ end
+
+ it "returns self after emptying it" do
+ cleared = @s.clear
+ cleared.should == ""
+ cleared.should.equal? @s
+ end
+
+ it "preserves its encoding" do
+ @s.encode!(Encoding::SHIFT_JIS)
+ @s.encoding.should == Encoding::SHIFT_JIS
+ @s.clear.encoding.should == Encoding::SHIFT_JIS
+ @s.encoding.should == Encoding::SHIFT_JIS
+ end
+
+ it "works with multibyte Strings" do
+ s = "\u{9765}\u{876}"
+ s.clear
+ s.should == ""
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ @s.freeze
+ -> { @s.clear }.should.raise(FrozenError)
+ -> { "".freeze.clear }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/clone_spec.rb b/spec/ruby/core/string/clone_spec.rb
new file mode 100644
index 0000000000..2cb289d2c1
--- /dev/null
+++ b/spec/ruby/core/string/clone_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#clone" do
+ before :each do
+ ScratchPad.clear
+ @obj = StringSpecs::InitializeString.new "string"
+ end
+
+ it "calls #initialize_copy on the new instance" do
+ clone = @obj.clone
+ ScratchPad.recorded.should_not == @obj.object_id
+ ScratchPad.recorded.should == clone.object_id
+ end
+
+ it "copies instance variables" do
+ clone = @obj.clone
+ clone.ivar.should == 1
+ end
+
+ it "copies singleton methods" do
+ def @obj.special() :the_one end
+ clone = @obj.clone
+ clone.special.should == :the_one
+ end
+
+ it "copies modules included in the singleton class" do
+ class << @obj
+ include StringSpecs::StringModule
+ end
+
+ clone = @obj.clone
+ clone.repr.should == 1
+ end
+
+ it "copies constants defined in the singleton class" do
+ class << @obj
+ CLONE = :clone
+ end
+
+ clone = @obj.clone
+ (class << clone; CLONE; end).should == :clone
+ end
+
+ it "copies frozen state" do
+ @obj.freeze.clone.frozen?.should == true
+ "".freeze.clone.frozen?.should == true
+ end
+
+ it "does not modify the original string when changing cloned string" do
+ orig = "string"[0..100]
+ clone = orig.clone
+ orig[0] = 'x'
+ orig.should == "xtring"
+ clone.should == "string"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "a".encode("US-ASCII").clone.encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/string/codepoints_spec.rb b/spec/ruby/core/string/codepoints_spec.rb
new file mode 100644
index 0000000000..51bd57d127
--- /dev/null
+++ b/spec/ruby/core/string/codepoints_spec.rb
@@ -0,0 +1,18 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'shared/codepoints'
+require_relative 'shared/each_codepoint_without_block'
+
+describe "String#codepoints" do
+ it_behaves_like :string_codepoints, :codepoints
+
+ it "returns an Array when no block is given" do
+ "abc".codepoints.should == [?a.ord, ?b.ord, ?c.ord]
+ end
+
+ it "raises an ArgumentError when no block is given if self has an invalid encoding" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.codepoints }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/comparison_spec.rb b/spec/ruby/core/string/comparison_spec.rb
new file mode 100644
index 0000000000..0737c131bb
--- /dev/null
+++ b/spec/ruby/core/string/comparison_spec.rb
@@ -0,0 +1,112 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#<=> with String" do
+ it "compares individual characters based on their ascii value" do
+ ascii_order = Array.new(256) { |x| x.chr }
+ sort_order = ascii_order.sort
+ sort_order.should == ascii_order
+ end
+
+ it "returns -1 when self is less than other" do
+ ("this" <=> "those").should == -1
+ end
+
+ it "returns 0 when self is equal to other" do
+ ("yep" <=> "yep").should == 0
+ end
+
+ it "returns 1 when self is greater than other" do
+ ("yoddle" <=> "griddle").should == 1
+ end
+
+ it "considers string that comes lexicographically first to be less if strings have same size" do
+ ("aba" <=> "abc").should == -1
+ ("abc" <=> "aba").should == 1
+ end
+
+ it "doesn't consider shorter string to be less if longer string starts with shorter one" do
+ ("abc" <=> "abcd").should == -1
+ ("abcd" <=> "abc").should == 1
+ end
+
+ it "compares shorter string with corresponding number of first chars of longer string" do
+ ("abx" <=> "abcd").should == 1
+ ("abcd" <=> "abx").should == -1
+ end
+
+ it "ignores subclass differences" do
+ a = "hello"
+ b = StringSpecs::MyString.new("hello")
+
+ (a <=> b).should == 0
+ (b <=> a).should == 0
+ end
+
+ it "returns 0 if self and other are bytewise identical and have the same encoding" do
+ ("ÄÖÜ" <=> "ÄÖÜ").should == 0
+ end
+
+ it "returns 0 if self and other are bytewise identical and have the same encoding" do
+ ("ÄÖÜ" <=> "ÄÖÜ").should == 0
+ end
+
+ it "returns -1 if self is bytewise less than other" do
+ ("ÄÖÛ" <=> "ÄÖÜ").should == -1
+ end
+
+ it "returns 1 if self is bytewise greater than other" do
+ ("ÄÖÜ" <=> "ÄÖÛ").should == 1
+ end
+
+ it "ignores encoding difference" do
+ ("ÄÖÛ".dup.force_encoding("utf-8") <=> "ÄÖÜ".dup.force_encoding("iso-8859-1")).should == -1
+ ("ÄÖÜ".dup.force_encoding("utf-8") <=> "ÄÖÛ".dup.force_encoding("iso-8859-1")).should == 1
+ end
+
+ it "returns 0 with identical ASCII-compatible bytes of different encodings" do
+ ("abc".dup.force_encoding("utf-8") <=> "abc".dup.force_encoding("iso-8859-1")).should == 0
+ end
+
+ it "compares the indices of the encodings when the strings have identical non-ASCII-compatible bytes" do
+ xff_1 = [0xFF].pack('C').force_encoding("utf-8")
+ xff_2 = [0xFF].pack('C').force_encoding("iso-8859-1")
+ (xff_1 <=> xff_2).should == -1
+ (xff_2 <=> xff_1).should == 1
+ end
+
+ it "returns 0 when comparing 2 empty strings but one is not ASCII-compatible" do
+ ("" <=> "".dup.force_encoding('iso-2022-jp')).should == 0
+ end
+end
+
+# Note: This is inconsistent with Array#<=> which calls #to_ary instead of
+# just using it as an indicator.
+describe "String#<=>" do
+ it "returns nil if its argument provides neither #to_str nor #<=>" do
+ ("abc" <=> mock('x')).should == nil
+ end
+
+ it "uses the result of calling #to_str for comparison when #to_str is defined" do
+ obj = mock('x')
+ obj.should_receive(:to_str).and_return("aaa")
+
+ ("abc" <=> obj).should == 1
+ end
+
+ it "uses the result of calling #<=> on its argument when #<=> is defined but #to_str is not" do
+ obj = mock('x')
+ obj.should_receive(:<=>).and_return(-1)
+
+ ("abc" <=> obj).should == 1
+ end
+
+ it "returns nil if argument also uses an inverse comparison for <=>" do
+ obj = mock('x')
+ def obj.<=>(other); other <=> self; end
+ obj.should_receive(:<=>).once
+
+ ("abc" <=> obj).should == nil
+ end
+end
diff --git a/spec/ruby/core/string/concat_spec.rb b/spec/ruby/core/string/concat_spec.rb
new file mode 100644
index 0000000000..0194a357a0
--- /dev/null
+++ b/spec/ruby/core/string/concat_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/concat'
+
+describe "String#concat" do
+ it_behaves_like :string_concat, :concat
+ it_behaves_like :string_concat_encoding, :concat
+ it_behaves_like :string_concat_type_coercion, :concat
+
+ it "takes multiple arguments" do
+ str = +"hello "
+ str.concat "wo", "", "rld"
+ str.should == "hello world"
+ end
+
+ it "concatenates the initial value when given arguments contain 2 self" do
+ str = +"hello"
+ str.concat str, str
+ str.should == "hellohellohello"
+ end
+
+ it "returns self when given no arguments" do
+ str = +"hello"
+ str.concat.should.equal?(str)
+ str.should == "hello"
+ end
+end
diff --git a/spec/ruby/core/string/count_spec.rb b/spec/ruby/core/string/count_spec.rb
new file mode 100644
index 0000000000..fd127c6ff2
--- /dev/null
+++ b/spec/ruby/core/string/count_spec.rb
@@ -0,0 +1,105 @@
+# encoding: binary
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#count" do
+ it "counts occurrences of chars from the intersection of the specified sets" do
+ s = "hello\nworld\x00\x00"
+
+ s.count(s).should == s.size
+ s.count("lo").should == 5
+ s.count("eo").should == 3
+ s.count("l").should == 3
+ s.count("\n").should == 1
+ s.count("\x00").should == 2
+
+ s.count("").should == 0
+ "".count("").should == 0
+
+ s.count("l", "lo").should == s.count("l")
+ s.count("l", "lo", "o").should == s.count("")
+ s.count("helo", "hel", "h").should == s.count("h")
+ s.count("helo", "", "x").should == 0
+ end
+
+ it "raises an ArgumentError when given no arguments" do
+ -> { "hell yeah".count }.should.raise(ArgumentError)
+ end
+
+ it "negates sets starting with ^" do
+ s = "^hello\nworld\x00\x00"
+
+ s.count("^").should == 1 # no negation, counts ^
+
+ s.count("^leh").should == 9
+ s.count("^o").should == 12
+
+ s.count("helo", "^el").should == s.count("ho")
+ s.count("aeiou", "^e").should == s.count("aiou")
+
+ "^_^".count("^^").should == 1
+ "oa^_^o".count("a^").should == 3
+ end
+
+ it "counts all chars in a sequence" do
+ s = "hel-[()]-lo012^"
+
+ s.count("\x00-\xFF").should == s.size
+ s.count("ej-m").should == 3
+ s.count("e-h").should == 2
+
+ # no sequences
+ s.count("-").should == 2
+ s.count("e-").should == s.count("e") + s.count("-")
+ s.count("-h").should == s.count("h") + s.count("-")
+
+ s.count("---").should == s.count("-")
+
+ # see an ASCII table for reference
+ s.count("--2").should == s.count("-./012")
+ s.count("(--").should == s.count("()*+,-")
+ s.count("A-a").should == s.count("A-Z[\\]^_`a")
+
+ # negated sequences
+ s.count("^e-h").should == s.size - s.count("e-h")
+ s.count("^^-^").should == s.size - s.count("^")
+ s.count("^---").should == s.size - s.count("-")
+
+ "abcdefgh".count("a-ce-fh").should == 6
+ "abcdefgh".count("he-fa-c").should == 6
+ "abcdefgh".count("e-fha-c").should == 6
+
+ "abcde".count("ac-e").should == 4
+ "abcde".count("^ac-e").should == 1
+ end
+
+ it "raises if the given sequences are invalid" do
+ s = "hel-[()]-lo012^"
+
+ -> { s.count("h-e") }.should.raise(ArgumentError)
+ -> { s.count("^h-e") }.should.raise(ArgumentError)
+ end
+
+ it 'returns the number of occurrences of a multi-byte character' do
+ str = "\u{2605}"
+ str.count(str).should == 1
+ "asd#{str}zzz#{str}ggg".count(str).should == 2
+ end
+
+ it "calls #to_str to convert each set arg to a String" do
+ other_string = mock('lo')
+ other_string.should_receive(:to_str).and_return("lo")
+
+ other_string2 = mock('o')
+ other_string2.should_receive(:to_str).and_return("o")
+
+ s = "hello world"
+ s.count(other_string, other_string2).should == s.count("o")
+ end
+
+ it "raises a TypeError when a set arg can't be converted to a string" do
+ -> { "hello world".count(100) }.should.raise(TypeError)
+ -> { "hello world".count([]) }.should.raise(TypeError)
+ -> { "hello world".count(mock('x')) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/string/crypt_spec.rb b/spec/ruby/core/string/crypt_spec.rb
new file mode 100644
index 0000000000..924b4d48cf
--- /dev/null
+++ b/spec/ruby/core/string/crypt_spec.rb
@@ -0,0 +1,92 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#crypt" do
+ platform_is :openbsd do
+ it "returns a cryptographic hash of self by applying the bcrypt algorithm with the specified salt" do
+ "mypassword".crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWuBQGbkjzgtSc3gJbmdy0GAGMa45MFM2."
+
+ # Only uses first 72 characters of string
+ ("12345678"*9).crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWukj/ORBnsMjCGpST/zCJnAypc7eAbutK"
+ ("12345678"*10).crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWukj/ORBnsMjCGpST/zCJnAypc7eAbutK"
+
+ # Only uses first 29 characters of salt
+ "mypassword".crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWuB").should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWuBQGbkjzgtSc3gJbmdy0GAGMa45MFM2."
+ end
+
+ it "raises Errno::EINVAL when the salt is shorter than 29 characters" do
+ -> { "mypassword".crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHW") }.should.raise(Errno::EINVAL)
+ end
+
+ it "calls #to_str to converts the salt arg to a String" do
+ obj = mock('$2a$04$0WVaz0pV3jzfZ5G5tpmHWu')
+ obj.should_receive(:to_str).and_return("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu")
+
+ "mypassword".crypt(obj).should == "$2a$04$0WVaz0pV3jzfZ5G5tpmHWuBQGbkjzgtSc3gJbmdy0GAGMa45MFM2."
+ end
+
+ it "doesn't return subclass instances" do
+ StringSpecs::MyString.new("mypassword").crypt("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu").should.instance_of?(String)
+ "mypassword".crypt(StringSpecs::MyString.new("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu")).should.instance_of?(String)
+ StringSpecs::MyString.new("mypassword").crypt(StringSpecs::MyString.new("$2a$04$0WVaz0pV3jzfZ5G5tpmHWu")).should.instance_of?(String)
+ end
+ end
+
+ platform_is_not :openbsd do
+ # Note: MRI's documentation just says that the C stdlib function crypt() is
+ # called.
+ #
+ # I'm not sure if crypt() is guaranteed to produce the same result across
+ # different platforms. It seems that there is one standard UNIX implementation
+ # of crypt(), but that alternative implementations are possible. See
+ # http://www.unix.org.ua/orelly/networking/puis/ch08_06.htm
+ it "returns a cryptographic hash of self by applying the UNIX crypt algorithm with the specified salt" do
+ "".crypt("aa").should == "aaQSqAReePlq6"
+ "nutmeg".crypt("Mi").should == "MiqkFWCm1fNJI"
+ "ellen1".crypt("ri").should == "ri79kNd7V6.Sk"
+ "Sharon".crypt("./").should == "./UY9Q7TvYJDg"
+ "norahs".crypt("am").should == "amfIADT2iqjA."
+ "norahs".crypt("7a").should == "7azfT5tIdyh0I"
+
+ # Only uses first 8 chars of string
+ "01234567".crypt("aa").should == "aa4c4gpuvCkSE"
+ "012345678".crypt("aa").should == "aa4c4gpuvCkSE"
+ "0123456789".crypt("aa").should == "aa4c4gpuvCkSE"
+
+ # Only uses first 2 chars of salt
+ "hello world".crypt("aa").should == "aayPz4hyPS1wI"
+ "hello world".crypt("aab").should == "aayPz4hyPS1wI"
+ "hello world".crypt("aabc").should == "aayPz4hyPS1wI"
+ end
+
+ it "raises an ArgumentError when the string contains NUL character" do
+ -> { "poison\0null".crypt("aa") }.should.raise(ArgumentError)
+ end
+
+ it "calls #to_str to converts the salt arg to a String" do
+ obj = mock('aa')
+ obj.should_receive(:to_str).and_return("aa")
+
+ "".crypt(obj).should == "aaQSqAReePlq6"
+ end
+
+ it "doesn't return subclass instances" do
+ StringSpecs::MyString.new("hello").crypt("aa").should.instance_of?(String)
+ "hello".crypt(StringSpecs::MyString.new("aa")).should.instance_of?(String)
+ StringSpecs::MyString.new("hello").crypt(StringSpecs::MyString.new("aa")).should.instance_of?(String)
+ end
+
+ it "raises an ArgumentError when the salt is shorter than two characters" do
+ -> { "hello".crypt("") }.should.raise(ArgumentError)
+ -> { "hello".crypt("f") }.should.raise(ArgumentError)
+ -> { "hello".crypt("\x00\x00") }.should.raise(ArgumentError)
+ -> { "hello".crypt("\x00a") }.should.raise(ArgumentError)
+ -> { "hello".crypt("a\x00") }.should.raise(ArgumentError)
+ end
+ end
+
+ it "raises a type error when the salt arg can't be converted to a string" do
+ -> { "".crypt(5) }.should.raise(TypeError)
+ -> { "".crypt(mock('x')) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/string/dedup_spec.rb b/spec/ruby/core/string/dedup_spec.rb
new file mode 100644
index 0000000000..2b31d80708
--- /dev/null
+++ b/spec/ruby/core/string/dedup_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/dedup'
+
+describe 'String#dedup' do
+ it_behaves_like :string_dedup, :dedup
+end
diff --git a/spec/ruby/core/string/delete_prefix_spec.rb b/spec/ruby/core/string/delete_prefix_spec.rb
new file mode 100644
index 0000000000..fc69adcbad
--- /dev/null
+++ b/spec/ruby/core/string/delete_prefix_spec.rb
@@ -0,0 +1,83 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#delete_prefix" do
+ it "returns a copy of the string, with the given prefix removed" do
+ 'hello'.delete_prefix('hell').should == 'o'
+ 'hello'.delete_prefix('hello').should == ''
+ end
+
+ it "returns a copy of the string, when the prefix isn't found" do
+ s = 'hello'
+ r = s.delete_prefix('hello!')
+ r.should_not.equal? s
+ r.should == s
+ r = s.delete_prefix('ell')
+ r.should_not.equal? s
+ r.should == s
+ r = s.delete_prefix('')
+ r.should_not.equal? s
+ r.should == s
+ end
+
+ it "does not remove partial bytes, only full characters" do
+ "\xe3\x81\x82".delete_prefix("\xe3").should == "\xe3\x81\x82"
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello'.delete_prefix('hell')
+ $~.should == nil
+ end
+
+ it "calls to_str on its argument" do
+ o = mock('x')
+ o.should_receive(:to_str).and_return 'hell'
+ 'hello'.delete_prefix(o).should == 'o'
+ end
+
+ it "returns a String instance when called on a subclass instance" do
+ s = StringSpecs::MyString.new('hello')
+ s.delete_prefix('hell').should.instance_of?(String)
+ end
+
+ it "returns a String in the same encoding as self" do
+ 'hello'.encode("US-ASCII").delete_prefix('hell').encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#delete_prefix!" do
+ it "removes the found prefix" do
+ s = 'hello'
+ s.delete_prefix!('hell').should.equal?(s)
+ s.should == 'o'
+ end
+
+ it "returns nil if no change is made" do
+ s = 'hello'
+ s.delete_prefix!('ell').should == nil
+ s.delete_prefix!('').should == nil
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello'.delete_prefix!('hell')
+ $~.should == nil
+ end
+
+ it "calls to_str on its argument" do
+ o = mock('x')
+ o.should_receive(:to_str).and_return 'hell'
+ 'hello'.delete_prefix!(o).should == 'o'
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ -> { 'hello'.freeze.delete_prefix!('hell') }.should.raise(FrozenError)
+ -> { 'hello'.freeze.delete_prefix!('') }.should.raise(FrozenError)
+ -> { ''.freeze.delete_prefix!('') }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/delete_spec.rb b/spec/ruby/core/string/delete_spec.rb
new file mode 100644
index 0000000000..adb5150cff
--- /dev/null
+++ b/spec/ruby/core/string/delete_spec.rb
@@ -0,0 +1,117 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#delete" do
+ it "returns a new string with the chars from the intersection of sets removed" do
+ s = "hello"
+ s.delete("lo").should == "he"
+ s.should == "hello"
+
+ "hello".delete("l", "lo").should == "heo"
+
+ "hell yeah".delete("").should == "hell yeah"
+ end
+
+ it "raises an ArgumentError when given no arguments" do
+ -> { "hell yeah".delete }.should.raise(ArgumentError)
+ end
+
+ it "negates sets starting with ^" do
+ "hello".delete("aeiou", "^e").should == "hell"
+ "hello".delete("^leh").should == "hell"
+ "hello".delete("^o").should == "o"
+ "hello".delete("^").should == "hello"
+ "^_^".delete("^^").should == "^^"
+ "oa^_^o".delete("a^").should == "o_o"
+ end
+
+ it "deletes all chars in a sequence" do
+ "hello".delete("ej-m").should == "ho"
+ "hello".delete("e-h").should == "llo"
+ "hel-lo".delete("e-").should == "hllo"
+ "hel-lo".delete("-h").should == "ello"
+ "hel-lo".delete("---").should == "hello"
+ "hel-012".delete("--2").should == "hel"
+ "hel-()".delete("(--").should == "hel"
+ "hello".delete("^e-h").should == "he"
+ "hello^".delete("^^-^").should == "^"
+ "hel--lo".delete("^---").should == "--"
+
+ "abcdefgh".delete("a-ce-fh").should == "dg"
+ "abcdefgh".delete("he-fa-c").should == "dg"
+ "abcdefgh".delete("e-fha-c").should == "dg"
+
+ "abcde".delete("ac-e").should == "b"
+ "abcde".delete("^ac-e").should == "acde"
+
+ "ABCabc[]".delete("A-a").should == "bc"
+ end
+
+ it "deletes multibyte characters" do
+ "四月".delete("月").should == "四"
+ '哥哥我倒'.delete('哥').should == "我倒"
+ end
+
+ it "respects backslash for escaping a -" do
+ 'Non-Authoritative Information'.delete(' \-\'').should ==
+ 'NonAuthoritativeInformation'
+ end
+
+ it "raises if the given ranges are invalid" do
+ not_supported_on :opal do
+ xFF = [0xFF].pack('C')
+ range = "\x00 - #{xFF}".force_encoding('utf-8')
+ -> { "hello".delete(range).should == "" }.should.raise(ArgumentError)
+ end
+ -> { "hello".delete("h-e") }.should.raise(ArgumentError)
+ -> { "hello".delete("^h-e") }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert each set arg to a string using to_str" do
+ other_string = mock('lo')
+ other_string.should_receive(:to_str).and_return("lo")
+
+ other_string2 = mock('o')
+ other_string2.should_receive(:to_str).and_return("o")
+
+ "hello world".delete(other_string, other_string2).should == "hell wrld"
+ end
+
+ it "raises a TypeError when one set arg can't be converted to a string" do
+ -> { "hello world".delete(100) }.should.raise(TypeError)
+ -> { "hello world".delete([]) }.should.raise(TypeError)
+ -> { "hello world".delete(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("oh no!!!").delete("!").should.instance_of?(String)
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello".encode("US-ASCII").delete("lo").encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#delete!" do
+ it "modifies self in place and returns self" do
+ a = "hello"
+ a.delete!("aeiou", "^e").should.equal?(a)
+ a.should == "hell"
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.delete!("z").should == nil
+ a.should == "hello"
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ a = "hello"
+ a.freeze
+
+ -> { a.delete!("") }.should.raise(FrozenError)
+ -> { a.delete!("aeiou", "^e") }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/delete_suffix_spec.rb b/spec/ruby/core/string/delete_suffix_spec.rb
new file mode 100644
index 0000000000..11d8fbbac1
--- /dev/null
+++ b/spec/ruby/core/string/delete_suffix_spec.rb
@@ -0,0 +1,83 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#delete_suffix" do
+ it "returns a copy of the string, with the given suffix removed" do
+ 'hello'.delete_suffix('ello').should == 'h'
+ 'hello'.delete_suffix('hello').should == ''
+ end
+
+ it "returns a copy of the string, when the suffix isn't found" do
+ s = 'hello'
+ r = s.delete_suffix('!hello')
+ r.should_not.equal? s
+ r.should == s
+ r = s.delete_suffix('ell')
+ r.should_not.equal? s
+ r.should == s
+ r = s.delete_suffix('')
+ r.should_not.equal? s
+ r.should == s
+ end
+
+ it "does not remove partial bytes, only full characters" do
+ "\xe3\x81\x82".delete_suffix("\x82").should == "\xe3\x81\x82"
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello'.delete_suffix('ello')
+ $~.should == nil
+ end
+
+ it "calls to_str on its argument" do
+ o = mock('x')
+ o.should_receive(:to_str).and_return 'ello'
+ 'hello'.delete_suffix(o).should == 'h'
+ end
+
+ it "returns a String instance when called on a subclass instance" do
+ s = StringSpecs::MyString.new('hello')
+ s.delete_suffix('ello').should.instance_of?(String)
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello".encode("US-ASCII").delete_suffix("ello").encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#delete_suffix!" do
+ it "removes the found prefix" do
+ s = 'hello'
+ s.delete_suffix!('ello').should.equal?(s)
+ s.should == 'h'
+ end
+
+ it "returns nil if no change is made" do
+ s = 'hello'
+ s.delete_suffix!('ell').should == nil
+ s.delete_suffix!('').should == nil
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello'.delete_suffix!('ello')
+ $~.should == nil
+ end
+
+ it "calls to_str on its argument" do
+ o = mock('x')
+ o.should_receive(:to_str).and_return 'ello'
+ 'hello'.delete_suffix!(o).should == 'h'
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ -> { 'hello'.freeze.delete_suffix!('ello') }.should.raise(FrozenError)
+ -> { 'hello'.freeze.delete_suffix!('') }.should.raise(FrozenError)
+ -> { ''.freeze.delete_suffix!('') }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/downcase_spec.rb b/spec/ruby/core/string/downcase_spec.rb
new file mode 100644
index 0000000000..4f7f44e5d4
--- /dev/null
+++ b/spec/ruby/core/string/downcase_spec.rb
@@ -0,0 +1,195 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#downcase" do
+ it "returns a copy of self with all uppercase letters downcased" do
+ "hELLO".downcase.should == "hello"
+ "hello".downcase.should == "hello"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hELLO".encode("US-ASCII").downcase.encoding.should == Encoding::US_ASCII
+ end
+
+ describe "full Unicode case mapping" do
+ it "works for all of Unicode with no option" do
+ "ÄÖÜ".downcase.should == "äöü"
+ end
+
+ it "updates string metadata" do
+ downcased = "\u{212A}ING".downcase
+
+ downcased.should == "king"
+ downcased.size.should == 4
+ downcased.bytesize.should == 4
+ downcased.ascii_only?.should == true
+ end
+ end
+
+ describe "ASCII-only case mapping" do
+ it "does not downcase non-ASCII characters" do
+ "Câ„«R".downcase(:ascii).should == "câ„«r"
+ end
+
+ it "works with substrings" do
+ "prefix TÉ"[-2..-1].downcase(:ascii).should == "tÉ"
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Turkic languages" do
+ it "downcases characters according to Turkic semantics" do
+ "İ".downcase(:turkic).should == "i"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ "İ".downcase(:turkic, :lithuanian).should == "i"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "İ".downcase(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ "İS".downcase(:lithuanian).should == "i\u{307}s"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ "İS".downcase(:lithuanian, :turkic).should == "is"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "İS".downcase(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "case folding" do
+ it "case folds special characters" do
+ "ß".downcase.should == "ß"
+ "ß".downcase(:fold).should == "ss"
+ end
+ end
+
+ it "does not allow invalid options" do
+ -> { "ABC".downcase(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns a String instance for subclasses" do
+ StringSpecs::MyString.new("FOObar").downcase.should.instance_of?(String)
+ end
+end
+
+describe "String#downcase!" do
+ it "modifies self in place" do
+ a = "HeLlO"
+ a.downcase!.should.equal?(a)
+ a.should == "hello"
+ end
+
+ it "modifies self in place for non-ascii-compatible encodings" do
+ a = "HeLlO".encode("utf-16le")
+ a.downcase!
+ a.should == "hello".encode("utf-16le")
+ end
+
+ describe "full Unicode case mapping" do
+ it "modifies self in place for all of Unicode with no option" do
+ a = "ÄÖÜ"
+ a.downcase!
+ a.should == "äöü"
+ end
+
+ it "updates string metadata" do
+ downcased = "\u{212A}ING"
+ downcased.downcase!
+
+ downcased.should == "king"
+ downcased.size.should == 4
+ downcased.bytesize.should == 4
+ downcased.ascii_only?.should == true
+ end
+ end
+
+ describe "ASCII-only case mapping" do
+ it "does not downcase non-ASCII characters" do
+ a = "Câ„«R"
+ a.downcase!(:ascii)
+ a.should == "câ„«r"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "ABC".encode("utf-16le")
+ a.downcase!(:ascii)
+ a.should == "abc".encode("utf-16le")
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Turkic languages" do
+ it "downcases characters according to Turkic semantics" do
+ a = "İ"
+ a.downcase!(:turkic)
+ a.should == "i"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ a = "İ"
+ a.downcase!(:turkic, :lithuanian)
+ a.should == "i"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "İ"; a.downcase!(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ a = "İS"
+ a.downcase!(:lithuanian)
+ a.should == "i\u{307}s"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ a = "İS"
+ a.downcase!(:lithuanian, :turkic)
+ a.should == "is"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "İS"; a.downcase!(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "case folding" do
+ it "case folds special characters" do
+ a = "ß"
+ a.downcase!
+ a.should == "ß"
+
+ a.downcase!(:fold)
+ a.should == "ss"
+ end
+ end
+
+ it "does not allow invalid options" do
+ -> { a = "ABC"; a.downcase!(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.downcase!.should == nil
+ a.should == "hello"
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ -> { "HeLlo".freeze.downcase! }.should.raise(FrozenError)
+ -> { "hello".freeze.downcase! }.should.raise(FrozenError)
+ end
+
+ it "sets the result String encoding to the source String encoding" do
+ "ABC".downcase.encoding.should.equal?(Encoding::UTF_8)
+ end
+end
diff --git a/spec/ruby/core/string/dump_spec.rb b/spec/ruby/core/string/dump_spec.rb
new file mode 100644
index 0000000000..176be79db2
--- /dev/null
+++ b/spec/ruby/core/string/dump_spec.rb
@@ -0,0 +1,396 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#dump" do
+ it "does not take into account if a string is frozen" do
+ "foo".freeze.dump.should_not.frozen?
+ end
+
+ it "returns a String instance" do
+ StringSpecs::MyString.new.dump.should.instance_of?(String)
+ end
+
+ it "wraps string with \"" do
+ "foo".dump.should == '"foo"'
+ end
+
+ it "returns a string with special characters replaced with \\<char> notation" do
+ [ ["\a", '"\\a"'],
+ ["\b", '"\\b"'],
+ ["\t", '"\\t"'],
+ ["\n", '"\\n"'],
+ ["\v", '"\\v"'],
+ ["\f", '"\\f"'],
+ ["\r", '"\\r"'],
+ ["\e", '"\\e"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with \" and \\ escaped with a backslash" do
+ [ ["\"", '"\\""'],
+ ["\\", '"\\\\"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with \\#<char> when # is followed by $, @, @@, {" do
+ [ ["\#$PATH", '"\\#$PATH"'],
+ ["\#@a", '"\\#@a"'],
+ ["\#@@a", '"\\#@@a"'],
+ ["\#{a}", '"\\#{a}"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with # not escaped when followed by any other character" do
+ [ ["#", '"#"'],
+ ["#1", '"#1"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with printable non-alphanumeric characters unescaped" do
+ [ [" ", '" "'],
+ ["!", '"!"'],
+ ["$", '"$"'],
+ ["%", '"%"'],
+ ["&", '"&"'],
+ ["'", '"\'"'],
+ ["(", '"("'],
+ [")", '")"'],
+ ["*", '"*"'],
+ ["+", '"+"'],
+ [",", '","'],
+ ["-", '"-"'],
+ [".", '"."'],
+ ["/", '"/"'],
+ [":", '":"'],
+ [";", '";"'],
+ ["<", '"<"'],
+ ["=", '"="'],
+ [">", '">"'],
+ ["?", '"?"'],
+ ["@", '"@"'],
+ ["[", '"["'],
+ ["]", '"]"'],
+ ["^", '"^"'],
+ ["_", '"_"'],
+ ["`", '"`"'],
+ ["{", '"{"'],
+ ["|", '"|"'],
+ ["}", '"}"'],
+ ["~", '"~"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with numeric characters unescaped" do
+ [ ["0", '"0"'],
+ ["1", '"1"'],
+ ["2", '"2"'],
+ ["3", '"3"'],
+ ["4", '"4"'],
+ ["5", '"5"'],
+ ["6", '"6"'],
+ ["7", '"7"'],
+ ["8", '"8"'],
+ ["9", '"9"'],
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with upper-case alpha characters unescaped" do
+ [ ["A", '"A"'],
+ ["B", '"B"'],
+ ["C", '"C"'],
+ ["D", '"D"'],
+ ["E", '"E"'],
+ ["F", '"F"'],
+ ["G", '"G"'],
+ ["H", '"H"'],
+ ["I", '"I"'],
+ ["J", '"J"'],
+ ["K", '"K"'],
+ ["L", '"L"'],
+ ["M", '"M"'],
+ ["N", '"N"'],
+ ["O", '"O"'],
+ ["P", '"P"'],
+ ["Q", '"Q"'],
+ ["R", '"R"'],
+ ["S", '"S"'],
+ ["T", '"T"'],
+ ["U", '"U"'],
+ ["V", '"V"'],
+ ["W", '"W"'],
+ ["X", '"X"'],
+ ["Y", '"Y"'],
+ ["Z", '"Z"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with lower-case alpha characters unescaped" do
+ [ ["a", '"a"'],
+ ["b", '"b"'],
+ ["c", '"c"'],
+ ["d", '"d"'],
+ ["e", '"e"'],
+ ["f", '"f"'],
+ ["g", '"g"'],
+ ["h", '"h"'],
+ ["i", '"i"'],
+ ["j", '"j"'],
+ ["k", '"k"'],
+ ["l", '"l"'],
+ ["m", '"m"'],
+ ["n", '"n"'],
+ ["o", '"o"'],
+ ["p", '"p"'],
+ ["q", '"q"'],
+ ["r", '"r"'],
+ ["s", '"s"'],
+ ["t", '"t"'],
+ ["u", '"u"'],
+ ["v", '"v"'],
+ ["w", '"w"'],
+ ["x", '"x"'],
+ ["y", '"y"'],
+ ["z", '"z"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with non-printing ASCII characters replaced by \\x notation" do
+ # Avoid the file encoding by computing the string with #chr.
+ [ [0000.chr, '"\\x00"'],
+ [0001.chr, '"\\x01"'],
+ [0002.chr, '"\\x02"'],
+ [0003.chr, '"\\x03"'],
+ [0004.chr, '"\\x04"'],
+ [0005.chr, '"\\x05"'],
+ [0006.chr, '"\\x06"'],
+ [0016.chr, '"\\x0E"'],
+ [0017.chr, '"\\x0F"'],
+ [0020.chr, '"\\x10"'],
+ [0021.chr, '"\\x11"'],
+ [0022.chr, '"\\x12"'],
+ [0023.chr, '"\\x13"'],
+ [0024.chr, '"\\x14"'],
+ [0025.chr, '"\\x15"'],
+ [0026.chr, '"\\x16"'],
+ [0027.chr, '"\\x17"'],
+ [0030.chr, '"\\x18"'],
+ [0031.chr, '"\\x19"'],
+ [0032.chr, '"\\x1A"'],
+ [0034.chr, '"\\x1C"'],
+ [0035.chr, '"\\x1D"'],
+ [0036.chr, '"\\x1E"'],
+ [0037.chr, '"\\x1F"'],
+ [0177.chr, '"\\x7F"'],
+ [0200.chr, '"\\x80"'],
+ [0201.chr, '"\\x81"'],
+ [0202.chr, '"\\x82"'],
+ [0203.chr, '"\\x83"'],
+ [0204.chr, '"\\x84"'],
+ [0205.chr, '"\\x85"'],
+ [0206.chr, '"\\x86"'],
+ [0207.chr, '"\\x87"'],
+ [0210.chr, '"\\x88"'],
+ [0211.chr, '"\\x89"'],
+ [0212.chr, '"\\x8A"'],
+ [0213.chr, '"\\x8B"'],
+ [0214.chr, '"\\x8C"'],
+ [0215.chr, '"\\x8D"'],
+ [0216.chr, '"\\x8E"'],
+ [0217.chr, '"\\x8F"'],
+ [0220.chr, '"\\x90"'],
+ [0221.chr, '"\\x91"'],
+ [0222.chr, '"\\x92"'],
+ [0223.chr, '"\\x93"'],
+ [0224.chr, '"\\x94"'],
+ [0225.chr, '"\\x95"'],
+ [0226.chr, '"\\x96"'],
+ [0227.chr, '"\\x97"'],
+ [0230.chr, '"\\x98"'],
+ [0231.chr, '"\\x99"'],
+ [0232.chr, '"\\x9A"'],
+ [0233.chr, '"\\x9B"'],
+ [0234.chr, '"\\x9C"'],
+ [0235.chr, '"\\x9D"'],
+ [0236.chr, '"\\x9E"'],
+ [0237.chr, '"\\x9F"'],
+ [0240.chr, '"\\xA0"'],
+ [0241.chr, '"\\xA1"'],
+ [0242.chr, '"\\xA2"'],
+ [0243.chr, '"\\xA3"'],
+ [0244.chr, '"\\xA4"'],
+ [0245.chr, '"\\xA5"'],
+ [0246.chr, '"\\xA6"'],
+ [0247.chr, '"\\xA7"'],
+ [0250.chr, '"\\xA8"'],
+ [0251.chr, '"\\xA9"'],
+ [0252.chr, '"\\xAA"'],
+ [0253.chr, '"\\xAB"'],
+ [0254.chr, '"\\xAC"'],
+ [0255.chr, '"\\xAD"'],
+ [0256.chr, '"\\xAE"'],
+ [0257.chr, '"\\xAF"'],
+ [0260.chr, '"\\xB0"'],
+ [0261.chr, '"\\xB1"'],
+ [0262.chr, '"\\xB2"'],
+ [0263.chr, '"\\xB3"'],
+ [0264.chr, '"\\xB4"'],
+ [0265.chr, '"\\xB5"'],
+ [0266.chr, '"\\xB6"'],
+ [0267.chr, '"\\xB7"'],
+ [0270.chr, '"\\xB8"'],
+ [0271.chr, '"\\xB9"'],
+ [0272.chr, '"\\xBA"'],
+ [0273.chr, '"\\xBB"'],
+ [0274.chr, '"\\xBC"'],
+ [0275.chr, '"\\xBD"'],
+ [0276.chr, '"\\xBE"'],
+ [0277.chr, '"\\xBF"'],
+ [0300.chr, '"\\xC0"'],
+ [0301.chr, '"\\xC1"'],
+ [0302.chr, '"\\xC2"'],
+ [0303.chr, '"\\xC3"'],
+ [0304.chr, '"\\xC4"'],
+ [0305.chr, '"\\xC5"'],
+ [0306.chr, '"\\xC6"'],
+ [0307.chr, '"\\xC7"'],
+ [0310.chr, '"\\xC8"'],
+ [0311.chr, '"\\xC9"'],
+ [0312.chr, '"\\xCA"'],
+ [0313.chr, '"\\xCB"'],
+ [0314.chr, '"\\xCC"'],
+ [0315.chr, '"\\xCD"'],
+ [0316.chr, '"\\xCE"'],
+ [0317.chr, '"\\xCF"'],
+ [0320.chr, '"\\xD0"'],
+ [0321.chr, '"\\xD1"'],
+ [0322.chr, '"\\xD2"'],
+ [0323.chr, '"\\xD3"'],
+ [0324.chr, '"\\xD4"'],
+ [0325.chr, '"\\xD5"'],
+ [0326.chr, '"\\xD6"'],
+ [0327.chr, '"\\xD7"'],
+ [0330.chr, '"\\xD8"'],
+ [0331.chr, '"\\xD9"'],
+ [0332.chr, '"\\xDA"'],
+ [0333.chr, '"\\xDB"'],
+ [0334.chr, '"\\xDC"'],
+ [0335.chr, '"\\xDD"'],
+ [0336.chr, '"\\xDE"'],
+ [0337.chr, '"\\xDF"'],
+ [0340.chr, '"\\xE0"'],
+ [0341.chr, '"\\xE1"'],
+ [0342.chr, '"\\xE2"'],
+ [0343.chr, '"\\xE3"'],
+ [0344.chr, '"\\xE4"'],
+ [0345.chr, '"\\xE5"'],
+ [0346.chr, '"\\xE6"'],
+ [0347.chr, '"\\xE7"'],
+ [0350.chr, '"\\xE8"'],
+ [0351.chr, '"\\xE9"'],
+ [0352.chr, '"\\xEA"'],
+ [0353.chr, '"\\xEB"'],
+ [0354.chr, '"\\xEC"'],
+ [0355.chr, '"\\xED"'],
+ [0356.chr, '"\\xEE"'],
+ [0357.chr, '"\\xEF"'],
+ [0360.chr, '"\\xF0"'],
+ [0361.chr, '"\\xF1"'],
+ [0362.chr, '"\\xF2"'],
+ [0363.chr, '"\\xF3"'],
+ [0364.chr, '"\\xF4"'],
+ [0365.chr, '"\\xF5"'],
+ [0366.chr, '"\\xF6"'],
+ [0367.chr, '"\\xF7"'],
+ [0370.chr, '"\\xF8"'],
+ [0371.chr, '"\\xF9"'],
+ [0372.chr, '"\\xFA"'],
+ [0373.chr, '"\\xFB"'],
+ [0374.chr, '"\\xFC"'],
+ [0375.chr, '"\\xFD"'],
+ [0376.chr, '"\\xFE"'],
+ [0377.chr, '"\\xFF"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with non-printing single-byte UTF-8 characters replaced by \\x notation" do
+ [ [0000.chr('utf-8'), '"\x00"'],
+ [0001.chr('utf-8'), '"\x01"'],
+ [0002.chr('utf-8'), '"\x02"'],
+ [0003.chr('utf-8'), '"\x03"'],
+ [0004.chr('utf-8'), '"\x04"'],
+ [0005.chr('utf-8'), '"\x05"'],
+ [0006.chr('utf-8'), '"\x06"'],
+ [0016.chr('utf-8'), '"\x0E"'],
+ [0017.chr('utf-8'), '"\x0F"'],
+ [0020.chr('utf-8'), '"\x10"'],
+ [0021.chr('utf-8'), '"\x11"'],
+ [0022.chr('utf-8'), '"\x12"'],
+ [0023.chr('utf-8'), '"\x13"'],
+ [0024.chr('utf-8'), '"\x14"'],
+ [0025.chr('utf-8'), '"\x15"'],
+ [0026.chr('utf-8'), '"\x16"'],
+ [0027.chr('utf-8'), '"\x17"'],
+ [0030.chr('utf-8'), '"\x18"'],
+ [0031.chr('utf-8'), '"\x19"'],
+ [0032.chr('utf-8'), '"\x1A"'],
+ [0034.chr('utf-8'), '"\x1C"'],
+ [0035.chr('utf-8'), '"\x1D"'],
+ [0036.chr('utf-8'), '"\x1E"'],
+ [0037.chr('utf-8'), '"\x1F"'],
+ [0177.chr('utf-8'), '"\x7F"']
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with multi-byte UTF-8 characters less than or equal 0xFFFF replaced by \\uXXXX notation with upper-case hex digits" do
+ [ [0200.chr('utf-8'), '"\u0080"'],
+ [0201.chr('utf-8'), '"\u0081"'],
+ [0202.chr('utf-8'), '"\u0082"'],
+ [0203.chr('utf-8'), '"\u0083"'],
+ [0204.chr('utf-8'), '"\u0084"'],
+ [0206.chr('utf-8'), '"\u0086"'],
+ [0207.chr('utf-8'), '"\u0087"'],
+ [0210.chr('utf-8'), '"\u0088"'],
+ [0211.chr('utf-8'), '"\u0089"'],
+ [0212.chr('utf-8'), '"\u008A"'],
+ [0213.chr('utf-8'), '"\u008B"'],
+ [0214.chr('utf-8'), '"\u008C"'],
+ [0215.chr('utf-8'), '"\u008D"'],
+ [0216.chr('utf-8'), '"\u008E"'],
+ [0217.chr('utf-8'), '"\u008F"'],
+ [0220.chr('utf-8'), '"\u0090"'],
+ [0221.chr('utf-8'), '"\u0091"'],
+ [0222.chr('utf-8'), '"\u0092"'],
+ [0223.chr('utf-8'), '"\u0093"'],
+ [0224.chr('utf-8'), '"\u0094"'],
+ [0225.chr('utf-8'), '"\u0095"'],
+ [0226.chr('utf-8'), '"\u0096"'],
+ [0227.chr('utf-8'), '"\u0097"'],
+ [0230.chr('utf-8'), '"\u0098"'],
+ [0231.chr('utf-8'), '"\u0099"'],
+ [0232.chr('utf-8'), '"\u009A"'],
+ [0233.chr('utf-8'), '"\u009B"'],
+ [0234.chr('utf-8'), '"\u009C"'],
+ [0235.chr('utf-8'), '"\u009D"'],
+ [0236.chr('utf-8'), '"\u009E"'],
+ [0237.chr('utf-8'), '"\u009F"'],
+ [0177777.chr('utf-8'), '"\uFFFF"'],
+ ].should be_computed_by(:dump)
+ end
+
+ it "returns a string with multi-byte UTF-8 characters greater than 0xFFFF replaced by \\u{XXXXXX} notation with upper-case hex digits" do
+ 0x10000.chr('utf-8').dump.should == '"\u{10000}"'
+ 0x10FFFF.chr('utf-8').dump.should == '"\u{10FFFF}"'
+ end
+
+ it "includes .force_encoding(name) if the encoding isn't ASCII compatible" do
+ "\u{876}".encode('utf-16be').dump.should.end_with?(".force_encoding(\"UTF-16BE\")")
+ "\u{876}".encode('utf-16le').dump.should.end_with?(".force_encoding(\"UTF-16LE\")")
+ end
+
+ it "returns a String in the same encoding as self" do
+ "foo".encode("ISO-8859-1").dump.encoding.should == Encoding::ISO_8859_1
+ "foo".encode('windows-1251').dump.encoding.should == Encoding::Windows_1251
+ 1.chr.dump.encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/string/dup_spec.rb b/spec/ruby/core/string/dup_spec.rb
new file mode 100644
index 0000000000..e367f7a311
--- /dev/null
+++ b/spec/ruby/core/string/dup_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#dup" do
+ before :each do
+ ScratchPad.clear
+ @obj = StringSpecs::InitializeString.new "string"
+ end
+
+ it "calls #initialize_copy on the new instance" do
+ dup = @obj.dup
+ ScratchPad.recorded.should_not == @obj.object_id
+ ScratchPad.recorded.should == dup.object_id
+ end
+
+ it "copies instance variables" do
+ dup = @obj.dup
+ dup.ivar.should == 1
+ end
+
+ it "does not copy singleton methods" do
+ def @obj.special() :the_one end
+ dup = @obj.dup
+ -> { dup.special }.should.raise(NameError)
+ end
+
+ it "does not copy modules included in the singleton class" do
+ class << @obj
+ include StringSpecs::StringModule
+ end
+
+ dup = @obj.dup
+ -> { dup.repr }.should.raise(NameError)
+ end
+
+ it "does not copy constants defined in the singleton class" do
+ class << @obj
+ CLONE = :clone
+ end
+
+ dup = @obj.dup
+ -> { class << dup; CLONE; end }.should.raise(NameError)
+ end
+
+ it "does not modify the original string when changing dupped string" do
+ orig = "string"[0..100]
+ dup = orig.dup
+ orig[0] = 'x'
+ orig.should == "xtring"
+ dup.should == "string"
+ end
+
+ it "does not modify the original setbyte-mutated string when changing dupped string" do
+ orig = +"a"
+ orig.setbyte 0, "b".ord
+ copy = orig.dup
+ orig.setbyte 0, "c".ord
+ orig.should == "c"
+ copy.should == "b"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello".encode("US-ASCII").dup.encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/string/each_byte_spec.rb b/spec/ruby/core/string/each_byte_spec.rb
new file mode 100644
index 0000000000..1884df416b
--- /dev/null
+++ b/spec/ruby/core/string/each_byte_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#each_byte" do
+ it "passes each byte in self to the given block" do
+ a = []
+ "hello\x00".each_byte { |c| a << c }
+ a.should == [104, 101, 108, 108, 111, 0]
+ end
+
+ it "keeps iterating from the old position (to new string end) when self changes" do
+ r = +""
+ s = +"hello world"
+ s.each_byte do |c|
+ r << c
+ s.insert(0, "<>") if r.size < 3
+ end
+ r.should == "h><>hello world"
+
+ r = +""
+ s = +"hello world"
+ s.each_byte { |c| s.slice!(-1); r << c }
+ r.should == "hello "
+
+ r = +""
+ s = +"hello world"
+ s.each_byte { |c| s.slice!(0); r << c }
+ r.should == "hlowrd"
+
+ r = +""
+ s = +"hello world"
+ s.each_byte { |c| s.slice!(0..-1); r << c }
+ r.should == "h"
+ end
+
+ it "returns self" do
+ s = "hello"
+ (s.each_byte {}).should.equal?(s)
+ end
+
+ describe "when no block is given" do
+ it "returns an enumerator" do
+ enum = "hello".each_byte
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == [104, 101, 108, 108, 111]
+ end
+
+ describe "returned enumerator" do
+ describe "size" do
+ it "should return the bytesize of the string" do
+ str = "hello"
+ str.each_byte.size.should == str.bytesize
+ str = "ola"
+ str.each_byte.size.should == str.bytesize
+ str = "\303\207\342\210\202\303\251\306\222g"
+ str.each_byte.size.should == str.bytesize
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/each_char_spec.rb b/spec/ruby/core/string/each_char_spec.rb
new file mode 100644
index 0000000000..36219f79db
--- /dev/null
+++ b/spec/ruby/core/string/each_char_spec.rb
@@ -0,0 +1,8 @@
+require_relative "../../spec_helper"
+require_relative 'shared/chars'
+require_relative 'shared/each_char_without_block'
+
+describe "String#each_char" do
+ it_behaves_like :string_chars, :each_char
+ it_behaves_like :string_each_char_without_block, :each_char
+end
diff --git a/spec/ruby/core/string/each_codepoint_spec.rb b/spec/ruby/core/string/each_codepoint_spec.rb
new file mode 100644
index 0000000000..c11cb1beae
--- /dev/null
+++ b/spec/ruby/core/string/each_codepoint_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/codepoints'
+require_relative 'shared/each_codepoint_without_block'
+
+describe "String#each_codepoint" do
+ it_behaves_like :string_codepoints, :each_codepoint
+ it_behaves_like :string_each_codepoint_without_block, :each_codepoint
+end
diff --git a/spec/ruby/core/string/each_grapheme_cluster_spec.rb b/spec/ruby/core/string/each_grapheme_cluster_spec.rb
new file mode 100644
index 0000000000..e1fa4ae67b
--- /dev/null
+++ b/spec/ruby/core/string/each_grapheme_cluster_spec.rb
@@ -0,0 +1,16 @@
+require_relative "../../spec_helper"
+require_relative 'shared/chars'
+require_relative 'shared/grapheme_clusters'
+require_relative 'shared/each_char_without_block'
+
+describe "String#each_grapheme_cluster" do
+ it_behaves_like :string_chars, :each_grapheme_cluster
+ it_behaves_like :string_grapheme_clusters, :each_grapheme_cluster
+ it_behaves_like :string_each_char_without_block, :each_grapheme_cluster
+
+ it "yields String instances for subclasses" do
+ a = []
+ StringSpecs::MyString.new("abc").each_grapheme_cluster { |s| a << s.class }
+ a.should == [String, String, String]
+ end
+end
diff --git a/spec/ruby/core/string/each_line_spec.rb b/spec/ruby/core/string/each_line_spec.rb
new file mode 100644
index 0000000000..90fc920bf1
--- /dev/null
+++ b/spec/ruby/core/string/each_line_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/each_line'
+require_relative 'shared/each_line_without_block'
+
+describe "String#each_line" do
+ it_behaves_like :string_each_line, :each_line
+ it_behaves_like :string_each_line_without_block, :each_line
+end
diff --git a/spec/ruby/core/string/element_reference_spec.rb b/spec/ruby/core/string/element_reference_spec.rb
new file mode 100644
index 0000000000..f6e1750c93
--- /dev/null
+++ b/spec/ruby/core/string/element_reference_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/slice'
+
+describe "String#[]" do
+ it_behaves_like :string_slice, :[]
+end
+
+describe "String#[] with index, length" do
+ it_behaves_like :string_slice_index_length, :[]
+end
+
+describe "String#[] with Range" do
+ it_behaves_like :string_slice_range, :[]
+end
+
+describe "String#[] with Regexp" do
+ it_behaves_like :string_slice_regexp, :[]
+end
+
+describe "String#[] with Regexp, index" do
+ it_behaves_like :string_slice_regexp_index, :[]
+end
+
+describe "String#[] with Regexp, group" do
+ it_behaves_like :string_slice_regexp_group, :[]
+end
+
+describe "String#[] with String" do
+ it_behaves_like :string_slice_string, :[]
+end
+
+describe "String#[] with Symbol" do
+ it_behaves_like :string_slice_symbol, :[]
+end
diff --git a/spec/ruby/core/string/element_set_spec.rb b/spec/ruby/core/string/element_set_spec.rb
new file mode 100644
index 0000000000..2abc5117aa
--- /dev/null
+++ b/spec/ruby/core/string/element_set_spec.rb
@@ -0,0 +1,589 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# TODO: Add missing String#[]= specs:
+# String#[re, idx] = obj
+
+describe "String#[]= with Integer index" do
+ it "replaces the char at idx with other_str" do
+ a = "hello"
+ a[0] = "bam"
+ a.should == "bamello"
+ a[-2] = ""
+ a.should == "bamelo"
+ end
+
+ it "raises an IndexError without changing self if idx is outside of self" do
+ str = "hello"
+
+ -> { str[20] = "bam" }.should.raise(IndexError)
+ str.should == "hello"
+
+ -> { str[-20] = "bam" }.should.raise(IndexError)
+ str.should == "hello"
+
+ -> { ""[-1] = "bam" }.should.raise(IndexError)
+ end
+
+ # Behaviour is verified by matz in
+ # http://redmine.ruby-lang.org/issues/show/1750
+ it "allows assignment to the zero'th element of an empty String" do
+ str = ""
+ str[0] = "bam"
+ str.should == "bam"
+ end
+
+ it "raises IndexError if the string index doesn't match a position in the string" do
+ str = "hello"
+ -> { str['y'] = "bam" }.should.raise(IndexError)
+ str.should == "hello"
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ a = "hello"
+ a.freeze
+
+ -> { a[0] = "bam" }.should.raise(FrozenError)
+ end
+
+ it "calls to_int on index" do
+ str = "hello"
+ str[0.5] = "hi "
+ str.should == "hi ello"
+
+ obj = mock('-1')
+ obj.should_receive(:to_int).and_return(-1)
+ str[obj] = "!"
+ str.should == "hi ell!"
+ end
+
+ it "calls #to_str to convert other to a String" do
+ other_str = mock('-test-')
+ other_str.should_receive(:to_str).and_return("-test-")
+
+ a = "abc"
+ a[1] = other_str
+ a.should == "a-test-c"
+ end
+
+ it "raises a TypeError if other_str can't be converted to a String" do
+ -> { "test"[1] = [] }.should.raise(TypeError)
+ -> { "test"[1] = mock('x') }.should.raise(TypeError)
+ -> { "test"[1] = nil }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed an Integer replacement" do
+ -> { "abc"[1] = 65 }.should.raise(TypeError)
+ end
+
+ it "raises an IndexError if the index is greater than character size" do
+ -> { "ã‚れ"[4] = "a" }.should.raise(IndexError)
+ end
+
+ it "calls #to_int to convert the index" do
+ index = mock("string element set")
+ index.should_receive(:to_int).and_return(1)
+
+ str = "ã‚れ"
+ str[index] = "a"
+ str.should == "ã‚a"
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ index = mock("string element set")
+ index.should_receive(:to_int).and_return('1')
+
+ -> { "abc"[index] = "d" }.should.raise(TypeError)
+ end
+
+ it "raises an IndexError if #to_int returns a value out of range" do
+ index = mock("string element set")
+ index.should_receive(:to_int).and_return(4)
+
+ -> { "ab"[index] = "c" }.should.raise(IndexError)
+ end
+
+ it "replaces a character with a multibyte character" do
+ str = "ã‚りãŒã¨u"
+ str[4] = "ã†"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "replaces a multibyte character with a character" do
+ str = "ã‚りãŒã¨ã†"
+ str[4] = "u"
+ str.should == "ã‚りãŒã¨u"
+ end
+
+ it "replaces a multibyte character with a multibyte character" do
+ str = "ã‚りãŒã¨ãŠ"
+ str[4] = "ã†"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "encodes the String in an encoding compatible with the replacement" do
+ str = " ".force_encoding Encoding::US_ASCII
+ rep = [160].pack('C').force_encoding Encoding::BINARY
+ str[0] = rep
+ str.encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "updates the string to a compatible encoding" do
+ str = " "
+ str[1] = [0xB9].pack("C*")
+ str.encoding.should == Encoding::ASCII_8BIT
+ end
+
+ it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do
+ str = "ã‚れ"
+ rep = "ãŒ".encode Encoding::EUC_JP
+ -> { str[0] = rep }.should.raise(Encoding::CompatibilityError)
+ end
+end
+
+describe "String#[]= with String index" do
+ it "replaces fewer characters with more characters" do
+ str = "abcde"
+ str["cd"] = "ghi"
+ str.should == "abghie"
+ end
+
+ it "replaces more characters with fewer characters" do
+ str = "abcde"
+ str["bcd"] = "f"
+ str.should == "afe"
+ end
+
+ it "replaces characters with no characters" do
+ str = "abcde"
+ str["cd"] = ""
+ str.should == "abe"
+ end
+
+ it "raises an IndexError if the search String is not found" do
+ str = "abcde"
+ -> { str["g"] = "h" }.should.raise(IndexError)
+ end
+
+ it "replaces characters with a multibyte character" do
+ str = "ã‚りgaã¨ã†"
+ str["ga"] = "ãŒ"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "replaces multibyte characters with characters" do
+ str = "ã‚りãŒã¨ã†"
+ str["ãŒ"] = "ga"
+ str.should == "ã‚りgaã¨ã†"
+ end
+
+ it "replaces multibyte characters with multibyte characters" do
+ str = "ã‚りãŒã¨ã†"
+ str["ãŒ"] = "ã‹"
+ str.should == "ã‚りã‹ã¨ã†"
+ end
+
+ it "encodes the String in an encoding compatible with the replacement" do
+ str = " ".force_encoding Encoding::US_ASCII
+ rep = [160].pack('C').force_encoding Encoding::BINARY
+ str[" "] = rep
+ str.encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do
+ str = "ã‚れ"
+ rep = "ãŒ".encode Encoding::EUC_JP
+ -> { str["れ"] = rep }.should.raise(Encoding::CompatibilityError)
+ end
+end
+
+describe "String#[]= with a Regexp index" do
+ it "replaces the matched text with the rhs" do
+ str = "hello"
+ str[/lo/] = "x"
+ str.should == "helx"
+ end
+
+ it "raises IndexError if the regexp index doesn't match a position in the string" do
+ str = "hello"
+ -> { str[/y/] = "bam" }.should.raise(IndexError)
+ str.should == "hello"
+ end
+
+ it "calls #to_str to convert the replacement" do
+ rep = mock("string element set regexp")
+ rep.should_receive(:to_str).and_return("b")
+
+ str = "abc"
+ str[/ab/] = rep
+ str.should == "bc"
+ end
+
+ it "checks the match before calling #to_str to convert the replacement" do
+ rep = mock("string element set regexp")
+ rep.should_not_receive(:to_str)
+
+ -> { "abc"[/def/] = rep }.should.raise(IndexError)
+ end
+
+ describe "with 3 arguments" do
+ it "calls #to_int to convert the second object" do
+ ref = mock("string element set regexp ref")
+ ref.should_receive(:to_int).and_return(1)
+
+ str = "abc"
+ str[/a(b)/, ref] = "x"
+ str.should == "axc"
+ end
+
+ it "raises a TypeError if #to_int does not return an Integer" do
+ ref = mock("string element set regexp ref")
+ ref.should_receive(:to_int).and_return(nil)
+
+ -> { "abc"[/a(b)/, ref] = "x" }.should.raise(TypeError)
+ end
+
+ it "uses the 2nd of 3 arguments as which capture should be replaced" do
+ str = "aaa bbb ccc"
+ str[/a (bbb) c/, 1] = "ddd"
+ str.should == "aaa ddd ccc"
+ end
+
+ it "allows the specified capture to be negative and count from the end" do
+ str = "abcd"
+ str[/(a)(b)(c)(d)/, -2] = "e"
+ str.should == "abed"
+ end
+
+ it "checks the match index before calling #to_str to convert the replacement" do
+ rep = mock("string element set regexp")
+ rep.should_not_receive(:to_str)
+
+ -> { "abc"[/a(b)/, 2] = rep }.should.raise(IndexError)
+ end
+
+ it "raises IndexError if the specified capture isn't available" do
+ str = "aaa bbb ccc"
+ -> { str[/a (bbb) c/, 2] = "ddd" }.should.raise(IndexError)
+ -> { str[/a (bbb) c/, -2] = "ddd" }.should.raise(IndexError)
+ end
+
+ describe "when the optional capture does not match" do
+ it "raises an IndexError before setting the replacement" do
+ str1 = "a b c"
+ str2 = str1.dup
+ -> { str2[/a (b) (Z)?/, 2] = "d" }.should.raise(IndexError)
+ str2.should == str1
+ end
+ end
+ end
+
+ it "replaces characters with a multibyte character" do
+ str = "ã‚りgaã¨ã†"
+ str[/ga/] = "ãŒ"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "replaces multibyte characters with characters" do
+ str = "ã‚りãŒã¨ã†"
+ str[/ãŒ/] = "ga"
+ str.should == "ã‚りgaã¨ã†"
+ end
+
+ it "replaces multibyte characters with multibyte characters" do
+ str = "ã‚りãŒã¨ã†"
+ str[/ãŒ/] = "ã‹"
+ str.should == "ã‚りã‹ã¨ã†"
+ end
+
+ it "encodes the String in an encoding compatible with the replacement" do
+ str = " ".force_encoding Encoding::US_ASCII
+ rep = [160].pack('C').force_encoding Encoding::BINARY
+ str[/ /] = rep
+ str.encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do
+ str = "ã‚れ"
+ rep = "ãŒ".encode Encoding::EUC_JP
+ -> { str[/れ/] = rep }.should.raise(Encoding::CompatibilityError)
+ end
+end
+
+describe "String#[]= with a Range index" do
+ describe "with an empty replacement" do
+ it "does not replace a character with a zero-index, zero exclude-end range" do
+ str = "abc"
+ str[0...0] = ""
+ str.should == "abc"
+ end
+
+ it "does not replace a character with a zero exclude-end range" do
+ str = "abc"
+ str[1...1] = ""
+ str.should == "abc"
+ end
+
+ it "replaces a character with zero-index, zero non-exclude-end range" do
+ str = "abc"
+ str[0..0] = ""
+ str.should == "bc"
+ end
+
+ it "replaces a character with a zero non-exclude-end range" do
+ str = "abc"
+ str[1..1] = ""
+ str.should == "ac"
+ end
+ end
+
+ it "replaces the contents with a shorter String" do
+ str = "abcde"
+ str[0..-1] = "hg"
+ str.should == "hg"
+ end
+
+ it "replaces the contents with a longer String" do
+ str = "abc"
+ str[0...4] = "uvwxyz"
+ str.should == "uvwxyz"
+ end
+
+ it "replaces a partial string" do
+ str = "abcde"
+ str[1..3] = "B"
+ str.should == "aBe"
+ end
+
+ it "raises a RangeError if negative Range begin is out of range" do
+ -> { "abc"[-4..-2] = "x" }.should.raise(RangeError, "-4..-2 out of range")
+ end
+
+ it "raises a RangeError if positive Range begin is greater than String size" do
+ -> { "abc"[4..2] = "x" }.should.raise(RangeError, "4..2 out of range")
+ end
+
+ it "uses the Range end as an index rather than a count" do
+ str = "abcdefg"
+ str[-5..3] = "xyz"
+ str.should == "abxyzefg"
+ end
+
+ it "treats a negative out-of-range Range end with a positive Range begin as a zero count" do
+ str = "abc"
+ str[1..-4] = "x"
+ str.should == "axbc"
+ end
+
+ it "treats a negative out-of-range Range end with a negative Range begin as a zero count" do
+ str = "abcd"
+ str[-1..-4] = "x"
+ str.should == "abcxd"
+ end
+
+ it "replaces characters with a multibyte character" do
+ str = "ã‚りgaã¨ã†"
+ str[2..3] = "ãŒ"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "replaces multibyte characters with characters" do
+ str = "ã‚りãŒã¨ã†"
+ str[2...3] = "ga"
+ str.should == "ã‚りgaã¨ã†"
+ end
+
+ it "replaces multibyte characters by negative indexes" do
+ str = "ã‚りãŒã¨ã†"
+ str[-3...-2] = "ga"
+ str.should == "ã‚りgaã¨ã†"
+ end
+
+ it "replaces multibyte characters with multibyte characters" do
+ str = "ã‚りãŒã¨ã†"
+ str[2..2] = "ã‹"
+ str.should == "ã‚りã‹ã¨ã†"
+ end
+
+ it "deletes a multibyte character" do
+ str = "ã‚りã¨ã†"
+ str[2..3] = ""
+ str.should == "ã‚り"
+ end
+
+ it "inserts a multibyte character" do
+ str = "ã‚りã¨ã†"
+ str[2...2] = "ãŒ"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "encodes the String in an encoding compatible with the replacement" do
+ str = " ".force_encoding Encoding::US_ASCII
+ rep = [160].pack('C').force_encoding Encoding::BINARY
+ str[0..1] = rep
+ str.encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do
+ str = "ã‚れ"
+ rep = "ãŒ".encode Encoding::EUC_JP
+ -> { str[0..1] = rep }.should.raise(Encoding::CompatibilityError)
+ end
+end
+
+describe "String#[]= with Integer index, count" do
+ it "starts at idx and overwrites count characters before inserting the rest of other_str" do
+ a = "hello"
+ a[0, 2] = "xx"
+ a.should == "xxllo"
+ a = "hello"
+ a[0, 2] = "jello"
+ a.should == "jellollo"
+ end
+
+ it "counts negative idx values from end of the string" do
+ a = "hello"
+ a[-1, 0] = "bob"
+ a.should == "hellbobo"
+ a = "hello"
+ a[-5, 0] = "bob"
+ a.should == "bobhello"
+ end
+
+ it "overwrites and deletes characters if count is more than the length of other_str" do
+ a = "hello"
+ a[0, 4] = "x"
+ a.should == "xo"
+ a = "hello"
+ a[0, 5] = "x"
+ a.should == "x"
+ end
+
+ it "deletes characters if other_str is an empty string" do
+ a = "hello"
+ a[0, 2] = ""
+ a.should == "llo"
+ end
+
+ it "deletes characters up to the maximum length of the existing string" do
+ a = "hello"
+ a[0, 6] = "x"
+ a.should == "x"
+ a = "hello"
+ a[0, 100] = ""
+ a.should == ""
+ end
+
+ it "appends other_str to the end of the string if idx == the length of the string" do
+ a = "hello"
+ a[5, 0] = "bob"
+ a.should == "hellobob"
+ end
+
+ it "calls #to_int to convert the index and count objects" do
+ index = mock("string element set index")
+ index.should_receive(:to_int).and_return(-4)
+
+ count = mock("string element set count")
+ count.should_receive(:to_int).and_return(2)
+
+ str = "abcde"
+ str[index, count] = "xyz"
+ str.should == "axyzde"
+ end
+
+ it "raises a TypeError if #to_int for index does not return an Integer" do
+ index = mock("string element set index")
+ index.should_receive(:to_int).and_return("1")
+
+ -> { "abc"[index, 2] = "xyz" }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if #to_int for count does not return an Integer" do
+ count = mock("string element set count")
+ count.should_receive(:to_int).and_return("1")
+
+ -> { "abc"[1, count] = "xyz" }.should.raise(TypeError)
+ end
+
+ it "calls #to_str to convert the replacement object" do
+ r = mock("string element set replacement")
+ r.should_receive(:to_str).and_return("xyz")
+
+ str = "abcde"
+ str[2, 2] = r
+ str.should == "abxyze"
+ end
+
+ it "raises a TypeError of #to_str does not return a String" do
+ r = mock("string element set replacement")
+ r.should_receive(:to_str).and_return(nil)
+
+ -> { "abc"[1, 1] = r }.should.raise(TypeError)
+ end
+
+ it "raises an IndexError if |idx| is greater than the length of the string" do
+ -> { "hello"[6, 0] = "bob" }.should.raise(IndexError)
+ -> { "hello"[-6, 0] = "bob" }.should.raise(IndexError)
+ end
+
+ it "raises an IndexError if count < 0" do
+ -> { "hello"[0, -1] = "bob" }.should.raise(IndexError)
+ -> { "hello"[1, -1] = "bob" }.should.raise(IndexError)
+ end
+
+ it "raises a TypeError if other_str is a type other than String" do
+ -> { "hello"[0, 2] = nil }.should.raise(TypeError)
+ -> { "hello"[0, 2] = [] }.should.raise(TypeError)
+ -> { "hello"[0, 2] = 33 }.should.raise(TypeError)
+ end
+
+ it "replaces characters with a multibyte character" do
+ str = "ã‚りgaã¨ã†"
+ str[2, 2] = "ãŒ"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "replaces multibyte characters with characters" do
+ str = "ã‚りãŒã¨ã†"
+ str[2, 1] = "ga"
+ str.should == "ã‚りgaã¨ã†"
+ end
+
+ it "replaces multibyte characters with multibyte characters" do
+ str = "ã‚りãŒã¨ã†"
+ str[2, 1] = "ã‹"
+ str.should == "ã‚りã‹ã¨ã†"
+ end
+
+ it "deletes a multibyte character" do
+ str = "ã‚りã¨ã†"
+ str[2, 2] = ""
+ str.should == "ã‚り"
+ end
+
+ it "inserts a multibyte character" do
+ str = "ã‚りã¨ã†"
+ str[2, 0] = "ãŒ"
+ str.should == "ã‚りãŒã¨ã†"
+ end
+
+ it "raises an IndexError if the character index is out of range of a multibyte String" do
+ -> { "ã‚れ"[3, 0] = "り" }.should.raise(IndexError)
+ end
+
+ it "encodes the String in an encoding compatible with the replacement" do
+ str = " ".force_encoding Encoding::US_ASCII
+ rep = [160].pack('C').force_encoding Encoding::BINARY
+ str[0, 1] = rep
+ str.encoding.should.equal?(Encoding::BINARY)
+ end
+
+ it "raises an Encoding::CompatibilityError if the replacement encoding is incompatible" do
+ str = "ã‚れ"
+ rep = "ãŒ".encode Encoding::EUC_JP
+ -> { str[0, 1] = rep }.should.raise(Encoding::CompatibilityError)
+ end
+end
diff --git a/spec/ruby/core/string/empty_spec.rb b/spec/ruby/core/string/empty_spec.rb
new file mode 100644
index 0000000000..8e53a16afc
--- /dev/null
+++ b/spec/ruby/core/string/empty_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#empty?" do
+ it "returns true if the string has a length of zero" do
+ "hello".should_not.empty?
+ " ".should_not.empty?
+ "\x00".should_not.empty?
+ "".should.empty?
+ StringSpecs::MyString.new("").should.empty?
+ end
+end
diff --git a/spec/ruby/core/string/encode_spec.rb b/spec/ruby/core/string/encode_spec.rb
new file mode 100644
index 0000000000..ab096f4041
--- /dev/null
+++ b/spec/ruby/core/string/encode_spec.rb
@@ -0,0 +1,240 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'shared/encode'
+
+describe "String#encode" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it_behaves_like :string_encode, :encode
+
+ describe "when passed no options" do
+ it "returns a copy when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ str = "ã‚"
+ encoded = str.encode
+ encoded.should_not.equal?(str)
+ encoded.should == str
+ end
+
+ it "returns a copy for a ASCII-only String when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ str = "abc"
+ encoded = str.encode
+ encoded.should_not.equal?(str)
+ encoded.should == str
+ end
+
+ it "encodes an ascii substring of a binary string to UTF-8" do
+ x82 = [0x82].pack('C')
+ str = "#{x82}foo".dup.force_encoding("binary")[1..-1].encode("utf-8")
+ str.should == "foo".dup.force_encoding("utf-8")
+ str.encoding.should.equal?(Encoding::UTF_8)
+ end
+ end
+
+ describe "when passed to encoding" do
+ it "returns a copy when passed the same encoding as the String" do
+ str = "ã‚"
+ encoded = str.encode(Encoding::UTF_8)
+ encoded.should_not.equal?(str)
+ encoded.should == str
+ end
+
+ it "round trips a String" do
+ str = "abc def".dup.force_encoding Encoding::US_ASCII
+ str.encode("utf-32be").encode("ascii").should == "abc def"
+ end
+ end
+
+ describe "when passed options" do
+ it "returns a copy when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ str = "ã‚"
+ str.encode(invalid: :replace).should_not.equal?(str)
+ end
+
+ it "normalizes newlines with cr_newline option" do
+ "\r\nfoo".encode(cr_newline: true).should == "\r\rfoo"
+ "\rfoo".encode(cr_newline: true).should == "\rfoo"
+ "\nfoo".encode(cr_newline: true).should == "\rfoo"
+ end
+
+ it "normalizes newlines with crlf_newline option" do
+ "\r\nfoo".encode(crlf_newline: true).should == "\r\r\nfoo"
+ "\rfoo".encode(crlf_newline: true).should == "\rfoo"
+ "\nfoo".encode(crlf_newline: true).should == "\r\nfoo"
+ end
+
+ it "normalizes newlines with universal_newline option" do
+ "\r\nfoo".encode(universal_newline: true).should == "\nfoo"
+ "\rfoo".encode(universal_newline: true).should == "\nfoo"
+ "\nfoo".encode(universal_newline: true).should == "\nfoo"
+ end
+
+ it "replaces invalid encoding in source with default replacement" do
+ encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace)
+ encoded.should == "\u3061\ufffd\ufffd".encode("UTF-16LE")
+ encoded.encode("UTF-8").should == "ã¡\ufffd\ufffd"
+ end
+
+ it "replaces invalid encoding in source with a specified replacement" do
+ encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace, replace: "foo")
+ encoded.should == "\u3061foofoo".encode("UTF-16LE")
+ encoded.encode("UTF-8").should == "ã¡foofoo"
+ end
+
+ it "replace multiple invalid bytes at the end with a single replacement character" do
+ "\xE3\x81\x93\xE3\x81".encode("UTF-8", invalid: :replace).should == "\u3053\ufffd"
+ end
+
+ it "replaces invalid encoding in source using a specified replacement even when a fallback is given" do
+ encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace, replace: "foo", fallback: -> c { "bar" })
+ encoded.should == "\u3061foofoo".encode("UTF-16LE")
+ encoded.encode("UTF-8").should == "ã¡foofoo"
+ end
+
+ it "replaces undefined encoding in destination with default replacement" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, undef: :replace)
+ encoded.should == "B?".encode(Encoding::US_ASCII)
+ encoded.encode("UTF-8").should == "B?"
+ end
+
+ it "replaces undefined encoding in destination with a specified replacement" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, undef: :replace, replace: "foo")
+ encoded.should == "Bfoo".encode(Encoding::US_ASCII)
+ encoded.encode("UTF-8").should == "Bfoo"
+ end
+
+ it "replaces undefined encoding in destination with a specified replacement even if a fallback is given" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, undef: :replace, replace: "foo", fallback: proc {|x| "bar"})
+ encoded.should == "Bfoo".encode(Encoding::US_ASCII)
+ encoded.encode("UTF-8").should == "Bfoo"
+ end
+
+ it "replaces undefined encoding in destination using a fallback proc" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc {|x| "bar"})
+ encoded.should == "Bbar".encode(Encoding::US_ASCII)
+ encoded.encode("UTF-8").should == "Bbar"
+ end
+
+ it "replaces invalid encoding in source using replace even when fallback is given as proc" do
+ encoded = "ã¡\xE3\x81\xFF".encode("UTF-16LE", invalid: :replace, replace: "foo", fallback: proc {|x| "bar"})
+ encoded.should == "\u3061foofoo".encode("UTF-16LE")
+ encoded.encode("UTF-8").should == "ã¡foofoo"
+ end
+ end
+
+ describe "when passed to, from" do
+ it "returns a copy in the destination encoding when both encodings are the same" do
+ str = "ã‚".dup.force_encoding("binary")
+ encoded = str.encode("utf-8", "utf-8")
+
+ encoded.should_not.equal?(str)
+ encoded.should == str.force_encoding("utf-8")
+ encoded.encoding.should == Encoding::UTF_8
+ end
+
+ it "returns the transcoded string" do
+ str = "\x00\x00\x00\x1F"
+ str.encode(Encoding::UTF_8, Encoding::UTF_16BE).should == "\u0000\u001f"
+ end
+ end
+
+ describe "when passed to, options" do
+ it "returns a copy when the destination encoding is the same as the String encoding" do
+ str = "ã‚"
+ encoded = str.encode(Encoding::UTF_8, undef: :replace)
+ encoded.should_not.equal?(str)
+ encoded.should == str
+ end
+ end
+
+ describe "when passed to, from, options" do
+ it "returns a copy when both encodings are the same" do
+ str = "ã‚"
+ encoded = str.encode("utf-8", "utf-8", invalid: :replace)
+ encoded.should_not.equal?(str)
+ encoded.should == str
+ end
+
+ it "returns a copy in the destination encoding when both encodings are the same" do
+ str = "ã‚".dup.force_encoding("binary")
+ encoded = str.encode("utf-8", "utf-8", invalid: :replace)
+
+ encoded.should_not.equal?(str)
+ encoded.should == str.force_encoding("utf-8")
+ encoded.encoding.should == Encoding::UTF_8
+ end
+ end
+end
+
+describe "String#encode!" do
+ before :each do
+ @external = Encoding.default_external
+ @internal = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_external = @external
+ Encoding.default_internal = @internal
+ end
+
+ it_behaves_like :string_encode, :encode!
+
+ it "raises a FrozenError when called on a frozen String" do
+ -> { "foo".freeze.encode!("euc-jp") }.should.raise(FrozenError)
+ end
+
+ # http://redmine.ruby-lang.org/issues/show/1836
+ it "raises a FrozenError when called on a frozen String when it's a no-op" do
+ -> { "foo".freeze.encode!("utf-8") }.should.raise(FrozenError)
+ end
+
+ describe "when passed no options" do
+ it "returns self when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ str = +"ã‚"
+ str.encode!.should.equal?(str)
+ end
+
+ it "returns self for a ASCII-only String when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ str = +"abc"
+ str.encode!.should.equal?(str)
+ end
+ end
+
+ describe "when passed options" do
+ it "returns self for ASCII-only String when Encoding.default_internal is nil" do
+ Encoding.default_internal = nil
+ str = +"abc"
+ str.encode!(invalid: :replace).should.equal?(str)
+ end
+ end
+
+ describe "when passed to encoding" do
+ it "returns self" do
+ str = +"abc"
+ result = str.encode!(Encoding::BINARY)
+ result.encoding.should.equal?(Encoding::BINARY)
+ result.should.equal?(str)
+ end
+ end
+
+ describe "when passed to, from" do
+ it "returns self" do
+ str = +"ã‚ã‚"
+ result = str.encode!("euc-jp", "utf-8")
+ result.encoding.should.equal?(Encoding::EUC_JP)
+ result.should.equal?(str)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/encoding_spec.rb b/spec/ruby/core/string/encoding_spec.rb
new file mode 100644
index 0000000000..aa0e4765ed
--- /dev/null
+++ b/spec/ruby/core/string/encoding_spec.rb
@@ -0,0 +1,184 @@
+# -*- encoding: us-ascii -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/iso-8859-9-encoding'
+
+describe "String#encoding" do
+ it "returns an Encoding object" do
+ String.new.encoding.should.instance_of?(Encoding)
+ end
+
+ it "is equal to the source encoding by default" do
+ s = StringSpecs::ISO88599Encoding.new
+ s.cedilla.encoding.should == s.source_encoding
+ s.cedilla.encode("utf-8").should == 350.chr(Encoding::UTF_8) # S-cedilla
+ end
+
+ it "returns the given encoding if #force_encoding has been called" do
+ "a".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ end
+
+ it "returns the given encoding if #encode!has been called" do
+ "a".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ end
+end
+
+describe "String#encoding for US-ASCII Strings" do
+ it "returns US-ASCII if self is US-ASCII" do
+ "a".encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns US-ASCII if self is US-ASCII only, despite the default internal encoding being different" do
+ default_internal = Encoding.default_internal
+ Encoding.default_internal = Encoding::UTF_8
+ "a".encoding.should == Encoding::US_ASCII
+ Encoding.default_internal = default_internal
+ end
+
+ it "returns US-ASCII if self is US-ASCII only, despite the default external encoding being different" do
+ default_external = Encoding.default_external
+ Encoding.default_external = Encoding::UTF_8
+ "a".encoding.should == Encoding::US_ASCII
+ Encoding.default_external = default_external
+ end
+
+ it "returns US-ASCII if self is US-ASCII only, despite the default internal and external encodings being different" do
+ default_internal = Encoding.default_internal
+ default_external = Encoding.default_external
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
+ "a".encoding.should == Encoding::US_ASCII
+ Encoding.default_external = default_external
+ Encoding.default_internal = default_internal
+ end
+
+ it "returns US-ASCII if self is US-ASCII only, despite the default encodings being different" do
+ default_internal = Encoding.default_internal
+ default_external = Encoding.default_external
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
+ "a".encoding.should == Encoding::US_ASCII
+ Encoding.default_external = default_external
+ Encoding.default_internal = default_internal
+ end
+
+end
+
+describe "String#encoding for Strings with \\u escapes" do
+ it "returns UTF-8" do
+ "\u{4040}".encoding.should == Encoding::UTF_8
+ end
+
+ it "returns US-ASCII if self is US-ASCII only" do
+ s = "\u{40}"
+ s.ascii_only?.should == true
+ s.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns UTF-8 if self isn't US-ASCII only" do
+ s = "\u{4076}\u{619}"
+ s.ascii_only?.should == false
+ s.encoding.should == Encoding::UTF_8
+ end
+
+ it "is not affected by the default internal encoding" do
+ default_internal = Encoding.default_internal
+ Encoding.default_internal = Encoding::ISO_8859_15
+ "\u{5050}".encoding.should == Encoding::UTF_8
+ "\u{50}".encoding.should == Encoding::US_ASCII
+ Encoding.default_internal = default_internal
+ end
+
+ it "is not affected by the default external encoding" do
+ default_external = Encoding.default_external
+ Encoding.default_external = Encoding::SHIFT_JIS
+ "\u{50}".encoding.should == Encoding::US_ASCII
+ "\u{5050}".encoding.should == Encoding::UTF_8
+ Encoding.default_external = default_external
+ end
+
+ it "is not affected by both the default internal and external encoding being set at the same time" do
+ default_internal = Encoding.default_internal
+ default_external = Encoding.default_external
+ Encoding.default_internal = Encoding::EUC_JP
+ Encoding.default_external = Encoding::SHIFT_JIS
+ "\u{50}".encoding.should == Encoding::US_ASCII
+ "\u{507}".encoding.should == Encoding::UTF_8
+ Encoding.default_external = default_external
+ Encoding.default_internal = default_internal
+ end
+
+ it "returns the given encoding if #force_encoding has been called" do
+ "\u{20}".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ "\u{2020}".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ end
+
+ it "returns the given encoding if #encode!has been called" do
+ "\u{20}".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ "\u{2020}".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ end
+end
+
+describe "String#encoding for Strings with \\x escapes" do
+
+ it "returns US-ASCII if self is US-ASCII only" do
+ s = "\x61"
+ s.ascii_only?.should == true
+ s.encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns BINARY when an escape creates a byte with the 8th bit set if the source encoding is US-ASCII" do
+ __ENCODING__.should == Encoding::US_ASCII
+ str = " "
+ str.encoding.should == Encoding::US_ASCII
+ str += [0xDF].pack('C')
+ str.ascii_only?.should == false
+ str.encoding.should == Encoding::BINARY
+ end
+
+ # TODO: Deal with case when the byte in question isn't valid in the source
+ # encoding?
+ it "returns the source encoding when an escape creates a byte with the 8th bit set if the source encoding isn't US-ASCII" do
+ fixture = StringSpecs::ISO88599Encoding.new
+ fixture.source_encoding.should == Encoding::ISO8859_9
+ fixture.x_escape.ascii_only?.should == false
+ fixture.x_escape.encoding.should == Encoding::ISO8859_9
+ end
+
+ it "is not affected by the default internal encoding" do
+ default_internal = Encoding.default_internal
+ Encoding.default_internal = Encoding::ISO_8859_15
+ "\x50".encoding.should == Encoding::US_ASCII
+ "\x50".encoding.should == Encoding::US_ASCII
+ Encoding.default_internal = default_internal
+ end
+
+ it "is not affected by the default external encoding" do
+ default_external = Encoding.default_external
+ Encoding.default_external = Encoding::SHIFT_JIS
+ "\x50".encoding.should == Encoding::US_ASCII
+ [0xD4].pack('C').encoding.should == Encoding::BINARY
+ Encoding.default_external = default_external
+ end
+
+ it "is not affected by both the default internal and external encoding being set at the same time" do
+ default_internal = Encoding.default_internal
+ default_external = Encoding.default_external
+ Encoding.default_internal = Encoding::EUC_JP
+ Encoding.default_external = Encoding::SHIFT_JIS
+ x50 = "\x50"
+ x50.encoding.should == Encoding::US_ASCII
+ [0xD4].pack('C').encoding.should == Encoding::BINARY
+ Encoding.default_external = default_external
+ Encoding.default_internal = default_internal
+ end
+
+ it "returns the given encoding if #force_encoding has been called" do
+ "\x50".dup.force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ [212].pack('C').force_encoding(Encoding::ISO_8859_9).encoding.should == Encoding::ISO_8859_9
+ end
+
+ it "returns the given encoding if #encode!has been called" do
+ "\x50".dup.encode!(Encoding::SHIFT_JIS).encoding.should == Encoding::SHIFT_JIS
+ "x\00".dup.encode!(Encoding::UTF_8).encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/string/end_with_spec.rb b/spec/ruby/core/string/end_with_spec.rb
new file mode 100644
index 0000000000..ac4fff72ad
--- /dev/null
+++ b/spec/ruby/core/string/end_with_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/string/end_with'
+
+describe "String#end_with?" do
+ it_behaves_like :end_with, :to_s
+end
diff --git a/spec/ruby/core/string/eql_spec.rb b/spec/ruby/core/string/eql_spec.rb
new file mode 100644
index 0000000000..46ebeda509
--- /dev/null
+++ b/spec/ruby/core/string/eql_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+
+describe "String#eql?" do
+ it_behaves_like :string_eql_value, :eql?
+
+ describe "when given a non-String" do
+ it "returns false" do
+ 'hello'.should_not.eql?(5)
+ not_supported_on :opal do
+ 'hello'.should_not.eql?(:hello)
+ end
+ 'hello'.should_not.eql?(mock('x'))
+ end
+
+ it "does not try to call #to_str on the given argument" do
+ (obj = mock('x')).should_not_receive(:to_str)
+ 'hello'.should_not.eql?(obj)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/equal_value_spec.rb b/spec/ruby/core/string/equal_value_spec.rb
new file mode 100644
index 0000000000..b9c9c372f8
--- /dev/null
+++ b/spec/ruby/core/string/equal_value_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'shared/eql'
+require_relative 'shared/equal_value'
+
+describe "String#==" do
+ it_behaves_like :string_eql_value, :==
+ it_behaves_like :string_equal_value, :==
+end
diff --git a/spec/ruby/core/string/fixtures/classes.rb b/spec/ruby/core/string/fixtures/classes.rb
new file mode 100644
index 0000000000..26fcd51b5d
--- /dev/null
+++ b/spec/ruby/core/string/fixtures/classes.rb
@@ -0,0 +1,60 @@
+class Object
+ # This helper is defined here rather than in MSpec because
+ # it is only used in #unpack specs.
+ def unpack_format(count=nil, repeat=nil)
+ format = "#{instance_variable_get(:@method)}#{count}"
+ format *= repeat if repeat
+ format.dup # because it may then become tainted
+ end
+end
+
+module StringSpecs
+ class MyString < String; end
+ class MyArray < Array; end
+ class MyRange < Range; end
+
+ class SubString < String
+ attr_reader :special
+
+ def initialize(str=nil)
+ @special = str
+ end
+ end
+
+ class InitializeString < String
+ attr_reader :ivar
+
+ def initialize(other)
+ super
+ @ivar = 1
+ end
+
+ def initialize_copy(other)
+ ScratchPad.record object_id
+ end
+ end
+
+ module StringModule
+ def repr
+ 1
+ end
+ end
+
+ class StringWithRaisingConstructor < String
+ def initialize(str)
+ raise ArgumentError.new('constructor was called') unless str == 'silly:string'
+ self.replace(str)
+ end
+ end
+
+ class SpecialVarProcessor
+ def process(match)
+ if $~ != nil
+ str = $~[0]
+ else
+ str = "unset"
+ end
+ "<#{str}>"
+ end
+ end
+end
diff --git a/spec/ruby/core/string/fixtures/freeze_magic_comment.rb b/spec/ruby/core/string/fixtures/freeze_magic_comment.rb
new file mode 100644
index 0000000000..2b87a16328
--- /dev/null
+++ b/spec/ruby/core/string/fixtures/freeze_magic_comment.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+print (+ 'frozen string').frozen? ? 'immutable' : 'mutable'
diff --git a/spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb b/spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb
new file mode 100644
index 0000000000..cfa91dedc3
--- /dev/null
+++ b/spec/ruby/core/string/fixtures/iso-8859-9-encoding.rb
@@ -0,0 +1,9 @@
+# -*- encoding: iso-8859-9 -*-
+module StringSpecs
+ class ISO88599Encoding
+ def source_encoding; __ENCODING__; end
+ def x_escape; [0xDF].pack('C').force_encoding("iso-8859-9"); end
+ def ascii_only; "glark"; end
+ def cedilla; "Þ"; end # S-cedilla
+ end
+end
diff --git a/spec/ruby/core/string/fixtures/to_c.rb b/spec/ruby/core/string/fixtures/to_c.rb
new file mode 100644
index 0000000000..7776933263
--- /dev/null
+++ b/spec/ruby/core/string/fixtures/to_c.rb
@@ -0,0 +1,5 @@
+module StringSpecs
+ def self.to_c_method(string)
+ string.to_c
+ end
+end
diff --git a/spec/ruby/core/string/force_encoding_spec.rb b/spec/ruby/core/string/force_encoding_spec.rb
new file mode 100644
index 0000000000..fc6914213f
--- /dev/null
+++ b/spec/ruby/core/string/force_encoding_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#force_encoding" do
+ it "accepts a String as the name of an Encoding" do
+ "abc".force_encoding('shift_jis').encoding.should == Encoding::Shift_JIS
+ end
+
+ describe "with a special encoding name" do
+ before :each do
+ @original_encoding = Encoding.default_internal
+ end
+
+ after :each do
+ Encoding.default_internal = @original_encoding
+ end
+
+ it "accepts valid special encoding names" do
+ Encoding.default_internal = "US-ASCII"
+ "abc".force_encoding("internal").encoding.should == Encoding::US_ASCII
+ end
+
+ it "defaults to BINARY if special encoding name is not set" do
+ Encoding.default_internal = nil
+ "abc".force_encoding("internal").encoding.should == Encoding::BINARY
+ end
+ end
+
+ it "accepts an Encoding instance" do
+ "abc".force_encoding(Encoding::SHIFT_JIS).encoding.should == Encoding::Shift_JIS
+ end
+
+ it "calls #to_str to convert an object to an encoding name" do
+ obj = mock("force_encoding")
+ obj.should_receive(:to_str).and_return("utf-8")
+
+ "abc".force_encoding(obj).encoding.should == Encoding::UTF_8
+ end
+
+ it "raises a TypeError if #to_str does not return a String" do
+ obj = mock("force_encoding")
+ obj.should_receive(:to_str).and_return(1)
+
+ -> { "abc".force_encoding(obj) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed nil" do
+ -> { "abc".force_encoding(nil) }.should.raise(TypeError)
+ end
+
+ it "returns self" do
+ str = "abc"
+ str.force_encoding('utf-8').should.equal?(str)
+ end
+
+ it "sets the encoding even if the String contents are invalid in that encoding" do
+ str = "\u{9765}"
+ str.force_encoding('euc-jp')
+ str.encoding.should == Encoding::EUC_JP
+ str.valid_encoding?.should == false
+ end
+
+ it "does not transcode self" do
+ str = "é"
+ str.dup.force_encoding('utf-16le').should_not == str.encode('utf-16le')
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ str = "abcd".freeze
+ -> { str.force_encoding(str.encoding) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/freeze_spec.rb b/spec/ruby/core/string/freeze_spec.rb
new file mode 100644
index 0000000000..8485e8de21
--- /dev/null
+++ b/spec/ruby/core/string/freeze_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#freeze" do
+
+ it "produces the same object whenever called on an instance of a literal in the source" do
+ "abc".freeze.should.equal? "abc".freeze
+ end
+
+ it "doesn't produce the same object for different instances of literals in the source" do
+ "abc".should_not.equal? "abc"
+ end
+
+ it "being a special form doesn't change the value of defined?" do
+ defined?("abc".freeze).should == "method"
+ end
+
+end
diff --git a/spec/ruby/core/string/getbyte_spec.rb b/spec/ruby/core/string/getbyte_spec.rb
new file mode 100644
index 0000000000..d4619dca61
--- /dev/null
+++ b/spec/ruby/core/string/getbyte_spec.rb
@@ -0,0 +1,69 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "String#getbyte" do
+ it "returns an Integer if given a valid index" do
+ "a".getbyte(0).should.is_a?(Integer)
+ end
+
+ it "starts indexing at 0" do
+ "b".getbyte(0).should == 98
+
+ # copy-on-write case
+ _str1, str2 = "fooXbar".split("X")
+ str2.getbyte(0).should == 98
+ end
+
+ it "counts from the end of the String if given a negative argument" do
+ "glark".getbyte(-1).should == "glark".getbyte(4)
+
+ # copy-on-write case
+ _str1, str2 = "fooXbar".split("X")
+ str2.getbyte(-1).should == 114
+ end
+
+ it "returns an Integer between 0 and 255" do
+ "\x00".getbyte(0).should == 0
+ [0xFF].pack('C').getbyte(0).should == 255
+ 256.chr('utf-8').getbyte(0).should == 196
+ 256.chr('utf-8').getbyte(1).should == 128
+ end
+
+ it "regards a multi-byte character as having multiple bytes" do
+ chr = "\u{998}"
+ chr.bytesize.should == 3
+ chr.getbyte(0).should == 224
+ chr.getbyte(1).should == 166
+ chr.getbyte(2).should == 152
+ end
+
+ it "mirrors the output of #bytes" do
+ xDE = [0xDE].pack('C').force_encoding('utf-8')
+ str = "UTF-8 (\u{9865}} characters and hex escapes (#{xDE})"
+ str.bytes.to_a.each_with_index do |byte, index|
+ str.getbyte(index).should == byte
+ end
+ end
+
+ it "interprets bytes relative to the String's encoding" do
+ str = "\u{333}"
+ str.encode('utf-8').getbyte(0).should_not == str.encode('utf-16le').getbyte(0)
+ end
+
+ it "returns nil for out-of-bound indexes" do
+ "g".getbyte(1).should == nil
+ end
+
+ it "regards the empty String as containing no bytes" do
+ "".getbyte(0).should == nil
+ end
+
+ it "raises an ArgumentError unless given one argument" do
+ -> { "glark".getbyte }.should.raise(ArgumentError)
+ -> { "food".getbyte(0,0) }.should.raise(ArgumentError)
+ end
+
+ it "raises a TypeError unless its argument can be coerced into an Integer" do
+ -> { "a".getbyte('a') }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/string/grapheme_clusters_spec.rb b/spec/ruby/core/string/grapheme_clusters_spec.rb
new file mode 100644
index 0000000000..380a245083
--- /dev/null
+++ b/spec/ruby/core/string/grapheme_clusters_spec.rb
@@ -0,0 +1,14 @@
+require_relative "../../spec_helper"
+require_relative 'shared/chars'
+require_relative 'shared/grapheme_clusters'
+
+describe "String#grapheme_clusters" do
+ it_behaves_like :string_chars, :grapheme_clusters
+ it_behaves_like :string_grapheme_clusters, :grapheme_clusters
+
+ it "returns an array when no block given" do
+ string = "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}"
+ string.grapheme_clusters.should == ['a', 'b', "\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}", "\u{1F43E}"]
+
+ end
+end
diff --git a/spec/ruby/core/string/gsub_spec.rb b/spec/ruby/core/string/gsub_spec.rb
new file mode 100644
index 0000000000..d0e1c30bd2
--- /dev/null
+++ b/spec/ruby/core/string/gsub_spec.rb
@@ -0,0 +1,615 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :string_gsub_named_capture, shared: true do
+ it "replaces \\k named backreferences with the regexp's corresponding capture" do
+ str = "hello"
+
+ str.gsub(/(?<foo>[aeiou])/, '<\k<foo>>').should == "h<e>ll<o>"
+ str.gsub(/(?<foo>.)/, '\k<foo>\k<foo>').should == "hheelllloo"
+ end
+end
+
+describe "String#gsub with pattern and replacement" do
+ it "inserts the replacement around every character when the pattern collapses" do
+ "hello".gsub(//, ".").should == ".h.e.l.l.o."
+ end
+
+ it "respects unicode when the pattern collapses" do
+ str = "ã“ã«ã¡ã‚"
+ reg = %r!!
+
+ str.gsub(reg, ".").should == ".ã“.ã«.ã¡.ã‚."
+ end
+
+ it "doesn't freak out when replacing ^" do
+ "Text\n".gsub(/^/, ' ').should == " Text\n"
+ "Text\nFoo".gsub(/^/, ' ').should == " Text\n Foo"
+ end
+
+ it "returns a copy of self with all occurrences of pattern replaced with replacement" do
+ "hello".gsub(/[aeiou]/, '*').should == "h*ll*"
+
+ str = "hello homely world. hah!"
+ str.gsub(/\Ah\S+\s*/, "huh? ").should == "huh? homely world. hah!"
+
+ str = "¿por qué?"
+ str.gsub(/([a-z\d]*)/, "*").should == "*¿** **é*?*"
+ end
+
+ it "ignores a block if supplied" do
+ "food".gsub(/f/, "g") { "w" }.should == "good"
+ end
+
+ it "supports \\G which matches at the beginning of the remaining (non-matched) string" do
+ str = "hello homely world. hah!"
+ str.gsub(/\Gh\S+\s*/, "huh? ").should == "huh? huh? world. hah!"
+ end
+
+ it "supports /i for ignoring case" do
+ str = "Hello. How happy are you?"
+ str.gsub(/h/i, "j").should == "jello. jow jappy are you?"
+ str.gsub(/H/i, "j").should == "jello. jow jappy are you?"
+ end
+
+ it "doesn't interpret regexp metacharacters if pattern is a string" do
+ "12345".gsub('\d', 'a').should == "12345"
+ '\d'.gsub('\d', 'a').should == "a"
+ end
+
+ it "replaces \\1 sequences with the regexp's corresponding capture" do
+ str = "hello"
+
+ str.gsub(/([aeiou])/, '<\1>').should == "h<e>ll<o>"
+ str.gsub(/(.)/, '\1\1').should == "hheelllloo"
+
+ str.gsub(/.(.?)/, '<\0>(\1)').should == "<he>(e)<ll>(l)<o>()"
+
+ str.gsub(/.(.)+/, '\1').should == "o"
+
+ str = "ABCDEFGHIJKLabcdefghijkl"
+ re = /#{"(.)" * 12}/
+ str.gsub(re, '\1').should == "Aa"
+ str.gsub(re, '\9').should == "Ii"
+ # Only the first 9 captures can be accessed in MRI
+ str.gsub(re, '\10').should == "A0a0"
+ end
+
+ it "treats \\1 sequences without corresponding captures as empty strings" do
+ str = "hello!"
+
+ str.gsub("", '<\1>').should == "<>h<>e<>l<>l<>o<>!<>"
+ str.gsub("h", '<\1>').should == "<>ello!"
+
+ str.gsub(//, '<\1>').should == "<>h<>e<>l<>l<>o<>!<>"
+ str.gsub(/./, '\1\2\3').should == ""
+ str.gsub(/.(.{20})?/, '\1').should == ""
+ end
+
+ it "replaces \\& and \\0 with the complete match" do
+ str = "hello!"
+
+ str.gsub("", '<\0>').should == "<>h<>e<>l<>l<>o<>!<>"
+ str.gsub("", '<\&>').should == "<>h<>e<>l<>l<>o<>!<>"
+ str.gsub("he", '<\0>').should == "<he>llo!"
+ str.gsub("he", '<\&>').should == "<he>llo!"
+ str.gsub("l", '<\0>').should == "he<l><l>o!"
+ str.gsub("l", '<\&>').should == "he<l><l>o!"
+
+ str.gsub(//, '<\0>').should == "<>h<>e<>l<>l<>o<>!<>"
+ str.gsub(//, '<\&>').should == "<>h<>e<>l<>l<>o<>!<>"
+ str.gsub(/../, '<\0>').should == "<he><ll><o!>"
+ str.gsub(/../, '<\&>').should == "<he><ll><o!>"
+ str.gsub(/(.)./, '<\0>').should == "<he><ll><o!>"
+ end
+
+ it "replaces \\` with everything before the current match" do
+ str = "hello!"
+
+ str.gsub("", '<\`>').should == "<>h<h>e<he>l<hel>l<hell>o<hello>!<hello!>"
+ str.gsub("h", '<\`>').should == "<>ello!"
+ str.gsub("l", '<\`>').should == "he<he><hel>o!"
+ str.gsub("!", '<\`>').should == "hello<hello>"
+
+ str.gsub(//, '<\`>').should == "<>h<h>e<he>l<hel>l<hell>o<hello>!<hello!>"
+ str.gsub(/../, '<\`>').should == "<><he><hell>"
+ end
+
+ it "replaces \\' with everything after the current match" do
+ str = "hello!"
+
+ str.gsub("", '<\\\'>').should == "<hello!>h<ello!>e<llo!>l<lo!>l<o!>o<!>!<>"
+ str.gsub("h", '<\\\'>').should == "<ello!>ello!"
+ str.gsub("ll", '<\\\'>').should == "he<o!>o!"
+ str.gsub("!", '<\\\'>').should == "hello<>"
+
+ str.gsub(//, '<\\\'>').should == "<hello!>h<ello!>e<llo!>l<lo!>l<o!>o<!>!<>"
+ str.gsub(/../, '<\\\'>').should == "<llo!><o!><>"
+ end
+
+ it "replaces \\+ with the last paren that actually matched" do
+ str = "hello!"
+
+ str.gsub(/(.)(.)/, '\+').should == "el!"
+ str.gsub(/(.)(.)+/, '\+').should == "!"
+ str.gsub(/(.)()/, '\+').should == ""
+ str.gsub(/(.)(.{20})?/, '<\+>').should == "<h><e><l><l><o><!>"
+
+ str = "ABCDEFGHIJKLabcdefghijkl"
+ re = /#{"(.)" * 12}/
+ str.gsub(re, '\+').should == "Ll"
+ end
+
+ it "treats \\+ as an empty string if there was no captures" do
+ "hello!".gsub(/./, '\+').should == ""
+ end
+
+ it "maps \\\\ in replacement to \\" do
+ "hello".gsub(/./, '\\\\').should == '\\' * 5
+ end
+
+ it "leaves unknown \\x escapes in replacement untouched" do
+ "hello".gsub(/./, '\\x').should == '\\x' * 5
+ "hello".gsub(/./, '\\y').should == '\\y' * 5
+ end
+
+ it "leaves \\ at the end of replacement untouched" do
+ "hello".gsub(/./, 'hah\\').should == 'hah\\' * 5
+ end
+
+ it_behaves_like :string_gsub_named_capture, :gsub
+
+ it "handles pattern collapse" do
+ str = "ã“ã«ã¡ã‚"
+ reg = %r!!
+ str.gsub(reg, ".").should == ".ã“.ã«.ã¡.ã‚."
+ end
+
+ it "tries to convert pattern to a string using to_str" do
+ pattern = mock('.')
+ def pattern.to_str() "." end
+
+ "hello.".gsub(pattern, "!").should == "hello!"
+ end
+
+ it "raises a TypeError when pattern can't be converted to a string" do
+ -> { "hello".gsub([], "x") }.should.raise(TypeError)
+ -> { "hello".gsub(Object.new, "x") }.should.raise(TypeError)
+ -> { "hello".gsub(nil, "x") }.should.raise(TypeError)
+ end
+
+ it "tries to convert replacement to a string using to_str" do
+ replacement = mock('hello_replacement')
+ def replacement.to_str() "hello_replacement" end
+
+ "hello".gsub(/hello/, replacement).should == "hello_replacement"
+ end
+
+ it "raises a TypeError when replacement can't be converted to a string" do
+ -> { "hello".gsub(/[aeiou]/, []) }.should.raise(TypeError)
+ -> { "hello".gsub(/[aeiou]/, Object.new) }.should.raise(TypeError)
+ -> { "hello".gsub(/[aeiou]/, nil) }.should.raise(TypeError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("").gsub(//, "").should.instance_of?(String)
+ StringSpecs::MyString.new("").gsub(/foo/, "").should.instance_of?(String)
+ StringSpecs::MyString.new("foo").gsub(/foo/, "").should.instance_of?(String)
+ StringSpecs::MyString.new("foo").gsub("foo", "").should.instance_of?(String)
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none" do
+ 'hello.'.gsub('hello', 'x')
+ $~[0].should == 'hello'
+
+ 'hello.'.gsub('not', 'x')
+ $~.should == nil
+
+ 'hello.'.gsub(/.(.)/, 'x')
+ $~[0].should == 'o.'
+
+ 'hello.'.gsub(/not/, 'x')
+ $~.should == nil
+ end
+
+ it "handles a pattern in a superset encoding" do
+ result = 'abc'.force_encoding(Encoding::US_ASCII).gsub('é', 'è')
+ result.should == 'abc'
+ result.encoding.should == Encoding::US_ASCII
+ end
+
+ it "handles a pattern in a subset encoding" do
+ result = 'été'.gsub('t'.force_encoding(Encoding::US_ASCII), 'u')
+ result.should == 'éué'
+ result.encoding.should == Encoding::UTF_8
+ end
+end
+
+describe "String#gsub with pattern and Hash" do
+ it "returns a copy of self with all occurrences of pattern replaced with the value of the corresponding hash key" do
+ "hello".gsub(/./, 'l' => 'L').should == "LL"
+ "hello!".gsub(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she said'
+ "hello".gsub('l', 'l' => 'el').should == 'heelelo'
+ end
+
+ it "ignores keys that don't correspond to matches" do
+ "hello".gsub(/./, 'z' => 'L', 'h' => 'b', 'o' => 'ow').should == "bow"
+ end
+
+ it "returns an empty string if the pattern matches but the hash specifies no replacements" do
+ "hello".gsub(/./, 'z' => 'L').should == ""
+ end
+
+ it "ignores non-String keys" do
+ "tattoo".gsub(/(tt)/, 'tt' => 'b', tt: 'z').should == "taboo"
+ end
+
+ it "uses a key's value as many times as needed" do
+ "food".gsub(/o/, 'o' => '0').should == "f00d"
+ end
+
+ it "uses the hash's default value for missing keys" do
+ hsh = {}
+ hsh.default='?'
+ hsh['o'] = '0'
+ "food".gsub(/./, hsh).should == "?00?"
+ end
+
+ it "coerces the hash values with #to_s" do
+ hsh = {}
+ hsh.default=[]
+ hsh['o'] = 0
+ obj = mock('!')
+ obj.should_receive(:to_s).and_return('!')
+ hsh['!'] = obj
+ "food!".gsub(/./, hsh).should == "[]00[]!"
+ end
+
+ it "uses the hash's value set from default_proc for missing keys" do
+ hsh = {}
+ hsh.default_proc = -> k, v { 'lamb' }
+ "food!".gsub(/./, hsh).should == "lamblamblamblamblamb"
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none for access from outside" do
+ 'hello.'.gsub('l', 'l' => 'L')
+ $~.begin(0).should == 3
+ $~[0].should == 'l'
+
+ 'hello.'.gsub('not', 'ot' => 'to')
+ $~.should == nil
+
+ 'hello.'.gsub(/.(.)/, 'o' => ' hole')
+ $~[0].should == 'o.'
+
+ 'hello.'.gsub(/not/, 'z' => 'glark')
+ $~.should == nil
+ end
+
+ it "doesn't interpolate special sequences like \\1 for the block's return value" do
+ repl = '\& \0 \1 \` \\\' \+ \\\\ foo'
+ "hello".gsub(/(.+)/, 'hello' => repl ).should == repl
+ end
+end
+
+describe "String#gsub! with pattern and Hash" do
+
+ it "returns self with all occurrences of pattern replaced with the value of the corresponding hash key" do
+ "hello".gsub!(/./, 'l' => 'L').should == "LL"
+ "hello!".gsub!(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she said'
+ "hello".gsub!('l', 'l' => 'el').should == 'heelelo'
+ end
+
+ it "ignores keys that don't correspond to matches" do
+ "hello".gsub!(/./, 'z' => 'L', 'h' => 'b', 'o' => 'ow').should == "bow"
+ end
+
+ it "replaces self with an empty string if the pattern matches but the hash specifies no replacements" do
+ "hello".gsub!(/./, 'z' => 'L').should == ""
+ end
+
+ it "ignores non-String keys" do
+ "hello".gsub!(/(ll)/, 'll' => 'r', ll: 'z').should == "hero"
+ end
+
+ it "uses a key's value as many times as needed" do
+ "food".gsub!(/o/, 'o' => '0').should == "f00d"
+ end
+
+ it "uses the hash's default value for missing keys" do
+ hsh = {}
+ hsh.default='?'
+ hsh['o'] = '0'
+ "food".gsub!(/./, hsh).should == "?00?"
+ end
+
+ it "coerces the hash values with #to_s" do
+ hsh = {}
+ hsh.default=[]
+ hsh['o'] = 0
+ obj = mock('!')
+ obj.should_receive(:to_s).and_return('!')
+ hsh['!'] = obj
+ "food!".gsub!(/./, hsh).should == "[]00[]!"
+ end
+
+ it "uses the hash's value set from default_proc for missing keys" do
+ hsh = {}
+ hsh.default_proc = -> k, v { 'lamb' }
+ "food!".gsub!(/./, hsh).should == "lamblamblamblamblamb"
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none for access from outside" do
+ 'hello.'.gsub!('l', 'l' => 'L')
+ $~.begin(0).should == 3
+ $~[0].should == 'l'
+
+ 'hello.'.gsub!('not', 'ot' => 'to')
+ $~.should == nil
+
+ 'hello.'.gsub!(/.(.)/, 'o' => ' hole')
+ $~[0].should == 'o.'
+
+ 'hello.'.gsub!(/not/, 'z' => 'glark')
+ $~.should == nil
+ end
+
+ it "doesn't interpolate special sequences like \\1 for the block's return value" do
+ repl = '\& \0 \1 \` \\\' \+ \\\\ foo'
+ "hello".gsub!(/(.+)/, 'hello' => repl ).should == repl
+ end
+end
+
+describe "String#gsub with pattern and block" do
+ it "returns a copy of self with all occurrences of pattern replaced with the block's return value" do
+ "hello".gsub(/./) { |s| s.succ + ' ' }.should == "i f m m p "
+ "hello!".gsub(/(.)(.)/) { |*a| a.inspect }.should == '["he"]["ll"]["o!"]'
+ "hello".gsub('l') { 'x'}.should == 'hexxo'
+ end
+
+ it "sets $~ for access from the block" do
+ str = "hello"
+ str.gsub(/([aeiou])/) { "<#{$~[1]}>" }.should == "h<e>ll<o>"
+ str.gsub(/([aeiou])/) { "<#{$1}>" }.should == "h<e>ll<o>"
+ str.gsub("l") { "<#{$~[0]}>" }.should == "he<l><l>o"
+
+ offsets = []
+
+ str.gsub(/([aeiou])/) do
+ md = $~
+ md.string.should == str
+ offsets << md.offset(0)
+ str
+ end.should == "hhellollhello"
+
+ offsets.should == [[1, 2], [4, 5]]
+ end
+
+ it "does not set $~ for procs created from methods" do
+ str = "hello"
+ str.gsub("l", &StringSpecs::SpecialVarProcessor.new.method(:process)).should == "he<unset><unset>o"
+ end
+
+ it "restores $~ after leaving the block" do
+ [/./, "l"].each do |pattern|
+ old_md = nil
+ "hello".gsub(pattern) do
+ old_md = $~
+ "ok".match(/./)
+ "x"
+ end
+
+ $~[0].should == old_md[0]
+ $~.string.should == "hello"
+ end
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none for access from outside" do
+ 'hello.'.gsub('l') { 'x' }
+ $~.begin(0).should == 3
+ $~[0].should == 'l'
+
+ 'hello.'.gsub('not') { 'x' }
+ $~.should == nil
+
+ 'hello.'.gsub(/.(.)/) { 'x' }
+ $~[0].should == 'o.'
+
+ 'hello.'.gsub(/not/) { 'x' }
+ $~.should == nil
+ end
+
+ it "doesn't interpolate special sequences like \\1 for the block's return value" do
+ repl = '\& \0 \1 \` \\\' \+ \\\\ foo'
+ "hello".gsub(/(.+)/) { repl }.should == repl
+ end
+
+ it "converts the block's return value to a string using to_s" do
+ replacement = mock('hello_replacement')
+ def replacement.to_s() "hello_replacement" end
+
+ "hello".gsub(/hello/) { replacement }.should == "hello_replacement"
+
+ obj = mock('ok')
+ def obj.to_s() "ok" end
+
+ "hello".gsub(/.+/) { obj }.should == "ok"
+ end
+
+ it "uses the compatible encoding if they are compatible" do
+ s = "hello"
+ s2 = "#{195.chr}#{192.chr}#{195.chr}"
+
+ s.gsub(/l/) { |bar| 195.chr }.encoding.should == Encoding::BINARY
+ s2.gsub("#{192.chr}") { |bar| "hello" }.encoding.should == Encoding::BINARY
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are not compatible" do
+ s = "hllëllo"
+ s2 = "hellö"
+
+ -> { s.gsub(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should.raise(Encoding::CompatibilityError)
+ -> { s2.gsub(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "replaces the incompatible part properly even if the encodings are not compatible" do
+ s = "hllëllo"
+
+ s.gsub(/ë/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") }.encoding.should == Encoding::ISO_8859_5
+ end
+
+ not_supported_on :opal do
+ it "raises an ArgumentError if encoding is not valid" do
+ x92 = [0x92].pack('C').force_encoding('utf-8')
+ -> { "a#{x92}b".gsub(/[^\x00-\x7f]/u, '') }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe "String#gsub with pattern and without replacement and block" do
+ it "returns an enumerator" do
+ enum = "abca".gsub(/a/)
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == ["a", "a"]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ "abca".gsub(/a/).size.should == nil
+ end
+ end
+ end
+end
+
+describe "String#gsub with a string pattern" do
+ it "handles multibyte characters" do
+ "é".gsub("é", "â").should == "â"
+ "aé".gsub("é", "â").should == "aâ"
+ "éa".gsub("é", "â").should == "âa"
+ end
+end
+
+describe "String#gsub! with pattern and replacement" do
+ it "modifies self in place and returns self" do
+ a = "hello"
+ a.gsub!(/[aeiou]/, '*').should.equal?(a)
+ a.should == "h*ll*"
+ end
+
+ it "modifies self in place with multi-byte characters and returns self" do
+ a = "¿por qué?"
+ a.gsub!(/([a-z\d]*)/, "*").should.equal?(a)
+ a.should == "*¿** **é*?*"
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.gsub!(/z/, '*').should == nil
+ a.gsub!(/z/, 'z').should == nil
+ a.should == "hello"
+ end
+
+ # See [ruby-core:23666]
+ it "raises a FrozenError when self is frozen" do
+ s = "hello"
+ s.freeze
+
+ -> { s.gsub!(/ROAR/, "x") }.should.raise(FrozenError)
+ -> { s.gsub!(/e/, "e") }.should.raise(FrozenError)
+ -> { s.gsub!(/[aeiou]/, '*') }.should.raise(FrozenError)
+ end
+
+ it "handles a pattern in a superset encoding" do
+ string = 'abc'.force_encoding(Encoding::US_ASCII)
+
+ result = string.gsub!('é', 'è')
+
+ result.should == nil
+ string.should == 'abc'
+ string.encoding.should == Encoding::US_ASCII
+ end
+
+ it "handles a pattern in a subset encoding" do
+ string = 'été'
+ pattern = 't'.force_encoding(Encoding::US_ASCII)
+
+ result = string.gsub!(pattern, 'u')
+
+ result.should == string
+ string.should == 'éué'
+ string.encoding.should == Encoding::UTF_8
+ end
+end
+
+describe "String#gsub! with pattern and block" do
+ it "modifies self in place and returns self" do
+ a = "hello"
+ a.gsub!(/[aeiou]/) { '*' }.should.equal?(a)
+ a.should == "h*ll*"
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.gsub!(/z/) { '*' }.should == nil
+ a.gsub!(/z/) { 'z' }.should == nil
+ a.should == "hello"
+ end
+
+ # See [ruby-core:23663]
+ it "raises a FrozenError when self is frozen" do
+ s = "hello"
+ s.freeze
+
+ -> { s.gsub!(/ROAR/) { "x" } }.should.raise(FrozenError)
+ -> { s.gsub!(/e/) { "e" } }.should.raise(FrozenError)
+ -> { s.gsub!(/[aeiou]/) { '*' } }.should.raise(FrozenError)
+ end
+
+ it "uses the compatible encoding if they are compatible" do
+ s = "hello"
+ s2 = "#{195.chr}#{192.chr}#{195.chr}"
+
+ s.gsub!(/l/) { |bar| 195.chr }.encoding.should == Encoding::BINARY
+ s2.gsub!("#{192.chr}") { |bar| "hello" }.encoding.should == Encoding::BINARY
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are not compatible" do
+ s = "hllëllo"
+ s2 = "hellö"
+
+ -> { s.gsub!(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should.raise(Encoding::CompatibilityError)
+ -> { s2.gsub!(/l/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") } }.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "replaces the incompatible part properly even if the encodings are not compatible" do
+ s = "hllëllo"
+
+ s.gsub!(/ë/) { |bar| "РуÑÑкий".force_encoding("iso-8859-5") }.encoding.should == Encoding::ISO_8859_5
+ end
+
+ not_supported_on :opal do
+ it "raises an ArgumentError if encoding is not valid" do
+ x92 = [0x92].pack('C').force_encoding('utf-8')
+ -> { "a#{x92}b".gsub!(/[^\x00-\x7f]/u, '') }.should.raise(ArgumentError)
+ end
+ end
+end
+
+describe "String#gsub! with pattern and without replacement and block" do
+ it "returns an enumerator" do
+ enum = "abca".gsub!(/a/)
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == ["a", "a"]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ "abca".gsub!(/a/).size.should == nil
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/hash_spec.rb b/spec/ruby/core/string/hash_spec.rb
new file mode 100644
index 0000000000..0b26214b55
--- /dev/null
+++ b/spec/ruby/core/string/hash_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#hash" do
+ it "returns a hash based on a string's length and content" do
+ "abc".hash.should == "abc".hash
+ "abc".hash.should_not == "cba".hash
+ end
+end
diff --git a/spec/ruby/core/string/hex_spec.rb b/spec/ruby/core/string/hex_spec.rb
new file mode 100644
index 0000000000..364e915681
--- /dev/null
+++ b/spec/ruby/core/string/hex_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# TODO: Move actual results to String#to_int() and spec in terms of it
+describe "String#hex" do
+ it "treats leading characters of self as a string of hex digits" do
+ "0a".hex.should == 10
+ "0o".hex.should == 0
+ "0x".hex.should == 0
+ "A_BAD_BABE".hex.should == 0xABADBABE
+ "0b1010".hex.should == "b1010".hex
+ "0d500".hex.should == "d500".hex
+ "abcdefG".hex.should == 0xabcdef
+ end
+
+ it "does not accept a sequence of underscores as part of a number" do
+ "a__b".hex.should == 0xa
+ "a____b".hex.should == 0xa
+ "a___f".hex.should == 0xa
+ end
+
+ it "takes an optional sign" do
+ "-1234".hex.should == -4660
+ "+1234".hex.should == 4660
+ end
+
+ it "takes an optional 0x" do
+ "0x0a".hex.should == 10
+ "0a".hex.should == 10
+ end
+
+ it "requires that the sign is in front of the 0x if present" do
+ "-0x1".hex.should == -1
+ "0x-1".hex.should == 0
+ end
+
+ it "returns 0 on error" do
+ "".hex.should == 0
+ "+-5".hex.should == 0
+ "wombat".hex.should == 0
+ "0x0x42".hex.should == 0
+ end
+
+ it "returns 0 if sequence begins with underscore" do
+ "_a".hex.should == 0
+ "___b".hex.should == 0
+ "___0xc".hex.should == 0
+ end
+end
diff --git a/spec/ruby/core/string/include_spec.rb b/spec/ruby/core/string/include_spec.rb
new file mode 100644
index 0000000000..d943430335
--- /dev/null
+++ b/spec/ruby/core/string/include_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#include? with String" do
+ it "returns true if self contains other_str" do
+ "hello".include?("lo").should == true
+ "hello".include?("ol").should == false
+ end
+
+ it "ignores subclass differences" do
+ "hello".include?(StringSpecs::MyString.new("lo")).should == true
+ StringSpecs::MyString.new("hello").include?("lo").should == true
+ StringSpecs::MyString.new("hello").include?(StringSpecs::MyString.new("lo")).should == true
+ end
+
+ it "returns true if both strings are empty" do
+ "".should.include?("")
+ "".dup.force_encoding("EUC-JP").should.include?("")
+ "".should.include?("".dup.force_encoding("EUC-JP"))
+ "".dup.force_encoding("EUC-JP").should.include?("".dup.force_encoding("EUC-JP"))
+ end
+
+ it "returns true if the RHS is empty" do
+ "a".should.include?("")
+ "a".dup.force_encoding("EUC-JP").should.include?("")
+ "a".should.include?("".dup.force_encoding("EUC-JP"))
+ "a".dup.force_encoding("EUC-JP").should.include?("".dup.force_encoding("EUC-JP"))
+ end
+
+ it "tries to convert other to string using to_str" do
+ other = mock('lo')
+ other.should_receive(:to_str).and_return("lo")
+
+ "hello".include?(other).should == true
+ end
+
+ it "raises a TypeError if other can't be converted to string" do
+ -> { "hello".include?([]) }.should.raise(TypeError)
+ -> { "hello".include?('h'.ord) }.should.raise(TypeError)
+ -> { "hello".include?(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ pat = "ã‚¢".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".include?(pat)
+ end.should.raise(Encoding::CompatibilityError)
+ end
+end
diff --git a/spec/ruby/core/string/index_spec.rb b/spec/ruby/core/string/index_spec.rb
new file mode 100644
index 0000000000..3f82181b98
--- /dev/null
+++ b/spec/ruby/core/string/index_spec.rb
@@ -0,0 +1,337 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#index" do
+ it "raises a TypeError if passed nil" do
+ -> { "abc".index nil }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a boolean" do
+ -> { "abc".index true }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a Symbol" do
+ -> { "abc".index :a }.should.raise(TypeError)
+ end
+
+ it "calls #to_str to convert the first argument" do
+ char = mock("string index char")
+ char.should_receive(:to_str).and_return("b")
+ "abc".index(char).should == 1
+ end
+
+ it "calls #to_int to convert the second argument" do
+ offset = mock("string index offset")
+ offset.should_receive(:to_int).and_return(1)
+ "abc".index("c", offset).should == 2
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> { "abc".index 97 }.should.raise(TypeError)
+ end
+end
+
+describe "String#index with String" do
+ it "behaves the same as String#index(char) for one-character strings" do
+ "blablabla hello cruel world...!".split("").uniq.each do |str|
+ chr = str[0]
+ str.index(str).should == str.index(chr)
+
+ 0.upto(str.size + 1) do |start|
+ str.index(str, start).should == str.index(chr, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.index(str, start).should == str.index(chr, start)
+ end
+ end
+ end
+
+ it "returns the index of the first occurrence of the given substring" do
+ "blablabla".index("").should == 0
+ "blablabla".index("b").should == 0
+ "blablabla".index("bla").should == 0
+ "blablabla".index("blabla").should == 0
+ "blablabla".index("blablabla").should == 0
+
+ "blablabla".index("l").should == 1
+ "blablabla".index("la").should == 1
+ "blablabla".index("labla").should == 1
+ "blablabla".index("lablabla").should == 1
+
+ "blablabla".index("a").should == 2
+ "blablabla".index("abla").should == 2
+ "blablabla".index("ablabla").should == 2
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello.'.index('ll')
+ $~.should == nil
+ end
+
+ it "ignores string subclasses" do
+ "blablabla".index(StringSpecs::MyString.new("bla")).should == 0
+ StringSpecs::MyString.new("blablabla").index("bla").should == 0
+ StringSpecs::MyString.new("blablabla").index(StringSpecs::MyString.new("bla")).should == 0
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".index("bl", 0).should == 0
+ "blablabla".index("bl", 1).should == 3
+ "blablabla".index("bl", 2).should == 3
+ "blablabla".index("bl", 3).should == 3
+
+ "blablabla".index("bla", 0).should == 0
+ "blablabla".index("bla", 1).should == 3
+ "blablabla".index("bla", 2).should == 3
+ "blablabla".index("bla", 3).should == 3
+
+ "blablabla".index("blab", 0).should == 0
+ "blablabla".index("blab", 1).should == 3
+ "blablabla".index("blab", 2).should == 3
+ "blablabla".index("blab", 3).should == 3
+
+ "blablabla".index("la", 1).should == 1
+ "blablabla".index("la", 2).should == 4
+ "blablabla".index("la", 3).should == 4
+ "blablabla".index("la", 4).should == 4
+
+ "blablabla".index("lab", 1).should == 1
+ "blablabla".index("lab", 2).should == 4
+ "blablabla".index("lab", 3).should == 4
+ "blablabla".index("lab", 4).should == 4
+
+ "blablabla".index("ab", 2).should == 2
+ "blablabla".index("ab", 3).should == 5
+ "blablabla".index("ab", 4).should == 5
+ "blablabla".index("ab", 5).should == 5
+
+ "blablabla".index("", 0).should == 0
+ "blablabla".index("", 1).should == 1
+ "blablabla".index("", 2).should == 2
+ "blablabla".index("", 7).should == 7
+ "blablabla".index("", 8).should == 8
+ "blablabla".index("", 9).should == 9
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.index(needle, offset).should ==
+ str.index(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".index("B").should == nil
+ "blablabla".index("z").should == nil
+ "blablabla".index("BLA").should == nil
+ "blablabla".index("blablablabla").should == nil
+ "blablabla".index("", 10).should == nil
+
+ "hello".index("he", 1).should == nil
+ "hello".index("he", 2).should == nil
+ "I’ve got a multibyte character.\n".index("\n\n").should == nil
+ end
+
+ it "returns the character index of a multibyte character" do
+ "ã‚りãŒã¨ã†".index("ãŒ").should == 2
+ end
+
+ it "returns the character index after offset" do
+ "ã‚れã‚れ".index("ã‚", 1).should == 2
+ "ã‚りãŒã¨ã†ã‚りãŒã¨ã†".index("ãŒ", 3).should == 7
+ end
+
+ it "returns the character index after a partial first match" do
+ "</</h".index("</h").should == 2
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ char = "れ".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".index char
+ end.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "handles a substring in a superset encoding" do
+ 'abc'.dup.force_encoding(Encoding::US_ASCII).index('é').should == nil
+ end
+
+ it "handles a substring in a subset encoding" do
+ 'été'.index('t'.dup.force_encoding(Encoding::US_ASCII)).should == 1
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ str = 'abc'.dup.force_encoding("ISO-2022-JP")
+ pattern = 'b'.dup.force_encoding("EUC-JP")
+
+ -> { str.index(pattern) }.should.raise(Encoding::CompatibilityError, "incompatible character encodings: ISO-2022-JP and EUC-JP")
+ end
+end
+
+describe "String#index with Regexp" do
+ it "behaves the same as String#index(string) for escaped string regexps" do
+ ["blablabla", "hello cruel world...!"].each do |str|
+ ["", "b", "bla", "lab", "o c", "d."].each do |needle|
+ regexp = Regexp.new(Regexp.escape(needle))
+ str.index(regexp).should == str.index(needle)
+
+ 0.upto(str.size + 1) do |start|
+ str.index(regexp, start).should == str.index(needle, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.index(regexp, start).should == str.index(needle, start)
+ end
+ end
+ end
+ end
+
+ it "returns the index of the first match of regexp" do
+ "blablabla".index(/bla/).should == 0
+ "blablabla".index(/BLA/i).should == 0
+
+ "blablabla".index(/.{0}/).should == 0
+ "blablabla".index(/.{6}/).should == 0
+ "blablabla".index(/.{9}/).should == 0
+
+ "blablabla".index(/.*/).should == 0
+ "blablabla".index(/.+/).should == 0
+
+ "blablabla".index(/lab|b/).should == 0
+
+ not_supported_on :opal do
+ "blablabla".index(/\A/).should == 0
+ "blablabla".index(/\Z/).should == 9
+ "blablabla".index(/\z/).should == 9
+ "blablabla\n".index(/\Z/).should == 9
+ "blablabla\n".index(/\z/).should == 10
+ end
+
+ "blablabla".index(/^/).should == 0
+ "\nblablabla".index(/^/).should == 0
+ "b\nablabla".index(/$/).should == 1
+ "bl\nablabla".index(/$/).should == 2
+
+ "blablabla".index(/.l./).should == 0
+ end
+
+ it "sets $~ to MatchData of match and nil when there's none" do
+ 'hello.'.index(/.(.)/)
+ $~[0].should == 'he'
+
+ 'hello.'.index(/not/)
+ $~.should == nil
+ end
+
+ it "always clear $~" do
+ "a".index(/a/)
+ $~.should_not == nil
+
+ string = "blablabla"
+ string.index(/bla/, string.length + 1)
+ $~.should == nil
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".index(/.{0}/, 5).should == 5
+ "blablabla".index(/.{1}/, 5).should == 5
+ "blablabla".index(/.{2}/, 5).should == 5
+ "blablabla".index(/.{3}/, 5).should == 5
+ "blablabla".index(/.{4}/, 5).should == 5
+
+ "blablabla".index(/.{0}/, 3).should == 3
+ "blablabla".index(/.{1}/, 3).should == 3
+ "blablabla".index(/.{2}/, 3).should == 3
+ "blablabla".index(/.{5}/, 3).should == 3
+ "blablabla".index(/.{6}/, 3).should == 3
+
+ "blablabla".index(/.l./, 0).should == 0
+ "blablabla".index(/.l./, 1).should == 3
+ "blablabla".index(/.l./, 2).should == 3
+ "blablabla".index(/.l./, 3).should == 3
+
+ "xblaxbla".index(/x./, 0).should == 0
+ "xblaxbla".index(/x./, 1).should == 4
+ "xblaxbla".index(/x./, 2).should == 4
+
+ not_supported_on :opal do
+ "blablabla\n".index(/\Z/, 9).should == 9
+ end
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.index(needle, offset).should ==
+ str.index(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".index(/BLA/).should == nil
+
+ "blablabla".index(/.{10}/).should == nil
+ "blaxbla".index(/.x/, 3).should == nil
+ "blaxbla".index(/..x/, 2).should == nil
+ end
+
+ it "returns nil if the Regexp matches the empty string and the offset is out of range" do
+ "ruby".index(//,12).should == nil
+ end
+
+ it "supports \\G which matches at the given start offset" do
+ "helloYOU.".index(/\GYOU/, 5).should == 5
+ "helloYOU.".index(/\GYOU/).should == nil
+
+ re = /\G.+YOU/
+ # The # marks where \G will match.
+ [
+ ["#hi!YOUall.", 0],
+ ["h#i!YOUall.", 1],
+ ["hi#!YOUall.", 2],
+ ["hi!#YOUall.", nil]
+ ].each do |spec|
+
+ start = spec[0].index("#")
+ str = spec[0].delete("#")
+
+ str.index(re, start).should == spec[1]
+ end
+ end
+
+ it "converts start_offset to an integer via to_int" do
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ "RWOARW".index(/R./, obj).should == 4
+ end
+
+ it "returns the character index of a multibyte character" do
+ "ã‚りãŒã¨ã†".index(/ãŒ/).should == 2
+ end
+
+ it "returns the character index after offset" do
+ "ã‚れã‚れ".index(/ã‚/, 1).should == 2
+ end
+
+ it "treats the offset as a character index" do
+ "ã‚れã‚ã‚れ".index(/ã‚/, 3).should == 3
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ re = Regexp.new "れ".encode(Encoding::EUC_JP)
+ -> do
+ "ã‚れ".index re
+ end.should.raise(Encoding::CompatibilityError, "incompatible encoding regexp match (EUC-JP regexp with UTF-8 string)")
+ end
+end
diff --git a/spec/ruby/core/string/initialize_spec.rb b/spec/ruby/core/string/initialize_spec.rb
new file mode 100644
index 0000000000..b0c1e2e573
--- /dev/null
+++ b/spec/ruby/core/string/initialize_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/replace'
+
+describe "String#initialize" do
+ it "is a private method" do
+ String.private_instance_methods(false).should.include?(:initialize)
+ end
+
+ describe "with no arguments" do
+ it "does not change self" do
+ s = "some string"
+ s.send :initialize
+ s.should == "some string"
+ end
+
+ it "does not raise an exception when frozen" do
+ a = "hello".freeze
+ a.send(:initialize).should.equal?(a)
+ end
+ end
+
+ describe "with an argument" do
+ it_behaves_like :string_replace, :initialize
+ end
+end
diff --git a/spec/ruby/core/string/insert_spec.rb b/spec/ruby/core/string/insert_spec.rb
new file mode 100644
index 0000000000..c89793a8ca
--- /dev/null
+++ b/spec/ruby/core/string/insert_spec.rb
@@ -0,0 +1,81 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#insert with index, other" do
+ it "inserts other before the character at the given index" do
+ "abcd".insert(0, 'X').should == "Xabcd"
+ "abcd".insert(3, 'X').should == "abcXd"
+ "abcd".insert(4, 'X').should == "abcdX"
+ end
+
+ it "modifies self in place" do
+ a = "abcd"
+ a.insert(4, 'X').should == "abcdX"
+ a.should == "abcdX"
+ end
+
+ it "inserts after the given character on an negative count" do
+ "abcd".insert(-5, 'X').should == "Xabcd"
+ "abcd".insert(-3, 'X').should == "abXcd"
+ "abcd".insert(-1, 'X').should == "abcdX"
+ end
+
+ it "raises an IndexError if the index is beyond string" do
+ -> { "abcd".insert(5, 'X') }.should.raise(IndexError)
+ -> { "abcd".insert(-6, 'X') }.should.raise(IndexError)
+ end
+
+ it "converts index to an integer using to_int" do
+ other = mock('-3')
+ other.should_receive(:to_int).and_return(-3)
+
+ "abcd".insert(other, "XYZ").should == "abXYZcd"
+ end
+
+ it "converts other to a string using to_str" do
+ other = mock('XYZ')
+ other.should_receive(:to_str).and_return("XYZ")
+
+ "abcd".insert(-3, other).should == "abXYZcd"
+ end
+
+ it "raises a TypeError if other can't be converted to string" do
+ -> { "abcd".insert(-6, Object.new)}.should.raise(TypeError)
+ -> { "abcd".insert(-6, []) }.should.raise(TypeError)
+ -> { "abcd".insert(-6, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ str = "abcd".freeze
+ -> { str.insert(4, '') }.should.raise(FrozenError)
+ -> { str.insert(4, 'X') }.should.raise(FrozenError)
+ end
+
+ it "inserts a character into a multibyte encoded string" do
+ "ã‚りãŒã¨ã†".insert(1, 'ü').should == "ã‚üりãŒã¨ã†"
+ end
+
+ it "returns a String in the compatible encoding" do
+ str = "".force_encoding(Encoding::US_ASCII)
+ str.insert(0, "ã‚りãŒã¨ã†")
+ str.encoding.should == Encoding::UTF_8
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ pat = "ã‚¢".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".insert 0, pat
+ end.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "should not call subclassed string methods" do
+ cls = Class.new(String) do
+ def replace(arg)
+ raise "should not call replace"
+ end
+ end
+ cls.new("abcd").insert(0, 'X').should == "Xabcd"
+ end
+end
diff --git a/spec/ruby/core/string/inspect_spec.rb b/spec/ruby/core/string/inspect_spec.rb
new file mode 100644
index 0000000000..8b91ce2f84
--- /dev/null
+++ b/spec/ruby/core/string/inspect_spec.rb
@@ -0,0 +1,520 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#inspect" do
+ it "does not return a subclass instance" do
+ StringSpecs::MyString.new.inspect.should.instance_of?(String)
+ end
+
+ it "returns a string with special characters replaced with \\<char> notation" do
+ [ ["\a", '"\\a"'],
+ ["\b", '"\\b"'],
+ ["\t", '"\\t"'],
+ ["\n", '"\\n"'],
+ ["\v", '"\\v"'],
+ ["\f", '"\\f"'],
+ ["\r", '"\\r"'],
+ ["\e", '"\\e"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with special characters replaced with \\<char> notation for UTF-16" do
+ pairs = [
+ ["\a", '"\\a"'],
+ ["\b", '"\\b"'],
+ ["\t", '"\\t"'],
+ ["\n", '"\\n"'],
+ ["\v", '"\\v"'],
+ ["\f", '"\\f"'],
+ ["\r", '"\\r"'],
+ ["\e", '"\\e"']
+ ].map { |str, result| [str.encode('UTF-16LE'), result] }
+
+ pairs.should be_computed_by(:inspect)
+ end
+
+ it "returns a string with \" and \\ escaped with a backslash" do
+ [ ["\"", '"\\""'],
+ ["\\", '"\\\\"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with \\#<char> when # is followed by $, @, {" do
+ [ ["\#$", '"\\#$"'],
+ ["\#@", '"\\#@"'],
+ ["\#{", '"\\#{"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with # not escaped when followed by any other character" do
+ [ ["#", '"#"'],
+ ["#1", '"#1"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with printable non-alphanumeric characters unescaped" do
+ [ [" ", '" "'],
+ ["!", '"!"'],
+ ["$", '"$"'],
+ ["%", '"%"'],
+ ["&", '"&"'],
+ ["'", '"\'"'],
+ ["(", '"("'],
+ [")", '")"'],
+ ["*", '"*"'],
+ ["+", '"+"'],
+ [",", '","'],
+ ["-", '"-"'],
+ [".", '"."'],
+ ["/", '"/"'],
+ [":", '":"'],
+ [";", '";"'],
+ ["<", '"<"'],
+ ["=", '"="'],
+ [">", '">"'],
+ ["?", '"?"'],
+ ["@", '"@"'],
+ ["[", '"["'],
+ ["]", '"]"'],
+ ["^", '"^"'],
+ ["_", '"_"'],
+ ["`", '"`"'],
+ ["{", '"{"'],
+ ["|", '"|"'],
+ ["}", '"}"'],
+ ["~", '"~"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with numeric characters unescaped" do
+ [ ["0", '"0"'],
+ ["1", '"1"'],
+ ["2", '"2"'],
+ ["3", '"3"'],
+ ["4", '"4"'],
+ ["5", '"5"'],
+ ["6", '"6"'],
+ ["7", '"7"'],
+ ["8", '"8"'],
+ ["9", '"9"'],
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with upper-case alpha characters unescaped" do
+ [ ["A", '"A"'],
+ ["B", '"B"'],
+ ["C", '"C"'],
+ ["D", '"D"'],
+ ["E", '"E"'],
+ ["F", '"F"'],
+ ["G", '"G"'],
+ ["H", '"H"'],
+ ["I", '"I"'],
+ ["J", '"J"'],
+ ["K", '"K"'],
+ ["L", '"L"'],
+ ["M", '"M"'],
+ ["N", '"N"'],
+ ["O", '"O"'],
+ ["P", '"P"'],
+ ["Q", '"Q"'],
+ ["R", '"R"'],
+ ["S", '"S"'],
+ ["T", '"T"'],
+ ["U", '"U"'],
+ ["V", '"V"'],
+ ["W", '"W"'],
+ ["X", '"X"'],
+ ["Y", '"Y"'],
+ ["Z", '"Z"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with lower-case alpha characters unescaped" do
+ [ ["a", '"a"'],
+ ["b", '"b"'],
+ ["c", '"c"'],
+ ["d", '"d"'],
+ ["e", '"e"'],
+ ["f", '"f"'],
+ ["g", '"g"'],
+ ["h", '"h"'],
+ ["i", '"i"'],
+ ["j", '"j"'],
+ ["k", '"k"'],
+ ["l", '"l"'],
+ ["m", '"m"'],
+ ["n", '"n"'],
+ ["o", '"o"'],
+ ["p", '"p"'],
+ ["q", '"q"'],
+ ["r", '"r"'],
+ ["s", '"s"'],
+ ["t", '"t"'],
+ ["u", '"u"'],
+ ["v", '"v"'],
+ ["w", '"w"'],
+ ["x", '"x"'],
+ ["y", '"y"'],
+ ["z", '"z"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with non-printing characters replaced by \\x notation" do
+ # Avoid the file encoding by computing the string with #chr.
+ [ [0001.chr, '"\\x01"'],
+ [0002.chr, '"\\x02"'],
+ [0003.chr, '"\\x03"'],
+ [0004.chr, '"\\x04"'],
+ [0005.chr, '"\\x05"'],
+ [0006.chr, '"\\x06"'],
+ [0016.chr, '"\\x0E"'],
+ [0017.chr, '"\\x0F"'],
+ [0020.chr, '"\\x10"'],
+ [0021.chr, '"\\x11"'],
+ [0022.chr, '"\\x12"'],
+ [0023.chr, '"\\x13"'],
+ [0024.chr, '"\\x14"'],
+ [0025.chr, '"\\x15"'],
+ [0026.chr, '"\\x16"'],
+ [0027.chr, '"\\x17"'],
+ [0030.chr, '"\\x18"'],
+ [0031.chr, '"\\x19"'],
+ [0032.chr, '"\\x1A"'],
+ [0034.chr, '"\\x1C"'],
+ [0035.chr, '"\\x1D"'],
+ [0036.chr, '"\\x1E"'],
+ [0037.chr, '"\\x1F"'],
+ [0177.chr, '"\\x7F"'],
+ [0200.chr, '"\\x80"'],
+ [0201.chr, '"\\x81"'],
+ [0202.chr, '"\\x82"'],
+ [0203.chr, '"\\x83"'],
+ [0204.chr, '"\\x84"'],
+ [0205.chr, '"\\x85"'],
+ [0206.chr, '"\\x86"'],
+ [0207.chr, '"\\x87"'],
+ [0210.chr, '"\\x88"'],
+ [0211.chr, '"\\x89"'],
+ [0212.chr, '"\\x8A"'],
+ [0213.chr, '"\\x8B"'],
+ [0214.chr, '"\\x8C"'],
+ [0215.chr, '"\\x8D"'],
+ [0216.chr, '"\\x8E"'],
+ [0217.chr, '"\\x8F"'],
+ [0220.chr, '"\\x90"'],
+ [0221.chr, '"\\x91"'],
+ [0222.chr, '"\\x92"'],
+ [0223.chr, '"\\x93"'],
+ [0224.chr, '"\\x94"'],
+ [0225.chr, '"\\x95"'],
+ [0226.chr, '"\\x96"'],
+ [0227.chr, '"\\x97"'],
+ [0230.chr, '"\\x98"'],
+ [0231.chr, '"\\x99"'],
+ [0232.chr, '"\\x9A"'],
+ [0233.chr, '"\\x9B"'],
+ [0234.chr, '"\\x9C"'],
+ [0235.chr, '"\\x9D"'],
+ [0236.chr, '"\\x9E"'],
+ [0237.chr, '"\\x9F"'],
+ [0240.chr, '"\\xA0"'],
+ [0241.chr, '"\\xA1"'],
+ [0242.chr, '"\\xA2"'],
+ [0243.chr, '"\\xA3"'],
+ [0244.chr, '"\\xA4"'],
+ [0245.chr, '"\\xA5"'],
+ [0246.chr, '"\\xA6"'],
+ [0247.chr, '"\\xA7"'],
+ [0250.chr, '"\\xA8"'],
+ [0251.chr, '"\\xA9"'],
+ [0252.chr, '"\\xAA"'],
+ [0253.chr, '"\\xAB"'],
+ [0254.chr, '"\\xAC"'],
+ [0255.chr, '"\\xAD"'],
+ [0256.chr, '"\\xAE"'],
+ [0257.chr, '"\\xAF"'],
+ [0260.chr, '"\\xB0"'],
+ [0261.chr, '"\\xB1"'],
+ [0262.chr, '"\\xB2"'],
+ [0263.chr, '"\\xB3"'],
+ [0264.chr, '"\\xB4"'],
+ [0265.chr, '"\\xB5"'],
+ [0266.chr, '"\\xB6"'],
+ [0267.chr, '"\\xB7"'],
+ [0270.chr, '"\\xB8"'],
+ [0271.chr, '"\\xB9"'],
+ [0272.chr, '"\\xBA"'],
+ [0273.chr, '"\\xBB"'],
+ [0274.chr, '"\\xBC"'],
+ [0275.chr, '"\\xBD"'],
+ [0276.chr, '"\\xBE"'],
+ [0277.chr, '"\\xBF"'],
+ [0300.chr, '"\\xC0"'],
+ [0301.chr, '"\\xC1"'],
+ [0302.chr, '"\\xC2"'],
+ [0303.chr, '"\\xC3"'],
+ [0304.chr, '"\\xC4"'],
+ [0305.chr, '"\\xC5"'],
+ [0306.chr, '"\\xC6"'],
+ [0307.chr, '"\\xC7"'],
+ [0310.chr, '"\\xC8"'],
+ [0311.chr, '"\\xC9"'],
+ [0312.chr, '"\\xCA"'],
+ [0313.chr, '"\\xCB"'],
+ [0314.chr, '"\\xCC"'],
+ [0315.chr, '"\\xCD"'],
+ [0316.chr, '"\\xCE"'],
+ [0317.chr, '"\\xCF"'],
+ [0320.chr, '"\\xD0"'],
+ [0321.chr, '"\\xD1"'],
+ [0322.chr, '"\\xD2"'],
+ [0323.chr, '"\\xD3"'],
+ [0324.chr, '"\\xD4"'],
+ [0325.chr, '"\\xD5"'],
+ [0326.chr, '"\\xD6"'],
+ [0327.chr, '"\\xD7"'],
+ [0330.chr, '"\\xD8"'],
+ [0331.chr, '"\\xD9"'],
+ [0332.chr, '"\\xDA"'],
+ [0333.chr, '"\\xDB"'],
+ [0334.chr, '"\\xDC"'],
+ [0335.chr, '"\\xDD"'],
+ [0336.chr, '"\\xDE"'],
+ [0337.chr, '"\\xDF"'],
+ [0340.chr, '"\\xE0"'],
+ [0341.chr, '"\\xE1"'],
+ [0342.chr, '"\\xE2"'],
+ [0343.chr, '"\\xE3"'],
+ [0344.chr, '"\\xE4"'],
+ [0345.chr, '"\\xE5"'],
+ [0346.chr, '"\\xE6"'],
+ [0347.chr, '"\\xE7"'],
+ [0350.chr, '"\\xE8"'],
+ [0351.chr, '"\\xE9"'],
+ [0352.chr, '"\\xEA"'],
+ [0353.chr, '"\\xEB"'],
+ [0354.chr, '"\\xEC"'],
+ [0355.chr, '"\\xED"'],
+ [0356.chr, '"\\xEE"'],
+ [0357.chr, '"\\xEF"'],
+ [0360.chr, '"\\xF0"'],
+ [0361.chr, '"\\xF1"'],
+ [0362.chr, '"\\xF2"'],
+ [0363.chr, '"\\xF3"'],
+ [0364.chr, '"\\xF4"'],
+ [0365.chr, '"\\xF5"'],
+ [0366.chr, '"\\xF6"'],
+ [0367.chr, '"\\xF7"'],
+ [0370.chr, '"\\xF8"'],
+ [0371.chr, '"\\xF9"'],
+ [0372.chr, '"\\xFA"'],
+ [0373.chr, '"\\xFB"'],
+ [0374.chr, '"\\xFC"'],
+ [0375.chr, '"\\xFD"'],
+ [0376.chr, '"\\xFE"'],
+ [0377.chr, '"\\xFF"']
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with a NUL character replaced by \\x notation" do
+ 0.chr.inspect.should == '"\\x00"'
+ end
+
+ it "uses \\x notation for broken UTF-8 sequences" do
+ "\xF0\x9F".inspect.should == '"\\xF0\\x9F"'
+ end
+
+ it "works for broken US-ASCII strings" do
+ s = "©".dup.force_encoding("US-ASCII")
+ s.inspect.should == '"\xC2\xA9"'
+ end
+
+ describe "when default external is UTF-8" do
+ before :each do
+ @extenc, Encoding.default_external = Encoding.default_external, Encoding::UTF_8
+ end
+
+ after :each do
+ Encoding.default_external = @extenc
+ end
+
+ it "returns a string with non-printing characters replaced by \\u notation for Unicode strings" do
+ [ [0001.chr('utf-8'), '"\u0001"'],
+ [0002.chr('utf-8'), '"\u0002"'],
+ [0003.chr('utf-8'), '"\u0003"'],
+ [0004.chr('utf-8'), '"\u0004"'],
+ [0005.chr('utf-8'), '"\u0005"'],
+ [0006.chr('utf-8'), '"\u0006"'],
+ [0016.chr('utf-8'), '"\u000E"'],
+ [0017.chr('utf-8'), '"\u000F"'],
+ [0020.chr('utf-8'), '"\u0010"'],
+ [0021.chr('utf-8'), '"\u0011"'],
+ [0022.chr('utf-8'), '"\u0012"'],
+ [0023.chr('utf-8'), '"\u0013"'],
+ [0024.chr('utf-8'), '"\u0014"'],
+ [0025.chr('utf-8'), '"\u0015"'],
+ [0026.chr('utf-8'), '"\u0016"'],
+ [0027.chr('utf-8'), '"\u0017"'],
+ [0030.chr('utf-8'), '"\u0018"'],
+ [0031.chr('utf-8'), '"\u0019"'],
+ [0032.chr('utf-8'), '"\u001A"'],
+ [0034.chr('utf-8'), '"\u001C"'],
+ [0035.chr('utf-8'), '"\u001D"'],
+ [0036.chr('utf-8'), '"\u001E"'],
+ [0037.chr('utf-8'), '"\u001F"'],
+ [0177.chr('utf-8'), '"\u007F"'],
+ [0200.chr('utf-8'), '"\u0080"'],
+ [0201.chr('utf-8'), '"\u0081"'],
+ [0202.chr('utf-8'), '"\u0082"'],
+ [0203.chr('utf-8'), '"\u0083"'],
+ [0204.chr('utf-8'), '"\u0084"'],
+ [0206.chr('utf-8'), '"\u0086"'],
+ [0207.chr('utf-8'), '"\u0087"'],
+ [0210.chr('utf-8'), '"\u0088"'],
+ [0211.chr('utf-8'), '"\u0089"'],
+ [0212.chr('utf-8'), '"\u008A"'],
+ [0213.chr('utf-8'), '"\u008B"'],
+ [0214.chr('utf-8'), '"\u008C"'],
+ [0215.chr('utf-8'), '"\u008D"'],
+ [0216.chr('utf-8'), '"\u008E"'],
+ [0217.chr('utf-8'), '"\u008F"'],
+ [0220.chr('utf-8'), '"\u0090"'],
+ [0221.chr('utf-8'), '"\u0091"'],
+ [0222.chr('utf-8'), '"\u0092"'],
+ [0223.chr('utf-8'), '"\u0093"'],
+ [0224.chr('utf-8'), '"\u0094"'],
+ [0225.chr('utf-8'), '"\u0095"'],
+ [0226.chr('utf-8'), '"\u0096"'],
+ [0227.chr('utf-8'), '"\u0097"'],
+ [0230.chr('utf-8'), '"\u0098"'],
+ [0231.chr('utf-8'), '"\u0099"'],
+ [0232.chr('utf-8'), '"\u009A"'],
+ [0233.chr('utf-8'), '"\u009B"'],
+ [0234.chr('utf-8'), '"\u009C"'],
+ [0235.chr('utf-8'), '"\u009D"'],
+ [0236.chr('utf-8'), '"\u009E"'],
+ [0237.chr('utf-8'), '"\u009F"'],
+ ].should be_computed_by(:inspect)
+ end
+
+ it "returns a string with a NUL character replaced by \\u notation" do
+ 0.chr('utf-8').inspect.should == '"\\u0000"'
+ end
+
+ it "returns a string with extended characters for Unicode strings" do
+ [ [0240.chr('utf-8'), '" "'],
+ [0241.chr('utf-8'), '"¡"'],
+ [0242.chr('utf-8'), '"¢"'],
+ [0243.chr('utf-8'), '"£"'],
+ [0244.chr('utf-8'), '"¤"'],
+ [0245.chr('utf-8'), '"Â¥"'],
+ [0246.chr('utf-8'), '"¦"'],
+ [0247.chr('utf-8'), '"§"'],
+ [0250.chr('utf-8'), '"¨"'],
+ [0251.chr('utf-8'), '"©"'],
+ [0252.chr('utf-8'), '"ª"'],
+ [0253.chr('utf-8'), '"«"'],
+ [0254.chr('utf-8'), '"¬"'],
+ [0255.chr('utf-8'), '"­"'],
+ [0256.chr('utf-8'), '"®"'],
+ [0257.chr('utf-8'), '"¯"'],
+ [0260.chr('utf-8'), '"°"'],
+ [0261.chr('utf-8'), '"±"'],
+ [0262.chr('utf-8'), '"²"'],
+ [0263.chr('utf-8'), '"³"'],
+ [0264.chr('utf-8'), '"´"'],
+ [0265.chr('utf-8'), '"µ"'],
+ [0266.chr('utf-8'), '"¶"'],
+ [0267.chr('utf-8'), '"·"'],
+ [0270.chr('utf-8'), '"¸"'],
+ [0271.chr('utf-8'), '"¹"'],
+ [0272.chr('utf-8'), '"º"'],
+ [0273.chr('utf-8'), '"»"'],
+ [0274.chr('utf-8'), '"¼"'],
+ [0275.chr('utf-8'), '"½"'],
+ [0276.chr('utf-8'), '"¾"'],
+ [0277.chr('utf-8'), '"¿"'],
+ [0300.chr('utf-8'), '"À"'],
+ [0301.chr('utf-8'), '"Ã"'],
+ [0302.chr('utf-8'), '"Â"'],
+ [0303.chr('utf-8'), '"Ã"'],
+ [0304.chr('utf-8'), '"Ä"'],
+ [0305.chr('utf-8'), '"Ã…"'],
+ [0306.chr('utf-8'), '"Æ"'],
+ [0307.chr('utf-8'), '"Ç"'],
+ [0310.chr('utf-8'), '"È"'],
+ [0311.chr('utf-8'), '"É"'],
+ [0312.chr('utf-8'), '"Ê"'],
+ [0313.chr('utf-8'), '"Ë"'],
+ [0314.chr('utf-8'), '"Ì"'],
+ [0315.chr('utf-8'), '"Ã"'],
+ [0316.chr('utf-8'), '"ÃŽ"'],
+ [0317.chr('utf-8'), '"Ã"'],
+ [0320.chr('utf-8'), '"Ã"'],
+ [0321.chr('utf-8'), '"Ñ"'],
+ [0322.chr('utf-8'), '"Ã’"'],
+ [0323.chr('utf-8'), '"Ó"'],
+ [0324.chr('utf-8'), '"Ô"'],
+ [0325.chr('utf-8'), '"Õ"'],
+ [0326.chr('utf-8'), '"Ö"'],
+ [0327.chr('utf-8'), '"×"'],
+ [0330.chr('utf-8'), '"Ø"'],
+ [0331.chr('utf-8'), '"Ù"'],
+ [0332.chr('utf-8'), '"Ú"'],
+ [0333.chr('utf-8'), '"Û"'],
+ [0334.chr('utf-8'), '"Ü"'],
+ [0335.chr('utf-8'), '"Ã"'],
+ [0336.chr('utf-8'), '"Þ"'],
+ [0337.chr('utf-8'), '"ß"'],
+ [0340.chr('utf-8'), '"à"'],
+ [0341.chr('utf-8'), '"á"'],
+ [0342.chr('utf-8'), '"â"'],
+ [0343.chr('utf-8'), '"ã"'],
+ [0344.chr('utf-8'), '"ä"'],
+ [0345.chr('utf-8'), '"Ã¥"'],
+ [0346.chr('utf-8'), '"æ"'],
+ [0347.chr('utf-8'), '"ç"'],
+ [0350.chr('utf-8'), '"è"'],
+ [0351.chr('utf-8'), '"é"'],
+ [0352.chr('utf-8'), '"ê"'],
+ [0353.chr('utf-8'), '"ë"'],
+ [0354.chr('utf-8'), '"ì"'],
+ [0355.chr('utf-8'), '"í"'],
+ [0356.chr('utf-8'), '"î"'],
+ [0357.chr('utf-8'), '"ï"'],
+ [0360.chr('utf-8'), '"ð"'],
+ [0361.chr('utf-8'), '"ñ"'],
+ [0362.chr('utf-8'), '"ò"'],
+ [0363.chr('utf-8'), '"ó"'],
+ [0364.chr('utf-8'), '"ô"'],
+ [0365.chr('utf-8'), '"õ"'],
+ [0366.chr('utf-8'), '"ö"'],
+ [0367.chr('utf-8'), '"÷"'],
+ [0370.chr('utf-8'), '"ø"'],
+ [0371.chr('utf-8'), '"ù"'],
+ [0372.chr('utf-8'), '"ú"'],
+ [0373.chr('utf-8'), '"û"'],
+ [0374.chr('utf-8'), '"ü"'],
+ [0375.chr('utf-8'), '"ý"'],
+ [0376.chr('utf-8'), '"þ"'],
+ [0377.chr('utf-8'), '"ÿ"']
+ ].should be_computed_by(:inspect)
+ end
+ end
+
+ describe "when the string's encoding is different than the result's encoding" do
+ describe "and the string's encoding is ASCII-compatible but the characters are non-ASCII" do
+ it "returns a string with the non-ASCII characters replaced by \\x notation" do
+ "\u{3042}".encode("EUC-JP").inspect.should == '"\\x{A4A2}"'
+ end
+ end
+
+ describe "and the string has both ASCII-compatible and ASCII-incompatible chars" do
+ it "returns a string with the non-ASCII characters replaced by \\u notation" do
+ "hello привет".encode("utf-16le").inspect.should == '"hello \\u043F\\u0440\\u0438\\u0432\\u0435\\u0442"'
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/intern_spec.rb b/spec/ruby/core/string/intern_spec.rb
new file mode 100644
index 0000000000..cd7dad4359
--- /dev/null
+++ b/spec/ruby/core/string/intern_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/to_sym'
+
+describe "String#intern" do
+ it_behaves_like :string_to_sym, :intern
+end
diff --git a/spec/ruby/core/string/length_spec.rb b/spec/ruby/core/string/length_spec.rb
new file mode 100644
index 0000000000..98cee1f03d
--- /dev/null
+++ b/spec/ruby/core/string/length_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/length'
+
+describe "String#length" do
+ it_behaves_like :string_length, :length
+end
diff --git a/spec/ruby/core/string/lines_spec.rb b/spec/ruby/core/string/lines_spec.rb
new file mode 100644
index 0000000000..40ab5f71d8
--- /dev/null
+++ b/spec/ruby/core/string/lines_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/each_line'
+
+describe "String#lines" do
+ it_behaves_like :string_each_line, :lines
+
+ it "returns an array when no block given" do
+ ary = "hello world".lines(' ')
+ ary.should == ["hello ", "world"]
+ end
+
+ context "when `chomp` keyword argument is passed" do
+ it "removes new line characters" do
+ "hello \nworld\n".lines(chomp: true).should == ["hello ", "world"]
+ "hello \r\nworld\r\n".lines(chomp: true).should == ["hello ", "world"]
+ end
+ end
+end
diff --git a/spec/ruby/core/string/ljust_spec.rb b/spec/ruby/core/string/ljust_spec.rb
new file mode 100644
index 0000000000..0b2aab2638
--- /dev/null
+++ b/spec/ruby/core/string/ljust_spec.rb
@@ -0,0 +1,100 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#ljust with length, padding" do
+ it "returns a new string of specified length with self left justified and padded with padstr" do
+ "hello".ljust(20, '1234').should == "hello123412341234123"
+
+ "".ljust(1, "abcd").should == "a"
+ "".ljust(2, "abcd").should == "ab"
+ "".ljust(3, "abcd").should == "abc"
+ "".ljust(4, "abcd").should == "abcd"
+ "".ljust(6, "abcd").should == "abcdab"
+
+ "OK".ljust(3, "abcd").should == "OKa"
+ "OK".ljust(4, "abcd").should == "OKab"
+ "OK".ljust(6, "abcd").should == "OKabcd"
+ "OK".ljust(8, "abcd").should == "OKabcdab"
+ end
+
+ it "pads with whitespace if no padstr is given" do
+ "hello".ljust(20).should == "hello "
+ end
+
+ it "returns self if it's longer than or as long as the specified length" do
+ "".ljust(0).should == ""
+ "".ljust(-1).should == ""
+ "hello".ljust(4).should == "hello"
+ "hello".ljust(-1).should == "hello"
+ "this".ljust(3).should == "this"
+ "radiology".ljust(8, '-').should == "radiology"
+ end
+
+ it "tries to convert length to an integer using to_int" do
+ "^".ljust(3.8, "_^").should == "^_^"
+
+ obj = mock('3')
+ obj.should_receive(:to_int).and_return(3)
+
+ "o".ljust(obj, "_o").should == "o_o"
+ end
+
+ it "raises a TypeError when length can't be converted to an integer" do
+ -> { "hello".ljust("x") }.should.raise(TypeError)
+ -> { "hello".ljust("x", "y") }.should.raise(TypeError)
+ -> { "hello".ljust([]) }.should.raise(TypeError)
+ -> { "hello".ljust(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "tries to convert padstr to a string using to_str" do
+ padstr = mock('123')
+ padstr.should_receive(:to_str).and_return("123")
+
+ "hello".ljust(10, padstr).should == "hello12312"
+ end
+
+ it "raises a TypeError when padstr can't be converted" do
+ -> { "hello".ljust(20, []) }.should.raise(TypeError)
+ -> { "hello".ljust(20, Object.new)}.should.raise(TypeError)
+ -> { "hello".ljust(20, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when padstr is empty" do
+ -> { "hello".ljust(10, '') }.should.raise(ArgumentError)
+ end
+
+ it "returns String instances when called on subclasses" do
+ StringSpecs::MyString.new("").ljust(10).should.instance_of?(String)
+ StringSpecs::MyString.new("foo").ljust(10).should.instance_of?(String)
+ StringSpecs::MyString.new("foo").ljust(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+
+ "".ljust(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+ "foo".ljust(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+ end
+
+ describe "with width" do
+ it "returns a String in the same encoding as the original" do
+ str = "abc".dup.force_encoding Encoding::IBM437
+ result = str.ljust 5
+ result.should == "abc "
+ result.encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "with width, pattern" do
+ it "returns a String in the compatible encoding" do
+ str = "abc".dup.force_encoding Encoding::IBM437
+ result = str.ljust 5, "ã‚"
+ result.should == "abcã‚ã‚"
+ result.encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ pat = "ã‚¢".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".ljust 5, pat
+ end.should.raise(Encoding::CompatibilityError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/lstrip_spec.rb b/spec/ruby/core/string/lstrip_spec.rb
new file mode 100644
index 0000000000..5896f8d7da
--- /dev/null
+++ b/spec/ruby/core/string/lstrip_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/strip'
+
+describe "String#lstrip" do
+ it_behaves_like :string_strip, :lstrip
+
+ it "returns a copy of self with leading whitespace removed" do
+ " hello ".lstrip.should == "hello "
+ " hello world ".lstrip.should == "hello world "
+ "\n\r\t\n\v\r hello world ".lstrip.should == "hello world "
+ "hello".lstrip.should == "hello"
+ " ã“ã«ã¡ã‚".lstrip.should == "ã“ã«ã¡ã‚"
+ end
+
+ it "works with lazy substrings" do
+ " hello "[1...-1].lstrip.should == "hello "
+ " hello world "[1...-1].lstrip.should == "hello world "
+ "\n\r\t\n\v\r hello world "[1...-1].lstrip.should == "hello world "
+ " ã“ã«ã¡ã‚ "[1...-1].lstrip.should == "ã“ã«ã¡ã‚"
+ end
+
+ it "strips leading \\0" do
+ "\x00hello".lstrip.should == "hello"
+ "\000 \000hello\000 \000".lstrip.should == "hello\000 \000"
+ end
+end
+
+describe "String#lstrip!" do
+ it "modifies self in place and returns self" do
+ a = " hello "
+ a.lstrip!.should.equal?(a)
+ a.should == "hello "
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.lstrip!.should == nil
+ a.should == "hello"
+ end
+
+ it "makes a string empty if it is only whitespace" do
+ "".lstrip!.should == nil
+ " ".lstrip.should == ""
+ " ".lstrip.should == ""
+ end
+
+ it "removes leading NULL bytes and whitespace" do
+ a = "\000 \000hello\000 \000"
+ a.lstrip!
+ a.should == "hello\000 \000"
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { " hello ".freeze.lstrip! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23657]
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> { "hello".freeze.lstrip! }.should.raise(FrozenError)
+ -> { "".freeze.lstrip! }.should.raise(FrozenError)
+ end
+
+ it "raises an ArgumentError if the first non-space codepoint is invalid" do
+ s = "\xDFabc".force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.lstrip! }.should.raise(ArgumentError)
+
+ s = " \xDFabc".force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.lstrip! }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/match_spec.rb b/spec/ruby/core/string/match_spec.rb
new file mode 100644
index 0000000000..3ea8d90aa8
--- /dev/null
+++ b/spec/ruby/core/string/match_spec.rb
@@ -0,0 +1,175 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe :string_match_escaped_literal, shared: true do
+ not_supported_on :opal do
+ it "matches a literal Regexp that uses ASCII-only UTF-8 escape sequences" do
+ "a b".match(/([\u{20}-\u{7e}])/)[0].should == "a"
+ end
+ end
+end
+
+describe "String#=~" do
+ it "behaves the same way as index() when given a regexp" do
+ ("rudder" =~ /udder/).should == "rudder".index(/udder/)
+ ("boat" =~ /[^fl]oat/).should == "boat".index(/[^fl]oat/)
+ ("bean" =~ /bag/).should == "bean".index(/bag/)
+ ("true" =~ /false/).should == "true".index(/false/)
+ end
+
+ it "raises a TypeError if a obj is a string" do
+ -> { "some string" =~ "another string" }.should.raise(TypeError)
+ -> { "a" =~ StringSpecs::MyString.new("b") }.should.raise(TypeError)
+ end
+
+ it "invokes obj.=~ with self if obj is neither a string nor regexp" do
+ str = "w00t"
+ obj = mock('x')
+
+ obj.should_receive(:=~).with(str).any_number_of_times.and_return(true)
+ str.should =~ obj
+
+ obj = mock('y')
+ obj.should_receive(:=~).with(str).any_number_of_times.and_return(false)
+ str.should_not =~ obj
+ end
+
+ it "sets $~ to MatchData when there is a match and nil when there's none" do
+ 'hello' =~ /./
+ $~[0].should == 'h'
+
+ 'hello' =~ /not/
+ $~.should == nil
+ end
+
+ it "returns the character index of a found match" do
+ ("ã“ã«ã¡ã‚" =~ /ã«/).should == 1
+ end
+
+end
+
+describe "String#match" do
+ it "matches the pattern against self" do
+ 'hello'.match(/(.)\1/)[0].should == 'll'
+ end
+
+ it_behaves_like :string_match_escaped_literal, :match
+
+ describe "with [pattern, position]" do
+ describe "when given a positive position" do
+ it "matches the pattern against self starting at an optional index" do
+ "01234".match(/(.).(.)/, 1).captures.should == ["1", "3"]
+ end
+
+ it "uses the start as a character offset" do
+ "零一二三四".match(/(.).(.)/, 1).captures.should == ["一", "三"]
+ end
+ end
+
+ describe "when given a negative position" do
+ it "matches the pattern against self starting at an optional index" do
+ "01234".match(/(.).(.)/, -4).captures.should == ["1", "3"]
+ end
+
+ it "uses the start as a character offset" do
+ "零一二三四".match(/(.).(.)/, -4).captures.should == ["一", "三"]
+ end
+ end
+ end
+
+ describe "when passed a block" do
+ it "yields the MatchData" do
+ "abc".match(/./) {|m| ScratchPad.record m }
+ ScratchPad.recorded.should.is_a?(MatchData)
+ end
+
+ it "returns the block result" do
+ "abc".match(/./) { :result }.should == :result
+ end
+
+ it "does not yield if there is no match" do
+ ScratchPad.record []
+ "b".match(/a/) {|m| ScratchPad << m }
+ ScratchPad.recorded.should == []
+ end
+ end
+
+ it "tries to convert pattern to a string via to_str" do
+ obj = mock('.')
+ def obj.to_str() "." end
+ "hello".match(obj)[0].should == "h"
+
+ obj = mock('.')
+ def obj.respond_to?(type, *) true end
+ def obj.method_missing(*args) "." end
+ "hello".match(obj)[0].should == "h"
+ end
+
+ it "raises a TypeError if pattern is not a regexp or a string" do
+ -> { 'hello'.match(10) }.should.raise(TypeError)
+ not_supported_on :opal do
+ -> { 'hello'.match(:ell) }.should.raise(TypeError)
+ end
+ end
+
+ it "converts string patterns to regexps without escaping" do
+ 'hello'.match('(.)\1')[0].should == 'll'
+ end
+
+ it "returns nil if there's no match" do
+ 'hello'.match('xx').should == nil
+ end
+
+ it "matches \\G at the start of the string" do
+ 'hello'.match(/\Gh/)[0].should == 'h'
+ 'hello'.match(/\Go/).should == nil
+ end
+
+ it "sets $~ to MatchData of match or nil when there is none" do
+ 'hello'.match(/./)
+ $~[0].should == 'h'
+ Regexp.last_match[0].should == 'h'
+
+ 'hello'.match(/X/)
+ $~.should == nil
+ Regexp.last_match.should == nil
+ end
+
+ it "calls match on the regular expression" do
+ # Can't use regexp.should_receive(:match).and_return(:foo) since regexps are frozen
+ ScratchPad.clear
+ regexp = Class.new(Regexp) {
+ def match(*args)
+ ScratchPad.record [:match, *args]
+ super(*args)
+ end
+ }.new('.')
+
+ 'hello'.match(regexp)
+ ScratchPad.recorded.should == [:match, 'hello']
+ end
+end
+
+describe "String#match?" do
+ before :each do
+ # Resetting Regexp.last_match
+ /DONTMATCH/.match ''
+ end
+
+ context "when matches the given regex" do
+ it "returns true but does not set Regexp.last_match" do
+ 'string'.match?(/string/i).should == true
+ Regexp.last_match.should == nil
+ end
+ end
+
+ it "returns false when does not match the given regex" do
+ 'string'.match?(/STRING/).should == false
+ end
+
+ it "takes matching position as the 2nd argument" do
+ 'string'.match?(/str/i, 0).should == true
+ 'string'.match?(/str/i, 1).should == false
+ end
+end
diff --git a/spec/ruby/core/string/modulo_spec.rb b/spec/ruby/core/string/modulo_spec.rb
new file mode 100644
index 0000000000..f93ec4bcf8
--- /dev/null
+++ b/spec/ruby/core/string/modulo_spec.rb
@@ -0,0 +1,797 @@
+require_relative '../../spec_helper'
+require_relative '../kernel/shared/sprintf'
+require_relative '../kernel/shared/sprintf_encoding'
+require_relative 'fixtures/classes'
+require_relative '../../shared/hash/key_error'
+
+describe "String#%" do
+ it_behaves_like :kernel_sprintf, -> format, *args {
+ format % args
+ }
+
+ it_behaves_like :kernel_sprintf_encoding, -> format, *args {
+ format % args
+ }
+end
+
+# TODO: these specs are mostly redundant with kernel/shared/sprintf.rb specs.
+# These specs should be moved there and deduplicated.
+describe "String#%" do
+ context "when key is missing from passed-in hash" do
+ it_behaves_like :key_error, -> obj, key { "%{#{key}}" % obj }, { a: 5 }
+ end
+
+ it "formats multiple expressions" do
+ ("%b %x %d %s" % [10, 10, 10, 10]).should == "1010 a 10 10"
+ end
+
+ it "formats expressions mid string" do
+ ("hello %s!" % "world").should == "hello world!"
+ end
+
+ it "formats %% into %" do
+ ("%d%% %s" % [10, "of chickens!"]).should == "10% of chickens!"
+ end
+
+ describe "output's encoding" do
+ it "is the same as the format string if passed value is encoding-compatible" do
+ [Encoding::BINARY, Encoding::US_ASCII, Encoding::UTF_8, Encoding::SHIFT_JIS].each do |encoding|
+ ("hello %s!".encode(encoding) % "world").encoding.should == encoding
+ end
+ end
+
+ it "negotiates a compatible encoding if necessary" do
+ ("hello %s" % 195.chr).encoding.should == Encoding::BINARY
+ ("hello %s".encode("shift_jis") % "wörld").encoding.should == Encoding::UTF_8
+ end
+
+ it "raises if a compatible encoding can't be found" do
+ -> { "hello %s".encode("utf-8") % "world".encode("UTF-16LE") }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ it "raises an error if single % appears at the end" do
+ -> { ("%" % []) }.should.raise(ArgumentError)
+ -> { ("foo%" % [])}.should.raise(ArgumentError)
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "formats single % character before a newline as literal %" do
+ ("%\n" % []).should == "%\n"
+ ("foo%\n" % []).should == "foo%\n"
+ ("%\n.3f" % 1.2).should == "%\n.3f"
+ end
+
+ it "formats single % character before a NUL as literal %" do
+ ("%\0" % []).should == "%\0"
+ ("foo%\0" % []).should == "foo%\0"
+ ("%\0.3f" % 1.2).should == "%\0.3f"
+ end
+
+ it "raises an error if single % appears anywhere else" do
+ -> { (" % " % []) }.should.raise(ArgumentError)
+ -> { ("foo%quux" % []) }.should.raise(ArgumentError)
+ end
+
+ it "raises an error if NULL or \\n appear anywhere else in the format string" do
+ begin
+ old_debug, $DEBUG = $DEBUG, false
+
+ -> { "%.\n3f" % 1.2 }.should.raise(ArgumentError)
+ -> { "%.3\nf" % 1.2 }.should.raise(ArgumentError)
+ -> { "%.\03f" % 1.2 }.should.raise(ArgumentError)
+ -> { "%.3\0f" % 1.2 }.should.raise(ArgumentError)
+ ensure
+ $DEBUG = old_debug
+ end
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError if % is not followed by a conversion specifier" do
+ -> { "%" % [] }.should.raise(ArgumentError)
+ -> { "%\n" % [] }.should.raise(ArgumentError)
+ -> { "%\0" % [] }.should.raise(ArgumentError)
+ -> { " % " % [] }.should.raise(ArgumentError)
+ -> { "%.\n3f" % 1.2 }.should.raise(ArgumentError)
+ -> { "%.3\nf" % 1.2 }.should.raise(ArgumentError)
+ -> { "%.\03f" % 1.2 }.should.raise(ArgumentError)
+ -> { "%.3\0f" % 1.2 }.should.raise(ArgumentError)
+ end
+ end
+
+ it "ignores unused arguments when $DEBUG is false" do
+ begin
+ old_debug = $DEBUG
+ $DEBUG = false
+
+ ("" % [1, 2, 3]).should == ""
+ ("%s" % [1, 2, 3]).should == "1"
+ ensure
+ $DEBUG = old_debug
+ end
+ end
+
+ it "raises an ArgumentError for unused arguments when $DEBUG is true" do
+ begin
+ old_debug = $DEBUG
+ $DEBUG = true
+ s = $stderr
+ $stderr = IOStub.new
+
+ -> { "" % [1, 2, 3] }.should.raise(ArgumentError)
+ -> { "%s" % [1, 2, 3] }.should.raise(ArgumentError)
+ ensure
+ $DEBUG = old_debug
+ $stderr = s
+ end
+ end
+
+ it "always allows unused arguments when positional argument style is used" do
+ begin
+ old_debug = $DEBUG
+ $DEBUG = false
+
+ ("%2$s" % [1, 2, 3]).should == "2"
+ $DEBUG = true
+ ("%2$s" % [1, 2, 3]).should == "2"
+ ensure
+ $DEBUG = old_debug
+ end
+ end
+
+ ruby_version_is ""..."3.4" do
+ it "replaces trailing absolute argument specifier without type with percent sign" do
+ ("hello %1$" % "foo").should == "hello %"
+ end
+ end
+
+ ruby_version_is "3.4" do
+ it "raises an ArgumentError if absolute argument specifier is followed by a conversion specifier" do
+ -> { "hello %1$" % "foo" }.should.raise(ArgumentError)
+ end
+ end
+
+ it "raises an ArgumentError when given invalid argument specifiers" do
+ -> { "%1" % [] }.should.raise(ArgumentError)
+ -> { "%+" % [] }.should.raise(ArgumentError)
+ -> { "%-" % [] }.should.raise(ArgumentError)
+ -> { "%#" % [] }.should.raise(ArgumentError)
+ -> { "%0" % [] }.should.raise(ArgumentError)
+ -> { "%*" % [] }.should.raise(ArgumentError)
+ -> { "%." % [] }.should.raise(ArgumentError)
+ -> { "%_" % [] }.should.raise(ArgumentError)
+ -> { "%0$s" % "x" }.should.raise(ArgumentError)
+ -> { "%*0$s" % [5, "x"] }.should.raise(ArgumentError)
+ -> { "%*1$.*0$1$s" % [1, 2, 3] }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when multiple positional argument tokens are given for one format specifier" do
+ -> { "%1$1$s" % "foo" }.should.raise(ArgumentError)
+ end
+
+ it "respects positional arguments and precision tokens given for one format specifier" do
+ ("%2$1d" % [1, 0]).should == "0"
+ ("%2$1d" % [0, 1]).should == "1"
+
+ ("%2$.2f" % [1, 0]).should == "0.00"
+ ("%2$.2f" % [0, 1]).should == "1.00"
+ end
+
+ it "allows more than one digit of position" do
+ ("%50$d" % (0..100).to_a).should == "49"
+ end
+
+ it "raises an ArgumentError when multiple width star tokens are given for one format specifier" do
+ -> { "%**s" % [5, 5, 5] }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when a width star token is seen after a width token" do
+ -> { "%5*s" % [5, 5] }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when multiple precision tokens are given" do
+ -> { "%.5.5s" % 5 }.should.raise(ArgumentError)
+ -> { "%.5.*s" % [5, 5] }.should.raise(ArgumentError)
+ -> { "%.*.5s" % [5, 5] }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when there are less arguments than format specifiers" do
+ ("foo" % []).should == "foo"
+ -> { "%s" % [] }.should.raise(ArgumentError)
+ -> { "%s %s" % [1] }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when absolute and relative argument numbers are mixed" do
+ -> { "%s %1$s" % "foo" }.should.raise(ArgumentError)
+ -> { "%1$s %s" % "foo" }.should.raise(ArgumentError)
+
+ -> { "%s %2$s" % ["foo", "bar"] }.should.raise(ArgumentError)
+ -> { "%2$s %s" % ["foo", "bar"] }.should.raise(ArgumentError)
+
+ -> { "%*2$s" % [5, 5, 5] }.should.raise(ArgumentError)
+ -> { "%*.*2$s" % [5, 5, 5] }.should.raise(ArgumentError)
+ -> { "%*2$.*2$s" % [5, 5, 5] }.should.raise(ArgumentError)
+ -> { "%*.*2$s" % [5, 5, 5] }.should.raise(ArgumentError)
+ end
+
+ it "allows reuse of the one argument multiple via absolute argument numbers" do
+ ("%1$s %1$s" % "foo").should == "foo foo"
+ ("%1$s %2$s %1$s %2$s" % ["foo", "bar"]).should == "foo bar foo bar"
+ end
+
+ it "always interprets an array argument as a list of argument parameters" do
+ -> { "%p" % [] }.should.raise(ArgumentError)
+ ("%p" % [1]).should == "1"
+ ("%p %p" % [1, 2]).should == "1 2"
+ end
+
+ it "always interprets an array subclass argument as a list of argument parameters" do
+ -> { "%p" % StringSpecs::MyArray[] }.should.raise(ArgumentError)
+ ("%p" % StringSpecs::MyArray[1]).should == "1"
+ ("%p %p" % StringSpecs::MyArray[1, 2]).should == "1 2"
+ end
+
+ it "allows positional arguments for width star and precision star arguments" do
+ ("%*1$.*2$3$d" % [10, 5, 1]).should == " 00001"
+ end
+
+ it "allows negative width to imply '-' flag" do
+ ("%*1$.*2$3$d" % [-10, 5, 1]).should == "00001 "
+ ("%-*1$.*2$3$d" % [10, 5, 1]).should == "00001 "
+ ("%-*1$.*2$3$d" % [-10, 5, 1]).should == "00001 "
+ end
+
+ it "ignores negative precision" do
+ ("%*1$.*2$3$d" % [10, -5, 1]).should == " 1"
+ end
+
+ it "allows a star to take an argument number to use as the width" do
+ ("%1$*2$s" % ["a", 8]).should == " a"
+ ("%1$*10$s" % ["a",0,0,0,0,0,0,0,0,8]).should == " a"
+ end
+
+ it "calls to_int on width star and precision star tokens" do
+ w = mock('10')
+ w.should_receive(:to_int).and_return(10)
+
+ p = mock('5')
+ p.should_receive(:to_int).and_return(5)
+
+ ("%*.*f" % [w, p, 1]).should == " 1.00000"
+
+
+ w = mock('10')
+ w.should_receive(:to_int).and_return(10)
+
+ p = mock('5')
+ p.should_receive(:to_int).and_return(5)
+
+ ("%*.*d" % [w, p, 1]).should == " 00001"
+ end
+
+ it "does not call #to_a to convert the argument" do
+ x = mock("string modulo to_a")
+ x.should_not_receive(:to_a)
+ x.should_receive(:to_s).and_return("x")
+
+ ("%s" % x).should == "x"
+ end
+
+ it "calls #to_ary to convert the argument" do
+ x = mock("string modulo to_ary")
+ x.should_not_receive(:to_s)
+ x.should_receive(:to_ary).and_return(["x"])
+
+ ("%s" % x).should == "x"
+ end
+
+ it "wraps the object in an Array if #to_ary returns nil" do
+ x = mock("string modulo to_ary")
+ x.should_receive(:to_ary).and_return(nil)
+ x.should_receive(:to_s).and_return("x")
+
+ ("%s" % x).should == "x"
+ end
+
+ it "raises a TypeError if #to_ary does not return an Array" do
+ x = mock("string modulo to_ary")
+ x.should_receive(:to_ary).and_return("x")
+
+ -> { "%s" % x }.should.raise(TypeError)
+ end
+
+ it "tries to convert the argument to Array by calling #to_ary" do
+ obj = mock('[1,2]')
+ def obj.to_ary() [1, 2] end
+ def obj.to_s() "obj" end
+ ("%s %s" % obj).should == "1 2"
+ ("%s" % obj).should == "1"
+ end
+
+ it "doesn't return subclass instances when called on a subclass" do
+ universal = mock('0')
+ def universal.to_int() 0 end
+ def universal.to_str() "0" end
+ def universal.to_f() 0.0 end
+
+ [
+ "", "foo",
+ "%b", "%B", "%c", "%d", "%e", "%E",
+ "%f", "%g", "%G", "%i", "%o", "%p",
+ "%s", "%u", "%x", "%X"
+ ].each do |format|
+ (StringSpecs::MyString.new(format) % universal).should.instance_of?(String)
+ end
+ end
+
+ it "supports binary formats using %b for positive numbers" do
+ ("%b" % 10).should == "1010"
+ ("% b" % 10).should == " 1010"
+ ("%1$b" % [10, 20]).should == "1010"
+ ("%#b" % 10).should == "0b1010"
+ ("%+b" % 10).should == "+1010"
+ ("%-9b" % 10).should == "1010 "
+ ("%05b" % 10).should == "01010"
+ ("%*b" % [10, 6]).should == " 110"
+ ("%*b" % [-10, 6]).should == "110 "
+ ("%.4b" % 2).should == "0010"
+ ("%.32b" % 2147483648).should == "10000000000000000000000000000000"
+ end
+
+ it "supports binary formats using %b for negative numbers" do
+ ("%b" % -5).should == "..1011"
+ ("%0b" % -5).should == "..1011"
+ ("%.1b" % -5).should == "..1011"
+ ("%.7b" % -5).should == "..11011"
+ ("%.10b" % -5).should == "..11111011"
+ ("% b" % -5).should == "-101"
+ ("%+b" % -5).should == "-101"
+ not_supported_on :opal do
+ ("%b" % -(2 ** 64 + 5)).should ==
+ "..101111111111111111111111111111111111111111111111111111111111111011"
+ end
+ end
+
+ it "supports binary formats using %B with same behaviour as %b except for using 0B instead of 0b for #" do
+ ("%B" % 10).should == ("%b" % 10)
+ ("% B" % 10).should == ("% b" % 10)
+ ("%1$B" % [10, 20]).should == ("%1$b" % [10, 20])
+ ("%+B" % 10).should == ("%+b" % 10)
+ ("%-9B" % 10).should == ("%-9b" % 10)
+ ("%05B" % 10).should == ("%05b" % 10)
+ ("%*B" % [10, 6]).should == ("%*b" % [10, 6])
+ ("%*B" % [-10, 6]).should == ("%*b" % [-10, 6])
+
+ ("%B" % -5).should == ("%b" % -5)
+ ("%0B" % -5).should == ("%0b" % -5)
+ ("%.1B" % -5).should == ("%.1b" % -5)
+ ("%.7B" % -5).should == ("%.7b" % -5)
+ ("%.10B" % -5).should == ("%.10b" % -5)
+ ("% B" % -5).should == ("% b" % -5)
+ ("%+B" % -5).should == ("%+b" % -5)
+ not_supported_on :opal do
+ ("%B" % -(2 ** 64 + 5)).should == ("%b" % -(2 ** 64 + 5))
+ end
+
+ ("%#B" % 10).should == "0B1010"
+ end
+
+ it "supports character formats using %c" do
+ ("%c" % 10).should == "\n"
+ ("%2$c" % [10, 11, 14]).should == "\v"
+ ("%-4c" % 10).should == "\n "
+ ("%*c" % [10, 3]).should == " \003"
+ ("%c" % 42).should == "*"
+
+ -> { "%c" % Object }.should.raise(TypeError)
+ end
+
+ it "supports single character strings as argument for %c" do
+ ("%c" % 'A').should == "A"
+ end
+
+ it "supports only the first character as argument for %c" do
+ ("%c" % 'AA').should == "A"
+ end
+
+ it "calls to_str on argument for %c formats" do
+ obj = mock('A')
+ obj.should_receive(:to_str).and_return('A')
+
+ ("%c" % obj).should == "A"
+ end
+
+ it "calls #to_ary on argument for %c formats" do
+ obj = mock('65')
+ obj.should_receive(:to_ary).and_return([65])
+ ("%c" % obj).should == ("%c" % [65])
+ end
+
+ it "calls #to_int on argument for %c formats, if the argument does not respond to #to_ary" do
+ obj = mock('65')
+ obj.should_receive(:to_int).and_return(65)
+
+ ("%c" % obj).should == ("%c" % 65)
+ end
+
+ %w(d i).each do |f|
+ format = "%" + f
+
+ it "supports integer formats using #{format}" do
+ ("%#{f}" % 10).should == "10"
+ ("% #{f}" % 10).should == " 10"
+ ("%1$#{f}" % [10, 20]).should == "10"
+ ("%+#{f}" % 10).should == "+10"
+ ("%-7#{f}" % 10).should == "10 "
+ ("%04#{f}" % 10).should == "0010"
+ ("%*#{f}" % [10, 4]).should == " 4"
+ ("%6.4#{f}" % 123).should == " 0123"
+ end
+
+ it "supports negative integers using #{format}" do
+ ("%#{f}" % -5).should == "-5"
+ ("%3#{f}" % -5).should == " -5"
+ ("%03#{f}" % -5).should == "-05"
+ ("%+03#{f}" % -5).should == "-05"
+ ("%+.2#{f}" % -5).should == "-05"
+ ("%-3#{f}" % -5).should == "-5 "
+ ("%6.4#{f}" % -123).should == " -0123"
+ end
+
+ it "supports negative integers using #{format}, giving priority to `-`" do
+ ("%-03#{f}" % -5).should == "-5 "
+ ("%+-03#{f}" % -5).should == "-5 "
+ end
+ end
+
+ it "supports float formats using %e" do
+ ("%e" % 10).should == "1.000000e+01"
+ ("% e" % 10).should == " 1.000000e+01"
+ ("%1$e" % 10).should == "1.000000e+01"
+ ("%#e" % 10).should == "1.000000e+01"
+ ("%+e" % 10).should == "+1.000000e+01"
+ ("%-7e" % 10).should == "1.000000e+01"
+ ("%05e" % 10).should == "1.000000e+01"
+ ("%*e" % [10, 9]).should == "9.000000e+00"
+ end
+
+ it "supports float formats using %e, but Inf, -Inf, and NaN are not floats" do
+ ("%e" % 1e1020).should == "Inf"
+ ("%e" % -1e1020).should == "-Inf"
+ ("%e" % -Float::NAN).should == "NaN"
+ ("%e" % Float::NAN).should == "NaN"
+ end
+
+ it "supports float formats using %E, but Inf, -Inf, and NaN are not floats" do
+ ("%E" % 1e1020).should == "Inf"
+ ("%E" % -1e1020).should == "-Inf"
+ ("%-10E" % 1e1020).should == "Inf "
+ ("%10E" % 1e1020).should == " Inf"
+ ("%+E" % 1e1020).should == "+Inf"
+ ("% E" % 1e1020).should == " Inf"
+ ("%E" % Float::NAN).should == "NaN"
+ ("%E" % -Float::NAN).should == "NaN"
+ end
+
+ it "supports float formats using %E" do
+ ("%E" % 10).should == "1.000000E+01"
+ ("% E" % 10).should == " 1.000000E+01"
+ ("%1$E" % 10).should == "1.000000E+01"
+ ("%#E" % 10).should == "1.000000E+01"
+ ("%+E" % 10).should == "+1.000000E+01"
+ ("%-7E" % 10).should == "1.000000E+01"
+ ("%05E" % 10).should == "1.000000E+01"
+ ("%*E" % [10, 9]).should == "9.000000E+00"
+ end
+
+ it "pads with spaces for %E with Inf, -Inf, and NaN" do
+ ("%010E" % -1e1020).should == " -Inf"
+ ("%010E" % 1e1020).should == " Inf"
+ ("%010E" % Float::NAN).should == " NaN"
+ end
+
+ it "supports float formats using %f" do
+ ("%f" % 10).should == "10.000000"
+ ("% f" % 10).should == " 10.000000"
+ ("%1$f" % 10).should == "10.000000"
+ ("%#f" % 10).should == "10.000000"
+ ("%#0.3f" % 10).should == "10.000"
+ ("%+f" % 10).should == "+10.000000"
+ ("%-7f" % 10).should == "10.000000"
+ ("%05f" % 10).should == "10.000000"
+ ("%0.5f" % 10).should == "10.00000"
+ ("%*f" % [10, 9]).should == " 9.000000"
+ end
+
+ it "supports float formats using %g" do
+ ("%g" % 10).should == "10"
+ ("% g" % 10).should == " 10"
+ ("%1$g" % 10).should == "10"
+ ("%#g" % 10).should == "10.0000"
+ ("%#.3g" % 10).should == "10.0"
+ ("%+g" % 10).should == "+10"
+ ("%-7g" % 10).should == "10 "
+ ("%05g" % 10).should == "00010"
+ ("%g" % 10**10).should == "1e+10"
+ ("%*g" % [10, 9]).should == " 9"
+ end
+
+ it "supports float formats using %G" do
+ ("%G" % 10).should == "10"
+ ("% G" % 10).should == " 10"
+ ("%1$G" % 10).should == "10"
+ ("%#G" % 10).should == "10.0000"
+ ("%#.3G" % 10).should == "10.0"
+ ("%+G" % 10).should == "+10"
+ ("%-7G" % 10).should == "10 "
+ ("%05G" % 10).should == "00010"
+ ("%G" % 10**10).should == "1E+10"
+ ("%*G" % [10, 9]).should == " 9"
+ end
+
+ it "supports octal formats using %o for positive numbers" do
+ ("%o" % 10).should == "12"
+ ("% o" % 10).should == " 12"
+ ("%1$o" % [10, 20]).should == "12"
+ ("%#o" % 10).should == "012"
+ ("%+o" % 10).should == "+12"
+ ("%-9o" % 10).should == "12 "
+ ("%05o" % 10).should == "00012"
+ ("%*o" % [10, 6]).should == " 6"
+ end
+
+ it "supports octal formats using %o for negative numbers" do
+ # These are incredibly wrong. -05 == -5, not 7177777...whatever
+ ("%o" % -5).should == "..73"
+ ("%0o" % -5).should == "..73"
+ ("%.4o" % 20).should == "0024"
+ ("%.1o" % -5).should == "..73"
+ ("%.7o" % -5).should == "..77773"
+ ("%.10o" % -5).should == "..77777773"
+
+ ("% o" % -26).should == "-32"
+ ("%+o" % -26).should == "-32"
+ not_supported_on :opal do
+ ("%o" % -(2 ** 64 + 5)).should == "..75777777777777777777773"
+ end
+ end
+
+ it "supports inspect formats using %p" do
+ ("%p" % 10).should == "10"
+ ("%1$p" % [10, 5]).should == "10"
+ ("%-22p" % 10).should == "10 "
+ ("%*p" % [10, 10]).should == " 10"
+ ("%p" % {capture: 1}).should == {capture: 1}.inspect
+ ("%p" % "str").should == "\"str\""
+ end
+
+ it "calls inspect on arguments for %p format" do
+ obj = mock('obj')
+ def obj.inspect() "obj" end
+ ("%p" % obj).should == "obj"
+
+ # undef is not working
+ # obj = mock('obj')
+ # class << obj; undef :inspect; end
+ # def obj.method_missing(*args) "obj" end
+ # ("%p" % obj).should == "obj"
+ end
+
+ it "supports string formats using %s" do
+ ("%s" % "hello").should == "hello"
+ ("%s" % "").should == ""
+ ("%s" % 10).should == "10"
+ ("%1$s" % [10, 8]).should == "10"
+ ("%-5s" % 10).should == "10 "
+ ("%*s" % [10, 9]).should == " 9"
+ end
+
+ it "respects a space padding request not as part of the width" do
+ x = "% -5s" % ["foo"]
+ x.should == "foo "
+ end
+
+ it "calls to_s on non-String arguments for %s format" do
+ obj = mock('obj')
+ def obj.to_s() "obj" end
+
+ ("%s" % obj).should == "obj"
+
+ # undef doesn't work
+ # obj = mock('obj')
+ # class << obj; undef :to_s; end
+ # def obj.method_missing(*args) "obj" end
+ #
+ # ("%s" % obj).should == "obj"
+ end
+
+ # MRI crashes on this one.
+ # See http://groups.google.com/group/ruby-core-google/t/c285c18cd94c216d
+ it "raises an ArgumentError for huge precisions for %s" do
+ block = -> { "%.25555555555555555555555555555555555555s" % "hello world" }
+ block.should.raise(ArgumentError)
+ end
+
+ # Note: %u has been changed to an alias for %d in 1.9.
+ it "supports unsigned formats using %u" do
+ ("%u" % 10).should == "10"
+ ("% u" % 10).should == " 10"
+ ("%1$u" % [10, 20]).should == "10"
+ ("%+u" % 10).should == "+10"
+ ("%-7u" % 10).should == "10 "
+ ("%04u" % 10).should == "0010"
+ ("%*u" % [10, 4]).should == " 4"
+ end
+
+ it "formats negative values with a leading sign using %u" do
+ ("% u" % -26).should == "-26"
+ ("%+u" % -26).should == "-26"
+ end
+
+ it "supports negative bignums with %u or %d" do
+ ("%u" % -(2 ** 64 + 5)).should == "-18446744073709551621"
+ ("%d" % -(2 ** 64 + 5)).should == "-18446744073709551621"
+ end
+
+ it "supports hex formats using %x for positive numbers" do
+ ("%x" % 10).should == "a"
+ ("% x" % 10).should == " a"
+ ("%1$x" % [10, 20]).should == "a"
+ ("%#x" % 10).should == "0xa"
+ ("%+x" % 10).should == "+a"
+ ("%-9x" % 10).should == "a "
+ ("%05x" % 10).should == "0000a"
+ ("%*x" % [10, 6]).should == " 6"
+ ("%.4x" % 20).should == "0014"
+ ("%x" % 0xFFFFFFFF).should == "ffffffff"
+ end
+
+ it "supports hex formats using %x for negative numbers" do
+ ("%x" % -5).should == "..fb"
+ ("%0x" % -5).should == "..fb"
+ ("%.1x" % -5).should == "..fb"
+ ("%.7x" % -5).should == "..ffffb"
+ ("%.10x" % -5).should == "..fffffffb"
+ ("% x" % -26).should == "-1a"
+ ("%+x" % -26).should == "-1a"
+ not_supported_on :opal do
+ ("%x" % -(2 ** 64 + 5)).should == "..fefffffffffffffffb"
+ end
+ end
+
+ it "supports hex formats using %X for positive numbers" do
+ ("%X" % 10).should == "A"
+ ("% X" % 10).should == " A"
+ ("%1$X" % [10, 20]).should == "A"
+ ("%#X" % 10).should == "0XA"
+ ("%+X" % 10).should == "+A"
+ ("%-9X" % 10).should == "A "
+ ("%05X" % 10).should == "0000A"
+ ("%*X" % [10, 6]).should == " 6"
+ ("%X" % 0xFFFFFFFF).should == "FFFFFFFF"
+ end
+
+ it "supports hex formats using %X for negative numbers" do
+ ("%X" % -5).should == "..FB"
+ ("%0X" % -5).should == "..FB"
+ ("%.1X" % -5).should == "..FB"
+ ("%.7X" % -5).should == "..FFFFB"
+ ("%.10X" % -5).should == "..FFFFFFFB"
+ ("% X" % -26).should == "-1A"
+ ("%+X" % -26).should == "-1A"
+ not_supported_on :opal do
+ ("%X" % -(2 ** 64 + 5)).should == "..FEFFFFFFFFFFFFFFFB"
+ end
+ end
+
+ it "formats zero without prefix using %#x" do
+ ("%#x" % 0).should == "0"
+ end
+
+ it "formats zero without prefix using %#X" do
+ ("%#X" % 0).should == "0"
+ end
+
+ %w(b d i o u x X).each do |f|
+ format = "%" + f
+
+ it "behaves as if calling Kernel#Integer for #{format} argument, if it does not respond to #to_ary" do
+ (format % "10").should == (format % Kernel.Integer("10"))
+ (format % "0x42").should == (format % Kernel.Integer("0x42"))
+ (format % "0b1101").should == (format % Kernel.Integer("0b1101"))
+ (format % "0b1101_0000").should == (format % Kernel.Integer("0b1101_0000"))
+ (format % "0777").should == (format % Kernel.Integer("0777"))
+ -> {
+ # see [ruby-core:14139] for more details
+ (format % "0777").should == (format % Kernel.Integer("0777"))
+ }.should_not.raise(ArgumentError)
+
+ -> { format % "0__7_7_7" }.should.raise(ArgumentError)
+
+ -> { format % "" }.should.raise(ArgumentError)
+ -> { format % "x" }.should.raise(ArgumentError)
+ -> { format % "5x" }.should.raise(ArgumentError)
+ -> { format % "08" }.should.raise(ArgumentError)
+ -> { format % "0b2" }.should.raise(ArgumentError)
+ -> { format % "123__456" }.should.raise(ArgumentError)
+
+ obj = mock('5')
+ obj.should_receive(:to_i).and_return(5)
+ (format % obj).should == (format % 5)
+
+ obj = mock('6')
+ obj.stub!(:to_i).and_return(5)
+ obj.should_receive(:to_int).and_return(6)
+ (format % obj).should == (format % 6)
+ end
+ end
+
+ %w(e E f g G).each do |f|
+ format = "%" + f
+
+ it "tries to convert the passed argument to an Array using #to_ary" do
+ obj = mock('3.14')
+ obj.should_receive(:to_ary).and_return([3.14])
+ (format % obj).should == (format % [3.14])
+ end
+
+ it "behaves as if calling Kernel#Float for #{format} arguments, when the passed argument does not respond to #to_ary" do
+ (format % 10).should == (format % 10.0)
+ (format % "-10.4e-20").should == (format % -10.4e-20)
+ (format % ".5").should == (format % 0.5)
+ (format % "-.5").should == (format % -0.5)
+
+ ruby_version_is "3.4" do
+ (format % "10.").should == (format % 10)
+ end
+
+ # Something's strange with this spec:
+ # it works just fine in individual mode, but not when run as part of a group
+ (format % "10_1_0.5_5_5").should == (format % 1010.555)
+
+ (format % "0777").should == (format % 777)
+
+ -> { format % "" }.should.raise(ArgumentError)
+ -> { format % "x" }.should.raise(ArgumentError)
+ -> { format % "." }.should.raise(ArgumentError)
+ -> { format % "5x" }.should.raise(ArgumentError)
+ -> { format % "0b1" }.should.raise(ArgumentError)
+ -> { format % "10e10.5" }.should.raise(ArgumentError)
+ -> { format % "10__10" }.should.raise(ArgumentError)
+ -> { format % "10.10__10" }.should.raise(ArgumentError)
+
+ obj = mock('5.0')
+ obj.should_receive(:to_f).and_return(5.0)
+ (format % obj).should == (format % 5.0)
+ end
+
+ it "behaves as if calling Kernel#Float for #{format} arguments, when the passed argument is hexadecimal string" do
+ (format % "0xA").should == (format % 0xA)
+ end
+ end
+
+ describe "when format string contains %{} sections" do
+ it "replaces %{} sections with values from passed-in hash" do
+ ("%{foo}bar" % {foo: 'oof'}).should == "oofbar"
+ end
+
+ it "should raise ArgumentError if no hash given" do
+ -> {"%{foo}" % []}.should.raise(ArgumentError)
+ end
+ end
+
+ describe "when format string contains %<> formats" do
+ it "uses the named argument for the format's value" do
+ ("%<foo>d" % {foo: 1}).should == "1"
+ end
+
+ it "raises KeyError if key is missing from passed-in hash" do
+ -> {"%<foo>d" % {}}.should.raise(KeyError)
+ end
+
+ it "should raise ArgumentError if no hash given" do
+ -> {"%<foo>" % []}.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/multiply_spec.rb b/spec/ruby/core/string/multiply_spec.rb
new file mode 100644
index 0000000000..c15f670c46
--- /dev/null
+++ b/spec/ruby/core/string/multiply_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/string/times'
+
+describe "String#*" do
+ it_behaves_like :string_times, :*, -> str, times { str * times }
+end
diff --git a/spec/ruby/core/string/new_spec.rb b/spec/ruby/core/string/new_spec.rb
new file mode 100644
index 0000000000..aadf1e0e46
--- /dev/null
+++ b/spec/ruby/core/string/new_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String.new" do
+ it "returns an instance of String" do
+ str = String.new
+ str.should.instance_of?(String)
+ end
+
+ it "accepts an encoding argument" do
+ xA4xA2 = [0xA4, 0xA2].pack('CC').force_encoding 'utf-8'
+ str = String.new(xA4xA2, encoding: 'euc-jp')
+ str.encoding.should == Encoding::EUC_JP
+ end
+
+ it "accepts a capacity argument" do
+ String.new("", capacity: 100_000).should == ""
+ String.new("abc", capacity: 100_000).should == "abc"
+ end
+
+ it "returns a fully-formed String" do
+ str = String.new
+ str.size.should == 0
+ str << "more"
+ str.should == "more"
+ end
+
+ it "returns a new string given a string argument" do
+ str1 = "test"
+ str = String.new(str1)
+ str.should.instance_of?(String)
+ str.should == str1
+ str << "more"
+ str.should == "testmore"
+ end
+
+ it "returns an instance of a subclass" do
+ a = StringSpecs::MyString.new("blah")
+ a.should.instance_of?(StringSpecs::MyString)
+ a.should == "blah"
+ end
+
+ it "is called on subclasses" do
+ s = StringSpecs::SubString.new
+ s.special.should == nil
+ s.should == ""
+
+ s = StringSpecs::SubString.new "subclass"
+ s.special.should == "subclass"
+ s.should == ""
+ end
+
+ it "raises TypeError on inconvertible object" do
+ -> { String.new 5 }.should.raise(TypeError)
+ -> { String.new nil }.should.raise(TypeError)
+ end
+
+ it "returns a binary String" do
+ String.new.encoding.should == Encoding::BINARY
+ end
+end
diff --git a/spec/ruby/core/string/next_spec.rb b/spec/ruby/core/string/next_spec.rb
new file mode 100644
index 0000000000..fcd3e5ef90
--- /dev/null
+++ b/spec/ruby/core/string/next_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/succ'
+
+describe "String#next" do
+ it_behaves_like :string_succ, :next
+end
+
+describe "String#next!" do
+ it_behaves_like :string_succ_bang, :"next!"
+end
diff --git a/spec/ruby/core/string/oct_spec.rb b/spec/ruby/core/string/oct_spec.rb
new file mode 100644
index 0000000000..7637692217
--- /dev/null
+++ b/spec/ruby/core/string/oct_spec.rb
@@ -0,0 +1,88 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Note: We can't completely spec this in terms of to_int() because hex()
+# allows the base to be changed by a base specifier in the string.
+# See http://groups.google.com/group/ruby-core-google/browse_frm/thread/b53e9c2003425703
+describe "String#oct" do
+ it "treats numeric digits as base-8 digits by default" do
+ "0".oct.should == 0
+ "77".oct.should == 077
+ "077".oct.should == 077
+ end
+
+ it "accepts numbers formatted as binary" do
+ "0b1010".oct.should == 0b1010
+ end
+
+ it "accepts numbers formatted as hexadecimal" do
+ "0xFF".oct.should == 0xFF
+ end
+
+ it "accepts numbers formatted as decimal" do
+ "0d500".oct.should == 500
+ end
+
+ describe "with a leading minus sign" do
+ it "treats numeric digits as base-8 digits by default" do
+ "-12348".oct.should == -01234
+ end
+
+ it "accepts numbers formatted as binary" do
+ "-0b0101".oct.should == -0b0101
+ end
+
+ it "accepts numbers formatted as hexadecimal" do
+ "-0xEE".oct.should == -0xEE
+ end
+
+ it "accepts numbers formatted as decimal" do
+ "-0d500".oct.should == -500
+ end
+ end
+
+ describe "with a leading plus sign" do
+ it "treats numeric digits as base-8 digits by default" do
+ "+12348".oct.should == 01234
+ end
+
+ it "accepts numbers formatted as binary" do
+ "+0b1010".oct.should == 0b1010
+ end
+
+ it "accepts numbers formatted as hexadecimal" do
+ "+0xFF".oct.should == 0xFF
+ end
+
+ it "accepts numbers formatted as decimal" do
+ "+0d500".oct.should == 500
+ end
+ end
+
+ it "accepts a single underscore separating digits" do
+ "755_333".oct.should == 0755_333
+ end
+
+ it "does not accept a sequence of underscores as part of a number" do
+ "7__3".oct.should == 07
+ "7___3".oct.should == 07
+ "7__5".oct.should == 07
+ end
+
+ it "ignores characters that are incorrect for the base-8 digits" do
+ "0o".oct.should == 0
+ "5678".oct.should == 0567
+ end
+
+ it "returns 0 if no characters can be interpreted as a base-8 number" do
+ "".oct.should == 0
+ "+-5".oct.should == 0
+ "wombat".oct.should == 0
+ end
+
+ it "returns 0 for strings with leading underscores" do
+ "_7".oct.should == 0
+ "_07".oct.should == 0
+ " _7".oct.should == 0
+ end
+end
diff --git a/spec/ruby/core/string/ord_spec.rb b/spec/ruby/core/string/ord_spec.rb
new file mode 100644
index 0000000000..5a17fc1d87
--- /dev/null
+++ b/spec/ruby/core/string/ord_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+
+describe "String#ord" do
+ it "returns an Integer" do
+ 'a'.ord.should.instance_of?(Integer)
+ end
+
+ it "returns the codepoint of the first character in the String" do
+ 'a'.ord.should == 97
+ end
+
+
+ it "ignores subsequent characters" do
+ "\u{287}a".ord.should == "\u{287}".ord
+ end
+
+ it "understands multibyte characters" do
+ "\u{9879}".ord.should == 39033
+ end
+
+ it "is equivalent to #codepoints.first" do
+ "\u{981}\u{982}".ord.should == "\u{981}\u{982}".codepoints.first
+ end
+
+ it "raises an ArgumentError if called on an empty String" do
+ -> { ''.ord }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the character is broken" do
+ s = "©".dup.force_encoding("US-ASCII")
+ -> { s.ord }.should.raise(ArgumentError, "invalid byte sequence in US-ASCII")
+ end
+end
diff --git a/spec/ruby/core/string/partition_spec.rb b/spec/ruby/core/string/partition_spec.rb
new file mode 100644
index 0000000000..29fe910b39
--- /dev/null
+++ b/spec/ruby/core/string/partition_spec.rb
@@ -0,0 +1,63 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/partition'
+
+describe "String#partition with String" do
+ it_behaves_like :string_partition, :partition
+
+ it "returns an array of substrings based on splitting on the given string" do
+ "hello world".partition("o").should == ["hell", "o", " world"]
+ end
+
+ it "always returns 3 elements" do
+ "hello".partition("x").should == ["hello", "", ""]
+ "hello".partition("hello").should == ["", "hello", ""]
+ end
+
+ it "accepts regexp" do
+ "hello!".partition(/l./).should == ["he", "ll", "o!"]
+ end
+
+ it "sets global vars if regexp used" do
+ "hello!".partition(/(.l)(.o)/)
+ $1.should == "el"
+ $2.should == "lo"
+ end
+
+ it "converts its argument using :to_str" do
+ find = mock('l')
+ find.should_receive(:to_str).and_return("l")
+ "hello".partition(find).should == ["he","l","lo"]
+ end
+
+ it "raises an error if not convertible to string" do
+ ->{ "hello".partition(5) }.should.raise(TypeError)
+ ->{ "hello".partition(nil) }.should.raise(TypeError)
+ end
+
+ it "takes precedence over a given block" do
+ "hello world".partition("o") { true }.should == ["hell", "o", " world"]
+ end
+
+ it "handles a pattern in a superset encoding" do
+ string = "hello".dup.force_encoding(Encoding::US_ASCII)
+
+ result = string.partition("é")
+
+ result.should == ["hello", "", ""]
+ result[0].encoding.should == Encoding::US_ASCII
+ result[1].encoding.should == Encoding::US_ASCII
+ result[2].encoding.should == Encoding::US_ASCII
+ end
+
+ it "handles a pattern in a subset encoding" do
+ pattern = "o".dup.force_encoding(Encoding::US_ASCII)
+
+ result = "héllo world".partition(pattern)
+
+ result.should == ["héll", "o", " world"]
+ result[0].encoding.should == Encoding::UTF_8
+ result[1].encoding.should == Encoding::US_ASCII
+ result[2].encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/string/plus_spec.rb b/spec/ruby/core/string/plus_spec.rb
new file mode 100644
index 0000000000..0464141c37
--- /dev/null
+++ b/spec/ruby/core/string/plus_spec.rb
@@ -0,0 +1,37 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/concat'
+
+describe "String#+" do
+ it_behaves_like :string_concat_encoding, :+
+ it_behaves_like :string_concat_type_coercion, :+
+
+ it "returns a new string containing the given string concatenated to self" do
+ ("" + "").should == ""
+ ("" + "Hello").should == "Hello"
+ ("Hello" + "").should == "Hello"
+ ("Ruby !" + "= Rubinius").should == "Ruby != Rubinius"
+ end
+
+ it "converts any non-String argument with #to_str" do
+ c = mock 'str'
+ c.should_receive(:to_str).any_number_of_times.and_return(' + 1 = 2')
+
+ ("1" + c).should == '1 + 1 = 2'
+ end
+
+ it "raises a TypeError when given any object that fails #to_str" do
+ -> { "" + Object.new }.should.raise(TypeError)
+ -> { "" + 65 }.should.raise(TypeError)
+ end
+
+ it "doesn't return subclass instances" do
+ (StringSpecs::MyString.new("hello") + "").should.instance_of?(String)
+ (StringSpecs::MyString.new("hello") + "foo").should.instance_of?(String)
+ (StringSpecs::MyString.new("hello") + StringSpecs::MyString.new("foo")).should.instance_of?(String)
+ (StringSpecs::MyString.new("hello") + StringSpecs::MyString.new("")).should.instance_of?(String)
+ (StringSpecs::MyString.new("") + StringSpecs::MyString.new("")).should.instance_of?(String)
+ ("hello" + StringSpecs::MyString.new("foo")).should.instance_of?(String)
+ ("hello" + StringSpecs::MyString.new("")).should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/string/prepend_spec.rb b/spec/ruby/core/string/prepend_spec.rb
new file mode 100644
index 0000000000..a8da4e62cb
--- /dev/null
+++ b/spec/ruby/core/string/prepend_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#prepend" do
+ it "prepends the given argument to self and returns self" do
+ str = "world"
+ str.prepend("hello ").should.equal?(str)
+ str.should == "hello world"
+ end
+
+ it "converts the given argument to a String using to_str" do
+ obj = mock("hello")
+ obj.should_receive(:to_str).and_return("hello")
+ a = " world!".prepend(obj)
+ a.should == "hello world!"
+ end
+
+ it "raises a TypeError if the given argument can't be converted to a String" do
+ -> { "hello ".prepend [] }.should.raise(TypeError)
+ -> { 'hello '.prepend mock('x') }.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ a = "hello"
+ a.freeze
+
+ -> { a.prepend "" }.should.raise(FrozenError)
+ -> { a.prepend "test" }.should.raise(FrozenError)
+ end
+
+ it "works when given a subclass instance" do
+ a = " world"
+ a.prepend StringSpecs::MyString.new("hello")
+ a.should == "hello world"
+ end
+
+ it "takes multiple arguments" do
+ str = " world"
+ str.prepend "he", "", "llo"
+ str.should == "hello world"
+ end
+
+ it "prepends the initial value when given arguments contain 2 self" do
+ str = "hello"
+ str.prepend str, str
+ str.should == "hellohellohello"
+ end
+
+ it "returns self when given no arguments" do
+ str = "hello"
+ str.prepend.should.equal?(str)
+ str.should == "hello"
+ end
+end
diff --git a/spec/ruby/core/string/replace_spec.rb b/spec/ruby/core/string/replace_spec.rb
new file mode 100644
index 0000000000..ef9bab4f3c
--- /dev/null
+++ b/spec/ruby/core/string/replace_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/replace'
+
+describe "String#replace" do
+ it_behaves_like :string_replace, :replace
+end
diff --git a/spec/ruby/core/string/reverse_spec.rb b/spec/ruby/core/string/reverse_spec.rb
new file mode 100644
index 0000000000..e37c1125db
--- /dev/null
+++ b/spec/ruby/core/string/reverse_spec.rb
@@ -0,0 +1,70 @@
+# encoding: utf-8
+# frozen_string_literal: false
+
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#reverse" do
+ it "returns a new string with the characters of self in reverse order" do
+ "stressed".reverse.should == "desserts"
+ "m".reverse.should == "m"
+ "".reverse.should == ""
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("stressed").reverse.should.instance_of?(String)
+ StringSpecs::MyString.new("m").reverse.should.instance_of?(String)
+ StringSpecs::MyString.new("").reverse.should.instance_of?(String)
+ end
+
+ it "reverses a string with multi byte characters" do
+ "微軟正黑體".reverse.should == "體黑正軟微"
+ end
+
+ it "works with a broken string" do
+ str = "微軟\xDF\xDE正黑體".force_encoding(Encoding::UTF_8)
+
+ str.valid_encoding?.should == false
+
+ str.reverse.should == "體黑正\xDE\xDF軟微"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "stressed".encode("US-ASCII").reverse.encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#reverse!" do
+ it "reverses self in place and always returns self" do
+ a = "stressed"
+ a.reverse!.should.equal?(a)
+ a.should == "desserts"
+
+ "".reverse!.should == ""
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { "anna".freeze.reverse! }.should.raise(FrozenError)
+ -> { "hello".freeze.reverse! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> { "".freeze.reverse! }.should.raise(FrozenError)
+ end
+
+ it "reverses a string with multi byte characters" do
+ str = "微軟正黑體"
+ str.reverse!
+ str.should == "體黑正軟微"
+ end
+
+ it "works with a broken string" do
+ str = "微軟\xDF\xDE正黑體".force_encoding(Encoding::UTF_8)
+
+ str.valid_encoding?.should == false
+ str.reverse!
+
+ str.should == "體黑正\xDE\xDF軟微"
+ end
+end
diff --git a/spec/ruby/core/string/rindex_spec.rb b/spec/ruby/core/string/rindex_spec.rb
new file mode 100644
index 0000000000..acecec224f
--- /dev/null
+++ b/spec/ruby/core/string/rindex_spec.rb
@@ -0,0 +1,384 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#rindex with object" do
+ it "raises a TypeError if obj isn't a String or Regexp" do
+ not_supported_on :opal do
+ -> { "hello".rindex(:sym) }.should.raise(TypeError)
+ end
+ -> { "hello".rindex(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if obj is an Integer" do
+ -> { "hello".rindex(42) }.should.raise(TypeError)
+ end
+
+ it "doesn't try to convert obj to an integer via to_int" do
+ obj = mock('x')
+ obj.should_not_receive(:to_int)
+ -> { "hello".rindex(obj) }.should.raise(TypeError)
+ end
+
+ it "tries to convert obj to a string via to_str" do
+ obj = mock('lo')
+ def obj.to_str() "lo" end
+ "hello".rindex(obj).should == "hello".rindex("lo")
+
+ obj = mock('o')
+ def obj.respond_to?(arg, *) true end
+ def obj.method_missing(*args) "o" end
+ "hello".rindex(obj).should == "hello".rindex("o")
+ end
+end
+
+describe "String#rindex with String" do
+ it "behaves the same as String#rindex(char) for one-character strings" do
+ "blablabla hello cruel world...!".split("").uniq.each do |str|
+ chr = str[0]
+ str.rindex(str).should == str.rindex(chr)
+
+ 0.upto(str.size + 1) do |start|
+ str.rindex(str, start).should == str.rindex(chr, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.rindex(str, start).should == str.rindex(chr, start)
+ end
+ end
+ end
+
+ it "behaves the same as String#rindex(?char) for one-character strings" do
+ "blablabla hello cruel world...!".split("").uniq.each do |str|
+ chr = str[0] =~ / / ? str[0] : eval("?#{str[0]}")
+ str.rindex(str).should == str.rindex(chr)
+
+ 0.upto(str.size + 1) do |start|
+ str.rindex(str, start).should == str.rindex(chr, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.rindex(str, start).should == str.rindex(chr, start)
+ end
+ end
+ end
+
+ it "returns the index of the last occurrence of the given substring" do
+ "blablabla".rindex("").should == 9
+ "blablabla".rindex("a").should == 8
+ "blablabla".rindex("la").should == 7
+ "blablabla".rindex("bla").should == 6
+ "blablabla".rindex("abla").should == 5
+ "blablabla".rindex("labla").should == 4
+ "blablabla".rindex("blabla").should == 3
+ "blablabla".rindex("ablabla").should == 2
+ "blablabla".rindex("lablabla").should == 1
+ "blablabla".rindex("blablabla").should == 0
+
+ "blablabla".rindex("l").should == 7
+ "blablabla".rindex("bl").should == 6
+ "blablabla".rindex("abl").should == 5
+ "blablabla".rindex("labl").should == 4
+ "blablabla".rindex("blabl").should == 3
+ "blablabla".rindex("ablabl").should == 2
+ "blablabla".rindex("lablabl").should == 1
+ "blablabla".rindex("blablabl").should == 0
+
+ "blablabla".rindex("b").should == 6
+ "blablabla".rindex("ab").should == 5
+ "blablabla".rindex("lab").should == 4
+ "blablabla".rindex("blab").should == 3
+ "blablabla".rindex("ablab").should == 2
+ "blablabla".rindex("lablab").should == 1
+ "blablabla".rindex("blablab").should == 0
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello.'.rindex('ll')
+ $~.should == nil
+ end
+
+ it "ignores string subclasses" do
+ "blablabla".rindex(StringSpecs::MyString.new("bla")).should == 6
+ StringSpecs::MyString.new("blablabla").rindex("bla").should == 6
+ StringSpecs::MyString.new("blablabla").rindex(StringSpecs::MyString.new("bla")).should == 6
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".rindex("bl", 0).should == 0
+ "blablabla".rindex("bl", 1).should == 0
+ "blablabla".rindex("bl", 2).should == 0
+ "blablabla".rindex("bl", 3).should == 3
+
+ "blablabla".rindex("bla", 0).should == 0
+ "blablabla".rindex("bla", 1).should == 0
+ "blablabla".rindex("bla", 2).should == 0
+ "blablabla".rindex("bla", 3).should == 3
+
+ "blablabla".rindex("blab", 0).should == 0
+ "blablabla".rindex("blab", 1).should == 0
+ "blablabla".rindex("blab", 2).should == 0
+ "blablabla".rindex("blab", 3).should == 3
+ "blablabla".rindex("blab", 6).should == 3
+ "blablablax".rindex("blab", 6).should == 3
+
+ "blablabla".rindex("la", 1).should == 1
+ "blablabla".rindex("la", 2).should == 1
+ "blablabla".rindex("la", 3).should == 1
+ "blablabla".rindex("la", 4).should == 4
+
+ "blablabla".rindex("lab", 1).should == 1
+ "blablabla".rindex("lab", 2).should == 1
+ "blablabla".rindex("lab", 3).should == 1
+ "blablabla".rindex("lab", 4).should == 4
+
+ "blablabla".rindex("ab", 2).should == 2
+ "blablabla".rindex("ab", 3).should == 2
+ "blablabla".rindex("ab", 4).should == 2
+ "blablabla".rindex("ab", 5).should == 5
+
+ "blablabla".rindex("", 0).should == 0
+ "blablabla".rindex("", 1).should == 1
+ "blablabla".rindex("", 2).should == 2
+ "blablabla".rindex("", 7).should == 7
+ "blablabla".rindex("", 8).should == 8
+ "blablabla".rindex("", 9).should == 9
+ "blablabla".rindex("", 10).should == 9
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.rindex(needle, offset).should ==
+ str.rindex(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".rindex("B").should == nil
+ "blablabla".rindex("z").should == nil
+ "blablabla".rindex("BLA").should == nil
+ "blablabla".rindex("blablablabla").should == nil
+
+ "hello".rindex("lo", 0).should == nil
+ "hello".rindex("lo", 1).should == nil
+ "hello".rindex("lo", 2).should == nil
+
+ "hello".rindex("llo", 0).should == nil
+ "hello".rindex("llo", 1).should == nil
+
+ "hello".rindex("el", 0).should == nil
+ "hello".rindex("ello", 0).should == nil
+
+ "hello".rindex("", -6).should == nil
+ "hello".rindex("", -7).should == nil
+
+ "hello".rindex("h", -6).should == nil
+ end
+
+ it "tries to convert start_offset to an integer via to_int" do
+ obj = mock('5')
+ def obj.to_int() 5 end
+ "str".rindex("st", obj).should == 0
+
+ obj = mock('5')
+ def obj.respond_to?(arg, *) true end
+ def obj.method_missing(*args) 5 end
+ "str".rindex("st", obj).should == 0
+ end
+
+ it "raises a TypeError when given offset is nil" do
+ -> { "str".rindex("st", nil) }.should.raise(TypeError)
+ end
+
+ it "handles a substring in a superset encoding" do
+ 'abc'.dup.force_encoding(Encoding::US_ASCII).rindex('é').should == nil
+ end
+
+ it "handles a substring in a subset encoding" do
+ 'été'.rindex('t'.dup.force_encoding(Encoding::US_ASCII)).should == 1
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ str = 'abc'.dup.force_encoding("ISO-2022-JP")
+ pattern = 'b'.dup.force_encoding("EUC-JP")
+
+ -> { str.rindex(pattern) }.should.raise(Encoding::CompatibilityError, "incompatible character encodings: ISO-2022-JP and EUC-JP")
+ end
+end
+
+describe "String#rindex with Regexp" do
+ it "behaves the same as String#rindex(string) for escaped string regexps" do
+ ["blablabla", "hello cruel world...!"].each do |str|
+ ["", "b", "bla", "lab", "o c", "d."].each do |needle|
+ regexp = Regexp.new(Regexp.escape(needle))
+ str.rindex(regexp).should == str.rindex(needle)
+
+ 0.upto(str.size + 1) do |start|
+ str.rindex(regexp, start).should == str.rindex(needle, start)
+ end
+
+ (-str.size - 1).upto(-1) do |start|
+ str.rindex(regexp, start).should == str.rindex(needle, start)
+ end
+ end
+ end
+ end
+
+ it "returns the index of the first match from the end of string of regexp" do
+ "blablabla".rindex(/bla/).should == 6
+ "blablabla".rindex(/BLA/i).should == 6
+
+ "blablabla".rindex(/.{0}/).should == 9
+ "blablabla".rindex(/.{1}/).should == 8
+ "blablabla".rindex(/.{2}/).should == 7
+ "blablabla".rindex(/.{6}/).should == 3
+ "blablabla".rindex(/.{9}/).should == 0
+
+ "blablabla".rindex(/.*/).should == 9
+ "blablabla".rindex(/.+/).should == 8
+
+ "blablabla".rindex(/bla|a/).should == 8
+
+ not_supported_on :opal do
+ "blablabla".rindex(/\A/).should == 0
+ "blablabla".rindex(/\Z/).should == 9
+ "blablabla".rindex(/\z/).should == 9
+ "blablabla\n".rindex(/\Z/).should == 10
+ "blablabla\n".rindex(/\z/).should == 10
+ end
+
+ "blablabla".rindex(/^/).should == 0
+ not_supported_on :opal do
+ "\nblablabla".rindex(/^/).should == 1
+ "b\nlablabla".rindex(/^/).should == 2
+ end
+ "blablabla".rindex(/$/).should == 9
+
+ "blablabla".rindex(/.l./).should == 6
+ end
+
+ it "sets $~ to MatchData of match and nil when there's none" do
+ 'hello.'.rindex(/.(.)/)
+ $~[0].should == 'o.'
+
+ 'hello.'.rindex(/not/)
+ $~.should == nil
+ end
+
+ it "starts the search at the given offset" do
+ "blablabla".rindex(/.{0}/, 5).should == 5
+ "blablabla".rindex(/.{1}/, 5).should == 5
+ "blablabla".rindex(/.{2}/, 5).should == 5
+ "blablabla".rindex(/.{3}/, 5).should == 5
+ "blablabla".rindex(/.{4}/, 5).should == 5
+
+ "blablabla".rindex(/.{0}/, 3).should == 3
+ "blablabla".rindex(/.{1}/, 3).should == 3
+ "blablabla".rindex(/.{2}/, 3).should == 3
+ "blablabla".rindex(/.{5}/, 3).should == 3
+ "blablabla".rindex(/.{6}/, 3).should == 3
+
+ "blablabla".rindex(/.l./, 0).should == 0
+ "blablabla".rindex(/.l./, 1).should == 0
+ "blablabla".rindex(/.l./, 2).should == 0
+ "blablabla".rindex(/.l./, 3).should == 3
+
+ "blablablax".rindex(/.x/, 10).should == 8
+ "blablablax".rindex(/.x/, 9).should == 8
+ "blablablax".rindex(/.x/, 8).should == 8
+
+ "blablablax".rindex(/..x/, 10).should == 7
+ "blablablax".rindex(/..x/, 9).should == 7
+ "blablablax".rindex(/..x/, 8).should == 7
+ "blablablax".rindex(/..x/, 7).should == 7
+
+ not_supported_on :opal do
+ "blablabla\n".rindex(/\Z/, 9).should == 9
+ end
+ end
+
+ it "starts the search at offset + self.length if offset is negative" do
+ str = "blablabla"
+
+ ["bl", "bla", "blab", "la", "lab", "ab", ""].each do |needle|
+ (-str.length .. -1).each do |offset|
+ str.rindex(needle, offset).should ==
+ str.rindex(needle, offset + str.length)
+ end
+ end
+ end
+
+ it "returns nil if the substring isn't found" do
+ "blablabla".rindex(/BLA/).should == nil
+ "blablabla".rindex(/.{10}/).should == nil
+ "blablablax".rindex(/.x/, 7).should == nil
+ "blablablax".rindex(/..x/, 6).should == nil
+
+ not_supported_on :opal do
+ "blablabla".rindex(/\Z/, 5).should == nil
+ "blablabla".rindex(/\z/, 5).should == nil
+ "blablabla\n".rindex(/\z/, 9).should == nil
+ end
+ end
+
+ not_supported_on :opal do
+ it "supports \\G which matches at the given start offset" do
+ "helloYOU.".rindex(/YOU\G/, 8).should == 5
+ "helloYOU.".rindex(/YOU\G/).should == nil
+
+ idx = "helloYOUall!".index("YOU")
+ re = /YOU.+\G.+/
+ # The # marks where \G will match.
+ [
+ ["helloYOU#all.", nil],
+ ["helloYOUa#ll.", idx],
+ ["helloYOUal#l.", idx],
+ ["helloYOUall#.", idx],
+ ["helloYOUall.#", nil]
+ ].each do |i|
+ start = i[0].index("#")
+ str = i[0].delete("#")
+
+ str.rindex(re, start).should == i[1]
+ end
+ end
+ end
+
+ it "tries to convert start_offset to an integer via to_int" do
+ obj = mock('5')
+ def obj.to_int() 5 end
+ "str".rindex(/../, obj).should == 1
+
+ obj = mock('5')
+ def obj.respond_to?(arg, *) true end
+ def obj.method_missing(*args); 5; end
+ "str".rindex(/../, obj).should == 1
+ end
+
+ it "raises a TypeError when given offset is nil" do
+ -> { "str".rindex(/../, nil) }.should.raise(TypeError)
+ end
+
+ it "returns the reverse character index of a multibyte character" do
+ "ã‚りãŒã‚ŠãŒã¨ã†".rindex("ãŒ").should == 4
+ "ã‚りãŒã‚ŠãŒã¨ã†".rindex(/ãŒ/).should == 4
+ end
+
+ it "returns the character index before the finish" do
+ "ã‚りãŒã‚ŠãŒã¨ã†".rindex("ãŒ", 3).should == 2
+ "ã‚りãŒã‚ŠãŒã¨ã†".rindex(/ãŒ/, 3).should == 2
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ re = Regexp.new "れ".encode(Encoding::EUC_JP)
+ -> do
+ "ã‚れ".rindex re
+ end.should.raise(Encoding::CompatibilityError, "incompatible encoding regexp match (EUC-JP regexp with UTF-8 string)")
+ end
+end
diff --git a/spec/ruby/core/string/rjust_spec.rb b/spec/ruby/core/string/rjust_spec.rb
new file mode 100644
index 0000000000..9f9c369745
--- /dev/null
+++ b/spec/ruby/core/string/rjust_spec.rb
@@ -0,0 +1,100 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#rjust with length, padding" do
+ it "returns a new string of specified length with self right justified and padded with padstr" do
+ "hello".rjust(20, '1234').should == "123412341234123hello"
+
+ "".rjust(1, "abcd").should == "a"
+ "".rjust(2, "abcd").should == "ab"
+ "".rjust(3, "abcd").should == "abc"
+ "".rjust(4, "abcd").should == "abcd"
+ "".rjust(6, "abcd").should == "abcdab"
+
+ "OK".rjust(3, "abcd").should == "aOK"
+ "OK".rjust(4, "abcd").should == "abOK"
+ "OK".rjust(6, "abcd").should == "abcdOK"
+ "OK".rjust(8, "abcd").should == "abcdabOK"
+ end
+
+ it "pads with whitespace if no padstr is given" do
+ "hello".rjust(20).should == " hello"
+ end
+
+ it "returns self if it's longer than or as long as the specified length" do
+ "".rjust(0).should == ""
+ "".rjust(-1).should == ""
+ "hello".rjust(4).should == "hello"
+ "hello".rjust(-1).should == "hello"
+ "this".rjust(3).should == "this"
+ "radiology".rjust(8, '-').should == "radiology"
+ end
+
+ it "tries to convert length to an integer using to_int" do
+ "^".rjust(3.8, "^_").should == "^_^"
+
+ obj = mock('3')
+ obj.should_receive(:to_int).and_return(3)
+
+ "o".rjust(obj, "o_").should == "o_o"
+ end
+
+ it "raises a TypeError when length can't be converted to an integer" do
+ -> { "hello".rjust("x") }.should.raise(TypeError)
+ -> { "hello".rjust("x", "y") }.should.raise(TypeError)
+ -> { "hello".rjust([]) }.should.raise(TypeError)
+ -> { "hello".rjust(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "tries to convert padstr to a string using to_str" do
+ padstr = mock('123')
+ padstr.should_receive(:to_str).and_return("123")
+
+ "hello".rjust(10, padstr).should == "12312hello"
+ end
+
+ it "raises a TypeError when padstr can't be converted" do
+ -> { "hello".rjust(20, []) }.should.raise(TypeError)
+ -> { "hello".rjust(20, Object.new)}.should.raise(TypeError)
+ -> { "hello".rjust(20, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError when padstr is empty" do
+ -> { "hello".rjust(10, '') }.should.raise(ArgumentError)
+ end
+
+ it "returns String instances when called on subclasses" do
+ StringSpecs::MyString.new("").rjust(10).should.instance_of?(String)
+ StringSpecs::MyString.new("foo").rjust(10).should.instance_of?(String)
+ StringSpecs::MyString.new("foo").rjust(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+
+ "".rjust(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+ "foo".rjust(10, StringSpecs::MyString.new("x")).should.instance_of?(String)
+ end
+
+ describe "with width" do
+ it "returns a String in the same encoding as the original" do
+ str = "abc".dup.force_encoding Encoding::IBM437
+ result = str.rjust 5
+ result.should == " abc"
+ result.encoding.should.equal?(Encoding::IBM437)
+ end
+ end
+
+ describe "with width, pattern" do
+ it "returns a String in the compatible encoding" do
+ str = "abc".dup.force_encoding Encoding::IBM437
+ result = str.rjust 5, "ã‚"
+ result.should == "ã‚ã‚abc"
+ result.encoding.should.equal?(Encoding::UTF_8)
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ pat = "ã‚¢".encode Encoding::EUC_JP
+ -> do
+ "ã‚れ".rjust 5, pat
+ end.should.raise(Encoding::CompatibilityError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/rpartition_spec.rb b/spec/ruby/core/string/rpartition_spec.rb
new file mode 100644
index 0000000000..a7dd7430b7
--- /dev/null
+++ b/spec/ruby/core/string/rpartition_spec.rb
@@ -0,0 +1,71 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/partition'
+
+describe "String#rpartition with String" do
+ it_behaves_like :string_partition, :rpartition
+
+ it "returns an array of substrings based on splitting on the given string" do
+ "hello world".rpartition("o").should == ["hello w", "o", "rld"]
+ end
+
+ it "always returns 3 elements" do
+ "hello".rpartition("x").should == ["", "", "hello"]
+ "hello".rpartition("hello").should == ["", "hello", ""]
+ end
+
+ it "returns original string if regexp doesn't match" do
+ "hello".rpartition("/x/").should == ["", "", "hello"]
+ end
+
+ it "returns new object if doesn't match" do
+ str = "hello"
+ str.rpartition("/no_match/").last.should_not.equal?(str)
+ end
+
+ it "handles multibyte string correctly" do
+ "ユーザ@ドメイン".rpartition(/@/).should == ["ユーザ", "@", "ドメイン"]
+ end
+
+ it "accepts regexp" do
+ "hello!".rpartition(/l./).should == ["hel", "lo", "!"]
+ end
+
+ it "affects $~" do
+ matched_string = "hello!".rpartition(/l./)[1]
+ matched_string.should == $~[0]
+ end
+
+ it "converts its argument using :to_str" do
+ find = mock('l')
+ find.should_receive(:to_str).and_return("l")
+ "hello".rpartition(find).should == ["hel","l","o"]
+ end
+
+ it "raises an error if not convertible to string" do
+ ->{ "hello".rpartition(5) }.should.raise(TypeError)
+ ->{ "hello".rpartition(nil) }.should.raise(TypeError)
+ end
+
+ it "handles a pattern in a superset encoding" do
+ string = "hello".dup.force_encoding(Encoding::US_ASCII)
+
+ result = string.rpartition("é")
+
+ result.should == ["", "", "hello"]
+ result[0].encoding.should == Encoding::US_ASCII
+ result[1].encoding.should == Encoding::US_ASCII
+ result[2].encoding.should == Encoding::US_ASCII
+ end
+
+ it "handles a pattern in a subset encoding" do
+ pattern = "o".dup.force_encoding(Encoding::US_ASCII)
+
+ result = "héllo world".rpartition(pattern)
+
+ result.should == ["héllo w", "o", "rld"]
+ result[0].encoding.should == Encoding::UTF_8
+ result[1].encoding.should == Encoding::US_ASCII
+ result[2].encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/string/rstrip_spec.rb b/spec/ruby/core/string/rstrip_spec.rb
new file mode 100644
index 0000000000..1638ea375d
--- /dev/null
+++ b/spec/ruby/core/string/rstrip_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/strip'
+
+describe "String#rstrip" do
+ it_behaves_like :string_strip, :rstrip
+
+ it "returns a copy of self with trailing whitespace removed" do
+ " hello ".rstrip.should == " hello"
+ " hello world ".rstrip.should == " hello world"
+ " hello world \n\r\t\n\v\r".rstrip.should == " hello world"
+ "hello".rstrip.should == "hello"
+ "hello\x00".rstrip.should == "hello"
+ "ã“ã«ã¡ã‚ ".rstrip.should == "ã“ã«ã¡ã‚"
+ end
+
+ it "works with lazy substrings" do
+ " hello "[1...-1].rstrip.should == " hello"
+ " hello world "[1...-1].rstrip.should == " hello world"
+ " hello world \n\r\t\n\v\r"[1...-1].rstrip.should == " hello world"
+ " ã“ã«ã¡ã‚ "[1...-1].rstrip.should == "ã“ã«ã¡ã‚"
+ end
+
+ it "returns a copy of self with all trailing whitespace and NULL bytes removed" do
+ "\x00 \x00hello\x00 \x00".rstrip.should == "\x00 \x00hello"
+ end
+end
+
+describe "String#rstrip!" do
+ it "modifies self in place and returns self" do
+ a = " hello "
+ a.rstrip!.should.equal?(a)
+ a.should == " hello"
+ end
+
+ it "modifies self removing trailing NULL bytes and whitespace" do
+ a = "\x00 \x00hello\x00 \x00"
+ a.rstrip!
+ a.should == "\x00 \x00hello"
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.rstrip!.should == nil
+ a.should == "hello"
+ end
+
+ it "makes a string empty if it is only whitespace" do
+ "".rstrip!.should == nil
+ " ".rstrip.should == ""
+ " ".rstrip.should == ""
+ end
+
+ it "removes trailing NULL bytes and whitespace" do
+ a = "\000 goodbye \000"
+ a.rstrip!
+ a.should == "\000 goodbye"
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { " hello ".freeze.rstrip! }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> { "hello".freeze.rstrip! }.should.raise(FrozenError)
+ -> { "".freeze.rstrip! }.should.raise(FrozenError)
+ end
+
+ it "raises an Encoding::CompatibilityError if the last non-space codepoint is invalid" do
+ s = "abc\xDF".force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.rstrip! }.should.raise(Encoding::CompatibilityError)
+
+ s = "abc\xDF ".force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.rstrip! }.should.raise(Encoding::CompatibilityError)
+ end
+end
diff --git a/spec/ruby/core/string/scan_spec.rb b/spec/ruby/core/string/scan_spec.rb
new file mode 100644
index 0000000000..47fa7214c2
--- /dev/null
+++ b/spec/ruby/core/string/scan_spec.rb
@@ -0,0 +1,173 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#scan" do
+ it "returns an array containing all matches" do
+ "cruel world".scan(/\w+/).should == ["cruel", "world"]
+ "cruel world".scan(/.../).should == ["cru", "el ", "wor"]
+
+ # Edge case
+ "hello".scan(//).should == ["", "", "", "", "", ""]
+ "".scan(//).should == [""]
+ end
+
+ it "respects unicode when the pattern collapses to nothing" do
+ str = "ã“ã«ã¡ã‚"
+ reg = %r!!
+ str.scan(reg).should == ["", "", "", "", ""]
+ end
+
+ it "stores groups as arrays in the returned arrays" do
+ "hello".scan(/()/).should == [[""]] * 6
+ "hello".scan(/()()/).should == [["", ""]] * 6
+ "cruel world".scan(/(...)/).should == [["cru"], ["el "], ["wor"]]
+ "cruel world".scan(/(..)(..)/).should == [["cr", "ue"], ["l ", "wo"]]
+ end
+
+ it "scans for occurrences of the string if pattern is a string" do
+ "one two one two".scan('one').should == ["one", "one"]
+ "hello.".scan('.').should == ['.']
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none" do
+ 'hello.'.scan(/.(.)/)
+ $~[0].should == 'o.'
+
+ 'hello.'.scan(/not/)
+ $~.should == nil
+
+ 'hello.'.scan('l')
+ $~.begin(0).should == 3
+ $~[0].should == 'l'
+
+ 'hello.'.scan('not')
+ $~.should == nil
+ end
+
+ it "supports \\G which matches the end of the previous match / string start for first match" do
+ "one two one two".scan(/\G\w+/).should == ["one"]
+ "one two one two".scan(/\G\w+\s*/).should == ["one ", "two ", "one ", "two"]
+ "one two one two".scan(/\G\s*\w+/).should == ["one", " two", " one", " two"]
+ end
+
+ it "tries to convert pattern to a string via to_str" do
+ obj = mock('o')
+ obj.should_receive(:to_str).and_return("o")
+ "o_o".scan(obj).should == ["o", "o"]
+ end
+
+ it "raises a TypeError if pattern isn't a Regexp and can't be converted to a String" do
+ -> { "cruel world".scan(5) }.should.raise(TypeError)
+ not_supported_on :opal do
+ -> { "cruel world".scan(:test) }.should.raise(TypeError)
+ end
+ -> { "cruel world".scan(mock('x')) }.should.raise(TypeError)
+ end
+
+ # jruby/jruby#5513
+ it "does not raise any errors when passed a multi-byte string" do
+ "ã‚ã‚ã‚aaaã‚ã‚ã‚".scan("ã‚ã‚ã‚").should == ["ã‚ã‚ã‚", "ã‚ã‚ã‚"]
+ end
+
+ it "returns Strings in the same encoding as self" do
+ "cruel world".encode("US-ASCII").scan(/\w+/).each do |s|
+ s.encoding.should == Encoding::US_ASCII
+ end
+ end
+end
+
+describe "String#scan with pattern and block" do
+ it "returns self" do
+ s = "foo"
+ s.scan(/./) {}.should.equal?(s)
+ s.scan(/roar/) {}.should.equal?(s)
+ end
+
+ it "passes each match to the block as one argument: an array" do
+ a = []
+ "cruel world".scan(/\w+/) { |*w| a << w }
+ a.should == [["cruel"], ["world"]]
+ end
+
+ it "passes groups to the block as one argument: an array" do
+ a = []
+ "cruel world".scan(/(..)(..)/) { |w| a << w }
+ a.should == [["cr", "ue"], ["l ", "wo"]]
+ end
+
+ it "sets $~ for access from the block" do
+ str = "hello"
+
+ matches = []
+ offsets = []
+
+ str.scan(/([aeiou])/) do
+ md = $~
+ md.string.should == str
+ matches << md.to_a
+ offsets << md.offset(0)
+ str
+ end
+
+ matches.should == [["e", "e"], ["o", "o"]]
+ offsets.should == [[1, 2], [4, 5]]
+
+ matches = []
+ offsets = []
+
+ str.scan("l") do
+ md = $~
+ md.string.should == str
+ matches << md.to_a
+ offsets << md.offset(0)
+ str
+ end
+
+ matches.should == [["l"], ["l"]]
+ offsets.should == [[2, 3], [3, 4]]
+ end
+
+ it "restores $~ after leaving the block" do
+ [/./, "l"].each do |pattern|
+ old_md = nil
+ "hello".scan(pattern) do
+ old_md = $~
+ "ok".match(/./)
+ "x"
+ end
+
+ $~[0].should == old_md[0]
+ $~.string.should == "hello"
+ end
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none for access from outside" do
+ 'hello.'.scan('l') { 'x' }
+ $~.begin(0).should == 3
+ $~[0].should == 'l'
+
+ 'hello.'.scan('not') { 'x' }
+ $~.should == nil
+
+ 'hello.'.scan(/.(.)/) { 'x' }
+ $~[0].should == 'o.'
+
+ 'hello.'.scan(/not/) { 'x' }
+ $~.should == nil
+ end
+
+ it "passes block arguments as individual arguments when blocks are provided" do
+ "a b c\na b c\na b c".scan(/(\w*) (\w*) (\w*)/) do |first,second,third|
+ first.should == 'a';
+ second.should == 'b';
+ third.should == 'c';
+ end
+ end
+
+ it "yields String instances for subclasses" do
+ a = []
+ StringSpecs::MyString.new("abc").scan(/./) { |s| a << s.class }
+ a.should == [String, String, String]
+ end
+end
diff --git a/spec/ruby/core/string/scrub_spec.rb b/spec/ruby/core/string/scrub_spec.rb
new file mode 100644
index 0000000000..9dc55dbef7
--- /dev/null
+++ b/spec/ruby/core/string/scrub_spec.rb
@@ -0,0 +1,164 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#scrub with a default replacement" do
+ it "returns self for valid strings" do
+ input = "foo"
+
+ input.scrub.should == input
+ end
+
+ it "replaces invalid byte sequences" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ "abc\u3042#{x81}".scrub.should == "abc\u3042\uFFFD"
+ end
+
+ it "replaces invalid byte sequences in lazy substrings" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ "abc\u3042#{x81}def"[1...-1].scrub.should == "bc\u3042\uFFFDde"
+ end
+
+ it "returns a copy of self when the input encoding is BINARY" do
+ input = "foo".encode('BINARY')
+
+ input.scrub.should == "foo"
+ end
+
+ it "replaces invalid byte sequences when using ASCII as the input encoding" do
+ xE3x80 = [0xE3, 0x80].pack('CC').force_encoding 'utf-8'
+ input = "abc\u3042#{xE3x80}".force_encoding('ASCII')
+ input.scrub.should == "abc?????"
+ end
+
+ it "returns a String in the same encoding as self" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ "abc\u3042#{x81}".scrub.encoding.should == Encoding::UTF_8
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("foo").scrub.should.instance_of?(String)
+ input = [0x81].pack('C').force_encoding('utf-8')
+ StringSpecs::MyString.new(input).scrub.should.instance_of?(String)
+ end
+end
+
+describe "String#scrub with a custom replacement" do
+ it "returns self for valid strings" do
+ input = "foo"
+
+ input.scrub("*").should == input
+ end
+
+ it "replaces invalid byte sequences" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ "abc\u3042#{x81}".scrub("*").should == "abc\u3042*"
+ end
+
+ it "replaces invalid byte sequences in frozen strings" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ (-"abc\u3042#{x81}").scrub("*").should == "abc\u3042*"
+
+ leading_surrogate = [0x00, 0xD8]
+ utf16_str = ("abc".encode('UTF-16LE').bytes + leading_surrogate).pack('c*').force_encoding('UTF-16LE')
+ (-(utf16_str)).scrub("*".encode('UTF-16LE')).should == "abc*".encode('UTF-16LE')
+ end
+
+ it "replaces an incomplete character at the end with a single replacement" do
+ xE3x80 = [0xE3, 0x80].pack('CC').force_encoding 'utf-8'
+ xE3x80.scrub("*").should == "*"
+ end
+
+ it "raises ArgumentError for replacements with an invalid encoding" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ xE4 = [0xE4].pack('C').force_encoding('utf-8')
+ block = -> { "foo#{x81}".scrub(xE4) }
+
+ block.should.raise(ArgumentError)
+ end
+
+ it "returns a String in the same encoding as self" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ "abc\u3042#{x81}".scrub("*").encoding.should == Encoding::UTF_8
+ end
+
+ it "raises TypeError when a non String replacement is given" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ block = -> { "foo#{x81}".scrub(1) }
+
+ block.should.raise(TypeError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("foo").scrub("*").should.instance_of?(String)
+ input = [0x81].pack('C').force_encoding('utf-8')
+ StringSpecs::MyString.new(input).scrub("*").should.instance_of?(String)
+ end
+end
+
+describe "String#scrub with a block" do
+ it "returns self for valid strings" do
+ input = "foo"
+
+ input.scrub { |b| "*" }.should == input
+ end
+
+ it "replaces invalid byte sequences" do
+ xE3x80 = [0xE3, 0x80].pack('CC').force_encoding 'utf-8'
+ replaced = "abc\u3042#{xE3x80}".scrub { |b| "<#{b.unpack("H*")[0]}>" }
+
+ replaced.should == "abc\u3042<e380>"
+ end
+
+ it "replaces invalid byte sequences using a custom encoding" do
+ x80x80 = [0x80, 0x80].pack('CC').force_encoding 'utf-8'
+ replaced = x80x80.scrub do |bad|
+ bad.encode(Encoding::UTF_8, Encoding::Windows_1252)
+ end
+
+ replaced.should == "€€"
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("foo").scrub { |b| "*" }.should.instance_of?(String)
+ input = [0x81].pack('C').force_encoding('utf-8')
+ StringSpecs::MyString.new(input).scrub { |b| "<#{b.unpack("H*")[0]}>" }.should.instance_of?(String)
+ end
+end
+
+describe "String#scrub!" do
+ it "modifies self for valid strings" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ input = "a#{x81}"
+ input.scrub!
+ input.should == "a\uFFFD"
+ end
+
+ it "accepts blocks" do
+ x81 = [0x81].pack('C').force_encoding('utf-8')
+ input = "a#{x81}"
+ input.scrub! { |b| "<?>" }
+ input.should == "a<?>"
+ end
+
+ it "maintains the state of frozen strings that are already valid" do
+ input = "a"
+ input.freeze
+ input.scrub!
+ input.frozen?.should == true
+ end
+
+ it "preserves the instance variables of already valid strings" do
+ input = "a"
+ input.instance_variable_set(:@a, 'b')
+ input.scrub!
+ input.instance_variable_get(:@a).should == 'b'
+ end
+
+ it "accepts a frozen string as a replacement" do
+ input = "a\xE2"
+ input.scrub!('.'.freeze)
+ input.should == 'a.'
+ end
+end
diff --git a/spec/ruby/core/string/setbyte_spec.rb b/spec/ruby/core/string/setbyte_spec.rb
new file mode 100644
index 0000000000..d9fcc279c0
--- /dev/null
+++ b/spec/ruby/core/string/setbyte_spec.rb
@@ -0,0 +1,112 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#setbyte" do
+ it "returns an Integer" do
+ "a".setbyte(0,1).should.is_a?(Integer)
+ end
+
+ it "modifies the receiver" do
+ str = "glark"
+ old_id = str.object_id
+ str.setbyte(0,88)
+ str.object_id.should == old_id
+ end
+
+ it "changes the byte at the given index to the new byte" do
+ str = "a"
+ str.setbyte(0,98)
+ str.should == 'b'
+
+ # copy-on-write case
+ str1, str2 = "fooXbar".split("X")
+ str2.setbyte(0, 50)
+ str2.should == "2ar"
+ str1.should == "foo"
+ end
+
+ it "allows changing bytes in multi-byte characters" do
+ str = "\u{915}"
+ str.setbyte(1,254)
+ str.getbyte(1).should == 254
+ end
+
+ it "can invalidate a String's encoding" do
+ str = "glark"
+ str.valid_encoding?.should == true
+ str.setbyte(2,253)
+ str.valid_encoding?.should == false
+
+ str = "ABC"
+ str.setbyte(0, 0x20) # ' '
+ str.should.valid_encoding?
+ str.setbyte(0, 0xE3)
+ str.should_not.valid_encoding?
+ end
+
+ it "regards a negative index as counting from the end of the String" do
+ str = "hedgehog"
+ str.setbyte(-3, 108)
+ str.should == "hedgelog"
+
+ # copy-on-write case
+ str1, str2 = "fooXbar".split("X")
+ str2.setbyte(-1, 50)
+ str2.should == "ba2"
+ str1.should == "foo"
+ end
+
+ it "raises an IndexError if the index is greater than the String bytesize" do
+ -> { "?".setbyte(1, 97) }.should.raise(IndexError)
+ end
+
+ it "raises an IndexError if the negative index is greater magnitude than the String bytesize" do
+ -> { "???".setbyte(-5, 97) }.should.raise(IndexError)
+ end
+
+ it "sets a byte at an index greater than String size" do
+ chr = "\u{998}"
+ chr.bytesize.should == 3
+ chr.setbyte(2, 150)
+ chr.should == "\xe0\xa6\x96"
+ end
+
+ it "does not modify the original string when using String.new" do
+ str1 = "hedgehog"
+ str2 = String.new(str1)
+ str2.setbyte(0, 108)
+ str2.should == "ledgehog"
+ str2.should_not == "hedgehog"
+ str1.should == "hedgehog"
+ str1.should_not == "ledgehog"
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ str = "cold".freeze
+ str.frozen?.should == true
+ -> { str.setbyte(3,96) }.should.raise(FrozenError)
+ end
+
+ it "raises a TypeError unless the second argument is an Integer" do
+ -> { "a".setbyte(0,'a') }.should.raise(TypeError)
+ end
+
+ it "calls #to_int to convert the index" do
+ index = mock("setbyte index")
+ index.should_receive(:to_int).and_return(1)
+
+ str = "hat"
+ str.setbyte(index, "i".ord)
+ str.should == "hit"
+ end
+
+ it "calls to_int to convert the value" do
+ value = mock("setbyte value")
+ value.should_receive(:to_int).and_return("i".ord)
+
+ str = "hat"
+ str.setbyte(1, value)
+ str.should == "hit"
+ end
+end
diff --git a/spec/ruby/core/string/shared/byte_index_common.rb b/spec/ruby/core/string/shared/byte_index_common.rb
new file mode 100644
index 0000000000..bae6cff49f
--- /dev/null
+++ b/spec/ruby/core/string/shared/byte_index_common.rb
@@ -0,0 +1,63 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../../spec_helper'
+
+describe :byte_index_common, shared: true do
+ describe "raises on type errors" do
+ it "raises a TypeError if passed nil" do
+ -> { "abc".send(@method, nil) }.should.raise(TypeError, "no implicit conversion of nil into String")
+ end
+
+ it "raises a TypeError if passed a boolean" do
+ -> { "abc".send(@method, true) }.should.raise(TypeError, "no implicit conversion of true into String")
+ end
+
+ it "raises a TypeError if passed a Symbol" do
+ not_supported_on :opal do
+ -> { "abc".send(@method, :a) }.should.raise(TypeError, "no implicit conversion of Symbol into String")
+ end
+ end
+
+ it "raises a TypeError if passed a Symbol" do
+ obj = mock('x')
+ obj.should_not_receive(:to_int)
+ -> { "hello".send(@method, obj) }.should.raise(TypeError, "no implicit conversion of MockObject into String")
+ end
+
+ it "raises a TypeError if passed an Integer" do
+ -> { "abc".send(@method, 97) }.should.raise(TypeError, "no implicit conversion of Integer into String")
+ end
+ end
+
+ describe "with multibyte codepoints" do
+ it "raises an IndexError when byte offset lands in the middle of a multibyte character" do
+ -> { "ã‚".send(@method, "", 1) }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ -> { "ã‚".send(@method, "", 2) }.should.raise(IndexError, "offset 2 does not land on character boundary")
+ -> { "ã‚".send(@method, "", -1) }.should.raise(IndexError, "offset 2 does not land on character boundary")
+ -> { "ã‚".send(@method, "", -2) }.should.raise(IndexError, "offset 1 does not land on character boundary")
+ end
+
+ it "raises an Encoding::CompatibilityError if the encodings are incompatible" do
+ re = Regexp.new "れ".encode(Encoding::EUC_JP)
+ -> do
+ "ã‚れ".send(@method, re)
+ end.should.raise(Encoding::CompatibilityError, "incompatible encoding regexp match (EUC-JP regexp with UTF-8 string)")
+ end
+ end
+
+ describe "with global variables" do
+ it "doesn't set $~ for non regex search" do
+ $~ = nil
+
+ 'hello.'.send(@method, 'll')
+ $~.should == nil
+ end
+
+ it "sets $~ to MatchData of match and nil when there's none" do
+ 'hello.'.send(@method, /.e./)
+ $~[0].should == 'hel'
+
+ 'hello.'.send(@method, /not/)
+ $~.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/string/shared/chars.rb b/spec/ruby/core/string/shared/chars.rb
new file mode 100644
index 0000000000..826d403589
--- /dev/null
+++ b/spec/ruby/core/string/shared/chars.rb
@@ -0,0 +1,86 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_chars, shared: true do
+ it "passes each char in self to the given block" do
+ a = []
+ "hello".send(@method) { |c| a << c }
+ a.should == ['h', 'e', 'l', 'l', 'o']
+ end
+
+ it "returns self" do
+ s = StringSpecs::MyString.new "hello"
+ s.send(@method){}.should.equal?(s)
+ end
+
+ it "is unicode aware" do
+ "\303\207\342\210\202\303\251\306\222g".send(@method).to_a.should ==
+ ["\303\207", "\342\210\202", "\303\251", "\306\222", "g"]
+ end
+
+ it "returns characters in the same encoding as self" do
+ "&%".dup.force_encoding('Shift_JIS').send(@method).to_a.all? {|c| c.encoding.name.should == 'Shift_JIS'}
+ "&%".encode('BINARY').send(@method).to_a.all? {|c| c.encoding.should == Encoding::BINARY }
+ end
+
+ it "works with multibyte characters" do
+ s = "\u{8987}".dup.force_encoding("UTF-8")
+ s.bytesize.should == 3
+ s.send(@method).to_a.should == [s]
+ end
+
+ it "works if the String's contents is invalid for its encoding" do
+ xA4 = [0xA4].pack('C')
+ xA4.force_encoding('UTF-8')
+ xA4.valid_encoding?.should == false
+ xA4.send(@method).to_a.should == [xA4.force_encoding("UTF-8")]
+ end
+
+ it "returns a different character if the String is transcoded" do
+ s = "\u{20AC}".dup.force_encoding('UTF-8')
+ s.encode('UTF-8').send(@method).to_a.should == ["\u{20AC}".dup.force_encoding('UTF-8')]
+ s.encode('iso-8859-15').send(@method).to_a.should == [[0xA4].pack('C').force_encoding('iso-8859-15')]
+ s.encode('iso-8859-15').encode('UTF-8').send(@method).to_a.should == ["\u{20AC}".dup.force_encoding('UTF-8')]
+ end
+
+ it "uses the String's encoding to determine what characters it contains" do
+ s = +"\u{24B62}"
+
+ s.force_encoding('UTF-8').send(@method).to_a.should == [
+ s.force_encoding('UTF-8')
+ ]
+ s.force_encoding('BINARY').send(@method).to_a.should == [
+ [0xF0].pack('C').force_encoding('BINARY'),
+ [0xA4].pack('C').force_encoding('BINARY'),
+ [0xAD].pack('C').force_encoding('BINARY'),
+ [0xA2].pack('C').force_encoding('BINARY')
+ ]
+ s.force_encoding('SJIS').send(@method).to_a.should == [
+ [0xF0,0xA4].pack('CC').force_encoding('SJIS'),
+ [0xAD].pack('C').force_encoding('SJIS'),
+ [0xA2].pack('C').force_encoding('SJIS')
+ ]
+ end
+
+ it "returns individual chars for dummy encodings" do
+ "ab".dup.force_encoding(Encoding::UTF_7).send(@method).to_a.should == [
+ "\x61".dup.force_encoding(Encoding::UTF_7),
+ "\x62".dup.force_encoding(Encoding::UTF_7)
+ ]
+
+ "abcd".dup.force_encoding(Encoding::UTF_16).send(@method).to_a.should == [
+ "\x61".dup.force_encoding(Encoding::UTF_16),
+ "\x62".dup.force_encoding(Encoding::UTF_16),
+ "\x63".dup.force_encoding(Encoding::UTF_16),
+ "\x64".dup.force_encoding(Encoding::UTF_16)
+ ]
+
+ "abcd".dup.force_encoding(Encoding::UTF_32).send(@method).to_a.should == [
+ "\x61".dup.force_encoding(Encoding::UTF_32),
+ "\x62".dup.force_encoding(Encoding::UTF_32),
+ "\x63".dup.force_encoding(Encoding::UTF_32),
+ "\x64".dup.force_encoding(Encoding::UTF_32)
+ ]
+ end
+end
diff --git a/spec/ruby/core/string/shared/codepoints.rb b/spec/ruby/core/string/shared/codepoints.rb
new file mode 100644
index 0000000000..b6abf6a3ff
--- /dev/null
+++ b/spec/ruby/core/string/shared/codepoints.rb
@@ -0,0 +1,67 @@
+# encoding: binary
+describe :string_codepoints, shared: true do
+ it "returns self" do
+ s = "foo"
+ result = s.send(@method) {}
+ result.should.equal? s
+ end
+
+ it "raises an ArgumentError when self has an invalid encoding and a method is called on the returned Enumerator" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.send(@method).to_a }.should.raise(ArgumentError)
+ end
+
+ it "yields each codepoint to the block if one is given" do
+ codepoints = []
+ "abcd".send(@method) do |codepoint|
+ codepoints << codepoint
+ end
+ codepoints.should == [97, 98, 99, 100]
+ end
+
+ it "raises an ArgumentError if self's encoding is invalid and a block is given" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ -> { s.send(@method) { } }.should.raise(ArgumentError)
+ end
+
+ it "yields codepoints as Integers" do
+ "glark\u{20}".send(@method).to_a.each do |codepoint|
+ codepoint.should.instance_of?(Integer)
+ end
+ end
+
+ it "yields one codepoint for each character" do
+ s = "\u{9876}\u{28}\u{1987}"
+ s.send(@method).to_a.size.should == s.chars.to_a.size
+ end
+
+ it "works for multibyte characters" do
+ s = "\u{9819}"
+ s.bytesize.should == 3
+ s.send(@method).to_a.should == [38937]
+ end
+
+ it "yields the codepoints corresponding to the character's position in the String's encoding" do
+ "\u{787}".send(@method).to_a.should == [1927]
+ end
+
+ it "round-trips to the original String using Integer#chr" do
+ s = "\u{13}\u{7711}\u{1010}"
+ s2 = +""
+ s.send(@method) {|n| s2 << n.chr(Encoding::UTF_8)}
+ s.should == s2
+ end
+
+ it "is synonymous with #bytes for Strings which are single-byte optimizable" do
+ s = "(){}".encode('ascii')
+ s.ascii_only?.should == true
+ s.send(@method).to_a.should == s.bytes.to_a
+ end
+
+ it "returns individual bytes for dummy encodings UTF-16 and UTF-32" do
+ "abcd".dup.force_encoding(Encoding::UTF_16).send(@method).to_a.should == [97, 98, 99, 100]
+ "abcd".dup.force_encoding(Encoding::UTF_32).send(@method).to_a.should == [97, 98, 99, 100]
+ end
+end
diff --git a/spec/ruby/core/string/shared/concat.rb b/spec/ruby/core/string/shared/concat.rb
new file mode 100644
index 0000000000..60cd0e12d4
--- /dev/null
+++ b/spec/ruby/core/string/shared/concat.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: false
+describe :string_concat, shared: true do
+ it "concatenates the given argument to self and returns self" do
+ str = 'hello '
+ str.send(@method, 'world').should.equal?(str)
+ str.should == "hello world"
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ a = "hello"
+ a.freeze
+
+ -> { a.send(@method, "") }.should.raise(FrozenError)
+ -> { a.send(@method, "test") }.should.raise(FrozenError)
+ end
+
+ it "returns a String when given a subclass instance" do
+ a = "hello"
+ a.send(@method, StringSpecs::MyString.new(" world"))
+ a.should == "hello world"
+ a.should.instance_of?(String)
+ end
+
+ it "returns an instance of same class when called on a subclass" do
+ str = StringSpecs::MyString.new("hello")
+ str.send(@method, " world")
+ str.should == "hello world"
+ str.should.instance_of?(StringSpecs::MyString)
+ end
+
+ describe "with Integer" do
+ it "concatenates the argument interpreted as a codepoint" do
+ b = "".send(@method, 33)
+ b.should == "!"
+
+ b.encode!(Encoding::UTF_8)
+ b.send(@method, 0x203D)
+ b.should == "!\u203D"
+ end
+
+ # #5855
+ it "returns a BINARY string if self is US-ASCII and the argument is between 128-255 (inclusive)" do
+ a = ("".encode(Encoding::US_ASCII).send(@method, 128))
+ a.encoding.should == Encoding::BINARY
+ a.should == 128.chr
+
+ a = ("".encode(Encoding::US_ASCII).send(@method, 255))
+ a.encoding.should == Encoding::BINARY
+ a.should == 255.chr
+ end
+
+ it "raises RangeError if the argument is an invalid codepoint for self's encoding" do
+ -> { "".encode(Encoding::US_ASCII).send(@method, 256) }.should.raise(RangeError)
+ -> { "".encode(Encoding::EUC_JP).send(@method, 0x81) }.should.raise(RangeError)
+ end
+
+ it "raises RangeError if the argument is negative" do
+ -> { "".send(@method, -200) }.should.raise(RangeError)
+ -> { "".send(@method, -bignum_value) }.should.raise(RangeError)
+ end
+
+ it "doesn't call to_int on its argument" do
+ x = mock('x')
+ x.should_not_receive(:to_int)
+
+ -> { "".send(@method, x) }.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ a = "hello"
+ a.freeze
+
+ -> { a.send(@method, 0) }.should.raise(FrozenError)
+ -> { a.send(@method, 33) }.should.raise(FrozenError)
+ end
+ end
+end
+
+describe :string_concat_encoding, shared: true do
+ describe "when self is in an ASCII-incompatible encoding incompatible with the argument's encoding" do
+ it "uses self's encoding if both are empty" do
+ "".encode("UTF-16LE").send(@method, "").encoding.should == Encoding::UTF_16LE
+ end
+
+ it "uses self's encoding if the argument is empty" do
+ "x".encode("UTF-16LE").send(@method, "").encoding.should == Encoding::UTF_16LE
+ end
+
+ it "uses the argument's encoding if self is empty" do
+ "".encode("UTF-16LE").send(@method, "x".encode("UTF-8")).encoding.should == Encoding::UTF_8
+ end
+
+ it "raises Encoding::CompatibilityError if neither are empty" do
+ -> { "x".encode("UTF-16LE").send(@method, "y".encode("UTF-8")) }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ describe "when the argument is in an ASCII-incompatible encoding incompatible with self's encoding" do
+ it "uses self's encoding if both are empty" do
+ "".encode("UTF-8").send(@method, "".encode("UTF-16LE")).encoding.should == Encoding::UTF_8
+ end
+
+ it "uses self's encoding if the argument is empty" do
+ "x".encode("UTF-8").send(@method, "".encode("UTF-16LE")).encoding.should == Encoding::UTF_8
+ end
+
+ it "uses the argument's encoding if self is empty" do
+ "".encode("UTF-8").send(@method, "x".encode("UTF-16LE")).encoding.should == Encoding::UTF_16LE
+ end
+
+ it "raises Encoding::CompatibilityError if neither are empty" do
+ -> { "x".encode("UTF-8").send(@method, "y".encode("UTF-16LE")) }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ describe "when self and the argument are in different ASCII-compatible encodings" do
+ it "uses self's encoding if both are ASCII-only" do
+ "abc".encode("UTF-8").send(@method, "123".encode("SHIFT_JIS")).encoding.should == Encoding::UTF_8
+ end
+
+ it "uses self's encoding if the argument is ASCII-only" do
+ "\u00E9".encode("UTF-8").send(@method, "123".encode("ISO-8859-1")).encoding.should == Encoding::UTF_8
+ end
+
+ it "uses the argument's encoding if self is ASCII-only" do
+ "abc".encode("UTF-8").send(@method, "\u00E9".encode("ISO-8859-1")).encoding.should == Encoding::ISO_8859_1
+ end
+
+ it "raises Encoding::CompatibilityError if neither are ASCII-only" do
+ -> { "\u00E9".encode("UTF-8").send(@method, "\u00E9".encode("ISO-8859-1")) }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ describe "when self is BINARY and argument is US-ASCII" do
+ it "uses BINARY encoding" do
+ "abc".encode("BINARY").send(@method, "123".encode("US-ASCII")).encoding.should == Encoding::BINARY
+ end
+ end
+end
+
+describe :string_concat_type_coercion, shared: true do
+ it "converts the given argument to a String using to_str" do
+ obj = mock('world!')
+ obj.should_receive(:to_str).and_return("world!")
+ a = 'hello '.send(@method, obj)
+ a.should == 'hello world!'
+ end
+
+ it "raises a TypeError if the given argument can't be converted to a String" do
+ -> { 'hello '.send(@method, []) }.should.raise(TypeError)
+ -> { 'hello '.send(@method, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises a NoMethodError if the given argument raises a NoMethodError during type coercion to a String" do
+ obj = mock('world!')
+ obj.should_receive(:to_str).and_raise(NoMethodError)
+ -> { 'hello '.send(@method, obj) }.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/string/shared/dedup.rb b/spec/ruby/core/string/shared/dedup.rb
new file mode 100644
index 0000000000..59506c2901
--- /dev/null
+++ b/spec/ruby/core/string/shared/dedup.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: false
+describe :string_dedup, shared: true do
+ it 'returns self if the String is frozen' do
+ input = 'foo'.freeze
+ output = input.send(@method)
+
+ output.should.equal?(input)
+ output.should.frozen?
+ end
+
+ it 'returns a frozen copy if the String is not frozen' do
+ input = 'foo'
+ output = input.send(@method)
+
+ output.should.frozen?
+ output.should_not.equal?(input)
+ output.should == 'foo'
+ end
+
+ it "returns the same object for equal unfrozen strings" do
+ origin = "this is a string"
+ dynamic = %w(this is a string).join(' ')
+
+ origin.should_not.equal?(dynamic)
+ origin.send(@method).should.equal?(dynamic.send(@method))
+ end
+
+ it "returns the same object when it's called on the same String literal" do
+ "unfrozen string".send(@method).should.equal?("unfrozen string".send(@method))
+ "unfrozen string".send(@method).should_not.equal?("another unfrozen string".send(@method))
+ end
+
+ it "deduplicates frozen strings" do
+ dynamic = %w(this string is frozen).join(' ').freeze
+
+ dynamic.should_not.equal?("this string is frozen".freeze)
+
+ dynamic.send(@method).should.equal?("this string is frozen".freeze)
+ dynamic.send(@method).should.equal?("this string is frozen".send(@method).freeze)
+ end
+
+ it "does not deduplicate a frozen string when it has instance variables" do
+ dynamic = %w(this string is frozen).join(' ')
+ dynamic.instance_variable_set(:@a, 1)
+ dynamic.freeze
+
+ dynamic.send(@method).should_not.equal?("this string is frozen".freeze)
+ dynamic.send(@method).should_not.equal?("this string is frozen".send(@method).freeze)
+ dynamic.send(@method).should.equal?(dynamic)
+ end
+end
diff --git a/spec/ruby/core/string/shared/each_char_without_block.rb b/spec/ruby/core/string/shared/each_char_without_block.rb
new file mode 100644
index 0000000000..3c32bae42b
--- /dev/null
+++ b/spec/ruby/core/string/shared/each_char_without_block.rb
@@ -0,0 +1,26 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_each_char_without_block, shared: true do
+ describe "when no block is given" do
+ it "returns an enumerator" do
+ enum = "hello".send(@method)
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == ['h', 'e', 'l', 'l', 'o']
+ end
+
+ describe "returned enumerator" do
+ describe "size" do
+ it "should return the size of the string" do
+ str = "hello"
+ str.send(@method).size.should == str.size
+ str = "ola"
+ str.send(@method).size.should == str.size
+ str = "\303\207\342\210\202\303\251\306\222g"
+ str.send(@method).size.should == str.size
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/shared/each_codepoint_without_block.rb b/spec/ruby/core/string/shared/each_codepoint_without_block.rb
new file mode 100644
index 0000000000..60d603954c
--- /dev/null
+++ b/spec/ruby/core/string/shared/each_codepoint_without_block.rb
@@ -0,0 +1,33 @@
+# encoding: binary
+describe :string_each_codepoint_without_block, shared: true do
+ describe "when no block is given" do
+ it "returns an Enumerator" do
+ "".send(@method).should.instance_of?(Enumerator)
+ end
+
+ it "returns an Enumerator even when self has an invalid encoding" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ s.send(@method).should.instance_of?(Enumerator)
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return the size of the string" do
+ str = "hello"
+ str.send(@method).size.should == str.size
+ str = "ola"
+ str.send(@method).size.should == str.size
+ str = "\303\207\342\210\202\303\251\306\222g"
+ str.send(@method).size.should == str.size
+ end
+
+ it "should return the size of the string even when the string has an invalid encoding" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+ s.valid_encoding?.should == false
+ s.send(@method).size.should == 1
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/shared/each_line.rb b/spec/ruby/core/string/shared/each_line.rb
new file mode 100644
index 0000000000..127db876ad
--- /dev/null
+++ b/spec/ruby/core/string/shared/each_line.rb
@@ -0,0 +1,198 @@
+describe :string_each_line, shared: true do
+ it "splits using default newline separator when none is specified" do
+ a = []
+ "one\ntwo\r\nthree".send(@method) { |s| a << s }
+ a.should == ["one\n", "two\r\n", "three"]
+
+ b = []
+ "hello\n\n\nworld".send(@method) { |s| b << s }
+ b.should == ["hello\n", "\n", "\n", "world"]
+
+ c = []
+ "\n\n\n\n\n".send(@method) {|s| c << s}
+ c.should == ["\n", "\n", "\n", "\n", "\n"]
+ end
+
+ it "splits self using the supplied record separator and passes each substring to the block" do
+ a = []
+ "one\ntwo\r\nthree".send(@method, "\n") { |s| a << s }
+ a.should == ["one\n", "two\r\n", "three"]
+
+ b = []
+ "hello\nworld".send(@method, 'l') { |s| b << s }
+ b.should == [ "hel", "l", "o\nworl", "d" ]
+
+ c = []
+ "hello\n\n\nworld".send(@method, "\n") { |s| c << s }
+ c.should == ["hello\n", "\n", "\n", "world"]
+ end
+
+ it "splits strings containing multibyte characters" do
+ s = <<~EOS
+ foo
+ 🤡🤡🤡🤡🤡🤡🤡
+ bar
+ baz
+ EOS
+
+ b = []
+ s.send(@method) { |part| b << part }
+ b.should == ["foo\n", "🤡🤡🤡🤡🤡🤡🤡\n", "bar\n", "baz\n"]
+ end
+
+ it "passes self as a whole to the block if the separator is nil" do
+ a = []
+ "one\ntwo\r\nthree".send(@method, nil) { |s| a << s }
+ a.should == ["one\ntwo\r\nthree"]
+ end
+
+ context "when passed '' (paragraph mode, broken by 2 or more successive newlines)" do
+ it "replaces multiple newlines with only two ones" do
+ a = []
+ "hello\nworld\n\n\nand\nuniverse\n\n\n\n\n".send(@method, '') { |s| a << s }
+ a.should == ["hello\nworld\n\n", "and\nuniverse\n\n"]
+
+ a = []
+ "hello\nworld\n\n\nand\nuniverse\n\n\n\n\ndog".send(@method, '') { |s| a << s }
+ a.should == ["hello\nworld\n\n", "and\nuniverse\n\n", "dog"]
+ end
+
+ it 'handles \r\n-style newlines' do
+ a = []
+ "hello\nworld\r\n\r\n\nand\nuniverse\n\r\n\n\n\n".send(@method, '') { |s| a << s }
+ a.should == ["hello\nworld\r\n\r\n", "and\nuniverse\n\r\n"]
+
+ a = []
+ "hello\r\nworld\n\n\nand\nuniverse\n\n\n\r\n\r\ndog".send(@method, '') { |s| a << s }
+ a.should == ["hello\r\nworld\n\n", "and\nuniverse\n\n", "dog"]
+ end
+
+ it "removes trailing newlines with `chomp: true`" do
+ a = []
+ "hello\nworld\n\n\nand\nuniverse\n\n\n\n\n".send(@method, '', chomp: true) { |s| a << s }
+ a.should == ["hello\nworld", "and\nuniverse"]
+
+ a = []
+ "hello\nworld\n\n\nand\nuniverse\n\n\n\n\ndog".send(@method, '', chomp: true) { |s| a << s }
+ a.should == ["hello\nworld", "and\nuniverse", "dog"]
+ end
+ end
+
+ describe "uses $/" do
+ before :each do
+ @before_separator = $/
+ end
+
+ after :each do
+ suppress_warning {$/ = @before_separator}
+ end
+
+ it "as the separator when none is given" do
+ [
+ "", "x", "x\ny", "x\ry", "x\r\ny", "x\n\r\r\ny",
+ "hello hullo bello"
+ ].each do |str|
+ ["", "llo", "\n", "\r", nil].each do |sep|
+ expected = []
+ str.send(@method, sep) { |x| expected << x }
+
+ suppress_warning {$/ = sep}
+
+ actual = []
+ suppress_warning {str.send(@method) { |x| actual << x }}
+
+ actual.should == expected
+ end
+ end
+ end
+ end
+
+ it "yields String instances for subclasses" do
+ a = []
+ StringSpecs::MyString.new("hello\nworld").send(@method) { |s| a << s.class }
+ a.should == [String, String]
+ end
+
+ it "returns self" do
+ s = "hello\nworld"
+ (s.send(@method) {}).should.equal?(s)
+ end
+
+ it "tries to convert the separator to a string using to_str" do
+ separator = mock('l')
+ separator.should_receive(:to_str).and_return("l")
+
+ a = []
+ "hello\nworld".send(@method, separator) { |s| a << s }
+ a.should == [ "hel", "l", "o\nworl", "d" ]
+ end
+
+ it "does not care if the string is modified while substituting" do
+ str = +"hello\nworld."
+ out = []
+ str.send(@method){|x| out << x; str[-1] = '!' }.should == "hello\nworld!"
+ out.should == ["hello\n", "world."]
+ end
+
+ it "returns Strings in the same encoding as self" do
+ "one\ntwo\r\nthree".encode("US-ASCII").send(@method) do |s|
+ s.encoding.should == Encoding::US_ASCII
+ end
+ end
+
+ it "raises a TypeError when the separator can't be converted to a string" do
+ -> { "hello world".send(@method, false) {} }.should.raise(TypeError)
+ -> { "hello world".send(@method, mock('x')) {} }.should.raise(TypeError)
+ end
+
+ it "accepts a string separator" do
+ "hello world".send(@method, ?o).to_a.should == ["hello", " wo", "rld"]
+ end
+
+ it "raises a TypeError when the separator is a symbol" do
+ -> { "hello world".send(@method, :o).to_a }.should.raise(TypeError)
+ end
+
+ context "when `chomp` keyword argument is passed" do
+ it "removes new line characters when separator is not specified" do
+ a = []
+ "hello \nworld\n".send(@method, chomp: true) { |s| a << s }
+ a.should == ["hello ", "world"]
+
+ a = []
+ "hello \r\nworld\r\n".send(@method, chomp: true) { |s| a << s }
+ a.should == ["hello ", "world"]
+ end
+
+ it "removes only specified separator" do
+ a = []
+ "hello world".send(@method, ' ', chomp: true) { |s| a << s }
+ a.should == ["hello", "world"]
+ end
+
+ # https://bugs.ruby-lang.org/issues/14257
+ it "ignores new line characters when separator is specified" do
+ a = []
+ "hello\n world\n".send(@method, ' ', chomp: true) { |s| a << s }
+ a.should == ["hello\n", "world\n"]
+
+ a = []
+ "hello\r\n world\r\n".send(@method, ' ', chomp: true) { |s| a << s }
+ a.should == ["hello\r\n", "world\r\n"]
+ end
+ end
+
+ it "does not split lines for dummy UTF-16" do
+ "a\nb".encode(Encoding::UTF_16).lines.should == [
+ "\xFE\xFF\x00\x61\x00\n\x00\x62".dup.force_encoding(Encoding::UTF_16)
+ ]
+
+ str = "\x00\n\n\x00".dup.force_encoding(Encoding::UTF_16)
+ str.lines.should == [str]
+ end
+
+ it "raises Encoding::ConverterNotFoundError for dummy UTF-7" do
+ str = "a\nb".dup.force_encoding(Encoding::UTF_7)
+ -> { str.lines }.should.raise(Encoding::ConverterNotFoundError)
+ end
+end
diff --git a/spec/ruby/core/string/shared/each_line_without_block.rb b/spec/ruby/core/string/shared/each_line_without_block.rb
new file mode 100644
index 0000000000..af0ab69c00
--- /dev/null
+++ b/spec/ruby/core/string/shared/each_line_without_block.rb
@@ -0,0 +1,17 @@
+describe :string_each_line_without_block, shared: true do
+ describe "when no block is given" do
+ it "returns an enumerator" do
+ enum = "hello world".send(@method, ' ')
+ enum.should.instance_of?(Enumerator)
+ enum.to_a.should == ["hello ", "world"]
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ "hello world".send(@method, ' ').size.should == nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/shared/encode.rb b/spec/ruby/core/string/shared/encode.rb
new file mode 100644
index 0000000000..7f644c26d9
--- /dev/null
+++ b/spec/ruby/core/string/shared/encode.rb
@@ -0,0 +1,448 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+describe :string_encode, shared: true do
+ describe "when passed no options" do
+ it "transcodes to Encoding.default_internal when set" do
+ Encoding.default_internal = Encoding::UTF_8
+ str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP
+ str.send(@method).should == "ã‚"
+ end
+
+ it "transcodes a 7-bit String despite no generic converting being available" do
+ -> do
+ Encoding::Converter.new Encoding::Emacs_Mule, Encoding::BINARY
+ end.should.raise(Encoding::ConverterNotFoundError)
+
+ Encoding.default_internal = Encoding::Emacs_Mule
+ str = "\x79".force_encoding Encoding::BINARY
+
+ str.send(@method).should == "y".force_encoding(Encoding::BINARY)
+ end
+
+ it "raises an Encoding::ConverterNotFoundError when no conversion is possible" do
+ Encoding.default_internal = Encoding::Emacs_Mule
+ str = [0x80].pack('C').force_encoding Encoding::BINARY
+ -> { str.send(@method) }.should.raise(Encoding::ConverterNotFoundError)
+ end
+ end
+
+ describe "when passed to encoding" do
+ it "accepts a String argument" do
+ str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP
+ str.send(@method, "utf-8").should == "ã‚"
+ end
+
+ it "calls #to_str to convert the object to an Encoding" do
+ enc = mock("string encode encoding")
+ enc.should_receive(:to_str).and_return("utf-8")
+
+ str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP
+ str.send(@method, enc).should == "ã‚"
+ end
+
+ it "transcodes to the passed encoding" do
+ str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP
+ str.send(@method, Encoding::UTF_8).should == "ã‚"
+ end
+
+ it "transcodes Japanese multibyte characters" do
+ str = "ã‚ã„ã†ãˆãŠ"
+ str.send(@method, Encoding::ISO_2022_JP).should ==
+ "\e\x24\x42\x24\x22\x24\x24\x24\x26\x24\x28\x24\x2A\e\x28\x42".force_encoding(Encoding::ISO_2022_JP)
+ end
+
+ it "transcodes a 7-bit String despite no generic converting being available" do
+ -> do
+ Encoding::Converter.new Encoding::Emacs_Mule, Encoding::BINARY
+ end.should.raise(Encoding::ConverterNotFoundError)
+
+ str = "\x79".force_encoding Encoding::BINARY
+ str.send(@method, Encoding::Emacs_Mule).should == "y".force_encoding(Encoding::BINARY)
+ end
+
+ it "raises an Encoding::ConverterNotFoundError when no conversion is possible" do
+ str = [0x80].pack('C').force_encoding Encoding::BINARY
+ -> do
+ str.send(@method, Encoding::Emacs_Mule)
+ end.should.raise(Encoding::ConverterNotFoundError)
+ end
+
+ it "raises an Encoding::ConverterNotFoundError for an invalid encoding" do
+ -> do
+ "abc".send(@method, "xyz")
+ end.should.raise(Encoding::ConverterNotFoundError)
+ end
+
+ it "raises an Encoding::UndefinedConversionError when a character cannot be represented in the destination encoding" do
+ # U+0100 (Ä€) is valid UTF-8 but not representable in windows-1252
+ str = "test\u0100".force_encoding('utf-8')
+ -> {
+ str.send(@method, Encoding::Windows_1252)
+ }.should.raise(Encoding::UndefinedConversionError)
+ end
+ end
+
+ describe "when passed options" do
+ it "does not process transcoding options if not transcoding" do
+ result = "ã‚\ufffdã‚".send(@method, undef: :replace)
+ result.should == "ã‚\ufffdã‚"
+ end
+
+ it "calls #to_hash to convert the object" do
+ options = mock("string encode options")
+ options.should_receive(:to_hash).and_return({ undef: :replace })
+
+ result = "ã‚\ufffdã‚".send(@method, **options)
+ result.should == "ã‚\ufffdã‚"
+ end
+
+ it "transcodes to Encoding.default_internal when set" do
+ Encoding.default_internal = Encoding::UTF_8
+ str = [0xA4, 0xA2].pack('CC').force_encoding Encoding::EUC_JP
+ str.send(@method, invalid: :replace).should == "ã‚"
+ end
+
+ it "raises an Encoding::ConverterNotFoundError when no conversion is possible despite 'invalid: :replace, undef: :replace'" do
+ Encoding.default_internal = Encoding::Emacs_Mule
+ str = [0x80].pack('C').force_encoding Encoding::BINARY
+ -> do
+ str.send(@method, invalid: :replace, undef: :replace)
+ end.should.raise(Encoding::ConverterNotFoundError)
+ end
+
+ it "replaces invalid characters when replacing Emacs-Mule encoded strings" do
+ got = [0x80].pack('C').force_encoding('Emacs-Mule').send(@method, invalid: :replace)
+
+ got.should == "?".encode('Emacs-Mule')
+ end
+ end
+
+ describe "when passed to, from" do
+ it "transcodes between the encodings ignoring the String encoding" do
+ str = "ã‚"
+ result = [0xA6, 0xD0, 0x8F, 0xAB, 0xE4, 0x8F, 0xAB, 0xB1].pack('C8')
+ result.force_encoding Encoding::EUC_JP
+ str.send(@method, "euc-jp", "ibm437").should == result
+ end
+
+ it "calls #to_str to convert the from object to an Encoding" do
+ enc = mock("string encode encoding")
+ enc.should_receive(:to_str).and_return("ibm437")
+
+ str = "ã‚"
+ result = [0xA6, 0xD0, 0x8F, 0xAB, 0xE4, 0x8F, 0xAB, 0xB1].pack('C8')
+ result.force_encoding Encoding::EUC_JP
+
+ str.send(@method, "euc-jp", enc).should == result
+ end
+ end
+
+ describe "when passed to, options" do
+ it "replaces undefined characters in the destination encoding" do
+ result = "ã‚?ã‚".send(@method, Encoding::EUC_JP, undef: :replace)
+ # testing for: "\xA4\xA2?\xA4\xA2"
+ xA4xA2 = [0xA4, 0xA2].pack('CC')
+ result.should == "#{xA4xA2}?#{xA4xA2}".force_encoding("euc-jp")
+ end
+
+ it "replaces invalid characters in the destination encoding" do
+ xFF = [0xFF].pack('C').force_encoding('utf-8')
+ "ab#{xFF}c".send(@method, Encoding::ISO_8859_1, invalid: :replace).should == "ab?c"
+ end
+
+ it "raises UndefinedConversionError for characters not representable in destination encoding with only invalid: :replace" do
+ # U+0100 (Ä€) is valid UTF-8 but not representable in windows-1252
+ str = "test\u0100".force_encoding('utf-8')
+ -> {
+ str.send(@method, Encoding::Windows_1252, invalid: :replace, replace: "")
+ }.should.raise(Encoding::UndefinedConversionError)
+ end
+
+ it "calls #to_hash to convert the options object" do
+ options = mock("string encode options")
+ options.should_receive(:to_hash).and_return({ undef: :replace })
+
+ result = "ã‚?ã‚".send(@method, Encoding::EUC_JP, **options)
+ xA4xA2 = [0xA4, 0xA2].pack('CC').force_encoding('utf-8')
+ result.should == "#{xA4xA2}?#{xA4xA2}".force_encoding("euc-jp")
+ end
+ end
+
+ describe "when passed to, from, options" do
+ it "replaces undefined characters in the destination encoding" do
+ str = "ã‚?ã‚".force_encoding Encoding::BINARY
+ result = str.send(@method, "euc-jp", "utf-8", undef: :replace)
+ xA4xA2 = [0xA4, 0xA2].pack('CC').force_encoding('utf-8')
+ result.should == "#{xA4xA2}?#{xA4xA2}".force_encoding("euc-jp")
+ end
+
+ it "replaces invalid characters in the destination encoding" do
+ xFF = [0xFF].pack('C').force_encoding('utf-8')
+ str = "ab#{xFF}c".force_encoding Encoding::BINARY
+ str.send(@method, "iso-8859-1", "utf-8", invalid: :replace).should == "ab?c"
+ end
+
+ it "calls #to_str to convert the to object to an encoding" do
+ to = mock("string encode to encoding")
+ to.should_receive(:to_str).and_return("iso-8859-1")
+
+ xFF = [0xFF].pack('C').force_encoding('utf-8')
+ str = "ab#{xFF}c".force_encoding Encoding::BINARY
+ str.send(@method, to, "utf-8", invalid: :replace).should == "ab?c"
+ end
+
+ it "calls #to_str to convert the from object to an encoding" do
+ from = mock("string encode to encoding")
+ from.should_receive(:to_str).and_return("utf-8")
+
+ xFF = [0xFF].pack('C').force_encoding('utf-8')
+ str = "ab#{xFF}c".force_encoding Encoding::BINARY
+ str.send(@method, "iso-8859-1", from, invalid: :replace).should == "ab?c"
+ end
+
+ it "calls #to_hash to convert the options object" do
+ options = mock("string encode options")
+ options.should_receive(:to_hash).and_return({ invalid: :replace })
+
+ xFF = [0xFF].pack('C').force_encoding('utf-8')
+ str = "ab#{xFF}c".force_encoding Encoding::BINARY
+ str.send(@method, "iso-8859-1", "utf-8", **options).should == "ab?c"
+ end
+ end
+
+ describe "given the fallback option" do
+ context "given a hash" do
+ it "looks up the replacement value from the hash" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => "bar" })
+ encoded.should == "Bbar"
+ end
+
+ it "calls to_str on the returned value" do
+ obj = Object.new
+ obj.should_receive(:to_str).and_return("bar")
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => obj })
+ encoded.should == "Bbar"
+ end
+
+ it "does not call to_s on the returned value" do
+ obj = Object.new
+ obj.should_not_receive(:to_s)
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => obj })
+ }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises an error if the key is not present in the hash" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: { "foo" => "bar" })
+ }.should.raise(Encoding::UndefinedConversionError, "U+FFFD from UTF-8 to US-ASCII")
+ end
+
+ it "raises an error if the value is itself invalid" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: { "\ufffd" => "\uffee" })
+ }.should.raise(ArgumentError, "too big fallback string")
+ end
+
+ it "uses the hash's default value if set" do
+ hash = {}
+ hash.default = "bar"
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: hash)
+ encoded.should == "Bbar"
+ end
+
+ it "uses the result of calling default_proc if set" do
+ hash = {}
+ hash.default_proc = -> _, _ { "bar" }
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: hash)
+ encoded.should == "Bbar"
+ end
+ end
+
+ context "given an object inheriting from Hash" do
+ before do
+ klass = Class.new(Hash)
+ @hash_like = klass.new
+ @hash_like["\ufffd"] = "bar"
+ end
+
+ it "looks up the replacement value from the object" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: @hash_like)
+ encoded.should == "Bbar"
+ end
+ end
+
+ context "given an object responding to []" do
+ before do
+ klass = Class.new do
+ def [](c) = c.bytes.inspect
+ end
+ @hash_like = klass.new
+ end
+
+ it "calls [] on the object, passing the invalid character" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: @hash_like)
+ encoded.should == "B[239, 191, 189]"
+ end
+ end
+
+ context "given an object not responding to []" do
+ before do
+ @non_hash_like = Object.new
+ end
+
+ it "raises an error" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: @non_hash_like)
+ }.should.raise(Encoding::UndefinedConversionError, "U+FFFD from UTF-8 to US-ASCII")
+ end
+ end
+
+ context "given a proc" do
+ it "calls the proc to get the replacement value, passing in the invalid character" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| c.bytes.inspect })
+ encoded.should == "B[239, 191, 189]"
+ end
+
+ it "calls to_str on the returned value" do
+ obj = Object.new
+ obj.should_receive(:to_str).and_return("bar")
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| obj })
+ encoded.should == "Bbar"
+ end
+
+ it "does not call to_s on the returned value" do
+ obj = Object.new
+ obj.should_not_receive(:to_s)
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: proc { |c| obj })
+ }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises an error if the returned value is itself invalid" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { "\uffee" })
+ }.should.raise(ArgumentError, "too big fallback string")
+ end
+ end
+
+ context "given a lambda" do
+ it "calls the lambda to get the replacement value, passing in the invalid character" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { c.bytes.inspect })
+ encoded.should == "B[239, 191, 189]"
+ end
+
+ it "calls to_str on the returned value" do
+ obj = Object.new
+ obj.should_receive(:to_str).and_return("bar")
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { obj })
+ encoded.should == "Bbar"
+ end
+
+ it "does not call to_s on the returned value" do
+ obj = Object.new
+ obj.should_not_receive(:to_s)
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { obj })
+ }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises an error if the returned value is itself invalid" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: -> c { "\uffee" })
+ }.should.raise(ArgumentError, "too big fallback string")
+ end
+ end
+
+ context "given a method" do
+ def replace(c) = c.bytes.inspect
+ def replace_bad(c) = "\uffee"
+
+ def replace_to_str(c)
+ obj = Object.new
+ obj.should_receive(:to_str).and_return("bar")
+ obj
+ end
+
+ def replace_to_s(c)
+ obj = Object.new
+ obj.should_not_receive(:to_s)
+ obj
+ end
+
+ it "calls the method to get the replacement value, passing in the invalid character" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace))
+ encoded.should == "B[239, 191, 189]"
+ end
+
+ it "calls to_str on the returned value" do
+ encoded = "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_to_str))
+ encoded.should == "Bbar"
+ end
+
+ it "does not call to_s on the returned value" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_to_s))
+ }.should.raise(TypeError, "no implicit conversion of Object into String")
+ end
+
+ it "raises an error if the returned value is itself invalid" do
+ -> {
+ "B\ufffd".encode(Encoding::US_ASCII, fallback: method(:replace_bad))
+ }.should.raise(ArgumentError, "too big fallback string")
+ end
+ end
+ end
+
+ describe "given the xml: :text option" do
+ it "replaces all instances of '&' with '&amp;'" do
+ '& and &'.send(@method, "UTF-8", xml: :text).should == '&amp; and &amp;'
+ end
+
+ it "replaces all instances of '<' with '&lt;'" do
+ '< and <'.send(@method, "UTF-8", xml: :text).should == '&lt; and &lt;'
+ end
+
+ it "replaces all instances of '>' with '&gt;'" do
+ '> and >'.send(@method, "UTF-8", xml: :text).should == '&gt; and &gt;'
+ end
+
+ it "does not replace '\"'" do
+ '" and "'.send(@method, "UTF-8", xml: :text).should == '" and "'
+ end
+
+ it "replaces undefined characters with their upper-case hexadecimal numeric character references" do
+ 'ürst'.send(@method, Encoding::US_ASCII, xml: :text).should == '&#xFC;rst'
+ end
+ end
+
+ describe "given the xml: :attr option" do
+ it "surrounds the encoded text with double-quotes" do
+ 'abc'.send(@method, "UTF-8", xml: :attr).should == '"abc"'
+ end
+
+ it "replaces all instances of '&' with '&amp;'" do
+ '& and &'.send(@method, "UTF-8", xml: :attr).should == '"&amp; and &amp;"'
+ end
+
+ it "replaces all instances of '<' with '&lt;'" do
+ '< and <'.send(@method, "UTF-8", xml: :attr).should == '"&lt; and &lt;"'
+ end
+
+ it "replaces all instances of '>' with '&gt;'" do
+ '> and >'.send(@method, "UTF-8", xml: :attr).should == '"&gt; and &gt;"'
+ end
+
+ it "replaces all instances of '\"' with '&quot;'" do
+ '" and "'.send(@method, "UTF-8", xml: :attr).should == '"&quot; and &quot;"'
+ end
+
+ it "replaces undefined characters with their upper-case hexadecimal numeric character references" do
+ 'ürst'.send(@method, Encoding::US_ASCII, xml: :attr).should == '"&#xFC;rst"'
+ end
+ end
+
+ it "raises ArgumentError if the value of the :xml option is not :text or :attr" do
+ -> { ''.send(@method, "UTF-8", xml: :other) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/shared/eql.rb b/spec/ruby/core/string/shared/eql.rb
new file mode 100644
index 0000000000..0e356c69e8
--- /dev/null
+++ b/spec/ruby/core/string/shared/eql.rb
@@ -0,0 +1,38 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_eql_value, shared: true do
+ it "returns true if self <=> string returns 0" do
+ 'hello'.send(@method, 'hello').should == true
+ end
+
+ it "returns false if self <=> string does not return 0" do
+ "more".send(@method, "MORE").should == false
+ "less".send(@method, "greater").should == false
+ end
+
+ it "ignores encoding difference of compatible string" do
+ "hello".dup.force_encoding("utf-8").send(@method, "hello".dup.force_encoding("iso-8859-1")).should == true
+ end
+
+ it "considers encoding difference of incompatible string" do
+ "\xff".dup.force_encoding("utf-8").send(@method, "\xff".dup.force_encoding("iso-8859-1")).should == false
+ end
+
+ it "considers encoding compatibility" do
+ "abcd".dup.force_encoding("utf-8").send(@method, "abcd".dup.force_encoding("utf-32le")).should == false
+ end
+
+ it "ignores subclass differences" do
+ a = "hello"
+ b = StringSpecs::MyString.new("hello")
+
+ a.send(@method, b).should == true
+ b.send(@method, a).should == true
+ end
+
+ it "returns true when comparing 2 empty strings but one is not ASCII-compatible" do
+ "".send(@method, "".dup.force_encoding('iso-2022-jp')).should == true
+ end
+end
diff --git a/spec/ruby/core/string/shared/equal_value.rb b/spec/ruby/core/string/shared/equal_value.rb
new file mode 100644
index 0000000000..dfc5c7cd29
--- /dev/null
+++ b/spec/ruby/core/string/shared/equal_value.rb
@@ -0,0 +1,29 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_equal_value, shared: true do
+ it "returns false if obj does not respond to to_str" do
+ 'hello'.send(@method, 5).should == false
+ not_supported_on :opal do
+ 'hello'.send(@method, :hello).should == false
+ end
+ 'hello'.send(@method, mock('x')).should == false
+ end
+
+ it "returns obj == self if obj responds to to_str" do
+ obj = Object.new
+
+ # String#== merely checks if #to_str is defined. It does
+ # not call it.
+ obj.stub!(:to_str)
+
+ # Don't use @method for :== in `obj.should_receive(:==)`
+ obj.should_receive(:==).and_return(true)
+
+ 'hello'.send(@method, obj).should == true
+ end
+
+ it "is not fooled by NUL characters" do
+ "abc\0def".send(@method, "abc\0xyz").should == false
+ end
+end
diff --git a/spec/ruby/core/string/shared/grapheme_clusters.rb b/spec/ruby/core/string/shared/grapheme_clusters.rb
new file mode 100644
index 0000000000..dd8c7ed5fe
--- /dev/null
+++ b/spec/ruby/core/string/shared/grapheme_clusters.rb
@@ -0,0 +1,25 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_grapheme_clusters, shared: true do
+ it "passes each grapheme cluster in self to the given block" do
+ a = []
+ # test string: abc[rainbow flag emoji][paw prints]
+ "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}".send(@method) { |c| a << c }
+ a.should == ['a', 'b', "\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}", "\u{1F43E}"]
+ end
+
+ it "returns grapheme clusters for various UTF encodings" do
+ [Encoding::UTF_16LE, Encoding::UTF_16BE, Encoding::UTF_32LE, Encoding::UTF_32BE].each do |enc|
+ a = []
+ # test string: abc[rainbow flag emoji][paw prints]
+ "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}".encode(enc).send(@method) { |c| a << c }
+ a.should == ['a', 'b', "\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}", "\u{1F43E}"].map { |s| s.encode(enc) }
+ end
+ end
+
+ it "returns self" do
+ s = StringSpecs::MyString.new "ab\u{1f3f3}\u{fe0f}\u{200d}\u{1f308}\u{1F43E}"
+ s.send(@method) {}.should.equal?(s)
+ end
+end
diff --git a/spec/ruby/core/string/shared/length.rb b/spec/ruby/core/string/shared/length.rb
new file mode 100644
index 0000000000..ae572ba755
--- /dev/null
+++ b/spec/ruby/core/string/shared/length.rb
@@ -0,0 +1,55 @@
+# encoding: utf-8
+
+describe :string_length, shared: true do
+ it "returns the length of self" do
+ "".send(@method).should == 0
+ "\x00".send(@method).should == 1
+ "one".send(@method).should == 3
+ "two".send(@method).should == 3
+ "three".send(@method).should == 5
+ "four".send(@method).should == 4
+ end
+
+ it "returns the length of a string in different encodings" do
+ utf8_str = 'ã“ã«ã¡ã‚' * 100
+ utf8_str.send(@method).should == 400
+ utf8_str.encode(Encoding::UTF_32BE).send(@method).should == 400
+ utf8_str.encode(Encoding::SHIFT_JIS).send(@method).should == 400
+ end
+
+ it "returns the length of the new self after encoding is changed" do
+ str = +'ã“ã«ã¡ã‚'
+ str.send(@method)
+
+ str.force_encoding('BINARY').send(@method).should == 12
+ end
+
+ it "returns the correct length after force_encoding(BINARY)" do
+ utf8 = "ã‚"
+ ascii = "a"
+ concat = utf8 + ascii
+
+ concat.encoding.should == Encoding::UTF_8
+ concat.bytesize.should == 4
+
+ concat.send(@method).should == 2
+ concat.force_encoding(Encoding::ASCII_8BIT)
+ concat.send(@method).should == 4
+ end
+
+ it "adds 1 for every invalid byte in UTF-8" do
+ "\xF4\x90\x80\x80".send(@method).should == 4
+ "a\xF4\x90\x80\x80b".send(@method).should == 6
+ "é\xF4\x90\x80\x80è".send(@method).should == 6
+ end
+
+ it "adds 1 (and not 2) for a incomplete surrogate in UTF-16" do
+ "\x00\xd8".dup.force_encoding("UTF-16LE").send(@method).should == 1
+ "\xd8\x00".dup.force_encoding("UTF-16BE").send(@method).should == 1
+ end
+
+ it "adds 1 for a broken sequence in UTF-32" do
+ "\x04\x03\x02\x01".dup.force_encoding("UTF-32LE").send(@method).should == 1
+ "\x01\x02\x03\x04".dup.force_encoding("UTF-32BE").send(@method).should == 1
+ end
+end
diff --git a/spec/ruby/core/string/shared/partition.rb b/spec/ruby/core/string/shared/partition.rb
new file mode 100644
index 0000000000..3f7e606eb3
--- /dev/null
+++ b/spec/ruby/core/string/shared/partition.rb
@@ -0,0 +1,33 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_partition, shared: true do
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("hello").send(@method, "l").each do |item|
+ item.should.instance_of?(String)
+ end
+
+ StringSpecs::MyString.new("hello").send(@method, "x").each do |item|
+ item.should.instance_of?(String)
+ end
+
+ StringSpecs::MyString.new("hello").send(@method, /l./).each do |item|
+ item.should.instance_of?(String)
+ end
+ end
+
+ it "returns before- and after- parts in the same encoding as self" do
+ strings = "hello".encode("US-ASCII").send(@method, "ello")
+ strings[0].encoding.should == Encoding::US_ASCII
+ strings[2].encoding.should == Encoding::US_ASCII
+
+ strings = "hello".encode("US-ASCII").send(@method, /ello/)
+ strings[0].encoding.should == Encoding::US_ASCII
+ strings[2].encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns the matching part in the separator's encoding" do
+ strings = "hello".encode("US-ASCII").send(@method, "ello")
+ strings[1].encoding.should == Encoding::UTF_8
+ end
+end
diff --git a/spec/ruby/core/string/shared/replace.rb b/spec/ruby/core/string/shared/replace.rb
new file mode 100644
index 0000000000..73b26351f1
--- /dev/null
+++ b/spec/ruby/core/string/shared/replace.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: false
+describe :string_replace, shared: true do
+ it "returns self" do
+ a = "a"
+ a.send(@method, "b").should.equal?(a)
+ end
+
+ it "replaces the content of self with other" do
+ a = "some string"
+ a.send(@method, "another string")
+ a.should == "another string"
+ end
+
+ it "replaces the encoding of self with that of other" do
+ a = "".encode("UTF-16LE")
+ b = "".encode("UTF-8")
+ a.send(@method, b)
+ a.encoding.should == Encoding::UTF_8
+ end
+
+ it "carries over the encoding invalidity" do
+ a = "\u{8765}".force_encoding('ascii')
+ "".send(@method, a).valid_encoding?.should == false
+ end
+
+ it "tries to convert other to string using to_str" do
+ other = mock('x')
+ other.should_receive(:to_str).and_return("converted to a string")
+ "hello".send(@method, other).should == "converted to a string"
+ end
+
+ it "raises a TypeError if other can't be converted to string" do
+ -> { "hello".send(@method, 123) }.should.raise(TypeError)
+ -> { "hello".send(@method, []) }.should.raise(TypeError)
+ -> { "hello".send(@method, mock('x')) }.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ a = "hello".freeze
+ -> { a.send(@method, "world") }.should.raise(FrozenError)
+ end
+
+ # see [ruby-core:23666]
+ it "raises a FrozenError on a frozen instance when self-replacing" do
+ a = "hello".freeze
+ -> { a.send(@method, a) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/shared/slice.rb b/spec/ruby/core/string/shared/slice.rb
new file mode 100644
index 0000000000..d296ab6680
--- /dev/null
+++ b/spec/ruby/core/string/shared/slice.rb
@@ -0,0 +1,517 @@
+describe :string_slice, shared: true do
+ it "returns the character code of the character at the given index" do
+ "hello".send(@method, 0).should == ?h
+ "hello".send(@method, -1).should == ?o
+ end
+
+ it "returns nil if index is outside of self" do
+ "hello".send(@method, 20).should == nil
+ "hello".send(@method, -20).should == nil
+
+ "".send(@method, 0).should == nil
+ "".send(@method, -1).should == nil
+ end
+
+ it "calls to_int on the given index" do
+ "hello".send(@method, 0.5).should == ?h
+
+ obj = mock('1')
+ obj.should_receive(:to_int).and_return(1)
+ "hello".send(@method, obj).should == ?e
+ end
+
+ it "raises a TypeError if the given index is nil" do
+ -> { "hello".send(@method, nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if the given index can't be converted to an Integer" do
+ -> { "hello".send(@method, mock('x')) }.should.raise(TypeError)
+ -> { "hello".send(@method, {}) }.should.raise(TypeError)
+ -> { "hello".send(@method, []) }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError if the index is too big" do
+ -> { "hello".send(@method, bignum_value) }.should.raise(RangeError)
+ end
+end
+
+describe :string_slice_index_length, shared: true do
+ it "returns the substring starting at the given index with the given length" do
+ "hello there".send(@method, 0,0).should == ""
+ "hello there".send(@method, 0,1).should == "h"
+ "hello there".send(@method, 0,3).should == "hel"
+ "hello there".send(@method, 0,6).should == "hello "
+ "hello there".send(@method, 0,9).should == "hello the"
+ "hello there".send(@method, 0,12).should == "hello there"
+
+ "hello there".send(@method, 1,0).should == ""
+ "hello there".send(@method, 1,1).should == "e"
+ "hello there".send(@method, 1,3).should == "ell"
+ "hello there".send(@method, 1,6).should == "ello t"
+ "hello there".send(@method, 1,9).should == "ello ther"
+ "hello there".send(@method, 1,12).should == "ello there"
+
+ "hello there".send(@method, 3,0).should == ""
+ "hello there".send(@method, 3,1).should == "l"
+ "hello there".send(@method, 3,3).should == "lo "
+ "hello there".send(@method, 3,6).should == "lo the"
+ "hello there".send(@method, 3,9).should == "lo there"
+
+ "hello there".send(@method, 4,0).should == ""
+ "hello there".send(@method, 4,3).should == "o t"
+ "hello there".send(@method, 4,6).should == "o ther"
+ "hello there".send(@method, 4,9).should == "o there"
+
+ "foo".send(@method, 2,1).should == "o"
+ "foo".send(@method, 3,0).should == ""
+ "foo".send(@method, 3,1).should == ""
+
+ "".send(@method, 0,0).should == ""
+ "".send(@method, 0,1).should == ""
+
+ "x".send(@method, 0,0).should == ""
+ "x".send(@method, 0,1).should == "x"
+ "x".send(@method, 1,0).should == ""
+ "x".send(@method, 1,1).should == ""
+
+ "x".send(@method, -1,0).should == ""
+ "x".send(@method, -1,1).should == "x"
+
+ "hello there".send(@method, -3,2).should == "er"
+ end
+
+ it "returns a string with the same encoding as self" do
+ s = "hello there"
+ s.send(@method, 1, 9).encoding.should == s.encoding
+
+ a = "hello".dup.force_encoding("binary")
+ b = " there".dup.force_encoding("ISO-8859-1")
+ c = (a + b).force_encoding(Encoding::US_ASCII)
+
+ c.send(@method, 0, 5).encoding.should == Encoding::US_ASCII
+ c.send(@method, 5, 6).encoding.should == Encoding::US_ASCII
+ c.send(@method, 1, 3).encoding.should == Encoding::US_ASCII
+ c.send(@method, 8, 2).encoding.should == Encoding::US_ASCII
+ c.send(@method, 1, 10).encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns nil if the offset falls outside of self" do
+ "hello there".send(@method, 20,3).should == nil
+ "hello there".send(@method, -20,3).should == nil
+
+ "".send(@method, 1,0).should == nil
+ "".send(@method, 1,1).should == nil
+
+ "".send(@method, -1,0).should == nil
+ "".send(@method, -1,1).should == nil
+
+ "x".send(@method, 2,0).should == nil
+ "x".send(@method, 2,1).should == nil
+
+ "x".send(@method, -2,0).should == nil
+ "x".send(@method, -2,1).should == nil
+
+ "x".send(@method, fixnum_max, 1).should == nil
+ end
+
+ it "returns nil if the length is negative" do
+ "hello there".send(@method, 4,-3).should == nil
+ "hello there".send(@method, -4,-3).should == nil
+ end
+
+ platform_is pointer_size: 64 do
+ it "returns nil if the length is negative big value" do
+ "hello there".send(@method, 4, -(1 << 31)).should == nil
+
+ # by some reason length < -(1 << 31) on CI on Windows leads to
+ # 'RangeError: bignum too big to convert into `long'' error
+ platform_is_not :windows do
+ "hello there".send(@method, 4, -(1 << 63)).should == nil
+ end
+ end
+ end
+
+ it "calls to_int on the given index and the given length" do
+ "hello".send(@method, 0.5, 1).should == "h"
+ "hello".send(@method, 0.5, 2.5).should == "he"
+ "hello".send(@method, 1, 2.5).should == "el"
+
+ obj = mock('2')
+ obj.should_receive(:to_int).exactly(4).times.and_return(2)
+
+ "hello".send(@method, obj, 1).should == "l"
+ "hello".send(@method, obj, obj).should == "ll"
+ "hello".send(@method, 0, obj).should == "he"
+ end
+
+ it "raises a TypeError when idx or length can't be converted to an integer" do
+ -> { "hello".send(@method, mock('x'), 0) }.should.raise(TypeError)
+ -> { "hello".send(@method, 0, mock('x')) }.should.raise(TypeError)
+
+ # I'm deliberately including this here.
+ # It means that str.send(@method, other, idx) isn't supported.
+ -> { "hello".send(@method, "", 0) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the given index or the given length is nil" do
+ -> { "hello".send(@method, 1, nil) }.should.raise(TypeError)
+ -> { "hello".send(@method, nil, 1) }.should.raise(TypeError)
+ -> { "hello".send(@method, nil, nil) }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError if the index or length is too big" do
+ -> { "hello".send(@method, bignum_value, 1) }.should.raise(RangeError)
+ -> { "hello".send(@method, 0, bignum_value) }.should.raise(RangeError)
+ end
+
+ it "raises a RangeError if the index or length is too small" do
+ -> { "hello".send(@method, -bignum_value, 1) }.should.raise(RangeError)
+ -> { "hello".send(@method, 0, -bignum_value) }.should.raise(RangeError)
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.send(@method, 0,0).should.instance_of?(String)
+ s.send(@method, 0,4).should.instance_of?(String)
+ s.send(@method, 1,4).should.instance_of?(String)
+ end
+
+ it "handles repeated application" do
+ "hello world".send(@method, 6, 5).send(@method, 0, 1).should == 'w'
+ "hello world".send(@method, 6, 5).send(@method, 0, 5).should == 'world'
+
+ "hello world".send(@method, 6, 5).send(@method, 1, 1).should == 'o'
+ "hello world".send(@method, 6, 5).send(@method, 1, 4).should == 'orld'
+
+ "hello world".send(@method, 6, 5).send(@method, 4, 1).should == 'd'
+ "hello world".send(@method, 6, 5).send(@method, 5, 0).should == ''
+
+ "hello world".send(@method, 6, 0).send(@method, -1, 0).should == nil
+ "hello world".send(@method, 6, 0).send(@method, 1, 1).should == nil
+ end
+end
+
+describe :string_slice_range, shared: true do
+ it "returns the substring given by the offsets of the range" do
+ "hello there".send(@method, 1..1).should == "e"
+ "hello there".send(@method, 1..3).should == "ell"
+ "hello there".send(@method, 1...3).should == "el"
+ "hello there".send(@method, -4..-2).should == "her"
+ "hello there".send(@method, -4...-2).should == "he"
+ "hello there".send(@method, 5..-1).should == " there"
+ "hello there".send(@method, 5...-1).should == " ther"
+
+ "".send(@method, 0..0).should == ""
+
+ "x".send(@method, 0..0).should == "x"
+ "x".send(@method, 0..1).should == "x"
+ "x".send(@method, 0...1).should == "x"
+ "x".send(@method, 0..-1).should == "x"
+
+ "x".send(@method, 1..1).should == ""
+ "x".send(@method, 1..-1).should == ""
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello there".encode("US-ASCII").send(@method, 1..1).encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns nil if the beginning of the range falls outside of self" do
+ "hello there".send(@method, 12..-1).should == nil
+ "hello there".send(@method, 20..25).should == nil
+ "hello there".send(@method, 20..1).should == nil
+ "hello there".send(@method, -20..1).should == nil
+ "hello there".send(@method, -20..-1).should == nil
+
+ "".send(@method, -1..-1).should == nil
+ "".send(@method, -1...-1).should == nil
+ "".send(@method, -1..0).should == nil
+ "".send(@method, -1...0).should == nil
+ end
+
+ it "returns an empty string if range.begin is inside self and > real end" do
+ "hello there".send(@method, 1...1).should == ""
+ "hello there".send(@method, 4..2).should == ""
+ "hello".send(@method, 4..-4).should == ""
+ "hello there".send(@method, -5..-6).should == ""
+ "hello there".send(@method, -2..-4).should == ""
+ "hello there".send(@method, -5..-6).should == ""
+ "hello there".send(@method, -5..2).should == ""
+
+ "".send(@method, 0...0).should == ""
+ "".send(@method, 0..-1).should == ""
+ "".send(@method, 0...-1).should == ""
+
+ "x".send(@method, 0...0).should == ""
+ "x".send(@method, 0...-1).should == ""
+ "x".send(@method, 1...1).should == ""
+ "x".send(@method, 1...-1).should == ""
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.send(@method, 0...0).should.instance_of?(String)
+ s.send(@method, 0..4).should.instance_of?(String)
+ s.send(@method, 1..4).should.instance_of?(String)
+ end
+
+ it "calls to_int on range arguments" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ from.should_receive(:<=>).twice.and_return(0)
+
+ from.should_receive(:to_int).twice.and_return(1)
+ to.should_receive(:to_int).twice.and_return(-2)
+
+ "hello there".send(@method, from..to).should == "ello ther"
+ "hello there".send(@method, from...to).should == "ello the"
+ end
+
+ it "works with Range subclasses" do
+ a = "GOOD"
+ range_incl = StringSpecs::MyRange.new(1, 2)
+ range_excl = StringSpecs::MyRange.new(-3, -1, true)
+
+ a.send(@method, range_incl).should == "OO"
+ a.send(@method, range_excl).should == "OO"
+ end
+
+ it "handles repeated application" do
+ "hello world".send(@method, 6..11).send(@method, 0..0).should == 'w'
+ "hello world".send(@method, 6..11).send(@method, 0..4).should == 'world'
+
+ "hello world".send(@method, 6..11).send(@method, 1..1).should == 'o'
+ "hello world".send(@method, 6..11).send(@method, 1..4).should == 'orld'
+
+ "hello world".send(@method, 6..11).send(@method, 4..4).should == 'd'
+ "hello world".send(@method, 6..11).send(@method, 5..4).should == ''
+
+ "hello world".send(@method, 6..5).send(@method, -1..-1).should == nil
+ "hello world".send(@method, 6..5).send(@method, 1..1).should == nil
+ end
+
+ it "raises a type error if a range is passed with a length" do
+ ->{ "hello".send(@method, 1..2, 1) }.should.raise(TypeError)
+ end
+
+ it "raises a RangeError if one of the bound is too big" do
+ -> { "hello".send(@method, bignum_value..(bignum_value + 1)) }.should.raise(RangeError)
+ -> { "hello".send(@method, 0..bignum_value) }.should.raise(RangeError)
+ end
+
+ it "works with endless ranges" do
+ "hello there".send(@method, eval("(2..)")).should == "llo there"
+ "hello there".send(@method, eval("(2...)")).should == "llo there"
+ "hello there".send(@method, eval("(-4..)")).should == "here"
+ "hello there".send(@method, eval("(-4...)")).should == "here"
+ end
+
+ it "works with beginless ranges" do
+ "hello there".send(@method, (..5)).should == "hello "
+ "hello there".send(@method, (...5)).should == "hello"
+ "hello there".send(@method, (..-4)).should == "hello th"
+ "hello there".send(@method, (...-4)).should == "hello t"
+ "hello there".send(@method, (...nil)).should == "hello there"
+ end
+end
+
+describe :string_slice_regexp, shared: true do
+ it "returns the matching portion of self" do
+ "hello there".send(@method, /[aeiou](.)\1/).should == "ell"
+ "".send(@method, //).should == ""
+ end
+
+ it "returns nil if there is no match" do
+ "hello there".send(@method, /xyz/).should == nil
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello there".encode("US-ASCII").send(@method, /[aeiou](.)\1/).encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.send(@method, //).should.instance_of?(String)
+ s.send(@method, /../).should.instance_of?(String)
+ end
+
+ it "sets $~ to MatchData when there is a match and nil when there's none" do
+ 'hello'.send(@method, /./)
+ $~[0].should == 'h'
+
+ 'hello'.send(@method, /not/)
+ $~.should == nil
+ end
+end
+
+describe :string_slice_regexp_index, shared: true do
+ it "returns the capture for the given index" do
+ "hello there".send(@method, /[aeiou](.)\1/, 0).should == "ell"
+ "hello there".send(@method, /[aeiou](.)\1/, 1).should == "l"
+ "hello there".send(@method, /[aeiou](.)\1/, -1).should == "l"
+
+ "har".send(@method, /(.)(.)(.)/, 0).should == "har"
+ "har".send(@method, /(.)(.)(.)/, 1).should == "h"
+ "har".send(@method, /(.)(.)(.)/, 2).should == "a"
+ "har".send(@method, /(.)(.)(.)/, 3).should == "r"
+ "har".send(@method, /(.)(.)(.)/, -1).should == "r"
+ "har".send(@method, /(.)(.)(.)/, -2).should == "a"
+ "har".send(@method, /(.)(.)(.)/, -3).should == "h"
+ end
+
+ it "returns nil if there is no match" do
+ "hello there".send(@method, /(what?)/, 1).should == nil
+ end
+
+ it "returns nil if the index is larger than the number of captures" do
+ "hello there".send(@method, /hello (.)/, 2).should == nil
+ # You can't refer to 0 using negative indices
+ "hello there".send(@method, /hello (.)/, -2).should == nil
+ end
+
+ it "returns nil if there is no capture for the given index" do
+ "hello there".send(@method, /[aeiou](.)\1/, 2).should == nil
+ end
+
+ it "returns nil if the given capture group was not matched but still sets $~" do
+ "test".send(@method, /te(z)?/, 1).should == nil
+ $~[0].should == "te"
+ $~[1].should == nil
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello there".encode("US-ASCII").send(@method, /[aeiou](.)\1/, 0).encoding.should == Encoding::US_ASCII
+ end
+
+ it "calls to_int on the given index" do
+ obj = mock('2')
+ obj.should_receive(:to_int).and_return(2)
+
+ "har".send(@method, /(.)(.)(.)/, 1.5).should == "h"
+ "har".send(@method, /(.)(.)(.)/, obj).should == "a"
+ end
+
+ it "raises a TypeError when the given index can't be converted to Integer" do
+ -> { "hello".send(@method, /(.)(.)(.)/, mock('x')) }.should.raise(TypeError)
+ -> { "hello".send(@method, /(.)(.)(.)/, {}) }.should.raise(TypeError)
+ -> { "hello".send(@method, /(.)(.)(.)/, []) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when the given index is nil" do
+ -> { "hello".send(@method, /(.)(.)(.)/, nil) }.should.raise(TypeError)
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.send(@method, /(.)(.)/, 0).should.instance_of?(String)
+ s.send(@method, /(.)(.)/, 1).should.instance_of?(String)
+ end
+
+ it "sets $~ to MatchData when there is a match and nil when there's none" do
+ 'hello'.send(@method, /.(.)/, 0)
+ $~[0].should == 'he'
+
+ 'hello'.send(@method, /.(.)/, 1)
+ $~[1].should == 'e'
+
+ 'hello'.send(@method, /not/, 0)
+ $~.should == nil
+ end
+end
+
+describe :string_slice_string, shared: true do
+ it "returns other_str if it occurs in self" do
+ s = "lo"
+ "hello there".send(@method, s).should == s
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello'.send(@method, 'll')
+ $~.should == nil
+ end
+
+ it "returns nil if there is no match" do
+ "hello there".send(@method, "bye").should == nil
+ end
+
+ it "doesn't call to_str on its argument" do
+ o = mock('x')
+ o.should_not_receive(:to_str)
+
+ -> { "hello".send(@method, o) }.should.raise(TypeError)
+ end
+
+ it "returns a String instance when given a subclass instance" do
+ s = StringSpecs::MyString.new("el")
+ r = "hello".send(@method, s)
+ r.should == "el"
+ r.should.instance_of?(String)
+ end
+end
+
+describe :string_slice_regexp_group, shared: true do
+ not_supported_on :opal do
+ it "returns the capture for the given name" do
+ "hello there".send(@method, /(?<g>[aeiou](.))/, 'g').should == "el"
+ "hello there".send(@method, /[aeiou](?<g>.)/, 'g').should == "l"
+
+ "har".send(@method, /(?<g>(.)(.)(.))/, 'g').should == "har"
+ "har".send(@method, /(?<h>.)(.)(.)/, 'h').should == "h"
+ "har".send(@method, /(.)(?<a>.)(.)/, 'a').should == "a"
+ "har".send(@method, /(.)(.)(?<r>.)/, 'r').should == "r"
+ "har".send(@method, /(?<h>.)(?<a>.)(?<r>.)/, 'r').should == "r"
+ end
+
+ it "returns the last capture for duplicate names" do
+ "hello there".send(@method, /(?<g>h)(?<g>.)/, 'g').should == "e"
+ "hello there".send(@method, /(?<g>h)(?<g>.)(?<f>.)/, 'g').should == "e"
+ end
+
+ it "returns the innermost capture for nested duplicate names" do
+ "hello there".send(@method, /(?<g>h(?<g>.))/, 'g').should == "e"
+ end
+
+ it "returns nil if there is no match" do
+ "hello there".send(@method, /(?<whut>what?)/, 'whut').should == nil
+ end
+
+ it "raises an IndexError if there is no capture for the given name" do
+ -> do
+ "hello there".send(@method, /[aeiou](.)\1/, 'non')
+ end.should.raise(IndexError)
+ end
+
+ it "raises a TypeError when the given name is not a String" do
+ -> { "hello".send(@method, /(?<q>.)/, mock('x')) }.should.raise(TypeError)
+ -> { "hello".send(@method, /(?<q>.)/, {}) }.should.raise(TypeError)
+ -> { "hello".send(@method, /(?<q>.)/, []) }.should.raise(TypeError)
+ end
+
+ it "raises an IndexError when given the empty String as a group name" do
+ -> { "hello".send(@method, /(?<q>)/, '') }.should.raise(IndexError)
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.send(@method, /(?<q>.)/, 'q').should.instance_of?(String)
+ end
+
+ it "sets $~ to MatchData when there is a match and nil when there's none" do
+ 'hello'.send(@method, /(?<hi>.(.))/, 'hi')
+ $~[0].should == 'he'
+
+ 'hello'.send(@method, /(?<non>not)/, 'non')
+ $~.should == nil
+ end
+ end
+end
+
+describe :string_slice_symbol, shared: true do
+ it "raises TypeError" do
+ -> { 'hello'.send(@method, :hello) }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/string/shared/strip.rb b/spec/ruby/core/string/shared/strip.rb
new file mode 100644
index 0000000000..39c7232ff9
--- /dev/null
+++ b/spec/ruby/core/string/shared/strip.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :string_strip, shared: true do
+ it "returns a String in the same encoding as self" do
+ " hello ".encode("US-ASCII").send(@method).encoding.should == Encoding::US_ASCII
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new(" hello ").send(@method).should.instance_of?(String)
+ StringSpecs::MyString.new(" ").send(@method).should.instance_of?(String)
+ StringSpecs::MyString.new("").send(@method).should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/string/shared/succ.rb b/spec/ruby/core/string/shared/succ.rb
new file mode 100644
index 0000000000..8f1d327741
--- /dev/null
+++ b/spec/ruby/core/string/shared/succ.rb
@@ -0,0 +1,87 @@
+# encoding: binary
+describe :string_succ, shared: true do
+ it "returns an empty string for empty strings" do
+ "".send(@method).should == ""
+ end
+
+ it "returns the successor by increasing the rightmost alphanumeric (digit => digit, letter => letter with same case)" do
+ "abcd".send(@method).should == "abce"
+ "THX1138".send(@method).should == "THX1139"
+
+ "<<koala>>".send(@method).should == "<<koalb>>"
+ "==A??".send(@method).should == "==B??"
+ end
+
+ it "increases non-alphanumerics (via ascii rules) if there are no alphanumerics" do
+ "***".send(@method).should == "**+"
+ "**`".send(@method).should == "**a"
+ end
+
+ it "increases the next best alphanumeric (jumping over non-alphanumerics) if there is a carry" do
+ "dz".send(@method).should == "ea"
+ "HZ".send(@method).should == "IA"
+ "49".send(@method).should == "50"
+
+ "izz".send(@method).should == "jaa"
+ "IZZ".send(@method).should == "JAA"
+ "699".send(@method).should == "700"
+
+ "6Z99z99Z".send(@method).should == "7A00a00A"
+
+ "1999zzz".send(@method).should == "2000aaa"
+ "NZ/[]ZZZ9999".send(@method).should == "OA/[]AAA0000"
+ end
+
+ it "increases the next best character if there is a carry for non-alphanumerics" do
+ "(\xFF".send(@method).should == ")\x00"
+ "`\xFF".send(@method).should == "a\x00"
+ "<\xFF\xFF".send(@method).should == "=\x00\x00"
+ end
+
+ it "adds an additional character (just left to the last increased one) if there is a carry and no character left to increase" do
+ "z".send(@method).should == "aa"
+ "Z".send(@method).should == "AA"
+ "9".send(@method).should == "10"
+
+ "zz".send(@method).should == "aaa"
+ "ZZ".send(@method).should == "AAA"
+ "99".send(@method).should == "100"
+
+ "9Z99z99Z".send(@method).should == "10A00a00A"
+
+ "ZZZ9999".send(@method).should == "AAAA0000"
+ "/[]9999".send(@method).should == "/[]10000"
+ "/[]ZZZ9999".send(@method).should == "/[]AAAA0000"
+ "Z/[]ZZZ9999".send(@method).should == "AA/[]AAA0000"
+
+ # non-alphanumeric cases
+ "\xFF".send(@method).should == "\x01\x00"
+ "\xFF\xFF".send(@method).should == "\x01\x00\x00"
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("").send(@method).should.instance_of?(String)
+ StringSpecs::MyString.new("a").send(@method).should.instance_of?(String)
+ StringSpecs::MyString.new("z").send(@method).should.instance_of?(String)
+ end
+
+ it "returns a String in the same encoding as self" do
+ "z".encode("US-ASCII").send(@method).encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe :string_succ_bang, shared: true do
+ it "is equivalent to succ, but modifies self in place (still returns self)" do
+ ["", "abcd", "THX1138"].each do |s|
+ s = +s
+ r = s.dup.send(@method)
+ s.send(@method).should.equal?(s)
+ s.should == r
+ end
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ -> { "".freeze.send(@method) }.should.raise(FrozenError)
+ -> { "abcd".freeze.send(@method) }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/shared/to_s.rb b/spec/ruby/core/string/shared/to_s.rb
new file mode 100644
index 0000000000..96c59470d6
--- /dev/null
+++ b/spec/ruby/core/string/shared/to_s.rb
@@ -0,0 +1,13 @@
+describe :string_to_s, shared: true do
+ it "returns self when self.class == String" do
+ a = "a string"
+ a.should.equal?(a.send(@method))
+ end
+
+ it "returns a new instance of String when called on a subclass" do
+ a = StringSpecs::MyString.new("a string")
+ s = a.send(@method)
+ s.should == "a string"
+ s.should.instance_of?(String)
+ end
+end
diff --git a/spec/ruby/core/string/shared/to_sym.rb b/spec/ruby/core/string/shared/to_sym.rb
new file mode 100644
index 0000000000..2a8a2e3182
--- /dev/null
+++ b/spec/ruby/core/string/shared/to_sym.rb
@@ -0,0 +1,72 @@
+describe :string_to_sym, shared: true do
+ it "returns the symbol corresponding to self" do
+ "Koala".send(@method).should.equal? :Koala
+ 'cat'.send(@method).should.equal? :cat
+ '@cat'.send(@method).should.equal? :@cat
+ 'cat and dog'.send(@method).should.equal? :"cat and dog"
+ "abc=".send(@method).should.equal? :abc=
+ end
+
+ it "does not special case +(binary) and -(binary)" do
+ "+(binary)".send(@method).should.equal? :"+(binary)"
+ "-(binary)".send(@method).should.equal? :"-(binary)"
+ end
+
+ it "does not special case certain operators" do
+ "!@".send(@method).should.equal? :"!@"
+ "~@".send(@method).should.equal? :"~@"
+ "!(unary)".send(@method).should.equal? :"!(unary)"
+ "~(unary)".send(@method).should.equal? :"~(unary)"
+ "+(unary)".send(@method).should.equal? :"+(unary)"
+ "-(unary)".send(@method).should.equal? :"-(unary)"
+ end
+
+ it "returns a US-ASCII Symbol for a UTF-8 String containing only US-ASCII characters" do
+ sym = "foobar".send(@method)
+ sym.encoding.should == Encoding::US_ASCII
+ sym.should.equal? :"foobar"
+ end
+
+ it "returns a US-ASCII Symbol for a binary String containing only US-ASCII characters" do
+ sym = "foobar".b.send(@method)
+ sym.encoding.should == Encoding::US_ASCII
+ sym.should.equal? :"foobar"
+ end
+
+ it "returns a UTF-8 Symbol for a UTF-8 String containing non US-ASCII characters" do
+ sym = "il était une fois".send(@method)
+ sym.encoding.should == Encoding::UTF_8
+ sym.should.equal? :"il était une #{'fois'}"
+ end
+
+ it "returns a UTF-16LE Symbol for a UTF-16LE String containing non US-ASCII characters" do
+ utf16_str = "UtéF16".encode(Encoding::UTF_16LE)
+ sym = utf16_str.send(@method)
+ sym.encoding.should == Encoding::UTF_16LE
+ sym.to_s.should == utf16_str
+ end
+
+ it "returns a binary Symbol for a binary String containing non US-ASCII characters" do
+ binary_string = "binarí".b
+ sym = binary_string.send(@method)
+ sym.encoding.should == Encoding::BINARY
+ sym.to_s.should == binary_string
+ end
+
+ it "ignores existing symbols with different encoding" do
+ source = "fée"
+
+ iso_symbol = source.dup.force_encoding(Encoding::ISO_8859_1).send(@method)
+ iso_symbol.encoding.should == Encoding::ISO_8859_1
+ binary_symbol = source.dup.force_encoding(Encoding::BINARY).send(@method)
+ binary_symbol.encoding.should == Encoding::BINARY
+ end
+
+ it "raises an EncodingError for UTF-8 String containing invalid bytes" do
+ invalid_utf8 = "\xC3"
+ invalid_utf8.should_not.valid_encoding?
+ -> {
+ invalid_utf8.send(@method)
+ }.should.raise(EncodingError, 'invalid symbol in encoding UTF-8 :"\xC3"')
+ end
+end
diff --git a/spec/ruby/core/string/size_spec.rb b/spec/ruby/core/string/size_spec.rb
new file mode 100644
index 0000000000..9e1f40c5ae
--- /dev/null
+++ b/spec/ruby/core/string/size_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/length'
+
+describe "String#size" do
+ it_behaves_like :string_length, :size
+end
diff --git a/spec/ruby/core/string/slice_spec.rb b/spec/ruby/core/string/slice_spec.rb
new file mode 100644
index 0000000000..14e2251b3f
--- /dev/null
+++ b/spec/ruby/core/string/slice_spec.rb
@@ -0,0 +1,390 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/slice'
+
+describe "String#slice" do
+ it_behaves_like :string_slice, :slice
+end
+
+describe "String#slice with index, length" do
+ it_behaves_like :string_slice_index_length, :slice
+end
+
+describe "String#slice with Range" do
+ it_behaves_like :string_slice_range, :slice
+end
+
+describe "String#slice with Regexp" do
+ it_behaves_like :string_slice_regexp, :slice
+end
+
+describe "String#slice with Regexp, index" do
+ it_behaves_like :string_slice_regexp_index, :slice
+end
+
+describe "String#slice with Regexp, group" do
+ it_behaves_like :string_slice_regexp_group, :slice
+end
+
+describe "String#slice with String" do
+ it_behaves_like :string_slice_string, :slice
+end
+
+describe "String#slice with Symbol" do
+ it_behaves_like :string_slice_symbol, :slice
+end
+
+describe "String#slice! with index" do
+ it "deletes and return the char at the given position" do
+ a = "hello"
+ a.slice!(1).should == ?e
+ a.should == "hllo"
+ a.slice!(-1).should == ?o
+ a.should == "hll"
+ end
+
+ it "returns nil if idx is outside of self" do
+ a = "hello"
+ a.slice!(20).should == nil
+ a.should == "hello"
+ a.slice!(-20).should == nil
+ a.should == "hello"
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ -> { "hello".freeze.slice!(1) }.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(10) }.should.raise(FrozenError)
+ -> { "".freeze.slice!(0) }.should.raise(FrozenError)
+ end
+
+ it "calls to_int on index" do
+ "hello".slice!(0.5).should == ?h
+
+ obj = mock('1')
+ obj.should_receive(:to_int).at_least(1).and_return(1)
+ "hello".slice!(obj).should == ?e
+
+ obj = mock('1')
+ obj.should_receive(:respond_to?).at_least(1).with(:to_int, true).and_return(true)
+ obj.should_receive(:method_missing).at_least(1).with(:to_int).and_return(1)
+ "hello".slice!(obj).should == ?e
+ end
+
+
+ it "returns the character given by the character index" do
+ "hellö there".slice!(1).should == "e"
+ "hellö there".slice!(4).should == "ö"
+ "hellö there".slice!(6).should == "t"
+ end
+
+end
+
+describe "String#slice! with index, length" do
+ it "deletes and returns the substring at idx and the given length" do
+ a = "hello"
+ a.slice!(1, 2).should == "el"
+ a.should == "hlo"
+
+ a.slice!(1, 0).should == ""
+ a.should == "hlo"
+
+ a.slice!(-2, 4).should == "lo"
+ a.should == "h"
+ end
+
+ it "returns nil if the given position is out of self" do
+ a = "hello"
+ a.slice(10, 3).should == nil
+ a.should == "hello"
+
+ a.slice(-10, 20).should == nil
+ a.should == "hello"
+ end
+
+ it "returns nil if the length is negative" do
+ a = "hello"
+ a.slice(4, -3).should == nil
+ a.should == "hello"
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ -> { "hello".freeze.slice!(1, 2) }.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(10, 3) }.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(-10, 3)}.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(4, -3) }.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(10, 3) }.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(-10, 3)}.should.raise(FrozenError)
+ -> { "hello".freeze.slice!(4, -3) }.should.raise(FrozenError)
+ end
+
+ it "calls to_int on idx and length" do
+ "hello".slice!(0.5, 2.5).should == "he"
+
+ obj = mock('2')
+ def obj.to_int() 2 end
+ "hello".slice!(obj, obj).should == "ll"
+
+ obj = mock('2')
+ def obj.respond_to?(name, *) name == :to_int; end
+ def obj.method_missing(name, *) name == :to_int ? 2 : super; end
+ "hello".slice!(obj, obj).should == "ll"
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.slice!(0, 0).should.instance_of?(String)
+ s.slice!(0, 4).should.instance_of?(String)
+ end
+
+ it "returns the substring given by the character offsets" do
+ "hellö there".slice!(1,0).should == ""
+ "hellö there".slice!(1,3).should == "ell"
+ "hellö there".slice!(1,6).should == "ellö t"
+ "hellö there".slice!(1,9).should == "ellö ther"
+ end
+
+ it "treats invalid bytes as single bytes" do
+ xE6xCB = [0xE6,0xCB].pack('CC').force_encoding('utf-8')
+ "a#{xE6xCB}b".slice!(1, 2).should == xE6xCB
+ end
+end
+
+describe "String#slice! Range" do
+ it "deletes and return the substring given by the offsets of the range" do
+ a = "hello"
+ a.slice!(1..3).should == "ell"
+ a.should == "ho"
+ a.slice!(0..0).should == "h"
+ a.should == "o"
+ a.slice!(0...0).should == ""
+ a.should == "o"
+
+ # Edge Case?
+ "hello".slice!(-3..-9).should == ""
+ end
+
+ it "returns nil if the given range is out of self" do
+ a = "hello"
+ a.slice!(-6..-9).should == nil
+ a.should == "hello"
+
+ b = "hello"
+ b.slice!(10..20).should == nil
+ b.should == "hello"
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.slice!(0...0).should.instance_of?(String)
+ s.slice!(0..4).should.instance_of?(String)
+ end
+
+ it "calls to_int on range arguments" do
+ from = mock('from')
+ to = mock('to')
+
+ # So we can construct a range out of them...
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ def from.to_int() 1 end
+ def to.to_int() -2 end
+
+ "hello there".slice!(from..to).should == "ello ther"
+
+ from = mock('from')
+ to = mock('to')
+
+ def from.<=>(o) 0 end
+ def to.<=>(o) 0 end
+
+ def from.respond_to?(name, *) name == :to_int; end
+ def from.method_missing(name) name == :to_int ? 1 : super; end
+ def to.respond_to?(name, *) name == :to_int; end
+ def to.method_missing(name) name == :to_int ? -2 : super; end
+
+ "hello there".slice!(from..to).should == "ello ther"
+ end
+
+ it "works with Range subclasses" do
+ a = "GOOD"
+ range_incl = StringSpecs::MyRange.new(1, 2)
+
+ a.slice!(range_incl).should == "OO"
+ end
+
+
+ it "returns the substring given by the character offsets of the range" do
+ "hellö there".slice!(1..1).should == "e"
+ "hellö there".slice!(1..3).should == "ell"
+ "hellö there".slice!(1...3).should == "el"
+ "hellö there".slice!(-4..-2).should == "her"
+ "hellö there".slice!(-4...-2).should == "he"
+ "hellö there".slice!(5..-1).should == " there"
+ "hellö there".slice!(5...-1).should == " ther"
+ end
+
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { "hello".freeze.slice!(1..3) }.should.raise(FrozenError)
+ end
+
+ # see redmine #1551
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> { "hello".freeze.slice!(10..20)}.should.raise(FrozenError)
+ end
+end
+
+describe "String#slice! with Regexp" do
+ it "deletes and returns the first match from self" do
+ s = "this is a string"
+ s.slice!(/s.*t/).should == 's is a st'
+ s.should == 'thiring'
+
+ c = "hello hello"
+ c.slice!(/llo/).should == "llo"
+ c.should == "he hello"
+ end
+
+ it "returns nil if there was no match" do
+ s = "this is a string"
+ s.slice!(/zzz/).should == nil
+ s.should == "this is a string"
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.slice!(//).should.instance_of?(String)
+ s.slice!(/../).should.instance_of?(String)
+ end
+
+ it "returns the matching portion of self with a multi byte character" do
+ "hëllo there".slice!(/[ë](.)\1/).should == "ëll"
+ "".slice!(//).should == ""
+ end
+
+ it "sets $~ to MatchData when there is a match and nil when there's none" do
+ 'hello'.slice!(/./)
+ $~[0].should == 'h'
+
+ 'hello'.slice!(/not/)
+ $~.should == nil
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { "this is a string".freeze.slice!(/s.*t/) }.should.raise(FrozenError)
+ end
+
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> { "this is a string".freeze.slice!(/zzz/) }.should.raise(FrozenError)
+ end
+end
+
+describe "String#slice! with Regexp, index" do
+ it "deletes and returns the capture for idx from self" do
+ str = "hello there"
+ str.slice!(/[aeiou](.)\1/, 0).should == "ell"
+ str.should == "ho there"
+ str.slice!(/(t)h/, 1).should == "t"
+ str.should == "ho here"
+ end
+
+ it "returns nil if there was no match" do
+ s = "this is a string"
+ s.slice!(/x(zzz)/, 1).should == nil
+ s.should == "this is a string"
+ end
+
+ it "returns nil if there is no capture for idx" do
+ "hello there".slice!(/[aeiou](.)\1/, 2).should == nil
+ # You can't refer to 0 using negative indices
+ "hello there".slice!(/[aeiou](.)\1/, -2).should == nil
+ end
+
+ it "accepts a Float for capture index" do
+ "har".slice!(/(.)(.)(.)/, 1.5).should == "h"
+ end
+
+ it "calls #to_int to convert an Object to capture index" do
+ obj = mock('2')
+ obj.should_receive(:to_int).at_least(1).times.and_return(2)
+
+ "har".slice!(/(.)(.)(.)/, obj).should == "a"
+ end
+
+ it "returns String instances" do
+ s = StringSpecs::MyString.new("hello")
+ s.slice!(/(.)(.)/, 0).should.instance_of?(String)
+ s.slice!(/(.)(.)/, 1).should.instance_of?(String)
+ end
+
+ it "returns the encoding aware capture for the given index" do
+ "hår".slice!(/(.)(.)(.)/, 0).should == "hår"
+ "hår".slice!(/(.)(.)(.)/, 1).should == "h"
+ "hår".slice!(/(.)(.)(.)/, 2).should == "å"
+ "hår".slice!(/(.)(.)(.)/, 3).should == "r"
+ "hår".slice!(/(.)(.)(.)/, -1).should == "r"
+ "hår".slice!(/(.)(.)(.)/, -2).should == "å"
+ "hår".slice!(/(.)(.)(.)/, -3).should == "h"
+ end
+
+ it "sets $~ to MatchData when there is a match and nil when there's none" do
+ 'hello'[/.(.)/, 0]
+ $~[0].should == 'he'
+
+ 'hello'[/.(.)/, 1]
+ $~[1].should == 'e'
+
+ 'hello'[/not/, 0]
+ $~.should == nil
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ -> { "this is a string".freeze.slice!(/s.*t/) }.should.raise(FrozenError)
+ -> { "this is a string".freeze.slice!(/zzz/, 0)}.should.raise(FrozenError)
+ -> { "this is a string".freeze.slice!(/(.)/, 2)}.should.raise(FrozenError)
+ end
+end
+
+describe "String#slice! with String" do
+ it "removes and returns the first occurrence of other_str from self" do
+ c = "hello hello"
+ c.slice!('llo').should == "llo"
+ c.should == "he hello"
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+
+ 'hello'.slice!('ll')
+ $~.should == nil
+ end
+
+ it "returns nil if self does not contain other" do
+ a = "hello"
+ a.slice!('zzz').should == nil
+ a.should == "hello"
+ end
+
+ it "doesn't call to_str on its argument" do
+ o = mock('x')
+ o.should_not_receive(:to_str)
+
+ -> { "hello".slice!(o) }.should.raise(TypeError)
+ end
+
+ it "returns a subclass instance when given a subclass instance" do
+ s = StringSpecs::MyString.new("el")
+ r = "hello".slice!(s)
+ r.should == "el"
+ r.should.instance_of?(String)
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ -> { "hello hello".freeze.slice!('llo') }.should.raise(FrozenError)
+ -> { "this is a string".freeze.slice!('zzz')}.should.raise(FrozenError)
+ -> { "this is a string".freeze.slice!('zzz')}.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/split_spec.rb b/spec/ruby/core/string/split_spec.rb
new file mode 100644
index 0000000000..6e8c1c6219
--- /dev/null
+++ b/spec/ruby/core/string/split_spec.rb
@@ -0,0 +1,546 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#split with String" do
+ it "throws an ArgumentError if the string is not a valid" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+
+ -> { s.split }.should.raise(ArgumentError)
+ -> { s.split(':') }.should.raise(ArgumentError)
+ end
+
+ it "throws an ArgumentError if the pattern is not a valid string" do
+ str = 'проверка'
+ broken_str = "\xDF".dup.force_encoding(Encoding::UTF_8)
+
+ -> { str.split(broken_str) }.should.raise(ArgumentError)
+ end
+
+ it "splits on multibyte characters" do
+ "ã‚りãŒã‚ŠãŒã¨ã†".split("ãŒ").should == ["ã‚り", "り", "ã¨ã†"]
+ end
+
+ it "returns an array of substrings based on splitting on the given string" do
+ "mellow yellow".split("ello").should == ["m", "w y", "w"]
+ end
+
+ it "suppresses trailing empty fields when limit isn't given or 0" do
+ "1,2,,3,4,,".split(',').should == ["1", "2", "", "3", "4"]
+ "1,2,,3,4,,".split(',', 0).should == ["1", "2", "", "3", "4"]
+ " a b c\nd ".split(" ").should == ["", "a", "b", "c\nd"]
+ " a ã‚ c\nd ".split(" ").should == ["", "a", "ã‚", "c\nd"]
+ "hai".split("hai").should == []
+ ",".split(",").should == []
+ ",".split(",", 0).should == []
+ "ã‚".split("ã‚").should == []
+ "ã‚".split("ã‚", 0).should == []
+ end
+
+ it "does not suppress trailing empty fields when a positive limit is given" do
+ " 1 2 ".split(" ", 2).should == ["1", "2 "]
+ " 1 2 ".split(" ", 3).should == ["1", "2", ""]
+ " 1 2 ".split(" ", 4).should == ["1", "2", ""]
+ " 1 ã‚ ".split(" ", 2).should == ["1", "ã‚ "]
+ " 1 ã‚ ".split(" ", 3).should == ["1", "ã‚", ""]
+ " 1 ã‚ ".split(" ", 4).should == ["1", "ã‚", ""]
+
+ "1,2,".split(',', 2).should == ["1", "2,"]
+ "1,2,".split(',', 3).should == ["1", "2", ""]
+ "1,2,".split(',', 4).should == ["1", "2", ""]
+ "1,ã‚,".split(',', 2).should == ["1", "ã‚,"]
+ "1,ã‚,".split(',', 3).should == ["1", "ã‚", ""]
+ "1,ã‚,".split(',', 4).should == ["1", "ã‚", ""]
+
+ "1 2 ".split(/ /, 2).should == ["1", "2 "]
+ "1 2 ".split(/ /, 3).should == ["1", "2", ""]
+ "1 2 ".split(/ /, 4).should == ["1", "2", ""]
+ "1 ã‚ ".split(/ /, 2).should == ["1", "ã‚ "]
+ "1 ã‚ ".split(/ /, 3).should == ["1", "ã‚", ""]
+ "1 ã‚ ".split(/ /, 4).should == ["1", "ã‚", ""]
+ end
+
+ it "returns an array with one entry if limit is 1: the original string" do
+ "hai".split("hai", 1).should == ["hai"]
+ "x.y.z".split(".", 1).should == ["x.y.z"]
+ "hello world ".split(" ", 1).should == ["hello world "]
+ "hi!".split("", 1).should == ["hi!"]
+ end
+
+ it "returns at most limit fields when limit > 1" do
+ "hai".split("hai", 2).should == ["", ""]
+
+ "1,2".split(",", 3).should == ["1", "2"]
+
+ "1,2,,3,4,,".split(',', 2).should == ["1", "2,,3,4,,"]
+ "1,2,,3,4,,".split(',', 3).should == ["1", "2", ",3,4,,"]
+ "1,2,,3,4,,".split(',', 4).should == ["1", "2", "", "3,4,,"]
+ "1,2,,3,4,,".split(',', 5).should == ["1", "2", "", "3", "4,,"]
+ "1,2,,3,4,,".split(',', 6).should == ["1", "2", "", "3", "4", ","]
+
+ "x".split('x', 2).should == ["", ""]
+ "xx".split('x', 2).should == ["", "x"]
+ "xx".split('x', 3).should == ["", "", ""]
+ "xxx".split('x', 2).should == ["", "xx"]
+ "xxx".split('x', 3).should == ["", "", "x"]
+ "xxx".split('x', 4).should == ["", "", "", ""]
+ end
+
+ it "doesn't suppress or limit fields when limit is negative" do
+ "1,2,,3,4,,".split(',', -1).should == ["1", "2", "", "3", "4", "", ""]
+ "1,2,,3,4,,".split(',', -5).should == ["1", "2", "", "3", "4", "", ""]
+ " a b c\nd ".split(" ", -1).should == ["", "a", "b", "c\nd", ""]
+ ",".split(",", -1).should == ["", ""]
+ end
+
+ it "raises a RangeError when the limit is larger than int" do
+ -> { "a,b".split(" ", 2147483649) }.should.raise(RangeError)
+ end
+
+ it "defaults to $; when string isn't given or nil" do
+ suppress_warning do
+ old_fs = $;
+ begin
+ [",", ":", "", "XY", nil].each do |fs|
+ $; = fs
+
+ ["x,y,z,,,", "1:2:", "aXYbXYcXY", ""].each do |str|
+ expected = str.split(fs || " ")
+
+ str.split(nil).should == expected
+ str.split.should == expected
+
+ str.split(nil, -1).should == str.split(fs || " ", -1)
+ str.split(nil, 0).should == str.split(fs || " ", 0)
+ str.split(nil, 2).should == str.split(fs || " ", 2)
+ end
+ end
+ ensure
+ $; = old_fs
+ end
+ end
+ end
+
+ context "when $; is not nil" do
+ before do
+ suppress_warning do
+ @old_value, $; = $;, 'foobar'
+ end
+ end
+
+ after do
+ $; = @old_value
+ end
+
+ it "warns" do
+ -> { "".split }.should complain(/warning: \$; is set to non-nil value/)
+ end
+ end
+
+ it "ignores leading and continuous whitespace when string is a single space" do
+ " now's the time ".split(' ').should == ["now's", "the", "time"]
+ " now's the time ".split(' ', -1).should == ["now's", "the", "time", ""]
+ " now's the time ".split(' ', 3).should == ["now's", "the", "time "]
+
+ "\t\n a\t\tb \n\r\r\nc\v\vd\v ".split(' ').should == ["a", "b", "c", "d"]
+ "a\x00a b".split(' ').should == ["a\x00a", "b"]
+ end
+
+ describe "when limit is zero" do
+ it "ignores leading and continuous whitespace when string is a single space" do
+ " now's the time ".split(' ', 0).should == ["now's", "the", "time"]
+ end
+ end
+
+ it "splits between characters when its argument is an empty string" do
+ "hi!".split("").should == ["h", "i", "!"]
+ "hi!".split("", -1).should == ["h", "i", "!", ""]
+ "hi!".split("", 0).should == ["h", "i", "!"]
+ "hi!".split("", 1).should == ["hi!"]
+ "hi!".split("", 2).should == ["h", "i!"]
+ "hi!".split("", 3).should == ["h", "i", "!"]
+ "hi!".split("", 4).should == ["h", "i", "!", ""]
+ "hi!".split("", 5).should == ["h", "i", "!", ""]
+ end
+
+ it "tries converting its pattern argument to a string via to_str" do
+ obj = mock('::')
+ obj.should_receive(:to_str).and_return("::")
+
+ "hello::world".split(obj).should == ["hello", "world"]
+ end
+
+ it "tries converting limit to an integer via to_int" do
+ obj = mock('2')
+ obj.should_receive(:to_int).and_return(2)
+
+ "1.2.3.4".split(".", obj).should == ["1", "2.3.4"]
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+ "x.y.z".split(".")
+ $~.should == nil
+ end
+
+ it "returns the original string if no matches are found" do
+ "foo".split("bar").should == ["foo"]
+ "foo".split("bar", -1).should == ["foo"]
+ "foo".split("bar", 0).should == ["foo"]
+ "foo".split("bar", 1).should == ["foo"]
+ "foo".split("bar", 2).should == ["foo"]
+ "foo".split("bar", 3).should == ["foo"]
+ end
+
+ it "returns String instances based on self" do
+ ["", "x.y.z.", " x y "].each do |str|
+ ["", ".", " "].each do |pat|
+ [-1, 0, 1, 2].each do |limit|
+ StringSpecs::MyString.new(str).split(pat, limit).each do |x|
+ x.should.instance_of?(String)
+ end
+
+ str.split(StringSpecs::MyString.new(pat), limit).each do |x|
+ x.should.instance_of?(String)
+ end
+ end
+ end
+ end
+ end
+
+ it "returns an empty array when whitespace is split on whitespace" do
+ " ".split(" ").should == []
+ " \n ".split(" ").should == []
+ " ".split(" ").should == []
+ " \t ".split(" ").should == []
+ end
+
+ it "doesn't split on non-ascii whitespace" do
+ "a\u{2008}b".split(" ").should == ["a\u{2008}b"]
+ end
+
+ it "returns Strings in the same encoding as self" do
+ strings = "hello world".encode("US-ASCII").split(" ")
+
+ strings[0].encoding.should == Encoding::US_ASCII
+ strings[1].encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#split with Regexp" do
+ it "throws an ArgumentError if the string is not a valid" do
+ s = "\xDF".dup.force_encoding(Encoding::UTF_8)
+
+ -> { s.split(/./) }.should.raise(ArgumentError)
+ end
+
+ it "divides self on regexp matches" do
+ " now's the time".split(/ /).should == ["", "now's", "", "the", "time"]
+ " x\ny ".split(/ /).should == ["", "x\ny"]
+ "1, 2.34,56, 7".split(/,\s*/).should == ["1", "2.34", "56", "7"]
+ "1x2X3".split(/x/i).should == ["1", "2", "3"]
+ end
+
+ it "treats negative limits as no limit" do
+ "".split(%r!/+!, -1).should == []
+ end
+
+ it "suppresses trailing empty fields when limit isn't given or 0" do
+ "1,2,,3,4,,".split(/,/).should == ["1", "2", "", "3", "4"]
+ "1,2,,3,4,,".split(/,/, 0).should == ["1", "2", "", "3", "4"]
+ " a b c\nd ".split(/\s+/).should == ["", "a", "b", "c", "d"]
+ "hai".split(/hai/).should == []
+ ",".split(/,/).should == []
+ ",".split(/,/, 0).should == []
+ end
+
+ it "returns an array with one entry if limit is 1: the original string" do
+ "hai".split(/hai/, 1).should == ["hai"]
+ "xAyBzC".split(/[A-Z]/, 1).should == ["xAyBzC"]
+ "hello world ".split(/\s+/, 1).should == ["hello world "]
+ "hi!".split(//, 1).should == ["hi!"]
+ end
+
+ it "returns at most limit fields when limit > 1" do
+ "hai".split(/hai/, 2).should == ["", ""]
+
+ "1,2".split(/,/, 3).should == ["1", "2"]
+
+ "1,2,,3,4,,".split(/,/, 2).should == ["1", "2,,3,4,,"]
+ "1,2,,3,4,,".split(/,/, 3).should == ["1", "2", ",3,4,,"]
+ "1,2,,3,4,,".split(/,/, 4).should == ["1", "2", "", "3,4,,"]
+ "1,2,,3,4,,".split(/,/, 5).should == ["1", "2", "", "3", "4,,"]
+ "1,2,,3,4,,".split(/,/, 6).should == ["1", "2", "", "3", "4", ","]
+
+ "x".split(/x/, 2).should == ["", ""]
+ "xx".split(/x/, 2).should == ["", "x"]
+ "xx".split(/x/, 3).should == ["", "", ""]
+ "xxx".split(/x/, 2).should == ["", "xx"]
+ "xxx".split(/x/, 3).should == ["", "", "x"]
+ "xxx".split(/x/, 4).should == ["", "", "", ""]
+ end
+
+ it "doesn't suppress or limit fields when limit is negative" do
+ "1,2,,3,4,,".split(/,/, -1).should == ["1", "2", "", "3", "4", "", ""]
+ "1,2,,3,4,,".split(/,/, -5).should == ["1", "2", "", "3", "4", "", ""]
+ " a b c\nd ".split(/\s+/, -1).should == ["", "a", "b", "c", "d", ""]
+ ",".split(/,/, -1).should == ["", ""]
+ end
+
+ it "defaults to $; when regexp isn't given or nil" do
+ suppress_warning do
+ old_fs = $;
+ begin
+ [/,/, /:/, //, /XY/, /./].each do |fs|
+ $; = fs
+
+ ["x,y,z,,,", "1:2:", "aXYbXYcXY", ""].each do |str|
+ expected = str.split(fs)
+
+ str.split(nil).should == expected
+ str.split.should == expected
+
+ str.split(nil, -1).should == str.split(fs, -1)
+ str.split(nil, 0).should == str.split(fs, 0)
+ str.split(nil, 2).should == str.split(fs, 2)
+ end
+ end
+ ensure
+ $; = old_fs
+ end
+ end
+ end
+
+ it "splits between characters when regexp matches a zero-length string" do
+ "hello".split(//).should == ["h", "e", "l", "l", "o"]
+ "hello".split(//, -1).should == ["h", "e", "l", "l", "o", ""]
+ "hello".split(//, 0).should == ["h", "e", "l", "l", "o"]
+ "hello".split(//, 1).should == ["hello"]
+ "hello".split(//, 2).should == ["h", "ello"]
+ "hello".split(//, 5).should == ["h", "e", "l", "l", "o"]
+ "hello".split(//, 6).should == ["h", "e", "l", "l", "o", ""]
+ "hello".split(//, 7).should == ["h", "e", "l", "l", "o", ""]
+
+ "hi mom".split(/\s*/).should == ["h", "i", "m", "o", "m"]
+
+ "AABCCBAA".split(/(?=B)/).should == ["AA", "BCC", "BAA"]
+ "AABCCBAA".split(/(?=B)/, -1).should == ["AA", "BCC", "BAA"]
+ "AABCCBAA".split(/(?=B)/, 2).should == ["AA", "BCCBAA"]
+ end
+
+ it "respects unicode when splitting between characters" do
+ str = "ã“ã«ã¡ã‚"
+ reg = %r!!
+ ary = str.split(reg)
+ ary.size.should == 4
+ ary.should == ["ã“", "ã«", "ã¡", "ã‚"]
+ end
+
+ it "respects the encoding of the regexp when splitting between characters" do
+ str = "\303\202"
+ ary = str.split(//u)
+ ary.size.should == 1
+ ary.should == ["\303\202"]
+ end
+
+ it "includes all captures in the result array" do
+ "hello".split(/(el)/).should == ["h", "el", "lo"]
+ "hi!".split(/()/).should == ["h", "", "i", "", "!"]
+ "hi!".split(/()/, -1).should == ["h", "", "i", "", "!", "", ""]
+ "hello".split(/((el))()/).should == ["h", "el", "el", "", "lo"]
+ "AabB".split(/([a-z])+/).should == ["A", "b", "B"]
+ end
+
+ it "applies the limit to the number of split substrings, without counting captures" do
+ "aBaBa".split(/(B)()()/, 2).should == ["a", "B", "", "", "aBa"]
+ end
+
+ it "does not include non-matching captures in the result array" do
+ "hello".split(/(el)|(xx)/).should == ["h", "el", "lo"]
+ end
+
+ it "tries converting limit to an integer via to_int" do
+ obj = mock('2')
+ obj.should_receive(:to_int).and_return(2)
+
+ "1.2.3.4".split(".", obj).should == ["1", "2.3.4"]
+ end
+
+ it "returns a type error if limit can't be converted to an integer" do
+ -> {"1.2.3.4".split(".", "three")}.should.raise(TypeError)
+ -> {"1.2.3.4".split(".", nil) }.should.raise(TypeError)
+ end
+
+ it "doesn't set $~" do
+ $~ = nil
+ "x:y:z".split(/:/)
+ $~.should == nil
+ end
+
+ it "returns the original string if no matches are found" do
+ "foo".split(/bar/).should == ["foo"]
+ "foo".split(/bar/, -1).should == ["foo"]
+ "foo".split(/bar/, 0).should == ["foo"]
+ "foo".split(/bar/, 1).should == ["foo"]
+ "foo".split(/bar/, 2).should == ["foo"]
+ "foo".split(/bar/, 3).should == ["foo"]
+ end
+
+ it "returns String instances based on self" do
+ ["", "x:y:z:", " x y "].each do |str|
+ [//, /:/, /\s+/].each do |pat|
+ [-1, 0, 1, 2].each do |limit|
+ StringSpecs::MyString.new(str).split(pat, limit).each do |x|
+ x.should.instance_of?(String)
+ end
+ end
+ end
+ end
+ end
+
+ it "returns Strings in the same encoding as self" do
+ ary = "а б в".split
+ encodings = ary.map { |s| s.encoding }
+ encodings.should == [Encoding::UTF_8, Encoding::UTF_8, Encoding::UTF_8]
+ end
+
+ it "splits a string on each character for a multibyte encoding and empty split" do
+ "That's why efficiency could not be helped".split("").size.should == 39
+ end
+
+ it "returns an ArgumentError if an invalid UTF-8 string is supplied" do
+ broken_str = +'проверка' # in russian, means "test"
+ broken_str.force_encoding('binary')
+ broken_str.chop!
+ broken_str.force_encoding('utf-8')
+ ->{ broken_str.split(/\r\n|\r|\n/) }.should.raise(ArgumentError)
+ end
+
+ # See https://bugs.ruby-lang.org/issues/12689 and https://github.com/jruby/jruby/issues/4868
+ it "allows concurrent Regexp calls in a shared context" do
+ str = 'a,b,c,d,e'
+
+ p = proc { str.split(/,/) }
+ results = 10.times.map { Thread.new { x = nil; 100.times { x = p.call }; x } }.map(&:value)
+
+ results.should == [%w[a b c d e]] * 10
+ end
+
+ context "when a block is given" do
+ it "yields each split substring with default pattern" do
+ a = []
+ returned_object = "chunky bacon".split { |str| a << str.capitalize }
+
+ returned_object.should == "chunky bacon"
+ a.should == ["Chunky", "Bacon"]
+ end
+
+ it "yields each split substring with default pattern for a lazy substring" do
+ a = []
+ returned_object = "chunky bacon"[1...-1].split { |str| a << str.capitalize }
+
+ returned_object.should == "hunky baco"
+ a.should == ["Hunky", "Baco"]
+ end
+
+ it "yields each split substring with default pattern for a non-ASCII string" do
+ a = []
+ returned_object = "l'été arrive bientôt".split { |str| a << str }
+
+ returned_object.should == "l'été arrive bientôt"
+ a.should == ["l'été", "arrive", "bientôt"]
+ end
+
+ it "yields each split substring with default pattern for a non-ASCII lazy substring" do
+ a = []
+ returned_object = "l'été arrive bientôt"[1...-1].split { |str| a << str }
+
+ returned_object.should == "'été arrive bientô"
+ a.should == ["'été", "arrive", "bientô"]
+ end
+
+ it "yields the string when limit is 1" do
+ a = []
+ returned_object = "chunky bacon".split("", 1) { |str| a << str.capitalize }
+
+ returned_object.should == "chunky bacon"
+ a.should == ["Chunky bacon"]
+ end
+
+ it "yields each split letter" do
+ a = []
+ returned_object = "chunky".split("", 0) { |str| a << str.capitalize }
+
+ returned_object.should == "chunky"
+ a.should == %w(C H U N K Y)
+ end
+
+ it "yields each split substring with a pattern" do
+ a = []
+ returned_object = "chunky-bacon".split("-", 0) { |str| a << str.capitalize }
+
+ returned_object.should == "chunky-bacon"
+ a.should == ["Chunky", "Bacon"]
+ end
+
+ it "yields each split substring with empty regexp pattern" do
+ a = []
+ returned_object = "chunky".split(//) { |str| a << str.capitalize }
+
+ returned_object.should == "chunky"
+ a.should == %w(C H U N K Y)
+ end
+
+ it "yields each split substring with empty regexp pattern and limit" do
+ a = []
+ returned_object = "chunky".split(//, 3) { |str| a << str.capitalize }
+
+ returned_object.should == "chunky"
+ a.should == %w(C H Unky)
+ end
+
+ it "yields each split substring with a regexp pattern" do
+ a = []
+ returned_object = "chunky:bacon".split(/:/) { |str| a << str.capitalize }
+
+ returned_object.should == "chunky:bacon"
+ a.should == ["Chunky", "Bacon"]
+ end
+
+ it "returns a string as is (and doesn't call block) if it is empty" do
+ a = []
+ returned_object = "".split { |str| a << str.capitalize }
+
+ returned_object.should == ""
+ a.should == []
+ end
+ end
+
+ describe "for a String subclass" do
+ it "yields instances of String" do
+ a = []
+ StringSpecs::MyString.new("a|b").split("|") { |str| a << str }
+ first, last = a
+
+ first.should.instance_of?(String)
+ first.should == "a"
+
+ last.should.instance_of?(String)
+ last.should == "b"
+ end
+ end
+
+ it "raises a TypeError when not called with nil, String, or Regexp" do
+ -> { "hello".split(42) }.should.raise(TypeError)
+ -> { "hello".split(:ll) }.should.raise(TypeError)
+ -> { "hello".split(false) }.should.raise(TypeError)
+ -> { "hello".split(Object.new) }.should.raise(TypeError)
+ end
+
+ it "returns Strings in the same encoding as self" do
+ strings = "hello world".encode("US-ASCII").split(/ /)
+
+ strings[0].encoding.should == Encoding::US_ASCII
+ strings[1].encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/string/squeeze_spec.rb b/spec/ruby/core/string/squeeze_spec.rb
new file mode 100644
index 0000000000..52b6e1eed4
--- /dev/null
+++ b/spec/ruby/core/string/squeeze_spec.rb
@@ -0,0 +1,111 @@
+# encoding: binary
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# TODO: rewrite all these specs
+
+describe "String#squeeze" do
+ it "returns new string where runs of the same character are replaced by a single character when no args are given" do
+ "yellow moon".squeeze.should == "yelow mon"
+ end
+
+ it "only squeezes chars that are in the intersection of all sets given" do
+ "woot squeeze cheese".squeeze("eost", "queo").should == "wot squeze chese"
+ " now is the".squeeze(" ").should == " now is the"
+ end
+
+ it "negates sets starting with ^" do
+ s = "<<subbookkeeper!!!>>"
+ s.squeeze("beko", "^e").should == s.squeeze("bko")
+ s.squeeze("^<bek!>").should == s.squeeze("o")
+ s.squeeze("^o").should == s.squeeze("<bek!>")
+ s.squeeze("^").should == s
+ "^__^".squeeze("^^").should == "^_^"
+ "((^^__^^))".squeeze("_^").should == "((^_^))"
+ end
+
+ it "squeezes all chars in a sequence" do
+ s = "--subbookkeeper--"
+ s.squeeze("\x00-\xFF").should == s.squeeze
+ s.squeeze("bk-o").should == s.squeeze("bklmno")
+ s.squeeze("b-e").should == s.squeeze("bcde")
+ s.squeeze("e-").should == "-subbookkeper-"
+ s.squeeze("-e").should == "-subbookkeper-"
+ s.squeeze("---").should == "-subbookkeeper-"
+ "ook--001122".squeeze("--2").should == "ook-012"
+ "ook--(())".squeeze("(--").should == "ook-()"
+ s.squeeze("^b-e").should == "-subbokeeper-"
+ "^^__^^".squeeze("^^-^").should == "^^_^^"
+ "^^--^^".squeeze("^---").should == "^--^"
+
+ s.squeeze("b-dk-o-").should == "-subokeeper-"
+ s.squeeze("-b-dk-o").should == "-subokeeper-"
+ s.squeeze("b-d-k-o").should == "-subokeeper-"
+
+ s.squeeze("bc-e").should == "--subookkeper--"
+ s.squeeze("^bc-e").should == "-subbokeeper-"
+
+ "AABBCCaabbcc[[]]".squeeze("A-a").should == "ABCabbcc[]"
+ end
+
+ it "raises an ArgumentError when the parameter is out of sequence" do
+ s = "--subbookkeeper--"
+ -> { s.squeeze("e-b") }.should.raise(ArgumentError)
+ -> { s.squeeze("^e-b") }.should.raise(ArgumentError)
+ end
+
+ it "tries to convert each set arg to a string using to_str" do
+ other_string = mock('lo')
+ other_string.should_receive(:to_str).and_return("lo")
+
+ other_string2 = mock('o')
+ other_string2.should_receive(:to_str).and_return("o")
+
+ "hello room".squeeze(other_string, other_string2).should == "hello rom"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "yellow moon".encode("US-ASCII").squeeze.encoding.should == Encoding::US_ASCII
+ "yellow moon".encode("US-ASCII").squeeze("a").encoding.should == Encoding::US_ASCII
+ end
+
+ it "raises a TypeError when one set arg can't be converted to a string" do
+ -> { "hello world".squeeze([]) }.should.raise(TypeError)
+ -> { "hello world".squeeze(Object.new)}.should.raise(TypeError)
+ -> { "hello world".squeeze(mock('x')) }.should.raise(TypeError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("oh no!!!").squeeze("!").should.instance_of?(String)
+ end
+end
+
+describe "String#squeeze!" do
+ it "modifies self in place and returns self" do
+ a = "yellow moon"
+ a.squeeze!.should.equal?(a)
+ a.should == "yelow mon"
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "squeeze"
+ a.squeeze!("u", "sq").should == nil
+ a.squeeze!("q").should == nil
+ a.should == "squeeze"
+ end
+
+ it "raises an ArgumentError when the parameter is out of sequence" do
+ s = "--subbookkeeper--"
+ -> { s.squeeze!("e-b") }.should.raise(ArgumentError)
+ -> { s.squeeze!("^e-b") }.should.raise(ArgumentError)
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ a = "yellow moon"
+ a.freeze
+
+ -> { a.squeeze!("") }.should.raise(FrozenError)
+ -> { a.squeeze! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/start_with_spec.rb b/spec/ruby/core/string/start_with_spec.rb
new file mode 100644
index 0000000000..8b0ba6b5a7
--- /dev/null
+++ b/spec/ruby/core/string/start_with_spec.rb
@@ -0,0 +1,18 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/string/start_with'
+
+describe "String#start_with?" do
+ it_behaves_like :start_with, :to_s
+
+ # Here and not in the shared examples because this is invalid as a Symbol
+ it "matches part of a character with the same part" do
+ "\xA9".should.start_with?("\xA9") # A9 is not a character head for UTF-8
+ end
+
+ it "checks we are matching only part of a character" do
+ "\xe3\x81\x82".size.should == 1
+ "\xe3\x81\x82".should_not.start_with?("\xe3")
+ end
+end
diff --git a/spec/ruby/core/string/string_spec.rb b/spec/ruby/core/string/string_spec.rb
new file mode 100644
index 0000000000..cdefbbecbd
--- /dev/null
+++ b/spec/ruby/core/string/string_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "String" do
+ it "includes Comparable" do
+ String.include?(Comparable).should == true
+ end
+end
diff --git a/spec/ruby/core/string/strip_spec.rb b/spec/ruby/core/string/strip_spec.rb
new file mode 100644
index 0000000000..81994a7f2e
--- /dev/null
+++ b/spec/ruby/core/string/strip_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/strip'
+
+describe "String#strip" do
+ it_behaves_like :string_strip, :strip
+
+ it "returns a new string with leading and trailing whitespace removed" do
+ " hello ".strip.should == "hello"
+ " hello world ".strip.should == "hello world"
+ "\tgoodbye\r\v\n".strip.should == "goodbye"
+ end
+
+ it "returns a copy of self without leading and trailing NULL bytes and whitespace" do
+ " \x00 goodbye \x00 ".strip.should == "goodbye"
+ end
+end
+
+describe "String#strip!" do
+ it "modifies self in place and returns self" do
+ a = " hello "
+ a.strip!.should.equal?(a)
+ a.should == "hello"
+
+ a = "\tgoodbye\r\v\n"
+ a.strip!
+ a.should == "goodbye"
+ end
+
+ it "returns nil if no modifications where made" do
+ a = "hello"
+ a.strip!.should == nil
+ a.should == "hello"
+ end
+
+ it "makes a string empty if it is only whitespace" do
+ "".strip!.should == nil
+ " ".strip.should == ""
+ " ".strip.should == ""
+ end
+
+ it "removes leading and trailing NULL bytes and whitespace" do
+ a = "\000 goodbye \000"
+ a.strip!
+ a.should == "goodbye"
+ end
+
+ it "raises a FrozenError on a frozen instance that is modified" do
+ -> { " hello ".freeze.strip! }.should.raise(FrozenError)
+ end
+
+ # see #1552
+ it "raises a FrozenError on a frozen instance that would not be modified" do
+ -> {"hello".freeze.strip! }.should.raise(FrozenError)
+ -> {"".freeze.strip! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/sub_spec.rb b/spec/ruby/core/string/sub_spec.rb
new file mode 100644
index 0000000000..f0082fba59
--- /dev/null
+++ b/spec/ruby/core/string/sub_spec.rb
@@ -0,0 +1,512 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#sub with pattern, replacement" do
+ it "returns a copy of self when no modification is made" do
+ a = "hello"
+ b = a.sub(/w.*$/, "*")
+
+ b.should_not.equal?(a)
+ b.should == "hello"
+ end
+
+ it "returns a copy of self with all occurrences of pattern replaced with replacement" do
+ "hello".sub(/[aeiou]/, '*').should == "h*llo"
+ "hello".sub(//, ".").should == ".hello"
+ end
+
+ it "ignores a block if supplied" do
+ "food".sub(/f/, "g") { "w" }.should == "good"
+ end
+
+ it "supports \\G which matches at the beginning of the string" do
+ "hello world!".sub(/\Ghello/, "hi").should == "hi world!"
+ end
+
+ it "supports /i for ignoring case" do
+ "Hello".sub(/h/i, "j").should == "jello"
+ "hello".sub(/H/i, "j").should == "jello"
+ end
+
+ it "doesn't interpret regexp metacharacters if pattern is a string" do
+ "12345".sub('\d', 'a').should == "12345"
+ '\d'.sub('\d', 'a').should == "a"
+ end
+
+ it "replaces \\1 sequences with the regexp's corresponding capture" do
+ str = "hello"
+
+ str.sub(/([aeiou])/, '<\1>').should == "h<e>llo"
+ str.sub(/(.)/, '\1\1').should == "hhello"
+
+ str.sub(/.(.?)/, '<\0>(\1)').should == "<he>(e)llo"
+
+ str.sub(/.(.)+/, '\1').should == "o"
+
+ str = "ABCDEFGHIJKL"
+ re = /#{"(.)" * 12}/
+ str.sub(re, '\1').should == "A"
+ str.sub(re, '\9').should == "I"
+ # Only the first 9 captures can be accessed in MRI
+ str.sub(re, '\10').should == "A0"
+ end
+
+ it "treats \\1 sequences without corresponding captures as empty strings" do
+ str = "hello!"
+
+ str.sub("", '<\1>').should == "<>hello!"
+ str.sub("h", '<\1>').should == "<>ello!"
+
+ str.sub(//, '<\1>').should == "<>hello!"
+ str.sub(/./, '\1\2\3').should == "ello!"
+ str.sub(/.(.{20})?/, '\1').should == "ello!"
+ end
+
+ it "replaces \\& and \\0 with the complete match" do
+ str = "hello!"
+
+ str.sub("", '<\0>').should == "<>hello!"
+ str.sub("", '<\&>').should == "<>hello!"
+ str.sub("he", '<\0>').should == "<he>llo!"
+ str.sub("he", '<\&>').should == "<he>llo!"
+ str.sub("l", '<\0>').should == "he<l>lo!"
+ str.sub("l", '<\&>').should == "he<l>lo!"
+
+ str.sub(//, '<\0>').should == "<>hello!"
+ str.sub(//, '<\&>').should == "<>hello!"
+ str.sub(/../, '<\0>').should == "<he>llo!"
+ str.sub(/../, '<\&>').should == "<he>llo!"
+ str.sub(/(.)./, '<\0>').should == "<he>llo!"
+ end
+
+ it "replaces \\` with everything before the current match" do
+ str = "hello!"
+
+ str.sub("", '<\`>').should == "<>hello!"
+ str.sub("h", '<\`>').should == "<>ello!"
+ str.sub("l", '<\`>').should == "he<he>lo!"
+ str.sub("!", '<\`>').should == "hello<hello>"
+
+ str.sub(//, '<\`>').should == "<>hello!"
+ str.sub(/..o/, '<\`>').should == "he<he>!"
+ end
+
+ it "replaces \\' with everything after the current match" do
+ str = "hello!"
+
+ str.sub("", '<\\\'>').should == "<hello!>hello!"
+ str.sub("h", '<\\\'>').should == "<ello!>ello!"
+ str.sub("ll", '<\\\'>').should == "he<o!>o!"
+ str.sub("!", '<\\\'>').should == "hello<>"
+
+ str.sub(//, '<\\\'>').should == "<hello!>hello!"
+ str.sub(/../, '<\\\'>').should == "<llo!>llo!"
+ end
+
+ it "replaces \\\\\\+ with \\\\+" do
+ "x".sub(/x/, '\\\+').should == "\\+"
+ end
+
+ it "replaces \\+ with the last paren that actually matched" do
+ str = "hello!"
+
+ str.sub(/(.)(.)/, '\+').should == "ello!"
+ str.sub(/(.)(.)+/, '\+').should == "!"
+ str.sub(/(.)()/, '\+').should == "ello!"
+ str.sub(/(.)(.{20})?/, '<\+>').should == "<h>ello!"
+
+ str = "ABCDEFGHIJKL"
+ re = /#{"(.)" * 12}/
+ str.sub(re, '\+').should == "L"
+ end
+
+ it "treats \\+ as an empty string if there was no captures" do
+ "hello!".sub(/./, '\+').should == "ello!"
+ end
+
+ it "maps \\\\ in replacement to \\" do
+ "hello".sub(/./, '\\\\').should == '\\ello'
+ end
+
+ it "leaves unknown \\x escapes in replacement untouched" do
+ "hello".sub(/./, '\\x').should == '\\xello'
+ "hello".sub(/./, '\\y').should == '\\yello'
+ end
+
+ it "leaves \\ at the end of replacement untouched" do
+ "hello".sub(/./, 'hah\\').should == 'hah\\ello'
+ end
+
+ it "tries to convert pattern to a string using to_str" do
+ pattern = mock('.')
+ pattern.should_receive(:to_str).and_return(".")
+
+ "hello.".sub(pattern, "!").should == "hello!"
+ end
+
+ not_supported_on :opal do
+ it "raises a TypeError when pattern is a Symbol" do
+ -> { "hello".sub(:woot, "x") }.should.raise(TypeError)
+ end
+ end
+
+ it "raises a TypeError when pattern is an Array" do
+ -> { "hello".sub([], "x") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when pattern can't be converted to a string" do
+ -> { "hello".sub(Object.new, nil) }.should.raise(TypeError)
+ end
+
+ it "tries to convert replacement to a string using to_str" do
+ replacement = mock('hello_replacement')
+ replacement.should_receive(:to_str).and_return("hello_replacement")
+
+ "hello".sub(/hello/, replacement).should == "hello_replacement"
+ end
+
+ it "raises a TypeError when replacement can't be converted to a string" do
+ -> { "hello".sub(/[aeiou]/, []) }.should.raise(TypeError)
+ -> { "hello".sub(/[aeiou]/, 99) }.should.raise(TypeError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("").sub(//, "").should.instance_of?(String)
+ StringSpecs::MyString.new("").sub(/foo/, "").should.instance_of?(String)
+ StringSpecs::MyString.new("foo").sub(/foo/, "").should.instance_of?(String)
+ StringSpecs::MyString.new("foo").sub("foo", "").should.instance_of?(String)
+ end
+
+ it "sets $~ to MatchData of match and nil when there's none" do
+ 'hello.'.sub('hello', 'x')
+ $~[0].should == 'hello'
+
+ 'hello.'.sub('not', 'x')
+ $~.should == nil
+
+ 'hello.'.sub(/.(.)/, 'x')
+ $~[0].should == 'he'
+
+ 'hello.'.sub(/not/, 'x')
+ $~.should == nil
+ end
+
+ it "replaces \\\\\\1 with \\1" do
+ "ababa".sub(/(b)/, '\\\1').should == "a\\1aba"
+ end
+
+ it "replaces \\\\\\\\1 with \\1" do
+ "ababa".sub(/(b)/, '\\\\1').should == "a\\1aba"
+ end
+
+ it "replaces \\\\\\\\\\1 with \\" do
+ "ababa".sub(/(b)/, '\\\\\1').should == "a\\baba"
+ end
+
+ it "handles a pattern in a superset encoding" do
+ result = 'abc'.force_encoding(Encoding::US_ASCII).sub('é', 'è')
+ result.should == 'abc'
+ result.encoding.should == Encoding::US_ASCII
+ end
+
+ it "handles a pattern in a subset encoding" do
+ result = 'été'.sub('t'.force_encoding(Encoding::US_ASCII), 'u')
+ result.should == 'éué'
+ result.encoding.should == Encoding::UTF_8
+ end
+end
+
+describe "String#sub with pattern and block" do
+ it "returns a copy of self with the first occurrences of pattern replaced with the block's return value" do
+ "hi".sub(/./) { |s| s + ' ' }.should == "h i"
+ "hi!".sub(/(.)(.)/) { |*a| a.inspect }.should == '["hi"]!'
+ end
+
+ it "sets $~ for access from the block" do
+ str = "hello"
+ str.sub(/([aeiou])/) { "<#{$~[1]}>" }.should == "h<e>llo"
+ str.sub(/([aeiou])/) { "<#{$1}>" }.should == "h<e>llo"
+ str.sub("l") { "<#{$~[0]}>" }.should == "he<l>lo"
+
+ offsets = []
+
+ str.sub(/([aeiou])/) do
+ md = $~
+ md.string.should == str
+ offsets << md.offset(0)
+ str
+ end.should == "hhellollo"
+
+ offsets.should == [[1, 2]]
+ end
+
+ it "sets $~ to MatchData of last match and nil when there's none for access from outside" do
+ 'hello.'.sub('l') { 'x' }
+ $~.begin(0).should == 2
+ $~[0].should == 'l'
+
+ 'hello.'.sub('not') { 'x' }
+ $~.should == nil
+
+ 'hello.'.sub(/.(.)/) { 'x' }
+ $~[0].should == 'he'
+
+ 'hello.'.sub(/not/) { 'x' }
+ $~.should == nil
+ end
+
+ it "doesn't raise a RuntimeError if the string is modified while substituting" do
+ str = "hello"
+ str.sub(//) { str[0] = 'x' }.should == "xhello"
+ str.should == "xello"
+ end
+
+ it "doesn't interpolate special sequences like \\1 for the block's return value" do
+ repl = '\& \0 \1 \` \\\' \+ \\\\ foo'
+ "hello".sub(/(.+)/) { repl }.should == repl
+ end
+
+ it "converts the block's return value to a string using to_s" do
+ obj = mock('hello_replacement')
+ obj.should_receive(:to_s).and_return("hello_replacement")
+ "hello".sub(/hello/) { obj }.should == "hello_replacement"
+
+ obj = mock('ok')
+ obj.should_receive(:to_s).and_return("ok")
+ "hello".sub(/.+/) { obj }.should == "ok"
+ end
+end
+
+describe "String#sub! with pattern, replacement" do
+ it "modifies self in place and returns self" do
+ a = "hello"
+ a.sub!(/[aeiou]/, '*').should.equal?(a)
+ a.should == "h*llo"
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.sub!(/z/, '*').should == nil
+ a.sub!(/z/, 'z').should == nil
+ a.should == "hello"
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ s = "hello"
+ s.freeze
+
+ -> { s.sub!(/ROAR/, "x") }.should.raise(FrozenError)
+ -> { s.sub!(/e/, "e") }.should.raise(FrozenError)
+ -> { s.sub!(/[aeiou]/, '*') }.should.raise(FrozenError)
+ end
+
+ it "handles a pattern in a superset encoding" do
+ string = 'abc'.force_encoding(Encoding::US_ASCII)
+
+ result = string.sub!('é', 'è')
+
+ result.should == nil
+ string.should == 'abc'
+ string.encoding.should == Encoding::US_ASCII
+ end
+
+ it "handles a pattern in a subset encoding" do
+ string = 'été'
+ pattern = 't'.force_encoding(Encoding::US_ASCII)
+
+ result = string.sub!(pattern, 'u')
+
+ result.should == string
+ string.should == 'éué'
+ string.encoding.should == Encoding::UTF_8
+ end
+end
+
+describe "String#sub! with pattern and block" do
+ it "modifies self in place and returns self" do
+ a = "hello"
+ a.sub!(/[aeiou]/) { '*' }.should.equal?(a)
+ a.should == "h*llo"
+ end
+
+ it "sets $~ for access from the block" do
+ str = "hello"
+ str.dup.sub!(/([aeiou])/) { "<#{$~[1]}>" }.should == "h<e>llo"
+ str.dup.sub!(/([aeiou])/) { "<#{$1}>" }.should == "h<e>llo"
+ str.dup.sub!("l") { "<#{$~[0]}>" }.should == "he<l>lo"
+
+ offsets = []
+
+ str.dup.sub!(/([aeiou])/) do
+ md = $~
+ md.string.should == str
+ offsets << md.offset(0)
+ str
+ end.should == "hhellollo"
+
+ offsets.should == [[1, 2]]
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "hello"
+ a.sub!(/z/) { '*' }.should == nil
+ a.sub!(/z/) { 'z' }.should == nil
+ a.should == "hello"
+ end
+
+ it "raises a RuntimeError if the string is modified while substituting" do
+ str = "hello"
+ -> { str.sub!(//) { str << 'x' } }.should.raise(RuntimeError)
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ s = "hello"
+ s.freeze
+
+ -> { s.sub!(/ROAR/) { "x" } }.should.raise(FrozenError)
+ -> { s.sub!(/e/) { "e" } }.should.raise(FrozenError)
+ -> { s.sub!(/[aeiou]/) { '*' } }.should.raise(FrozenError)
+ end
+end
+
+describe "String#sub with pattern and Hash" do
+
+ it "returns a copy of self with the first occurrence of pattern replaced with the value of the corresponding hash key" do
+ "hello".sub(/./, 'l' => 'L').should == "ello"
+ "hello!".sub(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she llo!'
+ "hello".sub('l', 'l' => 'el').should == 'heello'
+ end
+
+ it "removes keys that don't correspond to matches" do
+ "hello".sub(/./, 'z' => 'b', 'o' => 'ow').should == "ello"
+ end
+
+ it "ignores non-String keys" do
+ "tattoo".sub(/(tt)/, 'tt' => 'b', tt: 'z').should == "taboo"
+ end
+
+ it "uses a key's value only a single time" do
+ "food".sub(/o/, 'o' => '0').should == "f0od"
+ end
+
+ it "uses the hash's default value for missing keys" do
+ hsh = {}
+ hsh.default='?'
+ hsh['o'] = '0'
+ "food".sub(/./, hsh).should == "?ood"
+ end
+
+ it "coerces the hash values with #to_s" do
+ hsh = {}
+ hsh.default=[]
+ hsh['o'] = 0
+ obj = mock('!')
+ obj.should_receive(:to_s).and_return('!')
+ hsh['f'] = obj
+ "food!".sub(/./, hsh).should == "!ood!"
+ end
+
+ it "uses the hash's value set from default_proc for missing keys" do
+ hsh = {}
+ hsh.default_proc = -> k, v { 'lamb' }
+ "food!".sub(/./, hsh).should == "lambood!"
+ end
+
+ it "sets $~ to MatchData of first match and nil when there's none for access from outside" do
+ 'hello.'.sub('l', 'l' => 'L')
+ $~.begin(0).should == 2
+ $~[0].should == 'l'
+
+ 'hello.'.sub('not', 'ot' => 'to')
+ $~.should == nil
+
+ 'hello.'.sub(/.(.)/, 'o' => ' hole')
+ $~[0].should == 'he'
+
+ 'hello.'.sub(/not/, 'z' => 'glark')
+ $~.should == nil
+ end
+
+ it "doesn't interpolate special sequences like \\1 for the block's return value" do
+ repl = '\& \0 \1 \` \\\' \+ \\\\ foo'
+ "hello".sub(/(.+)/, 'hello' => repl ).should == repl
+ end
+
+end
+
+describe "String#sub! with pattern and Hash" do
+
+ it "returns self with the first occurrence of pattern replaced with the value of the corresponding hash key" do
+ "hello".sub!(/./, 'l' => 'L').should == "ello"
+ "hello!".sub!(/(.)(.)/, 'he' => 'she ', 'll' => 'said').should == 'she llo!'
+ "hello".sub!('l', 'l' => 'el').should == 'heello'
+ end
+
+ it "removes keys that don't correspond to matches" do
+ "hello".sub!(/./, 'z' => 'b', 'o' => 'ow').should == "ello"
+ end
+
+ it "ignores non-String keys" do
+ "hello".sub!(/(ll)/, 'll' => 'r', ll: 'z').should == "hero"
+ end
+
+ it "uses a key's value only a single time" do
+ "food".sub!(/o/, 'o' => '0').should == "f0od"
+ end
+
+ it "uses the hash's default value for missing keys" do
+ hsh = {}
+ hsh.default='?'
+ hsh['o'] = '0'
+ "food".sub!(/./, hsh).should == "?ood"
+ end
+
+ it "coerces the hash values with #to_s" do
+ hsh = {}
+ hsh.default=[]
+ hsh['o'] = 0
+ obj = mock('!')
+ obj.should_receive(:to_s).and_return('!')
+ hsh['f'] = obj
+ "food!".sub!(/./, hsh).should == "!ood!"
+ end
+
+ it "uses the hash's value set from default_proc for missing keys" do
+ hsh = {}
+ hsh.default_proc = -> k, v { 'lamb' }
+ "food!".sub!(/./, hsh).should == "lambood!"
+ end
+
+ it "sets $~ to MatchData of first match and nil when there's none for access from outside" do
+ 'hello.'.sub!('l', 'l' => 'L')
+ $~.begin(0).should == 2
+ $~[0].should == 'l'
+
+ 'hello.'.sub!('not', 'ot' => 'to')
+ $~.should == nil
+
+ 'hello.'.sub!(/.(.)/, 'o' => ' hole')
+ $~[0].should == 'he'
+
+ 'hello.'.sub!(/not/, 'z' => 'glark')
+ $~.should == nil
+ end
+
+ it "doesn't interpolate special sequences like \\1 for the block's return value" do
+ repl = '\& \0 \1 \` \\\' \+ \\\\ foo'
+ "hello".sub!(/(.+)/, 'hello' => repl ).should == repl
+ end
+end
+
+describe "String#sub with pattern and without replacement and block" do
+ it "raises a ArgumentError" do
+ -> { "abca".sub(/a/) }.should.raise(ArgumentError)
+ end
+end
+
+describe "String#sub! with pattern and without replacement and block" do
+ it "raises a ArgumentError" do
+ -> { "abca".sub!(/a/) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/succ_spec.rb b/spec/ruby/core/string/succ_spec.rb
new file mode 100644
index 0000000000..65047e0aa2
--- /dev/null
+++ b/spec/ruby/core/string/succ_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/succ'
+
+describe "String#succ" do
+ it_behaves_like :string_succ, :succ
+end
+
+describe "String#succ!" do
+ it_behaves_like :string_succ_bang, :"succ!"
+end
diff --git a/spec/ruby/core/string/sum_spec.rb b/spec/ruby/core/string/sum_spec.rb
new file mode 100644
index 0000000000..c283b7c254
--- /dev/null
+++ b/spec/ruby/core/string/sum_spec.rb
@@ -0,0 +1,22 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#sum" do
+ it "returns a basic n-bit checksum of the characters in self" do
+ "ruby".sum.should == 450
+ "ruby".sum(8).should == 194
+ "rubinius".sum(23).should == 881
+ end
+
+ it "tries to convert n to an integer using to_int" do
+ obj = mock('8')
+ obj.should_receive(:to_int).and_return(8)
+
+ "hello".sum(obj).should == "hello".sum(8)
+ end
+
+ it "returns sum of the bytes in self if n less or equal to zero" do
+ "xyz".sum(0).should == 363
+ "xyz".sum(-10).should == 363
+ end
+end
diff --git a/spec/ruby/core/string/swapcase_spec.rb b/spec/ruby/core/string/swapcase_spec.rb
new file mode 100644
index 0000000000..f0e6e0182c
--- /dev/null
+++ b/spec/ruby/core/string/swapcase_spec.rb
@@ -0,0 +1,193 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#swapcase" do
+ it "returns a new string with all uppercase chars from self converted to lowercase and vice versa" do
+ "Hello".swapcase.should == "hELLO"
+ "cYbEr_PuNk11".swapcase.should == "CyBeR_pUnK11"
+ "+++---111222???".swapcase.should == "+++---111222???"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "Hello".encode("US-ASCII").swapcase.encoding.should == Encoding::US_ASCII
+ end
+
+ describe "full Unicode case mapping" do
+ it "works for all of Unicode with no option" do
+ "äÖü".swapcase.should == "ÄöÜ"
+ end
+
+ it "updates string metadata" do
+ swapcased = "Aßet".swapcase
+
+ swapcased.should == "aSSET"
+ swapcased.size.should == 5
+ swapcased.bytesize.should == 5
+ swapcased.ascii_only?.should == true
+ end
+ end
+
+ describe "ASCII-only case mapping" do
+ it "does not swapcase non-ASCII characters" do
+ "aßet".swapcase(:ascii).should == "AßET"
+ end
+
+ it "works with substrings" do
+ "prefix aTé"[-3..-1].swapcase(:ascii).should == "Até"
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Turkic languages" do
+ it "swaps case of ASCII characters according to Turkic semantics" do
+ "aiS".swapcase(:turkic).should == "Aİs"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ "aiS".swapcase(:turkic, :lithuanian).should == "Aİs"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "aiS".swapcase(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ "Iß".swapcase(:lithuanian).should == "iSS"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ "iS".swapcase(:lithuanian, :turkic).should == "İs"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "aiS".swapcase(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not allow the :fold option for upcasing" do
+ -> { "abc".swapcase(:fold) }.should.raise(ArgumentError)
+ end
+
+ it "does not allow invalid options" do
+ -> { "abc".swapcase(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("").swapcase.should.instance_of?(String)
+ StringSpecs::MyString.new("hello").swapcase.should.instance_of?(String)
+ end
+end
+
+describe "String#swapcase!" do
+ it "modifies self in place" do
+ a = "cYbEr_PuNk11"
+ a.swapcase!.should.equal?(a)
+ a.should == "CyBeR_pUnK11"
+ end
+
+ it "modifies self in place for non-ascii-compatible encodings" do
+ a = "cYbEr_PuNk11".encode("utf-16le")
+ a.swapcase!
+ a.should == "CyBeR_pUnK11".encode("utf-16le")
+ end
+
+ describe "full Unicode case mapping" do
+ it "modifies self in place for all of Unicode with no option" do
+ a = "äÖü"
+ a.swapcase!
+ a.should == "ÄöÜ"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "äÖü".encode("utf-16le")
+ a.swapcase!
+ a.should == "ÄöÜ".encode("utf-16le")
+ end
+
+ it "updates string metadata" do
+ swapcased = "Aßet"
+ swapcased.swapcase!
+
+ swapcased.should == "aSSET"
+ swapcased.size.should == 5
+ swapcased.bytesize.should == 5
+ swapcased.ascii_only?.should == true
+ end
+ end
+
+ describe "modifies self in place for ASCII-only case mapping" do
+ it "does not swapcase non-ASCII characters" do
+ a = "aßet"
+ a.swapcase!(:ascii)
+ a.should == "AßET"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "aBc".encode("utf-16le")
+ a.swapcase!(:ascii)
+ a.should == "AbC".encode("utf-16le")
+ end
+ end
+
+ describe "modifies self in place for full Unicode case mapping adapted for Turkic languages" do
+ it "swaps case of ASCII characters according to Turkic semantics" do
+ a = "aiS"
+ a.swapcase!(:turkic)
+ a.should == "Aİs"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ a = "aiS"
+ a.swapcase!(:turkic, :lithuanian)
+ a.should == "Aİs"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "aiS"; a.swapcase!(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ a = "Iß"
+ a.swapcase!(:lithuanian)
+ a.should == "iSS"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ a = "iS"
+ a.swapcase!(:lithuanian, :turkic)
+ a.should == "İs"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "aiS"; a.swapcase!(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not allow the :fold option for upcasing" do
+ -> { a = "abc"; a.swapcase!(:fold) }.should.raise(ArgumentError)
+ end
+
+ it "does not allow invalid options" do
+ -> { a = "abc"; a.swapcase!(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "+++---111222???"
+ a.swapcase!.should == nil
+ a.should == "+++---111222???"
+
+ "".swapcase!.should == nil
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ ["", "hello"].each do |a|
+ a.freeze
+ -> { a.swapcase! }.should.raise(FrozenError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/to_c_spec.rb b/spec/ruby/core/string/to_c_spec.rb
new file mode 100644
index 0000000000..9cd0ed4401
--- /dev/null
+++ b/spec/ruby/core/string/to_c_spec.rb
@@ -0,0 +1,53 @@
+require_relative '../../spec_helper'
+require_relative '../../shared/kernel/complex'
+require_relative 'fixtures/to_c'
+
+describe "String#to_c" do
+ it_behaves_like :kernel_complex, :to_c_method, StringSpecs
+end
+
+describe "String#to_c" do
+ it "returns a complex number with 0 as the real part, 0 as the imaginary part for unrecognised Strings" do
+ 'ruby'.to_c.should == Complex(0, 0)
+ end
+
+ it "ignores trailing garbage" do
+ '79+4iruby'.to_c.should == Complex(79, 4)
+ '7__9+4__0i'.to_c.should == Complex(7, 0)
+ end
+
+ context "it treats special float value strings as characters" do
+ it "parses any string that starts with 'I' as 1i" do
+ 'Infinity'.to_c.should == Complex(0, 1)
+ '-Infinity'.to_c.should == Complex(0, -1)
+ 'Insecure'.to_c.should == Complex(0, 1)
+ '-Insecure'.to_c.should == Complex(0, -1)
+ end
+
+ it "does not parse any numeric information in 'NaN'" do
+ 'NaN'.to_c.should == Complex(0, 0)
+ end
+ end
+
+ it "allows null-byte" do
+ "1-2i\0".to_c.should == Complex(1, -2)
+ "1\0-2i".to_c.should == Complex(1, 0)
+ "\01-2i".to_c.should == Complex(0, 0)
+ end
+
+ it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do
+ -> {
+ '79+4i'.encode("UTF-16").to_c
+ }.should.raise(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16")
+ end
+
+ it "treats a sequence of underscores as an end of Complex string" do
+ "5+3_1i".to_c.should == Complex(5, 31)
+ "5+3__1i".to_c.should == Complex(5)
+ "5+3___1i".to_c.should == Complex(5)
+
+ "12_3".to_c.should == Complex(123)
+ "12__3".to_c.should == Complex(12)
+ "12___3".to_c.should == Complex(12)
+ end
+end
diff --git a/spec/ruby/core/string/to_f_spec.rb b/spec/ruby/core/string/to_f_spec.rb
new file mode 100644
index 0000000000..bb09e4f2f3
--- /dev/null
+++ b/spec/ruby/core/string/to_f_spec.rb
@@ -0,0 +1,140 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# src.scan(/[+-]?[\d_]+\.[\d_]+(e[+-]?[\d_]+)?\b|[+-]?[\d_]+e[+-]?[\d_]+\b/i)
+
+describe "String#to_f" do
+ it "treats leading characters of self as a floating point number" do
+ "123.45e1".to_f.should == 1234.5
+ "45.67 degrees".to_f.should == 45.67
+ "0".to_f.should == 0.0
+
+ ".5".to_f.should == 0.5
+ ".5e1".to_f.should == 5.0
+ "5.".to_f.should == 5.0
+ "5e".to_f.should == 5.0
+ "5E".to_f.should == 5.0
+ end
+
+ it "treats special float value strings as characters" do
+ "NaN".to_f.should == 0
+ "Infinity".to_f.should == 0
+ "-Infinity".to_f.should == 0
+ end
+
+ it "allows for varying case" do
+ "123.45e1".to_f.should == 1234.5
+ "123.45E1".to_f.should == 1234.5
+ end
+
+ it "allows for varying signs" do
+ "+123.45e1".to_f.should == +123.45e1
+ "-123.45e1".to_f.should == -123.45e1
+ "123.45e+1".to_f.should == 123.45e+1
+ "123.45e-1".to_f.should == 123.45e-1
+ "+123.45e+1".to_f.should == +123.45e+1
+ "+123.45e-1".to_f.should == +123.45e-1
+ "-123.45e+1".to_f.should == -123.45e+1
+ "-123.45e-1".to_f.should == -123.45e-1
+ end
+
+ it "allows for underscores, even in the decimal side" do
+ "1_234_567.890_1".to_f.should == 1_234_567.890_1
+ end
+
+ it "returns 0 for strings with leading underscores" do
+ "_9".to_f.should == 0
+ end
+
+ it "stops if the underscore is not followed or preceded by a number" do
+ "1__2".to_f.should == 1.0
+ "1_.2".to_f.should == 1.0
+ "1._2".to_f.should == 1.0
+ "1.2_e2".to_f.should == 1.2
+ "1.2e_2".to_f.should == 1.2
+ "1_x2".to_f.should == 1.0
+ "1x_2".to_f.should == 1.0
+ "+_1".to_f.should == 0.0
+ "-_1".to_f.should == 0.0
+ end
+
+ it "does not allow prefixes to autodetect the base" do
+ "0b10".to_f.should == 0
+ "010".to_f.should == 10
+ "0o10".to_f.should == 0
+ "0d10".to_f.should == 0
+ "0x10".to_f.should == 0
+ end
+
+ it "treats any non-numeric character other than '.', 'e' and '_' as terminals" do
+ "blah".to_f.should == 0
+ "1b5".to_f.should == 1
+ "1d5".to_f.should == 1
+ "1o5".to_f.should == 1
+ "1xx5".to_f.should == 1
+ "x5".to_f.should == 0
+ end
+
+ it "takes an optional sign" do
+ "-45.67 degrees".to_f.should == -45.67
+ "+45.67 degrees".to_f.should == 45.67
+ "-5_5e-5_0".to_f.should == -55e-50
+ "-".to_f.should == 0.0
+ (1.0 / "-0".to_f).to_s.should == "-Infinity"
+ end
+
+ it "treats a second 'e' as terminal" do
+ "1.234e1e2".to_f.should == 1.234e1
+ end
+
+ it "treats a second '.' as terminal" do
+ "1.2.3".to_f.should == 1.2
+ end
+
+ it "treats a '.' after an 'e' as terminal" do
+ "1.234e1.9".to_f.should == 1.234e1
+ end
+
+ it "returns 0.0 if the conversion fails" do
+ "bad".to_f.should == 0.0
+ "thx1138".to_f.should == 0.0
+ end
+
+ it "ignores leading and trailing whitespace" do
+ " 1.2".to_f.should == 1.2
+ "1.2 ".to_f.should == 1.2
+ " 1.2 ".to_f.should == 1.2
+ "\t1.2".to_f.should == 1.2
+ "\n1.2".to_f.should == 1.2
+ "\v1.2".to_f.should == 1.2
+ "\f1.2".to_f.should == 1.2
+ "\r1.2".to_f.should == 1.2
+ end
+
+ it "treats non-printable ASCII characters as terminals" do
+ "\0001.2".to_f.should == 0
+ "\0011.2".to_f.should == 0
+ "\0371.2".to_f.should == 0
+ "\1771.2".to_f.should == 0
+ "\2001.2".b.to_f.should == 0
+ "\3771.2".b.to_f.should == 0
+ end
+
+ it "raises Encoding::CompatibilityError if String is in not ASCII-compatible encoding" do
+ -> {
+ '1.2'.encode("UTF-16").to_f
+ }.should.raise(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16")
+ end
+
+ it "allows String representation without a fractional part" do
+ "1.".to_f.should == 1.0
+ "+1.".to_f.should == 1.0
+ "-1.".to_f.should == -1.0
+ "1.e+0".to_f.should == 1.0
+ "1.e+0".to_f.should == 1.0
+
+ ruby_bug "#20705", ""..."3.4" do
+ "1.e-2".to_f.should be_close(0.01, TOLERANCE)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/to_i_spec.rb b/spec/ruby/core/string/to_i_spec.rb
new file mode 100644
index 0000000000..629750bd73
--- /dev/null
+++ b/spec/ruby/core/string/to_i_spec.rb
@@ -0,0 +1,349 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#to_i" do
+ it "returns 0 for strings with leading underscores" do
+ "_123".to_i.should == 0
+ end
+
+ it "ignores underscores in between the digits" do
+ "1_2_3asdf".to_i.should == 123
+ end
+
+ it "ignores multiple non-consecutive underscores when the first digit is 0" do
+ (2..16).each do |base|
+ "0_0_010".to_i(base).should == base;
+ end
+ end
+
+ it "bails out at the first double underscore if the first digit is 0" do
+ (2..16).each do |base|
+ "010__1".to_i(base).should == base;
+ end
+ end
+
+ it "ignores leading whitespaces" do
+ [ " 123", " 123", "\r\n\r\n123", "\t\t123",
+ "\r\n\t\n123", " \t\n\r\t 123"].each do |str|
+ str.to_i.should == 123
+ end
+ end
+
+ it "ignores subsequent invalid characters" do
+ "123asdf".to_i.should == 123
+ "123#123".to_i.should == 123
+ "123 456".to_i.should == 123
+ end
+
+ it "returns 0 if self is no valid integer-representation" do
+ [ "++2", "+-2", "--2" ].each do |str|
+ str.to_i.should == 0
+ end
+ end
+
+ it "accepts '+' at the beginning of a String" do
+ "+0d56".to_i.should == 56
+ end
+
+ it "interprets leading characters as a number in the given base" do
+ "100110010010".to_i(2).should == 0b100110010010
+ "100110201001".to_i(3).should == 186409
+ "103110201001".to_i(4).should == 5064769
+ "103110241001".to_i(5).should == 55165126
+ "153110241001".to_i(6).should == 697341529
+ "153160241001".to_i(7).should == 3521513430
+ "153160241701".to_i(8).should == 14390739905
+ "853160241701".to_i(9).should == 269716550518
+ "853160241791".to_i(10).should == 853160241791
+
+ "F00D_BE_1337".to_i(16).should == 0xF00D_BE_1337
+ "-hello_world".to_i(32).should == -18306744
+ "abcXYZ".to_i(36).should == 623741435
+
+ ("z" * 24).to_i(36).should == 22452257707354557240087211123792674815
+
+ "5e10".to_i.should == 5
+ end
+
+ it "auto-detects base 8 via leading 0 when base = 0" do
+ "01778".to_i(0).should == 0177
+ "-01778".to_i(0).should == -0177
+ end
+
+ it "auto-detects base 2 via 0b when base = 0" do
+ "0b112".to_i(0).should == 0b11
+ "-0b112".to_i(0).should == -0b11
+ end
+
+ it "auto-detects base 10 via 0d when base = 0" do
+ "0d19A".to_i(0).should == 19
+ "-0d19A".to_i(0).should == -19
+ end
+
+ it "auto-detects base 8 via 0o when base = 0" do
+ "0o178".to_i(0).should == 0o17
+ "-0o178".to_i(0).should == -0o17
+ end
+
+ it "auto-detects base 16 via 0x when base = 0" do
+ "0xFAZ".to_i(0).should == 0xFA
+ "-0xFAZ".to_i(0).should == -0xFA
+ end
+
+ it "auto-detects base 10 with no base specifier when base = 0" do
+ "1234567890ABC".to_i(0).should == 1234567890
+ "-1234567890ABC".to_i(0).should == -1234567890
+ end
+
+ it "doesn't handle foreign base specifiers when base is > 0" do
+ [2, 3, 4, 8, 10].each do |base|
+ "0111".to_i(base).should == "111".to_i(base)
+
+ "0b11".to_i(base).should == (base == 2 ? 0b11 : 0)
+ "0d11".to_i(base).should == (base == 10 ? 0d11 : 0)
+ "0o11".to_i(base).should == (base == 8 ? 0o11 : 0)
+ "0xFA".to_i(base).should == 0
+ end
+
+ "0xD00D".to_i(16).should == 0xD00D
+
+ "0b11".to_i(16).should == 0xb11
+ "0d11".to_i(16).should == 0xd11
+ "0o11".to_i(25).should == 15026
+ "0x11".to_i(34).should == 38183
+
+ "0B11".to_i(16).should == 0xb11
+ "0D11".to_i(16).should == 0xd11
+ "0O11".to_i(25).should == 15026
+ "0X11".to_i(34).should == 38183
+ end
+
+ it "tries to convert the base to an integer using to_int" do
+ obj = mock('8')
+ obj.should_receive(:to_int).and_return(8)
+
+ "777".to_i(obj).should == 0777
+ end
+
+ it "requires that the sign if any appears before the base specifier" do
+ "0b-1".to_i( 2).should == 0
+ "0d-1".to_i(10).should == 0
+ "0o-1".to_i( 8).should == 0
+ "0x-1".to_i(16).should == 0
+
+ "0b-1".to_i(2).should == 0
+ "0o-1".to_i(8).should == 0
+ "0d-1".to_i(10).should == 0
+ "0x-1".to_i(16).should == 0
+ end
+
+ it "raises an ArgumentError for illegal bases (1, < 0 or > 36)" do
+ -> { "".to_i(1) }.should.raise(ArgumentError)
+ -> { "".to_i(-1) }.should.raise(ArgumentError)
+ -> { "".to_i(37) }.should.raise(ArgumentError)
+ end
+
+ it "returns an Integer for long strings with trailing spaces" do
+ "0 ".to_i.should == 0
+ "0 ".to_i.should.instance_of?(Integer)
+
+ "10 ".to_i.should == 10
+ "10 ".to_i.should.instance_of?(Integer)
+
+ "-10 ".to_i.should == -10
+ "-10 ".to_i.should.instance_of?(Integer)
+ end
+
+ it "returns an Integer for long strings with leading spaces" do
+ " 0".to_i.should == 0
+ " 0".to_i.should.instance_of?(Integer)
+
+ " 10".to_i.should == 10
+ " 10".to_i.should.instance_of?(Integer)
+
+ " -10".to_i.should == -10
+ " -10".to_i.should.instance_of?(Integer)
+ end
+
+ it "returns the correct Integer for long strings" do
+ "245789127594125924165923648312749312749327482".to_i.should == 245789127594125924165923648312749312749327482
+ "-245789127594125924165923648312749312749327482".to_i.should == -245789127594125924165923648312749312749327482
+ end
+end
+
+describe "String#to_i with bases" do
+ it "parses a String in base 2" do
+ str = "10" * 50
+ str.to_i(2).to_s(2).should == str
+ end
+
+ it "parses a String in base 3" do
+ str = "120" * 33
+ str.to_i(3).to_s(3).should == str
+ end
+
+ it "parses a String in base 4" do
+ str = "1230" * 25
+ str.to_i(4).to_s(4).should == str
+ end
+
+ it "parses a String in base 5" do
+ str = "12340" * 20
+ str.to_i(5).to_s(5).should == str
+ end
+
+ it "parses a String in base 6" do
+ str = "123450" * 16
+ str.to_i(6).to_s(6).should == str
+ end
+
+ it "parses a String in base 7" do
+ str = "1234560" * 14
+ str.to_i(7).to_s(7).should == str
+ end
+
+ it "parses a String in base 8" do
+ str = "12345670" * 12
+ str.to_i(8).to_s(8).should == str
+ end
+
+ it "parses a String in base 9" do
+ str = "123456780" * 11
+ str.to_i(9).to_s(9).should == str
+ end
+
+ it "parses a String in base 10" do
+ str = "1234567890" * 10
+ str.to_i(10).to_s(10).should == str
+ end
+
+ it "parses a String in base 11" do
+ str = "1234567890a" * 9
+ str.to_i(11).to_s(11).should == str
+ end
+
+ it "parses a String in base 12" do
+ str = "1234567890ab" * 8
+ str.to_i(12).to_s(12).should == str
+ end
+
+ it "parses a String in base 13" do
+ str = "1234567890abc" * 7
+ str.to_i(13).to_s(13).should == str
+ end
+
+ it "parses a String in base 14" do
+ str = "1234567890abcd" * 7
+ str.to_i(14).to_s(14).should == str
+ end
+
+ it "parses a String in base 15" do
+ str = "1234567890abcde" * 6
+ str.to_i(15).to_s(15).should == str
+ end
+
+ it "parses a String in base 16" do
+ str = "1234567890abcdef" * 6
+ str.to_i(16).to_s(16).should == str
+ end
+
+ it "parses a String in base 17" do
+ str = "1234567890abcdefg" * 5
+ str.to_i(17).to_s(17).should == str
+ end
+
+ it "parses a String in base 18" do
+ str = "1234567890abcdefgh" * 5
+ str.to_i(18).to_s(18).should == str
+ end
+
+ it "parses a String in base 19" do
+ str = "1234567890abcdefghi" * 5
+ str.to_i(19).to_s(19).should == str
+ end
+
+ it "parses a String in base 20" do
+ str = "1234567890abcdefghij" * 5
+ str.to_i(20).to_s(20).should == str
+ end
+
+ it "parses a String in base 21" do
+ str = "1234567890abcdefghijk" * 4
+ str.to_i(21).to_s(21).should == str
+ end
+
+ it "parses a String in base 22" do
+ str = "1234567890abcdefghijkl" * 4
+ str.to_i(22).to_s(22).should == str
+ end
+
+ it "parses a String in base 23" do
+ str = "1234567890abcdefghijklm" * 4
+ str.to_i(23).to_s(23).should == str
+ end
+
+ it "parses a String in base 24" do
+ str = "1234567890abcdefghijklmn" * 4
+ str.to_i(24).to_s(24).should == str
+ end
+
+ it "parses a String in base 25" do
+ str = "1234567890abcdefghijklmno" * 4
+ str.to_i(25).to_s(25).should == str
+ end
+
+ it "parses a String in base 26" do
+ str = "1234567890abcdefghijklmnop" * 3
+ str.to_i(26).to_s(26).should == str
+ end
+
+ it "parses a String in base 27" do
+ str = "1234567890abcdefghijklmnopq" * 3
+ str.to_i(27).to_s(27).should == str
+ end
+
+ it "parses a String in base 28" do
+ str = "1234567890abcdefghijklmnopqr" * 3
+ str.to_i(28).to_s(28).should == str
+ end
+
+ it "parses a String in base 29" do
+ str = "1234567890abcdefghijklmnopqrs" * 3
+ str.to_i(29).to_s(29).should == str
+ end
+
+ it "parses a String in base 30" do
+ str = "1234567890abcdefghijklmnopqrst" * 3
+ str.to_i(30).to_s(30).should == str
+ end
+
+ it "parses a String in base 31" do
+ str = "1234567890abcdefghijklmnopqrstu" * 3
+ str.to_i(31).to_s(31).should == str
+ end
+
+ it "parses a String in base 32" do
+ str = "1234567890abcdefghijklmnopqrstuv" * 3
+ str.to_i(32).to_s(32).should == str
+ end
+
+ it "parses a String in base 33" do
+ str = "1234567890abcdefghijklmnopqrstuvw" * 3
+ str.to_i(33).to_s(33).should == str
+ end
+
+ it "parses a String in base 34" do
+ str = "1234567890abcdefghijklmnopqrstuvwx" * 2
+ str.to_i(34).to_s(34).should == str
+ end
+
+ it "parses a String in base 35" do
+ str = "1234567890abcdefghijklmnopqrstuvwxy" * 2
+ str.to_i(35).to_s(35).should == str
+ end
+
+ it "parses a String in base 36" do
+ str = "1234567890abcdefghijklmnopqrstuvwxyz" * 2
+ str.to_i(36).to_s(36).should == str
+ end
+end
diff --git a/spec/ruby/core/string/to_r_spec.rb b/spec/ruby/core/string/to_r_spec.rb
new file mode 100644
index 0000000000..fb7c9d108e
--- /dev/null
+++ b/spec/ruby/core/string/to_r_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+
+describe "String#to_r" do
+ it "returns a Rational object" do
+ String.new.to_r.should.instance_of?(Rational)
+ end
+
+ it "returns (0/1) for the empty String" do
+ "".to_r.should == Rational(0, 1)
+ end
+
+ it "returns (n/1) for a String starting with a decimal _n_" do
+ "2".to_r.should == Rational(2, 1)
+ "1765".to_r.should == Rational(1765, 1)
+ end
+
+ it "ignores trailing characters" do
+ "2 foo".to_r.should == Rational(2, 1)
+ "1765, ".to_r.should == Rational(1765, 1)
+ end
+
+ it "ignores leading spaces" do
+ " 2".to_r.should == Rational(2, 1)
+ " 1765, ".to_r.should == Rational(1765, 1)
+ end
+
+ it "does not ignore arbitrary, non-numeric leading characters" do
+ "The rational form of 33 is...".to_r.should_not == Rational(33, 1)
+ "a1765, ".to_r.should_not == Rational(1765, 1)
+ end
+
+ it "treats leading hyphen as minus signs" do
+ "-20".to_r.should == Rational(-20, 1)
+ end
+
+ it "accepts leading plus signs" do
+ "+20".to_r.should == Rational(20, 1)
+ end
+
+ it "does not treat a leading period without a numeric prefix as a decimal point" do
+ ".9".to_r.should_not == Rational(8106479329266893, 9007199254740992)
+ end
+
+ it "understands decimal points" do
+ "3.33".to_r.should == Rational(333, 100)
+ "-3.33".to_r.should == Rational(-333, 100)
+ end
+
+ it "ignores underscores between numbers" do
+ "190_22".to_r.should == Rational(19022, 1)
+ "-190_22.7".to_r.should == Rational(-190227, 10)
+ end
+
+ it "understands a forward slash as separating the numerator from the denominator" do
+ "20/3".to_r.should == Rational(20, 3)
+ " -19.10/3".to_r.should == Rational(-191, 30)
+ end
+
+ it "returns (0/1) for Strings it can't parse" do
+ "glark".to_r.should == Rational(0,1)
+ end
+end
diff --git a/spec/ruby/core/string/to_s_spec.rb b/spec/ruby/core/string/to_s_spec.rb
new file mode 100644
index 0000000000..e5872745a8
--- /dev/null
+++ b/spec/ruby/core/string/to_s_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/to_s'
+
+describe "String#to_s" do
+ it_behaves_like :string_to_s, :to_s
+end
diff --git a/spec/ruby/core/string/to_str_spec.rb b/spec/ruby/core/string/to_str_spec.rb
new file mode 100644
index 0000000000..e24262a7ae
--- /dev/null
+++ b/spec/ruby/core/string/to_str_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/to_s'
+
+describe "String#to_str" do
+ it_behaves_like :string_to_s, :to_str
+end
diff --git a/spec/ruby/core/string/to_sym_spec.rb b/spec/ruby/core/string/to_sym_spec.rb
new file mode 100644
index 0000000000..f9135211ce
--- /dev/null
+++ b/spec/ruby/core/string/to_sym_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/to_sym'
+
+describe "String#to_sym" do
+ it_behaves_like :string_to_sym, :to_sym
+end
diff --git a/spec/ruby/core/string/tr_s_spec.rb b/spec/ruby/core/string/tr_s_spec.rb
new file mode 100644
index 0000000000..22a193ec4b
--- /dev/null
+++ b/spec/ruby/core/string/tr_s_spec.rb
@@ -0,0 +1,131 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#tr_s" do
+ it "returns a string processed according to tr with newly duplicate characters removed" do
+ "hello".tr_s('l', 'r').should == "hero"
+ "hello".tr_s('el', '*').should == "h*o"
+ "hello".tr_s('el', 'hx').should == "hhxo"
+ "hello".tr_s('o', '.').should == "hell."
+ end
+
+ it "accepts c1-c2 notation to denote ranges of characters" do
+ "hello".tr_s('a-y', 'b-z').should == "ifmp"
+ "123456789".tr_s("2-5", "abcdefg").should == "1abcd6789"
+ "hello ^--^".tr_s("e-", "__").should == "h_llo ^_^"
+ "hello ^--^".tr_s("---", "_").should == "hello ^_^"
+ end
+
+ it "accepts c1-c1 notation to denote range of one character" do
+ "hello".tr_s('e-e', 'x').should == "hxllo"
+ "123456789".tr_s("2-23","xy").should == "1xy456789"
+ "hello ^-^".tr_s("e-", "a-a_").should == "hallo ^_^"
+ "hello ^-^".tr_s("---o", "_a").should == "hella ^_^"
+ end
+
+ it "pads to_str with its last char if it is shorter than from_string" do
+ "this".tr_s("this", "x").should == "x"
+ end
+
+ it "translates chars not in from_string when it starts with a ^" do
+ "hello".tr_s('^aeiou', '*').should == "*e*o"
+ "123456789".tr_s("^345", "abc").should == "c345c"
+ "abcdefghijk".tr_s("^d-g", "9131").should == "1defg1"
+
+ "hello ^_^".tr_s("a-e^e", ".").should == "h.llo ._."
+ "hello ^_^".tr_s("^^", ".").should == ".^.^"
+ "hello ^_^".tr_s("^", "x").should == "hello x_x"
+ "hello ^-^".tr_s("^-^", "x").should == "x^-^"
+ "hello ^-^".tr_s("^^-^", "x").should == "x^x^"
+ "hello ^-^".tr_s("^---", "x").should == "x-x"
+ "hello ^-^".tr_s("^---l-o", "x").should == "xllox-x"
+ end
+
+ it "tries to convert from_str and to_str to strings using to_str" do
+ from_str = mock('ab')
+ from_str.should_receive(:to_str).and_return("ab")
+
+ to_str = mock('AB')
+ to_str.should_receive(:to_str).and_return("AB")
+
+ "bla".tr_s(from_str, to_str).should == "BlA"
+ end
+
+ it "returns String instances when called on a subclass" do
+ StringSpecs::MyString.new("hello").tr_s("e", "a").should.instance_of?(String)
+ end
+
+ # http://redmine.ruby-lang.org/issues/show/1839
+ it "can replace a 7-bit ASCII character with a multibyte one" do
+ a = "uber"
+ a.encoding.should == Encoding::UTF_8
+ b = a.tr_s("u","ü")
+ b.should == "über"
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "can replace multiple 7-bit ASCII characters with a multibyte one" do
+ a = "uuuber"
+ a.encoding.should == Encoding::UTF_8
+ b = a.tr_s("u","ü")
+ b.should == "über"
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "can replace a multibyte character with a single byte one" do
+ a = "über"
+ a.encoding.should == Encoding::UTF_8
+ b = a.tr_s("ü","u")
+ b.should == "uber"
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "can replace multiple multibyte characters with a single byte one" do
+ a = "üüüber"
+ a.encoding.should == Encoding::UTF_8
+ b = a.tr_s("ü","u")
+ b.should == "uber"
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "does not replace a multibyte character where part of the bytes match the tr string" do
+ str = "æ¤Žåæ·±å¤"
+ a = "\u0080\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008E\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009E\u009F"
+ b = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ"
+ str.tr_s(a, b).should == "æ¤Žåæ·±å¤"
+ end
+
+
+end
+
+describe "String#tr_s!" do
+ it "modifies self in place" do
+ s = "hello"
+ s.tr_s!("l", "r").should == "hero"
+ s.should == "hero"
+ end
+
+ it "returns nil if no modification was made" do
+ s = "hello"
+ s.tr_s!("za", "yb").should == nil
+ s.tr_s!("", "").should == nil
+ s.should == "hello"
+ end
+
+ it "does not modify self if from_str is empty" do
+ s = "hello"
+ s.tr_s!("", "").should == nil
+ s.should == "hello"
+ s.tr_s!("", "yb").should == nil
+ s.should == "hello"
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ s = "hello".freeze
+ -> { s.tr_s!("el", "ar") }.should.raise(FrozenError)
+ -> { s.tr_s!("l", "r") }.should.raise(FrozenError)
+ -> { s.tr_s!("", "") }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/tr_spec.rb b/spec/ruby/core/string/tr_spec.rb
new file mode 100644
index 0000000000..cb57c3851e
--- /dev/null
+++ b/spec/ruby/core/string/tr_spec.rb
@@ -0,0 +1,126 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#tr" do
+ it "returns a new string with the characters from from_string replaced by the ones in to_string" do
+ "hello".tr('aeiou', '*').should == "h*ll*"
+ "hello".tr('el', 'ip').should == "hippo"
+ "Lisp".tr("Lisp", "Ruby").should == "Ruby"
+ end
+
+ it "accepts c1-c2 notation to denote ranges of characters" do
+ "hello".tr('a-y', 'b-z').should == "ifmmp"
+ "123456789".tr("2-5","abcdefg").should == "1abcd6789"
+ "hello ^-^".tr("e-", "__").should == "h_llo ^_^"
+ "hello ^-^".tr("---", "_").should == "hello ^_^"
+ end
+
+ it "accepts c1-c1 notation to denote range of one character" do
+ "hello".tr('e-e', 'x').should == "hxllo"
+ "123456789".tr("2-23","xy").should == "1xy456789"
+ "hello ^-^".tr("e-", "a-a_").should == "hallo ^_^"
+ "hello ^-^".tr("---o", "_a").should == "hella ^_^"
+ end
+
+ it "pads to_str with its last char if it is shorter than from_string" do
+ "this".tr("this", "x").should == "xxxx"
+ "hello".tr("a-z", "A-H.").should == "HE..."
+ end
+
+ it "raises an ArgumentError a descending range in the replacement as containing just the start character" do
+ -> { "hello".tr("a-y", "z-b") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError a descending range in the source as empty" do
+ -> { "hello".tr("l-a", "z") }.should.raise(ArgumentError)
+ end
+
+ it "translates chars not in from_string when it starts with a ^" do
+ "hello".tr('^aeiou', '*').should == "*e**o"
+ "123456789".tr("^345", "abc").should == "cc345cccc"
+ "abcdefghijk".tr("^d-g", "9131").should == "111defg1111"
+
+ "hello ^_^".tr("a-e^e", ".").should == "h.llo ._."
+ "hello ^_^".tr("^^", ".").should == "......^.^"
+ "hello ^_^".tr("^", "x").should == "hello x_x"
+ "hello ^-^".tr("^-^", "x").should == "xxxxxx^-^"
+ "hello ^-^".tr("^^-^", "x").should == "xxxxxx^x^"
+ "hello ^-^".tr("^---", "x").should == "xxxxxxx-x"
+ "hello ^-^".tr("^---l-o", "x").should == "xxlloxx-x"
+ end
+
+ it "supports non-injective replacements" do
+ "hello".tr("helo", "1212").should == "12112"
+ end
+
+ it "tries to convert from_str and to_str to strings using to_str" do
+ from_str = mock('ab')
+ from_str.should_receive(:to_str).and_return("ab")
+
+ to_str = mock('AB')
+ to_str.should_receive(:to_str).and_return("AB")
+
+ "bla".tr(from_str, to_str).should == "BlA"
+ end
+
+ it "returns Stringinstances when called on a subclass" do
+ StringSpecs::MyString.new("hello").tr("e", "a").should.instance_of?(String)
+ end
+
+ # http://redmine.ruby-lang.org/issues/show/1839
+ it "can replace a 7-bit ASCII character with a multibyte one" do
+ a = "uber"
+ a.encoding.should == Encoding::UTF_8
+ b = a.tr("u","ü")
+ b.should == "über"
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "can replace a multibyte character with a single byte one" do
+ a = "über"
+ a.encoding.should == Encoding::UTF_8
+ b = a.tr("ü","u")
+ b.should == "uber"
+ b.encoding.should == Encoding::UTF_8
+ end
+
+ it "does not replace a multibyte character where part of the bytes match the tr string" do
+ str = "æ¤Žåæ·±å¤"
+ a = "\u0080\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008E\u0091\u0092\u0093\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009E\u009F"
+ b = "€‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’“â€â€¢â€“—˜™š›œžŸ"
+ str.tr(a, b).should == "æ¤Žåæ·±å¤"
+ end
+
+end
+
+describe "String#tr!" do
+ it "modifies self in place" do
+ s = "abcdefghijklmnopqR"
+ s.tr!("cdefg", "12").should == "ab12222hijklmnopqR"
+ s.should == "ab12222hijklmnopqR"
+ end
+
+ it "returns nil if no modification was made" do
+ s = "hello"
+ s.tr!("za", "yb").should == nil
+ s.tr!("", "").should == nil
+ s.should == "hello"
+ end
+
+ it "does not modify self if from_str is empty" do
+ s = "hello"
+ s.tr!("", "").should == nil
+ s.should == "hello"
+ s.tr!("", "yb").should == nil
+ s.should == "hello"
+ end
+
+ it "raises a FrozenError if self is frozen" do
+ s = "abcdefghijklmnopqR".freeze
+ -> { s.tr!("cdefg", "12") }.should.raise(FrozenError)
+ -> { s.tr!("R", "S") }.should.raise(FrozenError)
+ -> { s.tr!("", "") }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/try_convert_spec.rb b/spec/ruby/core/string/try_convert_spec.rb
new file mode 100644
index 0000000000..0c0219cd2e
--- /dev/null
+++ b/spec/ruby/core/string/try_convert_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String.try_convert" do
+ it "returns the argument if it's a String" do
+ x = String.new
+ String.try_convert(x).should.equal?(x)
+ end
+
+ it "returns the argument if it's a kind of String" do
+ x = StringSpecs::MyString.new
+ String.try_convert(x).should.equal?(x)
+ end
+
+ it "returns nil when the argument does not respond to #to_str" do
+ String.try_convert(Object.new).should == nil
+ end
+
+ it "sends #to_str to the argument and returns the result if it's nil" do
+ obj = mock("to_str")
+ obj.should_receive(:to_str).and_return(nil)
+ String.try_convert(obj).should == nil
+ end
+
+ it "sends #to_str to the argument and returns the result if it's a String" do
+ x = String.new
+ obj = mock("to_str")
+ obj.should_receive(:to_str).and_return(x)
+ String.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_str to the argument and returns the result if it's a kind of String" do
+ x = StringSpecs::MyString.new
+ obj = mock("to_str")
+ obj.should_receive(:to_str).and_return(x)
+ String.try_convert(obj).should.equal?(x)
+ end
+
+ it "sends #to_str to the argument and raises TypeError if it's not a kind of String" do
+ obj = mock("to_str")
+ obj.should_receive(:to_str).and_return(Object.new)
+ -> { String.try_convert obj }.should raise_consistent_error(TypeError, "can't convert MockObject into String (MockObject#to_str gives Object)")
+ end
+
+ it "does not rescue exceptions raised by #to_str" do
+ obj = mock("to_str")
+ obj.should_receive(:to_str).and_raise(RuntimeError)
+ -> { String.try_convert obj }.should.raise(RuntimeError)
+ end
+end
diff --git a/spec/ruby/core/string/uminus_spec.rb b/spec/ruby/core/string/uminus_spec.rb
new file mode 100644
index 0000000000..46d88f6704
--- /dev/null
+++ b/spec/ruby/core/string/uminus_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/dedup'
+
+describe 'String#-@' do
+ it_behaves_like :string_dedup, :-@
+end
diff --git a/spec/ruby/core/string/undump_spec.rb b/spec/ruby/core/string/undump_spec.rb
new file mode 100644
index 0000000000..8516e24b3b
--- /dev/null
+++ b/spec/ruby/core/string/undump_spec.rb
@@ -0,0 +1,441 @@
+# encoding: utf-8
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#undump" do
+ it "does not take into account if a string is frozen" do
+ '"foo"'.freeze.undump.should_not.frozen?
+ end
+
+ it "always returns String instance" do
+ StringSpecs::MyString.new('"foo"').undump.should.instance_of?(String)
+ end
+
+ it "strips outer \"" do
+ '"foo"'.undump.should == 'foo'
+ end
+
+ it "returns a string with special characters in \\<char> notation replaced with the characters" do
+ [ ['"\\a"', "\a"],
+ ['"\\b"', "\b"],
+ ['"\\t"', "\t"],
+ ['"\\n"', "\n"],
+ ['"\\v"', "\v"],
+ ['"\\f"', "\f"],
+ ['"\\r"', "\r"],
+ ['"\\e"', "\e"]
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with unescaped sequences \" and \\" do
+ [ ['"\\""' , "\""],
+ ['"\\\\"', "\\"]
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with unescaped sequences \\#<char> when # is followed by $, @, {" do
+ [ ['"\\#$PATH"', "\#$PATH"],
+ ['"\\#@a"', "\#@a"],
+ ['"\\#@@a"', "\#@@a"],
+ ['"\\#{a}"', "\#{a}"]
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with # not escaped when followed by any other character" do
+ [ ['"#"', '#'],
+ ['"#1"', '#1']
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with printable non-alphanumeric characters" do
+ [ ['" "', ' '],
+ ['"!"', '!'],
+ ['"$"', '$'],
+ ['"%"', '%'],
+ ['"&"', '&'],
+ ['"\'"', '\''],
+ ['"("', '('],
+ ['")"', ')'],
+ ['"*"', '*'],
+ ['"+"', '+'],
+ ['","', ','],
+ ['"-"', '-'],
+ ['"."', '.'],
+ ['"/"', '/'],
+ ['":"', ':'],
+ ['";"', ';'],
+ ['"<"', '<'],
+ ['"="', '='],
+ ['">"', '>'],
+ ['"?"', '?'],
+ ['"@"', '@'],
+ ['"["', '['],
+ ['"]"', ']'],
+ ['"^"', '^'],
+ ['"_"', '_'],
+ ['"`"', '`'],
+ ['"{"', '{'],
+ ['"|"', '|'],
+ ['"}"', '}'],
+ ['"~"', '~']
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with numeric characters unescaped" do
+ [ ['"0"', "0"],
+ ['"1"', "1"],
+ ['"2"', "2"],
+ ['"3"', "3"],
+ ['"4"', "4"],
+ ['"5"', "5"],
+ ['"6"', "6"],
+ ['"7"', "7"],
+ ['"8"', "8"],
+ ['"9"', "9"],
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with upper-case alpha characters unescaped" do
+ [ ['"A"', 'A'],
+ ['"B"', 'B'],
+ ['"C"', 'C'],
+ ['"D"', 'D'],
+ ['"E"', 'E'],
+ ['"F"', 'F'],
+ ['"G"', 'G'],
+ ['"H"', 'H'],
+ ['"I"', 'I'],
+ ['"J"', 'J'],
+ ['"K"', 'K'],
+ ['"L"', 'L'],
+ ['"M"', 'M'],
+ ['"N"', 'N'],
+ ['"O"', 'O'],
+ ['"P"', 'P'],
+ ['"Q"', 'Q'],
+ ['"R"', 'R'],
+ ['"S"', 'S'],
+ ['"T"', 'T'],
+ ['"U"', 'U'],
+ ['"V"', 'V'],
+ ['"W"', 'W'],
+ ['"X"', 'X'],
+ ['"Y"', 'Y'],
+ ['"Z"', 'Z']
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with lower-case alpha characters unescaped" do
+ [ ['"a"', 'a'],
+ ['"b"', 'b'],
+ ['"c"', 'c'],
+ ['"d"', 'd'],
+ ['"e"', 'e'],
+ ['"f"', 'f'],
+ ['"g"', 'g'],
+ ['"h"', 'h'],
+ ['"i"', 'i'],
+ ['"j"', 'j'],
+ ['"k"', 'k'],
+ ['"l"', 'l'],
+ ['"m"', 'm'],
+ ['"n"', 'n'],
+ ['"o"', 'o'],
+ ['"p"', 'p'],
+ ['"q"', 'q'],
+ ['"r"', 'r'],
+ ['"s"', 's'],
+ ['"t"', 't'],
+ ['"u"', 'u'],
+ ['"v"', 'v'],
+ ['"w"', 'w'],
+ ['"x"', 'x'],
+ ['"y"', 'y'],
+ ['"z"', 'z']
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with \\x notation replaced with non-printing ASCII character" do
+ [ ['"\\x00"', 0000.chr.force_encoding('utf-8')],
+ ['"\\x01"', 0001.chr.force_encoding('utf-8')],
+ ['"\\x02"', 0002.chr.force_encoding('utf-8')],
+ ['"\\x03"', 0003.chr.force_encoding('utf-8')],
+ ['"\\x04"', 0004.chr.force_encoding('utf-8')],
+ ['"\\x05"', 0005.chr.force_encoding('utf-8')],
+ ['"\\x06"', 0006.chr.force_encoding('utf-8')],
+ ['"\\x0E"', 0016.chr.force_encoding('utf-8')],
+ ['"\\x0F"', 0017.chr.force_encoding('utf-8')],
+ ['"\\x10"', 0020.chr.force_encoding('utf-8')],
+ ['"\\x11"', 0021.chr.force_encoding('utf-8')],
+ ['"\\x12"', 0022.chr.force_encoding('utf-8')],
+ ['"\\x13"', 0023.chr.force_encoding('utf-8')],
+ ['"\\x14"', 0024.chr.force_encoding('utf-8')],
+ ['"\\x15"', 0025.chr.force_encoding('utf-8')],
+ ['"\\x16"', 0026.chr.force_encoding('utf-8')],
+ ['"\\x17"', 0027.chr.force_encoding('utf-8')],
+ ['"\\x18"', 0030.chr.force_encoding('utf-8')],
+ ['"\\x19"', 0031.chr.force_encoding('utf-8')],
+ ['"\\x1A"', 0032.chr.force_encoding('utf-8')],
+ ['"\\x1C"', 0034.chr.force_encoding('utf-8')],
+ ['"\\x1D"', 0035.chr.force_encoding('utf-8')],
+ ['"\\x1E"', 0036.chr.force_encoding('utf-8')],
+ ['"\\x1F"', 0037.chr.force_encoding('utf-8')],
+ ['"\\x7F"', 0177.chr.force_encoding('utf-8')],
+ ['"\\x80"', 0200.chr.force_encoding('utf-8')],
+ ['"\\x81"', 0201.chr.force_encoding('utf-8')],
+ ['"\\x82"', 0202.chr.force_encoding('utf-8')],
+ ['"\\x83"', 0203.chr.force_encoding('utf-8')],
+ ['"\\x84"', 0204.chr.force_encoding('utf-8')],
+ ['"\\x85"', 0205.chr.force_encoding('utf-8')],
+ ['"\\x86"', 0206.chr.force_encoding('utf-8')],
+ ['"\\x87"', 0207.chr.force_encoding('utf-8')],
+ ['"\\x88"', 0210.chr.force_encoding('utf-8')],
+ ['"\\x89"', 0211.chr.force_encoding('utf-8')],
+ ['"\\x8A"', 0212.chr.force_encoding('utf-8')],
+ ['"\\x8B"', 0213.chr.force_encoding('utf-8')],
+ ['"\\x8C"', 0214.chr.force_encoding('utf-8')],
+ ['"\\x8D"', 0215.chr.force_encoding('utf-8')],
+ ['"\\x8E"', 0216.chr.force_encoding('utf-8')],
+ ['"\\x8F"', 0217.chr.force_encoding('utf-8')],
+ ['"\\x90"', 0220.chr.force_encoding('utf-8')],
+ ['"\\x91"', 0221.chr.force_encoding('utf-8')],
+ ['"\\x92"', 0222.chr.force_encoding('utf-8')],
+ ['"\\x93"', 0223.chr.force_encoding('utf-8')],
+ ['"\\x94"', 0224.chr.force_encoding('utf-8')],
+ ['"\\x95"', 0225.chr.force_encoding('utf-8')],
+ ['"\\x96"', 0226.chr.force_encoding('utf-8')],
+ ['"\\x97"', 0227.chr.force_encoding('utf-8')],
+ ['"\\x98"', 0230.chr.force_encoding('utf-8')],
+ ['"\\x99"', 0231.chr.force_encoding('utf-8')],
+ ['"\\x9A"', 0232.chr.force_encoding('utf-8')],
+ ['"\\x9B"', 0233.chr.force_encoding('utf-8')],
+ ['"\\x9C"', 0234.chr.force_encoding('utf-8')],
+ ['"\\x9D"', 0235.chr.force_encoding('utf-8')],
+ ['"\\x9E"', 0236.chr.force_encoding('utf-8')],
+ ['"\\x9F"', 0237.chr.force_encoding('utf-8')],
+ ['"\\xA0"', 0240.chr.force_encoding('utf-8')],
+ ['"\\xA1"', 0241.chr.force_encoding('utf-8')],
+ ['"\\xA2"', 0242.chr.force_encoding('utf-8')],
+ ['"\\xA3"', 0243.chr.force_encoding('utf-8')],
+ ['"\\xA4"', 0244.chr.force_encoding('utf-8')],
+ ['"\\xA5"', 0245.chr.force_encoding('utf-8')],
+ ['"\\xA6"', 0246.chr.force_encoding('utf-8')],
+ ['"\\xA7"', 0247.chr.force_encoding('utf-8')],
+ ['"\\xA8"', 0250.chr.force_encoding('utf-8')],
+ ['"\\xA9"', 0251.chr.force_encoding('utf-8')],
+ ['"\\xAA"', 0252.chr.force_encoding('utf-8')],
+ ['"\\xAB"', 0253.chr.force_encoding('utf-8')],
+ ['"\\xAC"', 0254.chr.force_encoding('utf-8')],
+ ['"\\xAD"', 0255.chr.force_encoding('utf-8')],
+ ['"\\xAE"', 0256.chr.force_encoding('utf-8')],
+ ['"\\xAF"', 0257.chr.force_encoding('utf-8')],
+ ['"\\xB0"', 0260.chr.force_encoding('utf-8')],
+ ['"\\xB1"', 0261.chr.force_encoding('utf-8')],
+ ['"\\xB2"', 0262.chr.force_encoding('utf-8')],
+ ['"\\xB3"', 0263.chr.force_encoding('utf-8')],
+ ['"\\xB4"', 0264.chr.force_encoding('utf-8')],
+ ['"\\xB5"', 0265.chr.force_encoding('utf-8')],
+ ['"\\xB6"', 0266.chr.force_encoding('utf-8')],
+ ['"\\xB7"', 0267.chr.force_encoding('utf-8')],
+ ['"\\xB8"', 0270.chr.force_encoding('utf-8')],
+ ['"\\xB9"', 0271.chr.force_encoding('utf-8')],
+ ['"\\xBA"', 0272.chr.force_encoding('utf-8')],
+ ['"\\xBB"', 0273.chr.force_encoding('utf-8')],
+ ['"\\xBC"', 0274.chr.force_encoding('utf-8')],
+ ['"\\xBD"', 0275.chr.force_encoding('utf-8')],
+ ['"\\xBE"', 0276.chr.force_encoding('utf-8')],
+ ['"\\xBF"', 0277.chr.force_encoding('utf-8')],
+ ['"\\xC0"', 0300.chr.force_encoding('utf-8')],
+ ['"\\xC1"', 0301.chr.force_encoding('utf-8')],
+ ['"\\xC2"', 0302.chr.force_encoding('utf-8')],
+ ['"\\xC3"', 0303.chr.force_encoding('utf-8')],
+ ['"\\xC4"', 0304.chr.force_encoding('utf-8')],
+ ['"\\xC5"', 0305.chr.force_encoding('utf-8')],
+ ['"\\xC6"', 0306.chr.force_encoding('utf-8')],
+ ['"\\xC7"', 0307.chr.force_encoding('utf-8')],
+ ['"\\xC8"', 0310.chr.force_encoding('utf-8')],
+ ['"\\xC9"', 0311.chr.force_encoding('utf-8')],
+ ['"\\xCA"', 0312.chr.force_encoding('utf-8')],
+ ['"\\xCB"', 0313.chr.force_encoding('utf-8')],
+ ['"\\xCC"', 0314.chr.force_encoding('utf-8')],
+ ['"\\xCD"', 0315.chr.force_encoding('utf-8')],
+ ['"\\xCE"', 0316.chr.force_encoding('utf-8')],
+ ['"\\xCF"', 0317.chr.force_encoding('utf-8')],
+ ['"\\xD0"', 0320.chr.force_encoding('utf-8')],
+ ['"\\xD1"', 0321.chr.force_encoding('utf-8')],
+ ['"\\xD2"', 0322.chr.force_encoding('utf-8')],
+ ['"\\xD3"', 0323.chr.force_encoding('utf-8')],
+ ['"\\xD4"', 0324.chr.force_encoding('utf-8')],
+ ['"\\xD5"', 0325.chr.force_encoding('utf-8')],
+ ['"\\xD6"', 0326.chr.force_encoding('utf-8')],
+ ['"\\xD7"', 0327.chr.force_encoding('utf-8')],
+ ['"\\xD8"', 0330.chr.force_encoding('utf-8')],
+ ['"\\xD9"', 0331.chr.force_encoding('utf-8')],
+ ['"\\xDA"', 0332.chr.force_encoding('utf-8')],
+ ['"\\xDB"', 0333.chr.force_encoding('utf-8')],
+ ['"\\xDC"', 0334.chr.force_encoding('utf-8')],
+ ['"\\xDD"', 0335.chr.force_encoding('utf-8')],
+ ['"\\xDE"', 0336.chr.force_encoding('utf-8')],
+ ['"\\xDF"', 0337.chr.force_encoding('utf-8')],
+ ['"\\xE0"', 0340.chr.force_encoding('utf-8')],
+ ['"\\xE1"', 0341.chr.force_encoding('utf-8')],
+ ['"\\xE2"', 0342.chr.force_encoding('utf-8')],
+ ['"\\xE3"', 0343.chr.force_encoding('utf-8')],
+ ['"\\xE4"', 0344.chr.force_encoding('utf-8')],
+ ['"\\xE5"', 0345.chr.force_encoding('utf-8')],
+ ['"\\xE6"', 0346.chr.force_encoding('utf-8')],
+ ['"\\xE7"', 0347.chr.force_encoding('utf-8')],
+ ['"\\xE8"', 0350.chr.force_encoding('utf-8')],
+ ['"\\xE9"', 0351.chr.force_encoding('utf-8')],
+ ['"\\xEA"', 0352.chr.force_encoding('utf-8')],
+ ['"\\xEB"', 0353.chr.force_encoding('utf-8')],
+ ['"\\xEC"', 0354.chr.force_encoding('utf-8')],
+ ['"\\xED"', 0355.chr.force_encoding('utf-8')],
+ ['"\\xEE"', 0356.chr.force_encoding('utf-8')],
+ ['"\\xEF"', 0357.chr.force_encoding('utf-8')],
+ ['"\\xF0"', 0360.chr.force_encoding('utf-8')],
+ ['"\\xF1"', 0361.chr.force_encoding('utf-8')],
+ ['"\\xF2"', 0362.chr.force_encoding('utf-8')],
+ ['"\\xF3"', 0363.chr.force_encoding('utf-8')],
+ ['"\\xF4"', 0364.chr.force_encoding('utf-8')],
+ ['"\\xF5"', 0365.chr.force_encoding('utf-8')],
+ ['"\\xF6"', 0366.chr.force_encoding('utf-8')],
+ ['"\\xF7"', 0367.chr.force_encoding('utf-8')],
+ ['"\\xF8"', 0370.chr.force_encoding('utf-8')],
+ ['"\\xF9"', 0371.chr.force_encoding('utf-8')],
+ ['"\\xFA"', 0372.chr.force_encoding('utf-8')],
+ ['"\\xFB"', 0373.chr.force_encoding('utf-8')],
+ ['"\\xFC"', 0374.chr.force_encoding('utf-8')],
+ ['"\\xFD"', 0375.chr.force_encoding('utf-8')],
+ ['"\\xFE"', 0376.chr.force_encoding('utf-8')],
+ ['"\\xFF"', 0377.chr.force_encoding('utf-8')]
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with \\u{} notation replaced with multi-byte UTF-8 characters" do
+ [ ['"\u{80}"', 0200.chr('utf-8')],
+ ['"\u{81}"', 0201.chr('utf-8')],
+ ['"\u{82}"', 0202.chr('utf-8')],
+ ['"\u{83}"', 0203.chr('utf-8')],
+ ['"\u{84}"', 0204.chr('utf-8')],
+ ['"\u{86}"', 0206.chr('utf-8')],
+ ['"\u{87}"', 0207.chr('utf-8')],
+ ['"\u{88}"', 0210.chr('utf-8')],
+ ['"\u{89}"', 0211.chr('utf-8')],
+ ['"\u{8a}"', 0212.chr('utf-8')],
+ ['"\u{8b}"', 0213.chr('utf-8')],
+ ['"\u{8c}"', 0214.chr('utf-8')],
+ ['"\u{8d}"', 0215.chr('utf-8')],
+ ['"\u{8e}"', 0216.chr('utf-8')],
+ ['"\u{8f}"', 0217.chr('utf-8')],
+ ['"\u{90}"', 0220.chr('utf-8')],
+ ['"\u{91}"', 0221.chr('utf-8')],
+ ['"\u{92}"', 0222.chr('utf-8')],
+ ['"\u{93}"', 0223.chr('utf-8')],
+ ['"\u{94}"', 0224.chr('utf-8')],
+ ['"\u{95}"', 0225.chr('utf-8')],
+ ['"\u{96}"', 0226.chr('utf-8')],
+ ['"\u{97}"', 0227.chr('utf-8')],
+ ['"\u{98}"', 0230.chr('utf-8')],
+ ['"\u{99}"', 0231.chr('utf-8')],
+ ['"\u{9a}"', 0232.chr('utf-8')],
+ ['"\u{9b}"', 0233.chr('utf-8')],
+ ['"\u{9c}"', 0234.chr('utf-8')],
+ ['"\u{9d}"', 0235.chr('utf-8')],
+ ['"\u{9e}"', 0236.chr('utf-8')],
+ ['"\u{9f}"', 0237.chr('utf-8')],
+ ].should be_computed_by(:undump)
+ end
+
+ it "returns a string with \\uXXXX notation replaced with multi-byte UTF-8 characters" do
+ [ ['"\u0080"', 0200.chr('utf-8')],
+ ['"\u0081"', 0201.chr('utf-8')],
+ ['"\u0082"', 0202.chr('utf-8')],
+ ['"\u0083"', 0203.chr('utf-8')],
+ ['"\u0084"', 0204.chr('utf-8')],
+ ['"\u0086"', 0206.chr('utf-8')],
+ ['"\u0087"', 0207.chr('utf-8')],
+ ['"\u0088"', 0210.chr('utf-8')],
+ ['"\u0089"', 0211.chr('utf-8')],
+ ['"\u008a"', 0212.chr('utf-8')],
+ ['"\u008b"', 0213.chr('utf-8')],
+ ['"\u008c"', 0214.chr('utf-8')],
+ ['"\u008d"', 0215.chr('utf-8')],
+ ['"\u008e"', 0216.chr('utf-8')],
+ ['"\u008f"', 0217.chr('utf-8')],
+ ['"\u0090"', 0220.chr('utf-8')],
+ ['"\u0091"', 0221.chr('utf-8')],
+ ['"\u0092"', 0222.chr('utf-8')],
+ ['"\u0093"', 0223.chr('utf-8')],
+ ['"\u0094"', 0224.chr('utf-8')],
+ ['"\u0095"', 0225.chr('utf-8')],
+ ['"\u0096"', 0226.chr('utf-8')],
+ ['"\u0097"', 0227.chr('utf-8')],
+ ['"\u0098"', 0230.chr('utf-8')],
+ ['"\u0099"', 0231.chr('utf-8')],
+ ['"\u009a"', 0232.chr('utf-8')],
+ ['"\u009b"', 0233.chr('utf-8')],
+ ['"\u009c"', 0234.chr('utf-8')],
+ ['"\u009d"', 0235.chr('utf-8')],
+ ['"\u009e"', 0236.chr('utf-8')],
+ ['"\u009f"', 0237.chr('utf-8')],
+ ].should be_computed_by(:undump)
+ end
+
+ it "undumps correctly string produced from non ASCII-compatible one" do
+ s = "\u{876}".encode('utf-16be')
+ s.dump.undump.should == s
+
+ '"\\bv".force_encoding("UTF-16BE")'.undump.should == "\u0876".encode('utf-16be')
+ end
+
+ it "returns a String in the same encoding as self" do
+ '"foo"'.encode("ISO-8859-1").undump.encoding.should == Encoding::ISO_8859_1
+ '"foo"'.encode('windows-1251').undump.encoding.should == Encoding::Windows_1251
+ end
+
+ describe "Limitations" do
+ it "cannot undump non ASCII-compatible string" do
+ -> { '"foo"'.encode('utf-16le').undump }.should.raise(Encoding::CompatibilityError)
+ end
+ end
+
+ describe "invalid dump" do
+ it "raises RuntimeError exception if wrapping \" are missing" do
+ -> { 'foo'.undump }.should.raise(RuntimeError, /invalid dumped string/)
+ -> { '"foo'.undump }.should.raise(RuntimeError, /unterminated dumped string/)
+ -> { 'foo"'.undump }.should.raise(RuntimeError, /invalid dumped string/)
+ -> { "'foo'".undump }.should.raise(RuntimeError, /invalid dumped string/)
+ end
+
+ it "raises RuntimeError if there is incorrect \\x sequence" do
+ -> { '"\x"'.undump }.should.raise(RuntimeError, /invalid hex escape/)
+ -> { '"\\x3y"'.undump }.should.raise(RuntimeError, /invalid hex escape/)
+ end
+
+ it "raises RuntimeError in there is incorrect \\u sequence" do
+ -> { '"\\u"'.undump }.should.raise(RuntimeError, /invalid Unicode escape/)
+ -> { '"\\u{"'.undump }.should.raise(RuntimeError, /invalid Unicode escape/)
+ -> { '"\\u{3042"'.undump }.should.raise(RuntimeError, /invalid Unicode escape/)
+ -> { '"\\u"'.undump }.should.raise(RuntimeError, /invalid Unicode escape/)
+ end
+
+ it "raises RuntimeError if there is malformed dump of non ASCII-compatible string" do
+ -> { '"".force_encoding("BINARY"'.undump }.should.raise(RuntimeError, /invalid dumped string/)
+ -> { '"".force_encoding("Unknown")'.undump }.should.raise(RuntimeError, /dumped string has unknown encoding name/)
+ -> { '"".force_encoding()'.undump }.should.raise(RuntimeError, /invalid dumped string/)
+ end
+
+ it "raises RuntimeError if string contains \0 character" do
+ -> { "\"foo\0\"".undump }.should.raise(RuntimeError, /string contains null byte/)
+ end
+
+ it "raises RuntimeError if string contains non ASCII character" do
+ -> { "\"\u3042\"".undump }.should.raise(RuntimeError, /non-ASCII character detected/)
+ end
+
+ it "raises RuntimeError if there are some excessive \"" do
+ -> { '" "" "'.undump }.should.raise(RuntimeError, /invalid dumped string/)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/unicode_normalize_spec.rb b/spec/ruby/core/string/unicode_normalize_spec.rb
new file mode 100644
index 0000000000..92b3a46b43
--- /dev/null
+++ b/spec/ruby/core/string/unicode_normalize_spec.rb
@@ -0,0 +1,116 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+# Examples taken from http://www.unicode.org/reports/tr15/#Norm_Forms
+
+describe "String#unicode_normalize" do
+ before :each do
+ @accented_f = "\u1e9b\u0323"
+ @angstrom = "\u212b"
+ @ohm = "\u2126"
+ end
+
+ it "normalizes code points in the string according to the form that is specified" do
+ @accented_f.unicode_normalize(:nfc).should == "\u1e9b\u0323"
+ @accented_f.unicode_normalize(:nfd).should == "\u017f\u0323\u0307"
+ @accented_f.unicode_normalize(:nfkc).should == "\u1e69"
+ @accented_f.unicode_normalize(:nfkd).should == "\u0073\u0323\u0307"
+ end
+
+ it "defaults to the nfc normalization form if no forms are specified" do
+ @accented_f.unicode_normalize.should == "\u1e9b\u0323"
+ @angstrom.unicode_normalize.should == "\u00c5"
+ @ohm.unicode_normalize.should == "\u03a9"
+ end
+
+ # http://unicode.org/faq/normalization.html#6
+ context "returns normalized form of string by default" do
+ it "03D3 (Ï“) GREEK UPSILON WITH ACUTE AND HOOK SYMBOL" do
+ "\u03D3".unicode_normalize(:nfc).should == "\u03D3"
+ "\u03D3".unicode_normalize(:nfd).should == "\u03D2\u0301"
+ "\u03D3".unicode_normalize(:nfkc).should == "\u038E"
+ "\u03D3".unicode_normalize(:nfkd).should == "\u03A5\u0301"
+ end
+
+ it "03D4 (Ï”) GREEK UPSILON WITH DIAERESIS AND HOOK SYMBOL" do
+ "\u03D4".unicode_normalize(:nfc).should == "\u03D4"
+ "\u03D4".unicode_normalize(:nfd).should == "\u03D2\u0308"
+ "\u03D4".unicode_normalize(:nfkc).should == "\u03AB"
+ "\u03D4".unicode_normalize(:nfkd).should == "\u03A5\u0308"
+ end
+
+ it "1E9B (ẛ) LATIN SMALL LETTER LONG S WITH DOT ABOVE" do
+ "\u1E9B".unicode_normalize(:nfc).should == "\u1E9B"
+ "\u1E9B".unicode_normalize(:nfd).should == "\u017F\u0307"
+ "\u1E9B".unicode_normalize(:nfkc).should == "\u1E61"
+ "\u1E9B".unicode_normalize(:nfkd).should == "\u0073\u0307"
+ end
+ end
+
+ it "raises an Encoding::CompatibilityError if string is not in an unicode encoding" do
+ -> do
+ [0xE0].pack('C').force_encoding("ISO-8859-1").unicode_normalize(:nfd)
+ end.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "raises an ArgumentError if the specified form is invalid" do
+ -> {
+ @angstrom.unicode_normalize(:invalid_form)
+ }.should.raise(ArgumentError)
+ end
+end
+
+describe "String#unicode_normalize!" do
+ it "normalizes code points and modifies the receiving string" do
+ angstrom = "\u212b"
+ angstrom.unicode_normalize!
+ angstrom.should == "\u00c5"
+ angstrom.should_not == "\u212b"
+ end
+
+ it "modifies original string (nfc)" do
+ str = "a\u0300"
+ str.unicode_normalize!(:nfc)
+
+ str.should_not == "a\u0300"
+ str.should == "à"
+ end
+
+ it "modifies self in place (nfd)" do
+ str = "\u00E0"
+ str.unicode_normalize!(:nfd)
+
+ str.should_not == "\u00E0"
+ str.should == "a\u0300"
+ end
+
+ it "modifies self in place (nfkc)" do
+ str = "\u1E9B\u0323"
+ str.unicode_normalize!(:nfkc)
+
+ str.should_not == "\u1E9B\u0323"
+ str.should == "\u1E69"
+ end
+
+ it "modifies self in place (nfkd)" do
+ str = "\u1E9B\u0323"
+ str.unicode_normalize!(:nfkd)
+
+ str.should_not == "\u1E9B\u0323"
+ str.should == "s\u0323\u0307"
+ end
+
+ it "raises an Encoding::CompatibilityError if the string is not in an unicode encoding" do
+ -> {
+ [0xE0].pack('C').force_encoding("ISO-8859-1").unicode_normalize!
+ }.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "raises an ArgumentError if the specified form is invalid" do
+ ohm = "\u2126"
+ -> {
+ ohm.unicode_normalize!(:invalid_form)
+ }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/unicode_normalized_spec.rb b/spec/ruby/core/string/unicode_normalized_spec.rb
new file mode 100644
index 0000000000..3ca27d35dd
--- /dev/null
+++ b/spec/ruby/core/string/unicode_normalized_spec.rb
@@ -0,0 +1,75 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe "String#unicode_normalized?" do
+ before :each do
+ @nfc_normalized_str = "\u1e9b\u0323"
+ @nfd_normalized_str = "\u017f\u0323\u0307"
+ @nfkc_normalized_str = "\u1e69"
+ @nfkd_normalized_str = "\u0073\u0323\u0307"
+ end
+
+ it "returns true if string is in the specified normalization form" do
+ @nfc_normalized_str.unicode_normalized?(:nfc).should == true
+ @nfd_normalized_str.unicode_normalized?(:nfd).should == true
+ @nfkc_normalized_str.unicode_normalized?(:nfkc).should == true
+ @nfkd_normalized_str.unicode_normalized?(:nfkd).should == true
+ end
+
+ it "returns false if string is not in the supplied normalization form" do
+ @nfd_normalized_str.unicode_normalized?(:nfc).should == false
+ @nfc_normalized_str.unicode_normalized?(:nfd).should == false
+ @nfc_normalized_str.unicode_normalized?(:nfkc).should == false
+ @nfc_normalized_str.unicode_normalized?(:nfkd).should == false
+ end
+
+ it "defaults to the nfc normalization form if no forms are specified" do
+ @nfc_normalized_str.should.unicode_normalized?
+ @nfd_normalized_str.should_not.unicode_normalized?
+ end
+
+ it "returns true if string is empty" do
+ "".should.unicode_normalized?
+ end
+
+ it "returns true if string does not contain any unicode codepoints" do
+ "abc".should.unicode_normalized?
+ end
+
+ it "raises an Encoding::CompatibilityError if the string is not in an unicode encoding" do
+ -> { @nfc_normalized_str.force_encoding("ISO-8859-1").unicode_normalized? }.should.raise(Encoding::CompatibilityError)
+ end
+
+ it "raises an ArgumentError if the specified form is invalid" do
+ -> { @nfc_normalized_str.unicode_normalized?(:invalid_form) }.should.raise(ArgumentError)
+ end
+
+ it "returns true if str is in Unicode normalization form (nfc)" do
+ str = "a\u0300"
+ str.unicode_normalized?(:nfc).should == false
+ str.unicode_normalize!(:nfc)
+ str.unicode_normalized?(:nfc).should == true
+ end
+
+ it "returns true if str is in Unicode normalization form (nfd)" do
+ str = "a\u00E0"
+ str.unicode_normalized?(:nfd).should == false
+ str.unicode_normalize!(:nfd)
+ str.unicode_normalized?(:nfd).should == true
+ end
+
+ it "returns true if str is in Unicode normalization form (nfkc)" do
+ str = "a\u0300"
+ str.unicode_normalized?(:nfkc).should == false
+ str.unicode_normalize!(:nfkc)
+ str.unicode_normalized?(:nfkc).should == true
+ end
+
+ it "returns true if str is in Unicode normalization form (nfkd)" do
+ str = "a\u00E0"
+ str.unicode_normalized?(:nfkd).should == false
+ str.unicode_normalize!(:nfkd)
+ str.unicode_normalized?(:nfkd).should == true
+ end
+end
diff --git a/spec/ruby/core/string/unpack/a_spec.rb b/spec/ruby/core/string/unpack/a_spec.rb
new file mode 100644
index 0000000000..a68e842e15
--- /dev/null
+++ b/spec/ruby/core/string/unpack/a_spec.rb
@@ -0,0 +1,66 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/string'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'A'" do
+ it_behaves_like :string_unpack_basic, 'A'
+ it_behaves_like :string_unpack_no_platform, 'A'
+ it_behaves_like :string_unpack_string, 'A'
+ it_behaves_like :string_unpack_Aa, 'A'
+ it_behaves_like :string_unpack_taint, 'A'
+
+ it "removes trailing space and NULL bytes from the decoded string" do
+ [ ["a\x00 b \x00", ["a\x00 b", ""]],
+ ["a\x00 b \x00 ", ["a\x00 b", ""]],
+ ["a\x00 b\x00 ", ["a\x00 b", ""]],
+ ["a\x00 b\x00", ["a\x00 b", ""]],
+ ["a\x00 b ", ["a\x00 b", ""]]
+ ].should be_computed_by(:unpack, "A*A")
+ end
+
+ it "does not remove whitespace other than space" do
+ [ ["a\x00 b\x00\f", ["a\x00 b\x00\f"]],
+ ["a\x00 b\x00\n", ["a\x00 b\x00\n"]],
+ ["a\x00 b\x00\r", ["a\x00 b\x00\r"]],
+ ["a\x00 b\x00\t", ["a\x00 b\x00\t"]],
+ ["a\x00 b\x00\v", ["a\x00 b\x00\v"]],
+ ].should be_computed_by(:unpack, "A*")
+ end
+
+ it "decodes into raw (ascii) string values" do
+ str = "str".dup.force_encoding('UTF-8').unpack("A*")[0]
+ str.encoding.should == Encoding::BINARY
+ end
+
+end
+
+describe "String#unpack with format 'a'" do
+ it_behaves_like :string_unpack_basic, 'a'
+ it_behaves_like :string_unpack_no_platform, 'a'
+ it_behaves_like :string_unpack_string, 'a'
+ it_behaves_like :string_unpack_Aa, 'a'
+ it_behaves_like :string_unpack_taint, 'a'
+
+ it "does not remove trailing whitespace or NULL bytes from the decoded string" do
+ [ ["a\x00 b \x00", ["a\x00 b \x00"]],
+ ["a\x00 b \x00 ", ["a\x00 b \x00 "]],
+ ["a\x00 b\x00 ", ["a\x00 b\x00 "]],
+ ["a\x00 b\x00", ["a\x00 b\x00"]],
+ ["a\x00 b ", ["a\x00 b "]],
+ ["a\x00 b\f", ["a\x00 b\f"]],
+ ["a\x00 b\n", ["a\x00 b\n"]],
+ ["a\x00 b\r", ["a\x00 b\r"]],
+ ["a\x00 b\t", ["a\x00 b\t"]],
+ ["a\x00 b\v", ["a\x00 b\v"]]
+ ].should be_computed_by(:unpack, "a*")
+ end
+
+ it "decodes into raw (ascii) string values" do
+ str = "".unpack("a*")[0]
+ str.encoding.should == Encoding::BINARY
+ end
+
+end
diff --git a/spec/ruby/core/string/unpack/at_spec.rb b/spec/ruby/core/string/unpack/at_spec.rb
new file mode 100644
index 0000000000..f4999f5922
--- /dev/null
+++ b/spec/ruby/core/string/unpack/at_spec.rb
@@ -0,0 +1,29 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+describe "String#unpack with format '@'" do
+ it_behaves_like :string_unpack_basic, '@'
+ it_behaves_like :string_unpack_no_platform, '@'
+
+ it "moves the read index to the byte specified by the count" do
+ "\x01\x02\x03\x04".unpack("C3@2C").should == [1, 2, 3, 3]
+ end
+
+ it "implicitly has a count of zero when count is not specified" do
+ "\x01\x02\x03\x04".unpack("C2@C").should == [1, 2, 1]
+ end
+
+ it "has no effect when passed the '*' modifier" do
+ "\x01\x02\x03\x04".unpack("C2@*C").should == [1, 2, 3]
+ end
+
+ it "positions the read index one beyond the last readable byte in the String" do
+ "\x01\x02\x03\x04".unpack("C2@4C").should == [1, 2, nil]
+ end
+
+ it "raises an ArgumentError if the count exceeds the size of the String" do
+ -> { "\x01\x02\x03\x04".unpack("C2@5C") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/unpack/b_spec.rb b/spec/ruby/core/string/unpack/b_spec.rb
new file mode 100644
index 0000000000..fac6ef5151
--- /dev/null
+++ b/spec/ruby/core/string/unpack/b_spec.rb
@@ -0,0 +1,201 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'B'" do
+ it_behaves_like :string_unpack_basic, 'B'
+ it_behaves_like :string_unpack_no_platform, 'B'
+ it_behaves_like :string_unpack_taint, 'B'
+
+ it "decodes one bit from each byte for each format character starting with the most significant bit" do
+ [ ["\x00", "B", ["0"]],
+ ["\x80", "B", ["1"]],
+ ["\x0f", "B", ["0"]],
+ ["\x8f", "B", ["1"]],
+ ["\x7f", "B", ["0"]],
+ ["\xff", "B", ["1"]],
+ ["\x80\x00", "BB", ["1", "0"]],
+ ["\x8f\x00", "BB", ["1", "0"]],
+ ["\x80\x0f", "BB", ["1", "0"]],
+ ["\x80\x8f", "BB", ["1", "1"]],
+ ["\x80\x80", "BB", ["1", "1"]],
+ ["\x0f\x80", "BB", ["0", "1"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes only the number of bits in the string when passed a count" do
+ "\x83".unpack("B25").should == ["10000011"]
+ end
+
+ it "decodes multiple differing bit counts from a single string" do
+ str = "\xaa\xaa\xaa\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7\xc3\xd4\xaa\x6b\xd7\xaa"
+ array = str.unpack("B5B6B7B8B9B10B13B14B16B17")
+ array.should == ["10101", "101010", "1010101", "10101010", "010101011",
+ "1101010011", "0110101111010", "10101010110101",
+ "1100001111010100", "10101010011010111"]
+ end
+
+ it "decodes a directive with a '*' modifier after a directive with a count modifier" do
+ "\xd4\xc3\x6b\xd7".unpack("B5B*").should == ["11010", "110000110110101111010111"]
+ end
+
+ it "decodes a directive with a count modifier after a directive with a '*' modifier" do
+ "\xd4\xc3\x6b\xd7".unpack("B*B5").should == ["11010100110000110110101111010111", ""]
+ end
+
+ it "decodes the number of bits specified by the count modifier" do
+ [ ["\x00", "B0", [""]],
+ ["\x80", "B1", ["1"]],
+ ["\x7f", "B2", ["01"]],
+ ["\x8f", "B3", ["100"]],
+ ["\x7f", "B4", ["0111"]],
+ ["\xff", "B5", ["11111"]],
+ ["\xf8", "B6", ["111110"]],
+ ["\x9c", "B7", ["1001110"]],
+ ["\xbd", "B8", ["10111101"]],
+ ["\x80\x80", "B9", ["100000001"]],
+ ["\x80\x70", "B10", ["1000000001"]],
+ ["\x80\x20", "B11", ["10000000001"]],
+ ["\x8f\x10", "B12", ["100011110001"]],
+ ["\x8f\x0f", "B13", ["1000111100001"]],
+ ["\x80\x0f", "B14", ["10000000000011"]],
+ ["\x80\x8f", "B15", ["100000001000111"]],
+ ["\x0f\x81", "B16", ["0000111110000001"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes all the bits when passed the '*' modifier" do
+ [ ["", [""]],
+ ["\x00", ["00000000"]],
+ ["\x80", ["10000000"]],
+ ["\x7f", ["01111111"]],
+ ["\x81", ["10000001"]],
+ ["\x0f", ["00001111"]],
+ ["\x80\x80", ["1000000010000000"]],
+ ["\x8f\x10", ["1000111100010000"]],
+ ["\x00\x10", ["0000000000010000"]]
+ ].should be_computed_by(:unpack, "B*")
+ end
+
+ it "adds an empty string for each element requested beyond the end of the String" do
+ [ ["", ["", "", ""]],
+ ["\x80", ["1", "", ""]],
+ ["\x80\x08", ["1", "0", ""]]
+ ].should be_computed_by(:unpack, "BBB")
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x80\x00".unpack("B\x00B")
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "\x80\x00".unpack("B B").should == ["1", "0"]
+ end
+
+ it "decodes into US-ASCII string values" do
+ str = "s".dup.force_encoding('UTF-8').unpack("B*")[0]
+ str.encoding.name.should == 'US-ASCII'
+ end
+end
+
+describe "String#unpack with format 'b'" do
+ it_behaves_like :string_unpack_basic, 'b'
+ it_behaves_like :string_unpack_no_platform, 'b'
+ it_behaves_like :string_unpack_taint, 'b'
+
+ it "decodes one bit from each byte for each format character starting with the least significant bit" do
+ [ ["\x00", "b", ["0"]],
+ ["\x01", "b", ["1"]],
+ ["\xf0", "b", ["0"]],
+ ["\xf1", "b", ["1"]],
+ ["\xfe", "b", ["0"]],
+ ["\xff", "b", ["1"]],
+ ["\x01\x00", "bb", ["1", "0"]],
+ ["\xf1\x00", "bb", ["1", "0"]],
+ ["\x01\xf0", "bb", ["1", "0"]],
+ ["\x01\xf1", "bb", ["1", "1"]],
+ ["\x01\x01", "bb", ["1", "1"]],
+ ["\xf0\x01", "bb", ["0", "1"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes only the number of bits in the string when passed a count" do
+ "\x83".unpack("b25").should == ["11000001"]
+ end
+
+ it "decodes multiple differing bit counts from a single string" do
+ str = "\xaa\xaa\xaa\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7\xc3\xd4\xaa\x6b\xd7\xaa"
+ array = str.unpack("b5b6b7b8b9b10b13b14b16b17")
+ array.should == ["01010", "010101", "0101010", "01010101", "101010100",
+ "0010101111", "1101011011101", "01010101111010",
+ "1100001100101011", "01010101110101101"]
+ end
+
+ it "decodes a directive with a '*' modifier after a directive with a count modifier" do
+ "\xd4\xc3\x6b\xd7".unpack("b5b*").should == ["00101", "110000111101011011101011"]
+ end
+
+ it "decodes a directive with a count modifier after a directive with a '*' modifier" do
+ "\xd4\xc3\x6b\xd7".unpack("b*b5").should == ["00101011110000111101011011101011", ""]
+ end
+
+ it "decodes the number of bits specified by the count modifier" do
+ [ ["\x00", "b0", [""]],
+ ["\x01", "b1", ["1"]],
+ ["\xfe", "b2", ["01"]],
+ ["\xfc", "b3", ["001"]],
+ ["\xf7", "b4", ["1110"]],
+ ["\xff", "b5", ["11111"]],
+ ["\xfe", "b6", ["011111"]],
+ ["\xce", "b7", ["0111001"]],
+ ["\xbd", "b8", ["10111101"]],
+ ["\x01\xff", "b9", ["100000001"]],
+ ["\x01\xfe", "b10", ["1000000001"]],
+ ["\x01\xfc", "b11", ["10000000001"]],
+ ["\xf1\xf8", "b12", ["100011110001"]],
+ ["\xe1\xf1", "b13", ["1000011110001"]],
+ ["\x03\xe0", "b14", ["11000000000001"]],
+ ["\x47\xc0", "b15", ["111000100000001"]],
+ ["\x81\x0f", "b16", ["1000000111110000"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes all the bits when passed the '*' modifier" do
+ [ ["", [""]],
+ ["\x00", ["00000000"]],
+ ["\x80", ["00000001"]],
+ ["\x7f", ["11111110"]],
+ ["\x81", ["10000001"]],
+ ["\x0f", ["11110000"]],
+ ["\x80\x80", ["0000000100000001"]],
+ ["\x8f\x10", ["1111000100001000"]],
+ ["\x00\x10", ["0000000000001000"]]
+ ].should be_computed_by(:unpack, "b*")
+ end
+
+ it "adds an empty string for each element requested beyond the end of the String" do
+ [ ["", ["", "", ""]],
+ ["\x01", ["1", "", ""]],
+ ["\x01\x80", ["1", "0", ""]]
+ ].should be_computed_by(:unpack, "bbb")
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x01\x00".unpack("b\x00b")
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "\x01\x00".unpack("b b").should == ["1", "0"]
+ end
+
+ it "decodes into US-ASCII string values" do
+ str = "s".dup.force_encoding('UTF-8').unpack("b*")[0]
+ str.encoding.name.should == 'US-ASCII'
+ end
+end
diff --git a/spec/ruby/core/string/unpack/c_spec.rb b/spec/ruby/core/string/unpack/c_spec.rb
new file mode 100644
index 0000000000..d881015b5e
--- /dev/null
+++ b/spec/ruby/core/string/unpack/c_spec.rb
@@ -0,0 +1,65 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+describe :string_unpack_8bit, shared: true do
+ it "decodes one byte for a single format character" do
+ "abc".unpack(unpack_format()).should == [97]
+ end
+
+ it "decodes two bytes for two format characters" do
+ "abc".unpack(unpack_format(nil, 2)).should == [97, 98]
+ end
+
+ it "decodes the number of bytes requested by the count modifier" do
+ "abc".unpack(unpack_format(2)).should == [97, 98]
+ end
+
+ it "decodes the remaining bytes when passed the '*' modifier" do
+ "abc".unpack(unpack_format('*')).should == [97, 98, 99]
+ end
+
+ it "decodes the remaining bytes when passed the '*' modifier after another directive" do
+ "abc".unpack(unpack_format()+unpack_format('*')).should == [97, 98, 99]
+ end
+
+ it "decodes zero bytes when no bytes remain and the '*' modifier is passed" do
+ "abc".unpack(unpack_format('*', 2)).should == [97, 98, 99]
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["a", [97, nil, nil]],
+ ["ab", [97, 98, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "abc".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "abc".unpack(unpack_format(' ', 2)).should == [97, 98]
+ end
+end
+
+describe "String#unpack with format 'C'" do
+ it_behaves_like :string_unpack_basic, 'C'
+ it_behaves_like :string_unpack_8bit, 'C'
+
+ it "decodes a byte with most significant bit set as a positive number" do
+ "\xff\x80\x82".unpack('C*').should == [255, 128, 130]
+ end
+end
+
+describe "String#unpack with format 'c'" do
+ it_behaves_like :string_unpack_basic, 'c'
+ it_behaves_like :string_unpack_8bit, 'c'
+
+ it "decodes a byte with most significant bit set as a negative number" do
+ "\xff\x80\x82".unpack('c*').should == [-1, -128, -126]
+ end
+end
diff --git a/spec/ruby/core/string/unpack/carret_spec.rb b/spec/ruby/core/string/unpack/carret_spec.rb
new file mode 100644
index 0000000000..815df0c718
--- /dev/null
+++ b/spec/ruby/core/string/unpack/carret_spec.rb
@@ -0,0 +1,43 @@
+# encoding: binary
+ruby_version_is "4.1" do
+ require_relative '../../../spec_helper'
+ require_relative '../fixtures/classes'
+ require_relative 'shared/basic'
+
+ describe "String#unpack with format '^'" do
+ it_behaves_like :string_unpack_basic, '^'
+ it_behaves_like :string_unpack_no_platform, '^'
+
+ it "returns the current offset that start from 0" do
+ "".unpack("^").should == [0]
+ end
+
+ it "returns the current offset after the last decode ended" do
+ "a".unpack("CC^").should == [97, nil, 1]
+ end
+
+ it "returns the current offset that start from the given offset" do
+ "abc".unpack("^", offset: 1).should == [1]
+ end
+
+ it "returns the offset moved by 'X'" do
+ "\x01\x02\x03\x04".unpack("C3X2^").should == [1, 2, 3, 1]
+ end
+
+ it "returns the offset moved by 'x'" do
+ "\x01\x02\x03\x04".unpack("Cx2^").should == [1, 3]
+ end
+
+ it "returns the offset to the position the previous decode ended" do
+ "foo".unpack("A4^").should == ["foo", 3]
+ "foo".unpack("a4^").should == ["foo", 3]
+ "foo".unpack("Z5^").should == ["foo", 3]
+ end
+
+ it "returns the offset including truncated part" do
+ "foo ".unpack("A*^").should == ["foo", 6]
+ "foo\0".unpack("Z*^").should == ["foo", 4]
+ "foo\0\0\0".unpack("Z5^").should == ["foo", 5]
+ end
+ end
+end
diff --git a/spec/ruby/core/string/unpack/comment_spec.rb b/spec/ruby/core/string/unpack/comment_spec.rb
new file mode 100644
index 0000000000..050d2b7fc0
--- /dev/null
+++ b/spec/ruby/core/string/unpack/comment_spec.rb
@@ -0,0 +1,25 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe "String#unpack" do
+ it "ignores directives text from '#' to the first newline" do
+ "\x01\x02\x03".unpack("c#this is a comment\nc").should == [1, 2]
+ end
+
+ it "ignores directives text from '#' to the end if no newline is present" do
+ "\x01\x02\x03".unpack("c#this is a comment c").should == [1]
+ end
+
+ it "ignores comments at the start of the directives string" do
+ "\x01\x02\x03".unpack("#this is a comment\nc").should == [1]
+ end
+
+ it "ignores the entire directive string if it is a comment" do
+ "\x01\x02\x03".unpack("#this is a comment c").should == []
+ end
+
+ it "ignores multiple comments" do
+ "\x01\x02\x03".unpack("c#comment\nc#comment\nc#c").should == [1, 2, 3]
+ end
+end
diff --git a/spec/ruby/core/string/unpack/d_spec.rb b/spec/ruby/core/string/unpack/d_spec.rb
new file mode 100644
index 0000000000..0e4f57ec04
--- /dev/null
+++ b/spec/ruby/core/string/unpack/d_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/float'
+
+little_endian do
+ describe "String#unpack with format 'D'" do
+ it_behaves_like :string_unpack_basic, 'D'
+ it_behaves_like :string_unpack_double_le, 'D'
+ end
+
+ describe "String#unpack with format 'd'" do
+ it_behaves_like :string_unpack_basic, 'd'
+ it_behaves_like :string_unpack_double_le, 'd'
+ end
+end
+
+big_endian do
+ describe "String#unpack with format 'D'" do
+ it_behaves_like :string_unpack_basic, 'D'
+ it_behaves_like :string_unpack_double_be, 'D'
+ end
+
+ describe "String#unpack with format 'd'" do
+ it_behaves_like :string_unpack_basic, 'd'
+ it_behaves_like :string_unpack_double_be, 'd'
+ end
+end
diff --git a/spec/ruby/core/string/unpack/e_spec.rb b/spec/ruby/core/string/unpack/e_spec.rb
new file mode 100644
index 0000000000..c958be1c8b
--- /dev/null
+++ b/spec/ruby/core/string/unpack/e_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/float'
+
+describe "String#unpack with format 'E'" do
+ it_behaves_like :string_unpack_basic, 'E'
+ it_behaves_like :string_unpack_double_le, 'E'
+end
+
+describe "String#unpack with format 'e'" do
+ it_behaves_like :string_unpack_basic, 'e'
+ it_behaves_like :string_unpack_float_le, 'e'
+end
diff --git a/spec/ruby/core/string/unpack/f_spec.rb b/spec/ruby/core/string/unpack/f_spec.rb
new file mode 100644
index 0000000000..ec8b9d435e
--- /dev/null
+++ b/spec/ruby/core/string/unpack/f_spec.rb
@@ -0,0 +1,28 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/float'
+
+little_endian do
+ describe "String#unpack with format 'F'" do
+ it_behaves_like :string_unpack_basic, 'F'
+ it_behaves_like :string_unpack_float_le, 'F'
+ end
+
+ describe "String#unpack with format 'f'" do
+ it_behaves_like :string_unpack_basic, 'f'
+ it_behaves_like :string_unpack_float_le, 'f'
+ end
+end
+
+big_endian do
+ describe "String#unpack with format 'F'" do
+ it_behaves_like :string_unpack_basic, 'F'
+ it_behaves_like :string_unpack_float_be, 'F'
+ end
+
+ describe "String#unpack with format 'f'" do
+ it_behaves_like :string_unpack_basic, 'f'
+ it_behaves_like :string_unpack_float_be, 'f'
+ end
+end
diff --git a/spec/ruby/core/string/unpack/g_spec.rb b/spec/ruby/core/string/unpack/g_spec.rb
new file mode 100644
index 0000000000..ffc423b152
--- /dev/null
+++ b/spec/ruby/core/string/unpack/g_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/float'
+
+describe "String#unpack with format 'G'" do
+ it_behaves_like :string_unpack_basic, 'G'
+ it_behaves_like :string_unpack_double_be, 'G'
+end
+
+describe "String#unpack with format 'g'" do
+ it_behaves_like :string_unpack_basic, 'g'
+ it_behaves_like :string_unpack_float_be, 'g'
+end
diff --git a/spec/ruby/core/string/unpack/h_spec.rb b/spec/ruby/core/string/unpack/h_spec.rb
new file mode 100644
index 0000000000..0cf8d943a7
--- /dev/null
+++ b/spec/ruby/core/string/unpack/h_spec.rb
@@ -0,0 +1,139 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'H'" do
+ it_behaves_like :string_unpack_basic, 'H'
+ it_behaves_like :string_unpack_no_platform, 'H'
+ it_behaves_like :string_unpack_taint, 'H'
+
+ it "decodes one nibble from each byte for each format character starting with the most significant bit" do
+ [ ["\x8f", "H", ["8"]],
+ ["\xf8\x0f", "HH", ["f", "0"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes only the number of nibbles in the string when passed a count" do
+ "\xca\xfe".unpack("H5").should == ["cafe"]
+ end
+
+ it "decodes multiple differing nibble counts from a single string" do
+ array = "\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7".unpack("HH2H3H4H5")
+ array.should == ["a", "55", "aad", "c36b", "d7aad"]
+ end
+
+ it "decodes a directive with a '*' modifier after a directive with a count modifier" do
+ "\xaa\x55\xaa\xd4\xc3\x6b".unpack("H3H*").should == ["aa5", "aad4c36b"]
+ end
+
+ it "decodes a directive with a count modifier after a directive with a '*' modifier" do
+ "\xaa\x55\xaa\xd4\xc3\x6b".unpack("H*H3").should == ["aa55aad4c36b", ""]
+ end
+
+ it "decodes the number of nibbles specified by the count modifier" do
+ [ ["\xab", "H0", [""]],
+ ["\x00", "H1", ["0"]],
+ ["\x01", "H2", ["01"]],
+ ["\x01\x23", "H3", ["012"]],
+ ["\x01\x23", "H4", ["0123"]],
+ ["\x01\x23\x45", "H5", ["01234"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes all the nibbles when passed the '*' modifier" do
+ [ ["", [""]],
+ ["\xab", ["ab"]],
+ ["\xca\xfe", ["cafe"]],
+ ].should be_computed_by(:unpack, "H*")
+ end
+
+ it "adds an empty string for each element requested beyond the end of the String" do
+ [ ["", ["", "", ""]],
+ ["\x01", ["0", "", ""]],
+ ["\x01\x80", ["0", "8", ""]]
+ ].should be_computed_by(:unpack, "HHH")
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x01\x10".unpack("H\x00H")
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "\x01\x10".unpack("H H").should == ["0", "1"]
+ end
+
+ it "should make strings with US_ASCII encoding" do
+ "\x01".unpack("H")[0].encoding.should == Encoding::US_ASCII
+ end
+end
+
+describe "String#unpack with format 'h'" do
+ it_behaves_like :string_unpack_basic, 'h'
+ it_behaves_like :string_unpack_no_platform, 'h'
+ it_behaves_like :string_unpack_taint, 'h'
+
+ it "decodes one nibble from each byte for each format character starting with the least significant bit" do
+ [ ["\x8f", "h", ["f"]],
+ ["\xf8\x0f", "hh", ["8", "f"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes only the number of nibbles in the string when passed a count" do
+ "\xac\xef".unpack("h5").should == ["cafe"]
+ end
+
+ it "decodes multiple differing nibble counts from a single string" do
+ array = "\xaa\x55\xaa\xd4\xc3\x6b\xd7\xaa\xd7".unpack("hh2h3h4h5")
+ array.should == ["a", "55", "aa4", "3cb6", "7daa7"]
+ end
+
+ it "decodes a directive with a '*' modifier after a directive with a count modifier" do
+ "\xba\x55\xaa\xd4\xc3\x6b".unpack("h3h*").should == ["ab5", "aa4d3cb6"]
+ end
+
+ it "decodes a directive with a count modifier after a directive with a '*' modifier" do
+ "\xba\x55\xaa\xd4\xc3\x6b".unpack("h*h3").should == ["ab55aa4d3cb6", ""]
+ end
+
+ it "decodes the number of nibbles specified by the count modifier" do
+ [ ["\xab", "h0", [""]],
+ ["\x00", "h1", ["0"]],
+ ["\x01", "h2", ["10"]],
+ ["\x01\x23", "h3", ["103"]],
+ ["\x01\x23", "h4", ["1032"]],
+ ["\x01\x23\x45", "h5", ["10325"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes all the nibbles when passed the '*' modifier" do
+ [ ["", [""]],
+ ["\xab", ["ba"]],
+ ["\xac\xef", ["cafe"]],
+ ].should be_computed_by(:unpack, "h*")
+ end
+
+ it "adds an empty string for each element requested beyond the end of the String" do
+ [ ["", ["", "", ""]],
+ ["\x01", ["1", "", ""]],
+ ["\x01\x80", ["1", "0", ""]]
+ ].should be_computed_by(:unpack, "hhh")
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x01\x10".unpack("h\x00h")
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "\x01\x10".unpack("h h").should == ["1", "0"]
+ end
+
+ it "should make strings with US_ASCII encoding" do
+ "\x01".unpack("h")[0].encoding.should == Encoding::US_ASCII
+ end
+end
diff --git a/spec/ruby/core/string/unpack/i_spec.rb b/spec/ruby/core/string/unpack/i_spec.rb
new file mode 100644
index 0000000000..b4bbba1923
--- /dev/null
+++ b/spec/ruby/core/string/unpack/i_spec.rb
@@ -0,0 +1,152 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+describe "String#unpack with format 'I'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_32bit_le, 'I<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'I<_'
+ it_behaves_like :string_unpack_32bit_le, 'I_<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I<_'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'I<!'
+ it_behaves_like :string_unpack_32bit_le, 'I!<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I<!'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_32bit_be, 'I>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'I>_'
+ it_behaves_like :string_unpack_32bit_be, 'I_>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I>_'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'I>!'
+ it_behaves_like :string_unpack_32bit_be, 'I!>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I>!'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I!>'
+ end
+end
+
+describe "String#unpack with format 'i'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_32bit_le, 'i<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'i<_'
+ it_behaves_like :string_unpack_32bit_le, 'i_<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i<_'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'i<!'
+ it_behaves_like :string_unpack_32bit_le, 'i!<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i<!'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_32bit_be, 'i>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'i>_'
+ it_behaves_like :string_unpack_32bit_be, 'i_>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i>_'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'i>!'
+ it_behaves_like :string_unpack_32bit_be, 'i!>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i>!'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i!>'
+ end
+end
+
+little_endian do
+ describe "String#unpack with format 'I'" do
+ it_behaves_like :string_unpack_basic, 'I'
+ it_behaves_like :string_unpack_32bit_le, 'I'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I'
+ end
+
+ describe "String#unpack with format 'I' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'I_'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I_'
+ end
+
+ describe "String#unpack with format 'I' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'I!'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'I!'
+ end
+
+ describe "String#unpack with format 'i'" do
+ it_behaves_like :string_unpack_basic, 'i'
+ it_behaves_like :string_unpack_32bit_le, 'i'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i'
+ end
+
+ describe "String#unpack with format 'i' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'i_'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i_'
+ end
+
+ describe "String#unpack with format 'i' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'i!'
+ it_behaves_like :string_unpack_32bit_le_signed, 'i!'
+ end
+end
+
+big_endian do
+ describe "String#unpack with format 'I'" do
+ it_behaves_like :string_unpack_basic, 'I'
+ it_behaves_like :string_unpack_32bit_be, 'I'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I'
+ end
+
+ describe "String#unpack with format 'I' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'I_'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I_'
+ end
+
+ describe "String#unpack with format 'I' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'I!'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'I!'
+ end
+
+ describe "String#unpack with format 'i'" do
+ it_behaves_like :string_unpack_basic, 'i'
+ it_behaves_like :string_unpack_32bit_be, 'i'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i'
+ end
+
+ describe "String#unpack with format 'i' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'i_'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i_'
+ end
+
+ describe "String#unpack with format 'i' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'i!'
+ it_behaves_like :string_unpack_32bit_be_signed, 'i!'
+ end
+end
diff --git a/spec/ruby/core/string/unpack/j_spec.rb b/spec/ruby/core/string/unpack/j_spec.rb
new file mode 100644
index 0000000000..3c2baad642
--- /dev/null
+++ b/spec/ruby/core/string/unpack/j_spec.rb
@@ -0,0 +1,272 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+platform_is pointer_size: 64 do
+ little_endian do
+ describe "String#unpack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'J_'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'J!'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J!'
+ end
+ end
+
+ describe "String#unpack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'j_'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'j!'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j!'
+ end
+ end
+ end
+
+ big_endian do
+ describe "String#unpack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'J_'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'J!'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J!'
+ end
+ end
+
+ describe "String#unpack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'j_'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'j!'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j!'
+ end
+ end
+ end
+
+ describe "String#unpack with format 'J'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_64bit_le, 'J<'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_64bit_be, 'J>'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J>'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'J<_'
+ it_behaves_like :string_unpack_64bit_le, 'J_<'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J<_'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'J<!'
+ it_behaves_like :string_unpack_64bit_le, 'J!<'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J<!'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'J!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'J>_'
+ it_behaves_like :string_unpack_64bit_be, 'J_>'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J>_'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'J>!'
+ it_behaves_like :string_unpack_64bit_be, 'J!>'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J>!'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'J!>'
+ end
+ end
+
+ describe "String#unpack with format 'j'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_64bit_le, 'j<'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_64bit_be, 'j>'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j>'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'j<_'
+ it_behaves_like :string_unpack_64bit_le, 'j_<'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j<_'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'j<!'
+ it_behaves_like :string_unpack_64bit_le, 'j!<'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j<!'
+ it_behaves_like :string_unpack_64bit_le_signed, 'j!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'j>_'
+ it_behaves_like :string_unpack_64bit_be, 'j_>'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j>_'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'j>!'
+ it_behaves_like :string_unpack_64bit_be, 'j!>'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j>!'
+ it_behaves_like :string_unpack_64bit_be_signed, 'j!>'
+ end
+ end
+end
+
+platform_is pointer_size: 32 do
+ little_endian do
+ describe "String#unpack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'J_'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'J!'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J!'
+ end
+ end
+
+ describe "String#unpack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'j_'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'j!'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j!'
+ end
+ end
+ end
+
+ big_endian do
+ describe "String#unpack with format 'J'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'J_'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'J!'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J!'
+ end
+ end
+
+ describe "String#unpack with format 'j'" do
+ describe "with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'j_'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j_'
+ end
+
+ describe "with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'j!'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j!'
+ end
+ end
+ end
+
+ describe "String#unpack with format 'J'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_32bit_le, 'J<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_32bit_be, 'J>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J>'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'J<_'
+ it_behaves_like :string_unpack_32bit_le, 'J_<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J<_'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'J<!'
+ it_behaves_like :string_unpack_32bit_le, 'J!<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J<!'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'J!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'J>_'
+ it_behaves_like :string_unpack_32bit_be, 'J_>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J>_'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'J>!'
+ it_behaves_like :string_unpack_32bit_be, 'J!>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J>!'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'J!>'
+ end
+ end
+
+ describe "String#unpack with format 'j'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_32bit_le, 'j<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_32bit_be, 'j>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j>'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'j<_'
+ it_behaves_like :string_unpack_32bit_le, 'j_<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j<_'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'j<!'
+ it_behaves_like :string_unpack_32bit_le, 'j!<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j<!'
+ it_behaves_like :string_unpack_32bit_le_signed, 'j!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'j>_'
+ it_behaves_like :string_unpack_32bit_be, 'j_>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j>_'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'j>!'
+ it_behaves_like :string_unpack_32bit_be, 'j!>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j>!'
+ it_behaves_like :string_unpack_32bit_be_signed, 'j!>'
+ end
+ end
+end
diff --git a/spec/ruby/core/string/unpack/l_spec.rb b/spec/ruby/core/string/unpack/l_spec.rb
new file mode 100644
index 0000000000..0adb567eca
--- /dev/null
+++ b/spec/ruby/core/string/unpack/l_spec.rb
@@ -0,0 +1,265 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+describe "String#unpack with format 'L'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_32bit_le, 'L<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_32bit_be, 'L>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L>'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'L<_'
+ it_behaves_like :string_unpack_32bit_le, 'L_<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L<_'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'L<!'
+ it_behaves_like :string_unpack_32bit_le, 'L!<'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L<!'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'L>_'
+ it_behaves_like :string_unpack_32bit_be, 'L_>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L>_'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'L>!'
+ it_behaves_like :string_unpack_32bit_be, 'L!>'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L>!'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L!>'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'L<_'
+ it_behaves_like :string_unpack_64bit_le, 'L_<'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'L<_'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'L_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'L<!'
+ it_behaves_like :string_unpack_64bit_le, 'L!<'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'L<!'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'L!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'L>_'
+ it_behaves_like :string_unpack_64bit_be, 'L_>'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'L>_'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'L_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'L>!'
+ it_behaves_like :string_unpack_64bit_be, 'L!>'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'L>!'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'L!>'
+ end
+ end
+end
+
+describe "String#unpack with format 'l'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_32bit_le, 'l<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_32bit_be, 'l>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l>'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'l<_'
+ it_behaves_like :string_unpack_32bit_le, 'l_<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l<_'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'l<!'
+ it_behaves_like :string_unpack_32bit_le, 'l!<'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l<!'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'l>_'
+ it_behaves_like :string_unpack_32bit_be, 'l_>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l>_'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'l>!'
+ it_behaves_like :string_unpack_32bit_be, 'l!>'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l>!'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l!>'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'l<_'
+ it_behaves_like :string_unpack_64bit_le, 'l_<'
+ it_behaves_like :string_unpack_64bit_le_signed, 'l<_'
+ it_behaves_like :string_unpack_64bit_le_signed, 'l_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'l<!'
+ it_behaves_like :string_unpack_64bit_le, 'l!<'
+ it_behaves_like :string_unpack_64bit_le_signed, 'l<!'
+ it_behaves_like :string_unpack_64bit_le_signed, 'l!<'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'l>_'
+ it_behaves_like :string_unpack_64bit_be, 'l_>'
+ it_behaves_like :string_unpack_64bit_be_signed, 'l>_'
+ it_behaves_like :string_unpack_64bit_be_signed, 'l_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'l>!'
+ it_behaves_like :string_unpack_64bit_be, 'l!>'
+ it_behaves_like :string_unpack_64bit_be_signed, 'l>!'
+ it_behaves_like :string_unpack_64bit_be_signed, 'l!>'
+ end
+ end
+end
+
+little_endian do
+ describe "String#unpack with format 'L'" do
+ it_behaves_like :string_unpack_basic, 'L'
+ it_behaves_like :string_unpack_32bit_le, 'L'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L'
+ end
+
+ describe "String#unpack with format 'l'" do
+ it_behaves_like :string_unpack_basic, 'l'
+ it_behaves_like :string_unpack_32bit_le, 'l'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "String#unpack with format 'L' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'L_'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L_'
+ end
+
+ describe "String#unpack with format 'L' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'L!'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'L!'
+ end
+
+ describe "String#unpack with format 'l' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_le, 'l_'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l'
+ end
+
+ describe "String#unpack with format 'l' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_le, 'l!'
+ it_behaves_like :string_unpack_32bit_le_signed, 'l'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "String#unpack with format 'L' with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'L_'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'L_'
+ end
+
+ describe "String#unpack with format 'L' with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'L!'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'L!'
+ end
+
+ describe "String#unpack with format 'l' with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_le, 'l_'
+ it_behaves_like :string_unpack_64bit_le_signed, 'l_'
+ end
+
+ describe "String#unpack with format 'l' with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_le, 'l!'
+ it_behaves_like :string_unpack_64bit_le_signed, 'l!'
+ end
+ end
+end
+
+big_endian do
+ describe "String#unpack with format 'L'" do
+ it_behaves_like :string_unpack_basic, 'L'
+ it_behaves_like :string_unpack_32bit_be, 'L'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L'
+ end
+
+ describe "String#unpack with format 'l'" do
+ it_behaves_like :string_unpack_basic, 'l'
+ it_behaves_like :string_unpack_32bit_be, 'l'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l'
+ end
+
+ platform_is c_long_size: 32 do
+ describe "String#unpack with format 'L' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'L_'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L_'
+ end
+
+ describe "String#unpack with format 'L' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'L!'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'L!'
+ end
+
+ describe "String#unpack with format 'l' with modifier '_'" do
+ it_behaves_like :string_unpack_32bit_be, 'l_'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l'
+ end
+
+ describe "String#unpack with format 'l' with modifier '!'" do
+ it_behaves_like :string_unpack_32bit_be, 'l!'
+ it_behaves_like :string_unpack_32bit_be_signed, 'l'
+ end
+ end
+
+ platform_is c_long_size: 64 do
+ describe "String#unpack with format 'L' with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'L_'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'L_'
+ end
+
+ describe "String#unpack with format 'L' with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'L!'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'L!'
+ end
+
+ describe "String#unpack with format 'l' with modifier '_'" do
+ it_behaves_like :string_unpack_64bit_be, 'l_'
+ it_behaves_like :string_unpack_64bit_be_signed, 'l_'
+ end
+
+ describe "String#unpack with format 'l' with modifier '!'" do
+ it_behaves_like :string_unpack_64bit_be, 'l!'
+ it_behaves_like :string_unpack_64bit_be_signed, 'l!'
+ end
+ end
+
+end
diff --git a/spec/ruby/core/string/unpack/m_spec.rb b/spec/ruby/core/string/unpack/m_spec.rb
new file mode 100644
index 0000000000..c1c1eea629
--- /dev/null
+++ b/spec/ruby/core/string/unpack/m_spec.rb
@@ -0,0 +1,192 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'M'" do
+ it_behaves_like :string_unpack_basic, 'M'
+ it_behaves_like :string_unpack_no_platform, 'M'
+ it_behaves_like :string_unpack_taint, 'M'
+
+ it "decodes an empty string" do
+ "".unpack("M").should == [""]
+ end
+
+ it "decodes the complete string ignoring newlines when given a single directive" do
+ "a=\nb=\nc=\n".unpack("M").should == ["abc"]
+ end
+
+ it "appends empty string to the array for directives exceeding the input size" do
+ "a=\nb=\nc=\n".unpack("MMM").should == ["abc", "", ""]
+ end
+
+ it "ignores the count or '*' modifier and decodes the entire string" do
+ [ ["a=\nb=\nc=\n", "M238", ["abc"]],
+ ["a=\nb=\nc=\n", "M*", ["abc"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes the '=' character" do
+ "=3D=\n".unpack("M").should == ["="]
+ end
+
+ it "decodes an embedded space character" do
+ "a b=\n".unpack("M").should == ["a b"]
+ end
+
+ it "decodes a space at the end of the pre-encoded string" do
+ "a =\n".unpack("M").should == ["a "]
+ end
+
+ it "decodes an embedded tab character" do
+ "a\tb=\n".unpack("M").should == ["a\tb"]
+ end
+
+ it "decodes a tab character at the end of the pre-encoded string" do
+ "a\t=\n".unpack("M").should == ["a\t"]
+ end
+
+ it "decodes an embedded newline" do
+ "a\nb=\n".unpack("M").should == ["a\nb"]
+ end
+
+ it "decodes pre-encoded byte values 33..60" do
+ [ ["!\"\#$%&'()*+,-./=\n", ["!\"\#$%&'()*+,-./"]],
+ ["0123456789=\n", ["0123456789"]],
+ [":;<=\n", [":;<"]]
+ ].should be_computed_by(:unpack, "M")
+ end
+
+ it "decodes pre-encoded byte values 62..126" do
+ [ [">?@=\n", [">?@"]],
+ ["ABCDEFGHIJKLMNOPQRSTUVWXYZ=\n", ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"]],
+ ["[\\]^_`=\n", ["[\\]^_`"]],
+ ["abcdefghijklmnopqrstuvwxyz=\n", ["abcdefghijklmnopqrstuvwxyz"]],
+ ["{|}~=\n", ["{|}~"]]
+ ].should be_computed_by(:unpack, "M")
+ end
+
+ it "decodes pre-encoded byte values 0..31 except tab and newline" do
+ [ ["=00=01=02=03=04=05=06=\n", ["\x00\x01\x02\x03\x04\x05\x06"]],
+ ["=07=08=0B=0C=0D=\n", ["\a\b\v\f\r"]],
+ ["=0E=0F=10=11=12=13=14=\n", ["\x0e\x0f\x10\x11\x12\x13\x14"]],
+ ["=15=16=17=18=19=1A=\n", ["\x15\x16\x17\x18\x19\x1a"]],
+ ["=1B=\n", ["\e"]],
+ ["=1C=1D=1E=1F=\n", ["\x1c\x1d\x1e\x1f"]]
+ ].should be_computed_by(:unpack, "M")
+ end
+
+ it "decodes pre-encoded byte values 127..255" do
+ [ ["=7F=80=81=82=83=84=85=86=\n", ["\x7f\x80\x81\x82\x83\x84\x85\x86"]],
+ ["=87=88=89=8A=8B=8C=8D=8E=\n", ["\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"]],
+ ["=8F=90=91=92=93=94=95=96=\n", ["\x8f\x90\x91\x92\x93\x94\x95\x96"]],
+ ["=97=98=99=9A=9B=9C=9D=9E=\n", ["\x97\x98\x99\x9a\x9b\x9c\x9d\x9e"]],
+ ["=9F=A0=A1=A2=A3=A4=A5=A6=\n", ["\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6"]],
+ ["=A7=A8=A9=AA=AB=AC=AD=AE=\n", ["\xa7\xa8\xa9\xaa\xab\xac\xad\xae"]],
+ ["=AF=B0=B1=B2=B3=B4=B5=B6=\n", ["\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6"]],
+ ["=B7=B8=B9=BA=BB=BC=BD=BE=\n", ["\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe"]],
+ ["=BF=C0=C1=C2=C3=C4=C5=C6=\n", ["\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6"]],
+ ["=C7=C8=C9=CA=CB=CC=CD=CE=\n", ["\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce"]],
+ ["=CF=D0=D1=D2=D3=D4=D5=D6=\n", ["\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6"]],
+ ["=D7=D8=D9=DA=DB=DC=DD=DE=\n", ["\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde"]],
+ ["=DF=E0=E1=E2=E3=E4=E5=E6=\n", ["\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6"]],
+ ["=E7=E8=E9=EA=EB=EC=ED=EE=\n", ["\xe7\xe8\xe9\xea\xeb\xec\xed\xee"]],
+ ["=EF=F0=F1=F2=F3=F4=F5=F6=\n", ["\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"]],
+ ["=F7=F8=F9=FA=FB=FC=FD=FE=\n", ["\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe"]],
+ ["=FF=\n", ["\xff"]]
+ ].should be_computed_by(:unpack, "M")
+ end
+
+ it "unpacks incomplete escape sequences as literal characters" do
+ "foo=".unpack("M").should == ["foo="]
+ "foo=4".unpack("M").should == ["foo=4"]
+ end
+end
+
+describe "String#unpack with format 'm'" do
+ it_behaves_like :string_unpack_basic, 'm'
+ it_behaves_like :string_unpack_no_platform, 'm'
+ it_behaves_like :string_unpack_taint, 'm'
+
+ it "decodes an empty string" do
+ "".unpack("m").should == [""]
+ end
+
+ it "decodes the complete string ignoring newlines when given a single directive" do
+ "YWJj\nREVG\n".unpack("m").should == ["abcDEF"]
+ end
+
+ it "ignores the count or '*' modifier and decodes the entire string" do
+ [ ["YWJj\nREVG\n", "m238", ["abcDEF"]],
+ ["YWJj\nREVG\n", "m*", ["abcDEF"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "appends empty string to the array for directives exceeding the input size" do
+ "YWJj\nREVG\n".unpack("mmm").should == ["abcDEF", "", ""]
+ end
+
+ it "decodes all pre-encoded ascii byte values" do
+ [ ["AAECAwQFBg==\n", ["\x00\x01\x02\x03\x04\x05\x06"]],
+ ["BwgJCgsMDQ==\n", ["\a\b\t\n\v\f\r"]],
+ ["Dg8QERITFBUW\n", ["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"]],
+ ["FxgZGhscHR4f\n", ["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"]],
+ ["ISIjJCUmJygpKissLS4v\n", ["!\"\#$%&'()*+,-./"]],
+ ["MDEyMzQ1Njc4OQ==\n", ["0123456789"]],
+ ["Ojs8PT4/QA==\n", [":;<=>?@"]],
+ ["QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=\n", ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"]],
+ ["W1xdXl9g\n", ["[\\]^_`"]],
+ ["YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXo=\n", ["abcdefghijklmnopqrstuvwxyz"]],
+ ["e3x9fg==\n", ["{|}~"]],
+ ["f8KAwoHCgsKD\n", ["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"]],
+ ["woTChcKGwofC\n", ["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"]],
+ ["iMKJworCi8KM\n", ["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"]],
+ ["wo3CjsKPwpDC\n", ["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"]],
+ ["kcKSwpPClMKV\n", ["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"]],
+ ["wpbCl8KYwpnC\n", ["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"]],
+ ["msKbwpzCncKe\n", ["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"]],
+ ["wp/CoMKhwqLC\n", ["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"]],
+ ["o8KkwqXCpsKn\n", ["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"]],
+ ["wqjCqcKqwqvC\n", ["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"]],
+ ["rMKtwq7Cr8Kw\n", ["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"]],
+ ["wrHCssKzwrTC\n", ["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"]],
+ ["tcK2wrfCuMK5\n", ["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"]],
+ ["wrrCu8K8wr3C\n", ["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"]],
+ ["vsK/w4DDgcOC\n", ["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"]],
+ ["w4PDhMOFw4bD\n", ["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"]],
+ ["h8OIw4nDisOL\n", ["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"]],
+ ["w4zDjcOOw4/D\n", ["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"]],
+ ["kMORw5LDk8OU\n", ["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"]],
+ ["w5XDlsOXw5jD\n", ["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"]],
+ ["mcOaw5vDnMOd\n", ["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"]],
+ ["w57Dn8Ogw6HD\n", ["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"]],
+ ["osOjw6TDpcOm\n", ["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"]],
+ ["w6fDqMOpw6rD\n", ["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"]],
+ ["q8Osw63DrsOv\n", ["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"]],
+ ["w7DDscOyw7PD\n", ["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"]],
+ ["tMO1w7bDt8O4\n", ["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"]],
+ ["w7nDusO7w7zD\n", ["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"]],
+ ["vcO+w78=\n", ["\xbd\xc3\xbe\xc3\xbf"]]
+ ].should be_computed_by(:unpack, "m")
+ end
+
+ it "produces binary strings" do
+ "".unpack("m").first.encoding.should == Encoding::BINARY
+ "Ojs8PT4/QA==\n".unpack("m").first.encoding.should == Encoding::BINARY
+ end
+
+ it "does not raise an error for an invalid base64 character" do
+ "dGV%zdA==".unpack("m").should == ["test"]
+ end
+
+ describe "when given count 0" do
+ it "decodes base64" do
+ "dGVzdA==".unpack("m0").should == ["test"]
+ end
+
+ it "raises an ArgumentError for an invalid base64 character" do
+ -> { "dGV%zdA==".unpack("m0") }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/unpack/n_spec.rb b/spec/ruby/core/string/unpack/n_spec.rb
new file mode 100644
index 0000000000..09173f4fcb
--- /dev/null
+++ b/spec/ruby/core/string/unpack/n_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+describe "String#unpack with format 'N'" do
+ it_behaves_like :string_unpack_basic, 'N'
+ it_behaves_like :string_unpack_32bit_be, 'N'
+ it_behaves_like :string_unpack_32bit_be_unsigned, 'N'
+ it_behaves_like :string_unpack_no_platform, 'N'
+end
+
+describe "String#unpack with format 'n'" do
+ it_behaves_like :string_unpack_basic, 'n'
+ it_behaves_like :string_unpack_16bit_be, 'n'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'n'
+ it_behaves_like :string_unpack_no_platform, 'n'
+end
diff --git a/spec/ruby/core/string/unpack/p_spec.rb b/spec/ruby/core/string/unpack/p_spec.rb
new file mode 100644
index 0000000000..4103730269
--- /dev/null
+++ b/spec/ruby/core/string/unpack/p_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'P'" do
+ it_behaves_like :string_unpack_basic, 'P'
+ it_behaves_like :string_unpack_taint, 'P'
+
+ it "round-trips a string through pack and unpack" do
+ ["hello"].pack("P").unpack("P5").should == ["hello"]
+ end
+
+ it "cannot unpack a string except from the same object that created it, or a duplicate of it" do
+ packed = ["hello"].pack("P")
+ packed.unpack("P5").should == ["hello"]
+ packed.dup.unpack("P5").should == ["hello"]
+ -> { packed.to_sym.to_s.unpack("P5") }.should.raise(ArgumentError, /no associated pointer/)
+ end
+
+ it "reads as many characters as specified" do
+ ["hello"].pack("P").unpack("P1").should == ["h"]
+ end
+
+ it "reads only as far as a NUL character" do
+ ["hello"].pack("P").unpack("P10").should == ["hello"]
+ end
+end
+
+describe "String#unpack with format 'p'" do
+ it_behaves_like :string_unpack_basic, 'p'
+ it_behaves_like :string_unpack_taint, 'p'
+
+ it "round-trips a string through pack and unpack" do
+ ["hello"].pack("p").unpack("p").should == ["hello"]
+ end
+
+ it "cannot unpack a string except from the same object that created it, or a duplicate of it" do
+ packed = ["hello"].pack("p")
+ packed.unpack("p").should == ["hello"]
+ packed.dup.unpack("p").should == ["hello"]
+ -> { packed.to_sym.to_s.unpack("p") }.should.raise(ArgumentError, /no associated pointer/)
+ end
+end
diff --git a/spec/ruby/core/string/unpack/percent_spec.rb b/spec/ruby/core/string/unpack/percent_spec.rb
new file mode 100644
index 0000000000..7142bbf241
--- /dev/null
+++ b/spec/ruby/core/string/unpack/percent_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../../spec_helper'
+
+describe "String#unpack with format '%'" do
+ it "raises an Argument Error" do
+ -> { "abc".unpack("%") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/unpack/q_spec.rb b/spec/ruby/core/string/unpack/q_spec.rb
new file mode 100644
index 0000000000..2f667d6c4d
--- /dev/null
+++ b/spec/ruby/core/string/unpack/q_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+describe "String#unpack with format 'Q'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_64bit_le, 'Q<'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'Q<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_64bit_be, 'Q>'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'Q>'
+ end
+end
+
+describe "String#unpack with format 'q'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_64bit_le, 'q<'
+ it_behaves_like :string_unpack_64bit_le_signed, 'q<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_64bit_be, 'q>'
+ it_behaves_like :string_unpack_64bit_be_signed, 'q>'
+ end
+end
+
+describe "String#unpack with format 'Q'" do
+ it_behaves_like :string_unpack_basic, 'Q'
+end
+
+describe "String#unpack with format 'q'" do
+ it_behaves_like :string_unpack_basic, 'q'
+end
+
+little_endian do
+ describe "String#unpack with format 'Q'" do
+ it_behaves_like :string_unpack_64bit_le, 'Q'
+ it_behaves_like :string_unpack_64bit_le_extra, 'Q'
+ it_behaves_like :string_unpack_64bit_le_unsigned, 'Q'
+ end
+
+ describe "String#unpack with format 'q'" do
+ it_behaves_like :string_unpack_64bit_le, 'q'
+ it_behaves_like :string_unpack_64bit_le_extra, 'q'
+ it_behaves_like :string_unpack_64bit_le_signed, 'q'
+ end
+end
+
+big_endian do
+ describe "String#unpack with format 'Q'" do
+ it_behaves_like :string_unpack_64bit_be, 'Q'
+ it_behaves_like :string_unpack_64bit_be_extra, 'Q'
+ it_behaves_like :string_unpack_64bit_be_unsigned, 'Q'
+ end
+
+ describe "String#unpack with format 'q'" do
+ it_behaves_like :string_unpack_64bit_be, 'q'
+ it_behaves_like :string_unpack_64bit_be_extra, 'q'
+ it_behaves_like :string_unpack_64bit_be_signed, 'q'
+ end
+end
diff --git a/spec/ruby/core/string/unpack/r_spec.rb b/spec/ruby/core/string/unpack/r_spec.rb
new file mode 100644
index 0000000000..a385951aa8
--- /dev/null
+++ b/spec/ruby/core/string/unpack/r_spec.rb
@@ -0,0 +1,85 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+ruby_version_is "4.1" do
+ describe "String#unpack with format 'R'" do
+ it_behaves_like :string_unpack_basic, 'R'
+ it_behaves_like :string_unpack_no_platform, 'R'
+
+ it "decodes a ULEB128 integer" do
+ [ ["\x00", [0]],
+ ["\x01", [1]],
+ ["\x7f", [127]],
+ ["\x80\x01", [128]],
+ ["\xff\x7f", [0x3fff]],
+ ["\x80\x80\x01", [0x4000]],
+ ["\xff\xff\xff\xff\x0f", [0xffffffff]],
+ ["\x80\x80\x80\x80\x10", [0x100000000]],
+ ["\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", [0xffff_ffff_ffff_ffff]],
+ ].should be_computed_by(:unpack, "R")
+ end
+
+ it "decodes multiple values with '*' modifier" do
+ "\x01\x02".unpack("R*").should == [1, 2]
+ "\x7f\x80\x01".unpack("R*").should == [127, 128]
+ end
+
+ it "returns nil for incomplete data" do
+ "\xFF".unpack("R").should == [nil]
+ "\xFF".unpack1("R").should == nil
+ end
+
+ it "returns nil for remaining incomplete values after a valid one" do
+ bytes = [256].pack("R")
+ (bytes + "\xFF").unpack("RRRR").should == [256, nil, nil, nil]
+ end
+
+ it "skips incomplete values with '*' modifier" do
+ "\xFF".unpack("R*").should == []
+ end
+ end
+
+ describe "String#unpack with format 'r'" do
+ it_behaves_like :string_unpack_basic, 'r'
+ it_behaves_like :string_unpack_no_platform, 'r'
+
+ it "decodes a SLEB128 integer" do
+ [ ["\x00", [0]],
+ ["\x01", [1]],
+ ["\x7f", [-1]],
+ ["\x7e", [-2]],
+ ["\xff\x00", [127]],
+ ["\x80\x01", [128]],
+ ["\x81\x7f", [-127]],
+ ["\x80\x7f", [-128]],
+ ].should be_computed_by(:unpack, "r")
+ end
+
+ it "decodes larger numbers" do
+ "\xff\xff\x00".unpack("r").should == [0x3fff]
+ "\x80\x80\x01".unpack("r").should == [0x4000]
+ "\x81\x80\x7f".unpack("r").should == [-0x3fff]
+ "\x80\x80\x7f".unpack("r").should == [-0x4000]
+ end
+
+ it "decodes multiple values with '*' modifier" do
+ "\x00\x01\x7f".unpack("r*").should == [0, 1, -1]
+ end
+
+ it "returns nil for incomplete data" do
+ "\xFF".unpack("r").should == [nil]
+ "\xFF".unpack1("r").should == nil
+ end
+
+ it "returns nil for remaining incomplete values after a valid one" do
+ bytes = [256].pack("r")
+ (bytes + "\xFF").unpack("rrrr").should == [256, nil, nil, nil]
+ end
+
+ it "skips incomplete values with '*' modifier" do
+ "\xFF".unpack("r*").should == []
+ end
+ end
+end
diff --git a/spec/ruby/core/string/unpack/s_spec.rb b/spec/ruby/core/string/unpack/s_spec.rb
new file mode 100644
index 0000000000..d331fd720e
--- /dev/null
+++ b/spec/ruby/core/string/unpack/s_spec.rb
@@ -0,0 +1,152 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+describe "String#unpack with format 'S'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_16bit_le, 'S<'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_16bit_le, 'S<_'
+ it_behaves_like :string_unpack_16bit_le, 'S_<'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S_<'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S<_'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_16bit_le, 'S<!'
+ it_behaves_like :string_unpack_16bit_le, 'S!<'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S!<'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S<!'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_16bit_be, 'S>'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_16bit_be, 'S>_'
+ it_behaves_like :string_unpack_16bit_be, 'S_>'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S>_'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_16bit_be, 'S>!'
+ it_behaves_like :string_unpack_16bit_be, 'S!>'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S>!'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S!>'
+ end
+end
+
+describe "String#unpack with format 's'" do
+ describe "with modifier '<'" do
+ it_behaves_like :string_unpack_16bit_le, 's<'
+ it_behaves_like :string_unpack_16bit_le_signed, 's<'
+ end
+
+ describe "with modifier '<' and '_'" do
+ it_behaves_like :string_unpack_16bit_le, 's<_'
+ it_behaves_like :string_unpack_16bit_le, 's_<'
+ it_behaves_like :string_unpack_16bit_le_signed, 's<_'
+ it_behaves_like :string_unpack_16bit_le_signed, 's_<'
+ end
+
+ describe "with modifier '<' and '!'" do
+ it_behaves_like :string_unpack_16bit_le, 's<!'
+ it_behaves_like :string_unpack_16bit_le, 's!<'
+ it_behaves_like :string_unpack_16bit_le_signed, 's<!'
+ it_behaves_like :string_unpack_16bit_le_signed, 's!<'
+ end
+
+ describe "with modifier '>'" do
+ it_behaves_like :string_unpack_16bit_be, 's>'
+ it_behaves_like :string_unpack_16bit_be_signed, 's>'
+ end
+
+ describe "with modifier '>' and '_'" do
+ it_behaves_like :string_unpack_16bit_be, 's>_'
+ it_behaves_like :string_unpack_16bit_be, 's_>'
+ it_behaves_like :string_unpack_16bit_be_signed, 's>_'
+ it_behaves_like :string_unpack_16bit_be_signed, 's_>'
+ end
+
+ describe "with modifier '>' and '!'" do
+ it_behaves_like :string_unpack_16bit_be, 's>!'
+ it_behaves_like :string_unpack_16bit_be, 's!>'
+ it_behaves_like :string_unpack_16bit_be_signed, 's>!'
+ it_behaves_like :string_unpack_16bit_be_signed, 's!>'
+ end
+end
+
+little_endian do
+ describe "String#unpack with format 'S'" do
+ it_behaves_like :string_unpack_basic, 'S'
+ it_behaves_like :string_unpack_16bit_le, 'S'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S'
+ end
+
+ describe "String#unpack with format 'S' with modifier '_'" do
+ it_behaves_like :string_unpack_16bit_le, 'S_'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S_'
+ end
+
+ describe "String#unpack with format 'S' with modifier '!'" do
+ it_behaves_like :string_unpack_16bit_le, 'S!'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'S!'
+ end
+
+ describe "String#unpack with format 's'" do
+ it_behaves_like :string_unpack_basic, 's'
+ it_behaves_like :string_unpack_16bit_le, 's'
+ it_behaves_like :string_unpack_16bit_le_signed, 's'
+ end
+
+ describe "String#unpack with format 's' with modifier '_'" do
+ it_behaves_like :string_unpack_16bit_le, 's_'
+ it_behaves_like :string_unpack_16bit_le_signed, 's_'
+ end
+
+ describe "String#unpack with format 's' with modifier '!'" do
+ it_behaves_like :string_unpack_16bit_le, 's!'
+ it_behaves_like :string_unpack_16bit_le_signed, 's!'
+ end
+end
+
+big_endian do
+ describe "String#unpack with format 'S'" do
+ it_behaves_like :string_unpack_basic, 'S'
+ it_behaves_like :string_unpack_16bit_be, 'S'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S'
+ end
+
+ describe "String#unpack with format 'S' with modifier '_'" do
+ it_behaves_like :string_unpack_16bit_be, 'S_'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S_'
+ end
+
+ describe "String#unpack with format 'S' with modifier '!'" do
+ it_behaves_like :string_unpack_16bit_be, 'S!'
+ it_behaves_like :string_unpack_16bit_be_unsigned, 'S!'
+ end
+
+ describe "String#unpack with format 's'" do
+ it_behaves_like :string_unpack_basic, 's'
+ it_behaves_like :string_unpack_16bit_be, 's'
+ it_behaves_like :string_unpack_16bit_be_signed, 's'
+ end
+
+ describe "String#unpack with format 's' with modifier '_'" do
+ it_behaves_like :string_unpack_16bit_be, 's_'
+ it_behaves_like :string_unpack_16bit_be_signed, 's_'
+ end
+
+ describe "String#unpack with format 's' with modifier '!'" do
+ it_behaves_like :string_unpack_16bit_be, 's!'
+ it_behaves_like :string_unpack_16bit_be_signed, 's!'
+ end
+end
diff --git a/spec/ruby/core/string/unpack/shared/basic.rb b/spec/ruby/core/string/unpack/shared/basic.rb
new file mode 100644
index 0000000000..2ee2d6899a
--- /dev/null
+++ b/spec/ruby/core/string/unpack/shared/basic.rb
@@ -0,0 +1,27 @@
+describe :string_unpack_basic, shared: true do
+ it "ignores whitespace in the format string" do
+ "abc".unpack("a \t\n\v\f\r"+unpack_format).should.instance_of?(Array)
+ end
+
+ it "calls #to_str to coerce the directives string" do
+ d = mock("unpack directive")
+ d.should_receive(:to_str).and_return("a"+unpack_format)
+ "abc".unpack(d).should.instance_of?(Array)
+ end
+
+ it "raises ArgumentError when a directive is unknown" do
+ -> { "abcdefgh".unpack("a K" + unpack_format) }.should.raise(ArgumentError, "unknown unpack directive 'K' in 'a K#{unpack_format}'")
+ -> { "abcdefgh".unpack("a 0" + unpack_format) }.should.raise(ArgumentError, "unknown unpack directive '0' in 'a 0#{unpack_format}'")
+ -> { "abcdefgh".unpack("a :" + unpack_format) }.should.raise(ArgumentError, "unknown unpack directive ':' in 'a :#{unpack_format}'")
+ end
+end
+
+describe :string_unpack_no_platform, shared: true do
+ it "raises an ArgumentError when the format modifier is '_'" do
+ -> { "abcdefgh".unpack(unpack_format("_")) }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError when the format modifier is '!'" do
+ -> { "abcdefgh".unpack(unpack_format("!")) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/unpack/shared/float.rb b/spec/ruby/core/string/unpack/shared/float.rb
new file mode 100644
index 0000000000..5525c3fe73
--- /dev/null
+++ b/spec/ruby/core/string/unpack/shared/float.rb
@@ -0,0 +1,277 @@
+# encoding: binary
+
+describe :string_unpack_float_le, shared: true do
+ it "decodes one float for a single format character" do
+ "\x8f\xc2\xb5?".unpack(unpack_format).should == [1.4199999570846558]
+ end
+
+ it "decodes a negative float" do
+ "\xcd\xcc\x08\xc2".unpack(unpack_format).should == [-34.200000762939453]
+ end
+
+ it "decodes two floats for two format characters" do
+ array = "\x9a\x999@33\xb3?".unpack(unpack_format(nil, 2))
+ array.should == [2.9000000953674316, 1.399999976158142]
+ end
+
+ it "decodes the number of floats requested by the count modifier" do
+ array = "\x9a\x999@33\xb3?33\x03A".unpack(unpack_format(3))
+ array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137]
+ end
+
+ it "decodes the remaining floats when passed the '*' modifier" do
+ array = "\x9a\x999@33\xb3?33\x03A".unpack(unpack_format("*"))
+ array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137]
+ end
+
+ it "decodes the remaining floats when passed the '*' modifier after another directive" do
+ array = "\x9a\x99\xa9@33\x13A".unpack(unpack_format()+unpack_format('*'))
+ array.should == [5.300000190734863, 9.199999809265137]
+ end
+
+ it "does not decode a float when fewer bytes than a float remain and the '*' modifier is passed" do
+ [ ["\xff", []],
+ ["\xff\x00", []],
+ ["\xff\x00\xff", []]
+ ].should be_computed_by(:unpack, unpack_format("*"))
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["abc", [nil, nil, nil]],
+ ["\x8f\xc2\xb5?abc", [1.4199999570846558, nil, nil]],
+ ["\x9a\x999@33\xb3?abc", [2.9000000953674316, 1.399999976158142, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "decodes positive Infinity" do
+ "\x00\x00\x80\x7f".unpack(unpack_format).should == [infinity_value]
+ end
+
+ it "decodes negative Infinity" do
+ "\x00\x00\x80\xff".unpack(unpack_format).should == [-infinity_value]
+ end
+
+ it "decodes NaN" do
+ # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884
+ [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should == true
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x9a\x999@33\xb3?".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ array = "\x9a\x999@33\xb3?".unpack(unpack_format(' ', 2))
+ array.should == [2.9000000953674316, 1.399999976158142]
+ end
+end
+
+describe :string_unpack_float_be, shared: true do
+ it "decodes one float for a single format character" do
+ "?\xb5\xc2\x8f".unpack(unpack_format).should == [1.4199999570846558]
+ end
+
+ it "decodes a negative float" do
+ "\xc2\x08\xcc\xcd".unpack(unpack_format).should == [-34.200000762939453]
+ end
+
+ it "decodes two floats for two format characters" do
+ array = "@9\x99\x9a?\xb333".unpack(unpack_format(nil, 2))
+ array.should == [2.9000000953674316, 1.399999976158142]
+ end
+
+ it "decodes the number of floats requested by the count modifier" do
+ array = "@9\x99\x9a?\xb333A\x0333".unpack(unpack_format(3))
+ array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137]
+ end
+
+ it "decodes the remaining floats when passed the '*' modifier" do
+ array = "@9\x99\x9a?\xb333A\x0333".unpack(unpack_format("*"))
+ array.should == [2.9000000953674316, 1.399999976158142, 8.199999809265137]
+ end
+
+ it "decodes the remaining floats when passed the '*' modifier after another directive" do
+ array = "@\xa9\x99\x9aA\x1333".unpack(unpack_format()+unpack_format('*'))
+ array.should == [5.300000190734863, 9.199999809265137]
+ end
+
+ it "does not decode a float when fewer bytes than a float remain and the '*' modifier is passed" do
+ [ ["\xff", []],
+ ["\xff\x00", []],
+ ["\xff\x00\xff", []]
+ ].should be_computed_by(:unpack, unpack_format("*"))
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["abc", [nil, nil, nil]],
+ ["?\xb5\xc2\x8fabc", [1.4199999570846558, nil, nil]],
+ ["@9\x99\x9a?\xb333abc", [2.9000000953674316, 1.399999976158142, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "decodes positive Infinity" do
+ "\x7f\x80\x00\x00".unpack(unpack_format).should == [infinity_value]
+ end
+
+ it "decodes negative Infinity" do
+ "\xff\x80\x00\x00".unpack(unpack_format).should == [-infinity_value]
+ end
+
+ it "decodes NaN" do
+ # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884
+ [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should == true
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "@9\x99\x9a?\xb333".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ array = "@9\x99\x9a?\xb333".unpack(unpack_format(' ', 2))
+ array.should == [2.9000000953674316, 1.399999976158142]
+ end
+end
+
+describe :string_unpack_double_le, shared: true do
+ it "decodes one double for a single format character" do
+ "\xb8\x1e\x85\xebQ\xb8\xf6?".unpack(unpack_format).should == [1.42]
+ end
+
+ it "decodes a negative double" do
+ "\x9a\x99\x99\x99\x99\x19A\xc0".unpack(unpack_format).should == [-34.2]
+ end
+
+ it "decodes two doubles for two format characters" do
+ "333333\x07@ffffff\xf6?".unpack(unpack_format(nil, 2)).should == [2.9, 1.4]
+ end
+
+ it "decodes the number of doubles requested by the count modifier" do
+ array = "333333\x07@ffffff\xf6?ffffff\x20@".unpack(unpack_format(3))
+ array.should == [2.9, 1.4, 8.2]
+ end
+
+ it "decodes the remaining doubles when passed the '*' modifier" do
+ array = "333333\x07@ffffff\xf6?ffffff\x20@".unpack(unpack_format("*"))
+ array.should == [2.9, 1.4, 8.2]
+ end
+
+ it "decodes the remaining doubles when passed the '*' modifier after another directive" do
+ array = "333333\x15@ffffff\x22@".unpack(unpack_format()+unpack_format('*'))
+ array.should == [5.3, 9.2]
+ end
+
+ it "does not decode a double when fewer bytes than a double remain and the '*' modifier is passed" do
+ [ ["\xff", []],
+ ["\xff\x00", []],
+ ["\xff\x00\xff", []],
+ ["\xff\x00\xff\x00", []],
+ ["\xff\x00\xff\x00\xff", []],
+ ["\xff\x00\xff\x00\xff\x00", []],
+ ["\xff\x00\xff\x00\xff\x00\xff", []]
+ ].should be_computed_by(:unpack, unpack_format("*"))
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["\xff\x00\xff\x00\xff\x00\xff", [nil, nil, nil]],
+ ["\xb8\x1e\x85\xebQ\xb8\xf6?abc", [1.42, nil, nil]],
+ ["333333\x07@ffffff\xf6?abcd", [2.9, 1.4, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "decodes positive Infinity" do
+ "\x00\x00\x00\x00\x00\x00\xf0\x7f".unpack(unpack_format).should == [infinity_value]
+ end
+
+ it "decodes negative Infinity" do
+ "\x00\x00\x00\x00\x00\x00\xf0\xff".unpack(unpack_format).should == [-infinity_value]
+ end
+
+ it "decodes NaN" do
+ # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884
+ [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should == true
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "333333\x07@ffffff\xf6?".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "333333\x07@ffffff\xf6?".unpack(unpack_format(' ', 2)).should == [2.9, 1.4]
+ end
+end
+
+describe :string_unpack_double_be, shared: true do
+ it "decodes one double for a single format character" do
+ "?\xf6\xb8Q\xeb\x85\x1e\xb8".unpack(unpack_format).should == [1.42]
+ end
+
+ it "decodes a negative double" do
+ "\xc0A\x19\x99\x99\x99\x99\x9a".unpack(unpack_format).should == [-34.2]
+ end
+
+ it "decodes two doubles for two format characters" do
+ "@\x07333333?\xf6ffffff".unpack(unpack_format(nil, 2)).should == [2.9, 1.4]
+ end
+
+ it "decodes the number of doubles requested by the count modifier" do
+ array = "@\x07333333?\xf6ffffff@\x20ffffff".unpack(unpack_format(3))
+ array.should == [2.9, 1.4, 8.2]
+ end
+
+ it "decodes the remaining doubles when passed the '*' modifier" do
+ array = "@\x07333333?\xf6ffffff@\x20ffffff".unpack(unpack_format("*"))
+ array.should == [2.9, 1.4, 8.2]
+ end
+
+ it "decodes the remaining doubles when passed the '*' modifier after another directive" do
+ array = "@\x15333333@\x22ffffff".unpack(unpack_format()+unpack_format('*'))
+ array.should == [5.3, 9.2]
+ end
+
+ it "does not decode a double when fewer bytes than a double remain and the '*' modifier is passed" do
+ [ ["\xff", []],
+ ["\xff\x00", []],
+ ["\xff\x00\xff", []],
+ ["\xff\x00\xff\x00", []],
+ ["\xff\x00\xff\x00\xff", []],
+ ["\xff\x00\xff\x00\xff\x00", []],
+ ["\xff\x00\xff\x00\xff\x00\xff", []]
+ ].should be_computed_by(:unpack, unpack_format("*"))
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["abcdefg", [nil, nil, nil]],
+ ["?\xf6\xb8Q\xeb\x85\x1e\xb8abc", [1.42, nil, nil]],
+ ["@\x07333333?\xf6ffffffabcd", [2.9, 1.4, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "decodes positive Infinity" do
+ "\x7f\xf0\x00\x00\x00\x00\x00\x00".unpack(unpack_format).should == [infinity_value]
+ end
+
+ it "decodes negative Infinity" do
+ "\xff\xf0\x00\x00\x00\x00\x00\x00".unpack(unpack_format).should == [-infinity_value]
+ end
+
+ it "decodes NaN" do
+ # mumble mumble NaN mumble https://bugs.ruby-lang.org/issues/5884
+ [nan_value].pack(unpack_format).unpack(unpack_format).first.nan?.should == true
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "@\x07333333?\xf6ffffff".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "@\x07333333?\xf6ffffff".unpack(unpack_format(' ', 2)).should == [2.9, 1.4]
+ end
+end
diff --git a/spec/ruby/core/string/unpack/shared/integer.rb b/spec/ruby/core/string/unpack/shared/integer.rb
new file mode 100644
index 0000000000..c66156536b
--- /dev/null
+++ b/spec/ruby/core/string/unpack/shared/integer.rb
@@ -0,0 +1,349 @@
+# encoding: binary
+
+describe :string_unpack_16bit_le, shared: true do
+ it "decodes one short for a single format character" do
+ "ab".unpack(unpack_format).should == [25185]
+ end
+
+ it "decodes two shorts for two format characters" do
+ "abcd".unpack(unpack_format(nil, 2)).should == [25185, 25699]
+ end
+
+ it "decodes the number of shorts requested by the count modifier" do
+ "abcdef".unpack(unpack_format(3)).should == [25185, 25699, 26213]
+ end
+
+ it "decodes the remaining shorts when passed the '*' modifier" do
+ "abcd".unpack(unpack_format('*')).should == [25185, 25699]
+ end
+
+ it "decodes the remaining shorts when passed the '*' modifier after another directive" do
+ "abcd".unpack(unpack_format()+unpack_format('*')).should == [25185, 25699]
+ end
+
+ it "does not decode a short when fewer bytes than a short remain and the '*' modifier is passed" do
+ "\xff".unpack(unpack_format('*')).should == []
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["abc", [25185, nil, nil]],
+ ["abcd", [25185, 25699, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "abcd".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "abcd".unpack(unpack_format(' ', 2)).should == [25185, 25699]
+ end
+end
+
+describe :string_unpack_16bit_le_signed, shared: true do
+ it "decodes a short with most significant bit set as a negative number" do
+ "\x00\xff".unpack(unpack_format()).should == [-256]
+ end
+end
+
+describe :string_unpack_16bit_le_unsigned, shared: true do
+ it "decodes a short with most significant bit set as a positive number" do
+ "\x00\xff".unpack(unpack_format()).should == [65280]
+ end
+end
+
+describe :string_unpack_16bit_be, shared: true do
+ it "decodes one short for a single format character" do
+ "ba".unpack(unpack_format).should == [25185]
+ end
+
+ it "decodes two shorts for two format characters" do
+ "badc".unpack(unpack_format(nil, 2)).should == [25185, 25699]
+ end
+
+ it "decodes the number of shorts requested by the count modifier" do
+ "badcfe".unpack(unpack_format(3)).should == [25185, 25699, 26213]
+ end
+
+ it "decodes the remaining shorts when passed the '*' modifier" do
+ "badc".unpack(unpack_format('*')).should == [25185, 25699]
+ end
+
+ it "decodes the remaining shorts when passed the '*' modifier after another directive" do
+ "badc".unpack(unpack_format()+unpack_format('*')).should == [25185, 25699]
+ end
+
+ it "does not decode a short when fewer bytes than a short remain and the '*' modifier is passed" do
+ "\xff".unpack(unpack_format('*')).should == []
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["bac", [25185, nil, nil]],
+ ["badc", [25185, 25699, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "badc".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "badc".unpack(unpack_format(' ', 2)).should == [25185, 25699]
+ end
+end
+
+describe :string_unpack_16bit_be_signed, shared: true do
+ it "decodes a short with most significant bit set as a negative number" do
+ "\xff\x00".unpack(unpack_format()).should == [-256]
+ end
+end
+
+describe :string_unpack_16bit_be_unsigned, shared: true do
+ it "decodes a short with most significant bit set as a positive number" do
+ "\xff\x00".unpack(unpack_format()).should == [65280]
+ end
+end
+
+describe :string_unpack_32bit_le, shared: true do
+ it "decodes one int for a single format character" do
+ "abcd".unpack(unpack_format).should == [1684234849]
+ end
+
+ it "decodes two ints for two format characters" do
+ "abghefcd".unpack(unpack_format(nil, 2)).should == [1751605857, 1684235877]
+ end
+
+ it "decodes the number of ints requested by the count modifier" do
+ "abcedfgh".unpack(unpack_format(2)).should == [1701012065, 1751606884]
+ end
+
+ it "decodes the remaining ints when passed the '*' modifier" do
+ "acbdegfh".unpack(unpack_format('*')).should == [1684169569, 1751541605]
+ end
+
+ it "decodes the remaining ints when passed the '*' modifier after another directive" do
+ "abcdefgh".unpack(unpack_format()+unpack_format('*')).should == [1684234849, 1751606885]
+ end
+
+ it "does not decode an int when fewer bytes than an int remain and the '*' modifier is passed" do
+ "abc".unpack(unpack_format('*')).should == []
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["abcde", [1684234849, nil, nil]],
+ ["abcdefg", [1684234849, nil, nil]],
+ ["abcdefgh", [1684234849, 1751606885, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "abcdefgh".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "abcdefgh".unpack(unpack_format(' ', 2)).should == [1684234849, 1751606885]
+ end
+end
+
+describe :string_unpack_32bit_le_signed, shared: true do
+ it "decodes an int with most significant bit set as a negative number" do
+ "\x00\xaa\x00\xff".unpack(unpack_format()).should == [-16733696]
+ end
+end
+
+describe :string_unpack_32bit_le_unsigned, shared: true do
+ it "decodes an int with most significant bit set as a positive number" do
+ "\x00\xaa\x00\xff".unpack(unpack_format()).should == [4278233600]
+ end
+end
+
+describe :string_unpack_32bit_be, shared: true do
+ it "decodes one int for a single format character" do
+ "dcba".unpack(unpack_format).should == [1684234849]
+ end
+
+ it "decodes two ints for two format characters" do
+ "hgbadcfe".unpack(unpack_format(nil, 2)).should == [1751605857, 1684235877]
+ end
+
+ it "decodes the number of ints requested by the count modifier" do
+ "ecbahgfd".unpack(unpack_format(2)).should == [1701012065, 1751606884]
+ end
+
+ it "decodes the remaining ints when passed the '*' modifier" do
+ "dbcahfge".unpack(unpack_format('*')).should == [1684169569, 1751541605]
+ end
+
+ it "decodes the remaining ints when passed the '*' modifier after another directive" do
+ "dcbahgfe".unpack(unpack_format()+unpack_format('*')).should == [1684234849, 1751606885]
+ end
+
+ it "does not decode an int when fewer bytes than an int remain and the '*' modifier is passed" do
+ "abc".unpack(unpack_format('*')).should == []
+ end
+
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["dcbae", [1684234849, nil, nil]],
+ ["dcbaefg", [1684234849, nil, nil]],
+ ["dcbahgfe", [1684234849, 1751606885, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "dcbahgfe".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "dcbahgfe".unpack(unpack_format(' ', 2)).should == [1684234849, 1751606885]
+ end
+end
+
+describe :string_unpack_32bit_be_signed, shared: true do
+ it "decodes an int with most significant bit set as a negative number" do
+ "\xff\x00\xaa\x00".unpack(unpack_format()).should == [-16733696]
+ end
+end
+
+describe :string_unpack_32bit_be_unsigned, shared: true do
+ it "decodes an int with most significant bit set as a positive number" do
+ "\xff\x00\xaa\x00".unpack(unpack_format()).should == [4278233600]
+ end
+end
+
+describe :string_unpack_64bit_le, shared: true do
+ it "decodes one long for a single format character" do
+ "abcdefgh".unpack(unpack_format).should == [7523094288207667809]
+ end
+
+ it "decodes two longs for two format characters" do
+ array = "abghefcdghefabcd".unpack(unpack_format(nil, 2))
+ array.should == [7233738012216484449, 7233733596956420199]
+ end
+
+ it "decodes the number of longs requested by the count modifier" do
+ array = "abcedfghefcdghef".unpack(unpack_format(2))
+ array.should == [7523094283929477729, 7378418357791581797]
+ end
+
+ it "decodes the remaining longs when passed the '*' modifier" do
+ array = "acbdegfhdegfhacb".unpack(unpack_format('*'))
+ array.should == [7522813912742519649, 7089617339433837924]
+ end
+
+ it "decodes the remaining longs when passed the '*' modifier after another directive" do
+ array = "bcahfgedhfgedbca".unpack(unpack_format()+unpack_format('*'))
+ array.should == [7234302065976107874, 7017560827710891624]
+ end
+
+ it "does not decode a long when fewer bytes than a long remain and the '*' modifier is passed" do
+ "abc".unpack(unpack_format('*')).should == []
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "badc".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ array = "abcdefghabghefcd".unpack(unpack_format(' ', 2))
+ array.should == [7523094288207667809, 7233738012216484449]
+ end
+end
+
+describe :string_unpack_64bit_le_extra, shared: true do
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["abcdefgh", [7523094288207667809, nil, nil]],
+ ["abcdefghcdefab", [7523094288207667809, nil, nil]],
+ ["abcdefghcdefabde", [7523094288207667809, 7306072665971057763, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+end
+
+describe :string_unpack_64bit_le_signed, shared: true do
+ it "decodes a long with most significant bit set as a negative number" do
+ "\x00\xcc\x00\xbb\x00\xaa\x00\xff".unpack(unpack_format()).should == [-71870673923814400]
+ end
+end
+
+describe :string_unpack_64bit_le_unsigned, shared: true do
+ it "decodes a long with most significant bit set as a positive number" do
+ "\x00\xcc\x00\xbb\x00\xaa\x00\xff".unpack(unpack_format()).should == [18374873399785737216]
+ end
+end
+
+describe :string_unpack_64bit_be, shared: true do
+ it "decodes one long for a single format character" do
+ "hgfedcba".unpack(unpack_format).should == [7523094288207667809]
+ end
+
+ it "decodes two longs for two format characters" do
+ array = "dcfehgbadcbafehg".unpack(unpack_format(nil, 2))
+ array.should == [7233738012216484449, 7233733596956420199]
+ end
+
+ it "decodes the number of longs requested by the count modifier" do
+ array = "hgfdecbafehgdcfe".unpack(unpack_format(2))
+ array.should == [7523094283929477729, 7378418357791581797]
+ end
+
+ it "decodes the remaining longs when passed the '*' modifier" do
+ array = "hfgedbcabcahfged".unpack(unpack_format('*'))
+ array.should == [7522813912742519649, 7089617339433837924]
+ end
+
+ it "decodes the remaining longs when passed the '*' modifier after another directive" do
+ array = "degfhacbacbdegfh".unpack(unpack_format()+unpack_format('*'))
+ array.should == [7234302065976107874, 7017560827710891624]
+ end
+
+ it "does not decode a long when fewer bytes than a long remain and the '*' modifier is passed" do
+ "abc".unpack(unpack_format('*')).should == []
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "hgfedcbadcfehgba".unpack(unpack_format("\000", 2))
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ array = "hgfedcbadcfehgba".unpack(unpack_format(' ', 2))
+ array.should == [7523094288207667809, 7233738012216484449]
+ end
+end
+
+describe :string_unpack_64bit_be_extra, shared: true do
+ it "adds nil for each element requested beyond the end of the String" do
+ [ ["", [nil, nil, nil]],
+ ["hgfedcba", [7523094288207667809, nil, nil]],
+ ["hgfedcbacdefab", [7523094288207667809, nil, nil]],
+ ["hgfedcbaedbafedc", [7523094288207667809, 7306072665971057763, nil]]
+ ].should be_computed_by(:unpack, unpack_format(3))
+ end
+end
+
+describe :string_unpack_64bit_be_signed, shared: true do
+ it "decodes a long with most significant bit set as a negative number" do
+ "\xff\x00\xaa\x00\xbb\x00\xcc\x00".unpack(unpack_format()).should == [-71870673923814400]
+ end
+end
+
+describe :string_unpack_64bit_be_unsigned, shared: true do
+ it "decodes a long with most significant bit set as a positive number" do
+ "\xff\x00\xaa\x00\xbb\x00\xcc\x00".unpack(unpack_format()).should == [18374873399785737216]
+ end
+end
diff --git a/spec/ruby/core/string/unpack/shared/string.rb b/spec/ruby/core/string/unpack/shared/string.rb
new file mode 100644
index 0000000000..9d85eedf26
--- /dev/null
+++ b/spec/ruby/core/string/unpack/shared/string.rb
@@ -0,0 +1,51 @@
+describe :string_unpack_string, shared: true do
+ it "returns an empty string if the input is empty" do
+ "".unpack(unpack_format).should == [""]
+ end
+
+ it "returns empty strings for repeated formats if the input is empty" do
+ "".unpack(unpack_format(nil, 3)).should == ["", "", ""]
+ end
+
+ it "returns an empty string and does not decode any bytes when the count modifier is zero" do
+ "abc".unpack(unpack_format(0)+unpack_format).should == ["", "a"]
+ end
+
+ it "implicitly has a count of one when no count is specified" do
+ "abc".unpack(unpack_format).should == ["a"]
+ end
+
+ it "decodes the number of bytes specified by the count modifier" do
+ "abc".unpack(unpack_format(3)).should == ["abc"]
+ end
+
+ it "decodes the number of bytes specified by the count modifier including whitespace bytes" do
+ [ ["a bc", ["a b", "c"]],
+ ["a\fbc", ["a\fb", "c"]],
+ ["a\nbc", ["a\nb", "c"]],
+ ["a\rbc", ["a\rb", "c"]],
+ ["a\tbc", ["a\tb", "c"]],
+ ["a\vbc", ["a\vb", "c"]]
+ ].should be_computed_by(:unpack, unpack_format(3)+unpack_format)
+ end
+
+ it "decodes past whitespace bytes when passed the '*' modifier" do
+ [ ["a b c", ["a b c"]],
+ ["a\fb c", ["a\fb c"]],
+ ["a\nb c", ["a\nb c"]],
+ ["a\rb c", ["a\rb c"]],
+ ["a\tb c", ["a\tb c"]],
+ ["a\vb c", ["a\vb c"]],
+ ].should be_computed_by(:unpack, unpack_format("*"))
+ end
+end
+
+describe :string_unpack_Aa, shared: true do
+ it "decodes the number of bytes specified by the count modifier including NULL bytes" do
+ "a\x00bc".unpack(unpack_format(3)+unpack_format).should == ["a\x00b", "c"]
+ end
+
+ it "decodes past NULL bytes when passed the '*' modifier" do
+ "a\x00b c".unpack(unpack_format("*")).should == ["a\x00b c"]
+ end
+end
diff --git a/spec/ruby/core/string/unpack/shared/taint.rb b/spec/ruby/core/string/unpack/shared/taint.rb
new file mode 100644
index 0000000000..79c7251f01
--- /dev/null
+++ b/spec/ruby/core/string/unpack/shared/taint.rb
@@ -0,0 +1,2 @@
+describe :string_unpack_taint, shared: true do
+end
diff --git a/spec/ruby/core/string/unpack/shared/unicode.rb b/spec/ruby/core/string/unpack/shared/unicode.rb
new file mode 100644
index 0000000000..9b4e0c09de
--- /dev/null
+++ b/spec/ruby/core/string/unpack/shared/unicode.rb
@@ -0,0 +1,62 @@
+# -*- encoding: utf-8 -*-
+
+describe :string_unpack_unicode, shared: true do
+ it "decodes Unicode codepoints as ASCII values" do
+ [ ["\x00", [0]],
+ ["\x01", [1]],
+ ["\x08", [8]],
+ ["\x0f", [15]],
+ ["\x18", [24]],
+ ["\x1f", [31]],
+ ["\x7f", [127]],
+ ["\xc2\x80", [128]],
+ ["\xc2\x81", [129]],
+ ["\xc3\xbf", [255]]
+ ].should be_computed_by(:unpack, "U")
+ end
+
+ it "decodes the number of characters specified by the count modifier" do
+ [ ["\xc2\x80\xc2\x81\xc2\x82\xc2\x83", "U1", [0x80]],
+ ["\xc2\x80\xc2\x81\xc2\x82\xc2\x83", "U2", [0x80, 0x81]],
+ ["\xc2\x80\xc2\x81\xc2\x82\xc2\x83", "U3", [0x80, 0x81, 0x82]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "implicitly has a count of one when no count modifier is passed" do
+ "\xc2\x80\xc2\x81\xc2\x82\xc2\x83".unpack("U1").should == [0x80]
+ end
+
+ it "decodes all remaining characters when passed the '*' modifier" do
+ "\xc2\x80\xc2\x81\xc2\x82\xc2\x83".unpack("U*").should == [0x80, 0x81, 0x82, 0x83]
+ end
+
+ it "decodes UTF-8 BMP codepoints" do
+ [ ["\xc2\x80", [0x80]],
+ ["\xdf\xbf", [0x7ff]],
+ ["\xe0\xa0\x80", [0x800]],
+ ["\xef\xbf\xbf", [0xffff]]
+ ].should be_computed_by(:unpack, "U")
+ end
+
+ it "decodes UTF-8 max codepoints" do
+ [ ["\xf0\x90\x80\x80", [0x10000]],
+ ["\xf3\xbf\xbf\xbf", [0xfffff]],
+ ["\xf4\x80\x80\x80", [0x100000]],
+ ["\xf4\x8f\xbf\xbf", [0x10ffff]]
+ ].should be_computed_by(:unpack, "U")
+ end
+
+ it "does not decode any items for directives exceeding the input string size" do
+ "\xc2\x80".unpack("UUUU").should == [0x80]
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x01\x02".unpack("U\x00U")
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "\x01\x02".unpack("U U").should == [1, 2]
+ end
+end
diff --git a/spec/ruby/core/string/unpack/u_spec.rb b/spec/ruby/core/string/unpack/u_spec.rb
new file mode 100644
index 0000000000..720c1b8583
--- /dev/null
+++ b/spec/ruby/core/string/unpack/u_spec.rb
@@ -0,0 +1,97 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/unicode'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'U'" do
+ it_behaves_like :string_unpack_basic, 'U'
+ it_behaves_like :string_unpack_no_platform, 'U'
+ it_behaves_like :string_unpack_unicode, 'U'
+ it_behaves_like :string_unpack_taint, 'U'
+
+ it "raises ArgumentError on a malformed byte sequence" do
+ -> { "\xE3".unpack('U') }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError on a malformed byte sequence and doesn't continue when used with the * modifier" do
+ -> { "\xE3".unpack('U*') }.should.raise(ArgumentError)
+ end
+end
+
+describe "String#unpack with format 'u'" do
+ it_behaves_like :string_unpack_basic, 'u'
+ it_behaves_like :string_unpack_no_platform, 'u'
+ it_behaves_like :string_unpack_taint, 'u'
+
+ it "decodes an empty string as an empty string" do
+ "".unpack("u").should == [""]
+ end
+
+ it "decodes into raw (ascii) string values" do
+ str = "".unpack("u")[0]
+ str.encoding.should == Encoding::BINARY
+
+ str = "1".dup.force_encoding('UTF-8').unpack("u")[0]
+ str.encoding.should == Encoding::BINARY
+ end
+
+ it "decodes the complete string ignoring newlines when given a single directive" do
+ "#86)C\n#1$5&\n".unpack("u").should == ["abcDEF"]
+ end
+
+ it "appends empty string to the array for directives exceeding the input size" do
+ "#86)C\n#1$5&\n".unpack("uuu").should == ["abcDEF", "", ""]
+ end
+
+ it "ignores the count or '*' modifier and decodes the entire string" do
+ [ ["#86)C\n#1$5&\n", "u238", ["abcDEF"]],
+ ["#86)C\n#1$5&\n", "u*", ["abcDEF"]]
+ ].should be_computed_by(:unpack)
+ end
+
+ it "decodes all ascii characters" do
+ [ ["'``$\"`P0%!@``\n", ["\x00\x01\x02\x03\x04\x05\x06"]],
+ ["'!P@)\"@L,#0``\n", ["\a\b\t\n\v\f\r"]],
+ [")\#@\\0$1(3%!46\n", ["\x0E\x0F\x10\x11\x12\x13\x14\x15\x16"]],
+ [")%Q@9&AL<'1X?\n", ["\x17\x18\x19\x1a\e\x1c\x1d\x1e\x1f"]],
+ ["/(2(C)\"4F)R@I*BLL+2XO\n", ["!\"\#$%&'()*+,-./"]],
+ ["*,\#$R,S0U-C<X.0``\n", ["0123456789"]],
+ ["'.CL\\/3X_0```\n", [":;<=>?@"]],
+ [":04)#1$5&1TA)2DM,34Y/4%%24U155E=865H`\n", ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"]],
+ ["&6UQ=7E]@\n", ["[\\]^_`"]],
+ [":86)C9&5F9VAI:FML;6YO<'%R<W1U=G=X>7H`\n", ["abcdefghijklmnopqrstuvwxyz"]],
+ ["$>WQ]?@``\n", ["{|}~"]],
+ [")?\\*`PH'\"@L*#\n", ["\x7f\xc2\x80\xc2\x81\xc2\x82\xc2\x83"]],
+ [")PH3\"A<*&PH?\"\n", ["\xc2\x84\xc2\x85\xc2\x86\xc2\x87\xc2"]],
+ [")B,*)PHK\"B\\*,\n", ["\x88\xc2\x89\xc2\x8a\xc2\x8b\xc2\x8c"]],
+ [")PHW\"CL*/PI#\"\n", ["\xc2\x8d\xc2\x8e\xc2\x8f\xc2\x90\xc2"]],
+ [")D<*2PI/\"E,*5\n", ["\x91\xc2\x92\xc2\x93\xc2\x94\xc2\x95"]],
+ [")PI;\"E\\*8PIG\"\n", ["\xc2\x96\xc2\x97\xc2\x98\xc2\x99\xc2"]],
+ [")FL*;PIS\"G<*>\n", ["\x9a\xc2\x9b\xc2\x9c\xc2\x9d\xc2\x9e"]],
+ [")PI_\"H,*APJ+\"\n", ["\xc2\x9f\xc2\xa0\xc2\xa1\xc2\xa2\xc2"]],
+ [")H\\*DPJ7\"IL*G\n", ["\xa3\xc2\xa4\xc2\xa5\xc2\xa6\xc2\xa7"]],
+ [")PJC\"J<*JPJO\"\n", ["\xc2\xa8\xc2\xa9\xc2\xaa\xc2\xab\xc2"]],
+ [")K,*MPJ[\"K\\*P\n", ["\xac\xc2\xad\xc2\xae\xc2\xaf\xc2\xb0"]],
+ [")PK'\"LL*SPK3\"\n", ["\xc2\xb1\xc2\xb2\xc2\xb3\xc2\xb4\xc2"]],
+ [")M<*VPK?\"N,*Y\n", ["\xb5\xc2\xb6\xc2\xb7\xc2\xb8\xc2\xb9"]],
+ [")PKK\"N\\*\\PKW\"\n", ["\xc2\xba\xc2\xbb\xc2\xbc\xc2\xbd\xc2"]],
+ [")OL*_PX#\#@<.\"\n", ["\xbe\xc2\xbf\xc3\x80\xc3\x81\xc3\x82"]],
+ [")PX/#A,.%PX;#\n", ["\xc3\x83\xc3\x84\xc3\x85\xc3\x86\xc3"]],
+ [")A\\.(PXG#BL.+\n", ["\x87\xc3\x88\xc3\x89\xc3\x8a\xc3\x8b"]],
+ [")PXS#C<..PX_#\n", ["\xc3\x8c\xc3\x8d\xc3\x8e\xc3\x8f\xc3"]],
+ [")D,.1PY+#D\\.4\n", ["\x90\xc3\x91\xc3\x92\xc3\x93\xc3\x94"]],
+ [")PY7#EL.7PYC#\n", ["\xc3\x95\xc3\x96\xc3\x97\xc3\x98\xc3"]],
+ [")F<.:PYO#G,.=\n", ["\x99\xc3\x9a\xc3\x9b\xc3\x9c\xc3\x9d"]],
+ [")PY[#G\\.@PZ'#\n", ["\xc3\x9e\xc3\x9f\xc3\xa0\xc3\xa1\xc3"]],
+ [")HL.CPZ3#I<.F\n", ["\xa2\xc3\xa3\xc3\xa4\xc3\xa5\xc3\xa6"]],
+ [")PZ?#J,.IPZK#\n", ["\xc3\xa7\xc3\xa8\xc3\xa9\xc3\xaa\xc3"]],
+ [")J\\.LPZW#KL.O\n", ["\xab\xc3\xac\xc3\xad\xc3\xae\xc3\xaf"]],
+ [")P[##L<.RP[/#\n", ["\xc3\xb0\xc3\xb1\xc3\xb2\xc3\xb3\xc3"]],
+ [")M,.UP[;#M\\.X\n", ["\xb4\xc3\xb5\xc3\xb6\xc3\xb7\xc3\xb8"]],
+ [")P[G#NL.[P[S#\n", ["\xc3\xb9\xc3\xba\xc3\xbb\xc3\xbc\xc3"]],
+ ["%O<.^P[\\`\n", ["\xbd\xc3\xbe\xc3\xbf"]]
+ ].should be_computed_by(:unpack, "u")
+ end
+end
diff --git a/spec/ruby/core/string/unpack/v_spec.rb b/spec/ruby/core/string/unpack/v_spec.rb
new file mode 100644
index 0000000000..929e8712cb
--- /dev/null
+++ b/spec/ruby/core/string/unpack/v_spec.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/integer'
+
+describe "String#unpack with format 'V'" do
+ it_behaves_like :string_unpack_basic, 'V'
+ it_behaves_like :string_unpack_32bit_le, 'V'
+ it_behaves_like :string_unpack_32bit_le_unsigned, 'V'
+ it_behaves_like :string_unpack_no_platform, 'V'
+end
+
+describe "String#unpack with format 'v'" do
+ it_behaves_like :string_unpack_basic, 'v'
+ it_behaves_like :string_unpack_16bit_le, 'v'
+ it_behaves_like :string_unpack_16bit_le_unsigned, 'v'
+ it_behaves_like :string_unpack_no_platform, 'v'
+end
diff --git a/spec/ruby/core/string/unpack/w_spec.rb b/spec/ruby/core/string/unpack/w_spec.rb
new file mode 100644
index 0000000000..cc9aecac9c
--- /dev/null
+++ b/spec/ruby/core/string/unpack/w_spec.rb
@@ -0,0 +1,37 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+describe "String#unpack with directive 'w'" do
+ it_behaves_like :string_unpack_basic, 'w'
+ it_behaves_like :string_unpack_no_platform, 'w'
+
+ it "decodes a BER-compressed integer" do
+ [ ["\x00", [0]],
+ ["\x01", [1]],
+ ["\xce\x0f", [9999]],
+ ["\x84\x80\x80\x80\x80\x80\x80\x80\x80\x00", [2**65]]
+ ].should be_computed_by(:unpack, "w")
+ end
+
+ it "raise ArgumentError for NULL bytes between directives" do
+ -> {
+ "\x01\x02\x03".unpack("w\x00w")
+ }.should.raise(ArgumentError, /unknown unpack directive/)
+ end
+
+ it "ignores spaces between directives" do
+ "\x01\x02\x03".unpack("w w").should == [1, 2]
+ end
+end
+
+describe "String#unpack with directive 'w*'" do
+
+ it "decodes BER-compressed integers" do
+ "\x01\x02\x03\x04".unpack("w*").should == [1, 2, 3, 4]
+ "\x00\xCE\x0F\x84\x80\x80\x80\x80\x80\x80\x80\x80\x00\x01\x00".unpack("w*").should == [0, 9999, 2**65, 1, 0]
+ "\x81\x80\x80\x80\x80\x80\x80\x80\x80\x00\x90\x80\x80\x80\x80\x80\x80\x80\x03\x01\x02".unpack("w*").should == [2**63, (2**60 + 3), 1, 2]
+ end
+
+end
diff --git a/spec/ruby/core/string/unpack/x_spec.rb b/spec/ruby/core/string/unpack/x_spec.rb
new file mode 100644
index 0000000000..fb2e79fc1f
--- /dev/null
+++ b/spec/ruby/core/string/unpack/x_spec.rb
@@ -0,0 +1,62 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+
+describe "String#unpack with format 'X'" do
+ it_behaves_like :string_unpack_basic, 'X'
+ it_behaves_like :string_unpack_no_platform, 'X'
+
+ it "moves the read index back by the number of bytes specified by count" do
+ "\x01\x02\x03\x04".unpack("C3X2C").should == [1, 2, 3, 2]
+ end
+
+ it "does not change the read index when passed a count of zero" do
+ "\x01\x02\x03\x04".unpack("C3X0C").should == [1, 2, 3, 4]
+ end
+
+ it "implicitly has a count of one when count is not specified" do
+ "\x01\x02\x03\x04".unpack("C3XC").should == [1, 2, 3, 3]
+ end
+
+ it "moves the read index back by the remaining bytes when passed the '*' modifier" do
+ "abcd".unpack("C3X*C").should == [97, 98, 99, 99]
+ end
+
+ it "raises an ArgumentError when passed the '*' modifier if the remaining bytes exceed the bytes from the index to the start of the String" do
+ -> { "abcd".unpack("CX*C") }.should.raise(ArgumentError)
+ end
+
+ it "raises an ArgumentError if the count exceeds the bytes from current index to the start of the String" do
+ -> { "\x01\x02\x03\x04".unpack("C3X4C") }.should.raise(ArgumentError)
+ end
+end
+
+describe "String#unpack with format 'x'" do
+ it_behaves_like :string_unpack_basic, 'x'
+ it_behaves_like :string_unpack_no_platform, 'x'
+
+ it "moves the read index forward by the number of bytes specified by count" do
+ "\x01\x02\x03\x04".unpack("Cx2C").should == [1, 4]
+ end
+
+ it "implicitly has a count of one when count is not specified" do
+ "\x01\x02\x03\x04".unpack("CxC").should == [1, 3]
+ end
+
+ it "does not change the read index when passed a count of zero" do
+ "\x01\x02\x03\x04".unpack("Cx0C").should == [1, 2]
+ end
+
+ it "moves the read index to the end of the string when passed the '*' modifier" do
+ "\x01\x02\x03\x04".unpack("Cx*C").should == [1, nil]
+ end
+
+ it "positions the read index one beyond the last readable byte in the String" do
+ "\x01\x02\x03\x04".unpack("C2x2C").should == [1, 2, nil]
+ end
+
+ it "raises an ArgumentError if the count exceeds the size of the String" do
+ -> { "\x01\x02\x03\x04".unpack("C2x3C") }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/string/unpack/z_spec.rb b/spec/ruby/core/string/unpack/z_spec.rb
new file mode 100644
index 0000000000..1030390550
--- /dev/null
+++ b/spec/ruby/core/string/unpack/z_spec.rb
@@ -0,0 +1,28 @@
+# encoding: binary
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+require_relative 'shared/basic'
+require_relative 'shared/string'
+require_relative 'shared/taint'
+
+describe "String#unpack with format 'Z'" do
+ it_behaves_like :string_unpack_basic, 'Z'
+ it_behaves_like :string_unpack_no_platform, 'Z'
+ it_behaves_like :string_unpack_string, 'Z'
+ it_behaves_like :string_unpack_taint, 'Z'
+
+ it "stops decoding at NULL bytes when passed the '*' modifier" do
+ "a\x00\x00 b \x00c".unpack('Z*Z*Z*Z*').should == ["a", "", " b ", "c"]
+ end
+
+ it "decodes the number of bytes specified by the count modifier and truncates the decoded string at the first NULL byte" do
+ [ ["a\x00 \x00b c", ["a", " "]],
+ ["\x00a\x00 bc \x00", ["", "c"]]
+ ].should be_computed_by(:unpack, "Z5Z")
+ end
+
+ it "does not advance past the null byte when given a 'Z' format specifier" do
+ "a\x00\x0f".unpack('Zxc').should == ['a', 15]
+ "a\x00\x0f".unpack('Zcc').should == ['a', 0, 15]
+ end
+end
diff --git a/spec/ruby/core/string/unpack1_spec.rb b/spec/ruby/core/string/unpack1_spec.rb
new file mode 100644
index 0000000000..ee10042eb8
--- /dev/null
+++ b/spec/ruby/core/string/unpack1_spec.rb
@@ -0,0 +1,61 @@
+require_relative '../../spec_helper'
+
+describe "String#unpack1" do
+ it "returns the first value of #unpack" do
+ "ABCD".unpack1('x3C').should == "ABCD".unpack('x3C')[0]
+ "\u{3042 3044 3046}".unpack1("U*").should == 0x3042
+ "aG9nZWZ1Z2E=".unpack1("m").should == "hogefuga"
+ "A".unpack1("B*").should == "01000001"
+ end
+
+ it "starts unpacking from the given offset" do
+ "ZZABCD".unpack1('x3C', offset: 2).should == "ABCD".unpack('x3C')[0]
+ "ZZZZaG9nZWZ1Z2E=".unpack1("m", offset: 4).should == "hogefuga"
+ "ZA".unpack1("B*", offset: 1).should == "01000001"
+ end
+
+ it "traits offset as a bytes offset" do
+ "؈".unpack("CC").should == [216, 136]
+ "؈".unpack1("C").should == 216
+ "؈".unpack1("C", offset: 1).should == 136
+ end
+
+ describe "when the offset is negative" do
+ ruby_version_is "4.1" do
+ it "starts unpacking from the end" do
+ "abc".unpack1("C", offset: -2).should == 98
+ end
+
+ it "raises an ArgumentError if it is less than -length" do
+ -> { "a".unpack1("C", offset: -2) }.should.raise(ArgumentError, "offset outside of string")
+ end
+ end
+
+ ruby_version_is ""..."4.1" do
+ it "raises an ArgumentError" do
+ -> { "a".unpack1("C", offset: -1) }.should.raise(ArgumentError, "offset can't be negative")
+ end
+ end
+ end
+
+ it "returns nil if the offset is at the end of the string" do
+ "a".unpack1("C", offset: 1).should == nil
+ end
+
+ it "raises an ArgumentError when the offset is larger than the string bytesize" do
+ -> { "a".unpack1("C", offset: 2) }.should.raise(ArgumentError, "offset outside of string")
+ end
+
+ context "with format 'm0'" do
+ # unpack1("m0") takes a special code path that calls Pack.unpackBase46Strict instead of Pack.unpack_m,
+ # which is why we repeat the tests for unpack("m0") here.
+
+ it "decodes base64" do
+ "dGVzdA==".unpack1("m0").should == "test"
+ end
+
+ it "raises an ArgumentError for an invalid base64 character" do
+ -> { "dGV%zdA==".unpack1("m0") }.should.raise(ArgumentError)
+ end
+ end
+end
diff --git a/spec/ruby/core/string/unpack_spec.rb b/spec/ruby/core/string/unpack_spec.rb
new file mode 100644
index 0000000000..eb4710ce14
--- /dev/null
+++ b/spec/ruby/core/string/unpack_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+
+describe "String#unpack" do
+ it "raises a TypeError when passed nil" do
+ -> { "abc".unpack(nil) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when passed an Integer" do
+ -> { "abc".unpack(1) }.should.raise(TypeError)
+ end
+
+ it "starts unpacking from the given offset" do
+ "abc".unpack("CC", offset: 1).should == [98, 99]
+ end
+
+ it "traits offset as a bytes offset" do
+ "؈".unpack("CC").should == [216, 136]
+ "؈".unpack("CC", offset: 1).should == [136, nil]
+ end
+
+ describe "when the offset is negative" do
+ ruby_version_is "4.1" do
+ it "starts unpacking from the end" do
+ "abc".unpack("CC", offset: -2).should == [98, 99]
+ end
+
+ it "raises an ArgumentError if it is less than -length" do
+ -> { "a".unpack("C", offset: -2) }.should.raise(ArgumentError, "offset outside of string")
+ end
+ end
+
+ ruby_version_is ""..."4.1" do
+ it "raises an ArgumentError" do
+ -> { "a".unpack("C", offset: -1) }.should.raise(ArgumentError, "offset can't be negative")
+ end
+ end
+ end
+
+ it "returns nil if the offset is at the end of the string" do
+ "a".unpack("C", offset: 1).should == [nil]
+ end
+
+ it "raises an ArgumentError when the offset is larger than the string" do
+ -> { "a".unpack("C", offset: 2) }.should.raise(ArgumentError, "offset outside of string")
+ end
+end
diff --git a/spec/ruby/core/string/upcase_spec.rb b/spec/ruby/core/string/upcase_spec.rb
new file mode 100644
index 0000000000..a6e1869267
--- /dev/null
+++ b/spec/ruby/core/string/upcase_spec.rb
@@ -0,0 +1,187 @@
+# -*- encoding: utf-8 -*-
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#upcase" do
+ it "returns a copy of self with all lowercase letters upcased" do
+ "Hello".upcase.should == "HELLO"
+ "hello".upcase.should == "HELLO"
+ end
+
+ it "returns a String in the same encoding as self" do
+ "hello".encode("US-ASCII").upcase.encoding.should == Encoding::US_ASCII
+ end
+
+ describe "full Unicode case mapping" do
+ it "works for all of Unicode with no option" do
+ "äöü".upcase.should == "ÄÖÜ"
+ end
+
+ it "updates string metadata" do
+ upcased = "aßet".upcase
+
+ upcased.should == "ASSET"
+ upcased.size.should == 5
+ upcased.bytesize.should == 5
+ upcased.ascii_only?.should == true
+ end
+ end
+
+ describe "ASCII-only case mapping" do
+ it "does not upcase non-ASCII characters" do
+ "aßet".upcase(:ascii).should == "AßET"
+ end
+
+ it "works with substrings" do
+ "prefix té"[-2..-1].upcase(:ascii).should == "Té"
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Turkic languages" do
+ it "upcases ASCII characters according to Turkic semantics" do
+ "i".upcase(:turkic).should == "İ"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ "i".upcase(:turkic, :lithuanian).should == "İ"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "i".upcase(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ "iß".upcase(:lithuanian).should == "ISS"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ "iß".upcase(:lithuanian, :turkic).should == "İSS"
+ end
+
+ it "does not allow any other additional option" do
+ -> { "iß".upcase(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not allow the :fold option for upcasing" do
+ -> { "abc".upcase(:fold) }.should.raise(ArgumentError)
+ end
+
+ it "does not allow invalid options" do
+ -> { "abc".upcase(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns a String instance for subclasses" do
+ StringSpecs::MyString.new("fooBAR").upcase.should.instance_of?(String)
+ end
+end
+
+describe "String#upcase!" do
+ it "modifies self in place" do
+ a = "HeLlO"
+ a.upcase!.should.equal?(a)
+ a.should == "HELLO"
+ end
+
+ it "modifies self in place for non-ascii-compatible encodings" do
+ a = "HeLlO".encode("utf-16le")
+ a.upcase!
+ a.should == "HELLO".encode("utf-16le")
+ end
+
+ describe "full Unicode case mapping" do
+ it "modifies self in place for all of Unicode with no option" do
+ a = "äöü"
+ a.upcase!
+ a.should == "ÄÖÜ"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "äöü".encode("utf-16le")
+ a.upcase!
+ a.should == "ÄÖÜ".encode("utf-16le")
+ end
+
+ it "updates string metadata for self" do
+ upcased = "aßet"
+ upcased.upcase!
+
+ upcased.should == "ASSET"
+ upcased.size.should == 5
+ upcased.bytesize.should == 5
+ upcased.ascii_only?.should == true
+ end
+ end
+
+ describe "modifies self in place for ASCII-only case mapping" do
+ it "does not upcase non-ASCII characters" do
+ a = "aßet"
+ a.upcase!(:ascii)
+ a.should == "AßET"
+ end
+
+ it "works for non-ascii-compatible encodings" do
+ a = "abc".encode("utf-16le")
+ a.upcase!(:ascii)
+ a.should == "ABC".encode("utf-16le")
+ end
+ end
+
+ describe "modifies self in place for full Unicode case mapping adapted for Turkic languages" do
+ it "upcases ASCII characters according to Turkic semantics" do
+ a = "i"
+ a.upcase!(:turkic)
+ a.should == "İ"
+ end
+
+ it "allows Lithuanian as an extra option" do
+ a = "i"
+ a.upcase!(:turkic, :lithuanian)
+ a.should == "İ"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "i"; a.upcase!(:turkic, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ describe "modifies self in place for full Unicode case mapping adapted for Lithuanian" do
+ it "currently works the same as full Unicode case mapping" do
+ a = "iß"
+ a.upcase!(:lithuanian)
+ a.should == "ISS"
+ end
+
+ it "allows Turkic as an extra option (and applies Turkic semantics)" do
+ a = "iß"
+ a.upcase!(:lithuanian, :turkic)
+ a.should == "İSS"
+ end
+
+ it "does not allow any other additional option" do
+ -> { a = "iß"; a.upcase!(:lithuanian, :ascii) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "does not allow the :fold option for upcasing" do
+ -> { a = "abc"; a.upcase!(:fold) }.should.raise(ArgumentError)
+ end
+
+ it "does not allow invalid options" do
+ -> { a = "abc"; a.upcase!(:invalid_option) }.should.raise(ArgumentError)
+ end
+
+ it "returns nil if no modifications were made" do
+ a = "HELLO"
+ a.upcase!.should == nil
+ a.should == "HELLO"
+ end
+
+ it "raises a FrozenError when self is frozen" do
+ -> { "HeLlo".freeze.upcase! }.should.raise(FrozenError)
+ -> { "HELLO".freeze.upcase! }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/string/uplus_spec.rb b/spec/ruby/core/string/uplus_spec.rb
new file mode 100644
index 0000000000..20767bcc01
--- /dev/null
+++ b/spec/ruby/core/string/uplus_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: false
+require_relative '../../spec_helper'
+
+describe 'String#+@' do
+ it 'returns an unfrozen copy of a frozen String' do
+ input = 'foo'.freeze
+ output = +input
+
+ output.should_not.frozen?
+ output.should == 'foo'
+
+ output << 'bar'
+ output.should == 'foobar'
+ end
+
+ it 'returns a mutable String itself' do
+ input = String.new("foo")
+ output = +input
+
+ output.should.equal?(input)
+
+ input << "bar"
+ output.should == "foobar"
+ end
+
+ context 'if file has "frozen_string_literal: true" magic comment' do
+ it 'returns mutable copy of a literal' do
+ ruby_exe(fixture(__FILE__, "freeze_magic_comment.rb")).should == 'mutable'
+ end
+ end
+
+ context 'if file has "frozen_string_literal: false" magic comment' do
+ it 'returns literal string itself' do
+ input = 'foo'
+ output = +input
+
+ output.equal?(input).should == true
+ end
+ end
+
+ context 'if file has no frozen_string_literal magic comment' do
+ ruby_version_is ''...'3.4' do
+ it 'returns literal string itself' do
+ eval(<<~RUBY).should == true
+ s = "foo"
+ s.equal?(+s)
+ RUBY
+ end
+ end
+
+ ruby_version_is '3.4' do
+ it 'returns mutable copy of a literal' do
+ eval(<<~RUBY).should == false
+ s = "foo"
+ s.equal?(+s)
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/upto_spec.rb b/spec/ruby/core/string/upto_spec.rb
new file mode 100644
index 0000000000..2eea06fd01
--- /dev/null
+++ b/spec/ruby/core/string/upto_spec.rb
@@ -0,0 +1,110 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "String#upto" do
+ it "passes successive values, starting at self and ending at other_string, to the block" do
+ a = []
+ "*+".upto("*3") { |s| a << s }
+ a.should == ["*+", "*,", "*-", "*.", "*/", "*0", "*1", "*2", "*3"]
+ end
+
+ it "calls the block once even when start equals stop" do
+ a = []
+ "abc".upto("abc") { |s| a << s }
+ a.should == ["abc"]
+ end
+
+ it "doesn't call block with self even if self is less than stop but stop length is less than self length" do
+ a = []
+ "25".upto("5") { |s| a << s }
+ a.should == []
+ end
+
+ it "doesn't call block if stop is less than self and stop length is less than self length" do
+ a = []
+ "25".upto("1") { |s| a << s }
+ a.should == []
+ end
+
+ it "doesn't call the block if self is greater than stop" do
+ a = []
+ "5".upto("2") { |s| a << s }
+ a.should == []
+ end
+
+ it "stops iterating as soon as the current value's character count gets higher than stop's" do
+ a = []
+ "96".upto("AA") { |s| a << s }
+ a.should == ["96", "97", "98", "99"]
+ end
+
+ it "returns self" do
+ "abc".upto("abd") { }.should == "abc"
+ "5".upto("2") { |i| i }.should == "5"
+ end
+
+ it "tries to convert other to string using to_str" do
+ other = mock('abd')
+ def other.to_str() "abd" end
+
+ a = []
+ "abc".upto(other) { |s| a << s }
+ a.should == ["abc", "abd"]
+ end
+
+ it "raises a TypeError if other can't be converted to a string" do
+ -> { "abc".upto(123) { } }.should.raise(TypeError)
+ -> { "abc".upto(mock('x')){ } }.should.raise(TypeError)
+ end
+
+
+ it "does not work with symbols" do
+ -> { "a".upto(:c).to_a }.should.raise(TypeError)
+ end
+
+ it "returns non-alphabetic characters in the ASCII range for single letters" do
+ "9".upto("A").to_a.should == ["9", ":", ";", "<", "=", ">", "?", "@", "A"]
+ "Z".upto("a").to_a.should == ["Z", "[", "\\", "]", "^", "_", "`", "a"]
+ "z".upto("~").to_a.should == ["z", "{", "|", "}", "~"]
+ end
+
+ it "stops before the last value if exclusive" do
+ a = []
+ "a".upto("d", true) { |s| a << s}
+ a.should == ["a", "b", "c"]
+ end
+
+ it "works with non-ASCII ranges" do
+ a = []
+ 'Σ'.upto('Ω') { |s| a << s }
+ a.should == ["Σ", "Τ", "Υ", "Φ", "Χ", "Ψ", "Ω"]
+ end
+
+ it "raises Encoding::CompatibilityError when incompatible characters are given" do
+ char1 = 'a'.dup.force_encoding("EUC-JP")
+ char2 = 'b'.dup.force_encoding("ISO-2022-JP")
+ -> { char1.upto(char2) {} }.should.raise(Encoding::CompatibilityError, "incompatible character encodings: EUC-JP and ISO-2022-JP")
+ end
+
+ describe "on sequence of numbers" do
+ it "calls the block as Integer#upto" do
+ "8".upto("11").to_a.should == 8.upto(11).map(&:to_s)
+ end
+ end
+
+ describe "when no block is given" do
+ it "returns an enumerator" do
+ enum = "aaa".upto("baa", true)
+ enum.should.instance_of?(Enumerator)
+ enum.count.should == 26**2
+ end
+
+ describe "returned Enumerator" do
+ describe "size" do
+ it "should return nil" do
+ "a".upto("b").size.should == nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/string/valid_encoding/utf_8_spec.rb b/spec/ruby/core/string/valid_encoding/utf_8_spec.rb
new file mode 100644
index 0000000000..a14c3af830
--- /dev/null
+++ b/spec/ruby/core/string/valid_encoding/utf_8_spec.rb
@@ -0,0 +1,214 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../../spec_helper'
+
+describe "String#valid_encoding? and UTF-8" do
+ def utf8(bytes)
+ bytes.pack("C*").force_encoding("UTF-8")
+ end
+
+ describe "1-byte character" do
+ it "is valid if is in format 0xxxxxxx" do
+ utf8([0b00000000]).valid_encoding?.should == true
+ utf8([0b01111111]).valid_encoding?.should == true
+ end
+
+ it "is not valid if is not in format 0xxxxxxx" do
+ utf8([0b10000000]).valid_encoding?.should == false
+ utf8([0b11111111]).valid_encoding?.should == false
+ end
+ end
+
+ describe "2-bytes character" do
+ it "is valid if in format [110xxxxx 10xxxxx]" do
+ utf8([0b11000010, 0b10000000]).valid_encoding?.should == true
+ utf8([0b11000010, 0b10111111]).valid_encoding?.should == true
+
+ utf8([0b11011111, 0b10000000]).valid_encoding?.should == true
+ utf8([0b11011111, 0b10111111]).valid_encoding?.should == true
+ end
+
+ it "is not valid if the first byte is not in format 110xxxxx" do
+ utf8([0b00000010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b00100010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01000010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01100010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10000010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10100010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11000010, 0b10000000]).valid_encoding?.should == true # correct bytes
+ utf8([0b11100010, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second byte is not in format 10xxxxxx" do
+ utf8([0b11000010, 0b00000000]).valid_encoding?.should == false
+ utf8([0b11000010, 0b01000000]).valid_encoding?.should == false
+ utf8([0b11000010, 0b11000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if is smaller than [xxxxxx10 xx000000] (codepoints < U+007F, that are encoded with the 1-byte format)" do
+ utf8([0b11000000, 0b10111111]).valid_encoding?.should == false
+ utf8([0b11000001, 0b10111111]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the first byte is missing" do
+ bytes = [0b11000010, 0b10000000]
+ utf8(bytes[1..1]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second byte is missing" do
+ bytes = [0b11000010, 0b10000000]
+ utf8(bytes[0..0]).valid_encoding?.should == false
+ end
+ end
+
+ describe "3-bytes character" do
+ it "is valid if in format [1110xxxx 10xxxxxx 10xxxxxx]" do
+ utf8([0b11100000, 0b10100000, 0b10000000]).valid_encoding?.should == true
+ utf8([0b11100000, 0b10100000, 0b10111111]).valid_encoding?.should == true
+ utf8([0b11100000, 0b10111111, 0b10111111]).valid_encoding?.should == true
+ utf8([0b11101111, 0b10111111, 0b10111111]).valid_encoding?.should == true
+ end
+
+ it "is not valid if the first byte is not in format 1110xxxx" do
+ utf8([0b00000000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b00010000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b00100000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b00110000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01000000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01010000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01100000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01110000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10000000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10010000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10100000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10110000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11000000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11010000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10100000, 0b10000000]).valid_encoding?.should == true # correct bytes
+ utf8([0b11110000, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second byte is not in format 10xxxxxx" do
+ utf8([0b11100000, 0b00100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b01100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b11100000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the third byte is not in format 10xxxxxx" do
+ utf8([0b11100000, 0b10100000, 0b00000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10100000, 0b01000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10100000, 0b01000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if is smaller than [xxxx0000 xx100000 xx000000] (codepoints < U+07FF that are encoded with the 2-byte format)" do
+ utf8([0b11100000, 0b10010000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10001000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10000100, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10000010, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10000001, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11100000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if in range [xxxx1101 xx100000 xx000000] - [xxxx1101 xx111111 xx111111] (codepoints U+D800 - U+DFFF)" do
+ utf8([0b11101101, 0b10100000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11101101, 0b10100000, 0b10000001]).valid_encoding?.should == false
+ utf8([0b11101101, 0b10111111, 0b10111111]).valid_encoding?.should == false
+
+ utf8([0b11101101, 0b10011111, 0b10111111]).valid_encoding?.should == true # lower boundary - 1
+ utf8([0b11101110, 0b10000000, 0b10000000]).valid_encoding?.should == true # upper boundary + 1
+ end
+
+ it "is not valid if the first byte is missing" do
+ bytes = [0b11100000, 0b10100000, 0b10000000]
+ utf8(bytes[2..3]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second byte is missing" do
+ bytes = [0b11100000, 0b10100000, 0b10000000]
+ utf8([bytes[0], bytes[2]]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second and the third bytes are missing" do
+ bytes = [0b11100000, 0b10100000, 0b10000000]
+ utf8(bytes[0..0]).valid_encoding?.should == false
+ end
+ end
+
+ describe "4-bytes character" do
+ it "is valid if in format [11110xxx 10xxxxxx 10xxxxxx 10xxxxxx]" do
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b10111111]).valid_encoding?.should == true
+ utf8([0b11110000, 0b10010000, 0b10111111, 0b10111111]).valid_encoding?.should == true
+ utf8([0b11110000, 0b10111111, 0b10111111, 0b10111111]).valid_encoding?.should == true
+ utf8([0b11110100, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == true
+ end
+
+ it "is not valid if the first byte is not in format 11110xxx" do
+ utf8([0b11100000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11010000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b10110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b01110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second byte is not in format 10xxxxxx" do
+ utf8([0b11110000, 0b00010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b01010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true # correct bytes
+ utf8([0b11110000, 0b11010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the third byte is not in format 10xxxxxx" do
+ utf8([0b11110000, 0b10010000, 0b00000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10010000, 0b01000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true # correct bytes
+ utf8([0b11110000, 0b10010000, 0b11000000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the forth byte is not in format 10xxxxxx" do
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b00000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b01000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == true # correct bytes
+ utf8([0b11110000, 0b10010000, 0b10000000, 0b11000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if is smaller than [xxxxx000 xx001000 xx000000 xx000000] (codepoint < U+10000)" do
+ utf8([0b11110000, 0b10000111, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000110, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000101, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000100, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000011, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000010, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000001, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110000, 0b10000000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ end
+
+ it "is not valid if is greater than [xxxxx100 xx001111 xx111111 xx111111] (codepoint > U+10FFFF)" do
+ utf8([0b11110100, 0b10010000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110100, 0b10100000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+ utf8([0b11110100, 0b10110000, 0b10000000, 0b10000000]).valid_encoding?.should == false
+
+ utf8([0b11110101, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == false
+ utf8([0b11110110, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == false
+ utf8([0b11110111, 0b10001111, 0b10111111, 0b10111111]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the first byte is missing" do
+ bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000]
+ utf8(bytes[1..3]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second byte is missing" do
+ bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000]
+ utf8([bytes[0], bytes[2], bytes[3]]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second and the third bytes are missing" do
+ bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000]
+ utf8([bytes[0], bytes[3]]).valid_encoding?.should == false
+ end
+
+ it "is not valid if the second, the third and the fourth bytes are missing" do
+ bytes = [0b11110000, 0b10010000, 0b10000000, 0b10000000]
+ utf8(bytes[0..0]).valid_encoding?.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/string/valid_encoding_spec.rb b/spec/ruby/core/string/valid_encoding_spec.rb
new file mode 100644
index 0000000000..f29220fc99
--- /dev/null
+++ b/spec/ruby/core/string/valid_encoding_spec.rb
@@ -0,0 +1,133 @@
+require_relative '../../spec_helper'
+
+describe "String#valid_encoding?" do
+ it "returns true if the String's encoding is valid" do
+ "a".valid_encoding?.should == true
+ "\u{8365}\u{221}".valid_encoding?.should == true
+ end
+
+ it "returns true if self is valid in the current encoding and other encodings" do
+ str = +"\x77"
+ str.force_encoding('utf-8').valid_encoding?.should == true
+ str.force_encoding('binary').valid_encoding?.should == true
+ end
+
+ it "returns true for all encodings self is valid in" do
+ str = +"\xE6\x9D\x94"
+ str.force_encoding('BINARY').valid_encoding?.should == true
+ str.force_encoding('UTF-8').valid_encoding?.should == true
+ str.force_encoding('US-ASCII').valid_encoding?.should == false
+ str.force_encoding('Big5').valid_encoding?.should == false
+ str.force_encoding('CP949').valid_encoding?.should == false
+ str.force_encoding('Emacs-Mule').valid_encoding?.should == false
+ str.force_encoding('EUC-JP').valid_encoding?.should == false
+ str.force_encoding('EUC-KR').valid_encoding?.should == false
+ str.force_encoding('EUC-TW').valid_encoding?.should == false
+ str.force_encoding('GB18030').valid_encoding?.should == false
+ str.force_encoding('GBK').valid_encoding?.should == false
+ str.force_encoding('ISO-8859-1').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-2').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-3').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-4').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-5').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-6').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-7').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-8').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-9').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-10').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-11').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-13').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-14').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-15').valid_encoding?.should == true
+ str.force_encoding('ISO-8859-16').valid_encoding?.should == true
+ str.force_encoding('KOI8-R').valid_encoding?.should == true
+ str.force_encoding('KOI8-U').valid_encoding?.should == true
+ str.force_encoding('Shift_JIS').valid_encoding?.should == false
+ "\xD8\x00".dup.force_encoding('UTF-16BE').valid_encoding?.should == false
+ "\x00\xD8".dup.force_encoding('UTF-16LE').valid_encoding?.should == false
+ "\x04\x03\x02\x01".dup.force_encoding('UTF-32BE').valid_encoding?.should == false
+ "\x01\x02\x03\x04".dup.force_encoding('UTF-32LE').valid_encoding?.should == false
+ str.force_encoding('Windows-1251').valid_encoding?.should == true
+ str.force_encoding('IBM437').valid_encoding?.should == true
+ str.force_encoding('IBM737').valid_encoding?.should == true
+ str.force_encoding('IBM775').valid_encoding?.should == true
+ str.force_encoding('CP850').valid_encoding?.should == true
+ str.force_encoding('IBM852').valid_encoding?.should == true
+ str.force_encoding('CP852').valid_encoding?.should == true
+ str.force_encoding('IBM855').valid_encoding?.should == true
+ str.force_encoding('CP855').valid_encoding?.should == true
+ str.force_encoding('IBM857').valid_encoding?.should == true
+ str.force_encoding('IBM860').valid_encoding?.should == true
+ str.force_encoding('IBM861').valid_encoding?.should == true
+ str.force_encoding('IBM862').valid_encoding?.should == true
+ str.force_encoding('IBM863').valid_encoding?.should == true
+ str.force_encoding('IBM864').valid_encoding?.should == true
+ str.force_encoding('IBM865').valid_encoding?.should == true
+ str.force_encoding('IBM866').valid_encoding?.should == true
+ str.force_encoding('IBM869').valid_encoding?.should == true
+ str.force_encoding('Windows-1258').valid_encoding?.should == true
+ str.force_encoding('GB1988').valid_encoding?.should == true
+ str.force_encoding('macCentEuro').valid_encoding?.should == true
+ str.force_encoding('macCroatian').valid_encoding?.should == true
+ str.force_encoding('macCyrillic').valid_encoding?.should == true
+ str.force_encoding('macGreek').valid_encoding?.should == true
+ str.force_encoding('macIceland').valid_encoding?.should == true
+ str.force_encoding('macRoman').valid_encoding?.should == true
+ str.force_encoding('macRomania').valid_encoding?.should == true
+ str.force_encoding('macThai').valid_encoding?.should == true
+ str.force_encoding('macTurkish').valid_encoding?.should == true
+ str.force_encoding('macUkraine').valid_encoding?.should == true
+ str.force_encoding('stateless-ISO-2022-JP').valid_encoding?.should == false
+ str.force_encoding('eucJP-ms').valid_encoding?.should == false
+ str.force_encoding('CP51932').valid_encoding?.should == false
+ str.force_encoding('GB2312').valid_encoding?.should == false
+ str.force_encoding('GB12345').valid_encoding?.should == false
+ str.force_encoding('ISO-2022-JP').valid_encoding?.should == true
+ str.force_encoding('ISO-2022-JP-2').valid_encoding?.should == true
+ str.force_encoding('CP50221').valid_encoding?.should == true
+ str.force_encoding('Windows-1252').valid_encoding?.should == true
+ str.force_encoding('Windows-1250').valid_encoding?.should == true
+ str.force_encoding('Windows-1256').valid_encoding?.should == true
+ str.force_encoding('Windows-1253').valid_encoding?.should == true
+ str.force_encoding('Windows-1255').valid_encoding?.should == true
+ str.force_encoding('Windows-1254').valid_encoding?.should == true
+ str.force_encoding('TIS-620').valid_encoding?.should == true
+ str.force_encoding('Windows-874').valid_encoding?.should == true
+ str.force_encoding('Windows-1257').valid_encoding?.should == true
+ str.force_encoding('Windows-31J').valid_encoding?.should == false
+ str.force_encoding('MacJapanese').valid_encoding?.should == false
+ str.force_encoding('UTF-7').valid_encoding?.should == true
+ str.force_encoding('UTF8-MAC').valid_encoding?.should == true
+ end
+
+ it "returns true for IBM720 encoding self is valid in" do
+ str = +"\xE6\x9D\x94"
+ str.force_encoding('IBM720').valid_encoding?.should == true
+ str.force_encoding('CP720').valid_encoding?.should == true
+ end
+
+ it "returns false if self is valid in one encoding, but invalid in the one it's tagged with" do
+ str = +"\u{8765}"
+ str.valid_encoding?.should == true
+ str.force_encoding('ascii')
+ str.valid_encoding?.should == false
+ end
+
+ it "returns false if self contains a character invalid in the associated encoding" do
+ "abc#{[0x80].pack('C')}".dup.force_encoding('ascii').valid_encoding?.should == false
+ end
+
+ it "returns false if a valid String had an invalid character appended to it" do
+ str = +"a"
+ str.valid_encoding?.should == true
+ str << [0xDD].pack('C').force_encoding('utf-8')
+ str.valid_encoding?.should == false
+ end
+
+ it "returns true if an invalid string is appended another invalid one but both make a valid string" do
+ str = [0xD0].pack('C').force_encoding('utf-8')
+ str.valid_encoding?.should == false
+ str << [0xBF].pack('C').force_encoding('utf-8')
+ str.valid_encoding?.should == true
+ end
+end
diff --git a/spec/ruby/core/struct/clone_spec.rb b/spec/ruby/core/struct/clone_spec.rb
new file mode 100644
index 0000000000..40c4d52d57
--- /dev/null
+++ b/spec/ruby/core/struct/clone_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/dup'
+
+describe "Struct-based class#clone" do
+ it_behaves_like :struct_dup, :clone
+end
diff --git a/spec/ruby/core/struct/constants_spec.rb b/spec/ruby/core/struct/constants_spec.rb
new file mode 100644
index 0000000000..7e8af1a211
--- /dev/null
+++ b/spec/ruby/core/struct/constants_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "Struct::Group" do
+ it "is no longer defined" do
+ Struct.should_not.const_defined?(:Group)
+ end
+end
+
+describe "Struct::Passwd" do
+ it "is no longer defined" do
+ Struct.should_not.const_defined?(:Passwd)
+ end
+end
diff --git a/spec/ruby/core/struct/deconstruct_keys_spec.rb b/spec/ruby/core/struct/deconstruct_keys_spec.rb
new file mode 100644
index 0000000000..590e1ab40b
--- /dev/null
+++ b/spec/ruby/core/struct/deconstruct_keys_spec.rb
@@ -0,0 +1,130 @@
+require_relative '../../spec_helper'
+
+describe "Struct#deconstruct_keys" do
+ it "returns a hash of attributes" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct_keys([:x, :y]).should == {x: 1, y: 2}
+ end
+
+ it "requires one argument" do
+ struct = Struct.new(:x)
+ obj = struct.new(1)
+
+ -> {
+ obj.deconstruct_keys
+ }.should.raise(ArgumentError, /wrong number of arguments \(given 0, expected 1\)/)
+ end
+
+ it "returns only specified keys" do
+ struct = Struct.new(:x, :y, :z)
+ s = struct.new(1, 2, 3)
+
+ s.deconstruct_keys([:x, :y]).should == {x: 1, y: 2}
+ s.deconstruct_keys([:x] ).should == {x: 1}
+ s.deconstruct_keys([] ).should == {}
+ end
+
+ it "accepts string attribute names" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct_keys(['x', 'y']).should == {'x' => 1, 'y' => 2}
+ end
+
+ it "accepts argument position number as well but returns them as keys" do
+ struct = Struct.new(:x, :y, :z)
+ s = struct.new(10, 20, 30)
+
+ s.deconstruct_keys([0, 1, 2]).should == {0 => 10, 1 => 20, 2 => 30}
+ s.deconstruct_keys([0, 1] ).should == {0 => 10, 1 => 20}
+ s.deconstruct_keys([0] ).should == {0 => 10}
+ s.deconstruct_keys([-1] ).should == {-1 => 30}
+ end
+
+ it "ignores incorrect position numbers" do
+ struct = Struct.new(:x, :y, :z)
+ s = struct.new(10, 20, 30)
+
+ s.deconstruct_keys([0, 3]).should == {0 => 10}
+ end
+
+ it "support mixing attribute names and argument position numbers" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct_keys([0, :x]).should == {0 => 1, :x => 1}
+ end
+
+ it "returns an empty hash when there are more keys than attributes" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct_keys([:x, :y, :a]).should == {}
+ end
+
+ it "returns at first not existing attribute name" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct_keys([:a, :x]).should == {}
+ s.deconstruct_keys([:x, :a]).should == {x: 1}
+ end
+
+ it "returns at first not existing argument position number" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct_keys([3, 0]).should == {}
+ s.deconstruct_keys([0, 3]).should == {0 => 1}
+ end
+
+ it "accepts nil argument and return all the attributes" do
+ struct = Struct.new(:x, :y)
+ obj = struct.new(1, 2)
+
+ obj.deconstruct_keys(nil).should == {x: 1, y: 2}
+ end
+
+ it "tries to convert a key with #to_int if index is not a String nor a Symbol, but responds to #to_int" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ key = mock("to_int")
+ key.should_receive(:to_int).and_return(1)
+
+ s.deconstruct_keys([key]).should == { key => 2 }
+ end
+
+ it "raises a TypeError if the conversion with #to_int does not return an Integer" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ key = mock("to_int")
+ key.should_receive(:to_int).and_return("not an Integer")
+
+ -> {
+ s.deconstruct_keys([key])
+ }.should raise_consistent_error(TypeError, /can't convert MockObject into Integer/)
+ end
+
+ it "raises TypeError if index is not a String, a Symbol and not convertible to Integer" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ -> {
+ s.deconstruct_keys([0, []])
+ }.should.raise(TypeError, "no implicit conversion of Array into Integer")
+ end
+
+ it "raise TypeError if passed anything except nil or array" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ -> { s.deconstruct_keys('x') }.should.raise(TypeError, /expected Array or nil/)
+ -> { s.deconstruct_keys(1) }.should.raise(TypeError, /expected Array or nil/)
+ -> { s.deconstruct_keys(:x) }.should.raise(TypeError, /expected Array or nil/)
+ -> { s.deconstruct_keys({}) }.should.raise(TypeError, /expected Array or nil/)
+ end
+end
diff --git a/spec/ruby/core/struct/deconstruct_spec.rb b/spec/ruby/core/struct/deconstruct_spec.rb
new file mode 100644
index 0000000000..32d4f6bac4
--- /dev/null
+++ b/spec/ruby/core/struct/deconstruct_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+
+describe "Struct#deconstruct" do
+ it "returns an array of attribute values" do
+ struct = Struct.new(:x, :y)
+ s = struct.new(1, 2)
+
+ s.deconstruct.should == [1, 2]
+ end
+end
diff --git a/spec/ruby/core/struct/dig_spec.rb b/spec/ruby/core/struct/dig_spec.rb
new file mode 100644
index 0000000000..52e4d1dd9a
--- /dev/null
+++ b/spec/ruby/core/struct/dig_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../../spec_helper'
+
+describe "Struct#dig" do
+ before(:each) do
+ @klass = Struct.new(:a)
+ @instance = @klass.new(@klass.new({ b: [1, 2, 3] }))
+ end
+
+ it "returns the nested value specified by the sequence of keys" do
+ @instance.dig(:a, :a).should == { b: [1, 2, 3] }
+ end
+
+ it "accepts String keys" do
+ @instance.dig('a', 'a').should == { b: [1, 2, 3] }
+ end
+
+ it "returns the value by the index" do
+ instance = Struct.new(:a, :b).new(:one, :two)
+ instance.dig(0).should == :one
+ instance.dig(1).should == :two
+ end
+
+ it "returns the nested value specified if the sequence includes an index" do
+ @instance.dig(:a, :a, :b, 0).should == 1
+ end
+
+ it "returns nil if any intermediate step is nil" do
+ @instance.dig(:b, 0).should == nil
+ end
+
+ it "raises a TypeError if any intermediate step does not respond to #dig" do
+ instance = @klass.new(1)
+ -> {
+ instance.dig(:a, 3)
+ }.should.raise(TypeError)
+ end
+
+ it "raises an ArgumentError if no arguments provided" do
+ -> { @instance.dig }.should.raise(ArgumentError)
+ end
+
+ it "calls #dig on any intermediate step with the rest of the sequence as arguments" do
+ obj = Object.new
+ instance = @klass.new(obj)
+
+ def obj.dig(*args)
+ {dug: args}
+ end
+
+ instance.dig(:a, :bar, :baz).should == { dug: [:bar, :baz] }
+ end
+end
diff --git a/spec/ruby/core/struct/dup_spec.rb b/spec/ruby/core/struct/dup_spec.rb
new file mode 100644
index 0000000000..8b50c39014
--- /dev/null
+++ b/spec/ruby/core/struct/dup_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/dup'
+
+describe "Struct-based class#dup" do
+
+ it_behaves_like :struct_dup, :dup
+
+ # From https://github.com/jruby/jruby/issues/3686
+ it "retains an included module in the ancestor chain for the struct's singleton class" do
+ klass = Struct.new(:foo)
+ mod = Module.new do
+ def hello
+ "hello"
+ end
+ end
+
+ klass.extend(mod)
+ klass_dup = klass.dup
+ klass_dup.hello.should == "hello"
+ end
+
+end
diff --git a/spec/ruby/core/struct/each_pair_spec.rb b/spec/ruby/core/struct/each_pair_spec.rb
new file mode 100644
index 0000000000..db146c81e9
--- /dev/null
+++ b/spec/ruby/core/struct/each_pair_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Struct#each_pair" do
+ before :each do
+ @car = StructClasses::Car.new('Ford', 'Ranger', 2001)
+ end
+
+ it "passes each key value pair to the given block" do
+ @car.each_pair do |key, value|
+ value.should == @car[key]
+ end
+ end
+
+ context "with a block variable" do
+ it "passes an array to the given block" do
+ @car.each_pair.map { |var| var }.should == StructClasses::Car.members.zip(@car.values)
+ end
+ end
+
+ it "returns self if passed a block" do
+ @car.each_pair {}.should.equal?(@car)
+ end
+
+ it "returns an Enumerator if not passed a block" do
+ @car.each_pair.should.instance_of?(Enumerator)
+ end
+
+ it_behaves_like :struct_accessor, :each_pair
+ it_behaves_like :enumeratorized_with_origin_size, :each_pair, StructClasses::Car.new('Ford', 'Ranger')
+end
diff --git a/spec/ruby/core/struct/each_spec.rb b/spec/ruby/core/struct/each_spec.rb
new file mode 100644
index 0000000000..4fbdfee02a
--- /dev/null
+++ b/spec/ruby/core/struct/each_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Struct#each" do
+ it "passes each value to the given block" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ i = -1
+ car.each do |value|
+ value.should == car[i += 1]
+ end
+ end
+
+ it "returns self if passed a block" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.each {}.should == car
+ end
+
+ it "returns an Enumerator if not passed a block" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.each.should.instance_of?(Enumerator)
+ end
+
+ it_behaves_like :struct_accessor, :each
+ it_behaves_like :enumeratorized_with_origin_size, :each, StructClasses::Car.new('Ford', 'Ranger')
+end
diff --git a/spec/ruby/core/struct/element_reference_spec.rb b/spec/ruby/core/struct/element_reference_spec.rb
new file mode 100644
index 0000000000..b94f3aae8c
--- /dev/null
+++ b/spec/ruby/core/struct/element_reference_spec.rb
@@ -0,0 +1,52 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct[]" do
+ it "is a synonym for new" do
+ StructClasses::Ruby['2.0', 'i686'].should.is_a?(StructClasses::Ruby)
+ end
+end
+
+describe "Struct#[]" do
+ it "returns the attribute referenced" do
+ car = StructClasses::Car.new('Ford', 'Ranger', 1983)
+ car['make'].should == 'Ford'
+ car['model'].should == 'Ranger'
+ car['year'].should == 1983
+ car[:make].should == 'Ford'
+ car[:model].should == 'Ranger'
+ car[:year].should == 1983
+ car[0].should == 'Ford'
+ car[1].should == 'Ranger'
+ car[2].should == 1983
+ car[-3].should == 'Ford'
+ car[-2].should == 'Ranger'
+ car[-1].should == 1983
+ end
+
+ it "fails when it does not know about the requested attribute" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ -> { car[3] }.should.raise(IndexError)
+ -> { car[-4] }.should.raise(IndexError)
+ -> { car[:body] }.should.raise(NameError)
+ -> { car['wheels'] }.should.raise(NameError)
+ end
+
+ it "fails if passed too many arguments" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ -> { car[:make, :model] }.should.raise(ArgumentError)
+ end
+
+ it "fails if not passed a string, symbol, or integer" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ -> { car[Object.new] }.should.raise(TypeError)
+ end
+
+ it "returns attribute names that contain hyphens" do
+ klass = Struct.new(:'current-state')
+ tuple = klass.new(0)
+ tuple['current-state'].should == 0
+ tuple[:'current-state'].should == 0
+ tuple[0].should == 0
+ end
+end
diff --git a/spec/ruby/core/struct/element_set_spec.rb b/spec/ruby/core/struct/element_set_spec.rb
new file mode 100644
index 0000000000..e5438ca09a
--- /dev/null
+++ b/spec/ruby/core/struct/element_set_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct#[]=" do
+ it "assigns the passed value" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+
+ car[:model] = 'Escape'
+ car[:model].should == 'Escape'
+
+ car['model'] = 'Fusion'
+ car[:model].should == 'Fusion'
+
+ car[1] = 'Excursion'
+ car[:model].should == 'Excursion'
+
+ car[-1] = '2000-2005'
+ car[:year].should == '2000-2005'
+ end
+
+ it "fails when trying to assign attributes which don't exist" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+
+ -> { car[:something] = true }.should.raise(NameError)
+ -> { car[3] = true }.should.raise(IndexError)
+ -> { car[-4] = true }.should.raise(IndexError)
+ -> { car[Object.new] = true }.should.raise(TypeError)
+ end
+
+ it "raises a FrozenError on a frozen struct" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.freeze
+
+ -> { car[:model] = 'Escape' }.should.raise(FrozenError)
+ end
+end
diff --git a/spec/ruby/core/struct/eql_spec.rb b/spec/ruby/core/struct/eql_spec.rb
new file mode 100644
index 0000000000..327c927278
--- /dev/null
+++ b/spec/ruby/core/struct/eql_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/equal_value'
+
+describe "Struct#eql?" do
+ it_behaves_like :struct_equal_value, :eql?
+
+ it "returns false if any corresponding elements are not #eql?" do
+ car = StructClasses::Car.new("Honda", "Accord", 1998)
+ similar_car = StructClasses::Car.new("Honda", "Accord", 1998.0)
+ car.should_not.eql?(similar_car)
+ end
+end
diff --git a/spec/ruby/core/struct/equal_value_spec.rb b/spec/ruby/core/struct/equal_value_spec.rb
new file mode 100644
index 0000000000..0c0f7ba570
--- /dev/null
+++ b/spec/ruby/core/struct/equal_value_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/equal_value'
+
+describe "Struct#==" do
+ it_behaves_like :struct_equal_value, :==
+end
diff --git a/spec/ruby/core/struct/filter_spec.rb b/spec/ruby/core/struct/filter_spec.rb
new file mode 100644
index 0000000000..0ccd8ad6b2
--- /dev/null
+++ b/spec/ruby/core/struct/filter_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/select'
+require_relative 'shared/accessor'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Struct#filter" do
+ it_behaves_like :struct_select, :filter
+ it_behaves_like :struct_accessor, :filter
+ it_behaves_like :enumeratorized_with_origin_size, :filter, Struct.new(:foo).new
+end
diff --git a/spec/ruby/core/struct/fixtures/classes.rb b/spec/ruby/core/struct/fixtures/classes.rb
new file mode 100644
index 0000000000..675d403abd
--- /dev/null
+++ b/spec/ruby/core/struct/fixtures/classes.rb
@@ -0,0 +1,35 @@
+module StructClasses
+
+ class Apple < Struct; end
+
+ Ruby = Struct.new(:version, :platform)
+ Single = Struct.new(:value)
+
+ Car = Struct.new(:make, :model, :year)
+
+ class Honda < Car
+ def initialize(*args)
+ self.make = "Honda"
+ super(*args)
+ end
+ end
+
+ class StructWithOverriddenName < Struct.new(:a)
+ def self.name
+ "A"
+ end
+ end
+
+ class SubclassX < Struct
+ end
+
+ class SubclassX
+ attr_reader :key
+ def initialize(*)
+ @key = :value
+ super
+ end
+ end
+
+ class StructSubclass < Struct; end
+end
diff --git a/spec/ruby/core/struct/hash_spec.rb b/spec/ruby/core/struct/hash_spec.rb
new file mode 100644
index 0000000000..750387b326
--- /dev/null
+++ b/spec/ruby/core/struct/hash_spec.rb
@@ -0,0 +1,64 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+
+describe "Struct#hash" do
+
+ it "returns the same integer for structs with the same content" do
+ [StructClasses::Ruby.new("1.8.6", "PPC"),
+ StructClasses::Car.new("Hugo", "Foo", "1972")].each do |stc|
+ stc.hash.should == stc.dup.hash
+ stc.hash.should.is_a?(Integer)
+ end
+ end
+
+ it "returns the same value if structs are #eql?" do
+ car = StructClasses::Car.new("Honda", "Accord", "1998")
+ similar_car = StructClasses::Car.new("Honda", "Accord", "1998")
+ car.should.eql?(similar_car)
+ car.hash.should == similar_car.hash
+ end
+
+ it "returns different hashes for structs with different values" do
+ s1 = StructClasses::Ruby.new('2.7.0', 'linux')
+ s2 = StructClasses::Ruby.new('2.7.0', 'macos')
+ s1.hash.should_not == s2.hash
+ end
+
+ it "returns different hashes for structs with different values when using keyword_init: true" do
+ key = :"1 non symbol member"
+ struct_class = Struct.new(key, keyword_init: true)
+ t1 = struct_class.new(key => 1)
+ t2 = struct_class.new(key => 2)
+ t1.hash.should_not == t2.hash
+ end
+
+ it "allows for overriding methods in an included module" do
+ mod = Module.new do
+ def hash
+ "different"
+ end
+ end
+ s = Struct.new(:arg) do
+ include mod
+ end
+ s.new.hash.should == "different"
+ end
+
+ it "returns the same hash for recursive structs" do
+ car = StructClasses::Car.new("Honda", "Accord", "1998")
+ similar_car = StructClasses::Car.new("Honda", "Accord", "1998")
+ car[:make] = car
+ similar_car[:make] = car
+ car.hash.should == similar_car.hash
+ # This is because car.eql?(similar_car).
+ # Objects that are eql? must return the same hash.
+ # See the Struct#eql? specs
+ end
+
+ it "returns different hashes for different struct classes" do
+ Struct.new(:x).new(1).hash.should != Struct.new(:y).new(1).hash
+ end
+
+ it_behaves_like :struct_accessor, :hash
+end
diff --git a/spec/ruby/core/struct/initialize_spec.rb b/spec/ruby/core/struct/initialize_spec.rb
new file mode 100644
index 0000000000..c824f52e13
--- /dev/null
+++ b/spec/ruby/core/struct/initialize_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct#initialize" do
+
+ it "is private" do
+ StructClasses::Car.private_instance_methods(true).should.include?(:initialize)
+ end
+
+ it 'allows valid Ruby method names for members' do
+ valid_method_names = [
+ :method1,
+ :method_1,
+ :method_1?,
+ :method_1!,
+ :a_method
+ ]
+ valid_method_names.each do |method_name|
+ klass = Struct.new(method_name)
+ instance = klass.new(:value)
+ instance.send(method_name).should == :value
+ writer_method = "#{method_name}=".to_sym
+ result = instance.send(writer_method, :new_value)
+ result.should == :new_value
+ instance.send(method_name).should == :new_value
+ end
+ end
+
+ it "does nothing when passed a set of fields equal to self" do
+ car = same_car = StructClasses::Car.new("Honda", "Accord", "1998")
+ car.instance_eval { initialize("Honda", "Accord", "1998") }
+ car.should == same_car
+ end
+
+ it "explicitly sets instance variables to nil when args not provided to initialize" do
+ car = StructClasses::Honda.new
+ car.make.should == nil # still nil despite override in Honda#initialize b/c of super order
+ end
+
+ it "can be overridden" do
+ StructClasses::SubclassX.new(:y).new.key.should == :value
+ end
+
+ it "can be initialized with keyword arguments" do
+ positional_args = StructClasses::Ruby.new("3.2", "OS")
+ keyword_args = StructClasses::Ruby.new(version: "3.2", platform: "OS")
+
+ positional_args.version.should == keyword_args.version
+ positional_args.platform.should == keyword_args.platform
+ end
+
+ it "accepts positional arguments with empty keyword arguments" do
+ data = StructClasses::Single.new(42, **{})
+
+ data.value.should == 42
+
+ data = StructClasses::Ruby.new("3.2", "OS", **{})
+
+ data.version.should == "3.2"
+ data.platform.should == "OS"
+ end
+
+ it "can be called via delegated ... from a prepended module" do
+ wrapper = Module.new do
+ def initialize(...)
+ super(...)
+ end
+ end
+
+ klass = Class.new(Struct.new(:a)) { prepend wrapper }
+ s = klass.new("x")
+ s.a.should == "x"
+ end
+end
diff --git a/spec/ruby/core/struct/inspect_spec.rb b/spec/ruby/core/struct/inspect_spec.rb
new file mode 100644
index 0000000000..657b06abc1
--- /dev/null
+++ b/spec/ruby/core/struct/inspect_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/inspect'
+
+describe "Struct#inspect" do
+ it_behaves_like :struct_inspect, :inspect
+end
diff --git a/spec/ruby/core/struct/instance_variable_get_spec.rb b/spec/ruby/core/struct/instance_variable_get_spec.rb
new file mode 100644
index 0000000000..e3555cd246
--- /dev/null
+++ b/spec/ruby/core/struct/instance_variable_get_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct#instance_variable_get" do
+ it "returns nil for attributes" do
+ car = StructClasses::Car.new("Hugo", "Foo", "1972")
+ car.instance_variable_get(:@make).should == nil
+ end
+
+ it "returns a user value for variables with the same name as attributes" do
+ car = StructClasses::Car.new("Hugo", "Foo", "1972")
+ car.instance_variable_set :@make, "explicit"
+ car.instance_variable_get(:@make).should == "explicit"
+ car.make.should == "Hugo"
+ end
+end
diff --git a/spec/ruby/core/struct/instance_variables_spec.rb b/spec/ruby/core/struct/instance_variables_spec.rb
new file mode 100644
index 0000000000..f6d30ea97e
--- /dev/null
+++ b/spec/ruby/core/struct/instance_variables_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct#instance_variables" do
+ it "returns an empty array if only attributes are defined" do
+ car = StructClasses::Car.new("Hugo", "Foo", "1972")
+ car.instance_variables.should == []
+ end
+
+ it "returns an array with one name if an instance variable is added" do
+ car = StructClasses::Car.new("Hugo", "Foo", "1972")
+ car.instance_variables.should == []
+ car.instance_variable_set("@test", 1)
+ car.instance_variables.should == [:@test]
+ end
+end
diff --git a/spec/ruby/core/struct/keyword_init_spec.rb b/spec/ruby/core/struct/keyword_init_spec.rb
new file mode 100644
index 0000000000..42fcd3cc29
--- /dev/null
+++ b/spec/ruby/core/struct/keyword_init_spec.rb
@@ -0,0 +1,45 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# See https://bugs.ruby-lang.org/issues/18008
+describe "StructClass#keyword_init?" do
+ it "returns true for a struct that accepts keyword arguments to initialize" do
+ struct = Struct.new(:arg, keyword_init: true)
+ struct.keyword_init?.should == true
+ end
+
+ it "returns false for a struct that does not accept keyword arguments to initialize" do
+ struct = Struct.new(:arg, keyword_init: false)
+ struct.keyword_init?.should == false
+ end
+
+ it "returns nil for a struct that did not explicitly specify keyword_init" do
+ struct = Struct.new(:arg)
+ struct.keyword_init?.should == nil
+ end
+
+ it "returns nil for a struct that does specify keyword_init to be nil" do
+ struct = Struct.new(:arg, keyword_init: nil)
+ struct.keyword_init?.should == nil
+ end
+
+ it "returns true for any truthy value, not just for true" do
+ struct = Struct.new(:arg, keyword_init: 1)
+ struct.keyword_init?.should == true
+
+ struct = Struct.new(:arg, keyword_init: "")
+ struct.keyword_init?.should == true
+
+ struct = Struct.new(:arg, keyword_init: [])
+ struct.keyword_init?.should == true
+
+ struct = Struct.new(:arg, keyword_init: {})
+ struct.keyword_init?.should == true
+ end
+
+ context "class inheriting Struct" do
+ it "isn't available in a subclass" do
+ StructClasses::StructSubclass.should_not.respond_to?(:keyword_init?)
+ end
+ end
+end
diff --git a/spec/ruby/core/struct/length_spec.rb b/spec/ruby/core/struct/length_spec.rb
new file mode 100644
index 0000000000..1143676122
--- /dev/null
+++ b/spec/ruby/core/struct/length_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+
+describe "Struct#length" do
+ it "returns the number of attributes" do
+ StructClasses::Car.new('Cadillac', 'DeVille').length.should == 3
+ StructClasses::Car.new.length.should == 3
+ end
+
+ it_behaves_like :struct_accessor, :length
+end
diff --git a/spec/ruby/core/struct/members_spec.rb b/spec/ruby/core/struct/members_spec.rb
new file mode 100644
index 0000000000..1ff7b9387a
--- /dev/null
+++ b/spec/ruby/core/struct/members_spec.rb
@@ -0,0 +1,25 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+
+describe "Struct#members" do
+ it "returns an array of attribute names" do
+ StructClasses::Car.new.members.should == [:make, :model, :year]
+ StructClasses::Car.new('Cadillac').members.should == [:make, :model, :year]
+ StructClasses::Ruby.members.should == [:version, :platform]
+ end
+
+ it_behaves_like :struct_accessor, :members
+end
+
+describe "StructClass#members" do
+ it "returns an array of attribute names" do
+ StructClasses::Car.members.should == [:make, :model, :year]
+ end
+
+ context "class inheriting Struct" do
+ it "isn't available in a subclass" do
+ StructClasses::StructSubclass.should_not.respond_to?(:members)
+ end
+ end
+end
diff --git a/spec/ruby/core/struct/new_spec.rb b/spec/ruby/core/struct/new_spec.rb
new file mode 100644
index 0000000000..b3ece2efed
--- /dev/null
+++ b/spec/ruby/core/struct/new_spec.rb
@@ -0,0 +1,255 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct.new" do
+ it "creates a constant in Struct namespace with string as first argument" do
+ struct = Struct.new('Animal', :name, :legs, :eyeballs)
+ struct.should == Struct::Animal
+ struct.name.should == "Struct::Animal"
+ ensure
+ Struct.send(:remove_const, :Animal)
+ end
+
+ it "overwrites previously defined constants with string as first argument" do
+ first = Struct.new('Person', :height, :weight)
+ first.should == Struct::Person
+
+ second = nil
+ -> {
+ second = Struct.new('Person', :hair, :sex)
+ }.should complain(/constant/)
+ second.should == Struct::Person
+
+ first.members.should_not == second.members
+ ensure
+ Struct.send(:remove_const, :Person)
+ end
+
+ it "calls to_str on its first argument (constant name)" do
+ obj = mock('Foo')
+ def obj.to_str() "Foo" end
+ struct = Struct.new(obj)
+ struct.should == Struct::Foo
+ struct.name.should == "Struct::Foo"
+ ensure
+ Struct.send(:remove_const, :Foo)
+ end
+
+ it "creates a new anonymous class with nil first argument" do
+ struct = Struct.new(nil, :foo)
+ struct.new("bar").foo.should == "bar"
+ struct.should.is_a?(Class)
+ struct.name.should == nil
+ end
+
+ it "creates a new anonymous class with symbol arguments" do
+ struct = Struct.new(:make, :model)
+ struct.should.is_a?(Class)
+ struct.name.should == nil
+ end
+
+ it "does not create a constant with symbol as first argument" do
+ Struct.new(:Animal2, :name, :legs, :eyeballs)
+ Struct.const_defined?("Animal2").should == false
+ end
+
+ it "allows non-ASCII member name" do
+ name = "r\xe9sum\xe9".dup.force_encoding(Encoding::ISO_8859_1).to_sym
+ struct = Struct.new(name)
+ struct.new("foo").send(name).should == "foo"
+ end
+
+ it "fails with invalid constant name as first argument" do
+ -> { Struct.new('animal', :name, :legs, :eyeballs) }.should.raise(NameError)
+ end
+
+ it "raises a TypeError if object doesn't respond to to_sym" do
+ -> { Struct.new(:animal, mock('giraffe')) }.should.raise(TypeError)
+ -> { Struct.new(:animal, 1.0) }.should.raise(TypeError)
+ -> { Struct.new(:animal, Time.now) }.should.raise(TypeError)
+ -> { Struct.new(:animal, Class) }.should.raise(TypeError)
+ -> { Struct.new(:animal, nil) }.should.raise(TypeError)
+ -> { Struct.new(:animal, true) }.should.raise(TypeError)
+ -> { Struct.new(:animal, ['chris', 'evan']) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError if passed a Hash with an unknown key" do
+ -> { Struct.new(:animal, { name: 'chris' }) }.should.raise(TypeError)
+ end
+
+ it "works when not provided any arguments" do
+ c = Struct.new
+ c.should.is_a?(Class)
+ c.superclass.should == Struct
+ end
+
+ it "raises ArgumentError when there is a duplicate member" do
+ -> { Struct.new(:foo, :foo) }.should.raise(ArgumentError, "duplicate member: foo")
+ end
+
+ it "raises a TypeError if object is not a Symbol" do
+ obj = mock(':ruby')
+ def obj.to_sym() :ruby end
+ -> { Struct.new(:animal, obj) }.should.raise(TypeError)
+ end
+
+ it "processes passed block with instance_eval" do
+ klass = Struct.new(:something) { @something_else = 'something else entirely!' }
+ klass.instance_variables.should.include?(:@something_else)
+ end
+
+ context "with a block" do
+ it "allows class to be modified via the block" do
+ klass = Struct.new(:version) do
+ def platform
+ :ruby
+ end
+ end
+ instance = klass.new('2.2')
+
+ instance.version.should == '2.2'
+ instance.platform.should == :ruby
+ end
+
+ it "passes same struct class to the block" do
+ given = nil
+ klass = Struct.new(:attr) do |block_parameter|
+ given = block_parameter
+ end
+ klass.should.equal?(given)
+ end
+ end
+
+ context "on subclasses" do
+ it "creates a constant in subclass' namespace" do
+ struct = StructClasses::Apple.new('Computer', :size)
+ struct.should == StructClasses::Apple::Computer
+ ensure
+ StructClasses::Apple.send(:remove_const, :Computer)
+ end
+
+ it "creates an instance" do
+ StructClasses::Ruby.new.kind_of?(StructClasses::Ruby).should == true
+ end
+
+ it "creates reader methods" do
+ StructClasses::Ruby.new.should.respond_to?(:version)
+ StructClasses::Ruby.new.should.respond_to?(:platform)
+ end
+
+ it "creates writer methods" do
+ StructClasses::Ruby.new.should.respond_to?(:version=)
+ StructClasses::Ruby.new.should.respond_to?(:platform=)
+ end
+
+ it "fails with too many arguments" do
+ -> { StructClasses::Ruby.new('2.0', 'i686', true) }.should.raise(ArgumentError)
+ end
+
+ it "accepts keyword arguments to initialize" do
+ type = Struct.new(:args)
+
+ obj = type.new(args: 42)
+ obj2 = type.new(42)
+
+ obj.should == obj2
+ obj.args.should == 42
+ obj2.args.should == 42
+ end
+
+ context "given positional and keyword arguments" do
+ it "treats keyword arguments as a positional parameter" do
+ type = Struct.new(:a, :b)
+ s = type.new("a", b: "b")
+ s.a.should == "a"
+ s.b.should == {b: "b"}
+
+ type = Struct.new(:a, :b, :c)
+ s = type.new("a", b: "b", c: "c")
+ s.a.should == "a"
+ s.b.should == {b: "b", c: "c"}
+ s.c.should == nil
+ end
+
+ it "ignores empty keyword arguments" do
+ type = Struct.new(:a, :b)
+ h = {}
+ s = type.new("a", **h)
+
+ s.a.should == "a"
+ s.b.should == nil
+ end
+
+ it "raises ArgumentError when all struct attribute values are specified" do
+ type = Struct.new(:a, :b)
+ -> { type.new("a", "b", c: "c") }.should.raise(ArgumentError, "struct size differs")
+ end
+ end
+ end
+
+ context "keyword_init: true option" do
+ before :all do
+ @struct_with_kwa = Struct.new(:name, :legs, keyword_init: true)
+ end
+
+ it "creates a class that accepts keyword arguments to initialize" do
+ obj = @struct_with_kwa.new(name: "elefant", legs: 4)
+ obj.name.should == "elefant"
+ obj.legs.should == 4
+ end
+
+ it "raises when there is a duplicate member" do
+ -> { Struct.new(:foo, :foo, keyword_init: true) }.should.raise(ArgumentError, "duplicate member: foo")
+ end
+
+ describe "new class instantiation" do
+ it "accepts arguments as hash as well" do
+ obj = @struct_with_kwa.new({name: "elefant", legs: 4})
+ obj.name.should == "elefant"
+ obj.legs.should == 4
+ end
+
+ it "allows missing arguments" do
+ obj = @struct_with_kwa.new(name: "elefant")
+ obj.name.should == "elefant"
+ obj.legs.should == nil
+ end
+
+ it "allows no arguments" do
+ obj = @struct_with_kwa.new
+ obj.name.should == nil
+ obj.legs.should == nil
+ end
+
+ it "raises ArgumentError when passed not declared keyword argument" do
+ -> {
+ @struct_with_kwa.new(name: "elefant", legs: 4, foo: "bar")
+ }.should.raise(ArgumentError, /unknown keywords: foo/)
+ end
+
+ it "raises ArgumentError when passed a list of arguments" do
+ -> {
+ @struct_with_kwa.new("elefant", 4)
+ }.should.raise(ArgumentError, /wrong number of arguments/)
+ end
+
+ it "raises ArgumentError when passed a single non-hash argument" do
+ -> {
+ @struct_with_kwa.new("elefant")
+ }.should.raise(ArgumentError, /wrong number of arguments/)
+ end
+ end
+ end
+
+ context "keyword_init: false option" do
+ before :all do
+ @struct_without_kwa = Struct.new(:name, :legs, keyword_init: false)
+ end
+
+ it "behaves like it does without :keyword_init option" do
+ obj = @struct_without_kwa.new("elefant", 4)
+ obj.name.should == "elefant"
+ obj.legs.should == 4
+ end
+ end
+end
diff --git a/spec/ruby/core/struct/select_spec.rb b/spec/ruby/core/struct/select_spec.rb
new file mode 100644
index 0000000000..ee846ec45f
--- /dev/null
+++ b/spec/ruby/core/struct/select_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/select'
+require_relative 'shared/accessor'
+require_relative '../enumerable/shared/enumeratorized'
+
+describe "Struct#select" do
+ it_behaves_like :struct_select, :select
+ it_behaves_like :struct_accessor, :select
+ it_behaves_like :enumeratorized_with_origin_size, :select, Struct.new(:foo).new
+end
diff --git a/spec/ruby/core/struct/shared/accessor.rb b/spec/ruby/core/struct/shared/accessor.rb
new file mode 100644
index 0000000000..dbf5e78f43
--- /dev/null
+++ b/spec/ruby/core/struct/shared/accessor.rb
@@ -0,0 +1,7 @@
+describe :struct_accessor, shared: true do
+ it "does not override the instance accessor method" do
+ struct = Struct.new(@method.to_sym)
+ instance = struct.new 42
+ instance.send(@method).should == 42
+ end
+end
diff --git a/spec/ruby/core/struct/shared/dup.rb b/spec/ruby/core/struct/shared/dup.rb
new file mode 100644
index 0000000000..994f3f443e
--- /dev/null
+++ b/spec/ruby/core/struct/shared/dup.rb
@@ -0,0 +1,9 @@
+describe :struct_dup, shared: true do
+ it "duplicates members" do
+ klass = Struct.new(:foo, :bar)
+ instance = klass.new(14, 2)
+ duped = instance.send(@method)
+ duped.foo.should == 14
+ duped.bar.should == 2
+ end
+end
diff --git a/spec/ruby/core/struct/shared/equal_value.rb b/spec/ruby/core/struct/shared/equal_value.rb
new file mode 100644
index 0000000000..a7e0856df5
--- /dev/null
+++ b/spec/ruby/core/struct/shared/equal_value.rb
@@ -0,0 +1,37 @@
+describe :struct_equal_value, shared: true do
+ it "returns true if the other is the same object" do
+ car = same_car = StructClasses::Car.new("Honda", "Accord", "1998")
+ car.send(@method, same_car).should == true
+ end
+
+ it "returns true if the other has all the same fields" do
+ car = StructClasses::Car.new("Honda", "Accord", "1998")
+ similar_car = StructClasses::Car.new("Honda", "Accord", "1998")
+ car.send(@method, similar_car).should == true
+ end
+
+ it "returns false if the other is a different object or has different fields" do
+ car = StructClasses::Car.new("Honda", "Accord", "1998")
+ different_car = StructClasses::Car.new("Honda", "Accord", "1995")
+ car.send(@method, different_car).should == false
+ end
+
+ it "returns false if other is of a different class" do
+ car = StructClasses::Car.new("Honda", "Accord", "1998")
+ klass = Struct.new(:make, :model, :year)
+ clone = klass.new("Honda", "Accord", "1998")
+ car.send(@method, clone).should == false
+ end
+
+ it "handles recursive structures by returning false if a difference can be found" do
+ x = StructClasses::Car.new("Honda", "Accord", "1998")
+ x[:make] = x
+ stepping = StructClasses::Car.new("Honda", "Accord", "1998")
+ stone = StructClasses::Car.new(stepping, "Accord", "1998")
+ stepping[:make] = stone
+ x.send(@method, stepping).should == true
+
+ stone[:year] = "1999" # introduce a difference
+ x.send(@method, stepping).should == false
+ end
+end
diff --git a/spec/ruby/core/struct/shared/inspect.rb b/spec/ruby/core/struct/shared/inspect.rb
new file mode 100644
index 0000000000..1a0fb6a6b2
--- /dev/null
+++ b/spec/ruby/core/struct/shared/inspect.rb
@@ -0,0 +1,40 @@
+describe :struct_inspect, shared: true do
+ it "returns a string representation showing members and values" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.send(@method).should == '#<struct StructClasses::Car make="Ford", model="Ranger", year=nil>'
+ end
+
+ it "returns a string representation without the class name for anonymous structs" do
+ Struct.new(:a).new("").send(@method).should == '#<struct a="">'
+ end
+
+ it "returns a string representation without the class name for structs nested in anonymous classes" do
+ c = Class.new
+ c.class_eval <<~DOC
+ class Foo < Struct.new(:a); end
+ DOC
+
+ c::Foo.new("").send(@method).should == '#<struct a="">'
+ end
+
+ it "returns a string representation without the class name for structs nested in anonymous modules" do
+ m = Module.new
+ m.module_eval <<~DOC
+ class Foo < Struct.new(:a); end
+ DOC
+
+ m::Foo.new("").send(@method).should == '#<struct a="">'
+ end
+
+ it "does not call #name method" do
+ struct = StructClasses::StructWithOverriddenName.new("")
+ struct.send(@method).should == '#<struct StructClasses::StructWithOverriddenName a="">'
+ end
+
+ it "does not call #name method when struct is anonymous" do
+ struct = Struct.new(:a)
+ def struct.name; "A"; end
+
+ struct.new("").send(@method).should == '#<struct a="">'
+ end
+end
diff --git a/spec/ruby/core/struct/shared/select.rb b/spec/ruby/core/struct/shared/select.rb
new file mode 100644
index 0000000000..dfa1a809fe
--- /dev/null
+++ b/spec/ruby/core/struct/shared/select.rb
@@ -0,0 +1,26 @@
+require_relative '../../../spec_helper'
+require_relative '../fixtures/classes'
+
+describe :struct_select, shared: true do
+ it "raises an ArgumentError if given any non-block arguments" do
+ struct = StructClasses::Car.new
+ -> { struct.send(@method, 1) { } }.should.raise(ArgumentError)
+ end
+
+ it "returns a new array of elements for which block is true" do
+ struct = StructClasses::Car.new("Toyota", "Tercel", "2000")
+ struct.send(@method) { |i| i == "2000" }.should == [ "2000" ]
+ end
+
+ it "returns an instance of Array" do
+ struct = StructClasses::Car.new("Ford", "Escort", "1995")
+ struct.send(@method) { true }.should.instance_of?(Array)
+ end
+
+ describe "without block" do
+ it "returns an instance of Enumerator" do
+ struct = Struct.new(:foo).new
+ struct.send(@method).should.instance_of?(Enumerator)
+ end
+ end
+end
diff --git a/spec/ruby/core/struct/size_spec.rb b/spec/ruby/core/struct/size_spec.rb
new file mode 100644
index 0000000000..09f260cf20
--- /dev/null
+++ b/spec/ruby/core/struct/size_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+
+describe "Struct#size" do
+ it "is a synonym for length" do
+ StructClasses::Car.new.size.should == StructClasses::Car.new.length
+ end
+
+ it_behaves_like :struct_accessor, :size
+end
diff --git a/spec/ruby/core/struct/struct_spec.rb b/spec/ruby/core/struct/struct_spec.rb
new file mode 100644
index 0000000000..9fab9c0629
--- /dev/null
+++ b/spec/ruby/core/struct/struct_spec.rb
@@ -0,0 +1,50 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct" do
+ it "includes Enumerable" do
+ Struct.include?(Enumerable).should == true
+ end
+end
+
+describe "Struct anonymous class instance methods" do
+ it "includes Enumerable" do
+ StructClasses::Car.include?(Enumerable).should == true
+ end
+
+ it "reader method should be a synonym for []" do
+ klass = Struct.new(:clock, :radio)
+ alarm = klass.new(true)
+ alarm.clock.should == alarm[:clock]
+ alarm.radio.should == alarm['radio']
+ end
+
+ it "reader method should not interfere with undefined methods" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ -> { car.something_weird }.should.raise(NoMethodError)
+ end
+
+ it "writer method be a synonym for []=" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.model.should == 'Ranger'
+ car.model = 'F150'
+ car.model.should == 'F150'
+ car[:model].should == 'F150'
+ car['model'].should == 'F150'
+ car[1].should == 'F150'
+ end
+
+ it "writer methods raise a FrozenError on a frozen struct" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.freeze
+
+ -> { car.model = 'Escape' }.should.raise(FrozenError)
+ end
+end
+
+describe "Struct subclasses" do
+ it "can be subclassed" do
+ compact = Class.new StructClasses::Car
+ compact.new.class.should == compact
+ end
+end
diff --git a/spec/ruby/core/struct/to_a_spec.rb b/spec/ruby/core/struct/to_a_spec.rb
new file mode 100644
index 0000000000..cb61dc45cc
--- /dev/null
+++ b/spec/ruby/core/struct/to_a_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/accessor'
+
+describe "Struct#to_a" do
+ it "returns the values for this instance as an array" do
+ StructClasses::Car.new('Geo', 'Metro', 1995).to_a.should == ['Geo', 'Metro', 1995]
+ StructClasses::Car.new('Ford').to_a.should == ['Ford', nil, nil]
+ end
+
+ it_behaves_like :struct_accessor, :to_a
+end
diff --git a/spec/ruby/core/struct/to_h_spec.rb b/spec/ruby/core/struct/to_h_spec.rb
new file mode 100644
index 0000000000..e0846ef268
--- /dev/null
+++ b/spec/ruby/core/struct/to_h_spec.rb
@@ -0,0 +1,68 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct#to_h" do
+ it "returns a Hash with members as keys" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.to_h.should == {make: "Ford", model: "Ranger", year: nil}
+ end
+
+ it "returns a Hash that is independent from the struct" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.to_h[:make] = 'Suzuki'
+ car.make.should == 'Ford'
+ end
+
+ context "with block" do
+ it "converts [key, value] pairs returned by the block to a hash" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+
+ h = car.to_h { |k, v| [k.to_s, "#{v}".downcase] }
+ h.should == { "make" => "ford", "model" => "ranger", "year" => "" }
+ end
+
+ it "passes to a block each pair's key and value as separate arguments" do
+ s = StructClasses::Ruby.new('3.2.4', 'macos')
+
+ ScratchPad.record []
+ s.to_h { |k, v| ScratchPad << [k, v]; [k, v] }
+ ScratchPad.recorded.sort.should == [[:platform, 'macos'], [:version, '3.2.4']]
+
+ ScratchPad.record []
+ s.to_h { |*args| ScratchPad << args; [args[0], args[1]] }
+ ScratchPad.recorded.sort.should == [[:platform, 'macos'], [:version, '3.2.4']]
+ end
+
+ it "raises ArgumentError if block returns longer or shorter array" do
+ -> do
+ StructClasses::Car.new.to_h { |k, v| [k.to_s, "#{v}".downcase, 1] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+
+ -> do
+ StructClasses::Car.new.to_h { |k, v| [k] }
+ end.should.raise(ArgumentError, /element has wrong array length/)
+ end
+
+ it "raises TypeError if block returns something other than Array" do
+ -> do
+ StructClasses::Car.new.to_h { |k, v| "not-array" }
+ end.should.raise(TypeError, /wrong element type String/)
+ end
+
+ it "coerces returned pair to Array with #to_ary" do
+ x = mock('x')
+ x.stub!(:to_ary).and_return([:b, 'b'])
+
+ StructClasses::Car.new.to_h { |k| x }.should == { :b => 'b' }
+ end
+
+ it "does not coerce returned pair to Array with #to_a" do
+ x = mock('x')
+ x.stub!(:to_a).and_return([:b, 'b'])
+
+ -> do
+ StructClasses::Car.new.to_h { |k| x }
+ end.should.raise(TypeError, /wrong element type MockObject/)
+ end
+ end
+end
diff --git a/spec/ruby/core/struct/to_s_spec.rb b/spec/ruby/core/struct/to_s_spec.rb
new file mode 100644
index 0000000000..94c672d3d5
--- /dev/null
+++ b/spec/ruby/core/struct/to_s_spec.rb
@@ -0,0 +1,12 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/inspect'
+
+describe "Struct#to_s" do
+ it "is a synonym for inspect" do
+ car = StructClasses::Car.new('Ford', 'Ranger')
+ car.inspect.should == car.to_s
+ end
+
+ it_behaves_like :struct_inspect, :to_s
+end
diff --git a/spec/ruby/core/struct/values_at_spec.rb b/spec/ruby/core/struct/values_at_spec.rb
new file mode 100644
index 0000000000..6aac5d96b3
--- /dev/null
+++ b/spec/ruby/core/struct/values_at_spec.rb
@@ -0,0 +1,59 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+# Should be synchronized with core/array/values_at_spec.rb
+describe "Struct#values_at" do
+ before do
+ clazz = Struct.new(:name, :director, :year)
+ @movie = clazz.new('Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002)
+ end
+
+ context "when passed a list of Integers" do
+ it "returns an array containing each value given by one of integers" do
+ @movie.values_at(0, 1).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park']
+ end
+
+ it "raises IndexError if any of integers is out of range" do
+ -> { @movie.values_at(3) }.should.raise(IndexError, "offset 3 too large for struct(size:3)")
+ -> { @movie.values_at(-4) }.should.raise(IndexError, "offset -4 too small for struct(size:3)")
+ end
+ end
+
+ context "when passed an integer Range" do
+ it "returns an array containing each value given by the elements of the range" do
+ @movie.values_at(0..2).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002]
+ end
+
+ it "fills with nil values for range elements larger than the structure" do
+ @movie.values_at(0..3).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002, nil]
+ end
+
+ it "raises RangeError if any element of the range is negative and out of range" do
+ -> { @movie.values_at(-4..3) }.should.raise(RangeError, "-4..3 out of range")
+ end
+
+ it "supports endless Range" do
+ @movie.values_at(0..).should == ["Sympathy for Mr. Vengeance", "Chan-wook Park", 2002]
+ end
+
+ it "supports beginningless Range" do
+ @movie.values_at(..2).should == ["Sympathy for Mr. Vengeance", "Chan-wook Park", 2002]
+ end
+ end
+
+ it "supports multiple integer Ranges" do
+ @movie.values_at(0..2, 1..2).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002, 'Chan-wook Park', 2002]
+ end
+
+ it "supports mixing integer Ranges and Integers" do
+ @movie.values_at(0..2, 2).should == ['Sympathy for Mr. Vengeance', 'Chan-wook Park', 2002, 2002]
+ end
+
+ it "returns a new empty Array if no arguments given" do
+ @movie.values_at().should == []
+ end
+
+ it "fails when passed unsupported types" do
+ -> { @movie.values_at('make') }.should.raise(TypeError, "no implicit conversion of String into Integer")
+ end
+end
diff --git a/spec/ruby/core/struct/values_spec.rb b/spec/ruby/core/struct/values_spec.rb
new file mode 100644
index 0000000000..b2d11725b9
--- /dev/null
+++ b/spec/ruby/core/struct/values_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Struct#values" do
+ it "is a synonym for to_a" do
+ car = StructClasses::Car.new('Nissan', 'Maxima')
+ car.values.should == car.to_a
+
+ StructClasses::Car.new.values.should == StructClasses::Car.new.to_a
+ end
+end
diff --git a/spec/ruby/core/symbol/all_symbols_spec.rb b/spec/ruby/core/symbol/all_symbols_spec.rb
new file mode 100644
index 0000000000..689f6211de
--- /dev/null
+++ b/spec/ruby/core/symbol/all_symbols_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Symbol.all_symbols" do
+ it "returns an array of Symbols" do
+ all_symbols = Symbol.all_symbols
+ all_symbols.should.instance_of?(Array)
+ all_symbols.each { |s| s.should.instance_of?(Symbol) }
+ end
+
+ it "includes symbols that are strongly referenced" do
+ symbol = "symbol_specs_#{rand(5_000_000)}".to_sym
+ Symbol.all_symbols.should.include?(symbol)
+ end
+
+ it "includes symbols that are referenced in source code but not yet executed" do
+ Symbol.all_symbols.any? { |s| s.to_s == 'symbol_specs_referenced_in_source_code' }.should == true
+ :symbol_specs_referenced_in_source_code
+ end
+end
diff --git a/spec/ruby/core/symbol/capitalize_spec.rb b/spec/ruby/core/symbol/capitalize_spec.rb
new file mode 100644
index 0000000000..a93d951e6a
--- /dev/null
+++ b/spec/ruby/core/symbol/capitalize_spec.rb
@@ -0,0 +1,41 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Symbol#capitalize" do
+ it "returns a Symbol" do
+ :glark.capitalize.should.instance_of?(Symbol)
+ end
+
+ it "converts the first character to uppercase if it is ASCII" do
+ :lower.capitalize.should == :Lower
+ end
+
+ it "leaves the first character alone if it is not an alphabetical character" do
+ :"£1.20".capitalize.should == :"£1.20"
+ end
+
+ it "capitalizes the first character if it is Unicode" do
+ :"äöü".capitalize.should == :"Äöü"
+ :"aou".capitalize.should == :"Aou"
+ end
+
+ it "converts subsequent uppercase ASCII characters to their lowercase equivalents" do
+ :lOWER.capitalize.should == :Lower
+ end
+
+ it "leaves ASCII characters already in the correct case as they were" do
+ :Title.capitalize.should == :Title
+ end
+
+ it "works with both upper- and lowercase ASCII characters in the same Symbol" do
+ :mIxEd.capitalize.should == :Mixed
+ end
+
+ it "leaves lowercase Unicode characters (except in first position) as they were" do
+ "a\u{00DF}C".to_sym.capitalize.should == :"Aßc"
+ end
+
+ it "leaves non-alphabetic ASCII characters as they were" do
+ "Glark?!?".to_sym.capitalize.should == :"Glark?!?"
+ end
+end
diff --git a/spec/ruby/core/symbol/case_compare_spec.rb b/spec/ruby/core/symbol/case_compare_spec.rb
new file mode 100644
index 0000000000..0c6bc1eda5
--- /dev/null
+++ b/spec/ruby/core/symbol/case_compare_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#===" do
+ it "returns true when the argument is a Symbol" do
+ (Symbol === :ruby).should == true
+ end
+
+ it "returns false when the argument is a String" do
+ (Symbol === 'ruby').should == false
+ end
+end
diff --git a/spec/ruby/core/symbol/casecmp_spec.rb b/spec/ruby/core/symbol/casecmp_spec.rb
new file mode 100644
index 0000000000..dcb77a8350
--- /dev/null
+++ b/spec/ruby/core/symbol/casecmp_spec.rb
@@ -0,0 +1,152 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Symbol#casecmp with Symbol" do
+ it "compares symbols without regard to case" do
+ :abcdef.casecmp(:abcde).should == 1
+ :aBcDeF.casecmp(:abcdef).should == 0
+ :abcdef.casecmp(:abcdefg).should == -1
+ :abcdef.casecmp(:ABCDEF).should == 0
+ end
+
+ it "doesn't consider non-ascii characters equal that aren't" do
+ # -- Latin-1 --
+ upper_a_tilde = "\xC3".b.to_sym
+ upper_a_umlaut = "\xC4".b.to_sym
+ lower_a_tilde = "\xE3".b.to_sym
+ lower_a_umlaut = "\xE4".b.to_sym
+
+ lower_a_tilde.casecmp(lower_a_umlaut).should_not == 0
+ lower_a_umlaut.casecmp(lower_a_tilde).should_not == 0
+ upper_a_tilde.casecmp(upper_a_umlaut).should_not == 0
+ upper_a_umlaut.casecmp(upper_a_tilde).should_not == 0
+
+ # -- UTF-8 --
+ upper_a_tilde = :"Ã"
+ lower_a_tilde = :"ã"
+ upper_a_umlaut = :"Ä"
+ lower_a_umlaut = :"ä"
+
+ lower_a_tilde.casecmp(lower_a_umlaut).should_not == 0
+ lower_a_umlaut.casecmp(lower_a_tilde).should_not == 0
+ upper_a_tilde.casecmp(upper_a_umlaut).should_not == 0
+ upper_a_umlaut.casecmp(upper_a_tilde).should_not == 0
+ end
+
+ it "doesn't do case mapping for non-ascii characters" do
+ # -- Latin-1 --
+ upper_a_tilde = "\xC3".b.to_sym
+ upper_a_umlaut = "\xC4".b.to_sym
+ lower_a_tilde = "\xE3".b.to_sym
+ lower_a_umlaut = "\xE4".b.to_sym
+
+ upper_a_tilde.casecmp(lower_a_tilde).should == -1
+ upper_a_umlaut.casecmp(lower_a_umlaut).should == -1
+ lower_a_tilde.casecmp(upper_a_tilde).should == 1
+ lower_a_umlaut.casecmp(upper_a_umlaut).should == 1
+
+ # -- UTF-8 --
+ upper_a_tilde = :"Ã"
+ lower_a_tilde = :"ã"
+ upper_a_umlaut = :"Ä"
+ lower_a_umlaut = :"ä"
+
+ upper_a_tilde.casecmp(lower_a_tilde).should == -1
+ upper_a_umlaut.casecmp(lower_a_umlaut).should == -1
+ lower_a_tilde.casecmp(upper_a_tilde).should == 1
+ lower_a_umlaut.casecmp(upper_a_umlaut).should == 1
+ end
+
+ it "returns 0 for empty strings in different encodings" do
+ ''.to_sym.casecmp(''.encode("UTF-32LE").to_sym).should == 0
+ end
+end
+
+describe "Symbol#casecmp" do
+ it "returns nil if other is a String" do
+ :abc.casecmp("abc").should == nil
+ end
+
+ it "returns nil if other is an Integer" do
+ :abc.casecmp(1).should == nil
+ end
+
+ it "returns nil if other is an object" do
+ obj = mock("string <=>")
+ :abc.casecmp(obj).should == nil
+ end
+end
+
+describe 'Symbol#casecmp?' do
+ it "compares symbols without regard to case" do
+ :abcdef.casecmp?(:abcde).should == false
+ :aBcDeF.casecmp?(:abcdef).should == true
+ :abcdef.casecmp?(:abcdefg).should == false
+ :abcdef.casecmp?(:ABCDEF).should == true
+ end
+
+ it "doesn't consider non-ascii characters equal that aren't" do
+ # -- Latin-1 --
+ upper_a_tilde = "\xC3".b.to_sym
+ upper_a_umlaut = "\xC4".b.to_sym
+ lower_a_tilde = "\xE3".b.to_sym
+ lower_a_umlaut = "\xE4".b.to_sym
+
+ lower_a_tilde.casecmp?(lower_a_umlaut).should_not == true
+ lower_a_umlaut.casecmp?(lower_a_tilde).should_not == true
+ upper_a_tilde.casecmp?(upper_a_umlaut).should_not == true
+ upper_a_umlaut.casecmp?(upper_a_tilde).should_not == true
+
+ # -- UTF-8 --
+ upper_a_tilde = :"Ã"
+ lower_a_tilde = :"ã"
+ upper_a_umlaut = :"Ä"
+ lower_a_umlaut = :"ä"
+
+ lower_a_tilde.casecmp?(lower_a_umlaut).should_not == true
+ lower_a_umlaut.casecmp?(lower_a_tilde).should_not == true
+ upper_a_tilde.casecmp?(upper_a_umlaut).should_not == true
+ upper_a_umlaut.casecmp?(upper_a_tilde).should_not == true
+ end
+
+ it "doesn't do case mapping for non-ascii and non-unicode characters" do
+ # -- Latin-1 --
+ upper_a_tilde = "\xC3".b.to_sym
+ upper_a_umlaut = "\xC4".b.to_sym
+ lower_a_tilde = "\xE3".b.to_sym
+ lower_a_umlaut = "\xE4".b.to_sym
+
+ upper_a_tilde.casecmp?(lower_a_tilde).should == false
+ upper_a_umlaut.casecmp?(lower_a_umlaut).should == false
+ lower_a_tilde.casecmp?(upper_a_tilde).should == false
+ lower_a_umlaut.casecmp?(upper_a_umlaut).should == false
+ end
+
+ it 'does case mapping for unicode characters' do
+ # -- UTF-8 --
+ upper_a_tilde = :"Ã"
+ lower_a_tilde = :"ã"
+ upper_a_umlaut = :"Ä"
+ lower_a_umlaut = :"ä"
+
+ upper_a_tilde.casecmp?(lower_a_tilde).should == true
+ upper_a_umlaut.casecmp?(lower_a_umlaut).should == true
+ lower_a_tilde.casecmp?(upper_a_tilde).should == true
+ lower_a_umlaut.casecmp?(upper_a_umlaut).should == true
+ end
+
+ it 'returns nil when comparing characters with different encodings' do
+ # -- Latin-1 --
+ upper_a_tilde = "\xC3".b.to_sym
+
+ # -- UTF-8 --
+ lower_a_tilde = :"ã"
+
+ upper_a_tilde.casecmp?(lower_a_tilde).should == nil
+ lower_a_tilde.casecmp?(upper_a_tilde).should == nil
+ end
+
+ it "returns true for empty symbols in different encodings" do
+ ''.to_sym.should.casecmp?(''.encode("UTF-32LE").to_sym)
+ end
+end
diff --git a/spec/ruby/core/symbol/comparison_spec.rb b/spec/ruby/core/symbol/comparison_spec.rb
new file mode 100644
index 0000000000..6d56176e97
--- /dev/null
+++ b/spec/ruby/core/symbol/comparison_spec.rb
@@ -0,0 +1,51 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#<=> with Symbol" do
+ it "compares individual characters based on their ascii value" do
+ ascii_order = Array.new(256) { |x| x.chr.to_sym }
+ sort_order = ascii_order.sort
+ sort_order.should == ascii_order
+ end
+
+ it "returns -1 when self is less than other" do
+ (:this <=> :those).should == -1
+ end
+
+ it "returns 0 when self is equal to other" do
+ (:yep <=> :yep).should == 0
+ end
+
+ it "returns 1 when self is greater than other" do
+ (:yoddle <=> :griddle).should == 1
+ end
+
+ it "considers symbol that comes lexicographically first to be less if the symbols have same size" do
+ (:aba <=> :abc).should == -1
+ (:abc <=> :aba).should == 1
+ end
+
+ it "doesn't consider shorter string to be less if longer string starts with shorter one" do
+ (:abc <=> :abcd).should == -1
+ (:abcd <=> :abc).should == 1
+ end
+
+ it "compares shorter string with corresponding number of first chars of longer string" do
+ (:abx <=> :abcd).should == 1
+ (:abcd <=> :abx).should == -1
+ end
+end
+
+describe "Symbol#<=>" do
+ it "returns nil if other is a String" do
+ (:abc <=> "abc").should == nil
+ end
+
+ it "returns nil if other is an Integer" do
+ (:abc <=> 1).should == nil
+ end
+
+ it "returns nil if other is an object" do
+ obj = mock("string <=>")
+ (:abc <=> obj).should == nil
+ end
+end
diff --git a/spec/ruby/core/symbol/downcase_spec.rb b/spec/ruby/core/symbol/downcase_spec.rb
new file mode 100644
index 0000000000..76418aa9da
--- /dev/null
+++ b/spec/ruby/core/symbol/downcase_spec.rb
@@ -0,0 +1,25 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Symbol#downcase" do
+ it "returns a Symbol" do
+ :glark.downcase.should.instance_of?(Symbol)
+ end
+
+ it "converts uppercase ASCII characters to their lowercase equivalents" do
+ :lOwEr.downcase.should == :lower
+ end
+
+ it "leaves lowercase Unicode characters as they were" do
+ "\u{E0}Bc".to_sym.downcase.should == :"àbc"
+ end
+
+ it "uncapitalizes all Unicode characters" do
+ "ÄÖÜ".to_sym.downcase.should == :"äöü"
+ "AOU".to_sym.downcase.should == :"aou"
+ end
+
+ it "leaves non-alphabetic ASCII characters as they were" do
+ "Glark?!?".to_sym.downcase.should == :"glark?!?"
+ end
+end
diff --git a/spec/ruby/core/symbol/dup_spec.rb b/spec/ruby/core/symbol/dup_spec.rb
new file mode 100644
index 0000000000..eef3078030
--- /dev/null
+++ b/spec/ruby/core/symbol/dup_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#dup" do
+ it "returns self" do
+ :a_symbol.dup.should.equal?(:a_symbol)
+ end
+end
diff --git a/spec/ruby/core/symbol/element_reference_spec.rb b/spec/ruby/core/symbol/element_reference_spec.rb
new file mode 100644
index 0000000000..df6bc15ddb
--- /dev/null
+++ b/spec/ruby/core/symbol/element_reference_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/slice'
+
+describe "Symbol#[]" do
+ it_behaves_like :symbol_slice, :[]
+end
diff --git a/spec/ruby/core/symbol/empty_spec.rb b/spec/ruby/core/symbol/empty_spec.rb
new file mode 100644
index 0000000000..1d90a59a5b
--- /dev/null
+++ b/spec/ruby/core/symbol/empty_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#empty?" do
+ it "returns true if self is empty" do
+ :"".empty?.should == true
+ end
+
+ it "returns false if self is non-empty" do
+ :"a".empty?.should == false
+ end
+end
diff --git a/spec/ruby/core/symbol/encoding_spec.rb b/spec/ruby/core/symbol/encoding_spec.rb
new file mode 100644
index 0000000000..732fd62e26
--- /dev/null
+++ b/spec/ruby/core/symbol/encoding_spec.rb
@@ -0,0 +1,23 @@
+# encoding: utf-8
+
+require_relative '../../spec_helper'
+
+describe "Symbol#encoding for ASCII symbols" do
+ it "is US-ASCII" do
+ :foo.encoding.name.should == "US-ASCII"
+ end
+
+ it "is US-ASCII after converting to string" do
+ :foo.to_s.encoding.name.should == "US-ASCII"
+ end
+end
+
+describe "Symbol#encoding for UTF-8 symbols" do
+ it "is UTF-8" do
+ :åäö.encoding.name.should == "UTF-8"
+ end
+
+ it "is UTF-8 after converting to string" do
+ :åäö.to_s.encoding.name.should == "UTF-8"
+ end
+end
diff --git a/spec/ruby/core/symbol/end_with_spec.rb b/spec/ruby/core/symbol/end_with_spec.rb
new file mode 100644
index 0000000000..4b9f5a4996
--- /dev/null
+++ b/spec/ruby/core/symbol/end_with_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: utf-8 -*-
+
+require_relative '../../spec_helper'
+require_relative '../../shared/string/end_with'
+
+describe "Symbol#end_with?" do
+ it_behaves_like :end_with, :to_sym
+end
diff --git a/spec/ruby/core/symbol/equal_value_spec.rb b/spec/ruby/core/symbol/equal_value_spec.rb
new file mode 100644
index 0000000000..3fe997d02a
--- /dev/null
+++ b/spec/ruby/core/symbol/equal_value_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#==" do
+ it "only returns true when the other is exactly the same symbol" do
+ (:ruby == :ruby).should == true
+ (:ruby == :"ruby").should == true
+ (:ruby == :'ruby').should == true
+ (:@ruby == :@ruby).should == true
+
+ (:ruby == :@ruby).should == false
+ (:foo == :bar).should == false
+ (:ruby == 'ruby').should == false
+ end
+end
diff --git a/spec/ruby/core/symbol/fixtures/classes.rb b/spec/ruby/core/symbol/fixtures/classes.rb
new file mode 100644
index 0000000000..6552f6ee38
--- /dev/null
+++ b/spec/ruby/core/symbol/fixtures/classes.rb
@@ -0,0 +1,3 @@
+module SymbolSpecs
+ class MyRange < Range; end
+end
diff --git a/spec/ruby/core/symbol/id2name_spec.rb b/spec/ruby/core/symbol/id2name_spec.rb
new file mode 100644
index 0000000000..2caa89fc37
--- /dev/null
+++ b/spec/ruby/core/symbol/id2name_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/id2name'
+
+describe "Symbol#id2name" do
+ it_behaves_like :symbol_id2name, :id2name
+end
diff --git a/spec/ruby/core/symbol/inspect_spec.rb b/spec/ruby/core/symbol/inspect_spec.rb
new file mode 100644
index 0000000000..f2269996af
--- /dev/null
+++ b/spec/ruby/core/symbol/inspect_spec.rb
@@ -0,0 +1,131 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#inspect" do
+ symbols = {
+ fred: ":fred",
+ :fred? => ":fred?",
+ :fred! => ":fred!",
+ :BAD! => ":BAD!",
+ :_BAD! => ":_BAD!",
+ :$ruby => ":$ruby",
+ :@ruby => ":@ruby",
+ :@@ruby => ":@@ruby",
+ :"$ruby!" => ":\"$ruby!\"",
+ :"$ruby?" => ":\"$ruby?\"",
+ :"@ruby!" => ":\"@ruby!\"",
+ :"@ruby?" => ":\"@ruby?\"",
+ :"@@ruby!" => ":\"@@ruby!\"",
+ :"@@ruby?" => ":\"@@ruby?\"",
+
+ :$-w => ":$-w",
+ :"$-ww" => ":\"$-ww\"",
+ :"$+" => ":$+",
+ :"$~" => ":$~",
+ :"$:" => ":$:",
+ :"$?" => ":$?",
+ :"$<" => ":$<",
+ :"$_" => ":$_",
+ :"$/" => ":$/",
+ :"$'" => ":$'",
+ :"$\"" => ":$\"",
+ :"$$" => ":$$",
+ :"$." => ":$.",
+ :"$," => ":$,",
+ :"$`" => ":$`",
+ :"$!" => ":$!",
+ :"$;" => ":$;",
+ :"$\\" => ":$\\",
+ :"$=" => ":$=",
+ :"$*" => ":$*",
+ :"$>" => ":$>",
+ :"$&" => ":$&",
+ :"$@" => ":$@",
+ :"$1234" => ":$1234",
+
+ :-@ => ":-@",
+ :+@ => ":+@",
+ :% => ":%",
+ :& => ":&",
+ :* => ":*",
+ :** => ":**",
+ :"/" => ":/", # lhs quoted for emacs happiness
+ :< => ":<",
+ :<= => ":<=",
+ :<=> => ":<=>",
+ :== => ":==",
+ :=== => ":===",
+ :=~ => ":=~",
+ :> => ":>",
+ :>= => ":>=",
+ :>> => ":>>",
+ :[] => ":[]",
+ :[]= => ":[]=",
+ :"\<\<" => ":\<\<",
+ :^ => ":^",
+ :"`" => ":`", # for emacs, and justice!
+ :~ => ":~",
+ :| => ":|",
+
+ :"!" => ":!",
+ :"!=" => ":!=",
+ :"!~" => ":!~",
+ :"\$" => ":\"$\"", # for justice!
+ :"&&" => ":\"&&\"",
+ :"'" => ":\"\'\"",
+ :"," => ":\",\"",
+ :"." => ":\".\"",
+ :".." => ":\"..\"",
+ :"..." => ":\"...\"",
+ :":" => ":\":\"",
+ :"::" => ":\"::\"",
+ :";" => ":\";\"",
+ :"=" => ":\"=\"",
+ :"=>" => ":\"=>\"",
+ :"\?" => ":\"?\"", # rawr!
+ :"@" => ":\"@\"",
+ :"||" => ":\"||\"",
+ :"|||" => ":\"|||\"",
+ :"++" => ":\"++\"",
+
+ :"\"" => ":\"\\\"\"",
+ :"\"\"" => ":\"\\\"\\\"\"",
+
+ :"9" => ":\"9\"",
+ :"foo bar" => ":\"foo bar\"",
+ :"*foo" => ":\"*foo\"",
+ :"foo " => ":\"foo \"",
+ :" foo" => ":\" foo\"",
+ :" " => ":\" \"",
+
+ :"ê" => [":ê", ":\"\\u00EA\""],
+ :"测" => [":测", ":\"\\u6D4B\""],
+ :"🦊" => [":🦊", ":\"\\u{1F98A}\""],
+ }
+
+ expected_by_encoding = Encoding::default_external == Encoding::UTF_8 ? 0 : 1
+ symbols.each do |input, expected|
+ expected = expected[expected_by_encoding] if expected.is_a?(Array)
+ it "returns self as a symbol literal for #{expected}" do
+ input.inspect.should == expected
+ end
+ end
+
+ it "quotes BINARY symbols" do
+ sym = "foo\xA4".b.to_sym
+ sym.inspect.should == ':"foo\xA4"'
+ end
+
+ it "quotes symbols in non-ASCII-compatible encodings" do
+ Encoding.list.reject(&:ascii_compatible?).reject(&:dummy?).each do |encoding|
+ sym = "foo".encode(encoding).to_sym
+ sym.inspect.should == ':"foo"'
+ end
+ end
+
+ it "quotes and escapes symbols in dummy encodings" do
+ Encoding.list.select(&:dummy?).each do |encoding|
+ sym = "abcd".dup.force_encoding(encoding).to_sym
+ sym.inspect.should == ':"\x61\x62\x63\x64"'
+ end
+ end
+end
diff --git a/spec/ruby/core/symbol/intern_spec.rb b/spec/ruby/core/symbol/intern_spec.rb
new file mode 100644
index 0000000000..9d0914c7fb
--- /dev/null
+++ b/spec/ruby/core/symbol/intern_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#intern" do
+ it "returns self" do
+ :foo.intern.should == :foo
+ end
+
+ it "returns a Symbol" do
+ :foo.intern.should.is_a?(Symbol)
+ end
+end
diff --git a/spec/ruby/core/symbol/length_spec.rb b/spec/ruby/core/symbol/length_spec.rb
new file mode 100644
index 0000000000..27bee575ef
--- /dev/null
+++ b/spec/ruby/core/symbol/length_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "Symbol#length" do
+ it_behaves_like :symbol_length, :length
+end
diff --git a/spec/ruby/core/symbol/match_spec.rb b/spec/ruby/core/symbol/match_spec.rb
new file mode 100644
index 0000000000..7b165218c6
--- /dev/null
+++ b/spec/ruby/core/symbol/match_spec.rb
@@ -0,0 +1,77 @@
+require_relative '../../spec_helper'
+
+describe :symbol_match, shared: true do
+ it "returns the index of the beginning of the match" do
+ :abc.send(@method, /b/).should == 1
+ end
+
+ it "returns nil if there is no match" do
+ :a.send(@method, /b/).should == nil
+ end
+
+ it "sets the last match pseudo-variables" do
+ :a.send(@method, /(.)/).should == 0
+ $1.should == "a"
+ end
+end
+
+describe "Symbol#=~" do
+ it_behaves_like :symbol_match, :=~
+end
+
+describe "Symbol#match" do
+ it "returns the MatchData" do
+ result = :abc.match(/b/)
+ result.should.is_a?(MatchData)
+ result[0].should == 'b'
+ end
+
+ it "returns nil if there is no match" do
+ :a.match(/b/).should == nil
+ end
+
+ it "sets the last match pseudo-variables" do
+ :a.match(/(.)/)[0].should == 'a'
+ $1.should == "a"
+ end
+
+ describe "when passed a block" do
+ it "yields the MatchData" do
+ :abc.match(/./) {|m| ScratchPad.record m }
+ ScratchPad.recorded.should.is_a?(MatchData)
+ end
+
+ it "returns the block result" do
+ :abc.match(/./) { :result }.should == :result
+ end
+
+ it "does not yield if there is no match" do
+ ScratchPad.record []
+ :b.match(/a/) {|m| ScratchPad << m }
+ ScratchPad.recorded.should == []
+ end
+ end
+end
+
+describe "Symbol#match?" do
+ before :each do
+ # Resetting Regexp.last_match
+ /DONTMATCH/.match ''
+ end
+
+ context "when matches the given regex" do
+ it "returns true but does not set Regexp.last_match" do
+ :string.match?(/string/i).should == true
+ Regexp.last_match.should == nil
+ end
+ end
+
+ it "returns false when does not match the given regex" do
+ :string.match?(/STRING/).should == false
+ end
+
+ it "takes matching position as the 2nd argument" do
+ :string.match?(/str/i, 0).should == true
+ :string.match?(/str/i, 1).should == false
+ end
+end
diff --git a/spec/ruby/core/symbol/name_spec.rb b/spec/ruby/core/symbol/name_spec.rb
new file mode 100644
index 0000000000..f9b631266c
--- /dev/null
+++ b/spec/ruby/core/symbol/name_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#name" do
+ it "returns string" do
+ :ruby.name.should == "ruby"
+ :ルビー.name.should == "ルビー"
+ end
+
+ it "returns same string instance" do
+ :"ruby_3".name.should.equal?(:ruby_3.name)
+ :"ruby_#{1+2}".name.should.equal?(:ruby_3.name)
+ end
+
+ it "returns frozen string" do
+ :symbol.name.should.frozen?
+ end
+end
diff --git a/spec/ruby/core/symbol/next_spec.rb b/spec/ruby/core/symbol/next_spec.rb
new file mode 100644
index 0000000000..97fe913739
--- /dev/null
+++ b/spec/ruby/core/symbol/next_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/succ'
+
+describe "Symbol#next" do
+ it_behaves_like :symbol_succ, :next
+end
diff --git a/spec/ruby/core/symbol/shared/id2name.rb b/spec/ruby/core/symbol/shared/id2name.rb
new file mode 100644
index 0000000000..00a9c7d7dc
--- /dev/null
+++ b/spec/ruby/core/symbol/shared/id2name.rb
@@ -0,0 +1,30 @@
+describe :symbol_id2name, shared: true do
+ it "returns the string corresponding to self" do
+ :rubinius.send(@method).should == "rubinius"
+ :squash.send(@method).should == "squash"
+ :[].send(@method).should == "[]"
+ :@ruby.send(@method).should == "@ruby"
+ :@@ruby.send(@method).should == "@@ruby"
+ end
+
+ it "returns a String in the same encoding as self" do
+ string = "ruby".encode("US-ASCII")
+ symbol = string.to_sym
+
+ symbol.send(@method).encoding.should == Encoding::US_ASCII
+ end
+
+ ruby_version_is "3.4" do
+ it "warns about mutating returned string" do
+ -> { :bad!.send(@method).upcase! }.should complain(/warning: string returned by :bad!.to_s will be frozen in the future/)
+ end
+
+ it "does not warn about mutation when Warning[:deprecated] is false" do
+ deprecated = Warning[:deprecated]
+ Warning[:deprecated] = false
+ -> { :bad!.send(@method).upcase! }.should_not complain
+ ensure
+ Warning[:deprecated] = deprecated
+ end
+ end
+end
diff --git a/spec/ruby/core/symbol/shared/length.rb b/spec/ruby/core/symbol/shared/length.rb
new file mode 100644
index 0000000000..692e8c57e3
--- /dev/null
+++ b/spec/ruby/core/symbol/shared/length.rb
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+
+describe :symbol_length, shared: true do
+ it "returns 0 for empty name" do
+ :''.send(@method).should == 0
+ end
+
+ it "returns 1 for name formed by a NUL character" do
+ :"\x00".send(@method).should == 1
+ end
+
+ it "returns 3 for name formed by 3 ASCII characters" do
+ :one.send(@method).should == 3
+ end
+
+ it "returns 4 for name formed by 4 ASCII characters" do
+ :four.send(@method).should == 4
+ end
+
+ it "returns 4 for name formed by 1 multibyte and 3 ASCII characters" do
+ :"\xC3\x9Cber".send(@method).should == 4
+ end
+end
diff --git a/spec/ruby/core/symbol/shared/slice.rb b/spec/ruby/core/symbol/shared/slice.rb
new file mode 100644
index 0000000000..4e3a35240c
--- /dev/null
+++ b/spec/ruby/core/symbol/shared/slice.rb
@@ -0,0 +1,262 @@
+require_relative '../fixtures/classes'
+
+describe :symbol_slice, shared: true do
+ describe "with an Integer index" do
+ it "returns the character code of the element at the index" do
+ :symbol.send(@method, 1).should == ?y
+ end
+
+ it "returns nil if the index starts from the end and is greater than the length" do
+ :symbol.send(@method, -10).should == nil
+ end
+
+ it "returns nil if the index is greater than the length" do
+ :symbol.send(@method, 42).should == nil
+ end
+ end
+
+ describe "with an Integer index and length" do
+ describe "and a positive index and length" do
+ it "returns a slice" do
+ :symbol.send(@method, 1,3).should == "ymb"
+ end
+
+ it "returns a blank slice if the length is 0" do
+ :symbol.send(@method, 0,0).should == ""
+ :symbol.send(@method, 1,0).should == ""
+ end
+
+ it "returns a slice of all remaining characters if the given length is greater than the actual length" do
+ :symbol.send(@method, 1,100).should == "ymbol"
+ end
+
+ it "returns nil if the index is greater than the length" do
+ :symbol.send(@method, 10,1).should == nil
+ end
+ end
+
+ describe "and a positive index and negative length" do
+ it "returns nil" do
+ :symbol.send(@method, 0,-1).should == nil
+ :symbol.send(@method, 1,-1).should == nil
+ end
+ end
+
+ describe "and a negative index and positive length" do
+ it "returns a slice starting from the end upto the length" do
+ :symbol.send(@method, -3,2).should == "bo"
+ end
+
+ it "returns a blank slice if the length is 0" do
+ :symbol.send(@method, -1,0).should == ""
+ end
+
+ it "returns a slice of all remaining characters if the given length is larger than the actual length" do
+ :symbol.send(@method, -4,100).should == "mbol"
+ end
+
+ it "returns nil if the index is past the start" do
+ :symbol.send(@method, -10,1).should == nil
+ end
+ end
+
+ describe "and a negative index and negative length" do
+ it "returns nil" do
+ :symbol.send(@method, -1,-1).should == nil
+ end
+ end
+
+ describe "and a Float length" do
+ it "converts the length to an Integer" do
+ :symbol.send(@method, 2,2.5).should == "mb"
+ end
+ end
+
+ describe "and a nil length" do
+ it "raises a TypeError" do
+ -> { :symbol.send(@method, 1,nil) }.should.raise(TypeError)
+ end
+ end
+
+ describe "and a length that cannot be converted into an Integer" do
+ it "raises a TypeError when given an Array" do
+ -> { :symbol.send(@method, 1,Array.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Hash" do
+ -> { :symbol.send(@method, 1,Hash.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Object" do
+ -> { :symbol.send(@method, 1,Object.new) }.should.raise(TypeError)
+ end
+ end
+ end
+
+ describe "with a Float index" do
+ it "converts the index to an Integer" do
+ :symbol.send(@method, 1.5).should == ?y
+ end
+ end
+
+ describe "with a nil index" do
+ it "raises a TypeError" do
+ -> { :symbol.send(@method, nil) }.should.raise(TypeError)
+ end
+ end
+
+ describe "with an index that cannot be converted into an Integer" do
+ it "raises a TypeError when given an Array" do
+ -> { :symbol.send(@method, Array.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Hash" do
+ -> { :symbol.send(@method, Hash.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Object" do
+ -> { :symbol.send(@method, Object.new) }.should.raise(TypeError)
+ end
+ end
+
+ describe "with a Range slice" do
+ describe "that is within bounds" do
+ it "returns a slice if both range values begin at the start and are within bounds" do
+ :symbol.send(@method, 1..4).should == "ymbo"
+ end
+
+ it "returns a slice if the first range value begins at the start and the last begins at the end" do
+ :symbol.send(@method, 1..-1).should == "ymbol"
+ end
+
+ it "returns a slice if the first range value begins at the end and the last begins at the end" do
+ :symbol.send(@method, -4..-1).should == "mbol"
+ end
+ end
+
+ describe "that is out of bounds" do
+ it "returns nil if the first range value begins past the end" do
+ :symbol.send(@method, 10..12).should == nil
+ end
+
+ it "returns a blank string if the first range value is within bounds and the last range value is not" do
+ :symbol.send(@method, -2..-10).should == ""
+ :symbol.send(@method, 2..-10).should == ""
+ end
+
+ it "returns nil if the first range value starts from the end and is within bounds and the last value starts from the end and is greater than the length" do
+ :symbol.send(@method, -10..-12).should == nil
+ end
+
+ it "returns nil if the first range value starts from the end and is out of bounds and the last value starts from the end and is less than the length" do
+ :symbol.send(@method, -10..-2).should == nil
+ end
+ end
+
+ describe "with Float values" do
+ it "converts the first value to an Integer" do
+ :symbol.send(@method, 0.5..2).should == "sym"
+ end
+
+ it "converts the last value to an Integer" do
+ :symbol.send(@method, 0..2.5).should == "sym"
+ end
+ end
+ end
+
+ describe "with a Range subclass slice" do
+ it "returns a slice" do
+ range = SymbolSpecs::MyRange.new(1, 4)
+ :symbol.send(@method, range).should == "ymbo"
+ end
+ end
+
+ describe "with a Regex slice" do
+ describe "without a capture index" do
+ it "returns a string of the match" do
+ :symbol.send(@method, /[^bol]+/).should == "sym"
+ end
+
+ it "returns nil if the expression does not match" do
+ :symbol.send(@method, /0-9/).should == nil
+ end
+
+ it "sets $~ to the MatchData if there is a match" do
+ :symbol.send(@method, /[^bol]+/)
+ $~[0].should == "sym"
+ end
+
+ it "does not set $~ if there if there is not a match" do
+ :symbol.send(@method, /[0-9]+/)
+ $~.should == nil
+ end
+ end
+
+ describe "with a capture index" do
+ it "returns a string of the complete match if the capture index is 0" do
+ :symbol.send(@method, /(sy)(mb)(ol)/, 0).should == "symbol"
+ end
+
+ it "returns a string for the matched capture at the given index" do
+ :symbol.send(@method, /(sy)(mb)(ol)/, 1).should == "sy"
+ :symbol.send(@method, /(sy)(mb)(ol)/, -1).should == "ol"
+ end
+
+ it "returns nil if there is no capture for the index" do
+ :symbol.send(@method, /(sy)(mb)(ol)/, 4).should == nil
+ :symbol.send(@method, /(sy)(mb)(ol)/, -4).should == nil
+ end
+
+ it "converts the index to an Integer" do
+ :symbol.send(@method, /(sy)(mb)(ol)/, 1.5).should == "sy"
+ end
+
+ describe "and an index that cannot be converted to an Integer" do
+ it "raises a TypeError when given an Hash" do
+ -> { :symbol.send(@method, /(sy)(mb)(ol)/, Hash.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Array" do
+ -> { :symbol.send(@method, /(sy)(mb)(ol)/, Array.new) }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError when given an Object" do
+ -> { :symbol.send(@method, /(sy)(mb)(ol)/, Object.new) }.should.raise(TypeError)
+ end
+ end
+
+ it "raises a TypeError if the index is nil" do
+ -> { :symbol.send(@method, /(sy)(mb)(ol)/, nil) }.should.raise(TypeError)
+ end
+
+ it "sets $~ to the MatchData if there is a match" do
+ :symbol.send(@method, /(sy)(mb)(ol)/, 0)
+ $~[0].should == "symbol"
+ $~[1].should == "sy"
+ $~[2].should == "mb"
+ $~[3].should == "ol"
+ end
+
+ it "does not set $~ to the MatchData if there is not a match" do
+ :symbol.send(@method, /0-9/, 0)
+ $~.should == nil
+ end
+ end
+ end
+
+ describe "with a String slice" do
+ it "does not set $~" do
+ $~ = nil
+ :symbol.send(@method, "sym")
+ $~.should == nil
+ end
+
+ it "returns a string if there is match" do
+ :symbol.send(@method, "ymb").should == "ymb"
+ end
+
+ it "returns nil if there is not a match" do
+ :symbol.send(@method, "foo").should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/symbol/shared/succ.rb b/spec/ruby/core/symbol/shared/succ.rb
new file mode 100644
index 0000000000..dde298207e
--- /dev/null
+++ b/spec/ruby/core/symbol/shared/succ.rb
@@ -0,0 +1,18 @@
+require_relative '../../../spec_helper'
+
+describe :symbol_succ, shared: true do
+ it "returns a successor" do
+ :abcd.send(@method).should == :abce
+ :THX1138.send(@method).should == :THX1139
+ end
+
+ it "propagates a 'carry'" do
+ :"1999zzz".send(@method).should == :"2000aaa"
+ :ZZZ9999.send(@method).should == :AAAA0000
+ end
+
+ it "increments non-alphanumeric characters when no alphanumeric characters are present" do
+ :"<<koala>>".send(@method).should == :"<<koalb>>"
+ :"***".send(@method).should == :"**+"
+ end
+end
diff --git a/spec/ruby/core/symbol/size_spec.rb b/spec/ruby/core/symbol/size_spec.rb
new file mode 100644
index 0000000000..5e2aa8d4d2
--- /dev/null
+++ b/spec/ruby/core/symbol/size_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/length'
+
+describe "Symbol#size" do
+ it_behaves_like :symbol_length, :size
+end
diff --git a/spec/ruby/core/symbol/slice_spec.rb b/spec/ruby/core/symbol/slice_spec.rb
new file mode 100644
index 0000000000..d2421c474c
--- /dev/null
+++ b/spec/ruby/core/symbol/slice_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/slice'
+
+describe "Symbol#slice" do
+ it_behaves_like :symbol_slice, :slice
+end
diff --git a/spec/ruby/core/symbol/start_with_spec.rb b/spec/ruby/core/symbol/start_with_spec.rb
new file mode 100644
index 0000000000..cd43279003
--- /dev/null
+++ b/spec/ruby/core/symbol/start_with_spec.rb
@@ -0,0 +1,8 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/string/start_with'
+
+describe "Symbol#start_with?" do
+ it_behaves_like :start_with, :to_sym
+end
diff --git a/spec/ruby/core/symbol/succ_spec.rb b/spec/ruby/core/symbol/succ_spec.rb
new file mode 100644
index 0000000000..694bfff862
--- /dev/null
+++ b/spec/ruby/core/symbol/succ_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/succ'
+
+describe "Symbol#succ" do
+ it_behaves_like :symbol_succ, :succ
+end
diff --git a/spec/ruby/core/symbol/swapcase_spec.rb b/spec/ruby/core/symbol/swapcase_spec.rb
new file mode 100644
index 0000000000..95fc29e32b
--- /dev/null
+++ b/spec/ruby/core/symbol/swapcase_spec.rb
@@ -0,0 +1,29 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Symbol#swapcase" do
+ it "returns a Symbol" do
+ :glark.swapcase.should.instance_of?(Symbol)
+ end
+
+ it "converts lowercase ASCII characters to their uppercase equivalents" do
+ :lower.swapcase.should == :LOWER
+ end
+
+ it "converts uppercase ASCII characters to their lowercase equivalents" do
+ :UPPER.swapcase.should == :upper
+ end
+
+ it "works with both upper- and lowercase ASCII characters in the same Symbol" do
+ :mIxEd.swapcase.should == :MiXeD
+ end
+
+ it "swaps the case for Unicode characters" do
+ "äÖü".to_sym.swapcase.should == :"ÄöÜ"
+ "aOu".to_sym.swapcase.should == :"AoU"
+ end
+
+ it "leaves non-alphabetic ASCII characters as they were" do
+ "Glark?!?".to_sym.swapcase.should == :"gLARK?!?"
+ end
+end
diff --git a/spec/ruby/core/symbol/symbol_spec.rb b/spec/ruby/core/symbol/symbol_spec.rb
new file mode 100644
index 0000000000..3534686a08
--- /dev/null
+++ b/spec/ruby/core/symbol/symbol_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Symbol" do
+ it "includes Comparable" do
+ Symbol.include?(Comparable).should == true
+ end
+
+ it ".allocate raises a TypeError" do
+ -> do
+ Symbol.allocate
+ end.should.raise(TypeError)
+ end
+
+ it ".new is undefined" do
+ -> do
+ Symbol.new
+ end.should.raise(NoMethodError)
+ end
+end
diff --git a/spec/ruby/core/symbol/to_proc_spec.rb b/spec/ruby/core/symbol/to_proc_spec.rb
new file mode 100644
index 0000000000..93ed1e9e9b
--- /dev/null
+++ b/spec/ruby/core/symbol/to_proc_spec.rb
@@ -0,0 +1,78 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#to_proc" do
+ it "returns a new Proc" do
+ proc = :to_s.to_proc
+ proc.should.is_a?(Proc)
+ end
+
+ it "sends self to arguments passed when calling #call on the Proc" do
+ obj = mock("Receiving #to_s")
+ obj.should_receive(:to_s).and_return("Received #to_s")
+ :to_s.to_proc.call(obj).should == "Received #to_s"
+ end
+
+ it "returns a Proc with #lambda? true" do
+ pr = :to_s.to_proc
+ pr.should.lambda?
+ end
+
+ it "produces a Proc with arity -2" do
+ pr = :to_s.to_proc
+ pr.arity.should == -2
+ end
+
+ it "produces a Proc that always returns [[:req], [:rest]] for #parameters" do
+ pr = :to_s.to_proc
+ pr.parameters.should == [[:req], [:rest]]
+ end
+
+ it "only calls public methods" do
+ body = proc do
+ public def pub; @a << :pub end
+ protected def pro; @a << :pro end
+ private def pri; @a << :pri end
+ attr_reader :a
+ end
+
+ @a = []
+ singleton_class.class_eval(&body)
+ tap(&:pub)
+ proc{tap(&:pro)}.should.raise(NoMethodError, /protected method [`']pro' called/)
+ proc{tap(&:pri)}.should.raise(NoMethodError, /private method [`']pri' called/)
+ @a.should == [:pub]
+
+ @a = []
+ c = Class.new(&body)
+ o = c.new
+ o.instance_variable_set(:@a, [])
+ o.tap(&:pub)
+ proc{tap(&:pro)}.should.raise(NoMethodError, /protected method [`']pro' called/)
+ proc{o.tap(&:pri)}.should.raise(NoMethodError, /private method [`']pri' called/)
+ o.a.should == [:pub]
+ end
+
+ it "raises an ArgumentError when calling #call on the Proc without receiver" do
+ -> {
+ :object_id.to_proc.call
+ }.should.raise(ArgumentError, /no receiver given|wrong number of arguments \(given 0, expected 1\+\)/)
+ end
+
+ it "passes along the block passed to Proc#call" do
+ klass = Class.new do
+ def m
+ yield
+ end
+
+ def to_proc
+ :m.to_proc.call(self) { :value }
+ end
+ end
+ klass.new.to_proc.should == :value
+ end
+
+ it "produces a proc with source location nil" do
+ pr = :to_s.to_proc
+ pr.source_location.should == nil
+ end
+end
diff --git a/spec/ruby/core/symbol/to_s_spec.rb b/spec/ruby/core/symbol/to_s_spec.rb
new file mode 100644
index 0000000000..cd963faa28
--- /dev/null
+++ b/spec/ruby/core/symbol/to_s_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/id2name'
+
+describe "Symbol#to_s" do
+ it_behaves_like :symbol_id2name, :to_s
+end
diff --git a/spec/ruby/core/symbol/to_sym_spec.rb b/spec/ruby/core/symbol/to_sym_spec.rb
new file mode 100644
index 0000000000..e75f3d48a8
--- /dev/null
+++ b/spec/ruby/core/symbol/to_sym_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Symbol#to_sym" do
+ it "returns self" do
+ [:rubinius, :squash, :[], :@ruby, :@@ruby].each do |sym|
+ sym.to_sym.should == sym
+ end
+ end
+end
diff --git a/spec/ruby/core/symbol/upcase_spec.rb b/spec/ruby/core/symbol/upcase_spec.rb
new file mode 100644
index 0000000000..3895d95efb
--- /dev/null
+++ b/spec/ruby/core/symbol/upcase_spec.rb
@@ -0,0 +1,21 @@
+# -*- encoding: utf-8 -*-
+require_relative '../../spec_helper'
+
+describe "Symbol#upcase" do
+ it "returns a Symbol" do
+ :glark.upcase.should.instance_of?(Symbol)
+ end
+
+ it "converts lowercase ASCII characters to their uppercase equivalents" do
+ :lOwEr.upcase.should == :LOWER
+ end
+
+ it "capitalizes all Unicode characters" do
+ "äöü".to_sym.upcase.should == :"ÄÖÜ"
+ "aou".to_sym.upcase.should == :"AOU"
+ end
+
+ it "leaves non-alphabetic ASCII characters as they were" do
+ "Glark?!?".to_sym.upcase.should == :"GLARK?!?"
+ end
+end
diff --git a/spec/ruby/core/systemexit/initialize_spec.rb b/spec/ruby/core/systemexit/initialize_spec.rb
new file mode 100644
index 0000000000..2cebaa7993
--- /dev/null
+++ b/spec/ruby/core/systemexit/initialize_spec.rb
@@ -0,0 +1,26 @@
+require_relative '../../spec_helper'
+
+describe "SystemExit#initialize" do
+ it "accepts a status" do
+ s = SystemExit.new 1
+ s.status.should == 1
+ s.message.should == 'SystemExit'
+ end
+
+ it "accepts a message" do
+ s = SystemExit.new 'message'
+ s.status.should == 0
+ s.message.should == 'message'
+ end
+
+ it "accepts a status and message" do
+ s = SystemExit.new 10, 'message'
+ s.status.should == 10
+ s.message.should == 'message'
+ end
+
+ it "sets the status to 0 by default" do
+ s = SystemExit.new
+ s.status.should == 0
+ end
+end
diff --git a/spec/ruby/core/systemexit/success_spec.rb b/spec/ruby/core/systemexit/success_spec.rb
new file mode 100644
index 0000000000..ba2fd22ded
--- /dev/null
+++ b/spec/ruby/core/systemexit/success_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../spec_helper'
+
+describe "SystemExit#success?" do
+ it "returns true when the status is 0" do
+ s = SystemExit.new 0
+ s.should.success?
+ end
+
+ it "returns false when the status is not 0" do
+ s = SystemExit.new 1
+ s.should_not.success?
+ end
+end
diff --git a/spec/ruby/core/thread/abort_on_exception_spec.rb b/spec/ruby/core/thread/abort_on_exception_spec.rb
new file mode 100644
index 0000000000..aeca50e5c1
--- /dev/null
+++ b/spec/ruby/core/thread/abort_on_exception_spec.rb
@@ -0,0 +1,106 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#abort_on_exception" do
+ before do
+ ThreadSpecs.clear_state
+ @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit }
+ end
+
+ after do
+ ThreadSpecs.state = :exit
+ @thread.join
+ end
+
+ it "is false by default" do
+ @thread.abort_on_exception.should == false
+ end
+
+ it "returns true when #abort_on_exception= is passed true" do
+ @thread.abort_on_exception = true
+ @thread.abort_on_exception.should == true
+ end
+end
+
+describe :thread_abort_on_exception, shared: true do
+ before do
+ @thread = Thread.new do
+ Thread.pass until ThreadSpecs.state == :run
+ raise RuntimeError, "Thread#abort_on_exception= specs"
+ end
+ end
+
+ it "causes the main thread to raise the exception raised in the thread" do
+ begin
+ ScratchPad << :before
+
+ @thread.abort_on_exception = true if @object
+ -> do
+ ThreadSpecs.state = :run
+ # Wait for the main thread to be interrupted
+ sleep
+ end.should.raise(RuntimeError, "Thread#abort_on_exception= specs")
+
+ ScratchPad << :after
+ rescue Exception => e
+ ScratchPad << [:rescue, e]
+ end
+
+ ScratchPad.recorded.should == [:before, :after]
+ end
+end
+
+describe "Thread#abort_on_exception=" do
+ describe "when enabled and the thread dies due to an exception" do
+ before do
+ ScratchPad.record []
+ ThreadSpecs.clear_state
+ @stderr, $stderr = $stderr, IOStub.new
+ end
+
+ after do
+ $stderr = @stderr
+ end
+
+ it_behaves_like :thread_abort_on_exception, nil, true
+ end
+end
+
+describe "Thread.abort_on_exception" do
+ before do
+ @abort_on_exception = Thread.abort_on_exception
+ end
+
+ after do
+ Thread.abort_on_exception = @abort_on_exception
+ end
+
+ it "is false by default" do
+ Thread.abort_on_exception.should == false
+ end
+
+ it "returns true when .abort_on_exception= is passed true" do
+ Thread.abort_on_exception = true
+ Thread.abort_on_exception.should == true
+ end
+end
+
+describe "Thread.abort_on_exception=" do
+ describe "when enabled and a non-main thread dies due to an exception" do
+ before :each do
+ ScratchPad.record []
+ ThreadSpecs.clear_state
+ @stderr, $stderr = $stderr, IOStub.new
+
+ @abort_on_exception = Thread.abort_on_exception
+ Thread.abort_on_exception = true
+ end
+
+ after :each do
+ Thread.abort_on_exception = @abort_on_exception
+ $stderr = @stderr
+ end
+
+ it_behaves_like :thread_abort_on_exception, nil, false
+ end
+end
diff --git a/spec/ruby/core/thread/add_trace_func_spec.rb b/spec/ruby/core/thread/add_trace_func_spec.rb
new file mode 100644
index 0000000000..0abae81a78
--- /dev/null
+++ b/spec/ruby/core/thread/add_trace_func_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Thread#add_trace_func" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/thread/alive_spec.rb b/spec/ruby/core/thread/alive_spec.rb
new file mode 100644
index 0000000000..c2f5f5371d
--- /dev/null
+++ b/spec/ruby/core/thread/alive_spec.rb
@@ -0,0 +1,58 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#alive?" do
+ it "can check it's own status" do
+ ThreadSpecs.status_of_current_thread.should.alive?
+ end
+
+ it "describes a running thread" do
+ ThreadSpecs.status_of_running_thread.should.alive?
+ end
+
+ it "describes a sleeping thread" do
+ ThreadSpecs.status_of_sleeping_thread.should.alive?
+ end
+
+ it "describes a blocked thread" do
+ ThreadSpecs.status_of_blocked_thread.should.alive?
+ end
+
+ it "describes a completed thread" do
+ ThreadSpecs.status_of_completed_thread.should_not.alive?
+ end
+
+ it "describes a killed thread" do
+ ThreadSpecs.status_of_killed_thread.should_not.alive?
+ end
+
+ it "describes a thread with an uncaught exception" do
+ ThreadSpecs.status_of_thread_with_uncaught_exception.should_not.alive?
+ end
+
+ it "describes a dying running thread" do
+ ThreadSpecs.status_of_dying_running_thread.should.alive?
+ end
+
+ it "describes a dying sleeping thread" do
+ ThreadSpecs.status_of_dying_sleeping_thread.should.alive?
+ end
+
+ it "returns true for a killed but still running thread" do
+ exit = false
+ t = Thread.new do
+ begin
+ sleep
+ ensure
+ Thread.pass until exit
+ end
+ end
+
+ ThreadSpecs.spin_until_sleeping(t)
+
+ t.kill
+ t.should.alive?
+ exit = true
+ t.join
+ end
+end
diff --git a/spec/ruby/core/thread/allocate_spec.rb b/spec/ruby/core/thread/allocate_spec.rb
new file mode 100644
index 0000000000..0b4e4f1b1f
--- /dev/null
+++ b/spec/ruby/core/thread/allocate_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+
+describe "Thread.allocate" do
+ it "raises a TypeError" do
+ -> {
+ Thread.allocate
+ }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/limit_spec.rb b/spec/ruby/core/thread/backtrace/limit_spec.rb
new file mode 100644
index 0000000000..b55ca67ea0
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/limit_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../../spec_helper'
+
+describe "Thread::Backtrace.limit" do
+ it "returns maximum backtrace length set by --backtrace-limit command-line option" do
+ out = ruby_exe("print Thread::Backtrace.limit", options: "--backtrace-limit=2")
+ out.should == "2"
+ end
+
+ it "returns -1 when --backtrace-limit command-line option is not set" do
+ out = ruby_exe("print Thread::Backtrace.limit")
+ out.should == "-1"
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb
new file mode 100644
index 0000000000..6d9482f2ae
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/absolute_path_spec.rb
@@ -0,0 +1,93 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#absolute_path' do
+ before :each do
+ @frame = ThreadBacktraceLocationSpecs.locations[0]
+ end
+
+ it 'returns the absolute path of the call frame' do
+ @frame.absolute_path.should == File.realpath(__FILE__)
+ end
+
+ it 'returns an absolute path when using a relative main script path' do
+ script = fixture(__FILE__, 'absolute_path_main.rb')
+ Dir.chdir(File.dirname(script)) do
+ ruby_exe('absolute_path_main.rb').should == "absolute_path_main.rb\n#{script}\n"
+ end
+ end
+
+ it 'returns the correct absolute path when using a relative main script path and changing CWD' do
+ script = fixture(__FILE__, 'subdir/absolute_path_main_chdir.rb')
+ sibling = fixture(__FILE__, 'subdir/sibling.rb')
+ subdir = File.dirname script
+ Dir.chdir(fixture(__FILE__)) do
+ ruby_exe('subdir/absolute_path_main_chdir.rb').should == "subdir/absolute_path_main_chdir.rb\n#{subdir}\n#{subdir}\n#{script}\n#{sibling}\n"
+ end
+ end
+
+ context "when used in eval with a given filename" do
+ it "returns nil with absolute_path" do
+ code = "caller_locations(0)[0].absolute_path"
+
+ eval(code, nil, "foo.rb").should == nil
+ eval(code, nil, "foo/bar.rb").should == nil
+ end
+ end
+
+ context "when used in #method_added" do
+ it "returns the user filename that defined the method" do
+ path = fixture(__FILE__, "absolute_path_method_added.rb")
+ load path
+ locations = ScratchPad.recorded
+ locations[0].absolute_path.should == path
+ # Make sure it's from the class body, not from the file top-level
+ locations[0].label.should.include? 'MethodAddedAbsolutePath'
+ end
+ end
+
+ context "when used in a core method" do
+ it "returns nil" do
+ location = nil
+ tap { location = caller_locations(1, 1)[0] }
+ location.label.should =~ /\A(?:Kernel#)?tap\z/
+ if location.path.start_with?("<internal:")
+ location.absolute_path.should == nil
+ else
+ location.absolute_path.should == File.realpath(__FILE__)
+ end
+ end
+ end
+
+ context "canonicalization" do
+ platform_is_not :windows do
+ before :each do
+ @file = fixture(__FILE__, "absolute_path.rb")
+ @symlink = tmp("symlink.rb")
+ File.symlink(@file, @symlink)
+ ScratchPad.record []
+ end
+
+ after :each do
+ rm_r @symlink
+ end
+
+ it "returns a canonical path without symlinks, even when __FILE__ does not" do
+ realpath = File.realpath(@symlink)
+ realpath.should_not == @symlink
+
+ load @symlink
+ ScratchPad.recorded.should == [@symlink, realpath]
+ end
+
+ it "returns a canonical path without symlinks, even when __FILE__ is removed" do
+ realpath = File.realpath(@symlink)
+ realpath.should_not == @symlink
+
+ ScratchPad << -> { rm_r(@symlink) }
+ load @symlink
+ ScratchPad.recorded.should == [@symlink, realpath]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/base_label_spec.rb b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb
new file mode 100644
index 0000000000..739f62f42f
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/base_label_spec.rb
@@ -0,0 +1,49 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#base_label' do
+ before :each do
+ @frame = ThreadBacktraceLocationSpecs.locations[0]
+ end
+
+ it 'returns the base label of the call frame' do
+ @frame.base_label.should == '<top (required)>'
+ end
+
+ describe 'when call frame is inside a block' do
+ before :each do
+ @frame = ThreadBacktraceLocationSpecs.block_location[0]
+ end
+
+ it 'returns the name of the method that contains the block' do
+ @frame.base_label.should == 'block_location'
+ end
+ end
+
+ it "is <module:A> for a module body" do
+ module ThreadBacktraceLocationSpecs
+ module ModuleLabel
+ ScratchPad.record caller_locations(0, 1)[0].base_label
+ end
+ end
+ ScratchPad.recorded.should == '<module:ModuleLabel>'
+ end
+
+ it "is <class:A> for a class body" do
+ module ThreadBacktraceLocationSpecs
+ class ClassLabel
+ ScratchPad.record caller_locations(0, 1)[0].base_label
+ end
+ end
+ ScratchPad.recorded.should == '<class:ClassLabel>'
+ end
+
+ it "is 'singleton class' for a singleton class body" do
+ module ThreadBacktraceLocationSpecs
+ class << Object.new
+ ScratchPad.record caller_locations(0, 1)[0].base_label
+ end
+ end
+ ScratchPad.recorded.should =~ /\A(singleton class|<singleton class>)\z/
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb
new file mode 100644
index 0000000000..875e97ffac
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path.rb
@@ -0,0 +1,4 @@
+action = ScratchPad.recorded.pop
+ScratchPad << __FILE__
+action.call if action
+ScratchPad << caller_locations(0)[0].absolute_path
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb
new file mode 100644
index 0000000000..d2b23393d4
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_main.rb
@@ -0,0 +1,2 @@
+puts __FILE__
+puts caller_locations(0)[0].absolute_path
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb
new file mode 100644
index 0000000000..26d6298a19
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/absolute_path_method_added.rb
@@ -0,0 +1,10 @@
+module ThreadBacktraceLocationSpecs
+ class MethodAddedAbsolutePath
+ def self.method_added(name)
+ ScratchPad.record caller_locations
+ end
+
+ def foo
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb
new file mode 100644
index 0000000000..103c36b3a0
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/classes.rb
@@ -0,0 +1,139 @@
+# These are top-level def on purpose to test those cases
+
+def label_top_method = ThreadBacktraceLocationSpecs::LABEL.call
+
+def self.label_sdef_method_of_main = ThreadBacktraceLocationSpecs::LABEL.call
+
+class << self
+ def label_sclass_method_of_main = ThreadBacktraceLocationSpecs::LABEL.call
+end
+
+module ThreadBacktraceLocationSpecs
+ MODULE_LOCATION = caller_locations(0) rescue nil
+ INSTANCE = Object.new.extend(self)
+ LABEL = -> { caller_locations(1, 1)[0].label }
+
+ def self.locations
+ caller_locations
+ end
+
+ def instance_method_location
+ caller_locations(0)
+ end
+
+ def self.method_location
+ caller_locations(0)
+ end
+
+ def self.block_location
+ 1.times do
+ return caller_locations(0)
+ end
+ end
+
+ def instance_block_location
+ 1.times do
+ return caller_locations(0)
+ end
+ end
+
+ def self.locations_inside_nested_blocks
+ first_level_location = nil
+ second_level_location = nil
+ third_level_location = nil
+
+ 1.times do
+ first_level_location = locations[0]
+ 1.times do
+ second_level_location = locations[0]
+ 1.times do
+ third_level_location = locations[0]
+ end
+ end
+ end
+
+ [first_level_location, second_level_location, third_level_location]
+ end
+
+ def instance_locations_inside_nested_block
+ loc = nil
+ 1.times do
+ 1.times do
+ loc = caller_locations(0)
+ end
+ end
+ loc
+ end
+
+ def original_method = LABEL.call
+ alias_method :aliased_method, :original_method
+
+ module M
+ class C
+ def regular_instance_method = LABEL.call
+
+ def self.sdef_class_method = LABEL.call
+
+ class << self
+ def sclass_method = LABEL.call
+
+ def block_in_sclass_method
+ -> {
+ -> { LABEL.call }.call
+ }.call
+ end
+ end
+ block_in_sclass_method
+ end
+ end
+
+ class M::D
+ def scoped_method = LABEL.call
+
+ def self.sdef_scoped_method = LABEL.call
+
+ class << self
+ def sclass_scoped_method = LABEL.call
+ end
+
+ module ::ThreadBacktraceLocationSpecs
+ def top = LABEL.call
+ end
+
+ class ::ThreadBacktraceLocationSpecs::Nested
+ def top_nested = LABEL.call
+
+ class C
+ def top_nested_c = LABEL.call
+ end
+ end
+ end
+
+ SOME_OBJECT = Object.new
+ SOME_OBJECT.instance_exec do
+ def unknown_def_singleton_method = LABEL.call
+
+ def self.unknown_sdef_singleton_method = LABEL.call
+ end
+
+ M.module_eval do
+ def module_eval_method = LABEL.call
+
+ def self.sdef_module_eval_method = LABEL.call
+ end
+
+ def ThreadBacktraceLocationSpecs.string_class_method = LABEL.call
+
+ module M
+ def ThreadBacktraceLocationSpecs.nested_class_method = LABEL.call
+ end
+
+ module M
+ module_function def mod_function = LABEL.call
+ end
+
+ expr = self
+ def expr.sdef_expression = LABEL.call
+
+ def expr.block_in_sdef_expression = -> { LABEL.call }.call
+end
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb
new file mode 100644
index 0000000000..b124c8161c
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_main.rb
@@ -0,0 +1,5 @@
+1.times do
+ puts Thread.current.backtrace_locations(1..1)[0].label
+end
+
+require_relative 'locations_in_required'
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb
new file mode 100644
index 0000000000..5f5ed89e98
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/locations_in_required.rb
@@ -0,0 +1,3 @@
+1.times do
+ puts Thread.current.backtrace_locations(1..1)[0].label
+end
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/main.rb b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb
new file mode 100644
index 0000000000..bde208a059
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/main.rb
@@ -0,0 +1,5 @@
+def backtrace_location_example
+ caller_locations[0].path
+end
+
+print backtrace_location_example
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/path.rb b/spec/ruby/core/thread/backtrace/location/fixtures/path.rb
new file mode 100644
index 0000000000..fba34cb0bc
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/path.rb
@@ -0,0 +1,2 @@
+ScratchPad << __FILE__
+ScratchPad << caller_locations(0)[0].path
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb
new file mode 100644
index 0000000000..33c8fb36ef
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/absolute_path_main_chdir.rb
@@ -0,0 +1,11 @@
+puts __FILE__
+puts __dir__
+Dir.chdir __dir__
+
+# Check __dir__ is still correct after chdir
+puts __dir__
+
+puts caller_locations(0)[0].absolute_path
+
+# require_relative also needs to know the absolute path of the current file so we test it here too
+require_relative 'sibling'
diff --git a/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb
new file mode 100644
index 0000000000..2a854ddccd
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/fixtures/subdir/sibling.rb
@@ -0,0 +1 @@
+puts __FILE__
diff --git a/spec/ruby/core/thread/backtrace/location/inspect_spec.rb b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb
new file mode 100644
index 0000000000..4df88a2f33
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/inspect_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#inspect' do
+ before :each do
+ @frame = ThreadBacktraceLocationSpecs.locations[0]
+ @line = __LINE__ - 1
+ end
+
+ it 'converts the call frame to a String' do
+ @frame.inspect.should.include?("#{__FILE__}:#{@line}:in ")
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/label_spec.rb b/spec/ruby/core/thread/backtrace/location/label_spec.rb
new file mode 100644
index 0000000000..5f6a7b73df
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/label_spec.rb
@@ -0,0 +1,227 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#label' do
+ it 'returns the base label of the call frame' do
+ ThreadBacktraceLocationSpecs.locations[0].label.should.include?('<top (required)>')
+ end
+
+ it 'returns the method name for a method location' do
+ ThreadBacktraceLocationSpecs.method_location[0].label.should =~ /\A(?:ThreadBacktraceLocationSpecs\.)?method_location\z/
+ end
+
+ it 'returns the block name for a block location' do
+ ThreadBacktraceLocationSpecs.block_location[0].label.should =~ /\Ablock in (?:ThreadBacktraceLocationSpecs\.)?block_location\z/
+ end
+
+ it 'returns the module name for a module location' do
+ ThreadBacktraceLocationSpecs::MODULE_LOCATION[0].label.should == "<module:ThreadBacktraceLocationSpecs>"
+ end
+
+ it 'includes the nesting level of a block as part of the location label' do
+ first_level_location, second_level_location, third_level_location =
+ ThreadBacktraceLocationSpecs.locations_inside_nested_blocks
+
+ first_level_location.label.should =~ /\Ablock in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/
+ second_level_location.label.should =~ /\Ablock \(2 levels\) in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/
+ third_level_location.label.should =~ /\Ablock \(3 levels\) in (?:ThreadBacktraceLocationSpecs\.)?locations_inside_nested_blocks\z/
+ end
+
+ it 'sets the location label for a top-level block differently depending on it being in the main file or a required file' do
+ path = fixture(__FILE__, "locations_in_main.rb")
+ main_label, required_label = ruby_exe(path).lines
+
+ main_label.should == "block in <main>\n"
+ required_label.should == "block in <top (required)>\n"
+ end
+
+ it "return the same name as the caller for eval" do
+ this = caller_locations(0)[0].label
+ eval("caller_locations(0)[0]").label.should == this
+
+ b = binding
+ b.eval("caller_locations(0)[0]").label.should == this
+
+ b.local_variable_set(:binding_var1, 1)
+ b.eval("caller_locations(0)[0]").label.should == this
+
+ b.local_variable_set(:binding_var2, 2)
+ b.eval("caller_locations(0)[0]").label.should == this
+
+ b.local_variable_set(:binding_var2, 2)
+ eval("caller_locations(0)[0]", b).label.should == this
+ end
+
+ ruby_version_is "3.4" do
+ describe "is Module#method for" do
+ it "a core method defined natively" do
+ BasicObject.instance_method(:instance_exec).should_not.source_location
+ loc = nil
+ loc = instance_exec { caller_locations(1, 1)[0] }
+ loc.label.should == "BasicObject#instance_exec"
+ end
+
+ it "a core method defined in Ruby" do
+ Kernel.instance_method(:tap).should.source_location
+ loc = nil
+ tap { loc = caller_locations(1, 1)[0] }
+ loc.label.should == "Kernel#tap"
+ end
+
+ it "an instance method defined in Ruby" do
+ ThreadBacktraceLocationSpecs::INSTANCE.instance_method_location[0].label.should == "ThreadBacktraceLocationSpecs#instance_method_location"
+ end
+
+ it "a block in an instance method defined in Ruby" do
+ ThreadBacktraceLocationSpecs::INSTANCE.instance_block_location[0].label.should == "block in ThreadBacktraceLocationSpecs#instance_block_location"
+ end
+
+ it "a nested block in an instance method defined in Ruby" do
+ ThreadBacktraceLocationSpecs::INSTANCE.instance_locations_inside_nested_block[0].label.should == "block (2 levels) in ThreadBacktraceLocationSpecs#instance_locations_inside_nested_block"
+ end
+
+ it "a method defined via module_exec" do
+ ThreadBacktraceLocationSpecs.module_exec do
+ def in_module_exec
+ caller_locations(0)
+ end
+ end
+ ThreadBacktraceLocationSpecs::INSTANCE.in_module_exec[0].label.should == "ThreadBacktraceLocationSpecs#in_module_exec"
+ end
+
+ it "a method defined via module_eval" do
+ ThreadBacktraceLocationSpecs.module_eval <<~RUBY
+ def in_module_eval
+ caller_locations(0)
+ end
+ RUBY
+ ThreadBacktraceLocationSpecs::INSTANCE.in_module_eval[0].label.should == "ThreadBacktraceLocationSpecs#in_module_eval"
+ end
+ end
+
+ describe "is Module.method for" do
+ it "a singleton method defined in Ruby" do
+ ThreadBacktraceLocationSpecs.method_location[0].label.should == "ThreadBacktraceLocationSpecs.method_location"
+ end
+
+ it "a block in a singleton method defined in Ruby" do
+ ThreadBacktraceLocationSpecs.block_location[0].label.should == "block in ThreadBacktraceLocationSpecs.block_location"
+ end
+
+ it "a nested block in a singleton method defined in Ruby" do
+ ThreadBacktraceLocationSpecs.locations_inside_nested_blocks[2].label.should == "block (3 levels) in ThreadBacktraceLocationSpecs.locations_inside_nested_blocks"
+ end
+
+ it "a singleton method defined via def Const.method" do
+ def ThreadBacktraceLocationSpecs.def_singleton
+ caller_locations(0)
+ end
+ ThreadBacktraceLocationSpecs.def_singleton[0].label.should == "ThreadBacktraceLocationSpecs.def_singleton"
+ end
+ end
+
+ it "shows the original method name for an aliased method" do
+ ThreadBacktraceLocationSpecs::INSTANCE.aliased_method.should == "ThreadBacktraceLocationSpecs#original_method"
+ end
+
+ # A wide variety of cases.
+ # These show interesting cases when trying to determine the name statically/at parse time
+ describe "is correct for" do
+ base = ThreadBacktraceLocationSpecs
+
+ it "M::C#regular_instance_method" do
+ base::M::C.new.regular_instance_method.should == "#{base}::M::C#regular_instance_method"
+ end
+
+ it "M::C.sdef_class_method" do
+ base::M::C.sdef_class_method.should == "#{base}::M::C.sdef_class_method"
+ end
+
+ it "M::C.sclass_method" do
+ base::M::C.sclass_method.should == "#{base}::M::C.sclass_method"
+ end
+
+ it "M::C.block_in_sclass_method" do
+ base::M::C.block_in_sclass_method.should == "block (2 levels) in #{base}::M::C.block_in_sclass_method"
+ end
+
+ it "M::D#scoped_method" do
+ base::M::D.new.scoped_method.should == "#{base}::M::D#scoped_method"
+ end
+
+ it "M::D.sdef_scoped_method" do
+ base::M::D.sdef_scoped_method.should == "#{base}::M::D.sdef_scoped_method"
+ end
+
+ it "M::D.sclass_scoped_method" do
+ base::M::D.sclass_scoped_method.should == "#{base}::M::D.sclass_scoped_method"
+ end
+
+ it "ThreadBacktraceLocationSpecs#top" do
+ ThreadBacktraceLocationSpecs::INSTANCE.top.should == "ThreadBacktraceLocationSpecs#top"
+ end
+
+ it "ThreadBacktraceLocationSpecs::Nested#top_nested" do
+ ThreadBacktraceLocationSpecs::Nested.new.top_nested.should == "ThreadBacktraceLocationSpecs::Nested#top_nested"
+ end
+
+ it "ThreadBacktraceLocationSpecs::Nested::C#top_nested_c" do
+ ThreadBacktraceLocationSpecs::Nested::C.new.top_nested_c.should == "ThreadBacktraceLocationSpecs::Nested::C#top_nested_c"
+ end
+
+ it "Object#label_top_method" do
+ label_top_method.should == "Object#label_top_method"
+ end
+
+ it "main.label_sdef_method_of_main" do
+ main = TOPLEVEL_BINDING.receiver
+ main.label_sdef_method_of_main.should == "label_sdef_method_of_main"
+ end
+
+ it "main.label_sclass_method_of_main" do
+ main = TOPLEVEL_BINDING.receiver
+ main.label_sclass_method_of_main.should == "label_sclass_method_of_main"
+ end
+
+ it "unknown_def_singleton_method" do
+ base::SOME_OBJECT.unknown_def_singleton_method.should == "unknown_def_singleton_method"
+ end
+
+ it "unknown_sdef_singleton_method" do
+ base::SOME_OBJECT.unknown_sdef_singleton_method.should == "unknown_sdef_singleton_method"
+ end
+
+ it "M#module_eval_method" do
+ Object.new.extend(base::M).module_eval_method.should == "#{base}::M#module_eval_method"
+ end
+
+ it "M.sdef_module_eval_method" do
+ base::M.sdef_module_eval_method.should == "#{base}::M.sdef_module_eval_method"
+ end
+
+ it "ThreadBacktraceLocationSpecs.string_class_method" do
+ ThreadBacktraceLocationSpecs.string_class_method.should == "ThreadBacktraceLocationSpecs.string_class_method"
+ end
+
+ it "ThreadBacktraceLocationSpecs.nested_class_method" do
+ ThreadBacktraceLocationSpecs.nested_class_method.should == "ThreadBacktraceLocationSpecs.nested_class_method"
+ end
+
+ it "M#mod_function" do
+ Object.new.extend(base::M).send(:mod_function).should == "#{base}::M#mod_function"
+ end
+
+ it "M.mod_function" do
+ base::M.mod_function.should == "#{base}::M.mod_function"
+ end
+
+ it "sdef_expression" do
+ base.sdef_expression.should == "#{base}.sdef_expression"
+ end
+
+ it "block_in_sdef_expression" do
+ base.block_in_sdef_expression.should == "block in #{base}.block_in_sdef_expression"
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/lineno_spec.rb b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb
new file mode 100644
index 0000000000..10457f80f0
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/lineno_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#lineno' do
+ before :each do
+ @frame = ThreadBacktraceLocationSpecs.locations[0]
+ @line = __LINE__ - 1
+ end
+
+ it 'returns the line number of the call frame' do
+ @frame.lineno.should == @line
+ end
+
+ it 'should be the same line number as in #to_s, including for core methods' do
+ # Get the caller_locations from a call made into a core library method
+ locations = [:non_empty].map { caller_locations }[0]
+
+ locations.each do |location|
+ line_number = location.to_s[/:(\d+):/, 1]
+ location.lineno.should == Integer(line_number)
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/path_spec.rb b/spec/ruby/core/thread/backtrace/location/path_spec.rb
new file mode 100644
index 0000000000..75f76833a9
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/path_spec.rb
@@ -0,0 +1,124 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#path' do
+ context 'outside a main script' do
+ it 'returns an absolute path' do
+ frame = ThreadBacktraceLocationSpecs.locations[0]
+
+ frame.path.should == __FILE__
+ end
+ end
+
+ context 'in a main script' do
+ before do
+ @script = fixture(__FILE__, 'main.rb')
+ end
+
+ context 'when the script is in the working directory' do
+ before do
+ @directory = File.dirname(@script)
+ end
+
+ context 'when using a relative script path' do
+ it 'returns a path relative to the working directory' do
+ Dir.chdir(@directory) {
+ ruby_exe('main.rb')
+ }.should == 'main.rb'
+ end
+ end
+
+ context 'when using an absolute script path' do
+ it 'returns an absolute path' do
+ Dir.chdir(@directory) {
+ ruby_exe(@script)
+ }.should == @script
+ end
+ end
+ end
+
+ context 'when the script is in a sub directory of the working directory' do
+ context 'when using a relative script path' do
+ it 'returns a path relative to the working directory' do
+ path = 'fixtures/main.rb'
+ directory = __dir__
+ Dir.chdir(directory) {
+ ruby_exe(path)
+ }.should == path
+ end
+ end
+
+ context 'when using an absolute script path' do
+ it 'returns an absolute path' do
+ ruby_exe(@script).should == @script
+ end
+ end
+ end
+
+ context 'when the script is outside of the working directory' do
+ before :each do
+ @parent_dir = tmp('path_outside_pwd')
+ @sub_dir = File.join(@parent_dir, 'sub')
+ @script = File.join(@parent_dir, 'main.rb')
+ source = fixture(__FILE__, 'main.rb')
+
+ mkdir_p(@sub_dir)
+
+ cp(source, @script)
+ end
+
+ after :each do
+ rm_r(@parent_dir)
+ end
+
+ context 'when using a relative script path' do
+ it 'returns a path relative to the working directory' do
+ Dir.chdir(@sub_dir) {
+ ruby_exe('../main.rb')
+ }.should == '../main.rb'
+ end
+ end
+
+ context 'when using an absolute path' do
+ it 'returns an absolute path' do
+ ruby_exe(@script).should == @script
+ end
+ end
+ end
+ end
+
+ it 'should be the same path as in #to_s, including for core methods' do
+ # Get the caller_locations from a call made into a core library method
+ locations = [:non_empty].map { caller_locations }[0]
+
+ locations.each do |location|
+ filename = location.to_s[/^(.+):\d+:/, 1]
+ path = location.path
+
+ path.should == filename
+ end
+ end
+
+ context "canonicalization" do
+ platform_is_not :windows do
+ before :each do
+ @file = fixture(__FILE__, "path.rb")
+ @symlink = tmp("symlink.rb")
+ File.symlink(@file, @symlink)
+ ScratchPad.record []
+ end
+
+ after :each do
+ rm_r @symlink
+ end
+
+ it "returns a non-canonical path with symlinks, the same as __FILE__" do
+ realpath = File.realpath(@symlink)
+ realpath.should_not == @symlink
+
+ load @symlink
+ ScratchPad.recorded.should == [@symlink, @symlink]
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace/location/to_s_spec.rb b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb
new file mode 100644
index 0000000000..983ce4c3f8
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace/location/to_s_spec.rb
@@ -0,0 +1,13 @@
+require_relative '../../../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe 'Thread::Backtrace::Location#to_s' do
+ before :each do
+ @frame = ThreadBacktraceLocationSpecs.locations[0]
+ @line = __LINE__ - 1
+ end
+
+ it 'converts the call frame to a String' do
+ @frame.to_s.should.include?("#{__FILE__}:#{@line}:in ")
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace_locations_spec.rb b/spec/ruby/core/thread/backtrace_locations_spec.rb
new file mode 100644
index 0000000000..28a488f311
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace_locations_spec.rb
@@ -0,0 +1,79 @@
+require_relative '../../spec_helper'
+
+describe "Thread#backtrace_locations" do
+ it "returns an Array" do
+ locations = Thread.current.backtrace_locations
+ locations.should.instance_of?(Array)
+ locations.should_not.empty?
+ end
+
+ it "sets each element to a Thread::Backtrace::Location" do
+ locations = Thread.current.backtrace_locations
+ locations.each { |loc| loc.should.instance_of?(Thread::Backtrace::Location) }
+ end
+
+ it "can be called on any Thread" do
+ locations = Thread.new { Thread.current.backtrace_locations }.value
+ locations.should.instance_of?(Array)
+ locations.should_not.empty?
+ locations.each { |loc| loc.should.instance_of?(Thread::Backtrace::Location) }
+ end
+
+ it "can be called with a number of locations to omit" do
+ locations1 = Thread.current.backtrace_locations
+ locations2 = Thread.current.backtrace_locations(2)
+ locations2.length.should == locations1[2..-1].length
+ locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s)
+ end
+
+ it "can be called with a maximum number of locations to return as second parameter" do
+ locations1 = Thread.current.backtrace_locations
+ locations2 = Thread.current.backtrace_locations(2, 3)
+ locations2.map(&:to_s).should == locations1[2..4].map(&:to_s)
+ end
+
+ it "can be called with a range" do
+ locations1 = Thread.current.backtrace_locations
+ locations2 = Thread.current.backtrace_locations(2..4)
+ locations2.map(&:to_s).should == locations1[2..4].map(&:to_s)
+ end
+
+ it "can be called with a range whose end is negative" do
+ Thread.current.backtrace_locations(2..-1).map(&:to_s).should == Thread.current.backtrace_locations[2..-1].map(&:to_s)
+ Thread.current.backtrace_locations(2..-2).map(&:to_s).should == Thread.current.backtrace_locations[2..-2].map(&:to_s)
+ end
+
+ it "can be called with an endless range" do
+ locations1 = Thread.current.backtrace_locations(0)
+ locations2 = Thread.current.backtrace_locations(eval("(2..)"))
+ locations2.map(&:to_s).should == locations1[2..-1].map(&:to_s)
+ end
+
+ it "can be called with an beginless range" do
+ locations1 = Thread.current.backtrace_locations(0)
+ locations2 = Thread.current.backtrace_locations((..5))
+ locations2.map(&:to_s)[eval("(2..)")].should == locations1[(..5)].map(&:to_s)[eval("(2..)")]
+ end
+
+ it "returns nil if omitting more locations than available" do
+ Thread.current.backtrace_locations(100).should == nil
+ Thread.current.backtrace_locations(100..-1).should == nil
+ end
+
+ it "returns [] if omitting exactly the number of locations available" do
+ omit = Thread.current.backtrace_locations.length
+ Thread.current.backtrace_locations(omit).should == []
+ end
+
+ it "without argument is the same as showing all locations with 0..-1" do
+ Thread.current.backtrace_locations.map(&:to_s).should == Thread.current.backtrace_locations(0..-1).map(&:to_s)
+ end
+
+ it "the first location reports the call to #backtrace_locations" do
+ Thread.current.backtrace_locations(0..0)[0].to_s.should =~ /\A#{__FILE__ }:#{__LINE__ }:in [`'](?:Thread#)?backtrace_locations'\z/
+ end
+
+ it "[1..-1] is the same as #caller_locations(0..-1) for Thread.current" do
+ Thread.current.backtrace_locations(1..-1).map(&:to_s).should == caller_locations(0..-1).map(&:to_s)
+ end
+end
diff --git a/spec/ruby/core/thread/backtrace_spec.rb b/spec/ruby/core/thread/backtrace_spec.rb
new file mode 100644
index 0000000000..770c300f06
--- /dev/null
+++ b/spec/ruby/core/thread/backtrace_spec.rb
@@ -0,0 +1,69 @@
+require_relative '../../spec_helper'
+
+describe "Thread#backtrace" do
+ it "returns the current backtrace of a thread" do
+ t = Thread.new do
+ begin
+ sleep
+ rescue
+ end
+ end
+
+ Thread.pass while t.status && t.status != 'sleep'
+
+ backtrace = t.backtrace
+ backtrace.should.is_a?(Array)
+ backtrace.first.should =~ /[`'](?:Kernel#)?sleep'/
+
+ t.raise 'finish the thread'
+ t.join
+ end
+
+ it "returns nil for dead thread" do
+ t = Thread.new {}
+ t.join
+ t.backtrace.should == nil
+ end
+
+ it "returns an array (which may be empty) immediately after the thread is created" do
+ t = Thread.new { sleep }
+ backtrace = t.backtrace
+ t.kill
+ t.join
+ backtrace.should.is_a?(Array)
+ end
+
+ it "can be called with a number of locations to omit" do
+ locations1 = Thread.current.backtrace
+ locations2 = Thread.current.backtrace(2)
+ locations1[2..-1].length.should == locations2.length
+ locations1[2..-1].map(&:to_s).should == locations2.map(&:to_s)
+ end
+
+ it "can be called with a maximum number of locations to return as second parameter" do
+ locations1 = Thread.current.backtrace
+ locations2 = Thread.current.backtrace(2, 3)
+ locations1[2..4].map(&:to_s).should == locations2.map(&:to_s)
+ end
+
+ it "can be called with a range" do
+ locations1 = Thread.current.backtrace
+ locations2 = Thread.current.backtrace(2..4)
+ locations1[2..4].map(&:to_s).should == locations2.map(&:to_s)
+ end
+
+ it "can be called with a range whose end is negative" do
+ Thread.current.backtrace(2..-1).should == Thread.current.backtrace[2..-1]
+ Thread.current.backtrace(2..-2).should == Thread.current.backtrace[2..-2]
+ end
+
+ it "returns nil if omitting more locations than available" do
+ Thread.current.backtrace(100).should == nil
+ Thread.current.backtrace(100..-1).should == nil
+ end
+
+ it "returns [] if omitting exactly the number of locations available" do
+ omit = Thread.current.backtrace.length
+ Thread.current.backtrace(omit).should == []
+ end
+end
diff --git a/spec/ruby/core/thread/current_spec.rb b/spec/ruby/core/thread/current_spec.rb
new file mode 100644
index 0000000000..f893f078ba
--- /dev/null
+++ b/spec/ruby/core/thread/current_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread.current" do
+ it "returns a thread" do
+ current = Thread.current
+ current.should.is_a?(Thread)
+ end
+
+ it "returns the current thread" do
+ t = Thread.new { Thread.current }
+ t.value.should.equal?(t)
+ Thread.current.should_not.equal?(t.value)
+ end
+
+ it "returns the correct thread in a Fiber" do
+ # This catches a bug where Fibers are running on a thread-pool
+ # and Fibers from a different Ruby Thread reuse the same native thread.
+ # Caching the Ruby Thread based on the native thread is not correct in that case.
+ 2.times do
+ t = Thread.new {
+ cur = Thread.current
+ Fiber.new {
+ Thread.current
+ }.resume.should.equal? cur
+ cur
+ }
+ t.value.should.equal? t
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/each_caller_location_spec.rb b/spec/ruby/core/thread/each_caller_location_spec.rb
new file mode 100644
index 0000000000..15fda1a37b
--- /dev/null
+++ b/spec/ruby/core/thread/each_caller_location_spec.rb
@@ -0,0 +1,47 @@
+require_relative '../../spec_helper'
+
+describe "Thread.each_caller_location" do
+ it "iterates through the current execution stack and matches caller_locations content and type" do
+ ScratchPad.record []
+ Thread.each_caller_location { |l| ScratchPad << l; }
+
+ ScratchPad.recorded.map(&:to_s).should == caller_locations.map(&:to_s)
+ ScratchPad.recorded[0].should.is_a?(Thread::Backtrace::Location)
+ end
+
+ it "returns subset of 'Thread.to_enum(:each_caller_location)' locations" do
+ ar = []
+ ecl = Thread.each_caller_location { |x| ar << x }
+
+ (ar.map(&:to_s) - Thread.to_enum(:each_caller_location).to_a.map(&:to_s)).should.empty?
+ end
+
+ it "stops the backtrace iteration if 'break' occurs" do
+ i = 0
+ ar = []
+ ecl = Thread.each_caller_location do |x|
+ ar << x
+ i += 1
+ break x if i == 2
+ end
+
+ ar.map(&:to_s).should == caller_locations(1, 2).map(&:to_s)
+ ecl.should.is_a?(Thread::Backtrace::Location)
+ end
+
+ it "returns nil" do
+ Thread.each_caller_location {}.should == nil
+ end
+
+ it "raises LocalJumpError when called without a block" do
+ -> {
+ Thread.each_caller_location
+ }.should.raise(LocalJumpError, "no block given")
+ end
+
+ it "doesn't accept keyword arguments" do
+ -> {
+ Thread.each_caller_location(12, foo: 10) {}
+ }.should.raise(ArgumentError);
+ end
+end
diff --git a/spec/ruby/core/thread/element_reference_spec.rb b/spec/ruby/core/thread/element_reference_spec.rb
new file mode 100644
index 0000000000..72892f6c50
--- /dev/null
+++ b/spec/ruby/core/thread/element_reference_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#[]" do
+ it "gives access to thread local values" do
+ th = Thread.new do
+ Thread.current[:value] = 5
+ end
+ th.join
+ th[:value].should == 5
+ Thread.current[:value].should == nil
+ end
+
+ it "is not shared across threads" do
+ t1 = Thread.new do
+ Thread.current[:value] = 1
+ end
+ t2 = Thread.new do
+ Thread.current[:value] = 2
+ end
+ [t1,t2].each {|x| x.join}
+ t1[:value].should == 1
+ t2[:value].should == 2
+ end
+
+ it "is accessible using strings or symbols" do
+ t1 = Thread.new do
+ Thread.current[:value] = 1
+ end
+ t2 = Thread.new do
+ Thread.current["value"] = 2
+ end
+ [t1,t2].each {|x| x.join}
+ t1[:value].should == 1
+ t1["value"].should == 1
+ t2[:value].should == 2
+ t2["value"].should == 2
+ end
+
+ it "converts a key that is neither String nor Symbol with #to_str" do
+ key = mock('value')
+ key.should_receive(:to_str).and_return('value')
+
+ th = Thread.new do
+ Thread.current[:value] = 1
+ end.join
+
+ th[key].should == 1
+ end
+
+ it "raises exceptions on the wrong type of keys" do
+ -> { Thread.current[nil] }.should.raise(TypeError)
+ -> { Thread.current[5] }.should.raise(TypeError)
+ end
+end
diff --git a/spec/ruby/core/thread/element_set_spec.rb b/spec/ruby/core/thread/element_set_spec.rb
new file mode 100644
index 0000000000..97d6c23980
--- /dev/null
+++ b/spec/ruby/core/thread/element_set_spec.rb
@@ -0,0 +1,74 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#[]=" do
+ after :each do
+ Thread.current[:value] = nil
+ end
+
+ it "raises a FrozenError if the thread is frozen" do
+ Thread.new do
+ th = Thread.current
+ th.freeze
+ -> {
+ th[:foo] = "bar"
+ }.should.raise(FrozenError, "can't modify frozen thread locals")
+ end.join
+ end
+
+ it "accepts Strings and Symbols" do
+ t1 = Thread.new do
+ Thread.current[:value] = 1
+ end.join
+ t2 = Thread.new do
+ Thread.current["value"] = 2
+ end.join
+
+ t1[:value].should == 1
+ t2[:value].should == 2
+ end
+
+ it "converts a key that is neither String nor Symbol with #to_str" do
+ key = mock('value')
+ key.should_receive(:to_str).and_return('value')
+
+ th = Thread.new do
+ Thread.current[key] = 1
+ end.join
+
+ th[:value].should == 1
+ end
+
+ it "raises exceptions on the wrong type of keys" do
+ -> { Thread.current[nil] = true }.should.raise(TypeError)
+ -> { Thread.current[5] = true }.should.raise(TypeError)
+ end
+
+ it "is not shared across fibers" do
+ fib = Fiber.new do
+ Thread.current[:value] = 1
+ Fiber.yield
+ Thread.current[:value].should == 1
+ end
+ fib.resume
+ Thread.current[:value].should == nil
+ Thread.current[:value] = 2
+ fib.resume
+ Thread.current[:value] = 2
+ end
+
+ it "stores a local in another thread when in a fiber" do
+ fib = Fiber.new do
+ t = Thread.new do
+ sleep
+ Thread.current[:value].should == 1
+ end
+
+ Thread.pass while t.status and t.status != "sleep"
+ t[:value] = 1
+ t.wakeup
+ t.join
+ end
+ fib.resume
+ end
+end
diff --git a/spec/ruby/core/thread/exit_spec.rb b/spec/ruby/core/thread/exit_spec.rb
new file mode 100644
index 0000000000..b2e923c680
--- /dev/null
+++ b/spec/ruby/core/thread/exit_spec.rb
@@ -0,0 +1,15 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/exit'
+
+describe "Thread#exit!" do
+ it "needs to be reviewed for spec completeness"
+end
+
+describe "Thread.exit" do
+ it "causes the current thread to exit" do
+ thread = Thread.new { Thread.exit; sleep }
+ thread.join
+ thread.status.should == false
+ end
+end
diff --git a/spec/ruby/core/thread/fetch_spec.rb b/spec/ruby/core/thread/fetch_spec.rb
new file mode 100644
index 0000000000..fe27dec4a2
--- /dev/null
+++ b/spec/ruby/core/thread/fetch_spec.rb
@@ -0,0 +1,66 @@
+require_relative '../../spec_helper'
+
+describe 'Thread#fetch' do
+ describe 'with 2 arguments' do
+ it 'returns the value of the fiber-local variable if value has been assigned' do
+ th = Thread.new { Thread.current[:cat] = 'meow' }
+ th.join
+ th.fetch(:cat, true).should == 'meow'
+ end
+
+ it "returns the default value if fiber-local variable hasn't been assigned" do
+ th = Thread.new {}
+ th.join
+ th.fetch(:cat, true).should == true
+ end
+ end
+
+ describe 'with 1 argument' do
+ it 'raises a KeyError when the Thread does not have a fiber-local variable of the same name' do
+ th = Thread.new {}
+ th.join
+ -> { th.fetch(:cat) }.should.raise(KeyError)
+ end
+
+ it 'returns the value of the fiber-local variable if value has been assigned' do
+ th = Thread.new { Thread.current[:cat] = 'meow' }
+ th.join
+ th.fetch(:cat).should == 'meow'
+ end
+ end
+
+ describe 'with a block' do
+ it 'returns the value of the fiber-local variable if value has been assigned' do
+ th = Thread.new { Thread.current[:cat] = 'meow' }
+ th.join
+ th.fetch(:cat) { true }.should == 'meow'
+ end
+
+ it "returns the block value if fiber-local variable hasn't been assigned" do
+ th = Thread.new {}
+ th.join
+ th.fetch(:cat) { true }.should == true
+ end
+
+ it "does not call the block if value has been assigned" do
+ th = Thread.new { Thread.current[:cat] = 'meow' }
+ th.join
+ var = :not_updated
+ th.fetch(:cat) { var = :updated }.should == 'meow'
+ var.should == :not_updated
+ end
+
+ it "uses the block if a default is given and warns about it" do
+ th = Thread.new {}
+ th.join
+ -> {
+ th.fetch(:cat, false) { true }.should == true
+ }.should complain(/warning: block supersedes default value argument/)
+ end
+ end
+
+ it 'raises an ArgumentError when not passed one or two arguments' do
+ -> { Thread.current.fetch() }.should.raise(ArgumentError)
+ -> { Thread.current.fetch(1, 2, 3) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/thread/fixtures/classes.rb b/spec/ruby/core/thread/fixtures/classes.rb
new file mode 100644
index 0000000000..14d5d2f7bf
--- /dev/null
+++ b/spec/ruby/core/thread/fixtures/classes.rb
@@ -0,0 +1,322 @@
+module ThreadSpecs
+
+ class SubThread < Thread
+ def initialize(*args)
+ super { args.first << 1 }
+ end
+ end
+
+ class NewThreadToRaise
+ def self.raise(*args, **kwargs, &block)
+ thread = Thread.new do
+ Thread.current.report_on_exception = false
+
+ if block_given?
+ block.call do
+ sleep
+ end
+ else
+ sleep
+ end
+ end
+
+ Thread.pass until thread.stop?
+
+ thread.raise(*args, **kwargs)
+
+ thread.join
+ ensure
+ thread.kill if thread.alive?
+ Thread.pass while thread.alive? # Thread#kill may not terminate a thread immediately so it may be detected as a leaked one
+ end
+ end
+
+ class Status
+ attr_reader :thread, :inspect, :status, :to_s
+ def initialize(thread)
+ @thread = thread
+ @alive = thread.alive?
+ @inspect = thread.inspect
+ @to_s = thread.to_s
+ @status = thread.status
+ @stop = thread.stop?
+ end
+
+ def alive?
+ @alive
+ end
+
+ def stop?
+ @stop
+ end
+ end
+
+ # TODO: In the great Thread spec rewrite, abstract this
+ class << self
+ attr_accessor :state
+ end
+
+ def self.clear_state
+ @state = nil
+ end
+
+ def self.spin_until_sleeping(t)
+ Thread.pass while t.status and t.status != "sleep"
+ end
+
+ def self.sleeping_thread
+ Thread.new do
+ begin
+ sleep
+ ScratchPad.record :woken
+ rescue Object => e
+ ScratchPad.record e
+ end
+ end
+ end
+
+ def self.running_thread
+ Thread.new do
+ begin
+ ThreadSpecs.state = :running
+ loop { Thread.pass }
+ ScratchPad.record :woken
+ rescue Object => e
+ ScratchPad.record e
+ end
+ end
+ end
+
+ def self.completed_thread
+ Thread.new {}
+ end
+
+ def self.status_of_current_thread
+ Thread.new { Status.new(Thread.current) }.value
+ end
+
+ def self.status_of_running_thread
+ t = running_thread
+ Thread.pass while t.status and t.status != "run"
+ status = Status.new t
+ t.kill
+ t.join
+ status
+ end
+
+ def self.status_of_completed_thread
+ t = completed_thread
+ t.join
+ Status.new t
+ end
+
+ def self.status_of_sleeping_thread
+ t = sleeping_thread
+ Thread.pass while t.status and t.status != 'sleep'
+ status = Status.new t
+ t.run
+ t.join
+ status
+ end
+
+ def self.status_of_blocked_thread
+ m = Mutex.new
+ m.lock
+ t = Thread.new { m.lock }
+ Thread.pass while t.status and t.status != 'sleep'
+ status = Status.new t
+ m.unlock
+ t.join
+ status
+ end
+
+ def self.status_of_killed_thread
+ t = Thread.new { sleep }
+ Thread.pass while t.status and t.status != 'sleep'
+ t.kill
+ t.join
+ Status.new t
+ end
+
+ def self.status_of_thread_with_uncaught_exception
+ t = Thread.new {
+ Thread.current.report_on_exception = false
+ raise "error"
+ }
+ begin
+ t.join
+ rescue RuntimeError
+ end
+ Status.new t
+ end
+
+ def self.status_of_dying_running_thread
+ status = nil
+ t = dying_thread_ensures { status = Status.new Thread.current }
+ t.join
+ status
+ end
+
+ def self.status_of_dying_sleeping_thread
+ t = dying_thread_ensures { Thread.stop; }
+ Thread.pass while t.status and t.status != 'sleep'
+ status = Status.new t
+ t.wakeup
+ t.join
+ status
+ end
+
+ def self.status_of_dying_thread_after_sleep
+ status = nil
+ t = dying_thread_ensures {
+ Thread.stop
+ status = Status.new(Thread.current)
+ }
+ Thread.pass while t.status and t.status != 'sleep'
+ t.wakeup
+ Thread.pass while t.status and t.status == 'sleep'
+ t.join
+ status
+ end
+
+ def self.dying_thread_ensures(kill_method_name=:kill)
+ Thread.new do
+ Thread.current.report_on_exception = false
+ begin
+ Thread.current.send(kill_method_name)
+ ensure
+ yield
+ end
+ end
+ end
+
+ def self.dying_thread_with_outer_ensure(kill_method_name=:kill)
+ Thread.new do
+ Thread.current.report_on_exception = false
+ begin
+ begin
+ Thread.current.send(kill_method_name)
+ ensure
+ raise "In dying thread"
+ end
+ ensure
+ yield
+ end
+ end
+ end
+
+ def self.join_dying_thread_with_outer_ensure(kill_method_name=:kill)
+ t = dying_thread_with_outer_ensure(kill_method_name) { yield }
+ -> { t.join }.should.raise(RuntimeError, "In dying thread")
+ return t
+ end
+
+ def self.wakeup_dying_sleeping_thread(kill_method_name=:kill)
+ t = ThreadSpecs.dying_thread_ensures(kill_method_name) { yield }
+ Thread.pass while t.status and t.status != 'sleep'
+ t.wakeup
+ t.join
+ end
+
+ def self.critical_is_reset
+ # Create another thread to verify that it can call Thread.critical=
+ t = Thread.new do
+ initial_critical = Thread.critical
+ Thread.critical = true
+ Thread.critical = false
+ initial_critical == false && Thread.critical == false
+ end
+ v = t.value
+ t.join
+ v
+ end
+
+ def self.counter
+ @@counter
+ end
+
+ def self.counter= c
+ @@counter = c
+ end
+
+ def self.increment_counter(incr)
+ incr.times do
+ begin
+ Thread.critical = true
+ @@counter += 1
+ ensure
+ Thread.critical = false
+ end
+ end
+ end
+
+ def self.critical_thread1
+ Thread.critical = true
+ Thread.current.key?(:thread_specs).should == false
+ end
+
+ def self.critical_thread2(is_thread_stop)
+ Thread.current[:thread_specs].should == 101
+ Thread.critical.should == !is_thread_stop
+ unless is_thread_stop
+ Thread.critical = false
+ end
+ end
+
+ def self.main_thread1(critical_thread, is_thread_sleep, is_thread_stop)
+ # Thread.stop resets Thread.critical. Also, with native threads, the Thread.Stop may not have executed yet
+ # since the main thread will race with the critical thread
+ unless is_thread_stop
+ Thread.critical.should == true
+ end
+ critical_thread[:thread_specs] = 101
+ if is_thread_sleep or is_thread_stop
+ # Thread#wakeup calls are not queued up. So we need to ensure that the thread is sleeping before calling wakeup
+ Thread.pass while critical_thread.status and critical_thread.status != "sleep"
+ critical_thread.wakeup
+ end
+ end
+
+ def self.main_thread2(critical_thread)
+ Thread.pass # The join below seems to cause a deadlock with CRuby unless Thread.pass is called first
+ critical_thread.join
+ Thread.critical.should == false
+ end
+
+ def self.critical_thread_yields_to_main_thread(is_thread_sleep=false, is_thread_stop=false)
+ @@after_first_sleep = false
+
+ critical_thread = Thread.new do
+ Thread.pass while Thread.main.status and Thread.main.status != "sleep"
+ critical_thread1()
+ Thread.main.wakeup
+ yield
+ Thread.pass while @@after_first_sleep != true # Need to ensure that the next statement does not see the first sleep itself
+ Thread.pass while Thread.main.status and Thread.main.status != "sleep"
+ critical_thread2(is_thread_stop)
+ Thread.main.wakeup
+ end
+
+ sleep 5
+ @@after_first_sleep = true
+ main_thread1(critical_thread, is_thread_sleep, is_thread_stop)
+ sleep 5
+ main_thread2(critical_thread)
+ end
+
+ def self.create_critical_thread
+ Thread.new do
+ Thread.critical = true
+ yield
+ Thread.critical = false
+ end
+ end
+
+ def self.create_and_kill_critical_thread(pass_after_kill=false)
+ ThreadSpecs.create_critical_thread do
+ Thread.current.kill
+ Thread.pass if pass_after_kill
+ ScratchPad.record("status=" + Thread.current.status)
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/fork_spec.rb b/spec/ruby/core/thread/fork_spec.rb
new file mode 100644
index 0000000000..a2f4181298
--- /dev/null
+++ b/spec/ruby/core/thread/fork_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/start'
+
+describe "Thread.fork" do
+ describe "Thread.start" do
+ it_behaves_like :thread_start, :fork
+ end
+end
diff --git a/spec/ruby/core/thread/group_spec.rb b/spec/ruby/core/thread/group_spec.rb
new file mode 100644
index 0000000000..d0d4704b66
--- /dev/null
+++ b/spec/ruby/core/thread/group_spec.rb
@@ -0,0 +1,16 @@
+require_relative '../../spec_helper'
+
+describe "Thread#group" do
+ it "returns the default thread group for the main thread" do
+ Thread.main.group.should == ThreadGroup::Default
+ end
+
+ it "returns the thread group explicitly set for this thread" do
+ thread = Thread.new { nil }
+ thread_group = ThreadGroup.new
+ thread_group.add(thread)
+ thread.group.should == thread_group
+ ensure
+ thread.join if thread
+ end
+end
diff --git a/spec/ruby/core/thread/handle_interrupt_spec.rb b/spec/ruby/core/thread/handle_interrupt_spec.rb
new file mode 100644
index 0000000000..aa03d4c66d
--- /dev/null
+++ b/spec/ruby/core/thread/handle_interrupt_spec.rb
@@ -0,0 +1,125 @@
+require_relative '../../spec_helper'
+
+describe "Thread.handle_interrupt" do
+ def make_handle_interrupt_thread(interrupt_config, blocking = true)
+ interrupt_class = Class.new(RuntimeError)
+
+ ScratchPad.record []
+
+ in_handle_interrupt = Queue.new
+ can_continue = Queue.new
+
+ thread = Thread.new do
+ begin
+ Thread.handle_interrupt(interrupt_config) do
+ begin
+ in_handle_interrupt << true
+ if blocking
+ Thread.pass # Make it clearer the other thread needs to wait for this one to be in #pop
+ can_continue.pop
+ else
+ begin
+ can_continue.pop(true)
+ rescue ThreadError
+ Thread.pass
+ retry
+ end
+ end
+ rescue interrupt_class
+ ScratchPad << :interrupted
+ end
+ end
+ rescue interrupt_class
+ ScratchPad << :deferred
+ end
+ end
+
+ in_handle_interrupt.pop
+ if blocking
+ # Ensure the thread is inside Thread#pop, as if thread.raise is done before it would be deferred
+ Thread.pass until thread.stop?
+ end
+ thread.raise interrupt_class, "interrupt"
+ can_continue << true
+ thread.join
+
+ ScratchPad.recorded
+ end
+
+ before :each do
+ Thread.pending_interrupt?.should == false # sanity check
+ end
+
+ it "with :never defers interrupts until exiting the handle_interrupt block" do
+ make_handle_interrupt_thread(RuntimeError => :never).should == [:deferred]
+ end
+
+ it "with :on_blocking defers interrupts until the next blocking call" do
+ make_handle_interrupt_thread(RuntimeError => :on_blocking).should == [:interrupted]
+ make_handle_interrupt_thread({ RuntimeError => :on_blocking }, false).should == [:deferred]
+ end
+
+ it "with :immediate handles interrupts immediately" do
+ make_handle_interrupt_thread(RuntimeError => :immediate).should == [:interrupted]
+ end
+
+ it "with :immediate immediately runs pending interrupts, before the block" do
+ Thread.handle_interrupt(RuntimeError => :never) do
+ current = Thread.current
+ Thread.new {
+ current.raise "interrupt immediate"
+ }.join
+
+ Thread.pending_interrupt?.should == true
+ -> {
+ Thread.handle_interrupt(RuntimeError => :immediate) {
+ flunk "not reached"
+ }
+ }.should.raise(RuntimeError, "interrupt immediate")
+ Thread.pending_interrupt?.should == false
+ end
+ end
+
+ it "also works with suspended Fibers and does not duplicate interrupts" do
+ fiber = Fiber.new { Fiber.yield }
+ fiber.resume
+
+ Thread.handle_interrupt(RuntimeError => :never) do
+ current = Thread.current
+ Thread.new {
+ current.raise "interrupt with fibers"
+ }.join
+
+ Thread.pending_interrupt?.should == true
+ -> {
+ Thread.handle_interrupt(RuntimeError => :immediate) {
+ flunk "not reached"
+ }
+ }.should.raise(RuntimeError, "interrupt with fibers")
+ Thread.pending_interrupt?.should == false
+ end
+
+ fiber.resume
+ end
+
+ it "runs pending interrupts at the end of the block, even if there was an exception raised in the block" do
+ executed = false
+ -> {
+ Thread.handle_interrupt(RuntimeError => :never) do
+ current = Thread.current
+ Thread.new {
+ current.raise "interrupt exception"
+ }.join
+
+ Thread.pending_interrupt?.should == true
+ executed = true
+ raise "regular exception"
+ end
+ }.should.raise(RuntimeError, "interrupt exception")
+ executed.should == true
+ end
+
+ it "supports multiple pairs in the Hash" do
+ make_handle_interrupt_thread(ArgumentError => :never, RuntimeError => :never).should == [:deferred]
+ end
+end
diff --git a/spec/ruby/core/thread/ignore_deadlock_spec.rb b/spec/ruby/core/thread/ignore_deadlock_spec.rb
new file mode 100644
index 0000000000..b48bc9f9b0
--- /dev/null
+++ b/spec/ruby/core/thread/ignore_deadlock_spec.rb
@@ -0,0 +1,19 @@
+require_relative '../../spec_helper'
+
+describe "Thread.ignore_deadlock" do
+ it "returns false by default" do
+ Thread.ignore_deadlock.should == false
+ end
+end
+
+describe "Thread.ignore_deadlock=" do
+ it "changes the value of Thread.ignore_deadlock" do
+ ignore_deadlock = Thread.ignore_deadlock
+ Thread.ignore_deadlock = true
+ begin
+ Thread.ignore_deadlock.should == true
+ ensure
+ Thread.ignore_deadlock = ignore_deadlock
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/initialize_spec.rb b/spec/ruby/core/thread/initialize_spec.rb
new file mode 100644
index 0000000000..b9a94560ee
--- /dev/null
+++ b/spec/ruby/core/thread/initialize_spec.rb
@@ -0,0 +1,27 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#initialize" do
+
+ describe "already initialized" do
+
+ before do
+ @t = Thread.new { sleep }
+ end
+
+ after do
+ @t.kill
+ @t.join
+ end
+
+ it "raises a ThreadError" do
+ -> {
+ @t.instance_eval do
+ initialize {}
+ end
+ }.should.raise(ThreadError)
+ end
+
+ end
+
+end
diff --git a/spec/ruby/core/thread/inspect_spec.rb b/spec/ruby/core/thread/inspect_spec.rb
new file mode 100644
index 0000000000..bd6e0c31fc
--- /dev/null
+++ b/spec/ruby/core/thread/inspect_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+
+describe "Thread#inspect" do
+ it_behaves_like :thread_to_s, :inspect
+end
diff --git a/spec/ruby/core/thread/join_spec.rb b/spec/ruby/core/thread/join_spec.rb
new file mode 100644
index 0000000000..f4332167f1
--- /dev/null
+++ b/spec/ruby/core/thread/join_spec.rb
@@ -0,0 +1,70 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#join" do
+ it "returns the thread when it is finished" do
+ t = Thread.new {}
+ t.join.should.equal?(t)
+ end
+
+ it "returns the thread when it is finished when given a timeout" do
+ t = Thread.new {}
+ t.join
+ t.join(0).should.equal?(t)
+ end
+
+ it "coerces timeout to a Float if it is not nil" do
+ t = Thread.new {}
+ t.join
+ t.join(0).should.equal?(t)
+ t.join(0.0).should.equal?(t)
+ t.join(nil).should.equal?(t)
+ end
+
+ it "raises TypeError if the argument is not a valid timeout" do
+ t = Thread.new { }
+ t.join
+ -> { t.join(:foo) }.should.raise TypeError
+ -> { t.join("bar") }.should.raise TypeError
+ end
+
+ it "returns nil if it is not finished when given a timeout" do
+ q = Queue.new
+ t = Thread.new { q.pop }
+ begin
+ t.join(0).should == nil
+ ensure
+ q << true
+ end
+ t.join.should == t
+ end
+
+ it "accepts a floating point timeout length" do
+ q = Queue.new
+ t = Thread.new { q.pop }
+ begin
+ t.join(0.01).should == nil
+ ensure
+ q << true
+ end
+ t.join.should == t
+ end
+
+ it "raises any exceptions encountered in the thread body" do
+ t = Thread.new {
+ Thread.current.report_on_exception = false
+ raise NotImplementedError.new("Just kidding")
+ }
+ -> { t.join }.should.raise(NotImplementedError)
+ end
+
+ it "returns the dead thread" do
+ t = Thread.new { Thread.current.kill }
+ t.join.should.equal?(t)
+ end
+
+ it "raises any uncaught exception encountered in ensure block" do
+ t = ThreadSpecs.dying_thread_ensures { raise NotImplementedError.new("Just kidding") }
+ -> { t.join }.should.raise(NotImplementedError)
+ end
+end
diff --git a/spec/ruby/core/thread/key_spec.rb b/spec/ruby/core/thread/key_spec.rb
new file mode 100644
index 0000000000..a14aeb8d31
--- /dev/null
+++ b/spec/ruby/core/thread/key_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#key?" do
+ before :each do
+ @th = Thread.new do
+ Thread.current[:oliver] = "a"
+ end
+ @th.join
+ end
+
+ it "tests for existence of thread local variables using symbols or strings" do
+ @th.key?(:oliver).should == true
+ @th.key?("oliver").should == true
+ @th.key?(:stanley).should == false
+ @th.key?(:stanley.to_s).should == false
+ end
+
+ it "converts a key that is neither String nor Symbol with #to_str" do
+ key = mock('key')
+ key.should_receive(:to_str).and_return('oliver')
+
+ @th.key?(key).should == true
+ end
+
+ it "raises exceptions on the wrong type of keys" do
+ -> { Thread.current.key? nil }.should.raise(TypeError)
+ -> { Thread.current.key? 5 }.should.raise(TypeError)
+ end
+
+ it "is not shared across fibers" do
+ fib = Fiber.new do
+ Thread.current[:val1] = 1
+ Fiber.yield
+ Thread.current.key?(:val1).should == true
+ Thread.current.key?(:val2).should == false
+ end
+ Thread.current.key?(:val1).should_not == true
+ fib.resume
+ Thread.current[:val2] = 2
+ fib.resume
+ Thread.current.key?(:val1).should == false
+ Thread.current.key?(:val2).should == true
+ end
+
+ it "stores a local in another thread when in a fiber" do
+ fib = Fiber.new do
+ t = Thread.new do
+ sleep
+ Thread.current.key?(:value).should == true
+ end
+
+ Thread.pass while t.status and t.status != "sleep"
+ t[:value] = 1
+ t.wakeup
+ t.join
+ end
+ fib.resume
+ end
+end
diff --git a/spec/ruby/core/thread/keys_spec.rb b/spec/ruby/core/thread/keys_spec.rb
new file mode 100644
index 0000000000..3a2edd2456
--- /dev/null
+++ b/spec/ruby/core/thread/keys_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#keys" do
+ it "returns an array of the names of the thread-local variables as symbols" do
+ th = Thread.new do
+ Thread.current["cat"] = 'woof'
+ Thread.current[:cat] = 'meow'
+ Thread.current[:dog] = 'woof'
+ end
+ th.join
+ th.keys.sort_by {|x| x.to_s}.should == [:cat,:dog]
+ end
+
+ it "is not shared across fibers" do
+ fib = Fiber.new do
+ Thread.current[:val1] = 1
+ Fiber.yield
+ Thread.current.keys.should.include?(:val1)
+ Thread.current.keys.should_not.include?(:val2)
+ end
+ Thread.current.keys.should_not.include?(:val1)
+ fib.resume
+ Thread.current[:val2] = 2
+ fib.resume
+ Thread.current.keys.should.include?(:val2)
+ Thread.current.keys.should_not.include?(:val1)
+ end
+
+ it "stores a local in another thread when in a fiber" do
+ fib = Fiber.new do
+ t = Thread.new do
+ sleep
+ Thread.current.keys.should.include?(:value)
+ end
+
+ Thread.pass while t.status and t.status != "sleep"
+ t[:value] = 1
+ t.wakeup
+ t.join
+ end
+ fib.resume
+ end
+end
diff --git a/spec/ruby/core/thread/kill_spec.rb b/spec/ruby/core/thread/kill_spec.rb
new file mode 100644
index 0000000000..f9f1f46744
--- /dev/null
+++ b/spec/ruby/core/thread/kill_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/exit'
+
+# This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19473223/job/f69derxnlo09xhuj
+# TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard.
+platform_is_not :mingw do
+ describe "Thread#kill" do
+ it_behaves_like :thread_exit, :kill
+ end
+
+ describe "Thread.kill" do
+ it "causes the given thread to exit" do
+ thread = Thread.new { sleep }
+ Thread.pass while thread.status and thread.status != "sleep"
+ Thread.kill(thread).should == thread
+ thread.join
+ thread.status.should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/list_spec.rb b/spec/ruby/core/thread/list_spec.rb
new file mode 100644
index 0000000000..5036841d58
--- /dev/null
+++ b/spec/ruby/core/thread/list_spec.rb
@@ -0,0 +1,55 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread.list" do
+ it "includes the current and main thread" do
+ Thread.list.should.include?(Thread.current)
+ Thread.list.should.include?(Thread.main)
+ end
+
+ it "includes threads of non-default thread groups" do
+ t = Thread.new { sleep }
+ begin
+ ThreadGroup.new.add(t)
+ Thread.list.should.include?(t)
+ ensure
+ t.kill
+ t.join
+ end
+ end
+
+ it "does not include deceased threads" do
+ t = Thread.new { 1; }
+ t.join
+ Thread.list.should_not.include?(t)
+ end
+
+ it "includes waiting threads" do
+ q = Queue.new
+ t = Thread.new { q.pop }
+ begin
+ Thread.pass while t.status and t.status != 'sleep'
+ Thread.list.should.include?(t)
+ ensure
+ q << nil
+ t.join
+ end
+ end
+
+ it "returns instances of Thread and not null or nil values" do
+ spawner = Thread.new do
+ Array.new(100) do
+ Thread.new {}
+ end
+ end
+
+ begin
+ Thread.list.each { |th|
+ th.should.is_a?(Thread)
+ }
+ end while spawner.alive?
+
+ threads = spawner.value
+ threads.each(&:join)
+ end
+end
diff --git a/spec/ruby/core/thread/main_spec.rb b/spec/ruby/core/thread/main_spec.rb
new file mode 100644
index 0000000000..ec91709576
--- /dev/null
+++ b/spec/ruby/core/thread/main_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread.main" do
+ it "returns the main thread" do
+ Thread.new { @main = Thread.main ; @current = Thread.current}.join
+ @main.should_not == @current
+ @main.should == Thread.current
+ end
+end
diff --git a/spec/ruby/core/thread/name_spec.rb b/spec/ruby/core/thread/name_spec.rb
new file mode 100644
index 0000000000..47d807be4d
--- /dev/null
+++ b/spec/ruby/core/thread/name_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+
+describe "Thread#name" do
+ before :each do
+ @thread = Thread.new {}
+ end
+
+ after :each do
+ @thread.join
+ end
+
+ it "is nil initially" do
+ @thread.name.should == nil
+ end
+
+ it "returns the thread name" do
+ @thread.name = "thread_name"
+ @thread.name.should == "thread_name"
+ end
+end
+
+describe "Thread#name=" do
+ before :each do
+ @thread = Thread.new {}
+ end
+
+ after :each do
+ @thread.join
+ end
+
+ it "can be set to a String" do
+ @thread.name = "new thread name"
+ @thread.name.should == "new thread name"
+ end
+
+ it "raises an ArgumentError if the name includes a null byte" do
+ -> {
+ @thread.name = "new thread\0name"
+ }.should.raise(ArgumentError)
+ end
+
+ it "can be reset to nil" do
+ @thread.name = nil
+ @thread.name.should == nil
+ end
+
+ it "calls #to_str to convert name to String" do
+ name = mock("Thread#name")
+ name.should_receive(:to_str).and_return("a thread name")
+
+ @thread.name = name
+ @thread.name.should == "a thread name"
+ end
+end
diff --git a/spec/ruby/core/thread/native_thread_id_spec.rb b/spec/ruby/core/thread/native_thread_id_spec.rb
new file mode 100644
index 0000000000..cc72e0b853
--- /dev/null
+++ b/spec/ruby/core/thread/native_thread_id_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+platform_is :linux, :darwin, :windows, :freebsd do
+ describe "Thread#native_thread_id" do
+ it "returns an integer when the thread is alive" do
+ Thread.current.native_thread_id.should.is_a?(Integer)
+ end
+
+ it "returns nil when the thread is not running" do
+ t = Thread.new {}
+ t.join
+ t.native_thread_id.should == nil
+ end
+
+ it "each thread has different native thread id" do
+ t = Thread.new { sleep }
+ Thread.pass until t.stop?
+ main_thread_id = Thread.current.native_thread_id
+ t_thread_id = t.native_thread_id
+
+ # native_thread_id can be nil on a M:N scheduler
+ t_thread_id.should.is_a?(Integer) if t_thread_id != nil
+
+ main_thread_id.should_not == t_thread_id
+
+ t.run
+ t.join
+ t.native_thread_id.should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/new_spec.rb b/spec/ruby/core/thread/new_spec.rb
new file mode 100644
index 0000000000..acb6cd4e30
--- /dev/null
+++ b/spec/ruby/core/thread/new_spec.rb
@@ -0,0 +1,83 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread.new" do
+ it "creates a thread executing the given block" do
+ q = Queue.new
+ Thread.new { q << true }.join
+ q << false
+ q.pop.should == true
+ end
+
+ it "can pass arguments to the thread block" do
+ arr = []
+ a, b, c = 1, 2, 3
+ t = Thread.new(a,b,c) {|d,e,f| arr << d << e << f }
+ t.join
+ arr.should == [a,b,c]
+ end
+
+ it "raises an exception when not given a block" do
+ -> { Thread.new }.should.raise(ThreadError)
+ end
+
+ it "creates a subclass of thread calls super with a block in initialize" do
+ arr = []
+ t = ThreadSpecs::SubThread.new(arr)
+ t.join
+ arr.should == [1]
+ end
+
+ it "calls #initialize and raises an error if super not used" do
+ c = Class.new(Thread) do
+ def initialize
+ end
+ end
+
+ -> {
+ c.new
+ }.should.raise(ThreadError)
+ end
+
+ it "calls and respects #initialize for the block to use" do
+ c = Class.new(Thread) do
+ def initialize
+ ScratchPad.record [:good]
+ super { ScratchPad << :in_thread }
+ end
+ end
+
+ t = c.new
+ t.join
+
+ ScratchPad.recorded.should == [:good, :in_thread]
+ end
+
+ it "releases Mutexes held by the Thread when the Thread finishes" do
+ m1 = Mutex.new
+ m2 = Mutex.new
+ t = Thread.new {
+ m1.lock
+ m1.should.locked?
+ m2.lock
+ m2.should.locked?
+ }
+ t.join
+ m1.should_not.locked?
+ m2.should_not.locked?
+ end
+
+ it "releases Mutexes held by the Thread when the Thread finishes, also with Mutex#synchronize" do
+ m = Mutex.new
+ t = Thread.new {
+ m.synchronize {
+ m.unlock
+ m.lock
+ }
+ m.lock
+ m.should.locked?
+ }
+ t.join
+ m.should_not.locked?
+ end
+end
diff --git a/spec/ruby/core/thread/pass_spec.rb b/spec/ruby/core/thread/pass_spec.rb
new file mode 100644
index 0000000000..a5ac11a58c
--- /dev/null
+++ b/spec/ruby/core/thread/pass_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread.pass" do
+ it "returns nil" do
+ Thread.pass.should == nil
+ end
+end
diff --git a/spec/ruby/core/thread/pending_interrupt_spec.rb b/spec/ruby/core/thread/pending_interrupt_spec.rb
new file mode 100644
index 0000000000..5fbe7422a9
--- /dev/null
+++ b/spec/ruby/core/thread/pending_interrupt_spec.rb
@@ -0,0 +1,32 @@
+require_relative '../../spec_helper'
+
+describe "Thread.pending_interrupt?" do
+ it "returns false if there are no pending interrupts, e.g., outside any Thread.handle_interrupt block" do
+ Thread.pending_interrupt?.should == false
+ end
+
+ it "returns true if there are pending interrupts, e.g., Thread#raise inside Thread.handle_interrupt" do
+ executed = false
+ -> {
+ Thread.handle_interrupt(RuntimeError => :never) do
+ Thread.pending_interrupt?.should == false
+
+ current = Thread.current
+ Thread.new {
+ current.raise "interrupt"
+ }.join
+
+ Thread.pending_interrupt?.should == true
+ executed = true
+ end
+ }.should.raise(RuntimeError, "interrupt")
+ executed.should == true
+ Thread.pending_interrupt?.should == false
+ end
+end
+
+describe "Thread#pending_interrupt?" do
+ it "returns whether the given threads has pending interrupts" do
+ Thread.current.pending_interrupt?.should == false
+ end
+end
diff --git a/spec/ruby/core/thread/priority_spec.rb b/spec/ruby/core/thread/priority_spec.rb
new file mode 100644
index 0000000000..970f7f9971
--- /dev/null
+++ b/spec/ruby/core/thread/priority_spec.rb
@@ -0,0 +1,72 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#priority" do
+ before :each do
+ @current_priority = Thread.current.priority
+ ThreadSpecs.clear_state
+ @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit }
+ Thread.pass until @thread.alive?
+ end
+
+ after :each do
+ ThreadSpecs.state = :exit
+ @thread.join
+ end
+
+ it "inherits the priority of the current thread while running" do
+ @thread.alive?.should == true
+ @thread.priority.should == @current_priority
+ end
+
+ it "maintain the priority of the current thread after death" do
+ ThreadSpecs.state = :exit
+ @thread.join
+ @thread.alive?.should == false
+ @thread.priority.should == @current_priority
+ end
+
+ it "returns an integer" do
+ @thread.priority.should.is_a?(Integer)
+ end
+end
+
+describe "Thread#priority=" do
+ before :each do
+ ThreadSpecs.clear_state
+ @thread = Thread.new { Thread.pass until ThreadSpecs.state == :exit }
+ Thread.pass until @thread.alive?
+ end
+
+ after :each do
+ ThreadSpecs.state = :exit
+ @thread.join
+ end
+
+ describe "when set with an integer" do
+ it "returns an integer" do
+ value = (@thread.priority = 3)
+ value.should == 3
+ end
+
+ it "clamps the priority to -3..3" do
+ @thread.priority = 42
+ @thread.priority.should == 3
+ @thread.priority = -42
+ @thread.priority.should == -3
+ end
+ end
+
+ describe "when set with a non-integer" do
+ it "raises a type error" do
+ ->{ @thread.priority = Object.new }.should.raise(TypeError)
+ end
+ end
+
+ it "sets priority even when the thread has died" do
+ thread = Thread.new {}
+ thread.join
+ thread.priority = 3
+ thread.priority.should == 3
+ end
+end
diff --git a/spec/ruby/core/thread/raise_spec.rb b/spec/ruby/core/thread/raise_spec.rb
new file mode 100644
index 0000000000..efc09d4a35
--- /dev/null
+++ b/spec/ruby/core/thread/raise_spec.rb
@@ -0,0 +1,267 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative '../../shared/kernel/raise'
+
+describe "Thread#raise" do
+ it "is a public method" do
+ Thread.public_instance_methods.should.include?(:raise)
+ end
+
+ it_behaves_like :kernel_raise, :raise, ThreadSpecs::NewThreadToRaise
+ it_behaves_like :kernel_raise_across_contexts, :raise, ThreadSpecs::NewThreadToRaise
+ ruby_version_is "4.0" do
+ it_behaves_like :kernel_raise_with_cause, :raise, ThreadSpecs::NewThreadToRaise
+ end
+
+ it "ignores dead threads and returns nil" do
+ t = Thread.new { :dead }
+ Thread.pass while t.alive?
+ t.raise("Kill the thread").should == nil
+ t.join
+ end
+end
+
+describe "Thread#raise on a sleeping thread" do
+ before :each do
+ ScratchPad.clear
+ @thr = ThreadSpecs.sleeping_thread
+ Thread.pass while @thr.status and @thr.status != "sleep"
+ end
+
+ after :each do
+ @thr.kill
+ @thr.join
+ end
+
+ it "raises a RuntimeError if no exception class is given" do
+ @thr.raise
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(RuntimeError)
+ end
+
+ it "raises the given exception" do
+ @thr.raise Exception
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(Exception)
+ end
+
+ it "raises the given exception with the given message" do
+ @thr.raise Exception, "get to work"
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(Exception)
+ ScratchPad.recorded.message.should == "get to work"
+ end
+
+ it "raises the given exception and the backtrace is the one of the interrupted thread" do
+ @thr.raise Exception
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(Exception)
+ ScratchPad.recorded.backtrace[0].should.include?("sleep")
+ end
+
+ it "is captured and raised by Thread#value" do
+ t = Thread.new do
+ Thread.current.report_on_exception = false
+ sleep
+ end
+
+ ThreadSpecs.spin_until_sleeping(t)
+
+ t.raise
+ -> { t.value }.should.raise(RuntimeError)
+ end
+
+ it "raises a RuntimeError when called with no arguments inside rescue" do
+ t = Thread.new do
+ Thread.current.report_on_exception = false
+ begin
+ 1/0
+ rescue ZeroDivisionError
+ sleep
+ end
+ end
+ begin
+ raise RangeError
+ rescue
+ ThreadSpecs.spin_until_sleeping(t)
+ t.raise
+ end
+ -> { t.value }.should.raise(RuntimeError)
+ end
+
+ it "re-raises a previously rescued exception without overwriting the backtrace" do
+ t = Thread.new do
+ -> { # To make sure there is at least one entry in the call stack
+ begin
+ sleep
+ rescue => e
+ e
+ end
+ }.call
+ end
+
+ ThreadSpecs.spin_until_sleeping(t)
+
+ begin
+ initial_raise_line = __LINE__; raise 'raised'
+ rescue => raised
+ raise_again_line = __LINE__; t.raise raised
+ raised_again = t.value
+
+ raised_again.backtrace.first.should.include?("#{__FILE__}:#{initial_raise_line}:")
+ raised_again.backtrace.first.should_not.include?("#{__FILE__}:#{raise_again_line}:")
+ end
+ end
+
+ it "calls #exception in both the caller and in the target thread" do
+ cls = Class.new(Exception) do
+ attr_accessor :log
+ def initialize(*args)
+ @log = [] # This is shared because the super #exception uses a shallow clone
+ super
+ end
+
+ def exception(*args)
+ @log << [self, Thread.current, args]
+ super
+ end
+ end
+ exc = cls.new
+
+ @thr.raise exc, "Thread#raise #exception spec"
+ @thr.join
+ ScratchPad.recorded.should.is_a?(cls)
+ exc.log.should == [
+ [exc, Thread.current, ["Thread#raise #exception spec"]],
+ [ScratchPad.recorded, @thr, []]
+ ]
+ end
+
+ it "calls #set_backtrace only in the caller thread" do
+ cls = Class.new(Exception) do
+ attr_accessor :log
+ def initialize(*args)
+ @log = [] # This is shared because the super #exception uses a shallow clone
+ super
+ end
+
+ def set_backtrace(backtrace)
+ @log << [Thread.current, backtrace]
+ super
+ end
+ end
+ exc = cls.new
+
+ backtrace = ["a.rb:1"]
+
+ @thr.raise exc, "Thread#raise #set_backtrace spec", backtrace
+ @thr.join
+ ScratchPad.recorded.should.is_a?(cls)
+ exc.log.should == [
+ [Thread.current, backtrace]
+ ]
+ end
+end
+
+describe "Thread#raise on a running thread" do
+ before :each do
+ ScratchPad.clear
+ ThreadSpecs.clear_state
+
+ @thr = ThreadSpecs.running_thread
+ Thread.pass until ThreadSpecs.state == :running
+ end
+
+ after :each do
+ @thr.kill
+ @thr.join
+ end
+
+ it "raises a RuntimeError if no exception class is given" do
+ @thr.raise
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(RuntimeError)
+ end
+
+ it "raises the given exception" do
+ @thr.raise Exception
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(Exception)
+ end
+
+ it "raises the given exception with the given message" do
+ @thr.raise Exception, "get to work"
+ Thread.pass while @thr.status
+ ScratchPad.recorded.should.is_a?(Exception)
+ ScratchPad.recorded.message.should == "get to work"
+ end
+
+ it "can go unhandled" do
+ q = Queue.new
+ t = Thread.new do
+ Thread.current.report_on_exception = false
+ q << true
+ loop { Thread.pass }
+ end
+
+ q.pop # wait for `report_on_exception = false`.
+ t.raise
+ -> { t.value }.should.raise(RuntimeError)
+ end
+
+ it "raises the given argument even when there is an active exception" do
+ raised = false
+ t = Thread.new do
+ Thread.current.report_on_exception = false
+ begin
+ 1/0
+ rescue ZeroDivisionError
+ raised = true
+ loop { Thread.pass }
+ end
+ end
+ begin
+ raise "Create an active exception for the current thread too"
+ rescue
+ Thread.pass until raised
+ t.raise RangeError
+ -> { t.value }.should.raise(RangeError)
+ end
+ end
+
+ it "raises a RuntimeError when called with no arguments inside rescue" do
+ raised = false
+ t = Thread.new do
+ Thread.current.report_on_exception = false
+ begin
+ 1/0
+ rescue ZeroDivisionError
+ raised = true
+ loop { Thread.pass }
+ end
+ end
+ begin
+ raise RangeError
+ rescue
+ Thread.pass until raised
+ t.raise
+ end
+ -> { t.value }.should.raise(RuntimeError)
+ end
+end
+
+describe "Thread#raise on same thread" do
+ it_behaves_like :kernel_raise, :raise, Thread.current
+
+ it "raises a RuntimeError when called with no arguments inside rescue" do
+ t = Thread.new do
+ Thread.current.report_on_exception = false
+ begin
+ 1/0
+ rescue ZeroDivisionError
+ Thread.current.raise
+ end
+ end
+ -> { t.value }.should.raise(RuntimeError, '')
+ end
+end
diff --git a/spec/ruby/core/thread/report_on_exception_spec.rb b/spec/ruby/core/thread/report_on_exception_spec.rb
new file mode 100644
index 0000000000..9cf5260808
--- /dev/null
+++ b/spec/ruby/core/thread/report_on_exception_spec.rb
@@ -0,0 +1,155 @@
+require_relative '../../spec_helper'
+
+describe "Thread.report_on_exception" do
+ it "defaults to true" do
+ ruby_exe("p Thread.report_on_exception").should == "true\n"
+ end
+end
+
+describe "Thread.report_on_exception=" do
+ before :each do
+ @report_on_exception = Thread.report_on_exception
+ end
+
+ after :each do
+ Thread.report_on_exception = @report_on_exception
+ end
+
+ it "changes the default value for new threads" do
+ Thread.report_on_exception = true
+ Thread.report_on_exception.should == true
+ t = Thread.new {}
+ t.join
+ t.report_on_exception.should == true
+ end
+end
+
+describe "Thread#report_on_exception" do
+ it "returns true for the main Thread" do
+ Thread.current.report_on_exception.should == true
+ end
+
+ it "returns true for new Threads" do
+ Thread.new { Thread.current.report_on_exception }.value.should == true
+ end
+
+ it "returns whether the Thread will print a backtrace if it exits with an exception" do
+ t = Thread.new { Thread.current.report_on_exception = true }
+ t.join
+ t.report_on_exception.should == true
+
+ t = Thread.new { Thread.current.report_on_exception = false }
+ t.join
+ t.report_on_exception.should == false
+ end
+end
+
+describe "Thread#report_on_exception=" do
+ describe "when set to true" do
+ it "prints a backtrace on $stderr if it terminates with an exception" do
+ t = nil
+ -> {
+ t = Thread.new {
+ Thread.current.report_on_exception = true
+ raise RuntimeError, "Thread#report_on_exception specs"
+ }
+ Thread.pass while t.alive?
+ }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m)
+
+ -> {
+ t.join
+ }.should.raise(RuntimeError, "Thread#report_on_exception specs")
+ end
+
+ it "prints a backtrace on $stderr in the regular backtrace order" do
+ line_raise = __LINE__ + 2
+ def foo
+ raise RuntimeError, "Thread#report_on_exception specs backtrace order"
+ end
+
+ line_call_foo = __LINE__ + 5
+ go = false
+ t = Thread.new {
+ Thread.current.report_on_exception = true
+ Thread.pass until go
+ foo
+ }
+
+ -> {
+ go = true
+ Thread.pass while t.alive?
+ }.should output("", /\A
+#{Regexp.quote(t.inspect)}\sterminated\swith\sexception\s\(report_on_exception\sis\strue\):\n
+#{Regexp.quote(__FILE__)}:#{line_raise}:in\s[`']foo':\sThread\#report_on_exception\sspecs\sbacktrace\sorder\s\(RuntimeError\)\n
+\tfrom\s#{Regexp.quote(__FILE__)}:#{line_call_foo}:in\s[`']block\s\(4\slevels\)\sin\s<top\s\(required\)>'\n
+\z/x)
+
+ -> {
+ t.join
+ }.should.raise(RuntimeError, "Thread#report_on_exception specs backtrace order")
+ end
+
+ it "prints the backtrace even if the thread was killed just after Thread#raise" do
+ t = nil
+ ready = false
+ -> {
+ t = Thread.new {
+ Thread.current.report_on_exception = true
+ ready = true
+ sleep
+ }
+
+ Thread.pass until ready and t.stop?
+ t.raise RuntimeError, "Thread#report_on_exception before kill spec"
+ t.kill
+ Thread.pass while t.alive?
+ }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception before kill spec/m)
+
+ -> {
+ t.join
+ }.should.raise(RuntimeError, "Thread#report_on_exception before kill spec")
+ end
+ end
+
+ describe "when set to false" do
+ it "lets the thread terminates silently with an exception" do
+ t = nil
+ -> {
+ t = Thread.new {
+ Thread.current.report_on_exception = false
+ raise RuntimeError, "Thread#report_on_exception specs"
+ }
+ Thread.pass while t.alive?
+ }.should output("", "")
+
+ -> {
+ t.join
+ }.should.raise(RuntimeError, "Thread#report_on_exception specs")
+ end
+ end
+
+ describe "when used in conjunction with Thread#abort_on_exception" do
+ it "first reports then send the exception back to the main Thread" do
+ t = nil
+ mutex = Mutex.new
+ mutex.lock
+ -> {
+ t = Thread.new {
+ Thread.current.abort_on_exception = true
+ Thread.current.report_on_exception = true
+ mutex.lock
+ mutex.unlock
+ raise RuntimeError, "Thread#report_on_exception specs"
+ }
+
+ -> {
+ mutex.sleep(5)
+ }.should.raise(RuntimeError, "Thread#report_on_exception specs")
+ }.should output("", /Thread.+terminated with exception.+Thread#report_on_exception specs/m)
+
+ -> {
+ t.join
+ }.should.raise(RuntimeError, "Thread#report_on_exception specs")
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/run_spec.rb b/spec/ruby/core/thread/run_spec.rb
new file mode 100644
index 0000000000..f86f793489
--- /dev/null
+++ b/spec/ruby/core/thread/run_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+require_relative 'shared/wakeup'
+
+describe "Thread#run" do
+ it_behaves_like :thread_wakeup, :run
+end
diff --git a/spec/ruby/core/thread/set_trace_func_spec.rb b/spec/ruby/core/thread/set_trace_func_spec.rb
new file mode 100644
index 0000000000..e5d8298ae0
--- /dev/null
+++ b/spec/ruby/core/thread/set_trace_func_spec.rb
@@ -0,0 +1,5 @@
+require_relative '../../spec_helper'
+
+describe "Thread#set_trace_func" do
+ it "needs to be reviewed for spec completeness"
+end
diff --git a/spec/ruby/core/thread/shared/exit.rb b/spec/ruby/core/thread/shared/exit.rb
new file mode 100644
index 0000000000..a294c3a4d6
--- /dev/null
+++ b/spec/ruby/core/thread/shared/exit.rb
@@ -0,0 +1,219 @@
+describe :thread_exit, shared: true do
+ before :each do
+ ScratchPad.clear
+ end
+
+ # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/builds/19390874/job/wv1bsm8skd4e1pxl
+ # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard.
+ platform_is_not :mingw do
+
+ it "kills sleeping thread" do
+ sleeping_thread = Thread.new do
+ sleep
+ ScratchPad.record :after_sleep
+ end
+ Thread.pass while sleeping_thread.status and sleeping_thread.status != "sleep"
+ sleeping_thread.send(@method)
+ sleeping_thread.join
+ ScratchPad.recorded.should == nil
+ end
+
+ it "kills current thread" do
+ thread = Thread.new do
+ Thread.current.send(@method)
+ ScratchPad.record :after_sleep
+ end
+ thread.join
+ ScratchPad.recorded.should == nil
+ end
+
+ it "runs ensure clause" do
+ thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record :in_ensure_clause }
+ thread.join
+ ScratchPad.recorded.should == :in_ensure_clause
+ end
+
+ it "runs nested ensure clauses" do
+ ScratchPad.record []
+ @outer = Thread.new do
+ begin
+ @inner = Thread.new do
+ begin
+ sleep
+ ensure
+ ScratchPad << :inner_ensure_clause
+ end
+ end
+ sleep
+ ensure
+ ScratchPad << :outer_ensure_clause
+ @inner.send(@method)
+ @inner.join
+ end
+ end
+ Thread.pass while @outer.status and @outer.status != "sleep"
+ Thread.pass until @inner
+ Thread.pass while @inner.status and @inner.status != "sleep"
+ @outer.send(@method)
+ @outer.join
+ ScratchPad.recorded.should.include?(:inner_ensure_clause)
+ ScratchPad.recorded.should.include?(:outer_ensure_clause)
+ end
+
+ it "does not set $!" do
+ thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record $! }
+ thread.join
+ ScratchPad.recorded.should == nil
+ end
+
+ it "does not reset $!" do
+ ScratchPad.record []
+
+ exc = RuntimeError.new("foo")
+ thread = Thread.new do
+ begin
+ raise exc
+ ensure
+ ScratchPad << $!
+ begin
+ Thread.current.send(@method)
+ ensure
+ ScratchPad << $!
+ end
+ end
+ end
+ thread.join
+ ScratchPad.recorded.should == [exc, exc]
+ end
+
+ it "cannot be rescued" do
+ thread = Thread.new do
+ begin
+ Thread.current.send(@method)
+ rescue Exception
+ ScratchPad.record :in_rescue
+ end
+ ScratchPad.record :end_of_thread_block
+ end
+
+ thread.join
+ ScratchPad.recorded.should == nil
+ end
+
+ it "kills the entire thread when a fiber is active" do
+ t = Thread.new do
+ Fiber.new do
+ sleep
+ end.resume
+ ScratchPad.record :fiber_resumed
+ end
+ Thread.pass while t.status and t.status != "sleep"
+ t.send(@method)
+ t.join
+ ScratchPad.recorded.should == nil
+ end
+
+ it "kills other fibers of that thread without running their ensure clauses" do
+ t = Thread.new do
+ f = Fiber.new do
+ ScratchPad.record :fiber_resumed
+ begin
+ Fiber.yield
+ ensure
+ ScratchPad.record :fiber_ensure
+ end
+ end
+ f.resume
+ sleep
+ end
+ Thread.pass until t.stop?
+ t.send(@method)
+ t.join
+ ScratchPad.recorded.should == :fiber_resumed
+ end
+
+ # This spec is a mess. It fails randomly, it hangs on MRI, it needs to be removed
+ quarantine! do
+ it "killing dying running does nothing" do
+ in_ensure_clause = false
+ exit_loop = true
+ t = ThreadSpecs.dying_thread_ensures do
+ in_ensure_clause = true
+ loop { if exit_loop then break end }
+ ScratchPad.record :after_stop
+ end
+
+ Thread.pass until in_ensure_clause == true
+ 10.times { t.send(@method); Thread.pass }
+ exit_loop = true
+ t.join
+ ScratchPad.recorded.should == :after_stop
+ end
+ end
+
+ quarantine! do
+
+ it "propagates inner exception to Thread.join if there is an outer ensure clause" do
+ thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { }
+ -> { thread.join }.should.raise(RuntimeError, "In dying thread")
+ end
+
+ it "runs all outer ensure clauses even if inner ensure clause raises exception" do
+ ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record :in_outer_ensure_clause }
+ ScratchPad.recorded.should == :in_outer_ensure_clause
+ end
+
+ it "sets $! in outer ensure clause if inner ensure clause raises exception" do
+ ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record $! }
+ ScratchPad.recorded.to_s.should == "In dying thread"
+ end
+ end
+
+ it "can be rescued by outer rescue clause when inner ensure clause raises exception" do
+ thread = Thread.new do
+ begin
+ begin
+ Thread.current.send(@method)
+ ensure
+ raise "In dying thread"
+ end
+ rescue Exception
+ ScratchPad.record $!
+ end
+ :end_of_thread_block
+ end
+
+ thread.value.should == :end_of_thread_block
+ ScratchPad.recorded.to_s.should == "In dying thread"
+ end
+
+ it "is deferred if ensure clause does Thread.stop" do
+ ThreadSpecs.wakeup_dying_sleeping_thread(@method) { Thread.stop; ScratchPad.record :after_sleep }
+ ScratchPad.recorded.should == :after_sleep
+ end
+
+ # Hangs on 1.8.6.114 OS X, possibly also on Linux
+ quarantine! do
+ it "is deferred if ensure clause sleeps" do
+ ThreadSpecs.wakeup_dying_sleeping_thread(@method) { sleep; ScratchPad.record :after_sleep }
+ ScratchPad.recorded.should == :after_sleep
+ end
+ end
+
+ # This case occurred in JRuby where native threads are used to provide
+ # the same behavior as MRI green threads. Key to this issue was the fact
+ # that the thread which called #exit in its block was also being explicitly
+ # sent #join from outside the thread. The 100.times provides a certain
+ # probability that the deadlock will occur. It was sufficient to reliably
+ # reproduce the deadlock in JRuby.
+ it "does not deadlock when called from within the thread while being joined from without" do
+ 100.times do
+ t = Thread.new { Thread.stop; Thread.current.send(@method) }
+ Thread.pass while t.status and t.status != "sleep"
+ t.wakeup.should == t
+ t.join.should == t
+ end
+ end
+
+ end # platform_is_not :mingw
+end
diff --git a/spec/ruby/core/thread/shared/start.rb b/spec/ruby/core/thread/shared/start.rb
new file mode 100644
index 0000000000..c5d62ab098
--- /dev/null
+++ b/spec/ruby/core/thread/shared/start.rb
@@ -0,0 +1,41 @@
+describe :thread_start, shared: true do
+ before :each do
+ ScratchPad.clear
+ end
+
+ it "raises an ArgumentError if not passed a block" do
+ -> {
+ Thread.send(@method)
+ }.should.raise(ArgumentError)
+ end
+
+ it "spawns a new Thread running the block" do
+ run = false
+ t = Thread.send(@method) { run = true }
+ t.should.is_a?(Thread)
+ t.join
+
+ run.should == true
+ end
+
+ it "respects Thread subclasses" do
+ c = Class.new(Thread)
+ t = c.send(@method) { }
+ t.should.is_a?(c)
+
+ t.join
+ end
+
+ it "does not call #initialize" do
+ c = Class.new(Thread) do
+ def initialize
+ ScratchPad.record :bad
+ end
+ end
+
+ t = c.send(@method) { }
+ t.join
+
+ ScratchPad.recorded.should == nil
+ end
+end
diff --git a/spec/ruby/core/thread/shared/to_s.rb b/spec/ruby/core/thread/shared/to_s.rb
new file mode 100644
index 0000000000..27e53ba4b8
--- /dev/null
+++ b/spec/ruby/core/thread/shared/to_s.rb
@@ -0,0 +1,53 @@
+require_relative '../fixtures/classes'
+
+describe :thread_to_s, shared: true do
+ it "returns a description including file and line number" do
+ thread, line = Thread.new { "hello" }, __LINE__
+ thread.join
+ thread.send(@method).should =~ /^#<Thread:([^ ]*?) #{Regexp.escape __FILE__}:#{line} \w+>$/
+ end
+
+ it "has a binary encoding" do
+ ThreadSpecs.status_of_current_thread.send(@method).encoding.should == Encoding::BINARY
+ end
+
+ it "can check it's own status" do
+ ThreadSpecs.status_of_current_thread.send(@method).should.include?('run')
+ end
+
+ it "describes a running thread" do
+ ThreadSpecs.status_of_running_thread.send(@method).should.include?('run')
+ end
+
+ it "describes a sleeping thread" do
+ ThreadSpecs.status_of_sleeping_thread.send(@method).should.include?('sleep')
+ end
+
+ it "describes a blocked thread" do
+ ThreadSpecs.status_of_blocked_thread.send(@method).should.include?('sleep')
+ end
+
+ it "describes a completed thread" do
+ ThreadSpecs.status_of_completed_thread.send(@method).should.include?('dead')
+ end
+
+ it "describes a killed thread" do
+ ThreadSpecs.status_of_killed_thread.send(@method).should.include?('dead')
+ end
+
+ it "describes a thread with an uncaught exception" do
+ ThreadSpecs.status_of_thread_with_uncaught_exception.send(@method).should.include?('dead')
+ end
+
+ it "describes a dying sleeping thread" do
+ ThreadSpecs.status_of_dying_sleeping_thread.send(@method).should.include?('sleep')
+ end
+
+ it "reports aborting on a killed thread" do
+ ThreadSpecs.status_of_dying_running_thread.send(@method).should.include?('aborting')
+ end
+
+ it "reports aborting on a killed thread after sleep" do
+ ThreadSpecs.status_of_dying_thread_after_sleep.send(@method).should.include?('aborting')
+ end
+end
diff --git a/spec/ruby/core/thread/shared/wakeup.rb b/spec/ruby/core/thread/shared/wakeup.rb
new file mode 100644
index 0000000000..c89235ba60
--- /dev/null
+++ b/spec/ruby/core/thread/shared/wakeup.rb
@@ -0,0 +1,62 @@
+describe :thread_wakeup, shared: true do
+ it "can interrupt Kernel#sleep" do
+ exit_loop = false
+ after_sleep1 = false
+ after_sleep2 = false
+
+ t = Thread.new do
+ while true
+ break if exit_loop == true
+ Thread.pass
+ end
+
+ sleep
+ after_sleep1 = true
+
+ sleep
+ after_sleep2 = true
+ end
+
+ 10.times { t.send(@method); Thread.pass }
+ t.status.should_not == "sleep"
+
+ exit_loop = true
+
+ 10.times { sleep 0.1 if t.status and t.status != "sleep" }
+ after_sleep1.should == false # t should be blocked on the first sleep
+ t.send(@method)
+
+ 10.times { sleep 0.1 if after_sleep1 != true }
+ 10.times { sleep 0.1 if t.status and t.status != "sleep" }
+ after_sleep2.should == false # t should be blocked on the second sleep
+ t.send(@method)
+
+ t.join
+ end
+
+ it "does not result in a deadlock" do
+ t = Thread.new do
+ 10.times { Thread.stop }
+ end
+
+ while t.status
+ begin
+ t.send(@method)
+ rescue ThreadError
+ # The thread might die right after.
+ t.status.should == false
+ end
+ Thread.pass
+ sleep 0.001
+ end
+
+ t.status.should == false
+ t.join
+ end
+
+ it "raises a ThreadError when trying to wake up a dead thread" do
+ t = Thread.new { 1 }
+ t.join
+ -> { t.send @method }.should.raise(ThreadError)
+ end
+end
diff --git a/spec/ruby/core/thread/start_spec.rb b/spec/ruby/core/thread/start_spec.rb
new file mode 100644
index 0000000000..3dd040f98b
--- /dev/null
+++ b/spec/ruby/core/thread/start_spec.rb
@@ -0,0 +1,9 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/start'
+
+describe "Thread.start" do
+ describe "Thread.start" do
+ it_behaves_like :thread_start, :start
+ end
+end
diff --git a/spec/ruby/core/thread/status_spec.rb b/spec/ruby/core/thread/status_spec.rb
new file mode 100644
index 0000000000..4fde663c91
--- /dev/null
+++ b/spec/ruby/core/thread/status_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#status" do
+ it "can check it's own status" do
+ ThreadSpecs.status_of_current_thread.status.should == 'run'
+ end
+
+ it "describes a running thread" do
+ ThreadSpecs.status_of_running_thread.status.should == 'run'
+ end
+
+ it "describes a sleeping thread" do
+ ThreadSpecs.status_of_sleeping_thread.status.should == 'sleep'
+ end
+
+ it "describes a blocked thread" do
+ ThreadSpecs.status_of_blocked_thread.status.should == 'sleep'
+ end
+
+ it "describes a completed thread" do
+ ThreadSpecs.status_of_completed_thread.status.should == false
+ end
+
+ it "describes a killed thread" do
+ ThreadSpecs.status_of_killed_thread.status.should == false
+ end
+
+ it "describes a thread with an uncaught exception" do
+ ThreadSpecs.status_of_thread_with_uncaught_exception.status.should == nil
+ end
+
+ it "describes a dying sleeping thread" do
+ ThreadSpecs.status_of_dying_sleeping_thread.status.should == 'sleep'
+ end
+
+ it "reports aborting on a killed thread" do
+ ThreadSpecs.status_of_dying_running_thread.status.should == 'aborting'
+ end
+
+ it "reports aborting on a killed thread after sleep" do
+ ThreadSpecs.status_of_dying_thread_after_sleep.status.should == 'aborting'
+ end
+
+ it "reports aborting on an externally killed thread that sleeps" do
+ q = Queue.new
+ t = Thread.new do
+ begin
+ q.push nil
+ sleep
+ ensure
+ q.push Thread.current.status
+ end
+ end
+ q.pop
+ t.kill
+ t.join
+ q.pop.should == 'aborting'
+ end
+end
diff --git a/spec/ruby/core/thread/stop_spec.rb b/spec/ruby/core/thread/stop_spec.rb
new file mode 100644
index 0000000000..084ab46ef6
--- /dev/null
+++ b/spec/ruby/core/thread/stop_spec.rb
@@ -0,0 +1,54 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread.stop" do
+ it "causes the current thread to sleep indefinitely" do
+ t = Thread.new { Thread.stop; 5 }
+ Thread.pass while t.status and t.status != 'sleep'
+ t.status.should == 'sleep'
+ t.run
+ t.value.should == 5
+ end
+end
+
+describe "Thread#stop?" do
+ it "can check it's own status" do
+ ThreadSpecs.status_of_current_thread.should_not.stop?
+ end
+
+ it "describes a running thread" do
+ ThreadSpecs.status_of_running_thread.should_not.stop?
+ end
+
+ it "describes a sleeping thread" do
+ ThreadSpecs.status_of_sleeping_thread.should.stop?
+ end
+
+ it "describes a blocked thread" do
+ ThreadSpecs.status_of_blocked_thread.should.stop?
+ end
+
+ it "describes a completed thread" do
+ ThreadSpecs.status_of_completed_thread.should.stop?
+ end
+
+ it "describes a killed thread" do
+ ThreadSpecs.status_of_killed_thread.should.stop?
+ end
+
+ it "describes a thread with an uncaught exception" do
+ ThreadSpecs.status_of_thread_with_uncaught_exception.should.stop?
+ end
+
+ it "describes a dying running thread" do
+ ThreadSpecs.status_of_dying_running_thread.should_not.stop?
+ end
+
+ it "describes a dying sleeping thread" do
+ ThreadSpecs.status_of_dying_sleeping_thread.should.stop?
+ end
+
+ it "describes a dying thread after sleep" do
+ ThreadSpecs.status_of_dying_thread_after_sleep.should_not.stop?
+ end
+end
diff --git a/spec/ruby/core/thread/terminate_spec.rb b/spec/ruby/core/thread/terminate_spec.rb
new file mode 100644
index 0000000000..cf6cab472b
--- /dev/null
+++ b/spec/ruby/core/thread/terminate_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/exit'
+
+describe "Thread#terminate" do
+ it_behaves_like :thread_exit, :terminate
+end
diff --git a/spec/ruby/core/thread/thread_variable_get_spec.rb b/spec/ruby/core/thread/thread_variable_get_spec.rb
new file mode 100644
index 0000000000..3d92cd5479
--- /dev/null
+++ b/spec/ruby/core/thread/thread_variable_get_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "Thread#thread_variable_get" do
+ before :each do
+ @t = Thread.new { }
+ end
+
+ after :each do
+ @t.join
+ end
+
+ it "returns nil if the variable is not set" do
+ @t.thread_variable_get(:a).should == nil
+ end
+
+ it "returns the value previously set by #thread_variable_set" do
+ @t.thread_variable_set(:a, 49)
+ @t.thread_variable_get(:a).should == 49
+ end
+
+ it "returns a value private to self" do
+ @t.thread_variable_set(:thread_variable_get_spec, 82)
+ Thread.current.thread_variable_get(:thread_variable_get_spec).should == nil
+ end
+
+ it "accepts String and Symbol keys interchangeably" do
+ @t.thread_variable_set("a", 49)
+ @t.thread_variable_get("a").should == 49
+ @t.thread_variable_get(:a).should == 49
+ end
+
+ it "converts a key that is neither String nor Symbol with #to_str" do
+ key = mock('key')
+ key.should_receive(:to_str).and_return('a')
+ @t.thread_variable_set(:a, 49)
+ @t.thread_variable_get(key).should == 49
+ end
+
+ it "does not raise FrozenError if the thread is frozen" do
+ @t.freeze
+ @t.thread_variable_get(:a).should == nil
+ end
+
+ it "raises a TypeError if the key is neither Symbol nor String when thread variables are already set" do
+ @t.thread_variable_set(:a, 49)
+ -> { @t.thread_variable_get(123) }.should.raise(TypeError, /123 is not a symbol/)
+ end
+
+ ruby_version_is '3.4' do
+ it "raises a TypeError if the key is neither Symbol nor String when no thread variables are set" do
+ -> { @t.thread_variable_get(123) }.should.raise(TypeError, /123 is not a symbol/)
+ end
+
+ it "raises a TypeError if the key is neither Symbol nor String without calling #to_sym" do
+ key = mock('key')
+ key.should_not_receive(:to_sym)
+ -> { @t.thread_variable_get(key) }.should.raise(TypeError, /#{Regexp.escape(key.inspect)} is not a symbol/)
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/thread_variable_set_spec.rb b/spec/ruby/core/thread/thread_variable_set_spec.rb
new file mode 100644
index 0000000000..f8d25364ae
--- /dev/null
+++ b/spec/ruby/core/thread/thread_variable_set_spec.rb
@@ -0,0 +1,62 @@
+require_relative '../../spec_helper'
+
+describe "Thread#thread_variable_set" do
+ before :each do
+ @t = Thread.new { }
+ end
+
+ after :each do
+ @t.join
+ end
+
+ it "returns the value set" do
+ @t.thread_variable_set(:a, 2).should == 2
+ end
+
+ it "sets a value that will be returned by #thread_variable_get" do
+ @t.thread_variable_set(:a, 49)
+ @t.thread_variable_get(:a).should == 49
+ end
+
+ it "sets a value private to self" do
+ @t.thread_variable_set(:thread_variable_get_spec, 82)
+ @t.thread_variable_get(:thread_variable_get_spec).should == 82
+ Thread.current.thread_variable_get(:thread_variable_get_spec).should == nil
+ end
+
+ it "accepts String and Symbol keys interchangeably" do
+ @t.thread_variable_set('a', 49)
+ @t.thread_variable_get('a').should == 49
+
+ @t.thread_variable_set(:a, 50)
+ @t.thread_variable_get('a').should == 50
+ end
+
+ it "converts a key that is neither String nor Symbol with #to_str" do
+ key = mock('key')
+ key.should_receive(:to_str).and_return('a')
+ @t.thread_variable_set(key, 49)
+ @t.thread_variable_get(:a).should == 49
+ end
+
+ it "removes a key if the value is nil" do
+ @t.thread_variable_set(:a, 52)
+ @t.thread_variable_set(:a, nil)
+ @t.thread_variable?(:a).should == false
+ end
+
+ it "raises a FrozenError if the thread is frozen" do
+ @t.freeze
+ -> { @t.thread_variable_set(:a, 1) }.should.raise(FrozenError, "can't modify frozen thread locals")
+ end
+
+ it "raises a TypeError if the key is neither Symbol nor String, nor responds to #to_str" do
+ -> { @t.thread_variable_set(123, 1) }.should.raise(TypeError, /123 is not a symbol/)
+ end
+
+ it "does not try to convert the key with #to_sym" do
+ key = mock('key')
+ key.should_not_receive(:to_sym)
+ -> { @t.thread_variable_set(key, 42) }.should.raise(TypeError, /#{Regexp.quote(key.inspect)} is not a symbol/)
+ end
+end
diff --git a/spec/ruby/core/thread/thread_variable_spec.rb b/spec/ruby/core/thread/thread_variable_spec.rb
new file mode 100644
index 0000000000..ebafd4f3eb
--- /dev/null
+++ b/spec/ruby/core/thread/thread_variable_spec.rb
@@ -0,0 +1,60 @@
+require_relative '../../spec_helper'
+
+describe "Thread#thread_variable?" do
+ before :each do
+ @t = Thread.new { }
+ end
+
+ after :each do
+ @t.join
+ end
+
+ it "returns false if the thread variables do not contain 'key'" do
+ @t.thread_variable_set(:a, 2)
+ @t.thread_variable?(:b).should == false
+ end
+
+ it "returns true if the thread variables contain 'key'" do
+ @t.thread_variable_set(:a, 2)
+ @t.thread_variable?(:a).should == true
+ end
+
+ it "accepts String and Symbol keys interchangeably" do
+ @t.thread_variable?('a').should == false
+ @t.thread_variable?(:a).should == false
+
+ @t.thread_variable_set(:a, 49)
+
+ @t.thread_variable?('a').should == true
+ @t.thread_variable?(:a).should == true
+ end
+
+ it "converts a key that is neither String nor Symbol with #to_str" do
+ key = mock('key')
+ key.should_receive(:to_str).and_return('a')
+ @t.thread_variable_set(:a, 49)
+ @t.thread_variable?(key).should == true
+ end
+
+ it "does not raise FrozenError if the thread is frozen" do
+ @t.freeze
+ @t.thread_variable?(:a).should == false
+ end
+
+ it "raises a TypeError if the key is neither Symbol nor String when thread variables are already set" do
+ @t.thread_variable_set(:a, 49)
+ -> { @t.thread_variable?(123) }.should.raise(TypeError, /123 is not a symbol/)
+ end
+
+ ruby_version_is '3.4' do
+ it "raises a TypeError if the key is neither Symbol nor String when no thread variables are set" do
+ -> { @t.thread_variable?(123) }.should.raise(TypeError, /123 is not a symbol/)
+ end
+
+ it "raises a TypeError if the key is neither Symbol nor String without calling #to_sym" do
+ key = mock('key')
+ key.should_not_receive(:to_sym)
+ -> { @t.thread_variable?(key) }.should.raise(TypeError, /#{Regexp.escape(key.inspect)} is not a symbol/)
+ end
+ end
+end
diff --git a/spec/ruby/core/thread/thread_variables_spec.rb b/spec/ruby/core/thread/thread_variables_spec.rb
new file mode 100644
index 0000000000..f15c681a8f
--- /dev/null
+++ b/spec/ruby/core/thread/thread_variables_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../spec_helper'
+
+describe "Thread#thread_variables" do
+ before :each do
+ @t = Thread.new { }
+ end
+
+ after :each do
+ @t.join
+ end
+
+ it "returns the keys of all the values set" do
+ @t.thread_variable_set(:a, 2)
+ @t.thread_variable_set(:b, 4)
+ @t.thread_variable_set(:c, 6)
+ @t.thread_variables.sort.should == [:a, :b, :c]
+ end
+
+ it "returns the keys private to self" do
+ @t.thread_variable_set(:a, 82)
+ @t.thread_variable_set(:b, 82)
+ Thread.current.thread_variables.should_not.include?(:a)
+ Thread.current.thread_variables.should_not.include?(:b)
+ end
+
+ it "only contains user thread variables and is empty initially" do
+ Thread.current.thread_variables.should == []
+ @t.thread_variables.should == []
+ end
+
+ it "returns keys as Symbols" do
+ key = mock('key')
+ key.should_receive(:to_str).and_return('a')
+
+ @t.thread_variable_set(key, 49)
+ @t.thread_variable_set('b', 50)
+ @t.thread_variable_set(:c, 51)
+ @t.thread_variables.sort.should == [:a, :b, :c]
+ end
+end
diff --git a/spec/ruby/core/thread/to_s_spec.rb b/spec/ruby/core/thread/to_s_spec.rb
new file mode 100644
index 0000000000..cb182a017f
--- /dev/null
+++ b/spec/ruby/core/thread/to_s_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/to_s'
+
+describe "Thread#to_s" do
+ it_behaves_like :thread_to_s, :to_s
+end
diff --git a/spec/ruby/core/thread/value_spec.rb b/spec/ruby/core/thread/value_spec.rb
new file mode 100644
index 0000000000..50c823171d
--- /dev/null
+++ b/spec/ruby/core/thread/value_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Thread#value" do
+ it "returns the result of the block" do
+ Thread.new { 3 }.value.should == 3
+ end
+
+ it "re-raises an error for an uncaught exception" do
+ t = Thread.new {
+ Thread.current.report_on_exception = false
+ raise "Hello"
+ }
+ -> { t.value }.should.raise(RuntimeError, "Hello")
+ end
+
+ it "is nil for a killed thread" do
+ t = Thread.new { Thread.current.exit }
+ t.value.should == nil
+ end
+
+ it "returns when the thread finished" do
+ q = Queue.new
+ t = Thread.new {
+ q.pop
+ }
+ -> { t.value }.should block_caller
+ q.push :result
+ t.value.should == :result
+ end
+end
diff --git a/spec/ruby/core/thread/wakeup_spec.rb b/spec/ruby/core/thread/wakeup_spec.rb
new file mode 100644
index 0000000000..da5dfea377
--- /dev/null
+++ b/spec/ruby/core/thread/wakeup_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/wakeup'
+
+describe "Thread#wakeup" do
+ it_behaves_like :thread_wakeup, :wakeup
+end
diff --git a/spec/ruby/core/threadgroup/add_spec.rb b/spec/ruby/core/threadgroup/add_spec.rb
new file mode 100644
index 0000000000..2921c1ab22
--- /dev/null
+++ b/spec/ruby/core/threadgroup/add_spec.rb
@@ -0,0 +1,39 @@
+require_relative '../../spec_helper'
+
+describe "ThreadGroup#add" do
+ before :each do
+ @q1, @q2 = Queue.new, Queue.new
+ @thread = Thread.new { @q1 << :go; @q2.pop }
+ @q1.pop
+ end
+
+ after :each do
+ @q2 << :done
+ @thread.join
+ end
+
+ # This spec randomly kills mspec worker like: https://ci.appveyor.com/project/ruby/ruby/build/9806/job/37tx2atojy96227m
+ # TODO: Investigate the cause or at least print helpful logs, and remove this `platform_is_not` guard.
+ platform_is_not :mingw do
+ it "adds the given thread to a group and returns self" do
+ @thread.group.should_not == nil
+
+ tg = ThreadGroup.new
+ tg.add(@thread).should == tg
+ @thread.group.should == tg
+ tg.list.include?(@thread).should == true
+ end
+
+ it "removes itself from any other threadgroup" do
+ tg1 = ThreadGroup.new
+ tg2 = ThreadGroup.new
+
+ tg1.add(@thread)
+ @thread.group.should == tg1
+ tg2.add(@thread)
+ @thread.group.should == tg2
+ tg2.list.include?(@thread).should == true
+ tg1.list.include?(@thread).should == false
+ end
+ end
+end
diff --git a/spec/ruby/core/threadgroup/default_spec.rb b/spec/ruby/core/threadgroup/default_spec.rb
new file mode 100644
index 0000000000..4f57508abf
--- /dev/null
+++ b/spec/ruby/core/threadgroup/default_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "ThreadGroup::Default" do
+ it "is a ThreadGroup instance" do
+ ThreadGroup::Default.should.is_a?(ThreadGroup)
+ end
+
+ it "is the ThreadGroup of the main thread" do
+ ThreadGroup::Default.should == Thread.main.group
+ end
+end
diff --git a/spec/ruby/core/threadgroup/enclose_spec.rb b/spec/ruby/core/threadgroup/enclose_spec.rb
new file mode 100644
index 0000000000..6f703d4ce2
--- /dev/null
+++ b/spec/ruby/core/threadgroup/enclose_spec.rb
@@ -0,0 +1,24 @@
+require_relative '../../spec_helper'
+
+describe "ThreadGroup#enclose" do
+ before :each do
+ @q1, @q2 = Queue.new, Queue.new
+ @thread = Thread.new { @q1 << :go; @q2.pop }
+ @q1.pop
+ end
+
+ after :each do
+ @q2 << :done
+ @thread.join
+ end
+
+ it "raises a ThreadError if attempting to move a Thread from an enclosed ThreadGroup" do
+ thread_group = ThreadGroup.new
+ default_group = @thread.group
+ thread_group.add(@thread)
+ thread_group.enclose
+ -> do
+ default_group.add(@thread)
+ end.should.raise(ThreadError)
+ end
+end
diff --git a/spec/ruby/core/threadgroup/enclosed_spec.rb b/spec/ruby/core/threadgroup/enclosed_spec.rb
new file mode 100644
index 0000000000..cf8a5bb4c6
--- /dev/null
+++ b/spec/ruby/core/threadgroup/enclosed_spec.rb
@@ -0,0 +1,14 @@
+require_relative '../../spec_helper'
+
+describe "ThreadGroup#enclosed?" do
+ it "returns false when a ThreadGroup has not been enclosed (default state)" do
+ thread_group = ThreadGroup.new
+ thread_group.enclosed?.should == false
+ end
+
+ it "returns true when a ThreadGroup is enclosed" do
+ thread_group = ThreadGroup.new
+ thread_group.enclose
+ thread_group.enclosed?.should == true
+ end
+end
diff --git a/spec/ruby/core/threadgroup/list_spec.rb b/spec/ruby/core/threadgroup/list_spec.rb
new file mode 100644
index 0000000000..ef601d75ea
--- /dev/null
+++ b/spec/ruby/core/threadgroup/list_spec.rb
@@ -0,0 +1,23 @@
+require_relative '../../spec_helper'
+
+describe "ThreadGroup#list" do
+ it "returns the list of threads in the group" do
+ q = Queue.new
+ th1 = Thread.new { q << :go; sleep }
+ q.pop.should == :go
+ tg = ThreadGroup.new
+ tg.add(th1)
+ tg.list.should.include?(th1)
+
+ th2 = Thread.new { q << :go; sleep }
+ q.pop.should == :go
+
+ tg.add(th2)
+ (tg.list & [th1, th2]).to_set.should == Set[th1, th2]
+
+ Thread.pass while th1.status and th1.status != 'sleep'
+ Thread.pass while th2.status and th2.status != 'sleep'
+ th1.run; th1.join
+ th2.run; th2.join
+ end
+end
diff --git a/spec/ruby/core/time/_dump_spec.rb b/spec/ruby/core/time/_dump_spec.rb
new file mode 100644
index 0000000000..21f0806327
--- /dev/null
+++ b/spec/ruby/core/time/_dump_spec.rb
@@ -0,0 +1,55 @@
+# encoding: binary
+require_relative '../../spec_helper'
+
+describe "Time#_dump" do
+ before :each do
+ @local = Time.at(946812800)
+ @t = Time.at(946812800)
+ @t = @t.gmtime
+ @s = @t.send(:_dump)
+ end
+
+ it "is a private method" do
+ Time.private_instance_methods(false).should.include?(:_dump)
+ end
+
+ # http://redmine.ruby-lang.org/issues/show/627
+ it "preserves the GMT flag" do
+ @t.should.gmt?
+ dump = @t.send(:_dump).unpack("VV").first
+ ((dump >> 30) & 0x1).should == 1
+
+ @local.should_not.gmt?
+ dump = @local.send(:_dump).unpack("VV").first
+ ((dump >> 30) & 0x1).should == 0
+ end
+
+ it "dumps a Time object to a bytestring" do
+ @s.should.instance_of?(String)
+ @s.should == [3222863947, 2235564032].pack("VV")
+ end
+
+ it "dumps an array with a date as first element" do
+ high = 1 << 31 |
+ (@t.gmt? ? 1 : 0) << 30 |
+ (@t.year - 1900) << 14 |
+ (@t.mon - 1) << 10 |
+ @t.mday << 5 |
+ @t.hour
+
+ high.should == @s.unpack("VV").first
+ end
+
+ it "dumps an array with a time as second element" do
+ low = @t.min << 26 |
+ @t.sec << 20 |
+ @t.usec
+ low.should == @s.unpack("VV").last
+ end
+
+ it "dumps like MRI's marshaled time format" do
+ t = Time.utc(2000, 1, 15, 20, 1, 1, 203).localtime
+
+ t.send(:_dump).should == "\364\001\031\200\313\000\020\004"
+ end
+end
diff --git a/spec/ruby/core/time/_load_spec.rb b/spec/ruby/core/time/_load_spec.rb
new file mode 100644
index 0000000000..a74e3dc129
--- /dev/null
+++ b/spec/ruby/core/time/_load_spec.rb
@@ -0,0 +1,51 @@
+# encoding: binary
+require_relative '../../spec_helper'
+
+describe "Time._load" do
+ it "is a private method" do
+ Time.private_methods(false).should.include?(:_load)
+ end
+
+ # http://redmine.ruby-lang.org/issues/show/627
+ it "loads a time object in the new format" do
+ t = Time.local(2000, 1, 15, 20, 1, 1)
+ t = t.gmtime
+
+ high = 1 << 31 |
+ (t.gmt? ? 1 : 0) << 30 |
+ (t.year - 1900) << 14 |
+ (t.mon - 1) << 10 |
+ t.mday << 5 |
+ t.hour
+
+ low = t.min << 26 |
+ t.sec << 20 |
+ t.usec
+
+ Time.send(:_load, [high, low].pack("VV")).should == t
+ end
+
+ it "loads a time object in the old UNIX timestamp based format" do
+ t = Time.local(2000, 1, 15, 20, 1, 1, 203)
+ timestamp = t.to_i
+
+ high = timestamp & ((1 << 31) - 1)
+
+ low = t.usec
+
+ Time.send(:_load, [high, low].pack("VV")).should == t
+ end
+
+ it "loads MRI's marshaled time format" do
+ t = Marshal.load("\004\bu:\tTime\r\320\246\e\200\320\001\r\347")
+ t.utc
+
+ t.to_s.should == "2010-10-22 16:57:48 UTC"
+ end
+
+ it "treats the data as binary data" do
+ data = "\x04\bu:\tTime\r\fM\x1C\xC0\x00\x00\xD0\xBE".dup.force_encoding Encoding::UTF_8
+ t = Marshal.load(data)
+ t.to_s.should == "2013-04-08 12:47:45 UTC"
+ end
+end
diff --git a/spec/ruby/core/time/asctime_spec.rb b/spec/ruby/core/time/asctime_spec.rb
new file mode 100644
index 0000000000..a41ee531e4
--- /dev/null
+++ b/spec/ruby/core/time/asctime_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/asctime'
+
+describe "Time#asctime" do
+ it_behaves_like :time_asctime, :asctime
+end
diff --git a/spec/ruby/core/time/at_spec.rb b/spec/ruby/core/time/at_spec.rb
new file mode 100644
index 0000000000..10d4d36a68
--- /dev/null
+++ b/spec/ruby/core/time/at_spec.rb
@@ -0,0 +1,316 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Time.at" do
+ describe "passed Numeric" do
+ it "returns a Time object representing the given number of Integer seconds since 1970-01-01 00:00:00 UTC" do
+ Time.at(1184027924).getgm.asctime.should == "Tue Jul 10 00:38:44 2007"
+ end
+
+ it "returns a Time object representing the given number of Float seconds since 1970-01-01 00:00:00 UTC" do
+ t = Time.at(10.5)
+ t.usec.should == 500000.0
+ t.should_not == Time.at(10)
+ end
+
+ it "returns a non-UTC Time" do
+ Time.at(1184027924).should_not.utc?
+ end
+
+ it "returns a subclass instance on a Time subclass" do
+ c = Class.new(Time)
+ t = c.at(0)
+ t.should.instance_of?(c)
+ end
+
+ it "roundtrips a Rational produced by #to_r" do
+ t = Time.now()
+ t2 = Time.at(t.to_r)
+
+ t2.should == t
+ t2.usec.should == t.usec
+ t2.nsec.should == t.nsec
+ end
+
+ describe "passed Rational" do
+ it "returns Time with correct microseconds" do
+ t = Time.at(Rational(1_486_570_508_539_759, 1_000_000))
+ t.usec.should == 539_759
+ t.nsec.should == 539_759_000
+ end
+
+ it "returns Time with correct nanoseconds" do
+ t = Time.at(Rational(1_486_570_508_539_759_123, 1_000_000_000))
+ t.usec.should == 539_759
+ t.nsec.should == 539_759_123
+ end
+ end
+ end
+
+ describe "passed Time" do
+ it "creates a new time object with the value given by time" do
+ t = Time.now
+ Time.at(t).inspect.should == t.inspect
+ end
+
+ it "creates a dup time object with the value given by time" do
+ t1 = Time.new
+ t2 = Time.at(t1)
+ t1.should_not.equal? t2
+ end
+
+ it "returns a UTC time if the argument is UTC" do
+ t = Time.now.getgm
+ Time.at(t).should.utc?
+ end
+
+ it "returns a non-UTC time if the argument is non-UTC" do
+ t = Time.now
+ Time.at(t).should_not.utc?
+ end
+
+ it "returns a subclass instance" do
+ c = Class.new(Time)
+ t = c.at(Time.now)
+ t.should.instance_of?(c)
+ end
+ end
+
+ describe "passed non-Time, non-Numeric" do
+ it "raises a TypeError with a String argument" do
+ -> { Time.at("0") }.should.raise(TypeError)
+ end
+
+ it "raises a TypeError with a nil argument" do
+ -> { Time.at(nil) }.should.raise(TypeError)
+ end
+
+ describe "with an argument that responds to #to_int" do
+ it "coerces using #to_int" do
+ o = mock('integer')
+ o.should_receive(:to_int).and_return(0)
+ Time.at(o).should == Time.at(0)
+ end
+ end
+
+ describe "with an argument that responds to #to_r" do
+ it "coerces using #to_r" do
+ o = mock_numeric('rational')
+ o.should_receive(:to_r).and_return(Rational(5, 2))
+ Time.at(o).should == Time.at(Rational(5, 2))
+ end
+
+ it "needs for the argument to respond to #to_int too" do
+ o = mock('rational-but-no-to_int')
+ def o.to_r; Rational(5, 2) end
+ -> { Time.at(o) }.should.raise(TypeError, "can't convert MockObject into an exact number")
+ end
+ end
+ end
+
+ describe "passed [Integer, Numeric]" do
+ it "returns a Time object representing the given number of seconds and Integer microseconds since 1970-01-01 00:00:00 UTC" do
+ t = Time.at(10, 500000)
+ t.tv_sec.should == 10
+ t.tv_usec.should == 500000
+ end
+
+ it "returns a Time object representing the given number of seconds and Float microseconds since 1970-01-01 00:00:00 UTC" do
+ t = Time.at(10, 500.500)
+ t.tv_sec.should == 10
+ t.tv_nsec.should == 500500
+ end
+ end
+
+ describe "with a second argument that responds to #to_int" do
+ it "coerces using #to_int" do
+ o = mock('integer')
+ o.should_receive(:to_int).and_return(10)
+ Time.at(0, o).should == Time.at(0, 10)
+ end
+ end
+
+ describe "with a second argument that responds to #to_r" do
+ it "coerces using #to_r" do
+ o = mock_numeric('rational')
+ o.should_receive(:to_r).and_return(Rational(5, 2))
+ Time.at(0, o).should == Time.at(0, Rational(5, 2))
+ end
+ end
+
+ describe "passed [Integer, nil]" do
+ it "raises a TypeError" do
+ -> { Time.at(0, nil) }.should.raise(TypeError)
+ end
+ end
+
+ describe "passed [Integer, String]" do
+ it "raises a TypeError" do
+ -> { Time.at(0, "0") }.should.raise(TypeError)
+ end
+ end
+
+ describe "passed [Time, Integer]" do
+ # #8173
+ it "raises a TypeError" do
+ -> { Time.at(Time.now, 500000) }.should.raise(TypeError)
+ end
+ end
+
+ describe "passed [Time, Numeric, format]" do
+ context ":nanosecond format" do
+ it "treats second argument as nanoseconds" do
+ Time.at(0, 123456789, :nanosecond).nsec.should == 123456789
+ end
+ end
+
+ context ":nsec format" do
+ it "treats second argument as nanoseconds" do
+ Time.at(0, 123456789, :nsec).nsec.should == 123456789
+ end
+ end
+
+ context ":microsecond format" do
+ it "treats second argument as microseconds" do
+ Time.at(0, 123456, :microsecond).nsec.should == 123456000
+ end
+ end
+
+ context ":usec format" do
+ it "treats second argument as microseconds" do
+ Time.at(0, 123456, :usec).nsec.should == 123456000
+ end
+ end
+
+ context ":millisecond format" do
+ it "treats second argument as milliseconds" do
+ Time.at(0, 123, :millisecond).nsec.should == 123000000
+ end
+ end
+
+ context "not supported format" do
+ it "raises ArgumentError" do
+ -> { Time.at(0, 123456, 2) }.should.raise(ArgumentError)
+ -> { Time.at(0, 123456, nil) }.should.raise(ArgumentError)
+ -> { Time.at(0, 123456, :invalid) }.should.raise(ArgumentError)
+ end
+
+ it "does not try to convert format to Symbol with #to_sym" do
+ format = +"usec"
+ format.should_not_receive(:to_sym)
+ -> { Time.at(0, 123456, format) }.should.raise(ArgumentError)
+ end
+ end
+
+ it "supports Float second argument" do
+ Time.at(0, 123456789.500, :nanosecond).nsec.should == 123456789
+ Time.at(0, 123456789.500, :nsec).nsec.should == 123456789
+ Time.at(0, 123456.500, :microsecond).nsec.should == 123456500
+ Time.at(0, 123456.500, :usec).nsec.should == 123456500
+ Time.at(0, 123.500, :millisecond).nsec.should == 123500000
+ end
+ end
+
+ describe ":in keyword argument" do
+ before do
+ @epoch_time = Time.now.to_i
+ end
+
+ it "could be UTC offset as a String in '+HH:MM or '-HH:MM' format" do
+ time = Time.at(@epoch_time, in: "+05:00")
+
+ time.utc_offset.should == 5*60*60
+ time.zone.should == nil
+ time.to_i.should == @epoch_time
+
+ time = Time.at(@epoch_time, in: "-09:00")
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == nil
+ time.to_i.should == @epoch_time
+
+ time = Time.at(@epoch_time, in: "-09:00:01")
+
+ time.utc_offset.should == -(9*60*60 + 1)
+ time.zone.should == nil
+ time.to_i.should == @epoch_time
+ end
+
+ it "could be UTC offset as a number of seconds" do
+ time = Time.at(@epoch_time, in: 5*60*60)
+
+ time.utc_offset.should == 5*60*60
+ time.zone.should == nil
+ time.to_i.should == @epoch_time
+
+ time = Time.at(@epoch_time, in: -9*60*60)
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == nil
+ time.to_i.should == @epoch_time
+ end
+
+ it "could be UTC offset as a 'UTC' String" do
+ time = Time.at(@epoch_time, in: "UTC")
+
+ time.utc_offset.should == 0
+ time.zone.should == "UTC"
+ time.to_i.should == @epoch_time
+ end
+
+ it "could be UTC offset as a military zone A-Z" do
+ time = Time.at(@epoch_time, in: "B")
+
+ time.utc_offset.should == 3600 * 2
+ time.zone.should == nil
+ time.to_i.should == @epoch_time
+ end
+
+ it "could be a timezone object" do
+ zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo")
+ time = Time.at(@epoch_time, in: zone)
+
+ time.utc_offset.should == 5*3600+30*60
+ time.zone.should == zone
+ time.to_i.should == @epoch_time
+
+ zone = TimeSpecs::TimezoneWithName.new(name: "PST")
+ time = Time.at(@epoch_time, in: zone)
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == zone
+ time.to_i.should == @epoch_time
+ end
+
+ it "raises ArgumentError if format is invalid" do
+ -> { Time.at(@epoch_time, in: "+09:99") }.should.raise(ArgumentError)
+ -> { Time.at(@epoch_time, in: "ABC") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if hours greater than 23" do # TODO
+ -> { Time.at(@epoch_time, in: "+24:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.at(@epoch_time, in: "+2400") }.should.raise(ArgumentError, "utc_offset out of range")
+
+ -> { Time.at(@epoch_time, in: "+99:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.at(@epoch_time, in: "+9900") }.should.raise(ArgumentError, "utc_offset out of range")
+ end
+
+ it "raises ArgumentError if minutes greater than 59" do # TODO
+ -> { Time.at(@epoch_time, in: "+00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60')
+ -> { Time.at(@epoch_time, in: "+0060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060')
+
+ -> { Time.at(@epoch_time, in: "+00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99')
+ -> { Time.at(@epoch_time, in: "+0099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099')
+ end
+
+ ruby_bug '#20797', ''...'3.4' do
+ it "raises ArgumentError if seconds greater than 59" do
+ -> { Time.at(@epoch_time, in: "+00:00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60')
+ -> { Time.at(@epoch_time, in: "+000060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060')
+
+ -> { Time.at(@epoch_time, in: "+00:00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99')
+ -> { Time.at(@epoch_time, in: "+000099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099')
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/time/ceil_spec.rb b/spec/ruby/core/time/ceil_spec.rb
new file mode 100644
index 0000000000..18e26f9994
--- /dev/null
+++ b/spec/ruby/core/time/ceil_spec.rb
@@ -0,0 +1,44 @@
+require_relative '../../spec_helper'
+
+describe "Time#ceil" do
+ before do
+ @time = Time.utc(2010, 3, 30, 5, 43, "25.0123456789".to_r)
+ end
+
+ it "defaults to ceiling to 0 places" do
+ @time.ceil.should == Time.utc(2010, 3, 30, 5, 43, 26.to_r)
+ end
+
+ it "ceils to 0 decimal places with an explicit argument" do
+ @time.ceil(0).should == Time.utc(2010, 3, 30, 5, 43, 26.to_r)
+ end
+
+ it "ceils to 2 decimal places with an explicit argument" do
+ @time.ceil(2).should == Time.utc(2010, 3, 30, 5, 43, "25.02".to_r)
+ end
+
+ it "ceils to 4 decimal places with an explicit argument" do
+ @time.ceil(4).should == Time.utc(2010, 3, 30, 5, 43, "25.0124".to_r)
+ end
+
+ it "ceils to 7 decimal places with an explicit argument" do
+ @time.ceil(7).should == Time.utc(2010, 3, 30, 5, 43, "25.0123457".to_r)
+ end
+
+ it "returns an instance of Time, even if #ceil is called on a subclass" do
+ subclass = Class.new(Time)
+ instance = subclass.at(0)
+ instance.class.should.equal? subclass
+ instance.ceil.should.instance_of?(Time)
+ end
+
+ it "copies own timezone to the returning value" do
+ @time.zone.should == @time.ceil.zone
+
+ time = with_timezone "JST-9" do
+ Time.at 0, 1
+ end
+
+ time.zone.should == time.ceil.zone
+ end
+end
diff --git a/spec/ruby/core/time/comparison_spec.rb b/spec/ruby/core/time/comparison_spec.rb
new file mode 100644
index 0000000000..0790088f9e
--- /dev/null
+++ b/spec/ruby/core/time/comparison_spec.rb
@@ -0,0 +1,130 @@
+require_relative '../../spec_helper'
+
+describe "Time#<=>" do
+ it "returns 1 if the first argument is a point in time after the second argument" do
+ (Time.now <=> Time.at(0)).should == 1
+ end
+
+ it "returns 1 if the first argument is a point in time after the second argument (down to a millisecond)" do
+ (Time.at(0, 1000) <=> Time.at(0, 0)).should == 1
+ (Time.at(1202778512, 1000) <=> Time.at(1202778512, 999)).should == 1
+ end
+
+ it "returns 1 if the first argument is a point in time after the second argument (down to a microsecond)" do
+ (Time.at(0, 100) <=> Time.at(0, 0)).should == 1
+ (Time.at(1202778512, 100) <=> Time.at(1202778512, 99)).should == 1
+ end
+
+ it "returns 0 if time is the same as other" do
+ (Time.at(1202778513) <=> Time.at(1202778513)).should == 0
+ (Time.at(100, 100) <=> Time.at(100, 100)).should == 0
+ end
+
+ it "returns -1 if the first argument is a point in time before the second argument" do
+ (Time.at(0) <=> Time.now).should == -1
+ (Time.at(100, 100) <=> Time.at(101, 100)).should == -1
+ end
+
+ it "returns -1 if the first argument is a point in time before the second argument (down to a millisecond)" do
+ (Time.at(0, 0) <=> Time.at(0, 1000)).should == -1
+ end
+
+ it "returns -1 if the first argument is a point in time before the second argument (down to a microsecond)" do
+ (Time.at(0, 0) <=> Time.at(0, 100)).should == -1
+ end
+
+ it "returns 1 if the first argument is a fraction of a microsecond after the second argument" do
+ (Time.at(100, Rational(1,1000)) <=> Time.at(100, 0)).should == 1
+ end
+
+ it "returns 0 if time is the same as other, including fractional microseconds" do
+ (Time.at(100, Rational(1,1000)) <=> Time.at(100, Rational(1,1000))).should == 0
+ end
+
+ it "returns -1 if the first argument is a fraction of a microsecond before the second argument" do
+ (Time.at(100, 0) <=> Time.at(100, Rational(1,1000))).should == -1
+ end
+
+ it "returns nil when compared to an Integer because Time does not respond to #coerce" do
+ time = Time.at(1)
+ time.respond_to?(:coerce).should == false
+ time.should_receive(:respond_to?).exactly(2).and_return(false)
+ -> {
+ (time <=> 2).should == nil
+ (2 <=> time).should == nil
+ }.should_not complain
+ end
+
+ context "given different timezones" do
+ it "returns 0 if time is the same as other" do
+ # three timezones, all at the same timestamp
+ time_utc = Time.new(2000, 1, 1, 0, 0, 0, 0)
+ time_cet = Time.new(2000, 1, 1, 1, 0, 0, '+01:00')
+ time_brt = Time.new(1999, 12, 31, 21, 0, 0, '-03:00')
+ (time_utc <=> time_cet).should == 0
+ (time_utc <=> time_brt).should == 0
+ (time_cet <=> time_brt).should == 0
+ end
+
+ it "returns -1 if the first argument is before the second argument" do
+ # time_brt is later, even though the date is earlier
+ time_utc = Time.new(2000, 1, 1, 0, 0, 0, 0)
+ time_brt = Time.new(1999, 12, 31, 23, 0, 0, '-03:00')
+ (time_utc <=> time_brt).should == -1
+ end
+
+ it "returns 1 if the first argument is before the second argument" do
+ # time_brt is later, even though the date is earlier
+ time_utc = Time.new(2000, 1, 1, 0, 0, 0, 0)
+ time_brt = Time.new(1999, 12, 31, 23, 0, 0, '-03:00')
+ (time_brt <=> time_utc).should == 1
+ end
+ end
+
+ describe "given a non-Time argument" do
+ it "returns nil if argument <=> self returns nil" do
+ t = Time.now
+ obj = mock('time')
+ obj.should_receive(:<=>).with(t).and_return(nil)
+ (t <=> obj).should == nil
+ end
+
+ it "returns -1 if argument <=> self is greater than 0" do
+ t = Time.now
+ r = mock('r')
+ r.should_receive(:>).with(0).and_return(true)
+ obj = mock('time')
+ obj.should_receive(:<=>).with(t).and_return(r)
+ (t <=> obj).should == -1
+ end
+
+ it "returns 1 if argument <=> self is not greater than 0 and is less than 0" do
+ t = Time.now
+ r = mock('r')
+ r.should_receive(:>).with(0).and_return(false)
+ r.should_receive(:<).with(0).and_return(true)
+ obj = mock('time')
+ obj.should_receive(:<=>).with(t).and_return(r)
+ (t <=> obj).should == 1
+ end
+
+ it "returns 0 if argument <=> self is neither greater than 0 nor less than 0" do
+ t = Time.now
+ r = mock('r')
+ r.should_receive(:>).with(0).and_return(false)
+ r.should_receive(:<).with(0).and_return(false)
+ obj = mock('time')
+ obj.should_receive(:<=>).with(t).and_return(r)
+ (t <=> obj).should == 0
+ end
+
+ it "returns nil if argument also uses an inverse comparison for <=>" do
+ t = Time.now
+ r = mock('r')
+ def r.<=>(other); other <=> self; end
+ r.should_receive(:<=>).once
+
+ (t <=> r).should == nil
+ end
+ end
+end
diff --git a/spec/ruby/core/time/ctime_spec.rb b/spec/ruby/core/time/ctime_spec.rb
new file mode 100644
index 0000000000..57e7cfd9ce
--- /dev/null
+++ b/spec/ruby/core/time/ctime_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/asctime'
+
+describe "Time#ctime" do
+ it_behaves_like :time_asctime, :ctime
+end
diff --git a/spec/ruby/core/time/day_spec.rb b/spec/ruby/core/time/day_spec.rb
new file mode 100644
index 0000000000..895bcd7a86
--- /dev/null
+++ b/spec/ruby/core/time/day_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/day'
+
+describe "Time#day" do
+ it_behaves_like :time_day, :day
+end
diff --git a/spec/ruby/core/time/deconstruct_keys_spec.rb b/spec/ruby/core/time/deconstruct_keys_spec.rb
new file mode 100644
index 0000000000..9918728c1d
--- /dev/null
+++ b/spec/ruby/core/time/deconstruct_keys_spec.rb
@@ -0,0 +1,43 @@
+require_relative '../../spec_helper'
+
+describe "Time#deconstruct_keys" do
+ it "returns whole hash for nil as an argument" do
+ d = Time.utc(2022, 10, 5, 13, 30)
+ res = { year: 2022, month: 10, day: 5, yday: 278, wday: 3, hour: 13,
+ min: 30, sec: 0, subsec: 0, dst: false, zone: "UTC" }
+ d.deconstruct_keys(nil).should == res
+ end
+
+ it "returns only specified keys" do
+ d = Time.utc(2022, 10, 5, 13, 39)
+ d.deconstruct_keys([:zone, :subsec]).should == { zone: "UTC", subsec: 0 }
+ end
+
+ it "requires one argument" do
+ -> {
+ Time.new(2022, 10, 5, 13, 30).deconstruct_keys
+ }.should.raise(ArgumentError)
+ end
+
+ it "it raises error when argument is neither nil nor array" do
+ d = Time.new(2022, 10, 5, 13, 30)
+
+ -> { d.deconstruct_keys(1) }.should.raise(TypeError, "wrong argument type Integer (expected Array or nil)")
+ -> { d.deconstruct_keys("asd") }.should.raise(TypeError, "wrong argument type String (expected Array or nil)")
+ -> { d.deconstruct_keys(:x) }.should.raise(TypeError, "wrong argument type Symbol (expected Array or nil)")
+ -> { d.deconstruct_keys({}) }.should.raise(TypeError, "wrong argument type Hash (expected Array or nil)")
+ end
+
+ it "returns {} when passed []" do
+ Time.new(2022, 10, 5, 13, 30).deconstruct_keys([]).should == {}
+ end
+
+ it "ignores non-Symbol keys" do
+ Time.new(2022, 10, 5, 13, 30).deconstruct_keys(['year', []]).should == {}
+ end
+
+ it "ignores not existing Symbol keys and processes keys after the first non-existing one" do
+ d = Time.utc(2022, 10, 5, 13, 30)
+ d.deconstruct_keys([:year, :a, :month, :b, :day]).should == { year: 2022, month: 10, day: 5 }
+ end
+end
diff --git a/spec/ruby/core/time/dst_spec.rb b/spec/ruby/core/time/dst_spec.rb
new file mode 100644
index 0000000000..436240aae5
--- /dev/null
+++ b/spec/ruby/core/time/dst_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/isdst'
+
+describe "Time#dst?" do
+ it_behaves_like :time_isdst, :dst?
+end
diff --git a/spec/ruby/core/time/dup_spec.rb b/spec/ruby/core/time/dup_spec.rb
new file mode 100644
index 0000000000..33aa1304ef
--- /dev/null
+++ b/spec/ruby/core/time/dup_spec.rb
@@ -0,0 +1,46 @@
+require_relative '../../spec_helper'
+
+describe "Time#dup" do
+ it "returns a Time object that represents the same time" do
+ t = Time.at(100)
+ t.dup.tv_sec.should == t.tv_sec
+ end
+
+ it "copies the gmt state flag" do
+ Time.now.gmtime.dup.should.gmt?
+ end
+
+ it "returns an independent Time object" do
+ t = Time.now
+ t2 = t.dup
+ t.gmtime
+
+ t2.should_not.gmt?
+ end
+
+ it "returns a subclass instance" do
+ c = Class.new(Time)
+ t = c.now
+
+ t.should.instance_of?(c)
+ t.dup.should.instance_of?(c)
+ end
+
+ it "returns a clone of Time instance" do
+ c = Time.dup
+ t = c.now
+
+ t.should.instance_of?(c)
+ t.should_not.instance_of?(Time)
+
+ t.dup.should.instance_of?(c)
+ t.dup.should_not.instance_of?(Time)
+ end
+
+ it "does not copy frozen status from the original" do
+ t = Time.now
+ t.freeze
+ t2 = t.dup
+ t2.frozen?.should == false
+ end
+end
diff --git a/spec/ruby/core/time/eql_spec.rb b/spec/ruby/core/time/eql_spec.rb
new file mode 100644
index 0000000000..b7505969dd
--- /dev/null
+++ b/spec/ruby/core/time/eql_spec.rb
@@ -0,0 +1,29 @@
+require_relative '../../spec_helper'
+
+describe "Time#eql?" do
+ it "returns true if self and other have the same whole number of seconds" do
+ Time.at(100).should.eql?(Time.at(100))
+ end
+
+ it "returns false if self and other have differing whole numbers of seconds" do
+ Time.at(100).should_not.eql?(Time.at(99))
+ end
+
+ it "returns true if self and other have the same number of microseconds" do
+ Time.at(100, 100).should.eql?(Time.at(100, 100))
+ end
+
+ it "returns false if self and other have differing numbers of microseconds" do
+ Time.at(100, 100).should_not.eql?(Time.at(100, 99))
+ end
+
+ it "returns false if self and other have differing fractional microseconds" do
+ Time.at(100, Rational(100,1000)).should_not.eql?(Time.at(100, Rational(99,1000)))
+ end
+
+ it "returns false when given a non-time value" do
+ Time.at(100, 100).should_not.eql?("100")
+ Time.at(100, 100).should_not.eql?(100)
+ Time.at(100, 100).should_not.eql?(100.1)
+ end
+end
diff --git a/spec/ruby/core/time/fixtures/classes.rb b/spec/ruby/core/time/fixtures/classes.rb
new file mode 100644
index 0000000000..21c4e1effb
--- /dev/null
+++ b/spec/ruby/core/time/fixtures/classes.rb
@@ -0,0 +1,105 @@
+module TimeSpecs
+
+ class SubTime < Time; end
+
+ class MethodHolder
+ class << self
+ define_method(:now, &Time.method(:now))
+ define_method(:new, &Time.method(:new))
+ end
+ end
+
+ class Timezone
+ def initialize(options)
+ @offset = options[:offset]
+ end
+
+ def local_to_utc(t)
+ t - @offset
+ end
+
+ def utc_to_local(t)
+ t + @offset
+ end
+ end
+
+ class TimezoneMethodCallRecorder < Timezone
+ def initialize(options, &blk)
+ super(options)
+ @blk = blk
+ end
+
+ def local_to_utc(t)
+ @blk.call(t)
+ super
+ end
+
+ def utc_to_local(t)
+ @blk.call(t)
+ super
+ end
+ end
+
+ class TimeLikeArgumentRecorder
+ def self.result
+ arguments = []
+
+ zone = TimeSpecs::TimezoneMethodCallRecorder.new(offset: 0) do |obj|
+ arguments << obj
+ end
+
+ # ensure timezone's methods are called at least once
+ Time.new(2000, 1, 1, 12, 0, 0, zone)
+
+ return arguments[0]
+ end
+ end
+
+ Z = Struct.new(:offset, :abbr)
+ Zone = Struct.new(:std, :dst, :dst_range)
+ Zones = {
+ "Asia/Colombo" => Zone[Z[5*3600+30*60, "MMT"], nil, nil],
+ "PST" => Zone[Z[(-9*60*60), "PST"], nil, nil],
+ }
+
+ class TimezoneWithName < Timezone
+ attr_reader :name
+
+ def initialize(options)
+ @name = options[:name]
+ @std, @dst, @dst_range = *Zones[@name]
+ end
+
+ def dst?(t)
+ @dst_range&.cover?(t.mon)
+ end
+
+ def zone(t)
+ (dst?(t) ? @dst : @std)
+ end
+
+ def utc_offset(t)
+ zone(t)&.offset || 0
+ end
+
+ def abbr(t)
+ zone(t)&.abbr
+ end
+
+ def local_to_utc(t)
+ t - utc_offset(t)
+ end
+
+ def utc_to_local(t)
+ t + utc_offset(t)
+ end
+ end
+
+ class TimeWithFindTimezone < Time
+ def self.find_timezone(name)
+ TimezoneWithName.new(name: name.to_s)
+ end
+ end
+
+ TimezoneWithAbbr = TimezoneWithName
+end
diff --git a/spec/ruby/core/time/floor_spec.rb b/spec/ruby/core/time/floor_spec.rb
new file mode 100644
index 0000000000..41e5142b19
--- /dev/null
+++ b/spec/ruby/core/time/floor_spec.rb
@@ -0,0 +1,36 @@
+require_relative '../../spec_helper'
+
+describe "Time#floor" do
+ before do
+ @time = Time.utc(2010, 3, 30, 5, 43, "25.123456789".to_r)
+ end
+
+ it "defaults to flooring to 0 places" do
+ @time.floor.should == Time.utc(2010, 3, 30, 5, 43, 25.to_r)
+ end
+
+ it "floors to 0 decimal places with an explicit argument" do
+ @time.floor(0).should == Time.utc(2010, 3, 30, 5, 43, 25.to_r)
+ end
+
+ it "floors to 7 decimal places with an explicit argument" do
+ @time.floor(7).should == Time.utc(2010, 3, 30, 5, 43, "25.1234567".to_r)
+ end
+
+ it "returns an instance of Time, even if #floor is called on a subclass" do
+ subclass = Class.new(Time)
+ instance = subclass.at(0)
+ instance.class.should.equal? subclass
+ instance.floor.should.instance_of?(Time)
+ end
+
+ it "copies own timezone to the returning value" do
+ @time.zone.should == @time.floor.zone
+
+ time = with_timezone "JST-9" do
+ Time.at 0, 1
+ end
+
+ time.zone.should == time.floor.zone
+ end
+end
diff --git a/spec/ruby/core/time/friday_spec.rb b/spec/ruby/core/time/friday_spec.rb
new file mode 100644
index 0000000000..8bee7f7558
--- /dev/null
+++ b/spec/ruby/core/time/friday_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Time#friday?" do
+ it "returns true if time represents Friday" do
+ Time.local(2000, 1, 7).should.friday?
+ end
+
+ it "returns false if time doesn't represent Friday" do
+ Time.local(2000, 1, 1).should_not.friday?
+ end
+end
diff --git a/spec/ruby/core/time/getgm_spec.rb b/spec/ruby/core/time/getgm_spec.rb
new file mode 100644
index 0000000000..b5d45b1d9f
--- /dev/null
+++ b/spec/ruby/core/time/getgm_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/getgm'
+
+describe "Time#getgm" do
+ it_behaves_like :time_getgm, :getgm
+end
diff --git a/spec/ruby/core/time/getlocal_spec.rb b/spec/ruby/core/time/getlocal_spec.rb
new file mode 100644
index 0000000000..7e5334c303
--- /dev/null
+++ b/spec/ruby/core/time/getlocal_spec.rb
@@ -0,0 +1,206 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Time#getlocal" do
+ it "returns a new time which is the local representation of time" do
+ # Testing with America/Regina here because it doesn't have DST.
+ with_timezone("CST", -6) do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime.should == Time.local(2007, 1, 9, 6, 0, 0)
+ end
+ end
+
+ it "returns a Time with UTC offset specified as an Integer number of seconds" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(3630)
+ t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630)
+ t.utc_offset.should == 3630
+ t.zone.should == nil
+ end
+
+ platform_is_not :windows do
+ it "returns a new time with the correct utc_offset according to the set timezone" do
+ t = Time.new(2005, 2, 27, 22, 50, 0, -3600)
+ t.utc_offset.should == -3600
+
+ with_timezone("America/New_York") do
+ t.getlocal.utc_offset.should == -18000
+ end
+ end
+ end
+
+ describe "with an argument that responds to #to_int" do
+ it "coerces using #to_int" do
+ o = mock('integer')
+ o.should_receive(:to_int).and_return(3630)
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630)
+ t.utc_offset.should == 3630
+ end
+ end
+
+ it "returns a Time with a UTC offset of the specified number of Rational seconds" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(Rational(7201, 2))
+ t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2))
+ t.utc_offset.should.eql?(Rational(7201, 2))
+ end
+
+ describe "with an argument that responds to #to_r" do
+ it "coerces using #to_r" do
+ o = mock_numeric('rational')
+ o.should_receive(:to_r).and_return(Rational(7201, 2))
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2))
+ t.utc_offset.should.eql?(Rational(7201, 2))
+ end
+ end
+
+ it "returns a Time with a UTC offset specified as +HH:MM" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("+01:00")
+ t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600)
+ t.utc_offset.should == 3600
+ end
+
+ it "returns a Time with a UTC offset specified as +HH:MM:SS" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("+01:00:01")
+ t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601)
+ t.utc_offset.should == 3601
+ end
+
+ it "returns a Time with a UTC offset specified as -HH:MM" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00")
+ t.should == Time.new(2007, 1, 9, 11, 0, 0, -3600)
+ t.utc_offset.should == -3600
+ end
+
+ it "returns a Time with a UTC offset specified as -HH:MM:SS" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal("-01:00:01")
+ t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601)
+ t.utc_offset.should == -3601
+ end
+
+ describe "with an argument that responds to #to_str" do
+ it "coerces using #to_str" do
+ o = mock('string')
+ o.should_receive(:to_str).and_return("+01:00")
+ t = Time.gm(2007, 1, 9, 12, 0, 0).getlocal(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600)
+ t.utc_offset.should == 3600
+ end
+ end
+
+ it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do
+ t = Time.now
+ -> { t.getlocal("3600") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the String argument is not in an ASCII-compatible encoding" do
+ t = Time.now
+ -> { t.getlocal("-01:00".encode("UTF-16LE")) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the argument represents a value less than or equal to -86400 seconds" do
+ t = Time.new
+ t.getlocal(-86400 + 1).utc_offset.should == (-86400 + 1)
+ -> { t.getlocal(-86400) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds" do
+ t = Time.new
+ t.getlocal(86400 - 1).utc_offset.should == (86400 - 1)
+ -> { t.getlocal(86400) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if String argument and hours greater than 23" do
+ -> { Time.now.getlocal("+24:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.now.getlocal("+2400") }.should.raise(ArgumentError, "utc_offset out of range")
+
+ -> { Time.now.getlocal("+99:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.now.getlocal("+9900") }.should.raise(ArgumentError, "utc_offset out of range")
+ end
+
+ it "raises ArgumentError if String argument and minutes greater than 59" do
+ -> { Time.now.getlocal("+00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60')
+ -> { Time.now.getlocal("+0060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060')
+
+ -> { Time.now.getlocal("+00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99')
+ -> { Time.now.getlocal("+0099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099')
+ end
+
+ ruby_bug '#20797', ''...'3.4' do
+ it "raises ArgumentError if String argument and seconds greater than 59" do
+ -> { Time.now.getlocal("+00:00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60')
+ -> { Time.now.getlocal("+000060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060')
+
+ -> { Time.now.getlocal("+00:00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99')
+ -> { Time.now.getlocal("+000099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099')
+ end
+ end
+
+ describe "with a timezone argument" do
+ it "returns a Time in the timezone" do
+ zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60))
+ time = Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone)
+
+ time.zone.should == zone
+ time.utc_offset.should == 5*3600+30*60
+ end
+
+ it "accepts timezone argument that must have #local_to_utc and #utc_to_local methods" do
+ zone = Object.new
+ def zone.utc_to_local(time)
+ time
+ end
+ def zone.local_to_utc(time)
+ time
+ end
+
+ -> {
+ Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone).should.is_a?(Time)
+ }.should_not.raise
+ end
+
+ it "raises TypeError if timezone does not implement #utc_to_local method" do
+ zone = Object.new
+ def zone.local_to_utc(time)
+ time
+ end
+
+ -> {
+ Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone)
+ }.should.raise(TypeError, /can't convert \w+ into an exact number/)
+ end
+
+ it "does not raise exception if timezone does not implement #local_to_utc method" do
+ zone = Object.new
+ def zone.utc_to_local(time)
+ time
+ end
+
+ -> {
+ Time.utc(2000, 1, 1, 12, 0, 0).getlocal(zone).should.is_a?(Time)
+ }.should_not.raise
+ end
+
+ context "subject's class implements .find_timezone method" do
+ it "calls .find_timezone to build a time object if passed zone name as a timezone argument" do
+ time = TimeSpecs::TimeWithFindTimezone.utc(2000, 1, 1, 12, 0, 0).getlocal("Asia/Colombo")
+ time.zone.should.is_a? TimeSpecs::TimezoneWithName
+ time.zone.name.should == "Asia/Colombo"
+
+ time = TimeSpecs::TimeWithFindTimezone.utc(2000, 1, 1, 12, 0, 0).getlocal("some invalid zone name")
+ time.zone.should.is_a? TimeSpecs::TimezoneWithName
+ time.zone.name.should == "some invalid zone name"
+ end
+
+ it "does not call .find_timezone if passed any not string/numeric/timezone timezone argument" do
+ [Object.new, [], {}, :"some zone"].each do |zone|
+ time = TimeSpecs::TimeWithFindTimezone.utc(2000, 1, 1, 12, 0, 0)
+
+ -> {
+ time.getlocal(zone)
+ }.should.raise(TypeError, /can't convert \w+ into an exact number/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/time/getutc_spec.rb b/spec/ruby/core/time/getutc_spec.rb
new file mode 100644
index 0000000000..0cd9c17b00
--- /dev/null
+++ b/spec/ruby/core/time/getutc_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/getgm'
+
+describe "Time#getutc" do
+ it_behaves_like :time_getgm, :getutc
+end
diff --git a/spec/ruby/core/time/gm_spec.rb b/spec/ruby/core/time/gm_spec.rb
new file mode 100644
index 0000000000..26dffbcedc
--- /dev/null
+++ b/spec/ruby/core/time/gm_spec.rb
@@ -0,0 +1,10 @@
+require_relative '../../spec_helper'
+require_relative 'shared/gm'
+require_relative 'shared/time_params'
+
+describe "Time.gm" do
+ it_behaves_like :time_gm, :gm
+ it_behaves_like :time_params, :gm
+ it_behaves_like :time_params_10_arg, :gm
+ it_behaves_like :time_params_microseconds, :gm
+end
diff --git a/spec/ruby/core/time/gmt_offset_spec.rb b/spec/ruby/core/time/gmt_offset_spec.rb
new file mode 100644
index 0000000000..df417e6e4e
--- /dev/null
+++ b/spec/ruby/core/time/gmt_offset_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/gmt_offset'
+
+describe "Time#gmt_offset" do
+ it_behaves_like :time_gmt_offset, :gmt_offset
+end
diff --git a/spec/ruby/core/time/gmt_spec.rb b/spec/ruby/core/time/gmt_spec.rb
new file mode 100644
index 0000000000..840f59e0e8
--- /dev/null
+++ b/spec/ruby/core/time/gmt_spec.rb
@@ -0,0 +1,8 @@
+require_relative '../../spec_helper'
+
+describe "Time#gmt?" do
+ it "returns true if time represents a time in UTC (GMT)" do
+ Time.now.should_not.gmt?
+ Time.now.gmtime.should.gmt?
+ end
+end
diff --git a/spec/ruby/core/time/gmtime_spec.rb b/spec/ruby/core/time/gmtime_spec.rb
new file mode 100644
index 0000000000..d965cd541d
--- /dev/null
+++ b/spec/ruby/core/time/gmtime_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/gmtime'
+
+describe "Time#gmtime" do
+ it_behaves_like :time_gmtime, :gmtime
+end
diff --git a/spec/ruby/core/time/gmtoff_spec.rb b/spec/ruby/core/time/gmtoff_spec.rb
new file mode 100644
index 0000000000..fa28520e9e
--- /dev/null
+++ b/spec/ruby/core/time/gmtoff_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/gmt_offset'
+
+describe "Time#gmtoff" do
+ it_behaves_like :time_gmt_offset, :gmtoff
+end
diff --git a/spec/ruby/core/time/hash_spec.rb b/spec/ruby/core/time/hash_spec.rb
new file mode 100644
index 0000000000..1cfc56eab0
--- /dev/null
+++ b/spec/ruby/core/time/hash_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Time#hash" do
+ it "returns an Integer" do
+ Time.at(100).hash.should.instance_of?(Integer)
+ end
+
+ it "is stable" do
+ Time.at(1234).hash.should == Time.at(1234).hash
+ end
+end
diff --git a/spec/ruby/core/time/hour_spec.rb b/spec/ruby/core/time/hour_spec.rb
new file mode 100644
index 0000000000..ca69c25adb
--- /dev/null
+++ b/spec/ruby/core/time/hour_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Time#hour" do
+ it "returns the hour of the day (0..23) for a local Time" do
+ with_timezone("CET", 1) do
+ Time.local(1970, 1, 1, 1).hour.should == 1
+ end
+ end
+
+ it "returns the hour of the day for a UTC Time" do
+ Time.utc(1970, 1, 1, 0).hour.should == 0
+ end
+
+ it "returns the hour of the day for a Time with a fixed offset" do
+ Time.new(2012, 1, 1, 0, 0, 0, -3600).hour.should == 0
+ end
+end
diff --git a/spec/ruby/core/time/inspect_spec.rb b/spec/ruby/core/time/inspect_spec.rb
new file mode 100644
index 0000000000..c3a4519a24
--- /dev/null
+++ b/spec/ruby/core/time/inspect_spec.rb
@@ -0,0 +1,33 @@
+require_relative '../../spec_helper'
+require_relative 'shared/inspect'
+
+describe "Time#inspect" do
+ it_behaves_like :inspect, :inspect
+
+ it "preserves microseconds" do
+ t = Time.utc(2007, 11, 1, 15, 25, 0, 123456)
+ t.inspect.should == "2007-11-01 15:25:00.123456 UTC"
+ end
+
+ it "omits trailing zeros from microseconds" do
+ t = Time.utc(2007, 11, 1, 15, 25, 0, 100000)
+ t.inspect.should == "2007-11-01 15:25:00.1 UTC"
+ end
+
+ it "uses the correct time zone without microseconds" do
+ t = Time.utc(2000, 1, 1)
+ t = t.localtime(9*3600)
+ t.inspect.should == "2000-01-01 09:00:00 +0900"
+ end
+
+ it "uses the correct time zone with microseconds" do
+ t = Time.utc(2000, 1, 1, 0, 0, 0, 123456)
+ t = t.localtime(9*3600)
+ t.inspect.should == "2000-01-01 09:00:00.123456 +0900"
+ end
+
+ it "preserves nanoseconds" do
+ t = Time.utc(2007, 11, 1, 15, 25, 0, 123456.789r)
+ t.inspect.should == "2007-11-01 15:25:00.123456789 UTC"
+ end
+end
diff --git a/spec/ruby/core/time/isdst_spec.rb b/spec/ruby/core/time/isdst_spec.rb
new file mode 100644
index 0000000000..173230ca07
--- /dev/null
+++ b/spec/ruby/core/time/isdst_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/isdst'
+
+describe "Time#isdst" do
+ it_behaves_like :time_isdst, :isdst
+end
diff --git a/spec/ruby/core/time/iso8601_spec.rb b/spec/ruby/core/time/iso8601_spec.rb
new file mode 100644
index 0000000000..ad60c3bb32
--- /dev/null
+++ b/spec/ruby/core/time/iso8601_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/xmlschema'
+
+describe "Time#iso8601" do
+ it_behaves_like :time_xmlschema, :iso8601
+end
diff --git a/spec/ruby/core/time/local_spec.rb b/spec/ruby/core/time/local_spec.rb
new file mode 100644
index 0000000000..581ed171d5
--- /dev/null
+++ b/spec/ruby/core/time/local_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'shared/local'
+require_relative 'shared/time_params'
+
+describe "Time.local" do
+ it_behaves_like :time_local, :local
+ it_behaves_like :time_local_10_arg, :local
+ it_behaves_like :time_params, :local
+ it_behaves_like :time_params_10_arg, :local
+ it_behaves_like :time_params_microseconds, :local
+end
diff --git a/spec/ruby/core/time/localtime_spec.rb b/spec/ruby/core/time/localtime_spec.rb
new file mode 100644
index 0000000000..1c0b11b7a6
--- /dev/null
+++ b/spec/ruby/core/time/localtime_spec.rb
@@ -0,0 +1,203 @@
+require_relative '../../spec_helper'
+
+describe "Time#localtime" do
+ it "converts self to local time, modifying the receiver" do
+ # Testing with America/Regina here because it doesn't have DST.
+ with_timezone("CST", -6) do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime
+ t.should == Time.local(2007, 1, 9, 6, 0, 0)
+ end
+ end
+
+ it "returns self" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime.should.equal?(t)
+ end
+
+ it "converts time to the UTC offset specified as an Integer number of seconds" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime(3630)
+ t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630)
+ t.utc_offset.should == 3630
+ end
+
+ describe "on a frozen time" do
+ it "does not raise an error if already in the right time zone" do
+ time = Time.now
+ time.freeze
+ time.localtime.should.equal?(time)
+ end
+
+ it "raises a FrozenError if the time has a different time zone" do
+ time = Time.gm(2007, 1, 9, 12, 0, 0)
+ time.freeze
+ -> { time.localtime }.should.raise(FrozenError)
+ end
+ end
+
+ describe "with an argument that responds to #to_int" do
+ it "coerces using #to_int" do
+ o = mock('integer')
+ o.should_receive(:to_int).and_return(3630)
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, 30, 3630)
+ t.utc_offset.should == 3630
+ end
+ end
+
+ it "returns a Time with a UTC offset of the specified number of Rational seconds" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime(Rational(7201, 2))
+ t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2))
+ t.utc_offset.should.eql?(Rational(7201, 2))
+ end
+
+ describe "with an argument that responds to #to_r" do
+ it "coerces using #to_r" do
+ o = mock_numeric('rational')
+ o.should_receive(:to_r).and_return(Rational(7201, 2))
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, Rational(1, 2), Rational(7201, 2))
+ t.utc_offset.should.eql?(Rational(7201, 2))
+ end
+ end
+
+ it "returns a Time with a UTC offset specified as +HH:MM" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime("+01:00")
+ t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600)
+ t.utc_offset.should == 3600
+ end
+
+ it "returns a Time with a UTC offset specified as +HH:MM:SS" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime("+01:00:01")
+ t.should == Time.new(2007, 1, 9, 13, 0, 1, 3601)
+ t.utc_offset.should == 3601
+ end
+
+ it "returns a Time with a UTC offset specified as -HH:MM" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime("-01:00")
+ t.should == Time.new(2007, 1, 9, 11, 0, 0, -3600)
+ t.utc_offset.should == -3600
+ end
+
+ it "returns a Time with a UTC offset specified as -HH:MM:SS" do
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime("-01:00:01")
+ t.should == Time.new(2007, 1, 9, 10, 59, 59, -3601)
+ t.utc_offset.should == -3601
+ end
+
+ it "returns a Time with a UTC offset specified as UTC" do
+ t = Time.new(2007, 1, 9, 12, 0, 0, 3600)
+ t.localtime("UTC")
+ t.utc_offset.should == 0
+ end
+
+ it "returns a Time with a UTC offset specified as A-Z military zone" do
+ t = Time.new(2007, 1, 9, 12, 0, 0, 3600)
+ t.localtime("B")
+ t.utc_offset.should == 3600 * 2
+ end
+
+ it "raises ArgumentError if String argument and hours greater than 23" do
+ -> { Time.now.localtime("+24:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.now.localtime("+2400") }.should.raise(ArgumentError, "utc_offset out of range")
+
+ -> { Time.now.localtime("+99:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.now.localtime("+9900") }.should.raise(ArgumentError, "utc_offset out of range")
+ end
+
+ it "raises ArgumentError if String argument and minutes greater than 59" do
+ -> { Time.now.localtime("+00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60')
+ -> { Time.now.localtime("+0060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060')
+
+ -> { Time.now.localtime("+00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99')
+ -> { Time.now.localtime("+0099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099')
+ end
+
+ ruby_bug '#20797', ''...'3.4' do
+ it "raises ArgumentError if String argument and seconds greater than 59" do
+ -> { Time.now.localtime("+00:00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60')
+ -> { Time.now.localtime("+000060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060')
+
+ -> { Time.now.localtime("+00:00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99')
+ -> { Time.now.localtime("+000099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099')
+ end
+ end
+
+ platform_is_not :windows do
+ it "changes the timezone according to the set one" do
+ t = Time.new(2005, 2, 27, 22, 50, 0, -3600)
+ t.utc_offset.should == -3600
+
+ with_timezone("America/New_York") do
+ t.localtime
+ end
+
+ t.utc_offset.should == -18000
+ end
+
+ it "does nothing if already in a local time zone" do
+ time = with_timezone("America/New_York") do
+ Time.new(2005, 2, 27, 22, 50, 0)
+ end
+ zone = time.zone
+
+ with_timezone("Europe/Amsterdam") do
+ time.localtime
+ end
+
+ time.zone.should == zone
+ end
+ end
+
+ describe "with an argument that responds to #to_str" do
+ it "coerces using #to_str" do
+ o = mock('string')
+ o.should_receive(:to_str).and_return("+01:00")
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600)
+ t.utc_offset.should == 3600
+ end
+ end
+
+ describe "with an argument that responds to #utc_to_local" do
+ it "coerces using #utc_to_local" do
+ o = mock('string')
+ o.should_receive(:utc_to_local).and_return(Time.new(2007, 1, 9, 13, 0, 0, 3600))
+ t = Time.gm(2007, 1, 9, 12, 0, 0)
+ t.localtime(o)
+ t.should == Time.new(2007, 1, 9, 13, 0, 0, 3600)
+ t.utc_offset.should == 3600
+ end
+ end
+
+ it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do
+ t = Time.now
+ -> { t.localtime("3600") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the String argument is not in an ASCII-compatible encoding" do
+ t = Time.now
+ -> { t.localtime("-01:00".encode("UTF-16LE")) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the argument represents a value less than or equal to -86400 seconds" do
+ t = Time.new
+ t.localtime(-86400 + 1).utc_offset.should == (-86400 + 1)
+ -> { t.localtime(-86400) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds" do
+ t = Time.new
+ t.localtime(86400 - 1).utc_offset.should == (86400 - 1)
+ -> { t.localtime(86400) }.should.raise(ArgumentError)
+ end
+end
diff --git a/spec/ruby/core/time/mday_spec.rb b/spec/ruby/core/time/mday_spec.rb
new file mode 100644
index 0000000000..3c21939890
--- /dev/null
+++ b/spec/ruby/core/time/mday_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/day'
+
+describe "Time#mday" do
+ it_behaves_like :time_day, :mday
+end
diff --git a/spec/ruby/core/time/min_spec.rb b/spec/ruby/core/time/min_spec.rb
new file mode 100644
index 0000000000..7d087d4046
--- /dev/null
+++ b/spec/ruby/core/time/min_spec.rb
@@ -0,0 +1,17 @@
+require_relative '../../spec_helper'
+
+describe "Time#min" do
+ it "returns the minute of the hour (0..59) for a local Time" do
+ with_timezone("CET", 1) do
+ Time.local(1970, 1, 1, 0, 0).min.should == 0
+ end
+ end
+
+ it "returns the minute of the hour for a UTC Time" do
+ Time.utc(1970, 1, 1, 0, 0).min.should == 0
+ end
+
+ it "returns the minute of the hour for a Time with a fixed offset" do
+ Time.new(2012, 1, 1, 0, 0, 0, -3600).min.should == 0
+ end
+end
diff --git a/spec/ruby/core/time/minus_spec.rb b/spec/ruby/core/time/minus_spec.rb
new file mode 100644
index 0000000000..ee3d8acda8
--- /dev/null
+++ b/spec/ruby/core/time/minus_spec.rb
@@ -0,0 +1,121 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Time#-" do
+ it "decrements the time by the specified amount" do
+ (Time.at(100) - 100).should == Time.at(0)
+ (Time.at(100) - Time.at(99)).should == 1.0
+ end
+
+ it "understands negative subtractions" do
+ t = Time.at(100) - -1.3
+ t.usec.should == 300000
+ t.to_i.should == 101
+ end
+
+ #see [ruby-dev:38446]
+ it "accepts arguments that can be coerced into Rational" do
+ (obj = mock_numeric('10')).should_receive(:to_r).and_return(Rational(10))
+ (Time.at(100) - obj).should == Time.at(90)
+ end
+
+ it "raises a TypeError if given argument is a coercible String" do
+ -> { Time.now - "1" }.should.raise(TypeError)
+ -> { Time.now - "0.1" }.should.raise(TypeError)
+ -> { Time.now - "1/3" }.should.raise(TypeError)
+ end
+
+ it "raises TypeError on argument that can't be coerced" do
+ -> { Time.now - Object.new }.should.raise(TypeError)
+ -> { Time.now - "stuff" }.should.raise(TypeError)
+ end
+
+ it "raises TypeError on nil argument" do
+ -> { Time.now - nil }.should.raise(TypeError)
+ end
+
+ it "tracks microseconds" do
+ time = Time.at(0.777777)
+ time -= 0.654321
+ time.usec.should == 123456
+ time -= 1
+ time.usec.should == 123456
+ end
+
+ it "tracks microseconds from a Rational" do
+ time = Time.at(Rational(777_777, 1_000_000))
+ time -= Rational(654_321, 1_000_000)
+ time.usec.should == 123_456
+ time -= Rational(123_456, 1_000_000)
+ time.usec.should == 0
+ end
+
+ it "tracks nanoseconds" do
+ time = Time.at(Rational(999_999_999, 1_000_000_000))
+ time -= Rational(876_543_210, 1_000_000_000)
+ time.nsec.should == 123_456_789
+ time -= Rational(123_456_789, 1_000_000_000)
+ time.nsec.should == 0
+ end
+
+ it "maintains precision" do
+ time = Time.at(10) - Rational(1_000_000_000_000_001, 1_000_000_000_000_000)
+ time.should_not == Time.at(9)
+ end
+
+ it "maintains microseconds precision" do
+ time = Time.at(10) - Rational(1, 1_000_000)
+ time.usec.should == 999_999
+ end
+
+ it "maintains nanoseconds precision" do
+ time = Time.at(10) - Rational(1, 1_000_000_000)
+ time.nsec.should == 999_999_999
+ end
+
+ it "maintains subseconds precision" do
+ time = Time.at(0) - Rational(1_000_000_000_000_001, 1_000_000_000_000_000)
+ time.subsec.should == Rational(999_999_999_999_999, 1_000_000_000_000_000)
+ end
+
+ it "returns a UTC time if self is UTC" do
+ (Time.utc(2012) - 10).should.utc?
+ end
+
+ it "returns a non-UTC time if self is non-UTC" do
+ (Time.local(2012) - 10).should_not.utc?
+ end
+
+ it "returns a time with the same fixed offset as self" do
+ (Time.new(2012, 1, 1, 0, 0, 0, 3600) - 10).utc_offset.should == 3600
+ end
+
+ it "preserves time zone" do
+ time_with_zone = Time.now.utc
+ time_with_zone.zone.should == (time_with_zone - 1).zone
+
+ time_with_zone = Time.now
+ time_with_zone.zone.should == (time_with_zone - 1).zone
+ end
+
+ context "zone is a timezone object" do
+ it "preserves time zone" do
+ zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60))
+ time = Time.new(2012, 1, 1, 12, 0, 0, zone) - 1
+
+ time.zone.should == zone
+ end
+ end
+
+ it "does not return a subclass instance" do
+ c = Class.new(Time)
+ x = c.now - 1
+ x.should.instance_of?(Time)
+ end
+
+ it "returns a time with nanoseconds precision between two time objects" do
+ time1 = Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000))
+ time2 = Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ (time1 - time2).should == 86_399.999999998
+ end
+end
diff --git a/spec/ruby/core/time/mktime_spec.rb b/spec/ruby/core/time/mktime_spec.rb
new file mode 100644
index 0000000000..78a6a6e772
--- /dev/null
+++ b/spec/ruby/core/time/mktime_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+require_relative 'shared/local'
+require_relative 'shared/time_params'
+
+describe "Time.mktime" do
+ it_behaves_like :time_local, :mktime
+ it_behaves_like :time_local_10_arg, :mktime
+ it_behaves_like :time_params, :mktime
+ it_behaves_like :time_params_10_arg, :mktime
+ it_behaves_like :time_params_microseconds, :mktime
+end
diff --git a/spec/ruby/core/time/mon_spec.rb b/spec/ruby/core/time/mon_spec.rb
new file mode 100644
index 0000000000..f41b39648b
--- /dev/null
+++ b/spec/ruby/core/time/mon_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/month'
+
+describe "Time#mon" do
+ it_behaves_like :time_month, :mon
+end
diff --git a/spec/ruby/core/time/monday_spec.rb b/spec/ruby/core/time/monday_spec.rb
new file mode 100644
index 0000000000..47ecaeb1db
--- /dev/null
+++ b/spec/ruby/core/time/monday_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Time#monday?" do
+ it "returns true if time represents Monday" do
+ Time.local(2000, 1, 3).should.monday?
+ end
+
+ it "returns false if time doesn't represent Monday" do
+ Time.local(2000, 1, 1).should_not.monday?
+ end
+end
diff --git a/spec/ruby/core/time/month_spec.rb b/spec/ruby/core/time/month_spec.rb
new file mode 100644
index 0000000000..81e20384ab
--- /dev/null
+++ b/spec/ruby/core/time/month_spec.rb
@@ -0,0 +1,6 @@
+require_relative '../../spec_helper'
+require_relative 'shared/month'
+
+describe "Time#month" do
+ it_behaves_like :time_month, :month
+end
diff --git a/spec/ruby/core/time/new_spec.rb b/spec/ruby/core/time/new_spec.rb
new file mode 100644
index 0000000000..91ce4b2e3a
--- /dev/null
+++ b/spec/ruby/core/time/new_spec.rb
@@ -0,0 +1,739 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+require_relative 'shared/now'
+require_relative 'shared/local'
+require_relative 'shared/time_params'
+
+describe "Time.new" do
+ it_behaves_like :time_now, :new
+end
+
+describe "Time.new" do
+ it_behaves_like :time_local, :new
+ it_behaves_like :time_params, :new
+end
+
+describe "Time.new with a utc_offset argument" do
+ it "returns a non-UTC time" do
+ Time.new(2000, 1, 1, 0, 0, 0, 0).should_not.utc?
+ end
+
+ it "returns a Time with a UTC offset of the specified number of Integer seconds" do
+ Time.new(2000, 1, 1, 0, 0, 0, 123).utc_offset.should == 123
+ end
+
+ describe "with an argument that responds to #to_int" do
+ it "coerces using #to_int" do
+ o = mock('integer')
+ o.should_receive(:to_int).and_return(123)
+ Time.new(2000, 1, 1, 0, 0, 0, o).utc_offset.should == 123
+ end
+ end
+
+ it "returns a Time with a UTC offset of the specified number of Rational seconds" do
+ Time.new(2000, 1, 1, 0, 0, 0, Rational(5, 2)).utc_offset.should.eql?(Rational(5, 2))
+ end
+
+ describe "with an argument that responds to #to_r" do
+ it "coerces using #to_r" do
+ o = mock_numeric('rational')
+ o.should_receive(:to_r).and_return(Rational(5, 2))
+ Time.new(2000, 1, 1, 0, 0, 0, o).utc_offset.should.eql?(Rational(5, 2))
+ end
+ end
+
+ it "returns a Time with a UTC offset specified as +HH:MM" do
+ Time.new(2000, 1, 1, 0, 0, 0, "+05:30").utc_offset.should == 19800
+ end
+
+ it "returns a Time with a UTC offset specified as -HH:MM" do
+ Time.new(2000, 1, 1, 0, 0, 0, "-04:10").utc_offset.should == -15000
+ end
+
+ it "returns a Time with a UTC offset specified as +HH:MM:SS" do
+ Time.new(2000, 1, 1, 0, 0, 0, "+05:30:37").utc_offset.should == 19837
+ end
+
+ it "returns a Time with a UTC offset specified as -HH:MM" do
+ Time.new(2000, 1, 1, 0, 0, 0, "-04:10:43").utc_offset.should == -15043
+ end
+
+ it "returns a Time with a UTC offset specified as +HH" do
+ Time.new(2000, 1, 1, 0, 0, 0, "+05").utc_offset.should == 3600 * 5
+ end
+
+ it "returns a Time with a UTC offset specified as -HH" do
+ Time.new(2000, 1, 1, 0, 0, 0, "-05").utc_offset.should == -3600 * 5
+ end
+
+ it "returns a Time with a UTC offset specified as +HHMM" do
+ Time.new(2000, 1, 1, 0, 0, 0, "+0530").utc_offset.should == 19800
+ end
+
+ it "returns a Time with a UTC offset specified as -HHMM" do
+ Time.new(2000, 1, 1, 0, 0, 0, "-0530").utc_offset.should == -19800
+ end
+
+ it "returns a Time with a UTC offset specified as +HHMMSS" do
+ Time.new(2000, 1, 1, 0, 0, 0, "+053037").utc_offset.should == 19837
+ end
+
+ it "returns a Time with a UTC offset specified as -HHMMSS" do
+ Time.new(2000, 1, 1, 0, 0, 0, "-053037").utc_offset.should == -19837
+ end
+
+ describe "with an argument that responds to #to_str" do
+ it "coerces using #to_str" do
+ o = mock('string')
+ o.should_receive(:to_str).and_return("+05:30")
+ Time.new(2000, 1, 1, 0, 0, 0, o).utc_offset.should == 19800
+ end
+ end
+
+ it "returns a Time with UTC offset specified as UTC" do
+ Time.new(2000, 1, 1, 0, 0, 0, "UTC").utc_offset.should == 0
+ end
+
+ it "returns a Time with UTC offset specified as a single letter military timezone" do
+ [
+ ["A", 3600],
+ ["B", 3600 * 2],
+ ["C", 3600 * 3],
+ ["D", 3600 * 4],
+ ["E", 3600 * 5],
+ ["F", 3600 * 6],
+ ["G", 3600 * 7],
+ ["H", 3600 * 8],
+ ["I", 3600 * 9],
+ # J is not supported
+ ["K", 3600 * 10],
+ ["L", 3600 * 11],
+ ["M", 3600 * 12],
+ ["N", 3600 * -1],
+ ["O", 3600 * -2],
+ ["P", 3600 * -3],
+ ["Q", 3600 * -4],
+ ["R", 3600 * -5],
+ ["S", 3600 * -6],
+ ["T", 3600 * -7],
+ ["U", 3600 * -8],
+ ["V", 3600 * -9],
+ ["W", 3600 * -10],
+ ["X", 3600 * -11],
+ ["Y", 3600 * -12],
+ ["Z", 0]
+ ].each do |letter, offset|
+ Time.new(2000, 1, 1, 0, 0, 0, letter).utc_offset.should == offset
+ end
+ end
+
+ it "raises ArgumentError if the string argument is J" do
+ message = '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: J'
+ -> { Time.new(2000, 1, 1, 0, 0, 0, "J") }.should.raise(ArgumentError, message)
+ end
+
+ it "returns a local Time if the argument is nil" do
+ with_timezone("PST", -8) do
+ t = Time.new(2000, 1, 1, 0, 0, 0, nil)
+ t.utc_offset.should == -28800
+ t.zone.should == "PST"
+ end
+ end
+
+ # [Bug #8679], r47676
+ it "disallows a value for minutes greater than 59" do
+ -> {
+ Time.new(2000, 1, 1, 0, 0, 0, "+01:60")
+ }.should.raise(ArgumentError)
+ -> {
+ Time.new(2000, 1, 1, 0, 0, 0, "+01:99")
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the String argument is not of the form (+|-)HH:MM" do
+ -> { Time.new(2000, 1, 1, 0, 0, 0, "3600") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the hour value is greater than 23" do
+ -> { Time.new(2000, 1, 1, 0, 0, 0, "+24:00") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the String argument is not in an ASCII-compatible encoding" do
+ # Don't check exception message - it was changed in previous CRuby versions:
+ # - "string contains null byte"
+ # - '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset'
+ -> {
+ Time.new(2000, 1, 1, 0, 0, 0, "-04:10".encode("UTF-16LE"))
+ }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the argument represents a value less than or equal to -86400 seconds" do
+ Time.new(2000, 1, 1, 0, 0, 0, -86400 + 1).utc_offset.should == (-86400 + 1)
+ -> { Time.new(2000, 1, 1, 0, 0, 0, -86400) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the argument represents a value greater than or equal to 86400 seconds" do
+ Time.new(2000, 1, 1, 0, 0, 0, 86400 - 1).utc_offset.should == (86400 - 1)
+ -> { Time.new(2000, 1, 1, 0, 0, 0, 86400) }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if the utc_offset argument is greater than or equal to 10e9" do
+ -> { Time.new(2000, 1, 1, 0, 0, 0, 1000000000) }.should.raise(ArgumentError)
+ end
+end
+
+# The method #local_to_utc is tested only here because Time.new is the only method that calls #local_to_utc.
+describe "Time.new with a timezone argument" do
+ it "returns a Time in the timezone" do
+ zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60))
+ time = Time.new(2000, 1, 1, 12, 0, 0, zone)
+
+ time.zone.should == zone
+ time.utc_offset.should == 5*3600+30*60
+ time.wday.should == 6
+ time.yday.should == 1
+ end
+
+ it "accepts timezone argument that must have #local_to_utc and #utc_to_local methods" do
+ zone = Object.new
+ def zone.utc_to_local(time)
+ time
+ end
+ def zone.local_to_utc(time)
+ time
+ end
+
+ Time.new(2000, 1, 1, 12, 0, 0, zone).should.is_a?(Time)
+ end
+
+ it "raises TypeError if timezone does not implement #local_to_utc method" do
+ zone = Object.new
+ def zone.utc_to_local(time)
+ time
+ end
+
+ -> {
+ Time.new(2000, 1, 1, 12, 0, 0, zone)
+ }.should.raise(TypeError, /can't convert Object into an exact number/)
+ end
+
+ it "does not raise exception if timezone does not implement #utc_to_local method" do
+ zone = Object.new
+ def zone.local_to_utc(time)
+ time
+ end
+
+ Time.new(2000, 1, 1, 12, 0, 0, zone).should.is_a?(Time)
+ end
+
+ # The result also should be a Time or Time-like object (not necessary to be the same class)
+ # or respond to #to_int method. The zone of the result is just ignored.
+ describe "returned value by #utc_to_local and #local_to_utc methods" do
+ it "could be Time instance" do
+ zone = Object.new
+ def zone.local_to_utc(t)
+ time = Time.utc(t.year, t.mon, t.day, t.hour, t.min, t.sec)
+ time - 60 * 60 # - 1 hour
+ end
+
+ Time.new(2000, 1, 1, 12, 0, 0, zone).should.is_a?(Time)
+ Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60
+ end
+
+ it "could be Time subclass instance" do
+ zone = Object.new
+ def zone.local_to_utc(t)
+ time = Time.utc(t.year, t.mon, t.day, t.hour, t.min, t.sec)
+ time -= 60 * 60 # - 1 hour
+ Class.new(Time).utc(time.year, time.mon, time.day, time.hour, t.min, t.sec)
+ end
+
+ Time.new(2000, 1, 1, 12, 0, 0, zone).should.is_a?(Time)
+ Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60
+ end
+
+ it "could be any object with #to_i method" do
+ zone = Object.new
+ def zone.local_to_utc(time)
+ obj = Object.new
+ obj.singleton_class.define_method(:to_i) { time.to_i - 60*60 }
+ obj
+ end
+
+ Time.new(2000, 1, 1, 12, 0, 0, zone).should.is_a?(Time)
+ Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 60*60
+ end
+
+ it "could have any #zone and #utc_offset because they are ignored if it isn't an instance of Time" do
+ zone = Object.new
+ def zone.local_to_utc(time)
+ Struct.new(:to_i, :zone, :utc_offset).new(time.to_i, 'America/New_York', -5*60*60)
+ end
+ Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 0
+
+ zone = Object.new
+ def zone.local_to_utc(time)
+ Struct.new(:to_i, :zone, :utc_offset).new(time.to_i, 'Asia/Tokyo', 9*60*60)
+ end
+ Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 0
+ end
+
+ it "cannot have arbitrary #utc_offset if it is an instance of Time" do
+ zone = Object.new
+ def zone.local_to_utc(t)
+ Time.new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, 9*60*60)
+ end
+ Time.new(2000, 1, 1, 12, 0, 0, zone).utc_offset.should == 9*60*60
+ end
+
+ it "raises ArgumentError if difference between argument and result is too large" do
+ zone = Object.new
+ def zone.local_to_utc(t)
+ Time.utc(t.year, t.mon, t.day + 1, t.hour, t.min, t.sec)
+ end
+
+ -> {
+ Time.new(2000, 1, 1, 12, 0, 0, zone)
+ }.should.raise(ArgumentError, "utc_offset out of range")
+ end
+ end
+
+ # https://github.com/ruby/ruby/blob/v2_6_0/time.c#L5330
+ #
+ # Time-like argument to these methods is similar to a Time object in UTC without sub-second;
+ # it has attribute readers for the parts, e.g. year, month, and so on, and epoch time readers, to_i
+ #
+ # The sub-second attributes are fixed as 0, and utc_offset, zone, isdst, and their aliases are same as a Time object in UTC
+ describe "Time-like argument of #utc_to_local and #local_to_utc methods" do
+ before do
+ @obj = TimeSpecs::TimeLikeArgumentRecorder.result
+ @obj.should_not == nil
+ end
+
+ it "implements subset of Time methods" do
+ # List only methods that are explicitly documented.
+ [
+ :year, :mon, :mday, :hour, :min, :sec, :to_i, :isdst
+ ].each do |name|
+ @obj.respond_to?(name).should == true
+ end
+ end
+
+ it "has attribute values the same as a Time object in UTC" do
+ @obj.usec.should == 0
+ @obj.nsec.should == 0
+ @obj.subsec.should == 0
+ @obj.tv_usec.should == 0
+ @obj.tv_nsec.should == 0
+
+ @obj.utc_offset.should == 0
+ @obj.zone.should == "UTC"
+ @obj.isdst.should == Time.new.utc.isdst
+ end
+ end
+
+ context "#name method" do
+ it "uses the optional #name method for marshaling" do
+ zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo")
+ time = Time.new(2000, 1, 1, 12, 0, 0, zone)
+ time_loaded = Marshal.load(Marshal.dump(time))
+
+ time_loaded.zone.should == "Asia/Colombo"
+ time_loaded.utc_offset.should == 5*3600+30*60
+ end
+
+ it "cannot marshal Time if #name method isn't implemented" do
+ zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60))
+ time = Time.new(2000, 1, 1, 12, 0, 0, zone)
+
+ -> {
+ Marshal.dump(time)
+ }.should.raise(NoMethodError, /undefined method [`']name' for/)
+ end
+ end
+
+ it "the #abbr method is used by '%Z' in #strftime" do
+ zone = TimeSpecs::TimezoneWithAbbr.new(name: "Asia/Colombo")
+ time = Time.new(2000, 1, 1, 12, 0, 0, zone)
+
+ time.strftime("%Z").should == "MMT"
+ end
+
+ # At loading marshaled data, a timezone name will be converted to a timezone object
+ # by find_timezone class method, if the method is defined.
+ # Similarly, that class method will be called when a timezone argument does not have
+ # the necessary methods mentioned above.
+ context "subject's class implements .find_timezone method" do
+ it "calls .find_timezone to build a time object at loading marshaled data" do
+ zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo")
+ time = TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, zone)
+ time_loaded = Marshal.load(Marshal.dump(time))
+
+ time_loaded.zone.should.is_a? TimeSpecs::TimezoneWithName
+ time_loaded.zone.name.should == "Asia/Colombo"
+ time_loaded.utc_offset.should == 5*3600+30*60
+ end
+
+ it "calls .find_timezone to build a time object if passed zone name as a timezone argument" do
+ time = TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, "Asia/Colombo")
+ time.zone.should.is_a? TimeSpecs::TimezoneWithName
+ time.zone.name.should == "Asia/Colombo"
+
+ time = TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, "some invalid zone name")
+ time.zone.should.is_a? TimeSpecs::TimezoneWithName
+ time.zone.name.should == "some invalid zone name"
+ end
+
+ it "does not call .find_timezone if passed any not string/numeric/timezone timezone argument" do
+ [Object.new, [], {}, :"some zone"].each do |zone|
+ -> {
+ TimeSpecs::TimeWithFindTimezone.new(2000, 1, 1, 12, 0, 0, zone)
+ }.should.raise(TypeError, /can't convert \w+ into an exact number/)
+ end
+ end
+ end
+
+ describe ":in keyword argument" do
+ it "could be UTC offset as a String in '+HH:MM or '-HH:MM' format" do
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: "+05:00")
+
+ time.utc_offset.should == 5*60*60
+ time.zone.should == nil
+
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: "-09:00")
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == nil
+
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: "-09:00:01")
+
+ time.utc_offset.should == -(9*60*60 + 1)
+ time.zone.should == nil
+ end
+
+ it "could be UTC offset as a number of seconds" do
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: 5*60*60)
+
+ time.utc_offset.should == 5*60*60
+ time.zone.should == nil
+
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: -9*60*60)
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == nil
+ end
+
+ it "returns a Time with UTC offset specified as a single letter military timezone" do
+ Time.new(2000, 1, 1, 0, 0, 0, in: "W").utc_offset.should == 3600 * -10
+ end
+
+ it "could be a timezone object" do
+ zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo")
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: zone)
+
+ time.utc_offset.should == 5*3600+30*60
+ time.zone.should == zone
+
+ zone = TimeSpecs::TimezoneWithName.new(name: "PST")
+ time = Time.new(2000, 1, 1, 12, 0, 0, in: zone)
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == zone
+ end
+
+ it "allows omitting minor arguments" do
+ Time.new(2000, 1, 1, 12, 1, 1, in: "+05:00").should == Time.new(2000, 1, 1, 12, 1, 1, "+05:00")
+ Time.new(2000, 1, 1, 12, 1, in: "+05:00").should == Time.new(2000, 1, 1, 12, 1, 0, "+05:00")
+ Time.new(2000, 1, 1, 12, in: "+05:00").should == Time.new(2000, 1, 1, 12, 0, 0, "+05:00")
+ Time.new(2000, 1, 1, in: "+05:00").should == Time.new(2000, 1, 1, 0, 0, 0, "+05:00")
+ Time.new(2000, 1, in: "+05:00").should == Time.new(2000, 1, 1, 0, 0, 0, "+05:00")
+ Time.new(2000, in: "+05:00").should == Time.new(2000, 1, 1, 0, 0, 0, "+05:00")
+ Time.new(in: "+05:00").should be_close(Time.now.getlocal("+05:00"), TIME_TOLERANCE)
+ end
+
+ it "converts to a provided timezone if all the positional arguments are omitted" do
+ Time.new(in: "+05:00").utc_offset.should == 5*3600
+ end
+
+ it "raises ArgumentError if format is invalid" do
+ -> { Time.new(2000, 1, 1, 12, 0, 0, in: "+09:99") }.should.raise(ArgumentError)
+ -> { Time.new(2000, 1, 1, 12, 0, 0, in: "ABC") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if two offset arguments are given" do
+ -> {
+ Time.new(2000, 1, 1, 12, 0, 0, "+05:00", in: "+05:00")
+ }.should.raise(ArgumentError, "timezone argument given as positional and keyword arguments")
+ end
+ end
+
+ describe "Time.new with a String argument" do
+ it "parses an ISO-8601 like format" do
+ t = Time.utc(2020, 12, 24, 15, 56, 17)
+
+ Time.new("2020-12-24T15:56:17Z").should == t
+ Time.new("2020-12-25 00:56:17 +09:00").should == t
+ Time.new("2020-12-25 00:57:47 +09:01:30").should == t
+ Time.new("2020-12-25 00:56:17 +0900").should == t
+ Time.new("2020-12-25 00:57:47 +090130").should == t
+ Time.new("2020-12-25T00:56:17+09:00").should == t
+
+ Time.new("2020-12-25T00:56:17.123456+09:00").should == Time.utc(2020, 12, 24, 15, 56, 17, 123456)
+ end
+
+ it "accepts precision keyword argument and truncates specified digits of sub-second part" do
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00").subsec.should == 0.123456789r
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: nil).subsec.should == 0.123456789876r
+ Time.new("2021-12-25 00:00:00 +09:00", precision: 0).subsec.should == 0
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: -1).subsec.should == 0.123456789876r
+ end
+
+ it "returns Time in local timezone if not provided in the String argument" do
+ Time.new("2021-12-25 00:00:00").zone.should == Time.new(2021, 12, 25).zone
+ Time.new("2021-12-25 00:00:00").utc_offset.should == Time.new(2021, 12, 25).utc_offset
+ end
+
+ it "returns Time in timezone specified in the String argument" do
+ Time.new("2021-12-25 00:00:00 +05:00").to_s.should == "2021-12-25 00:00:00 +0500"
+ end
+
+ it "returns Time in timezone specified in the String argument even if the in keyword argument provided" do
+ Time.new("2021-12-25 00:00:00 +09:00", in: "-01:00").to_s.should == "2021-12-25 00:00:00 +0900"
+ end
+
+ it "returns Time in timezone specified with in keyword argument if timezone isn't provided in the String argument" do
+ Time.new("2021-12-25 00:00:00", in: "-01:00").to_s.should == "2021-12-25 00:00:00 -0100"
+ end
+
+ it "returns Time of Jan 1 for string with just year" do
+ Time.new("2021").should == Time.new(2021, 1, 1)
+ Time.new("2021").zone.should == Time.new(2021, 1, 1).zone
+ Time.new("2021").utc_offset.should == Time.new(2021, 1, 1).utc_offset
+ end
+
+ it "returns Time of Jan 1 for string with just year in timezone specified with in keyword argument" do
+ Time.new("2021", in: "+17:00").to_s.should == "2021-01-01 00:00:00 +1700"
+ end
+
+ it "converts precision keyword argument into Integer if is not nil" do
+ obj = Object.new
+ def obj.to_int; 3; end
+
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: 1.2).subsec.should == 0.1r
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: obj).subsec.should == 0.123r
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: 3r).subsec.should == 0.123r
+ end
+
+ it "returns Time with correct subseconds when given seconds fraction is shorted than 6 digits" do
+ Time.new("2020-12-25T00:56:17.123 +09:00").nsec.should == 123000000
+ Time.new("2020-12-25T00:56:17.123 +09:00").usec.should == 123000
+ Time.new("2020-12-25T00:56:17.123 +09:00").subsec.should == 0.123
+ end
+
+ it "returns Time with correct subseconds when given seconds fraction is milliseconds" do
+ Time.new("2020-12-25T00:56:17.123456 +09:00").nsec.should == 123456000
+ Time.new("2020-12-25T00:56:17.123456 +09:00").usec.should == 123456
+ Time.new("2020-12-25T00:56:17.123456 +09:00").subsec.should == 0.123456
+ end
+
+ it "returns Time with correct subseconds when given seconds fraction is longer that 6 digits but shorted than 9 digits" do
+ Time.new("2020-12-25T00:56:17.12345678 +09:00").nsec.should == 123456780
+ Time.new("2020-12-25T00:56:17.12345678 +09:00").usec.should == 123456
+ Time.new("2020-12-25T00:56:17.12345678 +09:00").subsec.should == 0.12345678
+ end
+
+ it "returns Time with correct subseconds when given seconds fraction is nanoseconds" do
+ Time.new("2020-12-25T00:56:17.123456789 +09:00").nsec.should == 123456789
+ Time.new("2020-12-25T00:56:17.123456789 +09:00").usec.should == 123456
+ Time.new("2020-12-25T00:56:17.123456789 +09:00").subsec.should == 0.123456789
+ end
+
+ it "returns Time with correct subseconds when given seconds fraction is longer than 9 digits" do
+ Time.new("2020-12-25T00:56:17.123456789876 +09:00").nsec.should == 123456789
+ Time.new("2020-12-25T00:56:17.123456789876 +09:00").usec.should == 123456
+ Time.new("2020-12-25T00:56:17.123456789876 +09:00").subsec.should == 0.123456789
+ end
+
+ it "raise TypeError is can't convert precision keyword argument into Integer" do
+ -> {
+ Time.new("2021-12-25 00:00:00.123456789876 +09:00", precision: "")
+ }.should.raise(TypeError, "no implicit conversion of String into Integer")
+ end
+
+ it "raises ArgumentError if part of time string is missing" do
+ -> {
+ Time.new("2020-12-25 00:56 +09:00")
+ }.should.raise(ArgumentError, /missing sec part: 00:56 |can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00 +09:00")
+ }.should.raise(ArgumentError, /missing min part: 00 |can't parse:/)
+ end
+
+ it "raises ArgumentError if the time part is missing" do
+ -> {
+ Time.new("2020-12-25")
+ }.should.raise(ArgumentError, /no time information|can't parse:/)
+ end
+
+ it "raises ArgumentError if day is missing" do
+ -> {
+ Time.new("2020-12")
+ }.should.raise(ArgumentError, /no time information|can't parse:/)
+ end
+
+ it "raises ArgumentError if subsecond is missing after dot" do
+ -> {
+ Time.new("2020-12-25 00:56:17. +0900")
+ }.should.raise(ArgumentError, /subsecond expected after dot: 00:56:17. |can't parse:/)
+ end
+
+ it "raises ArgumentError if String argument is not in the supported format" do
+ -> {
+ Time.new("021-12-25 00:00:00.123456 +09:00")
+ }.should.raise(ArgumentError, /year must be 4 or more digits: 021|can't parse:/)
+
+ -> {
+ Time.new("2020-012-25 00:56:17 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits mon is expected after [`']-': -012-25 00:\z|can't parse:/)
+
+ -> {
+ Time.new("2020-2-25 00:56:17 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits mon is expected after [`']-': -2-25 00:56\z|can't parse:/)
+
+ -> {
+ Time.new("2020-12-215 00:56:17 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits mday is expected after [`']-': -215 00:56:\z|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 000:56:17 +0900")
+ }.should.raise(ArgumentError, /two digits hour is expected: 000:56:17 |can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 0:56:17 +0900")
+ }.should.raise(ArgumentError, /two digits hour is expected: 0:56:17 \+0|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00:516:17 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits min is expected after [`']:': :516:17 \+09\z|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00:6:17 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits min is expected after [`']:': :6:17 \+0900\z|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00:56:137 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits sec is expected after [`']:': :137 \+0900\z|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00:56:7 +0900")
+ }.should.raise(ArgumentError, /\Atwo digits sec is expected after [`']:': :7 \+0900\z|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00:56. +0900")
+ }.should.raise(ArgumentError, /fraction min is not supported: 00:56\.|can't parse:/)
+
+ -> {
+ Time.new("2020-12-25 00. +0900")
+ }.should.raise(ArgumentError, /fraction hour is not supported: 00\.|can't parse:/)
+ end
+
+ it "raises ArgumentError if date/time parts values are not valid" do
+ -> {
+ Time.new("2020-13-25 00:56:17 +09:00")
+ }.should.raise(ArgumentError, /(mon|argument) out of range/)
+
+ -> {
+ Time.new("2020-12-32 00:56:17 +09:00")
+ }.should.raise(ArgumentError, /(mday|argument) out of range/)
+
+ -> {
+ Time.new("2020-12-25 25:56:17 +09:00")
+ }.should.raise(ArgumentError, /(hour|argument) out of range/)
+
+ -> {
+ Time.new("2020-12-25 00:61:17 +09:00")
+ }.should.raise(ArgumentError, /(min|argument) out of range/)
+
+ -> {
+ Time.new("2020-12-25 00:56:61 +09:00")
+ }.should.raise(ArgumentError, /(sec|argument) out of range/)
+
+ -> {
+ Time.new("2020-12-25 00:56:17 +23:59:60")
+ }.should.raise(ArgumentError, /utc_offset|argument out of range/)
+
+ -> {
+ Time.new("2020-12-25 00:56:17 +24:00")
+ }.should.raise(ArgumentError, /(utc_offset|argument) out of range/)
+
+ -> {
+ Time.new("2020-12-25 00:56:17 +23:61")
+ }.should.raise(ArgumentError, /utc_offset/)
+
+ ruby_bug '#20797', ''...'3.4' do
+ -> {
+ Time.new("2020-12-25 00:56:17 +00:23:61")
+ }.should.raise(ArgumentError, /utc_offset/)
+ end
+ end
+
+ it "raises ArgumentError if utc offset parts are not valid" do
+ -> { Time.new("2020-12-25 00:56:17 +24:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.new("2020-12-25 00:56:17 +2400") }.should.raise(ArgumentError, "utc_offset out of range")
+
+ -> { Time.new("2020-12-25 00:56:17 +99:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.new("2020-12-25 00:56:17 +9900") }.should.raise(ArgumentError, "utc_offset out of range")
+
+ -> { Time.new("2020-12-25 00:56:17 +00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60')
+ -> { Time.new("2020-12-25 00:56:17 +0060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060')
+
+ -> { Time.new("2020-12-25 00:56:17 +00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99')
+ -> { Time.new("2020-12-25 00:56:17 +0099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099')
+
+ ruby_bug '#20797', ''...'3.4' do
+ -> { Time.new("2020-12-25 00:56:17 +00:00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60')
+ -> { Time.new("2020-12-25 00:56:17 +000060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060')
+
+ -> { Time.new("2020-12-25 00:56:17 +00:00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99')
+ -> { Time.new("2020-12-25 00:56:17 +000099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099')
+ end
+ end
+
+ it "raises ArgumentError if string has not ascii-compatible encoding" do
+ -> {
+ Time.new("2021-11-31 00:00:60 +09:00".encode("utf-32le"))
+ }.should.raise(ArgumentError, "time string should have ASCII compatible encoding")
+ end
+
+ it "raises ArgumentError if string doesn't start with year" do
+ -> {
+ Time.new("a\nb")
+ }.should.raise(ArgumentError, "can't parse: \"a\\nb\"")
+ end
+
+ it "raises ArgumentError if string has extra characters after offset" do
+ -> {
+ Time.new("2021-11-31 00:00:59 +09:00 abc")
+ }.should.raise(ArgumentError, /can't parse.+ abc/)
+ end
+
+ it "raises ArgumentError when there are leading space characters" do
+ -> { Time.new(" 2020-12-02 00:00:00") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("\t2020-12-02 00:00:00") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("\n2020-12-02 00:00:00") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("\v2020-12-02 00:00:00") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("\f2020-12-02 00:00:00") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("\r2020-12-02 00:00:00") }.should.raise(ArgumentError, /can't parse/)
+ end
+
+ it "raises ArgumentError when there are trailing whitespaces" do
+ -> { Time.new("2020-12-02 00:00:00 ") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("2020-12-02 00:00:00\t") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("2020-12-02 00:00:00\n") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("2020-12-02 00:00:00\v") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("2020-12-02 00:00:00\f") }.should.raise(ArgumentError, /can't parse/)
+ -> { Time.new("2020-12-02 00:00:00\r") }.should.raise(ArgumentError, /can't parse/)
+ end
+ end
+end
diff --git a/spec/ruby/core/time/now_spec.rb b/spec/ruby/core/time/now_spec.rb
new file mode 100644
index 0000000000..533cf68380
--- /dev/null
+++ b/spec/ruby/core/time/now_spec.rb
@@ -0,0 +1,181 @@
+require_relative '../../spec_helper'
+require_relative 'shared/now'
+
+describe "Time.now" do
+ it_behaves_like :time_now, :now
+
+ describe ":in keyword argument" do
+ it "could be UTC offset as a String in '+HH:MM or '-HH:MM' format" do
+ time = Time.now(in: "+05:00")
+
+ time.utc_offset.should == 5*60*60
+ time.zone.should == nil
+
+ time = Time.now(in: "-09:00")
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == nil
+
+ time = Time.now(in: "-09:00:01")
+
+ time.utc_offset.should == -(9*60*60 + 1)
+ time.zone.should == nil
+ end
+
+ it "could be UTC offset as a number of seconds" do
+ time = Time.now(in: 5*60*60)
+
+ time.utc_offset.should == 5*60*60
+ time.zone.should == nil
+
+ time = Time.now(in: -9*60*60)
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == nil
+ end
+
+ it "returns a Time with UTC offset specified as a single letter military timezone" do
+ Time.now(in: "W").utc_offset.should == 3600 * -10
+ end
+
+ it "could be a timezone object" do
+ zone = TimeSpecs::TimezoneWithName.new(name: "Asia/Colombo")
+ time = Time.now(in: zone)
+
+ time.utc_offset.should == 5*3600+30*60
+ time.zone.should == zone
+
+ zone = TimeSpecs::TimezoneWithName.new(name: "PST")
+ time = Time.now(in: zone)
+
+ time.utc_offset.should == -9*60*60
+ time.zone.should == zone
+ end
+
+ it "raises ArgumentError if format is invalid" do
+ -> { Time.now(in: "+09:99") }.should.raise(ArgumentError)
+ -> { Time.now(in: "ABC") }.should.raise(ArgumentError)
+ end
+
+ it "raises ArgumentError if String argument and hours greater than 23" do
+ -> { Time.now(in: "+24:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.now(in: "+2400") }.should.raise(ArgumentError, "utc_offset out of range")
+
+ -> { Time.now(in: "+99:00") }.should.raise(ArgumentError, "utc_offset out of range")
+ -> { Time.now(in: "+9900") }.should.raise(ArgumentError, "utc_offset out of range")
+ end
+
+ it "raises ArgumentError if String argument and minutes greater than 59" do
+ -> { Time.now(in: "+00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:60')
+ -> { Time.now(in: "+0060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0060')
+
+ -> { Time.now(in: "+00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:99')
+ -> { Time.now(in: "+0099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +0099')
+ end
+
+ ruby_bug '#20797', ''...'3.4' do
+ it "raises ArgumentError if String argument and seconds greater than 59" do
+ -> { Time.now(in: "+00:00:60") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:60')
+ -> { Time.now(in: "+000060") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000060')
+
+ -> { Time.now(in: "+00:00:99") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +00:00:99')
+ -> { Time.now(in: "+000099") }.should.raise(ArgumentError, '"+HH:MM", "-HH:MM", "UTC" or "A".."I","K".."Z" expected for utc_offset: +000099')
+ end
+ end
+ end
+
+ describe "Timezone object" do # https://bugs.ruby-lang.org/issues/17485
+ it "raises TypeError if timezone does not implement #utc_to_local method" do
+ zone = Object.new
+ def zone.local_to_utc(time)
+ time
+ end
+
+ -> {
+ Time.now(in: zone)
+ }.should.raise(TypeError, /can't convert Object into an exact number/)
+ end
+
+ it "does not raise exception if timezone does not implement #local_to_utc method" do
+ zone = Object.new
+ def zone.utc_to_local(time)
+ time
+ end
+
+ Time.now(in: zone).should.is_a?(Time)
+ end
+
+ # The result also should be a Time or Time-like object (not necessary to be the same class)
+ # or Integer. The zone of the result is just ignored.
+ describe "returned value by #utc_to_local and #local_to_utc methods" do
+ it "could be Time instance" do
+ zone = Object.new
+ def zone.utc_to_local(t)
+ time = Time.new(t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset)
+ time + 60 * 60 # + 1 hour
+ end
+
+ Time.now(in: zone).should.is_a?(Time)
+ Time.now(in: zone).utc_offset.should == 3600
+ end
+
+ it "could be Time subclass instance" do
+ zone = Object.new
+ def zone.utc_to_local(t)
+ time = Time.new(t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset)
+ time += 60 * 60 # + 1 hour
+
+ Class.new(Time).new(time.year, time.mon, time.day, time.hour, time.min, time.sec, time.utc_offset)
+ end
+
+ Time.now(in: zone).should.is_a?(Time)
+ Time.now(in: zone).utc_offset.should == 3600
+ end
+
+ it "could be Integer" do
+ zone = Object.new
+ def zone.utc_to_local(time)
+ time.to_i + 60*60
+ end
+
+ Time.now(in: zone).should.is_a?(Time)
+ Time.now(in: zone).utc_offset.should == 60*60
+ end
+
+ it "could have any #zone and #utc_offset because they are ignored" do
+ zone = Object.new
+ def zone.utc_to_local(t)
+ Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i, :zone, :utc_offset) # rubocop:disable Lint/StructNewOverride
+ .new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.isdst, t.to_i, 'America/New_York', -5*60*60)
+ end
+ Time.now(in: zone).utc_offset.should == 0
+
+ zone = Object.new
+ def zone.utc_to_local(t)
+ Struct.new(:year, :mon, :mday, :hour, :min, :sec, :isdst, :to_i, :zone, :utc_offset) # rubocop:disable Lint/StructNewOverride
+ .new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, t.isdst, t.to_i, 'Asia/Tokyo', 9*60*60)
+ end
+ Time.now(in: zone).utc_offset.should == 0
+
+ zone = Object.new
+ def zone.utc_to_local(t)
+ Time.new(t.year, t.mon, t.mday, t.hour, t.min, t.sec, 9*60*60)
+ end
+ Time.now(in: zone).utc_offset.should == 0
+ end
+
+ it "raises ArgumentError if difference between argument and result is too large" do
+ zone = Object.new
+ def zone.utc_to_local(t)
+ time = Time.utc(t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset)
+ time -= 24 * 60 * 60 # - 1 day
+ Time.utc(time.year, time.mon, time.day, time.hour, time.min, time.sec, time.utc_offset)
+ end
+
+ -> {
+ Time.now(in: zone)
+ }.should.raise(ArgumentError, "utc_offset out of range")
+ end
+ end
+ end
+end
diff --git a/spec/ruby/core/time/nsec_spec.rb b/spec/ruby/core/time/nsec_spec.rb
new file mode 100644
index 0000000000..9338eb435a
--- /dev/null
+++ b/spec/ruby/core/time/nsec_spec.rb
@@ -0,0 +1,31 @@
+require_relative '../../spec_helper'
+
+describe "Time#nsec" do
+ it "returns 0 for a Time constructed with a whole number of seconds" do
+ Time.at(100).nsec.should == 0
+ end
+
+ it "returns the nanoseconds part of a Time constructed with a Float number of seconds" do
+ Time.at(10.75).nsec.should == 750_000_000
+ end
+
+ it "returns the nanoseconds part of a Time constructed with an Integer number of microseconds" do
+ Time.at(0, 999_999).nsec.should == 999_999_000
+ end
+
+ it "returns the nanoseconds part of a Time constructed with an Float number of microseconds" do
+ Time.at(0, 3.75).nsec.should == 3750
+ end
+
+ it "returns the nanoseconds part of a Time constructed with a Rational number of seconds" do
+ Time.at(Rational(3, 2)).nsec.should == 500_000_000
+ end
+
+ it "returns the nanoseconds part of a Time constructed with an Rational number of microseconds" do
+ Time.at(0, Rational(99, 10)).nsec.should == 9900
+ end
+
+ it "returns a positive value for dates before the epoch" do
+ Time.utc(1969, 11, 12, 13, 18, 57, 404240).nsec.should == 404240000
+ end
+end
diff --git a/spec/ruby/core/time/plus_spec.rb b/spec/ruby/core/time/plus_spec.rb
new file mode 100644
index 0000000000..6bd01bcdf3
--- /dev/null
+++ b/spec/ruby/core/time/plus_spec.rb
@@ -0,0 +1,118 @@
+require_relative '../../spec_helper'
+require_relative 'fixtures/classes'
+
+describe "Time#+" do
+ it "increments the time by the specified amount" do
+ (Time.at(0) + 100).should == Time.at(100)
+ end
+
+ it "is a commutative operator" do
+ (Time.at(1.1) + 0.9).should == Time.at(0.9) + 1.1
+ end
+
+ it "adds a negative Float" do
+ t = Time.at(100) + -1.3
+ t.usec.should == 699999
+ t.to_i.should == 98
+ end
+
+ it "raises a TypeError if given argument is a coercible String" do
+ -> { Time.now + "1" }.should.raise(TypeError)
+ -> { Time.now + "0.1" }.should.raise(TypeError)
+ -> { Time.now + "1/3" }.should.raise(TypeError)
+ end
+
+ it "increments the time by the specified amount as rational numbers" do
+ (Time.at(Rational(11, 10)) + Rational(9, 10)).should == Time.at(2)
+ end
+
+ it "accepts arguments that can be coerced into Rational" do
+ (obj = mock_numeric('10')).should_receive(:to_r).and_return(Rational(10))
+ (Time.at(100) + obj).should == Time.at(110)
+ end
+
+ it "raises TypeError on argument that can't be coerced into Rational" do
+ -> { Time.now + Object.new }.should.raise(TypeError)
+ -> { Time.now + "stuff" }.should.raise(TypeError)
+ end
+
+ it "returns a UTC time if self is UTC" do
+ (Time.utc(2012) + 10).should.utc?
+ end
+
+ it "returns a non-UTC time if self is non-UTC" do
+ (Time.local(2012) + 10).should_not.utc?
+ end
+
+ it "returns a time with the same fixed offset as self" do
+ (Time.new(2012, 1, 1, 0, 0, 0, 3600) + 10).utc_offset.should == 3600
+ end
+
+ it "preserves time zone" do
+ time_with_zone = Time.now.utc
+ time_with_zone.zone.should == (time_with_zone + 1).zone
+
+ time_with_zone = Time.now
+ time_with_zone.zone.should == (time_with_zone + 1).zone
+ end
+
+ context "zone is a timezone object" do
+ it "preserves time zone" do
+ zone = TimeSpecs::Timezone.new(offset: (5*3600+30*60))
+ time = Time.new(2012, 1, 1, 12, 0, 0, zone) + 1
+
+ time.zone.should == zone
+ end
+ end
+
+ it "does not return a subclass instance" do
+ c = Class.new(Time)
+ x = c.now + 1
+ x.should.instance_of?(Time)
+ end
+
+ it "raises TypeError on Time argument" do
+ -> { Time.now + Time.now }.should.raise(TypeError)
+ end
+
+ it "raises TypeError on nil argument" do
+ -> { Time.now + nil }.should.raise(TypeError)
+ end
+
+ #see [ruby-dev:38446]
+ it "tracks microseconds" do
+ time = Time.at(0)
+ time += Rational(123_456, 1_000_000)
+ time.usec.should == 123_456
+ time += Rational(654_321, 1_000_000)
+ time.usec.should == 777_777
+ end
+
+ it "tracks nanoseconds" do
+ time = Time.at(0)
+ time += Rational(123_456_789, 1_000_000_000)
+ time.nsec.should == 123_456_789
+ time += Rational(876_543_210, 1_000_000_000)
+ time.nsec.should == 999_999_999
+ end
+
+ it "maintains precision" do
+ t = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000)
+ t.should_not == Time.at(9)
+ end
+
+ it "maintains microseconds precision" do
+ time = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000)
+ time.usec.should == 999_999
+ end
+
+ it "maintains nanoseconds precision" do
+ time = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000)
+ time.nsec.should == 999_999_999
+ end
+
+ it "maintains subseconds precision" do
+ time = Time.at(0) + Rational(8_999_999_999_999_999, 1_000_000_000_000_000)
+ time.subsec.should == Rational(999_999_999_999_999, 1_000_000_000_000_000)
+ end
+end
diff --git a/spec/ruby/core/time/round_spec.rb b/spec/ruby/core/time/round_spec.rb
new file mode 100644
index 0000000000..a739cabfdf
--- /dev/null
+++ b/spec/ruby/core/time/round_spec.rb
@@ -0,0 +1,35 @@
+require_relative '../../spec_helper'
+
+describe "Time#round" do
+ before do
+ @time = Time.utc(2010, 3, 30, 5, 43, "25.123456789".to_r)
+ end
+
+ it "defaults to rounding to 0 places" do
+ @time.round.should == Time.utc(2010, 3, 30, 5, 43, 25.to_r)
+ end
+
+ it "rounds to 0 decimal places with an explicit argument" do
+ @time.round(0).should == Time.utc(2010, 3, 30, 5, 43, 25.to_r)
+ end
+
+ it "rounds to 7 decimal places with an explicit argument" do
+ @time.round(7).should == Time.utc(2010, 3, 30, 5, 43, "25.1234568".to_r)
+ end
+
+ it "returns an instance of Time, even if #round is called on a subclass" do
+ subclass = Class.new(Time)
+ instance = subclass.at(0)
+ instance.class.should.equal? subclass
+ instance.round.should.instance_of?(Time)
+ end
+
+ it "copies own timezone to the returning value" do
+ @time.zone.should == @time.round.zone
+
+ with_timezone "JST-9" do
+ time = Time.at 0, 1
+ time.zone.should == time.round.zone
+ end
+ end
+end
diff --git a/spec/ruby/core/time/saturday_spec.rb b/spec/ruby/core/time/saturday_spec.rb
new file mode 100644
index 0000000000..0e51407366
--- /dev/null
+++ b/spec/ruby/core/time/saturday_spec.rb
@@ -0,0 +1,11 @@
+require_relative '../../spec_helper'
+
+describe "Time#saturday?" do
+ it "returns true if time represents Saturday" do
+ Time.local(2000, 1, 1).should.saturday?
+ end
+
+ it "returns false if time doesn't represent Saturday" do
+ Time.local(2000, 1, 2).should_not.saturday?
+ end
+end
diff --git a/spec/ruby/core/time/sec_spec.rb b/spec/ruby/core/time/sec_spec.rb
new file mode 100644
index 0000000000..73fc5ce1fc
--- /dev/null
+++ b/spec/ruby/core/time/sec_spec.rb
@@ -0,0 +1,7 @@
+require_relative '../../spec_helper'
+
+describe "Time#sec" do
+ it "returns the second of the minute(0..60) for time" do
+ Time.at(0).sec.should == 0
+ end
+end
diff --git a/spec/ruby/core/time/shared/asctime.rb b/spec/ruby/core/time/shared/asctime.rb
new file mode 100644
index 0000000000..d096666863
--- /dev/null
+++ b/spec/ruby/core/time/shared/asctime.rb
@@ -0,0 +1,6 @@
+describe :time_asctime, shared: true do
+ it "returns a canonical string representation of time" do
+ t = Time.now
+ t.send(@method).should == t.strftime("%a %b %e %H:%M:%S %Y")
+ end
+end
diff --git a/spec/ruby/core/time/shared/day.rb b/spec/ruby/core/time/shared/day.rb
new file mode 100644
index 0000000000..472dc959c1
--- /dev/null
+++ b/spec/ruby/core/time/shared/day.rb
@@ -0,0 +1,15 @@
+describe :time_day, shared: true do
+ it "returns the day of the month (1..n) for a local Time" do
+ with_timezone("CET", 1) do
+ Time.local(1970, 1, 1).send(@method).should == 1
+ end
+ end
+
+ it "returns the day of the month for a UTC Time" do
+ Time.utc(1970, 1, 1).send(@method).should == 1
+ end
+
+ it "returns the day of the month for a Time with a fixed offset" do
+ Time.new(2012, 1, 1, 0, 0, 0, -3600).send(@method).should == 1
+ end
+end
diff --git a/spec/ruby/core/time/shared/getgm.rb b/spec/ruby/core/time/shared/getgm.rb
new file mode 100644
index 0000000000..3576365772
--- /dev/null
+++ b/spec/ruby/core/time/shared/getgm.rb
@@ -0,0 +1,9 @@
+describe :time_getgm, shared: true do
+ it "returns a new time which is the utc representation of time" do
+ # Testing with America/Regina here because it doesn't have DST.
+ with_timezone("CST", -6) do
+ t = Time.local(2007, 1, 9, 6, 0, 0)
+ t.send(@method).should == Time.gm(2007, 1, 9, 12, 0, 0)
+ end
+ end
+end
diff --git a/spec/ruby/core/time/shared/gm.rb b/spec/ruby/core/time/shared/gm.rb
new file mode 100644