From a5e5d152e4095f2583c8184d79f456c3a893b4c1 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 26 Aug 2025 07:20:28 +0000 Subject: [PATCH 1/3] [rails] update psych for rails 7.1+ --- sentry-rails/Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-rails/Gemfile b/sentry-rails/Gemfile index 0bdcf787f..6582c1e09 100644 --- a/sentry-rails/Gemfile +++ b/sentry-rails/Gemfile @@ -36,6 +36,7 @@ if rails_version >= Gem::Version.new("8.0.0") gem "rspec-rails" gem "sqlite3", "~> 2.1.1", platform: :ruby elsif rails_version >= Gem::Version.new("7.1.0") + gem "psych", "~> 4.0.0" gem "rspec-rails" gem "sqlite3", "~> 1.7.3", platform: :ruby elsif rails_version >= Gem::Version.new("6.1.0") From 771814b22de2d382e480823ab535362a19b5d826 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Tue, 26 Aug 2025 07:24:26 +0000 Subject: [PATCH 2/3] [rails] update dummy app's constant --- sentry-rails/spec/dummy/test_rails_app/app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-rails/spec/dummy/test_rails_app/app.rb b/sentry-rails/spec/dummy/test_rails_app/app.rb index 2a16ff695..874b6d1ae 100644 --- a/sentry-rails/spec/dummy/test_rails_app/app.rb +++ b/sentry-rails/spec/dummy/test_rails_app/app.rb @@ -21,7 +21,7 @@ class TestApp < Rails::Application v6_0 = Gem::Version.new("6.0") v6_1 = Gem::Version.new("6.1") v7_0 = Gem::Version.new("7.0") -v7_1 = Gem::Version.new("7.1.alpha") +v7_1 = Gem::Version.new("7.1") FILE_NAME = case Gem::Version.new(Rails.version) From cc4f53cc129e00d8212a6fdebb33cd87f83e5ae1 Mon Sep 17 00:00:00 2001 From: Peter Solnica Date: Fri, 22 Aug 2025 13:33:31 +0000 Subject: [PATCH 3/3] [rails] add bin/test --- .gitignore | 1 + sentry-rails/Gemfile | 1 - sentry-rails/bin/test | 389 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 390 insertions(+), 1 deletion(-) create mode 100755 sentry-rails/bin/test diff --git a/.gitignore b/.gitignore index d08d6dcb0..83704eaa9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ node_modules .devcontainer/.env vendor/gems +sentry-rails/Gemfile-*.lock diff --git a/sentry-rails/Gemfile b/sentry-rails/Gemfile index 6582c1e09..8e94076b7 100644 --- a/sentry-rails/Gemfile +++ b/sentry-rails/Gemfile @@ -50,7 +50,6 @@ elsif rails_version >= Gem::Version.new("6.1.0") else gem "psych", "~> 3.0.0" gem "rspec-rails", "~> 4.0" - gem "psych", "~> 3.0.0" if rails_version >= Gem::Version.new("6.0.0") gem "sqlite3", "~> 1.4.0", platform: :ruby diff --git a/sentry-rails/bin/test b/sentry-rails/bin/test new file mode 100755 index 000000000..0e7cafce2 --- /dev/null +++ b/sentry-rails/bin/test @@ -0,0 +1,389 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +# Standalone CLI script to test sentry-rails against multiple Rails versions +# +# FEATURES: +# - Dedicated lock files for each Ruby/Rails version combination +# - Prevents dependency conflicts between different Rails versions +# - Automatic lock file management and restoration +# - Clean up functionality for old lock files +# +# LOCK FILE STRATEGY: +# Each Ruby/Rails combination gets its own lock file: +# - Ruby 3.4.5 + Rails 6.1 → Gemfile-ruby-3.4.5-rails-6.1.lock +# - Ruby 3.4.5 + Rails 7.0 → Gemfile-ruby-3.4.5-rails-7.0.lock +# +# Usage: +# ./bin/test --version 5.0 +# ./bin/test --all +# ./bin/test --help + +require 'optparse' +require 'fileutils' + +class RailsVersionTester + SUPPORTED_VERSIONS = %w[5.0 5.1 5.2 6.0 6.1 7.0 7.1 7.2 8.0].freeze + + def initialize + @options = {} + @failed_versions = [] + @ruby_version = RUBY_VERSION + @spec_paths = [] + end + + def run(args) + parse_options(args) + + case + when @options[:help] + show_help + when @options[:list] + list_versions + when @options[:clean] + clean_lock_files + when @options[:all] + test_all_versions + when @options[:version] + test_single_version(@options[:version]) + else + puts "Error: No action specified. Use --help for usage information." + exit(1) + end + end + + private + + def parse_options(args) + OptionParser.new do |opts| + opts.banner = "Usage: #{$0} [options] [spec_paths...]" + + opts.on("-v", "--version VERSION", "Test specific Rails version") do |version| + unless SUPPORTED_VERSIONS.include?(version) + puts "Error: Unsupported Rails version '#{version}'" + puts "Supported versions: #{SUPPORTED_VERSIONS.join(', ')}" + exit(1) + end + @options[:version] = version + end + + opts.on("-a", "--all", "Test all supported Rails versions") do + @options[:all] = true + end + + opts.on("-l", "--list", "List supported Rails versions and lock file status") do + @options[:list] = true + end + + opts.on("-c", "--clean", "Clean up old lock files for current Ruby version") do + @options[:clean] = true + end + + opts.on("-h", "--help", "Show this help message") do + @options[:help] = true + end + end.parse!(args) + + # Remaining arguments are spec paths + @spec_paths = args + end + + def show_help + puts <<~HELP + Rails Version Tester for sentry-rails + + This script tests sentry-rails against different Rails versions by: + 1. Setting the RAILS_VERSION environment variable + 2. Managing bundle dependencies with dedicated lock files per Ruby/Rails combination + 3. Running the test suite in isolated processes + 4. Providing proper exit codes for CI/CD integration + + Each Ruby/Rails version combination gets its own Gemfile.lock to prevent conflicts: + - Ruby #{@ruby_version} + Rails 6.1 → Gemfile-ruby-#{@ruby_version}-rails-6.1.lock + - Ruby #{@ruby_version} + Rails 7.0 → Gemfile-ruby-#{@ruby_version}-rails-7.0.lock + + Usage: + #{$0} --version 6.1 # Test specific Rails version (all specs) + #{$0} --version 7.0 spec/sentry/rails/log_subscribers # Test specific Rails version with specific specs + #{$0} --all # Test all supported versions + #{$0} --list # List supported versions and lock file status + #{$0} --clean # Clean up old lock files for current Ruby version + #{$0} --help # Show this help + + Supported Rails versions: #{SUPPORTED_VERSIONS.join(', ')} + + Examples: + #{$0} -v 7.1 # Test Rails 7.1 (all specs) + #{$0} -v 7.0 spec/sentry/rails/log_subscribers # Test Rails 7.0 log subscriber specs only + #{$0} -v 7.0 spec/sentry/rails/tracing # Test Rails 7.0 tracing specs only + #{$0} -a # Test all versions + #{$0} -c # Clean up old lock files + HELP + end + + def list_versions + puts "Supported Rails versions:" + SUPPORTED_VERSIONS.each do |version| + lock_file = generate_lock_file_name(version) + status = File.exist?(lock_file) ? "(has lock file)" : "(no lock file)" + puts " - #{version} #{status}" + end + puts + puts "Current Ruby version: #{@ruby_version}" + puts "Lock files are stored as: Gemfile-ruby-X.X.X-rails-Y.Y.lock" + end + + def test_all_versions + puts "Testing sentry-rails against all supported Rails versions: #{SUPPORTED_VERSIONS.join(', ')}" + puts + + SUPPORTED_VERSIONS.each do |version| + puts "=" * 60 + puts "Testing Rails #{version}" + puts "=" * 60 + + exit_code = test_rails_version(version) + + if exit_code == 0 + puts "✓ Rails #{version} - PASSED" + else + puts "✗ Rails #{version} - FAILED (exit code: #{exit_code})" + @failed_versions << version + end + puts + end + + print_summary + end + + def test_single_version(version) + puts "Testing sentry-rails against Rails #{version}..." + exit_code = test_rails_version(version) + exit(exit_code) unless exit_code == 0 + end + + def test_rails_version(version) + puts "Setting up environment for Rails #{version}..." + + # Generate dedicated lock file name for this Ruby/Rails combination + dedicated_lock_file = generate_lock_file_name(version) + current_lock_file = "Gemfile.lock" + + # Set up environment variables + env = { + "RAILS_VERSION" => version, + "BUNDLE_GEMFILE" => File.expand_path("Gemfile", Dir.pwd) + } + + puts "Using dedicated lock file: #{dedicated_lock_file}" + + # Manage lock file switching + setup_lock_file(dedicated_lock_file, current_lock_file) + + begin + # Check if bundle update is needed + if bundle_update_needed?(env, dedicated_lock_file) + puts "Dependencies need to be updated for Rails #{version}..." + unless update_bundle(env, dedicated_lock_file) + puts "✗ Failed to update bundle for Rails #{version}" + return 1 + end + end + + # Run the tests in a separate process + puts "Running test suite..." + run_tests(env, @spec_paths) + ensure + # Save the current lock file back to the dedicated location + save_lock_file(dedicated_lock_file, current_lock_file) + end + end + + def generate_lock_file_name(rails_version) + # Create a unique lock file name for this Ruby/Rails combination + ruby_version_clean = @ruby_version.gsub(/[^\d\.]/, '') + rails_version_clean = rails_version.gsub(/[^\d\.]/, '') + "Gemfile-ruby-#{ruby_version_clean}-rails-#{rails_version_clean}.lock" + end + + def setup_lock_file(dedicated_lock_file, current_lock_file) + # If we have a dedicated lock file, copy it to the current location + if File.exist?(dedicated_lock_file) + puts "Restoring lock file from #{dedicated_lock_file}" + FileUtils.cp(dedicated_lock_file, current_lock_file) + elsif File.exist?(current_lock_file) + # If no dedicated lock file exists but current one does, remove it + # so we get a fresh resolution + puts "Removing existing lock file for fresh dependency resolution" + File.delete(current_lock_file) + end + end + + def save_lock_file(dedicated_lock_file, current_lock_file) + # Save the current lock file to the dedicated location + if File.exist?(current_lock_file) + puts "Saving lock file to #{dedicated_lock_file}" + FileUtils.cp(current_lock_file, dedicated_lock_file) + end + end + + def bundle_update_needed?(env, dedicated_lock_file) + # Check if current Gemfile.lock exists + current_lock_file = "Gemfile.lock" + gemfile_path = env["BUNDLE_GEMFILE"] || "Gemfile" + + return true unless File.exist?(current_lock_file) + + # Check if Gemfile is newer than the current lock file + return true if File.mtime(gemfile_path) > File.mtime(current_lock_file) + + # For Rails version changes, check if lockfile has incompatible Rails version + if env["RAILS_VERSION"] && lockfile_has_incompatible_rails_version?(current_lock_file, env["RAILS_VERSION"]) + return true + end + + # Check if bundle check passes + system(env, "bundle check > /dev/null 2>&1") == false + end + + def lockfile_has_incompatible_rails_version?(lockfile_path, target_rails_version) + return false unless File.exist?(lockfile_path) + + lockfile_content = File.read(lockfile_path) + + # Extract Rails version from lockfile + if lockfile_content =~ /^\s*rails \(([^)]+)\)/ + locked_rails_version = $1 + target_major_minor = target_rails_version.split('.')[0..1].join('.') + locked_major_minor = locked_rails_version.split('.')[0..1].join('.') + + # If major.minor versions don't match, we need to update + return target_major_minor != locked_major_minor + end + + # If we can't determine the Rails version, assume update is needed + true + end + + def update_bundle(env, dedicated_lock_file) + puts "Updating bundle for Rails #{env['RAILS_VERSION']}..." + + current_lock_file = "Gemfile.lock" + + # Try bundle update first + if system(env, "bundle update --quiet") + puts "Bundle updated successfully" + return true + end + + puts "Bundle update failed, trying clean install..." + + # Remove the current lockfile and try fresh install + File.delete(current_lock_file) if File.exist?(current_lock_file) + + if system(env, "bundle install --quiet") + puts "Bundle installed successfully" + return true + end + + puts "Bundle install failed" + false + end + + def run_tests(env, spec_paths = []) + # Determine the command to run + if spec_paths.empty? + # Run all tests via rake + command = "bundle exec rake" + else + # Run specific specs via rspec + command = "bundle exec rspec #{spec_paths.join(' ')}" + end + + puts "Executing: #{command}" + + # Run the tests in a separate process with proper signal handling + pid = spawn(env, command, + out: $stdout, + err: $stderr, + pgroup: true) + + begin + _, status = Process.wait2(pid) + status.exitstatus + rescue Interrupt + puts "\nInterrupted! Terminating test process..." + terminate_process_group(pid) + 130 # Standard exit code for SIGINT + end + end + + def terminate_process_group(pid) + begin + Process.kill("TERM", -pid) # Kill the process group + sleep(2) + Process.kill("KILL", -pid) if process_running?(pid) + rescue Errno::ESRCH + # Process already terminated + end + end + + def process_running?(pid) + Process.getpgid(pid) + true + rescue Errno::ESRCH + false + end + + def clean_lock_files + puts "Cleaning up lock files for Ruby #{@ruby_version}..." + + # Find all lock files matching our pattern + pattern = "Gemfile-ruby-#{@ruby_version.gsub(/[^\d\.]/, '')}-rails-*.lock" + lock_files = Dir.glob(pattern) + + if lock_files.empty? + puts "No lock files found matching pattern: #{pattern}" + return + end + + puts "Found #{lock_files.length} lock file(s):" + lock_files.each { |file| puts " - #{file}" } + + print "Delete these files? [y/N]: " + response = $stdin.gets.chomp.downcase + + if response == 'y' || response == 'yes' + lock_files.each do |file| + File.delete(file) + puts "Deleted: #{file}" + end + puts "Cleanup complete!" + else + puts "Cleanup cancelled." + end + end + + def print_summary + puts "=" * 60 + puts "SUMMARY" + puts "=" * 60 + + if @failed_versions.empty? + puts "✓ All Rails versions passed!" + exit(0) + else + puts "✗ Failed versions: #{@failed_versions.join(', ')}" + puts + puts "Some Rails versions failed. See output above for details." + exit(1) + end + end +end + +# Run the script if called directly +if __FILE__ == $0 + tester = RailsVersionTester.new + tester.run(ARGV) +end