From de6c4b62b00023ef372bae4c434f58014c3c8382 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 12 Nov 2025 14:39:58 -0500 Subject: [PATCH 01/21] Use patched rbs aware of io/wait method removals --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index e7b5091bcc7e2e..3245e9efbc6ae7 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -18,7 +18,7 @@ net-pop 0.1.2 https://github.com/ruby/net-pop net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime -rbs 3.9.5 https://github.com/ruby/rbs +rbs 3.9.5 https://github.com/ruby/rbs 22451ecbe262326176eb3915b64366712930685c # waiting for https://github.com/ruby/rbs/pull/2706 typeprof 0.31.0 https://github.com/ruby/typeprof debug 1.11.0 https://github.com/ruby/debug racc 1.8.1 https://github.com/ruby/racc From 6ea4f36716f8970f418f32837575405ddeea75aa Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 11:53:04 -0800 Subject: [PATCH 02/21] [ruby/erb] Reapply "Remove safe_level and further positional arguments (https://github.com/ruby/erb/pull/7)" (https://github.com/ruby/erb/pull/95) This reverts commit https://github.com/ruby/erb/commit/1c02d23dc618. https://github.com/ruby/erb/commit/4162a24ecc --- lib/erb.rb | 15 +------- test/erb/test_erb.rb | 83 +++++++------------------------------------- 2 files changed, 13 insertions(+), 85 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index b1f4e9361b6384..1242866402f13c 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -853,20 +853,7 @@ def self.version # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines # [shorthand format]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags # - def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, trim_mode: nil, eoutvar: '_erbout') - # Complex initializer for $SAFE deprecation at [Feature #14256]. Use keyword arguments to pass trim_mode or eoutvar. - if safe_level != NOT_GIVEN - warn 'Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments.', uplevel: 1 - end - if legacy_trim_mode != NOT_GIVEN - warn 'Passing trim_mode with the 3rd argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, trim_mode: ...) instead.', uplevel: 1 - trim_mode = legacy_trim_mode - end - if legacy_eoutvar != NOT_GIVEN - warn 'Passing eoutvar with the 4th argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, eoutvar: ...) instead.', uplevel: 1 - eoutvar = legacy_eoutvar - end - + def initialize(str, trim_mode: nil, eoutvar: '_erbout') compiler = make_compiler(trim_mode) set_eoutvar(compiler, eoutvar) @src, @encoding, @frozen_string = *compiler.compile(str) diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index c0df690ccebe97..09496d31e25ca2 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -24,29 +24,6 @@ def test_with_filename assert_match(/\Atest filename:1\b/, e.backtrace[0]) end - # [deprecated] This will be removed later - def test_without_filename_with_safe_level - erb = EnvUtil.suppress_warning do - ERB.new("<% raise ::TestERB::MyError %>", 1) - end - e = assert_raise(MyError) { - erb.result - } - assert_match(/\A\(erb\):1\b/, e.backtrace[0]) - end - - # [deprecated] This will be removed later - def test_with_filename_and_safe_level - erb = EnvUtil.suppress_warning do - ERB.new("<% raise ::TestERB::MyError %>", 1) - end - erb.filename = "test filename" - e = assert_raise(MyError) { - erb.result - } - assert_match(/\Atest filename:1\b/, e.backtrace[0]) - end - def test_with_filename_lineno erb = ERB.new("<% raise ::TestERB::MyError %>") erb.filename = "test filename" @@ -117,25 +94,16 @@ def test_version end def test_core - # [deprecated] Fix initializer later - EnvUtil.suppress_warning do - _test_core(nil) - _test_core(0) - _test_core(1) - end - end - - def _test_core(safe) erb = @erb.new("hello") assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 0) + erb = @erb.new("hello", trim_mode: 0) assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 1) + erb = @erb.new("hello", trim_mode: 1) assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 2) + erb = @erb.new("hello", trim_mode: 2) assert_equal("hello", erb.result) src = <') + erb = @erb.new(src, trim_mode: '>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '<>') assert_equal(ans, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '%>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '%<>') assert_equal(ans, erb.result) end @@ -254,12 +222,6 @@ def test_explicit_trim_line_with_carriage_return assert_equal("line\r\n" * 3, erb.result) end - def test_safe_level_warning - assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do - @erb.new("", 1) - end - end - def test_invalid_trim_mode pend if RUBY_ENGINE == 'truffleruby' @@ -688,27 +650,6 @@ def test_half_working_comment_backward_compatibility end end - # [deprecated] These interfaces will be removed later - def test_deprecated_interface_warnings - [nil, 0, 1, 2].each do |safe| - assert_warn(/2nd argument of ERB.new is deprecated/) do - ERB.new('', safe) - end - end - - [nil, '', '%', '%<>'].each do |trim| - assert_warn(/3rd argument of ERB.new is deprecated/) do - ERB.new('', nil, trim) - end - end - - [nil, '_erbout', '_hamlout'].each do |eoutvar| - assert_warn(/4th argument of ERB.new is deprecated/) do - ERB.new('', nil, nil, eoutvar) - end - end - end - def test_prohibited_marshal_dump erb = ERB.new("") assert_raise(TypeError) {Marshal.dump(erb)} From 5b6658a406b5f1c535aed4cb68e8e18a3cbabb81 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 11:59:28 -0800 Subject: [PATCH 03/21] Revert "[ruby/erb] Reapply "Remove safe_level and further positional" This reverts commit 6ea4f36716f8970f418f32837575405ddeea75aa. I'll fix ruby/spec shortly. For now, let me just revert it for ruby/ruby. --- lib/erb.rb | 15 +++++++- test/erb/test_erb.rb | 83 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index 1242866402f13c..b1f4e9361b6384 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -853,7 +853,20 @@ def self.version # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines # [shorthand format]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags # - def initialize(str, trim_mode: nil, eoutvar: '_erbout') + def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, trim_mode: nil, eoutvar: '_erbout') + # Complex initializer for $SAFE deprecation at [Feature #14256]. Use keyword arguments to pass trim_mode or eoutvar. + if safe_level != NOT_GIVEN + warn 'Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments.', uplevel: 1 + end + if legacy_trim_mode != NOT_GIVEN + warn 'Passing trim_mode with the 3rd argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, trim_mode: ...) instead.', uplevel: 1 + trim_mode = legacy_trim_mode + end + if legacy_eoutvar != NOT_GIVEN + warn 'Passing eoutvar with the 4th argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, eoutvar: ...) instead.', uplevel: 1 + eoutvar = legacy_eoutvar + end + compiler = make_compiler(trim_mode) set_eoutvar(compiler, eoutvar) @src, @encoding, @frozen_string = *compiler.compile(str) diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index 09496d31e25ca2..c0df690ccebe97 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -24,6 +24,29 @@ def test_with_filename assert_match(/\Atest filename:1\b/, e.backtrace[0]) end + # [deprecated] This will be removed later + def test_without_filename_with_safe_level + erb = EnvUtil.suppress_warning do + ERB.new("<% raise ::TestERB::MyError %>", 1) + end + e = assert_raise(MyError) { + erb.result + } + assert_match(/\A\(erb\):1\b/, e.backtrace[0]) + end + + # [deprecated] This will be removed later + def test_with_filename_and_safe_level + erb = EnvUtil.suppress_warning do + ERB.new("<% raise ::TestERB::MyError %>", 1) + end + erb.filename = "test filename" + e = assert_raise(MyError) { + erb.result + } + assert_match(/\Atest filename:1\b/, e.backtrace[0]) + end + def test_with_filename_lineno erb = ERB.new("<% raise ::TestERB::MyError %>") erb.filename = "test filename" @@ -94,16 +117,25 @@ def test_version end def test_core + # [deprecated] Fix initializer later + EnvUtil.suppress_warning do + _test_core(nil) + _test_core(0) + _test_core(1) + end + end + + def _test_core(safe) erb = @erb.new("hello") assert_equal("hello", erb.result) - erb = @erb.new("hello", trim_mode: 0) + erb = @erb.new("hello", safe, 0) assert_equal("hello", erb.result) - erb = @erb.new("hello", trim_mode: 1) + erb = @erb.new("hello", safe, 1) assert_equal("hello", erb.result) - erb = @erb.new("hello", trim_mode: 2) + erb = @erb.new("hello", safe, 2) assert_equal("hello", erb.result) src = <') + erb = @erb.new(src, safe, '>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, safe, '<>') assert_equal(ans, erb.result) ans = <') + erb = @erb.new(src, safe, '%>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, safe, '%<>') assert_equal(ans, erb.result) end @@ -222,6 +254,12 @@ def test_explicit_trim_line_with_carriage_return assert_equal("line\r\n" * 3, erb.result) end + def test_safe_level_warning + assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do + @erb.new("", 1) + end + end + def test_invalid_trim_mode pend if RUBY_ENGINE == 'truffleruby' @@ -650,6 +688,27 @@ def test_half_working_comment_backward_compatibility end end + # [deprecated] These interfaces will be removed later + def test_deprecated_interface_warnings + [nil, 0, 1, 2].each do |safe| + assert_warn(/2nd argument of ERB.new is deprecated/) do + ERB.new('', safe) + end + end + + [nil, '', '%', '%<>'].each do |trim| + assert_warn(/3rd argument of ERB.new is deprecated/) do + ERB.new('', nil, trim) + end + end + + [nil, '_erbout', '_hamlout'].each do |eoutvar| + assert_warn(/4th argument of ERB.new is deprecated/) do + ERB.new('', nil, nil, eoutvar) + end + end + end + def test_prohibited_marshal_dump erb = ERB.new("") assert_raise(TypeError) {Marshal.dump(erb)} From 6365d7b0a585c1cd416a52058b0daa9a87d0c8d0 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 12:31:44 -0800 Subject: [PATCH 04/21] Reapply "[ruby/erb] Reapply "Remove safe_level and further positional" This reverts commit 5b6658a406b5f1c535aed4cb68e8e18a3cbabb81. With a ruby spec fix. --- lib/erb.rb | 15 +----- spec/ruby/library/erb/new_spec.rb | 24 +++++---- test/erb/test_erb.rb | 83 +++++-------------------------- 3 files changed, 26 insertions(+), 96 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index b1f4e9361b6384..1242866402f13c 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -853,20 +853,7 @@ def self.version # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines # [shorthand format]: rdoc-ref:ERB@Shorthand+Format+for+Execution+Tags # - def initialize(str, safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, trim_mode: nil, eoutvar: '_erbout') - # Complex initializer for $SAFE deprecation at [Feature #14256]. Use keyword arguments to pass trim_mode or eoutvar. - if safe_level != NOT_GIVEN - warn 'Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments.', uplevel: 1 - end - if legacy_trim_mode != NOT_GIVEN - warn 'Passing trim_mode with the 3rd argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, trim_mode: ...) instead.', uplevel: 1 - trim_mode = legacy_trim_mode - end - if legacy_eoutvar != NOT_GIVEN - warn 'Passing eoutvar with the 4th argument of ERB.new is deprecated. Use keyword argument like ERB.new(str, eoutvar: ...) instead.', uplevel: 1 - eoutvar = legacy_eoutvar - end - + def initialize(str, trim_mode: nil, eoutvar: '_erbout') compiler = make_compiler(trim_mode) set_eoutvar(compiler, eoutvar) @src, @encoding, @frozen_string = *compiler.compile(str) diff --git a/spec/ruby/library/erb/new_spec.rb b/spec/ruby/library/erb/new_spec.rb index ec1be5c2342f04..e9c914e9b6a75a 100644 --- a/spec/ruby/library/erb/new_spec.rb +++ b/spec/ruby/library/erb/new_spec.rb @@ -139,17 +139,19 @@ ->{ ERB.new("<%= list %>").result }.should raise_error(NameError) end - describe "warning about arguments" do - it "warns when passed safe_level and later arguments" do - -> { - ERB.new(@eruby_str, nil, '%') - }.should complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) - end - - it "does not warn when passed arguments as keyword argument" do - -> { - ERB.new(@eruby_str, trim_mode: '%') - }.should_not complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) + version_is ERB::VERSION, ""..."5.1.3" do # TODO: bump 5.1.3 to 6.0.0 once released + describe "warning about arguments" do + it "warns when passed safe_level and later arguments" do + -> { + ERB.new(@eruby_str, nil, '%') + }.should complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) + end + + it "does not warn when passed arguments as keyword argument" do + -> { + ERB.new(@eruby_str, trim_mode: '%') + }.should_not complain(/warning: Passing safe_level with the 2nd argument of ERB.new is deprecated. Do not use it, and specify other arguments as keyword arguments./) + end end end end diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index c0df690ccebe97..09496d31e25ca2 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -24,29 +24,6 @@ def test_with_filename assert_match(/\Atest filename:1\b/, e.backtrace[0]) end - # [deprecated] This will be removed later - def test_without_filename_with_safe_level - erb = EnvUtil.suppress_warning do - ERB.new("<% raise ::TestERB::MyError %>", 1) - end - e = assert_raise(MyError) { - erb.result - } - assert_match(/\A\(erb\):1\b/, e.backtrace[0]) - end - - # [deprecated] This will be removed later - def test_with_filename_and_safe_level - erb = EnvUtil.suppress_warning do - ERB.new("<% raise ::TestERB::MyError %>", 1) - end - erb.filename = "test filename" - e = assert_raise(MyError) { - erb.result - } - assert_match(/\Atest filename:1\b/, e.backtrace[0]) - end - def test_with_filename_lineno erb = ERB.new("<% raise ::TestERB::MyError %>") erb.filename = "test filename" @@ -117,25 +94,16 @@ def test_version end def test_core - # [deprecated] Fix initializer later - EnvUtil.suppress_warning do - _test_core(nil) - _test_core(0) - _test_core(1) - end - end - - def _test_core(safe) erb = @erb.new("hello") assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 0) + erb = @erb.new("hello", trim_mode: 0) assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 1) + erb = @erb.new("hello", trim_mode: 1) assert_equal("hello", erb.result) - erb = @erb.new("hello", safe, 2) + erb = @erb.new("hello", trim_mode: 2) assert_equal("hello", erb.result) src = <') + erb = @erb.new(src, trim_mode: '>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '<>') assert_equal(ans, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '%>') assert_equal(ans.chomp, erb.result) ans = <') + erb = @erb.new(src, trim_mode: '%<>') assert_equal(ans, erb.result) end @@ -254,12 +222,6 @@ def test_explicit_trim_line_with_carriage_return assert_equal("line\r\n" * 3, erb.result) end - def test_safe_level_warning - assert_warning(/#{__FILE__}:#{__LINE__ + 1}/) do - @erb.new("", 1) - end - end - def test_invalid_trim_mode pend if RUBY_ENGINE == 'truffleruby' @@ -688,27 +650,6 @@ def test_half_working_comment_backward_compatibility end end - # [deprecated] These interfaces will be removed later - def test_deprecated_interface_warnings - [nil, 0, 1, 2].each do |safe| - assert_warn(/2nd argument of ERB.new is deprecated/) do - ERB.new('', safe) - end - end - - [nil, '', '%', '%<>'].each do |trim| - assert_warn(/3rd argument of ERB.new is deprecated/) do - ERB.new('', nil, trim) - end - end - - [nil, '_erbout', '_hamlout'].each do |eoutvar| - assert_warn(/4th argument of ERB.new is deprecated/) do - ERB.new('', nil, nil, eoutvar) - end - end - end - def test_prohibited_marshal_dump erb = ERB.new("") assert_raise(TypeError) {Marshal.dump(erb)} From fb48b3020c351fab65e878af17dc941d0dd1e260 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 12:41:48 -0800 Subject: [PATCH 05/21] spec_guards.yml: Add `fail-fast: false` fail-fast is never a good idea for master branch. --- .github/workflows/spec_guards.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index bfbe8296845fd5..edbd268d0f0b19 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -42,6 +42,7 @@ jobs: - ruby-3.2 - ruby-3.3 - ruby-3.4 + fail-fast: false steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 From 7aba6ce2a6ee4dce9ed9578b71fe38999ed94bf1 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 12:44:14 -0800 Subject: [PATCH 06/21] erb/new_spec.rb: Fetch private ERB::VERSION for erb v4.0.4 or older --- spec/ruby/library/erb/new_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ruby/library/erb/new_spec.rb b/spec/ruby/library/erb/new_spec.rb index e9c914e9b6a75a..c8b8a311278620 100644 --- a/spec/ruby/library/erb/new_spec.rb +++ b/spec/ruby/library/erb/new_spec.rb @@ -139,7 +139,7 @@ ->{ ERB.new("<%= list %>").result }.should raise_error(NameError) end - version_is ERB::VERSION, ""..."5.1.3" do # TODO: bump 5.1.3 to 6.0.0 once released + version_is ERB.const_get(:VERSION, false), ""..."5.1.3" do # TODO: bump 5.1.3 to 6.0.0 once released describe "warning about arguments" do it "warns when passed safe_level and later arguments" do -> { From 07e78e31c6ebd2f6add2493aa072f45a57aec3b9 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 12:49:53 -0800 Subject: [PATCH 07/21] [ruby/erb] Drop an obsolete constant ERB::NOT_GIVEN and update some documentation https://github.com/ruby/erb/commit/9da628f21c --- lib/erb.rb | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index 1242866402f13c..10771dcf8ccf52 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -827,27 +827,6 @@ def self.version # # It's good practice to choose a variable name that begins with an underscore: `'_'`. # - # Backward Compatibility - # - # The calling sequence given above -- which is the one you should use -- - # is a simplified version of the complete formal calling sequence, - # which is: - # - # ``` - # ERB.new(template, - # safe_level=NOT_GIVEN, legacy_trim_mode=NOT_GIVEN, legacy_eoutvar=NOT_GIVEN, - # trim_mode: nil, eoutvar: '_erbout') - # ``` - # - # The second, third, and fourth positional arguments (those in the second line above) are deprecated; - # this method issues warnings if they are given. - # - # However, their values, if given, are handled thus: - # - # - `safe_level`: ignored. - # - `legacy_trim_mode`: overrides keyword argument `trim_mode`. - # - `legacy_eoutvar`: overrides keyword argument `eoutvar`. - # # [blank line control]: rdoc-ref:ERB@Suppressing+Unwanted+Blank+Lines # [combine trim modes]: rdoc-ref:ERB@Combining+Trim+Modes # [newline control]: rdoc-ref:ERB@Suppressing+Unwanted+Newlines @@ -862,12 +841,6 @@ def initialize(str, trim_mode: nil, eoutvar: '_erbout') @_init = self.class.singleton_class end - # :markup: markdown - # - # Placeholder constant; used as default value for certain method arguments. - NOT_GIVEN = defined?(Ractor) ? Ractor.make_shareable(Object.new) : Object.new - private_constant :NOT_GIVEN - # :markup: markdown # # :call-seq: @@ -881,7 +854,6 @@ def initialize(str, trim_mode: nil, eoutvar: '_erbout') # # => # # ``` # - def make_compiler(trim_mode) ERB::Compiler.new(trim_mode) end @@ -1198,7 +1170,6 @@ def def_module(methodname='erb') # # ``` # - # def def_class(superklass=Object, methodname='result') cls = Class.new(superklass) def_method(cls, methodname, @filename || '(ERB)') From fe1f8cd129cdfc23378e2c07f59e2a84bcbc7367 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 13:30:42 -0800 Subject: [PATCH 08/21] [ruby/erb] Drop a deprecated constant ERB::Revision https://github.com/ruby/erb/commit/1f83b2578f --- lib/erb.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index 10771dcf8ccf52..445d4795b018bb 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -778,9 +778,6 @@ # [template processor]: https://en.wikipedia.org/wiki/Template_processor # class ERB - Revision = '$Date:: $' # :nodoc: #' - deprecate_constant :Revision - # :markup: markdown # # :call-seq: From e25fdc3d0048cd2ab5c92add8fb76c17240e6ffc Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 13:47:27 -0800 Subject: [PATCH 09/21] [ruby/erb] Version 6.0.0 https://github.com/ruby/erb/commit/bbaaf1f51b --- lib/erb/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/erb/version.rb b/lib/erb/version.rb index 138bf3988206fc..3794e8288b1bb6 100644 --- a/lib/erb/version.rb +++ b/lib/erb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class ERB # The string \ERB version. - VERSION = '5.1.3' + VERSION = '6.0.0' end From 0533dba9113256b5ab36399bf755ed6fa6e9b9bd Mon Sep 17 00:00:00 2001 From: git Date: Wed, 12 Nov 2025 21:48:56 +0000 Subject: [PATCH 10/21] Update default gems list at e25fdc3d0048cd2ab5c92add8fb76c [ci skip] --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 96da441a627813..7722f3fe00c42a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -191,7 +191,7 @@ The following default gems are updated. * date 3.5.0 * digest 3.2.1 * english 0.8.1 -* erb 5.1.3 +* erb 6.0.0 * etc 1.4.6 * fcntl 1.3.0 * fileutils 1.8.0 From 936951b31238a873567be5fb049af9d383bc4c55 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 13:49:09 -0800 Subject: [PATCH 11/21] erb/new_spec.rb: Update a version guard to the released version --- spec/ruby/library/erb/new_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ruby/library/erb/new_spec.rb b/spec/ruby/library/erb/new_spec.rb index c8b8a311278620..1d1989b51a93ad 100644 --- a/spec/ruby/library/erb/new_spec.rb +++ b/spec/ruby/library/erb/new_spec.rb @@ -139,7 +139,7 @@ ->{ ERB.new("<%= list %>").result }.should raise_error(NameError) end - version_is ERB.const_get(:VERSION, false), ""..."5.1.3" do # TODO: bump 5.1.3 to 6.0.0 once released + version_is ERB.const_get(:VERSION, false), ""..."6.0.0" describe "warning about arguments" do it "warns when passed safe_level and later arguments" do -> { From c13b4d79e1573c6ab9c66eadaccf30df12ed03c8 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 12 Nov 2025 18:35:04 +0000 Subject: [PATCH 12/21] [DOC] Tweaks for String#to_s --- string.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/string.c b/string.c index 531b1c5594381b..c674e8327a85a8 100644 --- a/string.c +++ b/string.c @@ -7097,10 +7097,12 @@ rb_str_to_f(VALUE str) /* * call-seq: - * to_s -> self or string + * to_s -> self or new_string * * Returns +self+ if +self+ is a +String+, * or +self+ converted to a +String+ if +self+ is a subclass of +String+. + * + * Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. */ static VALUE From a4916a6f385b2c1323f0df45c5630f388c55bb4d Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 12 Nov 2025 18:43:45 +0000 Subject: [PATCH 13/21] [DOC] Tweaks for String#tr --- string.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/string.c b/string.c index c674e8327a85a8..c58573dc78d459 100644 --- a/string.c +++ b/string.c @@ -8641,17 +8641,15 @@ rb_str_tr_bang(VALUE str, VALUE src, VALUE repl) * * Arguments +selector+ and +replacements+ must be valid character selectors * (see {Character Selectors}[rdoc-ref:character_selectors.rdoc]), - * and may use any of its valid forms, including negation, ranges, and escaping: + * and may use any of its valid forms, including negation, ranges, and escapes: * - * # Negation. - * 'hello'.tr('^aeiou', '-') # => "-e--o" - * # Ranges. - * 'ibm'.tr('b-z', 'a-z') # => "hal" - * # Escapes. + * 'hello'.tr('^aeiou', '-') # => "-e--o" # Negation. + * 'ibm'.tr('b-z', 'a-z') # => "hal" # Range. * 'hel^lo'.tr('\^aeiou', '-') # => "h-l-l-" # Escaped leading caret. * 'i-b-m'.tr('b\-z', 'a-z') # => "ibabm" # Escaped embedded hyphen. * 'foo\\bar'.tr('ab\\', 'XYZ') # => "fooZYXr" # Escaped backslash. * + * Related: see {Converting to New String}[rdoc-ref:String@Converting+to+New+String]. */ static VALUE From cdc3faa6b3e02d9f40d9e1ff356bee9ad01ceb73 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Wed, 12 Nov 2025 14:02:01 -0800 Subject: [PATCH 14/21] erb/new_spec.rb: Fix a missing do --- spec/ruby/library/erb/new_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ruby/library/erb/new_spec.rb b/spec/ruby/library/erb/new_spec.rb index 1d1989b51a93ad..f8192bff9925cd 100644 --- a/spec/ruby/library/erb/new_spec.rb +++ b/spec/ruby/library/erb/new_spec.rb @@ -139,7 +139,7 @@ ->{ ERB.new("<%= list %>").result }.should raise_error(NameError) end - version_is ERB.const_get(:VERSION, false), ""..."6.0.0" + version_is ERB.const_get(:VERSION, false), ""..."6.0.0" do describe "warning about arguments" do it "warns when passed safe_level and later arguments" do -> { From 3efabc8355df489d1fa9717e7cb1b8daa8ddf92e Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 19:16:50 -0800 Subject: [PATCH 15/21] [ruby/rubygems] Add pattern matching support to Gem::Platform https://github.com/ruby/rubygems/commit/b59917447c --- lib/rubygems/platform.rb | 6 ++++++ test/rubygems/test_gem_platform.rb | 34 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index e30c266fab1c39..411512a465ef78 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -146,6 +146,12 @@ def to_s to_a.compact.join(@cpu.nil? ? "" : "-") end + alias_method :deconstruct, :to_a + + def deconstruct_keys(keys) + { cpu: @cpu, os: @os, version: @version } + end + ## # Is +other+ equal to this platform? Two platforms are equal if they have # the same CPU, OS and version. diff --git a/test/rubygems/test_gem_platform.rb b/test/rubygems/test_gem_platform.rb index a3ae919809d370..0f1a715ab81ac2 100644 --- a/test/rubygems/test_gem_platform.rb +++ b/test/rubygems/test_gem_platform.rb @@ -683,4 +683,38 @@ def assert_local_match(name) def refute_local_match(name) refute_match Gem::Platform.local, name end + + def test_deconstruct + platform = Gem::Platform.new("x86_64-linux") + assert_equal ["x86_64", "linux", nil], platform.deconstruct + end + + def test_deconstruct_keys + platform = Gem::Platform.new("x86_64-darwin-20") + assert_equal({ cpu: "x86_64", os: "darwin", version: "20" }, platform.deconstruct_keys(nil)) + end + + def test_pattern_matching_array + platform = Gem::Platform.new("arm64-darwin-21") + result = + case platform + in ["arm64", "darwin", version] + version + else + "no match" + end + assert_equal "21", result + end + + def test_pattern_matching_hash + platform = Gem::Platform.new("x86_64-linux") + result = + case platform + in cpu: "x86_64", os: "linux" + "matched" + else + "no match" + end + assert_equal "matched", result + end end From 2247b0becbb2ad0939645f460669f8351fc0e376 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Mon, 10 Nov 2025 21:58:41 -0800 Subject: [PATCH 16/21] [ruby/rubygems] Add documentation for pattern matching methods https://github.com/ruby/rubygems/commit/18f64c6b29 --- lib/rubygems/platform.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index 411512a465ef78..f0eae47c54281b 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -146,8 +146,33 @@ def to_s to_a.compact.join(@cpu.nil? ? "" : "-") end + ## + # Deconstructs the platform into an array for pattern matching. + # Returns [cpu, os, version]. + # + # Gem::Platform.new("x86_64-linux").deconstruct #=> ["x86_64", "linux", nil] + # + # This enables array pattern matching: + # + # case Gem::Platform.new("arm64-darwin-21") + # in ["arm64", "darwin", version] + # # version => "21" + # end alias_method :deconstruct, :to_a + ## + # Deconstructs the platform into a hash for pattern matching. + # Returns a hash with keys +:cpu+, +:os+, and +:version+. + # + # Gem::Platform.new("x86_64-darwin-20").deconstruct_keys(nil) + # #=> { cpu: "x86_64", os: "darwin", version: "20" } + # + # This enables hash pattern matching: + # + # case Gem::Platform.new("x86_64-linux") + # in cpu: "x86_64", os: "linux" + # # Matches Linux on x86_64 + # end def deconstruct_keys(keys) { cpu: @cpu, os: @os, version: @version } end From be86e7c50e6cc91b5b50f9a346f353546c555d5b Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 13 Nov 2025 07:26:57 +0900 Subject: [PATCH 17/21] [ruby/io-wait] Define ABI version https://github.com/ruby/io-wait/commit/ad6f47fd3a --- ext/io/wait/wait.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ext/io/wait/wait.c b/ext/io/wait/wait.c index f27d7c11e5db48..cf36346cf2f955 100644 --- a/ext/io/wait/wait.c +++ b/ext/io/wait/wait.c @@ -11,6 +11,8 @@ **********************************************************************/ +#include "ruby.h" /* abi_version */ + /* * IO wait methods are built in ruby now, just for backward compatibility. */ From 9f7ef20de165a20de951574c6f807a78872a2d22 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 13 Nov 2025 07:09:20 +0900 Subject: [PATCH 18/21] [ruby/rubygems] Re-use assert_headers_equal from Gem::Package::TarTestCase https://github.com/ruby/rubygems/commit/0cf49e22af --- test/rubygems/package/tar_test_case.rb | 38 +++++++------ .../test_gem_package_tar_header_ractor.rb | 53 +++---------------- 2 files changed, 27 insertions(+), 64 deletions(-) diff --git a/test/rubygems/package/tar_test_case.rb b/test/rubygems/package/tar_test_case.rb index e3d812bf3fd23d..26135cf296727d 100644 --- a/test/rubygems/package/tar_test_case.rb +++ b/test/rubygems/package/tar_test_case.rb @@ -6,23 +6,7 @@ ## # A test case for Gem::Package::Tar* classes -class Gem::Package::TarTestCase < Gem::TestCase - def ASCIIZ(str, length) - str + "\0" * (length - str.length) - end - - def SP(s) - s + " " - end - - def SP_Z(s) - s + " \0" - end - - def Z(s) - s + "\0" - end - +module Gem::Package::TarTestMethods def assert_headers_equal(expected, actual) expected = expected.to_s unless String === expected actual = actual.to_s unless String === actual @@ -66,6 +50,26 @@ def assert_headers_equal(expected, actual) assert_equal expected[chksum_off, 8], actual[chksum_off, 8] end +end + +class Gem::Package::TarTestCase < Gem::TestCase + include Gem::Package::TarTestMethods + + def ASCIIZ(str, length) + str + "\0" * (length - str.length) + end + + def SP(s) + s + " " + end + + def SP_Z(s) + s + " \0" + end + + def Z(s) + s + "\0" + end def calc_checksum(header) sum = header.sum(0) diff --git a/test/rubygems/test_gem_package_tar_header_ractor.rb b/test/rubygems/test_gem_package_tar_header_ractor.rb index bcd8ab6ad9a603..98fac2802c31d1 100644 --- a/test/rubygems/test_gem_package_tar_header_ractor.rb +++ b/test/rubygems/test_gem_package_tar_header_ractor.rb @@ -8,51 +8,6 @@ end class TestGemPackageTarHeaderRactor < Gem::Package::TarTestCase - ASSERT_HEADERS_EQUAL = <<~RUBY - def assert_headers_equal(expected, actual) - expected = expected.to_s unless String === expected - actual = actual.to_s unless String === actual - - fields = %w[ - name 100 - mode 8 - uid 8 - gid 8 - size 12 - mtime 12 - checksum 8 - typeflag 1 - linkname 100 - magic 6 - version 2 - uname 32 - gname 32 - devmajor 8 - devminor 8 - prefix 155 - ] - - offset = 0 - - until fields.empty? do - name = fields.shift - length = fields.shift.to_i - - if name == "checksum" - chksum_off = offset - offset += length - next - end - - assert_equal expected[offset, length], actual[offset, length] - - offset += length - end - - assert_equal expected[chksum_off, 8], actual[chksum_off, 8] - end - RUBY - SETUP = <<~RUBY header = { name: "x", @@ -78,7 +33,9 @@ class Ractor; alias value take unless method_defined?(:value); end RUBY def test_decode_in_ractor - assert_ractor(ASSERT_HEADERS_EQUAL + SETUP + <<~RUBY, require: "rubygems/package") + assert_ractor(SETUP + <<~RUBY, require: "rubygems/package", require_relative: "package/tar_test_case") + include Gem::Package::TarTestMethods + new_header = Ractor.new(tar_header.to_s) do |str| Gem::Package::TarHeader.from StringIO.new str end.value @@ -88,7 +45,9 @@ def test_decode_in_ractor end def test_encode_in_ractor - assert_ractor(ASSERT_HEADERS_EQUAL + SETUP + <<~RUBY, require: "rubygems/package") + assert_ractor(SETUP + <<~RUBY, require: "rubygems/package", require_relative: "package/tar_test_case") + include Gem::Package::TarTestMethods + header_bytes = tar_header.to_s new_header_bytes = Ractor.new(header_bytes) do |str| From 75f5a0bae9a288b71513d72ee1856bf67ad7b338 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 12 Nov 2025 07:37:37 -0500 Subject: [PATCH 19/21] Increase default stack sizes for LSAN --- vm_core.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm_core.h b/vm_core.h index 79d12f9ee44ad6..9f794d867841da 100644 --- a/vm_core.h +++ b/vm_core.h @@ -844,7 +844,7 @@ extern bool ruby_vm_during_cleanup; #define RUBY_VM_FIBER_MACHINE_STACK_SIZE_MIN ( 16 * 1024 * sizeof(VALUE)) /* 64 KB or 128 KB */ #endif -#if __has_feature(memory_sanitizer) || __has_feature(address_sanitizer) +#if __has_feature(memory_sanitizer) || __has_feature(address_sanitizer) || __has_feature(leak_sanitizer) /* It seems sanitizers consume A LOT of machine stacks */ #undef RUBY_VM_THREAD_MACHINE_STACK_SIZE #define RUBY_VM_THREAD_MACHINE_STACK_SIZE (1024 * 1024 * sizeof(VALUE)) From 8f9b038df30d1ba8310de642a5884d34fbf60e1a Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Wed, 12 Nov 2025 19:06:52 -0500 Subject: [PATCH 20/21] ZJIT: Add standalone JSON implementation (#15162) I split this off from https://github.com/ruby/ruby/pull/14999 to land the JSON component earlier. Iongraph's viewer is (as mentioned in the article above) a few notches above graphviz for viewing large CFGs. It also allows easily inspecting different compiler optimization passes and multiple functions in the same browser window. Since Spidermonkey is using this format, it may be beneficial to use it for our own JIT development. The requirement for JSON is downstream from that of the Iongraph format. As for writing the implementation myself, ZJIT leans towards having fewer dependencies, so this is the preferred approach. --- zjit/src/json.rs | 700 +++++++++++++++++++++++++++++++++++++++++++++++ zjit/src/lib.rs | 1 + 2 files changed, 701 insertions(+) create mode 100644 zjit/src/json.rs diff --git a/zjit/src/json.rs b/zjit/src/json.rs new file mode 100644 index 00000000000000..1e63ba7b425902 --- /dev/null +++ b/zjit/src/json.rs @@ -0,0 +1,700 @@ +//! Single file JSON serializer for iongraph output of ZJIT HIR. + +use std::{ + fmt, + io::{self, Write}, +}; + +pub trait Jsonable { + fn to_json(&self) -> Json; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Json { + Null, + Bool(bool), + Integer(isize), + UnsignedInteger(usize), + Floating(f64), + String(String), + Array(Vec), + Object(Vec<(String, Json)>), +} + +impl Json { + /// Convenience method for constructing a JSON array. + pub fn array(iter: I) -> Self + where + I: IntoIterator, + T: Into, + { + Json::Array(iter.into_iter().map(Into::into).collect()) + } + + pub fn empty_array() -> Self { + Json::Array(Vec::new()) + } + + pub fn object() -> JsonObjectBuilder { + JsonObjectBuilder::new() + } + + pub fn marshal(&self, writer: &mut W) -> JsonResult<()> { + match self { + Json::Null => writer.write_all(b"null"), + Json::Bool(b) => writer.write_all(if *b { b"true" } else { b"false" }), + Json::Integer(i) => write!(writer, "{}", i), + Json::UnsignedInteger(u) => write!(writer, "{}", u), + Json::Floating(f) => write!(writer, "{}", f), + Json::String(s) => return Self::write_str(writer, s), + Json::Array(jsons) => return Self::write_array(writer, jsons), + Json::Object(map) => return Self::write_object(writer, map), + }?; + Ok(()) + } + + pub fn write_str(writer: &mut W, s: &str) -> JsonResult<()> { + writer.write_all(b"\"")?; + + for ch in s.chars() { + match ch { + '"' => write!(writer, "\\\"")?, + '\\' => write!(writer, "\\\\")?, + // The following characters are control, but have a canonical representation. + // https://datatracker.ietf.org/doc/html/rfc8259#section-7 + '\n' => write!(writer, "\\n")?, + '\r' => write!(writer, "\\r")?, + '\t' => write!(writer, "\\t")?, + '\x08' => write!(writer, "\\b")?, + '\x0C' => write!(writer, "\\f")?, + ch if ch.is_control() => { + let code_point = ch as u32; + write!(writer, "\\u{:04X}", code_point)? + } + _ => write!(writer, "{}", ch)?, + }; + } + + writer.write_all(b"\"")?; + Ok(()) + } + + pub fn write_array(writer: &mut W, jsons: &[Json]) -> JsonResult<()> { + writer.write_all(b"[")?; + let mut prefix = ""; + for item in jsons { + write!(writer, "{}", prefix)?; + item.marshal(writer)?; + prefix = ", "; + } + writer.write_all(b"]")?; + Ok(()) + } + + pub fn write_object(writer: &mut W, pairs: &[(String, Json)]) -> JsonResult<()> { + writer.write_all(b"{")?; + let mut prefix = ""; + for (k, v) in pairs { + // Escape the keys, despite not being `Json::String` objects. + write!(writer, "{}", prefix)?; + Self::write_str(writer, k)?; + writer.write_all(b":")?; + v.marshal(writer)?; + prefix = ", "; + } + writer.write_all(b"}")?; + Ok(()) + } +} + +impl std::fmt::Display for Json { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut buf = Vec::new(); + self.marshal(&mut buf).map_err(|_| std::fmt::Error)?; + let s = String::from_utf8(buf).map_err(|_| std::fmt::Error)?; + write!(f, "{}", s) + } +} + +pub struct JsonObjectBuilder { + pairs: Vec<(String, Json)>, +} + +impl JsonObjectBuilder { + pub fn new() -> Self { + Self { pairs: Vec::new() } + } + + pub fn insert(mut self, key: K, value: V) -> Self + where + K: Into, + V: Into, + { + self.pairs.push((key.into(), value.into())); + self + } + + pub fn build(self) -> Json { + Json::Object(self.pairs) + } +} + +impl From<&str> for Json { + fn from(s: &str) -> Json { + Json::String(s.to_string()) + } +} + +impl From for Json { + fn from(s: String) -> Json { + Json::String(s) + } +} + +impl From for Json { + fn from(i: i32) -> Json { + Json::Integer(i as isize) + } +} + +impl From for Json { + fn from(i: i64) -> Json { + Json::Integer(i as isize) + } +} + +impl From for Json { + fn from(u: u32) -> Json { + Json::UnsignedInteger(u as usize) + } +} + +impl From for Json { + fn from(u: u64) -> Json { + Json::UnsignedInteger(u as usize) + } +} + +impl From for Json { + fn from(u: usize) -> Json { + Json::UnsignedInteger(u) + } +} + +impl From for Json { + fn from(b: bool) -> Json { + Json::Bool(b) + } +} + +impl TryFrom for Json { + type Error = JsonError; + fn try_from(f: f64) -> Result { + if f.is_finite() { + Ok(Json::Floating(f)) + } else { + Err(JsonError::FloatError(f)) + } + } +} + +impl> From> for Json { + fn from(v: Vec) -> Json { + Json::Array(v.into_iter().map(|item| item.into()).collect()) + } +} + +/// Convenience type for a result in JSON serialization. +pub type JsonResult = std::result::Result; + +#[derive(Debug)] +pub enum JsonError { + /// Wrapper for a standard `io::Error`. + IoError(io::Error), + /// On attempting to serialize an invalid `f32` or `f64`. + /// Stores invalid values as 64 bit float. + FloatError(f64), +} + +impl From for JsonError { + fn from(err: io::Error) -> Self { + JsonError::IoError(err) + } +} + +impl fmt::Display for JsonError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonError::FloatError(v) => write!(f, "Cannot serialize float {}", v), + JsonError::IoError(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for JsonError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + JsonError::IoError(e) => Some(e), + JsonError::FloatError(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + + fn marshal_to_string(json: &Json) -> String { + let mut buf = Vec::new(); + json.marshal(&mut buf).unwrap(); + String::from_utf8(buf).unwrap() + } + + #[test] + fn test_null() { + let json = Json::Null; + assert_snapshot!(marshal_to_string(&json), @"null"); + } + + #[test] + fn test_bool() { + let json: Json = true.into(); + assert_snapshot!(marshal_to_string(&json), @"true"); + let json: Json = false.into(); + assert_snapshot!(marshal_to_string(&json), @"false"); + } + + #[test] + fn test_integer_positive() { + let json: Json = 42.into(); + assert_snapshot!(marshal_to_string(&json), @"42"); + } + + #[test] + fn test_integer_negative() { + let json: Json = (-123).into(); + assert_snapshot!(marshal_to_string(&json), @"-123"); + } + + #[test] + fn test_integer_zero() { + let json: Json = 0.into(); + assert_snapshot!(marshal_to_string(&json), @"0"); + } + + #[test] + fn test_floating() { + let json = 2.14159.try_into(); + assert!(json.is_ok()); + let json = json.unwrap(); + assert_snapshot!(marshal_to_string(&json), @"2.14159"); + } + + #[test] + fn test_floating_negative() { + let json = (-2.5).try_into(); + assert!(json.is_ok()); + let json = json.unwrap(); + assert_snapshot!(marshal_to_string(&json), @"-2.5"); + } + + #[test] + fn test_floating_error() { + let json: Result = f64::NAN.try_into(); + assert!(matches!(json, Err(JsonError::FloatError(_)))); + + let json: Result = f64::INFINITY.try_into(); + assert!(matches!(json, Err(JsonError::FloatError(_)))); + + let json: Result = f64::NEG_INFINITY.try_into(); + assert!(matches!(json, Err(JsonError::FloatError(_)))); + } + + #[test] + fn test_string_simple() { + let json: Json = "hello".into(); + assert_snapshot!(marshal_to_string(&json), @r#""hello""#); + } + + #[test] + fn test_string_empty() { + let json: Json = "".into(); + assert_snapshot!(marshal_to_string(&json), @r#""""#); + } + + #[test] + fn test_string_with_quotes() { + let json: Json = r#"hello "world""#.into(); + assert_snapshot!(marshal_to_string(&json), @r#""hello \"world\"""#); + } + + #[test] + fn test_string_with_backslash() { + let json: Json = r"path\to\file".into(); + assert_snapshot!(marshal_to_string(&json), @r#""path\\to\\file""#); + } + + #[test] + fn test_string_with_slash() { + let json: Json = "path/to/file".into(); + assert_snapshot!(marshal_to_string(&json), @r#""path/to/file""#); + } + + #[test] + fn test_string_with_newline() { + let json: Json = "line1\nline2".into(); + assert_snapshot!(marshal_to_string(&json), @r#""line1\nline2""#); + } + + #[test] + fn test_string_with_carriage_return() { + let json: Json = "line1\rline2".into(); + assert_snapshot!(marshal_to_string(&json), @r#""line1\rline2""#); + } + + #[test] + fn test_string_with_tab() { + let json: Json = "col1\tcol2".into(); + assert_snapshot!(marshal_to_string(&json), @r#""col1\tcol2""#); + } + + #[test] + fn test_string_with_backspace() { + let json: Json = "text\x08back".into(); + assert_snapshot!(marshal_to_string(&json), @r#""text\bback""#); + } + + #[test] + fn test_string_with_form_feed() { + let json: Json = "page\x0Cnew".into(); + assert_snapshot!(marshal_to_string(&json), @r#""page\fnew""#); + } + + #[test] + fn test_string_with_control_chars() { + let json: Json = "test\x01\x02\x03".into(); + assert_snapshot!(marshal_to_string(&json), @r#""test\u0001\u0002\u0003""#); + } + + #[test] + fn test_string_with_all_escapes() { + let json: Json = "\"\\/\n\r\t\x08\x0C".into(); + assert_snapshot!(marshal_to_string(&json), @r#""\"\\/\n\r\t\b\f""#); + } + + #[test] + fn test_array_empty() { + let json: Json = Vec::::new().into(); + assert_snapshot!(marshal_to_string(&json), @"[]"); + } + + #[test] + fn test_array_single_element() { + let json: Json = vec![42].into(); + assert_snapshot!(marshal_to_string(&json), @"[42]"); + } + + #[test] + fn test_array_multiple_elements() { + let json: Json = vec![1, 2, 3].into(); + assert_snapshot!(marshal_to_string(&json), @"[1, 2, 3]"); + } + + #[test] + fn test_array_mixed_types() { + let json = Json::Array(vec![ + Json::Null, + true.into(), + 42.into(), + 3.134.try_into().unwrap(), + "hello".into(), + ]); + assert_snapshot!(marshal_to_string(&json), @r#"[null, true, 42, 3.134, "hello"]"#); + } + + #[test] + fn test_array_nested() { + let json = Json::Array(vec![1.into(), vec![2, 3].into(), 4.into()]); + assert_snapshot!(marshal_to_string(&json), @"[1, [2, 3], 4]"); + } + + #[test] + fn test_object_empty() { + let json = Json::Object(vec![]); + assert_snapshot!(marshal_to_string(&json), @"{}"); + } + + #[test] + fn test_object_single_field() { + let json = Json::Object(vec![("key".to_string(), "value".into())]); + assert_snapshot!(marshal_to_string(&json), @r#"{"key":"value"}"#); + } + + #[test] + fn test_object_multiple_fields() { + let json = Json::Object(vec![ + ("name".to_string(), "Alice".into()), + ("age".to_string(), 30.into()), + ("active".to_string(), true.into()), + ]); + assert_snapshot!(marshal_to_string(&json), @r#"{"name":"Alice", "age":30, "active":true}"#); + } + + #[test] + fn test_object_with_escaped_key() { + let json = Json::Object(vec![("key\nwith\nnewlines".to_string(), 42.into())]); + assert_snapshot!(marshal_to_string(&json), @r#"{"key\nwith\nnewlines":42}"#); + } + + #[test] + fn test_object_nested() { + let inner = Json::Object(vec![("inner_key".to_string(), "inner_value".into())]); + let json = Json::Object(vec![("outer_key".to_string(), inner)]); + assert_snapshot!(marshal_to_string(&json), @r#"{"outer_key":{"inner_key":"inner_value"}}"#); + } + + #[test] + fn test_from_str() { + let json: Json = "test string".into(); + assert_snapshot!(marshal_to_string(&json), @r#""test string""#); + } + + #[test] + fn test_from_i32() { + let json: Json = 42i32.into(); + assert_snapshot!(marshal_to_string(&json), @"42"); + } + + #[test] + fn test_from_i64() { + let json: Json = 9223372036854775807i64.into(); + assert_snapshot!(marshal_to_string(&json), @"9223372036854775807"); + } + + #[test] + fn test_from_u32() { + let json: Json = 42u32.into(); + assert_snapshot!(marshal_to_string(&json), @"42"); + } + + #[test] + fn test_from_u64() { + let json: Json = 18446744073709551615u64.into(); + assert_snapshot!(marshal_to_string(&json), @"18446744073709551615"); + } + + #[test] + fn test_unsigned_integer_zero() { + let json: Json = 0u64.into(); + assert_snapshot!(marshal_to_string(&json), @"0"); + } + + #[test] + fn test_from_bool() { + let json_true: Json = true.into(); + let json_false: Json = false.into(); + assert_snapshot!(marshal_to_string(&json_true), @"true"); + assert_snapshot!(marshal_to_string(&json_false), @"false"); + } + + #[test] + fn test_from_vec() { + let json: Json = vec![1i32, 2i32, 3i32].into(); + assert_snapshot!(marshal_to_string(&json), @"[1, 2, 3]"); + } + + #[test] + fn test_from_vec_strings() { + let json: Json = vec!["a", "b", "c"].into(); + assert_snapshot!(marshal_to_string(&json), @r#"["a", "b", "c"]"#); + } + + #[test] + fn test_complex_nested_structure() { + let settings = Json::Object(vec![ + ("notifications".to_string(), true.into()), + ("theme".to_string(), "dark".into()), + ]); + + let json = Json::Object(vec![ + ("id".to_string(), 1.into()), + ("name".to_string(), "Alice".into()), + ("tags".to_string(), vec!["admin", "user"].into()), + ("settings".to_string(), settings), + ]); + assert_snapshot!(marshal_to_string(&json), @r#"{"id":1, "name":"Alice", "tags":["admin", "user"], "settings":{"notifications":true, "theme":"dark"}}"#); + } + + #[test] + fn test_deeply_nested_arrays() { + let json = Json::Array(vec![ + Json::Array(vec![vec![1, 2].into(), 3.into()]), + 4.into(), + ]); + assert_snapshot!(marshal_to_string(&json), @"[[[1, 2], 3], 4]"); + } + + #[test] + fn test_unicode_string() { + let json: Json = "兵马俑".into(); + assert_snapshot!(marshal_to_string(&json), @r#""兵马俑""#); + } + + #[test] + fn test_json_array_convenience() { + let json = Json::array(vec![1, 2, 3]); + assert_snapshot!(marshal_to_string(&json), @"[1, 2, 3]"); + } + + #[test] + fn test_json_array_from_iterator() { + let json = Json::array([1, 2, 3].iter().map(|&x| x * 2)); + assert_snapshot!(marshal_to_string(&json), @"[2, 4, 6]"); + } + + #[test] + fn test_json_empty_array() { + let json = Json::empty_array(); + assert_snapshot!(marshal_to_string(&json), @"[]"); + } + + #[test] + fn test_object_builder_empty() { + let json = Json::object().build(); + assert_snapshot!(marshal_to_string(&json), @"{}"); + } + + #[test] + fn test_object_builder_single_field() { + let json = Json::object().insert("key", "value").build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"key":"value"}"#); + } + + #[test] + fn test_object_builder_multiple_fields() { + let json = Json::object() + .insert("name", "Alice") + .insert("age", 30) + .insert("active", true) + .build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"name":"Alice", "age":30, "active":true}"#); + } + + #[test] + fn test_object_builder_with_nested_objects() { + let inner = Json::object().insert("inner_key", "inner_value").build(); + let json = Json::object().insert("outer_key", inner).build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"outer_key":{"inner_key":"inner_value"}}"#); + } + + #[test] + fn test_object_builder_with_array() { + let json = Json::object().insert("items", vec![1, 2, 3]).build(); + assert_snapshot!(marshal_to_string(&json), @r#"{"items":[1, 2, 3]}"#); + } + + #[test] + fn test_display_trait() { + let json = Json::object() + .insert("name", "Bob") + .insert("count", 42) + .build(); + let display_output = format!("{}", json); + assert_snapshot!(display_output, @r#"{"name":"Bob", "count":42}"#); + } + + #[test] + fn test_display_trait_array() { + let json: Json = vec![1, 2, 3].into(); + let display_output = format!("{}", json); + assert_snapshot!(display_output, @"[1, 2, 3]"); + } + + #[test] + fn test_display_trait_string() { + let json: Json = "test".into(); + let display_output = format!("{}", json); + assert_snapshot!(display_output, @r#""test""#); + } + + #[test] + fn test_from_usize() { + let json: Json = 123usize.into(); + assert_snapshot!(marshal_to_string(&json), @"123"); + } + + #[test] + fn test_from_usize_large() { + let json: Json = usize::MAX.into(); + let expected = format!("{}", usize::MAX); + assert_eq!(marshal_to_string(&json), expected); + } + + #[test] + fn test_json_error_float_display() { + let err = JsonError::FloatError(f64::NAN); + let display_output = format!("{}", err); + assert!(display_output.contains("Cannot serialize float")); + assert!(display_output.contains("NaN")); + } + + #[test] + fn test_json_error_float_display_infinity() { + let err = JsonError::FloatError(f64::INFINITY); + let display_output = format!("{}", err); + assert_snapshot!(display_output, @"Cannot serialize float inf"); + } + + #[test] + fn test_json_error_io_display() { + let io_err = io::Error::new(io::ErrorKind::WriteZero, "write error"); + let err = JsonError::IoError(io_err); + let display_output = format!("{}", err); + assert_snapshot!(display_output, @"write error"); + } + + #[test] + fn test_io_error_during_marshal() { + struct FailingWriter; + impl Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result { + Err(io::Error::other("simulated write failure")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + let json: Json = "test".into(); + let mut writer = FailingWriter; + let result = json.marshal(&mut writer); + assert!(result.is_err()); + assert!(matches!(result, Err(JsonError::IoError(_)))); + } + + #[test] + fn test_clone_json() { + let json1: Json = vec![1, 2, 3].into(); + let json2 = json1.clone(); + assert_eq!(json1, json2); + } + + #[test] + fn test_debug_json() { + let json: Json = "test".into(); + let debug_output = format!("{:?}", json); + assert!(debug_output.contains("String")); + assert!(debug_output.contains("test")); + } + + #[test] + fn test_partial_eq_json() { + let json1: Json = 42.into(); + let json2: Json = 42.into(); + let json3: Json = 43.into(); + assert_eq!(json1, json2); + assert_ne!(json1, json3); + } +} diff --git a/zjit/src/lib.rs b/zjit/src/lib.rs index f8ff380148004e..88e7af02b75013 100644 --- a/zjit/src/lib.rs +++ b/zjit/src/lib.rs @@ -29,3 +29,4 @@ mod invariants; mod bitset; mod gc; mod payload; +mod json; From 37a05b591c989abe1ca524c3ff98b088d7e6a656 Mon Sep 17 00:00:00 2001 From: Kazuhiro NISHIYAMA Date: Thu, 13 Nov 2025 09:31:50 +0900 Subject: [PATCH 21/21] Ignore ~/.gitconfig I use `commit.gpgsign=true`, so I want to ignore it in tests. --- tool/test/test_commit_email.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tool/test/test_commit_email.rb b/tool/test/test_commit_email.rb index 988a26cc2eab0c..674275279efd08 100644 --- a/tool/test/test_commit_email.rb +++ b/tool/test/test_commit_email.rb @@ -13,7 +13,11 @@ def setup git('init', '--initial-branch=master') git('config', 'user.name', 'Jóhän Grübél') git('config', 'user.email', 'johan@example.com') - env = { 'GIT_AUTHOR_DATE' => '2025-10-08T12:00:00Z', 'TZ' => 'UTC' } + env = { + 'GIT_AUTHOR_DATE' => '2025-10-08T12:00:00Z', + 'GIT_CONFIG_GLOBAL' => IO::NULL, + 'TZ' => 'UTC', + } git('commit', '--allow-empty', '-m', 'New repository initialized by cvs2svn.', env:) git('commit', '--allow-empty', '-m', 'Initial revision', env:) git('commit', '--allow-empty', '-m', 'version 1.0.0', env:)