covet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,164 @@
1
+ require 'json'
2
+
3
+ module Covet
4
+ # Represents log file of JSON coverage information for each test method.
5
+ # For each write of a memory buffer to disk, a separate index file keeps track
6
+ # of the file offset and bytes written for the buffer. This is so that when
7
+ # there lots of tests in a test suite, we don't have to keep all coverage
8
+ # information in memory. Instead, we flush the information and write it to
9
+ # disk at certain intervals. This way, we can also load the information in
10
+ # chunks as well, using the same index file.
11
+ class LogFile
12
+ require 'tempfile'
13
+ require 'fileutils'
14
+
15
+ LoadError = Class.new(StandardError)
16
+
17
+ attr_reader :name, :writes
18
+
19
+ def initialize(options = {})
20
+ @mode = options[:mode] || 'w'
21
+ @name = options[:filename] || File.join(Dir.pwd, 'run_log.json')
22
+ if @mode != 'r'
23
+ # We only want to create the real file during the `write_end` method, so write to
24
+ # a tempfile until then. This is in case the user stops their test suite with an
25
+ # interrupt.
26
+ @tmpfile = Tempfile.new(File.basename(@name))
27
+ @tmpname = @tmpfile.path
28
+ else
29
+ @tmpfile = nil
30
+ @tmpname = nil
31
+ end
32
+ @index_file = LogFileIndex.new(:filename => options[:index_filename])
33
+ @writes = 0
34
+ end
35
+
36
+ def write_start
37
+ check_can_write!
38
+ @tmpfile.write('[')
39
+ @writes += 1
40
+ end
41
+
42
+ def write_buf(buf)
43
+ check_can_write!
44
+ pos_start = @tmpfile.pos
45
+ @tmpfile.write(JSON.dump(buf) + ',')
46
+ @writes += 1
47
+ pos_after = @tmpfile.pos
48
+ @index_file.add_index(pos_start, pos_after - pos_start)
49
+ end
50
+
51
+ def write_end
52
+ check_can_write!
53
+ @tmpfile.pos -= 1 # remove final comma at end of array
54
+ @tmpfile.write(']')
55
+ @writes += 1
56
+ @tmpfile.close
57
+ FileUtils.cp(@tmpfile, @name)
58
+ @index_file.finish!
59
+ end
60
+
61
+ def load!
62
+ JSON.load(File.read(@name))
63
+ end
64
+
65
+ # Yields each coverage buffer (Array) one a time from the run log.
66
+ # @raises LogFile::LoadError
67
+ def load_each_buf! # yields
68
+ @index_file.reload!('r')
69
+ reload!('r')
70
+ index = JSON.load(File.read(@index_file.name))
71
+ index.each do |(pos, bytes_to_read)|
72
+ res = load_buf_from_file!(pos, bytes_to_read)
73
+ yield res # @var Array
74
+ end
75
+ end
76
+
77
+ # @raises LogFile::LoadError
78
+ # @return Array
79
+ def load_buf!(buf_idx)
80
+ @index_file.reload!('r')
81
+ reload!('r')
82
+ index = JSON.load(File.read(@index_file.name))
83
+ pos, bytes_to_read = index[buf_idx]
84
+ load_buf_from_file!(pos, bytes_to_read)
85
+ end
86
+
87
+ # @raises LogFile::LoadError
88
+ # @return Hash
89
+ def load_run_stats!
90
+ load_buf!(-1).last[-1]
91
+ end
92
+
93
+ def file_exists?
94
+ File.exist?(@name)
95
+ end
96
+
97
+ # re-opens file, can raise Errno::ENOENT
98
+ def reload!(mode)
99
+ if @file && !@file.closed?
100
+ @file.close
101
+ end
102
+ @file = File.open(@name, mode)
103
+ end
104
+
105
+ private
106
+
107
+ def file
108
+ @file ||= File.open(@name, @mode)
109
+ end
110
+
111
+ def check_can_write!
112
+ if @mode == 'r'
113
+ raise "For writing to the log file, you must construct it with a different :mode"
114
+ end
115
+ end
116
+
117
+ # @raises LogFile::LoadError
118
+ # @return Array
119
+ def load_buf_from_file!(pos, bytes_to_read)
120
+ file.pos = pos
121
+ buf = file.read(bytes_to_read)
122
+ if buf.end_with?(',', ']]')
123
+ buf = buf[0..-2]
124
+ end
125
+ JSON.load(buf)
126
+ rescue JSON::ParserError => e
127
+ raise LogFile::LoadError, e.message
128
+ end
129
+
130
+ end
131
+
132
+ class LogFileIndex
133
+ attr_reader :name
134
+
135
+ def initialize(options = {})
136
+ @name = options[:filename] || File.join(Dir.pwd, 'run_log_index.json')
137
+ @index = []
138
+ end
139
+
140
+ def add_index(offset, bytes_written)
141
+ @index << [offset, bytes_written]
142
+ end
143
+
144
+ def finish!
145
+ if @index.size > 0
146
+ file.write(JSON.dump(@index))
147
+ file.close
148
+ end
149
+ end
150
+
151
+ def reload!(mode)
152
+ if @file && !@file.closed?
153
+ @file.close
154
+ end
155
+ @file = File.open(@name, mode)
156
+ end
157
+
158
+ private
159
+
160
+ def file
161
+ @file ||= File.open(@name, 'w')
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,159 @@
1
+ require 'rake/testtask'
2
+
3
+ module Covet
4
+ module TestRunners
5
+ module Minitest
6
+ @covet_hooked = false
7
+ @create_collection_file_on_exit = true
8
+ class << self
9
+ attr_accessor :create_collection_file_on_exit
10
+ end
11
+
12
+ def self.hook_into_test_methods!
13
+ if @covet_hooked
14
+ warn "Warning - Covet.register_coverage_collection! called multiple times"
15
+ return
16
+ end
17
+ gem 'minitest'
18
+ require 'minitest'
19
+
20
+ ::Minitest.after_run do
21
+ after_t = Time.now
22
+ diff_t = after_t - ::Minitest::Runnable.covet_start_time
23
+ time_taken = sprintf("%.2f", diff_t)
24
+ Covet.log_collection << ['stats', {
25
+ :time_taken => time_taken,
26
+ :files_filtered => CollectionFilter.files_filtered,
27
+ }]
28
+ if Covet::TestRunners::Minitest.create_collection_file_on_exit
29
+ Covet.log_collection.finish!
30
+ else
31
+ $stderr.puts "Covet: skipped writing to collection file"
32
+ end
33
+ end
34
+
35
+ ::Minitest::Runnable.class_eval do
36
+ @@covet_run_num = 0
37
+ @@covet_skips = 0
38
+ @@covet_failures = 0
39
+ @@covet_start_time = nil
40
+
41
+ class << self
42
+ def covet_start_time
43
+ @@covet_start_time
44
+ end
45
+
46
+ alias :covet_old_run_one_method :run_one_method
47
+
48
+ def run_one_method(klass, method_name, reporter)
49
+ # first run, collect coverage 'base' coverage information
50
+ # (coverage information for before any tests get run).
51
+ if @@covet_run_num == 0
52
+ @@covet_start_time = Time.now
53
+ base_coverage = CovetCoverage.peek_result
54
+ base_coverage = Covet.normalize_coverage_info(base_coverage)
55
+ if base_coverage.empty?
56
+ warn "Warning - covet is not properly set up, as it must be required " \
57
+ "before other libraries to work correctly.\nTry adding\n require 'covet'\n" \
58
+ "to the top of your test helper file."
59
+ end
60
+ Covet::BASE_COVERAGE.update base_coverage
61
+ # TODO: save Random::DEFAULT.seed in run log file if Covet.options[:test_order] == :random_seeded,
62
+ # then we can run the methods in the same order as before.
63
+ Covet.log_collection << ['base', base_coverage, {
64
+ :version => Covet::VERSION,
65
+ :options => Covet.options,
66
+ :seed => Random::DEFAULT.seed,
67
+ }]
68
+ end
69
+
70
+ @@covet_run_num += 1
71
+ file = nil
72
+ begin
73
+ file = klass.instance_method(method_name).source_location[0]
74
+ rescue
75
+ warn "\nWarning - Skipping collecting test coverage for method #{klass}##{method_name}\n"
76
+ return
77
+ end
78
+
79
+ before = CovetCoverage.peek_result
80
+
81
+ # Run test method
82
+ before_t = Time.now
83
+ result = covet_old_run_one_method(klass, method_name, reporter)
84
+ after_t = Time.now
85
+
86
+ summary_reporter = result.first
87
+ skips = summary_reporter.results.select(&:skipped?).size
88
+
89
+ # test was skipped, don't record coverage info
90
+ if @@covet_skips != skips
91
+ @@covet_skips = skips
92
+ @@covet_failures += 1
93
+ return result
94
+ end
95
+
96
+ # test failed, don't record coverage info
97
+ failures = summary_reporter.results.select(&:failures).size
98
+ if @@covet_failures != failures
99
+ @@covet_failures = failures
100
+ return result
101
+ end
102
+
103
+ after = CovetCoverage.peek_result
104
+
105
+ before_orig = Covet.normalize_coverage_info(before)
106
+ if Covet::BASE_COVERAGE.any?
107
+ before = Covet.diff_coverages(Covet::BASE_COVERAGE, before_orig)
108
+ end
109
+
110
+ after_orig = Covet.normalize_coverage_info(after)
111
+ after = Covet.diff_coverages(before_orig, after_orig)
112
+ if @@covet_run_num > 1
113
+ if [:random_seeded, :ordered].include?(Covet.options[:test_order])
114
+ Covet::BASE_COVERAGE.update(after_orig)
115
+ end
116
+ end
117
+
118
+ if after == before
119
+ after = nil
120
+ end
121
+ Covet.log_collection << ["#{file}##{method_name}", after, {
122
+ :time => sprintf("%.2f", after_t - before_t),
123
+ }]
124
+ result
125
+ # NOTE: if the interrupt is fired outside of `Minitest.run_one_method`, then the
126
+ # collection file gets logged even on interrupt :(
127
+ rescue Interrupt
128
+ Covet::TestRunners::Minitest.create_collection_file_on_exit = false
129
+ raise
130
+ end
131
+
132
+ end
133
+ end
134
+ @covet_hooked = true
135
+ end
136
+
137
+ def self.cmdline_for_run_list(run_list)
138
+ files = run_list.map { |double| double[1].split('#').first }
139
+ files.uniq!
140
+
141
+ files_str = files.map { |fname| %Q("#{fname}") }.join(' ')
142
+ rake_testtask = Rake::TestTask.new
143
+ rake_loader_str = rake_testtask.rake_loader
144
+ rake_include_arg = %Q(-I"#{rake_testtask.rake_lib_dir}")
145
+
146
+ cmd = %Q(ruby -I"test" -I"lib" #{rake_include_arg} "#{rake_loader_str}" ) +
147
+ files_str
148
+
149
+ unless Covet.options[:disable_test_method_filter]
150
+ test_methods = run_list.map { |double| double[1].split('#').last }
151
+ test_methods.uniq!
152
+ test_methods_regex = Regexp.union(test_methods)
153
+ cmd << %Q( "-n #{test_methods_regex.inspect}")
154
+ end
155
+ cmd
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,92 @@
1
+ module Covet
2
+ module TestRunners
3
+ module Rspec
4
+ @hooked = false
5
+ @run_num = 0
6
+ @skips = 0
7
+ @failures = 0
8
+
9
+ def self.hook_into_test_methods!
10
+ if @hooked
11
+ warn "Warning - Covet.register_coverage_collection! called multiple times"
12
+ return
13
+ end
14
+ gem 'rspec'
15
+ require 'rspec'
16
+
17
+ ::RSpec.configuration.after(:suite) do
18
+ Covet.log_collection.finish!
19
+ end
20
+
21
+ run_num = @run_num
22
+ skips = @skips
23
+ failures = @failures
24
+
25
+ ::RSpec.configuration.around(:each) do |test|
26
+ if run_num == 0
27
+ base_coverage = CovetCoverage.peek_result
28
+ base_coverage = Covet.normalize_coverage_info(base_coverage)
29
+ if base_coverage.empty?
30
+ warn "Warning - covet is not properly set up, as it must be required " \
31
+ "before other libraries to properly work.\nIf it isn't already, try " \
32
+ "adding\n require 'covet'\nas the first thing in your test helper file."
33
+ end
34
+ Covet::BASE_COVERAGE.update base_coverage
35
+ # TODO: save Random::DEFAULT.seed in run log file if Covet.options[:test_order] == :random_seeded,
36
+ # then we can run the methods in the same order as before.
37
+ Covet.log_collection << ['base', base_coverage]
38
+ end
39
+
40
+ run_num += 1
41
+ before = CovetCoverage.peek_result
42
+ test.call
43
+ rspec_metadata = test.metadata
44
+
45
+ # TODO: figure out if failed or skipped, and break out of block if it did
46
+ #
47
+ # XXX: is this right for all recent versions of rake?
48
+ file = rspec_metadata[:file_path].sub(%r{\A\./}, '') # remove leading './'
49
+ line = rspec_metadata[:line_number]
50
+
51
+ after = CovetCoverage.peek_result
52
+
53
+ before_orig = Covet.normalize_coverage_info(before)
54
+ if Covet::BASE_COVERAGE.any?
55
+ before = Covet.diff_coverages(Covet::BASE_COVERAGE, before_orig)
56
+ end
57
+ after_orig = Covet.normalize_coverage_info(after)
58
+ after = Covet.diff_coverages(before, after_orig)
59
+ if run_num > 1
60
+ if [:random_seeded, :ordered].include?(Covet.options[:test_order])
61
+ Covet::BASE_COVERAGE.update(after_orig)
62
+ end
63
+ end
64
+
65
+ if after == before
66
+ after = nil
67
+ end
68
+ Covet.log_collection << ["#{file}:#{line}", after]
69
+ end
70
+ @hooked = true
71
+ end
72
+
73
+ def self.cmdline_for_run_list(run_list)
74
+ require 'rspec/core/rake_task'
75
+ files = run_list.map { |double| double[1] }
76
+ files.uniq!
77
+ file_list = files.join(' ')
78
+ old_spec_env = ENV['SPEC']
79
+ begin
80
+ ENV['SPEC'] = file_list
81
+ rspec_rake_task = RSpec::Core::RakeTask.new(:spec) do |t|
82
+ end
83
+ ensure
84
+ ENV['SPEC'] = old_spec_env
85
+ end
86
+ # XXX: is this right for all recent versions of rake?
87
+ # TODO: try to figure out how to get test method names
88
+ rspec_rake_task.send(:spec_command)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,62 @@
1
+ module Covet
2
+ module VCS
3
+ module Git
4
+ require 'set'
5
+ require 'rugged'
6
+
7
+ # Find lines in git-indexed files that were changed (added/deleted/modified)
8
+ # in the codebase since `revision`.
9
+ # @raise Rugged::Error, Rugged::InvalidError, TypeError
10
+ # @return Set<Array>
11
+ def self.changes_since(revision = :last_commit)
12
+ repo = Rugged::Repository.new(find_git_repo_path!) # raises if can't find git repo
13
+ lines_to_run = Set.new
14
+ diff = case revision.to_s
15
+ when 'last_commit', 'HEAD'
16
+ repo.index.diff
17
+ else
18
+ # raises Rugged::Error or TypeError if `revision` is invalid Git object id
19
+ # (tag name or sha1, etc.)
20
+ commit = Rugged::Commit.new(repo, revision)
21
+ repo.index.diff(commit, {}) # NOTE: for some reason, this call doesn't work properly if the second parameter isn't given. Bug in rugged?
22
+ end
23
+ diff.each_patch { |patch|
24
+ file = patch.delta.old_file[:path] # NOTE: old file is the index's version
25
+
26
+ patch.each_hunk { |hunk|
27
+ hunk.each_line { |line|
28
+ case line.line_origin
29
+ when :addition
30
+ lines_to_run << [file, line.new_lineno]
31
+ when :deletion
32
+ lines_to_run << [file, line.old_lineno]
33
+ when :context
34
+ lines_to_run << [file, line.new_lineno]
35
+ end
36
+ }
37
+ }
38
+ }
39
+ lines_to_run
40
+ end
41
+
42
+ # find git repository path at or below `Dir.pwd`
43
+ def self.find_git_repo_path!
44
+ dir = orig_dir = Dir.pwd
45
+ found = Dir.exist?('.git') && dir
46
+ while !found && dir && Dir.exist?(dir)
47
+ old_dir = Dir.pwd
48
+ Dir.chdir('..')
49
+ dir = Dir.pwd
50
+ return if old_dir == dir # at root directory
51
+ if dir && Dir.exist?('.git')
52
+ found = dir
53
+ end
54
+ end
55
+ found
56
+ ensure
57
+ Dir.chdir(orig_dir) if Dir.pwd != orig_dir
58
+ end
59
+
60
+ end
61
+ end
62
+ end