github.com/quickfeed/quickfeed@v0.0.0-20240507093252-ed8ca812a09c/assignments/assignments_test.go (about) 1 package assignments 2 3 import ( 4 "context" 5 "testing" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/quickfeed/quickfeed/ci" 9 "github.com/quickfeed/quickfeed/internal/qtest" 10 "github.com/quickfeed/quickfeed/qf" 11 "github.com/quickfeed/quickfeed/scm" 12 "google.golang.org/protobuf/testing/protocmp" 13 ) 14 15 // To run this test, please see instructions in the developer guide (dev.md). 16 17 func dockerClient(t *testing.T) (*ci.Docker, func()) { 18 t.Helper() 19 docker, err := ci.NewDockerCI(qtest.Logger(t)) 20 if err != nil { 21 t.Fatalf("Failed to set up docker client: %v", err) 22 } 23 return docker, func() { _ = docker.Close() } 24 } 25 26 func TestFetchAssignments(t *testing.T) { 27 qfTestOrg := scm.GetTestOrganization(t) 28 s, _ := scm.GetTestSCM(t) 29 30 course := &qf.Course{ 31 Name: "QuickFeed Test Course", 32 Code: "qf101", 33 ScmOrganizationName: qfTestOrg, 34 } 35 36 clonedTestsRepo, err := s.Clone(context.Background(), &scm.CloneOptions{ 37 Organization: course.GetScmOrganizationName(), 38 Repository: qf.TestsRepo, 39 DestDir: course.CloneDir(), 40 }) 41 if err != nil { 42 t.Fatal(err) 43 } 44 // walk the cloned tests repository and extract the assignments and the course's Dockerfile 45 assignments, dockerfile, err := readTestsRepositoryContent(clonedTestsRepo, course.ID) 46 if err != nil { 47 t.Fatal(err) 48 } 49 // We don't actually test anything here since we don't know how many assignments are in QF_TEST_ORG 50 for _, assignment := range assignments { 51 t.Logf("%+v", assignment) 52 } 53 54 // This just to simulate the behavior of UpdateFromTestsRepo to confirm that the Dockerfile is built 55 course.UpdateDockerfile(dockerfile) 56 docker, closeFn := dockerClient(t) 57 defer closeFn() 58 if err := buildDockerImage(context.Background(), qtest.Logger(t), docker, course); err != nil { 59 t.Fatal(err) 60 } 61 } 62 63 // TestUpdateCriteria simulates the behavior of UpdateFromTestsRepo 64 // where we update the criteria for an assignment. 65 // Benchmarks and criteria specifically related to a review should not be affected by UpdateFromTestsRepo. 66 // Neither should reviews 67 func TestUpdateCriteria(t *testing.T) { 68 db, cleanup := qtest.TestDB(t) 69 defer cleanup() 70 71 course := &qf.Course{} 72 admin := qtest.CreateFakeUser(t, db) 73 user := qtest.CreateFakeUser(t, db) 74 qtest.CreateCourse(t, db, admin, course) 75 76 // Assignment that will be updated 77 assignment := &qf.Assignment{ 78 CourseID: course.ID, 79 Name: "Assignment 1", 80 Deadline: qtest.Timestamp(t, "2021-12-12T19:00:00"), 81 AutoApprove: false, 82 Order: 1, 83 IsGroupLab: false, 84 } 85 86 assignment2 := &qf.Assignment{ 87 CourseID: course.ID, 88 Name: "Assignment 2", 89 Deadline: qtest.Timestamp(t, "2022-01-12T19:00:00"), 90 AutoApprove: false, 91 Order: 2, 92 IsGroupLab: false, 93 } 94 95 for _, a := range []*qf.Assignment{assignment, assignment2} { 96 if err := db.CreateAssignment(a); err != nil { 97 t.Fatal(err) 98 } 99 } 100 101 benchmarks := []*qf.GradingBenchmark{ 102 { 103 ID: 1, 104 AssignmentID: assignment.ID, 105 Heading: "Test benchmark 1", 106 Criteria: []*qf.GradingCriterion{ 107 { 108 Description: "Criterion 1", 109 BenchmarkID: 1, 110 Points: 5, 111 }, 112 { 113 Description: "Criterion 2", 114 BenchmarkID: 1, 115 Points: 10, 116 }, 117 }, 118 }, 119 { 120 ID: 2, 121 AssignmentID: assignment.ID, 122 Heading: "Test benchmark 2", 123 Criteria: []*qf.GradingCriterion{ 124 { 125 Description: "Criterion 3", 126 BenchmarkID: 2, 127 Points: 1, 128 }, 129 }, 130 }, 131 } 132 133 benchmarks2 := []*qf.GradingBenchmark{ 134 { 135 ID: 3, 136 AssignmentID: assignment2.ID, 137 Heading: "Test benchmark 3", 138 Criteria: []*qf.GradingCriterion{ 139 { 140 Description: "Criterion 4", 141 BenchmarkID: 3, 142 Points: 2, 143 }, 144 }, 145 }, 146 } 147 148 for _, bms := range [][]*qf.GradingBenchmark{benchmarks, benchmarks2} { 149 for _, bm := range bms { 150 if err := db.CreateBenchmark(bm); err != nil { 151 t.Fatal(err) 152 } 153 } 154 } 155 156 assignment.GradingBenchmarks = benchmarks 157 158 submission := &qf.Submission{ 159 AssignmentID: assignment.ID, 160 UserID: user.ID, 161 } 162 163 submission2 := &qf.Submission{ 164 AssignmentID: assignment2.ID, 165 UserID: admin.ID, 166 } 167 168 for _, s := range []*qf.Submission{submission, submission2} { 169 if err := db.CreateSubmission(s); err != nil { 170 t.Fatal(err) 171 } 172 } 173 174 // Review for assignment that will be updated 175 review := &qf.Review{ 176 ReviewerID: admin.ID, 177 SubmissionID: submission.ID, 178 GradingBenchmarks: []*qf.GradingBenchmark{ 179 { 180 AssignmentID: assignment.ID, 181 Heading: "Test benchmark 2", 182 Comment: "This is a comment", 183 Criteria: []*qf.GradingCriterion{ 184 { 185 Description: "Criterion 3", 186 Comment: "This is a comment", 187 Grade: qf.GradingCriterion_PASSED, 188 BenchmarkID: 2, 189 Points: 1, 190 }, 191 }, 192 }, 193 }, 194 } 195 196 // Review for assignment that will *not* be updated 197 review2 := &qf.Review{ 198 ReviewerID: user.ID, 199 SubmissionID: submission2.ID, 200 GradingBenchmarks: []*qf.GradingBenchmark{ 201 { 202 AssignmentID: assignment2.ID, 203 Heading: "Test benchmark 2", 204 Comment: "This is another comment", 205 Criteria: []*qf.GradingCriterion{ 206 { 207 Description: "Criterion 3", 208 Comment: "This is another comment", 209 Grade: qf.GradingCriterion_PASSED, 210 BenchmarkID: 3, 211 Points: 1, 212 }, 213 }, 214 }, 215 }, 216 } 217 218 for _, r := range []*qf.Review{review, review2} { 219 if err := db.CreateReview(r); err != nil { 220 t.Fatal(err) 221 } 222 } 223 224 if diff := cmp.Diff(benchmarks, assignment.GradingBenchmarks, protocmp.Transform()); diff != "" { 225 t.Errorf("Sanity check: mismatch (-want +got):\n%s", diff) 226 } 227 228 // Update assignments. GradingBenchmarks should not be updated 229 if err := db.UpdateAssignments([]*qf.Assignment{assignment, assignment2}); err != nil { 230 t.Fatal(err) 231 } 232 // Assignment has no added or removed benchmarks, expect nil 233 if assignment.GradingBenchmarks != nil { 234 t.Errorf("Expected nil, got %v", assignment.GradingBenchmarks) 235 } 236 237 for _, wantReview := range []*qf.Review{review, review2} { 238 gotReview, err := db.GetReview(&qf.Review{ID: wantReview.ID}) 239 if err != nil { 240 t.Fatal(err) 241 } 242 // Review should not have changed 243 if diff := cmp.Diff(wantReview, gotReview, protocmp.Transform()); diff != "" { 244 t.Fatalf("GetReview() mismatch (-want +got):\n%s", diff) 245 } 246 } 247 248 gotBenchmarks, err := db.GetBenchmarks(&qf.Assignment{ID: assignment.ID, CourseID: course.ID}) 249 if err != nil { 250 t.Fatal(err) 251 } 252 253 if diff := cmp.Diff(benchmarks, gotBenchmarks, cmp.Options{ 254 protocmp.Transform(), 255 protocmp.IgnoreFields(&qf.GradingBenchmark{}, "ID", "AssignmentID", "ReviewID"), 256 protocmp.IgnoreFields(&qf.GradingCriterion{}, "ID", "BenchmarkID"), 257 protocmp.IgnoreEnums(), 258 }); diff != "" { 259 t.Errorf("GetBenchmarks() mismatch (-want +got):\n%s", diff) 260 } 261 262 updatedBenchmarks := []*qf.GradingBenchmark{ 263 { 264 ID: 1, 265 AssignmentID: assignment.ID, 266 Heading: "Test benchmark 1", 267 Criteria: []*qf.GradingCriterion{ 268 { 269 Description: "Criterion 1", 270 BenchmarkID: 1, 271 Points: 5, 272 }, 273 }, 274 }, 275 } 276 277 assignment.GradingBenchmarks = updatedBenchmarks 278 279 if diff := cmp.Diff(updatedBenchmarks, assignment.GradingBenchmarks, protocmp.Transform()); diff != "" { 280 t.Errorf("Sanity check: mismatch (-want +got):\n%s", diff) 281 } 282 283 // Update assignments. GradingBenchmarks should be updated. 284 // This should also delete the old benchmarks in the database, and return the new benchmarks. 285 if err := db.UpdateAssignments([]*qf.Assignment{assignment, assignment2}); err != nil { 286 t.Error(err) 287 } 288 // Assignment should still reflect the updated benchmark 289 if assignment.GradingBenchmarks == nil { 290 t.Fatal("Expected assignment.GradingBenchmarks to not be nil") 291 } 292 293 // Update assignments. GradingBenchmarks should be updated 294 err = db.UpdateAssignments([]*qf.Assignment{assignment, assignment2}) 295 if err != nil { 296 t.Fatal(err) 297 } 298 299 // Benchmarks should have been updated to reflect the removal of a benchmark and a criterion 300 gotBenchmarks, err = db.GetBenchmarks(&qf.Assignment{ID: assignment.ID, CourseID: course.ID}) 301 if err != nil { 302 t.Fatal(err) 303 } 304 305 if diff := cmp.Diff(updatedBenchmarks, gotBenchmarks, protocmp.Transform()); diff != "" { 306 t.Errorf("GetBenchmarks() mismatch (-want +got):\n%s", diff) 307 } 308 309 // Finally check that reviews are unaffected 310 for _, wantReview := range []*qf.Review{review, review2} { 311 gotReview, err := db.GetReview(&qf.Review{ID: wantReview.ID}) 312 if err != nil { 313 t.Fatal(err) 314 } 315 // Review should not have changed 316 if diff := cmp.Diff(wantReview, gotReview, protocmp.Transform()); diff != "" { 317 t.Fatalf("GetReview() mismatch (-want +got):\n%s", diff) 318 } 319 } 320 }