Skip to content

Commit 5704ee6

Browse files
authored
add multiple testing statistics APIs (#4388)
* add multiple APIs for test statistics Signed-off-by: Min Min <[email protected]> * add missing files Signed-off-by: Min Min <[email protected]> * add empty string support for array Signed-off-by: Min Min <[email protected]> --------- Signed-off-by: Min Min <[email protected]>
1 parent c6e83f1 commit 5704ee6

File tree

5 files changed

+290
-0
lines changed

5 files changed

+290
-0
lines changed

pkg/microservice/aslan/core/common/repository/mongodb/workflow_task_v4.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ type ListWorkflowTaskV4Option struct {
3737
ProjectName string
3838
ProjectNames []string
3939
WorkflowNames []string
40+
Type config.CustomWorkflowTaskType
4041
CreateTime int64
4142
BeforeCreatTime bool
4243
Limit int
@@ -118,6 +119,12 @@ func (c *WorkflowTaskv4Coll) List(opt *ListWorkflowTaskV4Option) ([]*models.Work
118119
if opt.ProjectName != "" {
119120
query["project_name"] = opt.ProjectName
120121
}
122+
if len(opt.ProjectNames) > 0 {
123+
query["project_name"] = bson.M{"$in": opt.ProjectNames}
124+
}
125+
if opt.Type != "" {
126+
query["type"] = opt.Type
127+
}
121128
query["is_archived"] = false
122129
query["is_deleted"] = false
123130
if opt.CreateTime > 0 {

pkg/microservice/aslan/core/stat/handler/router.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ func (*Router) Inject(router *gin.RouterGroup) {
105105
deployV2.GET("/service/failure", GetTopDeployFailuresByService)
106106
}
107107

108+
testV2 := qualityV2.Group("test")
109+
{
110+
testV2.GET("/count", GetTestCount)
111+
testV2.POST("/dailyHealthTrend", GetDailyTestHealthTrend)
112+
testV2.GET("/recentTask", GetRecentTestTask)
113+
}
114+
108115
}
109116

110117
type OpenAPIRouter struct{}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Copyright 2025 The KodeRover Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package handler
18+
19+
import (
20+
"strconv"
21+
22+
"github.com/gin-gonic/gin"
23+
24+
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/stat/service"
25+
internalhandler "github.com/koderover/zadig/v2/pkg/shared/handler"
26+
e "github.com/koderover/zadig/v2/pkg/tool/errors"
27+
)
28+
29+
// filterEmptyStrings removes empty strings from a slice
30+
// This handles the case where frontend passes projects= to mean "all projects"
31+
func filterEmptyStrings(slice []string) []string {
32+
result := make([]string, 0, len(slice))
33+
for _, s := range slice {
34+
if s != "" {
35+
result = append(result, s)
36+
}
37+
}
38+
return result
39+
}
40+
41+
type GetTestCountResp struct {
42+
Count int `json:"count"`
43+
}
44+
45+
func GetTestCount(c *gin.Context) {
46+
ctx := internalhandler.NewContext(c)
47+
defer func() { internalhandler.JSONResponse(c, ctx) }()
48+
49+
// Get projects from query parameters
50+
// Filter out empty strings (frontend passes projects= to mean "all projects")
51+
projects := filterEmptyStrings(c.QueryArray("projects"))
52+
53+
resp, err := service.GetTestCount(projects, ctx.Logger)
54+
if err != nil {
55+
ctx.RespErr = e.ErrGetTestCount.AddErr(err)
56+
return
57+
}
58+
59+
ctx.Resp = &GetTestCountResp{
60+
Count: resp,
61+
}
62+
}
63+
64+
type GetDailyTestHealthTrendReq struct {
65+
StartTime int64 `json:"start_time"`
66+
EndTime int64 `json:"end_time"`
67+
Projects []string `json:"projects"`
68+
}
69+
70+
func GetDailyTestHealthTrend(c *gin.Context) {
71+
ctx := internalhandler.NewContext(c)
72+
defer func() { internalhandler.JSONResponse(c, ctx) }()
73+
74+
//params validate
75+
args := new(GetDailyTestHealthTrendReq)
76+
if err := c.BindJSON(args); err != nil {
77+
ctx.RespErr = e.ErrInvalidParam.AddErr(err)
78+
return
79+
}
80+
81+
ctx.Resp, ctx.RespErr = service.GetDailyTestHealthTrend(args.StartTime, args.EndTime, args.Projects, ctx.Logger)
82+
}
83+
84+
func GetRecentTestTask(c *gin.Context) {
85+
ctx := internalhandler.NewContext(c)
86+
defer func() { internalhandler.JSONResponse(c, ctx) }()
87+
88+
// Get projects from query parameters
89+
// Filter out empty strings (frontend passes projects= to mean "all projects")
90+
projects := filterEmptyStrings(c.QueryArray("projects"))
91+
92+
// Get number from query parameter, default to 10
93+
number := 10
94+
if numStr := c.Query("number"); numStr != "" {
95+
if num, err := strconv.Atoi(numStr); err == nil && num > 0 {
96+
number = num
97+
}
98+
}
99+
100+
ctx.Resp, ctx.RespErr = service.GetRecentTestTask(projects, number, ctx.Logger)
101+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
Copyright 2025 The KodeRover Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package service
18+
19+
import (
20+
"fmt"
21+
"sort"
22+
23+
"go.uber.org/zap"
24+
25+
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
26+
commonrepo "github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/mongodb"
27+
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/stat/repository/mongodb"
28+
e "github.com/koderover/zadig/v2/pkg/tool/errors"
29+
)
30+
31+
func GetTestCount(projects []string, logger *zap.SugaredLogger) (int, error) {
32+
// if projects is empty, get all testings
33+
if len(projects) == 0 {
34+
testings, err := commonrepo.NewTestingColl().List(&commonrepo.ListTestOption{})
35+
if err != nil {
36+
logger.Errorf("TestingModule.List error: %v", err)
37+
return 0, e.ErrListTestModule.AddDesc(err.Error())
38+
}
39+
return len(testings), nil
40+
}
41+
42+
// otherwise, get testings for the specified projects
43+
testings, err := commonrepo.NewTestingColl().List(&commonrepo.ListTestOption{ProductNames: projects})
44+
if err != nil {
45+
logger.Errorf("TestingModule.List error: %v", err)
46+
return 0, e.ErrListTestModule.AddDesc(err.Error())
47+
}
48+
return len(testings), nil
49+
}
50+
51+
type DailyTestHealthStat struct {
52+
Date string `json:"date"`
53+
TotalSuccess int `json:"totalSuccess"`
54+
TotalFailure int `json:"totalFailure"`
55+
SuccessRate float64 `json:"successRate"`
56+
}
57+
58+
func GetDailyTestHealthTrend(startDate, endDate int64, projects []string, logger *zap.SugaredLogger) ([]*DailyTestHealthStat, error) {
59+
// Get test stats from database
60+
testStats, err := mongodb.NewTestStatColl().ListTestStat(&mongodb.TestStatOption{
61+
StartDate: startDate,
62+
EndDate: endDate,
63+
IsAsc: true,
64+
ProductNames: projects,
65+
})
66+
if err != nil {
67+
logger.Errorf("ListTestStat error: %v", err)
68+
return nil, fmt.Errorf("ListTestStat error: %v", err)
69+
}
70+
71+
// Group by date
72+
dailyStatMap := make(map[string]*DailyTestHealthStat)
73+
for _, testStat := range testStats {
74+
dateKey := testStat.Date
75+
76+
if _, exists := dailyStatMap[dateKey]; !exists {
77+
dailyStatMap[dateKey] = &DailyTestHealthStat{
78+
Date: dateKey,
79+
TotalSuccess: 0,
80+
TotalFailure: 0,
81+
}
82+
}
83+
84+
dailyStatMap[dateKey].TotalSuccess += testStat.TotalSuccess
85+
dailyStatMap[dateKey].TotalFailure += testStat.TotalFailure
86+
}
87+
88+
// Convert map to sorted array
89+
dateKeys := make([]string, 0, len(dailyStatMap))
90+
for dateKey := range dailyStatMap {
91+
dateKeys = append(dateKeys, dateKey)
92+
}
93+
sort.Strings(dateKeys)
94+
95+
result := make([]*DailyTestHealthStat, 0, len(dateKeys))
96+
for _, dateKey := range dateKeys {
97+
stat := dailyStatMap[dateKey]
98+
totalTests := stat.TotalSuccess + stat.TotalFailure
99+
if totalTests > 0 {
100+
stat.SuccessRate = float64(stat.TotalSuccess) / float64(totalTests) * 100
101+
}
102+
result = append(result, stat)
103+
}
104+
105+
return result, nil
106+
}
107+
108+
type RecentTestTask struct {
109+
TaskID int64 `json:"task_id"`
110+
TaskCreator string `json:"task_creator"`
111+
ProductName string `json:"product_name"`
112+
TestName string `json:"test_name"`
113+
WorkflowName string `json:"workflow_name"`
114+
Status config.Status `json:"status"`
115+
CreateTime int64 `json:"create_time"`
116+
StartTime int64 `json:"start_time"`
117+
EndTime int64 `json:"end_time"`
118+
}
119+
120+
func GetRecentTestTask(projects []string, number int, logger *zap.SugaredLogger) ([]*RecentTestTask, error) {
121+
// Default to 10 if number is not specified or invalid
122+
if number <= 0 {
123+
number = 10
124+
}
125+
126+
// Query workflow tasks filtered by testing type and projects at database level
127+
option := &commonrepo.ListWorkflowTaskV4Option{
128+
Type: config.WorkflowTaskTypeTesting, // Filter by testing type in database
129+
Limit: number,
130+
IsSort: true,
131+
}
132+
133+
// If projects are specified, filter by them
134+
if len(projects) > 0 {
135+
option.ProjectNames = projects
136+
}
137+
138+
workflowTasks, _, err := commonrepo.NewworkflowTaskv4Coll().List(option)
139+
if err != nil {
140+
logger.Errorf("failed to list workflow tasks, error: %s", err)
141+
return nil, fmt.Errorf("failed to list workflow tasks, error: %s", err)
142+
}
143+
144+
// Build response
145+
recentTasks := make([]*RecentTestTask, 0, len(workflowTasks))
146+
for _, task := range workflowTasks {
147+
// Extract test name from workflow display name
148+
testName := task.WorkflowDisplayName
149+
if testName == "" && task.WorkflowArgs != nil {
150+
testName = task.WorkflowArgs.DisplayName
151+
}
152+
if testName == "" {
153+
testName = task.WorkflowName
154+
}
155+
156+
recentTasks = append(recentTasks, &RecentTestTask{
157+
TaskID: task.TaskID,
158+
TaskCreator: task.TaskCreator,
159+
ProductName: task.ProjectName,
160+
TestName: testName,
161+
WorkflowName: task.WorkflowName,
162+
Status: task.Status,
163+
CreateTime: task.CreateTime,
164+
StartTime: task.StartTime,
165+
EndTime: task.EndTime,
166+
})
167+
}
168+
169+
return recentTasks, nil
170+
}

pkg/tool/errors/http_errors.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,4 +990,9 @@ var (
990990
//-----------------------------------------------------------------------------------------------
991991
ErrCreateTempFile = NewHTTPError(7140, "创建临时文件失败")
992992
ErrUpdateTempFile = NewHTTPError(7141, "更新临时文件失败")
993+
994+
//-----------------------------------------------------------------------------------------------
995+
// test stat releated errors: 7160 - 7169
996+
//-----------------------------------------------------------------------------------------------
997+
ErrGetTestCount = NewHTTPError(7160, "获取测试计数失败")
993998
)

0 commit comments

Comments
 (0)