diff --git a/.github/actions/setup/directories/action.yml b/.github/actions/setup/directories/action.yml index 4eed78f49e5dcd..247f24236e6b6a 100644 --- a/.github/actions/setup/directories/action.yml +++ b/.github/actions/setup/directories/action.yml @@ -170,7 +170,7 @@ runs: run: | ruby tool/missing-baseruby.bat --verbose bash tool/gen-sources.bash up - echo RUBY_DUMP_AST=true >> "$GITHUB_ENV" + echo RUBY_DUMP_AST=./dump_ast-required-unexpectedly >> "$GITHUB_ENV" - if: steps.which.outputs.sudo shell: bash diff --git a/.github/workflows/tarball-macos.yml b/.github/workflows/tarball-macos.yml index d248338b916d6c..7cff44a8d34d59 100644 --- a/.github/workflows/tarball-macos.yml +++ b/.github/workflows/tarball-macos.yml @@ -45,6 +45,7 @@ jobs: env: ARCHNAME: ${{ inputs.archname }} PREFIX: ${{ matrix.prefix || '/usr/local' }} + RUBY_DUMP_AST: ./dump_ast-required-unexpectedly steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/tarball-non-development.yml b/.github/workflows/tarball-non-development.yml index db6230b301dd9c..cecc1a4bf3ce85 100644 --- a/.github/workflows/tarball-non-development.yml +++ b/.github/workflows/tarball-non-development.yml @@ -18,6 +18,7 @@ jobs: runs-on: ubuntu-24.04 env: ruby_prefix: /tmp/ruby-snapshot + RUBY_DUMP_AST: ./dump_ast-required-unexpectedly steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index 0471e1b6efc77b..8cb0936b81fb4b 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -31,6 +31,7 @@ jobs: runs-on: ${{ matrix.os }} env: ARCHNAME: ${{ inputs.archname }} + RUBY_DUMP_AST: ./dump_ast-required-unexpectedly steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index 0f34ee06ee11e5..e1523dcc17f199 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -44,6 +44,7 @@ jobs: OS_VER: windows-${{ matrix.os }} VCPKG_DEFAULT_TRIPLET: x64-windows FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + RUBY_DUMP_AST: ./dump_ast-required-unexpectedly NoDefaultCurrentDirectoryInExePath: 1 steps: - run: md build diff --git a/array.c b/array.c index adb61a0d4cdeb9..95d817dca12da6 100644 --- a/array.c +++ b/array.c @@ -1943,7 +1943,7 @@ rb_ary_aref1(VALUE ary, VALUE arg) default: if (step == 0) rb_raise(rb_eArgError, "slice step cannot be zero"); len = ary_subseq_len(ary, beg, len); - if (len == 0) return ary_new(klass, 0); + if (len <= 0) return ary_new(klass, 0); if (step == 1) return ary_make_partial(ary, klass, beg, len); return ary_make_partial_step(ary, klass, beg, len, step); } diff --git a/file.c b/file.c index 412bc2e874368d..9088990792f1bc 100644 --- a/file.c +++ b/file.c @@ -1866,14 +1866,22 @@ rb_file_directory_p(VALUE obj, VALUE fname) } /* + * :markup: markdown + * * call-seq: - * File.pipe?(filepath) -> true or false + * File.pipe?(path) -> true or false * - * Returns +true+ if +filepath+ points to a pipe, +false+ otherwise: + * Returns whether the entry at the given `path` is a pipe: * - * File.mkfifo('tmp/fifo') - * File.pipe?('tmp/fifo') # => true - * File.pipe?('t.txt') # => false + * ```ruby + * File.pipe?('doc/syntax/') # => false # Directory. + * File.pipe?('doc/maintainers.md') # => false # Regular file. + * File.pipe?('nosuch') # => false # Non-existent. + * path = '/tmp/foo' + * File.mkfifo(path) + * File.pipe?(path) # => true + * File.delete(path) # Clean up. + * ``` * */ @@ -2082,14 +2090,25 @@ rb_file_exist_p(VALUE obj, VALUE fname) } /* + * :markup: markdown + * * call-seq: - * File.readable?(file_name) -> true or false + * File.readable?(path) -> true or false * - * Returns true if the named file is readable by the effective - * user and group id of this process. See eaccess(3). + * Returns whether the entry at the given `path` + * exists and is readable by the owner and group of the current process; + * see [Permissions](rdoc-ref:file/filesystem_modes.md@Permissions): + * + * ```ruby + * path = '/tmp/secret.txt' + * File.write(path, 'foo') + * File.readable?(path) # => true + * File.chmod(0o000, path) + * File.readable?(path) # => false + * File.delete(path) # Clean up. + * File.readable?('nosuch') # => false + * ``` * - * Note that some OS-level security features may cause this to return true - * even though the file is not readable by the effective user/group. */ static VALUE @@ -2099,14 +2118,13 @@ rb_file_readable_p(VALUE obj, VALUE fname) } /* - * call-seq: - * File.readable_real?(file_name) -> true or false + * :markup: markdown * - * Returns true if the named file is readable by the real - * user and group id of this process. See access(3). + * call-seq: + * File.readable_real?(path) -> true or false * - * Note that some OS-level security features may cause this to return true - * even though the file is not readable by the real user/group. + * Like File.readable?, but checks against the real user and group ids + * instead of the effective ids. */ static VALUE @@ -2381,14 +2399,27 @@ rb_file_size_p(VALUE obj, VALUE fname) } /* + * :markup: markdown + * * call-seq: - * File.owned?(file_name) -> true or false + * File.owned?(object) -> true or false * - * Returns true if the named file exists and the - * effective user id of the calling process is the owner of - * the file. + * Returns whether the given `object` represents a filesystem entry or IO object + * that exists and is owned by the user of the current process: + * + * ```ruby + * filepath = 'doc/t.tmp' + * File.write(filepath, 'foo') + * File.owned?(filepath) # => true + * File.delete(filepath) # Clean up. + * dirpath = 'doc/tmp' + * Dir.mkdir(dirpath) + * File.owned?(dirpath) # => true + * Dir.rmdir(dirpath) # Clean up. + * File.owned?($stdin) # => true + * File.owned?('/etc') # => false + * ``` * - * _file_name_ can be an IO object. */ static VALUE @@ -3851,11 +3882,11 @@ rb_file_s_symlink(VALUE klass, VALUE from, VALUE to) * by the [symbolic link](rdoc-ref:file/symbolic_links.md) at `link_path`: * * ```ruby - * filepath = 'README.md' - * linkpath = 'foo' + * filepath = 'doc/maintainers.md' + * linkpath = '/tmp/link' * File.symlink(filepath, linkpath) - * File.readlink(linkpath) # => "README.md" - * File.unlink(linkpath) # Clean up. + * File.readlink(linkpath) # => "doc/maintainers.md" + * File.delete(linkpath) # Clean up. * ``` * * Raises Errno::EINVAL if the entry referenced by `link_path` @@ -6817,11 +6848,22 @@ rb_stat_d(VALUE obj) } /* + * :markup: markdown + * * call-seq: - * stat.pipe? -> true or false + * stat.pipe? -> true or false + * + * Returns whether the entry at the path in `self` is a pipe: + * + * ```ruby + * File.stat('doc/syntax/').pipe? # => false # Directory . + * File.stat('doc/maintainers.md').pipe? # => false # Regular file. + * path = '/tmp/foo' + * File.mkfifo(path) + * File.stat(path).pipe? # => true + * File.delete(path) # Clean up. + * ``` * - * Returns true if the operating system supports pipes and - * stat is a pipe; false otherwise. */ static VALUE @@ -6930,14 +6972,31 @@ rb_stat_c(VALUE obj) } /* + * :markup: markdown + * * call-seq: - * stat.owned? -> true or false + * owned? -> true or false * - * Returns true if the effective user id of the process is - * the same as the owner of stat. + * Returns whether `self` represents a filesystem entry that, + * at the time `self` was created, + * existed and was owned by the user of the current process; + * see [Snapshot](rdoc-ref:File::Stat@Snapshot): * - * File.stat("testfile").owned? #=> true - * File.stat("/etc/passwd").owned? #=> false + * ```ruby + * filepath = 'doc/t.tmp' + * File.write(filepath, 'foo') + * filestat = File.stat(filepath) + * filestat.owned? # => true + * File.delete(filepath) + * filestat.owned? # => true # Snapshot unchanged. + * dirpath = 'doc/tmp' + * Dir.mkdir(dirpath) + * dirstat = File.stat(dirpath) + * dirstat.owned? # => true + * Dir.rmdir(dirpath) + * dirstat.owned? # => true # Snapshot unchanged. + * File.stat('/etc').owned? # => false + * ``` * */ @@ -6981,13 +7040,23 @@ rb_stat_grpowned(VALUE obj) } /* + * :markup: markdown + * * call-seq: - * stat.readable? -> true or false + * readable? -> true or false * - * Returns true if stat is readable by the - * effective user id of this process. + * Returns whether the entry represented by `self` + * exists and is readable by the owner and group of the current process; + * see [Permissions](rdoc-ref:file/filesystem_modes.md@Permissions): * - * File.stat("testfile").readable? #=> true + * ```ruby + * path = '/tmp/secret.txt' + * File.write(path, 'foo') + * File.stat(path).readable? # => true + * File.chmod(0o000, path) + * File.stat(path).readable? # => false + * File.delete(path) # Clean up. + * ``` * */ @@ -7014,14 +7083,13 @@ rb_stat_r(VALUE obj) } /* + * :markup: markdown + * * call-seq: * stat.readable_real? -> true or false * - * Returns true if stat is readable by the real - * user id of this process. - * - * File.stat("testfile").readable_real? #=> true - * + * Like #readable?, but checks against the real user and group ids + * instead of the effective ids. */ static VALUE diff --git a/gc/mmtk/src/heap/ruby_heap_trigger.rs b/gc/mmtk/src/heap/ruby_heap_trigger.rs index e3d7b05c93d1d9..b152a31d944b0d 100644 --- a/gc/mmtk/src/heap/ruby_heap_trigger.rs +++ b/gc/mmtk/src/heap/ruby_heap_trigger.rs @@ -27,6 +27,7 @@ pub struct RubyHeapTriggerConfig { pub struct RubyHeapTrigger { /// Target number of heap pages target_heap_pages: AtomicUsize, + pending_pages: AtomicUsize, } impl GCTriggerPolicy for RubyHeapTrigger { @@ -40,10 +41,20 @@ impl GCTriggerPolicy for RubyHeapTrigger { plan.collection_required(space_full, space) } + fn on_pending_allocation(&self, pages: usize) { + self.pending_pages.fetch_add(pages, Ordering::SeqCst); + } + fn on_pause_end(&self, mmtk: &'static MMTK) { - if let Some(plan) = mmtk.get_plan().generational() { - if plan.is_current_gc_nursery() { - return; + let pending_pages = self.pending_pages.swap(0, Ordering::SeqCst); + + // Nursery GCs don't resize the heap, unless a failed allocation is + // waiting on us to make room for it. + if pending_pages == 0 { + if let Some(plan) = mmtk.get_plan().generational() { + if plan.is_current_gc_nursery() { + return; + } } } @@ -53,14 +64,17 @@ impl GCTriggerPolicy for RubyHeapTrigger { (used_pages as f64 * (1.0 + Self::get_config().heap_pages_min_ratio)) as usize; let target_max = (used_pages as f64 * (1.0 + Self::get_config().heap_pages_max_ratio)) as usize; + // Grow the heap by the goal ratio over the live size, plus whatever + // is needed to fit allocations that failed and triggered this GC. let new_target = (((used_pages as f64) * (1.0 + Self::get_config().heap_pages_goal_ratio)) as usize) + .saturating_add(pending_pages) .clamp( Self::get_config().min_heap_pages, Self::get_config().max_heap_pages, ); - if used_pages < target_min || used_pages > target_max { + if pending_pages > 0 || used_pages < target_min || used_pages > target_max { self.target_heap_pages.store(new_target, Ordering::Relaxed); } } @@ -88,6 +102,7 @@ impl Default for RubyHeapTrigger { Self { target_heap_pages: AtomicUsize::new(min_heap_pages), + pending_pages: AtomicUsize::new(0), } } } @@ -122,6 +137,7 @@ mod tests { RubyHeapTrigger { target_heap_pages: AtomicUsize::new(target_heap_pages), + pending_pages: AtomicUsize::new(0), } } diff --git a/lib/fileutils.rb b/lib/fileutils.rb index 9c894b7a30e99b..85b069bc875c0b 100644 --- a/lib/fileutils.rb +++ b/lib/fileutils.rb @@ -715,7 +715,7 @@ def cp_lr(src, dest, noop: nil, verbose: nil, # Keyword arguments: # # - force: true - overwrites +dest+ if it exists. - # - relative: false - create links relative to +dest+. + # - relative: true - create links relative to +dest+. # - noop: true - does not create links. # - verbose: true - prints an equivalent command: # @@ -783,7 +783,7 @@ def ln_sr(src, dest, target_directory: true, force: nil, noop: nil, verbose: nil n = real_ddirs.size - i n -= 1 unless target_directory link2 = fu_clean_components(*Array.new([n, 0].max, '..'), *real_sdirs[i..-1]) - link1 = link2 if link1.size > link2.size + link1 = link2 if !link2.empty? and link1.size > link2.size end s = File.join(link1) fu_output_message [cmd, s, d].flatten.join(' ') if verbose @@ -2066,10 +2066,6 @@ def fu_windows?; true end #:nodoc: def fu_windows?; false end #:nodoc: end - def fu_copy_stream0(src, dest, blksize = nil) #:nodoc: - IO.copy_stream(src, dest) - end - def fu_stream_blksize(*streams) #:nodoc: streams.each do |s| next unless s.respond_to?(:stat) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 1a200256bd2b06..ad1700b8dc5cd1 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1776,11 +1776,11 @@ def open(...) # :yield: file # at the path stored in `self`: # # ```ruby - # file_pn = Pathname('README.md') - # link_pn = Pathname('foo') + # file_pn = Pathname('doc/maintainers.md') + # link_pn = Pathname('/tmp/link') # link_pn.make_symlink(file_pn) - # link_pn.readlink # => # - # link_pn.unlink # Clean up. + # link_pn.readlink # => # + # link_pn.delete # Clean up. # ``` # # Raises Errno::EINVAL if the path in `self` is not the path to a symbolic link. @@ -2479,14 +2479,14 @@ def file?() FileTest.file?(@path) end # call-seq: # pipe? -> true or false # - # Returns whether entry at the path in `self` is a pipe: + # Returns whether the entry at the path in `self` is a pipe: # # ```ruby + # Pathname('.').pipe? # => false # path = '/tmp/foo' # File.mkfifo(path) # pn = Pathname(path) # => # # pn.pipe? # => true - # Pathname('.').pipe? # => false # pn.delete # Clean up. # ``` # @@ -2527,11 +2527,11 @@ def socket?() FileTest.socket?(@path) end # pn = Pathname('doc/t.tmp') # pn.write('foo') # pn.owned? # => true - # pn.delete + # pn.delete # Clean up. # pn = Pathname('doc/tmp') # pn.mkdir # pn.owned? # => true - # pn.rmdir + # pn.rmdir # Clean up. # Pathname('/etc').owned? # => false # ``` # @@ -2543,7 +2543,8 @@ def owned?() FileTest.owned?(@path) end # readable? -> true or false # # Returns whether the entry at the path in `self` - # is readable by the owner and group of the current process: + # exists and is readable by the owner and group of the current process; + # see [Permissions](rdoc-ref:file/filesystem_modes.md@Permissions): # # ```ruby # pn = Pathname('/tmp/secret.txt') diff --git a/string.c b/string.c index 7c8f7de12c3db9..c890a03222b546 100644 --- a/string.c +++ b/string.c @@ -4801,8 +4801,8 @@ rb_str_byteindex_m(int argc, VALUE *argv, VALUE str) long pos; if (rb_scan_args(argc, argv, "11", &sub, &initpos) == 2) { - long slen = RSTRING_LEN(str); pos = NUM2LONG(initpos); + long slen = RSTRING_LEN(str); if (pos < 0 ? (pos += slen) < 0 : pos > slen) { if (RB_TYPE_P(sub, T_REGEXP)) { rb_backref_set(Qnil); @@ -5076,10 +5076,11 @@ rb_str_byterindex_m(int argc, VALUE *argv, VALUE str) { VALUE sub; VALUE initpos; - long pos, len = RSTRING_LEN(str); + long pos; if (rb_scan_args(argc, argv, "11", &sub, &initpos) == 2) { pos = NUM2LONG(initpos); + long len = RSTRING_LEN(str); if (pos < 0 && (pos += len) < 0) { if (RB_TYPE_P(sub, T_REGEXP)) { rb_backref_set(Qnil); @@ -5089,7 +5090,7 @@ rb_str_byterindex_m(int argc, VALUE *argv, VALUE str) if (pos > len) pos = len; } else { - pos = len; + pos = RSTRING_LEN(str); } str_ensure_byte_pos(str, pos); @@ -9794,6 +9795,11 @@ tr_trans_pairs(VALUE str, VALUE pairs_val) rb_hash_foreach(pairs_val, tr_trans_pairs_coerce_i, (VALUE)&coerce_args); rb_encoding *e1 = coerce_args.enc; + /* Keys could be deleted from pairs_val during rb_hash_foreach when coercing + * the keys/values, so we need to update pairs_count to the number of pairs we + * were actually able to extract from pairs_val. */ + pairs_count = coerce_args.index; + VALUE hash = 0; const unsigned char *sstart = (unsigned char *)RSTRING_PTR(str); diff --git a/test/fileutils/test_fileutils.rb b/test/fileutils/test_fileutils.rb index 92308d95573206..27511c2f50d0ea 100644 --- a/test/fileutils/test_fileutils.rb +++ b/test/fileutils/test_fileutils.rb @@ -41,6 +41,32 @@ def have_file_perm? /mswin|mingw|bcc|emx/ !~ RUBY_PLATFORM end + @@assignable_groups = nil + + # Filter the given group IDs down to those the current process can actually + # assign to a file with chown. Some environments (e.g. user-namespace + # containers) report supplementary groups such as the overflow GID + # (65534/nobody) that the kernel refuses to chgrp to; without this the + # group-ownership tests would fail with EPERM instead of being skipped. + def assignable_groups(groups) + @@assignable_groups ||= {} + groups.select do |gid| + @@assignable_groups.fetch(gid) do + Dir.mktmpdir("fileutils") do |dir| + probe = File.join(dir, "probe") + File.write(probe, "") + @@assignable_groups[gid] = + begin + File.chown(nil, gid, probe) + true + rescue Errno::EPERM + false + end + end + end + end + end + @@have_symlink = nil def have_symlink? @@ -182,7 +208,7 @@ def mymkdir(path) def setup @prevdir = Dir.pwd - @groups = [Process.gid] | Process.groups if have_file_perm? + @groups = assignable_groups([Process.gid] | Process.groups) if have_file_perm? tmproot = @tmproot = Dir.mktmpdir "fileutils" Dir.chdir tmproot my_rm_rf 'data'; mymkdir 'data' @@ -769,7 +795,7 @@ def test_rm_r_pathname def test_rm_r_no_permissions check_singleton :rm_rf - return if /mswin|mingw/ =~ RUBY_PLATFORM + return if root_in_posix? mkdir 'tmpdatadir' touch 'tmpdatadir/tmpdata' @@ -979,6 +1005,39 @@ def test_ln_s end end if have_symlink? and !no_broken_symlink? + def test_ln_s_relative_to_symlinked_directory + mkdir_p 'tmp/symlink_dir/.dotfiles/zsh' + mkdir_p 'tmp/symlink_dir/.config' + + src = File.expand_path('tmp/symlink_dir/.dotfiles/zsh') + dest = File.expand_path('tmp/symlink_dir/.config/zsh') + + assert_output_lines(["ln -s ../.dotfiles/zsh #{dest}"]) { + ln_s src, dest, relative: true, verbose: true, noop: true + } + + ln_s src, dest, relative: true + assert_file.symlink?(dest) + assert_equal '../.dotfiles/zsh', File.readlink(dest) + + lnfname = File.join(dest, 'zsh') + + if /mingw|mswin/ =~ RUBY_PLATFORM + unless // =~ IO.popen({"DIRCMD"=>nil}, "dir zsh", chdir: File.dirname(dest), &:read) + omit "[Bug #22338]" + end + end + + assert_output_lines(["ln -s ../../.dotfiles/zsh #{lnfname}"]) { + ln_s src, dest, relative: true, verbose: true, noop: true + } + + ln_s src, dest, relative: true + assert_file.symlink?(lnfname) + assert_equal '../../.dotfiles/zsh', File.readlink(lnfname) + assert_equal File.realpath(src), File.realpath(lnfname) + end if have_symlink? and !no_broken_symlink? + def test_ln_s_broken_symlink assert_nothing_raised { ln_s 'symlink', 'tmp/symlink' @@ -2000,6 +2059,37 @@ def test_touch check_singleton :touch end + def test_touch_verbose + assert_output_lines(["touch file"]) do + touch('file', verbose: true, noop: true) + end + assert_output_lines(["touch -c file"]) do + touch('file', verbose: true, noop: true, nocreate: true) + end + t = Time.new(2026, 5, 4, 3, 2, 1) + assert_output_lines(["touch -t 202605040302.01 file"]) do + touch('file', verbose: true, noop: true, mtime: t) + end + end + + def test_touch_create + t0 = Time.now - 10 # discrepancies caused by remote file systems? + assert_file.not_exist?('file') + assert_raise(Errno::ENOENT) {touch('file', nocreate: true)} + assert_file.not_exist?('file') + touch('file') + assert_file.exist?('file') + t = File.mtime('file') + assert_operator(t, :>=, t0) + assert_operator(t, :<=, Time.now + 10) + end + + def test_touch_mtime + t = Time.new(2026, 5, 4, 3, 2, 1) + touch('file', mtime: t) + assert_equal(t, File.mtime('file')) + end + def test_collect_methods end diff --git a/test/ruby/test_array.rb b/test/ruby/test_array.rb index 62373ab2f06722..d529788edd1722 100644 --- a/test/ruby/test_array.rb +++ b/test/ruby/test_array.rb @@ -1833,6 +1833,33 @@ def test_slice_out_of_range assert_equal([100], a.slice(-1, 1_000_000_000)) end + def test_slice_shrinking_array_by_to_int + bug22325 = '[Bug #22325]' + cls = Class.new(Numeric) do + attr_reader :val + def initialize(ary, val) + @ary = ary + @val = val + end + def <=>(other) + val <=> (other.is_a?(self.class) ? other.val : other) + end + def to_int + @ary.clear + val + end + def coerce(other) + [other, val] + end + end + + ary = @cls[*(1..100).to_a] + assert_equal([], ary[Range.new(cls.new(ary, 50), cls.new(ary, 60))], bug22325) + + ary.replace((1..100).to_a) + assert_equal([], ary[Range.new(cls.new(ary, 50), cls.new(ary, 60)).step(2)], bug22325) + end + def test_slice_gc_compact_stress EnvUtil.under_gc_compact_stress { assert_equal([1, 2, 3, 4, 5], (0..10).to_a[1, 5]) } EnvUtil.under_gc_compact_stress do diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 0220a5f1e6803f..8e2b3f7e9bb5fd 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -3013,6 +3013,18 @@ def test_tr_hash assert_equal(expected, actual) end + def test_tr_hash_modify + replacements = {} + obj = Object.new + obj.define_singleton_method(:to_str) do + replacements.clear + "a" + end + replacements[obj] = "x" + ("b".."z").each { |c| replacements[c] = (c.ord + 1).chr } + assert_equal(S("xb"), S("ab").tr(replacements)) + end + def test_tr! a = S("hello") b = a.dup @@ -4230,6 +4242,16 @@ def o.to_str; "bar"; end assert !1000.times.any? {s.byteindex("", 100_000_000)} end + def test_byteindex_modify_source + s = S("héllo" * 1000) + obj = Object.new + obj.define_singleton_method(:to_int) do + s.replace("é" * 50) + 4500 + end + assert_nil(s.byteindex("l", obj)) + end + def test_byterindex assert_byterindex(3, S("hello"), ?l) assert_byterindex(6, S("ell, hello"), S("ell")) @@ -4284,6 +4306,16 @@ def o.to_str; "bar"; end assert_byterindex(nil, S(""), S("こんにちは")) end + def test_byterindex_modify_source + s = S("héllo" * 1000) + obj = Object.new + obj.define_singleton_method(:to_int) do + s.replace("é" * 50) + 4500 + end + assert_nil(s.byterindex("l", obj)) + end + def test_bytesplice assert_bytesplice_raise(IndexError, S("hello"), -6, 0, "bye") assert_bytesplice_result("byehello", S("hello"), -5, 0, "bye")