github.com/blend/go-sdk@v1.20220411.3/copyright/copyright_test.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package copyright
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"fmt"
    14  	"io"
    15  	"io/fs"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  	"testing"
    20  	"text/template"
    21  	"time"
    22  
    23  	"github.com/blend/go-sdk/assert"
    24  	"github.com/blend/go-sdk/ref"
    25  )
    26  
    27  func Test_Copyright_GetStdout(t *testing.T) {
    28  	its := assert.New(t)
    29  
    30  	c := New()
    31  
    32  	its.Equal(os.Stdout, c.GetStdout())
    33  	buf := new(bytes.Buffer)
    34  	c.Stdout = buf
    35  	its.Equal(c.Stdout, c.GetStdout())
    36  	c.Quiet = ref.Bool(true)
    37  	its.Equal(io.Discard, c.GetStdout())
    38  }
    39  
    40  func Test_Copyright_GetStderr(t *testing.T) {
    41  	its := assert.New(t)
    42  
    43  	c := New()
    44  
    45  	its.Equal(os.Stderr, c.GetStderr())
    46  	buf := new(bytes.Buffer)
    47  	c.Stderr = buf
    48  	its.Equal(buf, c.GetStderr())
    49  	c.Quiet = ref.Bool(true)
    50  	its.Equal(io.Discard, c.GetStderr())
    51  }
    52  
    53  func Test_Copyright_mergeFileSections(t *testing.T) {
    54  	its := assert.New(t)
    55  
    56  	merged := Copyright{}.mergeFileSections([]byte("foo"), []byte("bar"), []byte("baz"))
    57  	its.Equal("foobarbaz", string(merged))
    58  }
    59  
    60  func Test_Copyright_fileHasCopyrightHeader(t *testing.T) {
    61  	its := assert.New(t)
    62  
    63  	var goodCorpus = []byte(`foo
    64  bar
    65  baz
    66  `)
    67  
    68  	notice, err := generateGoNotice(OptYear(2021))
    69  	its.Nil(err)
    70  
    71  	goodCorpusWithNotice := Copyright{}.mergeFileSections([]byte(notice), goodCorpus)
    72  	its.Contains(string(goodCorpusWithNotice), "Copyright (c) 2021")
    73  	its.True((Copyright{}).fileHasCopyrightHeader(goodCorpusWithNotice, []byte(notice)))
    74  }
    75  
    76  func Test_Copyright_fileHasCopyrightHeader_invalid(t *testing.T) {
    77  	its := assert.New(t)
    78  
    79  	c := Copyright{}
    80  
    81  	var invalidCorpus = []byte(`foo
    82  bar
    83  baz
    84  `)
    85  	expectedNotice, err := generateGoNotice(OptYear(2021))
    86  	its.Nil(err)
    87  
    88  	its.False(c.fileHasCopyrightHeader(invalidCorpus, []byte(expectedNotice)), "we haven't added the notice")
    89  }
    90  
    91  func Test_Copyright_fileHasCopyrightHeader_differentYear(t *testing.T) {
    92  	its := assert.New(t)
    93  
    94  	c := Copyright{}
    95  
    96  	var goodCorpus = []byte(`foo
    97  bar
    98  baz
    99  `)
   100  
   101  	notice, err := generateGoNotice(OptYear(2020))
   102  	its.Nil(err)
   103  
   104  	goodCorpusWithNotice := c.mergeFileSections(notice, goodCorpus)
   105  	its.Contains(string(goodCorpusWithNotice), "Copyright (c) 2020")
   106  
   107  	newNotice, err := generateGoNotice(OptYear(2021))
   108  	its.Nil(err)
   109  
   110  	its.True(c.fileHasCopyrightHeader(goodCorpusWithNotice, []byte(newNotice)))
   111  }
   112  
   113  func Test_Copyright_fileHasCopyrightHeader_leadingWhitespace(t *testing.T) {
   114  	its := assert.New(t)
   115  
   116  	c := Copyright{}
   117  
   118  	var goodCorpus = []byte(`foo
   119  bar
   120  baz
   121  `)
   122  
   123  	notice, err := generateGoNotice(OptYear(2021))
   124  	its.Nil(err)
   125  
   126  	goodCorpusWithNotice := c.mergeFileSections([]byte("\n\n"), notice, goodCorpus)
   127  	its.HasPrefix(string(goodCorpusWithNotice), "\n\n")
   128  	its.Contains(string(goodCorpusWithNotice), "Copyright (c) 2021")
   129  
   130  	its.True(c.fileHasCopyrightHeader(goodCorpusWithNotice, []byte(notice)))
   131  }
   132  
   133  func Test_Copyright_goBuildTagMatch(t *testing.T) {
   134  	its := assert.New(t)
   135  
   136  	c := Copyright{}
   137  
   138  	buildTag := []byte(`// +build foo
   139  
   140  `)
   141  	corpus := []byte(`foo
   142  bar
   143  baz
   144  `)
   145  
   146  	file := (Copyright{}).mergeFileSections(buildTag, corpus)
   147  
   148  	its.False(goBuildTagMatch.Match(corpus))
   149  	its.True(goBuildTagMatch.Match(c.mergeFileSections(buildTag)))
   150  
   151  	found := goBuildTagMatch.FindAll(file, -1)
   152  	its.NotEmpty(found)
   153  	its.True(goBuildTagMatch.Match(file))
   154  }
   155  
   156  func Test_Copyright_goBuildTagsMatch(t *testing.T) {
   157  	its := assert.New(t)
   158  
   159  	file := []byte(goBuildTags1) // testutil.GetTestFixture(its, "buildtags1.go")
   160  	its.True(goBuildTagMatch.Match(file))
   161  	found := goBuildTagMatch.Find(file)
   162  	its.Equal("//go:build tag1\n// +build tag1\n\n", string(found))
   163  
   164  	file2 := []byte(goBuildTags2) // testutil.GetTestFixture(its, "buildtags2.go")
   165  	its.True(goBuildTagMatch.Match(file2))
   166  	found2 := goBuildTagMatch.Find(file2)
   167  
   168  	expected := `// +build tag5
   169  //go:build tag1 && tag2 && tag3
   170  // +build tag1,tag2,tag3
   171  // +build tag6
   172  
   173  `
   174  	its.Equal(expected, string(found2))
   175  
   176  	file3 := []byte(goBuildTags3) // testutil.GetTestFixture(its, "buildtags3.go")
   177  	its.True(goBuildTagMatch.Match(file3))
   178  	found3 := goBuildTagMatch.Find(file3)
   179  	its.Equal("//go:build tag1 & tag2\n\n", string(found3))
   180  }
   181  
   182  func Test_Copyright_goInjectNotice(t *testing.T) {
   183  	its := assert.New(t)
   184  
   185  	c := Copyright{}
   186  
   187  	file := []byte(`foo
   188  bar
   189  baz
   190  `)
   191  
   192  	notice, err := generateGoNotice(OptYear(2021))
   193  	its.Nil(err)
   194  
   195  	output := c.goInjectNotice("foo.go", file, notice)
   196  	its.Contains(string(output), "Copyright (c) 2021")
   197  	its.HasSuffix(string(output), string(file))
   198  }
   199  
   200  func Test_Copyright_goInjectNotice_buildTag(t *testing.T) {
   201  	its := assert.New(t)
   202  	c := Copyright{}
   203  
   204  	buildTag := []byte(`// +build foo`)
   205  	corpus := []byte(`foo
   206  bar
   207  baz
   208  `)
   209  
   210  	file := c.mergeFileSections(buildTag, []byte("\n\n"), corpus)
   211  
   212  	notice, err := generateGoNotice(OptYear(2021))
   213  	its.Nil(err)
   214  
   215  	output := c.goInjectNotice("foo.go", file, notice)
   216  	its.Contains(string(output), "Copyright (c) 2021")
   217  	its.HasPrefix(string(output), string(buildTag)+"\n")
   218  	its.HasSuffix(string(output), string(corpus))
   219  
   220  	outputRepeat := c.goInjectNotice("foo.go", output, notice)
   221  	its.Empty(outputRepeat, "inject notice functions should return an empty slice if the header already exists")
   222  }
   223  
   224  func Test_Copyright_goInjectNotice_goBuildTags(t *testing.T) {
   225  	t.Parallel()
   226  
   227  	type testCase struct {
   228  		Name   string
   229  		Input  string
   230  		Expect string
   231  	}
   232  
   233  	cases := []testCase{
   234  		{
   235  			Name:   "standard build tags",
   236  			Input:  goBuildTags1, // "buildtags1.go",
   237  			Expect: goldenGoBuildTags1,
   238  		},
   239  		{
   240  			Name:   "multiple build tags",
   241  			Input:  goBuildTags2, // "buildtags2.go",
   242  			Expect: goldenGoBuildTags2,
   243  		},
   244  		{
   245  			Name:   "build tags split across file",
   246  			Input:  goBuildTags3, // "buildtags3.go",
   247  			Expect: goldenGoBuildTags3,
   248  		},
   249  	}
   250  
   251  	for _, tc := range cases {
   252  		tc := tc
   253  		t.Run(tc.Name, func(t *testing.T) {
   254  			it := assert.New(t)
   255  			c := Copyright{}
   256  
   257  			notice, err := generateGoNotice(OptYear(2001))
   258  			it.Nil(err)
   259  
   260  			output := c.goInjectNotice("foo.go", []byte(tc.Input), notice)
   261  			it.Equal(string(output), tc.Expect) // testutil.AssertGoldenFile(it, output, tc.TestFile)
   262  
   263  			outputRepeat := c.goInjectNotice("foo.go", output, notice)
   264  			it.Empty(outputRepeat)
   265  		})
   266  	}
   267  }
   268  
   269  func Test_Copyright_tsInjectNotice_tsReferenceTags(t *testing.T) {
   270  	t.Parallel()
   271  
   272  	type testCase struct {
   273  		Name   string
   274  		Input  string
   275  		Expect string
   276  	}
   277  
   278  	cases := []testCase{
   279  		{
   280  			Name:   "single reference tag",
   281  			Input:  tsReferenceTag,
   282  			Expect: goldenTsReferenceTag,
   283  		},
   284  		{
   285  			Name:   "multiple reference tags",
   286  			Input:  tsReferenceTags,
   287  			Expect: goldenTsReferenceTags,
   288  		},
   289  		{
   290  			Name:   "no reference tags",
   291  			Input:  tsTest, // "buildtags3.go",
   292  			Expect: goldenTs,
   293  		},
   294  	}
   295  
   296  	for _, tc := range cases {
   297  		tc := tc
   298  		t.Run(tc.Name, func(t *testing.T) {
   299  			it := assert.New(t)
   300  			c := Copyright{}
   301  
   302  			notice, err := generateTypescriptNotice(OptYear(2022))
   303  			it.Nil(err)
   304  
   305  			output := c.tsInjectNotice("foo.ts", []byte(tc.Input), notice)
   306  			it.Equal(tc.Expect, string(output))
   307  
   308  			outputRepeat := c.tsInjectNotice("foo.ts", output, notice)
   309  			it.Empty(outputRepeat)
   310  		})
   311  	}
   312  }
   313  
   314  func Test_Copyright_injectNotice_typescript(t *testing.T) {
   315  	its := assert.New(t)
   316  
   317  	c := Copyright{}
   318  
   319  	file := []byte(`foo
   320  bar
   321  baz
   322  `)
   323  
   324  	notice, err := generateTypescriptNotice(OptYear(2001))
   325  	its.Nil(err)
   326  
   327  	output := c.injectNotice("foo.ts", file, notice)
   328  	its.Contains(string(output), "Copyright (c) 2001")
   329  	its.HasSuffix(string(output), string(file))
   330  
   331  	outputRepeat := c.injectNotice("foo.ts", output, notice)
   332  	its.Empty(outputRepeat, "inject notice functions should return an empty slice if the header already exists")
   333  }
   334  
   335  func Test_Copyright_injectNotice_typescript_referenceTags(t *testing.T) {
   336  	its := assert.New(t)
   337  
   338  	c := Copyright{}
   339  
   340  	file := []byte(tsReferenceTags)
   341  
   342  	notice, err := generateTypescriptNotice(OptYear(2001))
   343  	its.Nil(err)
   344  
   345  	output := c.injectNotice("foo.ts", file, notice)
   346  	its.Contains(string(output), "Copyright (c) 2001")
   347  	its.HasSuffix(string(output), string(file))
   348  
   349  	outputRepeat := c.injectNotice("foo.ts", output, notice)
   350  	its.Empty(outputRepeat, "inject notice functions should return an empty slice if the header already exists")
   351  }
   352  
   353  func Test_Copyright_goInjectNotice_openSource(t *testing.T) {
   354  	its := assert.New(t)
   355  
   356  	c := new(Copyright)
   357  
   358  	file := []byte(`foo
   359  bar
   360  baz
   361  `)
   362  
   363  	notice, err := generateGoNotice(
   364  		OptYear(2021),
   365  		OptLicense("Apache 2.0"),
   366  		OptRestrictions(DefaultRestrictionsOpenSource),
   367  	)
   368  	its.Nil(err)
   369  
   370  	output := c.goInjectNotice("foo.go", file, notice)
   371  	its.Contains(string(output), "Copyright (c) 2021")
   372  	its.Contains(string(output), "Use of this source code is governed by a Apache 2.0")
   373  	its.HasSuffix(string(output), string(file))
   374  }
   375  
   376  func generateGoNotice(opts ...Option) ([]byte, error) {
   377  	c := New(opts...)
   378  	noticeBody, err := c.compileNoticeBodyTemplate(c.NoticeBodyTemplateOrDefault())
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  
   383  	compiled, err := c.compileNoticeTemplate(goNoticeTemplate, noticeBody)
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	return []byte(compiled), nil
   388  }
   389  
   390  func generateTypescriptNotice(opts ...Option) ([]byte, error) {
   391  	c := New(opts...)
   392  	noticeBody, err := c.compileNoticeBodyTemplate(c.NoticeBodyTemplateOrDefault())
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	compiled, err := c.compileNoticeTemplate(tsNoticeTemplate, noticeBody)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  	return []byte(compiled), nil
   402  }
   403  
   404  func Test_Copyright_GetNoticeTemplate(t *testing.T) {
   405  	its := assert.New(t)
   406  
   407  	c := Copyright{}
   408  
   409  	noticeTemplate, ok := c.noticeTemplateByExtension(".js")
   410  	its.True(ok)
   411  	its.Equal(jsNoticeTemplate, noticeTemplate)
   412  
   413  	// it handles no dot prefix
   414  	noticeTemplate, ok = c.noticeTemplateByExtension("js")
   415  	its.True(ok)
   416  	its.Equal(jsNoticeTemplate, noticeTemplate)
   417  
   418  	// it handles another file type
   419  	noticeTemplate, ok = c.noticeTemplateByExtension(".go")
   420  	its.True(ok)
   421  	its.Equal(goNoticeTemplate, noticeTemplate)
   422  
   423  	noticeTemplate, ok = c.noticeTemplateByExtension("not-a-real-extension")
   424  	its.False(ok)
   425  	its.Empty(noticeTemplate)
   426  
   427  	withDefault := Copyright{
   428  		Config: Config{
   429  			FallbackNoticeTemplate: "this is just a test",
   430  		},
   431  	}
   432  
   433  	noticeTemplate, ok = withDefault.noticeTemplateByExtension("not-a-real-extension")
   434  	its.True(ok)
   435  	its.Equal("this is just a test", noticeTemplate)
   436  }
   437  
   438  type mockInfoDir string
   439  
   440  func (mid mockInfoDir) Name() string       { return string(mid) }
   441  func (mid mockInfoDir) Size() int64        { return 1 << 8 }
   442  func (mid mockInfoDir) Mode() fs.FileMode  { return fs.FileMode(0755) }
   443  func (mid mockInfoDir) ModTime() time.Time { return time.Now().UTC() }
   444  func (mid mockInfoDir) IsDir() bool        { return true }
   445  func (mid mockInfoDir) Sys() interface{}   { return nil }
   446  
   447  type mockInfoFile string
   448  
   449  func (mif mockInfoFile) Name() string       { return string(mif) }
   450  func (mif mockInfoFile) Size() int64        { return 1 << 8 }
   451  func (mif mockInfoFile) Mode() fs.FileMode  { return fs.FileMode(0755) }
   452  func (mif mockInfoFile) ModTime() time.Time { return time.Now().UTC() }
   453  func (mif mockInfoFile) IsDir() bool        { return false }
   454  func (mif mockInfoFile) Sys() interface{}   { return nil }
   455  
   456  func Test_Copyright_includeOrExclude(t *testing.T) {
   457  	t.Parallel()
   458  	its := assert.New(t)
   459  
   460  	testCases := [...]struct {
   461  		Config   Config
   462  		Path     string
   463  		Info     fs.FileInfo
   464  		Expected error
   465  	}{
   466  		/*0*/ {Config: Config{}, Path: ".", Info: mockInfoDir("."), Expected: ErrWalkSkip},
   467  		/*1*/ {Config: Config{Excludes: []string{"/foo/**"}}, Path: "/foo/bar", Info: mockInfoDir("bar"), Expected: filepath.SkipDir},
   468  		/*2*/ {Config: Config{Excludes: []string{"/foo/**"}}, Path: "/foo/bar/baz.jpg", Info: mockInfoFile("baz.jpg"), Expected: ErrWalkSkip},
   469  		/*3*/ {Config: Config{IncludeFiles: []string{"/foo/bar/*.jpg"}}, Path: "/foo/bar/baz.jpg", Info: mockInfoFile("baz.jpg"), Expected: nil},
   470  		/*4*/ {Config: Config{Excludes: []string{}, IncludeFiles: []string{}}, Path: "/foo/bar/baz.jpg", Info: mockInfoFile("baz.jpg"), Expected: ErrWalkSkip},
   471  		/*5*/ {Config: Config{}, Path: "/foo/bar/baz.jpg", Info: mockInfoFile("baz.jpg"), Expected: nil},
   472  		/*6*/ {Config: Config{}, Path: "/foo/bar/baz.jpg", Info: mockInfoDir("baz"), Expected: ErrWalkSkip},
   473  	}
   474  
   475  	for index, tc := range testCases {
   476  		c := Copyright{Config: tc.Config}
   477  		its.Equal(tc.Expected, c.includeOrExclude(".", tc.Path, tc.Info), fmt.Sprintf("test %d", index))
   478  	}
   479  }
   480  
   481  const (
   482  	tsFile0 = `import * as axios from 'axios';`
   483  	tsFile1 = `/// <reference path="../types/testing.d.ts" />
   484  /// <reference path="../types/something.d.ts" />
   485  /// <reference path="../types/somethingElse.d.ts" />
   486  /// <reference path="../types/somethingMore.d.ts" />
   487  /// <reference path="../types/somethingLess.d.ts" />
   488  
   489  	import * as axios from 'axios';
   490  `
   491  
   492  	pyFile0 = `from __future__ import print_function
   493  	
   494  		import logging
   495  		import os
   496  		import shutil
   497  		import sys
   498  		import requests
   499  		import uuid
   500  		import json`
   501  
   502  	goFile0 = `// +build tools
   503  		package tools
   504  		
   505  		import (
   506  			// goimports organizes imports for us
   507  			_ "golang.org/x/tools/cmd/goimports"
   508  		
   509  			// golint is an opinionated linter
   510  			_ "golang.org/x/lint/golint"
   511  		
   512  			// ineffassign is an opinionated linter
   513  			_ "github.com/gordonklaus/ineffassign"
   514  		
   515  			// staticcheck is ineffassign but better
   516  			_ "honnef.co/go/tools/cmd/staticcheck"
   517  		)
   518  		`
   519  )
   520  
   521  // createTestFS creates a temp dir with files in them, with _no_ copyright headers.
   522  //
   523  // there should be at least (1) failure.
   524  func createTestFS(its *assert.Assertions) (tempDir string, revert func()) {
   525  	// create a temp dir
   526  	var err error
   527  	tempDir, err = os.MkdirTemp("", "copyright_test")
   528  	its.Nil(err)
   529  	revert = func() {
   530  		os.RemoveAll(tempDir)
   531  	}
   532  
   533  	// create some files
   534  	err = os.MkdirAll(filepath.Join(tempDir, "foo", "bar"), 0755)
   535  	its.Nil(err)
   536  	err = os.MkdirAll(filepath.Join(tempDir, "bar", "foo"), 0755)
   537  	its.Nil(err)
   538  
   539  	err = os.MkdirAll(filepath.Join(tempDir, "not-bar", "not-foo"), 0755)
   540  	its.Nil(err)
   541  
   542  	err = os.WriteFile(filepath.Join(tempDir, "file0.py"), []byte(pyFile0), 0644)
   543  	its.Nil(err)
   544  
   545  	err = os.WriteFile(filepath.Join(tempDir, "file0.ts"), []byte(tsFile0), 0644)
   546  	its.Nil(err)
   547  
   548  	err = os.WriteFile(filepath.Join(tempDir, "file1.ts"), []byte(tsFile1), 0644)
   549  	its.Nil(err)
   550  
   551  	err = os.WriteFile(filepath.Join(tempDir, "file0.go"), []byte(goFile0), 0644)
   552  	its.Nil(err)
   553  
   554  	err = os.WriteFile(filepath.Join(tempDir, "foo", "bar", "file0.py"), []byte(pyFile0), 0644)
   555  	its.Nil(err)
   556  
   557  	err = os.WriteFile(filepath.Join(tempDir, "foo", "bar", "file0.ts"), []byte(tsFile0), 0644)
   558  	its.Nil(err)
   559  
   560  	err = os.WriteFile(filepath.Join(tempDir, "foo", "bar", "file1.ts"), []byte(tsFile1), 0644)
   561  	its.Nil(err)
   562  
   563  	err = os.WriteFile(filepath.Join(tempDir, "foo", "bar", "file0.go"), []byte(goFile0), 0644)
   564  	its.Nil(err)
   565  
   566  	err = os.WriteFile(filepath.Join(tempDir, "bar", "foo", "file0.py"), []byte(pyFile0), 0644)
   567  	its.Nil(err)
   568  
   569  	err = os.WriteFile(filepath.Join(tempDir, "bar", "foo", "file0.ts"), []byte(tsFile0), 0644)
   570  	its.Nil(err)
   571  
   572  	err = os.WriteFile(filepath.Join(tempDir, "bar", "foo", "file1.ts"), []byte(tsFile1), 0644)
   573  	its.Nil(err)
   574  
   575  	err = os.WriteFile(filepath.Join(tempDir, "bar", "foo", "file0.go"), []byte(goFile0), 0644)
   576  	its.Nil(err)
   577  
   578  	err = os.WriteFile(filepath.Join(tempDir, "not-bar", "not-foo", "file0.py"), []byte(pyFile0), 0644)
   579  	its.Nil(err)
   580  
   581  	err = os.WriteFile(filepath.Join(tempDir, "not-bar", "not-foo", "file0.ts"), []byte(tsFile0), 0644)
   582  	its.Nil(err)
   583  
   584  	err = os.WriteFile(filepath.Join(tempDir, "not-bar", "not-foo", "file1.ts"), []byte(tsFile1), 0644)
   585  	its.Nil(err)
   586  
   587  	err = os.WriteFile(filepath.Join(tempDir, "not-bar", "not-foo", "file0.go"), []byte(goFile0), 0644)
   588  	its.Nil(err)
   589  	return
   590  }
   591  
   592  func Test_Copyright_Walk(t *testing.T) {
   593  	its := assert.New(t)
   594  
   595  	tempDir, revert := createTestFS(its)
   596  	defer revert()
   597  
   598  	c := New(
   599  		OptIncludeFiles("*.py", "*.ts"),
   600  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   601  	)
   602  
   603  	var err error
   604  	var seen []string
   605  	err = c.Walk(context.TODO(), func(path string, info os.FileInfo, file, notice []byte) error {
   606  		seen = append(seen, path)
   607  		return nil
   608  	}, tempDir)
   609  	its.Nil(err)
   610  	expected := []string{
   611  		filepath.Join(tempDir, "bar", "foo", "file0.py"),
   612  		filepath.Join(tempDir, "bar", "foo", "file0.ts"),
   613  		filepath.Join(tempDir, "bar", "foo", "file1.ts"),
   614  		filepath.Join(tempDir, "file0.py"),
   615  		filepath.Join(tempDir, "file0.ts"),
   616  		filepath.Join(tempDir, "file1.ts"),
   617  		filepath.Join(tempDir, "foo", "bar", "file0.py"),
   618  		filepath.Join(tempDir, "foo", "bar", "file0.ts"),
   619  		filepath.Join(tempDir, "foo", "bar", "file1.ts"),
   620  	}
   621  	its.Equal(expected, seen)
   622  }
   623  
   624  func Test_Copyright_Walk_noExitFirst(t *testing.T) {
   625  	its := assert.New(t)
   626  
   627  	tempDir, revert := createTestFS(its)
   628  	defer revert()
   629  
   630  	c := New(
   631  		OptIncludeFiles("*.py", "*.ts"),
   632  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   633  		OptExitFirst(false),
   634  	)
   635  
   636  	var err error
   637  	var seen []string
   638  	err = c.Walk(context.TODO(), func(path string, info os.FileInfo, file, notice []byte) error {
   639  		seen = append(seen, path)
   640  		if len(seen) > 0 {
   641  			return ErrFailure
   642  		}
   643  		return nil
   644  	}, tempDir)
   645  	its.NotNil(err)
   646  	expected := []string{
   647  		filepath.Join(tempDir, "bar", "foo", "file0.py"),
   648  		filepath.Join(tempDir, "bar", "foo", "file0.ts"),
   649  		filepath.Join(tempDir, "bar", "foo", "file1.ts"),
   650  		filepath.Join(tempDir, "file0.py"),
   651  		filepath.Join(tempDir, "file0.ts"),
   652  		filepath.Join(tempDir, "file1.ts"),
   653  		filepath.Join(tempDir, "foo", "bar", "file0.py"),
   654  		filepath.Join(tempDir, "foo", "bar", "file0.ts"),
   655  		filepath.Join(tempDir, "foo", "bar", "file1.ts"),
   656  	}
   657  	its.Equal(expected, seen)
   658  }
   659  
   660  func Test_Copyright_Walk_exitFirst(t *testing.T) {
   661  	its := assert.New(t)
   662  
   663  	tempDir, revert := createTestFS(its)
   664  	defer revert()
   665  
   666  	c := New(
   667  		OptIncludeFiles("*.py", "*.ts"),
   668  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   669  		OptExitFirst(true),
   670  	)
   671  
   672  	var err error
   673  	var seen []string
   674  	err = c.Walk(context.TODO(), func(path string, info os.FileInfo, file, notice []byte) error {
   675  		seen = append(seen, path)
   676  		if len(seen) > 0 {
   677  			return ErrFailure
   678  		}
   679  		return nil
   680  	}, tempDir)
   681  	its.NotNil(err)
   682  	expected := []string{
   683  		filepath.Join(tempDir, "bar", "foo", "file0.py"),
   684  	}
   685  	its.Equal(expected, seen)
   686  }
   687  
   688  func Test_Copyright_Inject(t *testing.T) {
   689  	its := assert.New(t)
   690  
   691  	tempDir, revert := createTestFS(its)
   692  	defer revert()
   693  
   694  	c := New(
   695  		OptIncludeFiles("*.py", "*.ts", "*.go"),
   696  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   697  	)
   698  
   699  	err := c.Inject(context.TODO(), tempDir)
   700  	its.Nil(err)
   701  
   702  	contents, err := os.ReadFile(filepath.Join(tempDir, "bar", "foo", "file0.py"))
   703  	its.Nil(err)
   704  	its.HasPrefix(string(contents), "#\n# Copyright")
   705  
   706  	contents, err = os.ReadFile(filepath.Join(tempDir, "bar", "foo", "file0.ts"))
   707  	its.Nil(err)
   708  	its.HasPrefix(string(contents), "/**\n * Copyright")
   709  }
   710  
   711  func Test_Copyright_Inject_Shebang(t *testing.T) {
   712  	t.Parallel()
   713  	its := assert.New(t)
   714  
   715  	tempDir, err := os.MkdirTemp("", "copyright_test")
   716  	its.Nil(err)
   717  	t.Cleanup(func() { os.RemoveAll(tempDir) })
   718  
   719  	// Write `shift.py` without
   720  	contents := strings.Join([]string{
   721  		"\r\t",
   722  		"  #!/usr/bin/env python",
   723  		"",
   724  		"def main():",
   725  		`    print("Hello world")`,
   726  		"",
   727  		"",
   728  		`if __name__ == "__main__":`,
   729  		"    main()",
   730  		"",
   731  	}, "\n")
   732  	filename := filepath.Join(tempDir, "shift.py")
   733  	err = os.WriteFile(filename, []byte(contents), 0755)
   734  	its.Nil(err)
   735  
   736  	// Actually inject
   737  	c := New(OptIncludeFiles("*shift.py"))
   738  	err = c.Inject(context.TODO(), tempDir)
   739  	its.Nil(err)
   740  
   741  	// Verify injected contents are as expected
   742  	contentInjected, err := os.ReadFile(filename)
   743  	its.Nil(err)
   744  	expected := strings.Join([]string{
   745  		"\r\t",
   746  		"  #!/usr/bin/env python",
   747  		"#",
   748  		"# " + expectedNoticePrefix(its),
   749  		"# " + DefaultRestrictionsInternal,
   750  		"#",
   751  		"",
   752  		"",
   753  		"def main():",
   754  		`    print("Hello world")`,
   755  		"",
   756  		"",
   757  		`if __name__ == "__main__":`,
   758  		"    main()",
   759  		"",
   760  	}, "\n")
   761  	its.Equal(expected, string(contentInjected))
   762  
   763  	// Verify no-op if notice header is already present
   764  	err = c.Inject(context.TODO(), tempDir)
   765  	its.Nil(err)
   766  	contentInjected, err = os.ReadFile(filename)
   767  	its.Nil(err)
   768  	its.Equal(expected, string(contentInjected))
   769  }
   770  
   771  func Test_Copyright_Verify(t *testing.T) {
   772  	its := assert.New(t)
   773  
   774  	tempDir, revert := createTestFS(its)
   775  	defer revert()
   776  
   777  	c := New(
   778  		OptIncludeFiles("*.py", "*.ts", "*.go"),
   779  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   780  		OptExitFirst(false),
   781  	)
   782  	c.Stdout = new(bytes.Buffer)
   783  	c.Stderr = new(bytes.Buffer)
   784  
   785  	err := c.Verify(context.TODO(), tempDir)
   786  	its.NotNil(err, "we must record a failure from walking the test fs")
   787  
   788  	err = c.Inject(context.TODO(), tempDir)
   789  	its.Nil(err)
   790  
   791  	err = c.Verify(context.TODO(), tempDir)
   792  	its.Nil(err)
   793  }
   794  
   795  func Test_Copyright_Verify_Shebang(t *testing.T) {
   796  	t.Parallel()
   797  	its := assert.New(t)
   798  
   799  	tempDir, err := os.MkdirTemp("", "copyright_test")
   800  	its.Nil(err)
   801  	t.Cleanup(func() { os.RemoveAll(tempDir) })
   802  
   803  	// Write `shift.py` already injected
   804  	contents := strings.Join([]string{
   805  		"#!/usr/bin/env python",
   806  		"#",
   807  		"# " + expectedNoticePrefix(its),
   808  		"# " + DefaultRestrictionsInternal,
   809  		"#",
   810  		"",
   811  		"",
   812  		"def main():",
   813  		`    print("Hello world")`,
   814  		"",
   815  		"",
   816  		`if __name__ == "__main__":`,
   817  		"    main()",
   818  		"",
   819  	}, "\n")
   820  	filename := filepath.Join(tempDir, "shift.py")
   821  	err = os.WriteFile(filename, []byte(contents), 0755)
   822  	its.Nil(err)
   823  
   824  	// Verify present
   825  	cfg := Config{
   826  		ShowDiff:     ref.Bool(false),
   827  		Quiet:        ref.Bool(true),
   828  		IncludeFiles: []string{"*shift.py"},
   829  	}
   830  	c := New(OptConfig(cfg))
   831  	err = c.Verify(context.TODO(), tempDir)
   832  	its.Nil(err)
   833  
   834  	// Write without and fail
   835  	contents = strings.Join([]string{
   836  		"#!/usr/bin/env python",
   837  		"def main():",
   838  		`    print("Hello world")`,
   839  		"",
   840  		"",
   841  		`if __name__ == "__main__":`,
   842  		"    main()",
   843  		"",
   844  	}, "\n")
   845  	err = os.WriteFile(filename, []byte(contents), 0755)
   846  	its.Nil(err)
   847  	err = c.Verify(context.TODO(), tempDir)
   848  	its.Equal("failure; one or more steps failed", fmt.Sprintf("%v", err))
   849  }
   850  
   851  func Test_Copyright_Remove(t *testing.T) {
   852  	its := assert.New(t)
   853  
   854  	tempDir, revert := createTestFS(its)
   855  	defer revert()
   856  
   857  	c := New(
   858  		OptIncludeFiles("*.py", "*.ts", "*.go"),
   859  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   860  	)
   861  	c.Stdout = new(bytes.Buffer)
   862  	c.Stderr = new(bytes.Buffer)
   863  
   864  	err := c.Inject(context.TODO(), tempDir)
   865  	its.Nil(err)
   866  
   867  	err = c.Verify(context.TODO(), tempDir)
   868  	its.Nil(err)
   869  
   870  	err = c.Remove(context.TODO(), tempDir)
   871  	its.Nil(err)
   872  
   873  	err = c.Verify(context.TODO(), tempDir)
   874  	its.NotNil(err)
   875  }
   876  
   877  func Test_Copyright_Remove_Shebang(t *testing.T) {
   878  	t.Parallel()
   879  	its := assert.New(t)
   880  
   881  	tempDir, err := os.MkdirTemp("", "copyright_test")
   882  	its.Nil(err)
   883  	t.Cleanup(func() { os.RemoveAll(tempDir) })
   884  
   885  	// Write `shift.py` already injected
   886  	contents := strings.Join([]string{
   887  		"#!/usr/bin/env python",
   888  		"#",
   889  		"# " + expectedNoticePrefix(its),
   890  		"# " + DefaultRestrictionsInternal,
   891  		"#",
   892  		"",
   893  		"",
   894  		"def main():",
   895  		`    print("Hello world")`,
   896  		"",
   897  		"",
   898  		`if __name__ == "__main__":`,
   899  		"    main()",
   900  		"",
   901  	}, "\n")
   902  	filename := filepath.Join(tempDir, "shift.py")
   903  	err = os.WriteFile(filename, []byte(contents), 0755)
   904  	its.Nil(err)
   905  
   906  	// Actually remove
   907  	c := New(OptIncludeFiles("*shift.py"))
   908  	err = c.Remove(context.TODO(), tempDir)
   909  	its.Nil(err)
   910  
   911  	// Verify removed contents are as expected
   912  	contentRemoved, err := os.ReadFile(filename)
   913  	its.Nil(err)
   914  	expected := strings.Join([]string{
   915  		"#!/usr/bin/env python",
   916  		"def main():",
   917  		`    print("Hello world")`,
   918  		"",
   919  		"",
   920  		`if __name__ == "__main__":`,
   921  		"    main()",
   922  		"",
   923  	}, "\n")
   924  	its.Equal(expected, string(contentRemoved))
   925  
   926  	// Verify no-op if notice header is already removed
   927  	err = c.Remove(context.TODO(), tempDir)
   928  	its.Nil(err)
   929  	contentRemoved, err = os.ReadFile(filename)
   930  	its.Nil(err)
   931  	its.Equal(expected, string(contentRemoved))
   932  }
   933  
   934  func Test_Copyright_Walk_singleFileRoot(t *testing.T) {
   935  	its := assert.New(t)
   936  
   937  	tempDir, revert := createTestFS(its)
   938  	defer revert()
   939  
   940  	c := New(
   941  		OptIncludeFiles("*.py", "*.ts"),
   942  		OptExcludes("*/not-bar/*", "*/not-foo/*"),
   943  	)
   944  
   945  	var err error
   946  	var seen []string
   947  	err = c.Walk(context.TODO(), func(path string, info os.FileInfo, file, notice []byte) error {
   948  		seen = append(seen, path)
   949  		return nil
   950  	}, filepath.Join(tempDir, "file0.py"))
   951  	its.Nil(err)
   952  	expected := []string{
   953  		filepath.Join(tempDir, "file0.py"),
   954  	}
   955  	its.Equal(expected, seen)
   956  }
   957  
   958  func expectedNoticePrefix(its *assert.Assertions) string {
   959  	vars := map[string]string{
   960  		"Year":         fmt.Sprintf("%d", time.Now().UTC().Year()),
   961  		"Company":      DefaultCompany,
   962  		"Restrictions": "",
   963  	}
   964  	tmpl := template.New("output")
   965  	_, err := tmpl.Parse(DefaultNoticeBodyTemplate)
   966  	its.Nil(err)
   967  	prefixBuffer := new(bytes.Buffer)
   968  	err = tmpl.Execute(prefixBuffer, vars)
   969  	its.Nil(err)
   970  	return strings.TrimRight(prefixBuffer.String(), "\n")
   971  }