task.go
  1  package task
  2  
  3  import (
  4  	"fmt"
  5  	"os"
  6  	"path/filepath"
  7  	"strings"
  8  
  9  	pkgerrors "github.com/pkg/errors"
 10  	"github.com/symflower/eval-dev-quality/language"
 11  	"github.com/symflower/eval-dev-quality/log"
 12  	evaltask "github.com/symflower/eval-dev-quality/task"
 13  )
 14  
 15  var (
 16  	// AllIdentifiers holds all available task identifiers.
 17  	AllIdentifiers []evaltask.Identifier
 18  	// LookupIdentifier holds a map of all available task identifiers.
 19  	LookupIdentifier = map[evaltask.Identifier]bool{}
 20  )
 21  
 22  // registerIdentifier registers the given identifier and makes it available.
 23  func registerIdentifier(name string) (identifier evaltask.Identifier) {
 24  	identifier = evaltask.Identifier(name)
 25  	AllIdentifiers = append(AllIdentifiers, identifier)
 26  
 27  	if _, ok := LookupIdentifier[identifier]; ok {
 28  		panic(fmt.Sprintf("task identifier already registered: %s", identifier))
 29  	}
 30  	LookupIdentifier[identifier] = true
 31  
 32  	return identifier
 33  }
 34  
 35  var (
 36  	// IdentifierWriteTests holds the identifier for the "write test" task.
 37  	IdentifierWriteTests = registerIdentifier("write-tests")
 38  	// IdentifierWriteTestsSymflowerFix holds the identifier for the "write test" task with the "symflower fix" applied.
 39  	IdentifierWriteTestsSymflowerFix = registerIdentifier("write-tests-symflower-fix")
 40  	// IdentifierCodeRepair holds the identifier for the "code repair" task.
 41  	IdentifierCodeRepair = registerIdentifier("code-repair")
 42  	// IdentifierTranspile holds the identifier for the "transpile" task.
 43  	IdentifierTranspile = registerIdentifier("transpile")
 44  	// IdentifierTranspileSymflowerFix holds the identifier for the "transpile" task with the "symflower fix" applied.
 45  	IdentifierTranspileSymflowerFix = registerIdentifier("transpile-symflower-fix")
 46  )
 47  
 48  // TaskForIdentifier returns a task based on the task identifier.
 49  func TaskForIdentifier(taskIdentifier evaltask.Identifier) (task evaltask.Task, err error) {
 50  	switch taskIdentifier {
 51  	case IdentifierWriteTests:
 52  		return &TaskWriteTests{}, nil
 53  	case IdentifierCodeRepair:
 54  		return &TaskCodeRepair{}, nil
 55  	case IdentifierTranspile:
 56  		return &TaskTranspile{}, nil
 57  	default:
 58  		return nil, pkgerrors.Wrap(evaltask.ErrTaskUnknown, string(taskIdentifier))
 59  	}
 60  }
 61  
 62  // taskLogger holds common logging functionality.
 63  type taskLogger struct {
 64  	*log.Logger
 65  
 66  	ctx  evaltask.Context
 67  	task evaltask.Task
 68  }
 69  
 70  // newTaskLogger initializes the logging.
 71  func newTaskLogger(ctx evaltask.Context, task evaltask.Task) (logging *taskLogger, err error) {
 72  	logging = &taskLogger{
 73  		ctx:  ctx,
 74  		task: task,
 75  	}
 76  
 77  	logging.Logger = ctx.Logger
 78  	logging.Logger.Printf("Evaluating model %q on task %q using language %q and repository %q", ctx.Model.ID(), task.Identifier(), ctx.Language.ID(), ctx.Repository.Name())
 79  
 80  	return logging, nil
 81  }
 82  
 83  // finalizeLogging finalizes the logging.
 84  func (t *taskLogger) finalize(problems []error) {
 85  	t.Logger.Printf("Evaluated model %q on task %q using language %q and repository %q: encountered %d problems: %+v", t.ctx.Model.ID(), t.task.Identifier(), t.ctx.Language.ID(), t.ctx.Repository.Name(), len(problems), problems)
 86  }
 87  
 88  // packageSourceFile returns the source file of a package.
 89  func packageSourceFile(log *log.Logger, packagePath string, language language.Language) (sourceFilePath string, err error) {
 90  	filePaths, err := language.Files(log, packagePath)
 91  	if err != nil {
 92  		return "", pkgerrors.WithStack(err)
 93  	}
 94  
 95  	for _, file := range filePaths {
 96  		if strings.HasSuffix(file, language.DefaultTestFileSuffix()) {
 97  			continue
 98  		} else if filepath.Ext(file) == language.DefaultFileExtension() { // We can assume there is only one source file because the package structure was previously verified.
 99  			return file, nil
100  		}
101  	}
102  
103  	return "", pkgerrors.WithStack(pkgerrors.Errorf("could not find any %s source file in package %q", language.Name(), packagePath))
104  }
105  
106  // repositoryOnlyHasPackages checks if a repository only has packages and returns all package paths.
107  func repositoryOnlyHasPackages(repositoryPath string) (packagePaths []string, err error) {
108  	files, err := os.ReadDir(repositoryPath)
109  	if err != nil {
110  		return nil, pkgerrors.WithStack(err)
111  	}
112  
113  	var otherFiles []string
114  	for _, file := range files {
115  		if file.Name() == "repository.json" {
116  			continue
117  		} else if file.Name() == ".git" || file.Name() == "target" { // Do not validate Git or Maven directories.
118  			continue
119  		} else if file.IsDir() {
120  			packagePaths = append(packagePaths, filepath.Join(repositoryPath, file.Name()))
121  		} else {
122  			otherFiles = append(otherFiles, file.Name())
123  		}
124  	}
125  
126  	if len(otherFiles) > 0 {
127  		return nil, pkgerrors.Errorf("the code repair repository %q must contain only packages, but found %+v", repositoryPath, otherFiles)
128  	}
129  
130  	return packagePaths, nil
131  }
132  
133  // packagesSourceAndTestFiles returns a list of all source and test relative file paths of a package.
134  func packagesSourceAndTestFiles(logger *log.Logger, packagePath string, language language.Language) (sourceFilePaths []string, testFilePaths []string, err error) {
135  	files, err := language.Files(logger, packagePath)
136  	if err != nil {
137  		return nil, nil, pkgerrors.WithStack(err)
138  	}
139  
140  	for _, file := range files {
141  		if strings.HasSuffix(file, "_init.rb") { // Exclude our custom Ruby test initialization.
142  			continue
143  		}
144  
145  		if strings.HasSuffix(file, language.DefaultTestFileSuffix()) {
146  			testFilePaths = append(testFilePaths, file)
147  		} else if strings.HasSuffix(file, language.DefaultFileExtension()) {
148  			sourceFilePaths = append(sourceFilePaths, file)
149  		}
150  	}
151  
152  	return sourceFilePaths, testFilePaths, nil
153  }