Skip to content

Commit ae31420

Browse files
rbstpBrend-Smits
andauthored
feat(plugins): initial implementation of the testmo plugin (#8485)
* feat(plugins): initial implementation of the testmo plugin This PR introduces a complete Testmo integration plugin for Apache DevLake, enabling comprehensive test management analytics and insights. ## What's Added ### Backend Implementation - **Core Plugin Architecture**: Complete Testmo plugin structure with connection management, data collection, and transformation - **API Integration**: Full integration with Testmo REST API for fetching projects, test runs, automation runs, milestones, and tests - **Data Models**: Comprehensive data models for all Testmo entities including raw and tool-specific schemas - **Data Collection**: Automated data collection workflows with proper error handling and pagination - **Database Schema**: Complete database schema with tables for projects, tests, automation runs, and milestones ### Frontend/UI Integration - **Connection Management**: User-friendly interface for configuring Testmo connections - **Data Source Configuration**: Intuitive setup wizard for Testmo API credentials and project selection - **Dashboard Integration**: Seamless integration with DevLake's project management interface ### Analytics & Visualization - **Grafana Dashboard**: Feature-rich dashboard providing comprehensive test analytics including: - Test execution overview and success rates - Test trend analysis over time - Project-wise test distribution - Test performance metrics - Recent test runs with detailed information ### Key Features - **Multi-project Support**: Handle multiple Testmo projects within a single connection - **Real-time Data Sync**: Automated data collection with configurable sync intervals - **Historical Data**: Complete historical test execution data with proper timestamp handling - **Status Mapping**: Proper mapping of Testmo test statuses (passed, failed, skipped) to DevLake schema - **Performance Optimization**: Efficient data collection with proper pagination and rate limiting No breaking change and no related issues * fix(github): skip jobs with no started_at in cicd_job_convertor (#8488) This change skips GitHub job records that have no started_at value, as a workaround for #8442. Closes #8442 * fix: linter and Test_GetPluginTablesInfo from CI --------- Co-authored-by: Brend Smits <[email protected]>
1 parent 3b5da40 commit ae31420

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4510
-0
lines changed

backend/plugins/table_info_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
starrocks "github.com/apache/incubator-devlake/plugins/starrocks/impl"
5252
tapd "github.com/apache/incubator-devlake/plugins/tapd/impl"
5353
teambition "github.com/apache/incubator-devlake/plugins/teambition/impl"
54+
testmo "github.com/apache/incubator-devlake/plugins/testmo/impl"
5455
trello "github.com/apache/incubator-devlake/plugins/trello/impl"
5556
webhook "github.com/apache/incubator-devlake/plugins/webhook/impl"
5657
zentao "github.com/apache/incubator-devlake/plugins/zentao/impl"
@@ -86,6 +87,7 @@ func Test_GetPluginTablesInfo(t *testing.T) {
8687
checker.FeedIn("starrocks", starrocks.StarRocks{}.GetTablesInfo)
8788
checker.FeedIn("tapd/models", tapd.Tapd{}.GetTablesInfo)
8889
checker.FeedIn("teambition/models", teambition.Teambition{}.GetTablesInfo)
90+
checker.FeedIn("testmo/models", testmo.Testmo{}.GetTablesInfo)
8991
checker.FeedIn("trello/models", trello.Trello{}.GetTablesInfo)
9092
checker.FeedIn("webhook/models", webhook.Webhook{}.GetTablesInfo)
9193
checker.FeedIn("zentao/models", zentao.Zentao{}.GetTablesInfo)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
// Blueprint functionality is temporarily disabled due to outdated plugin types
21+
// This file contains blueprint functionality which appears to use outdated plugin types.
22+
// TODO: Update to newer blueprint patterns once available
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/errors"
22+
coreModels "github.com/apache/incubator-devlake/core/models"
23+
"github.com/apache/incubator-devlake/core/plugin"
24+
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
25+
"github.com/apache/incubator-devlake/helpers/srvhelper"
26+
"github.com/apache/incubator-devlake/plugins/testmo/models"
27+
"github.com/apache/incubator-devlake/plugins/testmo/tasks"
28+
)
29+
30+
func MakeDataSourcePipelinePlanV200(
31+
subtaskMetas []plugin.SubTaskMeta,
32+
connectionId uint64,
33+
bpScopes []*coreModels.BlueprintScope,
34+
) (coreModels.PipelinePlan, []plugin.Scope, errors.Error) {
35+
connection, err := dsHelper.ConnSrv.FindByPk(connectionId)
36+
if err != nil {
37+
return nil, nil, err
38+
}
39+
scopeDetails, err := dsHelper.ScopeSrv.MapScopeDetails(connectionId, bpScopes)
40+
if err != nil {
41+
return nil, nil, err
42+
}
43+
plan, err := makePipelinePlanV200(subtaskMetas, scopeDetails, connection)
44+
if err != nil {
45+
return nil, nil, err
46+
}
47+
scopes, err := makeScopesV200(scopeDetails, connection)
48+
return plan, scopes, err
49+
}
50+
51+
func makePipelinePlanV200(
52+
subtaskMetas []plugin.SubTaskMeta,
53+
scopeDetails []*srvhelper.ScopeDetail[models.TestmoProject, models.TestmoScopeConfig],
54+
connection *models.TestmoConnection,
55+
) (coreModels.PipelinePlan, errors.Error) {
56+
plan := make(coreModels.PipelinePlan, len(scopeDetails))
57+
for i, scopeDetail := range scopeDetails {
58+
stage := plan[i]
59+
if stage == nil {
60+
stage = coreModels.PipelineStage{}
61+
}
62+
63+
scope, scopeConfig := scopeDetail.Scope, scopeDetail.ScopeConfig
64+
// construct task options for testmo
65+
task, err := api.MakePipelinePlanTask(
66+
"testmo",
67+
subtaskMetas,
68+
scopeConfig.Entities,
69+
tasks.TestmoOptions{
70+
ConnectionId: connection.ID,
71+
ProjectId: scope.Id,
72+
ScopeConfig: scopeConfig,
73+
},
74+
)
75+
if err != nil {
76+
return nil, err
77+
}
78+
stage = append(stage, task)
79+
plan[i] = stage
80+
}
81+
82+
return plan, nil
83+
}
84+
85+
func makeScopesV200(
86+
scopeDetails []*srvhelper.ScopeDetail[models.TestmoProject, models.TestmoScopeConfig],
87+
connection *models.TestmoConnection,
88+
) ([]plugin.Scope, errors.Error) {
89+
scopes := make([]plugin.Scope, 0, len(scopeDetails))
90+
91+
for _, scopeDetail := range scopeDetails {
92+
scope := scopeDetail.Scope
93+
// Return the TestmoProject itself as it implements plugin.Scope
94+
scopes = append(scopes, scope)
95+
}
96+
97+
return scopes, nil
98+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"context"
22+
"net/http"
23+
24+
"github.com/apache/incubator-devlake/core/errors"
25+
"github.com/apache/incubator-devlake/core/plugin"
26+
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
27+
"github.com/apache/incubator-devlake/plugins/testmo/models"
28+
)
29+
30+
func testConnection(ctx context.Context, connection models.TestmoConn) (*plugin.ApiResourceOutput, errors.Error) {
31+
if vld != nil {
32+
if err := vld.Struct(connection); err != nil {
33+
return nil, errors.Default.Wrap(err, "error validating target")
34+
}
35+
}
36+
apiClient, err := helper.NewApiClientFromConnection(ctx, basicRes, &connection)
37+
if err != nil {
38+
return nil, err
39+
}
40+
response, err := apiClient.Get("projects", nil, nil)
41+
if err != nil {
42+
return nil, err
43+
}
44+
if response.StatusCode == http.StatusUnauthorized {
45+
return nil, errors.HttpStatus(http.StatusBadRequest).New("StatusUnauthorized error while testing connection")
46+
}
47+
if response.StatusCode == http.StatusOK {
48+
return &plugin.ApiResourceOutput{Body: nil, Status: http.StatusOK}, nil
49+
}
50+
return &plugin.ApiResourceOutput{Body: nil, Status: response.StatusCode}, errors.HttpStatus(response.StatusCode).Wrap(err, "could not validate connection")
51+
}
52+
53+
func TestConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
54+
var connection models.TestmoConn
55+
err := helper.Decode(input.Body, &connection, vld)
56+
if err != nil {
57+
return nil, err
58+
}
59+
testConnectionResult, testConnectionErr := testConnection(context.TODO(), connection)
60+
if testConnectionErr != nil {
61+
return nil, plugin.WrapTestConnectionErrResp(basicRes, testConnectionErr)
62+
}
63+
return testConnectionResult, nil
64+
}
65+
66+
// TestExistingConnection test testmo connection
67+
// @Summary test testmo connection
68+
// @Description Test Testmo Connection
69+
// @Tags plugins/testmo
70+
// @Param connectionId path int true "connection ID"
71+
// @Success 200 {object} shared.ApiBody "Success"
72+
// @Failure 400 {string} errcode.Error "Bad Request"
73+
// @Failure 500 {string} errcode.Error "Internal Error"
74+
// @Router /plugins/testmo/connections/{connectionId}/test [POST]
75+
func TestExistingConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
76+
connection, err := dsHelper.ConnApi.GetMergedConnection(input)
77+
if err != nil {
78+
return nil, errors.BadInput.Wrap(err, "find connection from db")
79+
}
80+
if err := helper.DecodeMapStruct(input.Body, connection, false); err != nil {
81+
return nil, err
82+
}
83+
testConnectionResult, testConnectionErr := testConnection(context.TODO(), connection.TestmoConn)
84+
if testConnectionErr != nil {
85+
return nil, plugin.WrapTestConnectionErrResp(basicRes, testConnectionErr)
86+
}
87+
return testConnectionResult, nil
88+
}
89+
90+
// Connection CRUD operations
91+
func PostConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
92+
return dsHelper.ConnApi.Post(input)
93+
}
94+
95+
func PatchConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
96+
return dsHelper.ConnApi.Patch(input)
97+
}
98+
99+
func DeleteConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
100+
return dsHelper.ConnApi.Delete(input)
101+
}
102+
103+
func ListConnections(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
104+
return dsHelper.ConnApi.GetAll(input)
105+
}
106+
107+
func GetConnection(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
108+
return dsHelper.ConnApi.GetDetail(input)
109+
}

backend/plugins/testmo/api/init.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
Licensed to the Apache Software Foundation (ASF) under one or more
3+
contributor license agreements. See the NOTICE file distributed with
4+
this work for additional information regarding copyright ownership.
5+
The ASF licenses this file to You under the Apache License, Version 2.0
6+
(the "License"); you may not use this file except in compliance with
7+
the License. You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package api
19+
20+
import (
21+
"github.com/apache/incubator-devlake/core/context"
22+
"github.com/apache/incubator-devlake/core/plugin"
23+
"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
24+
"github.com/apache/incubator-devlake/plugins/testmo/models"
25+
"github.com/go-playground/validator/v10"
26+
)
27+
28+
var vld *validator.Validate
29+
var basicRes context.BasicRes
30+
31+
var dsHelper *api.DsHelper[models.TestmoConnection, models.TestmoProject, models.TestmoScopeConfig]
32+
var raProxy *api.DsRemoteApiProxyHelper[models.TestmoConnection]
33+
var raScopeList *api.DsRemoteApiScopeListHelper[models.TestmoConnection, models.TestmoProject, TestmoRemotePagination]
34+
var raScopeSearch *api.DsRemoteApiScopeSearchHelper[models.TestmoConnection, models.TestmoProject]
35+
36+
func Init(br context.BasicRes, p plugin.PluginMeta) {
37+
vld = validator.New()
38+
basicRes = br
39+
dsHelper = api.NewDataSourceHelper[
40+
models.TestmoConnection, models.TestmoProject, models.TestmoScopeConfig,
41+
](
42+
br,
43+
p.Name(),
44+
[]string{"name"},
45+
func(c models.TestmoConnection) models.TestmoConnection {
46+
return c.Sanitize()
47+
},
48+
nil,
49+
nil,
50+
)
51+
raProxy = api.NewDsRemoteApiProxyHelper[models.TestmoConnection](dsHelper.ConnApi.ModelApiHelper)
52+
raScopeList = api.NewDsRemoteApiScopeListHelper[models.TestmoConnection, models.TestmoProject, TestmoRemotePagination](raProxy, listTestmoRemoteScopes)
53+
raScopeSearch = api.NewDsRemoteApiScopeSearchHelper[models.TestmoConnection, models.TestmoProject](raProxy, searchTestmoRemoteScopes)
54+
}

0 commit comments

Comments
 (0)