From bce93232a3d79a8e2a73827540600abe4691665f Mon Sep 17 00:00:00 2001 From: Markus Schirp Date: Mon, 8 Apr 2024 04:04:16 +0000 Subject: [PATCH] Change to minimal license enforcement * This removes the license gem enitrely. * Specifies the user to declare his compliance with either opensource or commercial usage. --- Changelog.md | 10 + Gemfile.lock | 8 +- Gemfile.shared | 6 - README.md | 60 +- docs/commercial.md | 59 +- docs/mutant-minitest.md | 1 + docs/mutant-rspec.md | 1 + lib/mutant.rb | 10 +- lib/mutant/cli/command.rb | 10 +- lib/mutant/cli/command/environment.rb | 15 + lib/mutant/cli/command/environment/run.rb | 15 +- lib/mutant/cli/command/root.rb | 2 +- lib/mutant/cli/command/subscription.rb | 54 -- lib/mutant/config.rb | 10 +- lib/mutant/license.rb | 46 -- lib/mutant/license/subscription.rb | 69 -- lib/mutant/license/subscription/commercial.rb | 86 --- lib/mutant/license/subscription/opensource.rb | 30 - lib/mutant/license/subscription/repository.rb | 72 -- lib/mutant/reporter/cli/printer/config.rb | 1 + lib/mutant/usage.rb | 109 +++ lib/mutant/version.rb | 2 +- mutant.yml | 3 +- scripts/devloop.sh | 2 +- spec/integration/mutant/minitest_spec.rb | 9 +- spec/integration/mutant/rspec_spec.rb | 8 +- spec/support/corpus.rb | 1 + spec/unit/mutant/cli_spec.rb | 667 +++++++++--------- spec/unit/mutant/config_spec.rb | 31 + spec/unit/mutant/license/repository_spec.rb | 283 -------- spec/unit/mutant/license/subscription_spec.rb | 381 ---------- spec/unit/mutant/license_spec.rb | 125 ---- .../reporter/cli/printer/config_spec.rb | 5 + .../reporter/cli/printer/env_progress_spec.rb | 3 + .../reporter/cli/printer/env_result_spec.rb | 1 + spec/unit/mutant/reporter/cli_spec.rb | 2 + spec/unit/mutant/usage_spec.rb | 107 +++ test_app/Gemfile.minitest.lock | 12 +- test_app/Gemfile.rspec3.10.lock | 12 +- test_app/Gemfile.rspec3.11.lock | 12 +- test_app/Gemfile.rspec3.12.lock | 12 +- test_app/Gemfile.rspec3.13.lock | 12 +- test_app/Gemfile.rspec3.8.lock | 12 +- test_app/Gemfile.rspec3.9.lock | 12 +- 44 files changed, 722 insertions(+), 1666 deletions(-) delete mode 100644 lib/mutant/cli/command/subscription.rb delete mode 100644 lib/mutant/license.rb delete mode 100644 lib/mutant/license/subscription.rb delete mode 100644 lib/mutant/license/subscription/commercial.rb delete mode 100644 lib/mutant/license/subscription/opensource.rb delete mode 100644 lib/mutant/license/subscription/repository.rb create mode 100644 lib/mutant/usage.rb delete mode 100644 spec/unit/mutant/license/repository_spec.rb delete mode 100644 spec/unit/mutant/license/subscription_spec.rb delete mode 100644 spec/unit/mutant/license_spec.rb create mode 100644 spec/unit/mutant/usage_spec.rb diff --git a/Changelog.md b/Changelog.md index dfea086de..05cdb7cd8 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,13 @@ +# v0.12.0 [unreleased] + + Drop the license gem, entirely. This reduces DRM in mutants code base to the absolute minimal. + Mutant is *still* comercial software that requires payment if used on a commercial code base! + + Migration: + + * Commercial users: Add `usage: commercial` to your config file (or `--usage commercial` to your CLI) + * Opensource users: Add `usage: opensource` to your config file (or `--usage opensource` to your CLI) + # v0.11.34 2024-03-26 * [#1432](https://github.com/mbj/mutant/pull/1432) diff --git a/Gemfile.lock b/Gemfile.lock index 38c0b55a9..be48e787a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,18 +1,13 @@ PATH remote: . specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -69,7 +64,6 @@ PLATFORMS DEPENDENCIES mutant! - mutant-license! rspec (~> 3.10) rspec-core (~> 3.10) rspec-its (~> 1.3.0) diff --git a/Gemfile.shared b/Gemfile.shared index 59120bb3b..e69de29bb 100644 --- a/Gemfile.shared +++ b/Gemfile.shared @@ -1,6 +0,0 @@ -# Mutant itself uses an opensource license key. -# Scoped to https://github.com/mbj/mutant it'll -# not be useful elsewhere. -source 'https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev' do - gem 'mutant-license' -end diff --git a/README.md b/README.md index 7bdbb363f..e45435370 100644 --- a/README.md +++ b/README.md @@ -77,54 +77,27 @@ Labels: ## Licensing -Mutant is commercial software, with a free usage plan for opensource projects. +Mutant is commercial software, with a free usage option for opensource projects. +Opensource projects have to be on a public repository. -Commercial projects have to acquire a license per developer, with unlimited repositories -per developer. CI usage for licensed developers is included. +Commercial projects have to pay a monthly or annual subscription fee. -Opensource projects have to acquire their free license per repository. That license will -work for any contributor implicitly. Typically the project maintainer gets the license. +## Opensource usage -The license distribution happens through the `mutant-license` gem. Mutant installs without -that dependency, but will not be very cooperative unless `mutant-license` is also available. +Usage is free and does not require a signup. But it requires the code is under an +opensource license and public. Specify `--usage opensource` on the CLI or `usage: opensource` +in the config file. -The license gem is dynamically generated per licensee and comes with a unique license gem source -URL. +## Commercial usage -After signup for a license the following has to be added to your `Gemfile` replacing `${key}` -with the license key and `${plan}` with `com` for commercial or `oss` for opensource usage. +Commercial use requires payment via a subscription and requires a signup. See [pricing](#pricing) for +available plans. -```ruby -source 'https://${plan}:${key}@gem.mutant.dev' do - gem 'mutant-license' -end -``` - -The mutant license gem contains metadata that allows mutant to verify licensed use. - -For commercial licenses mutant checks the git commit author or the configured git email -to be in the set of licensed developers. - -For opensource licenses mutant checks the git remotes against the licensed git repositories. -This allows the project maintainer to sign up and not bother collaborators with the details. - -There are, apart from initial license gem installation, no remote interaction for -license validation. - -### Getting an Opensource license - -As stated above: Opensource projects of any kind are free to use mutant. - -Just mail [me](mailto:mbj@schirp-dso.com?subject=Mutant%20Opensource%20License): Please -include: - -* Just the git remote URL of your repository. Repository can be anywhere, must not be on Github, just has to be public. - -I do not need any more details. +After payment pecify `--usage commercial` on the CLI or `usage: commercial` in the config file. ### Pricing -**Mutant is free for [opensource use](#getting-an-opensource-license)!** +**Mutant is free for [opensource use](#opensource-usage)!** For commercial use mutants pricing is subscription based. @@ -137,9 +110,11 @@ For commercial use mutants pricing is subscription based. Costs are **per developer using mutant on any number of repositories**. -Volume licenses with custom plans are available on request. +Volume subscriptions with custom plans are available on request. -Should you want to procure a commercial mutant subscription please [mail me](mailto:mbj@schirp-dso.com?subject=Mutant%20Commercial%20License). +Should you want to procure a commercial mutant subscription please +[mail me](mailto:mbj@schirp-dso.com?subject=Mutant%20Commercial%20License) to start the payment +process. Please include the following information: @@ -147,8 +122,7 @@ Please include the following information: * A payment email address, if different from your email address. * Only for the EU: A valid VAT-ID is *required*, no sales to private customers to avoid the horrors cross border VAT / MOSS. - VAT for customers outside of Malta will use **reverse charging**. -* *Per developer* the git author email address as returned by `git config user.email` + VAT for EU customers outside of Malta will use **reverse charging**. Also feel free to ask any other question I forgot to proactively answer here. diff --git a/docs/commercial.md b/docs/commercial.md index e5cf0991c..148a28e8c 100644 --- a/docs/commercial.md +++ b/docs/commercial.md @@ -6,8 +6,8 @@ Is there a trial version? There is no free trial. -But mutant offers to refund the last monthly -payment on cancellation. Basically a rolling trial on monthly plans. +But mutant offers to refund the last monthly payment on cancellation. +Basically a rolling trial on monthly plans. Yearly and custom plans do not offer any refunds but come with discounts. @@ -35,17 +35,14 @@ This per subscription is valid for any number of private repositories. Depending on your plan subscriptions renew monthly or yearly. -After purchase you get access to a custom rubygem hosted on mutants license -server that enables mutants functionality in commercial repositories. - +After purchase you setup the commercial usage type in mutant +and are ready to go. How do I determine the number of required subscriptions? -------------------------------------------------------- Collect the git author email from each of your developers that need to use mutant. -Use `git config --get user.email`. Count that list. - -Mutant on CI will work on any commit authored by a developer in that list. +Use `git config --get user.email`. Count that list. So a designer, who contributes to your repository, but never touches Ruby/Mutant: Will not be forced to get a mutant subscription. @@ -54,35 +51,12 @@ What happens if my subsription lapses? -------------------------------------- If your credit card cannot be charged, even after retries: The subscription will be -considered inactive and you loose access to mutants license gem. Which prevents `bundle install` -and adjacent commands to succeed. +considered inactive and you loose the right to run mutant. **Please note that pricing can chance**. Once you purchase a subscription, you get that price forever **as long you pay on time**. If your subscription expires for a non-payment, you will lose that pricing and need to repurchase at current pricing. -What does the license require me to do? ---------------------------------------- - -Your purchase gets you unique access credentials for accessing the license gem which enables -mutant on a private repository. The license requires you to keep these access credentials private. - -If your access credentials are ever found to be publicized: - -1. You get a warning email with details. You need to remove the content and - will get new new credentials being generated. - The old credentials will stop working immediately so you'll need to update your - `Gemfile`. -2. If your credentials are publicized a second time, we reserve the right to permanently - remove access (but won't unless it's really egregious - sloppy contractors happen). - -Do I have to share the credentials with all of my developers? -------------------------------------------------------------- - -In general yes. The intention is that the license details gets checked into the -`Gemfile`. This only gives access to mutants funcionality, your billing account is -separate and individuall developers cannot update the subscription. - Can I get a refund? ------------------- @@ -98,7 +72,6 @@ Can I request a change to the license terms? -------------------------------------------- Mutant is sold as is. Pricing becomes negotiatable over 20 developers. - License terms may be amended for even bigger customers. Disputing a Charge @@ -133,15 +106,8 @@ Schirp DSO LTD only collects enough customer information provide its services wh * Per developer: The developers git author emaill address. * Standard HTTP logging for the license server with 14d expiry. -Mutant runs exclusively on your developers machines. Or your CI. The only time mutant -calls a service operated by Schirp DSO LTD: Is on `bundle install`. Where `bundler` sends a -HTTP request with your license key to the license server. - -BTW: As mutant does NOT control the HTTP call side (bundler does instead) there cannot be any -information leak being caused by mutant. Apart from HTTP logs at the license server. +Mutant runs exclusively on your developers machines. Or your CI. -At no point in time Schirp DSO LTD gets access to your source code, your customers data -or other sensitive material. Should mutant gain more features that would enable features such as distributed analysis and reporting, these features will be opt in, with a big warning. @@ -153,3 +119,14 @@ Customer information is never shared or sold to anyone. * Stripe for subscription, credit card and SEPA direct debits. * Transferwise for receiving SEPA and ACH transfers. * AWS to host the license server. + +At no point in time Schirp DSO LTD gets access to your source code, your customers data +or other sensitive material. + +### Pre mutant 0.12 license gem. + +The only time mutant calls a service operated by Schirp DSO LTD: Is on `bundle install`. +Where `bundler` sends a HTTP request with your license key to the license server. + +BTW: As mutant does NOT control the HTTP call side (bundler does instead) there cannot be any +information leak being caused by mutant. Apart from HTTP logs at the license server. diff --git a/docs/mutant-minitest.md b/docs/mutant-minitest.md index c3bf24b39..c6ff3d25a 100644 --- a/docs/mutant-minitest.md +++ b/docs/mutant-minitest.md @@ -65,6 +65,7 @@ This prints a report like: ```sh Mutant environment: +Usage: opensource Matcher: # Integration: Mutant::Integration::Minitest Jobs: 8 diff --git a/docs/mutant-rspec.md b/docs/mutant-rspec.md index 24191f1ac..80ee8fbfe 100644 --- a/docs/mutant-rspec.md +++ b/docs/mutant-rspec.md @@ -38,6 +38,7 @@ This prints a report like: ```sh Mutant environment: +Usage: opensource Matcher: # Integration: Mutant::Integration::Rspec Jobs: 8 diff --git a/lib/mutant.rb b/lib/mutant.rb index 79c9135ad..2f1d0967f 100644 --- a/lib/mutant.rb +++ b/lib/mutant.rb @@ -109,6 +109,7 @@ module Mutant require 'mutant/require_highjack' require 'mutant/mutation' require 'mutant/mutation/operators' + require 'mutant/usage' require 'mutant/mutation/config' require 'mutant/mutator' require 'mutant/mutator/util' @@ -221,7 +222,6 @@ module Mutant require 'mutant/config/coverage_criteria' require 'mutant/cli' require 'mutant/cli/command' - require 'mutant/cli/command/subscription' require 'mutant/cli/command/environment' require 'mutant/cli/command/environment/irb' require 'mutant/cli/command/environment/run' @@ -255,11 +255,6 @@ module Mutant require 'mutant/repository/diff/ranges' require 'mutant/zombifier' require 'mutant/range' - require 'mutant/license' - require 'mutant/license/subscription' - require 'mutant/license/subscription/commercial' - require 'mutant/license/subscription/opensource' - require 'mutant/license/subscription/repository' require 'mutant/segment' require 'mutant/segment/recorder' end @@ -363,7 +358,8 @@ class Config matcher: Matcher::Config::DEFAULT, mutation: Mutation::Config::EMPTY, reporter: Reporter::CLI.build(WORLD.stdout), - requires: EMPTY_ARRAY + requires: EMPTY_ARRAY, + usage: Usage::Unknown.new ) end # Config diff --git a/lib/mutant/cli/command.rb b/lib/mutant/cli/command.rb index 6ae277158..70398a423 100644 --- a/lib/mutant/cli/command.rb +++ b/lib/mutant/cli/command.rb @@ -122,7 +122,7 @@ def banner def parse(arguments) Either - .wrap_error(OptionParser::InvalidOption) { parser.order(arguments) } + .wrap_error(OptionParser::InvalidArgument, OptionParser::InvalidOption) { parser.order(arguments) } .lmap(&method(:with_help)) .bind(&method(:parse_remaining)) end @@ -176,14 +176,6 @@ def parse_remaining(remaining) end end - def parse_remaining_arguments(remaining) - if remaining.any? - Either::Left.new("#{full_name}: Does not expect extra arguments") - else - Either::Right.new(self) - end - end - def parse_subcommand(arguments) command_name, *arguments = arguments diff --git a/lib/mutant/cli/command/environment.rb b/lib/mutant/cli/command/environment.rb index cc1aa7a86..a49a724a4 100644 --- a/lib/mutant/cli/command/environment.rb +++ b/lib/mutant/cli/command/environment.rb @@ -15,6 +15,7 @@ class Environment < self add_integration_options add_matcher_options add_reporter_options + add_usage_options ].freeze private @@ -136,6 +137,20 @@ def add_reporter_options(parser) set(reporter: @config.reporter.with(print_warnings: true)) end end + + def add_usage_options(parser) + parser.separator('Usage:') + + parser.accept(Usage, Usage::CLI_REGEXP) do |value| + Usage.parse(value).from_right + end + + parser.on( + '--usage USAGE_TYPE', + Usage, + 'License usage: opensource|commercial' + ) { |usage| set(usage: usage) } + end end # Run # rubocop:enable Metrics/ClassLength end # Command diff --git a/lib/mutant/cli/command/environment/run.rb b/lib/mutant/cli/command/environment/run.rb index 89040c945..3db61a740 100644 --- a/lib/mutant/cli/command/environment/run.rb +++ b/lib/mutant/cli/command/environment/run.rb @@ -9,13 +9,6 @@ class Run < self SHORT_DESCRIPTION = 'Run code analysis' SUBCOMMANDS = EMPTY_ARRAY - UNLICENSED = <<~MESSAGE.lines.freeze - You are using mutant unlicensed. - - See https://github.com/mbj/mutant#licensing to aquire a license. - Note: Its free for opensource use, which is recommended for trials. - MESSAGE - NO_TESTS_MESSAGE = <<~'MESSAGE' =============== Mutant found no tests available for mutation testing. @@ -34,8 +27,8 @@ class Run < self private def action - License.call(world) - .bind { bootstrap } + bootstrap + .bind(&method(:verify_usage)) .bind(&method(:validate_tests)) .bind(&Mutation::Runner.public_method(:call)) .bind(&method(:from_result)) @@ -56,6 +49,10 @@ def from_result(result) Either::Left.new('Uncovered mutations detected, exiting nonzero!') end end + + def verify_usage(environment) + environment.config.usage.verify.fmap { environment } + end end # Run end # Environment end # Command diff --git a/lib/mutant/cli/command/root.rb b/lib/mutant/cli/command/root.rb index f3a857709..8888a3be4 100644 --- a/lib/mutant/cli/command/root.rb +++ b/lib/mutant/cli/command/root.rb @@ -10,7 +10,7 @@ class Environment < self class Root < self NAME = 'mutant' SHORT_DESCRIPTION = 'mutation testing engine main command' - SUBCOMMANDS = [Environment::Run, Environment, Subscription, Util].freeze + SUBCOMMANDS = [Environment::Run, Environment, Util].freeze end # Root end # Command end # CLI diff --git a/lib/mutant/cli/command/subscription.rb b/lib/mutant/cli/command/subscription.rb deleted file mode 100644 index 696885bc3..000000000 --- a/lib/mutant/cli/command/subscription.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -module Mutant - module CLI - class Command - class Subscription < self - NAME = 'subscription' - SHORT_DESCRIPTION = 'Subscription subcommands' - - private - - def license - License.call(world) - end - - class Test < self - NAME = 'test' - SUBCOMMANDS = [].freeze - SHORT_DESCRIPTION = 'Silently validates subscription, exits accordingly' - - private - - def execute - license.right? - end - end # Test - - class Show < self - NAME = 'show' - SUBCOMMANDS = [].freeze - SHORT_DESCRIPTION = 'Show subscription status' - - private - - def execute - license.either(method(:unlicensed), method(:licensed)) - end - - def licensed(subscription) - world.stdout.puts(subscription.description) - true - end - - def unlicensed(message) - world.stderr.puts(message) - false - end - end # Show - - SUBCOMMANDS = [Show, Test].freeze - end # Subscription - end # Command - end # CLI -end # Mutant diff --git a/lib/mutant/config.rb b/lib/mutant/config.rb index 3e1cb3bc3..fffdd8416 100644 --- a/lib/mutant/config.rb +++ b/lib/mutant/config.rb @@ -21,7 +21,8 @@ class Config :matcher, :mutation, :reporter, - :requires + :requires, + :usage ) %i[fail_fast].each do |name| @@ -87,7 +88,8 @@ def merge(other) jobs: other.jobs || jobs, matcher: matcher.merge(other.matcher), mutation: mutation.merge(other.mutation), - requires: requires + other.requires + requires: requires + other.requires, + usage: other.usage.merge(usage) ) end # rubocop:enable Metrics/AbcSize @@ -247,6 +249,10 @@ def self.integration_deprecation(reporter, hash) Transform::Hash::Key.new( transform: Transform::STRING_ARRAY, value: 'requires' + ), + Transform::Hash::Key.new( + transform: Usage::TRANSFORM, + value: 'usage' ) ], required: [] diff --git a/lib/mutant/license.rb b/lib/mutant/license.rb deleted file mode 100644 index 4e346e8df..000000000 --- a/lib/mutant/license.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Mutant - module License - NAME = 'mutant-license' - VERSION = ['>= 0.1', '< 0.3'].freeze - - # Load license - # - # @param [World] world - # - # @return [Either] - # - # @api private - def self.call(world) - load_mutant_license(world) - .fmap { license_path(world) } - .bind { |path| Subscription.load(world, world.json.load(path)) } - end - - def self.load_mutant_license(world) - Either - .wrap_error(LoadError) { world.gem_method.call(NAME, *VERSION) } - .lmap(&:message) - .lmap(&method(:check_for_rubygems_mutant_license)) - end - private_class_method :load_mutant_license - - def self.check_for_rubygems_mutant_license(message) - if message.include?('already activated mutant-license-0.0.0') - 'mutant-license gem from rubygems.org is a dummy' - else - message - end - end - private_class_method :check_for_rubygems_mutant_license - - def self.license_path(world) - world - .pathname - .new(world.gem.loaded_specs.fetch(NAME).full_gem_path) - .join('license.json') - end - private_class_method :license_path - end -end diff --git a/lib/mutant/license/subscription.rb b/lib/mutant/license/subscription.rb deleted file mode 100644 index 36729dc9b..000000000 --- a/lib/mutant/license/subscription.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -module Mutant - module License - class Subscription - include Anima.new(:licensed) - - FORMAT = <<~'MESSAGE' - %s subscription: - Licensed: - %s - MESSAGE - - FAILURE_FORMAT = <<~'MESSAGE' - Can not validate %s license. - Licensed: - %s - Present: - %s - MESSAGE - - # Load value into subscription - # - # @param [Object] value - # - # @return [Subscription] - def self.load(world, value) - { - 'com' => Commercial, - 'oss' => Opensource - }.fetch(value.fetch('type')) - .from_json(value.fetch('contents')) - .call(world) - end - - # Subscription self description - # - # @return [String] - def description - FORMAT % { - licensed: licensed.to_a.join("\n"), - subscription_name: subscription_name - } - end - - private - - def failure(expected, actual) - Either::Left.new(failure_message(expected, actual)) - end - - def success - Either::Right.new(self) - end - - def subscription_name - self.class::SUBSCRIPTION_NAME - end - - def failure_message(expected, actual) - FAILURE_FORMAT % { - actual: actual.any? ? actual.map(&:to_s).join("\n") : '[none]', - expected: expected.map(&:to_s).join("\n"), - subscription_name: subscription_name - } - end - end # Subscription - end # License -end # Mutant diff --git a/lib/mutant/license/subscription/commercial.rb b/lib/mutant/license/subscription/commercial.rb deleted file mode 100644 index a7fa6c461..000000000 --- a/lib/mutant/license/subscription/commercial.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -module Mutant - module License - class Subscription - class Commercial < self - include AbstractType - - def self.from_json(value) - { - 'individual' => Individual, - 'organization' => Organization - }.fetch(value.fetch('type', 'individual')).from_json(value) - end - - class Organization < self - SUBSCRIPTION_NAME = 'commercial organization' - - def self.from_json(value) - new(licensed: value.fetch('repositories').map(&Repository.public_method(:parse)).to_set) - end - - def call(world) - Repository.load_from_git(world).bind(&method(:check_subscription)) - end - - private - - def check_subscription(actual) - if licensed.any? { |repository| actual.any? { |other| repository.allow?(other) } } - success - else - failure(licensed, actual) - end - end - end - - class Individual < self - SUBSCRIPTION_NAME = 'commercial individual' - - class Author - include Anima.new(:email) - - alias_method :to_s, :email - public :to_s - end - - def self.from_json(value) - new(licensed: value.fetch('authors').to_set { |email| Author.new(email: email) }) - end - - def call(world) - candidates = candidates(world) - - if (licensed & candidates).any? - success - else - failure(licensed, candidates) - end - end - - private - - def candidates(world) - git_author(world).merge(commit_author(world)) - end - - def git_author(world) - capture(world, %w[git config --get user.email]) - end - - def commit_author(world) - capture(world, %w[git show --quiet --pretty=format:%ae]) - end - - def capture(world, command) - world - .capture_command(command) - .either(->(_) { EMPTY_ARRAY }, ->(status) { [Author.new(email: status.stdout.chomp)] }) - .to_set - end - end # Individual - end # Commercial - end # Subscription - end # License -end # Mutant diff --git a/lib/mutant/license/subscription/opensource.rb b/lib/mutant/license/subscription/opensource.rb deleted file mode 100644 index 0256a0072..000000000 --- a/lib/mutant/license/subscription/opensource.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module Mutant - module License - class Subscription - class Opensource < self - SUBSCRIPTION_NAME = 'opensource repository' - - def self.from_json(value) - new(licensed: value.fetch('repositories').map(&Repository.public_method(:parse)).to_set) - end - - def call(world) - Repository.load_from_git(world).bind(&method(:check_subscription)) - end - - private - - def check_subscription(actual) - if licensed.any? { |repository| actual.any? { |other| repository.allow?(other) } } - success - else - failure(licensed, actual) - end - end - - end # Opensource - end # Subscription - end # License -end # Mutant diff --git a/lib/mutant/license/subscription/repository.rb b/lib/mutant/license/subscription/repository.rb deleted file mode 100644 index 47d83a5cd..000000000 --- a/lib/mutant/license/subscription/repository.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -module Mutant - module License - class Subscription - class Repository - include Anima.new(:host, :path) - - REMOTE_REGEXP = /\A[^\t]+\t(?[^ ]+) \((?:fetch|push)\)\n\z/ - GIT_SSH_REGEXP = %r{\A[^@]+@(?[^:/]+)[:/](?.+?)(?:\.git)?\z} - GIT_HTTPS_REGEXP = %r{\Ahttps://(?[^/]+)/(?.+?)(?:\.git)?\z} - WILDCARD = '/*' - WILDCARD_RANGE = (..-WILDCARD.length) - - private_constant(*constants(false)) - - def to_s - [host, path].join('/') - end - - def self.load_from_git(world) - world - .capture_command(%w[git remote --verbose]) - .fmap { |status| parse_remotes(status.stdout) } - end - - def self.parse_remotes(input) - input.lines.map(&method(:parse_remote)).to_set - end - private_class_method :parse_remotes - - def self.parse(input) - host, path = *input.split('/', 2).map(&:downcase) - new(host: host, path: path) - end - - def self.parse_remote(input) - match = REMOTE_REGEXP.match(input) or - fail "Unmatched remote line: #{input.inspect}" - - parse_url(match[:url]) - end - private_class_method :parse_remote - - def self.parse_url(input) - match = GIT_SSH_REGEXP.match(input) || GIT_HTTPS_REGEXP.match(input) - - unless match - fail "Unmatched git remote URL: #{input.inspect}" - end - - new(host: match[:host], path: match[:path].downcase) - end - private_class_method :parse_url - - def allow?(other) - other.host.eql?(host) && path_match?(other.path) - end - - private - - def path_match?(other_path) - path.eql?(other_path) || wildcard_match?(path, other_path) || wildcard_match?(other_path, path) - end - - def wildcard_match?(left, right) - left.end_with?(WILDCARD) && right.start_with?(left[WILDCARD_RANGE]) - end - end # Repository - end # Subscription - end # License -end # Mutant diff --git a/lib/mutant/reporter/cli/printer/config.rb b/lib/mutant/reporter/cli/printer/config.rb index 0fc62fe1e..a0089ed21 100644 --- a/lib/mutant/reporter/cli/printer/config.rb +++ b/lib/mutant/reporter/cli/printer/config.rb @@ -15,6 +15,7 @@ class Config < self # # rubocop:disable Metrics/AbcSize def run + info 'Usage: %s', object.usage.value info 'Matcher: %s', object.matcher.inspect info 'Integration: %s', object.integration.name || 'null' info 'Jobs: %s', object.jobs || 'auto' diff --git a/lib/mutant/usage.rb b/lib/mutant/usage.rb new file mode 100644 index 000000000..b75ff1e16 --- /dev/null +++ b/lib/mutant/usage.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +module Mutant + class Usage + include Adamantium, Equalizer.new + + def value + self.class::VALUE + end + + def verify + Either::Right.new(nil) + end + + def message + self.class::MESSAGE + end + + def merge(_other) + self + end + + class Commercial < self + VALUE = 'commercial' + + MESSAGE = <<~'MESSAGE' + ## Commercial use + + `commercial` usage type requires [payment](https://github.com/mbj/mutant?tab=readme-ov-file#pricing), + If you are under an active payment plan you can use the commercial usage type on any + repository, including private ones. + + To use `commercial` usage type either specify `--usage commercial` on the command + line or use the config file key `usage`: + + ``` + # mutant.yml or config/mutant.yml + usage: commercial + ``` + MESSAGE + end + + class Opensource < self + VALUE = 'opensource' + + MESSAGE = <<~'MESSAGE' + ## Opensource use + + `opensource` usage is free while mutant is run on an opensource project. + Under that usage mutant does not require any kind of sign up or payment. + Set this usage type exclusively on public opensource projects. Any other + scenario requires payment. + Using the `opensource` usage type on private repotiories and or on commercial + code bases is not valid. + + To use `opensource` usage type either specify `--usage opensource` on the command + line or use the config file key `usage`: + + ``` + # mutant.yml or config/mutant.yml + usage: opensource + ``` + MESSAGE + end + + class Unknown < self + VALUE = 'unknown' + + MESSAGE = <<~"MESSAGE".freeze + # Unknown mutant usage type + + Mutant license usage is unspecified. Valid usage types are `opensource` or `commercial`. + + Usage can be specified via the `--usage` command line parameter or via the + config file under the `usage` key. + + #{Commercial::MESSAGE} + #{Opensource::MESSAGE} + This is a breaking change for users of the 0.11.x / 0.10.x mutant releases. + Sorry for that but its going to make future adoption much easier. + License gem is gone entirely. + MESSAGE + + def merge(other) + other + end + + def verify + Either::Left.new(MESSAGE) + end + end + + def self.parse(value) + { + 'commercial' => Either::Right.new(Commercial.new), + 'opensource' => Either::Right.new(Opensource.new) + }.fetch(value) { Either::Left.new("Unknown usage option: #{value.inspect}") } + end + + CLI_REGEXP = /\A(?:commercial|opensource)\z/ + + TRANSFORM = Transform::Sequence.new( + steps: [ + Transform::STRING, + Transform::Block.capture(:environment_variables, &method(:parse)) + ] + ) + end # Usage +end # Mutant diff --git a/lib/mutant/version.rb b/lib/mutant/version.rb index 61cb51dd0..d3548b251 100644 --- a/lib/mutant/version.rb +++ b/lib/mutant/version.rb @@ -2,5 +2,5 @@ module Mutant # Current mutant version - VERSION = '0.11.34' + VERSION = '0.12.0' end # Mutant diff --git a/mutant.yml b/mutant.yml index 1bfe2854f..bcfd598f9 100644 --- a/mutant.yml +++ b/mutant.yml @@ -1,7 +1,8 @@ --- +usage: commercial includes: - lib -integration: +integration: name: rspec requires: - mutant diff --git a/scripts/devloop.sh b/scripts/devloop.sh index 5176252b5..48c839274 100755 --- a/scripts/devloop.sh +++ b/scripts/devloop.sh @@ -1,5 +1,5 @@ while inotifywait lib/**/*.rb meta/**/*.rb spec/**/*.rb Gemfile Gemfile.shared mutant.gemspec; do - bundle exec mutant environment test run --fail-fast spec/unit spec/integration/mutant/rspec_spec.rb \ + bundle exec mutant environment test run --fail-fast spec/unit \ && bundle exec mutant run --fail-fast --since main --zombie -- 'Mutant*' \ && bundle exec rubocop done diff --git a/spec/integration/mutant/minitest_spec.rb b/spec/integration/mutant/minitest_spec.rb index b05395c0c..7dc5db4ce 100644 --- a/spec/integration/mutant/minitest_spec.rb +++ b/spec/integration/mutant/minitest_spec.rb @@ -2,7 +2,14 @@ RSpec.describe 'minitest integration', mutant: false do let(:base_cmd) do - %w[bundle exec mutant run -I test -I lib --require test_app --integration minitest] + %w[ + bundle exec mutant run + --include test + --include lib + --require test_app + --integration minitest + --usage opensource + ] end let(:gemfile) { 'Gemfile.minitest' } diff --git a/spec/integration/mutant/rspec_spec.rb b/spec/integration/mutant/rspec_spec.rb index 7544bffb5..2b9a407e9 100644 --- a/spec/integration/mutant/rspec_spec.rb +++ b/spec/integration/mutant/rspec_spec.rb @@ -2,7 +2,13 @@ RSpec.describe 'rspec integration', mutant: false do let(:base_cmd) do - %w[bundle exec mutant run -I lib --require test_app --integration rspec] + %w[ + bundle exec mutant run + --include lib + --integration rspec + --require test_app + --usage opensource + ] end %w[3.8 3.9 3.10 3.11 3.12 3.13].each do |version| diff --git a/spec/support/corpus.rb b/spec/support/corpus.rb index f3ccad50a..e9702031f 100644 --- a/spec/support/corpus.rb +++ b/spec/support/corpus.rb @@ -53,6 +53,7 @@ def verify_mutation_coverage --integration #{integration_name} --include lib --require #{name} + --usage opensource #{namespace}* ] + concurrency_limits ) diff --git a/spec/unit/mutant/cli_spec.rb b/spec/unit/mutant/cli_spec.rb index 90cec8900..3afed3896 100644 --- a/spec/unit/mutant/cli_spec.rb +++ b/spec/unit/mutant/cli_spec.rb @@ -2,14 +2,13 @@ RSpec.describe Mutant::CLI do describe '.parse' do - let(:env_config) { Mutant::Config::DEFAULT.with(jobs: 4) } - let(:events) { [] } - let(:expected_print_profile) { false } - let(:expected_zombie) { false } - let(:kernel) { class_double(Kernel) } - let(:stderr) { instance_double(IO, :stderr, tty?: false) } - let(:stdout) { instance_double(IO, :stdout, tty?: false) } - let(:timer) { instance_double(Mutant::Timer) } + let(:events) { [] } + let(:expected_print_profile) { false } + let(:expected_zombie) { false } + let(:kernel) { class_double(Kernel) } + let(:stderr) { instance_double(IO, :stderr, tty?: false) } + let(:stdout) { instance_double(IO, :stdout, tty?: false) } + let(:timer) { instance_double(Mutant::Timer) } let(:world) do instance_double( @@ -24,7 +23,8 @@ let(:load_config_config) do expected_cli_config.with( - mutation: Mutant::Mutation::Config::DEFAULT.merge(expected_cli_config.mutation) + mutation: Mutant::Mutation::Config::DEFAULT.merge(expected_cli_config.mutation), + usage: Mutant::Usage::Opensource.new ) end @@ -84,19 +84,6 @@ def apply end end - shared_context 'license validation' do - let(:license_validation_event) do - [:license, :call, world] - end - - before do - allow(Mutant::License).to receive(:call) do |world| - events << [:license, :call, world] - license_result - end - end - end - @tests = [] @test_klass = @@ -117,7 +104,7 @@ def self.make # rubocop:disable Metrics/MethodLength def self.main_body <<~MESSAGE.strip - usage: mutant [options] + usage: mutant [options] Summary: mutation testing engine main command @@ -132,10 +119,9 @@ def self.main_body Available subcommands: - run - Run code analysis - environment - Environment subcommands - subscription - Subscription subcommands - util - Utility subcommands + run - Run code analysis + environment - Environment subcommands + util - Utility subcommands MESSAGE end # rubocop:enable Metrics/MethodLength @@ -218,11 +204,11 @@ def self.main_body context 'missing required subcommand' do message = <<~MESSAGE - mutant subscription: Missing required subcommand! + mutant environment: Missing required subcommand! - usage: mutant subscription [options] + usage: mutant environment [options] - Summary: Subscription subcommands + Summary: Environment subcommands mutant version: #{Mutant::VERSION} @@ -235,21 +221,13 @@ def self.main_body Available subcommands: - show - Show subscription status - test - Silently validates subscription, exits accordingly + subject - Subject subcommands + show - Display environment without coverage analysis + irb - Run irb with mutant environment loaded + test - test subcommands MESSAGE - let(:arguments) { %w[subscription] } - - it 'returns expected message' do - expect(apply).to eql(left(message)) - end - end - - context 'unexpected extra argument' do - message = 'mutant subscription show: Does not expect extra arguments' - - let(:arguments) { %w[subscription show extra-argument] } + let(:arguments) { %w[environment] } it 'returns expected message' do expect(apply).to eql(left(message)) @@ -258,9 +236,9 @@ def self.main_body make do message = <<~MESSAGE - usage: mutant subscription show [options] + usage: mutant environment irb [options] - Summary: Show subscription status + Summary: Run irb with mutant environment loaded mutant version: #{Mutant::VERSION} @@ -270,10 +248,43 @@ def self.main_body --version Print mutants version --profile Profile mutant execution --zombie Run mutant zombified + + + Environment: + -I, --include DIRECTORY Add DIRECTORY to $LOAD_PATH + -r, --require NAME Require file with NAME + --env KEY=VALUE Set environment variable + + + Runner: + --fail-fast Fail fast + -j, --jobs NUMBER Number of kill jobs. Defaults to number of processors. + -t, --mutation-timeout NUMBER Per mutation analysis timeout + + + Integration: + --use INTEGRATION deprecated alias for --integration + --integration NAME Use test integration with NAME + --integration-argument ARGUMENT + Pass ARGUMENT to integration + + + Matcher: + --ignore-subject EXPRESSION Ignore subjects that match EXPRESSION as prefix + --start-subject EXPRESSION Start mutation testing at a specific subject + --since REVISION Only select subjects touched since REVISION + + + Reporting: + --print-warnings Print warnings + + + Usage: + --usage USAGE_TYPE License usage: opensource|commercial MESSAGE { - arguments: %w[subscription show --help], + arguments: %w[environment irb --help], expected_events: [[:stdout, :puts, message]], expected_exit: true, expected_print_profile: false, @@ -324,6 +335,10 @@ def self.main_body Reporting: --print-warnings Print warnings + + + Usage: + --usage USAGE_TYPE License usage: opensource|commercial MESSAGE { @@ -378,6 +393,10 @@ def self.main_body Reporting: --print-warnings Print warnings + + + Usage: + --usage USAGE_TYPE License usage: opensource|commercial MESSAGE { @@ -432,6 +451,10 @@ def self.main_body Reporting: --print-warnings Print warnings + + + Usage: + --usage USAGE_TYPE License usage: opensource|commercial MESSAGE { @@ -625,84 +648,11 @@ def self.main_body end end - context 'subscription show' do - let(:arguments) { %w[subscription show] } - - include_context 'license validation' - - context 'on valid license' do - let(:expected_exit) { true } - let(:license_result) { right(subscription) } - - let(:expected_events) do - [ - license_validation_event, - [:stdout, :puts, 'License-Description'] - ] - end - - let(:subscription) do - instance_double( - Mutant::License::Subscription, - description: 'License-Description' - ) - end - - include_examples 'CLI run' - end - - context 'on invalid license' do - let(:expected_exit) { false } - let(:license_result) { left('error-message') } - - let(:expected_events) do - [ - license_validation_event, - [:stderr, :puts, 'error-message'] - ] - end - - include_examples 'CLI run' - end - end - - context 'license display test' do - let(:arguments) { %w[subscription test] } - - let(:expected_events) do - [ [:license, :call, world ] ] - end - - include_context 'license validation' - - context 'on valid license' do - let(:expected_exit) { true } - let(:license_result) { right(subscription) } - - let(:subscription) do - instance_double( - Mutant::License::Subscription, - description: 'License-Description' - ) - end - - include_examples 'CLI run' - end - - context 'on invalid license' do - let(:expected_exit) { false } - let(:license_result) { left('error-message') } - - include_examples 'CLI run' - end - end - shared_context 'environment' do let(:arguments) { %w[run] } let(:bootstrap_result) { right(env) } let(:env_result) { instance_double(Mutant::Result::Env, success?: true) } let(:expected_exit) { true } - let(:license_result) { right(subscription) } let(:runner_result) { right(env_result) } let(:subjects) { [subject_a] } @@ -759,8 +709,6 @@ def self.main_body ) end - include_context 'license validation' - before do allow(Mutant::Config).to receive(:load) do |**attributes| events << [:load_config, attributes.inspect] @@ -1046,6 +994,7 @@ def self.main_body let(:expected_message) do <<~'MESSAGE' Mutant environment: + Usage: opensource Matcher: # Integration: null Jobs: auto @@ -1092,53 +1041,56 @@ def self.main_body context 'run' do include_context 'environment' - context 'on invalid license' do - let(:expected_exit) { false } - let(:license_result) { left('license-error') } - - let(:expected_events) do + let(:expected_events) do + [ + %i[ + record + config + ], [ - license_validation_event, - [:stderr, :puts, 'license-error'] + :load_config, + { cli_config: expected_cli_config, world: world }.inspect + ], + [ + :bootstrap, + Mutant::Env.empty(world, bootstrap_config).inspect + ], + [ + :runner, + env.inspect ] - end - - include_examples 'CLI run' + ] end - context 'on valid license' do - let(:subscription) do - instance_double( - Mutant::License::Subscription, - description: 'License-Description' - ) - end + context 'on runner fail' do + let(:expected_exit) { false } + let(:runner_result) { left('runner failure') } let(:expected_events) do [ - license_validation_event, - %i[ - record - config - ], + *super(), [ - :load_config, - { cli_config: expected_cli_config, world: world }.inspect - ], - [ - :bootstrap, - Mutant::Env.empty(world, bootstrap_config).inspect - ], - [ - :runner, - env.inspect + :stderr, + :puts, + 'runner failure' ] ] end - context 'on runner fail' do - let(:expected_exit) { false } - let(:runner_result) { left('runner failure') } + include_examples 'CLI run' + end + + context 'on runner success with unsuccessful result' do + context 'on alive mutations' do + let(:expected_exit) { false } + let(:runner_result) { right(env_result) } + + let(:env_result) do + instance_double( + Mutant::Result::Env, + success?: false + ) + end let(:expected_events) do [ @@ -1146,7 +1098,7 @@ def self.main_body [ :stderr, :puts, - 'runner failure' + 'Uncovered mutations detected, exiting nonzero!' ] ] end @@ -1154,245 +1106,306 @@ def self.main_body include_examples 'CLI run' end - context 'on runner success with unsuccessful result' do - context 'on alive mutations' do - let(:expected_exit) { false } - let(:runner_result) { right(env_result) } - - let(:env_result) do - instance_double( - Mutant::Result::Env, - success?: false - ) - end + context 'on not having found tests' do + let(:available_tests) { [] } - let(:expected_events) do + let(:expected_events) do + [ + *super()[..-2], [ - *super(), - [ - :stderr, - :puts, - 'Uncovered mutations detected, exiting nonzero!' - ] + :stderr, + :puts, + Mutant::CLI::Command::Environment::Run::NO_TESTS_MESSAGE ] - end - - include_examples 'CLI run' + ] end - context 'on not having found tests' do - let(:available_tests) { [] } + let(:expected_exit) { false } - let(:expected_events) do - [ - *super()[..-2], - [ - :stderr, - :puts, - Mutant::CLI::Command::Environment::Run::NO_TESTS_MESSAGE - ] - ] - end + include_examples 'CLI run' + end + end - let(:expected_exit) { false } + context 'with valid subject expression' do + let(:arguments) { super() + ['CLISubject'] } - include_examples 'CLI run' - end + let(:expected_cli_config) do + super().with( + matcher: super().matcher.with( + subjects: expected_bootstrap_subjects + ) + ) end - context 'with valid subject expression' do - let(:arguments) { super() + ['CLISubject'] } + let(:expected_bootstrap_subjects) { [parse_expression('CLISubject')] } - let(:expected_cli_config) do - super().with( - matcher: super().matcher.with( - subjects: expected_bootstrap_subjects - ) - ) - end + include_examples 'CLI run' + end - let(:expected_bootstrap_subjects) { [parse_expression('CLISubject')] } + context 'on invalid --usage' do + let(:arguments) { %w[run --usage invalid] } - include_examples 'CLI run' - end + it 'returns expected error' do + expect(apply).to eql(left(<<~"MESSAGE")) + mutant run: invalid argument: --usage invalid - context 'without subject expressions' do - let(:expected_cli_config) do - super().with( - matcher: super().matcher.with( - subjects: expected_bootstrap_subjects - ) - ) - end + usage: mutant run [options] - let(:expected_bootstrap_subjects) { [] } + Summary: Run code analysis - include_examples 'CLI run' - end + mutant version: #{Mutant::VERSION} - context 'with valid start-subject expression' do - let(:arguments) do - super() + ['--start-subject', 'Foo#bar', '--start-subject', 'Foo#baz'] - end + Global Options: - let(:expected_cli_config) do - super().with( - matcher: super().matcher.with( - start_expressions: %w[Foo#bar Foo#baz].map(&method(:parse_expression)) - ) - ) - end + --help Print help + --version Print mutants version + --profile Profile mutant execution + --zombie Run mutant zombified - include_examples 'CLI run' + + Environment: + -I, --include DIRECTORY Add DIRECTORY to $LOAD_PATH + -r, --require NAME Require file with NAME + --env KEY=VALUE Set environment variable + + + Runner: + --fail-fast Fail fast + -j, --jobs NUMBER Number of kill jobs. Defaults to number of processors. + -t, --mutation-timeout NUMBER Per mutation analysis timeout + + + Integration: + --use INTEGRATION deprecated alias for --integration + --integration NAME Use test integration with NAME + --integration-argument ARGUMENT + Pass ARGUMENT to integration + + + Matcher: + --ignore-subject EXPRESSION Ignore subjects that match EXPRESSION as prefix + --start-subject EXPRESSION Start mutation testing at a specific subject + --since REVISION Only select subjects touched since REVISION + + + Reporting: + --print-warnings Print warnings + + + Usage: + --usage USAGE_TYPE License usage: opensource|commercial + MESSAGE end + end - context 'with valid ignore-subject expression' do - let(:arguments) do - super() + ['--ignore-subject', 'Foo#bar', '--ignore-subject', 'Foo#baz'] - end + context 'on --usage commercial' do + let(:arguments) { %w[run --usage opensource] } - let(:expected_cli_config) do - super().with( - matcher: super().matcher.with( - ignore: %w[Foo#bar Foo#baz].map(&method(:parse_expression)) - ) - ) - end + let(:expected_cli_config) do + super().with(usage: Mutant::Usage::Opensource.new) + end - include_examples 'CLI run' + include_examples 'CLI run' + end + + context 'on absent --usage' do + let(:arguments) { %w[run] } + let(:expected_exit) { false } + + let(:load_config_config) do + expected_cli_config.with( + mutation: Mutant::Mutation::Config::DEFAULT.merge(expected_cli_config.mutation), + usage: Mutant::Usage::Unknown.new + ) end - context 'with --include option' do - let(:arguments) do - super() + %w[ - --include include-cli-a - --include include-cli-b + let(:expected_events) do + [ + %i[ + record + config + ], + [ + :load_config, + { cli_config: expected_cli_config, world: world }.inspect + ], + [ + :bootstrap, + Mutant::Env.empty(world, bootstrap_config).inspect + ], + [ + :stderr, + :puts, + Mutant::Usage::Unknown::MESSAGE ] - end + ] + end - let(:expected_cli_config) do - super().with( - includes: %w[ - include-cli-a - include-cli-b - ] - ) - end + include_examples 'CLI run' + end - include_examples 'CLI run' + context 'with valid start-subject expression' do + let(:arguments) do + super() + ['--start-subject', 'Foo#bar', '--start-subject', 'Foo#baz'] end - context 'with --require option' do - let(:arguments) { super() + %w[--require require-cli] } - - let(:expected_cli_config) do - super().with( - requires: %w[require-cli] + let(:expected_cli_config) do + super().with( + matcher: super().matcher.with( + start_expressions: %w[Foo#bar Foo#baz].map(&method(:parse_expression)) ) - end - - include_examples 'CLI run' + ) end - context 'with --env option' do - let(:arguments) { super() + %W[--env #{argument}] } + include_examples 'CLI run' + end - context 'on valid env syntax' do - let(:argument) { 'foo=bar' } + context 'with valid ignore-subject expression' do + let(:arguments) do + super() + ['--ignore-subject', 'Foo#bar', '--ignore-subject', 'Foo#baz'] + end - let(:expected_cli_config) do - super().with(environment_variables: { 'foo' => 'bar' }) - end + let(:expected_cli_config) do + super().with( + matcher: super().matcher.with( + ignore: %w[Foo#bar Foo#baz].map(&method(:parse_expression)) + ) + ) + end - include_examples 'CLI run' - end + include_examples 'CLI run' + end - context 'on invalid env syntax' do - let(:argument) { 'foobar' } + context 'with --include option' do + let(:arguments) do + super() + %w[ + --include include-cli-a + --include include-cli-b + ] + end - it 'raises expected error' do - expect { apply }.to raise_error(RuntimeError, 'Invalid env variable: "foobar"') - end - end + let(:expected_cli_config) do + super().with( + includes: %w[ + include-cli-a + include-cli-b + ] + ) end - context 'with --jobs option' do - let(:arguments) { super() + %w[--jobs 10] } - let(:expected_cli_config) { super().with(jobs: 10) } + include_examples 'CLI run' + end - include_examples 'CLI run' + context 'with --require option' do + let(:arguments) { super() + %w[--require require-cli] } + + let(:expected_cli_config) do + super().with( + requires: %w[require-cli] + ) end - context 'with --mutation-timeout option' do - let(:arguments) { super() + %w[--mutation-timeout 10] } + include_examples 'CLI run' + end + + context 'with --env option' do + let(:arguments) { super() + %W[--env #{argument}] } + + context 'on valid env syntax' do + let(:argument) { 'foo=bar' } let(:expected_cli_config) do - super().with(mutation: super().mutation.with(timeout: 10.0)) + super().with(environment_variables: { 'foo' => 'bar' }) end include_examples 'CLI run' end - context 'with --fail-fast option' do - let(:arguments) { super() + %w[--fail-fast] } - let(:expected_cli_config) { super().with(fail_fast: true) } + context 'on invalid env syntax' do + let(:argument) { 'foobar' } - include_examples 'CLI run' + it 'raises expected error' do + expect { apply }.to raise_error(RuntimeError, 'Invalid env variable: "foobar"') + end end + end - context 'with --use option' do - let(:arguments) { super() + ['--use', 'cli-integration'] } + context 'with --jobs option' do + let(:arguments) { super() + %w[--jobs 10] } + let(:expected_cli_config) { super().with(jobs: 10) } - let(:expected_cli_config) do - super().with(integration: super().integration.with(name: 'cli-integration')) - end + include_examples 'CLI run' + end - include_examples 'CLI run' + context 'with --mutation-timeout option' do + let(:arguments) { super() + %w[--mutation-timeout 10] } + + let(:expected_cli_config) do + super().with(mutation: super().mutation.with(timeout: 10.0)) end - context 'with --integration option' do - let(:arguments) { super() + ['--integration', 'cli-integration'] } + include_examples 'CLI run' + end - let(:expected_cli_config) do - super().with(integration: super().integration.with(name: 'cli-integration')) - end + context 'with --fail-fast option' do + let(:arguments) { super() + %w[--fail-fast] } + let(:expected_cli_config) { super().with(fail_fast: true) } - include_examples 'CLI run' + include_examples 'CLI run' + end + + context 'with --use option' do + let(:arguments) { super() + ['--use', 'cli-integration'] } + + let(:expected_cli_config) do + super().with(integration: super().integration.with(name: 'cli-integration')) end - context 'with --integration-argument option' do - let(:arguments) do - super() + %w[ - --integration-argument cli-integration-argument-a - --integration-argument cli-integration-argument-b - ] - end + include_examples 'CLI run' + end - let(:expected_cli_config) do - super().with( - integration: super().integration.with( - arguments: %w[ - cli-integration-argument-a - cli-integration-argument-b - ] - ) - ) - end + context 'with --integration option' do + let(:arguments) { super() + ['--integration', 'cli-integration'] } - include_examples 'CLI run' + let(:expected_cli_config) do + super().with(integration: super().integration.with(name: 'cli-integration')) + end + + include_examples 'CLI run' + end + + context 'with --integration-argument option' do + let(:arguments) do + super() + %w[ + --integration-argument cli-integration-argument-a + --integration-argument cli-integration-argument-b + ] end - context 'with --since option' do - let(:arguments) { super() + ['--since', 'reference'] } + let(:expected_cli_config) do + super().with( + integration: super().integration.with( + arguments: %w[ + cli-integration-argument-a + cli-integration-argument-b + ] + ) + ) + end - let(:expected_cli_config) do - diff = Mutant::Repository::Diff.new(to: 'reference', world: world) + include_examples 'CLI run' + end - super().with(matcher: super().matcher.with(diffs: [diff])) - end + context 'with --since option' do + let(:arguments) { super() + ['--since', 'reference'] } - include_examples 'CLI run' + let(:expected_cli_config) do + diff = Mutant::Repository::Diff.new(to: 'reference', world: world) + + super().with(matcher: super().matcher.with(diffs: [diff])) end + + include_examples 'CLI run' end end end diff --git a/spec/unit/mutant/config_spec.rb b/spec/unit/mutant/config_spec.rb index 95313f07a..7b5315ecd 100644 --- a/spec/unit/mutant/config_spec.rb +++ b/spec/unit/mutant/config_spec.rb @@ -113,6 +113,37 @@ def expect_value(value) end end + context 'merging usage' do + let(:key) { :usage } + + context 'when merging into unknown' do + let(:original_value) { Mutant::Usage::Unknown.new } + let(:other_value) { Mutant::Usage::Commercial.new } + + it 'merges with preference for other' do + expect_value(other_value) + end + end + + context 'when merging into commercial' do + let(:original_value) { Mutant::Usage::Commercial.new } + let(:other_value) { Mutant::Usage::Unknown.new } + + it 'merges with preference for other' do + expect_value(original_value) + end + end + + context 'when merging into opensource' do + let(:original_value) { Mutant::Usage::Opensource.new } + let(:other_value) { Mutant::Usage::Unknown.new } + + it 'merges with preference for other' do + expect_value(original_value) + end + end + end + context 'merging integration' do let(:key) { :integration } diff --git a/spec/unit/mutant/license/repository_spec.rb b/spec/unit/mutant/license/repository_spec.rb deleted file mode 100644 index ac9a5c138..000000000 --- a/spec/unit/mutant/license/repository_spec.rb +++ /dev/null @@ -1,283 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Mutant::License::Subscription::Repository do - def self.it_fails(expected) - it 'failse with exception' do - expect { apply }.to raise_error(expected) - end - end - - def self.it_is_successful - it 'returns expected repositories' do - expect(apply).to eql(right(repositories)) - end - end - - def self.it_fails_with_message(expected) - it 'returns expected message' do - expect(apply).to eql(left(expected)) - end - end - - describe '.parse' do - def apply - described_class.parse(input) - end - - let(:expected) do - described_class.new(host: 'github.com', path: 'mbj/mutant') - end - - context 'one to one' do - let(:input) { 'github.com/mbj/mutant' } - - it 'returns expected value' do - expect(apply).to eql(expected) - end - end - - context 'downcase' do - let(:input) { 'github.com/Mbj/Mutant' } - - it 'returns expected value' do - expect(apply).to eql(expected) - end - end - end - - describe '#to_s' do - def apply - described_class.new(host: 'github.com', path: 'mbj/mutant').to_s - end - - it 'returns expected value' do - expect(apply).to eql('github.com/mbj/mutant') - end - end - - describe '.load_from_git' do - def apply - described_class.load_from_git(world) - end - - before do - allow(world).to receive(:capture_command, &commands.public_method(:fetch)) - end - - let(:allowed_repositories) { %w[github.com/mbj/mutant] } - let(:git_remote_result) { right(git_remote_status) } - let(:world) { instance_double(Mutant::World) } - - let(:git_remote_status) do - instance_double(Mutant::World::CommandStatus, stdout: git_remote_stdout) - end - - let(:git_remote_stdout) do - <<~REMOTE - origin\tgit@github.com:mbj/Mutant (fetch) - origin\tgit@github.com:mbj/Mutant (push) - REMOTE - end - - let(:commands) do - { - %w[git remote --verbose] => git_remote_result - } - end - - let(:repositories) do - [ - described_class.new( - host: 'github.com', - path: 'mbj/mutant' - ) - ].to_set - end - - context 'on different casing' do - let(:allowed_repositories) { %w[github.com/MBJ/MUTANT] } - - it_is_successful - end - - context 'on ssh url without protocol and without suffix' do - it_is_successful - end - - context 'on ssh url with protocol and without suffix' do - let(:git_remote_stdout) do - <<~REMOTE - origin\tssh://git@github.com/mbj/mutant (fetch) - origin\tssh://git@github.com/mbj/mutant (push) - REMOTE - end - - it_is_successful - end - - context 'on ssh url with protocol and suffix' do - let(:git_remote_stdout) do - <<~REMOTE - origin\tssh://git@github.com/mbj/mutant.git (fetch) - origin\tssh://git@github.com/mbj/mutant.git (push) - REMOTE - end - - it_is_successful - end - - context 'on https url without suffix' do - let(:git_remote_stdout) do - <<~REMOTE - origin\thttps://github.com/mbj/mutant (fetch) - origin\thttps://github.com/mbj/mutant (push) - REMOTE - end - - it_is_successful - end - - context 'on multiple different urls' do - let(:git_remote_stdout) do - <<~REMOTE - origin\thttps://github.com/mbj/mutant (fetch) - origin\thttps://github.com/mbj/mutant (push) - origin\thttps://github.com/mbj/unparser (fetch) - origin\thttps://github.com/mbj/unparser (push) - REMOTE - end - - let(:repositories) do - [ - described_class.new( - host: 'github.com', - path: 'mbj/mutant' - ), - described_class.new( - host: 'github.com', - path: 'mbj/unparser' - ) - ].to_set - end - - it_is_successful - end - - context 'on https url with .git suffix' do - let(:git_remote_stdout) do - <<~REMOTE - origin\thttps://github.com/mbj/mutant.git (fetch) - origin\thttps://github.com/mbj/mutant.git (push) - REMOTE - end - - it_is_successful - end - - context 'when git remote line cannot be parsed' do - let(:git_remote_stdout) { "some-bad-remote-line\n" } - - it_fails 'Unmatched remote line: "some-bad-remote-line\n"' - end - - context 'when git remote url cannot be parsed' do - let(:git_remote_stdout) { "some-unknown\thttp://github.com/mbj/mutant (fetch)\n" } - - it_fails 'Unmatched git remote URL: "http://github.com/mbj/mutant"' - end - end - - describe 'allow?' do - subject { described_class.new(host: 'github.com', path: 'mbj/mutant') } - - def self.it_returns_false - it 'returns false' do - expect(apply).to be(false) - end - end - - def self.it_returns_true - it 'returns false' do - expect(apply).to be(true) - end - end - - def apply - subject.allow?(other) - end - - context 'when repository is allowed' do - context 'via full match' do - let(:other) { described_class.new(host: 'github.com', path: 'mbj/mutant') } - - it_returns_true - end - - context 'via path pattern on right hand site' do - let(:other) { described_class.new(host: 'github.com', path: 'mbj/*') } - - it_returns_true - end - - context 'via path pattern on left hand site' do - subject { described_class.new(host: 'github.com', path: 'mbj/*') } - let(:other) { described_class.new(host: 'github.com', path: 'mbj/mutant') } - - it_returns_true - end - - context 'via path pattern on both sides' do - subject { described_class.new(host: 'github.com', path: 'mbj/*') } - let(:other) { described_class.new(host: 'github.com', path: 'mbj/*') } - - it_returns_true - end - end - - context 'when repository is not allowed' do - context 'on different host' do - let(:other) { described_class.new(host: 'gitlab.com', path: 'mbj/mutant') } - - it_returns_false - end - - context 'on different depository last empty' do - subject { described_class.new(host: 'github.com', path: 'b') } - - let(:other) { described_class.new(host: 'github.com', path: 'a') } - - it_returns_false - end - - context 'on different depository last chars' do - let(:other) { described_class.new(host: 'github.com', path: 'mbj/mutantb') } - - it_returns_false - end - - context 'on different depository partial overlap' do - let(:other) { described_class.new(host: 'github.com', path: 'mb/*') } - - it_returns_false - end - - context 'on different org path pattern' do - let(:other) { described_class.new(host: 'github.com', path: 'schirp-dso/*') } - - it_returns_false - end - - context 'on different org' do - let(:other) { described_class.new(host: 'github.com', path: 'schirp-dso/mutant') } - - it_returns_false - end - - context 'on different repo' do - let(:other) { described_class.new(host: 'github.com', path: 'mbj/unparser') } - - it_returns_false - end - end - end -end diff --git a/spec/unit/mutant/license/subscription_spec.rb b/spec/unit/mutant/license/subscription_spec.rb deleted file mode 100644 index f46818a5b..000000000 --- a/spec/unit/mutant/license/subscription_spec.rb +++ /dev/null @@ -1,381 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Mutant::License::Subscription do - describe '#description' do - def apply - object.description - end - - context 'on commercial license' do - context 'on individual license' do - let(:object) do - described_class::Commercial::Individual.new( - licensed: [ - described_class::Commercial::Individual::Author.new(email: 'mbj@schirp-dso.com'), - described_class::Commercial::Individual::Author.new(email: 'other@schirp-dso.com') - ].to_set - ) - end - - it 'returns expected description' do - expect(apply).to eql(<<~'MESSAGE') - commercial individual subscription: - Licensed: - mbj@schirp-dso.com - other@schirp-dso.com - MESSAGE - end - end - - context 'on organization license' do - let(:object) do - described_class::Commercial::Organization.new( - licensed: [ - described_class::Repository.new(host: 'github.com', path: 'mbj/*'), - described_class::Repository.new(host: 'github.com', path: 'schirp-dso/*') - ].to_set - ) - end - - it 'returns expected description' do - expect(apply).to eql(<<~'MESSAGE') - commercial organization subscription: - Licensed: - github.com/mbj/* - github.com/schirp-dso/* - MESSAGE - end - end - end - - context 'on opensource license' do - let(:object) do - described_class::Opensource.new( - licensed: [ - described_class::Repository.new(host: 'github.com', path: 'mbj/mutant'), - described_class::Repository.new(host: 'github.com', path: 'mbj/unparser') - ].to_set - ) - end - - it 'returns expected description' do - expect(apply).to eql(<<~'MESSAGE') - opensource repository subscription: - Licensed: - github.com/mbj/mutant - github.com/mbj/unparser - MESSAGE - end - end - end - - describe '.load' do - def apply - described_class.load(world, license_json) - end - - let(:world) { instance_double(Mutant::World) } - - before do - allow(world).to receive(:capture_command, &commands.public_method(:fetch)) - end - - def self.it_fails(expected) - it 'failse with exception' do - expect { apply }.to raise_error(expected) - end - end - - def self.it_is_successful - it 'allows usage' do - expect(apply).to eql(right(subscription)) - end - end - - def self.it_fails_with_message(expected) - it 'returns expected message' do - expect(apply).to eql(left(expected)) - end - end - - describe 'on opensource license' do - let(:git_remote_result) { right(git_remote_status) } - let(:allowed_repositories) { %w[github.com/mbj/mutant] } - - let(:git_remote_status) do - instance_double(Mutant::World::CommandStatus, stdout: git_remote_stdout) - end - - let(:license_json) do - { - 'type' => 'oss', - 'contents' => { - 'repositories' => allowed_repositories - } - } - end - - let(:commands) do - { - %w[git remote --verbose] => git_remote_result - } - end - - let(:git_remote_stdout) do - <<~REMOTE - origin\tgit@github.com:mbj/mutant (fetch) - origin\tgit@github.com:mbj/mutant (push) - REMOTE - end - - context 'on one of many match' do - let(:allowed_repositories) { %w[github.com/mbj/something github.com/mbj/mutant] } - - let(:git_remote_stdout) do - <<~REMOTE - origin\tgit@github.com:mbj/mutant (fetch) - origin\tgit@github.com:mbj/mutant (push) - origin\tgit@github.com:mbj/unparser (fetch) - origin\tgit@github.com:mbj/unparser (push) - REMOTE - end - - let(:subscription) do - Mutant::License::Subscription::Opensource.new( - licensed: [ - Mutant::License::Subscription::Repository.new( - host: 'github.com', - path: 'mbj/mutant' - ), - Mutant::License::Subscription::Repository.new( - host: 'github.com', - path: 'mbj/something' - ) - ].to_set - ) - end - it_is_successful - end - - context 'on direct match' do - let(:subscription) do - Mutant::License::Subscription::Opensource.new( - licensed: [ - Mutant::License::Subscription::Repository.new( - host: 'github.com', - path: 'mbj/mutant' - ) - ].to_set - ) - end - it_is_successful - end - - context 'when repository is not whitelisted' do - let(:allowed_repositories) { %w[gitlab.com/mbj/mutant] } - - it_fails_with_message(<<~'MESSAGE') - Can not validate opensource repository license. - Licensed: - gitlab.com/mbj/mutant - Present: - github.com/mbj/mutant - MESSAGE - end - end - - describe 'on commercial license' do - context 'on organization licenses' do - let(:git_remote_result) { right(git_remote_status) } - - let(:git_remote_status) do - instance_double(Mutant::World::CommandStatus, stdout: git_remote_stdout) - end - - let(:license_json) do - { - 'type' => 'com', - 'contents' => { - 'type' => 'organization', - 'repositories' => allowed_repositories - } - } - end - - let(:commands) do - { - %w[git remote --verbose] => git_remote_result - } - end - - let(:git_remote_stdout) do - <<~REMOTE - origin\tgit@github.com:mbj/Mutant (fetch) - origin\tgit@github.com:mbj/Mutant (push) - REMOTE - end - - let(:allowed_repositories) { %w[github.com/mbj/mutant] } - - context 'on a direct match' do - let(:subscription) do - Mutant::License::Subscription::Commercial::Organization.new( - licensed: [ - Mutant::License::Subscription::Repository.new( - host: 'github.com', - path: 'mbj/mutant' - ) - ].to_set - ) - end - - it_is_successful - end - - context 'on a one of many match' do - let(:allowed_repositories) { %w[github.com/mbj/something github.com/mbj/mutant] } - - let(:git_remote_stdout) do - <<~REMOTE - origin\tgit@github.com:mbj/mutant (fetch) - origin\tgit@github.com:mbj/mutant (push) - origin\tgit@github.com:mbj/unparser (fetch) - origin\tgit@github.com:mbj/unparser (push) - REMOTE - end - - let(:subscription) do - Mutant::License::Subscription::Commercial::Organization.new( - licensed: [ - Mutant::License::Subscription::Repository.new( - host: 'github.com', - path: 'mbj/mutant' - ), - Mutant::License::Subscription::Repository.new( - host: 'github.com', - path: 'mbj/something' - ) - ].to_set - ) - end - - it_is_successful - end - - context 'when repository is not whitelisted' do - let(:allowed_repositories) { %w[gitlab.com/mbj/mutant] } - - it_fails_with_message(<<~'MESSAGE') - Can not validate commercial organization license. - Licensed: - gitlab.com/mbj/mutant - Present: - github.com/mbj/mutant - MESSAGE - end - end - - shared_examples 'individual licenses' do - let(:git_config_author) { "customer-a@example.com\n" } - let(:git_config_result) { Mutant::Either::Right.new(git_config_author_status) } - let(:git_show_author) { "customer-b@example.com\n" } - let(:git_show_result) { Mutant::Either::Right.new(git_show_author_status) } - - let(:git_config_author_status) do - instance_double( - Mutant::World::CommandStatus, - stdout: git_config_author - ) - end - - let(:git_show_author_status) do - instance_double( - Mutant::World::CommandStatus, - stdout: git_show_author - ) - end - - let(:licensed_authors) do - %w[ - customer-a@example.com - customer-b@example.com - ] - end - - let(:commands) do - { - %w[git config --get user.email] => git_config_result, - %w[git show --quiet --pretty=format:%ae] => git_show_result - } - end - - let(:subscription) do - Mutant::License::Subscription::Commercial::Individual.new( - licensed: licensed_authors - .to_set { |email| Mutant::License::Subscription::Commercial::Individual::Author.new(email: email) } - ) - end - - context 'when author is whitelisted' do - it_is_successful - end - - context 'when author is not whitelisted' do - let(:licensed_authors) { %w[customer-c@example.com] } - - it_fails_with_message(<<~'MESSAGE') - Can not validate commercial individual license. - Licensed: - customer-c@example.com - Present: - customer-a@example.com - customer-b@example.com - MESSAGE - end - - context 'when author cannot be found in commit or config' do - let(:git_config_result) { Mutant::Either::Left.new('fatal: error') } - let(:git_show_result) { Mutant::Either::Left.new('fatal: error') } - - it_fails_with_message(<<~'MESSAGE') - Can not validate commercial individual license. - Licensed: - customer-a@example.com - customer-b@example.com - Present: - [none] - MESSAGE - end - end - - context 'on individual license' do - context 'via legacy contents missing type field' do - let(:license_json) do - { - 'type' => 'com', - 'contents' => { - 'authors' => licensed_authors - } - } - end - - include_examples 'individual licenses' - end - - context 'via contents type field' do - let(:license_json) do - { - 'type' => 'com', - 'contents' => { - 'type' => 'individual', - 'authors' => licensed_authors - } - } - end - - include_examples 'individual licenses' - end - end - end - end -end diff --git a/spec/unit/mutant/license_spec.rb b/spec/unit/mutant/license_spec.rb deleted file mode 100644 index 7800bae69..000000000 --- a/spec/unit/mutant/license_spec.rb +++ /dev/null @@ -1,125 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Mutant::License do - def apply - described_class.call(world) - end - - let(:gem) { class_double(Gem, loaded_specs: loaded_specs) } - let(:gem_method) { instance_double(Method) } - let(:gem_path) { '/path/to/mutant-license' } - let(:gem_pathname) { instance_double(Pathname) } - let(:json) { class_double(JSON) } - let(:kernel) { class_double(Kernel) } - let(:license_json) { instance_double(Object) } - let(:license_pathname) { instance_double(Pathname) } - let(:load_json) { true } - let(:loaded_specs) { { 'mutant-license' => spec } } - let(:pathname) { class_double(Pathname) } - let(:stderr) { instance_double(IO) } - let(:subscription) { instance_double(Mutant::License::Subscription) } - let(:subscription_result) { right(subscription) } - - let(:spec) do - instance_double( - Gem::Specification, - full_gem_path: gem_path - ) - end - - let(:world) do - instance_double( - Mutant::World, - gem: gem, - gem_method: gem_method, - json: json, - kernel: kernel, - pathname: pathname, - stderr: stderr - ) - end - - before do - allow(gem_method).to receive_messages(call: undefined) - allow(gem_pathname).to receive_messages(join: license_pathname) - allow(json).to receive_messages(load: license_json) - allow(kernel).to receive_messages(sleep: undefined) - allow(pathname).to receive_messages(new: gem_pathname) - allow(Mutant::License::Subscription).to receive_messages(load: subscription_result) - end - - shared_examples 'license load' do - it 'performs IO in expected sequence' do - apply - - expect(gem_method) - .to have_received(:call) - .with('mutant-license', '>= 0.1', '< 0.3') - .ordered - - if load_json - expect(json) - .to have_received(:load) - .with(license_pathname) - .ordered - end - end - - it 'builds correct license.json path' do - if load_json - apply - - expect(pathname).to have_received(:new).with(gem_path) - expect(gem_pathname).to have_received(:join).with('license.json') - end - end - - it 'loads license json' do - if load_json - apply - - expect(Mutant::License::Subscription) - .to have_received(:load) - .with(world, license_json) - end - end - - it 'returns expected result' do - expect(apply).to eql(expected_result) - end - end - - def self.it_fails_with_message(message) - let(:expected_result) { left(message) } - - include_examples 'license load' - end - - context 'on successful license load' do - include_examples 'license load' - - let(:expected_result) { right(subscription) } - end - - context 'when mutant-license gem cannot be loaded' do - let(:load_json) { false } - - def self.setup_error(message) - before do - allow(gem_method).to receive(:call).and_raise(Gem::LoadError, message) - end - end - - context 'while the mutant license gem from rubygems is present' do - setup_error %{can't activate mutant-license (~> 0.1.0), already activated mutant-license-0.0.0.} - - it_fails_with_message 'mutant-license gem from rubygems.org is a dummy' - end - - context 'with other error message' do - setup_error 'test-error' - - it_fails_with_message 'test-error' - end - end -end diff --git a/spec/unit/mutant/reporter/cli/printer/config_spec.rb b/spec/unit/mutant/reporter/cli/printer/config_spec.rb index 5f214a9d4..45c3ca15f 100644 --- a/spec/unit/mutant/reporter/cli/printer/config_spec.rb +++ b/spec/unit/mutant/reporter/cli/printer/config_spec.rb @@ -8,6 +8,7 @@ describe '.call' do it_reports(<<~'REPORT') + Usage: unknown Matcher: # Integration: null Jobs: auto @@ -23,6 +24,7 @@ describe '.call' do it_reports(<<~'REPORT') + Usage: unknown Matcher: # Integration: null Jobs: 10 @@ -38,6 +40,7 @@ describe '.call' do it_reports(<<~'REPORT') + Usage: unknown Matcher: # Integration: null Jobs: auto @@ -53,6 +56,7 @@ describe '.call' do it_reports(<<~'REPORT') + Usage: unknown Matcher: # Integration: foo Jobs: auto @@ -68,6 +72,7 @@ describe '.call' do it_reports(<<~'REPORT') + Usage: unknown Matcher: # Integration: null Jobs: auto diff --git a/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb b/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb index a66a26b0a..b9c961bff 100644 --- a/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb +++ b/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb @@ -11,6 +11,7 @@ it_reports <<~'STR' Mutant environment: + Usage: unknown Matcher: # Integration: null Jobs: auto @@ -38,6 +39,7 @@ context 'on full coverage' do it_reports <<~'STR' Mutant environment: + Usage: unknown Matcher: # Integration: null Jobs: auto @@ -67,6 +69,7 @@ it_reports <<~'STR' Mutant environment: + Usage: unknown Matcher: # Integration: null Jobs: auto diff --git a/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb b/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb index 28ca9e72a..5dff2a36b 100644 --- a/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb +++ b/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb @@ -18,6 +18,7 @@ +false ----------------------- Mutant environment: + Usage: unknown Matcher: # Integration: null Jobs: auto diff --git a/spec/unit/mutant/reporter/cli_spec.rb b/spec/unit/mutant/reporter/cli_spec.rb index 1f56d036a..7368d813c 100644 --- a/spec/unit/mutant/reporter/cli_spec.rb +++ b/spec/unit/mutant/reporter/cli_spec.rb @@ -69,6 +69,7 @@ def self.it_reports(expected_content) it_reports(<<~REPORT) Mutant environment: + Usage: unknown Matcher: # Integration: null Jobs: auto @@ -101,6 +102,7 @@ def self.it_reports(expected_content) it_reports(<<~REPORT) Mutant environment: + Usage: unknown Matcher: # Integration: null Jobs: auto diff --git a/spec/unit/mutant/usage_spec.rb b/spec/unit/mutant/usage_spec.rb new file mode 100644 index 000000000..cd03bd081 --- /dev/null +++ b/spec/unit/mutant/usage_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +RSpec.describe Mutant::Usage do + let(:class_under_test) do + Class.new(described_class) do + const_set(:MESSAGE, 'the-message') + const_set(:VALUE, 'the-value') + end + end + + let(:object) { class_under_test.new } + + describe '#merge' do + let(:other) { Object.new } + + def apply + object.merge(other) + end + + it 'returns object' do + expect(apply).to be(object) + end + end + + describe '#message' do + def apply + object.message + end + + it 'returns expected message' do + expect(apply).to eql('the-message') + end + end + + describe '#value' do + def apply + object.value + end + + it 'returns success' do + expect(apply).to eql('the-value') + end + end + + describe '#verify' do + def apply + object.verify + end + + it 'returns success' do + expect(apply).to eql(right(nil)) + end + end + + describe '.parse' do + def apply + described_class.parse(value) + end + + { + 'opensource' => described_class::Opensource.new, + 'commercial' => described_class::Commercial.new + }.each do |value, usage| + context "on #{value.inspect}" do + let(:value) { value } + + it 'returns expected usage' do + expect(apply).to eql(right(usage)) + end + end + end + + context 'on unknown value' do + let(:value) { 'unknown' } + + it 'returns error' do + expect(apply).to eql(left('Unknown usage option: "unknown"')) + end + end + end +end + +RSpec.describe Mutant::Usage::Unknown do + let(:object) { described_class.new } + + describe '#verify' do + def apply + object.verify + end + + it 'returns error' do + expect(apply).to eql(left(described_class::MESSAGE)) + end + end + + describe '#merge' do + let(:other) { Object.new } + + def apply + object.merge(other) + end + + it 'returns other' do + expect(apply).to be(other) + end + end +end diff --git a/test_app/Gemfile.minitest.lock b/test_app/Gemfile.minitest.lock index 5e0119376..c687c1814 100644 --- a/test_app/Gemfile.minitest.lock +++ b/test_app/Gemfile.minitest.lock @@ -1,20 +1,15 @@ PATH remote: .. specs: - mutant (0.11.31) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-minitest (0.11.31) + mutant-minitest (0.12.0) minitest (~> 5.11) - mutant (= 0.11.31) - -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) + mutant (= 0.12.0) GEM remote: https://rubygems.org/ @@ -37,7 +32,6 @@ PLATFORMS DEPENDENCIES mutant! - mutant-license! mutant-minitest! BUNDLED WITH diff --git a/test_app/Gemfile.rspec3.10.lock b/test_app/Gemfile.rspec3.10.lock index f9803f363..82654b075 100644 --- a/test_app/Gemfile.rspec3.10.lock +++ b/test_app/Gemfile.rspec3.10.lock @@ -1,21 +1,16 @@ PATH remote: .. specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-rspec (0.11.34) - mutant (= 0.11.34) + mutant-rspec (0.12.0) + mutant (= 0.12.0) rspec-core (>= 3.8.0, < 4.0.0) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -57,7 +52,6 @@ PLATFORMS DEPENDENCIES adamantium mutant! - mutant-license! mutant-rspec! rspec (~> 3.10) rspec-core (~> 3.10) diff --git a/test_app/Gemfile.rspec3.11.lock b/test_app/Gemfile.rspec3.11.lock index 2f95d523c..a8edd3186 100644 --- a/test_app/Gemfile.rspec3.11.lock +++ b/test_app/Gemfile.rspec3.11.lock @@ -1,21 +1,16 @@ PATH remote: .. specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-rspec (0.11.34) - mutant (= 0.11.34) + mutant-rspec (0.12.0) + mutant (= 0.12.0) rspec-core (>= 3.8.0, < 4.0.0) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -57,7 +52,6 @@ PLATFORMS DEPENDENCIES adamantium mutant! - mutant-license! mutant-rspec! rspec (~> 3.12) rspec-core (~> 3.12) diff --git a/test_app/Gemfile.rspec3.12.lock b/test_app/Gemfile.rspec3.12.lock index 62f1f59bf..94551d794 100644 --- a/test_app/Gemfile.rspec3.12.lock +++ b/test_app/Gemfile.rspec3.12.lock @@ -1,21 +1,16 @@ PATH remote: .. specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-rspec (0.11.34) - mutant (= 0.11.34) + mutant-rspec (0.12.0) + mutant (= 0.12.0) rspec-core (>= 3.8.0, < 4.0.0) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -57,7 +52,6 @@ PLATFORMS DEPENDENCIES adamantium mutant! - mutant-license! mutant-rspec! rspec (~> 3.9) rspec-core (~> 3.9) diff --git a/test_app/Gemfile.rspec3.13.lock b/test_app/Gemfile.rspec3.13.lock index 316642652..d290e750c 100644 --- a/test_app/Gemfile.rspec3.13.lock +++ b/test_app/Gemfile.rspec3.13.lock @@ -1,21 +1,16 @@ PATH remote: .. specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-rspec (0.11.34) - mutant (= 0.11.34) + mutant-rspec (0.12.0) + mutant (= 0.12.0) rspec-core (>= 3.8.0, < 4.0.0) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -57,7 +52,6 @@ PLATFORMS DEPENDENCIES adamantium mutant! - mutant-license! mutant-rspec! rspec (~> 3.13) rspec-core (~> 3.13) diff --git a/test_app/Gemfile.rspec3.8.lock b/test_app/Gemfile.rspec3.8.lock index fdabbed7f..c6804d163 100644 --- a/test_app/Gemfile.rspec3.8.lock +++ b/test_app/Gemfile.rspec3.8.lock @@ -1,21 +1,16 @@ PATH remote: .. specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-rspec (0.11.34) - mutant (= 0.11.34) + mutant-rspec (0.12.0) + mutant (= 0.12.0) rspec-core (>= 3.8.0, < 4.0.0) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -57,7 +52,6 @@ PLATFORMS DEPENDENCIES adamantium mutant! - mutant-license! mutant-rspec! rspec (~> 3.8) rspec-core (~> 3.8) diff --git a/test_app/Gemfile.rspec3.9.lock b/test_app/Gemfile.rspec3.9.lock index 62f1f59bf..94551d794 100644 --- a/test_app/Gemfile.rspec3.9.lock +++ b/test_app/Gemfile.rspec3.9.lock @@ -1,21 +1,16 @@ PATH remote: .. specs: - mutant (0.11.34) + mutant (0.12.0) diff-lcs (~> 1.3) parser (~> 3.3.0) regexp_parser (~> 2.9.0) sorbet-runtime (~> 0.5.0) unparser (~> 0.6.9) - mutant-rspec (0.11.34) - mutant (= 0.11.34) + mutant-rspec (0.12.0) + mutant (= 0.12.0) rspec-core (>= 3.8.0, < 4.0.0) -GEM - remote: https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev/ - specs: - mutant-license (0.1.1.2.2355046999240944981729280251890364410689.5) - GEM remote: https://rubygems.org/ specs: @@ -57,7 +52,6 @@ PLATFORMS DEPENDENCIES adamantium mutant! - mutant-license! mutant-rspec! rspec (~> 3.9) rspec-core (~> 3.9)