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  }