package main import ( "errors" "fmt" "os" "os/exec" "path" "strings" "testing" "go.uber.org/mock/gomock" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" mock_common "src.opensuse.org/autogits/common/mock" ) func TestPR(t *testing.T) { baseConfig := common.AutogitConfig{ Reviewers: []string{"+super1", "*super2", "m1", "-m2"}, Branch: "branch", Organization: "foo", GitProjectName: "barPrj", } type prdata struct { pr *models.PullRequest pr_err error reviews []*models.PullReview review_error error } tests := []struct { name string data []prdata api_error string resLen int reviewed bool consistentSet bool prjGitPRIndex int reviewSetFetcher func(*mock_common.MockGiteaPRFetcher) (*PRSet, error) }{ { name: "Error fetching PullRequest", data: []prdata{ {pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}, pr_err: errors.New("Missing PR")}, }, prjGitPRIndex: -1, }, { name: "Error fetching PullRequest in PrjGit", data: []prdata{ {pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}, pr_err: errors.New("missing PR")}, {pr: &models.PullRequest{Body: "", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}}, }, }, { name: "Error fetching prjgit", data: []prdata{ {pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}}, }, resLen: 1, prjGitPRIndex: -1, }, { name: "Review set is consistent", data: []prdata{ {pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}}, {pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}}, }, resLen: 2, prjGitPRIndex: 1, consistentSet: true, }, { name: "Review set is consistent: 1pkg", data: []prdata{ {pr: &models.PullRequest{Body: "PR: foo/barPrj#22", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}}, {pr: &models.PullRequest{Body: "PR: test/repo#42", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}}, }, resLen: 2, prjGitPRIndex: 1, consistentSet: true, }, { name: "Review set is consistent: 2pkg", data: []prdata{ {pr: &models.PullRequest{Body: "some desc", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}}, {pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}}, {pr: &models.PullRequest{Body: "some other desc\nPR: foo/fer#33", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}}}, }, resLen: 3, prjGitPRIndex: 1, consistentSet: true, }, { name: "Review set of prjgit PR is consistent", data: []prdata{ { pr: &models.PullRequest{Body: "", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}, reviews: []*models.PullReview{ {Body: "LGTM", User: &models.User{UserName: "m2"}, State: common.ReviewStateApproved}, {Body: "LGTM", User: &models.User{UserName: "super2"}, State: common.ReviewStateApproved}, {Body: "LGTM", User: &models.User{UserName: common.Bot_BuildReview}, State: common.ReviewStateApproved}, }, }, }, resLen: 1, prjGitPRIndex: 0, consistentSet: true, reviewed: true, reviewSetFetcher: func(mock *mock_common.MockGiteaPRFetcher) (*PRSet, error) { return FetchPRSet(mock, "foo", "barPrj", 42, &baseConfig) }, }, { name: "Review set is consistent: 2pkg", data: []prdata{ {pr: &models.PullRequest{Body: "PR: foo/barPrj#222", Index: 42, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "test"}}}}}, {pr: &models.PullRequest{Body: "PR: test/repo2#41", Index: 20, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}}, {pr: &models.PullRequest{Body: "PR: test/repo#42\nPR: test/repo2#41", Index: 22, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "barPrj", Owner: &models.User{UserName: "foo"}}}}}, {pr: &models.PullRequest{Body: "PR: foo/barPrj#20", Index: 41, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo2", Owner: &models.User{UserName: "test"}}}}}, }, resLen: 3, prjGitPRIndex: 2, consistentSet: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctl := gomock.NewController(t) pr_mock := mock_common.NewMockGiteaPRFetcher(ctl) review_mock := mock_common.NewMockGiteaPRChecker(ctl) // reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl) if test.reviewSetFetcher == nil { // if we are fetching the prjgit directly, the these mocks are not called if test.prjGitPRIndex >= 0 { pr_mock.EXPECT().GetAssociatedPrjGitPR(baseConfig.Organization, baseConfig.GitProjectName, test.data[0].pr.Base.Repo.Owner.UserName, test.data[0].pr.Base.Repo.Name, test.data[0].pr.Index). Return(test.data[test.prjGitPRIndex].pr, test.data[test.prjGitPRIndex].pr_err) } else if test.prjGitPRIndex < 0 { // no prjgit PR pr_mock.EXPECT().GetAssociatedPrjGitPR(baseConfig.Organization, baseConfig.GitProjectName, test.data[0].pr.Base.Repo.Owner.UserName, test.data[0].pr.Base.Repo.Name, test.data[0].pr.Index). Return(nil, nil) } } var test_err error for _, data := range test.data { pr_mock.EXPECT().GetPullRequest(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.pr, data.pr_err).AnyTimes() if data.pr_err != nil { test_err = data.pr_err } review_mock.EXPECT().GetPullRequestReviews(data.pr.Base.Repo.Owner.UserName, data.pr.Base.Repo.Name, data.pr.Index).Return(data.reviews, data.review_error).AnyTimes() } var res *PRSet var err error if test.reviewSetFetcher != nil { res, err = test.reviewSetFetcher(pr_mock) } else { res, err = FetchPRSet(pr_mock, "test", "repo", 42, &baseConfig) } if err == nil { if test_err != nil { t.Fatal("Expected", test_err, "but got", err) } } else { if res != nil { t.Fatal("error but got ReviewSet?") } if test.api_error != "" { if err.Error() != test.api_error { t.Fatal("expected", test.api_error, "but got", err) } } else if test_err != err { t.Fatal("expected", test_err, "but got", err) } return } if test.resLen != len(res.prs) { t.Error("expected result len", test.resLen, "but got", len(res.prs)) } PrjGitPR, err := res.GetPrjGitPR() if test.prjGitPRIndex < 0 { if err == nil { t.Error("expected error, but nothing") } } pr_found := false if test.prjGitPRIndex >= 0 { for i := range test.data { if PrjGitPR == test.data[i].pr && i == test.prjGitPRIndex { t.Log("found at index", i) pr_found = true } } if !pr_found { t.Error("Cannot find expected PrjGit location in PR set", PrjGitPR) } } else { if PrjGitPR != nil { t.Log("Expected prjgit not found, but found?", PrjGitPR) } } if isConsistent := res.IsConsistent(); isConsistent != test.consistentSet { t.Error("IsConsistent() returned unexpected:", isConsistent) } /* if err := res.AssignReviewers(reviewer_mock); err != nil { t.Error("expected no errors assigning reviewers:", err) } */ maintainers := mock_common.NewMockMaintainershipData(ctl) maintainers.EXPECT().IsApproved(gomock.Any(), gomock.Any()).Return(true).AnyTimes() if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed { t.Error("expected reviewed to be NOT", isApproved) } }) } } func TestPRAssignReviewers(t *testing.T) { tests := []struct { name string config common.AutogitConfig reviewers []struct { org, repo string num int64 reviewer string } pkgReviews []*models.PullReview prjReviews []*models.PullReview expectedReviewerCall [2][]string }{ { name: "No reviewers", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{}, }, expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}}, }, { name: "One project reviewer only", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{"-user1"}, }, expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}}, }, { name: "One project reviewer and one pkg reviewer only", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{"-user1", "user2"}, }, expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"user2", "prjmaintainer", "pkgmaintainer"}}, }, { name: "No need to get reviews of submitter", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{"-user1", "submitter"}, }, expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}}, }, { name: "Reviews are done", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{"-user1", "user2"}, }, pkgReviews: []*models.PullReview{ { State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, }, { State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}, }, { State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}, }, }, prjReviews: []*models.PullReview{ { State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}, }, { State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}, }, }, expectedReviewerCall: [2][]string{}, }, { name: "Stale review is not done, re-request it", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{"-user1", "user2"}, }, pkgReviews: []*models.PullReview{ { State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}, }, { State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}, }, }, prjReviews: []*models.PullReview{ { State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}, Stale: true, }, { State: common.ReviewStateRequestReview, Stale: true, User: &models.User{UserName: "autogits_obs_staging_bot"}, }, }, expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer"}}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctl := gomock.NewController(t) pr_mock := mock_common.NewMockGiteaPRFetcher(ctl) review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl) maintainership_mock := mock_common.NewMockMaintainershipData(ctl) pr_mock.EXPECT().GetPullRequest("other", "pkgrepo", int64(1)).Return(&models.PullRequest{ Body: "Some description is here", User: &models.User{UserName: "submitter"}, RequestedReviewers: []*models.User{}, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "pkgrepo", Owner: &models.User{UserName: "other"}}}, Head: &models.PRBranchInfo{}, Index: 1, }, nil) review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, nil) pr_mock.EXPECT().GetAssociatedPrjGitPR("org", "repo", "other", "pkgrepo", int64(1)).Return(&models.PullRequest{ Body: fmt.Sprintf(common.PrPattern, "other", "pkgrepo", 1), User: &models.User{UserName: "bot1"}, RequestedReviewers: []*models.User{{UserName: "main_reviewer"}}, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}}, Head: &models.PRBranchInfo{}, Index: 42, }, nil) review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(42)).Return(test.prjReviews, nil) maintainership_mock.EXPECT().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes() maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo").Return([]string{"pkgmaintainer"}).AnyTimes() prs, _ := FetchPRSet(pr_mock, "other", "pkgrepo", int64(1), &test.config) if len(prs.prs) != 2 { t.Fatal("PRs not fetched") } for _, pr := range prs.prs { r := test.expectedReviewerCall[0] if !prs.IsPrjGitPR(pr.pr) { r = test.expectedReviewerCall[1] } if len(r) > 0 { review_mock.EXPECT().RequestReviews(pr.pr, r).Return(nil, nil) } } prs.AssignReviewers(review_mock, maintainership_mock) }) } prjgit_tests := []struct { name string config common.AutogitConfig reviewers []struct { org, repo string num int64 reviewer string } prjReviews []*models.PullReview expectedReviewerCall [2][]string }{ { name: "PrjMaintainers in prjgit review when not part of pkg set", config: common.AutogitConfig{ GitProjectName: "repo", Organization: "org", Branch: "main", Reviewers: []string{}, }, expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot", "prjmaintainer"}}, }, } for _, test := range prjgit_tests { t.Run(test.name, func(t *testing.T) { ctl := gomock.NewController(t) pr_mock := mock_common.NewMockGiteaPRFetcher(ctl) review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl) maintainership_mock := mock_common.NewMockMaintainershipData(ctl) pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{ Body: "Some description is here", User: &models.User{UserName: "submitter"}, RequestedReviewers: []*models.User{}, Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}}, Head: &models.PRBranchInfo{}, Index: 1, }, nil) review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(1)).Return(test.prjReviews, nil) maintainership_mock.EXPECT().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes() prs, _ := FetchPRSet(pr_mock, "org", "repo", int64(1), &test.config) if len(prs.prs) != 1 { t.Fatal("PRs not fetched") } for _, pr := range prs.prs { r := test.expectedReviewerCall[0] if !prs.IsPrjGitPR(pr.pr) { t.Fatal("only prjgit pr here") } if len(r) > 0 { review_mock.EXPECT().RequestReviews(pr.pr, r).Return(nil, nil) } } prs.AssignReviewers(review_mock, maintainership_mock) }) } } func TestPRMerge(t *testing.T) { cwd, _ := os.Getwd() cmd := exec.Command("/usr/bin/bash", path.Join(cwd, "test_repo_setup.sh")) cmd.Dir = t.TempDir() if out, err := cmd.CombinedOutput(); err != nil { t.Fatal(string(out)) } common.ExtraGitParams = []string{ "GIT_CONFIG_COUNT=1", "GIT_CONFIG_KEY_0=protocol.file.allow", "GIT_CONFIG_VALUE_0=always", "GIT_AUTHOR_NAME=testname", "GIT_AUTHOR_EMAIL=test@suse.com", "GIT_AUTHOR_DATE='2005-04-07T22:13:13'", "GIT_COMMITTER_NAME=testname", "GIT_COMMITTER_EMAIL=test@suse.com", "GIT_COMMITTER_DATE='2005-04-07T22:13:13'", } config := &common.AutogitConfig{ Organization: "org", GitProjectName: "prj", } tests := []struct { name string pr *models.PullRequest mergeError string }{ { name: "Merge base not merged in main", pr: &models.PullRequest{ Base: &models.PRBranchInfo{ Sha: "e8b0de43d757c96a9d2c7101f4bff404e322f53a1fa4041fb85d646110c38ad4", // "base_add_b1" Repo: &models.Repository{ Name: "prj", Owner: &models.User{ UserName: "org", }, SSHURL: path.Join(cmd.Dir, "prjgit"), }, }, Head: &models.PRBranchInfo{ Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2" }, }, mergeError: "Aborting merge", }, { name: "Merge conflict in modules", pr: &models.PullRequest{ Base: &models.PRBranchInfo{ Sha: "4fbd1026b2d7462ebe9229a49100c11f1ad6555520a21ba515122d8bc41328a8", Repo: &models.Repository{ Name: "prj", Owner: &models.User{ UserName: "org", }, SSHURL: path.Join(cmd.Dir, "prjgit"), }, }, Head: &models.PRBranchInfo{ Sha: "88584433de1c917c1d773f62b82381848d882491940b5e9b427a540aa9057d9a", // "base_add_b2" }, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ctl := gomock.NewController(t) mock := mock_common.NewMockGiteaPRFetcher(ctl) mock.EXPECT().GetPullRequest("org", "prj", int64(1)).Return(test.pr, nil) set, err := FetchPRSet(mock, "org", "prj", 1, config) if err != nil { t.Fatal(err) } if err = set.Merge(); err != nil && (test.mergeError == "" || (len(test.mergeError) > 0 && !strings.Contains(err.Error(), test.mergeError))) { t.Fatal(err) } }) } }