2022-08-15 21:56:32 +02:00
package testshared
import (
"bufio"
"go/build/constraint"
"os"
"runtime"
2022-08-20 18:53:45 +02:00
"strconv"
2022-08-15 21:56:32 +02:00
"strings"
"testing"
hcversion "github.com/hashicorp/go-version"
"github.com/stretchr/testify/require"
2022-08-20 18:53:45 +02:00
"github.com/golangci/golangci-lint/pkg/exitcodes"
2022-08-15 21:56:32 +02:00
)
2022-08-24 22:10:51 +02:00
// RunContext the information extracted from directives.
2022-08-15 21:56:32 +02:00
type RunContext struct {
Args [ ] string
ConfigPath string
ExpectedLinter string
2022-08-20 18:53:45 +02:00
ExitCode int
2022-08-15 21:56:32 +02:00
}
// ParseTestDirectives parses test directives from sources files.
//
2022-08-20 18:53:45 +02:00
//nolint:gocyclo,funlen
2022-08-15 21:56:32 +02:00
func ParseTestDirectives ( tb testing . TB , sourcePath string ) * RunContext {
tb . Helper ( )
f , err := os . Open ( sourcePath )
require . NoError ( tb , err )
tb . Cleanup ( func ( ) { _ = f . Close ( ) } )
2022-08-20 18:53:45 +02:00
rc := & RunContext {
ExitCode : exitcodes . IssuesFound ,
}
2022-08-15 21:56:32 +02:00
scanner := bufio . NewScanner ( f )
for scanner . Scan ( ) {
line := scanner . Text ( )
if strings . HasPrefix ( line , "/*" ) {
skipMultilineComment ( scanner )
continue
}
if strings . TrimSpace ( line ) == "" {
continue
}
if ! strings . HasPrefix ( line , "//" ) {
break
}
2022-08-24 22:10:51 +02:00
if constraint . IsGoBuild ( line ) {
if ! evaluateBuildTags ( tb , line ) {
2022-08-15 21:56:32 +02:00
return nil
}
continue
}
if ! strings . HasPrefix ( line , "//golangcitest:" ) {
require . Failf ( tb , "invalid prefix of comment line %s" , line )
}
before , after , found := strings . Cut ( line , " " )
require . Truef ( tb , found , "invalid prefix of comment line %s" , line )
after = strings . TrimSpace ( after )
switch before {
case "//golangcitest:args" :
require . Nil ( tb , rc . Args )
require . NotEmpty ( tb , after )
rc . Args = strings . Split ( after , " " )
continue
case "//golangcitest:config_path" :
require . NotEmpty ( tb , after )
rc . ConfigPath = after
continue
case "//golangcitest:expected_linter" :
require . NotEmpty ( tb , after )
rc . ExpectedLinter = after
continue
2022-08-20 18:53:45 +02:00
case "//golangcitest:expected_exitcode" :
require . NotEmpty ( tb , after )
val , err := strconv . Atoi ( after )
require . NoError ( tb , err )
rc . ExitCode = val
continue
2022-08-15 21:56:32 +02:00
default :
require . Failf ( tb , "invalid prefix of comment line %s" , line )
}
}
// guess the expected linter if none is specified
if rc . ExpectedLinter == "" {
for _ , arg := range rc . Args {
if strings . HasPrefix ( arg , "-E" ) && ! strings . Contains ( arg , "," ) {
require . Empty ( tb , rc . ExpectedLinter , "could not infer expected linter for errors because multiple linters are enabled. Please use the `//golangcitest:expected_linter ` directive in your test to indicate the linter-under-test." ) //nolint:lll
rc . ExpectedLinter = arg [ 2 : ]
}
}
}
return rc
}
func skipMultilineComment ( scanner * bufio . Scanner ) {
for line := scanner . Text ( ) ; ! strings . Contains ( line , "*/" ) && scanner . Scan ( ) ; {
line = scanner . Text ( )
}
}
2022-08-24 22:10:51 +02:00
// evaluateBuildTags Naive implementation of the evaluation of the build tags.
// Inspired by https://github.com/golang/go/blob/1dcef7b3bdcea4a829ea22c821e6a9484c325d61/src/cmd/go/internal/modindex/build.go#L914-L972
func evaluateBuildTags ( tb testing . TB , line string ) bool {
parse , err := constraint . Parse ( line )
require . NoError ( tb , err )
return parse . Eval ( func ( tag string ) bool {
if tag == runtime . GOOS {
return true
}
if buildTagGoVersion ( tag ) {
return true
}
return false
} )
}
2022-08-15 21:56:32 +02:00
func buildTagGoVersion ( tag string ) bool {
vRuntime , err := hcversion . NewVersion ( strings . TrimPrefix ( runtime . Version ( ) , "go" ) )
if err != nil {
return false
}
vTag , err := hcversion . NewVersion ( strings . TrimPrefix ( tag , "go" ) )
if err != nil {
return false
}
return vRuntime . GreaterThanOrEqual ( vTag )
}