Skip to content

Instantly share code, notes, and snippets.

@costa
Last active August 30, 2024 16:33
Show Gist options
  • Save costa/f9ae7184ca254b25f98872ae7a170358 to your computer and use it in GitHub Desktop.
Save costa/f9ae7184ca254b25f98872ae7a170358 to your computer and use it in GitHub Desktop.
Git-based "report-skip-retry" RSpec helper for detailing code version releases and more efficient testing -- just `require` it in your spec_helper.rb
require 'yaml'
require 'git'
module GitHelper
class << self
def record!(example, result, time:)
if dirty?
puts "\nNot actually git-recording (repo dirty)\n#{example.full_description}: #{result} (#{time}s)"
else
record.track! example, result, time
record.save!
end
end
def passed?(example)
!dirty? && passed_include?(example)
end
def dirty!
@dirty = true
end
private
def dirty?
@dirty || !record
end
def record
return @record if defined? @record
@record = Record.open(File.expand_path '.')
end
def passed_include?(example)
!!record.passed[example.id]
end
end
def run(example_group_instance, reporter)
start_t = Time.now
if GitHelper.passed?(self)
puts %[\n"#{full_description}"\n -- successfully passed and git-recorded already]
true
else
super(example_group_instance, reporter).tap do |result|
GitHelper.record! self, result, time: (Time.now - start_t)
end
end
rescue Exception
GitHelper.record! self, $!, time: (Time.now - start_t)
raise
end
class Record
class << self
def open(work_dir)
g = Git.open(work_dir)
return nil unless g.diff.size == 0
new git: g
end
end
attr_reader :failed, :passed
def initialize(git:)
@g = git
@spec_commit = last_commit!
data =
if @g.config('user.email') == @spec_commit.author.email
load
else
@spec_commit = nil
end
if data.respond_to?(:keys) && ([:failed, :passed] - data.keys).empty?
puts "git-record: will try and fix spec run results in YAML ending of commit message"
data
else
if data
puts "git-record WARN: commit message has incompatible YAML ending, will overwrite"
else
puts "git-record: will try and add spec run results as YAML ending of commit message"
end
{failed: {}, passed: {}}
end.tap do |data|
@failed = data[:failed]
@passed = data[:passed]
end
end
def track!(example, result, time)
if !result || result.is_a?(Exception)
[@failed, @passed]
else
[@passed, @failed]
end.tap do |to_insert, to_delete|
to_insert[example.id] = [example.full_description, time]
to_delete.delete example.id
end
end
def save!(retries: 3)
data = {total: @failed.size + @passed.size, failed: @failed, passed: @passed}
@g.commit "#{@message}\n\n#{data.to_yaml}", allow_empty: true, amend: !!@spec_commit
@spec_commit = last_commit!
rescue Git::FailedError
raise unless
(retries -= 1) >= 0
sleep 3
retry
end
private
def last_commit!
@g.object('HEAD')
end
def load
@message = @spec_commit.message
yaml_start = @message.lines.index{|l| l.strip == '---'}
if yaml_start
begin
YAML.load @message.lines[yaml_start..-1].join
rescue # TODO be more specific here
end.tap do
@message = @message.lines[0..(yaml_start - 1)].join.strip
end
end
end
end
end
# TODO couldn't get it working with an `around` hook (playing nicely with other hooks), had to resort to monkey patching
class RSpec::Core::Example
prepend GitHelper
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment