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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +37 -0
- data/.codeclimate.yml +20 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +232 -0
- data/LICENSE.md +21 -0
- data/README.md +31 -0
- data/Rakefile +8 -0
- data/app/models/test.rb +7 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/mutator_rails.yml +8 -0
- data/defaults.reek +131 -0
- data/lib/mutator_rails.rb +33 -0
- data/lib/mutator_rails/analyze.rb +27 -0
- data/lib/mutator_rails/cleanup.rb +60 -0
- data/lib/mutator_rails/config.rb +34 -0
- data/lib/mutator_rails/full_mutate.rb +39 -0
- data/lib/mutator_rails/guide.rb +69 -0
- data/lib/mutator_rails/list_maker.rb +25 -0
- data/lib/mutator_rails/mutation_log.rb +112 -0
- data/lib/mutator_rails/railtie.rb +18 -0
- data/lib/mutator_rails/single_mutate.rb +134 -0
- data/lib/mutator_rails/statistics.rb +192 -0
- data/lib/mutator_rails/version.rb +5 -0
- data/lib/tasks/mutator/analyze.rake +13 -0
- data/lib/tasks/mutator/cleanup.rake +13 -0
- data/lib/tasks/mutator/mutate_files.rake +12 -0
- data/lib/tasks/mutator/mutator.rake +17 -0
- data/lib/tasks/mutator/statistics.rake +13 -0
- data/log/mutant/analysis.tsv +3 -0
- data/log/mutant/guide.txt +1 -0
- data/log/mutant/models/test.log +92 -0
- data/log/mutant/models/test2.log +92 -0
- data/log/mutant/statistics.txt +19 -0
- data/mutator_rails.gemspec +37 -0
- data/spec/models/test_spec.rb +15 -0
- data/spec/mutator_rails/analyze_spec.rb +26 -0
- data/spec/mutator_rails/cleanup_spec.rb +13 -0
- data/spec/mutator_rails/full_mutate_spec.rb +13 -0
- data/spec/mutator_rails/guide_spec.rb +33 -0
- data/spec/mutator_rails/list_maker_spec.rb +17 -0
- data/spec/mutator_rails/mutation_log_spec.rb +36 -0
- data/spec/mutator_rails/single_mutate_spec.rb +111 -0
- data/spec/mutator_rails/statistics_spec.rb +48 -0
- data/spec/mutator_rails_spec.rb +9 -0
- data/spec/spec_helper.rb +49 -0
- 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
|