diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72e84fc..73a77d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,9 +23,9 @@ jobs: strategy: fail-fast: false matrix: - # Rutie 0.10, which the extension is built with, supports Ruby 2.5 to 2.7 + # Rutie 0.13, which the extension is built with, supports Ruby 3.2 to 3.4 os: [ubuntu-22.04, windows-latest] - ruby: ['2.5', '2.6', '2.7'] + ruby: ['3.2', '3.3', '3.4'] suite: - name: regression env: WITH_REGRESSION=true @@ -35,7 +35,7 @@ jobs: env: ENCODING=true TEST_MONKEYPATCHES=true WITH_REGRESSION=true include: - os: macos-latest - ruby: '2.7' + ruby: '3.4' suite: name: regression env: WITH_REGRESSION=true @@ -65,10 +65,10 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - ruby: ['2.5', '2.6', '2.7'] + ruby: ['3.2', '3.3', '3.4'] include: - os: macos-latest - ruby: '2.7' + ruby: '3.4' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable diff --git a/.rubocop.yml b/.rubocop.yml index 1bfaf38..129d8e0 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,4 +1,10 @@ +plugins: + - rubocop-performance + AllCops: + TargetRubyVersion: 3.2 + NewCops: disable + SuggestExtensions: false Include: - 'Gemfile' - 'Rakefile' @@ -88,7 +94,7 @@ Layout/EndAlignment: Description: 'Align ends correctly.' Enabled: true -Lint/EndInMethod: +Style/EndBlock: Description: 'END blocks should not be placed inside method definitions.' Enabled: true @@ -105,7 +111,7 @@ Lint/FormatParameterMismatch: Description: 'The number of parameters to format/sprint must match the fields.' Enabled: true -Lint/HandleExceptions: +Lint/SuppressedException: Description: "Don't suppress exception." StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#dont-hide-exceptions' Enabled: true @@ -154,7 +160,7 @@ Lint/ShadowingOuterLocalVariable: for block arguments or block local variables. Enabled: true -Lint/StringConversionInInterpolation: +Lint/RedundantStringCoercion: Description: 'Checks for Object#to_s usage in string interpolation.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-to-s' Enabled: true @@ -186,7 +192,7 @@ Lint/UselessAssignment: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars' Enabled: true -Lint/UselessComparison: +Lint/BinaryOperatorWithIdenticalOperands: Description: 'Checks for comparison of something with itself.' Enabled: true @@ -229,7 +235,7 @@ Metrics/CyclomaticComplexity: of test cases needed to validate a method. Enabled: true -Metrics/LineLength: +Layout/LineLength: Description: 'Limit lines to 80 characters.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits' Enabled: false @@ -290,7 +296,7 @@ Performance/ReverseEach: Reference: 'https://github.com/JuanitoFatas/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code' Enabled: true -Performance/Sample: +Style/Sample: Description: >- Use `sample` instead of `shuffle.first`, `shuffle.last`, and `shuffle[Fixnum]`. @@ -333,7 +339,7 @@ Performance/EndWith: Performance/FixedSize: Enabled: true -Performance/LstripRstrip: +Style/Strip: Enabled: true Performance/RangeInclude: @@ -348,7 +354,7 @@ Performance/RedundantMatch: Performance/RedundantMerge: Enabled: true -Performance/RedundantSortBy: +Style/RedundantSortBy: Enabled: true Performance/RegexpMatch: @@ -360,58 +366,6 @@ Performance/StartWith: Performance/TimesMap: Enabled: true -##################### Rails ################################## - -Rails/ActionFilter: - Description: 'Enforces consistent use of action filter methods.' - Enabled: false - -Rails/Date: - Description: >- - Checks the correct usage of date aware methods, - such as Date.today, Date.current etc. - Enabled: false - -Rails/Delegate: - Description: 'Prefer delegate method for delegations.' - Enabled: false - -Rails/FindBy: - Description: 'Prefer find_by over where.first.' - Enabled: false - -Rails/FindEach: - Description: 'Prefer all.find_each over all.find.' - Enabled: false - -Rails/HasAndBelongsToMany: - Description: 'Prefer has_many :through to has_and_belongs_to_many.' - Enabled: false - -Rails/Output: - Description: 'Checks for calls to puts, print, etc.' - Enabled: false - -Rails/ReadWriteAttribute: - Description: >- - Checks for read_attribute(:attr) and - write_attribute(:attr, val). - Enabled: false - -Rails/ScopeArgs: - Description: 'Checks the arguments of ActiveRecord scopes.' - Enabled: false - -Rails/TimeZone: - Description: 'Checks the correct usage of time zone aware methods.' - StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time' - Reference: 'http://danilenko.org/2012/7/6/rails_timezones' - Enabled: false - -Rails/Validation: - Description: 'Use validates :attribute, hash of validations.' - Enabled: false - ################## Style ################################# Layout/AccessModifierIndentation: @@ -428,20 +382,20 @@ Style/Alias: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#alias-method' Enabled: false -Layout/AlignArray: +Layout/ArrayAlignment: Description: >- Align the elements of an array literal if they span more than one line. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#align-multiline-arrays' Enabled: true -Layout/AlignHash: +Layout/HashAlignment: Description: >- Align the elements of a hash literal if they span more than one line. Enabled: true -Layout/AlignParameters: +Layout/ParameterAlignment: Description: >- Align the parameters of a method call if they span more than one line. WRONG! Only indent 2 (don't use this). @@ -500,10 +454,6 @@ Style/BlockDelimiters: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' Enabled: true -Style/BracesAroundHashParameters: - Description: 'Enforce braces style around hash parameters.' - Enabled: false - Style/CaseEquality: Description: 'Avoid explicit use of the case equality operator(===).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-case-equality' @@ -640,11 +590,6 @@ Style/EmptyLiteral: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#literal-array-hash' Enabled: false -Style/EndBlock: - Description: 'Avoid the use of END blocks.' - StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-END-blocks' - Enabled: true - Layout/EndOfLine: Description: 'Use Unix-style line endings.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#crlf' @@ -673,7 +618,7 @@ Layout/FirstParameterIndentation: Description: 'Checks the indentation of the first parameter in a method call.' Enabled: false -Style/FlipFlop: +Lint/FlipFlop: Description: 'Checks for flip flops' StyleGuide: 'https://blog.newrelic.com/2015/02/24/weird-ruby-part-3-fun-flip-flop-phenom/' Enabled: true @@ -727,13 +672,13 @@ Layout/IndentationWidth: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' Enabled: true -Layout/IndentArray: +Layout/FirstArrayElementIndentation: Description: >- Checks the indentation of the first element in an array literal. Enabled: false -Layout/IndentHash: +Layout/FirstHashElementIndentation: Description: 'Checks the indentation of the first key in a hash literal.' Enabled: false @@ -905,7 +850,7 @@ Style/PerlBackrefs: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-perl-regexp-last-matchers' Enabled: true -Naming/PredicateName: +Naming/PredicatePrefix: Description: 'Check the names of predicate methods.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bool-methods-qmark' Enabled: false @@ -1111,12 +1056,12 @@ Style/SymbolProc: Description: 'Use symbols as procs instead of blocks when possible.' Enabled: false -Layout/Tab: +Layout/IndentationStyle: Description: 'No hard tabs.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation' Enabled: true -Layout/TrailingBlankLines: +Layout/TrailingEmptyLines: Description: 'Checks trailing blank lines and final newline.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#newline-eof' Enabled: true @@ -1153,11 +1098,11 @@ Style/UnlessElse: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-else-with-unless' Enabled: true -Style/UnneededCapitalW: +Style/RedundantCapitalW: Description: 'Checks for %W when interpolation is not needed.' Enabled: true -Style/UnneededPercentQ: +Style/RedundantPercentQ: Description: 'Checks for %q/%Q when single quotes or double quotes would do.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q' Enabled: true diff --git a/Cargo.lock b/Cargo.lock index 0c87d7c..e53fcdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "rutie" -version = "0.10.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97641d6bd3731d263d914141e960a8b515bf5ff4ca6c961b25ca4b2694c6490" +checksum = "30d1529470ad0b69b81a6d0224632d00c01b46ef671452e24407512165b7ed84" dependencies = [ "lazy_static", "libc", diff --git a/Cargo.toml b/Cargo.toml index 0982b33..ef15e93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,5 @@ name = "faster_path" crate-type = ["cdylib"] [dependencies] -rutie = "0.10.2" +rutie = "0.13.1" memchr = "2" diff --git a/README.md b/README.md index e160186..7ea200e 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ As mentioned earlier Sprockets, which handles assets, changed away from using `P ## Requirements -* Ruby 2.5, 2.6 or 2.7 (the Rust extension is built with [Rutie](https://github.com/danielpclark/rutie) 0.10, which supports these) +* Ruby 3.2, 3.3 or 3.4 (the Rust extension is built with [Rutie](https://github.com/danielpclark/rutie) 0.13, which supports these) * Rust 1.71 or later ## Installation @@ -130,7 +130,7 @@ curl -sSf https://sh.rustup.rs | sh Add this line to your application's Gemfile: ```ruby -gem 'faster_path', '~> 0.4.0' +gem 'faster_path', '~> 0.5.0' ``` And then execute: @@ -159,25 +159,25 @@ require "faster_path" Current methods implemented: -|FasterPath Rust Implementation|Ruby Implementation|Time Shaved Off (Ruby 2.7)|Time Shaved Off (Ruby 2.5)| -|---|---|:---:|:---:| -| `FasterPath.absolute?` | `Pathname#absolute?` | 95.7% | 96.7% | -| `FasterPath.add_trailing_separator` | `Pathname#add_trailing_separator` | 85.4% | 83.0% | -| `FasterPath.basename` | `File.basename` | 42.4% | 39.1% | -| `FasterPath.children` | `Pathname#children` | 54.7% | 47.8% | -| `FasterPath.chop_basename` | `Pathname#chop_basename` | 70.4% | 75.1% | -| `FasterPath.cleanpath_aggressive` | `Pathname#cleanpath_aggressive` | 90.3% | 90.6% | -| `FasterPath.cleanpath_conservative` | `Pathname#cleanpath_conservative` | 89.6% | 91.6% | -| `FasterPath.del_trailing_separator` | `Pathname#del_trailing_separator` | 85.3% | 87.6% | -| `FasterPath.directory?` | `Pathname#directory?` | 34.3% | 37.5% | -| `FasterPath.dirname` | `File.dirname` | 56.8% | 54.7% | -| `FasterPath.entries` | `Pathname#entries` | 38.0% | 33.9% | -| `FasterPath.extname` | `File.extname` | 74.0% | 73.1% | -| `FasterPath.has_trailing_separator?` | `Pathname#has_trailing_separator` | 88.5% | 89.1% | -| `FasterPath.join` | `Pathname#join` | 88.1% | 90.3% | -| `FasterPath.plus` | `Pathname#plus` | 91.7% | 93.4% | -| `FasterPath.relative?` | `Pathname#relative?` | 92.8% | 95.2% | -| `FasterPath.relative_path_from` | `Pathname#relative_path_from` | 92.5% | 93.6% | +|FasterPath Rust Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.3)|Time Shaved Off (Ruby 3.2)| +|---|---|:---:|:---:|:---:| +| `FasterPath.absolute?` | `Pathname#absolute?` | 55.5% | 53.2% | 43.1% | +| `FasterPath.add_trailing_separator` | `Pathname#add_trailing_separator` | 87.4% | 85.3% | 85.8% | +| `FasterPath.basename` | `File.basename` | 30.7% | 22.2% | 27.5% | +| `FasterPath.children` | `Pathname#children` | 65.0% | 55.1% | 55.4% | +| `FasterPath.chop_basename` | `Pathname#chop_basename` | 75.3% | 73.5% | 72.8% | +| `FasterPath.cleanpath_aggressive` | `Pathname#cleanpath_aggressive` | 92.4% | 92.2% | 92.4% | +| `FasterPath.cleanpath_conservative` | `Pathname#cleanpath_conservative` | 91.4% | 91.3% | 91.8% | +| `FasterPath.del_trailing_separator` | `Pathname#del_trailing_separator` | 89.1% | 87.9% | 87.8% | +| `FasterPath.directory?` | `Pathname#directory?` | 41.5% | 39.9% | 44.2% | +| `FasterPath.dirname` | `File.dirname` | 52.0% | 40.3% | 40.8% | +| `FasterPath.entries` | `Pathname#entries` | 57.8% | 42.4% | 45.9% | +| `FasterPath.extname` | `File.extname` | 69.4% | 66.7% | 69.2% | +| `FasterPath.has_trailing_separator?` | `Pathname#has_trailing_separator` | 93.4% | 92.8% | 91.7% | +| `FasterPath.join` | `Pathname#join` | 89.1% | 89.0% | 88.4% | +| `FasterPath.plus` | `Pathname#plus` | 95.5% | 94.7% | 94.5% | +| `FasterPath.relative?` | `Pathname#relative?` | 66.9% | 66.3% | 60.4% | +| `FasterPath.relative_path_from` | `Pathname#relative_path_from` | 94.4% | 94.6% | 94.2% | See [Benchmarks](#benchmarks) for how these are measured. @@ -205,10 +205,10 @@ FasterPath.sledgehammer_everything! These will **not** be included by default in monkey-patches. To try them with monkeypatching use the environment flag of `WITH_REGRESSION`. These methods are here to be improved upon. -|FasterPath Implementation|Ruby Implementation|Time Shaved Off (Ruby 2.7)|Time Shaved Off (Ruby 2.5)| -|---|---|:---:|:---:| -| `FasterPath.entries_compat` | `Pathname.entries` | 16.8% | 9.2% | -| `FasterPath.children_compat` | `Pathname.children` | 32.1% | 26.6% | +|FasterPath Implementation|Ruby Implementation|Time Shaved Off (Ruby 3.4)|Time Shaved Off (Ruby 3.3)|Time Shaved Off (Ruby 3.2)| +|---|---|:---:|:---:|:---:| +| `FasterPath.entries_compat` | `Pathname.entries` | 35.7% | 22.0% | 15.9% | +| `FasterPath.children_compat` | `Pathname.children` | 46.7% | 33.6% | 32.3% | It's been my observation (and some others) that the Rust implementation of the C code for `File` has similar results but performance seems to vary based on CPU cache on possibly 64bit/32bit system environments. These are not included by default when the monkey patch method `FasterPath.sledgehammer_everything!` is executed. @@ -216,13 +216,19 @@ performance seems to vary based on CPU cache on possibly 64bit/32bit system envi ## Benchmarks The "Time Shaved Off" figures come from the project's Pinch-bench (`rake pbench`, in -`test/pbench`): each method runs against Ruby's own implementation, and the result is how much -less time FasterPath took. They are the median of 3 runs of `LONG_RUN=10 rake pbench` (500,000 -calls of each method, 5 times) on Linux x86_64 with Ruby 2.7.8 and 2.5.7, built with Rust 1.97. -Expect a few points of difference from run to run. +`test/pbench`): each method runs against Ruby's own implementation, alternating with it round by +round, and the result is how much less time FasterPath's best round took than Ruby's best round. +They are the median of 3 runs of `LONG_RUN=30 rake pbench` (5 rounds of 300,000 calls of each +method) on Linux x86_64, each run pinned to one CPU core, with Ruby 3.4.6, 3.3.6 and 3.2.9, built +with Rust 1.97. The three runs agree within a few points for most methods; the ones that touch +the filesystem (`children`, `directory?`, `entries`) vary the most. + +The figures are smaller than they were on Ruby 2 for the cheapest methods, `absolute?` and +`relative?` above all: Ruby 3's own `Pathname#absolute?` is a single regexp match, where Ruby 2's +walked the path with `chop_basename`, so there is less left to shave off. `rake pbench` is also the performance regression test: each method has a floor in -`test/pbench/pbench_suite.rb` (about half of the improvement in the table above) and the task +`test/pbench/pbench_suite.rb` (about half of its lowest figure in the table above) and the task fails when a method measures below it, so a change that makes a method slower shows up in `rake test`. On Windows, where `join` is currently slower than Ruby's ([#185](https://github.com/danielpclark/faster_path/issues/185)), the floors are reported but not @@ -230,8 +236,10 @@ enforced. ### Before and after the Rutie 0.10 upgrade -Measured the same way, alternating runs of the code before the upgrade (built with Rutie 0.6) -and after it. Higher is better; the change is in percentage points. +The last figures on Ruby 2, from the Rutie 0.10 upgrade (faster_path 0.4.0): Ruby 2.7.8 and +2.5.7, alternating runs of the code before the upgrade (built with Rutie 0.6) and after it, with +the benchmark then measuring the average of 5 rounds run one implementation after the other. +Higher is better; the change is in percentage points. |Method|Ruby 2.7 before|Ruby 2.7 after|Change|Ruby 2.5 before|Ruby 2.5 after|Change| |---|:---:|:---:|:---:|:---:|:---:|:---:| diff --git a/Rakefile b/Rakefile index 1220540..67a07a4 100644 --- a/Rakefile +++ b/Rakefile @@ -92,10 +92,8 @@ Rake::TestTask.new(bench: :build_lib) do |t| t.pattern = 'test/**/*_benchmark.rb' end +# LONG_RUN=<1..30> multiplies the calls per round, see test/pbench/pbench.rb Rake::TestTask.new(pbench: :build_lib) do |t| - if ARGV.last == '--long-run' - ENV['LONG_RUN'] = '10' - end t.libs = %w[lib test test/pbench] t.pattern = 'test/pbench/pbench_suite.rb' end diff --git a/faster_path.gemspec b/faster_path.gemspec index 9616f74..6c681b0 100644 --- a/faster_path.gemspec +++ b/faster_path.gemspec @@ -24,17 +24,20 @@ Gem::Specification.new do |spec| spec.extensions = ['ext/Rakefile'] spec.require_paths = ['lib'] - # Rutie 0.10, which the Rust extension is built with, supports Ruby 2.5 to 2.7 - spec.required_ruby_version = ['>= 2.5', '< 3.0'] + # Rutie 0.13, which the Rust extension is built with, supports Ruby 3.2 to 3.4 + spec.required_ruby_version = ['>= 3.2', '< 3.5'] spec.add_dependency 'bundler', '>= 1.12' spec.add_dependency 'rake', '>= 12.3' spec.add_dependency 'thermite', '0.13.0' + # thermite reads GitHub release feeds with REXML, a bundled gem since Ruby 3.0 + spec.add_dependency 'rexml', '~> 3.2' spec.add_development_dependency 'read_source', '~> 0.2.6' spec.add_development_dependency 'minitest', '~> 5.11' spec.add_development_dependency 'minitest-reporters', '~> 1.1' spec.add_development_dependency 'color_pound_spec_reporter', '~> 0.0.9' - spec.add_development_dependency 'rubocop', '0.53' + spec.add_development_dependency 'rubocop', '~> 1.80' + spec.add_development_dependency 'rubocop-performance', '~> 1.20' spec.add_development_dependency 'stop_watch', '~> 1.0' if !ENV['CI'] && ENV['GRAPH'] spec.add_development_dependency 'gruff', '~> 0.7.0' diff --git a/lib/faster_path/optional/monkeypatches.rb b/lib/faster_path/optional/monkeypatches.rb index 70027af..9909bb6 100644 --- a/lib/faster_path/optional/monkeypatches.rb +++ b/lib/faster_path/optional/monkeypatches.rb @@ -24,10 +24,10 @@ def self.extname(pth) end # @private :nodoc: - def self.dirname(pth) + def self.dirname(pth, level = 1) pth = pth.to_path if pth.respond_to? :to_path raise TypeError unless pth.is_a? String - FasterPath.dirname(pth) + FasterPath.dirname(pth, level) end end end diff --git a/lib/faster_path/optional/refinements.rb b/lib/faster_path/optional/refinements.rb index f5149cd..c47911c 100644 --- a/lib/faster_path/optional/refinements.rb +++ b/lib/faster_path/optional/refinements.rb @@ -20,10 +20,10 @@ def self.extname(pth) end # @private :nodoc: - def self.dirname(pth) + def self.dirname(pth, level = 1) pth = pth.to_path if pth.respond_to? :to_path raise TypeError unless pth.is_a? String - FasterPath.dirname(pth) + FasterPath.dirname(pth, level) end end end diff --git a/lib/faster_path/version.rb b/lib/faster_path/version.rb index 54bd02d..7847e1a 100644 --- a/lib/faster_path/version.rb +++ b/lib/faster_path/version.rb @@ -1,3 +1,3 @@ module FasterPath - VERSION = "0.4.0" + VERSION = "0.5.0" end diff --git a/spec/mspec b/spec/mspec index 0091e8a..5470479 160000 --- a/spec/mspec +++ b/spec/mspec @@ -1 +1 @@ -Subproject commit 0091e8a62e954717cd54641f935eaf1403692041 +Subproject commit 54704795e21128a930af2021c72c49cb87065134 diff --git a/spec/ruby_spec b/spec/ruby_spec index c503335..97f0762 160000 --- a/spec/ruby_spec +++ b/spec/ruby_spec @@ -1 +1 @@ -Subproject commit c503335d3d9f6ec6ef24de60a0716c34af69b64f +Subproject commit 97f076242b7fc6e60703e6a6053365065cd6fc30 diff --git a/src/pathname.rs b/src/pathname.rs index 5a88f51..39950ed 100644 --- a/src/pathname.rs +++ b/src/pathname.rs @@ -3,7 +3,7 @@ use std::ffi::OsStr; use std::fs; use std::io; -use rutie::{AnyException, AnyObject, Array, Boolean, Class, NilClass, Object, RString}; +use rutie::{AnyException, AnyObject, Array, Boolean, Class, Integer, NilClass, Object, RString, Symbol}; use crate::basename; use crate::chop_basename::{self, is_relative}; @@ -15,7 +15,7 @@ use crate::path_parsing::Rules; use crate::plus; use crate::relative_path_from::{self, RelativePathError}; use crate::ruby::{ - argument_error, check_max_arguments, error, new_pathname, path_like_to_string, pathname_class, + argument_error, check_max_arguments, error, inspect, new_pathname, path_like_to_string, pathname_class, truthy_argument, EncodingId, EncodingOf, PathArgument, PathString, RubyResult, }; @@ -152,11 +152,51 @@ pub fn pn_is_directory(arguments: &[AnyObject]) -> RubyResult { Ok(Boolean::new(is_directory).into()) } +// `File.dirname(path, level = 1)`: `level` is the number of trailing +// components to remove (Ruby 3.1+). pub fn pn_dirname(arguments: &[AnyObject]) -> RubyResult { - check_max_arguments(arguments, 1)?; + check_max_arguments(arguments, 2)?; let argument = PathArgument::new(arguments, 0)?; let path = argument.path()?; - Ok(path.to_ruby(&dirname::dirname(RULES, path.bytes())).into()) + let level = match arguments.get(1) { + Some(level) => level_argument(level)?, + None => 1, + }; + let mut result: Cow<[u8]> = Cow::Borrowed(path.bytes()); + for _ in 0..level { + let parent = dirname::dirname(RULES, &result); + if *parent == *result { + // The root ("/", "." or "C:/"), which is its own dirname + break; + } + result = Cow::Owned(parent.into_owned()); + } + if result.is_empty() { + // `File.dirname("", 0)` is "." like `File.dirname("")` + result = Cow::Borrowed(b"."); + } + Ok(path.to_ruby(&result).into()) +} + +// The `level` argument of `File.dirname`, converted like Ruby's `NUM2INT`. +fn level_argument(object: &AnyObject) -> Result { + let integer = match object.try_convert_to::() { + Ok(integer) => integer, + Err(_) => { + let responds = object.protect_send("respond_to?", &[Symbol::new("to_int").into()])?; + if !responds.value().is_true() { + let class_name = object.protect_send("class", &[]).ok().and_then(|class| inspect(&class).ok()).unwrap_or_default(); + return Err(error(&Class::type_error(), &format!("no implicit conversion of {} into Integer", class_name))); + } + let converted = object.protect_send("to_int", &[])?; + converted.try_convert_to::()? + } + }; + let level = integer.to_i64(); + if level < 0 { + return Err(argument_error(&format!("negative level: {}", level))); + } + Ok(level as usize) } // Returns an array of `String`s @@ -315,7 +355,7 @@ fn system_call_error(err: &io::Error, path: &AnyObject, func: &str) -> AnyExcept #[cfg(unix)] { if let Some(errno) = err.raw_os_error() { - let arguments = [path.clone(), rutie::Integer::new(errno.into()).into(), RString::new_utf8(func).into()]; + let arguments = [path.clone(), Integer::new(errno.into()).into(), RString::new_utf8(func).into()]; if let Ok(exception) = Class::system_call_error().protect_send("new", &arguments) { if let Ok(exception) = exception.try_convert_to::() { return exception; diff --git a/test/dirname_test.rb b/test/dirname_test.rb index 4edcdf2..6cf69ab 100644 --- a/test/dirname_test.rb +++ b/test/dirname_test.rb @@ -11,6 +11,54 @@ def test_it_returns_all_the_components_of_filename_except_the_last_one assert_equal('/foo', FasterPath.dirname('/foo/foo')) end + def test_it_removes_level_trailing_components + assert_equal('/home', FasterPath.dirname('/home/jason/poot.txt', 2)) + assert_equal('/home/jason', FasterPath.dirname('/home/jason', 0)) + assert_equal('/', FasterPath.dirname('/home/jason', 2)) + assert_equal('/', FasterPath.dirname('/home/jason', 10)) + assert_equal('.', FasterPath.dirname('a/b', 2)) + assert_equal('.', FasterPath.dirname('a/b', 10)) + assert_equal('a', FasterPath.dirname('a//b//c//', 2)) + assert_equal(String, FasterPath.dirname('/home/jason', 0).class) + end + + # What File.dirname(path, level) returns on Ruby 3.3 for levels 0 to 4 + LEVELS = { + '/home/jason/poot.txt' => ['/home/jason/poot.txt', '/home/jason', '/home', '/', '/'], + 'a/b/c' => ['a/b/c', 'a/b', 'a', '.', '.'], + '/a' => ['/a', '/', '/', '/', '/'], + 'a' => ['a', '.', '.', '.', '.'], + '' => ['.', '.', '.', '.', '.'], + '/' => ['/', '/', '/', '/', '/'], + './b/./' => ['./b/./', './b', '.', '.', '.'], + '/foo/../.' => ['/foo/../.', '/foo/..', '/foo', '/', '/'], + 'a//b//c//' => ['a//b//c//', 'a//b', 'a', '.', '.'] + }.freeze + + def test_level_agrees_with_file_dirname + LEVELS.each do |path, expected| + expected.each_with_index do |dirname, level| + assert_equal(dirname, FasterPath.dirname(path, level), "#{path.inspect}, #{level}") + end + end + end + + def test_level_is_converted_with_to_int + level = Object.new + def level.to_int + 2 + end + assert_equal('/home', FasterPath.dirname('/home/jason/poot.txt', level)) + end + + def test_level_must_be_a_non_negative_integer + error = assert_raises(ArgumentError) { FasterPath.dirname('/home/jason', -1) } + assert_equal('negative level: -1', error.message) + error = assert_raises(TypeError) { FasterPath.dirname('/home/jason', '1') } + assert_equal('no implicit conversion of String into Integer', error.message) + assert_raises(ArgumentError) { FasterPath.dirname('/home/jason', 1, 2) } + end + def test_it_returns_a_string assert_kind_of(String, FasterPath.dirname("foo")) end diff --git a/test/pbench/pbench.rb b/test/pbench/pbench.rb index e6f4784..29ff011 100644 --- a/test/pbench/pbench.rb +++ b/test/pbench/pbench.rb @@ -22,31 +22,29 @@ def self.bench_range [10_000 * amplitude] * 5 end + # The improvement of `new_impl` over `baseline`, in percent. + # + # The two run alternately, round by round, so that whatever else the + # machine is doing affects both alike, and each is measured by its best + # round: the slower rounds only show the interruptions, not the code. def performance(baseline, new_impl) range = self.class.bench_range times_a = [] - range.each do |x| - GC.start - t0 = Minitest.clock_time - baseline.call(x) - t = Minitest.clock_time - t0 - times_a << t - end - - # This seems to stabalize the results a bit - sleep 0.02; GC.start - times_b = [] range.each do |x| - GC.start - t0 = Minitest.clock_time - new_impl.call(x) - t = Minitest.clock_time - t0 - times_b << t + times_a << time(baseline, x) + times_b << time(new_impl, x) end - increase(average(times_a), average(times_b)).round(1) + increase(times_a.min, times_b.min).round(1) + end + + def time(impl, x) + GC.start + t0 = Minitest.clock_time + impl.call(x) + Minitest.clock_time - t0 end # run(hash) diff --git a/test/pbench/pbench_suite.rb b/test/pbench/pbench_suite.rb index 2d0c813..432827d 100644 --- a/test/pbench/pbench_suite.rb +++ b/test/pbench/pbench_suite.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'pbench' # PBENCHES[:""] = { @@ -45,7 +47,7 @@ end } PBENCHES[:"absolute?"] = { - min: 80, + min: 20, new: lambda do |x| x.times do FasterPath.absolute?("/hello") @@ -60,7 +62,7 @@ end, } PBENCHES[:add_trailing_separator] = { - min: 50, + min: 40, new: lambda do |x| x.times do FasterPath.add_trailing_separator('/hello/world') @@ -75,7 +77,7 @@ end } PBENCHES[:basename] = { - min: 15, + min: 10, new: lambda do |x| x.times do FasterPath.basename("/hello/world") @@ -123,7 +125,7 @@ end } PBENCHES[:chop_basename] = { - min: 50, + min: 35, new: lambda do |x| x.times do FasterPath.chop_basename "/hello/world.txt" @@ -142,7 +144,7 @@ PATHNAME_CA1 = Pathname.new('/../.././../a').freeze PATHNAME_CA2 = Pathname.new('a/b/../../../../c/../d').freeze PBENCHES[:cleanpath_aggressive] = { - min: 80, + min: 45, new: lambda do |x| x.times do Pathname.new(FasterPath.cleanpath_aggressive '/../.././../a') @@ -157,7 +159,7 @@ end } PBENCHES[:cleanpath_conservative] = { - min: 80, + min: 45, new: lambda do |x| x.times do Pathname.new(FasterPath.cleanpath_conservative '/../.././../a') @@ -172,7 +174,7 @@ end } PBENCHES[:del_trailing_separator] = { - min: 70, + min: 40, new: lambda do |x| x.times do FasterPath.del_trailing_separator('/hello/world') @@ -202,7 +204,7 @@ end } PBENCHES[:dirname] = { - min: 30, + min: 20, new: lambda do |x| x.times do FasterPath.dirname "/really/long/path/name/which/ruby/doesnt/like/bar.txt" @@ -248,7 +250,7 @@ end } PBENCHES[:extname] = { - min: 50, + min: 30, new: lambda do |x| x.times do FasterPath.extname('verylongfilename_verylongfilename.rb') @@ -265,7 +267,7 @@ end } PBENCHES[:"has_trailing_separator?"] = { - min: 75, + min: 45, new: lambda do |x| x.times do FasterPath.has_trailing_separator? '////a//aaa/a//a/aaa////' @@ -280,7 +282,7 @@ end } PBENCHES[:join] = { - min: 70, + min: 40, new: lambda do |x| x.times do FasterPath.join('a', 'b') @@ -297,7 +299,7 @@ end } PBENCHES[:plus] = { - min: 80, + min: 45, new: lambda do |x| x.times do FasterPath.plus('a', 'b') @@ -314,7 +316,7 @@ end } PBENCHES[:"relative?"] = { - min: 80, + min: 30, new: lambda do |x| x.times do FasterPath.relative?("/hello") @@ -331,7 +333,7 @@ PATHNAME_AB = Pathname("/a/b") PATHNAME_ABCD = Pathname("/a/b/c/d") PBENCHES[:relative_path_from] = { - min: 80, + min: 45, new: lambda do |x| x.times do FasterPath.relative_path_from "/a/b/c/d", "/a/b" diff --git a/test/refinements/dirname_test.rb b/test/refinements/dirname_test.rb index 712592a..a590714 100644 --- a/test/refinements/dirname_test.rb +++ b/test/refinements/dirname_test.rb @@ -4,8 +4,8 @@ class RefinedFile using FasterPath::RefineFile - def dirname(name) - File.dirname(name) + def dirname(name, *level) + File.dirname(name, *level) end end @@ -45,4 +45,11 @@ def test_it_gets_dirnames_correctly assert_equal("/foo/..", @refined_file.dirname("/foo/../.")) assert_equal("foo", @refined_file.dirname("foo/../")) end + + def test_it_takes_a_level + assert_equal('/home', @refined_file.dirname('/home/jason/poot.txt', 2)) + assert_equal('/home/jason', @refined_file.dirname('/home/jason', 0)) + assert_equal('/', @refined_file.dirname('/home/jason', 10)) + assert_raises(ArgumentError) { @refined_file.dirname('/home/jason', -1) } + end end