This is an automated email from the ASF dual-hosted git repository.
klesh 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 d8436cd4b fix(plugins): replace invalid tests with runs in testmo
(#8530)
d8436cd4b is described below
commit d8436cd4b8c93b9acb883d5234c11228c52c9132
Author: Richard Boisvert <[email protected]>
AuthorDate: Mon Aug 18 04:32:12 2025 -0400
fix(plugins): replace invalid tests with runs in testmo (#8530)
Replaces the previous test collection approach with a more reliable
runs-based data collection from the Testmo API.
Backend Changes:
- Replaced test collection with runs collection: Changed from
/automation/runs/{id}/tests to /runs endpoint for simpler, more reliable data
fetching
- Updated data models: Renamed TestmoTest → TestmoRun and removed unused
Key field to match actual API response
- Fixed data extraction issues: Added missing Params in extractor and
improved timestamp handling for NULL values
Dashboard Changes:
- Enhanced Grafana dashboard: Updated all panels to include both automation
runs and test runs data sources
---
backend/plugins/testmo/impl/impl.go | 9 +-
.../20250808_replace_tests_with_runs.go | 64 ++++++
.../{test.go => migrationscripts/archived/run.go} | 37 ++--
.../testmo/models/migrationscripts/register.go | 1 +
backend/plugins/testmo/models/{test.go => run.go} | 29 ++-
.../testmo/tasks/automation_run_converter.go | 14 +-
...ation_run_converter.go => project_converter.go} | 30 ++-
backend/plugins/testmo/tasks/run_collector.go | 76 +++++++
.../tasks/{test_converter.go => run_converter.go} | 62 +++---
backend/plugins/testmo/tasks/run_extractor.go | 156 +++++++++++++
backend/plugins/testmo/tasks/test_collector.go | 114 ----------
backend/plugins/testmo/tasks/test_extractor.go | 126 -----------
config-ui/src/routes/pipeline/components/task.tsx | 3 -
grafana/dashboards/Testmo.json | 241 ++++++++++++++++-----
14 files changed, 566 insertions(+), 396 deletions(-)
diff --git a/backend/plugins/testmo/impl/impl.go
b/backend/plugins/testmo/impl/impl.go
index 68de13212..9ef23f9c4 100644
--- a/backend/plugins/testmo/impl/impl.go
+++ b/backend/plugins/testmo/impl/impl.go
@@ -69,7 +69,7 @@ func (p Testmo) GetTablesInfo() []dal.Tabler {
&models.TestmoProject{},
&models.TestmoScopeConfig{},
&models.TestmoAutomationRun{},
- &models.TestmoTest{},
+ &models.TestmoRun{},
&models.TestmoMilestone{},
}
}
@@ -90,10 +90,11 @@ func (p Testmo) SubTaskMetas() []plugin.SubTaskMeta {
tasks.ExtractMilestonesMeta,
tasks.CollectAutomationRunsMeta,
tasks.ExtractAutomationRunsMeta,
- tasks.CollectTestsMeta,
- tasks.ExtractTestsMeta,
+ tasks.CollectRunsMeta,
+ tasks.ExtractRunsMeta,
+ tasks.ConvertProjectsMeta,
tasks.ConvertAutomationRunsMeta,
- tasks.ConvertTestsMeta,
+ tasks.ConvertRunsMeta,
}
}
diff --git
a/backend/plugins/testmo/models/migrationscripts/20250808_replace_tests_with_runs.go
b/backend/plugins/testmo/models/migrationscripts/20250808_replace_tests_with_runs.go
new file mode 100644
index 000000000..3d7a6d7bd
--- /dev/null
+++
b/backend/plugins/testmo/models/migrationscripts/20250808_replace_tests_with_runs.go
@@ -0,0 +1,64 @@
+/*
+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 migrationscripts
+
+import (
+ "github.com/apache/incubator-devlake/core/context"
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ "github.com/apache/incubator-devlake/helpers/migrationhelper"
+
"github.com/apache/incubator-devlake/plugins/testmo/models/migrationscripts/archived"
+)
+
+type replaceTestsWithRuns struct{}
+
+func (*replaceTestsWithRuns) Up(basicRes context.BasicRes) errors.Error {
+ db := basicRes.GetDal()
+
+ // Check if the old tool layer tests table exists and drop it if it does
+ if db.HasTable("_tool_testmo_tests") {
+ err := db.DropTables("_tool_testmo_tests")
+ if err != nil {
+ return err
+ }
+ }
+
+ // Check if the old raw tests table exists and drop it if it does
+ if db.HasTable("_raw_testmo_tests") {
+ err := db.DropTables("_raw_testmo_tests")
+ if err != nil {
+ return err
+ }
+ }
+
+ // Create the new runs table
+ return migrationhelper.AutoMigrateTables(
+ basicRes,
+ &archived.TestmoRun{},
+ )
+}
+
+func (*replaceTestsWithRuns) Version() uint64 {
+ return 20250808000001
+}
+
+func (*replaceTestsWithRuns) Name() string {
+ return "Replace testmo tests tables with runs table (both tool and raw
layers)"
+}
+
+var _ plugin.MigrationScript = (*replaceTestsWithRuns)(nil)
diff --git a/backend/plugins/testmo/models/test.go
b/backend/plugins/testmo/models/migrationscripts/archived/run.go
similarity index 54%
copy from backend/plugins/testmo/models/test.go
copy to backend/plugins/testmo/models/migrationscripts/archived/run.go
index 0355be0c7..db1cba3e6 100644
--- a/backend/plugins/testmo/models/test.go
+++ b/backend/plugins/testmo/models/migrationscripts/archived/run.go
@@ -15,28 +15,23 @@ See the License for the specific language governing
permissions and
limitations under the License.
*/
-package models
+package archived
import (
"time"
-
- "github.com/apache/incubator-devlake/core/models/common"
)
-type TestmoTest struct {
- ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
- Id uint64 `gorm:"primaryKey;type:BIGINT NOT
NULL;autoIncrement:false" json:"id"`
- ProjectId uint64 `gorm:"index;type:BIGINT NOT NULL"
json:"project_id"`
- AutomationRunId uint64 `gorm:"index;type:BIGINT NOT NULL"
json:"automation_run_id"`
- ThreadId uint64 `json:"thread_id"`
- Name string `gorm:"type:varchar(500)" json:"name"`
- Key string `gorm:"type:varchar(255)" json:"key"`
- Status int32 `json:"status"`
- StatusName string `gorm:"type:varchar(100)" json:"status_name"`
- Elapsed *int64 `json:"elapsed"`
- Message string `gorm:"type:text" json:"message"`
-
- // Test classification
+type TestmoRun struct {
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Id uint64 `gorm:"primaryKey;type:BIGINT NOT
NULL;autoIncrement:false" json:"id"`
+ ProjectId uint64 `gorm:"index;type:BIGINT NOT NULL"
json:"project_id"`
+ Name string `gorm:"type:varchar(500)" json:"name"`
+ Status int32 `json:"status"`
+ StatusName string `gorm:"type:varchar(100)" json:"status_name"`
+ Elapsed *int64 `json:"elapsed"`
+ Message string `gorm:"type:text" json:"message"`
+
+ // Run classification
IsAcceptanceTest bool `gorm:"index" json:"is_acceptance_test"`
IsSmokeTest bool `gorm:"index" json:"is_smoke_test"`
Team string `gorm:"type:varchar(255);index" json:"team"`
@@ -45,9 +40,11 @@ type TestmoTest struct {
TestmoCreatedAt *time.Time `json:"created_at"`
TestmoUpdatedAt *time.Time `json:"updated_at"`
- common.NoPKModel
+ // Inline definition of NoPKModel
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
}
-func (TestmoTest) TableName() string {
- return "_tool_testmo_tests"
+func (TestmoRun) TableName() string {
+ return "_tool_testmo_runs"
}
diff --git a/backend/plugins/testmo/models/migrationscripts/register.go
b/backend/plugins/testmo/models/migrationscripts/register.go
index 617f5e167..3ab9cc075 100644
--- a/backend/plugins/testmo/models/migrationscripts/register.go
+++ b/backend/plugins/testmo/models/migrationscripts/register.go
@@ -23,5 +23,6 @@ func All() []plugin.MigrationScript {
return []plugin.MigrationScript{
new(addInitTables),
new(addScopeConfigIdToProjects),
+ new(replaceTestsWithRuns),
}
}
diff --git a/backend/plugins/testmo/models/test.go
b/backend/plugins/testmo/models/run.go
similarity index 59%
rename from backend/plugins/testmo/models/test.go
rename to backend/plugins/testmo/models/run.go
index 0355be0c7..42f7b9ab1 100644
--- a/backend/plugins/testmo/models/test.go
+++ b/backend/plugins/testmo/models/run.go
@@ -23,20 +23,17 @@ import (
"github.com/apache/incubator-devlake/core/models/common"
)
-type TestmoTest struct {
- ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
- Id uint64 `gorm:"primaryKey;type:BIGINT NOT
NULL;autoIncrement:false" json:"id"`
- ProjectId uint64 `gorm:"index;type:BIGINT NOT NULL"
json:"project_id"`
- AutomationRunId uint64 `gorm:"index;type:BIGINT NOT NULL"
json:"automation_run_id"`
- ThreadId uint64 `json:"thread_id"`
- Name string `gorm:"type:varchar(500)" json:"name"`
- Key string `gorm:"type:varchar(255)" json:"key"`
- Status int32 `json:"status"`
- StatusName string `gorm:"type:varchar(100)" json:"status_name"`
- Elapsed *int64 `json:"elapsed"`
- Message string `gorm:"type:text" json:"message"`
-
- // Test classification
+type TestmoRun struct {
+ ConnectionId uint64 `gorm:"primaryKey;type:BIGINT NOT NULL"`
+ Id uint64 `gorm:"primaryKey;type:BIGINT NOT
NULL;autoIncrement:false" json:"id"`
+ ProjectId uint64 `gorm:"index;type:BIGINT NOT NULL"
json:"project_id"`
+ Name string `gorm:"type:varchar(500)" json:"name"`
+ Status int32 `json:"status"`
+ StatusName string `gorm:"type:varchar(100)" json:"status_name"`
+ Elapsed *int64 `json:"elapsed"`
+ Message string `gorm:"type:text" json:"message"`
+
+ // Run classification
IsAcceptanceTest bool `gorm:"index" json:"is_acceptance_test"`
IsSmokeTest bool `gorm:"index" json:"is_smoke_test"`
Team string `gorm:"type:varchar(255);index" json:"team"`
@@ -48,6 +45,6 @@ type TestmoTest struct {
common.NoPKModel
}
-func (TestmoTest) TableName() string {
- return "_tool_testmo_tests"
+func (TestmoRun) TableName() string {
+ return "_tool_testmo_runs"
}
diff --git a/backend/plugins/testmo/tasks/automation_run_converter.go
b/backend/plugins/testmo/tasks/automation_run_converter.go
index 99104e0d9..2f1046f87 100644
--- a/backend/plugins/testmo/tasks/automation_run_converter.go
+++ b/backend/plugins/testmo/tasks/automation_run_converter.go
@@ -22,9 +22,6 @@ import (
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
- "github.com/apache/incubator-devlake/core/models/domainlayer"
- "github.com/apache/incubator-devlake/core/models/domainlayer/didgen"
- "github.com/apache/incubator-devlake/core/models/domainlayer/qa"
"github.com/apache/incubator-devlake/core/plugin"
helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
testmoModels "github.com/apache/incubator-devlake/plugins/testmo/models"
@@ -60,17 +57,8 @@ func ConvertAutomationRuns(taskCtx plugin.SubTaskContext)
errors.Error {
InputRowType:
reflect.TypeOf(testmoModels.TestmoAutomationRun{}),
Input: cursor,
Convert: func(inputRow interface{}) ([]interface{},
errors.Error) {
- run := inputRow.(*testmoModels.TestmoAutomationRun)
- // Convert to domain layer QA project (representing
test suite)
- qaProject := &qa.QaProject{
- DomainEntityExtended:
domainlayer.DomainEntityExtended{
- Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoAutomationRun{}).Generate(data.Options.ConnectionId,
run.Id),
- },
- Name: run.Name,
- }
-
- return []interface{}{qaProject}, nil
+ return []interface{}{}, nil
},
})
diff --git a/backend/plugins/testmo/tasks/automation_run_converter.go
b/backend/plugins/testmo/tasks/project_converter.go
similarity index 69%
copy from backend/plugins/testmo/tasks/automation_run_converter.go
copy to backend/plugins/testmo/tasks/project_converter.go
index 99104e0d9..3bd5f9647 100644
--- a/backend/plugins/testmo/tasks/automation_run_converter.go
+++ b/backend/plugins/testmo/tasks/project_converter.go
@@ -18,6 +18,7 @@ limitations under the License.
package tasks
import (
+ "fmt"
"reflect"
"github.com/apache/incubator-devlake/core/dal"
@@ -30,19 +31,19 @@ import (
testmoModels "github.com/apache/incubator-devlake/plugins/testmo/models"
)
-var ConvertAutomationRunsMeta = plugin.SubTaskMeta{
- Name: "convertAutomationRuns",
- EntryPoint: ConvertAutomationRuns,
+var ConvertProjectsMeta = plugin.SubTaskMeta{
+ Name: "convertProjects",
+ EntryPoint: ConvertProjects,
EnabledByDefault: true,
- Description: "Convert tool layer table testmo_automation_runs into
domain layer table test_suites",
+ Description: "Convert tool layer table testmo_projects into domain
layer table qa_projects",
DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
}
-func ConvertAutomationRuns(taskCtx plugin.SubTaskContext) errors.Error {
+func ConvertProjects(taskCtx plugin.SubTaskContext) errors.Error {
data := taskCtx.GetData().(*TestmoTaskData)
db := taskCtx.GetDal()
- cursor, err := db.Cursor(dal.From(&testmoModels.TestmoAutomationRun{}),
dal.Where("connection_id = ? AND project_id = ?", data.Options.ConnectionId,
data.Options.ProjectId))
+ cursor, err := db.Cursor(dal.From(&testmoModels.TestmoProject{}),
dal.Where("connection_id = ? AND id = ?", data.Options.ConnectionId,
data.Options.ProjectId))
if err != nil {
return err
}
@@ -55,19 +56,24 @@ func ConvertAutomationRuns(taskCtx plugin.SubTaskContext)
errors.Error {
ConnectionId: data.Options.ConnectionId,
ProjectId: data.Options.ProjectId,
},
- Table: RAW_AUTOMATION_RUN_TABLE,
+ Table: RAW_PROJECT_TABLE,
},
- InputRowType:
reflect.TypeOf(testmoModels.TestmoAutomationRun{}),
+ InputRowType: reflect.TypeOf(testmoModels.TestmoProject{}),
Input: cursor,
Convert: func(inputRow interface{}) ([]interface{},
errors.Error) {
- run := inputRow.(*testmoModels.TestmoAutomationRun)
+ project := inputRow.(*testmoModels.TestmoProject)
+
+ // Convert to domain layer QA project
+ projectName := project.Name
+ if projectName == "" {
+ projectName = fmt.Sprintf("Project %d",
project.Id)
+ }
- // Convert to domain layer QA project (representing
test suite)
qaProject := &qa.QaProject{
DomainEntityExtended:
domainlayer.DomainEntityExtended{
- Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoAutomationRun{}).Generate(data.Options.ConnectionId,
run.Id),
+ Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoProject{}).Generate(data.Options.ConnectionId,
project.Id),
},
- Name: run.Name,
+ Name: projectName,
}
return []interface{}{qaProject}, nil
diff --git a/backend/plugins/testmo/tasks/run_collector.go
b/backend/plugins/testmo/tasks/run_collector.go
new file mode 100644
index 000000000..956f3f227
--- /dev/null
+++ b/backend/plugins/testmo/tasks/run_collector.go
@@ -0,0 +1,76 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+)
+
+const RAW_RUN_TABLE = "testmo_runs"
+
+var CollectRunsMeta = plugin.SubTaskMeta{
+ Name: "collectRuns",
+ EntryPoint: CollectRuns,
+ EnabledByDefault: true,
+ Description: "Collect runs data from Testmo api",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
+
+func CollectRuns(taskCtx plugin.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*TestmoTaskData)
+ logger := taskCtx.GetLogger()
+ logger.Info("collecting runs")
+
+ collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: TestmoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_RUN_TABLE,
+ },
+ ApiClient: data.ApiClient,
+ PageSize: 100,
+ Incremental: false,
+ UrlTemplate: "projects/{{ .Params.ProjectId }}/runs",
+ Query: func(reqData *helper.RequestData) (url.Values,
errors.Error) {
+ query := url.Values{}
+ query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+ query.Set("per_page", fmt.Sprintf("%v",
reqData.Pager.Size))
+ return query, nil
+ },
+ GetTotalPages: GetTotalPagesFromResponse,
+ ResponseParser: func(res *http.Response) ([]json.RawMessage,
errors.Error) {
+ return GetRawMessageFromResponse(res)
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return collector.Execute()
+}
diff --git a/backend/plugins/testmo/tasks/test_converter.go
b/backend/plugins/testmo/tasks/run_converter.go
similarity index 60%
rename from backend/plugins/testmo/tasks/test_converter.go
rename to backend/plugins/testmo/tasks/run_converter.go
index 4f8ed324e..4f91541f7 100644
--- a/backend/plugins/testmo/tasks/test_converter.go
+++ b/backend/plugins/testmo/tasks/run_converter.go
@@ -30,19 +30,19 @@ import (
testmoModels "github.com/apache/incubator-devlake/plugins/testmo/models"
)
-var ConvertTestsMeta = plugin.SubTaskMeta{
- Name: "convertTests",
- EntryPoint: ConvertTests,
+var ConvertRunsMeta = plugin.SubTaskMeta{
+ Name: "convertRuns",
+ EntryPoint: ConvertRuns,
EnabledByDefault: true,
- Description: "Convert tool layer table testmo_tests into domain
layer table test_cases",
+ Description: "Convert tool layer table testmo_runs into domain
layer table test_cases",
DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
}
-func ConvertTests(taskCtx plugin.SubTaskContext) errors.Error {
+func ConvertRuns(taskCtx plugin.SubTaskContext) errors.Error {
data := taskCtx.GetData().(*TestmoTaskData)
db := taskCtx.GetDal()
- cursor, err := db.Cursor(dal.From(&testmoModels.TestmoTest{}),
dal.Where("connection_id = ? AND project_id = ?", data.Options.ConnectionId,
data.Options.ProjectId))
+ cursor, err := db.Cursor(dal.From(&testmoModels.TestmoRun{}),
dal.Where("connection_id = ? AND project_id = ?", data.Options.ConnectionId,
data.Options.ProjectId))
if err != nil {
return err
}
@@ -55,43 +55,43 @@ func ConvertTests(taskCtx plugin.SubTaskContext)
errors.Error {
ConnectionId: data.Options.ConnectionId,
ProjectId: data.Options.ProjectId,
},
- Table: RAW_TEST_TABLE,
+ Table: RAW_RUN_TABLE,
},
- InputRowType: reflect.TypeOf(testmoModels.TestmoTest{}),
+ InputRowType: reflect.TypeOf(testmoModels.TestmoRun{}),
Input: cursor,
Convert: func(inputRow interface{}) ([]interface{},
errors.Error) {
- test := inputRow.(*testmoModels.TestmoTest)
+ run := inputRow.(*testmoModels.TestmoRun)
- // Convert to domain layer QA test case
+ // Convert to domain layer QA test case (treating runs
as test cases)
qaTestCase := &qa.QaTestCase{
DomainEntityExtended:
domainlayer.DomainEntityExtended{
- Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoTest{}).Generate(data.Options.ConnectionId,
test.Id),
+ Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoRun{}).Generate(data.Options.ConnectionId,
run.Id),
},
- Name: test.Name,
- Type: getTestType(test),
- QaProjectId:
didgen.NewDomainIdGenerator(&testmoModels.TestmoAutomationRun{}).Generate(data.Options.ConnectionId,
test.AutomationRunId),
+ Name: run.Name,
+ Type: getRunType(run),
+ QaProjectId:
didgen.NewDomainIdGenerator(&testmoModels.TestmoProject{}).Generate(data.Options.ConnectionId,
run.ProjectId),
}
- if test.TestmoCreatedAt != nil {
- qaTestCase.CreateTime = *test.TestmoCreatedAt
+ if run.TestmoCreatedAt != nil &&
!run.TestmoCreatedAt.IsZero() {
+ qaTestCase.CreateTime = *run.TestmoCreatedAt
}
// Create test case execution
qaExecution := &qa.QaTestCaseExecution{
DomainEntityExtended:
domainlayer.DomainEntityExtended{
- Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoTest{}).Generate(data.Options.ConnectionId,
test.Id) + ":execution",
+ Id:
didgen.NewDomainIdGenerator(&testmoModels.TestmoRun{}).Generate(data.Options.ConnectionId,
run.Id) + ":execution",
},
- QaProjectId:
didgen.NewDomainIdGenerator(&testmoModels.TestmoAutomationRun{}).Generate(data.Options.ConnectionId,
test.AutomationRunId),
- QaTestCaseId:
didgen.NewDomainIdGenerator(&testmoModels.TestmoTest{}).Generate(data.Options.ConnectionId,
test.Id),
- Status: convertTestStatus(test.Status),
+ QaProjectId:
didgen.NewDomainIdGenerator(&testmoModels.TestmoProject{}).Generate(data.Options.ConnectionId,
run.ProjectId),
+ QaTestCaseId:
didgen.NewDomainIdGenerator(&testmoModels.TestmoRun{}).Generate(data.Options.ConnectionId,
run.Id),
+ Status: convertRunStatus(run.Status),
}
- if test.TestmoCreatedAt != nil {
- qaExecution.CreateTime = *test.TestmoCreatedAt
- qaExecution.StartTime = *test.TestmoCreatedAt
+ if run.TestmoCreatedAt != nil &&
!run.TestmoCreatedAt.IsZero() {
+ qaExecution.CreateTime = *run.TestmoCreatedAt
+ qaExecution.StartTime = *run.TestmoCreatedAt
}
- if test.TestmoUpdatedAt != nil {
- qaExecution.FinishTime = *test.TestmoUpdatedAt
+ if run.TestmoUpdatedAt != nil &&
!run.TestmoUpdatedAt.IsZero() {
+ qaExecution.FinishTime = *run.TestmoUpdatedAt
}
return []interface{}{qaTestCase, qaExecution}, nil
@@ -105,8 +105,8 @@ func ConvertTests(taskCtx plugin.SubTaskContext)
errors.Error {
return converter.Execute()
}
-func convertTestStatus(status int32) string {
- // Testmo status mapping - this may need adjustment based on actual
Testmo status values
+func convertRunStatus(status int32) string {
+ // Testmo run status mapping
switch status {
case 1:
return "SUCCESS"
@@ -119,12 +119,12 @@ func convertTestStatus(status int32) string {
}
}
-func getTestType(test *testmoModels.TestmoTest) string {
- if test.IsAcceptanceTest {
+func getRunType(run *testmoModels.TestmoRun) string {
+ if run.IsAcceptanceTest {
return "functional"
}
- if test.IsSmokeTest {
- return "functional"
+ if run.IsSmokeTest {
+ return "smoke"
}
return "functional"
}
diff --git a/backend/plugins/testmo/tasks/run_extractor.go
b/backend/plugins/testmo/tasks/run_extractor.go
new file mode 100644
index 000000000..95c37f2b3
--- /dev/null
+++ b/backend/plugins/testmo/tasks/run_extractor.go
@@ -0,0 +1,156 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package tasks
+
+import (
+ "encoding/json"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/apache/incubator-devlake/core/errors"
+ "github.com/apache/incubator-devlake/core/plugin"
+ helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
+ "github.com/apache/incubator-devlake/plugins/testmo/models"
+)
+
+var ExtractRunsMeta = plugin.SubTaskMeta{
+ Name: "extractRuns",
+ EntryPoint: ExtractRuns,
+ EnabledByDefault: true,
+ Description: "Extract raw runs data into tool layer table
testmo_runs",
+ DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
+}
+
+func ExtractRuns(taskCtx plugin.SubTaskContext) errors.Error {
+ data := taskCtx.GetData().(*TestmoTaskData)
+ extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+ RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+ Ctx: taskCtx,
+ Params: TestmoApiParams{
+ ConnectionId: data.Options.ConnectionId,
+ ProjectId: data.Options.ProjectId,
+ },
+ Table: RAW_RUN_TABLE,
+ },
+ Extract: func(row *helper.RawData) ([]interface{},
errors.Error) {
+ var apiRun struct {
+ Id uint64 `json:"id"`
+ ProjectId uint64 `json:"project_id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Url string `json:"url"`
+ StateId int32 `json:"state_id"`
+ MilestoneId *uint64 `json:"milestone_id"`
+ ConfigIds []uint64 `json:"config_ids"`
+ IsClosed bool `json:"is_closed"`
+ IsCompleted bool `json:"is_completed"`
+ Elapsed *int64 `json:"elapsed"`
+ CreatedAt *time.Time `json:"created_at"`
+ CreatedBy *uint64 `json:"created_by"`
+ UpdatedAt *time.Time `json:"updated_at"`
+ UpdatedBy *uint64 `json:"updated_by"`
+ ClosedAt *time.Time `json:"closed_at"`
+ ClosedBy *uint64 `json:"closed_by"`
+ }
+
+ err := json.Unmarshal(row.Data, &apiRun)
+ if err != nil {
+ return nil, errors.Default.Wrap(err, "error
unmarshaling run")
+ }
+
+ run := &models.TestmoRun{
+ ConnectionId: data.Options.ConnectionId,
+ Id: apiRun.Id,
+ ProjectId: apiRun.ProjectId,
+ Name: apiRun.Name,
+ Status: apiRun.StateId,
+ StatusName: getStatusName(apiRun.StateId),
+ Elapsed: apiRun.Elapsed,
+ Message: apiRun.Description,
+ TestmoCreatedAt: apiRun.CreatedAt,
+ TestmoUpdatedAt: apiRun.UpdatedAt,
+ }
+
+ // If TestmoUpdatedAt is null, use TestmoCreatedAt as
fallback
+ if run.TestmoUpdatedAt == nil && run.TestmoCreatedAt !=
nil {
+ run.TestmoUpdatedAt = run.TestmoCreatedAt
+ }
+
+ // Classify run types based on scope config patterns
+ if data.Options.ScopeConfig != nil {
+ run.IsAcceptanceTest = matchesPattern(run.Name,
data.Options.ScopeConfig.AcceptanceTestPattern)
+ run.IsSmokeTest = matchesPattern(run.Name,
data.Options.ScopeConfig.SmokeTestPattern)
+ run.Team = extractTeam(run.Name,
data.Options.ScopeConfig.TeamPattern)
+ }
+
+ return []interface{}{run}, nil
+ },
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return extractor.Execute()
+}
+
+func matchesPattern(runName, pattern string) bool {
+ if pattern == "" {
+ return false
+ }
+
+ return strings.Contains(strings.ToLower(runName),
strings.ToLower(pattern))
+}
+
+func extractTeam(runName, pattern string) string {
+ if pattern == "" {
+ return ""
+ }
+
+ if regex, err := regexp.Compile(pattern); err == nil {
+ matches := regex.FindStringSubmatch(runName)
+ if len(matches) > 1 {
+ return matches[1] // Return first capture group
+ }
+ }
+
+ return ""
+}
+
+func getStatusName(stateId int32) string {
+ // Map Testmo state IDs to human-readable names
+ switch stateId {
+ case 1:
+ return "Passed"
+ case 2:
+ return "Failed"
+ case 3:
+ return "Blocked"
+ case 4:
+ return "Skipped"
+ case 5:
+ return "Retest"
+ case 6:
+ return "In Progress"
+ case 7:
+ return "Active"
+ default:
+ return "Unknown"
+ }
+}
diff --git a/backend/plugins/testmo/tasks/test_collector.go
b/backend/plugins/testmo/tasks/test_collector.go
deleted file mode 100644
index 876fb1f06..000000000
--- a/backend/plugins/testmo/tasks/test_collector.go
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements. See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package tasks
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "net/url"
-
- "github.com/apache/incubator-devlake/core/dal"
- "github.com/apache/incubator-devlake/core/errors"
- "github.com/apache/incubator-devlake/core/plugin"
- helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
- "github.com/apache/incubator-devlake/plugins/testmo/models"
-)
-
-const RAW_TEST_TABLE = "testmo_tests"
-
-var CollectTestsMeta = plugin.SubTaskMeta{
- Name: "collectTests",
- EntryPoint: CollectTests,
- EnabledByDefault: true,
- Description: "Collect tests data from Testmo api",
- DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
-}
-
-func CollectTests(taskCtx plugin.SubTaskContext) errors.Error {
- data := taskCtx.GetData().(*TestmoTaskData)
- logger := taskCtx.GetLogger()
- logger.Info("collecting tests")
-
- db := taskCtx.GetDal()
-
- // Get all automation runs for this project
- var automationRuns []models.TestmoAutomationRun
- err := db.All(&automationRuns, dal.Where("connection_id = ? AND
project_id = ?", data.Options.ConnectionId, data.Options.ProjectId))
- if err != nil {
- return err
- }
-
- logger.Info("found %d automation runs to collect tests for",
len(automationRuns))
-
- // Collect tests for each automation run
- for _, run := range automationRuns {
- logger.Info("collecting tests for automation run %d", run.Id)
-
- collector, err :=
helper.NewApiCollector(helper.ApiCollectorArgs{
- RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
- Ctx: taskCtx,
- Params: TestmoApiParamsWithRun{
- TestmoApiParams: TestmoApiParams{
- ConnectionId:
data.Options.ConnectionId,
- ProjectId:
data.Options.ProjectId,
- },
- AutomationRunId: run.Id,
- },
- Table: RAW_TEST_TABLE,
- },
- ApiClient: data.ApiClient,
- PageSize: 100,
- Incremental: false,
- UrlTemplate: "projects/{{
.Params.TestmoApiParams.ProjectId }}/automation/runs/{{ .Params.AutomationRunId
}}/tests",
- Query: func(reqData *helper.RequestData) (url.Values,
errors.Error) {
- query := url.Values{}
- query.Set("page", fmt.Sprintf("%v",
reqData.Pager.Page))
- query.Set("per_page", fmt.Sprintf("%v",
reqData.Pager.Size))
- return query, nil
- },
- GetTotalPages: GetTotalPagesFromResponse,
- ResponseParser: func(res *http.Response)
([]json.RawMessage, errors.Error) {
- return GetRawMessageFromResponse(res)
- },
- AfterResponse: func(res *http.Response) errors.Error {
- if res.StatusCode == http.StatusNotFound {
- logger.Info("automation run %d has no
tests endpoint (404), skipping", run.Id)
- return helper.ErrIgnoreAndContinue
- }
- return nil
- },
- })
-
- if err != nil {
- return err
- }
-
- err = collector.Execute()
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-type TestmoApiParamsWithRun struct {
- TestmoApiParams
- AutomationRunId uint64
-}
diff --git a/backend/plugins/testmo/tasks/test_extractor.go
b/backend/plugins/testmo/tasks/test_extractor.go
deleted file mode 100644
index 1d5d313ba..000000000
--- a/backend/plugins/testmo/tasks/test_extractor.go
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements. See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You under the Apache License, Version 2.0
-(the "License"); you may not use this file except in compliance with
-the License. You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package tasks
-
-import (
- "encoding/json"
- "regexp"
- "strings"
- "time"
-
- "github.com/apache/incubator-devlake/core/errors"
- "github.com/apache/incubator-devlake/core/plugin"
- helper "github.com/apache/incubator-devlake/helpers/pluginhelper/api"
- "github.com/apache/incubator-devlake/plugins/testmo/models"
-)
-
-var ExtractTestsMeta = plugin.SubTaskMeta{
- Name: "extractTests",
- EntryPoint: ExtractTests,
- EnabledByDefault: true,
- Description: "Extract raw tests data into tool layer table
testmo_tests",
- DomainTypes: []string{plugin.DOMAIN_TYPE_CODE_QUALITY},
-}
-
-func ExtractTests(taskCtx plugin.SubTaskContext) errors.Error {
- data := taskCtx.GetData().(*TestmoTaskData)
- extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
- RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
- Ctx: taskCtx,
- Table: RAW_TEST_TABLE,
- },
- Extract: func(row *helper.RawData) ([]interface{},
errors.Error) {
- var apiTest struct {
- Id uint64 `json:"id"`
- ProjectId uint64 `json:"project_id"`
- AutomationRunId uint64
`json:"automation_run_id"`
- ThreadId uint64 `json:"thread_id"`
- Name string `json:"name"`
- Key string `json:"key"`
- Status int32 `json:"status"`
- StatusName string `json:"status_name"`
- Elapsed *int64 `json:"elapsed"`
- Message string `json:"message"`
- CreatedAt *time.Time `json:"created_at"`
- UpdatedAt *time.Time `json:"updated_at"`
- }
-
- err := json.Unmarshal(row.Data, &apiTest)
- if err != nil {
- return nil, errors.Default.Wrap(err, "error
unmarshaling test")
- }
-
- test := &models.TestmoTest{
- ConnectionId: data.Options.ConnectionId,
- Id: apiTest.Id,
- ProjectId: apiTest.ProjectId,
- AutomationRunId: apiTest.AutomationRunId,
- ThreadId: apiTest.ThreadId,
- Name: apiTest.Name,
- Key: apiTest.Key,
- Status: apiTest.Status,
- StatusName: apiTest.StatusName,
- Elapsed: apiTest.Elapsed,
- Message: apiTest.Message,
- TestmoCreatedAt: apiTest.CreatedAt,
- TestmoUpdatedAt: apiTest.UpdatedAt,
- }
-
- // Classify test types based on scope config patterns
- if data.Options.ScopeConfig != nil {
- test.IsAcceptanceTest =
matchesPattern(test.Name, data.Options.ScopeConfig.AcceptanceTestPattern)
- test.IsSmokeTest = matchesPattern(test.Name,
data.Options.ScopeConfig.SmokeTestPattern)
- test.Team = extractTeam(test.Name,
data.Options.ScopeConfig.TeamPattern)
- }
-
- return []interface{}{test}, nil
- },
- })
-
- if err != nil {
- return err
- }
-
- return extractor.Execute()
-}
-
-func matchesPattern(testName, pattern string) bool {
- if pattern == "" {
- return false
- }
-
- // Simple case-insensitive substring match for now
- // Can be enhanced to support regex patterns
- return strings.Contains(strings.ToLower(testName),
strings.ToLower(pattern))
-}
-
-func extractTeam(testName, pattern string) string {
- if pattern == "" {
- return ""
- }
-
- // Try to extract team using regex pattern
- if regex, err := regexp.Compile(pattern); err == nil {
- matches := regex.FindStringSubmatch(testName)
- if len(matches) > 1 {
- return matches[1] // Return first capture group
- }
- }
-
- return ""
-}
diff --git a/config-ui/src/routes/pipeline/components/task.tsx
b/config-ui/src/routes/pipeline/components/task.tsx
index 937a7dc49..d4a012a11 100644
--- a/config-ui/src/routes/pipeline/components/task.tsx
+++ b/config-ui/src/routes/pipeline/components/task.tsx
@@ -86,9 +86,6 @@ export const PipelineTask = ({ task }: Props) => {
case ['bamboo'].includes(config.plugin):
name = `${name}:${options.planKey}`;
break;
- case ['testmo'].includes(config.plugin):
- name = `${name}:${options.name}`;
- break;
case ['azuredevops_go'].includes(config.plugin):
name = `ado:${options.name}`;
break;
diff --git a/grafana/dashboards/Testmo.json b/grafana/dashboards/Testmo.json
index ca2bd24df..934f13e75 100644
--- a/grafana/dashboards/Testmo.json
+++ b/grafana/dashboards/Testmo.json
@@ -168,7 +168,7 @@
"metricColumn": "none",
"queryType": "randomWalk",
"rawQuery": true,
- "rawSql": "select \n count(distinct ar.id) as value\nfrom
_tool_testmo_automation_runs ar\nwhere \n
$__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})",
+ "rawSql": "select \n count(*) as value\nfrom (\n select distinct
ar.id, ar.connection_id, ar.testmo_created_at from _tool_testmo_automation_runs
ar\n union all\n select distinct r.id, r.connection_id, r.testmo_created_at
from _tool_testmo_runs r\n) combined\nwhere \n
$__timeFilter(combined.testmo_created_at)\n and combined.connection_id in
(${connection_id})",
"refId": "A"
}
],
@@ -237,7 +237,7 @@
"metricColumn": "none",
"queryType": "randomWalk",
"rawQuery": true,
- "rawSql": "select \n count(distinct case when ar.status = 2 then
ar.id else null end) * 1.0 / \n count(distinct ar.id) as value\nfrom
_tool_testmo_automation_runs ar\nwhere \n
$__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})",
+ "rawSql": "select \n count(case when combined.status in (1, 2) then
1 else null end) * 1.0 / \n count(*) as value\nfrom (\n select ar.id,
ar.connection_id, ar.testmo_created_at, ar.status from
_tool_testmo_automation_runs ar\n union all\n select r.id, r.connection_id,
r.testmo_created_at, r.status from _tool_testmo_runs r\n) combined\nwhere \n
$__timeFilter(combined.testmo_created_at)\n and combined.connection_id in
(${connection_id})",
"refId": "A"
}
],
@@ -372,7 +372,7 @@
"metricColumn": "none",
"queryType": "randomWalk",
"rawQuery": true,
- "rawSql": "SELECT\n DATE(ar.testmo_created_at) as time,\n
count(distinct case when ar.status = 2 then ar.id else null end) as \"Passed
Tests\",\n count(distinct case when ar.status = 1 then ar.id else null end) as
\"Failed Tests\",\n count(distinct case when ar.status = 0 then ar.id else
null end) as \"Skipped Tests\"\nFROM _tool_testmo_automation_runs ar\nwhere \n
$__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})\ngroup by DATE(ar.testmo_cr [...]
+ "rawSql": "SELECT\n DATE(combined.testmo_created_at) as time,\n
count(case when combined.status = 1 then 1 else null end) as \"Passed
Tests\",\n count(case when combined.status = 2 then 1 else null end) as
\"Failed Tests\",\n count(case when combined.status not in (1,2) then 1 else
null end) as \"Other Tests\"\nFROM (\n select ar.id, ar.connection_id,
ar.testmo_created_at, ar.status from _tool_testmo_automation_runs ar\n union
all\n select r.id, r.connection_id, r.testmo [...]
"refId": "A"
}
],
@@ -381,7 +381,7 @@
},
{
"datasource": "mysql",
- "description": "Total number of test cases available",
+ "description": "Total number of Testmo runs (new data source)",
"fieldConfig": {
"defaults": {
"color": {
@@ -392,7 +392,7 @@
"mode": "absolute",
"steps": [
{
- "color": "blue",
+ "color": "green",
"value": null
}
]
@@ -431,11 +431,11 @@
"metricColumn": "none",
"queryType": "randomWalk",
"rawQuery": true,
- "rawSql": "select \n count(distinct t.id) as value\nfrom
_tool_testmo_tests t\nwhere \n t.connection_id in (${connection_id})",
+ "rawSql": "select \n count(distinct r.id) as value\nfrom
_tool_testmo_runs r\nwhere \n $__timeFilter(r.testmo_created_at)\n and
r.connection_id in (${connection_id})",
"refId": "A"
}
],
- "title": "Total Test Cases",
+ "title": "Testmo Runs",
"type": "stat"
},
{
@@ -509,63 +509,67 @@
},
{
"datasource": "mysql",
- "description": "Distribution of test results",
+ "description": "Daily count of test runs from the new runs data source",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "Number of Runs",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
- }
- },
- "mappings": []
- },
- "overrides": [
- {
- "matcher": {
- "id": "byName",
- "options": "Passed"
},
- "properties": [
- {
- "id": "color",
- "value": {
- "mode": "fixed",
- "fixedColor": "green"
- }
- }
- ]
- },
- {
- "matcher": {
- "id": "byName",
- "options": "Failed"
+ "lineInterpolation": "linear",
+ "lineWidth": 2,
+ "pointSize": 8,
+ "scaleDistribution": {
+ "type": "linear"
},
- "properties": [
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
{
- "id": "color",
- "value": {
- "mode": "fixed",
- "fixedColor": "red"
- }
+ "color": "green",
+ "value": null
}
]
},
+ "unit": "short",
+ "decimals": 0
+ },
+ "overrides": [
{
"matcher": {
"id": "byName",
- "options": "Skipped"
+ "options": "Test Runs"
},
"properties": [
{
"id": "color",
"value": {
"mode": "fixed",
- "fixedColor": "yellow"
+ "fixedColor": "purple"
}
}
]
@@ -581,18 +585,13 @@
"id": 121,
"options": {
"legend": {
+ "calcs": [
+ "sum"
+ ],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
- "pieType": "pie",
- "reduceOptions": {
- "calcs": [
- "lastNotNull"
- ],
- "fields": "",
- "values": false
- },
"tooltip": {
"mode": "single",
"sort": "none"
@@ -601,17 +600,17 @@
"targets": [
{
"datasource": "mysql",
- "format": "table",
+ "format": "time_series",
"group": [],
"metricColumn": "none",
"queryType": "randomWalk",
"rawQuery": true,
- "rawSql": "select \n case \n when ar.status = 0 then 'Skipped'\n
when ar.status = 1 then 'Failed' \n when ar.status = 2 then 'Passed'\n
else 'Unknown'\n end as metric,\n count(distinct ar.id) as value\nfrom
_tool_testmo_automation_runs ar\nwhere \n
$__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})\ngroup by ar.status\norder by value desc",
+ "rawSql": "SELECT\n DATE(r.testmo_created_at) as time,\n count(*)
as \"Test Runs\"\nFROM _tool_testmo_runs r\nwhere \n
$__timeFilter(r.testmo_created_at)\n and r.connection_id in
(${connection_id})\ngroup by DATE(r.testmo_created_at)\norder by time",
"refId": "A"
}
],
- "title": "Test Results Distribution",
- "type": "piechart"
+ "title": "Daily Test Runs",
+ "type": "timeseries"
},
{
"datasource": "mysql",
@@ -690,7 +689,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "select \n p.name as metric,\n count(distinct ar.id) as
\"Test Runs\"\nfrom _tool_testmo_automation_runs ar\n join
_tool_testmo_projects p on ar.project_id = p.id\nwhere \n
$__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})\ngroup by p.name\norder by count(distinct ar.id) desc\nlimit
10",
+ "rawSql": "select \n p.name as metric,\n count(*) as \"Test
Runs\"\nfrom (\n select ar.id, ar.connection_id, ar.testmo_created_at,
ar.project_id from _tool_testmo_automation_runs ar\n union all\n select r.id,
r.connection_id, r.testmo_created_at, r.project_id from _tool_testmo_runs r\n)
combined\n join _tool_testmo_projects p on combined.project_id = p.id and
combined.connection_id = p.connection_id\nwhere \n
$__timeFilter(combined.testmo_created_at)\n and combined.conn [...]
"refId": "A"
}
],
@@ -803,7 +802,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "SELECT\n DATE(ar.testmo_created_at) as time,\n
count(distinct case when ar.status = 2 then ar.id else null end) * 1.0 / \n
count(distinct ar.id) as \"Success Rate\"\nFROM _tool_testmo_automation_runs
ar\nwhere \n $__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})\ngroup by DATE(ar.testmo_created_at)\norder by time",
+ "rawSql": "SELECT\n DATE(combined.testmo_created_at) as time,\n
count(case when combined.status in (1, 2) then 1 else null end) * 1.0 / \n
count(*) as \"Success Rate\"\nFROM (\n select ar.id, ar.connection_id,
ar.testmo_created_at, ar.status from _tool_testmo_automation_runs ar\n union
all\n select r.id, r.connection_id, r.testmo_created_at, r.status from
_tool_testmo_runs r\n) combined\nwhere \n
$__timeFilter(combined.testmo_created_at)\n and combined.connection_id in
[...]
"refId": "A"
}
],
@@ -895,6 +894,134 @@
"title": "Test Count Trend",
"type": "timeseries"
},
+ {
+ "datasource": "mysql",
+ "description": "Comparison between automation runs and regular test runs
over time",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "Number of Runs",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 2,
+ "pointSize": 8,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": [
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Automation Runs"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "mode": "fixed",
+ "fixedColor": "blue"
+ }
+ }
+ ]
+ },
+ {
+ "matcher": {
+ "id": "byName",
+ "options": "Test Runs"
+ },
+ "properties": [
+ {
+ "id": "color",
+ "value": {
+ "mode": "fixed",
+ "fixedColor": "orange"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "gridPos": {
+ "h": 6,
+ "w": 24,
+ "x": 0,
+ "y": 23
+ },
+ "id": 131,
+ "options": {
+ "legend": {
+ "calcs": [
+ "sum",
+ "mean"
+ ],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": "mysql",
+ "format": "time_series",
+ "group": [],
+ "metricColumn": "none",
+ "rawQuery": true,
+ "rawSql": "SELECT\n DATE(ar.testmo_created_at) as time,\n
count(distinct ar.id) as \"Automation Runs\"\nFROM _tool_testmo_automation_runs
ar\nwhere \n $__timeFilter(ar.testmo_created_at)\n and ar.connection_id in
(${connection_id})\ngroup by DATE(ar.testmo_created_at)\norder by time",
+ "refId": "A"
+ },
+ {
+ "datasource": "mysql",
+ "format": "time_series",
+ "group": [],
+ "metricColumn": "none",
+ "rawQuery": true,
+ "rawSql": "SELECT\n DATE(r.testmo_created_at) as time,\n
count(distinct r.id) as \"Test Runs\"\nFROM _tool_testmo_runs r\nwhere \n
$__timeFilter(r.testmo_created_at)\n and r.connection_id in
(${connection_id})\ngroup by DATE(r.testmo_created_at)\norder by time",
+ "refId": "B"
+ }
+ ],
+ "title": "Automation Runs vs Test Runs Comparison",
+ "type": "timeseries"
+ },
{
"datasource": "mysql",
"description": "Test runs that take the longest time to execute",
@@ -971,7 +1098,7 @@
"h": 6,
"w": 24,
"x": 0,
- "y": 23
+ "y": 29
},
"id": 125,
"options": {
@@ -994,7 +1121,7 @@
"group": [],
"metricColumn": "none",
"rawQuery": true,
- "rawSql": "select \n ar.id as \"Test Run ID\",\n p.name as
\"Project\",\n ar.name as \"Test Name\",\n case \n when ar.status = 0 then
'Skipped'\n when ar.status = 1 then 'Failed' \n when ar.status = 2 then
'Passed'\n else 'Unknown'\n end as \"Status\",\n ar.testmo_created_at as
\"Created Date\"\nfrom _tool_testmo_automation_runs ar\n join
_tool_testmo_projects p on ar.project_id = p.id\nwhere \n
$__timeFilter(ar.testmo_created_at)\n and ar.connection_id in [...]
+ "rawSql": "select \n combined.id as \"Test Run ID\",\n p.name as
\"Project\",\n combined.name as \"Test Name\",\n combined.status_name as
\"Status\",\n combined.testmo_created_at as \"Created Date\",\n
combined.source as \"Source\"\nfrom (\n select ar.id, ar.connection_id,
ar.testmo_created_at, ar.project_id, ar.name, \n case when ar.status =
1 then 'Failed' when ar.status = 2 then 'Passed' else 'Other' end as
status_name,\n 'Automation Run' as source\n [...]
"refId": "A"
}
],
@@ -1010,7 +1137,7 @@
"h": 2,
"w": 24,
"x": 0,
- "y": 29
+ "y": 35
},
"id": 130,
"options": {
@@ -1102,4 +1229,4 @@
"uid": "testmo-dashboard",
"version": 1,
"weekStart": ""
-}
+}
\ No newline at end of file