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")