derailed_benchmarks 1.4.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/check_changelog.yml +11 -8
- data/.travis.yml +9 -7
- data/CHANGELOG.md +28 -1
- data/README.md +15 -5
- data/derailed_benchmarks.gemspec +4 -3
- data/lib/derailed_benchmarks.rb +2 -1
- data/lib/derailed_benchmarks/core_ext/kernel_require.rb +12 -18
- data/lib/derailed_benchmarks/git/commit.rb +36 -0
- data/lib/derailed_benchmarks/git/in_path.rb +59 -0
- data/lib/derailed_benchmarks/git/switch_project.rb +128 -0
- data/lib/derailed_benchmarks/git_switch_project.rb +1 -0
- data/lib/derailed_benchmarks/load_tasks.rb +10 -3
- data/lib/derailed_benchmarks/require_tree.rb +1 -1
- data/lib/derailed_benchmarks/{stats_in_file.rb → stats_for_file.rb} +15 -2
- data/lib/derailed_benchmarks/stats_from_dir.rb +84 -17
- data/lib/derailed_benchmarks/tasks.rb +43 -64
- data/lib/derailed_benchmarks/version.rb +1 -1
- data/test/derailed_benchmarks/git_switch_project_test.rb +83 -0
- data/test/derailed_benchmarks/stats_from_dir_test.rb +67 -16
- data/test/fixtures/require/child_one.rb +1 -1
- data/test/fixtures/require/child_two.rb +1 -1
- data/test/fixtures/require/parent_one.rb +1 -1
- data/test/integration/tasks_test.rb +43 -5
- data/test/rails_app/app/assets/config/manifest.js +0 -0
- data/test/rails_app/config/application.rb +2 -0
- data/test/test_helper.rb +6 -0
- metadata +40 -12
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -30,7 +30,7 @@ namespace :perf do
|
|
30
30
|
DERAILED_APP.initialize! unless DERAILED_APP.instance_variable_get(:@initialized)
|
31
31
|
end
|
32
32
|
|
33
|
-
if
|
33
|
+
if !ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
|
34
34
|
if defined? ActiveRecord::Tasks::DatabaseTasks
|
35
35
|
ActiveRecord::Tasks::DatabaseTasks.create_current
|
36
36
|
else # Rails 3.2
|
@@ -39,7 +39,14 @@ namespace :perf do
|
|
39
39
|
|
40
40
|
ActiveRecord::Migrator.migrations_paths = DERAILED_APP.paths['db/migrate'].to_a
|
41
41
|
ActiveRecord::Migration.verbose = true
|
42
|
-
|
42
|
+
|
43
|
+
if Rails.version.start_with? '6'
|
44
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration).migrate
|
45
|
+
elsif Rails.version.start_with? '5.2'
|
46
|
+
ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths).migrate
|
47
|
+
else
|
48
|
+
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil)
|
49
|
+
end
|
43
50
|
end
|
44
51
|
|
45
52
|
DERAILED_APP.config.consider_all_requests_local = true
|
@@ -135,4 +142,4 @@ namespace :perf do
|
|
135
142
|
WARM_COUNT.times { call_app }
|
136
143
|
end
|
137
144
|
end
|
138
|
-
end
|
145
|
+
end
|
@@ -40,7 +40,7 @@ module DerailedBenchmarks
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def to_string
|
43
|
-
str =
|
43
|
+
str = String.new("#{name}: #{cost.round(4)} MiB")
|
44
44
|
if parent && REQUIRED_BY[self.name.to_s]
|
45
45
|
names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
|
46
46
|
if names.any?
|
@@ -16,23 +16,34 @@ module DerailedBenchmarks
|
|
16
16
|
# x.average # => 10.5
|
17
17
|
# x.name # => "muhfile"
|
18
18
|
class StatsForFile
|
19
|
-
attr_reader :name, :values, :desc, :time
|
19
|
+
attr_reader :name, :values, :desc, :time, :short_sha
|
20
20
|
|
21
|
-
def initialize(file:, name:, desc: "", time: )
|
21
|
+
def initialize(file:, name:, desc: "", time: , short_sha: nil)
|
22
22
|
@file = Pathname.new(file)
|
23
23
|
FileUtils.touch(@file)
|
24
24
|
|
25
25
|
@name = name
|
26
26
|
@desc = desc
|
27
27
|
@time = time
|
28
|
+
@short_sha = short_sha
|
28
29
|
end
|
29
30
|
|
30
31
|
def call
|
31
32
|
load_file!
|
33
|
+
return if values.empty?
|
32
34
|
|
35
|
+
@median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0
|
33
36
|
@average = values.inject(:+) / values.length
|
34
37
|
end
|
35
38
|
|
39
|
+
def empty?
|
40
|
+
values.empty?
|
41
|
+
end
|
42
|
+
|
43
|
+
def median
|
44
|
+
@median.to_f
|
45
|
+
end
|
46
|
+
|
36
47
|
def average
|
37
48
|
@average.to_f
|
38
49
|
end
|
@@ -47,6 +58,8 @@ module DerailedBenchmarks
|
|
47
58
|
raise e, "Problem with file #{@file.inspect}:\n#{@file.read}\n#{e.message}"
|
48
59
|
end
|
49
60
|
end
|
61
|
+
|
62
|
+
values.sort!
|
50
63
|
values.freeze
|
51
64
|
end
|
52
65
|
end
|
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
require 'bigdecimal'
|
4
4
|
require 'statistics'
|
5
|
+
require 'stringio'
|
6
|
+
require 'mini_histogram'
|
7
|
+
require 'mini_histogram/plot'
|
5
8
|
|
6
9
|
module DerailedBenchmarks
|
7
10
|
# A class used to read several benchmark files
|
@@ -26,14 +29,28 @@ module DerailedBenchmarks
|
|
26
29
|
FORMAT = "%0.4f"
|
27
30
|
attr_reader :stats, :oldest, :newest
|
28
31
|
|
29
|
-
def initialize(
|
32
|
+
def initialize(input)
|
30
33
|
@files = []
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
if input.is_a?(Hash)
|
36
|
+
hash = input
|
37
|
+
hash.each do |branch, info_hash|
|
38
|
+
file = info_hash.fetch(:file)
|
39
|
+
desc = info_hash.fetch(:desc)
|
40
|
+
time = info_hash.fetch(:time)
|
41
|
+
short_sha = info_hash["short_sha"]
|
42
|
+
@files << StatsForFile.new(file: file, desc: desc, time: time, name: branch, short_sha: short_sha)
|
43
|
+
end
|
44
|
+
else
|
45
|
+
input.each do |commit|
|
46
|
+
@files << StatsForFile.new(
|
47
|
+
file: commit.file,
|
48
|
+
desc: commit.desc,
|
49
|
+
time: commit.time,
|
50
|
+
name: commit.ref,
|
51
|
+
short_sha: commit.short_sha
|
52
|
+
)
|
53
|
+
end
|
37
54
|
end
|
38
55
|
@files.sort_by! { |f| f.time }
|
39
56
|
@oldest = @files.first
|
@@ -42,14 +59,27 @@ module DerailedBenchmarks
|
|
42
59
|
|
43
60
|
def call
|
44
61
|
@files.each(&:call)
|
45
|
-
|
62
|
+
|
63
|
+
return self if @files.detect(&:empty?)
|
64
|
+
|
65
|
+
stats_95 = statistical_test(confidence: 95)
|
66
|
+
|
67
|
+
# If default check is good, see if we also pass a more rigorous test
|
68
|
+
# if so, then use the more rigourous test
|
69
|
+
if stats_95[:alternative]
|
70
|
+
stats_99 = statistical_test(confidence: 99)
|
71
|
+
@stats = stats_99 if stats_99[:alternative]
|
72
|
+
end
|
73
|
+
@stats ||= stats_95
|
74
|
+
|
46
75
|
self
|
47
76
|
end
|
48
77
|
|
49
|
-
def statistical_test(series_1=oldest.values, series_2=newest.values)
|
78
|
+
def statistical_test(series_1=oldest.values, series_2=newest.values, confidence: 95)
|
50
79
|
StatisticalTest::KSTest.two_samples(
|
51
80
|
group_one: series_1,
|
52
|
-
group_two: series_2
|
81
|
+
group_two: series_2,
|
82
|
+
alpha: (100 - confidence) / 100.0
|
53
83
|
)
|
54
84
|
end
|
55
85
|
|
@@ -66,18 +96,51 @@ module DerailedBenchmarks
|
|
66
96
|
end
|
67
97
|
|
68
98
|
def x_faster
|
69
|
-
|
99
|
+
(oldest.median/newest.median).to_f
|
100
|
+
end
|
101
|
+
|
102
|
+
def faster?
|
103
|
+
newest.median < oldest.median
|
70
104
|
end
|
71
105
|
|
72
106
|
def percent_faster
|
73
|
-
|
107
|
+
(((oldest.median - newest.median) / oldest.median).to_f * 100)
|
74
108
|
end
|
75
109
|
|
76
110
|
def change_direction
|
77
|
-
|
111
|
+
if faster?
|
112
|
+
"FASTER 🚀🚀🚀"
|
113
|
+
else
|
114
|
+
"SLOWER 🐢🐢🐢"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def align
|
119
|
+
" " * (percent_faster.to_s.index(".") - x_faster.to_s.index("."))
|
78
120
|
end
|
79
121
|
|
80
|
-
def
|
122
|
+
def histogram(io = $stdout)
|
123
|
+
newest_histogram = MiniHistogram.new(newest.values)
|
124
|
+
oldest_histogram = MiniHistogram.new(oldest.values)
|
125
|
+
MiniHistogram.set_average_edges!(newest_histogram, oldest_histogram)
|
126
|
+
|
127
|
+
{newest => newest_histogram, oldest => oldest_histogram}.each do |report, histogram|
|
128
|
+
plot = histogram.plot(
|
129
|
+
title: "\n#{' ' * 18 }Histogram - [#{report.short_sha || report.name}] #{report.desc.inspect}",
|
130
|
+
ylabel: "Time (s)",
|
131
|
+
xlabel: "# of runs in range"
|
132
|
+
)
|
133
|
+
|
134
|
+
plot.render(io)
|
135
|
+
io.puts
|
136
|
+
end
|
137
|
+
|
138
|
+
io.puts
|
139
|
+
end
|
140
|
+
|
141
|
+
def banner(io = $stdout)
|
142
|
+
return if @files.detect(&:empty?)
|
143
|
+
|
81
144
|
io.puts
|
82
145
|
if significant?
|
83
146
|
io.puts "❤️ ❤️ ❤️ (Statistically Significant) ❤️ ❤️ ❤️"
|
@@ -85,19 +148,23 @@ module DerailedBenchmarks
|
|
85
148
|
io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎"
|
86
149
|
end
|
87
150
|
io.puts
|
88
|
-
io.puts "[#{newest.name}] #{newest.desc.inspect}
|
151
|
+
io.puts "[#{newest.short_sha || newest.name}] (#{FORMAT % newest.median} seconds) #{newest.desc.inspect} ref: #{newest.name.inspect}"
|
89
152
|
io.puts " #{change_direction} by:"
|
90
|
-
io.puts " #{x_faster}x [older/newer]"
|
91
|
-
io.puts " #{percent_faster}\% [(older - newer) / older * 100]"
|
92
|
-
io.puts "[#{oldest.name}] #{oldest.desc.inspect}
|
153
|
+
io.puts " #{align}#{FORMAT % x_faster}x [older/newer]"
|
154
|
+
io.puts " #{FORMAT % percent_faster}\% [(older - newer) / older * 100]"
|
155
|
+
io.puts "[#{oldest.short_sha || oldest.name}] (#{FORMAT % oldest.median} seconds) #{oldest.desc.inspect} ref: #{oldest.name.inspect}"
|
93
156
|
io.puts
|
94
157
|
io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}"
|
95
158
|
io.puts "Samples: #{newest.values.length}"
|
96
159
|
io.puts
|
97
160
|
io.puts "Test type: Kolmogorov Smirnov"
|
161
|
+
io.puts "Confidence level: #{@stats[:confidence_level] * 100} %"
|
98
162
|
io.puts "Is significant? (max > critical): #{significant?}"
|
99
163
|
io.puts "D critical: #{d_critical}"
|
100
164
|
io.puts "D max: #{d_max}"
|
165
|
+
|
166
|
+
histogram(io)
|
167
|
+
|
101
168
|
io.puts
|
102
169
|
end
|
103
170
|
end
|
@@ -1,6 +1,12 @@
|
|
1
1
|
require_relative 'load_tasks'
|
2
2
|
|
3
3
|
namespace :perf do
|
4
|
+
desc "runs the performance test against two most recent commits of the current app"
|
5
|
+
task :app do
|
6
|
+
ENV["DERAILED_PATH_TO_LIBRARY"] = '.'
|
7
|
+
Rake::Task["perf:library"].invoke
|
8
|
+
end
|
9
|
+
|
4
10
|
desc "runs the same test against two different branches for statistical comparison"
|
5
11
|
task :library do
|
6
12
|
begin
|
@@ -11,92 +17,65 @@ namespace :perf do
|
|
11
17
|
script = ENV["DERAILED_SCRIPT"] || "bundle exec derailed exec perf:test"
|
12
18
|
|
13
19
|
if ENV["DERAILED_PATH_TO_LIBRARY"]
|
14
|
-
library_dir = ENV["DERAILED_PATH_TO_LIBRARY"]
|
20
|
+
library_dir = ENV["DERAILED_PATH_TO_LIBRARY"].chomp
|
15
21
|
else
|
16
22
|
library_dir = DerailedBenchmarks.rails_path_on_disk
|
17
23
|
end
|
24
|
+
library_dir = Pathname.new(library_dir)
|
18
25
|
|
19
|
-
|
20
|
-
|
21
|
-
# Use either the explicit SHAs when present or grab last two SHAs from commit history
|
22
|
-
# if only one SHA is given, then use it and the last SHA from commit history
|
23
|
-
branch_names = []
|
24
|
-
branch_names = ENV.fetch("SHAS_TO_TEST").split(",") if ENV["SHAS_TO_TEST"]
|
25
|
-
if branch_names.length < 2
|
26
|
-
Dir.chdir(library_dir) do
|
27
|
-
run!("git checkout '#{branch_names.first}'") unless branch_names.empty?
|
28
|
-
|
29
|
-
branches = run!('git log --format="%H" -n 2').chomp.split($/)
|
30
|
-
if branch_names.empty?
|
31
|
-
branch_names = branches
|
32
|
-
else
|
33
|
-
branches.shift
|
34
|
-
branch_names << branches.shift
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
current_library_branch = ""
|
40
|
-
Dir.chdir(library_dir) { current_library_branch = run!('git describe --contains --all HEAD').chomp }
|
41
|
-
|
42
|
-
out_dir = Pathname.new("tmp/library_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
|
26
|
+
out_dir = Pathname.new("tmp/compare_branches/#{Time.now.strftime('%Y-%m-%d-%H-%M-%s-%N')}")
|
43
27
|
out_dir.mkpath
|
44
28
|
|
45
|
-
|
46
|
-
branch_info = {}
|
47
|
-
branch_to_sha = {}
|
29
|
+
ref_string = ENV["SHAS_TO_TEST"] || ENV["REFS_TO_TEST"] || ""
|
48
30
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
short_sha = run!("git rev-parse --short HEAD").strip
|
55
|
-
branch_to_sha[branch] = short_sha
|
31
|
+
project = DerailedBenchmarks::Git::SwitchProject.new(
|
32
|
+
path: library_dir,
|
33
|
+
ref_array: ref_string.split(","),
|
34
|
+
log_dir: out_dir
|
35
|
+
)
|
56
36
|
|
57
|
-
|
58
|
-
end
|
59
|
-
run!("#{script}")
|
60
|
-
end
|
37
|
+
stats = DerailedBenchmarks::StatsFromDir.new(project.commits)
|
61
38
|
|
39
|
+
# Advertise branch names early to make sure people know what they're testing
|
62
40
|
puts
|
63
41
|
puts
|
64
|
-
|
65
|
-
short_sha
|
66
|
-
desc = branch_info[short_sha][:desc]
|
67
|
-
puts "Testing #{i + 1}: #{short_sha}: #{desc}"
|
42
|
+
project.commits.each_with_index do |commit, i|
|
43
|
+
puts "Testing #{i + 1}: #{commit.short_sha}: #{commit.description}"
|
68
44
|
end
|
69
45
|
puts
|
70
46
|
puts
|
71
47
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
48
|
+
project.restore_branch_on_return do
|
49
|
+
DERAILED_SCRIPT_COUNT.times do |i|
|
50
|
+
puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
|
51
|
+
project.commits.each do |commit|
|
52
|
+
commit.checkout!
|
53
|
+
|
54
|
+
output = run!("#{script} 2>&1")
|
55
|
+
commit.log.open("a") {|f| f.puts output.lines.last }
|
56
|
+
end
|
57
|
+
|
58
|
+
if (i % 50).zero?
|
59
|
+
puts "Intermediate result"
|
60
|
+
stats.call.banner
|
61
|
+
puts "Continuing execution"
|
62
|
+
end
|
83
63
|
end
|
84
|
-
times_significant += 1 if i >= 2 && stats.call.significant?
|
85
|
-
break if stop_valid_count != 0 && times_significant == stop_valid_count
|
86
64
|
end
|
87
65
|
|
88
66
|
ensure
|
89
|
-
if
|
90
|
-
|
91
|
-
|
92
|
-
|
67
|
+
if stats
|
68
|
+
stats.call.banner
|
69
|
+
|
70
|
+
result_file = out_dir.join("results.txt")
|
71
|
+
File.open(result_file, "w") do |f|
|
72
|
+
stats.banner(f)
|
93
73
|
end
|
94
|
-
end
|
95
74
|
|
96
|
-
|
75
|
+
puts "Output: #{result_file.to_s}"
|
76
|
+
end
|
97
77
|
end
|
98
|
-
|
99
|
-
|
78
|
+
end
|
100
79
|
|
101
80
|
desc "hits the url TEST_COUNT times"
|
102
81
|
task :test => [:setup] do
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
class GitSwitchProjectTest < ActiveSupport::TestCase
|
6
|
+
test "tells me when it's not pointing at a git project" do
|
7
|
+
exception = assert_raises {
|
8
|
+
DerailedBenchmarks::Git::SwitchProject.new(path: "/dev/null")
|
9
|
+
}
|
10
|
+
assert_includes(exception.message, '.git directory')
|
11
|
+
end
|
12
|
+
|
13
|
+
test "dirty gemspec cleaning" do
|
14
|
+
Dir.mktmpdir do |dir|
|
15
|
+
run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1")
|
16
|
+
run!("cd #{dir} && echo lol > foo.gemspec && git add .")
|
17
|
+
|
18
|
+
io = StringIO.new
|
19
|
+
project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, io: io)
|
20
|
+
|
21
|
+
assert project.dirty?
|
22
|
+
refute project.clean?
|
23
|
+
|
24
|
+
project.restore_branch_on_return do
|
25
|
+
project.commits.map(&:checkout!)
|
26
|
+
end
|
27
|
+
|
28
|
+
assert_includes io.string, "Bundler modifies gemspec files"
|
29
|
+
assert_includes io.string, "Applying stash"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
test "works on a git repo" do
|
34
|
+
Dir.mktmpdir do |dir|
|
35
|
+
run!("git clone https://github.com/sharpstone/default_ruby #{dir} 2>&1 && cd #{dir} && git checkout 6e642963acec0ff64af51bd6fba8db3c4176ed6e 2>&1 && git checkout -b mybranch 2>&1")
|
36
|
+
|
37
|
+
# finds shas when none given
|
38
|
+
project = DerailedBenchmarks::Git::SwitchProject.new(path: dir)
|
39
|
+
|
40
|
+
assert_equal ["6e642963acec0ff64af51bd6fba8db3c4176ed6e", "da748a59340be8b950e7bbbfb32077eb67d70c3c"], project.commits.map(&:ref)
|
41
|
+
first_commit = project.commits.first
|
42
|
+
|
43
|
+
assert_equal "CI test support", first_commit.description
|
44
|
+
assert_equal "6e64296", first_commit.short_sha
|
45
|
+
assert_equal "/dev/null/6e642963acec0ff64af51bd6fba8db3c4176ed6e.bench.txt", first_commit.log.to_s
|
46
|
+
assert_equal DateTime.parse("Tue, 14 Apr 2020 13:26:03 -0500"), first_commit.time
|
47
|
+
|
48
|
+
assert_equal "mybranch", project.current_branch_or_sha
|
49
|
+
|
50
|
+
# Finds shas when 1 is given
|
51
|
+
project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["da748a59340be8b950e7bbbfb32077eb67d70c3c"])
|
52
|
+
|
53
|
+
assert_equal ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "5c09f748957d2098182762004adee27d1ff83160"], project.commits.map(&:ref)
|
54
|
+
|
55
|
+
|
56
|
+
# Returns correct refs if given
|
57
|
+
project = DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"])
|
58
|
+
|
59
|
+
assert_equal ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"], project.commits.map(&:ref)
|
60
|
+
|
61
|
+
first_commit = project.commits.first
|
62
|
+
|
63
|
+
first_commit.checkout!
|
64
|
+
|
65
|
+
assert_equal first_commit.short_sha, project.current_branch_or_sha
|
66
|
+
|
67
|
+
# Test restore_branch_on_return
|
68
|
+
project.restore_branch_on_return(quiet: true) do
|
69
|
+
project.commits.last.checkout!
|
70
|
+
|
71
|
+
assert_equal project.commits.last.short_sha, project.current_branch_or_sha
|
72
|
+
end
|
73
|
+
|
74
|
+
assert_equal project.commits.first.short_sha, project.current_branch_or_sha
|
75
|
+
|
76
|
+
exception = assert_raise {
|
77
|
+
DerailedBenchmarks::Git::SwitchProject.new(path: dir, ref_array: ["6e642963acec0ff64af51bd6fba8db3c4176ed6e", "mybranch"])
|
78
|
+
}
|
79
|
+
|
80
|
+
assert_includes(exception.message, 'Duplicate SHA resolved "6e64296"')
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|