From 3e69231643bb48a708e635723ffe8d6a963d583a Mon Sep 17 00:00:00 2001 From: littlejian <17816869670@163.com> Date: Fri, 15 Oct 2021 15:28:05 +0800 Subject: [PATCH] feature: scene set supports parallel in autoTest (#2173) * feature: scene set supports parallel in autoTest * add ListTestPlanV2Step api * update move group * add split op of render * fix: add unit test * add unit test * add unit test for render * add migrations * polish code * fix: update timeout of Test_initCron --- .../20210928-auto-test-plan-step-groupID.sql | 1 + apistructs/sceneset.go | 26 +- apistructs/sceneset_test.go | 44 +++ apistructs/testplan_v2.go | 21 ++ bundle/autotest_plan.go | 29 +- modules/dop/dao/testplan_v2_step.go | 176 +++++++-- modules/dop/endpoints/endpoints.go | 1 + modules/dop/endpoints/testplan_v2.go | 41 +- modules/dop/services/apierrors/errors.go | 2 + .../dop/services/autotest_v2/testplan_v2.go | 114 ++++-- .../services/autotest_v2/testplan_v2_test.go | 82 ++++ modules/dop/services/sceneset/sceneset.go | 28 +- .../components/addScenesSetButton/render.go | 1 + .../components/stages/model.go | 8 +- .../components/stages/render.go | 22 +- .../components/stages/stages.go | 229 ++++++++++- .../components/stages/stages_test.go | 354 ++++++++++++++++++ 17 files changed, 1046 insertions(+), 133 deletions(-) create mode 100644 .erda/migrations/qa/20210928-auto-test-plan-step-groupID.sql create mode 100644 apistructs/sceneset_test.go create mode 100644 modules/dop/services/autotest_v2/testplan_v2_test.go create mode 100644 modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages_test.go diff --git a/.erda/migrations/qa/20210928-auto-test-plan-step-groupID.sql b/.erda/migrations/qa/20210928-auto-test-plan-step-groupID.sql new file mode 100644 index 00000000000..450b61ce252 --- /dev/null +++ b/.erda/migrations/qa/20210928-auto-test-plan-step-groupID.sql @@ -0,0 +1 @@ +ALTER TABLE `dice_autotest_plan_step` ADD `group_id` BIGINT(20) NOT NULL DEFAULT 0 COMMENT 'auto test plan step group'; diff --git a/apistructs/sceneset.go b/apistructs/sceneset.go index d265b60a2b6..4ea95f2db7b 100644 --- a/apistructs/sceneset.go +++ b/apistructs/sceneset.go @@ -15,12 +15,21 @@ package apistructs import ( + "fmt" + "regexp" "strconv" "time" + + "github.com/erda-project/erda/pkg/strutil" ) -const SceneSetsAutotestExecType = "sceneSets" -const SceneAutotestExecType = "scene" +const ( + SceneSetsAutotestExecType = "sceneSets" + SceneAutotestExecType = "scene" + + SceneSetNameMaxLength int = 50 + SceneSetDescMaxLength int = 255 +) type SceneSet struct { ID uint64 `json:"id"` @@ -53,6 +62,19 @@ type SceneSetRequest struct { IdentityInfo } +func (req *SceneSetRequest) Validate() error { + if err := strutil.Validate(req.Name, strutil.MaxRuneCountValidator(SceneSetNameMaxLength)); err != nil { + return err + } + if err := strutil.Validate(req.Description, strutil.MaxRuneCountValidator(SceneSetDescMaxLength)); err != nil { + return err + } + if ok, _ := regexp.MatchString("^[a-zA-Z\u4e00-\u9fa50-9_-]*$", req.Name); !ok { + return fmt.Errorf("the name not match %s", "^[a-zA-Z\u4e00-\u9fa50-9_-]*$") + } + return nil +} + // type SceneSetUpdateRequest struct { // Name string `json:"name"` // Description string `json:"description"` diff --git a/apistructs/sceneset_test.go b/apistructs/sceneset_test.go new file mode 100644 index 00000000000..1f4bdbdab9d --- /dev/null +++ b/apistructs/sceneset_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 apistructs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/rand" +) + +func TestSceneSetRequestValidate(t *testing.T) { + tt := []struct { + req SceneSetRequest + want bool + }{ + {SceneSetRequest{Name: rand.String(50), Description: "1"}, true}, + {SceneSetRequest{Name: rand.String(51), Description: "1"}, false}, + {SceneSetRequest{Name: "1", Description: rand.String(255)}, true}, + {SceneSetRequest{Name: "1", Description: rand.String(256)}, false}, + {SceneSetRequest{Name: "****", Description: "1"}, false}, + {SceneSetRequest{Name: "/", Description: "1"}, false}, + {SceneSetRequest{Name: "_abd1", Description: "1"}, true}, + {SceneSetRequest{Name: "_测试", Description: "1"}, true}, + {SceneSetRequest{Name: "1_测试a", Description: "1"}, true}, + {SceneSetRequest{Name: "a_测试1", Description: "1"}, true}, + } + for _, v := range tt { + assert.Equal(t, v.want, v.req.Validate() == nil) + } + +} diff --git a/apistructs/testplan_v2.go b/apistructs/testplan_v2.go index f4b9f78fa18..76ea7aa907c 100644 --- a/apistructs/testplan_v2.go +++ b/apistructs/testplan_v2.go @@ -171,6 +171,12 @@ type TestPlanV2StepGetResponse struct { Data TestPlanV2Step `json:"data"` } +// TestPlanV2StepListResponse testplan get response +type TestPlanV2StepListResponse struct { + Header + Data []*TestPlanV2Step `json:"data"` +} + // TestPlanV2PagingResponseData testplan query response data type TestPlanV2PagingResponseData struct { Total int `json:"total"` @@ -184,6 +190,7 @@ type TestPlanV2Step struct { SceneSetName string `json:"sceneSetName"` PreID uint64 `json:"preID"` PlanID uint64 `json:"planID"` + GroupID uint64 `json:"groupID"` ID uint64 `json:"id"` } @@ -192,6 +199,7 @@ type TestPlanV2StepAddRequest struct { SceneSetID uint64 `json:"sceneSetID"` PreID uint64 `json:"preID"` TestPlanID uint64 `json:"-"` + GroupID uint64 `json:"groupID"` IdentityInfo } @@ -228,3 +236,16 @@ type TestPlanV2StepUpdateResp struct { Header Data string `json:"data"` } + +// TestPlanV2StepMoveRequest move a step in the test plan request +type TestPlanV2StepMoveRequest struct { + StepID uint64 `json:"stepID"` + LastStepID uint64 `json:"lastStepID"` + PreID uint64 `json:"preID"` + ScenesSetId uint64 `json:"scenesSetId"` + TestPlanID uint64 `json:"-"` + TargetStepID uint64 `json:"targetStepID"` + IsGroup bool `json:"isGroup"` // true: means move with group + + IdentityInfo +} diff --git a/bundle/autotest_plan.go b/bundle/autotest_plan.go index 0df176f3aec..2c43a6ad5e2 100644 --- a/bundle/autotest_plan.go +++ b/bundle/autotest_plan.go @@ -68,7 +68,7 @@ func (b *Bundle) DeleteTestPlansV2Step(req apistructs.TestPlanV2StepDeleteReques } // MoveTestPlansV2Step 移动测试计划步骤 -func (b *Bundle) MoveTestPlansV2Step(req apistructs.TestPlanV2StepUpdateRequest) error { +func (b *Bundle) MoveTestPlansV2Step(req apistructs.TestPlanV2StepMoveRequest) error { host, err := b.urls.DOP() if err != nil { return err @@ -177,7 +177,7 @@ func (b *Bundle) GetTestPlanV2(testPlanID uint64) (*apistructs.TestPlanV2GetResp return &getResp, nil } -// GetTestPlanV2 获取测试计划步骤 +// GetTestPlanV2Step 获取测试计划步骤 func (b *Bundle) GetTestPlanV2Step(stepID uint64) (*apistructs.TestPlanV2Step, error) { host, err := b.urls.DOP() if err != nil { @@ -199,7 +199,30 @@ func (b *Bundle) GetTestPlanV2Step(stepID uint64) (*apistructs.TestPlanV2Step, e return &getResp.Data, nil } -// GetTestPlanV2 获取测试计划步骤 +// ListTestPlanV2Step list test plan step +func (b *Bundle) ListTestPlanV2Step(testPlanID, groupID uint64) ([]*apistructs.TestPlanV2Step, error) { + host, err := b.urls.DOP() + if err != nil { + return nil, err + } + hc := b.hc + + var getResp apistructs.TestPlanV2StepListResponse + resp, err := hc.Get(host).Path(fmt.Sprintf("/api/autotests/testplans/%d/steps/actions/list-by-group-id", testPlanID)). + Param("groupID", strconv.FormatUint(groupID, 10)). + Header(httputil.InternalHeader, "bundle").Do().JSON(&getResp) + + if err != nil { + return nil, apierrors.ErrInvoke.InternalError(err) + } + if !resp.IsOK() || !getResp.Success { + return nil, toAPIError(resp.StatusCode(), getResp.Error) + } + + return getResp.Data, nil +} + +// UpdateTestPlanV2Step 获取测试计划步骤 func (b *Bundle) UpdateTestPlanV2Step(req apistructs.TestPlanV2StepUpdateRequest) error { host, err := b.urls.DOP() if err != nil { diff --git a/modules/dop/dao/testplan_v2_step.go b/modules/dop/dao/testplan_v2_step.go index 5735e60dc30..247d0fa78c8 100644 --- a/modules/dop/dao/testplan_v2_step.go +++ b/modules/dop/dao/testplan_v2_step.go @@ -20,6 +20,7 @@ import ( "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/pkg/database/dbengine" + "github.com/erda-project/erda/pkg/strutil" ) // TestPlanV2Step 测试计划V2步骤 @@ -28,6 +29,7 @@ type TestPlanV2Step struct { PlanID uint64 SceneSetID uint64 PreID uint64 + GroupID uint64 } // TestPlanV2StepJoin 测试计划V2步骤join测试集表 @@ -49,6 +51,7 @@ func (tps TestPlanV2StepJoin) Convert2DTO() *apistructs.TestPlanV2Step { PreID: tps.PreID, PlanID: tps.PlanID, ID: tps.ID, + GroupID: tps.GroupID, } } @@ -69,102 +72,209 @@ func (client *DBClient) GetTestPlanV2Step(ID uint64) (*TestPlanV2StepJoin, error return &step, nil } +// ListTestPlanV2Step list testPlan step +func (client *DBClient) ListTestPlanV2Step(testPlanID, groupID uint64) ([]TestPlanV2StepJoin, error) { + var step []TestPlanV2StepJoin + err := client.Where("plan_id = ?", testPlanID). + Where("group_id = ? OR id = ?", groupID, groupID). + Find(&step).Error + return step, err +} + // AddTestPlanV2Step Insert a step in the test plan -func (client *DBClient) AddTestPlanV2Step(req *apistructs.TestPlanV2StepAddRequest) error { - return client.Transaction(func(tx *gorm.DB) error { +func (client *DBClient) AddTestPlanV2Step(req *apistructs.TestPlanV2StepAddRequest) (uint64, error) { + var newStepID uint64 + err := client.Transaction(func(tx *gorm.DB) error { var preStep, nextStep TestPlanV2Step - newStep := TestPlanV2Step{PreID: req.PreID, SceneSetID: req.SceneSetID, PlanID: req.TestPlanID} + newStep := TestPlanV2Step{PreID: req.PreID, SceneSetID: req.SceneSetID, PlanID: req.TestPlanID, GroupID: req.GroupID} // Check the pre step is exist if req.PreID != 0 && tx.Where("id = ?", req.PreID).First(&preStep).Error != nil { return errors.Errorf("the pre step is not found: %d", req.PreID) } + // If the groupID of preStep is 0, set its id as groupID + if req.PreID != 0 && preStep.GroupID == 0 && req.GroupID != 0 { + preStep.GroupID = preStep.ID + if err := tx.Save(&preStep).Error; err != nil { + return err + } + } + // Find the next step + hasNextStep := true if err := tx.Where("pre_id = ?", req.PreID).Where("plan_id = ?", req.TestPlanID).First(&nextStep).Error; err != nil { if gorm.IsRecordNotFoundError(err) { - // Insert to the end or beginning - return tx.Create(&newStep).Error + hasNextStep = false + } else { + return err } - return err } + // Insert new step if err := tx.Create(&newStep).Error; err != nil { return err } + newStepID = newStep.ID + + // If req.GroupID is 0, set stepID as groupID + if req.GroupID == 0 { + newStep.GroupID = newStepID + if err := tx.Save(&newStep).Error; err != nil { + return err + } + } + // Update the order of next step - nextStep.PreID = newStep.ID - return tx.Save(&nextStep).Error + if hasNextStep { + nextStep.PreID = newStepID + return tx.Save(&nextStep).Error + } + return nil }) + + return newStepID, err } // DeleteTestPlanV2Step Delete a step in the test plan func (client *DBClient) DeleteTestPlanV2Step(req *apistructs.TestPlanV2StepDeleteRequest) error { - return client.Transaction(func(tx *gorm.DB) error { + return client.Transaction(func(tx *gorm.DB) (err error) { var step, nextStep TestPlanV2Step + defer func() { + if err == nil { + err = updateStepGroup(tx, step.GroupID) + } + }() + // Get the step - if err := tx.Where("id = ?", req.StepID).First(&step).Error; err != nil { + if err = tx.Where("id = ?", req.StepID).First(&step).Error; err != nil { return err } // Get the next step - if err := tx.Where("pre_id = ?", req.StepID).Where("plan_id = ?", req.TestPlanID).First(&nextStep).Error; err != nil { + if err = tx.Where("pre_id = ?", req.StepID).Where("plan_id = ?", req.TestPlanID).First(&nextStep).Error; err != nil { if gorm.IsRecordNotFoundError(err) { // Delete the last step - return tx.Delete(&step).Error + err = tx.Delete(&step).Error + return err } return err + } // Update next step nextStep.PreID = step.PreID - if err := tx.Save(&nextStep).Error; err != nil { + if err = tx.Save(&nextStep).Error; err != nil { return err } - return tx.Delete(&step).Error + err = tx.Delete(&step).Error + return err }) } -// UpdateTestPlanV2Step Update a step in the test plan -func (client *DBClient) MoveTestPlanV2Step(req *apistructs.TestPlanV2StepUpdateRequest) error { - return client.Transaction(func(tx *gorm.DB) error { - var step, oldNextStep, newNextStep TestPlanV2Step - // Get the step - if err := tx.Where("id = ?", req.StepID).First(&step).Error; err != nil { +// MoveTestPlanV2Step move a step in the test plan +func (client *DBClient) MoveTestPlanV2Step(req *apistructs.TestPlanV2StepMoveRequest) error { + return client.Transaction(func(tx *gorm.DB) (err error) { + var oldGroupID, newGroupID uint64 + // update step groupID in the group if isGroup is false + defer func() { + if err == nil && !req.IsGroup { + groupIDs := strutil.DedupUint64Slice([]uint64{oldGroupID, newGroupID}, true) + err = updateStepGroup(tx, groupIDs...) + } + }() + + var ( + step, oldNextStep, newNextStep TestPlanV2Step + ) + + firstStepIDInGroup := req.StepID + lastStepIDInGroup := req.LastStepID + // get the first step in the group + if err = tx.Where("id = ?", firstStepIDInGroup).First(&step).Error; err != nil { return err } - // Get the old next step - if err := tx.Where("pre_id = ?", req.StepID).Where("plan_id = ?", req.TestPlanID).First(&oldNextStep).Error; err != nil { + oldGroupID = step.GroupID + + // the order of the linked list has not changed + if req.PreID == step.PreID || req.PreID == lastStepIDInGroup { + if req.IsGroup { + return nil + } + goto LABEL2 + } + + // Get the old next step and update its preID if exists + if err = tx.Where("pre_id = ?", lastStepIDInGroup). + Where("plan_id = ?", req.TestPlanID).First(&oldNextStep).Error; err != nil { if gorm.IsRecordNotFoundError(err) { // the step was the last step goto LABEL1 } return err } - // Update oldNextStep oldNextStep.PreID = step.PreID - if err := tx.Save(&oldNextStep).Error; err != nil { + if err = tx.Save(&oldNextStep).Error; err != nil { return err } - LABEL1: - // Get the new next step + LABEL1: // get the new next step and update its preID if exists step.PreID = req.PreID - if err := tx.Where("pre_id = ?", req.PreID).Where("plan_id = ?", req.TestPlanID).First(&newNextStep).Error; err != nil { + if err = tx.Where("pre_id = ?", req.PreID).Where("plan_id = ?", req.TestPlanID).First(&newNextStep).Error; err != nil { if gorm.IsRecordNotFoundError(err) { // target step is the last step goto LABEL2 } return err } - // Update newNextStep - newNextStep.PreID = req.StepID - if err := tx.Save(&newNextStep).Error; err != nil { + newNextStep.PreID = lastStepIDInGroup + if err = tx.Save(&newNextStep).Error; err != nil { return err } - LABEL2: - return tx.Save(&step).Error + LABEL2: // update the preID of the step, and update the groupID of the step if needed + if !req.IsGroup { + if req.TargetStepID == 0 { + newGroupID = 0 + } else { // else find the groupID of targetStep + var targetStep TestPlanV2Step + if err = tx.Where("id = ?", req.TargetStepID).First(&targetStep).Error; err != nil { + return err + } + newGroupID = targetStep.GroupID + // if the groupID of targetStep is 0, set its id as groupID + if newGroupID == 0 { + targetStep.GroupID = targetStep.ID + newGroupID = targetStep.ID + if err = tx.Save(&targetStep).Error; err != nil { + return err + } + } + } + step.GroupID = newGroupID + } + err = tx.Save(&step).Error + return err }) } +// updateStepGroup update step group, set min setID in the group as groupID +func updateStepGroup(tx *gorm.DB, groupIDs ...uint64) error { + for _, v := range groupIDs { + if v == 0 { + continue + } + + var stepGroup []TestPlanV2Step + if err := tx.Where("group_id = ?", v).Order("id").Find(&stepGroup).Error; err != nil { + return err + } + if len(stepGroup) > 0 { + if err := tx.Model(&TestPlanV2Step{}).Where("group_id = ?", v).Update("group_id", stepGroup[0].ID).Error; err != nil { + return err + } + } + } + return nil +} + func (client *DBClient) UpdateTestPlanV2Step(step TestPlanV2Step) error { return client.Save(&step).Error } @@ -178,7 +288,7 @@ func (client *DBClient) GetStepByTestPlanID(testPlanID uint64, needSort bool) ([ ) if err := client.Table("dice_autotest_plan_step").Select("dice_autotest_plan_step.id, dice_autotest_plan_step.created_at, "+ "dice_autotest_plan_step.updated_at, dice_autotest_plan_step.plan_id, dice_autotest_plan_step.pre_id, "+ - "dice_autotest_plan_step.scene_set_id, dice_autotest_scene_set.name"). + "dice_autotest_plan_step.scene_set_id, dice_autotest_scene_set.name,dice_autotest_plan_step.group_id"). Joins("left join dice_autotest_scene_set on dice_autotest_plan_step.scene_set_id = dice_autotest_scene_set.id"). Where("dice_autotest_plan_step.plan_id = ?", testPlanID).Limit(1000).Scan(&steps).Count(&count).Error; err != nil { return nil, 0, err diff --git a/modules/dop/endpoints/endpoints.go b/modules/dop/endpoints/endpoints.go index 82c06bc3e8d..9841295357e 100644 --- a/modules/dop/endpoints/endpoints.go +++ b/modules/dop/endpoints/endpoints.go @@ -409,6 +409,7 @@ func (e *Endpoints) Routes() []httpserver.Endpoint { {Path: "/api/autotests/testplans/{testPlanID}/actions/move-step", Method: http.MethodPut, Handler: e.MoveTestPlanV2Step}, {Path: "/api/autotests/testplans-step/{stepID}", Method: http.MethodGet, Handler: e.GetTestPlanV2Step}, {Path: "/api/autotests/testplans-step/{stepID}", Method: http.MethodPut, Handler: e.UpdateTestPlanV2Step}, + {Path: "/api/autotests/testplans/{testPlanID}/steps/actions/list-by-group-id", Method: http.MethodGet, Handler: e.ListTestPlanV2Step}, {Path: "/api/reportsets/{pipelineID}", Method: http.MethodGet, Handler: e.queryReportSets}, diff --git a/modules/dop/endpoints/testplan_v2.go b/modules/dop/endpoints/testplan_v2.go index 4fa99731869..777a8979ddb 100644 --- a/modules/dop/endpoints/testplan_v2.go +++ b/modules/dop/endpoints/testplan_v2.go @@ -249,24 +249,24 @@ func (e *Endpoints) DeleteTestPlanV2Step(ctx context.Context, r *http.Request, v return httpserver.OkResp("succ") } -// UpdateTestPlanV2Step Update the test plan step +// MoveTestPlanV2Step move the test plan step func (e *Endpoints) MoveTestPlanV2Step(ctx context.Context, r *http.Request, vars map[string]string) (httpserver.Responser, error) { identityInfo, err := user.GetIdentityInfo(r) if err != nil { - return apierrors.ErrUpdateTestPlanStep.NotLogin().ToResp(), nil + return apierrors.ErrMoveTestPlanStep.NotLogin().ToResp(), nil } testPlanID, err := getTestPlanID(vars) if err != nil { - return apierrors.ErrUpdateTestPlanStep.InvalidParameter(err).ToResp(), nil + return apierrors.ErrMoveTestPlanStep.InvalidParameter(err).ToResp(), nil } - var req apistructs.TestPlanV2StepUpdateRequest + var req apistructs.TestPlanV2StepMoveRequest if r.ContentLength == 0 { - return apierrors.ErrUpdateTestPlanStep.MissingParameter("request body").ToResp(), nil + return apierrors.ErrMoveTestPlanStep.MissingParameter("request body").ToResp(), nil } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - return apierrors.ErrUpdateTestPlanStep.InvalidParameter(err).ToResp(), nil + return apierrors.ErrMoveTestPlanStep.InvalidParameter(err).ToResp(), nil } req.IdentityInfo = identityInfo req.TestPlanID = testPlanID @@ -327,6 +327,35 @@ func (e *Endpoints) GetTestPlanV2Step(ctx context.Context, r *http.Request, vars return httpserver.OkResp(step) } +// ListTestPlanV2Step list TestPlan step +func (e *Endpoints) ListTestPlanV2Step(ctx context.Context, r *http.Request, vars map[string]string) (httpserver.Responser, error) { + _, err := user.GetIdentityInfo(r) + if err != nil { + return apierrors.ErrListTestPlanStep.NotLogin().ToResp(), nil + } + + testPlanID, err := strconv.ParseUint(vars["testPlanID"], 10, 64) + if err != nil { + return errorresp.ErrResp(errors.New("testPlanID id parse failed")) + } + + var groupID uint64 + groupIDStr := r.URL.Query().Get("groupID") + if groupIDStr != "" { + groupID, err = strconv.ParseUint(groupIDStr, 10, 64) + if err != nil { + return apierrors.ErrGetTestPlan.InvalidParameter(err).ToResp(), nil + } + } + + steps, err := e.autotestV2.ListTestPlanV2Step(testPlanID, groupID) + if err != nil { + return errorresp.ErrResp(err) + } + + return httpserver.OkResp(steps) +} + func getTestPlanID(vars map[string]string) (uint64, error) { testplanIDStr := vars["testPlanID"] testplanID, err := strconv.ParseUint(testplanIDStr, 10, 64) diff --git a/modules/dop/services/apierrors/errors.go b/modules/dop/services/apierrors/errors.go index 484d7e2ccef..405c5e133f4 100644 --- a/modules/dop/services/apierrors/errors.go +++ b/modules/dop/services/apierrors/errors.go @@ -238,6 +238,8 @@ var ( ErrAddTestPlanStep = err("ErrAddTestPlanStep", "添加测试计划步骤失败") ErrDeleteTestPlanStep = err("ErrDeleteTestPlanStep", "删除测试计划步骤失败") ErrUpdateTestPlanStep = err("ErrUpdateTestPlanStep", "更新测试计划步骤失败") + ErrListTestPlanStep = err("ErrListTestPlanStep", "获取测试计划步骤失败") + ErrMoveTestPlanStep = err("ErrMoveTestPlanStep", "移动测试计划步骤失败") ErrCreateTestPlanMember = err("ErrCreateTestPlanMember", "测试计划关联成员失败") ErrUpdateTestPlanMember = err("ErrUpdateTestPlanMember", "测试计划更新成员失败") ErrListTestPlanMembers = err("ErrListTestPlanMembers", "查询测试计划关联成员列表失败") diff --git a/modules/dop/services/autotest_v2/testplan_v2.go b/modules/dop/services/autotest_v2/testplan_v2.go index c7d77d6ce4e..07701ef38d0 100644 --- a/modules/dop/services/autotest_v2/testplan_v2.go +++ b/modules/dop/services/autotest_v2/testplan_v2.go @@ -268,20 +268,12 @@ func (svc *Service) AddTestPlanV2Step(req *apistructs.TestPlanV2StepAddRequest) } } - // Check the sceneset is exists + // Check the sceneSet is exists if err := svc.db.CheckSceneSetIsExists(req.SceneSetID); err != nil { return 0, err } - if err := svc.db.AddTestPlanV2Step(req); err != nil { - return 0, err - } - - newStep, err := svc.db.GetTestPlanV2StepByPreID(req.PreID) - if err != nil { - return 0, err - } - return newStep.ID, nil + return svc.db.AddTestPlanV2Step(req) } // DeleteTestPlanV2Step Delete a step in the test plan @@ -304,15 +296,15 @@ func (svc *Service) DeleteTestPlanV2Step(req *apistructs.TestPlanV2StepDeleteReq return err } if !access.Access { - return apierrors.ErrUpdateTestPlan.AccessDenied() + return apierrors.ErrDeleteTestPlan.AccessDenied() } } return svc.db.DeleteTestPlanV2Step(req) } -// UpdateTestPlanV2Step Update a step in the test plan -func (svc *Service) MoveTestPlanV2Step(req *apistructs.TestPlanV2StepUpdateRequest) error { +// MoveTestPlanV2Step move a step in the test plan +func (svc *Service) MoveTestPlanV2Step(req *apistructs.TestPlanV2StepMoveRequest) error { testPlan, err := svc.db.GetTestPlanV2ByID(req.TestPlanID) if err != nil { return err @@ -345,7 +337,6 @@ func (svc *Service) UpdateTestPlanV2Step(req *apistructs.TestPlanV2StepUpdateReq if err != nil { return err } - step.ID = req.StepID step.SceneSetID = req.ScenesSetId plan, err := svc.db.GetTestPlanV2ByID(step.PlanID) @@ -387,6 +378,21 @@ func (svc *Service) GetTestPlanV2Step(ID uint64) (*apistructs.TestPlanV2Step, er return step.Convert2DTO(), nil } +// ListTestPlanV2Step list testPlan step +func (svc *Service) ListTestPlanV2Step(testPlanID, groupID uint64) ([]*apistructs.TestPlanV2Step, error) { + steps, err := svc.db.ListTestPlanV2Step(testPlanID, groupID) + if err != nil { + return nil, err + } + + var stepDtos []*apistructs.TestPlanV2Step + for _, v := range steps { + stepDtos = append(stepDtos, v.Convert2DTO()) + } + + return stepDtos, nil +} + // getChangedFields get changed fields func (svc *Service) getChangedFields(req *apistructs.TestPlanV2UpdateRequest, model *dao.TestPlanV2) (map[string]interface{}, error) { fields := make(map[string]interface{}, 0) @@ -418,7 +424,6 @@ func (svc *Service) getChangedFields(req *apistructs.TestPlanV2UpdateRequest, mo } func (svc *Service) ExecuteDiceAutotestTestPlan(req apistructs.AutotestExecuteTestPlansRequest) (*apistructs.PipelineDTO, error) { - testPlan, err := svc.GetTestPlanV2(req.TestPlan.ID, req.IdentityInfo) if err != nil { return nil, err @@ -427,35 +432,42 @@ func (svc *Service) ExecuteDiceAutotestTestPlan(req apistructs.AutotestExecuteTe var spec pipelineyml.Spec spec.Version = "1.1" var stagesValue []*pipelineyml.Stage - for _, v := range testPlan.Steps { - if v.SceneSetID <= 0 { - continue - } + + // get steps group by groupID + stepGroupMap, groupIDs := getStepMapByGroupID(testPlan.Steps) + + for _, groupID := range groupIDs { var specStage pipelineyml.Stage - sceneSetJson, err := json.Marshal(v) - if err != nil { - return nil, err - } - specStage.Actions = append(specStage.Actions, map[pipelineyml.ActionType]*pipelineyml.Action{ - pipelineyml.Snippet: { - Alias: pipelineyml.ActionAlias(strconv.Itoa(int(v.ID))), - Type: pipelineyml.Snippet, - Labels: map[string]string{ - apistructs.AutotestSceneSet: base64.StdEncoding.EncodeToString(sceneSetJson), - apistructs.AutotestType: apistructs.AutotestSceneSet, - }, - If: expression.LeftPlaceholder + " 1 == 1 " + expression.RightPlaceholder, - SnippetConfig: &pipelineyml.SnippetConfig{ - Name: strconv.Itoa(int(v.SceneSetID)), - Source: apistructs.PipelineSourceAutoTest.String(), + for _, v := range stepGroupMap[groupID] { + if v.SceneSetID <= 0 { + continue + } + sceneSetJson, err := json.Marshal(v) + if err != nil { + return nil, err + } + action := map[pipelineyml.ActionType]*pipelineyml.Action{ + pipelineyml.Snippet: { + Alias: pipelineyml.ActionAlias(strconv.Itoa(int(v.ID))), + Type: pipelineyml.Snippet, Labels: map[string]string{ - apistructs.LabelAutotestExecType: apistructs.SceneSetsAutotestExecType, - apistructs.LabelSceneSetID: strconv.Itoa(int(v.SceneSetID)), - apistructs.LabelSpaceID: strconv.Itoa(int(testPlan.SpaceID)), + apistructs.AutotestSceneSet: base64.StdEncoding.EncodeToString(sceneSetJson), + apistructs.AutotestType: apistructs.AutotestSceneSet, + }, + If: expression.LeftPlaceholder + " 1 == 1 " + expression.RightPlaceholder, + SnippetConfig: &pipelineyml.SnippetConfig{ + Name: strconv.Itoa(int(v.SceneSetID)), + Source: apistructs.PipelineSourceAutoTest.String(), + Labels: map[string]string{ + apistructs.LabelAutotestExecType: apistructs.SceneSetsAutotestExecType, + apistructs.LabelSceneSetID: strconv.Itoa(int(v.SceneSetID)), + apistructs.LabelSpaceID: strconv.Itoa(int(testPlan.SpaceID)), + }, }, }, - }, - }) + } + specStage.Actions = append(specStage.Actions, action) + } stagesValue = append(stagesValue, &specStage) } spec.Stages = stagesValue @@ -494,6 +506,28 @@ func (svc *Service) ExecuteDiceAutotestTestPlan(req apistructs.AutotestExecuteTe return pipelineDTO, nil } +// getStepMapByGroupID get step group by groupID +func getStepMapByGroupID(steps []*apistructs.TestPlanV2Step) (map[uint64][]*apistructs.TestPlanV2Step, []uint64) { + // stepGroupMap key: groupID, if groupID is 0, set id as groupID + stepGroupMap := make(map[uint64][]*apistructs.TestPlanV2Step, 0) + groupIDs := make([]uint64, 0) // make sure the order remains the same + for _, v := range steps { + if v.SceneSetID <= 0 { + continue + } + if v.GroupID == 0 { + v.GroupID = v.ID + } + if _, ok := stepGroupMap[v.GroupID]; ok { + stepGroupMap[v.GroupID] = append(stepGroupMap[v.GroupID], v) + } else { + stepGroupMap[v.GroupID] = []*apistructs.TestPlanV2Step{v} + groupIDs = append(groupIDs, v.GroupID) + } + } + return stepGroupMap, groupIDs +} + func (svc *Service) GetTestClusterNameBySpaceID(spaceID uint64) (string, error) { space, err := svc.db.GetAutoTestSpace(spaceID) if err != nil { diff --git a/modules/dop/services/autotest_v2/testplan_v2_test.go b/modules/dop/services/autotest_v2/testplan_v2_test.go new file mode 100644 index 00000000000..da5324fed48 --- /dev/null +++ b/modules/dop/services/autotest_v2/testplan_v2_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 autotestv2 + +import ( + "reflect" + "testing" + + "github.com/erda-project/erda/apistructs" +) + +func TestGetStepMapByGroupID(t *testing.T) { + tt := []struct { + steps []*apistructs.TestPlanV2Step + wantGroupIDs []uint64 + wantStepGroupMap map[uint64][]*apistructs.TestPlanV2Step + }{ + { + steps: []*apistructs.TestPlanV2Step{ + {ID: 1, PreID: 0, GroupID: 0, SceneSetID: 1}, + {ID: 2, PreID: 1, GroupID: 2, SceneSetID: 2}, + {ID: 3, PreID: 2, GroupID: 2, SceneSetID: 3}, + {ID: 4, PreID: 3, GroupID: 4, SceneSetID: 4}, + {ID: 5, PreID: 4, GroupID: 4, SceneSetID: 5}, + {ID: 6, PreID: 5, GroupID: 4, SceneSetID: 6}, + {ID: 7, PreID: 6, GroupID: 7, SceneSetID: 7}, + }, + wantGroupIDs: []uint64{1, 2, 4, 7}, + wantStepGroupMap: map[uint64][]*apistructs.TestPlanV2Step{ + 1: {{ID: 1, PreID: 0, GroupID: 0, SceneSetID: 1}}, + 2: {{ID: 2, PreID: 1, GroupID: 2, SceneSetID: 2}, {ID: 3, PreID: 2, GroupID: 2, SceneSetID: 3}}, + 4: {{ID: 4, PreID: 3, GroupID: 4, SceneSetID: 4}, {ID: 5, PreID: 4, GroupID: 4, SceneSetID: 5}, {ID: 6, PreID: 5, GroupID: 4, SceneSetID: 6}}, + 7: {{ID: 7, PreID: 6, GroupID: 7, SceneSetID: 7}}, + }, + }, + { + steps: []*apistructs.TestPlanV2Step{ + {ID: 5, PreID: 0, GroupID: 0, SceneSetID: 1}, + {ID: 3, PreID: 5, GroupID: 3, SceneSetID: 2}, + {ID: 7, PreID: 3, GroupID: 4, SceneSetID: 3}, + {ID: 4, PreID: 7, GroupID: 4, SceneSetID: 4}, + {ID: 1, PreID: 4, GroupID: 1, SceneSetID: 5}, + {ID: 2, PreID: 1, GroupID: 1, SceneSetID: 6}, + {ID: 6, PreID: 2, GroupID: 6, SceneSetID: 7}, + }, + wantGroupIDs: []uint64{5, 3, 4, 1, 6}, + wantStepGroupMap: map[uint64][]*apistructs.TestPlanV2Step{ + 5: {{ID: 5, PreID: 0, GroupID: 0, SceneSetID: 1}}, + 3: {{ID: 3, PreID: 5, GroupID: 3, SceneSetID: 2}}, + 4: {{ID: 7, PreID: 3, GroupID: 4, SceneSetID: 3}, {ID: 4, PreID: 7, GroupID: 4, SceneSetID: 4}}, + 1: {{ID: 1, PreID: 4, GroupID: 1, SceneSetID: 5}, {ID: 2, PreID: 1, GroupID: 1, SceneSetID: 6}}, + 6: {{ID: 6, PreID: 2, GroupID: 6, SceneSetID: 7}}, + }, + }, + } + + for _, v := range tt { + stepGroupMap, groupIDs := getStepMapByGroupID(v.steps) + if !reflect.DeepEqual(v.wantGroupIDs, groupIDs) { + t.Error("fail") + } + for k, v1 := range stepGroupMap { + for i, v2 := range v1 { + if v.wantStepGroupMap[k][i].ID != v2.ID { + t.Error("fail") + } + } + } + } +} diff --git a/modules/dop/services/sceneset/sceneset.go b/modules/dop/services/sceneset/sceneset.go index d7f1b1deefe..11512ec6051 100644 --- a/modules/dop/services/sceneset/sceneset.go +++ b/modules/dop/services/sceneset/sceneset.go @@ -16,30 +16,20 @@ package sceneset import ( "fmt" - "regexp" "github.com/erda-project/erda/apistructs" "github.com/erda-project/erda/modules/dop/dao" "github.com/erda-project/erda/modules/dop/services/apierrors" - "github.com/erda-project/erda/pkg/strutil" ) const ( - maxSize int = 200 - nameMaxLength int = 50 - descMaxLength int = 255 + maxSize int = 200 ) func (svc *Service) CreateSceneSet(req apistructs.SceneSetRequest) (uint64, error) { - if err := strutil.Validate(req.Name, strutil.MaxRuneCountValidator(nameMaxLength)); err != nil { + if err := req.Validate(); err != nil { return 0, err } - if err := strutil.Validate(req.Description, strutil.MaxRuneCountValidator(descMaxLength)); err != nil { - return 0, err - } - if ok, _ := regexp.MatchString("^[a-zA-Z\u4e00-\u9fa50-9_-]*$", req.Name); !ok { - return 0, apierrors.ErrCreateAutoTestSceneSet.InvalidState("只可输入中文、英文、数字、中划线或下划线") - } count, err := svc.db.CountSceneSetByName(req.Name, req.SpaceID) if err != nil { @@ -54,7 +44,7 @@ func (svc *Service) CreateSceneSet(req apistructs.SceneSetRequest) (uint64, erro return 0, err } if len(sets) >= maxSize { - return 0, fmt.Errorf("Reach max sceneset size!") + return 0, fmt.Errorf("reach max sceneset size") } preID := uint64(0) @@ -98,23 +88,13 @@ func (svc *Service) GetSceneSetsBySpaceID(spaceID uint64) ([]apistructs.SceneSet res = append(res, s) } - // res := make([]apistructs.SceneSet, 0, len(sceneSets)) - // for _, item := range sceneSets { - // res = append(res, *mapping(&item)) - // } return res, nil } func (svc *Service) UpdateSceneSet(setID uint64, req apistructs.SceneSetRequest) (*apistructs.SceneSet, error) { - if err := strutil.Validate(req.Name, strutil.MaxRuneCountValidator(nameMaxLength)); err != nil { + if err := req.Validate(); err != nil { return nil, err } - if err := strutil.Validate(req.Description, strutil.MaxRuneCountValidator(descMaxLength)); err != nil { - return nil, err - } - if ok, _ := regexp.MatchString("^[a-zA-Z\u4e00-\u9fa50-9_-]*$", req.Name); !ok { - return nil, apierrors.ErrCreateAutoTestSceneSet.InvalidState("只可输入中文、英文、数字、中划线或下划线") - } s, err := svc.db.GetSceneSet(setID) if err != nil { diff --git a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/addScenesSetButton/render.go b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/addScenesSetButton/render.go index cac3bd69d7b..4090daeb6bc 100644 --- a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/addScenesSetButton/render.go +++ b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/addScenesSetButton/render.go @@ -44,6 +44,7 @@ func (ca *ComponentAction) Render(ctx context.Context, c *apistructs.Component, req.UserID = bdl.Identity.UserID req.TestPlanID = uint64(c.State["testPlanId"].(float64)) req.PreID = lastStepID + req.GroupID = 0 stepID, err := bdl.Bdl.CreateTestPlansV2Step(req) if err != nil { return err diff --git a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/model.go b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/model.go index 400908362d8..e4cf74aed99 100644 --- a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/model.go +++ b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/model.go @@ -54,9 +54,11 @@ type InParams struct { } type DragParams struct { - DragKey uint64 `json:"dragKey"` - DropKey uint64 `json:"dropKey"` - Position int64 `json:"position"` + DragGroupKey int64 `json:"dragGroupKey"` + DropGroupKey int64 `json:"dropGroupKey"` + DragKey int64 `json:"dragKey"` + DropKey int64 `json:"dropKey"` + Position int64 `json:"position"` } type State struct { diff --git a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/render.go b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/render.go index 43cd5e8b6ba..988d67e0957 100644 --- a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/render.go +++ b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/render.go @@ -145,7 +145,7 @@ func (i *ComponentStageForm) Render(ctx context.Context, c *apistructs.Component return } i.Props = visible - i.Props["groupDraggable"] = false + i.Props["groupDraggable"] = true switch event.Operation { case apistructs.InitializeOperation, apistructs.RenderingOperation: err = i.RenderListStageForm() @@ -153,7 +153,16 @@ func (i *ComponentStageForm) Render(ctx context.Context, c *apistructs.Component return err } case apistructs.AutoTestSceneStepMoveItemOperationKey: - err = i.RenderMoveStagesForm() + err = i.RenderItemMoveStagesForm() + if err != nil { + return err + } + err = i.RenderListStageForm() + if err != nil { + return err + } + case apistructs.AutoTestSceneStepMoveGroupOperationKey: + err = i.RenderGroupMoveStagesForm() if err != nil { return err } @@ -187,6 +196,15 @@ func (i *ComponentStageForm) Render(ctx context.Context, c *apistructs.Component if err := i.RenderListStageForm(); err != nil { return err } + case apistructs.AutoTestSceneStepSplitOperationKey: + err = i.RenderSplitStagesForm(event.OperationData) + if err != nil { + return err + } + err = i.RenderListStageForm() + if err != nil { + return err + } } i.RenderProtocol(c, gs) return diff --git a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages.go b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages.go index 47304360429..a2f4a070789 100644 --- a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages.go +++ b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages.go @@ -15,14 +15,22 @@ package stages import ( + "fmt" + + "github.com/pkg/errors" + "github.com/erda-project/erda/apistructs" ) -func RenderStage(index int, step apistructs.TestPlanV2Step) (StageData, error) { +func (i *ComponentStageForm) RenderStage(step apistructs.TestPlanV2Step) (StageData, error) { + groupID := int(step.GroupID) + if groupID == 0 { + groupID = int(step.ID) + } pd := StageData{ - Title: "场景集: " + step.SceneSetName, + Title: fmt.Sprintf("#%d 场景集: %s", step.ID, step.SceneSetName), ID: step.ID, - GroupID: index, + GroupID: groupID, Operations: make(map[string]interface{}), } o := CreateOperation{} @@ -43,6 +51,29 @@ func RenderStage(index int, step apistructs.TestPlanV2Step) (StageData, error) { oc.Meta.ID = step.ID pd.Operations["delete"] = oc + os := OperationInfo{ + OperationBaseInfo: OperationBaseInfo{ + Icon: "split", + Key: apistructs.AutoTestSceneStepSplitOperationKey.String(), + HoverTip: "改为串行", + Disabled: true, + Reload: true, + }, + } + os.Meta.ID = step.ID + m := map[string]interface{}{ + "groupID": groupID, + } + os.Meta.Data = m + stepGroup, err := i.ctxBdl.Bdl.ListTestPlanV2Step(step.PlanID, uint64(groupID)) + if err != nil { + return StageData{}, err + } + if len(stepGroup) > 1 { + os.Disabled = false + } + pd.Operations["split"] = os + return pd, nil } @@ -52,8 +83,8 @@ func (i *ComponentStageForm) RenderListStageForm() error { return err } var list []StageData - for i, v := range rsp.Data.Steps { - stageData, err := RenderStage(i, *v) + for _, v := range rsp.Data.Steps { + stageData, err := i.RenderStage(*v) if err != nil { return err } @@ -70,6 +101,12 @@ func (i *ComponentStageForm) RenderListStageForm() error { Reload: true, }, } + omg := OperationInfo{ + OperationBaseInfo: OperationBaseInfo{ + Key: apistructs.AutoTestSceneStepMoveGroupOperationKey.String(), + Reload: true, + }, + } ocl := OperationInfo{ OperationBaseInfo: OperationBaseInfo{ Key: "clickItem", @@ -79,6 +116,7 @@ func (i *ComponentStageForm) RenderListStageForm() error { Meta: OpMetaInfo{}, } i.Operations["moveItem"] = omi + i.Operations["moveGroup"] = omg i.Operations["clickItem"] = ocl return nil @@ -89,10 +127,19 @@ func (i *ComponentStageForm) RenderCreateStagesForm(opsData interface{}) error { if err != nil { return err } + preStep, err := i.ctxBdl.Bdl.GetTestPlanV2Step(meta.ID) + if err != nil { + return err + } + groupID := preStep.GroupID + if groupID == 0 { + groupID = preStep.ID + } _, err = i.ctxBdl.Bdl.CreateTestPlansV2Step(apistructs.TestPlanV2StepAddRequest{ PreID: meta.ID, TestPlanID: i.State.TestPlanId, + GroupID: groupID, IdentityInfo: apistructs.IdentityInfo{ UserID: i.ctxBdl.Identity.UserID, }, @@ -122,32 +169,174 @@ func (i *ComponentStageForm) RenderDeleteStagesForm(opsData interface{}) error { return nil } -func (i *ComponentStageForm) RenderMoveStagesForm() (err error) { +func (i *ComponentStageForm) RenderGroupMoveStagesForm() (err error) { var ( - step *apistructs.TestPlanV2Step - req apistructs.TestPlanV2StepUpdateRequest + req apistructs.TestPlanV2StepMoveRequest ) + dragGroupKey := uint64(i.State.DragParams.DragGroupKey) + dropGroupKey := uint64(i.State.DragParams.DropGroupKey) + req.UserID = i.ctxBdl.Identity.UserID req.TestPlanID = i.State.TestPlanId - req.StepID = i.State.DragParams.DragKey - if i.State.DragParams.Position == -1 { - step, err = i.ctxBdl.Bdl.GetTestPlanV2Step(i.State.DragParams.DropKey) + req.IsGroup = true + + stepDragGroup, err := i.ctxBdl.Bdl.ListTestPlanV2Step(i.State.TestPlanId, dragGroupKey) + if err != nil { + return err + } + if len(stepDragGroup) <= 0 { + return errors.New("the dragGroupKey is not exists") + } + firstStepDrag, lastStepDrag := findFirstLastStepInGroup(stepDragGroup) + req.StepID = firstStepDrag.ID + req.LastStepID = lastStepDrag.ID + + switch i.State.DragParams.Position { + case 0: // inside target + return nil + case -1: // in front of the target + stepDropGroup, err := i.ctxBdl.Bdl.ListTestPlanV2Step(i.State.TestPlanId, dropGroupKey) if err != nil { - return + return err + } + if len(stepDropGroup) <= 0 { + return errors.New("the dropGroupKey is not exists") + } + firstStepDrop, _ := findFirstLastStepInGroup(stepDropGroup) + req.PreID = firstStepDrop.PreID + req.TargetStepID = firstStepDrop.ID + // the order of the linked list has not changed + if req.PreID == req.LastStepID { + return nil + } + case 1: // behind the target + stepDropGroup, err := i.ctxBdl.Bdl.ListTestPlanV2Step(i.State.TestPlanId, dropGroupKey) + if err != nil { + return err + } + if len(stepDropGroup) <= 0 { + return errors.New("the dropGroupKey is not exists") + } + _, lastStepDrop := findFirstLastStepInGroup(stepDropGroup) + req.PreID = lastStepDrop.ID + req.TargetStepID = lastStepDrop.ID + // the order of the linked list has not changed + if req.PreID == firstStepDrag.PreID { + return nil + } + default: + return errors.New("unknown position") + } + return i.ctxBdl.Bdl.MoveTestPlansV2Step(req) +} + +func findFirstLastStepInGroup(steps []*apistructs.TestPlanV2Step) (firstStep, lastStep *apistructs.TestPlanV2Step) { + stepIDMap := make(map[uint64]*apistructs.TestPlanV2Step, len(steps)) + preIDMap := make(map[uint64]*apistructs.TestPlanV2Step, len(steps)) + for _, v := range steps { + stepIDMap[v.ID] = v + preIDMap[v.PreID] = v + } + for k := range preIDMap { + if _, ok := stepIDMap[k]; !ok { + firstStep = preIDMap[k] + break } - if step.PreID == i.State.DragParams.DragKey { - return + } + for k := range stepIDMap { + if _, ok := preIDMap[k]; !ok { + lastStep = stepIDMap[k] + break } - req.PreID = step.PreID + } + return +} + +func (i *ComponentStageForm) RenderItemMoveStagesForm() (err error) { + var ( + step *apistructs.TestPlanV2Step + req apistructs.TestPlanV2StepMoveRequest + testPlan *apistructs.TestPlanV2GetResponse + ) + dragGroupKey := uint64(i.State.DragParams.DragGroupKey) + dropGroupKey := uint64(i.State.DragParams.DropGroupKey) + + req.UserID = i.ctxBdl.Identity.UserID + req.TestPlanID = i.State.TestPlanId + req.StepID = uint64(i.State.DragParams.DragKey) + req.LastStepID = uint64(i.State.DragParams.DragKey) + req.IsGroup = false + if i.State.DragParams.DropKey == -1 { // move to the end and be independent group + req.TargetStepID = 0 } else { - step, err = i.ctxBdl.Bdl.GetTestPlanV2Step(i.State.DragParams.DragKey) + req.TargetStepID = uint64(i.State.DragParams.DropKey) + } + + // find preID + if i.State.DragParams.DropKey == -1 { // move to the end and be independent group + testPlan, err = i.ctxBdl.Bdl.GetTestPlanV2(i.State.TestPlanId) if err != nil { - return + return err } - if step.PreID == i.State.DragParams.DropKey { - return + req.PreID = testPlan.Data.Steps[len(testPlan.Data.Steps)-1].ID + } else { + switch i.State.DragParams.Position { + case 0: // inside target + return nil + case 1: // behind the target + step, err = i.ctxBdl.Bdl.GetTestPlanV2Step(uint64(i.State.DragParams.DragKey)) + if err != nil { + return + } + req.PreID = uint64(i.State.DragParams.DropKey) + // the order of the linked list has not changed in the same group + if req.PreID == step.PreID && dragGroupKey == dropGroupKey { + return nil + } + + case -1: // in front of the target + step, err = i.ctxBdl.Bdl.GetTestPlanV2Step(uint64(i.State.DragParams.DropKey)) + if err != nil { + return + } + req.PreID = step.PreID + // the order of the linked list has not changed in the same group + if req.PreID == req.LastStepID && dragGroupKey == dropGroupKey { + return nil + } + default: + return errors.New("unknown position") } - req.PreID = i.State.DragParams.DropKey } return i.ctxBdl.Bdl.MoveTestPlansV2Step(req) } + +func (i *ComponentStageForm) RenderSplitStagesForm(opsData interface{}) (err error) { + meta, err := GetOpsInfo(opsData) + if err != nil { + return err + } + + var req apistructs.TestPlanV2StepMoveRequest + + req.UserID = i.ctxBdl.Identity.UserID + req.TestPlanID = i.State.TestPlanId + req.StepID = meta.ID + req.LastStepID = meta.ID + req.IsGroup = false + req.TargetStepID = 0 + + stepGroup, err := i.ctxBdl.Bdl.ListTestPlanV2Step(i.State.TestPlanId, uint64(meta.Data["groupID"].(float64))) + if err != nil { + return err + } + if len(stepGroup) <= 0 { + return errors.New("the groupID is not exists") + } + if len(stepGroup) == 1 { + return nil + } + _, lastStep := findFirstLastStepInGroup(stepGroup) + req.PreID = lastStep.ID + return i.ctxBdl.Bdl.MoveTestPlansV2Step(req) +} diff --git a/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages_test.go b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages_test.go new file mode 100644 index 00000000000..cd2365e1f7f --- /dev/null +++ b/modules/openapi/component-protocol/scenarios/auto-test-plan-detail/components/stages/stages_test.go @@ -0,0 +1,354 @@ +// Copyright (c) 2021 Terminus, Inc. +// +// 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 stages + +import ( + "reflect" + "testing" + + "bou.ke/monkey" + "github.com/stretchr/testify/assert" + + "github.com/erda-project/erda/apistructs" + "github.com/erda-project/erda/bundle" + protocol "github.com/erda-project/erda/modules/openapi/component-protocol" +) + +func TestFindFirstLastStepInGroup(t *testing.T) { + tt := []struct { + steps []*apistructs.TestPlanV2Step + wantFirst uint64 + wantLast uint64 + }{ + { + steps: []*apistructs.TestPlanV2Step{ + {ID: 2, PreID: 1}, + {ID: 3, PreID: 2}, + {ID: 4, PreID: 3}, + {ID: 5, PreID: 4}, + }, + wantFirst: 2, + wantLast: 5, + }, + { + steps: []*apistructs.TestPlanV2Step{ + {ID: 4, PreID: 0}, + {ID: 2, PreID: 4}, + {ID: 3, PreID: 5}, + {ID: 5, PreID: 2}, + }, + wantFirst: 4, + wantLast: 3, + }, + { + steps: []*apistructs.TestPlanV2Step{ + {ID: 1, PreID: 0}, + }, + wantFirst: 1, + wantLast: 1, + }, + } + for _, v := range tt { + firstStep, lastStep := findFirstLastStepInGroup(v.steps) + firstStepID, lastStepID := firstStep.ID, lastStep.ID + assert.Equal(t, v.wantFirst, firstStepID) + assert.Equal(t, v.wantLast, lastStepID) + } +} + +// TestRenderItemMoveStagesFormFrontTarget [1,2] 3 move 2 to the front of 3 +func TestRenderItemMoveStagesFormFrontTarget(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "GetTestPlanV2Step", func(*bundle.Bundle, uint64) (*apistructs.TestPlanV2Step, error) { + return &apistructs.TestPlanV2Step{ + SceneSetID: 1, + PreID: 1, + PlanID: 1, + GroupID: 1, + ID: 2, + }, nil + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 1, + DropGroupKey: 3, + DragKey: 2, + DropKey: 3, + Position: -1, + }, + TestPlanId: 1, + }, + }, + } + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "MoveTestPlansV2Step", func(*bundle.Bundle, apistructs.TestPlanV2StepMoveRequest) error { + return nil + }) + if i.RenderItemMoveStagesForm() != nil { + t.Error("fail") + } +} + +// TestRenderItemMoveStagesFormBehindTarget [1,2] [3] move 2 to the behind of 3 +func TestRenderItemMoveStagesFormBehindTarget(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "GetTestPlanV2Step", func(*bundle.Bundle, uint64) (*apistructs.TestPlanV2Step, error) { + return &apistructs.TestPlanV2Step{ + SceneSetID: 1, + PreID: 1, + PlanID: 1, + GroupID: 1, + ID: 2, + }, nil + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 1, + DropGroupKey: 3, + DragKey: 2, + DropKey: 3, + Position: 1, + }, + TestPlanId: 1, + }, + }, + } + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "MoveTestPlansV2Step", func(*bundle.Bundle, apistructs.TestPlanV2StepMoveRequest) error { + return nil + }) + if i.RenderItemMoveStagesForm() != nil { + t.Error("fail") + } +} + +// TestRenderItemMoveStagesFormBehindTarget [1,2] [3] move 1 to the front of 2 +func TestRenderItemMoveStagesFormNoChange(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "GetTestPlanV2Step", func(*bundle.Bundle, uint64) (*apistructs.TestPlanV2Step, error) { + return &apistructs.TestPlanV2Step{ + PreID: 1, + GroupID: 1, + ID: 1, + }, nil + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 1, + DropGroupKey: 1, + DragKey: 1, + DropKey: 2, + Position: -1, + }, + TestPlanId: 1, + }, + }, + } + if i.RenderItemMoveStagesForm() != nil { + t.Error("fail") + } +} + +// TestRenderItemMoveStagesFormBehindTarget2 [1,2] [3] move 2 to the behind of 1 +func TestRenderItemMoveStagesFormNoChange2(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "GetTestPlanV2Step", func(*bundle.Bundle, uint64) (*apistructs.TestPlanV2Step, error) { + return &apistructs.TestPlanV2Step{ + PreID: 1, + GroupID: 1, + ID: 2, + }, nil + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 1, + DropGroupKey: 1, + DragKey: 2, + DropKey: 1, + Position: 1, + }, + TestPlanId: 1, + }, + }, + } + if i.RenderItemMoveStagesForm() != nil { + t.Error("fail") + } +} + +// TestRenderGroupMoveStagesFormNoChange [1,2] [3] move [1,2] to the front of [3] +func TestRenderGroupMoveStagesFormNoChange(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "ListTestPlanV2Step", func(bdl *bundle.Bundle, planID, groupID uint64) ([]*apistructs.TestPlanV2Step, error) { + if groupID == 1 { + return []*apistructs.TestPlanV2Step{ + {ID: 1, PreID: 0}, + {ID: 2, PreID: 1}, + }, nil + } + return []*apistructs.TestPlanV2Step{ + {ID: 3, PreID: 2}, + }, nil + + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 1, + DropGroupKey: 3, + Position: -1, + }, + TestPlanId: 1, + }, + }, + } + if i.RenderGroupMoveStagesForm() != nil { + t.Error("fail") + } +} + +// TestRenderGroupMoveStagesFormNoChange2 [1,2] [3] move [3] to the behind of [1,2] +func TestRenderGroupMoveStagesFormNoChange2(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "ListTestPlanV2Step", func(bdl *bundle.Bundle, planID, groupID uint64) ([]*apistructs.TestPlanV2Step, error) { + if groupID == 3 { + return []*apistructs.TestPlanV2Step{ + {ID: 3, PreID: 2}, + }, nil + } + return []*apistructs.TestPlanV2Step{ + {ID: 1, PreID: 0}, + {ID: 2, PreID: 1}, + }, nil + + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 3, + DropGroupKey: 1, + Position: 1, + }, + TestPlanId: 1, + }, + }, + } + if i.RenderGroupMoveStagesForm() != nil { + t.Error("fail") + } +} + +func TestRenderSplitStagesFormWithOneStep(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "ListTestPlanV2Step", func(*bundle.Bundle, uint64, uint64) ([]*apistructs.TestPlanV2Step, error) { + return []*apistructs.TestPlanV2Step{ + { + PreID: 0, + PlanID: 1, + GroupID: 1, + ID: 1, + }, + }, nil + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 3, + DropGroupKey: 1, + Position: 1, + }, + TestPlanId: 1, + }, + }, + } + + opsData := OperationInfo{ + OperationBaseInfo: OperationBaseInfo{}, + Meta: OpMetaInfo{ + ID: 1, + Data: map[string]interface{}{ + "groupID": 1, + }, + }, + } + if i.RenderSplitStagesForm(opsData) != nil { + t.Error("fail") + } +} + +func TestRenderSplitStagesFormWithNilStep(t *testing.T) { + var bdl *bundle.Bundle + monkey.PatchInstanceMethod(reflect.TypeOf(bdl), "ListTestPlanV2Step", func(*bundle.Bundle, uint64, uint64) ([]*apistructs.TestPlanV2Step, error) { + return nil, nil + }) + defer monkey.UnpatchAll() + + i := ComponentStageForm{ + ctxBdl: protocol.ContextBundle{}, + CommonStageForm: CommonStageForm{ + State: State{ + DragParams: DragParams{ + DragGroupKey: 3, + DropGroupKey: 1, + Position: 1, + }, + TestPlanId: 1, + }, + }, + } + + opsData := OperationInfo{ + OperationBaseInfo: OperationBaseInfo{}, + Meta: OpMetaInfo{ + ID: 1, + Data: map[string]interface{}{ + "groupID": 1, + }, + }, + } + if i.RenderSplitStagesForm(opsData).Error() != "the groupID is not exists" { + t.Error("fail") + } +}