2018-05-31 23:53:01 +03:00
package test
import (
2018-11-07 21:55:26 +03:00
"bufio"
2018-06-10 17:10:56 +03:00
"io/ioutil"
2018-11-07 21:55:26 +03:00
"os"
2018-05-31 23:53:01 +03:00
"os/exec"
"path/filepath"
2018-06-10 17:10:56 +03:00
"strings"
2018-05-31 23:53:01 +03:00
"testing"
2018-10-21 17:06:51 +03:00
assert "github.com/stretchr/testify/require"
2018-12-22 15:41:10 +03:00
yaml "gopkg.in/yaml.v2"
2019-09-27 20:58:49 -04:00
"github.com/golangci/golangci-lint/test/testshared"
2018-05-31 23:53:01 +03:00
)
2021-01-15 12:37:56 -08:00
func runGoErrchk ( c * exec . Cmd , defaultExpectedLinter string , files [ ] string , t * testing . T ) {
2018-05-31 23:53:01 +03:00
output , err := c . CombinedOutput ( )
2020-05-29 19:01:46 +05:30
// The returned error will be nil if the test file does not have any issues
// and thus the linter exits with exit code 0. So perform the additional
// assertions only if the error is non-nil.
if err != nil {
_ , ok := err . ( * exec . ExitError )
assert . True ( t , ok , err )
}
2019-03-05 21:44:52 +03:00
// TODO: uncomment after deprecating go1.11
// assert.Equal(t, exitcodes.IssuesFound, exitErr.ExitCode())
2019-03-05 21:20:43 +03:00
fullshort := make ( [ ] string , 0 , len ( files ) * 2 )
for _ , f := range files {
fullshort = append ( fullshort , f , filepath . Base ( f ) )
}
2018-05-31 23:53:01 +03:00
2021-01-15 12:37:56 -08:00
err = errorCheck ( string ( output ) , false , defaultExpectedLinter , fullshort ... )
2019-03-05 21:20:43 +03:00
assert . NoError ( t , err )
2018-05-31 23:53:01 +03:00
}
2018-06-10 17:10:56 +03:00
func testSourcesFromDir ( t * testing . T , dir string ) {
t . Log ( filepath . Join ( dir , "*.go" ) )
2018-05-31 23:53:01 +03:00
2018-11-07 21:55:26 +03:00
findSources := func ( pathPatterns ... string ) [ ] string {
sources , err := filepath . Glob ( filepath . Join ( pathPatterns ... ) )
assert . NoError ( t , err )
assert . NotEmpty ( t , sources )
return sources
}
sources := findSources ( dir , "*.go" )
testshared . NewLintRunner ( t ) . Install ( )
2018-05-31 23:53:01 +03:00
for _ , s := range sources {
s := s
2020-05-09 15:15:34 +03:00
t . Run ( filepath . Base ( s ) , func ( subTest * testing . T ) {
subTest . Parallel ( )
testOneSource ( subTest , s )
2018-05-31 23:53:01 +03:00
} )
}
}
2018-06-10 17:10:56 +03:00
func TestSourcesFromTestdataWithIssuesDir ( t * testing . T ) {
testSourcesFromDir ( t , testdataDir )
}
func TestTypecheck ( t * testing . T ) {
testSourcesFromDir ( t , filepath . Join ( testdataDir , "notcompiles" ) )
}
2018-11-07 21:55:26 +03:00
func TestGoimportsLocal ( t * testing . T ) {
sourcePath := filepath . Join ( testdataDir , "goimports" , "goimports.go" )
args := [ ] string {
"--disable-all" , "--print-issued-lines=false" , "--print-linter-name=false" , "--out-format=line-number" ,
sourcePath ,
}
rc := extractRunContextFromComments ( t , sourcePath )
args = append ( args , rc . args ... )
cfg , err := yaml . Marshal ( rc . config )
assert . NoError ( t , err )
testshared . NewLintRunner ( t ) . RunWithYamlConfig ( string ( cfg ) , args ... ) .
ExpectHasIssue ( "testdata/goimports/goimports.go:8: File is not `goimports`-ed" )
}
2020-07-28 18:55:02 +08:00
func TestGciLocal ( t * testing . T ) {
sourcePath := filepath . Join ( testdataDir , "gci" , "gci.go" )
args := [ ] string {
"--disable-all" , "--print-issued-lines=false" , "--print-linter-name=false" , "--out-format=line-number" ,
sourcePath ,
}
rc := extractRunContextFromComments ( t , sourcePath )
args = append ( args , rc . args ... )
cfg , err := yaml . Marshal ( rc . config )
assert . NoError ( t , err )
testshared . NewLintRunner ( t ) . RunWithYamlConfig ( string ( cfg ) , args ... ) .
2020-08-31 08:38:05 +08:00
ExpectHasIssue ( "testdata/gci/gci.go:7: File is not `gci`-ed" )
2020-07-28 18:55:02 +08:00
}
2018-11-07 21:55:26 +03:00
func saveConfig ( t * testing . T , cfg map [ string ] interface { } ) ( cfgPath string , finishFunc func ( ) ) {
f , err := ioutil . TempFile ( "" , "golangci_lint_test" )
assert . NoError ( t , err )
cfgPath = f . Name ( ) + ".yml"
err = os . Rename ( f . Name ( ) , cfgPath )
assert . NoError ( t , err )
err = yaml . NewEncoder ( f ) . Encode ( cfg )
assert . NoError ( t , err )
return cfgPath , func ( ) {
assert . NoError ( t , f . Close ( ) )
2018-12-23 11:36:21 +03:00
if os . Getenv ( "GL_KEEP_TEMP_FILES" ) != "1" {
assert . NoError ( t , os . Remove ( cfgPath ) )
}
2018-11-07 21:55:26 +03:00
}
}
2018-05-31 23:53:01 +03:00
func testOneSource ( t * testing . T , sourcePath string ) {
2018-06-10 17:10:56 +03:00
args := [ ] string {
2019-03-05 21:20:43 +03:00
"run" ,
2020-05-09 15:15:34 +03:00
"--allow-parallel-runners" ,
2018-06-10 17:10:56 +03:00
"--disable-all" ,
2018-05-31 23:53:01 +03:00
"--print-issued-lines=false" ,
"--out-format=line-number" ,
2020-06-09 05:21:58 +09:00
"--max-same-issues=100" ,
2018-06-10 17:10:56 +03:00
}
2018-11-07 21:55:26 +03:00
rc := extractRunContextFromComments ( t , sourcePath )
var cfgPath string
if rc . config != nil {
p , finish := saveConfig ( t , rc . config )
defer finish ( )
cfgPath = p
2018-12-23 11:36:21 +03:00
} else if rc . configPath != "" {
cfgPath = rc . configPath
2018-11-07 21:55:26 +03:00
}
2018-06-10 17:10:56 +03:00
for _ , addArg := range [ ] string { "" , "-Etypecheck" } {
caseArgs := append ( [ ] string { } , args ... )
2018-11-07 21:55:26 +03:00
caseArgs = append ( caseArgs , rc . args ... )
2018-06-10 17:10:56 +03:00
if addArg != "" {
caseArgs = append ( caseArgs , addArg )
}
2018-11-07 21:55:26 +03:00
if cfgPath == "" {
caseArgs = append ( caseArgs , "--no-config" )
} else {
caseArgs = append ( caseArgs , "-c" , cfgPath )
}
2018-06-10 17:10:56 +03:00
caseArgs = append ( caseArgs , sourcePath )
2019-03-05 21:20:43 +03:00
cmd := exec . Command ( binName , caseArgs ... )
2018-06-10 17:10:56 +03:00
t . Log ( caseArgs )
2021-01-15 12:37:56 -08:00
runGoErrchk ( cmd , rc . expectedLinter , [ ] string { sourcePath } , t )
2018-06-10 17:10:56 +03:00
}
}
2018-11-07 21:55:26 +03:00
type runContext struct {
2021-01-15 12:37:56 -08:00
args [ ] string
config map [ string ] interface { }
configPath string
expectedLinter string
2018-11-07 21:55:26 +03:00
}
func buildConfigFromShortRepr ( t * testing . T , repr string , config map [ string ] interface { } ) {
kv := strings . Split ( repr , "=" )
assert . Len ( t , kv , 2 )
keyParts := strings . Split ( kv [ 0 ] , "." )
assert . True ( t , len ( keyParts ) >= 2 , len ( keyParts ) )
lastObj := config
for _ , k := range keyParts [ : len ( keyParts ) - 1 ] {
var v map [ string ] interface { }
if lastObj [ k ] == nil {
v = map [ string ] interface { } { }
} else {
v = lastObj [ k ] . ( map [ string ] interface { } )
}
lastObj [ k ] = v
lastObj = v
}
lastObj [ keyParts [ len ( keyParts ) - 1 ] ] = kv [ 1 ]
}
2020-07-05 02:03:37 +07:00
func skipMultilineComment ( scanner * bufio . Scanner ) {
for line := scanner . Text ( ) ; ! strings . Contains ( line , "*/" ) && scanner . Scan ( ) ; {
line = scanner . Text ( )
}
}
2018-11-07 21:55:26 +03:00
func extractRunContextFromComments ( t * testing . T , sourcePath string ) * runContext {
f , err := os . Open ( sourcePath )
2018-06-10 17:10:56 +03:00
assert . NoError ( t , err )
2018-11-07 21:55:26 +03:00
defer f . Close ( )
2018-06-10 17:10:56 +03:00
2018-11-07 21:55:26 +03:00
rc := & runContext { }
2018-06-10 17:10:56 +03:00
2018-11-07 21:55:26 +03:00
scanner := bufio . NewScanner ( f )
for scanner . Scan ( ) {
line := scanner . Text ( )
2020-07-05 02:03:37 +07:00
if strings . HasPrefix ( line , "/*" ) {
skipMultilineComment ( scanner )
continue
}
2020-07-05 15:32:00 +07:00
if strings . TrimSpace ( line ) == "" {
continue
}
2018-11-07 21:55:26 +03:00
if ! strings . HasPrefix ( line , "//" ) {
2021-01-15 12:37:56 -08:00
break
2018-11-07 21:55:26 +03:00
}
2021-01-08 16:48:56 +01:00
line = strings . TrimLeft ( strings . TrimPrefix ( line , "//" ) , " " )
2018-11-07 21:55:26 +03:00
if strings . HasPrefix ( line , "args: " ) {
assert . Nil ( t , rc . args )
args := strings . TrimPrefix ( line , "args: " )
assert . NotEmpty ( t , args )
rc . args = strings . Split ( args , " " )
continue
}
if strings . HasPrefix ( line , "config: " ) {
repr := strings . TrimPrefix ( line , "config: " )
assert . NotEmpty ( t , repr )
if rc . config == nil {
rc . config = map [ string ] interface { } { }
}
buildConfigFromShortRepr ( t , repr , rc . config )
continue
}
2018-12-23 11:36:21 +03:00
if strings . HasPrefix ( line , "config_path: " ) {
configPath := strings . TrimPrefix ( line , "config_path: " )
assert . NotEmpty ( t , configPath )
rc . configPath = configPath
continue
}
2021-01-15 12:37:56 -08:00
if strings . HasPrefix ( line , "expected_linter: " ) {
expectedLinter := strings . TrimPrefix ( line , "expected_linter: " )
assert . NotEmpty ( t , expectedLinter )
rc . expectedLinter = expectedLinter
continue
}
2018-12-23 11:36:21 +03:00
assert . Fail ( t , "invalid prefix of comment line %s" , line )
2018-06-10 17:10:56 +03:00
}
2021-01-15 12:37:56 -08:00
// guess the expected linter if none is specified
if rc . expectedLinter == "" {
for _ , arg := range rc . args {
if strings . HasPrefix ( arg , "-E" ) && ! strings . Contains ( arg , "," ) {
if rc . expectedLinter != "" {
assert . Fail ( t , "could not infer expected linter for errors because multiple linters are enabled. Please use the `expected_linter: ` directive in your test to indicate the linter-under-test." ) //nolint:lll
break
}
rc . expectedLinter = arg [ 2 : ]
}
}
}
2018-11-07 21:55:26 +03:00
return rc
}
func TestExtractRunContextFromComments ( t * testing . T ) {
rc := extractRunContextFromComments ( t , filepath . Join ( testdataDir , "goimports" , "goimports.go" ) )
assert . Equal ( t , [ ] string { "-Egoimports" } , rc . args )
2018-06-10 17:10:56 +03:00
}
2020-10-02 13:00:46 -07:00
func TestTparallel ( t * testing . T ) {
t . Run ( "should fail on missing top-level Parallel()" , func ( t * testing . T ) {
sourcePath := filepath . Join ( testdataDir , "tparallel" , "missing_toplevel_test.go" )
args := [ ] string {
"--disable-all" , "--print-issued-lines=false" , "--print-linter-name=false" , "--out-format=line-number" , "--enable" , "tparallel" ,
sourcePath ,
}
rc := extractRunContextFromComments ( t , sourcePath )
args = append ( args , rc . args ... )
cfg , err := yaml . Marshal ( rc . config )
assert . NoError ( t , err )
testshared . NewLintRunner ( t ) . RunWithYamlConfig ( string ( cfg ) , args ... ) .
ExpectHasIssue (
"testdata/tparallel/missing_toplevel_test.go:7:6: TestTopLevel should call t.Parallel on the top level as well as its subtests\n" ,
)
} )
t . Run ( "should fail on missing subtest Parallel()" , func ( t * testing . T ) {
sourcePath := filepath . Join ( testdataDir , "tparallel" , "missing_subtest_test.go" )
args := [ ] string {
"--disable-all" , "--print-issued-lines=false" , "--print-linter-name=false" , "--out-format=line-number" , "--enable" , "tparallel" ,
sourcePath ,
}
rc := extractRunContextFromComments ( t , sourcePath )
args = append ( args , rc . args ... )
cfg , err := yaml . Marshal ( rc . config )
assert . NoError ( t , err )
testshared . NewLintRunner ( t ) . RunWithYamlConfig ( string ( cfg ) , args ... ) .
ExpectHasIssue (
"testdata/tparallel/missing_subtest_test.go:7:6: TestSubtests's subtests should call t.Parallel\n" ,
)
} )
t . Run ( "should pass on parallel test with no subtests" , func ( t * testing . T ) {
sourcePath := filepath . Join ( testdataDir , "tparallel" , "happy_path_test.go" )
args := [ ] string {
"--disable-all" , "--print-issued-lines=false" , "--print-linter-name=false" , "--out-format=line-number" , "--enable" , "tparallel" ,
sourcePath ,
}
rc := extractRunContextFromComments ( t , sourcePath )
args = append ( args , rc . args ... )
cfg , err := yaml . Marshal ( rc . config )
assert . NoError ( t , err )
2018-06-10 17:10:56 +03:00
2020-10-02 13:00:46 -07:00
testshared . NewLintRunner ( t ) . RunWithYamlConfig ( string ( cfg ) , args ... ) . ExpectNoIssues ( )
} )
2018-05-31 23:53:01 +03:00
}