derailed_benchmarks 1.5.0 → 2.0.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +75 -0
  3. data/.github/workflows/check_changelog.yml +11 -8
  4. data/CHANGELOG.md +26 -1
  5. data/README.md +44 -11
  6. data/derailed_benchmarks.gemspec +7 -5
  7. data/gemfiles/rails_5_1.gemfile +3 -1
  8. data/gemfiles/rails_5_2.gemfile +3 -3
  9. data/gemfiles/rails_6_1.gemfile +13 -0
  10. data/gemfiles/rails_git.gemfile +2 -2
  11. data/lib/derailed_benchmarks.rb +4 -2
  12. data/lib/derailed_benchmarks/core_ext/kernel_require.rb +29 -24
  13. data/lib/derailed_benchmarks/git/commit.rb +36 -0
  14. data/lib/derailed_benchmarks/git/in_path.rb +59 -0
  15. data/lib/derailed_benchmarks/git/switch_project.rb +128 -0
  16. data/lib/derailed_benchmarks/git_switch_project.rb +1 -0
  17. data/lib/derailed_benchmarks/load_tasks.rb +11 -4
  18. data/lib/derailed_benchmarks/require_tree.rb +11 -1
  19. data/lib/derailed_benchmarks/{stats_in_file.rb → stats_for_file.rb} +8 -2
  20. data/lib/derailed_benchmarks/stats_from_dir.rb +53 -9
  21. data/lib/derailed_benchmarks/tasks.rb +32 -63
  22. data/lib/derailed_benchmarks/version.rb +1 -1
  23. data/test/derailed_benchmarks/core_ext/kernel_require_test.rb +70 -11
  24. data/test/derailed_benchmarks/git_switch_project_test.rb +83 -0
  25. data/test/derailed_benchmarks/require_tree_test.rb +1 -1
  26. data/test/derailed_benchmarks/stats_from_dir_test.rb +43 -10
  27. data/test/fixtures/require/autoload_child.rb +5 -0
  28. data/test/fixtures/require/autoload_parent.rb +8 -0
  29. data/test/fixtures/require/child_one.rb +1 -1
  30. data/test/fixtures/require/child_two.rb +1 -1
  31. data/test/fixtures/require/load_child.rb +3 -0
  32. data/test/fixtures/require/load_parent.rb +5 -0
  33. data/test/fixtures/require/parent_one.rb +1 -1
  34. data/test/integration/tasks_test.rb +45 -5
  35. data/test/rails_app/config/application.rb +2 -0
  36. data/test/rails_app/config/storage.yml +0 -0
  37. data/test/test_helper.rb +6 -1
  38. metadata +86 -30
  39. data/.travis.yml +0 -18
  40. data/Appraisals +0 -26
@@ -0,0 +1,59 @@
1
+ module DerailedBenchmarks
2
+ # A class for running commands in a git directory
3
+ #
4
+ # It's faster to check if we're already in that directory instead
5
+ # of having to `cd` into each time. https://twitter.com/schneems/status/1305196730170961920
6
+ #
7
+ # Example:
8
+ #
9
+ # in_git_path = InGitPath.new(`bundle info heapy --path`.strip)
10
+ # in_git_path.checkout!("f0f92b06156f2274021aa42f15326da041ee9009")
11
+ # in_git_path.short_sha # => "f0f92b0"
12
+ class Git::InPath
13
+ attr_reader :path
14
+
15
+ def initialize(path)
16
+ @path = path
17
+ end
18
+
19
+ def description
20
+ run!("git log --oneline --format=%B -n 1 HEAD | head -n 1")
21
+ end
22
+
23
+ def short_sha
24
+ run!("git rev-parse --short HEAD")
25
+ end
26
+
27
+ def time_stamp_string
28
+ run!("git log -n 1 --pretty=format:%ci") # https://stackoverflow.com/a/25921837/147390
29
+ end
30
+
31
+ def branch
32
+ branch = run!("git rev-parse --abbrev-ref HEAD")
33
+ branch == "HEAD" ? nil : branch
34
+ end
35
+
36
+ def checkout!(ref)
37
+ run!("git checkout '#{ref}' 2>&1")
38
+ end
39
+
40
+ def time
41
+ DateTime.parse(time_stamp_string)
42
+ end
43
+
44
+ def run(cmd)
45
+ if Dir.pwd == path
46
+ out = `#{cmd}`.strip
47
+ else
48
+ out = `cd #{path} && #{cmd}`.strip
49
+ end
50
+ out
51
+ end
52
+
53
+ def run!(cmd)
54
+ out = run(cmd)
55
+ raise "Error while running #{cmd.inspect}: #{out}" unless $?.success?
56
+ out
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,128 @@
1
+ module DerailedBenchmarks
2
+ class Git
3
+ end
4
+ end
5
+ require_relative "in_path.rb"
6
+ require_relative "commit.rb"
7
+
8
+ module DerailedBenchmarks
9
+ # Wraps two or more git commits in a specific location
10
+ #
11
+ # Returns an array of GitCommit objects that can be used to manipulate
12
+ # and checkout the repo
13
+ #
14
+ # Example:
15
+ #
16
+ # `git clone https://sharpstone/default_ruby tmp/default_ruby`
17
+ #
18
+ # project = GitSwitchProject.new(path: "tmp/default_ruby")
19
+ #
20
+ # By default it will represent the last two commits:
21
+ #
22
+ # project.commits.length # => 2
23
+ #
24
+ # You can pass in explicit REFs in an array:
25
+ #
26
+ # ref_array = ["da748a59340be8b950e7bbbfb32077eb67d70c3c", "9b19275a592f148e2a53b87ead4ccd8c747539c9"]
27
+ # project = GitSwitchProject.new(path: "tmp/default_ruby", ref_array: ref_array)
28
+ #
29
+ # puts project.commits.map(&:ref) == ref_array # => true
30
+ #
31
+ #
32
+ # It knows the current branch or sha:
33
+ #
34
+ # `cd tmp/ruby && git checkout -b mybranch`
35
+ # project.current_branch_or_sha #=> "mybranch"
36
+ #
37
+ # It can be used for safely wrapping checkouts to ensure the project returns to it's original branch:
38
+ #
39
+ # project.restore_branch_on_return do
40
+ # project.commits.first.checkout!
41
+ # project.current_branch_or_sha # => "da748a593"
42
+ # end
43
+ #
44
+ # project.current_branch_or_sha # => "mybranch"
45
+ class Git::SwitchProject
46
+ attr_reader :commits
47
+
48
+ def initialize(path: , ref_array: [], io: STDOUT, log_dir: "/dev/null")
49
+ @path = Pathname.new(path)
50
+
51
+ @in_git_path = Git::InPath.new(@path.expand_path)
52
+
53
+ raise "Must be a path with a .git directory '#{@path}'" if !@path.join(".git").exist?
54
+ @io = io
55
+ @commits = []
56
+ log_dir = Pathname(log_dir)
57
+
58
+ expand_refs(ref_array).each do |ref|
59
+ restore_branch_on_return(quiet: true) do
60
+ @commits << Git::Commit.new(path: @path, ref: ref, log_dir: log_dir)
61
+ end
62
+ end
63
+
64
+ if (duplicate = @commits.group_by(&:short_sha).detect {|(k, v)| v.length > 1})
65
+ raise "Duplicate SHA resolved #{duplicate[0].inspect}: #{duplicate[1].map {|c| "'#{c.ref}' => '#{c.short_sha}'"}.join(", ") } at #{@path}"
66
+ end
67
+ end
68
+
69
+ def current_branch_or_sha
70
+ branch_or_sha = @in_git_path.branch
71
+ branch_or_sha ||= @in_git_path.short_sha
72
+ branch_or_sha
73
+ end
74
+
75
+ def dirty?
76
+ !clean?
77
+ end
78
+
79
+ # https://stackoverflow.com/a/3879077/147390
80
+ def clean?
81
+ @in_git_path.run("git diff-index --quiet HEAD --") && $?.success?
82
+ end
83
+
84
+ private def status(pattern: "*.gemspec")
85
+ @in_git_path.run("git status #{pattern}")
86
+ end
87
+
88
+ def restore_branch_on_return(quiet: false)
89
+ if dirty? && status.include?("gemspec")
90
+ dirty_gemspec = true
91
+ unless quiet
92
+ @io.puts "Working tree at #{@path} is dirty, stashing. This will be popped on return"
93
+ @io.puts "Bundler modifies gemspec files on git install, this is normal"
94
+ @io.puts "Original status:\n#{status}"
95
+ end
96
+ @in_git_path.run!("git stash")
97
+ end
98
+ branch_or_sha = self.current_branch_or_sha
99
+ yield
100
+ ensure
101
+ return unless branch_or_sha
102
+ @io.puts "Resetting git dir of '#{@path.to_s}' to #{branch_or_sha.inspect}" unless quiet
103
+
104
+ @in_git_path.checkout!(branch_or_sha)
105
+ if dirty_gemspec
106
+ out = @in_git_path.run!("git stash apply 2>&1")
107
+ @io.puts "Applying stash of '#{@path.to_s}':\n#{out}" unless quiet
108
+ end
109
+ end
110
+
111
+ # case ref_array.length
112
+ # when >= 2
113
+ # returns original array
114
+ # when 1
115
+ # returns the given ref plus the one before it
116
+ # when 0
117
+ # returns the most recent 2 refs
118
+ private def expand_refs(ref_array)
119
+ return ref_array if ref_array.length >= 2
120
+
121
+ @in_git_path.checkout!(ref_array.first) if ref_array.first
122
+
123
+ branches_string = @in_git_path.run!("git log --format='%H' -n 2")
124
+ ref_array = branches_string.split($/)
125
+ return ref_array
126
+ end
127
+ end
128
+ end
@@ -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 ENV["DERAILED_SKIP_ACTIVE_RECORD"] && defined? ActiveRecord
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
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, nil)
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
@@ -103,7 +110,7 @@ namespace :perf do
103
110
  STDERR.puts "Bad request to #{cmd.inspect} \n\n***RESPONSE***:\n\n#{ response.inspect }"
104
111
 
105
112
  FileUtils.mkdir_p("tmp")
106
- File.open("tmp/fail.html", "w+") {|f| f.write response.body }
113
+ File.open("tmp/fail.html", "w+") {|f| f.write response }
107
114
 
108
115
  `open #{File.expand_path("tmp/fail.html")}` if ENV["DERAILED_DEBUG"]
109
116
 
@@ -135,4 +142,4 @@ namespace :perf do
135
142
  WARM_COUNT.times { call_app }
136
143
  end
137
144
  end
138
- end
145
+ end
@@ -13,6 +13,16 @@ module DerailedBenchmarks
13
13
  def initialize(name)
14
14
  @name = name
15
15
  @children = {}
16
+ @cost = 0
17
+ end
18
+
19
+ def self.reset!
20
+ REQUIRED_BY.clear
21
+ if defined?(Kernel::REQUIRE_STACK)
22
+ Kernel::REQUIRE_STACK.clear
23
+
24
+ Kernel::REQUIRE_STACK.push(TOP_REQUIRE)
25
+ end
16
26
  end
17
27
 
18
28
  def <<(tree)
@@ -40,7 +50,7 @@ module DerailedBenchmarks
40
50
  end
41
51
 
42
52
  def to_string
43
- str = +"#{name}: #{cost.round(4)} MiB"
53
+ str = String.new("#{name}: #{cost.round(4)} MiB")
44
54
  if parent && REQUIRED_BY[self.name.to_s]
45
55
  names = REQUIRED_BY[self.name.to_s].uniq - [parent.name.to_s]
46
56
  if names.any?
@@ -16,24 +16,30 @@ 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
 
33
35
  @median = (values[(values.length - 1) / 2] + values[values.length/ 2]) / 2.0
34
36
  @average = values.inject(:+) / values.length
35
37
  end
36
38
 
39
+ def empty?
40
+ values.empty?
41
+ end
42
+
37
43
  def median
38
44
  @median.to_f
39
45
  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(hash)
32
+ def initialize(input)
30
33
  @files = []
31
34
 
32
- hash.each do |branch, info_hash|
33
- file = info_hash.fetch(:file)
34
- desc = info_hash.fetch(:desc)
35
- time = info_hash.fetch(:time)
36
- @files << StatsForFile.new(file: file, desc: desc, time: time, name: branch)
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
@@ -43,6 +60,8 @@ module DerailedBenchmarks
43
60
  def call
44
61
  @files.each(&:call)
45
62
 
63
+ return self if @files.detect(&:empty?)
64
+
46
65
  stats_95 = statistical_test(confidence: 95)
47
66
 
48
67
  # If default check is good, see if we also pass a more rigorous test
@@ -100,7 +119,29 @@ module DerailedBenchmarks
100
119
  " " * (percent_faster.to_s.index(".") - x_faster.to_s.index("."))
101
120
  end
102
121
 
103
- def banner(io = Kernel)
122
+ def histogram(io = $stdout)
123
+ dual_histogram = MiniHistogram.dual_plot do |a, b|
124
+ a.values = newest.values
125
+ a.options = {
126
+ title: "\n [#{newest.short_sha || newest.name}] description:\n #{newest.desc.inspect}",
127
+ xlabel: "# of runs in range"
128
+ }
129
+ b.values = oldest.values
130
+ b.options = {
131
+ title: "\n [#{oldest.short_sha || oldest.name}] description:\n #{oldest.desc.inspect}",
132
+ xlabel: "# of runs in range"
133
+ }
134
+ end
135
+
136
+ io.puts
137
+ io.puts "Histograms (time ranges are in seconds):"
138
+ io.puts(dual_histogram)
139
+ io.puts
140
+ end
141
+
142
+ def banner(io = $stdout)
143
+ return if @files.detect(&:empty?)
144
+
104
145
  io.puts
105
146
  if significant?
106
147
  io.puts "❤️ ❤️ ❤️ (Statistically Significant) ❤️ ❤️ ❤️"
@@ -108,11 +149,11 @@ module DerailedBenchmarks
108
149
  io.puts "👎👎👎(NOT Statistically Significant) 👎👎👎"
109
150
  end
110
151
  io.puts
111
- io.puts "[#{newest.name}] #{newest.desc.inspect} - (#{newest.median} seconds)"
152
+ io.puts "[#{newest.short_sha || newest.name}] (#{FORMAT % newest.median} seconds) #{newest.desc.inspect} ref: #{newest.name.inspect}"
112
153
  io.puts " #{change_direction} by:"
113
154
  io.puts " #{align}#{FORMAT % x_faster}x [older/newer]"
114
155
  io.puts " #{FORMAT % percent_faster}\% [(older - newer) / older * 100]"
115
- io.puts "[#{oldest.name}] #{oldest.desc.inspect} - (#{oldest.median} seconds)"
156
+ io.puts "[#{oldest.short_sha || oldest.name}] (#{FORMAT % oldest.median} seconds) #{oldest.desc.inspect} ref: #{oldest.name.inspect}"
116
157
  io.puts
117
158
  io.puts "Iterations per sample: #{ENV["TEST_COUNT"]}"
118
159
  io.puts "Samples: #{newest.values.length}"
@@ -122,6 +163,9 @@ module DerailedBenchmarks
122
163
  io.puts "Is significant? (max > critical): #{significant?}"
123
164
  io.puts "D critical: #{d_critical}"
124
165
  io.puts "D max: #{d_max}"
166
+
167
+ histogram(io)
168
+
125
169
  io.puts
126
170
  end
127
171
  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,94 +17,57 @@ 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
18
-
19
- raise "Must be a path with a .git directory '#{library_dir}'" unless File.exist?(File.join(library_dir, ".git"))
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 }
24
+ library_dir = Pathname.new(library_dir)
41
25
 
42
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
- branches_to_test = branch_names.each_with_object({}) {|elem, hash| hash[elem] = out_dir + "#{elem.gsub('/', ':')}.bench.txt" }
46
- branch_info = {}
47
- branch_to_sha = {}
29
+ ref_string = ENV["SHAS_TO_TEST"] || ENV["REFS_TO_TEST"] || ""
48
30
 
49
- branches_to_test.each do |branch, file|
50
- Dir.chdir(library_dir) do
51
- run!("git checkout '#{branch}'")
52
- description = run!("git log --oneline --format=%B -n 1 HEAD | head -n 1").strip
53
- time_stamp = run!("git log -n 1 --pretty=format:%ci").strip # https://stackoverflow.com/a/25921837/147390
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
- branch_info[short_sha] = { desc: description, time: DateTime.parse(time_stamp), file: file }
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
- branches_to_test.each.with_index do |(branch, _), i|
65
- short_sha = branch_to_sha[branch]
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
- raise "SHAs to test must be different" if branch_info.length == 1
73
- stats = DerailedBenchmarks::StatsFromDir.new(branch_info)
74
- puts "Env var no longer has any affect DERAILED_STOP_VALID_COUNT" if ENV["DERAILED_STOP_VALID_COUNT"]
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!
75
53
 
76
- DERAILED_SCRIPT_COUNT.times do |i|
77
- puts "Sample: #{i.next}/#{DERAILED_SCRIPT_COUNT} iterations per sample: #{ENV['TEST_COUNT']}"
78
- branches_to_test.each do |branch, file|
79
- Dir.chdir(library_dir) { run!("git checkout '#{branch}'") }
80
- run!(" #{script} 2>&1 | tail -n 1 >> '#{file}'")
81
- end
54
+ output = run!("#{script} 2>&1")
55
+ commit.log.open("a") {|f| f.puts output.lines.last }
56
+ end
82
57
 
83
- if (i % 50).zero?
84
- puts "Intermediate result"
85
- stats.call.banner
86
- puts "Continuing execution"
58
+ if (i % 50).zero?
59
+ puts "Intermediate result"
60
+ stats.call.banner
61
+ puts "Continuing execution"
62
+ end
87
63
  end
88
64
  end
89
65
 
90
66
  ensure
91
- if library_dir && current_library_branch
92
- puts "Resetting git dir of '#{library_dir.to_s}' to #{current_library_branch.inspect}"
93
- Dir.chdir(library_dir) do
94
- run!("git checkout '#{current_library_branch}'")
95
- end
96
- end
97
-
98
67
  if stats
99
68
  stats.call.banner
100
69
 
101
- result_file = out_dir + "results.txt"
70
+ result_file = out_dir.join("results.txt")
102
71
  File.open(result_file, "w") do |f|
103
72
  stats.banner(f)
104
73
  end