github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/assignments/walk_tests_repo.go (about)

     1  package assignments
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/fs"
     7  	"os"
     8  	"path/filepath"
     9  	"sort"
    10  
    11  	"github.com/quickfeed/quickfeed/qf"
    12  )
    13  
    14  const (
    15  	assignmentFile     = "assignment.yml"
    16  	assignmentFileYaml = "assignment.yaml"
    17  	criteriaFile       = "criteria.json"
    18  	dockerfile         = "Dockerfile"
    19  	taskFilePattern    = "task-*.md"
    20  )
    21  
    22  var patterns = []string{
    23  	assignmentFile,
    24  	assignmentFileYaml,
    25  	criteriaFile,
    26  	dockerfile,
    27  	taskFilePattern,
    28  }
    29  
    30  // matchAny returns true if filename matches one of the target patterns.
    31  func matchAny(filename string) bool {
    32  	for _, pattern := range patterns {
    33  		if ok, _ := filepath.Match(pattern, filename); ok {
    34  			return true
    35  		}
    36  	}
    37  	return false
    38  }
    39  
    40  // match returns true if filename matches the given pattern.
    41  func match(filename, pattern string) bool {
    42  	if ok, _ := filepath.Match(pattern, filename); ok {
    43  		return true
    44  	}
    45  	return false
    46  }
    47  
    48  // readTestsRepositoryContent reads dir and returns a list of assignments and
    49  // the course's Dockerfile content if there exists a 'tests/scripts/Dockerfile'.
    50  // Assignments are extracted from 'assignment.yml' files, one for each assignment.
    51  func readTestsRepositoryContent(dir string, courseID uint64) ([]*qf.Assignment, string, error) {
    52  	files, err := walkTestsRepository(dir)
    53  	if err != nil {
    54  		return nil, "", err
    55  	}
    56  
    57  	// Process all assignment.yml files first
    58  	assignmentsMap := make(map[string]*qf.Assignment)
    59  	for path, contents := range files {
    60  		assignmentName := filepath.Base(filepath.Dir(path))
    61  		switch filepath.Base(path) {
    62  		case assignmentFile, assignmentFileYaml:
    63  			assignment, err := newAssignmentFromFile(contents, assignmentName, courseID)
    64  			if err != nil {
    65  				return nil, "", err
    66  			}
    67  			assignmentsMap[assignmentName] = assignment
    68  		}
    69  	}
    70  
    71  	var courseDockerfile string
    72  
    73  	// Process other files in tests repository
    74  	for path, contents := range files {
    75  		assignmentName := filepath.Base(filepath.Dir(path))
    76  		filename := filepath.Base(path)
    77  
    78  		switch filename {
    79  		case criteriaFile:
    80  			var benchmarks []*qf.GradingBenchmark
    81  			if err := json.Unmarshal(contents, &benchmarks); err != nil {
    82  				return nil, "", fmt.Errorf("failed to unmarshal %q: %s", criteriaFile, err)
    83  			}
    84  			// Benchmarks and criteria must have courseID
    85  			// for access control checks.
    86  			for _, bm := range benchmarks {
    87  				bm.CourseID = courseID
    88  				for _, c := range bm.Criteria {
    89  					c.CourseID = courseID
    90  				}
    91  			}
    92  			assignmentsMap[assignmentName].GradingBenchmarks = benchmarks
    93  
    94  		case dockerfile:
    95  			courseDockerfile = string(contents)
    96  		}
    97  
    98  		if match(filename, taskFilePattern) {
    99  			assignment := assignmentsMap[assignmentName]
   100  			taskName := taskName(filename)
   101  			task, err := newTask(contents, assignment.GetOrder(), taskName)
   102  			if err != nil {
   103  				return nil, "", err
   104  			}
   105  			assignmentsMap[assignmentName].Tasks = append(assignmentsMap[assignmentName].Tasks, task)
   106  		}
   107  	}
   108  
   109  	assignments := make([]*qf.Assignment, 0)
   110  	for _, assignment := range assignmentsMap {
   111  		assignments = append(assignments, assignment)
   112  		sort.Slice(assignment.Tasks, func(i, j int) bool {
   113  			return assignment.Tasks[i].Title < assignment.Tasks[j].Title
   114  		})
   115  	}
   116  	sort.Slice(assignments, func(i, j int) bool {
   117  		return assignments[i].Order < assignments[j].Order
   118  	})
   119  
   120  	return assignments, courseDockerfile, nil
   121  }
   122  
   123  // walkTestsRepository walks the tests repository and returns a map of file names and their contents.
   124  func walkTestsRepository(dir string) (map[string][]byte, error) {
   125  	if _, err := os.Stat(dir); os.IsNotExist(err) {
   126  		return nil, err
   127  	}
   128  	files := make(map[string][]byte)
   129  	err := filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
   130  		if err != nil {
   131  			// Walk unable to read path; stop walking the tree
   132  			return err
   133  		}
   134  		if !info.IsDir() && matchAny(info.Name()) {
   135  			if files[path], err = os.ReadFile(path); err != nil {
   136  				return err
   137  			}
   138  		}
   139  		return nil
   140  	})
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  	return files, nil
   145  }