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

Reply via email to