blob: 3b62f777131e57e1be935964ee4af76b8c70dae5 [file] [log] [blame]
package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/go-chi/chi/v5"
"go.skia.org/infra/go/alogin"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/query"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/perf/go/alerts"
"go.skia.org/infra/perf/go/chromeperf"
"go.skia.org/infra/perf/go/chromeperf/compat"
"go.skia.org/infra/perf/go/config"
perfgit "go.skia.org/infra/perf/go/git"
"go.skia.org/infra/perf/go/regression"
"go.skia.org/infra/perf/go/subscription"
pb "go.skia.org/infra/perf/go/subscription/proto/v1"
"go.skia.org/infra/perf/go/types"
)
const (
defaultAnomaliesRequestTimeout = time.Second * 30
)
type anomaliesApi struct {
chromeperfClient chromeperf.ChromePerfClient
loginProvider alogin.Login
perfGit perfgit.Git
subStore subscription.Store
alertStore alerts.Store
regStore regression.Store
preferLegacy bool
}
// Response object for the request from sheriff list UI.
type GetSheriffListResponse struct {
SheriffList []string `json:"sheriff_list"`
Error string `json:"error"`
}
// Request object for the request from the anomaly table UI.
type GetAnomaliesRequest struct {
Sheriff string `json:"sheriff"`
IncludeTriaged bool `json:"triaged"`
IncludeImprovements bool `json:"improvements"`
QueryCursor string `json:"anomaly_cursor"`
Host string `json:"host"`
}
// Response object for the request from the anomaly table UI.
type GetAnomaliesResponse struct {
Subscription *pb.Subscription `json:"subscription"`
// List of alerts to display.
Alerts []alerts.Alert `json:"alerts"`
// The list of anomalies.
Anomalies []chromeperf.Anomaly `json:"anomaly_list"`
// The cursor of the current query. It will be used to 'Load More' for the next query.
QueryCursor string `json:"anomaly_cursor"`
// Error message if any.
Error string `json:"error"`
}
// Request object from report page to load the anomalies from Chromeperf
type GetGroupReportRequest struct {
// A revision number.
Revison string `json:"rev"`
// Comma-separated list of urlsafe Anomaly keys.
AnomalyIDs string `json:"anomalyIDs"`
// A Buganizer bug number ID.
BugID string `json:"bugID"`
// An Anomaly Group ID
AnomalyGroupID string `json:"anomalyGroupID"`
// A hash of a group of anomaly keys.
Sid string `json:"sid"`
}
type GetGroupReportByKeysRequest struct {
// comma separated anomaly keys
Keys string `json:"keys"`
// host value to filter anomalies
Host string `json:"host"`
}
type Timerange struct {
Begin int64 `json:"begin"`
End int64 `json:"end"`
}
type GetGroupReportResponse struct {
// The list of anomalies.
Anomalies []chromeperf.Anomaly `json:"anomaly_list"`
// The state id (hash of a list of anomaly keys)
// It is used in a share-able link for a report with multiple keys.
// This is generated on Chromeperf side and returned on POST call to /alerts_skia_by_keys
StateId string `json:"sid"`
// The list of anomalies which should be checked in report page.
SelectedKeys []string `json:"selected_keys"`
// Error message if any.
Error string `json:"error"`
// List of timeranges that will let report page know in what range to render
// each graph.
TimerangeMap map[string]Timerange `json:"timerange_map"`
}
func (api anomaliesApi) RegisterHandlers(router *chi.Mux) {
// Endpoints for using Chromeperf data.
router.Get("/_/anomalies/sheriff_list", api.GetSheriffListDefault)
router.Get("/_/anomalies/anomaly_list", api.GetAnomalyListDefault)
router.Post("/_/anomalies/group_report", api.GetGroupReportDefault)
// Endpoints for using data from the instance database.
router.Get("/_/anomalies/sheriff_list_skia", api.GetSheriffList)
router.Get("/_/anomalies/anomaly_list_skia", api.GetAnomalyList)
}
func NewAnomaliesApi(loginProvider alogin.Login, chromeperfClient chromeperf.ChromePerfClient, perfGit perfgit.Git, subStore subscription.Store, alertStore alerts.Store, regStore regression.Store, preferLegacy bool) anomaliesApi {
return anomaliesApi{
loginProvider: loginProvider,
chromeperfClient: chromeperfClient,
perfGit: perfGit,
subStore: subStore,
alertStore: alertStore,
regStore: regStore,
preferLegacy: preferLegacy,
}
}
func (api anomaliesApi) GetSheriffListDefault(w http.ResponseWriter, r *http.Request) {
if api.preferLegacy {
api.GetSheriffListLegacy(w, r)
} else {
api.GetSheriffList(w, r)
}
}
func (api anomaliesApi) GetAnomalyListDefault(w http.ResponseWriter, r *http.Request) {
if api.preferLegacy {
api.GetAnomalyListLegacy(w, r)
} else {
api.GetAnomalyList(w, r)
}
}
func (api anomaliesApi) GetGroupReportDefault(w http.ResponseWriter, r *http.Request) {
if api.preferLegacy {
api.GetGroupReportLegacy(w, r)
} else {
api.GetGroupReport(w, r)
}
}
func (api anomaliesApi) GetSheriffListLegacy(w http.ResponseWriter, r *http.Request) {
if api.loginProvider.LoggedInAs(r) == "" {
httputils.ReportError(w, errors.New("Not logged in"), fmt.Sprintf("You must be logged in to complete this action."), http.StatusUnauthorized)
return
}
sklog.Debug("[SkiaTriage] Get sheriff config list request received from frontend.")
w.Header().Set("Content-Type", "application/json")
ctx, cancel := context.WithTimeout(r.Context(), defaultAnomaliesRequestTimeout)
defer cancel()
getSheriffListResponse := &GetSheriffListResponse{}
err := api.chromeperfClient.SendGetRequest(ctx, "sheriff_configs_skia", "", url.Values{}, getSheriffListResponse)
if err != nil {
httputils.ReportError(w, err, "Failed to finish get sheriff list request.", http.StatusInternalServerError)
return
}
if getSheriffListResponse.Error != "" {
httputils.ReportError(w, errors.New(getSheriffListResponse.Error), "Load sheriff list request returned error.", http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(getSheriffListResponse); err != nil {
httputils.ReportError(w, err, "Failed to write sheriff list to UI response.", http.StatusInternalServerError)
return
}
sklog.Debugf("[SkiaTriage] sheriff config list is loaded: %v", getSheriffListResponse.SheriffList)
return
}
func (api anomaliesApi) GetAnomalyListLegacy(w http.ResponseWriter, r *http.Request) {
if api.loginProvider.LoggedInAs(r) == "" {
httputils.ReportError(w, errors.New("Not logged in"), fmt.Sprintf("You must be logged in to complete this action."), http.StatusUnauthorized)
return
}
query_values := r.URL.Query()
sklog.Debugf("[SkiaTriage] Get anomalies request received from frontend: %v", query_values)
if query_values.Get("host") == "" {
query_values["host"] = []string{config.Config.URL}
}
w.Header().Set("Content-Type", "application/json")
ctx, cancel := context.WithTimeout(r.Context(), defaultAnomaliesRequestTimeout)
defer cancel()
getAnoamliesResponse := &GetAnomaliesResponse{}
err := api.chromeperfClient.SendGetRequest(ctx, "alerts_skia", "", query_values, getAnoamliesResponse)
if err != nil {
httputils.ReportError(w, err, "Get anomalies request failed due to an internal server error. Please try again.", http.StatusInternalServerError)
return
}
if getAnoamliesResponse.Error != "" {
httputils.ReportError(w, errors.New(getAnoamliesResponse.Error), fmt.Sprintf("Error when getting the anomaly list. Please double check each request parameter, and try again: %v", getAnoamliesResponse.Error), http.StatusBadRequest)
return
}
if err := json.NewEncoder(w).Encode(getAnoamliesResponse); err != nil {
httputils.ReportError(w, err, "Failed to write get anoamlies response.", http.StatusInternalServerError)
return
}
sklog.Debugf("[SkiaTriage] %d anomalies are received.", len(getAnoamliesResponse.Anomalies))
return
}
// This function is to redirect the report page request to the group_report
// endpoint in Chromeperf.
func (api anomaliesApi) GetGroupReportLegacy(w http.ResponseWriter, r *http.Request) {
if api.loginProvider.LoggedInAs(r) == "" {
httputils.ReportError(w, errors.New("Not logged in"), fmt.Sprintf("You must be logged in to complete this action."), http.StatusUnauthorized)
return
}
var err error
var groupReportRequest GetGroupReportRequest
if err = json.NewDecoder(r.Body).Decode(&groupReportRequest); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON on anomaly group report request.", http.StatusInternalServerError)
return
}
sklog.Debugf("[SkiaTriage] Anomaly group report request received from frontend: %v", groupReportRequest)
if !IsGroupReportRequestValid(groupReportRequest) {
httputils.ReportError(w, err, "Group report request is invalid.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
ctx, cancel := context.WithTimeout(r.Context(), defaultAnomaliesRequestTimeout)
defer cancel()
groupReportResponse := &GetGroupReportResponse{}
host := config.Config.URL
if groupReportRequest.AnomalyIDs != "" {
if len(strings.Split(groupReportRequest.AnomalyIDs, ",")) == 1 {
err = api.chromeperfClient.SendGetRequest(
ctx, "alerts_skia_by_key", "", url.Values{"key": {groupReportRequest.AnomalyIDs}, "host": []string{host}}, groupReportResponse)
} else {
groupReportByKeysRequest := &GetGroupReportByKeysRequest{
Keys: groupReportRequest.AnomalyIDs,
Host: host,
}
err = api.chromeperfClient.SendPostRequest(ctx, "alerts_skia_by_keys", "", groupReportByKeysRequest, groupReportResponse, []int{200, 400, 500})
}
} else if groupReportRequest.BugID != "" {
err = api.chromeperfClient.SendGetRequest(
ctx, "alerts_skia_by_bug_id", "", url.Values{"bug_id": {groupReportRequest.BugID}, "host": []string{host}}, groupReportResponse)
} else if groupReportRequest.Sid != "" {
err = api.chromeperfClient.SendGetRequest(
ctx, "alerts_skia_by_sid", "", url.Values{"sid": {groupReportRequest.Sid}, "host": []string{host}}, groupReportResponse)
} else if groupReportRequest.Revison != "" {
err = api.chromeperfClient.SendGetRequest(
ctx, fmt.Sprintf("alerts/skia/rev/%s", groupReportRequest.Revison), "", url.Values{"host": []string{host}}, groupReportResponse)
} else if groupReportRequest.AnomalyGroupID != "" {
err = api.chromeperfClient.SendGetRequest(
ctx, fmt.Sprintf("alerts/skia/group_id/%s", groupReportRequest.AnomalyGroupID), "", url.Values{"host": []string{host}}, groupReportResponse)
} else {
httputils.ReportError(w, errors.New("Invalid Request"), fmt.Sprintf("Group report request does not have valid parameters: %v", groupReportRequest), http.StatusBadRequest)
sklog.Debug("[SkiaTriage] Group report request does not have valid parameters")
return
}
if err != nil {
httputils.ReportError(w, err, "Anomaly group report request failed due to an internal server error. Please try again.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Anomaly group report request failed due to an internal server error: %v", err)
return
}
if groupReportResponse.Error != "" {
httputils.ReportError(w, errors.New(groupReportResponse.Error), fmt.Sprintf("Error when getting the anomaly report group. Please double check each request parameter, and try again: %v", groupReportResponse.Error), http.StatusBadRequest)
sklog.Debugf("[SkiaTriage] Error when getting the anomaly report group: %v", groupReportResponse.Error)
return
}
// b/383913153: mitigation on the anomaly rendering scenario.
for i := range groupReportResponse.Anomalies {
groupReportResponse.Anomalies[i].TestPath, err = cleanTestName(groupReportResponse.Anomalies[i].TestPath)
if err != nil {
httputils.ReportError(w, err, "Failed to clean up test name by regex.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Failed to clean up test name by regex: %v", err)
return
}
}
groupReportResponse.TimerangeMap, err = api.getTimerangeMap(ctx, groupReportResponse.Anomalies)
if err != nil {
httputils.ReportError(w, err, "Failed to get timerange map.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Failed to get timerange map: %v", err)
return
}
if err := json.NewEncoder(w).Encode(groupReportResponse); err != nil {
httputils.ReportError(w, err, "Failed to write anomaly report response.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Failed to write anomaly report response: %v", err)
return
}
sklog.Debugf("[SkiaTriage] %d anomalies are received from anomaly report group.", len(groupReportResponse.Anomalies))
}
// GetSheriffListSkia handles requests to retrieve the list of sheriffs from the Skia internal store.
func (api anomaliesApi) GetSheriffList(w http.ResponseWriter, r *http.Request) {
if api.loginProvider.LoggedInAs(r) == "" {
httputils.ReportError(w, errors.New("Not logged in"), fmt.Sprintf("You must be logged in to complete this action."), http.StatusUnauthorized)
return
}
sklog.Debug("[SkiaTriage] Get sheriff config list from Skia request received from frontend.")
w.Header().Set("Content-Type", "application/json")
ctx, cancel := context.WithTimeout(r.Context(), defaultAnomaliesRequestTimeout)
defer cancel()
getSheriffListResponse := &GetSheriffListResponse{}
subscriptions, err := api.subStore.GetAllActiveSubscriptions(ctx)
if err != nil {
httputils.ReportError(w, err, "Failed to get all active subscriptions.", http.StatusInternalServerError)
return
}
for _, sub := range subscriptions {
getSheriffListResponse.SheriffList = append(getSheriffListResponse.SheriffList, sub.Name)
}
if err := json.NewEncoder(w).Encode(getSheriffListResponse); err != nil {
httputils.ReportError(w, err, "Failed to write sheriff list to UI response.", http.StatusInternalServerError)
return
}
sklog.Debugf("[SkiaTriage] sheriff config list is loaded: %v", getSheriffListResponse.SheriffList)
return
}
// GetAnomalyListSkia handles requests to retrieve the list of anomalies from the Skia internal store as
// well as Subscription and Alert information.
func (api anomaliesApi) GetAnomalyList(w http.ResponseWriter, r *http.Request) {
if api.loginProvider.LoggedInAs(r) == "" {
httputils.ReportError(w, errors.New("Not logged in"), fmt.Sprintf("You must be logged in to complete this action."), http.StatusUnauthorized)
return
}
query_values := r.URL.Query()
w.Header().Set("Content-Type", "application/json")
subName := query_values.Get("sheriff")
ctx, cancel := context.WithTimeout(r.Context(), defaultAnomaliesRequestTimeout)
defer cancel()
getAnomaliesResponse := &GetAnomaliesResponse{}
sub, err := api.subStore.GetActiveSubscription(ctx, subName)
if sub == nil {
httputils.ReportError(w, err, "No matching subscription found", http.StatusNotFound)
return
}
if err != nil {
httputils.ReportError(w, err, "Failed to get subscription", http.StatusInternalServerError)
return
}
getAnomaliesResponse.Subscription = sub
alertsFromStore, err := api.alertStore.ListForSubscription(ctx, subName)
if err != nil {
httputils.ReportError(w, err, "Failed to get list of alerts", http.StatusInternalServerError)
return
}
alertsForResponse := make([]alerts.Alert, len(alertsFromStore))
for i, alertPtr := range alertsFromStore {
if alertPtr != nil {
alertsForResponse[i] = *alertPtr
}
}
getAnomaliesResponse.Alerts = alertsForResponse
regressions, err := api.regStore.GetRegressionsBySubName(ctx, subName, 50, 0)
if err != nil {
httputils.ReportError(w, err, "Failed to get regressions", http.StatusInternalServerError)
return
}
anomalies := make([]chromeperf.Anomaly, 0)
for _, reg := range regressions {
convertedAnomalies, err := compat.ConvertRegressionToAnomalies(reg)
if err != nil {
sklog.Warningf("Could not convert regression with id %s to anomalies: %s", reg.Id, err)
continue
}
for _, commitNumberMap := range convertedAnomalies {
for _, anomaly := range commitNumberMap {
anomalies = append(anomalies, anomaly)
}
}
}
getAnomaliesResponse.Anomalies = anomalies
if err := json.NewEncoder(w).Encode(getAnomaliesResponse); err != nil {
httputils.ReportError(w, err, "Failed to write get anoamlies response.", http.StatusInternalServerError)
return
}
sklog.Debugf("[SkiaTriage] %d anomalies are received.", len(getAnomaliesResponse.Anomalies))
}
func (api anomaliesApi) GetGroupReport(w http.ResponseWriter, r *http.Request) {
if api.loginProvider.LoggedInAs(r) == "" {
httputils.ReportError(w, errors.New("Not logged in"), fmt.Sprintf("You must be logged in to complete this action."), http.StatusUnauthorized)
return
}
var err error
var groupReportRequest GetGroupReportRequest
if err = json.NewDecoder(r.Body).Decode(&groupReportRequest); err != nil {
httputils.ReportError(w, err, "Failed to decode JSON on anomaly group report request.", http.StatusInternalServerError)
return
}
sklog.Debugf("[SkiaTriage] Anomaly group report request received from frontend: Revision: %s, AnomalyIDs: %s, BugID: %s, AnomalyGroupID: %s, Sid: %s", groupReportRequest.Revison, groupReportRequest.AnomalyIDs, groupReportRequest.BugID, groupReportRequest.AnomalyGroupID, groupReportRequest.Sid)
if !IsGroupReportRequestValid(groupReportRequest) {
httputils.ReportError(w, err, "Group report request is invalid.", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
ctx, cancel := context.WithTimeout(r.Context(), defaultAnomaliesRequestTimeout)
defer cancel()
groupReportResponse := &GetGroupReportResponse{}
if groupReportRequest.AnomalyIDs != "" {
ids := strings.Split(groupReportRequest.AnomalyIDs, ",")
regressions, err := api.regStore.GetByIDs(ctx, ids)
if err != nil {
httputils.ReportError(w, err, "Failed to retrieve regressions by ID.", http.StatusInternalServerError)
sklog.Errorf("Failed to get regressions by ID: %s", err)
return
}
groupReportResponse.Anomalies = make([]chromeperf.Anomaly, 0)
for _, reg := range regressions {
anomalies, err := compat.ConvertRegressionToAnomalies(reg)
if err != nil {
sklog.Warningf("Could not convert regression with id %s to anomalies: %s", reg.Id, err)
continue
}
for _, commitNumberMap := range anomalies {
for _, anomaly := range commitNumberMap {
groupReportResponse.Anomalies = append(groupReportResponse.Anomalies, anomaly)
}
}
}
} else if groupReportRequest.BugID != "" {
httputils.ReportError(w, errors.New("not implemented"), "This API is not implemented for this parameter.", http.StatusInternalServerError)
sklog.Debugf("Unsupported parameters for group report: %v", groupReportRequest)
return
} else if groupReportRequest.Sid != "" {
httputils.ReportError(w, errors.New("not implemented"), "This API is not implemented for this parameter.", http.StatusInternalServerError)
sklog.Debugf("Unsupported parameters for group report: %v", groupReportRequest)
return
} else if groupReportRequest.Revison != "" {
httputils.ReportError(w, errors.New("not implemented"), "This API is not implemented for this parameter.", http.StatusInternalServerError)
sklog.Debugf("Unsupported parameters for group report: %v", groupReportRequest)
return
} else if groupReportRequest.AnomalyGroupID != "" {
httputils.ReportError(w, errors.New("not implemented"), "This API is not implemented for this parameter.", http.StatusInternalServerError)
sklog.Debugf("Unsupported parameters for group report: %v", groupReportRequest)
return
} else {
httputils.ReportError(w, errors.New("invalid Request"), fmt.Sprintf("Group report request does not have valid parameters: %v", groupReportRequest), http.StatusBadRequest)
sklog.Debug("[SkiaTriage] Group report request does not have valid parameters")
return
}
if groupReportResponse.Error != "" {
httputils.ReportError(w, errors.New(groupReportResponse.Error), fmt.Sprintf("Error when getting the anomaly report group. Please double check each request parameter, and try again: %v", groupReportResponse.Error), http.StatusBadRequest)
sklog.Debugf("[SkiaTriage] Error when getting the anomaly report group: %v", groupReportResponse.Error)
return
}
// b/383913153: mitigation on the anomaly rendering scenario.
for i := range groupReportResponse.Anomalies {
groupReportResponse.Anomalies[i].TestPath, err = cleanTestName(groupReportResponse.Anomalies[i].TestPath)
if err != nil {
httputils.ReportError(w, err, "Failed to clean up test name by regex.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Failed to clean up test name by regex: %v", err)
return
}
}
groupReportResponse.TimerangeMap, err = api.getTimerangeMap(ctx, groupReportResponse.Anomalies)
if err != nil {
httputils.ReportError(w, err, "Failed to get timerange map.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Failed to get timerange map: %v", err)
return
}
if err := json.NewEncoder(w).Encode(groupReportResponse); err != nil {
httputils.ReportError(w, err, "Failed to write anomaly report response.", http.StatusInternalServerError)
sklog.Debugf("[SkiaTriage] Failed to write anomaly report response: %v", err)
return
}
sklog.Debugf("[SkiaTriage] %d anomalies are received from anomaly report group.", len(groupReportResponse.Anomalies))
}
// The group report page should only regard one input parameters.
// If the request has more than one parameters, we consider it invalid.
func IsGroupReportRequestValid(req GetGroupReportRequest) bool {
valid_param_count := 0
if req.AnomalyIDs != "" {
valid_param_count += 1
}
if req.BugID != "" {
valid_param_count += 1
}
if req.Sid != "" {
valid_param_count += 1
}
if req.Revison != "" {
valid_param_count += 1
}
if req.AnomalyGroupID != "" {
valid_param_count += 1
}
return valid_param_count == 1
}
func (api anomaliesApi) getTimerangeMap(ctx context.Context, anomalies []chromeperf.Anomaly) (map[string]Timerange, error) {
timerangeMap := make(map[string]Timerange)
for i := range anomalies {
anomaly := &anomalies[i]
var startTime int64
var endTime int64
if strings.Contains(config.Config.InstanceName, "fuchsia") {
timestampStr := anomaly.Timestamp
const layout = "2006-01-02T15:04:05.999999" // Layout for "ISO Format"
timestamp, err := time.Parse(layout, timestampStr)
if err != nil {
return nil, skerr.Wrap(err)
}
// Since we don't have a start and end revision to determine range, we use
// one day before and one day after to capture this range, although less accurately.
startTime = int64(timestamp.AddDate(0, 0, -1).Unix())
endTime = int64(timestamp.AddDate(0, 0, 1).Unix())
} else {
startCommit, err := api.perfGit.CommitFromCommitNumber(ctx, types.CommitNumber(anomaly.StartRevision))
if err != nil {
sklog.Debugf("[SkiaTriage] CommitFromCommitNumber returns err: %v", err)
return nil, err
}
startTime = int64(startCommit.Timestamp)
endCommit, err := api.perfGit.CommitFromCommitNumber(ctx, types.CommitNumber(anomaly.EndRevision))
if err != nil {
sklog.Debugf("[SkiaTriage] CommitFromCommitNumber returns err: %v", err)
return nil, err
}
// We will shift the end time by a day so the graph doesn't render the anomalies right at the end
endTime = int64(time.Unix(endCommit.Timestamp, 0).AddDate(0, 0, 1).Unix())
}
timerangeMap[anomaly.Id] = Timerange{Begin: startTime, End: endTime}
}
return timerangeMap, nil
}
// cleanTestName cleans the given test name using the query.ForceValid function.
func cleanTestName(testName string) (string, error) {
var invalidParamCharRegex *regexp.Regexp
var err error
invalidParamCharRegex = query.InvalidChar
if config.Config.InvalidParamCharRegex != "" {
invalidParamCharRegex, err = regexp.Compile(config.Config.InvalidParamCharRegex)
if err != nil {
return testName, skerr.Wrap(err)
}
}
// Split the test name into parts.
parts := strings.Split(testName, "/")
// Clean each part individually.
for i := range parts {
parts[i] = query.ForceValidWithRegex(map[string]string{"a": parts[i]}, invalidParamCharRegex)["a"]
}
// Join the cleaned parts back together.
return strings.Join(parts, "/"), nil
}