mutant 0.10.14 → 0.10.19

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: 83c9b5421a7599da7222232827794b8ed437de3ef3c0044ed904dfb998a86285
4
- data.tar.gz: 5ad4553f547c8ffbda6ea025a42526696d721654a88b08d1a2a3b372220c2d33
3
+ metadata.gz: f4950653df0e159b7d178e4770ad8ecf03994f1d08c99e4ba3589d9d76131840
4
+ data.tar.gz: 143ea75408ac2d9e6fda64e44e4ee1172317a716e593150df323ef6005f04598
5
5
  SHA512:
6
- metadata.gz: 4563bb628a8848d55ba0caa2d53ca060f9976d138864811eae84692faa2ab03f3364afc8475a7d8e8ff51457be5033c5d49edb1e34bd90d9f25ae0a0f22ff9a3
7
- data.tar.gz: bdd237ce6278db78cebc94e665ef69192c4bb0ddf06ec8f0d1806e8bd9c13b7a2e3ce1eccbbf260c6b3c7974207c4557b0197ed6f3499aed86e340aa88df3dbb
6
+ metadata.gz: efc662d1906386fbbbf011cabebd4d88450169237680c64e52e2e2432dd22f0ae6dc6a0cecddebf0357932b4520be0ec5e4a3aa63cf0734b9d8f0e47d5b8d5b4
7
+ data.tar.gz: d0d84126e2bd05e9d6a8b4360821eb4473049f92ac7b7257286b92e48b8c9e1d428ba8bbb7f708e19590c065aa415b1928f58fd50ebeb6ac5decc568afb90b32
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
  #
@@ -13,10 +13,19 @@ module Mutant
13
13
  private
14
14
 
15
15
  def dispatch
16
- emit_singletons
17
- emit(N_TRUE)
16
+ emit(N_NIL)
17
+ emit_instance_variable_mutation
18
+ end
19
+
20
+ def emit_instance_variable_mutation
21
+ return unless n_ivar?(expression)
22
+
23
+ instance_variable_name = Mutant::Util.one(expression.children)
18
24
 
19
- emit_expression_mutations { |node| !n_self?(node) }
25
+ emit(
26
+ s(:send, nil, :instance_variable_defined?,
27
+ s(:sym, instance_variable_name))
28
+ )
20
29
  end
21
30
 
22
31
  end # Defined
@@ -4,12 +4,6 @@ module Mutant
4
4
  class Mutator
5
5
  class Node
6
6
  class ProcargZero < self
7
- MAP = {
8
- ::Parser::AST::Node => :emit_argument_node_mutations,
9
- Symbol => :emit_argument_symbol_mutations
10
- }.freeze
11
-
12
- private_constant(*constants(false))
13
7
 
14
8
  handle :procarg0
15
9
 
@@ -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
@@ -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.14'
5
+ VERSION = '0.10.19'
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.14
4
+ version: 0.10.19
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-04 00:00:00.000000000 Z
11
+ date: 2020-12-14 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