mutant 0.9.13 → 0.10.5

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.
@@ -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,161 @@
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 initialize(attributes)
37
+ super(attributes)
38
+ @config = Config::DEFAULT
39
+ end
40
+
41
+ def execute
42
+ soft_fail(License.apply(world))
43
+ .bind { Config.load_config_file(world) }
44
+ .fmap(&method(:expand))
45
+ .bind { Bootstrap.apply(world, @config) }
46
+ .bind(&Runner.public_method(:apply))
47
+ .from_right { |error| world.stderr.puts(error); return false }
48
+ .success?
49
+ end
50
+
51
+ def expand(file_config)
52
+ @config = Config.env.merge(file_config).merge(@config)
53
+ end
54
+
55
+ def soft_fail(result)
56
+ result.either(
57
+ lambda do |message|
58
+ stderr = world.stderr
59
+ stderr.puts(message)
60
+ UNLICENSED.each { |line| stderr.puts(unlicensed(line)) }
61
+ world.kernel.sleep(SLEEP)
62
+ Either::Right.new(nil)
63
+ end,
64
+ ->(_subscription) { Either::Right.new(nil) }
65
+ )
66
+ end
67
+
68
+ def unlicensed(message)
69
+ "[Mutant-License-Error]: #{message}"
70
+ end
71
+
72
+ def parse_remaining_arguments(arguments)
73
+ traverse(@config.expression_parser.public_method(:apply), arguments)
74
+ .fmap do |match_expressions|
75
+ matcher(match_expressions: match_expressions)
76
+ self
77
+ end
78
+ end
79
+
80
+ def traverse(action, values)
81
+ Either::Right.new(
82
+ values.map do |value|
83
+ action.call(value).from_right do |error|
84
+ return Either::Left.new(error)
85
+ end
86
+ end
87
+ )
88
+ end
89
+
90
+ def set(**attributes)
91
+ @config = @config.with(attributes)
92
+ end
93
+
94
+ def matcher(**attributes)
95
+ set(matcher: @config.matcher.with(attributes))
96
+ end
97
+
98
+ def add(attribute, value)
99
+ set(attribute => @config.public_send(attribute) + [value])
100
+ end
101
+
102
+ def add_matcher(attribute, value)
103
+ set(matcher: @config.matcher.add(attribute, value))
104
+ end
105
+
106
+ def add_environment_options(parser)
107
+ parser.separator('Environment:')
108
+ parser.on('--zombie', 'Run mutant zombified') do
109
+ set(zombie: true)
110
+ end
111
+ parser.on('-I', '--include DIRECTORY', 'Add DIRECTORY to $LOAD_PATH') do |directory|
112
+ add(:includes, directory)
113
+ end
114
+ parser.on('-r', '--require NAME', 'Require file with NAME') do |name|
115
+ add(:requires, name)
116
+ end
117
+ end
118
+
119
+ def add_integration_options(parser)
120
+ parser.separator('Integration:')
121
+
122
+ parser.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations') do |name|
123
+ set(integration: name)
124
+ end
125
+ end
126
+
127
+ # rubocop:disable Metrics/MethodLength
128
+ def add_matcher_options(parser)
129
+ parser.separator('Matcher:')
130
+
131
+ parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
132
+ add_matcher(:ignore_expressions, @config.expression_parser.apply(pattern).from_right)
133
+ end
134
+ parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
135
+ add_matcher(:start_expressions, @config.expression_parser.apply(pattern).from_right)
136
+ end
137
+ parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
138
+ add_matcher(
139
+ :subject_filters,
140
+ Repository::SubjectFilter.new(
141
+ Repository::Diff.new(to: revision, world: world)
142
+ )
143
+ )
144
+ end
145
+ end
146
+
147
+ def add_runner_options(parser)
148
+ parser.separator('Runner:')
149
+
150
+ parser.on('--fail-fast', 'Fail fast') do
151
+ set(fail_fast: true)
152
+ end
153
+ parser.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
154
+ set(jobs: Integer(number))
155
+ end
156
+ end
157
+ end # Run
158
+ # rubocop:enable Metrics/ClassLength
159
+ end # Command
160
+ end # CLI
161
+ 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
@@ -1,55 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mutant
4
- # The outer world IO objects mutant does interact with
5
- class World
6
- include Adamantium::Flat, Anima.new(
7
- :condition_variable,
8
- :gem,
9
- :gem_method,
10
- :io,
11
- :json,
12
- :kernel,
13
- :load_path,
14
- :marshal,
15
- :mutex,
16
- :object_space,
17
- :open3,
18
- :pathname,
19
- :process,
20
- :stderr,
21
- :stdout,
22
- :thread,
23
- :warnings
24
- )
25
-
26
- INSPECT = '#<Mutant::World>'
27
-
28
- private_constant(*constants(false))
29
-
30
- # Object inspection
31
- #
32
- # @return [String]
33
- def inspect
34
- INSPECT
35
- end
36
-
37
- # Capture stdout of a command
38
- #
39
- # @param [Array<String>] command
40
- #
41
- # @return [Either<String,String>]
42
- def capture_stdout(command)
43
- stdout, status = open3.capture2(*command, binmode: true)
44
-
45
- if status.success?
46
- Either::Right.new(stdout)
47
- else
48
- Either::Left.new("Command #{command} failed!")
49
- end
50
- end
51
- end # World
52
-
53
4
  # Standalone configuration of a mutant execution.
54
5
  #
55
6
  # Does not reference any "external" volatile state. The configuration applied
@@ -106,6 +57,23 @@ module Mutant
106
57
  mutant.yml
107
58
  ].freeze
108
59
 
60
+ # Merge with other config
61
+ #
62
+ # @param [Config] other
63
+ #
64
+ # @return [Config]
65
+ def merge(other)
66
+ other.with(
67
+ fail_fast: fail_fast || other.fail_fast,
68
+ includes: includes + other.includes,
69
+ jobs: other.jobs || jobs,
70
+ integration: other.integration || integration,
71
+ matcher: matcher.merge(other.matcher),
72
+ requires: requires + other.requires,
73
+ zombie: zombie || other.zombie
74
+ )
75
+ end
76
+
109
77
  private_constant(*constants(false))
110
78
 
111
79
  # Load config file
@@ -114,11 +82,12 @@ module Mutant
114
82
  # @param [Config] config
115
83
  #
116
84
  # @return [Either<String,Config>]
117
- def self.load_config_file(world, config)
118
- files = CANDIDATES.map(&world.pathname.method(:new)).select(&:readable?)
85
+ def self.load_config_file(world)
86
+ config = DEFAULT
87
+ files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)
119
88
 
120
89
  if files.one?
121
- load_contents(files.first).fmap(&config.method(:with))
90
+ load_contents(files.first).fmap(&config.public_method(:with))
122
91
  elsif files.empty?
123
92
  Either::Right.new(config)
124
93
  else
@@ -133,5 +102,12 @@ module Mutant
133
102
  .lmap(&:compact_message)
134
103
  end
135
104
  private_class_method :load_contents
105
+
106
+ # The configuration from the environment
107
+ #
108
+ # @return [Config]
109
+ def self.env
110
+ DEFAULT.with(jobs: Etc.nprocessors)
111
+ end
136
112
  end # Config
137
113
  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