diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1492d213..ebea87ba 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,6 +55,14 @@ jobs: gemfile: gems/sqlite3-v2.gemfile - ruby: 3.4 gemfile: gems/sqlite3-v2.gemfile + - ruby: 4.0 + gemfile: gems/sqlite3-v2.gemfile + - ruby: 4.0 + prepend: true + gemfile: gems/sqlite3-v2.gemfile + - ruby: 4.0 + gemfile: gems/instruments-ruby4.gemfile + test_features: "instruments" env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} SCOUT_TEST_FEATURES: ${{ matrix.test_features }} diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown index df0c3f02..c426104d 100644 --- a/CHANGELOG.markdown +++ b/CHANGELOG.markdown @@ -1,3 +1,7 @@ +# Pending + +- Ruby 4 support + # 6.0.2 - Fix `endpoint_sample_rate` and `job_sample_rate` to support float values diff --git a/gems/instruments-ruby4.gemfile b/gems/instruments-ruby4.gemfile new file mode 100644 index 00000000..9ce6fb0b --- /dev/null +++ b/gems/instruments-ruby4.gemfile @@ -0,0 +1,12 @@ +eval_gemfile("../Gemfile") + +gem 'minitest-mock' +gem 'ostruct' +gem "sqlite3", ">= 2.1" +gem 'httpclient' +gem 'http' +gem 'redis' +gem 'moped' +gem 'actionpack' +gem 'actionview' +gem 'httpx' diff --git a/gems/sqlite3-v2.gemfile b/gems/sqlite3-v2.gemfile index 4f5b704c..c6b93477 100644 --- a/gems/sqlite3-v2.gemfile +++ b/gems/sqlite3-v2.gemfile @@ -2,3 +2,4 @@ eval_gemfile("../Gemfile") gem "sqlite3", ">= 2.1" gem 'minitest-mock' +gem 'ostruct' diff --git a/lib/scout_apm/environment.rb b/lib/scout_apm/environment.rb index 52702a37..3d3d54ad 100644 --- a/lib/scout_apm/environment.rb +++ b/lib/scout_apm/environment.rb @@ -190,6 +190,16 @@ def ruby_3? @ruby_3 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^3/) end + def ruby_4? + return @ruby_4 if defined?(@ruby_4) + @ruby_4 = defined?(RUBY_VERSION) && RUBY_VERSION.match(/^4/) + end + + def ruby_2_or_above? + ruby_2? || ruby_3? || ruby_4? + end + + def ruby_minor return @ruby_minor if defined?(@ruby_minor) @ruby_minor = defined?(RUBY_VERSION) && RUBY_VERSION.split(".")[1].to_i @@ -197,12 +207,12 @@ def ruby_minor # Returns true if this Ruby version supports Module#prepend. def supports_module_prepend? - ruby_2? || ruby_3? + ruby_2_or_above? end # Returns true if this Ruby version makes positional and keyword arguments incompatible def supports_kwarg_delegation? - ruby_3? || (ruby_2? && ruby_minor >= 7) + ruby_4? || ruby_3? || (ruby_2? && ruby_minor >= 7) end # Returns a string representation of the OS (ex: darwin, linux) diff --git a/lib/scout_apm/layer.rb b/lib/scout_apm/layer.rb index f780d8a8..467f4e42 100644 --- a/lib/scout_apm/layer.rb +++ b/lib/scout_apm/layer.rb @@ -116,7 +116,7 @@ def capture_backtrace! # In Ruby 2.0+, we can pass the range directly to the caller to reduce the memory footprint. def caller_array # omits the first several callers which are in the ScoutAPM stack. - if ScoutApm::Agent.instance.context.environment.ruby_2? || ScoutApm::Agent.instance.context.environment.ruby_3? + if ScoutApm::Agent.instance.context.environment.ruby_2_or_above? caller(3...BACKTRACE_CALLER_LIMIT) else caller[3...BACKTRACE_CALLER_LIMIT] diff --git a/scout_apm.gemspec b/scout_apm.gemspec index bfd5249f..481f73e4 100644 --- a/scout_apm.gemspec +++ b/scout_apm.gemspec @@ -21,6 +21,7 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.1' + s.add_development_dependency "minitest" s.add_development_dependency "mocha" s.add_development_dependency "pry" @@ -34,10 +35,11 @@ Gem::Specification.new do |s| # tests. Specific versions are pulled in using specific gemfiles, e.g. # `gems/rails3.gemfile`. s.add_development_dependency "activerecord" - s.add_development_dependency "sqlite3", "~> 1.4" + s.add_development_dependency "sqlite3" s.add_development_dependency "rubocop" s.add_development_dependency "guard" s.add_development_dependency "guard-minitest" s.add_development_dependency "m" + end diff --git a/test/test_helper.rb b/test/test_helper.rb index 9261dba6..b54c4313 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -81,7 +81,10 @@ def remove_rails_namespace def fake_rails(version) remove_rails_namespace if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments") - Kernel.const_set("Rails", Module.new) + Kernel.const_set("Rails", Module.new { + # ActionView 8.1+ StructuredEventSubscriber calls Rails.try(:root) + def self.root; nil; end + }) Kernel.const_set("ActionController", Module.new) r = Kernel.const_get("Rails") r.const_set("VERSION", Module.new) diff --git a/test/unit/instruments/action_view_test.rb b/test/unit/instruments/action_view_test.rb index 2a9b3795..2b6006b9 100644 --- a/test/unit/instruments/action_view_test.rb +++ b/test/unit/instruments/action_view_test.rb @@ -4,9 +4,22 @@ if (ENV["SCOUT_TEST_FEATURES"] || "").include?("instruments") require 'test_helper' - require 'action_view' + + # Rails 8.1+ ActionView::StructuredEventSubscriber calls Rails.try(:root) + # https://github.com/rails/rails/blob/3ad79fcede4f9b620f03b9fd76507d9fb3c07e95/actionview/lib/action_view/structured_event_subscriber.rb#L67 + # which raises NameError if Rails is not defined. Define a minimal stub. + # Maybe this can get fixed at some point. + unless defined?(Rails) + module Rails + def self.root + nil + end + end + end + require 'action_pack' require 'action_controller' + require 'action_view' FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) @@ -62,6 +75,9 @@ class RenderTest < ActionController::TestCase def setup super + # Ensure Rails exists - other tests may have called clean_fake_rails + fake_rails(8) + @controller.logger = ActiveSupport::Logger.new(nil) ActionView::Base.logger = ActiveSupport::Logger.new(nil) @@ -75,6 +91,7 @@ def teardown ActionView::Base.logger = nil ActionController::Base.view_paths = @old_view_paths + clean_fake_rails end def test_partial_instrumentation