mutant 0.11.34 → 0.12.1

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: e98171c271934f978770ae00e52d0e3e801a4197fa4b5efb0a43f966ff5af54c
4
- data.tar.gz: 0d2d7823052db9d2a46efcc0fdd11808d95510dd58a77933288c063f7195851f
3
+ metadata.gz: 0e1cf4d744b27691dff3986c4af5dddd278e6eadcc348a9cd0d4b5e16fa22514
4
+ data.tar.gz: 77913d65e176d5150b17ca03ec5b05398dee759dfea2ad89dd438eeaf72e34f8
5
5
  SHA512:
6
- metadata.gz: ba1991d33cb05098cb78bc5325d05c8b4dbe974752a276a5daf43139c1e1202bb5ce5505b2ce1072d8d86ca4ca6ca22a828d971589b1c22b13fd012703cf7ebf
7
- data.tar.gz: 8e3f0a5ce588b5b49c08233bceb8c855c6b32ce7cbb4fb1a1056e981775e9007c7188b09e73438fb56d7faff64c6f13b280dbed580273b329b3981693123b3dd
6
+ metadata.gz: 6d8ec46587bb0b896b0322b57766a6e8bbe51ea00777e538cacd5f6a329d1a3e47c513ccaec720050a3a37049e65dc9be627f25006cc0404cb294fb0ba0a55d6
7
+ data.tar.gz: d0f457130db225049774066ad235cc00db9f3c1ca75c9994c618798a94567867fb21c84059c0a3878ae88736ad9b4d325b2e75f552bef583cd3cb846e48fc0c9
@@ -9,13 +9,6 @@ module Mutant
9
9
  SHORT_DESCRIPTION = 'Run code analysis'
10
10
  SUBCOMMANDS = EMPTY_ARRAY
11
11
 
12
- UNLICENSED = <<~MESSAGE.lines.freeze
13
- You are using mutant unlicensed.
14
-
15
- See https://github.com/mbj/mutant#licensing to aquire a license.
16
- Note: Its free for opensource use, which is recommended for trials.
17
- MESSAGE
18
-
19
12
  NO_TESTS_MESSAGE = <<~'MESSAGE'
20
13
  ===============
21
14
  Mutant found no tests available for mutation testing.
@@ -34,8 +27,8 @@ module Mutant
34
27
  private
35
28
 
36
29
  def action
37
- License.call(world)
38
- .bind { bootstrap }
30
+ bootstrap
31
+ .bind(&method(:verify_usage))
39
32
  .bind(&method(:validate_tests))
40
33
  .bind(&Mutation::Runner.public_method(:call))
41
34
  .bind(&method(:from_result))
@@ -56,6 +49,10 @@ module Mutant
56
49
  Either::Left.new('Uncovered mutations detected, exiting nonzero!')
57
50
  end
58
51
  end
52
+
53
+ def verify_usage(environment)
54
+ environment.config.usage.verify.fmap { environment }
55
+ end
59
56
  end # Run
60
57
  end # Environment
61
58
  end # Command
@@ -15,6 +15,7 @@ module Mutant
15
15
  add_integration_options
16
16
  add_matcher_options
17
17
  add_reporter_options
18
+ add_usage_options
18
19
  ].freeze
19
20
 
20
21
  private
@@ -136,6 +137,20 @@ module Mutant
136
137
  set(reporter: @config.reporter.with(print_warnings: true))
137
138
  end
138
139
  end
140
+
141
+ def add_usage_options(parser)
142
+ parser.separator('Usage:')
143
+
144
+ parser.accept(Usage, Usage::CLI_REGEXP) do |value|
145
+ Usage.parse(value).from_right
146
+ end
147
+
148
+ parser.on(
149
+ '--usage USAGE_TYPE',
150
+ Usage,
151
+ 'License usage: opensource|commercial'
152
+ ) { |usage| set(usage: usage) }
153
+ end
139
154
  end # Run
140
155
  # rubocop:enable Metrics/ClassLength
141
156
  end # Command
@@ -10,7 +10,7 @@ module Mutant
10
10
  class Root < self
11
11
  NAME = 'mutant'
12
12
  SHORT_DESCRIPTION = 'mutation testing engine main command'
13
- SUBCOMMANDS = [Environment::Run, Environment, Subscription, Util].freeze
13
+ SUBCOMMANDS = [Environment::Run, Environment, Util].freeze
14
14
  end # Root
15
15
  end # Command
16
16
  end # CLI
@@ -122,7 +122,7 @@ module Mutant
122
122
 
123
123
  def parse(arguments)
124
124
  Either
125
- .wrap_error(OptionParser::InvalidOption) { parser.order(arguments) }
125
+ .wrap_error(OptionParser::InvalidArgument, OptionParser::InvalidOption) { parser.order(arguments) }
126
126
  .lmap(&method(:with_help))
127
127
  .bind(&method(:parse_remaining))
128
128
  end
@@ -176,14 +176,6 @@ module Mutant
176
176
  end
177
177
  end
178
178
 
179
- def parse_remaining_arguments(remaining)
180
- if remaining.any?
181
- Either::Left.new("#{full_name}: Does not expect extra arguments")
182
- else
183
- Either::Right.new(self)
184
- end
185
- end
186
-
187
179
  def parse_subcommand(arguments)
188
180
  command_name, *arguments = arguments
189
181
 
data/lib/mutant/config.rb CHANGED
@@ -21,7 +21,8 @@ module Mutant
21
21
  :matcher,
22
22
  :mutation,
23
23
  :reporter,
24
- :requires
24
+ :requires,
25
+ :usage
25
26
  )
26
27
 
27
28
  %i[fail_fast].each do |name|
@@ -87,7 +88,8 @@ module Mutant
87
88
  jobs: other.jobs || jobs,
88
89
  matcher: matcher.merge(other.matcher),
89
90
  mutation: mutation.merge(other.mutation),
90
- requires: requires + other.requires
91
+ requires: requires + other.requires,
92
+ usage: other.usage.merge(usage)
91
93
  )
92
94
  end
93
95
  # rubocop:enable Metrics/AbcSize
@@ -247,6 +249,10 @@ module Mutant
247
249
  Transform::Hash::Key.new(
248
250
  transform: Transform::STRING_ARRAY,
249
251
  value: 'requires'
252
+ ),
253
+ Transform::Hash::Key.new(
254
+ transform: Usage::TRANSFORM,
255
+ value: 'usage'
250
256
  )
251
257
  ],
252
258
  required: []
@@ -18,9 +18,10 @@ module Mutant
18
18
  # @return [Result::Test]
19
19
  def call(_tests)
20
20
  Result::Test.new(
21
- output: '',
22
- passed: true,
23
- runtime: 0.0
21
+ job_index: nil,
22
+ output: '',
23
+ passed: true,
24
+ runtime: 0.0
24
25
  )
25
26
  end
26
27
 
@@ -50,5 +50,6 @@ module Mutant
50
50
  expression.prefix?(subject.expression)
51
51
  end
52
52
  end
53
+ private_class_method :ignore_subject?
53
54
  end # Matcher
54
55
  end # Mutant
@@ -21,14 +21,22 @@ module Mutant
21
21
  all?: %i[any?],
22
22
  any?: %i[all?],
23
23
  at: %i[fetch key?],
24
+ detect: %i[first last],
24
25
  fetch: %i[key?],
26
+ find: %i[first last],
27
+ first: %i[last],
25
28
  flat_map: %i[map],
26
29
  gsub: %i[sub],
27
30
  is_a?: %i[instance_of?],
28
31
  kind_of?: %i[instance_of?],
32
+ last: %i[first],
29
33
  map: %i[each],
30
34
  match: %i[match?],
35
+ max: %i[first last],
36
+ max_by: %i[first last],
31
37
  method: %i[public_method],
38
+ min: %i[first last],
39
+ min_by: %i[first last],
32
40
  reverse_each: %i[each],
33
41
  reverse_map: %i[map each],
34
42
  reverse_merge: %i[merge],
@@ -49,6 +57,8 @@ module Mutant
49
57
  .tap do |replacements|
50
58
  replacements.delete(:==)
51
59
  replacements.delete(:eql?)
60
+ replacements.delete(:first)
61
+ replacements.delete(:last)
52
62
  end
53
63
  .freeze
54
64
  end
@@ -21,11 +21,11 @@ module Mutant
21
21
  attr_reader :log
22
22
 
23
23
  def error
24
- @errors.first
24
+ Util.max_one(@errors)
25
25
  end
26
26
 
27
27
  def result
28
- @results.first
28
+ Util.max_one(@results)
29
29
  end
30
30
 
31
31
  def initialize(*)
@@ -96,7 +96,7 @@ module Mutant
96
96
  end
97
97
 
98
98
  def length
99
- @lengths.first
99
+ Util.max_one(@lengths)
100
100
  end
101
101
 
102
102
  def advance_log
@@ -15,6 +15,7 @@ module Mutant
15
15
  #
16
16
  # rubocop:disable Metrics/AbcSize
17
17
  def run
18
+ info 'Usage: %s', object.usage.value
18
19
  info 'Matcher: %s', object.matcher.inspect
19
20
  info 'Integration: %s', object.integration.name || 'null'
20
21
  info 'Jobs: %s', object.jobs || 'auto'
@@ -68,7 +68,7 @@ module Mutant
68
68
  #
69
69
  # @return [undefined]
70
70
  def run
71
- visit_collection(Result, object.failed_test_results)
71
+ visit_failed
72
72
  visit(Env, object.env)
73
73
  FORMATS.each do |report, format, value|
74
74
  __send__(report, format, __send__(value))
@@ -77,6 +77,27 @@ module Mutant
77
77
 
78
78
  private
79
79
 
80
+ def visit_failed
81
+ failed = object.failed_test_results
82
+
83
+ if object.env.config.fail_fast
84
+ visit_failed_tests(failed.take(1))
85
+ visit_other_failed(failed.drop(1))
86
+ else
87
+ visit_failed_tests(failed)
88
+ end
89
+ end
90
+
91
+ def visit_other_failed(other)
92
+ return if other.empty?
93
+
94
+ puts('Other failed tests (report suppressed from fail fast): %d' % other.length)
95
+ end
96
+
97
+ def visit_failed_tests(failed)
98
+ visit_collection(Result, failed)
99
+ end
100
+
80
101
  def efficiency_percent
81
102
  (testtime / runtime) * 100
82
103
  end
@@ -36,9 +36,8 @@ module Mutant
36
36
 
37
37
  def repository_root
38
38
  world
39
- .capture_stdout(%w[git rev-parse --show-toplevel])
40
- .fmap(&:chomp)
41
- .fmap(&world.pathname.public_method(:new))
39
+ .capture_command(%w[git rev-parse --show-toplevel])
40
+ .fmap { |status| world.pathname.new(status.stdout.chomp) }
42
41
  end
43
42
 
44
43
  def touched_path(path, &block)
@@ -52,11 +51,10 @@ module Mutant
52
51
 
53
52
  def diff_index(root)
54
53
  world
55
- .capture_stdout(%W[git diff-index #{to}])
56
- .fmap(&:lines)
57
- .bind do |lines|
54
+ .capture_command(%W[git diff-index #{to}])
55
+ .bind do |status|
58
56
  Mutant
59
- .traverse(->(line) { parse_line(root, line) }, lines)
57
+ .traverse(->(line) { parse_line(root, line) }, status.stdout.lines)
60
58
  .fmap do |paths|
61
59
  paths.to_h { |path| [path.path, path] }
62
60
  end
@@ -105,8 +103,8 @@ module Mutant
105
103
 
106
104
  def diff_ranges
107
105
  world
108
- .capture_stdout(%W[git diff --unified=0 #{to} -- #{path}])
109
- .fmap(&Ranges.public_method(:parse))
106
+ .capture_command(%W[git diff --unified=0 #{to} -- #{path}])
107
+ .fmap { |status| Ranges.parse(status.stdout) }
110
108
  .from_right
111
109
  end
112
110
  memoize :diff_ranges
data/lib/mutant/result.rb CHANGED
@@ -158,7 +158,7 @@ module Mutant
158
158
 
159
159
  # Test result
160
160
  class Test
161
- include Anima.new(:passed, :runtime, :output)
161
+ include Anima.new(:job_index, :passed, :runtime, :output)
162
162
 
163
163
  alias_method :success?, :passed
164
164
 
@@ -170,9 +170,10 @@ module Mutant
170
170
  # @return [undefined]
171
171
  def initialize
172
172
  super(
173
- output: '',
174
- passed: false,
175
- runtime: 0.0
173
+ job_index: nil,
174
+ output: '',
175
+ passed: false,
176
+ runtime: 0.0
176
177
  )
177
178
  end
178
179
  end # VoidValue
@@ -22,7 +22,7 @@ module Mutant
22
22
  Result::TestEnv.new(
23
23
  env: env,
24
24
  runtime: env.world.timer.now - @start,
25
- test_results: @test_results
25
+ test_results: @test_results.sort_by!(&:job_index)
26
26
  )
27
27
  end
28
28
 
@@ -42,7 +42,11 @@ module Mutant
42
42
  fail response.error
43
43
  end
44
44
 
45
- @test_results << response.result.with(output: response.log)
45
+ @test_results << response.result.with(
46
+ job_index: response.job.index,
47
+ output: response.log
48
+ )
49
+
46
50
  self
47
51
  end
48
52
  end # Sink
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Usage
5
+ include Adamantium, Equalizer.new
6
+
7
+ def value
8
+ self.class::VALUE
9
+ end
10
+
11
+ def verify
12
+ Either::Right.new(nil)
13
+ end
14
+
15
+ def message
16
+ self.class::MESSAGE
17
+ end
18
+
19
+ def merge(_other)
20
+ self
21
+ end
22
+
23
+ class Commercial < self
24
+ VALUE = 'commercial'
25
+
26
+ MESSAGE = <<~'MESSAGE'
27
+ ## Commercial use
28
+
29
+ `commercial` usage type requires [payment](https://github.com/mbj/mutant?tab=readme-ov-file#pricing),
30
+ If you are under an active payment plan you can use the commercial usage type on any
31
+ repository, including private ones.
32
+
33
+ To use `commercial` usage type either specify `--usage commercial` on the command
34
+ line or use the config file key `usage`:
35
+
36
+ ```
37
+ # mutant.yml or config/mutant.yml
38
+ usage: commercial
39
+ ```
40
+ MESSAGE
41
+ end
42
+
43
+ class Opensource < self
44
+ VALUE = 'opensource'
45
+
46
+ MESSAGE = <<~'MESSAGE'
47
+ ## Opensource use
48
+
49
+ `opensource` usage is free while mutant is run on an opensource project.
50
+ Under that usage mutant does not require any kind of sign up or payment.
51
+ Set this usage type exclusively on public opensource projects. Any other
52
+ scenario requires payment.
53
+ Using the `opensource` usage type on private repotiories and or on commercial
54
+ code bases is not valid.
55
+
56
+ To use `opensource` usage type either specify `--usage opensource` on the command
57
+ line or use the config file key `usage`:
58
+
59
+ ```
60
+ # mutant.yml or config/mutant.yml
61
+ usage: opensource
62
+ ```
63
+ MESSAGE
64
+ end
65
+
66
+ class Unknown < self
67
+ VALUE = 'unknown'
68
+
69
+ MESSAGE = <<~"MESSAGE".freeze
70
+ # Unknown mutant usage type
71
+
72
+ Mutant license usage is unspecified. Valid usage types are `opensource` or `commercial`.
73
+
74
+ Usage can be specified via the `--usage` command line parameter or via the
75
+ config file under the `usage` key.
76
+
77
+ #{Commercial::MESSAGE}
78
+ #{Opensource::MESSAGE}
79
+ This is a breaking change for users of the 0.11.x / 0.10.x mutant releases.
80
+ Sorry for that but its going to make future adoption much easier.
81
+ License gem is gone entirely.
82
+ MESSAGE
83
+
84
+ def merge(other)
85
+ other
86
+ end
87
+
88
+ def verify
89
+ Either::Left.new(MESSAGE)
90
+ end
91
+ end
92
+
93
+ def self.parse(value)
94
+ {
95
+ 'commercial' => Either::Right.new(Commercial.new),
96
+ 'opensource' => Either::Right.new(Opensource.new)
97
+ }.fetch(value) { Either::Left.new("Unknown usage option: #{value.inspect}") }
98
+ end
99
+
100
+ CLI_REGEXP = /\A(?:commercial|opensource)\z/
101
+
102
+ TRANSFORM = Transform::Sequence.new(
103
+ steps: [
104
+ Transform::STRING,
105
+ Transform::Block.capture(:environment_variables, &method(:parse))
106
+ ]
107
+ )
108
+ end # Usage
109
+ end # Mutant
data/lib/mutant/util.rb CHANGED
@@ -12,9 +12,30 @@ module Mutant
12
12
  #
13
13
  # @return [Object] first entry
14
14
  def self.one(array)
15
- return array.first if array.one?
15
+ case array
16
+ in [value]
17
+ value
18
+ else
19
+ fail SizeError, "expected size to be exactly 1 but size was #{array.size}"
20
+ end
21
+ end
16
22
 
17
- fail SizeError, "expected size to be exactly 1 but size was #{array.size}"
23
+ # Return only element in array if it contains max one member
24
+ #
25
+ # @param array [Array]
26
+ #
27
+ # @return [Object] first entry
28
+ # @return [nil] if empty
29
+ #
30
+ # rubocop:disable Lint/EmptyInPattern
31
+ def self.max_one(array)
32
+ case array
33
+ in []
34
+ in [value]
35
+ value
36
+ else
37
+ fail SizeError, "expected size to be max 1 but size was #{array.size}"
38
+ end
18
39
  end
19
40
  end # Util
20
41
  end # Mutant
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mutant
4
4
  # Current mutant version
5
- VERSION = '0.11.34'
5
+ VERSION = '0.12.1'
6
6
  end # Mutant
data/lib/mutant/world.rb CHANGED
@@ -39,19 +39,25 @@ module Mutant
39
39
  INSPECT
40
40
  end
41
41
 
42
+ class CommandStatus
43
+ include Adamantium, Anima.new(:process_status, :stderr, :stdout)
44
+ end # CommandStatus
45
+
42
46
  # Capture stdout of a command
43
47
  #
44
48
  # @param [Array<String>] command
45
49
  #
46
- # @return [Either<String,String>]
47
- def capture_stdout(command)
48
- stdout, status = open3.capture2(*command, binmode: true)
50
+ # @return [Either<CommandStatus,CommandStatus>]
51
+ def capture_command(command)
52
+ stdout, stderr, process_status = open3.capture3(*command, binmode: true)
49
53
 
50
- if status.success?
51
- Either::Right.new(stdout)
52
- else
53
- Either::Left.new("Command #{command} failed!")
54
- end
54
+ (process_status.success? ? Either::Right : Either::Left).new(
55
+ CommandStatus.new(
56
+ process_status: process_status,
57
+ stderr: stderr,
58
+ stdout: stdout
59
+ )
60
+ )
55
61
  end
56
62
 
57
63
  # Try const get
data/lib/mutant.rb CHANGED
@@ -109,6 +109,7 @@ module Mutant
109
109
  require 'mutant/require_highjack'
110
110
  require 'mutant/mutation'
111
111
  require 'mutant/mutation/operators'
112
+ require 'mutant/usage'
112
113
  require 'mutant/mutation/config'
113
114
  require 'mutant/mutator'
114
115
  require 'mutant/mutator/util'
@@ -221,7 +222,6 @@ module Mutant
221
222
  require 'mutant/config/coverage_criteria'
222
223
  require 'mutant/cli'
223
224
  require 'mutant/cli/command'
224
- require 'mutant/cli/command/subscription'
225
225
  require 'mutant/cli/command/environment'
226
226
  require 'mutant/cli/command/environment/irb'
227
227
  require 'mutant/cli/command/environment/run'
@@ -255,11 +255,6 @@ module Mutant
255
255
  require 'mutant/repository/diff/ranges'
256
256
  require 'mutant/zombifier'
257
257
  require 'mutant/range'
258
- require 'mutant/license'
259
- require 'mutant/license/subscription'
260
- require 'mutant/license/subscription/commercial'
261
- require 'mutant/license/subscription/opensource'
262
- require 'mutant/license/subscription/repository'
263
258
  require 'mutant/segment'
264
259
  require 'mutant/segment/recorder'
265
260
  end
@@ -363,7 +358,8 @@ module Mutant
363
358
  matcher: Matcher::Config::DEFAULT,
364
359
  mutation: Mutation::Config::EMPTY,
365
360
  reporter: Reporter::CLI.build(WORLD.stdout),
366
- requires: EMPTY_ARRAY
361
+ requires: EMPTY_ARRAY,
362
+ usage: Usage::Unknown.new
367
363
  )
368
364
  end # Config
369
365
 
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.11.34
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Schirp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-26 00:00:00.000000000 Z
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs
@@ -177,7 +177,6 @@ files:
177
177
  - lib/mutant/cli/command/environment/subject.rb
178
178
  - lib/mutant/cli/command/environment/test.rb
179
179
  - lib/mutant/cli/command/root.rb
180
- - lib/mutant/cli/command/subscription.rb
181
180
  - lib/mutant/cli/command/util.rb
182
181
  - lib/mutant/config.rb
183
182
  - lib/mutant/config/coverage_criteria.rb
@@ -197,11 +196,6 @@ files:
197
196
  - lib/mutant/isolation/exception.rb
198
197
  - lib/mutant/isolation/fork.rb
199
198
  - lib/mutant/isolation/none.rb
200
- - lib/mutant/license.rb
201
- - lib/mutant/license/subscription.rb
202
- - lib/mutant/license/subscription/commercial.rb
203
- - lib/mutant/license/subscription/opensource.rb
204
- - lib/mutant/license/subscription/repository.rb
205
199
  - lib/mutant/loader.rb
206
200
  - lib/mutant/matcher.rb
207
201
  - lib/mutant/matcher/chain.rb
@@ -341,6 +335,7 @@ files:
341
335
  - lib/mutant/test/runner/sink.rb
342
336
  - lib/mutant/timer.rb
343
337
  - lib/mutant/transform.rb
338
+ - lib/mutant/usage.rb
344
339
  - lib/mutant/util.rb
345
340
  - lib/mutant/variable.rb
346
341
  - lib/mutant/version.rb
@@ -366,7 +361,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
366
361
  - !ruby/object:Gem::Version
367
362
  version: '0'
368
363
  requirements: []
369
- rubygems_version: 3.3.25
364
+ rubygems_version: 3.5.3
370
365
  signing_key:
371
366
  specification_version: 4
372
367
  summary: ''
@@ -1,54 +0,0 @@
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.call(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,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mutant
4
- module License
5
- class Subscription
6
- class Commercial < self
7
- include AbstractType
8
-
9
- def self.from_json(value)
10
- {
11
- 'individual' => Individual,
12
- 'organization' => Organization
13
- }.fetch(value.fetch('type', 'individual')).from_json(value)
14
- end
15
-
16
- class Organization < self
17
- SUBSCRIPTION_NAME = 'commercial organization'
18
-
19
- def self.from_json(value)
20
- new(licensed: value.fetch('repositories').map(&Repository.public_method(:parse)).to_set)
21
- end
22
-
23
- def call(world)
24
- Repository.load_from_git(world).bind(&method(:check_subscription))
25
- end
26
-
27
- private
28
-
29
- def check_subscription(actual)
30
- if licensed.any? { |repository| actual.any? { |other| repository.allow?(other) } }
31
- success
32
- else
33
- failure(licensed, actual)
34
- end
35
- end
36
- end
37
-
38
- class Individual < self
39
- SUBSCRIPTION_NAME = 'commercial individual'
40
-
41
- class Author
42
- include Anima.new(:email)
43
-
44
- alias_method :to_s, :email
45
- public :to_s
46
- end
47
-
48
- def self.from_json(value)
49
- new(licensed: value.fetch('authors').to_set { |email| Author.new(email: email) })
50
- end
51
-
52
- def call(world)
53
- candidates = candidates(world)
54
-
55
- if (licensed & candidates).any?
56
- success
57
- else
58
- failure(licensed, candidates)
59
- end
60
- end
61
-
62
- private
63
-
64
- def candidates(world)
65
- git_author(world).merge(commit_author(world))
66
- end
67
-
68
- def git_author(world)
69
- capture(world, %w[git config --get user.email])
70
- end
71
-
72
- def commit_author(world)
73
- capture(world, %w[git show --quiet --pretty=format:%ae])
74
- end
75
-
76
- def capture(world, command)
77
- world
78
- .capture_stdout(command)
79
- .fmap(&:chomp)
80
- .fmap { |email| Author.new(email: email) }
81
- .fmap { |value| Set.new([value]) }
82
- .from_right { Set.new }
83
- end
84
- end # Individual
85
- end # Commercial
86
- end # Subscription
87
- end # License
88
- end # Mutant
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mutant
4
- module License
5
- class Subscription
6
- class Opensource < self
7
- SUBSCRIPTION_NAME = 'opensource repository'
8
-
9
- def self.from_json(value)
10
- new(licensed: value.fetch('repositories').map(&Repository.public_method(:parse)).to_set)
11
- end
12
-
13
- def call(world)
14
- Repository.load_from_git(world).bind(&method(:check_subscription))
15
- end
16
-
17
- private
18
-
19
- def check_subscription(actual)
20
- if licensed.any? { |repository| actual.any? { |other| repository.allow?(other) } }
21
- success
22
- else
23
- failure(licensed, actual)
24
- end
25
- end
26
-
27
- end # Opensource
28
- end # Subscription
29
- end # License
30
- end # Mutant
@@ -1,72 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mutant
4
- module License
5
- class Subscription
6
- class Repository
7
- include Anima.new(:host, :path)
8
-
9
- REMOTE_REGEXP = /\A[^\t]+\t(?<url>[^ ]+) \((?:fetch|push)\)\n\z/
10
- GIT_SSH_REGEXP = %r{\A[^@]+@(?<host>[^:/]+)[:/](?<path>.+?)(?:\.git)?\z}
11
- GIT_HTTPS_REGEXP = %r{\Ahttps://(?<host>[^/]+)/(?<path>.+?)(?:\.git)?\z}
12
- WILDCARD = '/*'
13
- WILDCARD_RANGE = (..-WILDCARD.length)
14
-
15
- private_constant(*constants(false))
16
-
17
- def to_s
18
- [host, path].join('/')
19
- end
20
-
21
- def self.load_from_git(world)
22
- world
23
- .capture_stdout(%w[git remote --verbose])
24
- .fmap(&method(:parse_remotes))
25
- end
26
-
27
- def self.parse_remotes(input)
28
- input.lines.map(&method(:parse_remote)).to_set
29
- end
30
- private_class_method :parse_remotes
31
-
32
- def self.parse(input)
33
- host, path = *input.split('/', 2).map(&:downcase)
34
- new(host: host, path: path)
35
- end
36
-
37
- def self.parse_remote(input)
38
- match = REMOTE_REGEXP.match(input) or
39
- fail "Unmatched remote line: #{input.inspect}"
40
-
41
- parse_url(match[:url])
42
- end
43
- private_class_method :parse_remote
44
-
45
- def self.parse_url(input)
46
- match = GIT_SSH_REGEXP.match(input) || GIT_HTTPS_REGEXP.match(input)
47
-
48
- unless match
49
- fail "Unmatched git remote URL: #{input.inspect}"
50
- end
51
-
52
- new(host: match[:host], path: match[:path].downcase)
53
- end
54
- private_class_method :parse_url
55
-
56
- def allow?(other)
57
- other.host.eql?(host) && path_match?(other.path)
58
- end
59
-
60
- private
61
-
62
- def path_match?(other_path)
63
- path.eql?(other_path) || wildcard_match?(path, other_path) || wildcard_match?(other_path, path)
64
- end
65
-
66
- def wildcard_match?(left, right)
67
- left.end_with?(WILDCARD) && right.start_with?(left[WILDCARD_RANGE])
68
- end
69
- end # Repository
70
- end # Subscription
71
- end # License
72
- end # Mutant
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mutant
4
- module License
5
- class Subscription
6
- include Anima.new(:licensed)
7
-
8
- FORMAT = <<~'MESSAGE'
9
- %<subscription_name>s subscription:
10
- Licensed:
11
- %<licensed>s
12
- MESSAGE
13
-
14
- FAILURE_FORMAT = <<~'MESSAGE'
15
- Can not validate %<subscription_name>s license.
16
- Licensed:
17
- %<expected>s
18
- Present:
19
- %<actual>s
20
- MESSAGE
21
-
22
- # Load value into subscription
23
- #
24
- # @param [Object] value
25
- #
26
- # @return [Subscription]
27
- def self.load(world, value)
28
- {
29
- 'com' => Commercial,
30
- 'oss' => Opensource
31
- }.fetch(value.fetch('type'))
32
- .from_json(value.fetch('contents'))
33
- .call(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
- }
44
- end
45
-
46
- private
47
-
48
- def failure(expected, actual)
49
- Either::Left.new(failure_message(expected, actual))
50
- end
51
-
52
- def success
53
- Either::Right.new(self)
54
- end
55
-
56
- def subscription_name
57
- self.class::SUBSCRIPTION_NAME
58
- end
59
-
60
- def failure_message(expected, actual)
61
- FAILURE_FORMAT % {
62
- actual: actual.any? ? actual.map(&:to_s).join("\n") : '[none]',
63
- expected: expected.map(&:to_s).join("\n"),
64
- subscription_name: subscription_name
65
- }
66
- end
67
- end # Subscription
68
- end # License
69
- end # Mutant
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mutant
4
- module License
5
- NAME = 'mutant-license'
6
- VERSION = ['>= 0.1', '< 0.3'].freeze
7
-
8
- # Load license
9
- #
10
- # @param [World] world
11
- #
12
- # @return [Either<String,Subscription>]
13
- #
14
- # @api private
15
- def self.call(world)
16
- load_mutant_license(world)
17
- .fmap { license_path(world) }
18
- .bind { |path| Subscription.load(world, world.json.load(path)) }
19
- end
20
-
21
- def self.load_mutant_license(world)
22
- Either
23
- .wrap_error(LoadError) { world.gem_method.call(NAME, *VERSION) }
24
- .lmap(&:message)
25
- .lmap(&method(:check_for_rubygems_mutant_license))
26
- end
27
- private_class_method :load_mutant_license
28
-
29
- def self.check_for_rubygems_mutant_license(message)
30
- if message.include?('already activated mutant-license-0.0.0')
31
- 'mutant-license gem from rubygems.org is a dummy'
32
- else
33
- message
34
- end
35
- end
36
- private_class_method :check_for_rubygems_mutant_license
37
-
38
- def self.license_path(world)
39
- world
40
- .pathname
41
- .new(world.gem.loaded_specs.fetch(NAME).full_gem_path)
42
- .join('license.json')
43
- end
44
- private_class_method :license_path
45
- end
46
- end