mutator_rails 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +37 -0
  3. data/.codeclimate.yml +20 -0
  4. data/.gitignore +12 -0
  5. data/.rspec +2 -0
  6. data/.ruby-version +1 -0
  7. data/CODE_OF_CONDUCT.md +46 -0
  8. data/Gemfile +7 -0
  9. data/Gemfile.lock +232 -0
  10. data/LICENSE.md +21 -0
  11. data/README.md +31 -0
  12. data/Rakefile +8 -0
  13. data/app/models/test.rb +7 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +8 -0
  16. data/config/mutator_rails.yml +8 -0
  17. data/defaults.reek +131 -0
  18. data/lib/mutator_rails.rb +33 -0
  19. data/lib/mutator_rails/analyze.rb +27 -0
  20. data/lib/mutator_rails/cleanup.rb +60 -0
  21. data/lib/mutator_rails/config.rb +34 -0
  22. data/lib/mutator_rails/full_mutate.rb +39 -0
  23. data/lib/mutator_rails/guide.rb +69 -0
  24. data/lib/mutator_rails/list_maker.rb +25 -0
  25. data/lib/mutator_rails/mutation_log.rb +112 -0
  26. data/lib/mutator_rails/railtie.rb +18 -0
  27. data/lib/mutator_rails/single_mutate.rb +134 -0
  28. data/lib/mutator_rails/statistics.rb +192 -0
  29. data/lib/mutator_rails/version.rb +5 -0
  30. data/lib/tasks/mutator/analyze.rake +13 -0
  31. data/lib/tasks/mutator/cleanup.rake +13 -0
  32. data/lib/tasks/mutator/mutate_files.rake +12 -0
  33. data/lib/tasks/mutator/mutator.rake +17 -0
  34. data/lib/tasks/mutator/statistics.rake +13 -0
  35. data/log/mutant/analysis.tsv +3 -0
  36. data/log/mutant/guide.txt +1 -0
  37. data/log/mutant/models/test.log +92 -0
  38. data/log/mutant/models/test2.log +92 -0
  39. data/log/mutant/statistics.txt +19 -0
  40. data/mutator_rails.gemspec +37 -0
  41. data/spec/models/test_spec.rb +15 -0
  42. data/spec/mutator_rails/analyze_spec.rb +26 -0
  43. data/spec/mutator_rails/cleanup_spec.rb +13 -0
  44. data/spec/mutator_rails/full_mutate_spec.rb +13 -0
  45. data/spec/mutator_rails/guide_spec.rb +33 -0
  46. data/spec/mutator_rails/list_maker_spec.rb +17 -0
  47. data/spec/mutator_rails/mutation_log_spec.rb +36 -0
  48. data/spec/mutator_rails/single_mutate_spec.rb +111 -0
  49. data/spec/mutator_rails/statistics_spec.rb +48 -0
  50. data/spec/mutator_rails_spec.rb +9 -0
  51. data/spec/spec_helper.rb +49 -0
  52. metadata +306 -0
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MutatorRails
4
+ class MutationLog
5
+ include Adamantium::Flat
6
+ include Concord.new(:target_log)
7
+
8
+ HEADER = (['log',
9
+ 'kills',
10
+ 'alive',
11
+ 'total',
12
+ 'pct killed',
13
+ 'mutations per sec',
14
+ 'runtime'].join("\t") + "\n").freeze
15
+
16
+ def initialize(*)
17
+ super
18
+
19
+ @content = File.read(target_log)
20
+ end
21
+
22
+ def to_s
23
+ return '' unless complete?
24
+
25
+ [link, kills, alive, total, pct, mutations_per_sec, runtime].join("\t")
26
+ rescue
27
+ ''
28
+ end
29
+
30
+ def complete?
31
+ /^Subjects: / === content
32
+ end
33
+
34
+ def details
35
+ [klass, kills, alive, total, pct, mutations_per_sec, runtime, failure, j1]
36
+ rescue
37
+ []
38
+ end
39
+
40
+ def pct
41
+ return 100 unless total.positive?
42
+
43
+ ((100.0 * kills.to_f) / total).round(3)
44
+ end
45
+
46
+ def alive
47
+ content.match(/Alive:.+?(\d+)$/)[1].to_i rescue 0
48
+ end
49
+
50
+ def j1
51
+ content.match(/Jobs:.+?(\d+)$/)[1].to_i.eql(1) rescue false
52
+ end
53
+
54
+ def link
55
+ "=HYPERLINK(\"#{relative_path}\",\"#{klass}\")"
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :content
61
+
62
+ def relative_path
63
+ absolute_file_path.relative_path_from(csv_file)
64
+ end
65
+
66
+ def csv_file
67
+ Pathname(File.dirname(Pathname(csv))).realpath
68
+ end
69
+
70
+ def csv
71
+ MutatorRails::Config.configuration.analysis_csv
72
+ end
73
+
74
+ def klass
75
+ k = content.match(/match_expressions: \[(.+?)\]>$/)
76
+ k ? k[1] : ''
77
+ end
78
+
79
+ def failure
80
+ /Failures:/ === content
81
+ end
82
+
83
+ def absolute_file_path
84
+ Pathname(target_log).realpath
85
+ end
86
+
87
+ def kills
88
+ content.match(/Kills:.+?(\d+)$/)[1] rescue 0
89
+ end
90
+
91
+ def mutations_per_sec
92
+ return 0 unless runtime.positive?
93
+
94
+ (total.to_f / runtime).round(3)
95
+ end
96
+
97
+
98
+ def runtime
99
+ content.match(/Runtime:\s+?(.+)s/).captures.first.to_f
100
+ rescue
101
+ 0.0
102
+ end
103
+
104
+ def total
105
+ alive.to_i + kills.to_i
106
+ end
107
+
108
+ def <=>(other)
109
+ [pct, -alive, link] <=> [other.pct, -other.alive, other.link]
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mutator_rails'
4
+ require 'rails'
5
+
6
+ module MutatorRails
7
+ class Railtie < Rails::Railtie
8
+ railtie_name :mutator_rails
9
+
10
+ rake_tasks do
11
+ load 'tasks/mutator/mutator.rake'
12
+ load 'tasks/mutator/analyze.rake'
13
+ load 'tasks/mutator/statistics.rake'
14
+ load 'tasks/mutator/cleanup.rake'
15
+ load 'tasks/mutator/mutate_files.rake'
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module MutatorRails
6
+ class SingleMutate
7
+ include Concord.new(:guide, :file)
8
+
9
+ def call
10
+ parms = BASIC_PARMS.dup
11
+ parms << preface(path.basename) + base
12
+
13
+ parms << '1> ' + log.to_s
14
+ log_dir
15
+
16
+ cmd = first_run(parms)
17
+ rerun(cmd)
18
+ end
19
+
20
+ def log
21
+ if File.exist?(old_log)
22
+ # repair - this is one time only
23
+ guide.update(full_log, code_md5, spec_md5)
24
+ File.rename(old_log, full_log)
25
+ end
26
+
27
+ full_log
28
+ end
29
+
30
+ def full_log
31
+ log_location.to_s + '.log'
32
+ end
33
+
34
+ def old_log
35
+ "#{log_location}_#{code_md5}_#{spec_md5}_#{MUTANT_VERSION}.log"
36
+ end
37
+
38
+ def log_location
39
+ path.sub(APP_BASE, logroot).sub('.rb', '')
40
+ end
41
+
42
+ def log_dir
43
+ log_location.dirname.tap do |dir|
44
+ FileUtils.mkdir_p(dir)
45
+ end
46
+ end
47
+
48
+ def spec_md5
49
+ Digest::MD5.file(spec_file).hexdigest
50
+ end
51
+
52
+ def base
53
+ path.basename.to_s.sub(path.extname, '').camelize
54
+ end
55
+
56
+ def code_md5
57
+ Digest::MD5.file(path).hexdigest
58
+ end
59
+
60
+ def path
61
+ Pathname.new(file)
62
+ end
63
+
64
+ def rerun(cmd)
65
+ return unless File.exist?(log)
66
+
67
+ content = File.read(log)
68
+ return unless /Failures:/ === content
69
+
70
+ FileUtils.cp(log, '/tmp')
71
+ cmd2 = cmd.sub('--use', '-j1 --use')
72
+ puts log
73
+ puts "[#{Time.current.iso8601}] #{cmd2}"
74
+ `#{cmd2}` unless ENV['RACK_ENV'].eql?('test')
75
+ end
76
+
77
+ def first_run(parms)
78
+ cmd = spec_opt + COMMAND + parms.join(' ')
79
+
80
+ if changed? || !complete?(log) || failed?(log)
81
+ puts "[#{Time.current.iso8601}] #{cmd}"
82
+ `#{cmd}` unless ENV['RACK_ENV'].eql?('test')
83
+ guide.update(log, code_md5, spec_md5)
84
+ end
85
+
86
+ cmd
87
+ end
88
+
89
+ def spec_opt
90
+ "SPEC_OPTS=\"--pattern #{spec_file}\" "
91
+ end
92
+
93
+ def spec_file
94
+ file.sub(APP_BASE, 'spec/').sub('.rb', '_spec.rb')
95
+ end
96
+
97
+ def complete?(log)
98
+ content = File.read(log)
99
+ /^Subjects: / === content
100
+ end
101
+
102
+ def failed?(log)
103
+ content = File.read(log)
104
+ /Failures:/ === content
105
+ end
106
+
107
+ def log_correct?
108
+ guide.current?(log, code_md5, spec_md5)
109
+ end
110
+
111
+ def preface(base)
112
+ rest = file.sub(APP_BASE, '').sub(/(lib)\//, '').sub(base.to_s, '')
113
+ return '' if rest == ''
114
+
115
+ content = File.read(spec_file)
116
+ d = content.match(/RSpec.describe\s+([^ ,]+)/)
117
+ cs = d[1].split('::')
118
+ cs.pop
119
+ f = cs.join('::')
120
+ f += '::' if f.present?
121
+ f
122
+ end
123
+
124
+ private
125
+
126
+ def changed?
127
+ !log_correct?
128
+ end
129
+
130
+ def logroot
131
+ MutatorRails::Config.configuration.logroot
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mutator_rails/mutation_log'
4
+
5
+ module MutatorRails
6
+ class Statistics
7
+ include Procto.call
8
+
9
+ def call
10
+ @content = ListMaker.new.make_list.map(&:details)
11
+
12
+ @stats = []
13
+ total_mutations
14
+ fully_mutated
15
+ failures
16
+ fallback_to_j1
17
+ top_10_alive
18
+ top_10_longest
19
+ top_10_total_mutations
20
+
21
+ puts " ... storing #{stats_file}"
22
+ puts text
23
+ File.write(stats_file, text)
24
+ end
25
+
26
+ private
27
+
28
+ def fully_mutated
29
+ stats << ''
30
+ stats << "#{full_mutations} module(s) were fully mutated (#{fully_pct.round(1)}%)"
31
+ end
32
+
33
+ def failures
34
+ header = false
35
+ content.each do |detail|
36
+ failure = detail[7]
37
+ if failure
38
+ unless header
39
+ failure_header
40
+ header = true
41
+ end
42
+ stats << detail[0]
43
+ end
44
+ end
45
+ end
46
+
47
+ def failure_header
48
+ stats << ''
49
+ stats << "The following modules remain with failures (check log):"
50
+ end
51
+
52
+ def fallback_to_j1
53
+ header = false
54
+ content.each do |detail|
55
+ failure = detail[8]
56
+ if failure
57
+ unless header
58
+ j1_header
59
+ header = true
60
+ end
61
+ stats << detail[0]
62
+ end
63
+ end
64
+ end
65
+
66
+ def j1_header
67
+ stats << ''
68
+ stats << "The following modules fell back to non-parallel(-j1):"
69
+ end
70
+
71
+ def top_10_alive
72
+ stats << ''
73
+ stats << "The following modules had most alive mutations (top 10):"
74
+ content.sort_by { |d| -d[2].to_i }.take(10).each do |detail|
75
+ alive = detail[2]
76
+ if alive.positive?
77
+ stats << " . #{detail[0]} (#{alive})"
78
+ end
79
+ end
80
+ end
81
+
82
+ def top_10_longest
83
+ stats << ''
84
+ stats << "The following modules had longest mutation time (top 10):"
85
+ content.sort_by { |d| -d[6].to_i }.take(10).each do |detail|
86
+ time = detail[6]
87
+ if time&.positive?
88
+ stats << " . #{detail[0]} (#{humanize(time.to_i)})"
89
+ end
90
+ end
91
+ end
92
+
93
+ def top_10_total_mutations
94
+ stats << ''
95
+ stats << "The following modules had largest mutation count (top 10):"
96
+ content.sort_by { |d| -d[3].to_i }.take(10).each do |detail|
97
+ cnt = detail[3]
98
+ if cnt&.positive?
99
+ stats << " . #{detail[0]} (#{cnt})"
100
+ end
101
+ end
102
+ end
103
+
104
+ def text
105
+ stats.join("\n")
106
+ end
107
+
108
+ def total_mutations
109
+ stats << ''
110
+ stats << "#{content.size} module(s) were mutated in #{total_mutation_time}"
111
+ stats << "for a total of #{tot_mutations} mutations tested @ #{per_sec.round(2)}/sec average"
112
+ stats << "which left #{total_alive} mutations alive (#{alive_pct.round(1)}%)"
113
+ stats << "and #{total_kills} killed (#{killed_pct.round(1)}%)"
114
+ end
115
+
116
+ def full_mutations
117
+ tot = 0
118
+ content.each do |detail|
119
+ alive = detail[2]
120
+ tot += 1 if alive&.zero?
121
+ end
122
+ tot
123
+ end
124
+
125
+ def fully_pct
126
+ 100.0 * full_mutations / content.size
127
+ end
128
+
129
+ def total_alive
130
+ tot = 0
131
+ content.each do |detail|
132
+ alive = detail[2]
133
+ tot += alive.to_i
134
+ end
135
+ tot
136
+ end
137
+
138
+ def total_mutation_time
139
+ humanize(total_seconds.to_i)
140
+ end
141
+
142
+ def total_seconds
143
+ tot = 0.0
144
+ content.each do |detail|
145
+ runtime = detail[6]
146
+ tot += runtime if runtime
147
+ end
148
+ tot
149
+ end
150
+
151
+ def per_sec
152
+ tot_mutations.to_f / total_seconds
153
+ end
154
+
155
+ def humanize(secs)
156
+ [[60, :seconds], [60, :minutes], [24, :hours], [1000, :days]].map { |count, name|
157
+ if secs > 0
158
+ secs, n = secs.divmod(count)
159
+ "#{n.to_i} #{n.to_i.eql?(1) ? name.to_s.chop : name}"
160
+ end
161
+ }.compact.reverse.join(' ')
162
+ end
163
+
164
+
165
+ def tot_mutations
166
+ tot = 0
167
+ content.each do |detail|
168
+ total = detail[3]
169
+ tot += total.to_i
170
+ end
171
+ tot
172
+ end
173
+
174
+ def total_kills
175
+ tot_mutations - total_alive
176
+ end
177
+
178
+ def alive_pct
179
+ 100.0 * total_alive / tot_mutations
180
+ end
181
+
182
+ def killed_pct
183
+ 100.0 * total_kills / tot_mutations
184
+ end
185
+
186
+ def stats_file
187
+ MutatorRails::Config.configuration.statistics
188
+ end
189
+
190
+ attr_reader :stats, :content
191
+ end
192
+ end