@@ -21,7 +21,9 @@ import (
2121 "magician/provider"
2222 "magician/teamcity"
2323 utils "magician/utility"
24+ "net/url"
2425 "os"
26+ "regexp"
2527 "strconv"
2628 "strings"
2729 "time"
@@ -45,6 +47,7 @@ type TestInfo struct {
4547 Resource string `json:"resource"`
4648 CommitSha string `json:"commit_sha"`
4749 ErrorMessage string `json:"error_message"`
50+ ErrorType string `json:"error_type"`
4851 LogLink string `json:"log_link"`
4952 ProviderVersion string `json:"provider_version"`
5053 QueuedDate time.Time `json:"queued_date"`
@@ -84,24 +87,27 @@ var collectNightlyTestStatusCmd = &cobra.Command{
8487 tc := teamcity .NewClient (env ["TEAMCITY_TOKEN" ])
8588 gcs := cloudstorage .NewClient ()
8689
87- now := time .Now ()
88-
8990 loc , err := time .LoadLocation ("America/Los_Angeles" )
9091 if err != nil {
9192 return fmt .Errorf ("Error loading location: %s" , err )
9293 }
93- date := now .In (loc )
94+
95+ now := time .Now ().In (loc )
96+ year , month , day := now .Date ()
97+
9498 customDate := args [0 ]
9599 // check if a specific date is provided
96100 if customDate != "" {
97101 parsedDate , err := time .Parse ("2006-01-02" , customDate ) // input format YYYY-MM-DD
98- // Set the time to 7pm PT
99- date = time .Date (parsedDate .Year (), parsedDate .Month (), parsedDate .Day (), 19 , 0 , 0 , 0 , loc )
100102 if err != nil {
101103 return fmt .Errorf ("invalid input time format: %w" , err )
102104 }
105+ year , month , day = parsedDate .Date ()
103106 }
104107
108+ // Set the time to 7pm PT
109+ date := time .Date (year , month , day , 19 , 0 , 0 , 0 , loc )
110+
105111 return execCollectNightlyTestStatus (date , tc , gcs )
106112 },
107113}
@@ -134,10 +140,40 @@ func execCollectNightlyTestStatus(now time.Time, tc TeamcityClient, gcs Cloudsto
134140}
135141
136142func createTestReport (pVersion provider.Version , tc TeamcityClient , gcs CloudstorageClient , formattedStartCut , formattedFinishCut , date string ) error {
143+
144+ baseLocator := fmt .Sprintf ("count:500,project:%s,branch:refs/heads/nightly-test,queuedDate:(date:%s,condition:before),queuedDate:(date:%s,condition:after)" , pVersion .TeamCityNightlyProjectName (), formattedFinishCut , formattedStartCut )
145+ fields := "build(id,buildTypeId,buildConfName,webUrl,number,queuedDate,startDate,finishDate)"
146+ params := url.Values {}
147+
148+ // Check Queued Builds
149+ params .Set ("locator" , fmt .Sprintf ("%s,state:queued" , baseLocator ))
150+ queuedBuilds , err := tc .GetBuilds (params )
151+ if err != nil {
152+ return fmt .Errorf ("failed to get queued builds: %w" , err )
153+ }
154+ if len (queuedBuilds .Builds ) > 0 {
155+ fmt .Printf ("%s Test unfinished: there are still %d builds queued.\n " , strings .ToUpper (pVersion .String ()), len (queuedBuilds .Builds ))
156+ return nil
157+ }
158+
159+ // Check Running Builds
160+ params .Set ("locator" , fmt .Sprintf ("%s,state:running,tag:cron-trigger" , baseLocator ))
161+ params .Set ("fields" , fields )
162+ runningBuilds , err := tc .GetBuilds (params )
163+ if err != nil {
164+ return fmt .Errorf ("failed to get running builds: %w" , err )
165+ }
166+ if len (runningBuilds .Builds ) > 0 {
167+ fmt .Printf ("%s Test unfinished: there are still %d builds running.\n " , strings .ToUpper (pVersion .String ()), len (runningBuilds .Builds ))
168+ return nil
169+ }
170+
137171 // Get all service test builds
138- builds , err := tc .GetBuilds (pVersion .TeamCityNightlyProjectName (), formattedFinishCut , formattedStartCut )
172+ params .Set ("locator" , fmt .Sprintf ("%s,state:finished,tag:cron-trigger" , baseLocator ))
173+ params .Set ("fields" , fields )
174+ builds , err := tc .GetBuilds (params )
139175 if err != nil {
140- return err
176+ return fmt . Errorf ( "failed to get finished builds: %w" , err )
141177 }
142178
143179 var testInfoList []TestInfo
@@ -164,12 +200,14 @@ func createTestReport(pVersion provider.Version, tc TeamcityClient, gcs Cloudsto
164200
165201 for _ , testResult := range serviceTestResults .TestResults {
166202 var errorMessage string
203+ var errorType string
167204 // Get test debug log gcs link
168205 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 )
169206 // Get concise error message for failed and skipped tests
170207 // Skipped tests have a status of "UNKNOWN" on TC
171208 if testResult .Status == "FAILURE" || testResult .Status == "UNKNOWN" {
172209 errorMessage = convertErrorMessage (testResult .ErrorMessage )
210+ errorType = categorizeError (errorMessage )
173211 }
174212
175213 queuedTime , err := time .Parse (tcTimeFormat , build .QueuedDate )
@@ -192,6 +230,7 @@ func createTestReport(pVersion provider.Version, tc TeamcityClient, gcs Cloudsto
192230 Resource : convertTestNameToResource (testResult .Name ),
193231 CommitSha : build .Number ,
194232 ErrorMessage : errorMessage ,
233+ ErrorType : errorType ,
195234 LogLink : logLink ,
196235 ProviderVersion : strings .ToUpper (pVersion .String ()),
197236 Duration : testResult .Duration ,
@@ -253,6 +292,145 @@ func convertErrorMessage(rawErrorMessage string) string {
253292 return strings .TrimSpace (rawErrorMessage [startIndex :endIndex ])
254293}
255294
295+ var (
296+ reSubnetNotReady = regexp .MustCompile (`The resource '[^']+/subnetworks/[^']+' is not ready` )
297+ reApiEnv = regexp .MustCompile (`has not been used in project (ci-test-project-188019|1067888929963|ci-test-project-nightly-ga|594424405950|ci-test-project-nightly-beta|653407317329|tf-vcr-private|808590572184) before or it is disabled` )
298+ reAttrSet = regexp .MustCompile (`Attribute '[^']+' expected to be set` )
299+ reQuotaLimit = regexp .MustCompile (`Quota limit '[^']+' has been exceeded` )
300+ reGoogleApi4xx = regexp .MustCompile (`googleapi: Error 4\d\d` )
301+ reGoogleApi5xx = regexp .MustCompile (`googleapi: Error 5\d\d` )
302+ reGoogleApiGeneric = regexp .MustCompile (`googleapi: Error` )
303+ )
304+
305+ func categorizeError (errMsg string ) string {
306+ if strings .Contains (errMsg , "Error code 13" ) {
307+ return "Error code 13"
308+ }
309+ if strings .Contains (errMsg , "Precondition check failed" ) {
310+ return "Precondition check failed"
311+ }
312+
313+ // Diff Category
314+ if strings .Contains (errMsg , "After applying this test step, the plan was not empty" ) ||
315+ strings .Contains (errMsg , "After applying this test step and performing a `terraform refresh`" ) ||
316+ strings .Contains (errMsg , "Expected a non-empty plan, but got an empty plan" ) ||
317+ strings .Contains (errMsg , "error: Check failed" ) {
318+ return "Diff"
319+ }
320+
321+ if strings .Contains (errMsg , "timeout while waiting for state" ) {
322+ return "Operation timeout"
323+ }
324+
325+ // Regex: Subnetwork not ready
326+ if reSubnetNotReady .MatchString (errMsg ) {
327+ return "Subnetwork not ready"
328+ }
329+
330+ // ImportStateVerify Category
331+ if strings .Contains (errMsg , "ImportStateVerify attributes not equivalent" ) ||
332+ strings .Contains (errMsg , "Cannot import non-existent remote object" ) ||
333+ strings .Contains (errMsg , "Error: Unexpected Import Identifier" ) {
334+ return "ImportStateVerify"
335+ }
336+
337+ // Deprecated (Case-insensitive check)
338+ if strings .Contains (strings .ToLower (errMsg ), "deprecated" ) {
339+ return "Deprecated"
340+ }
341+
342+ if strings .Contains (errMsg , "Provider produced inconsistent result after apply" ) &&
343+ strings .Contains (errMsg , "Root object was present, but now absent" ) {
344+ return "Root object was present, but now absent"
345+ }
346+
347+ if strings .Contains (errMsg , "Provider produced inconsistent final plan" ) {
348+ return "Provider produced inconsistent final plan"
349+ }
350+
351+ // API Enablement
352+ if reApiEnv .MatchString (errMsg ) {
353+ return "API enablement (Test environment)"
354+ }
355+ if strings .Contains (errMsg , "has not been used in project" ) && strings .Contains (errMsg , "before or it is disabled" ) {
356+ return "API enablement (Created project)"
357+ }
358+
359+ if strings .Contains (errMsg , "does not have required permissions" ) {
360+ return "Permissions"
361+ }
362+ if strings .Contains (errMsg , "bootstrap_iam_test_utils.go" ) {
363+ return "Bootstrapping"
364+ }
365+
366+ // Bad Config Category
367+ if strings .Contains (errMsg , "Inconsistent dependency lock file" ) ||
368+ strings .Contains (errMsg , "Invalid resource type" ) ||
369+ strings .Contains (errMsg , "Blocks of type" ) && strings .Contains (errMsg , "are not expected here" ) ||
370+ strings .Contains (errMsg , "Conflicting configuration arguments" ) ||
371+ reAttrSet .MatchString (errMsg ) {
372+ return "Bad config"
373+ }
374+
375+ // Quota Category
376+ if strings .Contains (errMsg , "Quota exhausted" ) ||
377+ strings .Contains (errMsg , "Quota exceeded" ) ||
378+ strings .Contains (errMsg , "You do not have quota" ) ||
379+ reQuotaLimit .MatchString (errMsg ) {
380+ return "Quota"
381+ }
382+
383+ if strings .Contains (errMsg , "does not have enough resources available" ) {
384+ return "Resource availability"
385+ }
386+
387+ // API Create/Read/Update/Delete
388+ if strings .Contains (errMsg , "Error: Error waiting to create" ) ||
389+ strings .Contains (errMsg , "Error: Error waiting for Create" ) ||
390+ strings .Contains (errMsg , "Error: Error waiting for creating" ) ||
391+ strings .Contains (errMsg , "Error: Error creating" ) ||
392+ strings .Contains (errMsg , "was created in the error state" ) ||
393+ strings .Contains (errMsg , "Error: Error changing instance status after creation:" ) {
394+ return "API Create"
395+ }
396+
397+ if strings .Contains (errMsg , "Error: Error reading" ) {
398+ return "API Read"
399+ }
400+
401+ if strings .Contains (errMsg , "Error setting IAM policy" ) ||
402+ strings .Contains (errMsg , "Error applying IAM policy" ) {
403+ return "API IAM"
404+ }
405+
406+ if strings .Contains (errMsg , "Error: Error waiting for Updating" ) ||
407+ strings .Contains (errMsg , "Error: Error updating" ) {
408+ return "API Update"
409+ }
410+
411+ if strings .Contains (errMsg , "Error: Error waiting for Deleting" ) ||
412+ strings .Contains (errMsg , "Error running post-test destroy" ) {
413+ return "API Delete"
414+ }
415+
416+ // Google API Errors (Order matters: check specific codes before generic)
417+ if reGoogleApi4xx .MatchString (errMsg ) {
418+ return "API (4xx)"
419+ }
420+ if reGoogleApi5xx .MatchString (errMsg ) {
421+ return "API (5xx)"
422+ }
423+ if reGoogleApiGeneric .MatchString (errMsg ) ||
424+ strings .Contains (errMsg , "Error: Error when reading or editing" ) ||
425+ strings .Contains (errMsg , "Error: Error waiting" ) ||
426+ strings .Contains (errMsg , "unable to queue the operation" ) ||
427+ strings .Contains (errMsg , "Error waiting for Switching runtime" ) {
428+ return "API (Other)"
429+ }
430+
431+ return "Other"
432+ }
433+
256434func init () {
257435 rootCmd .AddCommand (collectNightlyTestStatusCmd )
258436}
0 commit comments