language.go
1 package language 2 3 import ( 4 "errors" 5 "os" 6 "path/filepath" 7 "sort" 8 "strings" 9 "time" 10 11 pkgerrors "github.com/pkg/errors" 12 "github.com/zimmski/osutil" 13 14 "github.com/symflower/eval-dev-quality/log" 15 ) 16 17 var ( 18 // ErrCannotParseTestSummary indicates that the test summary cannot be parsed. 19 ErrCannotParseTestSummary = errors.New("cannot parse test summary") 20 ) 21 22 // DefaultExecutionTimeout defines the timeout for an execution. 23 // WORKAROUND For now we define the timeout as a global variable but it should eventually be moved to the "symflower test" command. 24 var DefaultExecutionTimeout = 5 * time.Minute 25 26 // Language defines a language to evaluate a repository. 27 type Language interface { 28 // ID returns the unique ID of this language. 29 ID() (id string) 30 // Name is the prose name of this language. 31 Name() (id string) 32 33 // Files returns a list of relative file paths of the repository that should be evaluated. 34 Files(logger *log.Logger, repositoryPath string) (filePaths []string, err error) 35 // ImportPath returns the import path of the given source file. 36 ImportPath(projectRootPath string, filePath string) (importPath string) 37 // TestFilePath returns the file path of a test file given the corresponding file path of the test's source file. 38 TestFilePath(projectRootPath string, filePath string) (testFilePath string) 39 // TestFramework returns the human-readable name of the test framework that should be used. 40 TestFramework() (testFramework string) 41 42 // DefaultFileExtension returns the default file extension of the implemented language. 43 DefaultFileExtension() string 44 // DefaultTestFileSuffix returns the default test file suffix of the implemented language. 45 DefaultTestFileSuffix() string 46 47 // ExecuteTests invokes the language specific testing on the given repository. 48 ExecuteTests(logger *log.Logger, repositoryPath string) (testResult *TestResult, problems []error, err error) 49 // Mistakes builds a repository and returns the list of mistakes found. 50 Mistakes(logger *log.Logger, repositoryPath string) (mistakes []string, err error) 51 } 52 53 // Languages holds a register of all languages. 54 var Languages = map[string]Language{} 55 56 // LanguageByFileExtension holds the language for a default file extension. 57 var LanguageByFileExtension = map[string]Language{} 58 59 // Register adds a language to the common language list. 60 func Register(language Language) { 61 id := language.ID() 62 if _, ok := Languages[id]; ok { 63 panic(pkgerrors.WithMessage(pkgerrors.New("language was already registered"), id)) 64 } 65 if _, ok := LanguageByFileExtension[language.DefaultFileExtension()]; ok { 66 panic(pkgerrors.WithMessage(pkgerrors.New("language file extension was already registered"), id)) 67 } 68 69 Languages[id] = language 70 LanguageByFileExtension[language.DefaultFileExtension()] = language 71 } 72 73 // RepositoriesForLanguage returns the relative repository paths for a language. 74 func RepositoriesForLanguage(language Language, testdataPath string) (relativeRepositoryPaths []string, err error) { 75 languagePath := filepath.Join(testdataPath, language.ID()) 76 languageRepositories, err := os.ReadDir(languagePath) 77 if err != nil { 78 pkgerrors.WithMessagef(err, "language path %q cannot be accessed", languagePath) 79 } 80 81 for _, repository := range languageRepositories { 82 if !repository.IsDir() { 83 continue 84 } 85 relativeRepositoryPaths = append(relativeRepositoryPaths, filepath.Join(language.ID(), repository.Name())) 86 } 87 88 sort.Strings(relativeRepositoryPaths) 89 90 return relativeRepositoryPaths, nil 91 } 92 93 // Files returns a list of relative file paths of the repository that should be evaluated. 94 func Files(logger *log.Logger, language Language, repositoryPath string) (filePaths []string, err error) { 95 repositoryPath, err = filepath.Abs(repositoryPath) 96 if err != nil { 97 return nil, pkgerrors.WithStack(err) 98 } 99 100 fs, err := osutil.FilesRecursive(repositoryPath) 101 if err != nil { 102 return nil, pkgerrors.WithStack(err) 103 } 104 105 repositoryPath = repositoryPath + string(os.PathSeparator) 106 for _, f := range fs { 107 if !strings.HasSuffix(f, language.DefaultFileExtension()) { 108 continue 109 } 110 111 filePaths = append(filePaths, strings.TrimPrefix(f, repositoryPath)) 112 } 113 114 return filePaths, nil 115 } 116 117 // TestResult holds the result of running tests. 118 type TestResult struct { 119 TestsTotal uint 120 TestsPass uint 121 122 Coverage uint64 123 } 124 125 // PassingTestsPercentage returns the percentage of passing tests. 126 func (tr *TestResult) PassingTestsPercentage() (percentage uint) { 127 return tr.TestsPass / tr.TestsTotal * 100 128 }