audit_spec.rb 34.6 KB
Newer Older
1
# typed: false
2
3
# frozen_string_literal: true

4
5
require "dev-cmd/audit"
require "formulary"
Mike McQuaid's avatar
Mike McQuaid committed
6
require "cmd/shared_examples/args_parse"
7
require "utils/spdx"
Mike McQuaid's avatar
Mike McQuaid committed
8
9
10
11

describe "Homebrew.audit_args" do
  it_behaves_like "parseable arguments"
end
12

13
14
15
module Count
  def self.increment
    @count ||= 0
16
    @count += 1
17
18
19
  end
end

20
module Homebrew
21
  describe FormulaTextAuditor do
22
23
24
    alias_matcher :have_data, :be_data
    alias_matcher :have_end, :be_end
    alias_matcher :have_trailing_newline, :be_trailing_newline
Markus Reiter's avatar
Markus Reiter committed
25

26
    let(:dir) { mktmpdir }
27

28
29
    def formula_text(name, body = nil, options = {})
      path = dir/"#{name}.rb"
30

31
      path.write <<~RUBY
32
33
34
35
        class #{Formulary.class_s(name)} < Formula
          #{body}
        end
        #{options[:patch]}
36
      RUBY
37

38
39
      described_class.new(path)
    end
40

41
    specify "simple valid Formula" do
42
      ft = formula_text "valid", <<~RUBY
43
        url "https://www.brew.sh/valid-1.0.tar.gz"
44
      RUBY
45

46
      expect(ft).to have_trailing_newline
47

48
49
50
51
52
      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
53

54
55
56
57
    specify "#trailing_newline?" do
      ft = formula_text "newline"
      expect(ft).to have_trailing_newline
    end
58
59
  end

60
61
62
63
64
65
  describe FormulaAuditor do
    def formula_auditor(name, text, options = {})
      path = Pathname.new "#{dir}/#{name}.rb"
      path.open("w") do |f|
        f.write text
      end
66

67
      described_class.new(Formulary.factory(path), options)
68
69
    end

70
    let(:dir) { mktmpdir }
71

72
73
    describe "#problems" do
      it "is empty by default" do
74
        fa = formula_auditor "foo", <<~RUBY
75
          class Foo < Formula
76
            url "https://brew.sh/foo-1.0.tgz"
77
          end
78
        RUBY
79

80
81
        expect(fa.problems).to be_empty
      end
82
83
    end

84
    describe "#audit_license" do
85
86
      let(:spdx_license_data) { SPDX.license_data }
      let(:spdx_exception_data) { SPDX.exception_data }
lionellloh's avatar
lionellloh committed
87

88
      let(:deprecated_spdx_id) { "GPL-1.0" }
89
90
91
92
93
94
95
96
      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"]' }
97

98
      it "does not check if the formula is not a new formula" do
99
        fa = formula_auditor "foo", <<~RUBY, new_formula: false
100
101
102
103
104
105
106
107
108
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
          end
        RUBY

        fa.audit_license
        expect(fa.problems).to be_empty
      end

109
      it "detects no license info" do
110
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true, core_tap: true
111
112
113
114
115
116
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
          end
        RUBY

        fa.audit_license
117
        expect(fa.problems.first[:message]).to match "Formulae in homebrew/core must specify a license."
118
119
      end

120
      it "detects if license is not a standard spdx-id" do
121
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true
122
123
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
124
            license "zzz"
125
126
127
128
          end
        RUBY

        fa.audit_license
129
        expect(fa.problems.first[:message]).to match <<~EOS
130
131
132
          Formula foo contains non-standard SPDX licenses: ["zzz"].
          For a list of valid licenses check: https://spdx.org/licenses/
        EOS
133
134
      end

135
      it "detects if license is a deprecated spdx-id" do
136
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true, strict: true
137
138
139
140
141
142
143
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
            license "#{deprecated_spdx_id}"
          end
        RUBY

        fa.audit_license
144
        expect(fa.problems.first[:message]).to match <<~EOS
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
          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: 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
160
        expect(fa.problems.first[:message]).to match <<~EOS
161
162
163
          Formula foo contains non-standard SPDX licenses: ["zzz"].
          For a list of valid licenses check: https://spdx.org/licenses/
        EOS
164
165
      end

166
      it "detects if license array contains a non-standard spdx-id" do
167
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true
168
169
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
170
            license #{license_any_nonstandard}
171
172
173
174
          end
        RUBY

        fa.audit_license
175
        expect(fa.problems.first[:message]).to match <<~EOS
176
177
178
          Formula foo contains non-standard SPDX licenses: ["zzz"].
          For a list of valid licenses check: https://spdx.org/licenses/
        EOS
179
180
      end

181
      it "detects if license array contains a deprecated spdx-id" do
182
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true, strict: true
183
184
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
185
            license #{license_any_deprecated}
186
187
188
189
          end
        RUBY

        fa.audit_license
190
        expect(fa.problems.first[:message]).to match <<~EOS
191
192
193
194
          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
195
196
      end

197
      it "verifies that a license info is a standard spdx id" do
198
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true
199
200
201
202
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
            license "0BSD"
          end
203
204
205
206
207
208
        RUBY

        fa.audit_license
        expect(fa.problems).to be_empty
      end

209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
      it "verifies that a license info with plus is a standard spdx id" do
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: 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: 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: 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_license_data, spdx_exception_data: spdx_exception_data

        fa.audit_license
        expect(fa.problems).to be_empty
      end

259
      it "verifies that a license array contains only standard spdx id" do
260
        fa = formula_auditor "foo", <<~RUBY, spdx_license_data: spdx_license_data, new_formula: true
261
262
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
            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: 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: spdx_license_data, new_formula: true
          class Foo < Formula
            url "https://brew.sh/foo-1.0.tgz"
            license #{license_nested_conditions}
288
289
290
291
292
293
294
          end
        RUBY

        fa.audit_license
        expect(fa.problems).to be_empty
      end

295
296
      it "checks online and verifies that a standard license id is the same "\
        "as what is indicated on its Github repo" do
297
        formula_text = <<~RUBY
298
          class Cask < Formula
lionellloh's avatar
lionellloh committed
299
300
301
            url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
            head "https://github.com/cask/cask.git"
            license "GPL-3.0"
302
303
          end
        RUBY
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
        fa = formula_auditor "cask", formula_text, spdx_license_data: 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" 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: spdx_license_data,
                             online: true, core_tap: true, new_formula: true
322
323

        fa.audit_license
lionellloh's avatar
lionellloh committed
324
        expect(fa.problems).to be_empty
325
      end
326

327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
      it "checks online and verifies that a standard license id with WITH is the same "\
        "as what is indicated on its Github repo" 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_license_data, spdx_exception_data: spdx_exception_data

        fa.audit_license
        expect(fa.problems).to be_empty
      end

      it "verifies that a license exception has standard spdx ids" 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_license_data, spdx_exception_data: spdx_exception_data

        fa.audit_license
355
        expect(fa.problems.first[:message]).to match <<~EOS
356
357
          Formula cask contains invalid or deprecated SPDX license exceptions: ["zzz"].
          For a list of valid license exceptions check:
358
            https://spdx.org/licenses/exceptions-index.html
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
        EOS
      end

      it "verifies that a license exception has non-deprecated spdx ids" 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_license_data, spdx_exception_data: spdx_exception_data

        fa.audit_license
374
        expect(fa.problems.first[:message]).to match <<~EOS
375
376
          Formula cask contains invalid or deprecated SPDX license exceptions: ["#{deprecated_spdx_exception}"].
          For a list of valid license exceptions check:
377
            https://spdx.org/licenses/exceptions-index.html
378
379
380
        EOS
      end

Jonathan Chang's avatar
Jonathan Chang committed
381
382
      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" do
383
        fa = formula_auditor "cask", <<~RUBY, spdx_license_data: spdx_license_data, online: true, new_formula: true
Jonathan Chang's avatar
Jonathan Chang committed
384
385
386
387
388
389
390
391
392
393
394
          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

Jonathan Chang's avatar
Jonathan Chang committed
395
396
      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" do
397
        fa = formula_auditor "cask", <<~RUBY, spdx_license_data: spdx_license_data, online: true, new_formula: true
Jonathan Chang's avatar
Jonathan Chang committed
398
399
400
          class Cask < Formula
            url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
            head "https://github.com/cask/cask.git"
401
            license any_of: ["GPL-3.0-or-later", "MIT"]
Jonathan Chang's avatar
Jonathan Chang committed
402
403
404
405
406
407
408
          end
        RUBY

        fa.audit_license
        expect(fa.problems).to be_empty
      end

409
410
      it "checks online and detects that a formula-specified license is not "\
        "the same as what is indicated on its Github repository" do
411
        formula_text = <<~RUBY
412
          class Cask < Formula
lionellloh's avatar
lionellloh committed
413
414
            url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
            head "https://github.com/cask/cask.git"
415
            license "0BSD"
416
417
          end
        RUBY
418
419
        fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
                             online: true, core_tap: true, new_formula: true
420
421

        fa.audit_license
422
423
        expect(fa.problems.first[:message])
          .to eq 'Formula license ["0BSD"] does not match GitHub license ["GPL-3.0"].'
424
      end
425

Rylan Polster's avatar
Rylan Polster committed
426
      it "allows a formula-specified license that differs from its GitHub "\
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
         "repository for formulae on the mismatched license allowlist" 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: 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

443
444
      it "checks online and detects that an array of license does not contain "\
        "what is indicated on its Github repository" do
445
        formula_text = <<~RUBY
446
447
448
          class Cask < Formula
            url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
            head "https://github.com/cask/cask.git"
449
            license #{license_any_mismatch}
450
451
          end
        RUBY
452
453
        fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
                             online: true, core_tap: true, new_formula: true
454
455

        fa.audit_license
456
        expect(fa.problems.first[:message]).to match "Formula license [\"0BSD\", \"MIT\"] "\
Jonathan Chang's avatar
Jonathan Chang committed
457
          "does not match GitHub license [\"GPL-3.0\"]."
458
459
460
461
      end

      it "checks online and verifies that an array of license contains "\
        "what is indicated on its Github repository" do
462
        formula_text = <<~RUBY
463
464
465
          class Cask < Formula
            url "https://github.com/cask/cask/archive/v0.8.4.tar.gz"
            head "https://github.com/cask/cask.git"
466
            license #{license_any}
467
468
          end
        RUBY
469
470
        fa = formula_auditor "cask", formula_text, spdx_license_data: spdx_license_data,
                             online: true, core_tap: true, new_formula: true
471
472
473
474

        fa.audit_license
        expect(fa.problems).to be_empty
      end
475
476
    end

477
478
    describe "#audit_file" do
      specify "no issue" do
479
        fa = formula_auditor "foo", <<~RUBY
480
          class Foo < Formula
481
482
            url "https://brew.sh/foo-1.0.tgz"
            homepage "https://brew.sh"
483
          end
484
        RUBY
485

486
        fa.audit_file
487
        expect(fa.problems).to be_empty
488
      end
489
490
    end

491
492
493
    describe "#audit_github_repository" do
      specify "#audit_github_repository when HOMEBREW_NO_GITHUB_API is set" do
        ENV["HOMEBREW_NO_GITHUB_API"] = "1"
494

495
        fa = formula_auditor "foo", <<~RUBY, strict: true, online: true
496
497
          class Foo < Formula
            homepage "https://github.com/example/example"
498
            url "https://brew.sh/foo-1.0.tgz"
499
          end
500
        RUBY
501

502
        fa.audit_github_repository
503
        expect(fa.problems).to be_empty
504
      end
505
506
    end

Sean Molenaar's avatar
Sean Molenaar committed
507
508
509
510
511
512
513
514
515
516
    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
517
        expect(fa.problems).to be_empty
Sean Molenaar's avatar
Sean Molenaar committed
518
519
520
      end
    end

Sean Molenaar's avatar
Sean Molenaar committed
521
522
523
524
525
526
527
528
529
530
    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
531
        expect(fa.problems).to be_empty
Sean Molenaar's avatar
Sean Molenaar committed
532
533
534
      end
    end

Sean Molenaar's avatar
Sean Molenaar committed
535
536
537
538
539
540
541
542
543
544
    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
545
        expect(fa.problems).to be_empty
Sean Molenaar's avatar
Sean Molenaar committed
546
547
548
      end
    end

549
550
551
552
553
554
555
556
557
558
    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
559
        expect(fa.problems).to be_empty
560
561
562
      end
    end

563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
    describe "#audit_specs" do
      let(:throttle_list) { { throttled_formulae: { "foo" => 10 } } }
      let(:versioned_head_spec_list) { { versioned_head_spec_allowlist: ["foo"] } }

      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"
          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"
          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"
          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"
          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"
            head "https://brew.sh/foo-1.0.tgz"
          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"
            head "https://brew.sh/foo-1.0.tgz"
          end
        RUBY

        fa.audit_specs
        expect(fa.problems.first[:message]).to match "Versioned formulae should not have a `HEAD` spec"
      end

      it "allows ersioned 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"
            head "https://brew.sh/foo-1.0.tgz"
          end
        RUBY

        fa.audit_specs
        expect(fa.problems).to be_empty
      end
    end

648
649
    describe "#audit_deps" do
      describe "a dependency on a macOS-provided keg-only formula" do
650
        describe "which is allowlisted" do
651
652
653
          subject { fa }

          let(:fa) do
654
            formula_auditor "foo", <<~RUBY, new_formula: true
655
              class Foo < Formula
656
657
                url "https://brew.sh/foo-1.0.tgz"
                homepage "https://brew.sh"
658

659
660
                depends_on "openssl"
              end
661
            RUBY
662
663
664
665
          end

          let(:f_openssl) do
            formula do
666
667
              url "https://brew.sh/openssl-1.0.tgz"
              homepage "https://brew.sh"
668

669
              keg_only :provided_by_macos
670
            end
671
          end
672

673
674
675
676
          before do
            allow(fa.formula.deps.first)
              .to receive(:to_formula).and_return(f_openssl)
            fa.audit_deps
677
678
          end

679
          its(:problems) { are_expected.to be_empty }
680
681
        end

682
        describe "which is not allowlisted", :needs_macos do
683
          subject { fa }
684

685
          let(:fa) do
686
            formula_auditor "foo", <<~RUBY, new_formula: true, core_tap: true
687
              class Foo < Formula
688
689
                url "https://brew.sh/foo-1.0.tgz"
                homepage "https://brew.sh"
690

691
692
                depends_on "bc"
              end
693
            RUBY
694
695
696
697
          end

          let(:f_bc) do
            formula do
698
699
              url "https://brew.sh/bc-1.0.tgz"
              homepage "https://brew.sh"
700

701
              keg_only :provided_by_macos
702
            end
703
          end
704

705
706
707
708
          before do
            allow(fa.formula.deps.first)
              .to receive(:to_formula).and_return(f_bc)
            fa.audit_deps
709
710
          end

711
712
713
          its(:new_formula_problems) {
            are_expected.to include(a_hash_including(message: a_string_matching(/is provided by macOS/)))
          }
714
715
716
717
        end
      end
    end

718
    describe "#audit_revision_and_version_scheme" do
719
      subject {
720
        fa = described_class.new(Formulary.factory(formula_path), git: true)
721
        fa.audit_revision_and_version_scheme
722
        fa.problems.first&.fetch(:message)
723
      }
724
725

      let(:origin_tap_path) { Tap::TAP_DIRECTORY/"homebrew/homebrew-foo" }
726
727
      let(:foo_version) { Count.increment }
      let(:formula_subpath) { "Formula/foo#{foo_version}.rb" }
728
729
730
      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 }
731

732
      before do
733
        origin_formula_path.write <<~RUBY
734
          class Foo#{foo_version} < Formula
735
            url "https://brew.sh/foo-1.0.tar.gz"
736
            sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"
737
738
739
            revision 2
            version_scheme 1
          end
740
        RUBY
741

742
743
744
745
746
        origin_tap_path.mkpath
        origin_tap_path.cd do
          system "git", "init"
          system "git", "add", "--all"
          system "git", "commit", "-m", "init"
747
748
        end

749
750
751
752
        tap_path.mkpath
        tap_path.cd do
          system "git", "clone", origin_tap_path, "."
        end
753
754
      end

755
756
757
758
759
      def formula_gsub(before, after = "")
        text = formula_path.read
        text.gsub! before, after
        formula_path.unlink
        formula_path.write text
760
761
      end

762
      def formula_gsub_origin_commit(before, after = "")
763
764
765
766
        text = origin_formula_path.read
        text.gsub!(before, after)
        origin_formula_path.unlink
        origin_formula_path.write text
767

768
769
770
        origin_tap_path.cd do
          system "git", "commit", "-am", "commit"
        end
771

772
773
774
775
        tap_path.cd do
          system "git", "fetch"
          system "git", "reset", "--hard", "origin/master"
        end
776
777
      end

778
779
780
781
782
783
784
785
786
      context "checksums" do
        context "should not change with the same version" do
          before do
            formula_gsub(
              'sha256 "31cccfc6630528db1c8e3a06f6decf2a370060b982841cfab2b8677400a5092e"',
              'sha256 "3622d2a53236ed9ca62de0616a7e80fd477a9a3f862ba09d503da188f53ca523"',
            )
          end

787
          it { is_expected.to match("stable sha256 changed without the url/version also changing") }
788
789
        end

790
791
792
793
794
795
796
797
798
799
800
801
802
803
        context "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

804
          it { is_expected.to match("stable sha256 changed without the url/version also changing") }
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
        end

        context "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
822
823
824
825
826
827
828
829
830
831
832
833
834

        context "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
835
836
      end

837
838
839
840
      context "revisions" do
        context "should not be removed when first committed above 0" do
          it { is_expected.to be_nil }
        end
841

842
        context "should not decrease with the same version" do
843
          before { formula_gsub_origin_commit "revision 2", "revision 1" }
844

845
846
          it { is_expected.to match("revision should not decrease (from 2 to 1)") }
        end
847

848
        context "should not be removed with the same version" do
849
          before { formula_gsub_origin_commit "revision 2" }
850

851
852
          it { is_expected.to match("revision should not decrease (from 2 to 0)") }
        end
853

854
855
        context "should not decrease with the same, uncommitted version" do
          before { formula_gsub "revision 2", "revision 1" }
856

857
858
          it { is_expected.to match("revision should not decrease (from 2 to 1)") }
        end
859

860
        context "should be removed with a newer version" do
861
          before { formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz" }
862

863
864
          it { is_expected.to match("'revision 2' should be removed") }
        end
865

866
867
868
869
870
871
        context "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

872
873
        context "should not warn on an newer version revision removal" do
          before do
874
875
            formula_gsub_origin_commit "revision 2", ""
            formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz"
876
          end
877

878
          it { is_expected.to be_nil }
879
880
        end

881
882
883
884
885
886
887
888
889
890
891
        context "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

892
893
894
895
896
        context "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
897

898
          it { is_expected.to match("revisions should only increment by 1") }
899
900
        end

901
902
        context "should not warn on past increment by more than 1" do
          before do
903
904
905
            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"
906
          end
907

908
          it { is_expected.to be_nil }
909
910
911
        end
      end

912
913
      context "version_schemes" do
        context "should not decrease with the same version" do
914
          before { formula_gsub_origin_commit "version_scheme 1" }
915

916
          it { is_expected.to match("version_scheme should not decrease (from 1 to 0)") }
917
918
        end

919
920
        context "should not decrease with a new version" do
          before do
921
922
            formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-1.1.tar.gz"
            formula_gsub_origin_commit "revision 2", ""
923
            formula_gsub_origin_commit "version_scheme 1", ""
924
          end
925

926
          it { is_expected.to match("version_scheme should not decrease (from 1 to 0)") }
927
928
        end

929
930
        context "should only increment by 1" do
          before do
931
932
933
934
            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"
935
          end
936

937
938
          it { is_expected.to match("version_schemes should only increment by 1") }
        end
939
940
      end

941
942
943
944
945
      context "versions" do
        context "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)") }
946
947
        end

948
949
        context "committed can decrease" do
          before do
950
951
            formula_gsub_origin_commit "revision 2"
            formula_gsub_origin_commit "foo-1.0.tar.gz", "foo-0.9.tar.gz"
952
          end
953

954
          it { is_expected.to be_nil }
955
956
        end

957
958
959
960
961
962
963
964
965
        context "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
966
967
      end
    end
968

969
970
971
972
    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
973
            url "https://brew.sh/foo-1.1.tgz"
974
975
976
977
978
          end
        RUBY

        fa.audit_versioned_keg_only

979
        expect(fa.problems.first[:message])
980
          .to match("Versioned formulae in homebrew/core should use `keg_only :versioned_formula`")
981
982
983
984
985
      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
986
            url "https://brew.sh/foo-1.1.tgz"
987
988
989
990
991
992
993

            keg_only :provided_by_macos
          end
        RUBY

        fa.audit_versioned_keg_only

994
        expect(fa.problems.first[:message])
995
          .to match("Versioned formulae in homebrew/core should use `keg_only :versioned_formula`")
996
997
998
999
1000
      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
For faster browsing, not all history is shown. View entire blame