Skip to content

Commit

Permalink
Add Nightly Test Data Collector (#12769)
Browse files Browse the repository at this point in the history
  • Loading branch information
shuyama1 authored Jan 22, 2025
1 parent 1a0143d commit 7b4389c
Show file tree
Hide file tree
Showing 11 changed files with 679 additions and 60 deletions.
58 changes: 58 additions & 0 deletions .ci/magician/cloudstorage/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2025 Google LLC. All Rights Reserved.
*
* Licensed 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 cloudstorage

import (
"context"
"fmt"
"io"
"os"
"time"

"cloud.google.com/go/storage"
)

func (gcs *Client) WriteToGCSBucket(bucket, object, filePath string) error {
ctx := context.Background()

client, err := storage.NewClient(ctx)
if err != nil {
return fmt.Errorf("storage.NewClient: %v", err)
}
defer client.Close()

file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("os.Open: %w", err)
}
defer file.Close()

ctx, cancel := context.WithTimeout(ctx, time.Second*50)
defer cancel()

writer := client.Bucket(bucket).Object(object).NewWriter(ctx)
writer.ContentType = "application/json"

if _, err = io.Copy(writer, file); err != nil {
return fmt.Errorf("io.Copy: %w", err)
}
if err := writer.Close(); err != nil {
return fmt.Errorf("Writer.Close: %w", err)
}

fmt.Printf("File uploaded to bucket %s as %s\n", bucket, object)
return nil
}
23 changes: 23 additions & 0 deletions .ci/magician/cloudstorage/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2025 Google LLC. All Rights Reserved.
*
* Licensed 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 cloudstorage

type Client struct {
}

func NewClient() *Client {
return &Client{}
}
228 changes: 228 additions & 0 deletions .ci/magician/cmd/collect_nightly_test_status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
* Copyright 2025 Google LLC. All Rights Reserved.
*
* Licensed 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 cmd

import (
"fmt"
"magician/cloudstorage"
"magician/provider"
"magician/teamcity"
utils "magician/utility"
"os"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"
)

const (
NIGHTLY_DATA_BUCKET = "nightly-test-data"
)

var cntsRequiredEnvironmentVariables = [...]string{
"TEAMCITY_TOKEN",
}

type TestInfo struct {
Name string `json:"name"`
Status string `json:"status"`
Service string `json:"service"`
ErrorMessage string `json:"error_message"`
LogLink string `json"log_link`
}

// collectNightlyTestStatusCmd represents the collectNightlyTestStatus command
var collectNightlyTestStatusCmd = &cobra.Command{
Use: "collect-nightly-test-status",
Short: "Collects and stores nightly test status",
Long: `This command collects nightly test status, stores the data in JSON files and upload the files to GCS.
The command expects the following argument(s):
1. Custom test date in YYYY-MM-DD format. default: ""(current time when the job is executed)
It then performs the following operations:
1. Collects nightly test status of the execution day or the specified test date (if provided)
2. Stores the collected data in JSON files
3. Uploads the JSON files to GCS
The following environment variables are required:
` + listCNTSRequiredEnvironmentVariables(),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
env := make(map[string]string)
for _, ev := range cntsRequiredEnvironmentVariables {
val, ok := os.LookupEnv(ev)
if !ok {
return fmt.Errorf("did not provide %s environment variable", ev)
}
env[ev] = val
}

tc := teamcity.NewClient(env["TEAMCITY_TOKEN"])
gcs := cloudstorage.NewClient()

now := time.Now()

loc, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
return fmt.Errorf("Error loading location: %s", err)
}
date := now.In(loc)
customDate := args[0]
// check if a specific date is provided
if customDate != "" {
parsedDate, err := time.Parse("2006-01-02", customDate) // input format YYYY-MM-DD
// Set the time to 6pm PT
date = time.Date(parsedDate.Year(), parsedDate.Month(), parsedDate.Day(), 18, 0, 0, 0, loc)
if err != nil {
return fmt.Errorf("invalid input time format: %w", err)
}
}

return execCollectNightlyTestStatus(date, tc, gcs)
},
}

func listCNTSRequiredEnvironmentVariables() string {
var result string
for i, ev := range cntsRequiredEnvironmentVariables {
result += fmt.Sprintf("\t%2d. %s\n", i+1, ev)
}
return result
}

func execCollectNightlyTestStatus(now time.Time, tc TeamcityClient, gcs CloudstorageClient) error {
lastday := now.AddDate(0, 0, -1)
formattedStartCut := lastday.Format(time.RFC3339)
formattedFinishCut := now.Format(time.RFC3339)
date := now.Format("2006-01-02")

err := createTestReport(provider.GA, tc, gcs, formattedStartCut, formattedFinishCut, date)
if err != nil {
return fmt.Errorf("Error getting GA nightly test status: %w", err)
}

err = createTestReport(provider.Beta, tc, gcs, formattedStartCut, formattedFinishCut, date)
if err != nil {
return fmt.Errorf("Error getting Beta nightly test status: %w", err)
}

return nil
}

func createTestReport(pVersion provider.Version, tc TeamcityClient, gcs CloudstorageClient, formattedStartCut, formattedFinishCut, date string) error {
// Get all service test builds
builds, err := tc.GetBuilds(pVersion.TeamCityNightlyProjectName(), formattedFinishCut, formattedStartCut)
if err != nil {
return err
}

var testInfoList []TestInfo
for _, build := range builds.Builds {
// Get service package name
serviceName, err := convertServiceName(build.BuildTypeId)
if err != nil {
return fmt.Errorf("failed to convert test service name for %s: %v", build.BuildTypeId, err)
}
// Skip sweeper package
if serviceName == "sweeper" {
continue
}

// Get test results
serviceTestResults, err := tc.GetTestResults(build)
if err != nil {
return fmt.Errorf("failed to get test results: %v", err)
}
if len(serviceTestResults.TestResults) == 0 {
fmt.Printf("Service %s has no tests\n", serviceName)
continue
}

for _, testResult := range serviceTestResults.TestResults {
var errorMessage string
// Get test debug log gcs link
logLink := fmt.Sprintf("https://storage.cloud.google.com/teamcity-logs/nightly/%s/%s/%s/debug-%s-%s-%s-%s.txt", pVersion.TeamCityNightlyProjectName(), date, build.Number, pVersion.ProviderName(), build.Number, strconv.Itoa(build.Id), testResult.Name)
// Get concise error message
if testResult.Status == "FAILURE" {
errorMessage = convertErrorMessage(testResult.ErrorMessage)
}
testInfoList = append(testInfoList, TestInfo{
Name: testResult.Name,
Status: testResult.Status,
Service: serviceName,
ErrorMessage: errorMessage,
LogLink: logLink,
})
}
}

// Write test status data to a JSON file
fmt.Println("Write test status")
testStatusFileName := fmt.Sprintf("%s-%s.json", date, pVersion.String())
err = utils.WriteToJson(testInfoList, testStatusFileName)
if err != nil {
return err
}

// Upload test status data file to gcs bucket
objectName := pVersion.String() + "/" + testStatusFileName
err = gcs.WriteToGCSBucket(NIGHTLY_DATA_BUCKET, objectName, testStatusFileName)
if err != nil {
return err
}

return nil
}

// convertServiceName extracts service package name from teamcity build type id
// input: TerraformProviders_GoogleCloud_GOOGLE_NIGHTLYTESTS_GOOGLE_PACKAGE_SECRETMANAGER
// output: secretmanager
func convertServiceName(servicePath string) (string, error) {
idx := strings.LastIndex(servicePath, "_")

if idx != -1 {
return strings.ToLower(servicePath[idx+1:]), nil
}
return "", fmt.Errorf("wrong service path format for %s", servicePath)
}

// convertErrorMessage returns concise error message
func convertErrorMessage(rawErrorMessage string) string {

startMarker := "------- Stdout: -------"
endMarker := "------- Stderr: -------"
startIndex := strings.Index(rawErrorMessage, startMarker)
endIndex := strings.Index(rawErrorMessage, endMarker)

if startIndex != -1 {
startIndex += len(startMarker)
} else {
startIndex = 0
}

if endIndex == -1 {
endIndex = len(rawErrorMessage)
}

return strings.TrimSpace(rawErrorMessage[startIndex:endIndex])
}

func init() {
rootCmd.AddCommand(collectNightlyTestStatusCmd)
}
83 changes: 83 additions & 0 deletions .ci/magician/cmd/collect_nightly_test_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2025 Google LLC. All Rights Reserved.
*
* Licensed 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 cmd

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestConvertServiceName(t *testing.T) {
cases := map[string]struct {
servicePath string
want string
wantError bool
}{
"valid service path": {
servicePath: "TerraformProviders_GoogleCloud_GOOGLE_NIGHTLYTESTS_GOOGLE_PACKAGE_SECRETMANAGER",
want: "secretmanager",
wantError: false,
},
"invalid service path": {
servicePath: "SECRETMANAGER",
want: "",
wantError: true,
},
}

for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
got, err := convertServiceName(tc.servicePath)
if tc.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.want, got)
}
})
}
}

func TestConvertErrorMessage(t *testing.T) {
cases := map[string]struct {
rawErrorMessage string
want string
}{
"error message with start and end markers": {
rawErrorMessage: "------- Stdout: ------- === RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS ------- Stderr: ------- 2025/01/21 08:06:22 [DEBUG] [transport] [server-transport 0xc002614000] Closing: EOF 2025/01/21 08:06:22 [DEBUG] [transport] [server-transport 0xc002614000] loopyWriter exiting with error: transport closed by client 2025/01/21 08:06:22",
want: "=== RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS",
},
"error message with start but no end markers": {
rawErrorMessage: "------- Stdout: ------- === RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS",
want: "=== RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS",
},
"error message with no start but with end markers": {
rawErrorMessage: "=== RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS ------- Stderr: ------- 2025/01/21 08:06:22 [DEBUG] [transport] [server-transport 0xc002614000] Closing: EOF 2025/01/21 08:06:22 [DEBUG] [transport] [server-transport 0xc002614000] loopyWriter exiting with error: transport closed by client 2025/01/21 08:06:22",
want: "=== RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS",
},
"error message with no start and no end markers": {
rawErrorMessage: "=== RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS",
want: "=== RUN TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === PAUSE TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample === CONT TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample --- PASS: TestAccColabRuntimeTemplate_colabRuntimeTemplateBasicExample (11.76s) PASS",
},
}

for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
got := convertErrorMessage(tc.rawErrorMessage)
assert.Equal(t, tc.want, got)
})
}
}
Loading

0 comments on commit 7b4389c

Please sign in to comment.