This is an automated email from the ASF dual-hosted git repository.

likyh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 657656e72 fix(dora): add comments and unit test (#4795)
657656e72 is described below

commit 657656e72b1d10291456d8b832fc3797d30df4b6
Author: Warren Chen <[email protected]>
AuthorDate: Tue Mar 28 16:54:18 2023 +0800

    fix(dora): add comments and unit test (#4795)
---
 .../dora/tasks/change_lead_time_calculator.go      | 152 +++++++++++++++------
 .../dora/tasks/change_lead_time_calculator_test.go |  60 ++++++++
 2 files changed, 169 insertions(+), 43 deletions(-)

diff --git a/backend/plugins/dora/tasks/change_lead_time_calculator.go 
b/backend/plugins/dora/tasks/change_lead_time_calculator.go
index 09eaff9a0..c28b22d14 100644
--- a/backend/plugins/dora/tasks/change_lead_time_calculator.go
+++ b/backend/plugins/dora/tasks/change_lead_time_calculator.go
@@ -29,40 +29,20 @@ import (
        "time"
 )
 
+// CalculateChangeLeadTime calculates change lead time for a project.
 func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) errors.Error {
+       // Get instances of the DAL and logger
        db := taskCtx.GetDal()
        logger := taskCtx.GetLogger()
        data := taskCtx.GetData().(*DoraTaskData)
-       // construct a list of tuple[task, oldPipelineCommitSha, 
newPipelineCommitSha, taskFinishedDate]
-       deploymentClause := []dal.Clause{
-               dal.Select(`ct.id as task_id, cpc.commit_sha as 
new_deploy_commit_sha,
-                       ct.finished_date as task_finished_date, cpc.repo_id as 
repo_id`),
-               dal.From(`cicd_tasks ct`),
-               dal.Join(`left join cicd_pipeline_commits cpc on ct.pipeline_id 
= cpc.pipeline_id`),
-               dal.Join(`left join project_mapping pm on pm.row_id = 
ct.cicd_scope_id`),
-               dal.Where(`ct.environment = ? and ct.type = ? and ct.result = ? 
and pm.project_name = ? and pm.table = ?`,
-                       devops.PRODUCTION, devops.DEPLOYMENT, devops.SUCCESS, 
data.Options.ProjectName, "cicd_scopes"),
-               dal.Orderby(`cpc.repo_id, ct.started_date `),
-       }
-       deploymentDiffPairs := make([]deploymentPair, 0)
-       err := db.All(&deploymentDiffPairs, deploymentClause...)
+
+       // Build deployment pairs
+       deploymentDiffPairs, err := buildDeploymentPairs(db, data)
        if err != nil {
                return err
        }
-       // deploymentDiffPairs[i-1].NewDeployCommitSha is 
deploymentDiffPairs[i].OldDeployCommitSha
-       oldDeployCommitSha := ""
-       lastRepoId := ""
-       for i := 0; i < len(deploymentDiffPairs); i++ {
-               // if two deployments belong to different repo, let's skip
-               if lastRepoId == deploymentDiffPairs[i].RepoId {
-                       deploymentDiffPairs[i].OldDeployCommitSha = 
oldDeployCommitSha
-               } else {
-                       lastRepoId = deploymentDiffPairs[i].RepoId
-               }
-               oldDeployCommitSha = deploymentDiffPairs[i].NewDeployCommitSha
-       }
 
-       // get prs by repo project_name
+       // Get pull requests by repo project_name
        clauses := []dal.Clause{
                dal.From(&code.PullRequest{}),
                dal.Join(`left join project_mapping pm on pm.row_id = 
pull_requests.base_repo_id`),
@@ -74,6 +54,7 @@ func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) 
errors.Error {
        }
        defer cursor.Close()
 
+       // Initialize a new data converter
        converter, err := api.NewDataConverter(api.DataConverterArgs{
                RawDataSubTaskArgs: api.RawDataSubTaskArgs{
                        Ctx: taskCtx,
@@ -86,17 +67,21 @@ func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) 
errors.Error {
                InputRowType: reflect.TypeOf(code.PullRequest{}),
                Input:        cursor,
                Convert: func(inputRow interface{}) ([]interface{}, 
errors.Error) {
+                       // Process each pull request
                        pr := inputRow.(*code.PullRequest)
+
+                       // Get the first commit for the PR
                        firstCommit, err := getFirstCommit(pr.Id, db)
                        if err != nil {
                                return nil, err
                        }
+
+                       // Initialize a new ProjectPrMetric
                        projectPrMetric := &crossdomain.ProjectPrMetric{}
                        projectPrMetric.Id = pr.Id
                        projectPrMetric.ProjectName = data.Options.ProjectName
-                       if err != nil {
-                               return nil, err
-                       }
+
+                       // Calculate PR coding time
                        if firstCommit != nil {
                                codingTime := 
int64(pr.CreatedDate.Sub(firstCommit.AuthoredDate).Seconds())
                                if codingTime/60 == 0 && codingTime%60 > 0 {
@@ -107,21 +92,28 @@ func CalculateChangeLeadTime(taskCtx 
plugin.SubTaskContext) errors.Error {
                                projectPrMetric.PrCodingTime = 
processNegativeValue(codingTime)
                                projectPrMetric.FirstCommitSha = firstCommit.Sha
                        }
+
+                       // Get the first review for the PR
                        firstReview, err := getFirstReview(pr.Id, pr.AuthorId, 
db)
                        if err != nil {
                                return nil, err
                        }
-                       // clauses filter by merged_date IS NOT NULL, so 
MergedDate must be not nil.
+
+                       // Calculate PR pickup time and PR review time
                        prDuring := 
processNegativeValue(int64(pr.MergedDate.Sub(pr.CreatedDate).Minutes()))
                        if firstReview != nil {
                                projectPrMetric.PrPickupTime = 
processNegativeValue(int64(firstReview.CreatedDate.Sub(pr.CreatedDate).Minutes()))
                                projectPrMetric.PrReviewTime = 
processNegativeValue(int64(pr.MergedDate.Sub(firstReview.CreatedDate).Minutes()))
                                projectPrMetric.FirstReviewId = firstReview.Id
                        }
+
+                       // Get the deployment for the PR
                        deployment, err := getDeployment(pr.MergeCommitSha, 
pr.BaseRepoId, deploymentDiffPairs, db)
                        if err != nil {
                                return nil, err
                        }
+
+                       // Calculate PR deploy time
                        if deployment != nil && deployment.TaskFinishedDate != 
nil {
                                timespan := 
deployment.TaskFinishedDate.Sub(*pr.MergedDate)
                                projectPrMetric.PrDeployTime = 
processNegativeValue(int64(timespan.Minutes()))
@@ -129,6 +121,8 @@ func CalculateChangeLeadTime(taskCtx plugin.SubTaskContext) 
errors.Error {
                        } else {
                                logger.Debug("deploy time of pr %v is nil\n", 
pr.PullRequestKey)
                        }
+
+                       // Calculate PR cycle time
                        projectPrMetric.PrCycleTime = nil
                        var result int64
                        if projectPrMetric.PrCodingTime != nil {
@@ -143,74 +137,144 @@ func CalculateChangeLeadTime(taskCtx 
plugin.SubTaskContext) errors.Error {
                        if result > 0 {
                                projectPrMetric.PrCycleTime = &result
                        }
+
+                       // Return the projectPrMetric
                        return []interface{}{projectPrMetric}, nil
                },
        })
        if err != nil {
                return err
        }
-
+       // Execute the data converter
        return converter.Execute()
 }
 
+// buildDeploymentPairs populates the OldDeployCommitSha field of each 
deploymentPair in the given slice.
+func buildDeploymentPairs(db dal.Dal, data *DoraTaskData) ([]deploymentPair, 
errors.Error) {
+       // Construct a list of tuple[task, oldPipelineCommitSha, 
newPipelineCommitSha, taskFinishedDate]
+       deploymentClause := []dal.Clause{
+               dal.Select(`ct.id as task_id, cpc.commit_sha as 
new_deploy_commit_sha,
+                       ct.finished_date as task_finished_date, cpc.repo_id as 
repo_id`),
+               dal.From(`cicd_tasks ct`),
+               dal.Join(`left join cicd_pipeline_commits cpc on ct.pipeline_id 
= cpc.pipeline_id`),
+               dal.Join(`left join project_mapping pm on pm.row_id = 
ct.cicd_scope_id`),
+               dal.Where(`ct.environment = ? and ct.type = ? and ct.result = ? 
and pm.project_name = ? and pm.table = ?`,
+                       devops.PRODUCTION, devops.DEPLOYMENT, devops.SUCCESS, 
data.Options.ProjectName, "cicd_scopes"),
+               dal.Orderby(`cpc.repo_id, ct.started_date`),
+       }
+
+       // Initialize deploymentDiffPairs without oldPipelineCommitSha
+       deploymentDiffPairs := make([]deploymentPair, 0)
+       err := db.All(&deploymentDiffPairs, deploymentClause...)
+       if err != nil {
+               return nil, err
+       }
+
+       oldDeployCommitSha := ""
+       lastRepoId := ""
+       for i := 0; i < len(deploymentDiffPairs); i++ {
+               // If two deployments belong to different repos, skip
+               if lastRepoId == deploymentDiffPairs[i].RepoId {
+                       deploymentDiffPairs[i].OldDeployCommitSha = 
oldDeployCommitSha
+               } else {
+                       lastRepoId = deploymentDiffPairs[i].RepoId
+               }
+               oldDeployCommitSha = deploymentDiffPairs[i].NewDeployCommitSha
+       }
+       return deploymentDiffPairs, nil
+}
+
+// getFirstCommit takes a PR ID and a database connection as input, and 
returns the first commit of the PR.
 func getFirstCommit(prId string, db dal.Dal) (*code.Commit, errors.Error) {
+       // Initialize a commit object
        commit := &code.Commit{}
+       // Define the SQL clauses for the database query
        commitClauses := []dal.Clause{
-               dal.From(&code.Commit{}),
-               dal.Join("left join pull_request_commits on commits.sha = 
pull_request_commits.commit_sha"),
-               dal.Where("pull_request_commits.pull_request_id = ?", prId),
-               dal.Orderby("commits.authored_date ASC"),
+               dal.From(&code.Commit{}), // Select from the "commits" table
+               dal.Join("left join pull_request_commits on commits.sha = 
pull_request_commits.commit_sha"), // Join with the "pull_request_commits" table
+               dal.Where("pull_request_commits.pull_request_id = ?", prId),    
                             // Filter by the PR ID
+               dal.Orderby("commits.authored_date ASC"),                       
                             // Order by the authored date of the commits 
(ascending)
        }
+
+       // Execute the query and retrieve the first commit
        err := db.First(commit, commitClauses...)
+
+       // If the error indicates that no commit was found, return nil and no 
error
        if db.IsErrorNotFound(err) {
                return nil, nil
        }
+
+       // If any other error occurred, return nil and the error
        if err != nil {
                return nil, err
        }
+
+       // If there were no errors, return the first commit and no error
        return commit, nil
 }
 
+// getFirstReview takes a PR ID, PR creator ID, and a database connection as 
input, and returns the first review comment of the PR.
 func getFirstReview(prId string, prCreator string, db dal.Dal) 
(*code.PullRequestComment, errors.Error) {
+       // Initialize a review comment object
        review := &code.PullRequestComment{}
+       // Define the SQL clauses for the database query
        commentClauses := []dal.Clause{
-               dal.From(&code.PullRequestComment{}),
-               dal.Where("pull_request_id = ? and account_id != ?", prId, 
prCreator),
-               dal.Orderby("created_date ASC"),
+               dal.From(&code.PullRequestComment{}),                           
       // Select from the "pull_request_comments" table
+               dal.Where("pull_request_id = ? and account_id != ?", prId, 
prCreator), // Filter by the PR ID and exclude comments from the PR creator
+               dal.Orderby("created_date ASC"),                                
       // Order by the created date of the review comments (ascending)
        }
+
+       // Execute the query and retrieve the first review comment
        err := db.First(review, commentClauses...)
+
+       // If the error indicates that no review comment was found, return nil 
and no error
        if db.IsErrorNotFound(err) {
                return nil, nil
        }
+
+       // If any other error occurred, return nil and the error
        if err != nil {
                return nil, err
        }
+
+       // If there were no errors, return the first review comment and no error
        return review, nil
 }
 
+// getDeployment takes a merge commit SHA, a repository ID, a list of 
deployment pairs, and a database connection as input.
+// It returns the deployment pair related to the merge commit, or nil if not 
found.
 func getDeployment(mergeSha string, repoId string, deploymentPairList 
[]deploymentPair, db dal.Dal) (*deploymentPair, errors.Error) {
-       // ignore environment at this point because detecting it by name is 
obviously not engouh
-       // take 
https://github.com/apache/incubator-devlake/actions/workflows/build.yml for 
example
-       // one can not distingush testing/production by looking at the job name 
solely.
+       // Ignore environment detection based on job name since it's not enough 
to distinguish between testing/production environments.
        commitDiff := &code.CommitsDiff{}
-       // find if tuple[merge_sha, new_commit_sha, old_commit_sha] exist in 
commits_diffs, if yes, return pair.FinishedDate
+       // Iterate through the deploymentPairList to find if a tuple 
[merge_sha, new_commit_sha, old_commit_sha] exists in commits_diffs.
+       // If found, return the corresponding deployment pair.
        for _, pair := range deploymentPairList {
+               // Continue to the next iteration if the repoId does not match 
the current deployment pair's RepoId.
                if repoId != pair.RepoId {
                        continue
                }
+
+               // Query the database to find the commit diff with the given 
merge SHA, new commit SHA, and old commit SHA.
                err := db.First(commitDiff, dal.Where(`commit_sha = ? and 
new_commit_sha = ? and old_commit_sha = ?`,
                        mergeSha, pair.NewDeployCommitSha, 
pair.OldDeployCommitSha))
+
+               // If no error occurred, return the current deployment pair.
                if err == nil {
                        return &pair, nil
                }
+
+               // If the error indicates that no commit diff was found, 
continue to the next iteration.
                if db.IsErrorNotFound(err) {
                        continue
                }
+
+               // If any other error occurred, return nil and the error.
                if err != nil {
                        return nil, err
                }
-
        }
+
+       // If no matching deployment pair was found, return nil and no error.
        return nil, nil
 }
 
@@ -222,6 +286,7 @@ func processNegativeValue(v int64) *int64 {
        }
 }
 
+// CalculateChangeLeadTimeMeta contains metadata for the 
CalculateChangeLeadTime subtask.
 var CalculateChangeLeadTimeMeta = plugin.SubTaskMeta{
        Name:             "calculateChangeLeadTime",
        EntryPoint:       CalculateChangeLeadTime,
@@ -230,6 +295,7 @@ var CalculateChangeLeadTimeMeta = plugin.SubTaskMeta{
        DomainTypes:      []string{plugin.DOMAIN_TYPE_CICD, 
plugin.DOMAIN_TYPE_CODE},
 }
 
+// deploymentPair is a struct representing a deployment pair with fields for 
task ID, repo ID, new and old commit SHAs, and task finished date.
 type deploymentPair struct {
        TaskId             string
        RepoId             string
diff --git a/backend/plugins/dora/tasks/change_lead_time_calculator_test.go 
b/backend/plugins/dora/tasks/change_lead_time_calculator_test.go
new file mode 100644
index 000000000..f7931165c
--- /dev/null
+++ b/backend/plugins/dora/tasks/change_lead_time_calculator_test.go
@@ -0,0 +1,60 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You 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 tasks
+
+import (
+       mockdal "github.com/apache/incubator-devlake/mocks/core/dal"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/mock"
+       "reflect"
+       "testing"
+)
+
+func TestBuildDeploymentPairs(t *testing.T) {
+       db := new(mockdal.Dal)
+       data := &DoraTaskData{
+               Options: &DoraOptions{
+                       ProjectName: "project1",
+               },
+       }
+       deploymentDiffPairs := []deploymentPair{
+               {TaskId: "1", RepoId: "repo1", NewDeployCommitSha: "sha1", 
OldDeployCommitSha: ""},
+               {TaskId: "2", RepoId: "repo1", NewDeployCommitSha: "sha2", 
OldDeployCommitSha: "sha1"},
+               {TaskId: "3", RepoId: "repo2", NewDeployCommitSha: "sha3", 
OldDeployCommitSha: ""},
+               {TaskId: "4", RepoId: "repo2", NewDeployCommitSha: "sha4", 
OldDeployCommitSha: "sha3"},
+       }
+
+       expectedPairs := []deploymentPair{
+               {TaskId: "1", RepoId: "repo1", NewDeployCommitSha: "sha1", 
OldDeployCommitSha: ""},
+               {TaskId: "2", RepoId: "repo1", NewDeployCommitSha: "sha2", 
OldDeployCommitSha: "sha1"},
+               {TaskId: "3", RepoId: "repo2", NewDeployCommitSha: "sha3", 
OldDeployCommitSha: ""},
+               {TaskId: "4", RepoId: "repo2", NewDeployCommitSha: "sha4", 
OldDeployCommitSha: "sha3"},
+       }
+       db.On("All", mock.Anything, mock.Anything).Run(func(args 
mock.Arguments) {
+               dst := args.Get(0).(*[]deploymentPair)
+               *dst = deploymentDiffPairs
+       }).Return(nil).Once()
+       res, err := buildDeploymentPairs(db, data)
+
+       assert.Nil(t, err)
+       assert.True(t, reflect.DeepEqual(res, expectedPairs))
+
+       if !reflect.DeepEqual(deploymentDiffPairs, expectedPairs) {
+               t.Errorf("buildDeploymentPairs() = %v, want %v", 
deploymentDiffPairs, expectedPairs)
+       }
+}

Reply via email to