mutant 0.9.13 → 0.10.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of mutant might be problematic. Click here for more details.

@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module CLI
5
+ class Command
6
+ class Root < self
7
+ SUBCOMMANDS = [Run, Subscription].freeze
8
+ SHORT_DESCRIPTION = 'mutation testing engine main command'
9
+ NAME = 'mutant'
10
+ end
11
+ end # Command
12
+ end # CLI
13
+ end # Mutant
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module CLI
5
+ class Command
6
+ # rubocop:disable Metrics/ClassLength
7
+ class Run < self
8
+ NAME = 'run'
9
+ SHORT_DESCRIPTION = 'Run code analysis'
10
+
11
+ OPTIONS =
12
+ %i[
13
+ add_environment_options
14
+ add_runner_options
15
+ add_integration_options
16
+ add_matcher_options
17
+ ].freeze
18
+
19
+ SLEEP = 40
20
+
21
+ UNLICENSED = <<~MESSAGE.lines.freeze
22
+ Soft fail, continuing in #{SLEEP} seconds
23
+ Next major version will enforce the license
24
+ See https://github.com/mbj/mutant#licensing
25
+ MESSAGE
26
+
27
+ # Test if command needs to be executed in zombie environment
28
+ #
29
+ # @return [Bool]
30
+ def zombie?
31
+ config.zombie
32
+ end
33
+
34
+ private
35
+
36
+ def execute
37
+ soft_fail(License.apply(world))
38
+ .bind { Config.load_config_file(world, config) }
39
+ .bind { |cli_config| Bootstrap.apply(world, cli_config) }
40
+ .bind(&Runner.public_method(:apply))
41
+ .from_right { |error| world.stderr.puts(error); return false }
42
+ .success?
43
+ end
44
+
45
+ def soft_fail(result)
46
+ result.either(
47
+ lambda do |message|
48
+ stderr = world.stderr
49
+ stderr.puts(message)
50
+ UNLICENSED.each { |line| stderr.puts(unlicensed(line)) }
51
+ world.kernel.sleep(SLEEP)
52
+ Either::Right.new(nil)
53
+ end,
54
+ ->(_subscription) { Either::Right.new(nil) }
55
+ )
56
+ end
57
+
58
+ def unlicensed(message)
59
+ "[Mutant-License-Error]: #{message}"
60
+ end
61
+
62
+ def parse_remaining_arguments(arguments)
63
+ traverse(config.expression_parser.public_method(:apply), arguments)
64
+ .fmap do |match_expressions|
65
+ matcher(match_expressions: match_expressions)
66
+ self
67
+ end
68
+ end
69
+
70
+ def traverse(action, values)
71
+ Either::Right.new(
72
+ values.map do |value|
73
+ action.call(value).from_right do |error|
74
+ return Either::Left.new(error)
75
+ end
76
+ end
77
+ )
78
+ end
79
+
80
+ def set(**attributes)
81
+ @config = config.with(attributes)
82
+ end
83
+
84
+ def matcher(**attributes)
85
+ set(matcher: config.matcher.with(attributes))
86
+ end
87
+
88
+ def add(attribute, value)
89
+ set(attribute => config.public_send(attribute) + [value])
90
+ end
91
+
92
+ def add_matcher(attribute, value)
93
+ set(matcher: config.matcher.add(attribute, value))
94
+ end
95
+
96
+ def add_environment_options(parser)
97
+ parser.separator('Environment:')
98
+ parser.on('--zombie', 'Run mutant zombified') do
99
+ set(zombie: true)
100
+ end
101
+ parser.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory|
102
+ add(:includes, directory)
103
+ end
104
+ parser.on('-r', '--require NAME', 'Require file with NAME') do |name|
105
+ add(:requires, name)
106
+ end
107
+ end
108
+
109
+ def add_integration_options(parser)
110
+ parser.separator('Integration:')
111
+
112
+ parser.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations') do |name|
113
+ set(integration: name)
114
+ end
115
+ end
116
+
117
+ # rubocop:disable Metrics/MethodLength
118
+ def add_matcher_options(parser)
119
+ parser.separator('Matcher:')
120
+
121
+ parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
122
+ add_matcher(:ignore_expressions, config.expression_parser.apply(pattern).from_right)
123
+ end
124
+ parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
125
+ add_matcher(:start_expressions, config.expression_parser.apply(pattern).from_right)
126
+ end
127
+ parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
128
+ add_matcher(
129
+ :subject_filters,
130
+ Repository::SubjectFilter.new(
131
+ Repository::Diff.new(to: revision, world: world)
132
+ )
133
+ )
134
+ end
135
+ end
136
+
137
+ def add_runner_options(parser)
138
+ parser.separator('Runner:')
139
+
140
+ parser.on('--fail-fast', 'Fail fast') do
141
+ set(fail_fast: true)
142
+ end
143
+ parser.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
144
+ set(jobs: Integer(number))
145
+ end
146
+ end
147
+ end # Run
148
+ # rubocop:enable Metrics/ClassLength
149
+ end # Command
150
+ end # CLI
151
+ end # Mutant
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module CLI
5
+ class Command
6
+ class Subscription < self
7
+ NAME = 'subscription'
8
+ SHORT_DESCRIPTION = 'Subscription subcommands'
9
+
10
+ private
11
+
12
+ def license
13
+ License.apply(world)
14
+ end
15
+
16
+ class Test < self
17
+ NAME = 'test'
18
+ SUBCOMMANDS = [].freeze
19
+ SHORT_DESCRIPTION = 'Silently validates subscription, exits accordingly'
20
+
21
+ private
22
+
23
+ def execute
24
+ license.right?
25
+ end
26
+ end # Test
27
+
28
+ class Show < self
29
+ NAME = 'show'
30
+ SUBCOMMANDS = [].freeze
31
+ SHORT_DESCRIPTION = 'Show subscription status'
32
+
33
+ private
34
+
35
+ def execute
36
+ license.either(method(:unlicensed), method(:licensed))
37
+ end
38
+
39
+ def licensed(subscription)
40
+ world.stdout.puts(subscription.description)
41
+ true
42
+ end
43
+
44
+ def unlicensed(message)
45
+ world.stderr.puts(message)
46
+ false
47
+ end
48
+ end # Show
49
+
50
+ SUBCOMMANDS = [Show, Test].freeze
51
+ end # Subscription
52
+ end # Command
53
+ end # CLI
54
+ end # Mutant
@@ -59,6 +59,5 @@ module Mutant
59
59
  names = anima.attribute_names
60
60
  new(Hash[names.zip(names.map(&match.method(:[])))])
61
61
  end
62
-
63
62
  end # Expression
64
63
  end # Mutant
@@ -4,53 +4,27 @@ module Mutant
4
4
  module License
5
5
  NAME = 'mutant-license'
6
6
  VERSION = '~> 0.1.0'
7
- SLEEP = 40
8
-
9
- UNLICENSED =
10
- IceNine.deep_freeze(
11
- [
12
- "Soft fail, continuing in #{SLEEP} seconds",
13
- 'Next major version will enforce the license',
14
- 'See https://github.com/mbj/mutant#licensing'
15
- ]
16
- )
17
7
 
8
+ # Load license
9
+ #
10
+ # @param [World] world
11
+ #
12
+ # @return [Either<String,Subscription>]
13
+ #
14
+ # @api private
18
15
  def self.apply(world)
19
- soft_fail(world, license_result(world))
20
- end
21
-
22
- def self.license_result(world)
23
16
  load_mutant_license(world)
24
17
  .fmap { license_path(world) }
25
- .fmap { |path| Subscription.from_json(world.json.load(path)) }
26
- .bind { |sub| sub.apply(world) }
18
+ .bind { |path| Subscription.load(world, world.json.load(path)) }
27
19
  end
28
- private_class_method :license_result
29
-
30
- # ignore :reek:NestedIterators
31
- def self.soft_fail(world, result)
32
- result.lmap do |message|
33
- stderr = world.stderr
34
- stderr.puts(message)
35
- UNLICENSED.each { |line| stderr.puts(unlicensed(line)) }
36
- world.kernel.sleep(SLEEP)
37
- end
38
-
39
- Either::Right.new(true)
40
- end
41
- private_class_method :soft_fail
42
20
 
43
21
  def self.load_mutant_license(world)
44
22
  Either
45
23
  .wrap_error(LoadError) { world.gem_method.call(NAME, VERSION) }
46
24
  .lmap(&:message)
47
25
  .lmap(&method(:check_for_rubygems_mutant_license))
48
- .lmap(&method(:unlicensed))
49
- end
50
-
51
- def self.unlicensed(message)
52
- "[Mutant-License-Error]: #{message}"
53
26
  end
27
+ private_class_method :load_mutant_license
54
28
 
55
29
  def self.check_for_rubygems_mutant_license(message)
56
30
  if message.include?('already activated mutant-license-0.0.0')
@@ -3,8 +3,15 @@
3
3
  module Mutant
4
4
  module License
5
5
  class Subscription
6
+ include Concord.new(:licensed)
6
7
 
7
- MESSAGE_FORMAT = <<~'MESSAGE'
8
+ FORMAT = <<~'MESSAGE'
9
+ %<subscription_name>s subscription:
10
+ Licensed:
11
+ %<licensed>s
12
+ MESSAGE
13
+
14
+ FAILURE_FORMAT = <<~'MESSAGE'
8
15
  Can not validate %<subscription_name>s license.
9
16
  Licensed:
10
17
  %<expected>s
@@ -12,31 +19,46 @@ module Mutant
12
19
  %<actual>s
13
20
  MESSAGE
14
21
 
15
- def self.from_json(value)
22
+ # Load value into subscription
23
+ #
24
+ # @param [Object] value
25
+ #
26
+ # @return [Subscription]
27
+ def self.load(world, value)
16
28
  {
17
29
  'com' => Commercial,
18
30
  'oss' => Opensource
19
- }.fetch(value.fetch('type')).from_json(value.fetch('contents'))
31
+ }.fetch(value.fetch('type'))
32
+ .from_json(value.fetch('contents'))
33
+ .apply(world)
34
+ end
35
+
36
+ # Subscription self description
37
+ #
38
+ # @return [String]
39
+ def description
40
+ FORMAT % {
41
+ licensed: licensed.to_a.join("\n"),
42
+ subscription_name: subscription_name
43
+ }
20
44
  end
21
45
 
22
46
  private
23
47
 
24
48
  def failure(expected, actual)
25
- Either::Left.new(message(expected, actual))
49
+ Either::Left.new(failure_message(expected, actual))
26
50
  end
27
51
 
28
- # ignore :reek:UtilityFunction
29
52
  def success
30
- # masked by soft fail
31
- Either::Right.new(nil)
53
+ Either::Right.new(self)
32
54
  end
33
55
 
34
56
  def subscription_name
35
57
  self.class.name.split('::').last.downcase
36
58
  end
37
59
 
38
- def message(expected, actual)
39
- MESSAGE_FORMAT % {
60
+ def failure_message(expected, actual)
61
+ FAILURE_FORMAT % {
40
62
  actual: actual.any? ? actual.map(&:to_s).join("\n") : '[none]',
41
63
  expected: expected.map(&:to_s).join("\n"),
42
64
  subscription_name: subscription_name
@@ -4,8 +4,6 @@ module Mutant
4
4
  module License
5
5
  class Subscription
6
6
  class Commercial < self
7
- include Concord.new(:authors)
8
-
9
7
  class Author
10
8
  include Concord.new(:email)
11
9
 
@@ -20,10 +18,10 @@ module Mutant
20
18
  def apply(world)
21
19
  candidates = candidates(world)
22
20
 
23
- if (authors & candidates).any?
21
+ if (licensed & candidates).any?
24
22
  success
25
23
  else
26
- failure(authors, candidates)
24
+ failure(licensed, candidates)
27
25
  end
28
26
  end
29
27
 
@@ -4,8 +4,6 @@ module Mutant
4
4
  module License
5
5
  class Subscription
6
6
  class Opensource < self
7
- include Concord.new(:repositories)
8
-
9
7
  class Repository
10
8
  include Concord.new(:host, :path)
11
9
 
@@ -43,10 +41,13 @@ module Mutant
43
41
  private_class_method :parse_url
44
42
  end
45
43
 
46
- private_constant(*constants(false))
47
-
48
44
  def self.from_json(value)
49
- new(value.fetch('repositories').map(&Repository.method(:parse)))
45
+ new(
46
+ value
47
+ .fetch('repositories')
48
+ .map(&Repository.public_method(:parse))
49
+ .to_set
50
+ )
50
51
  end
51
52
 
52
53
  def apply(world)
@@ -59,10 +60,10 @@ module Mutant
59
60
  private
60
61
 
61
62
  def check_subscription(actual)
62
- if (repositories.to_set & actual).any?
63
+ if (licensed & actual).any?
63
64
  success
64
65
  else
65
- failure(repositories, actual)
66
+ failure(licensed, actual)
66
67
  end
67
68
  end
68
69
 
@@ -3,7 +3,19 @@
3
3
  module Mutant
4
4
  module Meta
5
5
  class Example
6
- include Adamantium, Anima.new(:file, :node, :types, :expected)
6
+ include Adamantium
7
+ include Anima.new(
8
+ :expected,
9
+ :file,
10
+ :lvars,
11
+ :node,
12
+ :original_source,
13
+ :types
14
+ )
15
+
16
+ class Expected
17
+ include Anima.new(:original_source, :node)
18
+ end
7
19
 
8
20
  # Verification instance for example
9
21
  #
@@ -13,13 +25,13 @@ module Mutant
13
25
  end
14
26
  memoize :verification
15
27
 
16
- # Normalized source
28
+ # Original source as generated by unparser
17
29
  #
18
30
  # @return [String]
19
- def source
31
+ def original_source_generated
20
32
  Unparser.unparse(node)
21
33
  end
22
- memoize :source
34
+ memoize :original_source_generated
23
35
 
24
36
  # Generated mutations on example source
25
37
  #