Last active
August 17, 2022 06:36
-
-
Save andynu/69b052ed2f7680ba728b84fc34936578 to your computer and use it in GitHub Desktop.
Find the last version that passes the tests by doing a binary search through a range of versions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# | |
# Find the last version that passes the tests by doing | |
# a binary search through a range of versions | |
# | |
# Usage: gem-bisect <gem_name> [<from_version> [<to_version>]] | |
# | |
# Example | |
# ❯ gem-bisect rack-attack 6.3 6.6.1 | |
# Checking versions 6.6.1, 6.5.0, 6.4.0, 6.3.1 | |
# -------------------------------------------------------------------------------- | |
# 2022-08-15 13:09:20 -0400 | |
# Trying rack-attack 6.4.0 | |
# ... omit test output ... | |
# -------------------------------------------------------------------------------- | |
# 2022-08-15 13:10:22 -0400 | |
# Trying rack-attack 6.5.0 | |
# ... omit test output ... | |
# ... etc ... | |
# ================================================================================ | |
# last good: 6.6.1 | |
# first bad: Hurray! No failing versions. | |
# | |
# Or if there was a failure, you'd see that version string. | |
# | |
require 'rubygems' | |
gem_name, from, to = ARGV | |
gem_name ||= 'rails' | |
# @return {Array} of version {String}s | |
def load_version_list(gem) | |
results = `gem search #{gem} --all --exact` | |
results.lines.last.gsub("#{gem} (", '').gsub(')', '').split(',').map(&:strip) | |
end | |
def to_version_objects(gem_version_strings) | |
gem_version_strings.map do |version_string| | |
Gem::Version.new(version_string) | |
end | |
end | |
def filter_major_minor(versions) | |
max_per_major_minor = {} | |
versions.each do |version| | |
major, minor, = version.canonical_segments | |
last_version = max_per_major_minor[[major, minor]] | |
max_per_major_minor[[major, minor]] = version if last_version.nil? || version > last_version | |
end | |
max_per_major_minor.values | |
end | |
def filter_range(versions, from, to) | |
ver_from = Gem::Version.new(from) | |
ver_to = Gem::Version.new(to) | |
versions.select do |ver| | |
ver_from <= ver && ver <= ver_to | |
end | |
end | |
def git_dirty? | |
system('git diff --quiet') | |
!$?.success? | |
end | |
def gsub_file(path, rex, replace) | |
File.write(path, File.readlines(path).map{|line| line.gsub(rex, replace) }.join) | |
end | |
def change_version(gem_name, version) | |
gsub_file 'Gemfile', /\s*gem ['"]#{gem_name}['"].*$/, "gem '#{gem_name}', '= #{version}'" | |
#system("git diff Gemfile") # Show the diff | |
`bundle update #{gem_name}` | |
bundle_ok = $?.success? | |
warn "Could not bundle #{gem_name} #{version}" unless bundle_ok | |
bundle_ok | |
end | |
def reset_version | |
`git checkout Gemfile Gemfile.lock` | |
end | |
def test | |
system('rails test') | |
$?.success? | |
end | |
if $0 == __FILE__ | |
if git_dirty? | |
warn 'Do not run gem-bisect on a dirty repository. It needs to be able to change the Gemfile and reset the Gemfile' | |
return | |
end | |
versions = filter_major_minor(to_version_objects(load_version_list(gem_name))) | |
from ||= versions.first.to_s | |
to ||= versions.last.to_s | |
versions = filter_range(versions, from, to) | |
puts "Checking versions #{versions.map(&:to_s).join(', ')}" | |
good_version = versions.bsearch do |version| | |
test_result = nil | |
begin | |
puts | |
puts '-' * 80 | |
puts Time.now # rubocop:disable Rails/TimeZone (This is not a rails script) | |
puts "Trying #{gem_name} #{version}" | |
test_result = change_version(gem_name, version) && test | |
ensure | |
reset_version | |
end | |
test_result | |
end | |
good_idx = versions.index(good_version) | |
bad_idx = good_idx - 1 | |
bad_version = versions[bad_idx]&.to_s unless bad_idx < 0 | |
puts | |
puts '=' * 80 | |
puts | |
puts <<~STR | |
last good: #{good_version&.to_s} | |
first bad: #{bad_version || 'Hurray! No failing versions.'} | |
STR | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment