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 2a593b92 feat: added MetricPluginBlueprintV200 interface (#3677)
2a593b92 is described below

commit 2a593b92a8f6d95b911a1ca810ea944929312a27
Author: Klesh Wong <[email protected]>
AuthorDate: Wed Nov 9 18:22:06 2022 +0800

    feat: added MetricPluginBlueprintV200 interface (#3677)
    
    * feat: added MetricPluginBlueprintV200 interface
    
    * fix: method names conflict / add projectName
    
    * fix: move `Make` to the beginning
    
    * feat: added unit test for blueprint_makeplan_v200
    
    * docs: update comments for blueprint_makeplan_v200_test
    
    * fix: remove useless interface
    
    * feat: save project/scopes to project_mapping table
    
    * refactor: rename GeneratePlanJson to MakePlan
    
      GeneratePlanJson was a pure function. but in blueprint protocol
      v2.0.0, this is no longer ture, because it requires project_mapping
      to be updated before blueprint being saved. Better to rename it to
      avoid misconception
---
 plugins/core/plugin_blueprint.go         |  70 +++++++++++++-----
 services/blueprint.go                    | 107 +++++++++------------------
 services/blueprint_makeplan_v100.go      |  78 ++++++++++++++++++++
 services/blueprint_makeplan_v200.go      | 122 +++++++++++++++++++++++++++++++
 services/blueprint_makeplan_v200_test.go |  97 ++++++++++++++++++++++++
 services/blueprint_test.go               |  18 ++---
 6 files changed, 393 insertions(+), 99 deletions(-)

diff --git a/plugins/core/plugin_blueprint.go b/plugins/core/plugin_blueprint.go
index 38daebf9..cb1d8f7e 100644
--- a/plugins/core/plugin_blueprint.go
+++ b/plugins/core/plugin_blueprint.go
@@ -68,9 +68,10 @@ PluginBlueprintV200 for project support
 step 1: blueprint.settings like
        {
                "version": "2.0.0",
-               "scopes": [
+               "connections": [
                        {
                                "plugin": "github",
+                               "connectionId": 123,
                                "scopes": [
                                        { "id": null, "name": 
"apache/incubator-devlake" }
                                ]
@@ -101,29 +102,64 @@ step 3: framework should maintain the project_mapping 
table based on the []Scope
        ]
 */
 
-// Scope represents the top level entity for a data source, i.e. github repo, 
gitlab project, jira board.
-// They turn into repo, board in Domain Layer.
-// In Apache Devlake, a Project is essentially a set of these top level 
entities, for the framework to
-// maintain these relationships dynamically and automatically, all Domain 
Layer Top Level Entities should
-// implement this interface
+// Scope represents the top level entity for a data source, i.e. github repo,
+// gitlab project, jira board. They turn into repo, board in Domain Layer. In
+// Apache Devlake, a Project is essentially a set of these top level entities,
+// for the framework to maintain these relationships dynamically and
+// automatically, all Domain Layer Top Level Entities should implement this
+// interface
 type Scope interface {
        ScopeId() string
        ScopeName() string
        TableName() string
 }
 
-// PluginBlueprintV200 extends the V100 to provide support for Project to 
support complex metrics
-// like DORA
-type PluginBlueprintV200 interface {
-       MakePipelinePlan(scopes []*BlueprintScopeV200) (PipelinePlan, []Scope, 
errors.Error)
+// DataSourcePluginBlueprintV200 extends the V100 to provide support for
+// Project, so that complex metrics like DORA can be implemented based on a set
+// of Data Scopes
+type DataSourcePluginBlueprintV200 interface {
+       MakeDataSourcePipelinePlanV200(connectionId uint64, scopes 
[]*BlueprintScopeV200) (PipelinePlan, []Scope, errors.Error)
 }
 
-// BlueprintScopeV200 contains the Plugin name and related ScopeIds, 
connectionId and transformationRuleId should be
-// deduced by the ScopeId
+// BlueprintConnectionV200 contains the pluginName/connectionId  and related 
Scopes,
+type BlueprintConnectionV200 struct {
+       Plugin       string                `json:"plugin" validate:"required"`
+       ConnectionId uint64                `json:"connectionId" 
validate:"required"`
+       Scopes       []*BlueprintScopeV200 `json:"scopes" validate:"required"`
+}
+
+// BlueprintScopeV200 contains the `id` and `name` for a specific scope
+// transformationRuleId should be deduced by the ScopeId
 type BlueprintScopeV200 struct {
-       Plugin string `json:"plugin" validate:"required"`
-       Scopes []struct {
-               Id   string
-               Name string
-       }
+       Id   string `json:"id"`
+       Name string `json:"name"`
+}
+
+// MetricPluginBlueprintV200 is similar to the DataSourcePluginBlueprintV200
+// but for Metric Plugin, take dora as an example, it doens't have any scope,
+// nor does it produce any, however, it does require other plugin to be
+// executed beforehand, like calcuating refdiff before it can connect PR to the
+// right Deployment keep in mind it would be called IFF the plugin was enabled
+// for the project.
+type MetricPluginBlueprintV200 interface {
+       MakeMetricPluginPipelinePlanV200(projectName string, options 
json.RawMessage) (PipelinePlan, errors.Error)
+}
+
+// CompositeDataSourcePluginBlueprintV200 is for unit test
+type CompositeDataSourcePluginBlueprintV200 interface {
+       PluginMeta
+       DataSourcePluginBlueprintV200
+}
+
+// CompositeMetricPluginBlueprintV200 is for unit test
+type CompositeMetricPluginBlueprintV200 interface {
+       PluginMeta
+       MetricPluginBlueprintV200
+}
+
+// CompositeMetricPluginBlueprintV200 is for unit test
+type CompositePluginBlueprintV200 interface {
+       PluginMeta
+       DataSourcePluginBlueprintV200
+       MetricPluginBlueprintV200
 }
diff --git a/services/blueprint.go b/services/blueprint.go
index edb4a0e3..5c59b6fc 100644
--- a/services/blueprint.go
+++ b/services/blueprint.go
@@ -47,7 +47,7 @@ var (
 
 // CreateBlueprint accepts a Blueprint instance and insert it to database
 func CreateBlueprint(blueprint *models.Blueprint) errors.Error {
-       err := validateBlueprint(blueprint)
+       err := validateBlueprintAndMakePlan(blueprint)
        if err != nil {
                return err
        }
@@ -102,7 +102,7 @@ func GetBlueprint(blueprintId uint64) (*models.Blueprint, 
errors.Error) {
        return blueprint, nil
 }
 
-func validateBlueprint(blueprint *models.Blueprint) errors.Error {
+func validateBlueprintAndMakePlan(blueprint *models.Blueprint) errors.Error {
        // validation
        err := vld.Struct(blueprint)
        if err != nil {
@@ -130,10 +130,14 @@ func validateBlueprint(blueprint *models.Blueprint) 
errors.Error {
                        return errors.Default.New("empty plan")
                }
        } else if blueprint.Mode == models.BLUEPRINT_MODE_NORMAL {
-               blueprint.Plan, err = GeneratePlanJson(blueprint.Settings)
+               plan, err := MakePlanForBlueprint(blueprint)
                if err != nil {
                        return errors.Default.Wrap(err, "invalid plan")
                }
+               blueprint.Plan, err = errors.Convert01(json.Marshal(plan))
+               if err != nil {
+                       return errors.Default.Wrap(err, "failed to markshal 
plan")
+               }
        }
 
        return nil
@@ -157,7 +161,7 @@ func PatchBlueprint(id uint64, body map[string]interface{}) 
(*models.Blueprint,
                return nil, errors.Default.New("mode is not updatable")
        }
        // validation
-       err = validateBlueprint(blueprint)
+       err = validateBlueprintAndMakePlan(blueprint)
        if err != nil {
                return nil, errors.BadInput.WrapRaw(err)
        }
@@ -246,15 +250,15 @@ func createPipelineByBlueprint(blueprintId uint64, name 
string, plan core.Pipeli
        return pipeline, nil
 }
 
-// GeneratePlanJson generates pipeline plan by version
-func GeneratePlanJson(settings json.RawMessage) (json.RawMessage, 
errors.Error) {
+// MakePlanForBlueprint generates pipeline plan by version
+func MakePlanForBlueprint(blueprint *models.Blueprint) (core.PipelinePlan, 
errors.Error) {
        bpSettings := new(models.BlueprintSettings)
-       err := errors.Convert(json.Unmarshal(settings, bpSettings))
+       err := errors.Convert(json.Unmarshal(blueprint.Settings, bpSettings))
 
        if err != nil {
-               return nil, errors.Default.Wrap(err, fmt.Sprintf("settings:%s", 
string(settings)))
+               return nil, errors.Default.Wrap(err, fmt.Sprintf("settings:%s", 
string(blueprint.Settings)))
        }
-       var plan interface{}
+       var plan core.PipelinePlan
        switch bpSettings.Version {
        case "1.0.0":
                plan, err = GeneratePlanJsonV100(bpSettings)
@@ -264,87 +268,33 @@ func GeneratePlanJson(settings json.RawMessage) 
(json.RawMessage, errors.Error)
        if err != nil {
                return nil, err
        }
-       return errors.Convert01(json.Marshal(plan))
+       return WrapPipelinePlans(bpSettings.BeforePlan, plan, 
bpSettings.AfterPlan)
 }
 
-// GeneratePlanJsonV100 generates pipeline plan according v1.0.0 definition
-func GeneratePlanJsonV100(settings *models.BlueprintSettings) 
(core.PipelinePlan, errors.Error) {
-       connections := make([]*core.BlueprintConnectionV100, 0)
-       err := errors.Convert(json.Unmarshal(settings.Connections, 
&connections))
-       if err != nil {
-               return nil, err
-       }
-       hasDoraEnrich := false
-       doraRules := make(map[string]interface{})
-       plans := make([]core.PipelinePlan, len(connections))
-       for i, connection := range connections {
-               if len(connection.Scope) == 0 {
-                       return nil, 
errors.Default.New(fmt.Sprintf("connections[%d].scope is empty", i))
-               }
-               plugin, err := core.GetPlugin(connection.Plugin)
-               if err != nil {
-                       return nil, err
-               }
-               if pluginBp, ok := plugin.(core.PluginBlueprintV100); ok {
-                       plans[i], err = 
pluginBp.MakePipelinePlan(connection.ConnectionId, connection.Scope)
-                       if err != nil {
-                               return nil, err
-                       }
-               } else {
-                       return nil, errors.Default.New(fmt.Sprintf("plugin %s 
does not support blueprint protocol version 1.0.0", connection.Plugin))
-               }
-               for _, stage := range plans[i] {
-                       for _, task := range stage {
-                               if task.Plugin == "dora" {
-                                       hasDoraEnrich = true
-                                       for k, v := range task.Options {
-                                               doraRules[k] = v
-                                       }
-                               }
-                       }
-               }
-       }
-       mergedPipelinePlan := MergePipelinePlans(plans...)
-       if hasDoraEnrich {
-               plan := core.PipelineStage{
-                       &core.PipelineTask{
-                               Plugin:   "dora",
-                               Subtasks: []string{"calculateChangeLeadTime", 
"ConnectIssueDeploy"},
-                               Options:  doraRules,
-                       },
-               }
-               mergedPipelinePlan = append(mergedPipelinePlan, plan)
-       }
-       return FormatPipelinePlans(settings.BeforePlan, mergedPipelinePlan, 
settings.AfterPlan)
-}
+// WrapPipelinePlans merges multiple pipelines and append before and after 
pipeline
+func WrapPipelinePlans(beforePlanJson json.RawMessage, mainPlan 
core.PipelinePlan, afterPlanJson json.RawMessage) (core.PipelinePlan, 
errors.Error) {
+       beforePipelinePlan := core.PipelinePlan{}
+       afterPipelinePlan := core.PipelinePlan{}
 
-// FormatPipelinePlans merges multiple pipelines and append before and after 
pipeline
-func FormatPipelinePlans(beforePlanJson json.RawMessage, mainPlan 
core.PipelinePlan, afterPlanJson json.RawMessage) (core.PipelinePlan, 
errors.Error) {
-       newPipelinePlan := core.PipelinePlan{}
        if beforePlanJson != nil {
-               beforePipelinePlan := core.PipelinePlan{}
                err := errors.Convert(json.Unmarshal(beforePlanJson, 
&beforePipelinePlan))
                if err != nil {
                        return nil, err
                }
-               newPipelinePlan = append(newPipelinePlan, beforePipelinePlan...)
        }
-
-       newPipelinePlan = append(newPipelinePlan, mainPlan...)
-
        if afterPlanJson != nil {
-               afterPipelinePlan := core.PipelinePlan{}
                err := errors.Convert(json.Unmarshal(afterPlanJson, 
&afterPipelinePlan))
                if err != nil {
                        return nil, err
                }
-               newPipelinePlan = append(newPipelinePlan, afterPipelinePlan...)
        }
-       return newPipelinePlan, nil
+
+       return SequencializePipelinePlans(beforePipelinePlan, mainPlan, 
afterPipelinePlan), nil
 }
 
-// MergePipelinePlans merges multiple pipelines into one unified pipeline
-func MergePipelinePlans(plans ...core.PipelinePlan) core.PipelinePlan {
+// ParallelizePipelinePlans merges multiple pipelines into one unified plan
+// by assuming they can be executed in parallel
+func ParallelizePipelinePlans(plans ...core.PipelinePlan) core.PipelinePlan {
        merged := make(core.PipelinePlan, 0)
        // iterate all pipelineTasks and try to merge them into `merged`
        for _, plan := range plans {
@@ -360,6 +310,17 @@ func MergePipelinePlans(plans ...core.PipelinePlan) 
core.PipelinePlan {
        return merged
 }
 
+// SequencializePipelinePlans merges multiple pipelines into one unified plan
+// by assuming they must be executed in sequencial order
+func SequencializePipelinePlans(plans ...core.PipelinePlan) core.PipelinePlan {
+       merged := make(core.PipelinePlan, 0)
+       // iterate all pipelineTasks and try to merge them into `merged`
+       for _, plan := range plans {
+               merged = append(merged, plan...)
+       }
+       return merged
+}
+
 // TriggerBlueprint triggers blueprint immediately
 func TriggerBlueprint(id uint64) (*models.Pipeline, errors.Error) {
        // load record from db
diff --git a/services/blueprint_makeplan_v100.go 
b/services/blueprint_makeplan_v100.go
new file mode 100644
index 00000000..f3bd47d6
--- /dev/null
+++ b/services/blueprint_makeplan_v100.go
@@ -0,0 +1,78 @@
+/*
+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 services
+
+import (
+       "encoding/json"
+       "fmt"
+
+       "github.com/apache/incubator-devlake/errors"
+       "github.com/apache/incubator-devlake/models"
+       "github.com/apache/incubator-devlake/plugins/core"
+)
+
+// GeneratePlanJsonV100 generates pipeline plan according v1.0.0 definition
+func GeneratePlanJsonV100(settings *models.BlueprintSettings) 
(core.PipelinePlan, errors.Error) {
+       connections := make([]*core.BlueprintConnectionV100, 0)
+       err := errors.Convert(json.Unmarshal(settings.Connections, 
&connections))
+       if err != nil {
+               return nil, err
+       }
+       hasDoraEnrich := false
+       doraRules := make(map[string]interface{})
+       plans := make([]core.PipelinePlan, len(connections))
+       for i, connection := range connections {
+               if len(connection.Scope) == 0 {
+                       return nil, 
errors.Default.New(fmt.Sprintf("connections[%d].scope is empty", i))
+               }
+               plugin, err := core.GetPlugin(connection.Plugin)
+               if err != nil {
+                       return nil, err
+               }
+               if pluginBp, ok := plugin.(core.PluginBlueprintV100); ok {
+                       plans[i], err = 
pluginBp.MakePipelinePlan(connection.ConnectionId, connection.Scope)
+                       if err != nil {
+                               return nil, err
+                       }
+               } else {
+                       return nil, errors.Default.New(fmt.Sprintf("plugin %s 
does not support blueprint protocol version 1.0.0", connection.Plugin))
+               }
+               for _, stage := range plans[i] {
+                       for _, task := range stage {
+                               if task.Plugin == "dora" {
+                                       hasDoraEnrich = true
+                                       for k, v := range task.Options {
+                                               doraRules[k] = v
+                                       }
+                               }
+                       }
+               }
+       }
+       mergedPipelinePlan := ParallelizePipelinePlans(plans...)
+       if hasDoraEnrich {
+               plan := core.PipelineStage{
+                       &core.PipelineTask{
+                               Plugin:   "dora",
+                               Subtasks: []string{"calculateChangeLeadTime", 
"ConnectIssueDeploy"},
+                               Options:  doraRules,
+                       },
+               }
+               mergedPipelinePlan = append(mergedPipelinePlan, plan)
+       }
+       return mergedPipelinePlan, nil
+}
diff --git a/services/blueprint_makeplan_v200.go 
b/services/blueprint_makeplan_v200.go
new file mode 100644
index 00000000..c4835579
--- /dev/null
+++ b/services/blueprint_makeplan_v200.go
@@ -0,0 +1,122 @@
+/*
+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 services
+
+import (
+       "encoding/json"
+       "fmt"
+
+       "github.com/apache/incubator-devlake/errors"
+       "github.com/apache/incubator-devlake/models"
+       "github.com/apache/incubator-devlake/models/domainlayer/crossdomain"
+       "github.com/apache/incubator-devlake/plugins/core"
+)
+
+// GeneratePlanJsonV200 generates pipeline plan according v2.0.0 definition
+func GeneratePlanJsonV200(
+       projectName string,
+       sources *models.BlueprintSettings,
+       metrics map[string]json.RawMessage,
+) (core.PipelinePlan, errors.Error) {
+       // generate plan and collect scopes
+       plan, scopes, err := genPlanJsonV200(projectName, sources, metrics)
+       if err != nil {
+               return nil, err
+       }
+       // refresh project_mapping table to reflect project/scopes relationship
+       if len(scopes) > 0 {
+               e := db.Where("project_name = ?", 
projectName).Delete(&crossdomain.ProjectMapping{}).Error
+               if e != nil {
+                       return nil, errors.Convert(err)
+               }
+               e = db.Create(scopes).Error
+               if e != nil {
+                       return nil, errors.Convert(err)
+               }
+       }
+       return plan, err
+}
+
+func genPlanJsonV200(
+       projectName string,
+       sources *models.BlueprintSettings,
+       metrics map[string]json.RawMessage,
+) (core.PipelinePlan, []core.Scope, errors.Error) {
+       connections := make([]*core.BlueprintConnectionV200, 0)
+       err := errors.Convert(json.Unmarshal(sources.Connections, &connections))
+       if err != nil {
+               return nil, nil, err
+       }
+
+       // make plan for data-source plugins fist. generate plan for each
+       // connections, then merge them into one legitimate plan and collect the
+       // scopes produced by the data-source plugins
+       sourcePlans := make([]core.PipelinePlan, len(connections))
+       scopes := make([]core.Scope, 0, len(connections))
+       for i, connection := range connections {
+               if len(connection.Scopes) == 0 {
+                       return nil, nil, 
errors.Default.New(fmt.Sprintf("connections[%d].scope is empty", i))
+               }
+               plugin, err := core.GetPlugin(connection.Plugin)
+               if err != nil {
+                       return nil, nil, err
+               }
+               if pluginBp, ok := plugin.(core.DataSourcePluginBlueprintV200); 
ok {
+                       var pluginScopes []core.Scope
+                       sourcePlans[i], pluginScopes, err = 
pluginBp.MakeDataSourcePipelinePlanV200(
+                               connection.ConnectionId,
+                               connection.Scopes,
+                       )
+                       if err != nil {
+                               return nil, nil, err
+                       }
+                       // collect scopes for the project. a github repository 
may produces
+                       // 2 scopes, 1 repo and 1 board
+                       scopes = append(scopes, pluginScopes...)
+               } else {
+                       return nil, nil, errors.Default.New(
+                               fmt.Sprintf("plugin %s does not support 
DataSourcePluginBlueprintV200", connection.Plugin),
+                       )
+               }
+       }
+       // make plans for metric plugins
+       metricPlans := make([]core.PipelinePlan, len(metrics))
+       i := 0
+       for metricPluginName, metricPluginOptJson := range metrics {
+               plugin, err := core.GetPlugin(metricPluginName)
+               if err != nil {
+                       return nil, nil, err
+               }
+               if pluginBp, ok := plugin.(core.MetricPluginBlueprintV200); ok {
+                       metricPlans[i], err = 
pluginBp.MakeMetricPluginPipelinePlanV200(projectName, metricPluginOptJson)
+                       if err != nil {
+                               return nil, nil, err
+                       }
+                       i += 1
+               } else {
+                       return nil, nil, errors.Default.New(
+                               fmt.Sprintf("plugin %s does not support 
MetricPluginBlueprintV200", metricPluginName),
+                       )
+               }
+       }
+       plan := SequencializePipelinePlans(
+               ParallelizePipelinePlans(sourcePlans...),
+               ParallelizePipelinePlans(metricPlans...),
+       )
+       return plan, scopes, err
+}
diff --git a/services/blueprint_makeplan_v200_test.go 
b/services/blueprint_makeplan_v200_test.go
new file mode 100644
index 00000000..7cee6bf8
--- /dev/null
+++ b/services/blueprint_makeplan_v200_test.go
@@ -0,0 +1,97 @@
+/*
+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 services
+
+import (
+       "encoding/json"
+       "testing"
+
+       "github.com/apache/incubator-devlake/mocks"
+       "github.com/apache/incubator-devlake/models"
+       "github.com/apache/incubator-devlake/models/domainlayer"
+       "github.com/apache/incubator-devlake/models/domainlayer/code"
+       "github.com/apache/incubator-devlake/models/domainlayer/ticket"
+       "github.com/apache/incubator-devlake/plugins/core"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestMakePlanV200(t *testing.T) {
+       const projectName = "TestMakePlanV200-project"
+       githubName := "TestMakePlanV200-github" // mimic github
+       // mock github plugin as a data source plugin
+       githubConnId := uint64(1)
+       githubScopes := []*core.BlueprintScopeV200{
+               {Id: "", Name: "apache/incubator-devlake"},
+               {Id: "", Name: "apache/incubator-devlake-website"},
+       }
+       githubOutputPlan := core.PipelinePlan{
+               {
+                       {Plugin: githubName, Options: 
map[string]interface{}{"name": "apache/incubator-devlake"}},
+                       {Plugin: "gitextractor", Options: 
map[string]interface{}{"url": "http://gihub.com/apache/incubator-devlake.git"}},
+               },
+               {
+                       {Plugin: githubName, Options: 
map[string]interface{}{"name": "apache/incubator-devlake-website"}},
+                       {Plugin: "gitextractor", Options: 
map[string]interface{}{"url": 
"http://gihub.com/apache/incubator-devlake-website.git"}},
+               },
+       }
+       githubOutputScopes := []core.Scope{
+               &code.Repo{DomainEntity: domainlayer.DomainEntity{Id: 
"github:GithubRepo:1:123"}, Name: "apache/incubator-devlake"},
+               &ticket.Board{DomainEntity: domainlayer.DomainEntity{Id: 
"github:GithubRepo:1:123"}, Name: "apache/incubator-devlake"},
+       }
+       github := new(mocks.CompositeDataSourcePluginBlueprintV200)
+       github.On("MakeDataSourcePipelinePlanV200", githubConnId, 
githubScopes).Return(githubOutputPlan, githubOutputScopes, nil)
+
+       // mock dora plugin as a metric plugin
+       doraName := "TestMakePlanV200-dora"
+       doraOutputPlan := core.PipelinePlan{
+               {
+                       {Plugin: "refdiff", Subtasks: 
[]string{"calculateDeploymentDiffs"}, Options: 
map[string]interface{}{"projectName": projectName}},
+                       {Plugin: doraName},
+               },
+       }
+       dora := new(mocks.CompositeMetricPluginBlueprintV200)
+       dora.On("MakeMetricPluginPipelinePlanV200", projectName, 
json.RawMessage(nil)).Return(doraOutputPlan, nil)
+
+       // expectation, establish expectation before any code being launch to 
avoid unwanted modification
+       expectedPlan := make(core.PipelinePlan, 0)
+       expectedPlan = append(expectedPlan, githubOutputPlan...)
+       expectedPlan = append(expectedPlan, doraOutputPlan...)
+       expectedScopes := append(make([]core.Scope, 0), githubOutputScopes...)
+
+       // plugin registration
+       core.RegisterPlugin(githubName, github)
+       core.RegisterPlugin(doraName, dora)
+
+       // put them together and call GeneratePlanJsonV200
+       connections, _ := json.Marshal([]*core.BlueprintConnectionV200{
+               {Plugin: githubName, ConnectionId: githubConnId, Scopes: 
githubScopes},
+       })
+       sources := &models.BlueprintSettings{
+               Version:     "2.0.0",
+               Connections: connections,
+       }
+       metrics := map[string]json.RawMessage{
+               doraName: nil,
+       }
+
+       plan, scopes, err := genPlanJsonV200(projectName, sources, metrics)
+       assert.Nil(t, err)
+
+       assert.Equal(t, expectedPlan, plan)
+       assert.Equal(t, expectedScopes, scopes)
+}
diff --git a/services/blueprint_test.go b/services/blueprint_test.go
index 8c7862ad..b06be8d7 100644
--- a/services/blueprint_test.go
+++ b/services/blueprint_test.go
@@ -25,7 +25,7 @@ import (
        "github.com/stretchr/testify/assert"
 )
 
-func TestMergePipelineTasks(t *testing.T) {
+func TestParallelizePipelineTasks(t *testing.T) {
        plan1 := core.PipelinePlan{
                {
                        {Plugin: "github"},
@@ -55,8 +55,8 @@ func TestMergePipelineTasks(t *testing.T) {
                },
        }
 
-       assert.Equal(t, plan1, MergePipelinePlans(plan1))
-       assert.Equal(t, plan2, MergePipelinePlans(plan2))
+       assert.Equal(t, plan1, ParallelizePipelinePlans(plan1))
+       assert.Equal(t, plan2, ParallelizePipelinePlans(plan2))
        assert.Equal(
                t,
                core.PipelinePlan{
@@ -70,7 +70,7 @@ func TestMergePipelineTasks(t *testing.T) {
                                {Plugin: "gitextractor2"},
                        },
                },
-               MergePipelinePlans(plan1, plan2),
+               ParallelizePipelinePlans(plan1, plan2),
        )
        assert.Equal(
                t,
@@ -90,11 +90,11 @@ func TestMergePipelineTasks(t *testing.T) {
                                {Plugin: "jenkins"},
                        },
                },
-               MergePipelinePlans(plan1, plan2, plan3),
+               ParallelizePipelinePlans(plan1, plan2, plan3),
        )
 }
 
-func TestFormatPipelinePlans(t *testing.T) {
+func TestWrapPipelinePlans(t *testing.T) {
        beforePlan2 := 
json.RawMessage(`[[{"plugin":"github"},{"plugin":"gitlab"}],[{"plugin":"gitextractor1"},{"plugin":"gitextractor2"}]]`)
 
        mainPlan := core.PipelinePlan{
@@ -105,11 +105,11 @@ func TestFormatPipelinePlans(t *testing.T) {
 
        afterPlan2 := 
json.RawMessage(`[[{"plugin":"jenkins"}],[{"plugin":"jenkins"}]]`)
 
-       result1, err1 := FormatPipelinePlans(nil, mainPlan, nil)
+       result1, err1 := WrapPipelinePlans(nil, mainPlan, nil)
        assert.Nil(t, err1)
        assert.Equal(t, mainPlan, result1)
 
-       result2, err2 := FormatPipelinePlans(beforePlan2, mainPlan, afterPlan2)
+       result2, err2 := WrapPipelinePlans(beforePlan2, mainPlan, afterPlan2)
        assert.Nil(t, err2)
        assert.Equal(t, core.PipelinePlan{
                {
@@ -131,7 +131,7 @@ func TestFormatPipelinePlans(t *testing.T) {
                },
        }, result2)
 
-       result3, err3 := FormatPipelinePlans(json.RawMessage("[]"), mainPlan, 
json.RawMessage("[]"))
+       result3, err3 := WrapPipelinePlans(json.RawMessage("[]"), mainPlan, 
json.RawMessage("[]"))
        assert.Nil(t, err3)
        assert.Equal(t, mainPlan, result3)
 }

Reply via email to