mutant 0.10.13 → 0.10.18

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