From d6b621115c209346dc7230b9345c9f547dc3cff6 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 13:39:04 +0200 Subject: [PATCH 1/9] Run pathname specs from ruby/spec in CI * Provides extra testing and coverage. --- .github/workflows/test.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ed8b613..92b03e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,3 +40,20 @@ jobs: run: bundle install - name: Run test run: rake compile test + + spec: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + - uses: actions/checkout@v5 + with: + repository: ruby/spec + path: rubyspec + - name: Clone MSpec + run: git clone https://github.com/ruby/mspec.git ../mspec + - run: bundle install + - run: rake compile + - run: ../mspec/bin/mspec -Ilib rubyspec/library/pathname From 438fad2cc2c867a7ec8053179b1d72039b6de721 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 13:46:11 +0200 Subject: [PATCH 2/9] The Pathname method should be a module_function of Kernel * Found by ruby/spec, and confirmed pathname.c did the same. --- lib/pathname.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index 03edc42..9e0aed8 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -1219,5 +1219,5 @@ def Pathname(path) # :doc: return path if Pathname === path Pathname.new(path) end - private :Pathname + module_function :Pathname end From 96000f6061039421ff942783f18a5171f4efbe76 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 14:06:17 +0200 Subject: [PATCH 3/9] Pathname#initialize should raise TypeError if coercion fails * Found by ruby/spec, and confirmed pathname.c did the same. --- lib/pathname.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index 9e0aed8..aadcc00 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -238,7 +238,11 @@ class Pathname # If +path+ contains a NUL character (\0), an ArgumentError is raised. # def initialize(path) - path = path.to_path if path.respond_to? :to_path + unless String === path + path = path.to_path if path.respond_to? :to_path + raise TypeError unless String === path + end + if path.include?("\0") raise ArgumentError, "pathname contains \\0: #{path.inspect}" end From f21d8e4411c812d4fd2257cfdb82f21b3661c0cf Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 14:07:49 +0200 Subject: [PATCH 4/9] Add back Pathname#realdirpath * This has no test in test_pathname.rb, that is why it was missed. * Found by ruby/spec, and confirmed pathname.c did the same. --- lib/pathname.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pathname.rb b/lib/pathname.rb index aadcc00..cf500d4 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -986,6 +986,13 @@ def split() # # All components of the pathname must exist when this method is called. def realpath(...) self.class.new(File.realpath(@path, ...)) end + + # Returns the real (absolute) pathname of +self+ in the actual filesystem. + # + # Does not contain symlinks or useless dots, +..+ and +.+. + # + # The last component of the real pathname can be nonexistent. + def realdirpath(...) self.class.new(File.realdirpath(@path, ...)) end end From 03c20fb99cd05dd52de536876d3e58274aa7c715 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 22 Aug 2025 15:47:51 +0900 Subject: [PATCH 5/9] Suppress CodeQL warnings to use File methods instead of IO --- lib/pathname.rb | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index cf500d4..881c05a 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -146,6 +146,12 @@ module ::Kernel # === File property and manipulation methods # # These methods are a facade for File: +# - #each_line(*args, &block) +# - #read(*args) +# - #binread(*args) +# - #readlines(*args) +# - #write(*args) +# - #binwrite(*args) # - #atime # - #birthtime # - #ctime @@ -186,14 +192,8 @@ module ::Kernel # # === IO # -# These methods are a facade for IO: -# - #each_line(*args, &block) -# - #read(*args) -# - #binread(*args) -# - #readlines(*args) +# This method is a facade for IO: # - #sysopen(*args) -# - #write(*args) -# - #binwrite(*args) # # === Utilities # @@ -857,6 +857,11 @@ def relative_path_from(base_directory) end class Pathname # * IO * + # See IO.sysopen. + def sysopen(...) IO.sysopen(@path, ...) end +end + +class Pathname # * File * # # #each_line iterates over the line in the file. It yields a String object # for each line. @@ -864,34 +869,27 @@ class Pathname # * IO * # This method has existed since 1.8.1. # def each_line(...) # :yield: line - IO.foreach(@path, ...) + File.foreach(@path, ...) end - # See IO.read. Returns all data from the file, or the first +N+ bytes + # See File.read. Returns all data from the file, or the first +N+ bytes # if specified. - def read(...) IO.read(@path, ...) end + def read(...) File.read(@path, ...) end - # See IO.binread. Returns all the bytes from the file, or the first +N+ + # See File.binread. Returns all the bytes from the file, or the first +N+ # if specified. - def binread(...) IO.binread(@path, ...) end - - # See IO.readlines. Returns all the lines from the file. - def readlines(...) IO.readlines(@path, ...) end + def binread(...) File.binread(@path, ...) end - # See IO.sysopen. - def sysopen(...) IO.sysopen(@path, ...) end + # See File.readlines. Returns all the lines from the file. + def readlines(...) File.readlines(@path, ...) end # Writes +contents+ to the file. See File.write. - def write(...) IO.write(@path, ...) end + def write(...) File.write(@path, ...) end # Writes +contents+ to the file, opening it in binary mode. # # See File.binwrite. - def binwrite(...) IO.binwrite(@path, ...) end -end - - -class Pathname # * File * + def binwrite(...) File.binwrite(@path, ...) end # See File.atime. Returns last access time. def atime() File.atime(@path) end From c820afdea3326228bc9b2daa670778b0dd39331a Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 14:14:46 +0200 Subject: [PATCH 6/9] Use File.sysopen for consistency * There is no point to call IO.sysopen and that's the last IO method, with Pathname we know it's always a file path. --- lib/pathname.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index 881c05a..cdf4249 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -150,6 +150,7 @@ module ::Kernel # - #read(*args) # - #binread(*args) # - #readlines(*args) +# - #sysopen(*args) # - #write(*args) # - #binwrite(*args) # - #atime @@ -190,11 +191,6 @@ module ::Kernel # - #mkdir(*args) # - #opendir(*args) # -# === IO -# -# This method is a facade for IO: -# - #sysopen(*args) -# # === Utilities # # These methods are a mixture of Find, FileUtils, and others: @@ -856,11 +852,6 @@ def relative_path_from(base_directory) end end -class Pathname # * IO * - # See IO.sysopen. - def sysopen(...) IO.sysopen(@path, ...) end -end - class Pathname # * File * # # #each_line iterates over the line in the file. It yields a String object @@ -883,6 +874,9 @@ def binread(...) File.binread(@path, ...) end # See File.readlines. Returns all the lines from the file. def readlines(...) File.readlines(@path, ...) end + # See File.sysopen. + def sysopen(...) File.sysopen(@path, ...) end + # Writes +contents+ to the file. See File.write. def write(...) File.write(@path, ...) end From 08e3d92a51b61f6308a18ff4fdabf01325d3483e Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 14:25:17 +0200 Subject: [PATCH 7/9] Fix TestPathname#has_symlink? it was returning false on Linux --- test/pathname/test_pathname.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/pathname/test_pathname.rb b/test/pathname/test_pathname.rb index 7e0011c..b837f63 100644 --- a/test/pathname/test_pathname.rb +++ b/test/pathname/test_pathname.rb @@ -348,7 +348,7 @@ def has_symlink? rescue NotImplementedError return false rescue Errno::ENOENT - return false + return true rescue Errno::EACCES return false end From da66ef75e71e1f528aa04f0aec286d51754bb66b Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 14:26:40 +0200 Subject: [PATCH 8/9] Fix Pathname#lutime test and add the method back --- lib/pathname.rb | 7 +++++++ test/pathname/test_pathname.rb | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/pathname.rb b/lib/pathname.rb index cdf4249..1c4ec6e 100644 --- a/lib/pathname.rb +++ b/lib/pathname.rb @@ -952,6 +952,13 @@ def truncate(length) File.truncate(@path, length) end # See File.utime. Update the access and modification times. def utime(atime, mtime) File.utime(atime, mtime, @path) end + # Update the access and modification times of the file. + # + # Same as Pathname#utime, but does not follow symbolic links. + # + # See File.lutime. + def lutime(atime, mtime) File.lutime(atime, mtime, @path) end + # See File.basename. Returns the last component of the path. def basename(...) self.class.new(File.basename(@path, ...)) end diff --git a/test/pathname/test_pathname.rb b/test/pathname/test_pathname.rb index b837f63..43d6304 100644 --- a/test/pathname/test_pathname.rb +++ b/test/pathname/test_pathname.rb @@ -1054,7 +1054,11 @@ def test_lutime latime = Time.utc(2000) lmtime = Time.utc(1999) File.symlink("a", "l") - Pathname("l").utime(latime, lmtime) + begin + Pathname("l").lutime(latime, lmtime) + rescue NotImplementedError + next + end s = File.lstat("a") ls = File.lstat("l") assert_equal(atime, s.atime) From df5fe0046f04333c0867dede723b8896280f33d4 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Sat, 23 Aug 2025 14:41:35 +0200 Subject: [PATCH 9/9] Exclude a couple tests failing on JRuby for multiple reasons * https://github.com/jruby/jruby/issues/8972 but also at least 2 more incompatibilities. --- test/pathname/test_pathname.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/pathname/test_pathname.rb b/test/pathname/test_pathname.rb index 43d6304..e80473e 100644 --- a/test/pathname/test_pathname.rb +++ b/test/pathname/test_pathname.rb @@ -370,10 +370,11 @@ def has_hardlink? end def realpath(path, basedir=nil) - Pathname.new(path).realpath(basedir).to_s + Pathname.new(path).realpath(*basedir).to_s end def test_realpath + omit "not working yet" if RUBY_ENGINE == "jruby" return if !has_symlink? with_tmpchdir('rubytest-pathname') {|dir| assert_raise(Errno::ENOENT) { realpath("#{dir}/not-exist") } @@ -434,6 +435,7 @@ def realdirpath(path) end def test_realdirpath + omit "not working yet" if RUBY_ENGINE == "jruby" return if !has_symlink? Dir.mktmpdir('rubytest-pathname') {|dir| rdir = realpath(dir)