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 }