mutator_rails 0.1.8

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 (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