From 03b815df829669cc1d12b5f7b30540077876bf57 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Sat, 30 Mar 2024 11:18:58 -0700 Subject: [PATCH] Enable and fix RSpec/DescribedClassModuleWrapping --- Library/.rubocop.yml | 2 + Library/Homebrew/test/cask/download_spec.rb | 76 +- Library/Homebrew/test/dev-cmd/audit_spec.rb | 1335 ----------------- Library/Homebrew/test/formula_auditor_spec.rb | 1295 ++++++++++++++++ .../Homebrew/test/formula_free_port_spec.rb | 24 - .../test/formula_text_auditor_spec.rb | 42 + Library/Homebrew/test/free_port_spec.rb | 22 + .../exec_shell_metacharacters_spec.rb | 26 + .../test/rubocops/shell_commands_spec.rb | 425 +++--- 9 files changed, 1622 insertions(+), 1625 deletions(-) create mode 100644 Library/Homebrew/test/formula_auditor_spec.rb delete mode 100644 Library/Homebrew/test/formula_free_port_spec.rb create mode 100644 Library/Homebrew/test/formula_text_auditor_spec.rb create mode 100644 Library/Homebrew/test/free_port_spec.rb create mode 100644 Library/Homebrew/test/rubocops/exec_shell_metacharacters_spec.rb diff --git a/Library/.rubocop.yml b/Library/.rubocop.yml index 3b98089c10..f4c636ed80 100644 --- a/Library/.rubocop.yml +++ b/Library/.rubocop.yml @@ -253,6 +253,8 @@ RSpec/NestedGroups: RSpec/MultipleMemoizedHelpers: Enabled: false +RSpec/DescribedClassModuleWrapping: + Enabled: true # Annoying to have these autoremoved. RSpec/Focus: AutoCorrect: false diff --git a/Library/Homebrew/test/cask/download_spec.rb b/Library/Homebrew/test/cask/download_spec.rb index 5fb0f0cbe0..bd39587997 100644 --- a/Library/Homebrew/test/cask/download_spec.rb +++ b/Library/Homebrew/test/cask/download_spec.rb @@ -1,58 +1,56 @@ # frozen_string_literal: true -module Cask - RSpec.describe Download, :cask do - describe "#verify_download_integrity" do - subject(:verification) { described_class.new(cask).verify_download_integrity(downloaded_path) } +RSpec.describe Cask::Download, :cask do + describe "#verify_download_integrity" do + subject(:verification) { described_class.new(cask).verify_download_integrity(downloaded_path) } - let(:cask) { instance_double(Cask, token: "cask", sha256: expected_sha256) } - let(:cafebabe) { "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe" } - let(:deadbeef) { "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" } - let(:computed_sha256) { cafebabe } - let(:downloaded_path) { Pathname.new("cask.zip") } + let(:cask) { instance_double(Cask::Cask, token: "cask", sha256: expected_sha256) } + let(:cafebabe) { "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe" } + let(:deadbeef) { "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" } + let(:computed_sha256) { cafebabe } + let(:downloaded_path) { Pathname.new("cask.zip") } - before do - allow(downloaded_path).to receive_messages(file?: true, sha256: computed_sha256) + before do + allow(downloaded_path).to receive_messages(file?: true, sha256: computed_sha256) + end + + context "when the expected checksum is :no_check" do + let(:expected_sha256) { :no_check } + + it "skips the check" do + expect { verification }.to output(/skipping verification/).to_stderr end + end - context "when the expected checksum is :no_check" do - let(:expected_sha256) { :no_check } + context "when expected and computed checksums match" do + let(:expected_sha256) { Checksum.new(cafebabe) } - it "skips the check" do - expect { verification }.to output(/skipping verification/).to_stderr - end + it "does not raise an error" do + expect { verification }.not_to raise_error end + end - context "when expected and computed checksums match" do - let(:expected_sha256) { Checksum.new(cafebabe) } + context "when the expected checksum is nil" do + let(:expected_sha256) { nil } - it "does not raise an error" do - expect { verification }.not_to raise_error - end + it "outputs an error" do + expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr end + end - context "when the expected checksum is nil" do - let(:expected_sha256) { nil } + context "when the expected checksum is empty" do + let(:expected_sha256) { Checksum.new("") } - it "outputs an error" do - expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr - end + it "outputs an error" do + expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr end + end - context "when the expected checksum is empty" do - let(:expected_sha256) { Checksum.new("") } + context "when expected and computed checksums do not match" do + let(:expected_sha256) { Checksum.new(deadbeef) } - it "outputs an error" do - expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr - end - end - - context "when expected and computed checksums do not match" do - let(:expected_sha256) { Checksum.new(deadbeef) } - - it "raises an error" do - expect { verification }.to raise_error ChecksumMismatchError - end + it "raises an error" do + expect { verification }.to raise_error ChecksumMismatchError end end end diff --git a/Library/Homebrew/test/dev-cmd/audit_spec.rb b/Library/Homebrew/test/dev-cmd/audit_spec.rb index 5205b58cd2..a27ca9e2e1 100644 --- a/Library/Homebrew/test/dev-cmd/audit_spec.rb +++ b/Library/Homebrew/test/dev-cmd/audit_spec.rb @@ -1,1343 +1,8 @@ # frozen_string_literal: true require "dev-cmd/audit" -require "formulary" require "cmd/shared_examples/args_parse" -require "utils/spdx" RSpec.describe Homebrew::DevCmd::Audit do it_behaves_like "parseable arguments" end - -module Count - def self.increment - @count ||= 0 - @count += 1 - end -end - -module Homebrew - RSpec.describe FormulaTextAuditor do - alias_matcher :have_data, :be_data - alias_matcher :have_end, :be_end - alias_matcher :have_trailing_newline, :be_trailing_newline - - let(:dir) { mktmpdir } - - def formula_text(name, body = nil, options = {}) - path = dir/"#{name}.rb" - - path.write <<~RUBY - class #{Formulary.class_s(name)} < Formula - #{body} - end - #{options[:patch]} - RUBY - - described_class.new(path) - end - - specify "simple valid Formula" do - ft = formula_text "valid", <<~RUBY - url "https://www.brew.sh/valid-1.0.tar.gz" - RUBY - - expect(ft).to have_trailing_newline - - expect(ft =~ /\burl\b/).to be_truthy - expect(ft.line_number(/desc/)).to be_nil - expect(ft.line_number(/\burl\b/)).to eq(2) - expect(ft).to include("Valid") - end - - specify "#trailing_newline?" do - ft = formula_text "newline" - expect(ft).to have_trailing_newline - end - end - - RSpec.describe FormulaAuditor do - let(:dir) { mktmpdir } - let(:foo_version) { Count.increment } - let(:formula_subpath) { "Formula/foo#{foo_version}.rb" } - let(:origin_tap_path) { Tap::TAP_DIRECTORY/"homebrew/homebrew-foo" } - let(:origin_formula_path) { origin_tap_path/formula_subpath } - let(:tap_path) { Tap::TAP_DIRECTORY/"homebrew/homebrew-bar" } - let(:formula_path) { tap_path/formula_subpath } - - def formula_auditor(name, text, options = {}) - path = Pathname.new "#{dir}/#{name}.rb" - path.open("w") do |f| - f.write text - end - - formula = Formulary.factory(path) - - if options.key? :tap_audit_exceptions - tap = Tap.fetch("test/tap") - allow(tap).to receive(:audit_exceptions).and_return(options[:tap_audit_exceptions]) - allow(formula).to receive(:tap).and_return(tap) - options.delete :tap_audit_exceptions - end - - described_class.new(formula, options) - end - - def formula_gsub(before, after = "") - text = formula_path.read - text.gsub! before, after - formula_path.unlink - formula_path.write text - end - - def formula_gsub_origin_commit(before, after = "") - text = origin_formula_path.read - text.gsub!(before, after) - origin_formula_path.unlink - origin_formula_path.write text - - origin_tap_path.cd do - system "git", "commit", "-am", "commit" - end - - tap_path.cd do - system "git", "fetch" - system "git", "reset", "--hard", "origin/HEAD" - end - end - - describe "#problems" do - it "is empty by default" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - expect(fa.problems).to be_empty - end - end - - describe "#audit_license" do - let(:spdx_license_data) { SPDX.license_data } - let(:spdx_exception_data) { SPDX.exception_data } - - let(:deprecated_spdx_id) { "GPL-1.0" } - let(:license_all_custom_id) { 'all_of: ["MIT", "zzz"]' } - let(:deprecated_spdx_exception) { "Nokia-Qt-exception-1.1" } - let(:license_any) { 'any_of: ["0BSD", "GPL-3.0-only"]' } - let(:license_any_with_plus) { 'any_of: ["0BSD+", "GPL-3.0-only"]' } - let(:license_nested_conditions) { 'any_of: ["0BSD", { all_of: ["GPL-3.0-only", "MIT"] }]' } - let(:license_any_mismatch) { 'any_of: ["0BSD", "MIT"]' } - let(:license_any_nonstandard) { 'any_of: ["0BSD", "zzz", "MIT"]' } - let(:license_any_deprecated) { 'any_of: ["0BSD", "GPL-1.0", "MIT"]' } - - it "does not check if the formula is not a new formula" do - fa = formula_auditor "foo", <<~RUBY, new_formula: false - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "detects no license info" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true, core_tap: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_license - expect(fa.problems.first[:message]).to match "Formulae in homebrew/core must specify a license." - end - - it "detects if license is not a standard spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license "zzz" - end - RUBY - - fa.audit_license - expect(fa.problems.first[:message]).to match <<~EOS - Formula foo contains non-standard SPDX licenses: ["zzz"]. - For a list of valid licenses check: https://spdx.org/licenses/ - EOS - end - - it "detects if license is a deprecated spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true, strict: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license "#{deprecated_spdx_id}" - end - RUBY - - fa.audit_license - expect(fa.problems.first[:message]).to eq <<~EOS - Formula foo contains deprecated SPDX licenses: ["GPL-1.0"]. - You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). - For a list of valid licenses check: https://spdx.org/licenses/ - EOS - end - - it "detects if license with AND contains a non-standard spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license #{license_all_custom_id} - end - RUBY - - fa.audit_license - expect(fa.problems.first[:message]).to match <<~EOS - Formula foo contains non-standard SPDX licenses: ["zzz"]. - For a list of valid licenses check: https://spdx.org/licenses/ - EOS - end - - it "detects if license array contains a non-standard spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license #{license_any_nonstandard} - end - RUBY - - fa.audit_license - expect(fa.problems.first[:message]).to match <<~EOS - Formula foo contains non-standard SPDX licenses: ["zzz"]. - For a list of valid licenses check: https://spdx.org/licenses/ - EOS - end - - it "detects if license array contains a deprecated spdx-id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true, strict: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license #{license_any_deprecated} - end - RUBY - - fa.audit_license - expect(fa.problems.first[:message]).to eq <<~EOS - Formula foo contains deprecated SPDX licenses: ["GPL-1.0"]. - You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). - For a list of valid licenses check: https://spdx.org/licenses/ - EOS - end - - it "verifies that a license info is a standard spdx id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license "0BSD" - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license info with plus is a standard spdx id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license "0BSD+" - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "allows :public_domain license" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license :public_domain - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license info with multiple licenses are standard spdx ids" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license any_of: ["0BSD", "MIT"] - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license info with exceptions are standard spdx ids" do - formula_text = <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license "Apache-2.0" => { with: "LLVM-exception" } - end - RUBY - fa = formula_auditor("foo", formula_text, new_formula: true, - spdx_license_data:, spdx_exception_data:) - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license array contains only standard spdx id" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license #{license_any} - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license array contains only standard spdx id with plus" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license #{license_any_with_plus} - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license array with AND contains only standard spdx ids" do - fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - license #{license_nested_conditions} - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "checks online and verifies that a standard license id is the same " \ - "as what is indicated on its Github repo", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "GPL-3.0" - end - RUBY - fa = formula_auditor "cask", formula_text, spdx_license_data:, - online: true, core_tap: true, new_formula: true - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "checks online and verifies that a standard license id with AND is the same " \ - "as what is indicated on its Github repo", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license all_of: ["GPL-3.0-or-later", "MIT"] - end - RUBY - fa = formula_auditor "cask", formula_text, spdx_license_data:, - online: true, core_tap: true, new_formula: true - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "checks online and verifies that a standard license id with WITH is the same " \ - "as what is indicated on its Github repo", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "GPL-3.0-or-later" => { with: "LLVM-exception" } - end - RUBY - fa = formula_auditor("cask", formula_text, online: true, core_tap: true, new_formula: true, - spdx_license_data:, spdx_exception_data:) - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "verifies that a license exception has standard spdx ids", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "GPL-3.0-or-later" => { with: "zzz" } - end - RUBY - fa = formula_auditor("cask", formula_text, core_tap: true, new_formula: true, - spdx_license_data:, spdx_exception_data:) - - fa.audit_license - expect(fa.problems.first[:message]).to match <<~EOS - Formula cask contains invalid or deprecated SPDX license exceptions: ["zzz"]. - For a list of valid license exceptions check: - https://spdx.org/licenses/exceptions-index.html - EOS - end - - it "verifies that a license exception has non-deprecated spdx ids", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "GPL-3.0-or-later" => { with: "#{deprecated_spdx_exception}" } - end - RUBY - fa = formula_auditor("cask", formula_text, core_tap: true, new_formula: true, - spdx_license_data:, spdx_exception_data:) - - fa.audit_license - expect(fa.problems.first[:message]).to match <<~EOS - Formula cask contains invalid or deprecated SPDX license exceptions: ["#{deprecated_spdx_exception}"]. - For a list of valid license exceptions check: - https://spdx.org/licenses/exceptions-index.html - EOS - end - - it "checks online and verifies that a standard license id is in the same exempted license group " \ - "as what is indicated on its GitHub repo", :needs_network do - fa = formula_auditor "cask", <<~RUBY, spdx_license_data:, online: true, new_formula: true - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "GPL-3.0-or-later" - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "checks online and verifies that a standard license array is in the same exempted license group " \ - "as what is indicated on its GitHub repo", :needs_network do - fa = formula_auditor "cask", <<~RUBY, spdx_license_data:, online: true, new_formula: true - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license any_of: ["GPL-3.0-or-later", "MIT"] - end - RUBY - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "checks online and detects that a formula-specified license is not " \ - "the same as what is indicated on its Github repository", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "0BSD" - end - RUBY - fa = formula_auditor "cask", formula_text, spdx_license_data:, - online: true, core_tap: true, new_formula: true - - fa.audit_license - expect(fa.problems.first[:message]) - .to eq 'Formula license ["0BSD"] does not match GitHub license ["GPL-3.0"].' - end - - it "allows a formula-specified license that differs from its GitHub " \ - "repository for formulae on the mismatched license allowlist", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license "0BSD" - end - RUBY - fa = formula_auditor "cask", formula_text, spdx_license_data:, - online: true, core_tap: true, new_formula: true, - tap_audit_exceptions: { permitted_formula_license_mismatches: ["cask"] } - - fa.audit_license - expect(fa.problems).to be_empty - end - - it "checks online and detects that an array of license does not contain " \ - "what is indicated on its Github repository", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license #{license_any_mismatch} - end - RUBY - fa = formula_auditor "cask", formula_text, spdx_license_data:, - online: true, core_tap: true, new_formula: true - - fa.audit_license - expect(fa.problems.first[:message]).to match "Formula license [\"0BSD\", \"MIT\"] " \ - "does not match GitHub license [\"GPL-3.0\"]." - end - - it "checks online and verifies that an array of license contains " \ - "what is indicated on its Github repository", :needs_network do - formula_text = <<~RUBY - class Cask < Formula - url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" - head "https://github.com/cask/cask.git" - license #{license_any} - end - RUBY - fa = formula_auditor "cask", formula_text, spdx_license_data:, - online: true, core_tap: true, new_formula: true - - fa.audit_license - expect(fa.problems).to be_empty - end - end - - describe "#audit_file" do - specify "no issue" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - end - RUBY - - fa.audit_file - expect(fa.problems).to be_empty - end - end - - describe "#audit_formula_name" do - specify "no issue" do - fa = formula_auditor "foo", <<~RUBY, core_tap: true, strict: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - end - RUBY - - fa.audit_formula_name - expect(fa.problems).to be_empty - end - - specify "uppercase formula name" do - fa = formula_auditor "Foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/Foo-1.0.tgz" - homepage "https://brew.sh" - end - RUBY - - fa.audit_formula_name - expect(fa.problems.first[:message]).to match "must not contain uppercase letters" - end - end - - describe "#audit_resource_name_matches_pypi_package_name_in_url" do - it "reports a problem if the resource name does not match the python package name" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - sha256 "abc123" - homepage "https://brew.sh" - - resource "Something" do - url "https://files.pythonhosted.org/packages/FooSomething-1.0.0.tar.gz" - sha256 "def456" - end - end - RUBY - - fa.audit_specs - expect(fa.problems.first[:message]) - .to match("resource name should be `FooSomething` to match the PyPI package name") - end - end - - describe "#check_service_command" do - specify "Not installed" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - - service do - run [] - end - end - RUBY - - expect(fa.check_service_command(fa.formula)).to match nil - end - - specify "No service" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - end - RUBY - - mkdir_p fa.formula.prefix - expect(fa.check_service_command(fa.formula)).to match nil - end - - specify "No command" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - - service do - run [] - end - end - RUBY - - mkdir_p fa.formula.prefix - expect(fa.check_service_command(fa.formula)).to match nil - end - - specify "Invalid command" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - - service do - run [HOMEBREW_PREFIX/"bin/something"] - end - end - RUBY - - mkdir_p fa.formula.prefix - expect(fa.check_service_command(fa.formula)).to match "Service command does not exist" - end - end - - describe "#audit_github_repository" do - specify "#audit_github_repository when HOMEBREW_NO_GITHUB_API is set" do - ENV["HOMEBREW_NO_GITHUB_API"] = "1" - - fa = formula_auditor "foo", <<~RUBY, strict: true, online: true - class Foo < Formula - homepage "https://github.com/example/example" - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_github_repository - expect(fa.problems).to be_empty - end - end - - describe "#audit_github_repository_archived" do - specify "#audit_github_repository_archived when HOMEBREW_NO_GITHUB_API is set" do - fa = formula_auditor "foo", <<~RUBY, strict: true, online: true - class Foo < Formula - homepage "https://github.com/example/example" - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_github_repository_archived - expect(fa.problems).to be_empty - end - end - - describe "#audit_gitlab_repository" do - specify "#audit_gitlab_repository for stars, forks and creation date" do - fa = formula_auditor "foo", <<~RUBY, strict: true, online: true - class Foo < Formula - homepage "https://gitlab.com/libtiff/libtiff" - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_gitlab_repository - expect(fa.problems).to be_empty - end - end - - describe "#audit_gitlab_repository_archived" do - specify "#audit gitlab repository for archived status" do - fa = formula_auditor "foo", <<~RUBY, strict: true, online: true - class Foo < Formula - homepage "https://gitlab.com/libtiff/libtiff" - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_gitlab_repository_archived - expect(fa.problems).to be_empty - end - end - - describe "#audit_bitbucket_repository" do - specify "#audit_bitbucket_repository for stars, forks and creation date" do - fa = formula_auditor "foo", <<~RUBY, strict: true, online: true - class Foo < Formula - homepage "https://bitbucket.com/libtiff/libtiff" - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_bitbucket_repository - expect(fa.problems).to be_empty - end - end - - describe "#audit_specs" do - let(:throttle_list) { { throttled_formulae: { "foo" => 10 } } } - let(:versioned_head_spec_list) { { versioned_head_spec_allowlist: ["foo"] } } - - it "doesn't allow to miss a checksum" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - end - RUBY - - fa.audit_specs - expect(fa.problems.first[:message]).to match "Checksum is missing" - end - - it "allows to miss a checksum for git strategy" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo.git", tag: "1.0", revision: "f5e00e485e7aa4c5baa20355b27e3b84a6912790" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - - it "allows to miss a checksum for HEAD" do - fa = formula_auditor "foo", <<~RUBY - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - head "https://brew.sh/foo.tgz" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - - it "allows versions with no throttle rate" do - fa = formula_auditor "bar", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list - class Bar < Formula - url "https://brew.sh/foo-1.0.1.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - - it "allows major/minor versions with throttle rate" do - fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list - class Foo < Formula - url "https://brew.sh/foo-1.0.0.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - - it "allows patch versions to be multiples of the throttle rate" do - fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list - class Foo < Formula - url "https://brew.sh/foo-1.0.10.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - - it "doesn't allow patch versions that aren't multiples of the throttle rate" do - fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list - class Foo < Formula - url "https://brew.sh/foo-1.0.1.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - end - RUBY - - fa.audit_specs - expect(fa.problems.first[:message]).to match "should only be updated every 10 releases on multiples of 10" - end - - it "allows non-versioned formulae to have a `HEAD` spec" do - fa = formula_auditor "bar", <<~RUBY, core_tap: true, tap_audit_exceptions: versioned_head_spec_list - class Bar < Formula - url "https://brew.sh/foo-1.0.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - head "https://brew.sh/foo.git" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - - it "doesn't allow versioned formulae to have a `HEAD` spec" do - fa = formula_auditor "bar@1", <<~RUBY, core_tap: true, tap_audit_exceptions: versioned_head_spec_list - class BarAT1 < Formula - url "https://brew.sh/foo-1.0.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - head "https://brew.sh/foo.git" - end - RUBY - - fa.audit_specs - expect(fa.problems.first[:message]).to match "Versioned formulae should not have a `HEAD` spec" - end - - it "allows versioned formulae on the allowlist to have a `HEAD` spec" do - fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: versioned_head_spec_list - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - head "https://brew.sh/foo.git" - end - RUBY - - fa.audit_specs - expect(fa.problems).to be_empty - end - end - - describe "#audit_deps" do - describe "a dependency on a macOS-provided keg-only formula" do - describe "which is allowlisted" do - subject(:f_a) { fa } - - let(:fa) do - formula_auditor "foo", <<~RUBY, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - - depends_on "openssl" - end - RUBY - end - - let(:f_openssl) do - formula do - url "https://brew.sh/openssl-1.0.tgz" - homepage "https://brew.sh" - - keg_only :provided_by_macos - end - end - - before do - allow(fa.formula.deps.first) - .to receive(:to_formula).and_return(f_openssl) - fa.audit_deps - end - - it(:problems) { expect(f_a.problems).to be_empty } - end - - describe "which is not allowlisted", :needs_macos do - subject(:f_a) { fa } - - let(:fa) do - formula_auditor "foo", <<~RUBY, new_formula: true, core_tap: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - homepage "https://brew.sh" - - depends_on "bc" - end - RUBY - end - - let(:f_bc) do - formula do - url "https://brew.sh/bc-1.0.tgz" - homepage "https://brew.sh" - - keg_only :provided_by_macos - end - end - - before do - allow(fa.formula.deps.first) - .to receive(:to_formula).and_return(f_bc) - fa.audit_deps - end - - it(:new_formula_problems) do - expect(f_a.new_formula_problems) - .to include(a_hash_including(message: a_string_matching(/is provided by macOS/))) - end - end - end - end - - describe "#audit_stable_version" do - subject do - fa = described_class.new(Formulary.factory(formula_path), git: true) - fa.audit_stable_version - fa.problems.first&.fetch(:message) - end - - before do - origin_formula_path.dirname.mkpath - origin_formula_path.write <<~RUBY - class Foo#{foo_version} < Formula - url "https://brew.sh/foo-1.0.tar.gz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - revision 2 - version_scheme 1 - end - RUBY - - origin_tap_path.mkpath - origin_tap_path.cd do - system "git", "init" - system "git", "add", "--all" - system "git", "commit", "-m", "init" - end - - tap_path.mkpath - tap_path.cd do - system "git", "clone", origin_tap_path, "." - end - end - - describe "versions" do - context "when uncommitted should not decrease" do - before { formula_gsub "foo-1.0.tar.gz", "foo-0.9.tar.gz" } - - it { is_expected.to match("stable version should not decrease (from 1.0 to 0.9)") } - end - - context "when committed can decrease" do - before do - formula_gsub_origin_commit "revision 2" - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-0.9.tar.gz" - end - - it { is_expected.to be_nil } - end - - describe "can decrease with version_scheme increased" do - before do - formula_gsub "revision 2" - formula_gsub "foo-1.0.tar.gz", "foo-0.9.tar.gz" - formula_gsub "version_scheme 1", "version_scheme 2" - end - - it { is_expected.to be_nil } - end - end - end - - describe "#audit_revision" do - subject do - fa = described_class.new(Formulary.factory(formula_path), git: true) - fa.audit_revision - fa.problems.first&.fetch(:message) - end - - before do - origin_formula_path.dirname.mkpath - origin_formula_path.write <<~RUBY - class Foo#{foo_version} < Formula - url "https://brew.sh/foo-1.0.tar.gz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - revision 2 - version_scheme 1 - end - RUBY - - origin_tap_path.mkpath - origin_tap_path.cd do - system "git", "init" - system "git", "add", "--all" - system "git", "commit", "-m", "init" - end - - tap_path.mkpath - tap_path.cd do - system "git", "clone", origin_tap_path, "." - end - end - - describe "new formulae should not have a revision" do - it "doesn't allow new formulae to have a revision" do - fa = formula_auditor "foo", <<~RUBY, new_formula: true - class Foo < Formula - url "https://brew.sh/foo-1.0.tgz" - revision 1 - end - RUBY - - fa.audit_revision - - expect(fa.new_formula_problems).to include( - a_hash_including(message: a_string_matching(/should not define a revision/)), - ) - end - end - - describe "revisions" do - describe "should not be removed when first committed above 0" do - it { is_expected.to be_nil } - end - - describe "with the same version, should not decrease" do - before { formula_gsub_origin_commit "revision 2", "revision 1" } - - it { is_expected.to match("revision should not decrease (from 2 to 1)") } - end - - describe "should not be removed with the same version" do - before { formula_gsub_origin_commit "revision 2" } - - it { is_expected.to match("revision should not decrease (from 2 to 0)") } - end - - describe "should not decrease with the same, uncommitted version" do - before { formula_gsub "revision 2", "revision 1" } - - it { is_expected.to match("revision should not decrease (from 2 to 1)") } - end - - describe "should be removed with a newer version" do - before { formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" } - - it { is_expected.to match("'revision 2' should be removed") } - end - - describe "should be removed with a newer local version" do - before { formula_gsub "foo-1.0.tar.gz", "foo-1.1.tar.gz" } - - it { is_expected.to match("'revision 2' should be removed") } - end - - describe "should not warn on an newer version revision removal" do - before do - formula_gsub_origin_commit "revision 2", "" - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" - end - - it { is_expected.to be_nil } - end - - describe "should not warn when revision from previous version matches current revision" do - before do - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub_origin_commit "revision 2", "# no revision" - formula_gsub_origin_commit "# no revision", "revision 1" - formula_gsub_origin_commit "revision 1", "revision 2" - end - - it { is_expected.to be_nil } - end - - describe "should only increment by 1 with an uncommitted version" do - before do - formula_gsub "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub "revision 2", "revision 4" - end - - it { is_expected.to match("revisions should only increment by 1") } - end - - describe "should not warn on past increment by more than 1" do - before do - formula_gsub_origin_commit "revision 2", "# no revision" - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub_origin_commit "# no revision", "revision 3" - end - - it { is_expected.to be_nil } - end - end - end - - describe "#audit_version_scheme" do - subject do - fa = described_class.new(Formulary.factory(formula_path), git: true) - fa.audit_version_scheme - fa.problems.first&.fetch(:message) - end - - before do - origin_formula_path.dirname.mkpath - origin_formula_path.write <<~RUBY - class Foo#{foo_version} < Formula - url "https://brew.sh/foo-1.0.tar.gz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - revision 2 - version_scheme 1 - end - RUBY - - origin_tap_path.mkpath - origin_tap_path.cd do - system "git", "init" - system "git", "add", "--all" - system "git", "commit", "-m", "init" - end - - tap_path.mkpath - tap_path.cd do - system "git", "clone", origin_tap_path, "." - end - end - - describe "version_schemes" do - describe "should not decrease with the same version" do - before { formula_gsub_origin_commit "version_scheme 1" } - - it { is_expected.to match("version_scheme should not decrease (from 1 to 0)") } - end - - describe "should not decrease with a new version" do - before do - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub_origin_commit "revision 2", "" - formula_gsub_origin_commit "version_scheme 1", "" - end - - it { is_expected.to match("version_scheme should not decrease (from 1 to 0)") } - end - - describe "should only increment by 1" do - before do - formula_gsub_origin_commit "version_scheme 1", "# no version_scheme" - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub_origin_commit "revision 2", "" - formula_gsub_origin_commit "# no version_scheme", "version_scheme 3" - end - - it { is_expected.to match("version_schemes should only increment by 1") } - end - end - end - - describe "#audit_unconfirmed_checksum_change" do - subject do - fa = described_class.new(Formulary.factory(formula_path), git: true) - fa.audit_unconfirmed_checksum_change - fa.problems.first&.fetch(:message) - end - - before do - origin_formula_path.dirname.mkpath - origin_formula_path.write <<~RUBY - class Foo#{foo_version} < Formula - url "https://brew.sh/foo-1.0.tar.gz" - sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" - revision 2 - version_scheme 1 - end - RUBY - - origin_tap_path.mkpath - origin_tap_path.cd do - system "git", "init" - system "git", "add", "--all" - system "git", "commit", "-m", "init" - end - - tap_path.mkpath - tap_path.cd do - system "git", "clone", origin_tap_path, "." - end - end - - describe "checksums" do - describe "should not change with the same version" do - before do - formula_gsub( - 'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', - 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', - ) - end - - it { is_expected.to match("stable sha256 changed without the url/version also changing") } - end - - describe "should not change with the same version when not the first commit" do - before do - formula_gsub_origin_commit( - 'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', - 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', - ) - formula_gsub_origin_commit "revision 2" - formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub( - 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', - 'sha256 "e048c5e6144f5932d8672c2fade81d9073d5b3ca1517b84df006de3d25414fc1"', - ) - end - - it { is_expected.to match("stable sha256 changed without the url/version also changing") } - end - - describe "can change with the different version" do - before do - formula_gsub_origin_commit( - 'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', - 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', - ) - formula_gsub "foo-1.0.tar.gz", "foo-1.1.tar.gz" - formula_gsub_origin_commit( - 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', - 'sha256 "e048c5e6144f5932d8672c2fade81d9073d5b3ca1517b84df006de3d25414fc1"', - ) - end - - it { is_expected.to be_nil } - end - - describe "can be removed when switching schemes" do - before do - formula_gsub_origin_commit( - 'url "https://brew.sh/foo-1.0.tar.gz"', - 'url "https://foo.com/brew/bar.git", tag: "1.0", revision: "f5e00e485e7aa4c5baa20355b27e3b84a6912790"', - ) - formula_gsub_origin_commit('sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', - "") - end - - it { is_expected.to be_nil } - end - end - end - - describe "#audit_versioned_keg_only" do - specify "it warns when a versioned formula is not `keg_only`" do - fa = formula_auditor "foo@1.1", <<~RUBY, core_tap: true - class FooAT11 < Formula - url "https://brew.sh/foo-1.1.tgz" - end - RUBY - - fa.audit_versioned_keg_only - - expect(fa.problems.first[:message]) - .to match("Versioned formulae in homebrew/core should use `keg_only :versioned_formula`") - end - - specify "it warns when a versioned formula has an incorrect `keg_only` reason" do - fa = formula_auditor "foo@1.1", <<~RUBY, core_tap: true - class FooAT11 < Formula - url "https://brew.sh/foo-1.1.tgz" - - keg_only :provided_by_macos - end - RUBY - - fa.audit_versioned_keg_only - - expect(fa.problems.first[:message]) - .to match("Versioned formulae in homebrew/core should use `keg_only :versioned_formula`") - end - - specify "it does not warn when a versioned formula has `keg_only :versioned_formula`" do - fa = formula_auditor "foo@1.1", <<~RUBY, core_tap: true - class FooAT11 < Formula - url "https://brew.sh/foo-1.1.tgz" - - keg_only :versioned_formula - end - RUBY - - fa.audit_versioned_keg_only - - expect(fa.problems).to be_empty - end - end - - describe "#audit_conflicts" do - before do - # We don't really test FormulaTextAuditor here - allow(File).to receive(:open).and_return("") - end - - specify "it warns when conflicting with non-existing formula" do - foo = formula("foo") do - url "https://brew.sh/bar-1.0.tgz" - - conflicts_with "bar" - end - - fa = described_class.new foo - fa.audit_conflicts - - expect(fa.problems.first[:message]) - .to match("Can't find conflicting formula \"bar\"") - end - - specify "it warns when conflicting with itself" do - foo = formula("foo") do - url "https://brew.sh/bar-1.0.tgz" - - conflicts_with "foo" - end - stub_formula_loader foo - - fa = described_class.new foo - fa.audit_conflicts - - expect(fa.problems.first[:message]) - .to match("Formula should not conflict with itself") - end - - specify "it warns when another formula does not have a symmetric conflict" do - stub_formula_loader formula("gcc") { url "gcc-1.0" } - stub_formula_loader formula("glibc") { url "glibc-1.0" } - - foo = formula("foo") do - url "https://brew.sh/foo-1.0.tgz" - end - stub_formula_loader foo - - bar = formula("bar") do - url "https://brew.sh/bar-1.0.tgz" - - conflicts_with "foo" - end - - fa = described_class.new bar - fa.audit_conflicts - - expect(fa.problems.first[:message]) - .to match("Formula foo should also have a conflict declared with bar") - end - end - end -end diff --git a/Library/Homebrew/test/formula_auditor_spec.rb b/Library/Homebrew/test/formula_auditor_spec.rb new file mode 100644 index 0000000000..c25e208351 --- /dev/null +++ b/Library/Homebrew/test/formula_auditor_spec.rb @@ -0,0 +1,1295 @@ +# frozen_string_literal: true + +require "formula_auditor" + +module Count + def self.increment + @count ||= 0 + @count += 1 + end +end + +RSpec.describe Homebrew::FormulaAuditor do + let(:dir) { mktmpdir } + let(:foo_version) { Count.increment } + let(:formula_subpath) { "Formula/foo#{foo_version}.rb" } + let(:origin_tap_path) { Tap::TAP_DIRECTORY/"homebrew/homebrew-foo" } + let(:origin_formula_path) { origin_tap_path/formula_subpath } + let(:tap_path) { Tap::TAP_DIRECTORY/"homebrew/homebrew-bar" } + let(:formula_path) { tap_path/formula_subpath } + + def formula_auditor(name, text, options = {}) + path = Pathname.new "#{dir}/#{name}.rb" + path.open("w") do |f| + f.write text + end + + formula = Formulary.factory(path) + + if options.key? :tap_audit_exceptions + tap = Tap.fetch("test/tap") + allow(tap).to receive(:audit_exceptions).and_return(options[:tap_audit_exceptions]) + allow(formula).to receive(:tap).and_return(tap) + options.delete :tap_audit_exceptions + end + + described_class.new(formula, options) + end + + def formula_gsub(before, after = "") + text = formula_path.read + text.gsub! before, after + formula_path.unlink + formula_path.write text + end + + def formula_gsub_origin_commit(before, after = "") + text = origin_formula_path.read + text.gsub!(before, after) + origin_formula_path.unlink + origin_formula_path.write text + + origin_tap_path.cd do + system "git", "commit", "-am", "commit" + end + + tap_path.cd do + system "git", "fetch" + system "git", "reset", "--hard", "origin/HEAD" + end + end + + describe "#problems" do + it "is empty by default" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + expect(fa.problems).to be_empty + end + end + + describe "#audit_license" do + let(:spdx_license_data) { SPDX.license_data } + let(:spdx_exception_data) { SPDX.exception_data } + + let(:deprecated_spdx_id) { "GPL-1.0" } + let(:license_all_custom_id) { 'all_of: ["MIT", "zzz"]' } + let(:deprecated_spdx_exception) { "Nokia-Qt-exception-1.1" } + let(:license_any) { 'any_of: ["0BSD", "GPL-3.0-only"]' } + let(:license_any_with_plus) { 'any_of: ["0BSD+", "GPL-3.0-only"]' } + let(:license_nested_conditions) { 'any_of: ["0BSD", { all_of: ["GPL-3.0-only", "MIT"] }]' } + let(:license_any_mismatch) { 'any_of: ["0BSD", "MIT"]' } + let(:license_any_nonstandard) { 'any_of: ["0BSD", "zzz", "MIT"]' } + let(:license_any_deprecated) { 'any_of: ["0BSD", "GPL-1.0", "MIT"]' } + + it "does not check if the formula is not a new formula" do + fa = formula_auditor "foo", <<~RUBY, new_formula: false + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "detects no license info" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true, core_tap: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_license + expect(fa.problems.first[:message]).to match "Formulae in homebrew/core must specify a license." + end + + it "detects if license is not a standard spdx-id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license "zzz" + end + RUBY + + fa.audit_license + expect(fa.problems.first[:message]).to match <<~EOS + Formula foo contains non-standard SPDX licenses: ["zzz"]. + For a list of valid licenses check: https://spdx.org/licenses/ + EOS + end + + it "detects if license is a deprecated spdx-id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true, strict: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license "#{deprecated_spdx_id}" + end + RUBY + + fa.audit_license + expect(fa.problems.first[:message]).to eq <<~EOS + Formula foo contains deprecated SPDX licenses: ["GPL-1.0"]. + You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). + For a list of valid licenses check: https://spdx.org/licenses/ + EOS + end + + it "detects if license with AND contains a non-standard spdx-id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_all_custom_id} + end + RUBY + + fa.audit_license + expect(fa.problems.first[:message]).to match <<~EOS + Formula foo contains non-standard SPDX licenses: ["zzz"]. + For a list of valid licenses check: https://spdx.org/licenses/ + EOS + end + + it "detects if license array contains a non-standard spdx-id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_any_nonstandard} + end + RUBY + + fa.audit_license + expect(fa.problems.first[:message]).to match <<~EOS + Formula foo contains non-standard SPDX licenses: ["zzz"]. + For a list of valid licenses check: https://spdx.org/licenses/ + EOS + end + + it "detects if license array contains a deprecated spdx-id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true, strict: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_any_deprecated} + end + RUBY + + fa.audit_license + expect(fa.problems.first[:message]).to eq <<~EOS + Formula foo contains deprecated SPDX licenses: ["GPL-1.0"]. + You may need to add `-only` or `-or-later` for GNU licenses (e.g. `GPL`, `LGPL`, `AGPL`, `GFDL`). + For a list of valid licenses check: https://spdx.org/licenses/ + EOS + end + + it "verifies that a license info is a standard spdx id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license "0BSD" + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license info with plus is a standard spdx id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license "0BSD+" + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "allows :public_domain license" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license :public_domain + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license info with multiple licenses are standard spdx ids" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license any_of: ["0BSD", "MIT"] + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license info with exceptions are standard spdx ids" do + formula_text = <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license "Apache-2.0" => { with: "LLVM-exception" } + end + RUBY + fa = formula_auditor("foo", formula_text, new_formula: true, + spdx_license_data:, spdx_exception_data:) + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license array contains only standard spdx id" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_any} + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license array contains only standard spdx id with plus" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_any_with_plus} + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license array with AND contains only standard spdx ids" do + fa = formula_auditor "foo", <<~RUBY, spdx_license_data:, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + license #{license_nested_conditions} + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and verifies that a standard license id is the same " \ + "as what is indicated on its Github repo", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0" + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data:, + online: true, core_tap: true, new_formula: true + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and verifies that a standard license id with AND is the same " \ + "as what is indicated on its Github repo", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license all_of: ["GPL-3.0-or-later", "MIT"] + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data:, + online: true, core_tap: true, new_formula: true + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and verifies that a standard license id with WITH is the same " \ + "as what is indicated on its Github repo", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" => { with: "LLVM-exception" } + end + RUBY + fa = formula_auditor("cask", formula_text, online: true, core_tap: true, new_formula: true, + spdx_license_data:, spdx_exception_data:) + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "verifies that a license exception has standard spdx ids", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" => { with: "zzz" } + end + RUBY + fa = formula_auditor("cask", formula_text, core_tap: true, new_formula: true, + spdx_license_data:, spdx_exception_data:) + + fa.audit_license + expect(fa.problems.first[:message]).to match <<~EOS + Formula cask contains invalid or deprecated SPDX license exceptions: ["zzz"]. + For a list of valid license exceptions check: + https://spdx.org/licenses/exceptions-index.html + EOS + end + + it "verifies that a license exception has non-deprecated spdx ids", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" => { with: "#{deprecated_spdx_exception}" } + end + RUBY + fa = formula_auditor("cask", formula_text, core_tap: true, new_formula: true, + spdx_license_data:, spdx_exception_data:) + + fa.audit_license + expect(fa.problems.first[:message]).to match <<~EOS + Formula cask contains invalid or deprecated SPDX license exceptions: ["#{deprecated_spdx_exception}"]. + For a list of valid license exceptions check: + https://spdx.org/licenses/exceptions-index.html + EOS + end + + it "checks online and verifies that a standard license id is in the same exempted license group " \ + "as what is indicated on its GitHub repo", :needs_network do + fa = formula_auditor "cask", <<~RUBY, spdx_license_data:, online: true, new_formula: true + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "GPL-3.0-or-later" + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and verifies that a standard license array is in the same exempted license group " \ + "as what is indicated on its GitHub repo", :needs_network do + fa = formula_auditor "cask", <<~RUBY, spdx_license_data:, online: true, new_formula: true + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license any_of: ["GPL-3.0-or-later", "MIT"] + end + RUBY + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and detects that a formula-specified license is not " \ + "the same as what is indicated on its Github repository", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "0BSD" + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data:, + online: true, core_tap: true, new_formula: true + + fa.audit_license + expect(fa.problems.first[:message]) + .to eq 'Formula license ["0BSD"] does not match GitHub license ["GPL-3.0"].' + end + + it "allows a formula-specified license that differs from its GitHub " \ + "repository for formulae on the mismatched license allowlist", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license "0BSD" + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data:, + online: true, core_tap: true, new_formula: true, + tap_audit_exceptions: { permitted_formula_license_mismatches: ["cask"] } + + fa.audit_license + expect(fa.problems).to be_empty + end + + it "checks online and detects that an array of license does not contain " \ + "what is indicated on its Github repository", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license #{license_any_mismatch} + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data:, + online: true, core_tap: true, new_formula: true + + fa.audit_license + expect(fa.problems.first[:message]).to match "Formula license [\"0BSD\", \"MIT\"] " \ + "does not match GitHub license [\"GPL-3.0\"]." + end + + it "checks online and verifies that an array of license contains " \ + "what is indicated on its Github repository", :needs_network do + formula_text = <<~RUBY + class Cask < Formula + url "https://github.com/cask/cask/archive/v0.8.4.tar.gz" + head "https://github.com/cask/cask.git" + license #{license_any} + end + RUBY + fa = formula_auditor "cask", formula_text, spdx_license_data:, + online: true, core_tap: true, new_formula: true + + fa.audit_license + expect(fa.problems).to be_empty + end + end + + describe "#audit_file" do + specify "no issue" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + end + RUBY + + fa.audit_file + expect(fa.problems).to be_empty + end + end + + describe "#audit_formula_name" do + specify "no issue" do + fa = formula_auditor "foo", <<~RUBY, core_tap: true, strict: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + end + RUBY + + fa.audit_formula_name + expect(fa.problems).to be_empty + end + + specify "uppercase formula name" do + fa = formula_auditor "Foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/Foo-1.0.tgz" + homepage "https://brew.sh" + end + RUBY + + fa.audit_formula_name + expect(fa.problems.first[:message]).to match "must not contain uppercase letters" + end + end + + describe "#audit_resource_name_matches_pypi_package_name_in_url" do + it "reports a problem if the resource name does not match the python package name" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + sha256 "abc123" + homepage "https://brew.sh" + + resource "Something" do + url "https://files.pythonhosted.org/packages/FooSomething-1.0.0.tar.gz" + sha256 "def456" + end + end + RUBY + + fa.audit_specs + expect(fa.problems.first[:message]) + .to match("resource name should be `FooSomething` to match the PyPI package name") + end + end + + describe "#check_service_command" do + specify "Not installed" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + + service do + run [] + end + end + RUBY + + expect(fa.check_service_command(fa.formula)).to match nil + end + + specify "No service" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + end + RUBY + + mkdir_p fa.formula.prefix + expect(fa.check_service_command(fa.formula)).to match nil + end + + specify "No command" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + + service do + run [] + end + end + RUBY + + mkdir_p fa.formula.prefix + expect(fa.check_service_command(fa.formula)).to match nil + end + + specify "Invalid command" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + + service do + run [HOMEBREW_PREFIX/"bin/something"] + end + end + RUBY + + mkdir_p fa.formula.prefix + expect(fa.check_service_command(fa.formula)).to match "Service command does not exist" + end + end + + describe "#audit_github_repository" do + specify "#audit_github_repository when HOMEBREW_NO_GITHUB_API is set" do + ENV["HOMEBREW_NO_GITHUB_API"] = "1" + + fa = formula_auditor "foo", <<~RUBY, strict: true, online: true + class Foo < Formula + homepage "https://github.com/example/example" + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_github_repository + expect(fa.problems).to be_empty + end + end + + describe "#audit_github_repository_archived" do + specify "#audit_github_repository_archived when HOMEBREW_NO_GITHUB_API is set" do + fa = formula_auditor "foo", <<~RUBY, strict: true, online: true + class Foo < Formula + homepage "https://github.com/example/example" + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_github_repository_archived + expect(fa.problems).to be_empty + end + end + + describe "#audit_gitlab_repository" do + specify "#audit_gitlab_repository for stars, forks and creation date" do + fa = formula_auditor "foo", <<~RUBY, strict: true, online: true + class Foo < Formula + homepage "https://gitlab.com/libtiff/libtiff" + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_gitlab_repository + expect(fa.problems).to be_empty + end + end + + describe "#audit_gitlab_repository_archived" do + specify "#audit gitlab repository for archived status" do + fa = formula_auditor "foo", <<~RUBY, strict: true, online: true + class Foo < Formula + homepage "https://gitlab.com/libtiff/libtiff" + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_gitlab_repository_archived + expect(fa.problems).to be_empty + end + end + + describe "#audit_bitbucket_repository" do + specify "#audit_bitbucket_repository for stars, forks and creation date" do + fa = formula_auditor "foo", <<~RUBY, strict: true, online: true + class Foo < Formula + homepage "https://bitbucket.com/libtiff/libtiff" + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_bitbucket_repository + expect(fa.problems).to be_empty + end + end + + describe "#audit_specs" do + let(:throttle_list) { { throttled_formulae: { "foo" => 10 } } } + let(:versioned_head_spec_list) { { versioned_head_spec_allowlist: ["foo"] } } + + it "doesn't allow to miss a checksum" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + end + RUBY + + fa.audit_specs + expect(fa.problems.first[:message]).to match "Checksum is missing" + end + + it "allows to miss a checksum for git strategy" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo.git", tag: "1.0", revision: "f5e00e485e7aa4c5baa20355b27e3b84a6912790" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + + it "allows to miss a checksum for HEAD" do + fa = formula_auditor "foo", <<~RUBY + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + head "https://brew.sh/foo.tgz" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + + it "allows versions with no throttle rate" do + fa = formula_auditor "bar", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list + class Bar < Formula + url "https://brew.sh/foo-1.0.1.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + + it "allows major/minor versions with throttle rate" do + fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list + class Foo < Formula + url "https://brew.sh/foo-1.0.0.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + + it "allows patch versions to be multiples of the throttle rate" do + fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list + class Foo < Formula + url "https://brew.sh/foo-1.0.10.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + + it "doesn't allow patch versions that aren't multiples of the throttle rate" do + fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: throttle_list + class Foo < Formula + url "https://brew.sh/foo-1.0.1.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + end + RUBY + + fa.audit_specs + expect(fa.problems.first[:message]).to match "should only be updated every 10 releases on multiples of 10" + end + + it "allows non-versioned formulae to have a `HEAD` spec" do + fa = formula_auditor "bar", <<~RUBY, core_tap: true, tap_audit_exceptions: versioned_head_spec_list + class Bar < Formula + url "https://brew.sh/foo-1.0.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + head "https://brew.sh/foo.git" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + + it "doesn't allow versioned formulae to have a `HEAD` spec" do + fa = formula_auditor "bar@1", <<~RUBY, core_tap: true, tap_audit_exceptions: versioned_head_spec_list + class BarAT1 < Formula + url "https://brew.sh/foo-1.0.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + head "https://brew.sh/foo.git" + end + RUBY + + fa.audit_specs + expect(fa.problems.first[:message]).to match "Versioned formulae should not have a `HEAD` spec" + end + + it "allows versioned formulae on the allowlist to have a `HEAD` spec" do + fa = formula_auditor "foo", <<~RUBY, core_tap: true, tap_audit_exceptions: versioned_head_spec_list + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + head "https://brew.sh/foo.git" + end + RUBY + + fa.audit_specs + expect(fa.problems).to be_empty + end + end + + describe "#audit_deps" do + describe "a dependency on a macOS-provided keg-only formula" do + describe "which is allowlisted" do + subject(:f_a) { fa } + + let(:fa) do + formula_auditor "foo", <<~RUBY, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + + depends_on "openssl" + end + RUBY + end + + let(:f_openssl) do + formula do + url "https://brew.sh/openssl-1.0.tgz" + homepage "https://brew.sh" + + keg_only :provided_by_macos + end + end + + before do + allow(fa.formula.deps.first) + .to receive(:to_formula).and_return(f_openssl) + fa.audit_deps + end + + it(:problems) { expect(f_a.problems).to be_empty } + end + + describe "which is not allowlisted", :needs_macos do + subject(:f_a) { fa } + + let(:fa) do + formula_auditor "foo", <<~RUBY, new_formula: true, core_tap: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + homepage "https://brew.sh" + + depends_on "bc" + end + RUBY + end + + let(:f_bc) do + formula do + url "https://brew.sh/bc-1.0.tgz" + homepage "https://brew.sh" + + keg_only :provided_by_macos + end + end + + before do + allow(fa.formula.deps.first) + .to receive(:to_formula).and_return(f_bc) + fa.audit_deps + end + + it(:new_formula_problems) do + expect(f_a.new_formula_problems) + .to include(a_hash_including(message: a_string_matching(/is provided by macOS/))) + end + end + end + end + + describe "#audit_stable_version" do + subject do + fa = described_class.new(Formulary.factory(formula_path), git: true) + fa.audit_stable_version + fa.problems.first&.fetch(:message) + end + + before do + origin_formula_path.dirname.mkpath + origin_formula_path.write <<~RUBY + class Foo#{foo_version} < Formula + url "https://brew.sh/foo-1.0.tar.gz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + revision 2 + version_scheme 1 + end + RUBY + + origin_tap_path.mkpath + origin_tap_path.cd do + system "git", "init" + system "git", "add", "--all" + system "git", "commit", "-m", "init" + end + + tap_path.mkpath + tap_path.cd do + system "git", "clone", origin_tap_path, "." + end + end + + describe "versions" do + context "when uncommitted should not decrease" do + before { formula_gsub "foo-1.0.tar.gz", "foo-0.9.tar.gz" } + + it { is_expected.to match("stable version should not decrease (from 1.0 to 0.9)") } + end + + context "when committed can decrease" do + before do + formula_gsub_origin_commit "revision 2" + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-0.9.tar.gz" + end + + it { is_expected.to be_nil } + end + + describe "can decrease with version_scheme increased" do + before do + formula_gsub "revision 2" + formula_gsub "foo-1.0.tar.gz", "foo-0.9.tar.gz" + formula_gsub "version_scheme 1", "version_scheme 2" + end + + it { is_expected.to be_nil } + end + end + end + + describe "#audit_revision" do + subject do + fa = described_class.new(Formulary.factory(formula_path), git: true) + fa.audit_revision + fa.problems.first&.fetch(:message) + end + + before do + origin_formula_path.dirname.mkpath + origin_formula_path.write <<~RUBY + class Foo#{foo_version} < Formula + url "https://brew.sh/foo-1.0.tar.gz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + revision 2 + version_scheme 1 + end + RUBY + + origin_tap_path.mkpath + origin_tap_path.cd do + system "git", "init" + system "git", "add", "--all" + system "git", "commit", "-m", "init" + end + + tap_path.mkpath + tap_path.cd do + system "git", "clone", origin_tap_path, "." + end + end + + describe "new formulae should not have a revision" do + it "doesn't allow new formulae to have a revision" do + fa = formula_auditor "foo", <<~RUBY, new_formula: true + class Foo < Formula + url "https://brew.sh/foo-1.0.tgz" + revision 1 + end + RUBY + + fa.audit_revision + + expect(fa.new_formula_problems).to include( + a_hash_including(message: a_string_matching(/should not define a revision/)), + ) + end + end + + describe "revisions" do + describe "should not be removed when first committed above 0" do + it { is_expected.to be_nil } + end + + describe "with the same version, should not decrease" do + before { formula_gsub_origin_commit "revision 2", "revision 1" } + + it { is_expected.to match("revision should not decrease (from 2 to 1)") } + end + + describe "should not be removed with the same version" do + before { formula_gsub_origin_commit "revision 2" } + + it { is_expected.to match("revision should not decrease (from 2 to 0)") } + end + + describe "should not decrease with the same, uncommitted version" do + before { formula_gsub "revision 2", "revision 1" } + + it { is_expected.to match("revision should not decrease (from 2 to 1)") } + end + + describe "should be removed with a newer version" do + before { formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" } + + it { is_expected.to match("'revision 2' should be removed") } + end + + describe "should be removed with a newer local version" do + before { formula_gsub "foo-1.0.tar.gz", "foo-1.1.tar.gz" } + + it { is_expected.to match("'revision 2' should be removed") } + end + + describe "should not warn on an newer version revision removal" do + before do + formula_gsub_origin_commit "revision 2", "" + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" + end + + it { is_expected.to be_nil } + end + + describe "should not warn when revision from previous version matches current revision" do + before do + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub_origin_commit "revision 2", "# no revision" + formula_gsub_origin_commit "# no revision", "revision 1" + formula_gsub_origin_commit "revision 1", "revision 2" + end + + it { is_expected.to be_nil } + end + + describe "should only increment by 1 with an uncommitted version" do + before do + formula_gsub "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub "revision 2", "revision 4" + end + + it { is_expected.to match("revisions should only increment by 1") } + end + + describe "should not warn on past increment by more than 1" do + before do + formula_gsub_origin_commit "revision 2", "# no revision" + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub_origin_commit "# no revision", "revision 3" + end + + it { is_expected.to be_nil } + end + end + end + + describe "#audit_version_scheme" do + subject do + fa = described_class.new(Formulary.factory(formula_path), git: true) + fa.audit_version_scheme + fa.problems.first&.fetch(:message) + end + + before do + origin_formula_path.dirname.mkpath + origin_formula_path.write <<~RUBY + class Foo#{foo_version} < Formula + url "https://brew.sh/foo-1.0.tar.gz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + revision 2 + version_scheme 1 + end + RUBY + + origin_tap_path.mkpath + origin_tap_path.cd do + system "git", "init" + system "git", "add", "--all" + system "git", "commit", "-m", "init" + end + + tap_path.mkpath + tap_path.cd do + system "git", "clone", origin_tap_path, "." + end + end + + describe "version_schemes" do + describe "should not decrease with the same version" do + before { formula_gsub_origin_commit "version_scheme 1" } + + it { is_expected.to match("version_scheme should not decrease (from 1 to 0)") } + end + + describe "should not decrease with a new version" do + before do + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub_origin_commit "revision 2", "" + formula_gsub_origin_commit "version_scheme 1", "" + end + + it { is_expected.to match("version_scheme should not decrease (from 1 to 0)") } + end + + describe "should only increment by 1" do + before do + formula_gsub_origin_commit "version_scheme 1", "# no version_scheme" + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub_origin_commit "revision 2", "" + formula_gsub_origin_commit "# no version_scheme", "version_scheme 3" + end + + it { is_expected.to match("version_schemes should only increment by 1") } + end + end + end + + describe "#audit_unconfirmed_checksum_change" do + subject do + fa = described_class.new(Formulary.factory(formula_path), git: true) + fa.audit_unconfirmed_checksum_change + fa.problems.first&.fetch(:message) + end + + before do + origin_formula_path.dirname.mkpath + origin_formula_path.write <<~RUBY + class Foo#{foo_version} < Formula + url "https://brew.sh/foo-1.0.tar.gz" + sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e" + revision 2 + version_scheme 1 + end + RUBY + + origin_tap_path.mkpath + origin_tap_path.cd do + system "git", "init" + system "git", "add", "--all" + system "git", "commit", "-m", "init" + end + + tap_path.mkpath + tap_path.cd do + system "git", "clone", origin_tap_path, "." + end + end + + describe "checksums" do + describe "should not change with the same version" do + before do + formula_gsub( + 'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', + 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', + ) + end + + it { is_expected.to match("stable sha256 changed without the url/version also changing") } + end + + describe "should not change with the same version when not the first commit" do + before do + formula_gsub_origin_commit( + 'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', + 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', + ) + formula_gsub_origin_commit "revision 2" + formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub( + 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', + 'sha256 "e048c5e6144f5932d8672c2fade81d9073d5b3ca1517b84df006de3d25414fc1"', + ) + end + + it { is_expected.to match("stable sha256 changed without the url/version also changing") } + end + + describe "can change with the different version" do + before do + formula_gsub_origin_commit( + 'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', + 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', + ) + formula_gsub "foo-1.0.tar.gz", "foo-1.1.tar.gz" + formula_gsub_origin_commit( + 'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"', + 'sha256 "e048c5e6144f5932d8672c2fade81d9073d5b3ca1517b84df006de3d25414fc1"', + ) + end + + it { is_expected.to be_nil } + end + + describe "can be removed when switching schemes" do + before do + formula_gsub_origin_commit( + 'url "https://brew.sh/foo-1.0.tar.gz"', + 'url "https://foo.com/brew/bar.git", tag: "1.0", revision: "f5e00e485e7aa4c5baa20355b27e3b84a6912790"', + ) + formula_gsub_origin_commit('sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"', + "") + end + + it { is_expected.to be_nil } + end + end + end + + describe "#audit_versioned_keg_only" do + specify "it warns when a versioned formula is not `keg_only`" do + fa = formula_auditor "foo@1.1", <<~RUBY, core_tap: true + class FooAT11 < Formula + url "https://brew.sh/foo-1.1.tgz" + end + RUBY + + fa.audit_versioned_keg_only + + expect(fa.problems.first[:message]) + .to match("Versioned formulae in homebrew/core should use `keg_only :versioned_formula`") + end + + specify "it warns when a versioned formula has an incorrect `keg_only` reason" do + fa = formula_auditor "foo@1.1", <<~RUBY, core_tap: true + class FooAT11 < Formula + url "https://brew.sh/foo-1.1.tgz" + + keg_only :provided_by_macos + end + RUBY + + fa.audit_versioned_keg_only + + expect(fa.problems.first[:message]) + .to match("Versioned formulae in homebrew/core should use `keg_only :versioned_formula`") + end + + specify "it does not warn when a versioned formula has `keg_only :versioned_formula`" do + fa = formula_auditor "foo@1.1", <<~RUBY, core_tap: true + class FooAT11 < Formula + url "https://brew.sh/foo-1.1.tgz" + + keg_only :versioned_formula + end + RUBY + + fa.audit_versioned_keg_only + + expect(fa.problems).to be_empty + end + end + + describe "#audit_conflicts" do + before do + # We don't really test FormulaTextAuditor here + allow(File).to receive(:open).and_return("") + end + + specify "it warns when conflicting with non-existing formula" do + foo = formula("foo") do + url "https://brew.sh/bar-1.0.tgz" + + conflicts_with "bar" + end + + fa = described_class.new foo + fa.audit_conflicts + + expect(fa.problems.first[:message]) + .to match("Can't find conflicting formula \"bar\"") + end + + specify "it warns when conflicting with itself" do + foo = formula("foo") do + url "https://brew.sh/bar-1.0.tgz" + + conflicts_with "foo" + end + stub_formula_loader foo + + fa = described_class.new foo + fa.audit_conflicts + + expect(fa.problems.first[:message]) + .to match("Formula should not conflict with itself") + end + + specify "it warns when another formula does not have a symmetric conflict" do + stub_formula_loader formula("gcc") { url "gcc-1.0" } + stub_formula_loader formula("glibc") { url "glibc-1.0" } + + foo = formula("foo") do + url "https://brew.sh/foo-1.0.tgz" + end + stub_formula_loader foo + + bar = formula("bar") do + url "https://brew.sh/bar-1.0.tgz" + + conflicts_with "foo" + end + + fa = described_class.new bar + fa.audit_conflicts + + expect(fa.problems.first[:message]) + .to match("Formula foo should also have a conflict declared with bar") + end + end +end diff --git a/Library/Homebrew/test/formula_free_port_spec.rb b/Library/Homebrew/test/formula_free_port_spec.rb deleted file mode 100644 index d3e907825a..0000000000 --- a/Library/Homebrew/test/formula_free_port_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -require "socket" -require "formula_free_port" - -module Homebrew - RSpec.describe FreePort do - include described_class - - describe "#free_port" do - it "returns a free TCP/IP port" do - # IANA suggests user port from 1024 to 49151 - # and dynamic port for 49152 to 65535 - # http://www.iana.org/assignments/port-numbers - min_port = 1024 - max_port = 65535 - port = free_port - - expect(port).to be_between(min_port, max_port) - expect { TCPServer.new(port).close }.not_to raise_error - end - end - end -end diff --git a/Library/Homebrew/test/formula_text_auditor_spec.rb b/Library/Homebrew/test/formula_text_auditor_spec.rb new file mode 100644 index 0000000000..20971959f6 --- /dev/null +++ b/Library/Homebrew/test/formula_text_auditor_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "formula_text_auditor" + +RSpec.describe Homebrew::FormulaTextAuditor do + alias_matcher :have_data, :be_data + alias_matcher :have_end, :be_end + alias_matcher :have_trailing_newline, :be_trailing_newline + + let(:dir) { mktmpdir } + + def formula_text(name, body = nil, options = {}) + path = dir/"#{name}.rb" + + path.write <<~RUBY + class #{Formulary.class_s(name)} < Formula + #{body} + end + #{options[:patch]} + RUBY + + described_class.new(path) + end + + specify "simple valid Formula" do + ft = formula_text "valid", <<~RUBY + url "https://www.brew.sh/valid-1.0.tar.gz" + RUBY + + expect(ft).to have_trailing_newline + + expect(ft =~ /\burl\b/).to be_truthy + expect(ft.line_number(/desc/)).to be_nil + expect(ft.line_number(/\burl\b/)).to eq(2) + expect(ft).to include("Valid") + end + + specify "#trailing_newline?" do + ft = formula_text "newline" + expect(ft).to have_trailing_newline + end +end diff --git a/Library/Homebrew/test/free_port_spec.rb b/Library/Homebrew/test/free_port_spec.rb new file mode 100644 index 0000000000..94d3059685 --- /dev/null +++ b/Library/Homebrew/test/free_port_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "socket" +require "formula_free_port" + +RSpec.describe Homebrew::FreePort do + include described_class + + describe "#free_port" do + it "returns a free TCP/IP port" do + # IANA suggests user port from 1024 to 49151 + # and dynamic port for 49152 to 65535 + # http://www.iana.org/assignments/port-numbers + min_port = 1024 + max_port = 65535 + port = free_port + + expect(port).to be_between(min_port, max_port) + expect { TCPServer.new(port).close }.not_to raise_error + end + end +end diff --git a/Library/Homebrew/test/rubocops/exec_shell_metacharacters_spec.rb b/Library/Homebrew/test/rubocops/exec_shell_metacharacters_spec.rb new file mode 100644 index 0000000000..2f5c4ab4f1 --- /dev/null +++ b/Library/Homebrew/test/rubocops/exec_shell_metacharacters_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "rubocops/shell_commands" + +RSpec.describe RuboCop::Cop::Homebrew::ExecShellMetacharacters do + subject(:cop) { described_class.new } + + context "when auditing exec calls" do + it "reports aan offense when output piping is used" do + expect_offense(<<~RUBY) + fork do + exec "foo bar > output" + ^^^^^^^^^^^^^^^^^^ Homebrew/ExecShellMetacharacters: Don't use shell metacharacters in `exec`. Implement the logic in Ruby instead, using methods like `$stdout.reopen`. + end + RUBY + end + + it "reports no offenses when no metacharacters are used" do + expect_no_offenses(<<~RUBY) + fork do + exec "foo bar" + end + RUBY + end + end +end diff --git a/Library/Homebrew/test/rubocops/shell_commands_spec.rb b/Library/Homebrew/test/rubocops/shell_commands_spec.rb index 6c464f3e83..bcd8601ea9 100644 --- a/Library/Homebrew/test/rubocops/shell_commands_spec.rb +++ b/Library/Homebrew/test/rubocops/shell_commands_spec.rb @@ -2,239 +2,210 @@ require "rubocops/shell_commands" -module RuboCop - module Cop - module Homebrew - ::RSpec.describe ShellCommands do - subject(:cop) { described_class.new } +RSpec.describe RuboCop::Cop::Homebrew::ShellCommands do + subject(:cop) { described_class.new } - context "when auditing shell commands" do - it "reports and corrects an offense when `system` arguments should be separated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - system "foo bar" - ^^^^^^^^^ Homebrew/ShellCommands: Separate `system` commands into `"foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - system "foo", "bar" - end - end - RUBY - end - - it "reports and corrects an offense when `system` arguments involving interpolation should be separated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - system "\#{bin}/foo bar" - ^^^^^^^^^^^^^^^^ Homebrew/ShellCommands: Separate `system` commands into `"\#{bin}/foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - system "\#{bin}/foo", "bar" - end - end - RUBY - end - - it "reports no offenses when `system` with metacharacter arguments are called" do - expect_no_offenses(<<~RUBY) - class Foo < Formula - def install - system "foo bar > baz" - end - end - RUBY - end - - it "reports no offenses when trailing arguments to `system` are unseparated" do - expect_no_offenses(<<~RUBY) - class Foo < Formula - def install - system "foo", "bar baz" - end - end - RUBY - end - - it "reports no offenses when `Utils.popen` arguments are unseparated" do - expect_no_offenses(<<~RUBY) - class Foo < Formula - def install - Utils.popen("foo bar") - end - end - RUBY - end - - it "reports and corrects an offense when `Utils.popen_read` arguments are unseparated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read("foo bar") - ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_read` commands into `"foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read("foo", "bar") - end - end - RUBY - end - - it "reports and corrects an offense when `Utils.safe_popen_read` arguments are unseparated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - Utils.safe_popen_read("foo bar") - ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.safe_popen_read` commands into `"foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - Utils.safe_popen_read("foo", "bar") - end - end - RUBY - end - - it "reports and corrects an offense when `Utils.popen_write` arguments are unseparated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - Utils.popen_write("foo bar") - ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_write` commands into `"foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - Utils.popen_write("foo", "bar") - end - end - RUBY - end - - it "reports and corrects an offense when `Utils.safe_popen_write` arguments are unseparated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - Utils.safe_popen_write("foo bar") - ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.safe_popen_write` commands into `"foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - Utils.safe_popen_write("foo", "bar") - end - end - RUBY - end - - it "reports and corrects an offense when `Utils.popen_read` arguments with interpolation are unseparated" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read("\#{bin}/foo bar") - ^^^^^^^^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_read` commands into `"\#{bin}/foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read("\#{bin}/foo", "bar") - end - end - RUBY - end - - it "reports no offenses when `Utils.popen_read` arguments with metacharacters are unseparated" do - expect_no_offenses(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read("foo bar > baz") - end - end - RUBY - end - - it "reports no offenses when trailing arguments to `Utils.popen_read` are unseparated" do - expect_no_offenses(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read("foo", "bar baz") - end - end - RUBY - end - - it "reports and corrects an offense when `Utils.popen_read` arguments are unseparated after a shell env" do - expect_offense(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read({ "SHELL" => "bash"}, "foo bar") - ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_read` commands into `"foo", "bar"` - end - end - RUBY - - expect_correction(<<~RUBY) - class Foo < Formula - def install - Utils.popen_read({ "SHELL" => "bash"}, "foo", "bar") - end - end - RUBY + context "when auditing shell commands" do + it "reports and corrects an offense when `system` arguments should be separated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + system "foo bar" + ^^^^^^^^^ Homebrew/ShellCommands: Separate `system` commands into `"foo", "bar"` end end - end + RUBY - ::RSpec.describe ExecShellMetacharacters do - subject(:cop) { described_class.new } - - context "when auditing exec calls" do - it "reports aan offense when output piping is used" do - expect_offense(<<~RUBY) - fork do - exec "foo bar > output" - ^^^^^^^^^^^^^^^^^^ Homebrew/ExecShellMetacharacters: Don't use shell metacharacters in `exec`. Implement the logic in Ruby instead, using methods like `$stdout.reopen`. - end - RUBY - end - - it "reports no offenses when no metacharacters are used" do - expect_no_offenses(<<~RUBY) - fork do - exec "foo bar" - end - RUBY + expect_correction(<<~RUBY) + class Foo < Formula + def install + system "foo", "bar" end end - end + RUBY + end + + it "reports and corrects an offense when `system` arguments involving interpolation should be separated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + system "\#{bin}/foo bar" + ^^^^^^^^^^^^^^^^ Homebrew/ShellCommands: Separate `system` commands into `"\#{bin}/foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + system "\#{bin}/foo", "bar" + end + end + RUBY + end + + it "reports no offenses when `system` with metacharacter arguments are called" do + expect_no_offenses(<<~RUBY) + class Foo < Formula + def install + system "foo bar > baz" + end + end + RUBY + end + + it "reports no offenses when trailing arguments to `system` are unseparated" do + expect_no_offenses(<<~RUBY) + class Foo < Formula + def install + system "foo", "bar baz" + end + end + RUBY + end + + it "reports no offenses when `Utils.popen` arguments are unseparated" do + expect_no_offenses(<<~RUBY) + class Foo < Formula + def install + Utils.popen("foo bar") + end + end + RUBY + end + + it "reports and corrects an offense when `Utils.popen_read` arguments are unseparated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read("foo bar") + ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_read` commands into `"foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read("foo", "bar") + end + end + RUBY + end + + it "reports and corrects an offense when `Utils.safe_popen_read` arguments are unseparated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + Utils.safe_popen_read("foo bar") + ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.safe_popen_read` commands into `"foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + Utils.safe_popen_read("foo", "bar") + end + end + RUBY + end + + it "reports and corrects an offense when `Utils.popen_write` arguments are unseparated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + Utils.popen_write("foo bar") + ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_write` commands into `"foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + Utils.popen_write("foo", "bar") + end + end + RUBY + end + + it "reports and corrects an offense when `Utils.safe_popen_write` arguments are unseparated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + Utils.safe_popen_write("foo bar") + ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.safe_popen_write` commands into `"foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + Utils.safe_popen_write("foo", "bar") + end + end + RUBY + end + + it "reports and corrects an offense when `Utils.popen_read` arguments with interpolation are unseparated" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read("\#{bin}/foo bar") + ^^^^^^^^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_read` commands into `"\#{bin}/foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read("\#{bin}/foo", "bar") + end + end + RUBY + end + + it "reports no offenses when `Utils.popen_read` arguments with metacharacters are unseparated" do + expect_no_offenses(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read("foo bar > baz") + end + end + RUBY + end + + it "reports no offenses when trailing arguments to `Utils.popen_read` are unseparated" do + expect_no_offenses(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read("foo", "bar baz") + end + end + RUBY + end + + it "reports and corrects an offense when `Utils.popen_read` arguments are unseparated after a shell env" do + expect_offense(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read({ "SHELL" => "bash"}, "foo bar") + ^^^^^^^^^ Homebrew/ShellCommands: Separate `Utils.popen_read` commands into `"foo", "bar"` + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo < Formula + def install + Utils.popen_read({ "SHELL" => "bash"}, "foo", "bar") + end + end + RUBY end end end