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  }