/* Copyright 2015 Google Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package review import ( "github.com/google/git-appraise/repository" "github.com/google/git-appraise/review/comment" "github.com/google/git-appraise/review/request" "sort" "testing" ) func TestCommentSorting(t *testing.T) { sampleComments := []*comment.Comment{ &comment.Comment{ Timestamp: "012400", Description: "Fourth", }, &comment.Comment{ Timestamp: "012400", Description: "Fifth", }, &comment.Comment{ Timestamp: "012346", Description: "Second", }, &comment.Comment{ Timestamp: "012345", Description: "First", }, &comment.Comment{ Timestamp: "012347", Description: "Third", }, } sort.Stable(commentsByTimestamp(sampleComments)) descriptions := []string{} for _, comment := range sampleComments { descriptions = append(descriptions, comment.Description) } if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") { t.Fatalf("Comment ordering failed. Got %v", sampleComments) } } func TestThreadSorting(t *testing.T) { sampleThreads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012400", Description: "Fourth", }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012400", Description: "Fifth", }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Description: "Second", }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Description: "First", }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012347", Description: "Third", }, }, } sort.Stable(byTimestamp(sampleThreads)) descriptions := []string{} for _, thread := range sampleThreads { descriptions = append(descriptions, thread.Comment.Description) } if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") { t.Fatalf("Comment thread ordering failed. Got %v", sampleThreads) } } func TestRequestSorting(t *testing.T) { sampleRequests := []request.Request{ request.Request{ Timestamp: "012400", Description: "Fourth", }, request.Request{ Timestamp: "012400", Description: "Fifth", }, request.Request{ Timestamp: "012346", Description: "Second", }, request.Request{ Timestamp: "012345", Description: "First", }, request.Request{ Timestamp: "012347", Description: "Third", }, } sort.Stable(requestsByTimestamp(sampleRequests)) descriptions := []string{} for _, r := range sampleRequests { descriptions = append(descriptions, r.Description) } if !(descriptions[0] == "First" && descriptions[1] == "Second" && descriptions[2] == "Third" && descriptions[3] == "Fourth" && descriptions[4] == "Fifth") { t.Fatalf("Review request ordering failed. Got %v", sampleRequests) } } func validateUnresolved(t *testing.T, resolved *bool) { if resolved != nil { t.Fatalf("Expected resolved status to be unset, but instead it was %v", *resolved) } } func validateAccepted(t *testing.T, resolved *bool) { if resolved == nil { t.Fatal("Expected resolved status to be true, but it was unset") } if !*resolved { t.Fatal("Expected resolved status to be true, but it was false") } } func validateRejected(t *testing.T, resolved *bool) { if resolved == nil { t.Fatal("Expected resolved status to be false, but it was unset") } if *resolved { t.Fatal("Expected resolved status to be false, but it was true") } } func (commentThread *CommentThread) validateUnresolved(t *testing.T) { validateUnresolved(t, commentThread.Resolved) } func (commentThread *CommentThread) validateAccepted(t *testing.T) { validateAccepted(t, commentThread.Resolved) } func (commentThread *CommentThread) validateRejected(t *testing.T) { validateRejected(t, commentThread.Resolved) } func TestSimpleAcceptedThreadStatus(t *testing.T) { resolved := true simpleThread := CommentThread{ Comment: comment.Comment{ Resolved: &resolved, }, } simpleThread.updateResolvedStatus() simpleThread.validateAccepted(t) } func TestSimpleRejectedThreadStatus(t *testing.T) { resolved := false simpleThread := CommentThread{ Comment: comment.Comment{ Resolved: &resolved, }, } simpleThread.updateResolvedStatus() simpleThread.validateRejected(t) } func TestFYIThenAcceptedThreadStatus(t *testing.T) { accepted := true sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: nil, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &accepted, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateUnresolved(t) } func TestFYIThenFYIThreadStatus(t *testing.T) { sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: nil, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: nil, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateUnresolved(t) } func TestFYIThenRejectedThreadStatus(t *testing.T) { rejected := false sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: nil, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &rejected, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateRejected(t) } func TestAcceptedThenAcceptedThreadStatus(t *testing.T) { accepted := true sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: &accepted, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &accepted, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateAccepted(t) } func TestAcceptedThenFYIThreadStatus(t *testing.T) { accepted := true sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: &accepted, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: nil, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateAccepted(t) } func TestAcceptedThenRejectedThreadStatus(t *testing.T) { accepted := true rejected := false sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: &accepted, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &rejected, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateRejected(t) } func TestRejectedThenAcceptedThreadStatus(t *testing.T) { accepted := true rejected := false sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: &rejected, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &accepted, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateUnresolved(t) } func TestRejectedThenFYIThreadStatus(t *testing.T) { rejected := false sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: &rejected, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: nil, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateRejected(t) } func TestRejectedThenRejectedThreadStatus(t *testing.T) { rejected := false sampleThread := CommentThread{ Comment: comment.Comment{ Resolved: &rejected, }, Children: []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &rejected, }, }, }, } sampleThread.updateResolvedStatus() sampleThread.validateRejected(t) } func TestRejectedThenAcceptedThreadsStatus(t *testing.T) { accepted := true rejected := false threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &rejected, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: &accepted, }, }, } status := updateThreadsStatus(threads) validateRejected(t, status) } func TestRejectedThenFYIThreadsStatus(t *testing.T) { rejected := false threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &rejected, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: nil, }, }, } status := updateThreadsStatus(threads) validateRejected(t, status) } func TestRejectedThenRejectedThreadsStatus(t *testing.T) { rejected := false threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &rejected, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: &rejected, }, }, } status := updateThreadsStatus(threads) validateRejected(t, status) } func TestAcceptedThenAcceptedThreadsStatus(t *testing.T) { accepted := true threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &accepted, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: &accepted, }, }, } status := updateThreadsStatus(threads) validateAccepted(t, status) } func TestAcceptedThenFYIThreadsStatus(t *testing.T) { accepted := true threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &accepted, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: nil, }, }, } status := updateThreadsStatus(threads) validateAccepted(t, status) } func TestAcceptedThenRejectedThreadsStatus(t *testing.T) { accepted := true rejected := false threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: &accepted, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: &rejected, }, }, } status := updateThreadsStatus(threads) validateRejected(t, status) } func TestFYIThenAcceptedThreadsStatus(t *testing.T) { accepted := true threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: nil, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: &accepted, }, }, } status := updateThreadsStatus(threads) validateAccepted(t, status) } func TestFYIThenFYIThreadsStatus(t *testing.T) { threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: nil, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: nil, }, }, } status := updateThreadsStatus(threads) validateUnresolved(t, status) } func TestFYIThenRejectedThreadsStatus(t *testing.T) { rejected := false threads := []CommentThread{ CommentThread{ Comment: comment.Comment{ Timestamp: "012345", Resolved: nil, }, }, CommentThread{ Comment: comment.Comment{ Timestamp: "012346", Resolved: &rejected, }, }, } status := updateThreadsStatus(threads) validateRejected(t, status) } func TestBuildCommentThreads(t *testing.T) { rejected := false accepted := true root := comment.Comment{ Timestamp: "012345", Resolved: nil, Description: "root", } rootHash, err := root.Hash() if err != nil { t.Fatal(err) } child := comment.Comment{ Timestamp: "012346", Resolved: nil, Parent: rootHash, Description: "child", } childHash, err := child.Hash() updatedChild := comment.Comment{ Timestamp: "012346", Resolved: &rejected, Original: childHash, Description: "updated child", } updatedChildHash, err := updatedChild.Hash() if err != nil { t.Fatal(err) } leaf := comment.Comment{ Timestamp: "012347", Resolved: &accepted, Parent: childHash, Description: "leaf", } leafHash, err := leaf.Hash() if err != nil { t.Fatal(err) } commentsByHash := map[string]comment.Comment{ rootHash: root, childHash: child, updatedChildHash: updatedChild, leafHash: leaf, } threads := buildCommentThreads(commentsByHash) if len(threads) != 1 { t.Fatalf("Unexpected threads: %v", threads) } rootThread := threads[0] if rootThread.Comment.Description != "root" { t.Fatalf("Unexpected root thread: %v", rootThread) } if !rootThread.Edited { t.Fatalf("Unexpected root thread edited status: %v", rootThread) } if len(rootThread.Children) != 1 { t.Fatalf("Unexpected root children: %v", rootThread.Children) } rootChild := rootThread.Children[0] if rootChild.Comment.Description != "updated child" { t.Fatalf("Unexpected updated child: %v", rootChild) } if rootChild.Original.Description != "child" { t.Fatalf("Unexpected original child: %v", rootChild) } if len(rootChild.Edits) != 1 { t.Fatalf("Unexpected child history: %v", rootChild.Edits) } if len(rootChild.Children) != 1 { t.Fatalf("Unexpected leaves: %v", rootChild.Children) } threadLeaf := rootChild.Children[0] if threadLeaf.Comment.Description != "leaf" { t.Fatalf("Unexpected leaf: %v", threadLeaf) } if len(threadLeaf.Children) != 0 { t.Fatalf("Unexpected leaf children: %v", threadLeaf.Children) } if threadLeaf.Edited { t.Fatalf("Unexpected leaf edited status: %v", threadLeaf) } } func TestGetHeadCommit(t *testing.T) { repo := repository.NewMockRepoForTest() submittedSimpleReview, err := Get(repo, repository.TestCommitB) if err != nil { t.Fatal(err) } submittedSimpleReviewHead, err := submittedSimpleReview.GetHeadCommit() if err != nil { t.Fatal("Unable to compute the head commit for a known review of a simple commit: ", err) } if submittedSimpleReviewHead != repository.TestCommitB { t.Fatal("Unexpected head commit computed for a known review of a simple commit.") } submittedModifiedReview, err := Get(repo, repository.TestCommitD) if err != nil { t.Fatal(err) } submittedModifiedReviewHead, err := submittedModifiedReview.GetHeadCommit() if err != nil { t.Fatal("Unable to compute the head commit for a known, multi-commit review: ", err) } if submittedModifiedReviewHead != repository.TestCommitE { t.Fatal("Unexpected head commit for a known, multi-commit review.") } pendingReview, err := Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } pendingReviewHead, err := pendingReview.GetHeadCommit() if err != nil { t.Fatal("Unable to compute the head commit for a known review of a merge commit: ", err) } if pendingReviewHead != repository.TestCommitI { t.Fatal("Unexpected head commit computed for a pending review.") } } func TestGetBaseCommit(t *testing.T) { repo := repository.NewMockRepoForTest() submittedSimpleReview, err := Get(repo, repository.TestCommitB) if err != nil { t.Fatal(err) } submittedSimpleReviewBase, err := submittedSimpleReview.GetBaseCommit() if err != nil { t.Fatal("Unable to compute the base commit for a known review of a simple commit: ", err) } if submittedSimpleReviewBase != repository.TestCommitA { t.Fatal("Unexpected base commit computed for a known review of a simple commit.") } submittedMergeReview, err := Get(repo, repository.TestCommitD) if err != nil { t.Fatal(err) } submittedMergeReviewBase, err := submittedMergeReview.GetBaseCommit() if err != nil { t.Fatal("Unable to compute the base commit for a known review of a merge commit: ", err) } if submittedMergeReviewBase != repository.TestCommitC { t.Fatal("Unexpected base commit computed for a known review of a merge commit.") } pendingReview, err := Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } pendingReviewBase, err := pendingReview.GetBaseCommit() if err != nil { t.Fatal("Unable to compute the base commit for a known review of a merge commit: ", err) } if pendingReviewBase != repository.TestCommitF { t.Fatal("Unexpected base commit computed for a pending review.") } abandonRequest := pendingReview.Request abandonRequest.TargetRef = "" abandonNote, err := abandonRequest.Write() if err != nil { t.Fatal(err) } if err := repo.AppendNote(request.Ref, repository.TestCommitG, abandonNote); err != nil { t.Fatal(err) } abandonedReview, err := Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } if abandonedReview.IsOpen() { t.Fatal("Failed to update a review to be abandoned") } abandonedReviewBase, err := abandonedReview.GetBaseCommit() if err != nil { t.Fatal("Unable to compute the base commit for an abandoned review: ", err) } if abandonedReviewBase != repository.TestCommitE { t.Fatal("Unexpected base commit computed for an abandoned review.") } } func TestGetRequests(t *testing.T) { repo := repository.NewMockRepoForTest() pendingReview, err := Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } if len(pendingReview.AllRequests) != 3 || pendingReview.Request.Description != "Final description of G" { t.Fatal("Unexpected requests for a pending review: ", pendingReview.AllRequests, pendingReview.Request) } } func TestRebase(t *testing.T) { repo := repository.NewMockRepoForTest() pendingReview, err := Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } // Rebase the review and then confirm that it has been updated correctly. if err := pendingReview.Rebase(true); err != nil { t.Fatal(err) } reviewJSON, err := pendingReview.GetJSON() if err != nil { t.Fatal(err) } headRef, err := repo.GetHeadRef() if err != nil { t.Fatal(err) } if headRef != pendingReview.Request.ReviewRef { t.Fatal("Failed to switch to the review ref during a rebase") } isAncestor, err := repo.IsAncestor(pendingReview.Revision, archiveRef) if err != nil { t.Fatal(err) } if !isAncestor { t.Fatalf("Commit %q is not archived", pendingReview.Revision) } reviewCommit, err := repo.GetCommitHash(pendingReview.Request.ReviewRef) if err != nil { t.Fatal(err) } reviewAlias := pendingReview.Request.Alias if reviewAlias == "" || reviewAlias == pendingReview.Revision || reviewCommit != reviewAlias { t.Fatalf("Failed to set the review alias: %q", reviewJSON) } // Submit the review. if err := repo.SwitchToRef(pendingReview.Request.TargetRef); err != nil { t.Fatal(err) } if err := repo.MergeRef(pendingReview.Request.ReviewRef, true); err != nil { t.Fatal(err) } // Reread the review and confirm that it has been submitted. submittedReview, err := Get(repo, pendingReview.Revision) if err != nil { t.Fatal(err) } submittedReviewJSON, err := submittedReview.GetJSON() if err != nil { t.Fatal(err) } if !submittedReview.Submitted { t.Fatalf("Failed to submit the review: %q", submittedReviewJSON) } } func TestRebaseDetachedHead(t *testing.T) { repo := repository.NewMockRepoForTest() pendingReview, err := Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } // Switch the review to having a review ref that is not a branch. pendingReview.Request.ReviewRef = repository.TestAlternateReviewRef newNote, err := pendingReview.Request.Write() if err != nil { t.Fatal(err) } if err := repo.AppendNote(request.Ref, pendingReview.Revision, newNote); err != nil { t.Fatal(err) } pendingReview, err = Get(repo, repository.TestCommitG) if err != nil { t.Fatal(err) } // Rebase the review and then confirm that it has been updated correctly. if err := pendingReview.Rebase(true); err != nil { t.Fatal(err) } headRef, err := repo.GetHeadRef() if err != nil { t.Fatal(err) } if headRef != pendingReview.Request.Alias { t.Fatal("Failed to switch to a detached head during a rebase") } isAncestor, err := repo.IsAncestor(pendingReview.Revision, archiveRef) if err != nil { t.Fatal(err) } if !isAncestor { t.Fatalf("Commit %q is not archived", pendingReview.Revision) } // Submit the review. if err := repo.SwitchToRef(pendingReview.Request.TargetRef); err != nil { t.Fatal(err) } reviewHead, err := pendingReview.GetHeadCommit() if err != nil { t.Fatal(err) } if err := repo.MergeRef(reviewHead, true); err != nil { t.Fatal(err) } // Reread the review and confirm that it has been submitted. submittedReview, err := Get(repo, pendingReview.Revision) if err != nil { t.Fatal(err) } submittedReviewJSON, err := submittedReview.GetJSON() if err != nil { t.Fatal(err) } if !submittedReview.Submitted { t.Fatalf("Failed to submit the review: %q", submittedReviewJSON) } }