From 339cc80355bdec1e4812c5c3cbce50ab5f82ece5 Mon Sep 17 00:00:00 2001 From: Nathan Rijksen Date: Mon, 15 Apr 2019 12:00:55 -0400 Subject: [PATCH] Added tests endpoint and added TriggerInfo to Builds --- azuredevops/azuredevops.go | 2 + azuredevops/builds.go | 5 + azuredevops/tests.go | 150 ++++++++++++++++++++++++++ azuredevops/tests_test.go | 216 +++++++++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+) create mode 100644 azuredevops/tests.go create mode 100644 azuredevops/tests_test.go diff --git a/azuredevops/azuredevops.go b/azuredevops/azuredevops.go index e40d9f0..a1771f2 100644 --- a/azuredevops/azuredevops.go +++ b/azuredevops/azuredevops.go @@ -38,6 +38,7 @@ type Client struct { Iterations *IterationsService PullRequests *PullRequestsService Teams *TeamsService + Tests *TestsService WorkItems *WorkItemsService } @@ -59,6 +60,7 @@ func NewClient(account string, project string, token string) *Client { c.PullRequests = &PullRequestsService{client: c} c.WorkItems = &WorkItemsService{client: c} c.Teams = &TeamsService{client: c} + c.Tests = &TestsService{client: c} c.DeliveryPlans = &DeliveryPlansService{client: c} return c diff --git a/azuredevops/builds.go b/azuredevops/builds.go index bf8d60c..186aa2c 100644 --- a/azuredevops/builds.go +++ b/azuredevops/builds.go @@ -88,6 +88,11 @@ type Build struct { TriggerBuild *Build `json:"triggeredByBuild,omitempty"` URI string `json:"uri,omitempty"` URL string `json:"url,omitempty"` + TriggerInfo struct { + CiSourceBranch string `json:"ci.sourceBranch"` + CiSourceSha string `json:"ci.sourceSha"` + CiMessage string `json:"ci.message"` + } `json:"triggerInfo"` } // BuildListOrder is enum type for build list order diff --git a/azuredevops/tests.go b/azuredevops/tests.go new file mode 100644 index 0000000..435a58d --- /dev/null +++ b/azuredevops/tests.go @@ -0,0 +1,150 @@ +package azuredevops + +import ( + "fmt" + "time" +) + +// TestsService handles communication with the Tests methods on the API +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/test +type TestsService struct { + client *Client +} + +// TestListResponse is the wrapper around the main response for the List of Tests +type TestListResponse struct { + Tests []Test `json:"value"` +} + +// Test represents a test +type Test struct { + ID int `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + IsAutomated bool `json:"isAutomated"` + Iteration string `json:"iteration"` + Owner *struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + } `json:"owner,omitempty"` + StartedDate string `json:"startedDate"` + CompletedDate string `json:"completedDate"` + State string `json:"state"` + Plan *struct { + ID string `json:"id"` + } `json:"plan,omitempty"` + Revision int `json:"revision"` +} + +// TestsListOptions describes what the request to the API should look like +type TestsListOptions struct { + Count int `url:"$top,omitempty"` + BuildURI string `url:"buildUri,omitempty"` +} + +// List returns list of the tests +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/test/runs/list +func (s *TestsService) List(opts *TestsListOptions) ([]Test, error) { + URL := fmt.Sprintf("_apis/test/runs?api-version=4.1") + URL, err := addOptions(URL, opts) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, err + } + var response TestListResponse + _, err = s.client.Execute(request, &response) + + return response.Tests, err +} + +// TestResultsListResponse is the wrapper around the main response for the List of Tests +type TestResultsListResponse struct { + Results []TestResult `json:"value"` +} + +// TestResult represents a test result +type TestResult struct { + ID int `json:"id"` + Project struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"project"` + StartedDate time.Time `json:"startedDate"` + CompletedDate time.Time `json:"completedDate"` + DurationInMs float64 `json:"durationInMs"` + Outcome string `json:"outcome"` + Revision int `json:"revision"` + RunBy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + UniqueName string `json:"uniqueName"` + URL string `json:"url"` + ImageURL string `json:"imageUrl"` + } `json:"runBy"` + State string `json:"state"` + TestCase struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"testCase"` + TestRun struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"testRun"` + LastUpdatedDate time.Time `json:"lastUpdatedDate"` + LastUpdatedBy struct { + ID string `json:"id"` + DisplayName string `json:"displayName"` + UniqueName string `json:"uniqueName"` + URL string `json:"url"` + ImageURL string `json:"imageUrl"` + } `json:"lastUpdatedBy"` + Priority int `json:"priority"` + ComputerName string `json:"computerName"` + Build struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"build"` + CreatedDate time.Time `json:"createdDate"` + URL string `json:"url"` + FailureType string `json:"failureType"` + AutomatedTestStorage string `json:"automatedTestStorage"` + AutomatedTestType string `json:"automatedTestType"` + AutomatedTestTypeID string `json:"automatedTestTypeId"` + AutomatedTestID string `json:"automatedTestId"` + Area struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + } `json:"area"` + TestCaseTitle string `json:"testCaseTitle"` + CustomFields []interface{} `json:"customFields"` + AutomatedTestName string `json:"automatedTestName"` + StackTrace string `json:"stackTrace"` +} + +// TestResultsListOptions describes what the request to the API should look like +type TestResultsListOptions struct { + Count int `url:"$top,omitempty"` + RunID string `url:"runId,omitempty"` +} + +// ResultsList returns list of the test results +// utilising https://docs.microsoft.com/en-gb/rest/api/vsts/test/runs/list +func (s *TestsService) ResultsList(opts *TestResultsListOptions) ([]TestResult, error) { + URL := fmt.Sprintf("_apis/test/Runs/%s/results?api-version=4.1", opts.RunID) + opts.RunID = "" + URL, err := addOptions(URL, opts) + + request, err := s.client.NewRequest("GET", URL, nil) + if err != nil { + return nil, err + } + var response TestResultsListResponse + _, err = s.client.Execute(request, &response) + + return response.Results, err +} diff --git a/azuredevops/tests_test.go b/azuredevops/tests_test.go new file mode 100644 index 0000000..bfb8f42 --- /dev/null +++ b/azuredevops/tests_test.go @@ -0,0 +1,216 @@ +package azuredevops_test + +import ( + "fmt" + "net/http" + "testing" + + "github.com/benmatselby/go-azuredevops/azuredevops" +) + +const ( + testListURL = "/AZURE_DEVOPS_Project/_apis/test/runs" + testListResponse = `{ + "value": [ + { + "id": 1, + "name": "NewTestRun2", + "url": "https://dev.azure.com/fabrikam/fabrikam-fiber-tfvc/_apis/test/Runs/1", + "isAutomated": false, + "iteration": "Fabrikam-Fiber-TFVC\\Release 1\\Sprint 1", + "owner": { + "id": "e5a5f7f8-6507-4c34-b397-6c4818e002f4", + "displayName": "Fabrikam Fiber" + }, + "startedDate": "2014-05-04T12:50:33.17Z", + "completedDate": "2014-05-04T12:50:31.953Z", + "state": "Completed", + "plan": { + "id": "1" + }, + "revision": 4 + }, + { + "id": 2, + "name": "sprint1 (Manual)", + "url": "https://dev.azure.com/fabrikam/fabrikam-fiber-tfvc/_apis/test/Runs/2", + "isAutomated": false, + "iteration": "Fabrikam-Fiber-TFVC\\Release 1\\Sprint 1", + "owner": { + "id": "e5a5f7f8-6507-4c34-b397-6c4818e002f4", + "displayName": "Fabrikam Fiber" + }, + "startedDate": "2014-05-04T12:58:36.907Z", + "completedDate": "2014-05-04T12:58:36.47Z", + "state": "Completed", + "plan": { + "id": "1" + }, + "revision": 3 + } + ], + "count": 2 + }` + + testResultsListURL = "/AZURE_DEVOPS_Project/_apis/test/Runs/1/results" + testResultsListResponse = `{ + "count": 1, + "value": [ + { + "id": 100000, + "project": { + "id": "5c3d39df-a0cb-49da-be01-42e53792c0e1", + "name": "Fabrikam-Fiber-TFVC", + "url": "https://dev.azure.com/fabrikam/_apis/projects/Fabrikam-Fiber-TFVC" + }, + "startedDate": "2016-07-13T11:12:48.487Z", + "completedDate": "2016-07-13T11:12:48.493Z", + "durationInMs": 4, + "outcome": "Passed", + "revision": 1, + "runBy": { + "id": "a5cbf24d-799f-452e-82be-f049a85b5895", + "displayName": "Fabrikam", + "uniqueName": "fabrikamfiber.vsin@hotmail.com", + "url": "https://dev.azure.com/fabrikam/_apis/Identities/a5cbf24d-799f-452e-82be-f049a85b5895", + "imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=a5cbf24d-799f-452e-82be-f049a85b5895" + }, + "state": "Completed", + "testCase": { + "name": "Pass1" + }, + "testRun": { + "id": "16", + "name": "VSTest Test Run release any cpu", + "url": "https://dev.azure.com/fabrikam/Fabrikam-Fiber-TFVC/_apis/test/Runs/16" + }, + "lastUpdatedDate": "2016-07-13T11:12:49.123Z", + "lastUpdatedBy": { + "id": "375baa5b-5148-4e89-a549-ec202b722d89", + "displayName": "Project Collection Build Service (fabrikam)", + "uniqueName": "Build\\78b5727d-4a24-4ec8-9caf-704685572174", + "url": "https://vssps.dev.azure.com/fabrikam/_apis/Identities/375baa5b-5148-4e89-a549-ec202b722d89", + "imageUrl": "https://dev.azure.com/fabrikam/_api/_common/identityImage?id=375baa5b-5148-4e89-a549-ec202b722d89" + }, + "priority": 0, + "computerName": "TASKAGENT5-0055", + "build": { + "id": "5", + "name": "20160713.2", + "url": "https://dev.azure.com/fabrikam/_apis/build/Builds/5" + }, + "createdDate": "2016-07-13T11:12:49.123Z", + "url": "https://dev.azure.com/fabrikam/Fabrikam-Fiber-TFVC/_apis/test/Runs/16/Results/100000", + "failureType": "None", + "automatedTestStorage": "unittestproject1.dll", + "automatedTestType": "UnitTest", + "automatedTestTypeId": "13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b", + "automatedTestId": "aefba017-ab06-be36-6b92-de4e29836f72", + "area": { + "id": "37528", + "name": "Fabrikam-Fiber-TFVC", + "url": "vstfs:///Classification/Node/ebe8ac79-8d9f-4a5b-8d0a-c3095c81e70e" + }, + "testCaseTitle": "Pass1", + "customFields": [], + "automatedTestName": "UnitTestProject1.UnitTest1.Pass1" + } + ] + }` +) + +func TestTestsService_List(t *testing.T) { + tt := []struct { + name string + URL string + response string + count int + index int + state string + revision int + }{ + {name: "return two tests", URL: testListURL, response: testListResponse, count: 2, index: 0, state: "Completed", revision: 4}, + {name: "can handle no builds returned", URL: testListURL, response: "{}", count: 0, index: -1}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + c, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc(tc.URL, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + json := tc.response + fmt.Fprint(w, json) + }) + + options := &azuredevops.TestsListOptions{} + tests, err := c.Tests.List(options) + if err != nil { + t.Fatalf("returned error: %v", err) + } + + if tc.index > -1 { + if tests[tc.index].State != tc.state { + t.Fatalf("expected state %s, got %s", tc.state, tests[tc.index].State) + } + + if tests[tc.index].Revision != tc.revision { + t.Fatalf("expected revision %d, got %d", tc.revision, tests[tc.index].Revision) + } + } + + if len(tests) != tc.count { + t.Fatalf("expected length of tests to be %d; got %d", tc.count, len(tests)) + } + }) + } +} + +func TestTestsService_ResultsList(t *testing.T) { + tt := []struct { + name string + URL string + response string + count int + index int + outcome string + testcase string + }{ + {name: "return one result", URL: testResultsListURL, response: testResultsListResponse, count: 1, index: 0, outcome: "Passed", testcase: "Pass1"}, + {name: "can handle no results returned", URL: testResultsListURL, response: "{}", count: 0, index: -1}, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + c, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc(tc.URL, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + json := tc.response + fmt.Fprint(w, json) + }) + + options := &azuredevops.TestResultsListOptions{RunID: "1"} + tests, err := c.Tests.ResultsList(options) + if err != nil { + t.Fatalf("returned error: %v", err) + } + + if tc.index > -1 { + if tests[tc.index].Outcome != tc.outcome { + t.Fatalf("expected outcome %s, got %s", tc.outcome, tests[tc.index].Outcome) + } + + if tests[tc.index].TestCase.Name != tc.testcase { + t.Fatalf("expected testcase %s got %s", tc.testcase, tests[tc.index].TestCase.Name) + } + } + + if len(tests) != tc.count { + t.Fatalf("expected length of tests to be %d; got %d", tc.count, len(tests)) + } + }) + } +}