mutant 0.10.13 → 0.10.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 561ad5a2483b3678f3bbcd3ab1ddbef3259f589548b59f3f42e993b76e50e120
4
- data.tar.gz: dd3b79cbeb3bca13e502b9c43cbb59d2d3656e0f37905d5cbb33ac1e1ac98fb1
3
+ metadata.gz: 9150c0a23d648637691af94ce404b1f1d2317469ca669455b1419948b1c4e106
4
+ data.tar.gz: 6a100d36685ffe0fd66e7194d6f5bb075b9d9370450c7e585a5b34a31449ae48
5
5
  SHA512:
6
- metadata.gz: 8d314dfd39fba6e6becf5d3410df2b6c95f94a5861df6446cb804b058edcad4d8b4a6ed98a6a21f4cfed3af48a450a17a3579b9b39e6814a60c4f10d746f77f9
7
- data.tar.gz: 1ef113b288fca26803593efe136e2c8e49002eb6f33722373eaacf0276078ac6ac4624139c10636498cf2e9c5eec8c64512b08796833380f457e5f1497bcab61
6
+ metadata.gz: 95858317d816637debda988640307698dbf32670fc246c980b3d46243d198b546dae36138cb9793efd3575c44464d6a7d22b6ef3457752811fac2204120a5071
7
+ data.tar.gz: 6b6f0545d7fbdecd68c24e65fa75c386a1d95c1b06c557cb2d127e74119e7eca0e456922b1f2765e48c15f2dba3b9dc1498deff556844c9fd9eea52bcc20e71a
data/bin/mutant CHANGED
@@ -28,12 +28,14 @@ status =
28
28
  .call(Kernel),
29
29
  root_require: 'mutant',
30
30
  includes: %w[
31
- mutant
32
- unparser
33
31
  adamantium
34
- equalizer
35
32
  anima
36
33
  concord
34
+ equalizer
35
+ mprelude
36
+ mutant
37
+ unparser
38
+ variable
37
39
  ]
38
40
  )
39
41
 
@@ -171,6 +171,7 @@ require 'mutant/cli/command/subscription'
171
171
  require 'mutant/cli/command/environment'
172
172
  require 'mutant/cli/command/environment/run'
173
173
  require 'mutant/cli/command/environment/show'
174
+ require 'mutant/cli/command/environment/subject'
174
175
  require 'mutant/cli/command/root'
175
176
  require 'mutant/runner'
176
177
  require 'mutant/runner/sink'
@@ -245,4 +246,17 @@ module Mutant
245
246
  zombie: false
246
247
  )
247
248
  end # Config
249
+
250
+ # Traverse values against action
251
+ #
252
+ # Specialized to Either. Its *always* traverse.
253
+ def self.traverse(action, values)
254
+ Either::Right.new(
255
+ values.map do |value|
256
+ action.call(value).from_right do |error|
257
+ return Either::Left.new(error)
258
+ end
259
+ end
260
+ )
261
+ end
248
262
  end # Mutant
@@ -30,7 +30,7 @@ module Mutant
30
30
  # @return [Either<String, Env>]
31
31
  #
32
32
  # rubocop:disable Metrics/MethodLength
33
- def self.apply(world, config)
33
+ def self.call(world, config)
34
34
  env = Env
35
35
  .empty(world, config)
36
36
  .tap(&method(:infect))
@@ -109,7 +109,7 @@ module Mutant
109
109
  return
110
110
  end
111
111
 
112
- expression_parser.apply(name).from_right {}
112
+ expression_parser.call(name).from_right {}
113
113
  end
114
114
  private_class_method :expression
115
115
  # rubocop:enable Metrics/MethodLength
@@ -75,6 +75,18 @@ module Mutant
75
75
  self.class::SUBCOMMANDS
76
76
  end
77
77
 
78
+ def execute
79
+ action.either(
80
+ method(:fail_message),
81
+ ->(_) { true }
82
+ )
83
+ end
84
+
85
+ def fail_message(message)
86
+ world.stderr.puts(message)
87
+ false
88
+ end
89
+
78
90
  def parser
79
91
  OptionParser.new do |parser|
80
92
  parser.banner = "usage: #{banner}"
@@ -25,7 +25,7 @@ module Mutant
25
25
  def bootstrap
26
26
  Config.load_config_file(world)
27
27
  .fmap(&method(:expand))
28
- .bind { Bootstrap.apply(world, @config) }
28
+ .bind { Bootstrap.call(world, @config) }
29
29
  end
30
30
 
31
31
  def expand(file_config)
@@ -33,23 +33,13 @@ module Mutant
33
33
  end
34
34
 
35
35
  def parse_remaining_arguments(arguments)
36
- traverse(@config.expression_parser.public_method(:apply), arguments)
36
+ Mutant.traverse(@config.expression_parser, arguments)
37
37
  .fmap do |match_expressions|
38
38
  matcher(match_expressions: match_expressions)
39
39
  self
40
40
  end
41
41
  end
42
42
 
43
- def traverse(action, values)
44
- Either::Right.new(
45
- values.map do |value|
46
- action.call(value).from_right do |error|
47
- return Either::Left.new(error)
48
- end
49
- end
50
- )
51
- end
52
-
53
43
  def set(**attributes)
54
44
  @config = @config.with(attributes)
55
45
  end
@@ -92,10 +82,10 @@ module Mutant
92
82
  parser.separator('Matcher:')
93
83
 
94
84
  parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
95
- add_matcher(:ignore_expressions, @config.expression_parser.apply(pattern).from_right)
85
+ add_matcher(:ignore_expressions, @config.expression_parser.call(pattern).from_right)
96
86
  end
97
87
  parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
98
- add_matcher(:start_expressions, @config.expression_parser.apply(pattern).from_right)
88
+ add_matcher(:start_expressions, @config.expression_parser.call(pattern).from_right)
99
89
  end
100
90
  parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
101
91
  add_matcher(
@@ -25,12 +25,19 @@ module Mutant
25
25
 
26
26
  private
27
27
 
28
- def execute
29
- soft_fail(License.apply(world))
28
+ def action
29
+ soft_fail(License.call(world))
30
30
  .bind { bootstrap }
31
- .bind(&Runner.public_method(:apply))
32
- .from_right { |error| world.stderr.puts(error); return false }
33
- .success?
31
+ .bind(&Runner.public_method(:call))
32
+ .bind(&method(:from_result))
33
+ end
34
+
35
+ def from_result(result)
36
+ if result.success?
37
+ Either::Right.new(nil)
38
+ else
39
+ Either::Left.new('Uncovered mutations detected, exiting nonzero!')
40
+ end
34
41
  end
35
42
 
36
43
  def soft_fail(result)
@@ -11,12 +11,8 @@ module Mutant
11
11
 
12
12
  private
13
13
 
14
- def execute
15
- Config.load_config_file(world)
16
- .fmap(&method(:expand))
17
- .bind { Bootstrap.apply(world, @config) }
18
- .fmap(&method(:report_env))
19
- .right?
14
+ def action
15
+ bootstrap.fmap(&method(:report_env))
20
16
  end
21
17
 
22
18
  def report_env(env)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module CLI
5
+ class Command
6
+ class Environment
7
+ class Subject < self
8
+ NAME = 'subject'
9
+ SHORT_DESCRIPTION = 'Subject subcommands'
10
+
11
+ class List < self
12
+ NAME = 'list'
13
+ SHORT_DESCRIPTION = 'List subjects'
14
+ SUBCOMMANDS = EMPTY_ARRAY
15
+
16
+ private
17
+
18
+ def action
19
+ bootstrap.fmap(&method(:list_subjects))
20
+ end
21
+
22
+ def list_subjects(env)
23
+ print('Subjects in environment: %d' % env.subjects.length)
24
+ env.subjects.each do |subject|
25
+ print(subject.expression.syntax)
26
+ end
27
+ end
28
+
29
+ def print(message)
30
+ world.stdout.puts(message)
31
+ end
32
+ end
33
+
34
+ SUBCOMMANDS = [List].freeze
35
+ end # Subject
36
+ end # Environment
37
+ end # Command
38
+ end # CLI
39
+ end # Mutant
@@ -4,7 +4,7 @@ module Mutant
4
4
  module CLI
5
5
  class Command
6
6
  class Environment < self
7
- SUBCOMMANDS = [Environment::Show].freeze
7
+ SUBCOMMANDS = [Environment::Subject, Environment::Show].freeze
8
8
  end # Environment
9
9
 
10
10
  class Root < self
@@ -10,7 +10,7 @@ module Mutant
10
10
  private
11
11
 
12
12
  def license
13
- License.apply(world)
13
+ License.call(world)
14
14
  end
15
15
 
16
16
  class Test < self
@@ -55,9 +55,14 @@ module Mutant
55
55
  # otherwise
56
56
  def self.try_parse(input)
57
57
  match = self::REGEXP.match(input)
58
- return unless match
58
+ from_match(match) if match
59
+ end
60
+
61
+ def self.from_match(match)
59
62
  names = anima.attribute_names
60
- new(Hash[names.zip(names.map(&match.method(:[])))])
63
+ new(Hash[names.zip(names.map(&match.public_method(:[])))])
61
64
  end
65
+ private_class_method :from_match
66
+
62
67
  end # Expression
63
68
  end # Mutant
@@ -5,6 +5,8 @@ module Mutant
5
5
 
6
6
  # Explicit method expression
7
7
  class Method < self
8
+ extend AST::Sexp
9
+
8
10
  include Anima.new(
9
11
  :method_name,
10
12
  :scope_name,
@@ -18,10 +20,7 @@ module Mutant
18
20
  '#' => [Matcher::Methods::Instance]
19
21
  )
20
22
 
21
- METHOD_NAME_PATTERN = Regexp.union(
22
- /(?<method_name>[A-Za-z_][A-Za-z\d_]*[!?=]?)/,
23
- *AST::Types::OPERATOR_METHODS.map(&:to_s)
24
- ).freeze
23
+ METHOD_NAME_PATTERN = /(?<method_name>.+)/.freeze
25
24
 
26
25
  private_constant(*constants(false))
27
26
 
@@ -47,6 +46,30 @@ module Mutant
47
46
  Matcher::Filter.new(methods_matcher, ->(subject) { subject.expression.eql?(self) })
48
47
  end
49
48
 
49
+ def self.try_parse(input)
50
+ match = REGEXP.match(input) or return
51
+
52
+ from_match(match) if valid_method_name?(match[:method_name])
53
+ end
54
+
55
+ # Test if string is a valid Ruby method name
56
+ #
57
+ # Note that this crazyness is indeed the "correct" solution.
58
+ #
59
+ # See: https://github.com/whitequark/parser/issues/213
60
+ #
61
+ # @param [String]
62
+ #
63
+ # @return [Boolean]
64
+ def self.valid_method_name?(name)
65
+ buffer = ::Parser::Source::Buffer.new(nil, source: "def #{name}; end")
66
+
67
+ ::Parser::CurrentRuby
68
+ .new
69
+ .parse(buffer).eql?(s(:def, name.to_sym, s(:args), nil))
70
+ end
71
+ private_class_method :valid_method_name?
72
+
50
73
  private
51
74
 
52
75
  def scope
@@ -67,7 +67,13 @@ module Mutant
67
67
  #
68
68
  # @return [Matcher]
69
69
  def matcher
70
- Matcher::Scope.new(Object.const_get(scope_name))
70
+ scope = find_scope
71
+
72
+ if scope
73
+ Matcher::Scope.new(scope)
74
+ else
75
+ Matcher::Null.new
76
+ end
71
77
  end
72
78
 
73
79
  # Syntax for expression
@@ -76,6 +82,13 @@ module Mutant
76
82
  alias_method :syntax, :scope_name
77
83
  public :syntax
78
84
 
85
+ private
86
+
87
+ def find_scope
88
+ Object.const_get(scope_name)
89
+ rescue NameError # rubocop:disable Lint/SuppressedException
90
+ end
91
+
79
92
  end # Exact
80
93
  end # Namespace
81
94
  end # Expression
@@ -5,7 +5,7 @@ module Mutant
5
5
  class Parser
6
6
  include Concord.new(:types)
7
7
 
8
- # Apply expression parsing
8
+ # Parse expression
9
9
  #
10
10
  # @param [String] input
11
11
  #
@@ -14,7 +14,7 @@ module Mutant
14
14
  #
15
15
  # @return [nil]
16
16
  # otherwise
17
- def apply(input)
17
+ def call(input)
18
18
  expressions = expressions(input)
19
19
  case expressions.length
20
20
  when 0
@@ -64,6 +64,7 @@ module Mutant
64
64
  end
65
65
  end # Pipe
66
66
 
67
+ # rubocop:disable Metrics/ClassLength
67
68
  class Parent
68
69
  include(
69
70
  Anima.new(*ATTRIBUTES),
@@ -149,17 +150,26 @@ module Mutant
149
150
 
150
151
  break unless ready
151
152
 
152
- ready.each do |fd|
153
- if fd.eof?
154
- targets.delete(fd)
153
+ ready.each do |target|
154
+ if target.eof?
155
+ targets.delete(target)
155
156
  else
156
- targets.fetch(fd) << fd.read_nonblock(READ_SIZE)
157
+ read_fragment(target, targets.fetch(target))
157
158
  end
158
159
  end
159
160
  end
160
161
  end
161
162
  # rubocop:enable Metrics/MethodLength
162
163
 
164
+ def read_fragment(target, fragments)
165
+ loop do
166
+ result = target.read_nonblock(READ_SIZE, exception: false)
167
+ break unless result.instance_of?(String)
168
+ fragments << result
169
+ break if result.bytesize < READ_SIZE
170
+ end
171
+ end
172
+
163
173
  # rubocop:disable Metrics/MethodLength
164
174
  def terminate_graceful
165
175
  status = nil
@@ -199,6 +209,7 @@ module Mutant
199
209
  @result = defined?(@result) ? @result.add_error(result) : result
200
210
  end
201
211
  end # Parent
212
+ # rubocop:enable Metrics/ClassLength
202
213
 
203
214
  class Child
204
215
  include(
@@ -12,7 +12,7 @@ module Mutant
12
12
  # @return [Either<String,Subscription>]
13
13
  #
14
14
  # @api private
15
- def self.apply(world)
15
+ def self.call(world)
16
16
  load_mutant_license(world)
17
17
  .fmap { license_path(world) }
18
18
  .bind { |path| Subscription.load(world, world.json.load(path)) }
@@ -30,7 +30,7 @@ module Mutant
30
30
  'oss' => Opensource
31
31
  }.fetch(value.fetch('type'))
32
32
  .from_json(value.fetch('contents'))
33
- .apply(world)
33
+ .call(world)
34
34
  end
35
35
 
36
36
  # Subscription self description
@@ -15,7 +15,7 @@ module Mutant
15
15
  new(value.fetch('authors').map(&Author.public_method(:new)).to_set)
16
16
  end
17
17
 
18
- def apply(world)
18
+ def call(world)
19
19
  candidates = candidates(world)
20
20
 
21
21
  if (licensed & candidates).any?
@@ -50,7 +50,7 @@ module Mutant
50
50
  )
51
51
  end
52
52
 
53
- def apply(world)
53
+ def call(world)
54
54
  world
55
55
  .capture_stdout(%w[git remote --verbose])
56
56
  .fmap(&method(:parse_remotes))
@@ -9,34 +9,37 @@ module Mutant
9
9
  CODE_DELIMITER = "\0"
10
10
  CODE_RANGE = (0..4).freeze
11
11
 
12
- def initialize(subject, node)
13
- super(subject, node)
14
-
15
- @source = Unparser.unparse(node)
16
- @code = sha1[CODE_RANGE]
17
- @identification = "#{self.class::SYMBOL}:#{subject.identification}:#{code}"
18
- @monkeypatch = Unparser.unparse(subject.context.root(node))
19
- end
20
-
21
12
  # Mutation identification code
22
13
  #
23
14
  # @return [String]
24
- attr_reader :code
15
+ def code
16
+ sha1[CODE_RANGE]
17
+ end
18
+ memoize :code
25
19
 
26
20
  # Normalized mutation source
27
21
  #
28
22
  # @return [String]
29
- attr_reader :source
23
+ def source
24
+ Unparser.unparse(node)
25
+ end
26
+ memoize :source
30
27
 
31
28
  # Identification string
32
29
  #
33
30
  # @return [String]
34
- attr_reader :identification
31
+ def identification
32
+ "#{self.class::SYMBOL}:#{subject.identification}:#{code}"
33
+ end
34
+ memoize :identification
35
35
 
36
36
  # The monkeypatch to insert the mutation
37
37
  #
38
38
  # @return [String]
39
- attr_reader :monkeypatch
39
+ def monkeypatch
40
+ Unparser.unparse(subject.context.root(node))
41
+ end
42
+ memoize :monkeypatch
40
43
 
41
44
  # Normalized original source
42
45
  #
@@ -23,39 +23,55 @@ module Mutant
23
23
  # when git command failed
24
24
  def touches?(path, line_range)
25
25
  touched_paths
26
+ .from_right { |message| fail Error, message }
26
27
  .fetch(path) { return false }
27
28
  .touches?(line_range)
28
29
  end
29
30
 
30
31
  private
31
32
 
32
- # rubocop:disable Metrics/MethodLength
33
+ def repository_root
34
+ world
35
+ .capture_stdout(%w[git rev-parse --show-toplevel])
36
+ .fmap(&:chomp)
37
+ .fmap(&world.pathname.public_method(:new))
38
+ end
39
+
33
40
  def touched_paths
34
- pathname = world.pathname
35
- work_dir = pathname.pwd
41
+ repository_root.bind(&method(:diff_index))
42
+ end
43
+ memoize :touched_paths
36
44
 
45
+ def diff_index(root)
37
46
  world
38
47
  .capture_stdout(%W[git diff-index #{to}])
39
- .from_right
40
- .lines
41
- .map do |line|
42
- path = parse_line(work_dir, line)
43
- [path.path, path]
48
+ .fmap(&:lines)
49
+ .bind do |lines|
50
+ Mutant
51
+ .traverse(->(line) { parse_line(root, line) }, lines)
52
+ .fmap do |paths|
53
+ paths.map { |path| [path.path, path] }.to_h
54
+ end
44
55
  end
45
- .to_h
46
56
  end
47
- memoize :touched_paths
48
- # rubocop:enable Metrics/MethodLength
49
-
50
- def parse_line(work_dir, line)
51
- match = FORMAT.match(line) or fail Error, "Invalid git diff-index line: #{line}"
52
57
 
53
- Path.new(
54
- path: work_dir.join(match.captures.first),
55
- to: to,
56
- world: world
57
- )
58
+ # rubocop:disable Metrics/MethodLength
59
+ def parse_line(root, line)
60
+ match = FORMAT.match(line)
61
+
62
+ if match
63
+ Either::Right.new(
64
+ Path.new(
65
+ path: root.join(match.captures.first),
66
+ to: to,
67
+ world: world
68
+ )
69
+ )
70
+ else
71
+ Either::Left.new("Invalid git diff-index line: #{line}")
72
+ end
58
73
  end
74
+ # rubocop:enable Metrics/MethodLength
59
75
 
60
76
  # Path touched by a diff
61
77
  class Path
@@ -276,7 +276,7 @@ module Mutant
276
276
  def process_abort?
277
277
  process_status = isolation_result.process_status or return false
278
278
 
279
- !timeout? && !process_status.exited?
279
+ !timeout? && !process_status.success?
280
280
  end
281
281
 
282
282
  private
@@ -6,7 +6,7 @@ module Mutant
6
6
  # Run against env
7
7
  #
8
8
  # @return [Either<String, Result>]
9
- def self.apply(env)
9
+ def self.call(env)
10
10
  reporter(env).start(env)
11
11
 
12
12
  Either::Right.new(run_mutation_analysis(env))
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mutant
4
4
  # Current mutant version
5
- VERSION = '0.10.13'
5
+ VERSION = '0.10.18'
6
6
  end # Mutant
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.13
4
+ version: 0.10.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Schirp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-03 00:00:00.000000000 Z
11
+ date: 2020-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: abstract_type
@@ -307,6 +307,7 @@ files:
307
307
  - lib/mutant/cli/command/environment.rb
308
308
  - lib/mutant/cli/command/environment/run.rb
309
309
  - lib/mutant/cli/command/environment/show.rb
310
+ - lib/mutant/cli/command/environment/subject.rb
310
311
  - lib/mutant/cli/command/root.rb
311
312
  - lib/mutant/cli/command/subscription.rb
312
313
  - lib/mutant/config.rb